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

428 lines (423 loc) 18.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; const base64 = str => Buffer.from(String(str)).toString("base64"); const hasNonNullKey = row => { if (Array.isArray(row.__identifiers) && row.__identifiers.every(i => i != null)) { return true; } for (const k in row) { if (Object.prototype.hasOwnProperty.call(row, k)) { if ((k[0] !== "_" || k[1] !== "_") && row[k] !== null) { return true; } } } return false; }; var PgTablesPlugin = function PgTablesPlugin(builder, { pgForbidSetofFunctionsToReturnNull = false, subscriptions = false }) { const handleNullRow = pgForbidSetofFunctionsToReturnNull ? (row, _identifiers) => row : (row, identifiers) => { if (identifiers && hasNonNullKey(identifiers) || hasNonNullKey(row)) { return row; } else { return null; } }; builder.hook("init", (_, build) => { const { getNodeIdForTypeAndIdentifiers, nodeIdFieldName, newWithHooks, getSafeAliasFromResolveInfo, pgSql: sql, pgIntrospectionResultsByKind: introspectionResultsByKind, getTypeByName, pgGetGqlTypeByTypeIdAndModifier, pgGetGqlInputTypeByTypeIdAndModifier, pgRegisterGqlTypeByTypeId, pgRegisterGqlInputTypeByTypeId, pg2GqlMapper, gql2pg, graphql: { GraphQLObjectType, GraphQLNonNull, GraphQLID, GraphQLList, GraphQLInputObjectType }, inflection, describePgEntity, sqlCommentByAddingTags, pgField } = build; const nullableIf = (condition, Type) => condition ? Type : new GraphQLNonNull(Type); const Cursor = getTypeByName("Cursor"); introspectionResultsByKind.class.forEach(table => { if (table.tags.enum) { return; } const tablePgType = table.type; if (!tablePgType) { throw new Error("Could not determine the type for this table"); } const arrayTablePgType = tablePgType.arrayType; const primaryKeyConstraint = table.primaryKeyConstraint; const primaryKeys = primaryKeyConstraint && primaryKeyConstraint.keyAttributes; const attributes = table.attributes; const tableTypeName = inflection.tableType(table); const shouldHaveNodeId = nodeIdFieldName && table.isSelectable && table.namespace && primaryKeys && primaryKeys.length ? true : false; let TableType; let TablePatchType; let TableBaseInputType; pgRegisterGqlTypeByTypeId(tablePgType.id, cb => { if (TableType) { return TableType; } if (pg2GqlMapper[tablePgType.id]) { // Already handled throw new Error(`Register was called but there's already a mapper in place for '${tablePgType.id}'!`); } TableType = newWithHooks(GraphQLObjectType, { description: table.description || tablePgType.description, name: tableTypeName, interfaces: () => { if (shouldHaveNodeId) { return [getTypeByName(inflection.builtin("Node"))]; } else { return []; } }, fields: ({ addDataGeneratorForField, Self }) => { const fields = {}; if (shouldHaveNodeId) { // Enable nodeId interface addDataGeneratorForField(nodeIdFieldName, () => { return { pgQuery: queryBuilder => { queryBuilder.selectIdentifiers(table); } }; }); fields[nodeIdFieldName] = { description: build.wrapDescription("A globally unique identifier. Can be used in various places throughout the system to identify this single value.", "field"), type: new GraphQLNonNull(GraphQLID), resolve(data) { const identifiers = data.__identifiers; if (!identifiers) { return null; } /* * For bigint we want NodeIDs to be the same as int up * to the limits of int, and only to be strings after * that point. */ const finalIdentifiers = identifiers.map((identifier, idx) => { const key = primaryKeys[idx]; const type = key.type.domainBaseType || key.type; if (type.id === "20" /* bigint */) { /* * When migrating from 'int' to 'bigint' we want * to maintain nodeIDs in the safe range before * moving to strings for larger numbers. Since we * can represent ints up to MAX_SAFE_INTEGER * (2^53 - 1) fine, we're using that as the * boundary. */ const int = parseInt(identifier, 10); if (int >= -Number.MAX_SAFE_INTEGER && int <= Number.MAX_SAFE_INTEGER) { return int; } } return identifier; }); return getNodeIdForTypeAndIdentifiers(Self, ...finalIdentifiers); } }; } return fields; } }, { __origin: `Adding table type for ${describePgEntity(table)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(table, { name: "newNameHere" })}`, pgIntrospection: table, isPgRowType: table.isSelectable, isPgCompoundType: !table.isSelectable, // TODO:v5: remove - typo isPgCompositeType: !table.isSelectable }); cb(TableType); const pgCreateInputFields = {}; const pgPatchInputFields = {}; const pgBaseInputFields = {}; newWithHooks(GraphQLInputObjectType, { description: build.wrapDescription(`An input for mutations affecting \`${tableTypeName}\``, "type"), name: inflection.inputType(TableType) }, { __origin: `Adding table input type for ${describePgEntity(table)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(table, { name: "newNameHere" })}`, pgIntrospection: table, isInputType: true, isPgRowType: table.isSelectable, isPgCompoundType: !table.isSelectable, pgAddSubfield(fieldName, attrName, pgType, spec, typeModifier) { pgCreateInputFields[fieldName] = { name: attrName, type: pgType, typeModifier }; return spec; } }, true // If no fields, skip type automatically ); if (table.isSelectable) { // XXX: these don't belong here; but we have to keep them here // because third-party code depends on `getTypeByName` to find // them; so we have to register them ahead of time. A better // approach is to use the modifier to specify the type you need, // 'patch' or 'base', so they can be registered just in time. TablePatchType = newWithHooks(GraphQLInputObjectType, { description: build.wrapDescription(`Represents an update to a \`${tableTypeName}\`. Fields that are set will be updated.`, "type"), name: inflection.patchType(TableType) }, { __origin: `Adding table patch type for ${describePgEntity(table)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(table, { name: "newNameHere" })}`, pgIntrospection: table, isPgRowType: table.isSelectable, isPgCompoundType: !table.isSelectable, isPgPatch: true, pgAddSubfield(fieldName, attrName, pgType, spec, typeModifier) { pgPatchInputFields[fieldName] = { name: attrName, type: pgType, typeModifier }; return spec; } }, true // Safe to skip this if no fields support updating ); TableBaseInputType = newWithHooks(GraphQLInputObjectType, { description: build.wrapDescription(`An input representation of \`${tableTypeName}\` with nullable fields.`, "type"), name: inflection.baseInputType(TableType) }, { __origin: `Adding table base input type for ${describePgEntity(table)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(table, { name: "newNameHere" })}`, pgIntrospection: table, isPgRowType: table.isSelectable, isPgCompoundType: !table.isSelectable, isPgBaseInput: true, pgAddSubfield(fieldName, attrName, pgType, spec, typeModifier) { pgBaseInputFields[fieldName] = { name: attrName, type: pgType, typeModifier }; return spec; } }); } pg2GqlMapper[tablePgType.id] = { map: _ => _, unmap: (obj, modifier) => { let fieldLookup; if (modifier === "patch") { fieldLookup = pgPatchInputFields; } else if (modifier === "base") { fieldLookup = pgBaseInputFields; } else { fieldLookup = pgCreateInputFields; } const attr2sql = attr => { // TODO: this should use `fieldInput[*].name` to find the attribute const fieldName = inflection.column(attr); const inputField = fieldLookup[fieldName]; const v = obj[fieldName]; if (inputField && v != null) { const { type, typeModifier } = inputField; return sql.fragment`${gql2pg(v, type, typeModifier)}::${type.isFake ? sql.identifier("unknown") : sql.identifier(type.namespaceName, type.name)}`; } else { return sql.null; // TODO: return default instead. } }; return sql.fragment`row(${sql.join(attributes.map(attr2sql), ",")})::${tablePgType.isFake ? sql.identifier("unknown") : sql.identifier(tablePgType.namespaceName, tablePgType.name)}`; } }; const EdgeType = newWithHooks(GraphQLObjectType, { description: build.wrapDescription(`A \`${tableTypeName}\` edge in the connection.`, "type"), name: inflection.edge(TableType.name), fields: ({ fieldWithHooks }) => { return { cursor: fieldWithHooks("cursor", ({ addDataGenerator }) => { addDataGenerator(() => ({ usesCursor: [true], pgQuery: queryBuilder => { if (primaryKeys) { queryBuilder.selectIdentifiers(table); } } })); return { description: build.wrapDescription("A cursor for use in pagination.", "field"), type: Cursor, resolve(data) { return data.__cursor && base64(JSON.stringify(data.__cursor)); } }; }, { isCursorField: true }), node: pgField(build, fieldWithHooks, "node", { description: build.wrapDescription(`The \`${tableTypeName}\` at the end of the edge.`, "field"), type: nullableIf(!pgForbidSetofFunctionsToReturnNull, TableType), resolve(data, _args, resolveContext, resolveInfo) { const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); const record = handleNullRow(data[safeAlias], data.__identifiers); const liveRecord = resolveInfo.rootValue && resolveInfo.rootValue.liveRecord; if (record && primaryKeys && liveRecord && data.__identifiers) { liveRecord("pg", table, data.__identifiers); } return record; } }, {}, false, { withQueryBuilder: queryBuilder => { if (subscriptions) { queryBuilder.selectIdentifiers(table); } } }) }; } }, { __origin: `Adding table edge type for ${describePgEntity(table)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(table, { name: "newNameHere" })}`, isEdgeType: true, isPgRowEdgeType: true, nodeType: TableType, pgIntrospection: table }); const PageInfo = getTypeByName(inflection.builtin("PageInfo")); /*const ConnectionType = */ newWithHooks(GraphQLObjectType, { description: build.wrapDescription(`A connection to a list of \`${tableTypeName}\` values.`, "type"), name: inflection.connection(TableType.name), fields: ({ recurseDataGeneratorsForField, fieldWithHooks }) => { recurseDataGeneratorsForField("pageInfo", true); return { nodes: pgField(build, fieldWithHooks, "nodes", { description: build.wrapDescription(`A list of \`${tableTypeName}\` objects.`, "field"), type: new GraphQLNonNull(new GraphQLList(nullableIf(!pgForbidSetofFunctionsToReturnNull, TableType))), resolve(data, _args, resolveContext, resolveInfo) { const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); const liveRecord = resolveInfo.rootValue && resolveInfo.rootValue.liveRecord; return data.data.map(entry => { const record = handleNullRow(entry[safeAlias], entry[safeAlias].__identifiers); if (record && liveRecord && primaryKeys && entry[safeAlias].__identifiers) { liveRecord("pg", table, entry[safeAlias].__identifiers); } return record; }); } }, {}, false, { withQueryBuilder: queryBuilder => { if (subscriptions) { queryBuilder.selectIdentifiers(table); } } }), edges: pgField(build, fieldWithHooks, "edges", { description: build.wrapDescription(`A list of edges which contains the \`${tableTypeName}\` and cursor to aid in pagination.`, "field"), type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(EdgeType))), resolve(data, _args, _context, resolveInfo) { const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); return data.data.map(entry => ({ ...entry, ...entry[safeAlias] })); } }, {}, false, { hoistCursor: true }), pageInfo: PageInfo && { description: build.wrapDescription("Information to aid in pagination.", "field"), type: new GraphQLNonNull(PageInfo), resolve(data) { return data; } } }; } }, { __origin: `Adding table connection type for ${describePgEntity(table)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(table, { name: "newNameHere" })}`, isConnectionType: true, isPgRowConnectionType: true, edgeType: EdgeType, nodeType: TableType, pgIntrospection: table }); }, true); pgRegisterGqlInputTypeByTypeId(tablePgType.id, (_set, modifier) => { // This must come first, it triggers creation of all the types const TableType = pgGetGqlTypeByTypeIdAndModifier(tablePgType.id, null); // This must come after the pgGetGqlTypeByTypeIdAndModifier call if (modifier === "patch") { // TODO: v5: move the definition from above down here return TablePatchType; } if (modifier === "base") { // TODO: v5: move the definition from above down here return TableBaseInputType; } if (TableType) { return getTypeByName(inflection.inputType(TableType)); } return null; }, true); if (arrayTablePgType) { // Note: these do not return // // `new GraphQLList(new GraphQLNonNull(...))` // // because it's possible to return null entries from postgresql // functions. We should probably add a flag to instead export // the non-null version as that's more typical. pgRegisterGqlTypeByTypeId(arrayTablePgType.id, () => { const TableType = pgGetGqlTypeByTypeIdAndModifier(tablePgType.id, null); return new GraphQLList(TableType); }, true); pgRegisterGqlInputTypeByTypeId(arrayTablePgType.id, (_set, modifier) => { const RelevantTableInputType = pgGetGqlInputTypeByTypeIdAndModifier(tablePgType.id, modifier); if (RelevantTableInputType) { return new GraphQLList(RelevantTableInputType); } }, true); } }); return _; }, ["PgTables"], [], ["PgTypes"]); }; exports.default = PgTablesPlugin; //# sourceMappingURL=PgTablesPlugin.js.map