├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── lib
├── authProvider.js
├── fetch.js
├── index.js
└── storage.js
├── package.json
└── src
├── authProvider.js
├── fetch.js
├── index.js
└── storage.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "es2015", "stage-0"],
3 | "plugins": ["transform-object-rest-spread"]
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #lib
2 | node_modules
3 | .npm
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Carlos Almeida
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Loopback Client for react-admin
2 | For using [Loopback 3](https://loopback.io/) with [react-admin](https://github.com/marmelab/react-admin).
3 |
4 | ## Installation
5 |
6 | ```bash
7 | npm install --save react-admin-loopback
8 | ```
9 |
10 | ## Prerequisite
11 |
12 | * Your loopback server must response `Content-Range` header when querying list. Please use [loopback-content-range](https://github.com/darthwesker/loopback-content-range) on your server end.
13 |
14 | ## Usage
15 |
16 | ```js
17 | // in src/App.js
18 | import React from 'react';
19 | import { Admin, Resource } from 'react-admin';
20 | import loopbackClient, { authProvider } from 'react-admin-loopback';
21 | import { List, Datagrid, TextField, NumberField } from 'react-admin';
22 |
23 | import { ShowButton, EditButton, Edit, SimpleForm, DisabledInput, TextInput, NumberInput } from 'react-admin';
24 | import { Create} from 'react-admin';
25 | import { Show, SimpleShowLayout } from 'react-admin';
26 |
27 | const BookList = (props) => (
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | export const BookShow = (props) => (
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 | export const BookEdit = (props) => (
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | export const BookCreate = (props) => (
55 |
56 |
57 |
58 |
59 |
60 |
61 | );
62 | const App = () => (
63 |
64 |
65 |
66 | );
67 |
68 | export default App;
69 | ```
70 |
71 | The dataProvider supports include:
72 |
73 | ```js
74 | // dataProvider.js
75 | import loopbackProvider from 'react-admin-loopback';
76 |
77 | const dataProvider = loopbackProvider('http://localhost:3000');
78 | export default (type, resource, params) =>
79 | new Promise(resolve =>
80 | setTimeout(() => resolve(dataProvider(type, resource, params)), 500)
81 | );
82 | ```
83 |
84 | ```js
85 |
86 | import dataProvider from './dataProvider';
87 |
88 | ...
89 |
90 | dataProvider(GET_LIST, 'books', {
91 | filter: { hide: false },
92 | sort: { field: 'id', order: 'DESC' },
93 | pagination: { page: 1, perPage: 0 },
94 | include: 'users'
95 | }).then(response => {
96 | ...
97 | });
98 |
99 | ...
100 |
101 | ```
102 |
103 | ## License
104 |
105 | This library is licensed under the [MIT Licence](LICENSE).
--------------------------------------------------------------------------------
/lib/authProvider.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.authProvider = undefined;
7 |
8 | var _reactAdmin = require('react-admin');
9 |
10 | var _storage = require('./storage');
11 |
12 | var _storage2 = _interopRequireDefault(_storage);
13 |
14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15 |
16 | function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
17 |
18 | var authProvider = function authProvider(loginApiUrl) {
19 | var noAccessPage = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '/login';
20 |
21 | return function (type, params) {
22 | if (type === _reactAdmin.AUTH_LOGIN) {
23 | var request = new Request(loginApiUrl, {
24 | method: 'POST',
25 | body: JSON.stringify(params),
26 | headers: new Headers({ 'Content-Type': 'application/json' })
27 | });
28 | return fetch(request).then(function (response) {
29 | if (response.status < 200 || response.status >= 300) {
30 | throw new Error(response.statusText);
31 | }
32 | return response.json();
33 | }).then(function (_ref) {
34 | var ttl = _ref.ttl,
35 | data = _objectWithoutProperties(_ref, ['ttl']);
36 |
37 | _storage2.default.save('lbtoken', data, ttl);
38 | });
39 | }
40 | if (type === _reactAdmin.AUTH_LOGOUT) {
41 | _storage2.default.remove('lbtoken');
42 | return Promise.resolve();
43 | }
44 | if (type === _reactAdmin.AUTH_ERROR) {
45 | var status = params.status;
46 |
47 | if (status === 401 || status === 403) {
48 | _storage2.default.remove('lbtoken');
49 | return Promise.reject();
50 | }
51 | return Promise.resolve();
52 | }
53 | if (type === _reactAdmin.AUTH_CHECK) {
54 | var token = _storage2.default.load('lbtoken');
55 | if (token && token.id) {
56 | return Promise.resolve();
57 | } else {
58 | _storage2.default.remove('lbtoken');
59 | return Promise.reject({ redirectTo: noAccessPage });
60 | }
61 | }
62 | return Promise.reject('Unknown method');
63 | };
64 | };
65 | exports.authProvider = authProvider;
--------------------------------------------------------------------------------
/lib/fetch.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
8 |
9 | var _reactAdmin = require('react-admin');
10 |
11 | var _storage = require('./storage');
12 |
13 | var _storage2 = _interopRequireDefault(_storage);
14 |
15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
16 |
17 | function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
18 |
19 | var fetchJson = function () {
20 | var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(url) {
21 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
22 | var requestHeaders, response, text, o, status, statusText, headers, body, json;
23 | return regeneratorRuntime.wrap(function _callee$(_context) {
24 | while (1) {
25 | switch (_context.prev = _context.next) {
26 | case 0:
27 | requestHeaders = options.headers || new Headers({ Accept: 'application/json' });
28 |
29 | if (!requestHeaders.has('Content-Type') && !(options && options.body && options.body instanceof FormData)) {
30 | requestHeaders.set('Content-Type', 'application/json');
31 | }
32 | if (options.user && options.user.authenticated && options.user.token) {
33 | requestHeaders.set('Authorization', options.user.token);
34 | }
35 | _context.next = 5;
36 | return fetch(url, _extends({}, options, { headers: requestHeaders }));
37 |
38 | case 5:
39 | response = _context.sent;
40 | _context.next = 8;
41 | return response.text();
42 |
43 | case 8:
44 | text = _context.sent;
45 | o = {
46 | status: response.status,
47 | statusText: response.statusText,
48 | headers: response.headers,
49 | body: text
50 | };
51 | status = o.status, statusText = o.statusText, headers = o.headers, body = o.body;
52 | json = void 0;
53 |
54 | try {
55 | json = JSON.parse(body);
56 | } catch (e) {
57 | // not json, no big deal
58 | }
59 |
60 | if (!(status < 200 || status >= 300)) {
61 | _context.next = 15;
62 | break;
63 | }
64 |
65 | return _context.abrupt('return', Promise.reject(new _reactAdmin.HttpError(json && json.error && json.error.message || statusText, status, json)));
66 |
67 | case 15:
68 | return _context.abrupt('return', Promise.resolve({ status: status, headers: headers, body: body, json: json }));
69 |
70 | case 16:
71 | case 'end':
72 | return _context.stop();
73 | }
74 | }
75 | }, _callee, undefined);
76 | }));
77 |
78 | return function fetchJson(_x) {
79 | return _ref.apply(this, arguments);
80 | };
81 | }();
82 |
83 | exports.default = function (url) {
84 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
85 |
86 | options.user = {
87 | authenticated: true,
88 | token: _storage2.default.load('lbtoken').id
89 | };
90 | return fetchJson(url, options);
91 | };
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.storage = undefined;
7 |
8 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
9 |
10 | var _authProvider = require('./authProvider');
11 |
12 | Object.keys(_authProvider).forEach(function (key) {
13 | if (key === "default" || key === "__esModule") return;
14 | Object.defineProperty(exports, key, {
15 | enumerable: true,
16 | get: function get() {
17 | return _authProvider[key];
18 | }
19 | });
20 | });
21 |
22 | var _storage = require('./storage');
23 |
24 | Object.defineProperty(exports, 'storage', {
25 | enumerable: true,
26 | get: function get() {
27 | return _interopRequireDefault(_storage).default;
28 | }
29 | });
30 |
31 | var _queryString = require('query-string');
32 |
33 | var _fetch = require('./fetch');
34 |
35 | var _fetch2 = _interopRequireDefault(_fetch);
36 |
37 | var _reactAdmin = require('react-admin');
38 |
39 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
40 |
41 | exports.default = function (apiUrl) {
42 | var httpClient = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _fetch2.default;
43 |
44 | /**
45 | * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE'
46 | * @param {String} resource Name of the resource to fetch, e.g. 'posts'
47 | * @param {Object} params The data request params, depending on the type
48 | * @returns {Object} { url, options } The HTTP request parameters
49 | */
50 | var convertDataRequestToHTTP = function convertDataRequestToHTTP(type, resource, params) {
51 | var url = '';
52 | var options = {};
53 | var specialParams = ['pagination', 'sort', 'filter'];
54 | switch (type) {
55 | case _reactAdmin.GET_LIST:
56 | {
57 | var _params$pagination = params.pagination,
58 | page = _params$pagination.page,
59 | perPage = _params$pagination.perPage;
60 | var _params$sort = params.sort,
61 | field = _params$sort.field,
62 | order = _params$sort.order;
63 |
64 | var query = {};
65 | query['where'] = _extends({}, params.filter);
66 | if (field) query['order'] = [field + ' ' + order];
67 | if (perPage >= 0) query['limit'] = perPage;
68 | if (perPage > 0 && page >= 0) query['skip'] = (page - 1) * perPage;
69 |
70 | Object.keys(params).forEach(function (key) {
71 | if (!specialParams.includes(key) && params[key] !== undefined) query[key] = params[key];
72 | });
73 | url = apiUrl + '/' + resource + '?' + (0, _queryString.stringify)({ filter: JSON.stringify(query) });
74 | break;
75 | }
76 | case _reactAdmin.GET_ONE:
77 | url = apiUrl + '/' + resource + '/' + params.id;
78 | break;
79 | case _reactAdmin.GET_MANY:
80 | {
81 | var listId = params.ids.map(function (id) {
82 | return { id: id };
83 | });
84 |
85 | var _query = '';
86 | if (listId.length > 0) {
87 | var filter = {
88 | where: { or: listId }
89 | };
90 | _query = '?' + (0, _queryString.stringify)({ filter: JSON.stringify(filter) });
91 | }
92 | url = apiUrl + '/' + resource + _query;
93 | break;
94 | }
95 | case _reactAdmin.GET_MANY_REFERENCE:
96 | {
97 | var _params$pagination2 = params.pagination,
98 | _page = _params$pagination2.page,
99 | _perPage = _params$pagination2.perPage;
100 | var _params$sort2 = params.sort,
101 | _field = _params$sort2.field,
102 | _order = _params$sort2.order;
103 |
104 | var _query2 = {};
105 | _query2['where'] = _extends({}, params.filter);
106 | _query2['where'][params.target] = params.id;
107 | if (_field) _query2['order'] = [_field + ' ' + _order];
108 | if (_perPage >= 0) _query2['limit'] = _perPage;
109 | if (_perPage > 0 && _page >= 0) _query2['skip'] = (_page - 1) * _perPage;
110 |
111 | Object.keys(params).forEach(function (key) {
112 | if (!specialParams.includes(key) && params[key] !== undefined) _query2[key] = params[key];
113 | });
114 |
115 | url = apiUrl + '/' + resource + '?' + (0, _queryString.stringify)({ filter: JSON.stringify(_query2) });
116 | break;
117 | }
118 | case _reactAdmin.UPDATE:
119 | url = apiUrl + '/' + resource + '/' + params.id;
120 | options.method = 'PATCH';
121 | options.body = JSON.stringify(params.data);
122 | break;
123 | case _reactAdmin.CREATE:
124 | url = apiUrl + '/' + resource;
125 | options.method = 'POST';
126 | options.body = JSON.stringify(params.data);
127 | break;
128 | case _reactAdmin.DELETE:
129 | url = apiUrl + '/' + resource + '/' + params.id;
130 | options.method = 'DELETE';
131 | break;
132 | default:
133 | throw new Error('Unsupported fetch action type ' + type);
134 | }
135 | return { url: url, options: options };
136 | };
137 |
138 | /**
139 | * @param {Object} response HTTP response from fetch()
140 | * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE'
141 | * @param {String} resource Name of the resource to fetch, e.g. 'posts'
142 | * @param {Object} params The data request params, depending on the type
143 | * @returns {Object} Data response
144 | */
145 | var convertHTTPResponse = function convertHTTPResponse(response, type, resource, params) {
146 | var headers = response.headers,
147 | json = response.json;
148 |
149 | switch (type) {
150 | case _reactAdmin.GET_LIST:
151 | case _reactAdmin.GET_MANY_REFERENCE:
152 | if (!headers.has('content-range')) {
153 | 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?');
154 | }
155 | return {
156 | data: json,
157 | total: parseInt(headers.get('content-range').split('/').pop(), 10)
158 | };
159 | case _reactAdmin.CREATE:
160 | return { data: _extends({}, params.data, { id: json.id }) };
161 | case _reactAdmin.DELETE:
162 | return { data: _extends({}, json, { id: params.id }) };
163 | default:
164 | return { data: json };
165 | }
166 | };
167 |
168 | /**
169 | * @param {string} type Request type, e.g GET_LIST
170 | * @param {string} resource Resource name, e.g. "posts"
171 | * @param {Object} payload Request parameters. Depends on the request type
172 | * @returns {Promise} the Promise for a data response
173 | */
174 | return function (type, resource, params) {
175 | // simple-rest doesn't handle filters on UPDATE route, so we fallback to calling UPDATE n times instead
176 | if (type === _reactAdmin.UPDATE_MANY) {
177 | return Promise.all(params.ids.map(function (id) {
178 | return httpClient(apiUrl + '/' + resource + '/' + id, {
179 | method: 'PUT',
180 | body: JSON.stringify(params.data)
181 | });
182 | })).then(function (responses) {
183 | return {
184 | data: responses.map(function (response) {
185 | return response.json;
186 | })
187 | };
188 | });
189 | }
190 | // simple-rest doesn't handle filters on DELETE route, so we fallback to calling DELETE n times instead
191 | if (type === _reactAdmin.DELETE_MANY) {
192 | return Promise.all(params.ids.map(function (id) {
193 | return httpClient(apiUrl + '/' + resource + '/' + id, {
194 | method: 'DELETE'
195 | });
196 | })).then(function (responses) {
197 | return {
198 | data: responses.map(function (response) {
199 | return response.json;
200 | })
201 | };
202 | });
203 | }
204 |
205 | var _convertDataRequestTo = convertDataRequestToHTTP(type, resource, params),
206 | url = _convertDataRequestTo.url,
207 | options = _convertDataRequestTo.options;
208 |
209 | return httpClient(url, options).then(function (response) {
210 | return convertHTTPResponse(response, type, resource, params);
211 | });
212 | };
213 | };
--------------------------------------------------------------------------------
/lib/storage.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.default = {
7 | save: function save(key, value, expirationSec) {
8 | if (typeof Storage === "undefined") {
9 | return false;
10 | }
11 | var expirationMS = expirationSec * 1000;
12 | var record = { value: value, timestamp: new Date().getTime() + expirationMS };
13 | localStorage.setItem(key, JSON.stringify(record));
14 |
15 | return value;
16 | },
17 | load: function load(key) {
18 | if (typeof Storage === "undefined") {
19 | return false;
20 | }
21 | try {
22 | var record = JSON.parse(localStorage.getItem(key));
23 | if (!record) {
24 | return false;
25 | }
26 | return new Date().getTime() < record.timestamp && record.value;
27 | } catch (e) {
28 | return false;
29 | }
30 | },
31 | remove: function remove(key) {
32 | if (typeof Storage === "undefined") {
33 | return false;
34 | }
35 | localStorage.removeItem(key);
36 | },
37 | update: function update(key, value) {
38 | if (typeof Storage === "undefined") {
39 | return false;
40 | }
41 | try {
42 | var record = JSON.parse(localStorage.getItem(key));
43 | if (!record) {
44 | return false;
45 | }
46 | var updatedRecord = { value: value, timestamp: record.timestamp };
47 | localStorage.setItem(key, JSON.stringify(updatedRecord));
48 | return updatedRecord;
49 | } catch (e) {
50 | return false;
51 | }
52 | }
53 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-admin-loopback",
3 | "version": "1.0.6",
4 | "description": "Packages related to using Loopback with react-admin",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "babel ./src -d lib --ignore '*.spec.js'"
9 | },
10 | "files": [
11 | "*.md",
12 | "lib",
13 | "src"
14 | ],
15 | "devDependencies": {
16 | "babel-cli": "^6.23.0",
17 | "babel-core": "^6.23.1",
18 | "babel-plugin-transform-object-rest-spread": "^6.23.0",
19 | "babel-preset-env": "^1.7.0",
20 | "babel-preset-es2015": "^6.24.1",
21 | "babel-preset-stage-0": "^6.24.1"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/darthwesker/react-admin-loopback.git"
26 | },
27 | "keywords": [
28 | "react",
29 | "react-admin",
30 | "loopback",
31 | "rest-client"
32 | ],
33 | "author": "Carlos Almeida",
34 | "license": "MIT",
35 | "bugs": {
36 | "url": "https://github.com/darthwesker/react-admin-loopback/issues"
37 | },
38 | "homepage": "https://github.com/darthwesker/react-admin-loopback#readme"
39 | }
40 |
--------------------------------------------------------------------------------
/src/authProvider.js:
--------------------------------------------------------------------------------
1 | import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_CHECK, AUTH_ERROR } from 'react-admin';
2 | import storage from './storage';
3 |
4 | export const authProvider = (loginApiUrl, noAccessPage = '/login') => {
5 | return (type, params) => {
6 | if (type === AUTH_LOGIN) {
7 | const request = new Request(loginApiUrl, {
8 | method: 'POST',
9 | body: JSON.stringify(params),
10 | headers: new Headers({ 'Content-Type': 'application/json' }),
11 | });
12 | return fetch(request)
13 | .then(response => {
14 | if (response.status < 200 || response.status >= 300) {
15 | throw new Error(response.statusText);
16 | }
17 | return response.json();
18 | })
19 | .then(({ ttl, ...data }) => {
20 | storage.save('lbtoken', data, ttl);
21 | });
22 | }
23 | if (type === AUTH_LOGOUT) {
24 | storage.remove('lbtoken');
25 | return Promise.resolve();
26 | }
27 | if (type === AUTH_ERROR) {
28 | const { status } = params;
29 | if (status === 401 || status === 403) {
30 | storage.remove('lbtoken');
31 | return Promise.reject();
32 | }
33 | return Promise.resolve();
34 | }
35 | if (type === AUTH_CHECK) {
36 | const token = storage.load('lbtoken');
37 | if (token && token.id) {
38 | return Promise.resolve();
39 | } else {
40 | storage.remove('lbtoken');
41 | return Promise.reject({ redirectTo: noAccessPage });
42 | }
43 | }
44 | return Promise.reject('Unknown method');
45 | };
46 | };
47 |
--------------------------------------------------------------------------------
/src/fetch.js:
--------------------------------------------------------------------------------
1 | import { HttpError } from 'react-admin';
2 | import storage from './storage';
3 |
4 | const fetchJson = async (url, options = {}) => {
5 | const requestHeaders = (options.headers || new Headers({ Accept: 'application/json' }));
6 | if (!requestHeaders.has('Content-Type') &&
7 | !(options && options.body && options.body instanceof FormData)) {
8 | requestHeaders.set('Content-Type', 'application/json');
9 | }
10 | if (options.user && options.user.authenticated && options.user.token) {
11 | requestHeaders.set('Authorization', options.user.token);
12 | }
13 | const response = await fetch(url, { ...options, headers: requestHeaders })
14 | const text = await response.text()
15 | const o = {
16 | status: response.status,
17 | statusText: response.statusText,
18 | headers: response.headers,
19 | body: text,
20 | };
21 | let status = o.status, statusText = o.statusText, headers = o.headers, body = o.body;
22 | let json;
23 | try {
24 | json = JSON.parse(body);
25 | } catch (e) {
26 | // not json, no big deal
27 | }
28 | if (status < 200 || status >= 300) {
29 | return Promise.reject(new HttpError((json && json.error && json.error.message) || statusText, status, json));
30 | }
31 | return Promise.resolve({ status: status, headers: headers, body: body, json: json });
32 | };
33 |
34 | export default (url, options = {}) => {
35 | options.user = {
36 | authenticated: true,
37 | token: storage.load('lbtoken').id
38 | };
39 | return fetchJson(url, options);
40 | }
41 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { stringify } from 'query-string';
2 | import fetchJson from './fetch';
3 |
4 | import {
5 | GET_LIST,
6 | GET_ONE,
7 | GET_MANY,
8 | GET_MANY_REFERENCE,
9 | CREATE,
10 | UPDATE,
11 | UPDATE_MANY,
12 | DELETE,
13 | DELETE_MANY,
14 | } from 'react-admin';
15 |
16 | export * from './authProvider';
17 | export { default as storage } from './storage';
18 |
19 | export default (apiUrl, httpClient = fetchJson) => {
20 | /**
21 | * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE'
22 | * @param {String} resource Name of the resource to fetch, e.g. 'posts'
23 | * @param {Object} params The data request params, depending on the type
24 | * @returns {Object} { url, options } The HTTP request parameters
25 | */
26 | const convertDataRequestToHTTP = (type, resource, params) => {
27 | let url = '';
28 | const options = {};
29 | const specialParams = ['pagination', 'sort', 'filter'];
30 | switch (type) {
31 | case GET_LIST: {
32 | const { page, perPage } = params.pagination;
33 | const { field, order } = params.sort;
34 | const query = {};
35 | query['where'] = {...params.filter};
36 | if (field) query['order'] = [field + ' ' + order];
37 | if (perPage >= 0) query['limit'] = perPage;
38 | if ((perPage > 0) && (page >= 0)) query['skip'] = (page - 1) * perPage;
39 |
40 | Object.keys(params).forEach(key => {
41 | if (!specialParams.includes(key) && params[key] !== undefined)
42 | query[key] = params[key];
43 | });
44 | url = `${apiUrl}/${resource}?${stringify({filter: JSON.stringify(query)})}`;
45 | break;
46 | }
47 | case GET_ONE:
48 | url = `${apiUrl}/${resource}/${params.id}`;
49 | break;
50 | case GET_MANY: {
51 | const listId = params.ids.map(id => {
52 | return {id};
53 | });
54 |
55 | let query = '';
56 | if (listId.length > 0) {
57 | const filter = {
58 | where: {or: listId},
59 | };
60 | query = `?${stringify({filter: JSON.stringify(filter)})}`;
61 | }
62 | url = `${apiUrl}/${resource}${query}`;
63 | break;
64 | }
65 | case GET_MANY_REFERENCE: {
66 | const { page, perPage } = params.pagination;
67 | const { field, order } = params.sort;
68 | const query = {};
69 | query['where'] = {...params.filter};
70 | query['where'][params.target] = params.id;
71 | if (field) query['order'] = [field + ' ' + order];
72 | if (perPage >= 0) query['limit'] = perPage;
73 | if ((perPage > 0) && (page >= 0)) query['skip'] = (page - 1) * perPage;
74 |
75 | Object.keys(params).forEach(key => {
76 | if (!specialParams.includes(key) && params[key] !== undefined)
77 | query[key] = params[key];
78 | });
79 |
80 | url = `${apiUrl}/${resource}?${stringify({filter: JSON.stringify(query)})}`;
81 | break;
82 | }
83 | case UPDATE:
84 | url = `${apiUrl}/${resource}/${params.id}`;
85 | options.method = 'PATCH';
86 | options.body = JSON.stringify(params.data);
87 | break;
88 | case CREATE:
89 | url = `${apiUrl}/${resource}`;
90 | options.method = 'POST';
91 | options.body = JSON.stringify(params.data);
92 | break;
93 | case DELETE:
94 | url = `${apiUrl}/${resource}/${params.id}`;
95 | options.method = 'DELETE';
96 | break;
97 | default:
98 | throw new Error(`Unsupported fetch action type ${type}`);
99 | }
100 | return { url, options };
101 | };
102 |
103 | /**
104 | * @param {Object} response HTTP response from fetch()
105 | * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE'
106 | * @param {String} resource Name of the resource to fetch, e.g. 'posts'
107 | * @param {Object} params The data request params, depending on the type
108 | * @returns {Object} Data response
109 | */
110 | const convertHTTPResponse = (response, type, resource, params) => {
111 | const { headers, json } = response;
112 | switch (type) {
113 | case GET_LIST:
114 | case GET_MANY_REFERENCE:
115 | if (!headers.has('content-range')) {
116 | throw new Error(
117 | '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?'
118 | );
119 | }
120 | return {
121 | data: json,
122 | total: parseInt(
123 | headers
124 | .get('content-range')
125 | .split('/')
126 | .pop(),
127 | 10
128 | ),
129 | };
130 | case CREATE:
131 | return { data: { ...params.data, id: json.id } };
132 | case DELETE:
133 | return { data: { ...json, id: params.id } };
134 | default:
135 | return { data: json };
136 | }
137 | };
138 |
139 | /**
140 | * @param {string} type Request type, e.g GET_LIST
141 | * @param {string} resource Resource name, e.g. "posts"
142 | * @param {Object} payload Request parameters. Depends on the request type
143 | * @returns {Promise} the Promise for a data response
144 | */
145 | return (type, resource, params) => {
146 | // simple-rest doesn't handle filters on UPDATE route, so we fallback to calling UPDATE n times instead
147 | if (type === UPDATE_MANY) {
148 | return Promise.all(
149 | params.ids.map(id =>
150 | httpClient(`${apiUrl}/${resource}/${id}`, {
151 | method: 'PUT',
152 | body: JSON.stringify(params.data),
153 | })
154 | )
155 | ).then(responses => ({
156 | data: responses.map(response => response.json),
157 | }));
158 | }
159 | // simple-rest doesn't handle filters on DELETE route, so we fallback to calling DELETE n times instead
160 | if (type === DELETE_MANY) {
161 | return Promise.all(
162 | params.ids.map(id =>
163 | httpClient(`${apiUrl}/${resource}/${id}`, {
164 | method: 'DELETE',
165 | })
166 | )
167 | ).then(responses => ({
168 | data: responses.map(response => response.json),
169 | }));
170 | }
171 |
172 | const { url, options } = convertDataRequestToHTTP(
173 | type,
174 | resource,
175 | params
176 | );
177 |
178 | return httpClient(url, options).then(response =>
179 | convertHTTPResponse(response, type, resource, params)
180 | );
181 | };
182 | };
--------------------------------------------------------------------------------
/src/storage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | save : function(key, value, expirationSec){
3 | if (typeof (Storage) === "undefined") { return false; }
4 | var expirationMS = expirationSec * 1000;
5 | var record = {value: value, timestamp: new Date().getTime() + expirationMS};
6 | localStorage.setItem(key, JSON.stringify(record));
7 |
8 | return value;
9 | },
10 | load : function(key){
11 | if (typeof (Storage) === "undefined") { return false; }
12 | try {
13 | var record = JSON.parse(localStorage.getItem(key));
14 | if (!record) {
15 | return false;
16 | }
17 | return (new Date().getTime() < record.timestamp && record.value);
18 | } catch (e) {
19 | return false;
20 | }
21 | },
22 | remove : function(key){
23 | if (typeof (Storage) === "undefined") { return false; }
24 | localStorage.removeItem(key);
25 | },
26 | update : function(key, value){
27 | if (typeof (Storage) === "undefined") { return false; }
28 | try {
29 | var record = JSON.parse(localStorage.getItem(key));
30 | if (!record) {
31 | return false;
32 | }
33 | var updatedRecord = {value: value, timestamp: record.timestamp};
34 | localStorage.setItem(key, JSON.stringify(updatedRecord));
35 | return updatedRecord;
36 | } catch (e) {
37 | return false;
38 | }
39 | },
40 | };
--------------------------------------------------------------------------------