diff --git a/package.json b/package.json index b78c446..9d81738 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,8 @@ "typescript": "^3.4.4" }, "peerDependencies": { - "graphile-build": "^4.4.2" + "graphile-build": "^4.4.2", + "graphile-build-pg": "^4.4.5" }, "files": [ "build" diff --git a/src/index.ts b/src/index.ts index ec04f85..4beb662 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { import { Plugin } from "graphile-build"; import printFederatedSchema from "./printFederatedSchema"; import { ObjectTypeDefinition, Directive, StringValue } from "./AST"; +import { PgAttribute, QueryBuilder } from "graphile-build-pg"; /** * This plugin installs the schema outlined in the Apollo Federation spec, and @@ -22,8 +23,13 @@ const SchemaExtensionPlugin = makeExtendSchemaPlugin(build => { $$isQuery, $$nodeType, getTypeByName, + scopeByType, inflection, nodeIdFieldName, + pgSql: sql, + parseResolveInfo, + pgQueryFromResolveData: queryFromResolveData, + pgPrepareAndRun, } = build; // Cache let Query: any; @@ -81,25 +87,83 @@ const SchemaExtensionPlugin = makeExtendSchemaPlugin(build => { resolvers: { Query: { _entities(data, { representations }, context, resolveInfo) { + const { pgClient } = context; const { graphile: { fieldContext }, } = resolveInfo; - return representations.map((representation: any) => { + return representations.map(async (representation: any) => { if (!representation || typeof representation !== "object") { throw new Error("Invalid representation"); } + const { __typename, [nodeIdFieldName]: nodeId } = representation; - if (!__typename || typeof nodeId !== "string") { - throw new Error("Failed to interpret representation"); + if (!__typename) { + throw new Error( + "Failed to interpret representation, no typename" + ); + } + if (nodeId) { + if (typeof nodeId !== "string") { + throw new Error( + "Failed to interpret representation, invalid nodeId" + ); + } + return resolveNode( + nodeId, + build, + fieldContext, + data, + context, + resolveInfo + ); + } else { + const type = getTypeByName(__typename); + const { pgIntrospection: table } = scopeByType.get(type); + + if (!table.primaryKeyConstraint) { + throw new Error("Failed to interpret representation"); + } + const { + primaryKeyConstraint: { keyAttributes }, + } = table; + + const whereClause = sql.fragment`(${sql.join( + keyAttributes.map( + (attr: PgAttribute) => + sql.fragment`${sql.identifier(attr.name)} = ${sql.value( + representation[inflection.column(attr)] + )}` + ), + ") and (" + )})`; + + const resolveData = fieldContext.getDataFromParsedResolveInfoFragment( + parseResolveInfo(resolveInfo), + type + ); + + const query = queryFromResolveData( + sql.identifier(table.namespace.name, table.name), + undefined, + resolveData, + { + useAsterisk: false, // Because it's only a single relation, no need + }, + (queryBuilder: QueryBuilder) => { + queryBuilder.where(whereClause); + }, + context, + resolveInfo.rootValue + ); + + const { text, values } = sql.compile(query); + + const { + rows: [row], + } = await pgPrepareAndRun(pgClient, text, values); + + return { [$$nodeType]: __typename, ...row }; } - return resolveNode( - nodeId, - build, - fieldContext, - data, - context, - resolveInfo - ); }); }, @@ -147,6 +211,44 @@ const AddKeyPlugin: Plugin = builder => { return build; }); + builder.hook("GraphQLObjectType", (type, build, context) => { + const { + scope: { pgIntrospection, isPgRowType }, + } = context; + + const { inflection } = build; + + if ( + !( + isPgRowType && + pgIntrospection.isSelectable && + pgIntrospection.namespace && + pgIntrospection.primaryKeyConstraint + ) + ) { + return type; + } + + const primaryKeyNames = pgIntrospection.primaryKeyConstraint.keyAttributes.map( + (attr: PgAttribute) => inflection.column(attr) + ); + + if (!primaryKeyNames.length) { + return type; + } + + const astNode = { + ...ObjectTypeDefinition({ name: type.name }), + ...type.astNode, + }; + + (astNode.directives as any).push( + Directive("key", { fields: StringValue(primaryKeyNames.join(" ")) }) + ); + + return { ...type, astNode } as typeof type; + }); + // Find out what types implement the Node interface builder.hook("GraphQLObjectType:interfaces", (interfaces, build, context) => { const { getTypeByName, inflection, nodeIdFieldName } = build;