@vantreeseba/drizzle-graphql 5.1.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.d.cts CHANGED
@@ -57,12 +57,24 @@ type FilterColumnOperatorsCore<TColumn extends Column, TColType = GetColumnGqlDa
57
57
  notIlike: string;
58
58
  startsWith: string;
59
59
  endsWith: string;
60
- contains: string;
60
+ /**
61
+ * String columns: safe substring match (`%`, `_` and `\` matched literally).
62
+ * JSON columns: structural containment (Postgres `@>` / MySQL JSON_CONTAINS).
63
+ */
64
+ contains: string | TColType;
61
65
  iStartsWith: string;
62
66
  iEndsWith: string;
63
67
  iContains: string;
64
68
  inArray: Array<TColType>;
65
69
  notInArray: Array<TColType>;
70
+ /** Array columns only: the array contains this element. */
71
+ has: TColType extends Array<infer TElement> ? TElement : never;
72
+ /** Array columns only: the array overlaps these elements (`&&`). */
73
+ hasSome: TColType extends Array<infer TElement> ? Array<TElement> : never;
74
+ /** Array columns only: the array contains all of these elements (`@>`). */
75
+ hasEvery: TColType extends Array<infer TElement> ? Array<TElement> : never;
76
+ /** Array columns only: when true, matches arrays with no elements. */
77
+ isEmpty: boolean;
66
78
  isNull: boolean;
67
79
  isNotNull: boolean;
68
80
  }>;
package/dist/index.d.ts CHANGED
@@ -57,12 +57,24 @@ type FilterColumnOperatorsCore<TColumn extends Column, TColType = GetColumnGqlDa
57
57
  notIlike: string;
58
58
  startsWith: string;
59
59
  endsWith: string;
60
- contains: string;
60
+ /**
61
+ * String columns: safe substring match (`%`, `_` and `\` matched literally).
62
+ * JSON columns: structural containment (Postgres `@>` / MySQL JSON_CONTAINS).
63
+ */
64
+ contains: string | TColType;
61
65
  iStartsWith: string;
62
66
  iEndsWith: string;
63
67
  iContains: string;
64
68
  inArray: Array<TColType>;
65
69
  notInArray: Array<TColType>;
70
+ /** Array columns only: the array contains this element. */
71
+ has: TColType extends Array<infer TElement> ? TElement : never;
72
+ /** Array columns only: the array overlaps these elements (`&&`). */
73
+ hasSome: TColType extends Array<infer TElement> ? Array<TElement> : never;
74
+ /** Array columns only: the array contains all of these elements (`@>`). */
75
+ hasEvery: TColType extends Array<infer TElement> ? Array<TElement> : never;
76
+ /** Array columns only: when true, matches arrays with no elements. */
77
+ isEmpty: boolean;
66
78
  isNull: boolean;
67
79
  isNotNull: boolean;
68
80
  }>;
