|
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` |
|
); |
|
}); |
|
}; |