@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/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: !(fkSide === "child" ? foreignCol : localCol).notNull
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` are offered unconditionally, since breaking a link deletes a
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 (!filters) return;
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
- fields["disconnect"] = {
6051
- type: listOf(filters),
6052
- description: `Unlink every linked ${targetTypeName} row these match, leaving the rows in place`
6053
- };
6054
- fields["set"] = {
6055
- type: listOf(filters),
6056
- description: `Replace the whole set: unlink everything linked, then link every ${targetTypeName} row these match. \`set: []\` clears the relation. Applied before \`disconnect\` and \`connect\`.`
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 && plan.canDetach) fields["disconnect"] = {
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
- fields["connect"] = {
6079
- type: listOf(filters),
6080
- description: `Attach every existing ${targetTypeName} row these match`
6081
- };
6082
- if (forUpdate && plan.canDetach) {
6083
- fields["disconnect"] = {
6084
- type: listOf(filters),
6085
- description: `Detach every attached ${targetTypeName} row these match, leaving the rows in place`
6086
- };
6087
- fields["set"] = {
6088
- type: listOf(filters),
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 supplied = [
6327
+ const given = [
6149
6328
  "create",
6150
6329
  "connect",
6151
6330
  "disconnect"
6152
- ].filter((key) => op[key] !== void 0 && op[key] !== null);
6153
- if (supplied.length > 1) throw drizzleError(`Drizzle-GraphQL Error: '${plan.relationName}' takes one of ${supplied.join(", ")} at a time — it holds a single row.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
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"] !== void 0 && op["set"] !== null) {
6184
- await unlinkAll();
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"] !== void 0 && op["disconnect"] !== null) for (const filter of asArray(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"] !== void 0 && op["connect"] !== null) await link(asArray(op["connect"]), "connect");
6369
+ if (supplied(op["connect"])) await link(asArray(op["connect"]), "connect");
6192
6370
  };
6193
- const detachAll = async (executor, plan, key, scope) => executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where(withScope(scope, plan.targetTableName, plan.targetTable, eq(plan.foreignCol, key)));
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, context) => {
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 created = (await executor.insert(plan.targetTable).values(values).returning({ [plan.foreignColPropName]: plan.foreignCol }))[0];
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, context) => {
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 attaches = op["connect"] !== void 0 || op["create"] !== void 0;
6269
- if (plan.isOne && attaches && plan.canDetach && op["set"] === void 0) await detachAll(executor, plan, key, scope);
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, context } = params;
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, context);
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, context);
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, context } = params;
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, context);
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, context);
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
- context
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
- context
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
- context
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, context }) => executor.transaction(async (tx) => {
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, context)
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, context);
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
- context
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, context }) => {
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, context)
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, context);
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) => {