@lunora/do 1.0.0-alpha.23 → 1.0.0-alpha.25

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 CHANGED
@@ -103,3 +103,9 @@ Unless required by applicable law or agreed to in writing, software distributed
103
103
  under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
104
  CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
105
  specific language governing permissions and limitations under the License.
106
+
107
+ <!-- DEPENDENCIES -->
108
+ <!-- /DEPENDENCIES -->
109
+
110
+ <!-- TYPE_DEPENDENCIES -->
111
+ <!-- /TYPE_DEPENDENCIES -->
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
@@ -3806,43 +3839,6 @@ declare const MIN_AUTH_SECRET_LENGTH = 32;
3806
3839
  * a *missing* token is never itself a finding here (introspection is simply off).
3807
3840
  */
3808
3841
  declare const buildSecurityAudit: (rawEnv: unknown) => SecurityAuditResult;
3809
- /**
3810
- * Durable Object that owns auth session state.
3811
- *
3812
- * `@lunora/auth` used to write sessions directly into D1 alongside user
3813
- * records. That worked but coupled session lifecycle to a global database —
3814
- * every read had to cross the region, every write contended with user
3815
- * inserts. SessionDO owns sessions in a DO-local KV store: same-prefix
3816
- * tokens co-locate (via `idFromName(token.slice(0, 16))`) so the DO instance
3817
- * count stays bounded; reads and writes never round-trip to D1.
3818
- *
3819
- * Wire shape: HTTP only, never RPC. The auth package calls
3820
- *
3821
- * `await env.SESSION.get(env.SESSION.idFromName(prefix)).fetch(...)`
3822
- *
3823
- * with one of:
3824
- *
3825
- * POST /create body: { token, userId, ttlSeconds }
3826
- * GET /get header: `x-lunora-session-token: &lt;token>`
3827
- * DELETE /revoke header: `x-lunora-session-token: &lt;token>`
3828
- *
3829
- * Every request must additionally carry an `x-lunora-session-secret` header
3830
- * whose value matches `env.SESSION_DO_SECRET`. The DO is reachable from any
3831
- * worker bound to its namespace, so a shared secret is the only thing that
3832
- * prevents a compromised or misbehaving worker from reading arbitrary
3833
- * sessions — the binding alone is not an auth surface.
3834
- *
3835
- * The DO returns JSON bodies that `@lunora/auth` reshapes into its public
3836
- * `AuthSession` type. Keep the surface narrow — anything more elaborate
3837
- * should ride on top via a wrapper, not by widening this contract.
3838
- *
3839
- * # Subclassing
3840
- *
3841
- * Apps subclass `SessionDO` (or use the codegen subclass) and register the
3842
- * subclass in `wrangler.jsonc` as `SESSION`. The platform DO binding requires
3843
- * a concrete `DurableObject` class today; the structural state shape used by
3844
- * the unit tests is preserved so plain-object doubles still work.
3845
- */
3846
3842
  /** Default TTL for new sessions (7 days), matching `@lunora/auth`. */
3847
3843
  declare const SESSION_DO_TTL_DEFAULT: number;
3848
3844
  /** Hard ceiling on the requested TTL — 90 days. Longer sessions should ride on top via refresh. */
@@ -6453,42 +6449,6 @@ declare abstract class ShardDO {
6453
6449
  private deliverWhisperLocal;
6454
6450
  private readAttachment;
6455
6451
  }
6456
- /**
6457
- * Durable Object that owns the live set of shard keys per sharded table.
6458
- *
6459
- * The query coordinator (`@lunora/runtime`) fans out cross-shard reads to
6460
- * every live shard. With the static registry, the app supplies the shard
6461
- * key list at boot — which is fine for fixed-cardinality deployments
6462
- * (a known set of tenants) and unworkable for dynamic ones (one shard per
6463
- * user-created channel, organisation, project, …).
6464
- *
6465
- * `ShardRegistryDO` is the persistent source of truth. A worker:
6466
- *
6467
- * - calls `POST /register {table, shardKey}` when a sharded table first
6468
- * sees a write on a new key (typically from `ctx.db.&lt;table>.insert` via
6469
- * the worker's onWrite hook, fired through `ctx.waitUntil` so the
6470
- * user-facing write doesn't pay the registry round-trip);
6471
- * - calls `POST /unregister {table, shardKey}` when a shard is decommissioned;
6472
- * - calls `GET /list?table=X` to materialise the fan-out target list. The
6473
- * client (`createDynamicShardRegistry` in `@lunora/runtime`) caches the
6474
- * answer with a small TTL so a wide fan-out doesn't pay a registry
6475
- * round-trip on every call.
6476
- *
6477
- * Single-instance contract: deploy one DO instance per environment, by
6478
- * convention named {@link SHARD_REGISTRY_DO_NAME}. The DO is small (just a
6479
- * `Map&lt;table, Set&lt;shardKey>>`) and writes are infrequent (only on first-seen
6480
- * shardKey per table), so a single instance is sufficient up to tens of
6481
- * thousands of distinct shard keys.
6482
- *
6483
- * Wire shape: HTTP only, never RPC.
6484
- *
6485
- * POST /register body: { table, shardKey }
6486
- * POST /unregister body: { table, shardKey }
6487
- * GET /list?table=...
6488
- * GET /snapshot (debug: returns the full table → [keys] map)
6489
- *
6490
- * Responses are JSON; the client shapes them. Keep the surface narrow.
6491
- */
6492
6452
  /** Conventional DO instance name, passed to `idFromName` to address the single registry instance. */
