@lunora/do 1.0.0-alpha.4 → 1.0.0-alpha.41
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 +5236 -3449
- package/dist/index.d.ts +5236 -3449
- package/dist/index.mjs +34 -25
- package/dist/packem_shared/{ADMIN_FUNCTIONS-Dzdqq5J2.mjs → ADMIN_FUNCTIONS-CnZkbXH_.mjs} +69 -8
- package/dist/packem_shared/{AGGREGATE_SQL_FUNCTION-CFk6adSu.mjs → AGGREGATE_SQL_FUNCTION-CQsu2Xga.mjs} +5 -3
- package/dist/packem_shared/{CDC_LOG_TABLE-Ctdmxmrv.mjs → CDC_LOG_TABLE-uwOJxJJZ.mjs} +8 -4
- 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/{DEFAULT_MAX_RELATION_KEYS-DyVYReuT.mjs → DEFAULT_MAX_RELATION_KEYS-BEan1CRD.mjs} +58 -7
- package/dist/packem_shared/GEO_DEFAULT_PRECISION-BWnsNmpP.mjs +114 -0
- package/dist/packem_shared/{MAX_SQL_ROWS-dDcFE1YZ.mjs → MAX_SQL_ROWS-iFAA8FbD.mjs} +35 -3
- package/dist/packem_shared/NotFoundError-C70b9hLw.mjs +9 -0
- package/dist/packem_shared/{NotUniqueError-B6tzuDmM.mjs → NotUniqueError-DbtlWwcG.mjs} +400 -114
- package/dist/packem_shared/{rank-CrkEIpF4.mjs → RANK_TIEBREAK-CXhdcA1o.mjs} +2 -13
- package/dist/packem_shared/{RLS_UNWRAP_SYMBOL-u3UlnCH5.mjs → RLS_UNWRAP_SYMBOL-DTvHvRzY.mjs} +22 -9
- package/dist/packem_shared/ROOT_DO_SIZE_WARN_BYTES-B4Cpm7Qv.mjs +7653 -0
- package/dist/packem_shared/{ReactiveCache-ByVzgH3d.mjs → ReactiveCache-DnSvbjil.mjs} +4 -30
- 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-BbVPvciS.mjs → backfillAggregateIndexes-BGTwynwh.mjs} +4 -3
- 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/context-telemetry-CDpyil58.mjs +223 -0
- package/dist/packem_shared/createMetrics-C4I2Yanj.mjs +1 -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-DTeFiHYS.mjs +53 -0
- package/dist/packem_shared/diffExternalSource-CovHfdyo.mjs +44 -0
- package/dist/packem_shared/{do-sql-BCHCWtrD.mjs → do-sql-CGAgiQUz.mjs} +2 -1
- package/dist/packem_shared/{exportShardRows-DZEhUeyI.mjs → exportShardRows-Dy3oFZ26.mjs} +4 -3
- package/dist/packem_shared/isSoftDeleted-CFJmhjFP.mjs +217 -0
- package/dist/packem_shared/json-response-BdbtpOhm.mjs +3 -0
- package/dist/packem_shared/materializeExternalRows-BtEGs1Fv.mjs +45 -0
- package/dist/packem_shared/{runShardMigrations-PabobOjF.mjs → runShardMigrations-BVax6rYu.mjs} +21 -4
- package/dist/packem_shared/selectExpiredIds-C5W29Upb.mjs +18 -0
- package/dist/packem_shared/serialize-sql-BlRUoiQe.mjs +14 -0
- package/dist/packem_shared/{serveRelationFanout-CFBKWJ8Q.mjs → serveRelationFanout-k7HTh3CC.mjs} +4 -7
- package/dist/packem_shared/stableStringify-mC40mZts.mjs +40 -0
- package/dist/packem_shared/stableWireKey-DKuXO7T5.mjs +6 -0
- package/dist/packem_shared/subscriptionListDeltas-CT76bYny.mjs +126 -0
- package/dist/packem_shared/wire-codec-CzQc1pvf.mjs +225 -0
- package/package.json +5 -3
- package/dist/packem_shared/NotFoundError-CMuMZt81.mjs +0 -10
- package/dist/packem_shared/RANK_TIEBREAK-C6blLR5K.mjs +0 -1
- package/dist/packem_shared/ROOT_DO_SIZE_WARN_BYTES-3lZ2yigq.mjs +0 -4003
- package/dist/packem_shared/ctx-db-idempotency-DkC9rP91.mjs +0 -35
|
@@ -1,27 +1,31 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
1
2
|
import { sql } from 'drizzle-orm';
|
|
2
|
-
import { matchesStaticWhere, aggregateSqlFunction, normalizeCountArgument, throwingScheduler } from './AGGREGATE_SQL_FUNCTION-
|
|
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 './CDC_LOG_TABLE-
|
|
6
|
-
export { CDC_LOG_TABLE, applyCdcChanges, bumpCdcEpoch, minCdcSeq, readCdcChanges, readCdcCursor, readCdcEpoch, trimCdcChanges } from './CDC_LOG_TABLE-
|
|
5
|
+
import { mergeWhere, CountRlsUnsupportedError, selectIndexForGroupBy, selectIndexForCount, selectIndexForAggregate } from './CountRlsUnsupportedError-BGxj0pgS.mjs';
|
|
6
|
+
import { appendCdcChange } from './CDC_LOG_TABLE-uwOJxJJZ.mjs';
|
|
7
|
+
export { CDC_LOG_TABLE, applyCdcChanges, bumpCdcEpoch, minCdcSeq, readCdcChanges, readCdcCursor, readCdcEpoch, trimCdcChanges } from './CDC_LOG_TABLE-uwOJxJJZ.mjs';
|
|
7
8
|
import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
|
|
8
|
-
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-
|
|
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, g as geoTableName, d as aggUpsertSql, j as jsonPathSql, q as quoteIdentifier, t as tableColumns, e as qualifiedJsonPathSql } from './do-sql-CGAgiQUz.mjs';
|
|
9
10
|
import { param } from './renderSql-D6eUcn2N.mjs';
|
|
10
|
-
import {
|
|
11
|
+
import { encodeGeohash, GEO_DEFAULT_PRECISION, coveringGeohashes, boundingBoxGeohashes, pointInBoundingBox, haversineMeters } from './GEO_DEFAULT_PRECISION-BWnsNmpP.mjs';
|
|
12
|
+
import { sortColumnName, matchesRankStaticWhere, encodePartitionKey, rankTableName, resolveRankPartition, RANK_TIEBREAK } from './RANK_TIEBREAK-CXhdcA1o.mjs';
|
|
11
13
|
import { stringifySearchText, ftsTableName, tokenizeSearch, buildFtsMatch, scoreDocument } from './buildFtsMatch-BLEMawrp.mjs';
|
|
14
|
+
import { s as serializeSqlValue } from './serialize-sql-BlRUoiQe.mjs';
|
|
12
15
|
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 './DEFAULT_MAX_RELATION_KEYS-
|
|
16
|
-
import { runRowValidators, resolveWith, applyOnDelete } from './applyOnDelete-
|
|
17
|
-
import { guardWriter } from './RLS_UNWRAP_SYMBOL-
|
|
18
|
-
import { createSystemReader } from './createSystemReader-
|
|
19
|
-
import { ConflictError } from './ConflictError-
|
|
16
|
+
import { decodeCursor, normalizeOrderKeys, buildSeekWhere, applySelect, encodeCursor, softDeleteScope, buildSeekBeforeWhere } from './applySelect-WQY8m62C.mjs';
|
|
17
|
+
import NotFoundError from './NotFoundError-C70b9hLw.mjs';
|
|
18
|
+
import { assertFlatPredicate, resolveRelationPredicates } from './DEFAULT_MAX_RELATION_KEYS-BEan1CRD.mjs';
|
|
19
|
+
import { runRowValidators, resolveWith, applyOnDelete, fanOutScalarCounts } from './applyOnDelete-BXSq3S70.mjs';
|
|
20
|
+
import { guardWriter } from './RLS_UNWRAP_SYMBOL-DTvHvRzY.mjs';
|
|
21
|
+
import { createSystemReader } from './createSystemReader-D12eNH13.mjs';
|
|
22
|
+
import { ConflictError } from './ConflictError-CLoq37xH.mjs';
|
|
20
23
|
import { runTriggers } from './hasTrigger-5N6_Fx0A.mjs';
|
|
21
|
-
import { compileWhereSql } from './compileWhereSql-
|
|
22
|
-
export { backfillAggregateIndexes, backfillRankIndexes } from './backfillAggregateIndexes-
|
|
23
|
-
export { I as IDEMPOTENCY_TABLE, r as readIdempotent, t as trimIdempotent, w as writeIdempotent } from './ctx-db-idempotency-
|
|
24
|
-
export { runShardMigrations } from './runShardMigrations-
|
|
24
|
+
import { compileWhereSql } from './compileWhereSql-DE6yfRcQ.mjs';
|
|
25
|
+
export { backfillAggregateIndexes, backfillRankIndexes } from './backfillAggregateIndexes-BGTwynwh.mjs';
|
|
26
|
+
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';
|
|
27
|
+
export { runShardMigrations } from './runShardMigrations-BVax6rYu.mjs';
|
|
28
|
+
export { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-DTeFiHYS.mjs';
|
|
25
29
|
|
|
26
30
|
const rankIndexFieldsUnchanged = (index, previous, next) => {
|
|
27
31
|
const fields = [...index.partitionBy ?? [], ...index.sortBy.map((key) => key.field), ...index.where ? Object.keys(index.where) : []];
|
|
@@ -311,8 +315,28 @@ const createCompanionSync = (deps) => {
|
|
|
311
315
|
}
|
|
312
316
|
}
|
|
313
317
|
};
|
|
318
|
+
const syncGeo = (tableName, id, document) => {
|
|
319
|
+
const indexes = schema.tables[tableName]?.geoIndexes;
|
|
320
|
+
if (!indexes || indexes.length === 0) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
for (const index of indexes) {
|
|
324
|
+
const geoTable = geoTableName(tableName, index.name);
|
|
325
|
+
runDrizzle(sql$1, sql`DELETE FROM ${sql.identifier(geoTable)} WHERE ${sql.identifier("__id__")} = ${id}`);
|
|
326
|
+
const point = document?.[index.field];
|
|
327
|
+
if (point !== null && typeof point === "object" && typeof point.lat === "number" && typeof point.lng === "number") {
|
|
328
|
+
const { lat, lng } = point;
|
|
329
|
+
const hash = encodeGeohash({ lat, lng }, index.precision ?? GEO_DEFAULT_PRECISION);
|
|
330
|
+
runDrizzle(
|
|
331
|
+
sql$1,
|
|
332
|
+
sql`INSERT INTO ${sql.identifier(geoTable)} (${sql.identifier("__id__")}, ${sql.identifier("__geohash__")}, ${sql.identifier("__lat__")}, ${sql.identifier("__lng__")}) VALUES (${id}, ${hash}, ${lat}, ${lng})`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
314
337
|
const syncCompanionsForInsert = (tableName, id, document) => {
|
|
315
338
|
syncSearch(tableName, id, document);
|
|
339
|
+
syncGeo(tableName, id, document);
|
|
316
340
|
syncAggregates(tableName, void 0, document);
|
|
317
341
|
syncRanks(tableName, id, void 0, document);
|
|
318
342
|
invalidateCache(tableName, id);
|
|
@@ -326,6 +350,7 @@ const createCompanionSync = (deps) => {
|
|
|
326
350
|
ensureRankBackfilledForTable,
|
|
327
351
|
syncAggregates,
|
|
328
352
|
syncCompanionsForInsert,
|
|
353
|
+
syncGeo,
|
|
329
354
|
syncRanks,
|
|
330
355
|
syncSearch
|
|
331
356
|
};
|
|
@@ -420,11 +445,11 @@ const computeRankPage = (deps, tableName, indexName, rankPageOptions) => {
|
|
|
420
445
|
const { assertRankPartitionLocal, ensureRankBackfilled, onRead, schema } = deps;
|
|
421
446
|
const definition = schema.tables[tableName];
|
|
422
447
|
if (!definition) {
|
|
423
|
-
throw new
|
|
448
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
424
449
|
}
|
|
425
450
|
const index = definition.rankIndexes?.find((i) => i.name === indexName);
|
|
426
451
|
if (!index) {
|
|
427
|
-
throw new
|
|
452
|
+
throw new LunoraError("INTERNAL", `unknown rankIndex "${indexName}" on table "${tableName}"`);
|
|
428
453
|
}
|
|
429
454
|
assertRankPartitionLocal(tableName, definition, index);
|
|
430
455
|
onRead(tableName, SCAN_DEP);
|
|
@@ -472,7 +497,7 @@ const computeRankPage = (deps, tableName, indexName, rankPageOptions) => {
|
|
|
472
497
|
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
498
|
const assertValidClientId = (clientId) => {
|
|
474
499
|
if (!CLIENT_ID_PATTERN.test(clientId)) {
|
|
475
|
-
throw new
|
|
500
|
+
throw new LunoraError("INTERNAL", `invalid clientId ${JSON.stringify(clientId)}: a client-supplied row id must be a UUID`);
|
|
476
501
|
}
|
|
477
502
|
};
|
|
478
503
|
const MAX_TRIGGER_DEPTH = 50;
|
|
@@ -480,11 +505,11 @@ const DEFAULT_BATCH_LIMIT = 500;
|
|
|
480
505
|
const assertBatchLimit = (count, limit, op) => {
|
|
481
506
|
const cap = limit ?? DEFAULT_BATCH_LIMIT;
|
|
482
507
|
if (count > cap) {
|
|
483
|
-
throw
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
status: 400
|
|
487
|
-
|
|
508
|
+
throw new LunoraError(
|
|
509
|
+
"BATCH_LIMIT_EXCEEDED",
|
|
510
|
+
`${op}: batch of ${String(count)} exceeds the limit of ${String(cap)} (raise options.limit or chunk the call)`,
|
|
511
|
+
{ status: 400 }
|
|
512
|
+
);
|
|
488
513
|
}
|
|
489
514
|
};
|
|
490
515
|
const createRangeBuilder = (stage) => {
|
|
@@ -516,14 +541,17 @@ const createSearchBuilder = (search, tableName) => {
|
|
|
516
541
|
const builder = {
|
|
517
542
|
eq: (field, value) => {
|
|
518
543
|
if (!search.definition.filterFields?.includes(field)) {
|
|
519
|
-
throw new
|
|
544
|
+
throw new LunoraError("INTERNAL", `field "${field}" is not a filter field of search index "${search.indexName}" on table "${tableName}"`);
|
|
520
545
|
}
|
|
521
546
|
search.filters.push({ field, value });
|
|
522
547
|
return builder;
|
|
523
548
|
},
|
|
524
549
|
search: (field, query) => {
|
|
525
550
|
if (field !== search.definition.field) {
|
|
526
|
-
throw new
|
|
551
|
+
throw new LunoraError(
|
|
552
|
+
"INTERNAL",
|
|
553
|
+
`search index "${search.indexName}" on table "${tableName}" indexes "${search.definition.field}", not "${field}"`
|
|
554
|
+
);
|
|
527
555
|
}
|
|
528
556
|
const stage = search;
|
|
529
557
|
stage.field = field;
|
|
@@ -534,7 +562,7 @@ const createSearchBuilder = (search, tableName) => {
|
|
|
534
562
|
};
|
|
535
563
|
return builder;
|
|
536
564
|
};
|
|
537
|
-
const searchViaFts = (sql$1, tableName, search, limit) => {
|
|
565
|
+
const searchViaFts = (sql$1, tableName, search, limit, scopeCondition) => {
|
|
538
566
|
const tokens = tokenizeSearch(search.query);
|
|
539
567
|
if (tokens.length === 0) {
|
|
540
568
|
return [];
|
|
@@ -544,6 +572,9 @@ const searchViaFts = (sql$1, tableName, search, limit) => {
|
|
|
544
572
|
for (const filter of search.filters) {
|
|
545
573
|
whereClauses.push(sql`${jsonPathSql(filter.field)} = ${serializeSqlValue(filter.value)}`);
|
|
546
574
|
}
|
|
575
|
+
if (scopeCondition) {
|
|
576
|
+
whereClauses.push(scopeCondition);
|
|
577
|
+
}
|
|
547
578
|
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
579
|
if (typeof limit === "number") {
|
|
549
580
|
query = sql`${query} LIMIT ${sql.raw(String(Math.max(0, Math.floor(limit))))}`;
|
|
@@ -558,7 +589,7 @@ const searchViaFts = (sql$1, tableName, search, limit) => {
|
|
|
558
589
|
}
|
|
559
590
|
return docs;
|
|
560
591
|
};
|
|
561
|
-
const searchViaScan = (sql$1, tableName, search, limit) => {
|
|
592
|
+
const searchViaScan = (sql$1, tableName, search, limit, scopeCondition) => {
|
|
562
593
|
const tokens = tokenizeSearch(search.query);
|
|
563
594
|
if (tokens.length === 0) {
|
|
564
595
|
return [];
|
|
@@ -567,6 +598,9 @@ const searchViaScan = (sql$1, tableName, search, limit) => {
|
|
|
567
598
|
for (const filter of search.filters) {
|
|
568
599
|
whereClauses.push(sql`${jsonPathSql(filter.field)} = ${serializeSqlValue(filter.value)}`);
|
|
569
600
|
}
|
|
601
|
+
if (scopeCondition) {
|
|
602
|
+
whereClauses.push(scopeCondition);
|
|
603
|
+
}
|
|
570
604
|
let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
|
|
571
605
|
if (whereClauses.length > 0) {
|
|
572
606
|
query = sql`${query} WHERE ${sql.join(whereClauses, sql` AND `)}`;
|
|
@@ -587,6 +621,123 @@ const searchViaScan = (sql$1, tableName, search, limit) => {
|
|
|
587
621
|
const docs = scored.map((entry) => entry.doc);
|
|
588
622
|
return typeof limit === "number" ? docs.slice(0, Math.max(0, Math.floor(limit))) : docs;
|
|
589
623
|
};
|
|
624
|
+
const createGeoBuilder = (geo, tableName) => {
|
|
625
|
+
const staged = geo;
|
|
626
|
+
const builder = {
|
|
627
|
+
near: (point, radiusMeters) => {
|
|
628
|
+
if (staged.within) {
|
|
629
|
+
throw new LunoraError("INTERNAL", `geo index "${staged.indexName}" on table "${tableName}": call .near() or .within(), not both`);
|
|
630
|
+
}
|
|
631
|
+
staged.near = { point: { lat: point.lat, lng: point.lng }, radiusMeters };
|
|
632
|
+
return builder;
|
|
633
|
+
},
|
|
634
|
+
within: (box) => {
|
|
635
|
+
if (staged.near) {
|
|
636
|
+
throw new LunoraError("INTERNAL", `geo index "${staged.indexName}" on table "${tableName}": call .near() or .within(), not both`);
|
|
637
|
+
}
|
|
638
|
+
staged.within = { ne: { lat: box.ne.lat, lng: box.ne.lng }, sw: { lat: box.sw.lat, lng: box.sw.lng } };
|
|
639
|
+
return builder;
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
return builder;
|
|
643
|
+
};
|
|
644
|
+
const readGeoPoint = (document, field) => {
|
|
645
|
+
const value = document[field];
|
|
646
|
+
if (value === null || typeof value !== "object") {
|
|
647
|
+
return void 0;
|
|
648
|
+
}
|
|
649
|
+
const { lat, lng } = value;
|
|
650
|
+
return typeof lat === "number" && typeof lng === "number" ? { lat, lng } : void 0;
|
|
651
|
+
};
|
|
652
|
+
const scoreGeoRow = (record, geo) => {
|
|
653
|
+
const point = readGeoPoint(record, geo.definition.field);
|
|
654
|
+
if (!point) {
|
|
655
|
+
return void 0;
|
|
656
|
+
}
|
|
657
|
+
const creationTime = typeof record["_creationTime"] === "number" ? record["_creationTime"] : 0;
|
|
658
|
+
if (geo.near) {
|
|
659
|
+
const distance = haversineMeters(geo.near.point, point);
|
|
660
|
+
return distance <= geo.near.radiusMeters ? { creationTime, distance } : void 0;
|
|
661
|
+
}
|
|
662
|
+
return pointInBoundingBox(point, geo.within) ? { creationTime, distance: 0 } : void 0;
|
|
663
|
+
};
|
|
664
|
+
const runGeoFetch = (sql$1, tableName, geo, limit, scopeCondition) => {
|
|
665
|
+
if (!geo.near && !geo.within) {
|
|
666
|
+
throw new LunoraError("INTERNAL", `geo index "${geo.indexName}" on table "${tableName}": call .near(point, radius) or .within(box)`);
|
|
667
|
+
}
|
|
668
|
+
const prefixes = geo.near ? coveringGeohashes(geo.near.point, geo.near.radiusMeters) : boundingBoxGeohashes(geo.within);
|
|
669
|
+
const geoTable = geoTableName(tableName, geo.indexName);
|
|
670
|
+
const prefixClauses = prefixes.map(
|
|
671
|
+
(prefix) => sql`(g.${sql.identifier("__geohash__")} >= ${prefix} AND g.${sql.identifier("__geohash__")} < ${`${prefix}{`})`
|
|
672
|
+
);
|
|
673
|
+
const whereClauses = [sql`(${sql.join(prefixClauses, sql` OR `)})`];
|
|
674
|
+
if (scopeCondition) {
|
|
675
|
+
whereClauses.push(scopeCondition);
|
|
676
|
+
}
|
|
677
|
+
const query = sql`SELECT m.id, m._creationTime, m.${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(geoTable)} g JOIN ${sql.identifier(tableName)} m ON m.id = g.${sql.identifier("__id__")} WHERE ${sql.join(whereClauses, sql` AND `)}`;
|
|
678
|
+
const rows = runDrizzle(sql$1, query).toArray();
|
|
679
|
+
const scored = [];
|
|
680
|
+
for (const row of rows) {
|
|
681
|
+
const record = rowToDocument(row);
|
|
682
|
+
const score = record ? scoreGeoRow(record, geo) : void 0;
|
|
683
|
+
if (record && score) {
|
|
684
|
+
scored.push({ creationTime: score.creationTime, distance: score.distance, doc: record });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
scored.sort((a, b) => a.distance - b.distance || b.creationTime - a.creationTime);
|
|
688
|
+
const docs = scored.map((entry) => entry.doc);
|
|
689
|
+
return typeof limit === "number" ? docs.slice(0, Math.max(0, Math.floor(limit))) : docs;
|
|
690
|
+
};
|
|
691
|
+
const runGeoTerminal = (sql, tableName, stage, scopeCondition, limit) => {
|
|
692
|
+
const { geo } = stage;
|
|
693
|
+
if (!geo) {
|
|
694
|
+
throw new LunoraError("INTERNAL", "runGeoTerminal called without a staged geo query");
|
|
695
|
+
}
|
|
696
|
+
const filtered = stage.inMemoryFilters.length > 0;
|
|
697
|
+
const docs = runGeoFetch(sql, tableName, geo, filtered ? void 0 : limit, scopeCondition);
|
|
698
|
+
if (!filtered) {
|
|
699
|
+
return docs;
|
|
700
|
+
}
|
|
701
|
+
const result = [];
|
|
702
|
+
for (const record of docs) {
|
|
703
|
+
if (stage.inMemoryFilters.every((predicate) => predicate(record))) {
|
|
704
|
+
result.push(record);
|
|
705
|
+
if (typeof limit === "number" && result.length >= limit) {
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return result;
|
|
711
|
+
};
|
|
712
|
+
const runPlainFetch = (sql$1, tableName, stage, scopeCondition, orderClause, limit) => {
|
|
713
|
+
const whereClauses = [];
|
|
714
|
+
for (const condition of stage.sqlConditions) {
|
|
715
|
+
whereClauses.push(sql`${jsonPathSql(condition.field)} ${sql.raw(condition.comparator)} ${serializeSqlValue(condition.value)}`);
|
|
716
|
+
}
|
|
717
|
+
if (scopeCondition) {
|
|
718
|
+
whereClauses.push(scopeCondition);
|
|
719
|
+
}
|
|
720
|
+
let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
|
|
721
|
+
if (whereClauses.length > 0) {
|
|
722
|
+
query = sql`${query} WHERE ${sql.join(whereClauses, sql` AND `)}`;
|
|
723
|
+
}
|
|
724
|
+
query = sql`${query} ORDER BY ${orderClause}`;
|
|
725
|
+
if (typeof limit === "number" && stage.inMemoryFilters.length === 0) {
|
|
726
|
+
query = sql`${query} LIMIT ${sql.raw(String(Math.max(0, Math.floor(limit))))}`;
|
|
727
|
+
}
|
|
728
|
+
const rows = runDrizzle(sql$1, query).toArray();
|
|
729
|
+
const docs = [];
|
|
730
|
+
for (const row of rows) {
|
|
731
|
+
const record = rowToDocument(row);
|
|
732
|
+
if (record && stage.inMemoryFilters.every((predicate) => predicate(record))) {
|
|
733
|
+
docs.push(record);
|
|
734
|
+
if (typeof limit === "number" && docs.length >= limit) {
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return docs;
|
|
740
|
+
};
|
|
590
741
|
const doWhereSqlStrategy = { fieldRef: jsonPathSql, serialize: serializeSqlValue };
|
|
591
742
|
const makeRelationExistsSqlStrategy = (onRead) => {
|
|
592
743
|
let aliasCounter = 0;
|
|
@@ -660,11 +811,12 @@ const scanDocs = (rows, filters, cap) => {
|
|
|
660
811
|
}
|
|
661
812
|
return docs;
|
|
662
813
|
};
|
|
663
|
-
const paginateStage = (sql$1, tableName, stage, options) => {
|
|
814
|
+
const paginateStage = (sql$1, tableName, stage, options, scopeCondition) => {
|
|
664
815
|
const numberItems = Math.max(0, Math.floor(options.numItems));
|
|
665
816
|
const orderKeys = paginateOrderKeys(stage);
|
|
666
817
|
const bounded = typeof options.endCursor === "string";
|
|
667
|
-
const
|
|
818
|
+
const pageWhere = compileWhereSql(paginateWhere(stage, orderKeys, options.cursor, options.endCursor), doWhereSqlStrategy);
|
|
819
|
+
const whereCondition = scopeCondition && pageWhere ? sql`${pageWhere} AND ${scopeCondition}` : scopeCondition ?? pageWhere;
|
|
668
820
|
let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
|
|
669
821
|
if (whereCondition) {
|
|
670
822
|
query = sql`${query} WHERE ${whereCondition}`;
|
|
@@ -697,19 +849,16 @@ const paginateStage = (sql$1, tableName, stage, options) => {
|
|
|
697
849
|
page
|
|
698
850
|
};
|
|
699
851
|
};
|
|
700
|
-
class NotUniqueError extends
|
|
701
|
-
code = "NOT_UNIQUE";
|
|
702
|
-
status = 400;
|
|
852
|
+
class NotUniqueError extends LunoraError {
|
|
703
853
|
constructor(message = "unique() found more than one matching document") {
|
|
704
|
-
super(message);
|
|
705
|
-
this.name = "NotUniqueError";
|
|
854
|
+
super("NOT_UNIQUE", message, { name: "NotUniqueError" });
|
|
706
855
|
}
|
|
707
856
|
}
|
|
708
857
|
const ID_WHITESPACE_PATTERN = /\s/u;
|
|
709
858
|
const NUL_CHARACTER = String.fromCodePoint(0);
|
|
710
859
|
const normalizeIdStructurally = (schema, tableName, id) => {
|
|
711
860
|
if (!schema.tables[tableName]) {
|
|
712
|
-
throw new
|
|
861
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
713
862
|
}
|
|
714
863
|
if (typeof id !== "string" || id.length === 0 || ID_WHITESPACE_PATTERN.test(id) || id.includes(NUL_CHARACTER)) {
|
|
715
864
|
return null;
|
|
@@ -719,8 +868,10 @@ const normalizeIdStructurally = (schema, tableName, id) => {
|
|
|
719
868
|
const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
|
|
720
869
|
const tableDefinition = schema.tables[tableName];
|
|
721
870
|
if (!tableDefinition) {
|
|
722
|
-
throw new
|
|
871
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
723
872
|
}
|
|
873
|
+
const scopeWhere = softDeleteScope(tableDefinition.softDeleteMode, void 0);
|
|
874
|
+
const scopeCondition = scopeWhere ? compileWhereSql(scopeWhere, doWhereSqlStrategy) : void 0;
|
|
724
875
|
const stage = {
|
|
725
876
|
indexFields: [],
|
|
726
877
|
indexName: void 0,
|
|
@@ -731,11 +882,11 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
|
|
|
731
882
|
const runSearchFetch = (limit) => {
|
|
732
883
|
const { search } = stage;
|
|
733
884
|
if (!search) {
|
|
734
|
-
throw new
|
|
885
|
+
throw new LunoraError("INTERNAL", "runSearchFetch called without a staged search");
|
|
735
886
|
}
|
|
736
887
|
const filtered = stage.inMemoryFilters.length > 0;
|
|
737
888
|
const engineLimit = filtered ? void 0 : limit;
|
|
738
|
-
const docs = isFtsAvailable(sql$1) ? searchViaFts(sql$1, tableName, search, engineLimit) : searchViaScan(sql$1, tableName, search, engineLimit);
|
|
889
|
+
const docs = isFtsAvailable(sql$1) ? searchViaFts(sql$1, tableName, search, engineLimit, scopeCondition) : searchViaScan(sql$1, tableName, search, engineLimit, scopeCondition);
|
|
739
890
|
if (!filtered) {
|
|
740
891
|
return docs;
|
|
741
892
|
}
|
|
@@ -762,33 +913,10 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
|
|
|
762
913
|
if (stage.search) {
|
|
763
914
|
return runSearchFetch(limit);
|
|
764
915
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
whereClauses.push(sql`${jsonPathSql(condition.field)} ${sql.raw(condition.comparator)} ${serializeSqlValue(condition.value)}`);
|
|
768
|
-
}
|
|
769
|
-
let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
|
|
770
|
-
if (whereClauses.length > 0) {
|
|
771
|
-
query = sql`${query} WHERE ${sql.join(whereClauses, sql` AND `)}`;
|
|
772
|
-
}
|
|
773
|
-
query = sql`${query} ORDER BY ${buildOrderClause()}`;
|
|
774
|
-
if (typeof limit === "number" && stage.inMemoryFilters.length === 0) {
|
|
775
|
-
query = sql`${query} LIMIT ${sql.raw(String(Math.max(0, Math.floor(limit))))}`;
|
|
916
|
+
if (stage.geo) {
|
|
917
|
+
return runGeoTerminal(sql$1, tableName, stage, scopeCondition, limit);
|
|
776
918
|
}
|
|
777
|
-
|
|
778
|
-
const docs = [];
|
|
779
|
-
for (const row of rows) {
|
|
780
|
-
const record = rowToDocument(row);
|
|
781
|
-
if (!record) {
|
|
782
|
-
continue;
|
|
783
|
-
}
|
|
784
|
-
if (stage.inMemoryFilters.every((predicate) => predicate(record))) {
|
|
785
|
-
docs.push(record);
|
|
786
|
-
if (typeof limit === "number" && docs.length >= limit) {
|
|
787
|
-
break;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
return docs;
|
|
919
|
+
return runPlainFetch(sql$1, tableName, stage, scopeCondition, buildOrderClause(), limit);
|
|
792
920
|
};
|
|
793
921
|
const reader = {
|
|
794
922
|
// 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
|
|
@@ -811,9 +939,12 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
|
|
|
811
939
|
// 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
940
|
async paginate(options) {
|
|
813
941
|
if (stage.search) {
|
|
814
|
-
throw new
|
|
942
|
+
throw new LunoraError("INTERNAL", "pagination is not supported on search queries; use .take(n) or .collect()");
|
|
815
943
|
}
|
|
816
|
-
|
|
944
|
+
if (stage.geo) {
|
|
945
|
+
throw new LunoraError("INTERNAL", "pagination is not supported on geo queries; use .take(n) or .collect()");
|
|
946
|
+
}
|
|
947
|
+
return paginateStage(sql$1, tableName, stage, options, scopeCondition);
|
|
817
948
|
},
|
|
818
949
|
// 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
950
|
async take(limit) {
|
|
@@ -827,10 +958,24 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
|
|
|
827
958
|
}
|
|
828
959
|
return rows[0] ?? null;
|
|
829
960
|
},
|
|
961
|
+
withGeoIndex(indexName, build) {
|
|
962
|
+
const definition = (tableDefinition.geoIndexes ?? []).find((index) => index.name === indexName);
|
|
963
|
+
if (!definition) {
|
|
964
|
+
throw new LunoraError("INTERNAL", `unknown geo index "${indexName}" on table "${tableName}"`);
|
|
965
|
+
}
|
|
966
|
+
onIndexUse(tableName, indexName, "geo");
|
|
967
|
+
const geoStage = { definition, indexName };
|
|
968
|
+
stage.geo = geoStage;
|
|
969
|
+
build(createGeoBuilder(geoStage, tableName));
|
|
970
|
+
if (!geoStage.near && !geoStage.within) {
|
|
971
|
+
throw new LunoraError("INTERNAL", `geo index "${indexName}" on table "${tableName}" requires a .near(point, radius) or .within(box) call`);
|
|
972
|
+
}
|
|
973
|
+
return reader;
|
|
974
|
+
},
|
|
830
975
|
withIndex(indexName, range) {
|
|
831
976
|
const definition = tableDefinition.indexes.find((index) => index.name === indexName);
|
|
832
977
|
if (!definition) {
|
|
833
|
-
throw new
|
|
978
|
+
throw new LunoraError("INTERNAL", `unknown index "${indexName}" on table "${tableName}"`);
|
|
834
979
|
}
|
|
835
980
|
onIndexUse(tableName, indexName, "index");
|
|
836
981
|
stage.indexName = indexName;
|
|
@@ -843,7 +988,7 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
|
|
|
843
988
|
withSearchIndex(indexName, search) {
|
|
844
989
|
const definition = (tableDefinition.searchIndexes ?? []).find((index) => index.name === indexName);
|
|
845
990
|
if (!definition) {
|
|
846
|
-
throw new
|
|
991
|
+
throw new LunoraError("INTERNAL", `unknown search index "${indexName}" on table "${tableName}"`);
|
|
847
992
|
}
|
|
848
993
|
onIndexUse(tableName, indexName, "search");
|
|
849
994
|
const searchStage = {
|
|
@@ -857,7 +1002,7 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
|
|
|
857
1002
|
stage.search = searchStage;
|
|
858
1003
|
search(createSearchBuilder(searchStage, tableName));
|
|
859
1004
|
if (!searchStage.hasQuery) {
|
|
860
|
-
throw new
|
|
1005
|
+
throw new LunoraError("INTERNAL", `search index "${indexName}" on table "${tableName}" requires a .search(field, query) call`);
|
|
861
1006
|
}
|
|
862
1007
|
return reader;
|
|
863
1008
|
}
|
|
@@ -899,7 +1044,10 @@ const applyOnUpdate = (definition, provided, target, auth) => {
|
|
|
899
1044
|
const assertNoExplicitUndefined = (op, document) => {
|
|
900
1045
|
for (const field of Object.keys(document)) {
|
|
901
1046
|
if (document[field] === void 0) {
|
|
902
|
-
throw new
|
|
1047
|
+
throw new LunoraError(
|
|
1048
|
+
"INTERNAL",
|
|
1049
|
+
`Cannot ${op} field '${field}' to undefined — use null to clear a nullable field, or omit the key to leave it unchanged.`
|
|
1050
|
+
);
|
|
903
1051
|
}
|
|
904
1052
|
}
|
|
905
1053
|
};
|
|
@@ -979,7 +1127,10 @@ const createShardCtxDb = (options) => {
|
|
|
979
1127
|
const routeBackend = (table, op) => {
|
|
980
1128
|
if (isGlobalTable(table)) {
|
|
981
1129
|
if (!globalDb) {
|
|
982
|
-
throw new
|
|
1130
|
+
throw new LunoraError(
|
|
1131
|
+
"INTERNAL",
|
|
1132
|
+
`cross-backend ${op} for global table '${table}' requires a globalDb writer — pass one to createShardCtxDb({ globalDb })`
|
|
1133
|
+
);
|
|
983
1134
|
}
|
|
984
1135
|
return globalDb;
|
|
985
1136
|
}
|
|
@@ -991,13 +1142,12 @@ const createShardCtxDb = (options) => {
|
|
|
991
1142
|
return void 0;
|
|
992
1143
|
}
|
|
993
1144
|
if (!globalDb) {
|
|
994
|
-
throw new
|
|
1145
|
+
throw new LunoraError("INTERNAL", `${op} on global table '${tableName}' requires a globalDb writer — pass one to createShardCtxDb({ globalDb })`);
|
|
995
1146
|
}
|
|
996
1147
|
return globalDb;
|
|
997
1148
|
};
|
|
998
1149
|
const globalFallback = () => globalDb;
|
|
999
1150
|
const relationFetcher = (relationTable, relationArgs) => routeBackend(relationTable, "relation load").findMany(relationTable, relationArgs);
|
|
1000
|
-
const relationCounter = (relationTable, relationWhere) => routeBackend(relationTable, "relation load").count(relationTable, relationWhere);
|
|
1001
1151
|
const relationPredicateFetcher = (relationTable, relationArgs) => {
|
|
1002
1152
|
if (isGlobalTable(relationTable)) {
|
|
1003
1153
|
onRead(relationTable, SCAN_DEP);
|
|
@@ -1015,6 +1165,31 @@ const createShardCtxDb = (options) => {
|
|
|
1015
1165
|
schema,
|
|
1016
1166
|
tableName: predicateTable
|
|
1017
1167
|
});
|
|
1168
|
+
const relationGroupedCounter = async (relationTable, whereField, values, policyWhere) => {
|
|
1169
|
+
const globalWriter = globalWriterFor(relationTable, "relation grouped count");
|
|
1170
|
+
if (globalWriter) {
|
|
1171
|
+
onRead(relationTable, SCAN_DEP);
|
|
1172
|
+
return fanOutScalarCounts((t, w) => globalWriter.count(t, w), relationTable, whereField, values, policyWhere);
|
|
1173
|
+
}
|
|
1174
|
+
const definition = schema.tables[relationTable];
|
|
1175
|
+
if (!definition) {
|
|
1176
|
+
throw new LunoraError("INTERNAL", `unknown table: ${relationTable}`);
|
|
1177
|
+
}
|
|
1178
|
+
onRead(relationTable, SCAN_DEP);
|
|
1179
|
+
const softScope = softDeleteScope(definition.softDeleteMode, void 0);
|
|
1180
|
+
const inFilter = { [whereField]: { in: values } };
|
|
1181
|
+
const combined = mergeWhere(mergeWhere(inFilter, policyWhere), softScope);
|
|
1182
|
+
const resolvedCombined = await resolveAggregateRelations(combined, relationTable, void 0);
|
|
1183
|
+
const whereCondition = compileWhereSql(resolvedCombined, doWhereSqlStrategy);
|
|
1184
|
+
const fieldSql = jsonPathSql(whereField);
|
|
1185
|
+
let query = sql`SELECT ${fieldSql} AS __fk__, COUNT(*) AS count FROM ${sql.identifier(relationTable)}`;
|
|
1186
|
+
if (whereCondition) {
|
|
1187
|
+
query = sql`${query} WHERE ${whereCondition}`;
|
|
1188
|
+
}
|
|
1189
|
+
query = sql`${query} GROUP BY ${fieldSql}`;
|
|
1190
|
+
const rows = runDrizzle(sql$1, query).toArray();
|
|
1191
|
+
return new Map(rows.map((row) => [row["__fk__"], row.count]));
|
|
1192
|
+
};
|
|
1018
1193
|
let triggerDepth = 0;
|
|
1019
1194
|
const triggerMatchers = /* @__PURE__ */ new Set();
|
|
1020
1195
|
for (const [tableName, definition] of Object.entries(schema.tables)) {
|
|
@@ -1045,6 +1220,7 @@ const createShardCtxDb = (options) => {
|
|
|
1045
1220
|
ensureRankBackfilledForTable,
|
|
1046
1221
|
syncAggregates,
|
|
1047
1222
|
syncCompanionsForInsert,
|
|
1223
|
+
syncGeo,
|
|
1048
1224
|
syncRanks,
|
|
1049
1225
|
syncSearch
|
|
1050
1226
|
} = createCompanionSync({
|
|
@@ -1115,7 +1291,7 @@ const createShardCtxDb = (options) => {
|
|
|
1115
1291
|
}
|
|
1116
1292
|
const definition = schema.tables[tableName];
|
|
1117
1293
|
if (!definition) {
|
|
1118
|
-
throw new
|
|
1294
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1119
1295
|
}
|
|
1120
1296
|
aggregateSqlFunction(aggOptions.op);
|
|
1121
1297
|
if (aggOptions.op === "count") {
|
|
@@ -1127,13 +1303,14 @@ const createShardCtxDb = (options) => {
|
|
|
1127
1303
|
});
|
|
1128
1304
|
}
|
|
1129
1305
|
if (!aggOptions.field) {
|
|
1130
|
-
throw new
|
|
1306
|
+
throw new LunoraError("INTERNAL", `aggregate(${tableName}, { op: "${aggOptions.op}" }): "field" is required for non-count reducers`);
|
|
1131
1307
|
}
|
|
1132
1308
|
onRead(tableName, SCAN_DEP);
|
|
1133
|
-
const
|
|
1309
|
+
const aggScope = softDeleteScope(definition.softDeleteMode, void 0);
|
|
1310
|
+
const effective = mergeWhere(mergeWhere(aggOptions.baseWhere, aggOptions.where), aggScope);
|
|
1134
1311
|
const resolved = await resolveAggregateRelations(effective, tableName, aggOptions.relationBaseWhere);
|
|
1135
1312
|
const hasRelation = resolved !== effective;
|
|
1136
|
-
if (definition.aggregateIndexes && !aggOptions.baseWhere && !hasRelation) {
|
|
1313
|
+
if (definition.aggregateIndexes && !aggOptions.baseWhere && !hasRelation && !aggScope) {
|
|
1137
1314
|
const planned = selectIndexForAggregate(definition.aggregateIndexes, aggOptions.op, aggOptions.field, aggOptions.where);
|
|
1138
1315
|
if (planned) {
|
|
1139
1316
|
ensureBackfilled(tableName, planned.index);
|
|
@@ -1168,17 +1345,18 @@ const createShardCtxDb = (options) => {
|
|
|
1168
1345
|
}
|
|
1169
1346
|
const definition = schema.tables[tableName];
|
|
1170
1347
|
if (!definition) {
|
|
1171
|
-
throw new
|
|
1348
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1172
1349
|
}
|
|
1173
1350
|
const countOptions = normalizeCountArgument(whereOrOptions);
|
|
1174
1351
|
if (countOptions.restrictsCounts) {
|
|
1175
1352
|
throw new CountRlsUnsupportedError(tableName);
|
|
1176
1353
|
}
|
|
1177
1354
|
onRead(tableName, SCAN_DEP);
|
|
1178
|
-
const
|
|
1355
|
+
const countScope = softDeleteScope(definition.softDeleteMode, void 0);
|
|
1356
|
+
const effective = mergeWhere(mergeWhere(countOptions.baseWhere, countOptions.where), countScope);
|
|
1179
1357
|
const resolved = await resolveAggregateRelations(effective, tableName, countOptions.relationBaseWhere);
|
|
1180
1358
|
const hasRelation = resolved !== effective;
|
|
1181
|
-
if (definition.aggregateIndexes && !countOptions.baseWhere && !hasRelation) {
|
|
1359
|
+
if (definition.aggregateIndexes && !countOptions.baseWhere && !hasRelation && !countScope) {
|
|
1182
1360
|
const planned = selectIndexForCount(definition.aggregateIndexes, countOptions.where);
|
|
1183
1361
|
if (planned) {
|
|
1184
1362
|
ensureBackfilled(tableName, planned.index);
|
|
@@ -1199,16 +1377,22 @@ const createShardCtxDb = (options) => {
|
|
|
1199
1377
|
const row = runDrizzle(sql$1, query).one();
|
|
1200
1378
|
return row.count;
|
|
1201
1379
|
},
|
|
1202
|
-
async delete(id, expectedTable) {
|
|
1380
|
+
async delete(id, expectedTable, deleteOptions) {
|
|
1203
1381
|
const located = locateRowById(id, expectedTable);
|
|
1204
1382
|
if (!located) {
|
|
1205
1383
|
const global = expectedTable === void 0 ? globalFallback() : void 0;
|
|
1206
1384
|
if (global) {
|
|
1207
|
-
await global.delete(id);
|
|
1385
|
+
await global.delete(id, void 0, deleteOptions);
|
|
1208
1386
|
}
|
|
1209
1387
|
return;
|
|
1210
1388
|
}
|
|
1211
1389
|
const { docJson: existingJson, row: existing, tableName } = located;
|
|
1390
|
+
const definition = schema.tables[tableName];
|
|
1391
|
+
const hard = deleteOptions?.hard === true;
|
|
1392
|
+
const softField = !hard && definition?.softDeleteMode ? definition.softDeleteMode.field : void 0;
|
|
1393
|
+
if (softField && existing[softField] !== null && existing[softField] !== void 0) {
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1212
1396
|
if (hasMatchingTrigger(tableName, "before", "delete")) {
|
|
1213
1397
|
await fireTriggers("before", "delete", { id, op: "delete", previous: existing, table: tableName });
|
|
1214
1398
|
}
|
|
@@ -1216,10 +1400,10 @@ const createShardCtxDb = (options) => {
|
|
|
1216
1400
|
deletedId: id,
|
|
1217
1401
|
deletedReference: (references) => existing[references],
|
|
1218
1402
|
findHolders: async (holderTable, field, value) => {
|
|
1219
|
-
const holders = await routeForHolder(holderTable).findMany(holderTable, { where: { [field]: value } });
|
|
1403
|
+
const holders = await routeForHolder(holderTable).findMany(holderTable, { includeDeleted: hard, where: { [field]: value } });
|
|
1220
1404
|
return holders.page;
|
|
1221
1405
|
},
|
|
1222
|
-
onCascade: (holderTable, holderId) => routeForHolder(holderTable).delete(holderId),
|
|
1406
|
+
onCascade: (holderTable, holderId) => routeForHolder(holderTable).delete(holderId, void 0, deleteOptions),
|
|
1223
1407
|
onRestrict: (message) => {
|
|
1224
1408
|
throw new ConflictError(message, "restrict");
|
|
1225
1409
|
},
|
|
@@ -1230,12 +1414,33 @@ const createShardCtxDb = (options) => {
|
|
|
1230
1414
|
});
|
|
1231
1415
|
ensureBackfilledForTable(tableName);
|
|
1232
1416
|
ensureRankBackfilledForTable(tableName);
|
|
1417
|
+
if (softField) {
|
|
1418
|
+
const merged = { ...existing, [softField]: clock(), _id: id };
|
|
1419
|
+
runGuardedWrite(
|
|
1420
|
+
sql$1,
|
|
1421
|
+
tableName,
|
|
1422
|
+
sql`UPDATE ${sql.identifier(tableName)} SET ${sql.identifier(DOC_COLUMN$1)} = ${JSON.stringify(merged)} WHERE id = ${id} AND ${sql.identifier(DOC_COLUMN$1)} = ${existingJson}`
|
|
1423
|
+
);
|
|
1424
|
+
syncSearch(tableName, id, merged);
|
|
1425
|
+
syncGeo(tableName, id, void 0);
|
|
1426
|
+
syncAggregates(tableName, existing, merged);
|
|
1427
|
+
syncRanks(tableName, id, existing, void 0);
|
|
1428
|
+
cache?.invalidate(tableName, id);
|
|
1429
|
+
recordCdc(tableName, id, "update", merged);
|
|
1430
|
+
broadcast({ key: id, op: "update", row: merged, table: tableName });
|
|
1431
|
+
if (hasMatchingTrigger(tableName, "after", "delete")) {
|
|
1432
|
+
await fireTriggers("after", "delete", { id, op: "delete", previous: existing, table: tableName });
|
|
1433
|
+
}
|
|
1434
|
+
await onWrite({ id, op: "delete", table: tableName });
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1233
1437
|
runGuardedWrite(
|
|
1234
1438
|
sql$1,
|
|
1235
1439
|
tableName,
|
|
1236
1440
|
sql`DELETE FROM ${sql.identifier(tableName)} WHERE id = ${id} AND ${sql.identifier(DOC_COLUMN$1)} = ${existingJson}`
|
|
1237
1441
|
);
|
|
1238
1442
|
syncSearch(tableName, id, void 0);
|
|
1443
|
+
syncGeo(tableName, id, void 0);
|
|
1239
1444
|
syncAggregates(tableName, existing, void 0);
|
|
1240
1445
|
syncRanks(tableName, id, existing, void 0);
|
|
1241
1446
|
cache?.invalidate(tableName, id);
|
|
@@ -1253,6 +1458,25 @@ const createShardCtxDb = (options) => {
|
|
|
1253
1458
|
}
|
|
1254
1459
|
return { deleted: ids.length };
|
|
1255
1460
|
},
|
|
1461
|
+
async deleteWhere(tableName, where, batchOptions) {
|
|
1462
|
+
const global = globalWriterFor(tableName, "deleteWhere");
|
|
1463
|
+
let ids;
|
|
1464
|
+
if (global) {
|
|
1465
|
+
const rows = await global.findMany(tableName, { where });
|
|
1466
|
+
ids = rows.page.map((row) => String(row["_id"]));
|
|
1467
|
+
} else {
|
|
1468
|
+
if (!schema.tables[tableName]) {
|
|
1469
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1470
|
+
}
|
|
1471
|
+
const page = await writer.findMany(tableName, { where });
|
|
1472
|
+
ids = page.page.map((row) => String(row["_id"]));
|
|
1473
|
+
}
|
|
1474
|
+
assertBatchLimit(ids.length, batchOptions?.limit, "deleteWhere");
|
|
1475
|
+
if (writer.deleteMany === void 0) {
|
|
1476
|
+
throw new LunoraError("INTERNAL", `ctx.db.${tableName}.deleteMany is unavailable: this writer has no batch delete`);
|
|
1477
|
+
}
|
|
1478
|
+
return writer.deleteMany(ids, batchOptions);
|
|
1479
|
+
},
|
|
1256
1480
|
async findFirst(tableName, args = {}) {
|
|
1257
1481
|
const result = await writer.findMany(tableName, { ...args, limit: 1 });
|
|
1258
1482
|
return result.page[0] ?? null;
|
|
@@ -1271,8 +1495,9 @@ const createShardCtxDb = (options) => {
|
|
|
1271
1495
|
onRead(tableName, SCAN_DEP);
|
|
1272
1496
|
return global.findMany(tableName, args);
|
|
1273
1497
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1498
|
+
const findManyDefinition = schema.tables[tableName];
|
|
1499
|
+
if (!findManyDefinition) {
|
|
1500
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1276
1501
|
}
|
|
1277
1502
|
const isFullScan = !args.where && !args.baseWhere;
|
|
1278
1503
|
if (isFullScan) {
|
|
@@ -1283,6 +1508,7 @@ const createShardCtxDb = (options) => {
|
|
|
1283
1508
|
const orderKeys = normalizeOrderKeys(args.orderBy);
|
|
1284
1509
|
const seek = args.cursor ? buildSeekWhere(orderKeys, decodeCursor(args.cursor)) : void 0;
|
|
1285
1510
|
let predicate = mergeWhere(args.baseWhere, args.where);
|
|
1511
|
+
predicate = mergeWhere(predicate, softDeleteScope(findManyDefinition.softDeleteMode, args.includeDeleted));
|
|
1286
1512
|
predicate = await resolveRelationPredicates(predicate, {
|
|
1287
1513
|
canPushExists: relationExistsPushDownEnabled ? canPushRelationExists : void 0,
|
|
1288
1514
|
existsPushMode: relationExistsPushDown === "always" ? "always" : "auto",
|
|
@@ -1320,7 +1546,7 @@ const createShardCtxDb = (options) => {
|
|
|
1320
1546
|
if (limit === void 0) {
|
|
1321
1547
|
if (args.with) {
|
|
1322
1548
|
await resolveWith({
|
|
1323
|
-
|
|
1549
|
+
groupedCounter: relationGroupedCounter,
|
|
1324
1550
|
fetcher: relationFetcher,
|
|
1325
1551
|
parents: docs,
|
|
1326
1552
|
relationBaseWhere: args.relationBaseWhere,
|
|
@@ -1329,15 +1555,15 @@ const createShardCtxDb = (options) => {
|
|
|
1329
1555
|
with: args.with
|
|
1330
1556
|
});
|
|
1331
1557
|
}
|
|
1332
|
-
return { continueCursor: null, isDone: true, page: docs };
|
|
1558
|
+
return { continueCursor: null, isDone: true, page: applySelect(docs, args.select, args.with) };
|
|
1333
1559
|
}
|
|
1334
1560
|
const hasMore = docs.length > limit;
|
|
1335
1561
|
const page = hasMore ? docs.slice(0, limit) : docs;
|
|
1336
1562
|
const last = page.at(-1);
|
|
1337
1563
|
if (args.with) {
|
|
1338
1564
|
await resolveWith({
|
|
1339
|
-
counter: relationCounter,
|
|
1340
1565
|
fetcher: relationFetcher,
|
|
1566
|
+
groupedCounter: relationGroupedCounter,
|
|
1341
1567
|
parents: page,
|
|
1342
1568
|
relationBaseWhere: args.relationBaseWhere,
|
|
1343
1569
|
schema,
|
|
@@ -1346,10 +1572,12 @@ const createShardCtxDb = (options) => {
|
|
|
1346
1572
|
});
|
|
1347
1573
|
}
|
|
1348
1574
|
return {
|
|
1575
|
+
// The cursor is encoded from `last` (the full, unprojected row) above,
|
|
1576
|
+
// so `applySelect` only trims the returned payload — paging is intact.
|
|
1349
1577
|
// eslint-disable-next-line unicorn/no-null -- QueryPage.continueCursor is `null | string`: null is the documented "no further page" cursor on the wire
|
|
1350
1578
|
continueCursor: hasMore && last ? encodeCursor(last, orderKeys) : null,
|
|
1351
1579
|
isDone: !hasMore,
|
|
1352
|
-
page
|
|
1580
|
+
page: applySelect(page, args.select, args.with)
|
|
1353
1581
|
};
|
|
1354
1582
|
},
|
|
1355
1583
|
async get(id, expectedTable) {
|
|
@@ -1382,18 +1610,19 @@ const createShardCtxDb = (options) => {
|
|
|
1382
1610
|
}
|
|
1383
1611
|
const definition = schema.tables[tableName];
|
|
1384
1612
|
if (!definition) {
|
|
1385
|
-
throw new
|
|
1613
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1386
1614
|
}
|
|
1387
1615
|
onRead(tableName, SCAN_DEP);
|
|
1388
1616
|
const agg = groupOptions.agg ?? { op: "count" };
|
|
1389
1617
|
aggregateSqlFunction(agg.op);
|
|
1390
1618
|
if (agg.op !== "count" && !agg.field) {
|
|
1391
|
-
throw new
|
|
1619
|
+
throw new LunoraError("INTERNAL", `groupBy(${tableName}, { agg: { op: "${agg.op}" } }): "field" is required for non-count reducers`);
|
|
1392
1620
|
}
|
|
1393
|
-
const
|
|
1621
|
+
const groupScope = softDeleteScope(definition.softDeleteMode, void 0);
|
|
1622
|
+
const effective = mergeWhere(mergeWhere(groupOptions.baseWhere, groupOptions.where), groupScope);
|
|
1394
1623
|
const resolved = await resolveAggregateRelations(effective, tableName, groupOptions.relationBaseWhere);
|
|
1395
1624
|
const hasRelation = resolved !== effective;
|
|
1396
|
-
if (definition.aggregateIndexes && !groupOptions.baseWhere && !hasRelation) {
|
|
1625
|
+
if (definition.aggregateIndexes && !groupOptions.baseWhere && !hasRelation && !groupScope) {
|
|
1397
1626
|
const planned = selectIndexForGroupBy(definition.aggregateIndexes, agg.op, agg.field, groupOptions.by, groupOptions.where);
|
|
1398
1627
|
if (planned) {
|
|
1399
1628
|
ensureBackfilled(tableName, planned.index);
|
|
@@ -1432,7 +1661,7 @@ const createShardCtxDb = (options) => {
|
|
|
1432
1661
|
} else {
|
|
1433
1662
|
const { field } = agg;
|
|
1434
1663
|
if (field === void 0) {
|
|
1435
|
-
throw new
|
|
1664
|
+
throw new LunoraError("INTERNAL", `groupBy(${tableName}, { agg: { op: "${agg.op}" } }): "field" is required for non-count reducers`);
|
|
1436
1665
|
}
|
|
1437
1666
|
select.push(sql`${sql.raw(aggregateSqlFunction(agg.op))}(${jsonPathSql(field)}) AS value`);
|
|
1438
1667
|
}
|
|
@@ -1465,7 +1694,7 @@ const createShardCtxDb = (options) => {
|
|
|
1465
1694
|
}
|
|
1466
1695
|
const definition = schema.tables[tableName];
|
|
1467
1696
|
if (!definition) {
|
|
1468
|
-
throw new
|
|
1697
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1469
1698
|
}
|
|
1470
1699
|
const withDefaults = applyInsertDefaults(definition, document, auth);
|
|
1471
1700
|
runRowValidators(definition, withDefaults);
|
|
@@ -1478,7 +1707,7 @@ const createShardCtxDb = (options) => {
|
|
|
1478
1707
|
} else {
|
|
1479
1708
|
id = generateId();
|
|
1480
1709
|
}
|
|
1481
|
-
const creationTime = typeof withDefaults["_creationTime"] === "number" ? withDefaults["_creationTime"] : clock();
|
|
1710
|
+
const creationTime = insertOptions?.allowExplicitId && typeof withDefaults["_creationTime"] === "number" ? withDefaults["_creationTime"] : clock();
|
|
1482
1711
|
const documentWithMeta = { ...withDefaults, _creationTime: creationTime, _id: id };
|
|
1483
1712
|
if (hasMatchingTrigger(tableName, "before", "insert")) {
|
|
1484
1713
|
await fireTriggers("before", "insert", { doc: { ...documentWithMeta }, id, op: "insert", table: tableName });
|
|
@@ -1514,14 +1743,14 @@ const createShardCtxDb = (options) => {
|
|
|
1514
1743
|
}
|
|
1515
1744
|
const definition = schema.tables[tableName];
|
|
1516
1745
|
if (!definition) {
|
|
1517
|
-
throw new
|
|
1746
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1518
1747
|
}
|
|
1519
1748
|
ensureBackfilledForTable(tableName);
|
|
1520
1749
|
ensureRankBackfilledForTable(tableName);
|
|
1521
1750
|
const rows = documents.map((document) => {
|
|
1522
1751
|
const withDefaults = applyInsertDefaults(definition, document, auth);
|
|
1523
1752
|
const id = batchOptions?.allowExplicitId === true && typeof withDefaults["_id"] === "string" ? withDefaults["_id"] : generateId();
|
|
1524
|
-
const creationTime = typeof withDefaults["_creationTime"] === "number" ? withDefaults["_creationTime"] : clock();
|
|
1753
|
+
const creationTime = batchOptions?.allowExplicitId === true && typeof withDefaults["_creationTime"] === "number" ? withDefaults["_creationTime"] : clock();
|
|
1525
1754
|
return { creationTime, document: { ...withDefaults, _creationTime: creationTime, _id: id }, id };
|
|
1526
1755
|
});
|
|
1527
1756
|
const valuesSql = sql.join(
|
|
@@ -1537,9 +1766,18 @@ const createShardCtxDb = (options) => {
|
|
|
1537
1766
|
},
|
|
1538
1767
|
async insertMany(tableName, documents, batchOptions) {
|
|
1539
1768
|
assertBatchLimit(documents.length, batchOptions?.limit, "insertMany");
|
|
1769
|
+
const skipDuplicates = batchOptions?.skipDuplicates === true;
|
|
1540
1770
|
const ids = [];
|
|
1541
1771
|
for (const document of documents) {
|
|
1542
|
-
|
|
1772
|
+
try {
|
|
1773
|
+
ids.push(await writer.insert(tableName, document));
|
|
1774
|
+
} catch (error) {
|
|
1775
|
+
if (skipDuplicates && error instanceof ConflictError && error.kind === "unique") {
|
|
1776
|
+
ids.push(null);
|
|
1777
|
+
} else {
|
|
1778
|
+
throw error;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1543
1781
|
}
|
|
1544
1782
|
return ids;
|
|
1545
1783
|
},
|
|
@@ -1554,12 +1792,12 @@ const createShardCtxDb = (options) => {
|
|
|
1554
1792
|
await global.patch(id, patch);
|
|
1555
1793
|
return;
|
|
1556
1794
|
}
|
|
1557
|
-
throw new
|
|
1795
|
+
throw new LunoraError("INTERNAL", `document not found: ${id}`);
|
|
1558
1796
|
}
|
|
1559
1797
|
const { docJson: existingJson, row: existing, tableName } = located;
|
|
1560
1798
|
const tableDefinition = schema.tables[tableName];
|
|
1561
1799
|
if (!tableDefinition) {
|
|
1562
|
-
throw new
|
|
1800
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1563
1801
|
}
|
|
1564
1802
|
onRead(tableName, id);
|
|
1565
1803
|
assertNoExplicitUndefined("patch", patch);
|
|
@@ -1577,6 +1815,7 @@ const createShardCtxDb = (options) => {
|
|
|
1577
1815
|
sql`UPDATE ${sql.identifier(tableName)} SET ${sql.identifier(DOC_COLUMN$1)} = ${JSON.stringify(merged)} WHERE id = ${id} AND ${sql.identifier(DOC_COLUMN$1)} = ${existingJson}`
|
|
1578
1816
|
);
|
|
1579
1817
|
syncSearch(tableName, id, merged);
|
|
1818
|
+
syncGeo(tableName, id, merged);
|
|
1580
1819
|
syncAggregates(tableName, existing, merged);
|
|
1581
1820
|
syncRanks(tableName, id, existing, merged);
|
|
1582
1821
|
cache?.invalidate(tableName, id);
|
|
@@ -1592,6 +1831,31 @@ const createShardCtxDb = (options) => {
|
|
|
1592
1831
|
for (const entry of patches) {
|
|
1593
1832
|
await writer.patch(entry.id, entry.patch, expectedTable);
|
|
1594
1833
|
}
|
|
1834
|
+
return { patched: patches.length };
|
|
1835
|
+
},
|
|
1836
|
+
async patchWhere(tableName, args, batchOptions) {
|
|
1837
|
+
const global = globalWriterFor(tableName, "patchWhere");
|
|
1838
|
+
let patches;
|
|
1839
|
+
if (global) {
|
|
1840
|
+
const rows = await global.findMany(tableName, { where: args.where });
|
|
1841
|
+
patches = rows.page.map((row) => {
|
|
1842
|
+
return { id: String(row["_id"]), patch: args.patch };
|
|
1843
|
+
});
|
|
1844
|
+
} else {
|
|
1845
|
+
if (!schema.tables[tableName]) {
|
|
1846
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1847
|
+
}
|
|
1848
|
+
const page = await writer.findMany(tableName, { where: args.where });
|
|
1849
|
+
patches = page.page.map((row) => {
|
|
1850
|
+
return { id: String(row["_id"]), patch: args.patch };
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
assertBatchLimit(patches.length, batchOptions?.limit, "patchWhere");
|
|
1854
|
+
if (writer.patchMany === void 0) {
|
|
1855
|
+
throw new LunoraError("INTERNAL", `ctx.db.${tableName}.patchMany is unavailable: this writer has no batch patch`);
|
|
1856
|
+
}
|
|
1857
|
+
await writer.patchMany(patches, batchOptions);
|
|
1858
|
+
return { patched: patches.length };
|
|
1595
1859
|
},
|
|
1596
1860
|
query(tableName) {
|
|
1597
1861
|
const global = globalWriterFor(tableName, "query");
|
|
@@ -1611,11 +1875,11 @@ const createShardCtxDb = (options) => {
|
|
|
1611
1875
|
onIndexUse(tableName, indexName, "rank");
|
|
1612
1876
|
const definition = schema.tables[tableName];
|
|
1613
1877
|
if (!definition) {
|
|
1614
|
-
throw new
|
|
1878
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1615
1879
|
}
|
|
1616
1880
|
const index = definition.rankIndexes?.find((i) => i.name === indexName);
|
|
1617
1881
|
if (!index) {
|
|
1618
|
-
throw new
|
|
1882
|
+
throw new LunoraError("INTERNAL", `unknown rankIndex "${indexName}" on table "${tableName}"`);
|
|
1619
1883
|
}
|
|
1620
1884
|
assertRankPartitionLocal(tableName, definition, index);
|
|
1621
1885
|
if (rankOptions.restrictsCounts) {
|
|
@@ -1656,17 +1920,18 @@ const createShardCtxDb = (options) => {
|
|
|
1656
1920
|
// eslint-disable-next-line @typescript-eslint/require-await -- DatabaseWriterLike returns Promises (the D1 twin awaits I/O); the body is synchronous SQLite
|
|
1657
1921
|
async rankBefore(tableName, indexName, rankBeforeOptions) {
|
|
1658
1922
|
if (isGlobalTable(tableName)) {
|
|
1659
|
-
throw new
|
|
1923
|
+
throw new LunoraError(
|
|
1924
|
+
"INTERNAL",
|
|
1660
1925
|
`rankBefore is not supported on the global (.global()) table '${tableName}' — cross-shard rank cursors apply only to sharded tables`
|
|
1661
1926
|
);
|
|
1662
1927
|
}
|
|
1663
1928
|
const definition = schema.tables[tableName];
|
|
1664
1929
|
if (!definition) {
|
|
1665
|
-
throw new
|
|
1930
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1666
1931
|
}
|
|
1667
1932
|
const index = definition.rankIndexes?.find((i) => i.name === indexName);
|
|
1668
1933
|
if (!index) {
|
|
1669
|
-
throw new
|
|
1934
|
+
throw new LunoraError("INTERNAL", `unknown rankIndex "${indexName}" on table "${tableName}"`);
|
|
1670
1935
|
}
|
|
1671
1936
|
if (rankBeforeOptions.restrictsCounts) {
|
|
1672
1937
|
throw new CountRlsUnsupportedError(tableName);
|
|
@@ -1696,23 +1961,43 @@ const createShardCtxDb = (options) => {
|
|
|
1696
1961
|
const { directions, hasMore, rows } = computeRankPage(rankPageDeps, tableName, indexName, rankPageOptions);
|
|
1697
1962
|
return { directions, hasMore, rows };
|
|
1698
1963
|
},
|
|
1699
|
-
async
|
|
1964
|
+
async restore(id, expectedTable) {
|
|
1965
|
+
const located = locateRowById(id, expectedTable);
|
|
1966
|
+
if (!located) {
|
|
1967
|
+
const global = expectedTable === void 0 ? globalFallback() : void 0;
|
|
1968
|
+
if (global?.restore) {
|
|
1969
|
+
await global.restore(id);
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
throw new LunoraError("INTERNAL", `document not found: ${id}`);
|
|
1973
|
+
}
|
|
1974
|
+
const field = schema.tables[located.tableName]?.softDeleteMode?.field;
|
|
1975
|
+
if (!field) {
|
|
1976
|
+
throw new LunoraError("INTERNAL", `ctx.db.restore: table "${located.tableName}" is not a .softDelete() table`);
|
|
1977
|
+
}
|
|
1978
|
+
const wasDeleted = located.row[field] !== null && located.row[field] !== void 0;
|
|
1979
|
+
await writer.patch(id, { [field]: null }, expectedTable);
|
|
1980
|
+
if (wasDeleted) {
|
|
1981
|
+
syncRanks(located.tableName, id, void 0, located.row);
|
|
1982
|
+
}
|
|
1983
|
+
},
|
|
1984
|
+
async replace(id, document, expectedTable, replaceOptions) {
|
|
1700
1985
|
const located = locateRowById(id, expectedTable);
|
|
1701
1986
|
if (!located) {
|
|
1702
1987
|
const global = expectedTable === void 0 ? globalFallback() : void 0;
|
|
1703
1988
|
if (global) {
|
|
1704
|
-
await global.replace(id, document);
|
|
1989
|
+
await global.replace(id, document, void 0, replaceOptions);
|
|
1705
1990
|
return;
|
|
1706
1991
|
}
|
|
1707
|
-
throw new
|
|
1992
|
+
throw new LunoraError("INTERNAL", `document not found: ${id}`);
|
|
1708
1993
|
}
|
|
1709
1994
|
const { docJson: existingJson, row: previous, tableName } = located;
|
|
1710
1995
|
const tableDefinition = schema.tables[tableName];
|
|
1711
1996
|
if (!tableDefinition) {
|
|
1712
|
-
throw new
|
|
1997
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
1713
1998
|
}
|
|
1714
1999
|
assertNoExplicitUndefined("replace", document);
|
|
1715
|
-
const creationTime = typeof document["_creationTime"] === "number" ? document["_creationTime"] : clock();
|
|
2000
|
+
const creationTime = replaceOptions?.allowExplicitId && typeof document["_creationTime"] === "number" ? document["_creationTime"] : clock();
|
|
1716
2001
|
const replaced = { ...document, _creationTime: creationTime, _id: id };
|
|
1717
2002
|
applyOnUpdate(tableDefinition, document, replaced, auth);
|
|
1718
2003
|
runRowValidators(tableDefinition, replaced);
|
|
@@ -1727,6 +2012,7 @@ const createShardCtxDb = (options) => {
|
|
|
1727
2012
|
sql`UPDATE ${sql.identifier(tableName)} SET _creationTime = ${creationTime}, ${sql.identifier(DOC_COLUMN$1)} = ${JSON.stringify(replaced)} WHERE id = ${id} AND ${sql.identifier(DOC_COLUMN$1)} = ${existingJson}`
|
|
1728
2013
|
);
|
|
1729
2014
|
syncSearch(tableName, id, replaced);
|
|
2015
|
+
syncGeo(tableName, id, replaced);
|
|
1730
2016
|
syncAggregates(tableName, previous, replaced);
|
|
1731
2017
|
syncRanks(tableName, id, previous, replaced);
|
|
1732
2018
|
cache?.invalidate(tableName, id);
|