@vantreeseba/drizzle-graphql 15.1.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 +17 -2
- package/dist/index.cjs +92 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +92 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1042,6 +1042,11 @@ The pattern operators are `like` / `notLike` / `ilike` / `notIlike`, the safe
|
|
|
1042
1042
|
`startsWith` / `endsWith` / `contains` and their `i`-prefixed forms, plus the `insensitive`
|
|
1043
1043
|
flag that modifies them.
|
|
1044
1044
|
|
|
1045
|
+
An operand of `null` leaves its operator out, as though it had not been sent. `false` is an
|
|
1046
|
+
ordinary value: `active: { eq: false }` matches the rows that are false, `ne: false` the rows
|
|
1047
|
+
that are true, and `has: false` the arrays holding a `false`. Only the flag operators —
|
|
1048
|
+
`isNull`, `isNotNull` and `isEmpty` — read `false` as "add no condition".
|
|
1049
|
+
|
|
1045
1050
|
The omissions are the cases where the operator cannot ask anything a caller means:
|
|
1046
1051
|
|
|
1047
1052
|
- **Pattern matching a timestamp, a boolean or an enum member** compares whatever the value
|
|
@@ -1479,7 +1484,12 @@ field, and passing it back as `after` resumes strictly after that row.
|
|
|
1479
1484
|
in `asc`), MySQL and SQLite smallest (first in `asc`). The predicate matches either way,
|
|
1480
1485
|
so rows with `NULL` in an ordered column are paged through, not skipped, and `nulls` is
|
|
1481
1486
|
part of the ordering a cursor is tied to
|
|
1482
|
-
- Dates and bigints round-trip losslessly through the cursor
|
|
1487
|
+
- Dates and bigints round-trip losslessly through the cursor, at the column's own precision:
|
|
1488
|
+
a PostgreSQL `timestamp` / `timestamptz` (microseconds unless declared `precision: 3` or
|
|
1489
|
+
less) or a MySQL `datetime` / `timestamp` with `fsp` above 3 holds more than the
|
|
1490
|
+
millisecond JS `Date` that `mode: 'date'` reads it into, so the cursor carries the
|
|
1491
|
+
database's own text of the value instead, and rows a microsecond apart are still distinct
|
|
1492
|
+
pages
|
|
1483
1493
|
- A cursor is only valid under the ordering it was issued for — reusing it with a
|
|
1484
1494
|
different `orderBy` (or a malformed/corrupted cursor) returns a `GraphQLError`
|
|
1485
1495
|
- `after` cannot be combined with `distinct`, with `matchFilterOrder`, or with an `orderBy`
|
|
@@ -1891,6 +1901,10 @@ Two details worth stating plainly:
|
|
|
1891
1901
|
- **A rejection is the default because truncation lies.** Silently cutting a page down to
|
|
1892
1902
|
`maxLimit` tells a paginating client it has reached the end when it has not. `clampToMax:
|
|
1893
1903
|
true` opts into that when you would rather never fail a query.
|
|
1904
|
+
- **A negative `limit` or `offset` is always rejected** with `DRIZZLE_INVALID_INPUT_VALUE`,
|
|
1905
|
+
whether or not `limits` is configured — on root lists, single queries, relation fields and
|
|
1906
|
+
`resolveSelection`. SQLite reads `LIMIT -1` as "no limit", so a negative value would
|
|
1907
|
+
otherwise walk straight past `maxLimit`; `0` stays valid and returns no rows.
|
|
1894
1908
|
|
|
1895
1909
|
The policy applies to root list queries and to to-many relation fields, on both the eager and
|
|
1896
1910
|
the lazy relation paths. A relation takes the policy of the table it *reads*, not its parent's:
|
|
@@ -2114,7 +2128,8 @@ to match on prose:
|
|
|
2114
2128
|
`DrizzleErrorContext`.
|
|
2115
2129
|
|
|
2116
2130
|
A value the request could not supply is `DRIZZLE_INVALID_INPUT_VALUE` — a date that does not
|
|
2117
|
-
parse, a `BigInt` argument that is not an integer, a malformed JSON column value
|
|
2131
|
+
parse, a `BigInt` argument that is not an integer, a malformed JSON column value, a negative
|
|
2132
|
+
`limit` or `offset` — and a write
|
|
2118
2133
|
naming something that is not a column of its table is `DRIZZLE_UNKNOWN_COLUMN`. The mirror
|
|
2119
2134
|
image, a stored value the scalar transporting it cannot represent, is
|
|
2120
2135
|
`DRIZZLE_UNREPRESENTABLE_VALUE`; it says the data is out of range, not that the request was
|
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 ===
|
|
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));
|
|
@@ -2756,6 +2763,45 @@ const cursorOrderExprs = (table, entries) => {
|
|
|
2756
2763
|
return entries.flatMap(([column, direction, nulls]) => orderExpressions(cols[column], direction, nulls ?? void 0, cols[column]));
|
|
2757
2764
|
};
|
|
2758
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
|
+
/**
|
|
2759
2805
|
* Serializes one ordering-tuple value into a JSON-safe shape. Dates and bigints don't survive
|
|
2760
2806
|
* JSON.stringify losslessly (bigint throws, Date turns into an untagged string), so they are
|
|
2761
2807
|
* tagged; decimals already arrive from the driver as strings, which are lossless as-is.
|
|
@@ -2790,7 +2836,11 @@ const cursorEncoder = (entries) => {
|
|
|
2790
2836
|
const spec = JSON.stringify(entries);
|
|
2791
2837
|
const columns = entries.map(([column]) => column);
|
|
2792
2838
|
return (row) => {
|
|
2793
|
-
const values = JSON.stringify(columns.map((column) =>
|
|
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
|
+
}));
|
|
2794
2844
|
return Buffer.from(`{"o":${spec},"v":${values}}`, "utf8").toString("base64url");
|
|
2795
2845
|
};
|
|
2796
2846
|
};
|
|
@@ -2883,7 +2933,11 @@ const selectsCursorField = (info, table) => {
|
|
|
2883
2933
|
*/
|
|
2884
2934
|
const attachRowCursors = (rows, entries) => {
|
|
2885
2935
|
const encode = cursorEncoder(entries);
|
|
2886
|
-
|
|
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
|
+
}
|
|
2887
2941
|
};
|
|
2888
2942
|
/** Resolver for the `cursor` meta field: reads the value a list resolver attached, if any. */
|
|
2889
2943
|
const rowCursorResolver = (source) => source?.[ROW_CURSOR_PROP] ?? null;
|
|
@@ -3241,6 +3295,21 @@ const withDefaultOrderBy = (args, tableName, defaults) => {
|
|
|
3241
3295
|
} : args;
|
|
3242
3296
|
};
|
|
3243
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
|
+
/**
|
|
3244
3313
|
* The `limit` a query actually runs with, given the policy for the table it reads.
|
|
3245
3314
|
*
|
|
3246
3315
|
* A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
|
|
@@ -3252,6 +3321,7 @@ const withDefaultOrderBy = (args, tableName, defaults) => {
|
|
|
3252
3321
|
* fail a client's query over.
|
|
3253
3322
|
*/
|
|
3254
3323
|
const applyLimitPolicy = (limit, policy, errorCtx) => {
|
|
3324
|
+
assertPagingNotNegative({ limit }, errorCtx);
|
|
3255
3325
|
if (!policy) return limit ?? void 0;
|
|
3256
3326
|
const { defaultLimit, maxLimit, clampToMax } = policy;
|
|
3257
3327
|
if (limit == null) {
|
|
@@ -3769,6 +3839,11 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
|
|
|
3769
3839
|
thisRecord.columns = columns;
|
|
3770
3840
|
const relationArgs = (0, drizzle_orm.is)(relEntry.relation, drizzle_orm.One) ? field.args : withDefaultOrderBy(field.args ?? {}, targetTableName, defaultOrderBy);
|
|
3771
3841
|
const offset = relationArgs?.offset ?? void 0;
|
|
3842
|
+
assertPagingNotNegative({ offset }, {
|
|
3843
|
+
table: targetTableName,
|
|
3844
|
+
operation: "relation",
|
|
3845
|
+
relation: relName
|
|
3846
|
+
});
|
|
3772
3847
|
const limit = (0, drizzle_orm.is)(relEntry.relation, drizzle_orm.One) ? relationArgs?.limit ?? void 0 : applyLimitPolicy(relationArgs?.limit, limits?.(targetTableName), {
|
|
3773
3848
|
table: targetTableName,
|
|
3774
3849
|
operation: "relation",
|
|
@@ -3812,6 +3887,10 @@ const pruneNonEagerRelations = (relationMap, shouldEagerLoad) => {
|
|
|
3812
3887
|
const runRelationalSelect = async (opts) => {
|
|
3813
3888
|
const { queryBase, tables, tableName, table, relationMap, typeName, typeNameMapper, parsedInfo, offset, orderBy, where, single, filterCtx, pkNames, after, scope, deleted } = opts;
|
|
3814
3889
|
const distinct = opts.distinct?.length ? opts.distinct : void 0;
|
|
3890
|
+
assertPagingNotNegative({
|
|
3891
|
+
limit: opts.limit,
|
|
3892
|
+
offset
|
|
3893
|
+
});
|
|
3815
3894
|
const cursorSelected = !single && isCursorFieldSelected(parsedInfo.fieldsByTypeName[typeName], table);
|
|
3816
3895
|
let cursorEntries;
|
|
3817
3896
|
if (!single && (after != null || cursorSelected)) {
|
|
@@ -3863,7 +3942,11 @@ const runRelationalSelect = async (opts) => {
|
|
|
3863
3942
|
const result = await queryBase.findFirst(omitUndefinedKeys(params));
|
|
3864
3943
|
return result ? remapToGraphQLSingleOutput(result, tableName, table, relationMap) : void 0;
|
|
3865
3944
|
}
|
|
3866
|
-
if (cursorEntries && cursorSelected)
|
|
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
|
+
}
|
|
3867
3950
|
params.limit = distinctKeys ? void 0 : opts.limit;
|
|
3868
3951
|
const result = await queryBase.findMany(omitUndefinedKeys(params));
|
|
3869
3952
|
if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
|
|
@@ -4767,7 +4850,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4767
4850
|
const column = targetColumns[columnName];
|
|
4768
4851
|
if (column) columns[columnName] = column;
|
|
4769
4852
|
}
|
|
4770
|
-
return
|
|
4853
|
+
return cursorEntries ? {
|
|
4854
|
+
...columns,
|
|
4855
|
+
...cursorPreciseSelection(targetTable, cursorEntries)
|
|
4856
|
+
} : columns;
|
|
4771
4857
|
};
|
|
4772
4858
|
const resolve = async (parent, args, context, info) => {
|
|
4773
4859
|
if (parent[relationName] !== void 0) return parent[relationName];
|
|
@@ -4777,6 +4863,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4777
4863
|
const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `relation:${tableName}::${relationName}`, () => {
|
|
4778
4864
|
const orderByArg = isOne ? args?.orderBy : withDefaultOrderBy(args ?? {}, targetTableName, policies?.defaultOrderBy).orderBy;
|
|
4779
4865
|
const limit = applyLimitPolicy(requestedLimit, limitPolicy, errorCtx);
|
|
4866
|
+
assertPagingNotNegative({ offset }, errorCtx);
|
|
4780
4867
|
const distinct = (args ?? {}).distinct?.length ? (args ?? {}).distinct : void 0;
|
|
4781
4868
|
const cursorSelected = !isOne && !!info && selectsCursorField(info, targetTable);
|
|
4782
4869
|
let cursorEntries;
|