@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 +6 -0
- package/dist/index.d.mts +40 -80
- package/dist/index.d.ts +40 -80
- package/dist/index.mjs +10 -10
- package/dist/packem_shared/{ADMIN_FUNCTIONS-BbQdj8h1.mjs → ADMIN_FUNCTIONS-Bxdijn66.mjs} +7 -5
- package/dist/packem_shared/{MAX_SQL_ROWS-dDcFE1YZ.mjs → MAX_SQL_ROWS-D57CJaT9.mjs} +3 -1
- package/dist/packem_shared/{NotUniqueError-D1U5SBFR.mjs → NotUniqueError-Dmf-HNdQ.mjs} +60 -7
- package/dist/packem_shared/{RLS_UNWRAP_SYMBOL-DnjkqVgY.mjs → RLS_UNWRAP_SYMBOL-G1eNK4DQ.mjs} +8 -0
- package/dist/packem_shared/{ROOT_DO_SIZE_WARN_BYTES-BM4TOOvf.mjs → ROOT_DO_SIZE_WARN_BYTES-CpURtawE.mjs} +79 -133
- package/dist/packem_shared/{SESSION_DO_TTL_DEFAULT-ilPZsVwu.mjs → SESSION_DO_TTL_DEFAULT-D71QAL5h.mjs} +15 -17
- package/dist/packem_shared/{SHARD_REGISTRY_DO_NAME-BsAbi5Mn.mjs → SHARD_REGISTRY_DO_NAME-D99roc-r.mjs} +12 -14
- package/dist/packem_shared/{armRestore-BJk53Ro8.mjs → armRestore-4Px61hHS.mjs} +4 -10
- package/dist/packem_shared/{exportShardRows-DZEhUeyI.mjs → exportShardRows-Dy3oFZ26.mjs} +4 -3
- package/dist/packem_shared/json-response-BdbtpOhm.mjs +3 -0
- package/dist/packem_shared/{serveRelationFanout-BhZF9AuB.mjs → serveRelationFanout-C56hKu1O.mjs} +4 -7
- package/package.json +2 -2
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.
|
|
1718
|
-
*
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1721
|
-
*
|
|
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
|
-
|
|
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<
|
|
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: <token>`
|
|
3827
|
-
* DELETE /revoke header: `x-lunora-session-token: <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.<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<table, Set<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.
|
|
1718
|
-
*
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1721
|
-
*
|
|
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
|
-
|
|
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<
|
|
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: <token>`
|
|
3827
|
-
* DELETE /revoke header: `x-lunora-session-token: <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.<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<table, Set<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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
29
|
-
export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-
|
|
30
|
-
export { SHARD_REGISTRY_DO_NAME, ShardRegistryDO } from './packem_shared/SHARD_REGISTRY_DO_NAME-
|
|
31
|
-
export { MAX_SQL_ROWS, assertReadonly, runReadonlySql } from './packem_shared/MAX_SQL_ROWS-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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-
|
|
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
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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");
|
package/dist/packem_shared/{RLS_UNWRAP_SYMBOL-DnjkqVgY.mjs → RLS_UNWRAP_SYMBOL-G1eNK4DQ.mjs}
RENAMED
|
@@ -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);
|