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.

Revisions

  1. benjie revised this gist Apr 29, 2021. 1 changed file with 3 additions and 0 deletions.
    3 changes: 3 additions & 0 deletions PgUpsertPlugin.js
    Original file line number Diff line number Diff line change
    @@ -87,6 +87,9 @@ module.exports = function PgUpsertPlugin(
    null
    );
    const TablePatch = getTypeByName(inflection.patchType(tableTypeName));
    if (!TableInput || !TablePatch) {
    return {};
    }
    const PayloadType = newWithHooks(
    GraphQLObjectType,
    {
  2. benjie revised this gist Jul 9, 2019. 1 changed file with 11 additions and 0 deletions.
    11 changes: 11 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,11 @@
    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:

    ```sql
    comment on table my_table is E'@upsert';
    -- or
    comment on constraint my_table_pkey of table my_tabls is E'@upsert';
    ```
  3. benjie created this gist Mar 5, 2019.
    324 changes: 324 additions & 0 deletions PgUpsertPlugin.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,324 @@
    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));
    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`
    );
    });
    };