packages/hasura/src/dataProvider/index.ts
import { BaseRecord, DataProvider } from "@refinedev/core";
import camelCase from "camelcase";
import * as gql from "gql-query-builder";
import { GraphQLClient } from "graphql-request";
import {
camelizeKeys,
generateFilters,
generateSorting,
getOperationFields,
isMutation,
metaFieldsToGqlFields,
upperCaseValues,
} from "../utils";
import camelcase from "camelcase";
import gqlTag from "graphql-tag";
type IDType = "uuid" | "Int" | "String" | "Numeric";
export type NamingConvention = "hasura-default" | "graphql-default";
export type HasuraDataProviderOptions = {
idType?: IDType | ((resource: string) => IDType);
namingConvention?: NamingConvention;
};
const dataProvider = (
client: GraphQLClient,
options?: HasuraDataProviderOptions,
): Required<DataProvider> => {
const { idType, namingConvention = "hasura-default" } = options ?? {};
const defaultNamingConvention = namingConvention === "hasura-default";
const getIdType = (resource: string) => {
if (typeof idType === "function") {
return idType(resource);
}
return idType ?? "uuid";
};
return {
getOne: async ({ resource, id, meta }) => {
const operation = defaultNamingConvention
? `${meta?.operation ?? resource}_by_pk`
: camelCase(`${meta?.operation ?? resource}_by_pk`);
const pascalOperation = camelcase(operation, {
pascalCase: true,
});
const gqlOperation = meta?.gqlQuery ?? meta?.gqlMutation;
if (gqlOperation) {
let query = gqlOperation;
const variables = {
id,
};
if (isMutation(gqlOperation)) {
const stringFields = getOperationFields(gqlOperation);
query = gqlTag`
query Get${pascalOperation}($id: ${getIdType(resource)}!) {
${operation}(id: $id) {
${stringFields}
}
}
`;
}
const response = await client.request<BaseRecord>(query, variables);
return {
data: response[operation],
};
}
const { query, variables } = gql.query({
operation,
variables: {
id: {
value: id,
type: getIdType(resource),
required: true,
},
...meta?.variables,
},
fields: meta?.fields,
});
const response = await client.request<BaseRecord>(query, variables);
return {
data: response[operation],
};
},
getMany: async ({ resource, ids, meta }) => {
const operation = defaultNamingConvention
? meta?.operation ?? resource
: camelCase(meta?.operation ?? resource);
const type = defaultNamingConvention
? `${operation}_bool_exp`
: camelCase(`${operation}_bool_exp`, { pascalCase: true });
if (meta?.gqlQuery) {
const response = await client.request<BaseRecord>(meta.gqlQuery, {
where: {
id: {
_in: ids,
},
},
});
return {
data: response[operation],
};
}
const { query, variables } = gql.query({
operation,
fields: meta?.fields,
variables: meta?.variables ?? {
where: {
type,
value: {
id: {
_in: ids,
},
},
},
},
});
const response = await client.request<BaseRecord>(query, variables);
return {
data: response[operation],
};
},
getList: async ({ resource, sorters, filters, pagination, meta }) => {
const operation = defaultNamingConvention
? meta?.operation ?? resource
: camelCase(meta?.operation ?? resource);
const aggregateOperation = defaultNamingConvention
? `${operation}_aggregate`
: camelCase(`${operation}_aggregate`);
const {
current = 1,
pageSize: limit = 10,
mode = "server",
} = pagination ?? {};
const hasuraPagination =
mode === "server" ? { limit, offset: (current - 1) * limit } : {};
const hasuraSorting = defaultNamingConvention
? generateSorting(sorters)
: upperCaseValues(camelizeKeys(generateSorting(sorters)));
const hasuraFilters = generateFilters(filters, namingConvention);
let query;
let variables;
if (meta?.gqlQuery) {
query = meta.gqlQuery;
variables = {
...hasuraPagination,
...(hasuraSorting &&
(namingConvention === "graphql-default"
? {
orderBy: hasuraSorting,
}
: {
order_by: hasuraSorting,
})),
...(hasuraFilters && {
where: hasuraFilters,
}),
};
} else {
const hasuraSortingType = defaultNamingConvention
? `[${operation}_order_by!]`
: `[${camelCase(`${operation}_order_by!`, {
pascalCase: true,
})}]`;
const hasuraFiltersType = defaultNamingConvention
? `${operation}_bool_exp`
: camelCase(`${operation}_bool_exp`, { pascalCase: true });
const gqlQuery = gql.query([
{
operation,
fields: meta?.fields,
variables: {
...hasuraPagination,
...(hasuraSorting &&
(namingConvention === "graphql-default"
? {
orderBy: {
value: hasuraSorting,
type: hasuraSortingType,
},
}
: {
order_by: {
value: hasuraSorting,
type: hasuraSortingType,
},
})),
...(hasuraFilters && {
where: {
value: hasuraFilters,
type: hasuraFiltersType,
},
}),
},
},
{
operation: aggregateOperation,
fields: [{ aggregate: ["count"] }],
variables: {
where: {
value: hasuraFilters,
type: hasuraFiltersType,
},
},
},
]);
query = gqlQuery.query;
variables = gqlQuery.variables;
}
const response = await client.request<BaseRecord>(query, variables);
return {
data: response[operation],
total: response[aggregateOperation].aggregate.count,
};
},
create: async ({ resource, variables, meta }) => {
const operation = defaultNamingConvention
? meta?.operation ?? resource
: camelCase(meta?.operation ?? resource);
const insertOperation = defaultNamingConvention
? `insert_${operation}_one`
: camelCase(`insert_${operation}_one`);
const gqlOperation = meta?.gqlMutation ?? meta?.gqlQuery;
if (gqlOperation) {
const response = await client.request<BaseRecord>(gqlOperation, {
object: variables || {},
});
return {
data: response[insertOperation],
};
}
const insertType = defaultNamingConvention
? `${operation}_insert_input`
: camelCase(`${operation}_insert_input`, { pascalCase: true });
const { query, variables: gqlVariables } = gql.mutation({
operation: insertOperation,
variables: {
object: {
type: insertType,
value: variables,
required: true,
},
},
fields: meta?.fields ?? ["id", ...Object.keys(variables || {})],
});
const response = await client.request<BaseRecord>(query, gqlVariables);
return {
data: response[insertOperation],
};
},
createMany: async ({ resource, variables: variablesFromParams, meta }) => {
const operation = meta?.operation ?? resource;
const pascalOperation = camelcase(operation, {
pascalCase: true,
});
const insertOperation = defaultNamingConvention
? `insert_${operation}`
: camelCase(`insert_${operation}`);
if (meta?.gqlMutation) {
const response = await client.request<BaseRecord>(meta.gqlMutation, {
objects: variablesFromParams,
});
return {
data: response[insertOperation]["returning"],
};
}
const insertType = defaultNamingConvention
? `[${operation}_insert_input!]`
: `[${camelCase(`${operation}_insert_input!`, {
pascalCase: true,
})}]`;
const query = gqlTag`
mutation CreateMany${pascalOperation}($objects: ${insertType}!) {
${insertOperation}(objects: $objects) {
returning {
id
${metaFieldsToGqlFields(meta?.fields)}
}
}
}
`;
const variables = {
objects: variablesFromParams,
};
const response = await client.request<BaseRecord>(query, variables);
return {
data: response[insertOperation]["returning"],
};
},
update: async ({ resource, id, variables, meta }) => {
const operation = meta?.operation ?? resource;
const updateOperation = defaultNamingConvention
? `update_${operation}_by_pk`
: camelCase(`update_${operation}_by_pk`);
const gqlOperation = meta?.gqlMutation ?? meta?.gqlQuery;
if (gqlOperation) {
const response = await client.request<BaseRecord>(gqlOperation, {
id,
object: variables || {},
});
return {
data: response[updateOperation],
};
}
const pkColumnsType = defaultNamingConvention
? `${operation}_pk_columns_input`
: camelCase(`${operation}_pk_columns_input!`, {
pascalCase: true,
});
const setInputType = defaultNamingConvention
? `${operation}_set_input`
: camelCase(`${operation}_set_input`, { pascalCase: true });
const { query, variables: gqlVariables } = gql.mutation({
operation: updateOperation,
variables: {
...(defaultNamingConvention
? {
pk_columns: {
type: pkColumnsType,
value: {
id: id,
},
required: true,
},
}
: {
pkColumns: {
type: pkColumnsType,
value: {
id: id,
},
},
}),
_set: {
type: setInputType,
value: variables,
required: true,
},
},
fields: meta?.fields ?? ["id"],
});
const response = await client.request<BaseRecord>(query, gqlVariables);
return {
data: response[updateOperation],
};
},
updateMany: async ({
resource,
ids,
variables: variablesFromParams,
meta,
}) => {
const operation = meta?.operation ?? resource;
const pascalOperation = camelcase(operation, {
pascalCase: true,
});
const updateOperation = defaultNamingConvention
? `update_${operation}`
: camelCase(`update_${operation}`);
if (meta?.gqlMutation) {
const response = await client.request<BaseRecord>(meta.gqlMutation, {
ids,
_set: variablesFromParams,
});
return {
data: response[updateOperation]["returning"],
};
}
const whereType = defaultNamingConvention
? `${operation}_bool_exp`
: camelCase(`${operation}_bool_exp`, { pascalCase: true });
const setInputType = defaultNamingConvention
? `${operation}_set_input`
: camelCase(`${operation}_set_input`, { pascalCase: true });
const query = gqlTag`
mutation UpdateMany${pascalOperation}($where: ${whereType}!, $_set: ${setInputType}!) {
${updateOperation}(where: $where, _set: $_set) {
returning {
id
${metaFieldsToGqlFields(meta?.fields)}
}
}
}
`;
const variables = meta?.variables ?? {
where: {
id: {
_in: ids,
},
},
_set: variablesFromParams,
};
const response = await client.request<BaseRecord>(query, variables);
return {
data: response[updateOperation]["returning"],
};
},
deleteOne: async ({ resource, id, meta }) => {
const operation = meta?.operation ?? resource;
const deleteOperation = defaultNamingConvention
? `delete_${operation}_by_pk`
: camelCase(`delete_${operation}_by_pk`);
if (meta?.gqlMutation) {
const response = await client.request<BaseRecord>(meta.gqlMutation, {
id,
...meta?.variables,
});
return {
data: response[deleteOperation],
};
}
const { query, variables } = gql.mutation({
operation: deleteOperation,
variables: {
id: {
value: id,
type: getIdType(resource),
required: true,
},
...meta?.variables,
},
fields: meta?.fields ?? ["id"],
});
const response = await client.request<BaseRecord>(query, variables);
return {
data: response[deleteOperation],
};
},
deleteMany: async ({ resource, ids, meta }) => {
const operation = meta?.operation ?? resource;
const pascalOperation = camelcase(operation, {
pascalCase: true,
});
const deleteOperation = defaultNamingConvention
? `delete_${operation}`
: camelCase(`delete_${operation}`);
if (meta?.gqlMutation) {
const response = await client.request<BaseRecord>(meta?.gqlMutation, {
where: {
id: {
_in: ids,
},
},
});
return {
data: response[deleteOperation]["returning"],
};
}
const whereType = defaultNamingConvention
? `${operation}_bool_exp`
: camelCase(`${operation}_bool_exp`, { pascalCase: true });
const query = gqlTag`
mutation DeleteMany${pascalOperation}($where: ${whereType}!) {
${deleteOperation}(where: $where) {
returning {
id
${metaFieldsToGqlFields(meta?.fields)}
}
}
}
`;
const variables = meta?.variables ?? {
where: {
id: {
_in: ids,
},
},
};
const response = await client.request<BaseRecord>(query, variables);
return {
data: response[deleteOperation]["returning"],
};
},
getApiUrl: () => {
throw new Error(
"getApiUrl method is not implemented on refine-hasura data provider.",
);
},
custom: async ({ url, method, headers, meta }) => {
let gqlClient = client;
if (url) {
gqlClient = new GraphQLClient(url, { headers });
}
const gqlOperation = meta?.gqlMutation ?? meta?.gqlQuery;
if (gqlOperation) {
const response: any = await client.request(
gqlOperation,
meta?.variables ?? {},
);
return { data: response };
}
if (meta) {
if (meta.operation) {
if (method === "get") {
const { query, variables } = gql.query({
operation: meta.operation,
fields: meta.fields,
variables: meta.variables,
});
const response = await gqlClient.request<BaseRecord>(
query,
variables,
);
response.data;
return {
data: response[meta.operation],
};
}
const { query, variables } = gql.mutation({
operation: meta.operation,
fields: meta.fields,
variables: meta.variables,
});
const response = await gqlClient.request<BaseRecord>(
query,
variables,
);
return {
data: response[meta.operation],
};
}
throw Error("GraphQL operation name required.");
}
throw Error(
"GraphQL need to operation, fields and variables values in meta object.",
);
},
};
};
export default dataProvider;