@vantreeseba/drizzle-graphql 5.2.0 → 5.3.0

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.js CHANGED
@@ -1020,6 +1020,13 @@ var generateTableSelectTypeFieldsCached = (table, tableName) => {
1020
1020
  drizzleColumnToGraphQLType(columnDescription, columnName, tableName)
1021
1021
  ])
1022
1022
  );
1023
+ if (!remapped[CURSOR_FIELD_NAME]) {
1024
+ remapped[CURSOR_FIELD_NAME] = {
1025
+ type: GraphQLString2,
1026
+ description: "Opaque cursor of this row's position in the query's ordering. Pass it as `after` to resume from here. Only set on rows returned by a list query.",
1027
+ resolve: rowCursorResolver
1028
+ };
1029
+ }
1023
1030
  fieldMap.set(table, remapped);
1024
1031
  return remapped;
1025
1032
  };
@@ -1847,6 +1854,117 @@ var primaryKeyOrderExprs = (table, pkNames) => {
1847
1854
  const cols = getColumns(table);
1848
1855
  return pkNames.map((n) => cols[n]).filter(Boolean).map((col) => asc(col));
1849
1856
  };
1857
+ var CURSOR_FIELD_NAME = "cursor";
1858
+ var ROW_CURSOR_PROP = "__drizzle_graphql_cursor";
1859
+ var orderByHasRelationEntry = (orderBy) => !!orderBy && Object.values(orderBy).some((config) => config && typeof config === "object" && config.direction === void 0);
1860
+ var cursorOrderingEntries = (orderBy, pkNames) => {
1861
+ const entries = orderBy ? orderByEntries(orderBy) : [];
1862
+ const seen = new Set(entries.map(([column]) => column));
1863
+ for (const pk of pkNames) {
1864
+ if (!seen.has(pk)) {
1865
+ entries.push([pk, "asc"]);
1866
+ }
1867
+ }
1868
+ return entries;
1869
+ };
1870
+ var cursorOrderExprs = (table, entries) => {
1871
+ const cols = getColumns(table);
1872
+ return entries.flatMap(
1873
+ ([column, direction, nulls]) => orderExpressions(cols[column], direction, nulls ?? void 0, cols[column])
1874
+ );
1875
+ };
1876
+ var encodeCursorValue = (value) => {
1877
+ if (value instanceof Date) {
1878
+ return { $type: "date", value: value.toISOString() };
1879
+ }
1880
+ if (typeof value === "bigint") {
1881
+ return { $type: "bigint", value: value.toString() };
1882
+ }
1883
+ return value;
1884
+ };
1885
+ var decodeCursorValue = (value) => {
1886
+ if (value && typeof value === "object" && typeof value.$type === "string") {
1887
+ if (value.$type === "date") {
1888
+ return new Date(value.value);
1889
+ }
1890
+ if (value.$type === "bigint") {
1891
+ return BigInt(value.value);
1892
+ }
1893
+ }
1894
+ return value;
1895
+ };
1896
+ var encodeCursor = (entries, row) => {
1897
+ const payload = {
1898
+ o: entries,
1899
+ v: entries.map(([column]) => encodeCursorValue(row[column] ?? null))
1900
+ };
1901
+ return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
1902
+ };
1903
+ var decodeCursor = (after, entries) => {
1904
+ let payload;
1905
+ try {
1906
+ payload = JSON.parse(Buffer.from(after, "base64url").toString("utf8"));
1907
+ } catch (_e) {
1908
+ throw new GraphQLError3("Invalid cursor: unable to decode it. Pass a cursor returned by a previous page.");
1909
+ }
1910
+ if (!payload || typeof payload !== "object" || !Array.isArray(payload.o) || !Array.isArray(payload.v) || payload.o.length !== payload.v.length) {
1911
+ throw new GraphQLError3("Invalid cursor: malformed payload. Pass a cursor returned by a previous page.");
1912
+ }
1913
+ const matchesOrdering = payload.o.length === entries.length && payload.o.every(
1914
+ (entry, i) => Array.isArray(entry) && entry[0] === entries[i][0] && entry[1] === entries[i][1] && // The nulls override changes where NULLs sort, so it is part of the ordering identity.
1915
+ // JSON round-trips undefined as null — compare the normalized forms.
1916
+ (entry[2] ?? null) === (entries[i][2] ?? null)
1917
+ );
1918
+ if (!matchesOrdering) {
1919
+ throw new GraphQLError3(
1920
+ "Invalid cursor: it was issued for a different ordering. Pass the same orderBy the cursor's page used."
1921
+ );
1922
+ }
1923
+ return payload.v.map(decodeCursorValue);
1924
+ };
1925
+ var buildCursorCondition = (table, entries, values, nullOrdering) => {
1926
+ const cols = getColumns(table);
1927
+ const disjuncts = [];
1928
+ const equalities = [];
1929
+ entries.forEach(([column, direction, nulls], i) => {
1930
+ const col = cols[column];
1931
+ if (!col) {
1932
+ throw new GraphQLError3(`Invalid cursor: unknown column '${column}'.`);
1933
+ }
1934
+ const value = values[i];
1935
+ const nullsSortLast = nulls != null ? nulls === "last" : direction === "asc" === (nullOrdering === "nulls-largest");
1936
+ let strictlyAfter;
1937
+ if (value === null || value === void 0) {
1938
+ strictlyAfter = nullsSortLast ? void 0 : isNotNull(col);
1939
+ } else {
1940
+ const comparison = direction === "asc" ? gt(col, value) : lt(col, value);
1941
+ strictlyAfter = nullsSortLast ? or(comparison, isNull(col)) : comparison;
1942
+ }
1943
+ if (strictlyAfter) {
1944
+ disjuncts.push(equalities.length ? and(...equalities, strictlyAfter) : strictlyAfter);
1945
+ }
1946
+ equalities.push(value === null || value === void 0 ? isNull(col) : eq(col, value));
1947
+ });
1948
+ if (!disjuncts.length) {
1949
+ return sql`1 = 0`;
1950
+ }
1951
+ return disjuncts.length > 1 ? or(...disjuncts) : disjuncts[0];
1952
+ };
1953
+ var isCursorFieldSelected = (tree, table) => {
1954
+ if (!tree) {
1955
+ return false;
1956
+ }
1957
+ if (getColumns(table)[CURSOR_FIELD_NAME]) {
1958
+ return false;
1959
+ }
1960
+ return Object.values(tree).some((field) => field.name === CURSOR_FIELD_NAME);
1961
+ };
1962
+ var attachRowCursors = (rows, entries) => {
1963
+ for (const row of rows) {
1964
+ row[ROW_CURSOR_PROP] = encodeCursor(entries, row);
1965
+ }
1966
+ };
1967
+ var rowCursorResolver = (source) => source?.[ROW_CURSOR_PROP] ?? null;
1850
1968
  var getUniqueColumnSets = (table, getTableConfig4) => {
1851
1969
  const cols = getColumns(table);
1852
1970
  const propNameByColumnName = new Map(Object.entries(cols).map(([propName, col]) => [col.name, propName]));
@@ -2171,6 +2289,10 @@ var selectArrayArgs = (orderArgs, filterArgs, distinctEnum) => ({
2171
2289
  limit: { type: GraphQLInt2 },
2172
2290
  orderBy: { type: orderArgs },
2173
2291
  where: { type: filterArgs },
2292
+ after: {
2293
+ type: GraphQLString2,
2294
+ description: "Keyset pagination: only return rows strictly after this cursor (a row's `cursor` field from a previous page, under the same orderBy)."
2295
+ },
2174
2296
  ...distinctEnum ? { distinct: { type: new GraphQLList2(new GraphQLNonNull2(distinctEnum)) } } : {}
2175
2297
  });
2176
2298
  var selectSingleArgs = (orderArgs, filterArgs) => ({
@@ -2193,9 +2315,31 @@ var runRelationalSelect = async (opts) => {
2193
2315
  where,
2194
2316
  single,
2195
2317
  filterCtx,
2196
- pkNames
2318
+ pkNames,
2319
+ after
2197
2320
  } = opts;
2198
2321
  const distinct = opts.distinct?.length ? opts.distinct : void 0;
2322
+ const cursorSelected = !single && isCursorFieldSelected(parsedInfo.fieldsByTypeName[typeName], table);
2323
+ let cursorEntries;
2324
+ if (!single && (after != null || cursorSelected)) {
2325
+ if (after != null && distinct) {
2326
+ throw new GraphQLError3("'after' cannot be combined with 'distinct'.");
2327
+ }
2328
+ if (orderByHasRelationEntry(orderBy)) {
2329
+ if (after != null) {
2330
+ throw new GraphQLError3(
2331
+ "'after' cannot be combined with an orderBy that orders through a relation \u2014 a related row's value cannot be encoded into a cursor."
2332
+ );
2333
+ }
2334
+ } else if (!pkNames?.length) {
2335
+ if (after != null) {
2336
+ throw new GraphQLError3(`Table ${tableName} has no primary key, so cursor pagination cannot be used on it.`);
2337
+ }
2338
+ } else {
2339
+ cursorEntries = cursorOrderingEntries(orderBy, pkNames);
2340
+ }
2341
+ }
2342
+ const cursorValues = after != null && cursorEntries ? decodeCursor(after, cursorEntries) : void 0;
2199
2343
  const needsDefaultOrder = single || offset != null || opts.limit != null;
2200
2344
  let distinctKeys;
2201
2345
  if (distinct) {
@@ -2226,9 +2370,21 @@ var runRelationalSelect = async (opts) => {
2226
2370
  orderBy: distinctKeys ? (aliasedTable2) => [
2227
2371
  ...orderBy ? extractOrderBy(aliasedTable2, orderBy, relationFilterCtx(filterCtx, tableName)) : [],
2228
2372
  ...primaryKeyOrderExprs(aliasedTable2, pkNames)
2229
- ] : orderBy ? (aliasedTable2) => extractOrderBy(aliasedTable2, orderBy, relationFilterCtx(filterCtx, tableName)) : needsDefaultOrder && pkNames?.length ? (aliasedTable2) => primaryKeyOrderExprs(aliasedTable2, pkNames) : void 0,
2230
- where: distinctKeys ? { RAW: (aliasedTable2) => primaryKeyRestriction(aliasedTable2, pkNames, distinctKeys) } : where ? {
2231
- RAW: (aliasedTable2) => extractFilters(aliasedTable2, tableName, where, relationFilterCtx(filterCtx, tableName))
2373
+ ] : cursorEntries ? (
2374
+ // Cursor pagination needs a total order: the request's orderBy plus the PK tiebreak,
2375
+ // exactly the ordering the cursor encodes and the keyset predicate compares against.
2376
+ (aliasedTable2) => cursorOrderExprs(aliasedTable2, cursorEntries)
2377
+ ) : orderBy ? (aliasedTable2) => extractOrderBy(aliasedTable2, orderBy, relationFilterCtx(filterCtx, tableName)) : needsDefaultOrder && pkNames?.length ? (aliasedTable2) => primaryKeyOrderExprs(aliasedTable2, pkNames) : void 0,
2378
+ where: distinctKeys ? { RAW: (aliasedTable2) => primaryKeyRestriction(aliasedTable2, pkNames, distinctKeys) } : where || cursorValues ? {
2379
+ RAW: (aliasedTable2) => and(
2380
+ where ? extractFilters(aliasedTable2, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0,
2381
+ cursorValues ? buildCursorCondition(
2382
+ aliasedTable2,
2383
+ cursorEntries,
2384
+ cursorValues,
2385
+ opts.nullOrdering ?? "nulls-smallest"
2386
+ ) : void 0
2387
+ )
2232
2388
  } : void 0,
2233
2389
  with: relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, typeNameMapper, filterCtx) : void 0
2234
2390
  };
@@ -2236,8 +2392,16 @@ var runRelationalSelect = async (opts) => {
2236
2392
  const result2 = await queryBase.findFirst(params);
2237
2393
  return result2 ? remapToGraphQLSingleOutput(result2, tableName, table, relationMap) : void 0;
2238
2394
  }
2395
+ if (cursorEntries && cursorSelected) {
2396
+ for (const [column] of cursorEntries) {
2397
+ params.columns[column] = true;
2398
+ }
2399
+ }
2239
2400
  params.limit = distinctKeys ? void 0 : opts.limit;
2240
2401
  const result = await queryBase.findMany(params);
2402
+ if (cursorEntries && cursorSelected) {
2403
+ attachRowCursors(result, cursorEntries);
2404
+ }
2241
2405
  return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
2242
2406
  };
2243
2407
  var eagerLoadMutationRelations = async (db, tableName, rows, pkNames, withParams) => {
@@ -2816,7 +2980,9 @@ var generateSelectArray = (db, tableName, tables, relationMap, orderArgs, filter
2816
2980
  single: false,
2817
2981
  filterCtx,
2818
2982
  pkNames,
2819
- db: executor
2983
+ db: executor,
2984
+ // MySQL sorts NULLs as the smallest values (first in ASC).
2985
+ nullOrdering: "nulls-smallest"
2820
2986
  });
2821
2987
  } catch (e) {
2822
2988
  throw toGraphQLError(e);
@@ -3281,7 +3447,7 @@ var generateSchemaData = (db, schema, relations, options) => {
3281
3447
  };
3282
3448
 
3283
3449
  // src/util/builders/pg.ts
3284
- import { is as is5, One as One4 } from "drizzle-orm";
3450
+ import { and as and3, getColumns as getColumns3, is as is5, One as One4 } from "drizzle-orm";
3285
3451
  import { getTableConfig as getTableConfig2, PgTable } from "drizzle-orm/pg-core";
3286
3452
  import {
3287
3453
  GraphQLError as GraphQLError6,
@@ -3318,16 +3484,48 @@ var generateSelectArray2 = (db, tableName, tables, relationMap, orderArgs, filte
3318
3484
  single: false,
3319
3485
  filterCtx,
3320
3486
  pkNames,
3321
- db: executor
3487
+ db: executor,
3488
+ // PostgreSQL sorts NULLs as the largest values (last in ASC).
3489
+ nullOrdering: "nulls-largest"
3322
3490
  });
3323
3491
  }
3324
- const { offset, limit, orderBy, where, distinct } = args;
3492
+ const { offset, limit, orderBy, where, distinct, after } = args;
3325
3493
  const selectedColumnsSql = extractSelectedColumnsFromTreeSQLFormat(
3326
3494
  parsedInfo.fieldsByTypeName[typeName],
3327
3495
  table,
3328
3496
  { tableName, relationMap, tables }
3329
3497
  );
3330
- const whereSql = where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0;
3498
+ const cursorSelected = isCursorFieldSelected(parsedInfo.fieldsByTypeName[typeName], table);
3499
+ let cursorEntries;
3500
+ if (after != null || cursorSelected) {
3501
+ if (after != null && distinct?.length) {
3502
+ throw new GraphQLError6("'after' cannot be combined with 'distinct'.");
3503
+ }
3504
+ if (orderByHasRelationEntry(orderBy)) {
3505
+ if (after != null) {
3506
+ throw new GraphQLError6(
3507
+ "'after' cannot be combined with an orderBy that orders through a relation \u2014 a related row's value cannot be encoded into a cursor."
3508
+ );
3509
+ }
3510
+ } else if (!pkNames.length) {
3511
+ if (after != null) {
3512
+ throw new GraphQLError6(
3513
+ `Table ${tableName} has no primary key, so cursor pagination cannot be used on it.`
3514
+ );
3515
+ }
3516
+ } else {
3517
+ cursorEntries = cursorOrderingEntries(orderBy, pkNames);
3518
+ if (cursorSelected) {
3519
+ const allColumns = getColumns3(table);
3520
+ for (const [column] of cursorEntries) {
3521
+ selectedColumnsSql[column] ??= allColumns[column];
3522
+ }
3523
+ }
3524
+ }
3525
+ }
3526
+ const cursorCondition = after != null && cursorEntries ? buildCursorCondition(table, cursorEntries, decodeCursor(after, cursorEntries), "nulls-largest") : void 0;
3527
+ const baseWhereSql = where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0;
3528
+ const whereSql = cursorCondition ? and3(baseWhereSql, cursorCondition) : baseWhereSql;
3331
3529
  let distinctKeys;
3332
3530
  if (distinct?.length) {
3333
3531
  distinctKeys = await selectDistinctKeys({
@@ -3351,7 +3549,9 @@ var generateSelectArray2 = (db, tableName, tables, relationMap, orderArgs, filte
3351
3549
  } else if (whereSql) {
3352
3550
  q = q.where(whereSql);
3353
3551
  }
3354
- if (orderBy) {
3552
+ if (cursorEntries && !distinctKeys) {
3553
+ q = q.orderBy(...cursorOrderExprs(table, cursorEntries));
3554
+ } else if (orderBy) {
3355
3555
  q = q.orderBy(
3356
3556
  ...extractOrderBy(table, orderBy, relationFilterCtx(filterCtx, tableName)),
3357
3557
  ...distinctKeys ? primaryKeyOrderExprs(table, pkNames) : []
@@ -3367,7 +3567,11 @@ var generateSelectArray2 = (db, tableName, tables, relationMap, orderArgs, filte
3367
3567
  q = q.limit(limit);
3368
3568
  }
3369
3569
  }
3370
- return remapToGraphQLArrayOutput(await q, tableName, table, relationMap);
3570
+ const rows = await q;
3571
+ if (cursorEntries && cursorSelected) {
3572
+ attachRowCursors(rows, cursorEntries);
3573
+ }
3574
+ return remapToGraphQLArrayOutput(rows, tableName, table, relationMap);
3371
3575
  } catch (e) {
3372
3576
  throw toGraphQLError(e);
3373
3577
  }
@@ -4083,7 +4287,9 @@ var generateSelectArray3 = (db, tableName, tables, relationMap, orderArgs, filte
4083
4287
  single: false,
4084
4288
  filterCtx,
4085
4289
  pkNames,
4086
- db: executor
4290
+ db: executor,
4291
+ // SQLite sorts NULLs as the smallest values (first in ASC).
4292
+ nullOrdering: "nulls-smallest"
4087
4293
  });
4088
4294
  } catch (e) {
4089
4295
  throw toGraphQLError(e);