package/dist/index.js CHANGED
@@ -12,6 +12,8 @@ import {
12
12
  import {
13
13
  aliasedTable,
14
14
  and,
15
+ arrayContains,
16
+ arrayOverlaps,
15
17
  asc,
16
18
  desc,
17
19
  eq,
@@ -416,6 +418,21 @@ var geoXyInputType = new GraphQLInputObjectType({
416
418
  }
417
419
  });
418
420
  var columnToGraphQLCore = (column, columnName, tableName, isInput) => {
421
+ const dimensions = column.dimensions;
422
+ if (dimensions !== void 0 && dimensions > 0) {
423
+ const inner = columnBaseToGraphQLCore(column, columnName, tableName, isInput);
424
+ const innerDesc = inner.description ?? inner.type.name;
425
+ let type = inner.type;
426
+ let description = innerDesc;
427
+ for (let i = 0; i < dimensions; i++) {
428
+ type = new GraphQLList(new GraphQLNonNull(type));
429
+ description = `Array<${description}>`;
430
+ }
431
+ return { type, description };
432
+ }
433
+ return columnBaseToGraphQLCore(column, columnName, tableName, isInput);
434
+ };
435
+ var columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
419
436
  const { type: baseType, constraint } = extractExtendedColumnType(column);
420
437
  switch (baseType) {
421
438
  case "boolean":
@@ -447,28 +464,10 @@ var columnToGraphQLCore = (column, columnName, tableName, isInput) => {
447
464
  if (column instanceof PgDateString) {
448
465
  return isInput ? { type: GraphQLString, description: "Date" } : { type: GraphQLDate, description: "Date" };
449
466
  }
450
- {
451
- const dims = column.dimensions;
452
- if (dims !== void 0 && dims > 0) {
453
- return {
454
- type: new GraphQLList(new GraphQLNonNull(GraphQLString)),
455
- description: "Array<String>"
456
- };
457
- }
458
- }
459
467
  return { type: GraphQLString, description: "String" };
460
468
  case "bigint":
461
469
  return { type: GraphQLBigIntString, description: "BigInt" };
462
470
  case "number": {
463
- const dims = column.dimensions;
464
- if (dims !== void 0 && dims > 0) {
465
- const baseDesc = is2(column, PgInteger) || is2(column, PgSerial) ? "Integer" : "Float";
466
- const baseType2 = baseDesc === "Integer" ? GraphQLInt : GraphQLFloat;
467
- return {
468
- type: new GraphQLList(new GraphQLNonNull(baseType2)),
469
- description: `Array<${baseDesc}>`
470
- };
471
- }
472
471
  return is2(column, PgInteger) || is2(column, PgSerial) || is2(column, MySqlInt) || is2(column, MySqlSerial) || is2(column, SQLiteInteger) ? { type: GraphQLInt, description: "Integer" } : { type: GraphQLFloat, description: "Float" };
473
472
  }
474
473
  case "array": {
@@ -813,52 +812,95 @@ var innerOrder = new GraphQLInputObjectType2({
813
812
  }
814
813
  }
815
814
  });
816
- var resolveGenericFilterName = (column, columnGraphQLType) => {
815
+ var columnDialect = (column) => {
816
+ const ct = column.columnType ?? "";
817
+ if (ct.startsWith("Pg")) {
818
+ return "pg";
819
+ }
820
+ if (ct.startsWith("MySql")) {
821
+ return "mysql";
822
+ }
823
+ if (ct.startsWith("SQLite")) {
824
+ return "sqlite";
825
+ }
826
+ return void 0;
827
+ };
828
+ var resolveGenericFilterDescriptor = (column, columnGraphQLType) => {
829
+ if (columnGraphQLType.type === GraphQLJSON) {
830
+ return { name: "JSON", kind: "json" };
831
+ }
832
+ if (columnGraphQLType.type instanceof GraphQLList2) {
833
+ let element = columnGraphQLType.type.ofType;
834
+ if (element instanceof GraphQLNonNull2) {
835
+ element = element.ofType;
836
+ }
837
+ return { name: `${element.name}Array`, kind: "array" };
838
+ }
817
839
  if (columnGraphQLType.type === GraphQLUUID) {
818
- return "Id";
840
+ return { name: "Id", kind: "scalar" };
819
841
  }
820
842
  if (columnGraphQLType.type === GraphQLBoolean2) {
821
- return "Boolean";
843
+ return { name: "Boolean", kind: "scalar" };
822
844
  }
823
845
  if (columnGraphQLType.type instanceof GraphQLEnumType2) {
824
- return columnGraphQLType.type.name;
846
+ return { name: columnGraphQLType.type.name, kind: "scalar" };
825
847
  }
826
848
  if (columnGraphQLType.type === GraphQLBigIntString) {
827
- return "BigInt";
849
+ return { name: "BigInt", kind: "scalar" };
828
850
  }
829
851
  if (columnGraphQLType.type === GraphQLDecimalString) {
830
- return "Decimal";
831
- }
832
- if (columnGraphQLType.type instanceof GraphQLList2) {
833
- const desc2 = columnGraphQLType.description ?? "";
834
- let inner = columnGraphQLType.type.ofType;
835
- if (inner instanceof GraphQLNonNull2) {
836
- inner = inner.ofType;
837
- }
838
- if (desc2.includes("Integer") || inner === GraphQLInt2) {
839
- return "IntArray";
840
- }
841
- if (desc2.includes("Float") || inner === GraphQLFloat2) {
842
- return "FloatArray";
843
- }
844
- return "StringArray";
852
+ return { name: "Decimal", kind: "scalar" };
845
853
  }
846
854
  const ct = column.columnType ?? "";
847
855
  if (ct === "PgTimestamp" || ct === "PgTimestampString" || ct === "PgDate") {
848
- return "DateTime";
856
+ return { name: "DateTime", kind: "scalar" };
849
857
  }
850
858
  if (columnGraphQLType.type === GraphQLInt2) {
851
- return "Int";
859
+ return { name: "Int", kind: "scalar" };
852
860
  }
853
861
  if (columnGraphQLType.type === GraphQLFloat2) {
854
- return "Float";
862
+ return { name: "Float", kind: "scalar" };
855
863
  }
856
- return "String";
864
+ return { name: "String", kind: "scalar" };
865
+ };
866
+ var jsonFilterFields = (column, colType) => {
867
+ const dialect = columnDialect(column);
868
+ return {
869
+ eq: { type: colType, description: "JSON equality on the whole value" },
870
+ ne: { type: colType, description: "JSON inequality on the whole value" },
871
+ ...dialect === "pg" || dialect === "mysql" ? {
872
+ contains: {
873
+ type: colType,
874
+ description: "Value structurally contains this JSON (Postgres `@>` / MySQL JSON_CONTAINS)"
875
+ }
876
+ } : {},
877
+ isNull: { type: GraphQLBoolean2 },
878
+ isNotNull: { type: GraphQLBoolean2 }
879
+ };
880
+ };
881
+ var arrayFilterFields = (colType, colArr, colDesc) => {
882
+ let element = colType.ofType;
883
+ if (element instanceof GraphQLNonNull2) {
884
+ element = element.ofType;
885
+ }
886
+ const elementList = new GraphQLList2(new GraphQLNonNull2(element));
887
+ return {
888
+ eq: { type: colType, description: colDesc },
889
+ ne: { type: colType, description: colDesc },
890
+ has: { type: element, description: "Array contains this element" },
891
+ hasSome: { type: elementList, description: "Array contains at least one of these elements (overlap, `&&`)" },
892
+ hasEvery: { type: elementList, description: "Array contains every one of these elements (containment, `@>`)" },
893
+ isEmpty: { type: GraphQLBoolean2, description: "When true, matches arrays with no elements" },
894
+ inArray: { type: colArr, description: `Array<${colDesc}>` },
895
+ notInArray: { type: colArr, description: `Array<${colDesc}>` },
896
+ isNull: { type: GraphQLBoolean2 },
897
+ isNotNull: { type: GraphQLBoolean2 }
898
+ };
857
899
  };
858
900
  var FILTERS_WITHOUT_STRING_OPS = /* @__PURE__ */ new Set(["Id", "Int", "Float", "BigInt", "Decimal"]);
859
901
  var generateColumnFilterValues = (column, tableName, columnName, cacheCtx) => {
860
902
  const columnGraphQLType = drizzleColumnToGraphQLType(column, columnName, tableName, true, false, true);
861
- const genericName = resolveGenericFilterName(column, columnGraphQLType);
903
+ const { name: genericName, kind } = resolveGenericFilterDescriptor(column, columnGraphQLType);
862
904
  const cached = cacheCtx.genericFilterCache.get(genericName);
863
905
  if (cached) {
864
906
  return cached;
@@ -867,7 +909,7 @@ var generateColumnFilterValues = (column, tableName, columnName, cacheCtx) => {
867
909
  const colDesc = columnGraphQLType.description;
868
910
  const colArr = new GraphQLList2(new GraphQLNonNull2(colType));
869
911
  const omitStringOps = FILTERS_WITHOUT_STRING_OPS.has(genericName);
870
- const baseFields = {
912
+ const baseFields = kind === "json" ? jsonFilterFields(column, colType) : kind === "array" ? arrayFilterFields(colType, colArr, colDesc) : {
871
913
  eq: { type: colType, description: colDesc },
872
914
  ne: { type: colType, description: colDesc },
873
915
  lt: { type: colType, description: colDesc },
@@ -978,6 +1020,13 @@ var generateTableSelectTypeFieldsCached = (table, tableName) => {
978
1020
  drizzleColumnToGraphQLType(columnDescription, columnName, tableName)
979
1021
  ])
980
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
+ }
981
1030
  fieldMap.set(table, remapped);
982
1031
  return remapped;
983
1032
  };
@@ -1440,12 +1489,28 @@ var safeLikeCondition = (column, pattern, insensitive) => {
1440
1489
  const isPg = (column.columnType ?? "").startsWith("Pg");
1441
1490
  return isPg ? sql`${column} ilike ${pattern} escape ${LIKE_ESCAPE_CHAR}` : sql`lower(${column}) like ${pattern.toLowerCase()} escape ${LIKE_ESCAPE_CHAR}`;
1442
1491
  };
1492
+ var isJsonColumn2 = (column) => (column.columnType ?? "").includes("Json");
1493
+ var jsonContains = (column, columnName, value) => {
1494
+ const serialized = JSON.stringify(value);
1495
+ switch (columnDialect(column)) {
1496
+ case "pg":
1497
+ return column.columnType === "PgJson" ? sql`${column}::jsonb @> ${serialized}::jsonb` : sql`${column} @> ${serialized}::jsonb`;
1498
+ case "mysql":
1499
+ return sql`json_contains(${column}, ${serialized})`;
1500
+ default:
1501
+ throw new GraphQLError3(`WHERE ${columnName}: Operator 'contains' is not supported for this dialect!`);
1502
+ }
1503
+ };
1443
1504
  var extractFiltersColumn = (column, columnName, operators) => {
1444
1505
  const { OR, AND, NOT, ...restOperators } = operators;
1445
1506
  const entries = Object.entries(restOperators);
1446
1507
  const singleValueOps = { eq, ne, gt, gte, lt, lte };
1447
1508
  const stringValueOps = { like, notLike, ilike, notIlike };
1448
1509
  const arrayValueOps = { inArray, notInArray };
1510
+ const arrayMembershipOps = {
1511
+ hasSome: arrayOverlaps,
1512
+ hasEvery: arrayContains
1513
+ };
1449
1514
  const nullableOps = { isNull, isNotNull };
1450
1515
  const variants = [];
1451
1516
  for (const [operatorName, operatorValue] of entries) {
@@ -1457,6 +1522,8 @@ var extractFiltersColumn = (column, columnName, operators) => {
1457
1522
  variants.push(singleValueOps[operatorName](column, singleValue));
1458
1523
  } else if (operatorName in stringValueOps) {
1459
1524
  variants.push(stringValueOps[operatorName](column, operatorValue));
1525
+ } else if (operatorName === "contains" && isJsonColumn2(column)) {
1526
+ variants.push(jsonContains(column, columnName, operatorValue));
1460
1527
  } else if (operatorName in safeLikeOps) {
1461
1528
  const { buildPattern, insensitive } = safeLikeOps[operatorName];
1462
1529
  variants.push(safeLikeCondition(column, buildPattern(operatorValue), insensitive));
@@ -1466,6 +1533,15 @@ var extractFiltersColumn = (column, columnName, operators) => {
1466
1533
  }
1467
1534
  const arrayValue = operatorValue.map((val) => remapFromGraphQLCore(val, column, columnName));
1468
1535
  variants.push(arrayValueOps[operatorName](column, arrayValue));
1536
+ } else if (operatorName === "has") {
1537
+ variants.push(arrayContains(column, [operatorValue]));
1538
+ } else if (operatorName in arrayMembershipOps) {
1539
+ if (!operatorValue.length) {
1540
+ throw new GraphQLError3(`WHERE ${columnName}: Unable to use operator ${operatorName} with an empty array!`);
1541
+ }
1542
+ variants.push(arrayMembershipOps[operatorName](column, operatorValue));
1543
+ } else if (operatorName === "isEmpty") {
1544
+ variants.push(sql`cardinality(${column}) = 0`);
1469
1545
  } else if (operatorName in nullableOps) {
1470
1546
  variants.push(nullableOps[operatorName](column));
1471
1547
  } else {
@@ -1778,6 +1854,117 @@ var primaryKeyOrderExprs = (table, pkNames) => {
1778
1854
  const cols = getColumns(table);
1779
1855
  return pkNames.map((n) => cols[n]).filter(Boolean).map((col) => asc(col));
1780
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;
1781
1968
  var getUniqueColumnSets = (table, getTableConfig4) => {
1782
1969
  const cols = getColumns(table);
1783
1970
  const propNameByColumnName = new Map(Object.entries(cols).map(([propName, col]) => [col.name, propName]));
@@ -2102,6 +2289,10 @@ var selectArrayArgs = (orderArgs, filterArgs, distinctEnum) => ({
2102
2289
  limit: { type: GraphQLInt2 },
2103
2290
  orderBy: { type: orderArgs },
2104
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
+ },
2105
2296
  ...distinctEnum ? { distinct: { type: new GraphQLList2(new GraphQLNonNull2(distinctEnum)) } } : {}
2106
2297
  });
2107
2298
  var selectSingleArgs = (orderArgs, filterArgs) => ({
@@ -2124,9 +2315,31 @@ var runRelationalSelect = async (opts) => {
2124
2315
  where,
2125
2316
  single,
2126
2317
  filterCtx,
2127
- pkNames
2318
+ pkNames,
2319
+ after
2128
2320
  } = opts;
2129
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;
2130
2343
  const needsDefaultOrder = single || offset != null || opts.limit != null;
2131
2344
  let distinctKeys;
2132
2345
  if (distinct) {
@@ -2157,9 +2370,21 @@ var runRelationalSelect = async (opts) => {
2157
2370
  orderBy: distinctKeys ? (aliasedTable2) => [
2158
2371
  ...orderBy ? extractOrderBy(aliasedTable2, orderBy, relationFilterCtx(filterCtx, tableName)) : [],
2159
2372
  ...primaryKeyOrderExprs(aliasedTable2, pkNames)
2160
- ] : orderBy ? (aliasedTable2) => extractOrderBy(aliasedTable2, orderBy, relationFilterCtx(filterCtx, tableName)) : needsDefaultOrder && pkNames?.length ? (aliasedTable2) => primaryKeyOrderExprs(aliasedTable2, pkNames) : void 0,
2161
- where: distinctKeys ? { RAW: (aliasedTable2) => primaryKeyRestriction(aliasedTable2, pkNames, distinctKeys) } : where ? {
2162
- 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
+ )
2163
2388
  } : void 0,
2164
2389
  with: relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, typeNameMapper, filterCtx) : void 0
2165
2390
  };
@@ -2167,8 +2392,16 @@ var runRelationalSelect = async (opts) => {
2167
2392
  const result2 = await queryBase.findFirst(params);
2168
2393
  return result2 ? remapToGraphQLSingleOutput(result2, tableName, table, relationMap) : void 0;
2169
2394
  }
2395
+ if (cursorEntries && cursorSelected) {
2396
+ for (const [column] of cursorEntries) {
2397
+ params.columns[column] = true;
2398
+ }
2399
+ }
2170
2400
  params.limit = distinctKeys ? void 0 : opts.limit;
2171
2401
  const result = await queryBase.findMany(params);
2402
+ if (cursorEntries && cursorSelected) {
2403
+ attachRowCursors(result, cursorEntries);
2404
+ }
2172
2405
  return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
2173
2406
  };
2174
2407
  var eagerLoadMutationRelations = async (db, tableName, rows, pkNames, withParams) => {
@@ -2747,7 +2980,9 @@ var generateSelectArray = (db, tableName, tables, relationMap, orderArgs, filter
2747
2980
  single: false,
2748
2981
  filterCtx,
2749
2982
  pkNames,
2750
- db: executor
2983
+ db: executor,
2984
+ // MySQL sorts NULLs as the smallest values (first in ASC).
2985
+ nullOrdering: "nulls-smallest"
2751
2986
  });
2752
2987
  } catch (e) {
2753
2988
  throw toGraphQLError(e);
@@ -3212,7 +3447,7 @@ var generateSchemaData = (db, schema, relations, options) => {
3212
3447
  };
3213
3448
 
3214
3449
  // src/util/builders/pg.ts
3215
- 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";
3216
3451
  import { getTableConfig as getTableConfig2, PgTable } from "drizzle-orm/pg-core";
3217
3452
  import {
3218
3453
  GraphQLError as GraphQLError6,
@@ -3249,16 +3484,48 @@ var generateSelectArray2 = (db, tableName, tables, relationMap, orderArgs, filte
3249
3484
  single: false,
3250
3485
  filterCtx,
3251
3486
  pkNames,
3252
- db: executor
3487
+ db: executor,
3488
+ // PostgreSQL sorts NULLs as the largest values (last in ASC).
3489
+ nullOrdering: "nulls-largest"
3253
3490
  });
3254
3491
  }
3255
- const { offset, limit, orderBy, where, distinct } = args;
3492
+ const { offset, limit, orderBy, where, distinct, after } = args;
3256
3493
  const selectedColumnsSql = extractSelectedColumnsFromTreeSQLFormat(
3257
3494
  parsedInfo.fieldsByTypeName[typeName],
3258
3495
  table,
3259
3496
  { tableName, relationMap, tables }
3260
3497
  );
3261
- 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;
3262
3529
  let distinctKeys;
3263
3530
  if (distinct?.length) {
3264
3531
  distinctKeys = await selectDistinctKeys({
@@ -3282,7 +3549,9 @@ var generateSelectArray2 = (db, tableName, tables, relationMap, orderArgs, filte
3282
3549
  } else if (whereSql) {
3283
3550
  q = q.where(whereSql);
3284
3551
  }
3285
- if (orderBy) {
3552
+ if (cursorEntries && !distinctKeys) {
3553
+ q = q.orderBy(...cursorOrderExprs(table, cursorEntries));
3554
+ } else if (orderBy) {
3286
3555
  q = q.orderBy(
3287
3556
  ...extractOrderBy(table, orderBy, relationFilterCtx(filterCtx, tableName)),
3288
3557
  ...distinctKeys ? primaryKeyOrderExprs(table, pkNames) : []
@@ -3298,7 +3567,11 @@ var generateSelectArray2 = (db, tableName, tables, relationMap, orderArgs, filte
3298
3567
  q = q.limit(limit);
3299
3568
  }
3300
3569
  }
3301
- 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);
3302
3575
  } catch (e) {
3303
3576
  throw toGraphQLError(e);
3304
3577
  }
@@ -4014,7 +4287,9 @@ var generateSelectArray3 = (db, tableName, tables, relationMap, orderArgs, filte
4014
4287
  single: false,
4015
4288
  filterCtx,
4016
4289
  pkNames,
4017
- db: executor
4290
+ db: executor,
4291
+ // SQLite sorts NULLs as the smallest values (first in ASC).
4292
+ nullOrdering: "nulls-smallest"
4018
4293
  });
4019
4294
  } catch (e) {
4020
4295
  throw toGraphQLError(e);