@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.js CHANGED
@@ -165,6 +165,14 @@ const GraphQLDecimalString = new GraphQLScalarType({
165
165
  const uncapitalize = (input) => input?.length ? `${input[0].toLocaleLowerCase()}${input.length > 1 ? input.slice(1, input.length) : ""}` : input;
166
166
  const capitalize = (input) => input?.length ? `${input[0].toLocaleUpperCase()}${input.length > 1 ? input.slice(1, input.length) : ""}` : input;
167
167
  /**
168
+ * `inProgress` / `in-progress` / `in progress` / `InProgress` → `IN_PROGRESS`; `HTTPError` →
169
+ * `HTTP_ERROR`. Words are split at non-alphanumeric runs and at lower→upper and
170
+ * acronym→word boundaries, then upper-cased and joined with `_`.
171
+ *
172
+ * The preset behind `enumValueNameMapper: 'constant'`.
173
+ */
174
+ 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("_");
175
+ /**
168
176
  * The `typeNameMapper` preset behind `typeNameMapper: 'singularize'`.
169
177
  *
170
178
  * Derives the singular and plural forms of a table key with the library's own `pluralize`,
@@ -185,6 +193,8 @@ const singularizeMapper = (tableName) => ({
185
193
  //#endregion
186
194
  //#region src/util/type-converter/index.ts
187
195
  const allowedNameChars = /^[a-zA-Z0-9_]+$/;
196
+ /** A GraphQL `Name` that is also allowed as an enum value — the spec reserves the three literals. */
197
+ const validEnumValueName = (name) => /^[_a-zA-Z][_a-zA-Z0-9]*$/.test(name) && name !== "true" && name !== "false" && name !== "null";
188
198
  /**
189
199
  * The enum type a column was declared with, when it is a *named* one — `pgEnum('status', …)`
190
200
  * shared by however many tables reference it. An inline value list (`text({ enum: [...] })`,
@@ -205,20 +215,35 @@ const pascalize = (input) => input.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((p
205
215
  */
206
216
  const enumValuesKey = (values) => JSON.stringify(values);
207
217
  /**
218
+ * Identity of the GraphQL names those values were given. Kept apart from {@link enumValuesKey}
219
+ * because `enumValueNameMapper` sees the column, so two columns reaching one type can name its
220
+ * values differently — and only one set of names can win.
221
+ */
222
+ const enumValueNamesKey = (names) => JSON.stringify(names);
223
+ /**
208
224
  * Every GraphQL enum name this build has handed out. Two columns that resolve to the same
209
225
  * name share one type — that is how the columns of one `pgEnum` end up on a single type —
210
226
  * so the values are kept alongside it to tell a genuine collision from a re-use.
211
227
  */
212
228
  let enumNamesInUse = /* @__PURE__ */ new Map();
213
229
  let enumNameMapper;
230
+ let enumValueNameMapper;
231
+ /** The value-name presets, by the string a config passes for them. */
232
+ const enumValueNamePresets = { constant: ({ value }) => constantCase(value) };
214
233
  /**
215
234
  * Resets the enum registry for a fresh schema build and installs that build's
216
- * `enumNameMapper`. Called once per `generateSchemaData`, alongside
235
+ * `enumNameMapper` and `enumValueNameMapper`. Called once per `generateSchemaData`, alongside
217
236
  * {@link registerScalarOverrides} — without the reset a second build would reuse the first
218
237
  * build's enum types, and so its naming decisions.
219
238
  */
220
239
  const registerEnumConfig = (config) => {
221
240
  enumNameMapper = config.enumNameMapper;
241
+ const valueMapper = config.enumValueNameMapper;
242
+ if (typeof valueMapper === "string") {
243
+ const preset = enumValueNamePresets[valueMapper];
244
+ if (!preset) throw new Error(`Drizzle-GraphQL Error: Unknown enumValueNameMapper preset '${valueMapper}'. The only preset is 'constant'; pass a function for anything else.`);
245
+ enumValueNameMapper = preset;
246
+ } else enumValueNameMapper = valueMapper;
222
247
  enumNamesInUse = /* @__PURE__ */ new Map();
223
248
  };
224
249
  /**
@@ -247,22 +272,50 @@ const generateEnumCached = (column, columnName, tableName) => {
247
272
  columnName,
248
273
  values
249
274
  }) ?? (declared ? `${pascalize(declared.enumName)}Enum` : `${capitalize(tableName)}${capitalize(columnName)}Enum`);
275
+ const valueNames = values.map((value, index) => {
276
+ const mapped = enumValueNameMapper?.({
277
+ enumName: declared?.enumName,
278
+ schema: declared?.schema,
279
+ tableName,
280
+ columnName,
281
+ values,
282
+ value,
283
+ index,
284
+ typeName: name
285
+ });
286
+ if (mapped === void 0) return allowedNameChars.test(value) ? value : `Option${index}`;
287
+ 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.`);
288
+ return mapped;
289
+ });
290
+ const seen = /* @__PURE__ */ new Map();
291
+ values.forEach((value, index) => {
292
+ const valueName = valueNames[index];
293
+ const earlier = seen.get(valueName);
294
+ 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.`);
295
+ seen.set(valueName, value);
296
+ });
250
297
  const valuesKey = enumValuesKey(values);
298
+ const namesKey = enumValueNamesKey(valueNames);
251
299
  const claimed = enumNamesInUse.get(name);
252
300
  if (claimed) {
253
301
  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.`);
302
+ if (claimed.namesKey !== namesKey) {
303
+ const before = claimed.type.getValues().map((v) => v.name).join(", ");
304
+ 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.`);
305
+ }
254
306
  return claimed.type;
255
307
  }
256
308
  const gqlEnum = new GraphQLEnumType({
257
309
  name,
258
- values: Object.fromEntries(values.map((e, index) => [allowedNameChars.test(e) ? e : `Option${index}`, {
310
+ values: Object.fromEntries(values.map((e, index) => [valueNames[index], {
259
311
  value: e,
260
312
  description: `Value: ${e}`
261
313
  }]))
262
314
  });
263
315
  enumNamesInUse.set(name, {
264
316
  type: gqlEnum,
265
- valuesKey
317
+ valuesKey,
318
+ namesKey
266
319
  });
267
320
  return gqlEnum;
268
321
  };
@@ -1017,6 +1070,7 @@ const remapToGraphQLArrayOutput = (queryOutput, tableName, table, relationMap) =
1017
1070
  return queryOutput;
1018
1071
  };
1019
1072
  const remapFromGraphQLCore = (value, column, columnName) => {
1073
+ if (value === null) return null;
1020
1074
  if (getColumnScalarOverride(column, true)) {
1021
1075
  if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
1022
1076
  return value;
@@ -3670,6 +3724,145 @@ const runMutation = async (db, context, info, txCtx, body, forceTx) => {
3670
3724
  return runOnSharedTx(state, body);
3671
3725
  };
3672
3726
  //#endregion
3727
+ //#region src/util/builders/common/write-hooks.ts
3728
+ /**
3729
+ * Normalizes the two registration shapes: a bare function is the `after` hook — the position
3730
+ * that has rows, and the one the common audit/outbox case wants — while `{ before, after }`
3731
+ * names them explicitly.
3732
+ */
3733
+ const normalizeWriteHooks = (hooks, tableColumns, where) => {
3734
+ if (!hooks) return;
3735
+ if (typeof hooks === "function") return { after: hooks };
3736
+ const resolved = {
3737
+ before: hooks.before,
3738
+ after: hooks.after
3739
+ };
3740
+ if (hooks.columns !== void 0) resolved.columns = resolveHookColumns(hooks.columns, !!hooks.after, tableColumns, where);
3741
+ return resolved.before || resolved.after ? resolved : void 0;
3742
+ };
3743
+ /**
3744
+ * Resolves a hook's `columns` against the table it runs on. `where` names the registration
3745
+ * (`config.onWrite.posts`) and, for a top-level one, the table, so the error says which.
3746
+ */
3747
+ const resolveHookColumns = (spec, hasAfter, tableColumns, where) => {
3748
+ if (!hasAfter) throw new Error(`Drizzle-GraphQL Error: ${where}.columns only applies to an 'after' hook, and none is registered.`);
3749
+ if (spec === "all") return { ...tableColumns };
3750
+ if (!Array.isArray(spec)) throw new Error(`Drizzle-GraphQL Error: ${where}.columns must be an array of column names or 'all'.`);
3751
+ const resolved = {};
3752
+ for (const name of spec) {
3753
+ const column = typeof name === "string" && Object.keys(tableColumns).includes(name) ? tableColumns[name] : void 0;
3754
+ if (!column) throw new Error(`Drizzle-GraphQL Error: ${where}.columns names '${String(name)}', which is not a column of that table.`);
3755
+ resolved[name] = column;
3756
+ }
3757
+ return resolved;
3758
+ };
3759
+ /**
3760
+ * Adds a hook's `columns` to a RETURNING column map, and reports which keys it added — the
3761
+ * ones the client never asked for, which have to come back off before the rows are mapped.
3762
+ * Returns the map untouched, and no keys, when the hook named none or the selection already
3763
+ * covers them.
3764
+ */
3765
+ const withHookColumns = (hooks, columns) => {
3766
+ const hookColumns = hooks?.columns;
3767
+ if (!hookColumns) return {
3768
+ returning: columns,
3769
+ added: void 0
3770
+ };
3771
+ const selected = new Set(Object.keys(columns));
3772
+ const added = Object.keys(hookColumns).filter((key) => !selected.has(key));
3773
+ if (!added.length) return {
3774
+ returning: columns,
3775
+ added: void 0
3776
+ };
3777
+ const returning = { ...columns };
3778
+ for (const key of added) returning[key] = hookColumns[key];
3779
+ return {
3780
+ returning,
3781
+ added
3782
+ };
3783
+ };
3784
+ /**
3785
+ * The rows with the hook-only keys taken off, as fresh objects: the hook was handed the
3786
+ * originals and may still hold them, so they are not mutated under it.
3787
+ */
3788
+ const omitHookColumns = (rows, added) => {
3789
+ if (!added) return rows;
3790
+ return rows.map((row) => {
3791
+ const copy = { ...row };
3792
+ for (const key of added) delete copy[key];
3793
+ return copy;
3794
+ });
3795
+ };
3796
+ /**
3797
+ * Invokes one position of a field's hooks. Returns `undefined` rather than a resolved promise
3798
+ * when nothing is registered, so the awaiting call site costs nothing on an unhooked build.
3799
+ */
3800
+ const runWriteHook = (hooks, position, payload) => {
3801
+ const hook = position === "before" ? hooks?.before : hooks?.after;
3802
+ if (!hook) return;
3803
+ return Promise.resolve(hook({
3804
+ ...payload,
3805
+ position,
3806
+ rows: payload.rows ?? []
3807
+ }));
3808
+ };
3809
+ //#endregion
3810
+ //#region src/util/builders/common/write-scaffold.ts
3811
+ /**
3812
+ * Wraps one mutation body in the scaffolding above and returns the field it becomes.
3813
+ *
3814
+ * A field carrying a hook is forced into a transaction even when it would have run straight
3815
+ * on `db`: a hook exists to write atomically with the mutation, and one that ran outside the
3816
+ * transaction could not roll anything back.
3817
+ */
3818
+ const writeResolver = ({ db, tableName, operation, single, fieldName, args: queryArgs, txCtx, policies, run }) => {
3819
+ const hooks = policies?.onWrite?.(tableName, operation);
3820
+ const errorCtx = {
3821
+ table: tableName,
3822
+ operation,
3823
+ field: fieldName
3824
+ };
3825
+ return {
3826
+ name: fieldName,
3827
+ resolver: async (_source, args, context, info) => {
3828
+ try {
3829
+ return await runMutation(db, context, info, txCtx, (executor) => {
3830
+ const payload = {
3831
+ table: tableName,
3832
+ operation,
3833
+ single,
3834
+ args,
3835
+ context,
3836
+ info,
3837
+ tx: executor
3838
+ };
3839
+ let added;
3840
+ return run({
3841
+ executor,
3842
+ args,
3843
+ context,
3844
+ info,
3845
+ before: () => runWriteHook(hooks, "before", payload),
3846
+ after: (rows) => runWriteHook(hooks, "after", {
3847
+ ...payload,
3848
+ rows: rows ?? []
3849
+ }),
3850
+ returning: (columns) => {
3851
+ const withHook = withHookColumns(hooks, columns);
3852
+ added = withHook.added;
3853
+ return withHook.returning;
3854
+ },
3855
+ output: (rows) => omitHookColumns(rows, added)
3856
+ });
3857
+ }, !!hooks);
3858
+ } catch (e) {
3859
+ throw withErrorContext(toGraphQLError(e), errorCtx);
3860
+ }
3861
+ },
3862
+ args: queryArgs
3863
+ };
3864
+ };
3865
+ //#endregion
3673
3866
  //#region src/util/builders/common/mutation-helpers.ts
3674
3867
  /**
3675
3868
  * The `hard` argument on the delete mutations of a soft-deleting table that opted into
@@ -3813,54 +4006,79 @@ const assertSingleMatch = async (executor, table, filters) => {
3813
4006
  * The plural `update` and `delete` always `RETURNING *`, which is the wrong shape for
3814
4007
  * "archive everything older than a year": the caller pays to ship rows it does not want, and
3815
4008
  * on a large match that cost is the whole mutation. These return `Int!` instead — how many
3816
- * rows the write touched, as the driver reported it, with no RETURNING clause at all.
4009
+ * rows the write touched.
3817
4010
  *
3818
- * Dialect-independent: drizzle's update/delete builders take the same calls everywhere, and
3819
- * the drivers' disagreement about how to spell the row count is handled by
3820
- * {@link rowsAffected}.
3821
- */
3822
- const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested }) => {
4011
+ * Leaving the payload off changes nothing else: the write obeys every rule of the plural
4012
+ * mutation it mirrors, through the same {@link writeResolver} scaffolding.
4013
+ *
4014
+ * - `scope` (and the soft-delete marker) is ANDed into the `where`, so only rows the plural
4015
+ * write could reach are counted and written.
4016
+ * - On a soft-deleting table the delete is the marking `UPDATE`, not a `DELETE`, and rows that
4017
+ * are already marked are not matched; the update skips marked rows too. `hard: true` exists
4018
+ * exactly where the plural delete takes it.
4019
+ * - Context-derived columns are stripped from the update's `set`, as the plural update does.
4020
+ * - `onWrite` hooks run `before` and `after`, inside a transaction. The `after` hook is handed
4021
+ * rows, so a table with one pays for a `RETURNING` — of the columns the hook names in
4022
+ * `columns`, or of the primary key when it names none (the whole row on a table without a
4023
+ * key) — and the count is the number of rows returned. Every other write, and every write on
4024
+ * MySQL (no `RETURNING`; its hooks see `rows: []` as on every MySQL mutation), skips the
4025
+ * `RETURNING` and reads the count off the driver's result via {@link rowsAffected}.
4026
+ */
4027
+ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested, policies, pkNames = [], returnsRows }) => {
4028
+ const softDelete = kind === "delete" ? policies?.softDelete?.(tableName) : void 0;
4029
+ const canHardDelete = softDelete?.hardDelete === true;
3823
4030
  const queryArgs = {
3824
4031
  ...setArgs ? { set: { type: new GraphQLNonNull(setArgs) } } : {},
3825
- where: { type: requireWhere ? new GraphQLNonNull(filterArgs) : filterArgs }
4032
+ where: { type: requireWhere ? new GraphQLNonNull(filterArgs) : filterArgs },
4033
+ ...canHardDelete ? { hard: hardDeleteArg } : {}
3826
4034
  };
3827
- const errorCtx = {
3828
- table: tableName,
4035
+ const readsRows = returnsRows && !!policies?.onWrite?.(tableName, kind)?.after;
4036
+ const allColumns = getColumns(table);
4037
+ const keyColumns = pkNames.length ? Object.fromEntries(pkNames.map((name) => [name, allColumns[name]])) : allColumns;
4038
+ return writeResolver({
4039
+ db,
4040
+ tableName,
3829
4041
  operation: kind,
3830
- field: fieldName
3831
- };
3832
- return {
3833
- name: fieldName,
3834
- resolver: async (_source, args, context, info) => {
3835
- try {
3836
- return await runMutation(db, context, info, txCtx, async (executor) => {
3837
- const { where, set } = args;
3838
- const filters = scopedWhere({
3839
- scope: void 0,
3840
- tableName,
3841
- table,
3842
- where,
3843
- relationCtx: relationFilterCtx(filterCtx, tableName),
3844
- required: requireWhere
3845
- });
3846
- let query;
3847
- if (kind === "delete") query = executor.delete(table);
3848
- else {
3849
- const entry = nested?.enabled(tableName) ? nested.split(tableName, set) : void 0;
3850
- 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" });
3851
- const values = remapUpdateInput(entry ? entry.columns : set, table, tableName);
3852
- if (!Object.keys(values).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
3853
- query = executor.update(table).set(values);
3854
- }
3855
- if (filters) query = query.where(filters);
3856
- return rowsAffected(await query);
3857
- });
3858
- } catch (e) {
3859
- throw withErrorContext(toGraphQLError(e), errorCtx);
4042
+ single: false,
4043
+ fieldName,
4044
+ txCtx,
4045
+ policies,
4046
+ args: queryArgs,
4047
+ run: async ({ executor, args, context, before, after, returning }) => {
4048
+ const { where, set } = args;
4049
+ const hard = canHardDelete && args.hard === true;
4050
+ const scope = policies?.scope?.(context);
4051
+ await before();
4052
+ let query;
4053
+ if (kind === "delete") query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: softDelete.writeDeleted() }) : executor.delete(table);
4054
+ else {
4055
+ const entry = nested?.enabled(tableName) ? nested.split(tableName, set) : void 0;
4056
+ 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" });
4057
+ const values = stripContextValues(remapUpdateInput(entry ? entry.columns : set, table, tableName), policies?.contextValues?.(tableName));
4058
+ if (!Object.keys(values).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
4059
+ query = executor.update(table).set(values);
3860
4060
  }
3861
- },
3862
- args: queryArgs
3863
- };
4061
+ const filters = scopedWhere({
4062
+ scope,
4063
+ tableName,
4064
+ table,
4065
+ where,
4066
+ relationCtx: relationFilterCtx(filterCtx, tableName),
4067
+ required: requireWhere,
4068
+ deleted: hard ? "INCLUDE" : void 0
4069
+ });
4070
+ if (filters) query = query.where(filters);
4071
+ if (!readsRows) {
4072
+ const result = await query;
4073
+ await after();
4074
+ return rowsAffected(result);
4075
+ }
4076
+ const named = returning({});
4077
+ const rows = await query.returning(Object.keys(named).length ? named : keyColumns);
4078
+ await after(rows);
4079
+ return rows.length;
4080
+ }
4081
+ });
3864
4082
  };
3865
4083
  //#endregion
3866
4084
  //#region src/util/batch-loader/index.ts
@@ -4798,84 +5016,6 @@ const generateTableTypes = (tableName, tables, relationMap, withReturning, relat
4798
5016
  };
4799
5017
  };
4800
5018
  //#endregion
4801
- //#region src/util/builders/common/write-hooks.ts
4802
- /**
4803
- * Normalizes the two registration shapes: a bare function is the `after` hook — the position
4804
- * that has rows, and the one the common audit/outbox case wants — while `{ before, after }`
4805
- * names them explicitly.
4806
- */
4807
- const normalizeWriteHooks = (hooks) => {
4808
- if (!hooks) return;
4809
- if (typeof hooks === "function") return { after: hooks };
4810
- const resolved = {
4811
- before: hooks.before,
4812
- after: hooks.after
4813
- };
4814
- return resolved.before || resolved.after ? resolved : void 0;
4815
- };
4816
- /**
4817
- * Invokes one position of a field's hooks. Returns `undefined` rather than a resolved promise
4818
- * when nothing is registered, so the awaiting call site costs nothing on an unhooked build.
4819
- */
4820
- const runWriteHook = (hooks, position, payload) => {
4821
- const hook = position === "before" ? hooks?.before : hooks?.after;
4822
- if (!hook) return;
4823
- return Promise.resolve(hook({
4824
- ...payload,
4825
- position,
4826
- rows: payload.rows ?? []
4827
- }));
4828
- };
4829
- //#endregion
4830
- //#region src/util/builders/common/write-scaffold.ts
4831
- /**
4832
- * Wraps one mutation body in the scaffolding above and returns the field it becomes.
4833
- *
4834
- * A field carrying a hook is forced into a transaction even when it would have run straight
4835
- * on `db`: a hook exists to write atomically with the mutation, and one that ran outside the
4836
- * transaction could not roll anything back.
4837
- */
4838
- const writeResolver = ({ db, tableName, operation, single, fieldName, args: queryArgs, txCtx, policies, run }) => {
4839
- const hooks = policies?.onWrite?.(tableName, operation);
4840
- const errorCtx = {
4841
- table: tableName,
4842
- operation,
4843
- field: fieldName
4844
- };
4845
- return {
4846
- name: fieldName,
4847
- resolver: async (_source, args, context, info) => {
4848
- try {
4849
- return await runMutation(db, context, info, txCtx, (executor) => {
4850
- const payload = {
4851
- table: tableName,
4852
- operation,
4853
- single,
4854
- args,
4855
- context,
4856
- info,
4857
- tx: executor
4858
- };
4859
- return run({
4860
- executor,
4861
- args,
4862
- context,
4863
- info,
4864
- before: () => runWriteHook(hooks, "before", payload),
4865
- after: (rows) => runWriteHook(hooks, "after", {
4866
- ...payload,
4867
- rows: rows ?? []
4868
- })
4869
- });
4870
- }, !!hooks);
4871
- } catch (e) {
4872
- throw withErrorContext(toGraphQLError(e), errorCtx);
4873
- }
4874
- },
4875
- args: queryArgs
4876
- };
4877
- };
4878
- //#endregion
4879
5019
  //#region src/util/features.ts
4880
5020
  /**
4881
5021
  * What a table generates when the config says nothing. Everything is on except the opt-in
@@ -5039,19 +5179,18 @@ const resolveBuildConfig = (config, schema) => {
5039
5179
  }
5040
5180
  const onWriteConfig = config?.onWrite;
5041
5181
  const writeHooks = /* @__PURE__ */ new Map();
5042
- let globalWriteHooks;
5043
5182
  if (onWriteConfig) {
5044
- if (typeof onWriteConfig === "function" || typeof onWriteConfig.before === "function" || typeof onWriteConfig.after === "function") globalWriteHooks = normalizeWriteHooks(onWriteConfig);
5183
+ if (typeof onWriteConfig === "function" || typeof onWriteConfig.before === "function" || typeof onWriteConfig.after === "function") for (const tableName of featureTables) writeHooks.set(tableName, normalizeWriteHooks(onWriteConfig, getColumns(schema[tableName]), `config.onWrite (on table '${tableName}')`));
5045
5184
  else {
5046
5185
  const tableNames = new Set(tableKeys);
5047
5186
  for (const [tableName, hooks] of Object.entries(onWriteConfig)) {
5048
5187
  if (!tableNames.has(tableName)) throw new Error(`Drizzle-GraphQL Error: config.onWrite names '${tableName}', which is not a table in the Drizzle schema.`);
5049
5188
  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'.`);
5050
- writeHooks.set(tableName, normalizeWriteHooks(hooks));
5189
+ writeHooks.set(tableName, normalizeWriteHooks(hooks, getColumns(schema[tableName]), `config.onWrite.${tableName}`));
5051
5190
  }
5052
5191
  }
5053
5192
  }
