@vantreeseba/drizzle-graphql 14.0.0 → 15.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 instanceof drizzle_orm_pg_core.PgTimestamp || column instanceof drizzle_orm_pg_core.PgDate) return {
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 instanceof drizzle_orm_pg_core.PgTimestamp || column instanceof drizzle_orm_pg_core.PgTimestampString) return {
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 instanceof drizzle_orm_pg_core.PgDateString) return isInput ? {
415
- type: graphql.GraphQLString,
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
- * - "DateTime" → timestamp and date columns (ordering kept, no string pattern operators)
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
- const ct = column.columnType ?? "";
645
- if (ct === "PgTimestamp" || ct === "PgTimestampString" || ct === "PgDate") return {
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
- graphql_scalars.GraphQLDate,
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 (columnType === "SQLiteTimestamp" || columnType === "SQLiteTimestampMs" || columnType === "MySqlTimestamp" || columnType === "MySqlDateTime" || columnType === "PgTimestamp" || columnType === "PgTimestampString") {
1105
- const formatted = new Date(value);
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 ((columnType === "MySqlDate" || columnType === "PgDate") && typeof value === "string") {
1110
- const dateOnly = value.includes("T") ? value.split("T")[0] : value;
1111
- const check = new Date(dateOnly);
1112
- if (Number.isNaN(check.getTime())) throw drizzleError(`Field '${columnName}' is not a valid date!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1113
- return dateOnly;
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
- if (extracted) variants.push((0, drizzle_orm.not)(extracted));
1664
+ const negated = extracted ? (0, drizzle_orm.not)(extracted) : void 0;
1665
+ if (negated) variants.push(negated);
1470
1666
  }
1471
1667
  if (OR?.length) {
1472
1668
  const orVariants = [];
@@ -1479,6 +1675,150 @@ const extractFiltersColumn = (column, columnName, operators) => {
1479
1675
  return variants.length ? variants.length > 1 ? (0, drizzle_orm.and)(...variants) : variants[0] : void 0;
1480
1676
  };
1481
1677
  //#endregion
1678
+ //#region src/util/builders/common/relations.ts
1679
+ /**
1680
+ * Flatten drizzle-orm v1 TablesRelationalConfig into the canonical
1681
+ * Record<tableName, Record<relName, TableNamedRelations>> shape used
1682
+ * throughout common.ts. Both pg.ts and sqlite.ts call this before
1683
+ * passing the relation map to any shared function.
1684
+ */
1685
+ const buildNamedRelations = (relations, tableEntries) => {
1686
+ const namedRelations = {};
1687
+ for (const [relTableName, relConfig] of Object.entries(relations)) {
1688
+ if (!relConfig?.relations) continue;
1689
+ const namedConfig = {};
1690
+ for (const [innerRelName, innerRelValue] of Object.entries(relConfig.relations)) {
1691
+ const targetTable = innerRelValue.targetTable ?? innerRelValue.referencedTable;
1692
+ const directTargetName = innerRelValue.targetTableName;
1693
+ let targetTableName;
1694
+ if (directTargetName) targetTableName = tableEntries.find(([key]) => key === directTargetName)?.[0];
1695
+ else if (targetTable) targetTableName = tableEntries.find(([, tableValue]) => tableValue === targetTable)?.[0];
1696
+ if (!targetTableName) continue;
1697
+ namedConfig[innerRelName] = {
1698
+ relation: innerRelValue,
1699
+ targetTableName
1700
+ };
1701
+ }
1702
+ if (Object.keys(namedConfig).length > 0) namedRelations[relTableName] = namedConfig;
1703
+ }
1704
+ return namedRelations;
1705
+ };
1706
+ /**
1707
+ * Records each relation's target-table primary-key property names on the relation entry,
1708
+ * so the pagination paths (the window-function batch loader and the eager `with:` orderBy
1709
+ * default) can fall back to a deterministic PK order without re-deriving it per request.
1710
+ *
1711
+ * Composite primary keys are only visible through the dialect's getTableConfig, so the
1712
+ * dialect builder passes a `resolvePkNames` that threads the composite column names in.
1713
+ * Mutates the relation entries in place (they are shared with the pruned eager map and
1714
+ * the resolver factory, so attaching once covers every consumer).
1715
+ */
1716
+ const attachTargetPrimaryKeys = (namedRelations, tables, resolvePkNames) => {
1717
+ const cache = /* @__PURE__ */ new Map();
1718
+ for (const rels of Object.values(namedRelations)) for (const relEntry of Object.values(rels)) {
1719
+ const { targetTableName } = relEntry;
1720
+ let pk = cache.get(targetTableName);
1721
+ if (!pk) {
1722
+ const targetTable = tables[targetTableName];
1723
+ pk = targetTable ? resolvePkNames(targetTable) : [];
1724
+ cache.set(targetTableName, pk);
1725
+ }
1726
+ relEntry.targetPkNames = pk;
1727
+ }
1728
+ };
1729
+ /**
1730
+ * The table column behind one entry of a relation's `sourceColumns`, `targetColumns` or
1731
+ * `through` lists.
1732
+ *
1733
+ * drizzle-orm rc.4 stores the table's own Column objects in `sourceColumns` / `targetColumns`
1734
+ * and a `RelationsBuilderColumn` descriptor (`{ _: { column, key, tableName } }`) in `through`.
1735
+ * The rc.5 snapshots store the descriptor in all of them. Both shapes are accepted here, told
1736
+ * apart structurally rather than by importing a class one of those versions does not export.
1737
+ * Anything else yields `undefined`, which callers treat as "join columns not readable".
1738
+ */
1739
+ const unwrapRelationColumn = (entry) => {
1740
+ if ((0, drizzle_orm.is)(entry, drizzle_orm.Column)) return entry;
1741
+ const inner = entry?._?.column;
1742
+ return (0, drizzle_orm.is)(inner, drizzle_orm.Column) ? inner : void 0;
1743
+ };
1744
+ /**
1745
+ * A relation's `sourceColumns` or `targetColumns` as table columns, via
1746
+ * {@link unwrapRelationColumn}. `undefined` when the list is missing, empty, or has an entry
1747
+ * that is not a column in either known shape.
1748
+ */
1749
+ const relationColumns = (relation, side) => {
1750
+ const entries = relation?.[side === "source" ? "sourceColumns" : "targetColumns"];
1751
+ if (!entries?.length) return;
1752
+ const columns = [];
1753
+ for (const entry of entries) {
1754
+ const column = unwrapRelationColumn(entry);
1755
+ if (!column) return;
1756
+ columns.push(column);
1757
+ }
1758
+ return columns;
1759
+ };
1760
+ /**
1761
+ * drizzle-orm's "no filter" sentinel. rc.5 snapshots export it as `EmptyFilter` and use it as
1762
+ * the default `relation.where`; rc.4 has no such export and leaves `where` undefined. It is a
1763
+ * registered symbol, so it can be compared against without importing anything.
1764
+ */
1765
+ const EMPTY_FILTER = Symbol.for("drizzle:EmptyFilter");
1766
+ /**
1767
+ * The relation's own declared `where` filter, or `undefined` when it has none. rc.4 leaves
1768
+ * `where` undefined in that case; rc.5 snapshots set it to the `EmptyFilter` sentinel, which is
1769
+ * truthy and must not be read as a filter.
1770
+ */
1771
+ const relationOwnWhere = (relation) => {
1772
+ const where = relation?.where;
1773
+ return where == null || where === EMPTY_FILTER ? void 0 : where;
1774
+ };
1775
+ /**
1776
+ * Whether the relation's `where` was inherited from its reverse relation, and so is written
1777
+ * against the parent table rather than the target. rc.4 calls this flag `isReversed`; rc.5
1778
+ * snapshots renamed it to `isFilterReversed`. Both are set in the same place with the same
1779
+ * meaning (true only for a relation declared without `from`/`to` and without its own `where`).
1780
+ */
1781
+ const isRelationFilterReversed = (relation) => !!(relation?.isFilterReversed ?? relation?.isReversed);
1782
+ /**
1783
+ * Warns, at build time, that a field was left out because the relation's join columns could
1784
+ * not be matched to its tables. Omitting it silently is how a drizzle-orm change in the shape
1785
+ * of relation columns (issue #179) went unnoticed. A relation that declares no join columns
1786
+ * is not reported: there is nothing that failed to match.
1787
+ */
1788
+ const warnUnresolvedJoin = (relEntry, tableName, relationName, omitted) => {
1789
+ const rel = relEntry.relation ?? relEntry;
1790
+ if (!rel?.sourceColumns?.length || !rel?.targetColumns?.length) return;
1791
+ console.warn(`Drizzle-GraphQL Warning: relation '${tableName}.${relationName}' declares join columns that could not be matched to columns of '${tableName}' and '${relEntry.targetTableName}', so ${omitted} is not generated. The installed drizzle-orm probably stores relation columns in a shape this version of drizzle-graphql does not recognize.`);
1792
+ };
1793
+ /**
1794
+ * Extracts the join column info from a drizzle-orm v1 Relation object.
1795
+ * Returns the JS property name of the local column on the parent table and the
1796
+ * Column object for the foreign column on the target table, or undefined if the
1797
+ * relation internals are not accessible.
1798
+ */
1799
+ const extractRelationJoinColumns = (relEntry, parentTable, targetTable) => {
1800
+ const rel = relEntry.relation ?? relEntry;
1801
+ const sourceCol = relationColumns(rel, "source")?.[0];
1802
+ const targetCol = relationColumns(rel, "target")?.[0];
1803
+ if (!sourceCol || !targetCol) return;
1804
+ const parentCols = (0, drizzle_orm.getColumns)(parentTable);
1805
+ const localColPropName = Object.entries(parentCols).find(([, c]) => c === sourceCol)?.[0];
1806
+ const targetCols = (0, drizzle_orm.getColumns)(targetTable);
1807
+ const foreignColPropName = Object.entries(targetCols).find(([, c]) => c === targetCol)?.[0];
1808
+ if (!localColPropName || !foreignColPropName) return;
1809
+ return {
1810
+ localColPropName,
1811
+ foreignCol: targetCol,
1812
+ foreignColPropName
1813
+ };
1814
+ };
1815
+ /**
1816
+ * Whether a to-one relation can back a relation orderBy hop. `.through()` (junction)
1817
+ * relations are excluded — the ordering subquery only joins the direct target, never the
1818
+ * junction table. (Relation *filters* do support `.through()` — this guard is orderBy-only.)
1819
+ */
1820
+ const isFilterableRelation = (relation) => !relation.through;
1821
+ //#endregion
1482
1822
  //#region src/util/builders/common/exclusions.ts
1483
1823
  /**
1484
1824
  * Columns this build keeps out of the generated schema, keyed by the table object.
@@ -1654,8 +1994,8 @@ const relationFilterCtx = (base, tableKey) => {
1654
1994
  * the parent is an aliased proxy (as it is inside a relational `with:` where callback).
1655
1995
  */
1656
1996
  const buildRelationJoinCondition = (parentTable, relation, aliasedTarget, relationName) => {
1657
- const sourceColumns = relation.sourceColumns;
1658
- const targetColumns = relation.targetColumns;
1997
+ const sourceColumns = relationColumns(relation, "source");
1998
+ const targetColumns = relationColumns(relation, "target");
1659
1999
  if (!sourceColumns?.length || sourceColumns.length !== targetColumns?.length) throw drizzleError(`WHERE ${relationName}: Relation cannot be used as a filter`, { code: "DRIZZLE_INVALID_FILTER" });
1660
2000
  const parentColumns = Object.values((0, drizzle_orm.getColumns)(parentTable));
1661
2001
  const targetColumnsByName = Object.values((0, drizzle_orm.getColumns)(aliasedTarget));
@@ -1679,7 +2019,7 @@ const resolveJunctionColumn = (aliasedThrough, junctionRef, relationName) => {
1679
2019
  const key = junctionRef?._?.key;
1680
2020
  const byKey = key ? throughColumns[key] : void 0;
1681
2021
  if (byKey) return byKey;
1682
- const columnName = junctionRef?._?.column?.name;
2022
+ const columnName = unwrapRelationColumn(junctionRef)?.name;
1683
2023
  const byName = columnName ? Object.values(throughColumns).find((c) => c.name === columnName) : void 0;
1684
2024
  if (!byName) throw drizzleError(`WHERE ${relationName}: Relation cannot be used as a filter`, { code: "DRIZZLE_INVALID_FILTER" });
1685
2025
  return byName;
@@ -1693,8 +2033,8 @@ const resolveJunctionColumn = (aliasedThrough, junctionRef, relationName) => {
1693
2033
  * `through` metadata.
1694
2034
  */
1695
2035
  const buildThroughJoinConditions = (parentTable, relation, aliasedThrough, aliasedTarget, relationName) => {
1696
- const sourceColumns = relation.sourceColumns;
1697
- const targetColumns = relation.targetColumns;
2036
+ const sourceColumns = relationColumns(relation, "source");
2037
+ const targetColumns = relationColumns(relation, "target");
1698
2038
  const through = relation.through;
1699
2039
  if (!sourceColumns?.length || !targetColumns?.length || through?.source.length !== sourceColumns.length || through.target.length !== targetColumns.length) throw drizzleError(`WHERE ${relationName}: Relation cannot be used as a filter`, { code: "DRIZZLE_INVALID_FILTER" });
1700
2040
  const parentColumns = Object.values((0, drizzle_orm.getColumns)(parentTable));
@@ -1746,7 +2086,8 @@ const buildRelationExists = (parentTable, relationName, relEntry, innerFilters,
1746
2086
  fromClause = (0, drizzle_orm.getTableAsAliasSQL)(aliasedTarget);
1747
2087
  joinCondition = buildRelationJoinCondition(parentTable, relation, aliasedTarget, relationName);
1748
2088
  }
1749
- const relationWhere = relation.where ? (0, drizzle_orm.relationsFilterToSQL)(relation.isReversed ? parentTable : aliasedTarget, relation.where) : void 0;
2089
+ const ownWhere = relationOwnWhere(relation);
2090
+ const relationWhere = ownWhere ? (0, drizzle_orm.relationsFilterToSQL)(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
1750
2091
  const inner = innerFilters ? extractFilters(aliasedTarget, targetTableName, innerFilters, {
1751
2092
  ...ctx,
1752
2093
  tableKey: targetTableName,
@@ -1800,7 +2141,8 @@ const extractFilters = (table, tableName, filters, relationCtx) => {
1800
2141
  }
1801
2142
  if (NOT) {
1802
2143
  const extracted = extractFilters(table, tableName, NOT, relationCtx);
1803
- if (extracted) variants.push((0, drizzle_orm.not)(extracted));
2144
+ const negated = extracted ? (0, drizzle_orm.not)(extracted) : void 0;
2145
+ if (negated) variants.push(negated);
1804
2146
  }
1805
2147
  if (OR?.length) {
1806
2148
  const orVariants = [];
@@ -2197,88 +2539,6 @@ const excludedColumnRef = (columnName) => drizzle_orm.sql`excluded.${drizzle_orm
2197
2539
  /** `values(<column>)` — MySQL's equivalent inside ON DUPLICATE KEY UPDATE. */
2198
2540
  const mysqlValuesColumnRef = (columnName) => drizzle_orm.sql`values(${drizzle_orm.sql.identifier(columnName)})`;
2199
2541
  //#endregion
2200
- //#region src/util/builders/common/relations.ts
2201
- /**
2202
- * Flatten drizzle-orm v1 TablesRelationalConfig into the canonical
2203
- * Record<tableName, Record<relName, TableNamedRelations>> shape used
2204
- * throughout common.ts. Both pg.ts and sqlite.ts call this before
2205
- * passing the relation map to any shared function.
2206
- */
2207
- const buildNamedRelations = (relations, tableEntries) => {
2208
- const namedRelations = {};
2209
- for (const [relTableName, relConfig] of Object.entries(relations)) {
2210
- if (!relConfig?.relations) continue;
2211
- const namedConfig = {};
2212
- for (const [innerRelName, innerRelValue] of Object.entries(relConfig.relations)) {
2213
- const targetTable = innerRelValue.targetTable ?? innerRelValue.referencedTable;
2214
- const directTargetName = innerRelValue.targetTableName;
2215
- let targetTableName;
2216
- if (directTargetName) targetTableName = tableEntries.find(([key]) => key === directTargetName)?.[0];
2217
- else if (targetTable) targetTableName = tableEntries.find(([, tableValue]) => tableValue === targetTable)?.[0];
2218
- if (!targetTableName) continue;
2219
- namedConfig[innerRelName] = {
2220
- relation: innerRelValue,
2221
- targetTableName
2222
- };
2223
- }
2224
- if (Object.keys(namedConfig).length > 0) namedRelations[relTableName] = namedConfig;
2225
- }
2226
- return namedRelations;
2227
- };
2228
- /**
2229
- * Records each relation's target-table primary-key property names on the relation entry,
2230
- * so the pagination paths (the window-function batch loader and the eager `with:` orderBy
2231
- * default) can fall back to a deterministic PK order without re-deriving it per request.
2232
- *
2233
- * Composite primary keys are only visible through the dialect's getTableConfig, so the
2234
- * dialect builder passes a `resolvePkNames` that threads the composite column names in.
2235
- * Mutates the relation entries in place (they are shared with the pruned eager map and
2236
- * the resolver factory, so attaching once covers every consumer).
2237
- */
2238
- const attachTargetPrimaryKeys = (namedRelations, tables, resolvePkNames) => {
2239
- const cache = /* @__PURE__ */ new Map();
2240
- for (const rels of Object.values(namedRelations)) for (const relEntry of Object.values(rels)) {
2241
- const { targetTableName } = relEntry;
2242
- let pk = cache.get(targetTableName);
2243
- if (!pk) {
2244
- const targetTable = tables[targetTableName];
2245
- pk = targetTable ? resolvePkNames(targetTable) : [];
2246
- cache.set(targetTableName, pk);
2247
- }
2248
- relEntry.targetPkNames = pk;
2249
- }
2250
- };
2251
- /**
2252
- * Extracts the join column info from a drizzle-orm v1 Relation object.
2253
- * Returns the JS property name of the local column on the parent table and the
2254
- * Column object for the foreign column on the target table, or undefined if the
2255
- * relation internals are not accessible.
2256
- */
2257
- const extractRelationJoinColumns = (relEntry, parentTable, targetTable) => {
2258
- const rel = relEntry.relation ?? relEntry;
2259
- const sourceColumns = rel.sourceColumns;
2260
- const targetColumns = rel.targetColumns;
2261
- if (!sourceColumns?.length || !targetColumns?.length) return;
2262
- const sourceCol = sourceColumns[0];
2263
- const targetCol = targetColumns[0];
2264
- const parentCols = (0, drizzle_orm.getColumns)(parentTable);
2265
- const localColPropName = Object.entries(parentCols).find(([, c]) => c === sourceCol)?.[0];
2266
- const targetCols = (0, drizzle_orm.getColumns)(targetTable);
2267
- const foreignColPropName = Object.entries(targetCols).find(([, c]) => c === targetCol)?.[0];
2268
- if (!localColPropName || !foreignColPropName) return;
2269
- return {
2270
- localColPropName,
2271
- foreignCol: targetCol,
2272
- foreignColPropName
2273
- };
2274
- };
2275
- /**
2276
- * Whether a to-one relation can back a relation orderBy hop. `.through()` (junction)
2277
- * relations are excluded — the ordering subquery only joins the direct target, never the
2278
- * junction table. (Relation *filters* do support `.through()` — this guard is orderBy-only.)
2279
- */
2280
- const isFilterableRelation = (relation) => !relation.through;
2281
- //#endregion
2282
2542
  //#region src/util/builders/common/order-by.ts
2283
2543
  /**
2284
2544
  * Column name / direction / nulls triples from an `orderBy` argument, highest priority
@@ -2324,7 +2584,8 @@ const extendRelationOrderChain = (parentTable, relationName, relEntry, ctx, chai
2324
2584
  ctx.aliases ??= { n: 0 };
2325
2585
  const aliasedTarget = (0, drizzle_orm.aliasedTable)(targetTable, `dgql_ord_${ctx.aliases.n++}`);
2326
2586
  const joinCondition = buildRelationJoinCondition(parentTable, relation, aliasedTarget, relationName);
2327
- const relationWhere = relation.where ? (0, drizzle_orm.relationsFilterToSQL)(relation.isReversed ? parentTable : aliasedTarget, relation.where) : void 0;
2587
+ const ownWhere = relationOwnWhere(relation);
2588
+ const relationWhere = ownWhere ? (0, drizzle_orm.relationsFilterToSQL)(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
2328
2589
  return {
2329
2590
  fromParts: [...chain?.fromParts ?? [], (0, drizzle_orm.getTableAsAliasSQL)(aliasedTarget)],
2330
2591
  conditions: [
@@ -2341,15 +2602,17 @@ const extendRelationOrderChain = (parentTable, relationName, relEntry, ctx, chai
2341
2602
  *
2342
2603
  * A ladder rather than a dialect array function (`array_position` on Postgres, `FIELD()` on
2343
2604
  * MySQL, neither on SQLite) because it is the one form all three share, and because every
2344
- * list value stays a bound parameter. The branch indices are library-generated integers, so
2345
- * they are written into the SQL text — a bound parameter there would leave Postgres unable
2346
- * to infer the CASE result type. Values outside the list sort after every listed one.
2605
+ * list value stays a bound parameter — encoded by the column, as `eq` would, so a value whose
2606
+ * driver form differs from its JS one (a SQLite integer timestamp's `Date`) is bound as stored.
2607
+ * The branch indices are library-generated integers, so they are written into the SQL text — a
2608
+ * bound parameter there would leave Postgres unable to infer the CASE result type. Values
2609
+ * outside the list sort after every listed one.
2347
2610
  */
2348
2611
  const filterOrderExpression = (column, columnName, whereArgs) => {
2349
2612
  const values = whereArgs?.[columnName]?.inArray;
2350
2613
  if (!Array.isArray(values)) throw drizzleError(`ORDER BY ${columnName}: 'matchFilterOrder' needs an 'inArray' filter on the same column in this query's 'where'`, { code: "DRIZZLE_INVALID_ORDER_BY" });
2351
2614
  if (!values.length) return drizzle_orm.sql`0`;
2352
- const branches = values.map((value, index) => drizzle_orm.sql`when ${column} = ${remapFromGraphQLCore(value, column, columnName)} then ${drizzle_orm.sql.raw(String(index))}`);
2615
+ const branches = values.map((value, index) => drizzle_orm.sql`when ${column} = ${drizzle_orm.sql.param(remapFromGraphQLCore(value, column, columnName), column)} then ${drizzle_orm.sql.raw(String(index))}`);
2353
2616
  return drizzle_orm.sql`case ${drizzle_orm.sql.join(branches, drizzle_orm.sql` `)} else ${drizzle_orm.sql.raw(String(values.length))} end`;
2354
2617
  };
2355
2618
  /**
@@ -3315,6 +3578,26 @@ const remapUpdateInput = (values, table, tableName) => {
3315
3578
  } : remapped;
3316
3579
  };
3317
3580
  //#endregion
3581
+ //#region src/util/builders/common/rqb-config.ts
3582
+ /**
3583
+ * A copy of a relational query config with every key whose value is `undefined` left out.
3584
+ *
3585
+ * drizzle-orm rc.5 snapshots test `"where" in params` rather than its value, and throw
3586
+ * `Unexpected 'undefined' in filter value` for `{ where: undefined }`; rc.4 read the value and
3587
+ * treated both the same. The library builds its configs with conditional keys
3588
+ * (`where: cond ? … : undefined`), so every config goes through here before it reaches drizzle.
3589
+ * Only `where` is checked that way today, but leaving out every undefined key costs nothing
3590
+ * and keeps the next key that gets the same treatment from breaking queries.
3591
+ *
3592
+ * Shallow on purpose: nested `with:` entries are built (and passed through here) one level at
3593
+ * a time, and the values of the other keys are drizzle's own (column maps, callbacks, RAW).
3594
+ */
3595
+ const omitUndefinedKeys = (config) => {
3596
+ const out = {};
3597
+ for (const [key, value] of Object.entries(config)) if (value !== void 0) out[key] = value;
3598
+ return out;
3599
+ };
3600
+ //#endregion
3318
3601
  //#region src/util/builders/common/selected-columns.ts
3319
3602
  const rqbCrashTypes = [
3320
3603
  "SQLiteBigInt",
@@ -3443,7 +3726,7 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
3443
3726
  thisRecord.offset = offset;
3444
3727
  thisRecord.limit = limit;
3445
3728
  thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
3446
- args[relName] = thisRecord;
3729
+ args[relName] = omitUndefinedKeys(thisRecord);
3447
3730
  }
3448
3731
  return args;
3449
3732
  };
@@ -3520,12 +3803,12 @@ const runRelationalSelect = async (opts) => {
3520
3803
  }) : void 0
3521
3804
  };
3522
3805
  if (single) {
3523
- const result = await queryBase.findFirst(params);
3806
+ const result = await queryBase.findFirst(omitUndefinedKeys(params));
3524
3807
  return result ? remapToGraphQLSingleOutput(result, tableName, table, relationMap) : void 0;
3525
3808
  }
3526
3809
  if (cursorEntries && cursorSelected) for (const [column] of cursorEntries) params.columns[column] = true;
3527
3810
  params.limit = distinctKeys ? void 0 : opts.limit;
3528
- const result = await queryBase.findMany(params);
3811
+ const result = await queryBase.findMany(omitUndefinedKeys(params));
3529
3812
  if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
3530
3813
  return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
3531
3814
  };
@@ -5563,13 +5846,21 @@ const GROUP_KEY = "__dgql_group_key";
5563
5846
  * for every other aggregate.
5564
5847
  */
5565
5848
  const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, filterCtx, policies) => {
5849
+ const warned = /* @__PURE__ */ new Set();
5566
5850
  return ({ tableName, relationName, relEntry }) => {
5567
5851
  const parentTable = tables[tableName];
5568
5852
  const targetTableName = relEntry.targetTableName;
5569
5853
  const targetTable = tables[targetTableName];
5570
5854
  if (!parentTable || !targetTable) return;
5571
5855
  const joinCols = extractRelationJoinColumns(relEntry, parentTable, targetTable);
5572
- if (!joinCols) return;
5856
+ if (!joinCols) {
5857
+ const key = `${tableName}.${relationName}`;
5858
+ if (!warned.has(key)) {
5859
+ warned.add(key);
5860
+ warnUnresolvedJoin(relEntry, tableName, relationName, `its '${relationName}Aggregate' field`);
5861
+ }
5862
+ return;
5863
+ }
5573
5864
  const { localColPropName, foreignCol } = joinCols;
5574
5865
  const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
5575
5866
  const targetTypeName = resolveTypeName(targetTableName, typeNameMapper);
@@ -5873,12 +6164,6 @@ const propNameOf = (table, column) => {
5873
6164
  for (const [propName, candidate] of Object.entries((0, drizzle_orm.getColumns)(table))) if (candidate === column) return propName;
5874
6165
  };
5875
6166
  /**
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
6167
  * A `.through()` relation's plan: the junction table plus the two columns it joins on.
5883
6168
  *
5884
6169
  * Nothing about the parent or the target row changes when a many-to-many link is made or
@@ -5889,21 +6174,27 @@ const throughColumn = (entry) => entry && (0, drizzle_orm.is)(entry, drizzle_orm
5889
6174
  * row" guarantee lives in the junction's constraints rather than in the relation; a relation
5890
6175
  * joining on more than one column per side; and a junction carrying a further NOT NULL column
5891
6176
  * with no default, which no `connect` operand could supply a value for.
6177
+ *
6178
+ * `onUnresolved` is called when the relation's columns are of a supported shape but cannot be
6179
+ * matched to the three tables — a drizzle-orm mismatch rather than an unsupported relation.
5892
6180
  */
5893
- const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable, tableNameOf) => {
6181
+ const buildThroughPlan = (relationName, relation, table, targetTableName, targetTable, tableNameOf, onUnresolved) => {
5894
6182
  if ((0, drizzle_orm.is)(relation, drizzle_orm.One)) return;
5895
6183
  const junction = relation.throughTable;
5896
6184
  const sourceEntries = relation.through?.source;
5897
6185
  const targetEntries = relation.through?.target;
5898
6186
  if (!junction || sourceEntries?.length !== 1 || targetEntries?.length !== 1) return;
5899
6187
  if (relation.sourceColumns?.length !== 1 || relation.targetColumns?.length !== 1) return;
5900
- const localCol = relation.sourceColumns[0];
5901
- const foreignCol = relation.targetColumns[0];
6188
+ const localCol = unwrapRelationColumn(relation.sourceColumns[0]);
6189
+ const foreignCol = unwrapRelationColumn(relation.targetColumns[0]);
5902
6190
  const localColPropName = propNameOf(table, localCol);
5903
6191
  const foreignColPropName = propNameOf(targetTable, foreignCol);
5904
- const sourceColPropName = propNameOf(junction, throughColumn(sourceEntries[0]));
5905
- const targetColPropName = propNameOf(junction, throughColumn(targetEntries[0]));
5906
- if (!localColPropName || !foreignColPropName || !sourceColPropName || !targetColPropName) return;
6192
+ const sourceColPropName = propNameOf(junction, unwrapRelationColumn(sourceEntries[0]));
6193
+ const targetColPropName = propNameOf(junction, unwrapRelationColumn(targetEntries[0]));
6194
+ if (!localCol || !foreignCol || !localColPropName || !foreignColPropName || !sourceColPropName || !targetColPropName) {
6195
+ onUnresolved();
6196
+ return;
6197
+ }
5907
6198
  const junctionColumns = (0, drizzle_orm.getColumns)(junction);
5908
6199
  const sourceCol = junctionColumns[sourceColPropName];
5909
6200
  const targetCol = junctionColumns[targetColPropName];
@@ -5973,13 +6264,17 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
5973
6264
  const targetTableName = relEntry.targetTableName;
5974
6265
  const targetTable = tables[targetTableName];
5975
6266
  if (!targetTable) continue;
6267
+ const omitted = `the '${relationName}' field of its nested-write create and update inputs`;
5976
6268
  if (relation.through) {
5977
- const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable, tableNameOf);
6269
+ const throughPlan = buildThroughPlan(relationName, relation, table, targetTableName, targetTable, tableNameOf, () => warnUnresolvedJoin(relEntry, tableName, relationName, omitted));
5978
6270
  if (throughPlan) tablePlans[relationName] = throughPlan;
5979
6271
  continue;
5980
6272
  }
5981
6273
  const joinColumns = extractJoinColumns(relEntry, table, targetTable);
5982
- if (!joinColumns) continue;
6274
+ if (!joinColumns) {
6275
+ warnUnresolvedJoin(relEntry, tableName, relationName, omitted);
6276
+ continue;
6277
+ }
5983
6278
  const { localColPropName, foreignCol, foreignColPropName } = joinColumns;
5984
6279
  const localCol = (0, drizzle_orm.getColumns)(table)[localColPropName];
5985
6280
  if (!localCol) continue;
@@ -8385,7 +8680,7 @@ const buildSchema = (db, config) => {
8385
8680
  };
8386
8681
  //#endregion
8387
8682
  exports.GraphQLBigIntString = GraphQLBigIntString;
8388
- exports.GraphQLDate = graphql_scalars.GraphQLDate;
8683
+ exports.GraphQLDate = GraphQLDate;
8389
8684
  exports.GraphQLDateTime = graphql_scalars.GraphQLDateTime;
8390
8685
  exports.GraphQLDecimalString = GraphQLDecimalString;
8391
8686
  exports.GraphQLJSON = graphql_scalars.GraphQLJSON;