From 1926ec8443e8edd5e416615f2b515bfec4b6fbba Mon Sep 17 00:00:00 2001 From: Mohammad Alavi Date: Mon, 25 Nov 2024 16:47:28 +0330 Subject: [PATCH] refactor(API-1167)!: adhere to `jsonapi` spec https://jsonapi.org/format/#fetching-sparse-fieldsets --- src/Services/Response.php | 51 ++++++++----------- tests/Unit/Services/ResponseTest.php | 76 ++++++++++++++-------------- 2 files changed, 58 insertions(+), 69 deletions(-) diff --git a/src/Services/Response.php b/src/Services/Response.php index 07c8a65d..a86a7c6e 100644 --- a/src/Services/Response.php +++ b/src/Services/Response.php @@ -24,26 +24,6 @@ public function createData(): Scope return parent::createData(); } - private function setAvailableIncludesMeta(): void - { - $this->addMeta([ - 'include' => $this->getTransformerAvailableIncludes(), - ]); - } - - private function getTransformerAvailableIncludes(): array - { - if (is_null($this->transformer) || is_callable($this->transformer)) { - return []; - } - - if (is_string($this->transformer)) { - return (new $this->transformer())->getAvailableIncludes(); - } - - return $this->transformer->getAvailableIncludes(); - } - private function defaultResourceName(): string { if (is_string($this->getResourceName())) { @@ -68,18 +48,27 @@ private function defaultResourceName(): string private function getRequestedFieldsets(): array { - $fieldSets = []; - // TODO: BREAKING CHANGE: rename the default to fieldset - if ($requestFieldSets = Request::get(Config::get('apiato.requests.params.filter', 'filter'))) { - foreach ($requestFieldSets as $fieldSet) { - [$resourceName, $fields] = explode(':', $fieldSet); - // TODO: Maybe just split by comma and remove the explode? - // Decide between the two ';', & ',' and stick with one - $field = explode(';', $fields); - $fieldSets[$resourceName] = $field; - } + // TODO: BREAKING CHANGE: rename the default to "fields" + return Request::get(Config::get('apiato.requests.params.filter', 'filter')) ?? []; + } + + private function setAvailableIncludesMeta(): void + { + $this->addMeta([ + 'include' => $this->getTransformerAvailableIncludes(), + ]); + } + + private function getTransformerAvailableIncludes(): array + { + if (is_null($this->transformer) || is_callable($this->transformer)) { + return []; } - return $fieldSets; + if (is_string($this->transformer)) { + return (new $this->transformer())->getAvailableIncludes(); + } + + return $this->transformer->getAvailableIncludes(); } } diff --git a/tests/Unit/Services/ResponseTest.php b/tests/Unit/Services/ResponseTest.php index 05bac0d5..54e48d37 100644 --- a/tests/Unit/Services/ResponseTest.php +++ b/tests/Unit/Services/ResponseTest.php @@ -17,7 +17,7 @@ #[CoversClass(Response::class)] class ResponseTest extends UnitTestCase { - private const FIELDSET_KEY = 'fieldset'; + private const FIELDSET_KEY = 'fields'; private User $user; public static function csvIncludeDataProvider(): array @@ -87,42 +87,6 @@ public static function paginatedIncludeMetaDataDataProvider(): array ]; } - public static function fieldsetDataProvider(): array - { - return [ - 'without includes' => [ - self::FIELDSET_KEY => ['User:id;email'], - 'expected' => ['data.id', 'data.email'], - 'missing' => ['data.object', 'data.name', 'data.created_at', 'data.updated_at', 'data.children', 'data.books'], - ], - 'only filter nested include keys' => [ - self::FIELDSET_KEY => ['Book:author;title'], - 'expected' => ['data.object', 'data.id', 'data.email', 'data.name', 'data.created_at', 'data.updated_at', 'data.books.data.0.author', 'data.books.data.0.title'], - 'missing' => ['data.books.data.0.id', 'data.books.data.0.created_at', 'data.books.data.0.updated_at'], - ], - 'with first level includes - no filter' => [ - self::FIELDSET_KEY => ['User:object,id;email;books'], - 'expected' => ['data.object', 'data.id', 'data.email', 'data.books.data.0.object', 'data.books.data.0.id', 'data.books.data.0.title', 'data.books.data.0.author', 'data.books.data.0.created_at', 'data.books.data.0.updated_at'], - 'missing' => ['data.name', 'data.created_at', 'data.updated_at'], - ], - 'with first level includes - filter' => [ - self::FIELDSET_KEY => ['User:object,id;email;books', 'Book:object,author'], - 'expected' => ['data.object', 'data.id', 'data.email', 'data.books.data.0.object', 'data.books.data.0.author'], - 'missing' => ['data.children', 'data.books.data.0.id', 'data.books.data.0.title', 'data.books.data.0.created_at', 'data.books.data.0.updated_at', 'data.name', 'data.created_at', 'data.updated_at'], - ], - 'with nested includes - no filter' => [ - self::FIELDSET_KEY => ['User:object,id;email,children;books'], - 'expected' => ['data.object', 'data.id', 'data.email', 'data.children.data.0.object', 'data.children.data.0.id', 'data.children.data.0.email', 'data.children.data.0.books.data.0.object', 'data.children.data.0.books.data.0.id', 'data.children.data.0.books.data.0.title', 'data.children.data.0.books.data.0.author', 'data.children.data.0.books.data.0.created_at', 'data.children.data.0.books.data.0.updated_at'], - 'missing' => ['data.name', 'data.created_at', 'data.updated_at'], - ], - 'with nested includes - filter' => [ - self::FIELDSET_KEY => ['User:id;email;children;books', 'Book:id'], - 'expected' => ['data.id', 'data.email', 'data.children.data.0.id', 'data.children.data.0.email', 'data.children.data.0.books.data.0.id'], - 'missing' => ['data.object', 'data.children.data.0.object', 'data.children.data.0.books.data.0.object', 'data.children.data.0.books.data.0.title', 'data.children.data.0.books.data.0.author', 'data.children.data.0.books.data.0.created_at', 'data.children.data.0.books.data.0.updated_at', 'data.name', 'data.created_at', 'data.updated_at'], - ], - ]; - } - public static function csvExcludeDataProvider(): array { return [ @@ -214,6 +178,42 @@ public static function invalidResourceNameProvider(): array ]; } + public static function fieldsetDataProvider(): array + { + return [ + 'without includes' => [ + self::FIELDSET_KEY => ['User' => 'id,email'], + 'expected' => ['data.id', 'data.email'], + 'missing' => ['data.object', 'data.name', 'data.created_at', 'data.updated_at', 'data.children', 'data.books'], + ], + 'only filter nested include keys' => [ + self::FIELDSET_KEY => ['Book' => 'author,title'], + 'expected' => ['data.object', 'data.id', 'data.email', 'data.name', 'data.created_at', 'data.updated_at', 'data.books.data.0.author', 'data.books.data.0.title'], + 'missing' => ['data.books.data.0.id', 'data.books.data.0.created_at', 'data.books.data.0.updated_at'], + ], + 'with first level includes - no filter' => [ + self::FIELDSET_KEY => ['User' => 'object,id,email,books'], + 'expected' => ['data.object', 'data.id', 'data.email', 'data.books.data.0.object', 'data.books.data.0.id', 'data.books.data.0.title', 'data.books.data.0.author', 'data.books.data.0.created_at', 'data.books.data.0.updated_at'], + 'missing' => ['data.name', 'data.created_at', 'data.updated_at'], + ], + 'with first level includes - filter' => [ + self::FIELDSET_KEY => ['User' => 'object,id,email,books', 'Book' => 'object,author'], + 'expected' => ['data.object', 'data.id', 'data.email', 'data.books.data.0.object', 'data.books.data.0.author'], + 'missing' => ['data.children', 'data.books.data.0.id', 'data.books.data.0.title', 'data.books.data.0.created_at', 'data.books.data.0.updated_at', 'data.name', 'data.created_at', 'data.updated_at'], + ], + 'with nested includes - no filter' => [ + self::FIELDSET_KEY => ['User' => 'object,id,email,children,books'], + 'expected' => ['data.object', 'data.id', 'data.email', 'data.children.data.0.object', 'data.children.data.0.id', 'data.children.data.0.email', 'data.children.data.0.books.data.0.object', 'data.children.data.0.books.data.0.id', 'data.children.data.0.books.data.0.title', 'data.children.data.0.books.data.0.author', 'data.children.data.0.books.data.0.created_at', 'data.children.data.0.books.data.0.updated_at'], + 'missing' => ['data.name', 'data.created_at', 'data.updated_at'], + ], + 'with nested includes - filter' => [ + self::FIELDSET_KEY => ['User' => 'id,email,children,books', 'Book' => 'id'], + 'expected' => ['data.id', 'data.email', 'data.children.data.0.id', 'data.children.data.0.email', 'data.children.data.0.books.data.0.id'], + 'missing' => ['data.object', 'data.children.data.0.object', 'data.children.data.0.books.data.0.object', 'data.children.data.0.books.data.0.title', 'data.children.data.0.books.data.0.author', 'data.children.data.0.books.data.0.created_at', 'data.children.data.0.books.data.0.updated_at', 'data.name', 'data.created_at', 'data.updated_at'], + ], + ]; + } + #[DataProvider('csvIncludeDataProvider')] public function testSingleResourceCanHandleCSVInclude($include, $expected): void { @@ -317,7 +317,7 @@ public function testPaginatedResourceMetaDataAndExclude($exclude): void #[DataProvider('validResourceNameProvider')] public function testCanOverrideMainResourceName($resourceName): void { - request()->merge(['include' => 'books,children.books', self::FIELDSET_KEY => ["{$resourceName}:id", 'Book:author,title']]); + request()->merge(['include' => 'books,children.books', self::FIELDSET_KEY => [$resourceName => 'id', 'Book' => 'author,title']]); $response = Response::createFrom($this->user); $response->transformWith(UserTransformer::class); $response->withResourceName($resourceName);