@vantreeseba/drizzle-graphql 13.0.0 → 13.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -711,6 +711,51 @@ lists is the build-time error above.
711
711
  > `${tableName}${columnName[0]!.toUpperCase()}${columnName.slice(1)}Enum`
712
712
  > ```
713
713
 
714
+ ### Enum value names
715
+
716
+ A value's GraphQL name is the stored value itself, or `Option<index>` when the stored value
717
+ is not a usable GraphQL name. `enumValueNameMapper` renames the values — most often to follow
718
+ GraphQL's SCREAMING_CASE convention while the database keeps its lowercase values:
719
+
720
+ ```Typescript
721
+ export const Runs = pgTable('runs', {
722
+ status: text('status', { enum: ['running', 'ok', 'error', 'stopped'] }),
723
+ })
724
+
725
+ const { schema } = buildSchema(db, {
726
+ // The built-in preset: `inProgress` / `in-progress` → `IN_PROGRESS`
727
+ enumValueNameMapper: 'constant',
728
+ })
729
+ ```
730
+
731
+ ```graphql
732
+ enum RunsStatusEnum {
733
+ RUNNING # stored as 'running'
734
+ OK
735
+ ERROR
736
+ STOPPED
737
+ }
738
+ ```
739
+
740
+ - Only the GraphQL name changes. Each value's internal `value` is still the stored string,
741
+ so filters (`eq`, `inArray`, …), create/update inputs, outputs and your own resolvers keep
742
+ sending and receiving exactly what the database stores. The generated TypeScript types
743
+ keep the stored values too
744
+ - The function form receives the stored `value`, its `index`, the enum's GraphQL `typeName`
745
+ (after `enumNameMapper`) and everything `enumNameMapper` receives. Returning `undefined`
746
+ keeps the default name for that value:
747
+
748
+ ```Typescript
749
+ enumValueNameMapper: ({ value, typeName }) =>
750
+ typeName === 'RunsStatusEnum' ? value.toUpperCase() : undefined,
751
+ ```
752
+
753
+ - The build fails, rather than producing an invalid or lossy schema, when a returned name is
754
+ not a valid GraphQL enum value name, when two values of one enum map to the same name, or
755
+ when columns that share one enum type (the columns of one `pgEnum`) would name its values
756
+ differently — give those columns separate types with `enumNameMapper` if they really
757
+ should differ
758
+
714
759
  ## Documenting the schema
715
760
 
716
761
  Four hooks put descriptions and deprecations on the generated types. Each is called per
@@ -2019,8 +2064,12 @@ outside the resolver, so its after-position is after the commit.
2019
2064
  ```Typescript
