Skip to content

Commit

Permalink
feat: allow omitting defaulted columns (#444)
Browse files Browse the repository at this point in the history
* feat: allow omitting defaulted columns

* feat: add distinguishable property to upsert field
  • Loading branch information
mgagliardo91 committed Sep 11, 2022
1 parent e220754 commit a67934d
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 17 deletions.
78 changes: 78 additions & 0 deletions src/__tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,23 @@ const fetchAllRoles = async (t: PluginExecutionContext) => {
return execGqlOp(t, query);
};

const fetchAllCars = async (t: PluginExecutionContext) => {
const query = nanographql`
query {
allCars {
edges {
node {
make
model
trim
active
}
}
}
}`;
return execGqlOp(t, query);
};

const create = async (
t: PluginExecutionContext,
extraProperties: Record<string, unknown> = {}
Expand Down Expand Up @@ -390,3 +407,64 @@ test("upsert where clause", async (t) => {
t.is(res.data.allRoles.edges.length, 1);
}
});

test("upsert handling of nullable defaulted columns", async (t) => {
await t.context.client.query(`
create table car(
id serial primary key,
make text not null,
model text not null,
trim text not null default 'standard',
active boolean,
unique (make, model, trim)
)
`);
await initializePostgraphile(t);
const upsertCar = async ({
trim,
active = false,
}: {
trim?: string;
make?: string;
model?: string;
active?: boolean;
} = {}) => {
const query = nanographql(`
mutation {
upsertCar(where: {
make: "Honda",
model: "Civic",
},
input: {
car: {
make: "Honda",
model: "Civic",
${trim ? `trim: "${trim}"` : ""}
active: ${active}
}
}) {
clientMutationId
}
}
`);
return execGqlOp(t, query);
};
{
await upsertCar();
await upsertCar({ active: true });
let res = await fetchAllCars(t);
t.is(res.data.allCars.edges.length, 1);
t.like(res.data.allCars.edges[0], {
node: {
active: true,
make: "Honda",
model: "Civic",
trim: "standard",
},
});

await upsertCar({ trim: "non-standard" });
res = await fetchAllCars(t);
t.is(res.data.allCars.edges.length, 2);
}
});
72 changes: 55 additions & 17 deletions src/postgraphile-upsert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,29 +288,66 @@ function createUpsertField({
{}
);

// Depending on whether a where clause was passed, we want to determine which
// constraint to use in the upsert ON CONFLICT cause.
// If where clause: Check for the first constraint that the where clause provides all matching unique columns
// If no where clause: Check for the first constraint that our upsert columns provides all matching unique columns
// or default to primary key constraint (existing functionality).
const fieldToAttributeMap = attributes.reduce(
(acc, attr) => ({
...acc,
[inflection.camelCase(attr.name)]: attr,
}),
{}
);

// Pre-process our primary key constraint
const primaryKeyConstraint = uniqueConstraints.find(
(con) => con.type === "p"
);
const primaryKeyConstraintCols = new Set(
primaryKeyConstraint?.keyAttributes.map(({ name }) => name) ?? []
);

// Pre-process our data inputs from the payload (what was manually passed in)
const inputDataKeys = new Set(Object.keys(inputData));
const matchingConstraint = where
? Object.entries(columnsByConstraintName).find(([, columns]) =>
[...columns].every(
(col) => inflection.camelCase(col.name) in where
)
const inputDataColumns = new Set(
[...inputDataKeys].map((key) => fieldToAttributeMap[key].name)
);

// Construct a super-set of fields passed up plus columns with default values
// (as these can be set before the constraint kicks into place)
const inputDataColumnsWithDefaults = new Set([
...inputDataColumns,
...attributes
.filter(
(a) => a.hasDefault && !primaryKeyConstraintCols.has(a.name)
)
: Object.entries(columnsByConstraintName).find(([, columns]) =>
[...columns].every((col) =>
inputDataKeys.has(inflection.camelCase(col.name))
.map(({ name }) => name),
]);

/**
* Depending on whether a where clause was passed, we want to determine which
* constraint to use in the upsert ON CONFLICT cause.
* Decision flow:
* 1. if we have a where clause, attempt to find matching constraint
* 2. attempt to find a matching constraint given our data input
* 3. attempt to find a matching constraint given our data input + defaulted columns
* 4. else, use the primary key constraint if it exists
*/
const matchingConstraint =
(where
? Object.entries(columnsByConstraintName).find(([, columns]) =>
[...columns].every(
(col) => inflection.camelCase(col.name) in where
)
)
) ??
Object.entries(columnsByConstraintName).find(
([key]) => key === primaryKeyConstraint?.name
);
: Object.entries(columnsByConstraintName).find(([, columns]) =>
[...columns].every((col) => inputDataColumns.has(col.name))
)) ??
Object.entries(columnsByConstraintName).find(([, columns]) =>
[...columns].every((col) =>
inputDataColumnsWithDefaults.has(col.name)
)
) ??
Object.entries(columnsByConstraintName).find(
([key]) => key === primaryKeyConstraint?.name
);

if (!matchingConstraint) {
throw new Error(
Expand Down Expand Up @@ -411,6 +448,7 @@ function createUpsertField({
{
pgFieldIntrospection: table,
isPgCreateMutationField: false,
isPgUpsertMutationField: true,
}
),
};
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type Tags = unknown;
export interface KeyAttribute {
$ref: string;
name: string;
}
export interface Constraint {
kind: string;
Expand Down

0 comments on commit a67934d

Please sign in to comment.