├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── buildGqlQuery.js ├── buildQuery.js ├── buildVariables.js ├── fetchActions.js ├── getFinalType.js ├── getResponseParser.js ├── index.js ├── isList.js └── isRequired.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | *.org 4 | 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | webpack.config.js 3 | .prettierrc 4 | *.org 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Radcliffe Robinson 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 | # Migration Note 2 | 3 | This repository is now depreciated and the has been migrated to the Hausra organization. 4 | 5 | Going forward, the React Admin Hasura data provider repository is maintained here: 6 | 7 | [https://github.com/hasura/ra-data-hasura](https://github.com/hasura/ra-data-hasura) 8 | 9 | A big thanks to Steams and all the contributors to this library. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ra-data-hasura-graphql", 3 | "version": "0.1.12", 4 | "description": "A data provider for connecting react-admin to a Hasura endpoint", 5 | "main": "dist/index.js", 6 | "sideEffects": false, 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/steams/ra-data-hasura-graphql.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/steams/ra-data-hasura-graphql/issues" 13 | }, 14 | "homepage": "https://github.com/steams/ra-data-hasura-graphql#readme", 15 | "authors": [ 16 | "Radcliffe Robinson" 17 | ], 18 | "keywords": [ 19 | "reactjs", 20 | "react", 21 | "react-admin", 22 | "admin-on-rest", 23 | "rest", 24 | "graphql", 25 | "hasura" 26 | ], 27 | "license": "MIT", 28 | "scripts": { 29 | "build": "webpack --mode production", 30 | "prettier": "prettier --config ./.prettierrc --write '**/*.{js,jsx,md}'" 31 | }, 32 | "dependencies": { 33 | "graphql-ast-types-browser": "~1.0.2", 34 | "kind-of": ">=6.0.3", 35 | "lodash": "~4.17.5", 36 | "minimist": ">=1.2.3", 37 | "ra-data-graphql": "^3.6.1" 38 | }, 39 | "peerDependencies": { 40 | "graphql": "^14.1.1", 41 | "ra-core": "^3.0.0" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.7.7", 45 | "babel-loader": "^8.0.6", 46 | "connected-react-router": "^6.8.0", 47 | "final-form": "^4.18.7", 48 | "final-form-arrays": "^3.0.2", 49 | "graphql": "^14.5.8", 50 | "husky": "^4.3.0", 51 | "jsonexport": "^2.4.1", 52 | "prettier": "^2.1.2", 53 | "pretty-quick": "^3.1.0", 54 | "ra-core": "^3.0.0", 55 | "react": "^16.13.1", 56 | "react-dom": "^16.13.1", 57 | "react-final-form": "^6.3.5", 58 | "react-redux": "^7.2.0", 59 | "react-router-dom": "^5.1.2", 60 | "redux-saga": "^1.1.3", 61 | "webpack": "^4.41.4", 62 | "webpack-cli": "^3.3.10" 63 | }, 64 | "husky": { 65 | "hooks": { 66 | "pre-commit": "pretty-quick --staged" 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/buildGqlQuery.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_LIST, 3 | GET_MANY, 4 | GET_MANY_REFERENCE, 5 | DELETE, 6 | CREATE, 7 | UPDATE, 8 | UPDATE_MANY, 9 | DELETE_MANY, 10 | } from './fetchActions'; 11 | 12 | import { TypeKind } from 'graphql'; 13 | import * as gqlTypes from 'graphql-ast-types-browser'; 14 | 15 | import getFinalType from './getFinalType'; 16 | import isList from './isList'; 17 | import isRequired from './isRequired'; 18 | 19 | export const buildFragments = (introspectionResults) => (possibleTypes) => 20 | possibleTypes.reduce((acc, possibleType) => { 21 | const type = getFinalType(possibleType); 22 | 23 | const linkedType = introspectionResults.types.find( 24 | (t) => t.name === type.name 25 | ); 26 | 27 | return [ 28 | ...acc, 29 | gqlTypes.inlineFragment( 30 | gqlTypes.selectionSet(buildFields(linkedType)), 31 | gqlTypes.namedType(gqlTypes.name(type.name)) 32 | ), 33 | ]; 34 | }, []); 35 | 36 | export const buildFields = (type) => 37 | type.fields.reduce((acc, field) => { 38 | const type = getFinalType(field.type); 39 | 40 | if (type.name.startsWith('_')) { 41 | return acc; 42 | } 43 | 44 | if (type.kind !== TypeKind.OBJECT && type.kind !== TypeKind.INTERFACE) { 45 | return [...acc, gqlTypes.field(gqlTypes.name(field.name))]; 46 | } 47 | 48 | return acc; 49 | }, []); 50 | 51 | export const getArgType = (arg) => { 52 | const type = getFinalType(arg.type); 53 | const required = isRequired(arg.type); 54 | const list = isList(arg.type); 55 | 56 | if (required) { 57 | if (list) { 58 | return gqlTypes.nonNullType( 59 | gqlTypes.listType( 60 | gqlTypes.nonNullType(gqlTypes.namedType(gqlTypes.name(type.name))) 61 | ) 62 | ); 63 | } 64 | 65 | return gqlTypes.nonNullType(gqlTypes.namedType(gqlTypes.name(type.name))); 66 | } 67 | 68 | if (list) { 69 | return gqlTypes.listType(gqlTypes.namedType(gqlTypes.name(type.name))); 70 | } 71 | 72 | return gqlTypes.namedType(gqlTypes.name(type.name)); 73 | }; 74 | 75 | export const buildArgs = (query, variables) => { 76 | if (query.args.length === 0) { 77 | return []; 78 | } 79 | 80 | const validVariables = Object.keys(variables).filter( 81 | (k) => typeof variables[k] !== 'undefined' 82 | ); 83 | 84 | let args = query.args 85 | .filter((a) => validVariables.includes(a.name)) 86 | .reduce( 87 | (acc, arg) => [ 88 | ...acc, 89 | gqlTypes.argument( 90 | gqlTypes.name(arg.name), 91 | gqlTypes.variable(gqlTypes.name(arg.name)) 92 | ), 93 | ], 94 | [] 95 | ); 96 | 97 | return args; 98 | }; 99 | 100 | export const buildMetaArgs = (query, variables, aorFetchType) => { 101 | if (query.args.length === 0) { 102 | return []; 103 | } 104 | 105 | const validVariables = Object.keys(variables).filter((k) => { 106 | if ( 107 | aorFetchType === GET_LIST || 108 | aorFetchType === GET_MANY || 109 | aorFetchType === GET_MANY_REFERENCE 110 | ) { 111 | return ( 112 | typeof variables[k] !== 'undefined' && k !== 'limit' && k !== 'offset' 113 | ); 114 | } 115 | 116 | return typeof variables[k] !== 'undefined'; 117 | }); 118 | 119 | let args = query.args 120 | .filter((a) => validVariables.includes(a.name)) 121 | .reduce( 122 | (acc, arg) => [ 123 | ...acc, 124 | gqlTypes.argument( 125 | gqlTypes.name(arg.name), 126 | gqlTypes.variable(gqlTypes.name(arg.name)) 127 | ), 128 | ], 129 | [] 130 | ); 131 | 132 | return args; 133 | }; 134 | 135 | export const buildApolloArgs = (query, variables) => { 136 | if (query.args.length === 0) { 137 | return []; 138 | } 139 | 140 | const validVariables = Object.keys(variables).filter( 141 | (k) => typeof variables[k] !== 'undefined' 142 | ); 143 | 144 | let args = query.args 145 | .filter((a) => validVariables.includes(a.name)) 146 | .reduce((acc, arg) => { 147 | return [ 148 | ...acc, 149 | gqlTypes.variableDefinition( 150 | gqlTypes.variable(gqlTypes.name(arg.name)), 151 | getArgType(arg) 152 | ), 153 | ]; 154 | }, []); 155 | 156 | return args; 157 | }; 158 | 159 | export const buildGqlQuery = ( 160 | introspectionResults, 161 | buildFields, 162 | buildMetaArgs, 163 | buildArgs, 164 | buildApolloArgs 165 | ) => (resource, aorFetchType, queryType, variables) => { 166 | const { sortField, sortOrder, ...metaVariables } = variables; 167 | const apolloArgs = buildApolloArgs(queryType, variables); 168 | const args = buildArgs(queryType, variables); 169 | const metaArgs = buildMetaArgs(queryType, metaVariables, aorFetchType); 170 | const fields = buildFields(resource.type, aorFetchType); 171 | if ( 172 | aorFetchType === GET_LIST || 173 | aorFetchType === GET_MANY || 174 | aorFetchType === GET_MANY_REFERENCE 175 | ) { 176 | return gqlTypes.document([ 177 | gqlTypes.operationDefinition( 178 | 'query', 179 | gqlTypes.selectionSet([ 180 | gqlTypes.field( 181 | gqlTypes.name(queryType.name), 182 | gqlTypes.name('items'), 183 | args, 184 | null, 185 | gqlTypes.selectionSet(fields) 186 | ), 187 | gqlTypes.field( 188 | gqlTypes.name(`${queryType.name}_aggregate`), 189 | gqlTypes.name('total'), 190 | metaArgs, 191 | null, 192 | gqlTypes.selectionSet([ 193 | gqlTypes.field( 194 | gqlTypes.name('aggregate'), 195 | null, 196 | null, 197 | null, 198 | gqlTypes.selectionSet([gqlTypes.field(gqlTypes.name('count'))]) 199 | ), 200 | ]) 201 | ), 202 | ]), 203 | gqlTypes.name(queryType.name), 204 | apolloArgs 205 | ), 206 | ]); 207 | } 208 | 209 | if ( 210 | aorFetchType === CREATE || 211 | aorFetchType === UPDATE || 212 | aorFetchType === UPDATE_MANY || 213 | aorFetchType === DELETE || 214 | aorFetchType === DELETE_MANY 215 | ) { 216 | return gqlTypes.document([ 217 | gqlTypes.operationDefinition( 218 | 'mutation', 219 | gqlTypes.selectionSet([ 220 | gqlTypes.field( 221 | gqlTypes.name(queryType.name), 222 | gqlTypes.name('data'), 223 | args, 224 | null, 225 | gqlTypes.selectionSet([ 226 | gqlTypes.field( 227 | gqlTypes.name('returning'), 228 | null, 229 | null, 230 | null, 231 | gqlTypes.selectionSet(fields) 232 | ), 233 | ]) 234 | ), 235 | ]), 236 | gqlTypes.name(queryType.name), 237 | apolloArgs 238 | ), 239 | ]); 240 | } 241 | 242 | return gqlTypes.document([ 243 | gqlTypes.operationDefinition( 244 | 'query', 245 | gqlTypes.selectionSet([ 246 | gqlTypes.field( 247 | gqlTypes.name(queryType.name), 248 | gqlTypes.name('returning'), 249 | args, 250 | null, 251 | gqlTypes.selectionSet(fields) 252 | ), 253 | ]), 254 | gqlTypes.name(queryType.name), 255 | apolloArgs 256 | ), 257 | ]); 258 | }; 259 | 260 | export default (introspectionResults) => 261 | buildGqlQuery( 262 | introspectionResults, 263 | buildFields, 264 | buildMetaArgs, 265 | buildArgs, 266 | buildApolloArgs 267 | ); 268 | -------------------------------------------------------------------------------- /src/buildQuery.js: -------------------------------------------------------------------------------- 1 | import buildVariables from './buildVariables'; 2 | import buildGqlQuery from './buildGqlQuery'; 3 | import getResponseParser from './getResponseParser'; 4 | 5 | export const buildQueryFactory = ( 6 | buildVariablesImpl, 7 | buildGqlQueryImpl, 8 | getResponseParserImpl 9 | ) => (introspectionResults) => { 10 | const knownResources = introspectionResults.resources.map((r) => r.type.name); 11 | 12 | return (aorFetchType, resourceName, params) => { 13 | const resource = introspectionResults.resources.find( 14 | (r) => r.type.name === resourceName 15 | ); 16 | 17 | if (!resource) { 18 | if (knownResources.length) { 19 | throw new Error( 20 | `Unknown resource ${resourceName}. Make sure it has been declared on your server side schema. Known resources are ${knownResources.join( 21 | ', ' 22 | )}` 23 | ); 24 | } else { 25 | throw new Error( 26 | `Unknown resource ${resourceName}. No resources were found. Make sure it has been declared on your server side schema and check if your Authorization header is properly set up.` 27 | ); 28 | } 29 | } 30 | 31 | const queryType = resource[aorFetchType]; 32 | 33 | if (!queryType) { 34 | throw new Error( 35 | `No query or mutation matching fetch type ${aorFetchType} could be found for resource ${resource.type.name}` 36 | ); 37 | } 38 | 39 | const variables = buildVariablesImpl(introspectionResults)( 40 | resource, 41 | aorFetchType, 42 | params, 43 | queryType 44 | ); 45 | const query = buildGqlQueryImpl(introspectionResults)( 46 | resource, 47 | aorFetchType, 48 | queryType, 49 | variables 50 | ); 51 | const parseResponse = getResponseParserImpl(introspectionResults)( 52 | aorFetchType, 53 | resource, 54 | queryType 55 | ); 56 | 57 | return { 58 | query, 59 | variables, 60 | parseResponse, 61 | }; 62 | }; 63 | }; 64 | 65 | export default buildQueryFactory( 66 | buildVariables, 67 | buildGqlQuery, 68 | getResponseParser 69 | ); 70 | -------------------------------------------------------------------------------- /src/buildVariables.js: -------------------------------------------------------------------------------- 1 | import set from 'lodash/set'; 2 | import omit from 'lodash/omit'; 3 | import { 4 | GET_ONE, 5 | GET_LIST, 6 | GET_MANY, 7 | GET_MANY_REFERENCE, 8 | DELETE, 9 | CREATE, 10 | UPDATE, 11 | UPDATE_MANY, 12 | DELETE_MANY, 13 | } from './fetchActions'; 14 | 15 | import getFinalType from './getFinalType'; 16 | 17 | const buildGetListVariables = (introspectionResults) => ( 18 | resource, 19 | aorFetchType, 20 | params 21 | ) => { 22 | const result = {}; 23 | let { filter: filterObj = {} } = params; 24 | const { customFilters = [] } = params; 25 | 26 | /** 27 | keys with comma separated values 28 | { 29 | 'title@ilike,body@like,authors@similar': 'test', 30 | 'col1@like,col2@like': 'val' 31 | } 32 | */ 33 | const orFilterKeys = Object.keys(filterObj).filter((e) => e.includes(',')); 34 | /** 35 | format filters 36 | { 37 | 'title@ilike': 'test', 38 | 'body@like': 'test', 39 | 'authors@similar': 'test', 40 | 'col1@like': 'val', 41 | 'col2@like': 'val' 42 | } 43 | */ 44 | const orFilterObj = orFilterKeys.reduce((acc, commaSeparatedKey) => { 45 | const keys = commaSeparatedKey.split(','); 46 | return { 47 | ...acc, 48 | ...keys.reduce((acc2, key) => { 49 | return { 50 | ...acc2, 51 | [key]: filterObj[commaSeparatedKey], 52 | }; 53 | }, {}), 54 | }; 55 | }, {}); 56 | filterObj = omit(filterObj, orFilterKeys); 57 | const filterReducer = (obj) => (acc, key) => { 58 | let filter; 59 | if (key === 'ids') { 60 | filter = { id: { _in: obj['ids'] } }; 61 | } else if (Array.isArray(obj[key])) { 62 | filter = { [key]: { _in: obj[key] } }; 63 | } else if (obj[key] && obj[key].format === 'hasura-raw-query') { 64 | filter = { [key]: obj[key].value || {} }; 65 | } else { 66 | let [keyName, operation = ''] = key.split('@'); 67 | const field = resource.type.fields.find((f) => f.name === keyName); 68 | switch (getFinalType(field.type).name) { 69 | case 'String': 70 | operation = operation || '_ilike'; 71 | filter = { 72 | [keyName]: { 73 | [operation]: operation.includes('like') 74 | ? `%${obj[key]}%` 75 | : obj[key], 76 | }, 77 | }; 78 | break; 79 | default: 80 | filter = { [keyName]: { [operation || '_eq']: obj[key] } }; 81 | } 82 | } 83 | return [...acc, filter]; 84 | }; 85 | const andFilters = Object.keys(filterObj).reduce( 86 | filterReducer(filterObj), 87 | customFilters 88 | ); 89 | const orFilters = Object.keys(orFilterObj).reduce( 90 | filterReducer(orFilterObj), 91 | [] 92 | ); 93 | 94 | result['where'] = { 95 | _and: andFilters, 96 | ...(orFilters.length && { _or: orFilters }), 97 | }; 98 | 99 | if (params.pagination) { 100 | result['limit'] = parseInt(params.pagination.perPage, 10); 101 | result['offset'] = parseInt( 102 | (params.pagination.page - 1) * params.pagination.perPage, 103 | 10 104 | ); 105 | } 106 | 107 | if (params.sort) { 108 | result['order_by'] = set( 109 | {}, 110 | params.sort.field, 111 | params.sort.order.toLowerCase() 112 | ); 113 | } 114 | 115 | return result; 116 | }; 117 | 118 | const buildUpdateVariables = (resource, aorFetchType, params, queryType) => 119 | Object.keys(params.data).reduce((acc, key) => { 120 | // If hasura permissions do not allow a field to be updated like (id), 121 | // we are not allowed to put it inside the variables 122 | // RA passes the whole previous Object here 123 | // https://github.com/marmelab/react-admin/issues/2414#issuecomment-428945402 124 | 125 | // TODO: To overcome this permission issue, 126 | // it would be better to allow only permitted inputFields from *_set_input INPUT_OBJECT 127 | if (params.previousData && params.data[key] === params.previousData[key]) { 128 | return acc; 129 | } 130 | 131 | if (resource.type.fields.some((f) => f.name === key)) { 132 | return { 133 | ...acc, 134 | [key]: params.data[key], 135 | }; 136 | } 137 | 138 | return acc; 139 | }, {}); 140 | 141 | const buildCreateVariables = (resource, aorFetchType, params, queryType) => { 142 | return params.data; 143 | }; 144 | 145 | export default (introspectionResults) => ( 146 | resource, 147 | aorFetchType, 148 | params, 149 | queryType 150 | ) => { 151 | switch (aorFetchType) { 152 | case GET_LIST: 153 | return buildGetListVariables(introspectionResults)( 154 | resource, 155 | aorFetchType, 156 | params, 157 | queryType 158 | ); 159 | case GET_MANY_REFERENCE: { 160 | var built = buildGetListVariables(introspectionResults)( 161 | resource, 162 | aorFetchType, 163 | params, 164 | queryType 165 | ); 166 | if (params.filter) { 167 | return { 168 | ...built, 169 | where: { 170 | _and: [ 171 | ...built['where']['_and'], 172 | { [params.target]: { _eq: params.id } }, 173 | ], 174 | }, 175 | }; 176 | } 177 | return { 178 | ...built, 179 | where: { 180 | [params.target]: { _eq: params.id }, 181 | }, 182 | }; 183 | } 184 | case GET_MANY: 185 | case DELETE_MANY: 186 | return { 187 | where: { id: { _in: params.ids } }, 188 | }; 189 | 190 | case GET_ONE: 191 | return { 192 | where: { id: { _eq: params.id } }, 193 | limit: 1, 194 | }; 195 | 196 | case DELETE: 197 | return { 198 | where: { id: { _eq: params.id } }, 199 | }; 200 | case CREATE: 201 | return { 202 | objects: buildCreateVariables( 203 | resource, 204 | aorFetchType, 205 | params, 206 | queryType 207 | ), 208 | }; 209 | 210 | case UPDATE: 211 | return { 212 | _set: buildUpdateVariables(resource, aorFetchType, params, queryType), 213 | where: { id: { _eq: params.id } }, 214 | }; 215 | 216 | case UPDATE_MANY: 217 | return { 218 | _set: buildUpdateVariables(resource, aorFetchType, params, queryType), 219 | where: { id: { _in: params.ids } }, 220 | }; 221 | } 222 | }; 223 | -------------------------------------------------------------------------------- /src/fetchActions.js: -------------------------------------------------------------------------------- 1 | export const GET_LIST = 'GET_LIST'; 2 | export const GET_ONE = 'GET_ONE'; 3 | export const GET_MANY = 'GET_MANY'; 4 | export const GET_MANY_REFERENCE = 'GET_MANY_REFERENCE'; 5 | export const CREATE = 'CREATE'; 6 | export const UPDATE = 'UPDATE'; 7 | export const UPDATE_MANY = 'UPDATE_MANY'; 8 | export const DELETE = 'DELETE'; 9 | export const DELETE_MANY = 'DELETE_MANY'; 10 | 11 | export const fetchActionsWithRecordResponse = [GET_ONE, CREATE, UPDATE]; 12 | export const fetchActionsWithArrayOfIdentifiedRecordsResponse = [ 13 | GET_LIST, 14 | GET_MANY, 15 | GET_MANY_REFERENCE, 16 | ]; 17 | export const fetchActionsWithArrayOfRecordsResponse = [ 18 | ...fetchActionsWithArrayOfIdentifiedRecordsResponse, 19 | UPDATE_MANY, 20 | DELETE_MANY, 21 | ]; 22 | export const fetchActionsWithTotalResponse = [GET_LIST, GET_MANY_REFERENCE]; 23 | 24 | export const sanitizeFetchType = (fetchType) => { 25 | switch (fetchType) { 26 | case GET_LIST: 27 | return 'getList'; 28 | case GET_ONE: 29 | return 'getOne'; 30 | case GET_MANY: 31 | return 'getMany'; 32 | case GET_MANY_REFERENCE: 33 | return 'getManyReference'; 34 | case CREATE: 35 | return 'create'; 36 | case UPDATE: 37 | return 'update'; 38 | case UPDATE_MANY: 39 | return 'updateMany'; 40 | case DELETE: 41 | return 'delete'; 42 | case DELETE_MANY: 43 | return 'deleteMany'; 44 | default: 45 | return fetchType; 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/getFinalType.js: -------------------------------------------------------------------------------- 1 | import { TypeKind } from 'graphql'; 2 | 3 | /** 4 | * Ensure we get the real type even if the root type is NON_NULL or LIST 5 | * @param {GraphQLType} type 6 | */ 7 | const getFinalType = (type) => { 8 | if (type.kind === TypeKind.NON_NULL || type.kind === TypeKind.LIST) { 9 | return getFinalType(type.ofType); 10 | } 11 | 12 | return type; 13 | }; 14 | 15 | export default getFinalType; 16 | -------------------------------------------------------------------------------- /src/getResponseParser.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_LIST, 3 | GET_MANY, 4 | GET_MANY_REFERENCE, 5 | GET_ONE, 6 | CREATE, 7 | UPDATE, 8 | DELETE, 9 | UPDATE_MANY, 10 | DELETE_MANY, 11 | } from './fetchActions'; 12 | 13 | const sanitizeResource = (data = {}) => { 14 | const result = Object.keys(data).reduce((acc, key) => { 15 | if (key.startsWith('_')) { 16 | return acc; 17 | } 18 | 19 | const dataKey = data[key]; 20 | 21 | if (dataKey === null || dataKey === undefined) { 22 | return acc; 23 | } 24 | if (Array.isArray(dataKey)) { 25 | if (typeof dataKey[0] === 'object') { 26 | // if var is an array of reference objects with id properties 27 | if (dataKey[0].id != null) { 28 | return { 29 | ...acc, 30 | [key]: dataKey.map(sanitizeResource), 31 | [`${key}Ids`]: dataKey.map((d) => d.id), 32 | }; 33 | } else { 34 | return { 35 | ...acc, 36 | [key]: dataKey.map(sanitizeResource), 37 | }; 38 | } 39 | } else { 40 | return { ...acc, [key]: dataKey }; 41 | } 42 | } 43 | 44 | if (typeof dataKey === 'object') { 45 | return { 46 | ...acc, 47 | ...(dataKey && 48 | dataKey.id && { 49 | [`${key}.id`]: dataKey.id, 50 | }), 51 | [key]: sanitizeResource(dataKey), 52 | }; 53 | } 54 | 55 | return { ...acc, [key]: dataKey }; 56 | }, {}); 57 | 58 | return result; 59 | }; 60 | 61 | export default (introspectionResults) => (aorFetchType, resource) => (res) => { 62 | const response = res.data; 63 | 64 | switch (aorFetchType) { 65 | case GET_MANY_REFERENCE: 66 | case GET_LIST: 67 | return { 68 | data: response.items.map(sanitizeResource), 69 | total: response.total.aggregate.count, 70 | }; 71 | 72 | case GET_MANY: 73 | return { data: response.items.map(sanitizeResource) }; 74 | 75 | case GET_ONE: 76 | return { data: sanitizeResource(response.returning[0]) }; 77 | 78 | case CREATE: 79 | case UPDATE: 80 | case DELETE: 81 | return { data: sanitizeResource(response.data.returning[0]) }; 82 | 83 | case UPDATE_MANY: 84 | case DELETE_MANY: 85 | return { data: response.data.returning.map((x) => x.id) }; 86 | 87 | default: 88 | throw Error('Expected a propper fetchType, got: ', aorFetchType); 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge'; 2 | import buildDataProvider from 'ra-data-graphql'; 3 | import { 4 | GET_ONE, 5 | GET_LIST, 6 | GET_MANY, 7 | GET_MANY_REFERENCE, 8 | DELETE, 9 | CREATE, 10 | UPDATE, 11 | UPDATE_MANY, 12 | DELETE_MANY, 13 | } from './fetchActions'; 14 | 15 | import defaultBuildVariables from './buildVariables'; 16 | import defaultGetResponseParser from './getResponseParser'; 17 | import { 18 | buildGqlQuery, 19 | buildFields, 20 | buildMetaArgs, 21 | buildArgs, 22 | buildApolloArgs, 23 | } from './buildGqlQuery'; 24 | import { buildQueryFactory } from './buildQuery'; 25 | 26 | export { 27 | buildFields, 28 | buildMetaArgs, 29 | buildArgs, 30 | buildApolloArgs, 31 | defaultBuildVariables, 32 | defaultGetResponseParser, 33 | }; 34 | 35 | const defaultOptions = { 36 | introspection: { 37 | operationNames: { 38 | [GET_LIST]: (resource) => `${resource.name}`, 39 | [GET_ONE]: (resource) => `${resource.name}`, 40 | [GET_MANY]: (resource) => `${resource.name}`, 41 | [GET_MANY_REFERENCE]: (resource) => `${resource.name}`, 42 | [CREATE]: (resource) => `insert_${resource.name}`, 43 | [UPDATE]: (resource) => `update_${resource.name}`, 44 | [UPDATE_MANY]: (resource) => `update_${resource.name}`, 45 | [DELETE]: (resource) => `delete_${resource.name}`, 46 | [DELETE_MANY]: (resource) => `delete_${resource.name}`, 47 | }, 48 | }, 49 | }; 50 | 51 | const buildGqlQueryDefaults = { 52 | buildFields, 53 | buildMetaArgs, 54 | buildArgs, 55 | buildApolloArgs, 56 | }; 57 | 58 | const buildCustomDataProvider = ( 59 | options, 60 | buildGqlQueryOverrides = {}, 61 | customBuildVariables = defaultBuildVariables, 62 | customGetResponseParser = defaultGetResponseParser 63 | ) => { 64 | const buildGqlQueryOptions = { 65 | ...buildGqlQueryDefaults, 66 | ...buildGqlQueryOverrides, 67 | }; 68 | 69 | const customBuildGqlQuery = (introspectionResults) => 70 | buildGqlQuery( 71 | introspectionResults, 72 | buildGqlQueryOptions.buildFields, 73 | buildGqlQueryOptions.buildMetaArgs, 74 | buildGqlQueryOptions.buildArgs, 75 | buildGqlQueryOptions.buildApolloArgs 76 | ); 77 | 78 | const buildQuery = buildQueryFactory( 79 | customBuildVariables, 80 | customBuildGqlQuery, 81 | customGetResponseParser 82 | ); 83 | 84 | return buildDataProvider( 85 | merge({}, defaultOptions, { buildQuery }, options) 86 | ).then((dataProvider) => { 87 | return (fetchType, resource, params) => { 88 | return dataProvider(fetchType, resource, params); 89 | }; 90 | }); 91 | }; 92 | 93 | export default buildCustomDataProvider; 94 | -------------------------------------------------------------------------------- /src/isList.js: -------------------------------------------------------------------------------- 1 | import { TypeKind } from 'graphql'; 2 | 3 | const isList = (type) => { 4 | if (type.kind === TypeKind.NON_NULL) { 5 | return isList(type.ofType); 6 | } 7 | 8 | return type.kind === TypeKind.LIST; 9 | }; 10 | 11 | export default isList; 12 | -------------------------------------------------------------------------------- /src/isRequired.js: -------------------------------------------------------------------------------- 1 | import { TypeKind } from 'graphql'; 2 | 3 | const isRequired = (type) => { 4 | if (type.kind === TypeKind.LIST) { 5 | return isRequired(type.ofType); 6 | } 7 | 8 | return type.kind === TypeKind.NON_NULL; 9 | }; 10 | 11 | export default isRequired; 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: './src/index.js', 6 | output: { 7 | path: path.resolve('dist'), 8 | filename: 'index.js', 9 | libraryTarget: 'commonjs2', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.js?$/, 15 | exclude: /(node_modules)/, 16 | use: 'babel-loader', 17 | }, 18 | { 19 | test: /\.mjs$/, 20 | include: /node_modules/, 21 | type: 'javascript/auto', 22 | }, 23 | ], 24 | }, 25 | resolve: { 26 | extensions: ['.js'], 27 | }, 28 | }; 29 | --------------------------------------------------------------------------------