@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.cjs CHANGED
@@ -5251,7 +5251,6 @@ const resolveBuildConfig = (config, schema) => {
5251
5251
  const missing = [!tableFeatures.insert ? "insert" : void 0, !tableFeatures.update ? "update" : void 0].filter(Boolean).join(" and ");
5252
5252
  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);
5253
5253
  }
5254
- 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);
5255
5254
  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);
5256
5255
  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);
5257
5256
  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);
@@ -5857,6 +5856,12 @@ const generateGroupBy = (db, tableName, table, typeName, fieldName, filterArgs,
5857
5856
  };
5858
5857
  //#endregion
5859
5858
  //#region src/util/builders/nested-writes.ts
5859
+ const ALL_OPS = [
5860
+ "create",
5861
+ "connect",
5862
+ "disconnect",
5863
+ "set"
5864
+ ];
5860
5865
  /**
5861
5866
  * Whether a column can be left out of an INSERT — either the database fills it in or it
5862
5867
  * accepts NULL.
@@ -5885,7 +5890,7 @@ const throughColumn = (entry) => entry && (0, drizzle_orm.is)(entry, drizzle_orm
5885
5890
  * joining on more than one column per side; and a junction carrying a further NOT NULL column
5886
5891
  * with no default, which no `connect` operand could supply a value for.
5887
5892
  */
5888
- const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable) => {
5893
+ const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable, tableNameOf) => {
5889
5894
  if ((0, drizzle_orm.is)(relation, drizzle_orm.One)) return;
5890
5895
  const junction = relation.throughTable;
5891
5896
  const sourceEntries = relation.through?.source;
@@ -5916,11 +5921,18 @@ const buildThroughPlan = (relationName, relation, table, targetTableName, target
5916
5921
  canDetach: true,
5917
5922
  through: {
5918
5923
  table: junction,
5924
+ tableName: tableNameOf(junction),
5919
5925
  sourceColPropName,
5920
5926
  targetColPropName,
5921
5927
  sourceCol,
5922
5928
  targetCol
5923
- }
5929
+ },
5930
+ targetKeyColumns: { [foreignColPropName]: foreignCol },
5931
+ allowed: /* @__PURE__ */ new Set([
5932
+ "connect",
5933
+ "disconnect",
5934
+ "set"
5935
+ ])
5924
5936
  };
5925
5937
  };
5926
5938
  /**
@@ -5933,7 +5945,7 @@ const buildThroughPlan = (relationName, relation, table, targetTableName, target
5933
5945
  * relations whose join columns cannot be read off the drizzle relation; and relations where
5934
5946
  * neither join column is unique, which leaves no way to tell which side stores the key.
5935
5947
  */
5936
- const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePrimaryKeys, extractJoinColumns) => {
5948
+ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePrimaryKeys, extractJoinColumns, tableNameOf = () => void 0) => {
5937
5949
  const uniqueCache = /* @__PURE__ */ new Map();
5938
5950
  const singleColumnUniques = (tableName, table) => {
5939
5951
  let cached = uniqueCache.get(tableName);
@@ -5962,7 +5974,7 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
5962
5974
  const targetTable = tables[targetTableName];
5963
5975
  if (!targetTable) continue;
5964
5976
  if (relation.through) {
5965
- const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable);
5977
+ const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable, tableNameOf);
5966
5978
  if (throughPlan) tablePlans[relationName] = throughPlan;
5967
5979
  continue;
5968
5980
  }
@@ -5983,6 +5995,10 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
5983
5995
  else if (foreignIsUnique) fkSide = "parent";
5984
5996
  else if (localIsUnique) fkSide = "child";
5985
5997
  else continue;
5998
+ const fkColumn = fkSide === "child" ? foreignCol : localCol;
5999
+ const targetColumns = (0, drizzle_orm.getColumns)(targetTable);
6000
+ const targetKeyColumns = Object.fromEntries(resolvePrimaryKeys(targetTable).filter((name) => targetColumns[name]).map((name) => [name, targetColumns[name]]));
6001
+ targetKeyColumns[foreignColPropName] = foreignCol;
5986
6002
  tablePlans[relationName] = {
5987
6003
  relationName,
5988
6004
  targetTableName,
@@ -5993,13 +6009,87 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
5993
6009
  localCol,
5994
6010
  isOne,
5995
6011
  fkSide,
5996
- canDetach: !(fkSide === "child" ? foreignCol : localCol).notNull
6012
+ canDetach: !fkColumn.notNull,
6013
+ targetKeyColumns,
6014
+ allowed: new Set(!fkColumn.notNull ? isOne ? [
6015
+ "create",
6016
+ "connect",
6017
+ "disconnect"
6018
+ ] : ALL_OPS : ["create", "connect"])
5997
6019
  };
5998
6020
  }
