ra-data-graphql
Version:
A GraphQL data provider for react-admin
283 lines (247 loc) • 8.76 kB
text/typescript
import merge from 'lodash/merge';
import get from 'lodash/get';
import pluralize from 'pluralize';
import {
DataProvider,
HttpError,
GET_LIST,
GET_ONE,
GET_MANY,
GET_MANY_REFERENCE,
CREATE,
UPDATE,
DELETE,
DELETE_MANY,
UPDATE_MANY,
} from 'ra-core';
import {
ApolloClient,
ApolloClientOptions,
ApolloError,
ApolloQueryResult,
MutationOptions,
WatchQueryOptions,
QueryOptions,
OperationVariables,
ServerError,
} from '@apollo/client';
import buildApolloClient from './buildApolloClient';
import {
QUERY_TYPES as INNER_QUERY_TYPES,
MUTATION_TYPES as INNER_MUTATION_TYPES,
ALL_TYPES as INNER_ALL_TYPES,
} from './constants';
import {
introspectSchema,
IntrospectionOptions,
IntrospectionResult,
} from './introspection';
export * from './introspection';
export const QUERY_TYPES = INNER_QUERY_TYPES;
export const MUTATION_TYPES = INNER_MUTATION_TYPES;
export const ALL_TYPES = INNER_ALL_TYPES;
/**
* Map dataProvider method names to GraphQL queries and mutations
*
* @example for the Customer resource:
* dataProvider.getList() // query allCustomers() { ... }
* dataProvider.getOne() // query Customer($id: id) { ... }
* dataProvider.getMany() // query allCustomers($filter: { ids: [ids] }) { ... }
* dataProvider.getManyReference() // query allCustomers($filter: { [target]: [id] }) { ... }
* dataProvider.create() // mutation createCustomer($firstName: firstName, $lastName: lastName, ...) { ... }
* dataProvider.update() // mutation updateCustomer($id: id, firstName: firstName, $lastName: lastName, ...) { ... }
* dataProvider.delete() // mutation deleteCustomer($id: id) { ... }
* // note that updateMany and deleteMany aren't mapped in this adapter
*/
export const defaultOptions = {
resolveIntrospection: introspectSchema,
introspection: {
operationNames: {
[GET_LIST]: resource => `all${pluralize(resource.name)}`,
[GET_ONE]: resource => `${resource.name}`,
[GET_MANY]: resource => `all${pluralize(resource.name)}`,
[GET_MANY_REFERENCE]: resource => `all${pluralize(resource.name)}`,
[CREATE]: resource => `create${resource.name}`,
[UPDATE]: resource => `update${resource.name}`,
[DELETE]: resource => `delete${resource.name}`,
},
exclude: undefined,
include: undefined,
},
};
const getOptions = (
options:
| GetQueryOptions
| GetMutationOptions
| GetWatchQueryOptions
| undefined,
raFetchMethod: string,
resource: string
) => {
if (typeof options === 'function') {
return options(resource, raFetchMethod);
}
return options;
};
export type BuildQueryResult = QueryOptions<OperationVariables, any> & {
parseResponse: (response: ApolloQueryResult<any>) => any;
};
export type BuildQuery = (
name: string,
resource: string,
params: any
) => BuildQueryResult;
export type BuildQueryFactory = (
introspectionResults: IntrospectionResult
) => BuildQuery;
export type GetQueryOptions = (
resource: string,
raFetchMethod: string
) => Partial<QueryOptions<OperationVariables, any>>;
export type GetMutationOptions = (
resource: string,
raFetchMethod: string
) => Partial<MutationOptions<OperationVariables, any>>;
export type GetWatchQueryOptions = (
resource: string,
raFetchMethod: string
) => Partial<WatchQueryOptions<OperationVariables, any>>;
export type Options = {
client?: ApolloClient<unknown>;
clientOptions?: Partial<ApolloClientOptions<unknown>>;
introspection?: false | Partial<IntrospectionOptions>;
override?: {
[key: string]: (params: any) => BuildQueryResult;
};
buildQuery: BuildQueryFactory;
query?: GetQueryOptions;
mutation?: GetMutationOptions;
watchQuery?: GetWatchQueryOptions;
};
const buildGraphQLProvider = (options: Options): GraphqlDataProvider => {
const {
client: clientObject,
clientOptions,
introspection,
resolveIntrospection,
buildQuery: buildQueryFactory,
override = {},
...otherOptions
} = merge({}, defaultOptions, options);
if (
Object.keys(override).length > 0 &&
process.env.NODE_ENV === 'production'
) {
console.warn(
'The override option is deprecated. You should instead wrap the buildQuery function provided by the dataProvider you use.'
);
}
const client = clientObject || buildApolloClient(clientOptions);
let introspectionResults;
let introspectionResultsPromise;
const callApollo = async (raFetchMethod, resource, params) => {
if (introspection) {
if (!introspectionResultsPromise) {
introspectionResultsPromise = resolveIntrospection(
client,
introspection
);
}
introspectionResults = await introspectionResultsPromise;
}
const buildQuery = buildQueryFactory(introspectionResults);
const overriddenBuildQuery = get(
override,
`${resource}.${raFetchMethod}`
);
const { parseResponse, ...query } = overriddenBuildQuery
? {
...buildQuery(raFetchMethod, resource, params),
...overriddenBuildQuery(params),
}
: buildQuery(raFetchMethod, resource, params);
const operation = getQueryOperation(query.query);
if (operation === 'query') {
const apolloQuery = {
...query,
fetchPolicy: 'network-only',
...getOptions(otherOptions.query, raFetchMethod, resource),
};
apolloQuery.context = merge(
{
fetchOptions: {
signal: params?.signal,
},
},
apolloQuery.context
);
return (
client
// @ts-ignore
.query(apolloQuery)
.then(response => parseResponse(response))
.catch(handleError)
);
}
const apolloQuery = {
mutation: query.query,
variables: query.variables,
...getOptions(otherOptions.mutation, raFetchMethod, resource),
};
return (
client
// @ts-ignore
.mutate(apolloQuery)
.then(parseResponse)
.catch(handleError)
);
};
const raDataProvider: GraphqlDataProvider = {
create: (resource, params) => callApollo(CREATE, resource, params),
delete: (resource, params) => callApollo(DELETE, resource, params),
deleteMany: (resource, params) =>
callApollo(DELETE_MANY, resource, params),
getList: (resource, params) => callApollo(GET_LIST, resource, params),
getMany: (resource, params) => callApollo(GET_MANY, resource, params),
getManyReference: (resource, params) =>
callApollo(GET_MANY_REFERENCE, resource, params),
getOne: (resource, params) => callApollo(GET_ONE, resource, params),
update: (resource, params) => callApollo(UPDATE, resource, params),
updateMany: (resource, params) =>
callApollo(UPDATE_MANY, resource, params),
getIntrospection: () => {
if (introspection) {
if (!introspectionResultsPromise) {
introspectionResultsPromise = resolveIntrospection(
client,
introspection
);
}
return introspectionResultsPromise;
}
},
client,
};
return raDataProvider;
};
const handleError = (error: ApolloError) => {
if (error?.networkError as ServerError) {
throw new HttpError(
(error?.networkError as ServerError)?.message,
(error?.networkError as ServerError)?.statusCode
);
}
throw new HttpError(error.message, 200, error);
};
const getQueryOperation = query => {
if (query && query.definitions && query.definitions.length > 0) {
return query.definitions[0].operation;
}
throw new Error('Unable to determine the query operation');
};
export type GetIntrospection = () => Promise<IntrospectionResult>;
export type GraphqlDataProvider = DataProvider & {
getIntrospection: GetIntrospection;
client: ApolloClient<unknown>;
};
export default buildGraphQLProvider;