Skip to content

Instantly share code, notes, and snippets.

@benjie
Last active August 16, 2022 18:29
Show Gist options
  • Save benjie/2db7bdfa9578660bdef6b35cc3485c42 to your computer and use it in GitHub Desktop.
Save benjie/2db7bdfa9578660bdef6b35cc3485c42 to your computer and use it in GitHub Desktop.

Clone this plugin into your codebase.

Add the plugin to PostGraphile via --append-plugins (CLI) or appendPlugins: [...].

Add a comment on the relevant constraint and/or table to enable upsert:

comment on table my_table is E'@upsert';
-- or
comment on constraint my_table_pkey of table my_tabls is E'@upsert';
module.exports = function PgUpsertPlugin(
builder,
{ pgDisableDefaultMutations }
) {
if (pgDisableDefaultMutations) {
return;
}
builder.hook("inflection", (inflection, build) =>
build.extend(inflection, {
upsertPayloadType(table) {
return this.upperCamelCase(
`upsert-${this._singularizedTableName(table)}-payload`
);
},
upsertByConstraint(constraint) {
if (constraint.tags.upsertFieldName) {
return constraint.tags.upsertFieldName;
}
return this.camelCase(
`upsert-${this._singularizedTableName(
constraint.class
)}-by-${constraint.keyAttributes
.map(key => this.column(key))
.join("-and-")}`
);
},
upsertByConstraintInputType(constraint) {
return this.camelCase(`${this.upsertByConstraint(constraint)}-input`);
},
})
);
builder.hook("GraphQLObjectType:fields", (fields, build, context) => {
const {
newWithHooks,
extend,
parseResolveInfo,
getTypeByName,
gql2pg,
pgGetGqlTypeByTypeIdAndModifier,
pgGetGqlInputTypeByTypeIdAndModifier,
pgIntrospectionResultsByKind: introspectionResultsByKind,
pgSql: sql,
graphql: {
GraphQLNonNull,
GraphQLInputObjectType,
GraphQLString,
GraphQLObjectType,
},
inflection,
pgQueryFromResolveData: queryFromResolveData,
pgOmit: omit,
pgViaTemporaryTable: viaTemporaryTable,
describePgEntity,
pgField,
} = build;
const {
scope: { isRootMutation },
fieldWithHooks,
} = context;
if (!isRootMutation) {
return fields;
}
function upsertFieldsForTable(table) {
if (
!table.namespace ||
!table.isUpdatable ||
!table.isInsertable ||
omit(table, "upsert")
) {
return {};
}
const TableType = pgGetGqlTypeByTypeIdAndModifier(table.type.id, null);
if (!TableType) {
return {};
}
const uniqueConstraints = table.constraints.filter(
con => con.type === "u" || con.type === "p"
);
const tableTypeName = TableType.name;
const TableInput = pgGetGqlInputTypeByTypeIdAndModifier(
table.type.id,
null
);
const TablePatch = getTypeByName(inflection.patchType(tableTypeName));
if (!TableInput || !TablePatch) {
return {};
}
const PayloadType = newWithHooks(
GraphQLObjectType,
{
name: inflection.upsertPayloadType(table),
description: `The output of our upsert \`${tableTypeName}\` mutation.`,
// eslint-disable-next-line no-shadow
fields: ({ fieldWithHooks }) => {
const tableName = inflection.tableFieldName(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 upserted by this mutation.`,
type: TableType,
},
{},
false
),
});
},
},
{
__origin: `Adding table upsert mutation payload type for ${describePgEntity(
table
)}.`,
isMutationPayload: true,
isPgUpsertPayloadType: true,
pgIntrospection: table,
}
);
return uniqueConstraints.reduce((memo, constraint) => {
if (omit(constraint, "upsert")) return memo;
if (!constraint.tags.upsert && !table.tags.upsert) return memo;
const keys = constraint.keyAttributes;
if (keys.some(key => omit(key, "read"))) {
return memo;
}
const fieldName = inflection.upsertByConstraint(constraint);
const InputType = newWithHooks(
GraphQLInputObjectType,
{
description: `All input for the \`${fieldName}\` mutation.`,
name: inflection.upsertByConstraintInputType(constraint),
fields: Object.assign({
clientMutationId: {
type: GraphQLString,
},
[inflection.tableFieldName(table)]: {
description: `The \`${tableTypeName}\` to be created by this mutation.`,
type: new GraphQLNonNull(TableInput),
},
[inflection.patchField(inflection.tableFieldName(table))]: {
description: `An object where the defined keys will be set on the \`${tableTypeName}\` being updated.`,
type: new GraphQLNonNull(TablePatch),
},
}),
},
{
__origin: `Adding table upsert mutation input type for ${describePgEntity(
constraint
)}.`,
isPgUpsertByKeysInputType: true,
pgIntrospection: table,
pgFieldIntrospection: constraint,
pgKeys: keys,
isMutationInput: true,
}
);
return extend(
memo,
{
[fieldName]: fieldWithHooks(
fieldName,
fieldContext => {
const { getDataFromParsedResolveInfoFragment } = fieldContext;
return {
description: `Inserts a single \`${tableTypeName}\`; if the record already exists then insteads updates the record.`,
type: PayloadType,
args: {
input: {
type: new GraphQLNonNull(InputType),
},
},
async resolve(parent, args, resolveContext, resolveInfo) {
const { input } = args;
const { pgClient } = resolveContext;
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
);
const inputCreate = input[inflection.tableFieldName(table)];
const inputPatch =
input[
inflection.patchField(inflection.tableFieldName(table))
];
const columnsToInsert = [];
const valuesToInsert = [];
const updateStatements = [];
table.attributes.forEach(attr => {
const attrFieldName = inflection.column(attr);
if (!omit(attr, "insert")) {
if (attrFieldName in inputCreate) {
columnsToInsert.push(sql.identifier(attr.name));
valuesToInsert.push(
gql2pg(
inputCreate[attrFieldName],
attr.type,
attr.typeModifier
)
);
}
}
if (!omit(attr, "update")) {
if (attrFieldName in inputPatch) {
updateStatements.push(
sql.fragment`${sql.identifier(
attr.name
)} = ${gql2pg(
inputPatch[attrFieldName],
attr.type,
attr.typeModifier
)}`
);
}
}
});
const sqlMutationQuery = sql.query`
insert into ${sql.identifier(
table.namespace.name,
table.name
)} (${sql.join(columnsToInsert, ", ")})
values(${sql.join(valuesToInsert, ", ")})
on conflict (${sql.join(
keys.map(key => sql.identifier(key.name)),
", "
)})
do ${
updateStatements.length === 0
? sql.fragment`nothing`
: sql.fragment`update set
${sql.join(updateStatements, ", ")}
returning *`
}`;
const modifiedRowAlias = sql.identifier(
Symbol("modified_row")
);
const query = queryFromResolveData(
modifiedRowAlias,
modifiedRowAlias,
resolveData,
{},
null,
resolveContext
);
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(
`Could not upsert that record - check your permissions`
);
}
return {
clientMutationId: input.clientMutationId,
data: row,
};
},
};
},
{
isPgNodeMutation: false,
pgFieldIntrospection: table,
pgFieldConstraint: constraint,
isPgUpsertMutationField: true,
}
),
},
`Adding upsert mutation for ${describePgEntity(constraint)}`
);
}, {});
}
return extend(
fields,
introspectionResultsByKind.class.reduce(
(memo, table) =>
extend(
memo,
upsertFieldsForTable(table),
`Adding upsert mutations for ${describePgEntity(table)}`
),
{}
),
`Adding upsert mutations to root Mutation type`
);
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment