@vantreeseba/drizzle-graphql 13.0.0 → 13.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -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;
@@ -3694,6 +3748,145 @@ const runMutation = async (db, context, info, txCtx, body, forceTx) => {
3694
3748
  return runOnSharedTx(state, body);
3695
3749
  };
3696
3750
  //#endregion
3751
+ //#region src/util/builders/common/write-hooks.ts
3752
+ /**
3753
+ * Normalizes the two registration shapes: a bare function is the `after` hook — the position
3754
+ * that has rows, and the one the common audit/outbox case wants — while `{ before, after }`
3755
+ * names them explicitly.
3756
+ */
3757
+ const normalizeWriteHooks = (hooks, tableColumns, where) => {
3758
+ if (!hooks) return;
3759
+ if (typeof hooks === "function") return { after: hooks };
3760
+ const resolved = {
3761
+ before: hooks.before,
3762
+ after: hooks.after
3763
+ };
3764
+ if (hooks.columns !== void 0) resolved.columns = resolveHookColumns(hooks.columns, !!hooks.after, tableColumns, where);
3765
+ return resolved.before || resolved.after ? resolved : void 0;
3766
+ };
3767
+ /**
3768
+ * Resolves a hook's `columns` against the table it runs on. `where` names the registration
3769
+ * (`config.onWrite.posts`) and, for a top-level one, the table, so the error says which.
3770
+ */
3771
+ const resolveHookColumns = (spec, hasAfter, tableColumns, where) => {
3772
+ if (!hasAfter) throw new Error(`Drizzle-GraphQL Error: ${where}.columns only applies to an 'after' hook, and none is registered.`);
3773
+ if (spec === "all") return { ...tableColumns };
3774
+ if (!Array.isArray(spec)) throw new Error(`Drizzle-GraphQL Error: ${where}.columns must be an array of column names or 'all'.`);
3775
+ const resolved = {};
3776
+ for (const name of spec) {
3777
+ const column = typeof name === "string" && Object.keys(tableColumns).includes(name) ? tableColumns[name] : void 0;
3778
+ if (!column) throw new Error(`Drizzle-GraphQL Error: ${where}.columns names '${String(name)}', which is not a column of that table.`);
3779
+ resolved[name] = column;
3780
+ }
3781
+ return resolved;
3782
+ };
3783
+ /**
3784
+ * Adds a hook's `columns` to a RETURNING column map, and reports which keys it added — the
3785
+ * ones the client never asked for, which have to come back off before the rows are mapped.
3786
+ * Returns the map untouched, and no keys, when the hook named none or the selection already
3787
+ * covers them.
3788
+ */
3789
+ const withHookColumns = (hooks, columns) => {
3790
+ const hookColumns = hooks?.columns;
3791
+ if (!hookColumns) return {
3792
+ returning: columns,
3793
+ added: void 0
3794
+ };
3795
+ const selected = new Set(Object.keys(columns));
3796
+ const added = Object.keys(hookColumns).filter((key) => !selected.has(key));
3797
+ if (!added.length) return {
3798
+ returning: columns,
3799
+ added: void 0
3800
+ };
3801
+ const returning = { ...columns };
3802
+ for (const key of added) returning[key] = hookColumns[key];
3803
+ return {
3804
+ returning,
3805
+ added
3806
+ };
3807
+ };
3808
+ /**
3809
+ * The rows with the hook-only keys taken off, as fresh objects: the hook was handed the
3810
+ * originals and may still hold them, so they are not mutated under it.
3811
+ */
3812
+ const omitHookColumns = (rows, added) => {
3813
+ if (!added) return rows;
3814
+ return rows.map((row) => {
3815
+ const copy = { ...row };
3816
+ for (const key of added) delete copy[key];
3817
+ return copy;
3818
+ });
3819
+ };
3820
+ /**
3821
+ * Invokes one position of a field's hooks. Returns `undefined` rather than a resolved promise
3822
+ * when nothing is registered, so the awaiting call site costs nothing on an unhooked build.
3823
+ */
3824
+ const runWriteHook = (hooks, position, payload) => {
3825
+ const hook = position === "before" ? hooks?.before : hooks?.after;
3826
+ if (!hook) return;
3827
+ return Promise.resolve(hook({
3828
+ ...payload,
3829
+ position,
3830
+ rows: payload.rows ?? []
3831
+ }));
3832
+ };
3833
+ //#endregion
3834
+ //#region src/util/builders/common/write-scaffold.ts
3835
+ /**
3836
+ * Wraps one mutation body in the scaffolding above and returns the field it becomes.
3837
+ *
3838
+ * A field carrying a hook is forced into a transaction even when it would have run straight
3839
+ * on `db`: a hook exists to write atomically with the mutation, and one that ran outside the
3840
+ * transaction could not roll anything back.
3841
+ */
3842
+ const writeResolver = ({ db, tableName, operation, single, fieldName, args: queryArgs, txCtx, policies, run }) => {
3843
+ const hooks = policies?.onWrite?.(tableName, operation);
3844
+ const errorCtx = {
3845
+ table: tableName,
3846
+ operation,
3847
+ field: fieldName
3848
+ };
3849
+ return {
3850
+ name: fieldName,
3851
+ resolver: async (_source, args, context, info) => {
3852
+ try {
3853
+ return await runMutation(db, context, info, txCtx, (executor) => {
3854
+ const payload = {
3855
+ table: tableName,
3856
+ operation,
3857
+ single,
3858
+ args,
3859
+ context,
3860
+ info,
3861
+ tx: executor
3862
+ };
3863
+ let added;
3864
+ return run({
3865
+ executor,
3866
+ args,
3867
+ context,
3868
+ info,
3869
+ before: () => runWriteHook(hooks, "before", payload),
3870
+ after: (rows) => runWriteHook(hooks, "after", {
3871
+ ...payload,
3872
+ rows: rows ?? []
3873
+ }),
3874
+ returning: (columns) => {
3875
+ const withHook = withHookColumns(hooks, columns);
3876
+ added = withHook.added;
3877
+ return withHook.returning;
3878
+ },
3879
+ output: (rows) => omitHookColumns(rows, added)
3880
+ });
3881
+ }, !!hooks);
3882
+ } catch (e) {
3883
+ throw withErrorContext(toGraphQLError(e), errorCtx);
3884
+ }
3885
+ },
3886
+ args: queryArgs
3887
+ };
3888
+ };
3889
+ //#endregion
3697
3890
  //#region src/util/builders/common/mutation-helpers.ts
3698
3891
  /**
3699
3892
  * The `hard` argument on the delete mutations of a soft-deleting table that opted into
@@ -3837,54 +4030,79 @@ const assertSingleMatch = async (executor, table, filters) => {
3837
4030
  * The plural `update` and `delete` always `RETURNING *`, which is the wrong shape for
3838
4031
  * "archive everything older than a year": the caller pays to ship rows it does not want, and
3839
4032
  * on a large match that cost is the whole mutation. These return `Int!` instead — how many
3840
- * rows the write touched, as the driver reported it, with no RETURNING clause at all.
4033
+ * rows the write touched.
3841
4034
  *
3842
- * Dialect-independent: drizzle's update/delete builders take the same calls everywhere, and
3843
- * the drivers' disagreement about how to spell the row count is handled by
3844
- * {@link rowsAffected}.
3845
- */
3846
- const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested }) => {
4035
+ * Leaving the payload off changes nothing else: the write obeys every rule of the plural
4036
+ * mutation it mirrors, through the same {@link writeResolver} scaffolding.
4037
+ *
4038
+ * - `scope` (and the soft-delete marker) is ANDed into the `where`, so only rows the plural
4039
+ * write could reach are counted and written.
4040
+ * - On a soft-deleting table the delete is the marking `UPDATE`, not a `DELETE`, and rows that
4041
+ * are already marked are not matched; the update skips marked rows too. `hard: true` exists
4042
+ * exactly where the plural delete takes it.
4043
+ * - Context-derived columns are stripped from the update's `set`, as the plural update does.
4044
+ * - `onWrite` hooks run `before` and `after`, inside a transaction. The `after` hook is handed
4045
+ * rows, so a table with one pays for a `RETURNING` — of the columns the hook names in
4046
+ * `columns`, or of the primary key when it names none (the whole row on a table without a
4047
+ * key) — and the count is the number of rows returned. Every other write, and every write on
4048
+ * MySQL (no `RETURNING`; its hooks see `rows: []` as on every MySQL mutation), skips the
4049
+ * `RETURNING` and reads the count off the driver's result via {@link rowsAffected}.
4050
+ */
4051
+ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested, policies, pkNames = [], returnsRows }) => {
4052
+ const softDelete = kind === "delete" ? policies?.softDelete?.(tableName) : void 0;
4053
+ const canHardDelete = softDelete?.hardDelete === true;
3847
4054
  const queryArgs = {
3848
4055
  ...setArgs ? { set: { type: new graphql.GraphQLNonNull(setArgs) } } : {},
3849
- where: { type: requireWhere ? new graphql.GraphQLNonNull(filterArgs) : filterArgs }
4056
+ where: { type: requireWhere ? new graphql.GraphQLNonNull(filterArgs) : filterArgs },
4057
+ ...canHardDelete ? { hard: hardDeleteArg } : {}
3850
4058
  };
3851
- const errorCtx = {
3852
- table: tableName,
4059
+ const readsRows = returnsRows && !!policies?.onWrite?.(tableName, kind)?.after;
4060
+ const allColumns = (0, drizzle_orm.getColumns)(table);
4061
+ const keyColumns = pkNames.length ? Object.fromEntries(pkNames.map((name) => [name, allColumns[name]])) : allColumns;
4062
+ return writeResolver({
4063
+ db,
4064
+ tableName,
3853
4065
  operation: kind,
3854
- field: fieldName
3855
- };
3856
- return {
3857
- name: fieldName,
3858
- resolver: async (_source, args, context, info) => {
3859
- try {
3860
- return await runMutation(db, context, info, txCtx, async (executor) => {
3861
- const { where, set } = args;
3862
- const filters = scopedWhere({
3863
- scope: void 0,
3864
- tableName,
3865
- table,
3866
- where,
3867
- relationCtx: relationFilterCtx(filterCtx, tableName),
3868
- required: requireWhere
3869
- });
3870
- let query;
3871
- if (kind === "delete") query = executor.delete(table);
3872
- else {
3873
- const entry = nested?.enabled(tableName) ? nested.split(tableName, set) : void 0;
3874
- if (entry && nested.hasOps(entry.ops)) throw drizzleError("This mutation does not support nested writes — use the one that returns the rows instead.", { code: "DRIZZLE_NESTED_WRITES_UNSUPPORTED" });
3875
- const values = remapUpdateInput(entry ? entry.columns : set, table, tableName);
3876
- if (!Object.keys(values).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
3877
- query = executor.update(table).set(values);
3878
- }
3879
- if (filters) query = query.where(filters);
3880
- return rowsAffected(await query);
3881
- });
3882
- } catch (e) {
3883
- throw withErrorContext(toGraphQLError(e), errorCtx);
4066
+ single: false,
4067
+ fieldName,
4068
+ txCtx,
4069
+ policies,
4070
+ args: queryArgs,
4071
+ run: async ({ executor, args, context, before, after, returning }) => {
4072
+ const { where, set } = args;
4073
+ const hard = canHardDelete && args.hard === true;
4074
+ const scope = policies?.scope?.(context);
4075
+ await before();
4076
+ let query;
4077
+ if (kind === "delete") query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: softDelete.writeDeleted() }) : executor.delete(table);
4078
+ else {
4079
+ const entry = nested?.enabled(tableName) ? nested.split(tableName, set) : void 0;
4080
+ if (entry && nested.hasOps(entry.ops)) throw drizzleError("This mutation does not support nested writes — use the one that returns the rows instead.", { code: "DRIZZLE_NESTED_WRITES_UNSUPPORTED" });
4081
+ const values = stripContextValues(remapUpdateInput(entry ? entry.columns : set, table, tableName), policies?.contextValues?.(tableName));
4082
+ if (!Object.keys(values).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
4083
+ query = executor.update(table).set(values);
3884
4084
  }
3885
- },
3886
- args: queryArgs
3887
- };
4085
+ const filters = scopedWhere({
4086
+ scope,
4087
+ tableName,
4088
+ table,
4089
+ where,
4090
+ relationCtx: relationFilterCtx(filterCtx, tableName),
4091
+ required: requireWhere,
4092
+ deleted: hard ? "INCLUDE" : void 0
4093
+ });
4094
+ if (filters) query = query.where(filters);
4095
+ if (!readsRows) {
4096
+ const result = await query;
4097
+ await after();
4098
+ return rowsAffected(result);
4099
+ }
4100
+ const named = returning({});
4101
+ const rows = await query.returning(Object.keys(named).length ? named : keyColumns);
4102
+ await after(rows);
4103
+ return rows.length;
4104
+ }
4105
+ });
3888
4106
  };
3889
4107
  //#endregion
3890
4108
  //#region src/util/batch-loader/index.ts
@@ -4822,84 +5040,6 @@ const generateTableTypes = (tableName, tables, relationMap, withReturning, relat
4822
5040
  };
4823
5041
  };
4824
5042
  //#endregion
4825
- //#region src/util/builders/common/write-hooks.ts
4826
- /**
4827
- * Normalizes the two registration shapes: a bare function is the `after` hook — the position
4828
- * that has rows, and the one the common audit/outbox case wants — while `{ before, after }`
4829
- * names them explicitly.
4830
- */
4831
- const normalizeWriteHooks = (hooks) => {
4832
- if (!hooks) return;
4833
- if (typeof hooks === "function") return { after: hooks };
4834
- const resolved = {
4835
- before: hooks.before,
4836
- after: hooks.after
4837
- };
4838
- return resolved.before || resolved.after ? resolved : void 0;
4839
- };
4840
- /**
4841
- * Invokes one position of a field's hooks. Returns `undefined` rather than a resolved promise
4842
- * when nothing is registered, so the awaiting call site costs nothing on an unhooked build.
4843
- */
4844
- const runWriteHook = (hooks, position, payload) => {
4845
- const hook = position === "before" ? hooks?.before : hooks?.after;
4846
- if (!hook) return;
4847
- return Promise.resolve(hook({
4848
- ...payload,
4849
- position,
4850
- rows: payload.rows ?? []
4851
- }));
4852
- };
4853
- //#endregion
4854
- //#region src/util/builders/common/write-scaffold.ts
4855
- /**
4856
- * Wraps one mutation body in the scaffolding above and returns the field it becomes.
4857
- *
4858
- * A field carrying a hook is forced into a transaction even when it would have run straight
4859
- * on `db`: a hook exists to write atomically with the mutation, and one that ran outside the
4860
- * transaction could not roll anything back.
4861
- */
4862
- const writeResolver = ({ db, tableName, operation, single, fieldName, args: queryArgs, txCtx, policies, run }) => {
4863
- const hooks = policies?.onWrite?.(tableName, operation);
4864
- const errorCtx = {
4865
- table: tableName,
4866
- operation,
4867
- field: fieldName
4868
- };
4869
- return {
4870
- name: fieldName,
4871
- resolver: async (_source, args, context, info) => {
4872
- try {
4873
- return await runMutation(db, context, info, txCtx, (executor) => {
4874
- const payload = {
4875
- table: tableName,
4876
- operation,
4877
- single,
4878
- args,
4879
- context,
4880
- info,
4881
- tx: executor
4882
- };
4883
- return run({
4884
- executor,
4885
- args,
4886
- context,
4887
- info,
4888
- before: () => runWriteHook(hooks, "before", payload),
4889
- after: (rows) => runWriteHook(hooks, "after", {
4890
- ...payload,
4891
- rows: rows ?? []
4892
- })
4893
- });
4894
- }, !!hooks);
4895
- } catch (e) {
4896
- throw withErrorContext(toGraphQLError(e), errorCtx);
4897
- }
4898
- },
4899
- args: queryArgs
4900
- };
4901
- };
4902
- //#endregion
4903
5043
  //#region src/util/features.ts
4904
5044
  /**
4905
5045
  * What a table generates when the config says nothing. Everything is on except the opt-in
@@ -5063,19 +5203,18 @@ const resolveBuildConfig = (config, schema) => {
5063
5203
  }
5064
5204
  const onWriteConfig = config?.onWrite;
5065
5205
  const writeHooks = /* @__PURE__ */ new Map();
5066
- let globalWriteHooks;
5067
5206
  if (onWriteConfig) {
5068
- if (typeof onWriteConfig === "function" || typeof onWriteConfig.before === "function" || typeof onWriteConfig.after === "function") globalWriteHooks = normalizeWriteHooks(onWriteConfig);
5207
+ 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
5208
  else {
5070
5209
  const tableNames = new Set(tableKeys);
5071
5210
  for (const [tableName, hooks] of Object.entries(onWriteConfig)) {
5072
5211
  if (!tableNames.has(tableName)) throw new Error(`Drizzle-GraphQL Error: config.onWrite names '${tableName}', which is not a table in the Drizzle schema.`);
5073
5212
  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));
5213
+ writeHooks.set(tableName, normalizeWriteHooks(hooks, (0, drizzle_orm.getColumns)(schema[tableName]), `config.onWrite.${tableName}`));
5075
5214
  }
5076
5215
  }
5077
5216
  }
5078
- const onWrite = globalWriteHooks || writeHooks.size ? (tableName) => globalWriteHooks ?? writeHooks.get(tableName) : void 0;
5217
+ const onWrite = writeHooks.size ? (tableName) => writeHooks.get(tableName) : void 0;
5079
5218
  const policies = scopeConfig || contextValuesConfig || softDeleteInfos.size || onWrite || defaultOrderBy ? {
5080
5219
  scope: scopeConfig ? (tableName) => scopeConfig[tableName] : void 0,
5081
5220
  contextValues: contextValuesConfig ? (tableName) => contextValuesConfig[tableName] : void 0,
@@ -5141,6 +5280,7 @@ const resolveBuildConfig = (config, schema) => {
5141
5280
  scalars: config?.scalars,
5142
5281
  mapColumnType: config?.mapColumnType,
5143
5282
  enumNameMapper: config?.enumNameMapper,
5283
+ enumValueNameMapper: config?.enumValueNameMapper,
5144
5284
  transactions,
5145
5285
  limits,
5146
5286
  policies,
@@ -6901,7 +7041,9 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
6901
7041
  fieldName: updateCountFieldName,
6902
7042
  requireWhere: tableFeatures.requireWhere,
6903
7043
  filterCtx,
6904
- txCtx: mutationTxCtx
7044
+ txCtx: mutationTxCtx,
7045
+ policies,
7046
+ returnsRows: false
6905
7047
  }) : void 0;
6906
7048
  const deleteCountGenerated = tableFeatures.delete && tableFeatures.countMutations ? generateWriteCount({
6907
7049
  db,
@@ -6912,7 +7054,9 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
6912
7054
  fieldName: deleteCountFieldName,
6913
7055
  requireWhere: tableFeatures.requireWhere,
6914
7056
  filterCtx,
6915
- txCtx: mutationTxCtx
7057
+ txCtx: mutationTxCtx,
7058
+ policies,
7059
+ returnsRows: false
6916
7060
  }) : void 0;
6917
7061
  const hardDeleteMeta = softDeleteInfo?.hardDelete ? { hardDelete: true } : {};
6918
7062
  const withMeta = (generated, meta) => ({
@@ -6982,12 +7126,25 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
6982
7126
  {
6983
7127
  generated: updateCountGenerated,
6984
7128
  type: new graphql.GraphQLNonNull(graphql.GraphQLInt),
7129
+ meta: {
7130
+ operation: "update",
7131
+ single: false,
7132
+ targetArg: "where",
7133
+ count: true
7134
+ },
6985
7135
  description: "How many rows the update touched. The rows themselves are not read back, which is the point of this mutation."
6986
7136
  },
6987
7137
  {
6988
7138
  generated: deleteCountGenerated,
6989
7139
  type: new graphql.GraphQLNonNull(graphql.GraphQLInt),
6990
- description: "How many rows the delete removed. The rows themselves are not read back, which is the point of this mutation."
7140
+ meta: {
7141
+ operation: "delete",
7142
+ single: false,
7143
+ targetArg: "where",
7144
+ count: true,
7145
+ ...hardDeleteMeta
7146
+ },
7147
+ description: "How many rows the delete removed — or, on a soft-deleting table, marked deleted. The rows themselves are not read back, which is the point of this mutation."
6991
7148
  }
6992
7149
  ],
6993
7150
  onConflictInput,
@@ -7024,7 +7181,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7024
7181
  txCtx,
7025
7182
  policies,
7026
7183
  args: queryArgs,
7027
- run: async ({ executor, args, context, info, before, after }) => {
7184
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7028
7185
  const supplied = single ? [args.values] : args.values;
7029
7186
  if (!supplied.length) throw drizzleError("No values were provided!", { code: "DRIZZLE_NO_VALUES" });
7030
7187
  await before();
@@ -7046,13 +7203,13 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7046
7203
  scope,
7047
7204
  defaultOrderBy: policies?.defaultOrderBy
7048
7205
  });
7049
- const returning = nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns;
7206
+ const returning = withHookReturning(nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns);
7050
7207
  const runInsert = async (target, values) => {
7051
7208
  let query = target.insert(table).values(values).returning(returning);
7052
7209
  if (conflictDoNothing) query = query.onConflictDoNothing();
7053
7210
  return await query;
7054
7211
  };
7055
- const result = nestedEntries ? await writeWithNestedOps({
7212
+ const written = nestedEntries ? await writeWithNestedOps({
7056
7213
  executor,
7057
7214
  runtime: nested,
7058
7215
  tableName,
@@ -7061,7 +7218,8 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7061
7218
  write: (tx, values) => runInsert(tx, [values]),
7062
7219
  context
7063
7220
  }) : await runInsert(executor, input);
7064
- await after(result);
7221
+ await after(written);
7222
+ const result = output(written);
7065
7223
  if (single && !result[0]) {
7066
7224
  if (!conflictDoNothing) throw drizzleError("The insert returned no row.", { code: "DRIZZLE_NO_ROW_RETURNED" });
7067
7225
  return;
@@ -7097,7 +7255,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7097
7255
  txCtx,
7098
7256
  policies,
7099
7257
  args: queryArgs,
7100
- run: async ({ executor, args, context, info, before, after }) => {
7258
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7101
7259
  const supplied = single ? [args.values] : args.values;
7102
7260
  if (!supplied.length) throw drizzleError("No values were provided!", { code: "DRIZZLE_NO_VALUES" });
7103
7261
  await before();
@@ -7119,7 +7277,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7119
7277
  scope,
7120
7278
  defaultOrderBy: policies?.defaultOrderBy
7121
7279
  });
7122
- const returning = nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns;
7280
+ const returning = withHookReturning(nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns);
7123
7281
  const runUpsert = async (target, values) => {
7124
7282
  const plan = resolveConflictPlan({
7125
7283
  table,
@@ -7140,7 +7298,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7140
7298
  });
7141
7299
  return await query;
7142
7300
  };
7143
- const result = nestedEntries ? await writeWithNestedOps({
7301
+ const written = nestedEntries ? await writeWithNestedOps({
7144
7302
  executor,
7145
7303
  runtime: nested,
7146
7304
  tableName,
@@ -7149,7 +7307,8 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7149
7307
  write: (tx, values) => runUpsert(tx, [values]),
7150
7308
  context
7151
7309
  }) : await runUpsert(executor, input);
7152
- await after(result);
7310
+ await after(written);
7311
+ const result = output(written);
7153
7312
  if (single && !result[0]) return;
7154
7313
  const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
7155
7314
  return single ? remapToGraphQLSingleOutput(enriched[0], tableName, table, relationMap) : remapToGraphQLArrayOutput(enriched, tableName, table, relationMap);
@@ -7173,7 +7332,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7173
7332
  txCtx,
7174
7333
  policies,
7175
7334
  args: queryArgs,
7176
- run: async ({ executor, args, context, info, before, after }) => {
7335
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7177
7336
  const { where, set } = args;
7178
7337
  const scope = policies?.scope?.(context);
7179
7338
  await before();
@@ -7204,14 +7363,14 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7204
7363
  required: single || requireWhere
7205
7364
  });
7206
7365
  if (single) await assertSingleMatch(executor, table, filters);
7207
- const returning = nestedOps ? nested.withJoinColumns(tableName, nestedOps, { ...columns }, table) : columns;
7366
+ const returning = withHookReturning(nestedOps ? nested.withJoinColumns(tableName, nestedOps, { ...columns }, table) : columns);
7208
7367
  const runUpdate = async (target, values) => {
7209
7368
  const writes = Object.keys(values).length > 0;
7210
7369
  let query = writes ? target.update(table).set(values) : target.select(returning).from(table);
7211
7370
  if (filters) query = query.where(filters);
7212
7371
  return await (writes ? query.returning(returning) : query);
7213
7372
  };
7214
- const result = nestedOps ? await updateWithNestedOps({
7373
+ const written = nestedOps ? await updateWithNestedOps({
7215
7374
  executor,
7216
7375
  runtime: nested,
7217
7376
  tableName,
@@ -7221,7 +7380,8 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7221
7380
  write: runUpdate,
7222
7381
  context
7223
7382
  }) : await runUpdate(executor, input);
7224
- await after(result);
7383
+ await after(written);
7384
+ const result = output(written);
7225
7385
  if (single && result.length > 1) throw drizzleError("'where' matched more than one row!", { code: "DRIZZLE_MULTI_ROW_MATCH" });
7226
7386
  if (single && !result[0]) return;
7227
7387
  const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
@@ -7261,7 +7421,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7261
7421
  txCtx,
7262
7422
  policies,
7263
7423
  args: queryArgs,
7264
- run: async ({ executor, args, context, info, before, after }) => {
7424
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7265
7425
  const { where } = args;
7266
7426
  const hard = canHardDelete && args.hard === true;
7267
7427
  const scope = policies?.scope?.(context);
@@ -7281,9 +7441,10 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7281
7441
  if (single) await assertSingleMatch(executor, table, filters);
7282
7442
  let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
7283
7443
  if (filters) query = query.where(filters);
7284
- query = query.returning(columns);
7285
- const result = await query;
7286
- await after(result);
7444
+ query = query.returning(withHookReturning(columns));
7445
+ const written = await query;
7446
+ await after(written);
7447
+ const result = output(written);
7287
7448
  if (single && result.length > 1) throw drizzleError("'where' matched more than one row!", { code: "DRIZZLE_MULTI_ROW_MATCH" });
7288
7449
  if (single) return result[0] ? remapToGraphQLSingleOutput(result[0], tableName, table) : void 0;
7289
7450
  return remapToGraphQLArrayOutput(result, tableName, table);
@@ -7484,7 +7645,10 @@ const createSchemaDataGenerator = (adapter) => {
7484
7645
  requireWhere: tableFeatures.requireWhere,
7485
7646
  filterCtx,
7486
7647
  txCtx: mutationTxCtx,
7487
- nested: nestedRuntime
7648
+ nested: nestedRuntime,
7649
+ policies,
7650
+ pkNames: primaryKeyPropNames(schema[tableName]),
7651
+ returnsRows: true
7488
7652
  }) : void 0;
7489
7653
  const deleteCountGenerated = tableFeatures.delete && tableFeatures.countMutations ? generateWriteCount({
7490
7654
  db,
@@ -7495,7 +7659,10 @@ const createSchemaDataGenerator = (adapter) => {
7495
7659
  fieldName: deleteCountFieldName,
7496
7660
  requireWhere: tableFeatures.requireWhere,
7497
7661
  filterCtx,
7498
- txCtx: mutationTxCtx
7662
+ txCtx: mutationTxCtx,
7663
+ policies,
7664
+ pkNames: primaryKeyPropNames(schema[tableName]),
7665
+ returnsRows: true
7499
7666
  }) : void 0;
7500
7667
  const hardDeleteMeta = softDeleteInfo?.hardDelete ? { hardDelete: true } : {};
7501
7668
  return {
@@ -7605,12 +7772,25 @@ const createSchemaDataGenerator = (adapter) => {
7605
7772
  {
7606
7773
  generated: updateCountGenerated,
7607
7774
  type: new graphql.GraphQLNonNull(graphql.GraphQLInt),
7775
+ meta: {
7776
+ operation: "update",
7777
+ single: false,
7778
+ targetArg: "where",
7779
+ count: true
7780
+ },
7608
7781
  description: "How many rows the update touched. The rows themselves are not read back, which is the point of this mutation."
7609
7782
  },
7610
7783
  {
7611
7784
  generated: deleteCountGenerated,
7612
7785
  type: new graphql.GraphQLNonNull(graphql.GraphQLInt),
7613
- description: "How many rows the delete removed. The rows themselves are not read back, which is the point of this mutation."
7786
+ meta: {
7787
+ operation: "delete",
7788
+ single: false,
7789
+ targetArg: "where",
7790
+ count: true,
7791
+ ...hardDeleteMeta
7792
+ },
7793
+ description: "How many rows the delete removed — or, on a soft-deleting table, marked deleted. The rows themselves are not read back, which is the point of this mutation."
7614
7794
  }
7615
7795
  ],
7616
7796
  onConflictInput,
@@ -7672,7 +7852,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7672
7852
  txCtx,
7673
7853
  policies,
7674
7854
  args: queryArgs,
7675
- run: async ({ executor, args, context, info, before, after }) => {
7855
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output: withoutHookColumns }) => {
7676
7856
  const { updates } = args;
7677
7857
  if (!updates.length) throw drizzleError("No updates were provided!", { code: "DRIZZLE_NO_VALUES" });
7678
7858
  await before();
@@ -7703,7 +7883,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7703
7883
  };
7704
7884
  });
7705
7885
  const anyNested = entries.some((entry) => entry.ops);
7706
- const returning = anyNested ? nested.withJoinColumns(tableName, mergedOps(entries.map((entry) => ({ ops: entry.ops ?? {} }))), { ...columns }, table) : columns;
7886
+ const returning = withHookReturning(anyNested ? nested.withJoinColumns(tableName, mergedOps(entries.map((entry) => ({ ops: entry.ops ?? {} }))), { ...columns }, table) : columns);
7707
7887
  const perEntry = await runBatch({
7708
7888
  executor,
7709
7889
  entries,
@@ -7714,8 +7894,9 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7714
7894
  tableName,
7715
7895
  context
7716
7896
  });
7717
- const flatRows = perEntry.flat();
7718
- await after(flatRows);
7897
+ const written = perEntry.flat();
7898
+ await after(written);
7899
+ const flatRows = withoutHookColumns(written);
7719
7900
  const enriched = await withEagerRelations(executor, tableName, flatRows, pkNames, withParams, hasRelations);
7720
7901
  const output = [];
7721
7902
  let offset = 0;