@vantreeseba/drizzle-graphql 15.1.5 → 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 +188 -25
- package/dist/index.cjs +913 -367
- 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 +916 -370
- 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,18 +1308,51 @@ 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;
|
|
1261
1322
|
};
|
|
1323
|
+
/**
|
|
1324
|
+
* Where a remapped row keeps the values its columns held before remapping, by property name —
|
|
1325
|
+
* only for the columns remapping changed. The output form is not always reversible: a
|
|
1326
|
+
* microsecond timestamp read in `'string'` mode leaves as an ISO string the `DateTime` input
|
|
1327
|
+
* side would cut to milliseconds. A relation field correlating on the row's join columns reads
|
|
1328
|
+
* them from here (see `parentJoinValues`). Non-enumerable, so the row's own keys, JSON form and
|
|
1329
|
+
* equality are unchanged.
|
|
1330
|
+
*/
|
|
1331
|
+
const RAW_COLUMN_VALUES = Symbol("drizzle-graphql.rawColumnValues");
|
|
1332
|
+
/**
|
|
1333
|
+
* The value column `key` held in `row` before remapping: `{ found: true, value }` when remapping
|
|
1334
|
+
* changed it, `{ found: false }` when it did not (or the row was never remapped), in which case
|
|
1335
|
+
* `row[key]` is still the column's own value.
|
|
1336
|
+
*/
|
|
1337
|
+
const rawColumnValue = (row, key) => {
|
|
1338
|
+
const raw = row[RAW_COLUMN_VALUES];
|
|
1339
|
+
return raw && key in raw ? {
|
|
1340
|
+
found: true,
|
|
1341
|
+
value: raw[key]
|
|
1342
|
+
} : { found: false };
|
|
1343
|
+
};
|
|
1344
|
+
const keepRawColumnValue = (row, key, value) => {
|
|
1345
|
+
let raw = row[RAW_COLUMN_VALUES];
|
|
1346
|
+
if (!raw) {
|
|
1347
|
+
raw = Object.create(null);
|
|
1348
|
+
Object.defineProperty(row, RAW_COLUMN_VALUES, {
|
|
1349
|
+
value: raw,
|
|
1350
|
+
enumerable: false,
|
|
1351
|
+
configurable: true
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
if (!(key in raw)) raw[key] = value;
|
|
1355
|
+
};
|
|
1262
1356
|
const remapToGraphQLSingleOutput = (queryOutput, tableName, table, relationMap) => {
|
|
1263
1357
|
const columns = (0, drizzle_orm.getColumns)(table);
|
|
1264
1358
|
for (const [key, value] of Object.entries(queryOutput)) {
|
|
@@ -1272,11 +1366,9 @@ const remapToGraphQLSingleOutput = (queryOutput, tableName, table, relationMap)
|
|
|
1272
1366
|
continue;
|
|
1273
1367
|
}
|
|
1274
1368
|
const column = columns[key];
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
}
|
|
1279
|
-
queryOutput[key] = remapToGraphQLCore(key, value, tableName, column, relationMap);
|
|
1369
|
+
const remapped = remapToGraphQLCore(key, value, tableName, column, relationMap);
|
|
1370
|
+
if (column && remapped !== value) keepRawColumnValue(queryOutput, key, value);
|
|
1371
|
+
queryOutput[key] = remapped;
|
|
1280
1372
|
}
|
|
1281
1373
|
return queryOutput;
|
|
1282
1374
|
};
|
|
@@ -1293,6 +1385,7 @@ const remapFromGraphQLCore = (value, column, columnName) => {
|
|
|
1293
1385
|
const dataType = column.dataType ?? "";
|
|
1294
1386
|
const columnType = column.columnType ?? "";
|
|
1295
1387
|
if (isDateTimeColumn(column)) {
|
|
1388
|
+
if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
|
|
1296
1389
|
const isMySql = columnType.startsWith("MySql");
|
|
1297
1390
|
const isPgString = isPgTimestampStringColumn(column);
|
|
1298
1391
|
const toIso = isMySql ? mySqlDateTimeTextToIso : isPgString ? pgTimestampTextToIso : void 0;
|
|
@@ -1307,41 +1400,29 @@ const remapFromGraphQLCore = (value, column, columnName) => {
|
|
|
1307
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" });
|
|
1308
1401
|
return day;
|
|
1309
1402
|
}
|
|
1310
|
-
if (dataType.includes("bigint"))
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1314
|
-
}
|
|
1315
|
-
if (dataType.includes("json") && column.columnType !== "PgGeometryObject") return value;
|
|
1316
|
-
switch (dataType) {
|
|
1317
|
-
case "date": {
|
|
1318
|
-
const formatted = new Date(value);
|
|
1319
|
-
if (Number.isNaN(formatted.getTime())) throw drizzleError(`Field '${columnName}' is not a valid date!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1320
|
-
return formatted;
|
|
1321
|
-
}
|
|
1322
|
-
case "buffer":
|
|
1323
|
-
if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1324
|
-
return Buffer.from(value);
|
|
1325
|
-
case "json":
|
|
1326
|
-
if (column.columnType === "PgGeometryObject") return value;
|
|
1327
|
-
try {
|
|
1328
|
-
return JSON.parse(value);
|
|
1329
|
-
} catch (e) {
|
|
1330
|
-
throw drizzleError(`Invalid JSON in field '${columnName}':\n${e instanceof Error ? e.message : "Unknown error"}`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1331
|
-
}
|
|
1332
|
-
case "array":
|
|
1333
|
-
if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1334
|
-
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" });
|
|
1335
|
-
return value;
|
|
1336
|
-
case "bigint": try {
|
|
1403
|
+
if (dataType.includes("bigint")) {
|
|
1404
|
+
if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
|
|
1405
|
+
try {
|
|
1337
1406
|
return BigInt(value);
|
|
1338
|
-
} catch
|
|
1407
|
+
} catch {
|
|
1339
1408
|
throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1340
1409
|
}
|
|
1341
|
-
default:
|
|
1342
|
-
if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
|
|
1343
|
-
return value;
|
|
1344
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;
|
|
1345
1426
|
};
|
|
1346
1427
|
const remapFromGraphQLSingleInput = (queryInput, table, operation = "insert") => {
|
|
1347
1428
|
const columns = (0, drizzle_orm.getColumns)(table);
|
|
@@ -1383,36 +1464,60 @@ const escapeLikeValue = (value) => value.replace(/[\\%_]/g, (char) => `\\${char}
|
|
|
1383
1464
|
const safeLikeOps = {
|
|
1384
1465
|
startsWith: {
|
|
1385
1466
|
buildPattern: (value) => `${escapeLikeValue(value)}%`,
|
|
1467
|
+
buildGlob: (value) => `${escapeGlobValue(value)}*`,
|
|
1386
1468
|
insensitive: false
|
|
1387
1469
|
},
|
|
1388
1470
|
endsWith: {
|
|
1389
1471
|
buildPattern: (value) => `%${escapeLikeValue(value)}`,
|
|
1472
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}`,
|
|
1390
1473
|
insensitive: false
|
|
1391
1474
|
},
|
|
1392
1475
|
contains: {
|
|
1393
1476
|
buildPattern: (value) => `%${escapeLikeValue(value)}%`,
|
|
1477
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}*`,
|
|
1394
1478
|
insensitive: false
|
|
1395
1479
|
},
|
|
1396
1480
|
iStartsWith: {
|
|
1397
1481
|
buildPattern: (value) => `${escapeLikeValue(value)}%`,
|
|
1482
|
+
buildGlob: (value) => `${escapeGlobValue(value)}*`,
|
|
1398
1483
|
insensitive: true
|
|
1399
1484
|
},
|
|
1400
1485
|
iEndsWith: {
|
|
1401
1486
|
buildPattern: (value) => `%${escapeLikeValue(value)}`,
|
|
1487
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}`,
|
|
1402
1488
|
insensitive: true
|
|
1403
1489
|
},
|
|
1404
1490
|
iContains: {
|
|
1405
1491
|
buildPattern: (value) => `%${escapeLikeValue(value)}%`,
|
|
1492
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}*`,
|
|
1406
1493
|
insensitive: true
|
|
1407
1494
|
}
|
|
1408
1495
|
};
|
|
1409
1496
|
/**
|
|
1410
|
-
* `
|
|
1411
|
-
*
|
|
1412
|
-
*
|
|
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.
|
|
1413
1500
|
*/
|
|
1414
|
-
const
|
|
1415
|
-
|
|
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).
|
|
1517
|
+
*/
|
|
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}`;
|
|
1416
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}`;
|
|
1417
1522
|
};
|
|
1418
1523
|
/**
|
|
@@ -1468,6 +1573,35 @@ const jsonContains = (column, columnName, value) => {
|
|
|
1468
1573
|
}
|
|
1469
1574
|
};
|
|
1470
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
|
+
/**
|
|
1471
1605
|
* Constant predicates, used where an operator's operand list is empty and the answer is
|
|
1472
1606
|
* therefore known without touching the column. Written as `1 = 0` / `1 = 1` rather than the
|
|
1473
1607
|
* `FALSE` / `TRUE` keywords so they compile identically on all three dialects. Built fresh
|
|
@@ -1502,7 +1636,8 @@ const NUMERIC_TEXT_PATTERN = "^\\s*-?(\\d+\\.?\\d*|\\.\\d+)([eE][-+]?\\d+)?\\s*$
|
|
|
1502
1636
|
* pattern guard.
|
|
1503
1637
|
* - **SQLite** — `json_extract` returns the value in SQLite's own type, so the reads cast
|
|
1504
1638
|
* explicitly: without that, SQLite's cross-type ordering puts every string above every
|
|
1505
|
-
* 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.
|
|
1506
1641
|
*/
|
|
1507
1642
|
const jsonPathExprs = (column, path) => {
|
|
1508
1643
|
switch (columnDialect(column)) {
|
|
@@ -1528,10 +1663,11 @@ const jsonPathExprs = (column, path) => {
|
|
|
1528
1663
|
}
|
|
1529
1664
|
default: {
|
|
1530
1665
|
const extracted = drizzle_orm.sql`json_extract(${column}, ${jsonPathString(path)})`;
|
|
1666
|
+
const type = drizzle_orm.sql`json_type(${column}, ${jsonPathString(path)})`;
|
|
1531
1667
|
return {
|
|
1532
|
-
text: drizzle_orm.sql`cast(${extracted} as text)`,
|
|
1533
|
-
number: drizzle_orm.sql`(case when
|
|
1534
|
-
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)`,
|
|
1535
1671
|
encodeBoolean: (value) => value ? 1 : 0
|
|
1536
1672
|
};
|
|
1537
1673
|
}
|
|
@@ -1570,9 +1706,9 @@ const jsonPathCondition = (column, columnName, filter) => {
|
|
|
1570
1706
|
continue;
|
|
1571
1707
|
}
|
|
1572
1708
|
if (operatorName in safeLikeOps) {
|
|
1573
|
-
const
|
|
1709
|
+
const op = safeLikeOps[operatorName];
|
|
1574
1710
|
if (typeof operatorValue !== "string") throw drizzleError(`WHERE ${locator}: operator '${operatorName}' takes a string`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1575
|
-
variants.push(safeLikeCondition(exprs.text,
|
|
1711
|
+
variants.push(safeLikeCondition(exprs.text, operatorValue, op, op.insensitive, column));
|
|
1576
1712
|
continue;
|
|
1577
1713
|
}
|
|
1578
1714
|
const comparison = JSON_PATH_COMPARISONS[operatorName];
|
|
@@ -1630,20 +1766,25 @@ const extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1630
1766
|
for (const [operatorName, operatorValue] of entries) {
|
|
1631
1767
|
if (operatorValue === null || operatorValue === void 0) continue;
|
|
1632
1768
|
if (operatorValue === false && FLAG_OPERATORS.has(operatorName)) continue;
|
|
1633
|
-
if (operatorName
|
|
1769
|
+
if ((operatorName === "eq" || operatorName === "ne") && isJsonColumn(column)) variants.push(jsonEquality(column, columnName, operatorValue, operatorName === "ne"));
|
|
1770
|
+
else if (operatorName in singleValueOps) {
|
|
1634
1771
|
const singleValue = remapFromGraphQLCore(operatorValue, column, columnName);
|
|
1635
1772
|
variants.push(foldCase ? lowerComparison(column, operatorName, singleValue) : singleValueOps[operatorName](column, singleValue));
|
|
1636
1773
|
} else if (operatorName in stringValueOps) {
|
|
1637
|
-
const
|
|
1638
|
-
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));
|
|
1639
1780
|
} else if (operatorName === "path" && isJsonColumn(column)) for (const pathFilter of operatorValue) {
|
|
1640
1781
|
const extracted = jsonPathCondition(column, columnName, pathFilter);
|
|
1641
1782
|
if (extracted) variants.push(extracted);
|
|
1642
1783
|
}
|
|
1643
1784
|
else if (operatorName === "contains" && isJsonColumn(column)) variants.push(jsonContains(column, columnName, operatorValue));
|
|
1644
1785
|
else if (operatorName in safeLikeOps) {
|
|
1645
|
-
const
|
|
1646
|
-
variants.push(safeLikeCondition(column,
|
|
1786
|
+
const op = safeLikeOps[operatorName];
|
|
1787
|
+
variants.push(safeLikeCondition(column, operatorValue, op, op.insensitive || foldCase));
|
|
1647
1788
|
} else if (operatorName in arrayValueOps) {
|
|
1648
1789
|
if (!operatorValue.length) {
|
|
1649
1790
|
variants.push(operatorName === "inArray" ? sqlFalse() : sqlTrue());
|
|
@@ -1651,13 +1792,13 @@ const extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1651
1792
|
}
|
|
1652
1793
|
const arrayValue = operatorValue.map((val) => remapFromGraphQLCore(val, column, columnName));
|
|
1653
1794
|
variants.push(foldCase ? lowerMembership(column, arrayValue, operatorName === "notInArray") : arrayValueOps[operatorName](column, arrayValue));
|
|
1654
|
-
} 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)]));
|
|
1655
1796
|
else if (operatorName in arrayMembershipOps) {
|
|
1656
1797
|
if (!operatorValue.length) {
|
|
1657
1798
|
variants.push(operatorName === "hasSome" ? sqlFalse() : sqlTrue());
|
|
1658
1799
|
continue;
|
|
1659
1800
|
}
|
|
1660
|
-
variants.push(arrayMembershipOps[operatorName](column, operatorValue));
|
|
1801
|
+
variants.push(arrayMembershipOps[operatorName](column, operatorValue.map((element) => remapFromGraphQLCore(element, column, columnName))));
|
|
1661
1802
|
} else if (operatorName === "isEmpty") variants.push(drizzle_orm.sql`cardinality(${column}) = 0`);
|
|
1662
1803
|
else if (operatorName in nullableOps) variants.push(nullableOps[operatorName](column));
|
|
1663
1804
|
else throw drizzleError(`WHERE ${columnName}: Unknown operator: ${operatorName}`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
@@ -1855,17 +1996,104 @@ const extractRelationJoinColumnPairs = (relEntry, parentTable, targetTable) => {
|
|
|
1855
1996
|
}
|
|
1856
1997
|
return pairs;
|
|
1857
1998
|
};
|
|
1999
|
+
/** A decimal written as text, in a form `canonicalDecimal` can normalise. */
|
|
2000
|
+
const DECIMAL_TEXT = /^([+-]?)(\d*)(?:\.(\d*))?$/;
|
|
2001
|
+
/**
|
|
2002
|
+
* Decimal text without the zeros that do not change its value: `1.500` and `01.5` are both
|
|
2003
|
+
* `1.5`, as SQL compares them. Anything else (an exponent, `NaN`) is returned unchanged.
|
|
2004
|
+
*/
|
|
2005
|
+
const canonicalDecimal = (value) => {
|
|
2006
|
+
const match = DECIMAL_TEXT.exec(value);
|
|
2007
|
+
if (!match || !match[2] && !match[3]) return value;
|
|
2008
|
+
const whole = match[2].replace(/^0+(?=\d)/, "") || "0";
|
|
2009
|
+
const fraction = (match[3] ?? "").replace(/0+$/, "");
|
|
2010
|
+
const digits = fraction ? `${whole}.${fraction}` : whole;
|
|
2011
|
+
return match[1] === "-" && digits !== "0" ? `-${digits}` : digits;
|
|
2012
|
+
};
|
|
2013
|
+
/** An ISO instant without trailing zeros in its fraction: `…:00.100Z` and `…:00.1Z` agree. */
|
|
2014
|
+
const trimIsoFraction = (iso) => iso.replace(/(\.\d*?)0+Z$/, "$1Z").replace(/\.Z$/, "Z");
|
|
2015
|
+
/**
|
|
2016
|
+
* One join value in a form both sides of a relation produce for the same SQL value. The parent's
|
|
2017
|
+
* key and a related row's can arrive in different JS forms — a `Date` and the text of the same
|
|
2018
|
+
* instant, `1.50` and `1.500` from decimals of different scale — and `String()` would tell them
|
|
2019
|
+
* apart (it even drops a `Date`'s milliseconds).
|
|
2020
|
+
*/
|
|
2021
|
+
const joinValueKey = (value, column) => {
|
|
2022
|
+
if (value instanceof Date) {
|
|
2023
|
+
if (Number.isNaN(value.getTime())) return String(value);
|
|
2024
|
+
if (column && isDateOnlyColumn(column)) return dateToCalendarDate(value) ?? value.toISOString();
|
|
2025
|
+
return trimIsoFraction(value.toISOString());
|
|
2026
|
+
}
|
|
2027
|
+
if (typeof value === "bigint") return value.toString();
|
|
2028
|
+
if (value instanceof Uint8Array) return Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("hex");
|
|
2029
|
+
if (typeof value === "string" && column) {
|
|
2030
|
+
if (isMySqlDateTimeStringColumn(column)) return trimIsoFraction(mySqlDateTimeTextToIso(value));
|
|
2031
|
+
if (isPgTimestampStringColumn(column)) return trimIsoFraction(pgTimestampTextToIso(value));
|
|
2032
|
+
if ((column.dataType ?? "") === "string numeric") return canonicalDecimal(value);
|
|
2033
|
+
}
|
|
2034
|
+
return String(value);
|
|
2035
|
+
};
|
|
1858
2036
|
/**
|
|
1859
2037
|
* A string identifying one combination of join-column values, for grouping a batch's rows by
|
|
1860
|
-
* parent.
|
|
2038
|
+
* parent. `columns` are the columns the values were read from, in the same order: the parent's
|
|
2039
|
+
* join columns for a parent key, the target's (or the junction's) for a related row. Values the
|
|
2040
|
+
* database compares equal give the same key — see {@link joinValueKey}.
|
|
2041
|
+
*/
|
|
2042
|
+
const joinKeyOf = (values, columns) => values.length === 1 ? joinValueKey(values[0], columns?.[0]) : JSON.stringify(values.map((value, i) => joinValueKey(value, columns?.[i])));
|
|
2043
|
+
/**
|
|
2044
|
+
* The parent row's values for the relation's join columns, as the database holds them. The row
|
|
2045
|
+
* a relation field resolves under has been remapped to its GraphQL form — a `Date` is an ISO
|
|
2046
|
+
* string by then, a `bigint` a decimal string — which can neither be bound back through every
|
|
2047
|
+
* column's encoder (SQLite's integer timestamp calls `getTime()` on it) nor be compared with the
|
|
2048
|
+
* related rows' decoded values. The values from before remapping are used when the row kept
|
|
2049
|
+
* them; otherwise the output form is mapped back the way an input value would be.
|
|
2050
|
+
*/
|
|
2051
|
+
const parentJoinValues = (parent, pairs) => pairs.map(({ localColPropName, localCol }) => {
|
|
2052
|
+
const raw = rawColumnValue(parent, localColPropName);
|
|
2053
|
+
if (raw.found) return raw.value;
|
|
2054
|
+
const value = parent[localColPropName];
|
|
2055
|
+
if (value == null) return value;
|
|
2056
|
+
try {
|
|
2057
|
+
return remapFromGraphQLCore(value, localCol, localColPropName);
|
|
2058
|
+
} catch {
|
|
2059
|
+
return value;
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
/**
|
|
2063
|
+
* At most this many bound parameters for the parent keys of one batched relation query. SQLite
|
|
2064
|
+
* accepts 32,766 per statement, Postgres and MySQL 65,535; the rest is left to the request's own
|
|
2065
|
+
* filters. A batch with more parents runs as several queries.
|
|
2066
|
+
*/
|
|
2067
|
+
const MAX_PARENT_KEY_PARAMS = 1e4;
|
|
2068
|
+
/**
|
|
2069
|
+
* The parent keys of one batch split into the groups each query reads, so that no query binds
|
|
2070
|
+
* more than {@link MAX_PARENT_KEY_PARAMS} key values. Every parent lands in exactly one group, so
|
|
2071
|
+
* a per-parent result — its rows, its window, its count — is the same whichever query made it.
|
|
2072
|
+
*/
|
|
2073
|
+
const parentKeyChunks = (keys, width) => {
|
|
2074
|
+
const size = Math.max(1, Math.floor(MAX_PARENT_KEY_PARAMS / Math.max(1, width)));
|
|
2075
|
+
const chunks = [];
|
|
2076
|
+
for (let i = 0; i < keys.length; i += size) chunks.push(keys.slice(i, i + size));
|
|
2077
|
+
return chunks;
|
|
2078
|
+
};
|
|
2079
|
+
/**
|
|
2080
|
+
* `or` over `conditions` as a balanced tree. A flat `a OR b OR c …` is parsed left-deep, and
|
|
2081
|
+
* SQLite refuses an expression more than 1,000 levels deep; balanced, a batch of thousands of
|
|
2082
|
+
* parents stays a few levels deep.
|
|
1861
2083
|
*/
|
|
1862
|
-
const
|
|
2084
|
+
const balancedOr = (conditions) => {
|
|
2085
|
+
if (conditions.length <= 2) return (0, drizzle_orm.or)(...conditions);
|
|
2086
|
+
const middle = Math.ceil(conditions.length / 2);
|
|
2087
|
+
return (0, drizzle_orm.or)(balancedOr(conditions.slice(0, middle)), balancedOr(conditions.slice(middle)));
|
|
2088
|
+
};
|
|
1863
2089
|
/**
|
|
1864
2090
|
* The rows whose `cols` hold one of `keys`: `col IN (…)` for a single join column, and an `OR`
|
|
1865
2091
|
* of per-parent equalities for several — `(a = 1 AND b = 1) OR (a = 1 AND b = 2)` — which
|
|
1866
|
-
* every dialect accepts, unlike a row-value `IN` list.
|
|
2092
|
+
* every dialect accepts, unlike a row-value `IN` list. The `OR` is nested as a balanced tree.
|
|
2093
|
+
* Callers split a large batch with {@link parentKeyChunks} first, which keeps either form within
|
|
2094
|
+
* the dialects' parameter limits.
|
|
1867
2095
|
*/
|
|
1868
|
-
const parentKeyCondition = (cols, keys) => cols.length === 1 ? (0, drizzle_orm.inArray)(cols[0], keys.map((key) => key[0])) : (
|
|
2096
|
+
const parentKeyCondition = (cols, keys) => cols.length === 1 ? (0, drizzle_orm.inArray)(cols[0], keys.map((key) => key[0])) : balancedOr(keys.map((key) => (0, drizzle_orm.and)(...cols.map((col, i) => (0, drizzle_orm.eq)(col, key[i])))));
|
|
1869
2097
|
/**
|
|
1870
2098
|
* The junction table and its two columns for a `.through()` (many-to-many) relation:
|
|
1871
2099
|
* `sourceCol` holds the parent's key, `targetCol` the target's. `undefined` for a relation
|
|
@@ -2418,6 +2646,32 @@ const withScope = (scope, tableName, table, condition, mode) => {
|
|
|
2418
2646
|
const predicate = scope?.on(tableName, table, mode);
|
|
2419
2647
|
return predicate ? (0, drizzle_orm.and)(condition, predicate) : condition;
|
|
2420
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;
|
|
2421
2675
|
/** Binds a build's {@link TablePolicies} to its relation context, once, at schema-build time. */
|
|
2422
2676
|
const bindPolicies = (policies, filterCtx) => policies?.scope || policies?.contextValues || policies?.softDelete || policies?.onWrite || policies?.defaultOrderBy ? {
|
|
2423
2677
|
scope: policies.scope || policies.softDelete ? (context) => resolveScope(policies, context, filterCtx) : void 0,
|
|
@@ -2468,7 +2722,7 @@ const deletedArg = (softDelete, tableName, resolveName) => softDelete?.(tableNam
|
|
|
2468
2722
|
const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableName, resolveName) => ({
|
|
2469
2723
|
offset: { type: graphql.GraphQLInt },
|
|
2470
2724
|
limit: { type: graphql.GraphQLInt },
|
|
2471
|
-
orderBy: { type: orderArgs },
|
|
2725
|
+
...orderArgs ? { orderBy: { type: orderArgs } } : {},
|
|
2472
2726
|
where: { type: filterArgs },
|
|
2473
2727
|
after: {
|
|
2474
2728
|
type: graphql.GraphQLString,
|
|
@@ -2480,7 +2734,7 @@ const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableN
|
|
|
2480
2734
|
/** GraphQL argument map for a single-row select field (no `limit`). */
|
|
2481
2735
|
const selectSingleArgs = (orderArgs, filterArgs, softDelete, tableName, resolveName) => ({
|
|
2482
2736
|
offset: { type: graphql.GraphQLInt },
|
|
2483
|
-
orderBy: { type: orderArgs },
|
|
2737
|
+
...orderArgs ? { orderBy: { type: orderArgs } } : {},
|
|
2484
2738
|
where: { type: filterArgs },
|
|
2485
2739
|
...deletedArg(softDelete, tableName, resolveName)
|
|
2486
2740
|
});
|
|
@@ -2530,7 +2784,7 @@ const generateDistinctEnum = (table, typeName, resolveName, tableKey) => {
|
|
|
2530
2784
|
defaultName,
|
|
2531
2785
|
table: tableKey,
|
|
2532
2786
|
operation: "distinct"
|
|
2533
|
-
}) ?? 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));
|
|
2534
2788
|
};
|
|
2535
2789
|
//#endregion
|
|
2536
2790
|
//#region src/util/builders/common/conflict.ts
|
|
@@ -2700,6 +2954,44 @@ const resolveConflictPlan = (params) => {
|
|
|
2700
2954
|
plan
|
|
2701
2955
|
}));
|
|
2702
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
|
+
};
|
|
2703
2995
|
/** `excluded.<column>` — PostgreSQL and SQLite name the rejected row this way. */
|
|
2704
2996
|
const excludedColumnRef = (columnName) => drizzle_orm.sql`excluded.${drizzle_orm.sql.identifier(columnName)}`;
|
|
2705
2997
|
/** `values(<column>)` — MySQL's equivalent inside ON DUPLICATE KEY UPDATE. */
|
|
@@ -2981,6 +3273,21 @@ const decodeCursor = (after, entries) => {
|
|
|
2981
3273
|
return payload.v.map(decodeCursorValue);
|
|
2982
3274
|
};
|
|
2983
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
|
+
/**
|
|
2984
3291
|
* The keyset predicate selecting rows strictly after the cursor position in the given total
|
|
2985
3292
|
* order — the expanded lexicographic form
|
|
2986
3293
|
* `after(k0) OR (k0 = v0 AND after(k1)) OR (k0 = v0 AND k1 = v1 AND after(k2)) …`,
|
|
@@ -3005,7 +3312,7 @@ const buildCursorCondition = (table, entries, values, nullOrdering) => {
|
|
|
3005
3312
|
let strictlyAfter;
|
|
3006
3313
|
if (value === null || value === void 0) strictlyAfter = nullsSortLast ? void 0 : (0, drizzle_orm.isNotNull)(col);
|
|
3007
3314
|
else {
|
|
3008
|
-
const comparison =
|
|
3315
|
+
const comparison = strictComparison(col, direction, value);
|
|
3009
3316
|
strictlyAfter = nullsSortLast ? (0, drizzle_orm.or)(comparison, (0, drizzle_orm.isNull)(col)) : comparison;
|
|
3010
3317
|
}
|
|
3011
3318
|
if (strictlyAfter) disjuncts.push(equalities.length ? (0, drizzle_orm.and)(...equalities, strictlyAfter) : strictlyAfter);
|
|
@@ -3208,6 +3515,115 @@ const getUniqueTargets = (table, getTableConfig) => {
|
|
|
3208
3515
|
*/
|
|
3209
3516
|
const getUniqueColumnSets = (table, getTableConfig, options = {}) => getUniqueTargets(table, getTableConfig).filter((target) => options.partial || !target.where).map((target) => target.columns);
|
|
3210
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
|
|
3211
3627
|
//#region src/util/builders/common/distinct.ts
|
|
3212
3628
|
/**
|
|
3213
3629
|
* Alias the parent key comes back under when the pass runs through a junction table. See
|
|
@@ -3255,13 +3671,15 @@ const selectDistinctKeys = async (params) => {
|
|
|
3255
3671
|
...junction ? { [DISTINCT_PARENT]: sub[DISTINCT_PARENT] } : {}
|
|
3256
3672
|
}).from(sub).where((0, drizzle_orm.eq)(sub[DISTINCT_RN], 1)).orderBy(...outerOrder);
|
|
3257
3673
|
if (offset) query = query.offset(offset);
|
|
3258
|
-
|
|
3674
|
+
const effectiveLimit = offsetOnlyLimit(table, limit, offset);
|
|
3675
|
+
if (effectiveLimit != null) query = query.limit(effectiveLimit < 0 ? drizzle_orm.sql`${effectiveLimit}` : effectiveLimit);
|
|
3259
3676
|
return await query;
|
|
3260
3677
|
};
|
|
3261
3678
|
/**
|
|
3262
3679
|
* Condition matching exactly the rows identified by `keys` — an `IN (…)` for a single-column
|
|
3263
|
-
* primary key, an `OR` of per-row equality for a composite one
|
|
3264
|
-
*
|
|
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.
|
|
3265
3683
|
*/
|
|
3266
3684
|
const primaryKeyRestriction = (table, pkNames, keys) => {
|
|
3267
3685
|
const cols = (0, drizzle_orm.getColumns)(table);
|
|
@@ -3269,7 +3687,7 @@ const primaryKeyRestriction = (table, pkNames, keys) => {
|
|
|
3269
3687
|
const name = pkNames[0];
|
|
3270
3688
|
return (0, drizzle_orm.inArray)(cols[name], keys.map((key) => key[name]));
|
|
3271
3689
|
}
|
|
3272
|
-
return (
|
|
3690
|
+
return balancedOr(keys.map((key) => (0, drizzle_orm.and)(...pkNames.map((name) => (0, drizzle_orm.eq)(cols[name], key[name])))));
|
|
3273
3691
|
};
|
|
3274
3692
|
//#endregion
|
|
3275
3693
|
//#region src/util/builders/common/docs.ts
|
|
@@ -3415,99 +3833,6 @@ const innerOrderType = (typeName) => sharedType(typeName, {
|
|
|
3415
3833
|
}
|
|
3416
3834
|
}));
|
|
3417
3835
|
//#endregion
|
|
3418
|
-
//#region src/util/builders/common/limits.ts
|
|
3419
|
-
/**
|
|
3420
|
-
* Substitutes a table's default ordering for an absent `orderBy`.
|
|
3421
|
-
*
|
|
3422
|
-
* Applied where the arguments are first read, so everything downstream — the cursor tuple, a
|
|
3423
|
-
* `distinct` pass, the plain-select fallback — sees one effective ordering and cannot
|
|
3424
|
-
* disagree about it. Only a missing argument is replaced: `orderBy: {}` is a request for no
|
|
3425
|
-
* ordering, and stays one.
|
|
3426
|
-
*/
|
|
3427
|
-
const withDefaultOrderBy = (args, tableName, defaults) => {
|
|
3428
|
-
if (!defaults || args?.orderBy != null) return args;
|
|
3429
|
-
const fallback = defaults(tableName);
|
|
3430
|
-
return fallback ? {
|
|
3431
|
-
...args,
|
|
3432
|
-
orderBy: fallback
|
|
3433
|
-
} : args;
|
|
3434
|
-
};
|
|
3435
|
-
/**
|
|
3436
|
-
* Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
|
|
3437
|
-
* dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
|
|
3438
|
-
* straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
|
|
3439
|
-
* whether or not the table has a limit policy.
|
|
3440
|
-
*/
|
|
3441
|
-
const assertPagingNotNegative = (paging, errorCtx) => {
|
|
3442
|
-
for (const name of ["limit", "offset"]) {
|
|
3443
|
-
const value = paging[name];
|
|
3444
|
-
if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
|
|
3445
|
-
code: "DRIZZLE_INVALID_INPUT_VALUE",
|
|
3446
|
-
...errorCtx
|
|
3447
|
-
});
|
|
3448
|
-
}
|
|
3449
|
-
};
|
|
3450
|
-
/**
|
|
3451
|
-
* The `limit` a query actually runs with, given the policy for the table it reads.
|
|
3452
|
-
*
|
|
3453
|
-
* A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
|
|
3454
|
-
* that is configured — an absent limit means every row, which is above any maximum. A limit
|
|
3455
|
-
* the client did ask for is rejected when it exceeds `maxLimit`, since silently truncating a
|
|
3456
|
-
* page tells a paginating client it has reached the end when it has not; `clampToMax` opts
|
|
3457
|
-
* into the truncation instead. A limit the *policy* supplied is capped rather than rejected —
|
|
3458
|
-
* a misconfigured `defaultLimit` above `maxLimit` is the operator's problem, not something to
|
|
3459
|
-
* fail a client's query over.
|
|
3460
|
-
*/
|
|
3461
|
-
const applyLimitPolicy = (limit, policy, errorCtx) => {
|
|
3462
|
-
assertPagingNotNegative({ limit }, errorCtx);
|
|
3463
|
-
if (!policy) return limit ?? void 0;
|
|
3464
|
-
const { defaultLimit, maxLimit, clampToMax } = policy;
|
|
3465
|
-
if (limit == null) {
|
|
3466
|
-
const fallback = defaultLimit ?? maxLimit;
|
|
3467
|
-
if (fallback === void 0) return;
|
|
3468
|
-
return maxLimit === void 0 ? fallback : Math.min(fallback, maxLimit);
|
|
3469
|
-
}
|
|
3470
|
-
if (maxLimit !== void 0 && limit > maxLimit) {
|
|
3471
|
-
if (clampToMax) return maxLimit;
|
|
3472
|
-
throw drizzleError(`'limit' of ${limit} exceeds the maximum of ${maxLimit}.`, {
|
|
3473
|
-
code: "DRIZZLE_LIMIT_EXCEEDED",
|
|
3474
|
-
...errorCtx
|
|
3475
|
-
});
|
|
3476
|
-
}
|
|
3477
|
-
return limit;
|
|
3478
|
-
};
|
|
3479
|
-
/** Merges a per-table override over the global policy, or `undefined` when neither bounds anything. */
|
|
3480
|
-
const resolveLimitPolicy = (global, table) => {
|
|
3481
|
-
const defaultLimit = table?.defaultLimit ?? global?.defaultLimit;
|
|
3482
|
-
const maxLimit = table?.maxLimit ?? global?.maxLimit;
|
|
3483
|
-
if (defaultLimit === void 0 && maxLimit === void 0) return;
|
|
3484
|
-
return {
|
|
3485
|
-
defaultLimit,
|
|
3486
|
-
maxLimit,
|
|
3487
|
-
clampToMax: table?.clampToMax ?? global?.clampToMax ?? false
|
|
3488
|
-
};
|
|
3489
|
-
};
|
|
3490
|
-
/**
|
|
3491
|
-
* A paginated field costs its page size times whatever one row of it costs, so `users(limit: 100)
|
|
3492
|
-
* { posts(limit: 10) { id } }` is charged for the thousand rows it can return rather than the two
|
|
3493
|
-
* fields it mentions. `childComplexity` floors at 1 so a row is never free.
|
|
3494
|
-
*
|
|
3495
|
-
* The page size is the *effective* one: a request without `limit` is priced at the policy's
|
|
3496
|
-
* `defaultLimit` rather than the estimator's guess, and no request is priced above `maxLimit`,
|
|
3497
|
-
* so the cost a query is charged matches the rows it can actually pull.
|
|
3498
|
-
*/
|
|
3499
|
-
const listFieldComplexity = (options, policy) => ({ args, childComplexity }) => {
|
|
3500
|
-
const limit = args["limit"];
|
|
3501
|
-
let rows = (typeof limit === "number" && limit > 0 ? limit : void 0) ?? policy?.defaultLimit ?? options.defaultListSize;
|
|
3502
|
-
if (policy?.maxLimit !== void 0) rows = Math.min(rows, policy.maxLimit);
|
|
3503
|
-
return rows * Math.max(childComplexity, 1);
|
|
3504
|
-
};
|
|
3505
|
-
/**
|
|
3506
|
-
* An aggregate returns a single row but reads however many match, so its cost tracks the scan
|
|
3507
|
-
* rather than the response.
|
|
3508
|
-
*/
|
|
3509
|
-
const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
|
|
3510
|
-
//#endregion
|
|
3511
3836
|
//#region src/util/parse-resolve-info.ts
|
|
3512
3837
|
/** Unwraps `[T]!`-style wrappers down to the named type. `getNamedType` without the realm tie. */
|
|
3513
3838
|
const namedTypeOf = (type) => {
|
|
@@ -3671,14 +3996,10 @@ const NUMERIC_SCALARS = /* @__PURE__ */ new Set([
|
|
|
3671
3996
|
]);
|
|
3672
3997
|
/**
|
|
3673
3998
|
* Column types that convert to a GraphQL list without being a list *in the database* —
|
|
3674
|
-
* a bytea reads as `[Int]`, a vector
|
|
3675
|
-
* 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.
|
|
3676
4001
|
*/
|
|
3677
|
-
const
|
|
3678
|
-
"PgVector",
|
|
3679
|
-
"PgGeometry",
|
|
3680
|
-
"PgBytea"
|
|
3681
|
-
]);
|
|
4002
|
+
const isNonListArrayColumn = (column) => isFloatTupleColumn(column) || isBufferColumn(column);
|
|
3682
4003
|
const kindCache = /* @__PURE__ */ new WeakMap();
|
|
3683
4004
|
/**
|
|
3684
4005
|
* Whether a column takes an operations input, and which one.
|
|
@@ -3695,7 +4016,7 @@ const fieldUpdateKind = (column, columnName, tableName) => {
|
|
|
3695
4016
|
if (!getColumnScalarOverride(column, true)) {
|
|
3696
4017
|
const { type } = drizzleColumnToGraphQLType(column, columnName, tableName, true, false, true);
|
|
3697
4018
|
if (NUMERIC_SCALARS.has(type)) kind = "numeric";
|
|
3698
|
-
else if (type instanceof graphql.GraphQLList && !
|
|
4019
|
+
else if (type instanceof graphql.GraphQLList && !isNonListArrayColumn(column)) kind = "list";
|
|
3699
4020
|
}
|
|
3700
4021
|
kindCache.set(column, kind);
|
|
3701
4022
|
return kind;
|
|
@@ -3966,6 +4287,16 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
|
|
|
3966
4287
|
if (!relFieldSelection) continue;
|
|
3967
4288
|
const eagerArgs = field.args;
|
|
3968
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
|
+
}
|
|
3969
4300
|
const columns = extractSelectedColumnsFromTree(relFieldSelection, tables[targetTableName], {
|
|
3970
4301
|
tableName: targetTableName,
|
|
3971
4302
|
relationMap,
|
|
@@ -3991,9 +4322,11 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
|
|
|
3991
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;
|
|
3992
4323
|
const hasPagination = offset != null || limit != null;
|
|
3993
4324
|
const pkNames = targetPkNames ?? [];
|
|
3994
|
-
|
|
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;
|
|
3995
4328
|
thisRecord.offset = offset;
|
|
3996
|
-
thisRecord.limit = limit;
|
|
4329
|
+
thisRecord.limit = offsetOnlyLimit(tables[targetTableName], limit, offset);
|
|
3997
4330
|
thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
|
|
3998
4331
|
args[relName] = omitUndefinedKeys(thisRecord);
|
|
3999
4332
|
}
|
|
@@ -4084,7 +4417,7 @@ const runRelationalSelect = async (opts) => {
|
|
|
4084
4417
|
const preciseProps = Object.keys(cursorPreciseExtras(table, cursorEntries));
|
|
4085
4418
|
if (preciseProps.length) params.extras = Object.fromEntries(preciseProps.map((prop) => [prop, (aliasedTable) => cursorPreciseExtras(aliasedTable, cursorEntries)[prop]]));
|
|
4086
4419
|
}
|
|
4087
|
-
params.limit = distinctKeys ? void 0 : opts.limit;
|
|
4420
|
+
params.limit = distinctKeys ? void 0 : offsetOnlyLimit(table, opts.limit, offset);
|
|
4088
4421
|
const result = await queryBase.findMany(omitUndefinedKeys(params));
|
|
4089
4422
|
if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
|
|
4090
4423
|
return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
|
|
@@ -4141,6 +4474,8 @@ const eagerLoadMutationRelations = async (db, tableName, rows, pkNames, withPara
|
|
|
4141
4474
|
}
|
|
4142
4475
|
return rows;
|
|
4143
4476
|
};
|
|
4477
|
+
/** The longest delay `setTimeout` honours; anything above, Infinity included, fires after 1 ms. */
|
|
4478
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
4144
4479
|
const createMutationTxCtx = (options) => options ? {
|
|
4145
4480
|
timeoutMs: options.timeoutMs,
|
|
4146
4481
|
fieldNames: /* @__PURE__ */ new Set(),
|
|
@@ -4167,28 +4502,28 @@ const includedByDirectives = (node, variables) => {
|
|
|
4167
4502
|
* this build did not generate — its completion cannot be tracked, so the caller must not
|
|
4168
4503
|
* open a transaction it might never be able to close.
|
|
4169
4504
|
*/
|
|
4170
|
-
const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments) => {
|
|
4505
|
+
const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce) => {
|
|
4171
4506
|
for (const selection of selectionSet.selections) {
|
|
4172
4507
|
if (!includedByDirectives(selection, variables)) continue;
|
|
4173
4508
|
if (selection.kind === "Field") {
|
|
4174
4509
|
const name = selection.name.value;
|
|
4175
4510
|
if (name === "__typename") continue;
|
|
4176
4511
|
if (!known.has(name)) return false;
|
|
4177
|
-
keys.add(selection.alias?.value ?? name);
|
|
4512
|
+
if (argumentsCoerce(selection)) keys.add(selection.alias?.value ?? name);
|
|
4178
4513
|
} else if (selection.kind === "InlineFragment") {
|
|
4179
|
-
if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
|
|
4514
|
+
if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
|
|
4180
4515
|
} else if (selection.kind === "FragmentSpread") {
|
|
4181
4516
|
const fragmentName = selection.name.value;
|
|
4182
4517
|
if (visitedFragments.has(fragmentName)) continue;
|
|
4183
4518
|
visitedFragments.add(fragmentName);
|
|
4184
4519
|
const fragment = fragments?.[fragmentName];
|
|
4185
4520
|
if (!fragment) return false;
|
|
4186
|
-
if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
|
|
4521
|
+
if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
|
|
4187
4522
|
}
|
|
4188
4523
|
}
|
|
4189
4524
|
return true;
|
|
4190
4525
|
};
|
|
4191
|
-
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" });
|
|
4192
4527
|
/** Opens the request-wide transaction and wires up latch, inactivity timer and cleanup. */
|
|
4193
4528
|
const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
4194
4529
|
let release;
|
|
@@ -4223,6 +4558,7 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
|
4223
4558
|
completed: 0,
|
|
4224
4559
|
failure: void 0,
|
|
4225
4560
|
aborted: false,
|
|
4561
|
+
timedOut: false,
|
|
4226
4562
|
done: false,
|
|
4227
4563
|
ready,
|
|
4228
4564
|
settled,
|
|
@@ -4237,8 +4573,9 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
|
4237
4573
|
suspendTimer: clearTimer,
|
|
4238
4574
|
bumpTimer: () => {
|
|
4239
4575
|
clearTimer();
|
|
4240
|
-
if (state.done) return;
|
|
4576
|
+
if (state.done || txCtx.timeoutMs === Number.POSITIVE_INFINITY) return;
|
|
4241
4577
|
timer = setTimeout(() => {
|
|
4578
|
+
if (!state.aborted) state.timedOut = true;
|
|
4242
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" }));
|
|
4243
4580
|
}, txCtx.timeoutMs);
|
|
4244
4581
|
timer.unref?.();
|
|
@@ -4257,15 +4594,15 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
|
4257
4594
|
state.bumpTimer();
|
|
4258
4595
|
return state;
|
|
4259
4596
|
};
|
|
4260
|
-
const runOnSharedTx = async (state, body) => {
|
|
4597
|
+
const runOnSharedTx = async (state, timeoutMs, body) => {
|
|
4261
4598
|
if (state.aborted) {
|
|
4262
4599
|
await state.settled.catch(() => {});
|
|
4263
|
-
throw abortedBatchError();
|
|
4600
|
+
throw abortedBatchError(state, timeoutMs);
|
|
4264
4601
|
}
|
|
4265
4602
|
state.suspendTimer();
|
|
4266
4603
|
const tx = await state.ready;
|
|
4267
4604
|
try {
|
|
4268
|
-
if (state.aborted) throw abortedBatchError();
|
|
4605
|
+
if (state.aborted) throw abortedBatchError(state, timeoutMs);
|
|
4269
4606
|
const result = await body(tx);
|
|
4270
4607
|
state.completed += 1;
|
|
4271
4608
|
if (state.completed >= state.expected) {
|
|
@@ -4298,14 +4635,24 @@ const runMutation = async (db, context, info, txCtx, body, forceTx) => {
|
|
|
4298
4635
|
if (contextIsObject && context[drizzleExecutorKey]) return body(context[drizzleExecutorKey]);
|
|
4299
4636
|
if (!txCtx || info.operation.operation !== "mutation") return forceTx ? db.transaction((tx) => body(tx)) : body(db);
|
|
4300
4637
|
const keys = /* @__PURE__ */ new Set();
|
|
4301
|
-
|
|
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);
|
|
4302
4649
|
txCtx.states.set(stateKey, state);
|
|
4303
4650
|
}
|
|
4304
4651
|
if (state.kind === "passthrough") {
|
|
4305
4652
|
const executor = resolveExecutor(db, context);
|
|
4306
4653
|
return forceTx && executor === db ? db.transaction((tx) => body(tx)) : body(executor);
|
|
4307
4654
|
}
|
|
4308
|
-
return runOnSharedTx(state, body);
|
|
4655
|
+
return runOnSharedTx(state, txCtx.timeoutMs, body);
|
|
4309
4656
|
};
|
|
4310
4657
|
//#endregion
|
|
4311
4658
|
//#region src/util/builders/common/write-hooks.ts
|
|
@@ -4459,10 +4806,11 @@ const hardDeleteArg = {
|
|
|
4459
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."
|
|
4460
4807
|
};
|
|
4461
4808
|
const prepareMutationRelationColumns = (params) => {
|
|
4462
|
-
const { relationMap, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
|
|
4809
|
+
const { relationMap, filterCtx, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
|
|
4463
4810
|
const typeName = resolveObjectTypeName(tableName, typeNameMapper, resolveName);
|
|
4464
4811
|
const withParams = relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, {
|
|
4465
4812
|
typeNameMapper,
|
|
4813
|
+
filterCtx,
|
|
4466
4814
|
limits: params.limits,
|
|
4467
4815
|
scope: params.scope,
|
|
4468
4816
|
defaultOrderBy: params.defaultOrderBy,
|
|
@@ -4472,7 +4820,8 @@ const prepareMutationRelationColumns = (params) => {
|
|
|
4472
4820
|
const baseColumns = extractSelectedColumnsFromTreeSQLFormat(parsedInfo.fieldsByTypeName[typeName], table, {
|
|
4473
4821
|
tableName,
|
|
4474
4822
|
relationMap,
|
|
4475
|
-
tables
|
|
4823
|
+
tables,
|
|
4824
|
+
allRelations: filterCtx?.relationMap
|
|
4476
4825
|
});
|
|
4477
4826
|
return {
|
|
4478
4827
|
columns: hasRelations ? withPrimaryKeyColumns(baseColumns, table, pkNames) : baseColumns,
|
|
@@ -4608,7 +4957,7 @@ const assertSingleMatch = async (executor, table, filters) => {
|
|
|
4608
4957
|
* MySQL (no `RETURNING`; its hooks see `rows: []` as on every MySQL mutation), skips the
|
|
4609
4958
|
* `RETURNING` and reads the count off the driver's result via {@link rowsAffected}.
|
|
4610
4959
|
*/
|
|
4611
|
-
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 }) => {
|
|
4612
4961
|
const softDelete = kind === "delete" ? policies?.softDelete?.(tableName) : void 0;
|
|
4613
4962
|
const canHardDelete = softDelete?.hardDelete === true;
|
|
4614
4963
|
const queryArgs = {
|
|
@@ -4651,7 +5000,7 @@ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, f
|
|
|
4651
5000
|
required: requireWhere,
|
|
4652
5001
|
deleted: hard ? "INCLUDE" : void 0
|
|
4653
5002
|
});
|
|
4654
|
-
if (filters) query = query.where(filters);
|
|
5003
|
+
if (filters) query = query.where(writeFilter ? writeFilter(filters) : filters);
|
|
4655
5004
|
if (!readsRows) {
|
|
4656
5005
|
const result = await query;
|
|
4657
5006
|
await after();
|
|
@@ -4711,27 +5060,40 @@ const getOrCreateLoader = (context, key, batchFn) => {
|
|
|
4711
5060
|
if (!loaders.has(key)) loaders.set(key, new BatchLoader(batchFn));
|
|
4712
5061
|
return loaders.get(key);
|
|
4713
5062
|
};
|
|
5063
|
+
const requestCacheNode = () => ({
|
|
5064
|
+
children: /* @__PURE__ */ new WeakMap(),
|
|
5065
|
+
entries: /* @__PURE__ */ new Map()
|
|
5066
|
+
});
|
|
4714
5067
|
/**
|
|
4715
5068
|
* Memoizes a value that a resolver derives from its own field — its args and its selection —
|
|
4716
5069
|
* for the length of one request. Resolvers run once per parent row, so anything derived only
|
|
4717
5070
|
* from the field is otherwise recomputed for every row of a batch.
|
|
4718
5071
|
*
|
|
4719
|
-
* The
|
|
4720
|
-
*
|
|
4721
|
-
*
|
|
4722
|
-
*
|
|
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.
|
|
4723
5081
|
*
|
|
4724
5082
|
* `compute` must not read the parent row.
|
|
4725
5083
|
*/
|
|
4726
|
-
const getOrCreateRequestValue = (context,
|
|
4727
|
-
if (!context || typeof context !== "object" || !node || typeof node !== "object") return compute();
|
|
4728
|
-
if (!context[DRIZZLE_REQUEST_CACHE_KEY]) context[DRIZZLE_REQUEST_CACHE_KEY] =
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
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;
|
|
4734
5095
|
}
|
|
5096
|
+
const entries = level.entries;
|
|
4735
5097
|
if (entries.has(key)) return entries.get(key);
|
|
4736
5098
|
const value = compute();
|
|
4737
5099
|
entries.set(key, value);
|
|
@@ -4961,11 +5323,12 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4961
5323
|
if (!parentTable || !targetTable) return;
|
|
4962
5324
|
const joinPairs = extractRelationJoinColumnPairs(relEntry, parentTable, targetTable);
|
|
4963
5325
|
if (!joinPairs) return;
|
|
4964
|
-
const
|
|
5326
|
+
const localCols = joinPairs.map((pair) => pair.localCol);
|
|
4965
5327
|
const foreignCol = joinPairs[0].foreignCol;
|
|
4966
5328
|
const junction = extractJunctionColumns(relEntry);
|
|
4967
5329
|
const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
|
|
4968
5330
|
const groupKeys = junction ? [PARENT_KEY] : joinPairs.map((pair) => pair.foreignColPropName);
|
|
5331
|
+
const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
|
|
4969
5332
|
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
|
|
4970
5333
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, isOne && relEntry.relation?.optional === false);
|
|
4971
5334
|
const limitPolicy = isOne ? void 0 : limits?.(targetTableName);
|
|
@@ -5007,10 +5370,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
5007
5370
|
};
|
|
5008
5371
|
const resolve = async (parent, args, context, info) => {
|
|
5009
5372
|
if (parent[relationName] !== void 0) return parent[relationName];
|
|
5010
|
-
const localValues =
|
|
5373
|
+
const localValues = parentJoinValues(parent, joinPairs);
|
|
5011
5374
|
if (localValues.some((value) => value == null)) return isOne ? null : [];
|
|
5012
5375
|
const { where: whereArg, limit: requestedLimit, offset, after, deleted } = args ?? {};
|
|
5013
|
-
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}`, () => {
|
|
5014
5377
|
const orderByArg = isOne ? args?.orderBy : withDefaultOrderBy(args ?? {}, targetTableName, policies?.defaultOrderBy).orderBy;
|
|
5015
5378
|
const limit = applyLimitPolicy(requestedLimit, limitPolicy, errorCtx);
|
|
5016
5379
|
assertPagingNotNegative({ offset }, errorCtx);
|
|
@@ -5051,55 +5414,60 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
5051
5414
|
if (junction === null) throw drizzleError(`Relation '${tableName}.${relationName}' goes through a junction with a composite key, which can only be loaded together with its parent. Leave it to eager loading (eagerLoadRelations).`, { code: "DRIZZLE_UNSUPPORTED_RELATION" });
|
|
5052
5415
|
return getOrCreateLoader(context, loaderKey, async (parentKeys) => {
|
|
5053
5416
|
const executor = resolveExecutor(db, context);
|
|
5054
|
-
const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key), key])).values()];
|
|
5417
|
+
const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key, localCols), key])).values()];
|
|
5055
5418
|
const scope = resolveScope(policies, context, filterCtx);
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
const
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5419
|
+
const loadChunk = async (chunkKeys) => {
|
|
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);
|
|
5421
|
+
if (distinct) {
|
|
5422
|
+
const keys = await selectDistinctKeys({
|
|
5423
|
+
db: executor,
|
|
5424
|
+
table: targetTable,
|
|
5425
|
+
tableName: targetTableName,
|
|
5426
|
+
distinct,
|
|
5427
|
+
pkNames: targetPkNames,
|
|
5428
|
+
where: whereCondition,
|
|
5429
|
+
orderBy: orderByArg,
|
|
5430
|
+
partitionBy: groupCols,
|
|
5431
|
+
junction: junction ? {
|
|
5432
|
+
table: junction.table,
|
|
5433
|
+
on: (0, drizzle_orm.eq)(junction.targetCol, foreignCol),
|
|
5434
|
+
parentKey: junction.sourceCol
|
|
5435
|
+
} : void 0
|
|
5436
|
+
});
|
|
5437
|
+
if (!keys.length) return /* @__PURE__ */ new Map();
|
|
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));
|
|
5439
|
+
}
|
|
5440
|
+
const selection = junction ? {
|
|
5441
|
+
...columns ?? (0, drizzle_orm.getColumns)(targetTable),
|
|
5442
|
+
[PARENT_KEY]: junction.sourceCol
|
|
5443
|
+
} : columns;
|
|
5444
|
+
let rows;
|
|
5445
|
+
if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, groupCols, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName, scope), whereArg, junction);
|
|
5446
|
+
else {
|
|
5447
|
+
let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
|
|
5448
|
+
const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName, scope), whereArg) : [];
|
|
5449
|
+
if (orderExprs.length) q = q.orderBy(...orderExprs);
|
|
5450
|
+
rows = await q;
|
|
5451
|
+
}
|
|
5452
|
+
if (cursorEntries) attachRowCursors(rows, cursorEntries);
|
|
5453
|
+
const keyOf = (row) => {
|
|
5454
|
+
const key = joinKeyOf(groupKeys.map((name) => row[name]), groupCols);
|
|
5455
|
+
if (junction) delete row[PARENT_KEY];
|
|
5456
|
+
return key;
|
|
5457
|
+
};
|
|
5458
|
+
if (isOne) {
|
|
5459
|
+
const byKey = new Map(rows.map((row) => [keyOf(row), row]));
|
|
5460
|
+
remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
|
|
5461
|
+
return byKey;
|
|
5462
|
+
}
|
|
5463
|
+
const grouped = new Map(chunkKeys.map((key) => [joinKeyOf(key, localCols), []]));
|
|
5464
|
+
for (const row of rows) grouped.get(keyOf(row))?.push(row);
|
|
5096
5465
|
remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
|
|
5097
|
-
return
|
|
5098
|
-
}
|
|
5099
|
-
const
|
|
5100
|
-
for (const
|
|
5101
|
-
|
|
5102
|
-
return parentKeys.map((key) => grouped.get(joinKeyOf(key)) ?? []);
|
|
5466
|
+
return grouped;
|
|
5467
|
+
};
|
|
5468
|
+
const results = /* @__PURE__ */ new Map();
|
|
5469
|
+
for (const chunk of parentKeyChunks(uniqueKeys, groupCols.length)) for (const [key, value] of await loadChunk(chunk)) results.set(key, value);
|
|
5470
|
+
return parentKeys.map((key) => results.get(joinKeyOf(key, localCols)) ?? (isOne ? null : []));
|
|
5103
5471
|
}).load(localValues);
|
|
5104
5472
|
};
|
|
5105
5473
|
return async (parent, args, context, info) => {
|
|
@@ -5204,7 +5572,7 @@ const generateTableOrderCached = (table, innerOrder) => {
|
|
|
5204
5572
|
try {
|
|
5205
5573
|
const columns = visibleColumns(table);
|
|
5206
5574
|
const columnEntries = Object.entries(columns);
|
|
5207
|
-
remapped = Object.fromEntries(columnEntries.
|
|
5575
|
+
remapped = Object.fromEntries(columnEntries.filter(([, column]) => isOrderableColumn(column)).map(([columnName]) => [columnName, { type: innerOrder }]));
|
|
5208
5576
|
if (cacheable) {
|
|
5209
5577
|
let byInnerOrder = orderMap.get(table);
|
|
5210
5578
|
if (!byInnerOrder) {
|
|
@@ -5247,43 +5615,71 @@ const generateTableSelectTypeFieldsCached = (table, tableName, cacheCtx) => {
|
|
|
5247
5615
|
return remapped;
|
|
5248
5616
|
};
|
|
5249
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
|
+
/**
|
|
5250
5647
|
* Order fields for a table's to-one relations: each takes the target table's own OrderBy
|
|
5251
5648
|
* input, so a list can be sorted by a related row's column (compiled as a correlated
|
|
5252
|
-
* subquery in the ORDER BY — see `extractOrderBy`).
|
|
5253
|
-
*
|
|
5254
|
-
* 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.
|
|
5255
5651
|
*/
|
|
5256
|
-
const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper,
|
|
5257
|
-
const relations = relationMap?.[tableName];
|
|
5258
|
-
if (!relations || !tables) return {};
|
|
5652
|
+
const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper, relationMap, tables) => {
|
|
5259
5653
|
const fields = {};
|
|
5260
|
-
for (const [relationName,
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
if (!targetTable || !(0, drizzle_orm.is)(relation, drizzle_orm.One) || !isFilterableRelation(relation)) continue;
|
|
5265
|
-
fields[relationName] = {
|
|
5266
|
-
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,
|
|
5267
5658
|
description: `Order by columns of the related ${relationName} row`
|
|
5268
5659
|
};
|
|
5269
5660
|
}
|
|
5270
5661
|
return fields;
|
|
5271
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
|
+
*/
|
|
5272
5667
|
const generateTableOrderTypeCached = (table, tableName, typeNameMapper, cacheCtx, relationMap, tables) => {
|
|
5273
5668
|
if (cacheCtx.orderTypeCache.has(table)) return cacheCtx.orderTypeCache.get(table);
|
|
5669
|
+
if (!hasOrderFields(tableName, relationMap, {
|
|
5670
|
+
...tables,
|
|
5671
|
+
[tableName]: table
|
|
5672
|
+
})) return;
|
|
5274
5673
|
const order = new graphql.GraphQLInputObjectType({
|
|
5275
5674
|
name: cacheCtx.typeName({
|
|
5276
5675
|
kind: "orderBy",
|
|
5277
5676
|
defaultName: `${resolveTypeName(tableName, typeNameMapper)}OrderBy`,
|
|
5278
5677
|
table: tableName
|
|
5279
5678
|
}),
|
|
5280
|
-
fields: () => {
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, orderColumns, relationMap, tables)
|
|
5285
|
-
};
|
|
5286
|
-
}
|
|
5679
|
+
fields: () => ({
|
|
5680
|
+
...generateTableOrderCached(table, innerOrderType(cacheCtx.typeName)),
|
|
5681
|
+
...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, relationMap, tables)
|
|
5682
|
+
})
|
|
5287
5683
|
});
|
|
5288
5684
|
cacheCtx.orderTypeCache.set(table, order);
|
|
5289
5685
|
return order;
|
|
@@ -5502,7 +5898,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
|
|
|
5502
5898
|
type: new graphql.GraphQLNonNull(new graphql.GraphQLList(new graphql.GraphQLNonNull(relType))),
|
|
5503
5899
|
args: {
|
|
5504
5900
|
where: { type: relSelectData.filters },
|
|
5505
|
-
orderBy: { type: relSelectData.order },
|
|
5901
|
+
...relSelectData.order ? { orderBy: { type: relSelectData.order } } : {},
|
|
5506
5902
|
offset: { type: graphql.GraphQLInt },
|
|
5507
5903
|
limit: { type: graphql.GraphQLInt },
|
|
5508
5904
|
...deletedArg(cacheCtx.softDeleteOf, targetTableName, cacheCtx.typeName),
|
|
@@ -5730,6 +6126,7 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5730
6126
|
};
|
|
5731
6127
|
const transactionsOpt = config?.transactions;
|
|
5732
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}.`);
|
|
5733
6130
|
const limitsConfig = config?.limits;
|
|
5734
6131
|
if (limitsConfig) {
|
|
5735
6132
|
const check = (policy, where) => {
|
|
@@ -5762,6 +6159,7 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5762
6159
|
for (const [columnName, entry] of Object.entries(orderBy)) {
|
|
5763
6160
|
if (!columns[columnName]) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which is not a column of '${tableName}'.`);
|
|
5764
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.`);
|
|
5765
6163
|
const direction = typeof entry === "string" ? entry : entry?.direction;
|
|
5766
6164
|
if (direction !== "asc" && direction !== "desc") throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy.${columnName} must be 'asc', 'desc', or { direction, priority? }.`);
|
|
5767
6165
|
const priority = typeof entry === "string" ? void 0 : entry?.priority;
|
|
@@ -5820,7 +6218,7 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5820
6218
|
}
|
|
5821
6219
|
}
|
|
5822
6220
|
}
|
|
5823
|
-
const onWrite = writeHooks.
|
|
6221
|
+
const onWrite = [...writeHooks.values()].some(Boolean) ? (tableName) => writeHooks.get(tableName) : void 0;
|
|
5824
6222
|
const policies = scopeConfig || contextValuesConfig || softDeleteInfos.size || onWrite || defaultOrderBy ? {
|
|
5825
6223
|
scope: scopeConfig ? (tableName) => scopeConfig[tableName] : void 0,
|
|
5826
6224
|
contextValues: contextValuesConfig ? (tableName) => contextValuesConfig[tableName] : void 0,
|
|
@@ -5911,8 +6309,14 @@ const AGGREGATE_OPS = [
|
|
|
5911
6309
|
];
|
|
5912
6310
|
/** Ops whose result is a row count: never null, and returned as `Int!` rather than the column's type. */
|
|
5913
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);
|
|
5914
6318
|
const OP_FNS = {
|
|
5915
|
-
avg:
|
|
6319
|
+
avg: avgOf,
|
|
5916
6320
|
sum: drizzle_orm.sum,
|
|
5917
6321
|
min: drizzle_orm.min,
|
|
5918
6322
|
max: drizzle_orm.max,
|
|
@@ -5922,6 +6326,18 @@ const OP_FNS = {
|
|
|
5922
6326
|
/** Separator for flat select aliases (`avg__price`) — reassembled into nested output by the resolver. */
|
|
5923
6327
|
const SEP = "__";
|
|
5924
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
|
+
/**
|
|
5925
6341
|
* Classifies a table's columns for aggregation. avg/sum only make sense on numeric scalars;
|
|
5926
6342
|
* min/max work on any orderable scalar (numbers, strings, bigints, dates, enums). Booleans,
|
|
5927
6343
|
* arrays (including array-typed number columns), JSON, buffers, and object-shaped columns
|
|
@@ -5953,6 +6369,7 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
5953
6369
|
if (cached) return cached;
|
|
5954
6370
|
const numeric = {};
|
|
5955
6371
|
const orderable = {};
|
|
6372
|
+
const comparable = {};
|
|
5956
6373
|
const all = {};
|
|
5957
6374
|
for (const [columnName, column] of Object.entries(visibleColumns(table))) {
|
|
5958
6375
|
all[columnName] = column;
|
|
@@ -5962,7 +6379,11 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
5962
6379
|
const { type: dataType, constraint } = (0, drizzle_orm.extractExtendedColumnType)(column);
|
|
5963
6380
|
if (dataType === "boolean" || dataType === "array" || dataType === "custom") continue;
|
|
5964
6381
|
if (dataType === "object" && constraint !== "date") continue;
|
|
5965
|
-
|
|
6382
|
+
comparable[columnName] = {
|
|
6383
|
+
column,
|
|
6384
|
+
converted
|
|
6385
|
+
};
|
|
6386
|
+
if (!NO_MIN_MAX_COLUMN_TYPES.has(column.columnType)) orderable[columnName] = {
|
|
5966
6387
|
column,
|
|
5967
6388
|
converted
|
|
5968
6389
|
};
|
|
@@ -5971,6 +6392,7 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
5971
6392
|
const sets = {
|
|
5972
6393
|
numeric,
|
|
5973
6394
|
orderable,
|
|
6395
|
+
comparable,
|
|
5974
6396
|
all
|
|
5975
6397
|
};
|
|
5976
6398
|
byName.set(tableName, sets);
|
|
@@ -5983,13 +6405,13 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
5983
6405
|
* and avg/sum of integers overflow Int / produce decimals)
|
|
5984
6406
|
* - `min` / `max` — per orderable column, the column's own (nullable) scalar type
|
|
5985
6407
|
* - `countNonNull` — per column, `Int!`: how many matching rows have a non-null value there
|
|
5986
|
-
* - `countDistinct` — per
|
|
6408
|
+
* - `countDistinct` — per comparable column, `Int!`: how many distinct non-null values there are
|
|
5987
6409
|
* Each wrapper is omitted when no column qualifies for it.
|
|
5988
6410
|
*/
|
|
5989
6411
|
const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
|
|
5990
6412
|
const cached = cacheCtx?.aggregateTypeCache.get(tableName);
|
|
5991
6413
|
if (cached) return cached;
|
|
5992
|
-
const { numeric, orderable, all } = classifyAggregateColumns(table, tableName);
|
|
6414
|
+
const { numeric, orderable, comparable, all } = classifyAggregateColumns(table, tableName);
|
|
5993
6415
|
const nameOf = (op) => cacheCtx?.typeName({
|
|
5994
6416
|
kind: "aggregate",
|
|
5995
6417
|
defaultName: `${typeName}${capitalize(op)}Aggregate`,
|
|
@@ -6010,7 +6432,7 @@ const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
|
|
|
6010
6432
|
}) };
|
|
6011
6433
|
const countSets = {
|
|
6012
6434
|
countNonNull: all,
|
|
6013
|
-
countDistinct:
|
|
6435
|
+
countDistinct: comparable
|
|
6014
6436
|
};
|
|
6015
6437
|
for (const [op, columns] of Object.entries(countSets)) {
|
|
6016
6438
|
const columnNames = Object.keys(columns);
|
|
@@ -6185,11 +6607,12 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
6185
6607
|
}
|
|
6186
6608
|
return;
|
|
6187
6609
|
}
|
|
6188
|
-
const
|
|
6610
|
+
const localCols = joinPairs.map((pair) => pair.localCol);
|
|
6189
6611
|
const foreignCol = joinPairs[0].foreignCol;
|
|
6190
6612
|
const junction = extractJunctionColumns(relEntry);
|
|
6191
6613
|
if (junction === null) return;
|
|
6192
6614
|
const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
|
|
6615
|
+
const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
|
|
6193
6616
|
const groupAliases = groupCols.map((_, i) => `${GROUP_KEY}${i}`);
|
|
6194
6617
|
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
|
|
6195
6618
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
|
|
@@ -6203,7 +6626,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
6203
6626
|
};
|
|
6204
6627
|
const resolve = async (parent, args, context, info) => {
|
|
6205
6628
|
try {
|
|
6206
|
-
const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes
|
|
6629
|
+
const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes, `aggregate:${tableName}::${relationName}`, () => {
|
|
6207
6630
|
const request = parseAggregateRequest(info, target);
|
|
6208
6631
|
return {
|
|
6209
6632
|
request,
|
|
@@ -6215,23 +6638,26 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
6215
6638
|
};
|
|
6216
6639
|
});
|
|
6217
6640
|
if (!Object.keys(request.selection).length) return {};
|
|
6218
|
-
const localValues =
|
|
6641
|
+
const localValues = parentJoinValues(parent, joinPairs);
|
|
6219
6642
|
if (localValues.some((value) => value == null)) return assembleAggregateRow({}, request, target);
|
|
6220
6643
|
const whereArg = args?.where;
|
|
6221
6644
|
const deleted = args?.deleted;
|
|
6222
6645
|
const loader = getOrCreateLoader(context, loaderKey, async (parentKeys) => {
|
|
6223
6646
|
const executor = resolveExecutor(db, context);
|
|
6224
|
-
const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key), key])).values()];
|
|
6647
|
+
const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key, localCols), key])).values()];
|
|
6225
6648
|
const scope = resolveScope(policies, context, filterCtx);
|
|
6226
|
-
const whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)(parentKeyCondition(groupCols, uniqueKeys), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
|
|
6227
6649
|
const groupSelection = Object.fromEntries(groupAliases.map((alias, i) => [alias, groupCols[i]]));
|
|
6228
|
-
const
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6650
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
6651
|
+
for (const chunk of parentKeyChunks(uniqueKeys, groupCols.length)) {
|
|
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);
|
|
6653
|
+
const selected = executor.select({
|
|
6654
|
+
...groupSelection,
|
|
6655
|
+
...request.selection
|
|
6656
|
+
}).from(targetTable);
|
|
6657
|
+
const rows = await (junction ? selected.innerJoin(junction.table, (0, drizzle_orm.eq)(junction.targetCol, foreignCol)) : selected).where(whereCondition).groupBy(...groupCols);
|
|
6658
|
+
for (const row of rows) byKey.set(joinKeyOf(groupAliases.map((alias) => row[alias]), groupCols), row);
|
|
6659
|
+
}
|
|
6660
|
+
return parentKeys.map((key) => byKey.get(joinKeyOf(key, localCols)) ?? {});
|
|
6235
6661
|
});
|
|
6236
6662
|
return assembleAggregateRow(await loader.load(localValues), request, target);
|
|
6237
6663
|
} catch (e) {
|
|
@@ -6257,12 +6683,12 @@ const HAVING_OPS = {
|
|
|
6257
6683
|
};
|
|
6258
6684
|
/**
|
|
6259
6685
|
* Columns a query can group on. Grouping needs an equality operator, which is the same
|
|
6260
|
-
* requirement
|
|
6686
|
+
* requirement countDistinct has, plus booleans — comparable excludes them because a min/max over
|
|
6261
6687
|
* true/false says nothing, but `GROUP BY isConfirmed` is perfectly ordinary.
|
|
6262
6688
|
*/
|
|
6263
6689
|
const groupableColumns = (table, tableName) => {
|
|
6264
|
-
const {
|
|
6265
|
-
const groupable = { ...
|
|
6690
|
+
const { comparable } = classifyAggregateColumns(table, tableName);
|
|
6691
|
+
const groupable = { ...comparable };
|
|
6266
6692
|
for (const [columnName, column] of Object.entries(visibleColumns(table))) {
|
|
6267
6693
|
if (groupable[columnName]) continue;
|
|
6268
6694
|
const { type: dataType } = (0, drizzle_orm.extractExtendedColumnType)(column);
|
|
@@ -6303,7 +6729,7 @@ const aggregateNumberFilterType = (typeName) => sharedType(typeName, {
|
|
|
6303
6729
|
* nothing to compare against.
|
|
6304
6730
|
*/
|
|
6305
6731
|
const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
|
|
6306
|
-
const { numeric,
|
|
6732
|
+
const { numeric, comparable, all } = classifyAggregateColumns(table, tableName);
|
|
6307
6733
|
const resolveName = cacheCtx?.typeName ?? ((info) => info.defaultName);
|
|
6308
6734
|
const numberFilter = aggregateNumberFilterType(resolveName);
|
|
6309
6735
|
const fields = { count: {
|
|
@@ -6316,7 +6742,7 @@ const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
|
|
|
6316
6742
|
min: numeric,
|
|
6317
6743
|
max: numeric,
|
|
6318
6744
|
countNonNull: all,
|
|
6319
|
-
countDistinct:
|
|
6745
|
+
countDistinct: comparable
|
|
6320
6746
|
};
|
|
6321
6747
|
for (const [op, columns] of Object.entries(opColumns)) {
|
|
6322
6748
|
const columnNames = Object.keys(columns);
|
|
@@ -6381,22 +6807,34 @@ const generateGroupByType = (table, tableName, typeName, cacheCtx) => {
|
|
|
6381
6807
|
}
|
|
6382
6808
|
});
|
|
6383
6809
|
};
|
|
6384
|
-
/**
|
|
6385
|
-
|
|
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
|
+
};
|
|
6386
6822
|
const buildHavingCondition = (having, columns) => {
|
|
6387
6823
|
if (!having) return;
|
|
6388
6824
|
const conditions = [];
|
|
6825
|
+
const firstColumn = Object.values(columns)[0];
|
|
6826
|
+
const isPg = firstColumn ? columnDialect(firstColumn) === "pg" : false;
|
|
6389
6827
|
for (const [key, value] of Object.entries(having)) {
|
|
6390
6828
|
if (value == null) continue;
|
|
6391
6829
|
if (key === "count") {
|
|
6392
|
-
conditions.push(...havingComparisons((0, drizzle_orm.count)(), value));
|
|
6830
|
+
conditions.push(...havingComparisons((0, drizzle_orm.count)(), value, isPg));
|
|
6393
6831
|
continue;
|
|
6394
6832
|
}
|
|
6395
6833
|
if (!AGGREGATE_OPS.includes(key)) continue;
|
|
6396
6834
|
for (const [columnName, filter] of Object.entries(value)) {
|
|
6397
6835
|
const column = columns[columnName];
|
|
6398
6836
|
if (!column || filter == null) continue;
|
|
6399
|
-
conditions.push(...havingComparisons(OP_FNS[key](column), filter));
|
|
6837
|
+
conditions.push(...havingComparisons(OP_FNS[key](column), filter, isPg));
|
|
6400
6838
|
}
|
|
6401
6839
|
}
|
|
6402
6840
|
return conditions.length ? (0, drizzle_orm.and)(...conditions) : void 0;
|
|
@@ -6548,12 +6986,32 @@ const isOptionalOnInsert = (column) => !column.notNull || !!column.hasDefault ||
|
|
|
6548
6986
|
* relation could come back without it. A `where` inherited from the reverse relation filters
|
|
6549
6987
|
* the parent instead, and becomes a per-parent check.
|
|
6550
6988
|
*/
|
|
6551
|
-
const ownWherePlan = (relEntry, parentTable, targetTable, localCol) => {
|
|
6989
|
+
const ownWherePlan = (relEntry, parentTable, targetTable, localCol, parentPrimaryKey) => {
|
|
6552
6990
|
const condition = () => relationOwnCondition(relEntry, parentTable, targetTable, [localCol], [localCol]);
|
|
6553
6991
|
if (!condition()) return {};
|
|
6554
|
-
|
|
6555
|
-
|
|
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
|
+
};
|
|
6556
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)}`;
|
|
6557
7015
|
/** The property name a column is stored under on its table, matched by identity. */
|
|
6558
7016
|
const propNameOf = (table, column) => {
|
|
6559
7017
|
if (!column) return;
|
|
@@ -6615,7 +7073,7 @@ const buildThroughPlan = (relationName, relEntry, relation, table, targetTableNa
|
|
|
6615
7073
|
targetCol
|
|
6616
7074
|
},
|
|
6617
7075
|
targetKeyColumns: { [foreignColPropName]: foreignCol },
|
|
6618
|
-
...ownWherePlan(relEntry, table, targetTable, localCol),
|
|
7076
|
+
...ownWherePlan(relEntry, table, targetTable, localCol, []),
|
|
6619
7077
|
allowed: /* @__PURE__ */ new Set([
|
|
6620
7078
|
"connect",
|
|
6621
7079
|
"disconnect",
|
|
@@ -6694,7 +7152,7 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
6694
7152
|
const targetPrimaryKey = resolvePrimaryKeys(targetTable).filter((name) => targetColumns[name]);
|
|
6695
7153
|
const targetKeyColumns = Object.fromEntries(targetPrimaryKey.map((name) => [name, targetColumns[name]]));
|
|
6696
7154
|
targetKeyColumns[foreignColPropName] = foreignCol;
|
|
6697
|
-
const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol);
|
|
7155
|
+
const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol, resolvePrimaryKeys(table));
|
|
6698
7156
|
const ops = !fkColumn.notNull ? isOne ? [
|
|
6699
7157
|
"create",
|
|
6700
7158
|
"connect",
|
|
@@ -7127,6 +7585,16 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7127
7585
|
}
|
|
7128
7586
|
if (supplied(op["connect"])) await link(asArray(op["connect"]), "connect");
|
|
7129
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
|
+
};
|
|
7130
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");
|
|
7131
7599
|
return {
|
|
7132
7600
|
enabled: (tableName) => !!plans[tableName],
|
|
@@ -7152,7 +7620,10 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7152
7620
|
const allColumns = (0, drizzle_orm.getColumns)(table);
|
|
7153
7621
|
for (const relationName of Object.keys(ops)) {
|
|
7154
7622
|
const plan = tablePlans[relationName];
|
|
7155
|
-
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
|
+
}
|
|
7156
7627
|
}
|
|
7157
7628
|
for (const name of selfTargetingPlan(tablePlans, tableName, ops)?.targetPrimaryKey ?? []) if (!(name in columns) && allColumns[name]) columns[name] = allColumns[name];
|
|
7158
7629
|
return columns;
|
|
@@ -7172,10 +7643,15 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7172
7643
|
...fresh.get(keyOf(row))
|
|
7173
7644
|
}));
|
|
7174
7645
|
},
|
|
7175
|
-
applyParentSide: async (executor, tableName, ops, request) => {
|
|
7646
|
+
applyParentSide: async (executor, tableName, ops, request, existing = false) => {
|
|
7176
7647
|
const tablePlans = plans[tableName];
|
|
7177
|
-
const
|
|
7178
|
-
|
|
7648
|
+
const result = {
|
|
7649
|
+
values: {},
|
|
7650
|
+
expected: {},
|
|
7651
|
+
existing
|
|
7652
|
+
};
|
|
7653
|
+
const patch = result.values;
|
|
7654
|
+
if (!tablePlans) return result;
|
|
7179
7655
|
const context = request?.context;
|
|
7180
7656
|
const scope = resolveScope(policies, context, filterCtx);
|
|
7181
7657
|
for (const [relationName, op] of Object.entries(ops)) {
|
|
@@ -7194,14 +7670,30 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7194
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" });
|
|
7195
7671
|
await steps.after();
|
|
7196
7672
|
patch[plan.localColPropName] = created[plan.foreignColPropName];
|
|
7673
|
+
result.expected[relationName] = created[plan.foreignColPropName];
|
|
7197
7674
|
} else if (op["connect"] !== void 0) {
|
|
7198
7675
|
const rows = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
|
|
7199
7676
|
if (rows.length !== 1) throw connectMatchError(relationName, rows.length);
|
|
7200
|
-
|
|
7201
|
-
|
|
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;
|
|
7202
7681
|
}
|
|
7203
7682
|
for (const columnName of Object.keys(contextValues?.(tableName) ?? {})) delete patch[columnName];
|
|
7204
|
-
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
|
+
}
|
|
7205
7697
|
},
|
|
7206
7698
|
applyChildSide: async (executor, tableName, ops, parentRows, request) => {
|
|
7207
7699
|
const tablePlans = plans[tableName];
|
|
@@ -7268,12 +7760,13 @@ var NoParentRow = class extends Error {};
|
|
|
7268
7760
|
* `executor` must already be a transaction.
|
|
7269
7761
|
*/
|
|
7270
7762
|
const writeNestedEntry = async (params) => {
|
|
7271
|
-
const { executor, runtime, tableName, ops, write, request } = params;
|
|
7763
|
+
const { executor, runtime, tableName, ops, write, request, existing } = params;
|
|
7272
7764
|
const guarded = runtime.createsParentSide(tableName, ops);
|
|
7273
7765
|
const run = async (tx) => {
|
|
7274
|
-
const patch = await runtime.applyParentSide(tx, tableName, ops, request);
|
|
7275
|
-
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);
|
|
7276
7768
|
if (guarded && !rows.length) throw new NoParentRow();
|
|
7769
|
+
await runtime.checkParentSide(tx, tableName, ops, rows, patch);
|
|
7277
7770
|
await runtime.applyChildSide(tx, tableName, ops, rows, request);
|
|
7278
7771
|
return runtime.refreshed(tx, tableName, ops, rows);
|
|
7279
7772
|
};
|
|
@@ -7335,7 +7828,8 @@ const updateWithNestedOps = async (params) => {
|
|
|
7335
7828
|
...remapValues(columns),
|
|
7336
7829
|
...patch
|
|
7337
7830
|
}),
|
|
7338
|
-
request
|
|
7831
|
+
request,
|
|
7832
|
+
existing: true
|
|
7339
7833
|
}));
|
|
7340
7834
|
};
|
|
7341
7835
|
//#endregion
|
|
@@ -7488,6 +7982,7 @@ const createSchemaBuilder = (adapter) => {
|
|
|
7488
7982
|
const anyTable = (feature) => tableEntries.some(([name]) => featureOf(name)[feature]);
|
|
7489
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?");
|
|
7490
7984
|
registerScalarOverrides(tables, options);
|
|
7985
|
+
installSqliteBlobJsonDecoders(tables);
|
|
7491
7986
|
registerEnumConfig(options);
|
|
7492
7987
|
registerColumnExclusions(tables, options.exclude);
|
|
7493
7988
|
resetAggregateColumnCache();
|
|
@@ -7719,7 +8214,7 @@ const createSchemaBuilder = (adapter) => {
|
|
|
7719
8214
|
...tableFeatures.update ? [updateInput] : [],
|
|
7720
8215
|
...built.updateManyInput ? [built.updateManyInput] : [],
|
|
7721
8216
|
tableFilters,
|
|
7722
|
-
tableOrder
|
|
8217
|
+
...tableOrder ? [tableOrder] : []
|
|
7723
8218
|
].forEach((e) => {
|
|
7724
8219
|
inputs[e.name] = e;
|
|
7725
8220
|
});
|
|
@@ -7849,14 +8344,12 @@ const generateUpsert = (db, tableName, table, baseType, onConflictType, fieldNam
|
|
|
7849
8344
|
withTarget: false,
|
|
7850
8345
|
serverOwned: Object.keys(contextColumns ?? {})
|
|
7851
8346
|
});
|
|
7852
|
-
const requestedNothing = args.onConflict?.action === "NOTHING";
|
|
7853
8347
|
const runBatch = async (target, { values, plan }) => {
|
|
7854
|
-
if (plan.action === "NOTHING"
|
|
8348
|
+
if (plan.action === "NOTHING") {
|
|
7855
8349
|
const columns = (0, drizzle_orm.getColumns)(table);
|
|
7856
8350
|
const keyName = pkNames[0] ?? Object.keys(columns)[0];
|
|
7857
8351
|
await target.insert(table).values(values).onDuplicateKeyUpdate({ set: { [keyName]: drizzle_orm.sql`${drizzle_orm.sql.identifier(columns[keyName].name)}` } });
|
|
7858
|
-
} else
|
|
7859
|
-
else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
|
|
8352
|
+
} else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
|
|
7860
8353
|
};
|
|
7861
8354
|
if (batches.length === 1) await runBatch(executor, batches[0]);
|
|
7862
8355
|
else await executor.transaction(async (tx) => {
|
|
@@ -7896,7 +8389,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
|
|
|
7896
8389
|
});
|
|
7897
8390
|
if (single) await assertSingleMatch(executor, table, filters);
|
|
7898
8391
|
let query = executor.update(table).set(input);
|
|
7899
|
-
if (filters) query = query.where(filters);
|
|
8392
|
+
if (filters) query = query.where(mysqlWriteFilter(table, filters));
|
|
7900
8393
|
await query;
|
|
7901
8394
|
await after();
|
|
7902
8395
|
return isSuccess;
|
|
@@ -7911,7 +8404,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
|
|
|
7911
8404
|
* in order. MySQL has no RETURNING, so — like every other MySQL mutation — the result is
|
|
7912
8405
|
* `{ isSuccess: true }` rather than the updated rows.
|
|
7913
8406
|
*/
|
|
7914
|
-
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({
|
|
7915
8408
|
db,
|
|
7916
8409
|
tableName,
|
|
7917
8410
|
operation: "updateMany",
|
|
@@ -7931,13 +8424,20 @@ const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName,
|
|
|
7931
8424
|
if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
|
|
7932
8425
|
return {
|
|
7933
8426
|
set: input,
|
|
7934
|
-
filters:
|
|
8427
|
+
filters: scopedWhere({
|
|
8428
|
+
scope,
|
|
8429
|
+
tableName,
|
|
8430
|
+
table,
|
|
8431
|
+
where,
|
|
8432
|
+
relationCtx: relationFilterCtx(filterCtx, tableName, scope),
|
|
8433
|
+
required: requireWhere
|
|
8434
|
+
})
|
|
7935
8435
|
};
|
|
7936
8436
|
});
|
|
7937
8437
|
await executor.transaction(async (tx) => {
|
|
7938
8438
|
for (const entry of entries) {
|
|
7939
8439
|
let query = tx.update(table).set(entry.set);
|
|
7940
|
-
if (entry.filters) query = query.where(entry.filters);
|
|
8440
|
+
if (entry.filters) query = query.where(mysqlWriteFilter(table, entry.filters));
|
|
7941
8441
|
await query;
|
|
7942
8442
|
}
|
|
7943
8443
|
});
|
|
@@ -7986,7 +8486,7 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
|
|
|
7986
8486
|
});
|
|
7987
8487
|
if (single) await assertSingleMatch(executor, table, filters);
|
|
7988
8488
|
let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
|
|
7989
|
-
if (filters) query = query.where(filters);
|
|
8489
|
+
if (filters) query = query.where(mysqlWriteFilter(table, filters));
|
|
7990
8490
|
await query;
|
|
7991
8491
|
await after();
|
|
7992
8492
|
return isSuccess;
|
|
@@ -7997,6 +8497,34 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
|
|
|
7997
8497
|
const getTableConfig$2 = memoizeTableConfig(drizzle_orm_mysql_core.getTableConfig);
|
|
7998
8498
|
/** Primary-key property names for a MySQL table, including table-level composite keys. */
|
|
7999
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
|
+
};
|
|
8000
8528
|
const { prepareBuild, forEachTable, finalizeBuild } = createSchemaBuilder({
|
|
8001
8529
|
tableClass: drizzle_orm_mysql_core.MySqlTable,
|
|
8002
8530
|
getTableConfig: getTableConfig$2,
|
|
@@ -8050,7 +8578,7 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
|
|
|
8050
8578
|
tableFilters,
|
|
8051
8579
|
cacheCtx
|
|
8052
8580
|
}) : void 0;
|
|
8053
|
-
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;
|
|
8054
8582
|
const deleteGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
|
|
8055
8583
|
const deleteSingleGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteSingleFieldName, true, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
|
|
8056
8584
|
const restoreGenerated = softDeleteInfo ? generateDelete(db, tableName, schema[tableName], tableFilters, restoreFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies, true) : void 0;
|
|
@@ -8067,7 +8595,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
|
|
|
8067
8595
|
filterCtx,
|
|
8068
8596
|
txCtx: mutationTxCtx,
|
|
8069
8597
|
policies,
|
|
8070
|
-
returnsRows: false
|
|
8598
|
+
returnsRows: false,
|
|
8599
|
+
writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
|
|
8071
8600
|
}) : void 0;
|
|
8072
8601
|
const deleteCountGenerated = tableFeatures.delete && tableFeatures.countMutations ? generateWriteCount({
|
|
8073
8602
|
db,
|
|
@@ -8080,7 +8609,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
|
|
|
8080
8609
|
filterCtx,
|
|
8081
8610
|
txCtx: mutationTxCtx,
|
|
8082
8611
|
policies,
|
|
8083
|
-
returnsRows: false
|
|
8612
|
+
returnsRows: false,
|
|
8613
|
+
writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
|
|
8084
8614
|
}) : void 0;
|
|
8085
8615
|
const hardDeleteMeta = softDeleteInfo?.hardDelete ? { hardDelete: true } : {};
|
|
8086
8616
|
const withMeta = (generated, meta) => ({
|
|
@@ -8192,7 +8722,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8192
8722
|
* one-element list.
|
|
8193
8723
|
*/
|
|
8194
8724
|
const generateInsert = (build, opts) => {
|
|
8195
|
-
const { db, tables, relationMap, typeNameMapper, txCtx, nested, limits, policies, resolveName } = build;
|
|
8725
|
+
const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
|
|
8196
8726
|
const { tableName, table, baseType, fieldName, typeName, single, conflictDoNothing = false } = opts;
|
|
8197
8727
|
const queryArgs = { values: { type: single ? new graphql.GraphQLNonNull(baseType) : new graphql.GraphQLNonNull(new graphql.GraphQLList(new graphql.GraphQLNonNull(baseType))) } };
|
|
8198
8728
|
const pkNames = primaryKeyPropNames(table);
|
|
@@ -8216,6 +8746,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8216
8746
|
const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
|
|
8217
8747
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
8218
8748
|
relationMap,
|
|
8749
|
+
filterCtx,
|
|
8219
8750
|
tables,
|
|
8220
8751
|
tableName,
|
|
8221
8752
|
typeName,
|
|
@@ -8294,6 +8825,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8294
8825
|
const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
|
|
8295
8826
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
8296
8827
|
relationMap,
|
|
8828
|
+
filterCtx,
|
|
8297
8829
|
tables,
|
|
8298
8830
|
tableName,
|
|
8299
8831
|
typeName,
|
|
@@ -8318,24 +8850,25 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8318
8850
|
serverOwned: Object.keys(contextColumns ?? {}),
|
|
8319
8851
|
buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope))
|
|
8320
8852
|
});
|
|
8321
|
-
const
|
|
8322
|
-
|
|
8853
|
+
const batchQuery = (executor, { values: rows, plan }) => {
|
|
8854
|
+
const query = executor.insert(table).values(rows).returning(returning);
|
|
8323
8855
|
const setWhere = withScope(scope, tableName, table, plan.setWhere);
|
|
8324
|
-
query
|
|
8325
|
-
target: plan.target,
|
|
8326
|
-
where: plan.targetWhere
|
|
8327
|
-
} : void 0) : query.onConflictDoUpdate({
|
|
8328
|
-
target: plan.target,
|
|
8329
|
-
targetWhere: plan.targetWhere,
|
|
8330
|
-
set: plan.set,
|
|
8331
|
-
setWhere
|
|
8332
|
-
});
|
|
8333
|
-
return await query;
|
|
8856
|
+
return applyConflictPlan(query, table, plan, setWhere);
|
|
8334
8857
|
};
|
|
8335
|
-
if (batches.length === 1) return
|
|
8336
|
-
return target.transaction(
|
|
8337
|
-
const
|
|
8338
|
-
|
|
8858
|
+
if (batches.length === 1) return await batchQuery(target, batches[0]);
|
|
8859
|
+
return target.transaction((tx) => {
|
|
8860
|
+
const execute = (batch) => {
|
|
8861
|
+
const query = batchQuery(tx, batch);
|
|
8862
|
+
return typeof query.all === "function" ? query.all() : query;
|
|
8863
|
+
};
|
|
8864
|
+
const first = execute(batches[0]);
|
|
8865
|
+
if (typeof first?.then === "function") return (async () => {
|
|
8866
|
+
const rows = [...await first];
|
|
8867
|
+
for (const batch of batches.slice(1)) rows.push(...await execute(batch));
|
|
8868
|
+
return rows;
|
|
8869
|
+
})();
|
|
8870
|
+
const rows = [...first];
|
|
8871
|
+
for (const batch of batches.slice(1)) rows.push(...execute(batch));
|
|
8339
8872
|
return rows;
|
|
8340
8873
|
});
|
|
8341
8874
|
};
|
|
@@ -8383,6 +8916,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8383
8916
|
await before();
|
|
8384
8917
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
8385
8918
|
relationMap,
|
|
8919
|
+
filterCtx,
|
|
8386
8920
|
tables,
|
|
8387
8921
|
tableName,
|
|
8388
8922
|
typeName,
|
|
@@ -8621,7 +9155,8 @@ const createSchemaDataGenerator = (adapter) => {
|
|
|
8621
9155
|
table: schema[tableName],
|
|
8622
9156
|
updateManyInput,
|
|
8623
9157
|
fieldName: updateManyFieldName,
|
|
8624
|
-
typeName
|
|
9158
|
+
typeName,
|
|
9159
|
+
requireWhere: tableFeatures.requireWhere
|
|
8625
9160
|
}) : void 0;
|
|
8626
9161
|
const deleteGenerated = tableFeatures.delete ? generateDelete(writeBuild, {
|
|
8627
9162
|
tableName,
|
|
@@ -8876,7 +9411,8 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
|
|
|
8876
9411
|
tableName,
|
|
8877
9412
|
ops: entry.ops,
|
|
8878
9413
|
write,
|
|
8879
|
-
request
|
|
9414
|
+
request,
|
|
9415
|
+
existing: true
|
|
8880
9416
|
}) : await write(tx, {}));
|
|
8881
9417
|
}
|
|
8882
9418
|
return results;
|
|
@@ -8897,7 +9433,7 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
|
|
|
8897
9433
|
const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateManyBatch) => {
|
|
8898
9434
|
return (build, opts) => {
|
|
8899
9435
|
const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
|
|
8900
|
-
const { tableName, table, updateManyInput, fieldName, typeName } = opts;
|
|
9436
|
+
const { tableName, table, updateManyInput, fieldName, typeName, requireWhere } = opts;
|
|
8901
9437
|
const queryArgs = { updates: { type: new graphql.GraphQLNonNull(new graphql.GraphQLList(new graphql.GraphQLNonNull(updateManyInput))) } };
|
|
8902
9438
|
const pkNames = primaryKeyPropNames(table);
|
|
8903
9439
|
return writeResolver({
|
|
@@ -8917,6 +9453,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
|
|
|
8917
9453
|
const contextColumns = policies?.contextValues?.(tableName);
|
|
8918
9454
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
8919
9455
|
relationMap,
|
|
9456
|
+
filterCtx,
|
|
8920
9457
|
tables,
|
|
8921
9458
|
tableName,
|
|
8922
9459
|
typeName,
|
|
@@ -8936,7 +9473,14 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
|
|
|
8936
9473
|
return {
|
|
8937
9474
|
set: input,
|
|
8938
9475
|
ops,
|
|
8939
|
-
filters:
|
|
9476
|
+
filters: scopedWhere({
|
|
9477
|
+
scope,
|
|
9478
|
+
tableName,
|
|
9479
|
+
table,
|
|
9480
|
+
where,
|
|
9481
|
+
relationCtx: relationFilterCtx(filterCtx, tableName, scope),
|
|
9482
|
+
required: requireWhere
|
|
9483
|
+
})
|
|
8940
9484
|
};
|
|
8941
9485
|
});
|
|
8942
9486
|
const anyNested = entries.some((entry) => entry.ops);
|
|
@@ -9025,7 +9569,8 @@ const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNest
|
|
|
9025
9569
|
tableName,
|
|
9026
9570
|
ops: entry.ops,
|
|
9027
9571
|
write,
|
|
9028
|
-
request
|
|
9572
|
+
request,
|
|
9573
|
+
existing: true
|
|
9029
9574
|
}) : write(tx, {});
|
|
9030
9575
|
};
|
|
9031
9576
|
return executor.transaction((tx) => {
|
|
@@ -9055,6 +9600,7 @@ const sqliteSchemaData = createSchemaDataGenerator({
|
|
|
9055
9600
|
preflight: (db, options) => {
|
|
9056
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.");
|
|
9057
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.");
|
|
9058
9604
|
},
|
|
9059
9605
|
generateUpdateMany
|
|
9060
9606
|
});
|