@lunora/do 1.0.0-alpha.3 → 1.0.0-alpha.5

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.
Files changed (25) hide show
  1. package/__assets__/package-og.svg +1 -1
  2. package/dist/index.d.mts +91 -32
  3. package/dist/index.d.ts +91 -32
  4. package/dist/index.mjs +18 -18
  5. package/dist/packem_shared/{assertFlatPredicate-DyVYReuT.mjs → DEFAULT_MAX_RELATION_KEYS-Dou2PWdO.mjs} +1 -1
  6. package/dist/packem_shared/{assertValidClientId-CBZ1zC96.mjs → NotUniqueError-h_thNFSZ.mjs} +96 -32
  7. package/dist/packem_shared/{guardWriter-u3UlnCH5.mjs → RLS_UNWRAP_SYMBOL-EtGQdC9d.mjs} +6 -2
  8. package/dist/packem_shared/{ROOT_DO_SIZE_WARN_BYTES-DQkmGiCS.mjs → ROOT_DO_SIZE_WARN_BYTES-3lZ2yigq.mjs} +10 -16
  9. package/dist/packem_shared/{applyOnDelete-CMif2RKw.mjs → applyOnDelete-sA7o1CqD.mjs} +6 -2
  10. package/dist/packem_shared/{buildSeekWhere-lVsNXSLy.mjs → applySelect-BvZdFUBT.mjs} +18 -1
  11. package/dist/packem_shared/{backfillAggregateIndexes-BF5eL7kW.mjs → backfillAggregateIndexes-BbVPvciS.mjs} +1 -1
  12. package/dist/packem_shared/{runShardMigrations-C3bn5r93.mjs → runShardMigrations-PabobOjF.mjs} +2 -2
  13. package/dist/packem_shared/{serveRelationFanout-Clr1a05L.mjs → serveRelationFanout-CFBKWJ8Q.mjs} +1 -1
  14. package/package.json +1 -1
  15. /package/dist/packem_shared/{ADMIN_FUNCTION_PREFIX-Dzdqq5J2.mjs → ADMIN_FUNCTIONS-Dzdqq5J2.mjs} +0 -0
  16. /package/dist/packem_shared/{matchesStaticWhere-CFk6adSu.mjs → AGGREGATE_SQL_FUNCTION-CFk6adSu.mjs} +0 -0
  17. /package/dist/packem_shared/{AUTH_METRICS_BUCKET_MS-CiHHYeJi.mjs → AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs} +0 -0
  18. /package/dist/packem_shared/{applyCdcChanges-Ctdmxmrv.mjs → CDC_LOG_TABLE-Ctdmxmrv.mjs} +0 -0
  19. /package/dist/packem_shared/{ensureFunctionMetricsTables-UDNVD7FS.mjs → FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs} +0 -0
  20. /package/dist/packem_shared/{clearCapturedMail-CPpgl-dX.mjs → MAIL_RETENTION-CPpgl-dX.mjs} +0 -0
  21. /package/dist/packem_shared/{assertReadonly-dDcFE1YZ.mjs → MAX_SQL_ROWS-dDcFE1YZ.mjs} +0 -0
  22. /package/dist/packem_shared/{buildSecurityAudit-CCAvoFlr.mjs → MIN_ADMIN_TOKEN_LENGTH-CCAvoFlr.mjs} +0 -0
  23. /package/dist/packem_shared/{encodePartitionKey-C6blLR5K.mjs → RANK_TIEBREAK-C6blLR5K.mjs} +0 -0
  24. /package/dist/packem_shared/{ftsTableName-BLEMawrp.mjs → buildFtsMatch-BLEMawrp.mjs} +0 -0
  25. /package/dist/packem_shared/{runTriggers-5N6_Fx0A.mjs → hasTrigger-5N6_Fx0A.mjs} +0 -0
@@ -1,27 +1,27 @@
1
1
  import { sql } from 'drizzle-orm';
2
- import { matchesStaticWhere, aggregateSqlFunction, normalizeCountArgument, throwingScheduler } from './matchesStaticWhere-CFk6adSu.mjs';
2
+ import { matchesStaticWhere, aggregateSqlFunction, normalizeCountArgument, throwingScheduler } from './AGGREGATE_SQL_FUNCTION-CFk6adSu.mjs';
3
3
  import { encodeAggregateKey, foldAggregateTally, aggregateTableName, coerceAggregateNumber, readAggregateValue } from './aggregateTableName-CxNqY1Sl.mjs';
4
4
  import { mergeWhere, CountRlsUnsupportedError, selectIndexForGroupBy, selectIndexForCount, selectIndexForAggregate } from './CountRlsUnsupportedError-28ZvvwKS.mjs';
