@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/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' |
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' |
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 === false) continue;
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));
@@ -2732,6 +2739,45 @@ const cursorOrderExprs = (table, entries) => {
2732
2739
  return entries.flatMap(([column, direction, nulls]) => orderExpressions(cols[column], direction, nulls ?? void 0, cols[column]));
2733
2740
  };
2734
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
+ /**
2735
2781
  * Serializes one ordering-tuple value into a JSON-safe shape. Dates and bigints don't survive
2736
2782
  * JSON.stringify losslessly (bigint throws, Date turns into an untagged string), so they are
2737
2783
  * tagged; decimals already arrive from the driver as strings, which are lossless as-is.
@@ -2766,7 +2812,11 @@ const cursorEncoder = (entries) => {
2766
2812
  const spec = JSON.stringify(entries);
2767
2813
  const columns = entries.map(([column]) => column);
2768
2814
  return (row) => {
2769
- const values = JSON.stringify(columns.map((column) => encodeCursorValue(row[column] ?? null)));
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
+ }));
2770
2820
  return Buffer.from(`{"o":${spec},"v":${values}}`, "utf8").toString("base64url");
2771
2821
  };
2772
2822
  };
@@ -2859,7 +2909,11 @@ const selectsCursorField = (info, table) => {
2859
2909
  */
2860
2910
  const attachRowCursors = (rows, entries) => {
2861
2911
  const encode = cursorEncoder(entries);
2862
- for (const row of rows) row[ROW_CURSOR_PROP] = encode(row);
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
+ }
2863
2917
  };
2864
2918
  /** Resolver for the `cursor` meta field: reads the value a list resolver attached, if any. */
2865
2919
  const rowCursorResolver = (source) => source?.[ROW_CURSOR_PROP] ?? null;
@@ -3217,6 +3271,21 @@ const withDefaultOrderBy = (args, tableName, defaults) => {
3217
3271
  } : args;
3218
3272
  };
3219
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
+ /**
3220
3289
  * The `limit` a query actually runs with, given the policy for the table it reads.
3221
3290
  *
3222
3291
  * A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
@@ -3228,6 +3297,7 @@ const withDefaultOrderBy = (args, tableName, defaults) => {
3228
3297
  * fail a client's query over.
3229
3298
  */
3230
3299
  const applyLimitPolicy = (limit, policy, errorCtx) => {
3300
+ assertPagingNotNegative({ limit }, errorCtx);
3231
3301
  if (!policy) return limit ?? void 0;
3232
3302
  const { defaultLimit, maxLimit, clampToMax } = policy;
3233
3303
  if (limit == null) {
@@ -3745,6 +3815,11 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
3745
3815
  thisRecord.columns = columns;
3746
3816
  const relationArgs = is(relEntry.relation, One) ? field.args : withDefaultOrderBy(field.args ?? {}, targetTableName, defaultOrderBy);
3747
3817
  const offset = relationArgs?.offset ?? void 0;
3818
+ assertPagingNotNegative({ offset }, {
3819
+ table: targetTableName,
3820
+ operation: "relation",
3821
+ relation: relName
3822
+ });
3748
3823
  const limit = is(relEntry.relation, One) ? relationArgs?.limit ?? void 0 : applyLimitPolicy(relationArgs?.limit, limits?.(targetTableName), {
3749
3824
  table: targetTableName,
3750
3825
  operation: "relation",
@@ -3788,6 +3863,10 @@ const pruneNonEagerRelations = (relationMap, shouldEagerLoad) => {
3788
3863
  const runRelationalSelect = async (opts) => {
3789
3864
  const { queryBase, tables, tableName, table, relationMap, typeName, typeNameMapper, parsedInfo, offset, orderBy, where, single, filterCtx, pkNames, after, scope, deleted } = opts;
3790
3865
  const distinct = opts.distinct?.length ? opts.distinct : void 0;
3866
+ assertPagingNotNegative({
3867
+ limit: opts.limit,
3868
+ offset
3869
+ });
3791
3870
  const cursorSelected = !single && isCursorFieldSelected(parsedInfo.fieldsByTypeName[typeName], table);
3792
3871
  let cursorEntries;
3793
3872
  if (!single && (after != null || cursorSelected)) {
@@ -3839,7 +3918,11 @@ const runRelationalSelect = async (opts) => {
3839
3918
  const result = await queryBase.findFirst(omitUndefinedKeys(params));
3840
3919
  return result ? remapToGraphQLSingleOutput(result, tableName, table, relationMap) : void 0;
3841
3920
  }
3842
- if (cursorEntries && cursorSelected) for (const [column] of cursorEntries) params.columns[column] = true;
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
+ }
3843
3926
  params.limit = distinctKeys ? void 0 : opts.limit;
3844
3927
  const result = await queryBase.findMany(omitUndefinedKeys(params));
3845
3928
  if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
@@ -4743,7 +4826,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4743
4826
  const column = targetColumns[columnName];
4744
4827
  if (column) columns[columnName] = column;
4745
4828
  }
4746
- return columns;
4829
+ return cursorEntries ? {
4830
+ ...columns,
4831
+ ...cursorPreciseSelection(targetTable, cursorEntries)
4832
+ } : columns;
4747
4833
  };
4748
4834
  const resolve = async (parent, args, context, info) => {
4749
4835
  if (parent[relationName] !== void 0) return parent[relationName];
@@ -4753,6 +4839,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4753
4839
  const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `relation:${tableName}::${relationName}`, () => {
4754
4840
  const orderByArg = isOne ? args?.orderBy : withDefaultOrderBy(args ?? {}, targetTableName, policies?.defaultOrderBy).orderBy;
4755
4841
  const limit = applyLimitPolicy(requestedLimit, limitPolicy, errorCtx);
4842
+ assertPagingNotNegative({ offset }, errorCtx);
4756
4843
  const distinct = (args ?? {}).distinct?.length ? (args ?? {}).distinct : void 0;
4757
4844
  const cursorSelected = !isOne && !!info && selectsCursorField(info, targetTable);
4758
4845
  let cursorEntries;