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
347 lines (344 loc) • 16.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _debug = _interopRequireDefault(require("debug"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const debug = (0, _debug.default)("graphile-build-pg");
var PgMutationUpdateDeletePlugin = async function PgMutationUpdateDeletePlugin(builder, {
pgDisableDefaultMutations
}) {
if (pgDisableDefaultMutations) {
return;
}
builder.hook("GraphQLObjectType:fields", (fields, build, context) => {
const {
newWithHooks,
getNodeIdForTypeAndIdentifiers,
getTypeAndIdentifiersFromNodeId,
nodeIdFieldName,
fieldDataGeneratorsByType,
extend,
parseResolveInfo,
getTypeByName,
gql2pg,
pgGetGqlTypeByTypeIdAndModifier,
pgGetGqlInputTypeByTypeIdAndModifier,
pgIntrospectionResultsByKind: introspectionResultsByKind,
pgSql: sql,
graphql: {
GraphQLNonNull,
GraphQLInputObjectType,
GraphQLString,
GraphQLObjectType,
GraphQLID
},
pgColumnFilter,
inflection,
pgQueryFromResolveData: queryFromResolveData,
pgOmit: omit,
pgViaTemporaryTable: viaTemporaryTable,
describePgEntity,
sqlCommentByAddingTags,
pgField
} = build;
const {
scope: {
isRootMutation
},
fieldWithHooks
} = context;
if (!isRootMutation) {
return fields;
}
return extend(fields, ["update", "delete"].reduce((outerMemo, mode) => introspectionResultsByKind.class.reduce((memo, table) => {
// PERFORMANCE: These used to be .filter(...) calls
if (!table.namespace) return memo;
const canUpdate = mode === "update" && table.isUpdatable && !omit(table, "update") &&
// Check at least one attribute is updatable
table.attributes.find(attr => !omit(attr, "update"));
const canDelete = mode === "delete" && table.isDeletable && !omit(table, "delete");
if (!canUpdate && !canDelete) return memo;
const TableType = pgGetGqlTypeByTypeIdAndModifier(table.type.id, null);
if (!TableType) {
return memo;
}
async function commonCodeRenameMe(pgClient, resolveInfo, getDataFromParsedResolveInfoFragment, PayloadType, args, condition, context, resolveContext) {
const {
input
} = args;
const parsedResolveInfoFragment = parseResolveInfo(resolveInfo);
parsedResolveInfoFragment.args = args; // Allow overriding via makeWrapResolversPlugin
const resolveData = getDataFromParsedResolveInfoFragment(parsedResolveInfoFragment, PayloadType);
const sqlTypeIdentifier = sql.identifier(table.namespace.name, table.name);
let sqlMutationQuery;
if (mode === "update") {
const sqlColumns = [];
const sqlValues = [];
const inputData = input[inflection.patchField(inflection.tableFieldName(table))];
table.attributes.forEach(attr => {
// PERFORMANCE: These used to be .filter(...) calls
if (!pgColumnFilter(attr, build, context)) return;
if (omit(attr, "update")) return;
const fieldName = inflection.column(attr);
if (fieldName in inputData /* Because we care about null! */) {
const val = inputData[fieldName];
sqlColumns.push(sql.identifier(attr.name));
sqlValues.push(gql2pg(val, attr.type, attr.typeModifier));
}
});
if (sqlColumns.length === 0) {
return null;
}
sqlMutationQuery = sql.query`\
update ${sql.identifier(table.namespace.name, table.name)} set ${sql.join(sqlColumns.map((col, i) => sql.fragment`${col} = ${sqlValues[i]}`), ", ")}
where ${condition}
returning *`;
} else {
sqlMutationQuery = sql.query`\
delete from ${sql.identifier(table.namespace.name, table.name)}
where ${condition}
returning *`;
}
const modifiedRowAlias = sql.identifier(Symbol());
const query = queryFromResolveData(modifiedRowAlias, modifiedRowAlias, resolveData, {}, null, resolveContext, resolveInfo.rootValue);
let row;
try {
await pgClient.query("SAVEPOINT graphql_mutation");
const rows = await viaTemporaryTable(pgClient, sqlTypeIdentifier, sqlMutationQuery, modifiedRowAlias, query);
row = rows[0];
await pgClient.query("RELEASE SAVEPOINT graphql_mutation");
} catch (e) {
await pgClient.query("ROLLBACK TO SAVEPOINT graphql_mutation");
throw e;
}
if (!row) {
throw new Error(`No values were ${mode}d in collection '${inflection.pluralize(inflection._singularizedTableName(table))}' because no values you can ${mode} were found matching these criteria.`);
}
return {
clientMutationId: input.clientMutationId,
data: row
};
}
if (TableType) {
const uniqueConstraints = table.constraints.filter(con => con.type === "u" || con.type === "p");
const Table = pgGetGqlTypeByTypeIdAndModifier(table.type.id, null);
const tableTypeName = Table.name;
const TablePatch = getTypeByName(inflection.patchType(Table.name));
if (mode === "update" && !TablePatch) {
return memo;
}
const PayloadType = newWithHooks(GraphQLObjectType, {
name: inflection[mode === "delete" ? "deletePayloadType" : "updatePayloadType"](table),
description: build.wrapDescription(`The output of our ${mode} \`${tableTypeName}\` mutation.`, "type"),
fields: ({
fieldWithHooks
}) => {
const tableName = inflection.tableFieldName(table);
// This should really be `-node-id` but for compatibility with PostGraphQL v3 we haven't made that change.
const deletedNodeIdFieldName = inflection.deletedNodeId(table);
return Object.assign({
clientMutationId: {
description: build.wrapDescription("The exact same `clientMutationId` that was provided in the mutation input, unchanged and unused. May be used by a client to track mutations.", "field"),
type: GraphQLString
},
[tableName]: pgField(build, fieldWithHooks, tableName, {
description: build.wrapDescription(`The \`${tableTypeName}\` that was ${mode}d by this mutation.`, "field"),
type: Table
}, {}, false)
}, mode === "delete" ? {
[deletedNodeIdFieldName]: fieldWithHooks(deletedNodeIdFieldName, ({
addDataGenerator
}) => {
const fieldDataGeneratorsByTableType = fieldDataGeneratorsByType.get(TableType);
const gens = fieldDataGeneratorsByTableType && fieldDataGeneratorsByTableType[nodeIdFieldName];
if (gens) {
gens.forEach(gen => addDataGenerator(gen));
}
return {
type: GraphQLID,
resolve(data) {
return data.data.__identifiers && getNodeIdForTypeAndIdentifiers(Table, ...data.data.__identifiers);
}
};
}, {
isPgMutationPayloadDeletedNodeIdField: true
})
} : null);
}
}, {
__origin: `Adding table ${mode} mutation payload type for ${describePgEntity(table)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(table, {
name: "newNameHere"
})}`,
isMutationPayload: true,
isPgUpdatePayloadType: mode === "update",
isPgDeletePayloadType: mode === "delete",
pgIntrospection: table
});
// NodeId
const primaryKeyConstraint = table.primaryKeyConstraint;
if (nodeIdFieldName && primaryKeyConstraint) {
const primaryKeys = primaryKeyConstraint && primaryKeyConstraint.keyAttributes;
const fieldName = inflection[mode === "update" ? "updateNode" : "deleteNode"](table);
const InputType = newWithHooks(GraphQLInputObjectType, {
description: build.wrapDescription(`All input for the \`${fieldName}\` mutation.`, "type"),
name: inflection[mode === "update" ? "updateNodeInputType" : "deleteNodeInputType"](table),
fields: Object.assign({
clientMutationId: {
description: build.wrapDescription("An arbitrary string value with no semantic meaning. Will be included in the payload verbatim. May be used to track mutations by the client.", "field"),
type: GraphQLString
},
[nodeIdFieldName]: {
description: build.wrapDescription(`The globally unique \`ID\` which will identify a single \`${tableTypeName}\` to be ${mode}d.`, "field"),
type: new GraphQLNonNull(GraphQLID)
}
}, mode === "update" && TablePatch ? {
[inflection.patchField(inflection.tableFieldName(table))]: {
description: build.wrapDescription(`An object where the defined keys will be set on the \`${tableTypeName}\` being ${mode}d.`, "field"),
type: new GraphQLNonNull(TablePatch)
}
} : null)
}, {
__origin: `Adding table ${mode} (by node ID) mutation input type for ${describePgEntity(table)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(table, {
name: "newNameHere"
})}`,
isPgUpdateInputType: mode === "update",
isPgUpdateNodeInputType: mode === "update",
isPgDeleteInputType: mode === "delete",
isPgDeleteNodeInputType: mode === "delete",
pgInflection: table,
// TODO:v5: remove - TYPO!
pgIntrospection: table,
isMutationInput: true
});
memo = extend(memo, {
[fieldName]: fieldWithHooks(fieldName, context => {
const {
getDataFromParsedResolveInfoFragment
} = context;
return {
description: build.wrapDescription(mode === "update" ? `Updates a single \`${tableTypeName}\` using its globally unique id and a patch.` : `Deletes a single \`${tableTypeName}\` using its globally unique id.`, "field"),
type: PayloadType,
args: {
input: {
type: new GraphQLNonNull(InputType)
}
},
async resolve(parent, args, resolveContext, resolveInfo) {
const {
input
} = args;
const {
pgClient
} = resolveContext;
const nodeId = input[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");
}
return commonCodeRenameMe(pgClient, resolveInfo, getDataFromParsedResolveInfoFragment, PayloadType, args, sql.fragment`(${sql.join(primaryKeys.map((key, idx) => sql.fragment`${sql.identifier(key.name)} = ${gql2pg(identifiers[idx], key.type, key.typeModifier)}`), ") and (")})`, context, resolveContext);
} catch (e) {
debug(e);
return null;
}
}
};
}, {
isPgNodeMutation: true,
pgFieldIntrospection: table,
[mode === "update" ? "isPgUpdateMutationField" : "isPgDeleteMutationField"]: true
})
}, "Adding ${mode} mutation for ${describePgEntity(table)}");
}
// Unique
uniqueConstraints.forEach(constraint => {
if (omit(constraint, mode)) {
return;
}
const keys = constraint.keyAttributes;
if (!keys.every(_ => _)) {
throw new Error(`Consistency error: could not find an attribute in the constraint when building the ${mode} mutation for ${describePgEntity(table)}!`);
}
if (keys.some(key => omit(key, "read"))) {
return;
}
const fieldName = inflection[mode === "update" ? "updateByKeys" : "deleteByKeys"](keys, table, constraint);
const InputType = newWithHooks(GraphQLInputObjectType, {
description: build.wrapDescription(`All input for the \`${fieldName}\` mutation.`, "type"),
name: inflection[mode === "update" ? "updateByKeysInputType" : "deleteByKeysInputType"](keys, table, constraint),
fields: Object.assign({
clientMutationId: {
type: GraphQLString
}
}, mode === "update" && TablePatch ? {
[inflection.patchField(inflection.tableFieldName(table))]: {
description: build.wrapDescription(`An object where the defined keys will be set on the \`${tableTypeName}\` being ${mode}d.`, "field"),
type: new GraphQLNonNull(TablePatch)
}
} : null, keys.reduce((memo, key) => {
memo[inflection.column(key)] = {
description: key.description,
type: new GraphQLNonNull(pgGetGqlInputTypeByTypeIdAndModifier(key.typeId, key.typeModifier))
};
return memo;
}, {}))
}, {
__origin: `Adding table ${mode} mutation input type for ${describePgEntity(constraint)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(table, {
name: "newNameHere"
})}`,
isPgUpdateInputType: mode === "update",
isPgUpdateByKeysInputType: mode === "update",
isPgDeleteInputType: mode === "delete",
isPgDeleteByKeysInputType: mode === "delete",
pgInflection: table,
// TODO:v5: remove - TYPO!
pgIntrospection: table,
pgKeys: keys,
isMutationInput: true
});
memo = extend(memo, {
[fieldName]: fieldWithHooks(fieldName, context => {
const {
getDataFromParsedResolveInfoFragment
} = context;
return {
description: build.wrapDescription(mode === "update" ? `Updates a single \`${tableTypeName}\` using a unique key and a patch.` : `Deletes a single \`${tableTypeName}\` using a unique key.`, "field"),
type: PayloadType,
args: {
input: {
type: new GraphQLNonNull(InputType)
}
},
async resolve(parent, args, resolveContext, resolveInfo) {
const {
input
} = args;
const {
pgClient
} = resolveContext;
return commonCodeRenameMe(pgClient, resolveInfo, getDataFromParsedResolveInfoFragment, PayloadType, args, sql.fragment`(${sql.join(keys.map(key => sql.fragment`${sql.identifier(key.name)} = ${gql2pg(input[inflection.column(key)], key.type, key.typeModifier)}`), ") and (")})`, context, resolveContext);
}
};
}, {
isPgNodeMutation: false,
pgFieldIntrospection: table,
pgFieldConstraint: constraint,
[mode === "update" ? "isPgUpdateMutationField" : "isPgDeleteMutationField"]: true
})
}, `Adding ${mode} mutation for ${describePgEntity(constraint)}`);
});
}
return memo;
}, outerMemo), {}), `Adding default update/delete mutations to root Mutation type`);
}, ["PgMutationUpdateDelete"]);
};
exports.default = PgMutationUpdateDeletePlugin;
//# sourceMappingURL=PgMutationUpdateDeletePlugin.js.map