diff --git a/src/lib/ColumnToCode.php b/src/lib/ColumnToCode.php index f4da8079..19de9052 100644 --- a/src/lib/ColumnToCode.php +++ b/src/lib/ColumnToCode.php @@ -220,25 +220,18 @@ public function getCode(bool $quoted = false):string array_unshift($parts, '$this'); return implode('->', array_filter(array_map('trim', $parts))); } - if ($this->rawParts['default'] === null) { - $default = ''; - } elseif (ApiGenerator::isPostgres() && $this->isEnum()) { - $default = - $this->rawParts['default'] !== null ? ' DEFAULT ' . trim($this->rawParts['default']) : ''; - } else { - $default = $this->rawParts['default'] !== null ? ' DEFAULT ' . trim($this->rawParts['default']) : ''; + + if (ApiGenerator::isPostgres() && $this->alterByXDbType) { + return $quoted ? VarDumper::export($this->rawParts['type']) : $this->rawParts['type']; } + $default = $this->rawParts['default'] !== null ? ' DEFAULT ' . trim($this->rawParts['default']) : ''; $code = $this->rawParts['type'] . ' ' . $this->rawParts['nullable'] . $default; if ((ApiGenerator::isMysql() || ApiGenerator::isMariaDb())) { $code .= $this->rawParts['position'] ? ' ' . $this->rawParts['position'] : ''; $code .= $this->rawParts['comment'] ? ' '.$this->rawParts['comment'] : ''; } - - if (ApiGenerator::isPostgres() && $this->alterByXDbType) { - return $quoted ? VarDumper::export($this->rawParts['type']) : $this->rawParts['type']; - } return $quoted ? VarDumper::export($code) : $code; } @@ -407,8 +400,6 @@ private function getIsBuiltinType($type, $dbType) private function resolveEnumType():void { if (ApiGenerator::isPostgres()) { - // $rawTableName = $this->dbSchema->getRawTableName($this->tableAlias); - // $this->rawParts['type'] = '"enum_'.$rawTableName.'_' . $this->column->name.'"'; $this->rawParts['type'] = '"'.$this->column->dbType.'"'; return; } diff --git a/src/lib/items/FractalAction.php b/src/lib/items/FractalAction.php index 20b7bb32..d95d7d6d 100644 --- a/src/lib/items/FractalAction.php +++ b/src/lib/items/FractalAction.php @@ -32,6 +32,8 @@ */ final class FractalAction extends BaseObject { + use OptionsRoutesTrait; + /**@var string* */ public $id; @@ -102,16 +104,6 @@ public function getRoute():string return $this->controllerId.'/'.$this->id; } - public function getOptionsRoute():string - { - //@TODO: re-check - if ($this->prefix && !empty($this->prefixSettings)) { - $prefix = $this->prefixSettings['module'] ?? $this->prefix; - return trim($prefix, '/').'/'.$this->controllerId.'/options'; - } - return $this->controllerId.'/options'; - } - public function getBaseModelName():string { return $this->modelFqn ? StringHelper::basename($this->modelFqn) : ''; diff --git a/src/lib/items/OptionsRoutesTrait.php b/src/lib/items/OptionsRoutesTrait.php new file mode 100644 index 00000000..df9e6714 --- /dev/null +++ b/src/lib/items/OptionsRoutesTrait.php @@ -0,0 +1,62 @@ + and contributors + * @license https://github.com/cebe/yii2-openapi/blob/master/LICENSE + */ + +namespace cebe\yii2openapi\lib\items; + +trait OptionsRoutesTrait +{ + public function getOptionsRoute():string + { + if ($this->prefix && !empty($this->prefixSettings)) { + if (isset($this->prefixSettings['module'])) { + $prefix = $this->prefixSettings['module']; + return static::finalOptionsRoute($prefix, $this->controllerId); + } elseif (isset($this->prefixSettings['namespace']) && str_contains($this->prefixSettings['namespace'], '\modules\\')) { # if `module` not present then check in namespace and then in path + $prefix = static::computeModule('\\', $this->prefixSettings['namespace']); + if ($prefix) { + return static::finalOptionsRoute($prefix, $this->controllerId); + } + } elseif (isset($this->prefixSettings['path']) && str_contains($this->prefixSettings['path'], '/modules/')) { + $prefix = static::computeModule('/', $this->prefixSettings['path']); + if ($prefix) { + return static::finalOptionsRoute($prefix, $this->controllerId); + } + } + } + return $this->controllerId.'/options'; + } + + /** + * @param string $separator + * @param string $entity path or namespace + * @return void + */ + public static function computeModule(string $separator, string $entity): ?string + { + $parts = explode($separator . 'modules' . $separator, $entity); # /app/modules/forum/controllers => /forum/controllers + if (empty($parts[1])) { + return null; + } + if (str_contains($parts[1], 'controller')) { + $result = explode($separator . 'controller', $parts[1]); // compute everything in between "modules" and "controllers" e.g. api/v1 + $result = array_map(function ($val) { + return str_replace('\\', '/', $val); + }, $result); + } else { + $result = explode($separator, $parts[1]); # forum/controllers => forum + } + if (empty($result[0])) { + return null; + } + return $result[0]; + } + + public static function finalOptionsRoute(string $prefix, string $controllerId): string + { + return trim($prefix, '/') . '/' . $controllerId . '/options'; + } +} diff --git a/src/lib/items/RestAction.php b/src/lib/items/RestAction.php index 342bc0d2..c1f8da90 100644 --- a/src/lib/items/RestAction.php +++ b/src/lib/items/RestAction.php @@ -31,6 +31,8 @@ */ final class RestAction extends BaseObject { + use OptionsRoutesTrait; + /**@var string* */ public $id; @@ -96,16 +98,6 @@ public function getRoute():string return $this->controllerId . '/' . $this->id; } - public function getOptionsRoute():string - { - //@TODO: re-check - if ($this->prefix && !empty($this->prefixSettings)) { - $prefix = $this->prefixSettings['module'] ?? $this->prefix; - return trim($prefix, '/').'/'.$this->controllerId.'/options'; - } - return $this->controllerId.'/options'; - } - public function getBaseModelName():string { return $this->modelFqn ? StringHelper::basename($this->modelFqn) : ''; diff --git a/src/lib/migrations/MysqlMigrationBuilder.php b/src/lib/migrations/MysqlMigrationBuilder.php index da3d967d..8d6b4339 100644 --- a/src/lib/migrations/MysqlMigrationBuilder.php +++ b/src/lib/migrations/MysqlMigrationBuilder.php @@ -34,9 +34,6 @@ protected function buildColumnChanges(ColumnSchema $current, ColumnSchema $desir foreach ($changed as $attr) { $newColumn->$attr = $desired->$attr; } - if (static::isEnum($newColumn)) { - $newColumn->dbType = 'enum'; // TODO this is concretely not correct - } $this->migration->addUpCode($this->recordBuilder->alterColumn($this->model->getTableAlias(), $newColumn, $positionDesired)) ->addDownCode($this->recordBuilder->alterColumn($this->model->getTableAlias(), $current, $positionCurrent)); } @@ -131,13 +128,12 @@ protected function findTableIndexes():array public static function getColumnSchemaBuilderClass(): string { - if (ApiGenerator::isMysql()) { - return \yii\db\mysql\ColumnSchemaBuilder::class; - } elseif (ApiGenerator::isMariaDb()) { + if (ApiGenerator::isMariaDb()) { return \SamIT\Yii2\MariaDb\ColumnSchemaBuilder::class; } else { throw new \Exception('Unknown database'); } + return \yii\db\mysql\ColumnSchemaBuilder::class; } public function modifyCurrent(ColumnSchema $current): void diff --git a/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/index.php b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/index.php new file mode 100644 index 00000000..f4e7c94b --- /dev/null +++ b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/index.php @@ -0,0 +1,21 @@ + '@specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/index.yaml', + 'generateUrls' => true, + 'generateModels' => false, + 'excludeModels' => [ + 'Error', + ], + 'generateControllers' => true, + 'generateMigrations' => false, + 'useJsonApi' => false, + 'urlPrefixes' => [ + 'animals' => '', + '/info' => ['module' => 'petinfo', 'namespace' => '\app\modules\petinfo\controllers'], + '/forum' => ['namespace' => '\app\modules\forum\controllers'], # namespace contains "\modules\" + '/forum2' => ['path' => '@app/modules/forum2/controllers', 'namespace' => '\app\forum2\controllers'], # path contains "/modules/" + '/api/v1' => ['path' => '@app/modules/some/controllers', 'namespace' => '\app\api\v1\controllers'], # namespace contains "\modules\"; module will be "api/v1" + '/api/v2' => ['path' => '@app/modules/api/v2/controllers', 'namespace' => '\app\some\controllers'], # namespace contains "\modules\"; module will be "api/v2" + ] +]; diff --git a/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/index.yaml b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/index.yaml new file mode 100644 index 00000000..5dd945dd --- /dev/null +++ b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/index.yaml @@ -0,0 +1,177 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /api/v1/pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /animals/pets/{id}: + parameters: + - name: id + in: path + required: true + description: The id of the pet to update + schema: + type: string + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + patch: + summary: update a specific pet + operationId: updatePetById + tags: + - pets + responses: + '200': + description: The updated pet + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + delete: + summary: delete a specific pet + operationId: deletePetById + tags: + - pets + responses: + '204': + description: successfully deleted pet + /petComments: + get: + description: list all pet comments + responses: + '200': + description: list of comments + /info/pet-details: + get: + description: list all pet details + responses: + '200': + description: list of details + /forum/pet2-details: + get: + description: list all pet details + responses: + '200': + description: list of details + /forum2/pet3-details: + get: + description: list all pet details + responses: + '200': + description: list of details + /api/v2/comments: + get: + description: list all pet details + responses: + '200': + description: list of details + +components: + schemas: + Pet: + description: A Pet + required: + - id + - name + properties: + id: + type: integer + format: int64 + readOnly: True + name: + type: string + store: + $ref: '#/components/schemas/Store' + tag: + type: string + x-faker: "$faker->randomElement(['one', 'two', 'three', 'four'])" + Store: + description: A store's description + required: + - id + - name + properties: + id: + type: integer + format: int64 + readOnly: True + name: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/config/urls.rest.php b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/config/urls.rest.php new file mode 100644 index 00000000..fb1998ac --- /dev/null +++ b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/config/urls.rest.php @@ -0,0 +1,25 @@ + 'api/v1/pet/list', + 'POST api/v1/pets' => 'api/v1/pet/create', + 'GET animals/pets/' => 'pet/view', + 'DELETE animals/pets/' => 'pet/delete', + 'PATCH animals/pets/' => 'pet/update', + 'GET petComments' => 'pet-comment/list', + 'GET info/pet-details' => 'petinfo/pet-detail/list', + 'GET forum/pet2-details' => 'forum/pet2-detail/list', + 'GET forum2/pet3-details' => 'forum2/pet3-detail/list', + 'GET api/v2/comments' => 'api/v2/comment/list', + 'api/v1/pets' => 'some/pet/options', + 'animals/pets/' => 'pet/options', + 'petComments' => 'pet-comment/options', + 'info/pet-details' => 'petinfo/pet-detail/options', + 'forum/pet2-details' => 'forum/pet2-detail/options', + 'forum2/pet3-details' => 'forum2/pet3-detail/options', + 'api/v2/comments' => 'api/v2/comment/options', +]; diff --git a/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/controllers/PetCommentController.php b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/controllers/PetCommentController.php new file mode 100644 index 00000000..b71a5cb0 --- /dev/null +++ b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/controllers/PetCommentController.php @@ -0,0 +1,20 @@ + [ + 'class' => \yii\rest\OptionsAction::class, + ], + ]; + } + + /** + * Checks the privilege of the current user. + * + * This method checks whether the current user has the privilege + * to run the specified action against the specified data model. + * If the user does not have access, a [[ForbiddenHttpException]] should be thrown. + * + * @param string $action the ID of the action to be executed + * @param object $model the model to be accessed. If null, it means no specific model is being accessed. + * @param array $params additional parameters + * @throws \yii\web\ForbiddenHttpException if the user does not have access + */ + abstract public function checkAccess($action, $model = null, $params = []); + + abstract public function actionList(); + +} diff --git a/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/modules/api/v2/controllers/CommentController.php b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/modules/api/v2/controllers/CommentController.php new file mode 100644 index 00000000..55b0bc54 --- /dev/null +++ b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/modules/api/v2/controllers/CommentController.php @@ -0,0 +1,20 @@ + [ + 'class' => \yii\rest\OptionsAction::class, + ], + ]; + } + + /** + * Checks the privilege of the current user. + * + * This method checks whether the current user has the privilege + * to run the specified action against the specified data model. + * If the user does not have access, a [[ForbiddenHttpException]] should be thrown. + * + * @param string $action the ID of the action to be executed + * @param object $model the model to be accessed. If null, it means no specific model is being accessed. + * @param array $params additional parameters + * @throws \yii\web\ForbiddenHttpException if the user does not have access + */ + abstract public function checkAccess($action, $model = null, $params = []); + + abstract public function actionList(); + +} diff --git a/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/modules/forum/controllers/Pet2DetailController.php b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/modules/forum/controllers/Pet2DetailController.php new file mode 100644 index 00000000..550fa36d --- /dev/null +++ b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/modules/forum/controllers/Pet2DetailController.php @@ -0,0 +1,20 @@ + [ + 'class' => \yii\rest\OptionsAction::class, + ], + ]; + } + + /** + * Checks the privilege of the current user. + * + * This method checks whether the current user has the privilege + * to run the specified action against the specified data model. + * If the user does not have access, a [[ForbiddenHttpException]] should be thrown. + * + * @param string $action the ID of the action to be executed + * @param object $model the model to be accessed. If null, it means no specific model is being accessed. + * @param array $params additional parameters + * @throws \yii\web\ForbiddenHttpException if the user does not have access + */ + abstract public function checkAccess($action, $model = null, $params = []); + + abstract public function actionList(); + +} diff --git a/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/modules/forum2/controllers/Pet3DetailController.php b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/modules/forum2/controllers/Pet3DetailController.php new file mode 100644 index 00000000..9fbebaf9 --- /dev/null +++ b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/modules/forum2/controllers/Pet3DetailController.php @@ -0,0 +1,20 @@ + [ + 'class' => \yii\rest\OptionsAction::class, + ], + ]; + } + + /** + * Checks the privilege of the current user. + * + * This method checks whether the current user has the privilege + * to run the specified action against the specified data model. + * If the user does not have access, a [[ForbiddenHttpException]] should be thrown. + * + * @param string $action the ID of the action to be executed + * @param object $model the model to be accessed. If null, it means no specific model is being accessed. + * @param array $params additional parameters + * @throws \yii\web\ForbiddenHttpException if the user does not have access + */ + abstract public function checkAccess($action, $model = null, $params = []); + + abstract public function actionList(); + +} diff --git a/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/modules/petinfo/controllers/PetDetailController.php b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/modules/petinfo/controllers/PetDetailController.php new file mode 100644 index 00000000..8f574d21 --- /dev/null +++ b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/modules/petinfo/controllers/PetDetailController.php @@ -0,0 +1,20 @@ + [ + 'class' => \yii\rest\OptionsAction::class, + ], + ]; + } + + /** + * Checks the privilege of the current user. + * + * This method checks whether the current user has the privilege + * to run the specified action against the specified data model. + * If the user does not have access, a [[ForbiddenHttpException]] should be thrown. + * + * @param string $action the ID of the action to be executed + * @param object $model the model to be accessed. If null, it means no specific model is being accessed. + * @param array $params additional parameters + * @throws \yii\web\ForbiddenHttpException if the user does not have access + */ + abstract public function checkAccess($action, $model = null, $params = []); + + abstract public function actionList(); + +} diff --git a/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/modules/some/controllers/PetController.php b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/modules/some/controllers/PetController.php new file mode 100644 index 00000000..729402bc --- /dev/null +++ b/tests/specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql/modules/some/controllers/PetController.php @@ -0,0 +1,15 @@ + [ + 'class' => \yii\rest\IndexAction::class, + 'modelClass' => \app\models\Pet::class, + 'checkAccess' => [$this, 'checkAccess'], + ], + 'create' => [ + 'class' => \yii\rest\CreateAction::class, + 'modelClass' => \app\models\Pet::class, + 'checkAccess' => [$this, 'checkAccess'], + ], + 'view' => [ + 'class' => \yii\rest\ViewAction::class, + 'modelClass' => \app\models\Pet::class, + 'checkAccess' => [$this, 'checkAccess'], + ], + 'delete' => [ + 'class' => \yii\rest\DeleteAction::class, + 'modelClass' => \app\models\Pet::class, + 'checkAccess' => [$this, 'checkAccess'], + ], + 'update' => [ + 'class' => \yii\rest\UpdateAction::class, + 'modelClass' => \app\models\Pet::class, + 'checkAccess' => [$this, 'checkAccess'], + ], + 'options' => [ + 'class' => \yii\rest\OptionsAction::class, + ], + ]; + } + + /** + * Checks the privilege of the current user. + * + * This method checks whether the current user has the privilege + * to run the specified action against the specified data model. + * If the user does not have access, a [[ForbiddenHttpException]] should be thrown. + * + * @param string $action the ID of the action to be executed + * @param object $model the model to be accessed. If null, it means no specific model is being accessed. + * @param array $params additional parameters + * @throws \yii\web\ForbiddenHttpException if the user does not have access + */ + abstract public function checkAccess($action, $model = null, $params = []); + +} diff --git a/tests/unit/EnumTest.php b/tests/unit/EnumTest.php index 8b48e249..79797d38 100644 --- a/tests/unit/EnumTest.php +++ b/tests/unit/EnumTest.php @@ -168,7 +168,6 @@ public function testChangeToAndFromEnum() // edit enum to string and vice versa $this->deleteTables(); } - // TODO ENH enum change is more work than just changing the eunm values. And for PgSQL it is even more // public function testEnumValuesChange() // { // $this->deleteTables(); @@ -201,7 +200,6 @@ public function testChangeToAndFromEnum() // edit enum to string and vice versa // public function testChangeEnumValues() // { - // // TODO // // add a value to list // // fix a typo in a enum value present in existing list // // remove a value from list diff --git a/tests/unit/IssueFixTest.php b/tests/unit/IssueFixTest.php index 324658c0..5578c695 100644 --- a/tests/unit/IssueFixTest.php +++ b/tests/unit/IssueFixTest.php @@ -886,6 +886,36 @@ public function test25GenerateInverseRelations() $this->checkFiles($actualFiles, $expectedFiles); } + // https://github.com/php-openapi/yii2-openapi/issues/35 + public function test35ResolveTodoReCheckOptionsRouteInFractalAction() + { + $testFile = Yii::getAlias("@specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/index.php"); + $this->runGenerator($testFile); + $actualFiles = FileHelper::findFiles(Yii::getAlias('@app'), [ + 'recursive' => true, + ]); + $expectedFiles = FileHelper::findFiles(Yii::getAlias("@specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql"), [ + 'recursive' => true, + ]); + $this->checkFiles($actualFiles, $expectedFiles); + } + + // https://github.com/php-openapi/yii2-openapi/issues/35 + public function test35ResolveTodoReCheckOptionsRouteInRestAction() + { + $testFile = Yii::getAlias("@specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/index.php"); + $content = str_replace("'useJsonApi' => true,", "'useJsonApi' => false,", file_get_contents($testFile)); + file_put_contents($testFile, $content); + $this->runGenerator($testFile); + $actualFiles = FileHelper::findFiles(Yii::getAlias('@app'), [ + 'recursive' => true, + ]); + $expectedFiles = FileHelper::findFiles(Yii::getAlias("@specs/issue_fix/35_resolve_todo_re_check_options_route_in_fractal_action/mysql"), [ + 'recursive' => true, + ]); + $this->checkFiles($actualFiles, $expectedFiles); + } + // https://github.com/php-openapi/yii2-openapi/issues/63 public function test63JustColumnNameRename() {