5054
- const onWrite = globalWriteHooks || writeHooks.size ? (tableName) => globalWriteHooks ?? writeHooks.get(tableName) : void 0;
5193
+ const onWrite = writeHooks.size ? (tableName) => writeHooks.get(tableName) : void 0;
5055
5194
  const policies = scopeConfig || contextValuesConfig || softDeleteInfos.size || onWrite || defaultOrderBy ? {
5056
5195
  scope: scopeConfig ? (tableName) => scopeConfig[tableName] : void 0,
5057
5196
  contextValues: contextValuesConfig ? (tableName) => contextValuesConfig[tableName] : void 0,
@@ -5117,6 +5256,7 @@ const resolveBuildConfig = (config, schema) => {
5117
5256
  scalars: config?.scalars,
5118
5257
  mapColumnType: config?.mapColumnType,
5119
5258
  enumNameMapper: config?.enumNameMapper,
5259
+ enumValueNameMapper: config?.enumValueNameMapper,
5120
5260
  transactions,
5121
5261
  limits,
5122
5262
  policies,
@@ -6877,7 +7017,9 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
6877
7017
  fieldName: updateCountFieldName,
6878
7018
  requireWhere: tableFeatures.requireWhere,
6879
7019
  filterCtx,
6880
- txCtx: mutationTxCtx
7020
+ txCtx: mutationTxCtx,
7021
+ policies,
7022
+ returnsRows: false
6881
7023
  }) : void 0;