6493
6453
  declare const SHARD_REGISTRY_DO_NAME: string;
6494
6454
  /**
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
@@ -3806,43 +3839,6 @@ declare const MIN_AUTH_SECRET_LENGTH = 32;
3806
3839
  * a *missing* token is never itself a finding here (introspection is simply off).
3807
3840
  */
3808
3841
  declare const buildSecurityAudit: (rawEnv: unknown) => SecurityAuditResult;
3809
- /**
3810
- * Durable Object that owns auth session state.
3811
- *
3812
- * `@lunora/auth` used to write sessions directly into D1 alongside user
3813
- * records. That worked but coupled session lifecycle to a global database —
3814
- * every read had to cross the region, every write contended with user
3815
- * inserts. SessionDO owns sessions in a DO-local KV store: same-prefix
3816
- * tokens co-locate (via `idFromName(token.slice(0, 16))`) so the DO instance
3817
- * count stays bounded; reads and writes never round-trip to D1.
3818
- *
3819
- * Wire shape: HTTP only, never RPC. The auth package calls
3820
- *
3821
- * `await env.SESSION.get(env.SESSION.idFromName(prefix)).fetch(...)`
3822
- *
3823
- * with one of:
3824
- *
3825
- * POST /create body: { token, userId, ttlSeconds }
3826
- * GET /get header: `x-lunora-session-token: &lt;token>`
3827
- * DELETE /revoke header: `x-lunora-session-token: &lt;token>`
3828
- *
3829
- * Every request must additionally carry an `x-lunora-session-secret` header
3830
- * whose value matches `env.SESSION_DO_SECRET`. The DO is reachable from any
3831
- * worker bound to its namespace, so a shared secret is the only thing that
3832
- * prevents a compromised or misbehaving worker from reading arbitrary
3833
- * sessions — the binding alone is not an auth surface.
3834
- *
3835
- * The DO returns JSON bodies that `@lunora/auth` reshapes into its public
3836
- * `AuthSession` type. Keep the surface narrow — anything more elaborate
3837
- * should ride on top via a wrapper, not by widening this contract.
3838
- *
3839
- * # Subclassing
3840
- *
3841
- * Apps subclass `SessionDO` (or use the codegen subclass) and register the
3842
- * subclass in `wrangler.jsonc` as `SESSION`. The platform DO binding requires
3843
- * a concrete `DurableObject` class today; the structural state shape used by
3844
- * the unit tests is preserved so plain-object doubles still work.
3845
- */
3846
3842
  /** Default TTL for new sessions (7 days), matching `@lunora/auth`. */
3847
3843
  declare const SESSION_DO_TTL_DEFAULT: number;
3848
3844
  /** Hard ceiling on the requested TTL — 90 days. Longer sessions should ride on top via refresh. */
@@ -6453,42 +6449,6 @@ declare abstract class ShardDO {
6453
6449
  private deliverWhisperLocal;
6454
6450
  private readAttachment;
6455
6451
  }
