kitcn 0.17.3 → 0.17.4
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/dist/aggregate/index.d.ts +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/{local-env-fAYvoOfl.mjs → local-env-DykABjCt.mjs} +44 -13
- package/dist/orm/index.d.ts +1 -1
- package/dist/orm/index.js +423 -190
- package/dist/watcher.mjs +1 -1
- package/dist/{where-clause-compiler-BgEc0GdS.d.ts → where-clause-compiler-BnVpjKn6.d.ts} +79 -33
- package/package.json +1 -1
package/dist/orm/index.js
CHANGED
|
@@ -350,6 +350,63 @@ const hasUserCreatedAtColumn = (table) => {
|
|
|
350
350
|
};
|
|
351
351
|
const usesSystemCreatedAtAlias = (_table) => true;
|
|
352
352
|
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/orm/write-fanout.ts
|
|
355
|
+
/**
|
|
356
|
+
* Shared primitives for fanning out independent writes.
|
|
357
|
+
*
|
|
358
|
+
* Deliberately dependency-free: `mutation-utils`, `lifecycle` and `query` all
|
|
359
|
+
* import it, and Convex bundles every static import of a function entry.
|
|
360
|
+
*/
|
|
361
|
+
const ORMLIFECYCLE_HOOKED_TABLES = Symbol.for("kitcn:OrmLifecycleHookedTables");
|
|
362
|
+
/** Matches the ORM's relation-loading default. */
|
|
363
|
+
const DEFAULT_WRITE_FANOUT_CONCURRENCY = 25;
|
|
364
|
+
/**
|
|
365
|
+
* Runs `worker` over `items` with at most `limit` in flight. Results keep input
|
|
366
|
+
* order. Unbounded `Promise.all` is not an option here: a fan-out is bounded
|
|
367
|
+
* only by `mutationMaxRows` (10,000), and that many simultaneous in-flight
|
|
368
|
+
* syscalls is its own failure mode.
|
|
369
|
+
*/
|
|
370
|
+
async function mapWithConcurrency(items, limit, worker) {
|
|
371
|
+
if (items.length === 0) return [];
|
|
372
|
+
const width = Math.max(1, Math.min(limit, items.length));
|
|
373
|
+
const results = new Array(items.length);
|
|
374
|
+
let nextIndex = 0;
|
|
375
|
+
const runWorker = async () => {
|
|
376
|
+
while (true) {
|
|
377
|
+
const index = nextIndex;
|
|
378
|
+
nextIndex += 1;
|
|
379
|
+
if (index >= items.length) return;
|
|
380
|
+
results[index] = await worker(items[index], index);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
await Promise.all(Array.from({ length: width }, () => runWorker()));
|
|
384
|
+
return results;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Records which tables the lifecycle writer intercepts, so write fan-out can
|
|
388
|
+
* see it without importing `lifecycle` (which would close an import cycle
|
|
389
|
+
* through `aggregate-index/runtime`).
|
|
390
|
+
*/
|
|
391
|
+
const markLifecycleHookedTables = (db, tableNames) => {
|
|
392
|
+
Object.defineProperty(db, ORMLIFECYCLE_HOOKED_TABLES, {
|
|
393
|
+
configurable: false,
|
|
394
|
+
enumerable: false,
|
|
395
|
+
value: tableNames,
|
|
396
|
+
writable: false
|
|
397
|
+
});
|
|
398
|
+
return db;
|
|
399
|
+
};
|
|
400
|
+
/**
|
|
401
|
+
* True when writes to `tableName` run through trigger / aggregate-index hooks.
|
|
402
|
+
* Those writes are serialized by the lifecycle write lock and their hooks fire
|
|
403
|
+
* in write order, so they must keep their sequential loop; everything else is
|
|
404
|
+
* free to fan out.
|
|
405
|
+
*/
|
|
406
|
+
const hasLifecycleHooks = (db, tableName) => {
|
|
407
|
+
return (db?.[ORMLIFECYCLE_HOOKED_TABLES])?.has(tableName) ?? false;
|
|
408
|
+
};
|
|
409
|
+
|
|
353
410
|
//#endregion
|
|
354
411
|
//#region src/orm/mutation-utils.ts
|
|
355
412
|
const UNDEFINED_SENTINEL_KEY = "__kitcnUndefined";
|
|
@@ -430,23 +487,6 @@ const normalizeTemporalComparableValue = (table, fieldName, value) => {
|
|
|
430
487
|
if (Array.isArray(value)) return value.map((entry) => normalizeTemporalWriteValue(descriptor, entry));
|
|
431
488
|
return normalizeTemporalWriteValue(descriptor, value);
|
|
432
489
|
};
|
|
433
|
-
const normalizePublicSystemFields = (value, options) => {
|
|
434
|
-
if (!isPlainObject$1(value)) return value;
|
|
435
|
-
const hasId = Object.hasOwn(value, INTERNAL_ID_FIELD$2);
|
|
436
|
-
const hasCreationTime = Object.hasOwn(value, INTERNAL_CREATION_TIME_FIELD);
|
|
437
|
-
if (!hasId && !hasCreationTime) return value;
|
|
438
|
-
const obj = value;
|
|
439
|
-
const { [INTERNAL_ID_FIELD$2]: internalId, ...rest } = obj;
|
|
440
|
-
const publicRow = { ...rest };
|
|
441
|
-
if (hasId) publicRow[PUBLIC_ID_FIELD$2] = internalId;
|
|
442
|
-
if (hasCreationTime) {
|
|
443
|
-
const raw = obj[INTERNAL_CREATION_TIME_FIELD];
|
|
444
|
-
if (options?.useSystemCreatedAtAlias && raw !== void 0) publicRow[PUBLIC_CREATED_AT_FIELD] = raw;
|
|
445
|
-
delete publicRow[INTERNAL_CREATION_TIME_FIELD];
|
|
446
|
-
}
|
|
447
|
-
delete publicRow[INTERNAL_ID_FIELD$2];
|
|
448
|
-
return publicRow;
|
|
449
|
-
};
|
|
450
490
|
const normalizeDateFieldsForWrite = (table, value) => {
|
|
451
491
|
const useSystemCreatedAt = usesSystemCreatedAtAlias(table);
|
|
452
492
|
const hasUserCreatedAt = hasUserCreatedAtColumn(table);
|
|
@@ -460,19 +500,34 @@ const normalizeDateFieldsForWrite = (table, value) => {
|
|
|
460
500
|
}
|
|
461
501
|
return result;
|
|
462
502
|
};
|
|
503
|
+
/**
|
|
504
|
+
* Reshapes a stored document into its public form in a single pass: renames
|
|
505
|
+
* `_id` to `id`, aliases `_creationTime` to `createdAt`, and hydrates temporal
|
|
506
|
+
* columns. Deliberately allocates exactly one object and never calls `delete`
|
|
507
|
+
* on it — deleting an existing own property drops the object into V8
|
|
508
|
+
* dictionary mode, which then poisons every downstream spread.
|
|
509
|
+
*/
|
|
463
510
|
const hydrateDateFieldsForRead = (table, value) => {
|
|
464
|
-
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
const
|
|
511
|
+
if (!isPlainObject$1(value)) return value;
|
|
512
|
+
const obj = value;
|
|
513
|
+
const hasId = Object.hasOwn(obj, INTERNAL_ID_FIELD$2);
|
|
514
|
+
const rawCreationTime = obj[INTERNAL_CREATION_TIME_FIELD];
|
|
515
|
+
const aliasCreatedAt = usesSystemCreatedAtAlias(table) && Object.hasOwn(obj, INTERNAL_CREATION_TIME_FIELD) && rawCreationTime !== void 0;
|
|
468
516
|
const temporalColumns = getTemporalColumnDescriptors(table);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
517
|
+
const result = {};
|
|
518
|
+
for (const key of Object.keys(obj)) {
|
|
519
|
+
if (key === INTERNAL_ID_FIELD$2 || key === INTERNAL_CREATION_TIME_FIELD) continue;
|
|
520
|
+
if (key === PUBLIC_CREATED_AT_FIELD && aliasCreatedAt) {
|
|
521
|
+
result[key] = void 0;
|
|
472
522
|
continue;
|
|
473
523
|
}
|
|
474
|
-
|
|
475
|
-
result[
|
|
524
|
+
const descriptor = temporalColumns.get(key);
|
|
525
|
+
result[key] = descriptor ? hydrateTemporalReadValue(descriptor, obj[key]) : obj[key];
|
|
526
|
+
}
|
|
527
|
+
if (hasId) result[PUBLIC_ID_FIELD$2] = obj[INTERNAL_ID_FIELD$2];
|
|
528
|
+
if (aliasCreatedAt) {
|
|
529
|
+
const descriptor = temporalColumns.get(PUBLIC_CREATED_AT_FIELD);
|
|
530
|
+
result[PUBLIC_CREATED_AT_FIELD] = descriptor ? hydrateTemporalReadValue(descriptor, rawCreationTime) : rawCreationTime;
|
|
476
531
|
}
|
|
477
532
|
return result;
|
|
478
533
|
};
|
|
@@ -996,8 +1051,24 @@ async function softDeleteRow(db, table, row) {
|
|
|
996
1051
|
await db.patch(tableName, row._id, { deletionTime });
|
|
997
1052
|
return deletionTime;
|
|
998
1053
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1054
|
+
/**
|
|
1055
|
+
* Applies one loop-invariant payload to every referencing row. The ids come
|
|
1056
|
+
* from a single index scan so they are distinct, and the write set is
|
|
1057
|
+
* order-independent — but a table whose writes run through lifecycle hooks
|
|
1058
|
+
* keeps the sequential loop: those writes are serialized by the lifecycle write
|
|
1059
|
+
* lock anyway, and their hooks would otherwise fire out of write order.
|
|
1060
|
+
*/
|
|
1061
|
+
async function patchReferencingRows(db, tableName, rows, patch) {
|
|
1062
|
+
if (hasLifecycleHooks(db, tableName)) {
|
|
1063
|
+
for (const row of rows) await db.patch(tableName, row._id, patch);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
await mapWithConcurrency(rows, DEFAULT_WRITE_FANOUT_CONCURRENCY, async (row) => {
|
|
1067
|
+
await db.patch(tableName, row._id, patch);
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
async function hardDeleteRow(db, tableName, row) {
|
|
1071
|
+
await db.delete(tableName, row._id);
|
|
1001
1072
|
}
|
|
1002
1073
|
async function applyIncomingForeignKeyActionsOnDelete(db, table, row, options) {
|
|
1003
1074
|
const tableName = getTableName(table);
|
|
@@ -1053,16 +1124,14 @@ async function applyIncomingForeignKeyActionsOnDelete(db, table, row, options) {
|
|
|
1053
1124
|
if (referencingRows.length === 0) continue;
|
|
1054
1125
|
if (action === "set null") {
|
|
1055
1126
|
ensureNullableColumns(foreignKey.sourceTable, foreignKey.sourceColumns, `Foreign key set null on '${foreignKey.sourceTableName}'`);
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
await db.patch(foreignKey.sourceTableName, referencingRow._id, patch);
|
|
1060
|
-
}
|
|
1127
|
+
const nullPatch = {};
|
|
1128
|
+
for (const columnName of foreignKey.sourceColumns) nullPatch[columnName] = null;
|
|
1129
|
+
await patchReferencingRows(db, foreignKey.sourceTableName, referencingRows, nullPatch);
|
|
1061
1130
|
continue;
|
|
1062
1131
|
}
|
|
1063
1132
|
if (action === "set default") {
|
|
1064
1133
|
const defaults = ensureDefaultColumns(foreignKey.sourceTable, foreignKey.sourceColumns, `Foreign key set default on '${foreignKey.sourceTableName}'`);
|
|
1065
|
-
|
|
1134
|
+
await patchReferencingRows(db, foreignKey.sourceTableName, referencingRows, defaults);
|
|
1066
1135
|
continue;
|
|
1067
1136
|
}
|
|
1068
1137
|
if (action === "cascade") for (const referencingRow of referencingRows) {
|
|
@@ -1130,23 +1199,21 @@ async function applyIncomingForeignKeyActionsOnUpdate(db, table, oldRow, newRow,
|
|
|
1130
1199
|
if (referencingRows.length === 0) continue;
|
|
1131
1200
|
if (action === "set null") {
|
|
1132
1201
|
ensureNullableColumns(foreignKey.sourceTable, foreignKey.sourceColumns, `Foreign key set null on '${foreignKey.sourceTableName}'`);
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
await db.patch(foreignKey.sourceTableName, referencingRow._id, patch);
|
|
1137
|
-
}
|
|
1202
|
+
const nullPatch = {};
|
|
1203
|
+
for (const columnName of foreignKey.sourceColumns) nullPatch[columnName] = null;
|
|
1204
|
+
await patchReferencingRows(db, foreignKey.sourceTableName, referencingRows, nullPatch);
|
|
1138
1205
|
continue;
|
|
1139
1206
|
}
|
|
1140
1207
|
if (action === "set default") {
|
|
1141
1208
|
const defaults = ensureDefaultColumns(foreignKey.sourceTable, foreignKey.sourceColumns, `Foreign key set default on '${foreignKey.sourceTableName}'`);
|
|
1142
|
-
|
|
1209
|
+
await patchReferencingRows(db, foreignKey.sourceTableName, referencingRows, defaults);
|
|
1143
1210
|
continue;
|
|
1144
1211
|
}
|
|
1145
1212
|
if (action === "cascade") {
|
|
1146
1213
|
const patchValues = {};
|
|
1147
1214
|
for (let i = 0; i < foreignKey.sourceColumns.length; i++) patchValues[foreignKey.sourceColumns[i]] = newValues[i];
|
|
1148
1215
|
ensureNonNullValues(foreignKey.sourceTable, patchValues, `Foreign key cascade update on '${foreignKey.sourceTableName}'`);
|
|
1149
|
-
|
|
1216
|
+
await patchReferencingRows(db, foreignKey.sourceTableName, referencingRows, patchValues);
|
|
1150
1217
|
}
|
|
1151
1218
|
}
|
|
1152
1219
|
}
|
|
@@ -3424,6 +3491,54 @@ function createCountBackfillHandlers(schema, getChunkRef) {
|
|
|
3424
3491
|
};
|
|
3425
3492
|
}
|
|
3426
3493
|
|
|
3494
|
+
//#endregion
|
|
3495
|
+
//#region src/orm/query-promise.ts
|
|
3496
|
+
/**
|
|
3497
|
+
* Query Promise - Lazy query execution via Promise interface
|
|
3498
|
+
*
|
|
3499
|
+
* Implements Drizzle's QueryPromise pattern:
|
|
3500
|
+
* - Queries don't execute until awaited or .then() is called
|
|
3501
|
+
* - Implements full Promise interface (then/catch/finally)
|
|
3502
|
+
* - Subclasses provide execute() implementation
|
|
3503
|
+
*
|
|
3504
|
+
* @example
|
|
3505
|
+
* const query = ctx.db.query.users.findMany();
|
|
3506
|
+
* // Query not executed yet
|
|
3507
|
+
* const users = await query; // Now executed
|
|
3508
|
+
*/
|
|
3509
|
+
/**
|
|
3510
|
+
* Abstract base class for promise-based query execution
|
|
3511
|
+
*
|
|
3512
|
+
* @template T - The result type returned by the query
|
|
3513
|
+
*
|
|
3514
|
+
* Pattern from Drizzle ORM: query-promise.ts:27-31
|
|
3515
|
+
*/
|
|
3516
|
+
var QueryPromise = class {
|
|
3517
|
+
/**
|
|
3518
|
+
* Promise tag for debugging and type identification
|
|
3519
|
+
*/
|
|
3520
|
+
[Symbol.toStringTag] = "QueryPromise";
|
|
3521
|
+
/**
|
|
3522
|
+
* Promise.then() implementation - delegates to execute()
|
|
3523
|
+
* This enables lazy evaluation: queries only run when awaited
|
|
3524
|
+
*/
|
|
3525
|
+
then(onFulfilled, onRejected) {
|
|
3526
|
+
return this.execute().then(onFulfilled, onRejected);
|
|
3527
|
+
}
|
|
3528
|
+
/**
|
|
3529
|
+
* Promise.catch() implementation - delegates to execute()
|
|
3530
|
+
*/
|
|
3531
|
+
catch(onRejected) {
|
|
3532
|
+
return this.execute().catch(onRejected);
|
|
3533
|
+
}
|
|
3534
|
+
/**
|
|
3535
|
+
* Promise.finally() implementation - delegates to execute()
|
|
3536
|
+
*/
|
|
3537
|
+
finally(onFinally) {
|
|
3538
|
+
return this.execute().finally(onFinally);
|
|
3539
|
+
}
|
|
3540
|
+
};
|
|
3541
|
+
|
|
3427
3542
|
//#endregion
|
|
3428
3543
|
//#region src/orm/errors.ts
|
|
3429
3544
|
var OrmNotFoundError = class extends Error {
|
|
@@ -3522,54 +3637,6 @@ async function* streamQuery(ctx, request) {
|
|
|
3522
3637
|
}
|
|
3523
3638
|
const DEFAULT_TARGET_MAX_ROWS = 100;
|
|
3524
3639
|
|
|
3525
|
-
//#endregion
|
|
3526
|
-
//#region src/orm/query-promise.ts
|
|
3527
|
-
/**
|
|
3528
|
-
* Query Promise - Lazy query execution via Promise interface
|
|
3529
|
-
*
|
|
3530
|
-
* Implements Drizzle's QueryPromise pattern:
|
|
3531
|
-
* - Queries don't execute until awaited or .then() is called
|
|
3532
|
-
* - Implements full Promise interface (then/catch/finally)
|
|
3533
|
-
* - Subclasses provide execute() implementation
|
|
3534
|
-
*
|
|
3535
|
-
* @example
|
|
3536
|
-
* const query = ctx.db.query.users.findMany();
|
|
3537
|
-
* // Query not executed yet
|
|
3538
|
-
* const users = await query; // Now executed
|
|
3539
|
-
*/
|
|
3540
|
-
/**
|
|
3541
|
-
* Abstract base class for promise-based query execution
|
|
3542
|
-
*
|
|
3543
|
-
* @template T - The result type returned by the query
|
|
3544
|
-
*
|
|
3545
|
-
* Pattern from Drizzle ORM: query-promise.ts:27-31
|
|
3546
|
-
*/
|
|
3547
|
-
var QueryPromise = class {
|
|
3548
|
-
/**
|
|
3549
|
-
* Promise tag for debugging and type identification
|
|
3550
|
-
*/
|
|
3551
|
-
[Symbol.toStringTag] = "QueryPromise";
|
|
3552
|
-
/**
|
|
3553
|
-
* Promise.then() implementation - delegates to execute()
|
|
3554
|
-
* This enables lazy evaluation: queries only run when awaited
|
|
3555
|
-
*/
|
|
3556
|
-
then(onFulfilled, onRejected) {
|
|
3557
|
-
return this.execute().then(onFulfilled, onRejected);
|
|
3558
|
-
}
|
|
3559
|
-
/**
|
|
3560
|
-
* Promise.catch() implementation - delegates to execute()
|
|
3561
|
-
*/
|
|
3562
|
-
catch(onRejected) {
|
|
3563
|
-
return this.execute().catch(onRejected);
|
|
3564
|
-
}
|
|
3565
|
-
/**
|
|
3566
|
-
* Promise.finally() implementation - delegates to execute()
|
|
3567
|
-
*/
|
|
3568
|
-
finally(onFinally) {
|
|
3569
|
-
return this.execute().finally(onFinally);
|
|
3570
|
-
}
|
|
3571
|
-
};
|
|
3572
|
-
|
|
3573
3640
|
//#endregion
|
|
3574
3641
|
//#region src/orm/rls/roles.ts
|
|
3575
3642
|
var RlsRole = class {
|
|
@@ -3605,6 +3672,12 @@ function isRlsRole(value) {
|
|
|
3605
3672
|
|
|
3606
3673
|
//#endregion
|
|
3607
3674
|
//#region src/orm/rls/evaluator.ts
|
|
3675
|
+
function createRlsPolicyResolutionCache() {
|
|
3676
|
+
return {
|
|
3677
|
+
applicable: /* @__PURE__ */ new WeakMap(),
|
|
3678
|
+
expressions: /* @__PURE__ */ new WeakMap()
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3608
3681
|
function isRlsEnabled(table) {
|
|
3609
3682
|
return Boolean(table[EnableRLS] || getRlsPolicies(table).length > 0);
|
|
3610
3683
|
}
|
|
@@ -3690,22 +3763,52 @@ async function resolveExpression(policy, checkType, ctx, table) {
|
|
|
3690
3763
|
if (typeof candidate === "function") return await candidate(ctx, table);
|
|
3691
3764
|
return candidate;
|
|
3692
3765
|
}
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3766
|
+
/**
|
|
3767
|
+
* The applicable-policy set depends only on `(table, operation, rls)`, never on
|
|
3768
|
+
* a row, so it is computed once per cache. `assertRlsRolesResolvable` is folded
|
|
3769
|
+
* in to keep the fail-closed ordering: configuration errors still throw before
|
|
3770
|
+
* any policy expression is resolved.
|
|
3771
|
+
*/
|
|
3772
|
+
function getApplicablePolicies(table, operation, rls, cache) {
|
|
3773
|
+
const byOperation = cache.applicable.get(table);
|
|
3774
|
+
const cached = byOperation?.get(operation);
|
|
3775
|
+
if (cached) return cached;
|
|
3696
3776
|
assertRlsRolesResolvable({
|
|
3697
3777
|
table,
|
|
3698
3778
|
operation,
|
|
3699
3779
|
rls
|
|
3700
3780
|
});
|
|
3701
|
-
const ctx = rls?.ctx ?? {};
|
|
3702
3781
|
const policies = getRlsPolicies(table).filter((policy) => policyApplies(policy, operation) && roleMatches(policy, rls));
|
|
3782
|
+
if (byOperation) byOperation.set(operation, policies);
|
|
3783
|
+
else cache.applicable.set(table, new Map([[operation, policies]]));
|
|
3784
|
+
return policies;
|
|
3785
|
+
}
|
|
3786
|
+
/**
|
|
3787
|
+
* Resolved lazily and per policy, so the permissive short-circuit still means a
|
|
3788
|
+
* later policy's callback is never invoked. The promise (not the value) is
|
|
3789
|
+
* memoized so concurrent rows cannot double-invoke a user callback.
|
|
3790
|
+
*/
|
|
3791
|
+
function resolveExpressionCached(policy, checkType, ctx, table, cache) {
|
|
3792
|
+
const byCheckType = cache.expressions.get(policy);
|
|
3793
|
+
const cached = byCheckType?.get(checkType);
|
|
3794
|
+
if (cached) return cached;
|
|
3795
|
+
const pending = resolveExpression(policy, checkType, ctx, table);
|
|
3796
|
+
if (byCheckType) byCheckType.set(checkType, pending);
|
|
3797
|
+
else cache.expressions.set(policy, new Map([[checkType, pending]]));
|
|
3798
|
+
return pending;
|
|
3799
|
+
}
|
|
3800
|
+
async function evaluatePolicySet({ table, operation, checkType, row, rls, cache }) {
|
|
3801
|
+
if (!isRlsEnabled(table)) return true;
|
|
3802
|
+
if (rls?.mode === "skip") return true;
|
|
3803
|
+
const resolution = cache ?? createRlsPolicyResolutionCache();
|
|
3804
|
+
const ctx = rls?.ctx ?? {};
|
|
3805
|
+
const policies = getApplicablePolicies(table, operation, rls, resolution);
|
|
3703
3806
|
if (policies.length === 0) return false;
|
|
3704
3807
|
const permissive = policies.filter((policy) => (policy.as ?? "permissive") !== "restrictive");
|
|
3705
3808
|
if (permissive.length === 0) return false;
|
|
3706
3809
|
let permissivePasses = false;
|
|
3707
3810
|
for (const policy of permissive) {
|
|
3708
|
-
const expression = await
|
|
3811
|
+
const expression = await resolveExpressionCached(policy, checkType, ctx, table, resolution);
|
|
3709
3812
|
if (!expression || policyExpressionPasses(row, expression)) {
|
|
3710
3813
|
permissivePasses = true;
|
|
3711
3814
|
break;
|
|
@@ -3714,7 +3817,7 @@ async function evaluatePolicySet({ table, operation, checkType, row, rls }) {
|
|
|
3714
3817
|
if (!permissivePasses) return false;
|
|
3715
3818
|
const restrictive = policies.filter((policy) => (policy.as ?? "permissive") === "restrictive");
|
|
3716
3819
|
for (const policy of restrictive) {
|
|
3717
|
-
const expression = await
|
|
3820
|
+
const expression = await resolveExpressionCached(policy, checkType, ctx, table, resolution);
|
|
3718
3821
|
if (!expression) continue;
|
|
3719
3822
|
if (!policyExpressionPasses(row, expression)) return false;
|
|
3720
3823
|
}
|
|
@@ -3726,7 +3829,8 @@ async function canSelectRow(options) {
|
|
|
3726
3829
|
operation: "select",
|
|
3727
3830
|
checkType: "using",
|
|
3728
3831
|
row: options.row,
|
|
3729
|
-
rls: options.rls
|
|
3832
|
+
rls: options.rls,
|
|
3833
|
+
cache: options.cache
|
|
3730
3834
|
});
|
|
3731
3835
|
}
|
|
3732
3836
|
async function canInsertRow(options) {
|
|
@@ -3735,7 +3839,8 @@ async function canInsertRow(options) {
|
|
|
3735
3839
|
operation: "insert",
|
|
3736
3840
|
checkType: "withCheck",
|
|
3737
3841
|
row: options.row,
|
|
3738
|
-
rls: options.rls
|
|
3842
|
+
rls: options.rls,
|
|
3843
|
+
cache: options.cache
|
|
3739
3844
|
});
|
|
3740
3845
|
}
|
|
3741
3846
|
async function canDeleteRow(options) {
|
|
@@ -3744,7 +3849,8 @@ async function canDeleteRow(options) {
|
|
|
3744
3849
|
operation: "delete",
|
|
3745
3850
|
checkType: "using",
|
|
3746
3851
|
row: options.row,
|
|
3747
|
-
rls: options.rls
|
|
3852
|
+
rls: options.rls,
|
|
3853
|
+
cache: options.cache
|
|
3748
3854
|
});
|
|
3749
3855
|
}
|
|
3750
3856
|
async function evaluateUpdateDecision(options) {
|
|
@@ -3753,14 +3859,16 @@ async function evaluateUpdateDecision(options) {
|
|
|
3753
3859
|
operation: "update",
|
|
3754
3860
|
checkType: "using",
|
|
3755
3861
|
row: options.existingRow,
|
|
3756
|
-
rls: options.rls
|
|
3862
|
+
rls: options.rls,
|
|
3863
|
+
cache: options.cache
|
|
3757
3864
|
});
|
|
3758
3865
|
const withCheckAllowed = await evaluatePolicySet({
|
|
3759
3866
|
table: options.table,
|
|
3760
3867
|
operation: "update",
|
|
3761
3868
|
checkType: "withCheck",
|
|
3762
3869
|
row: options.updatedRow,
|
|
3763
|
-
rls: options.rls
|
|
3870
|
+
rls: options.rls,
|
|
3871
|
+
cache: options.cache
|
|
3764
3872
|
});
|
|
3765
3873
|
return {
|
|
3766
3874
|
allowed: usingAllowed && withCheckAllowed,
|
|
@@ -3776,8 +3884,10 @@ async function filterSelectRows(options) {
|
|
|
3776
3884
|
operation: "select",
|
|
3777
3885
|
rls: options.rls
|
|
3778
3886
|
});
|
|
3887
|
+
const cache = options.cache ?? createRlsPolicyResolutionCache();
|
|
3779
3888
|
const rows = [];
|
|
3780
3889
|
for (const row of options.rows) if (await canSelectRow({
|
|
3890
|
+
cache,
|
|
3781
3891
|
table: options.table,
|
|
3782
3892
|
row,
|
|
3783
3893
|
rls: options.rls
|
|
@@ -4462,11 +4572,37 @@ var GelRankQuery = class {
|
|
|
4462
4572
|
*
|
|
4463
4573
|
* Pattern from Drizzle: gel-core/query-builders/query.ts:32-62
|
|
4464
4574
|
*/
|
|
4465
|
-
var GelRelationalQuery = class extends QueryPromise {
|
|
4575
|
+
var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
4466
4576
|
allowFullScan;
|
|
4467
|
-
|
|
4577
|
+
/**
|
|
4578
|
+
* Set synchronously by the first `execute()`. Later executions run on a fresh
|
|
4579
|
+
* instance instead, which is what keeps execution-scoped state from leaking
|
|
4580
|
+
* between two awaits of the same query object.
|
|
4581
|
+
*/
|
|
4582
|
+
_executionClaimed = false;
|
|
4583
|
+
/**
|
|
4584
|
+
* Scoped to one execution, because `_executionClaimed` diverts every later
|
|
4585
|
+
* execution to its own instance. Within a run, every `_applyRlsSelectFilter`
|
|
4586
|
+
* call shares it, including the streaming sites that pass a single row at a
|
|
4587
|
+
* time — those would otherwise re-resolve the whole policy set per row.
|
|
4588
|
+
*
|
|
4589
|
+
* SECURITY: a resolved policy expression can embed database state that a
|
|
4590
|
+
* later write invalidates, so it must never outlive the execution that
|
|
4591
|
+
* resolved it.
|
|
4592
|
+
*/
|
|
4593
|
+
_rlsPolicyResolution = createRlsPolicyResolutionCache();
|
|
4594
|
+
/**
|
|
4595
|
+
* Assigned in the constructor so callers that run many short-lived query
|
|
4596
|
+
* instances against the same tables (mutation `returning({ _count })`) can
|
|
4597
|
+
* share one readiness memo instead of re-probing per instance.
|
|
4598
|
+
*/
|
|
4599
|
+
_countIndexReadinessByKey;
|
|
4600
|
+
/**
|
|
4601
|
+
* Index readiness is a probe memo, not execution state, so `_forExecution`
|
|
4602
|
+
* hands it to the next run rather than re-probing.
|
|
4603
|
+
*/
|
|
4468
4604
|
_aggregateIndexReadinessByKey = /* @__PURE__ */ new Map();
|
|
4469
|
-
constructor(schema, tableConfig, edgeMetadata, db, config, mode, _allEdges, rls, relationLoading, vectorSearchProvider, configuredIndex) {
|
|
4605
|
+
constructor(schema, tableConfig, edgeMetadata, db, config, mode, _allEdges, rls, relationLoading, vectorSearchProvider, configuredIndex, countIndexReadiness) {
|
|
4470
4606
|
super();
|
|
4471
4607
|
this.schema = schema;
|
|
4472
4608
|
this.tableConfig = tableConfig;
|
|
@@ -4480,6 +4616,7 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
4480
4616
|
this.vectorSearchProvider = vectorSearchProvider;
|
|
4481
4617
|
this.configuredIndex = configuredIndex;
|
|
4482
4618
|
this.allowFullScan = config.allowFullScan === true;
|
|
4619
|
+
this._countIndexReadinessByKey = countIndexReadiness ?? /* @__PURE__ */ new Map();
|
|
4483
4620
|
}
|
|
4484
4621
|
_usesSystemCreatedAtAlias(tableConfig = this.tableConfig) {
|
|
4485
4622
|
return usesSystemCreatedAtAlias(tableConfig.table);
|
|
@@ -4558,6 +4695,7 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
4558
4695
|
async _applyRlsSelectFilter(rows, tableConfig) {
|
|
4559
4696
|
if (!tableConfig) return rows;
|
|
4560
4697
|
return await filterSelectRows({
|
|
4698
|
+
cache: this._rlsPolicyResolution,
|
|
4561
4699
|
table: tableConfig.table,
|
|
4562
4700
|
rows,
|
|
4563
4701
|
rls: this.rls
|
|
@@ -6567,10 +6705,21 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
6567
6705
|
return rows;
|
|
6568
6706
|
}
|
|
6569
6707
|
/**
|
|
6708
|
+
* A second instance of the same query: identical configuration, its own
|
|
6709
|
+
* execution-scoped state, the same index-readiness memos.
|
|
6710
|
+
*/
|
|
6711
|
+
_forExecution() {
|
|
6712
|
+
const next = new GelRelationalQuery(this.schema, this.tableConfig, this.edgeMetadata, this.db, this.config, this.mode, this._allEdges, this.rls, this.relationLoading, this.vectorSearchProvider, this.configuredIndex, this._countIndexReadinessByKey);
|
|
6713
|
+
next._aggregateIndexReadinessByKey = this._aggregateIndexReadinessByKey;
|
|
6714
|
+
return next;
|
|
6715
|
+
}
|
|
6716
|
+
/**
|
|
6570
6717
|
* Execute the query and return results
|
|
6571
6718
|
* Phase 4 implementation with WhereClauseCompiler integration
|
|
6572
6719
|
*/
|
|
6573
6720
|
async execute() {
|
|
6721
|
+
if (this._executionClaimed) return this._forExecution().execute();
|
|
6722
|
+
this._executionClaimed = true;
|
|
6574
6723
|
const config = this.config;
|
|
6575
6724
|
if (this.mode === "count") return await this._executeCount(config);
|
|
6576
6725
|
if (this.mode === "aggregate") return await this._executeAggregate(config);
|
|
@@ -7295,21 +7444,8 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
7295
7444
|
if (!this.allowFullScan) throw new Error(`${baseMessage} Set allowFullScan: true, reduce fan-out, or increase defineSchema(..., { defaults: { relationFanOutMaxKeys } }).`);
|
|
7296
7445
|
if (options.tableConfig.strict !== false) console.warn(`${baseMessage} Continuing because allowFullScan: true.`);
|
|
7297
7446
|
}
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
const limit = Math.min(this._getRelationConcurrency(), items.length);
|
|
7301
|
-
const results = new Array(items.length);
|
|
7302
|
-
let nextIndex = 0;
|
|
7303
|
-
const runWorker = async () => {
|
|
7304
|
-
while (true) {
|
|
7305
|
-
const index = nextIndex;
|
|
7306
|
-
nextIndex += 1;
|
|
7307
|
-
if (index >= items.length) return;
|
|
7308
|
-
results[index] = await worker(items[index], index);
|
|
7309
|
-
}
|
|
7310
|
-
};
|
|
7311
|
-
await Promise.all(Array.from({ length: limit }, () => runWorker()));
|
|
7312
|
-
return results;
|
|
7447
|
+
_mapWithConcurrency(items, worker) {
|
|
7448
|
+
return mapWithConcurrency(items, this._getRelationConcurrency(), worker);
|
|
7313
7449
|
}
|
|
7314
7450
|
/**
|
|
7315
7451
|
* Load relations for query results
|
|
@@ -7772,7 +7908,16 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
7772
7908
|
if (key in tableColumns) throw new Error(`extras.${key} conflicts with a column on table '${tableName}'.`);
|
|
7773
7909
|
if (withConfig && key in withConfig) throw new Error(`extras.${key} conflicts with a relation on table '${tableName}'.`);
|
|
7774
7910
|
}
|
|
7775
|
-
for (const row of rows)
|
|
7911
|
+
for (const row of rows) {
|
|
7912
|
+
let publicRow;
|
|
7913
|
+
for (const [key, definition] of entries) {
|
|
7914
|
+
if (typeof definition === "function") {
|
|
7915
|
+
publicRow ??= this._toPublicRow(row, tableConfig);
|
|
7916
|
+
row[key] = definition(publicRow);
|
|
7917
|
+
} else row[key] = definition;
|
|
7918
|
+
if (publicRow) publicRow[key] = row[key];
|
|
7919
|
+
}
|
|
7920
|
+
}
|
|
7776
7921
|
return rows;
|
|
7777
7922
|
}
|
|
7778
7923
|
/**
|
|
@@ -7810,6 +7955,35 @@ var GelRelationalQuery = class extends QueryPromise {
|
|
|
7810
7955
|
}
|
|
7811
7956
|
};
|
|
7812
7957
|
|
|
7958
|
+
//#endregion
|
|
7959
|
+
//#region src/orm/returning-count.ts
|
|
7960
|
+
/**
|
|
7961
|
+
* Builds the `returning({ _count })` reader once per mutation statement.
|
|
7962
|
+
*
|
|
7963
|
+
* The table config lookup and edge filter are invariant across rows, and — more
|
|
7964
|
+
* importantly — the aggregate-index readiness memo lives on the query instance.
|
|
7965
|
+
* Constructing a fresh `GelRelationalQuery` per row discarded it, so the
|
|
7966
|
+
* readiness probe ran a real indexed collect for every row x counted relation
|
|
7967
|
+
* instead of once per (table, index).
|
|
7968
|
+
*/
|
|
7969
|
+
function createReturningCountLoader(db, table, ormContext) {
|
|
7970
|
+
const schema = ormContext?.schema;
|
|
7971
|
+
const edgeMetadata = ormContext?.edgeMetadata;
|
|
7972
|
+
if (!schema || !edgeMetadata) throw new Error("returning({ _count }) requires orm.db(ctx) configured from createOrm({ schema, ... }).");
|
|
7973
|
+
const tableName = getTableName(table);
|
|
7974
|
+
const tableConfig = Object.values(schema).find((config) => config.name === tableName);
|
|
7975
|
+
if (!tableConfig) throw new Error(`Table config for '${tableName}' is not registered.`);
|
|
7976
|
+
const tableEdges = edgeMetadata.filter((edge) => edge.sourceTable === tableName);
|
|
7977
|
+
const countIndexReadiness = /* @__PURE__ */ new Map();
|
|
7978
|
+
return { async load(row, countSelection) {
|
|
7979
|
+
return (await new GelRelationalQuery(schema, tableConfig, tableEdges, db, {
|
|
7980
|
+
where: { id: row._id },
|
|
7981
|
+
columns: {},
|
|
7982
|
+
with: { _count: countSelection }
|
|
7983
|
+
}, "first", edgeMetadata, ormContext?.rls, ormContext?.relationLoading, void 0, void 0, countIndexReadiness).execute())?._count ?? {};
|
|
7984
|
+
} };
|
|
7985
|
+
}
|
|
7986
|
+
|
|
7813
7987
|
//#endregion
|
|
7814
7988
|
//#region src/orm/delete.ts
|
|
7815
7989
|
const applyIndexFilter$1 = (query, filter) => {
|
|
@@ -7834,18 +8008,14 @@ var ConvexDeleteBuilder = class extends QueryPromise {
|
|
|
7834
8008
|
scheduledDelayMs;
|
|
7835
8009
|
executionModeOverride;
|
|
7836
8010
|
paginateConfig;
|
|
8011
|
+
_returningCountLoader;
|
|
8012
|
+
/**
|
|
8013
|
+
* One loader per statement: the table config, edge filter and aggregate-index
|
|
8014
|
+
* readiness memo are invariant across the affected rows.
|
|
8015
|
+
*/
|
|
7837
8016
|
async _loadReturningCount(row, countSelection, ormContext) {
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
if (!schema || !edgeMetadata) throw new Error("returning({ _count }) requires orm.db(ctx) configured from createOrm({ schema, ... }).");
|
|
7841
|
-
const tableName = getTableName(this.table);
|
|
7842
|
-
const tableConfig = Object.values(schema).find((config) => config.name === tableName);
|
|
7843
|
-
if (!tableConfig) throw new Error(`Table config for '${tableName}' is not registered.`);
|
|
7844
|
-
return (await new GelRelationalQuery(schema, tableConfig, edgeMetadata.filter((edge) => edge.sourceTable === tableName), this.db, {
|
|
7845
|
-
where: { id: row._id },
|
|
7846
|
-
columns: {},
|
|
7847
|
-
with: { _count: countSelection }
|
|
7848
|
-
}, "first", edgeMetadata, ormContext?.rls, ormContext?.relationLoading).execute())?._count ?? {};
|
|
8017
|
+
this._returningCountLoader ??= createReturningCountLoader(this.db, this.table, ormContext);
|
|
8018
|
+
return await this._returningCountLoader.load(row, countSelection);
|
|
7849
8019
|
}
|
|
7850
8020
|
constructor(db, table) {
|
|
7851
8021
|
super();
|
|
@@ -8079,6 +8249,7 @@ var ConvexDeleteBuilder = class extends QueryPromise {
|
|
|
8079
8249
|
const fkBatchSize = isPaginated ? pagination.limit : batchSize;
|
|
8080
8250
|
for (const row of rows) {
|
|
8081
8251
|
if (!await canDeleteRow({
|
|
8252
|
+
cache: createRlsPolicyResolutionCache(),
|
|
8082
8253
|
table: this.table,
|
|
8083
8254
|
row,
|
|
8084
8255
|
rls
|
|
@@ -8151,18 +8322,14 @@ var ConvexInsertBuilder = class extends QueryPromise {
|
|
|
8151
8322
|
returningFields;
|
|
8152
8323
|
conflictConfig;
|
|
8153
8324
|
allowFullScanFlag = false;
|
|
8325
|
+
_returningCountLoader;
|
|
8326
|
+
/**
|
|
8327
|
+
* One loader per statement: the table config, edge filter and aggregate-index
|
|
8328
|
+
* readiness memo are invariant across the affected rows.
|
|
8329
|
+
*/
|
|
8154
8330
|
async _loadReturningCount(row, countSelection, ormContext) {
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
if (!schema || !edgeMetadata) throw new Error("returning({ _count }) requires orm.db(ctx) configured from createOrm({ schema, ... }).");
|
|
8158
|
-
const tableName = getTableName(this.table);
|
|
8159
|
-
const tableConfig = Object.values(schema).find((config) => config.name === tableName);
|
|
8160
|
-
if (!tableConfig) throw new Error(`Table config for '${tableName}' is not registered.`);
|
|
8161
|
-
return (await new GelRelationalQuery(schema, tableConfig, edgeMetadata.filter((edge) => edge.sourceTable === tableName), this.db, {
|
|
8162
|
-
where: { id: row._id },
|
|
8163
|
-
columns: {},
|
|
8164
|
-
with: { _count: countSelection }
|
|
8165
|
-
}, "first", edgeMetadata, ormContext?.rls, ormContext?.relationLoading).execute())?._count ?? {};
|
|
8331
|
+
this._returningCountLoader ??= createReturningCountLoader(this.db, this.table, ormContext);
|
|
8332
|
+
return await this._returningCountLoader.load(row, countSelection);
|
|
8166
8333
|
}
|
|
8167
8334
|
constructor(db, table) {
|
|
8168
8335
|
super();
|
|
@@ -8207,12 +8374,14 @@ var ConvexInsertBuilder = class extends QueryPromise {
|
|
|
8207
8374
|
enforcePolymorphicWrite(this.table, preparedValue);
|
|
8208
8375
|
const rls = ormContext?.rls;
|
|
8209
8376
|
const tableName = getTableName(this.table);
|
|
8377
|
+
const rlsResolution = createRlsPolicyResolutionCache();
|
|
8210
8378
|
if (!await canInsertRow({
|
|
8379
|
+
cache: rlsResolution,
|
|
8211
8380
|
table: this.table,
|
|
8212
8381
|
row: preparedValue,
|
|
8213
8382
|
rls
|
|
8214
8383
|
})) throw new Error(`RLS policy violation for insert on table "${tableName}"`);
|
|
8215
|
-
const conflictResult = await this.handleConflict(preparedValue);
|
|
8384
|
+
const conflictResult = await this.handleConflict(preparedValue, rlsResolution);
|
|
8216
8385
|
if (conflictResult?.status === "skip") continue;
|
|
8217
8386
|
if (conflictResult?.status === "updated") {
|
|
8218
8387
|
if (conflictResult.row && this.returningFields) results.push(await this.resolveReturningRow(conflictResult.row, returningSelection, ormContext));
|
|
@@ -8235,7 +8404,7 @@ var ConvexInsertBuilder = class extends QueryPromise {
|
|
|
8235
8404
|
if (returningSelection?.countSelection) selected._count = await this._loadReturningCount(row, returningSelection.countSelection, ormContext);
|
|
8236
8405
|
return selected;
|
|
8237
8406
|
}
|
|
8238
|
-
async handleConflict(value) {
|
|
8407
|
+
async handleConflict(value, rlsResolution) {
|
|
8239
8408
|
if (!this.conflictConfig) return;
|
|
8240
8409
|
const { action, config } = this.conflictConfig;
|
|
8241
8410
|
const targetColumns = Array.isArray(config.target) ? config.target : config.target ? [config.target] : [];
|
|
@@ -8280,6 +8449,7 @@ var ConvexInsertBuilder = class extends QueryPromise {
|
|
|
8280
8449
|
};
|
|
8281
8450
|
const writeSet = normalizeDateFieldsForWrite(this.table, effectiveSet);
|
|
8282
8451
|
const updateDecision = await evaluateUpdateDecision({
|
|
8452
|
+
cache: rlsResolution,
|
|
8283
8453
|
table: this.table,
|
|
8284
8454
|
existingRow: existing,
|
|
8285
8455
|
updatedRow: {
|
|
@@ -8455,9 +8625,14 @@ const getOrmLifecycleInnerDb = (db) => {
|
|
|
8455
8625
|
return inner;
|
|
8456
8626
|
};
|
|
8457
8627
|
const isBeforeDataResult = (value) => typeof value === "object" && value !== null && "data" in value;
|
|
8628
|
+
/**
|
|
8629
|
+
* FIFO mutex. A re-checking spin over one shared promise wakes every waiter on
|
|
8630
|
+
* each release, which costs n(n-1)/2 resumptions for n contenders; handing the
|
|
8631
|
+
* lock to exactly one queued waiter costs n-1.
|
|
8632
|
+
*/
|
|
8458
8633
|
var Lock = class {
|
|
8459
|
-
|
|
8460
|
-
|
|
8634
|
+
locked = false;
|
|
8635
|
+
waiters = [];
|
|
8461
8636
|
async withLock(fn) {
|
|
8462
8637
|
const unlock = await this.acquire();
|
|
8463
8638
|
try {
|
|
@@ -8467,15 +8642,20 @@ var Lock = class {
|
|
|
8467
8642
|
}
|
|
8468
8643
|
}
|
|
8469
8644
|
async acquire() {
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
this.promise = new Promise((res) => {
|
|
8473
|
-
resolve = res;
|
|
8645
|
+
if (this.locked) await new Promise((resolve) => {
|
|
8646
|
+
this.waiters.push(resolve);
|
|
8474
8647
|
});
|
|
8475
|
-
this.
|
|
8648
|
+
this.locked = true;
|
|
8649
|
+
let released = false;
|
|
8476
8650
|
return () => {
|
|
8477
|
-
|
|
8478
|
-
|
|
8651
|
+
if (released) return;
|
|
8652
|
+
released = true;
|
|
8653
|
+
const next = this.waiters.shift();
|
|
8654
|
+
if (next) {
|
|
8655
|
+
next();
|
|
8656
|
+
return;
|
|
8657
|
+
}
|
|
8658
|
+
this.locked = false;
|
|
8479
8659
|
};
|
|
8480
8660
|
}
|
|
8481
8661
|
};
|
|
@@ -8502,6 +8682,69 @@ const withPublicIdAlias = (doc, id) => doc.id !== void 0 ? doc : {
|
|
|
8502
8682
|
...doc,
|
|
8503
8683
|
id
|
|
8504
8684
|
};
|
|
8685
|
+
const isPlainRecord = (value) => {
|
|
8686
|
+
if (typeof value !== "object" || value === null) return false;
|
|
8687
|
+
const prototype = Object.getPrototypeOf(value);
|
|
8688
|
+
return prototype === Object.prototype || prototype === null;
|
|
8689
|
+
};
|
|
8690
|
+
/**
|
|
8691
|
+
* Convex value serialization drops `undefined` at any depth below the top
|
|
8692
|
+
* level, so a document read back from storage never carries a nested
|
|
8693
|
+
* `undefined` key. Mirror that when deriving a document locally, otherwise
|
|
8694
|
+
* `Object.keys(newDoc)` diverges from what a hook would observe.
|
|
8695
|
+
*/
|
|
8696
|
+
const stripUndefinedDeep = (value) => {
|
|
8697
|
+
if (Array.isArray(value)) {
|
|
8698
|
+
let changed = false;
|
|
8699
|
+
const next = value.map((entry) => {
|
|
8700
|
+
const stripped = stripUndefinedDeep(entry);
|
|
8701
|
+
if (stripped !== entry) changed = true;
|
|
8702
|
+
return stripped;
|
|
8703
|
+
});
|
|
8704
|
+
return changed ? next : value;
|
|
8705
|
+
}
|
|
8706
|
+
if (!isPlainRecord(value)) return value;
|
|
8707
|
+
let changed = false;
|
|
8708
|
+
const next = {};
|
|
8709
|
+
for (const key of Object.keys(value)) {
|
|
8710
|
+
const nested = value[key];
|
|
8711
|
+
if (nested === void 0) {
|
|
8712
|
+
changed = true;
|
|
8713
|
+
continue;
|
|
8714
|
+
}
|
|
8715
|
+
const stripped = stripUndefinedDeep(nested);
|
|
8716
|
+
if (stripped !== nested) changed = true;
|
|
8717
|
+
next[key] = stripped;
|
|
8718
|
+
}
|
|
8719
|
+
return changed ? next : value;
|
|
8720
|
+
};
|
|
8721
|
+
/**
|
|
8722
|
+
* Reproduce Convex `1.0/shallowMerge`: a top-level shallow merge where a
|
|
8723
|
+
* top-level `undefined` removes the key. The stored document is fully
|
|
8724
|
+
* determined by the document we already read plus the payload we are about to
|
|
8725
|
+
* write, so re-reading it after the patch buys nothing.
|
|
8726
|
+
*/
|
|
8727
|
+
const applyPatchLocally = (oldDoc, payload) => {
|
|
8728
|
+
const payloadKeys = Object.keys(payload);
|
|
8729
|
+
const removed = /* @__PURE__ */ new Set();
|
|
8730
|
+
for (const key of payloadKeys) if (payload[key] === void 0) removed.add(key);
|
|
8731
|
+
const newDoc = {};
|
|
8732
|
+
for (const key of Object.keys(oldDoc)) {
|
|
8733
|
+
if (removed.has(key)) continue;
|
|
8734
|
+
newDoc[key] = oldDoc[key];
|
|
8735
|
+
}
|
|
8736
|
+
for (const key of payloadKeys) {
|
|
8737
|
+
if (removed.has(key)) continue;
|
|
8738
|
+
newDoc[key] = stripUndefinedDeep(payload[key]);
|
|
8739
|
+
}
|
|
8740
|
+
return newDoc;
|
|
8741
|
+
};
|
|
8742
|
+
/** Convex owns these; a replace payload can never carry them forward. */
|
|
8743
|
+
const applyReplaceLocally = (oldDoc, payload) => ({
|
|
8744
|
+
...stripUndefinedDeep(payload),
|
|
8745
|
+
_id: oldDoc._id,
|
|
8746
|
+
_creationTime: oldDoc._creationTime
|
|
8747
|
+
});
|
|
8505
8748
|
const tableNameFromId = (db, hooksByTable, id) => {
|
|
8506
8749
|
for (const tableName of hooksByTable.keys()) if (db.normalizeId(tableName, id)) return tableName;
|
|
8507
8750
|
return null;
|
|
@@ -8548,18 +8791,16 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
|
|
|
8548
8791
|
const tableHooks = hooksByTable.get(tableName);
|
|
8549
8792
|
if (!tableHooks) return innerDb.patch(tableName, id, value);
|
|
8550
8793
|
return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
|
|
8551
|
-
const
|
|
8794
|
+
const needsDocuments = Boolean(tableHooks.update?.after || tableHooks.change);
|
|
8795
|
+
const oldDoc = needsDocuments ? await innerDb.get(tableName, id) : null;
|
|
8552
8796
|
const updatePayload = await mergeBeforeData(tableName, "update", tableHooks.update?.before, value, hookCtx);
|
|
8797
|
+
const currentDoc = needsDocuments && tableHooks.update?.before ? await innerDb.get(tableName, id) : oldDoc;
|
|
8553
8798
|
await innerDb.patch(tableName, id, updatePayload);
|
|
8554
8799
|
if (!oldDoc) return {
|
|
8555
8800
|
result: void 0,
|
|
8556
8801
|
queuedHooks: []
|
|
8557
8802
|
};
|
|
8558
|
-
const newDoc =
|
|
8559
|
-
if (!newDoc) return {
|
|
8560
|
-
result: void 0,
|
|
8561
|
-
queuedHooks: []
|
|
8562
|
-
};
|
|
8803
|
+
const newDoc = applyPatchLocally(currentDoc ?? oldDoc, updatePayload);
|
|
8563
8804
|
const oldDocWithId = withPublicIdAlias(oldDoc, id);
|
|
8564
8805
|
const newDocWithId = withPublicIdAlias(newDoc, id);
|
|
8565
8806
|
const change = {
|
|
@@ -8595,18 +8836,14 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
|
|
|
8595
8836
|
const tableHooks = hooksByTable.get(tableName);
|
|
8596
8837
|
if (!tableHooks) return innerDb.replace(tableName, id, value);
|
|
8597
8838
|
return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
|
|
8598
|
-
const oldDoc = await innerDb.get(tableName, id);
|
|
8839
|
+
const oldDoc = Boolean(tableHooks.update?.after || tableHooks.change) ? await innerDb.get(tableName, id) : null;
|
|
8599
8840
|
const updatePayload = await mergeBeforeData(tableName, "update", tableHooks.update?.before, value, hookCtx);
|
|
8600
8841
|
await innerDb.replace(tableName, id, updatePayload);
|
|
8601
8842
|
if (!oldDoc) return {
|
|
8602
8843
|
result: void 0,
|
|
8603
8844
|
queuedHooks: []
|
|
8604
8845
|
};
|
|
8605
|
-
const newDoc =
|
|
8606
|
-
if (!newDoc) return {
|
|
8607
|
-
result: void 0,
|
|
8608
|
-
queuedHooks: []
|
|
8609
|
-
};
|
|
8846
|
+
const newDoc = applyReplaceLocally(oldDoc, updatePayload);
|
|
8610
8847
|
const oldDocWithId = withPublicIdAlias(oldDoc, id);
|
|
8611
8848
|
const newDocWithId = withPublicIdAlias(newDoc, id);
|
|
8612
8849
|
const change = {
|
|
@@ -8634,7 +8871,7 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
|
|
|
8634
8871
|
const tableHooks = hooksByTable.get(tableName);
|
|
8635
8872
|
if (!tableHooks) return innerDb.delete(tableName, id);
|
|
8636
8873
|
return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
|
|
8637
|
-
const oldDoc = await innerDb.get(tableName, id);
|
|
8874
|
+
const oldDoc = Boolean(tableHooks.delete || tableHooks.change) ? await innerDb.get(tableName, id) : null;
|
|
8638
8875
|
if (!oldDoc) {
|
|
8639
8876
|
await innerDb.delete(tableName, id);
|
|
8640
8877
|
return {
|
|
@@ -8671,7 +8908,7 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
|
|
|
8671
8908
|
return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
|
|
8672
8909
|
const insertPayload = await mergeBeforeData(table, "create", tableHooks.create?.before, value, hookCtx);
|
|
8673
8910
|
const id = await innerDb.insert(table, insertPayload);
|
|
8674
|
-
const newDoc = await innerDb.get(table, id);
|
|
8911
|
+
const newDoc = Boolean(tableHooks.create?.after || tableHooks.change) ? await innerDb.get(table, id) : null;
|
|
8675
8912
|
if (!newDoc) return {
|
|
8676
8913
|
result: id,
|
|
8677
8914
|
queuedHooks: []
|
|
@@ -8768,11 +9005,13 @@ function createOrmDbLifecycle(schema, triggerDefinitions) {
|
|
|
8768
9005
|
});
|
|
8769
9006
|
}
|
|
8770
9007
|
if (tableHooks.size === 0) return createNoopLifecycle();
|
|
9008
|
+
const hookedTableNames = new Set(tableHooks.keys());
|
|
8771
9009
|
return {
|
|
8772
9010
|
enabled: true,
|
|
8773
9011
|
wrapDB: (ctx) => {
|
|
8774
9012
|
if (!isWriterDb(ctx.db) || isLifecycleWrappedDb(ctx.db)) return ctx;
|
|
8775
9013
|
const wrappedDb = writerWithHooks(ctx, ctx.db, tableHooks, false);
|
|
9014
|
+
markLifecycleHookedTables(wrappedDb, hookedTableNames);
|
|
8776
9015
|
return {
|
|
8777
9016
|
...ctx,
|
|
8778
9017
|
db: markLifecycleWrappedDb(wrappedDb)
|
|
@@ -8977,18 +9216,14 @@ var ConvexUpdateBuilder = class extends QueryPromise {
|
|
|
8977
9216
|
allowFullScanFlag = false;
|
|
8978
9217
|
paginateConfig;
|
|
8979
9218
|
executionModeOverride;
|
|
9219
|
+
_returningCountLoader;
|
|
9220
|
+
/**
|
|
9221
|
+
* One loader per statement: the table config, edge filter and aggregate-index
|
|
9222
|
+
* readiness memo are invariant across the affected rows.
|
|
9223
|
+
*/
|
|
8980
9224
|
async _loadReturningCount(row, countSelection, ormContext) {
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
if (!schema || !edgeMetadata) throw new Error("returning({ _count }) requires orm.db(ctx) configured from createOrm({ schema, ... }).");
|
|
8984
|
-
const tableName = getTableName(this.table);
|
|
8985
|
-
const tableConfig = Object.values(schema).find((config) => config.name === tableName);
|
|
8986
|
-
if (!tableConfig) throw new Error(`Table config for '${tableName}' is not registered.`);
|
|
8987
|
-
return (await new GelRelationalQuery(schema, tableConfig, edgeMetadata.filter((edge) => edge.sourceTable === tableName), this.db, {
|
|
8988
|
-
where: { id: row._id },
|
|
8989
|
-
columns: {},
|
|
8990
|
-
with: { _count: countSelection }
|
|
8991
|
-
}, "first", edgeMetadata, ormContext?.rls, ormContext?.relationLoading).execute())?._count ?? {};
|
|
9225
|
+
this._returningCountLoader ??= createReturningCountLoader(this.db, this.table, ormContext);
|
|
9226
|
+
return await this._returningCountLoader.load(row, countSelection);
|
|
8992
9227
|
}
|
|
8993
9228
|
constructor(db, table) {
|
|
8994
9229
|
super();
|
|
@@ -9103,6 +9338,7 @@ var ConvexUpdateBuilder = class extends QueryPromise {
|
|
|
9103
9338
|
operation: "update",
|
|
9104
9339
|
rls
|
|
9105
9340
|
});
|
|
9341
|
+
const rlsResolution = createRlsPolicyResolutionCache();
|
|
9106
9342
|
const onUpdateSet = {};
|
|
9107
9343
|
for (const [columnName, builder] of Object.entries(getTableColumns$2(this.table))) {
|
|
9108
9344
|
if (columnName in normalizedSetValues) continue;
|
|
@@ -9225,6 +9461,7 @@ var ConvexUpdateBuilder = class extends QueryPromise {
|
|
|
9225
9461
|
row,
|
|
9226
9462
|
updatedRow,
|
|
9227
9463
|
decision: await evaluateUpdateDecision({
|
|
9464
|
+
cache: rlsResolution,
|
|
9228
9465
|
table: this.table,
|
|
9229
9466
|
existingRow: row,
|
|
9230
9467
|
updatedRow,
|
|
@@ -10571,14 +10808,12 @@ function scheduledMutationBatchFactory(schema, edgeMetadata, scheduledMutationBa
|
|
|
10571
10808
|
if (workType === "cascade-delete") {
|
|
10572
10809
|
if (action === "set null") {
|
|
10573
10810
|
ensureNullableColumns(table, sourceColumns, `Foreign key set null on '${args.table}'`);
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
await ctx.db.patch(args.table, row._id, patch);
|
|
10578
|
-
}
|
|
10811
|
+
const nullPatch = {};
|
|
10812
|
+
for (const columnName of sourceColumns) nullPatch[columnName] = null;
|
|
10813
|
+
await patchReferencingRows(ctx.db, args.table, rows, nullPatch);
|
|
10579
10814
|
} else if (action === "set default") {
|
|
10580
10815
|
const defaults = ensureDefaultColumns(table, sourceColumns, `Foreign key set default on '${args.table}'`);
|
|
10581
|
-
|
|
10816
|
+
await patchReferencingRows(ctx.db, args.table, rows, defaults);
|
|
10582
10817
|
} else if (action === "cascade") {
|
|
10583
10818
|
if (!foreignKeyGraph) throw new Error("scheduledMutationBatch: foreign key graph is missing from ORM context.");
|
|
10584
10819
|
for (const row of rows) {
|
|
@@ -10607,21 +10842,19 @@ function scheduledMutationBatchFactory(schema, edgeMetadata, scheduledMutationBa
|
|
|
10607
10842
|
} else if (workType === "cascade-update") {
|
|
10608
10843
|
if (action === "set null") {
|
|
10609
10844
|
ensureNullableColumns(table, sourceColumns, `Foreign key set null on '${args.table}'`);
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
await ctx.db.patch(args.table, row._id, patch);
|
|
10614
|
-
}
|
|
10845
|
+
const nullPatch = {};
|
|
10846
|
+
for (const columnName of sourceColumns) nullPatch[columnName] = null;
|
|
10847
|
+
await patchReferencingRows(ctx.db, args.table, rows, nullPatch);
|
|
10615
10848
|
} else if (action === "set default") {
|
|
10616
10849
|
const defaults = ensureDefaultColumns(table, sourceColumns, `Foreign key set default on '${args.table}'`);
|
|
10617
|
-
|
|
10850
|
+
await patchReferencingRows(ctx.db, args.table, rows, defaults);
|
|
10618
10851
|
} else if (action === "cascade") {
|
|
10619
10852
|
const newValues = decodeUndefinedDeep(args.newValues);
|
|
10620
10853
|
if (!newValues || !Array.isArray(newValues)) throw new Error("scheduledMutationBatch: newValues are required for cascade update.");
|
|
10621
10854
|
const patchValues = {};
|
|
10622
10855
|
for (let i = 0; i < sourceColumns.length; i += 1) patchValues[sourceColumns[i]] = newValues[i];
|
|
10623
10856
|
ensureNonNullValues(table, patchValues, `Foreign key cascade update on '${args.table}'`);
|
|
10624
|
-
|
|
10857
|
+
await patchReferencingRows(ctx.db, args.table, rows, patchValues);
|
|
10625
10858
|
}
|
|
10626
10859
|
}
|
|
10627
10860
|
if (await queryWithIndex().first() !== null || hitByteLimit) await ctx.scheduler.runAfter(args.delayMs, scheduledMutationBatch, {
|