@vantreeseba/drizzle-graphql 12.0.1 → 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
@@ -104,8 +104,10 @@ as a preset:
104
104
  const { schema } = buildSchema(db, { typeNameMapper: 'singularize' })
105
105
  ```
106
106
 
107
- `tasks` then gives type `Task`, queries `tasks` (list) and `task` (single), mutations
108
- `createTasks` (array) and `createTask` (single), `updateTask`, `deleteTask`. Irregular
107
+ `tasks` then gives type `Task`, queries `tasks` (list) and `task` (single), and each write
108
+ as a pair named the same way: `createTasks` / `createTask`, `updateTasks` / `updateTask`,
109
+ `deleteTasks` / `deleteTask` — plural for the form that operates on a set of rows, singular
110
+ for the one that operates on exactly one. Irregular
109
111
  plurals go through `pluralize`, so `people` gives `Person` / `people` / `person` and
110
112
  `addresses` gives `Address` rather than the `addresss` a naive suffix would produce. A table
111
113
  key that is already singular is pluralized for the list side, and a capitalized key is
@@ -362,9 +364,9 @@ variants that carry their own disambiguator — `usersAggregate`, `usersGroupBy`
362
364
  The list and single suffixes cannot be identical unless a `typeNameMapper` is also given —
363
365
  `users` and `usersSingle` would otherwise collapse onto one field name, and `buildSchema`
364
366
  throws rather than generating a schema that silently drops one of them. With a mapper the
365
- plural and singular nouns keep the queries and the inserts apart, but the bulk update,
366
- delete and restore mutations are singular on both sides, so those three keep a `Single`
367
- suffix whenever the two configured suffixes are the same string.
367
+ plural and singular nouns keep every pair apart on their own, so both suffixes may be empty
368
+ and no operation carries a `Single` at all. A table the mapper returns `undefined` for is
369
+ back on the default naming and still needs the suffixes to differ.
368
370
 
369
371
  That check compares the configured suffixes, which is only part of what a name is made of.
370
372
  Every generated root field is also registered by name, and a build that puts two operations
@@ -381,7 +383,8 @@ import pluralize from 'pluralize'
381
383
  const { schema } = buildSchema(db, {
382
384
  typeNameMapper: (table) => ({ singular: pluralize.singular(table), plural: table }),
383
385
  })
384
- // type User; queries `users` / `user`; mutations `createUsers` / `createUser`
386
+ // type User; queries `users` / `user`; mutations `createUsers` / `createUser`,
387
+ // `updateUsers` / `updateUser`, `deleteUsers` / `deleteUser`
385
388
  ```
386
389
 
387
390
  Return `undefined` for any table that should keep its default naming.
@@ -708,6 +711,51 @@ lists is the build-time error above.
708
711
  > `${tableName}${columnName[0]!.toUpperCase()}${columnName.slice(1)}Enum`
709
712
  > ```
710
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
+
711
759
  ## Documenting the schema
712
760
 
713
761
  Four hooks put descriptions and deprecations on the generated types. Each is called per
@@ -1590,7 +1638,9 @@ mutation {
1590
1638
  ## Single-row update & delete
1591
1639
 
1592
1640
  Alongside the plural `update<Table>` / `delete<Table>` mutations, every table gets an
1593
- `update<Table>Single` / `delete<Table>Single` variant that targets exactly one row:
1641
+ `update<Table>Single` / `delete<Table>Single` variant that targets exactly one row. (Under a
1642
+ [`typeNameMapper`](#naming) the pair is separated by the noun instead — `updateUsers` /
1643
+ `updateUser` — and there is no `Single`.)
1594
1644
 
1595
1645
  ```graphql
