@lunora/do 1.0.0-alpha.24 → 1.0.0-alpha.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1679,6 +1679,18 @@ interface DatabaseWriterLike {
1679
1679
  }, expectedTable?: string) => Promise<{
1680
1680
  deleted: number;
1681
1681
  }>;
1682
+ /**
1683
+ * Delete every row matching `where` in one call. Matching rows are resolved
1684
+ * first, then each row is deleted through the single-row delete pipeline so
1685
+ * companions, CDC, and broadcast stay correct. **Atomic within a mutation** —
1686
+ * the DO wraps a mutation's dispatch in a BEGIN/COMMIT span, so a mid-batch
1687
+ * throw rolls the whole mutation back. (An action has no transaction span.)
1688
+ */
1689
+ deleteWhere?: (tableName: string, where: WhereInput, options?: {
1690
+ limit?: number;
1691
+ }) => Promise<{
1692
+ deleted: number;
1693
+ }>;
1682
1694
  findFirst: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown> | null>;
1683
1695
  findFirstOrThrow: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown>>;
1684
1696
  findMany: (tableName: string, args?: QueryArgs) => Promise<QueryPage>;
@@ -1714,11 +1726,13 @@ interface DatabaseWriterLike {
1714
1726
  * Insert many documents into one table (a loop over `insert()`),
1715
1727
  * returning the minted ids in input order. Each row gets defaults,
1716
1728
  * validators, triggers, companion sync, CDC, and broadcast exactly as a
1717
- * single insert; the caller pays one round-trip instead of N. **Atomic within
1718
- * a mutation** the DO wraps a mutation's dispatch in a BEGIN/COMMIT span, so
1719
- * a mid-batch throw rolls the whole mutation back. (An action has no
1720
- * transaction spanthere, the prior inserts persist; the in-memory test
1721
- * harness mirrors the span.)
1729
+ * single insert; the caller pays one round-trip instead of N. Pass
1730
+ * `options.skipDuplicates: true` to turn UNIQUE-constraint breaches into
1731
+ * `null` results for that row instead of failing the whole batch.
1732
+ * **Atomic within a mutation** — the DO wraps a mutation's dispatch in a
1733
+ * BEGIN/COMMIT span, so a mid-batch throw rolls the whole mutation back. (An
1734
+ * action has no transaction span — there, the prior inserts persist; the
1735
+ * in-memory test harness mirrors the span.)
1722
1736
  * Rejects a batch larger than `options.limit` (default {@link DEFAULT_BATCH_LIMIT}).
1723
1737
  *
1724
1738
  * Optional on the interface (like `rankBefore`): the DO writer implements it;
@@ -1728,7 +1742,8 @@ interface DatabaseWriterLike {
1728
1742
  */
1729
1743
  insertMany?: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1730
1744
  limit?: number;
1731
- }) => Promise<string[]>;
1745
+ skipDuplicates?: boolean;
1746
+ }) => Promise<(string | null)[]>;
1732
1747
  /**
1733
1748
  * Trusted bulk insert: one multi-row `INSERT` that **skips per-row `.check()`
1734
1749
  * validators and before/after triggers** for throughput on data the caller
@@ -1783,7 +1798,25 @@ interface DatabaseWriterLike {
1783
1798
  patch: Record<string, unknown>;
1784
1799
  }>, options?: {
1785
1800
  limit?: number;
1786
- }, expectedTable?: string) => Promise<void>;
1801
+ }, expectedTable?: string) => Promise<{
1802
+ patched: number;
1803
+ }>;
1804
+ /**
1805
+ * Patch every row matching `where` with the same `patch` in one call.
1806
+ * Matching rows are resolved first, then each row is patched through the
1807
+ * single-row patch pipeline so companions, CDC, and broadcast stay correct.
1808
+ * **Atomic within a mutation** — the DO wraps a mutation's dispatch in a
1809
+ * BEGIN/COMMIT span, so a mid-batch throw rolls the whole mutation back. (An
1810
+ * action has no transaction span.)
1811
+ */
1812
+ patchWhere?: (tableName: string, args: {
1813
+ patch: Record<string, unknown>;
1814
+ where: WhereInput;
1815
+ }, options?: {
1816
+ limit?: number;
1817
+ }) => Promise<{
1818
+ patched: number;
1819
+ }>;
1787
1820
  query: (tableName: string) => TableReaderLike;
1788
1821
  /**
1789
1822
  * Return the 1-based position of `options.row` within its partition under
package/dist/index.d.ts CHANGED
@@ -1679,6 +1679,18 @@ interface DatabaseWriterLike {
1679
1679
  }, expectedTable?: string) => Promise<{
1680
1680
  deleted: number;
1681
1681
  }>;
1682
+ /**
1683
+ * Delete every row matching `where` in one call. Matching rows are resolved
1684
+ * first, then each row is deleted through the single-row delete pipeline so
1685
+ * companions, CDC, and broadcast stay correct. **Atomic within a mutation** —
1686
+ * the DO wraps a mutation's dispatch in a BEGIN/COMMIT span, so a mid-batch
1687
+ * throw rolls the whole mutation back. (An action has no transaction span.)
1688
+ */
1689
+ deleteWhere?: (tableName: string, where: WhereInput, options?: {
1690
+ limit?: number;
1691
+ }) => Promise<{
1692
+ deleted: number;
1693
+ }>;
1682
1694
  findFirst: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown> | null>;
1683
1695
  findFirstOrThrow: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown>>;
1684
1696
  findMany: (tableName: string, args?: QueryArgs) => Promise<QueryPage>;
@@ -1714,11 +1726,13 @@ interface DatabaseWriterLike {
1714
1726
  * Insert many documents into one table (a loop over `insert()`),
1715
1727
  * returning the minted ids in input order. Each row gets defaults,
1716
1728
  * validators, triggers, companion sync, CDC, and broadcast exactly as a
1717
- * single insert; the caller pays one round-trip instead of N. **Atomic within
1718
- * a mutation** the DO wraps a mutation's dispatch in a BEGIN/COMMIT span, so
1719
- * a mid-batch throw rolls the whole mutation back. (An action has no
1720
- * transaction spanthere, the prior inserts persist; the in-memory test
1721
- * harness mirrors the span.)
1729
+ * single insert; the caller pays one round-trip instead of N. Pass
1730
+ * `options.skipDuplicates: true` to turn UNIQUE-constraint breaches into
1731
+ * `null` results for that row instead of failing the whole batch.
1732
+ * **Atomic within a mutation** — the DO wraps a mutation's dispatch in a
1733
+ * BEGIN/COMMIT span, so a mid-batch throw rolls the whole mutation back. (An
1734
+ * action has no transaction span — there, the prior inserts persist; the
1735
+ * in-memory test harness mirrors the span.)
1722
1736
  * Rejects a batch larger than `options.limit` (default {@link DEFAULT_BATCH_LIMIT}).
1723
1737
  *
1724
1738
  * Optional on the interface (like `rankBefore`): the DO writer implements it;
@@ -1728,7 +1742,8 @@ interface DatabaseWriterLike {
1728
1742
  */
1729
1743
  insertMany?: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1730
1744
  limit?: number;
1731
- }) => Promise<string[]>;
1745
+ skipDuplicates?: boolean;
1746
+ }) => Promise<(string | null)[]>;
1732
1747
  /**
1733
1748
  * Trusted bulk insert: one multi-row `INSERT` that **skips per-row `.check()`
1734
1749
  * validators and before/after triggers** for throughput on data the caller
@@ -1783,7 +1798,25 @@ interface DatabaseWriterLike {
1783
1798
  patch: Record<string, unknown>;
1784
1799
  }>, options?: {
1785
1800
  limit?: number;
1786
- }, expectedTable?: string) => Promise<void>;
1801
+ }, expectedTable?: string) => Promise<{
1802
+ patched: number;
1803
+ }>;
1804
+ /**
1805
+ * Patch every row matching `where` with the same `patch` in one call.
1806
+ * Matching rows are resolved first, then each row is patched through the
1807
+ * single-row patch pipeline so companions, CDC, and broadcast stay correct.
1808
+ * **Atomic within a mutation** — the DO wraps a mutation's dispatch in a
1809
+ * BEGIN/COMMIT span, so a mid-batch throw rolls the whole mutation back. (An
1810
+ * action has no transaction span.)
1811
+ */
1812
+ patchWhere?: (tableName: string, args: {
1813
+ patch: Record<string, unknown>;
1814
+ where: WhereInput;
1815
+ }, options?: {
1816
+ limit?: number;
1817
+ }) => Promise<{
1818
+ patched: number;
1819
+ }>;
1787
1820
  query: (tableName: string) => TableReaderLike;
