@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 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 — and a write
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 === 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));
@@ -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) => 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
+ }));
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
- 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
+ }
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) 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
+ }
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 columns;
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;