This commit is contained in:
Stefan Pejcic
2024-11-07 19:03:37 +01:00
parent c6df945ed5
commit 09f9f9502d
2472 changed files with 620417 additions and 0 deletions

View File

@@ -0,0 +1,500 @@
import type { BaseRecord, DataProvider, LogicalFilter } from "@refinedev/core";
import camelcase from "camelcase";
import * as gql from "gql-query-builder";
import type VariableOptions from "gql-query-builder/build/VariableOptions";
import { GraphQLClient } from "graphql-request";
import gqlTag from "graphql-tag";
import { singular } from "pluralize";
import { generateFilters, generatePaging, generateSorting } from "../utils";
import { getOperationFields, isMutation } from "../utils/graphql";
const dataProvider = (client: GraphQLClient): Required<DataProvider> => {
return {
getList: async ({ resource, pagination, sorters, filters, meta }) => {
const operation = camelcase(resource);
const paging = generatePaging(pagination || {});
const queryVariables: VariableOptions = {};
let query;
let variables;
if (meta?.gqlQuery) {
query = meta?.gqlQuery;
variables = {
filter: filters ? generateFilters(filters as LogicalFilter[]) : {},
sorting: sorters ? generateSorting(sorters) : [],
paging,
};
} else {
if (filters) {
queryVariables["filter"] = {
type: camelcase(`${singular(resource)}Filter`, {
pascalCase: true,
}),
required: true,
value: generateFilters(filters as LogicalFilter[]),
};
}
if (sorters) {
queryVariables["sorting"] = {
type: camelcase(`${singular(resource)}Sort`, {
pascalCase: true,
}),
required: true,
list: [true],
value: generateSorting(sorters),
};
}
if (paging) {
queryVariables["paging"] = {
type: "OffsetPaging",
required: true,
value: paging,
};
}
const gqlQuery = gql.query({
operation,
fields: [{ nodes: meta?.fields }, "totalCount"],
variables: queryVariables,
});
query = gqlQuery.query;
variables = gqlQuery.variables;
}
const response = await client.request<BaseRecord>(query, variables);
return {
data: response[operation].nodes,
total: response[operation].totalCount,
};
},
getMany: async ({ resource, ids, meta }) => {
const operation = camelcase(resource);
if (meta?.gqlQuery) {
const response = await client.request<BaseRecord>(meta.gqlQuery, {
filter: {
id: { in: ids },
},
});
return {
data: response[operation].nodes,
};
}
const { query, variables } = gql.query({
operation,
fields: [{ nodes: meta?.fields || ["id"] }],
variables: {
filter: {
type: camelcase(`${singular(resource)}Filter`, {
pascalCase: true,
}),
required: true,
value: {
id: { in: ids },
},
},
},
});
const response = await client.request<BaseRecord>(query, variables);
return {
data: response[operation].nodes,
};
},
create: async ({ resource, variables, meta }) => {
const operation = `createOne${camelcase(singular(resource), {
pascalCase: true,
})}`;
const gqlOperation = meta?.gqlMutation ?? meta?.gqlQuery;
if (gqlOperation) {
const response = await client.request<BaseRecord>(gqlOperation, {
input: { [camelcase(singular(resource))]: variables },
});
return {
data: response[operation],
};
}
const { query, variables: queryVariables } = gql.mutation({
operation,
fields: meta?.fields || ["id"],
variables: {
input: {
type: `CreateOne${camelcase(singular(resource), {
pascalCase: true,
})}Input`,
required: true,
value: {
[camelcase(singular(resource))]: variables,
},
},
},
});
const response = await client.request<BaseRecord>(query, queryVariables);
return {
data: response[operation],
};
},
createMany: async ({ resource, variables, meta }) => {
const pascalResource = camelcase(resource, { pascalCase: true });
const operation = `createMany${pascalResource}`;
const gqlOperation = meta?.gqlMutation ?? meta?.gqlQuery;
if (gqlOperation) {
const response = await client.request<BaseRecord>(gqlOperation, {
input: {
[camelcase(resource)]: variables,
},
});
return {
data: response[operation],
};
}
const { query, variables: queryVariables } = gql.mutation({
operation,
fields: meta?.fields || ["id"],
variables: {
input: {
type: `CreateMany${camelcase(resource, {
pascalCase: true,
})}Input`,
required: true,
value: {
[camelcase(resource)]: variables,
},
},
},
});
const response = await client.request<BaseRecord>(query, queryVariables);
return {
data: response[operation],
};
},
update: async ({ resource, id, variables, meta }) => {
const operation = `updateOne${camelcase(singular(resource), {
pascalCase: true,
})}`;
const gqlOperation = meta?.gqlMutation ?? meta?.gqlQuery;
if (gqlOperation) {
const response = await client.request<BaseRecord>(gqlOperation, {
input: {
id,
update: variables,
},
});
return {
data: response[operation],
};
}
const { query, variables: queryVariables } = gql.mutation({
operation,
fields: meta?.fields || ["id"],
variables: {
input: {
type: `UpdateOne${camelcase(singular(resource), {
pascalCase: true,
})}Input`,
required: true,
value: {
id,
update: variables,
},
},
},
});
const response = await client.request<BaseRecord>(query, queryVariables);
return {
data: response[operation],
};
},
updateMany: async ({ resource, ids, variables, meta }) => {
const pascalResource = camelcase(resource, {
pascalCase: true,
});
const mutationOperation = `updateMany${pascalResource}`;
const mutation = gqlTag`
mutation UpdateMany${pascalResource}($input: UpdateMany${pascalResource}Input!) {
${mutationOperation}(input: $input) {
updatedCount
}
}
`;
await client.request<BaseRecord>(mutation, {
input: { filter: { id: { in: ids } }, update: variables },
});
const operation = camelcase(resource);
let query;
let queryVariables;
if (meta?.fields) {
const gqlQuery = gql.query({
operation,
fields: [{ nodes: meta?.fields || ["id"] }],
variables: {
filter: {
type: camelcase(`${singular(resource)}Filter`, {
pascalCase: true,
}),
required: true,
value: {
id: { in: ids },
},
},
},
});
query = gqlQuery.query;
queryVariables = gqlQuery.variables;
} else {
query = gqlTag`
query GetMany${pascalResource}($filter: ${singular(
pascalResource,
)}Filter!) {
${operation}(filter: $filter) {
nodes {
id
}
}
}
`;
queryVariables = {
filter: { id: { in: ids } },
};
}
const response = await client.request<BaseRecord>(query, queryVariables);
return {
data: response[operation].nodes,
};
},
getOne: async ({ resource, id, meta }) => {
const operation = camelcase(singular(resource));
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${camelcase(singular(resource), {
pascalCase: true,
})}($id: ID!) {
${operation}(id: $id) {
${stringFields}
}
}
`;
}
const response = await client.request<BaseRecord>(query, variables);
return {
data: response[operation],
};
}
const { query, variables } = gql.query({
operation,
fields: meta?.fields || ["id"],
variables: {
id: {
type: "ID",
required: true,
value: id,
},
},
});
const response = await client.request<BaseRecord>(query, variables);
return {
data: response[operation],
};
},
deleteOne: async ({ resource, id, meta }) => {
const pascalResource = camelcase(singular(resource), {
pascalCase: true,
});
const operation = `deleteOne${pascalResource}`;
if (meta?.gqlMutation) {
const response = await client.request<BaseRecord>(meta.gqlMutation, {
input: { id },
});
return {
data: response[operation],
};
}
const query = gqlTag`
mutation DeleteOne${pascalResource}($input: DeleteOne${pascalResource}Input!) {
${operation}(input: $input) {
id
}
}
`;
const response = await client.request<BaseRecord>(query, {
input: { id },
});
return {
data: response[operation],
};
},
deleteMany: async ({ resource, ids }) => {
const pascalResource = camelcase(resource, {
pascalCase: true,
});
const operation = `deleteMany${pascalResource}`;
const query = gqlTag`
mutation DeleteMany${pascalResource}($input: DeleteMany${pascalResource}Input!) {
${operation}(input: $input) {
deletedCount
}
}
`;
const variables = {
input: {
filter: {
id: { in: ids },
},
},
};
await client.request<BaseRecord>(query, variables);
return {
data: [],
};
},
getApiUrl: () => {
return (client as any).url; // url field in GraphQLClient is private
},
custom: async ({ url, method, headers, meta }) => {
const SUPPORTED_METHODS = ["get", "post"];
const requestUrl = url || (client as any).url;
if (!SUPPORTED_METHODS.some((it) => it === method)) {
throw Error(`GraphQL does not support ${method} method.`);
}
const validMethod = method as "get" | "post";
const _client = new GraphQLClient(requestUrl, {
...client.requestConfig,
method: validMethod,
headers: { ...client.requestConfig.headers, ...headers },
});
const gqlOperation = meta?.gqlMutation ?? meta?.gqlQuery;
if (gqlOperation) {
const response: any = await _client.request<BaseRecord>({
document: gqlOperation,
variables: meta?.variables,
});
return { data: response };
}
if (meta?.rawQuery) {
const response = await _client.request<BaseRecord>({
document: meta.rawQuery,
variables: meta.variables,
});
return { data: response };
}
if (meta) {
if (meta.operation) {
let query;
let variables;
if (method === "get") {
const gqlQuery = gql.query({
operation: meta.operation,
fields: meta.fields,
variables: meta.variables,
});
query = gqlQuery.query;
variables = gqlQuery.variables;
} else {
const gqlMutation = gql.mutation({
operation: meta.operation,
fields: meta.fields,
variables: meta.variables,
});
query = gqlMutation.query;
variables = gqlMutation.variables;
}
const response = await _client.request<BaseRecord>({
document: query,
variables,
});
return {
data: response[meta.operation],
};
}
throw Error("GraphQL operation name required.");
}
throw Error(
"GraphQL needs operation, fields and variables values in meta object.",
);
},
};
};
export default dataProvider;

View File

@@ -0,0 +1,30 @@
import dataProvider from "./dataProvider/index.js";
export * from "./dataProvider/index.js";
export * from "./interfaces.js";
export * from "./liveProvider/index.js";
export * as qqlQueryBuilder from "gql-query-builder";
export type {
BatchRequestDocument,
BatchRequestsExtendedOptions,
BatchRequestsOptions,
ClientError,
GraphQLWebSocketClient,
RawRequestExtendedOptions,
RawRequestOptions,
RequestDocument,
RequestExtendedOptions,
RequestOptions,
Variables,
} from "graphql-request";
export {
batchRequests,
gql,
GraphQLClient,
rawRequest,
request,
resolveRequestDocument,
} from "graphql-request";
export * as graphqlWS from "graphql-ws";
export default dataProvider;

View File

@@ -0,0 +1,9 @@
export type GetFieldsFromList<Q extends Record<string, any>> =
Q[keyof Q]["nodes"][0];
export type GetFields<Q extends Record<string, any>> = Q[keyof Q];
export type GetVariables<Q extends Record<"input", any>> =
Q["input"]["update"] extends object
? Q["input"]["update"]
: Q["input"][keyof Q["input"]];

View File

@@ -0,0 +1,75 @@
import type { LiveProvider } from "@refinedev/core";
import type { Client } from "graphql-ws";
import { generateSubscription } from "../utils";
type SubscriptionAction = "created" | "updated" | "deleted";
export const liveProvider = (client: Client): LiveProvider => {
const subscribeToResource = (
client: Client,
callback: Function,
params: any,
meta: any,
action: SubscriptionAction,
resource: string,
unsubscribes: Function[],
) => {
const unsubscribe = generateSubscription(
client,
{ callback, params, meta },
action,
);
unsubscribes.push(unsubscribe);
};
return {
subscribe({ callback, params, meta }) {
const { resource, subscriptionType } = params ?? {};
if (!meta || !subscriptionType || !resource) {
throw new Error(
"[useSubscription]: `meta`, `subscriptionType` and `resource` are required in `params` for graphql subscriptions",
);
}
const unsubscribes: any[] = [];
if (params?.subscriptionType === "useList") {
["created", "updated", "deleted"].forEach((action) =>
subscribeToResource(
client,
callback,
params,
meta,
action as SubscriptionAction,
resource,
unsubscribes,
),
);
}
if (params?.subscriptionType === "useOne") {
subscribeToResource(
client,
callback,
params,
meta,
"updated",
resource,
unsubscribes,
);
}
const unsubscribe = () => {
unsubscribes.forEach((unsubscribe) => unsubscribe());
};
return unsubscribe;
},
unsubscribe(unsubscribe) {
unsubscribe();
},
};
};

View File

@@ -0,0 +1,75 @@
import {
type FieldNode,
type DocumentNode,
visit,
type SelectionSetNode,
} from "graphql";
const getChildNodesField = (node: FieldNode): FieldNode | undefined => {
return node?.selectionSet?.selections?.find(
(node) => node.kind === "Field" && node.name.value === "nodes",
) as FieldNode;
};
export const getOperationFields = (documentNode: DocumentNode) => {
const fieldLines: string[] = [];
let isInitialEnter = true;
let depth = 0;
let isNestedField = false;
visit(documentNode, {
Field: {
enter(node): SelectionSetNode | void {
if (isInitialEnter) {
isInitialEnter = false;
const childNodesField = getChildNodesField(node);
const nodeToReturn = childNodesField ?? node;
if (typeof nodeToReturn.selectionSet === "undefined") {
throw new Error("Operation must have a selection set");
}
return nodeToReturn.selectionSet;
}
fieldLines.push(
`${depth > 0 ? " ".repeat(isNestedField ? depth : depth - 1) : ""}${
node.name.value
}${node.selectionSet ? " {" : ""}`,
);
if (node.selectionSet) {
depth++;
isNestedField = true;
}
},
leave(node) {
if (node.selectionSet) {
depth--;
fieldLines.push(`${" ".repeat(depth)}}`);
isNestedField = false;
}
},
},
});
return fieldLines.join("\n").trim();
};
export const isMutation = (documentNode: DocumentNode) => {
let isMutation = false;
visit(documentNode, {
OperationDefinition: {
enter(node) {
if (node.operation === "mutation") {
isMutation = true;
}
},
},
});
return isMutation;
};

View File

@@ -0,0 +1,456 @@
import type {
CrudFilter,
CrudOperators,
CrudSorting,
LogicalFilter,
Pagination,
} from "@refinedev/core";
import camelcase from "camelcase";
import * as gql from "gql-query-builder";
import type VariableOptions from "gql-query-builder/build/VariableOptions";
import type { Client } from "graphql-ws";
import set from "lodash/set";
import { singular } from "pluralize";
import { getOperationFields } from "./graphql";
export const generateSubscription = (
client: Client,
{ callback, params, meta }: any,
type: string,
) => {
const generatorMap: any = {
created: generateCreatedSubscription,
updated: generateUpdatedSubscription,
deleted: generateDeletedSubscription,
};
const { resource, filters, subscriptionType, id, ids } = params ?? {};
const generator = generatorMap[type];
const { operation, query, variables, operationName } = generator({
ids,
id,
resource,
filters,
meta,
subscriptionType,
});
const onNext = (payload: any) => {
callback(payload.data[operation]);
};
const unsubscribe = client.subscribe(
{ query, variables, operationName },
{
next: onNext,
error: console.error,
complete: () => null,
},
);
return unsubscribe;
};
const operatorMap: { [key: string]: string } = {
eq: "eq",
ne: "neq",
lt: "lt",
gt: "gt",
lte: "lte",
gte: "gte",
in: "in",
nin: "notIn",
};
const operatorMapper = (
operator: CrudOperators,
value: any,
): { [key: string]: any } => {
if (operator === "contains") {
return { iLike: `%${value}%` };
}
if (operator === "ncontains") {
return { notILike: `%${value}%` };
}
if (operator === "containss") {
return { like: `%${value}%` };
}
if (operator === "ncontainss") {
return { notLike: `%${value}%` };
}
if (operator === "startswith") {
return { iLike: `${value}%` };
}
if (operator === "nstartswith") {
return { notILike: `${value}%` };
}
if (operator === "startswiths") {
return { like: `${value}%` };
}
if (operator === "nstartswiths") {
return { notLike: `${value}%` };
}
if (operator === "endswith") {
return { iLike: `%${value}` };
}
if (operator === "nendswith") {
return { notILike: `%${value}` };
}
if (operator === "endswiths") {
return { like: `%${value}` };
}
if (operator === "nendswiths") {
return { notLike: `%${value}` };
}
if (operator === "null") {
return { is: null };
}
if (operator === "nnull") {
return { isNot: null };
}
if (operator === "between") {
if (!Array.isArray(value)) {
throw new Error("Between operator requires an array");
}
if (value.length !== 2) {
return {};
}
return { between: { lower: value[0], upper: value[1] } };
}
if (operator === "nbetween") {
if (!Array.isArray(value)) {
throw new Error("NBetween operator requires an array");
}
if (value.length !== 2) {
return {};
}
return { notBetween: { lower: value[0], upper: value[1] } };
}
return { [operatorMap[operator]]: value };
};
export const generateFilters = (filters: LogicalFilter[]) => {
const result: { [key: string]: { [key: string]: string | number } } = {};
filters
.filter((f) => {
if (Array.isArray(f.value) && f.value.length === 0) {
return false;
}
if (typeof f.value === "number") {
return Number.isFinite(f.value);
}
// If the value is null or undefined, it returns false.
return !(f.value == null);
})
.map((filter: LogicalFilter | CrudFilter) => {
if (filter.operator === "and" || filter.operator === "or") {
return set(result, filter.operator, [
generateFilters(filter.value as LogicalFilter[]),
]);
}
if ("field" in filter) {
return set(
result,
filter.field,
operatorMapper(filter.operator, filter.value),
);
}
return {};
});
return result;
};
export const generateSorting = (sorters: CrudSorting) => {
return sorters.map((sorter) => {
return {
field: sorter.field,
direction: sorter.order.toUpperCase(),
};
});
};
export const generatePaging = (pagination: Pagination) => {
// maximum value of 32 bit signed integer
if (pagination.mode === "off") return { limit: 2147483647 };
if (pagination.mode !== "server") return undefined;
if (!pagination.current || !pagination.pageSize) return undefined;
return {
limit: pagination.pageSize,
offset: (pagination.current - 1) * pagination.pageSize,
};
};
export const generateCreatedSubscription = ({
resource,
filters,
meta,
}: any) => {
const gqlOperation = meta?.gqlQuery ?? meta?.gqlMutation;
if (gqlOperation) {
const singularResourceName = camelcase(singular(resource), {
pascalCase: true,
});
const operationName = `Created${singularResourceName}`;
const operation = `created${singularResourceName}`;
const query = `
subscription ${operationName}($input: Create${singularResourceName}SubscriptionFilterInput) {
${operation}(input: $input) {
${getOperationFields(gqlOperation)}
}
}
`;
const variables: VariableOptions = {};
if (filters) {
variables["input"] = {
filter: generateFilters(
filters.filter(
(filter: LogicalFilter) => !filter.field.includes("."),
),
),
};
}
return { query, variables, operation, operationName };
}
const operation = `created${camelcase(singular(resource), {
pascalCase: true,
})}`;
const queryVariables: VariableOptions = {};
if (filters) {
queryVariables["input"] = {
type: camelcase(
`create_${singular(resource)}_subscription_filter_input`,
{
pascalCase: true,
},
),
required: true,
value: {
filter: generateFilters(
filters.filter(
(filter: LogicalFilter) => !filter.field.includes("."),
),
),
},
};
}
const { query, variables } = gql.subscription({
operation,
fields: meta.fields,
variables: queryVariables,
});
return { query, variables, operation };
};
export const generateUpdatedSubscription = ({
id,
resource,
filters,
meta,
}: any) => {
const gqlOperation = meta?.gqlQuery ?? meta?.gqlMutation;
if (gqlOperation) {
const singularResourceName = camelcase(singular(resource), {
pascalCase: true,
});
const operationName = `Updated${singularResourceName}`;
const operation = `updatedOne${singularResourceName}`;
const query = `
subscription ${operationName}($input: UpdateOne${singularResourceName}SubscriptionFilterInput) {
${operation}(input: $input) {
${getOperationFields(gqlOperation)}
}
}
`;
const variables: VariableOptions = {};
if (filters) {
variables["input"] = {
filter: generateFilters(
filters.filter(
(filter: LogicalFilter) => !filter.field.includes("."),
),
),
};
}
if (id) {
variables["input"] = {
filter: {
id: { eq: id },
},
};
}
return { query, variables, operation, operationName };
}
const operation = `updatedOne${camelcase(singular(resource), {
pascalCase: true,
})}`;
const queryVariables: VariableOptions = {};
if (filters) {
queryVariables["input"] = {
type: camelcase(
`update_one_${singular(resource)}_subscription_filter_input`,
{
pascalCase: true,
},
),
required: true,
value: {
filter: generateFilters(
filters.filter(
(filter: LogicalFilter) => !filter.field.includes("."),
),
),
},
};
}
if (id) {
queryVariables["input"] = {
type: camelcase(
`update_one_${singular(resource)}_subscription_filter_input`,
{
pascalCase: true,
},
),
required: true,
value: {
filter: {
id: { eq: id },
},
},
};
}
const { query, variables } = gql.subscription({
operation,
fields: meta.fields,
variables: queryVariables,
});
return { query, variables, operation };
};
export const generateDeletedSubscription = ({
resource,
filters,
meta,
}: any) => {
if (meta?.gqlQuery) {
const singularResourceName = camelcase(singular(resource), {
pascalCase: true,
});
const operationName = `Deleted${singularResourceName}`;
const operation = `deletedOne${singularResourceName}`;
const query = `
subscription ${operationName}($input: DeleteOne${singularResourceName}SubscriptionFilterInput) {
${operation}(input: $input) {
id
}
}
`;
const variables: VariableOptions = {};
if (filters) {
variables["input"] = {
filter: generateFilters(
filters.filter(
(filter: LogicalFilter) => !filter.field.includes("."),
),
),
};
}
return { query, variables, operation, operationName };
}
const operation = `deletedOne${camelcase(singular(resource), {
pascalCase: true,
})}`;
const queryVariables: VariableOptions = {};
if (filters) {
queryVariables["input"] = {
type: camelcase(
`delete_one_${singular(resource)}_subscription_filter_input`,
{
pascalCase: true,
},
),
required: true,
value: {
filter: generateFilters(
filters.filter(
(filter: LogicalFilter) => !filter.field.includes("."),
),
),
},
};
}
const { query, variables } = gql.subscription({
operation,
fields: meta.fields.filter(
(field: string | object) => typeof field !== "object",
),
variables: queryVariables,
});
return { query, variables, operation };
};