@lunora/server 1.0.0-alpha.22 → 1.0.0-alpha.220
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 +213 -0
- package/README.md +18 -12
- package/dist/data-model.d.mts +392 -170
- package/dist/data-model.d.ts +392 -170
- package/dist/data-model.mjs +0 -1
- package/dist/drizzle.mjs +1 -1
- package/dist/in-process.d.mts +101 -0
- package/dist/in-process.d.ts +101 -0
- package/dist/in-process.mjs +1 -0
- package/dist/index.d.mts +3806 -1565
- package/dist/index.d.ts +3806 -1565
- package/dist/index.mjs +1 -29
- package/dist/otel.d.mts +554 -0
- package/dist/otel.d.ts +554 -0
- package/dist/otel.mjs +1 -0
- package/dist/packem_shared/ACTION_CACHE_DEFAULT_TTL_MS-mmP-6BcT.mjs +1 -0
- package/dist/packem_shared/DEFAULT_LIMIT-pT_tV8R0.mjs +1 -0
- package/dist/packem_shared/DOCUMENT_HISTORY_REDACTED_FIELDS-B5Zp-7SI.mjs +1 -0
- package/dist/packem_shared/LunoraEnvError-bkdMu_-N.mjs +4 -0
- package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-DFpy5J8G.mjs +1 -0
- package/dist/packem_shared/allowAll-DkRAgItV.mjs +1 -0
- package/dist/packem_shared/apply-output-C5wZ5EAL.mjs +1 -0
- package/dist/packem_shared/asBucketStorage-DoGYOjq3.mjs +1 -0
- package/dist/packem_shared/assertShapesDeclareReadPolicies-Bg4Ter7Q.mjs +1 -0
- package/dist/packem_shared/beginDeferredDeletes-DhwFBbvs.mjs +1 -0
- package/dist/packem_shared/beginDeferredSchedules-C7QLYr6C.mjs +1 -0
- package/dist/packem_shared/bindOrm-9fbLRbOb.mjs +1 -0
- package/dist/packem_shared/buildMaskRegistry-DpWMtalG.mjs +1 -0
- package/dist/packem_shared/compose-middleware-BK9FlblN.mjs +1 -0
- package/dist/packem_shared/composePluginMiddleware-ZtDIkbvJ.mjs +1 -0
- package/dist/packem_shared/context-identity-B2XYGA0B.mjs +1 -0
- package/dist/packem_shared/createPolicyDsl-BZa6SqLJ.mjs +1 -0
- package/dist/packem_shared/createSecrets-D6rLB42U.mjs +1 -0
- package/dist/packem_shared/createServices-IJvoqi0e.mjs +1 -0
- package/dist/packem_shared/defineAggregateIndex-DF8NiKHr.mjs +1 -0
- package/dist/packem_shared/defineIdentity-DwkNKwYa.mjs +1 -0
- package/dist/packem_shared/defineIdentityGuard-Bt8N65Zs.mjs +1 -0
- package/dist/packem_shared/defineLiveStream-BKomK5uG.mjs +1 -0
- package/dist/packem_shared/defineMigration-CXOS0Bvq.mjs +1 -0
- package/dist/packem_shared/defineModule-OecXWM3U.mjs +1 -0
- package/dist/packem_shared/defineMutator-B4ipDtDR.mjs +1 -0
- package/dist/packem_shared/defineRoom-CoGi-xwm.mjs +1 -0
- package/dist/packem_shared/defineShape-CwKm5KLp.mjs +1 -0
- package/dist/packem_shared/defineStorageRule-Dv4nJE0H.mjs +1 -0
- package/dist/packem_shared/defineWebSocket-BHoy6_ox.mjs +1 -0
- package/dist/packem_shared/fnv1a-D3ueNTWB.mjs +1 -0
- package/dist/packem_shared/functions-B2-H4PQT.mjs +1 -0
- package/dist/packem_shared/httpAction-D2S7bQzl.mjs +6 -0
- package/dist/packem_shared/initLunora-B-7Xs3Qq.mjs +1 -0
- package/dist/packem_shared/isPerDispatchMiddleware-DDQPatSm.mjs +1 -0
- package/dist/packem_shared/mask-D-jRCAhy.mjs +1 -0
- package/dist/packem_shared/middleware-3ySZVrpt.mjs +1 -0
- package/dist/packem_shared/onConnect-BLRoOpv2.mjs +1 -0
- package/dist/packem_shared/onQueryChange-iuLzmyPs.mjs +1 -0
- package/dist/packem_shared/onWhisper-CfDbFLQh.mjs +1 -0
- package/dist/packem_shared/platformAdmin-kczXQme8.mjs +1 -0
- package/dist/packem_shared/plugin-Bm9wIpiI.mjs +1 -0
- package/dist/packem_shared/policy-tag-V0lqFzgS.mjs +1 -0
- package/dist/packem_shared/protectPublic-Bi5_95aP.mjs +1 -0
- package/dist/packem_shared/rls-Dwj6TlY_.mjs +1 -0
- package/dist/packem_shared/run-middleware-BefYq7Xd.mjs +1 -0
- package/dist/packem_shared/serveStorageObject-aqBFrLyo.mjs +1 -0
- package/dist/packem_shared/storageRules-CBLioxF7.mjs +1 -0
- package/dist/packem_shared/types.d-B7FLdYUD.d.mts +148 -0
- package/dist/packem_shared/types.d-BCtjbIzl.d.ts +148 -0
- package/dist/rls/testing.d.mts +42 -34
- package/dist/rls/testing.d.ts +42 -34
- package/dist/rls/testing.mjs +1 -49
- package/dist/types.d.mts +2247 -639
- package/dist/types.d.ts +2247 -639
- package/dist/types.mjs +0 -31
- package/package.json +24 -6
- package/dist/packem_shared/LunoraEnvError-BGmd1Qs0.mjs +0 -187
- package/dist/packem_shared/LunoraError-WbxmrpxR.mjs +0 -9
- package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-pc2cfrnm.mjs +0 -114
- package/dist/packem_shared/asBucketStorage-Cnxd9y2q.mjs +0 -11
- package/dist/packem_shared/bindOrm-Bfgl7f-Q.mjs +0 -146
- package/dist/packem_shared/buildRlsReadRegistry-D54vUQe4.mjs +0 -107
- package/dist/packem_shared/composePluginMiddleware-CcTj1_v2.mjs +0 -103
- package/dist/packem_shared/createPolicyDsl-By3QB4he.mjs +0 -32
- package/dist/packem_shared/createSecrets-DwaR2rNG.mjs +0 -58
- package/dist/packem_shared/defineAggregateIndex-znq4ygKQ.mjs +0 -295
- package/dist/packem_shared/defineIdentity-DiX4zM9x.mjs +0 -35
- package/dist/packem_shared/defineMigration-Hx01yIht.mjs +0 -10
- package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +0 -11
- package/dist/packem_shared/defineShape-C5scNOrf.mjs +0 -18
- package/dist/packem_shared/defineStorageRule-B5nL4Z1P.mjs +0 -23
- package/dist/packem_shared/functions-Di9FUNkf.mjs +0 -5
- package/dist/packem_shared/httpAction-DCXoYPIk.mjs +0 -355
- package/dist/packem_shared/initLunora-CmMVk4i7.mjs +0 -107
- package/dist/packem_shared/mask-BT1YgRbF.mjs +0 -291
- package/dist/packem_shared/onConnect-CIPXKPyw.mjs +0 -13
- package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +0 -13
- package/dist/packem_shared/protectPublic-BlcGpiRc.mjs +0 -15
- package/dist/packem_shared/rls-B_ZWgslr.mjs +0 -595
- package/dist/packem_shared/run-middleware-I6EiQfxL.mjs +0 -20
- package/dist/packem_shared/storageRules-6QxzDOcx.mjs +0 -88
- package/dist/packem_shared/types.d-BB3pjV0m.d.mts +0 -141
- package/dist/packem_shared/types.d-Cxl6ndhm.d.ts +0 -141
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,406 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Id, Validator, Infer, ValidatorMap, InferValidatorMap } from '@lunora/values';
|
|
2
|
+
/**
|
|
3
|
+
* The set of languages a `.searchIndex()` can declare, in one place.
|
|
4
|
+
*
|
|
5
|
+
* Three layers need it and none of them can import the others: `@lunora/server`
|
|
6
|
+
* types the option and validates it at schema-build time, and `@lunora/do` maps
|
|
7
|
+
* it onto a stopword list at analysis time — with no dependency edge between
|
|
8
|
+
* them (nor should there be: the schema builder must stay usable without the DO
|
|
9
|
+
* runtime). So it lived in three hand-maintained copies, where adding a
|
|
10
|
+
* language silently produced a schema the builder accepts and the analyzer
|
|
11
|
+
* treats as `"none"` — an index that quietly stops dropping stopwords.
|
|
12
|
+
*
|
|
13
|
+
* The list is the single source: the type is derived from it, so the two cannot
|
|
14
|
+
* drift. `@lunora/do` still owns the stopword *lists*; it keys them by these
|
|
15
|
+
* names, and its own tests assert the two agree.
|
|
16
|
+
*
|
|
17
|
+
* Inlined by the bundler into each `dist` rather than published, so this stays
|
|
18
|
+
* a shared *constant*, not a dependency edge. See the `shared/` section of
|
|
19
|
+
* AGENTS.md.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Every accepted value, sorted, so an error message can list what *is*
|
|
23
|
+
* accepted without re-sorting. `"none"` is explicit rather than absent: it
|
|
24
|
+
* means "fold, but drop nothing", which is also what an index with no declared
|
|
25
|
+
* language gets.
|
|
26
|
+
*/
|
|
27
|
+
declare const SEARCH_LANGUAGES: readonly ["de", "en", "es", "fr", "it", "nl", "none", "pt"];
|
|
28
|
+
/** A declared analysis language, derived from the list above. */
|
|
29
|
+
type SearchLanguage = (typeof SEARCH_LANGUAGES)[number];
|
|
30
|
+
/**
|
|
31
|
+
* How a search index is stored. `"portable"` promises identical behaviour on
|
|
32
|
+
* every backend; `"native"` opts into the engine's own full-text index where it
|
|
33
|
+
* has one, trading that promise for its speed.
|
|
34
|
+
*
|
|
35
|
+
* Lives beside the languages for the same reason they do: it is declared in
|
|
36
|
+
* `@lunora/server`, acted on in the storage layer, and there is no dependency
|
|
37
|
+
* edge between them — so a third hand-maintained copy is how a typo turns into
|
|
38
|
+
* a silently different *physical layout*.
|
|
39
|
+
*/
|
|
40
|
+
declare const SEARCH_STRATEGIES: readonly ["native", "portable"];
|
|
41
|
+
/** A declared storage strategy, derived from the list above. */
|
|
42
|
+
type SearchStrategy = (typeof SEARCH_STRATEGIES)[number];
|
|
43
|
+
/**
|
|
44
|
+
* Declared HTTP caching for an exposed endpoint (`.expose({ cache })`). Lives
|
|
45
|
+
* here, alongside the path/method contract, for the same reason: the runtime
|
|
46
|
+
* WRITES these headers and the OpenAPI emitter DESCRIBES them, and the two must
|
|
47
|
+
* not be able to disagree. Deriving both from {@link cacheControlValue} /
|
|
48
|
+
* {@link cacheVaryValue} makes "the published spec matches what the runtime
|
|
49
|
+
* actually sends" structural rather than a hand-kept invariant.
|
|
50
|
+
*/
|
|
51
|
+
interface RestCachePolicy {
|
|
52
|
+
/**
|
|
53
|
+
* Extra request headers this app authenticates on, beyond
|
|
54
|
+
* {@link CREDENTIAL_HEADERS}. Declare these whenever `resolveIdentity` reads
|
|
55
|
+
* something else (`x-api-key`, a tenant header, …): they join both the
|
|
56
|
+
* credential check and the emitted `Vary`. Without them, a caller
|
|
57
|
+
* authenticating that way is treated as anonymous.
|
|
58
|
+
*/
|
|
59
|
+
readonly credentialHeaders?: ReadonlyArray<string>;
|
|
60
|
+
readonly maxAge: number;
|
|
61
|
+
readonly scope: "private" | "public";
|
|
62
|
+
readonly staleWhileRevalidate?: number;
|
|
63
|
+
readonly tag?: string;
|
|
64
|
+
readonly vary?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The `ctx.storage` surface, per function tier: {@link ReadOnlyStorage} for a
|
|
68
|
+
* query, {@link MutationStorage} for a mutation, {@link Storage} for an action.
|
|
69
|
+
*
|
|
70
|
+
* A structural mirror of `@lunora/storage`'s `Storage` — `@lunora/server` takes
|
|
71
|
+
* no dependency on that package. Re-exported through `../types`, so the public
|
|
72
|
+
* names are unchanged.
|
|
73
|
+
*/
|
|
74
|
+
/**
|
|
75
|
+
* Per-file metadata returned by {@link ReadOnlyStorage.getMetadata}. A clean
|
|
76
|
+
* public mirror of `@lunora/storage`'s `ObjectMetadata` — re-declared here so
|
|
77
|
+
* the ctx surface carries no dependency on the storage package's types. Matches
|
|
78
|
+
* the columns Convex surfaces for `ctx.storage.getMetadata` / `_storage`.
|
|
79
|
+
*/
|
|
80
|
+
interface StorageMetadata {
|
|
81
|
+
/** The object's `Content-Type`, when recorded. */
|
|
82
|
+
contentType?: string;
|
|
83
|
+
/** Custom metadata set at upload time, if any. */
|
|
84
|
+
customMetadata?: Record<string, string>;
|
|
85
|
+
/** The object's key. */
|
|
86
|
+
key: string;
|
|
87
|
+
/** Hex-encoded SHA-256 of the body, when R2 carries a checksum. */
|
|
88
|
+
sha256?: string;
|
|
89
|
+
/** Body length in bytes. */
|
|
90
|
+
size: number;
|
|
91
|
+
/** When the object was last written (epoch ms), when reported. */
|
|
92
|
+
uploaded?: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The body-free object shape returned by {@link ReadOnlyStorage.head} — a clean
|
|
96
|
+
* public mirror of `@lunora/storage`'s head projection, re-declared here for the
|
|
97
|
+
* same reason as {@link StorageMetadata}: the ctx surface carries no dependency
|
|
98
|
+
* on the storage package's types.
|
|
99
|
+
*
|
|
100
|
+
* Richer than {@link StorageMetadata} on purpose. `getMetadata` is the tidy
|
|
101
|
+
* Convex-shaped summary; `head` is what an HTTP layer needs, so it keeps the
|
|
102
|
+
* validator (`etag`) and the base64 digest RFC 9530 `Repr-Digest` requires, and
|
|
103
|
+
* leaves `uploaded` as the `Date` the binding reports rather than epoch ms.
|
|
104
|
+
*/
|
|
105
|
+
interface StorageObjectHead {
|
|
106
|
+
/** Custom metadata set at upload time, if any. */
|
|
107
|
+
customMetadata?: Record<string, string>;
|
|
108
|
+
/**
|
|
109
|
+
* R2's unquoted etag (the MD5 hex for a single-part upload). Required: R2
|
|
110
|
+
* reports one on every object, and an HTTP layer built on `head()` (the
|
|
111
|
+
* `serveStorageObject` helper) needs it to emit a validator.
|
|
112
|
+
*/
|
|
113
|
+
etag: string;
|
|
114
|
+
/** The already-quoted form of {@link StorageObjectHead.etag}, when the binding reports one. */
|
|
115
|
+
httpEtag?: string;
|
|
116
|
+
/** Recorded HTTP metadata, notably the `Content-Type`. */
|
|
117
|
+
httpMetadata?: {
|
|
118
|
+
contentType?: string;
|
|
119
|
+
};
|
|
120
|
+
/** The object's key. */
|
|
121
|
+
key: string;
|
|
122
|
+
/** Hex-encoded SHA-256 of the body, when R2 carries a checksum. */
|
|
123
|
+
sha256?: string;
|
|
124
|
+
/** Base64-encoded SHA-256 of the same checksum — the encoding RFC 9530 digest headers require. */
|
|
125
|
+
sha256Base64?: string;
|
|
126
|
+
/** The FULL object size in bytes: R2 reports the object's size, not a returned window's, which is what makes a head enough to resolve a `Range` against. */
|
|
127
|
+
size: number;
|
|
128
|
+
/** When the object was last written. */
|
|
129
|
+
uploaded?: Date;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Byte window forwarded to {@link ReadOnlyStorage.download} so R2 resolves the
|
|
133
|
+
* slice server-side and streams only those bytes back. Mirrors R2's own `range`
|
|
134
|
+
* option (`@lunora/platform`'s `R2RangeLike`), restated structurally so
|
|
135
|
+
* `@lunora/server` takes no dependency on the bindings package.
|
|
136
|
+
*/
|
|
137
|
+
type StorageRange = {
|
|
138
|
+
length?: number;
|
|
139
|
+
offset: number;
|
|
140
|
+
} | {
|
|
141
|
+
length: number;
|
|
142
|
+
offset?: number;
|
|
143
|
+
} | {
|
|
144
|
+
suffix: number;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* A downloaded object: the same metadata {@link StorageObjectHead} carries, plus
|
|
148
|
+
* the body stream. This is what `download()` resolves to — R2's object, NOT a
|
|
149
|
+
* bare stream.
|
|
150
|
+
*/
|
|
151
|
+
interface StorageObjectBody extends StorageObjectHead {
|
|
152
|
+
/** The object body stream. `null` for a zero-byte object. */
|
|
153
|
+
body: ReadableStream | null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Read-only projection of `Storage` exposed on `QueryCtx` / `MutationCtx`.
|
|
157
|
+
*
|
|
158
|
+
* Queries are pure reads, and mutations run inside a transactional scope —
|
|
159
|
+
* neither is allowed to perform side-effectful R2 writes (`upload`) or
|
|
160
|
+
* deletes (`delete`). They can, however, **read** existing objects and
|
|
161
|
+
* resolve signed URLs (the URL signing itself is HMAC-only — no R2 round
|
|
162
|
+
* trip), so the read-only surface keeps `download` and `getSignedUrl`. The
|
|
163
|
+
* full {@link Storage} surface stays on `ActionCtx`.
|
|
164
|
+
*/
|
|
165
|
+
interface ReadOnlyStorage<Buckets extends string = string> {
|
|
166
|
+
/**
|
|
167
|
+
* Select a named bucket (declared via `v.storage("name")`). The returned
|
|
168
|
+
* accessor's operations target that bucket — `ctx.storage.bucket("avatars")
|
|
169
|
+
* .download(key)`. The bare `ctx.storage` targets the default bucket.
|
|
170
|
+
*/
|
|
171
|
+
bucket: (name: Buckets) => ReadOnlyStorage<Buckets>;
|
|
172
|
+
/** The bucket this accessor's operations target (the default for the bare `ctx.storage`). */
|
|
173
|
+
readonly bucketName: string;
|
|
174
|
+
/**
|
|
175
|
+
* Fetch an existing object. Returns the R2 object — metadata plus a `body`
|
|
176
|
+
* stream — or `null` when absent.
|
|
177
|
+
*
|
|
178
|
+
* NOT a bare stream: `new Response(await ctx.storage.download(key))` would
|
|
179
|
+
* stringify the object and serve the literal text `[object Object]`. Reach
|
|
180
|
+
* for the body explicitly:
|
|
181
|
+
*
|
|
182
|
+
* ```ts
|
|
183
|
+
* const object = await ctx.storage.download(key);
|
|
184
|
+
*
|
|
185
|
+
* return object ? new Response(object.body) : new Response("Not found", { status: 404 });
|
|
186
|
+
* ```
|
|
187
|
+
*
|
|
188
|
+
* (`serveStorageObject` from `@lunora/server` does this, plus range/ETag
|
|
189
|
+
* handling, for the common "serve a stored file over HTTP" case.)
|
|
190
|
+
*
|
|
191
|
+
* Pass `range` to have R2 resolve the byte window server-side, so the
|
|
192
|
+
* unwanted bytes never reach the worker.
|
|
193
|
+
*/
|
|
194
|
+
download: (key: string, options?: {
|
|
195
|
+
range?: StorageRange;
|
|
196
|
+
}) => Promise<StorageObjectBody | null>;
|
|
197
|
+
/**
|
|
198
|
+
* Read a file's metadata (size, content-type, sha256, upload time, custom
|
|
199
|
+
* metadata) without fetching its body. Returns `null` when the object is
|
|
200
|
+
* absent. Mirrors Convex's `ctx.storage.getMetadata`.
|
|
201
|
+
*/
|
|
202
|
+
getMetadata: (key: string) => Promise<StorageMetadata | null>;
|
|
203
|
+
/** Resolve a short-lived signed URL for an existing object. */
|
|
204
|
+
getSignedUrl: (key: string, options?: {
|
|
205
|
+
expiresInSeconds?: number;
|
|
206
|
+
}) => Promise<string>;
|
|
207
|
+
/** Public URL pointing at the configured base for `key`. */
|
|
208
|
+
getUrl: (key: string) => string;
|
|
209
|
+
/**
|
|
210
|
+
* Read an object's metadata with NO body transfer, as the raw object shape —
|
|
211
|
+
* `etag` and the base64 digest included, which is what an HTTP layer needs to
|
|
212
|
+
* answer a `Range` request. Returns `null` when the object is absent.
|
|
213
|
+
*
|
|
214
|
+
* {@link ReadOnlyStorage.getMetadata} is the tidier summary over the same
|
|
215
|
+
* read; reach for this one when building a response.
|
|
216
|
+
*/
|
|
217
|
+
head: (key: string) => Promise<StorageObjectHead | null>;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* `ctx.storage` inside a **mutation**: the read surface, plus the one write a
|
|
221
|
+
* transactional context can safely express. See `@lunora/server`'s
|
|
222
|
+
* `deferred-deletes.ts` for why `delete` itself stays action-only.
|
|
223
|
+
*/
|
|
224
|
+
interface MutationStorage<Buckets extends string = string> extends ReadOnlyStorage<Buckets> {
|
|
225
|
+
/** Select a named bucket; deletes queued on it are flushed against that bucket. */
|
|
226
|
+
bucket: (name: Buckets) => MutationStorage<Buckets>;
|
|
227
|
+
/**
|
|
228
|
+
* Queue `key` for deletion once this mutation commits.
|
|
229
|
+
*
|
|
230
|
+
* Returns `void`, not a promise: nothing has been attempted yet, so the object
|
|
231
|
+
* is still there on the next line. A rolled-back mutation never flushes, so
|
|
232
|
+
* the row deletion and the object cleanup cannot disagree. A failed delete
|
|
233
|
+
* leaks the object rather than failing a mutation that already succeeded, and
|
|
234
|
+
* is logged with its key.
|
|
235
|
+
*/
|
|
236
|
+
deleteAfterCommit: (key: string) => void;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The body shapes `store` / `upload` accept — what R2's `put` stores bytes from.
|
|
240
|
+
* Mirrors `@lunora/storage`'s `Storage.upload` parameter.
|
|
241
|
+
*/
|
|
242
|
+
type StorageUploadBody = ArrayBuffer | ArrayBufferView | Blob | ReadableStream | string;
|
|
243
|
+
/**
|
|
244
|
+
* Options for `store` / `upload` — a structural mirror of `@lunora/storage`'s
|
|
245
|
+
* `UploadOptions`, so the `maxSize` / `allowedContentTypes` guards are reachable
|
|
246
|
+
* from `ctx.storage` without a type dependency on the storage package.
|
|
247
|
+
*/
|
|
248
|
+
interface StorageUploadOptions {
|
|
249
|
+
/** Content-type allowlist; the supplied `contentType` must match. */
|
|
250
|
+
allowedContentTypes?: ReadonlyArray<string>;
|
|
251
|
+
contentType?: string;
|
|
252
|
+
customMetadata?: Record<string, string>;
|
|
253
|
+
/**
|
|
254
|
+
* Maximum body size in bytes. A `ReadableStream` is buffered under the cap,
|
|
255
|
+
* so on the stream path `maxSize` is itself capped at 16 MiB — above that use
|
|
256
|
+
* {@link Storage.createMultipartUpload}.
|
|
257
|
+
*/
|
|
258
|
+
maxSize?: number;
|
|
259
|
+
/** SHA-256 of the body (hex or a 32-byte buffer); R2 records and verifies it. */
|
|
260
|
+
sha256?: ArrayBuffer | string;
|
|
261
|
+
}
|
|
262
|
+
/** What `store` / `upload` resolve to: the stored key plus its etag (unquoted and quoted). */
|
|
263
|
+
interface StorageUploadResult {
|
|
264
|
+
etag: string;
|
|
265
|
+
httpEtag: string;
|
|
266
|
+
key: string;
|
|
267
|
+
}
|
|
268
|
+
/** One uploaded multipart part — returned by `uploadPart`, required by `complete`. Mirrors R2's `R2UploadedPart`. */
|
|
269
|
+
interface StorageUploadedPart {
|
|
270
|
+
etag: string;
|
|
271
|
+
partNumber: number;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* An in-progress R2 multipart upload, as {@link Storage.createMultipartUpload} /
|
|
275
|
+
* {@link Storage.resumeMultipartUpload} return it. A structural mirror of
|
|
276
|
+
* R2's own multipart upload handle, as `@lunora/platform` declares it.
|
|
277
|
+
*
|
|
278
|
+
* Every part except the last must be the same size, and at least 5 MiB. The
|
|
279
|
+
* object does not guarantee the upload still exists — a parallel `complete` /
|
|
280
|
+
* `abort` invalidates it — so handle errors on each call.
|
|
281
|
+
*/
|
|
282
|
+
interface StorageMultipartUpload {
|
|
283
|
+
/** Abort the upload, discarding every uploaded part. */
|
|
284
|
+
abort: () => Promise<void>;
|
|
285
|
+
/** Finish the upload from the collected parts; resolves to the stored object's metadata. */
|
|
286
|
+
complete: (uploadedParts: StorageUploadedPart[]) => Promise<StorageObjectHead>;
|
|
287
|
+
/** The object key being assembled. */
|
|
288
|
+
readonly key: string;
|
|
289
|
+
/** The R2 upload id — persist it to resume the upload in a later request. */
|
|
290
|
+
readonly uploadId: string;
|
|
291
|
+
/** Upload one part (1-indexed); returns the `{ partNumber, etag }` to pass to `complete`. */
|
|
292
|
+
uploadPart: (partNumber: number, value: StorageUploadBody) => Promise<StorageUploadedPart>;
|
|
293
|
+
}
|
|
294
|
+
/** Options for {@link Storage.createMultipartUpload}: the metadata recorded on the assembled object. */
|
|
295
|
+
interface StorageMultipartUploadOptions {
|
|
296
|
+
contentType?: string;
|
|
297
|
+
customMetadata?: Record<string, string>;
|
|
298
|
+
}
|
|
299
|
+
/** Options for {@link Storage.list}. Mirrors `@lunora/storage`'s `ListOptions`. */
|
|
300
|
+
interface StorageListOptions {
|
|
301
|
+
/** The `cursor` of the previous page. */
|
|
302
|
+
cursor?: string;
|
|
303
|
+
/** R2 list delimiter — keys sharing a segment roll up into `delimitedPrefixes`. */
|
|
304
|
+
delimiter?: string;
|
|
305
|
+
/** Page-size ceiling: defaults to 100, capped at 1000. A positive integer; anything else throws. */
|
|
306
|
+
limit?: number;
|
|
307
|
+
}
|
|
308
|
+
/** One page of {@link Storage.list}. */
|
|
309
|
+
interface StorageListResult {
|
|
310
|
+
/** Pass back as `options.cursor` for the next page, while `truncated` is `true`. */
|
|
311
|
+
cursor?: string;
|
|
312
|
+
/** The rolled-up "folders" when `options.delimiter` is set — these are NOT in `objects`. */
|
|
313
|
+
delimitedPrefixes?: string[];
|
|
314
|
+
/** The objects on this page, body-free and JSON-serializable. */
|
|
315
|
+
objects: StorageObjectHead[];
|
|
316
|
+
/** `true` while more pages remain. Paginate on this, never on `objects.length`. */
|
|
317
|
+
truncated?: boolean;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* `ctx.storage` inside an **action**: the full `@lunora/storage` `Storage`
|
|
321
|
+
* surface, plus `bucket(name)`.
|
|
322
|
+
*
|
|
323
|
+
* Declared structurally rather than imported — `@lunora/server` takes no
|
|
324
|
+
* runtime or published-type dependency on `@lunora/storage` — so it tracks that
|
|
325
|
+
* surface by hand. A type test (`__tests__/storage-surface.test-d.ts`) fails
|
|
326
|
+
* `tsc` when the two drift —
|
|
327
|
+
* the member sets differ (a runtime member left untyped here, or a member typed
|
|
328
|
+
* here the runtime lacks — `bucket` aside, which the ctx wiring adds); the
|
|
329
|
+
* runtime object no longer satisfies a signature here (a return type widened,
|
|
330
|
+
* or a parameter narrowed, upstream); or a parameter list here differs from the
|
|
331
|
+
* runtime's (an option or body shape the runtime takes but this interface
|
|
332
|
+
* refuses). `getSignedUrl` is the one deliberate parameter exception: its PUT
|
|
333
|
+
* options would mint an upload URL from a query.
|
|
334
|
+
*
|
|
335
|
+
* Return types here may be NARROWER than the runtime's on purpose — `head`
|
|
336
|
+
* resolves to the {@link StorageObjectHead} mirror rather than R2's object.
|
|
337
|
+
*
|
|
338
|
+
* Every member here is action-only. Under a procedure guarded by
|
|
339
|
+
* `storageRules(...)`, the write paths (`store` / `upload` /
|
|
340
|
+
* `createMultipartUpload` / `resumeMultipartUpload`) are gated as `write`, and
|
|
341
|
+
* `getPresignedUrl` / `list`, which no rule can gate, reject with `FORBIDDEN`.
|
|
342
|
+
*/
|
|
343
|
+
interface Storage<Buckets extends string = string> extends ReadOnlyStorage<Buckets> {
|
|
344
|
+
/** Select a named bucket; the returned accessor exposes the full read/write surface. */
|
|
345
|
+
bucket: (name: Buckets) => Storage<Buckets>;
|
|
346
|
+
/**
|
|
347
|
+
* Begin a native R2 **multipart upload** — for objects above `store()`'s
|
|
348
|
+
* 16 MiB stream cap, streamed through the Worker without buffering. Upload
|
|
349
|
+
* the parts (each at least 5 MiB and uniform in size, except the last), then
|
|
350
|
+
* `complete` with the returned parts, or `abort`. Persist `uploadId` to
|
|
351
|
+
* continue in a later request via {@link Storage.resumeMultipartUpload}.
|
|
352
|
+
*/
|
|
353
|
+
createMultipartUpload: (key: string, options?: StorageMultipartUploadOptions) => Promise<StorageMultipartUpload>;
|
|
354
|
+
delete: (key: string) => Promise<void>;
|
|
355
|
+
/**
|
|
356
|
+
* Mint a short-lived signed `PUT` URL a client can upload directly to,
|
|
357
|
+
* optionally pinning the `Content-Type` the uploader must send. Mirrors
|
|
358
|
+
* Convex's `storage.generateUploadUrl`.
|
|
359
|
+
*/
|
|
360
|
+
generateUploadUrl: (key: string, options?: {
|
|
361
|
+
contentType?: string;
|
|
362
|
+
expiresInSeconds?: number;
|
|
363
|
+
}) => Promise<string>;
|
|
364
|
+
/**
|
|
365
|
+
* Mint a native S3 SigV4 URL that hits R2 **directly**, so the bytes never
|
|
366
|
+
* pass through the Worker — unlike {@link Storage.generateUploadUrl}, whose
|
|
367
|
+
* signed URL points back at this app so its storage rules still apply.
|
|
368
|
+
*
|
|
369
|
+
* Requires `s3` credentials on the `.storage({ s3 })` declaration; without
|
|
370
|
+
* them the call throws. That is the trade-off: no Worker in the path also
|
|
371
|
+
* means no rule enforcement in the path.
|
|
372
|
+
*/
|
|
373
|
+
getPresignedUrl: (key: string, options?: {
|
|
374
|
+
expiresInSeconds?: number;
|
|
375
|
+
method?: "GET" | "PUT";
|
|
376
|
+
}) => Promise<string>;
|
|
377
|
+
/**
|
|
378
|
+
* List objects under `prefix` — e.g. to sweep abandoned staging uploads. With
|
|
379
|
+
* `options.delimiter` set, keys sharing a segment roll up into
|
|
380
|
+
* `delimitedPrefixes` and are NOT in `objects`, so an empty `objects` is not
|
|
381
|
+
* an empty directory. A page may hold fewer objects than `options.limit`:
|
|
382
|
+
* paginate on `truncated` / `cursor`.
|
|
383
|
+
*
|
|
384
|
+
* Not gated by storage access rules (and dropped under `storageRules(...)`);
|
|
385
|
+
* a rule-scoped enumeration goes through `ctx.db.system.query("_storage")`.
|
|
386
|
+
*/
|
|
387
|
+
list: (prefix?: string, options?: StorageListOptions) => Promise<StorageListResult>;
|
|
388
|
+
/**
|
|
389
|
+
* Resume an in-progress multipart upload by its `uploadId` (e.g. across
|
|
390
|
+
* requests). Synchronous: R2 does not validate the id, so a stale one
|
|
391
|
+
* surfaces as an error on the first `uploadPart` / `complete`.
|
|
392
|
+
*/
|
|
393
|
+
resumeMultipartUpload: (key: string, uploadId: string) => StorageMultipartUpload;
|
|
394
|
+
/**
|
|
395
|
+
* Upload `body` to `key` from the server, returning the stored object's key
|
|
396
|
+
* and etag. Mirrors Convex's `storage.store`; the same function as
|
|
397
|
+
* {@link Storage.upload}, so the `maxSize` / `allowedContentTypes` guards
|
|
398
|
+
* apply through the alias.
|
|
399
|
+
*/
|
|
400
|
+
store: (key: string, body: StorageUploadBody, options?: StorageUploadOptions) => Promise<StorageUploadResult>;
|
|
401
|
+
/** Upload `body` to `key` — {@link Storage.store} under `@lunora/storage`'s own name, with one shared signature. */
|
|
402
|
+
upload: Storage<Buckets>["store"];
|
|
403
|
+
}
|
|
2
404
|
/** Map of validators describing a function's args record. Alias of `@lunora/values`' shared {@link ValidatorMap}. */
|
|
3
405
|
type ArgsValidator = ValidatorMap;
|
|
4
406
|
/** Infer the args object type from an {@link ArgsValidator}. Alias of `@lunora/values`' shared {@link InferValidatorMap}. */
|
|
@@ -6,12 +408,12 @@ type InferArgs<A extends ArgsValidator> = InferValidatorMap<A>;
|
|
|
6
408
|
/** Storage backend for a `.global()` table: D1 (default) or a Postgres/MySQL database via Cloudflare Hyperdrive (PlanetScale, Neon, …). */
|
|
7
409
|
type GlobalBackend = "d1" | "hyperdrive";
|
|
8
410
|
/**
|
|
9
|
-
* Cloudflare Durable Object data-residency jurisdiction declared via
|
|
10
|
-
* `defineSchema(...).jurisdiction("…")`. Restricts where every DO the app
|
|
11
|
-
* reaches runs and persists data (GDPR, FedRAMP, US data residency). Widening
|
|
12
|
-
* union — Cloudflare adds values over time.
|
|
13
|
-
* @see https://developers.cloudflare.com/durable-objects/reference/data-location/
|
|
14
|
-
*/
|
|
411
|
+
* Cloudflare Durable Object data-residency jurisdiction declared via
|
|
412
|
+
* `defineSchema(...).jurisdiction("…")`. Restricts where every DO the app
|
|
413
|
+
* reaches runs and persists data (GDPR, FedRAMP, US data residency). Widening
|
|
414
|
+
* union — Cloudflare adds values over time.
|
|
415
|
+
* @see https://developers.cloudflare.com/durable-objects/reference/data-location/
|
|
416
|
+
*/
|
|
15
417
|
type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
|
|
16
418
|
/** How a table is routed at runtime. */
|
|
17
419
|
type ShardMode = {
|
|
@@ -28,44 +430,83 @@ type ExternalSourceRefresh = "manual" | {
|
|
|
28
430
|
everyMs: number;
|
|
29
431
|
};
|
|
30
432
|
/**
|
|
31
|
-
* Delete-detection mode for external-source ingest
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
433
|
+
* Delete-detection mode for external-source ingest (plan 077 / 136).
|
|
434
|
+
*
|
|
435
|
+
* `"full-pull"` (the default) reads the **whole** tenant membership each tick and
|
|
436
|
+
* diffs it, so it observes upstream deletes for free — but costs a full read per tick
|
|
437
|
+
* (the Phase-0 bench put the ceiling at ~10k rows).
|
|
438
|
+
*
|
|
439
|
+
* `"incremental"` pulls **only rows past a durable watermark** (`cursor`), cheap for
|
|
440
|
+
* large low-churn tables above the full-pull cap. Because an absent row then means
|
|
441
|
+
* "unchanged", not "deleted", incremental requires a delete-visibility path: either a
|
|
442
|
+
* `reconcileEveryMs` periodic full-pull sweep, or a `softDeleteColumn` whose
|
|
443
|
+
* tombstones the pull returns. `defineSchema` throws (and the
|
|
444
|
+
* `external_source_incremental_no_delete_path` advisor lint fails the build) when an
|
|
445
|
+
* incremental source declares neither.
|
|
446
|
+
*/
|
|
36
447
|
type ExternalSourceMode = "full-pull" | "incremental";
|
|
37
448
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
449
|
+
* Incremental-ingest cursor (plan 136): the monotonic watermark column plus the
|
|
450
|
+
* watermark-parameterized pull query. `column` names the field in the pulled rows
|
|
451
|
+
* whose max becomes the next watermark (e.g. `"updated_at"`). `query` is a second
|
|
452
|
+
* SQL that returns only rows changed since the watermark — the watermark binds as
|
|
453
|
+
* the parameter AFTER `tenantBy`'s params (e.g. Postgres
|
|
454
|
+
* `... WHERE tenant_id = $1 AND updated_at >= $2 ORDER BY updated_at`). Prefer `>=`
|
|
455
|
+
* with the idempotent upsert apply so rows sharing the boundary timestamp are never
|
|
456
|
+
* skipped (re-pulling them is a no-op).
|
|
457
|
+
*/
|
|
458
|
+
interface ExternalSourceCursor {
|
|
459
|
+
/** The monotonic watermark column in the pulled rows; its max advances the stored watermark. */
|
|
460
|
+
column: string;
|
|
461
|
+
/** The incremental pull SQL. `tenantBy`'s params bind first, then the watermark as the trailing param. */
|
|
462
|
+
query: string;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Config for `.source(...)` (plan 077): declares a table as **materialized from an
|
|
466
|
+
* external Postgres/MySQL behind Cloudflare Hyperdrive**, not written by user
|
|
467
|
+
* mutations. A system-driven poll loop reads the tenant slice and lands it in the
|
|
468
|
+
* DO's SQLite (via the validated CDC writer), after which `defineShape` carries it
|
|
469
|
+
* to clients unchanged. Orthogonal to `shardMode` — a sourced table almost always
|
|
470
|
+
* also `.shardBy()`s, in which case `tenantBy` is the mandatory tenant-isolation
|
|
471
|
+
* boundary (enforced by the `external_source_unscoped` advisor lint).
|
|
472
|
+
*/
|
|
46
473
|
interface ExternalSourceDefinition {
|
|
47
474
|
/** The wrangler Hyperdrive binding name the poll loop reads from. */
|
|
48
475
|
binding: string;
|
|
49
476
|
/** Project the materialized rows to these columns (passed to the membership diff). Omit ⇒ the full mapped document. */
|
|
50
477
|
columns?: ReadonlyArray<string>;
|
|
478
|
+
/** **Required for `mode: "incremental"`**: the watermark column + watermark-parameterized pull query (plan 136). Rejected on a `"full-pull"` source. */
|
|
479
|
+
cursor?: ExternalSourceCursor;
|
|
51
480
|
/** Column whose value becomes the Lunora `_id`. Defaults to `"id"`. */
|
|
52
481
|
idColumn?: string;
|
|
53
482
|
/** Transform an external row into the stored document body. Omit ⇒ every selected column except `idColumn` is copied. */
|
|
54
483
|
map?: (row: Record<string, unknown>) => Record<string, unknown>;
|
|
55
|
-
/** Delete-detection mode.
|
|
484
|
+
/** Delete-detection mode. `"full-pull"` (the default) diffs the whole membership; `"incremental"` pulls past a `cursor` watermark. */
|
|
56
485
|
mode?: ExternalSourceMode;
|
|
57
486
|
/** The full tenant-membership query, with driver-native placeholders (`$1` / `?`). `tenantBy` binds its params. */
|
|
58
487
|
query: string;
|
|
59
|
-
/**
|
|
488
|
+
/**
|
|
489
|
+
* **Incremental delete-visibility (plan 136)**: run a full-pull sweep at most
|
|
490
|
+
* this often (millis) to GC upstream deletes an incremental slice can't see.
|
|
491
|
+
* One of `reconcileEveryMs` / `softDeleteColumn` is required for incremental;
|
|
492
|
+
* rejected on a `"full-pull"` source.
|
|
493
|
+
*/
|
|
60
494
|
reconcileEveryMs?: number;
|
|
61
495
|
/** Poll cadence, or `"manual"`. Omit ⇒ the runtime's size-scaled default. */
|
|
62
496
|
refresh?: ExternalSourceRefresh;
|
|
63
497
|
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
498
|
+
* **Incremental delete-visibility (plan 136)**: the upstream soft-delete
|
|
499
|
+
* tombstone column (e.g. `"deleted_at"`). When set, the incremental pull must
|
|
500
|
+
* return tombstoned rows and the ingest turns each into a local delete — an
|
|
501
|
+
* alternative to `reconcileEveryMs`. Rejected on a `"full-pull"` source.
|
|
502
|
+
*/
|
|
503
|
+
softDeleteColumn?: string;
|
|
504
|
+
/**
|
|
505
|
+
* **Mandatory under `.shardBy()`**: map this DO's shard key → the query's bound
|
|
506
|
+
* params, so a tenant DO can only ever pull its own rows. An unscoped sourced +
|
|
507
|
+
* sharded table replicates the whole multitenant table into every shard — the
|
|
508
|
+
* `external_source_unscoped` advisor lint fails the build when this is absent.
|
|
509
|
+
*/
|
|
69
510
|
tenantBy?: (shardKey: string) => ReadonlyArray<unknown>;
|
|
70
511
|
}
|
|
71
512
|
interface IndexDefinition {
|
|
@@ -74,25 +515,97 @@ interface IndexDefinition {
|
|
|
74
515
|
unique?: boolean;
|
|
75
516
|
}
|
|
76
517
|
interface SearchIndexDefinition {
|
|
518
|
+
/** Indexed text column; a dot-separated path (`"properties.name"`) reads a nested field. */
|
|
77
519
|
field: string;
|
|
520
|
+
/** Columns `.eq()` may narrow by inside the search builder. At most 16. */
|
|
78
521
|
filterFields?: ReadonlyArray<string>;
|
|
522
|
+
/**
|
|
523
|
+
* Text analysis for this index. Accent folding is always applied — it is
|
|
524
|
+
* what makes `café` and `cafe` the same token on every backend, which they
|
|
525
|
+
* otherwise are not. Naming a language additionally drops that language's
|
|
526
|
+
* stopwords from both documents and queries.
|
|
527
|
+
*
|
|
528
|
+
* Analysis is baked into the stored index, so changing this rebuilds it:
|
|
529
|
+
* the runtime records which profile a companion was built with and
|
|
530
|
+
* re-indexes when it no longer matches.
|
|
531
|
+
*/
|
|
532
|
+
language?: SearchLanguage;
|
|
533
|
+
name: string;
|
|
534
|
+
/**
|
|
535
|
+
* Skip the migration-time backfill. By default, creating the index's
|
|
536
|
+
* companion also indexes the rows already in the table, so a search index
|
|
537
|
+
* added to a populated table works immediately. On a very large table that
|
|
538
|
+
* scan is expensive to run inside a deploy: `staged: true` maintains the
|
|
539
|
+
* index on write only and leaves the initial population to an out-of-band
|
|
540
|
+
* run.
|
|
541
|
+
*
|
|
542
|
+
* That run is mandatory, not optional — until it happens, every row written
|
|
543
|
+
* BEFORE the deploy is unsearchable. Drive it against a deployment with
|
|
544
|
+
* `lunora run '__lunora_admin__:backfillSearch' --args '{"maxPages":20}'`,
|
|
545
|
+
* repeating until the response reports `done: true`; progress is durable, so
|
|
546
|
+
* each call resumes where the last stopped.
|
|
547
|
+
*/
|
|
548
|
+
staged?: boolean;
|
|
549
|
+
/**
|
|
550
|
+
* How the index is stored and matched.
|
|
551
|
+
*
|
|
552
|
+
* `"portable"` (default) keeps one implementation on every backend, with
|
|
553
|
+
* identical matching *and* ranking — the invariant the rest of the search
|
|
554
|
+
* docs rest on.
|
|
555
|
+
*
|
|
556
|
+
* `"native"` hands matching to the engine's own full-text index where it has
|
|
557
|
+
* one (Postgres `tsvector` + GIN today; ignored elsewhere). It scales far
|
|
558
|
+
* better on large corpora and common terms, because the portable path
|
|
559
|
+
* aggregates every matching token row before ranking. The trade: the engine
|
|
560
|
+
* ranks, so results are ordered by its formula rather than the shared
|
|
561
|
+
* scorer. Matching still agrees — the vector is built from the same analyzed
|
|
562
|
+
* tokens — but a `.global()` table using it will not return rows in the same
|
|
563
|
+
* order as the sharded twin.
|
|
564
|
+
*/
|
|
565
|
+
strategy?: SearchStrategy;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* A geospatial index declared via `.geoIndex(name, { field })`. The runtime
|
|
569
|
+
* maintains a geohash companion table over the `v.geoPoint()` column `field` so
|
|
570
|
+
* `withGeoIndex(name, q => q.near(point, radius) | q.within(bbox))` resolves a
|
|
571
|
+
* proximity / bounding-box read as a geohash-prefix range scan plus a Haversine
|
|
572
|
+
* refine/sort on the candidate rows.
|
|
573
|
+
*
|
|
574
|
+
* - `field` — the `v.geoPoint()` column whose lat/lng feed the geohash.
|
|
575
|
+
* - `precision` — geohash character length on the companion (default 9, ~4.8 m cells); higher precision narrows each cell.
|
|
576
|
+
*/
|
|
577
|
+
interface GeoIndexDefinition {
|
|
578
|
+
field: string;
|
|
79
579
|
name: string;
|
|
580
|
+
precision?: number;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Declarative table-level TTL declared via `.ttl(field, { after? })`. A DO
|
|
584
|
+
* alarm-driven sweep deletes (or, when the table also `.softDelete()`s,
|
|
585
|
+
* soft-deletes) rows whose expiry timestamp has passed.
|
|
586
|
+
*
|
|
587
|
+
* - `field` — an epoch-millisecond column. Without `after`, its value is the absolute expiry instant; with `after`, `field` is a base timestamp and the row expires `after` milliseconds later (`field + after`).
|
|
588
|
+
* - `after` — optional millisecond offset added to `field` to derive the expiry.
|
|
589
|
+
*/
|
|
590
|
+
interface TtlDefinition {
|
|
591
|
+
after?: number;
|
|
592
|
+
field: string;
|
|
80
593
|
}
|
|
81
594
|
/** Reducer applied by an aggregate index. */
|
|
82
595
|
type AggregateOp = "avg" | "count" | "max" | "min" | "sum";
|
|
83
596
|
/**
|
|
84
|
-
* Declared aggregate index — the schema-level seam that lets the runtime keep
|
|
85
|
-
* O(1) counters/sums in step with row writes (via the trigger runner) and
|
|
86
|
-
* route matching reads through them.
|
|
87
|
-
*
|
|
88
|
-
* - `on` — the table whose rows feed the aggregate.
|
|
89
|
-
* - `op` — the reducer. `count` is field-less; the others take `field`.
|
|
90
|
-
* - `field` — the column the reducer applies to (required for non-count ops).
|
|
91
|
-
* - `by` — group keys. When all `where` keys in a read participate in `by`, the
|
|
92
|
-
* reader can answer from the counter table without scanning rows.
|
|
93
|
-
* - `where` — optional static predicate baked into the counter (only the rows
|
|
94
|
-
* matching it ever land in the counter).
|
|
95
|
-
*/
|
|
597
|
+
* Declared aggregate index — the schema-level seam that lets the runtime keep
|
|
598
|
+
* O(1) counters/sums in step with row writes (via the trigger runner) and
|
|
599
|
+
* route matching reads through them.
|
|
600
|
+
*
|
|
601
|
+
* - `on` — the table whose rows feed the aggregate.
|
|
602
|
+
* - `op` — the reducer. `count` is field-less; the others take `field`.
|
|
603
|
+
* - `field` — the column the reducer applies to (required for non-count ops).
|
|
604
|
+
* - `by` — group keys. When all `where` keys in a read participate in `by`, the
|
|
605
|
+
* reader can answer from the counter table without scanning rows.
|
|
606
|
+
* - `where` — optional static predicate baked into the counter (only the rows
|
|
607
|
+
* matching it ever land in the counter).
|
|
608
|
+
*/
|
|
96
609
|
interface AggregateIndexDefinition {
|
|
97
610
|
by?: ReadonlyArray<string>;
|
|
98
611
|
field?: string;
|
|
@@ -102,32 +615,32 @@ interface AggregateIndexDefinition {
|
|
|
102
615
|
where?: Record<string, unknown>;
|
|
103
616
|
}
|
|
104
617
|
/**
|
|
105
|
-
* One ordering key on a `rankIndex.sortBy`: which column to sort by, and the
|
|
106
|
-
* direction. The runtime breaks ties on the row's `_id` ASC so the order is
|
|
107
|
-
* total and `rank()` always returns a deterministic 1-based position.
|
|
108
|
-
*/
|
|
618
|
+
* One ordering key on a `rankIndex.sortBy`: which column to sort by, and the
|
|
619
|
+
* direction. The runtime breaks ties on the row's `_id` ASC so the order is
|
|
620
|
+
* total and `rank()` always returns a deterministic 1-based position.
|
|
621
|
+
*/
|
|
109
622
|
interface RankSortKey {
|
|
110
623
|
direction: "asc" | "desc";
|
|
111
624
|
field: string;
|
|
112
625
|
}
|
|
113
626
|
/**
|
|
114
|
-
* Declared rank index — a sorted companion table per `(partition tuple, sortBy)`
|
|
115
|
-
* maintained by triggers, so:
|
|
116
|
-
*
|
|
117
|
-
* - `rank(row)` returns the row's 1-based position within its partition under
|
|
118
|
-
* the declared `sortBy` order, plus the partition's total row count, in
|
|
119
|
-
* O(log n) lookups against the SQLite btree on the companion table.
|
|
120
|
-
* - `rankPage({ where, take, from })` walks the same companion table to return
|
|
121
|
-
* rows in the declared order — a sorted-pagination accelerator.
|
|
122
|
-
*
|
|
123
|
-
* Fields mirror `AggregateIndexDefinition`:
|
|
124
|
-
*
|
|
125
|
-
* - `on` — the source table whose rows feed the rank.
|
|
126
|
-
* - `sortBy` — ordered keys driving the rank. Required.
|
|
127
|
-
* - `partitionBy` — columns that scope each rank context (e.g. `["channelId"]`
|
|
128
|
-
* to rank within a channel). Omitted ⇒ one global rank across the table.
|
|
129
|
-
* - `where` — static predicate baked into the index; only matching rows enter.
|
|
130
|
-
*/
|
|
627
|
+
* Declared rank index — a sorted companion table per `(partition tuple, sortBy)`
|
|
628
|
+
* maintained by triggers, so:
|
|
629
|
+
*
|
|
630
|
+
* - `rank(row)` returns the row's 1-based position within its partition under
|
|
631
|
+
* the declared `sortBy` order, plus the partition's total row count, in
|
|
632
|
+
* O(log n) lookups against the SQLite btree on the companion table.
|
|
633
|
+
* - `rankPage({ where, take, from })` walks the same companion table to return
|
|
634
|
+
* rows in the declared order — a sorted-pagination accelerator.
|
|
635
|
+
*
|
|
636
|
+
* Fields mirror `AggregateIndexDefinition`:
|
|
637
|
+
*
|
|
638
|
+
* - `on` — the source table whose rows feed the rank.
|
|
639
|
+
* - `sortBy` — ordered keys driving the rank. Required.
|
|
640
|
+
* - `partitionBy` — columns that scope each rank context (e.g. `["channelId"]`
|
|
641
|
+
* to rank within a channel). Omitted ⇒ one global rank across the table.
|
|
642
|
+
* - `where` — static predicate baked into the index; only matching rows enter.
|
|
643
|
+
*/
|
|
131
644
|
interface RankIndexDefinition {
|
|
132
645
|
name: string;
|
|
133
646
|
on: string;
|
|
@@ -138,198 +651,464 @@ interface RankIndexDefinition {
|
|
|
138
651
|
/** FK behavior when a referenced parent row is deleted (mirrors SQL `ON DELETE`). */
|
|
139
652
|
type OnDeleteAction = "cascade" | "restrict" | "set null";
|
|
140
653
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* - `
|
|
146
|
-
*
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
654
|
+
* The join table behind a many-to-many relation, declared by
|
|
655
|
+
* `r.many(target, { through: { table, from, to } })`.
|
|
656
|
+
*
|
|
657
|
+
* - `table` — the join table, one row per linked pair.
|
|
658
|
+
* - `from` — the join table's `v.id` column pointing at the table that declares the relation.
|
|
659
|
+
* - `to` — the join table's `v.id` column pointing at the relation's target table.
|
|
660
|
+
*/
|
|
661
|
+
interface RelationThrough {
|
|
662
|
+
from: string;
|
|
663
|
+
table: string;
|
|
664
|
+
to: string;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* A relation backed by one foreign-key column: `one` (many-to-one) or `many` (one-to-many).
|
|
668
|
+
*
|
|
669
|
+
* - `one`: the FK column `field` lives on **this** table and points at
|
|
670
|
+
* `table`.`references` (default `_id`). Loads a single doc.
|
|
671
|
+
* - `many`: the FK column `field` lives on the **target** table and points back
|
|
672
|
+
* at this table's `references` (default `_id`). Loads an array.
|
|
673
|
+
*
|
|
674
|
+
* `onDelete` is meaningful only on `one`: it is the action applied to the
|
|
675
|
+
* holder rows when the referenced parent row is deleted.
|
|
676
|
+
*/
|
|
677
|
+
interface KeyedRelationDefinition {
|
|
152
678
|
field: string;
|
|
153
679
|
kind: "many" | "one";
|
|
154
680
|
onDelete?: OnDeleteAction;
|
|
155
681
|
references: string;
|
|
156
682
|
table: string;
|
|
683
|
+
through?: never;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* A many-to-many relation: the link lives in the `through` join table, whose `from` column points at this
|
|
687
|
+
* table's `_id` and whose `to` column points at `table`'s `_id`. Loads an array of `table` rows.
|
|
688
|
+
*
|
|
689
|
+
* It has no `field`/`references`/`onDelete` of its own: the two foreign keys are the join table's, and a delete
|
|
690
|
+
* on either side cascades through the `onDelete` of the join table's own `r.one(...)` relations.
|
|
691
|
+
*/
|
|
692
|
+
interface ThroughRelationDefinition {
|
|
693
|
+
kind: "many";
|
|
694
|
+
table: string;
|
|
695
|
+
through: RelationThrough;
|
|
157
696
|
}
|
|
697
|
+
/** A declared relation between two tables, recorded by `.relations((r) => …)`: a single-key relation or a many-to-many one. */
|
|
698
|
+
type RelationDefinition = KeyedRelationDefinition | ThroughRelationDefinition;
|
|
158
699
|
/** Distance metric used by a Vectorize index. */
|
|
159
700
|
type VectorMetric = "cosine" | "dot-product" | "euclidean";
|
|
160
701
|
/**
|
|
161
|
-
* Bring-your-own-embedder: a user-supplied fn turning a source string into a
|
|
162
|
-
* numeric vector. The runtime calls it at upsert/query time so the framework
|
|
163
|
-
* never couples to a single embedding provider.
|
|
164
|
-
*/
|
|
702
|
+
* Bring-your-own-embedder: a user-supplied fn turning a source string into a
|
|
703
|
+
* numeric vector. The runtime calls it at upsert/query time so the framework
|
|
704
|
+
* never couples to a single embedding provider.
|
|
705
|
+
*/
|
|
165
706
|
type VectorEmbedder = (input: string) => Promise<ReadonlyArray<number>> | ReadonlyArray<number>;
|
|
166
707
|
/**
|
|
167
|
-
* Vector index declared inline on a table via `.vectorize(field, opts)`
|
|
168
|
-
* (DSL Shape A). The source is always a single column on the owning table.
|
|
169
|
-
*/
|
|
708
|
+
* Vector index declared inline on a table via `.vectorize(field, opts)`
|
|
709
|
+
* (DSL Shape A). The source is always a single column on the owning table.
|
|
710
|
+
*/
|
|
170
711
|
interface TableVectorIndex {
|
|
171
712
|
dimensions: number;
|
|
172
713
|
embed: VectorEmbedder;
|
|
173
714
|
field: string;
|
|
174
715
|
metadata?: ReadonlyArray<string>;
|
|
175
716
|
metric: VectorMetric;
|
|
717
|
+
/** Declared embedding model; see `VectorizeOptions.model`. */
|
|
718
|
+
model?: string;
|
|
176
719
|
name: string;
|
|
177
720
|
}
|
|
178
721
|
interface TableDefinition<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
179
722
|
/**
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
723
|
+
* Aggregate indexes declared via `.aggregateIndex(name, opts)`. The runtime
|
|
724
|
+
* maintains a counter row per `by` group via the trigger seam, so reads
|
|
725
|
+
* whose `where` keys all participate in the index's `by` set are answered
|
|
726
|
+
* without scanning the underlying table.
|
|
727
|
+
*/
|
|
185
728
|
aggregateIndexes: ReadonlyArray<AggregateIndexDefinition>;
|
|
186
729
|
/**
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
730
|
+
* Set by `.commitOrdered()` (named `commitOrderedMode`, not `commitOrdered`,
|
|
731
|
+
* so the data field doesn't collide with the fluent `.commitOrdered()`
|
|
732
|
+
* builder method — same convention as `shardBy()`/`shardMode`). When `true`,
|
|
733
|
+
* every write to a row stamps `_commitSeq`: a per-shard integer allocated
|
|
734
|
+
* once per mutation and strictly increasing in commit order. Absent/`false`
|
|
735
|
+
* ⇒ rows carry no `_commitSeq`, as before.
|
|
736
|
+
*/
|
|
737
|
+
commitOrderedMode?: boolean;
|
|
738
|
+
/**
|
|
739
|
+
* `.dropStalePatches()` — drop a `patch` whose fields moved since the caller's
|
|
740
|
+
* CDC baseline rather than clobbering the newer value. See the builder method
|
|
741
|
+
* for the rule, the whole-patch granularity, and why it fails open.
|
|
742
|
+
*/
|
|
743
|
+
dropStalePatchesMode?: boolean;
|
|
744
|
+
/**
|
|
745
|
+
* Set by `.source(...)` (named `externalSource`, not `source`, so the data
|
|
746
|
+
* field doesn't collide with the fluent `.source()` builder method — same
|
|
747
|
+
* convention as `shardBy()`/`shardMode`). When present, the table is
|
|
748
|
+
* materialized from an external Hyperdrive-backed database by a system poll
|
|
749
|
+
* loop rather than user mutations. Implies `isExternallyManaged`.
|
|
750
|
+
*/
|
|
193
751
|
externalSource?: ExternalSourceDefinition;
|
|
752
|
+
/**
|
|
753
|
+
* Geospatial indexes declared via `.geoIndex(name, { field })`. The runtime
|
|
754
|
+
* maintains a geohash companion over the named `v.geoPoint()` column so
|
|
755
|
+
* `withGeoIndex(name, q => q.near(point, radius) | q.within(bbox))` resolves
|
|
756
|
+
* a proximity/bounding-box read as a geohash-prefix range scan plus a
|
|
757
|
+
* Haversine refine/sort. Empty unless `.geoIndex()` was called.
|
|
758
|
+
*/
|
|
759
|
+
geoIndexes: ReadonlyArray<GeoIndexDefinition>;
|
|
194
760
|
indexes: ReadonlyArray<IndexDefinition>;
|
|
195
761
|
/**
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
762
|
+
* `true` when `.externallyManaged()` was called — the table's rows are
|
|
763
|
+
* written outside Lunora's discoverable insert path (an adapter, a
|
|
764
|
+
* migration, or framework middleware), e.g. `@lunora/auth`'s better-auth
|
|
765
|
+
* tables or `@lunora/ratelimit`'s store. Advisor insert-path lints
|
|
766
|
+
* (`table_without_insert`) skip such tables instead of flagging the absent
|
|
767
|
+
* `ctx.db.insert(...)`.
|
|
768
|
+
*/
|
|
203
769
|
isExternallyManaged?: boolean;
|
|
204
770
|
/**
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
771
|
+
* `true` when `.public()` was called — the table opts OUT of secure-by-default
|
|
772
|
+
* RLS. Under a schema marked `.rls("required")`, every table is protected (the
|
|
773
|
+
* DO/D1 write path denies raw, non-RLS `ctx.db` access) UNLESS it is `isPublic`.
|
|
774
|
+
* Has no effect when the schema does not require RLS.
|
|
775
|
+
*/
|
|
210
776
|
isPublic?: boolean;
|
|
211
777
|
/**
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
778
|
+
* Set by `.memory()` (named `memoryMode`, not `memory`, so the data field
|
|
779
|
+
* doesn't collide with the fluent `.memory()` builder method — same
|
|
780
|
+
* convention as `shardBy()`/`shardMode`). When `true`, the table's rows are
|
|
781
|
+
* cleared every time the Durable Object is reconstructed, and its writes
|
|
782
|
+
* never reach the CDC changelog. Absent/`false` ⇒ an ordinary durable table.
|
|
783
|
+
*/
|
|
784
|
+
memoryMode?: boolean;
|
|
785
|
+
/**
|
|
786
|
+
* Set by `.ownedBy(field)` — the column holding the owning user's id (named
|
|
787
|
+
* `ownerField`, a data field, rather than colliding with the fluent
|
|
788
|
+
* `.ownedBy()` builder method — same convention as `shardBy()`/`shardMode`).
|
|
789
|
+
*
|
|
790
|
+
* A shape over this table with `owner: true` derives its predicate from this
|
|
791
|
+
* field, so "only the owner may replicate these rows" is declared once on the
|
|
792
|
+
* table instead of being restated in every shape's `where`. Absent ⇒ the table
|
|
793
|
+
* has no single owning column and a shape must spell its predicate out.
|
|
794
|
+
*/
|
|
795
|
+
ownerField?: string;
|
|
796
|
+
/**
|
|
797
|
+
* Rank indexes declared via `.rankIndex(name, opts)`. The runtime maintains
|
|
798
|
+
* a sorted companion table per declared rank with a btree on
|
|
799
|
+
* `(partition, sortBy)` so `rank(row)` returns the row's 1-based position
|
|
800
|
+
* within its partition in O(log n), and `rankPage()` walks the index for
|
|
801
|
+
* sorted pagination.
|
|
802
|
+
*/
|
|
218
803
|
rankIndexes: ReadonlyArray<RankIndexDefinition>;
|
|
219
804
|
/**
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
805
|
+
* Declared relations keyed by accessor name; empty unless `.relations()`
|
|
806
|
+
* was called. Named `relationMap` (not `relations`) so the fluent
|
|
807
|
+
* `.relations((r) => …)` builder method doesn't collide with this field.
|
|
808
|
+
*/
|
|
224
809
|
relationMap: Record<string, RelationDefinition>;
|
|
225
810
|
searchIndexes: ReadonlyArray<SearchIndexDefinition>;
|
|
226
811
|
shape: Shape;
|
|
227
812
|
shardMode: ShardMode;
|
|
228
813
|
/**
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
814
|
+
* Set by `.softDelete()` (named `softDeleteMode`, not `softDelete`, so the
|
|
815
|
+
* data field doesn't collide with the fluent `.softDelete()` builder method —
|
|
816
|
+
* same convention as `shardBy()`/`shardMode`). When present, the table carries
|
|
817
|
+
* a nullable timestamp column (`field`, default `deletedAt`):
|
|
818
|
+
* `ctx.db.<table>.delete()` flips it instead of physically removing the row,
|
|
819
|
+
* and **list reads** (`findMany`/`findFirst`/`query()`/`count`/`aggregate`/
|
|
820
|
+
* relation loads) hide rows whose `field` is set unless
|
|
821
|
+
* `includeDeleted: true` is passed. By-id `get`/`patch`/`replace` and
|
|
822
|
+
* `restore` are unaffected. Absent ⇒ deletes are physical, as before.
|
|
823
|
+
*/
|
|
239
824
|
softDeleteMode?: {
|
|
240
825
|
field: string;
|
|
241
826
|
};
|
|
242
827
|
/**
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
828
|
+
* Declared lifecycle triggers keyed by accessor name; empty unless
|
|
829
|
+
* `.triggers()` was called. Named `triggerMap` (not `triggers`) so the
|
|
830
|
+
* fluent `.triggers((t) => …)` builder method doesn't collide with this
|
|
831
|
+
* field — same reasoning as {@link TableDefinition.relationMap}.
|
|
832
|
+
*/
|
|
248
833
|
triggerMap: Record<string, TriggerDefinition>;
|
|
834
|
+
/**
|
|
835
|
+
* Set by `.ttl(field, { after })` — the declarative auto-expiry policy. A DO
|
|
836
|
+
* alarm-driven sweep deletes rows whose expiry timestamp has passed (or
|
|
837
|
+
* soft-deletes them when the table also `.softDelete()`s). Named `ttlPolicy`
|
|
838
|
+
* (a data field) rather than colliding with the fluent `.ttl()` builder
|
|
839
|
+
* method — same convention as `shardBy()`/`shardMode`. Absent ⇒ rows never
|
|
840
|
+
* auto-expire.
|
|
841
|
+
*/
|
|
842
|
+
ttlPolicy?: TtlDefinition;
|
|
249
843
|
vectorIndexes: ReadonlyArray<TableVectorIndex>;
|
|
250
844
|
}
|
|
251
845
|
/**
|
|
252
|
-
* Standalone vector index declared via `defineVectorIndex(...)` (DSL Shape B).
|
|
253
|
-
* Unlike {@link TableVectorIndex}, the source is a `select` function so it can
|
|
254
|
-
* derive the embedded text from any computation (e.g. `title + body`).
|
|
255
|
-
*/
|
|
846
|
+
* Standalone vector index declared via `defineVectorIndex(...)` (DSL Shape B).
|
|
847
|
+
* Unlike {@link TableVectorIndex}, the source is a `select` function so it can
|
|
848
|
+
* derive the embedded text from any computation (e.g. `title + body`).
|
|
849
|
+
*/
|
|
256
850
|
interface VectorIndexDefinition {
|
|
257
851
|
readonly dimensions: number;
|
|
258
852
|
readonly embed: VectorEmbedder;
|
|
259
853
|
readonly kind: "vectorIndex";
|
|
260
854
|
readonly metadata?: (row: Record<string, unknown>) => Record<string, unknown>;
|
|
261
855
|
readonly metric: VectorMetric;
|
|
856
|
+
/** Declared embedding model; see `VectorIndexOptions.model`. */
|
|
857
|
+
readonly model?: string;
|
|
262
858
|
readonly select: (row: Record<string, unknown>) => string;
|
|
263
859
|
readonly table: string;
|
|
264
860
|
}
|
|
265
861
|
interface Schema<T extends Record<string, TableDefinition> = Record<string, TableDefinition>> {
|
|
266
862
|
/**
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
863
|
+
* Secure-by-default RLS mode declared via `.rls("required")`. When
|
|
864
|
+
* `"required"`, every table is protected: the DO/D1 write path denies raw
|
|
865
|
+
* (non-RLS-wrapped) `ctx.db` access at runtime, so a procedure that forgets
|
|
866
|
+
* `.use(rls(...))` fails closed instead of silently exposing the table. A
|
|
867
|
+
* table opts out with `.public()` (→ {@link TableDefinition.isPublic}).
|
|
868
|
+
* Absent ⇒ legacy opt-in behavior (RLS only where a policy is applied).
|
|
869
|
+
*/
|
|
274
870
|
readonly rlsMode?: "required";
|
|
275
871
|
readonly tables: T;
|
|
276
872
|
readonly vectorIndexes: Record<string, VectorIndexDefinition>;
|
|
277
873
|
}
|
|
278
874
|
type FunctionKind = "action" | "mutation" | "query" | "stream";
|
|
279
875
|
/**
|
|
280
|
-
* Call surface a function is exposed on. `public` functions are reachable from
|
|
281
|
-
* clients via the generated `api`; `internal` functions are reachable only
|
|
282
|
-
* server-to-server (`ctx.runQuery`/`runMutation`/`runAction`) and are rejected
|
|
283
|
-
* by the DO's external RPC path. Absence is treated as `public` for
|
|
284
|
-
* back-compat with functions registered before visibility existed.
|
|
285
|
-
*/
|
|
876
|
+
* Call surface a function is exposed on. `public` functions are reachable from
|
|
877
|
+
* clients via the generated `api`; `internal` functions are reachable only
|
|
878
|
+
* server-to-server (`ctx.runQuery`/`runMutation`/`runAction`) and are rejected
|
|
879
|
+
* by the DO's external RPC path. Absence is treated as `public` for
|
|
880
|
+
* back-compat with functions registered before visibility existed.
|
|
881
|
+
*/
|
|
286
882
|
type FunctionVisibility = "internal" | "public";
|
|
287
883
|
/**
|
|
288
|
-
* x402 payment tag attached by the `.x402({ price })` builder modifier. Marks a
|
|
289
|
-
* public procedure as paid: the origin worker answers an unpaid client RPC with
|
|
290
|
-
* HTTP 402, verifies + settles the payment, and only then dispatches to the
|
|
291
|
-
* shard. The runtime reads only `price` from here — the network, recipient, and
|
|
292
|
-
* facilitator live in the worker-level x402 charge config, so `@lunora/runtime`
|
|
293
|
-
* never has to import `@lunora/x402` (and its viem/solana deps).
|
|
294
|
-
*/
|
|
884
|
+
* x402 payment tag attached by the `.x402({ price })` builder modifier. Marks a
|
|
885
|
+
* public procedure as paid: the origin worker answers an unpaid client RPC with
|
|
886
|
+
* HTTP 402, verifies + settles the payment, and only then dispatches to the
|
|
887
|
+
* shard. The runtime reads only `price` from here — the network, recipient, and
|
|
888
|
+
* facilitator live in the worker-level x402 charge config, so `@lunora/runtime`
|
|
889
|
+
* never has to import `@lunora/x402` (and its viem/solana deps).
|
|
890
|
+
*/
|
|
295
891
|
interface X402ProcedureConfig {
|
|
296
892
|
/**
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
893
|
+
* USD-denominated price: a number of dollars (`0.01`) or a decimal string
|
|
894
|
+
* (`"0.01"`, or the `"$0.01"` shorthand). Resolved to the network
|
|
895
|
+
* stablecoin's base units (USDC has 6 decimals) at challenge time.
|
|
896
|
+
*/
|
|
301
897
|
readonly price: number | string;
|
|
302
898
|
}
|
|
899
|
+
/**
|
|
900
|
+
* HTTP caching for an exposed REST endpoint, declared as
|
|
901
|
+
* `.expose({ rest: true, cache: { scope: "public", maxAge: 60 } })`. The runtime
|
|
902
|
+
* turns this into `Cache-Control` / `Cache-Tag` / `Vary` response headers, and a
|
|
903
|
+
* `cache.tag` is purgeable through `ctx.cache.purge({ tags: [...] })`.
|
|
904
|
+
*
|
|
905
|
+
* This is NOT equivalent to `httpRoute(...).cacheControl()`, which writes whatever
|
|
906
|
+
* value the author passes with no credential downgrade. That is the unguarded
|
|
907
|
+
* escape hatch; this is the guarded surface.
|
|
908
|
+
*
|
|
909
|
+
* Caching a procedure whose result depends on the caller is how a REST cache
|
|
910
|
+
* turns into a data leak, so `scope` is enforced at runtime rather than trusted:
|
|
911
|
+
* a request that carries credentials (an `Authorization` header or any `Cookie`)
|
|
912
|
+
* is ALWAYS answered `private`, even under `scope: "public"` — see
|
|
913
|
+
* {@link https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control MDN}
|
|
914
|
+
* for what `private` forbids. So a per-user response can never be stored in a
|
|
915
|
+
* shared/edge cache, and the worst a mis-declared `scope` can cost is a missed
|
|
916
|
+
* cache hit. `scope: "public"` additionally emits `Vary: authorization, cookie`
|
|
917
|
+
* so an intermediary can't hand a stored anonymous variant to a signed-in caller.
|
|
918
|
+
*
|
|
919
|
+
* Only ever applied to a cacheable exchange: a `GET` (so `query` procedures — a
|
|
920
|
+
* `mutation` / `action` is `POST`-only) that produced a 2xx.
|
|
921
|
+
*/
|
|
922
|
+
type RestCacheConfig = RestCachePolicy;
|
|
923
|
+
/**
|
|
924
|
+
* Opt-in public-surface tag attached by the `.expose({ rest: true })` builder
|
|
925
|
+
* modifier (plan 167). Marks a procedure as deliberately published over the
|
|
926
|
+
* public REST surface: the runtime mints a `/_lunora/rest/<namespace>/<fn>` route
|
|
927
|
+
* that dispatches THROUGH the procedure (so `ctx.auth` / RLS / validators are
|
|
928
|
+
* enforced), and the generated OpenAPI describes it. Everything is default-closed
|
|
929
|
+
* — a procedure without this tag is unreachable over REST.
|
|
930
|
+
*/
|
|
931
|
+
interface ExposeConfig {
|
|
932
|
+
/** Opt this endpoint's responses into HTTP caching. Omit to leave them uncached. */
|
|
933
|
+
readonly cache?: RestCacheConfig;
|
|
934
|
+
/** Publish this procedure over the public REST surface. */
|
|
935
|
+
readonly rest?: boolean;
|
|
936
|
+
}
|
|
303
937
|
interface RegisteredFunction<A extends ArgsValidator, R, Kind extends FunctionKind> {
|
|
304
938
|
readonly args: A;
|
|
939
|
+
/**
|
|
940
|
+
* Set on a channel registration, which a socket reaches by its name rather
|
|
941
|
+
* than a lifecycle moment: a `defineRoom` (`kind: "room"`, with the room's name,
|
|
942
|
+
* WITHOUT the topic prefix). Codegen collects it into the generated
|
|
943
|
+
* `LUNORA_ROOMS` map (topic → function path) the DO resolves a room whisper
|
|
944
|
+
* topic through. Absent on every other registration.
|
|
945
|
+
*/
|
|
946
|
+
readonly channel?: {
|
|
947
|
+
readonly kind: "room";
|
|
948
|
+
readonly name: string;
|
|
949
|
+
};
|
|
950
|
+
/**
|
|
951
|
+
* Set by the `.expose({ rest: true })` builder modifier. Marks the procedure
|
|
952
|
+
* as published on the public REST surface (plan 167). Absent on procedures that
|
|
953
|
+
* are reachable only via typed RPC (the default).
|
|
954
|
+
*/
|
|
955
|
+
readonly expose?: ExposeConfig;
|
|
305
956
|
readonly handler: (context: unknown, args: InferArgs<A>) => Promise<R> | R;
|
|
306
957
|
readonly kind: Kind;
|
|
307
958
|
/**
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
959
|
+
* Set on connection-lifecycle hooks (`onConnect` / `onDisconnect`), and on the
|
|
960
|
+
* other four moments. Marks the function for the generated
|
|
961
|
+
* `LUNORA_LIFECYCLE_HOOKS` manifest so the DO dispatches it from the socket
|
|
962
|
+
* lifecycle rather than via a client RPC. Absent on ordinary registrations
|
|
963
|
+
* and on channel registrations (`channel`).
|
|
964
|
+
*/
|
|
313
965
|
readonly lifecycle?: LifecycleEventKind;
|
|
314
966
|
readonly visibility?: FunctionVisibility;
|
|
315
967
|
/**
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
968
|
+
* Set by the `.x402({ price })` builder modifier. Marks the procedure as paid
|
|
969
|
+
* so the origin worker gates it behind an x402 402-challenge before dispatch.
|
|
970
|
+
* Absent on unpaid functions.
|
|
971
|
+
*/
|
|
320
972
|
readonly x402?: X402ProcedureConfig;
|
|
321
973
|
}
|
|
322
974
|
type RegisteredQuery<A extends ArgsValidator, R> = RegisteredFunction<A, R, "query">;
|
|
323
975
|
type RegisteredMutation<A extends ArgsValidator, R> = RegisteredFunction<A, R, "mutation">;
|
|
324
976
|
type RegisteredAction<A extends ArgsValidator, R> = RegisteredFunction<A, R, "action">;
|
|
977
|
+
/**
|
|
978
|
+
* Structural mirror of `@lunora/client`'s `FunctionReference` — the handle the
|
|
979
|
+
* generated `api` / `internal` objects hand you, carrying `<file>:<function>`
|
|
980
|
+
* in `__lunoraRef`. Redeclared here so `@lunora/server` needs no dependency on
|
|
981
|
+
* the client package, exactly as {@link Scheduler} avoids one on
|
|
982
|
+
* `@lunora/scheduler`. `RegisteredFunction` has no `__lunoraRef`, so the two
|
|
983
|
+
* shapes never overlap.
|
|
984
|
+
*/
|
|
985
|
+
interface FunctionHandle<Kind extends "action" | "mutation" | "query" | "stream", Args, Return> {
|
|
986
|
+
/** Phantom marker carrying the type parameters; never present at runtime. */
|
|
987
|
+
readonly __lunoraPhantom?: {
|
|
988
|
+
args: Args;
|
|
989
|
+
kind: Kind;
|
|
990
|
+
returns: Return;
|
|
991
|
+
};
|
|
992
|
+
readonly __lunoraRef: string;
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* `ctx.runQuery` — overloaded, see the note above.
|
|
996
|
+
*
|
|
997
|
+
* A single generic signature over the reference would be nicer (TS will not
|
|
998
|
+
* contextually type a parameter against a multi-signature type, so a hand-built
|
|
999
|
+
* ctx object must annotate its `(reference, args)` explicitly — see
|
|
1000
|
+
* `@lunora/testing`'s harness). It does not work: a concrete
|
|
1001
|
+
* `RegisteredQuery<{…}, number>` is not assignable to a
|
|
1002
|
+
* `RegisteredFunction<ArgsValidator, …>` constraint, because `handler`'s args
|
|
1003
|
+
* are in a contravariant position. Two inference sites it is.
|
|
1004
|
+
*/
|
|
1005
|
+
/**
|
|
1006
|
+
* Options for `ctx.runQuery`.
|
|
1007
|
+
*
|
|
1008
|
+
* Lunora's answer to Convex's `useStaleSnapshot`, and deliberately NOT a port of
|
|
1009
|
+
* it. Convex needs that flag because a mutation there validates its whole read
|
|
1010
|
+
* set at commit, so merely *reading* a hot append-only table manufactures OCC
|
|
1011
|
+
* conflicts. Lunora has no such conflict class: its OCC is a compare-and-swap on
|
|
1012
|
+
* the `__doc__` of each row a mutation actually WRITES (see `runGuardedWrite`),
|
|
1013
|
+
* so a read costs a mutation nothing and there is nothing for a stale snapshot
|
|
1014
|
+
* to relieve.
|
|
1015
|
+
*
|
|
1016
|
+
* What a read does cost here is REACTIVITY. Every table a live query touches
|
|
1017
|
+
* enters its read footprint, and every write to any of those tables re-runs the
|
|
1018
|
+
* subscription. A query that reads one hot table — an append-only audit log, a
|
|
1019
|
+
* counter, a feed — therefore re-runs on every append even when the append
|
|
1020
|
+
* cannot change its result. That is Lunora's version of the pressure Convex is
|
|
1021
|
+
* relieving, and {@link RunQueryOptions.untracked} is the release valve.
|
|
1022
|
+
*/
|
|
1023
|
+
interface RunQueryOptions {
|
|
1024
|
+
/**
|
|
1025
|
+
* Run the query without recording its reads in the CALLER's read footprint.
|
|
1026
|
+
* The subscription therefore does not re-run when the tables that query
|
|
1027
|
+
* touched change.
|
|
1028
|
+
*
|
|
1029
|
+
* The result is exactly as fresh as a tracked call — same SQLite, same
|
|
1030
|
+
* instant, no snapshot involved. The only thing given up is the invalidation
|
|
1031
|
+
* edge, so use it when the read genuinely should not wake the subscriber
|
|
1032
|
+
* (a monotonic counter rendered once, a config row, an audit tail whose
|
|
1033
|
+
* growth is irrelevant to the result) — and never for data whose change
|
|
1034
|
+
* should reach the client, which is what makes the subscription stale
|
|
1035
|
+
* indefinitely rather than merely late.
|
|
1036
|
+
*
|
|
1037
|
+
* Scoped to the sub-query and nothing else: the untracked call runs on its
|
|
1038
|
+
* own context, so a read interleaved on the caller's `ctx.db` while it is in
|
|
1039
|
+
* flight still tracks normally. That is why this is an option on
|
|
1040
|
+
* `ctx.runQuery` rather than a `ctx.db.untracked(...)` scope — a
|
|
1041
|
+
* writer-wide flag would silently swallow a concurrent read's dependency
|
|
1042
|
+
* (compare `meterExempt`'s documented interleaving caveat, which is
|
|
1043
|
+
* tolerable for a resource meter and would not be here).
|
|
1044
|
+
*
|
|
1045
|
+
* No effect outside a subscription: a one-shot query records no footprint,
|
|
1046
|
+
* so there is nothing to opt out of.
|
|
1047
|
+
*/
|
|
1048
|
+
untracked?: boolean;
|
|
1049
|
+
}
|
|
1050
|
+
interface RunQuery {
|
|
1051
|
+
<A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>, options?: RunQueryOptions): Promise<R>;
|
|
1052
|
+
<Args, R>(reference: FunctionHandle<"query", Args, R>, args: Args, options?: RunQueryOptions): Promise<R>;
|
|
1053
|
+
}
|
|
1054
|
+
/** `ctx.runMutation` — overloaded for the same reason as {@link RunQuery}. */
|
|
1055
|
+
interface RunMutation {
|
|
1056
|
+
<A extends ArgsValidator, R>(reference: RegisteredMutation<A, R>, args: InferArgs<A>): Promise<R>;
|
|
1057
|
+
<Args, R>(reference: FunctionHandle<"mutation", Args, R>, args: Args): Promise<R>;
|
|
1058
|
+
}
|
|
1059
|
+
/** `ctx.runAction` — overloaded for the same reason as {@link RunQuery}. */
|
|
1060
|
+
interface RunAction {
|
|
1061
|
+
<A extends ArgsValidator, R>(reference: RegisteredAction<A, R>, args: InferArgs<A>): Promise<R>;
|
|
1062
|
+
<Args, R>(reference: FunctionHandle<"action", Args, R>, args: Args): Promise<R>;
|
|
1063
|
+
}
|
|
325
1064
|
/** Which side of the WebSocket lifecycle a hook fires on. */
|
|
326
|
-
type LifecycleEventKind = "connect" | "disconnect";
|
|
327
1065
|
/**
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
|
|
1066
|
+
* Which lifecycle moment a hook fires on.
|
|
1067
|
+
*
|
|
1068
|
+
* `connect`/`disconnect` are per-SOCKET and fire many times over a shard's life.
|
|
1069
|
+
* `init` is per-INSTANCE and fires once per cold start, before any handler runs
|
|
1070
|
+
* — see {@link ShardInitEvent}. `reactor` is per-WRITE-FLUSH and fires only when
|
|
1071
|
+
* a watched read's result changed — see `onQueryChange`. `whisper` is per-TOPIC
|
|
1072
|
+
* and decides whether a socket may join or broadcast to one — see `onWhisper`
|
|
1073
|
+
* and {@link WhisperEvent}; unlike the other four it is a query, and its return
|
|
1074
|
+
* value is the verdict. `socket` is per-ROUTE-FRAME and is
|
|
1075
|
+
* a mutation: a `defineWebSocket` registration dispatched when a socket opens,
|
|
1076
|
+
* sends a typed frame, or closes on the route's path.
|
|
1077
|
+
*/
|
|
1078
|
+
type LifecycleEventKind = "connect" | "disconnect" | "init" | "reactor" | "socket" | "whisper";
|
|
1079
|
+
/**
|
|
1080
|
+
* The event a whisper authorizer (`onWhisper`) receives as its second argument.
|
|
1081
|
+
*
|
|
1082
|
+
* Everything on it is either server-stamped from the socket's attachment
|
|
1083
|
+
* (`connectionId`, `shardKey`, `userId`, `context`) or the client-supplied
|
|
1084
|
+
* `topic` the verdict is about. The verified caller identity is also on
|
|
1085
|
+
* `ctx.auth` — the authorizer runs under the socket's own identity.
|
|
1086
|
+
*/
|
|
1087
|
+
interface WhisperEvent {
|
|
1088
|
+
/**
|
|
1089
|
+
* Which side of the channel is being authorized: `"subscribe"` when the
|
|
1090
|
+
* socket asks to JOIN the topic (and so to receive every message on it),
|
|
1091
|
+
* `"send"` when it asks to BROADCAST to it. Checked separately because they
|
|
1092
|
+
* are separate powers — a read-only observer is a coherent thing to allow.
|
|
1093
|
+
*/
|
|
1094
|
+
readonly action: "send" | "subscribe";
|
|
1095
|
+
/** Stable per-socket id, the same one `onConnect` saw. */
|
|
1096
|
+
readonly connectionId: string;
|
|
1097
|
+
/** App-supplied connection context from the client `connect` envelope (e.g. `{ roomId }`). */
|
|
1098
|
+
readonly context?: Record<string, unknown>;
|
|
1099
|
+
/** The shard this socket is bound to — the outer boundary the topic lives inside. */
|
|
1100
|
+
readonly shardKey: string;
|
|
1101
|
+
/** The topic name the client asked for. Client-supplied: validate it, never trust its shape. */
|
|
1102
|
+
readonly topic: string;
|
|
1103
|
+
/** Verified user id resolved at upgrade, or `null` for an anonymous socket. */
|
|
1104
|
+
readonly userId: string | null;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* The event a connection-lifecycle hook receives as its second argument. It is
|
|
1108
|
+
* the JSON-serializable payload the DO forwards on socket connect/disconnect;
|
|
1109
|
+
* the verified caller identity is also reflected on `ctx.auth` (the hook runs
|
|
1110
|
+
* under the connecting user via `resolveIdentity`).
|
|
1111
|
+
*/
|
|
333
1112
|
interface LifecycleEvent {
|
|
334
1113
|
/** Stable per-socket id, minted at upgrade and replayed verbatim on disconnect. */
|
|
335
1114
|
readonly connectionId: string;
|
|
@@ -341,33 +1120,85 @@ interface LifecycleEvent {
|
|
|
341
1120
|
readonly userId: string | null;
|
|
342
1121
|
}
|
|
343
1122
|
/**
|
|
344
|
-
* A registered connection-lifecycle hook — an internal mutation tagged with the
|
|
345
|
-
* lifecycle side it fires on. Produced by `onConnect` / `onDisconnect
|
|
346
|
-
|
|
1123
|
+
* A registered connection-lifecycle hook — an internal mutation tagged with the
|
|
1124
|
+
* lifecycle side it fires on. Produced by `onConnect` / `onDisconnect` /
|
|
1125
|
+
* `onShardInit`.
|
|
1126
|
+
*/
|
|
347
1127
|
type RegisteredLifecycleHook = RegisteredFunction<Record<string, never>, void, "mutation"> & {
|
|
348
1128
|
readonly lifecycle: LifecycleEventKind;
|
|
349
1129
|
};
|
|
350
1130
|
/**
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
|
|
1131
|
+
* The event an `onShardInit` hook receives.
|
|
1132
|
+
*
|
|
1133
|
+
* Deliberately thin, and deliberately NOT a {@link LifecycleEvent}: there is no
|
|
1134
|
+
* socket here. An init hook fires because the Durable Object was constructed,
|
|
1135
|
+
* not because anyone connected — so there is no `connectionId` to report and no
|
|
1136
|
+
* user to attribute it to. The hook runs as a trusted system dispatch with no
|
|
1137
|
+
* request identity, exactly like a cron tick; `ctx.auth` is anonymous and RLS
|
|
1138
|
+
* does not apply. Derive whatever you need from `shardKey` and the shard's own
|
|
1139
|
+
* durable tables, never from an ambient caller — there isn't one.
|
|
1140
|
+
*/
|
|
1141
|
+
interface ShardInitEvent {
|
|
1142
|
+
/** The shard this Durable Object instance serves. */
|
|
1143
|
+
readonly shardKey: string;
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* A streaming query registration. Unlike {@link RegisteredFunction} the handler
|
|
1147
|
+
* returns an `AsyncIterable<R>` synchronously (it does NOT `Promise<R>`); the
|
|
1148
|
+
* runtime drives it frame by frame and forwards each chunk to the caller. The
|
|
1149
|
+
* third `signal` argument is wired to the caller's cancel signal so the handler
|
|
1150
|
+
* can stop early — break out of the loop or check `signal.aborted` between
|
|
1151
|
+
* yields.
|
|
1152
|
+
*/
|
|
358
1153
|
interface RegisteredStream<A extends ArgsValidator, R> {
|
|
359
1154
|
readonly args: A;
|
|
1155
|
+
/**
|
|
1156
|
+
* Present when the stream was declared `durable`. Chunks are persisted per
|
|
1157
|
+
* run before they reach a socket, the producer outlives the socket that
|
|
1158
|
+
* opened it, and a reconnecting (or second) client attaches to the same run
|
|
1159
|
+
* and replays what it missed. See {@link DurableStreamOptions}.
|
|
1160
|
+
*/
|
|
1161
|
+
readonly durable?: DurableStreamOptions;
|
|
360
1162
|
readonly handler: (context: unknown, args: InferArgs<A>, signal: AbortSignal) => AsyncIterable<R>;
|
|
361
1163
|
readonly kind: "stream";
|
|
362
1164
|
readonly visibility?: FunctionVisibility;
|
|
363
1165
|
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Durability settings for a `.stream()` procedure.
|
|
1168
|
+
*
|
|
1169
|
+
* A run is identified by the socket's verified identity plus the function path
|
|
1170
|
+
* and arguments, so two clients of the SAME user calling the same stream with
|
|
1171
|
+
* the same arguments observe one producer and one transcript. That identity is
|
|
1172
|
+
* the feature: it is what makes a reload resume rather than re-generate, and
|
|
1173
|
+
* what lets a second tab watch the same answer. A different identity always gets
|
|
1174
|
+
* its own run.
|
|
1175
|
+
*
|
|
1176
|
+
* Sharing applies to a run still in flight. Once a run finishes, a later caller
|
|
1177
|
+
* asking the same question gets a fresh one — a transcript is the record of one
|
|
1178
|
+
* execution, not a cached response.
|
|
1179
|
+
*
|
|
1180
|
+
* **What an attach does not do:** it replays a transcript, it does not re-run the
|
|
1181
|
+
* handler — so the procedure's middleware chain (`.use(rls(...))`, rate limits,
|
|
1182
|
+
* any custom guard) runs once, for the caller that started the run. That is why
|
|
1183
|
+
* the run key is identity-scoped: a guard that has already passed for one user
|
|
1184
|
+
* must never be skipped for another.
|
|
1185
|
+
*/
|
|
1186
|
+
interface DurableStreamOptions {
|
|
1187
|
+
/**
|
|
1188
|
+
* How long a finished transcript is kept before it is trimmed, in
|
|
1189
|
+
* milliseconds. Defaults to 24 hours — long enough to survive a reload and
|
|
1190
|
+
* a commute, short enough that a chatty shard doesn't accumulate
|
|
1191
|
+
* transcripts forever.
|
|
1192
|
+
*/
|
|
1193
|
+
readonly ttlMs?: number;
|
|
1194
|
+
}
|
|
364
1195
|
/** The system tables `ctx.db.system` can read. */
|
|
365
1196
|
type SystemTableName = "_scheduled_functions" | "_storage";
|
|
366
1197
|
/**
|
|
367
|
-
* A pending scheduled invocation as surfaced by the `_scheduled_functions`
|
|
368
|
-
* system table. Mirrors {@link ScheduledJob} (the `ctx.scheduler` view); the
|
|
369
|
-
* separate name keeps the system-table read surface self-describing.
|
|
370
|
-
*/
|
|
1198
|
+
* A pending scheduled invocation as surfaced by the `_scheduled_functions`
|
|
1199
|
+
* system table. Mirrors {@link ScheduledJob} (the `ctx.scheduler` view); the
|
|
1200
|
+
* separate name keeps the system-table read surface self-describing.
|
|
1201
|
+
*/
|
|
371
1202
|
interface ScheduledFunctionDoc {
|
|
372
1203
|
/** Function arguments the job will be dispatched with. */
|
|
373
1204
|
args: Record<string, unknown>;
|
|
@@ -375,14 +1206,26 @@ interface ScheduledFunctionDoc {
|
|
|
375
1206
|
attempts?: number;
|
|
376
1207
|
/** When the job was enqueued (epoch ms). */
|
|
377
1208
|
enqueuedAt: number;
|
|
378
|
-
/**
|
|
379
|
-
|
|
1209
|
+
/**
|
|
1210
|
+
* Fully-qualified `ns:fn` path of the function to invoke. Absent when the job
|
|
1211
|
+
* targets a durable workflow/agent instead — exactly one of `functionPath` /
|
|
1212
|
+
* {@link ScheduledFunctionDoc.workflow} is set on any given row.
|
|
1213
|
+
*/
|
|
1214
|
+
functionPath?: string;
|
|
380
1215
|
/** The job's id (the `_scheduled_functions` row id). */
|
|
381
1216
|
id: string;
|
|
1217
|
+
/** Logical workpool the job is concurrency-gated by, when any. */
|
|
1218
|
+
pool?: string;
|
|
382
1219
|
/** When the job is scheduled to fire (epoch ms). */
|
|
383
1220
|
scheduledFor: number;
|
|
384
1221
|
/** Routing hint forwarded so dispatch lands on the right shard. */
|
|
385
1222
|
shardKey?: string;
|
|
1223
|
+
/**
|
|
1224
|
+
* The workflow/agent export key (its generated class name) a fresh durable instance is started from
|
|
1225
|
+
* on fire (the {@link ScheduledFunctionDoc.args} become its `params`). Set
|
|
1226
|
+
* instead of {@link ScheduledFunctionDoc.functionPath}.
|
|
1227
|
+
*/
|
|
1228
|
+
workflow?: string;
|
|
386
1229
|
}
|
|
387
1230
|
/** Maps each system table name to the document shape its reads return. */
|
|
388
1231
|
interface SystemDocMap {
|
|
@@ -397,55 +1240,110 @@ interface SystemQuery<T extends SystemTableName> {
|
|
|
397
1240
|
collect: () => Promise<SystemDoc<T>[]>;
|
|
398
1241
|
}
|
|
399
1242
|
/**
|
|
400
|
-
* Read-only reader over Lunora's system tables (`_scheduled_functions`,
|
|
401
|
-
* `_storage`), exposed as `ctx.db.system`. Mirrors Convex's `ctx.db.system`.
|
|
402
|
-
*
|
|
403
|
-
* **Best-effort and eventually consistent.** Unlike `ctx.db
|
|
404
|
-
* reads the shard's transactional SQLite snapshot — the data behind these tables
|
|
405
|
-
* lives OUTSIDE the shard (scheduled functions in the `SchedulerDO`, storage
|
|
406
|
-
* objects in R2). Every `collect()` / `get()` reaches across to that source.
|
|
407
|
-
*
|
|
408
|
-
* It is **not part of the mutation transaction snapshot** (no OCC guard, no
|
|
409
|
-
* subscription dependency recorded — reading it inside a mutation does not pin
|
|
410
|
-
* it), and results are **eventually consistent** with writes a mutation just
|
|
411
|
-
* made (e.g. a freshly scheduled job may not appear yet).
|
|
412
|
-
*
|
|
413
|
-
* Read-only by design: mutate scheduled jobs via `ctx.scheduler`, storage
|
|
414
|
-
* objects via `ctx.storage`.
|
|
415
|
-
*/
|
|
1243
|
+
* Read-only reader over Lunora's system tables (`_scheduled_functions`,
|
|
1244
|
+
* `_storage`), exposed as `ctx.db.system`. Mirrors Convex's `ctx.db.system`.
|
|
1245
|
+
*
|
|
1246
|
+
* **Best-effort and eventually consistent.** Unlike `ctx.db.<table>` — which
|
|
1247
|
+
* reads the shard's transactional SQLite snapshot — the data behind these tables
|
|
1248
|
+
* lives OUTSIDE the shard (scheduled functions in the `SchedulerDO`, storage
|
|
1249
|
+
* objects in R2). Every `collect()` / `get()` reaches across to that source.
|
|
1250
|
+
*
|
|
1251
|
+
* It is **not part of the mutation transaction snapshot** (no OCC guard, no
|
|
1252
|
+
* subscription dependency recorded — reading it inside a mutation does not pin
|
|
1253
|
+
* it), and results are **eventually consistent** with writes a mutation just
|
|
1254
|
+
* made (e.g. a freshly scheduled job may not appear yet).
|
|
1255
|
+
*
|
|
1256
|
+
* Read-only by design: mutate scheduled jobs via `ctx.scheduler`, storage
|
|
1257
|
+
* objects via `ctx.storage`.
|
|
1258
|
+
*/
|
|
416
1259
|
interface SystemDatabaseReader {
|
|
417
1260
|
/**
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
1261
|
+
* Resolve a single system-table row by id, or `null` when absent.
|
|
1262
|
+
* (`_scheduled_functions` → job id; `_storage` → object key.)
|
|
1263
|
+
*/
|
|
421
1264
|
get: <T extends SystemTableName>(table: T, id: string) => Promise<SystemDoc<T> | null>;
|
|
422
1265
|
/**
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
1266
|
+
* Begin a read over a system table; call `.collect()` to resolve the full
|
|
1267
|
+
* list. No filtering, indexing, or pagination — the backing source is remote
|
|
1268
|
+
* and the surface stays deliberately minimal.
|
|
1269
|
+
*/
|
|
427
1270
|
query: <T extends SystemTableName>(table: T) => SystemQuery<T>;
|
|
428
1271
|
}
|
|
429
1272
|
/**
|
|
430
|
-
* Read-only handle bound to a table. Used by `query`/`mutation`/`action`. The
|
|
431
|
-
* actual SQL implementation lives in `@lunora/do`; these are signatures only.
|
|
432
|
-
*/
|
|
1273
|
+
* Read-only handle bound to a table. Used by `query`/`mutation`/`action`. The
|
|
1274
|
+
* actual SQL implementation lives in `@lunora/do`; these are signatures only.
|
|
1275
|
+
*/
|
|
433
1276
|
interface DatabaseReader {
|
|
1277
|
+
/**
|
|
1278
|
+
* The throwing sibling of {@link DatabaseReader.normalizeId}: brand `id` as an
|
|
1279
|
+
* {@link Id} for `tableName`, or throw `BAD_REQUEST` when it is not structurally
|
|
1280
|
+
* an id. Pure — it never reads the database, so a valid id for a row that
|
|
1281
|
+
* doesn't exist still returns.
|
|
1282
|
+
*
|
|
1283
|
+
* This is the **parse boundary** for an id that arrived as a plain `string`: a
|
|
1284
|
+
* wire payload, a mutator's args, a change plan computed on the client. The
|
|
1285
|
+
* alternative is `value as Id<"table">` at every such call site — an assertion,
|
|
1286
|
+
* not a check, and one that has to be repeated for every table a helper is
|
|
1287
|
+
* generic over:
|
|
1288
|
+
*
|
|
1289
|
+
* ```ts
|
|
1290
|
+
* for (const patch of plan.patches) {
|
|
1291
|
+
* await ctx.db.patch(ctx.db.asId("nodes", patch.id), patch.fields);
|
|
1292
|
+
* }
|
|
1293
|
+
* ```
|
|
1294
|
+
*
|
|
1295
|
+
* Ids are opaque strings, so the check is exactly `normalizeId`'s: it rejects
|
|
1296
|
+
* empty, whitespace-bearing, and NUL-bearing values, not "an id that isn't in
|
|
1297
|
+
* this table". Use it to get the brand honestly, and `get()` to learn whether
|
|
1298
|
+
* the row exists.
|
|
1299
|
+
*/
|
|
1300
|
+
asId: <T extends string>(tableName: T, id: string) => Id<T>;
|
|
1301
|
+
/**
|
|
1302
|
+
* The first row of `tableName` matching `args.where`, or `null`. Routed like
|
|
1303
|
+
* every table-addressed read: a `.global()` table is read from its global
|
|
1304
|
+
* backend, where `query(table).withIndex(...)` is not available.
|
|
1305
|
+
*/
|
|
1306
|
+
findFirst: (tableName: string, args?: {
|
|
1307
|
+
where?: Record<string, unknown>;
|
|
1308
|
+
}) => Promise<Record<string, unknown> | null>;
|
|
434
1309
|
get: <T extends string>(id: Id<T>) => Promise<Record<string, unknown> | null>;
|
|
435
1310
|
/**
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
1311
|
+
* Validate an untrusted `id` string against the structural shape of an id
|
|
1312
|
+
* for `tableName`, returning the branded {@link Id} when it is well-formed
|
|
1313
|
+
* and `null` otherwise. Pure structural validation — it never reads the
|
|
1314
|
+
* database, so a structurally valid id for a row that doesn't exist still
|
|
1315
|
+
* returns the branded id (mirrors Convex's `db.normalizeId`).
|
|
1316
|
+
*/
|
|
442
1317
|
normalizeId: <T extends string>(tableName: T, id: string) => Id<T> | null;
|
|
443
1318
|
query: (tableName: string) => TableReader;
|
|
444
1319
|
/**
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
1320
|
+
* Walk the relation graph out of one row: follow the foreign keys the
|
|
1321
|
+
* schema's `v.id("target")` columns already declare, breadth-first, and
|
|
1322
|
+
* return each reached row with its `depth`, the edge names walked to reach
|
|
1323
|
+
* it (`path`), the ids along the way (`pathIds`), and a depth-decaying
|
|
1324
|
+
* `score`.
|
|
1325
|
+
*
|
|
1326
|
+
* This is the read a keyword or vector search cannot do: "everything
|
|
1327
|
+
* connected to this customer" is a property of the edges, not of the text.
|
|
1328
|
+
* Depth 1 (the default) is the direct neighbourhood; a multi-hop expansion
|
|
1329
|
+
* is the same call with a higher `depth`.
|
|
1330
|
+
*
|
|
1331
|
+
* ```ts
|
|
1332
|
+
* const { nodes } = await ctx.db.related({ table: "customers", id }, { depth: 2 });
|
|
1333
|
+
* ```
|
|
1334
|
+
*
|
|
1335
|
+
* Every hop is a normal `ctx.db` read, so row-level security, column
|
|
1336
|
+
* masking, soft-delete scoping and subscription dependency tracking all
|
|
1337
|
+
* apply per hop. `depth` is capped at 4 and `limit` at 200; a
|
|
1338
|
+
* `v.array(v.id(...))` column is followed outward only (there is no
|
|
1339
|
+
* array-containment filter to find its holders with).
|
|
1340
|
+
*/
|
|
1341
|
+
related: (start: RelatedStart, options?: RelatedOptions) => Promise<RelatedPage>;
|
|
1342
|
+
/**
|
|
1343
|
+
* Best-effort, read-only reader over Lunora's system tables
|
|
1344
|
+
* (`_scheduled_functions`, `_storage`). Eventually consistent and **not**
|
|
1345
|
+
* part of the transaction snapshot — see {@link SystemDatabaseReader}.
|
|
1346
|
+
*/
|
|
449
1347
|
readonly system: SystemDatabaseReader;
|
|
450
1348
|
}
|
|
451
1349
|
/** Options for {@link TableReader.paginate} — Convex-compatible page request. */
|
|
@@ -453,14 +1351,14 @@ interface PaginationOptions {
|
|
|
453
1351
|
/** Opaque cursor from the prior page's `continueCursor`; `null`/omitted starts at the first page. */
|
|
454
1352
|
cursor?: null | string;
|
|
455
1353
|
/**
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
1354
|
+
* Optional inclusive upper bound for reactive pagination. When supplied the
|
|
1355
|
+
* page covers the fixed half-open range `(cursor, endCursor]` (ignoring
|
|
1356
|
+
* `numItems`): every row strictly after `cursor` up to and including the
|
|
1357
|
+
* boundary row `endCursor` encodes. The page's `isDone` is `true` and its
|
|
1358
|
+
* `continueCursor` echoes `endCursor`, so the next page keeps starting where
|
|
1359
|
+
* this one ends even as rows are inserted/deleted inside the range. Omit (or
|
|
1360
|
+
* pass `null`) for the legacy "first `numItems` after `cursor`" behaviour.
|
|
1361
|
+
*/
|
|
464
1362
|
endCursor?: null | string;
|
|
465
1363
|
/** Maximum rows to return for this page. */
|
|
466
1364
|
numItems: number;
|
|
@@ -473,47 +1371,176 @@ interface PaginationResult<T = Record<string, unknown>> {
|
|
|
473
1371
|
isDone: boolean;
|
|
474
1372
|
page: T[];
|
|
475
1373
|
/**
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
1374
|
+
* Reactive-pagination only: the midpoint cursor of a bounded
|
|
1375
|
+
* `(cursor, endCursor]` page, used by the client to split an over-grown page
|
|
1376
|
+
* into two adjacent ranges. Absent on legacy (open-ended) pages.
|
|
1377
|
+
*/
|
|
480
1378
|
splitCursor?: null | string;
|
|
481
1379
|
}
|
|
1380
|
+
/** Which way foreign-key edges are followed by {@link DatabaseReader.related}. */
|
|
1381
|
+
type RelatedDirection = "both" | "in" | "out";
|
|
1382
|
+
/** An explicit `{ table, id }` start node for {@link DatabaseReader.related}. */
|
|
1383
|
+
interface RelatedStartReference {
|
|
1384
|
+
id: string;
|
|
1385
|
+
table: string;
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Where a traversal starts: a loaded document (recognised by its `_id`, whose
|
|
1389
|
+
* table the reader resolves), or an explicit `{ table, id }`.
|
|
1390
|
+
*
|
|
1391
|
+
* The document arm requires `_id`, which is what makes the two arms actually
|
|
1392
|
+
* distinguishable: against a bare `Record<string, unknown>` the union collapses
|
|
1393
|
+
* — `{ table, id }` is assignable to it — so a misspelled `{ tabel, id }` used
|
|
1394
|
+
* to type-check and fail only at runtime.
|
|
1395
|
+
*/
|
|
1396
|
+
type RelatedStart = (Record<string, unknown> & {
|
|
1397
|
+
_id: string;
|
|
1398
|
+
}) | RelatedStartReference;
|
|
1399
|
+
/** Options for {@link DatabaseReader.related}. */
|
|
1400
|
+
interface RelatedOptions {
|
|
1401
|
+
/** Opaque cursor from a prior page's `continueCursor`; `null`/omitted starts at the first page. */
|
|
1402
|
+
cursor?: null | string;
|
|
1403
|
+
/**
|
|
1404
|
+
* How many hops to expand. `1` (the default) is the direct neighbourhood.
|
|
1405
|
+
* Must be an integer in `1 … 4`; anything else is refused rather than
|
|
1406
|
+
* clamped, because a silently-shortened walk looks like a graph with fewer
|
|
1407
|
+
* edges than it has.
|
|
1408
|
+
*/
|
|
1409
|
+
depth?: number;
|
|
1410
|
+
/**
|
|
1411
|
+
* Which way foreign keys are followed. `"out"` follows the ids the start
|
|
1412
|
+
* row HOLDS (ticket → customer), `"in"` the rows that point AT it (customer
|
|
1413
|
+
* → tickets), `"both"` (the default) does both.
|
|
1414
|
+
*/
|
|
1415
|
+
direction?: RelatedDirection;
|
|
1416
|
+
/**
|
|
1417
|
+
* Restrict the walk to these edge-type names — `"<table>.<column>"`, e.g.
|
|
1418
|
+
* `"tickets.customerId"`. Omitted ⇒ every `v.id(...)` column the schema
|
|
1419
|
+
* declares. An unknown name is refused, so a typo cannot silently widen the
|
|
1420
|
+
* traversal.
|
|
1421
|
+
*/
|
|
1422
|
+
edges?: ReadonlyArray<string>;
|
|
1423
|
+
/** Maximum nodes per page. Default 50, capped at 200. */
|
|
1424
|
+
limit?: number;
|
|
1425
|
+
}
|
|
1426
|
+
/** One row reached by {@link DatabaseReader.related}, with how it was reached. */
|
|
1427
|
+
interface RelatedNode<T = Record<string, unknown>> {
|
|
1428
|
+
/** Hops from the start node; always `>= 1`. */
|
|
1429
|
+
depth: number;
|
|
1430
|
+
/** The reached row. */
|
|
1431
|
+
document: T;
|
|
1432
|
+
/** Edge-type names walked from the start node to this one, in order. Length equals `depth`. */
|
|
1433
|
+
path: ReadonlyArray<string>;
|
|
1434
|
+
/** Document ids from the start node to this one inclusive. Length equals `depth + 1`. */
|
|
1435
|
+
pathIds: ReadonlyArray<string>;
|
|
1436
|
+
/** Depth-decaying relevance: `1` at depth 1, halving each hop (`0.5 ** (depth - 1)`). */
|
|
1437
|
+
score: number;
|
|
1438
|
+
/** The table {@link RelatedNode.document} lives in. */
|
|
1439
|
+
table: string;
|
|
1440
|
+
}
|
|
1441
|
+
/** One page of a {@link DatabaseReader.related} traversal — the {@link PaginationResult} envelope, with `nodes` instead of `page`. */
|
|
1442
|
+
interface RelatedPage<T = Record<string, unknown>> {
|
|
1443
|
+
/** Cursor to pass back for the next page, or `null` once `isDone`. */
|
|
1444
|
+
continueCursor: null | string;
|
|
1445
|
+
/** `true` when this page is the last one. */
|
|
1446
|
+
isDone: boolean;
|
|
1447
|
+
/** The reached nodes, nearest first. */
|
|
1448
|
+
nodes: RelatedNode<T>[];
|
|
1449
|
+
}
|
|
482
1450
|
/**
|
|
483
|
-
* The fluent `ctx.db.query(table)` reader. Generic over the document type
|
|
484
|
-
* `Row` so the generated `ctx.db` can bind it to `Doc
|
|
485
|
-
* every terminal then resolve typed rows — no `as unknown as Doc
|
|
486
|
-
*
|
|
487
|
-
* (
|
|
488
|
-
|
|
489
|
-
|
|
1451
|
+
* The fluent `ctx.db.query(table)` reader. Generic over the document type
|
|
1452
|
+
* `Row` so the generated `ctx.db` can bind it to `Doc<table>` (the chain and
|
|
1453
|
+
* every terminal then resolve typed rows — no `as unknown as Doc<...>` casts),
|
|
1454
|
+
* and over the table's declared index names so `.withIndex()` / `.withSearchIndex()`
|
|
1455
|
+
* / `.withGeoIndex()` reject a name the table does not declare.
|
|
1456
|
+
*
|
|
1457
|
+
* All four default to the untyped shape for the base (schema-agnostic)
|
|
1458
|
+
* `@lunora/server` reader, which is also the wide `(table: string) => TableReader`
|
|
1459
|
+
* overload the generated `ctx.db` intersects in — so a caller holding a runtime
|
|
1460
|
+
* string (e.g. `@lunora/ratelimit`'s `createDbStore`) is unaffected.
|
|
1461
|
+
*
|
|
1462
|
+
* The index-name parameters are what make a stale index name a compile error.
|
|
1463
|
+
* They resolve to `never` for a table that declares none of that kind, so the
|
|
1464
|
+
* only way to satisfy the call is to declare the index. Before this, a renamed
|
|
1465
|
+
* or dropped index left its call sites typechecking, and the query either threw
|
|
1466
|
+
* at runtime or silently degraded to a full table scan — the second being the
|
|
1467
|
+
* worse outcome, since it stays green in tests and surfaces months later as a
|
|
1468
|
+
* latency regression.
|
|
1469
|
+
*
|
|
1470
|
+
* **Why `with*` are method signatures and everything else is a property.**
|
|
1471
|
+
* Narrowing a parameter makes the enclosing type contravariant in it, so as
|
|
1472
|
+
* function properties these would make a BOUND reader
|
|
1473
|
+
* (`TableReader<Doc, "by_x">`, what `ctx.db.query(t)` returns) unassignable to
|
|
1474
|
+
* the unbound `TableReader<Doc>` — quietly breaking every helper factored as
|
|
1475
|
+
* `(reader: TableReader<Doc<"users">>) => …`, which is the obvious way to share
|
|
1476
|
+
* query logic. A method signature is bivariant in its parameters, which keeps
|
|
1477
|
+
* that direction working while the narrow parameter still rejects an undeclared
|
|
1478
|
+
* name at the call site. Both directions are pinned in `types.test-d.ts`.
|
|
1479
|
+
*/
|
|
1480
|
+
interface TableReader<Row = Record<string, unknown>, Indexes extends string = string, SearchIndexes extends string = string, GeoIndexes extends string = string> {
|
|
1481
|
+
/**
|
|
1482
|
+
* Iterate rows lazily: `for await (const row of ctx.db.query("t").withIndex(…))`.
|
|
1483
|
+
*
|
|
1484
|
+
* Pages through the result set behind the scenes and yields row by row, so
|
|
1485
|
+
* a consumer that stops early stops the reads too. `.collect()` is still the
|
|
1486
|
+
* right terminal when you want the whole set; this exists for the cases
|
|
1487
|
+
* where you cannot know up front how far you need to read.
|
|
1488
|
+
*
|
|
1489
|
+
* That is what merged/ordered index streams need. Reimplementing Convex's
|
|
1490
|
+
* `convex-helpers/server/stream` in userland previously meant materialising
|
|
1491
|
+
* each branch with a bounded `.take(1024)` before merging, so asking a
|
|
1492
|
+
* merged stream for ONE row read up to 1,024 rows per branch. The k-way merge itself is application code and stays there — only
|
|
1493
|
+
* the laziness had to come from the database layer.
|
|
1494
|
+
*
|
|
1495
|
+
* Iteration pages through `.paginate()`, so it follows the same order —
|
|
1496
|
+
* which is `.collect()`'s order whenever the sort key is unique. Under a
|
|
1497
|
+
* TIED sort key (an unindexed read whose rows share `_creationTime`) the
|
|
1498
|
+
* two can disagree, because the tie-break is left to SQLite. Read through
|
|
1499
|
+
* an index when order matters, exactly as you would for `.paginate()`.
|
|
1500
|
+
*/
|
|
1501
|
+
[Symbol.asyncIterator]: () => AsyncIterator<Row>;
|
|
490
1502
|
collect: () => Promise<Row[]>;
|
|
491
|
-
filter: (predicate: (document: Row) => boolean) => TableReader<Row>;
|
|
1503
|
+
filter: (predicate: (document: Row) => boolean) => TableReader<Row, Indexes, SearchIndexes, GeoIndexes>;
|
|
492
1504
|
first: () => Promise<Row | null>;
|
|
493
1505
|
/**
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
order: (direction: "asc" | "desc") => TableReader<Row>;
|
|
1506
|
+
* Set the result order. Orders by the active `.withIndex()` (or by
|
|
1507
|
+
* `_creationTime` when none is staged), `"asc"` by default; `"desc"`
|
|
1508
|
+
* reverses it. Composes with `.withIndex()`, `.filter()`, and every
|
|
1509
|
+
* terminal (`collect`/`first`/`take`/`paginate`/`unique`). Mirrors Convex's
|
|
1510
|
+
* `.order("asc" | "desc")`.
|
|
1511
|
+
*/
|
|
1512
|
+
order: (direction: "asc" | "desc") => TableReader<Row, Indexes, SearchIndexes, GeoIndexes>;
|
|
501
1513
|
paginate: (options: PaginationOptions) => Promise<PaginationResult<Row>>;
|
|
502
1514
|
take: (limit: number) => Promise<Row[]>;
|
|
503
1515
|
/**
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
1516
|
+
* Return the single matching document. Returns `null` when nothing matches
|
|
1517
|
+
* and throws when more than one row matches. Mirrors Convex's `.unique()`.
|
|
1518
|
+
*/
|
|
507
1519
|
unique: () => Promise<Row | null>;
|
|
508
|
-
withIndex: (indexName: string, range?: (q: IndexRangeBuilder) => IndexRangeBuilder) => TableReader<Row>;
|
|
509
1520
|
/**
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
1521
|
+
* Restrict the query to a declared `.geoIndex()`. The builder's
|
|
1522
|
+
* `.near(point, radiusMeters)` returns rows within `radiusMeters` of `point`,
|
|
1523
|
+
* ordered nearest-first; `.within(bbox)` returns rows inside the
|
|
1524
|
+
* latitude/longitude bounding box. Both resolve as a geohash-prefix range
|
|
1525
|
+
* scan over the index's companion followed by a Haversine refine. Pair with
|
|
1526
|
+
* `.take(n)` to cap results (`.paginate()` is not supported on a geo query).
|
|
1527
|
+
*/
|
|
1528
|
+
withGeoIndex(indexName: GeoIndexes, build: (q: GeoFilterBuilder) => GeoFilterBuilder): TableReader<Row, Indexes, SearchIndexes, GeoIndexes>;
|
|
1529
|
+
/**
|
|
1530
|
+
* Restrict the query to a declared `.index()`. `indexName` is constrained to
|
|
1531
|
+
* this table's declared index names (`never` when it declares none), so a
|
|
1532
|
+
* renamed, dropped, or mistyped index is a compile error rather than a
|
|
1533
|
+
* runtime throw or a silent full-table scan.
|
|
1534
|
+
*/
|
|
1535
|
+
withIndex(indexName: Indexes, range?: (q: IndexRangeBuilder) => IndexRangeBuilder): TableReader<Row, Indexes, SearchIndexes, GeoIndexes>;
|
|
1536
|
+
/**
|
|
1537
|
+
* Restrict the query to a declared `.searchIndex()`. The builder's
|
|
1538
|
+
* `.search(field, query)` runs a full-text match against the index's
|
|
1539
|
+
* searchable field; `.eq(field, value)` narrows by a declared filter
|
|
1540
|
+
* field. Results come back ordered by relevance — pair with `.take(n)`
|
|
1541
|
+
* (`.paginate()` is not supported on a search query).
|
|
1542
|
+
*/
|
|
1543
|
+
withSearchIndex(indexName: SearchIndexes, search: (q: SearchFilterBuilder) => SearchFilterBuilder): TableReader<Row, Indexes, SearchIndexes, GeoIndexes>;
|
|
517
1544
|
}
|
|
518
1545
|
interface IndexRangeBuilder {
|
|
519
1546
|
eq: (field: string, value: unknown) => IndexRangeBuilder;
|
|
@@ -529,13 +1556,37 @@ interface SearchFilterBuilder {
|
|
|
529
1556
|
/** Full-text match `query` against the index's searchable `field`. Call exactly once. */
|
|
530
1557
|
search: (field: string, query: string) => SearchFilterBuilder;
|
|
531
1558
|
}
|
|
1559
|
+
/** A latitude/longitude point (WGS84 decimal degrees) accepted by geo queries. */
|
|
1560
|
+
interface GeoPointInput {
|
|
1561
|
+
lat: number;
|
|
1562
|
+
lng: number;
|
|
1563
|
+
}
|
|
532
1564
|
/**
|
|
533
|
-
*
|
|
534
|
-
* `
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1565
|
+
* An axis-aligned latitude/longitude bounding box: `sw` is the south-west
|
|
1566
|
+
* (min lat, min lng) corner, `ne` the north-east (max lat, max lng) corner.
|
|
1567
|
+
*/
|
|
1568
|
+
interface GeoBoundingBox {
|
|
1569
|
+
ne: GeoPointInput;
|
|
1570
|
+
sw: GeoPointInput;
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Builder passed to {@link TableReader.withGeoIndex}. Call exactly one of
|
|
1574
|
+
* `.near(...)` / `.within(...)` — the two are mutually exclusive proximity vs
|
|
1575
|
+
* bounding-box modes.
|
|
1576
|
+
*/
|
|
1577
|
+
interface GeoFilterBuilder {
|
|
1578
|
+
/** Rows within `radiusMeters` of `point`, resolved nearest-first. Call exactly once. */
|
|
1579
|
+
near: (point: GeoPointInput, radiusMeters: number) => GeoFilterBuilder;
|
|
1580
|
+
/** Rows whose point falls inside the bounding `box`. Call exactly once. */
|
|
1581
|
+
within: (box: GeoBoundingBox) => GeoFilterBuilder;
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Options shared by the batch-write methods (`insertMany`/`deleteMany`/
|
|
1585
|
+
* `patchMany`) — a per-call payload cap. The default cap (500) rejects an
|
|
1586
|
+
* oversized call up front so an accidental O(n²) or a payload past the Durable
|
|
1587
|
+
* Object request limit fails loudly instead of degrading the mutation. Callers
|
|
1588
|
+
* with larger sets should chunk their own loop or raise `limit`.
|
|
1589
|
+
*/
|
|
539
1590
|
interface BatchWriteOptions {
|
|
540
1591
|
/** Reject the call when the batch size exceeds this value (default 500). */
|
|
541
1592
|
limit?: number;
|
|
@@ -543,67 +1594,90 @@ interface BatchWriteOptions {
|
|
|
543
1594
|
/** Options accepted by {@link DatabaseWriter.insertMany} and the per-table facade. */
|
|
544
1595
|
interface InsertManyOptions extends BatchWriteOptions {
|
|
545
1596
|
/**
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
1597
|
+
* When `true`, a UNIQUE-constraint breach for a row resolves to `null`
|
|
1598
|
+
* instead of throwing — the rest of the batch is still inserted. Skipped rows
|
|
1599
|
+
* keep their input-order slot with `null` in the returned array. Mirrors
|
|
1600
|
+
* better-drizzle's `createMany({ skipDuplicates: true })`.
|
|
1601
|
+
*/
|
|
551
1602
|
skipDuplicates?: boolean;
|
|
552
1603
|
}
|
|
1604
|
+
/** What {@link DatabaseWriter.patchIf} resolves: whether the patch applied, and the row as it now stands (`null` when there is no such row). */
|
|
1605
|
+
interface PatchIfResult {
|
|
1606
|
+
applied: boolean;
|
|
1607
|
+
doc: Record<string, unknown> | null;
|
|
1608
|
+
}
|
|
553
1609
|
interface DatabaseWriter extends DatabaseReader {
|
|
554
1610
|
delete: <T extends string>(id: Id<T>) => Promise<void>;
|
|
555
1611
|
/**
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
1612
|
+
* Delete EVERY row in `tableName`, chunking internally until the table is
|
|
1613
|
+
* empty — the erasure primitive.
|
|
1614
|
+
*
|
|
1615
|
+
* Unlike `deleteWhere(tableName, {})` there is **no batch cap**: a
|
|
1616
|
+
* `BATCH_LIMIT_EXCEEDED` at row 501 of an account deletion is a bug, not a
|
|
1617
|
+
* safety rail. Rows still go through the single-row delete pipeline, so
|
|
1618
|
+
* triggers, cascades, companions, CDC, and live subscriptions stay correct.
|
|
1619
|
+
*
|
|
1620
|
+
* On a `.softDelete()` table the default flips the marker column; pass
|
|
1621
|
+
* `{ hard: true }` to remove the rows physically (what GDPR erasure means).
|
|
1622
|
+
*/
|
|
1623
|
+
deleteAll: (tableName: string, options?: {
|
|
1624
|
+
chunkSize?: number;
|
|
1625
|
+
hard?: boolean;
|
|
1626
|
+
}) => Promise<{
|
|
1627
|
+
deleted: number;
|
|
1628
|
+
}>;
|
|
1629
|
+
/**
|
|
1630
|
+
* Delete many rows by id in one call. Each id is deleted through the full
|
|
1631
|
+
* single-row pipeline (triggers + per-row RLS). The returned `deleted` is the
|
|
1632
|
+
* number of ids **requested**, not the rows actually removed — an unknown or
|
|
1633
|
+
* duplicated id is a silent no-op.
|
|
1634
|
+
*
|
|
1635
|
+
* **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
|
|
1636
|
+
* BEGIN/COMMIT span, so a mid-batch failure (a later RLS denial or handler
|
|
1637
|
+
* error) rolls back the whole mutation. (In an action there is no transaction
|
|
1638
|
+
* span, so the prior deletes persist; the in-memory test harness mirrors the span.)
|
|
1639
|
+
*/
|
|
566
1640
|
deleteMany: <T extends string>(ids: ReadonlyArray<Id<T>>, options?: BatchWriteOptions) => Promise<{
|
|
567
1641
|
deleted: number;
|
|
568
1642
|
}>;
|
|
569
1643
|
/**
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
1644
|
+
* Delete every row matching `where` in one call. Matching rows are resolved
|
|
1645
|
+
* first, then each row is deleted through the single-row delete pipeline
|
|
1646
|
+
* (triggers, companion sync, CDC, broadcast) so reactive subscriptions and
|
|
1647
|
+
* search/aggregate companions stay correct.
|
|
1648
|
+
*
|
|
1649
|
+
* **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
|
|
1650
|
+
* BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
|
|
1651
|
+
*/
|
|
578
1652
|
deleteWhere: (tableName: string, where: Record<string, unknown>, options?: BatchWriteOptions) => Promise<{
|
|
579
1653
|
deleted: number;
|
|
580
1654
|
}>;
|
|
581
1655
|
/**
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
1656
|
+
* Insert a document, returning its server id.
|
|
1657
|
+
*
|
|
1658
|
+
* Pass `options.clientId` (a UUID) to key the row yourself — for an
|
|
1659
|
+
* optimistic client that needs the persisted row to match the key it
|
|
1660
|
+
* already rendered. It's validated for shape and still subject to the
|
|
1661
|
+
* primary-key uniqueness constraint; omit it and the server mints the id.
|
|
1662
|
+
*/
|
|
589
1663
|
insert: <T extends string>(tableName: T, document: Record<string, unknown>, options?: {
|
|
590
1664
|
clientId?: string;
|
|
591
1665
|
}) => Promise<Id<T>>;
|
|
592
1666
|
/**
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
1667
|
+
* Insert many documents into one table in a single call, returning the
|
|
1668
|
+
* minted ids in input order. Equivalent to a per-row `insert()` loop — each
|
|
1669
|
+
* row gets defaults, validators, triggers, and a per-row RLS check — but the
|
|
1670
|
+
* caller pays one round-trip instead of N.
|
|
1671
|
+
*
|
|
1672
|
+
* Pass `{ skipDuplicates: true }` to turn UNIQUE-constraint breaches into
|
|
1673
|
+
* `null` results for that row instead of failing the whole batch; the rest of
|
|
1674
|
+
* the batch is still inserted and order is preserved.
|
|
1675
|
+
*
|
|
1676
|
+
* **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
|
|
1677
|
+
* BEGIN/COMMIT span, so a mid-batch failure (an invalid or RLS-denied row)
|
|
1678
|
+
* rolls back the whole mutation. (In an action there is no transaction span,
|
|
1679
|
+
* so the prior inserts persist; the in-memory test harness mirrors the span.)
|
|
1680
|
+
*/
|
|
607
1681
|
insertMany: {
|
|
608
1682
|
<T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options: BatchWriteOptions & {
|
|
609
1683
|
skipDuplicates: true;
|
|
@@ -611,32 +1685,63 @@ interface DatabaseWriter extends DatabaseReader {
|
|
|
611
1685
|
<T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options?: InsertManyOptions): Promise<Id<T>[]>;
|
|
612
1686
|
};
|
|
613
1687
|
/**
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1688
|
+
* **Trusted** bulk insert: one multi-row `INSERT` that **skips per-row
|
|
1689
|
+
* `.check()` validators and before/after triggers** for throughput on data you
|
|
1690
|
+
* control (seed, migration, admin import). Defaults, ids, and every companion
|
|
1691
|
+
* (search/aggregate/rank/CDC + live subscriptions) are still applied, so reads
|
|
1692
|
+
* stay correct.
|
|
1693
|
+
*
|
|
1694
|
+
* It is **"unsafe" only in that it bypasses the validation/trigger pipeline** —
|
|
1695
|
+
* RLS is **not** bypassed: secure-by-default and the table's insert policy still
|
|
1696
|
+
* apply (the framework ships no RLS-bypassing writer). Pass `allowExplicitId` to
|
|
1697
|
+
* preserve a supplied `_id` (import). Use only for data you trust; prefer
|
|
1698
|
+
* `insertMany` for anything user-supplied.
|
|
1699
|
+
*/
|
|
626
1700
|
insertManyUnsafe: <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options?: BatchWriteOptions & {
|
|
627
1701
|
allowExplicitId?: boolean;
|
|
628
1702
|
}) => Promise<Id<T>[]>;
|
|
629
1703
|
patch: <T extends string>(id: Id<T>, patch: Record<string, unknown>) => Promise<void>;
|
|
630
1704
|
/**
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
1705
|
+
* Conditional patch: apply `args.patch` to row `id` of `tableName` only while
|
|
1706
|
+
* the row as currently stored matches `args.where`, decided atomically with
|
|
1707
|
+
* the write. Use it for compare-and-set and admission checks, where reading
|
|
1708
|
+
* the row, deciding, and then calling `patch` lets two concurrent callers
|
|
1709
|
+
* both act on the same stale read.
|
|
1710
|
+
*
|
|
1711
|
+
* ```ts
|
|
1712
|
+
* const { applied } = await ctx.db.patchIf("seats", seatId, { where: { holder: null }, patch: { holder: userId } });
|
|
1713
|
+
* ```
|
|
1714
|
+
*
|
|
1715
|
+
* Resolves `{ applied: true, doc }` with the row as written, or
|
|
1716
|
+
* `{ applied: false, doc }` with the row left untouched (`doc` is `null` when
|
|
1717
|
+
* there is no such row). `where` takes the same flat column filters as
|
|
1718
|
+
* `findMany`; relation filters are not supported. Triggers, validators,
|
|
1719
|
+
* companions, CDC and live queries see an applied patch exactly as they see
|
|
1720
|
+
* `patch`.
|
|
1721
|
+
*
|
|
1722
|
+
* Platform mapping (`conditionalWrites` in `PlatformCapabilities`):
|
|
1723
|
+
* - shard-local tables: Cloudflare `storage.sql`, Node `better-sqlite3` and
|
|
1724
|
+
* celld's SQLite decide in one synchronous step; native everywhere.
|
|
1725
|
+
* - `.global()` tables: one conditional `UPDATE` through `@lunora/sql-store`
|
|
1726
|
+
* on D1 (Cloudflare, celld), the Node host's SQLite file, or Postgres/MySQL
|
|
1727
|
+
* over Hyperdrive.
|
|
1728
|
+
* - A backend without a conditional write refuses with `INTERNAL` instead of
|
|
1729
|
+
* reading and then writing.
|
|
1730
|
+
*/
|
|
1731
|
+
patchIf: <T extends string>(tableName: T, id: Id<T>, args: {
|
|
1732
|
+
patch: Record<string, unknown>;
|
|
1733
|
+
where: Record<string, unknown>;
|
|
1734
|
+
}) => Promise<PatchIfResult>;
|
|
1735
|
+
/**
|
|
1736
|
+
* Patch many rows by id in one call. Each `{ id, patch }` is applied like a
|
|
1737
|
+
* single `patch()` (per-row triggers + RLS). Returns the number of rows
|
|
1738
|
+
* actually patched.
|
|
1739
|
+
*
|
|
1740
|
+
* **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
|
|
1741
|
+
* BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
|
|
1742
|
+
* (In an action there is no transaction span, so the prior patches persist;
|
|
1743
|
+
* the in-memory test harness mirrors the span.)
|
|
1744
|
+
*/
|
|
640
1745
|
patchMany: <T extends string>(patches: ReadonlyArray<{
|
|
641
1746
|
id: Id<T>;
|
|
642
1747
|
patch: Record<string, unknown>;
|
|
@@ -644,14 +1749,14 @@ interface DatabaseWriter extends DatabaseReader {
|
|
|
644
1749
|
patched: number;
|
|
645
1750
|
}>;
|
|
646
1751
|
/**
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1752
|
+
* Patch every row matching `where` with the same `patch` in one call. The
|
|
1753
|
+
* matching rows are resolved first, then each row is updated through the
|
|
1754
|
+
* single-row patch pipeline (OCC, triggers, companion sync, CDC, broadcast)
|
|
1755
|
+
* so reactive subscriptions and search/aggregate companions stay correct.
|
|
1756
|
+
*
|
|
1757
|
+
* **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
|
|
1758
|
+
* BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
|
|
1759
|
+
*/
|
|
655
1760
|
patchWhere: (tableName: string, args: {
|
|
656
1761
|
patch: Record<string, unknown>;
|
|
657
1762
|
where: Record<string, unknown>;
|
|
@@ -659,6 +1764,29 @@ interface DatabaseWriter extends DatabaseReader {
|
|
|
659
1764
|
patched: number;
|
|
660
1765
|
}>;
|
|
661
1766
|
replace: <T extends string>(id: Id<T>, document: Record<string, unknown>) => Promise<void>;
|
|
1767
|
+
/**
|
|
1768
|
+
* Erase every shard-local table — the account-deletion / tenant-teardown
|
|
1769
|
+
* primitive. Sweeps the schema's non-`.global()` tables with
|
|
1770
|
+
* {@link DatabaseWriter.deleteAll}`({ hard: true })` and returns the per-table
|
|
1771
|
+
* counts.
|
|
1772
|
+
*
|
|
1773
|
+
* `.global()` tables are skipped by design: their rows live in D1 and are shared
|
|
1774
|
+
* across shards, so "wipe this shard" must not reach them. Restrict the sweep
|
|
1775
|
+
* with `options.tables`, or spare one with `options.exclude` (e.g. an audit log
|
|
1776
|
+
* that must outlive the data).
|
|
1777
|
+
*
|
|
1778
|
+
* ```ts
|
|
1779
|
+
* export const deleteAccount = internalMutation({ handler: async ({ ctx }) => ctx.db.wipeShard() });
|
|
1780
|
+
* ```
|
|
1781
|
+
*/
|
|
1782
|
+
wipeShard: (options?: {
|
|
1783
|
+
chunkSize?: number;
|
|
1784
|
+
exclude?: ReadonlyArray<string>;
|
|
1785
|
+
tables?: ReadonlyArray<string>;
|
|
1786
|
+
}) => Promise<{
|
|
1787
|
+
deleted: number;
|
|
1788
|
+
tables: Record<string, number>;
|
|
1789
|
+
}>;
|
|
662
1790
|
}
|
|
663
1791
|
/** Authenticated identity surfaced into every context. */
|
|
664
1792
|
interface AuthState {
|
|
@@ -666,24 +1794,81 @@ interface AuthState {
|
|
|
666
1794
|
readonly userId: string | null;
|
|
667
1795
|
}
|
|
668
1796
|
/**
|
|
669
|
-
* A pending scheduled invocation as surfaced by {@link Scheduler.list} /
|
|
670
|
-
* {@link Scheduler.get}. A clean public mirror of `@lunora/scheduler`'s
|
|
671
|
-
* `ScheduleRecord
|
|
672
|
-
*
|
|
673
|
-
|
|
1797
|
+
* A pending scheduled invocation as surfaced by {@link Scheduler.list} /
|
|
1798
|
+
* {@link Scheduler.get}. A clean public mirror of `@lunora/scheduler`'s
|
|
1799
|
+
* `ScheduleRecord`, re-declared so the public ctx surface names its own type
|
|
1800
|
+
* rather than re-exporting the scheduler's. It must stay field-for-field
|
|
1801
|
+
* identical: `__tests__/scheduler-mirror.test.ts` asserts mutual assignability
|
|
1802
|
+
* against the real `ScheduleRecord` and fails `lint:types` if either side moves.
|
|
1803
|
+
*/
|
|
1804
|
+
interface RetryPolicy {
|
|
1805
|
+
/** Backoff growth across attempts. Default `"exponential"`. */
|
|
1806
|
+
backoff?: "exponential" | "linear";
|
|
1807
|
+
/** Base delay in milliseconds for the first retry. Default `30_000`. */
|
|
1808
|
+
baseMs?: number;
|
|
1809
|
+
/** Maximum number of dispatch attempts before dead-lettering. Default `5`. */
|
|
1810
|
+
maxAttempts?: number;
|
|
1811
|
+
/** Optional ceiling clamping the computed backoff delay. */
|
|
1812
|
+
maxMs?: number;
|
|
1813
|
+
}
|
|
674
1814
|
interface ScheduledJob {
|
|
675
1815
|
args: Record<string, unknown>;
|
|
676
1816
|
/** Number of dispatch attempts already made (absent until the first retry). */
|
|
677
1817
|
attempts?: number;
|
|
678
1818
|
/** When the job was enqueued (epoch ms). */
|
|
679
1819
|
enqueuedAt: number;
|
|
680
|
-
|
|
1820
|
+
/**
|
|
1821
|
+
* The `ns:fn` path of the function to dispatch on fire. Absent when the job
|
|
1822
|
+
* targets a durable workflow/agent instead — see {@link ScheduledJob.workflow}.
|
|
1823
|
+
* Exactly one of `functionPath` / `workflow` is set.
|
|
1824
|
+
*/
|
|
1825
|
+
functionPath?: string;
|
|
681
1826
|
id: string;
|
|
1827
|
+
/** Scheduler/workpool instance the job was enqueued through. Absent for the default instance. */
|
|
1828
|
+
instanceName?: string;
|
|
1829
|
+
/** Logical workpool the job is concurrency-gated by. Absent for plain `runAfter`/`runAt` jobs. */
|
|
1830
|
+
pool?: string;
|
|
1831
|
+
/** Per-job retry policy; absent means the scheduler's built-in defaults. */
|
|
1832
|
+
retry?: RetryPolicy;
|
|
682
1833
|
/** When the job is scheduled to fire (epoch ms). */
|
|
683
1834
|
scheduledFor: number;
|
|
684
1835
|
/** Routing hint forwarded so dispatch lands on the right shard. */
|
|
685
1836
|
shardKey?: string;
|
|
1837
|
+
/**
|
|
1838
|
+
* The workflow/agent export key a fresh durable instance is started
|
|
1839
|
+
* from on fire (the {@link ScheduledJob.args} become its `params`). Set
|
|
1840
|
+
* instead of {@link ScheduledJob.functionPath}.
|
|
1841
|
+
*/
|
|
1842
|
+
workflow?: string;
|
|
686
1843
|
}
|
|
1844
|
+
/**
|
|
1845
|
+
* A schedulable durable-workflow reference — the generated `workflows.<name>` /
|
|
1846
|
+
* `agents.<name>` object, which carries its export key (the generated class name) and
|
|
1847
|
+
* stable name. Structural mirror of `@lunora/scheduler`'s `WorkflowReference` so
|
|
1848
|
+
* `ctx.scheduler` can target a workflow/agent without a dependency on
|
|
1849
|
+
* `@lunora/scheduler` / `@lunora/workflow`. A scheduled workflow target starts a
|
|
1850
|
+
* fresh instance on fire (the args become its `params`).
|
|
1851
|
+
*/
|
|
1852
|
+
interface SchedulableWorkflowReference {
|
|
1853
|
+
/** The workflow/agent export key — its generated class name (present on a generated ref). */
|
|
1854
|
+
readonly className?: string;
|
|
1855
|
+
readonly isLunoraWorkflow: true;
|
|
1856
|
+
/** The workflow/agent export/stable name (present on a generated ref). */
|
|
1857
|
+
readonly name?: string;
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* What `ctx.scheduler.runAfter` / `runAt` accept as a target:
|
|
1861
|
+
*
|
|
1862
|
+
* - a generated `internal.<file>.<fn>` / `api.<file>.<fn>` reference to a
|
|
1863
|
+
* mutation or action — the form the docs and the setup skills use, and the one
|
|
1864
|
+
* `@lunora/scheduler` has always resolved at runtime (it reads `__lunoraRef`);
|
|
1865
|
+
* - the equivalent `"file:fn"` path string;
|
|
1866
|
+
* - a generated `workflows.<name>` / `agents.<name>` reference, which starts a
|
|
1867
|
+
* fresh durable instance on fire.
|
|
1868
|
+
*
|
|
1869
|
+
* A `query` is not schedulable — a deferred job exists to have an effect.
|
|
1870
|
+
*/
|
|
1871
|
+
type SchedulableTarget = FunctionHandle<"action" | "mutation", unknown, unknown> | SchedulableWorkflowReference | string;
|
|
687
1872
|
interface Scheduler {
|
|
688
1873
|
/** Cancel a pending job by id. `cancelled` is `false` when no such job exists. */
|
|
689
1874
|
cancel: (id: string) => Promise<{
|
|
@@ -693,17 +1878,23 @@ interface Scheduler {
|
|
|
693
1878
|
get: (id: string) => Promise<ScheduledJob | null>;
|
|
694
1879
|
/** List all pending scheduled jobs. */
|
|
695
1880
|
list: () => Promise<ScheduledJob[]>;
|
|
696
|
-
|
|
697
|
-
|
|
1881
|
+
/**
|
|
1882
|
+
* Schedule a one-shot run `delayMs` from now; see {@link SchedulableTarget}
|
|
1883
|
+
* for the accepted targets. A workflow/agent reference starts a fresh
|
|
1884
|
+
* durable instance on fire (the args become its `params`).
|
|
1885
|
+
*/
|
|
1886
|
+
runAfter: (delayMs: number, target: SchedulableTarget, args?: Record<string, unknown>) => Promise<string>;
|
|
1887
|
+
/** Like {@link Scheduler.runAfter} but fires at an absolute epoch-ms timestamp. */
|
|
1888
|
+
runAt: (timestampMs: number, target: SchedulableTarget, args?: Record<string, unknown>) => Promise<string>;
|
|
698
1889
|
}
|
|
699
1890
|
/**
|
|
700
|
-
* A workflow instance's lifecycle status. Clean public mirror of
|
|
701
|
-
* `@lunora/workflow`'s `WorkflowInstanceStatus` (itself a mirror of Cloudflare's
|
|
702
|
-
* `WorkflowInstanceStatus`) — re-declared here so the ctx surface carries no
|
|
703
|
-
* dependency on the workflow package, exactly as {@link Scheduler} avoids a
|
|
704
|
-
* dependency on `@lunora/scheduler`.
|
|
705
|
-
*/
|
|
706
|
-
type WorkflowInstanceStatus = "complete" | "errored" | "paused" | "queued" | "running" | "terminated" | "unknown" | "waiting" | "waitingForPause";
|
|
1891
|
+
* A workflow instance's lifecycle status. Clean public mirror of
|
|
1892
|
+
* `@lunora/workflow`'s `WorkflowInstanceStatus` (itself a mirror of Cloudflare's
|
|
1893
|
+
* `WorkflowInstanceStatus`) — re-declared here so the ctx surface carries no
|
|
1894
|
+
* dependency on the workflow package, exactly as {@link Scheduler} avoids a
|
|
1895
|
+
* dependency on `@lunora/scheduler`.
|
|
1896
|
+
*/
|
|
1897
|
+
type WorkflowInstanceStatus = "complete" | "errored" | "paused" | "queued" | "rollingBack" | "running" | "terminated" | "unknown" | "waiting" | "waitingForPause";
|
|
707
1898
|
/** Result of {@link WorkflowInstance.status}. Mirrors `@lunora/workflow`'s `WorkflowStatusResult`. */
|
|
708
1899
|
interface WorkflowStatusResult {
|
|
709
1900
|
error?: {
|
|
@@ -713,20 +1904,62 @@ interface WorkflowStatusResult {
|
|
|
713
1904
|
output?: unknown;
|
|
714
1905
|
status: WorkflowInstanceStatus;
|
|
715
1906
|
}
|
|
1907
|
+
/** Options accepted by {@link WorkflowHandle.createBatch}: explicit `instances`, or `count` sharing one `params`/`retention`. Mirrors `@lunora/workflow`'s `WorkflowCreateBatchOptions`. */
|
|
1908
|
+
type WorkflowCreateBatchOptions<Params = Record<string, unknown>> = {
|
|
1909
|
+
instances: ReadonlyArray<WorkflowCreateOptions<Params>>;
|
|
1910
|
+
} | {
|
|
1911
|
+
count: number;
|
|
1912
|
+
params?: Params;
|
|
1913
|
+
retention?: {
|
|
1914
|
+
errorRetention?: string;
|
|
1915
|
+
successRetention?: string;
|
|
1916
|
+
};
|
|
1917
|
+
};
|
|
1918
|
+
/** One instance a batch did not create; `index` is its input position. Mirrors `@lunora/workflow`'s `WorkflowCreateBatchError`. */
|
|
1919
|
+
interface WorkflowCreateBatchError {
|
|
1920
|
+
code: number;
|
|
1921
|
+
id?: string;
|
|
1922
|
+
index: number;
|
|
1923
|
+
message: string;
|
|
1924
|
+
}
|
|
1925
|
+
/** Result of {@link WorkflowHandle.createBatch}. Mirrors `@lunora/workflow`'s `WorkflowCreateBatchResult`. */
|
|
1926
|
+
interface WorkflowCreateBatchResult {
|
|
1927
|
+
created: WorkflowInstance[];
|
|
1928
|
+
errors: WorkflowCreateBatchError[];
|
|
1929
|
+
}
|
|
716
1930
|
/** Options accepted by {@link WorkflowHandle.create}. Mirrors `@lunora/workflow`'s `WorkflowCreateOptions`. */
|
|
717
1931
|
interface WorkflowCreateOptions<Params = Record<string, unknown>> {
|
|
718
1932
|
/** Unique-within-the-workflow instance id. Generated by Cloudflare when omitted. */
|
|
719
1933
|
id?: string;
|
|
720
1934
|
/** The event payload the instance is triggered with — surfaced as `event.payload`. */
|
|
721
1935
|
params?: Params;
|
|
722
|
-
/** Instance retention policy (defaults to the
|
|
1936
|
+
/** Instance retention policy (defaults to the workflow's `defaultRetention`, else the plan default). */
|
|
723
1937
|
retention?: {
|
|
724
1938
|
errorRetention?: string;
|
|
725
1939
|
successRetention?: string;
|
|
726
1940
|
};
|
|
727
1941
|
}
|
|
1942
|
+
/**
|
|
1943
|
+
* One declared external event a workflow waits on. Mirrors `@lunora/workflow`'s
|
|
1944
|
+
* `WorkflowEventDefinition` — the value `defineWorkflowEvent` returns, taken by
|
|
1945
|
+
* both {@link WorkflowInstance.sendEvent} and the workflow's `ctx.waitForEvent`.
|
|
1946
|
+
*/
|
|
1947
|
+
interface WorkflowEventDefinition<Payload = unknown> {
|
|
1948
|
+
readonly isLunoraWorkflowEvent: true;
|
|
1949
|
+
readonly payload: Validator<Payload>;
|
|
1950
|
+
readonly type: string;
|
|
1951
|
+
}
|
|
1952
|
+
/** One event `WorkflowInstance.subscribe()` streams. Mirrors `@lunora/workflow`'s `WorkflowInstanceEventLike`. */
|
|
1953
|
+
interface WorkflowInstanceEvent {
|
|
1954
|
+
[field: string]: unknown;
|
|
1955
|
+
eventId: number;
|
|
1956
|
+
instanceId: string;
|
|
1957
|
+
timestamp: number;
|
|
1958
|
+
type: string;
|
|
1959
|
+
}
|
|
728
1960
|
/** A live handle to a single workflow instance. Mirrors `@lunora/workflow`'s `WorkflowInstanceLike`. */
|
|
729
1961
|
interface WorkflowInstance {
|
|
1962
|
+
delete: () => Promise<void>;
|
|
730
1963
|
readonly id: string;
|
|
731
1964
|
pause: () => Promise<void>;
|
|
732
1965
|
restart: () => Promise<void>;
|
|
@@ -736,66 +1969,86 @@ interface WorkflowInstance {
|
|
|
736
1969
|
type: string;
|
|
737
1970
|
}) => Promise<void>;
|
|
738
1971
|
status: () => Promise<WorkflowStatusResult>;
|
|
1972
|
+
/** Stream the instance's events — its full history first, then new ones as it runs. */
|
|
1973
|
+
subscribe: (options?: {
|
|
1974
|
+
cursor?: number;
|
|
1975
|
+
filter?: string[];
|
|
1976
|
+
}) => Promise<{
|
|
1977
|
+
next: () => Promise<IteratorResult<WorkflowInstanceEvent, void>>;
|
|
1978
|
+
}>;
|
|
739
1979
|
terminate: () => Promise<void>;
|
|
740
1980
|
}
|
|
741
1981
|
/**
|
|
742
|
-
* A typed handle to one declared workflow, addressable from `ctx.workflows`.
|
|
743
|
-
* Mirrors `@lunora/workflow`'s `WorkflowHandle`.
|
|
744
|
-
*/
|
|
1982
|
+
* A typed handle to one declared workflow, addressable from `ctx.workflows`.
|
|
1983
|
+
* Mirrors `@lunora/workflow`'s `WorkflowHandle`.
|
|
1984
|
+
*/
|
|
745
1985
|
interface WorkflowHandle<Params = Record<string, unknown>> {
|
|
746
1986
|
/** Start a new instance (optionally with an id + params). */
|
|
747
1987
|
create: (options?: WorkflowCreateOptions<Params>) => Promise<WorkflowInstance>;
|
|
748
|
-
/** Start many instances in one batched RPC. */
|
|
749
|
-
createBatch: (
|
|
1988
|
+
/** Start many instances in one batched RPC; per-instance failures come back in `errors`, not dropped. */
|
|
1989
|
+
createBatch: (options: WorkflowCreateBatchOptions<Params>) => Promise<WorkflowCreateBatchResult>;
|
|
1990
|
+
/** Delete up to 100 instances and their stored state; ids that do not exist come back as per-instance errors. */
|
|
1991
|
+
deleteBatch: (instanceIds: ReadonlyArray<string>) => Promise<{
|
|
1992
|
+
deleted: {
|
|
1993
|
+
id: string;
|
|
1994
|
+
}[];
|
|
1995
|
+
errors: {
|
|
1996
|
+
code: number;
|
|
1997
|
+
id: string;
|
|
1998
|
+
message: string;
|
|
1999
|
+
}[];
|
|
2000
|
+
}>;
|
|
750
2001
|
/** Get a handle to an existing instance by id. */
|
|
751
2002
|
get: (id: string) => Promise<WorkflowInstance>;
|
|
2003
|
+
/** Deliver a declared event (`defineWorkflowEvent`) to one instance; the payload is validated before the send. */
|
|
2004
|
+
sendEvent: <Payload>(instanceId: string, event: WorkflowEventDefinition<Payload>, payload: Payload) => Promise<void>;
|
|
752
2005
|
}
|
|
753
2006
|
/**
|
|
754
|
-
* The `ctx.workflows` surface on {@link MutationCtx} / {@link ActionCtx}. Each
|
|
755
|
-
* workflow declared in `lunora/workflows.ts` is reachable by its export name;
|
|
756
|
-
* codegen narrows the `get(name)` overloads to the known workflow names + their
|
|
757
|
-
* inferred param types. Mirrors `@lunora/workflow`'s `Workflows`.
|
|
758
|
-
*/
|
|
2007
|
+
* The `ctx.workflows` surface on {@link MutationCtx} / {@link ActionCtx}. Each
|
|
2008
|
+
* workflow declared in `lunora/workflows.ts` is reachable by its export name;
|
|
2009
|
+
* codegen narrows the `get(name)` overloads to the known workflow names + their
|
|
2010
|
+
* inferred param types. Mirrors `@lunora/workflow`'s `Workflows`.
|
|
2011
|
+
*/
|
|
759
2012
|
interface Workflows {
|
|
760
2013
|
/** Resolve the handle for a declared workflow by export name. */
|
|
761
2014
|
get: <Params = Record<string, unknown>>(name: string) => WorkflowHandle<Params>;
|
|
762
2015
|
}
|
|
763
2016
|
/**
|
|
764
|
-
* Programmatic cache purge surface exposed on {@link ActionCtx}. Actions run
|
|
765
|
-
* in the Worker (not the DO), so they can reach the Worker's `ctx.cache.purge`.
|
|
766
|
-
* Queries and mutations do not expose this — they run inside the Durable Object.
|
|
767
|
-
*/
|
|
2017
|
+
* Programmatic cache purge surface exposed on {@link ActionCtx}. Actions run
|
|
2018
|
+
* in the Worker (not the DO), so they can reach the Worker's `ctx.cache.purge`.
|
|
2019
|
+
* Queries and mutations do not expose this — they run inside the Durable Object.
|
|
2020
|
+
*/
|
|
768
2021
|
interface CachePurge {
|
|
769
2022
|
/**
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
2023
|
+
* Purge cached responses matching the given tags, or everything when
|
|
2024
|
+
* `purgeEverything` is true. Only available in action handlers.
|
|
2025
|
+
*/
|
|
773
2026
|
purge: (options: {
|
|
774
2027
|
purgeEverything?: boolean;
|
|
775
2028
|
tags?: string[];
|
|
776
2029
|
}) => Promise<unknown>;
|
|
777
2030
|
}
|
|
778
2031
|
/**
|
|
779
|
-
* Structural projection of workers-types' `SecretsStoreSecret` binding — the
|
|
780
|
-
* per-secret `secrets_store_secrets[]` binding whose `.get()` resolves the
|
|
781
|
-
* secret value (or throws if it does not exist). Mirrored structurally so the
|
|
782
|
-
* runtime resolves it without a workerd type dependency.
|
|
783
|
-
*/
|
|
2032
|
+
* Structural projection of workers-types' `SecretsStoreSecret` binding — the
|
|
2033
|
+
* per-secret `secrets_store_secrets[]` binding whose `.get()` resolves the
|
|
2034
|
+
* secret value (or throws if it does not exist). Mirrored structurally so the
|
|
2035
|
+
* runtime resolves it without a workerd type dependency.
|
|
2036
|
+
*/
|
|
784
2037
|
interface SecretsStoreSecretLike {
|
|
785
2038
|
get: () => Promise<string>;
|
|
786
2039
|
}
|
|
787
2040
|
/**
|
|
788
|
-
* `ctx.secrets` — read account-level secrets bound via Cloudflare Secrets Store.
|
|
789
|
-
* A core built-in (always present on every context, like `ctx.log`): a binding
|
|
790
|
-
* named in wrangler's `secrets_store_secrets[]` is read by its binding name.
|
|
791
|
-
*
|
|
792
|
-
* ```ts
|
|
793
|
-
* const apiKey = await ctx.secrets.get("STRIPE_KEY");
|
|
794
|
-
* ```
|
|
795
|
-
*
|
|
796
|
-
* The lookup is async (the platform fetches and decrypts on first read);
|
|
797
|
-
* reading an undeclared name throws a directed error naming the bound secrets.
|
|
798
|
-
*/
|
|
2041
|
+
* `ctx.secrets` — read account-level secrets bound via Cloudflare Secrets Store.
|
|
2042
|
+
* A core built-in (always present on every context, like `ctx.log`): a binding
|
|
2043
|
+
* named in wrangler's `secrets_store_secrets[]` is read by its binding name.
|
|
2044
|
+
*
|
|
2045
|
+
* ```ts
|
|
2046
|
+
* const apiKey = await ctx.secrets.get("STRIPE_KEY");
|
|
2047
|
+
* ```
|
|
2048
|
+
*
|
|
2049
|
+
* The lookup is async (the platform fetches and decrypts on first read);
|
|
2050
|
+
* reading an undeclared name throws a directed error naming the bound secrets.
|
|
2051
|
+
*/
|
|
799
2052
|
interface Secrets {
|
|
800
2053
|
/** Resolve a Secrets Store secret by its wrangler binding name. */
|
|
801
2054
|
get: (name: string) => Promise<string>;
|
|
@@ -805,11 +2058,11 @@ type TriggerTiming = "after" | "before";
|
|
|
805
2058
|
/** The CRUD operation a trigger reacts to. `patch` and `replace` both map to `update`. */
|
|
806
2059
|
type TriggerOp = "delete" | "insert" | "update";
|
|
807
2060
|
/**
|
|
808
|
-
* A row as observed by a trigger handler: the table's `Shape` (with the same
|
|
809
|
-
* optionality rules as {@link InferArgs}) plus the system columns every stored
|
|
810
|
-
* doc carries.
|
|
811
|
-
*/
|
|
812
|
-
type TriggerRow<Shape extends Record<string, Validator>> = { [K in keyof Shape as undefined extends Infer<Shape[K]> ? K : never]?: Infer<Shape[K]
|
|
2061
|
+
* A row as observed by a trigger handler: the table's `Shape` (with the same
|
|
2062
|
+
* optionality rules as {@link InferArgs}) plus the system columns every stored
|
|
2063
|
+
* doc carries.
|
|
2064
|
+
*/
|
|
2065
|
+
type TriggerRow<Shape extends Record<string, Validator>> = { [K in keyof Shape as undefined extends Infer<Shape[K]> ? K : never]?: Infer<Shape[K]>; } & { [K in keyof Shape as undefined extends Infer<Shape[K]> ? never : K]: Infer<Shape[K]>; } & {
|
|
813
2066
|
readonly _creationTime: number;
|
|
814
2067
|
readonly _id: string;
|
|
815
2068
|
};
|
|
@@ -821,11 +2074,11 @@ interface TriggerInsertEvent<Shape extends Record<string, Validator> = Record<st
|
|
|
821
2074
|
readonly table: string;
|
|
822
2075
|
}
|
|
823
2076
|
/**
|
|
824
|
-
* What an `update` trigger observes: the merged row plus the pre-write row.
|
|
825
|
-
* `previous` is typed as always present (the row must exist to be updated); the
|
|
826
|
-
* runtime supplies it best-effort and only omits it in the unreachable
|
|
827
|
-
* row-vanished-mid-write case.
|
|
828
|
-
*/
|
|
2077
|
+
* What an `update` trigger observes: the merged row plus the pre-write row.
|
|
2078
|
+
* `previous` is typed as always present (the row must exist to be updated); the
|
|
2079
|
+
* runtime supplies it best-effort and only omits it in the unreachable
|
|
2080
|
+
* row-vanished-mid-write case.
|
|
2081
|
+
*/
|
|
829
2082
|
interface TriggerUpdateEvent<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
830
2083
|
readonly doc: TriggerRow<Shape>;
|
|
831
2084
|
readonly id: string;
|
|
@@ -834,10 +2087,10 @@ interface TriggerUpdateEvent<Shape extends Record<string, Validator> = Record<st
|
|
|
834
2087
|
readonly table: string;
|
|
835
2088
|
}
|
|
836
2089
|
/**
|
|
837
|
-
* What a `delete` trigger observes: the row about to be (or just) removed.
|
|
838
|
-
* `previous` is typed as always present; the runtime supplies it best-effort
|
|
839
|
-
* and only omits it in the unreachable row-vanished-mid-write case.
|
|
840
|
-
*/
|
|
2090
|
+
* What a `delete` trigger observes: the row about to be (or just) removed.
|
|
2091
|
+
* `previous` is typed as always present; the runtime supplies it best-effort
|
|
2092
|
+
* and only omits it in the unreachable row-vanished-mid-write case.
|
|
2093
|
+
*/
|
|
841
2094
|
interface TriggerDeleteEvent<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
842
2095
|
readonly id: string;
|
|
843
2096
|
readonly op: "delete";
|
|
@@ -861,10 +2114,10 @@ interface TriggerQueryArgs {
|
|
|
861
2114
|
with?: Record<string, unknown>;
|
|
862
2115
|
}
|
|
863
2116
|
/**
|
|
864
|
-
* Args accepted by {@link TriggerDatabase.aggregate} — structural mirror of
|
|
865
|
-
* `@lunora/do`'s `AggregateOptions`, kept local so trigger handlers in
|
|
866
|
-
* `@lunora/server` don't take a hard dep on the DO runtime.
|
|
867
|
-
*/
|
|
2117
|
+
* Args accepted by {@link TriggerDatabase.aggregate} — structural mirror of
|
|
2118
|
+
* `@lunora/do`'s `AggregateOptions`, kept local so trigger handlers in
|
|
2119
|
+
* `@lunora/server` don't take a hard dep on the DO runtime.
|
|
2120
|
+
*/
|
|
868
2121
|
interface TriggerAggregateOptions {
|
|
869
2122
|
baseWhere?: Record<string, unknown>;
|
|
870
2123
|
field?: string;
|
|
@@ -909,17 +2162,17 @@ interface TriggerRankPageOptions {
|
|
|
909
2162
|
where?: Record<string, unknown>;
|
|
910
2163
|
}
|
|
911
2164
|
/**
|
|
912
|
-
* Portable, table/id-addressed ORM writer handed to trigger handlers via
|
|
913
|
-
* `ctx.db`. Mirrors `@lunora/do`'s runtime `DatabaseWriterLike` surface — it is
|
|
914
|
-
* **not** the generated per-table `ctx.db
|
|
915
|
-
* from inside `defineTable`, where the full schema isn't known).
|
|
916
|
-
*
|
|
917
|
-
* `aggregate`/`groupBy`/`count`/`rank`/`rankPage` route through the same
|
|
918
|
-
* trigger-maintained counter and rank tables the user-facing reader uses, so
|
|
919
|
-
* a handler's `ctx.db
|
|
920
|
-
* within the same DO transaction (the counter step happens before the trigger
|
|
921
|
-
* fires).
|
|
922
|
-
*/
|
|
2165
|
+
* Portable, table/id-addressed ORM writer handed to trigger handlers via
|
|
2166
|
+
* `ctx.db`. Mirrors `@lunora/do`'s runtime `DatabaseWriterLike` surface — it is
|
|
2167
|
+
* **not** the generated per-table `ctx.db.<table>` facade (which can't be typed
|
|
2168
|
+
* from inside `defineTable`, where the full schema isn't known).
|
|
2169
|
+
*
|
|
2170
|
+
* `aggregate`/`groupBy`/`count`/`rank`/`rankPage` route through the same
|
|
2171
|
+
* trigger-maintained counter and rank tables the user-facing reader uses, so
|
|
2172
|
+
* a handler's `ctx.db.<table>.aggregate(...)` observes the just-staged write
|
|
2173
|
+
* within the same DO transaction (the counter step happens before the trigger
|
|
2174
|
+
* fires).
|
|
2175
|
+
*/
|
|
923
2176
|
interface TriggerDatabase {
|
|
924
2177
|
aggregate: (tableName: string, options: TriggerAggregateOptions) => Promise<null | number>;
|
|
925
2178
|
count: (tableName: string, where?: Record<string, unknown>) => Promise<number>;
|
|
@@ -935,10 +2188,10 @@ interface TriggerDatabase {
|
|
|
935
2188
|
replace: (id: string, document: Record<string, unknown>) => Promise<void>;
|
|
936
2189
|
}
|
|
937
2190
|
/**
|
|
938
|
-
* Handle injected into every trigger handler. `db` is the portable ORM writer;
|
|
939
|
-
* `scheduler` enqueues async / cross-shard follow-up work (cross-shard work is
|
|
940
|
-
* **not** transactional with the firing write).
|
|
941
|
-
*/
|
|
2191
|
+
* Handle injected into every trigger handler. `db` is the portable ORM writer;
|
|
2192
|
+
* `scheduler` enqueues async / cross-shard follow-up work (cross-shard work is
|
|
2193
|
+
* **not** transactional with the firing write).
|
|
2194
|
+
*/
|
|
942
2195
|
interface TriggerCtx {
|
|
943
2196
|
readonly db: TriggerDatabase;
|
|
944
2197
|
readonly scheduler: Scheduler;
|
|
@@ -946,20 +2199,20 @@ interface TriggerCtx {
|
|
|
946
2199
|
/** A user-declared trigger handler. Throwing from a `before*` handler aborts the write. */
|
|
947
2200
|
type TriggerHandler<Event> = (context: TriggerCtx, event: Event) => Promise<void> | void;
|
|
948
2201
|
/**
|
|
949
|
-
* A single declared trigger, as stored in {@link TableDefinition.triggerMap}.
|
|
950
|
-
* The handler's event type is erased to the {@link TriggerEvent} union here; the
|
|
951
|
-
* per-op {@link TriggerBuilder} methods recover the precise event type for
|
|
952
|
-
* authors.
|
|
953
|
-
*/
|
|
2202
|
+
* A single declared trigger, as stored in {@link TableDefinition.triggerMap}.
|
|
2203
|
+
* The handler's event type is erased to the {@link TriggerEvent} union here; the
|
|
2204
|
+
* per-op {@link TriggerBuilder} methods recover the precise event type for
|
|
2205
|
+
* authors.
|
|
2206
|
+
*/
|
|
954
2207
|
interface TriggerDefinition {
|
|
955
2208
|
readonly handler: TriggerHandler<TriggerEvent>;
|
|
956
2209
|
readonly op: TriggerOp;
|
|
957
2210
|
readonly timing: TriggerTiming;
|
|
958
2211
|
}
|
|
959
2212
|
/**
|
|
960
|
-
* The `t` argument passed to `.triggers((t) => …)`. Each method binds a handler
|
|
961
|
-
* to one `timing`+`op` pair, typing the event against the table's `Shape`.
|
|
962
|
-
*/
|
|
2213
|
+
* The `t` argument passed to `.triggers((t) => …)`. Each method binds a handler
|
|
2214
|
+
* to one `timing`+`op` pair, typing the event against the table's `Shape`.
|
|
2215
|
+
*/
|
|
963
2216
|
interface TriggerBuilder<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
964
2217
|
afterDelete: (handler: TriggerHandler<TriggerDeleteEvent<Shape>>) => TriggerDefinition;
|
|
965
2218
|
afterInsert: (handler: TriggerHandler<TriggerInsertEvent<Shape>>) => TriggerDefinition;
|
|
@@ -968,89 +2221,6 @@ interface TriggerBuilder<Shape extends Record<string, Validator> = Record<string
|
|
|
968
2221
|
beforeInsert: (handler: TriggerHandler<TriggerInsertEvent<Shape>>) => TriggerDefinition;
|
|
969
2222
|
beforeUpdate: (handler: TriggerHandler<TriggerUpdateEvent<Shape>>) => TriggerDefinition;
|
|
970
2223
|
}
|
|
971
|
-
/**
|
|
972
|
-
* Per-file metadata returned by {@link ReadOnlyStorage.getMetadata}. A clean
|
|
973
|
-
* public mirror of `@lunora/storage`'s `ObjectMetadata` — re-declared here so
|
|
974
|
-
* the ctx surface carries no dependency on the storage package's types. Matches
|
|
975
|
-
* the columns Convex surfaces for `ctx.storage.getMetadata` / `_storage`.
|
|
976
|
-
*/
|
|
977
|
-
interface StorageMetadata {
|
|
978
|
-
/** The object's `Content-Type`, when recorded. */
|
|
979
|
-
contentType?: string;
|
|
980
|
-
/** Custom metadata set at upload time, if any. */
|
|
981
|
-
customMetadata?: Record<string, string>;
|
|
982
|
-
/** The object's key. */
|
|
983
|
-
key: string;
|
|
984
|
-
/** Hex-encoded SHA-256 of the body, when R2 carries a checksum. */
|
|
985
|
-
sha256?: string;
|
|
986
|
-
/** Body length in bytes. */
|
|
987
|
-
size: number;
|
|
988
|
-
/** When the object was last written (epoch ms), when reported. */
|
|
989
|
-
uploaded?: number;
|
|
990
|
-
}
|
|
991
|
-
/**
|
|
992
|
-
* Read-only projection of `Storage` exposed on `QueryCtx` / `MutationCtx`.
|
|
993
|
-
*
|
|
994
|
-
* Queries are pure reads, and mutations run inside a transactional scope —
|
|
995
|
-
* neither is allowed to perform side-effectful R2 writes (`upload`) or
|
|
996
|
-
* deletes (`delete`). They can, however, **read** existing objects and
|
|
997
|
-
* resolve signed URLs (the URL signing itself is HMAC-only — no R2 round
|
|
998
|
-
* trip), so the read-only surface keeps `download` and `getSignedUrl`. The
|
|
999
|
-
* full {@link Storage} surface stays on `ActionCtx`.
|
|
1000
|
-
*/
|
|
1001
|
-
interface ReadOnlyStorage<Buckets extends string = string> {
|
|
1002
|
-
/**
|
|
1003
|
-
* Select a named bucket (declared via `v.storage("name")`). The returned
|
|
1004
|
-
* accessor's operations target that bucket — `ctx.storage.bucket("avatars")
|
|
1005
|
-
* .download(key)`. The bare `ctx.storage` targets the default bucket.
|
|
1006
|
-
*/
|
|
1007
|
-
bucket: (name: Buckets) => ReadOnlyStorage<Buckets>;
|
|
1008
|
-
/** The bucket this accessor's operations target (the default for the bare `ctx.storage`). */
|
|
1009
|
-
readonly bucketName: string;
|
|
1010
|
-
/** Fetch the body of an existing object. Returns `null` when absent. */
|
|
1011
|
-
download: (key: string) => Promise<ReadableStream | null>;
|
|
1012
|
-
/**
|
|
1013
|
-
* Read a file's metadata (size, content-type, sha256, upload time, custom
|
|
1014
|
-
* metadata) without fetching its body. Returns `null` when the object is
|
|
1015
|
-
* absent. Mirrors Convex's `ctx.storage.getMetadata`.
|
|
1016
|
-
*/
|
|
1017
|
-
getMetadata: (key: string) => Promise<StorageMetadata | null>;
|
|
1018
|
-
/** Resolve a short-lived signed URL for an existing object. */
|
|
1019
|
-
getSignedUrl: (key: string, options?: {
|
|
1020
|
-
expiresInSeconds?: number;
|
|
1021
|
-
}) => Promise<string>;
|
|
1022
|
-
/** Public URL pointing at the configured base for `key`. */
|
|
1023
|
-
getUrl: (key: string) => string;
|
|
1024
|
-
}
|
|
1025
|
-
interface Storage<Buckets extends string = string> extends ReadOnlyStorage<Buckets> {
|
|
1026
|
-
/** Select a named bucket; the returned accessor exposes the full read/write surface. */
|
|
1027
|
-
bucket: (name: Buckets) => Storage<Buckets>;
|
|
1028
|
-
delete: (key: string) => Promise<void>;
|
|
1029
|
-
/**
|
|
1030
|
-
* Mint a short-lived signed `PUT` URL a client can upload directly to,
|
|
1031
|
-
* optionally pinning the `Content-Type` the uploader must send. Mirrors
|
|
1032
|
-
* Convex's `storage.generateUploadUrl`.
|
|
1033
|
-
*/
|
|
1034
|
-
generateUploadUrl: (key: string, options?: {
|
|
1035
|
-
contentType?: string;
|
|
1036
|
-
expiresInSeconds?: number;
|
|
1037
|
-
}) => Promise<string>;
|
|
1038
|
-
/**
|
|
1039
|
-
* Upload `body` to `key` from the server, returning the stored object's key
|
|
1040
|
-
* and etag. Mirrors Convex's `storage.store`. Accepts the same guard fields
|
|
1041
|
-
* as `@lunora/storage`'s `UploadOptions` so `maxSize` /
|
|
1042
|
-
* `allowedContentTypes` enforcement isn't lost behind the Convex-style alias.
|
|
1043
|
-
*/
|
|
1044
|
-
store: (key: string, body: ReadableStream | ArrayBuffer | Blob, options?: {
|
|
1045
|
-
allowedContentTypes?: ReadonlyArray<string>;
|
|
1046
|
-
contentType?: string;
|
|
1047
|
-
customMetadata?: Record<string, string>;
|
|
1048
|
-
maxSize?: number;
|
|
1049
|
-
}) => Promise<{
|
|
1050
|
-
etag: string;
|
|
1051
|
-
key: string;
|
|
1052
|
-
}>;
|
|
1053
|
-
}
|
|
1054
2224
|
interface VectorMatch {
|
|
1055
2225
|
id: string;
|
|
1056
2226
|
metadata?: Record<string, unknown>;
|
|
@@ -1084,138 +2254,538 @@ interface VectorRecord {
|
|
|
1084
2254
|
values: ReadonlyArray<number>;
|
|
1085
2255
|
}
|
|
1086
2256
|
/**
|
|
1087
|
-
* Read-only vector surface exposed on {@link QueryCtx}. Mirrors the read half
|
|
1088
|
-
* of `@lunora/bindings/vectors`' `LunoraVectors` so the live adapter is assignable.
|
|
1089
|
-
*/
|
|
1090
|
-
interface VectorSearchReader {
|
|
1091
|
-
getByIds: (indexName:
|
|
1092
|
-
query: (indexName:
|
|
1093
|
-
}
|
|
1094
|
-
/**
|
|
1095
|
-
* Mutating vector surface on {@link MutationCtx} / {@link ActionCtx}. `upsert`
|
|
1096
|
-
* is queued post-commit by default; `upsertNow` forces a synchronous write.
|
|
1097
|
-
* `db.delete` on a vectorized table auto-propagates the matching `deleteByIds`.
|
|
1098
|
-
*/
|
|
1099
|
-
interface VectorSearch extends VectorSearchReader {
|
|
1100
|
-
deleteByIds
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
/**
|
|
1105
|
-
*
|
|
1106
|
-
*
|
|
1107
|
-
* `
|
|
1108
|
-
*
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
1120
|
-
|
|
1121
|
-
|
|
2257
|
+
* Read-only vector surface exposed on {@link QueryCtx}. Mirrors the read half
|
|
2258
|
+
* of `@lunora/bindings/vectors`' `LunoraVectors` so the live adapter is assignable.
|
|
2259
|
+
*/
|
|
2260
|
+
interface VectorSearchReader<IndexName extends string = string> {
|
|
2261
|
+
getByIds: (indexName: IndexName, ids: ReadonlyArray<string>) => Promise<ReadonlyArray<VectorRecord>>;
|
|
2262
|
+
query: (indexName: IndexName, input: VectorQueryInput) => Promise<VectorMatches>;
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Mutating vector surface on {@link MutationCtx} / {@link ActionCtx}. `upsert`
|
|
2266
|
+
* is queued post-commit by default; `upsertNow` forces a synchronous write.
|
|
2267
|
+
* `db.delete` on a vectorized table auto-propagates the matching `deleteByIds`.
|
|
2268
|
+
*/
|
|
2269
|
+
interface VectorSearch<IndexName extends string = string> extends VectorSearchReader<IndexName> {
|
|
2270
|
+
/** Set when `deleteByIds` honours its `operation` and `afterDelete` arguments. */
|
|
2271
|
+
deferredDelete?: boolean;
|
|
2272
|
+
deleteByIds: (indexName: IndexName, ids: ReadonlyArray<string>, namespace?: string, operation?: object, afterDelete?: () => Promise<void>) => Promise<void>;
|
|
2273
|
+
upsert: (indexName: IndexName, input: VectorUpsertInput, operation?: object) => Promise<void>;
|
|
2274
|
+
/**
|
|
2275
|
+
* Batch form of `upsert` (at most 1000 inputs, one remote call). Optional so
|
|
2276
|
+
* existing implementors keep compiling. The optional `operation` token (on
|
|
2277
|
+
* both `upsert` and `upsertMany`) groups deferred writes: once one fails,
|
|
2278
|
+
* later deferred writes carrying the same token are skipped.
|
|
2279
|
+
*/
|
|
2280
|
+
upsertMany?: (indexName: IndexName, inputs: ReadonlyArray<VectorUpsertInput>, operation?: object) => Promise<void>;
|
|
2281
|
+
upsertNow: (indexName: IndexName, input: VectorUpsertInput) => Promise<void>;
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Structured, filterable key/value fields attached to a log line — the second
|
|
2285
|
+
* argument of a `ctx.log.<level>(message, fields)` call, or the fields bound by
|
|
2286
|
+
* `ctx.log.with(fields)`. They travel to an `ObservabilitySink`'s `onLog` and,
|
|
2287
|
+
* for a network sink, become OTLP log-record attributes a log pipeline (or the
|
|
2288
|
+
* Cloud log viewer) can filter and index on. Primitive values pass through;
|
|
2289
|
+
* objects/arrays are JSON-encoded at the sink boundary.
|
|
2290
|
+
*/
|
|
2291
|
+
type LogFields = Record<string, unknown>;
|
|
2292
|
+
/**
|
|
2293
|
+
* One `ctx.log` severity method. Two call forms:
|
|
2294
|
+
*
|
|
2295
|
+
* - **Structured** — `ctx.log.info("order placed", { orderId, total })`: a message string plus a `fields` object. The fields are indexed as attributes.
|
|
2296
|
+
* - **Console-style** — `ctx.log.info("state", value, other)`: any number of values, joined into the display message exactly like `console.log`.
|
|
2297
|
+
*
|
|
2298
|
+
* The structured form is matched when the second argument is a plain object;
|
|
2299
|
+
* otherwise the call is treated as console-style, so existing `console`-shaped
|
|
2300
|
+
* calls keep working unchanged.
|
|
2301
|
+
*/
|
|
2302
|
+
interface LunoraLogMethod {
|
|
2303
|
+
(message: string, fields?: LogFields): void;
|
|
2304
|
+
(...args: unknown[]): void;
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Structured logger on every function `ctx`. Each call emits one attributed log
|
|
2308
|
+
* line — tagged with the function path on the server — that flows to an
|
|
2309
|
+
* `ObservabilitySink`'s `onLog` (where you route it in production) and, in
|
|
2310
|
+
* development, to the dev server terminal via the CLI / Vite plugin formatter.
|
|
2311
|
+
* Mirrors the `console` method names so it's a drop-in for `console.log` inside a
|
|
2312
|
+
* handler, but with attribution, structured fields, and a routable transport.
|
|
2313
|
+
*
|
|
2314
|
+
* Six severities spanning the OpenTelemetry ramp: `trace`, `debug`, `info` (and
|
|
2315
|
+
* its `log` alias), `warn`, `error`, `fatal`.
|
|
2316
|
+
*
|
|
2317
|
+
* Two ways to attach structured {@link LogFields}: pass them per call
|
|
2318
|
+
* (`ctx.log.info(message, fields)`) or bind them once with {@link with} for a
|
|
2319
|
+
* child logger that stamps every line. The rendered `message` and the structured
|
|
2320
|
+
* `fields` reach the dev terminal and the platform's Workers Logs; the raw,
|
|
2321
|
+
* un-rendered console-style arguments are preserved ONLY on the in-process
|
|
2322
|
+
* `onLog` sink (which you opt into and control).
|
|
2323
|
+
*
|
|
2324
|
+
* Attribution follows the dispatched function: a log emitted inside an internal
|
|
2325
|
+
* function invoked via `ctx.runQuery`/`runMutation`/`runAction` is attributed to
|
|
2326
|
+
* the outer request entrypoint, since the composed call reuses its context.
|
|
2327
|
+
*/
|
|
1122
2328
|
interface LunoraLogger {
|
|
1123
|
-
readonly debug:
|
|
1124
|
-
readonly error:
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
2329
|
+
readonly debug: LunoraLogMethod;
|
|
2330
|
+
readonly error: LunoraLogMethod;
|
|
2331
|
+
/**
|
|
2332
|
+
* Emit a **structured event** instead of a log line — OpenTelemetry's Events
|
|
2333
|
+
* API, on the wire as `LogRecord.eventName` (plus an `event.name` attribute
|
|
2334
|
+
* for collectors predating that field).
|
|
2335
|
+
*
|
|
2336
|
+
* ```ts
|
|
2337
|
+
* ctx.log.event("checkout.completed", { plan: user.plan, total, currency });
|
|
2338
|
+
* ```
|
|
2339
|
+
*
|
|
2340
|
+
* The difference from `ctx.log.info("checkout completed", { … })` is what a
|
|
2341
|
+
* backend can do with it. A log line's payload is its message: prose, written
|
|
2342
|
+
* for a human, free to be reworded next sprint — so "how many checkouts
|
|
2343
|
+
* completed, by plan, this hour" degrades into a substring search over
|
|
2344
|
+
* English. An event's payload is its `fields` under a **stable name**, which a
|
|
2345
|
+
* collector can index, group, and alert on directly.
|
|
2346
|
+
*
|
|
2347
|
+
* Rule of thumb: `log.*` for narration you'd read while debugging, `event` for
|
|
2348
|
+
* anything you'd ever put on a dashboard. And for facts about the request as a
|
|
2349
|
+
* whole, prefer `ctx.span` — one wide event beats a dozen
|
|
2350
|
+
* events, however well named.
|
|
2351
|
+
*/
|
|
2352
|
+
readonly event: (name: string, fields?: LogFields) => void;
|
|
2353
|
+
readonly fatal: LunoraLogMethod;
|
|
2354
|
+
readonly info: LunoraLogMethod;
|
|
2355
|
+
readonly log: LunoraLogMethod;
|
|
2356
|
+
readonly trace: LunoraLogMethod;
|
|
2357
|
+
readonly warn: LunoraLogMethod;
|
|
2358
|
+
/**
|
|
2359
|
+
* Return a child logger that stamps `fields` onto every line it emits,
|
|
2360
|
+
* merged under any per-call fields (per-call wins on a key clash). Chainable
|
|
2361
|
+
* — `ctx.log.with({ requestId }).with({ step })` accumulates both. Use it to
|
|
2362
|
+
* bind request-scoped context once instead of repeating it per call.
|
|
2363
|
+
*/
|
|
2364
|
+
readonly with: (fields: LogFields) => LunoraLogger;
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* Handle the enclosing `ctx.trace` span hands its body, so the body can attach
|
|
2368
|
+
* attributes only known *after* it resolves (an AI call's token usage / dollar
|
|
2369
|
+
* cost, a downstream status, a computed count). Declared structurally here to
|
|
2370
|
+
* mirror `shared/span-event.ts`'s `SpanHandle` and `@lunora/do`'s implementation;
|
|
2371
|
+
* a cross-package assignability guard in `@lunora/testing` fails the build if the
|
|
2372
|
+
* three drift apart. Start attributes are snapshotted before the body runs;
|
|
2373
|
+
* handle writes are merged over them at record time, post-hoc winning on a clash.
|
|
2374
|
+
*/
|
|
2375
|
+
/**
|
|
2376
|
+
* One AI **evaluation** verdict — a scorer's `{name, score, label?}` — to attach
|
|
2377
|
+
* to a generation span via {@link SpanHandle.recordEvaluation}. Declared
|
|
2378
|
+
* structurally to mirror `shared/evaluation-attributes.ts`'s `EvaluationInput`
|
|
2379
|
+
* without a dependency edge.
|
|
2380
|
+
*/
|
|
2381
|
+
interface SpanEvaluation {
|
|
2382
|
+
/** Optional categorical label (e.g. `"pass"` / `"fail"`), emitted as `.label`. */
|
|
2383
|
+
label?: string;
|
|
2384
|
+
/** Scorer name — the key's name segment; non-`[A-Za-z0-9._-]` chars become `_`. */
|
|
2385
|
+
name: string;
|
|
2386
|
+
/** Numeric score (typically `[0, 1]`), emitted as `.score`. */
|
|
2387
|
+
score: number;
|
|
2388
|
+
}
|
|
2389
|
+
interface SpanHandle {
|
|
2390
|
+
/**
|
|
2391
|
+
* Record a timestamped event on the enclosing span — a retry, a cache miss, a
|
|
2392
|
+
* state transition. Prefer this over an extra `ctx.log` line for anything that
|
|
2393
|
+
* only makes sense *relative to this span*: it rides the span's own export, so
|
|
2394
|
+
* it costs no additional record and can never be separated from its context.
|
|
2395
|
+
*/
|
|
2396
|
+
addEvent: (name: string, attributes?: LogFields) => void;
|
|
2397
|
+
/**
|
|
2398
|
+
* Link this span to one in another trace — how a queue consumer points back at
|
|
2399
|
+
* the request that enqueued its message without collapsing every producer into
|
|
2400
|
+
* one giant trace.
|
|
2401
|
+
*/
|
|
2402
|
+
addLink: (link: SpanLink) => void;
|
|
2403
|
+
/**
|
|
2404
|
+
* Attach an AI **evaluation** verdict to this (generation) span as the
|
|
2405
|
+
* `gen_ai.evaluation.<name>.score` / `.label` OpenTelemetry attributes, so a
|
|
2406
|
+
* scorer's grade rides the same trace as the generation it graded. Convenience
|
|
2407
|
+
* over {@link SpanHandle.setAttributes} that owns the key format; privacy-safe —
|
|
2408
|
+
* only the name, score, and optional label are emitted. Throws on an empty name
|
|
2409
|
+
* or a non-finite score.
|
|
2410
|
+
*/
|
|
2411
|
+
recordEvaluation: (evaluation: SpanEvaluation) => void;
|
|
2412
|
+
/**
|
|
2413
|
+
* Record a **handled** exception as the OTel-conventional `exception` span
|
|
2414
|
+
* event (`exception.type` / `exception.message` / `exception.stacktrace`),
|
|
2415
|
+
* without marking the span failed.
|
|
2416
|
+
*
|
|
2417
|
+
* For an error you swallowed — a retried request, a fallback that worked. An
|
|
2418
|
+
* error that escapes the span body is recorded automatically and *does* set
|
|
2419
|
+
* the error status, so don't call this for one you're re-throwing.
|
|
2420
|
+
*/
|
|
2421
|
+
recordException: (error: unknown) => void;
|
|
2422
|
+
/** Set one attribute on the enclosing span (merged at record time; post-hoc wins on key clash). */
|
|
2423
|
+
setAttribute: (key: string, value: LogFields[string]) => void;
|
|
2424
|
+
/** Merge attributes onto the enclosing span (post-hoc wins on key clash). */
|
|
2425
|
+
setAttributes: (fields: LogFields) => void;
|
|
2426
|
+
/**
|
|
2427
|
+
* The W3C ids of the span this handle refers to (32-hex trace, 16-hex span).
|
|
2428
|
+
*
|
|
2429
|
+
* On `ctx.span` these are the DISPATCH's ids — the trace the whole request
|
|
2430
|
+
* belongs to. Use it to echo a trace id back to a caller so a user can quote
|
|
2431
|
+
* it in a bug report, to build a `traceparent` for a hand-rolled outbound
|
|
2432
|
+
* call, or to parent a third-party library's spans onto this request.
|
|
2433
|
+
*/
|
|
2434
|
+
spanContext: () => SpanContextIds;
|
|
2435
|
+
}
|
|
2436
|
+
/**
|
|
2437
|
+
* A span's W3C ids plus the trace's settled sampling verdict.
|
|
2438
|
+
*
|
|
2439
|
+
* `sampled` is the propagated head decision — absent means none reached this
|
|
2440
|
+
* tier, which every consumer reads as keep. It rides with the ids because
|
|
2441
|
+
* everything that announces this span downstream from them (a hand-built
|
|
2442
|
+
* `traceparent`, an `@opentelemetry/api` `SpanContext`) needs the flag in the
|
|
2443
|
+
* same breath: claiming SAMPLED on a trace that was sampled out leaves a
|
|
2444
|
+
* collector holding the middle of a trace nobody kept.
|
|
2445
|
+
*/
|
|
2446
|
+
interface SpanContextIds {
|
|
2447
|
+
/** The trace's settled W3C `sampled` verdict; absent when none was propagated. */
|
|
2448
|
+
sampled?: boolean;
|
|
2449
|
+
/** This span's id (16-hex). */
|
|
2450
|
+
spanId: string;
|
|
2451
|
+
/** The trace this span belongs to (32-hex). */
|
|
2452
|
+
traceId: string;
|
|
2453
|
+
}
|
|
2454
|
+
/**
|
|
2455
|
+
* A causal reference to a span in another trace (OTel `Span.links`). Ids are
|
|
2456
|
+
* lowercase hex — 32 chars for `traceId`, 16 for `spanId`.
|
|
2457
|
+
*/
|
|
2458
|
+
interface SpanLink {
|
|
2459
|
+
/** Attributes describing the relationship, e.g. `{ "link.kind": "enqueued_by" }`. */
|
|
2460
|
+
attributes?: LogFields;
|
|
2461
|
+
spanId: string;
|
|
2462
|
+
traceId: string;
|
|
2463
|
+
}
|
|
2464
|
+
/** OTel `SpanKind`. Drives a collector's service map — see {@link SpanOptions.kind}. */
|
|
2465
|
+
type SpanKind = "client" | "consumer" | "internal" | "producer" | "server";
|
|
2466
|
+
/** Options accepted by `ctx.trace(name, fn, options)` beyond a plain attribute bag. */
|
|
2467
|
+
interface SpanOptions {
|
|
2468
|
+
/** Start attributes, snapshotted before the body runs. */
|
|
2469
|
+
attributes?: LogFields;
|
|
2470
|
+
/**
|
|
2471
|
+
* OTel `SpanKind`, default `"internal"`. Set `"client"` for a call OUT to
|
|
2472
|
+
* another service and `"producer"`/`"consumer"` for queue hops: a collector
|
|
2473
|
+
* builds its service map from this, so leaving everything `"internal"` yields
|
|
2474
|
+
* a trace with no topology.
|
|
2475
|
+
*/
|
|
2476
|
+
kind?: SpanKind;
|
|
2477
|
+
/** Links to spans in other traces, known at span start. */
|
|
2478
|
+
links?: SpanLink[];
|
|
2479
|
+
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Span factory on every function `ctx`. Wraps a sub-operation so it becomes its
|
|
2482
|
+
* own **span** nested under the dispatch's RPC span, giving a trace real shape:
|
|
2483
|
+
* without it a slow request is one opaque bar, with it you see which part was
|
|
2484
|
+
* slow.
|
|
2485
|
+
*
|
|
2486
|
+
* ```ts
|
|
2487
|
+
* const charge = await ctx.trace("stripe.charge", () => stripe.charges.create(…), { orderId });
|
|
2488
|
+
* ```
|
|
2489
|
+
*
|
|
2490
|
+
* **Nesting is explicit.** The body receives a tracer bound to its own span;
|
|
2491
|
+
* calling *that* is what makes a child:
|
|
2492
|
+
*
|
|
2493
|
+
* ```ts
|
|
2494
|
+
* await ctx.trace("fulfil", async (trace) => {
|
|
2495
|
+
* // Children of "fulfil" — including under Promise.all, where an ambient
|
|
2496
|
+
* // "currently open span" would mis-record these as nested inside each other.
|
|
2497
|
+
* await Promise.all([trace("reserve.stock", …), trace("email.receipt", …)]);
|
|
2498
|
+
* });
|
|
2499
|
+
* ```
|
|
2500
|
+
*
|
|
2501
|
+
* Calling `ctx.trace` again inside a body (rather than the passed tracer) is not
|
|
2502
|
+
* an error — that span is simply parented to the dispatch instead of to the
|
|
2503
|
+
* enclosing span, which is flatter but never wrong.
|
|
2504
|
+
*
|
|
2505
|
+
* Spans share the dispatch's trace id with its `ctx.log` lines and any container
|
|
2506
|
+
* the handler calls (the same `traceparent` is propagated), so one trace spans
|
|
2507
|
+
* worker, shard, and container.
|
|
2508
|
+
*
|
|
2509
|
+
* The span is recorded when the body settles, and the body's value is returned
|
|
2510
|
+
* unchanged. A throw is recorded as an error span and then **re-thrown** — this
|
|
2511
|
+
* is instrumentation, never flow control. Recording is best-effort: a failing
|
|
2512
|
+
* sink can't turn a working handler into a broken one.
|
|
2513
|
+
*
|
|
2514
|
+
* **Post-hoc attributes.** The body also receives a {@link SpanHandle} as its
|
|
2515
|
+
* second argument. The `attributes` passed here are stamped at span start (and
|
|
2516
|
+
* snapshotted, so a later mutation can't rewrite them); anything the body sets
|
|
2517
|
+
* through the handle — `span.setAttribute(k, v)` / `span.setAttributes({…})` — is
|
|
2518
|
+
* merged over that snapshot when the span is recorded, so a value known only once
|
|
2519
|
+
* the body has resolved (an AI call's token usage / dollar cost, a computed
|
|
2520
|
+
* count) still lands on the span. Post-hoc wins on a key clash. The handle is a
|
|
2521
|
+
* trailing parameter, so every existing `(trace) => …` body keeps working
|
|
2522
|
+
* unchanged.
|
|
2523
|
+
* @param name Span name, e.g. `"stripe.charge"`. Prefer a low-cardinality name
|
|
2524
|
+
* and put the varying part in `attributes` — a name built from an id makes every
|
|
2525
|
+
* span its own group in a collector.
|
|
2526
|
+
* @param function_ The body to time, receiving a tracer bound to this span for any
|
|
2527
|
+
* nested spans and the enclosing span's {@link SpanHandle} for post-hoc
|
|
2528
|
+
* attributes. May be sync or async; the result is awaited.
|
|
2529
|
+
* @param attributes Either a plain attribute bag to stamp on the span at start
|
|
2530
|
+
* (normalized like a log line's `fields`), or a {@link SpanOptions} object when
|
|
2531
|
+
* you need `kind` or `links`. It is read as options only when *every* key is one
|
|
2532
|
+
* of `attributes`/`kind`/`links` AND a `kind`, if present, actually names a
|
|
2533
|
+
* {@link SpanKind}; `{ attributes: { kind: "premium" } }` is the explicit form if
|
|
2534
|
+
* your own attributes happen to be named that.
|
|
2535
|
+
* @param identity Adapter-only: record the span under ids the caller has ALREADY
|
|
2536
|
+
* published (see {@link SpanIdentity}). A handler never passes this — it exists
|
|
2537
|
+
* so the `@opentelemetry/api` bridge, which must hand a library a `SpanContext`
|
|
2538
|
+
* synchronously, is recorded under the id it handed out rather than a phantom.
|
|
2539
|
+
*/
|
|
2540
|
+
type LunoraTracer = <T>(name: string, function_: (trace: LunoraTracer, span: SpanHandle) => Promise<T> | T, attributes?: LogFields | SpanOptions, identity?: SpanIdentity) => Promise<T>;
|
|
2541
|
+
/**
|
|
2542
|
+
* Caller-supplied ids for one `ctx.trace` span — the tracer's fourth argument.
|
|
2543
|
+
*
|
|
2544
|
+
* For adapters that must publish a span's identity BEFORE the body runs: the
|
|
2545
|
+
* `@opentelemetry/api` bridge returns a `SpanContext` synchronously from
|
|
2546
|
+
* `startSpan` and a library builds a `traceparent` from it, so the span has to be
|
|
2547
|
+
* recorded under the id already announced or every downstream span parents to an
|
|
2548
|
+
* id that never reaches the collector. `parentSpanId` lets such an adapter
|
|
2549
|
+
* express its own parent/child structure without an ambient span stack.
|
|
2550
|
+
*
|
|
2551
|
+
* Both ids are required: an adapter that has published one has published the
|
|
2552
|
+
* other, and `identity` is itself optional — omitting it, not passing a partial
|
|
2553
|
+
* object, is how a caller says "no adapter involved".
|
|
2554
|
+
*/
|
|
2555
|
+
interface SpanIdentity {
|
|
2556
|
+
/** Parent to this span id instead of the enclosing `ctx.trace` / dispatch span. */
|
|
2557
|
+
parentSpanId: string;
|
|
2558
|
+
/** Record the span under this id (16-hex) instead of a freshly minted one. */
|
|
2559
|
+
spanId: string;
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* `ctx.span` — a handle onto **this request's own span**, and with it the
|
|
2563
|
+
* wide-event API.
|
|
2564
|
+
*
|
|
2565
|
+
* ```ts
|
|
2566
|
+
* export const checkout = mutation({ handler: async (ctx, args) => {
|
|
2567
|
+
* ctx.span.setAttributes({ "user.plan": user.plan, "cart.items": cart.length });
|
|
2568
|
+
* const payment = await charge(cart);
|
|
2569
|
+
* ctx.span.setAttributes({ "payment.provider": payment.provider, "payment.total": payment.total });
|
|
2570
|
+
* if (payment.retried) ctx.span.addEvent("payment.retried", { attempts: payment.attempts });
|
|
2571
|
+
* return payment;
|
|
2572
|
+
* }});
|
|
2573
|
+
* ```
|
|
2574
|
+
*
|
|
2575
|
+
* **Why this instead of more log lines.** The usual way to make a handler
|
|
2576
|
+
* observable is to sprinkle `ctx.log.info` through it, which costs one record per
|
|
2577
|
+
* call, scatters one request's facts across a dozen rows, and forces every
|
|
2578
|
+
* question to be answered by correlating them back together. The wide-event
|
|
2579
|
+
* pattern inverts that: accumulate the facts as you learn them, and emit **one**
|
|
2580
|
+
* richly-attributed record per unit of work. Cost is flat — one span per request
|
|
2581
|
+
* no matter how much you attach — and every question ("p99 checkout latency for
|
|
2582
|
+
* pro-plan users with >10 items") becomes a single filter over one table instead
|
|
2583
|
+
* of a join across log lines.
|
|
2584
|
+
*
|
|
2585
|
+
* **This is plain OpenTelemetry, not a Lunora convention.** The attributes land
|
|
2586
|
+
* on the span the dispatch already emits, and are additionally exported as an
|
|
2587
|
+
* OTel Event record named `lunora.dispatch`, correlated by `trace_id`/`span_id`.
|
|
2588
|
+
* Any OTLP backend groups and aggregates them with no special configuration.
|
|
2589
|
+
*
|
|
2590
|
+
* **`span` vs `trace`.** `ctx.trace(name, fn)` creates a NEW child span to time a
|
|
2591
|
+
* sub-operation; `ctx.span` attaches to the one that already exists for the
|
|
2592
|
+
* request. Use `trace` for "how long did this part take", `span` for "what was
|
|
2593
|
+
* true about this request". Inside a `ctx.trace` body, the handle passed as the
|
|
2594
|
+
* body's second argument is that child span's equivalent of this.
|
|
2595
|
+
*
|
|
2596
|
+
* Attributes are normalized exactly like `ctx.log` fields, and recording is
|
|
2597
|
+
* best-effort — a telemetry failure never breaks the handler.
|
|
2598
|
+
*/
|
|
2599
|
+
type LunoraWideEvent = SpanHandle;
|
|
2600
|
+
/**
|
|
2601
|
+
* Application metrics on every function `ctx` — the third signal alongside
|
|
2602
|
+
* `ctx.log` and `ctx.trace`. Each call records one measurement that flows to an
|
|
2603
|
+
* `ObservabilitySink`'s `onMetric`, and from `otlpSink` to a collector's
|
|
2604
|
+
* `/v1/metrics`.
|
|
2605
|
+
*
|
|
2606
|
+
* ```ts
|
|
2607
|
+
* ctx.metrics.count("orders.placed", 1, { plan: user.plan });
|
|
2608
|
+
* ctx.metrics.record("checkout.latency_ms", Date.now() - started);
|
|
2609
|
+
* ctx.metrics.gauge("cart.items", cart.items.length);
|
|
2610
|
+
* ```
|
|
2611
|
+
*
|
|
2612
|
+
* Pick the instrument by the question you want to answer: `count` for "how many"
|
|
2613
|
+
* (summed over time), `gauge` for "how many right now" (replaces the last
|
|
2614
|
+
* reading), `record` for "what's the distribution" (percentiles, not just a
|
|
2615
|
+
* mean).
|
|
2616
|
+
*
|
|
2617
|
+
* `attributes` are the metric's dimensions. Keep them **low-cardinality** — an
|
|
2618
|
+
* attribute valued by user id or order id creates a distinct time series per id,
|
|
2619
|
+
* which is how a metrics backend gets expensive. Put identifiers on a log line or
|
|
2620
|
+
* a span instead.
|
|
2621
|
+
*
|
|
2622
|
+
* No pre-aggregation happens: one call is one exported measurement, with counter
|
|
2623
|
+
* deltas for the collector to sum. In a hot loop, sum locally and record once
|
|
2624
|
+
* rather than calling per iteration.
|
|
2625
|
+
*/
|
|
2626
|
+
interface LunoraMetrics {
|
|
2627
|
+
/**
|
|
2628
|
+
* Add to a monotonic counter (default `1`) — requests served, retries,
|
|
2629
|
+
* bytes sent. The collector sums successive deltas.
|
|
2630
|
+
*/
|
|
2631
|
+
readonly count: (name: string, value?: number, attributes?: LogFields) => void;
|
|
2632
|
+
/**
|
|
2633
|
+
* Report a point-in-time reading that replaces the previous one — queue
|
|
2634
|
+
* depth, cache size, connections open.
|
|
2635
|
+
*/
|
|
2636
|
+
readonly gauge: (name: string, value: number, attributes?: LogFields) => void;
|
|
2637
|
+
/**
|
|
2638
|
+
* Observe one sample of a distribution — latency, payload size. Use this,
|
|
2639
|
+
* not a counter, when percentiles matter.
|
|
2640
|
+
*/
|
|
2641
|
+
readonly record: (name: string, value: number, attributes?: LogFields) => void;
|
|
1128
2642
|
}
|
|
1129
2643
|
interface QueryCtx {
|
|
1130
2644
|
readonly auth: AuthState;
|
|
1131
2645
|
readonly db: DatabaseReader;
|
|
1132
2646
|
/**
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
2647
|
+
* The validated, typed environment. Populated only when the project declares
|
|
2648
|
+
* a `defineEnv(...)` contract in `lunora/env.ts`; codegen then narrows this to
|
|
2649
|
+
* the validated `InferEnv` shape so `ctx.env.STRIPE_KEY` is parsed and
|
|
2650
|
+
* coercion-aware. Absent (optional) without a contract — declare
|
|
2651
|
+
* `lunora/env.ts` to populate and type it.
|
|
2652
|
+
*/
|
|
1139
2653
|
readonly env?: Record<string, unknown>;
|
|
1140
2654
|
/**
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
2655
|
+
* The caller's IP for this request, or `undefined` when nothing trustworthy
|
|
2656
|
+
* says who called.
|
|
2657
|
+
*
|
|
2658
|
+
* Populated only from Cloudflare's `CF-Connecting-IP`, forwarded server-side,
|
|
2659
|
+
* and only while running ON Cloudflare — that is the one place the edge stamps
|
|
2660
|
+
* the header over anything the client sent. On any other host it is a header
|
|
2661
|
+
* the caller typed, so the runtime resolves nothing rather than hand a handler
|
|
2662
|
+
* an attacker-chosen address; a rate limit keyed on a forgeable `ip` is worse
|
|
2663
|
+
* than no limit, because it reads as enforced. `undefined` therefore covers: a
|
|
2664
|
+
* live-subscription re-run, a server-initiated dispatch, and ANY non-Cloudflare
|
|
2665
|
+
* host. A convenient rate-limit key for anonymous traffic on Cloudflare;
|
|
2666
|
+
* elsewhere, key on something the caller cannot choose.
|
|
2667
|
+
*/
|
|
1146
2668
|
readonly ip?: string;
|
|
1147
2669
|
/** Structured, function-attributed logger; see {@link LunoraLogger}. */
|
|
1148
2670
|
readonly log: LunoraLogger;
|
|
1149
2671
|
/**
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
2672
|
+
* Static metadata declared on this procedure with `.meta(...)`, merged
|
|
2673
|
+
* across calls and deep-frozen. Present so middleware can read the policy it
|
|
2674
|
+
* is meant to enforce (`ctx.meta.rateLimit`, …) instead of having it
|
|
2675
|
+
* hard-wired at each `.use()` site; absent when the procedure never called
|
|
2676
|
+
* `.meta()`.
|
|
2677
|
+
*/
|
|
2678
|
+
readonly meta?: Readonly<Record<string, unknown>>;
|
|
2679
|
+
/** Application counters, gauges, and histograms; see {@link LunoraMetrics}. */
|
|
2680
|
+
readonly metrics: LunoraMetrics;
|
|
2681
|
+
/** `ctx.newId()`: see `createStableIdFactory`. Derived, not random, when the call carries a replay key. */
|
|
2682
|
+
readonly newId: () => string;
|
|
2683
|
+
/**
|
|
2684
|
+
* Wall-clock time (epoch ms) the function began, captured once so the whole
|
|
2685
|
+
* handler sees a single stable value. Query/mutation handlers must be
|
|
2686
|
+
* deterministic — they may be re-run on subscription re-evaluation (an OCC
|
|
2687
|
+
* conflict surfaces as a `409` to the caller, not an internal retry) — so
|
|
2688
|
+
* read time through `ctx.now` instead of `Date.now()` (the latter is flagged
|
|
2689
|
+
* by the `nondeterministic_query_mutation` advisor). Actions may use `Date.now()`.
|
|
2690
|
+
*/
|
|
1156
2691
|
readonly now: number;
|
|
1157
2692
|
/**
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
readonly runQuery:
|
|
2693
|
+
* Compose a read-only subquery in-process, reusing this query's read
|
|
2694
|
+
* context (same transaction, same `db`). Executes the referenced query's
|
|
2695
|
+
* handler directly — no fresh DO RPC round-trip — so it observes the exact
|
|
2696
|
+
* same snapshot. A query may only call other queries; there is no
|
|
2697
|
+
* `runMutation` on a `QueryCtx` (writes are not allowed from a query).
|
|
2698
|
+
* Mirrors Convex's `ctx.runQuery`.
|
|
2699
|
+
*/
|
|
2700
|
+
readonly runQuery: RunQuery;
|
|
1166
2701
|
/** Read account-level secrets from Cloudflare Secrets Store; see {@link Secrets}. */
|
|
1167
2702
|
readonly secrets: Secrets;
|
|
2703
|
+
/** Attach facts to THIS request's span — the wide event; see {@link LunoraWideEvent}. */
|
|
2704
|
+
readonly span: LunoraWideEvent;
|
|
1168
2705
|
readonly storage: ReadOnlyStorage;
|
|
2706
|
+
/** Wrap a sub-operation in its own nested span; see {@link LunoraTracer}. */
|
|
2707
|
+
readonly trace: LunoraTracer;
|
|
1169
2708
|
readonly vectors: VectorSearchReader;
|
|
1170
2709
|
}
|
|
1171
2710
|
interface MutationCtx {
|
|
1172
2711
|
readonly auth: AuthState;
|
|
1173
2712
|
readonly db: DatabaseWriter;
|
|
1174
2713
|
/**
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
2714
|
+
* The validated, typed environment. Populated only when the project declares
|
|
2715
|
+
* a `defineEnv(...)` contract in `lunora/env.ts`; codegen then narrows this to
|
|
2716
|
+
* the validated `InferEnv` shape so `ctx.env.STRIPE_KEY` is parsed and
|
|
2717
|
+
* coercion-aware. Absent (optional) without a contract — declare
|
|
2718
|
+
* `lunora/env.ts` to populate and type it.
|
|
2719
|
+
*/
|
|
1181
2720
|
readonly env?: Record<string, unknown>;
|
|
1182
2721
|
/**
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
*/
|
|
2722
|
+
* The caller's IP for this request, or `undefined` when nothing trustworthy
|
|
2723
|
+
* says who called. Identical to {@link QueryCtx.ip} — see there for which
|
|
2724
|
+
* host populates it and why every other one deliberately does not.
|
|
2725
|
+
*/
|
|
1188
2726
|
readonly ip?: string;
|
|
1189
2727
|
/** Structured, function-attributed logger; see {@link LunoraLogger}. */
|
|
1190
2728
|
readonly log: LunoraLogger;
|
|
1191
2729
|
/**
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
2730
|
+
* Static metadata declared on this procedure with `.meta(...)`, merged
|
|
2731
|
+
* across calls and deep-frozen. Present so middleware can read the policy it
|
|
2732
|
+
* is meant to enforce (`ctx.meta.rateLimit`, …) instead of having it
|
|
2733
|
+
* hard-wired at each `.use()` site; absent when the procedure never called
|
|
2734
|
+
* `.meta()`.
|
|
2735
|
+
*/
|
|
2736
|
+
readonly meta?: Readonly<Record<string, unknown>>;
|
|
2737
|
+
/** Application counters, gauges, and histograms; see {@link LunoraMetrics}. */
|
|
2738
|
+
readonly metrics: LunoraMetrics;
|
|
2739
|
+
/** `ctx.newId()`: see `createStableIdFactory`. Derived, not random, when the call carries a replay key. */
|
|
2740
|
+
readonly newId: () => string;
|
|
2741
|
+
/**
|
|
2742
|
+
* Wall-clock time (epoch ms) the function began, captured once so the whole
|
|
2743
|
+
* handler sees a single stable value. Mutation handlers must be deterministic
|
|
2744
|
+
* — an OCC conflict surfaces as a `409` to the caller rather than an internal
|
|
2745
|
+
* retry, but a caller's own retry is a fresh dispatch — so read time through
|
|
2746
|
+
* `ctx.now` instead of `Date.now()` (the latter is flagged by the
|
|
2747
|
+
* `nondeterministic_query_mutation` advisor). Actions may use `Date.now()`.
|
|
2748
|
+
*/
|
|
1198
2749
|
readonly now: number;
|
|
1199
2750
|
/**
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
2751
|
+
* The origin (`scheme://host[:port]`) the request behind this dispatch reached
|
|
2752
|
+
* the worker on: the base for an absolute URL back to this app, such as a
|
|
2753
|
+
* signed storage URL, with no per-environment value to configure.
|
|
2754
|
+
*
|
|
2755
|
+
* The runtime reads it off the request URL, never off a client header, and
|
|
2756
|
+
* forwards it to the shard with the dispatch. `undefined` wherever no request
|
|
2757
|
+
* is behind the call: a scheduled or workflow run, a queue consumer, a
|
|
2758
|
+
* server-initiated dispatch. A query has no `origin` at all: a live query
|
|
2759
|
+
* re-runs with no request behind it, and its cached result is shared across
|
|
2760
|
+
* callers on every host, so a query that builds such a URL needs a configured
|
|
2761
|
+
* base.
|
|
2762
|
+
*/
|
|
2763
|
+
readonly origin?: string;
|
|
2764
|
+
/**
|
|
2765
|
+
* Compose a submutation in-process, reusing this mutation's `db` writer.
|
|
2766
|
+
* Executes the referenced mutation's handler directly — no fresh DO RPC —
|
|
2767
|
+
* so its writes apply through the same shard invocation as the enclosing
|
|
2768
|
+
* mutation. The submutation joins the enclosing mutation's transaction
|
|
2769
|
+
* (SQLite in a Durable Object has no savepoints, so it rides the open span):
|
|
2770
|
+
* its writes commit or roll back together with the caller's, and a throw that
|
|
2771
|
+
* escapes the outer handler rolls back both. Mirrors Convex's `ctx.runMutation`.
|
|
2772
|
+
*/
|
|
2773
|
+
readonly runMutation: RunMutation;
|
|
2774
|
+
/**
|
|
2775
|
+
* Compose a read-only subquery in-process, reusing this mutation's `db`.
|
|
2776
|
+
* Executes the referenced query's handler directly — no fresh DO RPC — so
|
|
2777
|
+
* it observes this mutation's in-flight writes. Mirrors Convex's
|
|
2778
|
+
* `ctx.runQuery`.
|
|
2779
|
+
*/
|
|
2780
|
+
readonly runQuery: RunQuery;
|
|
1215
2781
|
readonly scheduler: Scheduler;
|
|
1216
2782
|
/** Read account-level secrets from Cloudflare Secrets Store; see {@link Secrets}. */
|
|
1217
2783
|
readonly secrets: Secrets;
|
|
1218
|
-
|
|
2784
|
+
/** Attach facts to THIS request's span — the wide event; see {@link LunoraWideEvent}. */
|
|
2785
|
+
readonly span: LunoraWideEvent;
|
|
2786
|
+
readonly storage: MutationStorage;
|
|
2787
|
+
/** Wrap a sub-operation in its own nested span; see {@link LunoraTracer}. */
|
|
2788
|
+
readonly trace: LunoraTracer;
|
|
1219
2789
|
readonly vectors: VectorSearch;
|
|
1220
2790
|
/** Start / resume / inspect durable workflows; see {@link Workflows}. */
|
|
1221
2791
|
readonly workflows: Workflows;
|
|
@@ -1223,52 +2793,90 @@ interface MutationCtx {
|
|
|
1223
2793
|
interface ActionCtx {
|
|
1224
2794
|
readonly auth: AuthState;
|
|
1225
2795
|
/**
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
2796
|
+
* Programmatic Workers Cache purge; see {@link CachePurge}.
|
|
2797
|
+
*
|
|
2798
|
+
* **HTTP actions only.** It is the Worker that holds the `cache` binding, and
|
|
2799
|
+
* only `HttpActionCtx` is built there — an `action` reached over RPC runs
|
|
2800
|
+
* inside the Durable Object like a query or a mutation, so `ctx.cache` is
|
|
2801
|
+
* `undefined` for it. Declared here because `HttpActionCtx` is a `Pick` of
|
|
2802
|
+
* this interface. Optional because Workers Cache is only present when
|
|
2803
|
+
* enabled in `wrangler.jsonc`; always branch on it.
|
|
2804
|
+
*/
|
|
1231
2805
|
readonly cache?: CachePurge;
|
|
1232
2806
|
readonly db: DatabaseWriter;
|
|
1233
2807
|
/**
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
2808
|
+
* The validated, typed environment. Populated only when the project declares
|
|
2809
|
+
* a `defineEnv(...)` contract in `lunora/env.ts`; codegen then narrows this to
|
|
2810
|
+
* the validated `InferEnv` shape so `ctx.env.STRIPE_KEY` is parsed and
|
|
2811
|
+
* coercion-aware. Absent (optional) without a contract — declare
|
|
2812
|
+
* `lunora/env.ts` to populate and type it.
|
|
2813
|
+
*/
|
|
1240
2814
|
readonly env?: Record<string, unknown>;
|
|
1241
2815
|
readonly fetch: typeof globalThis.fetch;
|
|
1242
2816
|
/**
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
*/
|
|
2817
|
+
* The caller's IP for this request, or `undefined` when nothing trustworthy
|
|
2818
|
+
* says who called. Identical to {@link QueryCtx.ip} — see there for which
|
|
2819
|
+
* host populates it and why every other one deliberately does not.
|
|
2820
|
+
*/
|
|
1248
2821
|
readonly ip?: string;
|
|
2822
|
+
/**
|
|
2823
|
+
* `ctx.live.publish(topic, payload, { stream })` — push an event to the SSE
|
|
2824
|
+
* subscribers of a `defineLiveStream` on THIS shard. Present on the shard
|
|
2825
|
+
* (Cloudflare) target only when the app declares a live stream; absent on a
|
|
2826
|
+
* target that rates `liveStreams` `unsupported`, where codegen reports why.
|
|
2827
|
+
* A scheduled job's action publishes this way too. Absent for a query or
|
|
2828
|
+
* mutation: a publish is a side effect, so only an action may make one.
|
|
2829
|
+
*
|
|
2830
|
+
* Delivery is local to the shard: an omitted `shardKey` means the shard the
|
|
2831
|
+
* action runs on, and a `shardKey` naming another shard rejects rather than
|
|
2832
|
+
* reaching zero subscribers silently. The docblock on `defineLiveStream` has the
|
|
2833
|
+
* full routing rules.
|
|
2834
|
+
*/
|
|
2835
|
+
readonly live?: {
|
|
2836
|
+
publish: (topic: string, payload: unknown, options: {
|
|
2837
|
+
shardKey?: string;
|
|
2838
|
+
stream: string;
|
|
2839
|
+
}) => Promise<void>;
|
|
2840
|
+
};
|
|
1249
2841
|
/** Structured, function-attributed logger; see {@link LunoraLogger}. */
|
|
1250
2842
|
readonly log: LunoraLogger;
|
|
1251
2843
|
/**
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
2844
|
+
* Static metadata declared on this procedure with `.meta(...)`, merged
|
|
2845
|
+
* across calls and deep-frozen. Present so middleware can read the policy it
|
|
2846
|
+
* is meant to enforce (`ctx.meta.rateLimit`, …) instead of having it
|
|
2847
|
+
* hard-wired at each `.use()` site; absent when the procedure never called
|
|
2848
|
+
* `.meta()`.
|
|
2849
|
+
*/
|
|
2850
|
+
readonly meta?: Readonly<Record<string, unknown>>;
|
|
2851
|
+
/** Application counters, gauges, and histograms; see {@link LunoraMetrics}. */
|
|
2852
|
+
readonly metrics: LunoraMetrics;
|
|
2853
|
+
/** `ctx.newId()`: see `createStableIdFactory`. Derived, not random, when the call carries a replay key. */
|
|
2854
|
+
readonly newId: () => string;
|
|
2855
|
+
/**
|
|
2856
|
+
* Wall-clock time (epoch ms) the action began, captured once for convenience
|
|
2857
|
+
* and parity with query/mutation `ctx.now`. Actions run exactly once, so they
|
|
2858
|
+
* may also use ambient `Date.now()` freely.
|
|
2859
|
+
*/
|
|
1256
2860
|
readonly now: number;
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
2861
|
+
/**
|
|
2862
|
+
* The origin the request behind this dispatch reached the worker on, or
|
|
2863
|
+
* `undefined` when no request is behind it. Identical to
|
|
2864
|
+
* {@link MutationCtx.origin}.
|
|
2865
|
+
*/
|
|
2866
|
+
readonly origin?: string;
|
|
2867
|
+
readonly runAction: RunAction;
|
|
2868
|
+
readonly runMutation: RunMutation;
|
|
2869
|
+
readonly runQuery: RunQuery;
|
|
1260
2870
|
readonly scheduler: Scheduler;
|
|
1261
2871
|
/** Read account-level secrets from Cloudflare Secrets Store; see {@link Secrets}. */
|
|
1262
2872
|
readonly secrets: Secrets;
|
|
2873
|
+
/** Attach facts to THIS request's span — the wide event; see {@link LunoraWideEvent}. */
|
|
2874
|
+
readonly span: LunoraWideEvent;
|
|
1263
2875
|
readonly storage: Storage;
|
|
2876
|
+
/** Wrap a sub-operation in its own nested span; see {@link LunoraTracer}. */
|
|
2877
|
+
readonly trace: LunoraTracer;
|
|
1264
2878
|
readonly vectors: VectorSearch;
|
|
1265
2879
|
/** Start / resume / inspect durable workflows; see {@link Workflows}. */
|
|
1266
2880
|
readonly workflows: Workflows;
|
|
1267
2881
|
}
|
|
1268
|
-
|
|
1269
|
-
* Stand-in returned by codegen so projects can `import { api } from "./_generated/api"`.
|
|
1270
|
-
* The runtime value is opaque; the types are filled in by generated declarations.
|
|
1271
|
-
*/
|
|
1272
|
-
type AnyApi = Record<string, Record<string, RegisteredFunction<ArgsValidator, unknown, FunctionKind>>>;
|
|
1273
|
-
declare const anyApi: AnyApi;
|
|
1274
|
-
export { type ActionCtx, type AggregateIndexDefinition, type AggregateOp, type AnyApi, type ArgsValidator, type AuthState, type CachePurge, type DatabaseReader, type DatabaseWriter, type DurableObjectJurisdiction, type ExternalSourceDefinition, type ExternalSourceMode, type ExternalSourceRefresh, type FunctionKind, type FunctionVisibility, type GlobalBackend, type IndexDefinition, type IndexRangeBuilder, type InferArgs, type LifecycleEvent, type LifecycleEventKind, type LunoraLogger, type MutationCtx, type OnDeleteAction, type PaginationOptions, type PaginationResult, type QueryCtx, type RankIndexDefinition, type RankSortKey, type ReadOnlyStorage, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMutation, type RegisteredQuery, type RegisteredStream, type RelationDefinition, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type Schema, type SearchFilterBuilder, type SearchIndexDefinition, type Secrets, type SecretsStoreSecretLike, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableDefinition, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerBuilder, type TriggerCtx, type TriggerDatabase, type TriggerDefinition, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorEmbedder, type VectorIndexDefinition, type VectorMatch, type VectorMatches, type VectorMetric, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, type X402ProcedureConfig, anyApi };
|
|
2882
|
+
export type { ActionCtx, AggregateIndexDefinition, AggregateOp, ArgsValidator, AuthState, CachePurge, DatabaseReader, DatabaseWriter, DurableObjectJurisdiction, DurableStreamOptions, ExposeConfig, ExternalSourceCursor, ExternalSourceDefinition, ExternalSourceMode, ExternalSourceRefresh, FunctionKind, FunctionVisibility, GeoBoundingBox, GeoFilterBuilder, GeoIndexDefinition, GeoPointInput, GlobalBackend, IndexDefinition, IndexRangeBuilder, InferArgs, KeyedRelationDefinition, LifecycleEvent, LifecycleEventKind, LogFields, LunoraLogMethod, LunoraLogger, LunoraMetrics, LunoraTracer, LunoraWideEvent, MutationCtx, MutationStorage, OnDeleteAction, PaginationOptions, PaginationResult, PatchIfResult, QueryCtx, RankIndexDefinition, RankSortKey, ReadOnlyStorage, RegisteredAction, RegisteredFunction, RegisteredLifecycleHook, RegisteredMutation, RegisteredQuery, RegisteredStream, RelatedDirection, RelatedNode, RelatedOptions, RelatedPage, RelatedStart, RelatedStartReference, RelationDefinition, RelationThrough, RestCacheConfig, RetryPolicy, RunQueryOptions, ScheduledFunctionDoc, ScheduledJob, Scheduler, Schema, SearchFilterBuilder, SearchIndexDefinition, SearchLanguage, SearchStrategy, Secrets, SecretsStoreSecretLike, ShardInitEvent, ShardMode, SpanContextIds, SpanEvaluation, SpanHandle, SpanIdentity, SpanKind, SpanLink, SpanOptions, Storage, StorageListOptions, StorageListResult, StorageMetadata, StorageMultipartUpload, StorageMultipartUploadOptions, StorageObjectBody, StorageObjectHead, StorageRange, StorageUploadBody, StorageUploadOptions, StorageUploadResult, StorageUploadedPart, SystemDatabaseReader, SystemDoc, SystemQuery, SystemTableName, TableDefinition, TableReader, TableVectorIndex, ThroughRelationDefinition, TriggerAggregateOptions, TriggerBuilder, TriggerCtx, TriggerDatabase, TriggerDefinition, TriggerDeleteEvent, TriggerEvent, TriggerGroupByEntry, TriggerGroupByOptions, TriggerHandler, TriggerInsertEvent, TriggerOp, TriggerQueryArgs, TriggerQueryPage, TriggerRankOptions, TriggerRankPageOptions, TriggerRankResult, TriggerRow, TriggerTiming, TriggerUpdateEvent, TtlDefinition, VectorEmbedder, VectorIndexDefinition, VectorMatch, VectorMatches, VectorMetric, VectorQueryInput, VectorRecord, VectorSearch, VectorSearchReader, VectorUpsertInput, WhisperEvent, WorkflowCreateOptions, WorkflowEventDefinition, WorkflowHandle, WorkflowInstance, WorkflowInstanceEvent, WorkflowInstanceStatus, WorkflowStatusResult, Workflows, X402ProcedureConfig };
|