1788
1821
  /**
1789
1822
  * Return the 1-based position of `options.row` within its partition under
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ export { AGGREGATE_SQL_FUNCTION, aggregateSqlFunction, matchesStaticWhere, norma
3
3
  export { aggregateTableName, coerceAggregateNumber, encodeAggregateKey, foldAggregateTally, readAggregateValue } from './packem_shared/aggregateTableName-CxNqY1Sl.mjs';
4
4
  export { CountRlsUnsupportedError, mergeWhere, planAggregateLookup, selectIndexForAggregate, selectIndexForCount, selectIndexForGroupBy } from './packem_shared/CountRlsUnsupportedError-BGxj0pgS.mjs';
5
5
  export { AUTH_METRICS_BUCKETS_TABLE, AUTH_METRICS_BUCKET_MS, AUTH_METRICS_BUCKET_RETENTION, AUTH_METRICS_TABLE, ensureAuthMetricsTables, readAuthMetrics, recordAuthEvent } from './packem_shared/AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
6
- export { NotUniqueError, assertValidClientId, createShardCtxDb, normalizeIdStructurally } from './packem_shared/NotUniqueError-D1U5SBFR.mjs';
6
+ export { NotUniqueError, assertValidClientId, createShardCtxDb, normalizeIdStructurally } from './packem_shared/NotUniqueError-BdDLev9w.mjs';
7
7
  export { DATA_MIGRATION_STATE_TABLE, readMigrationStatus, runDataMigration } from './packem_shared/DATA_MIGRATION_STATE_TABLE-DB3IYUR3.mjs';
8
8
  export { SCAN_DEP, createDependencyTracker, depKey } from './packem_shared/SCAN_DEP-DLJF8dsj.mjs';
9
9
  export { renderSql } from './packem_shared/renderSql-D6eUcn2N.mjs';
@@ -11,24 +11,24 @@ export { diffExternalSource } from './packem_shared/diffExternalSource-Cx9HUPJj.
11
11
  export { materializeExternalRows, readExternalSourceBaseline, runExternalSourceTick } from './packem_shared/materializeExternalRows-DNcvoLRT.mjs';
12
12
  export { isSourceDue, liftSourceId, pullExternalSourceTick } from './packem_shared/isSourceDue-BptUN8CR.mjs';
13
13
  export { FUNCTION_METRICS_BUCKETS_TABLE, FUNCTION_METRICS_BUCKET_MS, FUNCTION_METRICS_BUCKET_RETENTION, FUNCTION_METRICS_INDEX_TABLE, FUNCTION_METRICS_TABLE, ensureFunctionMetricsTables, readFunctionMetricBuckets, readFunctionMetricIndexHits, readFunctionMetrics, readFunctionMetricsTotals, recordFunctionMetric } from './packem_shared/FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
14
- export { ADMIN_FUNCTIONS, ADMIN_FUNCTION_PREFIX, FLAGS_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, facetColumn, listTables, readTablePage, selectMatchingIds } from './packem_shared/ADMIN_FUNCTIONS-BbQdj8h1.mjs';
14
+ export { ADMIN_FUNCTIONS, ADMIN_FUNCTION_PREFIX, FLAGS_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, facetColumn, listTables, readTablePage, selectMatchingIds } from './packem_shared/ADMIN_FUNCTIONS-Bxdijn66.mjs';
15
15
  export { LogBuffer } from './packem_shared/LogBuffer-B_Ezju_N.mjs';
16
16
  export { MAIL_RETENTION, MAIL_TABLE, clearCapturedMail, ensureMailTable, readCapturedMail, recordCapturedMail } from './packem_shared/MAIL_RETENTION-CPpgl-dX.mjs';
17
17
  export { default as NotFoundError } from './packem_shared/NotFoundError-C70b9hLw.mjs';
18
- export { armRestore, readBookmark } from './packem_shared/armRestore-BJk53Ro8.mjs';
19
- export { applySelect, buildSeekWhere, decodeCursor, encodeCursor, normalizeOrderKeys, softDeleteScope } from './packem_shared/applySelect-BvZdFUBT.mjs';
18
+ export { armRestore, readBookmark } from './packem_shared/armRestore-4Px61hHS.mjs';
19
+ export { applySelect, buildSeekWhere, decodeCursor, encodeCursor, normalizeOrderKeys, softDeleteScope } from './packem_shared/applySelect-WQY8m62C.mjs';
20
20
  export { RANK_TIEBREAK, encodePartitionKey, matchesRankStaticWhere, rankTableName, resolveRankPartition, sortColumnName } from './packem_shared/RANK_TIEBREAK-CXhdcA1o.mjs';
21
21
  export { ReactiveCache, reactiveCacheKey } from './packem_shared/ReactiveCache-BYlSGY0N.mjs';
22
- export { serveRelationFanout } from './packem_shared/serveRelationFanout-BhZF9AuB.mjs';
23
- export { DEFAULT_MAX_RELATION_KEYS, assertFlatPredicate, assertShapeShardable, containsRelationPredicate, isRelationPredicate, resolveRelationPredicates } from './packem_shared/DEFAULT_MAX_RELATION_KEYS-CjM9Y1_G.mjs';
24
- export { applyOnDelete, fanOutScalarCounts, resolveWith, runRowValidators } from './packem_shared/applyOnDelete-CAwZfp-5.mjs';
25
- export { RLS_UNWRAP_SYMBOL, RlsRequiredError, guardWriter } from './packem_shared/RLS_UNWRAP_SYMBOL-DnjkqVgY.mjs';
22
+ export { serveRelationFanout } from './packem_shared/serveRelationFanout-C56hKu1O.mjs';
23
+ export { DEFAULT_MAX_RELATION_KEYS, assertFlatPredicate, assertShapeShardable, containsRelationPredicate, isRelationPredicate, resolveRelationPredicates } from './packem_shared/DEFAULT_MAX_RELATION_KEYS-BEan1CRD.mjs';
24
+ export { applyOnDelete, fanOutScalarCounts, resolveWith, runRowValidators } from './packem_shared/applyOnDelete-BXSq3S70.mjs';
25
+ export { RLS_UNWRAP_SYMBOL, RlsRequiredError, guardWriter } from './packem_shared/RLS_UNWRAP_SYMBOL-G1eNK4DQ.mjs';
26
26
  export { buildFtsMatch, ftsTableName, scoreDocument, stringifySearchText, tokenizeSearch } from './packem_shared/buildFtsMatch-BLEMawrp.mjs';
27
27
  export { M as MIN_ADMIN_TOKEN_LENGTH, a as MIN_AUTH_SECRET_LENGTH, b as buildSecurityAudit } from './packem_shared/security-audit-CucgBice.mjs';
28
28
  export { SESSION_DO_TTL_DEFAULT, SessionDO } from './packem_shared/SESSION_DO_TTL_DEFAULT-D71QAL5h.mjs';
29
- export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-KQQiPDQ3.mjs';
29
+ export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-CpURtawE.mjs';
30
30
  export { SHARD_REGISTRY_DO_NAME, ShardRegistryDO } from './packem_shared/SHARD_REGISTRY_DO_NAME-D99roc-r.mjs';
31
- export { MAX_SQL_ROWS, assertReadonly, runReadonlySql } from './packem_shared/MAX_SQL_ROWS-dDcFE1YZ.mjs';
31
+ export { MAX_SQL_ROWS, assertReadonly, runReadonlySql } from './packem_shared/MAX_SQL_ROWS-D57CJaT9.mjs';
32
32
  export { createSystemReader } from './packem_shared/createSystemReader-D12eNH13.mjs';
33
33
  export { ConflictError } from './packem_shared/ConflictError-CLoq37xH.mjs';
34
34
  export { hasTrigger, runTriggers } from './packem_shared/hasTrigger-5N6_Fx0A.mjs';
@@ -1,3 +1,5 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
2
4
 
3
5
  const ADMIN_FUNCTION_PREFIX = "__lunora_admin__:";
@@ -179,7 +181,7 @@ const buildOrderBy = (orderBy, physicalColumns) => {
179
181
  const readTablePage = (sql, options) => {
180
182
  const { table } = options;
181
183
  if (isInternalTable(table) || !tableExists(sql, table)) {
182
- throw Object.assign(new Error(`unknown table: ${table}`), { code: "UNKNOWN_TABLE", name: "LunoraError", status: 404 });
184
+ throw new LunoraError("UNKNOWN_TABLE", `unknown table: ${table}`, { status: 404 });
183
185
  }
184
186
  const limit = clamp(Math.trunc(options.limit ?? DEFAULT_PAGE_SIZE), 1, MAX_PAGE_SIZE);
185
187
  const offset = Math.max(0, Math.trunc(options.offset ?? 0));
@@ -215,7 +217,7 @@ const readTablePage = (sql, options) => {
215
217
  const selectMatchingIds = (sql, options) => {
216
218
  const { table } = options;
217
219
  if (isInternalTable(table) || !tableExists(sql, table)) {
218
- throw Object.assign(new Error(`unknown table: ${table}`), { code: "UNKNOWN_TABLE", name: "LunoraError", status: 404 });
220
+ throw new LunoraError("UNKNOWN_TABLE", `unknown table: ${table}`, { status: 404 });
219
221
  }
220
222
  const limit = clamp(Math.trunc(options.limit ?? MAX_PAGE_SIZE), 1, MAX_PAGE_SIZE);
221
223
  const quoted = quoteIdentifier(table);
@@ -246,16 +248,16 @@ const knownDisplayColumns = (sql, quotedTable, physicalColumns) => {
246
248
  const facetColumn = (sql, options) => {
247
249
  const { column, table } = options;
248
250
  if (isInternalTable(table) || !tableExists(sql, table)) {
249
- throw Object.assign(new Error(`unknown table: ${table}`), { code: "UNKNOWN_TABLE", name: "LunoraError", status: 404 });
251
+ throw new LunoraError("UNKNOWN_TABLE", `unknown table: ${table}`, { status: 404 });
250
252
  }
251
253
  const quoted = quoteIdentifier(table);
252
254
  const physicalColumns = sql.exec(`PRAGMA table_info(${quoted})`).toArray().map((info) => info.name);
253
255
  if (!knownDisplayColumns(sql, quoted, physicalColumns).has(column)) {
254
- throw Object.assign(new Error(`unknown column: ${column}`), { code: "UNKNOWN_COLUMN", name: "LunoraError", status: 404 });
256
+ throw new LunoraError("UNKNOWN_COLUMN", `unknown column: ${column}`, { status: 404 });
255
257
  }
256
258
  const resolved = resolveColumnExpression(column, physicalColumns);
257
259
  if (resolved === void 0) {
258
- throw Object.assign(new Error(`unknown column: ${column}`), { code: "UNKNOWN_COLUMN", name: "LunoraError", status: 404 });
260
+ throw new LunoraError("UNKNOWN_COLUMN", `unknown column: ${column}`, { status: 404 });
259
261
  }
260
262
  const limit = clamp(Math.trunc(options.limit ?? DEFAULT_FACET_LIMIT), 1, MAX_FACET_LIMIT);
261
263
  const needle = options.search?.trim() ?? "";
@@ -1,5 +1,5 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
- import { distinctValues } from './applyOnDelete-CAwZfp-5.mjs';
2
+ import { distinctValues } from './applyOnDelete-BXSq3S70.mjs';
3
3
 
4
4
  const RELATION_EXISTS_KEY = "__relationExists";
5
5
 
@@ -1,10 +1,12 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const MAX_SQL_ROWS = 1e3;
2
4
  const READONLY_LEAD = /^(?:explain\s+(?:query\s+plan\s+)?)?(?:select|with)\b/iu;
3
5
  const FORBIDDEN_KEYWORD = /\b(?:alter|attach|create|delete|detach|drop|insert|pragma|reindex|replace|truncate|update|vacuum)\b/iu;
4
6
  const LEADING_NOISE = /^(?:\s|--[^\n]*\n?|\/\*[\s\S]*?\*\/)+/u;
5
7
  const TRAILING_SEMICOLON = /;\s*$/u;
6
8
  const stripLeading = (sql) => sql.replace(LEADING_NOISE, "");
7
- const sqlError = (message, code) => Object.assign(new Error(message), { code, name: "LunoraError", status: 400 });
9
+ const sqlError = (message, code) => new LunoraError(code, message, { status: 400 });
8
10
  const assertReadonly = (query) => {
9
11
  const trimmed = stripLeading(query).trim();
10
12
  if (trimmed === "") {
@@ -12,11 +12,11 @@ import { sortColumnName, matchesRankStaticWhere, encodePartitionKey, rankTableNa
12
12
  import { stringifySearchText, ftsTableName, tokenizeSearch, buildFtsMatch, scoreDocument } from './buildFtsMatch-BLEMawrp.mjs';
13
13
  import { s as serializeSqlValue } from './serialize-sql-BlRUoiQe.mjs';
14
14
  import { SCAN_DEP } from './SCAN_DEP-DLJF8dsj.mjs';
15
- import { decodeCursor, normalizeOrderKeys, buildSeekWhere, applySelect, encodeCursor, softDeleteScope, buildSeekBeforeWhere } from './applySelect-BvZdFUBT.mjs';
15
+ import { decodeCursor, normalizeOrderKeys, buildSeekWhere, applySelect, encodeCursor, softDeleteScope, buildSeekBeforeWhere } from './applySelect-WQY8m62C.mjs';
16
16
  import NotFoundError from './NotFoundError-C70b9hLw.mjs';
17
- import { assertFlatPredicate, resolveRelationPredicates } from './DEFAULT_MAX_RELATION_KEYS-CjM9Y1_G.mjs';
18
- import { runRowValidators, resolveWith, applyOnDelete, fanOutScalarCounts } from './applyOnDelete-CAwZfp-5.mjs';
19
- import { guardWriter } from './RLS_UNWRAP_SYMBOL-DnjkqVgY.mjs';
17
+ import { assertFlatPredicate, resolveRelationPredicates } from './DEFAULT_MAX_RELATION_KEYS-BEan1CRD.mjs';
18
+ import { runRowValidators, resolveWith, applyOnDelete, fanOutScalarCounts } from './applyOnDelete-BXSq3S70.mjs';
19
+ import { guardWriter } from './RLS_UNWRAP_SYMBOL-G1eNK4DQ.mjs';
20
20
  import { createSystemReader } from './createSystemReader-D12eNH13.mjs';
21
21
  import { ConflictError } from './ConflictError-CLoq37xH.mjs';
22
22
  import { runTriggers } from './hasTrigger-5N6_Fx0A.mjs';
@@ -483,11 +483,11 @@ const DEFAULT_BATCH_LIMIT = 500;
483
483
  const assertBatchLimit = (count, limit, op) => {
484
484
  const cap = limit ?? DEFAULT_BATCH_LIMIT;
485
485
  if (count > cap) {
486
- throw Object.assign(new Error(`${op}: batch of ${String(count)} exceeds the limit of ${String(cap)} (raise options.limit or chunk the call)`), {
487
- code: "BATCH_LIMIT_EXCEEDED",
488
- name: "LunoraError",
489
- status: 400
490
- });
486
+ throw new LunoraError(
487
+ "BATCH_LIMIT_EXCEEDED",
488
+ `${op}: batch of ${String(count)} exceeds the limit of ${String(cap)} (raise options.limit or chunk the call)`,
489
+ { status: 400 }
490
+ );
491
491
  }
492
492
  };
493
493
  const createRangeBuilder = (stage) => {
@@ -1325,6 +1325,25 @@ const createShardCtxDb = (options) => {
1325
1325
  }
1326
1326
  return { deleted: ids.length };
1327
1327
  },
1328
+ async deleteWhere(tableName, where, batchOptions) {
1329
+ const global = globalWriterFor(tableName, "deleteWhere");
1330
+ let ids;
1331
+ if (global) {
1332
+ const rows = await global.findMany(tableName, { where });
1333
+ ids = rows.page.map((row) => String(row["_id"]));
1334
+ } else {
1335
+ if (!schema.tables[tableName]) {
1336
+ throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
1337
+ }
1338
+ const page = await writer.findMany(tableName, { where });
1339
+ ids = page.page.map((row) => String(row["_id"]));
1340
+ }
1341
+ assertBatchLimit(ids.length, batchOptions?.limit, "deleteWhere");
1342
+ if (writer.deleteMany === void 0) {
1343
+ throw new LunoraError("INTERNAL", `ctx.db.${tableName}.deleteMany is unavailable: this writer has no batch delete`);
1344
+ }
1345
+ return writer.deleteMany(ids, batchOptions);
1346
+ },
1328
1347
  async findFirst(tableName, args = {}) {
1329
1348
  const result = await writer.findMany(tableName, { ...args, limit: 1 });
1330
1349
  return result.page[0] ?? null;
@@ -1614,9 +1633,18 @@ const createShardCtxDb = (options) => {
1614
1633
  },
1615
1634
  async insertMany(tableName, documents, batchOptions) {
1616
1635
  assertBatchLimit(documents.length, batchOptions?.limit, "insertMany");
1636
+ const skipDuplicates = batchOptions?.skipDuplicates === true;
1617
1637
  const ids = [];
1618
1638
  for (const document of documents) {
1619
- ids.push(await writer.insert(tableName, document));
1639
+ try {
1640
+ ids.push(await writer.insert(tableName, document));
1641
+ } catch (error) {
1642
+ if (skipDuplicates && error instanceof ConflictError && error.kind === "unique") {
1643
+ ids.push(null);
1644
+ } else {
1645
+ throw error;
1646
+ }
1647
+ }
1620
1648
  }
1621
1649
  return ids;
1622
1650
  },
@@ -1669,6 +1697,31 @@ const createShardCtxDb = (options) => {
1669
1697
  for (const entry of patches) {
1670
1698
  await writer.patch(entry.id, entry.patch, expectedTable);
1671
1699
  }
1700
+ return { patched: patches.length };
1701
+ },
1702
+ async patchWhere(tableName, args, batchOptions) {
1703
+ const global = globalWriterFor(tableName, "patchWhere");
1704
+ let patches;
1705
+ if (global) {
1706
+ const rows = await global.findMany(tableName, { where: args.where });
1707
+ patches = rows.page.map((row) => {
1708
+ return { id: String(row["_id"]), patch: args.patch };
1709
+ });
1710
+ } else {
1711
+ if (!schema.tables[tableName]) {
1712
+ throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
1713
+ }
1714
+ const page = await writer.findMany(tableName, { where: args.where });
1715
+ patches = page.page.map((row) => {
1716
+ return { id: String(row["_id"]), patch: args.patch };
1717
+ });
1718
+ }
1719
+ assertBatchLimit(patches.length, batchOptions?.limit, "patchWhere");
1720
+ if (writer.patchMany === void 0) {
1721
+ throw new LunoraError("INTERNAL", `ctx.db.${tableName}.patchMany is unavailable: this writer has no batch patch`);
1722
+ }
1723
+ await writer.patchMany(patches, batchOptions);
1724
+ return { patched: patches.length };
1672
1725
  },
1673
1726
  query(tableName) {
1674
1727
  const global = globalWriterFor(tableName, "query");
@@ -58,6 +58,10 @@ const guardWriter = (raw, schema, tableOfId) => {
58
58
  }
59
59
  return base.deleteMany(ids, options, expectedTable);
60
60
  },
61
+ deleteWhere: base.deleteWhere ? async (tableName, where, options) => {
62
+ guardTable(tableName);
63
+ return await base.deleteWhere?.(tableName, where, options);
64
+ } : void 0,
61
65
  findFirst: (tableName, args) => {
62
66
  guardTable(tableName);
63
67
  return base.findFirst(tableName, args);
@@ -100,6 +104,10 @@ const guardWriter = (raw, schema, tableOfId) => {
100
104
  }
101
105
  return base.patchMany(patches, options, expectedTable);
102
106
  },
107
+ patchWhere: base.patchWhere ? async (tableName, args, options) => {
108
+ guardTable(tableName);
109
+ return await base.patchWhere?.(tableName, args, options);
110
+ } : void 0,
103
111
  query: (tableName) => {
104
112
  guardTable(tableName);
105
113
  return base.query(tableName);
@@ -7,15 +7,15 @@ import { recordAuthEvent, readAuthMetrics } from './AUTH_METRICS_BUCKETS_TABLE-C
7
7
  import { DATA_MIGRATION_STATE_TABLE, readMigrationStatus } from './DATA_MIGRATION_STATE_TABLE-DB3IYUR3.mjs';
8
8
  import { SCAN_DEP, createDependencyTracker, tableFromDepKey } from './SCAN_DEP-DLJF8dsj.mjs';
9
9
  import { readFunctionMetricsTotals, readFunctionMetricIndexHits, recordFunctionMetric, mergeScanAttribution, readFunctionMetrics, readFunctionMetricBuckets } from './FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
10
- import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, summarizeFanoutTopics, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, recordFanoutPass, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-BbQdj8h1.mjs';
10
+ import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, summarizeFanoutTopics, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, recordFanoutPass, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-Bxdijn66.mjs';
11
11
  import { LogBuffer } from './LogBuffer-B_Ezju_N.mjs';
12
12
  import { recordCapturedMail, clearCapturedMail, readCapturedMail, MAIL_TABLE } from './MAIL_RETENTION-CPpgl-dX.mjs';
13
- import { readBookmark, armRestore } from './armRestore-BJk53Ro8.mjs';
13
+ import { readBookmark, armRestore } from './armRestore-4Px61hHS.mjs';
14
14
  import { ReactiveCache, reactiveCacheKey } from './ReactiveCache-BYlSGY0N.mjs';
15
15
  import { stableStringify } from './stableStringify-MydiuScU.mjs';
16
16
  import { redact, standardRules } from '@visulima/redact';
17
17
  import { i as isDevEnvironment, c as buildSettings, b as buildSecurityAudit } from './security-audit-CucgBice.mjs';
18
- import { runReadonlySql } from './MAX_SQL_ROWS-dDcFE1YZ.mjs';
18
+ import { runReadonlySql } from './MAX_SQL_ROWS-D57CJaT9.mjs';
19
19
  import { ConflictError } from './ConflictError-CLoq37xH.mjs';
20
20
  import { e as deleteGlobalShapeSnapshotsForConnection, g as readIdempotent, h as writeIdempotent, t as trimIdempotent, r as readClientWatermark, m as migrateClientWatermark, c as advanceClientWatermark, d as deleteGlobalShapeSnapshot, f as readGlobalShapeSnapshot, w as writeGlobalShapeSnapshot } from './ctx-db-idempotency-BdcNpvY4.mjs';
21
21
  import { CDC_LOG_TABLE, readCdcChanges, readCdcCursor, readCdcEpoch, minCdcSeq, bumpCdcEpoch } from './CDC_LOG_TABLE-DZSVKRr7.mjs';
@@ -1404,7 +1404,7 @@ const setsIntersect = (a, b) => {
1404
1404
  const parseRunMigrationArgs = (args) => {
1405
1405
  const id = typeof args["id"] === "string" ? args["id"] : "";
1406
1406
  if (id.trim() === "") {
1407
- throw Object.assign(new Error("runMigration: `id` is required"), { code: "MIGRATION_ID_REQUIRED", name: "LunoraError", status: 400 });
1407
+ throw new LunoraError("MIGRATION_ID_REQUIRED", "runMigration: `id` is required", { status: 400 });
1408
1408
  }
1409
1409
  return {
1410
1410
  batchSize: typeof args["batchSize"] === "number" ? args["batchSize"] : void 0,
@@ -1419,25 +1419,25 @@ const parseWriteRowArgs = (args) => {
1419
1419
  const { op } = args;
1420
1420
  const table = typeof args["table"] === "string" ? args["table"] : "";
1421
1421
  if (op !== "insert" && op !== "patch" && op !== "replace" && op !== "delete") {
1422
- throw Object.assign(new Error("writeRow: `op` must be insert|patch|replace|delete"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1422
+ throw new LunoraError("BAD_REQUEST", "writeRow: `op` must be insert|patch|replace|delete");
1423
1423
  }
1424
1424
  if (table.trim() === "") {
1425
- throw Object.assign(new Error("writeRow: `table` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1425
+ throw new LunoraError("BAD_REQUEST", "writeRow: `table` is required");
1426
1426
  }
1427
1427
  const id = typeof args["id"] === "string" ? args["id"] : void 0;
1428
1428
  const record = typeof args["doc"] === "object" && args["doc"] !== null && !Array.isArray(args["doc"]) ? args["doc"] : void 0;
1429
1429
  if (op !== "insert" && (id === void 0 || id === "")) {
1430
- throw Object.assign(new Error(`writeRow: \`id\` is required for op "${op}"`), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1430
+ throw new LunoraError("BAD_REQUEST", `writeRow: \`id\` is required for op "${op}"`);
1431
1431
  }
1432
1432
  if (op !== "delete" && record === void 0) {
1433
- throw Object.assign(new Error(`writeRow: \`doc\` is required for op "${op}"`), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1433
+ throw new LunoraError("BAD_REQUEST", `writeRow: \`doc\` is required for op "${op}"`);
1434
1434
  }
1435
1435
  return { doc: record, id, op, table };
1436
1436
  };
1437
1437
  const parseCreateWorkflowInstanceArgs = (args) => {
1438
1438
  const exportName = typeof args["exportName"] === "string" ? args["exportName"].trim() : "";
1439
1439
  if (exportName === "") {
1440
- throw Object.assign(new Error("createWorkflowInstance: `exportName` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1440
+ throw new LunoraError("BAD_REQUEST", "createWorkflowInstance: `exportName` is required");
1441
1441
  }
1442
1442
  const id = typeof args["id"] === "string" && args["id"] !== "" ? args["id"] : void 0;
1443
1443
  return { exportName, id, params: args["params"] };
@@ -1446,10 +1446,10 @@ const parseGetWorkflowInstanceStatusArgs = (args) => {
1446
1446
  const exportName = typeof args["exportName"] === "string" ? args["exportName"].trim() : "";
1447
1447
  const id = typeof args["id"] === "string" ? args["id"].trim() : "";
1448
1448
  if (exportName === "") {
1449
- throw Object.assign(new Error("getWorkflowInstanceStatus: `exportName` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1449
+ throw new LunoraError("BAD_REQUEST", "getWorkflowInstanceStatus: `exportName` is required");
1450
1450
  }
1451
1451
  if (id === "") {
1452
- throw Object.assign(new Error("getWorkflowInstanceStatus: `id` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1452
+ throw new LunoraError("BAD_REQUEST", "getWorkflowInstanceStatus: `id` is required");
1453
1453
  }
1454
1454
  return { exportName, id };
1455
1455
  };
@@ -1504,7 +1504,7 @@ const parseTablePageOrderBy = (raw) => {
1504
1504
  const parseBulkDeleteArgs = (args) => {
1505
1505
  const table = typeof args["table"] === "string" ? args["table"] : "";
1506
1506
  if (table.trim() === "") {
1507
- throw Object.assign(new Error("deleteRows: `table` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1507
+ throw new LunoraError("BAD_REQUEST", "deleteRows: `table` is required");
1508
1508
  }
1509
1509
  return {
1510
1510
  filters: parseTablePageFilters(args["filters"]),
@@ -1516,31 +1516,27 @@ const parseBulkDeleteArgs = (args) => {
1516
1516
  const parseClearTableArgs = (args) => {
1517
1517
  const table = typeof args["table"] === "string" ? args["table"] : "";
1518
1518
  if (table.trim() === "") {
1519
- throw Object.assign(new Error("clearTable: `table` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1519
+ throw new LunoraError("BAD_REQUEST", "clearTable: `table` is required");
1520
1520
  }
1521
1521
  return { limit: typeof args["limit"] === "number" ? args["limit"] : void 0, table };
1522
1522
  };
1523
1523
  const parseRecordAuthEventArgs = (args) => {
1524
1524
  const { outcome } = args;
1525
1525
  if (outcome !== "ok" && outcome !== "fail") {
1526
- throw Object.assign(new Error('recordAuthEvent: `outcome` must be "ok" or "fail"'), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1526
+ throw new LunoraError("BAD_REQUEST", 'recordAuthEvent: `outcome` must be "ok" or "fail"');
1527
1527
  }
1528
1528
  return { outcome };
1529
1529
  };
1530
1530
  const parseRecordContainerEventArgs = (args) => {
1531
1531
  const raw = args["event"];
1532
1532
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1533
- throw Object.assign(new Error("recordContainerEvent: `event` must be an object"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1533
+ throw new LunoraError("BAD_REQUEST", "recordContainerEvent: `event` must be an object");
1534
1534
  }
1535
1535
  const envelope = raw;
1536
1536
  const container = typeof envelope["container"] === "string" ? envelope["container"] : "";
1537
1537
  const event = typeof envelope["event"] === "string" ? envelope["event"] : "";
1538
1538
  if (container.trim() === "" || event.trim() === "") {
1539
- throw Object.assign(new Error("recordContainerEvent: `event.container` and `event.event` are required"), {
1540
- code: "BAD_REQUEST",
1541
- name: "LunoraError",
1542
- status: 400
1543
- });
1539
+ throw new LunoraError("BAD_REQUEST", "recordContainerEvent: `event.container` and `event.event` are required");
1544
1540
  }
1545
1541
  const level = envelope["level"] === "error" ? "error" : "info";
1546
1542
  const detail = typeof envelope["message"] === "string" ? envelope["message"] : void 0;
@@ -1556,21 +1552,21 @@ const parseRunAsArgs = (args) => {
1556
1552
  const functionPath = typeof args["functionPath"] === "string" ? args["functionPath"] : "";
1557
1553
  const userId = typeof args["userId"] === "string" ? args["userId"] : "";
1558
1554
  if (functionPath.trim() === "") {
1559
- throw Object.assign(new Error("runAs: `functionPath` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1555
+ throw new LunoraError("BAD_REQUEST", "runAs: `functionPath` is required");
1560
1556
  }
1561
1557
  if (functionPath.startsWith(ADMIN_FUNCTION_PREFIX)) {
1562
- throw Object.assign(new Error("runAs: cannot target a reserved admin function"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1558
+ throw new LunoraError("BAD_REQUEST", "runAs: cannot target a reserved admin function");
1563
1559
  }
1564
1560
  if (userId.trim() === "") {
1565
- throw Object.assign(new Error("runAs: `userId` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1561
+ throw new LunoraError("BAD_REQUEST", "runAs: `userId` is required");
1566
1562
  }
1567
1563
  const rawArgs = args["args"];
1568
1564
  if (rawArgs !== void 0 && (typeof rawArgs !== "object" || rawArgs === null || Array.isArray(rawArgs))) {
1569
- throw Object.assign(new Error("runAs: `args` must be an object"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1565
+ throw new LunoraError("BAD_REQUEST", "runAs: `args` must be an object");
1570
1566
  }
1571
1567
  const rawIdentity = args["identity"];
1572
1568
  if (rawIdentity !== void 0 && (typeof rawIdentity !== "object" || rawIdentity === null || Array.isArray(rawIdentity))) {
1573
- throw Object.assign(new Error("runAs: `identity` must be an object"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1569
+ throw new LunoraError("BAD_REQUEST", "runAs: `identity` must be an object");
1574
1570
  }
1575
1571
  return {
1576
1572
  args: rawArgs === void 0 ? {} : rawArgs,
@@ -1581,7 +1577,7 @@ const parseRunAsArgs = (args) => {
1581
1577
  };
1582
1578
  const parseRecordMailArgs = (args) => {
1583
1579
  const bad = (message) => {
1584
- throw Object.assign(new Error(`recordMail: ${message}`), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1580
+ throw new LunoraError("BAD_REQUEST", `recordMail: ${message}`);
1585
1581
  };
1586
1582
  const { bcc, cc, from, headers, html, replyTo, subject, text, to } = args;
1587
1583
  if (typeof subject !== "string") {
@@ -1622,7 +1618,7 @@ const TEST_MAIL_DEFAULT_TO = "test@lunora.sh";
1622
1618
  const buildTestMailInput = (args) => {
1623
1619
  const { to } = args;
1624
1620
  if (to !== void 0 && typeof to !== "string") {
1625
- throw Object.assign(new Error("sendTestMail: `to` must be a string"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1621
+ throw new LunoraError("BAD_REQUEST", "sendTestMail: `to` must be a string");
1626
1622
  }
1627
1623
  const recipient = to ?? TEST_MAIL_DEFAULT_TO;
1628
1624
  const link = "https://example.test/verify?token=demo";
@@ -1638,7 +1634,7 @@ Verify your email: ${link}`,
1638
1634
  };
1639
1635
  const parseRecordQueueMessageArgs = (args) => {
1640
1636
  const bad = (message) => {
1641
- throw Object.assign(new Error(`recordQueueMessage: ${message}`), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1637
+ throw new LunoraError("BAD_REQUEST", `recordQueueMessage: ${message}`);
1642
1638
  };
1643
1639
  const raw = args["messages"];
1644
1640
  if (!Array.isArray(raw)) {
@@ -1680,23 +1676,15 @@ const MAX_QUEUE_SEND_BATCH = 100;
1680
1676
  const parseSendQueueMessageArgs = (args) => {
1681
1677
  const exportName = typeof args["exportName"] === "string" ? args["exportName"].trim() : "";
1682
1678
  if (exportName === "") {
1683
- throw Object.assign(new Error("sendQueueMessage: `exportName` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1679
+ throw new LunoraError("BAD_REQUEST", "sendQueueMessage: `exportName` is required");
1684
1680
  }
1685
1681
  const delayRaw = args["delaySeconds"];
1686
1682
  if (delayRaw !== void 0 && (typeof delayRaw !== "number" || !Number.isFinite(delayRaw) || delayRaw < 0)) {
1687
- throw Object.assign(new Error("sendQueueMessage: `delaySeconds` must be a non-negative number"), {
1688
- code: "BAD_REQUEST",
1689
- name: "LunoraError",
1690
- status: 400
1691
- });
1683
+ throw new LunoraError("BAD_REQUEST", "sendQueueMessage: `delaySeconds` must be a non-negative number");
1692
1684
  }
1693
1685
  const batch = Array.isArray(args["batch"]) ? args["batch"] : void 0;
1694
1686
  if (batch !== void 0 && (batch.length === 0 || batch.length > MAX_QUEUE_SEND_BATCH)) {
1695
- throw Object.assign(new Error(`sendQueueMessage: \`batch\` must contain between 1 and ${String(MAX_QUEUE_SEND_BATCH)} messages`), {
1696
- code: "BAD_REQUEST",
1697
- name: "LunoraError",
1698
- status: 400
1699
- });
1687
+ throw new LunoraError("BAD_REQUEST", `sendQueueMessage: \`batch\` must contain between 1 and ${String(MAX_QUEUE_SEND_BATCH)} messages`);
1700
1688
  }
1701
1689
  return {
1702
1690
  batch,
@@ -1709,7 +1697,7 @@ const parseSendQueueMessageArgs = (args) => {
1709
1697
  const parseReplayQueueMessageArgs = (args) => {
1710
1698
  const id = typeof args["id"] === "string" ? args["id"].trim() : "";
1711
1699
  if (id === "") {
1712
- throw Object.assign(new Error("replayQueueMessage: `id` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1700
+ throw new LunoraError("BAD_REQUEST", "replayQueueMessage: `id` is required");
1713
1701
  }
1714
1702
  const target = typeof args["target"] === "string" && args["target"].trim() !== "" ? args["target"].trim() : void 0;
1715
1703
  return { id, target };
@@ -1719,24 +1707,24 @@ const parseRankBeforeArgs = (args) => {
1719
1707
  const index = typeof args["index"] === "string" ? args["index"] : "";
1720
1708
  const rowId = typeof args["rowId"] === "string" ? args["rowId"] : "";
1721
1709
  if (table.trim() === "") {
1722
- throw Object.assign(new Error("rankBefore: `table` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1710
+ throw new LunoraError("BAD_REQUEST", "rankBefore: `table` is required");
1723
1711
  }
1724
1712
  if (index.trim() === "") {
1725
- throw Object.assign(new Error("rankBefore: `index` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1713
+ throw new LunoraError("BAD_REQUEST", "rankBefore: `index` is required");
1726
1714
  }
1727
1715
  if (typeof args["partitionKey"] !== "string") {
1728
- throw Object.assign(new Error("rankBefore: `partitionKey` must be a string"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1716
+ throw new LunoraError("BAD_REQUEST", "rankBefore: `partitionKey` must be a string");
1729
1717
  }
1730
1718
  if (rowId.trim() === "") {
1731
- throw Object.assign(new Error("rankBefore: `rowId` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1719
+ throw new LunoraError("BAD_REQUEST", "rankBefore: `rowId` is required");
1732
1720
  }
1733
1721
  if (!Array.isArray(args["sortValues"])) {
1734
- throw Object.assign(new Error("rankBefore: `sortValues` must be an array"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1722
+ throw new LunoraError("BAD_REQUEST", "rankBefore: `sortValues` must be an array");
1735
1723
  }
1736
1724
  return { index, partitionKey: args["partitionKey"], rowId, sortValues: args["sortValues"], table };
1737
1725
  };
1738
1726
  const badRequest = (message) => {
1739
- throw Object.assign(new Error(message), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1727
+ throw new LunoraError("BAD_REQUEST", message);
1740
1728
  };
1741
1729
  const requireNonEmptyString = (value, field) => {
1742
1730
  if (typeof value !== "string" || value.trim() === "") {
@@ -1796,7 +1784,7 @@ const decodeIndexHitKey = (key) => {
1796
1784
  const parseApplyCdcArgs = (args) => {
1797
1785
  const raw = args["changes"];
1798
1786
  if (!Array.isArray(raw)) {
1799
- throw Object.assign(new Error("applyCdc: `changes` must be an array"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1787
+ throw new LunoraError("BAD_REQUEST", "applyCdc: `changes` must be an array");
1800
1788
  }
1801
1789
  const changes = raw.map((entry, index) => {
1802
1790
  const record = entry;
@@ -1804,27 +1792,15 @@ const parseApplyCdcArgs = (args) => {
1804
1792
  const table = typeof record["table"] === "string" ? record["table"] : "";
1805
1793
  const id = typeof record["id"] === "string" ? record["id"] : "";
1806
1794
  if (table === "" || id === "" || op !== "insert" && op !== "update" && op !== "delete") {
1807
- throw Object.assign(new Error(`applyCdc: changes[${String(index)}] must have a table, id, and op of insert|update|delete`), {
1808
- code: "BAD_REQUEST",
1809
- name: "LunoraError",
1810
- status: 400
1811
- });
1795
+ throw new LunoraError("BAD_REQUEST", `applyCdc: changes[${String(index)}] must have a table, id, and op of insert|update|delete`);
1812
1796
  }
1813
1797
  const rawDocument = record["doc"];
1814
1798
  if (rawDocument !== void 0 && (typeof rawDocument !== "object" || rawDocument === null || Array.isArray(rawDocument))) {
1815
- throw Object.assign(new Error(`applyCdc: changes[${String(index)}].doc must be an object`), {
1816
- code: "BAD_REQUEST",
1817
- name: "LunoraError",
1818
- status: 400
1819
- });
1799
+ throw new LunoraError("BAD_REQUEST", `applyCdc: changes[${String(index)}].doc must be an object`);
1820
1800
  }
1821
1801
  const document = rawDocument;
1822
1802
  if (document !== void 0 && typeof document["_id"] === "string" && document["_id"] !== id) {
1823
- throw Object.assign(new Error(`applyCdc: changes[${String(index)}].doc._id must match the entry id`), {
1824
- code: "BAD_REQUEST",
1825
- name: "LunoraError",
1826
- status: 400
1827
- });
1803
+ throw new LunoraError("BAD_REQUEST", `applyCdc: changes[${String(index)}].doc._id must match the entry id`);
1828
1804
  }
1829
1805
  return {
1830
1806
  doc: document,
@@ -2680,9 +2656,7 @@ class ShardDO {
2680
2656
  */
