@vantreeseba/drizzle-graphql 15.1.4 → 15.1.5

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.d.cts CHANGED
@@ -1548,8 +1548,15 @@ type SchemaFeatures = {
1548
1548
  * relation left with no input loses its field.
1549
1549
  * - **`onWrite`** — the hooks of the table a nested statement writes fire on the same `tx`,
1550
1550
  * with `payload.nested` set. See {@link BuildSchemaConfig.onWrite}.
1551
- * - **`scope`** and **`contextValues`** — `connect` / `disconnect` / `set` reach only
1552
- * in-scope rows, and a nested `create` is stamped like any insert.
1551
+ * - **`scope`**, **`softDelete`** and **`contextValues`** — `connect` / `disconnect` / `set`
1552
+ * reach only in-scope, unmarked rows (a many-to-many `set` keeps the links it cannot
1553
+ * reach), and a nested `create` is stamped like any insert. A relation whose foreign key
1554
+ * is a `contextValues` column is not offered at all.
1555
+ *
1556
+ * A relation's own `where` limits the rows its writes reach the same way, and takes its
1557
+ * `create` away. A relation joining on more than one column per side is not offered. A
1558
+ * to-one `connect` must match exactly one row. When the parent write produces no row, a
1559
+ * parent-side `create` made for it is rolled back.
1553
1560
  *
1554
1561
  * PostgreSQL and SQLite only. MySQL cannot return the rows it just wrote, which is how a
1555
1562
  * nested write learns the parent's key, so `buildSchema` rejects the flag there. On SQLite
@@ -2136,6 +2143,8 @@ type BuildSchemaConfig = {
2136
2143
  * - Every insert stamps it, including the rows created by a nested `create`.
2137
2144
  * - Updates never write it: an update cannot hand a row to another owner. That includes
2138
2145
  * the conflict branch of an upsert, whose `onConflict.update` does not offer it.
2146
+ * - With `nestedWrites`, a relation whose foreign key is such a column gets no nested-write
2147
+ * field, since every nested operation on it would write the column.
2139
2148
  * - Keyed by the Drizzle schema key, then by the column's property name (not its database
2140
2149
  * name), the same keying {@link BuildSchemaConfig.exclude} uses.
2141
2150
  */
package/dist/index.d.ts CHANGED
@@ -1548,8 +1548,15 @@ type SchemaFeatures = {
1548
1548
  * relation left with no input loses its field.
1549
1549
  * - **`onWrite`** — the hooks of the table a nested statement writes fire on the same `tx`,
1550
1550
  * with `payload.nested` set. See {@link BuildSchemaConfig.onWrite}.
1551
- * - **`scope`** and **`contextValues`** — `connect` / `disconnect` / `set` reach only
1552
- * in-scope rows, and a nested `create` is stamped like any insert.
1551
+ * - **`scope`**, **`softDelete`** and **`contextValues`** — `connect` / `disconnect` / `set`
1552
+ * reach only in-scope, unmarked rows (a many-to-many `set` keeps the links it cannot
1553
+ * reach), and a nested `create` is stamped like any insert. A relation whose foreign key
1554
+ * is a `contextValues` column is not offered at all.
1555
+ *
1556
+ * A relation's own `where` limits the rows its writes reach the same way, and takes its
1557
+ * `create` away. A relation joining on more than one column per side is not offered. A
1558
+ * to-one `connect` must match exactly one row. When the parent write produces no row, a
1559
+ * parent-side `create` made for it is rolled back.
1553
1560
  *
1554
1561
  * PostgreSQL and SQLite only. MySQL cannot return the rows it just wrote, which is how a
1555
1562
  * nested write learns the parent's key, so `buildSchema` rejects the flag there. On SQLite
@@ -2136,6 +2143,8 @@ type BuildSchemaConfig = {
2136
2143
  * - Every insert stamps it, including the rows created by a nested `create`.
2137
2144
  * - Updates never write it: an update cannot hand a row to another owner. That includes
2138
2145
  * the conflict branch of an upsert, whose `onConflict.update` does not offer it.
2146
+ * - With `nestedWrites`, a relation whose foreign key is such a column gets no nested-write
2147
+ * field, since every nested operation on it would write the column.
2139
2148
  * - Keyed by the Drizzle schema key, then by the column's property name (not its database
2140
2149
  * name), the same keying {@link BuildSchemaConfig.exclude} uses.
2141
2150
  */
package/dist/index.js CHANGED
@@ -6516,6 +6516,20 @@ const ALL_OPS = [
6516
6516
  * accepts NULL.
6517
6517
  */
6518
6518
  const isOptionalOnInsert = (column) => !column.notNull || !!column.hasDefault || !!column.defaultFn;
6519
+ /**
6520
+ * The plan fields that carry a relation's own `where` (see {@link relationOwnCondition}).
6521
+ *
6522
+ * A `where` on the target narrows every row selection the write makes, and takes `create`
6523
+ * away: the row it inserts is attached whether or not it satisfies the `where`, so the
6524
+ * relation could come back without it. A `where` inherited from the reverse relation filters
6525
+ * the parent instead, and becomes a per-parent check.
6526
+ */
6527
+ const ownWherePlan = (relEntry, parentTable, targetTable, localCol) => {
6528
+ const condition = () => relationOwnCondition(relEntry, parentTable, targetTable, [localCol], [localCol]);
6529
+ if (!condition()) return {};
6530
+ if (!isRelationFilterReversed(relEntry.relation ?? relEntry)) return { ownCondition: () => condition() };
6531
+ return { parentShows: async (executor, key) => (await executor.select({ key: localCol }).from(parentTable).where(and(eq(localCol, key), condition())).limit(1)).length > 0 };
6532
+ };
6519
6533
  /** The property name a column is stored under on its table, matched by identity. */
6520
6534
  const propNameOf = (table, column) => {
6521
6535
  if (!column) return;
@@ -6536,7 +6550,7 @@ const propNameOf = (table, column) => {
6536
6550
  * `onUnresolved` is called when the relation's columns are of a supported shape but cannot be
6537
6551
  * matched to the three tables — a drizzle-orm mismatch rather than an unsupported relation.
6538
6552
  */
6539
- const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable, tableNameOf, onUnresolved) => {
6553
+ const buildThroughPlan = (relationName, relEntry, relation, table, targetTableName, targetTable, tableNameOf, onUnresolved) => {
6540
6554
  if (is(relation, One)) return;
6541
6555
  const junction = relation.throughTable;
6542
6556
  const sourceEntries = relation.through?.source;
@@ -6577,6 +6591,7 @@ const buildThroughPlan = (relationName, relation, table, targetTableName, target
6577
6591
  targetCol
6578
6592
  },
6579
6593
  targetKeyColumns: { [foreignColPropName]: foreignCol },
6594
+ ...ownWherePlan(relEntry, table, targetTable, localCol),
6580
6595
  allowed: /* @__PURE__ */ new Set([
6581
6596
  "connect",
6582
6597
  "disconnect",
@@ -6591,8 +6606,9 @@ const buildThroughPlan = (relationName, relation, table, targetTableName, target
6591
6606
  * rather than a foreign-key one — see {@link buildThroughPlan}.
6592
6607
  *
6593
6608
  * Skipped, and so absent from the generated inputs rather than failing at runtime:
6594
- * relations whose join columns cannot be read off the drizzle relation; and relations where
6595
- * neither join column is unique, which leaves no way to tell which side stores the key.
6609
+ * relations whose join columns cannot be read off the drizzle relation; relations joining on
6610
+ * more than one column per side; and relations where neither join column is unique, which
6611
+ * leaves no way to tell which side stores the key.
6596
6612
  */
6597
6613
  const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePrimaryKeys, extractJoinColumns, tableNameOf = () => void 0) => {
6598
6614
  const uniqueCache = /* @__PURE__ */ new Map();
@@ -6624,10 +6640,11 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
6624
6640
  if (!targetTable) continue;
6625
6641
  const omitted = `the '${relationName}' field of its nested-write create and update inputs`;
6626
6642
  if (relation.through) {
6627
- const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable, tableNameOf, () => warnUnresolvedJoin(relEntry, tableName, relationName, omitted));
6643
+ const throughPlan = buildThroughPlan(relationName, relEntry, relation, table, targetTableName, targetTable, tableNameOf, () => warnUnresolvedJoin(relEntry, tableName, relationName, omitted));
6628
6644
  if (throughPlan) tablePlans[relationName] = throughPlan;
6629
6645
  continue;
6630
6646
  }
6647
+ if ((relationColumns(relation, "source")?.length ?? 0) > 1 || (relationColumns(relation, "target")?.length ?? 0) > 1) continue;
6631
6648
  const joinColumns = extractJoinColumns(relEntry, table, targetTable);
6632
6649
  if (!joinColumns) {
6633
6650
  warnUnresolvedJoin(relEntry, tableName, relationName, omitted);
@@ -6650,8 +6667,15 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
6650
6667
  else continue;
6651
6668
  const fkColumn = fkSide === "child" ? foreignCol : localCol;
6652
6669
  const targetColumns = getColumns(targetTable);
6653
- const targetKeyColumns = Object.fromEntries(resolvePrimaryKeys(targetTable).filter((name) => targetColumns[name]).map((name) => [name, targetColumns[name]]));
6670
+ const targetPrimaryKey = resolvePrimaryKeys(targetTable).filter((name) => targetColumns[name]);
6671
+ const targetKeyColumns = Object.fromEntries(targetPrimaryKey.map((name) => [name, targetColumns[name]]));
6654
6672
  targetKeyColumns[foreignColPropName] = foreignCol;
6673
+ const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol);
6674
+ const ops = !fkColumn.notNull ? isOne ? [
6675
+ "create",
6676
+ "connect",
6677
+ "disconnect"
6678
+ ] : ALL_OPS : ["create", "connect"];
6655
6679
  tablePlans[relationName] = {
6656
6680
  relationName,
6657
6681
  targetTableName,
@@ -6664,11 +6688,9 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
6664
6688
  fkSide,
6665
6689
  canDetach: !fkColumn.notNull,
6666
6690
  targetKeyColumns,
6667
- allowed: new Set(!fkColumn.notNull ? isOne ? [
6668
- "create",
6669
- "connect",
6670
- "disconnect"
6671
- ] : ALL_OPS : ["create", "connect"])
6691
+ targetPrimaryKey,
6692
+ ...ownWhere,
6693
+ allowed: new Set(ownWhere.ownCondition ? ops.filter((op) => op !== "create") : ops)
6672
6694
  };
6673
6695
  }
6674
6696
  if (Object.keys(tablePlans).length) plans[tableName] = tablePlans;
@@ -6743,6 +6765,35 @@ const gateNestedWritePlans = (plans, featureOf, isExcluded) => {
6743
6765
  }
6744
6766
  return omitted;
6745
6767
  };
6768
+ /**
6769
+ * Takes out every relation whose foreign key is a `contextValues` column of the table storing
6770
+ * it — the parent's own column for a parent-side relation, the target's for a child-side one,
6771
+ * either junction column for a many-to-many.
6772
+ *
6773
+ * Every operation on such a relation writes that column: `connect`, `disconnect` and `set`
6774
+ * by definition, and a child-side `create` too, since it fills the key in from the parent row
6775
+ * rather than from context. That column is the server's to set — an insert stamps it and an
6776
+ * update never touches it — so writing it from a nested input would let a client pick, or
6777
+ * change, the owner the server assigns. Whole relations are dropped, not single inputs: no
6778
+ * operation on them is left that does not write the key.
6779
+ *
6780
+ * Returns one line per relation taken out, for the build note.
6781
+ */
6782
+ const dropContextValueRelations = (plans, contextValuesOf) => {
6783
+ const omitted = [];
6784
+ if (!contextValuesOf) return omitted;
6785
+ for (const [tableName, tablePlans] of Object.entries(plans)) {
6786
+ for (const [relationName, plan] of Object.entries(tablePlans)) {
6787
+ const stamped = (plan.through ? [[plan.through.tableName, plan.through.sourceColPropName], [plan.through.tableName, plan.through.targetColPropName]] : plan.fkSide === "parent" ? [[tableName, plan.localColPropName]] : [[plan.targetTableName, plan.foreignColPropName]]).find(([owner, column]) => owner !== void 0 && contextValuesOf(owner)?.[column]);
6788
+ if (stamped) {
6789
+ delete tablePlans[relationName];
6790
+ omitted.push(`${tableName}.${relationName} (${stamped[0]}.${stamped[1]} is a contextValues column)`);
6791
+ }
6792
+ }
6793
+ if (!Object.keys(tablePlans).length) delete plans[tableName];
6794
+ }
6795
+ return omitted;
6796
+ };
6746
6797
  const NO_COLUMNS = /* @__PURE__ */ new Set();
6747
6798
  /**
6748
6799
  * Builds the nested-write input types for a schema.
@@ -6773,7 +6824,8 @@ const createNestedWriteTypes = (params) => {
6773
6824
  if (cached) return cached;
6774
6825
  const omitted = plan.fkSide === "child" ? plan.foreignColPropName : void 0;
6775
6826
  const contextColumns = cacheCtx.contextValuesOf?.(plan.targetTableName);
6776
- const fields = Object.fromEntries(Object.entries(visibleColumns(plan.targetTable)).filter(([columnName]) => columnName !== omitted && !contextColumns?.[columnName]).map(([columnName, column]) => [columnName, drizzleColumnToGraphQLType(column, columnName, plan.targetTableName, false, true, true)]));
6827
+ const markerColumn = cacheCtx.softDeleteOf?.(plan.targetTableName)?.columnName;
6828
+ const fields = Object.fromEntries(Object.entries(visibleColumns(plan.targetTable)).filter(([columnName]) => columnName !== omitted && !contextColumns?.[columnName] && columnName !== markerColumn).map(([columnName, column]) => [columnName, drizzleColumnToGraphQLType(column, columnName, plan.targetTableName, false, true, true)]));
6777
6829
  const type = new GraphQLInputObjectType({
6778
6830
  name: cacheCtx.typeName({
6779
6831
  kind: "nestedWriteInput",
@@ -6897,6 +6949,8 @@ const createNestedWriteTypes = (params) => {
6897
6949
  * join columns the whole batch's RETURNING clause needs, without inspecting each row's write.
6898
6950
  */
6899
6951
  const mergedOps = (entries) => Object.assign({}, ...entries.map((entry) => entry.ops));
6952
+ /** A to-one `connect` whose filter did not match exactly one row. */
6953
+ const connectMatchError = (relationName, matched) => drizzleError(`Drizzle-GraphQL Error: 'connect' on '${relationName}' matched ${matched} rows — it attaches a single row, so its filter must match exactly one.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
6900
6954
  const asArray = (value) => Array.isArray(value) ? value : value === void 0 ? [] : [value];
6901
6955
  const supplied = (value) => value !== void 0 && value !== null;
6902
6956
  /**
@@ -6988,6 +7042,8 @@ const registerChildSteps = (steps, plan, op) => {
6988
7042
  };
6989
7043
  /** The input that attaches a row to a to-one, which detaches whatever was attached before. */
6990
7044
  const attachingOp = (op) => op["connect"] !== void 0 ? "connect" : op["create"] !== void 0 ? "create" : void 0;
7045
+ /** The child-side relation in `ops` whose target is the parent's own table, if there is one. */
7046
+ const selfTargetingPlan = (tablePlans, tableName, ops) => Object.keys(ops).map((relationName) => tablePlans?.[relationName]).find((plan) => plan?.fkSide === "child" && !plan.through && plan.targetTableName === tableName);
6991
7047
  const createNestedWriteRuntime = (params) => {
6992
7048
  const { plans, filterCtx, policies, contextValues } = params;
6993
7049
  const onWrite = policies?.onWrite;
@@ -6998,7 +7054,7 @@ const createNestedWriteRuntime = (params) => {
6998
7054
  const conditionOf = (plan, filter, operation, scope) => {
6999
7055
  const condition = extractFilters(plan.targetTable, plan.targetTableName, filter, relationFilterCtx(filterCtx, plan.targetTableName, scope));
7000
7056
  if (!condition) throw drizzleError(`Drizzle-GraphQL Error: '${operation}' on '${plan.relationName}' needs a filter that selects rows — it was given one that matches everything.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
7001
- return withScope(scope, plan.targetTableName, plan.targetTable, condition);
7057
+ return withScope(scope, plan.targetTableName, plan.targetTable, and(condition, plan.ownCondition?.()));
7002
7058
  };
7003
7059
  const assertSingleOperation = (plan, op) => {
7004
7060
  const given = [
@@ -7021,22 +7077,24 @@ const createNestedWriteRuntime = (params) => {
7021
7077
  * error on some of them — the read and the write share the surrounding transaction, so no
7022
7078
  * other writer can slip a duplicate in between them.
7023
7079
  */
7024
- const applyThroughOps = async (executor, plan, key, op, scope, steps) => {
7080
+ const applyThroughOps = async (executor, plan, key, op, scope, steps, context) => {
7025
7081
  const through = plan.through;
7026
7082
  const link = async (operands, operation) => {
7027
7083
  const existing = new Set((await executor.select({ [through.targetColPropName]: through.targetCol }).from(through.table).where(eq(through.sourceCol, key))).map((row) => row[through.targetColPropName]));
7028
7084
  const rows = [];
7029
7085
  for (const filter of operands) for (const targetKey of await selectedTargetKeys(executor, plan, filter, operation, scope)) if (!existing.has(targetKey)) {
7030
7086
  existing.add(targetKey);
7031
- rows.push({
7087
+ rows.push(applyContextValues({
7032
7088
  [through.sourceColPropName]: key,
7033
7089
  [through.targetColPropName]: targetKey
7034
- });
7090
+ }, through.tableName ? contextValues?.(through.tableName) : void 0, context));
7035
7091
  }
7036
7092
  if (rows.length) await steps.run(executor.insert(through.table).values(rows), operation, "insert");
7037
7093
  };
7038
7094
  if (supplied(op["set"])) {
7039
- await steps.run(executor.delete(through.table).where(eq(through.sourceCol, key)), "set", "delete");
7095
+ const reachable = withScope(scope, plan.targetTableName, plan.targetTable, plan.ownCondition?.());
7096
+ const unlink = reachable ? and(eq(through.sourceCol, key), inArray(through.targetCol, executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(reachable))) : eq(through.sourceCol, key);
7097
+ await steps.run(executor.delete(through.table).where(unlink), "set", "delete");
7040
7098
  await link(asArray(op["set"]), "set");
7041
7099
  }
7042
7100
  if (supplied(op["disconnect"])) for (const filter of asArray(op["disconnect"])) {
@@ -7045,7 +7103,7 @@ const createNestedWriteRuntime = (params) => {
7045
7103
  }
7046
7104
  if (supplied(op["connect"])) await link(asArray(op["connect"]), "connect");
7047
7105
  };
7048
- 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");
7106
+ 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, and(eq(plan.foreignCol, key), plan.ownCondition?.()))), op, "update");
7049
7107
  return {
7050
7108
  enabled: (tableName) => !!plans[tableName],
7051
7109
  split: (tableName, values) => {
@@ -7072,8 +7130,24 @@ const createNestedWriteRuntime = (params) => {
7072
7130
  const plan = tablePlans[relationName];
7073
7131
  if (plan?.fkSide === "child" && !(plan.localColPropName in columns) && allColumns[plan.localColPropName]) columns[plan.localColPropName] = allColumns[plan.localColPropName];
7074
7132
  }
7133
+ for (const name of selfTargetingPlan(tablePlans, tableName, ops)?.targetPrimaryKey ?? []) if (!(name in columns) && allColumns[name]) columns[name] = allColumns[name];
7075
7134
  return columns;
7076
7135
  },
7136
+ createsParentSide: (tableName, ops) => Object.entries(ops).some(([relationName, op]) => plans[tableName]?.[relationName]?.fkSide === "parent" && supplied(op?.["create"])),
7137
+ refreshed: async (executor, tableName, ops, rows) => {
7138
+ const plan = selfTargetingPlan(plans[tableName], tableName, ops);
7139
+ const pk = plan?.targetPrimaryKey ?? [];
7140
+ if (!plan || !pk.length || !rows.length || !pk.every((name) => name in rows[0])) return rows;
7141
+ const columns = getColumns(plan.targetTable);
7142
+ const selection = Object.fromEntries(Object.keys(rows[0]).filter((name) => columns[name]).map((name) => [name, columns[name]]));
7143
+ const keyOf = (row) => pk.map((name) => String(row[name])).join("\0");
7144
+ const match = pk.length === 1 ? inArray(columns[pk[0]], rows.map((row) => row[pk[0]])) : or(...rows.map((row) => and(...pk.map((name) => eq(columns[name], row[name])))));
7145
+ const fresh = new Map((await executor.select(selection).from(plan.targetTable).where(match)).map((row) => [keyOf(row), row]));
7146
+ return rows.map((row) => ({
7147
+ ...row,
7148
+ ...fresh.get(keyOf(row))
7149
+ }));
7150
+ },
7077
7151
  applyParentSide: async (executor, tableName, ops, request) => {
7078
7152
  const tablePlans = plans[tableName];
7079
7153
  const patch = {};
@@ -7098,10 +7172,11 @@ const createNestedWriteRuntime = (params) => {
7098
7172
  patch[plan.localColPropName] = created[plan.foreignColPropName];
7099
7173
  } else if (op["connect"] !== void 0) {
7100
7174
  const rows = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
7101
- if (rows.length !== 1) throw drizzleError(`Drizzle-GraphQL Error: 'connect' on '${relationName}' matched ${rows.length} rows — it attaches a single row, so its filter must match exactly one.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
7175
+ if (rows.length !== 1) throw connectMatchError(relationName, rows.length);
7102
7176
  patch[plan.localColPropName] = rows[0][plan.foreignColPropName];
7103
7177
  } else if (op["disconnect"] === true) patch[plan.localColPropName] = null;
7104
7178
  }
7179
+ for (const columnName of Object.keys(contextValues?.(tableName) ?? {})) delete patch[columnName];
7105
7180
  return patch;
7106
7181
  },
7107
7182
  applyChildSide: async (executor, tableName, ops, parentRows, request) => {
@@ -7119,8 +7194,9 @@ const createNestedWriteRuntime = (params) => {
7119
7194
  for (const parentRow of parentRows) {
7120
7195
  const key = parentRow?.[plan.localColPropName];
7121
7196
  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" });
7197
+ if (plan.parentShows && !await plan.parentShows(executor, key)) throw drizzleError(`Drizzle-GraphQL Error: cannot write through '${relationName}': its \`where\` hides the relation on this ${tableName} row.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
7122
7198
  if (plan.through) {
7123
- await applyThroughOps(executor, plan, key, op, scope, steps);
7199
+ await applyThroughOps(executor, plan, key, op, scope, steps, context);
7124
7200
  continue;
7125
7201
  }
7126
7202
  if (op["set"] !== void 0) {
@@ -7132,14 +7208,18 @@ const createNestedWriteRuntime = (params) => {
7132
7208
  if (op["disconnect"] === true) await detachAll(executor, plan, key, scope, steps, "disconnect");
7133
7209
  } 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");
7134
7210
  }
7211
+ if (plan.isOne && op["connect"] !== void 0) {
7212
+ const matched = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
7213
+ if (matched.length !== 1) throw connectMatchError(relationName, matched.length);
7214
+ }
7135
7215
  const attachedBy = attachingOp(op);
7136
7216
  if (plan.isOne && attachedBy && plan.canDetach && op["set"] === void 0) await detachAll(executor, plan, key, scope, steps, attachedBy);
7137
7217
  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");
7138
7218
  if (op["create"] !== void 0) {
7139
- const rows = asArray(op["create"]).map((row) => ({
7140
- ...applyContextValues(remapFromGraphQLSingleInput({ ...row }, plan.targetTable), contextValues?.(plan.targetTableName), context),
7219
+ const rows = asArray(op["create"]).map((row) => applyContextValues({
7220
+ ...remapFromGraphQLSingleInput({ ...row }, plan.targetTable),
7141
7221
  [plan.foreignColPropName]: key
7142
- }));
7222
+ }, contextValues?.(plan.targetTableName), context));
7143
7223
  if (rows.length) await steps.run(executor.insert(plan.targetTable).values(rows), "create", "insert");
7144
7224
  }
7145
7225
  }
@@ -7148,6 +7228,39 @@ const createNestedWriteRuntime = (params) => {
7148
7228
  }
7149
7229
  };
7150
7230
  };
7231
+ /** Thrown inside an entry's savepoint to undo a parent-side `create` whose parent was never written. */
7232
+ var NoParentRow = class extends Error {};
7233
+ /**
7234
+ * One parent write with its nested operations around it: the parent-side operations, whose
7235
+ * values `write` receives as a patch, then the write, then the child-side operations — and
7236
+ * the rows it returns read back when a child-side statement may have changed them.
7237
+ *
7238
+ * A parent write can come back empty: an update whose `where` matched nothing, an insert or
7239
+ * upsert that a conflict turned into a no-op. Anything a parent-side `create` inserted would
7240
+ * then be left with nothing pointing at it, so such an entry runs in a savepoint of its own
7241
+ * that is rolled back when the write produced no row, and the entry returns no rows — as it
7242
+ * would have without the nested operation. The rest of the batch is unaffected.
7243
+ *
7244
+ * `executor` must already be a transaction.
7245
+ */
7246
+ const writeNestedEntry = async (params) => {
7247
+ const { executor, runtime, tableName, ops, write, request } = params;
7248
+ const guarded = runtime.createsParentSide(tableName, ops);
7249
+ const run = async (tx) => {
7250
+ const patch = await runtime.applyParentSide(tx, tableName, ops, request);
7251
+ const rows = await write(tx, patch);
7252
+ if (guarded && !rows.length) throw new NoParentRow();
7253
+ await runtime.applyChildSide(tx, tableName, ops, rows, request);
7254
+ return runtime.refreshed(tx, tableName, ops, rows);
7255
+ };
7256
+ if (!guarded) return run(executor);
7257
+ try {
7258
+ return await executor.transaction(run);
7259
+ } catch (error) {
7260
+ if (error instanceof NoParentRow) return [];
7261
+ throw error;
7262
+ }
7263
+ };
7151
7264
  /**
7152
7265
  * Runs one parent write per input entry, with that entry's nested operations around it, all
7153
7266
  * inside a single transaction — a savepoint when the caller already runs on one, so a failing
@@ -7166,12 +7279,17 @@ const writeWithNestedOps = async (params) => {
7166
7279
  return executor.transaction(async (tx) => {
7167
7280
  const rows = [];
7168
7281
  for (const entry of entries) {
7169
- const patch = await runtime.applyParentSide(tx, tableName, entry.ops, request);
7170
- const written = await write(tx, {
7171
- ...remapValues(entry.columns),
7172
- ...patch
7282
+ const written = await writeNestedEntry({
7283
+ executor: tx,
7284
+ runtime,
7285
+ tableName,
7286
+ ops: entry.ops,
7287
+ write: (target, patch) => write(target, {
7288
+ ...remapValues(entry.columns),
7289
+ ...patch
7290
+ }),
7291
+ request
7173
7292
  });
7174
- await runtime.applyChildSide(tx, tableName, entry.ops, written, request);
7175
7293
  rows.push(...written);
7176
7294
  }
7177
7295
  return rows;
@@ -7184,15 +7302,17 @@ const writeWithNestedOps = async (params) => {
7184
7302
  */
7185
7303
  const updateWithNestedOps = async (params) => {
7186
7304
  const { executor, runtime, tableName, columns, ops, remapValues, write, request } = params;
7187
- return executor.transaction(async (tx) => {
7188
- const patch = await runtime.applyParentSide(tx, tableName, ops, request);
7189
- const rows = await write(tx, {
7305
+ return executor.transaction((tx) => writeNestedEntry({
7306
+ executor: tx,
7307
+ runtime,
7308
+ tableName,
7309
+ ops,
7310
+ write: (target, patch) => write(target, {
7190
7311
  ...remapValues(columns),
7191
7312
  ...patch
7192
- });
7193
- await runtime.applyChildSide(tx, tableName, ops, rows, request);
7194
- return rows;
7195
- });
7313
+ }),
7314
+ request
7315
+ }));
7196
7316
  };
7197
7317
  //#endregion
7198
7318
  //#region src/util/builders/errors.ts
@@ -7399,6 +7519,10 @@ const createSchemaBuilder = (adapter) => {
7399
7519
  console.warn(`Drizzle-GraphQL Warning: config.features — nestedWrites leaves out the nested inputs whose table has that write turned off: ${listed}.`);
7400
7520
  }
7401
7521
  }
7522
+ if (nestedPlans) {
7523
+ const omitted = dropContextValueRelations(nestedPlans, contextValuesOf);
7524
+ if (omitted.length) console.warn(`Drizzle-GraphQL Warning: config.contextValues — nestedWrites leaves out the relations whose foreign key the server stamps from context: ${omitted.join(", ")}.`);
7525
+ }
7402
7526
  const nestedTypes = nestedPlans ? createNestedWriteTypes({
7403
7527
  plans: nestedPlans,
7404
7528
  cacheCtx,
@@ -8712,16 +8836,24 @@ const createSchemaDataGenerator = (adapter) => {
8712
8836
  const runUpdateManyBatch = ({ executor, entries, table, returning, nested, tableName, request }) => executor.transaction(async (tx) => {
8713
8837
  const results = [];
8714
8838
  for (const entry of entries) {
8715
- const values = entry.ops ? {
8716
- ...entry.set,
8717
- ...await nested.applyParentSide(tx, tableName, entry.ops, request)
8718
- } : entry.set;
8719
- const writes = Object.keys(values).length > 0;
8720
- let query = writes ? tx.update(table).set(values) : tx.select(returning).from(table);
8721
- if (entry.filters) query = query.where(entry.filters);
8722
- const rows = await (writes ? query.returning(returning) : query);
8723
- if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows, request);
8724
- results.push(rows);
8839
+ const write = async (target, patch) => {
8840
+ const values = {
8841
+ ...entry.set,
8842
+ ...patch
8843
+ };
8844
+ const writes = Object.keys(values).length > 0;
8845
+ let query = writes ? target.update(table).set(values) : target.select(returning).from(table);
8846
+ if (entry.filters) query = query.where(entry.filters);
8847
+ return await (writes ? query.returning(returning) : query);
8848
+ };
8849
+ results.push(entry.ops ? await writeNestedEntry({
8850
+ executor: tx,
8851
+ runtime: nested,
8852
+ tableName,
8853
+ ops: entry.ops,
8854
+ write,
8855
+ request
8856
+ }) : await write(tx, {}));
8725
8857
  }
8726
8858
  return results;
8727
8859
  });
@@ -8853,16 +8985,24 @@ const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNest
8853
8985
  return query.returning(returning).all();
8854
8986
  };
8855
8987
  const runNestedEntry = async (tx, entry) => {
8856
- const values = entry.ops ? {
8857
- ...entry.set,
8858
- ...await nested.applyParentSide(tx, tableName, entry.ops, request)
8859
- } : entry.set;
8860
- const writes = Object.keys(values).length > 0;
8861
- let query = writes ? tx.update(table).set(values) : tx.select(returning).from(table);
8862
- if (entry.filters) query = query.where(entry.filters);
8863
- const rows = await (writes ? query.returning(returning) : query);
8864
- if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows, request);
8865
- return rows;
8988
+ const write = async (target, patch) => {
8989
+ const values = {
8990
+ ...entry.set,
8991
+ ...patch
8992
+ };
8993
+ const writes = Object.keys(values).length > 0;
8994
+ let query = writes ? target.update(table).set(values) : target.select(returning).from(table);
8995
+ if (entry.filters) query = query.where(entry.filters);
8996
+ return await (writes ? query.returning(returning) : query);
8997
+ };
8998
+ return entry.ops ? writeNestedEntry({
8999
+ executor: tx,
9000
+ runtime: nested,
9001
+ tableName,
9002
+ ops: entry.ops,
9003
+ write,
9004
+ request
9005
+ }) : write(tx, {});
8866
9006
  };
8867
9007
  return executor.transaction((tx) => {
8868
9008
  if (anyNested) return (async () => {