@vantreeseba/drizzle-graphql 15.1.6 → 16.0.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 +172 -21
- package/dist/index.cjs +706 -300
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -7
- package/dist/index.d.ts +29 -7
- package/dist/index.js +709 -303
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Column, One, SQL, Table, aliasedTable, and, arrayContains, arrayOverlaps, asc, avg, count, countDistinct, desc, eq, extractExtendedColumnType, getColumns, getTableAsAliasSQL, gt, gte, ilike, inArray, is, isNotNull, isNull, like, lt, lte, max, min, ne, not, notIlike, notInArray, notLike, or, relationsFilterToSQL, sql, sum } from "drizzle-orm";
|
|
2
|
-
import { MySqlAsyncDatabase, MySqlDate, MySqlDateString, MySqlDateTime, MySqlDateTimeString, MySqlInt, MySqlSerial, MySqlTable, MySqlTimestamp, MySqlTimestampString, getTableConfig } from "drizzle-orm/mysql-core";
|
|
1
|
+
import { Column, One, SQL, Subquery, Table, aliasedTable, and, arrayContains, arrayOverlaps, asc, avg, count, countDistinct, desc, eq, extractExtendedColumnType, getColumns, getTableAsAliasSQL, gt, gte, ilike, inArray, is, isNotNull, isNull, like, lt, lte, max, min, ne, not, notIlike, notInArray, notLike, or, relationsFilterToSQL, sql, sum } from "drizzle-orm";
|
|
2
|
+
import { MySqlAsyncDatabase, MySqlDate, MySqlDateString, MySqlDateTime, MySqlDateTimeString, MySqlEnumColumn, MySqlEnumObjectColumn, MySqlInt, MySqlSerial, MySqlTable, MySqlTimestamp, MySqlTimestampString, getTableConfig } from "drizzle-orm/mysql-core";
|
|
3
3
|
import { PgAsyncDatabase, PgDate, PgDateString, PgInteger, PgSerial, PgTable, PgTimestamp, PgTimestampString, PgUUID, getTableConfig as getTableConfig$1 } from "drizzle-orm/pg-core";
|
|
4
|
-
import { SQLiteAsyncDatabase,
|
|
4
|
+
import { SQLiteAsyncDatabase, SQLiteTable, SQLiteTimestamp, getTableConfig as getTableConfig$2 } from "drizzle-orm/sqlite-core";
|
|
5
5
|
import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, Kind, getArgumentValues, getNamedType } from "graphql";
|
|
6
6
|
import { GraphQLDateTime, GraphQLJSON, GraphQLUUID } from "graphql-scalars";
|
|
7
7
|
import pluralize from "pluralize";
|
|
@@ -262,7 +262,6 @@ const singularizeMapper = (tableName) => ({
|
|
|
262
262
|
});
|
|
263
263
|
//#endregion
|
|
264
264
|
//#region src/util/type-converter/index.ts
|
|
265
|
-
const allowedNameChars = /^[a-zA-Z0-9_]+$/;
|
|
266
265
|
/** drizzle's `number` constraints whose values always fit a GraphQL `Int` (32-bit signed). */
|
|
267
266
|
const smallIntegerConstraints = /* @__PURE__ */ new Set([
|
|
268
267
|
"int8",
|
|
@@ -288,8 +287,15 @@ const declaredEnum = (column) => {
|
|
|
288
287
|
schema: typeof declared.schema === "string" ? declared.schema : void 0
|
|
289
288
|
} : void 0;
|
|
290
289
|
};
|
|
291
|
-
/**
|
|
292
|
-
|
|
290
|
+
/**
|
|
291
|
+
* `user_status` / `user-status` / `user status` → `UserStatus`; an already-PascalCase name is left
|
|
292
|
+
* alone. A GraphQL name cannot start with a digit, so a name that would (`2fa_mode`) is prefixed
|
|
293
|
+
* with an underscore: `_2faMode`.
|
|
294
|
+
*/
|
|
295
|
+
const pascalize = (input) => {
|
|
296
|
+
const pascal = input.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => capitalize(part)).join("");
|
|
297
|
+
return /^[0-9]/.test(pascal) ? `_${pascal}` : pascal;
|
|
298
|
+
};
|
|
293
299
|
/**
|
|
294
300
|
* Identity of the values a GraphQL enum was built from, so a name claimed twice can be told
|
|
295
301
|
* apart from the same enum reached twice.
|
|
@@ -364,7 +370,7 @@ const generateEnumCached = (column, columnName, tableName) => {
|
|
|
364
370
|
index,
|
|
365
371
|
typeName: name
|
|
366
372
|
});
|
|
367
|
-
if (mapped === void 0) return
|
|
373
|
+
if (mapped === void 0) return validEnumValueName(value) ? value : `Option${index}`;
|
|
368
374
|
if (!validEnumValueName(mapped)) throw new Error(`Drizzle-GraphQL Error: config.enumValueNameMapper named value '${value}' of enum '${name}' (on '${tableName}.${columnName}') '${mapped}', which is not a valid GraphQL enum value name. A name must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ and not be true, false or null.`);
|
|
369
375
|
return mapped;
|
|
370
376
|
});
|
|
@@ -460,6 +466,27 @@ const sqliteTimestampToDate = (column, value) => new Date(column.mode === "times
|
|
|
460
466
|
* mode it uses, for the same reason as {@link isDateOnlyColumn}.
|
|
461
467
|
*/
|
|
462
468
|
const isDateTimeColumn = (column) => is(column, PgTimestamp) || isPgTimestampStringColumn(column) || is(column, MySqlTimestamp) || is(column, MySqlDateTime) || isMySqlDateTimeStringColumn(column) || isSQLiteTimestampColumn(column);
|
|
469
|
+
/**
|
|
470
|
+
* Whether the column holds raw bytes: Postgres `bytea` and SQLite `blob({ mode: 'buffer' })`,
|
|
471
|
+
* both `object buffer` in drizzle v1. Their values are `[Int!]` lists of the bytes.
|
|
472
|
+
*/
|
|
473
|
+
const isBufferColumn = (column) => (column.dataType ?? "").endsWith(" buffer");
|
|
474
|
+
/**
|
|
475
|
+
* Whether the column is a float tuple or vector: drizzle v1 gives every such column an
|
|
476
|
+
* `array …` data type — `point` / `line` in tuple mode (`array point`, `array line`), PostGIS
|
|
477
|
+
* `geometry` in tuple mode, pgvector `vector` and `halfvec`. Each value is one list of numbers,
|
|
478
|
+
* typed `[Float!]`, not a database array of elements.
|
|
479
|
+
*/
|
|
480
|
+
const isFloatTupleColumn = (column) => (column.dataType ?? "").startsWith("array ");
|
|
481
|
+
/** The fixed length of a float tuple column's values, or `undefined` for a vector. */
|
|
482
|
+
const floatTupleLength = (column) => {
|
|
483
|
+
switch (column.dataType) {
|
|
484
|
+
case "array point":
|
|
485
|
+
case "array geometry": return 2;
|
|
486
|
+
case "array line": return 3;
|
|
487
|
+
default: return;
|
|
488
|
+
}
|
|
489
|
+
};
|
|
463
490
|
const columnToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
464
491
|
const dimensions = column.dimensions;
|
|
465
492
|
if (dimensions !== void 0 && dimensions > 0) {
|
|
@@ -497,7 +524,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
497
524
|
return column.columnType === "PgGeometryObject" ? {
|
|
498
525
|
type: isInput ? geoXyInputType : geoXyType,
|
|
499
526
|
typeLabel: "Geometry points XY"
|
|
500
|
-
} : column
|
|
527
|
+
} : isBufferColumn(column) ? {
|
|
501
528
|
type: new GraphQLList(new GraphQLNonNull(GraphQLInt)),
|
|
502
529
|
typeLabel: "Buffer"
|
|
503
530
|
} : {
|
|
@@ -530,7 +557,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
530
557
|
type: GraphQLBigIntString,
|
|
531
558
|
typeLabel: "BigInt"
|
|
532
559
|
};
|
|
533
|
-
case "number": return constraint !== void 0 && smallIntegerConstraints.has(constraint) || is(column, PgInteger) || is(column, PgSerial) || is(column, MySqlInt) || is(column, MySqlSerial)
|
|
560
|
+
case "number": return constraint !== void 0 && smallIntegerConstraints.has(constraint) || is(column, PgInteger) || is(column, PgSerial) || is(column, MySqlInt) || is(column, MySqlSerial) ? {
|
|
534
561
|
type: GraphQLInt,
|
|
535
562
|
typeLabel: "Integer"
|
|
536
563
|
} : {
|
|
@@ -538,18 +565,10 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
538
565
|
typeLabel: "Float"
|
|
539
566
|
};
|
|
540
567
|
case "array": {
|
|
541
|
-
|
|
542
|
-
type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
|
|
543
|
-
typeLabel: "Array<Float>"
|
|
544
|
-
};
|
|
545
|
-
if (column.columnType === "PgGeometry") return {
|
|
546
|
-
type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
|
|
547
|
-
typeLabel: "Tuple<[Float, Float]>"
|
|
548
|
-
};
|
|
549
|
-
const innerType = columnToGraphQLCore(column.baseColumn, columnName, tableName, isInput);
|
|
568
|
+
const length = floatTupleLength(column);
|
|
550
569
|
return {
|
|
551
|
-
type: new GraphQLList(new GraphQLNonNull(
|
|
552
|
-
typeLabel: `Array
|
|
570
|
+
type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
|
|
571
|
+
typeLabel: length === void 0 ? "Array<Float>" : `Tuple<[${Array(length).fill("Float").join(", ")}]>`
|
|
553
572
|
};
|
|
554
573
|
}
|
|
555
574
|
default: throw new Error(`Drizzle-GraphQL Error: Type ${column.dataType} is not implemented!`);
|
|
@@ -691,6 +710,18 @@ const columnDialect = (column) => {
|
|
|
691
710
|
*/
|
|
692
711
|
const isJsonColumn = (column) => (column.dataType ?? "").includes("json") && column.columnType !== "PgGeometryObject";
|
|
693
712
|
/**
|
|
713
|
+
* SQLite `blob({ mode: 'bigint' })`: the bigint is stored as the bytes of its decimal text, and
|
|
714
|
+
* SQLite compares blobs byte by byte, so `9` sorts above `10`. Equality still holds, so it can be
|
|
715
|
+
* grouped and made distinct on, but not ordered or given a min/max.
|
|
716
|
+
*/
|
|
717
|
+
const isSqliteBlobBigIntColumn = (column) => column.columnType === "SQLiteBigInt";
|
|
718
|
+
/**
|
|
719
|
+
* Whether `orderBy` (and so cursor pagination) offers a column. JSON has no meaningful order:
|
|
720
|
+
* PostgreSQL `json` cannot be ordered at all, and MySQL's JSON ordering disagrees with any
|
|
721
|
+
* keyset comparison a cursor could make. A SQLite blob bigint orders as text.
|
|
722
|
+
*/
|
|
723
|
+
const isOrderableColumn = (column) => !isJsonColumn(column) && !isSqliteBlobBigIntColumn(column);
|
|
724
|
+
/**
|
|
694
725
|
* Maps a Drizzle column to the generic filter type to use.
|
|
695
726
|
*
|
|
696
727
|
* Selection is keyed on the column's data type, never its name: a text column named
|
|
@@ -1197,6 +1228,36 @@ const pgTimestampTextToIso = (value) => {
|
|
|
1197
1228
|
const utc = (/* @__PURE__ */ new Date(wallMs - offsetSeconds * 1e3)).toISOString();
|
|
1198
1229
|
return utc.length === 24 ? `${utc.slice(0, 19)}${fraction}Z` : value;
|
|
1199
1230
|
};
|
|
1231
|
+
/** Marks a decoder installed by {@link installSqliteBlobJsonDecoders}, so a rebuild never wraps it twice. */
|
|
1232
|
+
const BLOB_JSON_DECODER = Symbol.for("drizzle-graphql.sqliteBlobJsonDecoder");
|
|
1233
|
+
/**
|
|
1234
|
+
* Gives SQLite's blob columns a decoder for the relational query builder's JSON path, so a NULL
|
|
1235
|
+
* there reads back as null.
|
|
1236
|
+
*
|
|
1237
|
+
* Inside a nested relation drizzle selects a blob as `hex(column)` (JSON cannot hold a blob), and
|
|
1238
|
+
* `hex(NULL)` is `''`. drizzle then decodes that `''` with the column's ordinary decoder: a
|
|
1239
|
+
* `blob({ mode: 'bigint' })` reads it as `0n` — indistinguishable from a stored 0 — and a
|
|
1240
|
+
* `blob({ mode: 'json' })` throws on it. drizzle consults a column's `mapFromJsonValue`, when it
|
|
1241
|
+
* has one, instead of `mapFromDriverValue` for exactly this path, so one is installed here that
|
|
1242
|
+
* reads `''` as NULL and hands everything else to the column's own decoder.
|
|
1243
|
+
*
|
|
1244
|
+
* `''` is never a stored bigint or JSON blob, so for those it can only be NULL. For a buffer
|
|
1245
|
+
* blob it is also how an empty blob arrives, so it is read as NULL only when the column is
|
|
1246
|
+
* nullable — a `notNull` column cannot hold the NULL, so there it is the empty blob.
|
|
1247
|
+
*/
|
|
1248
|
+
const installSqliteBlobJsonDecoders = (tables) => {
|
|
1249
|
+
for (const table of Object.values(tables)) for (const column of Object.values(getColumns(table))) {
|
|
1250
|
+
const columnType = column.columnType ?? "";
|
|
1251
|
+
const isBuffer = columnType === "SQLiteBlobBuffer";
|
|
1252
|
+
if (columnType !== "SQLiteBigInt" && columnType !== "SQLiteBlobJson" && !isBuffer) continue;
|
|
1253
|
+
const existing = column.mapFromJsonValue;
|
|
1254
|
+
if (existing && !existing[BLOB_JSON_DECODER]) continue;
|
|
1255
|
+
const emptyIsNull = !isBuffer || !column.notNull;
|
|
1256
|
+
const decoder = (value) => value === "" && emptyIsNull ? null : column.mapFromDriverValue(value);
|
|
1257
|
+
decoder[BLOB_JSON_DECODER] = true;
|
|
1258
|
+
column.mapFromJsonValue = decoder;
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1200
1261
|
const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
|
|
1201
1262
|
if (Array.isArray(value)) {
|
|
1202
1263
|
const relations = relationMap?.[tableName];
|
|
@@ -1223,14 +1284,14 @@ const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
|
|
|
1223
1284
|
if (value instanceof Date) return value.toISOString();
|
|
1224
1285
|
if (typeof value === "string" && isMySqlDateTimeStringColumn(column)) return mySqlDateTimeTextToIso(value);
|
|
1225
1286
|
if (typeof value === "string" && isPgTimestampStringColumn(column)) return pgTimestampTextToIso(value);
|
|
1226
|
-
if (value instanceof
|
|
1287
|
+
if (value instanceof Uint8Array) return Array.from(value);
|
|
1227
1288
|
if (typeof value === "bigint") return value.toString();
|
|
1228
1289
|
if (Array.isArray(value)) {
|
|
1229
|
-
if (column
|
|
1290
|
+
if (isFloatTupleColumn(column)) return value;
|
|
1230
1291
|
return value.map((arrVal) => remapToGraphQLCore(key, arrVal, tableName, column, relationMap));
|
|
1231
1292
|
}
|
|
1232
1293
|
if (typeof value === "object" && value !== null) {
|
|
1233
|
-
if (column.
|
|
1294
|
+
if ((column.dataType ?? "").startsWith("object")) return value;
|
|
1234
1295
|
return JSON.stringify(value);
|
|
1235
1296
|
}
|
|
1236
1297
|
return value;
|
|
@@ -1281,10 +1342,6 @@ const remapToGraphQLSingleOutput = (queryOutput, tableName, table, relationMap)
|
|
|
1281
1342
|
continue;
|
|
1282
1343
|
}
|
|
1283
1344
|
const column = columns[key];
|
|
1284
|
-
if (value === 0n && column && column.columnType === "SQLiteBigInt" && !column.notNull) {
|
|
1285
|
-
delete queryOutput[key];
|
|
1286
|
-
continue;
|
|
1287
|
-
}
|
|
1288
1345
|
const remapped = remapToGraphQLCore(key, value, tableName, column, relationMap);
|
|
1289
1346
|
if (column && remapped !== value) keepRawColumnValue(queryOutput, key, value);
|
|
1290
1347
|
queryOutput[key] = remapped;
|
|
@@ -1304,6 +1361,7 @@ const remapFromGraphQLCore = (value, column, columnName) => {
|
|
|
1304
1361
|
const dataType = column.dataType ?? "";
|
|
1305
1362
|
const columnType = column.columnType ?? "";
|
|
1306
1363
|
if (isDateTimeColumn(column)) {
|
|
1364
|
+
if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
|
|
1307
1365
|
const isMySql = columnType.startsWith("MySql");
|
|
1308
1366
|
const isPgString = isPgTimestampStringColumn(column);
|
|
1309
1367
|
const toIso = isMySql ? mySqlDateTimeTextToIso : isPgString ? pgTimestampTextToIso : void 0;
|
|
@@ -1318,41 +1376,29 @@ const remapFromGraphQLCore = (value, column, columnName) => {
|
|
|
1318
1376
|
if (typeof day !== "string" || !isCalendarDate(day)) throw drizzleError(`Field '${columnName}' is not a valid date! Expected a calendar date as YYYY-MM-DD.`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1319
1377
|
return day;
|
|
1320
1378
|
}
|
|
1321
|
-
if (dataType.includes("bigint"))
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1325
|
-
}
|
|
1326
|
-
if (dataType.includes("json") && column.columnType !== "PgGeometryObject") return value;
|
|
1327
|
-
switch (dataType) {
|
|
1328
|
-
case "date": {
|
|
1329
|
-
const formatted = new Date(value);
|
|
1330
|
-
if (Number.isNaN(formatted.getTime())) throw drizzleError(`Field '${columnName}' is not a valid date!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1331
|
-
return formatted;
|
|
1332
|
-
}
|
|
1333
|
-
case "buffer":
|
|
1334
|
-
if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1335
|
-
return Buffer.from(value);
|
|
1336
|
-
case "json":
|
|
1337
|
-
if (column.columnType === "PgGeometryObject") return value;
|
|
1338
|
-
try {
|
|
1339
|
-
return JSON.parse(value);
|
|
1340
|
-
} catch (e) {
|
|
1341
|
-
throw drizzleError(`Invalid JSON in field '${columnName}':\n${e instanceof Error ? e.message : "Unknown error"}`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1342
|
-
}
|
|
1343
|
-
case "array":
|
|
1344
|
-
if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1345
|
-
if (column.columnType === "PgGeometry" && value.length !== 2) throw drizzleError(`Invalid float tuple in field '${columnName}': expected array with length of 2, received ${value.length}`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1346
|
-
return value;
|
|
1347
|
-
case "bigint": try {
|
|
1379
|
+
if (dataType.includes("bigint")) {
|
|
1380
|
+
if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
|
|
1381
|
+
try {
|
|
1348
1382
|
return BigInt(value);
|
|
1349
|
-
} catch
|
|
1383
|
+
} catch {
|
|
1350
1384
|
throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1351
1385
|
}
|
|
1352
|
-
default:
|
|
1353
|
-
if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
|
|
1354
|
-
return value;
|
|
1355
1386
|
}
|
|
1387
|
+
if (isJsonColumn(column)) return value;
|
|
1388
|
+
if (isBufferColumn(column)) {
|
|
1389
|
+
if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1390
|
+
if (column.dimensions > 0 && value.every((element) => element === null || Array.isArray(element))) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
|
|
1391
|
+
if (!value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) throw drizzleError(`Field '${columnName}' is not a list of bytes! Expected integers from 0 to 255.`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1392
|
+
return Buffer.from(value);
|
|
1393
|
+
}
|
|
1394
|
+
if (isFloatTupleColumn(column)) {
|
|
1395
|
+
if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1396
|
+
const length = floatTupleLength(column);
|
|
1397
|
+
if (length !== void 0 && value.length !== length) throw drizzleError(`Invalid float tuple in field '${columnName}': expected array with length of ${length}, received ${value.length}`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1398
|
+
return value;
|
|
1399
|
+
}
|
|
1400
|
+
if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
|
|
1401
|
+
return value;
|
|
1356
1402
|
};
|
|
1357
1403
|
const remapFromGraphQLSingleInput = (queryInput, table, operation = "insert") => {
|
|
1358
1404
|
const columns = getColumns(table);
|
|
@@ -1394,36 +1440,60 @@ const escapeLikeValue = (value) => value.replace(/[\\%_]/g, (char) => `\\${char}
|
|
|
1394
1440
|
const safeLikeOps = {
|
|
1395
1441
|
startsWith: {
|
|
1396
1442
|
buildPattern: (value) => `${escapeLikeValue(value)}%`,
|
|
1443
|
+
buildGlob: (value) => `${escapeGlobValue(value)}*`,
|
|
1397
1444
|
insensitive: false
|
|
1398
1445
|
},
|
|
1399
1446
|
endsWith: {
|
|
1400
1447
|
buildPattern: (value) => `%${escapeLikeValue(value)}`,
|
|
1448
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}`,
|
|
1401
1449
|
insensitive: false
|
|
1402
1450
|
},
|
|
1403
1451
|
contains: {
|
|
1404
1452
|
buildPattern: (value) => `%${escapeLikeValue(value)}%`,
|
|
1453
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}*`,
|
|
1405
1454
|
insensitive: false
|
|
1406
1455
|
},
|
|
1407
1456
|
iStartsWith: {
|
|
1408
1457
|
buildPattern: (value) => `${escapeLikeValue(value)}%`,
|
|
1458
|
+
buildGlob: (value) => `${escapeGlobValue(value)}*`,
|
|
1409
1459
|
insensitive: true
|
|
1410
1460
|
},
|
|
1411
1461
|
iEndsWith: {
|
|
1412
1462
|
buildPattern: (value) => `%${escapeLikeValue(value)}`,
|
|
1463
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}`,
|
|
1413
1464
|
insensitive: true
|
|
1414
1465
|
},
|
|
1415
1466
|
iContains: {
|
|
1416
1467
|
buildPattern: (value) => `%${escapeLikeValue(value)}%`,
|
|
1468
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}*`,
|
|
1417
1469
|
insensitive: true
|
|
1418
1470
|
}
|
|
1419
1471
|
};
|
|
1420
1472
|
/**
|
|
1421
|
-
* `
|
|
1422
|
-
*
|
|
1423
|
-
*
|
|
1473
|
+
* Escapes SQLite's `GLOB` wildcards (`*`, `?`, `[`) in a literal search term by wrapping each in
|
|
1474
|
+
* a one-character class, so the term only ever matches literally inside a GLOB pattern. GLOB
|
|
1475
|
+
* has no escape character; `]` outside a class and everything else is already literal.
|
|
1476
|
+
*/
|
|
1477
|
+
const escapeGlobValue = (value) => value.replace(/[*?[]/g, (char) => `[${char}]`);
|
|
1478
|
+
/**
|
|
1479
|
+
* A caller-written LIKE pattern as the GLOB pattern matching the same strings, for SQLite's
|
|
1480
|
+
* case-sensitive `like` / `notLike`: `%` → `*`, `_` → `?`, and GLOB's own wildcards escaped.
|
|
1481
|
+
* The raw operators bind no `ESCAPE` clause, so `\` is an ordinary character in both.
|
|
1482
|
+
*/
|
|
1483
|
+
const likePatternToGlob = (pattern) => pattern.replace(/[%_*?[]/g, (char) => char === "%" ? "*" : char === "_" ? "?" : `[${char}]`);
|
|
1484
|
+
/**
|
|
1485
|
+
* The safe string operator's predicate. Case-insensitive variants use Postgres's native
|
|
1486
|
+
* `ILIKE`; MySQL and SQLite have no `ILIKE`, so they compare `lower()` on both sides instead
|
|
1487
|
+
* (mirroring how the raw `ilike` operator is Postgres-only).
|
|
1488
|
+
*
|
|
1489
|
+
* The case-sensitive variants are `LIKE <pattern> ESCAPE '\'`, except on SQLite, whose `LIKE`
|
|
1490
|
+
* folds ASCII case — `contains: "App"` would match `apple`. There they are `GLOB`, which is
|
|
1491
|
+
* case-sensitive, as `LIKE` is on Postgres. `dialectColumn` names the column the dialect is read
|
|
1492
|
+
* from when `column` is an expression over it (a JSON path read).
|
|
1424
1493
|
*/
|
|
1425
|
-
const safeLikeCondition = (column,
|
|
1426
|
-
|
|
1494
|
+
const safeLikeCondition = (column, value, op, insensitive, dialectColumn = column) => {
|
|
1495
|
+
const pattern = op.buildPattern(value);
|
|
1496
|
+
if (!insensitive) return columnDialect(dialectColumn) === "sqlite" ? sql`${column} glob ${op.buildGlob(value)}` : sql`${column} like ${pattern} escape ${LIKE_ESCAPE_CHAR}`;
|
|
1427
1497
|
return (column.columnType ?? "").startsWith("Pg") ? sql`${column} ilike ${pattern} escape ${LIKE_ESCAPE_CHAR}` : sql`lower(${column}) like ${pattern.toLowerCase()} escape ${LIKE_ESCAPE_CHAR}`;
|
|
1428
1498
|
};
|
|
1429
1499
|
/**
|
|
@@ -1479,6 +1549,35 @@ const jsonContains = (column, columnName, value) => {
|
|
|
1479
1549
|
}
|
|
1480
1550
|
};
|
|
1481
1551
|
/**
|
|
1552
|
+
* The nodes of a SQLite JSON document as `(path, kind, value)` rows, for a structural
|
|
1553
|
+
* comparison. Integers and reals share one kind so `1` and `1.0` are the same number, as they
|
|
1554
|
+
* are on the other two dialects.
|
|
1555
|
+
*/
|
|
1556
|
+
const sqliteJsonNodes = (document) => sql`select fullkey, case when type in ('integer', 'real') then 'number' else type end, atom from json_tree(${document})`;
|
|
1557
|
+
/**
|
|
1558
|
+
* Whole-document `eq` / `ne` on a JSON column, comparing documents rather than their text:
|
|
1559
|
+
* - Postgres: as `jsonb` (a plain `json` column has no `=` operator at all)
|
|
1560
|
+
* - MySQL: against `CAST(? AS JSON)`; a bare string parameter would be a JSON string scalar
|
|
1561
|
+
* - SQLite: the two documents' node sets must match both ways, so object key order and
|
|
1562
|
+
* whitespace do not matter, while array order still does
|
|
1563
|
+
* A SQL NULL column matches neither, as with `=` / `<>` on any other column.
|
|
1564
|
+
*/
|
|
1565
|
+
const jsonEquality = (column, columnName, value, negated) => {
|
|
1566
|
+
const serialized = JSON.stringify(value);
|
|
1567
|
+
switch (columnDialect(column)) {
|
|
1568
|
+
case "pg": {
|
|
1569
|
+
const left = column.columnType === "PgJson" ? sql`${column}::jsonb` : sql`${column}`;
|
|
1570
|
+
return negated ? sql`${left} <> ${serialized}::jsonb` : sql`${left} = ${serialized}::jsonb`;
|
|
1571
|
+
}
|
|
1572
|
+
case "mysql": return negated ? sql`${column} <> cast(${serialized} as json)` : sql`${column} = cast(${serialized} as json)`;
|
|
1573
|
+
case "sqlite": {
|
|
1574
|
+
const equal = sql`(not exists (${sqliteJsonNodes(column)} except ${sqliteJsonNodes(serialized)}) and not exists (${sqliteJsonNodes(serialized)} except ${sqliteJsonNodes(column)}))`;
|
|
1575
|
+
return negated ? sql`(${column} is not null and not ${equal})` : sql`(${column} is not null and ${equal})`;
|
|
1576
|
+
}
|
|
1577
|
+
default: throw drizzleError(`WHERE ${columnName}: JSON equality is not supported for this dialect!`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
/**
|
|
1482
1581
|
* Constant predicates, used where an operator's operand list is empty and the answer is
|
|
1483
1582
|
* therefore known without touching the column. Written as `1 = 0` / `1 = 1` rather than the
|
|
1484
1583
|
* `FALSE` / `TRUE` keywords so they compile identically on all three dialects. Built fresh
|
|
@@ -1513,7 +1612,8 @@ const NUMERIC_TEXT_PATTERN = "^\\s*-?(\\d+\\.?\\d*|\\.\\d+)([eE][-+]?\\d+)?\\s*$
|
|
|
1513
1612
|
* pattern guard.
|
|
1514
1613
|
* - **SQLite** — `json_extract` returns the value in SQLite's own type, so the reads cast
|
|
1515
1614
|
* explicitly: without that, SQLite's cross-type ordering puts every string above every
|
|
1516
|
-
* number and `'admin' > 0` would be true.
|
|
1615
|
+
* number and `'admin' > 0` would be true. A JSON boolean comes back as 1 / 0, so every read
|
|
1616
|
+
* also checks `json_type` to keep booleans and numbers apart.
|
|
1517
1617
|
*/
|
|
1518
1618
|
const jsonPathExprs = (column, path) => {
|
|
1519
1619
|
switch (columnDialect(column)) {
|
|
@@ -1539,10 +1639,11 @@ const jsonPathExprs = (column, path) => {
|
|
|
1539
1639
|
}
|
|
1540
1640
|
default: {
|
|
1541
1641
|
const extracted = sql`json_extract(${column}, ${jsonPathString(path)})`;
|
|
1642
|
+
const type = sql`json_type(${column}, ${jsonPathString(path)})`;
|
|
1542
1643
|
return {
|
|
1543
|
-
text: sql`cast(${extracted} as text)`,
|
|
1544
|
-
number: sql`(case when
|
|
1545
|
-
boolean: extracted
|
|
1644
|
+
text: sql`(case ${type} when 'true' then 'true' when 'false' then 'false' else cast(${extracted} as text) end)`,
|
|
1645
|
+
number: sql`(case when ${type} in ('integer', 'real') then ${extracted} end)`,
|
|
1646
|
+
boolean: sql`(case when ${type} in ('true', 'false') then ${extracted} end)`,
|
|
1546
1647
|
encodeBoolean: (value) => value ? 1 : 0
|
|
1547
1648
|
};
|
|
1548
1649
|
}
|
|
@@ -1581,9 +1682,9 @@ const jsonPathCondition = (column, columnName, filter) => {
|
|
|
1581
1682
|
continue;
|
|
1582
1683
|
}
|
|
1583
1684
|
if (operatorName in safeLikeOps) {
|
|
1584
|
-
const
|
|
1685
|
+
const op = safeLikeOps[operatorName];
|
|
1585
1686
|
if (typeof operatorValue !== "string") throw drizzleError(`WHERE ${locator}: operator '${operatorName}' takes a string`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1586
|
-
variants.push(safeLikeCondition(exprs.text,
|
|
1687
|
+
variants.push(safeLikeCondition(exprs.text, operatorValue, op, op.insensitive, column));
|
|
1587
1688
|
continue;
|
|
1588
1689
|
}
|
|
1589
1690
|
const comparison = JSON_PATH_COMPARISONS[operatorName];
|
|
@@ -1641,20 +1742,25 @@ const extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1641
1742
|
for (const [operatorName, operatorValue] of entries) {
|
|
1642
1743
|
if (operatorValue === null || operatorValue === void 0) continue;
|
|
1643
1744
|
if (operatorValue === false && FLAG_OPERATORS.has(operatorName)) continue;
|
|
1644
|
-
if (operatorName
|
|
1745
|
+
if ((operatorName === "eq" || operatorName === "ne") && isJsonColumn(column)) variants.push(jsonEquality(column, columnName, operatorValue, operatorName === "ne"));
|
|
1746
|
+
else if (operatorName in singleValueOps) {
|
|
1645
1747
|
const singleValue = remapFromGraphQLCore(operatorValue, column, columnName);
|
|
1646
1748
|
variants.push(foldCase ? lowerComparison(column, operatorName, singleValue) : singleValueOps[operatorName](column, singleValue));
|
|
1647
1749
|
} else if (operatorName in stringValueOps) {
|
|
1648
|
-
const
|
|
1649
|
-
variants.push(
|
|
1750
|
+
const isLike = operatorName === "like" || operatorName === "notLike";
|
|
1751
|
+
if (isLike && foldCase || (operatorName === "ilike" || operatorName === "notIlike") && columnDialect(column) !== "pg") variants.push(insensitiveLike(column, operatorValue, operatorName === "notLike" || operatorName === "notIlike"));
|
|
1752
|
+
else if (isLike && columnDialect(column) === "sqlite") {
|
|
1753
|
+
const glob = likePatternToGlob(operatorValue);
|
|
1754
|
+
variants.push(operatorName === "like" ? sql`${column} glob ${glob}` : sql`${column} not glob ${glob}`);
|
|
1755
|
+
} else variants.push(stringValueOps[operatorName](column, operatorValue));
|
|
1650
1756
|
} else if (operatorName === "path" && isJsonColumn(column)) for (const pathFilter of operatorValue) {
|
|
1651
1757
|
const extracted = jsonPathCondition(column, columnName, pathFilter);
|
|
1652
1758
|
if (extracted) variants.push(extracted);
|
|
1653
1759
|
}
|
|
1654
1760
|
else if (operatorName === "contains" && isJsonColumn(column)) variants.push(jsonContains(column, columnName, operatorValue));
|
|
1655
1761
|
else if (operatorName in safeLikeOps) {
|
|
1656
|
-
const
|
|
1657
|
-
variants.push(safeLikeCondition(column,
|
|
1762
|
+
const op = safeLikeOps[operatorName];
|
|
1763
|
+
variants.push(safeLikeCondition(column, operatorValue, op, op.insensitive || foldCase));
|
|
1658
1764
|
} else if (operatorName in arrayValueOps) {
|
|
1659
1765
|
if (!operatorValue.length) {
|
|
1660
1766
|
variants.push(operatorName === "inArray" ? sqlFalse() : sqlTrue());
|
|
@@ -1662,13 +1768,13 @@ const extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1662
1768
|
}
|
|
1663
1769
|
const arrayValue = operatorValue.map((val) => remapFromGraphQLCore(val, column, columnName));
|
|
1664
1770
|
variants.push(foldCase ? lowerMembership(column, arrayValue, operatorName === "notInArray") : arrayValueOps[operatorName](column, arrayValue));
|
|
1665
|
-
} else if (operatorName === "has") variants.push(arrayContains(column, [operatorValue]));
|
|
1771
|
+
} else if (operatorName === "has") variants.push(arrayContains(column, [remapFromGraphQLCore(operatorValue, column, columnName)]));
|
|
1666
1772
|
else if (operatorName in arrayMembershipOps) {
|
|
1667
1773
|
if (!operatorValue.length) {
|
|
1668
1774
|
variants.push(operatorName === "hasSome" ? sqlFalse() : sqlTrue());
|
|
1669
1775
|
continue;
|
|
1670
1776
|
}
|
|
1671
|
-
variants.push(arrayMembershipOps[operatorName](column, operatorValue));
|
|
1777
|
+
variants.push(arrayMembershipOps[operatorName](column, operatorValue.map((element) => remapFromGraphQLCore(element, column, columnName))));
|
|
1672
1778
|
} else if (operatorName === "isEmpty") variants.push(sql`cardinality(${column}) = 0`);
|
|
1673
1779
|
else if (operatorName in nullableOps) variants.push(nullableOps[operatorName](column));
|
|
1674
1780
|
else throw drizzleError(`WHERE ${columnName}: Unknown operator: ${operatorName}`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
@@ -2516,6 +2622,32 @@ const withScope = (scope, tableName, table, condition, mode) => {
|
|
|
2516
2622
|
const predicate = scope?.on(tableName, table, mode);
|
|
2517
2623
|
return predicate ? and(condition, predicate) : condition;
|
|
2518
2624
|
};
|
|
2625
|
+
/**
|
|
2626
|
+
* The junction table a `.through()` relation links through, by its schema key — the key its
|
|
2627
|
+
* `scope` and `softDelete` are configured under. `undefined` for a relation with no junction,
|
|
2628
|
+
* or a junction that is not one of the build's tables.
|
|
2629
|
+
*/
|
|
2630
|
+
const junctionTableName = (tables, relEntry) => {
|
|
2631
|
+
const throughTable = (relEntry?.relation ?? relEntry)?.throughTable;
|
|
2632
|
+
return throughTable ? Object.keys(tables).find((name) => tables[name] === throughTable) : void 0;
|
|
2633
|
+
};
|
|
2634
|
+
/**
|
|
2635
|
+
* Whether a `.through()` relation's junction restricts which links exist for this request —
|
|
2636
|
+
* a `scope` on the junction table, or a soft delete it declares.
|
|
2637
|
+
*/
|
|
2638
|
+
const hasJunctionPolicy = (scope, junctionName) => !!junctionName && !!scope?.has(junctionName, "EXCLUDE");
|
|
2639
|
+
/**
|
|
2640
|
+
* The junction's own row policy, compiled against the junction table as the query names it.
|
|
2641
|
+
* A `.through()` relation links only through the junction rows this keeps, on every path that
|
|
2642
|
+
* reads the relation — the relation field, `<relation>Aggregate`, and a relation filter — so
|
|
2643
|
+
* they all agree on which rows are related.
|
|
2644
|
+
*
|
|
2645
|
+
* The junction is always read at `EXCLUDE`: a marked junction row is a removed link. The
|
|
2646
|
+
* relation's `deleted` argument is about the *target* rows, and the junction's own
|
|
2647
|
+
* `softDelete.scope` only governs reads of the junction table's own fields — a link row has no
|
|
2648
|
+
* field of its own on the relation that `'root'` could keep rendering.
|
|
2649
|
+
*/
|
|
2650
|
+
const junctionPolicy = (scope, junctionName, table) => hasJunctionPolicy(scope, junctionName) ? scope.on(junctionName, table, "EXCLUDE") : void 0;
|
|
2519
2651
|
/** Binds a build's {@link TablePolicies} to its relation context, once, at schema-build time. */
|
|
2520
2652
|
const bindPolicies = (policies, filterCtx) => policies?.scope || policies?.contextValues || policies?.softDelete || policies?.onWrite || policies?.defaultOrderBy ? {
|
|
2521
2653
|
scope: policies.scope || policies.softDelete ? (context) => resolveScope(policies, context, filterCtx) : void 0,
|
|
@@ -2566,7 +2698,7 @@ const deletedArg = (softDelete, tableName, resolveName) => softDelete?.(tableNam
|
|
|
2566
2698
|
const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableName, resolveName) => ({
|
|
2567
2699
|
offset: { type: GraphQLInt },
|
|
2568
2700
|
limit: { type: GraphQLInt },
|
|
2569
|
-
orderBy: { type: orderArgs },
|
|
2701
|
+
...orderArgs ? { orderBy: { type: orderArgs } } : {},
|
|
2570
2702
|
where: { type: filterArgs },
|
|
2571
2703
|
after: {
|
|
2572
2704
|
type: GraphQLString,
|
|
@@ -2578,7 +2710,7 @@ const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableN
|
|
|
2578
2710
|
/** GraphQL argument map for a single-row select field (no `limit`). */
|
|
2579
2711
|
const selectSingleArgs = (orderArgs, filterArgs, softDelete, tableName, resolveName) => ({
|
|
2580
2712
|
offset: { type: GraphQLInt },
|
|
2581
|
-
orderBy: { type: orderArgs },
|
|
2713
|
+
...orderArgs ? { orderBy: { type: orderArgs } } : {},
|
|
2582
2714
|
where: { type: filterArgs },
|
|
2583
2715
|
...deletedArg(softDelete, tableName, resolveName)
|
|
2584
2716
|
});
|
|
@@ -2628,7 +2760,7 @@ const generateDistinctEnum = (table, typeName, resolveName, tableKey) => {
|
|
|
2628
2760
|
defaultName,
|
|
2629
2761
|
table: tableKey,
|
|
2630
2762
|
operation: "distinct"
|
|
2631
|
-
}) ?? defaultName, `Columns of ${typeName} that a query can be made distinct on
|
|
2763
|
+
}) ?? defaultName, `Columns of ${typeName} that a query can be made distinct on`, (column) => !isJsonColumn(column));
|
|
2632
2764
|
};
|
|
2633
2765
|
//#endregion
|
|
2634
2766
|
//#region src/util/builders/common/conflict.ts
|
|
@@ -2798,6 +2930,44 @@ const resolveConflictPlan = (params) => {
|
|
|
2798
2930
|
plan
|
|
2799
2931
|
}));
|
|
2800
2932
|
};
|
|
2933
|
+
/**
|
|
2934
|
+
* Adds a {@link ConflictPlan}'s clause to a PostgreSQL or SQLite insert.
|
|
2935
|
+
*
|
|
2936
|
+
* SQLite needs two things drizzle's builder does not give it when the target is a partial
|
|
2937
|
+
* unique index:
|
|
2938
|
+
*
|
|
2939
|
+
* - The index predicate written literally. SQLite matches a conflict target to an index by
|
|
2940
|
+
* comparing the predicates as written, and a bound `?` never equals `'active'`, so a
|
|
2941
|
+
* predicate that compares to a value would never find its index. Its params are inlined.
|
|
2942
|
+
* - A `DO NOTHING` it can parse. Drizzle writes the predicate after `do nothing`, which is a
|
|
2943
|
+
* syntax error, so the action is spelled as an update that matches no row instead:
|
|
2944
|
+
* `ON CONFLICT (…) WHERE … DO UPDATE SET col = col WHERE 0 = 1`. A row that is not updated is
|
|
2945
|
+
* not returned, which is what `DO NOTHING` does too.
|
|
2946
|
+
*/
|
|
2947
|
+
const applyConflictPlan = (query, table, plan, setWhere) => {
|
|
2948
|
+
const sqlite = is(table, SQLiteTable);
|
|
2949
|
+
const targetWhere = sqlite && plan.targetWhere ? sql`${plan.targetWhere}`.inlineParams() : plan.targetWhere;
|
|
2950
|
+
if (plan.action === "UPDATE") return query.onConflictDoUpdate({
|
|
2951
|
+
target: plan.target,
|
|
2952
|
+
targetWhere,
|
|
2953
|
+
set: plan.set,
|
|
2954
|
+
setWhere
|
|
2955
|
+
});
|
|
2956
|
+
if (!plan.target) return query.onConflictDoNothing();
|
|
2957
|
+
if (sqlite && targetWhere) {
|
|
2958
|
+
const [key] = Object.entries(getColumns(table)).find(([, column]) => column === plan.target[0]);
|
|
2959
|
+
return query.onConflictDoUpdate({
|
|
2960
|
+
target: plan.target,
|
|
2961
|
+
targetWhere,
|
|
2962
|
+
set: { [key]: sql`${plan.target[0]}` },
|
|
2963
|
+
setWhere: sql`0 = 1`
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
return query.onConflictDoNothing({
|
|
2967
|
+
target: plan.target,
|
|
2968
|
+
where: targetWhere
|
|
2969
|
+
});
|
|
2970
|
+
};
|
|
2801
2971
|
/** `excluded.<column>` — PostgreSQL and SQLite name the rejected row this way. */
|
|
2802
2972
|
const excludedColumnRef = (columnName) => sql`excluded.${sql.identifier(columnName)}`;
|
|
2803
2973
|
/** `values(<column>)` — MySQL's equivalent inside ON DUPLICATE KEY UPDATE. */
|
|
@@ -3079,6 +3249,21 @@ const decodeCursor = (after, entries) => {
|
|
|
3079
3249
|
return payload.v.map(decodeCursorValue);
|
|
3080
3250
|
};
|
|
3081
3251
|
/**
|
|
3252
|
+
* `col > value` (asc) or `col < value` (desc), compared by the rule the ORDER BY sorts by.
|
|
3253
|
+
* MySQL sorts an ENUM by its declaration index but compares it with a string as a string, so
|
|
3254
|
+
* there the comparison is between `col + 0` (the 1-based index) and the value's own index.
|
|
3255
|
+
*/
|
|
3256
|
+
const strictComparison = (col, direction, value) => {
|
|
3257
|
+
if (is(col, MySqlEnumColumn) || is(col, MySqlEnumObjectColumn)) {
|
|
3258
|
+
const index = col.enumValues?.indexOf(value) ?? -1;
|
|
3259
|
+
if (index >= 0) {
|
|
3260
|
+
const position = sql`(${col} + 0)`;
|
|
3261
|
+
return direction === "asc" ? sql`${position} > ${index + 1}` : sql`${position} < ${index + 1}`;
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
return direction === "asc" ? gt(col, value) : lt(col, value);
|
|
3265
|
+
};
|
|
3266
|
+
/**
|
|
3082
3267
|
* The keyset predicate selecting rows strictly after the cursor position in the given total
|
|
3083
3268
|
* order — the expanded lexicographic form
|
|
3084
3269
|
* `after(k0) OR (k0 = v0 AND after(k1)) OR (k0 = v0 AND k1 = v1 AND after(k2)) …`,
|
|
@@ -3103,7 +3288,7 @@ const buildCursorCondition = (table, entries, values, nullOrdering) => {
|
|
|
3103
3288
|
let strictlyAfter;
|
|
3104
3289
|
if (value === null || value === void 0) strictlyAfter = nullsSortLast ? void 0 : isNotNull(col);
|
|
3105
3290
|
else {
|
|
3106
|
-
const comparison =
|
|
3291
|
+
const comparison = strictComparison(col, direction, value);
|
|
3107
3292
|
strictlyAfter = nullsSortLast ? or(comparison, isNull(col)) : comparison;
|
|
3108
3293
|
}
|
|
3109
3294
|
if (strictlyAfter) disjuncts.push(equalities.length ? and(...equalities, strictlyAfter) : strictlyAfter);
|
|
@@ -3306,6 +3491,115 @@ const getUniqueTargets = (table, getTableConfig) => {
|
|
|
3306
3491
|
*/
|
|
3307
3492
|
const getUniqueColumnSets = (table, getTableConfig, options = {}) => getUniqueTargets(table, getTableConfig).filter((target) => options.partial || !target.where).map((target) => target.columns);
|
|
3308
3493
|
//#endregion
|
|
3494
|
+
//#region src/util/builders/common/limits.ts
|
|
3495
|
+
/**
|
|
3496
|
+
* The `limit` to send with an `offset` the request gave without one. SQLite and MySQL accept
|
|
3497
|
+
* `OFFSET` only after a `LIMIT`, so `offset` alone has to carry a limit that means "all of
|
|
3498
|
+
* them": SQLite reads `LIMIT -1` that way, and on MySQL it is the largest integer every driver
|
|
3499
|
+
* binds exactly (MySQL's own documented 2^64-1 does not fit a JS number). PostgreSQL takes
|
|
3500
|
+
* `OFFSET` alone, so the limit is left as it was.
|
|
3501
|
+
*/
|
|
3502
|
+
const offsetOnlyLimit = (table, limit, offset) => {
|
|
3503
|
+
if (limit != null || !offset) return limit;
|
|
3504
|
+
const firstColumn = Object.values(getColumns(table))[0];
|
|
3505
|
+
switch (firstColumn ? columnDialect(firstColumn) : void 0) {
|
|
3506
|
+
case "sqlite": return -1;
|
|
3507
|
+
case "mysql": return Number.MAX_SAFE_INTEGER;
|
|
3508
|
+
default: return limit;
|
|
3509
|
+
}
|
|
3510
|
+
};
|
|
3511
|
+
/**
|
|
3512
|
+
* Substitutes a table's default ordering for an absent `orderBy`.
|
|
3513
|
+
*
|
|
3514
|
+
* Applied where the arguments are first read, so everything downstream — the cursor tuple, a
|
|
3515
|
+
* `distinct` pass, the plain-select fallback — sees one effective ordering and cannot
|
|
3516
|
+
* disagree about it. Only a missing argument is replaced: `orderBy: {}` is a request for no
|
|
3517
|
+
* ordering, and stays one.
|
|
3518
|
+
*/
|
|
3519
|
+
const withDefaultOrderBy = (args, tableName, defaults) => {
|
|
3520
|
+
if (!defaults || args?.orderBy != null) return args;
|
|
3521
|
+
const fallback = defaults(tableName);
|
|
3522
|
+
return fallback ? {
|
|
3523
|
+
...args,
|
|
3524
|
+
orderBy: fallback
|
|
3525
|
+
} : args;
|
|
3526
|
+
};
|
|
3527
|
+
/**
|
|
3528
|
+
* Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
|
|
3529
|
+
* dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
|
|
3530
|
+
* straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
|
|
3531
|
+
* whether or not the table has a limit policy.
|
|
3532
|
+
*/
|
|
3533
|
+
const assertPagingNotNegative = (paging, errorCtx) => {
|
|
3534
|
+
for (const name of ["limit", "offset"]) {
|
|
3535
|
+
const value = paging[name];
|
|
3536
|
+
if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
|
|
3537
|
+
code: "DRIZZLE_INVALID_INPUT_VALUE",
|
|
3538
|
+
...errorCtx
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
};
|
|
3542
|
+
/**
|
|
3543
|
+
* The `limit` a query actually runs with, given the policy for the table it reads.
|
|
3544
|
+
*
|
|
3545
|
+
* A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
|
|
3546
|
+
* that is configured — an absent limit means every row, which is above any maximum. A limit
|
|
3547
|
+
* the client did ask for is rejected when it exceeds `maxLimit`, since silently truncating a
|
|
3548
|
+
* page tells a paginating client it has reached the end when it has not; `clampToMax` opts
|
|
3549
|
+
* into the truncation instead. A limit the *policy* supplied is capped rather than rejected —
|
|
3550
|
+
* a misconfigured `defaultLimit` above `maxLimit` is the operator's problem, not something to
|
|
3551
|
+
* fail a client's query over.
|
|
3552
|
+
*/
|
|
3553
|
+
const applyLimitPolicy = (limit, policy, errorCtx) => {
|
|
3554
|
+
assertPagingNotNegative({ limit }, errorCtx);
|
|
3555
|
+
if (!policy) return limit ?? void 0;
|
|
3556
|
+
const { defaultLimit, maxLimit, clampToMax } = policy;
|
|
3557
|
+
if (limit == null) {
|
|
3558
|
+
const fallback = defaultLimit ?? maxLimit;
|
|
3559
|
+
if (fallback === void 0) return;
|
|
3560
|
+
return maxLimit === void 0 ? fallback : Math.min(fallback, maxLimit);
|
|
3561
|
+
}
|
|
3562
|
+
if (maxLimit !== void 0 && limit > maxLimit) {
|
|
3563
|
+
if (clampToMax) return maxLimit;
|
|
3564
|
+
throw drizzleError(`'limit' of ${limit} exceeds the maximum of ${maxLimit}.`, {
|
|
3565
|
+
code: "DRIZZLE_LIMIT_EXCEEDED",
|
|
3566
|
+
...errorCtx
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
return limit;
|
|
3570
|
+
};
|
|
3571
|
+
/** Merges a per-table override over the global policy, or `undefined` when neither bounds anything. */
|
|
3572
|
+
const resolveLimitPolicy = (global, table) => {
|
|
3573
|
+
const defaultLimit = table?.defaultLimit ?? global?.defaultLimit;
|
|
3574
|
+
const maxLimit = table?.maxLimit ?? global?.maxLimit;
|
|
3575
|
+
if (defaultLimit === void 0 && maxLimit === void 0) return;
|
|
3576
|
+
return {
|
|
3577
|
+
defaultLimit,
|
|
3578
|
+
maxLimit,
|
|
3579
|
+
clampToMax: table?.clampToMax ?? global?.clampToMax ?? false
|
|
3580
|
+
};
|
|
3581
|
+
};
|
|
3582
|
+
/**
|
|
3583
|
+
* A paginated field costs its page size times whatever one row of it costs, so `users(limit: 100)
|
|
3584
|
+
* { posts(limit: 10) { id } }` is charged for the thousand rows it can return rather than the two
|
|
3585
|
+
* fields it mentions. `childComplexity` floors at 1 so a row is never free.
|
|
3586
|
+
*
|
|
3587
|
+
* The page size is the *effective* one: a request without `limit` is priced at the policy's
|
|
3588
|
+
* `defaultLimit` (or its `maxLimit` when that is all it sets) rather than the estimator's guess, and no request is priced above `maxLimit`,
|
|
3589
|
+
* so the cost a query is charged matches the rows it can actually pull.
|
|
3590
|
+
*/
|
|
3591
|
+
const listFieldComplexity = (options, policy) => ({ args, childComplexity }) => {
|
|
3592
|
+
const limit = args["limit"];
|
|
3593
|
+
let rows = (typeof limit === "number" && limit > 0 ? limit : void 0) ?? policy?.defaultLimit ?? policy?.maxLimit ?? options.defaultListSize;
|
|
3594
|
+
if (policy?.maxLimit !== void 0) rows = Math.min(rows, policy.maxLimit);
|
|
3595
|
+
return rows * Math.max(childComplexity, 1);
|
|
3596
|
+
};
|
|
3597
|
+
/**
|
|
3598
|
+
* An aggregate returns a single row but reads however many match, so its cost tracks the scan
|
|
3599
|
+
* rather than the response.
|
|
3600
|
+
*/
|
|
3601
|
+
const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
|
|
3602
|
+
//#endregion
|
|
3309
3603
|
//#region src/util/builders/common/distinct.ts
|
|
3310
3604
|
/**
|
|
3311
3605
|
* Alias the parent key comes back under when the pass runs through a junction table. See
|
|
@@ -3353,13 +3647,15 @@ const selectDistinctKeys = async (params) => {
|
|
|
3353
3647
|
...junction ? { [DISTINCT_PARENT]: sub[DISTINCT_PARENT] } : {}
|
|
3354
3648
|
}).from(sub).where(eq(sub[DISTINCT_RN], 1)).orderBy(...outerOrder);
|
|
3355
3649
|
if (offset) query = query.offset(offset);
|
|
3356
|
-
|
|
3650
|
+
const effectiveLimit = offsetOnlyLimit(table, limit, offset);
|
|
3651
|
+
if (effectiveLimit != null) query = query.limit(effectiveLimit < 0 ? sql`${effectiveLimit}` : effectiveLimit);
|
|
3357
3652
|
return await query;
|
|
3358
3653
|
};
|
|
3359
3654
|
/**
|
|
3360
3655
|
* Condition matching exactly the rows identified by `keys` — an `IN (…)` for a single-column
|
|
3361
|
-
* primary key, an `OR` of per-row equality for a composite one
|
|
3362
|
-
*
|
|
3656
|
+
* primary key, an `OR` of per-row equality for a composite one — nested as a balanced tree, so
|
|
3657
|
+
* thousands of keys stay within SQLite's expression-depth limit. `table` may be the aliased RQB
|
|
3658
|
+
* proxy.
|
|
3363
3659
|
*/
|
|
3364
3660
|
const primaryKeyRestriction = (table, pkNames, keys) => {
|
|
3365
3661
|
const cols = getColumns(table);
|
|
@@ -3367,7 +3663,7 @@ const primaryKeyRestriction = (table, pkNames, keys) => {
|
|
|
3367
3663
|
const name = pkNames[0];
|
|
3368
3664
|
return inArray(cols[name], keys.map((key) => key[name]));
|
|
3369
3665
|
}
|
|
3370
|
-
return
|
|
3666
|
+
return balancedOr(keys.map((key) => and(...pkNames.map((name) => eq(cols[name], key[name])))));
|
|
3371
3667
|
};
|
|
3372
3668
|
//#endregion
|
|
3373
3669
|
//#region src/util/builders/common/docs.ts
|
|
@@ -3513,99 +3809,6 @@ const innerOrderType = (typeName) => sharedType(typeName, {
|
|
|
3513
3809
|
}
|
|
3514
3810
|
}));
|
|
3515
3811
|
//#endregion
|
|
3516
|
-
//#region src/util/builders/common/limits.ts
|
|
3517
|
-
/**
|
|
3518
|
-
* Substitutes a table's default ordering for an absent `orderBy`.
|
|
3519
|
-
*
|
|
3520
|
-
* Applied where the arguments are first read, so everything downstream — the cursor tuple, a
|
|
3521
|
-
* `distinct` pass, the plain-select fallback — sees one effective ordering and cannot
|
|
3522
|
-
* disagree about it. Only a missing argument is replaced: `orderBy: {}` is a request for no
|
|
3523
|
-
* ordering, and stays one.
|
|
3524
|
-
*/
|
|
3525
|
-
const withDefaultOrderBy = (args, tableName, defaults) => {
|
|
3526
|
-
if (!defaults || args?.orderBy != null) return args;
|
|
3527
|
-
const fallback = defaults(tableName);
|
|
3528
|
-
return fallback ? {
|
|
3529
|
-
...args,
|
|
3530
|
-
orderBy: fallback
|
|
3531
|
-
} : args;
|
|
3532
|
-
};
|
|
3533
|
-
/**
|
|
3534
|
-
* Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
|
|
3535
|
-
* dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
|
|
3536
|
-
* straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
|
|
3537
|
-
* whether or not the table has a limit policy.
|
|
3538
|
-
*/
|
|
3539
|
-
const assertPagingNotNegative = (paging, errorCtx) => {
|
|
3540
|
-
for (const name of ["limit", "offset"]) {
|
|
3541
|
-
const value = paging[name];
|
|
3542
|
-
if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
|
|
3543
|
-
code: "DRIZZLE_INVALID_INPUT_VALUE",
|
|
3544
|
-
...errorCtx
|
|
3545
|
-
});
|
|
3546
|
-
}
|
|
3547
|
-
};
|
|
3548
|
-
/**
|
|
3549
|
-
* The `limit` a query actually runs with, given the policy for the table it reads.
|
|
3550
|
-
*
|
|
3551
|
-
* A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
|
|
3552
|
-
* that is configured — an absent limit means every row, which is above any maximum. A limit
|
|
3553
|
-
* the client did ask for is rejected when it exceeds `maxLimit`, since silently truncating a
|
|
3554
|
-
* page tells a paginating client it has reached the end when it has not; `clampToMax` opts
|
|
3555
|
-
* into the truncation instead. A limit the *policy* supplied is capped rather than rejected —
|
|
3556
|
-
* a misconfigured `defaultLimit` above `maxLimit` is the operator's problem, not something to
|
|
3557
|
-
* fail a client's query over.
|
|
3558
|
-
*/
|
|
3559
|
-
const applyLimitPolicy = (limit, policy, errorCtx) => {
|
|
3560
|
-
assertPagingNotNegative({ limit }, errorCtx);
|
|
3561
|
-
if (!policy) return limit ?? void 0;
|
|
3562
|
-
const { defaultLimit, maxLimit, clampToMax } = policy;
|
|
3563
|
-
if (limit == null) {
|
|
3564
|
-
const fallback = defaultLimit ?? maxLimit;
|
|
3565
|
-
if (fallback === void 0) return;
|
|
3566
|
-
return maxLimit === void 0 ? fallback : Math.min(fallback, maxLimit);
|
|
3567
|
-
}
|
|
3568
|
-
if (maxLimit !== void 0 && limit > maxLimit) {
|
|
3569
|
-
if (clampToMax) return maxLimit;
|
|
3570
|
-
throw drizzleError(`'limit' of ${limit} exceeds the maximum of ${maxLimit}.`, {
|
|
3571
|
-
code: "DRIZZLE_LIMIT_EXCEEDED",
|
|
3572
|
-
...errorCtx
|
|
3573
|
-
});
|
|
3574
|
-
}
|
|
3575
|
-
return limit;
|
|
3576
|
-
};
|
|
3577
|
-
/** Merges a per-table override over the global policy, or `undefined` when neither bounds anything. */
|
|
3578
|
-
const resolveLimitPolicy = (global, table) => {
|
|
3579
|
-
const defaultLimit = table?.defaultLimit ?? global?.defaultLimit;
|
|
3580
|
-
const maxLimit = table?.maxLimit ?? global?.maxLimit;
|
|
3581
|
-
if (defaultLimit === void 0 && maxLimit === void 0) return;
|
|
3582
|
-
return {
|
|
3583
|
-
defaultLimit,
|
|
3584
|
-
maxLimit,
|
|
3585
|
-
clampToMax: table?.clampToMax ?? global?.clampToMax ?? false
|
|
3586
|
-
};
|
|
3587
|
-
};
|
|
3588
|
-
/**
|
|
3589
|
-
* A paginated field costs its page size times whatever one row of it costs, so `users(limit: 100)
|
|
3590
|
-
* { posts(limit: 10) { id } }` is charged for the thousand rows it can return rather than the two
|
|
3591
|
-
* fields it mentions. `childComplexity` floors at 1 so a row is never free.
|
|
3592
|
-
*
|
|
3593
|
-
* The page size is the *effective* one: a request without `limit` is priced at the policy's
|
|
3594
|
-
* `defaultLimit` rather than the estimator's guess, and no request is priced above `maxLimit`,
|
|
3595
|
-
* so the cost a query is charged matches the rows it can actually pull.
|
|
3596
|
-
*/
|
|
3597
|
-
const listFieldComplexity = (options, policy) => ({ args, childComplexity }) => {
|
|
3598
|
-
const limit = args["limit"];
|
|
3599
|
-
let rows = (typeof limit === "number" && limit > 0 ? limit : void 0) ?? policy?.defaultLimit ?? options.defaultListSize;
|
|
3600
|
-
if (policy?.maxLimit !== void 0) rows = Math.min(rows, policy.maxLimit);
|
|
3601
|
-
return rows * Math.max(childComplexity, 1);
|
|
3602
|
-
};
|
|
3603
|
-
/**
|
|
3604
|
-
* An aggregate returns a single row but reads however many match, so its cost tracks the scan
|
|
3605
|
-
* rather than the response.
|
|
3606
|
-
*/
|
|
3607
|
-
const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
|
|
3608
|
-
//#endregion
|
|
3609
3812
|
//#region src/util/parse-resolve-info.ts
|
|
3610
3813
|
/** Unwraps `[T]!`-style wrappers down to the named type. `getNamedType` without the realm tie. */
|
|
3611
3814
|
const namedTypeOf = (type) => {
|
|
@@ -3769,14 +3972,10 @@ const NUMERIC_SCALARS = /* @__PURE__ */ new Set([
|
|
|
3769
3972
|
]);
|
|
3770
3973
|
/**
|
|
3771
3974
|
* Column types that convert to a GraphQL list without being a list *in the database* —
|
|
3772
|
-
* a bytea reads as `[Int]`, a vector
|
|
3773
|
-
* them is not the operation `push` means.
|
|
3975
|
+
* a bytea or SQLite buffer blob reads as `[Int]`, a vector, a point or a line as `[Float]` —
|
|
3976
|
+
* so appending to them is not the operation `push` means.
|
|
3774
3977
|
*/
|
|
3775
|
-
const
|
|
3776
|
-
"PgVector",
|
|
3777
|
-
"PgGeometry",
|
|
3778
|
-
"PgBytea"
|
|
3779
|
-
]);
|
|
3978
|
+
const isNonListArrayColumn = (column) => isFloatTupleColumn(column) || isBufferColumn(column);
|
|
3780
3979
|
const kindCache = /* @__PURE__ */ new WeakMap();
|
|
3781
3980
|
/**
|
|
3782
3981
|
* Whether a column takes an operations input, and which one.
|
|
@@ -3793,7 +3992,7 @@ const fieldUpdateKind = (column, columnName, tableName) => {
|
|
|
3793
3992
|
if (!getColumnScalarOverride(column, true)) {
|
|
3794
3993
|
const { type } = drizzleColumnToGraphQLType(column, columnName, tableName, true, false, true);
|
|
3795
3994
|
if (NUMERIC_SCALARS.has(type)) kind = "numeric";
|
|
3796
|
-
else if (type instanceof GraphQLList && !
|
|
3995
|
+
else if (type instanceof GraphQLList && !isNonListArrayColumn(column)) kind = "list";
|
|
3797
3996
|
}
|
|
3798
3997
|
kindCache.set(column, kind);
|
|
3799
3998
|
return kind;
|
|
@@ -4064,6 +4263,16 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
|
|
|
4064
4263
|
if (!relFieldSelection) continue;
|
|
4065
4264
|
const eagerArgs = field.args;
|
|
4066
4265
|
if (!is(relEntry.relation ?? relEntry, One) && (eagerArgs?.after != null || !!(eagerArgs?.distinct)?.length || isCursorFieldSelected(relFieldSelection, tables[targetTableName]))) continue;
|
|
4266
|
+
const junctionName = junctionTableName(tables, relEntry);
|
|
4267
|
+
if (hasJunctionPolicy(scope, junctionName)) {
|
|
4268
|
+
if (!extractJunctionColumns(relEntry)) throw drizzleError(`Relation '${tableName}.${relName}' goes through '${junctionName}', which has a scope or soft delete, over a composite key. Its links can only be filtered when the junction joins on one column per side.`, {
|
|
4269
|
+
code: "DRIZZLE_UNSUPPORTED_RELATION",
|
|
4270
|
+
table: tableName,
|
|
4271
|
+
operation: "relation",
|
|
4272
|
+
relation: relName
|
|
4273
|
+
});
|
|
4274
|
+
continue;
|
|
4275
|
+
}
|
|
4067
4276
|
const columns = extractSelectedColumnsFromTree(relFieldSelection, tables[targetTableName], {
|
|
4068
4277
|
tableName: targetTableName,
|
|
4069
4278
|
relationMap,
|
|
@@ -4089,9 +4298,11 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
|
|
|
4089
4298
|
thisRecord.where = relWhere || scope?.has(targetTableName, relDeleted) ? { RAW: (aliasedTable) => withScope(scope, targetTableName, aliasedTable, relWhere ? extractFilters(aliasedTable, relName, relWhere, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, relDeleted) } : void 0;
|
|
4090
4299
|
const hasPagination = offset != null || limit != null;
|
|
4091
4300
|
const pkNames = targetPkNames ?? [];
|
|
4092
|
-
|
|
4301
|
+
const tiebreak = hasPagination && pkNames.length > 0;
|
|
4302
|
+
const requestOrder = relationArgs?.orderBy;
|
|
4303
|
+
thisRecord.orderBy = requestOrder || tiebreak ? (aliasedTable) => [...requestOrder ? extractOrderBy(aliasedTable, requestOrder, relationFilterCtx(filterCtx, targetTableName, scope), relWhere) : [], ...tiebreak ? primaryKeyOrderExprs(aliasedTable, pkNames) : []] : void 0;
|
|
4093
4304
|
thisRecord.offset = offset;
|
|
4094
|
-
thisRecord.limit = limit;
|
|
4305
|
+
thisRecord.limit = offsetOnlyLimit(tables[targetTableName], limit, offset);
|
|
4095
4306
|
thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
|
|
4096
4307
|
args[relName] = omitUndefinedKeys(thisRecord);
|
|
4097
4308
|
}
|
|
@@ -4182,7 +4393,7 @@ const runRelationalSelect = async (opts) => {
|
|
|
4182
4393
|
const preciseProps = Object.keys(cursorPreciseExtras(table, cursorEntries));
|
|
4183
4394
|
if (preciseProps.length) params.extras = Object.fromEntries(preciseProps.map((prop) => [prop, (aliasedTable) => cursorPreciseExtras(aliasedTable, cursorEntries)[prop]]));
|
|
4184
4395
|
}
|
|
4185
|
-
params.limit = distinctKeys ? void 0 : opts.limit;
|
|
4396
|
+
params.limit = distinctKeys ? void 0 : offsetOnlyLimit(table, opts.limit, offset);
|
|
4186
4397
|
const result = await queryBase.findMany(omitUndefinedKeys(params));
|
|
4187
4398
|
if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
|
|
4188
4399
|
return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
|
|
@@ -4239,6 +4450,8 @@ const eagerLoadMutationRelations = async (db, tableName, rows, pkNames, withPara
|
|
|
4239
4450
|
}
|
|
4240
4451
|
return rows;
|
|
4241
4452
|
};
|
|
4453
|
+
/** The longest delay `setTimeout` honours; anything above, Infinity included, fires after 1 ms. */
|
|
4454
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
4242
4455
|
const createMutationTxCtx = (options) => options ? {
|
|
4243
4456
|
timeoutMs: options.timeoutMs,
|
|
4244
4457
|
fieldNames: /* @__PURE__ */ new Set(),
|
|
@@ -4265,28 +4478,28 @@ const includedByDirectives = (node, variables) => {
|
|
|
4265
4478
|
* this build did not generate — its completion cannot be tracked, so the caller must not
|
|
4266
4479
|
* open a transaction it might never be able to close.
|
|
4267
4480
|
*/
|
|
4268
|
-
const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments) => {
|
|
4481
|
+
const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce) => {
|
|
4269
4482
|
for (const selection of selectionSet.selections) {
|
|
4270
4483
|
if (!includedByDirectives(selection, variables)) continue;
|
|
4271
4484
|
if (selection.kind === "Field") {
|
|
4272
4485
|
const name = selection.name.value;
|
|
4273
4486
|
if (name === "__typename") continue;
|
|
4274
4487
|
if (!known.has(name)) return false;
|
|
4275
|
-
keys.add(selection.alias?.value ?? name);
|
|
4488
|
+
if (argumentsCoerce(selection)) keys.add(selection.alias?.value ?? name);
|
|
4276
4489
|
} else if (selection.kind === "InlineFragment") {
|
|
4277
|
-
if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
|
|
4490
|
+
if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
|
|
4278
4491
|
} else if (selection.kind === "FragmentSpread") {
|
|
4279
4492
|
const fragmentName = selection.name.value;
|
|
4280
4493
|
if (visitedFragments.has(fragmentName)) continue;
|
|
4281
4494
|
visitedFragments.add(fragmentName);
|
|
4282
4495
|
const fragment = fragments?.[fragmentName];
|
|
4283
4496
|
if (!fragment) return false;
|
|
4284
|
-
if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
|
|
4497
|
+
if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
|
|
4285
4498
|
}
|
|
4286
4499
|
}
|
|
4287
4500
|
return true;
|
|
4288
4501
|
};
|
|
4289
|
-
const abortedBatchError = () => drizzleError("Drizzle-GraphQL Error: Mutation was not executed because an earlier mutation in the same request failed and the shared transaction was rolled back.", { code: "DRIZZLE_TRANSACTION_ABORTED" });
|
|
4502
|
+
const abortedBatchError = (state, timeoutMs) => drizzleError(state.timedOut ? `Drizzle-GraphQL Error: Mutation was not executed because the shared transaction timed out after ${timeoutMs}ms waiting for the remaining mutation fields and was rolled back.` : "Drizzle-GraphQL Error: Mutation was not executed because an earlier mutation in the same request failed and the shared transaction was rolled back.", { code: "DRIZZLE_TRANSACTION_ABORTED" });
|
|
4290
4503
|
/** Opens the request-wide transaction and wires up latch, inactivity timer and cleanup. */
|
|
4291
4504
|
const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
4292
4505
|
let release;
|
|
@@ -4321,6 +4534,7 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
|
4321
4534
|
completed: 0,
|
|
4322
4535
|
failure: void 0,
|
|
4323
4536
|
aborted: false,
|
|
4537
|
+
timedOut: false,
|
|
4324
4538
|
done: false,
|
|
4325
4539
|
ready,
|
|
4326
4540
|
settled,
|
|
@@ -4335,8 +4549,9 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
|
4335
4549
|
suspendTimer: clearTimer,
|
|
4336
4550
|
bumpTimer: () => {
|
|
4337
4551
|
clearTimer();
|
|
4338
|
-
if (state.done) return;
|
|
4552
|
+
if (state.done || txCtx.timeoutMs === Number.POSITIVE_INFINITY) return;
|
|
4339
4553
|
timer = setTimeout(() => {
|
|
4554
|
+
if (!state.aborted) state.timedOut = true;
|
|
4340
4555
|
state.abort(drizzleError(`Drizzle-GraphQL Error: Shared mutation transaction timed out after ${txCtx.timeoutMs}ms waiting for the remaining mutation fields; rolling back.`, { code: "DRIZZLE_TRANSACTION_ABORTED" }));
|
|
4341
4556
|
}, txCtx.timeoutMs);
|
|
4342
4557
|
timer.unref?.();
|
|
@@ -4355,15 +4570,15 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
|
4355
4570
|
state.bumpTimer();
|
|
4356
4571
|
return state;
|
|
4357
4572
|
};
|
|
4358
|
-
const runOnSharedTx = async (state, body) => {
|
|
4573
|
+
const runOnSharedTx = async (state, timeoutMs, body) => {
|
|
4359
4574
|
if (state.aborted) {
|
|
4360
4575
|
await state.settled.catch(() => {});
|
|
4361
|
-
throw abortedBatchError();
|
|
4576
|
+
throw abortedBatchError(state, timeoutMs);
|
|
4362
4577
|
}
|
|
4363
4578
|
state.suspendTimer();
|
|
4364
4579
|
const tx = await state.ready;
|
|
4365
4580
|
try {
|
|
4366
|
-
if (state.aborted) throw abortedBatchError();
|
|
4581
|
+
if (state.aborted) throw abortedBatchError(state, timeoutMs);
|
|
4367
4582
|
const result = await body(tx);
|
|
4368
4583
|
state.completed += 1;
|
|
4369
4584
|
if (state.completed >= state.expected) {
|
|
@@ -4396,14 +4611,24 @@ const runMutation = async (db, context, info, txCtx, body, forceTx) => {
|
|
|
4396
4611
|
if (contextIsObject && context[drizzleExecutorKey]) return body(context[drizzleExecutorKey]);
|
|
4397
4612
|
if (!txCtx || info.operation.operation !== "mutation") return forceTx ? db.transaction((tx) => body(tx)) : body(db);
|
|
4398
4613
|
const keys = /* @__PURE__ */ new Set();
|
|
4399
|
-
|
|
4614
|
+
const mutationFields = info.schema.getMutationType()?.getFields() ?? {};
|
|
4615
|
+
state = !collectRootMutationKeys(info.operation.selectionSet, info.fragments, info.variableValues, txCtx.fieldNames, keys, /* @__PURE__ */ new Set(), (node) => {
|
|
4616
|
+
const fieldDef = mutationFields[node.name.value];
|
|
4617
|
+
if (!fieldDef) return false;
|
|
4618
|
+
try {
|
|
4619
|
+
getArgumentValues(fieldDef, node, info.variableValues);
|
|
4620
|
+
return true;
|
|
4621
|
+
} catch {
|
|
4622
|
+
return false;
|
|
4623
|
+
}
|
|
4624
|
+
}) || keys.size <= 1 ? { kind: "passthrough" } : openSharedTx(db, context, txCtx, stateKey, keys.size);
|
|
4400
4625
|
txCtx.states.set(stateKey, state);
|
|
4401
4626
|
}
|
|
4402
4627
|
if (state.kind === "passthrough") {
|
|
4403
4628
|
const executor = resolveExecutor(db, context);
|
|
4404
4629
|
return forceTx && executor === db ? db.transaction((tx) => body(tx)) : body(executor);
|
|
4405
4630
|
}
|
|
4406
|
-
return runOnSharedTx(state, body);
|
|
4631
|
+
return runOnSharedTx(state, txCtx.timeoutMs, body);
|
|
4407
4632
|
};
|
|
4408
4633
|
//#endregion
|
|
4409
4634
|
//#region src/util/builders/common/write-hooks.ts
|
|
@@ -4557,10 +4782,11 @@ const hardDeleteArg = {
|
|
|
4557
4782
|
description: "Remove the matched rows instead of marking them deleted. Reads at `INCLUDE`, so it reaches rows that are already marked; a `scope` still confines what it can reach."
|
|
4558
4783
|
};
|
|
4559
4784
|
const prepareMutationRelationColumns = (params) => {
|
|
4560
|
-
const { relationMap, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
|
|
4785
|
+
const { relationMap, filterCtx, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
|
|
4561
4786
|
const typeName = resolveObjectTypeName(tableName, typeNameMapper, resolveName);
|
|
4562
4787
|
const withParams = relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, {
|
|
4563
4788
|
typeNameMapper,
|
|
4789
|
+
filterCtx,
|
|
4564
4790
|
limits: params.limits,
|
|
4565
4791
|
scope: params.scope,
|
|
4566
4792
|
defaultOrderBy: params.defaultOrderBy,
|
|
@@ -4570,7 +4796,8 @@ const prepareMutationRelationColumns = (params) => {
|
|
|
4570
4796
|
const baseColumns = extractSelectedColumnsFromTreeSQLFormat(parsedInfo.fieldsByTypeName[typeName], table, {
|
|
4571
4797
|
tableName,
|
|
4572
4798
|
relationMap,
|
|
4573
|
-
tables
|
|
4799
|
+
tables,
|
|
4800
|
+
allRelations: filterCtx?.relationMap
|
|
4574
4801
|
});
|
|
4575
4802
|
return {
|
|
4576
4803
|
columns: hasRelations ? withPrimaryKeyColumns(baseColumns, table, pkNames) : baseColumns,
|
|
@@ -4706,7 +4933,7 @@ const assertSingleMatch = async (executor, table, filters) => {
|
|
|
4706
4933
|
* MySQL (no `RETURNING`; its hooks see `rows: []` as on every MySQL mutation), skips the
|
|
4707
4934
|
* `RETURNING` and reads the count off the driver's result via {@link rowsAffected}.
|
|
4708
4935
|
*/
|
|
4709
|
-
const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested, policies, pkNames = [], returnsRows }) => {
|
|
4936
|
+
const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested, policies, pkNames = [], returnsRows, writeFilter }) => {
|
|
4710
4937
|
const softDelete = kind === "delete" ? policies?.softDelete?.(tableName) : void 0;
|
|
4711
4938
|
const canHardDelete = softDelete?.hardDelete === true;
|
|
4712
4939
|
const queryArgs = {
|
|
@@ -4749,7 +4976,7 @@ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, f
|
|
|
4749
4976
|
required: requireWhere,
|
|
4750
4977
|
deleted: hard ? "INCLUDE" : void 0
|
|
4751
4978
|
});
|
|
4752
|
-
if (filters) query = query.where(filters);
|
|
4979
|
+
if (filters) query = query.where(writeFilter ? writeFilter(filters) : filters);
|
|
4753
4980
|
if (!readsRows) {
|
|
4754
4981
|
const result = await query;
|
|
4755
4982
|
await after();
|
|
@@ -4809,27 +5036,40 @@ const getOrCreateLoader = (context, key, batchFn) => {
|
|
|
4809
5036
|
if (!loaders.has(key)) loaders.set(key, new BatchLoader(batchFn));
|
|
4810
5037
|
return loaders.get(key);
|
|
4811
5038
|
};
|
|
5039
|
+
const requestCacheNode = () => ({
|
|
5040
|
+
children: /* @__PURE__ */ new WeakMap(),
|
|
5041
|
+
entries: /* @__PURE__ */ new Map()
|
|
5042
|
+
});
|
|
4812
5043
|
/**
|
|
4813
5044
|
* Memoizes a value that a resolver derives from its own field — its args and its selection —
|
|
4814
5045
|
* for the length of one request. Resolvers run once per parent row, so anything derived only
|
|
4815
5046
|
* from the field is otherwise recomputed for every row of a batch.
|
|
4816
5047
|
*
|
|
4817
|
-
* The
|
|
4818
|
-
*
|
|
4819
|
-
*
|
|
4820
|
-
*
|
|
5048
|
+
* The value is keyed by every AST node of the field — `info.fieldNodes`, the nodes graphql-js
|
|
5049
|
+
* merged into this one response field — not only the first. A fragment's field node is the first
|
|
5050
|
+
* node wherever the fragment is spread, but the merged selection differs when a sibling selection
|
|
5051
|
+
* at one spread site adds to it, so keying on one node would serve one site the other's selection.
|
|
5052
|
+
* The nodes are matched by identity, in order, through a chain of weak maps, so a document cached
|
|
5053
|
+
* across requests never carries values between them (the root lives on the context), and an array
|
|
5054
|
+
* graphql rebuilds per call still hits. `key` separates fields that share nodes: one fragment
|
|
5055
|
+
* selection can resolve for more than one parent type. Without a context object, or without
|
|
5056
|
+
* nodes, there is nowhere request-scoped to cache, so the value is simply computed.
|
|
4821
5057
|
*
|
|
4822
5058
|
* `compute` must not read the parent row.
|
|
4823
5059
|
*/
|
|
4824
|
-
const getOrCreateRequestValue = (context,
|
|
4825
|
-
if (!context || typeof context !== "object" || !node || typeof node !== "object") return compute();
|
|
4826
|
-
if (!context[DRIZZLE_REQUEST_CACHE_KEY]) context[DRIZZLE_REQUEST_CACHE_KEY] =
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
5060
|
+
const getOrCreateRequestValue = (context, nodes, key, compute) => {
|
|
5061
|
+
if (!context || typeof context !== "object" || !Array.isArray(nodes) || !nodes.length || nodes.some((node) => !node || typeof node !== "object")) return compute();
|
|
5062
|
+
if (!context[DRIZZLE_REQUEST_CACHE_KEY]) context[DRIZZLE_REQUEST_CACHE_KEY] = requestCacheNode();
|
|
5063
|
+
let level = context[DRIZZLE_REQUEST_CACHE_KEY];
|
|
5064
|
+
for (const node of nodes) {
|
|
5065
|
+
let next = level.children.get(node);
|
|
5066
|
+
if (!next) {
|
|
5067
|
+
next = requestCacheNode();
|
|
5068
|
+
level.children.set(node, next);
|
|
5069
|
+
}
|
|
5070
|
+
level = next;
|
|
4832
5071
|
}
|
|
5072
|
+
const entries = level.entries;
|
|
4833
5073
|
if (entries.has(key)) return entries.get(key);
|
|
4834
5074
|
const value = compute();
|
|
4835
5075
|
entries.set(key, value);
|
|
@@ -5064,6 +5304,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
5064
5304
|
const junction = extractJunctionColumns(relEntry);
|
|
5065
5305
|
const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
|
|
5066
5306
|
const groupKeys = junction ? [PARENT_KEY] : joinPairs.map((pair) => pair.foreignColPropName);
|
|
5307
|
+
const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
|
|
5067
5308
|
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
|
|
5068
5309
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, isOne && relEntry.relation?.optional === false);
|
|
5069
5310
|
const limitPolicy = isOne ? void 0 : limits?.(targetTableName);
|
|
@@ -5108,7 +5349,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
5108
5349
|
const localValues = parentJoinValues(parent, joinPairs);
|
|
5109
5350
|
if (localValues.some((value) => value == null)) return isOne ? null : [];
|
|
5110
5351
|
const { where: whereArg, limit: requestedLimit, offset, after, deleted } = args ?? {};
|
|
5111
|
-
const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes
|
|
5352
|
+
const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes, `relation:${tableName}::${relationName}`, () => {
|
|
5112
5353
|
const orderByArg = isOne ? args?.orderBy : withDefaultOrderBy(args ?? {}, targetTableName, policies?.defaultOrderBy).orderBy;
|
|
5113
5354
|
const limit = applyLimitPolicy(requestedLimit, limitPolicy, errorCtx);
|
|
5114
5355
|
assertPagingNotNegative({ offset }, errorCtx);
|
|
@@ -5152,7 +5393,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
5152
5393
|
const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key, localCols), key])).values()];
|
|
5153
5394
|
const scope = resolveScope(policies, context, filterCtx);
|
|
5154
5395
|
const loadChunk = async (chunkKeys) => {
|
|
5155
|
-
let whereCondition = withScope(scope, targetTableName, targetTable, and(parentKeyCondition(groupCols, chunkKeys), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
|
|
5396
|
+
let whereCondition = withScope(scope, targetTableName, targetTable, and(parentKeyCondition(groupCols, chunkKeys), ownCondition, junction ? junctionPolicy(scope, junctionName, junction.table) : void 0, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
|
|
5156
5397
|
if (distinct) {
|
|
5157
5398
|
const keys = await selectDistinctKeys({
|
|
5158
5399
|
db: executor,
|
|
@@ -5170,7 +5411,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
5170
5411
|
} : void 0
|
|
5171
5412
|
});
|
|
5172
5413
|
if (!keys.length) return /* @__PURE__ */ new Map();
|
|
5173
|
-
whereCondition = and(whereCondition, junction ?
|
|
5414
|
+
whereCondition = and(whereCondition, junction ? balancedOr(keys.map((key) => and(eq(junction.sourceCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
|
|
5174
5415
|
}
|
|
5175
5416
|
const selection = junction ? {
|
|
5176
5417
|
...columns ?? getColumns(targetTable),
|
|
@@ -5307,7 +5548,7 @@ const generateTableOrderCached = (table, innerOrder) => {
|
|
|
5307
5548
|
try {
|
|
5308
5549
|
const columns = visibleColumns(table);
|
|
5309
5550
|
const columnEntries = Object.entries(columns);
|
|
5310
|
-
remapped = Object.fromEntries(columnEntries.
|
|
5551
|
+
remapped = Object.fromEntries(columnEntries.filter(([, column]) => isOrderableColumn(column)).map(([columnName]) => [columnName, { type: innerOrder }]));
|
|
5311
5552
|
if (cacheable) {
|
|
5312
5553
|
let byInnerOrder = orderMap.get(table);
|
|
5313
5554
|
if (!byInnerOrder) {
|
|
@@ -5350,43 +5591,71 @@ const generateTableSelectTypeFieldsCached = (table, tableName, cacheCtx) => {
|
|
|
5350
5591
|
return remapped;
|
|
5351
5592
|
};
|
|
5352
5593
|
/**
|
|
5594
|
+
* The to-one relations a table's OrderBy can order through, as `[relationName, targetTableName]`.
|
|
5595
|
+
* To-many relations are left out: "order a parent by its many children" has no single defined
|
|
5596
|
+
* value to sort on. A relation whose name collides with a column is skipped — the column keeps
|
|
5597
|
+
* the name, even one with no ordering of its own, so `extractOrderBy` never reads it as a relation.
|
|
5598
|
+
*/
|
|
5599
|
+
const orderRelations = (tableName, relationMap, tables) => {
|
|
5600
|
+
const relations = relationMap?.[tableName];
|
|
5601
|
+
const table = tables?.[tableName];
|
|
5602
|
+
if (!relations || !tables || !table) return [];
|
|
5603
|
+
const columns = visibleColumns(table);
|
|
5604
|
+
return Object.entries(relations).filter(([relationName, relEntry]) => {
|
|
5605
|
+
const relation = relEntry.relation ?? relEntry;
|
|
5606
|
+
return !(relationName in columns) && !!tables[relEntry.targetTableName] && is(relation, One) && isFilterableRelation(relation);
|
|
5607
|
+
}).map(([relationName, relEntry]) => [relationName, relEntry.targetTableName]);
|
|
5608
|
+
};
|
|
5609
|
+
/**
|
|
5610
|
+
* Whether a table's OrderBy would have any field: an orderable column of its own, or a to-one
|
|
5611
|
+
* relation to a table that has one. A table of nothing but JSON columns has none, and an input
|
|
5612
|
+
* object with no fields is an invalid schema, so such a table gets no OrderBy and no `orderBy`
|
|
5613
|
+
* argument at all.
|
|
5614
|
+
*/
|
|
5615
|
+
const hasOrderFields = (tableName, relationMap, tables, visiting = /* @__PURE__ */ new Set()) => {
|
|
5616
|
+
const table = tables[tableName];
|
|
5617
|
+
if (table && Object.values(visibleColumns(table)).some((column) => isOrderableColumn(column))) return true;
|
|
5618
|
+
if (visiting.has(tableName)) return false;
|
|
5619
|
+
visiting.add(tableName);
|
|
5620
|
+
return orderRelations(tableName, relationMap, tables).some(([, target]) => hasOrderFields(target, relationMap, tables, visiting));
|
|
5621
|
+
};
|
|
5622
|
+
/**
|
|
5353
5623
|
* Order fields for a table's to-one relations: each takes the target table's own OrderBy
|
|
5354
5624
|
* input, so a list can be sorted by a related row's column (compiled as a correlated
|
|
5355
|
-
* subquery in the ORDER BY — see `extractOrderBy`).
|
|
5356
|
-
*
|
|
5357
|
-
* name collides with a column is skipped — the column keeps the field.
|
|
5625
|
+
* subquery in the ORDER BY — see `extractOrderBy`). A target with no OrderBy of its own is
|
|
5626
|
+
* left out.
|
|
5358
5627
|
*/
|
|
5359
|
-
const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper,
|
|
5360
|
-
const relations = relationMap?.[tableName];
|
|
5361
|
-
if (!relations || !tables) return {};
|
|
5628
|
+
const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper, relationMap, tables) => {
|
|
5362
5629
|
const fields = {};
|
|
5363
|
-
for (const [relationName,
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
if (!targetTable || !is(relation, One) || !isFilterableRelation(relation)) continue;
|
|
5368
|
-
fields[relationName] = {
|
|
5369
|
-
type: generateTableOrderTypeCached(targetTable, relEntry.targetTableName, typeNameMapper, cacheCtx, relationMap, tables),
|
|
5630
|
+
for (const [relationName, targetTableName] of orderRelations(tableName, relationMap, tables)) {
|
|
5631
|
+
const type = generateTableOrderTypeCached(tables[targetTableName], targetTableName, typeNameMapper, cacheCtx, relationMap, tables);
|
|
5632
|
+
if (type) fields[relationName] = {
|
|
5633
|
+
type,
|
|
5370
5634
|
description: `Order by columns of the related ${relationName} row`
|
|
5371
5635
|
};
|
|
5372
5636
|
}
|
|
5373
5637
|
return fields;
|
|
5374
5638
|
};
|
|
5639
|
+
/**
|
|
5640
|
+
* The table's `<Type>OrderBy` input, or `undefined` when it would have no fields (see
|
|
5641
|
+
* {@link hasOrderFields}); the caller then leaves the `orderBy` argument out.
|
|
5642
|
+
*/
|
|
5375
5643
|
const generateTableOrderTypeCached = (table, tableName, typeNameMapper, cacheCtx, relationMap, tables) => {
|
|
5376
5644
|
if (cacheCtx.orderTypeCache.has(table)) return cacheCtx.orderTypeCache.get(table);
|
|
5645
|
+
if (!hasOrderFields(tableName, relationMap, {
|
|
5646
|
+
...tables,
|
|
5647
|
+
[tableName]: table
|
|
5648
|
+
})) return;
|
|
5377
5649
|
const order = new GraphQLInputObjectType({
|
|
5378
5650
|
name: cacheCtx.typeName({
|
|
5379
5651
|
kind: "orderBy",
|
|
5380
5652
|
defaultName: `${resolveTypeName(tableName, typeNameMapper)}OrderBy`,
|
|
5381
5653
|
table: tableName
|
|
5382
5654
|
}),
|
|
5383
|
-
fields: () => {
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, orderColumns, relationMap, tables)
|
|
5388
|
-
};
|
|
5389
|
-
}
|
|
5655
|
+
fields: () => ({
|
|
5656
|
+
...generateTableOrderCached(table, innerOrderType(cacheCtx.typeName)),
|
|
5657
|
+
...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, relationMap, tables)
|
|
5658
|
+
})
|
|
5390
5659
|
});
|
|
5391
5660
|
cacheCtx.orderTypeCache.set(table, order);
|
|
5392
5661
|
return order;
|
|
@@ -5605,7 +5874,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
|
|
|
5605
5874
|
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(relType))),
|
|
5606
5875
|
args: {
|
|
5607
5876
|
where: { type: relSelectData.filters },
|
|
5608
|
-
orderBy: { type: relSelectData.order },
|
|
5877
|
+
...relSelectData.order ? { orderBy: { type: relSelectData.order } } : {},
|
|
5609
5878
|
offset: { type: GraphQLInt },
|
|
5610
5879
|
limit: { type: GraphQLInt },
|
|
5611
5880
|
...deletedArg(cacheCtx.softDeleteOf, targetTableName, cacheCtx.typeName),
|
|
@@ -5833,6 +6102,7 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5833
6102
|
};
|
|
5834
6103
|
const transactionsOpt = config?.transactions;
|
|
5835
6104
|
const transactions = transactionsOpt === void 0 || transactionsOpt === "none" ? void 0 : { timeoutMs: (transactionsOpt === "auto" ? void 0 : transactionsOpt.timeoutMs) ?? 3e4 };
|
|
6105
|
+
if (transactions && transactions.timeoutMs !== Number.POSITIVE_INFINITY && !(Number.isInteger(transactions.timeoutMs) && transactions.timeoutMs >= 1 && transactions.timeoutMs <= MAX_TIMER_MS)) throw new Error(`Drizzle-GraphQL Error: config.transactions.timeoutMs must be a positive integer no greater than ${MAX_TIMER_MS}, or Infinity for no timeout; got ${transactions.timeoutMs}.`);
|
|
5836
6106
|
const limitsConfig = config?.limits;
|
|
5837
6107
|
if (limitsConfig) {
|
|
5838
6108
|
const check = (policy, where) => {
|
|
@@ -5865,6 +6135,7 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5865
6135
|
for (const [columnName, entry] of Object.entries(orderBy)) {
|
|
5866
6136
|
if (!columns[columnName]) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which is not a column of '${tableName}'.`);
|
|
5867
6137
|
if (config?.exclude?.columns?.[tableName]?.includes(columnName)) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which config.exclude.columns hides. A default ordering is encoded into every row's cursor, so it can only use columns the schema exposes.`);
|
|
6138
|
+
if (!isOrderableColumn(columns[columnName])) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which has no meaningful ordering (a JSON column, or a SQLite blob bigint), so the generated orderBy does not offer it either.`);
|
|
5868
6139
|
const direction = typeof entry === "string" ? entry : entry?.direction;
|
|
5869
6140
|
if (direction !== "asc" && direction !== "desc") throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy.${columnName} must be 'asc', 'desc', or { direction, priority? }.`);
|
|
5870
6141
|
const priority = typeof entry === "string" ? void 0 : entry?.priority;
|
|
@@ -5923,7 +6194,7 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5923
6194
|
}
|
|
5924
6195
|
}
|
|
5925
6196
|
}
|
|
5926
|
-
const onWrite = writeHooks.
|
|
6197
|
+
const onWrite = [...writeHooks.values()].some(Boolean) ? (tableName) => writeHooks.get(tableName) : void 0;
|
|
5927
6198
|
const policies = scopeConfig || contextValuesConfig || softDeleteInfos.size || onWrite || defaultOrderBy ? {
|
|
5928
6199
|
scope: scopeConfig ? (tableName) => scopeConfig[tableName] : void 0,
|
|
5929
6200
|
contextValues: contextValuesConfig ? (tableName) => contextValuesConfig[tableName] : void 0,
|
|
@@ -6014,8 +6285,14 @@ const AGGREGATE_OPS = [
|
|
|
6014
6285
|
];
|
|
6015
6286
|
/** Ops whose result is a row count: never null, and returned as `Int!` rather than the column's type. */
|
|
6016
6287
|
const COUNT_OPS = /* @__PURE__ */ new Set(["countNonNull", "countDistinct"]);
|
|
6288
|
+
/**
|
|
6289
|
+
* `avg`, except on MySQL, where AVG over an integer column is a DECIMAL rounded to
|
|
6290
|
+
* `div_precision_increment` (4 by default) digits. Averaging the value as a DOUBLE keeps the
|
|
6291
|
+
* fraction the `Float` output promises, as the other two dialects do.
|
|
6292
|
+
*/
|
|
6293
|
+
const avgOf = (col) => columnDialect(col) === "mysql" ? sql`avg(cast(${col} as double))`.mapWith(String) : avg(col);
|
|
6017
6294
|
const OP_FNS = {
|
|
6018
|
-
avg,
|
|
6295
|
+
avg: avgOf,
|
|
6019
6296
|
sum,
|
|
6020
6297
|
min,
|
|
6021
6298
|
max,
|
|
@@ -6025,6 +6302,18 @@ const OP_FNS = {
|
|
|
6025
6302
|
/** Separator for flat select aliases (`avg__price`) — reassembled into nested output by the resolver. */
|
|
6026
6303
|
const SEP = "__";
|
|
6027
6304
|
/**
|
|
6305
|
+
* Column types PostgreSQL can test for equality but has no `min`/`max` aggregate for, plus the
|
|
6306
|
+
* SQLite bigint blob, whose bytes compare as text (`max(9, 10)` is 9). They still count
|
|
6307
|
+
* distinct values and group.
|
|
6308
|
+
*/
|
|
6309
|
+
const NO_MIN_MAX_COLUMN_TYPES = /* @__PURE__ */ new Set([
|
|
6310
|
+
"PgUUID",
|
|
6311
|
+
"PgMacaddr",
|
|
6312
|
+
"PgMacaddr8",
|
|
6313
|
+
"PgBinaryVector",
|
|
6314
|
+
"SQLiteBigInt"
|
|
6315
|
+
]);
|
|
6316
|
+
/**
|
|
6028
6317
|
* Classifies a table's columns for aggregation. avg/sum only make sense on numeric scalars;
|
|
6029
6318
|
* min/max work on any orderable scalar (numbers, strings, bigints, dates, enums). Booleans,
|
|
6030
6319
|
* arrays (including array-typed number columns), JSON, buffers, and object-shaped columns
|
|
@@ -6056,6 +6345,7 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
6056
6345
|
if (cached) return cached;
|
|
6057
6346
|
const numeric = {};
|
|
6058
6347
|
const orderable = {};
|
|
6348
|
+
const comparable = {};
|
|
6059
6349
|
const all = {};
|
|
6060
6350
|
for (const [columnName, column] of Object.entries(visibleColumns(table))) {
|
|
6061
6351
|
all[columnName] = column;
|
|
@@ -6065,7 +6355,11 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
6065
6355
|
const { type: dataType, constraint } = extractExtendedColumnType(column);
|
|
6066
6356
|
if (dataType === "boolean" || dataType === "array" || dataType === "custom") continue;
|
|
6067
6357
|
if (dataType === "object" && constraint !== "date") continue;
|
|
6068
|
-
|
|
6358
|
+
comparable[columnName] = {
|
|
6359
|
+
column,
|
|
6360
|
+
converted
|
|
6361
|
+
};
|
|
6362
|
+
if (!NO_MIN_MAX_COLUMN_TYPES.has(column.columnType)) orderable[columnName] = {
|
|
6069
6363
|
column,
|
|
6070
6364
|
converted
|
|
6071
6365
|
};
|
|
@@ -6074,6 +6368,7 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
6074
6368
|
const sets = {
|
|
6075
6369
|
numeric,
|
|
6076
6370
|
orderable,
|
|
6371
|
+
comparable,
|
|
6077
6372
|
all
|
|
6078
6373
|
};
|
|
6079
6374
|
byName.set(tableName, sets);
|
|
@@ -6086,13 +6381,13 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
6086
6381
|
* and avg/sum of integers overflow Int / produce decimals)
|
|
6087
6382
|
* - `min` / `max` — per orderable column, the column's own (nullable) scalar type
|
|
6088
6383
|
* - `countNonNull` — per column, `Int!`: how many matching rows have a non-null value there
|
|
6089
|
-
* - `countDistinct` — per
|
|
6384
|
+
* - `countDistinct` — per comparable column, `Int!`: how many distinct non-null values there are
|
|
6090
6385
|
* Each wrapper is omitted when no column qualifies for it.
|
|
6091
6386
|
*/
|
|
6092
6387
|
const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
|
|
6093
6388
|
const cached = cacheCtx?.aggregateTypeCache.get(tableName);
|
|
6094
6389
|
if (cached) return cached;
|
|
6095
|
-
const { numeric, orderable, all } = classifyAggregateColumns(table, tableName);
|
|
6390
|
+
const { numeric, orderable, comparable, all } = classifyAggregateColumns(table, tableName);
|
|
6096
6391
|
const nameOf = (op) => cacheCtx?.typeName({
|
|
6097
6392
|
kind: "aggregate",
|
|
6098
6393
|
defaultName: `${typeName}${capitalize(op)}Aggregate`,
|
|
@@ -6113,7 +6408,7 @@ const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
|
|
|
6113
6408
|
}) };
|
|
6114
6409
|
const countSets = {
|
|
6115
6410
|
countNonNull: all,
|
|
6116
|
-
countDistinct:
|
|
6411
|
+
countDistinct: comparable
|
|
6117
6412
|
};
|
|
6118
6413
|
for (const [op, columns] of Object.entries(countSets)) {
|
|
6119
6414
|
const columnNames = Object.keys(columns);
|
|
@@ -6293,6 +6588,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
6293
6588
|
const junction = extractJunctionColumns(relEntry);
|
|
6294
6589
|
if (junction === null) return;
|
|
6295
6590
|
const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
|
|
6591
|
+
const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
|
|
6296
6592
|
const groupAliases = groupCols.map((_, i) => `${GROUP_KEY}${i}`);
|
|
6297
6593
|
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
|
|
6298
6594
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
|
|
@@ -6306,7 +6602,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
6306
6602
|
};
|
|
6307
6603
|
const resolve = async (parent, args, context, info) => {
|
|
6308
6604
|
try {
|
|
6309
|
-
const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes
|
|
6605
|
+
const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes, `aggregate:${tableName}::${relationName}`, () => {
|
|
6310
6606
|
const request = parseAggregateRequest(info, target);
|
|
6311
6607
|
return {
|
|
6312
6608
|
request,
|
|
@@ -6329,7 +6625,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
6329
6625
|
const groupSelection = Object.fromEntries(groupAliases.map((alias, i) => [alias, groupCols[i]]));
|
|
6330
6626
|
const byKey = /* @__PURE__ */ new Map();
|
|
6331
6627
|
for (const chunk of parentKeyChunks(uniqueKeys, groupCols.length)) {
|
|
6332
|
-
const whereCondition = withScope(scope, targetTableName, targetTable, and(parentKeyCondition(groupCols, chunk), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
|
|
6628
|
+
const whereCondition = withScope(scope, targetTableName, targetTable, and(parentKeyCondition(groupCols, chunk), ownCondition, junction ? junctionPolicy(scope, junctionName, junction.table) : void 0, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
|
|
6333
6629
|
const selected = executor.select({
|
|
6334
6630
|
...groupSelection,
|
|
6335
6631
|
...request.selection
|
|
@@ -6363,12 +6659,12 @@ const HAVING_OPS = {
|
|
|
6363
6659
|
};
|
|
6364
6660
|
/**
|
|
6365
6661
|
* Columns a query can group on. Grouping needs an equality operator, which is the same
|
|
6366
|
-
* requirement
|
|
6662
|
+
* requirement countDistinct has, plus booleans — comparable excludes them because a min/max over
|
|
6367
6663
|
* true/false says nothing, but `GROUP BY isConfirmed` is perfectly ordinary.
|
|
6368
6664
|
*/
|
|
6369
6665
|
const groupableColumns = (table, tableName) => {
|
|
6370
|
-
const {
|
|
6371
|
-
const groupable = { ...
|
|
6666
|
+
const { comparable } = classifyAggregateColumns(table, tableName);
|
|
6667
|
+
const groupable = { ...comparable };
|
|
6372
6668
|
for (const [columnName, column] of Object.entries(visibleColumns(table))) {
|
|
6373
6669
|
if (groupable[columnName]) continue;
|
|
6374
6670
|
const { type: dataType } = extractExtendedColumnType(column);
|
|
@@ -6409,7 +6705,7 @@ const aggregateNumberFilterType = (typeName) => sharedType(typeName, {
|
|
|
6409
6705
|
* nothing to compare against.
|
|
6410
6706
|
*/
|
|
6411
6707
|
const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
|
|
6412
|
-
const { numeric,
|
|
6708
|
+
const { numeric, comparable, all } = classifyAggregateColumns(table, tableName);
|
|
6413
6709
|
const resolveName = cacheCtx?.typeName ?? ((info) => info.defaultName);
|
|
6414
6710
|
const numberFilter = aggregateNumberFilterType(resolveName);
|
|
6415
6711
|
const fields = { count: {
|
|
@@ -6422,7 +6718,7 @@ const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
|
|
|
6422
6718
|
min: numeric,
|
|
6423
6719
|
max: numeric,
|
|
6424
6720
|
countNonNull: all,
|
|
6425
|
-
countDistinct:
|
|
6721
|
+
countDistinct: comparable
|
|
6426
6722
|
};
|
|
6427
6723
|
for (const [op, columns] of Object.entries(opColumns)) {
|
|
6428
6724
|
const columnNames = Object.keys(columns);
|
|
@@ -6487,22 +6783,34 @@ const generateGroupByType = (table, tableName, typeName, cacheCtx) => {
|
|
|
6487
6783
|
}
|
|
6488
6784
|
});
|
|
6489
6785
|
};
|
|
6490
|
-
/**
|
|
6491
|
-
|
|
6786
|
+
/**
|
|
6787
|
+
* Turns one `${typeName}Having` entry into SQL conditions over the matching aggregate expression.
|
|
6788
|
+
*
|
|
6789
|
+
* The operand is a `Float`, but PostgreSQL types an untyped parameter after what it is compared
|
|
6790
|
+
* with: count and sum of integers are bigint, min/max of an int column int, so `gt: 1.5` would
|
|
6791
|
+
* fail to parse. There the aggregate is compared as `numeric`, which holds every one of them
|
|
6792
|
+
* exactly. MySQL and SQLite already compare numbers across types.
|
|
6793
|
+
*/
|
|
6794
|
+
const havingComparisons = (expression, filter, isPg) => {
|
|
6795
|
+
const compared = isPg ? sql`cast(${expression} as numeric)` : expression;
|
|
6796
|
+
return Object.entries(filter).filter(([op, value]) => value != null && op in HAVING_OPS).map(([op, value]) => HAVING_OPS[op](compared, value));
|
|
6797
|
+
};
|
|
6492
6798
|
const buildHavingCondition = (having, columns) => {
|
|
6493
6799
|
if (!having) return;
|
|
6494
6800
|
const conditions = [];
|
|
6801
|
+
const firstColumn = Object.values(columns)[0];
|
|
6802
|
+
const isPg = firstColumn ? columnDialect(firstColumn) === "pg" : false;
|
|
6495
6803
|
for (const [key, value] of Object.entries(having)) {
|
|
6496
6804
|
if (value == null) continue;
|
|
6497
6805
|
if (key === "count") {
|
|
6498
|
-
conditions.push(...havingComparisons(count(), value));
|
|
6806
|
+
conditions.push(...havingComparisons(count(), value, isPg));
|
|
6499
6807
|
continue;
|
|
6500
6808
|
}
|
|
6501
6809
|
if (!AGGREGATE_OPS.includes(key)) continue;
|
|
6502
6810
|
for (const [columnName, filter] of Object.entries(value)) {
|
|
6503
6811
|
const column = columns[columnName];
|
|
6504
6812
|
if (!column || filter == null) continue;
|
|
6505
|
-
conditions.push(...havingComparisons(OP_FNS[key](column), filter));
|
|
6813
|
+
conditions.push(...havingComparisons(OP_FNS[key](column), filter, isPg));
|
|
6506
6814
|
}
|
|
6507
6815
|
}
|
|
6508
6816
|
return conditions.length ? and(...conditions) : void 0;
|
|
@@ -6654,12 +6962,32 @@ const isOptionalOnInsert = (column) => !column.notNull || !!column.hasDefault ||
|
|
|
6654
6962
|
* relation could come back without it. A `where` inherited from the reverse relation filters
|
|
6655
6963
|
* the parent instead, and becomes a per-parent check.
|
|
6656
6964
|
*/
|
|
6657
|
-
const ownWherePlan = (relEntry, parentTable, targetTable, localCol) => {
|
|
6965
|
+
const ownWherePlan = (relEntry, parentTable, targetTable, localCol, parentPrimaryKey) => {
|
|
6658
6966
|
const condition = () => relationOwnCondition(relEntry, parentTable, targetTable, [localCol], [localCol]);
|
|
6659
6967
|
if (!condition()) return {};
|
|
6660
|
-
|
|
6661
|
-
|
|
6968
|
+
const relation = relEntry.relation ?? relEntry;
|
|
6969
|
+
if (!isRelationFilterReversed(relation)) return { ownCondition: () => condition() };
|
|
6970
|
+
const parentColumns = getColumns(parentTable);
|
|
6971
|
+
const parentKeyColumns = Object.fromEntries(parentPrimaryKey.filter((name) => parentColumns[name]).map((name) => [name, parentColumns[name]]));
|
|
6972
|
+
const keyNames = Object.keys(parentKeyColumns);
|
|
6973
|
+
return {
|
|
6974
|
+
parentShows: async (executor, key) => (await executor.select({ key: localCol }).from(parentTable).where(and(eq(localCol, key), condition())).limit(1)).length > 0,
|
|
6975
|
+
parentHidden: async (executor, rows) => {
|
|
6976
|
+
if (!rows.length) return false;
|
|
6977
|
+
if (!keyNames.length || !rows.every((row) => keyNames.every((name) => name in row))) throw drizzleError(`Drizzle-GraphQL Error: cannot check the \`where\` of this relation: the written row cannot be identified by a primary key.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
6978
|
+
const keyOf = (row) => keyNames.map((name) => keyString(row[name])).join("\0");
|
|
6979
|
+
const match = or(...rows.map((row) => and(...keyNames.map((name) => eq(parentColumns[name], row[name])))));
|
|
6980
|
+
const shown = new Set((await executor.select(parentKeyColumns).from(parentTable).where(and(match, relationsFilterToSQL(parentTable, relationOwnWhere(relation))))).map(keyOf));
|
|
6981
|
+
return rows.some((row) => !shown.has(keyOf(row)));
|
|
6982
|
+
},
|
|
6983
|
+
parentKeyColumns
|
|
6984
|
+
};
|
|
6662
6985
|
};
|
|
6986
|
+
/**
|
|
6987
|
+
* A key value in a form that compares equal across the two reads of it — the driver may hand
|
|
6988
|
+
* back a `Date` or a `bigint` that is a different object each time.
|
|
6989
|
+
*/
|
|
6990
|
+
const keyString = (value) => value instanceof Date ? `d${value.getTime()}` : value === null || value === void 0 ? "" : `v${String(value)}`;
|
|
6663
6991
|
/** The property name a column is stored under on its table, matched by identity. */
|
|
6664
6992
|
const propNameOf = (table, column) => {
|
|
6665
6993
|
if (!column) return;
|
|
@@ -6721,7 +7049,7 @@ const buildThroughPlan = (relationName, relEntry, relation, table, targetTableNa
|
|
|
6721
7049
|
targetCol
|
|
6722
7050
|
},
|
|
6723
7051
|
targetKeyColumns: { [foreignColPropName]: foreignCol },
|
|
6724
|
-
...ownWherePlan(relEntry, table, targetTable, localCol),
|
|
7052
|
+
...ownWherePlan(relEntry, table, targetTable, localCol, []),
|
|
6725
7053
|
allowed: /* @__PURE__ */ new Set([
|
|
6726
7054
|
"connect",
|
|
6727
7055
|
"disconnect",
|
|
@@ -6800,7 +7128,7 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
6800
7128
|
const targetPrimaryKey = resolvePrimaryKeys(targetTable).filter((name) => targetColumns[name]);
|
|
6801
7129
|
const targetKeyColumns = Object.fromEntries(targetPrimaryKey.map((name) => [name, targetColumns[name]]));
|
|
6802
7130
|
targetKeyColumns[foreignColPropName] = foreignCol;
|
|
6803
|
-
const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol);
|
|
7131
|
+
const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol, resolvePrimaryKeys(table));
|
|
6804
7132
|
const ops = !fkColumn.notNull ? isOne ? [
|
|
6805
7133
|
"create",
|
|
6806
7134
|
"connect",
|
|
@@ -7233,6 +7561,16 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7233
7561
|
}
|
|
7234
7562
|
if (supplied(op["connect"])) await link(asArray(op["connect"]), "connect");
|
|
7235
7563
|
};
|
|
7564
|
+
/**
|
|
7565
|
+
* The value a parent-side `connect` / `disconnect` writes to an existing row's key: `next`,
|
|
7566
|
+
* unless the key points at a row the relation's own `where` hides, which it then keeps.
|
|
7567
|
+
*/
|
|
7568
|
+
const keepingHidden = (plan, next) => {
|
|
7569
|
+
if (!plan.ownCondition) return next;
|
|
7570
|
+
const shown = sql`select ${plan.foreignCol} from ${plan.targetTable} where ${plan.ownCondition()}`;
|
|
7571
|
+
const replacement = next === null ? sql`null` : sql`${sql.param(next, plan.localCol)}`;
|
|
7572
|
+
return sql`case when ${plan.localCol} is null or ${plan.localCol} in (${shown}) then ${replacement} else ${plan.localCol} end`;
|
|
7573
|
+
};
|
|
7236
7574
|
const detachAll = (executor, plan, key, scope, steps, op) => steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where(withScope(scope, plan.targetTableName, plan.targetTable, and(eq(plan.foreignCol, key), plan.ownCondition?.()))), op, "update");
|
|
7237
7575
|
return {
|
|
7238
7576
|
enabled: (tableName) => !!plans[tableName],
|
|
@@ -7258,7 +7596,10 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7258
7596
|
const allColumns = getColumns(table);
|
|
7259
7597
|
for (const relationName of Object.keys(ops)) {
|
|
7260
7598
|
const plan = tablePlans[relationName];
|
|
7261
|
-
if (plan
|
|
7599
|
+
if (plan && !(plan.localColPropName in columns) && allColumns[plan.localColPropName]) columns[plan.localColPropName] = allColumns[plan.localColPropName];
|
|
7600
|
+
if (plan?.fkSide === "parent" && plan.parentHidden) {
|
|
7601
|
+
for (const [name, column] of Object.entries(plan.parentKeyColumns ?? {})) if (!(name in columns)) columns[name] = column;
|
|
7602
|
+
}
|
|
7262
7603
|
}
|
|
7263
7604
|
for (const name of selfTargetingPlan(tablePlans, tableName, ops)?.targetPrimaryKey ?? []) if (!(name in columns) && allColumns[name]) columns[name] = allColumns[name];
|
|
7264
7605
|
return columns;
|
|
@@ -7278,10 +7619,15 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7278
7619
|
...fresh.get(keyOf(row))
|
|
7279
7620
|
}));
|
|
7280
7621
|
},
|
|
7281
|
-
applyParentSide: async (executor, tableName, ops, request) => {
|
|
7622
|
+
applyParentSide: async (executor, tableName, ops, request, existing = false) => {
|
|
7282
7623
|
const tablePlans = plans[tableName];
|
|
7283
|
-
const
|
|
7284
|
-
|
|
7624
|
+
const result = {
|
|
7625
|
+
values: {},
|
|
7626
|
+
expected: {},
|
|
7627
|
+
existing
|
|
7628
|
+
};
|
|
7629
|
+
const patch = result.values;
|
|
7630
|
+
if (!tablePlans) return result;
|
|
7285
7631
|
const context = request?.context;
|
|
7286
7632
|
const scope = resolveScope(policies, context, filterCtx);
|
|
7287
7633
|
for (const [relationName, op] of Object.entries(ops)) {
|
|
@@ -7300,14 +7646,30 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7300
7646
|
if (!created) throw drizzleError(`Drizzle-GraphQL Error: 'create' on '${relationName}' inserted no row, so there is nothing to attach.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
7301
7647
|
await steps.after();
|
|
7302
7648
|
patch[plan.localColPropName] = created[plan.foreignColPropName];
|
|
7649
|
+
result.expected[relationName] = created[plan.foreignColPropName];
|
|
7303
7650
|
} else if (op["connect"] !== void 0) {
|
|
7304
7651
|
const rows = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
|
|
7305
7652
|
if (rows.length !== 1) throw connectMatchError(relationName, rows.length);
|
|
7306
|
-
|
|
7307
|
-
|
|
7653
|
+
const key = rows[0][plan.foreignColPropName];
|
|
7654
|
+
patch[plan.localColPropName] = existing ? keepingHidden(plan, key) : key;
|
|
7655
|
+
result.expected[relationName] = key;
|
|
7656
|
+
} else if (op["disconnect"] === true) patch[plan.localColPropName] = existing ? keepingHidden(plan, null) : null;
|
|
7308
7657
|
}
|
|
7309
7658
|
for (const columnName of Object.keys(contextValues?.(tableName) ?? {})) delete patch[columnName];
|
|
7310
|
-
return
|
|
7659
|
+
return result;
|
|
7660
|
+
},
|
|
7661
|
+
checkParentSide: async (executor, tableName, ops, rows, patch) => {
|
|
7662
|
+
const tablePlans = plans[tableName];
|
|
7663
|
+
if (!tablePlans || !rows.length) return;
|
|
7664
|
+
for (const relationName of Object.keys(ops)) {
|
|
7665
|
+
const plan = tablePlans[relationName];
|
|
7666
|
+
if (plan?.fkSide !== "parent") continue;
|
|
7667
|
+
if (relationName in patch.expected) {
|
|
7668
|
+
const expected = keyString(patch.expected[relationName]);
|
|
7669
|
+
if (rows.some((row) => keyString(row[plan.localColPropName]) !== expected)) throw drizzleError(patch.existing ? `Drizzle-GraphQL Error: cannot connect through '${relationName}': the row it holds now is one its \`where\` hides, which a write through it cannot detach.` : `Drizzle-GraphQL Error: cannot write through '${relationName}': the ${tableName} row was not written with it — an upsert whose conflict branch did not write '${plan.localColPropName}'. Include it in onConflict.update, or leave update unset.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
7670
|
+
}
|
|
7671
|
+
if (plan.parentHidden && await plan.parentHidden(executor, rows)) throw drizzleError(`Drizzle-GraphQL Error: cannot write through '${relationName}': its \`where\` hides the relation on this ${tableName} row.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
7672
|
+
}
|
|
7311
7673
|
},
|
|
7312
7674
|
applyChildSide: async (executor, tableName, ops, parentRows, request) => {
|
|
7313
7675
|
const tablePlans = plans[tableName];
|
|
@@ -7374,12 +7736,13 @@ var NoParentRow = class extends Error {};
|
|
|
7374
7736
|
* `executor` must already be a transaction.
|
|
7375
7737
|
*/
|
|
7376
7738
|
const writeNestedEntry = async (params) => {
|
|
7377
|
-
const { executor, runtime, tableName, ops, write, request } = params;
|
|
7739
|
+
const { executor, runtime, tableName, ops, write, request, existing } = params;
|
|
7378
7740
|
const guarded = runtime.createsParentSide(tableName, ops);
|
|
7379
7741
|
const run = async (tx) => {
|
|
7380
|
-
const patch = await runtime.applyParentSide(tx, tableName, ops, request);
|
|
7381
|
-
const rows = await write(tx, patch);
|
|
7742
|
+
const patch = await runtime.applyParentSide(tx, tableName, ops, request, existing);
|
|
7743
|
+
const rows = await write(tx, patch.values);
|
|
7382
7744
|
if (guarded && !rows.length) throw new NoParentRow();
|
|
7745
|
+
await runtime.checkParentSide(tx, tableName, ops, rows, patch);
|
|
7383
7746
|
await runtime.applyChildSide(tx, tableName, ops, rows, request);
|
|
7384
7747
|
return runtime.refreshed(tx, tableName, ops, rows);
|
|
7385
7748
|
};
|
|
@@ -7441,7 +7804,8 @@ const updateWithNestedOps = async (params) => {
|
|
|
7441
7804
|
...remapValues(columns),
|
|
7442
7805
|
...patch
|
|
7443
7806
|
}),
|
|
7444
|
-
request
|
|
7807
|
+
request,
|
|
7808
|
+
existing: true
|
|
7445
7809
|
}));
|
|
7446
7810
|
};
|
|
7447
7811
|
//#endregion
|
|
@@ -7594,6 +7958,7 @@ const createSchemaBuilder = (adapter) => {
|
|
|
7594
7958
|
const anyTable = (feature) => tableEntries.some(([name]) => featureOf(name)[feature]);
|
|
7595
7959
|
if (!tableEntries.length) throw new Error("Drizzle-GraphQL Error: No tables detected in Drizzle-ORM's database instance. Did you forget to pass schema to drizzle constructor?");
|
|
7596
7960
|
registerScalarOverrides(tables, options);
|
|
7961
|
+
installSqliteBlobJsonDecoders(tables);
|
|
7597
7962
|
registerEnumConfig(options);
|
|
7598
7963
|
registerColumnExclusions(tables, options.exclude);
|
|
7599
7964
|
resetAggregateColumnCache();
|
|
@@ -7825,7 +8190,7 @@ const createSchemaBuilder = (adapter) => {
|
|
|
7825
8190
|
...tableFeatures.update ? [updateInput] : [],
|
|
7826
8191
|
...built.updateManyInput ? [built.updateManyInput] : [],
|
|
7827
8192
|
tableFilters,
|
|
7828
|
-
tableOrder
|
|
8193
|
+
...tableOrder ? [tableOrder] : []
|
|
7829
8194
|
].forEach((e) => {
|
|
7830
8195
|
inputs[e.name] = e;
|
|
7831
8196
|
});
|
|
@@ -7955,14 +8320,12 @@ const generateUpsert = (db, tableName, table, baseType, onConflictType, fieldNam
|
|
|
7955
8320
|
withTarget: false,
|
|
7956
8321
|
serverOwned: Object.keys(contextColumns ?? {})
|
|
7957
8322
|
});
|
|
7958
|
-
const requestedNothing = args.onConflict?.action === "NOTHING";
|
|
7959
8323
|
const runBatch = async (target, { values, plan }) => {
|
|
7960
|
-
if (plan.action === "NOTHING"
|
|
8324
|
+
if (plan.action === "NOTHING") {
|
|
7961
8325
|
const columns = getColumns(table);
|
|
7962
8326
|
const keyName = pkNames[0] ?? Object.keys(columns)[0];
|
|
7963
8327
|
await target.insert(table).values(values).onDuplicateKeyUpdate({ set: { [keyName]: sql`${sql.identifier(columns[keyName].name)}` } });
|
|
7964
|
-
} else
|
|
7965
|
-
else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
|
|
8328
|
+
} else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
|
|
7966
8329
|
};
|
|
7967
8330
|
if (batches.length === 1) await runBatch(executor, batches[0]);
|
|
7968
8331
|
else await executor.transaction(async (tx) => {
|
|
@@ -8002,7 +8365,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
|
|
|
8002
8365
|
});
|
|
8003
8366
|
if (single) await assertSingleMatch(executor, table, filters);
|
|
8004
8367
|
let query = executor.update(table).set(input);
|
|
8005
|
-
if (filters) query = query.where(filters);
|
|
8368
|
+
if (filters) query = query.where(mysqlWriteFilter(table, filters));
|
|
8006
8369
|
await query;
|
|
8007
8370
|
await after();
|
|
8008
8371
|
return isSuccess;
|
|
@@ -8017,7 +8380,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
|
|
|
8017
8380
|
* in order. MySQL has no RETURNING, so — like every other MySQL mutation — the result is
|
|
8018
8381
|
* `{ isSuccess: true }` rather than the updated rows.
|
|
8019
8382
|
*/
|
|
8020
|
-
const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName, filterCtx, txCtx, policies) => writeResolver({
|
|
8383
|
+
const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName, requireWhere, filterCtx, txCtx, policies) => writeResolver({
|
|
8021
8384
|
db,
|
|
8022
8385
|
tableName,
|
|
8023
8386
|
operation: "updateMany",
|
|
@@ -8037,13 +8400,20 @@ const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName,
|
|
|
8037
8400
|
if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
|
|
8038
8401
|
return {
|
|
8039
8402
|
set: input,
|
|
8040
|
-
filters:
|
|
8403
|
+
filters: scopedWhere({
|
|
8404
|
+
scope,
|
|
8405
|
+
tableName,
|
|
8406
|
+
table,
|
|
8407
|
+
where,
|
|
8408
|
+
relationCtx: relationFilterCtx(filterCtx, tableName, scope),
|
|
8409
|
+
required: requireWhere
|
|
8410
|
+
})
|
|
8041
8411
|
};
|
|
8042
8412
|
});
|
|
8043
8413
|
await executor.transaction(async (tx) => {
|
|
8044
8414
|
for (const entry of entries) {
|
|
8045
8415
|
let query = tx.update(table).set(entry.set);
|
|
8046
|
-
if (entry.filters) query = query.where(entry.filters);
|
|
8416
|
+
if (entry.filters) query = query.where(mysqlWriteFilter(table, entry.filters));
|
|
8047
8417
|
await query;
|
|
8048
8418
|
}
|
|
8049
8419
|
});
|
|
@@ -8092,7 +8462,7 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
|
|
|
8092
8462
|
});
|
|
8093
8463
|
if (single) await assertSingleMatch(executor, table, filters);
|
|
8094
8464
|
let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
|
|
8095
|
-
if (filters) query = query.where(filters);
|
|
8465
|
+
if (filters) query = query.where(mysqlWriteFilter(table, filters));
|
|
8096
8466
|
await query;
|
|
8097
8467
|
await after();
|
|
8098
8468
|
return isSuccess;
|
|
@@ -8103,6 +8473,34 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
|
|
|
8103
8473
|
const getTableConfig$5 = memoizeTableConfig(getTableConfig);
|
|
8104
8474
|
/** Primary-key property names for a MySQL table, including table-level composite keys. */
|
|
8105
8475
|
const mysqlPrimaryKeyPropNames = (table) => getPrimaryKeyPropNamesFromConfig(table, getTableConfig$5);
|
|
8476
|
+
const TABLE_ORIGINAL_NAME = Symbol.for("drizzle:OriginalName");
|
|
8477
|
+
const TABLE_SCHEMA = Symbol.for("drizzle:Schema");
|
|
8478
|
+
/** Whether `chunk` reads `table` anywhere — under an alias, in a subquery, however deep. */
|
|
8479
|
+
const readsTable = (chunk, table) => {
|
|
8480
|
+
if (Array.isArray(chunk)) return chunk.some((part) => readsTable(part, table));
|
|
8481
|
+
if (is(chunk, Table)) return chunk[TABLE_ORIGINAL_NAME] === table[TABLE_ORIGINAL_NAME] && chunk[TABLE_SCHEMA] === table[TABLE_SCHEMA];
|
|
8482
|
+
if (is(chunk, SQL)) return readsTable(chunk.queryChunks, table);
|
|
8483
|
+
if (is(chunk, SQL.Aliased)) return readsTable(chunk.sql, table);
|
|
8484
|
+
if (is(chunk, Subquery)) return readsTable(chunk._.sql, table);
|
|
8485
|
+
return false;
|
|
8486
|
+
};
|
|
8487
|
+
/**
|
|
8488
|
+
* The `where` of an UPDATE or DELETE, in a form MySQL accepts. MySQL refuses a statement whose
|
|
8489
|
+
* `where` reads the table it writes (error 1093, "You can't specify target table … for update
|
|
8490
|
+
* in FROM clause") — which a relation filter through a self-referencing relation, or a scope
|
|
8491
|
+
* written as one, does. Such a filter is resolved to the primary keys it matches first, through
|
|
8492
|
+
* a derived table MySQL materializes (`DISTINCT` keeps the optimizer from merging it back in),
|
|
8493
|
+
* and the write matches those keys. Any other filter is returned as it is.
|
|
8494
|
+
*/
|
|
8495
|
+
const mysqlWriteFilter = (table, filters) => {
|
|
8496
|
+
if (!filters || !readsTable(filters, table)) return filters;
|
|
8497
|
+
const columns = getColumns(table);
|
|
8498
|
+
const keys = mysqlPrimaryKeyPropNames(table).map((name) => columns[name]);
|
|
8499
|
+
if (!keys.length) return filters;
|
|
8500
|
+
const keyList = sql.join(keys.map((column) => sql.identifier(column.name)), sql`, `);
|
|
8501
|
+
const target = keys.length === 1 ? sql`${keys[0]}` : sql`(${sql.join(keys, sql`, `)})`;
|
|
8502
|
+
return sql`${target} in (select ${keyList} from (select distinct ${sql.join(keys, sql`, `)} from ${table} where ${filters}) as ${sql.identifier("dgql_write_target")})`;
|
|
8503
|
+
};
|
|
8106
8504
|
const { prepareBuild, forEachTable, finalizeBuild } = createSchemaBuilder({
|
|
8107
8505
|
tableClass: MySqlTable,
|
|
8108
8506
|
getTableConfig: getTableConfig$5,
|
|
@@ -8156,7 +8554,7 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
|
|
|
8156
8554
|
tableFilters,
|
|
8157
8555
|
cacheCtx
|
|
8158
8556
|
}) : void 0;
|
|
8159
|
-
const updateManyGenerated = updateManyInput ? generateUpdateMany$2(db, tableName, schema[tableName], updateManyInput, updateManyFieldName, filterCtx, mutationTxCtx, policies) : void 0;
|
|
8557
|
+
const updateManyGenerated = updateManyInput ? generateUpdateMany$2(db, tableName, schema[tableName], updateManyInput, updateManyFieldName, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
|
|
8160
8558
|
const deleteGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
|
|
8161
8559
|
const deleteSingleGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteSingleFieldName, true, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
|
|
8162
8560
|
const restoreGenerated = softDeleteInfo ? generateDelete(db, tableName, schema[tableName], tableFilters, restoreFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies, true) : void 0;
|
|
@@ -8173,7 +8571,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
|
|
|
8173
8571
|
filterCtx,
|
|
8174
8572
|
txCtx: mutationTxCtx,
|
|
8175
8573
|
policies,
|
|
8176
|
-
returnsRows: false
|
|
8574
|
+
returnsRows: false,
|
|
8575
|
+
writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
|
|
8177
8576
|
}) : void 0;
|
|
8178
8577
|
const deleteCountGenerated = tableFeatures.delete && tableFeatures.countMutations ? generateWriteCount({
|
|
8179
8578
|
db,
|
|
@@ -8186,7 +8585,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
|
|
|
8186
8585
|
filterCtx,
|
|
8187
8586
|
txCtx: mutationTxCtx,
|
|
8188
8587
|
policies,
|
|
8189
|
-
returnsRows: false
|
|
8588
|
+
returnsRows: false,
|
|
8589
|
+
writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
|
|
8190
8590
|
}) : void 0;
|
|
8191
8591
|
const hardDeleteMeta = softDeleteInfo?.hardDelete ? { hardDelete: true } : {};
|
|
8192
8592
|
const withMeta = (generated, meta) => ({
|
|
@@ -8298,7 +8698,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8298
8698
|
* one-element list.
|
|
8299
8699
|
*/
|
|
8300
8700
|
const generateInsert = (build, opts) => {
|
|
8301
|
-
const { db, tables, relationMap, typeNameMapper, txCtx, nested, limits, policies, resolveName } = build;
|
|
8701
|
+
const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
|
|
8302
8702
|
const { tableName, table, baseType, fieldName, typeName, single, conflictDoNothing = false } = opts;
|
|
8303
8703
|
const queryArgs = { values: { type: single ? new GraphQLNonNull(baseType) : new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(baseType))) } };
|
|
8304
8704
|
const pkNames = primaryKeyPropNames(table);
|
|
@@ -8322,6 +8722,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8322
8722
|
const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
|
|
8323
8723
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
8324
8724
|
relationMap,
|
|
8725
|
+
filterCtx,
|
|
8325
8726
|
tables,
|
|
8326
8727
|
tableName,
|
|
8327
8728
|
typeName,
|
|
@@ -8400,6 +8801,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8400
8801
|
const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
|
|
8401
8802
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
8402
8803
|
relationMap,
|
|
8804
|
+
filterCtx,
|
|
8403
8805
|
tables,
|
|
8404
8806
|
tableName,
|
|
8405
8807
|
typeName,
|
|
@@ -8425,18 +8827,9 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8425
8827
|
buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope))
|
|
8426
8828
|
});
|
|
8427
8829
|
const batchQuery = (executor, { values: rows, plan }) => {
|
|
8428
|
-
|
|
8830
|
+
const query = executor.insert(table).values(rows).returning(returning);
|
|
8429
8831
|
const setWhere = withScope(scope, tableName, table, plan.setWhere);
|
|
8430
|
-
query
|
|
8431
|
-
target: plan.target,
|
|
8432
|
-
where: plan.targetWhere
|
|
8433
|
-
} : void 0) : query.onConflictDoUpdate({
|
|
8434
|
-
target: plan.target,
|
|
8435
|
-
targetWhere: plan.targetWhere,
|
|
8436
|
-
set: plan.set,
|
|
8437
|
-
setWhere
|
|
8438
|
-
});
|
|
8439
|
-
return query;
|
|
8832
|
+
return applyConflictPlan(query, table, plan, setWhere);
|
|
8440
8833
|
};
|
|
8441
8834
|
if (batches.length === 1) return await batchQuery(target, batches[0]);
|
|
8442
8835
|
return target.transaction((tx) => {
|
|
@@ -8499,6 +8892,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8499
8892
|
await before();
|
|
8500
8893
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
8501
8894
|
relationMap,
|
|
8895
|
+
filterCtx,
|
|
8502
8896
|
tables,
|
|
8503
8897
|
tableName,
|
|
8504
8898
|
typeName,
|
|
@@ -8737,7 +9131,8 @@ const createSchemaDataGenerator = (adapter) => {
|
|
|
8737
9131
|
table: schema[tableName],
|
|
8738
9132
|
updateManyInput,
|
|
8739
9133
|
fieldName: updateManyFieldName,
|
|
8740
|
-
typeName
|
|
9134
|
+
typeName,
|
|
9135
|
+
requireWhere: tableFeatures.requireWhere
|
|
8741
9136
|
}) : void 0;
|
|
8742
9137
|
const deleteGenerated = tableFeatures.delete ? generateDelete(writeBuild, {
|
|
8743
9138
|
tableName,
|
|
@@ -8992,7 +9387,8 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
|
|
|
8992
9387
|
tableName,
|
|
8993
9388
|
ops: entry.ops,
|
|
8994
9389
|
write,
|
|
8995
|
-
request
|
|
9390
|
+
request,
|
|
9391
|
+
existing: true
|
|
8996
9392
|
}) : await write(tx, {}));
|
|
8997
9393
|
}
|
|
8998
9394
|
return results;
|
|
@@ -9013,7 +9409,7 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
|
|
|
9013
9409
|
const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateManyBatch) => {
|
|
9014
9410
|
return (build, opts) => {
|
|
9015
9411
|
const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
|
|
9016
|
-
const { tableName, table, updateManyInput, fieldName, typeName } = opts;
|
|
9412
|
+
const { tableName, table, updateManyInput, fieldName, typeName, requireWhere } = opts;
|
|
9017
9413
|
const queryArgs = { updates: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(updateManyInput))) } };
|
|
9018
9414
|
const pkNames = primaryKeyPropNames(table);
|
|
9019
9415
|
return writeResolver({
|
|
@@ -9033,6 +9429,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
|
|
|
9033
9429
|
const contextColumns = policies?.contextValues?.(tableName);
|
|
9034
9430
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
9035
9431
|
relationMap,
|
|
9432
|
+
filterCtx,
|
|
9036
9433
|
tables,
|
|
9037
9434
|
tableName,
|
|
9038
9435
|
typeName,
|
|
@@ -9052,7 +9449,14 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
|
|
|
9052
9449
|
return {
|
|
9053
9450
|
set: input,
|
|
9054
9451
|
ops,
|
|
9055
|
-
filters:
|
|
9452
|
+
filters: scopedWhere({
|
|
9453
|
+
scope,
|
|
9454
|
+
tableName,
|
|
9455
|
+
table,
|
|
9456
|
+
where,
|
|
9457
|
+
relationCtx: relationFilterCtx(filterCtx, tableName, scope),
|
|
9458
|
+
required: requireWhere
|
|
9459
|
+
})
|
|
9056
9460
|
};
|
|
9057
9461
|
});
|
|
9058
9462
|
const anyNested = entries.some((entry) => entry.ops);
|
|
@@ -9141,7 +9545,8 @@ const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNest
|
|
|
9141
9545
|
tableName,
|
|
9142
9546
|
ops: entry.ops,
|
|
9143
9547
|
write,
|
|
9144
|
-
request
|
|
9548
|
+
request,
|
|
9549
|
+
existing: true
|
|
9145
9550
|
}) : write(tx, {});
|
|
9146
9551
|
};
|
|
9147
9552
|
return executor.transaction((tx) => {
|
|
@@ -9171,6 +9576,7 @@ const sqliteSchemaData = createSchemaDataGenerator({
|
|
|
9171
9576
|
preflight: (db, options) => {
|
|
9172
9577
|
if (options.features.nestedWrites && db.resultKind === "sync") throw new Error("Drizzle-GraphQL Error: features.nestedWrites requires an asynchronous SQLite driver (e.g. libsql). Synchronous drivers cannot run the multi-statement transaction a nested write needs.");
|
|
9173
9578
|
if (options.transactions && db.resultKind === "sync") throw new Error("Drizzle-GraphQL Error: transactions: 'auto' requires an asynchronous SQLite driver (e.g. libsql). Synchronous drivers cannot hold a transaction open across resolvers.");
|
|
9579
|
+
if (options.policies?.onWrite && db.resultKind === "sync") throw new Error("Drizzle-GraphQL Error: onWrite requires an asynchronous SQLite driver (e.g. libsql). Synchronous drivers commit before an awaited hook runs, so a throwing hook could not roll the mutation back.");
|
|
9174
9580
|
},
|
|
9175
9581
|
generateUpdateMany
|
|
9176
9582
|
});
|