Skip to content

Commit

Permalink
draft fix : hydra
Browse files Browse the repository at this point in the history
  • Loading branch information
Valentin Dassonville committed Oct 7, 2024
1 parent f3331e9 commit a677a52
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 41 deletions.
12 changes: 6 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 `<Admin>` sources and Storybook;
- `php`: holding the API sources.

Expand All @@ -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
Expand All @@ -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

Expand Down
18 changes: 12 additions & 6 deletions api/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^3.2",
"api-platform/core": "dev-fix/hydra",
"doctrine/doctrine-bundle": "^2.7",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^3.0",
Expand All @@ -32,10 +32,10 @@
"twig/twig": "^2.12|^3.0"
},
"require-dev": {
"symfony/debug-bundle": "6.4.*",
"symfony/stopwatch": "6.4.*",
"symfony/var-dumper": "6.4.*",
"symfony/web-profiler-bundle": "6.4.*"
"symfony/debug-bundle": "^6.4",
"symfony/stopwatch": "^6.4",
"symfony/var-dumper": "^6.4",
"symfony/web-profiler-bundle": "^6.4"
},
"config": {
"optimize-autoloader": true,
Expand Down Expand Up @@ -91,5 +91,11 @@
"require": "6.4.*",
"docker": false
}
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/valentin-dassonville/core.git"
}
]
}
4 changes: 0 additions & 4 deletions api/config/packages/api_platform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 1 addition & 3 deletions src/dataProvider/adminDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
57 changes: 57 additions & 0 deletions src/hydra/dataProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]);
});
});
50 changes: 36 additions & 14 deletions src/hydra/dataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import type {
DataProviderType,
HydraCollection,
HydraDataProviderFactoryParams,
HydraHttpClientResponse,
HydraHttpClientResponse, HydraView,

Check failure on line 38 in src/hydra/dataProvider.ts

View workflow job for this annotation

GitHub Actions / Continuous integration

Insert `⏎·`
MercureOptions,
SearchParams,
} from '../types.js';
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -599,7 +621,7 @@ function dataProvider(
data,
};
});

}
case DELETE:
return Promise.resolve({ data: { id: (params as DeleteParams).id } });

Expand Down
17 changes: 9 additions & 8 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,18 @@ 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': 'PartialCollectionView';
'@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 {
Expand Down

0 comments on commit a677a52

Please sign in to comment.