@lunora/do 1.0.0-alpha.3 → 1.0.0-alpha.30
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/LICENSE.md +6 -0
- package/__assets__/package-og.svg +1 -1
- package/dist/index.d.mts +1375 -289
- package/dist/index.d.ts +1375 -289
- package/dist/index.mjs +35 -30
- package/dist/packem_shared/{ADMIN_FUNCTION_PREFIX-Dzdqq5J2.mjs → ADMIN_FUNCTIONS-CAHLZMj8.mjs} +62 -8
- package/dist/packem_shared/{matchesStaticWhere-CFk6adSu.mjs → AGGREGATE_SQL_FUNCTION-CQsu2Xga.mjs} +5 -3
- package/dist/packem_shared/{applyCdcChanges-Ctdmxmrv.mjs → CDC_LOG_TABLE-DjJEHiM2.mjs} +7 -3
- package/dist/packem_shared/{ConflictError-C0STs6bU.mjs → ConflictError-CLoq37xH.mjs} +4 -5
- package/dist/packem_shared/{CountRlsUnsupportedError-28ZvvwKS.mjs → CountRlsUnsupportedError-BGxj0pgS.mjs} +4 -4
- package/dist/packem_shared/{DATA_MIGRATION_STATE_TABLE-PTtTiQ7U.mjs → DATA_MIGRATION_STATE_TABLE-CYwBpyTr.mjs} +11 -3
- package/dist/packem_shared/{assertFlatPredicate-DyVYReuT.mjs → DEFAULT_MAX_RELATION_KEYS-BEan1CRD.mjs} +58 -7
- package/dist/packem_shared/{assertReadonly-dDcFE1YZ.mjs → MAX_SQL_ROWS-D57CJaT9.mjs} +3 -1
- package/dist/packem_shared/NotFoundError-C70b9hLw.mjs +9 -0
- package/dist/packem_shared/{assertValidClientId-CBZ1zC96.mjs → NotUniqueError-BigrdT_W.mjs} +241 -90
- package/dist/packem_shared/{rank-CrkEIpF4.mjs → RANK_TIEBREAK-CXhdcA1o.mjs} +2 -13
- package/dist/packem_shared/{guardWriter-u3UlnCH5.mjs → RLS_UNWRAP_SYMBOL-DTvHvRzY.mjs} +22 -9
- package/dist/packem_shared/{ROOT_DO_SIZE_WARN_BYTES-DQkmGiCS.mjs → ROOT_DO_SIZE_WARN_BYTES-BA8QOChj.mjs} +2878 -326
- package/dist/packem_shared/{ReactiveCache-ByVzgH3d.mjs → ReactiveCache-BYlSGY0N.mjs} +1 -28
- package/dist/packem_shared/{SESSION_DO_TTL_DEFAULT-ilPZsVwu.mjs → SESSION_DO_TTL_DEFAULT-BnSKgVO4.mjs} +16 -27
- package/dist/packem_shared/{SHARD_REGISTRY_DO_NAME-BsAbi5Mn.mjs → SHARD_REGISTRY_DO_NAME-D99roc-r.mjs} +12 -14
- package/dist/packem_shared/{applyOnDelete-CMif2RKw.mjs → applyOnDelete-BXSq3S70.mjs} +23 -12
- package/dist/packem_shared/{buildSeekWhere-lVsNXSLy.mjs → applySelect-WQY8m62C.mjs} +21 -2
- package/dist/packem_shared/{armRestore-BJk53Ro8.mjs → armRestore-4Px61hHS.mjs} +4 -10
- package/dist/packem_shared/{backfillAggregateIndexes-BF5eL7kW.mjs → backfillAggregateIndexes-DDoT-UUI.mjs} +3 -2
- package/dist/packem_shared/{compileWhereSql-CXrhFA3G.mjs → compileWhereSql-DE6yfRcQ.mjs} +5 -3
- package/dist/packem_shared/constant-time-equal-BVRWZgES.mjs +12 -0
- package/dist/packem_shared/{createSystemReader-8CzSZP9V.mjs → createSystemReader-D12eNH13.mjs} +4 -2
- package/dist/packem_shared/ctx-db-idempotency-BdcNpvY4.mjs +108 -0
- package/dist/packem_shared/ctx-db-shapes-Cz9dHyh1.mjs +53 -0
- package/dist/packem_shared/diffExternalSource-Cx9HUPJj.mjs +44 -0
- package/dist/packem_shared/{exportShardRows-DZEhUeyI.mjs → exportShardRows-Dy3oFZ26.mjs} +4 -3
- package/dist/packem_shared/isSourceDue-CYkt7Ru8.mjs +41 -0
- package/dist/packem_shared/json-response-BdbtpOhm.mjs +3 -0
- package/dist/packem_shared/materializeExternalRows-CTqZisSC.mjs +23 -0
- package/dist/packem_shared/{runShardMigrations-C3bn5r93.mjs → runShardMigrations-BGx4v2B6.mjs} +6 -4
- package/dist/packem_shared/serialize-sql-BlRUoiQe.mjs +14 -0
- package/dist/packem_shared/{serveRelationFanout-Clr1a05L.mjs → serveRelationFanout-BgaNg3Hu.mjs} +4 -7
- package/dist/packem_shared/stableStringify-MydiuScU.mjs +40 -0
- package/dist/packem_shared/subscription-delivery-CWigSEr3.mjs +348 -0
- package/dist/packem_shared/subscriptionListDeltas-DRLvFlM1.mjs +1 -0
- package/package.json +5 -3
- package/dist/packem_shared/NotFoundError-CMuMZt81.mjs +0 -10
- package/dist/packem_shared/ctx-db-idempotency-DkC9rP91.mjs +0 -35
- package/dist/packem_shared/encodePartitionKey-C6blLR5K.mjs +0 -1
- /package/dist/packem_shared/{AUTH_METRICS_BUCKET_MS-CiHHYeJi.mjs → AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs} +0 -0
- /package/dist/packem_shared/{ensureFunctionMetricsTables-UDNVD7FS.mjs → FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs} +0 -0
- /package/dist/packem_shared/{clearCapturedMail-CPpgl-dX.mjs → MAIL_RETENTION-CPpgl-dX.mjs} +0 -0
- /package/dist/packem_shared/{buildSecurityAudit-CCAvoFlr.mjs → MIN_ADMIN_TOKEN_LENGTH-CCAvoFlr.mjs} +0 -0
- /package/dist/packem_shared/{ftsTableName-BLEMawrp.mjs → buildFtsMatch-BLEMawrp.mjs} +0 -0
- /package/dist/packem_shared/{runTriggers-5N6_Fx0A.mjs → hasTrigger-5N6_Fx0A.mjs} +0 -0
|
@@ -1,27 +1,30 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
1
2
|
import { sql } from 'drizzle-orm';
|
|
2
|
-
import { matchesStaticWhere, aggregateSqlFunction, normalizeCountArgument, throwingScheduler } from './
|
|
3
|
+
import { matchesStaticWhere, aggregateSqlFunction, normalizeCountArgument, throwingScheduler } from './AGGREGATE_SQL_FUNCTION-CQsu2Xga.mjs';
|
|
3
4
|
import { encodeAggregateKey, foldAggregateTally, aggregateTableName, coerceAggregateNumber, readAggregateValue } from './aggregateTableName-CxNqY1Sl.mjs';
|
|
4
|
-
import { mergeWhere, CountRlsUnsupportedError, selectIndexForGroupBy, selectIndexForCount, selectIndexForAggregate } from './CountRlsUnsupportedError-
|
|
5
|
-
import { appendCdcChange } from './
|
|
6
|
-
export { CDC_LOG_TABLE,
|
|
5
|
+
import { mergeWhere, CountRlsUnsupportedError, selectIndexForGroupBy, selectIndexForCount, selectIndexForAggregate } from './CountRlsUnsupportedError-BGxj0pgS.mjs';
|
|
6
|
+
import { appendCdcChange } from './CDC_LOG_TABLE-DjJEHiM2.mjs';
|
|
7
|
+
export { CDC_LOG_TABLE, applyCdcChanges, bumpCdcEpoch, minCdcSeq, readCdcChanges, readCdcCursor, readCdcEpoch, trimCdcChanges } from './CDC_LOG_TABLE-DjJEHiM2.mjs';
|
|
7
8
|
import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
|
|
8
9
|
import { i as isFtsAvailable, D as DOC_COLUMN$1, r as rowToDocument, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT, d as aggUpsertSql, j as jsonPathSql, q as quoteIdentifier, t as tableColumns, e as qualifiedJsonPathSql } from './do-sql-BCHCWtrD.mjs';
|
|
9
10
|
import { param } from './renderSql-D6eUcn2N.mjs';
|
|
10
|
-
import {
|
|
11
|
-
import { stringifySearchText, ftsTableName, tokenizeSearch, buildFtsMatch, scoreDocument } from './
|
|
11
|
+
import { sortColumnName, matchesRankStaticWhere, encodePartitionKey, rankTableName, resolveRankPartition, RANK_TIEBREAK } from './RANK_TIEBREAK-CXhdcA1o.mjs';
|
|
12
|
+
import { stringifySearchText, ftsTableName, tokenizeSearch, buildFtsMatch, scoreDocument } from './buildFtsMatch-BLEMawrp.mjs';
|
|
13
|
+
import { s as serializeSqlValue } from './serialize-sql-BlRUoiQe.mjs';
|
|
12
14
|
import { SCAN_DEP } from './SCAN_DEP-DLJF8dsj.mjs';
|
|
13
|
-
import { decodeCursor, normalizeOrderKeys, buildSeekWhere, encodeCursor, buildSeekBeforeWhere } from './
|
|
14
|
-
import NotFoundError from './NotFoundError-
|
|
15
|
-
import { assertFlatPredicate, resolveRelationPredicates } from './
|
|
16
|
-
import { runRowValidators, resolveWith, applyOnDelete } from './applyOnDelete-
|
|
17
|
-
import { guardWriter } from './
|
|
18
|
-
import { createSystemReader } from './createSystemReader-
|
|
19
|
-
import { ConflictError } from './ConflictError-
|
|
20
|
-
import { runTriggers } from './
|
|
21
|
-
import { compileWhereSql } from './compileWhereSql-
|
|
22
|
-
export { backfillAggregateIndexes, backfillRankIndexes } from './backfillAggregateIndexes-
|
|
23
|
-
export { I as IDEMPOTENCY_TABLE, m as
|
|
24
|
-
export { runShardMigrations } from './runShardMigrations-
|
|
15
|
+
import { decodeCursor, normalizeOrderKeys, buildSeekWhere, applySelect, encodeCursor, softDeleteScope, buildSeekBeforeWhere } from './applySelect-WQY8m62C.mjs';
|
|
16
|
+
import NotFoundError from './NotFoundError-C70b9hLw.mjs';
|
|
17
|
+
import { assertFlatPredicate, resolveRelationPredicates } from './DEFAULT_MAX_RELATION_KEYS-BEan1CRD.mjs';
|
|
18
|
+
import { runRowValidators, resolveWith, applyOnDelete, fanOutScalarCounts } from './applyOnDelete-BXSq3S70.mjs';
|
|
19
|
+
import { guardWriter } from './RLS_UNWRAP_SYMBOL-DTvHvRzY.mjs';
|
|
20
|
+
import { createSystemReader } from './createSystemReader-D12eNH13.mjs';
|
|
21
|
+
import { ConflictError } from './ConflictError-CLoq37xH.mjs';
|
|
22
|
+
import { runTriggers } from './hasTrigger-5N6_Fx0A.mjs';
|
|
23
|
+
import { compileWhereSql } from './compileWhereSql-DE6yfRcQ.mjs';
|
|
24
|
+
export { backfillAggregateIndexes, backfillRankIndexes } from './backfillAggregateIndexes-DDoT-UUI.mjs';
|
|
25
|
+
export { C as CLIENT_WATERMARK_TABLE, G as GLOBAL_SHAPE_SNAPSHOT_TABLE, I as IDEMPOTENCY_TABLE, c as advanceClientWatermark, d as deleteGlobalShapeSnapshot, e as deleteGlobalShapeSnapshotsForConnection, m as migrateClientWatermark, b as migrateGlobalShapeSnapshot, r as readClientWatermark, f as readGlobalShapeSnapshot, g as readIdempotent, t as trimIdempotent, w as writeGlobalShapeSnapshot, h as writeIdempotent } from './ctx-db-idempotency-BdcNpvY4.mjs';
|
|
26
|
+
export { runShardMigrations } from './runShardMigrations-BGx4v2B6.mjs';
|
|
27
|
+
export { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-Cz9dHyh1.mjs';
|
|
25
28
|
|
|
26
29
|
const rankIndexFieldsUnchanged = (index, previous, next) => {
|
|
27
30
|
const fields = [...index.partitionBy ?? [], ...index.sortBy.map((key) => key.field), ...index.where ? Object.keys(index.where) : []];
|
|
@@ -420,11 +423,11 @@ const computeRankPage = (deps, tableName, indexName, rankPageOptions) => {
|
|
|
420
423
|
const { assertRankPartitionLocal, ensureRankBackfilled, onRead, schema } = deps;
|
|
421
424
|
const definition = schema.tables[tableName];
|
|
422
425
|
if (!definition) {
|
|
423
|
-
throw new
|
|
426
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
424
427
|
}
|
|
425
428
|
const index = definition.rankIndexes?.find((i) => i.name === indexName);
|
|
426
429
|
if (!index) {
|
|
427
|
-
throw new
|
|
430
|
+
throw new LunoraError("INTERNAL", `unknown rankIndex "${indexName}" on table "${tableName}"`);
|
|
428
431
|
}
|
|
429
432
|
assertRankPartitionLocal(tableName, definition, index);
|
|
430
433
|
onRead(tableName, SCAN_DEP);
|
|
@@ -472,7 +475,7 @@ const computeRankPage = (deps, tableName, indexName, rankPageOptions) => {
|
|
|
472
475
|
const CLIENT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
473
476
|
const assertValidClientId = (clientId) => {
|
|
474
477
|
if (!CLIENT_ID_PATTERN.test(clientId)) {
|
|
475
|
-
throw new
|
|
478
|
+
throw new LunoraError("INTERNAL", `invalid clientId ${JSON.stringify(clientId)}: a client-supplied row id must be a UUID`);
|
|
476
479
|
}
|
|
477
480
|
};
|
|
478
481
|
const MAX_TRIGGER_DEPTH = 50;
|
|
@@ -480,11 +483,11 @@ const DEFAULT_BATCH_LIMIT = 500;
|
|
|
480
483
|
const assertBatchLimit = (count, limit, op) => {
|
|
481
484
|
const cap = limit ?? DEFAULT_BATCH_LIMIT;
|
|
482
485
|
if (count > cap) {
|
|
483
|
-
throw
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
status: 400
|
|
487
|
-
|
|
486
|
+
throw new LunoraError(
|
|
487
|
+
"BATCH_LIMIT_EXCEEDED",
|
|
488
|
+
`${op}: batch of ${String(count)} exceeds the limit of ${String(cap)} (raise options.limit or chunk the call)`,
|
|
489
|
+
{ status: 400 }
|
|
490
|
+
);
|
|
488
491
|
}
|
|
489
492
|
};
|
|
490
493
|
const createRangeBuilder = (stage) => {
|
|
@@ -516,14 +519,17 @@ const createSearchBuilder = (search, tableName) => {
|
|
|
516
519
|
const builder = {
|
|
517
520
|
eq: (field, value) => {
|
|
518
521
|
if (!search.definition.filterFields?.includes(field)) {
|
|
519
|
-
throw new
|
|
522
|
+
throw new LunoraError("INTERNAL", `field "${field}" is not a filter field of search index "${search.indexName}" on table "${tableName}"`);
|
|
520
523
|
}
|
|
521
524
|
search.filters.push({ field, value });
|
|
522
525
|
return builder;
|
|
523
526
|
},
|
|
524
527
|
search: (field, query) => {
|
|
525
528
|
if (field !== search.definition.field) {
|
|
526
|
-
throw new
|
|
529
|
+
throw new LunoraError(
|
|
530
|
+
"INTERNAL",
|
|
531
|
+
`search index "${search.indexName}" on table "${tableName}" indexes "${search.definition.field}", not "${field}"`
|
|
532
|
+
);
|
|
527
533
|
}
|
|
528
534
|
const stage = search;
|
|
529
535
|
stage.field = field;
|
|
@@ -534,7 +540,7 @@ const createSearchBuilder = (search, tableName) => {
|
|
|
534
540
|
};
|
|
535
541
|
return builder;
|
|
536
542
|
};
|
|
537
|
-
const searchViaFts = (sql$1, tableName, search, limit) => {
|
|
543
|
+
const searchViaFts = (sql$1, tableName, search, limit, scopeCondition) => {
|
|
538
544
|
const tokens = tokenizeSearch(search.query);
|
|
539
545
|
if (tokens.length === 0) {
|
|
540
546
|
return [];
|
|
@@ -544,6 +550,9 @@ const searchViaFts = (sql$1, tableName, search, limit) => {
|
|
|
544
550
|
for (const filter of search.filters) {
|
|
545
551
|
whereClauses.push(sql`${jsonPathSql(filter.field)} = ${serializeSqlValue(filter.value)}`);
|
|
546
552
|
}
|
|
553
|
+
if (scopeCondition) {
|
|
554
|
+
whereClauses.push(scopeCondition);
|
|
555
|
+
}
|
|
547
556
|
let query = sql`SELECT m.id, m._creationTime, m.${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(ftName)} f JOIN ${sql.identifier(tableName)} m ON m.id = f.${sql.identifier("__id__")} WHERE ${sql.join(whereClauses, sql` AND `)} ORDER BY f.rank, m._creationTime DESC`;
|
|
548
557
|
if (typeof limit === "number") {
|
|
549
558
|
query = sql`${query} LIMIT ${sql.raw(String(Math.max(0, Math.floor(limit))))}`;
|
|
@@ -558,7 +567,7 @@ const searchViaFts = (sql$1, tableName, search, limit) => {
|
|
|
558
567
|
}
|
|
559
568
|
return docs;
|
|
560
569
|
};
|
|
561
|
-
const searchViaScan = (sql$1, tableName, search, limit) => {
|
|
570
|
+
const searchViaScan = (sql$1, tableName, search, limit, scopeCondition) => {
|
|
562
571
|
const tokens = tokenizeSearch(search.query);
|
|
563
572
|
if (tokens.length === 0) {
|
|
564
573
|
return [];
|
|
@@ -567,6 +576,9 @@ const searchViaScan = (sql$1, tableName, search, limit) => {
|
|
|
567
576
|
for (const filter of search.filters) {
|
|
568
577
|
whereClauses.push(sql`${jsonPathSql(filter.field)} = ${serializeSqlValue(filter.value)}`);
|
|
569
578
|
}
|
|
579
|
+
if (scopeCondition) {
|
|
580
|
+
whereClauses.push(scopeCondition);
|
|
581
|
+
}
|
|
570
582
|
let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
|
|
571
583
|
if (whereClauses.length > 0) {
|
|
572
584
|
query = sql`${query} WHERE ${sql.join(whereClauses, sql` AND `)}`;
|
|
@@ -660,11 +672,12 @@ const scanDocs = (rows, filters, cap) => {
|
|
|
660
672
|
}
|
|
661
673
|
return docs;
|
|
662
674
|
};
|
|
663
|
-
const paginateStage = (sql$1, tableName, stage, options) => {
|
|
675
|
+
const paginateStage = (sql$1, tableName, stage, options, scopeCondition) => {
|
|
664
676
|
const numberItems = Math.max(0, Math.floor(options.numItems));
|
|
665
677
|
const orderKeys = paginateOrderKeys(stage);
|
|
666
678
|
const bounded = typeof options.endCursor === "string";
|
|
667
|
-
const
|
|
679
|
+
const pageWhere = compileWhereSql(paginateWhere(stage, orderKeys, options.cursor, options.endCursor), doWhereSqlStrategy);
|
|
680
|
+
const whereCondition = scopeCondition && pageWhere ? sql`${pageWhere} AND ${scopeCondition}` : scopeCondition ?? pageWhere;
|
|
668
681
|
let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
|
|
669
682
|
if (whereCondition) {
|
|
670
683
|
query = sql`${query} WHERE ${whereCondition}`;
|
|
@@ -697,19 +710,16 @@ const paginateStage = (sql$1, tableName, stage, options) => {
|
|
|
697
710
|
page
|
|
698
711
|
};
|
|
699
712
|
};
|
|
700
|
-
class NotUniqueError extends
|
|
701
|
-
code = "NOT_UNIQUE";
|
|
702
|
-
status = 400;
|
|
713
|
+
class NotUniqueError extends LunoraError {
|
|
703
714
|
constructor(message = "unique() found more than one matching document") {
|
|
704
|
-
super(message);
|
|
705
|
-
this.name = "NotUniqueError";
|
|
715
|
+
super("NOT_UNIQUE", message, { name: "NotUniqueError" });
|
|
706
716
|
}
|
|
707
717
|
}
|
|
708
718
|
const ID_WHITESPACE_PATTERN = /\s/u;
|
|
709
719
|
const NUL_CHARACTER = String.fromCodePoint(0);
|
|
710
720
|
const normalizeIdStructurally = (schema, tableName, id) => {
|
|
711
721
|
if (!schema.tables[tableName]) {
|
|
712
|
-
throw new
|
|
722
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
713
723
|
}
|
|
714
724
|
if (typeof id !== "string" || id.length === 0 || ID_WHITESPACE_PATTERN.test(id) || id.includes(NUL_CHARACTER)) {
|
|
715
725
|
return null;
|
|
@@ -719,8 +729,10 @@ const normalizeIdStructurally = (schema, tableName, id) => {
|
|
|
719
729
|
const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
|
|
720
730
|
const tableDefinition = schema.tables[tableName];
|
|
721
731
|
if (!tableDefinition) {
|
|
722
|
-
throw new
|
|
732
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
723
733
|
}
|
|
734
|
+
const scopeWhere = softDeleteScope(tableDefinition.softDeleteMode, void 0);
|
|
735
|
+
const scopeCondition = scopeWhere ? compileWhereSql(scopeWhere, doWhereSqlStrategy) : void 0;
|
|
724
736
|
const stage = {
|
|
725
737
|
indexFields: [],
|
|
726
738
|
indexName: void 0,
|
|
@@ -731,11 +743,11 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
|
|
|
731
743
|
const runSearchFetch = (limit) => {
|
|
732
744
|
const { search } = stage;
|
|
733
745
|
if (!search) {
|
|
734
|
-
throw new
|
|
746
|
+
throw new LunoraError("INTERNAL", "runSearchFetch called without a staged search");
|
|
735
747
|
}
|
|
736
748
|
const filtered = stage.inMemoryFilters.length > 0;
|
|
737
749
|
const engineLimit = filtered ? void 0 : limit;
|
|
738
|
-
const docs = isFtsAvailable(sql$1) ? searchViaFts(sql$1, tableName, search, engineLimit) : searchViaScan(sql$1, tableName, search, engineLimit);
|
|
750
|
+
const docs = isFtsAvailable(sql$1) ? searchViaFts(sql$1, tableName, search, engineLimit, scopeCondition) : searchViaScan(sql$1, tableName, search, engineLimit, scopeCondition);
|
|
739
751
|
if (!filtered) {
|
|
740
752
|
return docs;
|
|
741
753
|
}
|
|
@@ -766,6 +778,9 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
|
|
|
766
778
|
for (const condition of stage.sqlConditions) {
|
|
767
779
|
whereClauses.push(sql`${jsonPathSql(condition.field)} ${sql.raw(condition.comparator)} ${serializeSqlValue(condition.value)}`);
|
|
768
780
|
}
|
|
781
|
+
if (scopeCondition) {
|
|
782
|
+
whereClauses.push(scopeCondition);
|
|
783
|
+
}
|
|
769
784
|
let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
|
|
770
785
|
if (whereClauses.length > 0) {
|
|
771
786
|
query = sql`${query} WHERE ${sql.join(whereClauses, sql` AND `)}`;
|
|
@@ -811,9 +826,9 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
|
|
|
811
826
|
// eslint-disable-next-line @typescript-eslint/require-await -- TableReaderLike returns Promises (the D1 twin awaits real I/O); the DO impl is synchronous over local SQLite
|
|
812
827
|
async paginate(options) {
|
|
813
828
|
if (stage.search) {
|
|
814
|
-
throw new
|
|
829
|
+
throw new LunoraError("INTERNAL", "pagination is not supported on search queries; use .take(n) or .collect()");
|
|
815
830
|
}
|
|
816
|
-
return paginateStage(sql$1, tableName, stage, options);
|
|
831
|
+
return paginateStage(sql$1, tableName, stage, options, scopeCondition);
|
|
817
832
|
},
|
|
818
833
|
// eslint-disable-next-line @typescript-eslint/require-await -- TableReaderLike returns Promises (the D1 twin awaits real I/O); the DO impl is synchronous over local SQLite
|
|
819
834
|
async take(limit) {
|
|
@@ -830,7 +845,7 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
|
|
|
830
845
|
withIndex(indexName, range) {
|
|
831
846
|
const definition = tableDefinition.indexes.find((index) => index.name === indexName);
|
|
832
847
|
if (!definition) {
|
|
833
|
-
throw new
|
|
848
|
+
throw new LunoraError("INTERNAL", `unknown index "${indexName}" on table "${tableName}"`);
|
|
834
849
|
}
|
|
835
850
|
onIndexUse(tableName, indexName, "index");
|
|
836
851
|
stage.indexName = indexName;
|
|
@@ -843,7 +858,7 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
|
|
|
843
858
|
withSearchIndex(indexName, search) {
|
|
844
859
|
const definition = (tableDefinition.searchIndexes ?? []).find((index) => index.name === indexName);
|
|
845
860
|
if (!definition) {
|
|
846
|
-
throw new
|
|
861
|
+
throw new LunoraError("INTERNAL", `unknown search index "${indexName}" on table "${tableName}"`);
|
|
847
862
|
}
|
|
848
863
|
onIndexUse(tableName, indexName, "search");
|
|
849
864
|
const searchStage = {
|
|
@@ -857,7 +872,7 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
|
|
|
857
872
|
stage.search = searchStage;
|
|
858
873
|
search(createSearchBuilder(searchStage, tableName));
|
|
859
874
|
if (!searchStage.hasQuery) {
|
|
860
|
-
throw new
|
|
875
|
+
throw new LunoraError("INTERNAL", `search index "${indexName}" on table "${tableName}" requires a .search(field, query) call`);
|
|
861
876
|
}
|
|
862
877
|
return reader;
|
|
863
878
|
}
|
|
@@ -899,7 +914,10 @@ const applyOnUpdate = (definition, provided, target, auth) => {
|
|
|
899
914
|
const assertNoExplicitUndefined = (op, document) => {
|
|
900
915
|
for (const field of Object.keys(document)) {
|
|
901
916
|
if (document[field] === void 0) {
|
|
902
|
-
throw new
|
|
917
|
+
throw new LunoraError(
|
|
918
|
+
"INTERNAL",
|
|
919
|
+
`Cannot ${op} field '${field}' to undefined — use null to clear a nullable field, or omit the key to leave it unchanged.`
|
|
920
|
+
);
|
|
903
921
|
}
|
|
904
922
|
}
|
|
905
923
|
};
|
|
@@ -979,7 +997,10 @@ const createShardCtxDb = (options) => {
|
|
|
979
997
|
const routeBackend = (table, op) => {
|
|
980
998
|
if (isGlobalTable(table)) {
|
|
981
999
|
if (!globalDb) {
|
|
982
|
-
throw new
|
|
1000
|
+
throw new LunoraError(
|
|
1001
|
+
"INTERNAL",
|
|
1002
|
+
`cross-backend ${op} for global table '${table}' requires a globalDb writer — pass one to createShardCtxDb({ globalDb })`
|
|
1003
|
+
);
|
|
983
1004
|
}
|
|
984
1005
|
return globalDb;
|
|
985
1006
|
}
|
|
@@ -991,13 +1012,12 @@ const createShardCtxDb = (options) => {
|
|
|
991
1012
|
return void 0;
|
|
992
1013
|
}
|
|
993
1014
|
if (!globalDb) {
|
|
994
|
-
throw new
|
|
1015
|
+
throw new LunoraError("INTERNAL", `${op} on global table '${tableName}' requires a globalDb writer — pass one to createShardCtxDb({ globalDb })`);
|
|
995
1016
|
}
|
|
996
1017
|
return globalDb;
|
|
997
1018
|
};
|
|
998
1019
|
const globalFallback = () => globalDb;
|
|
999
1020
|
const relationFetcher = (relationTable, relationArgs) => routeBackend(relationTable, "relation load").findMany(relationTable, relationArgs);
|
|
1000
|
-
const relationCounter = (relationTable, relationWhere) => routeBackend(relationTable, "relation load").count(relationTable, relationWhere);
|
|
1001
1021
|
const relationPredicateFetcher = (relationTable, relationArgs) => {
|
|
1002
1022
|
if (isGlobalTable(relationTable)) {
|
|
1003
1023
|
onRead(relationTable, SCAN_DEP);
|
|
@@ -1015,6 +1035,31 @@ const createShardCtxDb = (options) => {
|
|
|
1015
1035
|
schema,
|
|
1016
1036
|
tableName: predicateTable
|
|
1017
1037
|
});
|
|
1038
|
+
const relationGroupedCounter = async (relationTable, whereField, values, policyWhere) => {
|
|
1039
|
+
const globalWriter = globalWriterFor(relationTable, "relation grouped count");
|
|
1040
|
+
if (globalWriter) {
|
|
1041
|
+
onRead(relationTable, SCAN_DEP);
|
|
1042
|
+
return fanOutScalarCounts((t, w) => globalWriter.count(t, w), relationTable, whereField, values, policyWhere);
|
|
1043
|
+
}
|
|
1044
|
+
const definition = schema.tables[relationTable];
|
|
1045
|
+
if (!definition) {
|
|
1046
|
+
throw new LunoraError("INTERNAL", `unknown table: ${relationTable}`);
|
|
1047
|
+
}
|
|
1048
|
+
onRead(relationTable, SCAN_DEP);
|
|
1049
|
+
const softScope = softDeleteScope(definition.softDeleteMode, void 0);
|
|
1050
|
+
const inFilter = { [whereField]: { in: values } };
|
|
1051
|
+
const combined = mergeWhere(mergeWhere(inFilter, policyWhere), softScope);
|
|
1052
|
+
const resolvedCombined = await resolveAggregateRelations(combined, relationTable, void 0);
|
|
1053
|
+
const whereCondition = compileWhereSql(resolvedCombined, doWhereSqlStrategy);
|
|
1054
|
+
const fieldSql = jsonPathSql(whereField);
|
|
1055
|
+
let query = sql`SELECT ${fieldSql} AS __fk__, COUNT(*) AS count FROM ${sql.identifier(relationTable)}`;
|
|
1056
|
+
if (whereCondition) {
|
|
1057
|
+
query = sql`${query} WHERE ${whereCondition}`;
|
|
1058
|
+
}
|
|
1059
|
+
query = sql`${query} GROUP BY ${fieldSql}`;
|
|
1060
|
+
const rows = runDrizzle(sql$1, query).toArray();
|
|
1061
|
+
return new Map(rows.map((row) => [row["__fk__"], row.count]));
|
|
1062
|
+
};
|
|
1018
1063
|
let triggerDepth = 0;
|
|
1019
1064
|
const triggerMatchers = /* @__PURE__ */ new Set();
|
|
1020
1065
|
for (const [tableName, definition] of Object.entries(schema.tables)) {
|
|
@@ -1115,7 +1160,7 @@ const createShardCtxDb = (options) => {
|
|
|
1115
1160
|
}
|
|
1116
1161
|
const definition = schema.tables[tableName];
|
|
1117
1162
|
if (!definition) {
|
|
1118
|
-
throw new
|
|
1163
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1119
1164
|
}
|
|
1120
1165
|
aggregateSqlFunction(aggOptions.op);
|
|
1121
1166
|
if (aggOptions.op === "count") {
|
|
@@ -1127,13 +1172,14 @@ const createShardCtxDb = (options) => {
|
|
|
1127
1172
|
});
|
|
1128
1173
|
}
|
|
1129
1174
|
if (!aggOptions.field) {
|
|
1130
|
-
throw new
|
|
1175
|
+
throw new LunoraError("INTERNAL", `aggregate(${tableName}, { op: "${aggOptions.op}" }): "field" is required for non-count reducers`);
|
|
1131
1176
|
}
|
|
1132
1177
|
onRead(tableName, SCAN_DEP);
|
|
1133
|
-
const
|
|
1178
|
+
const aggScope = softDeleteScope(definition.softDeleteMode, void 0);
|
|
1179
|
+
const effective = mergeWhere(mergeWhere(aggOptions.baseWhere, aggOptions.where), aggScope);
|
|
1134
1180
|
const resolved = await resolveAggregateRelations(effective, tableName, aggOptions.relationBaseWhere);
|
|
1135
1181
|
const hasRelation = resolved !== effective;
|
|
1136
|
-
if (definition.aggregateIndexes && !aggOptions.baseWhere && !hasRelation) {
|
|
1182
|
+
if (definition.aggregateIndexes && !aggOptions.baseWhere && !hasRelation && !aggScope) {
|
|
1137
1183
|
const planned = selectIndexForAggregate(definition.aggregateIndexes, aggOptions.op, aggOptions.field, aggOptions.where);
|
|
1138
1184
|
if (planned) {
|
|
1139
1185
|
ensureBackfilled(tableName, planned.index);
|
|
@@ -1168,17 +1214,18 @@ const createShardCtxDb = (options) => {
|
|
|
1168
1214
|
}
|
|
1169
1215
|
const definition = schema.tables[tableName];
|
|
1170
1216
|
if (!definition) {
|
|
1171
|
-
throw new
|
|
1217
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1172
1218
|
}
|
|
1173
1219
|
const countOptions = normalizeCountArgument(whereOrOptions);
|
|
1174
1220
|
if (countOptions.restrictsCounts) {
|
|
1175
1221
|
throw new CountRlsUnsupportedError(tableName);
|
|
1176
1222
|
}
|
|
1177
1223
|
onRead(tableName, SCAN_DEP);
|
|
1178
|
-
const
|
|
1224
|
+
const countScope = softDeleteScope(definition.softDeleteMode, void 0);
|
|
1225
|
+
const effective = mergeWhere(mergeWhere(countOptions.baseWhere, countOptions.where), countScope);
|
|
1179
1226
|
const resolved = await resolveAggregateRelations(effective, tableName, countOptions.relationBaseWhere);
|
|
1180
1227
|
const hasRelation = resolved !== effective;
|
|
1181
|
-
if (definition.aggregateIndexes && !countOptions.baseWhere && !hasRelation) {
|
|
1228
|
+
if (definition.aggregateIndexes && !countOptions.baseWhere && !hasRelation && !countScope) {
|
|
1182
1229
|
const planned = selectIndexForCount(definition.aggregateIndexes, countOptions.where);
|
|
1183
1230
|
if (planned) {
|
|
1184
1231
|
ensureBackfilled(tableName, planned.index);
|
|
@@ -1199,16 +1246,22 @@ const createShardCtxDb = (options) => {
|
|
|
1199
1246
|
const row = runDrizzle(sql$1, query).one();
|
|
1200
1247
|
return row.count;
|
|
1201
1248
|
},
|
|
1202
|
-
async delete(id, expectedTable) {
|
|
1249
|
+
async delete(id, expectedTable, deleteOptions) {
|
|
1203
1250
|
const located = locateRowById(id, expectedTable);
|
|
1204
1251
|
if (!located) {
|
|
1205
1252
|
const global = expectedTable === void 0 ? globalFallback() : void 0;
|
|
1206
1253
|
if (global) {
|
|
1207
|
-
await global.delete(id);
|
|
1254
|
+
await global.delete(id, void 0, deleteOptions);
|
|
1208
1255
|
}
|
|
1209
1256
|
return;
|
|
1210
1257
|
}
|
|
1211
1258
|
const { docJson: existingJson, row: existing, tableName } = located;
|
|
1259
|
+
const definition = schema.tables[tableName];
|
|
1260
|
+
const hard = deleteOptions?.hard === true;
|
|
1261
|
+
const softField = !hard && definition?.softDeleteMode ? definition.softDeleteMode.field : void 0;
|
|
1262
|
+
if (softField && existing[softField] !== null && existing[softField] !== void 0) {
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1212
1265
|
if (hasMatchingTrigger(tableName, "before", "delete")) {
|
|
1213
1266
|
await fireTriggers("before", "delete", { id, op: "delete", previous: existing, table: tableName });
|
|
1214
1267
|
}
|
|
@@ -1216,10 +1269,10 @@ const createShardCtxDb = (options) => {
|
|
|
1216
1269
|
deletedId: id,
|
|
1217
1270
|
deletedReference: (references) => existing[references],
|
|
1218
1271
|
findHolders: async (holderTable, field, value) => {
|
|
1219
|
-
const holders = await routeForHolder(holderTable).findMany(holderTable, { where: { [field]: value } });
|
|
1272
|
+
const holders = await routeForHolder(holderTable).findMany(holderTable, { includeDeleted: hard, where: { [field]: value } });
|
|
1220
1273
|
return holders.page;
|
|
1221
1274
|
},
|
|
1222
|
-
onCascade: (holderTable, holderId) => routeForHolder(holderTable).delete(holderId),
|
|
1275
|
+
onCascade: (holderTable, holderId) => routeForHolder(holderTable).delete(holderId, void 0, deleteOptions),
|
|
1223
1276
|
onRestrict: (message) => {
|
|
1224
1277
|
throw new ConflictError(message, "restrict");
|
|
1225
1278
|
},
|
|
@@ -1230,6 +1283,25 @@ const createShardCtxDb = (options) => {
|
|
|
1230
1283
|
});
|
|
1231
1284
|
ensureBackfilledForTable(tableName);
|
|
1232
1285
|
ensureRankBackfilledForTable(tableName);
|
|
1286
|
+
if (softField) {
|
|
1287
|
+
const merged = { ...existing, [softField]: clock(), _id: id };
|
|
1288
|
+
runGuardedWrite(
|
|
1289
|
+
sql$1,
|
|
1290
|
+
tableName,
|
|
1291
|
+
sql`UPDATE ${sql.identifier(tableName)} SET ${sql.identifier(DOC_COLUMN$1)} = ${JSON.stringify(merged)} WHERE id = ${id} AND ${sql.identifier(DOC_COLUMN$1)} = ${existingJson}`
|
|
1292
|
+
);
|
|
1293
|
+
syncSearch(tableName, id, merged);
|
|
1294
|
+
syncAggregates(tableName, existing, merged);
|
|
1295
|
+
syncRanks(tableName, id, existing, void 0);
|
|
1296
|
+
cache?.invalidate(tableName, id);
|
|
1297
|
+
recordCdc(tableName, id, "update", merged);
|
|
1298
|
+
broadcast({ key: id, op: "update", row: merged, table: tableName });
|
|
1299
|
+
if (hasMatchingTrigger(tableName, "after", "delete")) {
|
|
1300
|
+
await fireTriggers("after", "delete", { id, op: "delete", previous: existing, table: tableName });
|
|
1301
|
+
}
|
|
1302
|
+
await onWrite({ id, op: "delete", table: tableName });
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1233
1305
|
runGuardedWrite(
|
|
1234
1306
|
sql$1,
|
|
1235
1307
|
tableName,
|
|
@@ -1253,6 +1325,25 @@ const createShardCtxDb = (options) => {
|
|
|
1253
1325
|
}
|
|
1254
1326
|
return { deleted: ids.length };
|
|
1255
1327
|
},
|
|
1328
|
+
async deleteWhere(tableName, where, batchOptions) {
|
|
1329
|
+
const global = globalWriterFor(tableName, "deleteWhere");
|
|
1330
|
+
let ids;
|
|
1331
|
+
if (global) {
|
|
1332
|
+
const rows = await global.findMany(tableName, { where });
|
|
1333
|
+
ids = rows.page.map((row) => String(row["_id"]));
|
|
1334
|
+
} else {
|
|
1335
|
+
if (!schema.tables[tableName]) {
|
|
1336
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1337
|
+
}
|
|
1338
|
+
const page = await writer.findMany(tableName, { where });
|
|
1339
|
+
ids = page.page.map((row) => String(row["_id"]));
|
|
1340
|
+
}
|
|
1341
|
+
assertBatchLimit(ids.length, batchOptions?.limit, "deleteWhere");
|
|
1342
|
+
if (writer.deleteMany === void 0) {
|
|
1343
|
+
throw new LunoraError("INTERNAL", `ctx.db.${tableName}.deleteMany is unavailable: this writer has no batch delete`);
|
|
1344
|
+
}
|
|
1345
|
+
return writer.deleteMany(ids, batchOptions);
|
|
1346
|
+
},
|
|
1256
1347
|
async findFirst(tableName, args = {}) {
|
|
1257
1348
|
const result = await writer.findMany(tableName, { ...args, limit: 1 });
|
|
1258
1349
|
return result.page[0] ?? null;
|
|
@@ -1271,8 +1362,9 @@ const createShardCtxDb = (options) => {
|
|
|
1271
1362
|
onRead(tableName, SCAN_DEP);
|
|
1272
1363
|
return global.findMany(tableName, args);
|
|
1273
1364
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1365
|
+
const findManyDefinition = schema.tables[tableName];
|
|
1366
|
+
if (!findManyDefinition) {
|
|
1367
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1276
1368
|
}
|
|
1277
1369
|
const isFullScan = !args.where && !args.baseWhere;
|
|
1278
1370
|
if (isFullScan) {
|
|
@@ -1283,6 +1375,7 @@ const createShardCtxDb = (options) => {
|
|
|
1283
1375
|
const orderKeys = normalizeOrderKeys(args.orderBy);
|
|
1284
1376
|
const seek = args.cursor ? buildSeekWhere(orderKeys, decodeCursor(args.cursor)) : void 0;
|
|
1285
1377
|
let predicate = mergeWhere(args.baseWhere, args.where);
|
|
1378
|
+
predicate = mergeWhere(predicate, softDeleteScope(findManyDefinition.softDeleteMode, args.includeDeleted));
|
|
1286
1379
|
predicate = await resolveRelationPredicates(predicate, {
|
|
1287
1380
|
canPushExists: relationExistsPushDownEnabled ? canPushRelationExists : void 0,
|
|
1288
1381
|
existsPushMode: relationExistsPushDown === "always" ? "always" : "auto",
|
|
@@ -1320,7 +1413,7 @@ const createShardCtxDb = (options) => {
|
|
|
1320
1413
|
if (limit === void 0) {
|
|
1321
1414
|
if (args.with) {
|
|
1322
1415
|
await resolveWith({
|
|
1323
|
-
|
|
1416
|
+
groupedCounter: relationGroupedCounter,
|
|
1324
1417
|
fetcher: relationFetcher,
|
|
1325
1418
|
parents: docs,
|
|
1326
1419
|
relationBaseWhere: args.relationBaseWhere,
|
|
@@ -1329,15 +1422,15 @@ const createShardCtxDb = (options) => {
|
|
|
1329
1422
|
with: args.with
|
|
1330
1423
|
});
|
|
1331
1424
|
}
|
|
1332
|
-
return { continueCursor: null, isDone: true, page: docs };
|
|
1425
|
+
return { continueCursor: null, isDone: true, page: applySelect(docs, args.select, args.with) };
|
|
1333
1426
|
}
|
|
1334
1427
|
const hasMore = docs.length > limit;
|
|
1335
1428
|
const page = hasMore ? docs.slice(0, limit) : docs;
|
|
1336
1429
|
const last = page.at(-1);
|
|
1337
1430
|
if (args.with) {
|
|
1338
1431
|
await resolveWith({
|
|
1339
|
-
counter: relationCounter,
|
|
1340
1432
|
fetcher: relationFetcher,
|
|
1433
|
+
groupedCounter: relationGroupedCounter,
|
|
1341
1434
|
parents: page,
|
|
1342
1435
|
relationBaseWhere: args.relationBaseWhere,
|
|
1343
1436
|
schema,
|
|
@@ -1346,10 +1439,12 @@ const createShardCtxDb = (options) => {
|
|
|
1346
1439
|
});
|
|
1347
1440
|
}
|
|
1348
1441
|
return {
|
|
1442
|
+
// The cursor is encoded from `last` (the full, unprojected row) above,
|
|
1443
|
+
// so `applySelect` only trims the returned payload — paging is intact.
|
|
1349
1444
|
// eslint-disable-next-line unicorn/no-null -- QueryPage.continueCursor is `null | string`: null is the documented "no further page" cursor on the wire
|
|
1350
1445
|
continueCursor: hasMore && last ? encodeCursor(last, orderKeys) : null,
|
|
1351
1446
|
isDone: !hasMore,
|
|
1352
|
-
page
|
|
1447
|
+
page: applySelect(page, args.select, args.with)
|
|
1353
1448
|
};
|
|
1354
1449
|
},
|
|
1355
1450
|
async get(id, expectedTable) {
|
|
@@ -1382,18 +1477,19 @@ const createShardCtxDb = (options) => {
|
|
|
1382
1477
|
}
|
|
1383
1478
|
const definition = schema.tables[tableName];
|
|
1384
1479
|
if (!definition) {
|
|
1385
|
-
throw new
|
|
1480
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1386
1481
|
}
|
|
1387
1482
|
onRead(tableName, SCAN_DEP);
|
|
1388
1483
|
const agg = groupOptions.agg ?? { op: "count" };
|
|
1389
1484
|
aggregateSqlFunction(agg.op);
|
|
1390
1485
|
if (agg.op !== "count" && !agg.field) {
|
|
1391
|
-
throw new
|
|
1486
|
+
throw new LunoraError("INTERNAL", `groupBy(${tableName}, { agg: { op: "${agg.op}" } }): "field" is required for non-count reducers`);
|
|
1392
1487
|
}
|
|
1393
|
-
const
|
|
1488
|
+
const groupScope = softDeleteScope(definition.softDeleteMode, void 0);
|
|
1489
|
+
const effective = mergeWhere(mergeWhere(groupOptions.baseWhere, groupOptions.where), groupScope);
|
|
1394
1490
|
const resolved = await resolveAggregateRelations(effective, tableName, groupOptions.relationBaseWhere);
|
|
1395
1491
|
const hasRelation = resolved !== effective;
|
|
1396
|
-
if (definition.aggregateIndexes && !groupOptions.baseWhere && !hasRelation) {
|
|
1492
|
+
if (definition.aggregateIndexes && !groupOptions.baseWhere && !hasRelation && !groupScope) {
|
|
1397
1493
|
const planned = selectIndexForGroupBy(definition.aggregateIndexes, agg.op, agg.field, groupOptions.by, groupOptions.where);
|
|
1398
1494
|
if (planned) {
|
|
1399
1495
|
ensureBackfilled(tableName, planned.index);
|
|
@@ -1432,7 +1528,7 @@ const createShardCtxDb = (options) => {
|
|
|
1432
1528
|
} else {
|
|
1433
1529
|
const { field } = agg;
|
|
1434
1530
|
if (field === void 0) {
|
|
1435
|
-
throw new
|
|
1531
|
+
throw new LunoraError("INTERNAL", `groupBy(${tableName}, { agg: { op: "${agg.op}" } }): "field" is required for non-count reducers`);
|
|
1436
1532
|
}
|
|
1437
1533
|
select.push(sql`${sql.raw(aggregateSqlFunction(agg.op))}(${jsonPathSql(field)}) AS value`);
|
|
1438
1534
|
}
|
|
@@ -1465,7 +1561,7 @@ const createShardCtxDb = (options) => {
|
|
|
1465
1561
|
}
|
|
1466
1562
|
const definition = schema.tables[tableName];
|
|
1467
1563
|
if (!definition) {
|
|
1468
|
-
throw new
|
|
1564
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1469
1565
|
}
|
|
1470
1566
|
const withDefaults = applyInsertDefaults(definition, document, auth);
|
|
1471
1567
|
runRowValidators(definition, withDefaults);
|
|
@@ -1478,7 +1574,7 @@ const createShardCtxDb = (options) => {
|
|
|
1478
1574
|
} else {
|
|
1479
1575
|
id = generateId();
|
|
1480
1576
|
}
|
|
1481
|
-
const creationTime = typeof withDefaults["_creationTime"] === "number" ? withDefaults["_creationTime"] : clock();
|
|
1577
|
+
const creationTime = insertOptions?.allowExplicitId && typeof withDefaults["_creationTime"] === "number" ? withDefaults["_creationTime"] : clock();
|
|
1482
1578
|
const documentWithMeta = { ...withDefaults, _creationTime: creationTime, _id: id };
|
|
1483
1579
|
if (hasMatchingTrigger(tableName, "before", "insert")) {
|
|
1484
1580
|
await fireTriggers("before", "insert", { doc: { ...documentWithMeta }, id, op: "insert", table: tableName });
|
|
@@ -1514,14 +1610,14 @@ const createShardCtxDb = (options) => {
|
|
|
1514
1610
|
}
|
|
1515
1611
|
const definition = schema.tables[tableName];
|
|
1516
1612
|
if (!definition) {
|
|
1517
|
-
throw new
|
|
1613
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1518
1614
|
}
|
|
1519
1615
|
ensureBackfilledForTable(tableName);
|
|
1520
1616
|
ensureRankBackfilledForTable(tableName);
|
|
1521
1617
|
const rows = documents.map((document) => {
|
|
1522
1618
|
const withDefaults = applyInsertDefaults(definition, document, auth);
|
|
1523
1619
|
const id = batchOptions?.allowExplicitId === true && typeof withDefaults["_id"] === "string" ? withDefaults["_id"] : generateId();
|
|
1524
|
-
const creationTime = typeof withDefaults["_creationTime"] === "number" ? withDefaults["_creationTime"] : clock();
|
|
1620
|
+
const creationTime = batchOptions?.allowExplicitId === true && typeof withDefaults["_creationTime"] === "number" ? withDefaults["_creationTime"] : clock();
|
|
1525
1621
|
return { creationTime, document: { ...withDefaults, _creationTime: creationTime, _id: id }, id };
|
|
1526
1622
|
});
|
|
1527
1623
|
const valuesSql = sql.join(
|
|
@@ -1537,9 +1633,18 @@ const createShardCtxDb = (options) => {
|
|
|
1537
1633
|
},
|
|
1538
1634
|
async insertMany(tableName, documents, batchOptions) {
|
|
1539
1635
|
assertBatchLimit(documents.length, batchOptions?.limit, "insertMany");
|
|
1636
|
+
const skipDuplicates = batchOptions?.skipDuplicates === true;
|
|
1540
1637
|
const ids = [];
|
|
1541
1638
|
for (const document of documents) {
|
|
1542
|
-
|
|
1639
|
+
try {
|
|
1640
|
+
ids.push(await writer.insert(tableName, document));
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
if (skipDuplicates && error instanceof ConflictError && error.kind === "unique") {
|
|
1643
|
+
ids.push(null);
|
|
1644
|
+
} else {
|
|
1645
|
+
throw error;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1543
1648
|
}
|
|
1544
1649
|
return ids;
|
|
1545
1650
|
},
|
|
@@ -1554,12 +1659,12 @@ const createShardCtxDb = (options) => {
|
|
|
1554
1659
|
await global.patch(id, patch);
|
|
1555
1660
|
return;
|
|
1556
1661
|
}
|
|
1557
|
-
throw new
|
|
1662
|
+
throw new LunoraError("INTERNAL", `document not found: ${id}`);
|
|
1558
1663
|
}
|
|
1559
1664
|
const { docJson: existingJson, row: existing, tableName } = located;
|
|
1560
1665
|
const tableDefinition = schema.tables[tableName];
|
|
1561
1666
|
if (!tableDefinition) {
|
|
1562
|
-
throw new
|
|
1667
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1563
1668
|
}
|
|
1564
1669
|
onRead(tableName, id);
|
|
1565
1670
|
assertNoExplicitUndefined("patch", patch);
|
|
@@ -1592,6 +1697,31 @@ const createShardCtxDb = (options) => {
|
|
|
1592
1697
|
for (const entry of patches) {
|
|
1593
1698
|
await writer.patch(entry.id, entry.patch, expectedTable);
|
|
1594
1699
|
}
|
|
1700
|
+
return { patched: patches.length };
|
|
1701
|
+
},
|
|
1702
|
+
async patchWhere(tableName, args, batchOptions) {
|
|
1703
|
+
const global = globalWriterFor(tableName, "patchWhere");
|
|
1704
|
+
let patches;
|
|
1705
|
+
if (global) {
|
|
1706
|
+
const rows = await global.findMany(tableName, { where: args.where });
|
|
1707
|
+
patches = rows.page.map((row) => {
|
|
1708
|
+
return { id: String(row["_id"]), patch: args.patch };
|
|
1709
|
+
});
|
|
1710
|
+
} else {
|
|
1711
|
+
if (!schema.tables[tableName]) {
|
|
1712
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1713
|
+
}
|
|
1714
|
+
const page = await writer.findMany(tableName, { where: args.where });
|
|
1715
|
+
patches = page.page.map((row) => {
|
|
1716
|
+
return { id: String(row["_id"]), patch: args.patch };
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
assertBatchLimit(patches.length, batchOptions?.limit, "patchWhere");
|
|
1720
|
+
if (writer.patchMany === void 0) {
|
|
1721
|
+
throw new LunoraError("INTERNAL", `ctx.db.${tableName}.patchMany is unavailable: this writer has no batch patch`);
|
|
1722
|
+
}
|
|
1723
|
+
await writer.patchMany(patches, batchOptions);
|
|
1724
|
+
return { patched: patches.length };
|
|
1595
1725
|
},
|
|
1596
1726
|
query(tableName) {
|
|
1597
1727
|
const global = globalWriterFor(tableName, "query");
|
|
@@ -1611,11 +1741,11 @@ const createShardCtxDb = (options) => {
|
|
|
1611
1741
|
onIndexUse(tableName, indexName, "rank");
|
|
1612
1742
|
const definition = schema.tables[tableName];
|
|
1613
1743
|
if (!definition) {
|
|
1614
|
-
throw new
|
|
1744
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1615
1745
|
}
|
|
1616
1746
|
const index = definition.rankIndexes?.find((i) => i.name === indexName);
|
|
1617
1747
|
if (!index) {
|
|
1618
|
-
throw new
|
|
1748
|
+
throw new LunoraError("INTERNAL", `unknown rankIndex "${indexName}" on table "${tableName}"`);
|
|
1619
1749
|
}
|
|
1620
1750
|
assertRankPartitionLocal(tableName, definition, index);
|
|
1621
1751
|
if (rankOptions.restrictsCounts) {
|
|
@@ -1656,17 +1786,18 @@ const createShardCtxDb = (options) => {
|
|
|
1656
1786
|
// eslint-disable-next-line @typescript-eslint/require-await -- DatabaseWriterLike returns Promises (the D1 twin awaits I/O); the body is synchronous SQLite
|
|
1657
1787
|
async rankBefore(tableName, indexName, rankBeforeOptions) {
|
|
1658
1788
|
if (isGlobalTable(tableName)) {
|
|
1659
|
-
throw new
|
|
1789
|
+
throw new LunoraError(
|
|
1790
|
+
"INTERNAL",
|
|
1660
1791
|
`rankBefore is not supported on the global (.global()) table '${tableName}' — cross-shard rank cursors apply only to sharded tables`
|
|
1661
1792
|
);
|
|
1662
1793
|
}
|
|
1663
1794
|
const definition = schema.tables[tableName];
|
|
1664
1795
|
if (!definition) {
|
|
1665
|
-
throw new
|
|
1796
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1666
1797
|
}
|
|
1667
1798
|
const index = definition.rankIndexes?.find((i) => i.name === indexName);
|
|
1668
1799
|
if (!index) {
|
|
1669
|
-
throw new
|
|
1800
|
+
throw new LunoraError("INTERNAL", `unknown rankIndex "${indexName}" on table "${tableName}"`);
|
|
1670
1801
|
}
|
|
1671
1802
|
if (rankBeforeOptions.restrictsCounts) {
|
|
1672
1803
|
throw new CountRlsUnsupportedError(tableName);
|
|
@@ -1696,23 +1827,43 @@ const createShardCtxDb = (options) => {
|
|
|
1696
1827
|
const { directions, hasMore, rows } = computeRankPage(rankPageDeps, tableName, indexName, rankPageOptions);
|
|
1697
1828
|
return { directions, hasMore, rows };
|
|
1698
1829
|
},
|
|
1699
|
-
async
|
|
1830
|
+
async restore(id, expectedTable) {
|
|
1831
|
+
const located = locateRowById(id, expectedTable);
|
|
1832
|
+
if (!located) {
|
|
1833
|
+
const global = expectedTable === void 0 ? globalFallback() : void 0;
|
|
1834
|
+
if (global?.restore) {
|
|
1835
|
+
await global.restore(id);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
throw new LunoraError("INTERNAL", `document not found: ${id}`);
|
|
1839
|
+
}
|
|
1840
|
+
const field = schema.tables[located.tableName]?.softDeleteMode?.field;
|
|
1841
|
+
if (!field) {
|
|
1842
|
+
throw new LunoraError("INTERNAL", `ctx.db.restore: table "${located.tableName}" is not a .softDelete() table`);
|
|
1843
|
+
}
|
|
1844
|
+
const wasDeleted = located.row[field] !== null && located.row[field] !== void 0;
|
|
1845
|
+
await writer.patch(id, { [field]: null }, expectedTable);
|
|
1846
|
+
if (wasDeleted) {
|
|
1847
|
+
syncRanks(located.tableName, id, void 0, located.row);
|
|
1848
|
+
}
|
|
1849
|
+
},
|
|
1850
|
+
async replace(id, document, expectedTable, replaceOptions) {
|
|
1700
1851
|
const located = locateRowById(id, expectedTable);
|
|
1701
1852
|
if (!located) {
|
|
1702
1853
|
const global = expectedTable === void 0 ? globalFallback() : void 0;
|
|
1703
1854
|
if (global) {
|
|
1704
|
-
await global.replace(id, document);
|
|
1855
|
+
await global.replace(id, document, void 0, replaceOptions);
|
|
1705
1856
|
return;
|
|
1706
1857
|
}
|
|
1707
|
-
throw new
|
|
1858
|
+
throw new LunoraError("INTERNAL", `document not found: ${id}`);
|
|
1708
1859
|
}
|
|
1709
1860
|
const { docJson: existingJson, row: previous, tableName } = located;
|
|
1710
1861
|
const tableDefinition = schema.tables[tableName];
|
|
1711
1862
|
if (!tableDefinition) {
|
|
1712
|
-
throw new
|
|
1863
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1713
1864
|
}
|
|
1714
1865
|
assertNoExplicitUndefined("replace", document);
|
|
1715
|
-
const creationTime = typeof document["_creationTime"] === "number" ? document["_creationTime"] : clock();
|
|
1866
|
+
const creationTime = replaceOptions?.allowExplicitId && typeof document["_creationTime"] === "number" ? document["_creationTime"] : clock();
|
|
1716
1867
|
const replaced = { ...document, _creationTime: creationTime, _id: id };
|
|
1717
1868
|
applyOnUpdate(tableDefinition, document, replaced, auth);
|
|
1718
1869
|
runRowValidators(tableDefinition, replaced);
|
|
@@ -1742,4 +1893,4 @@ const createShardCtxDb = (options) => {
|
|
|
1742
1893
|
return options.enforceRls === true ? guardWriter(writer, schema, (id, expectedTable) => locateRowById(id, expectedTable)?.tableName) : writer;
|
|
1743
1894
|
};
|
|
1744
1895
|
|
|
1745
|
-
export { NotUniqueError,
|
|
1896
|
+
export { NotUniqueError, assertValidClientId, createShardCtxDb, normalizeIdStructurally };
|