@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/README.md +47 -0
- package/dist/README.md +47 -0
- package/dist/index.cjs +328 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +331 -56
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -405,6 +405,53 @@ supplies no `orderBy`:
|
|
|
405
405
|
An explicit `orderBy` always takes precedence, composite primary keys are ordered by every
|
|
406
406
|
key column, and an unpaginated list query is left unordered so no sort is paid for.
|
|
407
407
|
|
|
408
|
+
## Cursor pagination
|
|
409
|
+
|
|
410
|
+
`OFFSET` pagination degrades linearly and shifts under concurrent writes. List queries also
|
|
411
|
+
support keyset pagination: every row returned by a list query exposes an opaque `cursor`
|
|
412
|
+
field, and passing it back as `after` resumes strictly after that row.
|
|
413
|
+
|
|
414
|
+
```graphql
|
|
415
|
+
{
|
|
416
|
+
posts(orderBy: { createdAt: { direction: desc, priority: 1 } }, limit: 10) {
|
|
417
|
+
id
|
|
418
|
+
createdAt
|
|
419
|
+
cursor
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
# next page — same orderBy, plus the last row's cursor
|
|
424
|
+
{
|
|
425
|
+
posts(
|
|
426
|
+
orderBy: { createdAt: { direction: desc, priority: 1 } }
|
|
427
|
+
limit: 10
|
|
428
|
+
after: "eyJvIjogW1siY3JlYXRlZEF0IiwgImRlc2MiXSwgWyJpZCIsICJhc2MiXV0sIC4uLn0"
|
|
429
|
+
) {
|
|
430
|
+
id
|
|
431
|
+
createdAt
|
|
432
|
+
cursor
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
- The cursor encodes the row's position in the query's **total order** — your `orderBy`
|
|
438
|
+
columns plus the primary key as an ascending tiebreak — so pages are stable even when
|
|
439
|
+
the ordered columns aren't unique, and inserts or deletes mid-scroll don't shift the
|
|
440
|
+
window
|
|
441
|
+
- `after` compiles to a keyset predicate built from drizzle's `and`/`or`/`gt`/`lt`
|
|
442
|
+
comparisons (not SQL row-value syntax), so mixed-direction `orderBy` works
|
|
443
|
+
- `NULL`s in ordered columns follow each dialect's native `ORDER BY` placement —
|
|
444
|
+
PostgreSQL sorts them largest (last in `asc`), MySQL and SQLite smallest (first in
|
|
445
|
+
`asc`) — and the predicate matches, so rows with `NULL` in an ordered column are paged
|
|
446
|
+
through, not skipped
|
|
447
|
+
- Dates and bigints round-trip losslessly through the cursor
|
|
448
|
+
- A cursor is only valid under the ordering it was issued for — reusing it with a
|
|
449
|
+
different `orderBy` (or a malformed/corrupted cursor) returns a `GraphQLError`
|
|
450
|
+
- `after` cannot be combined with `distinct`, and a table with no primary key cannot use
|
|
451
|
+
cursor pagination (its `cursor` field resolves to `null`)
|
|
452
|
+
- If a table has a real column named `cursor`, the column wins and the meta field is
|
|
453
|
+
skipped
|
|
454
|
+
|
|
408
455
|
## Upsert
|
|
409
456
|
|
|
410
457
|
`features.upsert` adds a pair of mutations per table that insert rows, or update the ones
|
package/dist/README.md
CHANGED
|
@@ -405,6 +405,53 @@ supplies no `orderBy`:
|
|
|
405
405
|
An explicit `orderBy` always takes precedence, composite primary keys are ordered by every
|
|
406
406
|
key column, and an unpaginated list query is left unordered so no sort is paid for.
|
|
407
407
|
|
|
408
|
+
## Cursor pagination
|
|
409
|
+
|
|
410
|
+
`OFFSET` pagination degrades linearly and shifts under concurrent writes. List queries also
|
|
411
|
+
support keyset pagination: every row returned by a list query exposes an opaque `cursor`
|
|
412
|
+
field, and passing it back as `after` resumes strictly after that row.
|
|
413
|
+
|
|
414
|
+
```graphql
|
|
415
|
+
{
|
|
416
|
+
posts(orderBy: { createdAt: { direction: desc, priority: 1 } }, limit: 10) {
|
|
417
|
+
id
|
|
418
|
+
createdAt
|
|
419
|
+
cursor
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
# next page — same orderBy, plus the last row's cursor
|
|
424
|
+
{
|
|
425
|
+
posts(
|
|
426
|
+
orderBy: { createdAt: { direction: desc, priority: 1 } }
|
|
427
|
+
limit: 10
|
|
428
|
+
after: "eyJvIjogW1siY3JlYXRlZEF0IiwgImRlc2MiXSwgWyJpZCIsICJhc2MiXV0sIC4uLn0"
|
|
429
|
+
) {
|
|
430
|
+
id
|
|
431
|
+
createdAt
|
|
432
|
+
cursor
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
- The cursor encodes the row's position in the query's **total order** — your `orderBy`
|
|
438
|
+
columns plus the primary key as an ascending tiebreak — so pages are stable even when
|
|
439
|
+
the ordered columns aren't unique, and inserts or deletes mid-scroll don't shift the
|
|
440
|
+
window
|
|
441
|
+
- `after` compiles to a keyset predicate built from drizzle's `and`/`or`/`gt`/`lt`
|
|
442
|
+
comparisons (not SQL row-value syntax), so mixed-direction `orderBy` works
|
|
443
|
+
- `NULL`s in ordered columns follow each dialect's native `ORDER BY` placement —
|
|
444
|
+
PostgreSQL sorts them largest (last in `asc`), MySQL and SQLite smallest (first in
|
|
445
|
+
`asc`) — and the predicate matches, so rows with `NULL` in an ordered column are paged
|
|
446
|
+
through, not skipped
|
|
447
|
+
- Dates and bigints round-trip losslessly through the cursor
|
|
448
|
+
- A cursor is only valid under the ordering it was issued for — reusing it with a
|
|
449
|
+
different `orderBy` (or a malformed/corrupted cursor) returns a `GraphQLError`
|
|
450
|
+
- `after` cannot be combined with `distinct`, and a table with no primary key cannot use
|
|
451
|
+
cursor pagination (its `cursor` field resolves to `null`)
|
|
452
|
+
- If a table has a real column named `cursor`, the column wins and the meta field is
|
|
453
|
+
skipped
|
|
454
|
+
|
|
408
455
|
## Upsert
|
|
409
456
|
|
|
410
457
|
`features.upsert` adds a pair of mutations per table that insert rows, or update the ones
|
package/dist/index.cjs
CHANGED
|
@@ -411,6 +411,21 @@ var geoXyInputType = new import_graphql3.GraphQLInputObjectType({
|
|
|
411
411
|
}
|
|
412
412
|
});
|
|
413
413
|
var columnToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
414
|
+
const dimensions = column.dimensions;
|
|
415
|
+
if (dimensions !== void 0 && dimensions > 0) {
|
|
416
|
+
const inner = columnBaseToGraphQLCore(column, columnName, tableName, isInput);
|
|
417
|
+
const innerDesc = inner.description ?? inner.type.name;
|
|
418
|
+
let type = inner.type;
|
|
419
|
+
let description = innerDesc;
|
|
420
|
+
for (let i = 0; i < dimensions; i++) {
|
|
421
|
+
type = new import_graphql3.GraphQLList(new import_graphql3.GraphQLNonNull(type));
|
|
422
|
+
description = `Array<${description}>`;
|
|
423
|
+
}
|
|
424
|
+
return { type, description };
|
|
425
|
+
}
|
|
426
|
+
return columnBaseToGraphQLCore(column, columnName, tableName, isInput);
|
|
427
|
+
};
|
|
428
|
+
var columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
414
429
|
const { type: baseType, constraint } = (0, import_drizzle_orm2.extractExtendedColumnType)(column);
|
|
415
430
|
switch (baseType) {
|
|
416
431
|
case "boolean":
|
|
@@ -442,28 +457,10 @@ var columnToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
442
457
|
if (column instanceof import_pg_core.PgDateString) {
|
|
443
458
|
return isInput ? { type: import_graphql3.GraphQLString, description: "Date" } : { type: import_graphql_scalars.GraphQLDate, description: "Date" };
|
|
444
459
|
}
|
|
445
|
-
{
|
|
446
|
-
const dims = column.dimensions;
|
|
447
|
-
if (dims !== void 0 && dims > 0) {
|
|
448
|
-
return {
|
|
449
|
-
type: new import_graphql3.GraphQLList(new import_graphql3.GraphQLNonNull(import_graphql3.GraphQLString)),
|
|
450
|
-
description: "Array<String>"
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
460
|
return { type: import_graphql3.GraphQLString, description: "String" };
|
|
455
461
|
case "bigint":
|
|
456
462
|
return { type: GraphQLBigIntString, description: "BigInt" };
|
|
457
463
|
case "number": {
|
|
458
|
-
const dims = column.dimensions;
|
|
459
|
-
if (dims !== void 0 && dims > 0) {
|
|
460
|
-
const baseDesc = (0, import_drizzle_orm2.is)(column, import_pg_core.PgInteger) || (0, import_drizzle_orm2.is)(column, import_pg_core.PgSerial) ? "Integer" : "Float";
|
|
461
|
-
const baseType2 = baseDesc === "Integer" ? import_graphql3.GraphQLInt : import_graphql3.GraphQLFloat;
|
|
462
|
-
return {
|
|
463
|
-
type: new import_graphql3.GraphQLList(new import_graphql3.GraphQLNonNull(baseType2)),
|
|
464
|
-
description: `Array<${baseDesc}>`
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
464
|
return (0, import_drizzle_orm2.is)(column, import_pg_core.PgInteger) || (0, import_drizzle_orm2.is)(column, import_pg_core.PgSerial) || (0, import_drizzle_orm2.is)(column, import_mysql_core.MySqlInt) || (0, import_drizzle_orm2.is)(column, import_mysql_core.MySqlSerial) || (0, import_drizzle_orm2.is)(column, import_sqlite_core.SQLiteInteger) ? { type: import_graphql3.GraphQLInt, description: "Integer" } : { type: import_graphql3.GraphQLFloat, description: "Float" };
|
|
468
465
|
}
|
|
469
466
|
case "array": {
|
|
@@ -808,52 +805,95 @@ var innerOrder = new import_graphql4.GraphQLInputObjectType({
|
|
|
808
805
|
}
|
|
809
806
|
}
|
|
810
807
|
});
|
|
811
|
-
var
|
|
808
|
+
var columnDialect = (column) => {
|
|
809
|
+
const ct = column.columnType ?? "";
|
|
810
|
+
if (ct.startsWith("Pg")) {
|
|
811
|
+
return "pg";
|
|
812
|
+
}
|
|
813
|
+
if (ct.startsWith("MySql")) {
|
|
814
|
+
return "mysql";
|
|
815
|
+
}
|
|
816
|
+
if (ct.startsWith("SQLite")) {
|
|
817
|
+
return "sqlite";
|
|
818
|
+
}
|
|
819
|
+
return void 0;
|
|
820
|
+
};
|
|
821
|
+
var resolveGenericFilterDescriptor = (column, columnGraphQLType) => {
|
|
822
|
+
if (columnGraphQLType.type === import_graphql_scalars.GraphQLJSON) {
|
|
823
|
+
return { name: "JSON", kind: "json" };
|
|
824
|
+
}
|
|
825
|
+
if (columnGraphQLType.type instanceof import_graphql4.GraphQLList) {
|
|
826
|
+
let element = columnGraphQLType.type.ofType;
|
|
827
|
+
if (element instanceof import_graphql4.GraphQLNonNull) {
|
|
828
|
+
element = element.ofType;
|
|
829
|
+
}
|
|
830
|
+
return { name: `${element.name}Array`, kind: "array" };
|
|
831
|
+
}
|
|
812
832
|
if (columnGraphQLType.type === import_graphql_scalars.GraphQLUUID) {
|
|
813
|
-
return "Id";
|
|
833
|
+
return { name: "Id", kind: "scalar" };
|
|
814
834
|
}
|
|
815
835
|
if (columnGraphQLType.type === import_graphql4.GraphQLBoolean) {
|
|
816
|
-
return "Boolean";
|
|
836
|
+
return { name: "Boolean", kind: "scalar" };
|
|
817
837
|
}
|
|
818
838
|
if (columnGraphQLType.type instanceof import_graphql4.GraphQLEnumType) {
|
|
819
|
-
return columnGraphQLType.type.name;
|
|
839
|
+
return { name: columnGraphQLType.type.name, kind: "scalar" };
|
|
820
840
|
}
|
|
821
841
|
if (columnGraphQLType.type === GraphQLBigIntString) {
|
|
822
|
-
return "BigInt";
|
|
842
|
+
return { name: "BigInt", kind: "scalar" };
|
|
823
843
|
}
|
|
824
844
|
if (columnGraphQLType.type === GraphQLDecimalString) {
|
|
825
|
-
return "Decimal";
|
|
826
|
-
}
|
|
827
|
-
if (columnGraphQLType.type instanceof import_graphql4.GraphQLList) {
|
|
828
|
-
const desc2 = columnGraphQLType.description ?? "";
|
|
829
|
-
let inner = columnGraphQLType.type.ofType;
|
|
830
|
-
if (inner instanceof import_graphql4.GraphQLNonNull) {
|
|
831
|
-
inner = inner.ofType;
|
|
832
|
-
}
|
|
833
|
-
if (desc2.includes("Integer") || inner === import_graphql4.GraphQLInt) {
|
|
834
|
-
return "IntArray";
|
|
835
|
-
}
|
|
836
|
-
if (desc2.includes("Float") || inner === import_graphql4.GraphQLFloat) {
|
|
837
|
-
return "FloatArray";
|
|
838
|
-
}
|
|
839
|
-
return "StringArray";
|
|
845
|
+
return { name: "Decimal", kind: "scalar" };
|
|
840
846
|
}
|
|
841
847
|
const ct = column.columnType ?? "";
|
|
842
848
|
if (ct === "PgTimestamp" || ct === "PgTimestampString" || ct === "PgDate") {
|
|
843
|
-
return "DateTime";
|
|
849
|
+
return { name: "DateTime", kind: "scalar" };
|
|
844
850
|
}
|
|
845
851
|
if (columnGraphQLType.type === import_graphql4.GraphQLInt) {
|
|
846
|
-
return "Int";
|
|
852
|
+
return { name: "Int", kind: "scalar" };
|
|
847
853
|
}
|
|
848
854
|
if (columnGraphQLType.type === import_graphql4.GraphQLFloat) {
|
|
849
|
-
return "Float";
|
|
855
|
+
return { name: "Float", kind: "scalar" };
|
|
850
856
|
}
|
|
851
|
-
return "String";
|
|
857
|
+
return { name: "String", kind: "scalar" };
|
|
858
|
+
};
|
|
859
|
+
var jsonFilterFields = (column, colType) => {
|
|
860
|
+
const dialect = columnDialect(column);
|
|
861
|
+
return {
|
|
862
|
+
eq: { type: colType, description: "JSON equality on the whole value" },
|
|
863
|
+
ne: { type: colType, description: "JSON inequality on the whole value" },
|
|
864
|
+
...dialect === "pg" || dialect === "mysql" ? {
|
|
865
|
+
contains: {
|
|
866
|
+
type: colType,
|
|
867
|
+
description: "Value structurally contains this JSON (Postgres `@>` / MySQL JSON_CONTAINS)"
|
|
868
|
+
}
|
|
869
|
+
} : {},
|
|
870
|
+
isNull: { type: import_graphql4.GraphQLBoolean },
|
|
871
|
+
isNotNull: { type: import_graphql4.GraphQLBoolean }
|
|
872
|
+
};
|
|
873
|
+
};
|
|
874
|
+
var arrayFilterFields = (colType, colArr, colDesc) => {
|
|
875
|
+
let element = colType.ofType;
|
|
876
|
+
if (element instanceof import_graphql4.GraphQLNonNull) {
|
|
877
|
+
element = element.ofType;
|
|
878
|
+
}
|
|
879
|
+
const elementList = new import_graphql4.GraphQLList(new import_graphql4.GraphQLNonNull(element));
|
|
880
|
+
return {
|
|
881
|
+
eq: { type: colType, description: colDesc },
|
|
882
|
+
ne: { type: colType, description: colDesc },
|
|
883
|
+
has: { type: element, description: "Array contains this element" },
|
|
884
|
+
hasSome: { type: elementList, description: "Array contains at least one of these elements (overlap, `&&`)" },
|
|
885
|
+
hasEvery: { type: elementList, description: "Array contains every one of these elements (containment, `@>`)" },
|
|
886
|
+
isEmpty: { type: import_graphql4.GraphQLBoolean, description: "When true, matches arrays with no elements" },
|
|
887
|
+
inArray: { type: colArr, description: `Array<${colDesc}>` },
|
|
888
|
+
notInArray: { type: colArr, description: `Array<${colDesc}>` },
|
|
889
|
+
isNull: { type: import_graphql4.GraphQLBoolean },
|
|
890
|
+
isNotNull: { type: import_graphql4.GraphQLBoolean }
|
|
891
|
+
};
|
|
852
892
|
};
|
|
853
893
|
var FILTERS_WITHOUT_STRING_OPS = /* @__PURE__ */ new Set(["Id", "Int", "Float", "BigInt", "Decimal"]);
|
|
854
894
|
var generateColumnFilterValues = (column, tableName, columnName, cacheCtx) => {
|
|
855
895
|
const columnGraphQLType = drizzleColumnToGraphQLType(column, columnName, tableName, true, false, true);
|
|
856
|
-
const genericName =
|
|
896
|
+
const { name: genericName, kind } = resolveGenericFilterDescriptor(column, columnGraphQLType);
|
|
857
897
|
const cached = cacheCtx.genericFilterCache.get(genericName);
|
|
858
898
|
if (cached) {
|
|
859
899
|
return cached;
|
|
@@ -862,7 +902,7 @@ var generateColumnFilterValues = (column, tableName, columnName, cacheCtx) => {
|
|
|
862
902
|
const colDesc = columnGraphQLType.description;
|
|
863
903
|
const colArr = new import_graphql4.GraphQLList(new import_graphql4.GraphQLNonNull(colType));
|
|
864
904
|
const omitStringOps = FILTERS_WITHOUT_STRING_OPS.has(genericName);
|
|
865
|
-
const baseFields = {
|
|
905
|
+
const baseFields = kind === "json" ? jsonFilterFields(column, colType) : kind === "array" ? arrayFilterFields(colType, colArr, colDesc) : {
|
|
866
906
|
eq: { type: colType, description: colDesc },
|
|
867
907
|
ne: { type: colType, description: colDesc },
|
|
868
908
|
lt: { type: colType, description: colDesc },
|
|
@@ -973,6 +1013,13 @@ var generateTableSelectTypeFieldsCached = (table, tableName) => {
|
|
|
973
1013
|
drizzleColumnToGraphQLType(columnDescription, columnName, tableName)
|
|
974
1014
|
])
|
|
975
1015
|
);
|
|
1016
|
+
if (!remapped[CURSOR_FIELD_NAME]) {
|
|
1017
|
+
remapped[CURSOR_FIELD_NAME] = {
|
|
1018
|
+
type: import_graphql4.GraphQLString,
|
|
1019
|
+
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.",
|
|
1020
|
+
resolve: rowCursorResolver
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
976
1023
|
fieldMap.set(table, remapped);
|
|
977
1024
|
return remapped;
|
|
978
1025
|
};
|
|
@@ -1435,12 +1482,28 @@ var safeLikeCondition = (column, pattern, insensitive) => {
|
|
|
1435
1482
|
const isPg = (column.columnType ?? "").startsWith("Pg");
|
|
1436
1483
|
return isPg ? import_drizzle_orm3.sql`${column} ilike ${pattern} escape ${LIKE_ESCAPE_CHAR}` : import_drizzle_orm3.sql`lower(${column}) like ${pattern.toLowerCase()} escape ${LIKE_ESCAPE_CHAR}`;
|
|
1437
1484
|
};
|
|
1485
|
+
var isJsonColumn2 = (column) => (column.columnType ?? "").includes("Json");
|
|
1486
|
+
var jsonContains = (column, columnName, value) => {
|
|
1487
|
+
const serialized = JSON.stringify(value);
|
|
1488
|
+
switch (columnDialect(column)) {
|
|
1489
|
+
case "pg":
|
|
1490
|
+
return column.columnType === "PgJson" ? import_drizzle_orm3.sql`${column}::jsonb @> ${serialized}::jsonb` : import_drizzle_orm3.sql`${column} @> ${serialized}::jsonb`;
|
|
1491
|
+
case "mysql":
|
|
1492
|
+
return import_drizzle_orm3.sql`json_contains(${column}, ${serialized})`;
|
|
1493
|
+
default:
|
|
1494
|
+
throw new import_graphql4.GraphQLError(`WHERE ${columnName}: Operator 'contains' is not supported for this dialect!`);
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1438
1497
|
var extractFiltersColumn = (column, columnName, operators) => {
|
|
1439
1498
|
const { OR, AND, NOT, ...restOperators } = operators;
|
|
1440
1499
|
const entries = Object.entries(restOperators);
|
|
1441
1500
|
const singleValueOps = { eq: import_drizzle_orm3.eq, ne: import_drizzle_orm3.ne, gt: import_drizzle_orm3.gt, gte: import_drizzle_orm3.gte, lt: import_drizzle_orm3.lt, lte: import_drizzle_orm3.lte };
|
|
1442
1501
|
const stringValueOps = { like: import_drizzle_orm3.like, notLike: import_drizzle_orm3.notLike, ilike: import_drizzle_orm3.ilike, notIlike: import_drizzle_orm3.notIlike };
|
|
1443
1502
|
const arrayValueOps = { inArray: import_drizzle_orm3.inArray, notInArray: import_drizzle_orm3.notInArray };
|
|
1503
|
+
const arrayMembershipOps = {
|
|
1504
|
+
hasSome: import_drizzle_orm3.arrayOverlaps,
|
|
1505
|
+
hasEvery: import_drizzle_orm3.arrayContains
|
|
1506
|
+
};
|
|
1444
1507
|
const nullableOps = { isNull: import_drizzle_orm3.isNull, isNotNull: import_drizzle_orm3.isNotNull };
|
|
1445
1508
|
const variants = [];
|
|
1446
1509
|
for (const [operatorName, operatorValue] of entries) {
|
|
@@ -1452,6 +1515,8 @@ var extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1452
1515
|
variants.push(singleValueOps[operatorName](column, singleValue));
|
|
1453
1516
|
} else if (operatorName in stringValueOps) {
|
|
1454
1517
|
variants.push(stringValueOps[operatorName](column, operatorValue));
|
|
1518
|
+
} else if (operatorName === "contains" && isJsonColumn2(column)) {
|
|
1519
|
+
variants.push(jsonContains(column, columnName, operatorValue));
|
|
1455
1520
|
} else if (operatorName in safeLikeOps) {
|
|
1456
1521
|
const { buildPattern, insensitive } = safeLikeOps[operatorName];
|
|
1457
1522
|
variants.push(safeLikeCondition(column, buildPattern(operatorValue), insensitive));
|
|
@@ -1461,6 +1526,15 @@ var extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1461
1526
|
}
|
|
1462
1527
|
const arrayValue = operatorValue.map((val) => remapFromGraphQLCore(val, column, columnName));
|
|
1463
1528
|
variants.push(arrayValueOps[operatorName](column, arrayValue));
|
|
1529
|
+
} else if (operatorName === "has") {
|
|
1530
|
+
variants.push((0, import_drizzle_orm3.arrayContains)(column, [operatorValue]));
|
|
1531
|
+
} else if (operatorName in arrayMembershipOps) {
|
|
1532
|
+
if (!operatorValue.length) {
|
|
1533
|
+
throw new import_graphql4.GraphQLError(`WHERE ${columnName}: Unable to use operator ${operatorName} with an empty array!`);
|
|
1534
|
+
}
|
|
1535
|
+
variants.push(arrayMembershipOps[operatorName](column, operatorValue));
|
|
1536
|
+
} else if (operatorName === "isEmpty") {
|
|
1537
|
+
variants.push(import_drizzle_orm3.sql`cardinality(${column}) = 0`);
|
|
1464
1538
|
} else if (operatorName in nullableOps) {
|
|
1465
1539
|
variants.push(nullableOps[operatorName](column));
|
|
1466
1540
|
} else {
|
|
@@ -1773,6 +1847,117 @@ var primaryKeyOrderExprs = (table, pkNames) => {
|
|
|
1773
1847
|
const cols = (0, import_drizzle_orm3.getColumns)(table);
|
|
1774
1848
|
return pkNames.map((n) => cols[n]).filter(Boolean).map((col) => (0, import_drizzle_orm3.asc)(col));
|
|
1775
1849
|
};
|
|
1850
|
+
var CURSOR_FIELD_NAME = "cursor";
|
|
1851
|
+
var ROW_CURSOR_PROP = "__drizzle_graphql_cursor";
|
|
1852
|
+
var orderByHasRelationEntry = (orderBy) => !!orderBy && Object.values(orderBy).some((config) => config && typeof config === "object" && config.direction === void 0);
|
|
1853
|
+
var cursorOrderingEntries = (orderBy, pkNames) => {
|
|
1854
|
+
const entries = orderBy ? orderByEntries(orderBy) : [];
|
|
1855
|
+
const seen = new Set(entries.map(([column]) => column));
|
|
1856
|
+
for (const pk of pkNames) {
|
|
1857
|
+
if (!seen.has(pk)) {
|
|
1858
|
+
entries.push([pk, "asc"]);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
return entries;
|
|
1862
|
+
};
|
|
1863
|
+
var cursorOrderExprs = (table, entries) => {
|
|
1864
|
+
const cols = (0, import_drizzle_orm3.getColumns)(table);
|
|
1865
|
+
return entries.flatMap(
|
|
1866
|
+
([column, direction, nulls]) => orderExpressions(cols[column], direction, nulls ?? void 0, cols[column])
|
|
1867
|
+
);
|
|
1868
|
+
};
|
|
1869
|
+
var encodeCursorValue = (value) => {
|
|
1870
|
+
if (value instanceof Date) {
|
|
1871
|
+
return { $type: "date", value: value.toISOString() };
|
|
1872
|
+
}
|
|
1873
|
+
if (typeof value === "bigint") {
|
|
1874
|
+
return { $type: "bigint", value: value.toString() };
|
|
1875
|
+
}
|
|
1876
|
+
return value;
|
|
1877
|
+
};
|
|
1878
|
+
var decodeCursorValue = (value) => {
|
|
1879
|
+
if (value && typeof value === "object" && typeof value.$type === "string") {
|
|
1880
|
+
if (value.$type === "date") {
|
|
1881
|
+
return new Date(value.value);
|
|
1882
|
+
}
|
|
1883
|
+
if (value.$type === "bigint") {
|
|
1884
|
+
return BigInt(value.value);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
return value;
|
|
1888
|
+
};
|
|
1889
|
+
var encodeCursor = (entries, row) => {
|
|
1890
|
+
const payload = {
|
|
1891
|
+
o: entries,
|
|
1892
|
+
v: entries.map(([column]) => encodeCursorValue(row[column] ?? null))
|
|
1893
|
+
};
|
|
1894
|
+
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
1895
|
+
};
|
|
1896
|
+
var decodeCursor = (after, entries) => {
|
|
1897
|
+
let payload;
|
|
1898
|
+
try {
|
|
1899
|
+
payload = JSON.parse(Buffer.from(after, "base64url").toString("utf8"));
|
|
1900
|
+
} catch (_e) {
|
|
1901
|
+
throw new import_graphql4.GraphQLError("Invalid cursor: unable to decode it. Pass a cursor returned by a previous page.");
|
|
1902
|
+
}
|
|
1903
|
+
if (!payload || typeof payload !== "object" || !Array.isArray(payload.o) || !Array.isArray(payload.v) || payload.o.length !== payload.v.length) {
|
|
1904
|
+
throw new import_graphql4.GraphQLError("Invalid cursor: malformed payload. Pass a cursor returned by a previous page.");
|
|
1905
|
+
}
|
|
1906
|
+
const matchesOrdering = payload.o.length === entries.length && payload.o.every(
|
|
1907
|
+
(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.
|
|
1908
|
+
// JSON round-trips undefined as null — compare the normalized forms.
|
|
1909
|
+
(entry[2] ?? null) === (entries[i][2] ?? null)
|
|
1910
|
+
);
|
|
1911
|
+
if (!matchesOrdering) {
|
|
1912
|
+
throw new import_graphql4.GraphQLError(
|
|
1913
|
+
"Invalid cursor: it was issued for a different ordering. Pass the same orderBy the cursor's page used."
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
return payload.v.map(decodeCursorValue);
|
|
1917
|
+
};
|
|
1918
|
+
var buildCursorCondition = (table, entries, values, nullOrdering) => {
|
|
1919
|
+
const cols = (0, import_drizzle_orm3.getColumns)(table);
|
|
1920
|
+
const disjuncts = [];
|
|
1921
|
+
const equalities = [];
|
|
1922
|
+
entries.forEach(([column, direction, nulls], i) => {
|
|
1923
|
+
const col = cols[column];
|
|
1924
|
+
if (!col) {
|
|
1925
|
+
throw new import_graphql4.GraphQLError(`Invalid cursor: unknown column '${column}'.`);
|
|
1926
|
+
}
|
|
1927
|
+
const value = values[i];
|
|
1928
|
+
const nullsSortLast = nulls != null ? nulls === "last" : direction === "asc" === (nullOrdering === "nulls-largest");
|
|
1929
|
+
let strictlyAfter;
|
|
1930
|
+
if (value === null || value === void 0) {
|
|
1931
|
+
strictlyAfter = nullsSortLast ? void 0 : (0, import_drizzle_orm3.isNotNull)(col);
|
|
1932
|
+
} else {
|
|
1933
|
+
const comparison = direction === "asc" ? (0, import_drizzle_orm3.gt)(col, value) : (0, import_drizzle_orm3.lt)(col, value);
|
|
1934
|
+
strictlyAfter = nullsSortLast ? (0, import_drizzle_orm3.or)(comparison, (0, import_drizzle_orm3.isNull)(col)) : comparison;
|
|
1935
|
+
}
|
|
1936
|
+
if (strictlyAfter) {
|
|
1937
|
+
disjuncts.push(equalities.length ? (0, import_drizzle_orm3.and)(...equalities, strictlyAfter) : strictlyAfter);
|
|
1938
|
+
}
|
|
1939
|
+
equalities.push(value === null || value === void 0 ? (0, import_drizzle_orm3.isNull)(col) : (0, import_drizzle_orm3.eq)(col, value));
|
|
1940
|
+
});
|
|
1941
|
+
if (!disjuncts.length) {
|
|
1942
|
+
return import_drizzle_orm3.sql`1 = 0`;
|
|
1943
|
+
}
|
|
1944
|
+
return disjuncts.length > 1 ? (0, import_drizzle_orm3.or)(...disjuncts) : disjuncts[0];
|
|
1945
|
+
};
|
|
1946
|
+
var isCursorFieldSelected = (tree, table) => {
|
|
1947
|
+
if (!tree) {
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1950
|
+
if ((0, import_drizzle_orm3.getColumns)(table)[CURSOR_FIELD_NAME]) {
|
|
1951
|
+
return false;
|
|
1952
|
+
}
|
|
1953
|
+
return Object.values(tree).some((field) => field.name === CURSOR_FIELD_NAME);
|
|
1954
|
+
};
|
|
1955
|
+
var attachRowCursors = (rows, entries) => {
|
|
1956
|
+
for (const row of rows) {
|
|
1957
|
+
row[ROW_CURSOR_PROP] = encodeCursor(entries, row);
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
var rowCursorResolver = (source) => source?.[ROW_CURSOR_PROP] ?? null;
|
|
1776
1961
|
var getUniqueColumnSets = (table, getTableConfig4) => {
|
|
1777
1962
|
const cols = (0, import_drizzle_orm3.getColumns)(table);
|
|
1778
1963
|
const propNameByColumnName = new Map(Object.entries(cols).map(([propName, col]) => [col.name, propName]));
|
|
@@ -2097,6 +2282,10 @@ var selectArrayArgs = (orderArgs, filterArgs, distinctEnum) => ({
|
|
|
2097
2282
|
limit: { type: import_graphql4.GraphQLInt },
|
|
2098
2283
|
orderBy: { type: orderArgs },
|
|
2099
2284
|
where: { type: filterArgs },
|
|
2285
|
+
after: {
|
|
2286
|
+
type: import_graphql4.GraphQLString,
|
|
2287
|
+
description: "Keyset pagination: only return rows strictly after this cursor (a row's `cursor` field from a previous page, under the same orderBy)."
|
|
2288
|
+
},
|
|
2100
2289
|
...distinctEnum ? { distinct: { type: new import_graphql4.GraphQLList(new import_graphql4.GraphQLNonNull(distinctEnum)) } } : {}
|
|
2101
2290
|
});
|
|
2102
2291
|
var selectSingleArgs = (orderArgs, filterArgs) => ({
|
|
@@ -2119,9 +2308,31 @@ var runRelationalSelect = async (opts) => {
|
|
|
2119
2308
|
where,
|
|
2120
2309
|
single,
|
|
2121
2310
|
filterCtx,
|
|
2122
|
-
pkNames
|
|
2311
|
+
pkNames,
|
|
2312
|
+
after
|
|
2123
2313
|
} = opts;
|
|
2124
2314
|
const distinct = opts.distinct?.length ? opts.distinct : void 0;
|
|
2315
|
+
const cursorSelected = !single && isCursorFieldSelected(parsedInfo.fieldsByTypeName[typeName], table);
|
|
2316
|
+
let cursorEntries;
|
|
2317
|
+
if (!single && (after != null || cursorSelected)) {
|
|
2318
|
+
if (after != null && distinct) {
|
|
2319
|
+
throw new import_graphql4.GraphQLError("'after' cannot be combined with 'distinct'.");
|
|
2320
|
+
}
|
|
2321
|
+
if (orderByHasRelationEntry(orderBy)) {
|
|
2322
|
+
if (after != null) {
|
|
2323
|
+
throw new import_graphql4.GraphQLError(
|
|
2324
|
+
"'after' cannot be combined with an orderBy that orders through a relation \u2014 a related row's value cannot be encoded into a cursor."
|
|
2325
|
+
);
|
|
2326
|
+
}
|
|
2327
|
+
} else if (!pkNames?.length) {
|
|
2328
|
+
if (after != null) {
|
|
2329
|
+
throw new import_graphql4.GraphQLError(`Table ${tableName} has no primary key, so cursor pagination cannot be used on it.`);
|
|
2330
|
+
}
|
|
2331
|
+
} else {
|
|
2332
|
+
cursorEntries = cursorOrderingEntries(orderBy, pkNames);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
const cursorValues = after != null && cursorEntries ? decodeCursor(after, cursorEntries) : void 0;
|
|
2125
2336
|
const needsDefaultOrder = single || offset != null || opts.limit != null;
|
|
2126
2337
|
let distinctKeys;
|
|
2127
2338
|
if (distinct) {
|
|
@@ -2152,9 +2363,21 @@ var runRelationalSelect = async (opts) => {
|
|
|
2152
2363
|
orderBy: distinctKeys ? (aliasedTable2) => [
|
|
2153
2364
|
...orderBy ? extractOrderBy(aliasedTable2, orderBy, relationFilterCtx(filterCtx, tableName)) : [],
|
|
2154
2365
|
...primaryKeyOrderExprs(aliasedTable2, pkNames)
|
|
2155
|
-
] :
|
|
2156
|
-
|
|
2157
|
-
|
|
2366
|
+
] : cursorEntries ? (
|
|
2367
|
+
// Cursor pagination needs a total order: the request's orderBy plus the PK tiebreak,
|
|
2368
|
+
// exactly the ordering the cursor encodes and the keyset predicate compares against.
|
|
2369
|
+
(aliasedTable2) => cursorOrderExprs(aliasedTable2, cursorEntries)
|
|
2370
|
+
) : orderBy ? (aliasedTable2) => extractOrderBy(aliasedTable2, orderBy, relationFilterCtx(filterCtx, tableName)) : needsDefaultOrder && pkNames?.length ? (aliasedTable2) => primaryKeyOrderExprs(aliasedTable2, pkNames) : void 0,
|
|
2371
|
+
where: distinctKeys ? { RAW: (aliasedTable2) => primaryKeyRestriction(aliasedTable2, pkNames, distinctKeys) } : where || cursorValues ? {
|
|
2372
|
+
RAW: (aliasedTable2) => (0, import_drizzle_orm3.and)(
|
|
2373
|
+
where ? extractFilters(aliasedTable2, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0,
|
|
2374
|
+
cursorValues ? buildCursorCondition(
|
|
2375
|
+
aliasedTable2,
|
|
2376
|
+
cursorEntries,
|
|
2377
|
+
cursorValues,
|
|
2378
|
+
opts.nullOrdering ?? "nulls-smallest"
|
|
2379
|
+
) : void 0
|
|
2380
|
+
)
|
|
2158
2381
|
} : void 0,
|
|
2159
2382
|
with: relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, typeNameMapper, filterCtx) : void 0
|
|
2160
2383
|
};
|
|
@@ -2162,8 +2385,16 @@ var runRelationalSelect = async (opts) => {
|
|
|
2162
2385
|
const result2 = await queryBase.findFirst(params);
|
|
2163
2386
|
return result2 ? remapToGraphQLSingleOutput(result2, tableName, table, relationMap) : void 0;
|
|
2164
2387
|
}
|
|
2388
|
+
if (cursorEntries && cursorSelected) {
|
|
2389
|
+
for (const [column] of cursorEntries) {
|
|
2390
|
+
params.columns[column] = true;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2165
2393
|
params.limit = distinctKeys ? void 0 : opts.limit;
|
|
2166
2394
|
const result = await queryBase.findMany(params);
|
|
2395
|
+
if (cursorEntries && cursorSelected) {
|
|
2396
|
+
attachRowCursors(result, cursorEntries);
|
|
2397
|
+
}
|
|
2167
2398
|
return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
|
|
2168
2399
|
};
|
|
2169
2400
|
var eagerLoadMutationRelations = async (db, tableName, rows, pkNames, withParams) => {
|
|
@@ -2711,7 +2942,9 @@ var generateSelectArray = (db, tableName, tables, relationMap, orderArgs, filter
|
|
|
2711
2942
|
single: false,
|
|
2712
2943
|
filterCtx,
|
|
2713
2944
|
pkNames,
|
|
2714
|
-
db: executor
|
|
2945
|
+
db: executor,
|
|
2946
|
+
// MySQL sorts NULLs as the smallest values (first in ASC).
|
|
2947
|
+
nullOrdering: "nulls-smallest"
|
|
2715
2948
|
});
|
|
2716
2949
|
} catch (e) {
|
|
2717
2950
|
throw toGraphQLError(e);
|
|
@@ -3209,16 +3442,48 @@ var generateSelectArray2 = (db, tableName, tables, relationMap, orderArgs, filte
|
|
|
3209
3442
|
single: false,
|
|
3210
3443
|
filterCtx,
|
|
3211
3444
|
pkNames,
|
|
3212
|
-
db: executor
|
|
3445
|
+
db: executor,
|
|
3446
|
+
// PostgreSQL sorts NULLs as the largest values (last in ASC).
|
|
3447
|
+
nullOrdering: "nulls-largest"
|
|
3213
3448
|
});
|
|
3214
3449
|
}
|
|
3215
|
-
const { offset, limit, orderBy, where, distinct } = args;
|
|
3450
|
+
const { offset, limit, orderBy, where, distinct, after } = args;
|
|
3216
3451
|
const selectedColumnsSql = extractSelectedColumnsFromTreeSQLFormat(
|
|
3217
3452
|
parsedInfo.fieldsByTypeName[typeName],
|
|
3218
3453
|
table,
|
|
3219
3454
|
{ tableName, relationMap, tables }
|
|
3220
3455
|
);
|
|
3221
|
-
const
|
|
3456
|
+
const cursorSelected = isCursorFieldSelected(parsedInfo.fieldsByTypeName[typeName], table);
|
|
3457
|
+
let cursorEntries;
|
|
3458
|
+
if (after != null || cursorSelected) {
|
|
3459
|
+
if (after != null && distinct?.length) {
|
|
3460
|
+
throw new import_graphql7.GraphQLError("'after' cannot be combined with 'distinct'.");
|
|
3461
|
+
}
|
|
3462
|
+
if (orderByHasRelationEntry(orderBy)) {
|
|
3463
|
+
if (after != null) {
|
|
3464
|
+
throw new import_graphql7.GraphQLError(
|
|
3465
|
+
"'after' cannot be combined with an orderBy that orders through a relation \u2014 a related row's value cannot be encoded into a cursor."
|
|
3466
|
+
);
|
|
3467
|
+
}
|
|
3468
|
+
} else if (!pkNames.length) {
|
|
3469
|
+
if (after != null) {
|
|
3470
|
+
throw new import_graphql7.GraphQLError(
|
|
3471
|
+
`Table ${tableName} has no primary key, so cursor pagination cannot be used on it.`
|
|
3472
|
+
);
|
|
3473
|
+
}
|
|
3474
|
+
} else {
|
|
3475
|
+
cursorEntries = cursorOrderingEntries(orderBy, pkNames);
|
|
3476
|
+
if (cursorSelected) {
|
|
3477
|
+
const allColumns = (0, import_drizzle_orm6.getColumns)(table);
|
|
3478
|
+
for (const [column] of cursorEntries) {
|
|
3479
|
+
selectedColumnsSql[column] ??= allColumns[column];
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
const cursorCondition = after != null && cursorEntries ? buildCursorCondition(table, cursorEntries, decodeCursor(after, cursorEntries), "nulls-largest") : void 0;
|
|
3485
|
+
const baseWhereSql = where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0;
|
|
3486
|
+
const whereSql = cursorCondition ? (0, import_drizzle_orm6.and)(baseWhereSql, cursorCondition) : baseWhereSql;
|
|
3222
3487
|
let distinctKeys;
|
|
3223
3488
|
if (distinct?.length) {
|
|
3224
3489
|
distinctKeys = await selectDistinctKeys({
|
|
@@ -3242,7 +3507,9 @@ var generateSelectArray2 = (db, tableName, tables, relationMap, orderArgs, filte
|
|
|
3242
3507
|
} else if (whereSql) {
|
|
3243
3508
|
q = q.where(whereSql);
|
|
3244
3509
|
}
|
|
3245
|
-
if (
|
|
3510
|
+
if (cursorEntries && !distinctKeys) {
|
|
3511
|
+
q = q.orderBy(...cursorOrderExprs(table, cursorEntries));
|
|
3512
|
+
} else if (orderBy) {
|
|
3246
3513
|
q = q.orderBy(
|
|
3247
3514
|
...extractOrderBy(table, orderBy, relationFilterCtx(filterCtx, tableName)),
|
|
3248
3515
|
...distinctKeys ? primaryKeyOrderExprs(table, pkNames) : []
|
|
@@ -3258,7 +3525,11 @@ var generateSelectArray2 = (db, tableName, tables, relationMap, orderArgs, filte
|
|
|
3258
3525
|
q = q.limit(limit);
|
|
3259
3526
|
}
|
|
3260
3527
|
}
|
|
3261
|
-
|
|
3528
|
+
const rows = await q;
|
|
3529
|
+
if (cursorEntries && cursorSelected) {
|
|
3530
|
+
attachRowCursors(rows, cursorEntries);
|
|
3531
|
+
}
|
|
3532
|
+
return remapToGraphQLArrayOutput(rows, tableName, table, relationMap);
|
|
3262
3533
|
} catch (e) {
|
|
3263
3534
|
throw toGraphQLError(e);
|
|
3264
3535
|
}
|
|
@@ -3970,7 +4241,9 @@ var generateSelectArray3 = (db, tableName, tables, relationMap, orderArgs, filte
|
|
|
3970
4241
|
single: false,
|
|
3971
4242
|
filterCtx,
|
|
3972
4243
|
pkNames,
|
|
3973
|
-
db: executor
|
|
4244
|
+
db: executor,
|
|
4245
|
+
// SQLite sorts NULLs as the smallest values (first in ASC).
|
|
4246
|
+
nullOrdering: "nulls-smallest"
|
|
3974
4247
|
});
|
|
3975
4248
|
} catch (e) {
|
|
3976
4249
|
throw toGraphQLError(e);
|