postgraphile-plugin-many-create-update-delete
Version:
Postgraphile plugin that enables many create, update, & delete mutations in a single transaction.
257 lines • 14.8 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const debug_1 = __importDefault(require("debug"));
const debug = debug_1.default('graphile-build-pg');
const PostGraphileManyDeletePlugin = (builder, options) => {
if (options.pgDisableDefaultMutations)
return;
/**
* Add a hook to create the new root level delete mutation
*/
builder.hook(
// @ts-ignore
'GraphQLObjectType:fields', GQLObjectFieldsHookHandlerFcn, ['PgMutationManyDelete'], // hook provides
[], // hook before
['PgMutationUpdateDelete'] // hook after
);
/**
* Handles adding the new "many delete" root level fields
*/
function GQLObjectFieldsHookHandlerFcn(fields, build, context) {
const { extend, newWithHooks, getNodeIdForTypeAndIdentifiers, getTypeAndIdentifiersFromNodeId, nodeIdFieldName, fieldDataGeneratorsByFieldNameByType, parseResolveInfo, getTypeByName, gql2pg, pgGetGqlTypeByTypeIdAndModifier, pgGetGqlInputTypeByTypeIdAndModifier, pgIntrospectionResultsByKind, pgSql: sql, graphql: { GraphQLList, GraphQLNonNull, GraphQLInputObjectType, GraphQLString, GraphQLObjectType, GraphQLID, getNamedType }, pgColumnFilter, inflection, pgQueryFromResolveData: queryFromResolveData, pgOmit: omit, pgViaTemporaryTable: viaTemporaryTable, describePgEntity, sqlCommentByAddingTags, pgField } = build;
const { scope: { isRootMutation }, fieldWithHooks } = context;
if (!isRootMutation || !pgColumnFilter)
return fields;
let newFields = {}, i;
const noOfTables = pgIntrospectionResultsByKind.class.length;
for (i = 0; i < noOfTables; i++) {
handleAdditionsFromTableInfo(pgIntrospectionResultsByKind.class[i]);
}
function handleAdditionsFromTableInfo(table) {
if (!table.namespace ||
!table.isDeletable ||
omit(table, 'delete') ||
!table.tags.mncud)
return;
const tableType = pgGetGqlTypeByTypeIdAndModifier(table.type.id, null);
if (!tableType) {
debug(`There was no GQL Table Type for table '${table.namespace.name}.${table.name}',
so we're not generating a many delete mutation for it.`);
return;
}
const namedType = getNamedType(tableType);
const tablePatch = getTypeByName(inflection.patchType(namedType.name));
if (!tablePatch) {
throw new Error(`Could not find TablePatch type for table '${table.name}'`);
}
const tableTypeName = namedType.name;
const uniqueConstraints = table.constraints.filter(con => con.type === 'p');
// Setup and add the GraphQL Payload Type
const newPayloadHookType = GraphQLObjectType;
const newPayloadHookSpec = {
name: `mn${inflection.deletePayloadType(table)}`,
description: `The output of our delete mn \`${tableTypeName}\` mutation.`,
fields: ({ fieldWithHooks }) => {
const tableName = inflection.tableFieldName(table);
const deletedNodeIdFieldName = inflection.deletedNodeId(table);
return Object.assign({
clientMutationId: {
description: 'The exact same `clientMutationId` that was provided in the mutation input, unchanged and unused. May be used by a client to track mutations.',
type: GraphQLString
},
[tableName]: pgField(build, fieldWithHooks, tableName, {
description: `The \`${tableTypeName}\` that was deleted by this mutation.`,
type: tableType
}, {}, false)
}, {
[deletedNodeIdFieldName]: fieldWithHooks(deletedNodeIdFieldName, ({ addDataGenerator }) => {
const fieldDataGeneratorsByTableType = fieldDataGeneratorsByFieldNameByType.get(tableType);
const gens = fieldDataGeneratorsByTableType &&
fieldDataGeneratorsByTableType[nodeIdFieldName];
if (gens) {
gens.forEach(gen => addDataGenerator(gen));
}
return {
type: GraphQLID,
resolve(data) {
return (data.data.__identifiers &&
getNodeIdForTypeAndIdentifiers(tableType, ...data.data.__identifiers));
}
};
}, {
isPgMutationPayloadDeletedNodeIdField: true
})
});
}
};
const newPayloadHookScope = {
__origin: `Adding table mn delete 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,
isPgDeletePayloadType: true,
pgIntrospection: table
};
const PayloadType = newWithHooks(newPayloadHookType, newPayloadHookSpec, newPayloadHookScope);
if (!PayloadType) {
throw new Error(`Failed to determine payload type on the mn\`${tableTypeName}\` mutation`);
}
// Setup and add GQL Input Types for "Unique Constraint" based updates
// TODO: Add NodeId code updates
uniqueConstraints.forEach(constraint => {
if (omit(constraint, 'delete'))
return;
const keys = constraint.keyAttributes;
if (!keys.every(_ => _)) {
throw new Error(`Consistency error: could not find an attribute in the constraint when building the many\
delete mutation for ${describePgEntity(table)}!`);
}
if (keys.some(key => omit(key, 'read')))
return;
const fieldName = `mn${inflection.upperCamelCase(inflection.deleteByKeys(keys, table, constraint))}`;
const newInputHookType = GraphQLInputObjectType;
const patchName = inflection.patchField(inflection.tableFieldName(table));
const newInputHookSpec = {
name: `mn${inflection.upperCamelCase(inflection.deleteByKeysInputType(keys, table, constraint))}`,
description: `All input for the delete \`${fieldName}\` mutation.`,
fields: Object.assign({
clientMutationId: {
type: GraphQLString
}
}, {
[`mn${inflection.upperCamelCase(patchName)}`]: {
description: `The one or many \`${tableTypeName}\` to be deleted. You must provide the PK values!`,
// TODO: Add an actual type that has the PKs required
// instead of using the tablePatch in another file,
// and hook onto the input types to do so.
//@ts-ignore
type: new GraphQLList(new GraphQLNonNull(tablePatch))
}
}, {})
};
const newInputHookScope = {
__origin: `Adding table many delete mutation input type for ${describePgEntity(constraint)},
You can rename the table's GraphQL type via a 'Smart Comment':\n\n
${sqlCommentByAddingTags(table, {
name: 'newNameHere'
})}`,
isPgDeleteInputType: true,
isPgDeleteByKeysInputType: true,
isMutationInput: true,
pgInflection: table,
pgKeys: keys
};
const InputType = newWithHooks(newInputHookType, newInputHookSpec, newInputHookScope);
if (!InputType) {
throw new Error(`Failed to determine input type for '${fieldName}' mutation`);
}
// Define the new mutation field
function newFieldWithHooks() {
return fieldWithHooks(fieldName, context => {
context.table = table;
context.relevantAttributes = table.attributes.filter(attr => pgColumnFilter(attr, build, context) && !omit(attr, 'delete'));
return {
description: `Deletes one or many \`${tableTypeName}\` a unique key via a patch.`,
type: PayloadType,
args: {
input: {
type: new GraphQLNonNull(InputType)
}
},
resolve: resolver.bind(context)
};
}, {
pgFieldIntrospection: table,
pgFieldConstraint: constraint,
isPgNodeMutation: false,
isPgDeleteMutationField: true
});
}
async function resolver(_data, args, resolveContext, resolveInfo) {
const { input } = args;
const { table, getDataFromParsedResolveInfoFragment, relevantAttributes } = this;
const { pgClient } = resolveContext;
const parsedResolveInfoFragment = parseResolveInfo(resolveInfo);
// @ts-ignore
parsedResolveInfoFragment.args = args; // Allow overriding via makeWrapResolversPlugin
const resolveData = getDataFromParsedResolveInfoFragment(parsedResolveInfoFragment, PayloadType);
const sqlColumns = [];
const inputData = input[`mn${inflection.upperCamelCase(inflection.patchField(inflection.tableFieldName(table)))}`];
if (!inputData || inputData.length === 0)
return null;
const sqlValues = Array(inputData.length).fill([]);
let hasConstraintValue = true;
inputData.forEach((dataObj, i) => {
let setOfRcvdDataHasPKValue = false;
relevantAttributes.forEach((attr) => {
const fieldName = inflection.column(attr);
const dataValue = dataObj[fieldName];
const isConstraintAttr = keys.some(key => key.name === attr.name);
// Ensure that the field values are PKs since that's
// all we care about for deletions.
if (!isConstraintAttr)
return;
// Store all attributes on the first run.
if (i === 0) {
sqlColumns.push(sql.raw(attr.name));
}
if (fieldName in dataObj) {
sqlValues[i] = [
...sqlValues[i],
gql2pg(dataValue, attr.type, attr.typeModifier)
];
if (isConstraintAttr) {
setOfRcvdDataHasPKValue = true;
}
}
});
if (!setOfRcvdDataHasPKValue) {
hasConstraintValue = false;
}
});
if (!hasConstraintValue) {
throw new Error(`You must provide the primary key(s) in the provided data for deletes on '${inflection.pluralize(inflection._singularizedTableName(table))}'`);
}
if (sqlColumns.length === 0)
return null;
const mutationQuery = sql.query `\
DELETE FROM ${sql.identifier(table.namespace.name, table.name)}
WHERE
(${sql.join(sqlValues.map((dataGroup, i) => sql.fragment `(${sql.join(dataGroup.map((val, j) => sql.fragment `"${sqlColumns[j]}" = ${val}`), ') and (')})`), ') or (')})
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, sql.identifier(table.namespace.name, table.name), mutationQuery, 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 deleted in collection '${inflection.pluralize(inflection._singularizedTableName(table))}' because no values you can delete were found matching these criteria.`);
}
return {
clientMutationId: input.clientMutationId,
data: row
};
}
newFields = extend(newFields, {
[fieldName]: newFieldWithHooks
}, `Adding mn delete mutation for ${describePgEntity(constraint)}`);
});
}
return extend(fields, newFields, `Adding the many 'delete' mutation to the root mutation`);
}
};
exports.default = PostGraphileManyDeletePlugin;
//# sourceMappingURL=PostGraphileManyDeletePlugin.js.map