@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 +99 -11
- package/dist/index.cjs +157 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +453 -55
- package/dist/index.d.ts +453 -55
- package/dist/index.js +157 -39
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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),
|
|
108
|
-
`createTasks`
|
|
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
|
|
366
|
-
|
|
367
|
-
|
|
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": ["
|
|
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:
|
|
2018
|
-
|
|
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) => [
|
|
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
|
|
4324
|
-
const
|
|
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 = `${
|
|
4329
|
-
const restoreFieldName = `${
|
|
4330
|
-
const
|
|
4331
|
-
const updateSingleFieldName =
|
|
4332
|
-
const deleteSingleFieldName =
|
|
4333
|
-
const restoreSingleFieldName =
|
|
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")
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
7288
|
-
await after(
|
|
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
|
|
7720
|
-
await after(
|
|
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;
|