@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.cjs
CHANGED
|
@@ -286,7 +286,6 @@ const singularizeMapper = (tableName) => ({
|
|
|
286
286
|
});
|
|
287
287
|
//#endregion
|
|
288
288
|
//#region src/util/type-converter/index.ts
|
|
289
|
-
const allowedNameChars = /^[a-zA-Z0-9_]+$/;
|
|
290
289
|
/** drizzle's `number` constraints whose values always fit a GraphQL `Int` (32-bit signed). */
|
|
291
290
|
const smallIntegerConstraints = /* @__PURE__ */ new Set([
|
|
292
291
|
"int8",
|
|
@@ -312,8 +311,15 @@ const declaredEnum = (column) => {
|
|
|
312
311
|
schema: typeof declared.schema === "string" ? declared.schema : void 0
|
|
313
312
|
} : void 0;
|
|
314
313
|
};
|
|
315
|
-
/**
|
|
316
|
-
|
|
314
|
+
/**
|
|
315
|
+
* `user_status` / `user-status` / `user status` → `UserStatus`; an already-PascalCase name is left
|
|
316
|
+
* alone. A GraphQL name cannot start with a digit, so a name that would (`2fa_mode`) is prefixed
|
|
317
|
+
* with an underscore: `_2faMode`.
|
|
318
|
+
*/
|
|
319
|
+
const pascalize = (input) => {
|
|
320
|
+
const pascal = input.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => capitalize(part)).join("");
|
|
321
|
+
return /^[0-9]/.test(pascal) ? `_${pascal}` : pascal;
|
|
322
|
+
};
|
|
317
323
|
/**
|
|
318
324
|
* Identity of the values a GraphQL enum was built from, so a name claimed twice can be told
|
|
319
325
|
* apart from the same enum reached twice.
|
|
@@ -388,7 +394,7 @@ const generateEnumCached = (column, columnName, tableName) => {
|
|
|
388
394
|
index,
|
|
389
395
|
typeName: name
|
|
390
396
|
});
|
|
391
|
-
if (mapped === void 0) return
|
|
397
|
+
if (mapped === void 0) return validEnumValueName(value) ? value : `Option${index}`;
|
|
392
398
|
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.`);
|
|
393
399
|
return mapped;
|
|
394
400
|
});
|
|
@@ -484,6 +490,27 @@ const sqliteTimestampToDate = (column, value) => new Date(column.mode === "times
|
|
|
484
490
|
* mode it uses, for the same reason as {@link isDateOnlyColumn}.
|
|
485
491
|
*/
|
|
486
492
|
const isDateTimeColumn = (column) => (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgTimestamp) || isPgTimestampStringColumn(column) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlTimestamp) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlDateTime) || isMySqlDateTimeStringColumn(column) || isSQLiteTimestampColumn(column);
|
|
493
|
+
/**
|
|
494
|
+
* Whether the column holds raw bytes: Postgres `bytea` and SQLite `blob({ mode: 'buffer' })`,
|
|
495
|
+
* both `object buffer` in drizzle v1. Their values are `[Int!]` lists of the bytes.
|
|
496
|
+
*/
|
|
497
|
+
const isBufferColumn = (column) => (column.dataType ?? "").endsWith(" buffer");
|
|
498
|
+
/**
|
|
499
|
+
* Whether the column is a float tuple or vector: drizzle v1 gives every such column an
|
|
500
|
+
* `array …` data type — `point` / `line` in tuple mode (`array point`, `array line`), PostGIS
|
|
501
|
+
* `geometry` in tuple mode, pgvector `vector` and `halfvec`. Each value is one list of numbers,
|
|
502
|
+
* typed `[Float!]`, not a database array of elements.
|
|
503
|
+
*/
|
|
504
|
+
const isFloatTupleColumn = (column) => (column.dataType ?? "").startsWith("array ");
|
|
505
|
+
/** The fixed length of a float tuple column's values, or `undefined` for a vector. */
|
|
506
|
+
const floatTupleLength = (column) => {
|
|
507
|
+
switch (column.dataType) {
|
|
508
|
+
case "array point":
|
|
509
|
+
case "array geometry": return 2;
|
|
510
|
+
case "array line": return 3;
|
|
511
|
+
default: return;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
487
514
|
const columnToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
488
515
|
const dimensions = column.dimensions;
|
|
489
516
|
if (dimensions !== void 0 && dimensions > 0) {
|
|
@@ -521,7 +548,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
521
548
|
return column.columnType === "PgGeometryObject" ? {
|
|
522
549
|
type: isInput ? geoXyInputType : geoXyType,
|
|
523
550
|
typeLabel: "Geometry points XY"
|
|
524
|
-
} : column
|
|
551
|
+
} : isBufferColumn(column) ? {
|
|
525
552
|
type: new graphql.GraphQLList(new graphql.GraphQLNonNull(graphql.GraphQLInt)),
|
|
526
553
|
typeLabel: "Buffer"
|
|
527
554
|
} : {
|
|
@@ -554,7 +581,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
554
581
|
type: GraphQLBigIntString,
|
|
555
582
|
typeLabel: "BigInt"
|
|
556
583
|
};
|
|
557
|
-
case "number": return constraint !== void 0 && smallIntegerConstraints.has(constraint) || (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgInteger) || (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgSerial) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlInt) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlSerial)
|
|
584
|
+
case "number": return constraint !== void 0 && smallIntegerConstraints.has(constraint) || (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgInteger) || (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgSerial) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlInt) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlSerial) ? {
|
|
558
585
|
type: graphql.GraphQLInt,
|
|
559
586
|
typeLabel: "Integer"
|
|
560
587
|
} : {
|
|
@@ -562,18 +589,10 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
562
589
|
typeLabel: "Float"
|
|
563
590
|
};
|
|
564
591
|
case "array": {
|
|
565
|
-
|
|
566
|
-
type: new graphql.GraphQLList(new graphql.GraphQLNonNull(graphql.GraphQLFloat)),
|
|
567
|
-
typeLabel: "Array<Float>"
|
|
568
|
-
};
|
|
569
|
-
if (column.columnType === "PgGeometry") return {
|
|
570
|
-
type: new graphql.GraphQLList(new graphql.GraphQLNonNull(graphql.GraphQLFloat)),
|
|
571
|
-
typeLabel: "Tuple<[Float, Float]>"
|
|
572
|
-
};
|
|
573
|
-
const innerType = columnToGraphQLCore(column.baseColumn, columnName, tableName, isInput);
|
|
592
|
+
const length = floatTupleLength(column);
|
|
574
593
|
return {
|
|
575
|
-
type: new graphql.GraphQLList(new graphql.GraphQLNonNull(
|
|
576
|
-
typeLabel: `Array
|
|
594
|
+
type: new graphql.GraphQLList(new graphql.GraphQLNonNull(graphql.GraphQLFloat)),
|
|
595
|
+
typeLabel: length === void 0 ? "Array<Float>" : `Tuple<[${Array(length).fill("Float").join(", ")}]>`
|
|
577
596
|
};
|
|
578
597
|
}
|
|
579
598
|
default: throw new Error(`Drizzle-GraphQL Error: Type ${column.dataType} is not implemented!`);
|
|
@@ -715,6 +734,18 @@ const columnDialect = (column) => {
|
|
|
715
734
|
*/
|
|
716
735
|
const isJsonColumn = (column) => (column.dataType ?? "").includes("json") && column.columnType !== "PgGeometryObject";
|
|
717
736
|
/**
|
|
737
|
+
* SQLite `blob({ mode: 'bigint' })`: the bigint is stored as the bytes of its decimal text, and
|
|
738
|
+
* SQLite compares blobs byte by byte, so `9` sorts above `10`. Equality still holds, so it can be
|
|
739
|
+
* grouped and made distinct on, but not ordered or given a min/max.
|
|
740
|
+
*/
|
|
741
|
+
const isSqliteBlobBigIntColumn = (column) => column.columnType === "SQLiteBigInt";
|
|
742
|
+
/**
|
|
743
|
+
* Whether `orderBy` (and so cursor pagination) offers a column. JSON has no meaningful order:
|
|
744
|
+
* PostgreSQL `json` cannot be ordered at all, and MySQL's JSON ordering disagrees with any
|
|
745
|
+
* keyset comparison a cursor could make. A SQLite blob bigint orders as text.
|
|
746
|
+
*/
|
|
747
|
+
const isOrderableColumn = (column) => !isJsonColumn(column) && !isSqliteBlobBigIntColumn(column);
|
|
748
|
+
/**
|
|
718
749
|
* Maps a Drizzle column to the generic filter type to use.
|
|
719
750
|
*
|
|
720
751
|
* Selection is keyed on the column's data type, never its name: a text column named
|
|
@@ -1221,6 +1252,36 @@ const pgTimestampTextToIso = (value) => {
|
|
|
1221
1252
|
const utc = (/* @__PURE__ */ new Date(wallMs - offsetSeconds * 1e3)).toISOString();
|
|
1222
1253
|
return utc.length === 24 ? `${utc.slice(0, 19)}${fraction}Z` : value;
|
|
1223
1254
|
};
|
|
1255
|
+
/** Marks a decoder installed by {@link installSqliteBlobJsonDecoders}, so a rebuild never wraps it twice. */
|
|
1256
|
+
const BLOB_JSON_DECODER = Symbol.for("drizzle-graphql.sqliteBlobJsonDecoder");
|
|
1257
|
+
/**
|
|
1258
|
+
* Gives SQLite's blob columns a decoder for the relational query builder's JSON path, so a NULL
|
|
1259
|
+
* there reads back as null.
|
|
1260
|
+
*
|
|
1261
|
+
* Inside a nested relation drizzle selects a blob as `hex(column)` (JSON cannot hold a blob), and
|
|
1262
|
+
* `hex(NULL)` is `''`. drizzle then decodes that `''` with the column's ordinary decoder: a
|
|
1263
|
+
* `blob({ mode: 'bigint' })` reads it as `0n` — indistinguishable from a stored 0 — and a
|
|
1264
|
+
* `blob({ mode: 'json' })` throws on it. drizzle consults a column's `mapFromJsonValue`, when it
|
|
1265
|
+
* has one, instead of `mapFromDriverValue` for exactly this path, so one is installed here that
|
|
1266
|
+
* reads `''` as NULL and hands everything else to the column's own decoder.
|
|
1267
|
+
*
|
|
1268
|
+
* `''` is never a stored bigint or JSON blob, so for those it can only be NULL. For a buffer
|
|
1269
|
+
* blob it is also how an empty blob arrives, so it is read as NULL only when the column is
|
|
1270
|
+
* nullable — a `notNull` column cannot hold the NULL, so there it is the empty blob.
|
|
1271
|
+
*/
|
|
1272
|
+
const installSqliteBlobJsonDecoders = (tables) => {
|
|
1273
|
+
for (const table of Object.values(tables)) for (const column of Object.values((0, drizzle_orm.getColumns)(table))) {
|
|
1274
|
+
const columnType = column.columnType ?? "";
|
|
1275
|
+
const isBuffer = columnType === "SQLiteBlobBuffer";
|
|
1276
|
+
if (columnType !== "SQLiteBigInt" && columnType !== "SQLiteBlobJson" && !isBuffer) continue;
|
|
1277
|
+
const existing = column.mapFromJsonValue;
|
|
1278
|
+
if (existing && !existing[BLOB_JSON_DECODER]) continue;
|
|
1279
|
+
const emptyIsNull = !isBuffer || !column.notNull;
|
|
1280
|
+
const decoder = (value) => value === "" && emptyIsNull ? null : column.mapFromDriverValue(value);
|
|
1281
|
+
decoder[BLOB_JSON_DECODER] = true;
|
|
1282
|
+
column.mapFromJsonValue = decoder;
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1224
1285
|
const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
|
|
1225
1286
|
if (Array.isArray(value)) {
|
|
1226
1287
|
const relations = relationMap?.[tableName];
|
|
@@ -1247,14 +1308,14 @@ const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
|
|
|
1247
1308
|
if (value instanceof Date) return value.toISOString();
|
|
1248
1309
|
if (typeof value === "string" && isMySqlDateTimeStringColumn(column)) return mySqlDateTimeTextToIso(value);
|
|
1249
1310
|
if (typeof value === "string" && isPgTimestampStringColumn(column)) return pgTimestampTextToIso(value);
|
|
1250
|
-
if (value instanceof
|
|
1311
|
+
if (value instanceof Uint8Array) return Array.from(value);
|
|
1251
1312
|
if (typeof value === "bigint") return value.toString();
|
|
1252
1313
|
if (Array.isArray(value)) {
|
|
1253
|
-
if (column
|
|
1314
|
+
if (isFloatTupleColumn(column)) return value;
|
|
1254
1315
|
return value.map((arrVal) => remapToGraphQLCore(key, arrVal, tableName, column, relationMap));
|
|
1255
1316
|
}
|
|
1256
1317
|
if (typeof value === "object" && value !== null) {
|
|
1257
|
-
if (column.
|
|
1318
|
+
if ((column.dataType ?? "").startsWith("object")) return value;
|
|
1258
1319
|
return JSON.stringify(value);
|
|
1259
1320
|
}
|
|
1260
1321
|
return value;
|
|
@@ -1305,10 +1366,6 @@ const remapToGraphQLSingleOutput = (queryOutput, tableName, table, relationMap)
|
|
|
1305
1366
|
continue;
|
|
1306
1367
|
}
|
|
1307
1368
|
const column = columns[key];
|
|
1308
|
-
if (value === 0n && column && column.columnType === "SQLiteBigInt" && !column.notNull) {
|
|
1309
|
-
delete queryOutput[key];
|
|
1310
|
-
continue;
|
|
1311
|
-
}
|
|
1312
1369
|
const remapped = remapToGraphQLCore(key, value, tableName, column, relationMap);
|
|
1313
1370
|
if (column && remapped !== value) keepRawColumnValue(queryOutput, key, value);
|
|
1314
1371
|
queryOutput[key] = remapped;
|
|
@@ -1328,6 +1385,7 @@ const remapFromGraphQLCore = (value, column, columnName) => {
|
|
|
1328
1385
|
const dataType = column.dataType ?? "";
|
|
1329
1386
|
const columnType = column.columnType ?? "";
|
|
1330
1387
|
if (isDateTimeColumn(column)) {
|
|
1388
|
+
if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
|
|
1331
1389
|
const isMySql = columnType.startsWith("MySql");
|
|
1332
1390
|
const isPgString = isPgTimestampStringColumn(column);
|
|
1333
1391
|
const toIso = isMySql ? mySqlDateTimeTextToIso : isPgString ? pgTimestampTextToIso : void 0;
|
|
@@ -1342,41 +1400,29 @@ const remapFromGraphQLCore = (value, column, columnName) => {
|
|
|
1342
1400
|
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" });
|
|
1343
1401
|
return day;
|
|
1344
1402
|
}
|
|
1345
|
-
if (dataType.includes("bigint"))
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1349
|
-
}
|
|
1350
|
-
if (dataType.includes("json") && column.columnType !== "PgGeometryObject") return value;
|
|
1351
|
-
switch (dataType) {
|
|
1352
|
-
case "date": {
|
|
1353
|
-
const formatted = new Date(value);
|
|
1354
|
-
if (Number.isNaN(formatted.getTime())) throw drizzleError(`Field '${columnName}' is not a valid date!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1355
|
-
return formatted;
|
|
1356
|
-
}
|
|
1357
|
-
case "buffer":
|
|
1358
|
-
if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1359
|
-
return Buffer.from(value);
|
|
1360
|
-
case "json":
|
|
1361
|
-
if (column.columnType === "PgGeometryObject") return value;
|
|
1362
|
-
try {
|
|
1363
|
-
return JSON.parse(value);
|
|
1364
|
-
} catch (e) {
|
|
1365
|
-
throw drizzleError(`Invalid JSON in field '${columnName}':\n${e instanceof Error ? e.message : "Unknown error"}`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1366
|
-
}
|
|
1367
|
-
case "array":
|
|
1368
|
-
if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1369
|
-
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" });
|
|
1370
|
-
return value;
|
|
1371
|
-
case "bigint": try {
|
|
1403
|
+
if (dataType.includes("bigint")) {
|
|
1404
|
+
if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
|
|
1405
|
+
try {
|
|
1372
1406
|
return BigInt(value);
|
|
1373
|
-
} catch
|
|
1407
|
+
} catch {
|
|
1374
1408
|
throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1375
1409
|
}
|
|
1376
|
-
default:
|
|
1377
|
-
if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
|
|
1378
|
-
return value;
|
|
1379
1410
|
}
|
|
1411
|
+
if (isJsonColumn(column)) return value;
|
|
1412
|
+
if (isBufferColumn(column)) {
|
|
1413
|
+
if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1414
|
+
if (column.dimensions > 0 && value.every((element) => element === null || Array.isArray(element))) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
|
|
1415
|
+
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" });
|
|
1416
|
+
return Buffer.from(value);
|
|
1417
|
+
}
|
|
1418
|
+
if (isFloatTupleColumn(column)) {
|
|
1419
|
+
if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1420
|
+
const length = floatTupleLength(column);
|
|
1421
|
+
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" });
|
|
1422
|
+
return value;
|
|
1423
|
+
}
|
|
1424
|
+
if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
|
|
1425
|
+
return value;
|
|
1380
1426
|
};
|
|
1381
1427
|
const remapFromGraphQLSingleInput = (queryInput, table, operation = "insert") => {
|
|
1382
1428
|
const columns = (0, drizzle_orm.getColumns)(table);
|
|
@@ -1418,36 +1464,60 @@ const escapeLikeValue = (value) => value.replace(/[\\%_]/g, (char) => `\\${char}
|
|
|
1418
1464
|
const safeLikeOps = {
|
|
1419
1465
|
startsWith: {
|
|
1420
1466
|
buildPattern: (value) => `${escapeLikeValue(value)}%`,
|
|
1467
|
+
buildGlob: (value) => `${escapeGlobValue(value)}*`,
|
|
1421
1468
|
insensitive: false
|
|
1422
1469
|
},
|
|
1423
1470
|
endsWith: {
|
|
1424
1471
|
buildPattern: (value) => `%${escapeLikeValue(value)}`,
|
|
1472
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}`,
|
|
1425
1473
|
insensitive: false
|
|
1426
1474
|
},
|
|
1427
1475
|
contains: {
|
|
1428
1476
|
buildPattern: (value) => `%${escapeLikeValue(value)}%`,
|
|
1477
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}*`,
|
|
1429
1478
|
insensitive: false
|
|
1430
1479
|
},
|
|
1431
1480
|
iStartsWith: {
|
|
1432
1481
|
buildPattern: (value) => `${escapeLikeValue(value)}%`,
|
|
1482
|
+
buildGlob: (value) => `${escapeGlobValue(value)}*`,
|
|
1433
1483
|
insensitive: true
|
|
1434
1484
|
},
|
|
1435
1485
|
iEndsWith: {
|
|
1436
1486
|
buildPattern: (value) => `%${escapeLikeValue(value)}`,
|
|
1487
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}`,
|
|
1437
1488
|
insensitive: true
|
|
1438
1489
|
},
|
|
1439
1490
|
iContains: {
|
|
1440
1491
|
buildPattern: (value) => `%${escapeLikeValue(value)}%`,
|
|
1492
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}*`,
|
|
1441
1493
|
insensitive: true
|
|
1442
1494
|
}
|
|
1443
1495
|
};
|
|
1444
1496
|
/**
|
|
1445
|
-
* `
|
|
1446
|
-
*
|
|
1447
|
-
*
|
|
1497
|
+
* Escapes SQLite's `GLOB` wildcards (`*`, `?`, `[`) in a literal search term by wrapping each in
|
|
1498
|
+
* a one-character class, so the term only ever matches literally inside a GLOB pattern. GLOB
|
|
1499
|
+
* has no escape character; `]` outside a class and everything else is already literal.
|
|
1500
|
+
*/
|
|
1501
|
+
const escapeGlobValue = (value) => value.replace(/[*?[]/g, (char) => `[${char}]`);
|
|
1502
|
+
/**
|
|
1503
|
+
* A caller-written LIKE pattern as the GLOB pattern matching the same strings, for SQLite's
|
|
1504
|
+
* case-sensitive `like` / `notLike`: `%` → `*`, `_` → `?`, and GLOB's own wildcards escaped.
|
|
1505
|
+
* The raw operators bind no `ESCAPE` clause, so `\` is an ordinary character in both.
|
|
1506
|
+
*/
|
|
1507
|
+
const likePatternToGlob = (pattern) => pattern.replace(/[%_*?[]/g, (char) => char === "%" ? "*" : char === "_" ? "?" : `[${char}]`);
|
|
1508
|
+
/**
|
|
1509
|
+
* The safe string operator's predicate. Case-insensitive variants use Postgres's native
|
|
1510
|
+
* `ILIKE`; MySQL and SQLite have no `ILIKE`, so they compare `lower()` on both sides instead
|
|
1511
|
+
* (mirroring how the raw `ilike` operator is Postgres-only).
|
|
1512
|
+
*
|
|
1513
|
+
* The case-sensitive variants are `LIKE <pattern> ESCAPE '\'`, except on SQLite, whose `LIKE`
|
|
1514
|
+
* folds ASCII case — `contains: "App"` would match `apple`. There they are `GLOB`, which is
|
|
1515
|
+
* case-sensitive, as `LIKE` is on Postgres. `dialectColumn` names the column the dialect is read
|
|
1516
|
+
* from when `column` is an expression over it (a JSON path read).
|
|
1448
1517
|
*/
|
|
1449
|
-
const safeLikeCondition = (column,
|
|
1450
|
-
|
|
1518
|
+
const safeLikeCondition = (column, value, op, insensitive, dialectColumn = column) => {
|
|
1519
|
+
const pattern = op.buildPattern(value);
|
|
1520
|
+
if (!insensitive) return columnDialect(dialectColumn) === "sqlite" ? drizzle_orm.sql`${column} glob ${op.buildGlob(value)}` : drizzle_orm.sql`${column} like ${pattern} escape ${LIKE_ESCAPE_CHAR}`;
|
|
1451
1521
|
return (column.columnType ?? "").startsWith("Pg") ? drizzle_orm.sql`${column} ilike ${pattern} escape ${LIKE_ESCAPE_CHAR}` : drizzle_orm.sql`lower(${column}) like ${pattern.toLowerCase()} escape ${LIKE_ESCAPE_CHAR}`;
|
|
1452
1522
|
};
|
|
1453
1523
|
/**
|
|
@@ -1503,6 +1573,35 @@ const jsonContains = (column, columnName, value) => {
|
|
|
1503
1573
|
}
|
|
1504
1574
|
};
|
|
1505
1575
|
/**
|
|
1576
|
+
* The nodes of a SQLite JSON document as `(path, kind, value)` rows, for a structural
|
|
1577
|
+
* comparison. Integers and reals share one kind so `1` and `1.0` are the same number, as they
|
|
1578
|
+
* are on the other two dialects.
|
|
1579
|
+
*/
|
|
1580
|
+
const sqliteJsonNodes = (document) => drizzle_orm.sql`select fullkey, case when type in ('integer', 'real') then 'number' else type end, atom from json_tree(${document})`;
|
|
1581
|
+
/**
|
|
1582
|
+
* Whole-document `eq` / `ne` on a JSON column, comparing documents rather than their text:
|
|
1583
|
+
* - Postgres: as `jsonb` (a plain `json` column has no `=` operator at all)
|
|
1584
|
+
* - MySQL: against `CAST(? AS JSON)`; a bare string parameter would be a JSON string scalar
|
|
1585
|
+
* - SQLite: the two documents' node sets must match both ways, so object key order and
|
|
1586
|
+
* whitespace do not matter, while array order still does
|
|
1587
|
+
* A SQL NULL column matches neither, as with `=` / `<>` on any other column.
|
|
1588
|
+
*/
|
|
1589
|
+
const jsonEquality = (column, columnName, value, negated) => {
|
|
1590
|
+
const serialized = JSON.stringify(value);
|
|
1591
|
+
switch (columnDialect(column)) {
|
|
1592
|
+
case "pg": {
|
|
1593
|
+
const left = column.columnType === "PgJson" ? drizzle_orm.sql`${column}::jsonb` : drizzle_orm.sql`${column}`;
|
|
1594
|
+
return negated ? drizzle_orm.sql`${left} <> ${serialized}::jsonb` : drizzle_orm.sql`${left} = ${serialized}::jsonb`;
|
|
1595
|
+
}
|
|
1596
|
+
case "mysql": return negated ? drizzle_orm.sql`${column} <> cast(${serialized} as json)` : drizzle_orm.sql`${column} = cast(${serialized} as json)`;
|
|
1597
|
+
case "sqlite": {
|
|
1598
|
+
const equal = drizzle_orm.sql`(not exists (${sqliteJsonNodes(column)} except ${sqliteJsonNodes(serialized)}) and not exists (${sqliteJsonNodes(serialized)} except ${sqliteJsonNodes(column)}))`;
|
|
1599
|
+
return negated ? drizzle_orm.sql`(${column} is not null and not ${equal})` : drizzle_orm.sql`(${column} is not null and ${equal})`;
|
|
1600
|
+
}
|
|
1601
|
+
default: throw drizzleError(`WHERE ${columnName}: JSON equality is not supported for this dialect!`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
/**
|
|
1506
1605
|
* Constant predicates, used where an operator's operand list is empty and the answer is
|
|
1507
1606
|
* therefore known without touching the column. Written as `1 = 0` / `1 = 1` rather than the
|
|
1508
1607
|
* `FALSE` / `TRUE` keywords so they compile identically on all three dialects. Built fresh
|
|
@@ -1537,7 +1636,8 @@ const NUMERIC_TEXT_PATTERN = "^\\s*-?(\\d+\\.?\\d*|\\.\\d+)([eE][-+]?\\d+)?\\s*$
|
|
|
1537
1636
|
* pattern guard.
|
|
1538
1637
|
* - **SQLite** — `json_extract` returns the value in SQLite's own type, so the reads cast
|
|
1539
1638
|
* explicitly: without that, SQLite's cross-type ordering puts every string above every
|
|
1540
|
-
* number and `'admin' > 0` would be true.
|
|
1639
|
+
* number and `'admin' > 0` would be true. A JSON boolean comes back as 1 / 0, so every read
|
|
1640
|
+
* also checks `json_type` to keep booleans and numbers apart.
|
|
1541
1641
|
*/
|
|
1542
1642
|
const jsonPathExprs = (column, path) => {
|
|
1543
1643
|
switch (columnDialect(column)) {
|
|
@@ -1563,10 +1663,11 @@ const jsonPathExprs = (column, path) => {
|
|
|
1563
1663
|
}
|
|
1564
1664
|
default: {
|
|
1565
1665
|
const extracted = drizzle_orm.sql`json_extract(${column}, ${jsonPathString(path)})`;
|
|
1666
|
+
const type = drizzle_orm.sql`json_type(${column}, ${jsonPathString(path)})`;
|
|
1566
1667
|
return {
|
|
1567
|
-
text: drizzle_orm.sql`cast(${extracted} as text)`,
|
|
1568
|
-
number: drizzle_orm.sql`(case when
|
|
1569
|
-
boolean: extracted
|
|
1668
|
+
text: drizzle_orm.sql`(case ${type} when 'true' then 'true' when 'false' then 'false' else cast(${extracted} as text) end)`,
|
|
1669
|
+
number: drizzle_orm.sql`(case when ${type} in ('integer', 'real') then ${extracted} end)`,
|
|
1670
|
+
boolean: drizzle_orm.sql`(case when ${type} in ('true', 'false') then ${extracted} end)`,
|
|
1570
1671
|
encodeBoolean: (value) => value ? 1 : 0
|
|
1571
1672
|
};
|
|
1572
1673
|
}
|
|
@@ -1605,9 +1706,9 @@ const jsonPathCondition = (column, columnName, filter) => {
|
|
|
1605
1706
|
continue;
|
|
1606
1707
|
}
|
|
1607
1708
|
if (operatorName in safeLikeOps) {
|
|
1608
|
-
const
|
|
1709
|
+
const op = safeLikeOps[operatorName];
|
|
1609
1710
|
if (typeof operatorValue !== "string") throw drizzleError(`WHERE ${locator}: operator '${operatorName}' takes a string`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1610
|
-
variants.push(safeLikeCondition(exprs.text,
|
|
1711
|
+
variants.push(safeLikeCondition(exprs.text, operatorValue, op, op.insensitive, column));
|
|
1611
1712
|
continue;
|
|
1612
1713
|
}
|
|
1613
1714
|
const comparison = JSON_PATH_COMPARISONS[operatorName];
|
|
@@ -1665,20 +1766,25 @@ const extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1665
1766
|
for (const [operatorName, operatorValue] of entries) {
|
|
1666
1767
|
if (operatorValue === null || operatorValue === void 0) continue;
|
|
1667
1768
|
if (operatorValue === false && FLAG_OPERATORS.has(operatorName)) continue;
|
|
1668
|
-
if (operatorName
|
|
1769
|
+
if ((operatorName === "eq" || operatorName === "ne") && isJsonColumn(column)) variants.push(jsonEquality(column, columnName, operatorValue, operatorName === "ne"));
|
|
1770
|
+
else if (operatorName in singleValueOps) {
|
|
1669
1771
|
const singleValue = remapFromGraphQLCore(operatorValue, column, columnName);
|
|
1670
1772
|
variants.push(foldCase ? lowerComparison(column, operatorName, singleValue) : singleValueOps[operatorName](column, singleValue));
|
|
1671
1773
|
} else if (operatorName in stringValueOps) {
|
|
1672
|
-
const
|
|
1673
|
-
variants.push(
|
|
1774
|
+
const isLike = operatorName === "like" || operatorName === "notLike";
|
|
1775
|
+
if (isLike && foldCase || (operatorName === "ilike" || operatorName === "notIlike") && columnDialect(column) !== "pg") variants.push(insensitiveLike(column, operatorValue, operatorName === "notLike" || operatorName === "notIlike"));
|
|
1776
|
+
else if (isLike && columnDialect(column) === "sqlite") {
|
|
1777
|
+
const glob = likePatternToGlob(operatorValue);
|
|
1778
|
+
variants.push(operatorName === "like" ? drizzle_orm.sql`${column} glob ${glob}` : drizzle_orm.sql`${column} not glob ${glob}`);
|
|
1779
|
+
} else variants.push(stringValueOps[operatorName](column, operatorValue));
|
|
1674
1780
|
} else if (operatorName === "path" && isJsonColumn(column)) for (const pathFilter of operatorValue) {
|
|
1675
1781
|
const extracted = jsonPathCondition(column, columnName, pathFilter);
|
|
1676
1782
|
if (extracted) variants.push(extracted);
|
|
1677
1783
|
}
|
|
1678
1784
|
else if (operatorName === "contains" && isJsonColumn(column)) variants.push(jsonContains(column, columnName, operatorValue));
|
|
1679
1785
|
else if (operatorName in safeLikeOps) {
|
|
1680
|
-
const
|
|
1681
|
-
variants.push(safeLikeCondition(column,
|
|
1786
|
+
const op = safeLikeOps[operatorName];
|
|
1787
|
+
variants.push(safeLikeCondition(column, operatorValue, op, op.insensitive || foldCase));
|
|
1682
1788
|
} else if (operatorName in arrayValueOps) {
|
|
1683
1789
|
if (!operatorValue.length) {
|
|
1684
1790
|
variants.push(operatorName === "inArray" ? sqlFalse() : sqlTrue());
|
|
@@ -1686,13 +1792,13 @@ const extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1686
1792
|
}
|
|
1687
1793
|
const arrayValue = operatorValue.map((val) => remapFromGraphQLCore(val, column, columnName));
|
|
1688
1794
|
variants.push(foldCase ? lowerMembership(column, arrayValue, operatorName === "notInArray") : arrayValueOps[operatorName](column, arrayValue));
|
|
1689
|
-
} else if (operatorName === "has") variants.push((0, drizzle_orm.arrayContains)(column, [operatorValue]));
|
|
1795
|
+
} else if (operatorName === "has") variants.push((0, drizzle_orm.arrayContains)(column, [remapFromGraphQLCore(operatorValue, column, columnName)]));
|
|
1690
1796
|
else if (operatorName in arrayMembershipOps) {
|
|
1691
1797
|
if (!operatorValue.length) {
|
|
1692
1798
|
variants.push(operatorName === "hasSome" ? sqlFalse() : sqlTrue());
|
|
1693
1799
|
continue;
|
|
1694
1800
|
}
|
|
1695
|
-
variants.push(arrayMembershipOps[operatorName](column, operatorValue));
|
|
1801
|
+
variants.push(arrayMembershipOps[operatorName](column, operatorValue.map((element) => remapFromGraphQLCore(element, column, columnName))));
|
|
1696
1802
|
} else if (operatorName === "isEmpty") variants.push(drizzle_orm.sql`cardinality(${column}) = 0`);
|
|
1697
1803
|
else if (operatorName in nullableOps) variants.push(nullableOps[operatorName](column));
|
|
1698
1804
|
else throw drizzleError(`WHERE ${columnName}: Unknown operator: ${operatorName}`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
@@ -2540,6 +2646,32 @@ const withScope = (scope, tableName, table, condition, mode) => {
|
|
|
2540
2646
|
const predicate = scope?.on(tableName, table, mode);
|
|
2541
2647
|
return predicate ? (0, drizzle_orm.and)(condition, predicate) : condition;
|
|
2542
2648
|
};
|
|
2649
|
+
/**
|
|
2650
|
+
* The junction table a `.through()` relation links through, by its schema key — the key its
|
|
2651
|
+
* `scope` and `softDelete` are configured under. `undefined` for a relation with no junction,
|
|
2652
|
+
* or a junction that is not one of the build's tables.
|
|
2653
|
+
*/
|
|
2654
|
+
const junctionTableName = (tables, relEntry) => {
|
|
2655
|
+
const throughTable = (relEntry?.relation ?? relEntry)?.throughTable;
|
|
2656
|
+
return throughTable ? Object.keys(tables).find((name) => tables[name] === throughTable) : void 0;
|
|
2657
|
+
};
|
|
2658
|
+
/**
|
|
2659
|
+
* Whether a `.through()` relation's junction restricts which links exist for this request —
|
|
2660
|
+
* a `scope` on the junction table, or a soft delete it declares.
|
|
2661
|
+
*/
|
|
2662
|
+
const hasJunctionPolicy = (scope, junctionName) => !!junctionName && !!scope?.has(junctionName, "EXCLUDE");
|
|
2663
|
+
/**
|
|
2664
|
+
* The junction's own row policy, compiled against the junction table as the query names it.
|
|
2665
|
+
* A `.through()` relation links only through the junction rows this keeps, on every path that
|
|
2666
|
+
* reads the relation — the relation field, `<relation>Aggregate`, and a relation filter — so
|
|
2667
|
+
* they all agree on which rows are related.
|
|
2668
|
+
*
|
|
2669
|
+
* The junction is always read at `EXCLUDE`: a marked junction row is a removed link. The
|
|
2670
|
+
* relation's `deleted` argument is about the *target* rows, and the junction's own
|
|
2671
|
+
* `softDelete.scope` only governs reads of the junction table's own fields — a link row has no
|
|
2672
|
+
* field of its own on the relation that `'root'` could keep rendering.
|
|
2673
|
+
*/
|
|
2674
|
+
const junctionPolicy = (scope, junctionName, table) => hasJunctionPolicy(scope, junctionName) ? scope.on(junctionName, table, "EXCLUDE") : void 0;
|
|
2543
2675
|
/** Binds a build's {@link TablePolicies} to its relation context, once, at schema-build time. */
|
|
2544
2676
|
const bindPolicies = (policies, filterCtx) => policies?.scope || policies?.contextValues || policies?.softDelete || policies?.onWrite || policies?.defaultOrderBy ? {
|
|
2545
2677
|
scope: policies.scope || policies.softDelete ? (context) => resolveScope(policies, context, filterCtx) : void 0,
|
|
@@ -2590,7 +2722,7 @@ const deletedArg = (softDelete, tableName, resolveName) => softDelete?.(tableNam
|
|
|
2590
2722
|
const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableName, resolveName) => ({
|
|
2591
2723
|
offset: { type: graphql.GraphQLInt },
|
|
2592
2724
|
limit: { type: graphql.GraphQLInt },
|
|
2593
|
-
orderBy: { type: orderArgs },
|
|
2725
|
+
...orderArgs ? { orderBy: { type: orderArgs } } : {},
|
|
2594
2726
|
where: { type: filterArgs },
|
|
2595
2727
|
after: {
|
|
2596
2728
|
type: graphql.GraphQLString,
|
|
@@ -2602,7 +2734,7 @@ const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableN
|
|
|
2602
2734
|
/** GraphQL argument map for a single-row select field (no `limit`). */
|
|
2603
2735
|
const selectSingleArgs = (orderArgs, filterArgs, softDelete, tableName, resolveName) => ({
|
|
2604
2736
|
offset: { type: graphql.GraphQLInt },
|
|
2605
|
-
orderBy: { type: orderArgs },
|
|
2737
|
+
...orderArgs ? { orderBy: { type: orderArgs } } : {},
|
|
2606
2738
|
where: { type: filterArgs },
|
|
2607
2739
|
...deletedArg(softDelete, tableName, resolveName)
|
|
2608
2740
|
});
|
|
@@ -2652,7 +2784,7 @@ const generateDistinctEnum = (table, typeName, resolveName, tableKey) => {
|
|
|
2652
2784
|
defaultName,
|
|
2653
2785
|
table: tableKey,
|
|
2654
2786
|
operation: "distinct"
|
|
2655
|
-
}) ?? defaultName, `Columns of ${typeName} that a query can be made distinct on
|
|
2787
|
+
}) ?? defaultName, `Columns of ${typeName} that a query can be made distinct on`, (column) => !isJsonColumn(column));
|
|
2656
2788
|
};
|
|
2657
2789
|
//#endregion
|
|
2658
2790
|
//#region src/util/builders/common/conflict.ts
|
|
@@ -2822,6 +2954,44 @@ const resolveConflictPlan = (params) => {
|
|
|
2822
2954
|
plan
|
|
2823
2955
|
}));
|
|
2824
2956
|
};
|
|
2957
|
+
/**
|
|
2958
|
+
* Adds a {@link ConflictPlan}'s clause to a PostgreSQL or SQLite insert.
|
|
2959
|
+
*
|
|
2960
|
+
* SQLite needs two things drizzle's builder does not give it when the target is a partial
|
|
2961
|
+
* unique index:
|
|
2962
|
+
*
|
|
2963
|
+
* - The index predicate written literally. SQLite matches a conflict target to an index by
|
|
2964
|
+
* comparing the predicates as written, and a bound `?` never equals `'active'`, so a
|
|
2965
|
+
* predicate that compares to a value would never find its index. Its params are inlined.
|
|
2966
|
+
* - A `DO NOTHING` it can parse. Drizzle writes the predicate after `do nothing`, which is a
|
|
2967
|
+
* syntax error, so the action is spelled as an update that matches no row instead:
|
|
2968
|
+
* `ON CONFLICT (…) WHERE … DO UPDATE SET col = col WHERE 0 = 1`. A row that is not updated is
|
|
2969
|
+
* not returned, which is what `DO NOTHING` does too.
|
|
2970
|
+
*/
|
|
2971
|
+
const applyConflictPlan = (query, table, plan, setWhere) => {
|
|
2972
|
+
const sqlite = (0, drizzle_orm.is)(table, drizzle_orm_sqlite_core.SQLiteTable);
|
|
2973
|
+
const targetWhere = sqlite && plan.targetWhere ? drizzle_orm.sql`${plan.targetWhere}`.inlineParams() : plan.targetWhere;
|
|
2974
|
+
if (plan.action === "UPDATE") return query.onConflictDoUpdate({
|
|
2975
|
+
target: plan.target,
|
|
2976
|
+
targetWhere,
|
|
2977
|
+
set: plan.set,
|
|
2978
|
+
setWhere
|
|
2979
|
+
});
|
|
2980
|
+
if (!plan.target) return query.onConflictDoNothing();
|
|
2981
|
+
if (sqlite && targetWhere) {
|
|
2982
|
+
const [key] = Object.entries((0, drizzle_orm.getColumns)(table)).find(([, column]) => column === plan.target[0]);
|
|
2983
|
+
return query.onConflictDoUpdate({
|
|
2984
|
+
target: plan.target,
|
|
2985
|
+
targetWhere,
|
|
2986
|
+
set: { [key]: drizzle_orm.sql`${plan.target[0]}` },
|
|
2987
|
+
setWhere: drizzle_orm.sql`0 = 1`
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
return query.onConflictDoNothing({
|
|
2991
|
+
target: plan.target,
|
|
2992
|
+
where: targetWhere
|
|
2993
|
+
});
|
|
2994
|
+
};
|
|
2825
2995
|
/** `excluded.<column>` — PostgreSQL and SQLite name the rejected row this way. */
|
|
2826
2996
|
const excludedColumnRef = (columnName) => drizzle_orm.sql`excluded.${drizzle_orm.sql.identifier(columnName)}`;
|
|
2827
2997
|
/** `values(<column>)` — MySQL's equivalent inside ON DUPLICATE KEY UPDATE. */
|
|
@@ -3103,6 +3273,21 @@ const decodeCursor = (after, entries) => {
|
|
|
3103
3273
|
return payload.v.map(decodeCursorValue);
|
|
3104
3274
|
};
|
|
3105
3275
|
/**
|
|
3276
|
+
* `col > value` (asc) or `col < value` (desc), compared by the rule the ORDER BY sorts by.
|
|
3277
|
+
* MySQL sorts an ENUM by its declaration index but compares it with a string as a string, so
|
|
3278
|
+
* there the comparison is between `col + 0` (the 1-based index) and the value's own index.
|
|
3279
|
+
*/
|
|
3280
|
+
const strictComparison = (col, direction, value) => {
|
|
3281
|
+
if ((0, drizzle_orm.is)(col, drizzle_orm_mysql_core.MySqlEnumColumn) || (0, drizzle_orm.is)(col, drizzle_orm_mysql_core.MySqlEnumObjectColumn)) {
|
|
3282
|
+
const index = col.enumValues?.indexOf(value) ?? -1;
|
|
3283
|
+
if (index >= 0) {
|
|
3284
|
+
const position = drizzle_orm.sql`(${col} + 0)`;
|
|
3285
|
+
return direction === "asc" ? drizzle_orm.sql`${position} > ${index + 1}` : drizzle_orm.sql`${position} < ${index + 1}`;
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
return direction === "asc" ? (0, drizzle_orm.gt)(col, value) : (0, drizzle_orm.lt)(col, value);
|
|
3289
|
+
};
|
|
3290
|
+
/**
|
|
3106
3291
|
* The keyset predicate selecting rows strictly after the cursor position in the given total
|
|
3107
3292
|
* order — the expanded lexicographic form
|
|
3108
3293
|
* `after(k0) OR (k0 = v0 AND after(k1)) OR (k0 = v0 AND k1 = v1 AND after(k2)) …`,
|
|
@@ -3127,7 +3312,7 @@ const buildCursorCondition = (table, entries, values, nullOrdering) => {
|
|
|
3127
3312
|
let strictlyAfter;
|
|
3128
3313
|
if (value === null || value === void 0) strictlyAfter = nullsSortLast ? void 0 : (0, drizzle_orm.isNotNull)(col);
|
|
3129
3314
|
else {
|
|
3130
|
-
const comparison =
|
|
3315
|
+
const comparison = strictComparison(col, direction, value);
|
|
3131
3316
|
strictlyAfter = nullsSortLast ? (0, drizzle_orm.or)(comparison, (0, drizzle_orm.isNull)(col)) : comparison;
|
|
3132
3317
|
}
|
|
3133
3318
|
if (strictlyAfter) disjuncts.push(equalities.length ? (0, drizzle_orm.and)(...equalities, strictlyAfter) : strictlyAfter);
|
|
@@ -3330,6 +3515,115 @@ const getUniqueTargets = (table, getTableConfig) => {
|
|
|
3330
3515
|
*/
|
|
3331
3516
|
const getUniqueColumnSets = (table, getTableConfig, options = {}) => getUniqueTargets(table, getTableConfig).filter((target) => options.partial || !target.where).map((target) => target.columns);
|
|
3332
3517
|
//#endregion
|
|
3518
|
+
//#region src/util/builders/common/limits.ts
|
|
3519
|
+
/**
|
|
3520
|
+
* The `limit` to send with an `offset` the request gave without one. SQLite and MySQL accept
|
|
3521
|
+
* `OFFSET` only after a `LIMIT`, so `offset` alone has to carry a limit that means "all of
|
|
3522
|
+
* them": SQLite reads `LIMIT -1` that way, and on MySQL it is the largest integer every driver
|
|
3523
|
+
* binds exactly (MySQL's own documented 2^64-1 does not fit a JS number). PostgreSQL takes
|
|
3524
|
+
* `OFFSET` alone, so the limit is left as it was.
|
|
3525
|
+
*/
|
|
3526
|
+
const offsetOnlyLimit = (table, limit, offset) => {
|
|
3527
|
+
if (limit != null || !offset) return limit;
|
|
3528
|
+
const firstColumn = Object.values((0, drizzle_orm.getColumns)(table))[0];
|
|
3529
|
+
switch (firstColumn ? columnDialect(firstColumn) : void 0) {
|
|
3530
|
+
case "sqlite": return -1;
|
|
3531
|
+
case "mysql": return Number.MAX_SAFE_INTEGER;
|
|
3532
|
+
default: return limit;
|
|
3533
|
+
}
|
|
3534
|
+
};
|
|
3535
|
+
/**
|
|
3536
|
+
* Substitutes a table's default ordering for an absent `orderBy`.
|
|
3537
|
+
*
|
|
3538
|
+
* Applied where the arguments are first read, so everything downstream — the cursor tuple, a
|
|
3539
|
+
* `distinct` pass, the plain-select fallback — sees one effective ordering and cannot
|
|
3540
|
+
* disagree about it. Only a missing argument is replaced: `orderBy: {}` is a request for no
|
|
3541
|
+
* ordering, and stays one.
|
|
3542
|
+
*/
|
|
3543
|
+
const withDefaultOrderBy = (args, tableName, defaults) => {
|
|
3544
|
+
if (!defaults || args?.orderBy != null) return args;
|
|
3545
|
+
const fallback = defaults(tableName);
|
|
3546
|
+
return fallback ? {
|
|
3547
|
+
...args,
|
|
3548
|
+
orderBy: fallback
|
|
3549
|
+
} : args;
|
|
3550
|
+
};
|
|
3551
|
+
/**
|
|
3552
|
+
* Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
|
|
3553
|
+
* dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
|
|
3554
|
+
* straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
|
|
3555
|
+
* whether or not the table has a limit policy.
|
|
3556
|
+
*/
|
|
3557
|
+
const assertPagingNotNegative = (paging, errorCtx) => {
|
|
3558
|
+
for (const name of ["limit", "offset"]) {
|
|
3559
|
+
const value = paging[name];
|
|
3560
|
+
if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
|
|
3561
|
+
code: "DRIZZLE_INVALID_INPUT_VALUE",
|
|
3562
|
+
...errorCtx
|
|
3563
|
+
});
|
|
3564
|
+
}
|
|
3565
|
+
};
|
|
3566
|
+
/**
|
|
3567
|
+
* The `limit` a query actually runs with, given the policy for the table it reads.
|
|
3568
|
+
*
|
|
3569
|
+
* A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
|
|
3570
|
+
* that is configured — an absent limit means every row, which is above any maximum. A limit
|
|
3571
|
+
* the client did ask for is rejected when it exceeds `maxLimit`, since silently truncating a
|
|
3572
|
+
* page tells a paginating client it has reached the end when it has not; `clampToMax` opts
|
|
3573
|
+
* into the truncation instead. A limit the *policy* supplied is capped rather than rejected —
|
|
3574
|
+
* a misconfigured `defaultLimit` above `maxLimit` is the operator's problem, not something to
|
|
3575
|
+
* fail a client's query over.
|
|
3576
|
+
*/
|
|
3577
|
+
const applyLimitPolicy = (limit, policy, errorCtx) => {
|
|
3578
|
+
assertPagingNotNegative({ limit }, errorCtx);
|
|
3579
|
+
if (!policy) return limit ?? void 0;
|
|
3580
|
+
const { defaultLimit, maxLimit, clampToMax } = policy;
|
|
3581
|
+
if (limit == null) {
|
|
3582
|
+
const fallback = defaultLimit ?? maxLimit;
|
|
3583
|
+
if (fallback === void 0) return;
|
|
3584
|
+
return maxLimit === void 0 ? fallback : Math.min(fallback, maxLimit);
|
|
3585
|
+
}
|
|
3586
|
+
if (maxLimit !== void 0 && limit > maxLimit) {
|
|
3587
|
+
if (clampToMax) return maxLimit;
|
|
3588
|
+
throw drizzleError(`'limit' of ${limit} exceeds the maximum of ${maxLimit}.`, {
|
|
3589
|
+
code: "DRIZZLE_LIMIT_EXCEEDED",
|
|
3590
|
+
...errorCtx
|
|
3591
|
+
});
|
|
3592
|
+
}
|
|
3593
|
+
return limit;
|
|
3594
|
+
};
|
|
3595
|
+
/** Merges a per-table override over the global policy, or `undefined` when neither bounds anything. */
|
|
3596
|
+
const resolveLimitPolicy = (global, table) => {
|
|
3597
|
+
const defaultLimit = table?.defaultLimit ?? global?.defaultLimit;
|
|
3598
|
+
const maxLimit = table?.maxLimit ?? global?.maxLimit;
|
|
3599
|
+
if (defaultLimit === void 0 && maxLimit === void 0) return;
|
|
3600
|
+
return {
|
|
3601
|
+
defaultLimit,
|
|
3602
|
+
maxLimit,
|
|
3603
|
+
clampToMax: table?.clampToMax ?? global?.clampToMax ?? false
|
|
3604
|
+
};
|
|
3605
|
+
};
|
|
3606
|
+
/**
|
|
3607
|
+
* A paginated field costs its page size times whatever one row of it costs, so `users(limit: 100)
|
|
3608
|
+
* { posts(limit: 10) { id } }` is charged for the thousand rows it can return rather than the two
|
|
3609
|
+
* fields it mentions. `childComplexity` floors at 1 so a row is never free.
|
|
3610
|
+
*
|
|
3611
|
+
* The page size is the *effective* one: a request without `limit` is priced at the policy's
|
|
3612
|
+
* `defaultLimit` (or its `maxLimit` when that is all it sets) rather than the estimator's guess, and no request is priced above `maxLimit`,
|
|
3613
|
+
* so the cost a query is charged matches the rows it can actually pull.
|
|
3614
|
+
*/
|
|
3615
|
+
const listFieldComplexity = (options, policy) => ({ args, childComplexity }) => {
|
|
3616
|
+
const limit = args["limit"];
|
|
3617
|
+
let rows = (typeof limit === "number" && limit > 0 ? limit : void 0) ?? policy?.defaultLimit ?? policy?.maxLimit ?? options.defaultListSize;
|
|
3618
|
+
if (policy?.maxLimit !== void 0) rows = Math.min(rows, policy.maxLimit);
|
|
3619
|
+
return rows * Math.max(childComplexity, 1);
|
|
3620
|
+
};
|
|
3621
|
+
/**
|
|
3622
|
+
* An aggregate returns a single row but reads however many match, so its cost tracks the scan
|
|
3623
|
+
* rather than the response.
|
|
3624
|
+
*/
|
|
3625
|
+
const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
|
|
3626
|
+
//#endregion
|
|
3333
3627
|
//#region src/util/builders/common/distinct.ts
|
|
3334
3628
|
/**
|
|
3335
3629
|
* Alias the parent key comes back under when the pass runs through a junction table. See
|
|
@@ -3377,13 +3671,15 @@ const selectDistinctKeys = async (params) => {
|
|
|
3377
3671
|
...junction ? { [DISTINCT_PARENT]: sub[DISTINCT_PARENT] } : {}
|
|
3378
3672
|
}).from(sub).where((0, drizzle_orm.eq)(sub[DISTINCT_RN], 1)).orderBy(...outerOrder);
|
|
3379
3673
|
if (offset) query = query.offset(offset);
|
|
3380
|
-
|
|
3674
|
+
const effectiveLimit = offsetOnlyLimit(table, limit, offset);
|
|
3675
|
+
if (effectiveLimit != null) query = query.limit(effectiveLimit < 0 ? drizzle_orm.sql`${effectiveLimit}` : effectiveLimit);
|
|
3381
3676
|
return await query;
|
|
3382
3677
|
};
|
|
3383
3678
|
/**
|
|
3384
3679
|
* Condition matching exactly the rows identified by `keys` — an `IN (…)` for a single-column
|
|
3385
|
-
* primary key, an `OR` of per-row equality for a composite one
|
|
3386
|
-
*
|
|
3680
|
+
* primary key, an `OR` of per-row equality for a composite one — nested as a balanced tree, so
|
|
3681
|
+
* thousands of keys stay within SQLite's expression-depth limit. `table` may be the aliased RQB
|
|
3682
|
+
* proxy.
|
|
3387
3683
|
*/
|
|
3388
3684
|
const primaryKeyRestriction = (table, pkNames, keys) => {
|
|
3389
3685
|
const cols = (0, drizzle_orm.getColumns)(table);
|
|
@@ -3391,7 +3687,7 @@ const primaryKeyRestriction = (table, pkNames, keys) => {
|
|
|
3391
3687
|
const name = pkNames[0];
|
|
3392
3688
|
return (0, drizzle_orm.inArray)(cols[name], keys.map((key) => key[name]));
|
|
3393
3689
|
}
|
|
3394
|
-
return (
|
|
3690
|
+
return balancedOr(keys.map((key) => (0, drizzle_orm.and)(...pkNames.map((name) => (0, drizzle_orm.eq)(cols[name], key[name])))));
|
|
3395
3691
|
};
|
|
3396
3692
|
//#endregion
|
|
3397
3693
|
//#region src/util/builders/common/docs.ts
|
|
@@ -3537,99 +3833,6 @@ const innerOrderType = (typeName) => sharedType(typeName, {
|
|
|
3537
3833
|
}
|
|
3538
3834
|
}));
|
|
3539
3835
|
//#endregion
|
|
3540
|
-
//#region src/util/builders/common/limits.ts
|
|
3541
|
-
/**
|
|
3542
|
-
* Substitutes a table's default ordering for an absent `orderBy`.
|
|
3543
|
-
*
|
|
3544
|
-
* Applied where the arguments are first read, so everything downstream — the cursor tuple, a
|
|
3545
|
-
* `distinct` pass, the plain-select fallback — sees one effective ordering and cannot
|
|
3546
|
-
* disagree about it. Only a missing argument is replaced: `orderBy: {}` is a request for no
|
|
3547
|
-
* ordering, and stays one.
|
|
3548
|
-
*/
|
|
3549
|
-
const withDefaultOrderBy = (args, tableName, defaults) => {
|
|
3550
|
-
if (!defaults || args?.orderBy != null) return args;
|
|
3551
|
-
const fallback = defaults(tableName);
|
|
3552
|
-
return fallback ? {
|
|
3553
|
-
...args,
|
|
3554
|
-
orderBy: fallback
|
|
3555
|
-
} : args;
|
|
3556
|
-
};
|
|
3557
|
-
/**
|
|
3558
|
-
* Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
|
|
3559
|
-
* dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
|
|
3560
|
-
* straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
|
|
3561
|
-
* whether or not the table has a limit policy.
|
|
3562
|
-
*/
|
|
3563
|
-
const assertPagingNotNegative = (paging, errorCtx) => {
|
|
3564
|
-
for (const name of ["limit", "offset"]) {
|
|
3565
|
-
const value = paging[name];
|
|
3566
|
-
if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
|
|
3567
|
-
code: "DRIZZLE_INVALID_INPUT_VALUE",
|
|
3568
|
-
...errorCtx
|
|
3569
|
-
});
|
|
3570
|
-
}
|
|
3571
|
-
};
|
|
3572
|
-
/**
|
|
3573
|
-
* The `limit` a query actually runs with, given the policy for the table it reads.
|
|
3574
|
-
*
|
|
3575
|
-
* A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
|
|
3576
|
-
* that is configured — an absent limit means every row, which is above any maximum. A limit
|
|
3577
|
-
* the client did ask for is rejected when it exceeds `maxLimit`, since silently truncating a
|
|
3578
|
-
* page tells a paginating client it has reached the end when it has not; `clampToMax` opts
|
|
3579
|
-
* into the truncation instead. A limit the *policy* supplied is capped rather than rejected —
|
|
3580
|
-
* a misconfigured `defaultLimit` above `maxLimit` is the operator's problem, not something to
|
|
3581
|
-
* fail a client's query over.
|
|
3582
|
-
*/
|
|
3583
|
-
const applyLimitPolicy = (limit, policy, errorCtx) => {
|
|
3584
|
-
assertPagingNotNegative({ limit }, errorCtx);
|
|
3585
|
-
if (!policy) return limit ?? void 0;
|
|
3586
|
-
const { defaultLimit, maxLimit, clampToMax } = policy;
|
|
3587
|
-
if (limit == null) {
|
|
3588
|
-
const fallback = defaultLimit ?? maxLimit;
|
|
3589
|
-
if (fallback === void 0) return;
|
|
3590
|
-
return maxLimit === void 0 ? fallback : Math.min(fallback, maxLimit);
|
|
3591
|
-
}
|
|
3592
|
-
if (maxLimit !== void 0 && limit > maxLimit) {
|
|
3593
|
-
if (clampToMax) return maxLimit;
|
|
3594
|
-
throw drizzleError(`'limit' of ${limit} exceeds the maximum of ${maxLimit}.`, {
|
|
3595
|
-
code: "DRIZZLE_LIMIT_EXCEEDED",
|
|
3596
|
-
...errorCtx
|
|
3597
|
-
});
|
|
3598
|
-
}
|
|
3599
|
-
return limit;
|
|
3600
|
-
};
|
|
3601
|
-
/** Merges a per-table override over the global policy, or `undefined` when neither bounds anything. */
|
|
3602
|
-
const resolveLimitPolicy = (global, table) => {
|
|
3603
|
-
const defaultLimit = table?.defaultLimit ?? global?.defaultLimit;
|
|
3604
|
-
const maxLimit = table?.maxLimit ?? global?.maxLimit;
|
|
3605
|
-
if (defaultLimit === void 0 && maxLimit === void 0) return;
|
|
3606
|
-
return {
|
|
3607
|
-
defaultLimit,
|
|
3608
|
-
maxLimit,
|
|
3609
|
-
clampToMax: table?.clampToMax ?? global?.clampToMax ?? false
|
|
3610
|
-
};
|
|
3611
|
-
};
|
|
3612
|
-
/**
|
|
3613
|
-
* A paginated field costs its page size times whatever one row of it costs, so `users(limit: 100)
|
|
3614
|
-
* { posts(limit: 10) { id } }` is charged for the thousand rows it can return rather than the two
|
|
3615
|
-
* fields it mentions. `childComplexity` floors at 1 so a row is never free.
|
|
3616
|
-
*
|
|
3617
|
-
* The page size is the *effective* one: a request without `limit` is priced at the policy's
|
|
3618
|
-
* `defaultLimit` rather than the estimator's guess, and no request is priced above `maxLimit`,
|
|
3619
|
-
* so the cost a query is charged matches the rows it can actually pull.
|
|
3620
|
-
*/
|
|
3621
|
-
const listFieldComplexity = (options, policy) => ({ args, childComplexity }) => {
|
|
3622
|
-
const limit = args["limit"];
|
|
3623
|
-
let rows = (typeof limit === "number" && limit > 0 ? limit : void 0) ?? policy?.defaultLimit ?? options.defaultListSize;
|
|
3624
|
-
if (policy?.maxLimit !== void 0) rows = Math.min(rows, policy.maxLimit);
|
|
3625
|
-
return rows * Math.max(childComplexity, 1);
|
|
3626
|
-
};
|
|
3627
|
-
/**
|
|
3628
|
-
* An aggregate returns a single row but reads however many match, so its cost tracks the scan
|
|
3629
|
-
* rather than the response.
|
|
3630
|
-
*/
|
|
3631
|
-
const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
|
|
3632
|
-
//#endregion
|
|
3633
3836
|
//#region src/util/parse-resolve-info.ts
|
|
3634
3837
|
/** Unwraps `[T]!`-style wrappers down to the named type. `getNamedType` without the realm tie. */
|
|
3635
3838
|
const namedTypeOf = (type) => {
|
|
@@ -3793,14 +3996,10 @@ const NUMERIC_SCALARS = /* @__PURE__ */ new Set([
|
|
|
3793
3996
|
]);
|
|
3794
3997
|
/**
|
|
3795
3998
|
* Column types that convert to a GraphQL list without being a list *in the database* —
|
|
3796
|
-
* a bytea reads as `[Int]`, a vector
|
|
3797
|
-
* them is not the operation `push` means.
|
|
3999
|
+
* a bytea or SQLite buffer blob reads as `[Int]`, a vector, a point or a line as `[Float]` —
|
|
4000
|
+
* so appending to them is not the operation `push` means.
|
|
3798
4001
|
*/
|
|
3799
|
-
const
|
|
3800
|
-
"PgVector",
|
|
3801
|
-
"PgGeometry",
|
|
3802
|
-
"PgBytea"
|
|
3803
|
-
]);
|
|
4002
|
+
const isNonListArrayColumn = (column) => isFloatTupleColumn(column) || isBufferColumn(column);
|
|
3804
4003
|
const kindCache = /* @__PURE__ */ new WeakMap();
|
|
3805
4004
|
/**
|
|
3806
4005
|
* Whether a column takes an operations input, and which one.
|
|
@@ -3817,7 +4016,7 @@ const fieldUpdateKind = (column, columnName, tableName) => {
|
|
|
3817
4016
|
if (!getColumnScalarOverride(column, true)) {
|
|
3818
4017
|
const { type } = drizzleColumnToGraphQLType(column, columnName, tableName, true, false, true);
|
|
3819
4018
|
if (NUMERIC_SCALARS.has(type)) kind = "numeric";
|
|
3820
|
-
else if (type instanceof graphql.GraphQLList && !
|
|
4019
|
+
else if (type instanceof graphql.GraphQLList && !isNonListArrayColumn(column)) kind = "list";
|
|
3821
4020
|
}
|
|
3822
4021
|
kindCache.set(column, kind);
|
|
3823
4022
|
return kind;
|
|
@@ -4088,6 +4287,16 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
|
|
|
4088
4287
|
if (!relFieldSelection) continue;
|
|
4089
4288
|
const eagerArgs = field.args;
|
|
4090
4289
|
if (!(0, drizzle_orm.is)(relEntry.relation ?? relEntry, drizzle_orm.One) && (eagerArgs?.after != null || !!(eagerArgs?.distinct)?.length || isCursorFieldSelected(relFieldSelection, tables[targetTableName]))) continue;
|
|
4290
|
+
const junctionName = junctionTableName(tables, relEntry);
|
|
4291
|
+
if (hasJunctionPolicy(scope, junctionName)) {
|
|
4292
|
+
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.`, {
|
|
4293
|
+
code: "DRIZZLE_UNSUPPORTED_RELATION",
|
|
4294
|
+
table: tableName,
|
|
4295
|
+
operation: "relation",
|
|
4296
|
+
relation: relName
|
|
4297
|
+
});
|
|
4298
|
+
continue;
|
|
4299
|
+
}
|
|
4091
4300
|
const columns = extractSelectedColumnsFromTree(relFieldSelection, tables[targetTableName], {
|
|
4092
4301
|
tableName: targetTableName,
|
|
4093
4302
|
relationMap,
|
|
@@ -4113,9 +4322,11 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
|
|
|
4113
4322
|
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;
|
|
4114
4323
|
const hasPagination = offset != null || limit != null;
|
|
4115
4324
|
const pkNames = targetPkNames ?? [];
|
|
4116
|
-
|
|
4325
|
+
const tiebreak = hasPagination && pkNames.length > 0;
|
|
4326
|
+
const requestOrder = relationArgs?.orderBy;
|
|
4327
|
+
thisRecord.orderBy = requestOrder || tiebreak ? (aliasedTable) => [...requestOrder ? extractOrderBy(aliasedTable, requestOrder, relationFilterCtx(filterCtx, targetTableName, scope), relWhere) : [], ...tiebreak ? primaryKeyOrderExprs(aliasedTable, pkNames) : []] : void 0;
|
|
4117
4328
|
thisRecord.offset = offset;
|
|
4118
|
-
thisRecord.limit = limit;
|
|
4329
|
+
thisRecord.limit = offsetOnlyLimit(tables[targetTableName], limit, offset);
|
|
4119
4330
|
thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
|
|
4120
4331
|
args[relName] = omitUndefinedKeys(thisRecord);
|
|
4121
4332
|
}
|
|
@@ -4206,7 +4417,7 @@ const runRelationalSelect = async (opts) => {
|
|
|
4206
4417
|
const preciseProps = Object.keys(cursorPreciseExtras(table, cursorEntries));
|
|
4207
4418
|
if (preciseProps.length) params.extras = Object.fromEntries(preciseProps.map((prop) => [prop, (aliasedTable) => cursorPreciseExtras(aliasedTable, cursorEntries)[prop]]));
|
|
4208
4419
|
}
|
|
4209
|
-
params.limit = distinctKeys ? void 0 : opts.limit;
|
|
4420
|
+
params.limit = distinctKeys ? void 0 : offsetOnlyLimit(table, opts.limit, offset);
|
|
4210
4421
|
const result = await queryBase.findMany(omitUndefinedKeys(params));
|
|
4211
4422
|
if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
|
|
4212
4423
|
return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
|
|
@@ -4263,6 +4474,8 @@ const eagerLoadMutationRelations = async (db, tableName, rows, pkNames, withPara
|
|
|
4263
4474
|
}
|
|
4264
4475
|
return rows;
|
|
4265
4476
|
};
|
|
4477
|
+
/** The longest delay `setTimeout` honours; anything above, Infinity included, fires after 1 ms. */
|
|
4478
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
4266
4479
|
const createMutationTxCtx = (options) => options ? {
|
|
4267
4480
|
timeoutMs: options.timeoutMs,
|
|
4268
4481
|
fieldNames: /* @__PURE__ */ new Set(),
|
|
@@ -4289,28 +4502,28 @@ const includedByDirectives = (node, variables) => {
|
|
|
4289
4502
|
* this build did not generate — its completion cannot be tracked, so the caller must not
|
|
4290
4503
|
* open a transaction it might never be able to close.
|
|
4291
4504
|
*/
|
|
4292
|
-
const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments) => {
|
|
4505
|
+
const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce) => {
|
|
4293
4506
|
for (const selection of selectionSet.selections) {
|
|
4294
4507
|
if (!includedByDirectives(selection, variables)) continue;
|
|
4295
4508
|
if (selection.kind === "Field") {
|
|
4296
4509
|
const name = selection.name.value;
|
|
4297
4510
|
if (name === "__typename") continue;
|
|
4298
4511
|
if (!known.has(name)) return false;
|
|
4299
|
-
keys.add(selection.alias?.value ?? name);
|
|
4512
|
+
if (argumentsCoerce(selection)) keys.add(selection.alias?.value ?? name);
|
|
4300
4513
|
} else if (selection.kind === "InlineFragment") {
|
|
4301
|
-
if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
|
|
4514
|
+
if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
|
|
4302
4515
|
} else if (selection.kind === "FragmentSpread") {
|
|
4303
4516
|
const fragmentName = selection.name.value;
|
|
4304
4517
|
if (visitedFragments.has(fragmentName)) continue;
|
|
4305
4518
|
visitedFragments.add(fragmentName);
|
|
4306
4519
|
const fragment = fragments?.[fragmentName];
|
|
4307
4520
|
if (!fragment) return false;
|
|
4308
|
-
if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
|
|
4521
|
+
if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
|
|
4309
4522
|
}
|
|
4310
4523
|
}
|
|
4311
4524
|
return true;
|
|
4312
4525
|
};
|
|
4313
|
-
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" });
|
|
4526
|
+
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" });
|
|
4314
4527
|
/** Opens the request-wide transaction and wires up latch, inactivity timer and cleanup. */
|
|
4315
4528
|
const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
4316
4529
|
let release;
|
|
@@ -4345,6 +4558,7 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
|
4345
4558
|
completed: 0,
|
|
4346
4559
|
failure: void 0,
|
|
4347
4560
|
aborted: false,
|
|
4561
|
+
timedOut: false,
|
|
4348
4562
|
done: false,
|
|
4349
4563
|
ready,
|
|
4350
4564
|
settled,
|
|
@@ -4359,8 +4573,9 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
|
4359
4573
|
suspendTimer: clearTimer,
|
|
4360
4574
|
bumpTimer: () => {
|
|
4361
4575
|
clearTimer();
|
|
4362
|
-
if (state.done) return;
|
|
4576
|
+
if (state.done || txCtx.timeoutMs === Number.POSITIVE_INFINITY) return;
|
|
4363
4577
|
timer = setTimeout(() => {
|
|
4578
|
+
if (!state.aborted) state.timedOut = true;
|
|
4364
4579
|
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" }));
|
|
4365
4580
|
}, txCtx.timeoutMs);
|
|
4366
4581
|
timer.unref?.();
|
|
@@ -4379,15 +4594,15 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
|
4379
4594
|
state.bumpTimer();
|
|
4380
4595
|
return state;
|
|
4381
4596
|
};
|
|
4382
|
-
const runOnSharedTx = async (state, body) => {
|
|
4597
|
+
const runOnSharedTx = async (state, timeoutMs, body) => {
|
|
4383
4598
|
if (state.aborted) {
|
|
4384
4599
|
await state.settled.catch(() => {});
|
|
4385
|
-
throw abortedBatchError();
|
|
4600
|
+
throw abortedBatchError(state, timeoutMs);
|
|
4386
4601
|
}
|
|
4387
4602
|
state.suspendTimer();
|
|
4388
4603
|
const tx = await state.ready;
|
|
4389
4604
|
try {
|
|
4390
|
-
if (state.aborted) throw abortedBatchError();
|
|
4605
|
+
if (state.aborted) throw abortedBatchError(state, timeoutMs);
|
|
4391
4606
|
const result = await body(tx);
|
|
4392
4607
|
state.completed += 1;
|
|
4393
4608
|
if (state.completed >= state.expected) {
|
|
@@ -4420,14 +4635,24 @@ const runMutation = async (db, context, info, txCtx, body, forceTx) => {
|
|
|
4420
4635
|
if (contextIsObject && context[drizzleExecutorKey]) return body(context[drizzleExecutorKey]);
|
|
4421
4636
|
if (!txCtx || info.operation.operation !== "mutation") return forceTx ? db.transaction((tx) => body(tx)) : body(db);
|
|
4422
4637
|
const keys = /* @__PURE__ */ new Set();
|
|
4423
|
-
|
|
4638
|
+
const mutationFields = info.schema.getMutationType()?.getFields() ?? {};
|
|
4639
|
+
state = !collectRootMutationKeys(info.operation.selectionSet, info.fragments, info.variableValues, txCtx.fieldNames, keys, /* @__PURE__ */ new Set(), (node) => {
|
|
4640
|
+
const fieldDef = mutationFields[node.name.value];
|
|
4641
|
+
if (!fieldDef) return false;
|
|
4642
|
+
try {
|
|
4643
|
+
(0, graphql.getArgumentValues)(fieldDef, node, info.variableValues);
|
|
4644
|
+
return true;
|
|
4645
|
+
} catch {
|
|
4646
|
+
return false;
|
|
4647
|
+
}
|
|
4648
|
+
}) || keys.size <= 1 ? { kind: "passthrough" } : openSharedTx(db, context, txCtx, stateKey, keys.size);
|
|
4424
4649
|
txCtx.states.set(stateKey, state);
|
|
4425
4650
|
}
|
|
4426
4651
|
if (state.kind === "passthrough") {
|
|
4427
4652
|
const executor = resolveExecutor(db, context);
|
|
4428
4653
|
return forceTx && executor === db ? db.transaction((tx) => body(tx)) : body(executor);
|
|
4429
4654
|
}
|
|
4430
|
-
return runOnSharedTx(state, body);
|
|
4655
|
+
return runOnSharedTx(state, txCtx.timeoutMs, body);
|
|
4431
4656
|
};
|
|
4432
4657
|
//#endregion
|
|
4433
4658
|
//#region src/util/builders/common/write-hooks.ts
|
|
@@ -4581,10 +4806,11 @@ const hardDeleteArg = {
|
|
|
4581
4806
|
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."
|
|
4582
4807
|
};
|
|
4583
4808
|
const prepareMutationRelationColumns = (params) => {
|
|
4584
|
-
const { relationMap, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
|
|
4809
|
+
const { relationMap, filterCtx, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
|
|
4585
4810
|
const typeName = resolveObjectTypeName(tableName, typeNameMapper, resolveName);
|
|
4586
4811
|
const withParams = relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, {
|
|
4587
4812
|
typeNameMapper,
|
|
4813
|
+
filterCtx,
|
|
4588
4814
|
limits: params.limits,
|
|
4589
4815
|
scope: params.scope,
|
|
4590
4816
|
defaultOrderBy: params.defaultOrderBy,
|
|
@@ -4594,7 +4820,8 @@ const prepareMutationRelationColumns = (params) => {
|
|
|
4594
4820
|
const baseColumns = extractSelectedColumnsFromTreeSQLFormat(parsedInfo.fieldsByTypeName[typeName], table, {
|
|
4595
4821
|
tableName,
|
|
4596
4822
|
relationMap,
|
|
4597
|
-
tables
|
|
4823
|
+
tables,
|
|
4824
|
+
allRelations: filterCtx?.relationMap
|
|
4598
4825
|
});
|
|
4599
4826
|
return {
|
|
4600
4827
|
columns: hasRelations ? withPrimaryKeyColumns(baseColumns, table, pkNames) : baseColumns,
|
|
@@ -4730,7 +4957,7 @@ const assertSingleMatch = async (executor, table, filters) => {
|
|
|
4730
4957
|
* MySQL (no `RETURNING`; its hooks see `rows: []` as on every MySQL mutation), skips the
|
|
4731
4958
|
* `RETURNING` and reads the count off the driver's result via {@link rowsAffected}.
|
|
4732
4959
|
*/
|
|
4733
|
-
const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested, policies, pkNames = [], returnsRows }) => {
|
|
4960
|
+
const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested, policies, pkNames = [], returnsRows, writeFilter }) => {
|
|
4734
4961
|
const softDelete = kind === "delete" ? policies?.softDelete?.(tableName) : void 0;
|
|
4735
4962
|
const canHardDelete = softDelete?.hardDelete === true;
|
|
4736
4963
|
const queryArgs = {
|
|
@@ -4773,7 +5000,7 @@ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, f
|
|
|
4773
5000
|
required: requireWhere,
|
|
4774
5001
|
deleted: hard ? "INCLUDE" : void 0
|
|
4775
5002
|
});
|
|
4776
|
-
if (filters) query = query.where(filters);
|
|
5003
|
+
if (filters) query = query.where(writeFilter ? writeFilter(filters) : filters);
|
|
4777
5004
|
if (!readsRows) {
|
|
4778
5005
|
const result = await query;
|
|
4779
5006
|
await after();
|
|
@@ -4833,27 +5060,40 @@ const getOrCreateLoader = (context, key, batchFn) => {
|
|
|
4833
5060
|
if (!loaders.has(key)) loaders.set(key, new BatchLoader(batchFn));
|
|
4834
5061
|
return loaders.get(key);
|
|
4835
5062
|
};
|
|
5063
|
+
const requestCacheNode = () => ({
|
|
5064
|
+
children: /* @__PURE__ */ new WeakMap(),
|
|
5065
|
+
entries: /* @__PURE__ */ new Map()
|
|
5066
|
+
});
|
|
4836
5067
|
/**
|
|
4837
5068
|
* Memoizes a value that a resolver derives from its own field — its args and its selection —
|
|
4838
5069
|
* for the length of one request. Resolvers run once per parent row, so anything derived only
|
|
4839
5070
|
* from the field is otherwise recomputed for every row of a batch.
|
|
4840
5071
|
*
|
|
4841
|
-
* The
|
|
4842
|
-
*
|
|
4843
|
-
*
|
|
4844
|
-
*
|
|
5072
|
+
* The value is keyed by every AST node of the field — `info.fieldNodes`, the nodes graphql-js
|
|
5073
|
+
* merged into this one response field — not only the first. A fragment's field node is the first
|
|
5074
|
+
* node wherever the fragment is spread, but the merged selection differs when a sibling selection
|
|
5075
|
+
* at one spread site adds to it, so keying on one node would serve one site the other's selection.
|
|
5076
|
+
* The nodes are matched by identity, in order, through a chain of weak maps, so a document cached
|
|
5077
|
+
* across requests never carries values between them (the root lives on the context), and an array
|
|
5078
|
+
* graphql rebuilds per call still hits. `key` separates fields that share nodes: one fragment
|
|
5079
|
+
* selection can resolve for more than one parent type. Without a context object, or without
|
|
5080
|
+
* nodes, there is nowhere request-scoped to cache, so the value is simply computed.
|
|
4845
5081
|
*
|
|
4846
5082
|
* `compute` must not read the parent row.
|
|
4847
5083
|
*/
|
|
4848
|
-
const getOrCreateRequestValue = (context,
|
|
4849
|
-
if (!context || typeof context !== "object" || !node || typeof node !== "object") return compute();
|
|
4850
|
-
if (!context[DRIZZLE_REQUEST_CACHE_KEY]) context[DRIZZLE_REQUEST_CACHE_KEY] =
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
5084
|
+
const getOrCreateRequestValue = (context, nodes, key, compute) => {
|
|
5085
|
+
if (!context || typeof context !== "object" || !Array.isArray(nodes) || !nodes.length || nodes.some((node) => !node || typeof node !== "object")) return compute();
|
|
5086
|
+
if (!context[DRIZZLE_REQUEST_CACHE_KEY]) context[DRIZZLE_REQUEST_CACHE_KEY] = requestCacheNode();
|
|
5087
|
+
let level = context[DRIZZLE_REQUEST_CACHE_KEY];
|
|
5088
|
+
for (const node of nodes) {
|
|
5089
|
+
let next = level.children.get(node);
|
|
5090
|
+
if (!next) {
|
|
5091
|
+
next = requestCacheNode();
|
|
5092
|
+
level.children.set(node, next);
|
|
5093
|
+
}
|
|
5094
|
+
level = next;
|
|
4856
5095
|
}
|
|
5096
|
+
const entries = level.entries;
|
|
4857
5097
|
if (entries.has(key)) return entries.get(key);
|
|
4858
5098
|
const value = compute();
|
|
4859
5099
|
entries.set(key, value);
|
|
@@ -5088,6 +5328,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
5088
5328
|
const junction = extractJunctionColumns(relEntry);
|
|
5089
5329
|
const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
|
|
5090
5330
|
const groupKeys = junction ? [PARENT_KEY] : joinPairs.map((pair) => pair.foreignColPropName);
|
|
5331
|
+
const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
|
|
5091
5332
|
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
|
|
5092
5333
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, isOne && relEntry.relation?.optional === false);
|
|
5093
5334
|
const limitPolicy = isOne ? void 0 : limits?.(targetTableName);
|
|
@@ -5132,7 +5373,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
5132
5373
|
const localValues = parentJoinValues(parent, joinPairs);
|
|
5133
5374
|
if (localValues.some((value) => value == null)) return isOne ? null : [];
|
|
5134
5375
|
const { where: whereArg, limit: requestedLimit, offset, after, deleted } = args ?? {};
|
|
5135
|
-
const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes
|
|
5376
|
+
const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes, `relation:${tableName}::${relationName}`, () => {
|
|
5136
5377
|
const orderByArg = isOne ? args?.orderBy : withDefaultOrderBy(args ?? {}, targetTableName, policies?.defaultOrderBy).orderBy;
|
|
5137
5378
|
const limit = applyLimitPolicy(requestedLimit, limitPolicy, errorCtx);
|
|
5138
5379
|
assertPagingNotNegative({ offset }, errorCtx);
|
|
@@ -5176,7 +5417,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
5176
5417
|
const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key, localCols), key])).values()];
|
|
5177
5418
|
const scope = resolveScope(policies, context, filterCtx);
|
|
5178
5419
|
const loadChunk = async (chunkKeys) => {
|
|
5179
|
-
let whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.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);
|
|
5420
|
+
let whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.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);
|
|
5180
5421
|
if (distinct) {
|
|
5181
5422
|
const keys = await selectDistinctKeys({
|
|
5182
5423
|
db: executor,
|
|
@@ -5194,7 +5435,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
5194
5435
|
} : void 0
|
|
5195
5436
|
});
|
|
5196
5437
|
if (!keys.length) return /* @__PURE__ */ new Map();
|
|
5197
|
-
whereCondition = (0, drizzle_orm.and)(whereCondition, junction ? (
|
|
5438
|
+
whereCondition = (0, drizzle_orm.and)(whereCondition, junction ? balancedOr(keys.map((key) => (0, drizzle_orm.and)((0, drizzle_orm.eq)(junction.sourceCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
|
|
5198
5439
|
}
|
|
5199
5440
|
const selection = junction ? {
|
|
5200
5441
|
...columns ?? (0, drizzle_orm.getColumns)(targetTable),
|
|
@@ -5331,7 +5572,7 @@ const generateTableOrderCached = (table, innerOrder) => {
|
|
|
5331
5572
|
try {
|
|
5332
5573
|
const columns = visibleColumns(table);
|
|
5333
5574
|
const columnEntries = Object.entries(columns);
|
|
5334
|
-
remapped = Object.fromEntries(columnEntries.
|
|
5575
|
+
remapped = Object.fromEntries(columnEntries.filter(([, column]) => isOrderableColumn(column)).map(([columnName]) => [columnName, { type: innerOrder }]));
|
|
5335
5576
|
if (cacheable) {
|
|
5336
5577
|
let byInnerOrder = orderMap.get(table);
|
|
5337
5578
|
if (!byInnerOrder) {
|
|
@@ -5374,43 +5615,71 @@ const generateTableSelectTypeFieldsCached = (table, tableName, cacheCtx) => {
|
|
|
5374
5615
|
return remapped;
|
|
5375
5616
|
};
|
|
5376
5617
|
/**
|
|
5618
|
+
* The to-one relations a table's OrderBy can order through, as `[relationName, targetTableName]`.
|
|
5619
|
+
* To-many relations are left out: "order a parent by its many children" has no single defined
|
|
5620
|
+
* value to sort on. A relation whose name collides with a column is skipped — the column keeps
|
|
5621
|
+
* the name, even one with no ordering of its own, so `extractOrderBy` never reads it as a relation.
|
|
5622
|
+
*/
|
|
5623
|
+
const orderRelations = (tableName, relationMap, tables) => {
|
|
5624
|
+
const relations = relationMap?.[tableName];
|
|
5625
|
+
const table = tables?.[tableName];
|
|
5626
|
+
if (!relations || !tables || !table) return [];
|
|
5627
|
+
const columns = visibleColumns(table);
|
|
5628
|
+
return Object.entries(relations).filter(([relationName, relEntry]) => {
|
|
5629
|
+
const relation = relEntry.relation ?? relEntry;
|
|
5630
|
+
return !(relationName in columns) && !!tables[relEntry.targetTableName] && (0, drizzle_orm.is)(relation, drizzle_orm.One) && isFilterableRelation(relation);
|
|
5631
|
+
}).map(([relationName, relEntry]) => [relationName, relEntry.targetTableName]);
|
|
5632
|
+
};
|
|
5633
|
+
/**
|
|
5634
|
+
* Whether a table's OrderBy would have any field: an orderable column of its own, or a to-one
|
|
5635
|
+
* relation to a table that has one. A table of nothing but JSON columns has none, and an input
|
|
5636
|
+
* object with no fields is an invalid schema, so such a table gets no OrderBy and no `orderBy`
|
|
5637
|
+
* argument at all.
|
|
5638
|
+
*/
|
|
5639
|
+
const hasOrderFields = (tableName, relationMap, tables, visiting = /* @__PURE__ */ new Set()) => {
|
|
5640
|
+
const table = tables[tableName];
|
|
5641
|
+
if (table && Object.values(visibleColumns(table)).some((column) => isOrderableColumn(column))) return true;
|
|
5642
|
+
if (visiting.has(tableName)) return false;
|
|
5643
|
+
visiting.add(tableName);
|
|
5644
|
+
return orderRelations(tableName, relationMap, tables).some(([, target]) => hasOrderFields(target, relationMap, tables, visiting));
|
|
5645
|
+
};
|
|
5646
|
+
/**
|
|
5377
5647
|
* Order fields for a table's to-one relations: each takes the target table's own OrderBy
|
|
5378
5648
|
* input, so a list can be sorted by a related row's column (compiled as a correlated
|
|
5379
|
-
* subquery in the ORDER BY — see `extractOrderBy`).
|
|
5380
|
-
*
|
|
5381
|
-
* name collides with a column is skipped — the column keeps the field.
|
|
5649
|
+
* subquery in the ORDER BY — see `extractOrderBy`). A target with no OrderBy of its own is
|
|
5650
|
+
* left out.
|
|
5382
5651
|
*/
|
|
5383
|
-
const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper,
|
|
5384
|
-
const relations = relationMap?.[tableName];
|
|
5385
|
-
if (!relations || !tables) return {};
|
|
5652
|
+
const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper, relationMap, tables) => {
|
|
5386
5653
|
const fields = {};
|
|
5387
|
-
for (const [relationName,
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
if (!targetTable || !(0, drizzle_orm.is)(relation, drizzle_orm.One) || !isFilterableRelation(relation)) continue;
|
|
5392
|
-
fields[relationName] = {
|
|
5393
|
-
type: generateTableOrderTypeCached(targetTable, relEntry.targetTableName, typeNameMapper, cacheCtx, relationMap, tables),
|
|
5654
|
+
for (const [relationName, targetTableName] of orderRelations(tableName, relationMap, tables)) {
|
|
5655
|
+
const type = generateTableOrderTypeCached(tables[targetTableName], targetTableName, typeNameMapper, cacheCtx, relationMap, tables);
|
|
5656
|
+
if (type) fields[relationName] = {
|
|
5657
|
+
type,
|
|
5394
5658
|
description: `Order by columns of the related ${relationName} row`
|
|
5395
5659
|
};
|
|
5396
5660
|
}
|
|
5397
5661
|
return fields;
|
|
5398
5662
|
};
|
|
5663
|
+
/**
|
|
5664
|
+
* The table's `<Type>OrderBy` input, or `undefined` when it would have no fields (see
|
|
5665
|
+
* {@link hasOrderFields}); the caller then leaves the `orderBy` argument out.
|
|
5666
|
+
*/
|
|
5399
5667
|
const generateTableOrderTypeCached = (table, tableName, typeNameMapper, cacheCtx, relationMap, tables) => {
|
|
5400
5668
|
if (cacheCtx.orderTypeCache.has(table)) return cacheCtx.orderTypeCache.get(table);
|
|
5669
|
+
if (!hasOrderFields(tableName, relationMap, {
|
|
5670
|
+
...tables,
|
|
5671
|
+
[tableName]: table
|
|
5672
|
+
})) return;
|
|
5401
5673
|
const order = new graphql.GraphQLInputObjectType({
|
|
5402
5674
|
name: cacheCtx.typeName({
|
|
5403
5675
|
kind: "orderBy",
|
|
5404
5676
|
defaultName: `${resolveTypeName(tableName, typeNameMapper)}OrderBy`,
|
|
5405
5677
|
table: tableName
|
|
5406
5678
|
}),
|
|
5407
|
-
fields: () => {
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, orderColumns, relationMap, tables)
|
|
5412
|
-
};
|
|
5413
|
-
}
|
|
5679
|
+
fields: () => ({
|
|
5680
|
+
...generateTableOrderCached(table, innerOrderType(cacheCtx.typeName)),
|
|
5681
|
+
...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, relationMap, tables)
|
|
5682
|
+
})
|
|
5414
5683
|
});
|
|
5415
5684
|
cacheCtx.orderTypeCache.set(table, order);
|
|
5416
5685
|
return order;
|
|
@@ -5629,7 +5898,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
|
|
|
5629
5898
|
type: new graphql.GraphQLNonNull(new graphql.GraphQLList(new graphql.GraphQLNonNull(relType))),
|
|
5630
5899
|
args: {
|
|
5631
5900
|
where: { type: relSelectData.filters },
|
|
5632
|
-
orderBy: { type: relSelectData.order },
|
|
5901
|
+
...relSelectData.order ? { orderBy: { type: relSelectData.order } } : {},
|
|
5633
5902
|
offset: { type: graphql.GraphQLInt },
|
|
5634
5903
|
limit: { type: graphql.GraphQLInt },
|
|
5635
5904
|
...deletedArg(cacheCtx.softDeleteOf, targetTableName, cacheCtx.typeName),
|
|
@@ -5857,6 +6126,7 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5857
6126
|
};
|
|
5858
6127
|
const transactionsOpt = config?.transactions;
|
|
5859
6128
|
const transactions = transactionsOpt === void 0 || transactionsOpt === "none" ? void 0 : { timeoutMs: (transactionsOpt === "auto" ? void 0 : transactionsOpt.timeoutMs) ?? 3e4 };
|
|
6129
|
+
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}.`);
|
|
5860
6130
|
const limitsConfig = config?.limits;
|
|
5861
6131
|
if (limitsConfig) {
|
|
5862
6132
|
const check = (policy, where) => {
|
|
@@ -5889,6 +6159,7 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5889
6159
|
for (const [columnName, entry] of Object.entries(orderBy)) {
|
|
5890
6160
|
if (!columns[columnName]) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which is not a column of '${tableName}'.`);
|
|
5891
6161
|
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.`);
|
|
6162
|
+
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.`);
|
|
5892
6163
|
const direction = typeof entry === "string" ? entry : entry?.direction;
|
|
5893
6164
|
if (direction !== "asc" && direction !== "desc") throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy.${columnName} must be 'asc', 'desc', or { direction, priority? }.`);
|
|
5894
6165
|
const priority = typeof entry === "string" ? void 0 : entry?.priority;
|
|
@@ -5947,7 +6218,7 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5947
6218
|
}
|
|
5948
6219
|
}
|
|
5949
6220
|
}
|
|
5950
|
-
const onWrite = writeHooks.
|
|
6221
|
+
const onWrite = [...writeHooks.values()].some(Boolean) ? (tableName) => writeHooks.get(tableName) : void 0;
|
|
5951
6222
|
const policies = scopeConfig || contextValuesConfig || softDeleteInfos.size || onWrite || defaultOrderBy ? {
|
|
5952
6223
|
scope: scopeConfig ? (tableName) => scopeConfig[tableName] : void 0,
|
|
5953
6224
|
contextValues: contextValuesConfig ? (tableName) => contextValuesConfig[tableName] : void 0,
|
|
@@ -6038,8 +6309,14 @@ const AGGREGATE_OPS = [
|
|
|
6038
6309
|
];
|
|
6039
6310
|
/** Ops whose result is a row count: never null, and returned as `Int!` rather than the column's type. */
|
|
6040
6311
|
const COUNT_OPS = /* @__PURE__ */ new Set(["countNonNull", "countDistinct"]);
|
|
6312
|
+
/**
|
|
6313
|
+
* `avg`, except on MySQL, where AVG over an integer column is a DECIMAL rounded to
|
|
6314
|
+
* `div_precision_increment` (4 by default) digits. Averaging the value as a DOUBLE keeps the
|
|
6315
|
+
* fraction the `Float` output promises, as the other two dialects do.
|
|
6316
|
+
*/
|
|
6317
|
+
const avgOf = (col) => columnDialect(col) === "mysql" ? drizzle_orm.sql`avg(cast(${col} as double))`.mapWith(String) : (0, drizzle_orm.avg)(col);
|
|
6041
6318
|
const OP_FNS = {
|
|
6042
|
-
avg:
|
|
6319
|
+
avg: avgOf,
|
|
6043
6320
|
sum: drizzle_orm.sum,
|
|
6044
6321
|
min: drizzle_orm.min,
|
|
6045
6322
|
max: drizzle_orm.max,
|
|
@@ -6049,6 +6326,18 @@ const OP_FNS = {
|
|
|
6049
6326
|
/** Separator for flat select aliases (`avg__price`) — reassembled into nested output by the resolver. */
|
|
6050
6327
|
const SEP = "__";
|
|
6051
6328
|
/**
|
|
6329
|
+
* Column types PostgreSQL can test for equality but has no `min`/`max` aggregate for, plus the
|
|
6330
|
+
* SQLite bigint blob, whose bytes compare as text (`max(9, 10)` is 9). They still count
|
|
6331
|
+
* distinct values and group.
|
|
6332
|
+
*/
|
|
6333
|
+
const NO_MIN_MAX_COLUMN_TYPES = /* @__PURE__ */ new Set([
|
|
6334
|
+
"PgUUID",
|
|
6335
|
+
"PgMacaddr",
|
|
6336
|
+
"PgMacaddr8",
|
|
6337
|
+
"PgBinaryVector",
|
|
6338
|
+
"SQLiteBigInt"
|
|
6339
|
+
]);
|
|
6340
|
+
/**
|
|
6052
6341
|
* Classifies a table's columns for aggregation. avg/sum only make sense on numeric scalars;
|
|
6053
6342
|
* min/max work on any orderable scalar (numbers, strings, bigints, dates, enums). Booleans,
|
|
6054
6343
|
* arrays (including array-typed number columns), JSON, buffers, and object-shaped columns
|
|
@@ -6080,6 +6369,7 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
6080
6369
|
if (cached) return cached;
|
|
6081
6370
|
const numeric = {};
|
|
6082
6371
|
const orderable = {};
|
|
6372
|
+
const comparable = {};
|
|
6083
6373
|
const all = {};
|
|
6084
6374
|
for (const [columnName, column] of Object.entries(visibleColumns(table))) {
|
|
6085
6375
|
all[columnName] = column;
|
|
@@ -6089,7 +6379,11 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
6089
6379
|
const { type: dataType, constraint } = (0, drizzle_orm.extractExtendedColumnType)(column);
|
|
6090
6380
|
if (dataType === "boolean" || dataType === "array" || dataType === "custom") continue;
|
|
6091
6381
|
if (dataType === "object" && constraint !== "date") continue;
|
|
6092
|
-
|
|
6382
|
+
comparable[columnName] = {
|
|
6383
|
+
column,
|
|
6384
|
+
converted
|
|
6385
|
+
};
|
|
6386
|
+
if (!NO_MIN_MAX_COLUMN_TYPES.has(column.columnType)) orderable[columnName] = {
|
|
6093
6387
|
column,
|
|
6094
6388
|
converted
|
|
6095
6389
|
};
|
|
@@ -6098,6 +6392,7 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
6098
6392
|
const sets = {
|
|
6099
6393
|
numeric,
|
|
6100
6394
|
orderable,
|
|
6395
|
+
comparable,
|
|
6101
6396
|
all
|
|
6102
6397
|
};
|
|
6103
6398
|
byName.set(tableName, sets);
|
|
@@ -6110,13 +6405,13 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
6110
6405
|
* and avg/sum of integers overflow Int / produce decimals)
|
|
6111
6406
|
* - `min` / `max` — per orderable column, the column's own (nullable) scalar type
|
|
6112
6407
|
* - `countNonNull` — per column, `Int!`: how many matching rows have a non-null value there
|
|
6113
|
-
* - `countDistinct` — per
|
|
6408
|
+
* - `countDistinct` — per comparable column, `Int!`: how many distinct non-null values there are
|
|
6114
6409
|
* Each wrapper is omitted when no column qualifies for it.
|
|
6115
6410
|
*/
|
|
6116
6411
|
const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
|
|
6117
6412
|
const cached = cacheCtx?.aggregateTypeCache.get(tableName);
|
|
6118
6413
|
if (cached) return cached;
|
|
6119
|
-
const { numeric, orderable, all } = classifyAggregateColumns(table, tableName);
|
|
6414
|
+
const { numeric, orderable, comparable, all } = classifyAggregateColumns(table, tableName);
|
|
6120
6415
|
const nameOf = (op) => cacheCtx?.typeName({
|
|
6121
6416
|
kind: "aggregate",
|
|
6122
6417
|
defaultName: `${typeName}${capitalize(op)}Aggregate`,
|
|
@@ -6137,7 +6432,7 @@ const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
|
|
|
6137
6432
|
}) };
|
|
6138
6433
|
const countSets = {
|
|
6139
6434
|
countNonNull: all,
|
|
6140
|
-
countDistinct:
|
|
6435
|
+
countDistinct: comparable
|
|
6141
6436
|
};
|
|
6142
6437
|
for (const [op, columns] of Object.entries(countSets)) {
|
|
6143
6438
|
const columnNames = Object.keys(columns);
|
|
@@ -6317,6 +6612,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
6317
6612
|
const junction = extractJunctionColumns(relEntry);
|
|
6318
6613
|
if (junction === null) return;
|
|
6319
6614
|
const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
|
|
6615
|
+
const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
|
|
6320
6616
|
const groupAliases = groupCols.map((_, i) => `${GROUP_KEY}${i}`);
|
|
6321
6617
|
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
|
|
6322
6618
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
|
|
@@ -6330,7 +6626,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
6330
6626
|
};
|
|
6331
6627
|
const resolve = async (parent, args, context, info) => {
|
|
6332
6628
|
try {
|
|
6333
|
-
const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes
|
|
6629
|
+
const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes, `aggregate:${tableName}::${relationName}`, () => {
|
|
6334
6630
|
const request = parseAggregateRequest(info, target);
|
|
6335
6631
|
return {
|
|
6336
6632
|
request,
|
|
@@ -6353,7 +6649,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
6353
6649
|
const groupSelection = Object.fromEntries(groupAliases.map((alias, i) => [alias, groupCols[i]]));
|
|
6354
6650
|
const byKey = /* @__PURE__ */ new Map();
|
|
6355
6651
|
for (const chunk of parentKeyChunks(uniqueKeys, groupCols.length)) {
|
|
6356
|
-
const whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)(parentKeyCondition(groupCols, chunk), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
|
|
6652
|
+
const whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.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);
|
|
6357
6653
|
const selected = executor.select({
|
|
6358
6654
|
...groupSelection,
|
|
6359
6655
|
...request.selection
|
|
@@ -6387,12 +6683,12 @@ const HAVING_OPS = {
|
|
|
6387
6683
|
};
|
|
6388
6684
|
/**
|
|
6389
6685
|
* Columns a query can group on. Grouping needs an equality operator, which is the same
|
|
6390
|
-
* requirement
|
|
6686
|
+
* requirement countDistinct has, plus booleans — comparable excludes them because a min/max over
|
|
6391
6687
|
* true/false says nothing, but `GROUP BY isConfirmed` is perfectly ordinary.
|
|
6392
6688
|
*/
|
|
6393
6689
|
const groupableColumns = (table, tableName) => {
|
|
6394
|
-
const {
|
|
6395
|
-
const groupable = { ...
|
|
6690
|
+
const { comparable } = classifyAggregateColumns(table, tableName);
|
|
6691
|
+
const groupable = { ...comparable };
|
|
6396
6692
|
for (const [columnName, column] of Object.entries(visibleColumns(table))) {
|
|
6397
6693
|
if (groupable[columnName]) continue;
|
|
6398
6694
|
const { type: dataType } = (0, drizzle_orm.extractExtendedColumnType)(column);
|
|
@@ -6433,7 +6729,7 @@ const aggregateNumberFilterType = (typeName) => sharedType(typeName, {
|
|
|
6433
6729
|
* nothing to compare against.
|
|
6434
6730
|
*/
|
|
6435
6731
|
const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
|
|
6436
|
-
const { numeric,
|
|
6732
|
+
const { numeric, comparable, all } = classifyAggregateColumns(table, tableName);
|
|
6437
6733
|
const resolveName = cacheCtx?.typeName ?? ((info) => info.defaultName);
|
|
6438
6734
|
const numberFilter = aggregateNumberFilterType(resolveName);
|
|
6439
6735
|
const fields = { count: {
|
|
@@ -6446,7 +6742,7 @@ const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
|
|
|
6446
6742
|
min: numeric,
|
|
6447
6743
|
max: numeric,
|
|
6448
6744
|
countNonNull: all,
|
|
6449
|
-
countDistinct:
|
|
6745
|
+
countDistinct: comparable
|
|
6450
6746
|
};
|
|
6451
6747
|
for (const [op, columns] of Object.entries(opColumns)) {
|
|
6452
6748
|
const columnNames = Object.keys(columns);
|
|
@@ -6511,22 +6807,34 @@ const generateGroupByType = (table, tableName, typeName, cacheCtx) => {
|
|
|
6511
6807
|
}
|
|
6512
6808
|
});
|
|
6513
6809
|
};
|
|
6514
|
-
/**
|
|
6515
|
-
|
|
6810
|
+
/**
|
|
6811
|
+
* Turns one `${typeName}Having` entry into SQL conditions over the matching aggregate expression.
|
|
6812
|
+
*
|
|
6813
|
+
* The operand is a `Float`, but PostgreSQL types an untyped parameter after what it is compared
|
|
6814
|
+
* with: count and sum of integers are bigint, min/max of an int column int, so `gt: 1.5` would
|
|
6815
|
+
* fail to parse. There the aggregate is compared as `numeric`, which holds every one of them
|
|
6816
|
+
* exactly. MySQL and SQLite already compare numbers across types.
|
|
6817
|
+
*/
|
|
6818
|
+
const havingComparisons = (expression, filter, isPg) => {
|
|
6819
|
+
const compared = isPg ? drizzle_orm.sql`cast(${expression} as numeric)` : expression;
|
|
6820
|
+
return Object.entries(filter).filter(([op, value]) => value != null && op in HAVING_OPS).map(([op, value]) => HAVING_OPS[op](compared, value));
|
|
6821
|
+
};
|
|
6516
6822
|
const buildHavingCondition = (having, columns) => {
|
|
6517
6823
|
if (!having) return;
|
|
6518
6824
|
const conditions = [];
|
|
6825
|
+
const firstColumn = Object.values(columns)[0];
|
|
6826
|
+
const isPg = firstColumn ? columnDialect(firstColumn) === "pg" : false;
|
|
6519
6827
|
for (const [key, value] of Object.entries(having)) {
|
|
6520
6828
|
if (value == null) continue;
|
|
6521
6829
|
if (key === "count") {
|
|
6522
|
-
conditions.push(...havingComparisons((0, drizzle_orm.count)(), value));
|
|
6830
|
+
conditions.push(...havingComparisons((0, drizzle_orm.count)(), value, isPg));
|
|
6523
6831
|
continue;
|
|
6524
6832
|
}
|
|
6525
6833
|
if (!AGGREGATE_OPS.includes(key)) continue;
|
|
6526
6834
|
for (const [columnName, filter] of Object.entries(value)) {
|
|
6527
6835
|
const column = columns[columnName];
|
|
6528
6836
|
if (!column || filter == null) continue;
|
|
6529
|
-
conditions.push(...havingComparisons(OP_FNS[key](column), filter));
|
|
6837
|
+
conditions.push(...havingComparisons(OP_FNS[key](column), filter, isPg));
|
|
6530
6838
|
}
|
|
6531
6839
|
}
|
|
6532
6840
|
return conditions.length ? (0, drizzle_orm.and)(...conditions) : void 0;
|
|
@@ -6678,12 +6986,32 @@ const isOptionalOnInsert = (column) => !column.notNull || !!column.hasDefault ||
|
|
|
6678
6986
|
* relation could come back without it. A `where` inherited from the reverse relation filters
|
|
6679
6987
|
* the parent instead, and becomes a per-parent check.
|
|
6680
6988
|
*/
|
|
6681
|
-
const ownWherePlan = (relEntry, parentTable, targetTable, localCol) => {
|
|
6989
|
+
const ownWherePlan = (relEntry, parentTable, targetTable, localCol, parentPrimaryKey) => {
|
|
6682
6990
|
const condition = () => relationOwnCondition(relEntry, parentTable, targetTable, [localCol], [localCol]);
|
|
6683
6991
|
if (!condition()) return {};
|
|
6684
|
-
|
|
6685
|
-
|
|
6992
|
+
const relation = relEntry.relation ?? relEntry;
|
|
6993
|
+
if (!isRelationFilterReversed(relation)) return { ownCondition: () => condition() };
|
|
6994
|
+
const parentColumns = (0, drizzle_orm.getColumns)(parentTable);
|
|
6995
|
+
const parentKeyColumns = Object.fromEntries(parentPrimaryKey.filter((name) => parentColumns[name]).map((name) => [name, parentColumns[name]]));
|
|
6996
|
+
const keyNames = Object.keys(parentKeyColumns);
|
|
6997
|
+
return {
|
|
6998
|
+
parentShows: async (executor, key) => (await executor.select({ key: localCol }).from(parentTable).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(localCol, key), condition())).limit(1)).length > 0,
|
|
6999
|
+
parentHidden: async (executor, rows) => {
|
|
7000
|
+
if (!rows.length) return false;
|
|
7001
|
+
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" });
|
|
7002
|
+
const keyOf = (row) => keyNames.map((name) => keyString(row[name])).join("\0");
|
|
7003
|
+
const match = (0, drizzle_orm.or)(...rows.map((row) => (0, drizzle_orm.and)(...keyNames.map((name) => (0, drizzle_orm.eq)(parentColumns[name], row[name])))));
|
|
7004
|
+
const shown = new Set((await executor.select(parentKeyColumns).from(parentTable).where((0, drizzle_orm.and)(match, (0, drizzle_orm.relationsFilterToSQL)(parentTable, relationOwnWhere(relation))))).map(keyOf));
|
|
7005
|
+
return rows.some((row) => !shown.has(keyOf(row)));
|
|
7006
|
+
},
|
|
7007
|
+
parentKeyColumns
|
|
7008
|
+
};
|
|
6686
7009
|
};
|
|
7010
|
+
/**
|
|
7011
|
+
* A key value in a form that compares equal across the two reads of it — the driver may hand
|
|
7012
|
+
* back a `Date` or a `bigint` that is a different object each time.
|
|
7013
|
+
*/
|
|
7014
|
+
const keyString = (value) => value instanceof Date ? `d${value.getTime()}` : value === null || value === void 0 ? "" : `v${String(value)}`;
|
|
6687
7015
|
/** The property name a column is stored under on its table, matched by identity. */
|
|
6688
7016
|
const propNameOf = (table, column) => {
|
|
6689
7017
|
if (!column) return;
|
|
@@ -6745,7 +7073,7 @@ const buildThroughPlan = (relationName, relEntry, relation, table, targetTableNa
|
|
|
6745
7073
|
targetCol
|
|
6746
7074
|
},
|
|
6747
7075
|
targetKeyColumns: { [foreignColPropName]: foreignCol },
|
|
6748
|
-
...ownWherePlan(relEntry, table, targetTable, localCol),
|
|
7076
|
+
...ownWherePlan(relEntry, table, targetTable, localCol, []),
|
|
6749
7077
|
allowed: /* @__PURE__ */ new Set([
|
|
6750
7078
|
"connect",
|
|
6751
7079
|
"disconnect",
|
|
@@ -6824,7 +7152,7 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
6824
7152
|
const targetPrimaryKey = resolvePrimaryKeys(targetTable).filter((name) => targetColumns[name]);
|
|
6825
7153
|
const targetKeyColumns = Object.fromEntries(targetPrimaryKey.map((name) => [name, targetColumns[name]]));
|
|
6826
7154
|
targetKeyColumns[foreignColPropName] = foreignCol;
|
|
6827
|
-
const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol);
|
|
7155
|
+
const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol, resolvePrimaryKeys(table));
|
|
6828
7156
|
const ops = !fkColumn.notNull ? isOne ? [
|
|
6829
7157
|
"create",
|
|
6830
7158
|
"connect",
|
|
@@ -7257,6 +7585,16 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7257
7585
|
}
|
|
7258
7586
|
if (supplied(op["connect"])) await link(asArray(op["connect"]), "connect");
|
|
7259
7587
|
};
|
|
7588
|
+
/**
|
|
7589
|
+
* The value a parent-side `connect` / `disconnect` writes to an existing row's key: `next`,
|
|
7590
|
+
* unless the key points at a row the relation's own `where` hides, which it then keeps.
|
|
7591
|
+
*/
|
|
7592
|
+
const keepingHidden = (plan, next) => {
|
|
7593
|
+
if (!plan.ownCondition) return next;
|
|
7594
|
+
const shown = drizzle_orm.sql`select ${plan.foreignCol} from ${plan.targetTable} where ${plan.ownCondition()}`;
|
|
7595
|
+
const replacement = next === null ? drizzle_orm.sql`null` : drizzle_orm.sql`${drizzle_orm.sql.param(next, plan.localCol)}`;
|
|
7596
|
+
return drizzle_orm.sql`case when ${plan.localCol} is null or ${plan.localCol} in (${shown}) then ${replacement} else ${plan.localCol} end`;
|
|
7597
|
+
};
|
|
7260
7598
|
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, (0, drizzle_orm.and)((0, drizzle_orm.eq)(plan.foreignCol, key), plan.ownCondition?.()))), op, "update");
|
|
7261
7599
|
return {
|
|
7262
7600
|
enabled: (tableName) => !!plans[tableName],
|
|
@@ -7282,7 +7620,10 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7282
7620
|
const allColumns = (0, drizzle_orm.getColumns)(table);
|
|
7283
7621
|
for (const relationName of Object.keys(ops)) {
|
|
7284
7622
|
const plan = tablePlans[relationName];
|
|
7285
|
-
if (plan
|
|
7623
|
+
if (plan && !(plan.localColPropName in columns) && allColumns[plan.localColPropName]) columns[plan.localColPropName] = allColumns[plan.localColPropName];
|
|
7624
|
+
if (plan?.fkSide === "parent" && plan.parentHidden) {
|
|
7625
|
+
for (const [name, column] of Object.entries(plan.parentKeyColumns ?? {})) if (!(name in columns)) columns[name] = column;
|
|
7626
|
+
}
|
|
7286
7627
|
}
|
|
7287
7628
|
for (const name of selfTargetingPlan(tablePlans, tableName, ops)?.targetPrimaryKey ?? []) if (!(name in columns) && allColumns[name]) columns[name] = allColumns[name];
|
|
7288
7629
|
return columns;
|
|
@@ -7302,10 +7643,15 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7302
7643
|
...fresh.get(keyOf(row))
|
|
7303
7644
|
}));
|
|
7304
7645
|
},
|
|
7305
|
-
applyParentSide: async (executor, tableName, ops, request) => {
|
|
7646
|
+
applyParentSide: async (executor, tableName, ops, request, existing = false) => {
|
|
7306
7647
|
const tablePlans = plans[tableName];
|
|
7307
|
-
const
|
|
7308
|
-
|
|
7648
|
+
const result = {
|
|
7649
|
+
values: {},
|
|
7650
|
+
expected: {},
|
|
7651
|
+
existing
|
|
7652
|
+
};
|
|
7653
|
+
const patch = result.values;
|
|
7654
|
+
if (!tablePlans) return result;
|
|
7309
7655
|
const context = request?.context;
|
|
7310
7656
|
const scope = resolveScope(policies, context, filterCtx);
|
|
7311
7657
|
for (const [relationName, op] of Object.entries(ops)) {
|
|
@@ -7324,14 +7670,30 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7324
7670
|
if (!created) throw drizzleError(`Drizzle-GraphQL Error: 'create' on '${relationName}' inserted no row, so there is nothing to attach.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
7325
7671
|
await steps.after();
|
|
7326
7672
|
patch[plan.localColPropName] = created[plan.foreignColPropName];
|
|
7673
|
+
result.expected[relationName] = created[plan.foreignColPropName];
|
|
7327
7674
|
} else if (op["connect"] !== void 0) {
|
|
7328
7675
|
const rows = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
|
|
7329
7676
|
if (rows.length !== 1) throw connectMatchError(relationName, rows.length);
|
|
7330
|
-
|
|
7331
|
-
|
|
7677
|
+
const key = rows[0][plan.foreignColPropName];
|
|
7678
|
+
patch[plan.localColPropName] = existing ? keepingHidden(plan, key) : key;
|
|
7679
|
+
result.expected[relationName] = key;
|
|
7680
|
+
} else if (op["disconnect"] === true) patch[plan.localColPropName] = existing ? keepingHidden(plan, null) : null;
|
|
7332
7681
|
}
|
|
7333
7682
|
for (const columnName of Object.keys(contextValues?.(tableName) ?? {})) delete patch[columnName];
|
|
7334
|
-
return
|
|
7683
|
+
return result;
|
|
7684
|
+
},
|
|
7685
|
+
checkParentSide: async (executor, tableName, ops, rows, patch) => {
|
|
7686
|
+
const tablePlans = plans[tableName];
|
|
7687
|
+
if (!tablePlans || !rows.length) return;
|
|
7688
|
+
for (const relationName of Object.keys(ops)) {
|
|
7689
|
+
const plan = tablePlans[relationName];
|
|
7690
|
+
if (plan?.fkSide !== "parent") continue;
|
|
7691
|
+
if (relationName in patch.expected) {
|
|
7692
|
+
const expected = keyString(patch.expected[relationName]);
|
|
7693
|
+
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" });
|
|
7694
|
+
}
|
|
7695
|
+
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" });
|
|
7696
|
+
}
|
|
7335
7697
|
},
|
|
7336
7698
|
applyChildSide: async (executor, tableName, ops, parentRows, request) => {
|
|
7337
7699
|
const tablePlans = plans[tableName];
|
|
@@ -7398,12 +7760,13 @@ var NoParentRow = class extends Error {};
|
|
|
7398
7760
|
* `executor` must already be a transaction.
|
|
7399
7761
|
*/
|
|
7400
7762
|
const writeNestedEntry = async (params) => {
|
|
7401
|
-
const { executor, runtime, tableName, ops, write, request } = params;
|
|
7763
|
+
const { executor, runtime, tableName, ops, write, request, existing } = params;
|
|
7402
7764
|
const guarded = runtime.createsParentSide(tableName, ops);
|
|
7403
7765
|
const run = async (tx) => {
|
|
7404
|
-
const patch = await runtime.applyParentSide(tx, tableName, ops, request);
|
|
7405
|
-
const rows = await write(tx, patch);
|
|
7766
|
+
const patch = await runtime.applyParentSide(tx, tableName, ops, request, existing);
|
|
7767
|
+
const rows = await write(tx, patch.values);
|
|
7406
7768
|
if (guarded && !rows.length) throw new NoParentRow();
|
|
7769
|
+
await runtime.checkParentSide(tx, tableName, ops, rows, patch);
|
|
7407
7770
|
await runtime.applyChildSide(tx, tableName, ops, rows, request);
|
|
7408
7771
|
return runtime.refreshed(tx, tableName, ops, rows);
|
|
7409
7772
|
};
|
|
@@ -7465,7 +7828,8 @@ const updateWithNestedOps = async (params) => {
|
|
|
7465
7828
|
...remapValues(columns),
|
|
7466
7829
|
...patch
|
|
7467
7830
|
}),
|
|
7468
|
-
request
|
|
7831
|
+
request,
|
|
7832
|
+
existing: true
|
|
7469
7833
|
}));
|
|
7470
7834
|
};
|
|
7471
7835
|
//#endregion
|
|
@@ -7618,6 +7982,7 @@ const createSchemaBuilder = (adapter) => {
|
|
|
7618
7982
|
const anyTable = (feature) => tableEntries.some(([name]) => featureOf(name)[feature]);
|
|
7619
7983
|
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?");
|
|
7620
7984
|
registerScalarOverrides(tables, options);
|
|
7985
|
+
installSqliteBlobJsonDecoders(tables);
|
|
7621
7986
|
registerEnumConfig(options);
|
|
7622
7987
|
registerColumnExclusions(tables, options.exclude);
|
|
7623
7988
|
resetAggregateColumnCache();
|
|
@@ -7849,7 +8214,7 @@ const createSchemaBuilder = (adapter) => {
|
|
|
7849
8214
|
...tableFeatures.update ? [updateInput] : [],
|
|
7850
8215
|
...built.updateManyInput ? [built.updateManyInput] : [],
|
|
7851
8216
|
tableFilters,
|
|
7852
|
-
tableOrder
|
|
8217
|
+
...tableOrder ? [tableOrder] : []
|
|
7853
8218
|
].forEach((e) => {
|
|
7854
8219
|
inputs[e.name] = e;
|
|
7855
8220
|
});
|
|
@@ -7979,14 +8344,12 @@ const generateUpsert = (db, tableName, table, baseType, onConflictType, fieldNam
|
|
|
7979
8344
|
withTarget: false,
|
|
7980
8345
|
serverOwned: Object.keys(contextColumns ?? {})
|
|
7981
8346
|
});
|
|
7982
|
-
const requestedNothing = args.onConflict?.action === "NOTHING";
|
|
7983
8347
|
const runBatch = async (target, { values, plan }) => {
|
|
7984
|
-
if (plan.action === "NOTHING"
|
|
8348
|
+
if (plan.action === "NOTHING") {
|
|
7985
8349
|
const columns = (0, drizzle_orm.getColumns)(table);
|
|
7986
8350
|
const keyName = pkNames[0] ?? Object.keys(columns)[0];
|
|
7987
8351
|
await target.insert(table).values(values).onDuplicateKeyUpdate({ set: { [keyName]: drizzle_orm.sql`${drizzle_orm.sql.identifier(columns[keyName].name)}` } });
|
|
7988
|
-
} else
|
|
7989
|
-
else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
|
|
8352
|
+
} else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
|
|
7990
8353
|
};
|
|
7991
8354
|
if (batches.length === 1) await runBatch(executor, batches[0]);
|
|
7992
8355
|
else await executor.transaction(async (tx) => {
|
|
@@ -8026,7 +8389,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
|
|
|
8026
8389
|
});
|
|
8027
8390
|
if (single) await assertSingleMatch(executor, table, filters);
|
|
8028
8391
|
let query = executor.update(table).set(input);
|
|
8029
|
-
if (filters) query = query.where(filters);
|
|
8392
|
+
if (filters) query = query.where(mysqlWriteFilter(table, filters));
|
|
8030
8393
|
await query;
|
|
8031
8394
|
await after();
|
|
8032
8395
|
return isSuccess;
|
|
@@ -8041,7 +8404,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
|
|
|
8041
8404
|
* in order. MySQL has no RETURNING, so — like every other MySQL mutation — the result is
|
|
8042
8405
|
* `{ isSuccess: true }` rather than the updated rows.
|
|
8043
8406
|
*/
|
|
8044
|
-
const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName, filterCtx, txCtx, policies) => writeResolver({
|
|
8407
|
+
const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName, requireWhere, filterCtx, txCtx, policies) => writeResolver({
|
|
8045
8408
|
db,
|
|
8046
8409
|
tableName,
|
|
8047
8410
|
operation: "updateMany",
|
|
@@ -8061,13 +8424,20 @@ const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName,
|
|
|
8061
8424
|
if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
|
|
8062
8425
|
return {
|
|
8063
8426
|
set: input,
|
|
8064
|
-
filters:
|
|
8427
|
+
filters: scopedWhere({
|
|
8428
|
+
scope,
|
|
8429
|
+
tableName,
|
|
8430
|
+
table,
|
|
8431
|
+
where,
|
|
8432
|
+
relationCtx: relationFilterCtx(filterCtx, tableName, scope),
|
|
8433
|
+
required: requireWhere
|
|
8434
|
+
})
|
|
8065
8435
|
};
|
|
8066
8436
|
});
|
|
8067
8437
|
await executor.transaction(async (tx) => {
|
|
8068
8438
|
for (const entry of entries) {
|
|
8069
8439
|
let query = tx.update(table).set(entry.set);
|
|
8070
|
-
if (entry.filters) query = query.where(entry.filters);
|
|
8440
|
+
if (entry.filters) query = query.where(mysqlWriteFilter(table, entry.filters));
|
|
8071
8441
|
await query;
|
|
8072
8442
|
}
|
|
8073
8443
|
});
|
|
@@ -8116,7 +8486,7 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
|
|
|
8116
8486
|
});
|
|
8117
8487
|
if (single) await assertSingleMatch(executor, table, filters);
|
|
8118
8488
|
let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
|
|
8119
|
-
if (filters) query = query.where(filters);
|
|
8489
|
+
if (filters) query = query.where(mysqlWriteFilter(table, filters));
|
|
8120
8490
|
await query;
|
|
8121
8491
|
await after();
|
|
8122
8492
|
return isSuccess;
|
|
@@ -8127,6 +8497,34 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
|
|
|
8127
8497
|
const getTableConfig$2 = memoizeTableConfig(drizzle_orm_mysql_core.getTableConfig);
|
|
8128
8498
|
/** Primary-key property names for a MySQL table, including table-level composite keys. */
|
|
8129
8499
|
const mysqlPrimaryKeyPropNames = (table) => getPrimaryKeyPropNamesFromConfig(table, getTableConfig$2);
|
|
8500
|
+
const TABLE_ORIGINAL_NAME = Symbol.for("drizzle:OriginalName");
|
|
8501
|
+
const TABLE_SCHEMA = Symbol.for("drizzle:Schema");
|
|
8502
|
+
/** Whether `chunk` reads `table` anywhere — under an alias, in a subquery, however deep. */
|
|
8503
|
+
const readsTable = (chunk, table) => {
|
|
8504
|
+
if (Array.isArray(chunk)) return chunk.some((part) => readsTable(part, table));
|
|
8505
|
+
if ((0, drizzle_orm.is)(chunk, drizzle_orm.Table)) return chunk[TABLE_ORIGINAL_NAME] === table[TABLE_ORIGINAL_NAME] && chunk[TABLE_SCHEMA] === table[TABLE_SCHEMA];
|
|
8506
|
+
if ((0, drizzle_orm.is)(chunk, drizzle_orm.SQL)) return readsTable(chunk.queryChunks, table);
|
|
8507
|
+
if ((0, drizzle_orm.is)(chunk, drizzle_orm.SQL.Aliased)) return readsTable(chunk.sql, table);
|
|
8508
|
+
if ((0, drizzle_orm.is)(chunk, drizzle_orm.Subquery)) return readsTable(chunk._.sql, table);
|
|
8509
|
+
return false;
|
|
8510
|
+
};
|
|
8511
|
+
/**
|
|
8512
|
+
* The `where` of an UPDATE or DELETE, in a form MySQL accepts. MySQL refuses a statement whose
|
|
8513
|
+
* `where` reads the table it writes (error 1093, "You can't specify target table … for update
|
|
8514
|
+
* in FROM clause") — which a relation filter through a self-referencing relation, or a scope
|
|
8515
|
+
* written as one, does. Such a filter is resolved to the primary keys it matches first, through
|
|
8516
|
+
* a derived table MySQL materializes (`DISTINCT` keeps the optimizer from merging it back in),
|
|
8517
|
+
* and the write matches those keys. Any other filter is returned as it is.
|
|
8518
|
+
*/
|
|
8519
|
+
const mysqlWriteFilter = (table, filters) => {
|
|
8520
|
+
if (!filters || !readsTable(filters, table)) return filters;
|
|
8521
|
+
const columns = (0, drizzle_orm.getColumns)(table);
|
|
8522
|
+
const keys = mysqlPrimaryKeyPropNames(table).map((name) => columns[name]);
|
|
8523
|
+
if (!keys.length) return filters;
|
|
8524
|
+
const keyList = drizzle_orm.sql.join(keys.map((column) => drizzle_orm.sql.identifier(column.name)), drizzle_orm.sql`, `);
|
|
8525
|
+
const target = keys.length === 1 ? drizzle_orm.sql`${keys[0]}` : drizzle_orm.sql`(${drizzle_orm.sql.join(keys, drizzle_orm.sql`, `)})`;
|
|
8526
|
+
return drizzle_orm.sql`${target} in (select ${keyList} from (select distinct ${drizzle_orm.sql.join(keys, drizzle_orm.sql`, `)} from ${table} where ${filters}) as ${drizzle_orm.sql.identifier("dgql_write_target")})`;
|
|
8527
|
+
};
|
|
8130
8528
|
const { prepareBuild, forEachTable, finalizeBuild } = createSchemaBuilder({
|
|
8131
8529
|
tableClass: drizzle_orm_mysql_core.MySqlTable,
|
|
8132
8530
|
getTableConfig: getTableConfig$2,
|
|
@@ -8180,7 +8578,7 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
|
|
|
8180
8578
|
tableFilters,
|
|
8181
8579
|
cacheCtx
|
|
8182
8580
|
}) : void 0;
|
|
8183
|
-
const updateManyGenerated = updateManyInput ? generateUpdateMany$2(db, tableName, schema[tableName], updateManyInput, updateManyFieldName, filterCtx, mutationTxCtx, policies) : void 0;
|
|
8581
|
+
const updateManyGenerated = updateManyInput ? generateUpdateMany$2(db, tableName, schema[tableName], updateManyInput, updateManyFieldName, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
|
|
8184
8582
|
const deleteGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
|
|
8185
8583
|
const deleteSingleGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteSingleFieldName, true, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
|
|
8186
8584
|
const restoreGenerated = softDeleteInfo ? generateDelete(db, tableName, schema[tableName], tableFilters, restoreFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies, true) : void 0;
|
|
@@ -8197,7 +8595,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
|
|
|
8197
8595
|
filterCtx,
|
|
8198
8596
|
txCtx: mutationTxCtx,
|
|
8199
8597
|
policies,
|
|
8200
|
-
returnsRows: false
|
|
8598
|
+
returnsRows: false,
|
|
8599
|
+
writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
|
|
8201
8600
|
}) : void 0;
|
|
8202
8601
|
const deleteCountGenerated = tableFeatures.delete && tableFeatures.countMutations ? generateWriteCount({
|
|
8203
8602
|
db,
|
|
@@ -8210,7 +8609,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
|
|
|
8210
8609
|
filterCtx,
|
|
8211
8610
|
txCtx: mutationTxCtx,
|
|
8212
8611
|
policies,
|
|
8213
|
-
returnsRows: false
|
|
8612
|
+
returnsRows: false,
|
|
8613
|
+
writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
|
|
8214
8614
|
}) : void 0;
|
|
8215
8615
|
const hardDeleteMeta = softDeleteInfo?.hardDelete ? { hardDelete: true } : {};
|
|
8216
8616
|
const withMeta = (generated, meta) => ({
|
|
@@ -8322,7 +8722,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8322
8722
|
* one-element list.
|
|
8323
8723
|
*/
|
|
8324
8724
|
const generateInsert = (build, opts) => {
|
|
8325
|
-
const { db, tables, relationMap, typeNameMapper, txCtx, nested, limits, policies, resolveName } = build;
|
|
8725
|
+
const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
|
|
8326
8726
|
const { tableName, table, baseType, fieldName, typeName, single, conflictDoNothing = false } = opts;
|
|
8327
8727
|
const queryArgs = { values: { type: single ? new graphql.GraphQLNonNull(baseType) : new graphql.GraphQLNonNull(new graphql.GraphQLList(new graphql.GraphQLNonNull(baseType))) } };
|
|
8328
8728
|
const pkNames = primaryKeyPropNames(table);
|
|
@@ -8346,6 +8746,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8346
8746
|
const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
|
|
8347
8747
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
8348
8748
|
relationMap,
|
|
8749
|
+
filterCtx,
|
|
8349
8750
|
tables,
|
|
8350
8751
|
tableName,
|
|
8351
8752
|
typeName,
|
|
@@ -8424,6 +8825,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8424
8825
|
const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
|
|
8425
8826
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
8426
8827
|
relationMap,
|
|
8828
|
+
filterCtx,
|
|
8427
8829
|
tables,
|
|
8428
8830
|
tableName,
|
|
8429
8831
|
typeName,
|
|
@@ -8449,18 +8851,9 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8449
8851
|
buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope))
|
|
8450
8852
|
});
|
|
8451
8853
|
const batchQuery = (executor, { values: rows, plan }) => {
|
|
8452
|
-
|
|
8854
|
+
const query = executor.insert(table).values(rows).returning(returning);
|
|
8453
8855
|
const setWhere = withScope(scope, tableName, table, plan.setWhere);
|
|
8454
|
-
query
|
|
8455
|
-
target: plan.target,
|
|
8456
|
-
where: plan.targetWhere
|
|
8457
|
-
} : void 0) : query.onConflictDoUpdate({
|
|
8458
|
-
target: plan.target,
|
|
8459
|
-
targetWhere: plan.targetWhere,
|
|
8460
|
-
set: plan.set,
|
|
8461
|
-
setWhere
|
|
8462
|
-
});
|
|
8463
|
-
return query;
|
|
8856
|
+
return applyConflictPlan(query, table, plan, setWhere);
|
|
8464
8857
|
};
|
|
8465
8858
|
if (batches.length === 1) return await batchQuery(target, batches[0]);
|
|
8466
8859
|
return target.transaction((tx) => {
|
|
@@ -8523,6 +8916,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8523
8916
|
await before();
|
|
8524
8917
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
8525
8918
|
relationMap,
|
|
8919
|
+
filterCtx,
|
|
8526
8920
|
tables,
|
|
8527
8921
|
tableName,
|
|
8528
8922
|
typeName,
|
|
@@ -8761,7 +9155,8 @@ const createSchemaDataGenerator = (adapter) => {
|
|
|
8761
9155
|
table: schema[tableName],
|
|
8762
9156
|
updateManyInput,
|
|
8763
9157
|
fieldName: updateManyFieldName,
|
|
8764
|
-
typeName
|
|
9158
|
+
typeName,
|
|
9159
|
+
requireWhere: tableFeatures.requireWhere
|
|
8765
9160
|
}) : void 0;
|
|
8766
9161
|
const deleteGenerated = tableFeatures.delete ? generateDelete(writeBuild, {
|
|
8767
9162
|
tableName,
|
|
@@ -9016,7 +9411,8 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
|
|
|
9016
9411
|
tableName,
|
|
9017
9412
|
ops: entry.ops,
|
|
9018
9413
|
write,
|
|
9019
|
-
request
|
|
9414
|
+
request,
|
|
9415
|
+
existing: true
|
|
9020
9416
|
}) : await write(tx, {}));
|
|
9021
9417
|
}
|
|
9022
9418
|
return results;
|
|
@@ -9037,7 +9433,7 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
|
|
|
9037
9433
|
const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateManyBatch) => {
|
|
9038
9434
|
return (build, opts) => {
|
|
9039
9435
|
const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
|
|
9040
|
-
const { tableName, table, updateManyInput, fieldName, typeName } = opts;
|
|
9436
|
+
const { tableName, table, updateManyInput, fieldName, typeName, requireWhere } = opts;
|
|
9041
9437
|
const queryArgs = { updates: { type: new graphql.GraphQLNonNull(new graphql.GraphQLList(new graphql.GraphQLNonNull(updateManyInput))) } };
|
|
9042
9438
|
const pkNames = primaryKeyPropNames(table);
|
|
9043
9439
|
return writeResolver({
|
|
@@ -9057,6 +9453,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
|
|
|
9057
9453
|
const contextColumns = policies?.contextValues?.(tableName);
|
|
9058
9454
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
9059
9455
|
relationMap,
|
|
9456
|
+
filterCtx,
|
|
9060
9457
|
tables,
|
|
9061
9458
|
tableName,
|
|
9062
9459
|
typeName,
|
|
@@ -9076,7 +9473,14 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
|
|
|
9076
9473
|
return {
|
|
9077
9474
|
set: input,
|
|
9078
9475
|
ops,
|
|
9079
|
-
filters:
|
|
9476
|
+
filters: scopedWhere({
|
|
9477
|
+
scope,
|
|
9478
|
+
tableName,
|
|
9479
|
+
table,
|
|
9480
|
+
where,
|
|
9481
|
+
relationCtx: relationFilterCtx(filterCtx, tableName, scope),
|
|
9482
|
+
required: requireWhere
|
|
9483
|
+
})
|
|
9080
9484
|
};
|
|
9081
9485
|
});
|
|
9082
9486
|
const anyNested = entries.some((entry) => entry.ops);
|
|
@@ -9165,7 +9569,8 @@ const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNest
|
|
|
9165
9569
|
tableName,
|
|
9166
9570
|
ops: entry.ops,
|
|
9167
9571
|
write,
|
|
9168
|
-
request
|
|
9572
|
+
request,
|
|
9573
|
+
existing: true
|
|
9169
9574
|
}) : write(tx, {});
|
|
9170
9575
|
};
|
|
9171
9576
|
return executor.transaction((tx) => {
|
|
@@ -9195,6 +9600,7 @@ const sqliteSchemaData = createSchemaDataGenerator({
|
|
|
9195
9600
|
preflight: (db, options) => {
|
|
9196
9601
|
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.");
|
|
9197
9602
|
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.");
|
|
9603
|
+
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.");
|
|
9198
9604
|
},
|
|
9199
9605
|
generateUpdateMany
|
|
9200
9606
|
});
|