@lunora/server 0.0.0 → 1.0.0-alpha.10
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 +105 -0
- package/README.md +134 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/data-model.d.mts +416 -0
- package/dist/data-model.d.ts +416 -0
- package/dist/data-model.mjs +1 -0
- package/dist/drizzle.d.mts +1 -0
- package/dist/drizzle.d.ts +1 -0
- package/dist/drizzle.mjs +1 -0
- package/dist/index.d.mts +1985 -0
- package/dist/index.d.ts +1985 -0
- package/dist/index.mjs +28 -0
- package/dist/packem_shared/LunoraEnvError-DjFkpkSP.mjs +187 -0
- package/dist/packem_shared/LunoraError-DN7Zhhvu.mjs +54 -0
- package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-D8viLY1S.mjs +114 -0
- package/dist/packem_shared/asBucketStorage-Cnxd9y2q.mjs +11 -0
- package/dist/packem_shared/bindOrm-Ce57S3N9.mjs +128 -0
- package/dist/packem_shared/buildRlsReadRegistry-1jexWrb3.mjs +107 -0
- package/dist/packem_shared/composePluginMiddleware-Ck5_TUO8.mjs +100 -0
- package/dist/packem_shared/createPolicyDsl-De67zPDS.mjs +29 -0
- package/dist/packem_shared/createSecrets-TsIP9lOa.mjs +55 -0
- package/dist/packem_shared/defineAggregateIndex-ZdyU78gh.mjs +291 -0
- package/dist/packem_shared/defineMigration-CAJLr6fx.mjs +8 -0
- package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +11 -0
- package/dist/packem_shared/defineShape-CJ27Wx7o.mjs +17 -0
- package/dist/packem_shared/defineStorageRule-qu0mpilX.mjs +20 -0
- package/dist/packem_shared/functions-Di9FUNkf.mjs +5 -0
- package/dist/packem_shared/httpAction-FLwfsePg.mjs +340 -0
- package/dist/packem_shared/initLunora-lxwHTEV3.mjs +100 -0
- package/dist/packem_shared/mask-BV_jNzsN.mjs +211 -0
- package/dist/packem_shared/onConnect-CIPXKPyw.mjs +13 -0
- package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +13 -0
- package/dist/packem_shared/protectPublic-BjFkQ_Or.mjs +15 -0
- package/dist/packem_shared/rls-2Jhd0uev.mjs +569 -0
- package/dist/packem_shared/run-middleware-CYQOuoV6.mjs +18 -0
- package/dist/packem_shared/storageRules-Cje6Woea.mjs +88 -0
- package/dist/packem_shared/types.d-BDY0FYHK.d.ts +135 -0
- package/dist/packem_shared/types.d-DmvyEMD6.d.mts +135 -0
- package/dist/rls/testing.d.mts +63 -0
- package/dist/rls/testing.d.ts +63 -0
- package/dist/rls/testing.mjs +49 -0
- package/dist/types.d.mts +1157 -0
- package/dist/types.d.ts +1157 -0
- package/dist/types.mjs +31 -0
- package/package.json +59 -17
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,1157 @@
|
|
|
1
|
+
import { ValidatorMap, InferValidatorMap, Id, Validator, Infer } from '@lunora/values';
|
|
2
|
+
/** Map of validators describing a function's args record. Alias of `@lunora/values`' shared {@link ValidatorMap}. */
|
|
3
|
+
type ArgsValidator = ValidatorMap;
|
|
4
|
+
/** Infer the args object type from an {@link ArgsValidator}. Alias of `@lunora/values`' shared {@link InferValidatorMap}. */
|
|
5
|
+
type InferArgs<A extends ArgsValidator> = InferValidatorMap<A>;
|
|
6
|
+
/** Storage backend for a `.global()` table: D1 (default) or a Postgres/MySQL database via Cloudflare Hyperdrive (PlanetScale, Neon, …). */
|
|
7
|
+
type GlobalBackend = "d1" | "hyperdrive";
|
|
8
|
+
/**
|
|
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
|
+
*/
|
|
15
|
+
type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
|
|
16
|
+
/** How a table is routed at runtime. */
|
|
17
|
+
type ShardMode = {
|
|
18
|
+
backend?: GlobalBackend;
|
|
19
|
+
kind: "global";
|
|
20
|
+
} | {
|
|
21
|
+
field: string;
|
|
22
|
+
kind: "shardBy";
|
|
23
|
+
} | {
|
|
24
|
+
kind: "root";
|
|
25
|
+
};
|
|
26
|
+
/** Poll cadence for a sourced table — `"manual"` (pull only on an explicit trigger) or a fixed interval. */
|
|
27
|
+
type ExternalSourceRefresh = "manual" | {
|
|
28
|
+
everyMs: number;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Delete-detection mode for external-source ingest. `"full-pull"` (default) reads
|
|
32
|
+
* the whole tenant membership each tick and diffs it, so it observes upstream
|
|
33
|
+
* deletes; `"incremental"` pulls only changed rows (cheap) and is blind to deletes
|
|
34
|
+
* unless paired with a soft-delete column or a `reconcileEveryMs` full-pull sweep.
|
|
35
|
+
*/
|
|
36
|
+
type ExternalSourceMode = "full-pull" | "incremental";
|
|
37
|
+
/**
|
|
38
|
+
* Config for `.source(...)` (plan 077): declares a table as **materialized from an
|
|
39
|
+
* external Postgres/MySQL behind Cloudflare Hyperdrive**, not written by user
|
|
40
|
+
* mutations. A system-driven poll loop reads the tenant slice and lands it in the
|
|
41
|
+
* DO's SQLite (via the validated CDC writer), after which `defineShape` carries it
|
|
42
|
+
* to clients unchanged. Orthogonal to `shardMode` — a sourced table almost always
|
|
43
|
+
* also `.shardBy()`s, in which case `tenantBy` is the mandatory tenant-isolation
|
|
44
|
+
* boundary (enforced by the `external_source_unscoped` advisor lint).
|
|
45
|
+
*/
|
|
46
|
+
interface ExternalSourceDefinition {
|
|
47
|
+
/** The wrangler Hyperdrive binding name the poll loop reads from. */
|
|
48
|
+
binding: string;
|
|
49
|
+
/** Project the materialized rows to these columns (passed to the membership diff). Omit ⇒ the full mapped document. */
|
|
50
|
+
columns?: ReadonlyArray<string>;
|
|
51
|
+
/** Column whose value becomes the Lunora `_id`. Defaults to `"id"`. */
|
|
52
|
+
idColumn?: string;
|
|
53
|
+
/** Transform an external row into the stored document body. Omit ⇒ every selected column except `idColumn` is copied. */
|
|
54
|
+
map?: (row: Record<string, unknown>) => Record<string, unknown>;
|
|
55
|
+
/** Delete-detection mode. Defaults to `"full-pull"`. */
|
|
56
|
+
mode?: ExternalSourceMode;
|
|
57
|
+
/** The full tenant-membership query, with driver-native placeholders (`$1` / `?`). `tenantBy` binds its params. */
|
|
58
|
+
query: string;
|
|
59
|
+
/** `"incremental"` only: run a full-pull reconcile this often to garbage-collect tombstones the incremental path can't see. */
|
|
60
|
+
reconcileEveryMs?: number;
|
|
61
|
+
/** Poll cadence, or `"manual"`. Omit ⇒ the runtime's size-scaled default. */
|
|
62
|
+
refresh?: ExternalSourceRefresh;
|
|
63
|
+
/**
|
|
64
|
+
* **Mandatory under `.shardBy()`**: map this DO's shard key → the query's bound
|
|
65
|
+
* params, so a tenant DO can only ever pull its own rows. An unscoped sourced +
|
|
66
|
+
* sharded table replicates the whole multitenant table into every shard — the
|
|
67
|
+
* `external_source_unscoped` advisor lint fails the build when this is absent.
|
|
68
|
+
*/
|
|
69
|
+
tenantBy?: (shardKey: string) => ReadonlyArray<unknown>;
|
|
70
|
+
}
|
|
71
|
+
interface IndexDefinition {
|
|
72
|
+
fields: ReadonlyArray<string>;
|
|
73
|
+
name: string;
|
|
74
|
+
unique?: boolean;
|
|
75
|
+
}
|
|
76
|
+
interface SearchIndexDefinition {
|
|
77
|
+
field: string;
|
|
78
|
+
filterFields?: ReadonlyArray<string>;
|
|
79
|
+
name: string;
|
|
80
|
+
}
|
|
81
|
+
/** Reducer applied by an aggregate index. */
|
|
82
|
+
type AggregateOp = "avg" | "count" | "max" | "min" | "sum";
|
|
83
|
+
/**
|
|
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
|
+
*/
|
|
96
|
+
interface AggregateIndexDefinition {
|
|
97
|
+
by?: ReadonlyArray<string>;
|
|
98
|
+
field?: string;
|
|
99
|
+
name: string;
|
|
100
|
+
on: string;
|
|
101
|
+
op: AggregateOp;
|
|
102
|
+
where?: Record<string, unknown>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
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
|
+
*/
|
|
109
|
+
interface RankSortKey {
|
|
110
|
+
direction: "asc" | "desc";
|
|
111
|
+
field: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
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
|
+
*/
|
|
131
|
+
interface RankIndexDefinition {
|
|
132
|
+
name: string;
|
|
133
|
+
on: string;
|
|
134
|
+
partitionBy?: ReadonlyArray<string>;
|
|
135
|
+
sortBy: ReadonlyArray<RankSortKey>;
|
|
136
|
+
where?: Record<string, unknown>;
|
|
137
|
+
}
|
|
138
|
+
/** FK behavior when a referenced parent row is deleted (mirrors SQL `ON DELETE`). */
|
|
139
|
+
type OnDeleteAction = "cascade" | "restrict" | "set null";
|
|
140
|
+
/**
|
|
141
|
+
* A declared relation between two tables, recorded by `.relations((r) => …)`.
|
|
142
|
+
*
|
|
143
|
+
* - `one` (many-to-one): the FK column `field` lives on **this** table and
|
|
144
|
+
* points at `table`.`references` (default `_id`). Loads a single doc.
|
|
145
|
+
* - `many` (one-to-many): the FK column `field` lives on the **target** table
|
|
146
|
+
* and points back at this table's `references` (default `_id`). Loads an array.
|
|
147
|
+
*
|
|
148
|
+
* `onDelete` is meaningful only on `one`: it is the action applied to the
|
|
149
|
+
* holder rows when the referenced parent row is deleted.
|
|
150
|
+
*/
|
|
151
|
+
interface RelationDefinition {
|
|
152
|
+
field: string;
|
|
153
|
+
kind: "many" | "one";
|
|
154
|
+
onDelete?: OnDeleteAction;
|
|
155
|
+
references: string;
|
|
156
|
+
table: string;
|
|
157
|
+
}
|
|
158
|
+
/** Distance metric used by a Vectorize index. */
|
|
159
|
+
type VectorMetric = "cosine" | "dot-product" | "euclidean";
|
|
160
|
+
/**
|
|
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
|
+
*/
|
|
165
|
+
type VectorEmbedder = (input: string) => Promise<ReadonlyArray<number>> | ReadonlyArray<number>;
|
|
166
|
+
/**
|
|
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
|
+
*/
|
|
170
|
+
interface TableVectorIndex {
|
|
171
|
+
dimensions: number;
|
|
172
|
+
embed: VectorEmbedder;
|
|
173
|
+
field: string;
|
|
174
|
+
metadata?: ReadonlyArray<string>;
|
|
175
|
+
metric: VectorMetric;
|
|
176
|
+
name: string;
|
|
177
|
+
}
|
|
178
|
+
interface TableDefinition<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
179
|
+
/**
|
|
180
|
+
* Aggregate indexes declared via `.aggregateIndex(name, opts)`. The runtime
|
|
181
|
+
* maintains a counter row per `by` group via the trigger seam, so reads
|
|
182
|
+
* whose `where` keys all participate in the index's `by` set are answered
|
|
183
|
+
* without scanning the underlying table.
|
|
184
|
+
*/
|
|
185
|
+
aggregateIndexes: ReadonlyArray<AggregateIndexDefinition>;
|
|
186
|
+
/**
|
|
187
|
+
* Set by `.source(...)` (named `externalSource`, not `source`, so the data
|
|
188
|
+
* field doesn't collide with the fluent `.source()` builder method — same
|
|
189
|
+
* convention as `shardBy()`/`shardMode`). When present, the table is
|
|
190
|
+
* materialized from an external Hyperdrive-backed database by a system poll
|
|
191
|
+
* loop rather than user mutations. Implies `isExternallyManaged`.
|
|
192
|
+
*/
|
|
193
|
+
externalSource?: ExternalSourceDefinition;
|
|
194
|
+
indexes: ReadonlyArray<IndexDefinition>;
|
|
195
|
+
/**
|
|
196
|
+
* `true` when `.externallyManaged()` was called — the table's rows are
|
|
197
|
+
* written outside Lunora's discoverable insert path (an adapter, a
|
|
198
|
+
* migration, or framework middleware), e.g. `@lunora/auth`'s better-auth
|
|
199
|
+
* tables or `@lunora/ratelimit`'s store. Advisor insert-path lints
|
|
200
|
+
* (`table_without_insert`) skip such tables instead of flagging the absent
|
|
201
|
+
* `ctx.db.insert(...)`.
|
|
202
|
+
*/
|
|
203
|
+
isExternallyManaged?: boolean;
|
|
204
|
+
/**
|
|
205
|
+
* `true` when `.public()` was called — the table opts OUT of secure-by-default
|
|
206
|
+
* RLS. Under a schema marked `.rls("required")`, every table is protected (the
|
|
207
|
+
* DO/D1 write path denies raw, non-RLS `ctx.db` access) UNLESS it is `isPublic`.
|
|
208
|
+
* Has no effect when the schema does not require RLS.
|
|
209
|
+
*/
|
|
210
|
+
isPublic?: boolean;
|
|
211
|
+
/**
|
|
212
|
+
* Rank indexes declared via `.rankIndex(name, opts)`. The runtime maintains
|
|
213
|
+
* a sorted companion table per declared rank with a btree on
|
|
214
|
+
* `(partition, sortBy)` so `rank(row)` returns the row's 1-based position
|
|
215
|
+
* within its partition in O(log n), and `rankPage()` walks the index for
|
|
216
|
+
* sorted pagination.
|
|
217
|
+
*/
|
|
218
|
+
rankIndexes: ReadonlyArray<RankIndexDefinition>;
|
|
219
|
+
/**
|
|
220
|
+
* Declared relations keyed by accessor name; empty unless `.relations()`
|
|
221
|
+
* was called. Named `relationMap` (not `relations`) so the fluent
|
|
222
|
+
* `.relations((r) => …)` builder method doesn't collide with this field.
|
|
223
|
+
*/
|
|
224
|
+
relationMap: Record<string, RelationDefinition>;
|
|
225
|
+
searchIndexes: ReadonlyArray<SearchIndexDefinition>;
|
|
226
|
+
shape: Shape;
|
|
227
|
+
shardMode: ShardMode;
|
|
228
|
+
/**
|
|
229
|
+
* Set by `.softDelete()` (named `softDeleteMode`, not `softDelete`, so the
|
|
230
|
+
* data field doesn't collide with the fluent `.softDelete()` builder method —
|
|
231
|
+
* same convention as `shardBy()`/`shardMode`). When present, the table carries
|
|
232
|
+
* a nullable timestamp column (`field`, default `deletedAt`):
|
|
233
|
+
* `ctx.db.<table>.delete()` flips it instead of physically removing the row,
|
|
234
|
+
* and **list reads** (`findMany`/`findFirst`/`query()`/`count`/`aggregate`/
|
|
235
|
+
* relation loads) hide rows whose `field` is set unless
|
|
236
|
+
* `includeDeleted: true` is passed. By-id `get`/`patch`/`replace` and
|
|
237
|
+
* `restore` are unaffected. Absent ⇒ deletes are physical, as before.
|
|
238
|
+
*/
|
|
239
|
+
softDeleteMode?: {
|
|
240
|
+
field: string;
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* Declared lifecycle triggers keyed by accessor name; empty unless
|
|
244
|
+
* `.triggers()` was called. Named `triggerMap` (not `triggers`) so the
|
|
245
|
+
* fluent `.triggers((t) => …)` builder method doesn't collide with this
|
|
246
|
+
* field — same reasoning as {@link TableDefinition.relationMap}.
|
|
247
|
+
*/
|
|
248
|
+
triggerMap: Record<string, TriggerDefinition>;
|
|
249
|
+
vectorIndexes: ReadonlyArray<TableVectorIndex>;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
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
|
+
*/
|
|
256
|
+
interface VectorIndexDefinition {
|
|
257
|
+
readonly dimensions: number;
|
|
258
|
+
readonly embed: VectorEmbedder;
|
|
259
|
+
readonly kind: "vectorIndex";
|
|
260
|
+
readonly metadata?: (row: Record<string, unknown>) => Record<string, unknown>;
|
|
261
|
+
readonly metric: VectorMetric;
|
|
262
|
+
readonly select: (row: Record<string, unknown>) => string;
|
|
263
|
+
readonly table: string;
|
|
264
|
+
}
|
|
265
|
+
interface Schema<T extends Record<string, TableDefinition> = Record<string, TableDefinition>> {
|
|
266
|
+
/**
|
|
267
|
+
* Secure-by-default RLS mode declared via `.rls("required")`. When
|
|
268
|
+
* `"required"`, every table is protected: the DO/D1 write path denies raw
|
|
269
|
+
* (non-RLS-wrapped) `ctx.db` access at runtime, so a procedure that forgets
|
|
270
|
+
* `.use(rls(...))` fails closed instead of silently exposing the table. A
|
|
271
|
+
* table opts out with `.public()` (→ {@link TableDefinition.isPublic}).
|
|
272
|
+
* Absent ⇒ legacy opt-in behavior (RLS only where a policy is applied).
|
|
273
|
+
*/
|
|
274
|
+
readonly rlsMode?: "required";
|
|
275
|
+
readonly tables: T;
|
|
276
|
+
readonly vectorIndexes: Record<string, VectorIndexDefinition>;
|
|
277
|
+
}
|
|
278
|
+
type FunctionKind = "action" | "mutation" | "query" | "stream";
|
|
279
|
+
/**
|
|
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
|
+
*/
|
|
286
|
+
type FunctionVisibility = "internal" | "public";
|
|
287
|
+
interface RegisteredFunction<A extends ArgsValidator, R, Kind extends FunctionKind> {
|
|
288
|
+
readonly args: A;
|
|
289
|
+
readonly handler: (context: unknown, args: InferArgs<A>) => Promise<R> | R;
|
|
290
|
+
readonly kind: Kind;
|
|
291
|
+
/**
|
|
292
|
+
* Set on connection-lifecycle hooks (`onConnect` / `onDisconnect`).
|
|
293
|
+
* Marks the function for the generated `LUNORA_LIFECYCLE_HOOKS` manifest so the
|
|
294
|
+
* DO dispatches it on socket connect/disconnect rather than via a client RPC.
|
|
295
|
+
* Absent on ordinary registrations.
|
|
296
|
+
*/
|
|
297
|
+
readonly lifecycle?: LifecycleEventKind;
|
|
298
|
+
readonly visibility?: FunctionVisibility;
|
|
299
|
+
}
|
|
300
|
+
type RegisteredQuery<A extends ArgsValidator, R> = RegisteredFunction<A, R, "query">;
|
|
301
|
+
type RegisteredMutation<A extends ArgsValidator, R> = RegisteredFunction<A, R, "mutation">;
|
|
302
|
+
type RegisteredAction<A extends ArgsValidator, R> = RegisteredFunction<A, R, "action">;
|
|
303
|
+
/** Which side of the WebSocket lifecycle a hook fires on. */
|
|
304
|
+
type LifecycleEventKind = "connect" | "disconnect";
|
|
305
|
+
/**
|
|
306
|
+
* The event a connection-lifecycle hook receives as its second argument. It is
|
|
307
|
+
* the JSON-serializable payload the DO forwards on socket connect/disconnect;
|
|
308
|
+
* the verified caller identity is also reflected on `ctx.auth` (the hook runs
|
|
309
|
+
* under the connecting user via `resolveIdentity`).
|
|
310
|
+
*/
|
|
311
|
+
interface LifecycleEvent {
|
|
312
|
+
/** Stable per-socket id, minted at upgrade and replayed verbatim on disconnect. */
|
|
313
|
+
readonly connectionId: string;
|
|
314
|
+
/** App-supplied connection context from the client `connect` envelope (e.g. `{ roomId, sessionId }`). */
|
|
315
|
+
readonly context?: Record<string, unknown>;
|
|
316
|
+
/** The shard this socket is bound to. */
|
|
317
|
+
readonly shardKey: string;
|
|
318
|
+
/** Verified user id resolved at upgrade, or `null` for an anonymous socket. */
|
|
319
|
+
readonly userId: string | null;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* A registered connection-lifecycle hook — an internal mutation tagged with the
|
|
323
|
+
* lifecycle side it fires on. Produced by `onConnect` / `onDisconnect`.
|
|
324
|
+
*/
|
|
325
|
+
type RegisteredLifecycleHook = RegisteredFunction<Record<string, never>, void, "mutation"> & {
|
|
326
|
+
readonly lifecycle: LifecycleEventKind;
|
|
327
|
+
};
|
|
328
|
+
/**
|
|
329
|
+
* A streaming query registration. Unlike {@link RegisteredFunction} the handler
|
|
330
|
+
* returns an `AsyncIterable<R>` synchronously (it does NOT `Promise<R>`); the
|
|
331
|
+
* runtime drives it frame by frame and forwards each chunk to the caller. The
|
|
332
|
+
* third `signal` argument is wired to the caller's cancel signal so the handler
|
|
333
|
+
* can stop early — break out of the loop or check `signal.aborted` between
|
|
334
|
+
* yields.
|
|
335
|
+
*/
|
|
336
|
+
interface RegisteredStream<A extends ArgsValidator, R> {
|
|
337
|
+
readonly args: A;
|
|
338
|
+
readonly handler: (context: unknown, args: InferArgs<A>, signal: AbortSignal) => AsyncIterable<R>;
|
|
339
|
+
readonly kind: "stream";
|
|
340
|
+
readonly visibility?: FunctionVisibility;
|
|
341
|
+
}
|
|
342
|
+
/** The system tables `ctx.db.system` can read. */
|
|
343
|
+
type SystemTableName = "_scheduled_functions" | "_storage";
|
|
344
|
+
/**
|
|
345
|
+
* A pending scheduled invocation as surfaced by the `_scheduled_functions`
|
|
346
|
+
* system table. Mirrors {@link ScheduledJob} (the `ctx.scheduler` view); the
|
|
347
|
+
* separate name keeps the system-table read surface self-describing.
|
|
348
|
+
*/
|
|
349
|
+
interface ScheduledFunctionDoc {
|
|
350
|
+
/** Function arguments the job will be dispatched with. */
|
|
351
|
+
args: Record<string, unknown>;
|
|
352
|
+
/** Number of dispatch attempts already made (absent until the first retry). */
|
|
353
|
+
attempts?: number;
|
|
354
|
+
/** When the job was enqueued (epoch ms). */
|
|
355
|
+
enqueuedAt: number;
|
|
356
|
+
/** Fully-qualified path of the function to invoke. */
|
|
357
|
+
functionPath: string;
|
|
358
|
+
/** The job's id (the `_scheduled_functions` row id). */
|
|
359
|
+
id: string;
|
|
360
|
+
/** When the job is scheduled to fire (epoch ms). */
|
|
361
|
+
scheduledFor: number;
|
|
362
|
+
/** Routing hint forwarded so dispatch lands on the right shard. */
|
|
363
|
+
shardKey?: string;
|
|
364
|
+
}
|
|
365
|
+
/** Maps each system table name to the document shape its reads return. */
|
|
366
|
+
interface SystemDocMap {
|
|
367
|
+
_scheduled_functions: ScheduledFunctionDoc;
|
|
368
|
+
_storage: StorageMetadata;
|
|
369
|
+
}
|
|
370
|
+
/** Document type for a given system table name. */
|
|
371
|
+
type SystemDoc<T extends SystemTableName> = SystemDocMap[T];
|
|
372
|
+
/** Terminal returned by {@link SystemDatabaseReader.query}; only `.collect()` is supported. */
|
|
373
|
+
interface SystemQuery<T extends SystemTableName> {
|
|
374
|
+
/** Resolve the full list of rows in the backing source. */
|
|
375
|
+
collect: () => Promise<SystemDoc<T>[]>;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Read-only reader over Lunora's system tables (`_scheduled_functions`,
|
|
379
|
+
* `_storage`), exposed as `ctx.db.system`. Mirrors Convex's `ctx.db.system`.
|
|
380
|
+
*
|
|
381
|
+
* **Best-effort and eventually consistent.** Unlike `ctx.db.<table>` — which
|
|
382
|
+
* reads the shard's transactional SQLite snapshot — the data behind these tables
|
|
383
|
+
* lives OUTSIDE the shard (scheduled functions in the `SchedulerDO`, storage
|
|
384
|
+
* objects in R2). Every `collect()` / `get()` reaches across to that source.
|
|
385
|
+
*
|
|
386
|
+
* It is **not part of the mutation transaction snapshot** (no OCC guard, no
|
|
387
|
+
* subscription dependency recorded — reading it inside a mutation does not pin
|
|
388
|
+
* it), and results are **eventually consistent** with writes a mutation just
|
|
389
|
+
* made (e.g. a freshly scheduled job may not appear yet).
|
|
390
|
+
*
|
|
391
|
+
* Read-only by design: mutate scheduled jobs via `ctx.scheduler`, storage
|
|
392
|
+
* objects via `ctx.storage`.
|
|
393
|
+
*/
|
|
394
|
+
interface SystemDatabaseReader {
|
|
395
|
+
/**
|
|
396
|
+
* Resolve a single system-table row by id, or `null` when absent.
|
|
397
|
+
* (`_scheduled_functions` → job id; `_storage` → object key.)
|
|
398
|
+
*/
|
|
399
|
+
get: <T extends SystemTableName>(table: T, id: string) => Promise<SystemDoc<T> | null>;
|
|
400
|
+
/**
|
|
401
|
+
* Begin a read over a system table; call `.collect()` to resolve the full
|
|
402
|
+
* list. No filtering, indexing, or pagination — the backing source is remote
|
|
403
|
+
* and the surface stays deliberately minimal.
|
|
404
|
+
*/
|
|
405
|
+
query: <T extends SystemTableName>(table: T) => SystemQuery<T>;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Read-only handle bound to a table. Used by `query`/`mutation`/`action`. The
|
|
409
|
+
* actual SQL implementation lives in `@lunora/do`; these are signatures only.
|
|
410
|
+
*/
|
|
411
|
+
interface DatabaseReader {
|
|
412
|
+
get: <T extends string>(id: Id<T>) => Promise<Record<string, unknown> | null>;
|
|
413
|
+
/**
|
|
414
|
+
* Validate an untrusted `id` string against the structural shape of an id
|
|
415
|
+
* for `tableName`, returning the branded {@link Id} when it is well-formed
|
|
416
|
+
* and `null` otherwise. Pure structural validation — it never reads the
|
|
417
|
+
* database, so a structurally valid id for a row that doesn't exist still
|
|
418
|
+
* returns the branded id (mirrors Convex's `db.normalizeId`).
|
|
419
|
+
*/
|
|
420
|
+
normalizeId: <T extends string>(tableName: T, id: string) => Id<T> | null;
|
|
421
|
+
query: (tableName: string) => TableReader;
|
|
422
|
+
/**
|
|
423
|
+
* Best-effort, read-only reader over Lunora's system tables
|
|
424
|
+
* (`_scheduled_functions`, `_storage`). Eventually consistent and **not**
|
|
425
|
+
* part of the transaction snapshot — see {@link SystemDatabaseReader}.
|
|
426
|
+
*/
|
|
427
|
+
readonly system: SystemDatabaseReader;
|
|
428
|
+
}
|
|
429
|
+
/** Options for {@link TableReader.paginate} — Convex-compatible page request. */
|
|
430
|
+
interface PaginationOptions {
|
|
431
|
+
/** Opaque cursor from the prior page's `continueCursor`; `null`/omitted starts at the first page. */
|
|
432
|
+
cursor?: null | string;
|
|
433
|
+
/**
|
|
434
|
+
* Optional inclusive upper bound for reactive pagination. When supplied the
|
|
435
|
+
* page covers the fixed half-open range `(cursor, endCursor]` (ignoring
|
|
436
|
+
* `numItems`): every row strictly after `cursor` up to and including the
|
|
437
|
+
* boundary row `endCursor` encodes. The page's `isDone` is `true` and its
|
|
438
|
+
* `continueCursor` echoes `endCursor`, so the next page keeps starting where
|
|
439
|
+
* this one ends even as rows are inserted/deleted inside the range. Omit (or
|
|
440
|
+
* pass `null`) for the legacy "first `numItems` after `cursor`" behaviour.
|
|
441
|
+
*/
|
|
442
|
+
endCursor?: null | string;
|
|
443
|
+
/** Maximum rows to return for this page. */
|
|
444
|
+
numItems: number;
|
|
445
|
+
}
|
|
446
|
+
/** One page of a keyset-paginated query. */
|
|
447
|
+
interface PaginationResult<T = Record<string, unknown>> {
|
|
448
|
+
/** Cursor to pass back for the next page, or `null` once `isDone`. */
|
|
449
|
+
continueCursor: null | string;
|
|
450
|
+
/** `true` when this page is the last one. */
|
|
451
|
+
isDone: boolean;
|
|
452
|
+
page: T[];
|
|
453
|
+
/**
|
|
454
|
+
* Reactive-pagination only: the midpoint cursor of a bounded
|
|
455
|
+
* `(cursor, endCursor]` page, used by the client to split an over-grown page
|
|
456
|
+
* into two adjacent ranges. Absent on legacy (open-ended) pages.
|
|
457
|
+
*/
|
|
458
|
+
splitCursor?: null | string;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* The fluent `ctx.db.query(table)` reader. Generic over the document type
|
|
462
|
+
* `Row` so the generated `ctx.db` can bind it to `Doc<table>` (the chain and
|
|
463
|
+
* every terminal then resolve typed rows — no `as unknown as Doc<...>` casts).
|
|
464
|
+
* Defaults to the untyped `Record<string, unknown>` shape for the base
|
|
465
|
+
* (schema-agnostic) `@lunora/server` reader.
|
|
466
|
+
*/
|
|
467
|
+
interface TableReader<Row = Record<string, unknown>> {
|
|
468
|
+
collect: () => Promise<Row[]>;
|
|
469
|
+
filter: (predicate: (document: Row) => boolean) => TableReader<Row>;
|
|
470
|
+
first: () => Promise<Row | null>;
|
|
471
|
+
/**
|
|
472
|
+
* Set the result order. Orders by the active `.withIndex()` (or by
|
|
473
|
+
* `_creationTime` when none is staged), `"asc"` by default; `"desc"`
|
|
474
|
+
* reverses it. Composes with `.withIndex()`, `.filter()`, and every
|
|
475
|
+
* terminal (`collect`/`first`/`take`/`paginate`/`unique`). Mirrors Convex's
|
|
476
|
+
* `.order("asc" | "desc")`.
|
|
477
|
+
*/
|
|
478
|
+
order: (direction: "asc" | "desc") => TableReader<Row>;
|
|
479
|
+
paginate: (options: PaginationOptions) => Promise<PaginationResult<Row>>;
|
|
480
|
+
take: (limit: number) => Promise<Row[]>;
|
|
481
|
+
/**
|
|
482
|
+
* Return the single matching document. Returns `null` when nothing matches
|
|
483
|
+
* and throws when more than one row matches. Mirrors Convex's `.unique()`.
|
|
484
|
+
*/
|
|
485
|
+
unique: () => Promise<Row | null>;
|
|
486
|
+
withIndex: (indexName: string, range?: (q: IndexRangeBuilder) => IndexRangeBuilder) => TableReader<Row>;
|
|
487
|
+
/**
|
|
488
|
+
* Restrict the query to a declared `.searchIndex()`. The builder's
|
|
489
|
+
* `.search(field, query)` runs a full-text match against the index's
|
|
490
|
+
* searchable field; `.eq(field, value)` narrows by a declared filter
|
|
491
|
+
* field. Results come back ordered by relevance — pair with `.take(n)`
|
|
492
|
+
* (`.paginate()` is not supported on a search query).
|
|
493
|
+
*/
|
|
494
|
+
withSearchIndex: (indexName: string, search: (q: SearchFilterBuilder) => SearchFilterBuilder) => TableReader<Row>;
|
|
495
|
+
}
|
|
496
|
+
interface IndexRangeBuilder {
|
|
497
|
+
eq: (field: string, value: unknown) => IndexRangeBuilder;
|
|
498
|
+
gt: (field: string, value: unknown) => IndexRangeBuilder;
|
|
499
|
+
gte: (field: string, value: unknown) => IndexRangeBuilder;
|
|
500
|
+
lt: (field: string, value: unknown) => IndexRangeBuilder;
|
|
501
|
+
lte: (field: string, value: unknown) => IndexRangeBuilder;
|
|
502
|
+
}
|
|
503
|
+
/** Builder passed to {@link TableReader.withSearchIndex}; mirrors Convex's search query. */
|
|
504
|
+
interface SearchFilterBuilder {
|
|
505
|
+
/** Narrow by a declared filter field (exact match). */
|
|
506
|
+
eq: (field: string, value: unknown) => SearchFilterBuilder;
|
|
507
|
+
/** Full-text match `query` against the index's searchable `field`. Call exactly once. */
|
|
508
|
+
search: (field: string, query: string) => SearchFilterBuilder;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Options shared by the batch-write methods (`insertMany`/`deleteMany`/
|
|
512
|
+
* `patchMany`) — a per-call payload cap. The default cap (500) rejects an
|
|
513
|
+
* oversized call up front so an accidental O(n²) or a payload past the Durable
|
|
514
|
+
* Object request limit fails loudly instead of degrading the mutation. Callers
|
|
515
|
+
* with larger sets should chunk their own loop or raise `limit`.
|
|
516
|
+
*/
|
|
517
|
+
interface BatchWriteOptions {
|
|
518
|
+
/** Reject the call when the batch size exceeds this value (default 500). */
|
|
519
|
+
limit?: number;
|
|
520
|
+
}
|
|
521
|
+
interface DatabaseWriter extends DatabaseReader {
|
|
522
|
+
delete: <T extends string>(id: Id<T>) => Promise<void>;
|
|
523
|
+
/**
|
|
524
|
+
* Delete many rows by id in one call. Each id is deleted through the full
|
|
525
|
+
* single-row pipeline (triggers + per-row RLS). The returned `deleted` is the
|
|
526
|
+
* number of ids **requested**, not the rows actually removed — an unknown or
|
|
527
|
+
* duplicated id is a silent no-op.
|
|
528
|
+
*
|
|
529
|
+
* **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
|
|
530
|
+
* BEGIN/COMMIT span, so a mid-batch failure (a later RLS denial or handler
|
|
531
|
+
* error) rolls back the whole mutation. (In an action there is no transaction
|
|
532
|
+
* span, so the prior deletes persist; the in-memory test harness mirrors the span.)
|
|
533
|
+
*/
|
|
534
|
+
deleteMany: <T extends string>(ids: ReadonlyArray<Id<T>>, options?: BatchWriteOptions) => Promise<{
|
|
535
|
+
deleted: number;
|
|
536
|
+
}>;
|
|
537
|
+
/**
|
|
538
|
+
* Insert a document, returning its server id.
|
|
539
|
+
*
|
|
540
|
+
* Pass `options.clientId` (a UUID) to key the row yourself — for an
|
|
541
|
+
* optimistic client that needs the persisted row to match the key it
|
|
542
|
+
* already rendered. It's validated for shape and still subject to the
|
|
543
|
+
* primary-key uniqueness constraint; omit it and the server mints the id.
|
|
544
|
+
*/
|
|
545
|
+
insert: <T extends string>(tableName: T, document: Record<string, unknown>, options?: {
|
|
546
|
+
clientId?: string;
|
|
547
|
+
}) => Promise<Id<T>>;
|
|
548
|
+
/**
|
|
549
|
+
* Insert many documents into one table in a single call, returning the
|
|
550
|
+
* minted ids in input order. Equivalent to a per-row `insert()` loop — each
|
|
551
|
+
* row gets defaults, validators, triggers, and a per-row RLS check — but the
|
|
552
|
+
* caller pays one round-trip instead of N.
|
|
553
|
+
*
|
|
554
|
+
* **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
|
|
555
|
+
* BEGIN/COMMIT span, so a mid-batch failure (an invalid or RLS-denied row)
|
|
556
|
+
* rolls back the whole mutation. (In an action there is no transaction span,
|
|
557
|
+
* so the prior inserts persist; the in-memory test harness mirrors the span.)
|
|
558
|
+
*/
|
|
559
|
+
insertMany: <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options?: BatchWriteOptions) => Promise<Id<T>[]>;
|
|
560
|
+
/**
|
|
561
|
+
* **Trusted** bulk insert: one multi-row `INSERT` that **skips per-row
|
|
562
|
+
* `.check()` validators and before/after triggers** for throughput on data you
|
|
563
|
+
* control (seed, migration, admin import). Defaults, ids, and every companion
|
|
564
|
+
* (search/aggregate/rank/CDC + live subscriptions) are still applied, so reads
|
|
565
|
+
* stay correct.
|
|
566
|
+
*
|
|
567
|
+
* It is **"unsafe" only in that it bypasses the validation/trigger pipeline** —
|
|
568
|
+
* RLS is **not** bypassed: secure-by-default and the table's insert policy still
|
|
569
|
+
* apply (the framework ships no RLS-bypassing writer). Pass `allowExplicitId` to
|
|
570
|
+
* preserve a supplied `_id` (import). Use only for data you trust; prefer
|
|
571
|
+
* `insertMany` for anything user-supplied.
|
|
572
|
+
*/
|
|
573
|
+
insertManyUnsafe: <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options?: BatchWriteOptions & {
|
|
574
|
+
allowExplicitId?: boolean;
|
|
575
|
+
}) => Promise<Id<T>[]>;
|
|
576
|
+
patch: <T extends string>(id: Id<T>, patch: Record<string, unknown>) => Promise<void>;
|
|
577
|
+
/**
|
|
578
|
+
* Patch many rows by id in one call. Each `{ id, patch }` is applied like a
|
|
579
|
+
* single `patch()` (per-row triggers + RLS).
|
|
580
|
+
*
|
|
581
|
+
* **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
|
|
582
|
+
* BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
|
|
583
|
+
* (In an action there is no transaction span, so the prior patches persist;
|
|
584
|
+
* the in-memory test harness mirrors the span.)
|
|
585
|
+
*/
|
|
586
|
+
patchMany: <T extends string>(patches: ReadonlyArray<{
|
|
587
|
+
id: Id<T>;
|
|
588
|
+
patch: Record<string, unknown>;
|
|
589
|
+
}>, options?: BatchWriteOptions) => Promise<void>;
|
|
590
|
+
replace: <T extends string>(id: Id<T>, document: Record<string, unknown>) => Promise<void>;
|
|
591
|
+
}
|
|
592
|
+
/** Authenticated identity surfaced into every context. */
|
|
593
|
+
interface AuthState {
|
|
594
|
+
getIdentity: () => Promise<Record<string, unknown> | null>;
|
|
595
|
+
readonly userId: string | null;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* A pending scheduled invocation as surfaced by {@link Scheduler.list} /
|
|
599
|
+
* {@link Scheduler.get}. A clean public mirror of `@lunora/scheduler`'s internal
|
|
600
|
+
* `ScheduleRecord` — re-declared here so the public ctx surface carries no
|
|
601
|
+
* dependency on the scheduler package's internal types.
|
|
602
|
+
*/
|
|
603
|
+
interface ScheduledJob {
|
|
604
|
+
args: Record<string, unknown>;
|
|
605
|
+
/** Number of dispatch attempts already made (absent until the first retry). */
|
|
606
|
+
attempts?: number;
|
|
607
|
+
/** When the job was enqueued (epoch ms). */
|
|
608
|
+
enqueuedAt: number;
|
|
609
|
+
functionPath: string;
|
|
610
|
+
id: string;
|
|
611
|
+
/** When the job is scheduled to fire (epoch ms). */
|
|
612
|
+
scheduledFor: number;
|
|
613
|
+
/** Routing hint forwarded so dispatch lands on the right shard. */
|
|
614
|
+
shardKey?: string;
|
|
615
|
+
}
|
|
616
|
+
interface Scheduler {
|
|
617
|
+
/** Cancel a pending job by id. `cancelled` is `false` when no such job exists. */
|
|
618
|
+
cancel: (id: string) => Promise<{
|
|
619
|
+
cancelled: boolean;
|
|
620
|
+
}>;
|
|
621
|
+
/** Resolve a single pending job by id, or `null` when absent. */
|
|
622
|
+
get: (id: string) => Promise<ScheduledJob | null>;
|
|
623
|
+
/** List all pending scheduled jobs. */
|
|
624
|
+
list: () => Promise<ScheduledJob[]>;
|
|
625
|
+
runAfter: (delayMs: number, functionPath: string, args?: Record<string, unknown>) => Promise<string>;
|
|
626
|
+
runAt: (timestampMs: number, functionPath: string, args?: Record<string, unknown>) => Promise<string>;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* A workflow instance's lifecycle status. Clean public mirror of
|
|
630
|
+
* `@lunora/workflow`'s `WorkflowInstanceStatus` (itself a mirror of Cloudflare's
|
|
631
|
+
* `WorkflowInstanceStatus`) — re-declared here so the ctx surface carries no
|
|
632
|
+
* dependency on the workflow package, exactly as {@link Scheduler} avoids a
|
|
633
|
+
* dependency on `@lunora/scheduler`.
|
|
634
|
+
*/
|
|
635
|
+
type WorkflowInstanceStatus = "complete" | "errored" | "paused" | "queued" | "running" | "terminated" | "unknown" | "waiting" | "waitingForPause";
|
|
636
|
+
/** Result of {@link WorkflowInstance.status}. Mirrors `@lunora/workflow`'s `WorkflowStatusResult`. */
|
|
637
|
+
interface WorkflowStatusResult {
|
|
638
|
+
error?: {
|
|
639
|
+
message: string;
|
|
640
|
+
name: string;
|
|
641
|
+
};
|
|
642
|
+
output?: unknown;
|
|
643
|
+
status: WorkflowInstanceStatus;
|
|
644
|
+
}
|
|
645
|
+
/** Options accepted by {@link WorkflowHandle.create}. Mirrors `@lunora/workflow`'s `WorkflowCreateOptions`. */
|
|
646
|
+
interface WorkflowCreateOptions<Params = Record<string, unknown>> {
|
|
647
|
+
/** Unique-within-the-workflow instance id. Generated by Cloudflare when omitted. */
|
|
648
|
+
id?: string;
|
|
649
|
+
/** The event payload the instance is triggered with — surfaced as `event.payload`. */
|
|
650
|
+
params?: Params;
|
|
651
|
+
/** Instance retention policy (defaults to the account maximum). */
|
|
652
|
+
retention?: {
|
|
653
|
+
errorRetention?: string;
|
|
654
|
+
successRetention?: string;
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
/** A live handle to a single workflow instance. Mirrors `@lunora/workflow`'s `WorkflowInstanceLike`. */
|
|
658
|
+
interface WorkflowInstance {
|
|
659
|
+
readonly id: string;
|
|
660
|
+
pause: () => Promise<void>;
|
|
661
|
+
restart: () => Promise<void>;
|
|
662
|
+
resume: () => Promise<void>;
|
|
663
|
+
sendEvent: (event: {
|
|
664
|
+
payload: unknown;
|
|
665
|
+
type: string;
|
|
666
|
+
}) => Promise<void>;
|
|
667
|
+
status: () => Promise<WorkflowStatusResult>;
|
|
668
|
+
terminate: () => Promise<void>;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* A typed handle to one declared workflow, addressable from `ctx.workflows`.
|
|
672
|
+
* Mirrors `@lunora/workflow`'s `WorkflowHandle`.
|
|
673
|
+
*/
|
|
674
|
+
interface WorkflowHandle<Params = Record<string, unknown>> {
|
|
675
|
+
/** Start a new instance (optionally with an id + params). */
|
|
676
|
+
create: (options?: WorkflowCreateOptions<Params>) => Promise<WorkflowInstance>;
|
|
677
|
+
/** Start many instances in one batched RPC. */
|
|
678
|
+
createBatch: (batch: ReadonlyArray<WorkflowCreateOptions<Params>>) => Promise<WorkflowInstance[]>;
|
|
679
|
+
/** Get a handle to an existing instance by id. */
|
|
680
|
+
get: (id: string) => Promise<WorkflowInstance>;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* The `ctx.workflows` surface on {@link MutationCtx} / {@link ActionCtx}. Each
|
|
684
|
+
* workflow declared in `lunora/workflows.ts` is reachable by its export name;
|
|
685
|
+
* codegen narrows the `get(name)` overloads to the known workflow names + their
|
|
686
|
+
* inferred param types. Mirrors `@lunora/workflow`'s `Workflows`.
|
|
687
|
+
*/
|
|
688
|
+
interface Workflows {
|
|
689
|
+
/** Resolve the handle for a declared workflow by export name. */
|
|
690
|
+
get: <Params = Record<string, unknown>>(name: string) => WorkflowHandle<Params>;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Structural projection of workers-types' `SecretsStoreSecret` binding — the
|
|
694
|
+
* per-secret `secrets_store_secrets[]` binding whose `.get()` resolves the
|
|
695
|
+
* secret value (or throws if it does not exist). Mirrored structurally so the
|
|
696
|
+
* runtime resolves it without a workerd type dependency.
|
|
697
|
+
*/
|
|
698
|
+
interface SecretsStoreSecretLike {
|
|
699
|
+
get: () => Promise<string>;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* `ctx.secrets` — read account-level secrets bound via Cloudflare Secrets Store.
|
|
703
|
+
* A core built-in (always present on every context, like `ctx.log`): a binding
|
|
704
|
+
* named in wrangler's `secrets_store_secrets[]` is read by its binding name.
|
|
705
|
+
*
|
|
706
|
+
* ```ts
|
|
707
|
+
* const apiKey = await ctx.secrets.get("STRIPE_KEY");
|
|
708
|
+
* ```
|
|
709
|
+
*
|
|
710
|
+
* The lookup is async (the platform fetches and decrypts on first read);
|
|
711
|
+
* reading an undeclared name throws a directed error naming the bound secrets.
|
|
712
|
+
*/
|
|
713
|
+
interface Secrets {
|
|
714
|
+
/** Resolve a Secrets Store secret by its wrangler binding name. */
|
|
715
|
+
get: (name: string) => Promise<string>;
|
|
716
|
+
}
|
|
717
|
+
/** Lifecycle phase relative to the SQL write. */
|
|
718
|
+
type TriggerTiming = "after" | "before";
|
|
719
|
+
/** The CRUD operation a trigger reacts to. `patch` and `replace` both map to `update`. */
|
|
720
|
+
type TriggerOp = "delete" | "insert" | "update";
|
|
721
|
+
/**
|
|
722
|
+
* A row as observed by a trigger handler: the table's `Shape` (with the same
|
|
723
|
+
* optionality rules as {@link InferArgs}) plus the system columns every stored
|
|
724
|
+
* doc carries.
|
|
725
|
+
*/
|
|
726
|
+
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]> } & {
|
|
727
|
+
readonly _creationTime: number;
|
|
728
|
+
readonly _id: string;
|
|
729
|
+
};
|
|
730
|
+
/** What an `insert` trigger observes: the freshly written row. */
|
|
731
|
+
interface TriggerInsertEvent<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
732
|
+
readonly doc: TriggerRow<Shape>;
|
|
733
|
+
readonly id: string;
|
|
734
|
+
readonly op: "insert";
|
|
735
|
+
readonly table: string;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* What an `update` trigger observes: the merged row plus the pre-write row.
|
|
739
|
+
* `previous` is typed as always present (the row must exist to be updated); the
|
|
740
|
+
* runtime supplies it best-effort and only omits it in the unreachable
|
|
741
|
+
* row-vanished-mid-write case.
|
|
742
|
+
*/
|
|
743
|
+
interface TriggerUpdateEvent<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
744
|
+
readonly doc: TriggerRow<Shape>;
|
|
745
|
+
readonly id: string;
|
|
746
|
+
readonly op: "update";
|
|
747
|
+
readonly previous: TriggerRow<Shape>;
|
|
748
|
+
readonly table: string;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* What a `delete` trigger observes: the row about to be (or just) removed.
|
|
752
|
+
* `previous` is typed as always present; the runtime supplies it best-effort
|
|
753
|
+
* and only omits it in the unreachable row-vanished-mid-write case.
|
|
754
|
+
*/
|
|
755
|
+
interface TriggerDeleteEvent<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
756
|
+
readonly id: string;
|
|
757
|
+
readonly op: "delete";
|
|
758
|
+
readonly previous: TriggerRow<Shape>;
|
|
759
|
+
readonly table: string;
|
|
760
|
+
}
|
|
761
|
+
/** Union of every trigger event, with the table `Shape` erased (as stored in `triggerMap`). */
|
|
762
|
+
type TriggerEvent = TriggerDeleteEvent | TriggerInsertEvent | TriggerUpdateEvent;
|
|
763
|
+
/** Page returned by {@link TriggerDatabase.findMany}; mirrors `@lunora/do`'s `QueryPage`. */
|
|
764
|
+
interface TriggerQueryPage {
|
|
765
|
+
continueCursor: null | string;
|
|
766
|
+
isDone: boolean;
|
|
767
|
+
page: Record<string, unknown>[];
|
|
768
|
+
}
|
|
769
|
+
/** Args accepted by {@link TriggerDatabase} reads; mirrors `@lunora/do`'s `QueryArgs`. */
|
|
770
|
+
interface TriggerQueryArgs {
|
|
771
|
+
cursor?: null | string;
|
|
772
|
+
limit?: number;
|
|
773
|
+
orderBy?: ReadonlyArray<unknown>;
|
|
774
|
+
where?: Record<string, unknown>;
|
|
775
|
+
with?: Record<string, unknown>;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Args accepted by {@link TriggerDatabase.aggregate} — structural mirror of
|
|
779
|
+
* `@lunora/do`'s `AggregateOptions`, kept local so trigger handlers in
|
|
780
|
+
* `@lunora/server` don't take a hard dep on the DO runtime.
|
|
781
|
+
*/
|
|
782
|
+
interface TriggerAggregateOptions {
|
|
783
|
+
baseWhere?: Record<string, unknown>;
|
|
784
|
+
field?: string;
|
|
785
|
+
op: AggregateOp;
|
|
786
|
+
restrictsCounts?: boolean;
|
|
787
|
+
where?: Record<string, unknown>;
|
|
788
|
+
}
|
|
789
|
+
/** Args accepted by {@link TriggerDatabase.groupBy}. */
|
|
790
|
+
interface TriggerGroupByOptions {
|
|
791
|
+
agg?: {
|
|
792
|
+
field?: string;
|
|
793
|
+
op: AggregateOp;
|
|
794
|
+
};
|
|
795
|
+
baseWhere?: Record<string, unknown>;
|
|
796
|
+
by: ReadonlyArray<string>;
|
|
797
|
+
restrictsCounts?: boolean;
|
|
798
|
+
where?: Record<string, unknown>;
|
|
799
|
+
}
|
|
800
|
+
/** One entry returned by {@link TriggerDatabase.groupBy}. */
|
|
801
|
+
interface TriggerGroupByEntry {
|
|
802
|
+
key: Record<string, unknown>;
|
|
803
|
+
value: null | number;
|
|
804
|
+
}
|
|
805
|
+
/** Args accepted by {@link TriggerDatabase.rank}. */
|
|
806
|
+
interface TriggerRankOptions {
|
|
807
|
+
baseWhere?: Record<string, unknown>;
|
|
808
|
+
restrictsCounts?: boolean;
|
|
809
|
+
/** Either the row id or the full row document. */
|
|
810
|
+
row: Record<string, unknown> | string;
|
|
811
|
+
where?: Record<string, unknown>;
|
|
812
|
+
}
|
|
813
|
+
/** Result of {@link TriggerDatabase.rank} — 1-based position + partition total. */
|
|
814
|
+
interface TriggerRankResult {
|
|
815
|
+
position: number;
|
|
816
|
+
total: number;
|
|
817
|
+
}
|
|
818
|
+
/** Args accepted by {@link TriggerDatabase.rankPage}. */
|
|
819
|
+
interface TriggerRankPageOptions {
|
|
820
|
+
baseWhere?: Record<string, unknown>;
|
|
821
|
+
cursor?: null | string;
|
|
822
|
+
take?: number;
|
|
823
|
+
where?: Record<string, unknown>;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Portable, table/id-addressed ORM writer handed to trigger handlers via
|
|
827
|
+
* `ctx.db`. Mirrors `@lunora/do`'s runtime `DatabaseWriterLike` surface — it is
|
|
828
|
+
* **not** the generated per-table `ctx.db.<table>` facade (which can't be typed
|
|
829
|
+
* from inside `defineTable`, where the full schema isn't known).
|
|
830
|
+
*
|
|
831
|
+
* `aggregate`/`groupBy`/`count`/`rank`/`rankPage` route through the same
|
|
832
|
+
* trigger-maintained counter and rank tables the user-facing reader uses, so
|
|
833
|
+
* a handler's `ctx.db.<table>.aggregate(...)` observes the just-staged write
|
|
834
|
+
* within the same DO transaction (the counter step happens before the trigger
|
|
835
|
+
* fires).
|
|
836
|
+
*/
|
|
837
|
+
interface TriggerDatabase {
|
|
838
|
+
aggregate: (tableName: string, options: TriggerAggregateOptions) => Promise<null | number>;
|
|
839
|
+
count: (tableName: string, where?: Record<string, unknown>) => Promise<number>;
|
|
840
|
+
delete: (id: string) => Promise<void>;
|
|
841
|
+
findFirst: (tableName: string, args?: TriggerQueryArgs) => Promise<Record<string, unknown> | null>;
|
|
842
|
+
findMany: (tableName: string, args?: TriggerQueryArgs) => Promise<TriggerQueryPage>;
|
|
843
|
+
get: (id: string) => Promise<Record<string, unknown> | null>;
|
|
844
|
+
groupBy: (tableName: string, options: TriggerGroupByOptions) => Promise<ReadonlyArray<TriggerGroupByEntry>>;
|
|
845
|
+
insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
|
|
846
|
+
patch: (id: string, patch: Record<string, unknown>) => Promise<void>;
|
|
847
|
+
rank: (tableName: string, indexName: string, options: TriggerRankOptions) => Promise<null | TriggerRankResult>;
|
|
848
|
+
rankPage: (tableName: string, indexName: string, options?: TriggerRankPageOptions) => Promise<TriggerQueryPage>;
|
|
849
|
+
replace: (id: string, document: Record<string, unknown>) => Promise<void>;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Handle injected into every trigger handler. `db` is the portable ORM writer;
|
|
853
|
+
* `scheduler` enqueues async / cross-shard follow-up work (cross-shard work is
|
|
854
|
+
* **not** transactional with the firing write).
|
|
855
|
+
*/
|
|
856
|
+
interface TriggerCtx {
|
|
857
|
+
readonly db: TriggerDatabase;
|
|
858
|
+
readonly scheduler: Scheduler;
|
|
859
|
+
}
|
|
860
|
+
/** A user-declared trigger handler. Throwing from a `before*` handler aborts the write. */
|
|
861
|
+
type TriggerHandler<Event> = (context: TriggerCtx, event: Event) => Promise<void> | void;
|
|
862
|
+
/**
|
|
863
|
+
* A single declared trigger, as stored in {@link TableDefinition.triggerMap}.
|
|
864
|
+
* The handler's event type is erased to the {@link TriggerEvent} union here; the
|
|
865
|
+
* per-op {@link TriggerBuilder} methods recover the precise event type for
|
|
866
|
+
* authors.
|
|
867
|
+
*/
|
|
868
|
+
interface TriggerDefinition {
|
|
869
|
+
readonly handler: TriggerHandler<TriggerEvent>;
|
|
870
|
+
readonly op: TriggerOp;
|
|
871
|
+
readonly timing: TriggerTiming;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* The `t` argument passed to `.triggers((t) => …)`. Each method binds a handler
|
|
875
|
+
* to one `timing`+`op` pair, typing the event against the table's `Shape`.
|
|
876
|
+
*/
|
|
877
|
+
interface TriggerBuilder<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
878
|
+
afterDelete: (handler: TriggerHandler<TriggerDeleteEvent<Shape>>) => TriggerDefinition;
|
|
879
|
+
afterInsert: (handler: TriggerHandler<TriggerInsertEvent<Shape>>) => TriggerDefinition;
|
|
880
|
+
afterUpdate: (handler: TriggerHandler<TriggerUpdateEvent<Shape>>) => TriggerDefinition;
|
|
881
|
+
beforeDelete: (handler: TriggerHandler<TriggerDeleteEvent<Shape>>) => TriggerDefinition;
|
|
882
|
+
beforeInsert: (handler: TriggerHandler<TriggerInsertEvent<Shape>>) => TriggerDefinition;
|
|
883
|
+
beforeUpdate: (handler: TriggerHandler<TriggerUpdateEvent<Shape>>) => TriggerDefinition;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Per-file metadata returned by {@link ReadOnlyStorage.getMetadata}. A clean
|
|
887
|
+
* public mirror of `@lunora/storage`'s `ObjectMetadata` — re-declared here so
|
|
888
|
+
* the ctx surface carries no dependency on the storage package's types. Matches
|
|
889
|
+
* the columns Convex surfaces for `ctx.storage.getMetadata` / `_storage`.
|
|
890
|
+
*/
|
|
891
|
+
interface StorageMetadata {
|
|
892
|
+
/** The object's `Content-Type`, when recorded. */
|
|
893
|
+
contentType?: string;
|
|
894
|
+
/** Custom metadata set at upload time, if any. */
|
|
895
|
+
customMetadata?: Record<string, string>;
|
|
896
|
+
/** The object's key. */
|
|
897
|
+
key: string;
|
|
898
|
+
/** Hex-encoded SHA-256 of the body, when R2 carries a checksum. */
|
|
899
|
+
sha256?: string;
|
|
900
|
+
/** Body length in bytes. */
|
|
901
|
+
size: number;
|
|
902
|
+
/** When the object was last written (epoch ms), when reported. */
|
|
903
|
+
uploaded?: number;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Read-only projection of `Storage` exposed on `QueryCtx` / `MutationCtx`.
|
|
907
|
+
*
|
|
908
|
+
* Queries are pure reads, and mutations run inside a transactional scope —
|
|
909
|
+
* neither is allowed to perform side-effectful R2 writes (`upload`) or
|
|
910
|
+
* deletes (`delete`). They can, however, **read** existing objects and
|
|
911
|
+
* resolve signed URLs (the URL signing itself is HMAC-only — no R2 round
|
|
912
|
+
* trip), so the read-only surface keeps `download` and `getSignedUrl`. The
|
|
913
|
+
* full {@link Storage} surface stays on `ActionCtx`.
|
|
914
|
+
*/
|
|
915
|
+
interface ReadOnlyStorage<Buckets extends string = string> {
|
|
916
|
+
/**
|
|
917
|
+
* Select a named bucket (declared via `v.storage("name")`). The returned
|
|
918
|
+
* accessor's operations target that bucket — `ctx.storage.bucket("avatars")
|
|
919
|
+
* .download(key)`. The bare `ctx.storage` targets the default bucket.
|
|
920
|
+
*/
|
|
921
|
+
bucket: (name: Buckets) => ReadOnlyStorage<Buckets>;
|
|
922
|
+
/** The bucket this accessor's operations target (the default for the bare `ctx.storage`). */
|
|
923
|
+
readonly bucketName: string;
|
|
924
|
+
/** Fetch the body of an existing object. Returns `null` when absent. */
|
|
925
|
+
download: (key: string) => Promise<ReadableStream | null>;
|
|
926
|
+
/**
|
|
927
|
+
* Read a file's metadata (size, content-type, sha256, upload time, custom
|
|
928
|
+
* metadata) without fetching its body. Returns `null` when the object is
|
|
929
|
+
* absent. Mirrors Convex's `ctx.storage.getMetadata`.
|
|
930
|
+
*/
|
|
931
|
+
getMetadata: (key: string) => Promise<StorageMetadata | null>;
|
|
932
|
+
/** Resolve a short-lived signed URL for an existing object. */
|
|
933
|
+
getSignedUrl: (key: string, options?: {
|
|
934
|
+
expiresInSeconds?: number;
|
|
935
|
+
}) => Promise<string>;
|
|
936
|
+
/** Public URL pointing at the configured base for `key`. */
|
|
937
|
+
getUrl: (key: string) => string;
|
|
938
|
+
}
|
|
939
|
+
interface Storage<Buckets extends string = string> extends ReadOnlyStorage<Buckets> {
|
|
940
|
+
/** Select a named bucket; the returned accessor exposes the full read/write surface. */
|
|
941
|
+
bucket: (name: Buckets) => Storage<Buckets>;
|
|
942
|
+
delete: (key: string) => Promise<void>;
|
|
943
|
+
/**
|
|
944
|
+
* Mint a short-lived signed `PUT` URL a client can upload directly to,
|
|
945
|
+
* optionally pinning the `Content-Type` the uploader must send. Mirrors
|
|
946
|
+
* Convex's `storage.generateUploadUrl`.
|
|
947
|
+
*/
|
|
948
|
+
generateUploadUrl: (key: string, options?: {
|
|
949
|
+
contentType?: string;
|
|
950
|
+
expiresInSeconds?: number;
|
|
951
|
+
}) => Promise<string>;
|
|
952
|
+
/**
|
|
953
|
+
* Upload `body` to `key` from the server, returning the stored object's key
|
|
954
|
+
* and etag. Mirrors Convex's `storage.store`. Accepts the same guard fields
|
|
955
|
+
* as `@lunora/storage`'s `UploadOptions` so `maxSize` /
|
|
956
|
+
* `allowedContentTypes` enforcement isn't lost behind the Convex-style alias.
|
|
957
|
+
*/
|
|
958
|
+
store: (key: string, body: ReadableStream | ArrayBuffer | Blob, options?: {
|
|
959
|
+
allowedContentTypes?: ReadonlyArray<string>;
|
|
960
|
+
contentType?: string;
|
|
961
|
+
customMetadata?: Record<string, string>;
|
|
962
|
+
maxSize?: number;
|
|
963
|
+
}) => Promise<{
|
|
964
|
+
etag: string;
|
|
965
|
+
key: string;
|
|
966
|
+
}>;
|
|
967
|
+
}
|
|
968
|
+
interface VectorMatch {
|
|
969
|
+
id: string;
|
|
970
|
+
metadata?: Record<string, unknown>;
|
|
971
|
+
score: number;
|
|
972
|
+
}
|
|
973
|
+
interface VectorMatches {
|
|
974
|
+
count: number;
|
|
975
|
+
matches: ReadonlyArray<VectorMatch>;
|
|
976
|
+
}
|
|
977
|
+
interface VectorQueryInput {
|
|
978
|
+
/** Embedder used when `input` is supplied instead of a precomputed `vector`. */
|
|
979
|
+
embed?: VectorEmbedder;
|
|
980
|
+
filter?: Record<string, unknown>;
|
|
981
|
+
/** Natural-language input embedded via `embed`. Ignored when `vector` is set. */
|
|
982
|
+
input?: string;
|
|
983
|
+
namespace?: string;
|
|
984
|
+
topK?: number;
|
|
985
|
+
/** Precomputed query vector; skips `embed`. */
|
|
986
|
+
vector?: ReadonlyArray<number>;
|
|
987
|
+
}
|
|
988
|
+
interface VectorUpsertInput {
|
|
989
|
+
embed: VectorEmbedder;
|
|
990
|
+
id: string;
|
|
991
|
+
input: string;
|
|
992
|
+
metadata?: Record<string, unknown>;
|
|
993
|
+
namespace?: string;
|
|
994
|
+
}
|
|
995
|
+
interface VectorRecord {
|
|
996
|
+
id: string;
|
|
997
|
+
metadata?: Record<string, unknown>;
|
|
998
|
+
values: ReadonlyArray<number>;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Read-only vector surface exposed on {@link QueryCtx}. Mirrors the read half
|
|
1002
|
+
* of `@lunora/bindings/vectors`' `LunoraVectors` so the live adapter is assignable.
|
|
1003
|
+
*/
|
|
1004
|
+
interface VectorSearchReader {
|
|
1005
|
+
getByIds: (indexName: string, ids: ReadonlyArray<string>) => Promise<ReadonlyArray<VectorRecord>>;
|
|
1006
|
+
query: (indexName: string, input: VectorQueryInput) => Promise<VectorMatches>;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Mutating vector surface on {@link MutationCtx} / {@link ActionCtx}. `upsert`
|
|
1010
|
+
* is queued post-commit by default; `upsertNow` forces a synchronous write.
|
|
1011
|
+
* `db.delete` on a vectorized table auto-propagates the matching `deleteByIds`.
|
|
1012
|
+
*/
|
|
1013
|
+
interface VectorSearch extends VectorSearchReader {
|
|
1014
|
+
deleteByIds: (indexName: string, ids: ReadonlyArray<string>) => Promise<void>;
|
|
1015
|
+
upsert: (indexName: string, input: VectorUpsertInput) => Promise<void>;
|
|
1016
|
+
upsertNow: (indexName: string, input: VectorUpsertInput) => Promise<void>;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Structured logger on every function `ctx`. Each call emits one attributed log
|
|
1020
|
+
* line — tagged with the function path on the server — that flows to an
|
|
1021
|
+
* `ObservabilitySink`'s `onLog` (where you route it in production) and, in
|
|
1022
|
+
* development, to the dev server terminal via the CLI / Vite plugin formatter.
|
|
1023
|
+
* Mirrors the `console` method names so it's a drop-in for `console.log` inside a
|
|
1024
|
+
* handler, but with attribution and a routable transport.
|
|
1025
|
+
*
|
|
1026
|
+
* Accepts any number of values per call, exactly like `console`; objects are
|
|
1027
|
+
* rendered into the human-readable message. The raw, un-rendered arguments are
|
|
1028
|
+
* preserved ONLY on the in-process `onLog` sink (which you opt into and control);
|
|
1029
|
+
* the rendered message — not the structured args — is what reaches the dev
|
|
1030
|
+
* terminal and the platform's Workers Logs.
|
|
1031
|
+
*
|
|
1032
|
+
* Attribution follows the dispatched function: a log emitted inside an internal
|
|
1033
|
+
* function invoked via `ctx.runQuery`/`runMutation`/`runAction` is attributed to
|
|
1034
|
+
* the outer request entrypoint, since the composed call reuses its context.
|
|
1035
|
+
*/
|
|
1036
|
+
interface LunoraLogger {
|
|
1037
|
+
readonly debug: (...args: unknown[]) => void;
|
|
1038
|
+
readonly error: (...args: unknown[]) => void;
|
|
1039
|
+
readonly info: (...args: unknown[]) => void;
|
|
1040
|
+
readonly log: (...args: unknown[]) => void;
|
|
1041
|
+
readonly warn: (...args: unknown[]) => void;
|
|
1042
|
+
}
|
|
1043
|
+
interface QueryCtx {
|
|
1044
|
+
readonly auth: AuthState;
|
|
1045
|
+
readonly db: DatabaseReader;
|
|
1046
|
+
/**
|
|
1047
|
+
* The caller's IP for this request — Cloudflare's trusted `CF-Connecting-IP`,
|
|
1048
|
+
* forwarded server-side (never read from a client header). `undefined` when
|
|
1049
|
+
* unknown: a live-subscription re-run, a server-initiated dispatch, or
|
|
1050
|
+
* non-Cloudflare hosting. A convenient rate-limit key for anonymous traffic.
|
|
1051
|
+
*/
|
|
1052
|
+
readonly ip?: string;
|
|
1053
|
+
/** Structured, function-attributed logger; see {@link LunoraLogger}. */
|
|
1054
|
+
readonly log: LunoraLogger;
|
|
1055
|
+
/**
|
|
1056
|
+
* Wall-clock time (epoch ms) the function began, captured once so the whole
|
|
1057
|
+
* handler sees a single stable value. Query/mutation handlers must be
|
|
1058
|
+
* deterministic — they may be re-run on OCC retry / subscription re-eval — so
|
|
1059
|
+
* read time through `ctx.now` instead of `Date.now()` (the latter is flagged
|
|
1060
|
+
* by the `nondeterministic_query_mutation` advisor). Actions may use `Date.now()`.
|
|
1061
|
+
*/
|
|
1062
|
+
readonly now: number;
|
|
1063
|
+
/**
|
|
1064
|
+
* Compose a read-only subquery in-process, reusing this query's read
|
|
1065
|
+
* context (same transaction, same `db`). Executes the referenced query's
|
|
1066
|
+
* handler directly — no fresh DO RPC round-trip — so it observes the exact
|
|
1067
|
+
* same snapshot. A query may only call other queries; there is no
|
|
1068
|
+
* `runMutation` on a `QueryCtx` (writes are not allowed from a query).
|
|
1069
|
+
* Mirrors Convex's `ctx.runQuery`.
|
|
1070
|
+
*/
|
|
1071
|
+
readonly runQuery: <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>) => Promise<R>;
|
|
1072
|
+
/** Read account-level secrets from Cloudflare Secrets Store; see {@link Secrets}. */
|
|
1073
|
+
readonly secrets: Secrets;
|
|
1074
|
+
readonly storage: ReadOnlyStorage;
|
|
1075
|
+
readonly vectors: VectorSearchReader;
|
|
1076
|
+
}
|
|
1077
|
+
interface MutationCtx {
|
|
1078
|
+
readonly auth: AuthState;
|
|
1079
|
+
readonly db: DatabaseWriter;
|
|
1080
|
+
/**
|
|
1081
|
+
* The caller's IP for this request — Cloudflare's trusted `CF-Connecting-IP`,
|
|
1082
|
+
* forwarded server-side (never read from a client header). `undefined` when
|
|
1083
|
+
* unknown: a live-subscription re-run, a server-initiated dispatch, or
|
|
1084
|
+
* non-Cloudflare hosting. A convenient rate-limit key for anonymous traffic.
|
|
1085
|
+
*/
|
|
1086
|
+
readonly ip?: string;
|
|
1087
|
+
/** Structured, function-attributed logger; see {@link LunoraLogger}. */
|
|
1088
|
+
readonly log: LunoraLogger;
|
|
1089
|
+
/**
|
|
1090
|
+
* Wall-clock time (epoch ms) the function began, captured once so the whole
|
|
1091
|
+
* handler sees a single stable value. Mutation handlers must be deterministic
|
|
1092
|
+
* — they may be re-run on OCC retry — so read time through `ctx.now` instead
|
|
1093
|
+
* of `Date.now()` (the latter is flagged by the `nondeterministic_query_mutation`
|
|
1094
|
+
* advisor). Actions may use `Date.now()`.
|
|
1095
|
+
*/
|
|
1096
|
+
readonly now: number;
|
|
1097
|
+
/**
|
|
1098
|
+
* Compose a submutation in-process, reusing this mutation's `db` writer.
|
|
1099
|
+
* Executes the referenced mutation's handler directly — no fresh DO RPC —
|
|
1100
|
+
* so its writes apply through the same shard invocation as the enclosing
|
|
1101
|
+
* mutation. Note: writes are not wrapped in a SQL transaction, so a partial
|
|
1102
|
+
* failure does not roll back earlier writes (the same as a top-level
|
|
1103
|
+
* mutation). Mirrors Convex's `ctx.runMutation`.
|
|
1104
|
+
*/
|
|
1105
|
+
readonly runMutation: <A extends ArgsValidator, R>(reference: RegisteredMutation<A, R>, args: InferArgs<A>) => Promise<R>;
|
|
1106
|
+
/**
|
|
1107
|
+
* Compose a read-only subquery in-process, reusing this mutation's `db`.
|
|
1108
|
+
* Executes the referenced query's handler directly — no fresh DO RPC — so
|
|
1109
|
+
* it observes this mutation's in-flight writes. Mirrors Convex's
|
|
1110
|
+
* `ctx.runQuery`.
|
|
1111
|
+
*/
|
|
1112
|
+
readonly runQuery: <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>) => Promise<R>;
|
|
1113
|
+
readonly scheduler: Scheduler;
|
|
1114
|
+
/** Read account-level secrets from Cloudflare Secrets Store; see {@link Secrets}. */
|
|
1115
|
+
readonly secrets: Secrets;
|
|
1116
|
+
readonly storage: ReadOnlyStorage;
|
|
1117
|
+
readonly vectors: VectorSearch;
|
|
1118
|
+
/** Start / resume / inspect durable workflows; see {@link Workflows}. */
|
|
1119
|
+
readonly workflows: Workflows;
|
|
1120
|
+
}
|
|
1121
|
+
interface ActionCtx {
|
|
1122
|
+
readonly auth: AuthState;
|
|
1123
|
+
readonly db: DatabaseWriter;
|
|
1124
|
+
readonly fetch: typeof globalThis.fetch;
|
|
1125
|
+
/**
|
|
1126
|
+
* The caller's IP for this request — Cloudflare's trusted `CF-Connecting-IP`,
|
|
1127
|
+
* forwarded server-side (never read from a client header). `undefined` when
|
|
1128
|
+
* unknown: a live-subscription re-run, a server-initiated dispatch, or
|
|
1129
|
+
* non-Cloudflare hosting. A convenient rate-limit key for anonymous traffic.
|
|
1130
|
+
*/
|
|
1131
|
+
readonly ip?: string;
|
|
1132
|
+
/** Structured, function-attributed logger; see {@link LunoraLogger}. */
|
|
1133
|
+
readonly log: LunoraLogger;
|
|
1134
|
+
/**
|
|
1135
|
+
* Wall-clock time (epoch ms) the action began, captured once for convenience
|
|
1136
|
+
* and parity with query/mutation `ctx.now`. Actions run exactly once, so they
|
|
1137
|
+
* may also use ambient `Date.now()` freely.
|
|
1138
|
+
*/
|
|
1139
|
+
readonly now: number;
|
|
1140
|
+
readonly runAction: <A extends ArgsValidator, R>(reference: RegisteredAction<A, R>, args: InferArgs<A>) => Promise<R>;
|
|
1141
|
+
readonly runMutation: <A extends ArgsValidator, R>(reference: RegisteredMutation<A, R>, args: InferArgs<A>) => Promise<R>;
|
|
1142
|
+
readonly runQuery: <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>) => Promise<R>;
|
|
1143
|
+
readonly scheduler: Scheduler;
|
|
1144
|
+
/** Read account-level secrets from Cloudflare Secrets Store; see {@link Secrets}. */
|
|
1145
|
+
readonly secrets: Secrets;
|
|
1146
|
+
readonly storage: Storage;
|
|
1147
|
+
readonly vectors: VectorSearch;
|
|
1148
|
+
/** Start / resume / inspect durable workflows; see {@link Workflows}. */
|
|
1149
|
+
readonly workflows: Workflows;
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Stand-in returned by codegen so projects can `import { api } from "./_generated/api"`.
|
|
1153
|
+
* The runtime value is opaque; the types are filled in by generated declarations.
|
|
1154
|
+
*/
|
|
1155
|
+
type AnyApi = Record<string, Record<string, RegisteredFunction<ArgsValidator, unknown, FunctionKind>>>;
|
|
1156
|
+
declare const anyApi: AnyApi;
|
|
1157
|
+
export { type ActionCtx, type AggregateIndexDefinition, type AggregateOp, type AnyApi, type ArgsValidator, type AuthState, 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, anyApi };
|