@vantreeseba/drizzle-graphql 13.1.1 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -5
- package/dist/index.cjs +299 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -7
- package/dist/index.d.ts +66 -7
- package/dist/index.js +299 -81
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5227,7 +5227,6 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5227
5227
|
const missing = [!tableFeatures.insert ? "insert" : void 0, !tableFeatures.update ? "update" : void 0].filter(Boolean).join(" and ");
|
|
5228
5228
|
if (missing) noteConflict(`upsert is on while ${missing} is off, so the upsert mutations are a second write path past the operation you turned off`, tableName);
|
|
5229
5229
|
}
|
|
5230
|
-
if (config?.features?.nestedWrites && (!tableFeatures.insert || !tableFeatures.update)) noteConflict("nestedWrites is on while this table's own insert or update is off, so a nested `create` or `connect` under another table's mutation can still write it", tableName);
|
|
5231
5230
|
if (config?.features?.updateMany !== void 0 && tableFeatures.updateMany && !tableFeatures.update) noteConflict("updateMany is on while update is off, so no batch update is generated", tableName);
|
|
5232
5231
|
if (config?.features?.groupBy !== void 0 && tableFeatures.groupBy && !tableFeatures.aggregates) noteConflict("groupBy is on while aggregates is off, so no grouped query is generated", tableName);
|
|
5233
5232
|
if (tableFeatures.countMutations && !tableFeatures.update && !tableFeatures.delete) noteConflict("countMutations is on while update and delete are both off, so no count mutation is generated", tableName);
|
|
@@ -5833,6 +5832,12 @@ const generateGroupBy = (db, tableName, table, typeName, fieldName, filterArgs,
|
|
|
5833
5832
|
};
|
|
5834
5833
|
//#endregion
|
|
5835
5834
|
//#region src/util/builders/nested-writes.ts
|
|
5835
|
+
const ALL_OPS = [
|
|
5836
|
+
"create",
|
|
5837
|
+
"connect",
|
|
5838
|
+
"disconnect",
|
|
5839
|
+
"set"
|
|
5840
|
+
];
|
|
5836
5841
|
/**
|
|
5837
5842
|
* Whether a column can be left out of an INSERT — either the database fills it in or it
|
|
5838
5843
|
* accepts NULL.
|
|
@@ -5861,7 +5866,7 @@ const throughColumn = (entry) => entry && is(entry, Column) ? entry : entry?._?.
|
|
|
5861
5866
|
* joining on more than one column per side; and a junction carrying a further NOT NULL column
|
|
5862
5867
|
* with no default, which no `connect` operand could supply a value for.
|
|
5863
5868
|
*/
|
|
5864
|
-
const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable) => {
|
|
5869
|
+
const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable, tableNameOf) => {
|
|
5865
5870
|
if (is(relation, One)) return;
|
|
5866
5871
|
const junction = relation.throughTable;
|
|
5867
5872
|
const sourceEntries = relation.through?.source;
|
|
@@ -5892,11 +5897,18 @@ const buildThroughPlan = (relationName, relation, table, targetTableName, target
|
|
|
5892
5897
|
canDetach: true,
|
|
5893
5898
|
through: {
|
|
5894
5899
|
table: junction,
|
|
5900
|
+
tableName: tableNameOf(junction),
|
|
5895
5901
|
sourceColPropName,
|
|
5896
5902
|
targetColPropName,
|
|
5897
5903
|
sourceCol,
|
|
5898
5904
|
targetCol
|
|
5899
|
-
}
|
|
5905
|
+
},
|
|
5906
|
+
targetKeyColumns: { [foreignColPropName]: foreignCol },
|
|
5907
|
+
allowed: /* @__PURE__ */ new Set([
|
|
5908
|
+
"connect",
|
|
5909
|
+
"disconnect",
|
|
5910
|
+
"set"
|
|
5911
|
+
])
|
|
5900
5912
|
};
|
|
5901
5913
|
};
|
|
5902
5914
|
/**
|
|
@@ -5909,7 +5921,7 @@ const buildThroughPlan = (relationName, relation, table, targetTableName, target
|
|
|
5909
5921
|
* relations whose join columns cannot be read off the drizzle relation; and relations where
|
|
5910
5922
|
* neither join column is unique, which leaves no way to tell which side stores the key.
|
|
5911
5923
|
*/
|
|
5912
|
-
const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePrimaryKeys, extractJoinColumns) => {
|
|
5924
|
+
const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePrimaryKeys, extractJoinColumns, tableNameOf = () => void 0) => {
|
|
5913
5925
|
const uniqueCache = /* @__PURE__ */ new Map();
|
|
5914
5926
|
const singleColumnUniques = (tableName, table) => {
|
|
5915
5927
|
let cached = uniqueCache.get(tableName);
|
|
@@ -5938,7 +5950,7 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
5938
5950
|
const targetTable = tables[targetTableName];
|
|
5939
5951
|
if (!targetTable) continue;
|
|
5940
5952
|
if (relation.through) {
|
|
5941
|
-
const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable);
|
|
5953
|
+
const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable, tableNameOf);
|
|
5942
5954
|
if (throughPlan) tablePlans[relationName] = throughPlan;
|
|
5943
5955
|
continue;
|
|
5944
5956
|
}
|
|
@@ -5959,6 +5971,10 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
5959
5971
|
else if (foreignIsUnique) fkSide = "parent";
|
|
5960
5972
|
else if (localIsUnique) fkSide = "child";
|
|
5961
5973
|
else continue;
|
|
5974
|
+
const fkColumn = fkSide === "child" ? foreignCol : localCol;
|
|
5975
|
+
const targetColumns = getColumns(targetTable);
|
|
5976
|
+
const targetKeyColumns = Object.fromEntries(resolvePrimaryKeys(targetTable).filter((name) => targetColumns[name]).map((name) => [name, targetColumns[name]]));
|
|
5977
|
+
targetKeyColumns[foreignColPropName] = foreignCol;
|
|
5962
5978
|
tablePlans[relationName] = {
|
|
5963
5979
|
relationName,
|
|
5964
5980
|
targetTableName,
|
|
@@ -5969,13 +5985,87 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
5969
5985
|
localCol,
|
|
5970
5986
|
isOne,
|
|
5971
5987
|
fkSide,
|
|
5972
|
-
canDetach: !
|
|
5988
|
+
canDetach: !fkColumn.notNull,
|
|
5989
|
+
targetKeyColumns,
|
|
5990
|
+
allowed: new Set(!fkColumn.notNull ? isOne ? [
|
|
5991
|
+
"create",
|
|
5992
|
+
"connect",
|
|
5993
|
+
"disconnect"
|
|
5994
|
+
] : ALL_OPS : ["create", "connect"])
|
|
5973
5995
|
};
|
|
5974
5996
|
}
|
|
5975
5997
|
if (Object.keys(tablePlans).length) plans[tableName] = tablePlans;
|
|
5976
5998
|
}
|
|
5977
5999
|
return plans;
|
|
5978
6000
|
};
|
|
6001
|
+
/**
|
|
6002
|
+
* Which of the target's (or junction's) write features each input of a relation needs. A
|
|
6003
|
+
* parent-side `connect` / `disconnect` only rewrites the parent's own column, so it needs
|
|
6004
|
+
* nothing of the target; a child-side one moves the target's foreign key, which is an update
|
|
6005
|
+
* of the target. A to-one child-side `create` also detaches the row attached before it, when
|
|
6006
|
+
* the key is nullable.
|
|
6007
|
+
*/
|
|
6008
|
+
const requiredWrites = (plan, op) => {
|
|
6009
|
+
if (plan.through) {
|
|
6010
|
+
const junction = plan.through.tableName;
|
|
6011
|
+
if (op === "connect") return [{
|
|
6012
|
+
tableName: junction,
|
|
6013
|
+
feature: "insert"
|
|
6014
|
+
}];
|
|
6015
|
+
if (op === "disconnect") return [{
|
|
6016
|
+
tableName: junction,
|
|
6017
|
+
feature: "delete"
|
|
6018
|
+
}];
|
|
6019
|
+
if (op === "set") return [{
|
|
6020
|
+
tableName: junction,
|
|
6021
|
+
feature: "delete"
|
|
6022
|
+
}, {
|
|
6023
|
+
tableName: junction,
|
|
6024
|
+
feature: "insert"
|
|
6025
|
+
}];
|
|
6026
|
+
return [];
|
|
6027
|
+
}
|
|
6028
|
+
const target = plan.targetTableName;
|
|
6029
|
+
if (op === "create") return plan.fkSide === "child" && plan.isOne && plan.canDetach ? [{
|
|
6030
|
+
tableName: target,
|
|
6031
|
+
feature: "insert"
|
|
6032
|
+
}, {
|
|
6033
|
+
tableName: target,
|
|
6034
|
+
feature: "update"
|
|
6035
|
+
}] : [{
|
|
6036
|
+
tableName: target,
|
|
6037
|
+
feature: "insert"
|
|
6038
|
+
}];
|
|
6039
|
+
return plan.fkSide === "child" ? [{
|
|
6040
|
+
tableName: target,
|
|
6041
|
+
feature: "update"
|
|
6042
|
+
}] : [];
|
|
6043
|
+
};
|
|
6044
|
+
/**
|
|
6045
|
+
* Narrows every plan's `allowed` inputs to the ones whose writes the table they land on
|
|
6046
|
+
* permits: its `features.insert` / `update` / `delete`, and for a junction table that it is
|
|
6047
|
+
* not excluded. A plan left with nothing is dropped, so its relation field is not generated.
|
|
6048
|
+
*
|
|
6049
|
+
* Returns one line per input taken out, for the build note.
|
|
6050
|
+
*/
|
|
6051
|
+
const gateNestedWritePlans = (plans, featureOf, isExcluded) => {
|
|
6052
|
+
const omitted = [];
|
|
6053
|
+
for (const [tableName, tablePlans] of Object.entries(plans)) {
|
|
6054
|
+
for (const [relationName, plan] of Object.entries(tablePlans)) {
|
|
6055
|
+
for (const op of [...plan.allowed]) {
|
|
6056
|
+
const blocked = requiredWrites(plan, op).filter(({ tableName: target, feature }) => target !== void 0 && (isExcluded(target) || !featureOf(target)[feature]));
|
|
6057
|
+
if (blocked.length) {
|
|
6058
|
+
plan.allowed.delete(op);
|
|
6059
|
+
const reasons = blocked.map(({ tableName: target, feature }) => isExcluded(target) ? `${target} is excluded` : `${target}.${feature} is off`);
|
|
6060
|
+
omitted.push(`${tableName}.${relationName}.${op} (${[...new Set(reasons)].join(", ")})`);
|
|
6061
|
+
}
|
|
6062
|
+
}
|
|
6063
|
+
if (!plan.allowed.size) delete tablePlans[relationName];
|
|
6064
|
+
}
|
|
6065
|
+
if (!Object.keys(tablePlans).length) delete plans[tableName];
|
|
6066
|
+
}
|
|
6067
|
+
return omitted;
|
|
6068
|
+
};
|
|
5979
6069
|
const NO_COLUMNS = /* @__PURE__ */ new Set();
|
|
5980
6070
|
/**
|
|
5981
6071
|
* Builds the nested-write input types for a schema.
|
|
@@ -6030,8 +6120,11 @@ const createNestedWriteTypes = (params) => {
|
|
|
6030
6120
|
*
|
|
6031
6121
|
* A many-to-many relation offers no `create`: inserting a row *and* linking it is two
|
|
6032
6122
|
* writes to two tables, and the link half is what the relation field is for. Its
|
|
6033
|
-
* `disconnect` and `set`
|
|
6123
|
+
* `disconnect` and `set` do not depend on nullability, since breaking a link deletes a
|
|
6034
6124
|
* junction row rather than nulling a column that might be NOT NULL.
|
|
6125
|
+
*
|
|
6126
|
+
* Each input is also subject to `plan.allowed` — the table it writes has to have that
|
|
6127
|
+
* write turned on (see {@link gateNestedWritePlans}).
|
|
6035
6128
|
*/
|
|
6036
6129
|
const wrapperType = (tableName, typeName, plan, forUpdate) => {
|
|
6037
6130
|
const key = `${tableName}.${plan.relationName}.${forUpdate ? "update" : "create"}`;
|
|
@@ -6040,57 +6133,52 @@ const createNestedWriteTypes = (params) => {
|
|
|
6040
6133
|
const filters = targetFilters(plan);
|
|
6041
6134
|
const targetTypeName = resolveTypeName(plan.targetTableName, typeNameMapper);
|
|
6042
6135
|
const fields = {};
|
|
6136
|
+
const offers = (op) => plan.allowed.has(op);
|
|
6043
6137
|
if (plan.through) {
|
|
6044
|
-
if (
|
|
6045
|
-
fields["connect"] = {
|
|
6138
|
+
if (filters && offers("connect")) fields["connect"] = {
|
|
6046
6139
|
type: listOf(filters),
|
|
6047
6140
|
description: `Link every existing ${targetTypeName} row these match. Already-linked rows are left alone.`
|
|
6048
6141
|
};
|
|
6049
|
-
if (forUpdate) {
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
};
|
|
6058
|
-
}
|
|
6142
|
+
if (filters && forUpdate && offers("disconnect")) fields["disconnect"] = {
|
|
6143
|
+
type: listOf(filters),
|
|
6144
|
+
description: `Unlink every linked ${targetTypeName} row these match, leaving the rows in place`
|
|
6145
|
+
};
|
|
6146
|
+
if (filters && forUpdate && offers("set")) fields["set"] = {
|
|
6147
|
+
type: listOf(filters),
|
|
6148
|
+
description: `Replace the whole set: unlink everything linked, then link every ${targetTypeName} row these match. \`set: []\` clears the relation. Applied before \`disconnect\` and \`connect\`.`
|
|
6149
|
+
};
|
|
6059
6150
|
} else if (plan.isOne) {
|
|
6060
|
-
fields["create"] = {
|
|
6151
|
+
if (offers("create")) fields["create"] = {
|
|
6061
6152
|
type: payloadType(tableName, typeName, plan),
|
|
6062
6153
|
description: `Insert a ${targetTypeName} row and attach it`
|
|
6063
6154
|
};
|
|
6064
|
-
if (filters) fields["connect"] = {
|
|
6155
|
+
if (filters && offers("connect")) fields["connect"] = {
|
|
6065
6156
|
type: filters,
|
|
6066
6157
|
description: `Attach the existing ${targetTypeName} row this matches. Must match exactly one row.`
|
|
6067
6158
|
};
|
|
6068
|
-
if (forUpdate &&
|
|
6159
|
+
if (forUpdate && offers("disconnect")) fields["disconnect"] = {
|
|
6069
6160
|
type: GraphQLBoolean,
|
|
6070
6161
|
description: `Detach the currently attached ${targetTypeName} row, leaving it in place`
|
|
6071
6162
|
};
|
|
6072
6163
|
} else {
|
|
6073
|
-
fields["create"] = {
|
|
6164
|
+
if (offers("create")) fields["create"] = {
|
|
6074
6165
|
type: listOf(payloadType(tableName, typeName, plan)),
|
|
6075
6166
|
description: `Insert these ${targetTypeName} rows and attach them`
|
|
6076
6167
|
};
|
|
6077
|
-
if (filters) {
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
description: `Replace the whole set: detach everything attached, then attach every ${targetTypeName} row these match. Applied before \`disconnect\`, \`connect\` and \`create\`.`
|
|
6090
|
-
};
|
|
6091
|
-
}
|
|
6092
|
-
}
|
|
6168
|
+
if (filters && offers("connect")) fields["connect"] = {
|
|
6169
|
+
type: listOf(filters),
|
|
6170
|
+
description: `Attach every existing ${targetTypeName} row these match`
|
|
6171
|
+
};
|
|
6172
|
+
if (filters && forUpdate && offers("disconnect")) fields["disconnect"] = {
|
|
6173
|
+
type: listOf(filters),
|
|
6174
|
+
description: `Detach every attached ${targetTypeName} row these match, leaving the rows in place`
|
|
6175
|
+
};
|
|
6176
|
+
if (filters && forUpdate && offers("set")) fields["set"] = {
|
|
6177
|
+
type: listOf(filters),
|
|
6178
|
+
description: `Replace the whole set: detach everything attached, then attach every ${targetTypeName} row these match. Applied before \`disconnect\`, \`connect\` and \`create\`.`
|
|
6179
|
+
};
|
|
6093
6180
|
}
|
|
6181
|
+
if (!Object.keys(fields).length) return;
|
|
6094
6182
|
const type = new GraphQLInputObjectType({
|
|
6095
6183
|
name: cacheCtx.typeName({
|
|
6096
6184
|
kind: "nestedWriteInput",
|
|
@@ -6122,7 +6210,7 @@ const createNestedWriteTypes = (params) => {
|
|
|
6122
6210
|
const tablePlans = plans[tableName];
|
|
6123
6211
|
if (!tablePlans) return NO_COLUMNS;
|
|
6124
6212
|
const relaxed = /* @__PURE__ */ new Set();
|
|
6125
|
-
for (const plan of Object.values(tablePlans)) if (plan.fkSide === "parent" && !isOptionalOnInsert(plan.localCol)) relaxed.add(plan.localColPropName);
|
|
6213
|
+
for (const plan of Object.values(tablePlans)) if (plan.fkSide === "parent" && (plan.allowed.has("create") || plan.allowed.has("connect")) && !isOptionalOnInsert(plan.localCol)) relaxed.add(plan.localColPropName);
|
|
6126
6214
|
return relaxed.size ? relaxed : NO_COLUMNS;
|
|
6127
6215
|
}
|
|
6128
6216
|
};
|
|
@@ -6133,8 +6221,99 @@ const createNestedWriteTypes = (params) => {
|
|
|
6133
6221
|
*/
|
|
6134
6222
|
const mergedOps = (entries) => Object.assign({}, ...entries.map((entry) => entry.ops));
|
|
6135
6223
|
const asArray = (value) => Array.isArray(value) ? value : value === void 0 ? [] : [value];
|
|
6224
|
+
const supplied = (value) => value !== void 0 && value !== null;
|
|
6225
|
+
/**
|
|
6226
|
+
* The hook calls one relation field makes. Every step is registered before the field's first
|
|
6227
|
+
* statement, so each `before` runs ahead of all of them and each `after` once the last has
|
|
6228
|
+
* returned. A table without hooks registers nothing, and its statements run exactly as they
|
|
6229
|
+
* did before hooks reached nested writes — no RETURNING clause is added.
|
|
6230
|
+
*/
|
|
6231
|
+
const createHookSteps = (onWrite, executor, request, parentTable, plan) => {
|
|
6232
|
+
const steps = /* @__PURE__ */ new Map();
|
|
6233
|
+
const keyOf = (op, operation) => `${op}:${operation}`;
|
|
6234
|
+
const payload = (step) => ({
|
|
6235
|
+
table: step.tableName,
|
|
6236
|
+
operation: step.operation,
|
|
6237
|
+
single: plan.isOne,
|
|
6238
|
+
args: request?.args,
|
|
6239
|
+
context: request?.context,
|
|
6240
|
+
info: request?.info,
|
|
6241
|
+
tx: executor,
|
|
6242
|
+
nested: {
|
|
6243
|
+
parentTable,
|
|
6244
|
+
relation: plan.relationName,
|
|
6245
|
+
op: step.op
|
|
6246
|
+
}
|
|
6247
|
+
});
|
|
6248
|
+
return {
|
|
6249
|
+
/** Registers the hook call for `op`'s `operation` statements, if that table has hooks. */
|
|
6250
|
+
add: (op, operation) => {
|
|
6251
|
+
const through = plan.through;
|
|
6252
|
+
const tableName = through ? through.tableName : plan.targetTableName;
|
|
6253
|
+
const hooks = tableName ? onWrite?.(tableName, operation) : void 0;
|
|
6254
|
+
if (!tableName || !hooks || steps.has(keyOf(op, operation))) return;
|
|
6255
|
+
const keyColumns = through ? {
|
|
6256
|
+
[through.sourceColPropName]: through.sourceCol,
|
|
6257
|
+
[through.targetColPropName]: through.targetCol
|
|
6258
|
+
} : { ...plan.targetKeyColumns };
|
|
6259
|
+
steps.set(keyOf(op, operation), {
|
|
6260
|
+
tableName,
|
|
6261
|
+
operation,
|
|
6262
|
+
op,
|
|
6263
|
+
hooks,
|
|
6264
|
+
returning: withHookColumns(hooks, keyColumns).returning,
|
|
6265
|
+
rows: []
|
|
6266
|
+
});
|
|
6267
|
+
},
|
|
6268
|
+
get: (op, operation) => steps.get(keyOf(op, operation)),
|
|
6269
|
+
/** Runs one statement, returning its rows to the step it belongs to when there is one. */
|
|
6270
|
+
run: async (query, op, operation) => {
|
|
6271
|
+
const step = steps.get(keyOf(op, operation));
|
|
6272
|
+
if (!step) {
|
|
6273
|
+
await query;
|
|
6274
|
+
return;
|
|
6275
|
+
}
|
|
6276
|
+
step.rows.push(...await query.returning(step.returning));
|
|
6277
|
+
},
|
|
6278
|
+
before: async () => {
|
|
6279
|
+
for (const step of steps.values()) await runWriteHook(step.hooks, "before", payload(step));
|
|
6280
|
+
},
|
|
6281
|
+
after: async () => {
|
|
6282
|
+
for (const step of steps.values()) await runWriteHook(step.hooks, "after", {
|
|
6283
|
+
...payload(step),
|
|
6284
|
+
rows: step.rows
|
|
6285
|
+
});
|
|
6286
|
+
}
|
|
6287
|
+
};
|
|
6288
|
+
};
|
|
6289
|
+
/**
|
|
6290
|
+
* Registers the hook calls a child-side relation field will make, in the order its
|
|
6291
|
+
* statements run, so the `before` hooks fire in that order too. Mirrors the branches of
|
|
6292
|
+
* `applyChildSide` and `applyThroughOps` exactly: a step registered for an input that runs no
|
|
6293
|
+
* statement would fire with no rows, and one missing would miss them.
|
|
6294
|
+
*/
|
|
6295
|
+
const registerChildSteps = (steps, plan, op) => {
|
|
6296
|
+
if (plan.through) {
|
|
6297
|
+
if (supplied(op["set"])) {
|
|
6298
|
+
steps.add("set", "delete");
|
|
6299
|
+
steps.add("set", "insert");
|
|
6300
|
+
}
|
|
6301
|
+
if (supplied(op["disconnect"])) steps.add("disconnect", "delete");
|
|
6302
|
+
if (supplied(op["connect"])) steps.add("connect", "insert");
|
|
6303
|
+
return;
|
|
6304
|
+
}
|
|
6305
|
+
if (op["set"] !== void 0) steps.add("set", "update");
|
|
6306
|
+
if (op["disconnect"] !== void 0 && (!plan.isOne || op["disconnect"] === true)) steps.add("disconnect", "update");
|
|
6307
|
+
const attachedBy = attachingOp(op);
|
|
6308
|
+
if (plan.isOne && attachedBy && plan.canDetach && op["set"] === void 0) steps.add(attachedBy, "update");
|
|
6309
|
+
if (op["connect"] !== void 0) steps.add("connect", "update");
|
|
6310
|
+
if (op["create"] !== void 0) steps.add("create", "insert");
|
|
6311
|
+
};
|
|
6312
|
+
/** The input that attaches a row to a to-one, which detaches whatever was attached before. */
|
|
6313
|
+
const attachingOp = (op) => op["connect"] !== void 0 ? "connect" : op["create"] !== void 0 ? "create" : void 0;
|
|
6136
6314
|
const createNestedWriteRuntime = (params) => {
|
|
6137
6315
|
const { plans, filterCtx, policies, contextValues } = params;
|
|
6316
|
+
const onWrite = policies?.onWrite;
|
|
6138
6317
|
/**
|
|
6139
6318
|
* A `connect` / `disconnect` / `set` operand compiled to SQL. An operand that compiles to
|
|
6140
6319
|
* nothing is rejected: `connect: {}` would otherwise attach every row in the table.
|
|
@@ -6145,12 +6324,12 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6145
6324
|
return withScope(scope, plan.targetTableName, plan.targetTable, condition);
|
|
6146
6325
|
};
|
|
6147
6326
|
const assertSingleOperation = (plan, op) => {
|
|
6148
|
-
const
|
|
6327
|
+
const given = [
|
|
6149
6328
|
"create",
|
|
6150
6329
|
"connect",
|
|
6151
6330
|
"disconnect"
|
|
6152
|
-
].filter((key) => op[key]
|
|
6153
|
-
if (
|
|
6331
|
+
].filter((key) => supplied(op[key]));
|
|
6332
|
+
if (given.length > 1) throw drizzleError(`Drizzle-GraphQL Error: '${plan.relationName}' takes one of ${given.join(", ")} at a time — it holds a single row.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
6154
6333
|
};
|
|
6155
6334
|
/** The values the junction stores for the target rows one operand selects. */
|
|
6156
6335
|
const selectedTargetKeys = async (executor, plan, filter, operation, scope) => {
|
|
@@ -6165,9 +6344,8 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6165
6344
|
* error on some of them — the read and the write share the surrounding transaction, so no
|
|
6166
6345
|
* other writer can slip a duplicate in between them.
|
|
6167
6346
|
*/
|
|
6168
|
-
const applyThroughOps = async (executor, plan, key, op, scope) => {
|
|
6347
|
+
const applyThroughOps = async (executor, plan, key, op, scope, steps) => {
|
|
6169
6348
|
const through = plan.through;
|
|
6170
|
-
const unlinkAll = () => executor.delete(through.table).where(eq(through.sourceCol, key));
|
|
6171
6349
|
const link = async (operands, operation) => {
|
|
6172
6350
|
const existing = new Set((await executor.select({ [through.targetColPropName]: through.targetCol }).from(through.table).where(eq(through.sourceCol, key))).map((row) => row[through.targetColPropName]));
|
|
6173
6351
|
const rows = [];
|
|
@@ -6178,19 +6356,19 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6178
6356
|
[through.targetColPropName]: targetKey
|
|
6179
6357
|
});
|
|
6180
6358
|
}
|
|
6181
|
-
if (rows.length) await executor.insert(through.table).values(rows);
|
|
6359
|
+
if (rows.length) await steps.run(executor.insert(through.table).values(rows), operation, "insert");
|
|
6182
6360
|
};
|
|
6183
|
-
if (op["set"]
|
|
6184
|
-
await
|
|
6361
|
+
if (supplied(op["set"])) {
|
|
6362
|
+
await steps.run(executor.delete(through.table).where(eq(through.sourceCol, key)), "set", "delete");
|
|
6185
6363
|
await link(asArray(op["set"]), "set");
|
|
6186
6364
|
}
|
|
6187
|
-
if (op["disconnect"]
|
|
6365
|
+
if (supplied(op["disconnect"])) for (const filter of asArray(op["disconnect"])) {
|
|
6188
6366
|
const targetKeys = await selectedTargetKeys(executor, plan, filter, "disconnect", scope);
|
|
6189
|
-
if (targetKeys.length) await executor.delete(through.table).where(and(eq(through.sourceCol, key), inArray(through.targetCol, targetKeys)));
|
|
6367
|
+
if (targetKeys.length) await steps.run(executor.delete(through.table).where(and(eq(through.sourceCol, key), inArray(through.targetCol, targetKeys))), "disconnect", "delete");
|
|
6190
6368
|
}
|
|
6191
|
-
if (op["connect"]
|
|
6369
|
+
if (supplied(op["connect"])) await link(asArray(op["connect"]), "connect");
|
|
6192
6370
|
};
|
|
6193
|
-
const detachAll =
|
|
6371
|
+
const detachAll = (executor, plan, key, scope, steps, op) => steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where(withScope(scope, plan.targetTableName, plan.targetTable, eq(plan.foreignCol, key))), op, "update");
|
|
6194
6372
|
return {
|
|
6195
6373
|
enabled: (tableName) => !!plans[tableName],
|
|
6196
6374
|
split: (tableName, values) => {
|
|
@@ -6219,19 +6397,27 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6219
6397
|
}
|
|
6220
6398
|
return columns;
|
|
6221
6399
|
},
|
|
6222
|
-
applyParentSide: async (executor, tableName, ops,
|
|
6400
|
+
applyParentSide: async (executor, tableName, ops, request) => {
|
|
6223
6401
|
const tablePlans = plans[tableName];
|
|
6224
6402
|
const patch = {};
|
|
6225
6403
|
if (!tablePlans) return patch;
|
|
6404
|
+
const context = request?.context;
|
|
6226
6405
|
const scope = resolveScope(policies, context, filterCtx);
|
|
6227
6406
|
for (const [relationName, op] of Object.entries(ops)) {
|
|
6228
6407
|
const plan = tablePlans[relationName];
|
|
6229
6408
|
if (plan?.fkSide !== "parent") continue;
|
|
6230
6409
|
assertSingleOperation(plan, op);
|
|
6231
6410
|
if (op["create"] !== void 0) {
|
|
6411
|
+
const steps = createHookSteps(onWrite, executor, request, tableName, plan);
|
|
6412
|
+
steps.add("create", "insert");
|
|
6413
|
+
await steps.before();
|
|
6232
6414
|
const values = applyContextValues(remapFromGraphQLSingleInput({ ...op["create"] }, plan.targetTable), contextValues?.(plan.targetTableName), context);
|
|
6233
|
-
const
|
|
6415
|
+
const step = steps.get("create", "insert");
|
|
6416
|
+
const inserted = await executor.insert(plan.targetTable).values(values).returning(step ? step.returning : { [plan.foreignColPropName]: plan.foreignCol });
|
|
6417
|
+
step?.rows.push(...inserted);
|
|
6418
|
+
const created = inserted[0];
|
|
6234
6419
|
if (!created) throw drizzleError(`Drizzle-GraphQL Error: 'create' on '${relationName}' inserted no row, so there is nothing to attach.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
6420
|
+
await steps.after();
|
|
6235
6421
|
patch[plan.localColPropName] = created[plan.foreignColPropName];
|
|
6236
6422
|
} else if (op["connect"] !== void 0) {
|
|
6237
6423
|
const rows = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
|
|
@@ -6241,41 +6427,46 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6241
6427
|
}
|
|
6242
6428
|
return patch;
|
|
6243
6429
|
},
|
|
6244
|
-
applyChildSide: async (executor, tableName, ops, parentRows,
|
|
6430
|
+
applyChildSide: async (executor, tableName, ops, parentRows, request) => {
|
|
6245
6431
|
const tablePlans = plans[tableName];
|
|
6246
6432
|
if (!tablePlans) return;
|
|
6433
|
+
const context = request?.context;
|
|
6247
6434
|
const scope = resolveScope(policies, context, filterCtx);
|
|
6248
6435
|
for (const [relationName, op] of Object.entries(ops)) {
|
|
6249
6436
|
const plan = tablePlans[relationName];
|
|
6250
6437
|
if (plan?.fkSide !== "child") continue;
|
|
6251
6438
|
if (plan.isOne) assertSingleOperation(plan, op);
|
|
6439
|
+
const steps = createHookSteps(onWrite, executor, request, tableName, plan);
|
|
6440
|
+
registerChildSteps(steps, plan, op);
|
|
6441
|
+
await steps.before();
|
|
6252
6442
|
for (const parentRow of parentRows) {
|
|
6253
6443
|
const key = parentRow?.[plan.localColPropName];
|
|
6254
6444
|
if (key === void 0 || key === null) throw drizzleError(`Drizzle-GraphQL Error: cannot write through '${relationName}': the ${tableName} row has no '${plan.localColPropName}' value to attach to.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
6255
6445
|
if (plan.through) {
|
|
6256
|
-
await applyThroughOps(executor, plan, key, op, scope);
|
|
6446
|
+
await applyThroughOps(executor, plan, key, op, scope, steps);
|
|
6257
6447
|
continue;
|
|
6258
6448
|
}
|
|
6259
6449
|
if (op["set"] !== void 0) {
|
|
6260
|
-
await detachAll(executor, plan, key, scope);
|
|
6261
|
-
for (const filter of asArray(op["set"])) await executor.update(plan.targetTable).set({ [plan.foreignColPropName]: key }).where(conditionOf(plan, filter, "set", scope));
|
|
6450
|
+
await detachAll(executor, plan, key, scope, steps, "set");
|
|
6451
|
+
for (const filter of asArray(op["set"])) await steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: key }).where(conditionOf(plan, filter, "set", scope)), "set", "update");
|
|
6262
6452
|
}
|
|
6263
6453
|
if (op["disconnect"] !== void 0) {
|
|
6264
6454
|
if (plan.isOne) {
|
|
6265
|
-
if (op["disconnect"] === true) await detachAll(executor, plan, key, scope);
|
|
6266
|
-
} else for (const filter of asArray(op["disconnect"])) await executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where(and(eq(plan.foreignCol, key), conditionOf(plan, filter, "disconnect", scope)));
|
|
6455
|
+
if (op["disconnect"] === true) await detachAll(executor, plan, key, scope, steps, "disconnect");
|
|
6456
|
+
} else for (const filter of asArray(op["disconnect"])) await steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where(and(eq(plan.foreignCol, key), conditionOf(plan, filter, "disconnect", scope))), "disconnect", "update");
|
|
6267
6457
|
}
|
|
6268
|
-
const
|
|
6269
|
-
if (plan.isOne &&
|
|
6270
|
-
if (op["connect"] !== void 0) for (const filter of asArray(op["connect"])) await executor.update(plan.targetTable).set({ [plan.foreignColPropName]: key }).where(conditionOf(plan, filter, "connect", scope));
|
|
6458
|
+
const attachedBy = attachingOp(op);
|
|
6459
|
+
if (plan.isOne && attachedBy && plan.canDetach && op["set"] === void 0) await detachAll(executor, plan, key, scope, steps, attachedBy);
|
|
6460
|
+
if (op["connect"] !== void 0) for (const filter of asArray(op["connect"])) await steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: key }).where(conditionOf(plan, filter, "connect", scope)), "connect", "update");
|
|
6271
6461
|
if (op["create"] !== void 0) {
|
|
6272
6462
|
const rows = asArray(op["create"]).map((row) => ({
|
|
6273
6463
|
...applyContextValues(remapFromGraphQLSingleInput({ ...row }, plan.targetTable), contextValues?.(plan.targetTableName), context),
|
|
6274
6464
|
[plan.foreignColPropName]: key
|
|
6275
6465
|
}));
|
|
6276
|
-
if (rows.length) await executor.insert(plan.targetTable).values(rows);
|
|
6466
|
+
if (rows.length) await steps.run(executor.insert(plan.targetTable).values(rows), "create", "insert");
|
|
6277
6467
|
}
|
|
6278
6468
|
}
|
|
6469
|
+
await steps.after();
|
|
6279
6470
|
}
|
|
6280
6471
|
}
|
|
6281
6472
|
};
|
|
@@ -6285,21 +6476,25 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6285
6476
|
* inside a single transaction — a savepoint when the caller already runs on one, so a failing
|
|
6286
6477
|
* entry rolls the whole batch back.
|
|
6287
6478
|
*
|
|
6479
|
+
* That transaction is also the `tx` the target tables' `onWrite` hooks run on. Every nested
|
|
6480
|
+
* write opens it, so a hook on a nested target has the whole tree to roll back even when the
|
|
6481
|
+
* root field carries no hook of its own and was not forced into a transaction for one.
|
|
6482
|
+
*
|
|
6288
6483
|
* Entries are written one at a time rather than as a single multi-row statement: a nested
|
|
6289
6484
|
* operation attaches to the row its own entry produced, so the two have to stay correlated.
|
|
6290
6485
|
* The batch path is untouched when no entry carries a nested operation.
|
|
6291
6486
|
*/
|
|
6292
6487
|
const writeWithNestedOps = async (params) => {
|
|
6293
|
-
const { executor, runtime, tableName, entries, remapValues, write,
|
|
6488
|
+
const { executor, runtime, tableName, entries, remapValues, write, request } = params;
|
|
6294
6489
|
return executor.transaction(async (tx) => {
|
|
6295
6490
|
const rows = [];
|
|
6296
6491
|
for (const entry of entries) {
|
|
6297
|
-
const patch = await runtime.applyParentSide(tx, tableName, entry.ops,
|
|
6492
|
+
const patch = await runtime.applyParentSide(tx, tableName, entry.ops, request);
|
|
6298
6493
|
const written = await write(tx, {
|
|
6299
6494
|
...remapValues(entry.columns),
|
|
6300
6495
|
...patch
|
|
6301
6496
|
});
|
|
6302
|
-
await runtime.applyChildSide(tx, tableName, entry.ops, written,
|
|
6497
|
+
await runtime.applyChildSide(tx, tableName, entry.ops, written, request);
|
|
6303
6498
|
rows.push(...written);
|
|
6304
6499
|
}
|
|
6305
6500
|
return rows;
|
|
@@ -6311,14 +6506,14 @@ const writeWithNestedOps = async (params) => {
|
|
|
6311
6506
|
* attaches to each row that came back.
|
|
6312
6507
|
*/
|
|
6313
6508
|
const updateWithNestedOps = async (params) => {
|
|
6314
|
-
const { executor, runtime, tableName, columns, ops, remapValues, write,
|
|
6509
|
+
const { executor, runtime, tableName, columns, ops, remapValues, write, request } = params;
|
|
6315
6510
|
return executor.transaction(async (tx) => {
|
|
6316
|
-
const patch = await runtime.applyParentSide(tx, tableName, ops,
|
|
6511
|
+
const patch = await runtime.applyParentSide(tx, tableName, ops, request);
|
|
6317
6512
|
const rows = await write(tx, {
|
|
6318
6513
|
...remapValues(columns),
|
|
6319
6514
|
...patch
|
|
6320
6515
|
});
|
|
6321
|
-
await runtime.applyChildSide(tx, tableName, ops, rows,
|
|
6516
|
+
await runtime.applyChildSide(tx, tableName, ops, rows, request);
|
|
6322
6517
|
return rows;
|
|
6323
6518
|
});
|
|
6324
6519
|
};
|
|
@@ -6519,7 +6714,14 @@ const createSchemaBuilder = (adapter) => {
|
|
|
6519
6714
|
uniqueKeysOf: (tableName) => uniqueKeys[tableName]
|
|
6520
6715
|
};
|
|
6521
6716
|
adapter.preflight?.(db, options);
|
|
6522
|
-
const nestedPlans = features.nestedWrites ? buildNestedWritePlans(tables, namedRelations, (target) => uniqueColumnSets(target), (target) => primaryKeyPropNames(target), extractRelationJoinColumns) : void 0;
|
|
6717
|
+
const nestedPlans = features.nestedWrites ? buildNestedWritePlans(tables, namedRelations, (target) => uniqueColumnSets(target), (target) => primaryKeyPropNames(target), extractRelationJoinColumns, (junction) => schemaEntries.find(([, value]) => value === junction)?.[0]) : void 0;
|
|
6718
|
+
if (nestedPlans) {
|
|
6719
|
+
const omitted = gateNestedWritePlans(nestedPlans, featureOf, (tableName) => excludedTables.has(tableName));
|
|
6720
|
+
if (omitted.length) {
|
|
6721
|
+
const listed = omitted.length > 5 ? `${omitted.slice(0, 5).join(", ")} and ${omitted.length - 5} more` : omitted.join(", ");
|
|
6722
|
+
console.warn(`Drizzle-GraphQL Warning: config.features — nestedWrites leaves out the nested inputs whose table has that write turned off: ${listed}.`);
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6523
6725
|
const nestedTypes = nestedPlans ? createNestedWriteTypes({
|
|
6524
6726
|
plans: nestedPlans,
|
|
6525
6727
|
cacheCtx,
|
|
@@ -7192,7 +7394,11 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
7192
7394
|
entries: nestedEntries,
|
|
7193
7395
|
remapValues: (values) => applyContextValues(remapFromGraphQLSingleInput(values, table), contextColumns, context),
|
|
7194
7396
|
write: (tx, values) => runInsert(tx, [values]),
|
|
7195
|
-
|
|
7397
|
+
request: {
|
|
7398
|
+
context,
|
|
7399
|
+
args,
|
|
7400
|
+
info
|
|
7401
|
+
}
|
|
7196
7402
|
}) : await runInsert(executor, input);
|
|
7197
7403
|
await after(written);
|
|
7198
7404
|
const result = output(written);
|
|
@@ -7281,7 +7487,11 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
7281
7487
|
entries: nestedEntries,
|
|
7282
7488
|
remapValues: (values) => applyContextValues(remapFromGraphQLSingleInput(values, table), contextColumns, context),
|
|
7283
7489
|
write: (tx, values) => runUpsert(tx, [values]),
|
|
7284
|
-
|
|
7490
|
+
request: {
|
|
7491
|
+
context,
|
|
7492
|
+
args,
|
|
7493
|
+
info
|
|
7494
|
+
}
|
|
7285
7495
|
}) : await runUpsert(executor, input);
|
|
7286
7496
|
await after(written);
|
|
7287
7497
|
const result = output(written);
|
|
@@ -7354,7 +7564,11 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
7354
7564
|
ops: nestedOps,
|
|
7355
7565
|
remapValues: (values) => values,
|
|
7356
7566
|
write: runUpdate,
|
|
7357
|
-
|
|
7567
|
+
request: {
|
|
7568
|
+
context,
|
|
7569
|
+
args,
|
|
7570
|
+
info
|
|
7571
|
+
}
|
|
7358
7572
|
}) : await runUpdate(executor, input);
|
|
7359
7573
|
await after(written);
|
|
7360
7574
|
const result = output(written);
|
|
@@ -7784,18 +7998,18 @@ const createSchemaDataGenerator = (adapter) => {
|
|
|
7784
7998
|
* the shared multi-mutation transaction opened by `runMutation` — this opens a savepoint, so
|
|
7785
7999
|
* the batch stays atomic without breaking the outer transaction.
|
|
7786
8000
|
*/
|
|
7787
|
-
const runUpdateManyBatch = ({ executor, entries, table, returning, nested, tableName,
|
|
8001
|
+
const runUpdateManyBatch = ({ executor, entries, table, returning, nested, tableName, request }) => executor.transaction(async (tx) => {
|
|
7788
8002
|
const results = [];
|
|
7789
8003
|
for (const entry of entries) {
|
|
7790
8004
|
const values = entry.ops ? {
|
|
7791
8005
|
...entry.set,
|
|
7792
|
-
...await nested.applyParentSide(tx, tableName, entry.ops,
|
|
8006
|
+
...await nested.applyParentSide(tx, tableName, entry.ops, request)
|
|
7793
8007
|
} : entry.set;
|
|
7794
8008
|
const writes = Object.keys(values).length > 0;
|
|
7795
8009
|
let query = writes ? tx.update(table).set(values) : tx.select(returning).from(table);
|
|
7796
8010
|
if (entry.filters) query = query.where(entry.filters);
|
|
7797
8011
|
const rows = await (writes ? query.returning(returning) : query);
|
|
7798
|
-
if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows,
|
|
8012
|
+
if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows, request);
|
|
7799
8013
|
results.push(rows);
|
|
7800
8014
|
}
|
|
7801
8015
|
return results;
|
|
@@ -7868,7 +8082,11 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
|
|
|
7868
8082
|
anyNested,
|
|
7869
8083
|
nested,
|
|
7870
8084
|
tableName,
|
|
7871
|
-
|
|
8085
|
+
request: {
|
|
8086
|
+
context,
|
|
8087
|
+
args,
|
|
8088
|
+
info
|
|
8089
|
+
}
|
|
7872
8090
|
});
|
|
7873
8091
|
const written = perEntry.flat();
|
|
7874
8092
|
await after(written);
|
|
@@ -7917,7 +8135,7 @@ const sqlitePrimaryKeyPropNames = (table) => getPrimaryKeyPropNamesFromConfig(ta
|
|
|
7917
8135
|
* would run after the COMMIT. The driver kind is probed from the first statement's result
|
|
7918
8136
|
* rather than declared, so the same resolver serves both driver families.
|
|
7919
8137
|
*/
|
|
7920
|
-
const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNested, nested, tableName,
|
|
8138
|
+
const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNested, nested, tableName, request }) => {
|
|
7921
8139
|
const runEntry = (tx, entry) => {
|
|
7922
8140
|
let query = tx.update(table).set(entry.set);
|
|
7923
8141
|
if (entry.filters) query = query.where(entry.filters);
|
|
@@ -7926,13 +8144,13 @@ const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNest
|
|
|
7926
8144
|
const runNestedEntry = async (tx, entry) => {
|
|
7927
8145
|
const values = entry.ops ? {
|
|
7928
8146
|
...entry.set,
|
|
7929
|
-
...await nested.applyParentSide(tx, tableName, entry.ops,
|
|
8147
|
+
...await nested.applyParentSide(tx, tableName, entry.ops, request)
|
|
7930
8148
|
} : entry.set;
|
|
7931
8149
|
const writes = Object.keys(values).length > 0;
|
|
7932
8150
|
let query = writes ? tx.update(table).set(values) : tx.select(returning).from(table);
|
|
7933
8151
|
if (entry.filters) query = query.where(entry.filters);
|
|
7934
8152
|
const rows = await (writes ? query.returning(returning) : query);
|
|
7935
|
-
if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows,
|
|
8153
|
+
if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows, request);
|
|
7936
8154
|
return rows;
|
|
7937
8155
|
};
|
|
7938
8156
|
return executor.transaction((tx) => {
|