@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.cjs
CHANGED
|
@@ -184,6 +184,76 @@ const GraphQLDecimalString = new graphql.GraphQLScalarType({
|
|
|
184
184
|
return parseNumeric(ast.value);
|
|
185
185
|
}
|
|
186
186
|
});
|
|
187
|
+
const fullDatePattern = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
188
|
+
const daysInMonth = (year, month) => {
|
|
189
|
+
if (month === 2) return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0 ? 29 : 28;
|
|
190
|
+
return [
|
|
191
|
+
4,
|
|
192
|
+
6,
|
|
193
|
+
9,
|
|
194
|
+
11
|
|
195
|
+
].includes(month) ? 30 : 31;
|
|
196
|
+
};
|
|
197
|
+
/** Whether `value` is a `YYYY-MM-DD` string naming a day that exists on the calendar. */
|
|
198
|
+
const isCalendarDate = (value) => {
|
|
199
|
+
const match = fullDatePattern.exec(value);
|
|
200
|
+
if (!match) return false;
|
|
201
|
+
const year = Number(match[1]);
|
|
202
|
+
const month = Number(match[2]);
|
|
203
|
+
const day = Number(match[3]);
|
|
204
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(year, month);
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* A JS `Date` as the calendar day it stands for. Read in UTC, because that is how drivers
|
|
208
|
+
* build one from a `date` column: `new Date('2026-09-15')` is UTC midnight, so the local
|
|
209
|
+
* getters would name the previous day anywhere west of Greenwich.
|
|
210
|
+
*/
|
|
211
|
+
const dateToCalendarDate = (value) => {
|
|
212
|
+
if (Number.isNaN(value.getTime())) return;
|
|
213
|
+
const year = value.getUTCFullYear();
|
|
214
|
+
if (year < 0 || year > 9999) return;
|
|
215
|
+
const month = String(value.getUTCMonth() + 1).padStart(2, "0");
|
|
216
|
+
const day = String(value.getUTCDate()).padStart(2, "0");
|
|
217
|
+
return `${String(year).padStart(4, "0")}-${month}-${day}`;
|
|
218
|
+
};
|
|
219
|
+
const calendarDate = (code, acceptDate) => (value) => {
|
|
220
|
+
if (typeof value === "string") {
|
|
221
|
+
if (!isCalendarDate(value)) throw drizzleError(`Date cannot represent "${value}": expected a calendar date as YYYY-MM-DD`, { code });
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
if (acceptDate && value instanceof Date) {
|
|
225
|
+
const formatted = dateToCalendarDate(value);
|
|
226
|
+
if (formatted === void 0) throw drizzleError("Date cannot represent an invalid or out-of-range Date", { code });
|
|
227
|
+
return formatted;
|
|
228
|
+
}
|
|
229
|
+
throw drizzleError(`Date cannot represent value: ${JSON.stringify(value)}: expected a YYYY-MM-DD string`, { code });
|
|
230
|
+
};
|
|
231
|
+
const parseCalendarDate = calendarDate(INVALID_INPUT, false);
|
|
232
|
+
/**
|
|
233
|
+
* A calendar day — `date` columns, whichever `mode` drizzle reads them in. Always transported
|
|
234
|
+
* as an RFC 3339 `full-date` string (`YYYY-MM-DD`), in both directions: it has no time and no
|
|
235
|
+
* offset, so there is nothing for a client's timezone to shift. `graphql-scalars`' own
|
|
236
|
+
* `GraphQLDate` is deliberately not used: its `parseValue` returns a JS `Date`, so the same
|
|
237
|
+
* scalar would be a string on the way out and an instant on the way in.
|
|
238
|
+
*
|
|
239
|
+
* Input accepts only a `YYYY-MM-DD` string naming a real day (`2026-02-30` and datetime
|
|
240
|
+
* strings are rejected) and hands it on unchanged. Output also accepts a JS `Date`, read as
|
|
241
|
+
* the UTC day it encodes.
|
|
242
|
+
*/
|
|
243
|
+
const GraphQLDate = new graphql.GraphQLScalarType({
|
|
244
|
+
name: "Date",
|
|
245
|
+
description: "A calendar date with no time or offset, transported as an RFC 3339 full-date string (YYYY-MM-DD).",
|
|
246
|
+
specifiedByURL: "https://datatracker.ietf.org/doc/html/rfc3339#section-5.6",
|
|
247
|
+
serialize: calendarDate(UNREPRESENTABLE, true),
|
|
248
|
+
parseValue: parseCalendarDate,
|
|
249
|
+
parseLiteral: (ast) => {
|
|
250
|
+
if (ast.kind !== graphql.Kind.STRING) throw drizzleError(`Date cannot represent a ${ast.kind}`, {
|
|
251
|
+
code: INVALID_INPUT,
|
|
252
|
+
nodes: ast
|
|
253
|
+
});
|
|
254
|
+
return parseCalendarDate(ast.value);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
187
257
|
//#endregion
|
|
188
258
|
//#region src/util/case-ops/index.ts
|
|
189
259
|
const uncapitalize = (input) => input?.length ? `${input[0].toLocaleLowerCase()}${input.length > 1 ? input.slice(1, input.length) : ""}` : input;
|
|
@@ -217,6 +287,17 @@ const singularizeMapper = (tableName) => ({
|
|
|
217
287
|
//#endregion
|
|
218
288
|
//#region src/util/type-converter/index.ts
|
|
219
289
|
const allowedNameChars = /^[a-zA-Z0-9_]+$/;
|
|
290
|
+
/** drizzle's `number` constraints whose values always fit a GraphQL `Int` (32-bit signed). */
|
|
291
|
+
const smallIntegerConstraints = /* @__PURE__ */ new Set([
|
|
292
|
+
"int8",
|
|
293
|
+
"uint8",
|
|
294
|
+
"int16",
|
|
295
|
+
"uint16",
|
|
296
|
+
"int24",
|
|
297
|
+
"uint24",
|
|
298
|
+
"int32",
|
|
299
|
+
"year"
|
|
300
|
+
]);
|
|
220
301
|
/** A GraphQL `Name` that is also allowed as an enum value — the spec reserves the three literals. */
|
|
221
302
|
const validEnumValueName = (name) => /^[_a-zA-Z][_a-zA-Z0-9]*$/.test(name) && name !== "true" && name !== "false" && name !== "null";
|
|
222
303
|
/**
|
|
@@ -357,6 +438,52 @@ const geoXyInputType = new graphql.GraphQLInputObjectType({
|
|
|
357
438
|
y: { type: graphql.GraphQLFloat }
|
|
358
439
|
}
|
|
359
440
|
});
|
|
441
|
+
/**
|
|
442
|
+
* Whether the column holds a calendar day with no time component — a SQL `date`, in either of
|
|
443
|
+
* the modes drizzle reads one in (`'date'` → a JS `Date`, `'string'` → `YYYY-MM-DD`). Every
|
|
444
|
+
* position a column's type appears in — output, create/update input, filter operands,
|
|
445
|
+
* aggregates and group keys — gives such a column the `Date` scalar, whichever mode it uses:
|
|
446
|
+
* the mode is how the server holds the value, not what the value is. SQLite has no date-only
|
|
447
|
+
* column type (its integer timestamps are instants — see {@link isDateTimeColumn}), so only
|
|
448
|
+
* Postgres and MySQL appear here.
|
|
449
|
+
*/
|
|
450
|
+
const isDateOnlyColumn = (column) => (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgDate) || (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgDateString) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlDate) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlDateString);
|
|
451
|
+
/**
|
|
452
|
+
* Whether the column is a MySQL `timestamp` / `datetime` read in drizzle's `'string'` mode. The
|
|
453
|
+
* driver hands such a column back as MySQL's own `YYYY-MM-DD HH:MM:SS[.fraction]` text, with no
|
|
454
|
+
* offset, and expects that form on write, so the data mappers translate between it and the ISO
|
|
455
|
+
* 8601 instant the `DateTime` scalar carries. The text is read as UTC, the way drizzle's own
|
|
456
|
+
* `'date'`-mode codecs for these columns read it.
|
|
457
|
+
*/
|
|
458
|
+
const isMySqlDateTimeStringColumn = (column) => (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlTimestampString) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlDateTimeString);
|
|
459
|
+
/**
|
|
460
|
+
* Whether the column is a Postgres `timestamp` read in drizzle's `'string'` mode. The driver hands
|
|
461
|
+
* such a column back as Postgres's own text — `YYYY-MM-DD HH:MM:SS[.fraction]`, followed by an
|
|
462
|
+
* offset (`+00`, `-06`, `+05:30`) for `timestamptz` — so the data mappers translate between it
|
|
463
|
+
* and the ISO 8601 instant the `DateTime` scalar carries. Text without an offset (`timestamp`
|
|
464
|
+
* without time zone) is read as UTC, the way drizzle's own `'date'`-mode codec reads it.
|
|
465
|
+
*/
|
|
466
|
+
const isPgTimestampStringColumn = (column) => (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgTimestampString);
|
|
467
|
+
/**
|
|
468
|
+
* Whether the column is a SQLite `integer({ mode: 'timestamp' | 'timestamp_ms' })`. Both modes
|
|
469
|
+
* are one drizzle class, told apart by `mode`: the stored integer counts seconds or milliseconds
|
|
470
|
+
* since the Unix epoch. drizzle converts a JS `Date` to that integer on write (`'timestamp'`
|
|
471
|
+
* floors to the whole second) and back on read, but a value read through a path that skips the
|
|
472
|
+
* column's decoder arrives as the bare integer, which {@link sqliteTimestampToDate} reads.
|
|
473
|
+
*/
|
|
474
|
+
const isSQLiteTimestampColumn = (column) => (0, drizzle_orm.is)(column, drizzle_orm_sqlite_core.SQLiteTimestamp);
|
|
475
|
+
/**
|
|
476
|
+
* A SQLite timestamp column's stored integer as the instant it names, using the column's own
|
|
477
|
+
* unit (seconds for `'timestamp'`, milliseconds for `'timestamp_ms'`).
|
|
478
|
+
*/
|
|
479
|
+
const sqliteTimestampToDate = (column, value) => new Date(column.mode === "timestamp" ? value * 1e3 : value);
|
|
480
|
+
/**
|
|
481
|
+
* Whether the column holds an instant — a Postgres `timestamp`, a MySQL `timestamp` /
|
|
482
|
+
* `datetime`, in either drizzle mode, or a SQLite integer timestamp in either unit. Every position
|
|
483
|
+
* such a column appears in gives it the `DateTime` scalar and the `DateTimeFilter`, whichever
|
|
484
|
+
* mode it uses, for the same reason as {@link isDateOnlyColumn}.
|
|
485
|
+
*/
|
|
486
|
+
const isDateTimeColumn = (column) => (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgTimestamp) || isPgTimestampStringColumn(column) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlTimestamp) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlDateTime) || isMySqlDateTimeStringColumn(column) || isSQLiteTimestampColumn(column);
|
|
360
487
|
const columnToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
361
488
|
const dimensions = column.dimensions;
|
|
362
489
|
if (dimensions !== void 0 && dimensions > 0) {
|
|
@@ -383,7 +510,11 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
383
510
|
typeLabel: "Boolean"
|
|
384
511
|
};
|
|
385
512
|
case "object":
|
|
386
|
-
if (column
|
|
513
|
+
if (isDateOnlyColumn(column)) return {
|
|
514
|
+
type: GraphQLDate,
|
|
515
|
+
typeLabel: "Date"
|
|
516
|
+
};
|
|
517
|
+
if (isDateTimeColumn(column)) return {
|
|
387
518
|
type: graphql_scalars.GraphQLDateTime,
|
|
388
519
|
typeLabel: "DateTime"
|
|
389
520
|
};
|
|
@@ -403,7 +534,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
403
534
|
type: GraphQLDecimalString,
|
|
404
535
|
typeLabel: "Decimal"
|
|
405
536
|
};
|
|
406
|
-
if (column
|
|
537
|
+
if (isDateTimeColumn(column)) return {
|
|
407
538
|
type: graphql_scalars.GraphQLDateTime,
|
|
408
539
|
typeLabel: "DateTime"
|
|
409
540
|
};
|
|
@@ -411,11 +542,8 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
411
542
|
type: graphql_scalars.GraphQLUUID,
|
|
412
543
|
typeLabel: "UUID"
|
|
413
544
|
};
|
|
414
|
-
if (column
|
|
415
|
-
type:
|
|
416
|
-
typeLabel: "Date"
|
|
417
|
-
} : {
|
|
418
|
-
type: graphql_scalars.GraphQLDate,
|
|
545
|
+
if (isDateOnlyColumn(column)) return {
|
|
546
|
+
type: GraphQLDate,
|
|
419
547
|
typeLabel: "Date"
|
|
420
548
|
};
|
|
421
549
|
return {
|
|
@@ -426,7 +554,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
426
554
|
type: GraphQLBigIntString,
|
|
427
555
|
typeLabel: "BigInt"
|
|
428
556
|
};
|
|
429
|
-
case "number": return (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgInteger) || (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgSerial) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlInt) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlSerial) || (0, drizzle_orm.is)(column, drizzle_orm_sqlite_core.SQLiteInteger) ? {
|
|
557
|
+
case "number": return constraint !== void 0 && smallIntegerConstraints.has(constraint) || (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgInteger) || (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgSerial) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlInt) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlSerial) || (0, drizzle_orm.is)(column, drizzle_orm_sqlite_core.SQLiteInteger) ? {
|
|
430
558
|
type: graphql.GraphQLInt,
|
|
431
559
|
typeLabel: "Integer"
|
|
432
560
|
} : {
|
|
@@ -602,7 +730,8 @@ const isJsonColumn = (column) => (column.dataType ?? "").includes("json") && col
|
|
|
602
730
|
* - "Decimal" → numeric/decimal columns (Decimal-scalar-typed operators, no string pattern operators)
|
|
603
731
|
* - the enum GraphQL type name → enum columns (still unique per enum; equality and
|
|
604
732
|
* membership only, like Boolean)
|
|
605
|
-
* - "
|
|
733
|
+
* - "Date" → date columns, either mode (ordering kept, no string pattern operators)
|
|
734
|
+
* - "DateTime" → timestamp columns (ordering kept, no string pattern operators)
|
|
606
735
|
* - "Int" → integer/serial columns (no string pattern operators)
|
|
607
736
|
* - "Float" → real/double columns (no string pattern operators)
|
|
608
737
|
* - "String" → all other text/varchar columns
|
|
@@ -641,8 +770,11 @@ const resolveGenericFilterDescriptor = (column, columnGraphQLType) => {
|
|
|
641
770
|
name: "Decimal",
|
|
642
771
|
kind: "scalar"
|
|
643
772
|
};
|
|
644
|
-
|
|
645
|
-
|
|
773
|
+
if (columnGraphQLType.type === GraphQLDate) return {
|
|
774
|
+
name: "Date",
|
|
775
|
+
kind: "scalar"
|
|
776
|
+
};
|
|
777
|
+
if (isDateTimeColumn(column)) return {
|
|
646
778
|
name: "DateTime",
|
|
647
779
|
kind: "scalar"
|
|
648
780
|
};
|
|
@@ -895,7 +1027,7 @@ const BUILTIN_FILTER_SCALARS = /* @__PURE__ */ new Set([
|
|
|
895
1027
|
GraphQLDecimalString,
|
|
896
1028
|
graphql_scalars.GraphQLUUID,
|
|
897
1029
|
graphql_scalars.GraphQLJSON,
|
|
898
|
-
|
|
1030
|
+
GraphQLDate,
|
|
899
1031
|
graphql_scalars.GraphQLDateTime
|
|
900
1032
|
]);
|
|
901
1033
|
/**
|
|
@@ -1037,6 +1169,58 @@ const generateColumnFilterValues = (column, tableName, columnName, cacheCtx) =>
|
|
|
1037
1169
|
};
|
|
1038
1170
|
//#endregion
|
|
1039
1171
|
//#region src/util/data-mappers/index.ts
|
|
1172
|
+
/**
|
|
1173
|
+
* MySQL's own text form for a `timestamp` / `datetime`: `YYYY-MM-DD HH:MM:SS`, an optional
|
|
1174
|
+
* fraction, and no offset. mysql2 returns it for a `'string'`-mode column, and it is what such a
|
|
1175
|
+
* column expects on write.
|
|
1176
|
+
*/
|
|
1177
|
+
const MYSQL_DATETIME_TEXT = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/;
|
|
1178
|
+
/**
|
|
1179
|
+
* MySQL datetime text, read as UTC, as the ISO 8601 instant the `DateTime` scalar carries.
|
|
1180
|
+
* UTC because that is how drizzle's own `'date'`-mode codecs read the same text, so a column's
|
|
1181
|
+
* two modes agree on the instant. Anything else (a zero date, a value already carrying an
|
|
1182
|
+
* offset) is returned unchanged for the scalar to accept or reject.
|
|
1183
|
+
*/
|
|
1184
|
+
const mySqlDateTimeTextToIso = (value) => {
|
|
1185
|
+
const match = MYSQL_DATETIME_TEXT.exec(value);
|
|
1186
|
+
return match ? `${match[1]}T${match[2]}Z` : value;
|
|
1187
|
+
};
|
|
1188
|
+
/**
|
|
1189
|
+
* A JS `Date` as offset-less `YYYY-MM-DD HH:MM:SS.mmm` text in UTC: what a MySQL `'string'`-mode
|
|
1190
|
+
* column and a Postgres `timestamp` (without time zone) `'string'`-mode column are written with.
|
|
1191
|
+
*/
|
|
1192
|
+
const dateToUtcWallText = (value) => value.toISOString().slice(0, -1).replace("T", " ");
|
|
1193
|
+
/**
|
|
1194
|
+
* Postgres's text form for a `timestamp` / `timestamptz` (the `ISO` `DateStyle`, the default):
|
|
1195
|
+
* `YYYY-MM-DD HH:MM:SS`, an optional fraction, and — for `timestamptz` only — the session zone's
|
|
1196
|
+
* offset as `±HH`, `±HH:MM` or, for historical local mean time, `±HH:MM:SS`.
|
|
1197
|
+
*/
|
|
1198
|
+
const PG_TIMESTAMP_TEXT = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(\.\d+)?(?:([+-])(\d{2})(?::?(\d{2}))?(?::?(\d{2}))?)?$/;
|
|
1199
|
+
/**
|
|
1200
|
+
* Postgres timestamp text as the RFC 3339 UTC instant (`…Z`) the `DateTime` scalar accepts.
|
|
1201
|
+
*
|
|
1202
|
+
* - With an offset (`timestamptz`), the instant is shifted to UTC. The offset can carry seconds,
|
|
1203
|
+
* which RFC 3339 cannot express, so it is applied here rather than rewritten.
|
|
1204
|
+
* - Without one (`timestamp`), the wall time is read as UTC: that is what drizzle's `'date'`-mode
|
|
1205
|
+
* codec does (`new Date(text + '+0000')`) and what the input side writes, so a column's two
|
|
1206
|
+
* modes agree on the instant.
|
|
1207
|
+
* - The fraction is kept digit for digit. The scalar itself carries milliseconds, so microseconds
|
|
1208
|
+
* are truncated on the wire, never rounded into the next second.
|
|
1209
|
+
*
|
|
1210
|
+
* Anything else (`infinity`, a `BC` date, a five-digit year) is returned unchanged for the scalar
|
|
1211
|
+
* to reject.
|
|
1212
|
+
*/
|
|
1213
|
+
const pgTimestampTextToIso = (value) => {
|
|
1214
|
+
const match = PG_TIMESTAMP_TEXT.exec(value);
|
|
1215
|
+
if (!match) return value;
|
|
1216
|
+
const [, date, time, fraction = "", sign, hours, minutes = "0", seconds = "0"] = match;
|
|
1217
|
+
if (!sign) return `${date}T${time}${fraction}Z`;
|
|
1218
|
+
const wallMs = Date.parse(`${date}T${time}Z`);
|
|
1219
|
+
if (Number.isNaN(wallMs)) return value;
|
|
1220
|
+
const offsetSeconds = (sign === "-" ? -1 : 1) * (Number(hours) * 3600 + Number(minutes) * 60 + Number(seconds));
|
|
1221
|
+
const utc = (/* @__PURE__ */ new Date(wallMs - offsetSeconds * 1e3)).toISOString();
|
|
1222
|
+
return utc.length === 24 ? `${utc.slice(0, 19)}${fraction}Z` : value;
|
|
1223
|
+
};
|
|
1040
1224
|
const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
|
|
1041
1225
|
if (Array.isArray(value)) {
|
|
1042
1226
|
const relations = relationMap?.[tableName];
|
|
@@ -1055,7 +1239,14 @@ const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
|
|
|
1055
1239
|
if (!column) return value;
|
|
1056
1240
|
if (getColumnScalarOverride(column, false)) return value;
|
|
1057
1241
|
if (isJsonColumn(column)) return value;
|
|
1242
|
+
if (value instanceof Date && isDateOnlyColumn(column)) return dateToCalendarDate(value) ?? value;
|
|
1243
|
+
if (typeof value === "number" && isSQLiteTimestampColumn(column)) {
|
|
1244
|
+
const instant = sqliteTimestampToDate(column, value);
|
|
1245
|
+
return Number.isNaN(instant.getTime()) ? value : instant.toISOString();
|
|
1246
|
+
}
|
|
1058
1247
|
if (value instanceof Date) return value.toISOString();
|
|
1248
|
+
if (typeof value === "string" && isMySqlDateTimeStringColumn(column)) return mySqlDateTimeTextToIso(value);
|
|
1249
|
+
if (typeof value === "string" && isPgTimestampStringColumn(column)) return pgTimestampTextToIso(value);
|
|
1059
1250
|
if (value instanceof Buffer) return Array.from(value);
|
|
1060
1251
|
if (typeof value === "bigint") return value.toString();
|
|
1061
1252
|
if (Array.isArray(value)) {
|
|
@@ -1101,16 +1292,20 @@ const remapFromGraphQLCore = (value, column, columnName) => {
|
|
|
1101
1292
|
}
|
|
1102
1293
|
const dataType = column.dataType ?? "";
|
|
1103
1294
|
const columnType = column.columnType ?? "";
|
|
1104
|
-
if (
|
|
1105
|
-
const
|
|
1295
|
+
if (isDateTimeColumn(column)) {
|
|
1296
|
+
const isMySql = columnType.startsWith("MySql");
|
|
1297
|
+
const isPgString = isPgTimestampStringColumn(column);
|
|
1298
|
+
const toIso = isMySql ? mySqlDateTimeTextToIso : isPgString ? pgTimestampTextToIso : void 0;
|
|
1299
|
+
const formatted = new Date(toIso && typeof value === "string" ? toIso(value) : value);
|
|
1106
1300
|
if (Number.isNaN(formatted.getTime())) throw drizzleError(`Field '${columnName}' is not a valid date!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1107
|
-
return formatted;
|
|
1301
|
+
if (isPgString) return column.withTimezone ? formatted.toISOString() : dateToUtcWallText(formatted);
|
|
1302
|
+
return isMySqlDateTimeStringColumn(column) ? dateToUtcWallText(formatted) : formatted;
|
|
1108
1303
|
}
|
|
1109
|
-
if ((
|
|
1110
|
-
|
|
1111
|
-
const
|
|
1112
|
-
if (
|
|
1113
|
-
return
|
|
1304
|
+
if (isDateOnlyColumn(column)) {
|
|
1305
|
+
if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
|
|
1306
|
+
const day = value instanceof Date ? dateToCalendarDate(value) : value;
|
|
1307
|
+
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" });
|
|
1308
|
+
return day;
|
|
1114
1309
|
}
|
|
1115
1310
|
if (dataType.includes("bigint")) try {
|
|
1116
1311
|
return BigInt(value);
|
|
@@ -1466,7 +1661,8 @@ const extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1466
1661
|
}
|
|
1467
1662
|
if (NOT) {
|
|
1468
1663
|
const extracted = extractFiltersColumn(column, columnName, NOT);
|
|
1469
|
-
|
|
1664
|
+
const negated = extracted ? (0, drizzle_orm.not)(extracted) : void 0;
|
|
1665
|
+
if (negated) variants.push(negated);
|
|
1470
1666
|
}
|
|
1471
1667
|
if (OR?.length) {
|
|
1472
1668
|
const orVariants = [];
|
|
@@ -1479,6 +1675,150 @@ const extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1479
1675
|
return variants.length ? variants.length > 1 ? (0, drizzle_orm.and)(...variants) : variants[0] : void 0;
|
|
1480
1676
|
};
|
|
1481
1677
|
//#endregion
|
|
1678
|
+
//#region src/util/builders/common/relations.ts
|
|
1679
|
+
/**
|
|
1680
|
+
* Flatten drizzle-orm v1 TablesRelationalConfig into the canonical
|
|
1681
|
+
* Record<tableName, Record<relName, TableNamedRelations>> shape used
|
|
1682
|
+
* throughout common.ts. Both pg.ts and sqlite.ts call this before
|
|
1683
|
+
* passing the relation map to any shared function.
|
|
1684
|
+
*/
|
|
1685
|
+
const buildNamedRelations = (relations, tableEntries) => {
|
|
1686
|
+
const namedRelations = {};
|
|
1687
|
+
for (const [relTableName, relConfig] of Object.entries(relations)) {
|
|
1688
|
+
if (!relConfig?.relations) continue;
|
|
1689
|
+
const namedConfig = {};
|
|
1690
|
+
for (const [innerRelName, innerRelValue] of Object.entries(relConfig.relations)) {
|
|
1691
|
+
const targetTable = innerRelValue.targetTable ?? innerRelValue.referencedTable;
|
|
1692
|
+
const directTargetName = innerRelValue.targetTableName;
|
|
1693
|
+
let targetTableName;
|
|
1694
|
+
if (directTargetName) targetTableName = tableEntries.find(([key]) => key === directTargetName)?.[0];
|
|
1695
|
+
else if (targetTable) targetTableName = tableEntries.find(([, tableValue]) => tableValue === targetTable)?.[0];
|
|
1696
|
+
if (!targetTableName) continue;
|
|
1697
|
+
namedConfig[innerRelName] = {
|
|
1698
|
+
relation: innerRelValue,
|
|
1699
|
+
targetTableName
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
if (Object.keys(namedConfig).length > 0) namedRelations[relTableName] = namedConfig;
|
|
1703
|
+
}
|
|
1704
|
+
return namedRelations;
|
|
1705
|
+
};
|
|
1706
|
+
/**
|
|
1707
|
+
* Records each relation's target-table primary-key property names on the relation entry,
|
|
1708
|
+
* so the pagination paths (the window-function batch loader and the eager `with:` orderBy
|
|
1709
|
+
* default) can fall back to a deterministic PK order without re-deriving it per request.
|
|
1710
|
+
*
|
|
1711
|
+
* Composite primary keys are only visible through the dialect's getTableConfig, so the
|
|
1712
|
+
* dialect builder passes a `resolvePkNames` that threads the composite column names in.
|
|
1713
|
+
* Mutates the relation entries in place (they are shared with the pruned eager map and
|
|
1714
|
+
* the resolver factory, so attaching once covers every consumer).
|
|
1715
|
+
*/
|
|
1716
|
+
const attachTargetPrimaryKeys = (namedRelations, tables, resolvePkNames) => {
|
|
1717
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1718
|
+
for (const rels of Object.values(namedRelations)) for (const relEntry of Object.values(rels)) {
|
|
1719
|
+
const { targetTableName } = relEntry;
|
|
1720
|
+
let pk = cache.get(targetTableName);
|
|
1721
|
+
if (!pk) {
|
|
1722
|
+
const targetTable = tables[targetTableName];
|
|
1723
|
+
pk = targetTable ? resolvePkNames(targetTable) : [];
|
|
1724
|
+
cache.set(targetTableName, pk);
|
|
1725
|
+
}
|
|
1726
|
+
relEntry.targetPkNames = pk;
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
/**
|
|
1730
|
+
* The table column behind one entry of a relation's `sourceColumns`, `targetColumns` or
|
|
1731
|
+
* `through` lists.
|
|
1732
|
+
*
|
|
1733
|
+
* drizzle-orm rc.4 stores the table's own Column objects in `sourceColumns` / `targetColumns`
|
|
1734
|
+
* and a `RelationsBuilderColumn` descriptor (`{ _: { column, key, tableName } }`) in `through`.
|
|
1735
|
+
* The rc.5 snapshots store the descriptor in all of them. Both shapes are accepted here, told
|
|
1736
|
+
* apart structurally rather than by importing a class one of those versions does not export.
|
|
1737
|
+
* Anything else yields `undefined`, which callers treat as "join columns not readable".
|
|
1738
|
+
*/
|
|
1739
|
+
const unwrapRelationColumn = (entry) => {
|
|
1740
|
+
if ((0, drizzle_orm.is)(entry, drizzle_orm.Column)) return entry;
|
|
1741
|
+
const inner = entry?._?.column;
|
|
1742
|
+
return (0, drizzle_orm.is)(inner, drizzle_orm.Column) ? inner : void 0;
|
|
1743
|
+
};
|
|
1744
|
+
/**
|
|
1745
|
+
* A relation's `sourceColumns` or `targetColumns` as table columns, via
|
|
1746
|
+
* {@link unwrapRelationColumn}. `undefined` when the list is missing, empty, or has an entry
|
|
1747
|
+
* that is not a column in either known shape.
|
|
1748
|
+
*/
|
|
1749
|
+
const relationColumns = (relation, side) => {
|
|
1750
|
+
const entries = relation?.[side === "source" ? "sourceColumns" : "targetColumns"];
|
|
1751
|
+
if (!entries?.length) return;
|
|
1752
|
+
const columns = [];
|
|
1753
|
+
for (const entry of entries) {
|
|
1754
|
+
const column = unwrapRelationColumn(entry);
|
|
1755
|
+
if (!column) return;
|
|
1756
|
+
columns.push(column);
|
|
1757
|
+
}
|
|
1758
|
+
return columns;
|
|
1759
|
+
};
|
|
1760
|
+
/**
|
|
1761
|
+
* drizzle-orm's "no filter" sentinel. rc.5 snapshots export it as `EmptyFilter` and use it as
|
|
1762
|
+
* the default `relation.where`; rc.4 has no such export and leaves `where` undefined. It is a
|
|
1763
|
+
* registered symbol, so it can be compared against without importing anything.
|
|
1764
|
+
*/
|
|
1765
|
+
const EMPTY_FILTER = Symbol.for("drizzle:EmptyFilter");
|
|
1766
|
+
/**
|
|
1767
|
+
* The relation's own declared `where` filter, or `undefined` when it has none. rc.4 leaves
|
|
1768
|
+
* `where` undefined in that case; rc.5 snapshots set it to the `EmptyFilter` sentinel, which is
|
|
1769
|
+
* truthy and must not be read as a filter.
|
|
1770
|
+
*/
|
|
1771
|
+
const relationOwnWhere = (relation) => {
|
|
1772
|
+
const where = relation?.where;
|
|
1773
|
+
return where == null || where === EMPTY_FILTER ? void 0 : where;
|
|
1774
|
+
};
|
|
1775
|
+
/**
|
|
1776
|
+
* Whether the relation's `where` was inherited from its reverse relation, and so is written
|
|
1777
|
+
* against the parent table rather than the target. rc.4 calls this flag `isReversed`; rc.5
|
|
1778
|
+
* snapshots renamed it to `isFilterReversed`. Both are set in the same place with the same
|
|
1779
|
+
* meaning (true only for a relation declared without `from`/`to` and without its own `where`).
|
|
1780
|
+
*/
|
|
1781
|
+
const isRelationFilterReversed = (relation) => !!(relation?.isFilterReversed ?? relation?.isReversed);
|
|
1782
|
+
/**
|
|
1783
|
+
* Warns, at build time, that a field was left out because the relation's join columns could
|
|
1784
|
+
* not be matched to its tables. Omitting it silently is how a drizzle-orm change in the shape
|
|
1785
|
+
* of relation columns (issue #179) went unnoticed. A relation that declares no join columns
|
|
1786
|
+
* is not reported: there is nothing that failed to match.
|
|
1787
|
+
*/
|
|
1788
|
+
const warnUnresolvedJoin = (relEntry, tableName, relationName, omitted) => {
|
|
1789
|
+
const rel = relEntry.relation ?? relEntry;
|
|
1790
|
+
if (!rel?.sourceColumns?.length || !rel?.targetColumns?.length) return;
|
|
1791
|
+
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.`);
|
|
1792
|
+
};
|
|
1793
|
+
/**
|
|
1794
|
+
* Extracts the join column info from a drizzle-orm v1 Relation object.
|
|
1795
|
+
* Returns the JS property name of the local column on the parent table and the
|
|
1796
|
+
* Column object for the foreign column on the target table, or undefined if the
|
|
1797
|
+
* relation internals are not accessible.
|
|
1798
|
+
*/
|
|
1799
|
+
const extractRelationJoinColumns = (relEntry, parentTable, targetTable) => {
|
|
1800
|
+
const rel = relEntry.relation ?? relEntry;
|
|
1801
|
+
const sourceCol = relationColumns(rel, "source")?.[0];
|
|
1802
|
+
const targetCol = relationColumns(rel, "target")?.[0];
|
|
1803
|
+
if (!sourceCol || !targetCol) return;
|
|
1804
|
+
const parentCols = (0, drizzle_orm.getColumns)(parentTable);
|
|
1805
|
+
const localColPropName = Object.entries(parentCols).find(([, c]) => c === sourceCol)?.[0];
|
|
1806
|
+
const targetCols = (0, drizzle_orm.getColumns)(targetTable);
|
|
1807
|
+
const foreignColPropName = Object.entries(targetCols).find(([, c]) => c === targetCol)?.[0];
|
|
1808
|
+
if (!localColPropName || !foreignColPropName) return;
|
|
1809
|
+
return {
|
|
1810
|
+
localColPropName,
|
|
1811
|
+
foreignCol: targetCol,
|
|
1812
|
+
foreignColPropName
|
|
1813
|
+
};
|
|
1814
|
+
};
|
|
1815
|
+
/**
|
|
1816
|
+
* Whether a to-one relation can back a relation orderBy hop. `.through()` (junction)
|
|
1817
|
+
* relations are excluded — the ordering subquery only joins the direct target, never the
|
|
1818
|
+
* junction table. (Relation *filters* do support `.through()` — this guard is orderBy-only.)
|
|
1819
|
+
*/
|
|
1820
|
+
const isFilterableRelation = (relation) => !relation.through;
|
|
1821
|
+
//#endregion
|
|
1482
1822
|
//#region src/util/builders/common/exclusions.ts
|
|
1483
1823
|
/**
|
|
1484
1824
|
* Columns this build keeps out of the generated schema, keyed by the table object.
|
|
@@ -1654,8 +1994,8 @@ const relationFilterCtx = (base, tableKey) => {
|
|
|
1654
1994
|
* the parent is an aliased proxy (as it is inside a relational `with:` where callback).
|
|
1655
1995
|
*/
|
|
1656
1996
|
const buildRelationJoinCondition = (parentTable, relation, aliasedTarget, relationName) => {
|
|
1657
|
-
const sourceColumns = relation
|
|
1658
|
-
const targetColumns = relation
|
|
1997
|
+
const sourceColumns = relationColumns(relation, "source");
|
|
1998
|
+
const targetColumns = relationColumns(relation, "target");
|
|
1659
1999
|
if (!sourceColumns?.length || sourceColumns.length !== targetColumns?.length) throw drizzleError(`WHERE ${relationName}: Relation cannot be used as a filter`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1660
2000
|
const parentColumns = Object.values((0, drizzle_orm.getColumns)(parentTable));
|
|
1661
2001
|
const targetColumnsByName = Object.values((0, drizzle_orm.getColumns)(aliasedTarget));
|
|
@@ -1679,7 +2019,7 @@ const resolveJunctionColumn = (aliasedThrough, junctionRef, relationName) => {
|
|
|
1679
2019
|
const key = junctionRef?._?.key;
|
|
1680
2020
|
const byKey = key ? throughColumns[key] : void 0;
|
|
1681
2021
|
if (byKey) return byKey;
|
|
1682
|
-
const columnName = junctionRef?.
|
|
2022
|
+
const columnName = unwrapRelationColumn(junctionRef)?.name;
|
|
1683
2023
|
const byName = columnName ? Object.values(throughColumns).find((c) => c.name === columnName) : void 0;
|
|
1684
2024
|
if (!byName) throw drizzleError(`WHERE ${relationName}: Relation cannot be used as a filter`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1685
2025
|
return byName;
|
|
@@ -1693,8 +2033,8 @@ const resolveJunctionColumn = (aliasedThrough, junctionRef, relationName) => {
|
|
|
1693
2033
|
* `through` metadata.
|
|
1694
2034
|
*/
|
|
1695
2035
|
const buildThroughJoinConditions = (parentTable, relation, aliasedThrough, aliasedTarget, relationName) => {
|
|
1696
|
-
const sourceColumns = relation
|
|
1697
|
-
const targetColumns = relation
|
|
2036
|
+
const sourceColumns = relationColumns(relation, "source");
|
|
2037
|
+
const targetColumns = relationColumns(relation, "target");
|
|
1698
2038
|
const through = relation.through;
|
|
1699
2039
|
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" });
|
|
1700
2040
|
const parentColumns = Object.values((0, drizzle_orm.getColumns)(parentTable));
|
|
@@ -1746,7 +2086,8 @@ const buildRelationExists = (parentTable, relationName, relEntry, innerFilters,
|
|
|
1746
2086
|
fromClause = (0, drizzle_orm.getTableAsAliasSQL)(aliasedTarget);
|
|
1747
2087
|
joinCondition = buildRelationJoinCondition(parentTable, relation, aliasedTarget, relationName);
|
|
1748
2088
|
}
|
|
1749
|
-
const
|
|
2089
|
+
const ownWhere = relationOwnWhere(relation);
|
|
2090
|
+
const relationWhere = ownWhere ? (0, drizzle_orm.relationsFilterToSQL)(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
|
|
1750
2091
|
const inner = innerFilters ? extractFilters(aliasedTarget, targetTableName, innerFilters, {
|
|
1751
2092
|
...ctx,
|
|
1752
2093
|
tableKey: targetTableName,
|
|
@@ -1800,7 +2141,8 @@ const extractFilters = (table, tableName, filters, relationCtx) => {
|
|
|
1800
2141
|
}
|
|
1801
2142
|
if (NOT) {
|
|
1802
2143
|
const extracted = extractFilters(table, tableName, NOT, relationCtx);
|
|
1803
|
-
|
|
2144
|
+
const negated = extracted ? (0, drizzle_orm.not)(extracted) : void 0;
|
|
2145
|
+
if (negated) variants.push(negated);
|
|
1804
2146
|
}
|
|
1805
2147
|
if (OR?.length) {
|
|
1806
2148
|
const orVariants = [];
|
|
@@ -2197,88 +2539,6 @@ const excludedColumnRef = (columnName) => drizzle_orm.sql`excluded.${drizzle_orm
|
|
|
2197
2539
|
/** `values(<column>)` — MySQL's equivalent inside ON DUPLICATE KEY UPDATE. */
|
|
2198
2540
|
const mysqlValuesColumnRef = (columnName) => drizzle_orm.sql`values(${drizzle_orm.sql.identifier(columnName)})`;
|
|
2199
2541
|
//#endregion
|
|
2200
|
-
//#region src/util/builders/common/relations.ts
|
|
2201
|
-
/**
|
|
2202
|
-
* Flatten drizzle-orm v1 TablesRelationalConfig into the canonical
|
|
2203
|
-
* Record<tableName, Record<relName, TableNamedRelations>> shape used
|
|
2204
|
-
* throughout common.ts. Both pg.ts and sqlite.ts call this before
|
|
2205
|
-
* passing the relation map to any shared function.
|
|
2206
|
-
*/
|
|
2207
|
-
const buildNamedRelations = (relations, tableEntries) => {
|
|
2208
|
-
const namedRelations = {};
|
|
2209
|
-
for (const [relTableName, relConfig] of Object.entries(relations)) {
|
|
2210
|
-
if (!relConfig?.relations) continue;
|
|
2211
|
-
const namedConfig = {};
|
|
2212
|
-
for (const [innerRelName, innerRelValue] of Object.entries(relConfig.relations)) {
|
|
2213
|
-
const targetTable = innerRelValue.targetTable ?? innerRelValue.referencedTable;
|
|
2214
|
-
const directTargetName = innerRelValue.targetTableName;
|
|
2215
|
-
let targetTableName;
|
|
2216
|
-
if (directTargetName) targetTableName = tableEntries.find(([key]) => key === directTargetName)?.[0];
|
|
2217
|
-
else if (targetTable) targetTableName = tableEntries.find(([, tableValue]) => tableValue === targetTable)?.[0];
|
|
2218
|
-
if (!targetTableName) continue;
|
|
2219
|
-
namedConfig[innerRelName] = {
|
|
2220
|
-
relation: innerRelValue,
|
|
2221
|
-
targetTableName
|
|
2222
|
-
};
|
|
2223
|
-
}
|
|
2224
|
-
if (Object.keys(namedConfig).length > 0) namedRelations[relTableName] = namedConfig;
|
|
2225
|
-
}
|
|
2226
|
-
return namedRelations;
|
|
2227
|
-
};
|
|
2228
|
-
/**
|
|
2229
|
-
* Records each relation's target-table primary-key property names on the relation entry,
|
|
2230
|
-
* so the pagination paths (the window-function batch loader and the eager `with:` orderBy
|
|
2231
|
-
* default) can fall back to a deterministic PK order without re-deriving it per request.
|
|
2232
|
-
*
|
|
2233
|
-
* Composite primary keys are only visible through the dialect's getTableConfig, so the
|
|
2234
|
-
* dialect builder passes a `resolvePkNames` that threads the composite column names in.
|
|
2235
|
-
* Mutates the relation entries in place (they are shared with the pruned eager map and
|
|
2236
|
-
* the resolver factory, so attaching once covers every consumer).
|
|
2237
|
-
*/
|
|
2238
|
-
const attachTargetPrimaryKeys = (namedRelations, tables, resolvePkNames) => {
|
|
2239
|
-
const cache = /* @__PURE__ */ new Map();
|
|
2240
|
-
for (const rels of Object.values(namedRelations)) for (const relEntry of Object.values(rels)) {
|
|
2241
|
-
const { targetTableName } = relEntry;
|
|
2242
|
-
let pk = cache.get(targetTableName);
|
|
2243
|
-
if (!pk) {
|
|
2244
|
-
const targetTable = tables[targetTableName];
|
|
2245
|
-
pk = targetTable ? resolvePkNames(targetTable) : [];
|
|
2246
|
-
cache.set(targetTableName, pk);
|
|
2247
|
-
}
|
|
2248
|
-
relEntry.targetPkNames = pk;
|
|
2249
|
-
}
|
|
2250
|
-
};
|
|
2251
|
-
/**
|
|
2252
|
-
* Extracts the join column info from a drizzle-orm v1 Relation object.
|
|
2253
|
-
* Returns the JS property name of the local column on the parent table and the
|
|
2254
|
-
* Column object for the foreign column on the target table, or undefined if the
|
|
2255
|
-
* relation internals are not accessible.
|
|
2256
|
-
*/
|
|
2257
|
-
const extractRelationJoinColumns = (relEntry, parentTable, targetTable) => {
|
|
2258
|
-
const rel = relEntry.relation ?? relEntry;
|
|
2259
|
-
const sourceColumns = rel.sourceColumns;
|
|
2260
|
-
const targetColumns = rel.targetColumns;
|
|
2261
|
-
if (!sourceColumns?.length || !targetColumns?.length) return;
|
|
2262
|
-
const sourceCol = sourceColumns[0];
|
|
2263
|
-
const targetCol = targetColumns[0];
|
|
2264
|
-
const parentCols = (0, drizzle_orm.getColumns)(parentTable);
|
|
2265
|
-
const localColPropName = Object.entries(parentCols).find(([, c]) => c === sourceCol)?.[0];
|
|
2266
|
-
const targetCols = (0, drizzle_orm.getColumns)(targetTable);
|
|
2267
|
-
const foreignColPropName = Object.entries(targetCols).find(([, c]) => c === targetCol)?.[0];
|
|
2268
|
-
if (!localColPropName || !foreignColPropName) return;
|
|
2269
|
-
return {
|
|
2270
|
-
localColPropName,
|
|
2271
|
-
foreignCol: targetCol,
|
|
2272
|
-
foreignColPropName
|
|
2273
|
-
};
|
|
2274
|
-
};
|
|
2275
|
-
/**
|
|
2276
|
-
* Whether a to-one relation can back a relation orderBy hop. `.through()` (junction)
|
|
2277
|
-
* relations are excluded — the ordering subquery only joins the direct target, never the
|
|
2278
|
-
* junction table. (Relation *filters* do support `.through()` — this guard is orderBy-only.)
|
|
2279
|
-
*/
|
|
2280
|
-
const isFilterableRelation = (relation) => !relation.through;
|
|
2281
|
-
//#endregion
|
|
2282
2542
|
//#region src/util/builders/common/order-by.ts
|
|
2283
2543
|
/**
|
|
2284
2544
|
* Column name / direction / nulls triples from an `orderBy` argument, highest priority
|
|
@@ -2324,7 +2584,8 @@ const extendRelationOrderChain = (parentTable, relationName, relEntry, ctx, chai
|
|
|
2324
2584
|
ctx.aliases ??= { n: 0 };
|
|
2325
2585
|
const aliasedTarget = (0, drizzle_orm.aliasedTable)(targetTable, `dgql_ord_${ctx.aliases.n++}`);
|
|
2326
2586
|
const joinCondition = buildRelationJoinCondition(parentTable, relation, aliasedTarget, relationName);
|
|
2327
|
-
const
|
|
2587
|
+
const ownWhere = relationOwnWhere(relation);
|
|
2588
|
+
const relationWhere = ownWhere ? (0, drizzle_orm.relationsFilterToSQL)(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
|
|
2328
2589
|
return {
|
|
2329
2590
|
fromParts: [...chain?.fromParts ?? [], (0, drizzle_orm.getTableAsAliasSQL)(aliasedTarget)],
|
|
2330
2591
|
conditions: [
|
|
@@ -2341,15 +2602,17 @@ const extendRelationOrderChain = (parentTable, relationName, relEntry, ctx, chai
|
|
|
2341
2602
|
*
|
|
2342
2603
|
* A ladder rather than a dialect array function (`array_position` on Postgres, `FIELD()` on
|
|
2343
2604
|
* MySQL, neither on SQLite) because it is the one form all three share, and because every
|
|
2344
|
-
* list value stays a bound parameter
|
|
2345
|
-
*
|
|
2346
|
-
*
|
|
2605
|
+
* list value stays a bound parameter — encoded by the column, as `eq` would, so a value whose
|
|
2606
|
+
* driver form differs from its JS one (a SQLite integer timestamp's `Date`) is bound as stored.
|
|
2607
|
+
* The branch indices are library-generated integers, so they are written into the SQL text — a
|
|
2608
|
+
* bound parameter there would leave Postgres unable to infer the CASE result type. Values
|
|
2609
|
+
* outside the list sort after every listed one.
|
|
2347
2610
|
*/
|
|
2348
2611
|
const filterOrderExpression = (column, columnName, whereArgs) => {
|
|
2349
2612
|
const values = whereArgs?.[columnName]?.inArray;
|
|
2350
2613
|
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" });
|
|
2351
2614
|
if (!values.length) return drizzle_orm.sql`0`;
|
|
2352
|
-
const branches = values.map((value, index) => drizzle_orm.sql`when ${column} = ${remapFromGraphQLCore(value, column, columnName)} then ${drizzle_orm.sql.raw(String(index))}`);
|
|
2615
|
+
const branches = values.map((value, index) => drizzle_orm.sql`when ${column} = ${drizzle_orm.sql.param(remapFromGraphQLCore(value, column, columnName), column)} then ${drizzle_orm.sql.raw(String(index))}`);
|
|
2353
2616
|
return drizzle_orm.sql`case ${drizzle_orm.sql.join(branches, drizzle_orm.sql` `)} else ${drizzle_orm.sql.raw(String(values.length))} end`;
|
|
2354
2617
|
};
|
|
2355
2618
|
/**
|
|
@@ -3315,6 +3578,26 @@ const remapUpdateInput = (values, table, tableName) => {
|
|
|
3315
3578
|
} : remapped;
|
|
3316
3579
|
};
|
|
3317
3580
|
//#endregion
|
|
3581
|
+
//#region src/util/builders/common/rqb-config.ts
|
|
3582
|
+
/**
|
|
3583
|
+
* A copy of a relational query config with every key whose value is `undefined` left out.
|
|
3584
|
+
*
|
|
3585
|
+
* drizzle-orm rc.5 snapshots test `"where" in params` rather than its value, and throw
|
|
3586
|
+
* `Unexpected 'undefined' in filter value` for `{ where: undefined }`; rc.4 read the value and
|
|
3587
|
+
* treated both the same. The library builds its configs with conditional keys
|
|
3588
|
+
* (`where: cond ? … : undefined`), so every config goes through here before it reaches drizzle.
|
|
3589
|
+
* Only `where` is checked that way today, but leaving out every undefined key costs nothing
|
|
3590
|
+
* and keeps the next key that gets the same treatment from breaking queries.
|
|
3591
|
+
*
|
|
3592
|
+
* Shallow on purpose: nested `with:` entries are built (and passed through here) one level at
|
|
3593
|
+
* a time, and the values of the other keys are drizzle's own (column maps, callbacks, RAW).
|
|
3594
|
+
*/
|
|
3595
|
+
const omitUndefinedKeys = (config) => {
|
|
3596
|
+
const out = {};
|
|
3597
|
+
for (const [key, value] of Object.entries(config)) if (value !== void 0) out[key] = value;
|
|
3598
|
+
return out;
|
|
3599
|
+
};
|
|
3600
|
+
//#endregion
|
|
3318
3601
|
//#region src/util/builders/common/selected-columns.ts
|
|
3319
3602
|
const rqbCrashTypes = [
|
|
3320
3603
|
"SQLiteBigInt",
|
|
@@ -3443,7 +3726,7 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
|
|
|
3443
3726
|
thisRecord.offset = offset;
|
|
3444
3727
|
thisRecord.limit = limit;
|
|
3445
3728
|
thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
|
|
3446
|
-
args[relName] = thisRecord;
|
|
3729
|
+
args[relName] = omitUndefinedKeys(thisRecord);
|
|
3447
3730
|
}
|
|
3448
3731
|
return args;
|
|
3449
3732
|
};
|
|
@@ -3520,12 +3803,12 @@ const runRelationalSelect = async (opts) => {
|
|
|
3520
3803
|
}) : void 0
|
|
3521
3804
|
};
|
|
3522
3805
|
if (single) {
|
|
3523
|
-
const result = await queryBase.findFirst(params);
|
|
3806
|
+
const result = await queryBase.findFirst(omitUndefinedKeys(params));
|
|
3524
3807
|
return result ? remapToGraphQLSingleOutput(result, tableName, table, relationMap) : void 0;
|
|
3525
3808
|
}
|
|
3526
3809
|
if (cursorEntries && cursorSelected) for (const [column] of cursorEntries) params.columns[column] = true;
|
|
3527
3810
|
params.limit = distinctKeys ? void 0 : opts.limit;
|
|
3528
|
-
const result = await queryBase.findMany(params);
|
|
3811
|
+
const result = await queryBase.findMany(omitUndefinedKeys(params));
|
|
3529
3812
|
if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
|
|
3530
3813
|
return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
|
|
3531
3814
|
};
|
|
@@ -5251,7 +5534,6 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5251
5534
|
const missing = [!tableFeatures.insert ? "insert" : void 0, !tableFeatures.update ? "update" : void 0].filter(Boolean).join(" and ");
|
|
5252
5535
|
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);
|
|
5253
5536
|
}
|
|
5254
|
-
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);
|
|
5255
5537
|
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);
|
|
5256
5538
|
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);
|
|
5257
5539
|
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);
|
|
@@ -5564,13 +5846,21 @@ const GROUP_KEY = "__dgql_group_key";
|
|
|
5564
5846
|
* for every other aggregate.
|
|
5565
5847
|
*/
|
|
5566
5848
|
const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, filterCtx, policies) => {
|
|
5849
|
+
const warned = /* @__PURE__ */ new Set();
|
|
5567
5850
|
return ({ tableName, relationName, relEntry }) => {
|
|
5568
5851
|
const parentTable = tables[tableName];
|
|
5569
5852
|
const targetTableName = relEntry.targetTableName;
|
|
5570
5853
|
const targetTable = tables[targetTableName];
|
|
5571
5854
|
if (!parentTable || !targetTable) return;
|
|
5572
5855
|
const joinCols = extractRelationJoinColumns(relEntry, parentTable, targetTable);
|
|
5573
|
-
if (!joinCols)
|
|
5856
|
+
if (!joinCols) {
|
|
5857
|
+
const key = `${tableName}.${relationName}`;
|
|
5858
|
+
if (!warned.has(key)) {
|
|
5859
|
+
warned.add(key);
|
|
5860
|
+
warnUnresolvedJoin(relEntry, tableName, relationName, `its '${relationName}Aggregate' field`);
|
|
5861
|
+
}
|
|
5862
|
+
return;
|
|
5863
|
+
}
|
|
5574
5864
|
const { localColPropName, foreignCol } = joinCols;
|
|
5575
5865
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
|
|
5576
5866
|
const targetTypeName = resolveTypeName(targetTableName, typeNameMapper);
|
|
@@ -5857,6 +6147,12 @@ const generateGroupBy = (db, tableName, table, typeName, fieldName, filterArgs,
|
|
|
5857
6147
|
};
|
|
5858
6148
|
//#endregion
|
|
5859
6149
|
//#region src/util/builders/nested-writes.ts
|
|
6150
|
+
const ALL_OPS = [
|
|
6151
|
+
"create",
|
|
6152
|
+
"connect",
|
|
6153
|
+
"disconnect",
|
|
6154
|
+
"set"
|
|
6155
|
+
];
|
|
5860
6156
|
/**
|
|
5861
6157
|
* Whether a column can be left out of an INSERT — either the database fills it in or it
|
|
5862
6158
|
* accepts NULL.
|
|
@@ -5868,12 +6164,6 @@ const propNameOf = (table, column) => {
|
|
|
5868
6164
|
for (const [propName, candidate] of Object.entries((0, drizzle_orm.getColumns)(table))) if (candidate === column) return propName;
|
|
5869
6165
|
};
|
|
5870
6166
|
/**
|
|
5871
|
-
* The junction column one entry of a relation's `through` config points at. Drizzle wraps it
|
|
5872
|
-
* in a descriptor while the relation is being defined; the raw column is accepted too, so a
|
|
5873
|
-
* change of shape there degrades to skipping the relation rather than mis-planning it.
|
|
5874
|
-
*/
|
|
5875
|
-
const throughColumn = (entry) => entry && (0, drizzle_orm.is)(entry, drizzle_orm.Column) ? entry : entry?._?.column;
|
|
5876
|
-
/**
|
|
5877
6167
|
* A `.through()` relation's plan: the junction table plus the two columns it joins on.
|
|
5878
6168
|
*
|
|
5879
6169
|
* Nothing about the parent or the target row changes when a many-to-many link is made or
|
|
@@ -5884,21 +6174,27 @@ const throughColumn = (entry) => entry && (0, drizzle_orm.is)(entry, drizzle_orm
|
|
|
5884
6174
|
* row" guarantee lives in the junction's constraints rather than in the relation; a relation
|
|
5885
6175
|
* joining on more than one column per side; and a junction carrying a further NOT NULL column
|
|
5886
6176
|
* with no default, which no `connect` operand could supply a value for.
|
|
6177
|
+
*
|
|
6178
|
+
* `onUnresolved` is called when the relation's columns are of a supported shape but cannot be
|
|
6179
|
+
* matched to the three tables — a drizzle-orm mismatch rather than an unsupported relation.
|
|
5887
6180
|
*/
|
|
5888
|
-
const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable) => {
|
|
6181
|
+
const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable, tableNameOf, onUnresolved) => {
|
|
5889
6182
|
if ((0, drizzle_orm.is)(relation, drizzle_orm.One)) return;
|
|
5890
6183
|
const junction = relation.throughTable;
|
|
5891
6184
|
const sourceEntries = relation.through?.source;
|
|
5892
6185
|
const targetEntries = relation.through?.target;
|
|
5893
6186
|
if (!junction || sourceEntries?.length !== 1 || targetEntries?.length !== 1) return;
|
|
5894
6187
|
if (relation.sourceColumns?.length !== 1 || relation.targetColumns?.length !== 1) return;
|
|
5895
|
-
const localCol = relation.sourceColumns[0];
|
|
5896
|
-
const foreignCol = relation.targetColumns[0];
|
|
6188
|
+
const localCol = unwrapRelationColumn(relation.sourceColumns[0]);
|
|
6189
|
+
const foreignCol = unwrapRelationColumn(relation.targetColumns[0]);
|
|
5897
6190
|
const localColPropName = propNameOf(table, localCol);
|
|
5898
6191
|
const foreignColPropName = propNameOf(targetTable, foreignCol);
|
|
5899
|
-
const sourceColPropName = propNameOf(junction,
|
|
5900
|
-
const targetColPropName = propNameOf(junction,
|
|
5901
|
-
if (!localColPropName || !foreignColPropName || !sourceColPropName || !targetColPropName)
|
|
6192
|
+
const sourceColPropName = propNameOf(junction, unwrapRelationColumn(sourceEntries[0]));
|
|
6193
|
+
const targetColPropName = propNameOf(junction, unwrapRelationColumn(targetEntries[0]));
|
|
6194
|
+
if (!localCol || !foreignCol || !localColPropName || !foreignColPropName || !sourceColPropName || !targetColPropName) {
|
|
6195
|
+
onUnresolved();
|
|
6196
|
+
return;
|
|
6197
|
+
}
|
|
5902
6198
|
const junctionColumns = (0, drizzle_orm.getColumns)(junction);
|
|
5903
6199
|
const sourceCol = junctionColumns[sourceColPropName];
|
|
5904
6200
|
const targetCol = junctionColumns[targetColPropName];
|
|
@@ -5916,11 +6212,18 @@ const buildThroughPlan = (relationName, relation, table, targetTableName, target
|
|
|
5916
6212
|
canDetach: true,
|
|
5917
6213
|
through: {
|
|
5918
6214
|
table: junction,
|
|
6215
|
+
tableName: tableNameOf(junction),
|
|
5919
6216
|
sourceColPropName,
|
|
5920
6217
|
targetColPropName,
|
|
5921
6218
|
sourceCol,
|
|
5922
6219
|
targetCol
|
|
5923
|
-
}
|
|
6220
|
+
},
|
|
6221
|
+
targetKeyColumns: { [foreignColPropName]: foreignCol },
|
|
6222
|
+
allowed: /* @__PURE__ */ new Set([
|
|
6223
|
+
"connect",
|
|
6224
|
+
"disconnect",
|
|
6225
|
+
"set"
|
|
6226
|
+
])
|
|
5924
6227
|
};
|
|
5925
6228
|
};
|
|
5926
6229
|
/**
|
|
@@ -5933,7 +6236,7 @@ const buildThroughPlan = (relationName, relation, table, targetTableName, target
|
|
|
5933
6236
|
* relations whose join columns cannot be read off the drizzle relation; and relations where
|
|
5934
6237
|
* neither join column is unique, which leaves no way to tell which side stores the key.
|
|
5935
6238
|
*/
|
|
5936
|
-
const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePrimaryKeys, extractJoinColumns) => {
|
|
6239
|
+
const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePrimaryKeys, extractJoinColumns, tableNameOf = () => void 0) => {
|
|
5937
6240
|
const uniqueCache = /* @__PURE__ */ new Map();
|
|
5938
6241
|
const singleColumnUniques = (tableName, table) => {
|
|
5939
6242
|
let cached = uniqueCache.get(tableName);
|
|
@@ -5961,13 +6264,17 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
5961
6264
|
const targetTableName = relEntry.targetTableName;
|
|
5962
6265
|
const targetTable = tables[targetTableName];
|
|
5963
6266
|
if (!targetTable) continue;
|
|
6267
|
+
const omitted = `the '${relationName}' field of its nested-write create and update inputs`;
|
|
5964
6268
|
if (relation.through) {
|
|
5965
|
-
const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable);
|
|
6269
|
+
const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable, tableNameOf, () => warnUnresolvedJoin(relEntry, tableName, relationName, omitted));
|
|
5966
6270
|
if (throughPlan) tablePlans[relationName] = throughPlan;
|
|
5967
6271
|
continue;
|
|
5968
6272
|
}
|
|
5969
6273
|
const joinColumns = extractJoinColumns(relEntry, table, targetTable);
|
|
5970
|
-
if (!joinColumns)
|
|
6274
|
+
if (!joinColumns) {
|
|
6275
|
+
warnUnresolvedJoin(relEntry, tableName, relationName, omitted);
|
|
6276
|
+
continue;
|
|
6277
|
+
}
|
|
5971
6278
|
const { localColPropName, foreignCol, foreignColPropName } = joinColumns;
|
|
5972
6279
|
const localCol = (0, drizzle_orm.getColumns)(table)[localColPropName];
|
|
5973
6280
|
if (!localCol) continue;
|
|
@@ -5983,6 +6290,10 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
5983
6290
|
else if (foreignIsUnique) fkSide = "parent";
|
|
5984
6291
|
else if (localIsUnique) fkSide = "child";
|
|
5985
6292
|
else continue;
|
|
6293
|
+
const fkColumn = fkSide === "child" ? foreignCol : localCol;
|
|
6294
|
+
const targetColumns = (0, drizzle_orm.getColumns)(targetTable);
|
|
6295
|
+
const targetKeyColumns = Object.fromEntries(resolvePrimaryKeys(targetTable).filter((name) => targetColumns[name]).map((name) => [name, targetColumns[name]]));
|
|
6296
|
+
targetKeyColumns[foreignColPropName] = foreignCol;
|
|
5986
6297
|
tablePlans[relationName] = {
|
|
5987
6298
|
relationName,
|
|
5988
6299
|
targetTableName,
|
|
@@ -5993,13 +6304,87 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
5993
6304
|
localCol,
|
|
5994
6305
|
isOne,
|
|
5995
6306
|
fkSide,
|
|
5996
|
-
canDetach: !
|
|
6307
|
+
canDetach: !fkColumn.notNull,
|
|
6308
|
+
targetKeyColumns,
|
|
6309
|
+
allowed: new Set(!fkColumn.notNull ? isOne ? [
|
|
6310
|
+
"create",
|
|
6311
|
+
"connect",
|
|
6312
|
+
"disconnect"
|
|
6313
|
+
] : ALL_OPS : ["create", "connect"])
|
|
5997
6314
|
};
|
|
5998
6315
|
}
|
|
5999
6316
|
if (Object.keys(tablePlans).length) plans[tableName] = tablePlans;
|
|
6000
6317
|
}
|
|
6001
6318
|
return plans;
|
|
6002
6319
|
};
|
|
6320
|
+
/**
|
|
6321
|
+
* Which of the target's (or junction's) write features each input of a relation needs. A
|
|
6322
|
+
* parent-side `connect` / `disconnect` only rewrites the parent's own column, so it needs
|
|
6323
|
+
* nothing of the target; a child-side one moves the target's foreign key, which is an update
|
|
6324
|
+
* of the target. A to-one child-side `create` also detaches the row attached before it, when
|
|
6325
|
+
* the key is nullable.
|
|
6326
|
+
*/
|
|
6327
|
+
const requiredWrites = (plan, op) => {
|
|
6328
|
+
if (plan.through) {
|
|
6329
|
+
const junction = plan.through.tableName;
|
|
6330
|
+
if (op === "connect") return [{
|
|
6331
|
+
tableName: junction,
|
|
6332
|
+
feature: "insert"
|
|
6333
|
+
}];
|
|
6334
|
+
if (op === "disconnect") return [{
|
|
6335
|
+
tableName: junction,
|
|
6336
|
+
feature: "delete"
|
|
6337
|
+
}];
|
|
6338
|
+
if (op === "set") return [{
|
|
6339
|
+
tableName: junction,
|
|
6340
|
+
feature: "delete"
|
|
6341
|
+
}, {
|
|
6342
|
+
tableName: junction,
|
|
6343
|
+
feature: "insert"
|
|
6344
|
+
}];
|
|
6345
|
+
return [];
|
|
6346
|
+
}
|
|
6347
|
+
const target = plan.targetTableName;
|
|
6348
|
+
if (op === "create") return plan.fkSide === "child" && plan.isOne && plan.canDetach ? [{
|
|
6349
|
+
tableName: target,
|
|
6350
|
+
feature: "insert"
|
|
6351
|
+
}, {
|
|
6352
|
+
tableName: target,
|
|
6353
|
+
feature: "update"
|
|
6354
|
+
}] : [{
|
|
6355
|
+
tableName: target,
|
|
6356
|
+
feature: "insert"
|
|
6357
|
+
}];
|
|
6358
|
+
return plan.fkSide === "child" ? [{
|
|
6359
|
+
tableName: target,
|
|
6360
|
+
feature: "update"
|
|
6361
|
+
}] : [];
|
|
6362
|
+
};
|
|
6363
|
+
/**
|
|
6364
|
+
* Narrows every plan's `allowed` inputs to the ones whose writes the table they land on
|
|
6365
|
+
* permits: its `features.insert` / `update` / `delete`, and for a junction table that it is
|
|
6366
|
+
* not excluded. A plan left with nothing is dropped, so its relation field is not generated.
|
|
6367
|
+
*
|
|
6368
|
+
* Returns one line per input taken out, for the build note.
|
|
6369
|
+
*/
|
|
6370
|
+
const gateNestedWritePlans = (plans, featureOf, isExcluded) => {
|
|
6371
|
+
const omitted = [];
|
|
6372
|
+
for (const [tableName, tablePlans] of Object.entries(plans)) {
|
|
6373
|
+
for (const [relationName, plan] of Object.entries(tablePlans)) {
|
|
6374
|
+
for (const op of [...plan.allowed]) {
|
|
6375
|
+
const blocked = requiredWrites(plan, op).filter(({ tableName: target, feature }) => target !== void 0 && (isExcluded(target) || !featureOf(target)[feature]));
|
|
6376
|
+
if (blocked.length) {
|
|
6377
|
+
plan.allowed.delete(op);
|
|
6378
|
+
const reasons = blocked.map(({ tableName: target, feature }) => isExcluded(target) ? `${target} is excluded` : `${target}.${feature} is off`);
|
|
6379
|
+
omitted.push(`${tableName}.${relationName}.${op} (${[...new Set(reasons)].join(", ")})`);
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6382
|
+
if (!plan.allowed.size) delete tablePlans[relationName];
|
|
6383
|
+
}
|
|
6384
|
+
if (!Object.keys(tablePlans).length) delete plans[tableName];
|
|
6385
|
+
}
|
|
6386
|
+
return omitted;
|
|
6387
|
+
};
|
|
6003
6388
|
const NO_COLUMNS = /* @__PURE__ */ new Set();
|
|
6004
6389
|
/**
|
|
6005
6390
|
* Builds the nested-write input types for a schema.
|
|
@@ -6054,8 +6439,11 @@ const createNestedWriteTypes = (params) => {
|
|
|
6054
6439
|
*
|
|
6055
6440
|
* A many-to-many relation offers no `create`: inserting a row *and* linking it is two
|
|
6056
6441
|
* writes to two tables, and the link half is what the relation field is for. Its
|
|
6057
|
-
* `disconnect` and `set`
|
|
6442
|
+
* `disconnect` and `set` do not depend on nullability, since breaking a link deletes a
|
|
6058
6443
|
* junction row rather than nulling a column that might be NOT NULL.
|
|
6444
|
+
*
|
|
6445
|
+
* Each input is also subject to `plan.allowed` — the table it writes has to have that
|
|
6446
|
+
* write turned on (see {@link gateNestedWritePlans}).
|
|
6059
6447
|
*/
|
|
6060
6448
|
const wrapperType = (tableName, typeName, plan, forUpdate) => {
|
|
6061
6449
|
const key = `${tableName}.${plan.relationName}.${forUpdate ? "update" : "create"}`;
|
|
@@ -6064,57 +6452,52 @@ const createNestedWriteTypes = (params) => {
|
|
|
6064
6452
|
const filters = targetFilters(plan);
|
|
6065
6453
|
const targetTypeName = resolveTypeName(plan.targetTableName, typeNameMapper);
|
|
6066
6454
|
const fields = {};
|
|
6455
|
+
const offers = (op) => plan.allowed.has(op);
|
|
6067
6456
|
if (plan.through) {
|
|
6068
|
-
if (
|
|
6069
|
-
fields["connect"] = {
|
|
6457
|
+
if (filters && offers("connect")) fields["connect"] = {
|
|
6070
6458
|
type: listOf(filters),
|
|
6071
6459
|
description: `Link every existing ${targetTypeName} row these match. Already-linked rows are left alone.`
|
|
6072
6460
|
};
|
|
6073
|
-
if (forUpdate) {
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
};
|
|
6082
|
-
}
|
|
6461
|
+
if (filters && forUpdate && offers("disconnect")) fields["disconnect"] = {
|
|
6462
|
+
type: listOf(filters),
|
|
6463
|
+
description: `Unlink every linked ${targetTypeName} row these match, leaving the rows in place`
|
|
6464
|
+
};
|
|
6465
|
+
if (filters && forUpdate && offers("set")) fields["set"] = {
|
|
6466
|
+
type: listOf(filters),
|
|
6467
|
+
description: `Replace the whole set: unlink everything linked, then link every ${targetTypeName} row these match. \`set: []\` clears the relation. Applied before \`disconnect\` and \`connect\`.`
|
|
6468
|
+
};
|
|
6083
6469
|
} else if (plan.isOne) {
|
|
6084
|
-
fields["create"] = {
|
|
6470
|
+
if (offers("create")) fields["create"] = {
|
|
6085
6471
|
type: payloadType(tableName, typeName, plan),
|
|
6086
6472
|
description: `Insert a ${targetTypeName} row and attach it`
|
|
6087
6473
|
};
|
|
6088
|
-
if (filters) fields["connect"] = {
|
|
6474
|
+
if (filters && offers("connect")) fields["connect"] = {
|
|
6089
6475
|
type: filters,
|
|
6090
6476
|
description: `Attach the existing ${targetTypeName} row this matches. Must match exactly one row.`
|
|
6091
6477
|
};
|
|
6092
|
-
if (forUpdate &&
|
|
6478
|
+
if (forUpdate && offers("disconnect")) fields["disconnect"] = {
|
|
6093
6479
|
type: graphql.GraphQLBoolean,
|
|
6094
6480
|
description: `Detach the currently attached ${targetTypeName} row, leaving it in place`
|
|
6095
6481
|
};
|
|
6096
6482
|
} else {
|
|
6097
|
-
fields["create"] = {
|
|
6483
|
+
if (offers("create")) fields["create"] = {
|
|
6098
6484
|
type: listOf(payloadType(tableName, typeName, plan)),
|
|
6099
6485
|
description: `Insert these ${targetTypeName} rows and attach them`
|
|
6100
6486
|
};
|
|
6101
|
-
if (filters) {
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
description: `Replace the whole set: detach everything attached, then attach every ${targetTypeName} row these match. Applied before \`disconnect\`, \`connect\` and \`create\`.`
|
|
6114
|
-
};
|
|
6115
|
-
}
|
|
6116
|
-
}
|
|
6487
|
+
if (filters && offers("connect")) fields["connect"] = {
|
|
6488
|
+
type: listOf(filters),
|
|
6489
|
+
description: `Attach every existing ${targetTypeName} row these match`
|
|
6490
|
+
};
|
|
6491
|
+
if (filters && forUpdate && offers("disconnect")) fields["disconnect"] = {
|
|
6492
|
+
type: listOf(filters),
|
|
6493
|
+
description: `Detach every attached ${targetTypeName} row these match, leaving the rows in place`
|
|
6494
|
+
};
|
|
6495
|
+
if (filters && forUpdate && offers("set")) fields["set"] = {
|
|
6496
|
+
type: listOf(filters),
|
|
6497
|
+
description: `Replace the whole set: detach everything attached, then attach every ${targetTypeName} row these match. Applied before \`disconnect\`, \`connect\` and \`create\`.`
|
|
6498
|
+
};
|
|
6117
6499
|
}
|
|
6500
|
+
if (!Object.keys(fields).length) return;
|
|
6118
6501
|
const type = new graphql.GraphQLInputObjectType({
|
|
6119
6502
|
name: cacheCtx.typeName({
|
|
6120
6503
|
kind: "nestedWriteInput",
|
|
@@ -6146,7 +6529,7 @@ const createNestedWriteTypes = (params) => {
|
|
|
6146
6529
|
const tablePlans = plans[tableName];
|
|
6147
6530
|
if (!tablePlans) return NO_COLUMNS;
|
|
6148
6531
|
const relaxed = /* @__PURE__ */ new Set();
|
|
6149
|
-
for (const plan of Object.values(tablePlans)) if (plan.fkSide === "parent" && !isOptionalOnInsert(plan.localCol)) relaxed.add(plan.localColPropName);
|
|
6532
|
+
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);
|
|
6150
6533
|
return relaxed.size ? relaxed : NO_COLUMNS;
|
|
6151
6534
|
}
|
|
6152
6535
|
};
|
|
@@ -6157,8 +6540,99 @@ const createNestedWriteTypes = (params) => {
|
|
|
6157
6540
|
*/
|
|
6158
6541
|
const mergedOps = (entries) => Object.assign({}, ...entries.map((entry) => entry.ops));
|
|
6159
6542
|
const asArray = (value) => Array.isArray(value) ? value : value === void 0 ? [] : [value];
|
|
6543
|
+
const supplied = (value) => value !== void 0 && value !== null;
|
|
6544
|
+
/**
|
|
6545
|
+
* The hook calls one relation field makes. Every step is registered before the field's first
|
|
6546
|
+
* statement, so each `before` runs ahead of all of them and each `after` once the last has
|
|
6547
|
+
* returned. A table without hooks registers nothing, and its statements run exactly as they
|
|
6548
|
+
* did before hooks reached nested writes — no RETURNING clause is added.
|
|
6549
|
+
*/
|
|
6550
|
+
const createHookSteps = (onWrite, executor, request, parentTable, plan) => {
|
|
6551
|
+
const steps = /* @__PURE__ */ new Map();
|
|
6552
|
+
const keyOf = (op, operation) => `${op}:${operation}`;
|
|
6553
|
+
const payload = (step) => ({
|
|
6554
|
+
table: step.tableName,
|
|
6555
|
+
operation: step.operation,
|
|
6556
|
+
single: plan.isOne,
|
|
6557
|
+
args: request?.args,
|
|
6558
|
+
context: request?.context,
|
|
6559
|
+
info: request?.info,
|
|
6560
|
+
tx: executor,
|
|
6561
|
+
nested: {
|
|
6562
|
+
parentTable,
|
|
6563
|
+
relation: plan.relationName,
|
|
6564
|
+
op: step.op
|
|
6565
|
+
}
|
|
6566
|
+
});
|
|
6567
|
+
return {
|
|
6568
|
+
/** Registers the hook call for `op`'s `operation` statements, if that table has hooks. */
|
|
6569
|
+
add: (op, operation) => {
|
|
6570
|
+
const through = plan.through;
|
|
6571
|
+
const tableName = through ? through.tableName : plan.targetTableName;
|
|
6572
|
+
const hooks = tableName ? onWrite?.(tableName, operation) : void 0;
|
|
6573
|
+
if (!tableName || !hooks || steps.has(keyOf(op, operation))) return;
|
|
6574
|
+
const keyColumns = through ? {
|
|
6575
|
+
[through.sourceColPropName]: through.sourceCol,
|
|
6576
|
+
[through.targetColPropName]: through.targetCol
|
|
6577
|
+
} : { ...plan.targetKeyColumns };
|
|
6578
|
+
steps.set(keyOf(op, operation), {
|
|
6579
|
+
tableName,
|
|
6580
|
+
operation,
|
|
6581
|
+
op,
|
|
6582
|
+
hooks,
|
|
6583
|
+
returning: withHookColumns(hooks, keyColumns).returning,
|
|
6584
|
+
rows: []
|
|
6585
|
+
});
|
|
6586
|
+
},
|
|
6587
|
+
get: (op, operation) => steps.get(keyOf(op, operation)),
|
|
6588
|
+
/** Runs one statement, returning its rows to the step it belongs to when there is one. */
|
|
6589
|
+
run: async (query, op, operation) => {
|
|
6590
|
+
const step = steps.get(keyOf(op, operation));
|
|
6591
|
+
if (!step) {
|
|
6592
|
+
await query;
|
|
6593
|
+
return;
|
|
6594
|
+
}
|
|
6595
|
+
step.rows.push(...await query.returning(step.returning));
|
|
6596
|
+
},
|
|
6597
|
+
before: async () => {
|
|
6598
|
+
for (const step of steps.values()) await runWriteHook(step.hooks, "before", payload(step));
|
|
6599
|
+
},
|
|
6600
|
+
after: async () => {
|
|
6601
|
+
for (const step of steps.values()) await runWriteHook(step.hooks, "after", {
|
|
6602
|
+
...payload(step),
|
|
6603
|
+
rows: step.rows
|
|
6604
|
+
});
|
|
6605
|
+
}
|
|
6606
|
+
};
|
|
6607
|
+
};
|
|
6608
|
+
/**
|
|
6609
|
+
* Registers the hook calls a child-side relation field will make, in the order its
|
|
6610
|
+
* statements run, so the `before` hooks fire in that order too. Mirrors the branches of
|
|
6611
|
+
* `applyChildSide` and `applyThroughOps` exactly: a step registered for an input that runs no
|
|
6612
|
+
* statement would fire with no rows, and one missing would miss them.
|
|
6613
|
+
*/
|
|
6614
|
+
const registerChildSteps = (steps, plan, op) => {
|
|
6615
|
+
if (plan.through) {
|
|
6616
|
+
if (supplied(op["set"])) {
|
|
6617
|
+
steps.add("set", "delete");
|
|
6618
|
+
steps.add("set", "insert");
|
|
6619
|
+
}
|
|
6620
|
+
if (supplied(op["disconnect"])) steps.add("disconnect", "delete");
|
|
6621
|
+
if (supplied(op["connect"])) steps.add("connect", "insert");
|
|
6622
|
+
return;
|
|
6623
|
+
}
|
|
6624
|
+
if (op["set"] !== void 0) steps.add("set", "update");
|
|
6625
|
+
if (op["disconnect"] !== void 0 && (!plan.isOne || op["disconnect"] === true)) steps.add("disconnect", "update");
|
|
6626
|
+
const attachedBy = attachingOp(op);
|
|
6627
|
+
if (plan.isOne && attachedBy && plan.canDetach && op["set"] === void 0) steps.add(attachedBy, "update");
|
|
6628
|
+
if (op["connect"] !== void 0) steps.add("connect", "update");
|
|
6629
|
+
if (op["create"] !== void 0) steps.add("create", "insert");
|
|
6630
|
+
};
|
|
6631
|
+
/** The input that attaches a row to a to-one, which detaches whatever was attached before. */
|
|
6632
|
+
const attachingOp = (op) => op["connect"] !== void 0 ? "connect" : op["create"] !== void 0 ? "create" : void 0;
|
|
6160
6633
|
const createNestedWriteRuntime = (params) => {
|
|
6161
6634
|
const { plans, filterCtx, policies, contextValues } = params;
|
|
6635
|
+
const onWrite = policies?.onWrite;
|
|
6162
6636
|
/**
|
|
6163
6637
|
* A `connect` / `disconnect` / `set` operand compiled to SQL. An operand that compiles to
|
|
6164
6638
|
* nothing is rejected: `connect: {}` would otherwise attach every row in the table.
|
|
@@ -6169,12 +6643,12 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6169
6643
|
return withScope(scope, plan.targetTableName, plan.targetTable, condition);
|
|
6170
6644
|
};
|
|
6171
6645
|
const assertSingleOperation = (plan, op) => {
|
|
6172
|
-
const
|
|
6646
|
+
const given = [
|
|
6173
6647
|
"create",
|
|
6174
6648
|
"connect",
|
|
6175
6649
|
"disconnect"
|
|
6176
|
-
].filter((key) => op[key]
|
|
6177
|
-
if (
|
|
6650
|
+
].filter((key) => supplied(op[key]));
|
|
6651
|
+
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" });
|
|
6178
6652
|
};
|
|
6179
6653
|
/** The values the junction stores for the target rows one operand selects. */
|
|
6180
6654
|
const selectedTargetKeys = async (executor, plan, filter, operation, scope) => {
|
|
@@ -6189,9 +6663,8 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6189
6663
|
* error on some of them — the read and the write share the surrounding transaction, so no
|
|
6190
6664
|
* other writer can slip a duplicate in between them.
|
|
6191
6665
|
*/
|
|
6192
|
-
const applyThroughOps = async (executor, plan, key, op, scope) => {
|
|
6666
|
+
const applyThroughOps = async (executor, plan, key, op, scope, steps) => {
|
|
6193
6667
|
const through = plan.through;
|
|
6194
|
-
const unlinkAll = () => executor.delete(through.table).where((0, drizzle_orm.eq)(through.sourceCol, key));
|
|
6195
6668
|
const link = async (operands, operation) => {
|
|
6196
6669
|
const existing = new Set((await executor.select({ [through.targetColPropName]: through.targetCol }).from(through.table).where((0, drizzle_orm.eq)(through.sourceCol, key))).map((row) => row[through.targetColPropName]));
|
|
6197
6670
|
const rows = [];
|
|
@@ -6202,19 +6675,19 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6202
6675
|
[through.targetColPropName]: targetKey
|
|
6203
6676
|
});
|
|
6204
6677
|
}
|
|
6205
|
-
if (rows.length) await executor.insert(through.table).values(rows);
|
|
6678
|
+
if (rows.length) await steps.run(executor.insert(through.table).values(rows), operation, "insert");
|
|
6206
6679
|
};
|
|
6207
|
-
if (op["set"]
|
|
6208
|
-
await
|
|
6680
|
+
if (supplied(op["set"])) {
|
|
6681
|
+
await steps.run(executor.delete(through.table).where((0, drizzle_orm.eq)(through.sourceCol, key)), "set", "delete");
|
|
6209
6682
|
await link(asArray(op["set"]), "set");
|
|
6210
6683
|
}
|
|
6211
|
-
if (op["disconnect"]
|
|
6684
|
+
if (supplied(op["disconnect"])) for (const filter of asArray(op["disconnect"])) {
|
|
6212
6685
|
const targetKeys = await selectedTargetKeys(executor, plan, filter, "disconnect", scope);
|
|
6213
|
-
if (targetKeys.length) await executor.delete(through.table).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(through.sourceCol, key), (0, drizzle_orm.inArray)(through.targetCol, targetKeys)));
|
|
6686
|
+
if (targetKeys.length) await steps.run(executor.delete(through.table).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(through.sourceCol, key), (0, drizzle_orm.inArray)(through.targetCol, targetKeys))), "disconnect", "delete");
|
|
6214
6687
|
}
|
|
6215
|
-
if (op["connect"]
|
|
6688
|
+
if (supplied(op["connect"])) await link(asArray(op["connect"]), "connect");
|
|
6216
6689
|
};
|
|
6217
|
-
const detachAll =
|
|
6690
|
+
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, (0, drizzle_orm.eq)(plan.foreignCol, key))), op, "update");
|
|
6218
6691
|
return {
|
|
6219
6692
|
enabled: (tableName) => !!plans[tableName],
|
|
6220
6693
|
split: (tableName, values) => {
|
|
@@ -6243,19 +6716,27 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6243
6716
|
}
|
|
6244
6717
|
return columns;
|
|
6245
6718
|
},
|
|
6246
|
-
applyParentSide: async (executor, tableName, ops,
|
|
6719
|
+
applyParentSide: async (executor, tableName, ops, request) => {
|
|
6247
6720
|
const tablePlans = plans[tableName];
|
|
6248
6721
|
const patch = {};
|
|
6249
6722
|
if (!tablePlans) return patch;
|
|
6723
|
+
const context = request?.context;
|
|
6250
6724
|
const scope = resolveScope(policies, context, filterCtx);
|
|
6251
6725
|
for (const [relationName, op] of Object.entries(ops)) {
|
|
6252
6726
|
const plan = tablePlans[relationName];
|
|
6253
6727
|
if (plan?.fkSide !== "parent") continue;
|
|
6254
6728
|
assertSingleOperation(plan, op);
|
|
6255
6729
|
if (op["create"] !== void 0) {
|
|
6730
|
+
const steps = createHookSteps(onWrite, executor, request, tableName, plan);
|
|
6731
|
+
steps.add("create", "insert");
|
|
6732
|
+
await steps.before();
|
|
6256
6733
|
const values = applyContextValues(remapFromGraphQLSingleInput({ ...op["create"] }, plan.targetTable), contextValues?.(plan.targetTableName), context);
|
|
6257
|
-
const
|
|
6734
|
+
const step = steps.get("create", "insert");
|
|
6735
|
+
const inserted = await executor.insert(plan.targetTable).values(values).returning(step ? step.returning : { [plan.foreignColPropName]: plan.foreignCol });
|
|
6736
|
+
step?.rows.push(...inserted);
|
|
6737
|
+
const created = inserted[0];
|
|
6258
6738
|
if (!created) throw drizzleError(`Drizzle-GraphQL Error: 'create' on '${relationName}' inserted no row, so there is nothing to attach.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
6739
|
+
await steps.after();
|
|
6259
6740
|
patch[plan.localColPropName] = created[plan.foreignColPropName];
|
|
6260
6741
|
} else if (op["connect"] !== void 0) {
|
|
6261
6742
|
const rows = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
|
|
@@ -6265,41 +6746,46 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6265
6746
|
}
|
|
6266
6747
|
return patch;
|
|
6267
6748
|
},
|
|
6268
|
-
applyChildSide: async (executor, tableName, ops, parentRows,
|
|
6749
|
+
applyChildSide: async (executor, tableName, ops, parentRows, request) => {
|
|
6269
6750
|
const tablePlans = plans[tableName];
|
|
6270
6751
|
if (!tablePlans) return;
|
|
6752
|
+
const context = request?.context;
|
|
6271
6753
|
const scope = resolveScope(policies, context, filterCtx);
|
|
6272
6754
|
for (const [relationName, op] of Object.entries(ops)) {
|
|
6273
6755
|
const plan = tablePlans[relationName];
|
|
6274
6756
|
if (plan?.fkSide !== "child") continue;
|
|
6275
6757
|
if (plan.isOne) assertSingleOperation(plan, op);
|
|
6758
|
+
const steps = createHookSteps(onWrite, executor, request, tableName, plan);
|
|
6759
|
+
registerChildSteps(steps, plan, op);
|
|
6760
|
+
await steps.before();
|
|
6276
6761
|
for (const parentRow of parentRows) {
|
|
6277
6762
|
const key = parentRow?.[plan.localColPropName];
|
|
6278
6763
|
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" });
|
|
6279
6764
|
if (plan.through) {
|
|
6280
|
-
await applyThroughOps(executor, plan, key, op, scope);
|
|
6765
|
+
await applyThroughOps(executor, plan, key, op, scope, steps);
|
|
6281
6766
|
continue;
|
|
6282
6767
|
}
|
|
6283
6768
|
if (op["set"] !== void 0) {
|
|
6284
|
-
await detachAll(executor, plan, key, scope);
|
|
6285
|
-
for (const filter of asArray(op["set"])) await executor.update(plan.targetTable).set({ [plan.foreignColPropName]: key }).where(conditionOf(plan, filter, "set", scope));
|
|
6769
|
+
await detachAll(executor, plan, key, scope, steps, "set");
|
|
6770
|
+
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");
|
|
6286
6771
|
}
|
|
6287
6772
|
if (op["disconnect"] !== void 0) {
|
|
6288
6773
|
if (plan.isOne) {
|
|
6289
|
-
if (op["disconnect"] === true) await detachAll(executor, plan, key, scope);
|
|
6290
|
-
} else for (const filter of asArray(op["disconnect"])) await executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(plan.foreignCol, key), conditionOf(plan, filter, "disconnect", scope)));
|
|
6774
|
+
if (op["disconnect"] === true) await detachAll(executor, plan, key, scope, steps, "disconnect");
|
|
6775
|
+
} else for (const filter of asArray(op["disconnect"])) await steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(plan.foreignCol, key), conditionOf(plan, filter, "disconnect", scope))), "disconnect", "update");
|
|
6291
6776
|
}
|
|
6292
|
-
const
|
|
6293
|
-
if (plan.isOne &&
|
|
6294
|
-
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));
|
|
6777
|
+
const attachedBy = attachingOp(op);
|
|
6778
|
+
if (plan.isOne && attachedBy && plan.canDetach && op["set"] === void 0) await detachAll(executor, plan, key, scope, steps, attachedBy);
|
|
6779
|
+
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");
|
|
6295
6780
|
if (op["create"] !== void 0) {
|
|
6296
6781
|
const rows = asArray(op["create"]).map((row) => ({
|
|
6297
6782
|
...applyContextValues(remapFromGraphQLSingleInput({ ...row }, plan.targetTable), contextValues?.(plan.targetTableName), context),
|
|
6298
6783
|
[plan.foreignColPropName]: key
|
|
6299
6784
|
}));
|
|
6300
|
-
if (rows.length) await executor.insert(plan.targetTable).values(rows);
|
|
6785
|
+
if (rows.length) await steps.run(executor.insert(plan.targetTable).values(rows), "create", "insert");
|
|
6301
6786
|
}
|
|
6302
6787
|
}
|
|
6788
|
+
await steps.after();
|
|
6303
6789
|
}
|
|
6304
6790
|
}
|
|
6305
6791
|
};
|
|
@@ -6309,21 +6795,25 @@ const createNestedWriteRuntime = (params) => {
|
|
|
6309
6795
|
* inside a single transaction — a savepoint when the caller already runs on one, so a failing
|
|
6310
6796
|
* entry rolls the whole batch back.
|
|
6311
6797
|
*
|
|
6798
|
+
* That transaction is also the `tx` the target tables' `onWrite` hooks run on. Every nested
|
|
6799
|
+
* write opens it, so a hook on a nested target has the whole tree to roll back even when the
|
|
6800
|
+
* root field carries no hook of its own and was not forced into a transaction for one.
|
|
6801
|
+
*
|
|
6312
6802
|
* Entries are written one at a time rather than as a single multi-row statement: a nested
|
|
6313
6803
|
* operation attaches to the row its own entry produced, so the two have to stay correlated.
|
|
6314
6804
|
* The batch path is untouched when no entry carries a nested operation.
|
|
6315
6805
|
*/
|
|
6316
6806
|
const writeWithNestedOps = async (params) => {
|
|
6317
|
-
const { executor, runtime, tableName, entries, remapValues, write,
|
|
6807
|
+
const { executor, runtime, tableName, entries, remapValues, write, request } = params;
|
|
6318
6808
|
return executor.transaction(async (tx) => {
|
|
6319
6809
|
const rows = [];
|
|
6320
6810
|
for (const entry of entries) {
|
|
6321
|
-
const patch = await runtime.applyParentSide(tx, tableName, entry.ops,
|
|
6811
|
+
const patch = await runtime.applyParentSide(tx, tableName, entry.ops, request);
|
|
6322
6812
|
const written = await write(tx, {
|
|
6323
6813
|
...remapValues(entry.columns),
|
|
6324
6814
|
...patch
|
|
6325
6815
|
});
|
|
6326
|
-
await runtime.applyChildSide(tx, tableName, entry.ops, written,
|
|
6816
|
+
await runtime.applyChildSide(tx, tableName, entry.ops, written, request);
|
|
6327
6817
|
rows.push(...written);
|
|
6328
6818
|
}
|
|
6329
6819
|
return rows;
|
|
@@ -6335,14 +6825,14 @@ const writeWithNestedOps = async (params) => {
|
|
|
6335
6825
|
* attaches to each row that came back.
|
|
6336
6826
|
*/
|
|
6337
6827
|
const updateWithNestedOps = async (params) => {
|
|
6338
|
-
const { executor, runtime, tableName, columns, ops, remapValues, write,
|
|
6828
|
+
const { executor, runtime, tableName, columns, ops, remapValues, write, request } = params;
|
|
6339
6829
|
return executor.transaction(async (tx) => {
|
|
6340
|
-
const patch = await runtime.applyParentSide(tx, tableName, ops,
|
|
6830
|
+
const patch = await runtime.applyParentSide(tx, tableName, ops, request);
|
|
6341
6831
|
const rows = await write(tx, {
|
|
6342
6832
|
...remapValues(columns),
|
|
6343
6833
|
...patch
|
|
6344
6834
|
});
|
|
6345
|
-
await runtime.applyChildSide(tx, tableName, ops, rows,
|
|
6835
|
+
await runtime.applyChildSide(tx, tableName, ops, rows, request);
|
|
6346
6836
|
return rows;
|
|
6347
6837
|
});
|
|
6348
6838
|
};
|
|
@@ -6543,7 +7033,14 @@ const createSchemaBuilder = (adapter) => {
|
|
|
6543
7033
|
uniqueKeysOf: (tableName) => uniqueKeys[tableName]
|
|
6544
7034
|
};
|
|
6545
7035
|
adapter.preflight?.(db, options);
|
|
6546
|
-
const nestedPlans = features.nestedWrites ? buildNestedWritePlans(tables, namedRelations, (target) => uniqueColumnSets(target), (target) => primaryKeyPropNames(target), extractRelationJoinColumns) : void 0;
|
|
7036
|
+
const nestedPlans = features.nestedWrites ? buildNestedWritePlans(tables, namedRelations, (target) => uniqueColumnSets(target), (target) => primaryKeyPropNames(target), extractRelationJoinColumns, (junction) => schemaEntries.find(([, value]) => value === junction)?.[0]) : void 0;
|
|
7037
|
+
if (nestedPlans) {
|
|
7038
|
+
const omitted = gateNestedWritePlans(nestedPlans, featureOf, (tableName) => excludedTables.has(tableName));
|
|
7039
|
+
if (omitted.length) {
|
|
7040
|
+
const listed = omitted.length > 5 ? `${omitted.slice(0, 5).join(", ")} and ${omitted.length - 5} more` : omitted.join(", ");
|
|
7041
|
+
console.warn(`Drizzle-GraphQL Warning: config.features — nestedWrites leaves out the nested inputs whose table has that write turned off: ${listed}.`);
|
|
7042
|
+
}
|
|
7043
|
+
}
|
|
6547
7044
|
const nestedTypes = nestedPlans ? createNestedWriteTypes({
|
|
6548
7045
|
plans: nestedPlans,
|
|
6549
7046
|
cacheCtx,
|
|
@@ -7216,7 +7713,11 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
7216
7713
|
entries: nestedEntries,
|
|
7217
7714
|
remapValues: (values) => applyContextValues(remapFromGraphQLSingleInput(values, table), contextColumns, context),
|
|
7218
7715
|
write: (tx, values) => runInsert(tx, [values]),
|
|
7219
|
-
|
|
7716
|
+
request: {
|
|
7717
|
+
context,
|
|
7718
|
+
args,
|
|
7719
|
+
info
|
|
7720
|
+
}
|
|
7220
7721
|
}) : await runInsert(executor, input);
|
|
7221
7722
|
await after(written);
|
|
7222
7723
|
const result = output(written);
|
|
@@ -7305,7 +7806,11 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
7305
7806
|
entries: nestedEntries,
|
|
7306
7807
|
remapValues: (values) => applyContextValues(remapFromGraphQLSingleInput(values, table), contextColumns, context),
|
|
7307
7808
|
write: (tx, values) => runUpsert(tx, [values]),
|
|
7308
|
-
|
|
7809
|
+
request: {
|
|
7810
|
+
context,
|
|
7811
|
+
args,
|
|
7812
|
+
info
|
|
7813
|
+
}
|
|
7309
7814
|
}) : await runUpsert(executor, input);
|
|
7310
7815
|
await after(written);
|
|
7311
7816
|
const result = output(written);
|
|
@@ -7378,7 +7883,11 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
7378
7883
|
ops: nestedOps,
|
|
7379
7884
|
remapValues: (values) => values,
|
|
7380
7885
|
write: runUpdate,
|
|
7381
|
-
|
|
7886
|
+
request: {
|
|
7887
|
+
context,
|
|
7888
|
+
args,
|
|
7889
|
+
info
|
|
7890
|
+
}
|
|
7382
7891
|
}) : await runUpdate(executor, input);
|
|
7383
7892
|
await after(written);
|
|
7384
7893
|
const result = output(written);
|
|
@@ -7808,18 +8317,18 @@ const createSchemaDataGenerator = (adapter) => {
|
|
|
7808
8317
|
* the shared multi-mutation transaction opened by `runMutation` — this opens a savepoint, so
|
|
7809
8318
|
* the batch stays atomic without breaking the outer transaction.
|
|
7810
8319
|
*/
|
|
7811
|
-
const runUpdateManyBatch = ({ executor, entries, table, returning, nested, tableName,
|
|
8320
|
+
const runUpdateManyBatch = ({ executor, entries, table, returning, nested, tableName, request }) => executor.transaction(async (tx) => {
|
|
7812
8321
|
const results = [];
|
|
7813
8322
|
for (const entry of entries) {
|
|
7814
8323
|
const values = entry.ops ? {
|
|
7815
8324
|
...entry.set,
|
|
7816
|
-
...await nested.applyParentSide(tx, tableName, entry.ops,
|
|
8325
|
+
...await nested.applyParentSide(tx, tableName, entry.ops, request)
|
|
7817
8326
|
} : entry.set;
|
|
7818
8327
|
const writes = Object.keys(values).length > 0;
|
|
7819
8328
|
let query = writes ? tx.update(table).set(values) : tx.select(returning).from(table);
|
|
7820
8329
|
if (entry.filters) query = query.where(entry.filters);
|
|
7821
8330
|
const rows = await (writes ? query.returning(returning) : query);
|
|
7822
|
-
if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows,
|
|
8331
|
+
if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows, request);
|
|
7823
8332
|
results.push(rows);
|
|
7824
8333
|
}
|
|
7825
8334
|
return results;
|
|
@@ -7892,7 +8401,11 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
|
|
|
7892
8401
|
anyNested,
|
|
7893
8402
|
nested,
|
|
7894
8403
|
tableName,
|
|
7895
|
-
|
|
8404
|
+
request: {
|
|
8405
|
+
context,
|
|
8406
|
+
args,
|
|
8407
|
+
info
|
|
8408
|
+
}
|
|
7896
8409
|
});
|
|
7897
8410
|
const written = perEntry.flat();
|
|
7898
8411
|
await after(written);
|
|
@@ -7941,7 +8454,7 @@ const sqlitePrimaryKeyPropNames = (table) => getPrimaryKeyPropNamesFromConfig(ta
|
|
|
7941
8454
|
* would run after the COMMIT. The driver kind is probed from the first statement's result
|
|
7942
8455
|
* rather than declared, so the same resolver serves both driver families.
|
|
7943
8456
|
*/
|
|
7944
|
-
const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNested, nested, tableName,
|
|
8457
|
+
const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNested, nested, tableName, request }) => {
|
|
7945
8458
|
const runEntry = (tx, entry) => {
|
|
7946
8459
|
let query = tx.update(table).set(entry.set);
|
|
7947
8460
|
if (entry.filters) query = query.where(entry.filters);
|
|
@@ -7950,13 +8463,13 @@ const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNest
|
|
|
7950
8463
|
const runNestedEntry = async (tx, entry) => {
|
|
7951
8464
|
const values = entry.ops ? {
|
|
7952
8465
|
...entry.set,
|
|
7953
|
-
...await nested.applyParentSide(tx, tableName, entry.ops,
|
|
8466
|
+
...await nested.applyParentSide(tx, tableName, entry.ops, request)
|
|
7954
8467
|
} : entry.set;
|
|
7955
8468
|
const writes = Object.keys(values).length > 0;
|
|
7956
8469
|
let query = writes ? tx.update(table).set(values) : tx.select(returning).from(table);
|
|
7957
8470
|
if (entry.filters) query = query.where(entry.filters);
|
|
7958
8471
|
const rows = await (writes ? query.returning(returning) : query);
|
|
7959
|
-
if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows,
|
|
8472
|
+
if (entry.ops) await nested.applyChildSide(tx, tableName, entry.ops, rows, request);
|
|
7960
8473
|
return rows;
|
|
7961
8474
|
};
|
|
7962
8475
|
return executor.transaction((tx) => {
|
|
@@ -8167,7 +8680,7 @@ const buildSchema = (db, config) => {
|
|
|
8167
8680
|
};
|
|
8168
8681
|
//#endregion
|
|
8169
8682
|
exports.GraphQLBigIntString = GraphQLBigIntString;
|
|
8170
|
-
exports.GraphQLDate =
|
|
8683
|
+
exports.GraphQLDate = GraphQLDate;
|
|
8171
8684
|
exports.GraphQLDateTime = graphql_scalars.GraphQLDateTime;
|
|
8172
8685
|
exports.GraphQLDecimalString = GraphQLDecimalString;
|
|
8173
8686
|
exports.GraphQLJSON = graphql_scalars.GraphQLJSON;
|