2020
2065
  const { schema } = buildSchema(db, {
2021
2066
  onWrite: {
2022
- posts: async ({ table, operation, rows, tx }) => {
2023
- await tx.insert(auditLog).values(rows.map((row) => ({ table, operation, rowId: row.id })))
2067
+ posts: {
2068
+ after: async ({ table, operation, rows, tx }) => {
2069
+ await tx.insert(auditLog).values(rows.map((row) => ({ table, operation, rowId: row.id })))
2070
+ },
2071
+ // Every row carries `id`, even when the client's mutation did not select it.
2072
+ columns: ['id'],
2024
2073
  },
2025
2074
  },
2026
2075
  })
@@ -2046,6 +2095,39 @@ Registering a bare function (or a positions object) at the top level applies it
2046
2095
  anything else is read as a table map keyed by the **Drizzle schema key**, and a name that
2047
2096
  matches nothing throws at build time.
2048
2097
 
2098
+ ### Which columns `rows` holds
2099
+
2100
+ The statement's `RETURNING` list is built from the client's selection, so by default a row
2101
+ holds only the columns the mutation's document asked for (plus any key a selected relation or a
2102
+ nested write needed). A hook that reads a column the client may not select — `row.id` above, or
2103
+ the `personId` an `addresses` hook needs to bump its person's revision — names it in `columns`:
2104
+
2105
+ ```Typescript
2106
+ onWrite: {
2107
+ addresses: {
2108
+ after: async ({ rows, tx }) => { /* every row has `id` and `personId` */ },
2109
+ columns: ['id', 'personId'],
2110
+ },
2111
+ contactInfos: {
2112
+ after: async ({ rows, tx }) => { /* every row has every column */ },
2113
+ columns: 'all',
2114
+ },
2115
+ }
2116
+ ```
2117
+
2118
+ - The names are **column keys** — the property names on the Drizzle table object, which is
2119
+ what `rows` is keyed by. An unknown name throws at build time, and so does `columns` on a
2120
+ registration with no `after` hook (only `after` is handed rows).
2121
+ - On a top-level registration, a named column must exist on **every** table; `'all'` always
2122
+ works.
2123
+ - The columns are added to `RETURNING` for every write the hook is attached to: insert, upsert,
2124
+ update, `updateMany`, delete and restore. A delete's rows are the rows it removed.
2125
+ - **The client's response is unchanged.** Columns the hook asked for but the client did not
2126
+ select are taken back off before the rows are mapped, so they reach neither the response nor
2127
+ a custom resolver calling the generated one. The hook gets its own copy of the rows.
2128
+ - **MySQL** has no `RETURNING`, so its mutations return no rows and `columns` has nothing to
2129
+ add to: `rows` stays empty there. The names are still checked at build time.
2130
+
2049
2131
  The payload:
2050
2132
 
2051
2133
  | Field | |
@@ -2055,7 +2137,7 @@ The payload:
2055
2137
  | `position` | `'before'` or `'after'` |
2056
2138
  | `single` | whether the field writes one row or a list |
2057
2139
  | `args` | the field's GraphQL arguments, as the resolver received them |
2058
- | `rows` | the post-write rows **as the database returned them**, before the output mapper |
2140
+ | `rows` | the post-write rows **as the database returned them**, before the output mapper — the selected columns plus the hook's [`columns`](#which-columns-rows-holds) |
2059
2141
  | `context`, `info` | the GraphQL context value and resolve info |
2060
2142
  | `tx` | the executor the mutation ran on |
2061
2143
 