5999
6021
  if (Object.keys(tablePlans).length) plans[tableName] = tablePlans;
6000
6022
  }
6001
6023
  return plans;
6002
6024
  };
6025
+ /**
6026
+ * Which of the target's (or junction's) write features each input of a relation needs. A
6027
+ * parent-side `connect` / `disconnect` only rewrites the parent's own column, so it needs
6028
+ * nothing of the target; a child-side one moves the target's foreign key, which is an update
6029
+ * of the target. A to-one child-side `create` also detaches the row attached before it, when
6030
+ * the key is nullable.
6031
+ */
6032
+ const requiredWrites = (plan, op) => {
6033
+ if (plan.through) {
6034
+ const junction = plan.through.tableName;
6035
+ if (op === "connect") return [{
6036
+ tableName: junction,
6037
+ feature: "insert"
6038
+ }];
6039
+ if (op === "disconnect") return [{
6040
+ tableName: junction,
6041
+ feature: "delete"
6042
+ }];
6043
+ if (op === "set") return [{
6044
+ tableName: junction,
6045
+ feature: "delete"
6046
+ }, {
6047
+ tableName: junction,
6048
+ feature: "insert"
6049
+ }];
6050
+ return [];
6051
+ }
6052
+ const target = plan.targetTableName;
6053
+ if (op === "create") return plan.fkSide === "child" && plan.isOne && plan.canDetach ? [{
6054
+ tableName: target,
6055
+ feature: "insert"
6056
+ }, {
6057
+ tableName: target,
6058
+ feature: "update"
6059
+ }] : [{
6060
+ tableName: target,
6061
+ feature: "insert"
6062
+ }];
6063
+ return plan.fkSide === "child" ? [{
6064
+ tableName: target,
6065
+ feature: "update"
6066
+ }] : [];
6067
+ };
6068
+ /**
6069
+ * Narrows every plan's `allowed` inputs to the ones whose writes the table they land on
6070
+ * permits: its `features.insert` / `update` / `delete`, and for a junction table that it is
6071
+ * not excluded. A plan left with nothing is dropped, so its relation field is not generated.
6072
+ *
6073
+ * Returns one line per input taken out, for the build note.
6074
+ */
6075
+ const gateNestedWritePlans = (plans, featureOf, isExcluded) => {
6076
+ const omitted = [];
6077
+ for (const [tableName, tablePlans] of Object.entries(plans)) {
6078
+ for (const [relationName, plan] of Object.entries(tablePlans)) {
6079
+ for (const op of [...plan.allowed]) {
6080
+ const blocked = requiredWrites(plan, op).filter(({ tableName: target, feature }) => target !== void 0 && (isExcluded(target) || !featureOf(target)[feature]));
6081
+ if (blocked.length) {
6082
+ plan.allowed.delete(op);
6083
+ const reasons = blocked.map(({ tableName: target, feature }) => isExcluded(target) ? `${target} is excluded` : `${target}.${feature} is off`);
6084
+ omitted.push(`${tableName}.${relationName}.${op} (${[...new Set(reasons)].join(", ")})`);
6085
+ }
6086
+ }
6087
+ if (!plan.allowed.size) delete tablePlans[relationName];
6088
+ }
6089
+ if (!Object.keys(tablePlans).length) delete plans[tableName];
6090
+ }
6091
+ return omitted;
6092
+ };
6003
6093
  const NO_COLUMNS = /* @__PURE__ */ new Set();
6004
6094
  /**
6005
6095
  * Builds the nested-write input types for a schema.
@@ -6054,8 +6144,11 @@ const createNestedWriteTypes = (params) => {
6054
6144
  *
6055
6145
  * A many-to-many relation offers no `create`: inserting a row *and* linking it is two
6056
6146
  * writes to two tables, and the link half is what the relation field is for. Its
6057
- * `disconnect` and `set` are offered unconditionally, since breaking a link deletes a
6147
+ * `disconnect` and `set` do not depend on nullability, since breaking a link deletes a
6058
6148
  * junction row rather than nulling a column that might be NOT NULL.
6149
+ *
6150
+ * Each input is also subject to `plan.allowed` — the table it writes has to have that
6151
+ * write turned on (see {@link gateNestedWritePlans}).
6059
6152
  */