1596
1646
  mutation {
@@ -1874,7 +1924,7 @@ to match on prose:
1874
1924
  ```json
1875
1925
  {
1876
1926
  "message": "'where' matched more than one row — nothing was written!",
1877
- "path": ["updateUser"],
1927
+ "path": ["updateUsersSingle"],
1878
1928
  "extensions": {
1879
1929
  "code": "DRIZZLE_MULTI_ROW_MATCH",
1880
1930
  "drizzle": { "table": "Users", "operation": "update", "field": "updateUsersSingle" }
@@ -2014,8 +2064,12 @@ outside the resolver, so its after-position is after the commit.
2014
2064
  ```Typescript
2015
2065
  const { schema } = buildSchema(db, {
2016
2066
  onWrite: {
2017
- posts: async ({ table, operation, rows, tx }) => {
2018
- 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'],
2019
2073
  },
2020
2074
  },
2021
2075
  })
@@ -2041,6 +2095,39 @@ Registering a bare function (or a positions object) at the top level applies it
2041
2095
  anything else is read as a table map keyed by the **Drizzle schema key**, and a name that
2042
2096
  matches nothing throws at build time.
2043
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
+
2044
2131
  The payload:
2045
2132
 
2046
2133
  | Field | |
@@ -2050,7 +2137,7 @@ The payload:
2050
2137
  | `position` | `'before'` or `'after'` |
2051
2138
  | `single` | whether the field writes one row or a list |
2052
2139
  | `args` | the field's GraphQL arguments, as the resolver received them |
2053
- | `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) |
2054
2141
  | `context`, `info` | the GraphQL context value and resolve info |
2055
2142
  | `tx` | the executor the mutation ran on |
2056
2143
 
@@ -2280,6 +2367,7 @@ that explains it, where there is one.
2280
2367
  | `typeNamePrefix` / `typeNameSuffix` / `derivedTypeNameMapper` | none | [names of the derived types](#renaming-the-derived-types) — filters, inputs, aggregates, shared inputs |
2281
2368
  | `scalars` / `mapColumnType` | built-in detection | [override a column's GraphQL type](#overriding-a-columns-scalar), by name or by rule |
2282
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 |
2283
2371
  | `describeColumn` / `describeTable` / `describeRelation` | none | [GraphQL descriptions](#documenting-the-schema) |
2284
2372
  | `deprecateColumn` | none | `@deprecated` on a column's output and optional input fields |
2285
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;
@@ -4320,17 +4374,15 @@ const computeResolverFieldNames = (tableName, typeNameMapper, prefixes, suffixes
4320
4374
  const upsertPrefix = prefixes.upsert ?? "upsert";
4321
4375
  const upsertArrayFieldName = `${upsertPrefix}${pluralNoun}${suffixes.list}`;
4322
4376
  const upsertSingleFieldName = mapped ? `${upsertPrefix}${capitalize(mapped.singular)}` : `${upsertPrefix}${capitalize(tableName)}${suffixes.single}`;
4323
- const updateBase = `${prefixes.update}${singularNoun}`;
4324
- const deleteBase = `${prefixes.delete}${singularNoun}`;
4325
- const restoreBase = `${prefixes.restore ?? "restore"}${singularNoun}`;
4326
- const updateFieldName = `${updateBase}${suffixes.list}`;
4377
+ const restorePrefix = prefixes.restore ?? "restore";
4378
+ const updateFieldName = `${prefixes.update}${pluralNoun}${suffixes.list}`;
4327
4379
  const updateManyFieldName = `${prefixes.update}${pluralNoun}Many`;
4328
- const deleteFieldName = `${deleteBase}${suffixes.list}`;
4329
- const restoreFieldName = `${restoreBase}${suffixes.list}`;
4330
- const writeSingleSuffix = suffixes.single === suffixes.list ? "Single" : suffixes.single;
4331
- const updateSingleFieldName = `${updateBase}${writeSingleSuffix}`;
4332
- const deleteSingleFieldName = `${deleteBase}${writeSingleSuffix}`;
4333
- const restoreSingleFieldName = `${restoreBase}${writeSingleSuffix}`;
4380
+ const deleteFieldName = `${prefixes.delete}${pluralNoun}${suffixes.list}`;
4381
+ const restoreFieldName = `${restorePrefix}${pluralNoun}${suffixes.list}`;
4382
+ const singleWrite = (prefix) => mapped ? `${prefix}${singularNoun}` : `${prefix}${singularNoun}${suffixes.single}`;
4383
+ const updateSingleFieldName = singleWrite(prefixes.update);
4384
+ const deleteSingleFieldName = singleWrite(prefixes.delete);
4385
+ const restoreSingleFieldName = singleWrite(restorePrefix);
4334
4386
  return {
4335
4387
  typeName,
4336
4388
  listFieldName,
@@ -4830,16 +4882,70 @@ const generateTableTypes = (tableName, tables, relationMap, withReturning, relat
4830
4882
  * that has rows, and the one the common audit/outbox case wants — while `{ before, after }`
4831
4883
  * names them explicitly.
4832
4884
  */
4833
- const normalizeWriteHooks = (hooks) => {
4885
+ const normalizeWriteHooks = (hooks, tableColumns, where) => {
4834
4886
  if (!hooks) return;
4835
4887
  if (typeof hooks === "function") return { after: hooks };
4836
4888
  const resolved = {
4837
4889
  before: hooks.before,
4838
4890
  after: hooks.after
4839
4891
  };
4892
+ if (hooks.columns !== void 0) resolved.columns = resolveHookColumns(hooks.columns, !!hooks.after, tableColumns, where);
4840
4893
  return resolved.before || resolved.after ? resolved : void 0;
4841
4894
  };
4842
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
+ /**
4843
4949
  * Invokes one position of a field's hooks. Returns `undefined` rather than a resolved promise
4844
4950
  * when nothing is registered, so the awaiting call site costs nothing on an unhooked build.
4845
4951
  */
@@ -4882,6 +4988,7 @@ const writeResolver = ({ db, tableName, operation, single, fieldName, args: quer
4882
4988
  info,
4883
4989
  tx: executor
4884
4990
  };
4991
+ let added;
4885
4992
  return run({
4886
4993
  executor,
4887
4994
  args,
@@ -4891,7 +4998,13 @@ const writeResolver = ({ db, tableName, operation, single, fieldName, args: quer
4891
4998
  after: (rows) => runWriteHook(hooks, "after", {
4892
4999
  ...payload,
4893
5000
  rows: rows ?? []
4894
- })
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)
4895
5008
  });
4896
5009
  }, !!hooks);
4897
5010
  } catch (e) {
@@ -5065,19 +5178,18 @@ const resolveBuildConfig = (config, schema) => {
5065
5178
  }
5066
5179
  const onWriteConfig = config?.onWrite;
5067
5180
  const writeHooks = /* @__PURE__ */ new Map();
5068
- let globalWriteHooks;
5069
5181
  if (onWriteConfig) {
5070
- 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}')`));
5071
5183
  else {
5072
5184
  const tableNames = new Set(tableKeys);
5073
5185
  for (const [tableName, hooks] of Object.entries(onWriteConfig)) {
5074
5186
  if (!tableNames.has(tableName)) throw new Error(`Drizzle-GraphQL Error: config.onWrite names '${tableName}', which is not a table in the Drizzle schema.`);
5075
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'.`);
5076
- writeHooks.set(tableName, normalizeWriteHooks(hooks));
5188
+ writeHooks.set(tableName, normalizeWriteHooks(hooks, (0, drizzle_orm.getColumns)(schema[tableName]), `config.onWrite.${tableName}`));
5077
5189
  }
5078
5190
  }
