@vantreeseba/drizzle-graphql 13.1.1 → 15.0.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 +178 -15
- package/dist/index.cjs +726 -213
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -8
- package/dist/index.d.ts +79 -8
- package/dist/index.js +726 -213
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Column, One, SQL, Table, aliasedTable, and, arrayContains, arrayOverlaps, asc, avg, count, countDistinct, desc, eq, extractExtendedColumnType, getColumns, getTableAsAliasSQL, gt, gte, ilike, inArray, is, isNotNull, isNull, like, lt, lte, max, min, ne, not, notIlike, notInArray, notLike, or, relationsFilterToSQL, sql, sum } from "drizzle-orm";
|
|
2
|
-
import { MySqlAsyncDatabase, MySqlInt, MySqlSerial, MySqlTable, getTableConfig } from "drizzle-orm/mysql-core";
|
|
2
|
+
import { MySqlAsyncDatabase, MySqlDate, MySqlDateString, MySqlDateTime, MySqlDateTimeString, MySqlInt, MySqlSerial, MySqlTable, MySqlTimestamp, MySqlTimestampString, getTableConfig } from "drizzle-orm/mysql-core";
|
|
3
3
|
import { PgAsyncDatabase, PgDate, PgDateString, PgInteger, PgSerial, PgTable, PgTimestamp, PgTimestampString, PgUUID, getTableConfig as getTableConfig$1 } from "drizzle-orm/pg-core";
|
|
4
|
-
import { SQLiteAsyncDatabase, SQLiteInteger, SQLiteTable, getTableConfig as getTableConfig$2 } from "drizzle-orm/sqlite-core";
|
|
4
|
+
import { SQLiteAsyncDatabase, SQLiteInteger, SQLiteTable, SQLiteTimestamp, getTableConfig as getTableConfig$2 } from "drizzle-orm/sqlite-core";
|
|
5
5
|
import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, Kind, getArgumentValues } from "graphql";
|
|
6
|
-
import {
|
|
6
|
+
import { GraphQLDateTime, GraphQLJSON, GraphQLUUID } from "graphql-scalars";
|
|
7
7
|
import pluralize from "pluralize";
|
|
8
8
|
//#region src/util/builders/common/errors.ts
|
|
9
9
|
/**
|
|
@@ -160,6 +160,76 @@ const GraphQLDecimalString = new GraphQLScalarType({
|
|
|
160
160
|
return parseNumeric(ast.value);
|
|
161
161
|
}
|
|
162
162
|
});
|
|
163
|
+
const fullDatePattern = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
164
|
+
const daysInMonth = (year, month) => {
|
|
165
|
+
if (month === 2) return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0 ? 29 : 28;
|
|
166
|
+
return [
|
|
167
|
+
4,
|
|
168
|
+
6,
|
|
169
|
+
9,
|
|
170
|
+
11
|
|
171
|
+
].includes(month) ? 30 : 31;
|
|
172
|
+
};
|
|
173
|
+
/** Whether `value` is a `YYYY-MM-DD` string naming a day that exists on the calendar. */
|
|
174
|
+
const isCalendarDate = (value) => {
|
|
175
|
+
const match = fullDatePattern.exec(value);
|
|
176
|
+
if (!match) return false;
|
|
177
|
+
const year = Number(match[1]);
|
|
178
|
+
const month = Number(match[2]);
|
|
179
|
+
const day = Number(match[3]);
|
|
180
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(year, month);
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* A JS `Date` as the calendar day it stands for. Read in UTC, because that is how drivers
|
|
184
|
+
* build one from a `date` column: `new Date('2026-09-15')` is UTC midnight, so the local
|
|
185
|
+
* getters would name the previous day anywhere west of Greenwich.
|
|
186
|
+
*/
|
|
187
|
+
const dateToCalendarDate = (value) => {
|
|
188
|
+
if (Number.isNaN(value.getTime())) return;
|
|
189
|
+
const year = value.getUTCFullYear();
|
|
190
|
+
if (year < 0 || year > 9999) return;
|
|
191
|
+
const month = String(value.getUTCMonth() + 1).padStart(2, "0");
|
|
192
|
+
const day = String(value.getUTCDate()).padStart(2, "0");
|
|
193
|
+
return `${String(year).padStart(4, "0")}-${month}-${day}`;
|
|
194
|
+
};
|
|
195
|
+
const calendarDate = (code, acceptDate) => (value) => {
|
|
196
|
+
if (typeof value === "string") {
|
|
197
|
+
if (!isCalendarDate(value)) throw drizzleError(`Date cannot represent "${value}": expected a calendar date as YYYY-MM-DD`, { code });
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
if (acceptDate && value instanceof Date) {
|
|
201
|
+
const formatted = dateToCalendarDate(value);
|
|
202
|
+
if (formatted === void 0) throw drizzleError("Date cannot represent an invalid or out-of-range Date", { code });
|
|
203
|
+
return formatted;
|
|
204
|
+
}
|
|
205
|
+
throw drizzleError(`Date cannot represent value: ${JSON.stringify(value)}: expected a YYYY-MM-DD string`, { code });
|
|
206
|
+
};
|
|
207
|
+
const parseCalendarDate = calendarDate(INVALID_INPUT, false);
|
|
208
|
+
/**
|
|
209
|
+
* A calendar day — `date` columns, whichever `mode` drizzle reads them in. Always transported
|
|
210
|
+
* as an RFC 3339 `full-date` string (`YYYY-MM-DD`), in both directions: it has no time and no
|
|
211
|
+
* offset, so there is nothing for a client's timezone to shift. `graphql-scalars`' own
|
|
212
|
+
* `GraphQLDate` is deliberately not used: its `parseValue` returns a JS `Date`, so the same
|
|
213
|
+
* scalar would be a string on the way out and an instant on the way in.
|
|
214
|
+
*
|
|
215
|
+
* Input accepts only a `YYYY-MM-DD` string naming a real day (`2026-02-30` and datetime
|
|
216
|
+
* strings are rejected) and hands it on unchanged. Output also accepts a JS `Date`, read as
|
|
217
|
+
* the UTC day it encodes.
|
|
218
|
+
*/
|
|
219
|
+
const GraphQLDate = new GraphQLScalarType({
|
|
220
|
+
name: "Date",
|
|
221
|
+
description: "A calendar date with no time or offset, transported as an RFC 3339 full-date string (YYYY-MM-DD).",
|
|
222
|
+
specifiedByURL: "https://datatracker.ietf.org/doc/html/rfc3339#section-5.6",
|
|
223
|
+
serialize: calendarDate(UNREPRESENTABLE, true),
|
|
224
|
+
parseValue: parseCalendarDate,
|
|
225
|
+
parseLiteral: (ast) => {
|
|
226
|
+
if (ast.kind !== Kind.STRING) throw drizzleError(`Date cannot represent a ${ast.kind}`, {
|
|
227
|
+
code: INVALID_INPUT,
|
|
228
|
+
nodes: ast
|
|
229
|
+
});
|
|
230
|
+
return parseCalendarDate(ast.value);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
163
233
|
//#endregion
|
|
164
234
|
//#region src/util/case-ops/index.ts
|
|
165
235
|
const uncapitalize = (input) => input?.length ? `${input[0].toLocaleLowerCase()}${input.length > 1 ? input.slice(1, input.length) : ""}` : input;
|
|
@@ -193,6 +263,17 @@ const singularizeMapper = (tableName) => ({
|
|
|
193
263
|
//#endregion
|
|
194
264
|
//#region src/util/type-converter/index.ts
|
|
195
265
|
const allowedNameChars = /^[a-zA-Z0-9_]+$/;
|
|
266
|
+
/** drizzle's `number` constraints whose values always fit a GraphQL `Int` (32-bit signed). */
|
|
267
|
+
const smallIntegerConstraints = /* @__PURE__ */ new Set([
|
|
268
|
+
"int8",
|
|
269
|
+
"uint8",
|
|
270
|
+
"int16",
|
|
271
|
+
"uint16",
|
|
272
|
+
"int24",
|
|
273
|
+
"uint24",
|
|
274
|
+
"int32",
|
|
275
|
+
"year"
|
|
276
|
+
]);
|
|
196
277
|
/** A GraphQL `Name` that is also allowed as an enum value — the spec reserves the three literals. */
|
|
197
278
|
const validEnumValueName = (name) => /^[_a-zA-Z][_a-zA-Z0-9]*$/.test(name) && name !== "true" && name !== "false" && name !== "null";
|
|
198
279
|
/**
|
|
@@ -333,6 +414,52 @@ const geoXyInputType = new GraphQLInputObjectType({
|
|
|
333
414
|
y: { type: GraphQLFloat }
|
|
334
415
|
}
|
|
335
416
|
});
|
|
417
|
+
/**
|
|
418
|
+
* Whether the column holds a calendar day with no time component — a SQL `date`, in either of
|
|
419
|
+
* the modes drizzle reads one in (`'date'` → a JS `Date`, `'string'` → `YYYY-MM-DD`). Every
|
|
420
|
+
* position a column's type appears in — output, create/update input, filter operands,
|
|
421
|
+
* aggregates and group keys — gives such a column the `Date` scalar, whichever mode it uses:
|
|
422
|
+
* the mode is how the server holds the value, not what the value is. SQLite has no date-only
|
|
423
|
+
* column type (its integer timestamps are instants — see {@link isDateTimeColumn}), so only
|
|
424
|
+
* Postgres and MySQL appear here.
|
|
425
|
+
*/
|
|
426
|
+
const isDateOnlyColumn = (column) => is(column, PgDate) || is(column, PgDateString) || is(column, MySqlDate) || is(column, MySqlDateString);
|
|
427
|
+
/**
|
|
428
|
+
* Whether the column is a MySQL `timestamp` / `datetime` read in drizzle's `'string'` mode. The
|
|
429
|
+
* driver hands such a column back as MySQL's own `YYYY-MM-DD HH:MM:SS[.fraction]` text, with no
|
|
430
|
+
* offset, and expects that form on write, so the data mappers translate between it and the ISO
|
|
431
|
+
* 8601 instant the `DateTime` scalar carries. The text is read as UTC, the way drizzle's own
|
|
432
|
+
* `'date'`-mode codecs for these columns read it.
|
|
433
|
+
*/
|
|
434
|
+
const isMySqlDateTimeStringColumn = (column) => is(column, MySqlTimestampString) || is(column, MySqlDateTimeString);
|
|
435
|
+
/**
|
|
436
|
+
* Whether the column is a Postgres `timestamp` read in drizzle's `'string'` mode. The driver hands
|
|
437
|
+
* such a column back as Postgres's own text — `YYYY-MM-DD HH:MM:SS[.fraction]`, followed by an
|
|
438
|
+
* offset (`+00`, `-06`, `+05:30`) for `timestamptz` — so the data mappers translate between it
|
|
439
|
+
* and the ISO 8601 instant the `DateTime` scalar carries. Text without an offset (`timestamp`
|
|
440
|
+
* without time zone) is read as UTC, the way drizzle's own `'date'`-mode codec reads it.
|
|
441
|
+
*/
|
|
442
|
+
const isPgTimestampStringColumn = (column) => is(column, PgTimestampString);
|
|
443
|
+
/**
|
|
444
|
+
* Whether the column is a SQLite `integer({ mode: 'timestamp' | 'timestamp_ms' })`. Both modes
|
|
445
|
+
* are one drizzle class, told apart by `mode`: the stored integer counts seconds or milliseconds
|
|
446
|
+
* since the Unix epoch. drizzle converts a JS `Date` to that integer on write (`'timestamp'`
|
|
447
|
+
* floors to the whole second) and back on read, but a value read through a path that skips the
|
|
448
|
+
* column's decoder arrives as the bare integer, which {@link sqliteTimestampToDate} reads.
|
|
449
|
+
*/
|
|
450
|
+
const isSQLiteTimestampColumn = (column) => is(column, SQLiteTimestamp);
|
|
451
|
+
/**
|
|
452
|
+
* A SQLite timestamp column's stored integer as the instant it names, using the column's own
|
|
453
|
+
* unit (seconds for `'timestamp'`, milliseconds for `'timestamp_ms'`).
|
|
454
|
+
*/
|
|
455
|
+
const sqliteTimestampToDate = (column, value) => new Date(column.mode === "timestamp" ? value * 1e3 : value);
|
|
456
|
+
/**
|
|
457
|
+
* Whether the column holds an instant — a Postgres `timestamp`, a MySQL `timestamp` /
|
|
458
|
+
* `datetime`, in either drizzle mode, or a SQLite integer timestamp in either unit. Every position
|
|
459
|
+
* such a column appears in gives it the `DateTime` scalar and the `DateTimeFilter`, whichever
|
|
460
|
+
* mode it uses, for the same reason as {@link isDateOnlyColumn}.
|
|
461
|
+
*/
|
|
462
|
+
const isDateTimeColumn = (column) => is(column, PgTimestamp) || isPgTimestampStringColumn(column) || is(column, MySqlTimestamp) || is(column, MySqlDateTime) || isMySqlDateTimeStringColumn(column) || isSQLiteTimestampColumn(column);
|
|
336
463
|
const columnToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
337
464
|
const dimensions = column.dimensions;
|
|
338
465
|
if (dimensions !== void 0 && dimensions > 0) {
|
|
@@ -359,7 +486,11 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
359
486
|
typeLabel: "Boolean"
|
|
360
487
|
};
|
|
361
488
|
case "object":
|
|
362
|
-
if (column
|
|
489
|
+
if (isDateOnlyColumn(column)) return {
|
|
490
|
+
type: GraphQLDate,
|
|
491
|
+
typeLabel: "Date"
|
|
492
|
+
};
|
|
493
|
+
if (isDateTimeColumn(column)) return {
|
|
363
494
|
type: GraphQLDateTime,
|
|
364
495
|
typeLabel: "DateTime"
|
|
365
496
|
};
|
|
@@ -379,7 +510,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
379
510
|
type: GraphQLDecimalString,
|
|
380
511
|
typeLabel: "Decimal"
|
|
381
512
|
};
|
|
382
|
-
if (column
|
|
513
|
+
if (isDateTimeColumn(column)) return {
|
|
383
514
|
type: GraphQLDateTime,
|
|
384
515
|
typeLabel: "DateTime"
|
|
385
516
|
};
|
|
@@ -387,10 +518,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
387
518
|
type: GraphQLUUID,
|
|
388
519
|
typeLabel: "UUID"
|
|
389
520
|
};
|
|
390
|
-
if (column
|
|
391
|
-
type: GraphQLString,
|
|
392
|
-
typeLabel: "Date"
|
|
393
|
-
} : {
|
|
521
|
+
if (isDateOnlyColumn(column)) return {
|
|
394
522
|
type: GraphQLDate,
|
|
395
523
|
typeLabel: "Date"
|
|
396
524
|
};
|
|
@@ -402,7 +530,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
402
530
|
type: GraphQLBigIntString,
|
|
403
531
|
typeLabel: "BigInt"
|
|
404
532
|
};
|
|
405
|
-
case "number": return is(column, PgInteger) || is(column, PgSerial) || is(column, MySqlInt) || is(column, MySqlSerial) || is(column, SQLiteInteger) ? {
|
|
533
|
+
case "number": return constraint !== void 0 && smallIntegerConstraints.has(constraint) || is(column, PgInteger) || is(column, PgSerial) || is(column, MySqlInt) || is(column, MySqlSerial) || is(column, SQLiteInteger) ? {
|
|
406
534
|
type: GraphQLInt,
|
|
407
535
|
typeLabel: "Integer"
|
|
408
536
|
} : {
|
|
@@ -578,7 +706,8 @@ const isJsonColumn = (column) => (column.dataType ?? "").includes("json") && col
|
|
|
578
706
|
* - "Decimal" → numeric/decimal columns (Decimal-scalar-typed operators, no string pattern operators)
|
|
579
707
|
* - the enum GraphQL type name → enum columns (still unique per enum; equality and
|
|
580
708
|
* membership only, like Boolean)
|
|
581
|
-
* - "
|
|
709
|
+
* - "Date" → date columns, either mode (ordering kept, no string pattern operators)
|
|
710
|
+
* - "DateTime" → timestamp columns (ordering kept, no string pattern operators)
|
|
582
711
|
* - "Int" → integer/serial columns (no string pattern operators)
|
|
583
712
|
* - "Float" → real/double columns (no string pattern operators)
|
|
584
713
|
* - "String" → all other text/varchar columns
|
|
@@ -617,8 +746,11 @@ const resolveGenericFilterDescriptor = (column, columnGraphQLType) => {
|
|
|
617
746
|
name: "Decimal",
|
|
618
747
|
kind: "scalar"
|
|
619
748
|
};
|
|
620
|
-
|
|
621
|
-
|
|
749
|
+
if (columnGraphQLType.type === GraphQLDate) return {
|
|
750
|
+
name: "Date",
|
|
751
|
+
kind: "scalar"
|
|
752
|
+
};
|
|
753
|
+
if (isDateTimeColumn(column)) return {
|
|
622
754
|
name: "DateTime",
|
|
623
755
|
kind: "scalar"
|
|
624
756
|
};
|
|
@@ -1013,6 +1145,58 @@ const generateColumnFilterValues = (column, tableName, columnName, cacheCtx) =>
|
|
|
1013
1145
|
};
|
|
1014
1146
|
//#endregion
|
|
1015
1147
|
//#region src/util/data-mappers/index.ts
|
|
1148
|
+
/**
|
|
1149
|
+
* MySQL's own text form for a `timestamp` / `datetime`: `YYYY-MM-DD HH:MM:SS`, an optional
|
|
1150
|
+
* fraction, and no offset. mysql2 returns it for a `'string'`-mode column, and it is what such a
|
|
1151
|
+
* column expects on write.
|
|
1152
|
+
*/
|
|
1153
|
+
const MYSQL_DATETIME_TEXT = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/;
|
|
1154
|
+
/**
|
|
1155
|
+
* MySQL datetime text, read as UTC, as the ISO 8601 instant the `DateTime` scalar carries.
|
|
1156
|
+
* UTC because that is how drizzle's own `'date'`-mode codecs read the same text, so a column's
|
|
1157
|
+
* two modes agree on the instant. Anything else (a zero date, a value already carrying an
|
|
1158
|
+
* offset) is returned unchanged for the scalar to accept or reject.
|
|
1159
|
+
*/
|
|
1160
|
+
const mySqlDateTimeTextToIso = (value) => {
|
|
1161
|
+
const match = MYSQL_DATETIME_TEXT.exec(value);
|
|
1162
|
+
return match ? `${match[1]}T${match[2]}Z` : value;
|
|
1163
|
+
};
|
|
1164
|
+
/**
|
|
1165
|
+
* A JS `Date` as offset-less `YYYY-MM-DD HH:MM:SS.mmm` text in UTC: what a MySQL `'string'`-mode
|
|
1166
|
+
* column and a Postgres `timestamp` (without time zone) `'string'`-mode column are written with.
|
|
1167
|
+
*/
|
|
1168
|
+
const dateToUtcWallText = (value) => value.toISOString().slice(0, -1).replace("T", " ");
|
|
1169
|
+
/**
|
|
1170
|
+
* Postgres's text form for a `timestamp` / `timestamptz` (the `ISO` `DateStyle`, the default):
|
|
1171
|
+
* `YYYY-MM-DD HH:MM:SS`, an optional fraction, and — for `timestamptz` only — the session zone's
|
|
1172
|
+
* offset as `±HH`, `±HH:MM` or, for historical local mean time, `±HH:MM:SS`.
|
|
1173
|
+
*/
|
|
1174
|
+
const PG_TIMESTAMP_TEXT = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(\.\d+)?(?:([+-])(\d{2})(?::?(\d{2}))?(?::?(\d{2}))?)?$/;
|
|
1175
|
+
/**
|
|
1176
|
+
* Postgres timestamp text as the RFC 3339 UTC instant (`…Z`) the `DateTime` scalar accepts.
|
|
1177
|
+
*
|
|
1178
|
+
* - With an offset (`timestamptz`), the instant is shifted to UTC. The offset can carry seconds,
|
|
1179
|
+
* which RFC 3339 cannot express, so it is applied here rather than rewritten.
|
|
1180
|
+
* - Without one (`timestamp`), the wall time is read as UTC: that is what drizzle's `'date'`-mode
|
|
1181
|
+
* codec does (`new Date(text + '+0000')`) and what the input side writes, so a column's two
|
|
1182
|
+
* modes agree on the instant.
|
|
1183
|
+
* - The fraction is kept digit for digit. The scalar itself carries milliseconds, so microseconds
|
|
1184
|
+
* are truncated on the wire, never rounded into the next second.
|
|
1185
|
+
*
|
|
1186
|
+
* Anything else (`infinity`, a `BC` date, a five-digit year) is returned unchanged for the scalar
|
|
1187
|
+
* to reject.
|
|
1188
|
+
*/
|
|
1189
|
+
const pgTimestampTextToIso = (value) => {
|
|
1190
|
+
const match = PG_TIMESTAMP_TEXT.exec(value);
|
|
1191
|
+
if (!match) return value;
|
|
1192
|
+
const [, date, time, fraction = "", sign, hours, minutes = "0", seconds = "0"] = match;
|
|
1193
|
+
if (!sign) return `${date}T${time}${fraction}Z`;
|
|
1194
|
+
const wallMs = Date.parse(`${date}T${time}Z`);
|
|
1195
|
+
if (Number.isNaN(wallMs)) return value;
|
|
1196
|
+
const offsetSeconds = (sign === "-" ? -1 : 1) * (Number(hours) * 3600 + Number(minutes) * 60 + Number(seconds));
|
|
1197
|
+
const utc = (/* @__PURE__ */ new Date(wallMs - offsetSeconds * 1e3)).toISOString();
|
|
1198
|
+
return utc.length === 24 ? `${utc.slice(0, 19)}${fraction}Z` : value;
|
|
1199
|
+
};
|
|
1016
1200
|
const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
|
|
1017
1201
|
if (Array.isArray(value)) {
|
|
1018
1202
|
const relations = relationMap?.[tableName];
|
|
@@ -1031,7 +1215,14 @@ const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
|
|
|
1031
1215
|
if (!column) return value;
|
|
1032
1216
|
if (getColumnScalarOverride(column, false)) return value;
|
|
1033
1217
|
if (isJsonColumn(column)) return value;
|
|
1218
|
+
if (value instanceof Date && isDateOnlyColumn(column)) return dateToCalendarDate(value) ?? value;
|
|
1219
|
+
if (typeof value === "number" && isSQLiteTimestampColumn(column)) {
|
|
1220
|
+
const instant = sqliteTimestampToDate(column, value);
|
|
1221
|
+
return Number.isNaN(instant.getTime()) ? value : instant.toISOString();
|
|
1222
|
+
}
|
|
1034
1223
|
if (value instanceof Date) return value.toISOString();
|
|
1224
|
+
if (typeof value === "string" && isMySqlDateTimeStringColumn(column)) return mySqlDateTimeTextToIso(value);
|
|
1225
|
+
if (typeof value === "string" && isPgTimestampStringColumn(column)) return pgTimestampTextToIso(value);
|
|
1035
1226
|
if (value instanceof Buffer) return Array.from(value);
|
|
1036
1227
|
if (typeof value === "bigint") return value.toString();
|
|
1037
1228
|
if (Array.isArray(value)) {
|
|
@@ -1077,16 +1268,20 @@ const remapFromGraphQLCore = (value, column, columnName) => {
|
|
|
1077
1268
|
}
|
|
1078
1269
|
const dataType = column.dataType ?? "";
|
|
1079
1270
|
const columnType = column.columnType ?? "";
|
|
1080
|
-
if (
|
|
1081
|
-
const
|
|
1271
|
+
if (isDateTimeColumn(column)) {
|
|
1272
|
+
const isMySql = columnType.startsWith("MySql");
|
|
1273
|
+
const isPgString = isPgTimestampStringColumn(column);
|
|
1274
|
+
const toIso = isMySql ? mySqlDateTimeTextToIso : isPgString ? pgTimestampTextToIso : void 0;
|
|
1275
|
+
const formatted = new Date(toIso && typeof value === "string" ? toIso(value) : value);
|
|
1082
1276
|
if (Number.isNaN(formatted.getTime())) throw drizzleError(`Field '${columnName}' is not a valid date!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1083
|
-
return formatted;
|
|
1277
|
+
if (isPgString) return column.withTimezone ? formatted.toISOString() : dateToUtcWallText(formatted);
|
|
1278
|
+
return isMySqlDateTimeStringColumn(column) ? dateToUtcWallText(formatted) : formatted;
|
|
1084
1279
|
}
|
|
1085
|
-
if ((
|
|
1086
|
-
|
|
1087
|
-
const
|
|
1088
|
-
if (
|
|
1089
|
-
return
|
|
1280
|
+
if (isDateOnlyColumn(column)) {
|
|
1281
|
+
if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
|
|
1282
|
+
const day = value instanceof Date ? dateToCalendarDate(value) : value;
|
|
1283
|
+
if (typeof day !== "string" || !isCalendarDate(day)) throw drizzleError(`Field '${columnName}' is not a valid date! Expected a calendar date as YYYY-MM-DD.`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1284
|
+
return day;
|
|
1090
1285
|
}
|
|
1091
1286
|
if (dataType.includes("bigint")) try {
|
|
1092
1287
|
return BigInt(value);
|
|
@@ -1442,7 +1637,8 @@ const extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1442
1637
|
}
|
|
1443
1638
|
if (NOT) {
|
|
1444
1639
|
const extracted = extractFiltersColumn(column, columnName, NOT);
|
|
1445
|
-
|
|
1640
|
+
const negated = extracted ? not(extracted) : void 0;
|
|
1641
|
+
if (negated) variants.push(negated);
|
|
1446
1642
|
}
|
|
1447
1643
|
if (OR?.length) {
|
|
1448
1644
|
const orVariants = [];
|
|
@@ -1455,6 +1651,150 @@ const extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1455
1651
|
return variants.length ? variants.length > 1 ? and(...variants) : variants[0] : void 0;
|
|
1456
1652
|
};
|
|
1457
1653
|
//#endregion
|
|
1654
|
+
//#region src/util/builders/common/relations.ts
|
|
1655
|
+
/**
|
|
1656
|
+
* Flatten drizzle-orm v1 TablesRelationalConfig into the canonical
|
|
1657
|
+
* Record<tableName, Record<relName, TableNamedRelations>> shape used
|
|
1658
|
+
* throughout common.ts. Both pg.ts and sqlite.ts call this before
|
|
1659
|
+
* passing the relation map to any shared function.
|
|
1660
|
+
*/
|
|
1661
|
+
const buildNamedRelations = (relations, tableEntries) => {
|
|
1662
|
+
const namedRelations = {};
|
|
1663
|
+
for (const [relTableName, relConfig] of Object.entries(relations)) {
|
|
1664
|
+
if (!relConfig?.relations) continue;
|
|
1665
|
+
const namedConfig = {};
|
|
1666
|
+
for (const [innerRelName, innerRelValue] of Object.entries(relConfig.relations)) {
|
|
1667
|
+
const targetTable = innerRelValue.targetTable ?? innerRelValue.referencedTable;
|
|
1668
|
+
const directTargetName = innerRelValue.targetTableName;
|
|
1669
|
+
let targetTableName;
|
|
1670
|
+
if (directTargetName) targetTableName = tableEntries.find(([key]) => key === directTargetName)?.[0];
|
|
1671
|
+
else if (targetTable) targetTableName = tableEntries.find(([, tableValue]) => tableValue === targetTable)?.[0];
|
|
1672
|
+
if (!targetTableName) continue;
|
|
1673
|
+
namedConfig[innerRelName] = {
|
|
1674
|
+
relation: innerRelValue,
|
|
1675
|
+
targetTableName
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
if (Object.keys(namedConfig).length > 0) namedRelations[relTableName] = namedConfig;
|
|
1679
|
+
}
|
|
1680
|
+
return namedRelations;
|
|
1681
|
+
};
|
|
1682
|
+
/**
|
|
1683
|
+
* Records each relation's target-table primary-key property names on the relation entry,
|
|
1684
|
+
* so the pagination paths (the window-function batch loader and the eager `with:` orderBy
|
|
1685
|
+
* default) can fall back to a deterministic PK order without re-deriving it per request.
|
|
1686
|
+
*
|
|
1687
|
+
* Composite primary keys are only visible through the dialect's getTableConfig, so the
|
|
1688
|
+
* dialect builder passes a `resolvePkNames` that threads the composite column names in.
|
|
1689
|
+
* Mutates the relation entries in place (they are shared with the pruned eager map and
|
|
1690
|
+
* the resolver factory, so attaching once covers every consumer).
|
|
1691
|
+
*/
|
|
1692
|
+
const attachTargetPrimaryKeys = (namedRelations, tables, resolvePkNames) => {
|
|
1693
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1694
|
+
for (const rels of Object.values(namedRelations)) for (const relEntry of Object.values(rels)) {
|
|
1695
|
+
const { targetTableName } = relEntry;
|
|
1696
|
+
let pk = cache.get(targetTableName);
|
|
1697
|
+
if (!pk) {
|
|
1698
|
+
const targetTable = tables[targetTableName];
|
|
1699
|
+
pk = targetTable ? resolvePkNames(targetTable) : [];
|
|
1700
|
+
cache.set(targetTableName, pk);
|
|
1701
|
+
}
|
|
1702
|
+
relEntry.targetPkNames = pk;
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
/**
|
|
1706
|
+
* The table column behind one entry of a relation's `sourceColumns`, `targetColumns` or
|
|
1707
|
+
* `through` lists.
|
|
1708
|
+
*
|
|
1709
|
+
* drizzle-orm rc.4 stores the table's own Column objects in `sourceColumns` / `targetColumns`
|
|
1710
|
+
* and a `RelationsBuilderColumn` descriptor (`{ _: { column, key, tableName } }`) in `through`.
|
|
1711
|
+
* The rc.5 snapshots store the descriptor in all of them. Both shapes are accepted here, told
|
|
1712
|
+
* apart structurally rather than by importing a class one of those versions does not export.
|
|
1713
|
+
* Anything else yields `undefined`, which callers treat as "join columns not readable".
|
|
1714
|
+
*/
|
|
1715
|
+
const unwrapRelationColumn = (entry) => {
|
|
1716
|
+
if (is(entry, Column)) return entry;
|
|
1717
|
+
const inner = entry?._?.column;
|
|
1718
|
+
return is(inner, Column) ? inner : void 0;
|
|
1719
|
+
};
|
|
1720
|
+
/**
|
|
1721
|
+
* A relation's `sourceColumns` or `targetColumns` as table columns, via
|
|
1722
|
+
* {@link unwrapRelationColumn}. `undefined` when the list is missing, empty, or has an entry
|
|
1723
|
+
* that is not a column in either known shape.
|
|
1724
|
+
*/
|
|
1725
|
+
const relationColumns = (relation, side) => {
|
|
1726
|
+
const entries = relation?.[side === "source" ? "sourceColumns" : "targetColumns"];
|
|
1727
|
+
if (!entries?.length) return;
|
|
1728
|
+
const columns = [];
|
|
1729
|
+
for (const entry of entries) {
|
|
1730
|
+
const column = unwrapRelationColumn(entry);
|
|
1731
|
+
if (!column) return;
|
|
1732
|
+
columns.push(column);
|
|
1733
|
+
}
|
|
1734
|
+
return columns;
|
|
1735
|
+
};
|
|
1736
|
+
/**
|
|
1737
|
+
* drizzle-orm's "no filter" sentinel. rc.5 snapshots export it as `EmptyFilter` and use it as
|
|
1738
|
+
* the default `relation.where`; rc.4 has no such export and leaves `where` undefined. It is a
|
|
1739
|
+
* registered symbol, so it can be compared against without importing anything.
|
|
1740
|
+
*/
|
|
1741
|
+
const EMPTY_FILTER = Symbol.for("drizzle:EmptyFilter");
|
|
1742
|
+
/**
|
|
1743
|
+
* The relation's own declared `where` filter, or `undefined` when it has none. rc.4 leaves
|
|
1744
|
+
* `where` undefined in that case; rc.5 snapshots set it to the `EmptyFilter` sentinel, which is
|
|
1745
|
+
* truthy and must not be read as a filter.
|
|
1746
|
+
*/
|
|
1747
|
+
const relationOwnWhere = (relation) => {
|
|
1748
|
+
const where = relation?.where;
|
|
1749
|
+
return where == null || where === EMPTY_FILTER ? void 0 : where;
|
|
1750
|
+
};
|
|
1751
|
+
/**
|
|
1752
|
+
* Whether the relation's `where` was inherited from its reverse relation, and so is written
|
|
1753
|
+
* against the parent table rather than the target. rc.4 calls this flag `isReversed`; rc.5
|
|
1754
|
+
* snapshots renamed it to `isFilterReversed`. Both are set in the same place with the same
|
|
1755
|
+
* meaning (true only for a relation declared without `from`/`to` and without its own `where`).
|
|
1756
|
+
*/
|
|
1757
|
+
const isRelationFilterReversed = (relation) => !!(relation?.isFilterReversed ?? relation?.isReversed);
|
|
1758
|
+
/**
|
|
1759
|
+
* Warns, at build time, that a field was left out because the relation's join columns could
|
|
1760
|
+
* not be matched to its tables. Omitting it silently is how a drizzle-orm change in the shape
|
|
1761
|
+
* of relation columns (issue #179) went unnoticed. A relation that declares no join columns
|
|
1762
|
+
* is not reported: there is nothing that failed to match.
|
|
1763
|
+
*/
|
|
1764
|
+
const warnUnresolvedJoin = (relEntry, tableName, relationName, omitted) => {
|
|
1765
|
+
const rel = relEntry.relation ?? relEntry;
|
|
1766
|
+
if (!rel?.sourceColumns?.length || !rel?.targetColumns?.length) return;
|
|
1767
|
+
console.warn(`Drizzle-GraphQL Warning: relation '${tableName}.${relationName}' declares join columns that could not be matched to columns of '${tableName}' and '${relEntry.targetTableName}', so ${omitted} is not generated. The installed drizzle-orm probably stores relation columns in a shape this version of drizzle-graphql does not recognize.`);
|
|
1768
|
+
};
|
|
1769
|
+
/**
|
|
1770
|
+
* Extracts the join column info from a drizzle-orm v1 Relation object.
|
|
1771
|
+
* Returns the JS property name of the local column on the parent table and the
|
|
1772
|
+
* Column object for the foreign column on the target table, or undefined if the
|
|
1773
|
+
* relation internals are not accessible.
|
|
1774
|
+
*/
|
|
1775
|
+
const extractRelationJoinColumns = (relEntry, parentTable, targetTable) => {
|
|
1776
|
+
const rel = relEntry.relation ?? relEntry;
|
|
1777
|
+
const sourceCol = relationColumns(rel, "source")?.[0];
|
|
1778
|
+
const targetCol = relationColumns(rel, "target")?.[0];
|
|
1779
|
+
if (!sourceCol || !targetCol) return;
|
|
1780
|
+
const parentCols = getColumns(parentTable);
|
|
1781
|
+
const localColPropName = Object.entries(parentCols).find(([, c]) => c === sourceCol)?.[0];
|
|
1782
|
+
const targetCols = getColumns(targetTable);
|
|
1783
|
+
const foreignColPropName = Object.entries(targetCols).find(([, c]) => c === targetCol)?.[0];
|
|
1784
|
+
if (!localColPropName || !foreignColPropName) return;
|
|
1785
|
+
return {
|
|
1786
|
+
localColPropName,
|
|
1787
|
+
foreignCol: targetCol,
|
|
1788
|
+
foreignColPropName
|
|
1789
|
+
};
|
|
1790
|
+
};
|
|
1791
|
+
/**
|
|
1792
|
+
* Whether a to-one relation can back a relation orderBy hop. `.through()` (junction)
|
|
1793
|
+
* relations are excluded — the ordering subquery only joins the direct target, never the
|
|
1794
|
+
* junction table. (Relation *filters* do support `.through()` — this guard is orderBy-only.)
|
|
1795
|
+
*/
|
|
1796
|
+
const isFilterableRelation = (relation) => !relation.through;
|
|
1797
|
+
//#endregion
|
|
1458
1798
|
//#region src/util/builders/common/exclusions.ts
|
|
1459
1799
|
/**
|
|
1460
1800
|
* Columns this build keeps out of the generated schema, keyed by the table object.
|
|
@@ -1630,8 +1970,8 @@ const relationFilterCtx = (base, tableKey) => {
|
|
|
1630
1970
|
* the parent is an aliased proxy (as it is inside a relational `with:` where callback).
|
|
1631
1971
|
*/
|
|
1632
1972
|
const buildRelationJoinCondition = (parentTable, relation, aliasedTarget, relationName) => {
|
|
1633
|
-
const sourceColumns = relation
|
|
1634
|
-
const targetColumns = relation
|
|
1973
|
+
const sourceColumns = relationColumns(relation, "source");
|
|
1974
|
+
const targetColumns = relationColumns(relation, "target");
|
|
1635
1975
|
if (!sourceColumns?.length || sourceColumns.length !== targetColumns?.length) throw drizzleError(`WHERE ${relationName}: Relation cannot be used as a filter`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1636
1976
|
const parentColumns = Object.values(getColumns(parentTable));
|
|
1637
1977
|
const targetColumnsByName = Object.values(getColumns(aliasedTarget));
|
|
@@ -1655,7 +1995,7 @@ const resolveJunctionColumn = (aliasedThrough, junctionRef, relationName) => {
|
|
|
1655
1995
|
const key = junctionRef?._?.key;
|
|
1656
1996
|
const byKey = key ? throughColumns[key] : void 0;
|
|
1657
1997
|
if (byKey) return byKey;
|
|
1658
|
-
const columnName = junctionRef?.
|
|
1998
|
+
const columnName = unwrapRelationColumn(junctionRef)?.name;
|
|
1659
1999
|
const byName = columnName ? Object.values(throughColumns).find((c) => c.name === columnName) : void 0;
|
|
1660
2000
|
if (!byName) throw drizzleError(`WHERE ${relationName}: Relation cannot be used as a filter`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1661
2001
|
return byName;
|
|
@@ -1669,8 +2009,8 @@ const resolveJunctionColumn = (aliasedThrough, junctionRef, relationName) => {
|
|
|
1669
2009
|
* `through` metadata.
|
|
1670
2010
|
*/
|
|
1671
2011
|
const buildThroughJoinConditions = (parentTable, relation, aliasedThrough, aliasedTarget, relationName) => {
|
|
1672
|
-
const sourceColumns = relation
|
|
1673
|
-
const targetColumns = relation
|
|
2012
|
+
const sourceColumns = relationColumns(relation, "source");
|
|
2013
|
+
const targetColumns = relationColumns(relation, "target");
|
|
1674
2014
|
const through = relation.through;
|
|
1675
2015
|
if (!sourceColumns?.length || !targetColumns?.length || through?.source.length !== sourceColumns.length || through.target.length !== targetColumns.length) throw drizzleError(`WHERE ${relationName}: Relation cannot be used as a filter`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1676
2016
|
const parentColumns = Object.values(getColumns(parentTable));
|
|
@@ -1722,7 +2062,8 @@ const buildRelationExists = (parentTable, relationName, relEntry, innerFilters,
|
|
|
1722
2062
|
fromClause = getTableAsAliasSQL(aliasedTarget);
|
|
1723
2063
|
joinCondition = buildRelationJoinCondition(parentTable, relation, aliasedTarget, relationName);
|
|
1724
2064
|
}
|
|
1725
|
-
const
|
|
2065
|
+
const ownWhere = relationOwnWhere(relation);
|
|
2066
|
+
const relationWhere = ownWhere ? relationsFilterToSQL(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
|
|
1726
2067
|
const inner = innerFilters ? extractFilters(aliasedTarget, targetTableName, innerFilters, {
|
|
1727
2068
|
...ctx,
|
|
1728
2069
|
tableKey: targetTableName,
|
|
@@ -1776,7 +2117,8 @@ const extractFilters = (table, tableName, filters, relationCtx) => {
|
|
|
1776
2117
|
}
|
|
1777
2118
|
if (NOT) {
|
|
1778
2119
|
const extracted = extractFilters(table, tableName, NOT, relationCtx);
|
|
1779
|
-
|
|
2120
|
+
const negated = extracted ? not(extracted) : void 0;
|
|
2121
|
+
if (negated) variants.push(negated);
|
|
1780
2122
|
}
|
|
1781
2123
|
if (OR?.length) {
|
|
1782
2124
|
const orVariants = [];
|
|
@@ -2173,88 +2515,6 @@ const excludedColumnRef = (columnName) => sql`excluded.${sql.identifier(columnNa
|
|
|
2173
2515
|
/** `values(<column>)` — MySQL's equivalent inside ON DUPLICATE KEY UPDATE. */
|
|
2174
2516
|
const mysqlValuesColumnRef = (columnName) => sql`values(${sql.identifier(columnName)})`;
|
|
2175
2517
|
//#endregion
|
|
2176
|
-
//#region src/util/builders/common/relations.ts
|
|
2177
|
-
/**
|
|
2178
|
-
* Flatten drizzle-orm v1 TablesRelationalConfig into the canonical
|
|
2179
|
-
* Record<tableName, Record<relName, TableNamedRelations>> shape used
|
|
2180
|
-
* throughout common.ts. Both pg.ts and sqlite.ts call this before
|
|
2181
|
-
* passing the relation map to any shared function.
|
|
2182
|
-
*/
|
|
2183
|
-
const buildNamedRelations = (relations, tableEntries) => {
|
|
2184
|
-
const namedRelations = {};
|
|
2185
|
-
for (const [relTableName, relConfig] of Object.entries(relations)) {
|
|
2186
|
-
if (!relConfig?.relations) continue;
|
|
2187
|
-
const namedConfig = {};
|
|
2188
|
-
for (const [innerRelName, innerRelValue] of Object.entries(relConfig.relations)) {
|
|
2189
|
-
const targetTable = innerRelValue.targetTable ?? innerRelValue.referencedTable;
|
|
2190
|
-
const directTargetName = innerRelValue.targetTableName;
|
|
2191
|
-
let targetTableName;
|
|
2192
|
-
if (directTargetName) targetTableName = tableEntries.find(([key]) => key === directTargetName)?.[0];
|
|
2193
|
-
else if (targetTable) targetTableName = tableEntries.find(([, tableValue]) => tableValue === targetTable)?.[0];
|
|
2194
|
-
if (!targetTableName) continue;
|
|
2195
|
-
namedConfig[innerRelName] = {
|
|
2196
|
-
relation: innerRelValue,
|
|
2197
|
-
targetTableName
|
|
2198
|
-
};
|
|
2199
|
-
}
|
|
2200
|
-
if (Object.keys(namedConfig).length > 0) namedRelations[relTableName] = namedConfig;
|
|
2201
|
-
}
|
|
2202
|
-
return namedRelations;
|
|
2203
|
-
};
|
|
2204
|
-
/**
|
|
2205
|
-
* Records each relation's target-table primary-key property names on the relation entry,
|
|
2206
|
-
* so the pagination paths (the window-function batch loader and the eager `with:` orderBy
|
|
2207
|
-
* default) can fall back to a deterministic PK order without re-deriving it per request.
|
|
2208
|
-
*
|
|
2209
|
-
* Composite primary keys are only visible through the dialect's getTableConfig, so the
|
|
2210
|
-
* dialect builder passes a `resolvePkNames` that threads the composite column names in.
|
|
2211
|
-
* Mutates the relation entries in place (they are shared with the pruned eager map and
|
|
2212
|
-
* the resolver factory, so attaching once covers every consumer).
|
|
2213
|
-
*/
|
|
2214
|
-
const attachTargetPrimaryKeys = (namedRelations, tables, resolvePkNames) => {
|
|
2215
|
-
const cache = /* @__PURE__ */ new Map();
|
|
2216
|
-
for (const rels of Object.values(namedRelations)) for (const relEntry of Object.values(rels)) {
|
|
2217
|
-
const { targetTableName } = relEntry;
|
|
2218
|
-
let pk = cache.get(targetTableName);
|
|
2219
|
-
if (!pk) {
|
|
2220
|
-
const targetTable = tables[targetTableName];
|
|
2221
|
-
pk = targetTable ? resolvePkNames(targetTable) : [];
|
|
2222
|
-
cache.set(targetTableName, pk);
|
|
2223
|
-
}
|
|
2224
|
-
relEntry.targetPkNames = pk;
|
|
2225
|
-
}
|
|
2226
|
-
};
|
|
2227
|
-
/**
|
|
2228
|
-
* Extracts the join column info from a drizzle-orm v1 Relation object.
|
|
2229
|
-
* Returns the JS property name of the local column on the parent table and the
|
|
2230
|
-
* Column object for the foreign column on the target table, or undefined if the
|
|
2231
|
-
* relation internals are not accessible.
|
|
2232
|
-
*/
|
|
2233
|
-
const extractRelationJoinColumns = (relEntry, parentTable, targetTable) => {
|
|
2234
|
-
const rel = relEntry.relation ?? relEntry;
|
|
2235
|
-
const sourceColumns = rel.sourceColumns;
|
|
2236
|
-
const targetColumns = rel.targetColumns;
|
|
2237
|
-
if (!sourceColumns?.length || !targetColumns?.length) return;
|
|
2238
|
-
const sourceCol = sourceColumns[0];
|
|
2239
|
-
const targetCol = targetColumns[0];
|
|
2240
|
-
const parentCols = getColumns(parentTable);
|
|
2241
|
-
const localColPropName = Object.entries(parentCols).find(([, c]) => c === sourceCol)?.[0];
|
|
2242
|
-
const targetCols = getColumns(targetTable);
|
|
2243
|
-
const foreignColPropName = Object.entries(targetCols).find(([, c]) => c === targetCol)?.[0];
|
|
2244
|
-
if (!localColPropName || !foreignColPropName) return;
|
|
2245
|
-
return {
|
|
2246
|
-
localColPropName,
|
|
2247
|
-
foreignCol: targetCol,
|
|
2248
|
-
foreignColPropName
|
|
2249
|
-
};
|
|
2250
|
-
};
|
|
2251
|
-
/**
|
|
2252
|
-
* Whether a to-one relation can back a relation orderBy hop. `.through()` (junction)
|
|
2253
|
-
* relations are excluded — the ordering subquery only joins the direct target, never the
|
|
2254
|
-
* junction table. (Relation *filters* do support `.through()` — this guard is orderBy-only.)
|
|
2255
|
-
*/
|
|
2256
|
-
const isFilterableRelation = (relation) => !relation.through;
|
|
2257
|
-
//#endregion
|
|
2258
2518
|
//#region src/util/builders/common/order-by.ts
|
|
2259
2519
|
/**
|
|
2260
2520
|
* Column name / direction / nulls triples from an `orderBy` argument, highest priority
|
|
@@ -2300,7 +2560,8 @@ const extendRelationOrderChain = (parentTable, relationName, relEntry, ctx, chai
|
|
|
2300
2560
|
ctx.aliases ??= { n: 0 };
|
|
2301
2561
|
const aliasedTarget = aliasedTable(targetTable, `dgql_ord_${ctx.aliases.n++}`);
|
|
2302
2562
|
const joinCondition = buildRelationJoinCondition(parentTable, relation, aliasedTarget, relationName);
|
|
2303
|
-
const
|
|
2563
|
+
const ownWhere = relationOwnWhere(relation);
|
|
2564
|
+
const relationWhere = ownWhere ? relationsFilterToSQL(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
|
|
2304
2565
|
return {
|
|
2305
2566
|
fromParts: [...chain?.fromParts ?? [], getTableAsAliasSQL(aliasedTarget)],
|
|
2306
2567
|
conditions: [
|
|
@@ -2317,15 +2578,17 @@ const extendRelationOrderChain = (parentTable, relationName, relEntry, ctx, chai
|
|
|
2317
2578
|
*
|
|
2318
2579
|
* A ladder rather than a dialect array function (`array_position` on Postgres, `FIELD()` on
|
|
2319
2580
|
* MySQL, neither on SQLite) because it is the one form all three share, and because every
|
|
2320
|
-
* list value stays a bound parameter
|
|
2321
|
-
*
|
|
2322
|
-
*
|
|
2581
|
+
* list value stays a bound parameter — encoded by the column, as `eq` would, so a value whose
|
|
2582
|
+
* driver form differs from its JS one (a SQLite integer timestamp's `Date`) is bound as stored.
|
|
2583
|
+
* The branch indices are library-generated integers, so they are written into the SQL text — a
|
|
2584
|
+
* bound parameter there would leave Postgres unable to infer the CASE result type. Values
|
|
2585
|
+
* outside the list sort after every listed one.
|
|
2323
2586
|
*/
|
|
2324
2587
|
const filterOrderExpression = (column, columnName, whereArgs) => {
|
|
2325
2588
|
const values = whereArgs?.[columnName]?.inArray;
|
|
2326
2589
|
if (!Array.isArray(values)) throw drizzleError(`ORDER BY ${columnName}: 'matchFilterOrder' needs an 'inArray' filter on the same column in this query's 'where'`, { code: "DRIZZLE_INVALID_ORDER_BY" });
|
|
2327
2590
|
if (!values.length) return sql`0`;
|
|
2328
|
-
const branches = values.map((value, index) => sql`when ${column} = ${remapFromGraphQLCore(value, column, columnName)} then ${sql.raw(String(index))}`);
|
|
2591
|
+
const branches = values.map((value, index) => sql`when ${column} = ${sql.param(remapFromGraphQLCore(value, column, columnName), column)} then ${sql.raw(String(index))}`);
|
|
2329
2592
|
return sql`case ${sql.join(branches, sql` `)} else ${sql.raw(String(values.length))} end`;
|
|
2330
2593
|
};
|
|
2331
2594
|
/**
|
|
@@ -3291,6 +3554,26 @@ const remapUpdateInput = (values, table, tableName) => {
|
|
|
3291
3554
|
} : remapped;
|
|
3292
3555
|
};
|
|
3293
3556
|
//#endregion
|
|
3557
|
+
//#region src/util/builders/common/rqb-config.ts
|
|
3558
|
+
/**
|
|
3559
|
+
* A copy of a relational query config with every key whose value is `undefined` left out.
|
|
3560
|
+
*
|
|
3561
|
+
* drizzle-orm rc.5 snapshots test `"where" in params` rather than its value, and throw
|
|
3562
|
+
* `Unexpected 'undefined' in filter value` for `{ where: undefined }`; rc.4 read the value and
|
|
3563
|
+
* treated both the same. The library builds its configs with conditional keys
|
|
3564
|
+
* (`where: cond ? … : undefined`), so every config goes through here before it reaches drizzle.
|
|
3565
|
+
* Only `where` is checked that way today, but leaving out every undefined key costs nothing
|
|
3566
|
+
* and keeps the next key that gets the same treatment from breaking queries.
|
|
3567
|
+
*
|
|
3568
|
+
* Shallow on purpose: nested `with:` entries are built (and passed through here) one level at
|
|
3569
|
+
* a time, and the values of the other keys are drizzle's own (column maps, callbacks, RAW).
|
|
3570
|
+
*/
|
|
3571
|
+
const omitUndefinedKeys = (config) => {
|
|
3572
|
+
const out = {};
|
|
3573
|
+
for (const [key, value] of Object.entries(config)) if (value !== void 0) out[key] = value;
|
|
3574
|
+
return out;
|
|
3575
|
+
};
|
|
3576
|
+
//#endregion
|
|
3294
3577
|
//#region src/util/builders/common/selected-columns.ts
|
|
3295
3578
|
const rqbCrashTypes = [
|
|
3296
3579
|
"SQLiteBigInt",
|
|
@@ -3419,7 +3702,7 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
|
|
|
3419
3702
|
thisRecord.offset = offset;
|
|
3420
3703
|
thisRecord.limit = limit;
|
|
3421
3704
|
thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
|
|
3422
|
-
args[relName] = thisRecord;
|
|
3705
|
+
args[relName] = omitUndefinedKeys(thisRecord);
|
|
3423
3706
|
}
|
|
3424
3707
|
return args;
|
|
3425
3708
|
};
|
|
@@ -3496,12 +3779,12 @@ const runRelationalSelect = async (opts) => {
|
|
|
3496
3779
|
}) : void 0
|
|
3497
3780
|
};
|
|
3498
3781
|
if (single) {
|
|
3499
|
-
const result = await queryBase.findFirst(params);
|
|
3782
|
+
const result = await queryBase.findFirst(omitUndefinedKeys(params));
|
|
3500
3783
|
return result ? remapToGraphQLSingleOutput(result, tableName, table, relationMap) : void 0;
|
|
3501
3784
|
}
|
|
3502
3785
|
if (cursorEntries && cursorSelected) for (const [column] of cursorEntries) params.columns[column] = true;
|
|
3503
3786
|
params.limit = distinctKeys ? void 0 : opts.limit;
|
|
3504
|
-
const result = await queryBase.findMany(params);
|
|
3787
|
+
const result = await queryBase.findMany(omitUndefinedKeys(params));
|
|
3505
3788
|
if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
|
|
3506
3789
|
return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
|
|
3507
3790
|
};
|
|
@@ -5227,7 +5510,6 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5227
5510
|
const missing = [!tableFeatures.insert ? "insert" : void 0, !tableFeatures.update ? "update" : void 0].filter(Boolean).join(" and ");
|
|
5228
5511
|
if (missing) noteConflict(`upsert is on while ${missing} is off, so the upsert mutations are a second write path past the operation you turned off`, tableName);
|
|
5229
5512
|
}
|
|
5230
|
-
if (config?.features?.nestedWrites && (!tableFeatures.insert || !tableFeatures.update)) noteConflict("nestedWrites is on while this table's own insert or update is off, so a nested `create` or `connect` under another table's mutation can still write it", tableName);
|
|
5231
5513
|
if (config?.features?.updateMany !== void 0 && tableFeatures.updateMany && !tableFeatures.update) noteConflict("updateMany is on while update is off, so no batch update is generated", tableName);
|
|
5232
5514
|
if (config?.features?.groupBy !== void 0 && tableFeatures.groupBy && !tableFeatures.aggregates) noteConflict("groupBy is on while aggregates is off, so no grouped query is generated", tableName);
|
|
5233
5515
|
if (tableFeatures.countMutations && !tableFeatures.update && !tableFeatures.delete) noteConflict("countMutations is on while update and delete are both off, so no count mutation is generated", tableName);
|
|
@@ -5540,13 +5822,21 @@ const GROUP_KEY = "__dgql_group_key";
|
|
|
5540
5822
|
* for every other aggregate.
|
|
5541
5823
|
*/
|
|
5542
5824
|
const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, filterCtx, policies) => {
|
|
5825
|
+
const warned = /* @__PURE__ */ new Set();
|
|
5543
5826
|
return ({ tableName, relationName, relEntry }) => {
|
|
5544
5827
|
const parentTable = tables[tableName];
|
|
5545
5828
|
const targetTableName = relEntry.targetTableName;
|
|
5546
5829
|
const targetTable = tables[targetTableName];
|
|
5547
5830
|
if (!parentTable || !targetTable) return;
|
|
5548
5831
|
const joinCols = extractRelationJoinColumns(relEntry, parentTable, targetTable);
|
|
5549
|
-
if (!joinCols)
|
|
5832
|
+
if (!joinCols) {
|
|
5833
|
+
const key = `${tableName}.${relationName}`;
|
|
5834
|
+
if (!warned.has(key)) {
|
|
5835
|
+
warned.add(key);
|
|
5836
|
+
warnUnresolvedJoin(relEntry, tableName, relationName, `its '${relationName}Aggregate' field`);
|
|
5837
|
+
}
|
|
5838
|
+
return;
|
|
5839
|
+
}
|
|
5550
5840
|
const { localColPropName, foreignCol } = joinCols;
|
|
5551
5841
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
|
|
5552
5842
|
const targetTypeName = resolveTypeName(targetTableName, typeNameMapper);
|
|
@@ -5833,6 +6123,12 @@ const generateGroupBy = (db, tableName, table, typeName, fieldName, filterArgs,
|
|
|
5833
6123
|
};
|
|
5834
6124
|
//#endregion
|
|
5835
6125
|
//#region src/util/builders/nested-writes.ts
|
|
6126
|
+
const ALL_OPS = [
|
|
6127
|
+
"create",
|
|
6128
|
+
"connect",
|
|
6129
|
+
"disconnect",
|
|
6130
|
+
"set"
|
|
6131
|
+
];
|
|
5836
6132
|
/**
|
|
5837
6133
|
* Whether a column can be left out of an INSERT — either the database fills it in or it
|
|
5838
6134
|
* accepts NULL.
|
|
@@ -5844,12 +6140,6 @@ const propNameOf = (table, column) => {
|
|
|
5844
6140
|
for (const [propName, candidate] of Object.entries(getColumns(table))) if (candidate === column) return propName;
|
|
5845
6141
|
};
|
|
5846
6142
|
/**
|
|
5847
|
-
* The junction column one entry of a relation's `through` config points at. Drizzle wraps it
|
|
5848
|
-
* in a descriptor while the relation is being defined; the raw column is accepted too, so a
|
|
5849
|
-
* change of shape there degrades to skipping the relation rather than mis-planning it.
|
|
5850
|
-
*/
|
|
5851
|
-
const throughColumn = (entry) => entry && is(entry, Column) ? entry : entry?._?.column;
|
|
5852
|
-
/**
|
|
5853
6143
|
* A `.through()` relation's plan: the junction table plus the two columns it joins on.
|
|
5854
6144
|
*
|
|
5855
6145
|
* Nothing about the parent or the target row changes when a many-to-many link is made or
|
|
@@ -5860,21 +6150,27 @@ const throughColumn = (entry) => entry && is(entry, Column) ? entry : entry?._?.
|
|
|
5860
6150
|
* row" guarantee lives in the junction's constraints rather than in the relation; a relation
|
|
5861
6151
|
* joining on more than one column per side; and a junction carrying a further NOT NULL column
|
|
5862
6152
|
* with no default, which no `connect` operand could supply a value for.
|
|
6153
|
+
*
|
|
6154
|
+
* `onUnresolved` is called when the relation's columns are of a supported shape but cannot be
|
|
6155
|
+
* matched to the three tables — a drizzle-orm mismatch rather than an unsupported relation.
|
|
5863
6156
|
*/
|
|
5864
|
-
const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable) => {
|
|
6157
|
+
const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable, tableNameOf, onUnresolved) => {
|
|
5865
6158
|
if (is(relation, One)) return;
|
|
5866
6159
|
const junction = relation.throughTable;
|
|
5867
6160
|
const sourceEntries = relation.through?.source;
|
|
5868
6161
|
const targetEntries = relation.through?.target;
|
|
5869
6162
|
if (!junction || sourceEntries?.length !== 1 || targetEntries?.length !== 1) return;
|
|
5870
6163
|
if (relation.sourceColumns?.length !== 1 || relation.targetColumns?.length !== 1) return;
|
|
5871
|
-
const localCol = relation.sourceColumns[0];
|
|
5872
|
-
const foreignCol = relation.targetColumns[0];
|
|
6164
|
+
const localCol = unwrapRelationColumn(relation.sourceColumns[0]);
|
|
6165
|
+
const foreignCol = unwrapRelationColumn(relation.targetColumns[0]);
|
|
5873
6166
|
const localColPropName = propNameOf(table, localCol);
|
|
5874
6167
|
const foreignColPropName = propNameOf(targetTable, foreignCol);
|
|
5875
|
-
const sourceColPropName = propNameOf(junction,
|
|
5876
|
-
const targetColPropName = propNameOf(junction,
|
|
5877
|
-
if (!localColPropName || !foreignColPropName || !sourceColPropName || !targetColPropName)
|
|
6168
|
+
const sourceColPropName = propNameOf(junction, unwrapRelationColumn(sourceEntries[0]));
|
|
6169
|
+
const targetColPropName = propNameOf(junction, unwrapRelationColumn(targetEntries[0]));
|
|
6170
|
+
if (!localCol || !foreignCol || !localColPropName || !foreignColPropName || !sourceColPropName || !targetColPropName) {
|
|
6171
|
+
onUnresolved();
|
|
6172
|
+
return;
|
|
6173
|
+
}
|
|
5878
6174
|
const junctionColumns = getColumns(junction);
|
|
5879
6175
|
const sourceCol = junctionColumns[sourceColPropName];
|
|
5880
6176
|
const targetCol = junctionColumns[targetColPropName];
|
|
@@ -5892,11 +6188,18 @@ const buildThroughPlan = (relationName, relation, table, targetTableName, target
|
|
|
5892
6188
|
canDetach: true,
|
|
5893
6189
|
through: {
|
|
5894
6190
|
table: junction,
|
|
6191
|
+
tableName: tableNameOf(junction),
|
|
5895
6192
|
sourceColPropName,
|
|
5896
6193
|
targetColPropName,
|
|
5897
6194
|
sourceCol,
|
|
5898
6195
|
targetCol
|
|
5899
|
-
}
|
|
6196
|
+
},
|
|
6197
|
+
targetKeyColumns: { [foreignColPropName]: foreignCol },
|
|
6198
|
+
allowed: /* @__PURE__ */ new Set([
|
|
6199
|
+
"connect",
|
|
6200
|
+
"disconnect",
|
|
6201
|
+
"set"
|
|
6202
|
+
])
|
|
5900
6203
|
};
|
|
5901
6204
|
};
|
|
5902
6205
|
/**
|
|
@@ -5909,7 +6212,7 @@ const buildThroughPlan = (relationName, relation, table, targetTableName, target
|
|
|
5909
6212
|
* relations whose join columns cannot be read off the drizzle relation; and relations where
|
|
5910
6213
|
* neither join column is unique, which leaves no way to tell which side stores the key.
|
|
5911
6214
|
*/
|
|
5912
|
-
const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePrimaryKeys, extractJoinColumns) => {
|
|
6215
|
+
const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePrimaryKeys, extractJoinColumns, tableNameOf = () => void 0) => {
|
|
5913
6216
|
const uniqueCache = /* @__PURE__ */ new Map();
|
|
5914
6217
|
const singleColumnUniques = (tableName, table) => {
|
|
5915
6218
|
let cached = uniqueCache.get(tableName);
|
|
@@ -5937,13 +6240,17 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
5937
6240
|
const targetTableName = relEntry.targetTableName;
|
|
5938
6241
|
const targetTable = tables[targetTableName];
|
|
5939
6242
|
if (!targetTable) continue;
|
|
6243
|
+
const omitted = `the '${relationName}' field of its nested-write create and update inputs`;
|
|
5940
6244
|
if (relation.through) {
|
|
5941
|
-
const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable);
|
|
6245
|
+
const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable, tableNameOf, () => warnUnresolvedJoin(relEntry, tableName, relationName, omitted));
|
|
5942
6246
|
if (throughPlan) tablePlans[relationName] = throughPlan;
|
|
5943
6247
|
continue;
|
|
5944
6248
|
}
|
|
5945
6249
|
const joinColumns = extractJoinColumns(relEntry, table, targetTable);
|
|
5946
|
-
if (!joinColumns)
|
|
6250
|
+
if (!joinColumns) {
|
|
6251
|
+
warnUnresolvedJoin(relEntry, tableName, relationName, omitted);
|
|
6252
|
+
continue;
|
|
6253
|
+
}
|
|
5947
6254
|
const { localColPropName, foreignCol, foreignColPropName } = joinColumns;
|
|
5948
6255
|
const localCol = getColumns(table)[localColPropName];
|
|
5949
6256
|
if (!localCol) continue;
|
|
@@ -5959,6 +6266,10 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
5959
6266
|
else if (foreignIsUnique) fkSide = "parent";
|
|
5960
6267
|
else if (localIsUnique) fkSide = "child";
|
|
5961
6268
|
else continue;
|
|
6269
|
+
const fkColumn = fkSide === "child" ? foreignCol : localCol;
|
|
6270
|
+
const targetColumns = getColumns(targetTable);
|
|
6271
|
+
const targetKeyColumns = Object.fromEntries(resolvePrimaryKeys(targetTable).filter((name) => targetColumns[name]).map((name) => [name, targetColumns[name]]));
|
|
6272
|
+
targetKeyColumns[foreignColPropName] = foreignCol;
|
|
5962
6273
|
tablePlans[relationName] = {
|
|
5963
6274
|
relationName,
|
|
5964
6275
|
targetTableName,
|
|
@@ -5969,13 +6280,87 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
5969
6280
|
localCol,
|
|
5970
6281
|
isOne,
|
|
5971
6282
|
fkSide,
|
|
5972
|
-
canDetach: !
|
|
6283
|
+
canDetach: !fkColumn.notNull,
|
|
6284
|
+
targetKeyColumns,
|
|
6285
|
+
allowed: new Set(!fkColumn.notNull ? isOne ? [
|
|
6286
|
+
"create",
|
|
6287
|
+
"connect",
|
|
6288
|
+
"disconnect"
|
|
6289
|
+
] : ALL_OPS : ["create", "connect"])
|
|
5973
6290
|
};
|
|
5974
6291
|
}
|
|
5975
6292
|
if (Object.keys(tablePlans).length) plans[tableName] = tablePlans;
|
|
5976
6293
|
}
|
|
5977
6294
|
return plans;
|
|
5978
6295
|
};
|
|
6296
|
+
/**
|
|
6297
|
+
* Which of the target's (or junction's) write features each input of a relation needs. A
|
|
6298
|
+
* parent-side `connect` / `disconnect` only rewrites the parent's own column, so it needs
|
|
6299
|
+
* nothing of the target; a child-side one moves the target's foreign key, which is an update
|
|
6300
|
+
* of the target. A to-one child-side `create` also detaches the row attached before it, when
|
|
6301
|
+
* the key is nullable.
|
|
6302
|
+
*/
|
|
6303
|
+
const requiredWrites = (plan, op) => {
|
|
6304
|
+
if (plan.through) {
|
|
6305
|
+
const junction = plan.through.tableName;
|
|
6306
|
+
if (op === "connect") return [{
|
|
6307
|
+
tableName: junction,
|
|
6308
|
+
feature: "insert"
|
|
6309
|
+
}];
|
|
6310
|
+
if (op === "disconnect") return [{
|
|
6311
|
+
tableName: junction,
|
|
6312
|
+
feature: "delete"
|
|
6313
|
+
}];
|
|
6314
|
+
if (op === "set") return [{
|
|
6315
|
+
tableName: junction,
|
|
6316
|
+
feature: "delete"
|
|
6317
|
+
}, {
|
|
6318
|
+
tableName: junction,
|
|
6319
|
+
feature: "insert"
|
|
6320
|
+
}];
|
|
6321
|
+
return [];
|
|
6322
|
+
}
|
|
6323
|
+
const target = plan.targetTableName;
|
|
6324
|
+
if (op === "create") return plan.fkSide === "child" && plan.isOne && plan.canDetach ? [{
|
|
6325
|
+
tableName: target,
|
|
6326
|
+
feature: "insert"
|
|
6327
|
+
}, {
|
|
6328
|
+
tableName: target,
|
|
6329
|
+
feature: "update"
|
|
6330
|
+
}] : [{
|
|
6331
|
+
tableName: target,
|
|
6332
|
+
feature: "insert"
|
|
6333
|
+
}];
|
|
6334
|
+
return plan.fkSide === "child" ? [{
|
|
6335
|
+
tableName: target,
|
|
6336
|
+
feature: "update"
|
|
6337
|
+
}] : [];
|
|
6338
|
+
};
|
|
6339
|
+
/**
|
|
6340
|
+
* Narrows every plan's `allowed` inputs to the ones whose writes the table they land on
|
|
6341
|
+
* permits: its `features.insert` / `update` / `delete`, and for a junction table that it is
|
|
6342
|
+
* not excluded. A plan left with nothing is dropped, so its relation field is not generated.
|
|
6343
|
+
*
|
|
6344
|
+
* Returns one line per input taken out, for the build note.
|
|
6345
|
+
*/
|
|
6346
|
+
const gateNestedWritePlans = (plans, featureOf, isExcluded) => {
|
|
6347
|
+
const omitted = [];
|
|
6348
|
+
for (const [tableName, tablePlans] of Object.entries(plans)) {
|
|
6349
|
+
for (const [relationName, plan] of Object.entries(tablePlans)) {
|
|
6350
|
+
for (const op of [...plan.allowed]) {
|
|
6351
|
+
const blocked = requiredWrites(plan, op).filter(({ tableName: target, feature }) => target !== void 0 && (isExcluded(target) || !featureOf(target)[feature]));
|
|
6352
|
+
if (blocked.length) {
|
|
6353
|
+
plan.allowed.delete(op);
|
|
6354
|
+
const reasons = blocked.map(({ tableName: target, feature }) => isExcluded(target) ? `${target} is excluded` : `${target}.${feature} is off`);
|
|
6355
|
+
omitted.push(`${tableName}.${relationName}.${op} (${[...new Set(reasons)].join(", ")})`);
|
|
6356
|
+
}
|
|
6357
|
+
}
|
|
6358
|
+
if (!plan.allowed.size) delete tablePlans[relationName];
|
|
6359
|
+
}
|
|
6360
|
+
if (!Object.keys(tablePlans).length) delete plans[tableName];
|
|
6361
|
+
}
|
|
6362
|
+
return omitted;
|
|
6363
|
+
};
|
|
5979
6364
|
const NO_COLUMNS = /* @__PURE__ */ new Set();
|
|
5980
6365
|
/**
|
|
5981
6366
|
* Builds the nested-write input types for a schema.
|
|
@@ -6030,8 +6415,11 @@ const createNestedWriteTypes = (params) => {
|
|
|
6030
6415
|
*
|
|
6031
6416
|
* A many-to-many relation offers no `create`: inserting a row *and* linking it is two
|
|
6032
6417
|
* writes to two tables, and the link half is what the relation field is for. Its
|
|
6033
|
-
* `disconnect` and `set`
|
|
6418
|
+
* `disconnect` and `set` do not depend on nullability, since breaking a link deletes a
|
|
6034
6419
|
* junction row rather than nulling a column that might be NOT NULL.
|
|
6420
|
+
*
|
|
6421
|
+
* Each input is also subject to `plan.allowed` — the table it writes has to have that
|
|
6422
|
+
* write turned on (see {@link gateNestedWritePlans}).
|
|
6035
6423
|
*/
|
|
6036
6424
|
const wrapperType = (tableName, typeName, plan, forUpdate) => {
|
|
6037
6425
|
const key = `${tableName}.${plan.relationName}.${forUpdate ? "update" : "create"}`;
|
|
@@ -6040,57 +6428,52 @@ const createNestedWriteTypes = (params) => {
|
|
|
6040
6428
|
const filters = targetFilters(plan);
|
|
6041
6429
|
const targetTypeName = resolveTypeName(plan.targetTableName, typeNameMapper);
|
|
6042
6430
|
const fields = {};
|
|
6431
|
+
const offers = (op) => plan.allowed.has(op);
|
|
6043
6432
|
if (plan.through) {
|
|
6044
|
-
if (
|
|
6045
|
-
fields["connect"] = {
|
|
6433
|
+
if (filters && offers("connect")) fields["connect"] = {
|
|
6046
6434
|
type: listOf(filters),
|
|
6047
6435
|
description: `Link every existing ${targetTypeName} row these match. Already-linked rows are left alone.`
|
|
6048
6436
|
};
|
|
6049
|
-
if (forUpdate) {
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
};
|
|
6058
|
-
}
|
|
6437
|
+
if (filters && forUpdate && offers("disconnect")) fields["disconnect"] = {
|
|
6438
|
+
type: listOf(filters),
|
|
6439
|
+
description: `Unlink every linked ${targetTypeName} row these match, leaving the rows in place`
|
|
6440
|
+
};
|
|
6441
|
+
if (filters && forUpdate && offers("set")) fields["set"] = {
|
|
6442
|
+
type: listOf(filters),
|
|
6443
|
+
description: `Replace the whole set: unlink everything linked, then link every ${targetTypeName} row these match. \`set: []\` clears the relation. Applied before \`disconnect\` and \`connect\`.`
|
|
6444
|
+
};
|
|
6059
6445
|
} else if (plan.isOne) {
|
|
6060
|
-
fields["create"] = {
|
|
6446
|
+
if (offers("create")) fields["create"] = {
|
|
6061
6447
|
type: payloadType(tableName, typeName, plan),
|
|
6062
6448
|
description: `Insert a ${targetTypeName} row and attach it`
|
|
6063
6449
|
};
|
|
6064
|
-
if (filters) fields["connect"] = {
|
|
6450
|
+
if (filters && offers("connect")) fields["connect"] = {
|
|
6065
6451
|
type: filters,
|
|
6066
6452
|
description: `Attach the existing ${targetTypeName} row this matches. Must match exactly one row.`
|
|
6067
6453
|
};
|
|
6068
|
-
if (forUpdate &&
|
|
6454
|
+
if (forUpdate && offers("disconnect")) fields["disconnect"] = {
|
|
6069
6455
|
type: GraphQLBoolean,
|
|
6070
6456
|
description: `Detach the currently attached ${targetTypeName} row, leaving it in place`
|
|
6071
6457
|
};
|
|
6072
6458
|
} else {
|
|
6073
|
-
fields["create"] = {
|
|
6459
|
+
if (offers("create")) fields["create"] = {
|
|
6074
6460
|
type: listOf(payloadType(tableName, typeName, plan)),
|
|
6075
6461
|
description: `Insert these ${targetTypeName} rows and attach them`
|
|
6076
6462
|
};
|
|
6077
|
-
if (filters) {
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
description: `Replace the whole set: detach everything attached, then attach every ${targetTypeName} row these match. Applied before \`disconnect\`, \`connect\` and \`create\`.`
|
|
6090
|
-
};
|
|
6091
|
-
}
|
|
6092
|
-
}
|
|
6463
|
+
if (filters && offers("connect")) fields["connect"] = {
|
|
6464
|
+
type: listOf(filters),
|
|
6465
|
+
description: `Attach every existing ${targetTypeName} row these match`
|
|
6466
|
+
};
|
|
6467
|
+
if (filters && forUpdate && offers("disconnect")) fields["disconnect"] = {
|
|
6468
|
+
type: listOf(filters),
|
|
6469
|
+
description: `Detach every attached ${targetTypeName} row these match, leaving the rows in place`
|
|
6470
|
+
};
|
|
6471
|
+
if (filters && forUpdate && offers("set")) fields["set"] = {
|
|
6472
|
+
type: listOf(filters),
|
|
6473
|
+
description: `Replace the whole set: detach everything attached, then attach every ${targetTypeName} row these match. Applied before \`disconnect\`, \`connect\` and \`create\`.`
|
|
6474
|
+
};
|
|
6093
6475
|
}
|
|
6476
|
+
if (!Object.keys(fields).length) return;
|
|
6094
6477
|
const type = new GraphQLInputObjectType({
|
|
6095
6478
|
name: cacheCtx.typeName({
|
|
6096
6479
|
kind: "nestedWriteInput",
|
|
@@ -6122,7 +6505,7 @@ const createNestedWriteTypes = (params) => {
|
|
|
6122
6505
|
const tablePlans = plans[tableName];
|
|
6123
6506
|
if (!tablePlans) return NO_COLUMNS;
|
|
6124
6507
|
const relaxed = /* @__PURE__ */ new Set();
|
|
6125
|
-
for (const plan of Object.values(tablePlans)) if (plan.fkSide === "parent" && !isOptionalOnInsert(plan.localCol)) relaxed.add(plan.localColPropName);
|
|
6508
|
+
for (const plan of Object.values(tablePlans)) if (plan.fkSide === "parent" && (plan.allowed.has("create") || plan.allowed.has("connect")) && !isOptionalOnInsert(plan.localCol)) relaxed.add(plan.localColPropName);
|
|
6126
6509
|
return relaxed.size ? relaxed : NO_COLUMNS;
|
|
6127
6510
|
}
|
|
6128
6511
|
};
|
|
@@ -6133,8 +6516,99 @@ const createNestedWriteTypes = (params) => {
|
|
|
6133
6516
|
*/
|
|
6134
6517
|
const mergedOps = (entries) => Object.assign({}, ...entries.map((entry) => entry.ops));
|
|
6135
6518
|
const asArray = (value) => Array.isArray(value) ? value : value === void 0 ? [] : [value];
|
|
6519
|
+
const supplied = (value) => value !== void 0 && value !== null;
|
|
6520
|
+
/**
|
|
6521
|
+
* The hook calls one relation field makes. Every step is registered before the field's first
|
|
6522
|
+
* statement, so each `before` runs ahead of all of them and each `after` once the last has
|
|
6523
|
+
* returned. A table without hooks registers nothing, and its statements run exactly as they
|
|
6524
|
+
* did before hooks reached nested writes — no RETURNING clause is added.
|
|
6525
|
+
*/
|
|
6526
|
+
const createHookSteps = (onWrite, executor, request, parentTable, plan) => {
|
|
6527
|
+
const steps = /* @__PURE__ */ new Map();
|
|
6528
|
+
const keyOf = (op, operation) => `${op}:${operation}`;
|
|
6529
|
+
const payload = (step) => ({
|
|
6530
|
+
table: step.tableName,
|
|
6531
|
+
operation: step.operation,
|
|
6532
|
+
single: plan.isOne,
|
|
6533
|
+
args: request?.args,
|
|
6534
|
+
context: request?.context,
|
|
6535
|
+
info: request?.info,
|
|
6536
|
+
tx: executor,
|
|
6537
|
+
nested: {
|
|
6538
|
+
parentTable,
|
|
6539
|
+
relation: plan.relationName,
|
|
6540
|
+
op: step.op
|
|
6541
|
+
}
|
|
6542
|
+
});
|
|
6543
|
+
return {
|
|
6544
|
+
/** Registers the hook call for `op`'s `operation` statements, if that table has hooks. */
|
|
6545
|
+
add: (op, operation) => {
|
|
6546
|
+
const through = plan.through;
|
|
6547
|
+
const tableName = through ? through.tableName : plan.targetTableName;
|
|
6548
|
+
const hooks = tableName ? onWrite?.(tableName, operation) : void 0;
|
|
6549
|
+
if (!tableName || !hooks || steps.has(keyOf(op, operation))) return;
|
|
6550
|
+
const keyColumns = through ? {
|
|
6551
|
+
[through.sourceColPropName]: through.sourceCol,
|
|
6552
|
+
[through.targetColPropName]: through.targetCol
|
|
6553
|
+
} : { ...plan.targetKeyColumns };
|
|
6554
|
+
steps.set(keyOf(op, operation), {
|
|
6555
|
+
tableName,
|
|
6556
|
+
operation,
|
|
6557
|
+
op,
|
|
6558
|
+
hooks,
|
|
6559
|
+
returning: withHookColumns(hooks, keyColumns).returning,
|
|
6560
|
+
rows: []
|
|
6561
|
+
});
|
|
6562
|
+
},
|
|
6563
|
+
get: (op, operation) => steps.get(keyOf(op, operation)),
|
|
6564
|
+
/** Runs one statement, returning its rows to the step it belongs to when there is one. */
|
|
6565
|
+
run: async (query, op, operation) => {
|
|
6566
|
+
const step = steps.get(keyOf(op, operation));
|
|
6567
|
+
if (!step) {
|
|
6568
|
+
await query;
|
|
6569
|
+
return;
|
|
6570
|
+
}
|
|
6571
|
+
step.rows.push(...await query.returning(step.returning));
|
|
6572
|
+
},
|
|
6573
|
+
before: async () => {
|
|
6574
|
+
for (const step of steps.values()) await runWriteHook(step.hooks, "before", payload(step));
|
|
6575
|
+
},
|
|
6576
|
+
after: async () => {
|
|
6577
|
+
for (const step of steps.values()) await runWriteHook(step.hooks, "after", {
|
|
6578
|
+
...payload(step),
|
|
6579
|
+
rows: step.rows
|
|
6580
|
+
});
|
|
6581
|
+
}
|
|
6582
|
+
};
|
|
6583
|
+
};
|
|
6584
|
+
/**
|
|
6585
|
+
* Registers the hook calls a child-side relation field will make, in the order its
|
|
6586
|
+
* statements run, so the `before` hooks fire in that order too. Mirrors the branches of
|
|
6587
|
+
* `applyChildSide` and `applyThroughOps` exactly: a step registered for an input that runs no
|
|
6588
|
+
* statement would fire with no rows, and one missing would miss them.
|
|
6589
|
+
*/
|
|
6590
|
+
const registerChildSteps = (steps, plan, op) => {
|
|
6591
|
+
if (plan.through) {
|
|
6592
|
+
if (supplied(op["set"])) {
|
|
6593
|
+
steps.add("set", "delete");
|
|
6594
|
+
steps.add("set", "insert");
|
|
6595
|
+
}
|
|
6596
|
+
if (supplied(op["disconnect"])) steps.add("disconnect", "delete");
|
|
6597
|
+
if (supplied(op["connect"])) steps.add("connect", "insert");
|
|
6598
|
+
return;
|
|
6599
|
+
}
|
|
6600
|
+
if (op["set"] !== void 0) steps.add("set", "update");
|
|
6601
|
+
if (op["disconnect"] !== void 0 && (!plan.isOne || op["disconnect"] === true)) steps.add("disconnect", "update");
|
|
6602
|
+
const attachedBy = attachingOp(op);
|
|
6603
|
+
if (plan.isOne && attachedBy && plan.canDetach && op["set"] === void 0) steps.add(attachedBy, "update");
|
|
6604
|
+
if (op["connect"] !== void 0) steps.add("connect", "update");
|
|
6605
|
+
if (op["create"] !== void 0) steps.add("create", "insert");
|
|
6606
|
+
};
|
|
6607
|
+
/** The input that attaches a row to a to-one, which detaches whatever was attached before. */
|
|
6608
|
+
const attachingOp = (op) => op["connect"] !== void 0 ? "connect" : op["create"] !== void 0 ? "create" : void 0;
|
|
6136
6609
|
const createNestedWriteRuntime = (params) => {
|
|
6137
6610
|
const { plans, filterCtx, policies, contextValues } = params;
|
|
6611
|
+
const onWrite = policies?.onWrite;
|
|
6138
6612
|
/**
|
|
6139
6613
|
* A `connect` / `disconnect` / `set` operand compiled to SQL. An operand that compiles to
|
|
6140
6614
|
* nothing is rejected: `connect: {}` would otherwise attach every row in the table.
|
|
@@ -6145,12 +6619,12 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6145
6619
|
return withScope(scope, plan.targetTableName, plan.targetTable, condition);
|
|
6146
6620
|
};
|
|
6147
6621
|
const assertSingleOperation = (plan, op) => {
|
|
6148
|
-
const
|
|
6622
|
+
const given = [
|
|
6149
6623
|
"create",
|
|
6150
6624
|
"connect",
|
|
6151
6625
|
"disconnect"
|
|
6152
|
-
].filter((key) => op[key]
|
|
6153
|
-
if (
|
|
6626
|
+
].filter((key) => supplied(op[key]));
|
|
6627
|
+
if (given.length > 1) throw drizzleError(`Drizzle-GraphQL Error: '${plan.relationName}' takes one of ${given.join(", ")} at a time — it holds a single row.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
6154
6628
|
};
|
|
6155
6629
|
/** The values the junction stores for the target rows one operand selects. */
|
|
6156
6630
|
const selectedTargetKeys = async (executor, plan, filter, operation, scope) => {
|
|
@@ -6165,9 +6639,8 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6165
6639
|
* error on some of them — the read and the write share the surrounding transaction, so no
|
|
6166
6640
|
* other writer can slip a duplicate in between them.
|
|
6167
6641
|
*/
|
|
6168
|
-
const applyThroughOps = async (executor, plan, key, op, scope) => {
|
|
6642
|
+
const applyThroughOps = async (executor, plan, key, op, scope, steps) => {
|
|
6169
6643
|
const through = plan.through;
|
|
6170
|
-
const unlinkAll = () => executor.delete(through.table).where(eq(through.sourceCol, key));
|
|
6171
6644
|
const link = async (operands, operation) => {
|
|
6172
6645
|
const existing = new Set((await executor.select({ [through.targetColPropName]: through.targetCol }).from(through.table).where(eq(through.sourceCol, key))).map((row) => row[through.targetColPropName]));
|
|
6173
6646
|
const rows = [];
|
|
@@ -6178,19 +6651,19 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6178
6651
|
[through.targetColPropName]: targetKey
|
|
6179
6652
|
});
|
|
6180
6653
|
}
|
|
6181
|
-
if (rows.length) await executor.insert(through.table).values(rows);
|
|
6654
|
+
if (rows.length) await steps.run(executor.insert(through.table).values(rows), operation, "insert");
|
|
6182
6655
|
};
|
|
6183
|
-
if (op["set"]
|
|
6184
|
-
await
|
|
6656
|
+
if (supplied(op["set"])) {
|
|
6657
|
+
await steps.run(executor.delete(through.table).where(eq(through.sourceCol, key)), "set", "delete");
|
|
6185
6658
|
await link(asArray(op["set"]), "set");
|
|
6186
6659
|
}
|
|
6187
|
-
if (op["disconnect"]
|
|
6660
|
+
if (supplied(op["disconnect"])) for (const filter of asArray(op["disconnect"])) {
|
|
6188
6661
|
const targetKeys = await selectedTargetKeys(executor, plan, filter, "disconnect", scope);
|
|
6189
|
-
if (targetKeys.length) await executor.delete(through.table).where(and(eq(through.sourceCol, key), inArray(through.targetCol, targetKeys)));
|
|
6662
|
+
if (targetKeys.length) await steps.run(executor.delete(through.table).where(and(eq(through.sourceCol, key), inArray(through.targetCol, targetKeys))), "disconnect", "delete");
|
|
6190
6663
|
}
|
|
6191
|
-
if (op["connect"]
|
|
6664
|
+
if (supplied(op["connect"])) await link(asArray(op["connect"]), "connect");
|
|
6192
6665
|
};
|
|
6193
|
-
const detachAll =
|
|
6666
|
+
const detachAll = (executor, plan, key, scope, steps, op) => steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where(withScope(scope, plan.targetTableName, plan.targetTable, eq(plan.foreignCol, key))), op, "update");
|
|
6194
6667
|
return {
|
|
6195
6668
|
enabled: (tableName) => !!plans[tableName],
|
|
6196
6669
|
split: (tableName, values) => {
|
|
@@ -6219,19 +6692,27 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6219
6692
|
}
|
|
6220
6693
|
return columns;
|
|
6221
6694
|
},
|
|
6222
|
-
applyParentSide: async (executor, tableName, ops,
|
|
6695
|
+
applyParentSide: async (executor, tableName, ops, request) => {
|
|
6223
6696
|
const tablePlans = plans[tableName];
|
|
6224
6697
|
const patch = {};
|
|
6225
6698
|
if (!tablePlans) return patch;
|
|
6699
|
+
const context = request?.context;
|
|
6226
6700
|
const scope = resolveScope(policies, context, filterCtx);
|
|
6227
6701
|
for (const [relationName, op] of Object.entries(ops)) {
|
|
6228
6702
|
const plan = tablePlans[relationName];
|
|
6229
6703
|
if (plan?.fkSide !== "parent") continue;
|
|
6230
6704
|
assertSingleOperation(plan, op);
|
|
6231
6705
|
if (op["create"] !== void 0) {
|
|
6706
|
+
const steps = createHookSteps(onWrite, executor, request, tableName, plan);
|
|
6707
|
+
steps.add("create", "insert");
|
|
6708
|
+
await steps.before();
|
|
6232
6709
|
const values = applyContextValues(remapFromGraphQLSingleInput({ ...op["create"] }, plan.targetTable), contextValues?.(plan.targetTableName), context);
|
|
6233
|
-
const
|
|
6710
|
+
const step = steps.get("create", "insert");
|
|
6711
|
+
const inserted = await executor.insert(plan.targetTable).values(values).returning(step ? step.returning : { [plan.foreignColPropName]: plan.foreignCol });
|
|
6712
|
+
step?.rows.push(...inserted);
|
|
6713
|
+
const created = inserted[0];
|
|
6234
6714
|
if (!created) throw drizzleError(`Drizzle-GraphQL Error: 'create' on '${relationName}' inserted no row, so there is nothing to attach.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
6715
|
+
await steps.after();
|
|
6235
6716
|
patch[plan.localColPropName] = created[plan.foreignColPropName];
|
|
6236
6717
|
} else if (op["connect"] !== void 0) {
|
|
6237
6718
|
const rows = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
|
|
@@ -6241,41 +6722,46 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6241
6722
|
}
|
|
6242
6723
|
return patch;
|
|
6243
6724
|
},
|
|
6244
|
-
applyChildSide: async (executor, tableName, ops, parentRows,
|
|
6725
|
+
applyChildSide: async (executor, tableName, ops, parentRows, request) => {
|
|
6245
6726
|
const tablePlans = plans[tableName];
|
|
6246
6727
|
if (!tablePlans) return;
|
|
6728
|
+
const context = request?.context;
|
|
6247
6729
|
const scope = resolveScope(policies, context, filterCtx);
|
|
6248
6730
|
for (const [relationName, op] of Object.entries(ops)) {
|
|
6249
6731
|
const plan = tablePlans[relationName];
|
|
6250
6732
|
if (plan?.fkSide !== "child") continue;
|
|
6251
6733
|
if (plan.isOne) assertSingleOperation(plan, op);
|
|
6734
|
+
const steps = createHookSteps(onWrite, executor, request, tableName, plan);
|
|
6735
|
+
registerChildSteps(steps, plan, op);
|
|
6736
|
+
await steps.before();
|
|
6252
6737
|
for (const parentRow of parentRows) {
|
|
6253
6738
|
const key = parentRow?.[plan.localColPropName];
|
|
6254
6739
|
if (key === void 0 || key === null) throw drizzleError(`Drizzle-GraphQL Error: cannot write through '${relationName}': the ${tableName} row has no '${plan.localColPropName}' value to attach to.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
6255
6740
|
if (plan.through) {
|
|
6256
|
-
await applyThroughOps(executor, plan, key, op, scope);
|
|
6741
|
+
await applyThroughOps(executor, plan, key, op, scope, steps);
|
|
6257
6742
|
continue;
|
|
6258
6743
|
}
|
|
6259
6744
|
if (op["set"] !== void 0) {
|
|
6260
|
-
await detachAll(executor, plan, key, scope);
|
|
6261
|
-
for (const filter of asArray(op["set"])) await executor.update(plan.targetTable).set({ [plan.foreignColPropName]: key }).where(conditionOf(plan, filter, "set", scope));
|
|
6745
|
+
await detachAll(executor, plan, key, scope, steps, "set");
|
|
6746
|
+
for (const filter of asArray(op["set"])) await steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: key }).where(conditionOf(plan, filter, "set", scope)), "set", "update");
|
|
6262
6747
|
}
|
|
6263
6748
|
if (op["disconnect"] !== void 0) {
|
|
6264
6749
|
if (plan.isOne) {
|
|
6265
|
-
if (op["disconnect"] === true) await detachAll(executor, plan, key, scope);
|
|
6266
|
-
} else for (const filter of asArray(op["disconnect"])) await executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where(and(eq(plan.foreignCol, key), conditionOf(plan, filter, "disconnect", scope)));
|
|
6750
|
+
if (op["disconnect"] === true) await detachAll(executor, plan, key, scope, steps, "disconnect");
|
|
6751
|
+
} else for (const filter of asArray(op["disconnect"])) await steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where(and(eq(plan.foreignCol, key), conditionOf(plan, filter, "disconnect", scope))), "disconnect", "update");
|
|
6267
6752
|
}
|
|
6268
|
-
const
|
|
6269
|
-
if (plan.isOne &&
|
|
6270
|
-
if (op["connect"] !== void 0) for (const filter of asArray(op["connect"])) await executor.update(plan.targetTable).set({ [plan.foreignColPropName]: key }).where(conditionOf(plan, filter, "connect", scope));
|
|
6753
|
+
const attachedBy = attachingOp(op);
|
|
6754
|
+
if (plan.isOne && attachedBy && plan.canDetach && op["set"] === void 0) await detachAll(executor, plan, key, scope, steps, attachedBy);
|
|
6755
|
+
if (op["connect"] !== void 0) for (const filter of asArray(op["connect"])) await steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: key }).where(conditionOf(plan, filter, "connect", scope)), "connect", "update");
|
|
6271
6756
|
if (op["create"] !== void 0) {
|
|
6272
6757
|
const rows = asArray(op["create"]).map((row) => ({
|
|
6273
6758
|
...applyContextValues(remapFromGraphQLSingleInput({ ...row }, plan.targetTable), contextValues?.(plan.targetTableName), context),
|
|
6274
6759
|
[plan.foreignColPropName]: key
|
|
6275
6760
|
}));
|
|
6276
|
-
if (rows.length) await executor.insert(plan.targetTable).values(rows);
|
|
6761
|
+
if (rows.length) await steps.run(executor.insert(plan.targetTable).values(rows), "create", "insert");
|
|
6277
6762
|
}
|
|
6278
6763
|
}
|
|
6764
|
+
await steps.after();
|
|
6279
6765
|
}
|
|
6280
6766
|
}
|
|
6281
6767
|
};
|
|
@@ -6285,21 +6771,25 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6285
6771
|
* inside a single transaction — a savepoint when the caller already runs on one, so a failing
|
|
6286
6772
|
* entry rolls the whole batch back.
|
|
6287
6773
|
*
|
|
6774
|
+
* That transaction is also the `tx` the target tables' `onWrite` hooks run on. Every nested
|
|
6775
|
+
* write opens it, so a hook on a nested target has the whole tree to roll back even when the
|
|
6776
|
+
* root field carries no hook of its own and was not forced into a transaction for one.
|
|
6777
|
+
*
|
|
6288
6778
|
* Entries are written one at a time rather than as a single multi-row statement: a nested
|
|
6289
6779
|
* operation attaches to the row its own entry produced, so the two have to stay correlated.
|
|
6290
6780
|
* The batch path is untouched when no entry carries a nested operation.
|
|
6291
6781
|
*/
|
|
6292
6782
|
const writeWithNestedOps = async (params) => {
|
|
6293
|
-
const { executor, runtime, tableName, entries, remapValues, write,
|
|
6783
|
+
const { executor, runtime, tableName, entries, remapValues, write, request } = params;
|
|
6294
6784
|
return executor.transaction(async (tx) => {
|
|
6295
6785
|
const rows = [];
|
|
6296
6786
|
for (const entry of entries) {
|
|
6297
|
-
const patch = await runtime.applyParentSide(tx, tableName, entry.ops,
|
|
6787
|
+
const patch = await runtime.applyParentSide(tx, tableName, entry.ops, request);
|
|
6298
6788
|
const written = await write(tx, {
|
|
6299
6789
|
...remapValues(entry.columns),
|
|
6300
6790
|
...patch
|
|
6301
6791
|
});
|
|
6302
|
-
await runtime.applyChildSide(tx, tableName, entry.ops, written,
|
|
6792
|
+
await runtime.applyChildSide(tx, tableName, entry.ops, written, request);
|
|
6303
6793
|
rows.push(...written);
|
|
6304
6794
|
}
|
|
6305
6795
|
return rows;
|
|
@@ -6311,14 +6801,14 @@ const writeWithNestedOps = async (params) => {
|
|
|
6311
6801
|
* attaches to each row that came back.
|
|
6312
6802
|
*/
|
|
6313
6803
|
const updateWithNestedOps = async (params) => {
|
|
6314
|
-
const { executor, runtime, tableName, columns, ops, remapValues, write,
|
|
6804
|
+
const { executor, runtime, tableName, columns, ops, remapValues, write, request } = params;
|
|
6315
6805
|
return executor.transaction(async (tx) => {
|
|
6316
|
-
const patch = await runtime.applyParentSide(tx, tableName, ops,
|
|
6806
|
+
const patch = await runtime.applyParentSide(tx, tableName, ops, request);
|
|
6317
6807
|
const rows = await write(tx, {
|
|
6318
6808
|
...remapValues(columns),
|
|
6319
6809
|
...patch
|
|
6320
6810
|
});
|
|
6321
|
-
await runtime.applyChildSide(tx, tableName, ops, rows,
|
|
6811
|
+
await runtime.applyChildSide(tx, tableName, ops, rows, request);
|
|
6322
6812
|
return rows;
|
|
6323
6813
|
});
|
|
6324
6814
|
};
|
|
@@ -6519,7 +7009,14 @@ const createSchemaBuilder = (adapter) => {
|
|
|
6519
7009
|
uniqueKeysOf: (tableName) => uniqueKeys[tableName]
|
|
6520
7010
|
};
|
|
6521
7011
|
adapter.preflight?.(db, options);
|
|
6522
|
-
const nestedPlans = features.nestedWrites ? buildNestedWritePlans(tables, namedRelations, (target) => uniqueColumnSets(target), (target) => primaryKeyPropNames(target), extractRelationJoinColumns) : void 0;
|
|
7012
|
+
const nestedPlans = features.nestedWrites ? buildNestedWritePlans(tables, namedRelations, (target) => uniqueColumnSets(target), (target) => primaryKeyPropNames(target), extractRelationJoinColumns, (junction) => schemaEntries.find(([, value]) => value === junction)?.[0]) : void 0;
|
|
7013
|
+
if (nestedPlans) {
|
|
7014
|
+
const omitted = gateNestedWritePlans(nestedPlans, featureOf, (tableName) => excludedTables.has(tableName));
|
|
7015
|
+
if (omitted.length) {
|
|
7016
|
+
const listed = omitted.length > 5 ? `${omitted.slice(0, 5).join(", ")} and ${omitted.length - 5} more` : omitted.join(", ");
|
|
7017
|
+
console.warn(`Drizzle-GraphQL Warning: config.features — nestedWrites leaves out the nested inputs whose table has that write turned off: ${listed}.`);
|
|
7018
|
+
}
|
|
7019
|
+
}
|
|
6523
7020
|
const nestedTypes = nestedPlans ? createNestedWriteTypes({
|
|
6524
7021
|
plans: nestedPlans,
|
|
6525
7022
|
cacheCtx,
|
|
@@ -7192,7 +7689,11 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
7192
7689
|
entries: nestedEntries,
|
|
7193
7690
|
remapValues: (values) => applyContextValues(remapFromGraphQLSingleInput(values, table), contextColumns, context),
|
|
7194
7691
|
write: (tx, values) => runInsert(tx, [values]),
|
|
7195
|
-
|
|
7692
|
+
request: {
|
|
7693
|
+
context,
|
|
7694
|
+
args,
|
|
7695
|
+
info
|
|
7696
|
+
}
|
|
7196
7697
|
}) : await runInsert(executor, input);
|
|
7197
7698
|
await after(written);
|
|
7198
7699
|
const result = output(written);
|
|
@@ -7281,7 +7782,11 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
7281
7782
|
entries: nestedEntries,
|
|
7282
7783
|
remapValues: (values) => applyContextValues(remapFromGraphQLSingleInput(values, table), contextColumns, context),
|
|
7283
7784
|
write: (tx, values) => runUpsert(tx, [values]),
|
|
7284
|
-
|
|
7785
|
+
request: {
|
|
7786
|
+
context,
|
|
7787
|
+
args,
|
|
7788
|
+
info
|
|
7789
|
+
}
|
|
7285
7790
|
}) : await runUpsert(executor, input);
|
|
7286
7791
|
await after(written);
|
|
7287
7792
|
const result = output(written);
|
|
@@ -7354,7 +7859,11 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
7354
7859
|
ops: nestedOps,
|
|
7355
7860
|
remapValues: (values) => values,
|
|
7356
7861
|
write: runUpdate,
|
|
7357
|
-
|
|
7862
|
+
request: {
|
|
7863
|
+
context,
|
|
7864
|
+
args,
|
|
7865
|
+
info
|
|
7866
|
+
}
|
|
7358
7867
|
}) : await runUpdate(executor, input);
|
|
7359
7868
|
await after(written);
|
|
7360
7869
|
const result = output(written);
|
|
@@ -7784,18 +8293,18 @@ const createSchemaDataGenerator = (adapter) => {
|
|
|
7784
8293
|
* the shared multi-mutation transaction opened by `runMutation` — this opens a savepoint, so
|
|
7785
8294
|
* the batch stays atomic without breaking the outer transaction.
|
|
7786
8295
|
*/
|
|
7787
|
-
const runUpdateManyBatch = ({ executor, entries, table, returning, nested, tableName,
|
|
8296
|
+
const runUpdateManyBatch = ({ executor, entries, table, returning, nested, tableName, request }) => executor.transaction(async (tx) => {
|
|
7788
8297
|
const results = [];
|
|
7789
8298
|
for (const entry of entries) {
|
|
7790
8299
|
const values = entry.ops ? {
|
|
7791
8300
|
...entry.set,
|
|
7792
|
-
...await nested.applyParentSide(tx, tableName, entry.ops,
|
|
8301
|
+
...await nested.applyParentSide(tx, tableName, entry.ops, request)
|
|
7793
8302
|
} : entry.set;
|
|
7794
8303
|
const writes = Object.keys(values).length > 0;
|
|
7795
8304
|
let query = writes ? tx.update(table).set(values) : tx.select(returning).from(table);
|
|
7796
8305
|
if (entry.filters) query = query.where(entry.filters);
|
|
7797
8306
|
const rows = await (writes ? query.returning(returning) : query);
|
|
7798
|
-
if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows,
|
|
8307
|
+
if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows, request);
|
|
7799
8308
|
results.push(rows);
|
|
7800
8309
|
}
|
|
7801
8310
|
return results;
|
|
@@ -7868,7 +8377,11 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
|
|
|
7868
8377
|
anyNested,
|
|
7869
8378
|
nested,
|
|
7870
8379
|
tableName,
|
|
7871
|
-
|
|
8380
|
+
request: {
|
|
8381
|
+
context,
|
|
8382
|
+
args,
|
|
8383
|
+
info
|
|
8384
|
+
}
|
|
7872
8385
|
});
|
|
7873
8386
|
const written = perEntry.flat();
|
|
7874
8387
|
await after(written);
|
|
@@ -7917,7 +8430,7 @@ const sqlitePrimaryKeyPropNames = (table) => getPrimaryKeyPropNamesFromConfig(ta
|
|
|
7917
8430
|
* would run after the COMMIT. The driver kind is probed from the first statement's result
|
|
7918
8431
|
* rather than declared, so the same resolver serves both driver families.
|
|
7919
8432
|
*/
|
|
7920
|
-
const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNested, nested, tableName,
|
|
8433
|
+
const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNested, nested, tableName, request }) => {
|
|
7921
8434
|
const runEntry = (tx, entry) => {
|
|
7922
8435
|
let query = tx.update(table).set(entry.set);
|
|
7923
8436
|
if (entry.filters) query = query.where(entry.filters);
|
|
@@ -7926,13 +8439,13 @@ const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNest
|
|
|
7926
8439
|
const runNestedEntry = async (tx, entry) => {
|
|
7927
8440
|
const values = entry.ops ? {
|
|
7928
8441
|
...entry.set,
|
|
7929
|
-
...await nested.applyParentSide(tx, tableName, entry.ops,
|
|
8442
|
+
...await nested.applyParentSide(tx, tableName, entry.ops, request)
|
|
7930
8443
|
} : entry.set;
|
|
7931
8444
|
const writes = Object.keys(values).length > 0;
|
|
7932
8445
|
let query = writes ? tx.update(table).set(values) : tx.select(returning).from(table);
|
|
7933
8446
|
if (entry.filters) query = query.where(entry.filters);
|
|
7934
8447
|
const rows = await (writes ? query.returning(returning) : query);
|
|
7935
|
-
if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows,
|
|
8448
|
+
if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows, request);
|
|
7936
8449
|
return rows;
|
|
7937
8450
|
};
|
|
7938
8451
|
return executor.transaction((tx) => {
|