@vantreeseba/drizzle-graphql 14.0.0 → 15.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -75
- package/dist/index.cjs +541 -156
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -4
- package/dist/index.d.ts +32 -4
- package/dist/index.js +541 -156
- 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,192 @@ 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
|
+
* The junction table and its two columns for a `.through()` (many-to-many) relation:
|
|
1817
|
+
* `sourceCol` holds the parent's key, `targetCol` the target's. `undefined` for a relation
|
|
1818
|
+
* with no junction; `null` for one whose junction cannot be used here — more than one column
|
|
1819
|
+
* on either side, or columns that are not on the junction table.
|
|
1820
|
+
*/
|
|
1821
|
+
const extractJunctionColumns = (relEntry) => {
|
|
1822
|
+
const rel = relEntry.relation ?? relEntry;
|
|
1823
|
+
const junction = rel?.throughTable;
|
|
1824
|
+
if (!junction) return;
|
|
1825
|
+
const sourceEntries = rel.through?.source;
|
|
1826
|
+
const targetEntries = rel.through?.target;
|
|
1827
|
+
if (sourceEntries?.length !== 1 || targetEntries?.length !== 1) return null;
|
|
1828
|
+
const junctionCols = Object.values((0, drizzle_orm.getColumns)(junction));
|
|
1829
|
+
const sourceCol = unwrapRelationColumn(sourceEntries[0]);
|
|
1830
|
+
const targetCol = unwrapRelationColumn(targetEntries[0]);
|
|
1831
|
+
if (!sourceCol || !targetCol || !junctionCols.includes(sourceCol) || !junctionCols.includes(targetCol)) return null;
|
|
1832
|
+
return {
|
|
1833
|
+
table: junction,
|
|
1834
|
+
sourceCol,
|
|
1835
|
+
targetCol
|
|
1836
|
+
};
|
|
1837
|
+
};
|
|
1838
|
+
/**
|
|
1839
|
+
* The relation's own declared `where` as a condition on a query that reads the target table,
|
|
1840
|
+
* or `undefined` when it has none. The eager path gets this from drizzle's `with:`; the batched
|
|
1841
|
+
* field resolver and `<relation>Aggregate` read the target table directly, so they have to add
|
|
1842
|
+
* it themselves or they return rows the relation hides.
|
|
1843
|
+
*
|
|
1844
|
+
* A reversed filter (inherited from the relation on the other side) is written against the
|
|
1845
|
+
* parent table, so it is applied as "the row's parent key belongs to a parent that passes it".
|
|
1846
|
+
* `parentKeyCol` is the parent's join column and `groupCol` the column in the query that holds
|
|
1847
|
+
* the parent key for each row — the target's foreign key, or the junction's source column.
|
|
1848
|
+
*/
|
|
1849
|
+
const relationOwnCondition = (relEntry, parentTable, targetTable, parentKeyCol, groupCol) => {
|
|
1850
|
+
const rel = relEntry.relation ?? relEntry;
|
|
1851
|
+
const ownWhere = relationOwnWhere(rel);
|
|
1852
|
+
if (!ownWhere) return;
|
|
1853
|
+
if (!isRelationFilterReversed(rel)) return (0, drizzle_orm.relationsFilterToSQL)(targetTable, ownWhere);
|
|
1854
|
+
const parentFilter = (0, drizzle_orm.relationsFilterToSQL)(parentTable, ownWhere);
|
|
1855
|
+
return drizzle_orm.sql`${groupCol} in (select ${parentKeyCol} from ${parentTable} where ${parentFilter})`;
|
|
1856
|
+
};
|
|
1857
|
+
/**
|
|
1858
|
+
* Whether a to-one relation can back a relation orderBy hop. `.through()` (junction)
|
|
1859
|
+
* relations are excluded — the ordering subquery only joins the direct target, never the
|
|
1860
|
+
* junction table. (Relation *filters* do support `.through()` — this guard is orderBy-only.)
|
|
1861
|
+
*/
|
|
1862
|
+
const isFilterableRelation = (relation) => !relation.through;
|
|
1863
|
+
//#endregion
|
|
1482
1864
|
//#region src/util/builders/common/exclusions.ts
|
|
1483
1865
|
/**
|
|
1484
1866
|
* Columns this build keeps out of the generated schema, keyed by the table object.
|
|
@@ -1654,8 +2036,8 @@ const relationFilterCtx = (base, tableKey) => {
|
|
|
1654
2036
|
* the parent is an aliased proxy (as it is inside a relational `with:` where callback).
|
|
1655
2037
|
*/
|
|
1656
2038
|
const buildRelationJoinCondition = (parentTable, relation, aliasedTarget, relationName) => {
|
|
1657
|
-
const sourceColumns = relation
|
|
1658
|
-
const targetColumns = relation
|
|
2039
|
+
const sourceColumns = relationColumns(relation, "source");
|
|
2040
|
+
const targetColumns = relationColumns(relation, "target");
|
|
1659
2041
|
if (!sourceColumns?.length || sourceColumns.length !== targetColumns?.length) throw drizzleError(`WHERE ${relationName}: Relation cannot be used as a filter`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1660
2042
|
const parentColumns = Object.values((0, drizzle_orm.getColumns)(parentTable));
|
|
1661
2043
|
const targetColumnsByName = Object.values((0, drizzle_orm.getColumns)(aliasedTarget));
|
|
@@ -1679,7 +2061,7 @@ const resolveJunctionColumn = (aliasedThrough, junctionRef, relationName) => {
|
|
|
1679
2061
|
const key = junctionRef?._?.key;
|
|
1680
2062
|
const byKey = key ? throughColumns[key] : void 0;
|
|
1681
2063
|
if (byKey) return byKey;
|
|
1682
|
-
const columnName = junctionRef?.
|
|
2064
|
+
const columnName = unwrapRelationColumn(junctionRef)?.name;
|
|
1683
2065
|
const byName = columnName ? Object.values(throughColumns).find((c) => c.name === columnName) : void 0;
|
|
1684
2066
|
if (!byName) throw drizzleError(`WHERE ${relationName}: Relation cannot be used as a filter`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1685
2067
|
return byName;
|
|
@@ -1693,8 +2075,8 @@ const resolveJunctionColumn = (aliasedThrough, junctionRef, relationName) => {
|
|
|
1693
2075
|
* `through` metadata.
|
|
1694
2076
|
*/
|
|
1695
2077
|
const buildThroughJoinConditions = (parentTable, relation, aliasedThrough, aliasedTarget, relationName) => {
|
|
1696
|
-
const sourceColumns = relation
|
|
1697
|
-
const targetColumns = relation
|
|
2078
|
+
const sourceColumns = relationColumns(relation, "source");
|
|
2079
|
+
const targetColumns = relationColumns(relation, "target");
|
|
1698
2080
|
const through = relation.through;
|
|
1699
2081
|
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
2082
|
const parentColumns = Object.values((0, drizzle_orm.getColumns)(parentTable));
|
|
@@ -1746,7 +2128,8 @@ const buildRelationExists = (parentTable, relationName, relEntry, innerFilters,
|
|
|
1746
2128
|
fromClause = (0, drizzle_orm.getTableAsAliasSQL)(aliasedTarget);
|
|
1747
2129
|
joinCondition = buildRelationJoinCondition(parentTable, relation, aliasedTarget, relationName);
|
|
1748
2130
|
}
|
|
1749
|
-
const
|
|
2131
|
+
const ownWhere = relationOwnWhere(relation);
|
|
2132
|
+
const relationWhere = ownWhere ? (0, drizzle_orm.relationsFilterToSQL)(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
|
|
1750
2133
|
const inner = innerFilters ? extractFilters(aliasedTarget, targetTableName, innerFilters, {
|
|
1751
2134
|
...ctx,
|
|
1752
2135
|
tableKey: targetTableName,
|
|
@@ -1800,7 +2183,8 @@ const extractFilters = (table, tableName, filters, relationCtx) => {
|
|
|
1800
2183
|
}
|
|
1801
2184
|
if (NOT) {
|
|
1802
2185
|
const extracted = extractFilters(table, tableName, NOT, relationCtx);
|
|
1803
|
-
|
|
2186
|
+
const negated = extracted ? (0, drizzle_orm.not)(extracted) : void 0;
|
|
2187
|
+
if (negated) variants.push(negated);
|
|
1804
2188
|
}
|
|
1805
2189
|
if (OR?.length) {
|
|
1806
2190
|
const orVariants = [];
|
|
@@ -2197,88 +2581,6 @@ const excludedColumnRef = (columnName) => drizzle_orm.sql`excluded.${drizzle_orm
|
|
|
2197
2581
|
/** `values(<column>)` — MySQL's equivalent inside ON DUPLICATE KEY UPDATE. */
|
|
2198
2582
|
const mysqlValuesColumnRef = (columnName) => drizzle_orm.sql`values(${drizzle_orm.sql.identifier(columnName)})`;
|
|
2199
2583
|
//#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
2584
|
//#region src/util/builders/common/order-by.ts
|
|
2283
2585
|
/**
|
|
2284
2586
|
* Column name / direction / nulls triples from an `orderBy` argument, highest priority
|
|
@@ -2324,7 +2626,8 @@ const extendRelationOrderChain = (parentTable, relationName, relEntry, ctx, chai
|
|
|
2324
2626
|
ctx.aliases ??= { n: 0 };
|
|
2325
2627
|
const aliasedTarget = (0, drizzle_orm.aliasedTable)(targetTable, `dgql_ord_${ctx.aliases.n++}`);
|
|
2326
2628
|
const joinCondition = buildRelationJoinCondition(parentTable, relation, aliasedTarget, relationName);
|
|
2327
|
-
const
|
|
2629
|
+
const ownWhere = relationOwnWhere(relation);
|
|
2630
|
+
const relationWhere = ownWhere ? (0, drizzle_orm.relationsFilterToSQL)(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
|
|
2328
2631
|
return {
|
|
2329
2632
|
fromParts: [...chain?.fromParts ?? [], (0, drizzle_orm.getTableAsAliasSQL)(aliasedTarget)],
|
|
2330
2633
|
conditions: [
|
|
@@ -2341,15 +2644,17 @@ const extendRelationOrderChain = (parentTable, relationName, relEntry, ctx, chai
|
|
|
2341
2644
|
*
|
|
2342
2645
|
* A ladder rather than a dialect array function (`array_position` on Postgres, `FIELD()` on
|
|
2343
2646
|
* 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
|
-
*
|
|
2647
|
+
* list value stays a bound parameter — encoded by the column, as `eq` would, so a value whose
|
|
2648
|
+
* driver form differs from its JS one (a SQLite integer timestamp's `Date`) is bound as stored.
|
|
2649
|
+
* The branch indices are library-generated integers, so they are written into the SQL text — a
|
|
2650
|
+
* bound parameter there would leave Postgres unable to infer the CASE result type. Values
|
|
2651
|
+
* outside the list sort after every listed one.
|
|
2347
2652
|
*/
|
|
2348
2653
|
const filterOrderExpression = (column, columnName, whereArgs) => {
|
|
2349
2654
|
const values = whereArgs?.[columnName]?.inArray;
|
|
2350
2655
|
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
2656
|
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))}`);
|
|
2657
|
+
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
2658
|
return drizzle_orm.sql`case ${drizzle_orm.sql.join(branches, drizzle_orm.sql` `)} else ${drizzle_orm.sql.raw(String(values.length))} end`;
|
|
2354
2659
|
};
|
|
2355
2660
|
/**
|
|
@@ -2713,6 +3018,11 @@ const getUniqueColumnSets = (table, getTableConfig) => {
|
|
|
2713
3018
|
//#endregion
|
|
2714
3019
|
//#region src/util/builders/common/distinct.ts
|
|
2715
3020
|
/**
|
|
3021
|
+
* Alias the parent key comes back under when the pass runs through a junction table. See
|
|
3022
|
+
* `selectDistinctKeys`' `junction` parameter.
|
|
3023
|
+
*/
|
|
3024
|
+
const DISTINCT_PARENT = "__dgql_distinct_parent";
|
|
3025
|
+
/**
|
|
2716
3026
|
* Keeps the first row of each distinct combination of the requested columns, following the
|
|
2717
3027
|
* query's own ordering, then applies `limit`/`offset` to what survives — and returns the
|
|
2718
3028
|
* surviving rows' primary key values in that order.
|
|
@@ -2725,9 +3035,14 @@ const getUniqueColumnSets = (table, getTableConfig) => {
|
|
|
2725
3035
|
* `partitionBy` prepends extra columns to the window's partition. A to-many relation field
|
|
2726
3036
|
* uses it to pass the foreign key, which makes the pass distinct *within each parent* rather
|
|
2727
3037
|
* than across the whole batch — the same meaning `distinct` has on a root list.
|
|
3038
|
+
*
|
|
3039
|
+
* `junction` is for a many-to-many relation: the target is joined to the junction `on` the
|
|
3040
|
+
* given condition, and each returned key also carries the parent it was kept for, under
|
|
3041
|
+
* {@link DISTINCT_PARENT}. A target row linked to several parents can be kept for one and
|
|
3042
|
+
* dropped for another, so a key alone cannot narrow such a batch.
|
|
2728
3043
|
*/
|
|
2729
3044
|
const selectDistinctKeys = async (params) => {
|
|
2730
|
-
const { db, table, tableName, distinct, pkNames, where, orderBy, limit, offset, partitionBy } = params;
|
|
3045
|
+
const { db, table, tableName, distinct, pkNames, where, orderBy, limit, offset, partitionBy, junction } = params;
|
|
2731
3046
|
const cols = (0, drizzle_orm.getColumns)(table);
|
|
2732
3047
|
if (!pkNames.length) throw drizzleError(`Table ${tableName} has no primary key, so 'distinct' cannot be applied to it.`, { code: "DRIZZLE_INVALID_DISTINCT" });
|
|
2733
3048
|
const requestedCols = distinct.map((name) => cols[name]).filter(Boolean);
|
|
@@ -2736,12 +3051,17 @@ const selectDistinctKeys = async (params) => {
|
|
|
2736
3051
|
const orderEntries = orderBy ? orderByEntries(orderBy) : [];
|
|
2737
3052
|
const windowOrder = [...orderEntries.flatMap(([column, direction, nulls]) => orderExpressions(cols[column], direction, nulls, cols[column])), ...primaryKeyOrderExprs(table, pkNames)];
|
|
2738
3053
|
const rowNumber = drizzle_orm.sql`row_number() over (partition by ${drizzle_orm.sql.join(partitionCols, drizzle_orm.sql`, `)} order by ${drizzle_orm.sql.join(windowOrder, drizzle_orm.sql`, `)})`.as(DISTINCT_RN);
|
|
2739
|
-
const
|
|
3054
|
+
const selected = db.select({
|
|
2740
3055
|
...cols,
|
|
3056
|
+
...junction ? { [DISTINCT_PARENT]: junction.parentKey } : {},
|
|
2741
3057
|
[DISTINCT_RN]: rowNumber
|
|
2742
|
-
}).from(table)
|
|
3058
|
+
}).from(table);
|
|
3059
|
+
const sub = (junction ? selected.innerJoin(junction.table, junction.on) : selected).where(where).as("__dgql_distinct");
|
|
2743
3060
|
const outerOrder = [...orderEntries.flatMap(([column, direction, nulls]) => orderExpressions(sub[column], direction, nulls, cols[column])), ...pkNames.filter((name) => sub[name]).map((name) => (0, drizzle_orm.asc)(sub[name]))];
|
|
2744
|
-
let query = db.select(
|
|
3061
|
+
let query = db.select({
|
|
3062
|
+
...Object.fromEntries(pkNames.map((name) => [name, sub[name]])),
|
|
3063
|
+
...junction ? { [DISTINCT_PARENT]: sub[DISTINCT_PARENT] } : {}
|
|
3064
|
+
}).from(sub).where((0, drizzle_orm.eq)(sub[DISTINCT_RN], 1)).orderBy(...outerOrder);
|
|
2745
3065
|
if (offset) query = query.offset(offset);
|
|
2746
3066
|
if (limit != null) query = query.limit(limit);
|
|
2747
3067
|
return await query;
|
|
@@ -3315,6 +3635,26 @@ const remapUpdateInput = (values, table, tableName) => {
|
|
|
3315
3635
|
} : remapped;
|
|
3316
3636
|
};
|
|
3317
3637
|
//#endregion
|
|
3638
|
+
//#region src/util/builders/common/rqb-config.ts
|
|
3639
|
+
/**
|
|
3640
|
+
* A copy of a relational query config with every key whose value is `undefined` left out.
|
|
3641
|
+
*
|
|
3642
|
+
* drizzle-orm rc.5 snapshots test `"where" in params` rather than its value, and throw
|
|
3643
|
+
* `Unexpected 'undefined' in filter value` for `{ where: undefined }`; rc.4 read the value and
|
|
3644
|
+
* treated both the same. The library builds its configs with conditional keys
|
|
3645
|
+
* (`where: cond ? … : undefined`), so every config goes through here before it reaches drizzle.
|
|
3646
|
+
* Only `where` is checked that way today, but leaving out every undefined key costs nothing
|
|
3647
|
+
* and keeps the next key that gets the same treatment from breaking queries.
|
|
3648
|
+
*
|
|
3649
|
+
* Shallow on purpose: nested `with:` entries are built (and passed through here) one level at
|
|
3650
|
+
* a time, and the values of the other keys are drizzle's own (column maps, callbacks, RAW).
|
|
3651
|
+
*/
|
|
3652
|
+
const omitUndefinedKeys = (config) => {
|
|
3653
|
+
const out = {};
|
|
3654
|
+
for (const [key, value] of Object.entries(config)) if (value !== void 0) out[key] = value;
|
|
3655
|
+
return out;
|
|
3656
|
+
};
|
|
3657
|
+
//#endregion
|
|
3318
3658
|
//#region src/util/builders/common/selected-columns.ts
|
|
3319
3659
|
const rqbCrashTypes = [
|
|
3320
3660
|
"SQLiteBigInt",
|
|
@@ -3443,7 +3783,7 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
|
|
|
3443
3783
|
thisRecord.offset = offset;
|
|
3444
3784
|
thisRecord.limit = limit;
|
|
3445
3785
|
thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
|
|
3446
|
-
args[relName] = thisRecord;
|
|
3786
|
+
args[relName] = omitUndefinedKeys(thisRecord);
|
|
3447
3787
|
}
|
|
3448
3788
|
return args;
|
|
3449
3789
|
};
|
|
@@ -3520,12 +3860,12 @@ const runRelationalSelect = async (opts) => {
|
|
|
3520
3860
|
}) : void 0
|
|
3521
3861
|
};
|
|
3522
3862
|
if (single) {
|
|
3523
|
-
const result = await queryBase.findFirst(params);
|
|
3863
|
+
const result = await queryBase.findFirst(omitUndefinedKeys(params));
|
|
3524
3864
|
return result ? remapToGraphQLSingleOutput(result, tableName, table, relationMap) : void 0;
|
|
3525
3865
|
}
|
|
3526
3866
|
if (cursorEntries && cursorSelected) for (const [column] of cursorEntries) params.columns[column] = true;
|
|
3527
3867
|
params.limit = distinctKeys ? void 0 : opts.limit;
|
|
3528
|
-
const result = await queryBase.findMany(params);
|
|
3868
|
+
const result = await queryBase.findMany(omitUndefinedKeys(params));
|
|
3529
3869
|
if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
|
|
3530
3870
|
return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
|
|
3531
3871
|
};
|
|
@@ -4342,16 +4682,17 @@ const identifyRows = (field, args) => {
|
|
|
4342
4682
|
* Window functions require PostgreSQL, MySQL >= 8.0, or SQLite >= 3.25.
|
|
4343
4683
|
* Returns raw rows (NOT remapped); the caller groups + remaps them.
|
|
4344
4684
|
*/
|
|
4345
|
-
const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereCondition, orderByArg, limit, offset, pkNames, columns, orderCtx, whereArgs) => {
|
|
4685
|
+
const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereCondition, orderByArg, limit, offset, pkNames, columns, orderCtx, whereArgs, junction) => {
|
|
4346
4686
|
const cols = columns ?? (0, drizzle_orm.getColumns)(targetTable);
|
|
4347
4687
|
const orderExprs = [...orderByArg ? extractOrderBy(targetTable, orderByArg, orderCtx, whereArgs) : [], ...primaryKeyOrderExprs(targetTable, pkNames)];
|
|
4348
4688
|
const orderClause = orderExprs.length ? drizzle_orm.sql` order by ${drizzle_orm.sql.join(orderExprs, drizzle_orm.sql`, `)}` : drizzle_orm.sql``;
|
|
4349
4689
|
const RN = "__drizzle_graphql_rn";
|
|
4350
|
-
const
|
|
4351
|
-
const
|
|
4690
|
+
const partitionCol = junction ? junction.sourceCol : foreignCol;
|
|
4691
|
+
const rowNumber = drizzle_orm.sql`row_number() over (partition by ${partitionCol}${orderClause})`.as(RN);
|
|
4692
|
+
const sub = withJunction(db.select({
|
|
4352
4693
|
...cols,
|
|
4353
4694
|
[RN]: rowNumber
|
|
4354
|
-
}).from(targetTable).where(whereCondition).as("__paginated");
|
|
4695
|
+
}).from(targetTable), junction, foreignCol).where(whereCondition).as("__paginated");
|
|
4355
4696
|
const lower = offset ?? 0;
|
|
4356
4697
|
const windowConds = [(0, drizzle_orm.gt)(sub[RN], lower)];
|
|
4357
4698
|
if (limit != null) windowConds.push((0, drizzle_orm.lte)(sub[RN], lower + limit));
|
|
@@ -4360,6 +4701,14 @@ const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereC
|
|
|
4360
4701
|
return rows;
|
|
4361
4702
|
};
|
|
4362
4703
|
/**
|
|
4704
|
+
* Alias the parent key is selected under when a relation goes through a junction table. The
|
|
4705
|
+
* key lives on the junction, not the target, so it has to be selected explicitly to group the
|
|
4706
|
+
* rows by parent — and a target row linked to several parents comes back once per parent.
|
|
4707
|
+
*/
|
|
4708
|
+
const PARENT_KEY = "__drizzle_graphql_parent";
|
|
4709
|
+
/** `from(target)`, joined to the junction table when the relation has one. */
|
|
4710
|
+
const withJunction = (query, junction, foreignCol) => junction ? query.innerJoin(junction.table, (0, drizzle_orm.eq)(junction.targetCol, foreignCol)) : query;
|
|
4711
|
+
/**
|
|
4363
4712
|
* Creates a RelationResolverFactory that generates field-level resolvers for each relation.
|
|
4364
4713
|
* Each resolver:
|
|
4365
4714
|
* 1. Returns pre-fetched data if the parent resolver already included it (eager path, zero cost).
|
|
@@ -4381,6 +4730,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4381
4730
|
const joinCols = extractRelationJoinColumns(relEntry, parentTable, targetTable);
|
|
4382
4731
|
if (!joinCols) return;
|
|
4383
4732
|
const { localColPropName, foreignCol, foreignColPropName } = joinCols;
|
|
4733
|
+
const junction = extractJunctionColumns(relEntry);
|
|
4734
|
+
const groupCol = junction ? junction.sourceCol : foreignCol;
|
|
4735
|
+
const groupKey = junction ? PARENT_KEY : foreignColPropName;
|
|
4736
|
+
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, (0, drizzle_orm.getColumns)(parentTable)[localColPropName], groupCol);
|
|
4384
4737
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, isOne && relEntry.relation?.optional === false);
|
|
4385
4738
|
const limitPolicy = isOne ? void 0 : limits?.(targetTableName);
|
|
4386
4739
|
const targetPkNames = relEntry.targetPkNames ?? [];
|
|
@@ -4458,11 +4811,12 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4458
4811
|
loaderKey: `${tableName}::${relationName}::${argsKey}`
|
|
4459
4812
|
};
|
|
4460
4813
|
});
|
|
4814
|
+
if (junction === null) throw drizzleError(`Relation '${tableName}.${relationName}' goes through a junction with a composite key, which can only be loaded together with its parent. Leave it to eager loading (eagerLoadRelations).`, { code: "DRIZZLE_UNSUPPORTED_RELATION" });
|
|
4461
4815
|
return getOrCreateLoader(context, loaderKey, async (parentIds) => {
|
|
4462
4816
|
const executor = resolveExecutor(db, context);
|
|
4463
4817
|
const uniqueIds = [...new Set(parentIds)];
|
|
4464
4818
|
const scope = resolveScope(policies, context, filterCtx);
|
|
4465
|
-
let whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(
|
|
4819
|
+
let whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
|
|
4466
4820
|
if (distinct) {
|
|
4467
4821
|
const keys = await selectDistinctKeys({
|
|
4468
4822
|
db: executor,
|
|
@@ -4472,27 +4826,41 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4472
4826
|
pkNames: targetPkNames,
|
|
4473
4827
|
where: whereCondition,
|
|
4474
4828
|
orderBy: orderByArg,
|
|
4475
|
-
partitionBy: [
|
|
4829
|
+
partitionBy: [groupCol],
|
|
4830
|
+
junction: junction ? {
|
|
4831
|
+
table: junction.table,
|
|
4832
|
+
on: (0, drizzle_orm.eq)(junction.targetCol, foreignCol),
|
|
4833
|
+
parentKey: groupCol
|
|
4834
|
+
} : void 0
|
|
4476
4835
|
});
|
|
4477
4836
|
if (!keys.length) return parentIds.map(() => isOne ? null : []);
|
|
4478
|
-
whereCondition = (0, drizzle_orm.and)(whereCondition, primaryKeyRestriction(targetTable, targetPkNames, keys));
|
|
4837
|
+
whereCondition = (0, drizzle_orm.and)(whereCondition, junction ? (0, drizzle_orm.or)(...keys.map((key) => (0, drizzle_orm.and)((0, drizzle_orm.eq)(groupCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
|
|
4479
4838
|
}
|
|
4839
|
+
const selection = junction ? {
|
|
4840
|
+
...columns ?? (0, drizzle_orm.getColumns)(targetTable),
|
|
4841
|
+
[PARENT_KEY]: groupCol
|
|
4842
|
+
} : columns;
|
|
4480
4843
|
let rows;
|
|
4481
|
-
if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames,
|
|
4844
|
+
if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName), whereArg, junction);
|
|
4482
4845
|
else {
|
|
4483
|
-
let q = (
|
|
4846
|
+
let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
|
|
4484
4847
|
const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName), whereArg) : [];
|
|
4485
4848
|
if (orderExprs.length) q = q.orderBy(...orderExprs);
|
|
4486
4849
|
rows = await q;
|
|
4487
4850
|
}
|
|
4488
4851
|
if (cursorEntries) attachRowCursors(rows, cursorEntries);
|
|
4852
|
+
const keyOf = (row) => {
|
|
4853
|
+
const key = String(row[groupKey]);
|
|
4854
|
+
if (junction) delete row[PARENT_KEY];
|
|
4855
|
+
return key;
|
|
4856
|
+
};
|
|
4489
4857
|
if (isOne) {
|
|
4490
|
-
const byKey = new Map(rows.map((row) => [
|
|
4858
|
+
const byKey = new Map(rows.map((row) => [keyOf(row), row]));
|
|
4491
4859
|
remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
|
|
4492
4860
|
return parentIds.map((id) => byKey.get(String(id)) ?? null);
|
|
4493
4861
|
}
|
|
4494
4862
|
const grouped = new Map(uniqueIds.map((id) => [String(id), []]));
|
|
4495
|
-
for (const row of rows) grouped.get(
|
|
4863
|
+
for (const row of rows) grouped.get(keyOf(row))?.push(row);
|
|
4496
4864
|
remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
|
|
4497
4865
|
return parentIds.map((id) => grouped.get(String(id)) ?? []);
|
|
4498
4866
|
}).load(localValue);
|
|
@@ -4827,7 +5195,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
|
|
|
4827
5195
|
});
|
|
4828
5196
|
const shell = new graphql.GraphQLObjectType({
|
|
4829
5197
|
name: typeName,
|
|
4830
|
-
description: cacheCtx.docs.describeTable?.(tableName),
|
|
5198
|
+
description: cacheCtx.docs.describeTable?.(tableName, table),
|
|
4831
5199
|
fields: () => ({
|
|
4832
5200
|
...tableFields,
|
|
4833
5201
|
...container.fields
|
|
@@ -4861,7 +5229,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
|
|
|
4861
5229
|
defaultName: resolveTypeName(targetTableName, typeNameMapper),
|
|
4862
5230
|
table: targetTableName
|
|
4863
5231
|
}),
|
|
4864
|
-
description: cacheCtx.docs.describeTable?.(targetTableName),
|
|
5232
|
+
description: cacheCtx.docs.describeTable?.(targetTableName, targetTable),
|
|
4865
5233
|
fields: () => ({
|
|
4866
5234
|
...targetTableFields,
|
|
4867
5235
|
...capturedTargetContainer.fields
|
|
@@ -5012,7 +5380,7 @@ const generateTableTypes = (tableName, tables, relationMap, withReturning, relat
|
|
|
5012
5380
|
defaultName: resolveTypeName(tableName, typeNameMapper),
|
|
5013
5381
|
table: tableName
|
|
5014
5382
|
}),
|
|
5015
|
-
description: cacheCtx.docs.describeTable?.(tableName),
|
|
5383
|
+
description: cacheCtx.docs.describeTable?.(tableName, tables[tableName]),
|
|
5016
5384
|
fields: {
|
|
5017
5385
|
...tableFields,
|
|
5018
5386
|
...relationFields
|
|
@@ -5563,14 +5931,26 @@ const GROUP_KEY = "__dgql_group_key";
|
|
|
5563
5931
|
* for every other aggregate.
|
|
5564
5932
|
*/
|
|
5565
5933
|
const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, filterCtx, policies) => {
|
|
5934
|
+
const warned = /* @__PURE__ */ new Set();
|
|
5566
5935
|
return ({ tableName, relationName, relEntry }) => {
|
|
5567
5936
|
const parentTable = tables[tableName];
|
|
5568
5937
|
const targetTableName = relEntry.targetTableName;
|
|
5569
5938
|
const targetTable = tables[targetTableName];
|
|
5570
5939
|
if (!parentTable || !targetTable) return;
|
|
5571
5940
|
const joinCols = extractRelationJoinColumns(relEntry, parentTable, targetTable);
|
|
5572
|
-
if (!joinCols)
|
|
5941
|
+
if (!joinCols) {
|
|
5942
|
+
const key = `${tableName}.${relationName}`;
|
|
5943
|
+
if (!warned.has(key)) {
|
|
5944
|
+
warned.add(key);
|
|
5945
|
+
warnUnresolvedJoin(relEntry, tableName, relationName, `its '${relationName}Aggregate' field`);
|
|
5946
|
+
}
|
|
5947
|
+
return;
|
|
5948
|
+
}
|
|
5573
5949
|
const { localColPropName, foreignCol } = joinCols;
|
|
5950
|
+
const junction = extractJunctionColumns(relEntry);
|
|
5951
|
+
if (junction === null) return;
|
|
5952
|
+
const groupCol = junction ? junction.sourceCol : foreignCol;
|
|
5953
|
+
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, (0, drizzle_orm.getColumns)(parentTable)[localColPropName], groupCol);
|
|
5574
5954
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
|
|
5575
5955
|
const targetTypeName = resolveTypeName(targetTableName, typeNameMapper);
|
|
5576
5956
|
const type = generateAggregateTypes(targetTable, targetTableName, targetTypeName, cacheCtx);
|
|
@@ -5601,11 +5981,12 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
5601
5981
|
const loader = getOrCreateLoader(context, loaderKey, async (parentIds) => {
|
|
5602
5982
|
const executor = resolveExecutor(db, context);
|
|
5603
5983
|
const uniqueIds = [...new Set(parentIds)];
|
|
5604
|
-
const whereCondition = withScope(resolveScope(policies, context, filterCtx), targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(
|
|
5605
|
-
const
|
|
5606
|
-
[GROUP_KEY]:
|
|
5984
|
+
const whereCondition = withScope(resolveScope(policies, context, filterCtx), targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName)) : void 0), deleted ?? defaultDeleted);
|
|
5985
|
+
const selected = executor.select({
|
|
5986
|
+
[GROUP_KEY]: groupCol,
|
|
5607
5987
|
...request.selection
|
|
5608
|
-
}).from(targetTable)
|
|
5988
|
+
}).from(targetTable);
|
|
5989
|
+
const rows = await (junction ? selected.innerJoin(junction.table, (0, drizzle_orm.eq)(junction.targetCol, foreignCol)) : selected).where(whereCondition).groupBy(groupCol);
|
|
5609
5990
|
const byKey = new Map(rows.map((row) => [row[GROUP_KEY], row]));
|
|
5610
5991
|
return parentIds.map((id) => byKey.get(id) ?? {});
|
|
5611
5992
|
});
|
|
@@ -5873,12 +6254,6 @@ const propNameOf = (table, column) => {
|
|
|
5873
6254
|
for (const [propName, candidate] of Object.entries((0, drizzle_orm.getColumns)(table))) if (candidate === column) return propName;
|
|
5874
6255
|
};
|
|
5875
6256
|
/**
|
|
5876
|
-
* The junction column one entry of a relation's `through` config points at. Drizzle wraps it
|
|
5877
|
-
* in a descriptor while the relation is being defined; the raw column is accepted too, so a
|
|
5878
|
-
* change of shape there degrades to skipping the relation rather than mis-planning it.
|
|
5879
|
-
*/
|
|
5880
|
-
const throughColumn = (entry) => entry && (0, drizzle_orm.is)(entry, drizzle_orm.Column) ? entry : entry?._?.column;
|
|
5881
|
-
/**
|
|
5882
6257
|
* A `.through()` relation's plan: the junction table plus the two columns it joins on.
|
|
5883
6258
|
*
|
|
5884
6259
|
* Nothing about the parent or the target row changes when a many-to-many link is made or
|
|
@@ -5889,21 +6264,27 @@ const throughColumn = (entry) => entry && (0, drizzle_orm.is)(entry, drizzle_orm
|
|
|
5889
6264
|
* row" guarantee lives in the junction's constraints rather than in the relation; a relation
|
|
5890
6265
|
* joining on more than one column per side; and a junction carrying a further NOT NULL column
|
|
5891
6266
|
* with no default, which no `connect` operand could supply a value for.
|
|
6267
|
+
*
|
|
6268
|
+
* `onUnresolved` is called when the relation's columns are of a supported shape but cannot be
|
|
6269
|
+
* matched to the three tables — a drizzle-orm mismatch rather than an unsupported relation.
|
|
5892
6270
|
*/
|
|
5893
|
-
const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable, tableNameOf) => {
|
|
6271
|
+
const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable, tableNameOf, onUnresolved) => {
|
|
5894
6272
|
if ((0, drizzle_orm.is)(relation, drizzle_orm.One)) return;
|
|
5895
6273
|
const junction = relation.throughTable;
|
|
5896
6274
|
const sourceEntries = relation.through?.source;
|
|
5897
6275
|
const targetEntries = relation.through?.target;
|
|
5898
6276
|
if (!junction || sourceEntries?.length !== 1 || targetEntries?.length !== 1) return;
|
|
5899
6277
|
if (relation.sourceColumns?.length !== 1 || relation.targetColumns?.length !== 1) return;
|
|
5900
|
-
const localCol = relation.sourceColumns[0];
|
|
5901
|
-
const foreignCol = relation.targetColumns[0];
|
|
6278
|
+
const localCol = unwrapRelationColumn(relation.sourceColumns[0]);
|
|
6279
|
+
const foreignCol = unwrapRelationColumn(relation.targetColumns[0]);
|
|
5902
6280
|
const localColPropName = propNameOf(table, localCol);
|
|
5903
6281
|
const foreignColPropName = propNameOf(targetTable, foreignCol);
|
|
5904
|
-
const sourceColPropName = propNameOf(junction,
|
|
5905
|
-
const targetColPropName = propNameOf(junction,
|
|
5906
|
-
if (!localColPropName || !foreignColPropName || !sourceColPropName || !targetColPropName)
|
|
6282
|
+
const sourceColPropName = propNameOf(junction, unwrapRelationColumn(sourceEntries[0]));
|
|
6283
|
+
const targetColPropName = propNameOf(junction, unwrapRelationColumn(targetEntries[0]));
|
|
6284
|
+
if (!localCol || !foreignCol || !localColPropName || !foreignColPropName || !sourceColPropName || !targetColPropName) {
|
|
6285
|
+
onUnresolved();
|
|
6286
|
+
return;
|
|
6287
|
+
}
|
|
5907
6288
|
const junctionColumns = (0, drizzle_orm.getColumns)(junction);
|
|
5908
6289
|
const sourceCol = junctionColumns[sourceColPropName];
|
|
5909
6290
|
const targetCol = junctionColumns[targetColPropName];
|
|
@@ -5973,13 +6354,17 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
5973
6354
|
const targetTableName = relEntry.targetTableName;
|
|
5974
6355
|
const targetTable = tables[targetTableName];
|
|
5975
6356
|
if (!targetTable) continue;
|
|
6357
|
+
const omitted = `the '${relationName}' field of its nested-write create and update inputs`;
|
|
5976
6358
|
if (relation.through) {
|
|
5977
|
-
const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable, tableNameOf);
|
|
6359
|
+
const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable, tableNameOf, () => warnUnresolvedJoin(relEntry, tableName, relationName, omitted));
|
|
5978
6360
|
if (throughPlan) tablePlans[relationName] = throughPlan;
|
|
5979
6361
|
continue;
|
|
5980
6362
|
}
|
|
5981
6363
|
const joinColumns = extractJoinColumns(relEntry, table, targetTable);
|
|
5982
|
-
if (!joinColumns)
|
|
6364
|
+
if (!joinColumns) {
|
|
6365
|
+
warnUnresolvedJoin(relEntry, tableName, relationName, omitted);
|
|
6366
|
+
continue;
|
|
6367
|
+
}
|
|
5983
6368
|
const { localColPropName, foreignCol, foreignColPropName } = joinColumns;
|
|
5984
6369
|
const localCol = (0, drizzle_orm.getColumns)(table)[localColPropName];
|
|
5985
6370
|
if (!localCol) continue;
|
|
@@ -8385,7 +8770,7 @@ const buildSchema = (db, config) => {
|
|
|
8385
8770
|
};
|
|
8386
8771
|
//#endregion
|
|
8387
8772
|
exports.GraphQLBigIntString = GraphQLBigIntString;
|
|
8388
|
-
exports.GraphQLDate =
|
|
8773
|
+
exports.GraphQLDate = GraphQLDate;
|
|
8389
8774
|
exports.GraphQLDateTime = graphql_scalars.GraphQLDateTime;
|
|
8390
8775
|
exports.GraphQLDecimalString = GraphQLDecimalString;
|
|
8391
8776
|
exports.GraphQLJSON = graphql_scalars.GraphQLJSON;
|