Skip to content

Instantly share code, notes, and snippets.

@hendrikniemann
Last active November 2, 2020 08:23
Show Gist options
  • Save hendrikniemann/83648da05161a36e84c0ee4ab61ad605 to your computer and use it in GitHub Desktop.
Save hendrikniemann/83648da05161a36e84c0ee4ab61ad605 to your computer and use it in GitHub Desktop.
React Admin + Postgraphile Data Provider
// TODO: Skip tables without an id field?
module.exports = function LoadByIdsPlugin(builder) {
builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => {
const {
scope: { isPgCondition, pgIntrospection: table },
fieldWithHooks,
} = context;
if (!isPgCondition || !table || table.kind !== 'class') {
return fields;
}
const conditionFieldSpec = {
type: new build.graphql.GraphQLList(
new build.graphql.GraphQLNonNull(build.graphql.GraphQLInt),
),
description:
'Filters this list by checking if the id of the entity is inside of this array of ids.',
};
return build.extend(fields, {
ids: fieldWithHooks('ids', conditionFieldSpec, {
addPgTableCondition: {
schemaName: table.namespaceName,
tableName: table.name,
conditionFieldSpec,
conditionFieldName: 'ids',
},
}),
});
});
builder.hook('GraphQLObjectType:fields:field:args', (args, build, context) => {
const { pgSql: sql } = build;
const {
scope: {
isPgFieldConnection,
pgFieldIntrospection: procOrTable,
pgFieldIntrospectionTable: tableIfProc,
},
addArgDataGenerator,
} = context;
const table = tableIfProc || procOrTable;
if (!isPgFieldConnection || !table || table.kind !== 'class') {
return args;
}
addArgDataGenerator(function conditionSQLBuilder({ condition }) {
if (!condition) {
return {};
}
const { ids } = condition;
return {
pgQuery: queryBuilder => {
queryBuilder.where(sql.fragment`id IN (${sql.join(ids.map(sql.value), ', ')})`);
},
};
});
return args;
});
};
import { gql } from 'apollo-boost';
import { camelCase, constantCase } from 'change-case';
import { GET_LIST, GET_ONE, GET_MANY, CREATE, UPDATE, DELETE } from 'react-admin';
import { plural } from 'pluralize';
export default function buildQuery(introspectionResults) {
return (raFetchType, resourceName, params) => {
const gqlType = introspectionResults.types.find(
type =>
type.name.toLowerCase() === resourceName.toLowerCase() ||
type.name.toLowerCase() + 's' === resourceName.toLowerCase(),
);
if (!gqlType) {
const error = new Error(`Resource ${resourceName} not found.`);
// @ts-ignore
error.status = 404;
throw error;
}
const queryFields = gqlType.fields.filter(isSimpleField);
switch (raFetchType) {
case GET_LIST:
return {
query: gql`
query reactAdminGetList${gqlType.name}(
$first: Int,
$offset: Int,
$orderBy: [${getOrderByTypeName(gqlType.name)}!],
) {
data: all${plural(
gqlType.name,
)}(first: $first, offset: $offset, orderBy: $orderBy) {
edges {
node {
${queryFields.map(field => field.name).join(' ')}
}
}
totalCount
}
}
`,
variables: {
offset: (params.pagination.page - 1) * params.pagination.perPage,
first: params.pagination.perPage,
orderBy: [constantCase(params.sort.field) + '_' + params.sort.order.toUpperCase()],
},
parseResponse: response => ({
data: response.data.data.edges.map(edge => edge.node),
total: response.data.data.totalCount,
}),
};
case GET_ONE:
return {
query: gql`
query reactAdminGetOne${gqlType.name}($id: Int!) {
data: ${camelCase(gqlType.name)}ById(id: $id) {
${queryFields.map(field => field.name).join(' ')}
}
}
`,
variables: { id: parseInt(params.id, 10) },
parseResponse: response => response.data,
};
case GET_MANY:
return {
query: gql`
query reactAdminGetMany${gqlType.name}($ids: [Int!]!) {
data: all${plural(gqlType.name)}(condition: { ids: $ids }) {
edges {
node {
${queryFields.map(field => field.name).join(' ')}
}
}
totalCount
}
}
`,
variables: {
ids: params.ids.map(id => parseInt(id, 10)),
},
parseResponse: response => {
const results = response.data.data.edges.map(edge => edge.node);
const data = params.ids.map(id => results.find(res => res.id === parseInt(id, 10)));
return { data };
},
};
case CREATE:
return {
query: gql`
mutation reactAdminCreate${gqlType.name}($input: Create${gqlType.name}Input!) {
result: create${gqlType.name}(input: $input) {
entity: ${camelCase(gqlType.name)} {
${queryFields.map(field => field.name).join(' ')}
}
}
}
`,
variables: { input: { [camelCase(gqlType.name)]: params.data } },
parseResponse: response => ({ data: response.data.result.entity }),
};
case UPDATE:
const { id, __typename, createdAt, updatedAt, ...patch } = params.data;
patch.updatedAt = new Date().toISOString();
return {
query: gql`
mutation reactAdminUpdate${gqlType.name}($input: Update${gqlType.name}ByIdInput!) {
result: update${gqlType.name}ById(input: $input) {
entity: ${camelCase(gqlType.name)} {
${queryFields.map(field => field.name).join(' ')}
}
}
}
`,
variables: { input: { id, [camelCase(gqlType.name) + 'Patch']: patch } },
parseResponse: response => ({ data: response.data.result.entity }),
};
case DELETE:
return {
query: gql`
mutation reactAdminDelete${gqlType.name}($id: Int!) {
result: delete${gqlType.name}ById(input: { id: $id }) {
id: deleted${gqlType.name}Id
}
}
`,
variables: { id: parseInt(params.id, 10) },
parseResponse: response => {
if (response.data.result.id === null) {
const error = new Error('Could not delete resource. Unknown Error');
error.status = 500;
throw error;
}
return { data: null };
},
};
default: {
const error = new Error(`Fetch type ${raFetchType} not supported for ${resourceName}.`);
// @ts-ignore
error.status = 404;
throw error;
}
}
};
}
function isSimpleField(field) {
if (field.name === 'nodeId') {
return false;
}
// if (field.name.endsWith('Id')) {
// return false;
// }
return (
field.type.kind === 'ENUM' ||
field.type.kind === 'SCALAR' ||
(field.type.kind === 'NON_NULL' &&
(field.type.ofType.kind === 'ENUM' || field.type.ofType.kind === 'SCALAR'))
);
}
function getOrderByTypeName(typename) {
return plural(typename) + 'OrderBy';
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment