@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/README.md +117 -68
- package/dist/index.cjs +204 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -4
- package/dist/index.d.ts +20 -4
- package/dist/index.js +204 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -449,7 +449,7 @@ type DrizzleErrorCode =
|
|
|
449
449
|
'DRIZZLE_INVALID_DISTINCT' |
|
|
450
450
|
/** An `onConflict` named a target or update column the upsert cannot use. */
|
|
451
451
|
'DRIZZLE_INVALID_ON_CONFLICT' |
|
|
452
|
-
/** A written value could not be converted to what its column stores. */
|
|
452
|
+
/** A written value could not be converted to what its column stores, or a `limit` / `offset` was negative. */
|
|
453
453
|
'DRIZZLE_INVALID_INPUT_VALUE' |
|
|
454
454
|
/** An update's `set` gave a column that cannot hold null an explicit null. */
|
|
455
455
|
'DRIZZLE_NOT_NULL' |
|
|
@@ -458,7 +458,9 @@ type DrizzleErrorCode =
|
|
|
458
458
|
/** A stored value could not be represented by the scalar that transports it. */
|
|
459
459
|
'DRIZZLE_UNREPRESENTABLE_VALUE' |
|
|
460
460
|
/** The request's shared mutation transaction was rolled back, so this field never ran. */
|
|
461
|
-
'DRIZZLE_TRANSACTION_ABORTED'
|
|
461
|
+
'DRIZZLE_TRANSACTION_ABORTED' |
|
|
462
|
+
/** A relation was read on a path it cannot be loaded through (see the message). */
|
|
463
|
+
'DRIZZLE_UNSUPPORTED_RELATION';
|
|
462
464
|
/**
|
|
463
465
|
* What an error was about, published as `extensions.drizzle` beside the code — the runtime
|
|
464
466
|
* half of the `extensions.drizzle` markers a generated field carries.
|
|
@@ -607,8 +609,11 @@ type ColumnDescriber = (column: Column, info: ColumnDocInfo) => string | undefin
|
|
|
607
609
|
* `BuildSchemaConfig.deprecateColumn`.
|
|
608
610
|
*/
|
|
609
611
|
type ColumnDeprecator = (column: Column, info: ColumnDocInfo) => string | undefined;
|
|
610
|
-
/**
|
|
611
|
-
|
|
612
|
+
/**
|
|
613
|
+
* Supplies the GraphQL `description` for a table's object type. Receives the Drizzle table
|
|
614
|
+
* itself as well as its schema key. See `BuildSchemaConfig.describeTable`.
|
|
615
|
+
*/
|
|
616
|
+
type TableDescriber = (tableName: string, table: Table) => string | undefined;
|
|
612
617
|
/**
|
|
613
618
|
* Supplies the GraphQL `description` for a relation field. See
|
|
614
619
|
* `BuildSchemaConfig.describeRelation`.
|
|
@@ -2444,6 +2449,17 @@ type BuildSchemaConfig = {
|
|
|
2444
2449
|
* ```ts
|
|
2445
2450
|
* buildSchema(db, { describeTable: (tableName) => tableDocs[tableName] });
|
|
2446
2451
|
* ```
|
|
2452
|
+
*
|
|
2453
|
+
* The second argument is the Drizzle table itself, so documentation attached to the table
|
|
2454
|
+
* where it is declared can be read straight off it:
|
|
2455
|
+
*
|
|
2456
|
+
* ```ts
|
|
2457
|
+
* const tableDocs = new WeakMap<object, string>();
|
|
2458
|
+
* export const Users = pgTable('users', { ... });
|
|
2459
|
+
* tableDocs.set(Users, 'A person who can sign in.');
|
|
2460
|
+
*
|
|
2461
|
+
* buildSchema(db, { describeTable: (_tableName, table) => tableDocs.get(table) });
|
|
2462
|
+
* ```
|
|
2447
2463
|
*/
|
|
2448
2464
|
describeTable?: TableDescriber;
|
|
2449
2465
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -449,7 +449,7 @@ type DrizzleErrorCode =
|
|
|
449
449
|
'DRIZZLE_INVALID_DISTINCT' |
|
|
450
450
|
/** An `onConflict` named a target or update column the upsert cannot use. */
|
|
451
451
|
'DRIZZLE_INVALID_ON_CONFLICT' |
|
|
452
|
-
/** A written value could not be converted to what its column stores. */
|
|
452
|
+
/** A written value could not be converted to what its column stores, or a `limit` / `offset` was negative. */
|
|
453
453
|
'DRIZZLE_INVALID_INPUT_VALUE' |
|
|
454
454
|
/** An update's `set` gave a column that cannot hold null an explicit null. */
|
|
455
455
|
'DRIZZLE_NOT_NULL' |
|
|
@@ -458,7 +458,9 @@ type DrizzleErrorCode =
|
|
|
458
458
|
/** A stored value could not be represented by the scalar that transports it. */
|
|
459
459
|
'DRIZZLE_UNREPRESENTABLE_VALUE' |
|
|
460
460
|
/** The request's shared mutation transaction was rolled back, so this field never ran. */
|
|
461
|
-
'DRIZZLE_TRANSACTION_ABORTED'
|
|
461
|
+
'DRIZZLE_TRANSACTION_ABORTED' |
|
|
462
|
+
/** A relation was read on a path it cannot be loaded through (see the message). */
|
|
463
|
+
'DRIZZLE_UNSUPPORTED_RELATION';
|
|
462
464
|
/**
|
|
463
465
|
* What an error was about, published as `extensions.drizzle` beside the code — the runtime
|
|
464
466
|
* half of the `extensions.drizzle` markers a generated field carries.
|
|
@@ -607,8 +609,11 @@ type ColumnDescriber = (column: Column, info: ColumnDocInfo) => string | undefin
|
|
|
607
609
|
* `BuildSchemaConfig.deprecateColumn`.
|
|
608
610
|
*/
|
|
609
611
|
type ColumnDeprecator = (column: Column, info: ColumnDocInfo) => string | undefined;
|
|
610
|
-
/**
|
|
611
|
-
|
|
612
|
+
/**
|
|
613
|
+
* Supplies the GraphQL `description` for a table's object type. Receives the Drizzle table
|
|
614
|
+
* itself as well as its schema key. See `BuildSchemaConfig.describeTable`.
|
|
615
|
+
*/
|
|
616
|
+
type TableDescriber = (tableName: string, table: Table) => string | undefined;
|
|
612
617
|
/**
|
|
613
618
|
* Supplies the GraphQL `description` for a relation field. See
|
|
614
619
|
* `BuildSchemaConfig.describeRelation`.
|
|
@@ -2444,6 +2449,17 @@ type BuildSchemaConfig = {
|
|
|
2444
2449
|
* ```ts
|
|
2445
2450
|
* buildSchema(db, { describeTable: (tableName) => tableDocs[tableName] });
|
|
2446
2451
|
* ```
|
|
2452
|
+
*
|
|
2453
|
+
* The second argument is the Drizzle table itself, so documentation attached to the table
|
|
2454
|
+
* where it is declared can be read straight off it:
|
|
2455
|
+
*
|
|
2456
|
+
* ```ts
|
|
2457
|
+
* const tableDocs = new WeakMap<object, string>();
|
|
2458
|
+
* export const Users = pgTable('users', { ... });
|
|
2459
|
+
* tableDocs.set(Users, 'A person who can sign in.');
|
|
2460
|
+
*
|
|
2461
|
+
* buildSchema(db, { describeTable: (_tableName, table) => tableDocs.get(table) });
|
|
2462
|
+
* ```
|
|
2447
2463
|
*/
|
|
2448
2464
|
describeTable?: TableDescriber;
|
|
2449
2465
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1592,13 +1592,20 @@ const nullableOps = {
|
|
|
1592
1592
|
isNull,
|
|
1593
1593
|
isNotNull
|
|
1594
1594
|
};
|
|
1595
|
+
/** Operators whose operand is an on/off flag rather than a value: `false` adds no condition. */
|
|
1596
|
+
const FLAG_OPERATORS = /* @__PURE__ */ new Set([
|
|
1597
|
+
"isNull",
|
|
1598
|
+
"isNotNull",
|
|
1599
|
+
"isEmpty"
|
|
1600
|
+
]);
|
|
1595
1601
|
const extractFiltersColumn = (column, columnName, operators) => {
|
|
1596
1602
|
const { OR, AND, NOT, insensitive, ...restOperators } = operators;
|
|
1597
1603
|
const foldCase = insensitive === true;
|
|
1598
1604
|
const entries = Object.entries(restOperators);
|
|
1599
1605
|
const variants = [];
|
|
1600
1606
|
for (const [operatorName, operatorValue] of entries) {
|
|
1601
|
-
if (operatorValue === null || operatorValue ===
|
|
1607
|
+
if (operatorValue === null || operatorValue === void 0) continue;
|
|
1608
|
+
if (operatorValue === false && FLAG_OPERATORS.has(operatorName)) continue;
|
|
1602
1609
|
if (operatorName in singleValueOps) {
|
|
1603
1610
|
const singleValue = remapFromGraphQLCore(operatorValue, column, columnName);
|
|
1604
1611
|
variants.push(foldCase ? lowerComparison(column, operatorName, singleValue) : singleValueOps[operatorName](column, singleValue));
|
|
@@ -1789,6 +1796,48 @@ const extractRelationJoinColumns = (relEntry, parentTable, targetTable) => {
|
|
|
1789
1796
|
};
|
|
1790
1797
|
};
|
|
1791
1798
|
/**
|
|
1799
|
+
* The junction table and its two columns for a `.through()` (many-to-many) relation:
|
|
1800
|
+
* `sourceCol` holds the parent's key, `targetCol` the target's. `undefined` for a relation
|
|
1801
|
+
* with no junction; `null` for one whose junction cannot be used here — more than one column
|
|
1802
|
+
* on either side, or columns that are not on the junction table.
|
|
1803
|
+
*/
|
|
1804
|
+
const extractJunctionColumns = (relEntry) => {
|
|
1805
|
+
const rel = relEntry.relation ?? relEntry;
|
|
1806
|
+
const junction = rel?.throughTable;
|
|
1807
|
+
if (!junction) return;
|
|
1808
|
+
const sourceEntries = rel.through?.source;
|
|
1809
|
+
const targetEntries = rel.through?.target;
|
|
1810
|
+
if (sourceEntries?.length !== 1 || targetEntries?.length !== 1) return null;
|
|
1811
|
+
const junctionCols = Object.values(getColumns(junction));
|
|
1812
|
+
const sourceCol = unwrapRelationColumn(sourceEntries[0]);
|
|
1813
|
+
const targetCol = unwrapRelationColumn(targetEntries[0]);
|
|
1814
|
+
if (!sourceCol || !targetCol || !junctionCols.includes(sourceCol) || !junctionCols.includes(targetCol)) return null;
|
|
1815
|
+
return {
|
|
1816
|
+
table: junction,
|
|
1817
|
+
sourceCol,
|
|
1818
|
+
targetCol
|
|
1819
|
+
};
|
|
1820
|
+
};
|
|
1821
|
+
/**
|
|
1822
|
+
* The relation's own declared `where` as a condition on a query that reads the target table,
|
|
1823
|
+
* or `undefined` when it has none. The eager path gets this from drizzle's `with:`; the batched
|
|
1824
|
+
* field resolver and `<relation>Aggregate` read the target table directly, so they have to add
|
|
1825
|
+
* it themselves or they return rows the relation hides.
|
|
1826
|
+
*
|
|
1827
|
+
* A reversed filter (inherited from the relation on the other side) is written against the
|
|
1828
|
+
* parent table, so it is applied as "the row's parent key belongs to a parent that passes it".
|
|
1829
|
+
* `parentKeyCol` is the parent's join column and `groupCol` the column in the query that holds
|
|
1830
|
+
* the parent key for each row — the target's foreign key, or the junction's source column.
|
|
1831
|
+
*/
|
|
1832
|
+
const relationOwnCondition = (relEntry, parentTable, targetTable, parentKeyCol, groupCol) => {
|
|
1833
|
+
const rel = relEntry.relation ?? relEntry;
|
|
1834
|
+
const ownWhere = relationOwnWhere(rel);
|
|
1835
|
+
if (!ownWhere) return;
|
|
1836
|
+
if (!isRelationFilterReversed(rel)) return relationsFilterToSQL(targetTable, ownWhere);
|
|
1837
|
+
const parentFilter = relationsFilterToSQL(parentTable, ownWhere);
|
|
1838
|
+
return sql`${groupCol} in (select ${parentKeyCol} from ${parentTable} where ${parentFilter})`;
|
|
1839
|
+
};
|
|
1840
|
+
/**
|
|
1792
1841
|
* Whether a to-one relation can back a relation orderBy hop. `.through()` (junction)
|
|
1793
1842
|
* relations are excluded — the ordering subquery only joins the direct target, never the
|
|
1794
1843
|
* junction table. (Relation *filters* do support `.through()` — this guard is orderBy-only.)
|
|
@@ -2690,6 +2739,45 @@ const cursorOrderExprs = (table, entries) => {
|
|
|
2690
2739
|
return entries.flatMap(([column, direction, nulls]) => orderExpressions(cols[column], direction, nulls ?? void 0, cols[column]));
|
|
2691
2740
|
};
|
|
2692
2741
|
/**
|
|
2742
|
+
* Property a row carries an ordering column's full-precision text under, when the column holds
|
|
2743
|
+
* more than a JS Date can. Namespaced like {@link ROW_CURSOR_PROP}, and removed from the row
|
|
2744
|
+
* once its cursor is encoded.
|
|
2745
|
+
*/
|
|
2746
|
+
const preciseTextProp = (column) => `__drizzle_graphql_cursor_text_${column}`;
|
|
2747
|
+
/**
|
|
2748
|
+
* Whether a column reads into a JS Date (milliseconds) while the database keeps finer
|
|
2749
|
+
* fractional seconds: a PostgreSQL `timestamp` / `timestamptz` in `mode: 'date'` (precision 6
|
|
2750
|
+
* unless it says otherwise) or a MySQL `datetime` / `timestamp` in `mode: 'date'` with
|
|
2751
|
+
* `fsp` above 3. String-mode columns already arrive as the database's own text, and `time` is
|
|
2752
|
+
* always a string, so neither loses anything.
|
|
2753
|
+
*/
|
|
2754
|
+
const outgrowsJsDate = (column) => {
|
|
2755
|
+
if (is(column, PgTimestamp)) return column.precision === void 0 || column.precision > 3;
|
|
2756
|
+
if (is(column, MySqlDateTime) || is(column, MySqlTimestamp)) return (column.fsp ?? 0) > 3;
|
|
2757
|
+
return false;
|
|
2758
|
+
};
|
|
2759
|
+
/**
|
|
2760
|
+
* Extra SQL expressions a cursor-computing query selects alongside the row: for each ordering
|
|
2761
|
+
* column whose values a JS Date would truncate, the value as the database's own text. The
|
|
2762
|
+
* cursor then encodes that text rather than the Date, and `after` compares the column against
|
|
2763
|
+
* it — so two rows a microsecond apart stay distinct positions. `table` may be the aliased RQB
|
|
2764
|
+
* proxy. Empty when no ordering column needs it.
|
|
2765
|
+
*/
|
|
2766
|
+
const cursorPreciseExtras = (table, entries) => {
|
|
2767
|
+
const cols = getColumns(table);
|
|
2768
|
+
const extras = {};
|
|
2769
|
+
for (const [name] of entries) {
|
|
2770
|
+
const column = cols[name];
|
|
2771
|
+
if (column && outgrowsJsDate(column)) extras[preciseTextProp(name)] = is(column, PgTimestamp) ? sql`${column}::text` : sql`cast(${column} as char)`;
|
|
2772
|
+
}
|
|
2773
|
+
return extras;
|
|
2774
|
+
};
|
|
2775
|
+
/**
|
|
2776
|
+
* {@link cursorPreciseExtras} aliased under their property names, for a `select({...})` that
|
|
2777
|
+
* may be wrapped in a subquery (where a bare SQL expression would have no name).
|
|
2778
|
+
*/
|
|
2779
|
+
const cursorPreciseSelection = (table, entries) => Object.fromEntries(Object.entries(cursorPreciseExtras(table, entries)).map(([prop, expression]) => [prop, expression.as(prop)]));
|
|
2780
|
+
/**
|
|
2693
2781
|
* Serializes one ordering-tuple value into a JSON-safe shape. Dates and bigints don't survive
|
|
2694
2782
|
* JSON.stringify losslessly (bigint throws, Date turns into an untagged string), so they are
|
|
2695
2783
|
* tagged; decimals already arrive from the driver as strings, which are lossless as-is.
|
|
@@ -2724,7 +2812,11 @@ const cursorEncoder = (entries) => {
|
|
|
2724
2812
|
const spec = JSON.stringify(entries);
|
|
2725
2813
|
const columns = entries.map(([column]) => column);
|
|
2726
2814
|
return (row) => {
|
|
2727
|
-
const values = JSON.stringify(columns.map((column) =>
|
|
2815
|
+
const values = JSON.stringify(columns.map((column) => {
|
|
2816
|
+
const value = row[column] ?? null;
|
|
2817
|
+
const precise = row[preciseTextProp(column)];
|
|
2818
|
+
return value !== null && typeof precise === "string" ? precise : encodeCursorValue(value);
|
|
2819
|
+
}));
|
|
2728
2820
|
return Buffer.from(`{"o":${spec},"v":${values}}`, "utf8").toString("base64url");
|
|
2729
2821
|
};
|
|
2730
2822
|
};
|
|
@@ -2817,7 +2909,11 @@ const selectsCursorField = (info, table) => {
|
|
|
2817
2909
|
*/
|
|
2818
2910
|
const attachRowCursors = (rows, entries) => {
|
|
2819
2911
|
const encode = cursorEncoder(entries);
|
|
2820
|
-
|
|
2912
|
+
const preciseProps = entries.map(([column]) => preciseTextProp(column));
|
|
2913
|
+
for (const row of rows) {
|
|
2914
|
+
row[ROW_CURSOR_PROP] = encode(row);
|
|
2915
|
+
for (const prop of preciseProps) delete row[prop];
|
|
2916
|
+
}
|
|
2821
2917
|
};
|
|
2822
2918
|
/** Resolver for the `cursor` meta field: reads the value a list resolver attached, if any. */
|
|
2823
2919
|
const rowCursorResolver = (source) => source?.[ROW_CURSOR_PROP] ?? null;
|
|
@@ -2952,6 +3048,11 @@ const getUniqueColumnSets = (table, getTableConfig) => {
|
|
|
2952
3048
|
//#endregion
|
|
2953
3049
|
//#region src/util/builders/common/distinct.ts
|
|
2954
3050
|
/**
|
|
3051
|
+
* Alias the parent key comes back under when the pass runs through a junction table. See
|
|
3052
|
+
* `selectDistinctKeys`' `junction` parameter.
|
|
3053
|
+
*/
|
|
3054
|
+
const DISTINCT_PARENT = "__dgql_distinct_parent";
|
|
3055
|
+
/**
|
|
2955
3056
|
* Keeps the first row of each distinct combination of the requested columns, following the
|
|
2956
3057
|
* query's own ordering, then applies `limit`/`offset` to what survives — and returns the
|
|
2957
3058
|
* surviving rows' primary key values in that order.
|
|
@@ -2964,9 +3065,14 @@ const getUniqueColumnSets = (table, getTableConfig) => {
|
|
|
2964
3065
|
* `partitionBy` prepends extra columns to the window's partition. A to-many relation field
|
|
2965
3066
|
* uses it to pass the foreign key, which makes the pass distinct *within each parent* rather
|
|
2966
3067
|
* than across the whole batch — the same meaning `distinct` has on a root list.
|
|
3068
|
+
*
|
|
3069
|
+
* `junction` is for a many-to-many relation: the target is joined to the junction `on` the
|
|
3070
|
+
* given condition, and each returned key also carries the parent it was kept for, under
|
|
3071
|
+
* {@link DISTINCT_PARENT}. A target row linked to several parents can be kept for one and
|
|
3072
|
+
* dropped for another, so a key alone cannot narrow such a batch.
|
|
2967
3073
|
*/
|
|
2968
3074
|
const selectDistinctKeys = async (params) => {
|
|
2969
|
-
const { db, table, tableName, distinct, pkNames, where, orderBy, limit, offset, partitionBy } = params;
|
|
3075
|
+
const { db, table, tableName, distinct, pkNames, where, orderBy, limit, offset, partitionBy, junction } = params;
|
|
2970
3076
|
const cols = getColumns(table);
|
|
2971
3077
|
if (!pkNames.length) throw drizzleError(`Table ${tableName} has no primary key, so 'distinct' cannot be applied to it.`, { code: "DRIZZLE_INVALID_DISTINCT" });
|
|
2972
3078
|
const requestedCols = distinct.map((name) => cols[name]).filter(Boolean);
|
|
@@ -2975,12 +3081,17 @@ const selectDistinctKeys = async (params) => {
|
|
|
2975
3081
|
const orderEntries = orderBy ? orderByEntries(orderBy) : [];
|
|
2976
3082
|
const windowOrder = [...orderEntries.flatMap(([column, direction, nulls]) => orderExpressions(cols[column], direction, nulls, cols[column])), ...primaryKeyOrderExprs(table, pkNames)];
|
|
2977
3083
|
const rowNumber = sql`row_number() over (partition by ${sql.join(partitionCols, sql`, `)} order by ${sql.join(windowOrder, sql`, `)})`.as(DISTINCT_RN);
|
|
2978
|
-
const
|
|
3084
|
+
const selected = db.select({
|
|
2979
3085
|
...cols,
|
|
3086
|
+
...junction ? { [DISTINCT_PARENT]: junction.parentKey } : {},
|
|
2980
3087
|
[DISTINCT_RN]: rowNumber
|
|
2981
|
-
}).from(table)
|
|
3088
|
+
}).from(table);
|
|
3089
|
+
const sub = (junction ? selected.innerJoin(junction.table, junction.on) : selected).where(where).as("__dgql_distinct");
|
|
2982
3090
|
const outerOrder = [...orderEntries.flatMap(([column, direction, nulls]) => orderExpressions(sub[column], direction, nulls, cols[column])), ...pkNames.filter((name) => sub[name]).map((name) => asc(sub[name]))];
|
|
2983
|
-
let query = db.select(
|
|
3091
|
+
let query = db.select({
|
|
3092
|
+
...Object.fromEntries(pkNames.map((name) => [name, sub[name]])),
|
|
3093
|
+
...junction ? { [DISTINCT_PARENT]: sub[DISTINCT_PARENT] } : {}
|
|
3094
|
+
}).from(sub).where(eq(sub[DISTINCT_RN], 1)).orderBy(...outerOrder);
|
|
2984
3095
|
if (offset) query = query.offset(offset);
|
|
2985
3096
|
if (limit != null) query = query.limit(limit);
|
|
2986
3097
|
return await query;
|
|
@@ -3160,6 +3271,21 @@ const withDefaultOrderBy = (args, tableName, defaults) => {
|
|
|
3160
3271
|
} : args;
|
|
3161
3272
|
};
|
|
3162
3273
|
/**
|
|
3274
|
+
* Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
|
|
3275
|
+
* dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
|
|
3276
|
+
* straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
|
|
3277
|
+
* whether or not the table has a limit policy.
|
|
3278
|
+
*/
|
|
3279
|
+
const assertPagingNotNegative = (paging, errorCtx) => {
|
|
3280
|
+
for (const name of ["limit", "offset"]) {
|
|
3281
|
+
const value = paging[name];
|
|
3282
|
+
if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
|
|
3283
|
+
code: "DRIZZLE_INVALID_INPUT_VALUE",
|
|
3284
|
+
...errorCtx
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
};
|
|
3288
|
+
/**
|
|
3163
3289
|
* The `limit` a query actually runs with, given the policy for the table it reads.
|
|
3164
3290
|
*
|
|
3165
3291
|
* A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
|
|
@@ -3171,6 +3297,7 @@ const withDefaultOrderBy = (args, tableName, defaults) => {
|
|
|
3171
3297
|
* fail a client's query over.
|
|
3172
3298
|
*/
|
|
3173
3299
|
const applyLimitPolicy = (limit, policy, errorCtx) => {
|
|
3300
|
+
assertPagingNotNegative({ limit }, errorCtx);
|
|
3174
3301
|
if (!policy) return limit ?? void 0;
|
|
3175
3302
|
const { defaultLimit, maxLimit, clampToMax } = policy;
|
|
3176
3303
|
if (limit == null) {
|
|
@@ -3688,6 +3815,11 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
|
|
|
3688
3815
|
thisRecord.columns = columns;
|
|
3689
3816
|
const relationArgs = is(relEntry.relation, One) ? field.args : withDefaultOrderBy(field.args ?? {}, targetTableName, defaultOrderBy);
|
|
3690
3817
|
const offset = relationArgs?.offset ?? void 0;
|
|
3818
|
+
assertPagingNotNegative({ offset }, {
|
|
3819
|
+
table: targetTableName,
|
|
3820
|
+
operation: "relation",
|
|
3821
|
+
relation: relName
|
|
3822
|
+
});
|
|
3691
3823
|
const limit = is(relEntry.relation, One) ? relationArgs?.limit ?? void 0 : applyLimitPolicy(relationArgs?.limit, limits?.(targetTableName), {
|
|
3692
3824
|
table: targetTableName,
|
|
3693
3825
|
operation: "relation",
|
|
@@ -3731,6 +3863,10 @@ const pruneNonEagerRelations = (relationMap, shouldEagerLoad) => {
|
|
|
3731
3863
|
const runRelationalSelect = async (opts) => {
|
|
3732
3864
|
const { queryBase, tables, tableName, table, relationMap, typeName, typeNameMapper, parsedInfo, offset, orderBy, where, single, filterCtx, pkNames, after, scope, deleted } = opts;
|
|
3733
3865
|
const distinct = opts.distinct?.length ? opts.distinct : void 0;
|
|
3866
|
+
assertPagingNotNegative({
|
|
3867
|
+
limit: opts.limit,
|
|
3868
|
+
offset
|
|
3869
|
+
});
|
|
3734
3870
|
const cursorSelected = !single && isCursorFieldSelected(parsedInfo.fieldsByTypeName[typeName], table);
|
|
3735
3871
|
let cursorEntries;
|
|
3736
3872
|
if (!single && (after != null || cursorSelected)) {
|
|
@@ -3782,7 +3918,11 @@ const runRelationalSelect = async (opts) => {
|
|
|
3782
3918
|
const result = await queryBase.findFirst(omitUndefinedKeys(params));
|
|
3783
3919
|
return result ? remapToGraphQLSingleOutput(result, tableName, table, relationMap) : void 0;
|
|
3784
3920
|
}
|
|
3785
|
-
if (cursorEntries && cursorSelected)
|
|
3921
|
+
if (cursorEntries && cursorSelected) {
|
|
3922
|
+
for (const [column] of cursorEntries) params.columns[column] = true;
|
|
3923
|
+
const preciseProps = Object.keys(cursorPreciseExtras(table, cursorEntries));
|
|
3924
|
+
if (preciseProps.length) params.extras = Object.fromEntries(preciseProps.map((prop) => [prop, (aliasedTable) => cursorPreciseExtras(aliasedTable, cursorEntries)[prop]]));
|
|
3925
|
+
}
|
|
3786
3926
|
params.limit = distinctKeys ? void 0 : opts.limit;
|
|
3787
3927
|
const result = await queryBase.findMany(omitUndefinedKeys(params));
|
|
3788
3928
|
if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
|
|
@@ -4601,16 +4741,17 @@ const identifyRows = (field, args) => {
|
|
|
4601
4741
|
* Window functions require PostgreSQL, MySQL >= 8.0, or SQLite >= 3.25.
|
|
4602
4742
|
* Returns raw rows (NOT remapped); the caller groups + remaps them.
|
|
4603
4743
|
*/
|
|
4604
|
-
const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereCondition, orderByArg, limit, offset, pkNames, columns, orderCtx, whereArgs) => {
|
|
4744
|
+
const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereCondition, orderByArg, limit, offset, pkNames, columns, orderCtx, whereArgs, junction) => {
|
|
4605
4745
|
const cols = columns ?? getColumns(targetTable);
|
|
4606
4746
|
const orderExprs = [...orderByArg ? extractOrderBy(targetTable, orderByArg, orderCtx, whereArgs) : [], ...primaryKeyOrderExprs(targetTable, pkNames)];
|
|
4607
4747
|
const orderClause = orderExprs.length ? sql` order by ${sql.join(orderExprs, sql`, `)}` : sql``;
|
|
4608
4748
|
const RN = "__drizzle_graphql_rn";
|
|
4609
|
-
const
|
|
4610
|
-
const
|
|
4749
|
+
const partitionCol = junction ? junction.sourceCol : foreignCol;
|
|
4750
|
+
const rowNumber = sql`row_number() over (partition by ${partitionCol}${orderClause})`.as(RN);
|
|
4751
|
+
const sub = withJunction(db.select({
|
|
4611
4752
|
...cols,
|
|
4612
4753
|
[RN]: rowNumber
|
|
4613
|
-
}).from(targetTable).where(whereCondition).as("__paginated");
|
|
4754
|
+
}).from(targetTable), junction, foreignCol).where(whereCondition).as("__paginated");
|
|
4614
4755
|
const lower = offset ?? 0;
|
|
4615
4756
|
const windowConds = [gt(sub[RN], lower)];
|
|
4616
4757
|
if (limit != null) windowConds.push(lte(sub[RN], lower + limit));
|
|
@@ -4619,6 +4760,14 @@ const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereC
|
|
|
4619
4760
|
return rows;
|
|
4620
4761
|
};
|
|
4621
4762
|
/**
|
|
4763
|
+
* Alias the parent key is selected under when a relation goes through a junction table. The
|
|
4764
|
+
* key lives on the junction, not the target, so it has to be selected explicitly to group the
|
|
4765
|
+
* rows by parent — and a target row linked to several parents comes back once per parent.
|
|
4766
|
+
*/
|
|
4767
|
+
const PARENT_KEY = "__drizzle_graphql_parent";
|
|
4768
|
+
/** `from(target)`, joined to the junction table when the relation has one. */
|
|
4769
|
+
const withJunction = (query, junction, foreignCol) => junction ? query.innerJoin(junction.table, eq(junction.targetCol, foreignCol)) : query;
|
|
4770
|
+
/**
|
|
4622
4771
|
* Creates a RelationResolverFactory that generates field-level resolvers for each relation.
|
|
4623
4772
|
* Each resolver:
|
|
4624
4773
|
* 1. Returns pre-fetched data if the parent resolver already included it (eager path, zero cost).
|
|
@@ -4640,6 +4789,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4640
4789
|
const joinCols = extractRelationJoinColumns(relEntry, parentTable, targetTable);
|
|
4641
4790
|
if (!joinCols) return;
|
|
4642
4791
|
const { localColPropName, foreignCol, foreignColPropName } = joinCols;
|
|
4792
|
+
const junction = extractJunctionColumns(relEntry);
|
|
4793
|
+
const groupCol = junction ? junction.sourceCol : foreignCol;
|
|
4794
|
+
const groupKey = junction ? PARENT_KEY : foreignColPropName;
|
|
4795
|
+
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, getColumns(parentTable)[localColPropName], groupCol);
|
|
4643
4796
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, isOne && relEntry.relation?.optional === false);
|
|
4644
4797
|
const limitPolicy = isOne ? void 0 : limits?.(targetTableName);
|
|
4645
4798
|
const targetPkNames = relEntry.targetPkNames ?? [];
|
|
@@ -4673,7 +4826,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4673
4826
|
const column = targetColumns[columnName];
|
|
4674
4827
|
if (column) columns[columnName] = column;
|
|
4675
4828
|
}
|
|
4676
|
-
return
|
|
4829
|
+
return cursorEntries ? {
|
|
4830
|
+
...columns,
|
|
4831
|
+
...cursorPreciseSelection(targetTable, cursorEntries)
|
|
4832
|
+
} : columns;
|
|
4677
4833
|
};
|
|
4678
4834
|
const resolve = async (parent, args, context, info) => {
|
|
4679
4835
|
if (parent[relationName] !== void 0) return parent[relationName];
|
|
@@ -4683,6 +4839,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4683
4839
|
const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `relation:${tableName}::${relationName}`, () => {
|
|
4684
4840
|
const orderByArg = isOne ? args?.orderBy : withDefaultOrderBy(args ?? {}, targetTableName, policies?.defaultOrderBy).orderBy;
|
|
4685
4841
|
const limit = applyLimitPolicy(requestedLimit, limitPolicy, errorCtx);
|
|
4842
|
+
assertPagingNotNegative({ offset }, errorCtx);
|
|
4686
4843
|
const distinct = (args ?? {}).distinct?.length ? (args ?? {}).distinct : void 0;
|
|
4687
4844
|
const cursorSelected = !isOne && !!info && selectsCursorField(info, targetTable);
|
|
4688
4845
|
let cursorEntries;
|
|
@@ -4717,11 +4874,12 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4717
4874
|
loaderKey: `${tableName}::${relationName}::${argsKey}`
|
|
4718
4875
|
};
|
|
4719
4876
|
});
|
|
4877
|
+
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" });
|
|
4720
4878
|
return getOrCreateLoader(context, loaderKey, async (parentIds) => {
|
|
4721
4879
|
const executor = resolveExecutor(db, context);
|
|
4722
4880
|
const uniqueIds = [...new Set(parentIds)];
|
|
4723
4881
|
const scope = resolveScope(policies, context, filterCtx);
|
|
4724
|
-
let whereCondition = withScope(scope, targetTableName, targetTable, and(inArray(
|
|
4882
|
+
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);
|
|
4725
4883
|
if (distinct) {
|
|
4726
4884
|
const keys = await selectDistinctKeys({
|
|
4727
4885
|
db: executor,
|
|
@@ -4731,27 +4889,41 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4731
4889
|
pkNames: targetPkNames,
|
|
4732
4890
|
where: whereCondition,
|
|
4733
4891
|
orderBy: orderByArg,
|
|
4734
|
-
partitionBy: [
|
|
4892
|
+
partitionBy: [groupCol],
|
|
4893
|
+
junction: junction ? {
|
|
4894
|
+
table: junction.table,
|
|
4895
|
+
on: eq(junction.targetCol, foreignCol),
|
|
4896
|
+
parentKey: groupCol
|
|
4897
|
+
} : void 0
|
|
4735
4898
|
});
|
|
4736
4899
|
if (!keys.length) return parentIds.map(() => isOne ? null : []);
|
|
4737
|
-
whereCondition = and(whereCondition, primaryKeyRestriction(targetTable, targetPkNames, keys));
|
|
4900
|
+
whereCondition = and(whereCondition, junction ? or(...keys.map((key) => and(eq(groupCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
|
|
4738
4901
|
}
|
|
4902
|
+
const selection = junction ? {
|
|
4903
|
+
...columns ?? getColumns(targetTable),
|
|
4904
|
+
[PARENT_KEY]: groupCol
|
|
4905
|
+
} : columns;
|
|
4739
4906
|
let rows;
|
|
4740
|
-
if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames,
|
|
4907
|
+
if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName), whereArg, junction);
|
|
4741
4908
|
else {
|
|
4742
|
-
let q = (
|
|
4909
|
+
let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
|
|
4743
4910
|
const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName), whereArg) : [];
|
|
4744
4911
|
if (orderExprs.length) q = q.orderBy(...orderExprs);
|
|
4745
4912
|
rows = await q;
|
|
4746
4913
|
}
|
|
4747
4914
|
if (cursorEntries) attachRowCursors(rows, cursorEntries);
|
|
4915
|
+
const keyOf = (row) => {
|
|
4916
|
+
const key = String(row[groupKey]);
|
|
4917
|
+
if (junction) delete row[PARENT_KEY];
|
|
4918
|
+
return key;
|
|
4919
|
+
};
|
|
4748
4920
|
if (isOne) {
|
|
4749
|
-
const byKey = new Map(rows.map((row) => [
|
|
4921
|
+
const byKey = new Map(rows.map((row) => [keyOf(row), row]));
|
|
4750
4922
|
remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
|
|
4751
4923
|
return parentIds.map((id) => byKey.get(String(id)) ?? null);
|
|
4752
4924
|
}
|
|
4753
4925
|
const grouped = new Map(uniqueIds.map((id) => [String(id), []]));
|
|
4754
|
-
for (const row of rows) grouped.get(
|
|
4926
|
+
for (const row of rows) grouped.get(keyOf(row))?.push(row);
|
|
4755
4927
|
remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
|
|
4756
4928
|
return parentIds.map((id) => grouped.get(String(id)) ?? []);
|
|
4757
4929
|
}).load(localValue);
|
|
@@ -5086,7 +5258,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
|
|
|
5086
5258
|
});
|
|
5087
5259
|
const shell = new GraphQLObjectType({
|
|
5088
5260
|
name: typeName,
|
|
5089
|
-
description: cacheCtx.docs.describeTable?.(tableName),
|
|
5261
|
+
description: cacheCtx.docs.describeTable?.(tableName, table),
|
|
5090
5262
|
fields: () => ({
|
|
5091
5263
|
...tableFields,
|
|
5092
5264
|
...container.fields
|
|
@@ -5120,7 +5292,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
|
|
|
5120
5292
|
defaultName: resolveTypeName(targetTableName, typeNameMapper),
|
|
5121
5293
|
table: targetTableName
|
|
5122
5294
|
}),
|
|
5123
|
-
description: cacheCtx.docs.describeTable?.(targetTableName),
|
|
5295
|
+
description: cacheCtx.docs.describeTable?.(targetTableName, targetTable),
|
|
5124
5296
|
fields: () => ({
|
|
5125
5297
|
...targetTableFields,
|
|
5126
5298
|
...capturedTargetContainer.fields
|
|
@@ -5271,7 +5443,7 @@ const generateTableTypes = (tableName, tables, relationMap, withReturning, relat
|
|
|
5271
5443
|
defaultName: resolveTypeName(tableName, typeNameMapper),
|
|
5272
5444
|
table: tableName
|
|
5273
5445
|
}),
|
|
5274
|
-
description: cacheCtx.docs.describeTable?.(tableName),
|
|
5446
|
+
description: cacheCtx.docs.describeTable?.(tableName, tables[tableName]),
|
|
5275
5447
|
fields: {
|
|
5276
5448
|
...tableFields,
|
|
5277
5449
|
...relationFields
|
|
@@ -5838,6 +6010,10 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
5838
6010
|
return;
|
|
5839
6011
|
}
|
|
5840
6012
|
const { localColPropName, foreignCol } = joinCols;
|
|
6013
|
+
const junction = extractJunctionColumns(relEntry);
|
|
6014
|
+
if (junction === null) return;
|
|
6015
|
+
const groupCol = junction ? junction.sourceCol : foreignCol;
|
|
6016
|
+
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, getColumns(parentTable)[localColPropName], groupCol);
|
|
5841
6017
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
|
|
5842
6018
|
const targetTypeName = resolveTypeName(targetTableName, typeNameMapper);
|
|
5843
6019
|
const type = generateAggregateTypes(targetTable, targetTableName, targetTypeName, cacheCtx);
|
|
@@ -5868,11 +6044,12 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
5868
6044
|
const loader = getOrCreateLoader(context, loaderKey, async (parentIds) => {
|
|
5869
6045
|
const executor = resolveExecutor(db, context);
|
|
5870
6046
|
const uniqueIds = [...new Set(parentIds)];
|
|
5871
|
-
const whereCondition = withScope(resolveScope(policies, context, filterCtx), targetTableName, targetTable, and(inArray(
|
|
5872
|
-
const
|
|
5873
|
-
[GROUP_KEY]:
|
|
6047
|
+
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);
|
|
6048
|
+
const selected = executor.select({
|
|
6049
|
+
[GROUP_KEY]: groupCol,
|
|
5874
6050
|
...request.selection
|
|
5875
|
-
}).from(targetTable)
|
|
6051
|
+
}).from(targetTable);
|
|
6052
|
+
const rows = await (junction ? selected.innerJoin(junction.table, eq(junction.targetCol, foreignCol)) : selected).where(whereCondition).groupBy(groupCol);
|
|
5876
6053
|
const byKey = new Map(rows.map((row) => [row[GROUP_KEY], row]));
|
|
5877
6054
|
return parentIds.map((id) => byKey.get(id) ?? {});
|
|
5878
6055
|
});
|