diff --git a/api.php b/api.php index 25ab94a..43e3489 100644 --- a/api.php +++ b/api.php @@ -3879,23 +3879,43 @@ namespace Tqdev\PhpCrudApi\Column\Reflection { $columns[$column->getName()] = $column; } // set primary key - $columnNames = $reflection->getTablePrimaryKeys($name); - if (count($columnNames) == 1) { - $columnName = $columnNames[0]; - if (isset($columns[$columnName])) { - $pk = $columns[$columnName]; - $pk->setPk(true); + $columnName = false; + if ($type == 'view') { + $columnName = 'id'; + } else { + $columnNames = $reflection->getTablePrimaryKeys($name); + if (count($columnNames) == 1) { + $columnName = $columnNames[0]; } } + if ($columnName && isset($columns[$columnName])) { + $pk = $columns[$columnName]; + $pk->setPk(true); + } // set foreign keys - $fks = $reflection->getTableForeignKeys($name); - foreach ($fks as $columnName => $table) { - $columns[$columnName]->setFk($table); + if ($type == 'view') { + $tables = $reflection->getTables(); + foreach ($columns as $columnName => $column) { + if (substr($columnName, -3) == '_id') { + foreach ($tables as $table) { + $tableName = $table['TABLE_NAME']; + $suffix = $tableName . '_id'; + if (substr($columnName, -1 * strlen($suffix)) == $suffix) { + $column->setFk($tableName); + } + } + } + } + } else { + $fks = $reflection->getTableForeignKeys($name); + foreach ($fks as $columnName => $table) { + $columns[$columnName]->setFk($table); + } } return new ReflectedTable($name, $type, array_values($columns)); } - public static function fromJson(/* object */$json): ReflectedTable + public static function fromJson( /* object */$json): ReflectedTable { $name = $json->name; $type = isset($json->type) ? $json->type : 'table'; @@ -4590,9 +4610,6 @@ namespace Tqdev\PhpCrudApi\Controller { if (!$this->service->hasTable($table)) { return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table); } - if ($this->service->getType($table) != 'table') { - return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__); - } $id = RequestUtils::getPathSegment($request, 3); $params = RequestUtils::getParams($request); if (strpos($id, ',') !== false) { @@ -6994,7 +7011,9 @@ namespace Tqdev\PhpCrudApi\Middleware\Router { return substr($fullPath, 0, -1 * strlen($path)); } } - return $fullPath; + if ('/' . basename(__FILE__) == $fullPath) { + return $fullPath; + } } return '/'; } @@ -9045,9 +9064,9 @@ namespace Tqdev\PhpCrudApi\OpenApi { namespace Tqdev\PhpCrudApi\OpenApi { use Tqdev\PhpCrudApi\Column\ReflectionService; + use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn; use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore; use Tqdev\PhpCrudApi\OpenApi\OpenApiDefinition; - use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn; class OpenApiRecordsBuilder { @@ -9065,16 +9084,16 @@ namespace Tqdev\PhpCrudApi\OpenApi { 'integer' => ['type' => 'integer', 'format' => 'int32'], 'bigint' => ['type' => 'integer', 'format' => 'int64'], 'varchar' => ['type' => 'string'], - 'clob' => ['type' => 'string', 'format' => 'large-string'], //custom format + 'clob' => ['type' => 'string', 'format' => 'large-string'], //custom format 'varbinary' => ['type' => 'string', 'format' => 'byte'], - 'blob' => ['type' => 'string', 'format' => 'large-byte'], //custom format - 'decimal' => ['type' => 'string', 'format' => 'decimal'], //custom format + 'blob' => ['type' => 'string', 'format' => 'large-byte'], //custom format + 'decimal' => ['type' => 'string', 'format' => 'decimal'], //custom format 'float' => ['type' => 'number', 'format' => 'float'], 'double' => ['type' => 'number', 'format' => 'double'], 'date' => ['type' => 'string', 'format' => 'date'], - 'time' => ['type' => 'string', 'format' => 'time'], //custom format + 'time' => ['type' => 'string', 'format' => 'time'], //custom format 'timestamp' => ['type' => 'string', 'format' => 'date-time'], - 'geometry' => ['type' => 'string', 'format' => 'geometry'], //custom format + 'geometry' => ['type' => 'string', 'format' => 'geometry'], //custom format 'boolean' => ['type' => 'boolean'], ]; @@ -9264,7 +9283,10 @@ namespace Tqdev\PhpCrudApi\OpenApi { if (!$pkName && $operation != 'list') { continue; } - if ($type != 'table' && $operation != 'list') { + if ($type == 'view' && !in_array($operation, array('read', 'list'))) { + continue; + } + if ($type == 'view' && !$pkName && $operation == 'read') { continue; } if ($operation == 'delete') { @@ -11170,6 +11192,8 @@ namespace Tqdev\PhpCrudApi { class ResponseFactory { const OK = 200; + const MOVED_PERMANENTLY = 301; + const FOUND = 302; const UNAUTHORIZED = 401; const FORBIDDEN = 403; const NOT_FOUND = 404; @@ -11185,8 +11209,7 @@ namespace Tqdev\PhpCrudApi { public static function fromCsv(int $status, string $csv): ResponseInterface { - $response = self::from($status, 'text/csv', $csv); - return $response->withHeader('Content-Type', 'text/csv'); + return self::from($status, 'text/csv', $csv); } public static function fromHtml(int $status, string $html): ResponseInterface @@ -11200,7 +11223,7 @@ namespace Tqdev\PhpCrudApi { return self::from($status, 'application/json', $content); } - private static function from(int $status, string $contentType, string $content): ResponseInterface + public static function from(int $status, string $contentType, string $content): ResponseInterface { $psr17Factory = new Psr17Factory(); $response = $psr17Factory->createResponse($status); diff --git a/tests/fixtures/blog_mysql.sql b/tests/fixtures/blog_mysql.sql index 81eca36..3caf94d 100644 --- a/tests/fixtures/blog_mysql.sql +++ b/tests/fixtures/blog_mysql.sql @@ -130,7 +130,7 @@ INSERT INTO `events` (`name`, `datetime`, `visitors`) VALUES ('Launch', '2016-01-01 13:01:01', 0); DROP VIEW IF EXISTS `tag_usage`; -CREATE VIEW `tag_usage` AS select `name`, count(`name`) AS `count` from `tags`, `post_tags` where `tags`.`id` = `post_tags`.`tag_id` group by `name` order by `count` desc, `name`; +CREATE VIEW `tag_usage` AS select `tags`.`id` as `id`, `name`, count(`name`) AS `count` from `tags`, `post_tags` where `tags`.`id` = `post_tags`.`tag_id` group by `tags`.`id`, `name` order by `count` desc, `name`; DROP TABLE IF EXISTS `products`; CREATE TABLE `products` ( diff --git a/tests/fixtures/blog_pgsql.sql b/tests/fixtures/blog_pgsql.sql index 54d8c69..1c8e48c 100644 --- a/tests/fixtures/blog_pgsql.sql +++ b/tests/fixtures/blog_pgsql.sql @@ -127,7 +127,7 @@ CREATE TABLE events ( -- Name: tag_usage; Type: VIEW; Schema: public; Owner: postgres; Tablespace: -- -CREATE VIEW "tag_usage" AS select "name", count("name") AS "count" from "tags", "post_tags" where "tags"."id" = "post_tags"."tag_id" group by "name" order by "count" desc, "name"; +CREATE VIEW "tag_usage" AS select "tags"."id" as "id", "name", count("name") AS "count" from "tags", "post_tags" where "tags"."id" = "post_tags"."tag_id" group by "tags"."id", "name" order by "count" desc, "name"; -- -- Name: products; Type: TABLE; Schema: public; Owner: postgres; Tablespace: diff --git a/tests/fixtures/blog_sqlite.sql b/tests/fixtures/blog_sqlite.sql index 87abeef..4099451 100644 --- a/tests/fixtures/blog_sqlite.sql +++ b/tests/fixtures/blog_sqlite.sql @@ -115,7 +115,7 @@ CREATE TABLE "events" ( INSERT INTO "events" ("id", "name", "datetime", "visitors") VALUES (1, 'Launch', '2016-01-01 13:01:01', 0); DROP VIEW IF EXISTS "tag_usage"; -CREATE VIEW "tag_usage" AS select "name", count("name") AS "count" from "tags", "post_tags" where "tags"."id" = "post_tags"."tag_id" group by "name" order by "count" desc, "name"; +CREATE VIEW "tag_usage" AS select "tags"."id" as "id", "name", count("name") AS "count" from "tags", "post_tags" where "tags"."id" = "post_tags"."tag_id" group by "tags"."id", "name" order by "count" desc, "name"; DROP TABLE IF EXISTS "products"; CREATE TABLE "products" ( diff --git a/tests/fixtures/blog_sqlsrv.sql b/tests/fixtures/blog_sqlsrv.sql index 882bb35..3c95e60 100644 --- a/tests/fixtures/blog_sqlsrv.sql +++ b/tests/fixtures/blog_sqlsrv.sql @@ -246,7 +246,7 @@ GO CREATE VIEW [tag_usage] AS -SELECT top 100 PERCENT name, COUNT_BIG(name) AS [count] FROM tags, post_tags WHERE tags.id = post_tags.tag_id GROUP BY name ORDER BY [count] DESC, name +SELECT top 100 PERCENT tags.id as id, name, COUNT_BIG(name) AS [count] FROM tags, post_tags WHERE tags.id = post_tags.tag_id GROUP BY tags.id, name ORDER BY [count] DESC, name GO DROP SEQUENCE IF EXISTS [products_id_seq] diff --git a/tests/functional/001_records/075_list_tag_usage.log b/tests/functional/001_records/075_list_tag_usage.log index 2becc08..a965ed3 100644 --- a/tests/functional/001_records/075_list_tag_usage.log +++ b/tests/functional/001_records/075_list_tag_usage.log @@ -3,14 +3,22 @@ GET /records/tag_usage === 200 Content-Type: application/json; charset=utf-8 -Content-Length: 71 +Content-Length: 85 -{"records":[{"name":"funny","count":2},{"name":"important","count":2}]} +{"records":[{"id":1,"name":"funny","count":2},{"id":2,"name":"important","count":2}]} === GET /records/tag_usage/1 === +200 +Content-Type: application/json; charset=utf-8 +Content-Length: 33 + +{"id":1,"name":"funny","count":2} +=== +DELETE /records/tag_usage/1 +=== 405 Content-Type: application/json; charset=utf-8 -Content-Length: 56 +Content-Length: 58 -{"code":1015,"message":"Operation 'read' not supported"} +{"code":1015,"message":"Operation 'delete' not supported"} diff --git a/tests/functional/003_columns/001_get_database.log b/tests/functional/003_columns/001_get_database.log index 90c38cd..97ca1f5 100644 --- a/tests/functional/003_columns/001_get_database.log +++ b/tests/functional/003_columns/001_get_database.log @@ -4,6 +4,6 @@ GET /columns === 200 Content-Type: application/json; charset=utf-8 -Content-Length: 2799 +Content-Length: 2840 -{"tables":[{"name":"barcodes","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"product_id","type":"integer","fk":"products"},{"name":"hex","type":"varchar","length":255},{"name":"bin","type":"blob"},{"name":"ip_address","type":"varchar","length":15,"nullable":true}]},{"name":"categories","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"icon","type":"blob","nullable":true}]},{"name":"comments","type":"table","columns":[{"name":"id","type":"bigint","pk":true},{"name":"post_id","type":"integer","fk":"posts"},{"name":"message","type":"varchar","length":255},{"name":"category_id","type":"integer","fk":"categories"}]},{"name":"countries","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"shape","type":"geometry"}]},{"name":"events","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"datetime","type":"timestamp","nullable":true},{"name":"visitors","type":"bigint","nullable":true}]},{"name":"kunsthåndværk","type":"table","columns":[{"name":"id","type":"varchar","length":36,"pk":true},{"name":"Umlauts ä_ö_ü-COUNT","type":"integer"},{"name":"user_id","type":"integer","fk":"users"},{"name":"invisible_id","type":"varchar","length":36,"nullable":true,"fk":"invisibles"}]},{"name":"nopk","type":"table","columns":[{"name":"id","type":"varchar","length":36}]},{"name":"post_tags","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"post_id","type":"integer","fk":"posts"},{"name":"tag_id","type":"integer","fk":"tags"}]},{"name":"posts","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"user_id","type":"integer","fk":"users"},{"name":"category_id","type":"integer","fk":"categories"},{"name":"content","type":"varchar","length":255}]},{"name":"products","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"price","type":"decimal","precision":10,"scale":2},{"name":"properties","type":"clob"},{"name":"created_at","type":"timestamp"},{"name":"deleted_at","type":"timestamp","nullable":true}]},{"name":"tag_usage","type":"view","columns":[{"name":"name","type":"varchar","length":255},{"name":"count","type":"bigint"}]},{"name":"tags","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"is_important","type":"boolean"}]},{"name":"users","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"username","type":"varchar","length":255},{"name":"password","type":"varchar","length":255},{"name":"location","type":"geometry","nullable":true}]}]} +{"tables":[{"name":"barcodes","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"product_id","type":"integer","fk":"products"},{"name":"hex","type":"varchar","length":255},{"name":"bin","type":"blob"},{"name":"ip_address","type":"varchar","length":15,"nullable":true}]},{"name":"categories","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"icon","type":"blob","nullable":true}]},{"name":"comments","type":"table","columns":[{"name":"id","type":"bigint","pk":true},{"name":"post_id","type":"integer","fk":"posts"},{"name":"message","type":"varchar","length":255},{"name":"category_id","type":"integer","fk":"categories"}]},{"name":"countries","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"shape","type":"geometry"}]},{"name":"events","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"datetime","type":"timestamp","nullable":true},{"name":"visitors","type":"bigint","nullable":true}]},{"name":"kunsthåndværk","type":"table","columns":[{"name":"id","type":"varchar","length":36,"pk":true},{"name":"Umlauts ä_ö_ü-COUNT","type":"integer"},{"name":"user_id","type":"integer","fk":"users"},{"name":"invisible_id","type":"varchar","length":36,"nullable":true,"fk":"invisibles"}]},{"name":"nopk","type":"table","columns":[{"name":"id","type":"varchar","length":36}]},{"name":"post_tags","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"post_id","type":"integer","fk":"posts"},{"name":"tag_id","type":"integer","fk":"tags"}]},{"name":"posts","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"user_id","type":"integer","fk":"users"},{"name":"category_id","type":"integer","fk":"categories"},{"name":"content","type":"varchar","length":255}]},{"name":"products","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"price","type":"decimal","precision":10,"scale":2},{"name":"properties","type":"clob"},{"name":"created_at","type":"timestamp"},{"name":"deleted_at","type":"timestamp","nullable":true}]},{"name":"tag_usage","type":"view","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"count","type":"bigint"}]},{"name":"tags","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"is_important","type":"boolean"}]},{"name":"users","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"username","type":"varchar","length":255},{"name":"password","type":"varchar","length":255},{"name":"location","type":"geometry","nullable":true}]}]}