From c0879d2ee233477a37710dcbb7628950ff4bad03 Mon Sep 17 00:00:00 2001 From: valentin-dassonville <129871973+valentin-dassonville@users.noreply.github.com> Date: Tue, 22 Oct 2024 18:16:18 +0200 Subject: [PATCH] fix(hydra) : change hydra prefix (hardcoded values) (#586) Co-authored-by: Valentin Dassonville --- CONTRIBUTING.md | 12 +++--- api/composer.json | 2 +- api/config/packages/api_platform.yaml | 4 -- package.json | 4 +- src/dataProvider/adminDataProvider.ts | 4 +- src/hydra/dataProvider.test.ts | 57 +++++++++++++++++++++++++++ src/hydra/dataProvider.ts | 50 ++++++++++++++++------- src/types.ts | 16 ++++---- 8 files changed, 111 insertions(+), 38 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd4ad4e3..857402de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,7 +36,7 @@ If you already have a project in progress, you can develop directly from it. The instructions below explain how to install the source version of API Platform Admin in your project and contribute a patch. -Your client should already use `@api-platform/admin` and its bootstrap file (usually: `src/App.tsx`) should at least contains: +Your client should already use `@api-platform/admin` and its bootstrap file (usually: `src/App.tsx`) should at least contains: ```tsx import React from 'react'; @@ -97,9 +97,9 @@ yarn dev --force #### Running Admin Through Storybook -If you do not have an existing project, you can use [Storybook](https://storybook.js.org/) to visualize changes in the source code, and test them. +If you do not have an existing project, you can use [Storybook](https://storybook.js.org/) to visualize changes in the source code, and test them. -This development stack consists of two Docker containers: +This development stack consists of two Docker containers: - `pwa`: containing the `` sources and Storybook; - `php`: holding the API sources. @@ -119,7 +119,7 @@ Now you can go to http://localhost:3000/ to see the Storybook instance in action To run a command directly inside a container, run: ```shell -# Run a command in the php container +# Run a command in the php container docker compose exec -T php your-command # Run a command in the pwa container @@ -137,9 +137,9 @@ yarn test yarn test-storybook --url http://127.0.0.1:3000/ ``` -If you add a new feature, don't forget to add tests for it. +If you add a new feature, don't forget to add tests for it. - Functionnal tests are written with [Jest](https://jestjs.io/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/); -- End-to-end tests are written with [Storybook play funcitons](https://storybook.js.org/docs/writing-stories/play-function/). +- End-to-end tests are written with [Storybook play functions](https://storybook.js.org/docs/writing-stories/play-function/). ### Matching Coding Standards diff --git a/api/composer.json b/api/composer.json index ed2cc65d..edb39cd7 100644 --- a/api/composer.json +++ b/api/composer.json @@ -5,7 +5,7 @@ "php": ">=8.2", "ext-ctype": "*", "ext-iconv": "*", - "api-platform/core": "^3.2", + "api-platform/core": "^4.0.4", "doctrine/doctrine-bundle": "^2.7", "doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/orm": "^3.0", diff --git a/api/config/packages/api_platform.yaml b/api/config/packages/api_platform.yaml index cb40c62d..84703a21 100644 --- a/api/config/packages/api_platform.yaml +++ b/api/config/packages/api_platform.yaml @@ -16,7 +16,3 @@ api_platform: cache_headers: vary: ['Content-Type', 'Authorization', 'Origin'] extra_properties: - standard_put: true - rfc_7807_compliant_errors: true - event_listeners_backward_compatibility_layer: false - keep_legacy_inflector: false diff --git a/package.json b/package.json index 72349c36..a136923d 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "license": "MIT", "sideEffects": false, "dependencies": { - "@api-platform/api-doc-parser": "^0.16.2", - "jsonld": "^8.1.0", + "@api-platform/api-doc-parser": "^0.16.4", + "jsonld": "^8.3.2", "lodash.isplainobject": "^4.0.6", "react-admin": "^5.0.3" }, diff --git a/src/dataProvider/adminDataProvider.ts b/src/dataProvider/adminDataProvider.ts index e425203a..868550e0 100644 --- a/src/dataProvider/adminDataProvider.ts +++ b/src/dataProvider/adminDataProvider.ts @@ -29,9 +29,7 @@ export default ( introspect: (_resource = '', _params = {}) => apiSchema ? Promise.resolve({ data: apiSchema }) - : apiDocumentationParser(docEntrypointUrl.toString(), { - headers: { accept: 'application/ld+json' }, - }) + : apiDocumentationParser(docEntrypointUrl.toString()) .then(({ api }: ApiDocumentationParserResponse) => { if (api.resources && api.resources.length > 0) { apiSchema = { ...api, resources: api.resources }; diff --git a/src/hydra/dataProvider.test.ts b/src/hydra/dataProvider.test.ts index d081010a..bbaa61d0 100644 --- a/src/hydra/dataProvider.test.ts +++ b/src/hydra/dataProvider.test.ts @@ -645,4 +645,61 @@ describe('Transform a React Admin request to an Hydra request', () => { 'http://localhost/entrypoint/comments?order%5Btext%5D=DESC&order%5Bid%5D=DESC&page=1&itemsPerPage=30', ); }); + + test('React Admin get list without hydra prefix', async () => { + mockFetchHydra.mockClear(); + mockFetchHydra.mockReturnValue( + Promise.resolve({ + status: 200, + headers: new Headers(), + json: { member: [], totalItems: 3 }, + }), + ); + await dataProvider.current.getList('resource', { + pagination: { + page: 1, + perPage: 30, + }, + sort: { + order: 'ASC', + field: '', + }, + filter: { + simple: 'foo', + nested: { param: 'bar' }, + sub_nested: { sub: { param: true } }, + array: ['/iri/1', '/iri/2'], + nested_array: { nested: ['/nested_iri/1', '/nested_iri/2'] }, + exists: { foo: true }, + nested_date: { date: { before: '2000' } }, + nested_range: { range: { between: '12.99..15.99' } }, + }, + searchParams: { pagination: 'true' }, + }); + const searchParams = Array.from( + mockFetchHydra.mock.calls?.[0]?.[0]?.searchParams.entries() ?? [], + ); + expect(searchParams[0]).toEqual(['pagination', 'true']); + expect(searchParams[1]).toEqual(['page', '1']); + expect(searchParams[2]).toEqual(['itemsPerPage', '30']); + expect(searchParams[3]).toEqual(['simple', 'foo']); + expect(searchParams[4]).toEqual(['nested.param', 'bar']); + expect(searchParams[5]).toEqual(['sub_nested.sub.param', 'true']); + expect(searchParams[6]).toEqual(['array[0]', '/iri/1']); + expect(searchParams[7]).toEqual(['array[1]', '/iri/2']); + expect(searchParams[8]).toEqual([ + 'nested_array.nested[0]', + '/nested_iri/1', + ]); + expect(searchParams[9]).toEqual([ + 'nested_array.nested[1]', + '/nested_iri/2', + ]); + expect(searchParams[10]).toEqual(['exists[foo]', 'true']); + expect(searchParams[11]).toEqual(['nested_date.date[before]', '2000']); + expect(searchParams[12]).toEqual([ + 'nested_range.range[between]', + '12.99..15.99', + ]); + }); }); diff --git a/src/hydra/dataProvider.ts b/src/hydra/dataProvider.ts index 48009285..f5f65089 100644 --- a/src/hydra/dataProvider.ts +++ b/src/hydra/dataProvider.ts @@ -35,7 +35,7 @@ import type { DataProviderType, HydraCollection, HydraDataProviderFactoryParams, - HydraHttpClientResponse, + HydraHttpClientResponse, HydraView, MercureOptions, SearchParams, } from '../types.js'; @@ -160,6 +160,16 @@ const defaultParams: Required< disableCache: false, }; +function normalizeHydraKey(json: JsonLdObj, key: string): JsonLdObj { + if (json[`hydra:${key}`]) { + const copy = JSON.parse(JSON.stringify(json)); + copy[key] = copy[`hydra:${key}`]; + delete copy[`hydra:${key}`]; + return copy; + } + return json; +} + /** * Maps react-admin queries to a Hydra powered REST API * @@ -545,22 +555,22 @@ function dataProvider( switch (type) { case GET_LIST: - case GET_MANY_REFERENCE: + case GET_MANY_REFERENCE: { if (!response.json) { return Promise.reject( new Error(`An empty response was received for "${type}".`), ); } - if (!('hydra:member' in response.json)) { + const json = normalizeHydraKey(response.json, 'member'); + if (!json.member) { return Promise.reject( - new Error(`Response doesn't have a "hydra:member" field.`), + new Error("Response doesn't have a member field."), ); } // TODO: support other prefixes than "hydra:" - // eslint-disable-next-line no-case-declarations - const hydraCollection = response.json as HydraCollection; + let hydraCollection = json as HydraCollection; return Promise.resolve( - hydraCollection['hydra:member'].map((document) => + hydraCollection.member.map((document: JsonLdObj) => transformJsonLdDocumentToReactAdminDocument( document, true, @@ -577,17 +587,29 @@ function dataProvider( ), ) .then((data) => { - if (hydraCollection['hydra:totalItems'] !== undefined) { + hydraCollection = normalizeHydraKey( + hydraCollection, + 'totalItems', + ) as HydraCollection; + if (hydraCollection.totalItems !== undefined) { return { data, - total: hydraCollection['hydra:totalItems'], + total: hydraCollection.totalItems, }; } - if (hydraCollection['hydra:view']) { + hydraCollection = normalizeHydraKey( + hydraCollection, + 'view', + ) as HydraCollection; + if (hydraCollection.view) { + let hydraView = normalizeHydraKey( + hydraCollection.view, + 'next', + ) as HydraView; + hydraView = normalizeHydraKey(hydraView, 'previous') as HydraView; const pageInfo = { - hasNextPage: !!hydraCollection['hydra:view']['hydra:next'], - hasPreviousPage: - !!hydraCollection['hydra:view']['hydra:previous'], + hasNextPage: !!hydraView.next, + hasPreviousPage: !!hydraView.previous, }; return { data, @@ -599,7 +621,7 @@ function dataProvider( data, }; }); - + } case DELETE: return Promise.resolve({ data: { id: (params as DeleteParams).id } }); diff --git a/src/types.ts b/src/types.ts index 9f9deb49..211352ac 100644 --- a/src/types.ts +++ b/src/types.ts @@ -78,17 +78,17 @@ export type DataTransformer = (parsedData: any) => ApiPlatformAdminRecord; export type Hydra = JsonLdObj | HydraCollection; export interface HydraView extends JsonLdObj { - '@type': 'hydra:PartialCollectionView'; - 'hydra:first': string; - 'hydra:last': string; - 'hydra:next': string; - 'hydra:previous': string; + '@type': string; + first: string; + last: string; + next: string; + previous: string; } export interface HydraCollection extends JsonLdObj { - 'hydra:member': JsonLdObj[]; - 'hydra:totalItems'?: number; - 'hydra:view'?: HydraView; + member: JsonLdObj[]; + totalItems?: number; + view?: HydraView; } export interface HttpClientOptions {