@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.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Column, One, SQL, Table, aliasedTable, and, arrayContains, arrayOverlaps, asc, avg, count, countDistinct, desc, eq, extractExtendedColumnType, getColumns, getTableAsAliasSQL, gt, gte, ilike, inArray, is, isNotNull, isNull, like, lt, lte, max, min, ne, not, notIlike, notInArray, notLike, or, relationsFilterToSQL, sql, sum } from "drizzle-orm";
|
|
2
|
-
import { MySqlAsyncDatabase, MySqlInt, MySqlSerial, MySqlTable, getTableConfig } from "drizzle-orm/mysql-core";
|
|
2
|
+
import { MySqlAsyncDatabase, MySqlDate, MySqlDateString, MySqlDateTime, MySqlDateTimeString, MySqlInt, MySqlSerial, MySqlTable, MySqlTimestamp, MySqlTimestampString, getTableConfig } from "drizzle-orm/mysql-core";
|
|
3
3
|
import { PgAsyncDatabase, PgDate, PgDateString, PgInteger, PgSerial, PgTable, PgTimestamp, PgTimestampString, PgUUID, getTableConfig as getTableConfig$1 } from "drizzle-orm/pg-core";
|
|
4
|
-
import { SQLiteAsyncDatabase, SQLiteInteger, SQLiteTable, getTableConfig as getTableConfig$2 } from "drizzle-orm/sqlite-core";
|
|
4
|
+
import { SQLiteAsyncDatabase, SQLiteInteger, SQLiteTable, SQLiteTimestamp, getTableConfig as getTableConfig$2 } from "drizzle-orm/sqlite-core";
|
|
5
5
|
import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, Kind, getArgumentValues } from "graphql";
|
|
6
|
-
import {
|
|
6
|
+
import { GraphQLDateTime, GraphQLJSON, GraphQLUUID } from "graphql-scalars";
|
|
7
7
|
import pluralize from "pluralize";
|
|
8
8
|
//#region src/util/builders/common/errors.ts
|
|
9
9
|
/**
|
|
@@ -160,6 +160,76 @@ const GraphQLDecimalString = new GraphQLScalarType({
|
|
|
160
160
|
return parseNumeric(ast.value);
|
|
161
161
|
}
|
|
162
162
|
});
|
|
163
|
+
const fullDatePattern = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
164
|
+
const daysInMonth = (year, month) => {
|
|
165
|
+
if (month === 2) return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0 ? 29 : 28;
|
|
166
|
+
return [
|
|
167
|
+
4,
|
|
168
|
+
6,
|
|
169
|
+
9,
|
|
170
|
+
11
|
|
171
|
+
].includes(month) ? 30 : 31;
|
|
172
|
+
};
|
|
173
|
+
/** Whether `value` is a `YYYY-MM-DD` string naming a day that exists on the calendar. */
|
|
174
|
+
const isCalendarDate = (value) => {
|
|
175
|
+
const match = fullDatePattern.exec(value);
|
|
176
|
+
if (!match) return false;
|
|
177
|
+
const year = Number(match[1]);
|
|
178
|
+
const month = Number(match[2]);
|
|
179
|
+
const day = Number(match[3]);
|
|
180
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(year, month);
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* A JS `Date` as the calendar day it stands for. Read in UTC, because that is how drivers
|
|
184
|
+
* build one from a `date` column: `new Date('2026-09-15')` is UTC midnight, so the local
|
|
185
|
+
* getters would name the previous day anywhere west of Greenwich.
|
|
186
|
+
*/
|
|
187
|
+
const dateToCalendarDate = (value) => {
|
|
188
|
+
if (Number.isNaN(value.getTime())) return;
|
|
189
|
+
const year = value.getUTCFullYear();
|
|
190
|
+
if (year < 0 || year > 9999) return;
|
|
191
|
+
const month = String(value.getUTCMonth() + 1).padStart(2, "0");
|
|
192
|
+
const day = String(value.getUTCDate()).padStart(2, "0");
|
|
193
|
+
return `${String(year).padStart(4, "0")}-${month}-${day}`;
|
|
194
|
+
};
|
|
195
|
+
const calendarDate = (code, acceptDate) => (value) => {
|
|
196
|
+
if (typeof value === "string") {
|
|
197
|
+
if (!isCalendarDate(value)) throw drizzleError(`Date cannot represent "${value}": expected a calendar date as YYYY-MM-DD`, { code });
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
if (acceptDate && value instanceof Date) {
|
|
201
|
+
const formatted = dateToCalendarDate(value);
|
|
202
|
+
if (formatted === void 0) throw drizzleError("Date cannot represent an invalid or out-of-range Date", { code });
|
|
203
|
+
return formatted;
|
|
204
|
+
}
|
|
205
|
+
throw drizzleError(`Date cannot represent value: ${JSON.stringify(value)}: expected a YYYY-MM-DD string`, { code });
|
|
206
|
+
};
|
|
207
|
+
const parseCalendarDate = calendarDate(INVALID_INPUT, false);
|
|
208
|
+
/**
|
|
209
|
+
* A calendar day — `date` columns, whichever `mode` drizzle reads them in. Always transported
|
|
210
|
+
* as an RFC 3339 `full-date` string (`YYYY-MM-DD`), in both directions: it has no time and no
|
|
211
|
+
* offset, so there is nothing for a client's timezone to shift. `graphql-scalars`' own
|
|
212
|
+
* `GraphQLDate` is deliberately not used: its `parseValue` returns a JS `Date`, so the same
|
|
213
|
+
* scalar would be a string on the way out and an instant on the way in.
|
|
214
|
+
*
|
|
215
|
+
* Input accepts only a `YYYY-MM-DD` string naming a real day (`2026-02-30` and datetime
|
|
216
|
+
* strings are rejected) and hands it on unchanged. Output also accepts a JS `Date`, read as
|
|
217
|
+
* the UTC day it encodes.
|
|
218
|
+
*/
|
|
219
|
+
const GraphQLDate = new GraphQLScalarType({
|
|
220
|
+
name: "Date",
|
|
221
|
+
description: "A calendar date with no time or offset, transported as an RFC 3339 full-date string (YYYY-MM-DD).",
|
|
222
|
+
specifiedByURL: "https://datatracker.ietf.org/doc/html/rfc3339#section-5.6",
|
|
223
|
+
serialize: calendarDate(UNREPRESENTABLE, true),
|
|
224
|
+
parseValue: parseCalendarDate,
|
|
225
|
+
parseLiteral: (ast) => {
|
|
226
|
+
if (ast.kind !== Kind.STRING) throw drizzleError(`Date cannot represent a ${ast.kind}`, {
|
|
227
|
+
code: INVALID_INPUT,
|
|
228
|
+
nodes: ast
|
|
229
|
+
});
|
|
230
|
+
return parseCalendarDate(ast.value);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
163
233
|
//#endregion
|
|
164
234
|
//#region src/util/case-ops/index.ts
|
|
165
235
|
const uncapitalize = (input) => input?.length ? `${input[0].toLocaleLowerCase()}${input.length > 1 ? input.slice(1, input.length) : ""}` : input;
|
|
@@ -193,6 +263,17 @@ const singularizeMapper = (tableName) => ({
|
|
|
193
263
|
//#endregion
|
|
194
264
|
//#region src/util/type-converter/index.ts
|
|
195
265
|
const allowedNameChars = /^[a-zA-Z0-9_]+$/;
|
|
266
|
+
/** drizzle's `number` constraints whose values always fit a GraphQL `Int` (32-bit signed). */
|
|
267
|
+
const smallIntegerConstraints = /* @__PURE__ */ new Set([
|
|
268
|
+
"int8",
|
|
269
|
+
"uint8",
|
|
270
|
+
"int16",
|
|
271
|
+
"uint16",
|
|
272
|
+
"int24",
|
|
273
|
+
"uint24",
|
|
274
|
+
"int32",
|
|
275
|
+
"year"
|
|
276
|
+
]);
|
|
196
277
|
/** A GraphQL `Name` that is also allowed as an enum value — the spec reserves the three literals. */
|
|
197
278
|
const validEnumValueName = (name) => /^[_a-zA-Z][_a-zA-Z0-9]*$/.test(name) && name !== "true" && name !== "false" && name !== "null";
|
|
198
279
|
/**
|
|
@@ -333,6 +414,52 @@ const geoXyInputType = new GraphQLInputObjectType({
|
|
|
333
414
|
y: { type: GraphQLFloat }
|
|
334
415
|
}
|
|
335
416
|
});
|
|
417
|
+
/**
|
|
418
|
+
* Whether the column holds a calendar day with no time component — a SQL `date`, in either of
|
|
419
|
+
* the modes drizzle reads one in (`'date'` → a JS `Date`, `'string'` → `YYYY-MM-DD`). Every
|
|
420
|
+
* position a column's type appears in — output, create/update input, filter operands,
|
|
421
|
+
* aggregates and group keys — gives such a column the `Date` scalar, whichever mode it uses:
|
|
422
|
+
* the mode is how the server holds the value, not what the value is. SQLite has no date-only
|
|
423
|
+
* column type (its integer timestamps are instants — see {@link isDateTimeColumn}), so only
|
|
424
|
+
* Postgres and MySQL appear here.
|
|
425
|
+
*/
|
|
426
|
+
const isDateOnlyColumn = (column) => is(column, PgDate) || is(column, PgDateString) || is(column, MySqlDate) || is(column, MySqlDateString);
|
|
427
|
+
/**
|
|
428
|
+
* Whether the column is a MySQL `timestamp` / `datetime` read in drizzle's `'string'` mode. The
|
|
429
|
+
* driver hands such a column back as MySQL's own `YYYY-MM-DD HH:MM:SS[.fraction]` text, with no
|
|
430
|
+
* offset, and expects that form on write, so the data mappers translate between it and the ISO
|
|
431
|
+
* 8601 instant the `DateTime` scalar carries. The text is read as UTC, the way drizzle's own
|
|
432
|
+
* `'date'`-mode codecs for these columns read it.
|
|
433
|
+
*/
|
|
434
|
+
const isMySqlDateTimeStringColumn = (column) => is(column, MySqlTimestampString) || is(column, MySqlDateTimeString);
|
|
435
|
+
/**
|
|
436
|
+
* Whether the column is a Postgres `timestamp` read in drizzle's `'string'` mode. The driver hands
|
|
437
|
+
* such a column back as Postgres's own text — `YYYY-MM-DD HH:MM:SS[.fraction]`, followed by an
|
|
438
|
+
* offset (`+00`, `-06`, `+05:30`) for `timestamptz` — so the data mappers translate between it
|
|
439
|
+
* and the ISO 8601 instant the `DateTime` scalar carries. Text without an offset (`timestamp`
|
|
440
|
+
* without time zone) is read as UTC, the way drizzle's own `'date'`-mode codec reads it.
|
|
441
|
+
*/
|
|
442
|
+
const isPgTimestampStringColumn = (column) => is(column, PgTimestampString);
|
|
443
|
+
/**
|
|
444
|
+
* Whether the column is a SQLite `integer({ mode: 'timestamp' | 'timestamp_ms' })`. Both modes
|
|
445
|
+
* are one drizzle class, told apart by `mode`: the stored integer counts seconds or milliseconds
|
|
446
|
+
* since the Unix epoch. drizzle converts a JS `Date` to that integer on write (`'timestamp'`
|
|
447
|
+
* floors to the whole second) and back on read, but a value read through a path that skips the
|
|
448
|
+
* column's decoder arrives as the bare integer, which {@link sqliteTimestampToDate} reads.
|
|
449
|
+
*/
|
|
450
|
+
const isSQLiteTimestampColumn = (column) => is(column, SQLiteTimestamp);
|
|
451
|
+
/**
|
|
452
|
+
* A SQLite timestamp column's stored integer as the instant it names, using the column's own
|
|
453
|
+
* unit (seconds for `'timestamp'`, milliseconds for `'timestamp_ms'`).
|
|
454
|
+
*/
|
|
455
|
+
const sqliteTimestampToDate = (column, value) => new Date(column.mode === "timestamp" ? value * 1e3 : value);
|
|
456
|
+
/**
|
|
457
|
+
* Whether the column holds an instant — a Postgres `timestamp`, a MySQL `timestamp` /
|
|
458
|
+
* `datetime`, in either drizzle mode, or a SQLite integer timestamp in either unit. Every position
|
|
459
|
+
* such a column appears in gives it the `DateTime` scalar and the `DateTimeFilter`, whichever
|
|
460
|
+
* mode it uses, for the same reason as {@link isDateOnlyColumn}.
|
|
461
|
+
*/
|
|
462
|
+
const isDateTimeColumn = (column) => is(column, PgTimestamp) || isPgTimestampStringColumn(column) || is(column, MySqlTimestamp) || is(column, MySqlDateTime) || isMySqlDateTimeStringColumn(column) || isSQLiteTimestampColumn(column);
|
|
336
463
|
const columnToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
337
464
|
const dimensions = column.dimensions;
|
|
338
465
|
if (dimensions !== void 0 && dimensions > 0) {
|
|
@@ -359,7 +486,11 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
359
486
|
typeLabel: "Boolean"
|
|
360
487
|
};
|
|
361
488
|
case "object":
|
|
362
|
-
if (column
|
|
489
|
+
if (isDateOnlyColumn(column)) return {
|
|
490
|
+
type: GraphQLDate,
|
|
491
|
+
typeLabel: "Date"
|
|
492
|
+
};
|
|
493
|
+
if (isDateTimeColumn(column)) return {
|
|
363
494
|
type: GraphQLDateTime,
|
|
364
495
|
typeLabel: "DateTime"
|
|
365
496
|
};
|
|
@@ -379,7 +510,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
379
510
|
type: GraphQLDecimalString,
|
|
380
511
|
typeLabel: "Decimal"
|
|
381
512
|
};
|
|
382
|
-
if (column
|
|
513
|
+
if (isDateTimeColumn(column)) return {
|
|
383
514
|
type: GraphQLDateTime,
|
|
384
515
|
typeLabel: "DateTime"
|
|
385
516
|
};
|
|
@@ -387,10 +518,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
387
518
|
type: GraphQLUUID,
|
|
388
519
|
typeLabel: "UUID"
|
|
389
520
|
};
|
|
390
|
-
if (column
|
|
391
|
-
type: GraphQLString,
|
|
392
|
-
typeLabel: "Date"
|
|
393
|
-
} : {
|
|
521
|
+
if (isDateOnlyColumn(column)) return {
|
|
394
522
|
type: GraphQLDate,
|
|
395
523
|
typeLabel: "Date"
|
|
396
524
|
};
|
|
@@ -402,7 +530,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
402
530
|
type: GraphQLBigIntString,
|
|
403
531
|
typeLabel: "BigInt"
|
|
404
532
|
};
|
|
405
|
-
case "number": return is(column, PgInteger) || is(column, PgSerial) || is(column, MySqlInt) || is(column, MySqlSerial) || is(column, SQLiteInteger) ? {
|
|
533
|
+
case "number": return constraint !== void 0 && smallIntegerConstraints.has(constraint) || is(column, PgInteger) || is(column, PgSerial) || is(column, MySqlInt) || is(column, MySqlSerial) || is(column, SQLiteInteger) ? {
|
|
406
534
|
type: GraphQLInt,
|
|
407
535
|
typeLabel: "Integer"
|
|
408
536
|
} : {
|
|
@@ -578,7 +706,8 @@ const isJsonColumn = (column) => (column.dataType ?? "").includes("json") && col
|
|
|
578
706
|
* - "Decimal" → numeric/decimal columns (Decimal-scalar-typed operators, no string pattern operators)
|
|
579
707
|
* - the enum GraphQL type name → enum columns (still unique per enum; equality and
|
|
580
708
|
* membership only, like Boolean)
|
|
581
|
-
* - "
|
|
709
|
+
* - "Date" → date columns, either mode (ordering kept, no string pattern operators)
|
|
710
|
+
* - "DateTime" → timestamp columns (ordering kept, no string pattern operators)
|
|
582
711
|
* - "Int" → integer/serial columns (no string pattern operators)
|
|
583
712
|
* - "Float" → real/double columns (no string pattern operators)
|
|
584
713
|
* - "String" → all other text/varchar columns
|
|
@@ -617,8 +746,11 @@ const resolveGenericFilterDescriptor = (column, columnGraphQLType) => {
|
|
|
617
746
|
name: "Decimal",
|
|
618
747
|
kind: "scalar"
|
|
619
748
|
};
|
|
620
|
-
|
|
621
|
-
|
|
749
|
+
if (columnGraphQLType.type === GraphQLDate) return {
|
|
750
|
+
name: "Date",
|
|
751
|
+
kind: "scalar"
|
|
752
|
+
};
|
|
753
|
+
if (isDateTimeColumn(column)) return {
|
|
622
754
|
name: "DateTime",
|
|
623
755
|
kind: "scalar"
|
|
624
756
|
};
|
|
@@ -1013,6 +1145,58 @@ const generateColumnFilterValues = (column, tableName, columnName, cacheCtx) =>
|
|
|
1013
1145
|
};
|
|
1014
1146
|
//#endregion
|
|
1015
1147
|
//#region src/util/data-mappers/index.ts
|
|
1148
|
+
/**
|
|
1149
|
+
* MySQL's own text form for a `timestamp` / `datetime`: `YYYY-MM-DD HH:MM:SS`, an optional
|
|
1150
|
+
* fraction, and no offset. mysql2 returns it for a `'string'`-mode column, and it is what such a
|
|
1151
|
+
* column expects on write.
|
|
1152
|
+
*/
|
|
1153
|
+
const MYSQL_DATETIME_TEXT = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/;
|
|
1154
|
+
/**
|
|
1155
|
+
* MySQL datetime text, read as UTC, as the ISO 8601 instant the `DateTime` scalar carries.
|
|
1156
|
+
* UTC because that is how drizzle's own `'date'`-mode codecs read the same text, so a column's
|
|
1157
|
+
* two modes agree on the instant. Anything else (a zero date, a value already carrying an
|
|
1158
|
+
* offset) is returned unchanged for the scalar to accept or reject.
|
|
1159
|
+
*/
|
|
1160
|
+
const mySqlDateTimeTextToIso = (value) => {
|
|
1161
|
+
const match = MYSQL_DATETIME_TEXT.exec(value);
|
|
1162
|
+
return match ? `${match[1]}T${match[2]}Z` : value;
|
|
1163
|
+
};
|
|
1164
|
+
/**
|
|
1165
|
+
* A JS `Date` as offset-less `YYYY-MM-DD HH:MM:SS.mmm` text in UTC: what a MySQL `'string'`-mode
|
|
1166
|
+
* column and a Postgres `timestamp` (without time zone) `'string'`-mode column are written with.
|
|
1167
|
+
*/
|
|
1168
|
+
const dateToUtcWallText = (value) => value.toISOString().slice(0, -1).replace("T", " ");
|
|
1169
|
+
/**
|
|
1170
|
+
* Postgres's text form for a `timestamp` / `timestamptz` (the `ISO` `DateStyle`, the default):
|
|
1171
|
+
* `YYYY-MM-DD HH:MM:SS`, an optional fraction, and — for `timestamptz` only — the session zone's
|
|
1172
|
+
* offset as `±HH`, `±HH:MM` or, for historical local mean time, `±HH:MM:SS`.
|
|
1173
|
+
*/
|
|
1174
|
+
const PG_TIMESTAMP_TEXT = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(\.\d+)?(?:([+-])(\d{2})(?::?(\d{2}))?(?::?(\d{2}))?)?$/;
|
|
1175
|
+
/**
|
|
1176
|
+
* Postgres timestamp text as the RFC 3339 UTC instant (`…Z`) the `DateTime` scalar accepts.
|
|
1177
|
+
*
|
|
1178
|
+
* - With an offset (`timestamptz`), the instant is shifted to UTC. The offset can carry seconds,
|
|
1179
|
+
* which RFC 3339 cannot express, so it is applied here rather than rewritten.
|
|
1180
|
+
* - Without one (`timestamp`), the wall time is read as UTC: that is what drizzle's `'date'`-mode
|
|
1181
|
+
* codec does (`new Date(text + '+0000')`) and what the input side writes, so a column's two
|
|
1182
|
+
* modes agree on the instant.
|
|
1183
|
+
* - The fraction is kept digit for digit. The scalar itself carries milliseconds, so microseconds
|
|
1184
|
+
* are truncated on the wire, never rounded into the next second.
|
|
1185
|
+
*
|
|
1186
|
+
* Anything else (`infinity`, a `BC` date, a five-digit year) is returned unchanged for the scalar
|
|
1187
|
+
* to reject.
|
|
1188
|
+
*/
|
|
1189
|
+
const pgTimestampTextToIso = (value) => {
|
|
1190
|
+
const match = PG_TIMESTAMP_TEXT.exec(value);
|
|
1191
|
+
if (!match) return value;
|
|
1192
|
+
const [, date, time, fraction = "", sign, hours, minutes = "0", seconds = "0"] = match;
|
|
1193
|
+
if (!sign) return `${date}T${time}${fraction}Z`;
|
|
1194
|
+
const wallMs = Date.parse(`${date}T${time}Z`);
|
|
1195
|
+
if (Number.isNaN(wallMs)) return value;
|
|
1196
|
+
const offsetSeconds = (sign === "-" ? -1 : 1) * (Number(hours) * 3600 + Number(minutes) * 60 + Number(seconds));
|
|
1197
|
+
const utc = (/* @__PURE__ */ new Date(wallMs - offsetSeconds * 1e3)).toISOString();
|
|
1198
|
+
return utc.length === 24 ? `${utc.slice(0, 19)}${fraction}Z` : value;
|
|
1199
|
+
};
|
|
1016
1200
|
const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
|
|
1017
1201
|
if (Array.isArray(value)) {
|
|
1018
1202
|
const relations = relationMap?.[tableName];
|
|
@@ -1031,7 +1215,14 @@ const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
|
|
|
1031
1215
|
if (!column) return value;
|
|
1032
1216
|
if (getColumnScalarOverride(column, false)) return value;
|
|
1033
1217
|
if (isJsonColumn(column)) return value;
|
|
1218
|
+
if (value instanceof Date && isDateOnlyColumn(column)) return dateToCalendarDate(value) ?? value;
|
|
1219
|
+
if (typeof value === "number" && isSQLiteTimestampColumn(column)) {
|
|
1220
|
+
const instant = sqliteTimestampToDate(column, value);
|
|
1221
|
+
return Number.isNaN(instant.getTime()) ? value : instant.toISOString();
|
|
1222
|
+
}
|
|
1034
1223
|
if (value instanceof Date) return value.toISOString();
|
|
1224
|
+
if (typeof value === "string" && isMySqlDateTimeStringColumn(column)) return mySqlDateTimeTextToIso(value);
|
|
1225
|
+
if (typeof value === "string" && isPgTimestampStringColumn(column)) return pgTimestampTextToIso(value);
|
|
1035
1226
|
if (value instanceof Buffer) return Array.from(value);
|
|
1036
1227
|
if (typeof value === "bigint") return value.toString();
|
|
1037
1228
|
if (Array.isArray(value)) {
|
|
@@ -1077,16 +1268,20 @@ const remapFromGraphQLCore = (value, column, columnName) => {
|
|
|
1077
1268
|
}
|
|
1078
1269
|
const dataType = column.dataType ?? "";
|
|
1079
1270
|
const columnType = column.columnType ?? "";
|
|
1080
|
-
if (
|
|
1081
|
-
const
|
|
1271
|
+
if (isDateTimeColumn(column)) {
|
|
1272
|
+
const isMySql = columnType.startsWith("MySql");
|
|
1273
|
+
const isPgString = isPgTimestampStringColumn(column);
|
|
1274
|
+
const toIso = isMySql ? mySqlDateTimeTextToIso : isPgString ? pgTimestampTextToIso : void 0;
|
|
1275
|
+
const formatted = new Date(toIso && typeof value === "string" ? toIso(value) : value);
|
|
1082
1276
|
if (Number.isNaN(formatted.getTime())) throw drizzleError(`Field '${columnName}' is not a valid date!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1083
|
-
return formatted;
|
|
1277
|
+
if (isPgString) return column.withTimezone ? formatted.toISOString() : dateToUtcWallText(formatted);
|
|
1278
|
+
return isMySqlDateTimeStringColumn(column) ? dateToUtcWallText(formatted) : formatted;
|
|
1084
1279
|
}
|
|
1085
|
-
if ((
|
|
1086
|
-
|
|
1087
|
-
const
|
|
1088
|
-
if (
|
|
1089
|
-
return
|
|
1280
|
+
if (isDateOnlyColumn(column)) {
|
|
1281
|
+
if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
|
|
1282
|
+
const day = value instanceof Date ? dateToCalendarDate(value) : value;
|
|
1283
|
+
if (typeof day !== "string" || !isCalendarDate(day)) throw drizzleError(`Field '${columnName}' is not a valid date! Expected a calendar date as YYYY-MM-DD.`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1284
|
+
return day;
|
|
1090
1285
|
}
|
|
1091
1286
|
if (dataType.includes("bigint")) try {
|
|
1092
1287
|
return BigInt(value);
|
|
@@ -1442,7 +1637,8 @@ const extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1442
1637
|
}
|
|
1443
1638
|
if (NOT) {
|
|
1444
1639
|
const extracted = extractFiltersColumn(column, columnName, NOT);
|
|
1445
|
-
|
|
1640
|
+
const negated = extracted ? not(extracted) : void 0;
|
|
1641
|
+
if (negated) variants.push(negated);
|
|
1446
1642
|
}
|
|
1447
1643
|
if (OR?.length) {
|
|
1448
1644
|
const orVariants = [];
|
|
@@ -1455,6 +1651,192 @@ const extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1455
1651
|
return variants.length ? variants.length > 1 ? and(...variants) : variants[0] : void 0;
|
|
1456
1652
|
};
|
|
1457
1653
|
//#endregion
|
|
1654
|
+
//#region src/util/builders/common/relations.ts
|
|
1655
|
+
/**
|
|
1656
|
+
* Flatten drizzle-orm v1 TablesRelationalConfig into the canonical
|
|
1657
|
+
* Record<tableName, Record<relName, TableNamedRelations>> shape used
|
|
1658
|
+
* throughout common.ts. Both pg.ts and sqlite.ts call this before
|
|
1659
|
+
* passing the relation map to any shared function.
|
|
1660
|
+
*/
|
|
1661
|
+
const buildNamedRelations = (relations, tableEntries) => {
|
|
1662
|
+
const namedRelations = {};
|
|
1663
|
+
for (const [relTableName, relConfig] of Object.entries(relations)) {
|
|
1664
|
+
if (!relConfig?.relations) continue;
|
|
1665
|
+
const namedConfig = {};
|
|
1666
|
+
for (const [innerRelName, innerRelValue] of Object.entries(relConfig.relations)) {
|
|
1667
|
+
const targetTable = innerRelValue.targetTable ?? innerRelValue.referencedTable;
|
|
1668
|
+
const directTargetName = innerRelValue.targetTableName;
|
|
1669
|
+
let targetTableName;
|
|
1670
|
+
if (directTargetName) targetTableName = tableEntries.find(([key]) => key === directTargetName)?.[0];
|
|
1671
|
+
else if (targetTable) targetTableName = tableEntries.find(([, tableValue]) => tableValue === targetTable)?.[0];
|
|
1672
|
+
if (!targetTableName) continue;
|
|
1673
|
+
namedConfig[innerRelName] = {
|
|
1674
|
+
relation: innerRelValue,
|
|
1675
|
+
targetTableName
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
if (Object.keys(namedConfig).length > 0) namedRelations[relTableName] = namedConfig;
|
|
1679
|
+
}
|
|
1680
|
+
return namedRelations;
|
|
1681
|
+
};
|
|
1682
|
+
/**
|
|
1683
|
+
* Records each relation's target-table primary-key property names on the relation entry,
|
|
1684
|
+
* so the pagination paths (the window-function batch loader and the eager `with:` orderBy
|
|
1685
|
+
* default) can fall back to a deterministic PK order without re-deriving it per request.
|
|
1686
|
+
*
|
|
1687
|
+
* Composite primary keys are only visible through the dialect's getTableConfig, so the
|
|
1688
|
+
* dialect builder passes a `resolvePkNames` that threads the composite column names in.
|
|
1689
|
+
* Mutates the relation entries in place (they are shared with the pruned eager map and
|
|
1690
|
+
* the resolver factory, so attaching once covers every consumer).
|
|
1691
|
+
*/
|
|
1692
|
+
const attachTargetPrimaryKeys = (namedRelations, tables, resolvePkNames) => {
|
|
1693
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1694
|
+
for (const rels of Object.values(namedRelations)) for (const relEntry of Object.values(rels)) {
|
|
1695
|
+
const { targetTableName } = relEntry;
|
|
1696
|
+
let pk = cache.get(targetTableName);
|
|
1697
|
+
if (!pk) {
|
|
1698
|
+
const targetTable = tables[targetTableName];
|
|
1699
|
+
pk = targetTable ? resolvePkNames(targetTable) : [];
|
|
1700
|
+
cache.set(targetTableName, pk);
|
|
1701
|
+
}
|
|
1702
|
+
relEntry.targetPkNames = pk;
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
/**
|
|
1706
|
+
* The table column behind one entry of a relation's `sourceColumns`, `targetColumns` or
|
|
1707
|
+
* `through` lists.
|
|
1708
|
+
*
|
|
1709
|
+
* drizzle-orm rc.4 stores the table's own Column objects in `sourceColumns` / `targetColumns`
|
|
1710
|
+
* and a `RelationsBuilderColumn` descriptor (`{ _: { column, key, tableName } }`) in `through`.
|
|
1711
|
+
* The rc.5 snapshots store the descriptor in all of them. Both shapes are accepted here, told
|
|
1712
|
+
* apart structurally rather than by importing a class one of those versions does not export.
|
|
1713
|
+
* Anything else yields `undefined`, which callers treat as "join columns not readable".
|
|
1714
|
+
*/
|
|
1715
|
+
const unwrapRelationColumn = (entry) => {
|
|
1716
|
+
if (is(entry, Column)) return entry;
|
|
1717
|
+
const inner = entry?._?.column;
|
|
1718
|
+
return is(inner, Column) ? inner : void 0;
|
|
1719
|
+
};
|
|
1720
|
+
/**
|
|
1721
|
+
* A relation's `sourceColumns` or `targetColumns` as table columns, via
|
|
1722
|
+
* {@link unwrapRelationColumn}. `undefined` when the list is missing, empty, or has an entry
|
|
1723
|
+
* that is not a column in either known shape.
|
|
1724
|
+
*/
|
|
1725
|
+
const relationColumns = (relation, side) => {
|
|
1726
|
+
const entries = relation?.[side === "source" ? "sourceColumns" : "targetColumns"];
|
|
1727
|
+
if (!entries?.length) return;
|
|
1728
|
+
const columns = [];
|
|
1729
|
+
for (const entry of entries) {
|
|
1730
|
+
const column = unwrapRelationColumn(entry);
|
|
1731
|
+
if (!column) return;
|
|
1732
|
+
columns.push(column);
|
|
1733
|
+
}
|
|
1734
|
+
return columns;
|
|
1735
|
+
};
|
|
1736
|
+
/**
|
|
1737
|
+
* drizzle-orm's "no filter" sentinel. rc.5 snapshots export it as `EmptyFilter` and use it as
|
|
1738
|
+
* the default `relation.where`; rc.4 has no such export and leaves `where` undefined. It is a
|
|
1739
|
+
* registered symbol, so it can be compared against without importing anything.
|
|
1740
|
+
*/
|
|
1741
|
+
const EMPTY_FILTER = Symbol.for("drizzle:EmptyFilter");
|
|
1742
|
+
/**
|
|
1743
|
+
* The relation's own declared `where` filter, or `undefined` when it has none. rc.4 leaves
|
|
1744
|
+
* `where` undefined in that case; rc.5 snapshots set it to the `EmptyFilter` sentinel, which is
|
|
1745
|
+
* truthy and must not be read as a filter.
|
|
1746
|
+
*/
|
|
1747
|
+
const relationOwnWhere = (relation) => {
|
|
1748
|
+
const where = relation?.where;
|
|
1749
|
+
return where == null || where === EMPTY_FILTER ? void 0 : where;
|
|
1750
|
+
};
|
|
1751
|
+
/**
|
|
1752
|
+
* Whether the relation's `where` was inherited from its reverse relation, and so is written
|
|
1753
|
+
* against the parent table rather than the target. rc.4 calls this flag `isReversed`; rc.5
|
|
1754
|
+
* snapshots renamed it to `isFilterReversed`. Both are set in the same place with the same
|
|
1755
|
+
* meaning (true only for a relation declared without `from`/`to` and without its own `where`).
|
|
1756
|
+
*/
|
|
1757
|
+
const isRelationFilterReversed = (relation) => !!(relation?.isFilterReversed ?? relation?.isReversed);
|
|
1758
|
+
/**
|
|
1759
|
+
* Warns, at build time, that a field was left out because the relation's join columns could
|
|
1760
|
+
* not be matched to its tables. Omitting it silently is how a drizzle-orm change in the shape
|
|
1761
|
+
* of relation columns (issue #179) went unnoticed. A relation that declares no join columns
|
|
1762
|
+
* is not reported: there is nothing that failed to match.
|
|
1763
|
+
*/
|
|
1764
|
+
const warnUnresolvedJoin = (relEntry, tableName, relationName, omitted) => {
|
|
1765
|
+
const rel = relEntry.relation ?? relEntry;
|
|
1766
|
+
if (!rel?.sourceColumns?.length || !rel?.targetColumns?.length) return;
|
|
1767
|
+
console.warn(`Drizzle-GraphQL Warning: relation '${tableName}.${relationName}' declares join columns that could not be matched to columns of '${tableName}' and '${relEntry.targetTableName}', so ${omitted} is not generated. The installed drizzle-orm probably stores relation columns in a shape this version of drizzle-graphql does not recognize.`);
|
|
1768
|
+
};
|
|
1769
|
+
/**
|
|
1770
|
+
* Extracts the join column info from a drizzle-orm v1 Relation object.
|
|
1771
|
+
* Returns the JS property name of the local column on the parent table and the
|
|
1772
|
+
* Column object for the foreign column on the target table, or undefined if the
|
|
1773
|
+
* relation internals are not accessible.
|
|
1774
|
+
*/
|
|
1775
|
+
const extractRelationJoinColumns = (relEntry, parentTable, targetTable) => {
|
|
1776
|
+
const rel = relEntry.relation ?? relEntry;
|
|
1777
|
+
const sourceCol = relationColumns(rel, "source")?.[0];
|
|
1778
|
+
const targetCol = relationColumns(rel, "target")?.[0];
|
|
1779
|
+
if (!sourceCol || !targetCol) return;
|
|
1780
|
+
const parentCols = getColumns(parentTable);
|
|
1781
|
+
const localColPropName = Object.entries(parentCols).find(([, c]) => c === sourceCol)?.[0];
|
|
1782
|
+
const targetCols = getColumns(targetTable);
|
|
1783
|
+
const foreignColPropName = Object.entries(targetCols).find(([, c]) => c === targetCol)?.[0];
|
|
1784
|
+
if (!localColPropName || !foreignColPropName) return;
|
|
1785
|
+
return {
|
|
1786
|
+
localColPropName,
|
|
1787
|
+
foreignCol: targetCol,
|
|
1788
|
+
foreignColPropName
|
|
1789
|
+
};
|
|
1790
|
+
};
|
|
1791
|
+
/**
|
|
1792
|
+
* The junction table and its two columns for a `.through()` (many-to-many) relation:
|
|
1793
|
+
* `sourceCol` holds the parent's key, `targetCol` the target's. `undefined` for a relation
|
|
1794
|
+
* with no junction; `null` for one whose junction cannot be used here — more than one column
|
|
1795
|
+
* on either side, or columns that are not on the junction table.
|
|
1796
|
+
*/
|
|
1797
|
+
const extractJunctionColumns = (relEntry) => {
|
|
1798
|
+
const rel = relEntry.relation ?? relEntry;
|
|
1799
|
+
const junction = rel?.throughTable;
|
|
1800
|
+
if (!junction) return;
|
|
1801
|
+
const sourceEntries = rel.through?.source;
|
|
1802
|
+
const targetEntries = rel.through?.target;
|
|
1803
|
+
if (sourceEntries?.length !== 1 || targetEntries?.length !== 1) return null;
|
|
1804
|
+
const junctionCols = Object.values(getColumns(junction));
|
|
1805
|
+
const sourceCol = unwrapRelationColumn(sourceEntries[0]);
|
|
1806
|
+
const targetCol = unwrapRelationColumn(targetEntries[0]);
|
|
1807
|
+
if (!sourceCol || !targetCol || !junctionCols.includes(sourceCol) || !junctionCols.includes(targetCol)) return null;
|
|
1808
|
+
return {
|
|
1809
|
+
table: junction,
|
|
1810
|
+
sourceCol,
|
|
1811
|
+
targetCol
|
|
1812
|
+
};
|
|
1813
|
+
};
|
|
1814
|
+
/**
|
|
1815
|
+
* The relation's own declared `where` as a condition on a query that reads the target table,
|
|
1816
|
+
* or `undefined` when it has none. The eager path gets this from drizzle's `with:`; the batched
|
|
1817
|
+
* field resolver and `<relation>Aggregate` read the target table directly, so they have to add
|
|
1818
|
+
* it themselves or they return rows the relation hides.
|
|
1819
|
+
*
|
|
1820
|
+
* A reversed filter (inherited from the relation on the other side) is written against the
|
|
1821
|
+
* parent table, so it is applied as "the row's parent key belongs to a parent that passes it".
|
|
1822
|
+
* `parentKeyCol` is the parent's join column and `groupCol` the column in the query that holds
|
|
1823
|
+
* the parent key for each row — the target's foreign key, or the junction's source column.
|
|
1824
|
+
*/
|
|
1825
|
+
const relationOwnCondition = (relEntry, parentTable, targetTable, parentKeyCol, groupCol) => {
|
|
1826
|
+
const rel = relEntry.relation ?? relEntry;
|
|
1827
|
+
const ownWhere = relationOwnWhere(rel);
|
|
1828
|
+
if (!ownWhere) return;
|
|
1829
|
+
if (!isRelationFilterReversed(rel)) return relationsFilterToSQL(targetTable, ownWhere);
|
|
1830
|
+
const parentFilter = relationsFilterToSQL(parentTable, ownWhere);
|
|
1831
|
+
return sql`${groupCol} in (select ${parentKeyCol} from ${parentTable} where ${parentFilter})`;
|
|
1832
|
+
};
|
|
1833
|
+
/**
|
|
1834
|
+
* Whether a to-one relation can back a relation orderBy hop. `.through()` (junction)
|
|
1835
|
+
* relations are excluded — the ordering subquery only joins the direct target, never the
|
|
1836
|
+
* junction table. (Relation *filters* do support `.through()` — this guard is orderBy-only.)
|
|
1837
|
+
*/
|
|
1838
|
+
const isFilterableRelation = (relation) => !relation.through;
|
|
1839
|
+
//#endregion
|
|
1458
1840
|
//#region src/util/builders/common/exclusions.ts
|
|
1459
1841
|
/**
|
|
1460
1842
|
* Columns this build keeps out of the generated schema, keyed by the table object.
|
|
@@ -1630,8 +2012,8 @@ const relationFilterCtx = (base, tableKey) => {
|
|
|
1630
2012
|
* the parent is an aliased proxy (as it is inside a relational `with:` where callback).
|
|
1631
2013
|
*/
|
|
1632
2014
|
const buildRelationJoinCondition = (parentTable, relation, aliasedTarget, relationName) => {
|
|
1633
|
-
const sourceColumns = relation
|
|
1634
|
-
const targetColumns = relation
|
|
2015
|
+
const sourceColumns = relationColumns(relation, "source");
|
|
2016
|
+
const targetColumns = relationColumns(relation, "target");
|
|
1635
2017
|
if (!sourceColumns?.length || sourceColumns.length !== targetColumns?.length) throw drizzleError(`WHERE ${relationName}: Relation cannot be used as a filter`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1636
2018
|
const parentColumns = Object.values(getColumns(parentTable));
|
|
1637
2019
|
const targetColumnsByName = Object.values(getColumns(aliasedTarget));
|
|
@@ -1655,7 +2037,7 @@ const resolveJunctionColumn = (aliasedThrough, junctionRef, relationName) => {
|
|
|
1655
2037
|
const key = junctionRef?._?.key;
|
|
1656
2038
|
const byKey = key ? throughColumns[key] : void 0;
|
|
1657
2039
|
if (byKey) return byKey;
|
|
1658
|
-
const columnName = junctionRef?.
|
|
2040
|
+
const columnName = unwrapRelationColumn(junctionRef)?.name;
|
|
1659
2041
|
const byName = columnName ? Object.values(throughColumns).find((c) => c.name === columnName) : void 0;
|
|
1660
2042
|
if (!byName) throw drizzleError(`WHERE ${relationName}: Relation cannot be used as a filter`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1661
2043
|
return byName;
|
|
@@ -1669,8 +2051,8 @@ const resolveJunctionColumn = (aliasedThrough, junctionRef, relationName) => {
|
|
|
1669
2051
|
* `through` metadata.
|
|
1670
2052
|
*/
|
|
1671
2053
|
const buildThroughJoinConditions = (parentTable, relation, aliasedThrough, aliasedTarget, relationName) => {
|
|
1672
|
-
const sourceColumns = relation
|
|
1673
|
-
const targetColumns = relation
|
|
2054
|
+
const sourceColumns = relationColumns(relation, "source");
|
|
2055
|
+
const targetColumns = relationColumns(relation, "target");
|
|
1674
2056
|
const through = relation.through;
|
|
1675
2057
|
if (!sourceColumns?.length || !targetColumns?.length || through?.source.length !== sourceColumns.length || through.target.length !== targetColumns.length) throw drizzleError(`WHERE ${relationName}: Relation cannot be used as a filter`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1676
2058
|
const parentColumns = Object.values(getColumns(parentTable));
|
|
@@ -1722,7 +2104,8 @@ const buildRelationExists = (parentTable, relationName, relEntry, innerFilters,
|
|
|
1722
2104
|
fromClause = getTableAsAliasSQL(aliasedTarget);
|
|
1723
2105
|
joinCondition = buildRelationJoinCondition(parentTable, relation, aliasedTarget, relationName);
|
|
1724
2106
|
}
|
|
1725
|
-
const
|
|
2107
|
+
const ownWhere = relationOwnWhere(relation);
|
|
2108
|
+
const relationWhere = ownWhere ? relationsFilterToSQL(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
|
|
1726
2109
|
const inner = innerFilters ? extractFilters(aliasedTarget, targetTableName, innerFilters, {
|
|
1727
2110
|
...ctx,
|
|
1728
2111
|
tableKey: targetTableName,
|
|
@@ -1776,7 +2159,8 @@ const extractFilters = (table, tableName, filters, relationCtx) => {
|
|
|
1776
2159
|
}
|
|
1777
2160
|
if (NOT) {
|
|
1778
2161
|
const extracted = extractFilters(table, tableName, NOT, relationCtx);
|
|
1779
|
-
|
|
2162
|
+
const negated = extracted ? not(extracted) : void 0;
|
|
2163
|
+
if (negated) variants.push(negated);
|
|
1780
2164
|
}
|
|
1781
2165
|
if (OR?.length) {
|
|
1782
2166
|
const orVariants = [];
|
|
@@ -2173,88 +2557,6 @@ const excludedColumnRef = (columnName) => sql`excluded.${sql.identifier(columnNa
|
|
|
2173
2557
|
/** `values(<column>)` — MySQL's equivalent inside ON DUPLICATE KEY UPDATE. */
|
|
2174
2558
|
const mysqlValuesColumnRef = (columnName) => sql`values(${sql.identifier(columnName)})`;
|
|
2175
2559
|
//#endregion
|
|
2176
|
-
//#region src/util/builders/common/relations.ts
|
|
2177
|
-
/**
|
|
2178
|
-
* Flatten drizzle-orm v1 TablesRelationalConfig into the canonical
|
|
2179
|
-
* Record<tableName, Record<relName, TableNamedRelations>> shape used
|
|
2180
|
-
* throughout common.ts. Both pg.ts and sqlite.ts call this before
|
|
2181
|
-
* passing the relation map to any shared function.
|
|
2182
|
-
*/
|
|
2183
|
-
const buildNamedRelations = (relations, tableEntries) => {
|
|
2184
|
-
const namedRelations = {};
|
|
2185
|
-
for (const [relTableName, relConfig] of Object.entries(relations)) {
|
|
2186
|
-
if (!relConfig?.relations) continue;
|
|
2187
|
-
const namedConfig = {};
|
|
2188
|
-
for (const [innerRelName, innerRelValue] of Object.entries(relConfig.relations)) {
|
|
2189
|
-
const targetTable = innerRelValue.targetTable ?? innerRelValue.referencedTable;
|
|
2190
|
-
const directTargetName = innerRelValue.targetTableName;
|
|
2191
|
-
let targetTableName;
|
|
2192
|
-
if (directTargetName) targetTableName = tableEntries.find(([key]) => key === directTargetName)?.[0];
|
|
2193
|
-
else if (targetTable) targetTableName = tableEntries.find(([, tableValue]) => tableValue === targetTable)?.[0];
|
|
2194
|
-
if (!targetTableName) continue;
|
|
2195
|
-
namedConfig[innerRelName] = {
|
|
2196
|
-
relation: innerRelValue,
|
|
2197
|
-
targetTableName
|
|
2198
|
-
};
|
|
2199
|
-
}
|
|
2200
|
-
if (Object.keys(namedConfig).length > 0) namedRelations[relTableName] = namedConfig;
|
|
2201
|
-
}
|
|
2202
|
-
return namedRelations;
|
|
2203
|
-
};
|
|
2204
|
-
/**
|
|
2205
|
-
* Records each relation's target-table primary-key property names on the relation entry,
|
|
2206
|
-
* so the pagination paths (the window-function batch loader and the eager `with:` orderBy
|
|
2207
|
-
* default) can fall back to a deterministic PK order without re-deriving it per request.
|
|
2208
|
-
*
|
|
2209
|
-
* Composite primary keys are only visible through the dialect's getTableConfig, so the
|
|
2210
|
-
* dialect builder passes a `resolvePkNames` that threads the composite column names in.
|
|
2211
|
-
* Mutates the relation entries in place (they are shared with the pruned eager map and
|
|
2212
|
-
* the resolver factory, so attaching once covers every consumer).
|
|
2213
|
-
*/
|
|
2214
|
-
const attachTargetPrimaryKeys = (namedRelations, tables, resolvePkNames) => {
|
|
2215
|
-
const cache = /* @__PURE__ */ new Map();
|
|
2216
|
-
for (const rels of Object.values(namedRelations)) for (const relEntry of Object.values(rels)) {
|
|
2217
|
-
const { targetTableName } = relEntry;
|
|
2218
|
-
let pk = cache.get(targetTableName);
|
|
2219
|
-
if (!pk) {
|
|
2220
|
-
const targetTable = tables[targetTableName];
|
|
2221
|
-
pk = targetTable ? resolvePkNames(targetTable) : [];
|
|
2222
|
-
cache.set(targetTableName, pk);
|
|
2223
|
-
}
|
|
2224
|
-
relEntry.targetPkNames = pk;
|
|
2225
|
-
}
|
|
2226
|
-
};
|
|
2227
|
-
/**
|
|
2228
|
-
* Extracts the join column info from a drizzle-orm v1 Relation object.
|
|
2229
|
-
* Returns the JS property name of the local column on the parent table and the
|
|
2230
|
-
* Column object for the foreign column on the target table, or undefined if the
|
|
2231
|
-
* relation internals are not accessible.
|
|
2232
|
-
*/
|
|
2233
|
-
const extractRelationJoinColumns = (relEntry, parentTable, targetTable) => {
|
|
2234
|
-
const rel = relEntry.relation ?? relEntry;
|
|
2235
|
-
const sourceColumns = rel.sourceColumns;
|
|
2236
|
-
const targetColumns = rel.targetColumns;
|
|
2237
|
-
if (!sourceColumns?.length || !targetColumns?.length) return;
|
|
2238
|
-
const sourceCol = sourceColumns[0];
|
|
2239
|
-
const targetCol = targetColumns[0];
|
|
2240
|
-
const parentCols = getColumns(parentTable);
|
|
2241
|
-
const localColPropName = Object.entries(parentCols).find(([, c]) => c === sourceCol)?.[0];
|
|
2242
|
-
const targetCols = getColumns(targetTable);
|
|
2243
|
-
const foreignColPropName = Object.entries(targetCols).find(([, c]) => c === targetCol)?.[0];
|
|
2244
|
-
if (!localColPropName || !foreignColPropName) return;
|
|
2245
|
-
return {
|
|
2246
|
-
localColPropName,
|
|
2247
|
-
foreignCol: targetCol,
|
|
2248
|
-
foreignColPropName
|
|
2249
|
-
};
|
|
2250
|
-
};
|
|
2251
|
-
/**
|
|
2252
|
-
* Whether a to-one relation can back a relation orderBy hop. `.through()` (junction)
|
|
2253
|
-
* relations are excluded — the ordering subquery only joins the direct target, never the
|
|
2254
|
-
* junction table. (Relation *filters* do support `.through()` — this guard is orderBy-only.)
|
|
2255
|
-
*/
|
|
2256
|
-
const isFilterableRelation = (relation) => !relation.through;
|
|
2257
|
-
//#endregion
|
|
2258
2560
|
//#region src/util/builders/common/order-by.ts
|
|
2259
2561
|
/**
|
|
2260
2562
|
* Column name / direction / nulls triples from an `orderBy` argument, highest priority
|
|
@@ -2300,7 +2602,8 @@ const extendRelationOrderChain = (parentTable, relationName, relEntry, ctx, chai
|
|
|
2300
2602
|
ctx.aliases ??= { n: 0 };
|
|
2301
2603
|
const aliasedTarget = aliasedTable(targetTable, `dgql_ord_${ctx.aliases.n++}`);
|
|
2302
2604
|
const joinCondition = buildRelationJoinCondition(parentTable, relation, aliasedTarget, relationName);
|
|
2303
|
-
const
|
|
2605
|
+
const ownWhere = relationOwnWhere(relation);
|
|
2606
|
+
const relationWhere = ownWhere ? relationsFilterToSQL(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
|
|
2304
2607
|
return {
|
|
2305
2608
|
fromParts: [...chain?.fromParts ?? [], getTableAsAliasSQL(aliasedTarget)],
|
|
2306
2609
|
conditions: [
|
|
@@ -2317,15 +2620,17 @@ const extendRelationOrderChain = (parentTable, relationName, relEntry, ctx, chai
|
|
|
2317
2620
|
*
|
|
2318
2621
|
* A ladder rather than a dialect array function (`array_position` on Postgres, `FIELD()` on
|
|
2319
2622
|
* MySQL, neither on SQLite) because it is the one form all three share, and because every
|
|
2320
|
-
* list value stays a bound parameter
|
|
2321
|
-
*
|
|
2322
|
-
*
|
|
2623
|
+
* list value stays a bound parameter — encoded by the column, as `eq` would, so a value whose
|
|
2624
|
+
* driver form differs from its JS one (a SQLite integer timestamp's `Date`) is bound as stored.
|
|
2625
|
+
* The branch indices are library-generated integers, so they are written into the SQL text — a
|
|
2626
|
+
* bound parameter there would leave Postgres unable to infer the CASE result type. Values
|
|
2627
|
+
* outside the list sort after every listed one.
|
|
2323
2628
|
*/
|
|
2324
2629
|
const filterOrderExpression = (column, columnName, whereArgs) => {
|
|
2325
2630
|
const values = whereArgs?.[columnName]?.inArray;
|
|
2326
2631
|
if (!Array.isArray(values)) throw drizzleError(`ORDER BY ${columnName}: 'matchFilterOrder' needs an 'inArray' filter on the same column in this query's 'where'`, { code: "DRIZZLE_INVALID_ORDER_BY" });
|
|
2327
2632
|
if (!values.length) return sql`0`;
|
|
2328
|
-
const branches = values.map((value, index) => sql`when ${column} = ${remapFromGraphQLCore(value, column, columnName)} then ${sql.raw(String(index))}`);
|
|
2633
|
+
const branches = values.map((value, index) => sql`when ${column} = ${sql.param(remapFromGraphQLCore(value, column, columnName), column)} then ${sql.raw(String(index))}`);
|
|
2329
2634
|
return sql`case ${sql.join(branches, sql` `)} else ${sql.raw(String(values.length))} end`;
|
|
2330
2635
|
};
|
|
2331
2636
|
/**
|
|
@@ -2689,6 +2994,11 @@ const getUniqueColumnSets = (table, getTableConfig) => {
|
|
|
2689
2994
|
//#endregion
|
|
2690
2995
|
//#region src/util/builders/common/distinct.ts
|
|
2691
2996
|
/**
|
|
2997
|
+
* Alias the parent key comes back under when the pass runs through a junction table. See
|
|
2998
|
+
* `selectDistinctKeys`' `junction` parameter.
|
|
2999
|
+
*/
|
|
3000
|
+
const DISTINCT_PARENT = "__dgql_distinct_parent";
|
|
3001
|
+
/**
|
|
2692
3002
|
* Keeps the first row of each distinct combination of the requested columns, following the
|
|
2693
3003
|
* query's own ordering, then applies `limit`/`offset` to what survives — and returns the
|
|
2694
3004
|
* surviving rows' primary key values in that order.
|
|
@@ -2701,9 +3011,14 @@ const getUniqueColumnSets = (table, getTableConfig) => {
|
|
|
2701
3011
|
* `partitionBy` prepends extra columns to the window's partition. A to-many relation field
|
|
2702
3012
|
* uses it to pass the foreign key, which makes the pass distinct *within each parent* rather
|
|
2703
3013
|
* than across the whole batch — the same meaning `distinct` has on a root list.
|
|
3014
|
+
*
|
|
3015
|
+
* `junction` is for a many-to-many relation: the target is joined to the junction `on` the
|
|
3016
|
+
* given condition, and each returned key also carries the parent it was kept for, under
|
|
3017
|
+
* {@link DISTINCT_PARENT}. A target row linked to several parents can be kept for one and
|
|
3018
|
+
* dropped for another, so a key alone cannot narrow such a batch.
|
|
2704
3019
|
*/
|
|
2705
3020
|
const selectDistinctKeys = async (params) => {
|
|
2706
|
-
const { db, table, tableName, distinct, pkNames, where, orderBy, limit, offset, partitionBy } = params;
|
|
3021
|
+
const { db, table, tableName, distinct, pkNames, where, orderBy, limit, offset, partitionBy, junction } = params;
|
|
2707
3022
|
const cols = getColumns(table);
|
|
2708
3023
|
if (!pkNames.length) throw drizzleError(`Table ${tableName} has no primary key, so 'distinct' cannot be applied to it.`, { code: "DRIZZLE_INVALID_DISTINCT" });
|
|
2709
3024
|
const requestedCols = distinct.map((name) => cols[name]).filter(Boolean);
|
|
@@ -2712,12 +3027,17 @@ const selectDistinctKeys = async (params) => {
|
|
|
2712
3027
|
const orderEntries = orderBy ? orderByEntries(orderBy) : [];
|
|
2713
3028
|
const windowOrder = [...orderEntries.flatMap(([column, direction, nulls]) => orderExpressions(cols[column], direction, nulls, cols[column])), ...primaryKeyOrderExprs(table, pkNames)];
|
|
2714
3029
|
const rowNumber = sql`row_number() over (partition by ${sql.join(partitionCols, sql`, `)} order by ${sql.join(windowOrder, sql`, `)})`.as(DISTINCT_RN);
|
|
2715
|
-
const
|
|
3030
|
+
const selected = db.select({
|
|
2716
3031
|
...cols,
|
|
3032
|
+
...junction ? { [DISTINCT_PARENT]: junction.parentKey } : {},
|
|
2717
3033
|
[DISTINCT_RN]: rowNumber
|
|
2718
|
-
}).from(table)
|
|
3034
|
+
}).from(table);
|
|
3035
|
+
const sub = (junction ? selected.innerJoin(junction.table, junction.on) : selected).where(where).as("__dgql_distinct");
|
|
2719
3036
|
const outerOrder = [...orderEntries.flatMap(([column, direction, nulls]) => orderExpressions(sub[column], direction, nulls, cols[column])), ...pkNames.filter((name) => sub[name]).map((name) => asc(sub[name]))];
|
|
2720
|
-
let query = db.select(
|
|
3037
|
+
let query = db.select({
|
|
3038
|
+
...Object.fromEntries(pkNames.map((name) => [name, sub[name]])),
|
|
3039
|
+
...junction ? { [DISTINCT_PARENT]: sub[DISTINCT_PARENT] } : {}
|
|
3040
|
+
}).from(sub).where(eq(sub[DISTINCT_RN], 1)).orderBy(...outerOrder);
|
|
2721
3041
|
if (offset) query = query.offset(offset);
|
|
2722
3042
|
if (limit != null) query = query.limit(limit);
|
|
2723
3043
|
return await query;
|
|
@@ -3291,6 +3611,26 @@ const remapUpdateInput = (values, table, tableName) => {
|
|
|
3291
3611
|
} : remapped;
|
|
3292
3612
|
};
|
|
3293
3613
|
//#endregion
|
|
3614
|
+
//#region src/util/builders/common/rqb-config.ts
|
|
3615
|
+
/**
|
|
3616
|
+
* A copy of a relational query config with every key whose value is `undefined` left out.
|
|
3617
|
+
*
|
|
3618
|
+
* drizzle-orm rc.5 snapshots test `"where" in params` rather than its value, and throw
|
|
3619
|
+
* `Unexpected 'undefined' in filter value` for `{ where: undefined }`; rc.4 read the value and
|
|
3620
|
+
* treated both the same. The library builds its configs with conditional keys
|
|
3621
|
+
* (`where: cond ? … : undefined`), so every config goes through here before it reaches drizzle.
|
|
3622
|
+
* Only `where` is checked that way today, but leaving out every undefined key costs nothing
|
|
3623
|
+
* and keeps the next key that gets the same treatment from breaking queries.
|
|
3624
|
+
*
|
|
3625
|
+
* Shallow on purpose: nested `with:` entries are built (and passed through here) one level at
|
|
3626
|
+
* a time, and the values of the other keys are drizzle's own (column maps, callbacks, RAW).
|
|
3627
|
+
*/
|
|
3628
|
+
const omitUndefinedKeys = (config) => {
|
|
3629
|
+
const out = {};
|
|
3630
|
+
for (const [key, value] of Object.entries(config)) if (value !== void 0) out[key] = value;
|
|
3631
|
+
return out;
|
|
3632
|
+
};
|
|
3633
|
+
//#endregion
|
|
3294
3634
|
//#region src/util/builders/common/selected-columns.ts
|
|
3295
3635
|
const rqbCrashTypes = [
|
|
3296
3636
|
"SQLiteBigInt",
|
|
@@ -3419,7 +3759,7 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
|
|
|
3419
3759
|
thisRecord.offset = offset;
|
|
3420
3760
|
thisRecord.limit = limit;
|
|
3421
3761
|
thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
|
|
3422
|
-
args[relName] = thisRecord;
|
|
3762
|
+
args[relName] = omitUndefinedKeys(thisRecord);
|
|
3423
3763
|
}
|
|
3424
3764
|
return args;
|
|
3425
3765
|
};
|
|
@@ -3496,12 +3836,12 @@ const runRelationalSelect = async (opts) => {
|
|
|
3496
3836
|
}) : void 0
|
|
3497
3837
|
};
|
|
3498
3838
|
if (single) {
|
|
3499
|
-
const result = await queryBase.findFirst(params);
|
|
3839
|
+
const result = await queryBase.findFirst(omitUndefinedKeys(params));
|
|
3500
3840
|
return result ? remapToGraphQLSingleOutput(result, tableName, table, relationMap) : void 0;
|
|
3501
3841
|
}
|
|
3502
3842
|
if (cursorEntries && cursorSelected) for (const [column] of cursorEntries) params.columns[column] = true;
|
|
3503
3843
|
params.limit = distinctKeys ? void 0 : opts.limit;
|
|
3504
|
-
const result = await queryBase.findMany(params);
|
|
3844
|
+
const result = await queryBase.findMany(omitUndefinedKeys(params));
|
|
3505
3845
|
if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
|
|
3506
3846
|
return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
|
|
3507
3847
|
};
|
|
@@ -4318,16 +4658,17 @@ const identifyRows = (field, args) => {
|
|
|
4318
4658
|
* Window functions require PostgreSQL, MySQL >= 8.0, or SQLite >= 3.25.
|
|
4319
4659
|
* Returns raw rows (NOT remapped); the caller groups + remaps them.
|
|
4320
4660
|
*/
|
|
4321
|
-
const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereCondition, orderByArg, limit, offset, pkNames, columns, orderCtx, whereArgs) => {
|
|
4661
|
+
const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereCondition, orderByArg, limit, offset, pkNames, columns, orderCtx, whereArgs, junction) => {
|
|
4322
4662
|
const cols = columns ?? getColumns(targetTable);
|
|
4323
4663
|
const orderExprs = [...orderByArg ? extractOrderBy(targetTable, orderByArg, orderCtx, whereArgs) : [], ...primaryKeyOrderExprs(targetTable, pkNames)];
|
|
4324
4664
|
const orderClause = orderExprs.length ? sql` order by ${sql.join(orderExprs, sql`, `)}` : sql``;
|
|
4325
4665
|
const RN = "__drizzle_graphql_rn";
|
|
4326
|
-
const
|
|
4327
|
-
const
|
|
4666
|
+
const partitionCol = junction ? junction.sourceCol : foreignCol;
|
|
4667
|
+
const rowNumber = sql`row_number() over (partition by ${partitionCol}${orderClause})`.as(RN);
|
|
4668
|
+
const sub = withJunction(db.select({
|
|
4328
4669
|
...cols,
|
|
4329
4670
|
[RN]: rowNumber
|
|
4330
|
-
}).from(targetTable).where(whereCondition).as("__paginated");
|
|
4671
|
+
}).from(targetTable), junction, foreignCol).where(whereCondition).as("__paginated");
|
|
4331
4672
|
const lower = offset ?? 0;
|
|
4332
4673
|
const windowConds = [gt(sub[RN], lower)];
|
|
4333
4674
|
if (limit != null) windowConds.push(lte(sub[RN], lower + limit));
|
|
@@ -4336,6 +4677,14 @@ const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereC
|
|
|
4336
4677
|
return rows;
|
|
4337
4678
|
};
|
|
4338
4679
|
/**
|
|
4680
|
+
* Alias the parent key is selected under when a relation goes through a junction table. The
|
|
4681
|
+
* key lives on the junction, not the target, so it has to be selected explicitly to group the
|
|
4682
|
+
* rows by parent — and a target row linked to several parents comes back once per parent.
|
|
4683
|
+
*/
|
|
4684
|
+
const PARENT_KEY = "__drizzle_graphql_parent";
|
|
4685
|
+
/** `from(target)`, joined to the junction table when the relation has one. */
|
|
4686
|
+
const withJunction = (query, junction, foreignCol) => junction ? query.innerJoin(junction.table, eq(junction.targetCol, foreignCol)) : query;
|
|
4687
|
+
/**
|
|
4339
4688
|
* Creates a RelationResolverFactory that generates field-level resolvers for each relation.
|
|
4340
4689
|
* Each resolver:
|
|
4341
4690
|
* 1. Returns pre-fetched data if the parent resolver already included it (eager path, zero cost).
|
|
@@ -4357,6 +4706,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4357
4706
|
const joinCols = extractRelationJoinColumns(relEntry, parentTable, targetTable);
|
|
4358
4707
|
if (!joinCols) return;
|
|
4359
4708
|
const { localColPropName, foreignCol, foreignColPropName } = joinCols;
|
|
4709
|
+
const junction = extractJunctionColumns(relEntry);
|
|
4710
|
+
const groupCol = junction ? junction.sourceCol : foreignCol;
|
|
4711
|
+
const groupKey = junction ? PARENT_KEY : foreignColPropName;
|
|
4712
|
+
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, getColumns(parentTable)[localColPropName], groupCol);
|
|
4360
4713
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, isOne && relEntry.relation?.optional === false);
|
|
4361
4714
|
const limitPolicy = isOne ? void 0 : limits?.(targetTableName);
|
|
4362
4715
|
const targetPkNames = relEntry.targetPkNames ?? [];
|
|
@@ -4434,11 +4787,12 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4434
4787
|
loaderKey: `${tableName}::${relationName}::${argsKey}`
|
|
4435
4788
|
};
|
|
4436
4789
|
});
|
|
4790
|
+
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" });
|
|
4437
4791
|
return getOrCreateLoader(context, loaderKey, async (parentIds) => {
|
|
4438
4792
|
const executor = resolveExecutor(db, context);
|
|
4439
4793
|
const uniqueIds = [...new Set(parentIds)];
|
|
4440
4794
|
const scope = resolveScope(policies, context, filterCtx);
|
|
4441
|
-
let whereCondition = withScope(scope, targetTableName, targetTable, and(inArray(
|
|
4795
|
+
let whereCondition = withScope(scope, targetTableName, targetTable, and(inArray(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
|
|
4442
4796
|
if (distinct) {
|
|
4443
4797
|
const keys = await selectDistinctKeys({
|
|
4444
4798
|
db: executor,
|
|
@@ -4448,27 +4802,41 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4448
4802
|
pkNames: targetPkNames,
|
|
4449
4803
|
where: whereCondition,
|
|
4450
4804
|
orderBy: orderByArg,
|
|
4451
|
-
partitionBy: [
|
|
4805
|
+
partitionBy: [groupCol],
|
|
4806
|
+
junction: junction ? {
|
|
4807
|
+
table: junction.table,
|
|
4808
|
+
on: eq(junction.targetCol, foreignCol),
|
|
4809
|
+
parentKey: groupCol
|
|
4810
|
+
} : void 0
|
|
4452
4811
|
});
|
|
4453
4812
|
if (!keys.length) return parentIds.map(() => isOne ? null : []);
|
|
4454
|
-
whereCondition = and(whereCondition, primaryKeyRestriction(targetTable, targetPkNames, keys));
|
|
4813
|
+
whereCondition = and(whereCondition, junction ? or(...keys.map((key) => and(eq(groupCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
|
|
4455
4814
|
}
|
|
4815
|
+
const selection = junction ? {
|
|
4816
|
+
...columns ?? getColumns(targetTable),
|
|
4817
|
+
[PARENT_KEY]: groupCol
|
|
4818
|
+
} : columns;
|
|
4456
4819
|
let rows;
|
|
4457
|
-
if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames,
|
|
4820
|
+
if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName), whereArg, junction);
|
|
4458
4821
|
else {
|
|
4459
|
-
let q = (
|
|
4822
|
+
let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
|
|
4460
4823
|
const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName), whereArg) : [];
|
|
4461
4824
|
if (orderExprs.length) q = q.orderBy(...orderExprs);
|
|
4462
4825
|
rows = await q;
|
|
4463
4826
|
}
|
|
4464
4827
|
if (cursorEntries) attachRowCursors(rows, cursorEntries);
|
|
4828
|
+
const keyOf = (row) => {
|
|
4829
|
+
const key = String(row[groupKey]);
|
|
4830
|
+
if (junction) delete row[PARENT_KEY];
|
|
4831
|
+
return key;
|
|
4832
|
+
};
|
|
4465
4833
|
if (isOne) {
|
|
4466
|
-
const byKey = new Map(rows.map((row) => [
|
|
4834
|
+
const byKey = new Map(rows.map((row) => [keyOf(row), row]));
|
|
4467
4835
|
remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
|
|
4468
4836
|
return parentIds.map((id) => byKey.get(String(id)) ?? null);
|
|
4469
4837
|
}
|
|
4470
4838
|
const grouped = new Map(uniqueIds.map((id) => [String(id), []]));
|
|
4471
|
-
for (const row of rows) grouped.get(
|
|
4839
|
+
for (const row of rows) grouped.get(keyOf(row))?.push(row);
|
|
4472
4840
|
remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
|
|
4473
4841
|
return parentIds.map((id) => grouped.get(String(id)) ?? []);
|
|
4474
4842
|
}).load(localValue);
|
|
@@ -4803,7 +5171,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
|
|
|
4803
5171
|
});
|
|
4804
5172
|
const shell = new GraphQLObjectType({
|
|
4805
5173
|
name: typeName,
|
|
4806
|
-
description: cacheCtx.docs.describeTable?.(tableName),
|
|
5174
|
+
description: cacheCtx.docs.describeTable?.(tableName, table),
|
|
4807
5175
|
fields: () => ({
|
|
4808
5176
|
...tableFields,
|
|
4809
5177
|
...container.fields
|
|
@@ -4837,7 +5205,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
|
|
|
4837
5205
|
defaultName: resolveTypeName(targetTableName, typeNameMapper),
|
|
4838
5206
|
table: targetTableName
|
|
4839
5207
|
}),
|
|
4840
|
-
description: cacheCtx.docs.describeTable?.(targetTableName),
|
|
5208
|
+
description: cacheCtx.docs.describeTable?.(targetTableName, targetTable),
|
|
4841
5209
|
fields: () => ({
|
|
4842
5210
|
...targetTableFields,
|
|
4843
5211
|
...capturedTargetContainer.fields
|
|
@@ -4988,7 +5356,7 @@ const generateTableTypes = (tableName, tables, relationMap, withReturning, relat
|
|
|
4988
5356
|
defaultName: resolveTypeName(tableName, typeNameMapper),
|
|
4989
5357
|
table: tableName
|
|
4990
5358
|
}),
|
|
4991
|
-
description: cacheCtx.docs.describeTable?.(tableName),
|
|
5359
|
+
description: cacheCtx.docs.describeTable?.(tableName, tables[tableName]),
|
|
4992
5360
|
fields: {
|
|
4993
5361
|
...tableFields,
|
|
4994
5362
|
...relationFields
|
|
@@ -5539,14 +5907,26 @@ const GROUP_KEY = "__dgql_group_key";
|
|
|
5539
5907
|
* for every other aggregate.
|
|
5540
5908
|
*/
|
|
5541
5909
|
const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, filterCtx, policies) => {
|
|
5910
|
+
const warned = /* @__PURE__ */ new Set();
|
|
5542
5911
|
return ({ tableName, relationName, relEntry }) => {
|
|
5543
5912
|
const parentTable = tables[tableName];
|
|
5544
5913
|
const targetTableName = relEntry.targetTableName;
|
|
5545
5914
|
const targetTable = tables[targetTableName];
|
|
5546
5915
|
if (!parentTable || !targetTable) return;
|
|
5547
5916
|
const joinCols = extractRelationJoinColumns(relEntry, parentTable, targetTable);
|
|
5548
|
-
if (!joinCols)
|
|
5917
|
+
if (!joinCols) {
|
|
5918
|
+
const key = `${tableName}.${relationName}`;
|
|
5919
|
+
if (!warned.has(key)) {
|
|
5920
|
+
warned.add(key);
|
|
5921
|
+
warnUnresolvedJoin(relEntry, tableName, relationName, `its '${relationName}Aggregate' field`);
|
|
5922
|
+
}
|
|
5923
|
+
return;
|
|
5924
|
+
}
|
|
5549
5925
|
const { localColPropName, foreignCol } = joinCols;
|
|
5926
|
+
const junction = extractJunctionColumns(relEntry);
|
|
5927
|
+
if (junction === null) return;
|
|
5928
|
+
const groupCol = junction ? junction.sourceCol : foreignCol;
|
|
5929
|
+
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, getColumns(parentTable)[localColPropName], groupCol);
|
|
5550
5930
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
|
|
5551
5931
|
const targetTypeName = resolveTypeName(targetTableName, typeNameMapper);
|
|
5552
5932
|
const type = generateAggregateTypes(targetTable, targetTableName, targetTypeName, cacheCtx);
|
|
@@ -5577,11 +5957,12 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
5577
5957
|
const loader = getOrCreateLoader(context, loaderKey, async (parentIds) => {
|
|
5578
5958
|
const executor = resolveExecutor(db, context);
|
|
5579
5959
|
const uniqueIds = [...new Set(parentIds)];
|
|
5580
|
-
const whereCondition = withScope(resolveScope(policies, context, filterCtx), targetTableName, targetTable, and(inArray(
|
|
5581
|
-
const
|
|
5582
|
-
[GROUP_KEY]:
|
|
5960
|
+
const whereCondition = withScope(resolveScope(policies, context, filterCtx), targetTableName, targetTable, and(inArray(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName)) : void 0), deleted ?? defaultDeleted);
|
|
5961
|
+
const selected = executor.select({
|
|
5962
|
+
[GROUP_KEY]: groupCol,
|
|
5583
5963
|
...request.selection
|
|
5584
|
-
}).from(targetTable)
|
|
5964
|
+
}).from(targetTable);
|
|
5965
|
+
const rows = await (junction ? selected.innerJoin(junction.table, eq(junction.targetCol, foreignCol)) : selected).where(whereCondition).groupBy(groupCol);
|
|
5585
5966
|
const byKey = new Map(rows.map((row) => [row[GROUP_KEY], row]));
|
|
5586
5967
|
return parentIds.map((id) => byKey.get(id) ?? {});
|
|
5587
5968
|
});
|
|
@@ -5849,12 +6230,6 @@ const propNameOf = (table, column) => {
|
|
|
5849
6230
|
for (const [propName, candidate] of Object.entries(getColumns(table))) if (candidate === column) return propName;
|
|
5850
6231
|
};
|
|
5851
6232
|
/**
|
|
5852
|
-
* The junction column one entry of a relation's `through` config points at. Drizzle wraps it
|
|
5853
|
-
* in a descriptor while the relation is being defined; the raw column is accepted too, so a
|
|
5854
|
-
* change of shape there degrades to skipping the relation rather than mis-planning it.
|
|
5855
|
-
*/
|
|
5856
|
-
const throughColumn = (entry) => entry && is(entry, Column) ? entry : entry?._?.column;
|
|
5857
|
-
/**
|
|
5858
6233
|
* A `.through()` relation's plan: the junction table plus the two columns it joins on.
|
|
5859
6234
|
*
|
|
5860
6235
|
* Nothing about the parent or the target row changes when a many-to-many link is made or
|
|
@@ -5865,21 +6240,27 @@ const throughColumn = (entry) => entry && is(entry, Column) ? entry : entry?._?.
|
|
|
5865
6240
|
* row" guarantee lives in the junction's constraints rather than in the relation; a relation
|
|
5866
6241
|
* joining on more than one column per side; and a junction carrying a further NOT NULL column
|
|
5867
6242
|
* with no default, which no `connect` operand could supply a value for.
|
|
6243
|
+
*
|
|
6244
|
+
* `onUnresolved` is called when the relation's columns are of a supported shape but cannot be
|
|
6245
|
+
* matched to the three tables — a drizzle-orm mismatch rather than an unsupported relation.
|
|
5868
6246
|
*/
|
|
5869
|
-
const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable, tableNameOf) => {
|
|
6247
|
+
const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable, tableNameOf, onUnresolved) => {
|
|
5870
6248
|
if (is(relation, One)) return;
|
|
5871
6249
|
const junction = relation.throughTable;
|
|
5872
6250
|
const sourceEntries = relation.through?.source;
|
|
5873
6251
|
const targetEntries = relation.through?.target;
|
|
5874
6252
|
if (!junction || sourceEntries?.length !== 1 || targetEntries?.length !== 1) return;
|
|
5875
6253
|
if (relation.sourceColumns?.length !== 1 || relation.targetColumns?.length !== 1) return;
|
|
5876
|
-
const localCol = relation.sourceColumns[0];
|
|
5877
|
-
const foreignCol = relation.targetColumns[0];
|
|
6254
|
+
const localCol = unwrapRelationColumn(relation.sourceColumns[0]);
|
|
6255
|
+
const foreignCol = unwrapRelationColumn(relation.targetColumns[0]);
|
|
5878
6256
|
const localColPropName = propNameOf(table, localCol);
|
|
5879
6257
|
const foreignColPropName = propNameOf(targetTable, foreignCol);
|
|
5880
|
-
const sourceColPropName = propNameOf(junction,
|
|
5881
|
-
const targetColPropName = propNameOf(junction,
|
|
5882
|
-
if (!localColPropName || !foreignColPropName || !sourceColPropName || !targetColPropName)
|
|
6258
|
+
const sourceColPropName = propNameOf(junction, unwrapRelationColumn(sourceEntries[0]));
|
|
6259
|
+
const targetColPropName = propNameOf(junction, unwrapRelationColumn(targetEntries[0]));
|
|
6260
|
+
if (!localCol || !foreignCol || !localColPropName || !foreignColPropName || !sourceColPropName || !targetColPropName) {
|
|
6261
|
+
onUnresolved();
|
|
6262
|
+
return;
|
|
6263
|
+
}
|
|
5883
6264
|
const junctionColumns = getColumns(junction);
|
|
5884
6265
|
const sourceCol = junctionColumns[sourceColPropName];
|
|
5885
6266
|
const targetCol = junctionColumns[targetColPropName];
|
|
@@ -5949,13 +6330,17 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
5949
6330
|
const targetTableName = relEntry.targetTableName;
|
|
5950
6331
|
const targetTable = tables[targetTableName];
|
|
5951
6332
|
if (!targetTable) continue;
|
|
6333
|
+
const omitted = `the '${relationName}' field of its nested-write create and update inputs`;
|
|
5952
6334
|
if (relation.through) {
|
|
5953
|
-
const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable, tableNameOf);
|
|
6335
|
+
const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable, tableNameOf, () => warnUnresolvedJoin(relEntry, tableName, relationName, omitted));
|
|
5954
6336
|
if (throughPlan) tablePlans[relationName] = throughPlan;
|
|
5955
6337
|
continue;
|
|
5956
6338
|
}
|
|
5957
6339
|
const joinColumns = extractJoinColumns(relEntry, table, targetTable);
|
|
5958
|
-
if (!joinColumns)
|
|
6340
|
+
if (!joinColumns) {
|
|
6341
|
+
warnUnresolvedJoin(relEntry, tableName, relationName, omitted);
|
|
6342
|
+
continue;
|
|
6343
|
+
}
|
|
5959
6344
|
const { localColPropName, foreignCol, foreignColPropName } = joinColumns;
|
|
5960
6345
|
const localCol = getColumns(table)[localColPropName];
|
|
5961
6346
|
if (!localCol) continue;
|