6882
7024
  const deleteCountGenerated = tableFeatures.delete && tableFeatures.countMutations ? generateWriteCount({
6883
7025
  db,
@@ -6888,7 +7030,9 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
6888
7030
  fieldName: deleteCountFieldName,
6889
7031
  requireWhere: tableFeatures.requireWhere,
6890
7032
  filterCtx,
6891
- txCtx: mutationTxCtx
7033
+ txCtx: mutationTxCtx,
7034
+ policies,
7035
+ returnsRows: false
6892
7036
  }) : void 0;
6893
7037
  const hardDeleteMeta = softDeleteInfo?.hardDelete ? { hardDelete: true } : {};
6894
7038
  const withMeta = (generated, meta) => ({
@@ -6958,12 +7102,25 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
6958
7102
  {
6959
7103
  generated: updateCountGenerated,
6960
7104
  type: new GraphQLNonNull(GraphQLInt),
7105
+ meta: {
7106
+ operation: "update",
7107
+ single: false,
7108
+ targetArg: "where",
7109
+ count: true
7110
+ },
6961
7111
  description: "How many rows the update touched. The rows themselves are not read back, which is the point of this mutation."
6962
7112
  },
6963
7113
  {
6964
7114
  generated: deleteCountGenerated,
6965
7115
  type: new GraphQLNonNull(GraphQLInt),
6966
- description: "How many rows the delete removed. The rows themselves are not read back, which is the point of this mutation."
7116
+ meta: {
7117
+ operation: "delete",
7118
+ single: false,
7119
+ targetArg: "where",
7120
+ count: true,
7121
+ ...hardDeleteMeta
7122
+ },
7123
+ 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."
6967
7124
  }
6968
7125
  ],
6969
7126
  onConflictInput,
@@ -7000,7 +7157,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7000
7157
  txCtx,
7001
7158
  policies,
7002
7159
  args: queryArgs,
7003
- run: async ({ executor, args, context, info, before, after }) => {
7160
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7004
7161
  const supplied = single ? [args.values] : args.values;
7005
7162
  if (!supplied.length) throw drizzleError("No values were provided!", { code: "DRIZZLE_NO_VALUES" });
7006
7163
  await before();
@@ -7022,13 +7179,13 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7022
7179
  scope,
7023
7180
  defaultOrderBy: policies?.defaultOrderBy
7024
7181
  });