5
- import { appendCdcChange } from './applyCdcChanges-Ctdmxmrv.mjs';
6
- export { CDC_LOG_TABLE, CDC_META_TABLE, applyCdcChanges, bumpCdcEpoch, migrateCdcLog, migrateCdcMeta, minCdcSeq, readCdcChanges, readCdcCursor, readCdcEpoch, trimCdcChanges } from './applyCdcChanges-Ctdmxmrv.mjs';
5
+ import { appendCdcChange } from './CDC_LOG_TABLE-Ctdmxmrv.mjs';
6
+ export { CDC_LOG_TABLE, applyCdcChanges, bumpCdcEpoch, minCdcSeq, readCdcChanges, readCdcCursor, readCdcEpoch, trimCdcChanges } from './CDC_LOG_TABLE-Ctdmxmrv.mjs';
7
7
  import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
8
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-BCHCWtrD.mjs';
9
9
  import { param } from './renderSql-D6eUcn2N.mjs';
10
10
  import { s as sortColumnName, m as matchesRankStaticWhere, e as encodePartitionKey, b as serializeSqlValue, r as rankTableName, a as resolveRankPartition, R as RANK_TIEBREAK } from './rank-CrkEIpF4.mjs';
11
- import { stringifySearchText, ftsTableName, tokenizeSearch, buildFtsMatch, scoreDocument } from './ftsTableName-BLEMawrp.mjs';
11
+ import { stringifySearchText, ftsTableName, tokenizeSearch, buildFtsMatch, scoreDocument } from './buildFtsMatch-BLEMawrp.mjs';
12
12
  import { SCAN_DEP } from './SCAN_DEP-DLJF8dsj.mjs';
13
- import { decodeCursor, normalizeOrderKeys, buildSeekWhere, encodeCursor, buildSeekBeforeWhere } from './buildSeekWhere-lVsNXSLy.mjs';
13
+ import { decodeCursor, normalizeOrderKeys, buildSeekWhere, applySelect, encodeCursor, softDeleteScope, buildSeekBeforeWhere } from './applySelect-BvZdFUBT.mjs';
14
14
  import NotFoundError from './NotFoundError-CMuMZt81.mjs';
15
- import { assertFlatPredicate, resolveRelationPredicates } from './assertFlatPredicate-DyVYReuT.mjs';
16
- import { runRowValidators, resolveWith, applyOnDelete } from './applyOnDelete-CMif2RKw.mjs';
17
- import { guardWriter } from './guardWriter-u3UlnCH5.mjs';
15
+ import { assertFlatPredicate, resolveRelationPredicates } from './DEFAULT_MAX_RELATION_KEYS-Dou2PWdO.mjs';
16
+ import { runRowValidators, resolveWith, applyOnDelete } from './applyOnDelete-sA7o1CqD.mjs';
17
+ import { guardWriter } from './RLS_UNWRAP_SYMBOL-EtGQdC9d.mjs';
18
18
  import { createSystemReader } from './createSystemReader-8CzSZP9V.mjs';
19
19
  import { ConflictError } from './ConflictError-C0STs6bU.mjs';
20
- import { runTriggers } from './runTriggers-5N6_Fx0A.mjs';
20
+ import { runTriggers } from './hasTrigger-5N6_Fx0A.mjs';
21
21
  import { compileWhereSql } from './compileWhereSql-CXrhFA3G.mjs';
22
- export { backfillAggregateIndexes, backfillRankIndexes } from './backfillAggregateIndexes-BF5eL7kW.mjs';
23
- export { I as IDEMPOTENCY_TABLE, m as migrateIdempotency, r as readIdempotent, t as trimIdempotent, w as writeIdempotent } from './ctx-db-idempotency-DkC9rP91.mjs';
24
- export { runShardMigrations } from './runShardMigrations-C3bn5r93.mjs';
22
+ export { backfillAggregateIndexes, backfillRankIndexes } from './backfillAggregateIndexes-BbVPvciS.mjs';
23
+ export { I as IDEMPOTENCY_TABLE, r as readIdempotent, t as trimIdempotent, w as writeIdempotent } from './ctx-db-idempotency-DkC9rP91.mjs';
24
+ export { runShardMigrations } from './runShardMigrations-PabobOjF.mjs';
25
25
 
26
26
  const rankIndexFieldsUnchanged = (index, previous, next) => {
27
27
  const fields = [...index.partitionBy ?? [], ...index.sortBy.map((key) => key.field), ...index.where ? Object.keys(index.where) : []];
@@ -534,7 +534,7 @@ const createSearchBuilder = (search, tableName) => {
534
534
  };
535
535
  return builder;
536
536
  };
