├── .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 | }; --------------------------------------------------------------------------------