2681
2657
  // eslint-disable-next-line class-methods-use-this -- base-class override hook: the codegen subclass overrides this with a schema-aware reader that uses `this`
2682
2658
  runRelationFanoutRead(_functionPath, _args) {
2683
- throw Object.assign(new Error("__lunora_relation__: no schema bound — the base ShardDO cannot serve cross-shard relation reads"), {
2684
- code: "NOT_IMPLEMENTED",
2685
- name: "LunoraError",
2659
+ throw new LunoraError("NOT_IMPLEMENTED", "__lunora_relation__: no schema bound — the base ShardDO cannot serve cross-shard relation reads", {
2686
2660
  status: 500
2687
2661
  });
2688
2662
  }
@@ -2809,19 +2783,11 @@ class ShardDO {
2809
2783
  */
2810
2784
  async runInTransaction(handler) {
2811
2785
  if (this.transactionDepth > 0) {
2812
- throw Object.assign(new Error("nested transactions are not supported in SQLite-in-DO"), {
2813
- code: "NESTED_TRANSACTION",
2814
- name: "LunoraError",
2815
- status: 500
2816
- });
2786
+ throw new LunoraError("NESTED_TRANSACTION", "nested transactions are not supported in SQLite-in-DO", { status: 500 });
2817
2787
  }
2818
2788
  const sqlHandle = this.state.storage.sql;
2819
2789
  if (!sqlHandle || typeof sqlHandle.exec !== "function") {
2820
- throw Object.assign(new Error("storage.sql is not available on this ShardDO state"), {
2821
- code: "SQL_UNAVAILABLE",
2822
- name: "LunoraError",
2823
- status: 500
2824
- });
2790
+ throw new LunoraError("SQL_UNAVAILABLE", "storage.sql is not available on this ShardDO state", { status: 500 });
2825
2791
  }
2826
2792
  const transactionalStorage = this.state.storage;
2827
2793
  const run = async () => {
@@ -2901,9 +2867,7 @@ class ShardDO {
2901
2867
  */
2902
2868
  // eslint-disable-next-line class-methods-use-this -- base-class override hook: the codegen subclass overrides this and uses `this` to reach the generated migration registry
2903
2869
  runShardDataMigration(args) {
2904
- return Promise.reject(
2905
- Object.assign(new Error(`data migration "${args.id}" is not registered`), { code: "MIGRATION_NOT_FOUND", name: "LunoraError", status: 404 })
2906
- );
2870
+ return Promise.reject(new LunoraError("MIGRATION_NOT_FOUND", `data migration "${args.id}" is not registered`, { status: 404 }));
2907
2871
  }
2908
2872
  /**
2909
2873
  * Lazily provision the shard's physical tables before an operation that
@@ -3161,7 +3125,7 @@ class ShardDO {
3161
3125
  */
3162
3126
  // eslint-disable-next-line class-methods-use-this -- base-class override hook: the codegen subclass overrides this and uses `this` to build a schema-aware writer
3163
3127
  runShardWrite(args) {
3164
- return Promise.reject(Object.assign(new Error(`unknown table: ${args.table}`), { code: "UNKNOWN_TABLE", name: "LunoraError", status: 404 }));
3128
+ return Promise.reject(new LunoraError("UNKNOWN_TABLE", `unknown table: ${args.table}`, { status: 404 }));
3165
3129
  }
3166
3130
  /**
3167
3131
  * Delete one row by primary key THROUGH the schema-aware writer — the
@@ -3176,7 +3140,7 @@ class ShardDO {
3176
3140
  */
3177
3141
  // eslint-disable-next-line class-methods-use-this -- base-class override hook: the codegen subclass overrides this and uses `this` to build a schema-aware writer
3178
3142
  deleteRowThroughWriter(_table, _id) {
3179
- return Promise.reject(Object.assign(new Error(`unknown table: ${_table}`), { code: "UNKNOWN_TABLE", name: "LunoraError", status: 404 }));
3143
+ return Promise.reject(new LunoraError("UNKNOWN_TABLE", `unknown table: ${_table}`, { status: 404 }));
3180
3144
  }
3181
3145
  /**
3182
3146
  * Bulk-delete the rows of `table` matching the active `filters`/`search`
@@ -3218,9 +3182,7 @@ class ShardDO {
3218
3182
  */
3219
3183
  // eslint-disable-next-line class-methods-use-this -- base-class override hook: the codegen subclass overrides this and uses `this` to build a schema-aware writer
3220
3184
  runShardRankBefore(_args) {
3221
- return Promise.reject(
3222
- Object.assign(new Error("rankBefore is not implemented in base ShardDO"), { code: "NOT_IMPLEMENTED", name: "LunoraError", status: 500 })
3223
- );
3185
+ return Promise.reject(new LunoraError("NOT_IMPLEMENTED", "rankBefore is not implemented in base ShardDO", { status: 500 }));
3224
3186
  }
3225
3187
  /**
3226
3188
  * Page this shard's local ranked slice under `index`, each row tagged with
@@ -3234,9 +3196,7 @@ class ShardDO {
3234
3196
  */
3235
3197
  // eslint-disable-next-line class-methods-use-this -- base-class override hook: the codegen subclass overrides this and uses `this` to build a schema-aware writer
3236
3198
  runShardRankPage(_args) {
3237
- return Promise.reject(
3238
- Object.assign(new Error("rankPage is not implemented in base ShardDO"), { code: "NOT_IMPLEMENTED", name: "LunoraError", status: 500 })
3239
- );
3199
+ return Promise.reject(new LunoraError("NOT_IMPLEMENTED", "rankPage is not implemented in base ShardDO", { status: 500 }));
3240
3200
  }
3241
3201
  /**
3242
3202
  * Page this shard's change-data-capture log past `sinceSeq`. Read-only and
@@ -3565,9 +3525,7 @@ class ShardDO {
3565
3525
  */
3566
3526
  // eslint-disable-next-line class-methods-use-this -- base-class override hook: the codegen subclass overrides this and uses `this` to build a schema-aware writer
3567
3527
  runShardApplyCdc(_args) {
3568
- return Promise.reject(
3569
- Object.assign(new Error("applyCdc is not implemented in base ShardDO"), { code: "NOT_IMPLEMENTED", name: "LunoraError", status: 500 })
3570
- );
3528
+ return Promise.reject(new LunoraError("NOT_IMPLEMENTED", "applyCdc is not implemented in base ShardDO", { status: 500 }));
3571
3529
  }
3572
3530
  /**
3573
3531
  * Register a subscription on the given socket. Stored via
@@ -4477,15 +4435,11 @@ class ShardDO {
4477
4435
  resolveWorkflowBinding(exportName) {
4478
4436
  const metadata = this.workflowsMetadata().workflows.find((workflow) => workflow.exportName === exportName);
4479
4437
  if (!metadata) {
4480
- throw Object.assign(new Error(`workflow "${exportName}" is not declared`), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
4438
+ throw new LunoraError("BAD_REQUEST", `workflow "${exportName}" is not declared`);
4481
4439
  }
4482
4440
  const binding = this.env?.[metadata.binding];
4483
4441
  if (typeof binding !== "object" || binding === null || typeof binding.create !== "function" || typeof binding.get !== "function") {
4484
- throw Object.assign(new Error(`workflow binding "${metadata.binding}" is not available on this deployment`), {
4485
- code: "BAD_REQUEST",
4486
- name: "LunoraError",
4487
- status: 400
4488
- });
4442
+ throw new LunoraError("BAD_REQUEST", `workflow binding "${metadata.binding}" is not available on this deployment`);
4489
4443
  }
4490
4444
  return binding;
4491
4445
  }
@@ -4665,25 +4619,21 @@ class ShardDO {
4665
4619
  const parsed = parseReplayQueueMessageArgs(args);
4666
4620
  const row = readQueueMessageById(this.state.storage.sql, parsed.id);
4667
4621
  if (row === void 0) {
4668
- throw Object.assign(new Error(`replayQueueMessage: captured message "${parsed.id}" was not found`), {
4669
- code: "BAD_REQUEST",
4670
- name: "LunoraError",
4671
- status: 404
4672
- });
4622
+ throw new LunoraError("BAD_REQUEST", `replayQueueMessage: captured message "${parsed.id}" was not found`, { status: 404 });
4673
4623
  }
4674
4624
  if (isLossyBody(row.body)) {
4675
- throw Object.assign(
4676
- new Error(`replayQueueMessage: captured message "${parsed.id}" has a truncated or unserializable body and can't be replayed faithfully`),
4677
- { code: "BAD_REQUEST", name: "LunoraError", status: 422 }
4625
+ throw new LunoraError(
4626
+ "BAD_REQUEST",
4627
+ `replayQueueMessage: captured message "${parsed.id}" has a truncated or unserializable body and can't be replayed faithfully`,
4628
+ { status: 422 }
4678
4629
  );
4679
4630
  }
4680
4631
  const target = parsed.target ?? this.resolveReplayTarget(row.queue) ?? row.exportName;
4681
4632
  if (typeof target !== "string" || target === "") {
4682
- throw Object.assign(new Error(`replayQueueMessage: captured message "${parsed.id}" has no declared producer to replay onto (pass \`target\`)`), {
4683
- code: "BAD_REQUEST",
4684
- name: "LunoraError",
4685
- status: 400
4686
- });
4633
+ throw new LunoraError(
4634
+ "BAD_REQUEST",
4635
+ `replayQueueMessage: captured message "${parsed.id}" has no declared producer to replay onto (pass \`target\`)`
4636
+ );
4687
4637
  }
4688
4638
  const { binding } = this.resolveQueueBinding(target);
4689
4639
  await binding.send(row.body);
@@ -4701,15 +4651,11 @@ class ShardDO {
4701
4651
  resolveQueueBinding(exportName) {
4702
4652
  const metadata = this.queuesMetadata().queues.find((queue) => queue.exportName === exportName);
4703
4653
  if (!metadata) {
4704
- throw Object.assign(new Error(`queue "${exportName}" is not declared`), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
4654
+ throw new LunoraError("BAD_REQUEST", `queue "${exportName}" is not declared`);
4705
4655
  }
4706
4656
  const binding = this.env?.[metadata.binding];
4707
4657
  if (typeof binding !== "object" || binding === null || typeof binding.send !== "function") {
4708
- throw Object.assign(new Error(`queue binding "${metadata.binding}" is not available on this deployment`), {
4709
- code: "BAD_REQUEST",
4710
- name: "LunoraError",
4711
- status: 400
4712
- });
4658
+ throw new LunoraError("BAD_REQUEST", `queue binding "${metadata.binding}" is not available on this deployment`);
4713
4659
  }
4714
4660
  return { binding, metadata };
4715
4661
  }
@@ -1,5 +1,5 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
- import { applySelect } from './applySelect-BvZdFUBT.mjs';
2
+ import { applySelect } from './applySelect-WQY8m62C.mjs';
3
3
 
4
4
  const projectChildren = (documents, nested) => nested.select ? applySelect(documents, nested.select, nested.with) : documents;
5
5
  const fanOutScalarCounts = async (counter, tableName, whereField, values, policyWhere) => {
@@ -1,3 +1,5 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const TIEBREAK_FIELD = "id";
2
4
  const ID_FIELDS = /* @__PURE__ */ new Set(["_id", "id"]);
3
5
  const normalizeOrderKeys = (orderBy) => {
@@ -30,7 +32,7 @@ const encodeCursor = (record, keys) => {
30
32
  values.push(record["_id"]);
31
33
  return toBase64(JSON.stringify(values));
32
34
  };
33
- const invalidCursor = () => Object.assign(new TypeError("invalid cursor"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
35
+ const invalidCursor = () => new LunoraError("BAD_REQUEST", "invalid cursor");
34
36
  const decodeCursor = (cursor) => {
35
37
  let decoded;
36
38
  try {
@@ -1,3 +1,5 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const pitrUnavailable = () => Object.assign(
2
4
  new Error("native PITR is unavailable here (local dev or a non-SQLite Durable Object); use `lunora backup restore` for off-platform recovery"),
3
5
  {
@@ -12,11 +14,7 @@ const toTime = (time) => {
12
14
  }
13
15
  const parsed = Date.parse(time);
14
16
  if (Number.isNaN(parsed)) {
15
- throw Object.assign(new Error(`pitr: invalid time "${time}" — expected epoch-ms or an ISO timestamp`), {
16
- code: "BAD_REQUEST",
17
- name: "LunoraError",
18
- status: 400
19
- });
17
+ throw new LunoraError("BAD_REQUEST", `pitr: invalid time "${time}" — expected epoch-ms or an ISO timestamp`);
20
18
  }
21
19
  return parsed;
22
20
  };
@@ -37,11 +35,7 @@ const armRestore = async (storage, args) => {
37
35
  let target = args.bookmark;
38
36
  if (target === void 0) {
39
37
  if (args.time === void 0) {
40
- throw Object.assign(new Error("pitrRestore: provide a `bookmark` or a `time` to restore to"), {
41
- code: "BAD_REQUEST",
42
- name: "LunoraError",
43
- status: 400
44
- });
38
+ throw new LunoraError("BAD_REQUEST", "pitrRestore: provide a `bookmark` or a `time` to restore to");
45
39
  }
46
40
  if (!storage.getBookmarkForTime) {
47
41
  throw pitrUnavailable();
@@ -1,17 +1,14 @@
1
- import { RELATION_FUNCTION_PREFIX } from './ADMIN_FUNCTIONS-BbQdj8h1.mjs';
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { RELATION_FUNCTION_PREFIX } from './ADMIN_FUNCTIONS-Bxdijn66.mjs';
2
3
 
3
4
  const serveRelationFanout = async (schema, database, functionPath, args) => {
4
5
  const table = typeof args["table"] === "string" ? args["table"] : "";
5
6
  const definition = schema.tables[table];
6
7
  if (!definition) {
7
- throw Object.assign(new Error(`${RELATION_FUNCTION_PREFIX} unknown table "${table}"`), { code: "UNKNOWN_TABLE", name: "LunoraError", status: 404 });
8
+ throw new LunoraError("UNKNOWN_TABLE", `${RELATION_FUNCTION_PREFIX} unknown table "${table}"`, { status: 404 });
8
9
  }
9
10
  if (definition.shardMode?.kind === "global") {
10
- throw Object.assign(new Error(`${RELATION_FUNCTION_PREFIX} table "${table}" is global, not shard-local`), {
11
- code: "BAD_REQUEST",
12
- name: "LunoraError",
13
- status: 400
14
- });
11
+ throw new LunoraError("BAD_REQUEST", `${RELATION_FUNCTION_PREFIX} table "${table}" is global, not shard-local`);
15
12
  }
16
13
  const where = args["where"] ?? void 0;
17
14
  if (functionPath === `${RELATION_FUNCTION_PREFIX}count`) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/do",
3
- "version": "1.0.0-alpha.24",
3
+ "version": "1.0.0-alpha.26",
4
4
  "description": "Lunora Durable Objects: ShardDO (SQLite, OCC, hibernated WebSocket subscriptions) and SessionDO",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -46,8 +46,8 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/errors": "1.0.0-alpha.2",
50
- "@visulima/redact": "3.0.0-alpha.14",
49
+ "@lunora/errors": "1.0.0-alpha.3",
50
+ "@visulima/redact": "3.0.0",
51
51
  "drizzle-orm": "^0.45.2"
52
52
  },
53
53
  "engines": {