7025
- const returning = nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns;
7182
+ const returning = withHookReturning(nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns);
7026
7183
  const runInsert = async (target, values) => {
7027
7184
  let query = target.insert(table).values(values).returning(returning);
7028
7185
  if (conflictDoNothing) query = query.onConflictDoNothing();
7029
7186
  return await query;
7030
7187
  };
7031
- const result = nestedEntries ? await writeWithNestedOps({
7188
+ const written = nestedEntries ? await writeWithNestedOps({
7032
7189
  executor,
7033
7190
  runtime: nested,
7034
7191
  tableName,
@@ -7037,7 +7194,8 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7037
7194
  write: (tx, values) => runInsert(tx, [values]),
7038
7195
  context
7039
7196
  }) : await runInsert(executor, input);
7040
- await after(result);
7197
+ await after(written);
7198
+ const result = output(written);
7041
7199
  if (single && !result[0]) {
7042
7200
  if (!conflictDoNothing) throw drizzleError("The insert returned no row.", { code: "DRIZZLE_NO_ROW_RETURNED" });
7043
7201
  return;
@@ -7073,7 +7231,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7073
7231
  txCtx,
7074
7232
  policies,
7075
7233
  args: queryArgs,
7076
- run: async ({ executor, args, context, info, before, after }) => {
7234
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7077
7235
  const supplied = single ? [args.values] : args.values;
7078
7236
  if (!supplied.length) throw drizzleError("No values were provided!", { code: "DRIZZLE_NO_VALUES" });
7079
7237
  await before();
@@ -7095,7 +7253,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7095
7253
  scope,
7096
7254
  defaultOrderBy: policies?.defaultOrderBy
7097
7255
  });
7098
- const returning = nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns;
7256
+ const returning = withHookReturning(nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns);
7099
7257
  const runUpsert = async (target, values) => {
7100
7258
  const plan = resolveConflictPlan({
7101
7259
  table,
@@ -7116,7 +7274,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7116
7274
  });
7117
7275
  return await query;
7118
7276
  };
7119
- const result = nestedEntries ? await writeWithNestedOps({
7277
+ const written = nestedEntries ? await writeWithNestedOps({
7120
7278
  executor,
7121
7279
  runtime: nested,
7122
7280
  tableName,
@@ -7125,7 +7283,8 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7125
7283
  write: (tx, values) => runUpsert(tx, [values]),
7126
7284
  context
7127
7285
  }) : await runUpsert(executor, input);
7128
- await after(result);
7286
+ await after(written);
7287
+ const result = output(written);
7129
7288
  if (single && !result[0]) return;
7130
7289
  const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
7131
7290
  return single ? remapToGraphQLSingleOutput(enriched[0], tableName, table, relationMap) : remapToGraphQLArrayOutput(enriched, tableName, table, relationMap);
@@ -7149,7 +7308,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7149
7308
  txCtx,
7150
7309
  policies,
7151
7310
  args: queryArgs,
7152
- run: async ({ executor, args, context, info, before, after }) => {
7311
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7153
7312
  const { where, set } = args;
7154
7313
  const scope = policies?.scope?.(context);
7155
7314
  await before();
@@ -7180,14 +7339,14 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7180
7339
  required: single || requireWhere
7181
7340
  });
7182
7341
  if (single) await assertSingleMatch(executor, table, filters);
7183
- const returning = nestedOps ? nested.withJoinColumns(tableName, nestedOps, { ...columns }, table) : columns;
7342
+ const returning = withHookReturning(nestedOps ? nested.withJoinColumns(tableName, nestedOps, { ...columns }, table) : columns);
7184
7343
  const runUpdate = async (target, values) => {
7185
7344
  const writes = Object.keys(values).length > 0;
7186
7345
  let query = writes ? target.update(table).set(values) : target.select(returning).from(table);
7187
7346
  if (filters) query = query.where(filters);
7188
7347
  return await (writes ? query.returning(returning) : query);
7189
7348
  };
7190
- const result = nestedOps ? await updateWithNestedOps({
7349
+ const written = nestedOps ? await updateWithNestedOps({
7191
7350
  executor,
7192
7351
  runtime: nested,
7193
7352
  tableName,
@@ -7197,7 +7356,8 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7197
7356
  write: runUpdate,
7198
7357
  context
7199
7358
  }) : await runUpdate(executor, input);
7200
- await after(result);
7359
+ await after(written);
7360
+ const result = output(written);
7201
7361
  if (single && result.length > 1) throw drizzleError("'where' matched more than one row!", { code: "DRIZZLE_MULTI_ROW_MATCH" });
7202
7362
  if (single && !result[0]) return;
7203
7363
  const enriched = await withEagerRelations(executor, tableName, result, pkNames, withParams, hasRelations);
@@ -7237,7 +7397,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7237
7397
  txCtx,
7238
7398
  policies,
7239
7399
  args: queryArgs,
7240
- run: async ({ executor, args, context, info, before, after }) => {
7400
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output }) => {
7241
7401
  const { where } = args;
7242
7402
  const hard = canHardDelete && args.hard === true;
7243
7403
  const scope = policies?.scope?.(context);
@@ -7257,9 +7417,10 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7257
7417
  if (single) await assertSingleMatch(executor, table, filters);
7258
7418
  let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
7259
7419
  if (filters) query = query.where(filters);
7260
- query = query.returning(columns);
7261
- const result = await query;
7262
- await after(result);
7420
+ query = query.returning(withHookReturning(columns));
7421
+ const written = await query;
7422
+ await after(written);
7423
+ const result = output(written);
7263
7424
  if (single && result.length > 1) throw drizzleError("'where' matched more than one row!", { code: "DRIZZLE_MULTI_ROW_MATCH" });
7264
7425
  if (single) return result[0] ? remapToGraphQLSingleOutput(result[0], tableName, table) : void 0;
7265
7426
  return remapToGraphQLArrayOutput(result, tableName, table);
@@ -7460,7 +7621,10 @@ const createSchemaDataGenerator = (adapter) => {
7460
7621
  requireWhere: tableFeatures.requireWhere,
7461
7622
  filterCtx,
7462
7623
  txCtx: mutationTxCtx,
7463
- nested: nestedRuntime
7624
+ nested: nestedRuntime,
7625
+ policies,
7626
+ pkNames: primaryKeyPropNames(schema[tableName]),
7627
+ returnsRows: true
7464
7628
  }) : void 0;
7465
7629
  const deleteCountGenerated = tableFeatures.delete && tableFeatures.countMutations ? generateWriteCount({
7466
7630
  db,
@@ -7471,7 +7635,10 @@ const createSchemaDataGenerator = (adapter) => {
7471
7635
  fieldName: deleteCountFieldName,
7472
7636
  requireWhere: tableFeatures.requireWhere,
7473
7637
  filterCtx,
7474
- txCtx: mutationTxCtx
7638
+ txCtx: mutationTxCtx,
7639
+ policies,
7640
+ pkNames: primaryKeyPropNames(schema[tableName]),
7641
+ returnsRows: true
7475
7642
  }) : void 0;
7476
7643
  const hardDeleteMeta = softDeleteInfo?.hardDelete ? { hardDelete: true } : {};
7477
7644
  return {
@@ -7581,12 +7748,25 @@ const createSchemaDataGenerator = (adapter) => {
7581
7748
  {
7582
7749
  generated: updateCountGenerated,
7583
7750
  type: new GraphQLNonNull(GraphQLInt),
7751
+ meta: {
7752
+ operation: "update",
7753
+ single: false,
7754
+ targetArg: "where",
7755
+ count: true
7756
+ },
7584
7757
  description: "How many rows the update touched. The rows themselves are not read back, which is the point of this mutation."
7585
7758
  },
7586
7759
  {
7587
7760
  generated: deleteCountGenerated,
7588
7761
  type: new GraphQLNonNull(GraphQLInt),
7589
- description: "How many rows the delete removed. The rows themselves are not read back, which is the point of this mutation."
7762
+ meta: {
7763
+ operation: "delete",
7764
+ single: false,
7765
+ targetArg: "where",
7766
+ count: true,
7767
+ ...hardDeleteMeta
7768
+ },
7769
+ 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."
7590
7770
  }
7591
7771
  ],
7592
7772
  onConflictInput,
@@ -7648,7 +7828,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7648
7828
  txCtx,
7649
7829
  policies,
7650
7830
  args: queryArgs,
7651
- run: async ({ executor, args, context, info, before, after }) => {
7831
+ run: async ({ executor, args, context, info, before, after, returning: withHookReturning, output: withoutHookColumns }) => {
7652
7832
  const { updates } = args;
7653
7833
  if (!updates.length) throw drizzleError("No updates were provided!", { code: "DRIZZLE_NO_VALUES" });
7654
7834
  await before();
@@ -7679,7 +7859,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7679
7859
  };
7680
7860
  });
7681
7861
  const anyNested = entries.some((entry) => entry.ops);
7682
- const returning = anyNested ? nested.withJoinColumns(tableName, mergedOps(entries.map((entry) => ({ ops: entry.ops ?? {} }))), { ...columns }, table) : columns;
7862
+ const returning = withHookReturning(anyNested ? nested.withJoinColumns(tableName, mergedOps(entries.map((entry) => ({ ops: entry.ops ?? {} }))), { ...columns }, table) : columns);
7683
7863
  const perEntry = await runBatch({
7684
7864
  executor,
7685
7865
  entries,
@@ -7690,8 +7870,9 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
7690
7870
  tableName,
7691
7871
  context
7692
7872
  });
7693
- const flatRows = perEntry.flat();
7694
- await after(flatRows);
7873
+ const written = perEntry.flat();
7874
+ await after(written);
7875
+ const flatRows = withoutHookColumns(written);
7695
7876
  const enriched = await withEagerRelations(executor, tableName, flatRows, pkNames, withParams, hasRelations);
7696
7877
  const output = [];
7697
7878
  let offset = 0;