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

610 lines (605 loc) 24.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = makeProcField; exports.procFieldDetails = procFieldDetails; var _debugSql = _interopRequireDefault(require("./debugSql")); var _chalk = _interopRequireDefault(require("chalk")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const nullableIf = (GraphQLNonNull, condition, Type) => condition ? Type : new GraphQLNonNull(Type); const firstValue = obj => { let firstKey; for (const k in obj) { if (k[0] !== "_" && k[1] !== "_") { firstKey = k; } } return obj[firstKey]; }; function procFieldDetails(proc, build, options) { const { computed = false, isMutation = false } = options; const { pgIntrospectionResultsByKind: introspectionResultsByKind, pgGetGqlInputTypeByTypeIdAndModifier, pgSql: sql, gql2pg, pgStrictFunctions: strictFunctions, graphql: { GraphQLNonNull }, inflection, describePgEntity, sqlCommentByAddingTags } = build; if (computed && isMutation) { throw new Error("Mutation procedure cannot be computed"); } const sliceAmount = computed ? 1 : 0; const argNames = proc.argTypeIds.reduce((prev, _, idx) => { if (idx >= sliceAmount && ( // Was a .slice() call proc.argModes.length === 0 || // all args are `in` proc.argModes[idx] === "i" || // this arg is `in` proc.argModes[idx] === "b") // this arg is `inout` ) { prev.push(proc.argNames[idx] || ""); } return prev; }, []); const argTypes = proc.argTypeIds.reduce((prev, typeId, idx) => { if (idx >= sliceAmount && ( // Was a .slice() call proc.argModes.length === 0 || // all args are `in` proc.argModes[idx] === "i" || // this arg is `in` proc.argModes[idx] === "b") // this arg is `inout` ) { prev.push(introspectionResultsByKind.typeById[typeId]); } return prev; }, []); const argModesWithOutput = ["o", // OUT, "b", // INOUT "t" // TABLE ]; const outputArgNames = proc.argTypeIds.reduce((prev, _, idx) => { if (argModesWithOutput.includes(proc.argModes[idx])) { prev.push(proc.argNames[idx] || ""); } return prev; }, []); const outputArgTypes = proc.argTypeIds.reduce((prev, typeId, idx) => { if (argModesWithOutput.includes(proc.argModes[idx])) { prev.push(introspectionResultsByKind.typeById[typeId]); } return prev; }, []); const requiredArgCount = Math.max(0, argNames.length - proc.argDefaultsNum); const variantFromName = name => { if (name.match(/(_p|P)atch$/)) { return "patch"; } return null; }; const variantFromTags = (tags, idx) => { const variant = tags[`arg${idx}variant`]; if (variant && variant.match && variant.match(/^[0-9]+$/)) { return parseInt(variant, 10); } return variant; }; const notNullArgCount = proc.isStrict || strictFunctions ? requiredArgCount : 0; const argGqlTypes = argTypes.map((type, idx) => { // TODO: PG10 doesn't support the equivalent of pg_attribute.atttypemod on function return values, but maybe a later version might const variant = variantFromTags(proc.tags, idx) || variantFromName(argNames[idx]); const Type = pgGetGqlInputTypeByTypeIdAndModifier(type.id, variant); if (!Type) { const hint = type.class ? `; this might be because no INSERT column privileges are granted on ${describePgEntity(type.class)}. You can use 'Smart Comments' to tell PostGraphile to instead use the "${_chalk.default.bold.green("base")}" input type which includes all columns:\n\n ${sqlCommentByAddingTags(proc, { [`arg${idx}variant`]: "base" })}\n` : ""; throw new Error(`Could not determine type for argument ${idx} ('${argNames[idx]}') of function ${describePgEntity(proc)}${hint}`); } if (idx >= notNullArgCount) { return Type; } else { return new GraphQLNonNull(Type); } }); // This wants to be compatible with both being field arguments and being // input object fields (e.g. for the aggregates plugin). const inputs = argNames.reduce((memo, argName, argIndex) => { const gqlArgName = inflection.argument(argName, argIndex); memo[gqlArgName] = { type: argGqlTypes[argIndex] }; return memo; }, {}); function makeSqlFunctionCall(rawArgs = {}, { implicitArgs = [], unnest = false } = {}) { const args = isMutation ? rawArgs.input : rawArgs; const sqlArgValues = []; let haveNames = true; for (let argIndex = argNames.length - 1; argIndex >= 0; argIndex--) { const argName = argNames[argIndex]; const gqlArgName = inflection.argument(argName, argIndex); const value = args[gqlArgName]; const variant = variantFromTags(proc.tags, argIndex) || variantFromName(argNames[argIndex]); const sqlValue = gql2pg(value, argTypes[argIndex], variant); if (argIndex + 1 > requiredArgCount && haveNames && value == null) { // No need to pass argument to function continue; } else if (argIndex + 1 > requiredArgCount && haveNames) { const sqlArgName = argName ? sql.identifier(argName) : null; if (sqlArgName) { sqlArgValues.unshift(sql.fragment`${sqlArgName} := ${sqlValue}`); } else { haveNames = false; sqlArgValues.unshift(sqlValue); } } else { sqlArgValues.unshift(sqlValue); } } const functionCall = sql.fragment`${sql.identifier(proc.namespace.name, proc.name)}(${sql.join([...implicitArgs, ...sqlArgValues], ", ")})`; return unnest ? sql.fragment`unnest(${functionCall})` : functionCall; } return { inputs, makeSqlFunctionCall, outputArgNames, outputArgTypes }; } function makeProcField(fieldName, proc, build, options) { const { fieldWithHooks, computed = false, isMutation = false, isRootQuery = false, forceList = false, aggregateWrapper = null, description: overrideDescription = null, pgTypeAndModifierModifier = null } = options; const { pgIntrospectionResultsByKind: introspectionResultsByKind, pgGetGqlTypeByTypeIdAndModifier, getTypeByName, pgSql: sql, parseResolveInfo, getSafeAliasFromResolveInfo, getSafeAliasFromAlias, pg2gql, newWithHooks, pgTweakFragmentForTypeAndModifier, graphql: { GraphQLNonNull, GraphQLList, GraphQLString, GraphQLObjectType, GraphQLInputObjectType, getNamedType, isCompositeType }, inflection, pgQueryFromResolveData: queryFromResolveData, pgAddStartEndCursor: addStartEndCursor, pgViaTemporaryTable: viaTemporaryTable, describePgEntity, sqlCommentByAddingTags, pgField, options: { subscriptions = false, pgForbidSetofFunctionsToReturnNull = false }, pgPrepareAndRun } = build; const { inputs, makeSqlFunctionCall, outputArgNames, outputArgTypes } = procFieldDetails(proc, build, options); let args = inputs; /** * This is the return type the function claims to have; we * should not use it anywhere but these next few lines. */ const baseReturnType = introspectionResultsByKind.typeById[proc.returnTypeId]; /** * This is the return type we treat it as having, e.g. in * case it was modified by wrapping it in an aggregate or * similar. */ const rawReturnType = pgTypeAndModifierModifier ? pgTypeAndModifierModifier(baseReturnType, null)[0] : baseReturnType; /** * This is the type without the array wrapper. */ const returnType = rawReturnType.isPgArray ? rawReturnType.arrayItemType : rawReturnType; const returnTypeTable = introspectionResultsByKind.classById[returnType.classId]; if (!returnType) { throw new Error(`Could not determine return type for function '${proc.name}'`); } let type; const fieldScope = {}; const payloadTypeScope = {}; fieldScope.pgFieldIntrospection = proc; payloadTypeScope.pgIntrospection = proc; let returnFirstValueAsValue = false; const TableType = returnTypeTable && pgGetGqlTypeByTypeIdAndModifier(returnTypeTable.type.id, null); const isTableLike = TableType && isCompositeType(TableType) || false; const isRecordLike = returnType.id === "2249"; if (isTableLike) { if (proc.returnsSet) { if (isMutation) { const innerType = pgForbidSetofFunctionsToReturnNull ? new GraphQLNonNull(TableType) : TableType; type = new GraphQLList(innerType); } else if (forceList) { const innerType = pgForbidSetofFunctionsToReturnNull ? new GraphQLNonNull(TableType) : TableType; type = new GraphQLList(innerType); fieldScope.isPgFieldSimpleCollection = true; } else { const ConnectionType = getTypeByName(inflection.connection(TableType.name)); if (!ConnectionType) { throw new Error(`Do not have a connection type '${inflection.connection(TableType.name)}' for '${TableType.name}' so cannot create procedure field`); } type = ConnectionType; fieldScope.isPgFieldConnection = true; } fieldScope.pgFieldIntrospectionTable = returnTypeTable; payloadTypeScope.pgIntrospectionTable = returnTypeTable; } else { type = TableType; if (rawReturnType.isPgArray) { // Not implementing pgForbidSetofFunctionsToReturnNull here because it's not a set type = new GraphQLList(type); } fieldScope.pgFieldIntrospectionTable = returnTypeTable; payloadTypeScope.pgIntrospectionTable = returnTypeTable; } } else if (isRecordLike) { const RecordType = getTypeByName(inflection.recordFunctionReturnType(proc)); if (!RecordType) { throw new Error(`Do not have a record type '${inflection.recordFunctionReturnType(proc)}' for '${proc.name}' so cannot create procedure field`); } if (proc.returnsSet) { if (isMutation) { type = new GraphQLList(RecordType); } else if (forceList) { type = new GraphQLList(RecordType); fieldScope.isPgFieldSimpleCollection = true; } else { const ConnectionType = getTypeByName(inflection.recordFunctionConnection(proc)); if (!ConnectionType) { throw new Error(`Do not have a connection type '${inflection.recordFunctionConnection(proc)}' for '${RecordType.name}' so cannot create procedure field`); } type = ConnectionType; fieldScope.isPgFieldConnection = true; } } else { type = RecordType; if (rawReturnType.isPgArray) { type = new GraphQLList(type); } } } else { // TODO: PG10 doesn't support the equivalent of pg_attribute.atttypemod on function return values, but maybe a later version might const Type = pgGetGqlTypeByTypeIdAndModifier(returnType.id, null) || GraphQLString; if (proc.returnsSet) { const connectionTypeName = inflection.scalarFunctionConnection(proc); const ConnectionType = getTypeByName(connectionTypeName); if (isMutation) { // Cannot return a connection because it would have to run the mutation again type = new GraphQLList(Type); returnFirstValueAsValue = true; } else if (forceList || !ConnectionType) { type = new GraphQLList(Type); returnFirstValueAsValue = true; fieldScope.isPgFieldSimpleCollection = true; } else { type = ConnectionType; fieldScope.isPgFieldConnection = true; // We don't return the first value as the value here because it gets // sent down into PgScalarFunctionConnectionPlugin so the relevant // EdgeType can return cursor / node; i.e. we might want to add an // `__cursor` field so we can't just use a scalar. } } else { returnFirstValueAsValue = true; type = Type; if (rawReturnType.isPgArray) { type = new GraphQLList(type); } } } return fieldWithHooks(fieldName, ({ addDataGenerator, getDataFromParsedResolveInfoFragment, addArgDataGenerator }) => { if (proc.returnsSet && !isTableLike && !returnFirstValueAsValue && !isMutation) { // Natural ordering addArgDataGenerator(function addPgCursorPrefix() { return { pgCursorPrefix: sql.literal("natural") }; }); } function makeQuery(parsedResolveInfoFragment, ReturnType, sqlMutationQuery, functionAlias, parentQueryBuilder, resolveContext, resolveInfo) { const resolveData = getDataFromParsedResolveInfoFragment(parsedResolveInfoFragment, ReturnType); const isConnection = !forceList && !isMutation && proc.returnsSet; const query = queryFromResolveData(sqlMutationQuery, functionAlias, resolveData, { useAsterisk: !isMutation && (isTableLike || isRecordLike) && (forceList || proc.returnsSet || rawReturnType.isPgArray) && // only bother with lists proc.language !== "sql", // sql functions can be inlined, so GRANTs still apply withPagination: isConnection, withPaginationAsFields: isConnection && !computed, asJson: computed && (forceList || !proc.returnsSet && !returnFirstValueAsValue), asJsonAggregate: computed && (forceList || !proc.returnsSet && rawReturnType.isPgArray), addNullCase: !proc.returnsSet && !rawReturnType.isPgArray && (isTableLike || isRecordLike) }, innerQueryBuilder => { innerQueryBuilder.parentQueryBuilder = parentQueryBuilder; if (!isTableLike) { if (returnTypeTable) { innerQueryBuilder.select(pgTweakFragmentForTypeAndModifier(sql.fragment`${functionAlias}`, returnTypeTable.type, null, resolveData), "value"); } else { innerQueryBuilder.select(pgTweakFragmentForTypeAndModifier(sql.fragment`${functionAlias}`, returnType, null, // We can't determine a type modifier for functions resolveData), "value"); } } else if (subscriptions && returnTypeTable && !isConnection && returnTypeTable.primaryKeyConstraint) { innerQueryBuilder.selectIdentifiers(returnTypeTable); } }, parentQueryBuilder ? parentQueryBuilder.context : resolveContext, parentQueryBuilder ? parentQueryBuilder.rootValue : resolveInfo && resolveInfo.rootValue); return query; } if (computed) { addDataGenerator((parsedResolveInfoFragment, ReturnType) => { return { pgQuery: queryBuilder => { queryBuilder.select(() => { const parentTableAlias = queryBuilder.getTableAlias(); const functionAlias = sql.identifier(Symbol()); const sqlFunctionCall = makeSqlFunctionCall(parsedResolveInfoFragment.args, { implicitArgs: [parentTableAlias], unnest: rawReturnType.isPgArray }); if (aggregateWrapper) { return aggregateWrapper(sqlFunctionCall); } else { const query = makeQuery(parsedResolveInfoFragment, ReturnType, sqlFunctionCall, functionAlias, queryBuilder); return sql.fragment`(${query})`; } }, getSafeAliasFromAlias(parsedResolveInfoFragment.alias)); } }; }); } let ReturnType = type; let PayloadType; if (isMutation) { const resultFieldName = inflection.functionMutationResultFieldName(proc, getNamedType(type), proc.returnsSet || rawReturnType.isPgArray, outputArgNames); const isNotVoid = String(returnType.id) !== "2278"; // If set then plural name PayloadType = newWithHooks(GraphQLObjectType, { name: inflection.functionPayloadType(proc), description: build.wrapDescription(`The output of our \`${inflection.functionMutationName(proc)}\` mutation.`, "type"), fields: ({ fieldWithHooks }) => { return Object.assign({}, { clientMutationId: { type: GraphQLString } }, isNotVoid ? { [resultFieldName]: pgField(build, fieldWithHooks, resultFieldName, { type: type, ...(returnFirstValueAsValue ? { resolve(data) { return data.data; } } : null) }, {}, false, { pgType: returnType }) // Result } : null); } }, { __origin: `Adding mutation function payload type for ${describePgEntity(proc)}. You can rename the function's GraphQL field (and its dependent types) via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(proc, { name: "newNameHere" })}`, isMutationPayload: true, ...payloadTypeScope }); ReturnType = PayloadType; const InputType = newWithHooks(GraphQLInputObjectType, { name: inflection.functionInputType(proc), description: build.wrapDescription(`All input for the \`${inflection.functionMutationName(proc)}\` mutation.`, "type"), fields: { clientMutationId: { type: GraphQLString }, ...args } }, { __origin: `Adding mutation function input type for ${describePgEntity(proc)}. You can rename the function's GraphQL field (and its dependent types) via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(proc, { name: "newNameHere" })}`, isMutationInput: true }); args = { input: { type: new GraphQLNonNull(InputType) } }; } // If this is a table we can process it directly; but if it's a scalar // setof function we must dereference '.value' from it, because this // makes space for '__cursor' to exist alongside it (whereas on a table // the '__cursor' can just be on the table object itself) const scalarAwarePg2gql = v => isTableLike ? pg2gql(v, returnType) : { ...v, value: pg2gql(v.value, returnType) }; return { description: overrideDescription ? overrideDescription : proc.description ? proc.description : isMutation ? null : isTableLike && proc.returnsSet ? build.wrapDescription(`Reads and enables pagination through a set of \`${TableType.name}\`.`, "field") : null, type: nullableIf(GraphQLNonNull, !proc.tags.notNull && (!fieldScope.isPgFieldConnection || isMutation || isRootQuery), ReturnType), args: args, resolve: computed ? (data, _args, resolveContext, resolveInfo) => { const liveRecord = resolveInfo.rootValue && resolveInfo.rootValue.liveRecord; const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); const value = data[safeAlias]; if (returnFirstValueAsValue) { // Is not table like; is not record like. if (proc.returnsSet && !forceList) { // EITHER `isMutation` is true, or `ConnectionType` does not // exist - either way, we're not returning a connection. return value.data.map(v => pg2gql(firstValue(v), returnType)); } else if (proc.returnsSet || rawReturnType.isPgArray) { return value.map(v => pg2gql(firstValue(v), returnType)); } else { return pg2gql(value, returnType); } } else { const makeRecordLive = subscriptions && isTableLike && returnTypeTable && liveRecord ? record => { if (record) { liveRecord("pg", returnTypeTable, record.__identifiers); } } : _record => {}; if (proc.returnsSet && !isMutation && !forceList) { // Connection - do not make live (the connection will handle this) return addStartEndCursor({ ...value, data: value.data ? value.data.map(scalarAwarePg2gql) : null }); } else if (proc.returnsSet || rawReturnType.isPgArray) { // List const records = value.map(v => { makeRecordLive(v); return pg2gql(v, returnType); }); return records; } else { // Object if (value) { makeRecordLive(value); } return pg2gql(value, returnType); } } } : async (data, args, resolveContext, resolveInfo) => { const { pgClient } = resolveContext; const liveRecord = resolveInfo.rootValue && resolveInfo.rootValue.liveRecord; const parsedResolveInfoFragment = parseResolveInfo(resolveInfo); parsedResolveInfoFragment.args = args; // Allow overriding via makeWrapResolversPlugin const functionAlias = sql.identifier(Symbol()); const sqlFunctionCall = makeSqlFunctionCall(parsedResolveInfoFragment.args, { unnest: rawReturnType.isPgArray }); let queryResultRows; if (isMutation) { const query = makeQuery(parsedResolveInfoFragment, resolveInfo.returnType, functionAlias, functionAlias, null, resolveContext, resolveInfo); const intermediateIdentifier = sql.identifier(Symbol()); const isVoid = returnType.id === "2278"; const isPgRecord = returnType.id === "2249"; const isPgClass = !isPgRecord && (!returnFirstValueAsValue || returnTypeTable || false); try { await pgClient.query("SAVEPOINT graphql_mutation"); queryResultRows = await viaTemporaryTable(pgClient, isVoid ? null : sql.identifier(returnType.namespaceName, returnType.name), sql.query`select ${isPgClass ? sql.query`${intermediateIdentifier}.*` : isPgRecord ? sql.query`${intermediateIdentifier}.*` : sql.query`${intermediateIdentifier} as ${functionAlias}`} from ${sqlFunctionCall} ${intermediateIdentifier}`, functionAlias, query, isPgClass, isPgRecord ? { outputArgTypes, outputArgNames } : null); await pgClient.query("RELEASE SAVEPOINT graphql_mutation"); } catch (e) { await pgClient.query("ROLLBACK TO SAVEPOINT graphql_mutation"); throw e; } } else { const query = makeQuery(parsedResolveInfoFragment, resolveInfo.returnType, sqlFunctionCall, functionAlias, null, resolveContext, resolveInfo); const { text, values } = sql.compile(query); if (_debugSql.default.enabled) (0, _debugSql.default)(text); const queryResult = await pgPrepareAndRun(pgClient, text, values); queryResultRows = queryResult.rows; } const rows = queryResultRows; const [row] = rows; const result = (() => { const makeRecordLive = subscriptions && isTableLike && returnTypeTable && liveRecord ? record => { if (record) { liveRecord("pg", returnTypeTable, record.__identifiers); } } : _record => {}; if (returnFirstValueAsValue) { // `returnFirstValueAsValue` implies either `isMutation` is // true, or `ConnectionType` does not exist - either way, // we're not returning a connection. if (proc.returnsSet && !isMutation && !forceList) { return row.data.map(v => { const fv = firstValue(v); makeRecordLive(fv); return pg2gql(fv, returnType); }); } else if (proc.returnsSet || rawReturnType.isPgArray) { return rows.map(v => { const fv = firstValue(v); makeRecordLive(fv); return pg2gql(fv, returnType); }); } else { const fv = firstValue(row); makeRecordLive(fv); const record = pg2gql(fv, returnType); return record; } } else { if (proc.returnsSet && !isMutation && !forceList) { // Connection const data = row.data ? row.data.map(scalarAwarePg2gql) : null; return addStartEndCursor({ ...row, data }); } else if (proc.returnsSet || rawReturnType.isPgArray) { // List return rows.map(row => { makeRecordLive(row); return pg2gql(row, returnType); }); } else { // Object makeRecordLive(row); return pg2gql(row, returnType); } } })(); if (isMutation) { return { clientMutationId: args.input.clientMutationId, data: result }; } else { return result; } } }; }, fieldScope); } //# sourceMappingURL=makeProcField.js.map