Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

simpleRestProvider: using a custom http header to count items in a collection #5224

Merged
merged 3 commits into from
Sep 7, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/ra-data-simple-rest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,40 @@ const httpClient = (url, options = {}) => {

Now all the requests to the REST API will contain the `Authorization: SRTRDFVESGNJYTUKTYTHRG` header.

## Note about Content-Range

Historically, Simple REST Data Provider uses the http `Content-Range` header to retrieve the number of items in a collection. But this is a *hack* of the [primary role of this header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range).

And this can be problematic, for example within an infrastructure using a Varnish that may use, modify or delete this header. We also have feedback indicating that using this header is problematic when you host your application on [Versel](https://vercel.com/).
alexisjanvier marked this conversation as resolved.
Show resolved Hide resolved

The solution is to use another http header to return the number of collection's items, the other header commonly used for this is `X-Total-Count`. So if you use `X-Total-Count`, you will have to :
alexisjanvier marked this conversation as resolved.
Show resolved Hide resolved

* Whitelist this header with an `Access-Control-Expose-Headers` [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) header.

```
Access-Control-Expose-Headers: X-Total-Count
```

* Use the third parameter of `simpleRestProvider` to specify the name of the header to use :

```jsx
// in src/App.js
import * as React from "react";
import { Admin, Resource } from 'react-admin';
import { fetchUtils } from 'ra-core';
import simpleRestProvider from 'ra-data-simple-rest';

import { PostList } from './posts';

const App = () => (
<Admin dataProvider={simpleRestProvider('http://path.to.my.api/', fetchUtils.fetchJson, 'X-Total-Count')}>
<Resource name="posts" list={PostList} />
</Admin>
);

export default App;
```

## License

This data provider is licensed under the MIT License, and sponsored by [marmelab](http://marmelab.com).
32 changes: 32 additions & 0 deletions packages/ra-data-simple-rest/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,37 @@ describe('Data Simple REST Client', () => {
}
);
});

it('should use a custom http header to retrieve the number of items in the collection', async () => {
const httpClient = jest.fn(() =>
Promise.resolve({
headers: new Headers({
'x-total-count': '42',
}),
json: [{ id: 1 }],
status: 200,
body: '',
})
);
const client = simpleClient(
'http://localhost:3000',
httpClient,
'X-Total-Count'
);

const result = await client.getList('posts', {
filter: {},
pagination: {
page: 1,
perPage: 10,
},
sort: {
field: 'title',
order: 'desc',
},
});

expect(result.total).toEqual(42);
});
});
});
79 changes: 53 additions & 26 deletions packages/ra-data-simple-rest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ import { fetchUtils, DataProvider } from 'ra-core';
*
* export default App;
*/
export default (apiUrl, httpClient = fetchUtils.fetchJson): DataProvider => ({
export default (
apiUrl: string,
httpClient = fetchUtils.fetchJson,
countHeader: string = 'Content-Range'
): DataProvider => ({
getList: (resource, params) => {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
Expand All @@ -47,27 +51,34 @@ export default (apiUrl, httpClient = fetchUtils.fetchJson): DataProvider => ({
filter: JSON.stringify(params.filter),
};
const url = `${apiUrl}/${resource}?${stringify(query)}`;
const options =
countHeader === 'Content-Range'
? {
// Chrome doesn't return `Content-Range` header if no `Range` is provided in the request.
headers: new Headers({
Range: `${resource}=${rangeStart}-${rangeEnd}`,
}),
}
: {};

return httpClient(url, {
// Chrome doesn't return `Content-Range` header if no `Range` is provided in the request.
headers: new Headers({
Range: `${resource}=${rangeStart}-${rangeEnd}`,
}),
}).then(({ headers, json }) => {
if (!headers.has('content-range')) {
return httpClient(url, options).then(({ headers, json }) => {
if (!headers.has(countHeader)) {
throw new Error(
'The Content-Range header is missing in the HTTP Response. The simple REST data provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare Content-Range in the Access-Control-Expose-Headers header?'
`The ${countHeader} header is missing in the HTTP Response. The simple REST data provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare ${countHeader} in the Access-Control-Expose-Headers header?`
);
}
return {
data: json,
total: parseInt(
headers
.get('content-range')
.split('/')
.pop(),
10
),
total:
countHeader === 'Content-Range'
? parseInt(
headers
.get('content-range')
.split('/')
.pop(),
10
)
: parseInt(headers.get(countHeader.toLowerCase())),
};
});
},
Expand All @@ -88,6 +99,10 @@ export default (apiUrl, httpClient = fetchUtils.fetchJson): DataProvider => ({
getManyReference: (resource, params) => {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;

const rangeStart = (page - 1) * perPage;
const rangeEnd = page * perPage - 1;

const query = {
sort: JSON.stringify([field, order]),
range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]),
Expand All @@ -97,22 +112,34 @@ export default (apiUrl, httpClient = fetchUtils.fetchJson): DataProvider => ({
}),
};
const url = `${apiUrl}/${resource}?${stringify(query)}`;
const options =
countHeader === 'Content-Range'
? {
// Chrome doesn't return `Content-Range` header if no `Range` is provided in the request.
headers: new Headers({
Range: `${resource}=${rangeStart}-${rangeEnd}`,
}),
}
: {};

return httpClient(url).then(({ headers, json }) => {
if (!headers.has('content-range')) {
return httpClient(url, options).then(({ headers, json }) => {
if (!headers.has(countHeader)) {
throw new Error(
'The Content-Range header is missing in the HTTP Response. The simple REST data provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare Content-Range in the Access-Control-Expose-Headers header?'
`The ${countHeader} header is missing in the HTTP Response. The simple REST data provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare ${countHeader} in the Access-Control-Expose-Headers header?`
);
}
return {
data: json,
total: parseInt(
headers
.get('content-range')
.split('/')
.pop(),
10
),
total:
countHeader === 'Content-Range'
? parseInt(
headers
.get('content-range')
.split('/')
.pop(),
10
)
: parseInt(headers.get(countHeader.toLowerCase())),
};
});
},
Expand Down