5079
5191
  }
5080
- const onWrite = globalWriteHooks || writeHooks.size ? (tableName) => globalWriteHooks ?? writeHooks.get(tableName) : void 0;
5192
+ const onWrite = writeHooks.size ? (tableName) => writeHooks.get(tableName) : void 0;
5081
5193
  const policies = scopeConfig || contextValuesConfig || softDeleteInfos.size || onWrite || defaultOrderBy ? {
5082
5194
  scope: scopeConfig ? (tableName) => scopeConfig[tableName] : void 0,
5083
5195
  contextValues: contextValuesConfig ? (tableName) => contextValuesConfig[tableName] : void 0,
@@ -5143,6 +5255,7 @@ const resolveBuildConfig = (config, schema) => {
5143
5255
  scalars: config?.scalars,
5144
5256
  mapColumnType: config?.mapColumnType,
5145
5257
  enumNameMapper: config?.enumNameMapper,
5258
+ enumValueNameMapper: config?.enumValueNameMapper,
5146
5259
  transactions,
5147
5260
  limits,
5148
5261
  policies,
@@ -7026,7 +7139,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7026
7139
  txCtx,
7027
7140
  policies,
7028
7141
  args: queryArgs,
7029
- run: async ({ executor, args, context, info, before, after }) => {
7142
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7030
7143
  const supplied = single ? [args.values] : args.values;
7031
7144
  if (!supplied.length) throw drizzleError("No values were provided!", { code: "DRIZZLE_NO_VALUES" });
7032
7145
  await before();
@@ -7048,13 +7161,13 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7048
7161
  scope,
7049
7162
  defaultOrderBy: policies?.defaultOrderBy
7050
7163
  });
7051
- const returning = nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns;
7164
+ const returning = withHookReturning(nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns);
7052
7165
  const runInsert = async (target, values) => {
7053
7166
  let query = target.insert(table).values(values).returning(returning);
7054
7167
  if (conflictDoNothing) query = query.onConflictDoNothing();
7055
7168
  return await query;
7056
7169
  };
7057
- const result = nestedEntries ? await writeWithNestedOps({
7170
+ const written = nestedEntries ? await writeWithNestedOps({
7058
7171
  executor,
7059
7172
  runtime: nested,
7060
7173
  tableName,
@@ -7063,7 +7176,8 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7063
7176
  write: (tx, values) => runInsert(tx, [values]),
7064
7177
  context
7065
7178
  }) : await runInsert(executor, input);
7066
- await after(result);
7179
+ await after(written);
7180
+ const result = output(written);
7067
7181
  if (single && !result[0]) {
7068
7182
  if (!conflictDoNothing) throw drizzleError("The insert returned no row.", { code: "DRIZZLE_NO_ROW_RETURNED" });
7069
7183
  return;
@@ -7099,7 +7213,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7099
7213
  txCtx,
7100
7214
  policies,
7101
7215
  args: queryArgs,
7102
- run: async ({ executor, args, context, info, before, after }) => {
7216
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7103
7217
  const supplied = single ? [args.values] : args.values;
7104
7218
  if (!supplied.length) throw drizzleError("No values were provided!", { code: "DRIZZLE_NO_VALUES" });
7105
7219
  await before();
@@ -7121,7 +7235,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7121
7235
  scope,
7122
7236
  defaultOrderBy: policies?.defaultOrderBy
7123
7237
  });
7124
- const returning = nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns;
7238
+ const returning = withHookReturning(nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns);
7125
7239
  const runUpsert = async (target, values) => {
7126
7240
  const plan = resolveConflictPlan({
7127
7241
  table,
@@ -7142,7 +7256,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7142
7256
  });
7143
7257
  return await query;
7144
7258
  };
7145
- const result = nestedEntries ? await writeWithNestedOps({
7259
+ const written = nestedEntries ? await writeWithNestedOps({
7146
7260
  executor,
7147
7261
  runtime: nested,
7148
7262
  tableName,
@@ -7151,7 +7265,8 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7151
7265
  write: (tx, values) => runUpsert(tx, [values]),
7152
7266
  context
7153
7267
  }) : await runUpsert(executor, input);
7154
- await after(result);
7268
+ await after(written);
7269
+ const result = output(written);
7155
7270
  if (single && !result[0]) return;
7156
7271
  const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
7157
7272
  return single ? remapToGraphQLSingleOutput(enriched[0], tableName, table, relationMap) : remapToGraphQLArrayOutput(enriched, tableName, table, relationMap);
@@ -7175,7 +7290,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7175
7290
  txCtx,
7176
7291
  policies,
7177
7292
  args: queryArgs,
7178
- run: async ({ executor, args, context, info, before, after }) => {
7293
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7179
7294
  const { where, set } = args;
7180
7295
  const scope = policies?.scope?.(context);
7181
7296
  await before();
@@ -7206,14 +7321,14 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7206
7321
  required: single || requireWhere
7207
7322
  });
7208
7323
  if (single) await assertSingleMatch(executor, table, filters);
7209
- const returning = nestedOps ? nested.withJoinColumns(tableName, nestedOps, { ...columns }, table) : columns;
7324
+ const returning = withHookReturning(nestedOps ? nested.withJoinColumns(tableName, nestedOps, { ...columns }, table) : columns);
7210
7325
  const runUpdate = async (target, values) => {
7211
7326
  const writes = Object.keys(values).length > 0;
7212
7327
  let query = writes ? target.update(table).set(values) : target.select(returning).from(table);
7213
7328
  if (filters) query = query.where(filters);
7214
7329
  return await (writes ? query.returning(returning) : query);
7215
7330
  };
7216
- const result = nestedOps ? await updateWithNestedOps({
7331
+ const written = nestedOps ? await updateWithNestedOps({
7217
7332
  executor,
7218
7333
  runtime: nested,
7219
7334
  tableName,
@@ -7223,7 +7338,8 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7223
7338
  write: runUpdate,
7224
7339
  context
7225
7340
  }) : await runUpdate(executor, input);
7226
- await after(result);
7341
+ await after(written);
7342
+ const result = output(written);
7227
7343
  if (single && result.length > 1) throw drizzleError("'where' matched more than one row!", { code: "DRIZZLE_MULTI_ROW_MATCH" });
7228
7344
  if (single && !result[0]) return;
7229
7345
  const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
@@ -7263,7 +7379,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7263
7379
  txCtx,
7264
7380
  policies,
7265
7381
  args: queryArgs,
7266
- run: async ({ executor, args, context, info, before, after }) => {
7382
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7267
7383
  const { where } = args;
7268
7384
  const hard = canHardDelete && args.hard === true;
7269
7385
  const scope = policies?.scope?.(context);
@@ -7283,9 +7399,10 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7283
7399
  if (single) await assertSingleMatch(executor, table, filters);
7284
7400
  let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
7285
7401
  if (filters) query = query.where(filters);
7286
- query = query.returning(columns);
7287
- const result = await query;
7288
- await after(result);
7402
+ query = query.returning(withHookReturning(columns));
7403
+ const written = await query;
7404
+ await after(written);
7405
+ const result = output(written);
7289
7406
  if (single && result.length > 1) throw drizzleError("'where' matched more than one row!", { code: "DRIZZLE_MULTI_ROW_MATCH" });
7290
7407
  if (single) return result[0] ? remapToGraphQLSingleOutput(result[0], tableName, table) : void 0;
7291
7408
  return remapToGraphQLArrayOutput(result, tableName, table);
@@ -7674,7 +7791,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7674
7791
  txCtx,
7675
7792
  policies,
7676
7793
  args: queryArgs,
7677
- run: async ({ executor, args, context, info, before, after }) => {
7794
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output: withoutHookColumns }) => {
7678
7795
  const { updates } = args;
7679
7796
  if (!updates.length) throw drizzleError("No updates were provided!", { code: "DRIZZLE_NO_VALUES" });
7680
7797
  await before();
@@ -7705,7 +7822,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7705
7822
  };
7706
7823
  });
7707
7824
  const anyNested = entries.some((entry) => entry.ops);
7708
- 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);
7709
7826
  const perEntry = await runBatch({
7710
7827
  executor,
7711
7828
  entries,
@@ -7716,8 +7833,9 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7716
7833
  tableName,
7717
7834
  context
7718
7835
  });
7719
- const flatRows = perEntry.flat();
7720
- await after(flatRows);
7836
+ const written = perEntry.flat();
7837
+ await after(written);
7838
+ const flatRows = withoutHookColumns(written);
7721
7839
  const enriched = await withEagerRelations(executor, tableName, flatRows, pkNames, withParams, hasRelations);
7722
7840
  const output = [];
7723
7841
  let offset = 0;