537
- const searchViaFts = (sql$1, tableName, search, limit) => {
537
+ const searchViaFts = (sql$1, tableName, search, limit, scopeCondition) => {
538
538
  const tokens = tokenizeSearch(search.query);
539
539
  if (tokens.length === 0) {
540
540
  return [];
@@ -544,6 +544,9 @@ const searchViaFts = (sql$1, tableName, search, limit) => {
544
544
  for (const filter of search.filters) {
545
545
  whereClauses.push(sql`${jsonPathSql(filter.field)} = ${serializeSqlValue(filter.value)}`);
546
546
  }
547
+ if (scopeCondition) {
548
+ whereClauses.push(scopeCondition);
549
+ }
547
550
  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
551
  if (typeof limit === "number") {
549
552
  query = sql`${query} LIMIT ${sql.raw(String(Math.max(0, Math.floor(limit))))}`;
@@ -558,7 +561,7 @@ const searchViaFts = (sql$1, tableName, search, limit) => {
558
561
  }
559
562
  return docs;
560
563
  };
561
- const searchViaScan = (sql$1, tableName, search, limit) => {
564
+ const searchViaScan = (sql$1, tableName, search, limit, scopeCondition) => {
562
565
  const tokens = tokenizeSearch(search.query);
563
566
  if (tokens.length === 0) {
564
567
  return [];
@@ -567,6 +570,9 @@ const searchViaScan = (sql$1, tableName, search, limit) => {
567
570
  for (const filter of search.filters) {
568
571
  whereClauses.push(sql`${jsonPathSql(filter.field)} = ${serializeSqlValue(filter.value)}`);
569
572
  }
573
+ if (scopeCondition) {
574
+ whereClauses.push(scopeCondition);
575
+ }
570
576
  let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
571
577
  if (whereClauses.length > 0) {
572
578
  query = sql`${query} WHERE ${sql.join(whereClauses, sql` AND `)}`;
@@ -660,11 +666,12 @@ const scanDocs = (rows, filters, cap) => {
660
666
  }
661
667
  return docs;
662
668
  };
663
- const paginateStage = (sql$1, tableName, stage, options) => {
669
+ const paginateStage = (sql$1, tableName, stage, options, scopeCondition) => {
664
670
  const numberItems = Math.max(0, Math.floor(options.numItems));
665
671
  const orderKeys = paginateOrderKeys(stage);
666
672
  const bounded = typeof options.endCursor === "string";
667
- const whereCondition = compileWhereSql(paginateWhere(stage, orderKeys, options.cursor, options.endCursor), doWhereSqlStrategy);
673
+ const pageWhere = compileWhereSql(paginateWhere(stage, orderKeys, options.cursor, options.endCursor), doWhereSqlStrategy);
674
+ const whereCondition = scopeCondition && pageWhere ? sql`${pageWhere} AND ${scopeCondition}` : scopeCondition ?? pageWhere;
668
675
  let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
669
676
  if (whereCondition) {
670
677
  query = sql`${query} WHERE ${whereCondition}`;
@@ -721,6 +728,8 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
721
728
  if (!tableDefinition) {
722
729
  throw new Error(`unknown table: ${tableName}`);
723
730
  }
731
+ const scopeWhere = softDeleteScope(tableDefinition.softDeleteMode, void 0);
732
+ const scopeCondition = scopeWhere ? compileWhereSql(scopeWhere, doWhereSqlStrategy) : void 0;
724
733
  const stage = {
725
734
  indexFields: [],
726
735
  indexName: void 0,
@@ -735,7 +744,7 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
735
744
  }
736
745
  const filtered = stage.inMemoryFilters.length > 0;
737
746
  const engineLimit = filtered ? void 0 : limit;
738
- const docs = isFtsAvailable(sql$1) ? searchViaFts(sql$1, tableName, search, engineLimit) : searchViaScan(sql$1, tableName, search, engineLimit);
747
+ const docs = isFtsAvailable(sql$1) ? searchViaFts(sql$1, tableName, search, engineLimit, scopeCondition) : searchViaScan(sql$1, tableName, search, engineLimit, scopeCondition);
739
748
  if (!filtered) {
740
749
  return docs;
741
750
  }
@@ -766,6 +775,9 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
766
775
  for (const condition of stage.sqlConditions) {
767
776
  whereClauses.push(sql`${jsonPathSql(condition.field)} ${sql.raw(condition.comparator)} ${serializeSqlValue(condition.value)}`);
768
777
  }
778
+ if (scopeCondition) {
779
+ whereClauses.push(scopeCondition);
780
+ }
769
781
  let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
770
782
  if (whereClauses.length > 0) {
771
783
  query = sql`${query} WHERE ${sql.join(whereClauses, sql` AND `)}`;
@@ -813,7 +825,7 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
813
825
  if (stage.search) {
814
826
  throw new Error("pagination is not supported on search queries; use .take(n) or .collect()");
815
827
  }
816
- return paginateStage(sql$1, tableName, stage, options);
828
+ return paginateStage(sql$1, tableName, stage, options, scopeCondition);
817
829
  },
818
830
  // 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
831
  async take(limit) {
@@ -1130,10 +1142,11 @@ const createShardCtxDb = (options) => {
1130
1142
  throw new Error(`aggregate(${tableName}, { op: "${aggOptions.op}" }): "field" is required for non-count reducers`);
1131
1143
  }
1132
1144
  onRead(tableName, SCAN_DEP);
1133
- const effective = mergeWhere(aggOptions.baseWhere, aggOptions.where);
1145
+ const aggScope = softDeleteScope(definition.softDeleteMode, void 0);
1146
+ const effective = mergeWhere(mergeWhere(aggOptions.baseWhere, aggOptions.where), aggScope);
1134
1147
  const resolved = await resolveAggregateRelations(effective, tableName, aggOptions.relationBaseWhere);
1135
1148
  const hasRelation = resolved !== effective;
1136
- if (definition.aggregateIndexes && !aggOptions.baseWhere && !hasRelation) {
1149
+ if (definition.aggregateIndexes && !aggOptions.baseWhere && !hasRelation && !aggScope) {
1137
1150
  const planned = selectIndexForAggregate(definition.aggregateIndexes, aggOptions.op, aggOptions.field, aggOptions.where);
1138
1151
  if (planned) {
1139
1152
  ensureBackfilled(tableName, planned.index);
@@ -1175,10 +1188,11 @@ const createShardCtxDb = (options) => {
1175
1188
  throw new CountRlsUnsupportedError(tableName);
1176
1189
  }
1177
1190
  onRead(tableName, SCAN_DEP);
1178
- const effective = mergeWhere(countOptions.baseWhere, countOptions.where);
1191
+ const countScope = softDeleteScope(definition.softDeleteMode, void 0);
1192
+ const effective = mergeWhere(mergeWhere(countOptions.baseWhere, countOptions.where), countScope);
1179
1193
  const resolved = await resolveAggregateRelations(effective, tableName, countOptions.relationBaseWhere);
1180
1194
  const hasRelation = resolved !== effective;
1181
- if (definition.aggregateIndexes && !countOptions.baseWhere && !hasRelation) {
1195
+ if (definition.aggregateIndexes && !countOptions.baseWhere && !hasRelation && !countScope) {
1182
1196
  const planned = selectIndexForCount(definition.aggregateIndexes, countOptions.where);
1183
1197
  if (planned) {
1184
1198
  ensureBackfilled(tableName, planned.index);
@@ -1199,16 +1213,22 @@ const createShardCtxDb = (options) => {
1199
1213
  const row = runDrizzle(sql$1, query).one();
1200
1214
  return row.count;
1201
1215
  },
1202
- async delete(id, expectedTable) {
1216
+ async delete(id, expectedTable, deleteOptions) {
1203
1217
  const located = locateRowById(id, expectedTable);
1204
1218
  if (!located) {
1205
1219
  const global = expectedTable === void 0 ? globalFallback() : void 0;
1206
1220
  if (global) {
1207
- await global.delete(id);
1221
+ await global.delete(id, void 0, deleteOptions);
1208
1222
  }
1209
1223
  return;
1210
1224
  }
1211
1225
  const { docJson: existingJson, row: existing, tableName } = located;
1226
+ const definition = schema.tables[tableName];
1227
+ const hard = deleteOptions?.hard === true;
1228
+ const softField = !hard && definition?.softDeleteMode ? definition.softDeleteMode.field : void 0;
1229
+ if (softField && existing[softField] !== null && existing[softField] !== void 0) {
1230
+ return;
1231
+ }
1212
1232
  if (hasMatchingTrigger(tableName, "before", "delete")) {
1213
1233
  await fireTriggers("before", "delete", { id, op: "delete", previous: existing, table: tableName });
1214
1234
  }
@@ -1216,10 +1236,10 @@ const createShardCtxDb = (options) => {
1216
1236
  deletedId: id,
1217
1237
  deletedReference: (references) => existing[references],
1218
1238
  findHolders: async (holderTable, field, value) => {
1219
- const holders = await routeForHolder(holderTable).findMany(holderTable, { where: { [field]: value } });
1239
+ const holders = await routeForHolder(holderTable).findMany(holderTable, { includeDeleted: hard, where: { [field]: value } });
1220
1240
  return holders.page;
1221
1241
  },
1222
- onCascade: (holderTable, holderId) => routeForHolder(holderTable).delete(holderId),
1242
+ onCascade: (holderTable, holderId) => routeForHolder(holderTable).delete(holderId, void 0, deleteOptions),
1223
1243
  onRestrict: (message) => {
1224
1244
  throw new ConflictError(message, "restrict");
1225
1245
  },
@@ -1230,6 +1250,25 @@ const createShardCtxDb = (options) => {
1230
1250
  });
1231
1251
  ensureBackfilledForTable(tableName);
1232
1252
  ensureRankBackfilledForTable(tableName);
1253
+ if (softField) {
1254
+ const merged = { ...existing, [softField]: clock(), _id: id };
1255
+ runGuardedWrite(
1256
+ sql$1,
1257
+ tableName,
1258
+ sql`UPDATE ${sql.identifier(tableName)} SET ${sql.identifier(DOC_COLUMN$1)} = ${JSON.stringify(merged)} WHERE id = ${id} AND ${sql.identifier(DOC_COLUMN$1)} = ${existingJson}`
1259
+ );
1260
+ syncSearch(tableName, id, merged);
1261
+ syncAggregates(tableName, existing, merged);
1262
+ syncRanks(tableName, id, existing, void 0);
1263
+ cache?.invalidate(tableName, id);
1264
+ recordCdc(tableName, id, "update", merged);
1265
+ broadcast({ key: id, op: "update", row: merged, table: tableName });
1266
+ if (hasMatchingTrigger(tableName, "after", "delete")) {
1267
+ await fireTriggers("after", "delete", { id, op: "delete", previous: existing, table: tableName });
1268
+ }
1269
+ await onWrite({ id, op: "delete", table: tableName });
1270
+ return;
1271
+ }
1233
1272
  runGuardedWrite(
1234
1273
  sql$1,
1235
1274
  tableName,
@@ -1271,7 +1310,8 @@ const createShardCtxDb = (options) => {
1271
1310
  onRead(tableName, SCAN_DEP);
1272
1311
  return global.findMany(tableName, args);
1273
1312
  }
1274
- if (!schema.tables[tableName]) {
1313
+ const findManyDefinition = schema.tables[tableName];
1314
+ if (!findManyDefinition) {
1275
1315
  throw new Error(`unknown table: ${tableName}`);
1276
1316
  }
1277
1317
  const isFullScan = !args.where && !args.baseWhere;
@@ -1283,6 +1323,7 @@ const createShardCtxDb = (options) => {
1283
1323
  const orderKeys = normalizeOrderKeys(args.orderBy);
1284
1324
  const seek = args.cursor ? buildSeekWhere(orderKeys, decodeCursor(args.cursor)) : void 0;
1285
1325
  let predicate = mergeWhere(args.baseWhere, args.where);
1326
+ predicate = mergeWhere(predicate, softDeleteScope(findManyDefinition.softDeleteMode, args.includeDeleted));
1286
1327
  predicate = await resolveRelationPredicates(predicate, {
1287
1328
  canPushExists: relationExistsPushDownEnabled ? canPushRelationExists : void 0,
1288
1329
  existsPushMode: relationExistsPushDown === "always" ? "always" : "auto",
@@ -1329,7 +1370,7 @@ const createShardCtxDb = (options) => {
1329
1370
  with: args.with
1330
1371
  });
1331
1372
  }
1332
- return { continueCursor: null, isDone: true, page: docs };
1373
+ return { continueCursor: null, isDone: true, page: applySelect(docs, args.select, args.with) };
1333
1374
  }
1334
1375
  const hasMore = docs.length > limit;
1335
1376
  const page = hasMore ? docs.slice(0, limit) : docs;
@@ -1346,10 +1387,12 @@ const createShardCtxDb = (options) => {
1346
1387
  });
1347
1388
  }
1348
1389
  return {
1390
+ // The cursor is encoded from `last` (the full, unprojected row) above,
1391
+ // so `applySelect` only trims the returned payload — paging is intact.
1349
1392
  // eslint-disable-next-line unicorn/no-null -- QueryPage.continueCursor is `null | string`: null is the documented "no further page" cursor on the wire
1350
1393
  continueCursor: hasMore && last ? encodeCursor(last, orderKeys) : null,
1351
1394
  isDone: !hasMore,
1352
- page
1395
+ page: applySelect(page, args.select, args.with)
1353
1396
  };
1354
1397
  },
1355
1398
  async get(id, expectedTable) {
@@ -1390,10 +1433,11 @@ const createShardCtxDb = (options) => {
1390
1433
  if (agg.op !== "count" && !agg.field) {
1391
1434
  throw new Error(`groupBy(${tableName}, { agg: { op: "${agg.op}" } }): "field" is required for non-count reducers`);
1392
1435
  }
1393
- const effective = mergeWhere(groupOptions.baseWhere, groupOptions.where);
1436
+ const groupScope = softDeleteScope(definition.softDeleteMode, void 0);
1437
+ const effective = mergeWhere(mergeWhere(groupOptions.baseWhere, groupOptions.where), groupScope);
1394
1438
  const resolved = await resolveAggregateRelations(effective, tableName, groupOptions.relationBaseWhere);
1395
1439
  const hasRelation = resolved !== effective;
1396
- if (definition.aggregateIndexes && !groupOptions.baseWhere && !hasRelation) {
1440
+ if (definition.aggregateIndexes && !groupOptions.baseWhere && !hasRelation && !groupScope) {
1397
1441
  const planned = selectIndexForGroupBy(definition.aggregateIndexes, agg.op, agg.field, groupOptions.by, groupOptions.where);
1398
1442
  if (planned) {
1399
1443
  ensureBackfilled(tableName, planned.index);
@@ -1696,6 +1740,26 @@ const createShardCtxDb = (options) => {
1696
1740
  const { directions, hasMore, rows } = computeRankPage(rankPageDeps, tableName, indexName, rankPageOptions);
1697
1741
  return { directions, hasMore, rows };
1698
1742
  },
1743
+ async restore(id, expectedTable) {
1744
+ const located = locateRowById(id, expectedTable);
1745
+ if (!located) {
1746
+ const global = expectedTable === void 0 ? globalFallback() : void 0;
1747
+ if (global?.restore) {
1748
+ await global.restore(id);
1749
+ return;
1750
+ }
1751
+ throw new Error(`document not found: ${id}`);
1752
+ }
1753
+ const field = schema.tables[located.tableName]?.softDeleteMode?.field;
1754
+ if (!field) {
1755
+ throw new Error(`ctx.db.restore: table "${located.tableName}" is not a .softDelete() table`);
1756
+ }
1757
+ const wasDeleted = located.row[field] !== null && located.row[field] !== void 0;
1758
+ await writer.patch(id, { [field]: null }, expectedTable);
1759
+ if (wasDeleted) {
1760
+ syncRanks(located.tableName, id, void 0, located.row);
1761
+ }
1762
+ },
1699
1763
  async replace(id, document, expectedTable) {
1700
1764
  const located = locateRowById(id, expectedTable);
1701
1765
  if (!located) {
@@ -1742,4 +1806,4 @@ const createShardCtxDb = (options) => {
1742
1806
  return options.enforceRls === true ? guardWriter(writer, schema, (id, expectedTable) => locateRowById(id, expectedTable)?.tableName) : writer;
1743
1807
  };
1744
1808
 
1745
- export { NotUniqueError, appendCdcChange, assertValidClientId, createShardCtxDb, normalizeIdStructurally };
1809
+ export { NotUniqueError, assertValidClientId, createShardCtxDb, normalizeIdStructurally };
@@ -47,9 +47,9 @@ const guardWriter = (raw, schema, tableOfId) => {
47
47
  guardTable(tableName);
48
48
  return base.count(tableName, whereOrArgs);
49
49
  },
50
- delete: async (id, expectedTable) => {
50
+ delete: async (id, expectedTable, options) => {
51
51
  await guardById(id, expectedTable);
52
- return base.delete(id, expectedTable);
52
+ return base.delete(id, expectedTable, options);
53
53
  },
54
54
  deleteMany: async (ids, options, expectedTable) => {
55
55
  for (const id of ids) {
@@ -114,6 +114,10 @@ const guardWriter = (raw, schema, tableOfId) => {
114
114
  replace: async (id, document, expectedTable) => {
115
115
  await guardById(id, expectedTable);
116
116
  return base.replace(id, document, expectedTable);
117
+ },
118
+ restore: async (id, expectedTable) => {
119
+ await guardById(id, expectedTable);
120
+ return base.restore?.(id, expectedTable);
117
121
  }
118
122
  };
119
123
  if (baseRankBefore) {
@@ -1,19 +1,19 @@
1
1
  import { drizzle } from 'drizzle-orm/durable-sqlite';
2
2
  import { parseExportShardArgs, parseImportShardArgs } from './exportShardRows-DZEhUeyI.mjs';
3
- import { recordAuthEvent, readAuthMetrics } from './AUTH_METRICS_BUCKET_MS-CiHHYeJi.mjs';
3
+ import { recordAuthEvent, readAuthMetrics } from './AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
4
4
  import { DATA_MIGRATION_STATE_TABLE, readMigrationStatus } from './DATA_MIGRATION_STATE_TABLE-PTtTiQ7U.mjs';
5
5
  import { SCAN_DEP, createDependencyTracker, tableFromDepKey } from './SCAN_DEP-DLJF8dsj.mjs';
6
- import { readFunctionMetricsTotals, readFunctionMetricIndexHits, recordFunctionMetric, mergeScanAttribution, readFunctionMetrics, readFunctionMetricBuckets } from './ensureFunctionMetricsTables-UDNVD7FS.mjs';
7
- import { ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, readTablePage, facetColumn, MAX_PAGE_SIZE } from './ADMIN_FUNCTION_PREFIX-Dzdqq5J2.mjs';
6
+ import { readFunctionMetricsTotals, readFunctionMetricIndexHits, recordFunctionMetric, mergeScanAttribution, readFunctionMetrics, readFunctionMetricBuckets } from './FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
7
+ import { ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, readTablePage, facetColumn, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-Dzdqq5J2.mjs';
8
8
  import { LogBuffer } from './LogBuffer-B_Ezju_N.mjs';
9
- import { recordCapturedMail, clearCapturedMail, readCapturedMail, MAIL_TABLE } from './clearCapturedMail-CPpgl-dX.mjs';
9
+ import { recordCapturedMail, clearCapturedMail, readCapturedMail, MAIL_TABLE } from './MAIL_RETENTION-CPpgl-dX.mjs';
10
10
  import { readBookmark, armRestore } from './armRestore-BJk53Ro8.mjs';
11
11
  import { ReactiveCache, reactiveCacheKey } from './ReactiveCache-ByVzgH3d.mjs';
12
12
  import { redact, standardRules } from '@visulima/redact';
13
13
  import { i as isDevEnvironment, c as buildSettings, b as buildSecurityAudit } from './security-audit-CucgBice.mjs';
14
- import { runReadonlySql } from './assertReadonly-dDcFE1YZ.mjs';
14
+ import { runReadonlySql } from './MAX_SQL_ROWS-dDcFE1YZ.mjs';
15
15
  import { ConflictError } from './ConflictError-C0STs6bU.mjs';
16
- import { CDC_LOG_TABLE, readCdcChanges, readCdcCursor, readCdcEpoch, minCdcSeq, bumpCdcEpoch } from './applyCdcChanges-Ctdmxmrv.mjs';
16
+ import { CDC_LOG_TABLE, readCdcChanges, readCdcCursor, readCdcEpoch, minCdcSeq, bumpCdcEpoch } from './CDC_LOG_TABLE-Ctdmxmrv.mjs';
17
17
  import { r as readIdempotent, w as writeIdempotent, t as trimIdempotent } from './ctx-db-idempotency-DkC9rP91.mjs';
18
18
 
19
19
  const AUDIT_LOG_TABLE = "__lunora_audit__";
@@ -1648,20 +1648,14 @@ class ShardDO {
1648
1648
  status: 500
1649
1649
  });
1650
1650
  }
1651
- const sqlExec = sqlHandle.exec.bind(sqlHandle);
1651
+ const transactionalStorage = this.state.storage;
1652
1652
  const run = async () => {
1653
1653
  this.transactionDepth = 1;
1654
- sqlExec("BEGIN");
1655
1654
  try {
1656
- const value = await handler();
1657
- sqlExec("COMMIT");
1658
- return value;
1659
- } catch (error) {
1660
- try {
1661
- sqlExec("ROLLBACK");
1662
- } catch {
1655
+ if (typeof transactionalStorage?.transaction === "function") {
1656
+ return await transactionalStorage.transaction(async () => handler());
1663
1657
  }
1664
- throw error;
1658
+ return await handler();
1665
1659
  } finally {
1666
1660
  this.transactionDepth = 0;
1667
1661
  }
@@ -1,3 +1,6 @@
1
+ import { applySelect } from './applySelect-BvZdFUBT.mjs';
2
+
3
+ const projectChildren = (documents, nested) => nested.select ? applySelect(documents, nested.select, nested.with) : documents;
1
4
  const distinctValues = (rows, field) => {
2
5
  const seen = /* @__PURE__ */ new Set();
3
6
  for (const row of rows) {
@@ -44,7 +47,8 @@ const resolveWith = async (options) => {
44
47
  byReference.set(child[relation.references], child);
45
48
  }
46
49
  for (const parent of parents) {
47
- parent[name] = byReference.get(parent[relation.field]) ?? null;
50
+ const child = byReference.get(parent[relation.field]);
51
+ parent[name] = child ? projectChildren([child], nested)[0] ?? null : null;
48
52
  }
49
53
  };
50
54
  const loadMany = async (name, relation, nested) => {
@@ -77,7 +81,7 @@ const resolveWith = async (options) => {
77
81
  const cap = typeof nested.limit === "number" ? Math.max(0, Math.floor(nested.limit)) : void 0;
78
82
  for (const parent of parents) {
79
83
  const group = groups.get(parent[relation.references]) ?? [];
80
- parent[name] = cap === void 0 ? group : group.slice(0, cap);
84
+ parent[name] = projectChildren(cap === void 0 ? group : group.slice(0, cap), nested);
81
85
  }
82
86
  };
83
87
  const resolveCounts = async (countInput) => {
@@ -80,5 +80,22 @@ const buildSeekBeforeWhere = (keys, cursorValues) => {
80
80
  }
81
81
  return { OR: branches };
82
82
  };
83
+ const SELECT_SYSTEM_FIELDS = ["_id", "_creationTime"];
84
+ const applySelect = (page, select, withInput) => {
85
+ if (!select) {
86
+ return page;
87
+ }
88
+ const keep = /* @__PURE__ */ new Set([...select, ...SELECT_SYSTEM_FIELDS, ...withInput ? Object.keys(withInput) : []]);
89
+ return page.map((document) => {
90
+ const projected = {};
91
+ for (const key of keep) {
92
+ if (key in document) {
93
+ projected[key] = document[key];
94
+ }
95
+ }
96
+ return projected;
97
+ });
98
+ };
99
+ const softDeleteScope = (softDeleteMode, includeDeleted) => softDeleteMode && includeDeleted !== true ? { [softDeleteMode.field]: { isNull: true } } : void 0;
83
100
 
84
- export { buildSeekBeforeWhere, buildSeekWhere, decodeCursor, encodeCursor, normalizeOrderKeys };
101
+ export { applySelect, buildSeekBeforeWhere, buildSeekWhere, decodeCursor, encodeCursor, normalizeOrderKeys, softDeleteScope };
@@ -1,5 +1,5 @@
1
1
  import { sql } from 'drizzle-orm';
2
- import { matchesStaticWhere } from './matchesStaticWhere-CFk6adSu.mjs';
2
+ import { matchesStaticWhere } from './AGGREGATE_SQL_FUNCTION-CFk6adSu.mjs';
3
3
  import { encodeAggregateKey, foldAggregateTally, aggregateTableName } from './aggregateTableName-CxNqY1Sl.mjs';
4
4
  import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
5
5
  import { D as DOC_COLUMN, r as rowToDocument, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT } from './do-sql-BCHCWtrD.mjs';
@@ -1,11 +1,11 @@
1
1
  import { sql } from 'drizzle-orm';
2
2
  import { aggregateTableName } from './aggregateTableName-CxNqY1Sl.mjs';
3
- import { migrateCdcLog, migrateCdcMeta } from './applyCdcChanges-Ctdmxmrv.mjs';
3
+ import { migrateCdcLog, migrateCdcMeta } from './CDC_LOG_TABLE-Ctdmxmrv.mjs';
4
4
  import { m as migrateIdempotency } from './ctx-db-idempotency-DkC9rP91.mjs';
5
5
  import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
6
6
  import { D as DOC_COLUMN, j as jsonPathSql, c as createIndexSql, t as tableColumns, i as isFtsAvailable, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT } from './do-sql-BCHCWtrD.mjs';
7
7
  import { s as sortColumnName, r as rankTableName } from './rank-CrkEIpF4.mjs';
8
- import { ftsTableName } from './ftsTableName-BLEMawrp.mjs';
8
+ import { ftsTableName } from './buildFtsMatch-BLEMawrp.mjs';
9
9
 
10
10
  const migrateSecondaryIndexes = (sql$1, tableName, definition) => {
11
11
  for (const index of definition.indexes) {
@@ -1,4 +1,4 @@
1
- import { RELATION_FUNCTION_PREFIX } from './ADMIN_FUNCTION_PREFIX-Dzdqq5J2.mjs';
1
+ import { RELATION_FUNCTION_PREFIX } from './ADMIN_FUNCTIONS-Dzdqq5J2.mjs';
2
2
 
3
3
  const serveRelationFanout = async (schema, database, functionPath, args) => {
4
4
  const table = typeof args["table"] === "string" ? args["table"] : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/do",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.5",
4
4
  "description": "Lunora Durable Objects: ShardDO (SQLite, OCC, hibernated WebSocket subscriptions) and SessionDO",
5
5
  "keywords": [
6
6
  "cloudflare",