-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Open
Labels
enhancementNew feature or requestNew feature or request
Description
Is your feature request related to a problem? Please describe.
Hi, I'm building an application with Refine and I would like to use Apollo.io as my data source. I noticed that there isn't an official or community-built Apollo.io data provider available. Is there one in development, or could you provide guidance on how to create a custom one for their API?
Describe alternatives you've considered
In fact, I have already developed a working solution for my own use. I would be happy to contribute it as a starting point for an official connector, with the understanding that the code would need to be improved and aligned with your standards. Would you be interested in this?
Additional context
No response
Describe the thing to improve
I have create this custom apollo provider
import { ApolloClient, gql } from '@apollo/client';
import {
ConditionalFilter,
CrudFilter,
DataProvider,
LogicalFilter,
MetaDataQuery,
NestedField,
Fields,
QueryBuilderOptions,
} from '@refinedev/core';
type RelationStructure = {
fields: Fields;
relations?: Record<string, Fields | RelationStructure>;
};
type QueryStructure = {
fields: Fields;
relations?: Record<string, Fields | RelationStructure>;
};
interface CustomMetaDataQuery extends MetaDataQuery {
queryStructure?: QueryStructure;
}
function normalizeFields(
fields: string | QueryBuilderOptions[] | Fields | RelationStructure,
): (string | NestedField)[] {
if (typeof fields === 'string') {
return [fields];
}
if (Array.isArray(fields)) {
return fields as (string | NestedField)[];
}
if ('fields' in fields) {
return normalizeFields(fields.fields);
}
return [];
}
function fieldsToString(
fields: string | QueryBuilderOptions[] | Fields | RelationStructure,
): string {
const normalizedFields = normalizeFields(fields);
return normalizedFields
.map((field) => {
if (typeof field === 'string') {
return field;
}
return Object.entries(field)
.map(([key, value]) => {
return `${key} {\n${fieldsToString(value)}\n}`;
})
.join('\n');
})
.join('\n ');
}
function buildGraphQLFields(queryDef: QueryStructure): string {
const { fields, relations } = queryDef;
let query = fieldsToString(fields);
if (relations) {
for (const [relationName, relationDef] of Object.entries(relations)) {
if (!relationDef) continue;
query += `\n ${relationName} {\n ${fieldsToString(relationDef)}\n }`;
}
}
return query;
}
function getFieldsToUse(meta?: CustomMetaDataQuery): string {
if (!meta) return 'id';
if (meta.queryStructure) {
return buildGraphQLFields(meta.queryStructure);
}
if (meta.fields) {
return fieldsToString(meta.fields);
}
return 'id';
}
const isLogicalFilter = (filter: CrudFilter): filter is LogicalFilter => {
return 'field' in filter && 'operator' in filter && 'value' in filter;
};
const isConditionalFilter = (filter: CrudFilter): filter is ConditionalFilter => {
return (
'operator' in filter &&
(filter.operator === 'and' || filter.operator === 'or') &&
'value' in filter
);
};
const buildWhereClause = (filter: CrudFilter): Record<string, any> => {
if (isLogicalFilter(filter)) {
if (filter.value === '' || filter.value == null) {
return {};
}
const operatorMap: Record<string, string> = {
eq: '_eq',
ne: '_neq',
lt: '_lt',
gt: '_gt',
lte: '_lte',
gte: '_gte',
contains: '_ilike',
in: '_in',
null: '_is_null',
};
const hasuraOperator = operatorMap[filter.operator] || '_eq';
if (filter.operator === 'contains') {
return { [filter.field]: { [hasuraOperator]: `%${filter.value}%` } };
}
if (filter.operator === 'null') {
return { [filter.field]: { [hasuraOperator]: true } };
}
return { [filter.field]: { [hasuraOperator]: filter.value } };
}
if (isConditionalFilter(filter)) {
return {
[filter.operator]: filter.value.map(buildWhereClause),
};
}
return {};
};
export const createApolloDataProvider = (client: ApolloClient<any>): DataProvider => ({
getList: async ({ resource, pagination, filters, sorters, meta }) => {
const { current = 1, pageSize = 20, mode = 'server' } = pagination || {};
const whereClause = filters?.reduce(
(acc: Record<string, any>, filter) => ({
...acc,
...buildWhereClause(filter),
}),
{},
);
const orderBy = sorters?.map((sorter) => ({
[sorter.field]: sorter.order.toLowerCase(),
}));
const baseVariables: {
limit?: number;
offset?: number;
where: Record<string, any>;
order_by?: any[];
} & Record<string, any> = {
where: whereClause || {},
...(orderBy && orderBy.length > 0 ? { order_by: orderBy } : {}),
};
if (mode === 'server') {
baseVariables.limit = pageSize;
baseVariables.offset = (current - 1) * pageSize;
}
if (meta?.variables) {
for (const [key, def] of Object.entries(meta.variables)) {
baseVariables[key] = def.value;
}
}
const fieldsToUse = getFieldsToUse(meta);
const paginationParams = mode === 'server' ? `$limit: Int, $offset: Int,` : '';
const paginationArgs = mode === 'server' ? `limit: $limit, offset: $offset,` : '';
const queryText = `query Get${resource}List(
${paginationParams}
$where: ${resource}_bool_exp
${orderBy && orderBy.length > 0 ? '$order_by: [' + resource + '_order_by!]' : ''}
${
meta?.variables
? Object.entries(meta.variables)
.map(([key, def]) => `$${key}: ${def.type}`)
.join('\n ')
: ''
}
) {
${resource}(
${paginationArgs}
where: $where
${orderBy && orderBy.length > 0 ? 'order_by: $order_by' : ''}
) {
${fieldsToUse}
}
${
mode === 'server'
? `${resource}_aggregate(where: $where) {
aggregate {
count
}
}`
: ''
}
}`;
const query = gql(queryText);
const { data } = await client.query({
query,
variables: baseVariables,
});
const result = {
data: data[resource],
total:
mode === 'server'
? data[`${resource}_aggregate`]?.aggregate?.count || 0
: data[resource].length,
};
if (mode === 'client') {
const startIndex = (current - 1) * pageSize;
const endIndex = startIndex + pageSize;
result.data = result.data.slice(startIndex, endIndex);
result.total = data[resource].length;
}
return result;
},
getOne: async ({ resource, id, meta }) => {
const fieldsToUse = getFieldsToUse(meta);
const baseVariables: { id: any } & Record<string, any> = { id };
let additionalParams = '';
let additionalArgs = '';
if (meta?.variables) {
for (const [key, def] of Object.entries(meta.variables)) {
baseVariables[key] = def.value;
additionalParams += `$${key}: ${def.type}, `;
additionalArgs += `${key}: $${key}, `;
}
}
const queryText = `query Get${resource}ById($id: Int!, ${additionalParams}) {
${resource}_by_pk(id: $id, ${additionalArgs}) {
${fieldsToUse}
}
}`;
const query = gql(queryText);
const { data } = await client.query({
query,
variables: baseVariables,
});
return { data: data[`${resource}_by_pk`] };
},
create: async ({ resource, variables, meta }) => {
const fieldsToUse = getFieldsToUse(meta);
const mutationText = `mutation Create${resource}($object: ${resource}_insert_input!) {
insert_${resource}_one(object: $object) {
${fieldsToUse}
}
}`;
const mutation = gql(mutationText);
const { data } = await client.mutate({
mutation,
variables: { object: variables },
});
return { data: data[`insert_${resource}_one`] };
},
update: async ({ resource, id, variables, meta }) => {
const fieldsToUse = getFieldsToUse(meta);
const pk_columns = meta?.pk_columns ? { id, ...meta.pk_columns } : { id };
const mutationText = `mutation Update${resource}(
$pk_columns: ${resource}_pk_columns_input!
$_set: ${resource}_set_input
) {
update_${resource}_by_pk(pk_columns: $pk_columns, _set: $_set) {
${fieldsToUse}
}
}`;
const mutation = gql(mutationText);
const { data } = await client.mutate({
mutation,
variables: {
pk_columns,
_set: variables,
},
});
return { data: data[`update_${resource}_by_pk`] };
},
deleteOne: async ({ resource, id, meta }) => {
const fieldsToUse = getFieldsToUse(meta);
const mutationText = `mutation Delete${resource}($id: Int!) {
delete_${resource}_by_pk(id: $id) {
${fieldsToUse}
}
}`;
const mutation = gql(mutationText);
const { data } = await client.mutate({
mutation,
variables: { id },
});
return { data: data[`delete_${resource}_by_pk`] };
},
getApiUrl: () => {
return import.meta.env.VITE_API_URL;
},
});
import { CrudOperators, Fields, MetaDataQuery, NestedField } from '@refinedev/core';
import { Maybe } from 'graphql/jsutils/Maybe';
export type ExtractRelationType<T> =
T extends Maybe<infer U> ? U : T extends Array<infer U> ? U : T;
export type ExtractRelations<T> = {
[K in keyof T]: T[K] extends Maybe<infer U>
? U extends Record<string, any>
? U
: never
: T[K] extends Array<infer U>
? U extends Record<string, any>
? U
: never
: T[K] extends Record<string, any>
? T[K]
: never;
};
type ScalarFields<T> = {
[K in keyof T]: T[K] extends Maybe<infer U>
? U extends Record<string, any>
? never
: K
: T[K] extends Array<infer U>
? U extends Record<string, any>
? never
: K
: T[K] extends Record<string, any>
? never
: K;
}[keyof T];
type FieldSelection<T> = ReadonlyArray<ScalarFields<T>>;
export type RelationDef<T> = {
fields: FieldSelection<T>;
relations?: RelationSelection<T>;
};
type RelationSelection<T> = {
[K in keyof ExtractRelations<T>]?: T[K] extends Array<infer U>
? FieldSelection<U> | RelationDef<U> | Fields | NestedField
:
| FieldSelection<ExtractRelations<T>[K]>
| RelationDef<ExtractRelations<T>[K]>
| Fields
| NestedField;
};
export type QueryStructure<TEntity> = {
fields: FieldSelection<TEntity> | Fields;
relations?: RelationSelection<TEntity>;
};
export type GraphQLVariableType =
| 'String'
| 'String!'
| 'Int'
| 'Int!'
| 'Float'
| 'Float!'
| 'Boolean'
| 'Boolean!'
| 'ID'
| 'ID!'
| string; // Per tipi custom come enum
export type GraphQLVariable<T = any> = {
type: GraphQLVariableType;
value: T;
};
export type GraphQLVariables = Record<string, GraphQLVariable>;
export interface TypedMetaDataQuery<TEntity = any> extends MetaDataQuery {
queryStructure?: QueryStructure<TEntity>;
variables?: GraphQLVariables;
}
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request