@@ -2285,6 +2367,7 @@ that explains it, where there is one.
2285
2367
  | `typeNamePrefix` / `typeNameSuffix` / `derivedTypeNameMapper` | none | [names of the derived types](#renaming-the-derived-types) — filters, inputs, aggregates, shared inputs |
2286
2368
  | `scalars` / `mapColumnType` | built-in detection | [override a column's GraphQL type](#overriding-a-columns-scalar), by name or by rule |
2287
2369
  | `enumNameMapper` | shared `<EnumName>Enum` per database enum | [names the enum a column maps to](#enum-types) |
2370
+ | `enumValueNameMapper` | the stored value | [names the enum's values](#enum-value-names); `'constant'` for CONSTANT_CASE |
2288
2371
  | `describeColumn` / `describeTable` / `describeRelation` | none | [GraphQL descriptions](#documenting-the-schema) |
2289
2372
  | `deprecateColumn` | none | `@deprecated` on a column's output and optional input fields |
2290
2373
  | `conflictDoNothing` | `false` | PostgreSQL only — `ON CONFLICT DO NOTHING` on inserts, which makes `create<Table>Single` nullable |
package/dist/index.cjs CHANGED
@@ -189,6 +189,14 @@ const GraphQLDecimalString = new graphql.GraphQLScalarType({
189
189
  const uncapitalize = (input) => input?.length ? `${input[0].toLocaleLowerCase()}${input.length > 1 ? input.slice(1, input.length) : ""}` : input;
190
190
  const capitalize = (input) => input?.length ? `${input[0].toLocaleUpperCase()}${input.length > 1 ? input.slice(1, input.length) : ""}` : input;
191
191
  /**
192
+ * `inProgress` / `in-progress` / `in progress` / `InProgress` → `IN_PROGRESS`; `HTTPError` →
193
+ * `HTTP_ERROR`. Words are split at non-alphanumeric runs and at lower→upper and
194
+ * acronym→word boundaries, then upper-cased and joined with `_`.
195
+ *
196
+ * The preset behind `enumValueNameMapper: 'constant'`.
197
+ */
198
+ const constantCase = (input) => input.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2").split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.toUpperCase()).join("_");
199
+ /**
192
200
  * The `typeNameMapper` preset behind `typeNameMapper: 'singularize'`.
193
201
  *
194
202
  * Derives the singular and plural forms of a table key with the library's own `pluralize`,
@@ -209,6 +217,8 @@ const singularizeMapper = (tableName) => ({
209
217
  //#endregion
210
218
  //#region src/util/type-converter/index.ts
211
219
  const allowedNameChars = /^[a-zA-Z0-9_]+$/;
220
+ /** A GraphQL `Name` that is also allowed as an enum value — the spec reserves the three literals. */
221
+ const validEnumValueName = (name) => /^[_a-zA-Z][_a-zA-Z0-9]*$/.test(name) && name !== "true" && name !== "false" && name !== "null";
212
222
  /**
213
223
  * The enum type a column was declared with, when it is a *named* one — `pgEnum('status', …)`
214
224
  * shared by however many tables reference it. An inline value list (`text({ enum: [...] })`,
@@ -229,20 +239,35 @@ const pascalize = (input) => input.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((p
229
239
  */
230
240
  const enumValuesKey = (values) => JSON.stringify(values);
231
241
  /**
242
+ * Identity of the GraphQL names those values were given. Kept apart from {@link enumValuesKey}
243
+ * because `enumValueNameMapper` sees the column, so two columns reaching one type can name its
244
+ * values differently — and only one set of names can win.
245
+ */
246
+ const enumValueNamesKey = (names) => JSON.stringify(names);
247
+ /**
232
248
  * Every GraphQL enum name this build has handed out. Two columns that resolve to the same
233
249
  * name share one type — that is how the columns of one `pgEnum` end up on a single type —
234
250
  * so the values are kept alongside it to tell a genuine collision from a re-use.
235
251
  */
236
252
  let enumNamesInUse = /* @__PURE__ */ new Map();
237
253
  let enumNameMapper;
254
+ let enumValueNameMapper;
255
+ /** The value-name presets, by the string a config passes for them. */
256
+ const enumValueNamePresets = { constant: ({ value }) => constantCase(value) };
238
257
  /**
239
258
  * Resets the enum registry for a fresh schema build and installs that build's
240
- * `enumNameMapper`. Called once per `generateSchemaData`, alongside
259
+ * `enumNameMapper` and `enumValueNameMapper`. Called once per `generateSchemaData`, alongside
241
260
  * {@link registerScalarOverrides} — without the reset a second build would reuse the first
242
261
  * build's enum types, and so its naming decisions.
243
262
  */
244
263
  const registerEnumConfig = (config) => {
245
264
  enumNameMapper = config.enumNameMapper;
265
+ const valueMapper = config.enumValueNameMapper;
266
+ if (typeof valueMapper === "string") {
267
+ const preset = enumValueNamePresets[valueMapper];
268
+ if (!preset) throw new Error(`Drizzle-GraphQL Error: Unknown enumValueNameMapper preset '${valueMapper}'. The only preset is 'constant'; pass a function for anything else.`);
269
+ enumValueNameMapper = preset;
270
+ } else enumValueNameMapper = valueMapper;
246
271
  enumNamesInUse = /* @__PURE__ */ new Map();
247
272
  };
248
273
  /**
@@ -271,22 +296,50 @@ const generateEnumCached = (column, columnName, tableName) => {
271
296
  columnName,
272
297
  values
273
298
  }) ?? (declared ? `${pascalize(declared.enumName)}Enum` : `${capitalize(tableName)}${capitalize(columnName)}Enum`);
299
+ const valueNames = values.map((value, index) => {
300
+ const mapped = enumValueNameMapper?.({
301
+ enumName: declared?.enumName,
302
+ schema: declared?.schema,
303
+ tableName,
304
+ columnName,
305
+ values,
306
+ value,
307
+ index,
308
+ typeName: name
309
+ });
310
+ if (mapped === void 0) return allowedNameChars.test(value) ? value : `Option${index}`;
311
+ if (!validEnumValueName(mapped)) throw new Error(`Drizzle-GraphQL Error: config.enumValueNameMapper named value '${value}' of enum '${name}' (on '${tableName}.${columnName}') '${mapped}', which is not a valid GraphQL enum value name. A name must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ and not be true, false or null.`);
312
+ return mapped;
313
+ });
314
+ const seen = /* @__PURE__ */ new Map();
315
+ values.forEach((value, index) => {
316
+ const valueName = valueNames[index];
317
+ const earlier = seen.get(valueName);
318
+ if (earlier !== void 0) throw new Error(`Drizzle-GraphQL Error: Values '${earlier}' and '${value}' of enum '${name}' (on '${tableName}.${columnName}') both map to the GraphQL value name '${valueName}'. Give one of them a different name via config.enumValueNameMapper.`);
319
+ seen.set(valueName, value);
320
+ });
274
321
  const valuesKey = enumValuesKey(values);
322
+ const namesKey = enumValueNamesKey(valueNames);
275
323
  const claimed = enumNamesInUse.get(name);
276
324
  if (claimed) {
277
325
  if (claimed.valuesKey !== valuesKey) throw new Error(`Drizzle-GraphQL Error: Two different enums both map to the GraphQL type name '${name}' (most recently '${tableName}.${columnName}'). Give one of them a different name via config.enumNameMapper.`);
326
+ if (claimed.namesKey !== namesKey) {
327
+ const before = claimed.type.getValues().map((v) => v.name).join(", ");
328
+ throw new Error(`Drizzle-GraphQL Error: Columns sharing the GraphQL enum '${name}' name its values differently ('${tableName}.${columnName}' gives ${valueNames.join(", ")}; an earlier column gave ${before}). config.enumValueNameMapper must name a shared enum's values the same way for every column, or config.enumNameMapper must give the columns separate types.`);
329
+ }
278
330
  return claimed.type;
279
331
  }
280
332
  const gqlEnum = new graphql.GraphQLEnumType({
281
333
  name,
282
- values: Object.fromEntries(values.map((e, index) => [allowedNameChars.test(e) ? e : `Option${index}`, {
334
+ values: Object.fromEntries(values.map((e, index) => [valueNames[index], {
283
335
  value: e,
284
336
  description: `Value: ${e}`
285
337
  }]))
286
338
  });
287
339
  enumNamesInUse.set(name, {
288
340
  type: gqlEnum,
289
- valuesKey
341
+ valuesKey,
342
+ namesKey
290
343
  });
291
344
  return gqlEnum;
292
345
  };
@@ -1041,6 +1094,7 @@ const remapToGraphQLArrayOutput = (queryOutput, tableName, table, relationMap) =
1041
1094
  return queryOutput;
1042
1095
  };
1043
1096
  const remapFromGraphQLCore = (value, column, columnName) => {
1097
+ if (value === null) return null;
1044
1098
  if (getColumnScalarOverride(column, true)) {
1045
1099
  if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
1046
1100
  return value;
@@ -4828,16 +4882,70 @@ const generateTableTypes = (tableName, tables, relationMap, withReturning, relat
4828
4882
  * that has rows, and the one the common audit/outbox case wants — while `{ before, after }`
4829
4883
  * names them explicitly.
4830
4884
  */
4831
- const normalizeWriteHooks = (hooks) => {
4885
+ const normalizeWriteHooks = (hooks, tableColumns, where) => {
4832
4886
  if (!hooks) return;
4833
4887
  if (typeof hooks === "function") return { after: hooks };
4834
4888
  const resolved = {
4835
4889
  before: hooks.before,
4836
4890
  after: hooks.after
4837
4891
  };
4892
+ if (hooks.columns !== void 0) resolved.columns = resolveHookColumns(hooks.columns, !!hooks.after, tableColumns, where);
4838
4893
  return resolved.before || resolved.after ? resolved : void 0;
4839
4894
  };
4840
4895
  /**
4896
+ * Resolves a hook's `columns` against the table it runs on. `where` names the registration
4897
+ * (`config.onWrite.posts`) and, for a top-level one, the table, so the error says which.
4898
+ */
4899
+ const resolveHookColumns = (spec, hasAfter, tableColumns, where) => {
4900
+ if (!hasAfter) throw new Error(`Drizzle-GraphQL Error: ${where}.columns only applies to an 'after' hook, and none is registered.`);
4901
+ if (spec === "all") return { ...tableColumns };
4902
+ if (!Array.isArray(spec)) throw new Error(`Drizzle-GraphQL Error: ${where}.columns must be an array of column names or 'all'.`);
4903
+ const resolved = {};
4904
+ for (const name of spec) {
4905
+ const column = typeof name === "string" && Object.keys(tableColumns).includes(name) ? tableColumns[name] : void 0;
4906
+ if (!column) throw new Error(`Drizzle-GraphQL Error: ${where}.columns names '${String(name)}', which is not a column of that table.`);
4907
+ resolved[name] = column;
4908
+ }
4909
+ return resolved;
4910
+ };
4911
+ /**
4912
+ * Adds a hook's `columns` to a RETURNING column map, and reports which keys it added — the
4913
+ * ones the client never asked for, which have to come back off before the rows are mapped.
4914
+ * Returns the map untouched, and no keys, when the hook named none or the selection already
4915
+ * covers them.
4916
+ */
4917
+ const withHookColumns = (hooks, columns) => {
4918
+ const hookColumns = hooks?.columns;
4919
+ if (!hookColumns) return {
4920
+ returning: columns,
4921
+ added: void 0
4922
+ };
4923
+ const selected = new Set(Object.keys(columns));
4924
+ const added = Object.keys(hookColumns).filter((key) => !selected.has(key));
4925
+ if (!added.length) return {
4926
+ returning: columns,
4927
+ added: void 0
4928
+ };
4929
+ const returning = { ...columns };
4930
+ for (const key of added) returning[key] = hookColumns[key];
4931
+ return {
4932
+ returning,
4933
+ added
4934
+ };
4935
+ };
4936
+ /**
4937
+ * The rows with the hook-only keys taken off, as fresh objects: the hook was handed the
4938
+ * originals and may still hold them, so they are not mutated under it.
4939
+ */
4940
+ const omitHookColumns = (rows, added) => {
4941
+ if (!added) return rows;
4942
+ return rows.map((row) => {
4943
+ const copy = { ...row };
4944
+ for (const key of added) delete copy[key];
4945
+ return copy;
4946
+ });
4947
+ };
4948
+ /**
4841
4949
  * Invokes one position of a field's hooks. Returns `undefined` rather than a resolved promise
4842
4950
  * when nothing is registered, so the awaiting call site costs nothing on an unhooked build.
4843
4951
  */
@@ -4880,6 +4988,7 @@ const writeResolver = ({ db, tableName, operation, single, fieldName, args: quer
4880
4988
  info,
4881
4989
  tx: executor
4882
4990
  };
4991
+ let added;
4883
4992
  return run({
4884
4993
  executor,
4885
4994
  args,
@@ -4889,7 +4998,13 @@ const writeResolver = ({ db, tableName, operation, single, fieldName, args: quer
4889
4998
  after: (rows) => runWriteHook(hooks, "after", {
4890
4999
  ...payload,
4891
5000
  rows: rows ?? []
4892
- })
5001
+ }),
5002
+ returning: (columns) => {
5003
+ const withHook = withHookColumns(hooks, columns);
5004
+ added = withHook.added;
5005
+ return withHook.returning;
5006
+ },
5007
+ output: (rows) => omitHookColumns(rows, added)
4893
5008
  });
4894
5009
  }, !!hooks);
4895
5010
  } catch (e) {
@@ -5063,19 +5178,18 @@ const resolveBuildConfig = (config, schema) => {
5063
5178
  }
5064
5179
  const onWriteConfig = config?.onWrite;
5065
5180
  const writeHooks = /* @__PURE__ */ new Map();
5066
- let globalWriteHooks;
5067
5181
  if (onWriteConfig) {
5068
- if (typeof onWriteConfig === "function" || typeof onWriteConfig.before === "function" || typeof onWriteConfig.after === "function") globalWriteHooks = normalizeWriteHooks(onWriteConfig);
5182
+ if (typeof onWriteConfig === "function" || typeof onWriteConfig.before === "function" || typeof onWriteConfig.after === "function") for (const tableName of featureTables) writeHooks.set(tableName, normalizeWriteHooks(onWriteConfig, (0, drizzle_orm.getColumns)(schema[tableName]), `config.onWrite (on table '${tableName}')`));
5069
5183
  else {
5070
5184
  const tableNames = new Set(tableKeys);
5071
5185
  for (const [tableName, hooks] of Object.entries(onWriteConfig)) {
5072
5186
  if (!tableNames.has(tableName)) throw new Error(`Drizzle-GraphQL Error: config.onWrite names '${tableName}', which is not a table in the Drizzle schema.`);
5073
5187
  if (typeof hooks !== "function" && typeof hooks !== "object") throw new Error(`Drizzle-GraphQL Error: config.onWrite.${tableName} must be a function or an object with 'before' and/or 'after'.`);
5074
- writeHooks.set(tableName, normalizeWriteHooks(hooks));
5188
+ writeHooks.set(tableName, normalizeWriteHooks(hooks, (0, drizzle_orm.getColumns)(schema[tableName]), `config.onWrite.${tableName}`));
5075
5189
  }
5076
5190
  }
5077
5191
  }
5078
- const onWrite = globalWriteHooks || writeHooks.size ? (tableName) => globalWriteHooks ?? writeHooks.get(tableName) : void 0;
5192
+ const onWrite = writeHooks.size ? (tableName) => writeHooks.get(tableName) : void 0;
5079
5193
  const policies = scopeConfig || contextValuesConfig || softDeleteInfos.size || onWrite || defaultOrderBy ? {
5080
5194
  scope: scopeConfig ? (tableName) => scopeConfig[tableName] : void 0,
5081
5195
  contextValues: contextValuesConfig ? (tableName) => contextValuesConfig[tableName] : void 0,
@@ -5141,6 +5255,7 @@ const resolveBuildConfig = (config, schema) => {
5141
5255
  scalars: config?.scalars,
5142
5256
  mapColumnType: config?.mapColumnType,
5143
5257
  enumNameMapper: config?.enumNameMapper,
5258
+ enumValueNameMapper: config?.enumValueNameMapper,
5144
5259
  transactions,
5145
5260
  limits,
5146
5261
  policies,
@@ -7024,7 +7139,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7024
7139
  txCtx,
7025
7140
  policies,
7026
7141
  args: queryArgs,
7027
- run: async ({ executor, args, context, info, before, after }) => {
7142
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7028
7143
  const supplied = single ? [args.values] : args.values;
7029
7144
  if (!supplied.length) throw drizzleError("No values were provided!", { code: "DRIZZLE_NO_VALUES" });
7030
7145
  await before();
@@ -7046,13 +7161,13 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7046
7161
  scope,
7047
7162
  defaultOrderBy: policies?.defaultOrderBy
7048
7163
  });
7049
- const returning = nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns;
7164
+ const returning = withHookReturning(nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns);
7050
7165
  const runInsert = async (target, values) => {
7051
7166
  let query = target.insert(table).values(values).returning(returning);
7052
7167
  if (conflictDoNothing) query = query.onConflictDoNothing();
7053
7168
  return await query;
7054
7169
  };
7055
- const result = nestedEntries ? await writeWithNestedOps({
7170
+ const written = nestedEntries ? await writeWithNestedOps({
7056
7171
  executor,
7057
7172
  runtime: nested,
7058
7173
  tableName,
@@ -7061,7 +7176,8 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7061
7176
  write: (tx, values) => runInsert(tx, [values]),
7062
7177
  context
7063
7178
  }) : await runInsert(executor, input);
7064
- await after(result);
7179
+ await after(written);
7180
+ const result = output(written);
7065
7181
  if (single && !result[0]) {
7066
7182
  if (!conflictDoNothing) throw drizzleError("The insert returned no row.", { code: "DRIZZLE_NO_ROW_RETURNED" });
7067
7183
  return;
@@ -7097,7 +7213,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7097
7213
  txCtx,
7098
7214
  policies,
7099
7215
  args: queryArgs,
7100
- run: async ({ executor, args, context, info, before, after }) => {
7216
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7101
7217
  const supplied = single ? [args.values] : args.values;
7102
7218
  if (!supplied.length) throw drizzleError("No values were provided!", { code: "DRIZZLE_NO_VALUES" });
7103
7219
  await before();
@@ -7119,7 +7235,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7119
7235
  scope,
7120
7236
  defaultOrderBy: policies?.defaultOrderBy
7121
7237
  });
7122
- const returning = nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns;
7238
+ const returning = withHookReturning(nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns);
7123
7239
  const runUpsert = async (target, values) => {
7124
7240
  const plan = resolveConflictPlan({
7125
7241
  table,
@@ -7140,7 +7256,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7140
7256
  });
7141
7257
  return await query;
7142
7258
  };
7143
- const result = nestedEntries ? await writeWithNestedOps({
7259
+ const written = nestedEntries ? await writeWithNestedOps({
7144
7260
  executor,
7145
7261
  runtime: nested,
7146
7262
  tableName,
@@ -7149,7 +7265,8 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7149
7265
  write: (tx, values) => runUpsert(tx, [values]),
7150
7266
  context
7151
7267
  }) : await runUpsert(executor, input);
7152
- await after(result);
7268
+ await after(written);
7269
+ const result = output(written);
7153
7270
  if (single && !result[0]) return;
7154
7271
  const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
7155
7272
  return single ? remapToGraphQLSingleOutput(enriched[0], tableName, table, relationMap) : remapToGraphQLArrayOutput(enriched, tableName, table, relationMap);
@@ -7173,7 +7290,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7173
7290
  txCtx,
7174
7291
  policies,
7175
7292
  args: queryArgs,
7176
- run: async ({ executor, args, context, info, before, after }) => {
7293
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7177
7294
  const { where, set } = args;
7178
7295
  const scope = policies?.scope?.(context);
7179
7296
  await before();
@@ -7204,14 +7321,14 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7204
7321
  required: single || requireWhere
7205
7322
  });
7206
7323
  if (single) await assertSingleMatch(executor, table, filters);
7207
- const returning = nestedOps ? nested.withJoinColumns(tableName, nestedOps, { ...columns }, table) : columns;
7324
+ const returning = withHookReturning(nestedOps ? nested.withJoinColumns(tableName, nestedOps, { ...columns }, table) : columns);
7208
7325
  const runUpdate = async (target, values) => {
7209
7326
  const writes = Object.keys(values).length > 0;
7210
7327
  let query = writes ? target.update(table).set(values) : target.select(returning).from(table);
7211
7328
  if (filters) query = query.where(filters);
7212
7329
  return await (writes ? query.returning(returning) : query);
7213
7330
  };
7214
- const result = nestedOps ? await updateWithNestedOps({
7331
+ const written = nestedOps ? await updateWithNestedOps({
7215
7332
  executor,
7216
7333
  runtime: nested,
7217
7334
  tableName,
@@ -7221,7 +7338,8 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7221
7338
  write: runUpdate,
7222
7339
  context
7223
7340
  }) : await runUpdate(executor, input);
7224
- await after(result);
7341
+ await after(written);
7342
+ const result = output(written);
7225
7343
  if (single && result.length > 1) throw drizzleError("'where' matched more than one row!", { code: "DRIZZLE_MULTI_ROW_MATCH" });
7226
7344
  if (single && !result[0]) return;
7227
7345
  const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
@@ -7261,7 +7379,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7261
7379
  txCtx,
7262
7380
  policies,
7263
7381
  args: queryArgs,
7264
- run: async ({ executor, args, context, info, before, after }) => {
7382
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7265
7383
  const { where } = args;
7266
7384
  const hard = canHardDelete && args.hard === true;
7267
7385
  const scope = policies?.scope?.(context);
@@ -7281,9 +7399,10 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7281
7399
  if (single) await assertSingleMatch(executor, table, filters);
7282
7400
  let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
7283
7401
  if (filters) query = query.where(filters);
7284
- query = query.returning(columns);
7285
- const result = await query;
7286
- await after(result);
7402
+ query = query.returning(withHookReturning(columns));
7403
+ const written = await query;
7404
+ await after(written);
7405
+ const result = output(written);
7287
7406
  if (single && result.length > 1) throw drizzleError("'where' matched more than one row!", { code: "DRIZZLE_MULTI_ROW_MATCH" });
7288
7407
  if (single) return result[0] ? remapToGraphQLSingleOutput(result[0], tableName, table) : void 0;
7289
7408
  return remapToGraphQLArrayOutput(result, tableName, table);
@@ -7672,7 +7791,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7672
7791
  txCtx,
7673
7792
  policies,
7674
7793
  args: queryArgs,
7675
- run: async ({ executor, args, context, info, before, after }) => {
7794
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output: withoutHookColumns }) => {
7676
7795
  const { updates } = args;
7677
7796
  if (!updates.length) throw drizzleError("No updates were provided!", { code: "DRIZZLE_NO_VALUES" });
7678
7797
  await before();
@@ -7703,7 +7822,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7703
7822
  };
7704
7823
  });
7705
7824
  const anyNested = entries.some((entry) => entry.ops);
7706
- const returning = anyNested ? nested.withJoinColumns(tableName, mergedOps(entries.map((entry) => ({ ops: entry.ops ?? {} }))), { ...columns }, table) : columns;
7825
+ const returning = withHookReturning(anyNested ? nested.withJoinColumns(tableName, mergedOps(entries.map((entry) => ({ ops: entry.ops ?? {} }))), { ...columns }, table) : columns);
7707
7826
  const perEntry = await runBatch({
7708
7827
  executor,
7709
7828
  entries,
@@ -7714,8 +7833,9 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7714
7833
  tableName,
7715
7834
  context
7716
7835
  });
7717
- const flatRows = perEntry.flat();
7718
- await after(flatRows);
7836
+ const written = perEntry.flat();
7837
+ await after(written);
7838
+ const flatRows = withoutHookColumns(written);
7719
7839
  const enriched = await withEagerRelations(executor, tableName, flatRows, pkNames, withParams, hasRelations);
7720
7840
  const output = [];
7721
7841
  let offset = 0;