6456
- /**
6457
- * Durable Object that owns the live set of shard keys per sharded table.
6458
- *
6459
- * The query coordinator (`@lunora/runtime`) fans out cross-shard reads to
6460
- * every live shard. With the static registry, the app supplies the shard
6461
- * key list at boot — which is fine for fixed-cardinality deployments
6462
- * (a known set of tenants) and unworkable for dynamic ones (one shard per
6463
- * user-created channel, organisation, project, …).
6464
- *
6465
- * `ShardRegistryDO` is the persistent source of truth. A worker:
6466
- *
6467
- * - calls `POST /register {table, shardKey}` when a sharded table first
6468
- * sees a write on a new key (typically from `ctx.db.&lt;table>.insert` via
6469
- * the worker's onWrite hook, fired through `ctx.waitUntil` so the
6470
- * user-facing write doesn't pay the registry round-trip);
6471
- * - calls `POST /unregister {table, shardKey}` when a shard is decommissioned;
6472
- * - calls `GET /list?table=X` to materialise the fan-out target list. The
6473
- * client (`createDynamicShardRegistry` in `@lunora/runtime`) caches the
6474
- * answer with a small TTL so a wide fan-out doesn't pay a registry
6475
- * round-trip on every call.
6476
- *
6477
- * Single-instance contract: deploy one DO instance per environment, by
6478
- * convention named {@link SHARD_REGISTRY_DO_NAME}. The DO is small (just a
6479
- * `Map&lt;table, Set&lt;shardKey>>`) and writes are infrequent (only on first-seen
6480
- * shardKey per table), so a single instance is sufficient up to tens of
6481
- * thousands of distinct shard keys.
6482
- *
6483
- * Wire shape: HTTP only, never RPC.
6484
- *
6485
- * POST /register body: { table, shardKey }
6486
- * POST /unregister body: { table, shardKey }
6487
- * GET /list?table=...
6488
- * GET /snapshot (debug: returns the full table → [keys] map)
6489
- *
6490
- * Responses are JSON; the client shapes them. Keep the surface narrow.
6491
- */
6492
6452
  /** Conventional DO instance name, passed to `idFromName` to address the single registry instance. */
6493
6453
  declare const SHARD_REGISTRY_DO_NAME: string;
6494
6454
  /**
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
- export { exportShardRows, exportShardTable, importShardRows, parseExportShardArgs, parseImportShardArgs, selectExportTables, validateImportRow } from './packem_shared/exportShardRows-DZEhUeyI.mjs';
1
+ export { exportShardRows, exportShardTable, importShardRows, parseExportShardArgs, parseImportShardArgs, selectExportTables, validateImportRow } from './packem_shared/exportShardRows-Dy3oFZ26.mjs';
2
2
  export { AGGREGATE_SQL_FUNCTION, aggregateSqlFunction, matchesStaticWhere, normalizeCountArgument, throwingScheduler } from './packem_shared/AGGREGATE_SQL_FUNCTION-CQsu2Xga.mjs';
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-Dmf-HNdQ.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';
18
+ export { armRestore, readBookmark } from './packem_shared/armRestore-4Px61hHS.mjs';
19
19
  export { applySelect, buildSeekWhere, decodeCursor, encodeCursor, normalizeOrderKeys, softDeleteScope } from './packem_shared/applySelect-BvZdFUBT.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';
22
+ export { serveRelationFanout } from './packem_shared/serveRelationFanout-C56hKu1O.mjs';
23
23
  export { DEFAULT_MAX_RELATION_KEYS, assertFlatPredicate, assertShapeShardable, containsRelationPredicate, isRelationPredicate, resolveRelationPredicates } from './packem_shared/DEFAULT_MAX_RELATION_KEYS-CjM9Y1_G.mjs';
24
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';
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
- export { SESSION_DO_TTL_DEFAULT, SessionDO } from './packem_shared/SESSION_DO_TTL_DEFAULT-ilPZsVwu.mjs';
29
- export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-BM4TOOvf.mjs';
30
- export { SHARD_REGISTRY_DO_NAME, ShardRegistryDO } from './packem_shared/SHARD_REGISTRY_DO_NAME-BsAbi5Mn.mjs';
31
- export { MAX_SQL_ROWS, assertReadonly, runReadonlySql } from './packem_shared/MAX_SQL_ROWS-dDcFE1YZ.mjs';
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-CpURtawE.mjs';
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-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,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 === "") {
@@ -16,7 +16,7 @@ import { decodeCursor, normalizeOrderKeys, buildSeekWhere, applySelect, encodeCu
16
16
  import NotFoundError from './NotFoundError-C70b9hLw.mjs';
17
17
  import { assertFlatPredicate, resolveRelationPredicates } from './DEFAULT_MAX_RELATION_KEYS-CjM9Y1_G.mjs';
18
18
  import { runRowValidators, resolveWith, applyOnDelete, fanOutScalarCounts } from './applyOnDelete-CAwZfp-5.mjs';
19
- import { guardWriter } from './RLS_UNWRAP_SYMBOL-DnjkqVgY.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);