@vantreeseba/drizzle-graphql 15.0.0 → 15.1.1

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
@@ -1616,13 +1616,20 @@ const nullableOps = {
1616
1616
  isNull: drizzle_orm.isNull,
1617
1617
  isNotNull: drizzle_orm.isNotNull
1618
1618
  };
1619
+ /** Operators whose operand is an on/off flag rather than a value: `false` adds no condition. */
1620
+ const FLAG_OPERATORS = /* @__PURE__ */ new Set([
1621
+ "isNull",
1622
+ "isNotNull",
1623
+ "isEmpty"
1624
+ ]);
1619
1625
  const extractFiltersColumn = (column, columnName, operators) => {
1620
1626
  const { OR, AND, NOT, insensitive, ...restOperators } = operators;
1621
1627
  const foldCase = insensitive === true;
1622
1628
  const entries = Object.entries(restOperators);
1623
1629
  const variants = [];
1624
1630
  for (const [operatorName, operatorValue] of entries) {
1625
- if (operatorValue === null || operatorValue === false) continue;
1631
+ if (operatorValue === null || operatorValue === void 0) continue;
1632
+ if (operatorValue === false && FLAG_OPERATORS.has(operatorName)) continue;
1626
1633
  if (operatorName in singleValueOps) {
1627
1634
  const singleValue = remapFromGraphQLCore(operatorValue, column, columnName);
1628
1635
  variants.push(foldCase ? lowerComparison(column, operatorName, singleValue) : singleValueOps[operatorName](column, singleValue));
@@ -1813,6 +1820,48 @@ const extractRelationJoinColumns = (relEntry, parentTable, targetTable) => {
1813
1820
  };
1814
1821
  };
1815
1822
  /**
1823
+ * The junction table and its two columns for a `.through()` (many-to-many) relation:
1824
+ * `sourceCol` holds the parent's key, `targetCol` the target's. `undefined` for a relation
1825
+ * with no junction; `null` for one whose junction cannot be used here — more than one column
1826
+ * on either side, or columns that are not on the junction table.
1827
+ */
1828
+ const extractJunctionColumns = (relEntry) => {
1829
+ const rel = relEntry.relation ?? relEntry;
1830
+ const junction = rel?.throughTable;
1831
+ if (!junction) return;
1832
+ const sourceEntries = rel.through?.source;
1833
+ const targetEntries = rel.through?.target;
1834
+ if (sourceEntries?.length !== 1 || targetEntries?.length !== 1) return null;
1835
+ const junctionCols = Object.values((0, drizzle_orm.getColumns)(junction));
1836
+ const sourceCol = unwrapRelationColumn(sourceEntries[0]);
1837
+ const targetCol = unwrapRelationColumn(targetEntries[0]);
1838
+ if (!sourceCol || !targetCol || !junctionCols.includes(sourceCol) || !junctionCols.includes(targetCol)) return null;
1839
+ return {
1840
+ table: junction,
1841
+ sourceCol,
1842
+ targetCol
1843
+ };
1844
+ };
1845
+ /**
1846
+ * The relation's own declared `where` as a condition on a query that reads the target table,
1847
+ * or `undefined` when it has none. The eager path gets this from drizzle's `with:`; the batched
1848
+ * field resolver and `<relation>Aggregate` read the target table directly, so they have to add
1849
+ * it themselves or they return rows the relation hides.
1850
+ *
1851
+ * A reversed filter (inherited from the relation on the other side) is written against the
1852
+ * parent table, so it is applied as "the row's parent key belongs to a parent that passes it".
1853
+ * `parentKeyCol` is the parent's join column and `groupCol` the column in the query that holds
1854
+ * the parent key for each row — the target's foreign key, or the junction's source column.
1855
+ */
1856
+ const relationOwnCondition = (relEntry, parentTable, targetTable, parentKeyCol, groupCol) => {
1857
+ const rel = relEntry.relation ?? relEntry;
1858
+ const ownWhere = relationOwnWhere(rel);
1859
+ if (!ownWhere) return;
1860
+ if (!isRelationFilterReversed(rel)) return (0, drizzle_orm.relationsFilterToSQL)(targetTable, ownWhere);
1861
+ const parentFilter = (0, drizzle_orm.relationsFilterToSQL)(parentTable, ownWhere);
1862
+ return drizzle_orm.sql`${groupCol} in (select ${parentKeyCol} from ${parentTable} where ${parentFilter})`;
1863
+ };
1864
+ /**
1816
1865
  * Whether a to-one relation can back a relation orderBy hop. `.through()` (junction)
1817
1866
  * relations are excluded — the ordering subquery only joins the direct target, never the
1818
1867
  * junction table. (Relation *filters* do support `.through()` — this guard is orderBy-only.)
@@ -2714,6 +2763,45 @@ const cursorOrderExprs = (table, entries) => {
2714
2763
  return entries.flatMap(([column, direction, nulls]) => orderExpressions(cols[column], direction, nulls ?? void 0, cols[column]));
2715
2764
  };
2716
2765
  /**
2766
+ * Property a row carries an ordering column's full-precision text under, when the column holds
2767
+ * more than a JS Date can. Namespaced like {@link ROW_CURSOR_PROP}, and removed from the row
2768
+ * once its cursor is encoded.
2769
+ */
2770
+ const preciseTextProp = (column) => `__drizzle_graphql_cursor_text_${column}`;
2771
+ /**
2772
+ * Whether a column reads into a JS Date (milliseconds) while the database keeps finer
2773
+ * fractional seconds: a PostgreSQL `timestamp` / `timestamptz` in `mode: 'date'` (precision 6
2774
+ * unless it says otherwise) or a MySQL `datetime` / `timestamp` in `mode: 'date'` with
2775
+ * `fsp` above 3. String-mode columns already arrive as the database's own text, and `time` is
2776
+ * always a string, so neither loses anything.
2777
+ */
2778
+ const outgrowsJsDate = (column) => {
2779
+ if ((0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgTimestamp)) return column.precision === void 0 || column.precision > 3;
2780
+ if ((0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlDateTime) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlTimestamp)) return (column.fsp ?? 0) > 3;
2781
+ return false;
2782
+ };
2783
+ /**
2784
+ * Extra SQL expressions a cursor-computing query selects alongside the row: for each ordering
2785
+ * column whose values a JS Date would truncate, the value as the database's own text. The
2786
+ * cursor then encodes that text rather than the Date, and `after` compares the column against
2787
+ * it — so two rows a microsecond apart stay distinct positions. `table` may be the aliased RQB
2788
+ * proxy. Empty when no ordering column needs it.
2789
+ */
2790
+ const cursorPreciseExtras = (table, entries) => {
2791
+ const cols = (0, drizzle_orm.getColumns)(table);
2792
+ const extras = {};
2793
+ for (const [name] of entries) {
2794
+ const column = cols[name];
2795
+ if (column && outgrowsJsDate(column)) extras[preciseTextProp(name)] = (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgTimestamp) ? drizzle_orm.sql`${column}::text` : drizzle_orm.sql`cast(${column} as char)`;
2796
+ }
2797
+ return extras;
2798
+ };
2799
+ /**
2800
+ * {@link cursorPreciseExtras} aliased under their property names, for a `select({...})` that
2801
+ * may be wrapped in a subquery (where a bare SQL expression would have no name).
2802
+ */
2803
+ const cursorPreciseSelection = (table, entries) => Object.fromEntries(Object.entries(cursorPreciseExtras(table, entries)).map(([prop, expression]) => [prop, expression.as(prop)]));
2804
+ /**
2717
2805
  * Serializes one ordering-tuple value into a JSON-safe shape. Dates and bigints don't survive
2718
2806
  * JSON.stringify losslessly (bigint throws, Date turns into an untagged string), so they are
2719
2807
  * tagged; decimals already arrive from the driver as strings, which are lossless as-is.
@@ -2748,7 +2836,11 @@ const cursorEncoder = (entries) => {
2748
2836
  const spec = JSON.stringify(entries);
2749
2837
  const columns = entries.map(([column]) => column);
2750
2838
  return (row) => {
2751
- const values = JSON.stringify(columns.map((column) => encodeCursorValue(row[column] ?? null)));
2839
+ const values = JSON.stringify(columns.map((column) => {
2840
+ const value = row[column] ?? null;
2841
+ const precise = row[preciseTextProp(column)];
2842
+ return value !== null && typeof precise === "string" ? precise : encodeCursorValue(value);
2843
+ }));
2752
2844
  return Buffer.from(`{"o":${spec},"v":${values}}`, "utf8").toString("base64url");
2753
2845
  };
2754
2846
  };
@@ -2841,7 +2933,11 @@ const selectsCursorField = (info, table) => {
2841
2933
  */
2842
2934
  const attachRowCursors = (rows, entries) => {
2843
2935
  const encode = cursorEncoder(entries);
2844
- for (const row of rows) row[ROW_CURSOR_PROP] = encode(row);
2936
+ const preciseProps = entries.map(([column]) => preciseTextProp(column));
2937
+ for (const row of rows) {
2938
+ row[ROW_CURSOR_PROP] = encode(row);
2939
+ for (const prop of preciseProps) delete row[prop];
2940
+ }
2845
2941
  };
2846
2942
  /** Resolver for the `cursor` meta field: reads the value a list resolver attached, if any. */
2847
2943
  const rowCursorResolver = (source) => source?.[ROW_CURSOR_PROP] ?? null;
@@ -2976,6 +3072,11 @@ const getUniqueColumnSets = (table, getTableConfig) => {
2976
3072
  //#endregion
2977
3073
  //#region src/util/builders/common/distinct.ts
2978
3074
  /**
3075
+ * Alias the parent key comes back under when the pass runs through a junction table. See
3076
+ * `selectDistinctKeys`' `junction` parameter.
3077
+ */
3078
+ const DISTINCT_PARENT = "__dgql_distinct_parent";
3079
+ /**
2979
3080
  * Keeps the first row of each distinct combination of the requested columns, following the
2980
3081
  * query's own ordering, then applies `limit`/`offset` to what survives — and returns the
2981
3082
  * surviving rows' primary key values in that order.
@@ -2988,9 +3089,14 @@ const getUniqueColumnSets = (table, getTableConfig) => {
2988
3089
  * `partitionBy` prepends extra columns to the window's partition. A to-many relation field
2989
3090
  * uses it to pass the foreign key, which makes the pass distinct *within each parent* rather
2990
3091
  * than across the whole batch — the same meaning `distinct` has on a root list.
3092
+ *
3093
+ * `junction` is for a many-to-many relation: the target is joined to the junction `on` the
3094
+ * given condition, and each returned key also carries the parent it was kept for, under
3095
+ * {@link DISTINCT_PARENT}. A target row linked to several parents can be kept for one and
3096
+ * dropped for another, so a key alone cannot narrow such a batch.
2991
3097
  */
2992
3098
  const selectDistinctKeys = async (params) => {
2993
- const { db, table, tableName, distinct, pkNames, where, orderBy, limit, offset, partitionBy } = params;
3099
+ const { db, table, tableName, distinct, pkNames, where, orderBy, limit, offset, partitionBy, junction } = params;
2994
3100
  const cols = (0, drizzle_orm.getColumns)(table);
2995
3101
  if (!pkNames.length) throw drizzleError(`Table ${tableName} has no primary key, so 'distinct' cannot be applied to it.`, { code: "DRIZZLE_INVALID_DISTINCT" });
2996
3102
  const requestedCols = distinct.map((name) => cols[name]).filter(Boolean);
@@ -2999,12 +3105,17 @@ const selectDistinctKeys = async (params) => {
2999
3105
  const orderEntries = orderBy ? orderByEntries(orderBy) : [];
3000
3106
  const windowOrder = [...orderEntries.flatMap(([column, direction, nulls]) => orderExpressions(cols[column], direction, nulls, cols[column])), ...primaryKeyOrderExprs(table, pkNames)];
3001
3107
  const rowNumber = drizzle_orm.sql`row_number() over (partition by ${drizzle_orm.sql.join(partitionCols, drizzle_orm.sql`, `)} order by ${drizzle_orm.sql.join(windowOrder, drizzle_orm.sql`, `)})`.as(DISTINCT_RN);
3002
- const sub = db.select({
3108
+ const selected = db.select({
3003
3109
  ...cols,
3110
+ ...junction ? { [DISTINCT_PARENT]: junction.parentKey } : {},
3004
3111
  [DISTINCT_RN]: rowNumber
3005
- }).from(table).where(where).as("__dgql_distinct");
3112
+ }).from(table);
3113
+ const sub = (junction ? selected.innerJoin(junction.table, junction.on) : selected).where(where).as("__dgql_distinct");
3006
3114
  const outerOrder = [...orderEntries.flatMap(([column, direction, nulls]) => orderExpressions(sub[column], direction, nulls, cols[column])), ...pkNames.filter((name) => sub[name]).map((name) => (0, drizzle_orm.asc)(sub[name]))];
3007
- let query = db.select(Object.fromEntries(pkNames.map((name) => [name, sub[name]]))).from(sub).where((0, drizzle_orm.eq)(sub[DISTINCT_RN], 1)).orderBy(...outerOrder);
3115
+ let query = db.select({
3116
+ ...Object.fromEntries(pkNames.map((name) => [name, sub[name]])),
3117
+ ...junction ? { [DISTINCT_PARENT]: sub[DISTINCT_PARENT] } : {}
3118
+ }).from(sub).where((0, drizzle_orm.eq)(sub[DISTINCT_RN], 1)).orderBy(...outerOrder);
3008
3119
  if (offset) query = query.offset(offset);
3009
3120
  if (limit != null) query = query.limit(limit);
3010
3121
  return await query;
@@ -3184,6 +3295,21 @@ const withDefaultOrderBy = (args, tableName, defaults) => {
3184
3295
  } : args;
3185
3296
  };
3186
3297
  /**
3298
+ * Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
3299
+ * dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
3300
+ * straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
3301
+ * whether or not the table has a limit policy.
3302
+ */
3303
+ const assertPagingNotNegative = (paging, errorCtx) => {
3304
+ for (const name of ["limit", "offset"]) {
3305
+ const value = paging[name];
3306
+ if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
3307
+ code: "DRIZZLE_INVALID_INPUT_VALUE",
3308
+ ...errorCtx
3309
+ });
3310
+ }
3311
+ };
3312
+ /**
3187
3313
  * The `limit` a query actually runs with, given the policy for the table it reads.
3188
3314
  *
3189
3315
  * A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
@@ -3195,6 +3321,7 @@ const withDefaultOrderBy = (args, tableName, defaults) => {
3195
3321
  * fail a client's query over.
3196
3322
  */
3197
3323
  const applyLimitPolicy = (limit, policy, errorCtx) => {
3324
+ assertPagingNotNegative({ limit }, errorCtx);
3198
3325
  if (!policy) return limit ?? void 0;
3199
3326
  const { defaultLimit, maxLimit, clampToMax } = policy;
3200
3327
  if (limit == null) {
@@ -3712,6 +3839,11 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
3712
3839
  thisRecord.columns = columns;
3713
3840
  const relationArgs = (0, drizzle_orm.is)(relEntry.relation, drizzle_orm.One) ? field.args : withDefaultOrderBy(field.args ?? {}, targetTableName, defaultOrderBy);
3714
3841
  const offset = relationArgs?.offset ?? void 0;
3842
+ assertPagingNotNegative({ offset }, {
3843
+ table: targetTableName,
3844
+ operation: "relation",
3845
+ relation: relName
3846
+ });
3715
3847
  const limit = (0, drizzle_orm.is)(relEntry.relation, drizzle_orm.One) ? relationArgs?.limit ?? void 0 : applyLimitPolicy(relationArgs?.limit, limits?.(targetTableName), {
3716
3848
  table: targetTableName,
3717
3849
  operation: "relation",
@@ -3755,6 +3887,10 @@ const pruneNonEagerRelations = (relationMap, shouldEagerLoad) => {
3755
3887
  const runRelationalSelect = async (opts) => {
3756
3888
  const { queryBase, tables, tableName, table, relationMap, typeName, typeNameMapper, parsedInfo, offset, orderBy, where, single, filterCtx, pkNames, after, scope, deleted } = opts;
3757
3889
  const distinct = opts.distinct?.length ? opts.distinct : void 0;
3890
+ assertPagingNotNegative({
3891
+ limit: opts.limit,
3892
+ offset
3893
+ });
3758
3894
  const cursorSelected = !single && isCursorFieldSelected(parsedInfo.fieldsByTypeName[typeName], table);
3759
3895
  let cursorEntries;
3760
3896
  if (!single && (after != null || cursorSelected)) {
@@ -3806,7 +3942,11 @@ const runRelationalSelect = async (opts) => {
3806
3942
  const result = await queryBase.findFirst(omitUndefinedKeys(params));
3807
3943
  return result ? remapToGraphQLSingleOutput(result, tableName, table, relationMap) : void 0;
3808
3944
  }
3809
- if (cursorEntries && cursorSelected) for (const [column] of cursorEntries) params.columns[column] = true;
3945
+ if (cursorEntries && cursorSelected) {
3946
+ for (const [column] of cursorEntries) params.columns[column] = true;
3947
+ const preciseProps = Object.keys(cursorPreciseExtras(table, cursorEntries));
3948
+ if (preciseProps.length) params.extras = Object.fromEntries(preciseProps.map((prop) => [prop, (aliasedTable) => cursorPreciseExtras(aliasedTable, cursorEntries)[prop]]));
3949
+ }
3810
3950
  params.limit = distinctKeys ? void 0 : opts.limit;
3811
3951
  const result = await queryBase.findMany(omitUndefinedKeys(params));
3812
3952
  if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
@@ -4625,16 +4765,17 @@ const identifyRows = (field, args) => {
4625
4765
  * Window functions require PostgreSQL, MySQL >= 8.0, or SQLite >= 3.25.
4626
4766
  * Returns raw rows (NOT remapped); the caller groups + remaps them.
4627
4767
  */
4628
- const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereCondition, orderByArg, limit, offset, pkNames, columns, orderCtx, whereArgs) => {
4768
+ const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereCondition, orderByArg, limit, offset, pkNames, columns, orderCtx, whereArgs, junction) => {
4629
4769
  const cols = columns ?? (0, drizzle_orm.getColumns)(targetTable);
4630
4770
  const orderExprs = [...orderByArg ? extractOrderBy(targetTable, orderByArg, orderCtx, whereArgs) : [], ...primaryKeyOrderExprs(targetTable, pkNames)];
4631
4771
  const orderClause = orderExprs.length ? drizzle_orm.sql` order by ${drizzle_orm.sql.join(orderExprs, drizzle_orm.sql`, `)}` : drizzle_orm.sql``;
4632
4772
  const RN = "__drizzle_graphql_rn";
4633
- const rowNumber = drizzle_orm.sql`row_number() over (partition by ${foreignCol}${orderClause})`.as(RN);
4634
- const sub = db.select({
4773
+ const partitionCol = junction ? junction.sourceCol : foreignCol;
4774
+ const rowNumber = drizzle_orm.sql`row_number() over (partition by ${partitionCol}${orderClause})`.as(RN);
4775
+ const sub = withJunction(db.select({
4635
4776
  ...cols,
4636
4777
  [RN]: rowNumber
4637
- }).from(targetTable).where(whereCondition).as("__paginated");
4778
+ }).from(targetTable), junction, foreignCol).where(whereCondition).as("__paginated");
4638
4779
  const lower = offset ?? 0;
4639
4780
  const windowConds = [(0, drizzle_orm.gt)(sub[RN], lower)];
4640
4781
  if (limit != null) windowConds.push((0, drizzle_orm.lte)(sub[RN], lower + limit));
@@ -4643,6 +4784,14 @@ const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereC
4643
4784
  return rows;
4644
4785
  };
4645
4786
  /**
4787
+ * Alias the parent key is selected under when a relation goes through a junction table. The
4788
+ * key lives on the junction, not the target, so it has to be selected explicitly to group the
4789
+ * rows by parent — and a target row linked to several parents comes back once per parent.
4790
+ */
4791
+ const PARENT_KEY = "__drizzle_graphql_parent";
4792
+ /** `from(target)`, joined to the junction table when the relation has one. */
4793
+ const withJunction = (query, junction, foreignCol) => junction ? query.innerJoin(junction.table, (0, drizzle_orm.eq)(junction.targetCol, foreignCol)) : query;
4794
+ /**
4646
4795
  * Creates a RelationResolverFactory that generates field-level resolvers for each relation.
4647
4796
  * Each resolver:
4648
4797
  * 1. Returns pre-fetched data if the parent resolver already included it (eager path, zero cost).
@@ -4664,6 +4813,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4664
4813
  const joinCols = extractRelationJoinColumns(relEntry, parentTable, targetTable);
4665
4814
  if (!joinCols) return;
4666
4815
  const { localColPropName, foreignCol, foreignColPropName } = joinCols;
4816
+ const junction = extractJunctionColumns(relEntry);
4817
+ const groupCol = junction ? junction.sourceCol : foreignCol;
4818
+ const groupKey = junction ? PARENT_KEY : foreignColPropName;
4819
+ const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, (0, drizzle_orm.getColumns)(parentTable)[localColPropName], groupCol);
4667
4820
  const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, isOne && relEntry.relation?.optional === false);
4668
4821
  const limitPolicy = isOne ? void 0 : limits?.(targetTableName);
4669
4822
  const targetPkNames = relEntry.targetPkNames ?? [];
@@ -4697,7 +4850,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4697
4850
  const column = targetColumns[columnName];
4698
4851
  if (column) columns[columnName] = column;
4699
4852
  }
4700
- return columns;
4853
+ return cursorEntries ? {
4854
+ ...columns,
4855
+ ...cursorPreciseSelection(targetTable, cursorEntries)
4856
+ } : columns;
4701
4857
  };
4702
4858
  const resolve = async (parent, args, context, info) => {
4703
4859
  if (parent[relationName] !== void 0) return parent[relationName];
@@ -4707,6 +4863,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4707
4863
  const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `relation:${tableName}::${relationName}`, () => {
4708
4864
  const orderByArg = isOne ? args?.orderBy : withDefaultOrderBy(args ?? {}, targetTableName, policies?.defaultOrderBy).orderBy;
4709
4865
  const limit = applyLimitPolicy(requestedLimit, limitPolicy, errorCtx);
4866
+ assertPagingNotNegative({ offset }, errorCtx);
4710
4867
  const distinct = (args ?? {}).distinct?.length ? (args ?? {}).distinct : void 0;
4711
4868
  const cursorSelected = !isOne && !!info && selectsCursorField(info, targetTable);
4712
4869
  let cursorEntries;
@@ -4741,11 +4898,12 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4741
4898
  loaderKey: `${tableName}::${relationName}::${argsKey}`
4742
4899
  };
4743
4900
  });
4901
+ 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" });
4744
4902
  return getOrCreateLoader(context, loaderKey, async (parentIds) => {
4745
4903
  const executor = resolveExecutor(db, context);
4746
4904
  const uniqueIds = [...new Set(parentIds)];
4747
4905
  const scope = resolveScope(policies, context, filterCtx);
4748
- let whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(foreignCol, uniqueIds), whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
4906
+ let whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
4749
4907
  if (distinct) {
4750
4908
  const keys = await selectDistinctKeys({
4751
4909
  db: executor,
@@ -4755,27 +4913,41 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4755
4913
  pkNames: targetPkNames,
4756
4914
  where: whereCondition,
4757
4915
  orderBy: orderByArg,
4758
- partitionBy: [foreignCol]
4916
+ partitionBy: [groupCol],
4917
+ junction: junction ? {
4918
+ table: junction.table,
4919
+ on: (0, drizzle_orm.eq)(junction.targetCol, foreignCol),
4920
+ parentKey: groupCol
4921
+ } : void 0
4759
4922
  });
4760
4923
  if (!keys.length) return parentIds.map(() => isOne ? null : []);
4761
- whereCondition = (0, drizzle_orm.and)(whereCondition, primaryKeyRestriction(targetTable, targetPkNames, keys));
4924
+ whereCondition = (0, drizzle_orm.and)(whereCondition, junction ? (0, drizzle_orm.or)(...keys.map((key) => (0, drizzle_orm.and)((0, drizzle_orm.eq)(groupCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
4762
4925
  }
4926
+ const selection = junction ? {
4927
+ ...columns ?? (0, drizzle_orm.getColumns)(targetTable),
4928
+ [PARENT_KEY]: groupCol
4929
+ } : columns;
4763
4930
  let rows;
4764
- if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, columns, relationFilterCtx(filterCtx, targetTableName), whereArg);
4931
+ if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName), whereArg, junction);
4765
4932
  else {
4766
- let q = (columns ? executor.select(columns) : executor.select()).from(targetTable).where(whereCondition);
4933
+ let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
4767
4934
  const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName), whereArg) : [];
4768
4935
  if (orderExprs.length) q = q.orderBy(...orderExprs);
4769
4936
  rows = await q;
4770
4937
  }
4771
4938
  if (cursorEntries) attachRowCursors(rows, cursorEntries);
4939
+ const keyOf = (row) => {
4940
+ const key = String(row[groupKey]);
4941
+ if (junction) delete row[PARENT_KEY];
4942
+ return key;
4943
+ };
4772
4944
  if (isOne) {
4773
- const byKey = new Map(rows.map((row) => [String(row[foreignColPropName]), row]));
4945
+ const byKey = new Map(rows.map((row) => [keyOf(row), row]));
4774
4946
  remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
4775
4947
  return parentIds.map((id) => byKey.get(String(id)) ?? null);
4776
4948
  }
4777
4949
  const grouped = new Map(uniqueIds.map((id) => [String(id), []]));
4778
- for (const row of rows) grouped.get(String(row[foreignColPropName]))?.push(row);
4950
+ for (const row of rows) grouped.get(keyOf(row))?.push(row);
4779
4951
  remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
4780
4952
  return parentIds.map((id) => grouped.get(String(id)) ?? []);
4781
4953
  }).load(localValue);
@@ -5110,7 +5282,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
5110
5282
  });
5111
5283
  const shell = new graphql.GraphQLObjectType({
5112
5284
  name: typeName,
5113
- description: cacheCtx.docs.describeTable?.(tableName),
5285
+ description: cacheCtx.docs.describeTable?.(tableName, table),
5114
5286
  fields: () => ({
5115
5287
  ...tableFields,
5116
5288
  ...container.fields
@@ -5144,7 +5316,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
5144
5316
  defaultName: resolveTypeName(targetTableName, typeNameMapper),
5145
5317
  table: targetTableName
5146
5318
  }),
5147
- description: cacheCtx.docs.describeTable?.(targetTableName),
5319
+ description: cacheCtx.docs.describeTable?.(targetTableName, targetTable),
5148
5320
  fields: () => ({
5149
5321
  ...targetTableFields,
5150
5322
  ...capturedTargetContainer.fields
@@ -5295,7 +5467,7 @@ const generateTableTypes = (tableName, tables, relationMap, withReturning, relat
5295
5467
  defaultName: resolveTypeName(tableName, typeNameMapper),
5296
5468
  table: tableName
5297
5469
  }),
5298
- description: cacheCtx.docs.describeTable?.(tableName),
5470
+ description: cacheCtx.docs.describeTable?.(tableName, tables[tableName]),
5299
5471
  fields: {
5300
5472
  ...tableFields,
5301
5473
  ...relationFields
@@ -5862,6 +6034,10 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
5862
6034
  return;
5863
6035
  }
5864
6036
  const { localColPropName, foreignCol } = joinCols;
6037
+ const junction = extractJunctionColumns(relEntry);
6038
+ if (junction === null) return;
6039
+ const groupCol = junction ? junction.sourceCol : foreignCol;
6040
+ const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, (0, drizzle_orm.getColumns)(parentTable)[localColPropName], groupCol);
5865
6041
  const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
5866
6042
  const targetTypeName = resolveTypeName(targetTableName, typeNameMapper);
5867
6043
  const type = generateAggregateTypes(targetTable, targetTableName, targetTypeName, cacheCtx);
@@ -5892,11 +6068,12 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
5892
6068
  const loader = getOrCreateLoader(context, loaderKey, async (parentIds) => {
5893
6069
  const executor = resolveExecutor(db, context);
5894
6070
  const uniqueIds = [...new Set(parentIds)];
5895
- const whereCondition = withScope(resolveScope(policies, context, filterCtx), targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(foreignCol, uniqueIds), whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName)) : void 0), deleted ?? defaultDeleted);
5896
- const rows = await executor.select({
5897
- [GROUP_KEY]: foreignCol,
6071
+ const whereCondition = withScope(resolveScope(policies, context, filterCtx), targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName)) : void 0), deleted ?? defaultDeleted);
6072
+ const selected = executor.select({
6073
+ [GROUP_KEY]: groupCol,
5898
6074
  ...request.selection
5899
- }).from(targetTable).where(whereCondition).groupBy(foreignCol);
6075
+ }).from(targetTable);
6076
+ const rows = await (junction ? selected.innerJoin(junction.table, (0, drizzle_orm.eq)(junction.targetCol, foreignCol)) : selected).where(whereCondition).groupBy(groupCol);
5900
6077
  const byKey = new Map(rows.map((row) => [row[GROUP_KEY], row]));
5901
6078
  return parentIds.map((id) => byKey.get(id) ?? {});
5902
6079
  });