@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.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Column, One, SQL, Table, aliasedTable, and, arrayContains, arrayOverlaps, asc, avg, count, countDistinct, desc, eq, extractExtendedColumnType, getColumns, getTableAsAliasSQL, gt, gte, ilike, inArray, is, isNotNull, isNull, like, lt, lte, max, min, ne, not, notIlike, notInArray, notLike, or, relationsFilterToSQL, sql, sum } from "drizzle-orm";
|
|
2
|
-
import { MySqlAsyncDatabase, MySqlDate, MySqlDateString, MySqlDateTime, MySqlDateTimeString, MySqlInt, MySqlSerial, MySqlTable, MySqlTimestamp, MySqlTimestampString, getTableConfig } from "drizzle-orm/mysql-core";
|
|
1
|
+
import { Column, One, SQL, Subquery, Table, aliasedTable, and, arrayContains, arrayOverlaps, asc, avg, count, countDistinct, desc, eq, extractExtendedColumnType, getColumns, getTableAsAliasSQL, gt, gte, ilike, inArray, is, isNotNull, isNull, like, lt, lte, max, min, ne, not, notIlike, notInArray, notLike, or, relationsFilterToSQL, sql, sum } from "drizzle-orm";
|
|
2
|
+
import { MySqlAsyncDatabase, MySqlDate, MySqlDateString, MySqlDateTime, MySqlDateTimeString, MySqlEnumColumn, MySqlEnumObjectColumn, MySqlInt, MySqlSerial, MySqlTable, MySqlTimestamp, MySqlTimestampString, getTableConfig } from "drizzle-orm/mysql-core";
|
|
3
3
|
import { PgAsyncDatabase, PgDate, PgDateString, PgInteger, PgSerial, PgTable, PgTimestamp, PgTimestampString, PgUUID, getTableConfig as getTableConfig$1 } from "drizzle-orm/pg-core";
|
|
4
|
-
import { SQLiteAsyncDatabase,
|
|
4
|
+
import { SQLiteAsyncDatabase, SQLiteTable, SQLiteTimestamp, getTableConfig as getTableConfig$2 } from "drizzle-orm/sqlite-core";
|
|
5
5
|
import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, Kind, getArgumentValues, getNamedType } from "graphql";
|
|
6
6
|
import { GraphQLDateTime, GraphQLJSON, GraphQLUUID } from "graphql-scalars";
|
|
7
7
|
import pluralize from "pluralize";
|
|
@@ -262,7 +262,6 @@ const singularizeMapper = (tableName) => ({
|
|
|
262
262
|
});
|
|
263
263
|
//#endregion
|
|
264
264
|
//#region src/util/type-converter/index.ts
|
|
265
|
-
const allowedNameChars = /^[a-zA-Z0-9_]+$/;
|
|
266
265
|
/** drizzle's `number` constraints whose values always fit a GraphQL `Int` (32-bit signed). */
|
|
267
266
|
const smallIntegerConstraints = /* @__PURE__ */ new Set([
|
|
268
267
|
"int8",
|
|
@@ -288,8 +287,15 @@ const declaredEnum = (column) => {
|
|
|
288
287
|
schema: typeof declared.schema === "string" ? declared.schema : void 0
|
|
289
288
|
} : void 0;
|
|
290
289
|
};
|
|
291
|
-
/**
|
|
292
|
-
|
|
290
|
+
/**
|
|
291
|
+
* `user_status` / `user-status` / `user status` → `UserStatus`; an already-PascalCase name is left
|
|
292
|
+
* alone. A GraphQL name cannot start with a digit, so a name that would (`2fa_mode`) is prefixed
|
|
293
|
+
* with an underscore: `_2faMode`.
|
|
294
|
+
*/
|
|
295
|
+
const pascalize = (input) => {
|
|
296
|
+
const pascal = input.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => capitalize(part)).join("");
|
|
297
|
+
return /^[0-9]/.test(pascal) ? `_${pascal}` : pascal;
|
|
298
|
+
};
|
|
293
299
|
/**
|
|
294
300
|
* Identity of the values a GraphQL enum was built from, so a name claimed twice can be told
|
|
295
301
|
* apart from the same enum reached twice.
|
|
@@ -364,7 +370,7 @@ const generateEnumCached = (column, columnName, tableName) => {
|
|
|
364
370
|
index,
|
|
365
371
|
typeName: name
|
|
366
372
|
});
|
|
367
|
-
if (mapped === void 0) return
|
|
373
|
+
if (mapped === void 0) return validEnumValueName(value) ? value : `Option${index}`;
|
|
368
374
|
if (!validEnumValueName(mapped)) throw new Error(`Drizzle-GraphQL Error: config.enumValueNameMapper named value '${value}' of enum '${name}' (on '${tableName}.${columnName}') '${mapped}', which is not a valid GraphQL enum value name. A name must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ and not be true, false or null.`);
|
|
369
375
|
return mapped;
|
|
370
376
|
});
|
|
@@ -460,6 +466,27 @@ const sqliteTimestampToDate = (column, value) => new Date(column.mode === "times
|
|
|
460
466
|
* mode it uses, for the same reason as {@link isDateOnlyColumn}.
|
|
461
467
|
*/
|
|
462
468
|
const isDateTimeColumn = (column) => is(column, PgTimestamp) || isPgTimestampStringColumn(column) || is(column, MySqlTimestamp) || is(column, MySqlDateTime) || isMySqlDateTimeStringColumn(column) || isSQLiteTimestampColumn(column);
|
|
469
|
+
/**
|
|
470
|
+
* Whether the column holds raw bytes: Postgres `bytea` and SQLite `blob({ mode: 'buffer' })`,
|
|
471
|
+
* both `object buffer` in drizzle v1. Their values are `[Int!]` lists of the bytes.
|
|
472
|
+
*/
|
|
473
|
+
const isBufferColumn = (column) => (column.dataType ?? "").endsWith(" buffer");
|
|
474
|
+
/**
|
|
475
|
+
* Whether the column is a float tuple or vector: drizzle v1 gives every such column an
|
|
476
|
+
* `array …` data type — `point` / `line` in tuple mode (`array point`, `array line`), PostGIS
|
|
477
|
+
* `geometry` in tuple mode, pgvector `vector` and `halfvec`. Each value is one list of numbers,
|
|
478
|
+
* typed `[Float!]`, not a database array of elements.
|
|
479
|
+
*/
|
|
480
|
+
const isFloatTupleColumn = (column) => (column.dataType ?? "").startsWith("array ");
|
|
481
|
+
/** The fixed length of a float tuple column's values, or `undefined` for a vector. */
|
|
482
|
+
const floatTupleLength = (column) => {
|
|
483
|
+
switch (column.dataType) {
|
|
484
|
+
case "array point":
|
|
485
|
+
case "array geometry": return 2;
|
|
486
|
+
case "array line": return 3;
|
|
487
|
+
default: return;
|
|
488
|
+
}
|
|
489
|
+
};
|
|
463
490
|
const columnToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
464
491
|
const dimensions = column.dimensions;
|
|
465
492
|
if (dimensions !== void 0 && dimensions > 0) {
|
|
@@ -497,7 +524,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
497
524
|
return column.columnType === "PgGeometryObject" ? {
|
|
498
525
|
type: isInput ? geoXyInputType : geoXyType,
|
|
499
526
|
typeLabel: "Geometry points XY"
|
|
500
|
-
} : column
|
|
527
|
+
} : isBufferColumn(column) ? {
|
|
501
528
|
type: new GraphQLList(new GraphQLNonNull(GraphQLInt)),
|
|
502
529
|
typeLabel: "Buffer"
|
|
503
530
|
} : {
|
|
@@ -530,7 +557,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
530
557
|
type: GraphQLBigIntString,
|
|
531
558
|
typeLabel: "BigInt"
|
|
532
559
|
};
|
|
533
|
-
case "number": return constraint !== void 0 && smallIntegerConstraints.has(constraint) || is(column, PgInteger) || is(column, PgSerial) || is(column, MySqlInt) || is(column, MySqlSerial)
|
|
560
|
+
case "number": return constraint !== void 0 && smallIntegerConstraints.has(constraint) || is(column, PgInteger) || is(column, PgSerial) || is(column, MySqlInt) || is(column, MySqlSerial) ? {
|
|
534
561
|
type: GraphQLInt,
|
|
535
562
|
typeLabel: "Integer"
|
|
536
563
|
} : {
|
|
@@ -538,18 +565,10 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
|
|
|
538
565
|
typeLabel: "Float"
|
|
539
566
|
};
|
|
540
567
|
case "array": {
|
|
541
|
-
|
|
542
|
-
type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
|
|
543
|
-
typeLabel: "Array<Float>"
|
|
544
|
-
};
|
|
545
|
-
if (column.columnType === "PgGeometry") return {
|
|
546
|
-
type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
|
|
547
|
-
typeLabel: "Tuple<[Float, Float]>"
|
|
548
|
-
};
|
|
549
|
-
const innerType = columnToGraphQLCore(column.baseColumn, columnName, tableName, isInput);
|
|
568
|
+
const length = floatTupleLength(column);
|
|
550
569
|
return {
|
|
551
|
-
type: new GraphQLList(new GraphQLNonNull(
|
|
552
|
-
typeLabel: `Array
|
|
570
|
+
type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
|
|
571
|
+
typeLabel: length === void 0 ? "Array<Float>" : `Tuple<[${Array(length).fill("Float").join(", ")}]>`
|
|
553
572
|
};
|
|
554
573
|
}
|
|
555
574
|
default: throw new Error(`Drizzle-GraphQL Error: Type ${column.dataType} is not implemented!`);
|
|
@@ -691,6 +710,18 @@ const columnDialect = (column) => {
|
|
|
691
710
|
*/
|
|
692
711
|
const isJsonColumn = (column) => (column.dataType ?? "").includes("json") && column.columnType !== "PgGeometryObject";
|
|
693
712
|
/**
|
|
713
|
+
* SQLite `blob({ mode: 'bigint' })`: the bigint is stored as the bytes of its decimal text, and
|
|
714
|
+
* SQLite compares blobs byte by byte, so `9` sorts above `10`. Equality still holds, so it can be
|
|
715
|
+
* grouped and made distinct on, but not ordered or given a min/max.
|
|
716
|
+
*/
|
|
717
|
+
const isSqliteBlobBigIntColumn = (column) => column.columnType === "SQLiteBigInt";
|
|
718
|
+
/**
|
|
719
|
+
* Whether `orderBy` (and so cursor pagination) offers a column. JSON has no meaningful order:
|
|
720
|
+
* PostgreSQL `json` cannot be ordered at all, and MySQL's JSON ordering disagrees with any
|
|
721
|
+
* keyset comparison a cursor could make. A SQLite blob bigint orders as text.
|
|
722
|
+
*/
|
|
723
|
+
const isOrderableColumn = (column) => !isJsonColumn(column) && !isSqliteBlobBigIntColumn(column);
|
|
724
|
+
/**
|
|
694
725
|
* Maps a Drizzle column to the generic filter type to use.
|
|
695
726
|
*
|
|
696
727
|
* Selection is keyed on the column's data type, never its name: a text column named
|
|
@@ -1197,6 +1228,36 @@ const pgTimestampTextToIso = (value) => {
|
|
|
1197
1228
|
const utc = (/* @__PURE__ */ new Date(wallMs - offsetSeconds * 1e3)).toISOString();
|
|
1198
1229
|
return utc.length === 24 ? `${utc.slice(0, 19)}${fraction}Z` : value;
|
|
1199
1230
|
};
|
|
1231
|
+
/** Marks a decoder installed by {@link installSqliteBlobJsonDecoders}, so a rebuild never wraps it twice. */
|
|
1232
|
+
const BLOB_JSON_DECODER = Symbol.for("drizzle-graphql.sqliteBlobJsonDecoder");
|
|
1233
|
+
/**
|
|
1234
|
+
* Gives SQLite's blob columns a decoder for the relational query builder's JSON path, so a NULL
|
|
1235
|
+
* there reads back as null.
|
|
1236
|
+
*
|
|
1237
|
+
* Inside a nested relation drizzle selects a blob as `hex(column)` (JSON cannot hold a blob), and
|
|
1238
|
+
* `hex(NULL)` is `''`. drizzle then decodes that `''` with the column's ordinary decoder: a
|
|
1239
|
+
* `blob({ mode: 'bigint' })` reads it as `0n` — indistinguishable from a stored 0 — and a
|
|
1240
|
+
* `blob({ mode: 'json' })` throws on it. drizzle consults a column's `mapFromJsonValue`, when it
|
|
1241
|
+
* has one, instead of `mapFromDriverValue` for exactly this path, so one is installed here that
|
|
1242
|
+
* reads `''` as NULL and hands everything else to the column's own decoder.
|
|
1243
|
+
*
|
|
1244
|
+
* `''` is never a stored bigint or JSON blob, so for those it can only be NULL. For a buffer
|
|
1245
|
+
* blob it is also how an empty blob arrives, so it is read as NULL only when the column is
|
|
1246
|
+
* nullable — a `notNull` column cannot hold the NULL, so there it is the empty blob.
|
|
1247
|
+
*/
|
|
1248
|
+
const installSqliteBlobJsonDecoders = (tables) => {
|
|
1249
|
+
for (const table of Object.values(tables)) for (const column of Object.values(getColumns(table))) {
|
|
1250
|
+
const columnType = column.columnType ?? "";
|
|
1251
|
+
const isBuffer = columnType === "SQLiteBlobBuffer";
|
|
1252
|
+
if (columnType !== "SQLiteBigInt" && columnType !== "SQLiteBlobJson" && !isBuffer) continue;
|
|
1253
|
+
const existing = column.mapFromJsonValue;
|
|
1254
|
+
if (existing && !existing[BLOB_JSON_DECODER]) continue;
|
|
1255
|
+
const emptyIsNull = !isBuffer || !column.notNull;
|
|
1256
|
+
const decoder = (value) => value === "" && emptyIsNull ? null : column.mapFromDriverValue(value);
|
|
1257
|
+
decoder[BLOB_JSON_DECODER] = true;
|
|
1258
|
+
column.mapFromJsonValue = decoder;
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1200
1261
|
const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
|
|
1201
1262
|
if (Array.isArray(value)) {
|
|
1202
1263
|
const relations = relationMap?.[tableName];
|
|
@@ -1223,18 +1284,51 @@ const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
|
|
|
1223
1284
|
if (value instanceof Date) return value.toISOString();
|
|
1224
1285
|
if (typeof value === "string" && isMySqlDateTimeStringColumn(column)) return mySqlDateTimeTextToIso(value);
|
|
1225
1286
|
if (typeof value === "string" && isPgTimestampStringColumn(column)) return pgTimestampTextToIso(value);
|
|
1226
|
-
if (value instanceof
|
|
1287
|
+
if (value instanceof Uint8Array) return Array.from(value);
|
|
1227
1288
|
if (typeof value === "bigint") return value.toString();
|
|
1228
1289
|
if (Array.isArray(value)) {
|
|
1229
|
-
if (column
|
|
1290
|
+
if (isFloatTupleColumn(column)) return value;
|
|
1230
1291
|
return value.map((arrVal) => remapToGraphQLCore(key, arrVal, tableName, column, relationMap));
|
|
1231
1292
|
}
|
|
1232
1293
|
if (typeof value === "object" && value !== null) {
|
|
1233
|
-
if (column.
|
|
1294
|
+
if ((column.dataType ?? "").startsWith("object")) return value;
|
|
1234
1295
|
return JSON.stringify(value);
|
|
1235
1296
|
}
|
|
1236
1297
|
return value;
|
|
1237
1298
|
};
|
|
1299
|
+
/**
|
|
1300
|
+
* Where a remapped row keeps the values its columns held before remapping, by property name —
|
|
1301
|
+
* only for the columns remapping changed. The output form is not always reversible: a
|
|
1302
|
+
* microsecond timestamp read in `'string'` mode leaves as an ISO string the `DateTime` input
|
|
1303
|
+
* side would cut to milliseconds. A relation field correlating on the row's join columns reads
|
|
1304
|
+
* them from here (see `parentJoinValues`). Non-enumerable, so the row's own keys, JSON form and
|
|
1305
|
+
* equality are unchanged.
|
|
1306
|
+
*/
|
|
1307
|
+
const RAW_COLUMN_VALUES = Symbol("drizzle-graphql.rawColumnValues");
|
|
1308
|
+
/**
|
|
1309
|
+
* The value column `key` held in `row` before remapping: `{ found: true, value }` when remapping
|
|
1310
|
+
* changed it, `{ found: false }` when it did not (or the row was never remapped), in which case
|
|
1311
|
+
* `row[key]` is still the column's own value.
|
|
1312
|
+
*/
|
|
1313
|
+
const rawColumnValue = (row, key) => {
|
|
1314
|
+
const raw = row[RAW_COLUMN_VALUES];
|
|
1315
|
+
return raw && key in raw ? {
|
|
1316
|
+
found: true,
|
|
1317
|
+
value: raw[key]
|
|
1318
|
+
} : { found: false };
|
|
1319
|
+
};
|
|
1320
|
+
const keepRawColumnValue = (row, key, value) => {
|
|
1321
|
+
let raw = row[RAW_COLUMN_VALUES];
|
|
1322
|
+
if (!raw) {
|
|
1323
|
+
raw = Object.create(null);
|
|
1324
|
+
Object.defineProperty(row, RAW_COLUMN_VALUES, {
|
|
1325
|
+
value: raw,
|
|
1326
|
+
enumerable: false,
|
|
1327
|
+
configurable: true
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
if (!(key in raw)) raw[key] = value;
|
|
1331
|
+
};
|
|
1238
1332
|
const remapToGraphQLSingleOutput = (queryOutput, tableName, table, relationMap) => {
|
|
1239
1333
|
const columns = getColumns(table);
|
|
1240
1334
|
for (const [key, value] of Object.entries(queryOutput)) {
|
|
@@ -1248,11 +1342,9 @@ const remapToGraphQLSingleOutput = (queryOutput, tableName, table, relationMap)
|
|
|
1248
1342
|
continue;
|
|
1249
1343
|
}
|
|
1250
1344
|
const column = columns[key];
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
}
|
|
1255
|
-
queryOutput[key] = remapToGraphQLCore(key, value, tableName, column, relationMap);
|
|
1345
|
+
const remapped = remapToGraphQLCore(key, value, tableName, column, relationMap);
|
|
1346
|
+
if (column && remapped !== value) keepRawColumnValue(queryOutput, key, value);
|
|
1347
|
+
queryOutput[key] = remapped;
|
|
1256
1348
|
}
|
|
1257
1349
|
return queryOutput;
|
|
1258
1350
|
};
|
|
@@ -1269,6 +1361,7 @@ const remapFromGraphQLCore = (value, column, columnName) => {
|
|
|
1269
1361
|
const dataType = column.dataType ?? "";
|
|
1270
1362
|
const columnType = column.columnType ?? "";
|
|
1271
1363
|
if (isDateTimeColumn(column)) {
|
|
1364
|
+
if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
|
|
1272
1365
|
const isMySql = columnType.startsWith("MySql");
|
|
1273
1366
|
const isPgString = isPgTimestampStringColumn(column);
|
|
1274
1367
|
const toIso = isMySql ? mySqlDateTimeTextToIso : isPgString ? pgTimestampTextToIso : void 0;
|
|
@@ -1283,41 +1376,29 @@ const remapFromGraphQLCore = (value, column, columnName) => {
|
|
|
1283
1376
|
if (typeof day !== "string" || !isCalendarDate(day)) throw drizzleError(`Field '${columnName}' is not a valid date! Expected a calendar date as YYYY-MM-DD.`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1284
1377
|
return day;
|
|
1285
1378
|
}
|
|
1286
|
-
if (dataType.includes("bigint"))
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1290
|
-
}
|
|
1291
|
-
if (dataType.includes("json") && column.columnType !== "PgGeometryObject") return value;
|
|
1292
|
-
switch (dataType) {
|
|
1293
|
-
case "date": {
|
|
1294
|
-
const formatted = new Date(value);
|
|
1295
|
-
if (Number.isNaN(formatted.getTime())) throw drizzleError(`Field '${columnName}' is not a valid date!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1296
|
-
return formatted;
|
|
1297
|
-
}
|
|
1298
|
-
case "buffer":
|
|
1299
|
-
if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1300
|
-
return Buffer.from(value);
|
|
1301
|
-
case "json":
|
|
1302
|
-
if (column.columnType === "PgGeometryObject") return value;
|
|
1303
|
-
try {
|
|
1304
|
-
return JSON.parse(value);
|
|
1305
|
-
} catch (e) {
|
|
1306
|
-
throw drizzleError(`Invalid JSON in field '${columnName}':\n${e instanceof Error ? e.message : "Unknown error"}`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1307
|
-
}
|
|
1308
|
-
case "array":
|
|
1309
|
-
if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1310
|
-
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" });
|
|
1311
|
-
return value;
|
|
1312
|
-
case "bigint": try {
|
|
1379
|
+
if (dataType.includes("bigint")) {
|
|
1380
|
+
if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
|
|
1381
|
+
try {
|
|
1313
1382
|
return BigInt(value);
|
|
1314
|
-
} catch
|
|
1383
|
+
} catch {
|
|
1315
1384
|
throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1316
1385
|
}
|
|
1317
|
-
default:
|
|
1318
|
-
if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
|
|
1319
|
-
return value;
|
|
1320
1386
|
}
|
|
1387
|
+
if (isJsonColumn(column)) return value;
|
|
1388
|
+
if (isBufferColumn(column)) {
|
|
1389
|
+
if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1390
|
+
if (column.dimensions > 0 && value.every((element) => element === null || Array.isArray(element))) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
|
|
1391
|
+
if (!value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) throw drizzleError(`Field '${columnName}' is not a list of bytes! Expected integers from 0 to 255.`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1392
|
+
return Buffer.from(value);
|
|
1393
|
+
}
|
|
1394
|
+
if (isFloatTupleColumn(column)) {
|
|
1395
|
+
if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1396
|
+
const length = floatTupleLength(column);
|
|
1397
|
+
if (length !== void 0 && value.length !== length) throw drizzleError(`Invalid float tuple in field '${columnName}': expected array with length of ${length}, received ${value.length}`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
|
|
1398
|
+
return value;
|
|
1399
|
+
}
|
|
1400
|
+
if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
|
|
1401
|
+
return value;
|
|
1321
1402
|
};
|
|
1322
1403
|
const remapFromGraphQLSingleInput = (queryInput, table, operation = "insert") => {
|
|
1323
1404
|
const columns = getColumns(table);
|
|
@@ -1359,36 +1440,60 @@ const escapeLikeValue = (value) => value.replace(/[\\%_]/g, (char) => `\\${char}
|
|
|
1359
1440
|
const safeLikeOps = {
|
|
1360
1441
|
startsWith: {
|
|
1361
1442
|
buildPattern: (value) => `${escapeLikeValue(value)}%`,
|
|
1443
|
+
buildGlob: (value) => `${escapeGlobValue(value)}*`,
|
|
1362
1444
|
insensitive: false
|
|
1363
1445
|
},
|
|
1364
1446
|
endsWith: {
|
|
1365
1447
|
buildPattern: (value) => `%${escapeLikeValue(value)}`,
|
|
1448
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}`,
|
|
1366
1449
|
insensitive: false
|
|
1367
1450
|
},
|
|
1368
1451
|
contains: {
|
|
1369
1452
|
buildPattern: (value) => `%${escapeLikeValue(value)}%`,
|
|
1453
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}*`,
|
|
1370
1454
|
insensitive: false
|
|
1371
1455
|
},
|
|
1372
1456
|
iStartsWith: {
|
|
1373
1457
|
buildPattern: (value) => `${escapeLikeValue(value)}%`,
|
|
1458
|
+
buildGlob: (value) => `${escapeGlobValue(value)}*`,
|
|
1374
1459
|
insensitive: true
|
|
1375
1460
|
},
|
|
1376
1461
|
iEndsWith: {
|
|
1377
1462
|
buildPattern: (value) => `%${escapeLikeValue(value)}`,
|
|
1463
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}`,
|
|
1378
1464
|
insensitive: true
|
|
1379
1465
|
},
|
|
1380
1466
|
iContains: {
|
|
1381
1467
|
buildPattern: (value) => `%${escapeLikeValue(value)}%`,
|
|
1468
|
+
buildGlob: (value) => `*${escapeGlobValue(value)}*`,
|
|
1382
1469
|
insensitive: true
|
|
1383
1470
|
}
|
|
1384
1471
|
};
|
|
1385
1472
|
/**
|
|
1386
|
-
* `
|
|
1387
|
-
*
|
|
1388
|
-
*
|
|
1473
|
+
* Escapes SQLite's `GLOB` wildcards (`*`, `?`, `[`) in a literal search term by wrapping each in
|
|
1474
|
+
* a one-character class, so the term only ever matches literally inside a GLOB pattern. GLOB
|
|
1475
|
+
* has no escape character; `]` outside a class and everything else is already literal.
|
|
1389
1476
|
*/
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1477
|
+
const escapeGlobValue = (value) => value.replace(/[*?[]/g, (char) => `[${char}]`);
|
|
1478
|
+
/**
|
|
1479
|
+
* A caller-written LIKE pattern as the GLOB pattern matching the same strings, for SQLite's
|
|
1480
|
+
* case-sensitive `like` / `notLike`: `%` → `*`, `_` → `?`, and GLOB's own wildcards escaped.
|
|
1481
|
+
* The raw operators bind no `ESCAPE` clause, so `\` is an ordinary character in both.
|
|
1482
|
+
*/
|
|
1483
|
+
const likePatternToGlob = (pattern) => pattern.replace(/[%_*?[]/g, (char) => char === "%" ? "*" : char === "_" ? "?" : `[${char}]`);
|
|
1484
|
+
/**
|
|
1485
|
+
* The safe string operator's predicate. Case-insensitive variants use Postgres's native
|
|
1486
|
+
* `ILIKE`; MySQL and SQLite have no `ILIKE`, so they compare `lower()` on both sides instead
|
|
1487
|
+
* (mirroring how the raw `ilike` operator is Postgres-only).
|
|
1488
|
+
*
|
|
1489
|
+
* The case-sensitive variants are `LIKE <pattern> ESCAPE '\'`, except on SQLite, whose `LIKE`
|
|
1490
|
+
* folds ASCII case — `contains: "App"` would match `apple`. There they are `GLOB`, which is
|
|
1491
|
+
* case-sensitive, as `LIKE` is on Postgres. `dialectColumn` names the column the dialect is read
|
|
1492
|
+
* from when `column` is an expression over it (a JSON path read).
|
|
1493
|
+
*/
|
|
1494
|
+
const safeLikeCondition = (column, value, op, insensitive, dialectColumn = column) => {
|
|
1495
|
+
const pattern = op.buildPattern(value);
|
|
1496
|
+
if (!insensitive) return columnDialect(dialectColumn) === "sqlite" ? sql`${column} glob ${op.buildGlob(value)}` : sql`${column} like ${pattern} escape ${LIKE_ESCAPE_CHAR}`;
|
|
1392
1497
|
return (column.columnType ?? "").startsWith("Pg") ? sql`${column} ilike ${pattern} escape ${LIKE_ESCAPE_CHAR}` : sql`lower(${column}) like ${pattern.toLowerCase()} escape ${LIKE_ESCAPE_CHAR}`;
|
|
1393
1498
|
};
|
|
1394
1499
|
/**
|
|
@@ -1444,6 +1549,35 @@ const jsonContains = (column, columnName, value) => {
|
|
|
1444
1549
|
}
|
|
1445
1550
|
};
|
|
1446
1551
|
/**
|
|
1552
|
+
* The nodes of a SQLite JSON document as `(path, kind, value)` rows, for a structural
|
|
1553
|
+
* comparison. Integers and reals share one kind so `1` and `1.0` are the same number, as they
|
|
1554
|
+
* are on the other two dialects.
|
|
1555
|
+
*/
|
|
1556
|
+
const sqliteJsonNodes = (document) => sql`select fullkey, case when type in ('integer', 'real') then 'number' else type end, atom from json_tree(${document})`;
|
|
1557
|
+
/**
|
|
1558
|
+
* Whole-document `eq` / `ne` on a JSON column, comparing documents rather than their text:
|
|
1559
|
+
* - Postgres: as `jsonb` (a plain `json` column has no `=` operator at all)
|
|
1560
|
+
* - MySQL: against `CAST(? AS JSON)`; a bare string parameter would be a JSON string scalar
|
|
1561
|
+
* - SQLite: the two documents' node sets must match both ways, so object key order and
|
|
1562
|
+
* whitespace do not matter, while array order still does
|
|
1563
|
+
* A SQL NULL column matches neither, as with `=` / `<>` on any other column.
|
|
1564
|
+
*/
|
|
1565
|
+
const jsonEquality = (column, columnName, value, negated) => {
|
|
1566
|
+
const serialized = JSON.stringify(value);
|
|
1567
|
+
switch (columnDialect(column)) {
|
|
1568
|
+
case "pg": {
|
|
1569
|
+
const left = column.columnType === "PgJson" ? sql`${column}::jsonb` : sql`${column}`;
|
|
1570
|
+
return negated ? sql`${left} <> ${serialized}::jsonb` : sql`${left} = ${serialized}::jsonb`;
|
|
1571
|
+
}
|
|
1572
|
+
case "mysql": return negated ? sql`${column} <> cast(${serialized} as json)` : sql`${column} = cast(${serialized} as json)`;
|
|
1573
|
+
case "sqlite": {
|
|
1574
|
+
const equal = sql`(not exists (${sqliteJsonNodes(column)} except ${sqliteJsonNodes(serialized)}) and not exists (${sqliteJsonNodes(serialized)} except ${sqliteJsonNodes(column)}))`;
|
|
1575
|
+
return negated ? sql`(${column} is not null and not ${equal})` : sql`(${column} is not null and ${equal})`;
|
|
1576
|
+
}
|
|
1577
|
+
default: throw drizzleError(`WHERE ${columnName}: JSON equality is not supported for this dialect!`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
/**
|
|
1447
1581
|
* Constant predicates, used where an operator's operand list is empty and the answer is
|
|
1448
1582
|
* therefore known without touching the column. Written as `1 = 0` / `1 = 1` rather than the
|
|
1449
1583
|
* `FALSE` / `TRUE` keywords so they compile identically on all three dialects. Built fresh
|
|
@@ -1478,7 +1612,8 @@ const NUMERIC_TEXT_PATTERN = "^\\s*-?(\\d+\\.?\\d*|\\.\\d+)([eE][-+]?\\d+)?\\s*$
|
|
|
1478
1612
|
* pattern guard.
|
|
1479
1613
|
* - **SQLite** — `json_extract` returns the value in SQLite's own type, so the reads cast
|
|
1480
1614
|
* explicitly: without that, SQLite's cross-type ordering puts every string above every
|
|
1481
|
-
* number and `'admin' > 0` would be true.
|
|
1615
|
+
* number and `'admin' > 0` would be true. A JSON boolean comes back as 1 / 0, so every read
|
|
1616
|
+
* also checks `json_type` to keep booleans and numbers apart.
|
|
1482
1617
|
*/
|
|
1483
1618
|
const jsonPathExprs = (column, path) => {
|
|
1484
1619
|
switch (columnDialect(column)) {
|
|
@@ -1504,10 +1639,11 @@ const jsonPathExprs = (column, path) => {
|
|
|
1504
1639
|
}
|
|
1505
1640
|
default: {
|
|
1506
1641
|
const extracted = sql`json_extract(${column}, ${jsonPathString(path)})`;
|
|
1642
|
+
const type = sql`json_type(${column}, ${jsonPathString(path)})`;
|
|
1507
1643
|
return {
|
|
1508
|
-
text: sql`cast(${extracted} as text)`,
|
|
1509
|
-
number: sql`(case when
|
|
1510
|
-
boolean: extracted
|
|
1644
|
+
text: sql`(case ${type} when 'true' then 'true' when 'false' then 'false' else cast(${extracted} as text) end)`,
|
|
1645
|
+
number: sql`(case when ${type} in ('integer', 'real') then ${extracted} end)`,
|
|
1646
|
+
boolean: sql`(case when ${type} in ('true', 'false') then ${extracted} end)`,
|
|
1511
1647
|
encodeBoolean: (value) => value ? 1 : 0
|
|
1512
1648
|
};
|
|
1513
1649
|
}
|
|
@@ -1546,9 +1682,9 @@ const jsonPathCondition = (column, columnName, filter) => {
|
|
|
1546
1682
|
continue;
|
|
1547
1683
|
}
|
|
1548
1684
|
if (operatorName in safeLikeOps) {
|
|
1549
|
-
const
|
|
1685
|
+
const op = safeLikeOps[operatorName];
|
|
1550
1686
|
if (typeof operatorValue !== "string") throw drizzleError(`WHERE ${locator}: operator '${operatorName}' takes a string`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
1551
|
-
variants.push(safeLikeCondition(exprs.text,
|
|
1687
|
+
variants.push(safeLikeCondition(exprs.text, operatorValue, op, op.insensitive, column));
|
|
1552
1688
|
continue;
|
|
1553
1689
|
}
|
|
1554
1690
|
const comparison = JSON_PATH_COMPARISONS[operatorName];
|
|
@@ -1606,20 +1742,25 @@ const extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1606
1742
|
for (const [operatorName, operatorValue] of entries) {
|
|
1607
1743
|
if (operatorValue === null || operatorValue === void 0) continue;
|
|
1608
1744
|
if (operatorValue === false && FLAG_OPERATORS.has(operatorName)) continue;
|
|
1609
|
-
if (operatorName
|
|
1745
|
+
if ((operatorName === "eq" || operatorName === "ne") && isJsonColumn(column)) variants.push(jsonEquality(column, columnName, operatorValue, operatorName === "ne"));
|
|
1746
|
+
else if (operatorName in singleValueOps) {
|
|
1610
1747
|
const singleValue = remapFromGraphQLCore(operatorValue, column, columnName);
|
|
1611
1748
|
variants.push(foldCase ? lowerComparison(column, operatorName, singleValue) : singleValueOps[operatorName](column, singleValue));
|
|
1612
1749
|
} else if (operatorName in stringValueOps) {
|
|
1613
|
-
const
|
|
1614
|
-
variants.push(
|
|
1750
|
+
const isLike = operatorName === "like" || operatorName === "notLike";
|
|
1751
|
+
if (isLike && foldCase || (operatorName === "ilike" || operatorName === "notIlike") && columnDialect(column) !== "pg") variants.push(insensitiveLike(column, operatorValue, operatorName === "notLike" || operatorName === "notIlike"));
|
|
1752
|
+
else if (isLike && columnDialect(column) === "sqlite") {
|
|
1753
|
+
const glob = likePatternToGlob(operatorValue);
|
|
1754
|
+
variants.push(operatorName === "like" ? sql`${column} glob ${glob}` : sql`${column} not glob ${glob}`);
|
|
1755
|
+
} else variants.push(stringValueOps[operatorName](column, operatorValue));
|
|
1615
1756
|
} else if (operatorName === "path" && isJsonColumn(column)) for (const pathFilter of operatorValue) {
|
|
1616
1757
|
const extracted = jsonPathCondition(column, columnName, pathFilter);
|
|
1617
1758
|
if (extracted) variants.push(extracted);
|
|
1618
1759
|
}
|
|
1619
1760
|
else if (operatorName === "contains" && isJsonColumn(column)) variants.push(jsonContains(column, columnName, operatorValue));
|
|
1620
1761
|
else if (operatorName in safeLikeOps) {
|
|
1621
|
-
const
|
|
1622
|
-
variants.push(safeLikeCondition(column,
|
|
1762
|
+
const op = safeLikeOps[operatorName];
|
|
1763
|
+
variants.push(safeLikeCondition(column, operatorValue, op, op.insensitive || foldCase));
|
|
1623
1764
|
} else if (operatorName in arrayValueOps) {
|
|
1624
1765
|
if (!operatorValue.length) {
|
|
1625
1766
|
variants.push(operatorName === "inArray" ? sqlFalse() : sqlTrue());
|
|
@@ -1627,13 +1768,13 @@ const extractFiltersColumn = (column, columnName, operators) => {
|
|
|
1627
1768
|
}
|
|
1628
1769
|
const arrayValue = operatorValue.map((val) => remapFromGraphQLCore(val, column, columnName));
|
|
1629
1770
|
variants.push(foldCase ? lowerMembership(column, arrayValue, operatorName === "notInArray") : arrayValueOps[operatorName](column, arrayValue));
|
|
1630
|
-
} else if (operatorName === "has") variants.push(arrayContains(column, [operatorValue]));
|
|
1771
|
+
} else if (operatorName === "has") variants.push(arrayContains(column, [remapFromGraphQLCore(operatorValue, column, columnName)]));
|
|
1631
1772
|
else if (operatorName in arrayMembershipOps) {
|
|
1632
1773
|
if (!operatorValue.length) {
|
|
1633
1774
|
variants.push(operatorName === "hasSome" ? sqlFalse() : sqlTrue());
|
|
1634
1775
|
continue;
|
|
1635
1776
|
}
|
|
1636
|
-
variants.push(arrayMembershipOps[operatorName](column, operatorValue));
|
|
1777
|
+
variants.push(arrayMembershipOps[operatorName](column, operatorValue.map((element) => remapFromGraphQLCore(element, column, columnName))));
|
|
1637
1778
|
} else if (operatorName === "isEmpty") variants.push(sql`cardinality(${column}) = 0`);
|
|
1638
1779
|
else if (operatorName in nullableOps) variants.push(nullableOps[operatorName](column));
|
|
1639
1780
|
else throw drizzleError(`WHERE ${columnName}: Unknown operator: ${operatorName}`, { code: "DRIZZLE_INVALID_FILTER" });
|
|
@@ -1831,17 +1972,104 @@ const extractRelationJoinColumnPairs = (relEntry, parentTable, targetTable) => {
|
|
|
1831
1972
|
}
|
|
1832
1973
|
return pairs;
|
|
1833
1974
|
};
|
|
1975
|
+
/** A decimal written as text, in a form `canonicalDecimal` can normalise. */
|
|
1976
|
+
const DECIMAL_TEXT = /^([+-]?)(\d*)(?:\.(\d*))?$/;
|
|
1977
|
+
/**
|
|
1978
|
+
* Decimal text without the zeros that do not change its value: `1.500` and `01.5` are both
|
|
1979
|
+
* `1.5`, as SQL compares them. Anything else (an exponent, `NaN`) is returned unchanged.
|
|
1980
|
+
*/
|
|
1981
|
+
const canonicalDecimal = (value) => {
|
|
1982
|
+
const match = DECIMAL_TEXT.exec(value);
|
|
1983
|
+
if (!match || !match[2] && !match[3]) return value;
|
|
1984
|
+
const whole = match[2].replace(/^0+(?=\d)/, "") || "0";
|
|
1985
|
+
const fraction = (match[3] ?? "").replace(/0+$/, "");
|
|
1986
|
+
const digits = fraction ? `${whole}.${fraction}` : whole;
|
|
1987
|
+
return match[1] === "-" && digits !== "0" ? `-${digits}` : digits;
|
|
1988
|
+
};
|
|
1989
|
+
/** An ISO instant without trailing zeros in its fraction: `…:00.100Z` and `…:00.1Z` agree. */
|
|
1990
|
+
const trimIsoFraction = (iso) => iso.replace(/(\.\d*?)0+Z$/, "$1Z").replace(/\.Z$/, "Z");
|
|
1991
|
+
/**
|
|
1992
|
+
* One join value in a form both sides of a relation produce for the same SQL value. The parent's
|
|
1993
|
+
* key and a related row's can arrive in different JS forms — a `Date` and the text of the same
|
|
1994
|
+
* instant, `1.50` and `1.500` from decimals of different scale — and `String()` would tell them
|
|
1995
|
+
* apart (it even drops a `Date`'s milliseconds).
|
|
1996
|
+
*/
|
|
1997
|
+
const joinValueKey = (value, column) => {
|
|
1998
|
+
if (value instanceof Date) {
|
|
1999
|
+
if (Number.isNaN(value.getTime())) return String(value);
|
|
2000
|
+
if (column && isDateOnlyColumn(column)) return dateToCalendarDate(value) ?? value.toISOString();
|
|
2001
|
+
return trimIsoFraction(value.toISOString());
|
|
2002
|
+
}
|
|
2003
|
+
if (typeof value === "bigint") return value.toString();
|
|
2004
|
+
if (value instanceof Uint8Array) return Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("hex");
|
|
2005
|
+
if (typeof value === "string" && column) {
|
|
2006
|
+
if (isMySqlDateTimeStringColumn(column)) return trimIsoFraction(mySqlDateTimeTextToIso(value));
|
|
2007
|
+
if (isPgTimestampStringColumn(column)) return trimIsoFraction(pgTimestampTextToIso(value));
|
|
2008
|
+
if ((column.dataType ?? "") === "string numeric") return canonicalDecimal(value);
|
|
2009
|
+
}
|
|
2010
|
+
return String(value);
|
|
2011
|
+
};
|
|
1834
2012
|
/**
|
|
1835
2013
|
* A string identifying one combination of join-column values, for grouping a batch's rows by
|
|
1836
|
-
* parent.
|
|
2014
|
+
* parent. `columns` are the columns the values were read from, in the same order: the parent's
|
|
2015
|
+
* join columns for a parent key, the target's (or the junction's) for a related row. Values the
|
|
2016
|
+
* database compares equal give the same key — see {@link joinValueKey}.
|
|
2017
|
+
*/
|
|
2018
|
+
const joinKeyOf = (values, columns) => values.length === 1 ? joinValueKey(values[0], columns?.[0]) : JSON.stringify(values.map((value, i) => joinValueKey(value, columns?.[i])));
|
|
2019
|
+
/**
|
|
2020
|
+
* The parent row's values for the relation's join columns, as the database holds them. The row
|
|
2021
|
+
* a relation field resolves under has been remapped to its GraphQL form — a `Date` is an ISO
|
|
2022
|
+
* string by then, a `bigint` a decimal string — which can neither be bound back through every
|
|
2023
|
+
* column's encoder (SQLite's integer timestamp calls `getTime()` on it) nor be compared with the
|
|
2024
|
+
* related rows' decoded values. The values from before remapping are used when the row kept
|
|
2025
|
+
* them; otherwise the output form is mapped back the way an input value would be.
|
|
2026
|
+
*/
|
|
2027
|
+
const parentJoinValues = (parent, pairs) => pairs.map(({ localColPropName, localCol }) => {
|
|
2028
|
+
const raw = rawColumnValue(parent, localColPropName);
|
|
2029
|
+
if (raw.found) return raw.value;
|
|
2030
|
+
const value = parent[localColPropName];
|
|
2031
|
+
if (value == null) return value;
|
|
2032
|
+
try {
|
|
2033
|
+
return remapFromGraphQLCore(value, localCol, localColPropName);
|
|
2034
|
+
} catch {
|
|
2035
|
+
return value;
|
|
2036
|
+
}
|
|
2037
|
+
});
|
|
2038
|
+
/**
|
|
2039
|
+
* At most this many bound parameters for the parent keys of one batched relation query. SQLite
|
|
2040
|
+
* accepts 32,766 per statement, Postgres and MySQL 65,535; the rest is left to the request's own
|
|
2041
|
+
* filters. A batch with more parents runs as several queries.
|
|
2042
|
+
*/
|
|
2043
|
+
const MAX_PARENT_KEY_PARAMS = 1e4;
|
|
2044
|
+
/**
|
|
2045
|
+
* The parent keys of one batch split into the groups each query reads, so that no query binds
|
|
2046
|
+
* more than {@link MAX_PARENT_KEY_PARAMS} key values. Every parent lands in exactly one group, so
|
|
2047
|
+
* a per-parent result — its rows, its window, its count — is the same whichever query made it.
|
|
2048
|
+
*/
|
|
2049
|
+
const parentKeyChunks = (keys, width) => {
|
|
2050
|
+
const size = Math.max(1, Math.floor(MAX_PARENT_KEY_PARAMS / Math.max(1, width)));
|
|
2051
|
+
const chunks = [];
|
|
2052
|
+
for (let i = 0; i < keys.length; i += size) chunks.push(keys.slice(i, i + size));
|
|
2053
|
+
return chunks;
|
|
2054
|
+
};
|
|
2055
|
+
/**
|
|
2056
|
+
* `or` over `conditions` as a balanced tree. A flat `a OR b OR c …` is parsed left-deep, and
|
|
2057
|
+
* SQLite refuses an expression more than 1,000 levels deep; balanced, a batch of thousands of
|
|
2058
|
+
* parents stays a few levels deep.
|
|
1837
2059
|
*/
|
|
1838
|
-
const
|
|
2060
|
+
const balancedOr = (conditions) => {
|
|
2061
|
+
if (conditions.length <= 2) return or(...conditions);
|
|
2062
|
+
const middle = Math.ceil(conditions.length / 2);
|
|
2063
|
+
return or(balancedOr(conditions.slice(0, middle)), balancedOr(conditions.slice(middle)));
|
|
2064
|
+
};
|
|
1839
2065
|
/**
|
|
1840
2066
|
* The rows whose `cols` hold one of `keys`: `col IN (…)` for a single join column, and an `OR`
|
|
1841
2067
|
* of per-parent equalities for several — `(a = 1 AND b = 1) OR (a = 1 AND b = 2)` — which
|
|
1842
|
-
* every dialect accepts, unlike a row-value `IN` list.
|
|
2068
|
+
* every dialect accepts, unlike a row-value `IN` list. The `OR` is nested as a balanced tree.
|
|
2069
|
+
* Callers split a large batch with {@link parentKeyChunks} first, which keeps either form within
|
|
2070
|
+
* the dialects' parameter limits.
|
|
1843
2071
|
*/
|
|
1844
|
-
const parentKeyCondition = (cols, keys) => cols.length === 1 ? inArray(cols[0], keys.map((key) => key[0])) :
|
|
2072
|
+
const parentKeyCondition = (cols, keys) => cols.length === 1 ? inArray(cols[0], keys.map((key) => key[0])) : balancedOr(keys.map((key) => and(...cols.map((col, i) => eq(col, key[i])))));
|
|
1845
2073
|
/**
|
|
1846
2074
|
* The junction table and its two columns for a `.through()` (many-to-many) relation:
|
|
1847
2075
|
* `sourceCol` holds the parent's key, `targetCol` the target's. `undefined` for a relation
|
|
@@ -2394,6 +2622,32 @@ const withScope = (scope, tableName, table, condition, mode) => {
|
|
|
2394
2622
|
const predicate = scope?.on(tableName, table, mode);
|
|
2395
2623
|
return predicate ? and(condition, predicate) : condition;
|
|
2396
2624
|
};
|
|
2625
|
+
/**
|
|
2626
|
+
* The junction table a `.through()` relation links through, by its schema key — the key its
|
|
2627
|
+
* `scope` and `softDelete` are configured under. `undefined` for a relation with no junction,
|
|
2628
|
+
* or a junction that is not one of the build's tables.
|
|
2629
|
+
*/
|
|
2630
|
+
const junctionTableName = (tables, relEntry) => {
|
|
2631
|
+
const throughTable = (relEntry?.relation ?? relEntry)?.throughTable;
|
|
2632
|
+
return throughTable ? Object.keys(tables).find((name) => tables[name] === throughTable) : void 0;
|
|
2633
|
+
};
|
|
2634
|
+
/**
|
|
2635
|
+
* Whether a `.through()` relation's junction restricts which links exist for this request —
|
|
2636
|
+
* a `scope` on the junction table, or a soft delete it declares.
|
|
2637
|
+
*/
|
|
2638
|
+
const hasJunctionPolicy = (scope, junctionName) => !!junctionName && !!scope?.has(junctionName, "EXCLUDE");
|
|
2639
|
+
/**
|
|
2640
|
+
* The junction's own row policy, compiled against the junction table as the query names it.
|
|
2641
|
+
* A `.through()` relation links only through the junction rows this keeps, on every path that
|
|
2642
|
+
* reads the relation — the relation field, `<relation>Aggregate`, and a relation filter — so
|
|
2643
|
+
* they all agree on which rows are related.
|
|
2644
|
+
*
|
|
2645
|
+
* The junction is always read at `EXCLUDE`: a marked junction row is a removed link. The
|
|
2646
|
+
* relation's `deleted` argument is about the *target* rows, and the junction's own
|
|
2647
|
+
* `softDelete.scope` only governs reads of the junction table's own fields — a link row has no
|
|
2648
|
+
* field of its own on the relation that `'root'` could keep rendering.
|
|
2649
|
+
*/
|
|
2650
|
+
const junctionPolicy = (scope, junctionName, table) => hasJunctionPolicy(scope, junctionName) ? scope.on(junctionName, table, "EXCLUDE") : void 0;
|
|
2397
2651
|
/** Binds a build's {@link TablePolicies} to its relation context, once, at schema-build time. */
|
|
2398
2652
|
const bindPolicies = (policies, filterCtx) => policies?.scope || policies?.contextValues || policies?.softDelete || policies?.onWrite || policies?.defaultOrderBy ? {
|
|
2399
2653
|
scope: policies.scope || policies.softDelete ? (context) => resolveScope(policies, context, filterCtx) : void 0,
|
|
@@ -2444,7 +2698,7 @@ const deletedArg = (softDelete, tableName, resolveName) => softDelete?.(tableNam
|
|
|
2444
2698
|
const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableName, resolveName) => ({
|
|
2445
2699
|
offset: { type: GraphQLInt },
|
|
2446
2700
|
limit: { type: GraphQLInt },
|
|
2447
|
-
orderBy: { type: orderArgs },
|
|
2701
|
+
...orderArgs ? { orderBy: { type: orderArgs } } : {},
|
|
2448
2702
|
where: { type: filterArgs },
|
|
2449
2703
|
after: {
|
|
2450
2704
|
type: GraphQLString,
|
|
@@ -2456,7 +2710,7 @@ const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableN
|
|
|
2456
2710
|
/** GraphQL argument map for a single-row select field (no `limit`). */
|
|
2457
2711
|
const selectSingleArgs = (orderArgs, filterArgs, softDelete, tableName, resolveName) => ({
|
|
2458
2712
|
offset: { type: GraphQLInt },
|
|
2459
|
-
orderBy: { type: orderArgs },
|
|
2713
|
+
...orderArgs ? { orderBy: { type: orderArgs } } : {},
|
|
2460
2714
|
where: { type: filterArgs },
|
|
2461
2715
|
...deletedArg(softDelete, tableName, resolveName)
|
|
2462
2716
|
});
|
|
@@ -2506,7 +2760,7 @@ const generateDistinctEnum = (table, typeName, resolveName, tableKey) => {
|
|
|
2506
2760
|
defaultName,
|
|
2507
2761
|
table: tableKey,
|
|
2508
2762
|
operation: "distinct"
|
|
2509
|
-
}) ?? defaultName, `Columns of ${typeName} that a query can be made distinct on
|
|
2763
|
+
}) ?? defaultName, `Columns of ${typeName} that a query can be made distinct on`, (column) => !isJsonColumn(column));
|
|
2510
2764
|
};
|
|
2511
2765
|
//#endregion
|
|
2512
2766
|
//#region src/util/builders/common/conflict.ts
|
|
@@ -2676,6 +2930,44 @@ const resolveConflictPlan = (params) => {
|
|
|
2676
2930
|
plan
|
|
2677
2931
|
}));
|
|
2678
2932
|
};
|
|
2933
|
+
/**
|
|
2934
|
+
* Adds a {@link ConflictPlan}'s clause to a PostgreSQL or SQLite insert.
|
|
2935
|
+
*
|
|
2936
|
+
* SQLite needs two things drizzle's builder does not give it when the target is a partial
|
|
2937
|
+
* unique index:
|
|
2938
|
+
*
|
|
2939
|
+
* - The index predicate written literally. SQLite matches a conflict target to an index by
|
|
2940
|
+
* comparing the predicates as written, and a bound `?` never equals `'active'`, so a
|
|
2941
|
+
* predicate that compares to a value would never find its index. Its params are inlined.
|
|
2942
|
+
* - A `DO NOTHING` it can parse. Drizzle writes the predicate after `do nothing`, which is a
|
|
2943
|
+
* syntax error, so the action is spelled as an update that matches no row instead:
|
|
2944
|
+
* `ON CONFLICT (…) WHERE … DO UPDATE SET col = col WHERE 0 = 1`. A row that is not updated is
|
|
2945
|
+
* not returned, which is what `DO NOTHING` does too.
|
|
2946
|
+
*/
|
|
2947
|
+
const applyConflictPlan = (query, table, plan, setWhere) => {
|
|
2948
|
+
const sqlite = is(table, SQLiteTable);
|
|
2949
|
+
const targetWhere = sqlite && plan.targetWhere ? sql`${plan.targetWhere}`.inlineParams() : plan.targetWhere;
|
|
2950
|
+
if (plan.action === "UPDATE") return query.onConflictDoUpdate({
|
|
2951
|
+
target: plan.target,
|
|
2952
|
+
targetWhere,
|
|
2953
|
+
set: plan.set,
|
|
2954
|
+
setWhere
|
|
2955
|
+
});
|
|
2956
|
+
if (!plan.target) return query.onConflictDoNothing();
|
|
2957
|
+
if (sqlite && targetWhere) {
|
|
2958
|
+
const [key] = Object.entries(getColumns(table)).find(([, column]) => column === plan.target[0]);
|
|
2959
|
+
return query.onConflictDoUpdate({
|
|
2960
|
+
target: plan.target,
|
|
2961
|
+
targetWhere,
|
|
2962
|
+
set: { [key]: sql`${plan.target[0]}` },
|
|
2963
|
+
setWhere: sql`0 = 1`
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
return query.onConflictDoNothing({
|
|
2967
|
+
target: plan.target,
|
|
2968
|
+
where: targetWhere
|
|
2969
|
+
});
|
|
2970
|
+
};
|
|
2679
2971
|
/** `excluded.<column>` — PostgreSQL and SQLite name the rejected row this way. */
|
|
2680
2972
|
const excludedColumnRef = (columnName) => sql`excluded.${sql.identifier(columnName)}`;
|
|
2681
2973
|
/** `values(<column>)` — MySQL's equivalent inside ON DUPLICATE KEY UPDATE. */
|
|
@@ -2957,6 +3249,21 @@ const decodeCursor = (after, entries) => {
|
|
|
2957
3249
|
return payload.v.map(decodeCursorValue);
|
|
2958
3250
|
};
|
|
2959
3251
|
/**
|
|
3252
|
+
* `col > value` (asc) or `col < value` (desc), compared by the rule the ORDER BY sorts by.
|
|
3253
|
+
* MySQL sorts an ENUM by its declaration index but compares it with a string as a string, so
|
|
3254
|
+
* there the comparison is between `col + 0` (the 1-based index) and the value's own index.
|
|
3255
|
+
*/
|
|
3256
|
+
const strictComparison = (col, direction, value) => {
|
|
3257
|
+
if (is(col, MySqlEnumColumn) || is(col, MySqlEnumObjectColumn)) {
|
|
3258
|
+
const index = col.enumValues?.indexOf(value) ?? -1;
|
|
3259
|
+
if (index >= 0) {
|
|
3260
|
+
const position = sql`(${col} + 0)`;
|
|
3261
|
+
return direction === "asc" ? sql`${position} > ${index + 1}` : sql`${position} < ${index + 1}`;
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
return direction === "asc" ? gt(col, value) : lt(col, value);
|
|
3265
|
+
};
|
|
3266
|
+
/**
|
|
2960
3267
|
* The keyset predicate selecting rows strictly after the cursor position in the given total
|
|
2961
3268
|
* order — the expanded lexicographic form
|
|
2962
3269
|
* `after(k0) OR (k0 = v0 AND after(k1)) OR (k0 = v0 AND k1 = v1 AND after(k2)) …`,
|
|
@@ -2981,7 +3288,7 @@ const buildCursorCondition = (table, entries, values, nullOrdering) => {
|
|
|
2981
3288
|
let strictlyAfter;
|
|
2982
3289
|
if (value === null || value === void 0) strictlyAfter = nullsSortLast ? void 0 : isNotNull(col);
|
|
2983
3290
|
else {
|
|
2984
|
-
const comparison =
|
|
3291
|
+
const comparison = strictComparison(col, direction, value);
|
|
2985
3292
|
strictlyAfter = nullsSortLast ? or(comparison, isNull(col)) : comparison;
|
|
2986
3293
|
}
|
|
2987
3294
|
if (strictlyAfter) disjuncts.push(equalities.length ? and(...equalities, strictlyAfter) : strictlyAfter);
|
|
@@ -3184,6 +3491,115 @@ const getUniqueTargets = (table, getTableConfig) => {
|
|
|
3184
3491
|
*/
|
|
3185
3492
|
const getUniqueColumnSets = (table, getTableConfig, options = {}) => getUniqueTargets(table, getTableConfig).filter((target) => options.partial || !target.where).map((target) => target.columns);
|
|
3186
3493
|
//#endregion
|
|
3494
|
+
//#region src/util/builders/common/limits.ts
|
|
3495
|
+
/**
|
|
3496
|
+
* The `limit` to send with an `offset` the request gave without one. SQLite and MySQL accept
|
|
3497
|
+
* `OFFSET` only after a `LIMIT`, so `offset` alone has to carry a limit that means "all of
|
|
3498
|
+
* them": SQLite reads `LIMIT -1` that way, and on MySQL it is the largest integer every driver
|
|
3499
|
+
* binds exactly (MySQL's own documented 2^64-1 does not fit a JS number). PostgreSQL takes
|
|
3500
|
+
* `OFFSET` alone, so the limit is left as it was.
|
|
3501
|
+
*/
|
|
3502
|
+
const offsetOnlyLimit = (table, limit, offset) => {
|
|
3503
|
+
if (limit != null || !offset) return limit;
|
|
3504
|
+
const firstColumn = Object.values(getColumns(table))[0];
|
|
3505
|
+
switch (firstColumn ? columnDialect(firstColumn) : void 0) {
|
|
3506
|
+
case "sqlite": return -1;
|
|
3507
|
+
case "mysql": return Number.MAX_SAFE_INTEGER;
|
|
3508
|
+
default: return limit;
|
|
3509
|
+
}
|
|
3510
|
+
};
|
|
3511
|
+
/**
|
|
3512
|
+
* Substitutes a table's default ordering for an absent `orderBy`.
|
|
3513
|
+
*
|
|
3514
|
+
* Applied where the arguments are first read, so everything downstream — the cursor tuple, a
|
|
3515
|
+
* `distinct` pass, the plain-select fallback — sees one effective ordering and cannot
|
|
3516
|
+
* disagree about it. Only a missing argument is replaced: `orderBy: {}` is a request for no
|
|
3517
|
+
* ordering, and stays one.
|
|
3518
|
+
*/
|
|
3519
|
+
const withDefaultOrderBy = (args, tableName, defaults) => {
|
|
3520
|
+
if (!defaults || args?.orderBy != null) return args;
|
|
3521
|
+
const fallback = defaults(tableName);
|
|
3522
|
+
return fallback ? {
|
|
3523
|
+
...args,
|
|
3524
|
+
orderBy: fallback
|
|
3525
|
+
} : args;
|
|
3526
|
+
};
|
|
3527
|
+
/**
|
|
3528
|
+
* Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
|
|
3529
|
+
* dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
|
|
3530
|
+
* straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
|
|
3531
|
+
* whether or not the table has a limit policy.
|
|
3532
|
+
*/
|
|
3533
|
+
const assertPagingNotNegative = (paging, errorCtx) => {
|
|
3534
|
+
for (const name of ["limit", "offset"]) {
|
|
3535
|
+
const value = paging[name];
|
|
3536
|
+
if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
|
|
3537
|
+
code: "DRIZZLE_INVALID_INPUT_VALUE",
|
|
3538
|
+
...errorCtx
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
};
|
|
3542
|
+
/**
|
|
3543
|
+
* The `limit` a query actually runs with, given the policy for the table it reads.
|
|
3544
|
+
*
|
|
3545
|
+
* A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
|
|
3546
|
+
* that is configured — an absent limit means every row, which is above any maximum. A limit
|
|
3547
|
+
* the client did ask for is rejected when it exceeds `maxLimit`, since silently truncating a
|
|
3548
|
+
* page tells a paginating client it has reached the end when it has not; `clampToMax` opts
|
|
3549
|
+
* into the truncation instead. A limit the *policy* supplied is capped rather than rejected —
|
|
3550
|
+
* a misconfigured `defaultLimit` above `maxLimit` is the operator's problem, not something to
|
|
3551
|
+
* fail a client's query over.
|
|
3552
|
+
*/
|
|
3553
|
+
const applyLimitPolicy = (limit, policy, errorCtx) => {
|
|
3554
|
+
assertPagingNotNegative({ limit }, errorCtx);
|
|
3555
|
+
if (!policy) return limit ?? void 0;
|
|
3556
|
+
const { defaultLimit, maxLimit, clampToMax } = policy;
|
|
3557
|
+
if (limit == null) {
|
|
3558
|
+
const fallback = defaultLimit ?? maxLimit;
|
|
3559
|
+
if (fallback === void 0) return;
|
|
3560
|
+
return maxLimit === void 0 ? fallback : Math.min(fallback, maxLimit);
|
|
3561
|
+
}
|
|
3562
|
+
if (maxLimit !== void 0 && limit > maxLimit) {
|
|
3563
|
+
if (clampToMax) return maxLimit;
|
|
3564
|
+
throw drizzleError(`'limit' of ${limit} exceeds the maximum of ${maxLimit}.`, {
|
|
3565
|
+
code: "DRIZZLE_LIMIT_EXCEEDED",
|
|
3566
|
+
...errorCtx
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
return limit;
|
|
3570
|
+
};
|
|
3571
|
+
/** Merges a per-table override over the global policy, or `undefined` when neither bounds anything. */
|
|
3572
|
+
const resolveLimitPolicy = (global, table) => {
|
|
3573
|
+
const defaultLimit = table?.defaultLimit ?? global?.defaultLimit;
|
|
3574
|
+
const maxLimit = table?.maxLimit ?? global?.maxLimit;
|
|
3575
|
+
if (defaultLimit === void 0 && maxLimit === void 0) return;
|
|
3576
|
+
return {
|
|
3577
|
+
defaultLimit,
|
|
3578
|
+
maxLimit,
|
|
3579
|
+
clampToMax: table?.clampToMax ?? global?.clampToMax ?? false
|
|
3580
|
+
};
|
|
3581
|
+
};
|
|
3582
|
+
/**
|
|
3583
|
+
* A paginated field costs its page size times whatever one row of it costs, so `users(limit: 100)
|
|
3584
|
+
* { posts(limit: 10) { id } }` is charged for the thousand rows it can return rather than the two
|
|
3585
|
+
* fields it mentions. `childComplexity` floors at 1 so a row is never free.
|
|
3586
|
+
*
|
|
3587
|
+
* The page size is the *effective* one: a request without `limit` is priced at the policy's
|
|
3588
|
+
* `defaultLimit` (or its `maxLimit` when that is all it sets) rather than the estimator's guess, and no request is priced above `maxLimit`,
|
|
3589
|
+
* so the cost a query is charged matches the rows it can actually pull.
|
|
3590
|
+
*/
|
|
3591
|
+
const listFieldComplexity = (options, policy) => ({ args, childComplexity }) => {
|
|
3592
|
+
const limit = args["limit"];
|
|
3593
|
+
let rows = (typeof limit === "number" && limit > 0 ? limit : void 0) ?? policy?.defaultLimit ?? policy?.maxLimit ?? options.defaultListSize;
|
|
3594
|
+
if (policy?.maxLimit !== void 0) rows = Math.min(rows, policy.maxLimit);
|
|
3595
|
+
return rows * Math.max(childComplexity, 1);
|
|
3596
|
+
};
|
|
3597
|
+
/**
|
|
3598
|
+
* An aggregate returns a single row but reads however many match, so its cost tracks the scan
|
|
3599
|
+
* rather than the response.
|
|
3600
|
+
*/
|
|
3601
|
+
const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
|
|
3602
|
+
//#endregion
|
|
3187
3603
|
//#region src/util/builders/common/distinct.ts
|
|
3188
3604
|
/**
|
|
3189
3605
|
* Alias the parent key comes back under when the pass runs through a junction table. See
|
|
@@ -3231,13 +3647,15 @@ const selectDistinctKeys = async (params) => {
|
|
|
3231
3647
|
...junction ? { [DISTINCT_PARENT]: sub[DISTINCT_PARENT] } : {}
|
|
3232
3648
|
}).from(sub).where(eq(sub[DISTINCT_RN], 1)).orderBy(...outerOrder);
|
|
3233
3649
|
if (offset) query = query.offset(offset);
|
|
3234
|
-
|
|
3650
|
+
const effectiveLimit = offsetOnlyLimit(table, limit, offset);
|
|
3651
|
+
if (effectiveLimit != null) query = query.limit(effectiveLimit < 0 ? sql`${effectiveLimit}` : effectiveLimit);
|
|
3235
3652
|
return await query;
|
|
3236
3653
|
};
|
|
3237
3654
|
/**
|
|
3238
3655
|
* Condition matching exactly the rows identified by `keys` — an `IN (…)` for a single-column
|
|
3239
|
-
* primary key, an `OR` of per-row equality for a composite one
|
|
3240
|
-
*
|
|
3656
|
+
* primary key, an `OR` of per-row equality for a composite one — nested as a balanced tree, so
|
|
3657
|
+
* thousands of keys stay within SQLite's expression-depth limit. `table` may be the aliased RQB
|
|
3658
|
+
* proxy.
|
|
3241
3659
|
*/
|
|
3242
3660
|
const primaryKeyRestriction = (table, pkNames, keys) => {
|
|
3243
3661
|
const cols = getColumns(table);
|
|
@@ -3245,7 +3663,7 @@ const primaryKeyRestriction = (table, pkNames, keys) => {
|
|
|
3245
3663
|
const name = pkNames[0];
|
|
3246
3664
|
return inArray(cols[name], keys.map((key) => key[name]));
|
|
3247
3665
|
}
|
|
3248
|
-
return
|
|
3666
|
+
return balancedOr(keys.map((key) => and(...pkNames.map((name) => eq(cols[name], key[name])))));
|
|
3249
3667
|
};
|
|
3250
3668
|
//#endregion
|
|
3251
3669
|
//#region src/util/builders/common/docs.ts
|
|
@@ -3391,99 +3809,6 @@ const innerOrderType = (typeName) => sharedType(typeName, {
|
|
|
3391
3809
|
}
|
|
3392
3810
|
}));
|
|
3393
3811
|
//#endregion
|
|
3394
|
-
//#region src/util/builders/common/limits.ts
|
|
3395
|
-
/**
|
|
3396
|
-
* Substitutes a table's default ordering for an absent `orderBy`.
|
|
3397
|
-
*
|
|
3398
|
-
* Applied where the arguments are first read, so everything downstream — the cursor tuple, a
|
|
3399
|
-
* `distinct` pass, the plain-select fallback — sees one effective ordering and cannot
|
|
3400
|
-
* disagree about it. Only a missing argument is replaced: `orderBy: {}` is a request for no
|
|
3401
|
-
* ordering, and stays one.
|
|
3402
|
-
*/
|
|
3403
|
-
const withDefaultOrderBy = (args, tableName, defaults) => {
|
|
3404
|
-
if (!defaults || args?.orderBy != null) return args;
|
|
3405
|
-
const fallback = defaults(tableName);
|
|
3406
|
-
return fallback ? {
|
|
3407
|
-
...args,
|
|
3408
|
-
orderBy: fallback
|
|
3409
|
-
} : args;
|
|
3410
|
-
};
|
|
3411
|
-
/**
|
|
3412
|
-
* Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
|
|
3413
|
-
* dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
|
|
3414
|
-
* straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
|
|
3415
|
-
* whether or not the table has a limit policy.
|
|
3416
|
-
*/
|
|
3417
|
-
const assertPagingNotNegative = (paging, errorCtx) => {
|
|
3418
|
-
for (const name of ["limit", "offset"]) {
|
|
3419
|
-
const value = paging[name];
|
|
3420
|
-
if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
|
|
3421
|
-
code: "DRIZZLE_INVALID_INPUT_VALUE",
|
|
3422
|
-
...errorCtx
|
|
3423
|
-
});
|
|
3424
|
-
}
|
|
3425
|
-
};
|
|
3426
|
-
/**
|
|
3427
|
-
* The `limit` a query actually runs with, given the policy for the table it reads.
|
|
3428
|
-
*
|
|
3429
|
-
* A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
|
|
3430
|
-
* that is configured — an absent limit means every row, which is above any maximum. A limit
|
|
3431
|
-
* the client did ask for is rejected when it exceeds `maxLimit`, since silently truncating a
|
|
3432
|
-
* page tells a paginating client it has reached the end when it has not; `clampToMax` opts
|
|
3433
|
-
* into the truncation instead. A limit the *policy* supplied is capped rather than rejected —
|
|
3434
|
-
* a misconfigured `defaultLimit` above `maxLimit` is the operator's problem, not something to
|
|
3435
|
-
* fail a client's query over.
|
|
3436
|
-
*/
|
|
3437
|
-
const applyLimitPolicy = (limit, policy, errorCtx) => {
|
|
3438
|
-
assertPagingNotNegative({ limit }, errorCtx);
|
|
3439
|
-
if (!policy) return limit ?? void 0;
|
|
3440
|
-
const { defaultLimit, maxLimit, clampToMax } = policy;
|
|
3441
|
-
if (limit == null) {
|
|
3442
|
-
const fallback = defaultLimit ?? maxLimit;
|
|
3443
|
-
if (fallback === void 0) return;
|
|
3444
|
-
return maxLimit === void 0 ? fallback : Math.min(fallback, maxLimit);
|
|
3445
|
-
}
|
|
3446
|
-
if (maxLimit !== void 0 && limit > maxLimit) {
|
|
3447
|
-
if (clampToMax) return maxLimit;
|
|
3448
|
-
throw drizzleError(`'limit' of ${limit} exceeds the maximum of ${maxLimit}.`, {
|
|
3449
|
-
code: "DRIZZLE_LIMIT_EXCEEDED",
|
|
3450
|
-
...errorCtx
|
|
3451
|
-
});
|
|
3452
|
-
}
|
|
3453
|
-
return limit;
|
|
3454
|
-
};
|
|
3455
|
-
/** Merges a per-table override over the global policy, or `undefined` when neither bounds anything. */
|
|
3456
|
-
const resolveLimitPolicy = (global, table) => {
|
|
3457
|
-
const defaultLimit = table?.defaultLimit ?? global?.defaultLimit;
|
|
3458
|
-
const maxLimit = table?.maxLimit ?? global?.maxLimit;
|
|
3459
|
-
if (defaultLimit === void 0 && maxLimit === void 0) return;
|
|
3460
|
-
return {
|
|
3461
|
-
defaultLimit,
|
|
3462
|
-
maxLimit,
|
|
3463
|
-
clampToMax: table?.clampToMax ?? global?.clampToMax ?? false
|
|
3464
|
-
};
|
|
3465
|
-
};
|
|
3466
|
-
/**
|
|
3467
|
-
* A paginated field costs its page size times whatever one row of it costs, so `users(limit: 100)
|
|
3468
|
-
* { posts(limit: 10) { id } }` is charged for the thousand rows it can return rather than the two
|
|
3469
|
-
* fields it mentions. `childComplexity` floors at 1 so a row is never free.
|
|
3470
|
-
*
|
|
3471
|
-
* The page size is the *effective* one: a request without `limit` is priced at the policy's
|
|
3472
|
-
* `defaultLimit` rather than the estimator's guess, and no request is priced above `maxLimit`,
|
|
3473
|
-
* so the cost a query is charged matches the rows it can actually pull.
|
|
3474
|
-
*/
|
|
3475
|
-
const listFieldComplexity = (options, policy) => ({ args, childComplexity }) => {
|
|
3476
|
-
const limit = args["limit"];
|
|
3477
|
-
let rows = (typeof limit === "number" && limit > 0 ? limit : void 0) ?? policy?.defaultLimit ?? options.defaultListSize;
|
|
3478
|
-
if (policy?.maxLimit !== void 0) rows = Math.min(rows, policy.maxLimit);
|
|
3479
|
-
return rows * Math.max(childComplexity, 1);
|
|
3480
|
-
};
|
|
3481
|
-
/**
|
|
3482
|
-
* An aggregate returns a single row but reads however many match, so its cost tracks the scan
|
|
3483
|
-
* rather than the response.
|
|
3484
|
-
*/
|
|
3485
|
-
const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
|
|
3486
|
-
//#endregion
|
|
3487
3812
|
//#region src/util/parse-resolve-info.ts
|
|
3488
3813
|
/** Unwraps `[T]!`-style wrappers down to the named type. `getNamedType` without the realm tie. */
|
|
3489
3814
|
const namedTypeOf = (type) => {
|
|
@@ -3647,14 +3972,10 @@ const NUMERIC_SCALARS = /* @__PURE__ */ new Set([
|
|
|
3647
3972
|
]);
|
|
3648
3973
|
/**
|
|
3649
3974
|
* Column types that convert to a GraphQL list without being a list *in the database* —
|
|
3650
|
-
* a bytea reads as `[Int]`, a vector
|
|
3651
|
-
* them is not the operation `push` means.
|
|
3975
|
+
* a bytea or SQLite buffer blob reads as `[Int]`, a vector, a point or a line as `[Float]` —
|
|
3976
|
+
* so appending to them is not the operation `push` means.
|
|
3652
3977
|
*/
|
|
3653
|
-
const
|
|
3654
|
-
"PgVector",
|
|
3655
|
-
"PgGeometry",
|
|
3656
|
-
"PgBytea"
|
|
3657
|
-
]);
|
|
3978
|
+
const isNonListArrayColumn = (column) => isFloatTupleColumn(column) || isBufferColumn(column);
|
|
3658
3979
|
const kindCache = /* @__PURE__ */ new WeakMap();
|
|
3659
3980
|
/**
|
|
3660
3981
|
* Whether a column takes an operations input, and which one.
|
|
@@ -3671,7 +3992,7 @@ const fieldUpdateKind = (column, columnName, tableName) => {
|
|
|
3671
3992
|
if (!getColumnScalarOverride(column, true)) {
|
|
3672
3993
|
const { type } = drizzleColumnToGraphQLType(column, columnName, tableName, true, false, true);
|
|
3673
3994
|
if (NUMERIC_SCALARS.has(type)) kind = "numeric";
|
|
3674
|
-
else if (type instanceof GraphQLList && !
|
|
3995
|
+
else if (type instanceof GraphQLList && !isNonListArrayColumn(column)) kind = "list";
|
|
3675
3996
|
}
|
|
3676
3997
|
kindCache.set(column, kind);
|
|
3677
3998
|
return kind;
|
|
@@ -3942,6 +4263,16 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
|
|
|
3942
4263
|
if (!relFieldSelection) continue;
|
|
3943
4264
|
const eagerArgs = field.args;
|
|
3944
4265
|
if (!is(relEntry.relation ?? relEntry, One) && (eagerArgs?.after != null || !!(eagerArgs?.distinct)?.length || isCursorFieldSelected(relFieldSelection, tables[targetTableName]))) continue;
|
|
4266
|
+
const junctionName = junctionTableName(tables, relEntry);
|
|
4267
|
+
if (hasJunctionPolicy(scope, junctionName)) {
|
|
4268
|
+
if (!extractJunctionColumns(relEntry)) throw drizzleError(`Relation '${tableName}.${relName}' goes through '${junctionName}', which has a scope or soft delete, over a composite key. Its links can only be filtered when the junction joins on one column per side.`, {
|
|
4269
|
+
code: "DRIZZLE_UNSUPPORTED_RELATION",
|
|
4270
|
+
table: tableName,
|
|
4271
|
+
operation: "relation",
|
|
4272
|
+
relation: relName
|
|
4273
|
+
});
|
|
4274
|
+
continue;
|
|
4275
|
+
}
|
|
3945
4276
|
const columns = extractSelectedColumnsFromTree(relFieldSelection, tables[targetTableName], {
|
|
3946
4277
|
tableName: targetTableName,
|
|
3947
4278
|
relationMap,
|
|
@@ -3967,9 +4298,11 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
|
|
|
3967
4298
|
thisRecord.where = relWhere || scope?.has(targetTableName, relDeleted) ? { RAW: (aliasedTable) => withScope(scope, targetTableName, aliasedTable, relWhere ? extractFilters(aliasedTable, relName, relWhere, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, relDeleted) } : void 0;
|
|
3968
4299
|
const hasPagination = offset != null || limit != null;
|
|
3969
4300
|
const pkNames = targetPkNames ?? [];
|
|
3970
|
-
|
|
4301
|
+
const tiebreak = hasPagination && pkNames.length > 0;
|
|
4302
|
+
const requestOrder = relationArgs?.orderBy;
|
|
4303
|
+
thisRecord.orderBy = requestOrder || tiebreak ? (aliasedTable) => [...requestOrder ? extractOrderBy(aliasedTable, requestOrder, relationFilterCtx(filterCtx, targetTableName, scope), relWhere) : [], ...tiebreak ? primaryKeyOrderExprs(aliasedTable, pkNames) : []] : void 0;
|
|
3971
4304
|
thisRecord.offset = offset;
|
|
3972
|
-
thisRecord.limit = limit;
|
|
4305
|
+
thisRecord.limit = offsetOnlyLimit(tables[targetTableName], limit, offset);
|
|
3973
4306
|
thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
|
|
3974
4307
|
args[relName] = omitUndefinedKeys(thisRecord);
|
|
3975
4308
|
}
|
|
@@ -4060,7 +4393,7 @@ const runRelationalSelect = async (opts) => {
|
|
|
4060
4393
|
const preciseProps = Object.keys(cursorPreciseExtras(table, cursorEntries));
|
|
4061
4394
|
if (preciseProps.length) params.extras = Object.fromEntries(preciseProps.map((prop) => [prop, (aliasedTable) => cursorPreciseExtras(aliasedTable, cursorEntries)[prop]]));
|
|
4062
4395
|
}
|
|
4063
|
-
params.limit = distinctKeys ? void 0 : opts.limit;
|
|
4396
|
+
params.limit = distinctKeys ? void 0 : offsetOnlyLimit(table, opts.limit, offset);
|
|
4064
4397
|
const result = await queryBase.findMany(omitUndefinedKeys(params));
|
|
4065
4398
|
if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
|
|
4066
4399
|
return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
|
|
@@ -4117,6 +4450,8 @@ const eagerLoadMutationRelations = async (db, tableName, rows, pkNames, withPara
|
|
|
4117
4450
|
}
|
|
4118
4451
|
return rows;
|
|
4119
4452
|
};
|
|
4453
|
+
/** The longest delay `setTimeout` honours; anything above, Infinity included, fires after 1 ms. */
|
|
4454
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
4120
4455
|
const createMutationTxCtx = (options) => options ? {
|
|
4121
4456
|
timeoutMs: options.timeoutMs,
|
|
4122
4457
|
fieldNames: /* @__PURE__ */ new Set(),
|
|
@@ -4143,28 +4478,28 @@ const includedByDirectives = (node, variables) => {
|
|
|
4143
4478
|
* this build did not generate — its completion cannot be tracked, so the caller must not
|
|
4144
4479
|
* open a transaction it might never be able to close.
|
|
4145
4480
|
*/
|
|
4146
|
-
const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments) => {
|
|
4481
|
+
const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce) => {
|
|
4147
4482
|
for (const selection of selectionSet.selections) {
|
|
4148
4483
|
if (!includedByDirectives(selection, variables)) continue;
|
|
4149
4484
|
if (selection.kind === "Field") {
|
|
4150
4485
|
const name = selection.name.value;
|
|
4151
4486
|
if (name === "__typename") continue;
|
|
4152
4487
|
if (!known.has(name)) return false;
|
|
4153
|
-
keys.add(selection.alias?.value ?? name);
|
|
4488
|
+
if (argumentsCoerce(selection)) keys.add(selection.alias?.value ?? name);
|
|
4154
4489
|
} else if (selection.kind === "InlineFragment") {
|
|
4155
|
-
if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
|
|
4490
|
+
if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
|
|
4156
4491
|
} else if (selection.kind === "FragmentSpread") {
|
|
4157
4492
|
const fragmentName = selection.name.value;
|
|
4158
4493
|
if (visitedFragments.has(fragmentName)) continue;
|
|
4159
4494
|
visitedFragments.add(fragmentName);
|
|
4160
4495
|
const fragment = fragments?.[fragmentName];
|
|
4161
4496
|
if (!fragment) return false;
|
|
4162
|
-
if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
|
|
4497
|
+
if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
|
|
4163
4498
|
}
|
|
4164
4499
|
}
|
|
4165
4500
|
return true;
|
|
4166
4501
|
};
|
|
4167
|
-
const abortedBatchError = () => drizzleError("Drizzle-GraphQL Error: Mutation was not executed because an earlier mutation in the same request failed and the shared transaction was rolled back.", { code: "DRIZZLE_TRANSACTION_ABORTED" });
|
|
4502
|
+
const abortedBatchError = (state, timeoutMs) => drizzleError(state.timedOut ? `Drizzle-GraphQL Error: Mutation was not executed because the shared transaction timed out after ${timeoutMs}ms waiting for the remaining mutation fields and was rolled back.` : "Drizzle-GraphQL Error: Mutation was not executed because an earlier mutation in the same request failed and the shared transaction was rolled back.", { code: "DRIZZLE_TRANSACTION_ABORTED" });
|
|
4168
4503
|
/** Opens the request-wide transaction and wires up latch, inactivity timer and cleanup. */
|
|
4169
4504
|
const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
4170
4505
|
let release;
|
|
@@ -4199,6 +4534,7 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
|
4199
4534
|
completed: 0,
|
|
4200
4535
|
failure: void 0,
|
|
4201
4536
|
aborted: false,
|
|
4537
|
+
timedOut: false,
|
|
4202
4538
|
done: false,
|
|
4203
4539
|
ready,
|
|
4204
4540
|
settled,
|
|
@@ -4213,8 +4549,9 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
|
4213
4549
|
suspendTimer: clearTimer,
|
|
4214
4550
|
bumpTimer: () => {
|
|
4215
4551
|
clearTimer();
|
|
4216
|
-
if (state.done) return;
|
|
4552
|
+
if (state.done || txCtx.timeoutMs === Number.POSITIVE_INFINITY) return;
|
|
4217
4553
|
timer = setTimeout(() => {
|
|
4554
|
+
if (!state.aborted) state.timedOut = true;
|
|
4218
4555
|
state.abort(drizzleError(`Drizzle-GraphQL Error: Shared mutation transaction timed out after ${txCtx.timeoutMs}ms waiting for the remaining mutation fields; rolling back.`, { code: "DRIZZLE_TRANSACTION_ABORTED" }));
|
|
4219
4556
|
}, txCtx.timeoutMs);
|
|
4220
4557
|
timer.unref?.();
|
|
@@ -4233,15 +4570,15 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
|
|
|
4233
4570
|
state.bumpTimer();
|
|
4234
4571
|
return state;
|
|
4235
4572
|
};
|
|
4236
|
-
const runOnSharedTx = async (state, body) => {
|
|
4573
|
+
const runOnSharedTx = async (state, timeoutMs, body) => {
|
|
4237
4574
|
if (state.aborted) {
|
|
4238
4575
|
await state.settled.catch(() => {});
|
|
4239
|
-
throw abortedBatchError();
|
|
4576
|
+
throw abortedBatchError(state, timeoutMs);
|
|
4240
4577
|
}
|
|
4241
4578
|
state.suspendTimer();
|
|
4242
4579
|
const tx = await state.ready;
|
|
4243
4580
|
try {
|
|
4244
|
-
if (state.aborted) throw abortedBatchError();
|
|
4581
|
+
if (state.aborted) throw abortedBatchError(state, timeoutMs);
|
|
4245
4582
|
const result = await body(tx);
|
|
4246
4583
|
state.completed += 1;
|
|
4247
4584
|
if (state.completed >= state.expected) {
|
|
@@ -4274,14 +4611,24 @@ const runMutation = async (db, context, info, txCtx, body, forceTx) => {
|
|
|
4274
4611
|
if (contextIsObject && context[drizzleExecutorKey]) return body(context[drizzleExecutorKey]);
|
|
4275
4612
|
if (!txCtx || info.operation.operation !== "mutation") return forceTx ? db.transaction((tx) => body(tx)) : body(db);
|
|
4276
4613
|
const keys = /* @__PURE__ */ new Set();
|
|
4277
|
-
|
|
4614
|
+
const mutationFields = info.schema.getMutationType()?.getFields() ?? {};
|
|
4615
|
+
state = !collectRootMutationKeys(info.operation.selectionSet, info.fragments, info.variableValues, txCtx.fieldNames, keys, /* @__PURE__ */ new Set(), (node) => {
|
|
4616
|
+
const fieldDef = mutationFields[node.name.value];
|
|
4617
|
+
if (!fieldDef) return false;
|
|
4618
|
+
try {
|
|
4619
|
+
getArgumentValues(fieldDef, node, info.variableValues);
|
|
4620
|
+
return true;
|
|
4621
|
+
} catch {
|
|
4622
|
+
return false;
|
|
4623
|
+
}
|
|
4624
|
+
}) || keys.size <= 1 ? { kind: "passthrough" } : openSharedTx(db, context, txCtx, stateKey, keys.size);
|
|
4278
4625
|
txCtx.states.set(stateKey, state);
|
|
4279
4626
|
}
|
|
4280
4627
|
if (state.kind === "passthrough") {
|
|
4281
4628
|
const executor = resolveExecutor(db, context);
|
|
4282
4629
|
return forceTx && executor === db ? db.transaction((tx) => body(tx)) : body(executor);
|
|
4283
4630
|
}
|
|
4284
|
-
return runOnSharedTx(state, body);
|
|
4631
|
+
return runOnSharedTx(state, txCtx.timeoutMs, body);
|
|
4285
4632
|
};
|
|
4286
4633
|
//#endregion
|
|
4287
4634
|
//#region src/util/builders/common/write-hooks.ts
|
|
@@ -4435,10 +4782,11 @@ const hardDeleteArg = {
|
|
|
4435
4782
|
description: "Remove the matched rows instead of marking them deleted. Reads at `INCLUDE`, so it reaches rows that are already marked; a `scope` still confines what it can reach."
|
|
4436
4783
|
};
|
|
4437
4784
|
const prepareMutationRelationColumns = (params) => {
|
|
4438
|
-
const { relationMap, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
|
|
4785
|
+
const { relationMap, filterCtx, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
|
|
4439
4786
|
const typeName = resolveObjectTypeName(tableName, typeNameMapper, resolveName);
|
|
4440
4787
|
const withParams = relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, {
|
|
4441
4788
|
typeNameMapper,
|
|
4789
|
+
filterCtx,
|
|
4442
4790
|
limits: params.limits,
|
|
4443
4791
|
scope: params.scope,
|
|
4444
4792
|
defaultOrderBy: params.defaultOrderBy,
|
|
@@ -4448,7 +4796,8 @@ const prepareMutationRelationColumns = (params) => {
|
|
|
4448
4796
|
const baseColumns = extractSelectedColumnsFromTreeSQLFormat(parsedInfo.fieldsByTypeName[typeName], table, {
|
|
4449
4797
|
tableName,
|
|
4450
4798
|
relationMap,
|
|
4451
|
-
tables
|
|
4799
|
+
tables,
|
|
4800
|
+
allRelations: filterCtx?.relationMap
|
|
4452
4801
|
});
|
|
4453
4802
|
return {
|
|
4454
4803
|
columns: hasRelations ? withPrimaryKeyColumns(baseColumns, table, pkNames) : baseColumns,
|
|
@@ -4584,7 +4933,7 @@ const assertSingleMatch = async (executor, table, filters) => {
|
|
|
4584
4933
|
* MySQL (no `RETURNING`; its hooks see `rows: []` as on every MySQL mutation), skips the
|
|
4585
4934
|
* `RETURNING` and reads the count off the driver's result via {@link rowsAffected}.
|
|
4586
4935
|
*/
|
|
4587
|
-
const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested, policies, pkNames = [], returnsRows }) => {
|
|
4936
|
+
const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested, policies, pkNames = [], returnsRows, writeFilter }) => {
|
|
4588
4937
|
const softDelete = kind === "delete" ? policies?.softDelete?.(tableName) : void 0;
|
|
4589
4938
|
const canHardDelete = softDelete?.hardDelete === true;
|
|
4590
4939
|
const queryArgs = {
|
|
@@ -4627,7 +4976,7 @@ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, f
|
|
|
4627
4976
|
required: requireWhere,
|
|
4628
4977
|
deleted: hard ? "INCLUDE" : void 0
|
|
4629
4978
|
});
|
|
4630
|
-
if (filters) query = query.where(filters);
|
|
4979
|
+
if (filters) query = query.where(writeFilter ? writeFilter(filters) : filters);
|
|
4631
4980
|
if (!readsRows) {
|
|
4632
4981
|
const result = await query;
|
|
4633
4982
|
await after();
|
|
@@ -4687,27 +5036,40 @@ const getOrCreateLoader = (context, key, batchFn) => {
|
|
|
4687
5036
|
if (!loaders.has(key)) loaders.set(key, new BatchLoader(batchFn));
|
|
4688
5037
|
return loaders.get(key);
|
|
4689
5038
|
};
|
|
5039
|
+
const requestCacheNode = () => ({
|
|
5040
|
+
children: /* @__PURE__ */ new WeakMap(),
|
|
5041
|
+
entries: /* @__PURE__ */ new Map()
|
|
5042
|
+
});
|
|
4690
5043
|
/**
|
|
4691
5044
|
* Memoizes a value that a resolver derives from its own field — its args and its selection —
|
|
4692
5045
|
* for the length of one request. Resolvers run once per parent row, so anything derived only
|
|
4693
5046
|
* from the field is otherwise recomputed for every row of a batch.
|
|
4694
5047
|
*
|
|
4695
|
-
* The
|
|
4696
|
-
*
|
|
4697
|
-
*
|
|
4698
|
-
*
|
|
5048
|
+
* The value is keyed by every AST node of the field — `info.fieldNodes`, the nodes graphql-js
|
|
5049
|
+
* merged into this one response field — not only the first. A fragment's field node is the first
|
|
5050
|
+
* node wherever the fragment is spread, but the merged selection differs when a sibling selection
|
|
5051
|
+
* at one spread site adds to it, so keying on one node would serve one site the other's selection.
|
|
5052
|
+
* The nodes are matched by identity, in order, through a chain of weak maps, so a document cached
|
|
5053
|
+
* across requests never carries values between them (the root lives on the context), and an array
|
|
5054
|
+
* graphql rebuilds per call still hits. `key` separates fields that share nodes: one fragment
|
|
5055
|
+
* selection can resolve for more than one parent type. Without a context object, or without
|
|
5056
|
+
* nodes, there is nowhere request-scoped to cache, so the value is simply computed.
|
|
4699
5057
|
*
|
|
4700
5058
|
* `compute` must not read the parent row.
|
|
4701
5059
|
*/
|
|
4702
|
-
const getOrCreateRequestValue = (context,
|
|
4703
|
-
if (!context || typeof context !== "object" || !node || typeof node !== "object") return compute();
|
|
4704
|
-
if (!context[DRIZZLE_REQUEST_CACHE_KEY]) context[DRIZZLE_REQUEST_CACHE_KEY] =
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
5060
|
+
const getOrCreateRequestValue = (context, nodes, key, compute) => {
|
|
5061
|
+
if (!context || typeof context !== "object" || !Array.isArray(nodes) || !nodes.length || nodes.some((node) => !node || typeof node !== "object")) return compute();
|
|
5062
|
+
if (!context[DRIZZLE_REQUEST_CACHE_KEY]) context[DRIZZLE_REQUEST_CACHE_KEY] = requestCacheNode();
|
|
5063
|
+
let level = context[DRIZZLE_REQUEST_CACHE_KEY];
|
|
5064
|
+
for (const node of nodes) {
|
|
5065
|
+
let next = level.children.get(node);
|
|
5066
|
+
if (!next) {
|
|
5067
|
+
next = requestCacheNode();
|
|
5068
|
+
level.children.set(node, next);
|
|
5069
|
+
}
|
|
5070
|
+
level = next;
|
|
4710
5071
|
}
|
|
5072
|
+
const entries = level.entries;
|
|
4711
5073
|
if (entries.has(key)) return entries.get(key);
|
|
4712
5074
|
const value = compute();
|
|
4713
5075
|
entries.set(key, value);
|
|
@@ -4937,11 +5299,12 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4937
5299
|
if (!parentTable || !targetTable) return;
|
|
4938
5300
|
const joinPairs = extractRelationJoinColumnPairs(relEntry, parentTable, targetTable);
|
|
4939
5301
|
if (!joinPairs) return;
|
|
4940
|
-
const
|
|
5302
|
+
const localCols = joinPairs.map((pair) => pair.localCol);
|
|
4941
5303
|
const foreignCol = joinPairs[0].foreignCol;
|
|
4942
5304
|
const junction = extractJunctionColumns(relEntry);
|
|
4943
5305
|
const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
|
|
4944
5306
|
const groupKeys = junction ? [PARENT_KEY] : joinPairs.map((pair) => pair.foreignColPropName);
|
|
5307
|
+
const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
|
|
4945
5308
|
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
|
|
4946
5309
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, isOne && relEntry.relation?.optional === false);
|
|
4947
5310
|
const limitPolicy = isOne ? void 0 : limits?.(targetTableName);
|
|
@@ -4983,10 +5346,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4983
5346
|
};
|
|
4984
5347
|
const resolve = async (parent, args, context, info) => {
|
|
4985
5348
|
if (parent[relationName] !== void 0) return parent[relationName];
|
|
4986
|
-
const localValues =
|
|
5349
|
+
const localValues = parentJoinValues(parent, joinPairs);
|
|
4987
5350
|
if (localValues.some((value) => value == null)) return isOne ? null : [];
|
|
4988
5351
|
const { where: whereArg, limit: requestedLimit, offset, after, deleted } = args ?? {};
|
|
4989
|
-
const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes
|
|
5352
|
+
const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes, `relation:${tableName}::${relationName}`, () => {
|
|
4990
5353
|
const orderByArg = isOne ? args?.orderBy : withDefaultOrderBy(args ?? {}, targetTableName, policies?.defaultOrderBy).orderBy;
|
|
4991
5354
|
const limit = applyLimitPolicy(requestedLimit, limitPolicy, errorCtx);
|
|
4992
5355
|
assertPagingNotNegative({ offset }, errorCtx);
|
|
@@ -5027,55 +5390,60 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
5027
5390
|
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" });
|
|
5028
5391
|
return getOrCreateLoader(context, loaderKey, async (parentKeys) => {
|
|
5029
5392
|
const executor = resolveExecutor(db, context);
|
|
5030
|
-
const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key), key])).values()];
|
|
5393
|
+
const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key, localCols), key])).values()];
|
|
5031
5394
|
const scope = resolveScope(policies, context, filterCtx);
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
const
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5395
|
+
const loadChunk = async (chunkKeys) => {
|
|
5396
|
+
let whereCondition = withScope(scope, targetTableName, targetTable, and(parentKeyCondition(groupCols, chunkKeys), ownCondition, junction ? junctionPolicy(scope, junctionName, junction.table) : void 0, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
|
|
5397
|
+
if (distinct) {
|
|
5398
|
+
const keys = await selectDistinctKeys({
|
|
5399
|
+
db: executor,
|
|
5400
|
+
table: targetTable,
|
|
5401
|
+
tableName: targetTableName,
|
|
5402
|
+
distinct,
|
|
5403
|
+
pkNames: targetPkNames,
|
|
5404
|
+
where: whereCondition,
|
|
5405
|
+
orderBy: orderByArg,
|
|
5406
|
+
partitionBy: groupCols,
|
|
5407
|
+
junction: junction ? {
|
|
5408
|
+
table: junction.table,
|
|
5409
|
+
on: eq(junction.targetCol, foreignCol),
|
|
5410
|
+
parentKey: junction.sourceCol
|
|
5411
|
+
} : void 0
|
|
5412
|
+
});
|
|
5413
|
+
if (!keys.length) return /* @__PURE__ */ new Map();
|
|
5414
|
+
whereCondition = and(whereCondition, junction ? balancedOr(keys.map((key) => and(eq(junction.sourceCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
|
|
5415
|
+
}
|
|
5416
|
+
const selection = junction ? {
|
|
5417
|
+
...columns ?? getColumns(targetTable),
|
|
5418
|
+
[PARENT_KEY]: junction.sourceCol
|
|
5419
|
+
} : columns;
|
|
5420
|
+
let rows;
|
|
5421
|
+
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);
|
|
5422
|
+
else {
|
|
5423
|
+
let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
|
|
5424
|
+
const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName, scope), whereArg) : [];
|
|
5425
|
+
if (orderExprs.length) q = q.orderBy(...orderExprs);
|
|
5426
|
+
rows = await q;
|
|
5427
|
+
}
|
|
5428
|
+
if (cursorEntries) attachRowCursors(rows, cursorEntries);
|
|
5429
|
+
const keyOf = (row) => {
|
|
5430
|
+
const key = joinKeyOf(groupKeys.map((name) => row[name]), groupCols);
|
|
5431
|
+
if (junction) delete row[PARENT_KEY];
|
|
5432
|
+
return key;
|
|
5433
|
+
};
|
|
5434
|
+
if (isOne) {
|
|
5435
|
+
const byKey = new Map(rows.map((row) => [keyOf(row), row]));
|
|
5436
|
+
remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
|
|
5437
|
+
return byKey;
|
|
5438
|
+
}
|
|
5439
|
+
const grouped = new Map(chunkKeys.map((key) => [joinKeyOf(key, localCols), []]));
|
|
5440
|
+
for (const row of rows) grouped.get(keyOf(row))?.push(row);
|
|
5072
5441
|
remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
|
|
5073
|
-
return
|
|
5074
|
-
}
|
|
5075
|
-
const
|
|
5076
|
-
for (const
|
|
5077
|
-
|
|
5078
|
-
return parentKeys.map((key) => grouped.get(joinKeyOf(key)) ?? []);
|
|
5442
|
+
return grouped;
|
|
5443
|
+
};
|
|
5444
|
+
const results = /* @__PURE__ */ new Map();
|
|
5445
|
+
for (const chunk of parentKeyChunks(uniqueKeys, groupCols.length)) for (const [key, value] of await loadChunk(chunk)) results.set(key, value);
|
|
5446
|
+
return parentKeys.map((key) => results.get(joinKeyOf(key, localCols)) ?? (isOne ? null : []));
|
|
5079
5447
|
}).load(localValues);
|
|
5080
5448
|
};
|
|
5081
5449
|
return async (parent, args, context, info) => {
|
|
@@ -5180,7 +5548,7 @@ const generateTableOrderCached = (table, innerOrder) => {
|
|
|
5180
5548
|
try {
|
|
5181
5549
|
const columns = visibleColumns(table);
|
|
5182
5550
|
const columnEntries = Object.entries(columns);
|
|
5183
|
-
remapped = Object.fromEntries(columnEntries.
|
|
5551
|
+
remapped = Object.fromEntries(columnEntries.filter(([, column]) => isOrderableColumn(column)).map(([columnName]) => [columnName, { type: innerOrder }]));
|
|
5184
5552
|
if (cacheable) {
|
|
5185
5553
|
let byInnerOrder = orderMap.get(table);
|
|
5186
5554
|
if (!byInnerOrder) {
|
|
@@ -5223,43 +5591,71 @@ const generateTableSelectTypeFieldsCached = (table, tableName, cacheCtx) => {
|
|
|
5223
5591
|
return remapped;
|
|
5224
5592
|
};
|
|
5225
5593
|
/**
|
|
5594
|
+
* The to-one relations a table's OrderBy can order through, as `[relationName, targetTableName]`.
|
|
5595
|
+
* To-many relations are left out: "order a parent by its many children" has no single defined
|
|
5596
|
+
* value to sort on. A relation whose name collides with a column is skipped — the column keeps
|
|
5597
|
+
* the name, even one with no ordering of its own, so `extractOrderBy` never reads it as a relation.
|
|
5598
|
+
*/
|
|
5599
|
+
const orderRelations = (tableName, relationMap, tables) => {
|
|
5600
|
+
const relations = relationMap?.[tableName];
|
|
5601
|
+
const table = tables?.[tableName];
|
|
5602
|
+
if (!relations || !tables || !table) return [];
|
|
5603
|
+
const columns = visibleColumns(table);
|
|
5604
|
+
return Object.entries(relations).filter(([relationName, relEntry]) => {
|
|
5605
|
+
const relation = relEntry.relation ?? relEntry;
|
|
5606
|
+
return !(relationName in columns) && !!tables[relEntry.targetTableName] && is(relation, One) && isFilterableRelation(relation);
|
|
5607
|
+
}).map(([relationName, relEntry]) => [relationName, relEntry.targetTableName]);
|
|
5608
|
+
};
|
|
5609
|
+
/**
|
|
5610
|
+
* Whether a table's OrderBy would have any field: an orderable column of its own, or a to-one
|
|
5611
|
+
* relation to a table that has one. A table of nothing but JSON columns has none, and an input
|
|
5612
|
+
* object with no fields is an invalid schema, so such a table gets no OrderBy and no `orderBy`
|
|
5613
|
+
* argument at all.
|
|
5614
|
+
*/
|
|
5615
|
+
const hasOrderFields = (tableName, relationMap, tables, visiting = /* @__PURE__ */ new Set()) => {
|
|
5616
|
+
const table = tables[tableName];
|
|
5617
|
+
if (table && Object.values(visibleColumns(table)).some((column) => isOrderableColumn(column))) return true;
|
|
5618
|
+
if (visiting.has(tableName)) return false;
|
|
5619
|
+
visiting.add(tableName);
|
|
5620
|
+
return orderRelations(tableName, relationMap, tables).some(([, target]) => hasOrderFields(target, relationMap, tables, visiting));
|
|
5621
|
+
};
|
|
5622
|
+
/**
|
|
5226
5623
|
* Order fields for a table's to-one relations: each takes the target table's own OrderBy
|
|
5227
5624
|
* input, so a list can be sorted by a related row's column (compiled as a correlated
|
|
5228
|
-
* subquery in the ORDER BY — see `extractOrderBy`).
|
|
5229
|
-
*
|
|
5230
|
-
* name collides with a column is skipped — the column keeps the field.
|
|
5625
|
+
* subquery in the ORDER BY — see `extractOrderBy`). A target with no OrderBy of its own is
|
|
5626
|
+
* left out.
|
|
5231
5627
|
*/
|
|
5232
|
-
const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper,
|
|
5233
|
-
const relations = relationMap?.[tableName];
|
|
5234
|
-
if (!relations || !tables) return {};
|
|
5628
|
+
const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper, relationMap, tables) => {
|
|
5235
5629
|
const fields = {};
|
|
5236
|
-
for (const [relationName,
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
if (!targetTable || !is(relation, One) || !isFilterableRelation(relation)) continue;
|
|
5241
|
-
fields[relationName] = {
|
|
5242
|
-
type: generateTableOrderTypeCached(targetTable, relEntry.targetTableName, typeNameMapper, cacheCtx, relationMap, tables),
|
|
5630
|
+
for (const [relationName, targetTableName] of orderRelations(tableName, relationMap, tables)) {
|
|
5631
|
+
const type = generateTableOrderTypeCached(tables[targetTableName], targetTableName, typeNameMapper, cacheCtx, relationMap, tables);
|
|
5632
|
+
if (type) fields[relationName] = {
|
|
5633
|
+
type,
|
|
5243
5634
|
description: `Order by columns of the related ${relationName} row`
|
|
5244
5635
|
};
|
|
5245
5636
|
}
|
|
5246
5637
|
return fields;
|
|
5247
5638
|
};
|
|
5639
|
+
/**
|
|
5640
|
+
* The table's `<Type>OrderBy` input, or `undefined` when it would have no fields (see
|
|
5641
|
+
* {@link hasOrderFields}); the caller then leaves the `orderBy` argument out.
|
|
5642
|
+
*/
|
|
5248
5643
|
const generateTableOrderTypeCached = (table, tableName, typeNameMapper, cacheCtx, relationMap, tables) => {
|
|
5249
5644
|
if (cacheCtx.orderTypeCache.has(table)) return cacheCtx.orderTypeCache.get(table);
|
|
5645
|
+
if (!hasOrderFields(tableName, relationMap, {
|
|
5646
|
+
...tables,
|
|
5647
|
+
[tableName]: table
|
|
5648
|
+
})) return;
|
|
5250
5649
|
const order = new GraphQLInputObjectType({
|
|
5251
5650
|
name: cacheCtx.typeName({
|
|
5252
5651
|
kind: "orderBy",
|
|
5253
5652
|
defaultName: `${resolveTypeName(tableName, typeNameMapper)}OrderBy`,
|
|
5254
5653
|
table: tableName
|
|
5255
5654
|
}),
|
|
5256
|
-
fields: () => {
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, orderColumns, relationMap, tables)
|
|
5261
|
-
};
|
|
5262
|
-
}
|
|
5655
|
+
fields: () => ({
|
|
5656
|
+
...generateTableOrderCached(table, innerOrderType(cacheCtx.typeName)),
|
|
5657
|
+
...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, relationMap, tables)
|
|
5658
|
+
})
|
|
5263
5659
|
});
|
|
5264
5660
|
cacheCtx.orderTypeCache.set(table, order);
|
|
5265
5661
|
return order;
|
|
@@ -5478,7 +5874,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
|
|
|
5478
5874
|
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(relType))),
|
|
5479
5875
|
args: {
|
|
5480
5876
|
where: { type: relSelectData.filters },
|
|
5481
|
-
orderBy: { type: relSelectData.order },
|
|
5877
|
+
...relSelectData.order ? { orderBy: { type: relSelectData.order } } : {},
|
|
5482
5878
|
offset: { type: GraphQLInt },
|
|
5483
5879
|
limit: { type: GraphQLInt },
|
|
5484
5880
|
...deletedArg(cacheCtx.softDeleteOf, targetTableName, cacheCtx.typeName),
|
|
@@ -5706,6 +6102,7 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5706
6102
|
};
|
|
5707
6103
|
const transactionsOpt = config?.transactions;
|
|
5708
6104
|
const transactions = transactionsOpt === void 0 || transactionsOpt === "none" ? void 0 : { timeoutMs: (transactionsOpt === "auto" ? void 0 : transactionsOpt.timeoutMs) ?? 3e4 };
|
|
6105
|
+
if (transactions && transactions.timeoutMs !== Number.POSITIVE_INFINITY && !(Number.isInteger(transactions.timeoutMs) && transactions.timeoutMs >= 1 && transactions.timeoutMs <= MAX_TIMER_MS)) throw new Error(`Drizzle-GraphQL Error: config.transactions.timeoutMs must be a positive integer no greater than ${MAX_TIMER_MS}, or Infinity for no timeout; got ${transactions.timeoutMs}.`);
|
|
5709
6106
|
const limitsConfig = config?.limits;
|
|
5710
6107
|
if (limitsConfig) {
|
|
5711
6108
|
const check = (policy, where) => {
|
|
@@ -5738,6 +6135,7 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5738
6135
|
for (const [columnName, entry] of Object.entries(orderBy)) {
|
|
5739
6136
|
if (!columns[columnName]) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which is not a column of '${tableName}'.`);
|
|
5740
6137
|
if (config?.exclude?.columns?.[tableName]?.includes(columnName)) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which config.exclude.columns hides. A default ordering is encoded into every row's cursor, so it can only use columns the schema exposes.`);
|
|
6138
|
+
if (!isOrderableColumn(columns[columnName])) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which has no meaningful ordering (a JSON column, or a SQLite blob bigint), so the generated orderBy does not offer it either.`);
|
|
5741
6139
|
const direction = typeof entry === "string" ? entry : entry?.direction;
|
|
5742
6140
|
if (direction !== "asc" && direction !== "desc") throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy.${columnName} must be 'asc', 'desc', or { direction, priority? }.`);
|
|
5743
6141
|
const priority = typeof entry === "string" ? void 0 : entry?.priority;
|
|
@@ -5796,7 +6194,7 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
5796
6194
|
}
|
|
5797
6195
|
}
|
|
5798
6196
|
}
|
|
5799
|
-
const onWrite = writeHooks.
|
|
6197
|
+
const onWrite = [...writeHooks.values()].some(Boolean) ? (tableName) => writeHooks.get(tableName) : void 0;
|
|
5800
6198
|
const policies = scopeConfig || contextValuesConfig || softDeleteInfos.size || onWrite || defaultOrderBy ? {
|
|
5801
6199
|
scope: scopeConfig ? (tableName) => scopeConfig[tableName] : void 0,
|
|
5802
6200
|
contextValues: contextValuesConfig ? (tableName) => contextValuesConfig[tableName] : void 0,
|
|
@@ -5887,8 +6285,14 @@ const AGGREGATE_OPS = [
|
|
|
5887
6285
|
];
|
|
5888
6286
|
/** Ops whose result is a row count: never null, and returned as `Int!` rather than the column's type. */
|
|
5889
6287
|
const COUNT_OPS = /* @__PURE__ */ new Set(["countNonNull", "countDistinct"]);
|
|
6288
|
+
/**
|
|
6289
|
+
* `avg`, except on MySQL, where AVG over an integer column is a DECIMAL rounded to
|
|
6290
|
+
* `div_precision_increment` (4 by default) digits. Averaging the value as a DOUBLE keeps the
|
|
6291
|
+
* fraction the `Float` output promises, as the other two dialects do.
|
|
6292
|
+
*/
|
|
6293
|
+
const avgOf = (col) => columnDialect(col) === "mysql" ? sql`avg(cast(${col} as double))`.mapWith(String) : avg(col);
|
|
5890
6294
|
const OP_FNS = {
|
|
5891
|
-
avg,
|
|
6295
|
+
avg: avgOf,
|
|
5892
6296
|
sum,
|
|
5893
6297
|
min,
|
|
5894
6298
|
max,
|
|
@@ -5898,6 +6302,18 @@ const OP_FNS = {
|
|
|
5898
6302
|
/** Separator for flat select aliases (`avg__price`) — reassembled into nested output by the resolver. */
|
|
5899
6303
|
const SEP = "__";
|
|
5900
6304
|
/**
|
|
6305
|
+
* Column types PostgreSQL can test for equality but has no `min`/`max` aggregate for, plus the
|
|
6306
|
+
* SQLite bigint blob, whose bytes compare as text (`max(9, 10)` is 9). They still count
|
|
6307
|
+
* distinct values and group.
|
|
6308
|
+
*/
|
|
6309
|
+
const NO_MIN_MAX_COLUMN_TYPES = /* @__PURE__ */ new Set([
|
|
6310
|
+
"PgUUID",
|
|
6311
|
+
"PgMacaddr",
|
|
6312
|
+
"PgMacaddr8",
|
|
6313
|
+
"PgBinaryVector",
|
|
6314
|
+
"SQLiteBigInt"
|
|
6315
|
+
]);
|
|
6316
|
+
/**
|
|
5901
6317
|
* Classifies a table's columns for aggregation. avg/sum only make sense on numeric scalars;
|
|
5902
6318
|
* min/max work on any orderable scalar (numbers, strings, bigints, dates, enums). Booleans,
|
|
5903
6319
|
* arrays (including array-typed number columns), JSON, buffers, and object-shaped columns
|
|
@@ -5929,6 +6345,7 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
5929
6345
|
if (cached) return cached;
|
|
5930
6346
|
const numeric = {};
|
|
5931
6347
|
const orderable = {};
|
|
6348
|
+
const comparable = {};
|
|
5932
6349
|
const all = {};
|
|
5933
6350
|
for (const [columnName, column] of Object.entries(visibleColumns(table))) {
|
|
5934
6351
|
all[columnName] = column;
|
|
@@ -5938,7 +6355,11 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
5938
6355
|
const { type: dataType, constraint } = extractExtendedColumnType(column);
|
|
5939
6356
|
if (dataType === "boolean" || dataType === "array" || dataType === "custom") continue;
|
|
5940
6357
|
if (dataType === "object" && constraint !== "date") continue;
|
|
5941
|
-
|
|
6358
|
+
comparable[columnName] = {
|
|
6359
|
+
column,
|
|
6360
|
+
converted
|
|
6361
|
+
};
|
|
6362
|
+
if (!NO_MIN_MAX_COLUMN_TYPES.has(column.columnType)) orderable[columnName] = {
|
|
5942
6363
|
column,
|
|
5943
6364
|
converted
|
|
5944
6365
|
};
|
|
@@ -5947,6 +6368,7 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
5947
6368
|
const sets = {
|
|
5948
6369
|
numeric,
|
|
5949
6370
|
orderable,
|
|
6371
|
+
comparable,
|
|
5950
6372
|
all
|
|
5951
6373
|
};
|
|
5952
6374
|
byName.set(tableName, sets);
|
|
@@ -5959,13 +6381,13 @@ const classifyAggregateColumns = (table, tableName) => {
|
|
|
5959
6381
|
* and avg/sum of integers overflow Int / produce decimals)
|
|
5960
6382
|
* - `min` / `max` — per orderable column, the column's own (nullable) scalar type
|
|
5961
6383
|
* - `countNonNull` — per column, `Int!`: how many matching rows have a non-null value there
|
|
5962
|
-
* - `countDistinct` — per
|
|
6384
|
+
* - `countDistinct` — per comparable column, `Int!`: how many distinct non-null values there are
|
|
5963
6385
|
* Each wrapper is omitted when no column qualifies for it.
|
|
5964
6386
|
*/
|
|
5965
6387
|
const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
|
|
5966
6388
|
const cached = cacheCtx?.aggregateTypeCache.get(tableName);
|
|
5967
6389
|
if (cached) return cached;
|
|
5968
|
-
const { numeric, orderable, all } = classifyAggregateColumns(table, tableName);
|
|
6390
|
+
const { numeric, orderable, comparable, all } = classifyAggregateColumns(table, tableName);
|
|
5969
6391
|
const nameOf = (op) => cacheCtx?.typeName({
|
|
5970
6392
|
kind: "aggregate",
|
|
5971
6393
|
defaultName: `${typeName}${capitalize(op)}Aggregate`,
|
|
@@ -5986,7 +6408,7 @@ const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
|
|
|
5986
6408
|
}) };
|
|
5987
6409
|
const countSets = {
|
|
5988
6410
|
countNonNull: all,
|
|
5989
|
-
countDistinct:
|
|
6411
|
+
countDistinct: comparable
|
|
5990
6412
|
};
|
|
5991
6413
|
for (const [op, columns] of Object.entries(countSets)) {
|
|
5992
6414
|
const columnNames = Object.keys(columns);
|
|
@@ -6161,11 +6583,12 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
6161
6583
|
}
|
|
6162
6584
|
return;
|
|
6163
6585
|
}
|
|
6164
|
-
const
|
|
6586
|
+
const localCols = joinPairs.map((pair) => pair.localCol);
|
|
6165
6587
|
const foreignCol = joinPairs[0].foreignCol;
|
|
6166
6588
|
const junction = extractJunctionColumns(relEntry);
|
|
6167
6589
|
if (junction === null) return;
|
|
6168
6590
|
const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
|
|
6591
|
+
const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
|
|
6169
6592
|
const groupAliases = groupCols.map((_, i) => `${GROUP_KEY}${i}`);
|
|
6170
6593
|
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
|
|
6171
6594
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
|
|
@@ -6179,7 +6602,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
6179
6602
|
};
|
|
6180
6603
|
const resolve = async (parent, args, context, info) => {
|
|
6181
6604
|
try {
|
|
6182
|
-
const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes
|
|
6605
|
+
const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes, `aggregate:${tableName}::${relationName}`, () => {
|
|
6183
6606
|
const request = parseAggregateRequest(info, target);
|
|
6184
6607
|
return {
|
|
6185
6608
|
request,
|
|
@@ -6191,23 +6614,26 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
6191
6614
|
};
|
|
6192
6615
|
});
|
|
6193
6616
|
if (!Object.keys(request.selection).length) return {};
|
|
6194
|
-
const localValues =
|
|
6617
|
+
const localValues = parentJoinValues(parent, joinPairs);
|
|
6195
6618
|
if (localValues.some((value) => value == null)) return assembleAggregateRow({}, request, target);
|
|
6196
6619
|
const whereArg = args?.where;
|
|
6197
6620
|
const deleted = args?.deleted;
|
|
6198
6621
|
const loader = getOrCreateLoader(context, loaderKey, async (parentKeys) => {
|
|
6199
6622
|
const executor = resolveExecutor(db, context);
|
|
6200
|
-
const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key), key])).values()];
|
|
6623
|
+
const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key, localCols), key])).values()];
|
|
6201
6624
|
const scope = resolveScope(policies, context, filterCtx);
|
|
6202
|
-
const whereCondition = withScope(scope, targetTableName, targetTable, and(parentKeyCondition(groupCols, uniqueKeys), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
|
|
6203
6625
|
const groupSelection = Object.fromEntries(groupAliases.map((alias, i) => [alias, groupCols[i]]));
|
|
6204
|
-
const
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6626
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
6627
|
+
for (const chunk of parentKeyChunks(uniqueKeys, groupCols.length)) {
|
|
6628
|
+
const whereCondition = withScope(scope, targetTableName, targetTable, and(parentKeyCondition(groupCols, chunk), ownCondition, junction ? junctionPolicy(scope, junctionName, junction.table) : void 0, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
|
|
6629
|
+
const selected = executor.select({
|
|
6630
|
+
...groupSelection,
|
|
6631
|
+
...request.selection
|
|
6632
|
+
}).from(targetTable);
|
|
6633
|
+
const rows = await (junction ? selected.innerJoin(junction.table, eq(junction.targetCol, foreignCol)) : selected).where(whereCondition).groupBy(...groupCols);
|
|
6634
|
+
for (const row of rows) byKey.set(joinKeyOf(groupAliases.map((alias) => row[alias]), groupCols), row);
|
|
6635
|
+
}
|
|
6636
|
+
return parentKeys.map((key) => byKey.get(joinKeyOf(key, localCols)) ?? {});
|
|
6211
6637
|
});
|
|
6212
6638
|
return assembleAggregateRow(await loader.load(localValues), request, target);
|
|
6213
6639
|
} catch (e) {
|
|
@@ -6233,12 +6659,12 @@ const HAVING_OPS = {
|
|
|
6233
6659
|
};
|
|
6234
6660
|
/**
|
|
6235
6661
|
* Columns a query can group on. Grouping needs an equality operator, which is the same
|
|
6236
|
-
* requirement
|
|
6662
|
+
* requirement countDistinct has, plus booleans — comparable excludes them because a min/max over
|
|
6237
6663
|
* true/false says nothing, but `GROUP BY isConfirmed` is perfectly ordinary.
|
|
6238
6664
|
*/
|
|
6239
6665
|
const groupableColumns = (table, tableName) => {
|
|
6240
|
-
const {
|
|
6241
|
-
const groupable = { ...
|
|
6666
|
+
const { comparable } = classifyAggregateColumns(table, tableName);
|
|
6667
|
+
const groupable = { ...comparable };
|
|
6242
6668
|
for (const [columnName, column] of Object.entries(visibleColumns(table))) {
|
|
6243
6669
|
if (groupable[columnName]) continue;
|
|
6244
6670
|
const { type: dataType } = extractExtendedColumnType(column);
|
|
@@ -6279,7 +6705,7 @@ const aggregateNumberFilterType = (typeName) => sharedType(typeName, {
|
|
|
6279
6705
|
* nothing to compare against.
|
|
6280
6706
|
*/
|
|
6281
6707
|
const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
|
|
6282
|
-
const { numeric,
|
|
6708
|
+
const { numeric, comparable, all } = classifyAggregateColumns(table, tableName);
|
|
6283
6709
|
const resolveName = cacheCtx?.typeName ?? ((info) => info.defaultName);
|
|
6284
6710
|
const numberFilter = aggregateNumberFilterType(resolveName);
|
|
6285
6711
|
const fields = { count: {
|
|
@@ -6292,7 +6718,7 @@ const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
|
|
|
6292
6718
|
min: numeric,
|
|
6293
6719
|
max: numeric,
|
|
6294
6720
|
countNonNull: all,
|
|
6295
|
-
countDistinct:
|
|
6721
|
+
countDistinct: comparable
|
|
6296
6722
|
};
|
|
6297
6723
|
for (const [op, columns] of Object.entries(opColumns)) {
|
|
6298
6724
|
const columnNames = Object.keys(columns);
|
|
@@ -6357,22 +6783,34 @@ const generateGroupByType = (table, tableName, typeName, cacheCtx) => {
|
|
|
6357
6783
|
}
|
|
6358
6784
|
});
|
|
6359
6785
|
};
|
|
6360
|
-
/**
|
|
6361
|
-
|
|
6786
|
+
/**
|
|
6787
|
+
* Turns one `${typeName}Having` entry into SQL conditions over the matching aggregate expression.
|
|
6788
|
+
*
|
|
6789
|
+
* The operand is a `Float`, but PostgreSQL types an untyped parameter after what it is compared
|
|
6790
|
+
* with: count and sum of integers are bigint, min/max of an int column int, so `gt: 1.5` would
|
|
6791
|
+
* fail to parse. There the aggregate is compared as `numeric`, which holds every one of them
|
|
6792
|
+
* exactly. MySQL and SQLite already compare numbers across types.
|
|
6793
|
+
*/
|
|
6794
|
+
const havingComparisons = (expression, filter, isPg) => {
|
|
6795
|
+
const compared = isPg ? sql`cast(${expression} as numeric)` : expression;
|
|
6796
|
+
return Object.entries(filter).filter(([op, value]) => value != null && op in HAVING_OPS).map(([op, value]) => HAVING_OPS[op](compared, value));
|
|
6797
|
+
};
|
|
6362
6798
|
const buildHavingCondition = (having, columns) => {
|
|
6363
6799
|
if (!having) return;
|
|
6364
6800
|
const conditions = [];
|
|
6801
|
+
const firstColumn = Object.values(columns)[0];
|
|
6802
|
+
const isPg = firstColumn ? columnDialect(firstColumn) === "pg" : false;
|
|
6365
6803
|
for (const [key, value] of Object.entries(having)) {
|
|
6366
6804
|
if (value == null) continue;
|
|
6367
6805
|
if (key === "count") {
|
|
6368
|
-
conditions.push(...havingComparisons(count(), value));
|
|
6806
|
+
conditions.push(...havingComparisons(count(), value, isPg));
|
|
6369
6807
|
continue;
|
|
6370
6808
|
}
|
|
6371
6809
|
if (!AGGREGATE_OPS.includes(key)) continue;
|
|
6372
6810
|
for (const [columnName, filter] of Object.entries(value)) {
|
|
6373
6811
|
const column = columns[columnName];
|
|
6374
6812
|
if (!column || filter == null) continue;
|
|
6375
|
-
conditions.push(...havingComparisons(OP_FNS[key](column), filter));
|
|
6813
|
+
conditions.push(...havingComparisons(OP_FNS[key](column), filter, isPg));
|
|
6376
6814
|
}
|
|
6377
6815
|
}
|
|
6378
6816
|
return conditions.length ? and(...conditions) : void 0;
|
|
@@ -6524,12 +6962,32 @@ const isOptionalOnInsert = (column) => !column.notNull || !!column.hasDefault ||
|
|
|
6524
6962
|
* relation could come back without it. A `where` inherited from the reverse relation filters
|
|
6525
6963
|
* the parent instead, and becomes a per-parent check.
|
|
6526
6964
|
*/
|
|
6527
|
-
const ownWherePlan = (relEntry, parentTable, targetTable, localCol) => {
|
|
6965
|
+
const ownWherePlan = (relEntry, parentTable, targetTable, localCol, parentPrimaryKey) => {
|
|
6528
6966
|
const condition = () => relationOwnCondition(relEntry, parentTable, targetTable, [localCol], [localCol]);
|
|
6529
6967
|
if (!condition()) return {};
|
|
6530
|
-
|
|
6531
|
-
|
|
6968
|
+
const relation = relEntry.relation ?? relEntry;
|
|
6969
|
+
if (!isRelationFilterReversed(relation)) return { ownCondition: () => condition() };
|
|
6970
|
+
const parentColumns = getColumns(parentTable);
|
|
6971
|
+
const parentKeyColumns = Object.fromEntries(parentPrimaryKey.filter((name) => parentColumns[name]).map((name) => [name, parentColumns[name]]));
|
|
6972
|
+
const keyNames = Object.keys(parentKeyColumns);
|
|
6973
|
+
return {
|
|
6974
|
+
parentShows: async (executor, key) => (await executor.select({ key: localCol }).from(parentTable).where(and(eq(localCol, key), condition())).limit(1)).length > 0,
|
|
6975
|
+
parentHidden: async (executor, rows) => {
|
|
6976
|
+
if (!rows.length) return false;
|
|
6977
|
+
if (!keyNames.length || !rows.every((row) => keyNames.every((name) => name in row))) throw drizzleError(`Drizzle-GraphQL Error: cannot check the \`where\` of this relation: the written row cannot be identified by a primary key.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
6978
|
+
const keyOf = (row) => keyNames.map((name) => keyString(row[name])).join("\0");
|
|
6979
|
+
const match = or(...rows.map((row) => and(...keyNames.map((name) => eq(parentColumns[name], row[name])))));
|
|
6980
|
+
const shown = new Set((await executor.select(parentKeyColumns).from(parentTable).where(and(match, relationsFilterToSQL(parentTable, relationOwnWhere(relation))))).map(keyOf));
|
|
6981
|
+
return rows.some((row) => !shown.has(keyOf(row)));
|
|
6982
|
+
},
|
|
6983
|
+
parentKeyColumns
|
|
6984
|
+
};
|
|
6532
6985
|
};
|
|
6986
|
+
/**
|
|
6987
|
+
* A key value in a form that compares equal across the two reads of it — the driver may hand
|
|
6988
|
+
* back a `Date` or a `bigint` that is a different object each time.
|
|
6989
|
+
*/
|
|
6990
|
+
const keyString = (value) => value instanceof Date ? `d${value.getTime()}` : value === null || value === void 0 ? "" : `v${String(value)}`;
|
|
6533
6991
|
/** The property name a column is stored under on its table, matched by identity. */
|
|
6534
6992
|
const propNameOf = (table, column) => {
|
|
6535
6993
|
if (!column) return;
|
|
@@ -6591,7 +7049,7 @@ const buildThroughPlan = (relationName, relEntry, relation, table, targetTableNa
|
|
|
6591
7049
|
targetCol
|
|
6592
7050
|
},
|
|
6593
7051
|
targetKeyColumns: { [foreignColPropName]: foreignCol },
|
|
6594
|
-
...ownWherePlan(relEntry, table, targetTable, localCol),
|
|
7052
|
+
...ownWherePlan(relEntry, table, targetTable, localCol, []),
|
|
6595
7053
|
allowed: /* @__PURE__ */ new Set([
|
|
6596
7054
|
"connect",
|
|
6597
7055
|
"disconnect",
|
|
@@ -6670,7 +7128,7 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
|
|
|
6670
7128
|
const targetPrimaryKey = resolvePrimaryKeys(targetTable).filter((name) => targetColumns[name]);
|
|
6671
7129
|
const targetKeyColumns = Object.fromEntries(targetPrimaryKey.map((name) => [name, targetColumns[name]]));
|
|
6672
7130
|
targetKeyColumns[foreignColPropName] = foreignCol;
|
|
6673
|
-
const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol);
|
|
7131
|
+
const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol, resolvePrimaryKeys(table));
|
|
6674
7132
|
const ops = !fkColumn.notNull ? isOne ? [
|
|
6675
7133
|
"create",
|
|
6676
7134
|
"connect",
|
|
@@ -7103,6 +7561,16 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7103
7561
|
}
|
|
7104
7562
|
if (supplied(op["connect"])) await link(asArray(op["connect"]), "connect");
|
|
7105
7563
|
};
|
|
7564
|
+
/**
|
|
7565
|
+
* The value a parent-side `connect` / `disconnect` writes to an existing row's key: `next`,
|
|
7566
|
+
* unless the key points at a row the relation's own `where` hides, which it then keeps.
|
|
7567
|
+
*/
|
|
7568
|
+
const keepingHidden = (plan, next) => {
|
|
7569
|
+
if (!plan.ownCondition) return next;
|
|
7570
|
+
const shown = sql`select ${plan.foreignCol} from ${plan.targetTable} where ${plan.ownCondition()}`;
|
|
7571
|
+
const replacement = next === null ? sql`null` : sql`${sql.param(next, plan.localCol)}`;
|
|
7572
|
+
return sql`case when ${plan.localCol} is null or ${plan.localCol} in (${shown}) then ${replacement} else ${plan.localCol} end`;
|
|
7573
|
+
};
|
|
7106
7574
|
const detachAll = (executor, plan, key, scope, steps, op) => steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where(withScope(scope, plan.targetTableName, plan.targetTable, and(eq(plan.foreignCol, key), plan.ownCondition?.()))), op, "update");
|
|
7107
7575
|
return {
|
|
7108
7576
|
enabled: (tableName) => !!plans[tableName],
|
|
@@ -7128,7 +7596,10 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7128
7596
|
const allColumns = getColumns(table);
|
|
7129
7597
|
for (const relationName of Object.keys(ops)) {
|
|
7130
7598
|
const plan = tablePlans[relationName];
|
|
7131
|
-
if (plan
|
|
7599
|
+
if (plan && !(plan.localColPropName in columns) && allColumns[plan.localColPropName]) columns[plan.localColPropName] = allColumns[plan.localColPropName];
|
|
7600
|
+
if (plan?.fkSide === "parent" && plan.parentHidden) {
|
|
7601
|
+
for (const [name, column] of Object.entries(plan.parentKeyColumns ?? {})) if (!(name in columns)) columns[name] = column;
|
|
7602
|
+
}
|
|
7132
7603
|
}
|
|
7133
7604
|
for (const name of selfTargetingPlan(tablePlans, tableName, ops)?.targetPrimaryKey ?? []) if (!(name in columns) && allColumns[name]) columns[name] = allColumns[name];
|
|
7134
7605
|
return columns;
|
|
@@ -7148,10 +7619,15 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7148
7619
|
...fresh.get(keyOf(row))
|
|
7149
7620
|
}));
|
|
7150
7621
|
},
|
|
7151
|
-
applyParentSide: async (executor, tableName, ops, request) => {
|
|
7622
|
+
applyParentSide: async (executor, tableName, ops, request, existing = false) => {
|
|
7152
7623
|
const tablePlans = plans[tableName];
|
|
7153
|
-
const
|
|
7154
|
-
|
|
7624
|
+
const result = {
|
|
7625
|
+
values: {},
|
|
7626
|
+
expected: {},
|
|
7627
|
+
existing
|
|
7628
|
+
};
|
|
7629
|
+
const patch = result.values;
|
|
7630
|
+
if (!tablePlans) return result;
|
|
7155
7631
|
const context = request?.context;
|
|
7156
7632
|
const scope = resolveScope(policies, context, filterCtx);
|
|
7157
7633
|
for (const [relationName, op] of Object.entries(ops)) {
|
|
@@ -7170,14 +7646,30 @@ const createNestedWriteRuntime = (params) => {
|
|
|
7170
7646
|
if (!created) throw drizzleError(`Drizzle-GraphQL Error: 'create' on '${relationName}' inserted no row, so there is nothing to attach.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
7171
7647
|
await steps.after();
|
|
7172
7648
|
patch[plan.localColPropName] = created[plan.foreignColPropName];
|
|
7649
|
+
result.expected[relationName] = created[plan.foreignColPropName];
|
|
7173
7650
|
} else if (op["connect"] !== void 0) {
|
|
7174
7651
|
const rows = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
|
|
7175
7652
|
if (rows.length !== 1) throw connectMatchError(relationName, rows.length);
|
|
7176
|
-
|
|
7177
|
-
|
|
7653
|
+
const key = rows[0][plan.foreignColPropName];
|
|
7654
|
+
patch[plan.localColPropName] = existing ? keepingHidden(plan, key) : key;
|
|
7655
|
+
result.expected[relationName] = key;
|
|
7656
|
+
} else if (op["disconnect"] === true) patch[plan.localColPropName] = existing ? keepingHidden(plan, null) : null;
|
|
7178
7657
|
}
|
|
7179
7658
|
for (const columnName of Object.keys(contextValues?.(tableName) ?? {})) delete patch[columnName];
|
|
7180
|
-
return
|
|
7659
|
+
return result;
|
|
7660
|
+
},
|
|
7661
|
+
checkParentSide: async (executor, tableName, ops, rows, patch) => {
|
|
7662
|
+
const tablePlans = plans[tableName];
|
|
7663
|
+
if (!tablePlans || !rows.length) return;
|
|
7664
|
+
for (const relationName of Object.keys(ops)) {
|
|
7665
|
+
const plan = tablePlans[relationName];
|
|
7666
|
+
if (plan?.fkSide !== "parent") continue;
|
|
7667
|
+
if (relationName in patch.expected) {
|
|
7668
|
+
const expected = keyString(patch.expected[relationName]);
|
|
7669
|
+
if (rows.some((row) => keyString(row[plan.localColPropName]) !== expected)) throw drizzleError(patch.existing ? `Drizzle-GraphQL Error: cannot connect through '${relationName}': the row it holds now is one its \`where\` hides, which a write through it cannot detach.` : `Drizzle-GraphQL Error: cannot write through '${relationName}': the ${tableName} row was not written with it — an upsert whose conflict branch did not write '${plan.localColPropName}'. Include it in onConflict.update, or leave update unset.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
7670
|
+
}
|
|
7671
|
+
if (plan.parentHidden && await plan.parentHidden(executor, rows)) throw drizzleError(`Drizzle-GraphQL Error: cannot write through '${relationName}': its \`where\` hides the relation on this ${tableName} row.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
|
|
7672
|
+
}
|
|
7181
7673
|
},
|
|
7182
7674
|
applyChildSide: async (executor, tableName, ops, parentRows, request) => {
|
|
7183
7675
|
const tablePlans = plans[tableName];
|
|
@@ -7244,12 +7736,13 @@ var NoParentRow = class extends Error {};
|
|
|
7244
7736
|
* `executor` must already be a transaction.
|
|
7245
7737
|
*/
|
|
7246
7738
|
const writeNestedEntry = async (params) => {
|
|
7247
|
-
const { executor, runtime, tableName, ops, write, request } = params;
|
|
7739
|
+
const { executor, runtime, tableName, ops, write, request, existing } = params;
|
|
7248
7740
|
const guarded = runtime.createsParentSide(tableName, ops);
|
|
7249
7741
|
const run = async (tx) => {
|
|
7250
|
-
const patch = await runtime.applyParentSide(tx, tableName, ops, request);
|
|
7251
|
-
const rows = await write(tx, patch);
|
|
7742
|
+
const patch = await runtime.applyParentSide(tx, tableName, ops, request, existing);
|
|
7743
|
+
const rows = await write(tx, patch.values);
|
|
7252
7744
|
if (guarded && !rows.length) throw new NoParentRow();
|
|
7745
|
+
await runtime.checkParentSide(tx, tableName, ops, rows, patch);
|
|
7253
7746
|
await runtime.applyChildSide(tx, tableName, ops, rows, request);
|
|
7254
7747
|
return runtime.refreshed(tx, tableName, ops, rows);
|
|
7255
7748
|
};
|
|
@@ -7311,7 +7804,8 @@ const updateWithNestedOps = async (params) => {
|
|
|
7311
7804
|
...remapValues(columns),
|
|
7312
7805
|
...patch
|
|
7313
7806
|
}),
|
|
7314
|
-
request
|
|
7807
|
+
request,
|
|
7808
|
+
existing: true
|
|
7315
7809
|
}));
|
|
7316
7810
|
};
|
|
7317
7811
|
//#endregion
|
|
@@ -7464,6 +7958,7 @@ const createSchemaBuilder = (adapter) => {
|
|
|
7464
7958
|
const anyTable = (feature) => tableEntries.some(([name]) => featureOf(name)[feature]);
|
|
7465
7959
|
if (!tableEntries.length) throw new Error("Drizzle-GraphQL Error: No tables detected in Drizzle-ORM's database instance. Did you forget to pass schema to drizzle constructor?");
|
|
7466
7960
|
registerScalarOverrides(tables, options);
|
|
7961
|
+
installSqliteBlobJsonDecoders(tables);
|
|
7467
7962
|
registerEnumConfig(options);
|
|
7468
7963
|
registerColumnExclusions(tables, options.exclude);
|
|
7469
7964
|
resetAggregateColumnCache();
|
|
@@ -7695,7 +8190,7 @@ const createSchemaBuilder = (adapter) => {
|
|
|
7695
8190
|
...tableFeatures.update ? [updateInput] : [],
|
|
7696
8191
|
...built.updateManyInput ? [built.updateManyInput] : [],
|
|
7697
8192
|
tableFilters,
|
|
7698
|
-
tableOrder
|
|
8193
|
+
...tableOrder ? [tableOrder] : []
|
|
7699
8194
|
].forEach((e) => {
|
|
7700
8195
|
inputs[e.name] = e;
|
|
7701
8196
|
});
|
|
@@ -7825,14 +8320,12 @@ const generateUpsert = (db, tableName, table, baseType, onConflictType, fieldNam
|
|
|
7825
8320
|
withTarget: false,
|
|
7826
8321
|
serverOwned: Object.keys(contextColumns ?? {})
|
|
7827
8322
|
});
|
|
7828
|
-
const requestedNothing = args.onConflict?.action === "NOTHING";
|
|
7829
8323
|
const runBatch = async (target, { values, plan }) => {
|
|
7830
|
-
if (plan.action === "NOTHING"
|
|
8324
|
+
if (plan.action === "NOTHING") {
|
|
7831
8325
|
const columns = getColumns(table);
|
|
7832
8326
|
const keyName = pkNames[0] ?? Object.keys(columns)[0];
|
|
7833
8327
|
await target.insert(table).values(values).onDuplicateKeyUpdate({ set: { [keyName]: sql`${sql.identifier(columns[keyName].name)}` } });
|
|
7834
|
-
} else
|
|
7835
|
-
else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
|
|
8328
|
+
} else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
|
|
7836
8329
|
};
|
|
7837
8330
|
if (batches.length === 1) await runBatch(executor, batches[0]);
|
|
7838
8331
|
else await executor.transaction(async (tx) => {
|
|
@@ -7872,7 +8365,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
|
|
|
7872
8365
|
});
|
|
7873
8366
|
if (single) await assertSingleMatch(executor, table, filters);
|
|
7874
8367
|
let query = executor.update(table).set(input);
|
|
7875
|
-
if (filters) query = query.where(filters);
|
|
8368
|
+
if (filters) query = query.where(mysqlWriteFilter(table, filters));
|
|
7876
8369
|
await query;
|
|
7877
8370
|
await after();
|
|
7878
8371
|
return isSuccess;
|
|
@@ -7887,7 +8380,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
|
|
|
7887
8380
|
* in order. MySQL has no RETURNING, so — like every other MySQL mutation — the result is
|
|
7888
8381
|
* `{ isSuccess: true }` rather than the updated rows.
|
|
7889
8382
|
*/
|
|
7890
|
-
const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName, filterCtx, txCtx, policies) => writeResolver({
|
|
8383
|
+
const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName, requireWhere, filterCtx, txCtx, policies) => writeResolver({
|
|
7891
8384
|
db,
|
|
7892
8385
|
tableName,
|
|
7893
8386
|
operation: "updateMany",
|
|
@@ -7907,13 +8400,20 @@ const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName,
|
|
|
7907
8400
|
if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
|
|
7908
8401
|
return {
|
|
7909
8402
|
set: input,
|
|
7910
|
-
filters:
|
|
8403
|
+
filters: scopedWhere({
|
|
8404
|
+
scope,
|
|
8405
|
+
tableName,
|
|
8406
|
+
table,
|
|
8407
|
+
where,
|
|
8408
|
+
relationCtx: relationFilterCtx(filterCtx, tableName, scope),
|
|
8409
|
+
required: requireWhere
|
|
8410
|
+
})
|
|
7911
8411
|
};
|
|
7912
8412
|
});
|
|
7913
8413
|
await executor.transaction(async (tx) => {
|
|
7914
8414
|
for (const entry of entries) {
|
|
7915
8415
|
let query = tx.update(table).set(entry.set);
|
|
7916
|
-
if (entry.filters) query = query.where(entry.filters);
|
|
8416
|
+
if (entry.filters) query = query.where(mysqlWriteFilter(table, entry.filters));
|
|
7917
8417
|
await query;
|
|
7918
8418
|
}
|
|
7919
8419
|
});
|
|
@@ -7962,7 +8462,7 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
|
|
|
7962
8462
|
});
|
|
7963
8463
|
if (single) await assertSingleMatch(executor, table, filters);
|
|
7964
8464
|
let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
|
|
7965
|
-
if (filters) query = query.where(filters);
|
|
8465
|
+
if (filters) query = query.where(mysqlWriteFilter(table, filters));
|
|
7966
8466
|
await query;
|
|
7967
8467
|
await after();
|
|
7968
8468
|
return isSuccess;
|
|
@@ -7973,6 +8473,34 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
|
|
|
7973
8473
|
const getTableConfig$5 = memoizeTableConfig(getTableConfig);
|
|
7974
8474
|
/** Primary-key property names for a MySQL table, including table-level composite keys. */
|
|
7975
8475
|
const mysqlPrimaryKeyPropNames = (table) => getPrimaryKeyPropNamesFromConfig(table, getTableConfig$5);
|
|
8476
|
+
const TABLE_ORIGINAL_NAME = Symbol.for("drizzle:OriginalName");
|
|
8477
|
+
const TABLE_SCHEMA = Symbol.for("drizzle:Schema");
|
|
8478
|
+
/** Whether `chunk` reads `table` anywhere — under an alias, in a subquery, however deep. */
|
|
8479
|
+
const readsTable = (chunk, table) => {
|
|
8480
|
+
if (Array.isArray(chunk)) return chunk.some((part) => readsTable(part, table));
|
|
8481
|
+
if (is(chunk, Table)) return chunk[TABLE_ORIGINAL_NAME] === table[TABLE_ORIGINAL_NAME] && chunk[TABLE_SCHEMA] === table[TABLE_SCHEMA];
|
|
8482
|
+
if (is(chunk, SQL)) return readsTable(chunk.queryChunks, table);
|
|
8483
|
+
if (is(chunk, SQL.Aliased)) return readsTable(chunk.sql, table);
|
|
8484
|
+
if (is(chunk, Subquery)) return readsTable(chunk._.sql, table);
|
|
8485
|
+
return false;
|
|
8486
|
+
};
|
|
8487
|
+
/**
|
|
8488
|
+
* The `where` of an UPDATE or DELETE, in a form MySQL accepts. MySQL refuses a statement whose
|
|
8489
|
+
* `where` reads the table it writes (error 1093, "You can't specify target table … for update
|
|
8490
|
+
* in FROM clause") — which a relation filter through a self-referencing relation, or a scope
|
|
8491
|
+
* written as one, does. Such a filter is resolved to the primary keys it matches first, through
|
|
8492
|
+
* a derived table MySQL materializes (`DISTINCT` keeps the optimizer from merging it back in),
|
|
8493
|
+
* and the write matches those keys. Any other filter is returned as it is.
|
|
8494
|
+
*/
|
|
8495
|
+
const mysqlWriteFilter = (table, filters) => {
|
|
8496
|
+
if (!filters || !readsTable(filters, table)) return filters;
|
|
8497
|
+
const columns = getColumns(table);
|
|
8498
|
+
const keys = mysqlPrimaryKeyPropNames(table).map((name) => columns[name]);
|
|
8499
|
+
if (!keys.length) return filters;
|
|
8500
|
+
const keyList = sql.join(keys.map((column) => sql.identifier(column.name)), sql`, `);
|
|
8501
|
+
const target = keys.length === 1 ? sql`${keys[0]}` : sql`(${sql.join(keys, sql`, `)})`;
|
|
8502
|
+
return sql`${target} in (select ${keyList} from (select distinct ${sql.join(keys, sql`, `)} from ${table} where ${filters}) as ${sql.identifier("dgql_write_target")})`;
|
|
8503
|
+
};
|
|
7976
8504
|
const { prepareBuild, forEachTable, finalizeBuild } = createSchemaBuilder({
|
|
7977
8505
|
tableClass: MySqlTable,
|
|
7978
8506
|
getTableConfig: getTableConfig$5,
|
|
@@ -8026,7 +8554,7 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
|
|
|
8026
8554
|
tableFilters,
|
|
8027
8555
|
cacheCtx
|
|
8028
8556
|
}) : void 0;
|
|
8029
|
-
const updateManyGenerated = updateManyInput ? generateUpdateMany$2(db, tableName, schema[tableName], updateManyInput, updateManyFieldName, filterCtx, mutationTxCtx, policies) : void 0;
|
|
8557
|
+
const updateManyGenerated = updateManyInput ? generateUpdateMany$2(db, tableName, schema[tableName], updateManyInput, updateManyFieldName, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
|
|
8030
8558
|
const deleteGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
|
|
8031
8559
|
const deleteSingleGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteSingleFieldName, true, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
|
|
8032
8560
|
const restoreGenerated = softDeleteInfo ? generateDelete(db, tableName, schema[tableName], tableFilters, restoreFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies, true) : void 0;
|
|
@@ -8043,7 +8571,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
|
|
|
8043
8571
|
filterCtx,
|
|
8044
8572
|
txCtx: mutationTxCtx,
|
|
8045
8573
|
policies,
|
|
8046
|
-
returnsRows: false
|
|
8574
|
+
returnsRows: false,
|
|
8575
|
+
writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
|
|
8047
8576
|
}) : void 0;
|
|
8048
8577
|
const deleteCountGenerated = tableFeatures.delete && tableFeatures.countMutations ? generateWriteCount({
|
|
8049
8578
|
db,
|
|
@@ -8056,7 +8585,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
|
|
|
8056
8585
|
filterCtx,
|
|
8057
8586
|
txCtx: mutationTxCtx,
|
|
8058
8587
|
policies,
|
|
8059
|
-
returnsRows: false
|
|
8588
|
+
returnsRows: false,
|
|
8589
|
+
writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
|
|
8060
8590
|
}) : void 0;
|
|
8061
8591
|
const hardDeleteMeta = softDeleteInfo?.hardDelete ? { hardDelete: true } : {};
|
|
8062
8592
|
const withMeta = (generated, meta) => ({
|
|
@@ -8168,7 +8698,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8168
8698
|
* one-element list.
|
|
8169
8699
|
*/
|
|
8170
8700
|
const generateInsert = (build, opts) => {
|
|
8171
|
-
const { db, tables, relationMap, typeNameMapper, txCtx, nested, limits, policies, resolveName } = build;
|
|
8701
|
+
const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
|
|
8172
8702
|
const { tableName, table, baseType, fieldName, typeName, single, conflictDoNothing = false } = opts;
|
|
8173
8703
|
const queryArgs = { values: { type: single ? new GraphQLNonNull(baseType) : new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(baseType))) } };
|
|
8174
8704
|
const pkNames = primaryKeyPropNames(table);
|
|
@@ -8192,6 +8722,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8192
8722
|
const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
|
|
8193
8723
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
8194
8724
|
relationMap,
|
|
8725
|
+
filterCtx,
|
|
8195
8726
|
tables,
|
|
8196
8727
|
tableName,
|
|
8197
8728
|
typeName,
|
|
@@ -8270,6 +8801,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8270
8801
|
const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
|
|
8271
8802
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
8272
8803
|
relationMap,
|
|
8804
|
+
filterCtx,
|
|
8273
8805
|
tables,
|
|
8274
8806
|
tableName,
|
|
8275
8807
|
typeName,
|
|
@@ -8294,24 +8826,25 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8294
8826
|
serverOwned: Object.keys(contextColumns ?? {}),
|
|
8295
8827
|
buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope))
|
|
8296
8828
|
});
|
|
8297
|
-
const
|
|
8298
|
-
|
|
8829
|
+
const batchQuery = (executor, { values: rows, plan }) => {
|
|
8830
|
+
const query = executor.insert(table).values(rows).returning(returning);
|
|
8299
8831
|
const setWhere = withScope(scope, tableName, table, plan.setWhere);
|
|
8300
|
-
query
|
|
8301
|
-
target: plan.target,
|
|
8302
|
-
where: plan.targetWhere
|
|
8303
|
-
} : void 0) : query.onConflictDoUpdate({
|
|
8304
|
-
target: plan.target,
|
|
8305
|
-
targetWhere: plan.targetWhere,
|
|
8306
|
-
set: plan.set,
|
|
8307
|
-
setWhere
|
|
8308
|
-
});
|
|
8309
|
-
return await query;
|
|
8832
|
+
return applyConflictPlan(query, table, plan, setWhere);
|
|
8310
8833
|
};
|
|
8311
|
-
if (batches.length === 1) return
|
|
8312
|
-
return target.transaction(
|
|
8313
|
-
const
|
|
8314
|
-
|
|
8834
|
+
if (batches.length === 1) return await batchQuery(target, batches[0]);
|
|
8835
|
+
return target.transaction((tx) => {
|
|
8836
|
+
const execute = (batch) => {
|
|
8837
|
+
const query = batchQuery(tx, batch);
|
|
8838
|
+
return typeof query.all === "function" ? query.all() : query;
|
|
8839
|
+
};
|
|
8840
|
+
const first = execute(batches[0]);
|
|
8841
|
+
if (typeof first?.then === "function") return (async () => {
|
|
8842
|
+
const rows = [...await first];
|
|
8843
|
+
for (const batch of batches.slice(1)) rows.push(...await execute(batch));
|
|
8844
|
+
return rows;
|
|
8845
|
+
})();
|
|
8846
|
+
const rows = [...first];
|
|
8847
|
+
for (const batch of batches.slice(1)) rows.push(...execute(batch));
|
|
8315
8848
|
return rows;
|
|
8316
8849
|
});
|
|
8317
8850
|
};
|
|
@@ -8359,6 +8892,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
8359
8892
|
await before();
|
|
8360
8893
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
8361
8894
|
relationMap,
|
|
8895
|
+
filterCtx,
|
|
8362
8896
|
tables,
|
|
8363
8897
|
tableName,
|
|
8364
8898
|
typeName,
|
|
@@ -8597,7 +9131,8 @@ const createSchemaDataGenerator = (adapter) => {
|
|
|
8597
9131
|
table: schema[tableName],
|
|
8598
9132
|
updateManyInput,
|
|
8599
9133
|
fieldName: updateManyFieldName,
|
|
8600
|
-
typeName
|
|
9134
|
+
typeName,
|
|
9135
|
+
requireWhere: tableFeatures.requireWhere
|
|
8601
9136
|
}) : void 0;
|
|
8602
9137
|
const deleteGenerated = tableFeatures.delete ? generateDelete(writeBuild, {
|
|
8603
9138
|
tableName,
|
|
@@ -8852,7 +9387,8 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
|
|
|
8852
9387
|
tableName,
|
|
8853
9388
|
ops: entry.ops,
|
|
8854
9389
|
write,
|
|
8855
|
-
request
|
|
9390
|
+
request,
|
|
9391
|
+
existing: true
|
|
8856
9392
|
}) : await write(tx, {}));
|
|
8857
9393
|
}
|
|
8858
9394
|
return results;
|
|
@@ -8873,7 +9409,7 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
|
|
|
8873
9409
|
const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateManyBatch) => {
|
|
8874
9410
|
return (build, opts) => {
|
|
8875
9411
|
const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
|
|
8876
|
-
const { tableName, table, updateManyInput, fieldName, typeName } = opts;
|
|
9412
|
+
const { tableName, table, updateManyInput, fieldName, typeName, requireWhere } = opts;
|
|
8877
9413
|
const queryArgs = { updates: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(updateManyInput))) } };
|
|
8878
9414
|
const pkNames = primaryKeyPropNames(table);
|
|
8879
9415
|
return writeResolver({
|
|
@@ -8893,6 +9429,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
|
|
|
8893
9429
|
const contextColumns = policies?.contextValues?.(tableName);
|
|
8894
9430
|
const { columns, hasRelations, withParams } = mutationSelection(info, {
|
|
8895
9431
|
relationMap,
|
|
9432
|
+
filterCtx,
|
|
8896
9433
|
tables,
|
|
8897
9434
|
tableName,
|
|
8898
9435
|
typeName,
|
|
@@ -8912,7 +9449,14 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
|
|
|
8912
9449
|
return {
|
|
8913
9450
|
set: input,
|
|
8914
9451
|
ops,
|
|
8915
|
-
filters:
|
|
9452
|
+
filters: scopedWhere({
|
|
9453
|
+
scope,
|
|
9454
|
+
tableName,
|
|
9455
|
+
table,
|
|
9456
|
+
where,
|
|
9457
|
+
relationCtx: relationFilterCtx(filterCtx, tableName, scope),
|
|
9458
|
+
required: requireWhere
|
|
9459
|
+
})
|
|
8916
9460
|
};
|
|
8917
9461
|
});
|
|
8918
9462
|
const anyNested = entries.some((entry) => entry.ops);
|
|
@@ -9001,7 +9545,8 @@ const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNest
|
|
|
9001
9545
|
tableName,
|
|
9002
9546
|
ops: entry.ops,
|
|
9003
9547
|
write,
|
|
9004
|
-
request
|
|
9548
|
+
request,
|
|
9549
|
+
existing: true
|
|
9005
9550
|
}) : write(tx, {});
|
|
9006
9551
|
};
|
|
9007
9552
|
return executor.transaction((tx) => {
|
|
@@ -9031,6 +9576,7 @@ const sqliteSchemaData = createSchemaDataGenerator({
|
|
|
9031
9576
|
preflight: (db, options) => {
|
|
9032
9577
|
if (options.features.nestedWrites && db.resultKind === "sync") throw new Error("Drizzle-GraphQL Error: features.nestedWrites requires an asynchronous SQLite driver (e.g. libsql). Synchronous drivers cannot run the multi-statement transaction a nested write needs.");
|
|
9033
9578
|
if (options.transactions && db.resultKind === "sync") throw new Error("Drizzle-GraphQL Error: transactions: 'auto' requires an asynchronous SQLite driver (e.g. libsql). Synchronous drivers cannot hold a transaction open across resolvers.");
|
|
9579
|
+
if (options.policies?.onWrite && db.resultKind === "sync") throw new Error("Drizzle-GraphQL Error: onWrite requires an asynchronous SQLite driver (e.g. libsql). Synchronous drivers commit before an awaited hook runs, so a throwing hook could not roll the mutation back.");
|
|
9034
9580
|
},
|
|
9035
9581
|
generateUpdateMany
|
|
9036
9582
|
});
|