├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── babel.config.js ├── index.js ├── package-lock.json ├── package.json ├── src ├── convertHttpResponse │ └── index.js ├── dataRequestToHttp │ ├── create │ │ ├── create.test.js │ │ └── index.js │ ├── delete │ │ ├── delete.test.js │ │ └── index.js │ ├── getList │ │ ├── getList.test.js │ │ └── index.js │ ├── getMany │ │ └── index.js │ ├── getManyReference │ │ └── index.js │ ├── getOne │ │ └── index.js │ ├── index.js │ └── update │ │ └── index.js ├── helpers │ └── transformSort.js └── index.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*.md] 7 | trim_trailing_whitespace = false 8 | 9 | [*.js] 10 | trim_trailing_whitespace = true 11 | 12 | # Unix-style newlines with a newline ending every file 13 | [*] 14 | indent_style = space 15 | indent_size = 2 16 | end_of_line = lf 17 | charset = utf-8 18 | insert_final_newline = true 19 | max_line_length = 100 20 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true, 6 | "jest/globals": true 7 | }, 8 | "plugins": ["jest"], 9 | "extends": "eslint:recommended", 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2018, 16 | "sourceType": "module" 17 | }, 18 | "rules": { 19 | "indent": ["error", 2, { "SwitchCase": 1 }], 20 | "linebreak-style": ["error", "unix"], 21 | "quotes": ["error", "double"], 22 | "semi": ["error", "never"], 23 | "comma-dangle": [ 24 | "error", 25 | { 26 | "arrays": "alwais", 27 | "objects": "alwais", 28 | "imports": "never", 29 | "exports": "never", 30 | "functions": "never" 31 | } 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/convertHttpResponse/data-provider.code-workspace 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "useTabs": false, 4 | "semi": false, 5 | "singleQuote": false, 6 | "jsxSingleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "endOfLine": "lf", 12 | "formatOnSave": "true" 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-Admin Moleculer Services data provider 2 | React-Admin Data provider to interact with Moleculer Services APIs 3 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | node: "current", 8 | }, 9 | }, 10 | ], 11 | ], 12 | } 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RancaguaInnova/moleculer-data-provider/b39579e59fa67ae789c4ddec2c7740bf5feae3b2/index.js -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moleculer-data-provider", 3 | "version": "1.0.4", 4 | "description": "React-Admin data provider to interact with Moleculer Microservices APIs", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "jest": { 10 | "transform": { 11 | ".js": "/node_modules/babel-jest" 12 | } 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/RancaguaInnova/moleculer-data-provider.git" 17 | }, 18 | "keywords": [ 19 | "microservices", 20 | "react-admin", 21 | "moleculer", 22 | "data", 23 | "provider", 24 | "javascript", 25 | "api", 26 | "client", 27 | "data provide" 28 | ], 29 | "author": "Joaquín Gumucio ", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/RancaguaInnova/moleculer-data-provider/issues" 33 | }, 34 | "homepage": "https://github.com/RancaguaInnova/moleculer-data-provider#readme", 35 | "dependencies": { 36 | "query-string": "^6.8.3" 37 | }, 38 | "peerDependencies": { 39 | "ra-core": "^2.9.7" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.6.4", 43 | "@babel/preset-env": "^7.6.3", 44 | "babel-jest": "^24.9.0", 45 | "eslint": "^6.5.1", 46 | "eslint-plugin-jest": "^22.20.0", 47 | "jest": "^24.9.0", 48 | "prettier": "^1.18.2", 49 | "prettier-standard": "^15.0.1", 50 | "standard": "^14.3.1", 51 | "ra-core": "^2.9.7" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/convertHttpResponse/index.js: -------------------------------------------------------------------------------- 1 | import { GET_LIST, GET_MANY_REFERENCE, CREATE, DELETE_MANY } from "ra-core" 2 | 3 | /** 4 | * @param {Object} response HTTP response from fetch() 5 | * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE' 6 | * @param {String} resource Name of the resource to fetch, e.g. 'posts' 7 | * @param {Object} params The data request params, depending on the type 8 | * @returns {Object} Data response 9 | */ 10 | const convertHTTPResponse = (response, type, resource, params) => { 11 | const { json } = response 12 | switch (type) { 13 | case GET_LIST: 14 | case GET_MANY_REFERENCE: 15 | return { 16 | data: json, 17 | total: json.length 18 | } 19 | case CREATE: 20 | return { data: { ...params.data, id: json.id } } 21 | case DELETE_MANY: { 22 | return { data: json || [] } 23 | } 24 | default: 25 | return { data: json } 26 | } 27 | } 28 | 29 | export default convertHTTPResponse 30 | -------------------------------------------------------------------------------- /src/dataRequestToHttp/create/create.test.js: -------------------------------------------------------------------------------- 1 | import create from "./index" 2 | 3 | describe("Makes a post request to an API to insert a Document", () => { 4 | test("Returns an object with the full API url and request options", () => { 5 | const mockCreate = jest.fn(create) 6 | const request = mockCreate({data: {firstName: "Test", lastName: "User" }}, "http://localhost:3500", "users") 7 | expect(request).toStrictEqual({ 8 | url: expect.any(String), 9 | options: expect.any(Object), 10 | }) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/dataRequestToHttp/create/index.js: -------------------------------------------------------------------------------- 1 | const create = (params, apiUrl, resource) => ({ 2 | url: `${apiUrl}/${resource}`, 3 | options: { 4 | method: "POST", 5 | body: JSON.stringify(params.data) 6 | } 7 | }) 8 | 9 | export default create 10 | -------------------------------------------------------------------------------- /src/dataRequestToHttp/delete/delete.test.js: -------------------------------------------------------------------------------- 1 | import deleteRequest from "./index" 2 | 3 | describe("Creates a DELETE request object for an API to delete a Document", () => { 4 | test("Returns an object with the full API url and request options", () => { 5 | const mockDelete = jest.fn(deleteRequest) 6 | const request = mockDelete({id: "asdfas" }, "http://localhost:3500", "users") 7 | expect(request).toStrictEqual({ 8 | url: expect.any(String), 9 | options: expect.any(Object), 10 | }) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/dataRequestToHttp/delete/index.js: -------------------------------------------------------------------------------- 1 | const deleteRequest = (params, apiUrl, resource) => ({ 2 | url: `${apiUrl}/${resource}/${params.id}`, 3 | options: { 4 | method: "DELETE" 5 | }, 6 | }) 7 | 8 | export default deleteRequest 9 | -------------------------------------------------------------------------------- /src/dataRequestToHttp/getList/getList.test.js: -------------------------------------------------------------------------------- 1 | import getList from "./index" 2 | 3 | describe("Creates a GET request object for an API to get a list of Documents", () => { 4 | test("Return an object with the full API url and request options", () => { 5 | const mockGetList = jest.fn(getList) 6 | const params = { 7 | pagination: { 8 | page: 1, 9 | perPage: 25, 10 | }, 11 | sort: { 12 | field: "firstName", 13 | order: "desc", 14 | }, 15 | filter: { email: "johndoe@email.com" } 16 | } 17 | const request = mockGetList(params, "https://services.smartrancagua.com", "users") 18 | expect(request).toMatch(/(http|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?/) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /src/dataRequestToHttp/getList/index.js: -------------------------------------------------------------------------------- 1 | import { stringify } from "query-string" 2 | import transformSort from "../../helpers/transformSort" 3 | 4 | const getList = (params, apiUrl, resource) => { 5 | const { page, perPage } = params.pagination 6 | const { field, order } = params.sort 7 | const { search } = params.filter 8 | 9 | const request = { 10 | sort: transformSort(field, order), 11 | pageSize: JSON.stringify(perPage), 12 | page: JSON.stringify(page) 13 | } 14 | 15 | let query = Object.assign({}, params.filter) 16 | 17 | if (search) { 18 | delete query.search 19 | request.search = search 20 | } 21 | 22 | request.query = JSON.stringify(query) 23 | return `${apiUrl}/${resource}?${stringify(request)}` 24 | } 25 | 26 | export default getList 27 | -------------------------------------------------------------------------------- /src/dataRequestToHttp/getMany/index.js: -------------------------------------------------------------------------------- 1 | import { stringify } from "query-string" 2 | 3 | const getMany = (params, apiUrl, resource) => { 4 | const query = { 5 | filter: JSON.stringify({ id: params.ids }) 6 | } 7 | return `${apiUrl}/${resource}?${stringify(query)}` 8 | } 9 | 10 | export default getMany 11 | -------------------------------------------------------------------------------- /src/dataRequestToHttp/getManyReference/index.js: -------------------------------------------------------------------------------- 1 | import { stringify } from "query-string" 2 | import transformSort from "../../helpers/transformSort" 3 | 4 | const getManyReference = (params, apiUrl, resource) => { 5 | const { page, perPage } = params.pagination 6 | const { field, order } = params.sort 7 | const query = { 8 | sort: transformSort(field, order), 9 | pageSize: JSON.stringify(perPage), 10 | page: JSON.stringify(page), 11 | query: JSON.stringify({ 12 | ...params.filter, 13 | [params.target]: params.id 14 | }) 15 | } 16 | return `${apiUrl}/${resource}?${stringify(query)}` 17 | } 18 | 19 | export default getManyReference 20 | -------------------------------------------------------------------------------- /src/dataRequestToHttp/getOne/index.js: -------------------------------------------------------------------------------- 1 | const getOne = (params, apiUrl, resource) => `${apiUrl}/${resource}/${params.id}` 2 | 3 | export default getOne 4 | -------------------------------------------------------------------------------- /src/dataRequestToHttp/index.js: -------------------------------------------------------------------------------- 1 | import { GET_LIST, GET_ONE, GET_MANY, GET_MANY_REFERENCE, CREATE, UPDATE, DELETE } from "ra-core" 2 | 3 | import getList from "./getList" 4 | import getOne from "./getOne" 5 | import getMany from "./getMany" 6 | import getManyReference from "./getManyReference" 7 | import update from "./update" 8 | import create from "./create" 9 | import deleteRequest from "./delete" 10 | 11 | /** 12 | * @param {string} type One of the constants appearing at the top of this file, e.g. 'UPDATE' 13 | * @param {string} resource Name of the resource to fetch, e.g. 'posts' 14 | * @param {object} params The data request params, depending on the type 15 | * @returns {object} { url, options } The HTTP request parameters 16 | */ 17 | const convertDataRequestToHTTP = (apiUrl, type, resource, params) => { 18 | let httpRequest = { 19 | url: "", 20 | options: {} 21 | } 22 | switch (type) { 23 | case GET_LIST: { 24 | httpRequest.url = getList(params, apiUrl, resource) 25 | break 26 | } 27 | case GET_ONE: 28 | httpRequest.url = getOne(params, apiUrl, resource) 29 | break 30 | case GET_MANY: { 31 | httpRequest.url = getMany(params, apiUrl, resource) 32 | break 33 | } 34 | case GET_MANY_REFERENCE: { 35 | httpRequest.url = getManyReference(params, apiUrl, resource) 36 | break 37 | } 38 | case UPDATE: 39 | httpRequest = update(params, apiUrl, resource) 40 | break 41 | case CREATE: 42 | httpRequest = create(params, apiUrl, resource) 43 | break 44 | case DELETE: 45 | httpRequest = deleteRequest(params, apiUrl, resource) 46 | break 47 | default: 48 | throw new Error(`Unsupported fetch action type ${type}`) 49 | } 50 | return httpRequest 51 | } 52 | 53 | export default convertDataRequestToHTTP 54 | -------------------------------------------------------------------------------- /src/dataRequestToHttp/update/index.js: -------------------------------------------------------------------------------- 1 | const update = (params, apiUrl, resource) => ({ 2 | url: `${apiUrl}/${resource}/${params.id}`, 3 | options: { 4 | method: "PUT", 5 | body: JSON.stringify(params.data) 6 | } 7 | }) 8 | 9 | export default update 10 | -------------------------------------------------------------------------------- /src/helpers/transformSort.js: -------------------------------------------------------------------------------- 1 | export default (field, order) => order === "ASC" ? "-".concat(field) : field 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { fetchUtils, UPDATE_MANY, DELETE_MANY } from "ra-core" 2 | 3 | import convertDataRequestToHTTP from "./dataRequestToHttp" 4 | import convertHTTPResponse from "./convertHttpResponse" 5 | 6 | /** 7 | * Maps react-admin queries to a simple REST API 8 | * 9 | * The REST dialect is similar to the one of FakeRest 10 | * @see https://github.com/marmelab/FakeRest 11 | * @example 12 | * GET_LIST => GET http://my.api.url/posts?sort=['title','ASC']&range=[0, 24] 13 | * GET_ONE => GET http://my.api.url/posts/123 14 | * GET_MANY => GET http://my.api.url/posts?filter={ids:[123,456,789]} 15 | * UPDATE => PUT http://my.api.url/posts/123 16 | * CREATE => POST http://my.api.url/posts 17 | * DELETE => DELETE http://my.api.url/posts/123 18 | */ 19 | export default (apiUrl, httpClient = fetchUtils.fetchJson) => { 20 | /** 21 | * @param {string} type Request type, e.g GET_LIST 22 | * @param {string} resource Resource name, e.g. "posts" 23 | * @param {Object} payload Request parameters. Depends on the request type 24 | * @returns {Promise} the Promise for a data response 25 | */ 26 | return async (type, resource, params) => { 27 | // simple-rest doesn't handle filters on UPDATE route, so we fallback to calling UPDATE n times instead 28 | if (type === UPDATE_MANY) { 29 | const responses = await Promise.all( 30 | params.ids.map(id => 31 | httpClient(`${apiUrl}/${resource}/${id}`, { 32 | method: "PUT", 33 | body: JSON.stringify(params.data) 34 | }) 35 | ) 36 | ) 37 | return { data: responses.map(response => response.json) } 38 | } 39 | // simple-rest doesn't handle filters on DELETE route, so we fallback to calling DELETE n times instead 40 | if (type === DELETE_MANY) { 41 | const responses = await Promise.all( 42 | params.ids.map(id => 43 | httpClient(`${apiUrl}/${resource}/${id}`, { 44 | method: "DELETE" 45 | }) 46 | ) 47 | ) 48 | return { data: responses.map(response => response.json) } 49 | } 50 | 51 | const { url, options } = convertDataRequestToHTTP(apiUrl, type, resource, params) 52 | 53 | const response = await httpClient(url, options) 54 | return convertHTTPResponse(response, type, resource, params) 55 | } 56 | } 57 | --------------------------------------------------------------------------------