6060
6153
  const wrapperType = (tableName, typeName, plan, forUpdate) => {
6061
6154
  const key = `${tableName}.${plan.relationName}.${forUpdate ? "update" : "create"}`;
@@ -6064,57 +6157,52 @@ const createNestedWriteTypes = (params) => {
6064
6157
  const filters = targetFilters(plan);
6065
6158
  const targetTypeName = resolveTypeName(plan.targetTableName, typeNameMapper);
6066
6159
  const fields = {};
6160
+ const offers = (op) => plan.allowed.has(op);
6067
6161
  if (plan.through) {
6068
- if (!filters) return;
6069
- fields["connect"] = {
6162
+ if (filters && offers("connect")) fields["connect"] = {
6070
6163
  type: listOf(filters),
6071
6164
  description: `Link every existing ${targetTypeName} row these match. Already-linked rows are left alone.`
6072
6165
  };
6073
- if (forUpdate) {
6074
- fields["disconnect"] = {
6075
- type: listOf(filters),
6076
- description: `Unlink every linked ${targetTypeName} row these match, leaving the rows in place`
6077
- };
6078
- fields["set"] = {
6079
- type: listOf(filters),
6080
- description: `Replace the whole set: unlink everything linked, then link every ${targetTypeName} row these match. \`set: []\` clears the relation. Applied before \`disconnect\` and \`connect\`.`
6081
- };
6082
- }
6166
+ if (filters && forUpdate && offers("disconnect")) fields["disconnect"] = {
6167
+ type: listOf(filters),
6168
+ description: `Unlink every linked ${targetTypeName} row these match, leaving the rows in place`
6169
+ };
6170
+ if (filters && forUpdate && offers("set")) fields["set"] = {
6171
+ type: listOf(filters),
6172
+ description: `Replace the whole set: unlink everything linked, then link every ${targetTypeName} row these match. \`set: []\` clears the relation. Applied before \`disconnect\` and \`connect\`.`
6173
+ };
6083
6174
  } else if (plan.isOne) {
6084
- fields["create"] = {
6175
+ if (offers("create")) fields["create"] = {
6085
6176
  type: payloadType(tableName, typeName, plan),
6086
6177
  description: `Insert a ${targetTypeName} row and attach it`
6087
6178
  };
6088
- if (filters) fields["connect"] = {
6179
+ if (filters && offers("connect")) fields["connect"] = {
6089
6180
  type: filters,
6090
6181
  description: `Attach the existing ${targetTypeName} row this matches. Must match exactly one row.`
6091
6182
  };
6092
- if (forUpdate && plan.canDetach) fields["disconnect"] = {
6183
+ if (forUpdate && offers("disconnect")) fields["disconnect"] = {
6093
6184
  type: graphql.GraphQLBoolean,
6094
6185
  description: `Detach the currently attached ${targetTypeName} row, leaving it in place`
6095
6186
  };
6096
6187
  } else {
6097
- fields["create"] = {
6188
+ if (offers("create")) fields["create"] = {
6098
6189
  type: listOf(payloadType(tableName, typeName, plan)),
6099
6190
  description: `Insert these ${targetTypeName} rows and attach them`
6100
6191
  };
6101
- if (filters) {
6102
- fields["connect"] = {
6103
- type: listOf(filters),
6104
- description: `Attach every existing ${targetTypeName} row these match`
6105
- };
6106
- if (forUpdate && plan.canDetach) {
6107
- fields["disconnect"] = {
6108
- type: listOf(filters),
6109
- description: `Detach every attached ${targetTypeName} row these match, leaving the rows in place`
6110
- };
6111
- fields["set"] = {
6112
- type: listOf(filters),
6113
- description: `Replace the whole set: detach everything attached, then attach every ${targetTypeName} row these match. Applied before \`disconnect\`, \`connect\` and \`create\`.`
6114
- };
6115
- }
6116
- }
6192
+ if (filters && offers("connect")) fields["connect"] = {
6193
+ type: listOf(filters),
6194
+ description: `Attach every existing ${targetTypeName} row these match`
6195
+ };
6196
+ if (filters && forUpdate && offers("disconnect")) fields["disconnect"] = {
6197
+ type: listOf(filters),
6198
+ description: `Detach every attached ${targetTypeName} row these match, leaving the rows in place`
6199
+ };
6200
+ if (filters && forUpdate && offers("set")) fields["set"] = {
6201
+ type: listOf(filters),
6202
+ description: `Replace the whole set: detach everything attached, then attach every ${targetTypeName} row these match. Applied before \`disconnect\`, \`connect\` and \`create\`.`
6203
+ };
6117
6204
  }
6205
+ if (!Object.keys(fields).length) return;
6118
6206
  const type = new graphql.GraphQLInputObjectType({
6119
6207
  name: cacheCtx.typeName({
6120
6208
  kind: "nestedWriteInput",
@@ -6146,7 +6234,7 @@ const createNestedWriteTypes = (params) => {
6146
6234
  const tablePlans = plans[tableName];
6147
6235
  if (!tablePlans) return NO_COLUMNS;
6148
6236
  const relaxed = /* @__PURE__ */ new Set();
6149
- for (const plan of Object.values(tablePlans)) if (plan.fkSide === "parent" && !isOptionalOnInsert(plan.localCol)) relaxed.add(plan.localColPropName);
6237
+ 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);
6150
6238
  return relaxed.size ? relaxed : NO_COLUMNS;
6151
6239
  }
6152
6240
  };
@@ -6157,8 +6245,99 @@ const createNestedWriteTypes = (params) => {
6157
6245
  */
6158
6246
  const mergedOps = (entries) => Object.assign({}, ...entries.map((entry) => entry.ops));
6159
6247
  const asArray = (value) => Array.isArray(value) ? value : value === void 0 ? [] : [value];
6248
+ const supplied = (value) => value !== void 0 && value !== null;
6249
+ /**
6250
+ * The hook calls one relation field makes. Every step is registered before the field's first
6251
+ * statement, so each `before` runs ahead of all of them and each `after` once the last has
6252
+ * returned. A table without hooks registers nothing, and its statements run exactly as they
6253
+ * did before hooks reached nested writes — no RETURNING clause is added.
6254
+ */
6255
+ const createHookSteps = (onWrite, executor, request, parentTable, plan) => {
6256
+ const steps = /* @__PURE__ */ new Map();
6257
+ const keyOf = (op, operation) => `${op}:${operation}`;
6258
+ const payload = (step) => ({
6259
+ table: step.tableName,
6260
+ operation: step.operation,
6261
+ single: plan.isOne,
6262
+ args: request?.args,
6263
+ context: request?.context,
6264
+ info: request?.info,
6265
+ tx: executor,
6266
+ nested: {
6267
+ parentTable,
6268
+ relation: plan.relationName,
6269
+ op: step.op
6270
+ }
6271
+ });
6272
+ return {
6273
+ /** Registers the hook call for `op`'s `operation` statements, if that table has hooks. */
6274
+ add: (op, operation) => {
6275
+ const through = plan.through;
6276
+ const tableName = through ? through.tableName : plan.targetTableName;
6277
+ const hooks = tableName ? onWrite?.(tableName, operation) : void 0;
6278
+ if (!tableName || !hooks || steps.has(keyOf(op, operation))) return;
6279
+ const keyColumns = through ? {
6280
+ [through.sourceColPropName]: through.sourceCol,
6281
+ [through.targetColPropName]: through.targetCol
6282
+ } : { ...plan.targetKeyColumns };
6283
+ steps.set(keyOf(op, operation), {
6284
+ tableName,
6285
+ operation,
6286
+ op,
6287
+ hooks,
6288
+ returning: withHookColumns(hooks, keyColumns).returning,
6289
+ rows: []
6290
+ });
6291
+ },
6292
+ get: (op, operation) => steps.get(keyOf(op, operation)),
6293
+ /** Runs one statement, returning its rows to the step it belongs to when there is one. */
6294
+ run: async (query, op, operation) => {
6295
+ const step = steps.get(keyOf(op, operation));
6296
+ if (!step) {
6297
+ await query;
6298
+ return;
6299
+ }
6300
+ step.rows.push(...await query.returning(step.returning));
6301
+ },
6302
+ before: async () => {
6303
+ for (const step of steps.values()) await runWriteHook(step.hooks, "before", payload(step));
6304
+ },
6305
+ after: async () => {
6306
+ for (const step of steps.values()) await runWriteHook(step.hooks, "after", {
6307
+ ...payload(step),
6308
+ rows: step.rows
6309
+ });
6310
+ }
6311
+ };
6312
+ };
6313
+ /**
6314
+ * Registers the hook calls a child-side relation field will make, in the order its
6315
+ * statements run, so the `before` hooks fire in that order too. Mirrors the branches of
6316
+ * `applyChildSide` and `applyThroughOps` exactly: a step registered for an input that runs no
6317
+ * statement would fire with no rows, and one missing would miss them.
6318
+ */
6319
+ const registerChildSteps = (steps, plan, op) => {
6320
+ if (plan.through) {
6321
+ if (supplied(op["set"])) {
6322
+ steps.add("set", "delete");
6323
+ steps.add("set", "insert");
6324
+ }
6325
+ if (supplied(op["disconnect"])) steps.add("disconnect", "delete");
6326
+ if (supplied(op["connect"])) steps.add("connect", "insert");
6327
+ return;
6328
+ }
6329
+ if (op["set"] !== void 0) steps.add("set", "update");
6330
+ if (op["disconnect"] !== void 0 && (!plan.isOne || op["disconnect"] === true)) steps.add("disconnect", "update");
6331
+ const attachedBy = attachingOp(op);
6332
+ if (plan.isOne && attachedBy && plan.canDetach && op["set"] === void 0) steps.add(attachedBy, "update");
6333
+ if (op["connect"] !== void 0) steps.add("connect", "update");
6334
+ if (op["create"] !== void 0) steps.add("create", "insert");
6335
+ };
6336
+ /** The input that attaches a row to a to-one, which detaches whatever was attached before. */
6337
+ const attachingOp = (op) => op["connect"] !== void 0 ? "connect" : op["create"] !== void 0 ? "create" : void 0;
6160
6338
  const createNestedWriteRuntime = (params) => {
6161
6339
  const { plans, filterCtx, policies, contextValues } = params;
6340
+ const onWrite = policies?.onWrite;
6162
6341
  /**
6163
6342
  * A `connect` / `disconnect` / `set` operand compiled to SQL. An operand that compiles to
6164
6343
  * nothing is rejected: `connect: {}` would otherwise attach every row in the table.
@@ -6169,12 +6348,12 @@ const createNestedWriteRuntime = (params) => {
6169
6348
  return withScope(scope, plan.targetTableName, plan.targetTable, condition);
6170
6349
  };
6171
6350
  const assertSingleOperation = (plan, op) => {
6172
- const supplied = [
6351
+ const given = [
6173
6352
  "create",
6174
6353
  "connect",
6175
6354
  "disconnect"
6176
- ].filter((key) => op[key] !== void 0 && op[key] !== null);
6177
- 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" });
6355
+ ].filter((key) => supplied(op[key]));
6356
+ 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" });
6178
6357
  };
6179
6358
  /** The values the junction stores for the target rows one operand selects. */
6180
6359
  const selectedTargetKeys = async (executor, plan, filter, operation, scope) => {
@@ -6189,9 +6368,8 @@ const createNestedWriteRuntime = (params) => {
6189
6368
  * error on some of them — the read and the write share the surrounding transaction, so no
6190
6369
  * other writer can slip a duplicate in between them.
6191
6370
  */
6192
- const applyThroughOps = async (executor, plan, key, op, scope) => {
6371
+ const applyThroughOps = async (executor, plan, key, op, scope, steps) => {
6193
6372
  const through = plan.through;
6194
- const unlinkAll = () => executor.delete(through.table).where((0, drizzle_orm.eq)(through.sourceCol, key));
6195
6373
  const link = async (operands, operation) => {
6196
6374
  const existing = new Set((await executor.select({ [through.targetColPropName]: through.targetCol }).from(through.table).where((0, drizzle_orm.eq)(through.sourceCol, key))).map((row) => row[through.targetColPropName]));
6197
6375
  const rows = [];
@@ -6202,19 +6380,19 @@ const createNestedWriteRuntime = (params) => {
6202
6380
  [through.targetColPropName]: targetKey
6203
6381
  });
6204
6382
  }
6205
- if (rows.length) await executor.insert(through.table).values(rows);
6383
+ if (rows.length) await steps.run(executor.insert(through.table).values(rows), operation, "insert");
6206
6384
  };
6207
- if (op["set"] !== void 0 && op["set"] !== null) {
6208
- await unlinkAll();
6385
+ if (supplied(op["set"])) {
6386
+ await steps.run(executor.delete(through.table).where((0, drizzle_orm.eq)(through.sourceCol, key)), "set", "delete");
6209
6387
  await link(asArray(op["set"]), "set");
6210
6388
  }
6211
- if (op["disconnect"] !== void 0 && op["disconnect"] !== null) for (const filter of asArray(op["disconnect"])) {
6389
+ if (supplied(op["disconnect"])) for (const filter of asArray(op["disconnect"])) {
6212
6390
  const targetKeys = await selectedTargetKeys(executor, plan, filter, "disconnect", scope);
6213
- if (targetKeys.length) await executor.delete(through.table).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(through.sourceCol, key), (0, drizzle_orm.inArray)(through.targetCol, targetKeys)));
6391
+ if (targetKeys.length) await steps.run(executor.delete(through.table).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(through.sourceCol, key), (0, drizzle_orm.inArray)(through.targetCol, targetKeys))), "disconnect", "delete");
6214
6392
  }
6215
- if (op["connect"] !== void 0 && op["connect"] !== null) await link(asArray(op["connect"]), "connect");
6393
+ if (supplied(op["connect"])) await link(asArray(op["connect"]), "connect");
6216
6394
  };
6217
- const detachAll = async (executor, plan, key, scope) => executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where(withScope(scope, plan.targetTableName, plan.targetTable, (0, drizzle_orm.eq)(plan.foreignCol, key)));
6395
+ 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, (0, drizzle_orm.eq)(plan.foreignCol, key))), op, "update");
6218
6396
  return {
6219
6397
  enabled: (tableName) => !!plans[tableName],
6220
6398
  split: (tableName, values) => {
@@ -6243,19 +6421,27 @@ const createNestedWriteRuntime = (params) => {
6243
6421
  }
6244
6422
  return columns;
6245
6423
  },
6246
- applyParentSide: async (executor, tableName, ops, context) => {
6424
+ applyParentSide: async (executor, tableName, ops, request) => {
6247
6425
  const tablePlans = plans[tableName];
6248
6426
  const patch = {};
6249
6427
  if (!tablePlans) return patch;
6428
+ const context = request?.context;
6250
6429
  const scope = resolveScope(policies, context, filterCtx);
6251
6430
  for (const [relationName, op] of Object.entries(ops)) {
6252
6431
  const plan = tablePlans[relationName];
6253
6432
  if (plan?.fkSide !== "parent") continue;
6254
6433
  assertSingleOperation(plan, op);
6255
6434
  if (op["create"] !== void 0) {
6435
+ const steps = createHookSteps(onWrite, executor, request, tableName, plan);
6436
+ steps.add("create", "insert");
6437
+ await steps.before();
6256
6438
  const values = applyContextValues(remapFromGraphQLSingleInput({ ...op["create"] }, plan.targetTable), contextValues?.(plan.targetTableName), context);
6257
- const created = (await executor.insert(plan.targetTable).values(values).returning({ [plan.foreignColPropName]: plan.foreignCol }))[0];
6439
+ const step = steps.get("create", "insert");
6440
+ const inserted = await executor.insert(plan.targetTable).values(values).returning(step ? step.returning : { [plan.foreignColPropName]: plan.foreignCol });
6441
+ step?.rows.push(...inserted);
6442
+ const created = inserted[0];
6258
6443
  if (!created) throw drizzleError(`Drizzle-GraphQL Error: 'create' on '${relationName}' inserted no row, so there is nothing to attach.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
6444
+ await steps.after();
6259
6445
  patch[plan.localColPropName] = created[plan.foreignColPropName];
6260
6446
  } else if (op["connect"] !== void 0) {
6261
6447
  const rows = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
@@ -6265,41 +6451,46 @@ const createNestedWriteRuntime = (params) => {
6265
6451
  }
6266
6452
  return patch;
6267
6453
  },
6268
- applyChildSide: async (executor, tableName, ops, parentRows, context) => {
6454
+ applyChildSide: async (executor, tableName, ops, parentRows, request) => {
6269
6455
  const tablePlans = plans[tableName];
6270
6456
  if (!tablePlans) return;
6457
+ const context = request?.context;
6271
6458
  const scope = resolveScope(policies, context, filterCtx);
6272
6459
  for (const [relationName, op] of Object.entries(ops)) {
6273
6460
  const plan = tablePlans[relationName];
6274
6461
  if (plan?.fkSide !== "child") continue;
6275
6462
  if (plan.isOne) assertSingleOperation(plan, op);
6463
+ const steps = createHookSteps(onWrite, executor, request, tableName, plan);
6464
+ registerChildSteps(steps, plan, op);
6465
+ await steps.before();
6276
6466
  for (const parentRow of parentRows) {
6277
6467
  const key = parentRow?.[plan.localColPropName];
6278
6468
  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" });
6279
6469
  if (plan.through) {
6280
- await applyThroughOps(executor, plan, key, op, scope);
6470
+ await applyThroughOps(executor, plan, key, op, scope, steps);
6281
6471
  continue;
6282
6472
  }
6283
6473
  if (op["set"] !== void 0) {
6284
- await detachAll(executor, plan, key, scope);
6285
- for (const filter of asArray(op["set"])) await executor.update(plan.targetTable).set({ [plan.foreignColPropName]: key }).where(conditionOf(plan, filter, "set", scope));
6474
+ await detachAll(executor, plan, key, scope, steps, "set");
6475
+ 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");
6286
6476
  }
6287
6477
  if (op["disconnect"] !== void 0) {
6288
6478
  if (plan.isOne) {
6289
- if (op["disconnect"] === true) await detachAll(executor, plan, key, scope);
6290
- } else for (const filter of asArray(op["disconnect"])) await executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(plan.foreignCol, key), conditionOf(plan, filter, "disconnect", scope)));
6479
+ if (op["disconnect"] === true) await detachAll(executor, plan, key, scope, steps, "disconnect");
6480
+ } else for (const filter of asArray(op["disconnect"])) await steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(plan.foreignCol, key), conditionOf(plan, filter, "disconnect", scope))), "disconnect", "update");
6291
6481
  }
6292
- const attaches = op["connect"] !== void 0 || op["create"] !== void 0;
6293
- if (plan.isOne && attaches && plan.canDetach && op["set"] === void 0) await detachAll(executor, plan, key, scope);
6294
- 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));
6482
+ const attachedBy = attachingOp(op);
6483
+ if (plan.isOne && attachedBy && plan.canDetach && op["set"] === void 0) await detachAll(executor, plan, key, scope, steps, attachedBy);
6484
+ 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");
6295
6485
  if (op["create"] !== void 0) {
6296
6486
  const rows = asArray(op["create"]).map((row) => ({
6297
6487
  ...applyContextValues(remapFromGraphQLSingleInput({ ...row }, plan.targetTable), contextValues?.(plan.targetTableName), context),
6298
6488
  [plan.foreignColPropName]: key
6299
6489
  }));
6300
- if (rows.length) await executor.insert(plan.targetTable).values(rows);
6490
+ if (rows.length) await steps.run(executor.insert(plan.targetTable).values(rows), "create", "insert");
6301
6491
  }
6302
6492
  }
6493
+ await steps.after();
6303
6494
  }
6304
6495
  }
6305
6496
  };
@@ -6309,21 +6500,25 @@ const createNestedWriteRuntime = (params) => {
6309
6500
  * inside a single transaction — a savepoint when the caller already runs on one, so a failing
6310
6501
  * entry rolls the whole batch back.
6311
6502
  *
6503
+ * That transaction is also the `tx` the target tables' `onWrite` hooks run on. Every nested
6504
+ * write opens it, so a hook on a nested target has the whole tree to roll back even when the
6505
+ * root field carries no hook of its own and was not forced into a transaction for one.
6506
+ *
6312
6507
  * Entries are written one at a time rather than as a single multi-row statement: a nested
6313
6508
  * operation attaches to the row its own entry produced, so the two have to stay correlated.
6314
6509
  * The batch path is untouched when no entry carries a nested operation.
6315
6510
  */
6316
6511
  const writeWithNestedOps = async (params) => {
6317
- const { executor, runtime, tableName, entries, remapValues, write, context } = params;
6512
+ const { executor, runtime, tableName, entries, remapValues, write, request } = params;
6318
6513
  return executor.transaction(async (tx) => {
6319
6514
  const rows = [];
6320
6515
  for (const entry of entries) {
6321
- const patch = await runtime.applyParentSide(tx, tableName, entry.ops, context);
6516
+ const patch = await runtime.applyParentSide(tx, tableName, entry.ops, request);
6322
6517
  const written = await write(tx, {
6323
6518
  ...remapValues(entry.columns),
6324
6519
  ...patch
6325
6520
  });
6326
- await runtime.applyChildSide(tx, tableName, entry.ops, written, context);
6521
+ await runtime.applyChildSide(tx, tableName, entry.ops, written, request);
6327
6522
  rows.push(...written);
6328
6523
  }
6329
6524
  return rows;
@@ -6335,14 +6530,14 @@ const writeWithNestedOps = async (params) => {
6335
6530
  * attaches to each row that came back.
6336
6531
  */
6337
6532
  const updateWithNestedOps = async (params) => {
6338
- const { executor, runtime, tableName, columns, ops, remapValues, write, context } = params;
6533
+ const { executor, runtime, tableName, columns, ops, remapValues, write, request } = params;
6339
6534
  return executor.transaction(async (tx) => {
6340
- const patch = await runtime.applyParentSide(tx, tableName, ops, context);
6535
+ const patch = await runtime.applyParentSide(tx, tableName, ops, request);
6341
6536
  const rows = await write(tx, {
6342
6537
  ...remapValues(columns),
6343
6538
  ...patch
6344
6539
  });
6345
- await runtime.applyChildSide(tx, tableName, ops, rows, context);
6540
+ await runtime.applyChildSide(tx, tableName, ops, rows, request);
6346
6541
  return rows;
6347
6542
  });
6348
6543
  };
@@ -6543,7 +6738,14 @@ const createSchemaBuilder = (adapter) => {
6543
6738
  uniqueKeysOf: (tableName) => uniqueKeys[tableName]
6544
6739
  };
6545
6740
  adapter.preflight?.(db, options);
6546
- const nestedPlans = features.nestedWrites ? buildNestedWritePlans(tables, namedRelations, (target) => uniqueColumnSets(target), (target) => primaryKeyPropNames(target), extractRelationJoinColumns) : void 0;
6741
+ const nestedPlans = features.nestedWrites ? buildNestedWritePlans(tables, namedRelations, (target) => uniqueColumnSets(target), (target) => primaryKeyPropNames(target), extractRelationJoinColumns, (junction) => schemaEntries.find(([, value]) => value === junction)?.[0]) : void 0;
6742
+ if (nestedPlans) {
6743
+ const omitted = gateNestedWritePlans(nestedPlans, featureOf, (tableName) => excludedTables.has(tableName));
6744
+ if (omitted.length) {
6745
+ const listed = omitted.length > 5 ? `${omitted.slice(0, 5).join(", ")} and ${omitted.length - 5} more` : omitted.join(", ");
6746
+ console.warn(`Drizzle-GraphQL Warning: config.features — nestedWrites leaves out the nested inputs whose table has that write turned off: ${listed}.`);
6747
+ }
6748
+ }
6547
6749
  const nestedTypes = nestedPlans ? createNestedWriteTypes({
6548
6750
  plans: nestedPlans,
6549
6751
  cacheCtx,
@@ -7216,7 +7418,11 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7216
7418
  entries: nestedEntries,
7217
7419
  remapValues: (values) => applyContextValues(remapFromGraphQLSingleInput(values, table), contextColumns, context),
7218
7420
  write: (tx, values) => runInsert(tx, [values]),
7219
- context
7421
+ request: {
7422
+ context,
7423
+ args,
7424
+ info
7425
+ }
7220
7426
  }) : await runInsert(executor, input);
7221
7427
  await after(written);
7222
7428
  const result = output(written);
@@ -7305,7 +7511,11 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7305
7511
  entries: nestedEntries,
7306
7512
  remapValues: (values) => applyContextValues(remapFromGraphQLSingleInput(values, table), contextColumns, context),
7307
7513
  write: (tx, values) => runUpsert(tx, [values]),
7308
- context
7514
+ request: {
7515
+ context,
7516
+ args,
7517
+ info
7518
+ }
7309
7519
  }) : await runUpsert(executor, input);
7310
7520
  await after(written);
7311
7521
  const result = output(written);
@@ -7378,7 +7588,11 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7378
7588
  ops: nestedOps,
7379
7589
  remapValues: (values) => values,
7380
7590
  write: runUpdate,
7381
- context
7591
+ request: {
7592
+ context,
7593
+ args,
7594
+ info
7595
+ }
7382
7596
  }) : await runUpdate(executor, input);
7383
7597
  await after(written);
7384
7598
  const result = output(written);
@@ -7808,18 +8022,18 @@ const createSchemaDataGenerator = (adapter) => {
7808
8022
  * the shared multi-mutation transaction opened by `runMutation` — this opens a savepoint, so
7809
8023
  * the batch stays atomic without breaking the outer transaction.
7810
8024
  */
7811
- const runUpdateManyBatch = ({ executor, entries, table, returning, nested, tableName, context }) => executor.transaction(async (tx) => {
8025
+ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, tableName, request }) => executor.transaction(async (tx) => {
7812
8026
  const results = [];
7813
8027
  for (const entry of entries) {
7814
8028
  const values = entry.ops ? {
7815
8029
  ...entry.set,
7816
- ...await nested.applyParentSide(tx, tableName, entry.ops, context)
8030
+ ...await nested.applyParentSide(tx, tableName, entry.ops, request)
7817
8031
  } : entry.set;
7818
8032
  const writes = Object.keys(values).length > 0;
7819
8033
  let query = writes ? tx.update(table).set(values) : tx.select(returning).from(table);
7820
8034
  if (entry.filters) query = query.where(entry.filters);
7821
8035
  const rows = await (writes ? query.returning(returning) : query);
7822
- if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows, context);
8036
+ if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows, request);
7823
8037
  results.push(rows);
7824
8038
  }
7825
8039
  return results;
@@ -7892,7 +8106,11 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7892
8106
  anyNested,
7893
8107
  nested,
7894
8108
  tableName,
7895
- context
8109
+ request: {
8110
+ context,
8111
+ args,
8112
+ info
8113
+ }
7896
8114
  });
7897
8115
  const written = perEntry.flat();
7898
8116
  await after(written);
@@ -7941,7 +8159,7 @@ const sqlitePrimaryKeyPropNames = (table) => getPrimaryKeyPropNamesFromConfig(ta
7941
8159
  * would run after the COMMIT. The driver kind is probed from the first statement's result
7942
8160
  * rather than declared, so the same resolver serves both driver families.
7943
8161
  */
7944
- const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNested, nested, tableName, context }) => {
8162
+ const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNested, nested, tableName, request }) => {
7945
8163
  const runEntry = (tx, entry) => {
7946
8164
  let query = tx.update(table).set(entry.set);
7947
8165
  if (entry.filters) query = query.where(entry.filters);
@@ -7950,13 +8168,13 @@ const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNest
7950
8168
  const runNestedEntry = async (tx, entry) => {
7951
8169
  const values = entry.ops ? {
7952
8170
  ...entry.set,
7953
- ...await nested.applyParentSide(tx, tableName, entry.ops, context)
8171
+ ...await nested.applyParentSide(tx, tableName, entry.ops, request)
7954
8172
  } : entry.set;
7955
8173
  const writes = Object.keys(values).length > 0;
7956
8174
  let query = writes ? tx.update(table).set(values) : tx.select(returning).from(table);
7957
8175
  if (entry.filters) query = query.where(entry.filters);
7958
8176
  const rows = await (writes ? query.returning(returning) : query);
7959
- if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows, context);
8177
+ if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows, request);
7960
8178
  return rows;
7961
8179
  };
7962
8180
  return executor.transaction((tx) => {