UNPKG

graphile-build-pg

Version:

Build a GraphQL schema by reflection over a PostgreSQL schema. Easy to customize since it's built with plugins on graphile-build

257 lines (248 loc) 9.36 kB
// @flow import type { Plugin } from "graphile-build"; import debugSql from "./debugSql"; export default (async function PgRowNode(builder, { subscriptions }) { builder.hook( "GraphQLObjectType", (object, build, context) => { const { addNodeFetcherForTypeName, pgSql: sql, gql2pg, pgQueryFromResolveData: queryFromResolveData, pgOmit: omit, pgPrepareAndRun, } = build; const { scope: { isPgRowType, pgIntrospection: table }, } = context; if (!addNodeFetcherForTypeName) { // Node plugin must be disabled. return object; } if (!isPgRowType || !table.namespace || omit(table, "read")) { return object; } const sqlFullTableName = sql.identifier(table.namespace.name, table.name); const primaryKeyConstraint = table.primaryKeyConstraint; if (!primaryKeyConstraint) { return object; } const primaryKeys = primaryKeyConstraint && primaryKeyConstraint.keyAttributes; addNodeFetcherForTypeName( object.name, async ( data, identifiers, resolveContext, parsedResolveInfoFragment, ReturnType, resolveData, resolveInfo ) => { const { pgClient } = resolveContext; const liveRecord = resolveInfo && resolveInfo.rootValue && resolveInfo.rootValue.liveRecord; if (identifiers.length !== primaryKeys.length) { throw new Error("Invalid ID"); } const query = queryFromResolveData( sqlFullTableName, undefined, resolveData, { useAsterisk: false, // Because it's only a single relation, no need }, queryBuilder => { if (subscriptions && table.primaryKeyConstraint) { queryBuilder.selectIdentifiers(table); } primaryKeys.forEach((key, idx) => { queryBuilder.where( sql.fragment`${queryBuilder.getTableAlias()}.${sql.identifier( key.name )} = ${gql2pg( identifiers[idx], primaryKeys[idx].type, primaryKeys[idx].typeModifier )}` ); }); }, resolveContext, resolveInfo && resolveInfo.rootValue ); const { text, values } = sql.compile(query); if (debugSql.enabled) debugSql(text); const { rows: [row], } = await pgPrepareAndRun(pgClient, text, values); if (subscriptions && liveRecord && row) { liveRecord("pg", table, row.__identifiers); } return row; } ); return object; }, ["PgRowNode"] ); builder.hook( "GraphQLObjectType:fields", (fields, build, context) => { const { nodeIdFieldName, getTypeAndIdentifiersFromNodeId, extend, parseResolveInfo, pgGetGqlTypeByTypeIdAndModifier, pgIntrospectionResultsByKind: introspectionResultsByKind, pgSql: sql, gql2pg, graphql: { GraphQLNonNull, GraphQLID }, inflection, pgQueryFromResolveData: queryFromResolveData, pgOmit: omit, describePgEntity, sqlCommentByAddingTags, pgPrepareAndRun, } = build; const { scope: { isRootQuery }, fieldWithHooks, } = context; if (!isRootQuery || !nodeIdFieldName) { return fields; } return extend( fields, introspectionResultsByKind.class.reduce((memo, table) => { // PERFORMANCE: These used to be .filter(...) calls if (!table.namespace) return memo; if (omit(table, "read")) return memo; const TableType = pgGetGqlTypeByTypeIdAndModifier( table.type.id, null ); const sqlFullTableName = sql.identifier( table.namespace.name, table.name ); if (TableType) { const primaryKeyConstraint = table.primaryKeyConstraint; if (!primaryKeyConstraint) { return memo; } const primaryKeys = primaryKeyConstraint && primaryKeyConstraint.keyAttributes; const fieldName = inflection.tableNode(table); memo = extend( memo, { [fieldName]: fieldWithHooks( fieldName, ({ getDataFromParsedResolveInfoFragment }) => { return { description: build.wrapDescription( `Reads a single \`${TableType.name}\` using its globally unique \`ID\`.`, "field" ), type: TableType, args: { [nodeIdFieldName]: { description: build.wrapDescription( `The globally unique \`ID\` to be used in selecting a single \`${TableType.name}\`.`, "arg" ), type: new GraphQLNonNull(GraphQLID), }, }, async resolve(parent, args, resolveContext, resolveInfo) { const { pgClient } = resolveContext; const liveRecord = resolveInfo.rootValue && resolveInfo.rootValue.liveRecord; const nodeId = args[nodeIdFieldName]; try { const { Type, identifiers } = getTypeAndIdentifiersFromNodeId(nodeId); if (Type !== TableType) { throw new Error("Mismatched type"); } if (identifiers.length !== primaryKeys.length) { throw new Error("Invalid ID"); } const parsedResolveInfoFragment = parseResolveInfo(resolveInfo); parsedResolveInfoFragment.args = args; // Allow overriding via makeWrapResolversPlugin const resolveData = getDataFromParsedResolveInfoFragment( parsedResolveInfoFragment, TableType ); const query = queryFromResolveData( sqlFullTableName, undefined, resolveData, { useAsterisk: false, // Because it's only a single relation, no need }, queryBuilder => { if (subscriptions && table.primaryKeyConstraint) { queryBuilder.selectIdentifiers(table); } primaryKeys.forEach((key, idx) => { queryBuilder.where( sql.fragment`${queryBuilder.getTableAlias()}.${sql.identifier( key.name )} = ${gql2pg( identifiers[idx], primaryKeys[idx].type, primaryKeys[idx].typeModifier )}` ); }); }, resolveContext, resolveInfo.rootValue ); const { text, values } = sql.compile(query); if (debugSql.enabled) debugSql(text); const { rows: [row], } = await pgPrepareAndRun(pgClient, text, values); if (liveRecord && row) { liveRecord("pg", table, row.__identifiers); } return row; } catch (e) { return null; } }, }; }, { isPgNodeQuery: true, pgFieldIntrospection: table, } ), }, `Adding row by globally unique identifier field for ${describePgEntity( table )}. You can rename this table via a 'Smart Comment':\n\n ${sqlCommentByAddingTags( table, { name: "newNameHere" } )}` ); } return memo; }, {}), `Adding "row by node ID" fields to root Query type` ); }, ["PgRowNode"] ); }: Plugin);