@lunora/codegen 1.0.0-alpha.4 → 1.0.0-alpha.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +6 -0
- package/__assets__/package-og.svg +1 -1
- package/dist/index.d.mts +1175 -25
- package/dist/index.d.ts +1175 -25
- package/dist/index.mjs +30 -24
- package/dist/packem_shared/{CONTAINERS_FILENAME-0K-pjNb8.mjs → CONTAINERS_FILENAME-DjpXMqhp.mjs} +1 -1
- package/dist/packem_shared/{CodegenDiagnosticError-54jWDxA9.mjs → CodegenDiagnosticError-DyQ5FwkM.mjs} +7 -5
- package/dist/packem_shared/FLAGS_FILENAME-fEZtzWXi.mjs +139 -0
- package/dist/packem_shared/GENERATED_HEADER-cUyu5VOU.mjs +3 -0
- package/dist/packem_shared/{LUNORA_ERROR_CODES-CySpQPD3.mjs → LUNORA_ERROR_CODES-DvTLozCu.mjs} +8 -1
- package/dist/packem_shared/MUTATORS_FILENAME-BZOfUhlY.mjs +81 -0
- package/dist/packem_shared/{OPENRPC_VERSION-C7zKVeKi.mjs → OPENRPC_VERSION-DxbqafcJ.mjs} +2 -2
- package/dist/packem_shared/QUEUES_FILENAME-Xv6Vy-Ku.mjs +145 -0
- package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-C1yHg38Z.mjs +3438 -0
- package/dist/packem_shared/{SCHEMA_SNAPSHOT_VERSION-DzLDbWk3.mjs → SCHEMA_SNAPSHOT_VERSION-D0ARY6rL.mjs} +18 -2
- package/dist/packem_shared/SHAPES_FILENAME-DOhPGi-6.mjs +94 -0
- package/dist/packem_shared/{WORKFLOWS_FILENAME-DRDQdhfq.mjs → WORKFLOWS_FILENAME-B2By8S4s.mjs} +70 -2
- package/dist/packem_shared/{buildOpenApiDocument-Cns1EkCQ.mjs → buildOpenApiDocument-_mwWOc2N.mjs} +2 -2
- package/dist/packem_shared/{discoverAuthApiCalls-C35R6z0T.mjs → discoverAuthApiCalls-WMx8L2FG.mjs} +1 -1
- package/dist/packem_shared/{discoverCrons-BL6iGuJ3.mjs → discoverCrons-D-jrpm97.mjs} +22 -20
- package/dist/packem_shared/{discoverFunctions-DEgAcRuD.mjs → discoverFunctions-CZ91aenA.mjs} +72 -14
- package/dist/packem_shared/{discoverHttpRoutes-C978pBiG.mjs → discoverHttpRoutes-DNZLDCmm.mjs} +2 -2
- package/dist/packem_shared/{discoverInserts-CRQdXvHO.mjs → discoverInserts-DFRJhmCv.mjs} +24 -2
- package/dist/packem_shared/{discoverMaskProcedures-B64zA740.mjs → discoverMaskProcedures-B5iFnZ-X.mjs} +58 -2
- package/dist/packem_shared/{discoverMigrations-Doj_-BAA.mjs → discoverMigrations-CdKtMlBc.mjs} +10 -4
- package/dist/packem_shared/{discoverNondeterministicCalls-4KiPQxQU.mjs → discoverNondeterministicCalls-C_PrQPkA.mjs} +1 -1
- package/dist/packem_shared/{discoverQueries-BkIi0dBD.mjs → discoverQueries-B8W9UPrt.mjs} +1 -1
- package/dist/packem_shared/{discoverR2sqlCalls-BpDqvcUn.mjs → discoverR2sqlCalls-Bm7Rfnf4.mjs} +1 -1
- package/dist/packem_shared/{discoverRlsMetadata-DpRB1HMe.mjs → discoverRlsMetadata-B4GL4ZKm.mjs} +1 -1
- package/dist/packem_shared/{discoverSchema-BBulgGbH.mjs → discoverSchema-DFWVgU5o.mjs} +349 -17
- package/dist/packem_shared/{discoverStorageRulesMetadata-DAqJUxUv.mjs → discoverStorageRulesMetadata-nosjYcvN.mjs} +1 -1
- package/dist/packem_shared/{GENERATED_HEADER-DF1hQcix.mjs → emit-CFz-9dcD.mjs} +1100 -122
- package/dist/packem_shared/{emitApp-Ci_hcJNO.mjs → emitApp-D8rVuM__.mjs} +99 -30
- package/dist/packem_shared/formatAdvisories-DRhEiJUz.mjs +114 -0
- package/dist/packem_shared/{parse-validator-tuQtHrsr.mjs → parse-validator-BSJo1HGP.mjs} +18 -6
- package/dist/packem_shared/redact-oTmsol5A.mjs +33 -0
- package/package.json +9 -7
- package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-C5UlpWKF.mjs +0 -922
- package/dist/packem_shared/formatAdvisories-8NIv1k0I.mjs +0 -69
|
@@ -1,27 +1,378 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
1
2
|
import { s as sanitizeNamespace } from './paths-BRd6JHuF.mjs';
|
|
2
3
|
|
|
4
|
+
const CAPABILITY_ROWS = [
|
|
5
|
+
// The middleware (`accessContext()` / `accessRoles()`) imports the `/context`
|
|
6
|
+
// and `/roles` subpaths, NOT the bare `@lunora/cloudflare-access` specifier —
|
|
7
|
+
// so the per-procedure middleware never trips the global `ctx.access` wiring.
|
|
8
|
+
// A handler reading `ctx.access` is the signal that wires it onto every ctx.
|
|
9
|
+
// `access` has a synchronous facade type, so its ctx field stays bespoke in
|
|
10
|
+
// `emit.ts` (no `serverCtxField` here).
|
|
11
|
+
{ contextProperty: "access", key: "access", moduleSpecifier: "@lunora/cloudflare-access" },
|
|
12
|
+
{
|
|
13
|
+
appMethod: { configKey: "ai", doc: "Override the Workers AI binding backing `ctx.ai` (defaults to `env.AI`).", method: "ai" },
|
|
14
|
+
contextProperty: "ai",
|
|
15
|
+
key: "ai",
|
|
16
|
+
moduleSpecifier: "@lunora/ai"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
appMethod: {
|
|
20
|
+
configKey: "analytics",
|
|
21
|
+
doc: "Override the Analytics Engine dataset backing `ctx.analytics` (defaults to `env.ANALYTICS`).",
|
|
22
|
+
method: "analytics"
|
|
23
|
+
},
|
|
24
|
+
contextProperty: "analytics",
|
|
25
|
+
key: "analytics",
|
|
26
|
+
moduleSpecifier: "@lunora/bindings/analytics",
|
|
27
|
+
// `ctx.analytics` — Analytics Engine write helper. EVERY ctx: a write-only,
|
|
28
|
+
// fire-and-forget side effect, not a determinism hazard for reads.
|
|
29
|
+
serverCtxField: {
|
|
30
|
+
field: `
|
|
31
|
+
/** Analytics Engine telemetry sink. Fire-and-forget and sampled; do not read it back in-handler. */
|
|
32
|
+
readonly analytics: import("@lunora/bindings/analytics").AnalyticsClient;`,
|
|
33
|
+
tier: "every"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
appMethod: {
|
|
38
|
+
configKey: "browser",
|
|
39
|
+
doc: "Override the Browser Rendering binding backing `ctx.browser` (defaults to `env.BROWSER`).",
|
|
40
|
+
method: "browser"
|
|
41
|
+
},
|
|
42
|
+
contextProperty: "browser",
|
|
43
|
+
key: "browser",
|
|
44
|
+
moduleSpecifier: "@lunora/browser",
|
|
45
|
+
// `ctx.browser` — Browser Rendering. ActionCtx ONLY: non-deterministic network I/O.
|
|
46
|
+
serverCtxField: {
|
|
47
|
+
field: `
|
|
48
|
+
/** Browser Rendering (screenshots/PDF/scrape). Non-deterministic — available only in actions. */
|
|
49
|
+
readonly browser: import("@lunora/browser").Browser;`,
|
|
50
|
+
tier: "action"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
// `lunora/containers.ts` imports `defineContainer` from `@lunora/container`,
|
|
54
|
+
// and handlers reach live instances via `ctx.containers` — either signals the
|
|
55
|
+
// app wires containers, so the studio should show the Containers page. The ctx
|
|
56
|
+
// field is a per-declaration emitter (kept bespoke), so no `serverCtxField`.
|
|
57
|
+
{ contextProperty: "containers", key: "container", moduleSpecifier: "@lunora/container" },
|
|
58
|
+
// `ctx.flags` — OpenFeature. Umbrella-aware specifier + its own provider
|
|
59
|
+
// client, so both the ctx field and the shard fragment stay bespoke.
|
|
60
|
+
{ contextProperty: "flags", key: "flags", moduleSpecifier: "@lunora/flags" },
|
|
61
|
+
{
|
|
62
|
+
appMethod: {
|
|
63
|
+
configKey: "sql",
|
|
64
|
+
doc: "Wire the Hyperdrive SQL client backing `ctx.sql` — build it with `createHyperdrive` + `fromPostgresJs`/`fromNodePg`/`fromMysql2`.",
|
|
65
|
+
method: "hyperdrive"
|
|
66
|
+
},
|
|
67
|
+
contextProperty: "sql",
|
|
68
|
+
key: "hyperdrive",
|
|
69
|
+
moduleSpecifier: "@lunora/hyperdrive",
|
|
70
|
+
// `ctx.sql` — Hyperdrive (external Postgres/MySQL). ActionCtx ONLY: external,
|
|
71
|
+
// non-deterministic I/O whose writes are invisible to Lunora live queries.
|
|
72
|
+
serverCtxField: {
|
|
73
|
+
field: `
|
|
74
|
+
/**
|
|
75
|
+
* External database access via Hyperdrive. Non-deterministic — available only in actions. Writes here are NOT tracked by Lunora live queries; subscriptions will not re-run on external DB changes.
|
|
76
|
+
*/
|
|
77
|
+
readonly sql: import("@lunora/hyperdrive").SqlClient;`,
|
|
78
|
+
tier: "action"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
appMethod: { configKey: "images", doc: "Override the Images binding backing `ctx.images` (defaults to `env.IMAGES`).", method: "images" },
|
|
83
|
+
contextProperty: "images",
|
|
84
|
+
key: "images",
|
|
85
|
+
moduleSpecifier: "@lunora/bindings/images",
|
|
86
|
+
// `ctx.images` — Cloudflare Images binding transforms. ActionCtx ONLY: non-deterministic compute/network I/O.
|
|
87
|
+
serverCtxField: {
|
|
88
|
+
field: `
|
|
89
|
+
/** Cloudflare Images transforms (resize/format/optimize). Non-deterministic — available only in actions. */
|
|
90
|
+
readonly images: import("@lunora/bindings/images").Images;`,
|
|
91
|
+
tier: "action"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
appMethod: { configKey: "kv", doc: "Override the Workers KV binding backing `ctx.kv` (defaults to `env.KV`).", method: "kv" },
|
|
96
|
+
contextProperty: "kv",
|
|
97
|
+
key: "kv",
|
|
98
|
+
moduleSpecifier: "@lunora/bindings/kv",
|
|
99
|
+
// `ctx.kv` — Workers KV. Typed on EVERY ctx (a KV read is allowed in a
|
|
100
|
+
// deterministic read path the way `ctx.db` is; the binding is user-named).
|
|
101
|
+
serverCtxField: { field: `
|
|
102
|
+
readonly kv: import("@lunora/bindings/kv").Kv;`, tier: "every" }
|
|
103
|
+
},
|
|
104
|
+
// `mail` is import-only — no `ctx.mail` helper (mail is reached through its own
|
|
105
|
+
// client), so only a `@lunora/mail` import flips it.
|
|
106
|
+
{ key: "mail", moduleSpecifier: "@lunora/mail" },
|
|
107
|
+
{
|
|
108
|
+
appMethod: { configKey: "payment", doc: "Wire the payment options backing `ctx.payments`.", method: "payment" },
|
|
109
|
+
contextProperty: "payments",
|
|
110
|
+
key: "payments",
|
|
111
|
+
moduleSpecifier: "@lunora/payment"
|
|
112
|
+
},
|
|
113
|
+
// `ctx.x402` — the x402 agent-wallet pay rail. ActionCtx ONLY: it signs and
|
|
114
|
+
// settles real USDC over the network per request. Like `payments`, its ctx
|
|
115
|
+
// field is bespoke (a lazily-built, per-run-metered rail), so no
|
|
116
|
+
// `serverCtxField` — `emit.ts` hand-wires it.
|
|
117
|
+
{
|
|
118
|
+
appMethod: {
|
|
119
|
+
configKey: "x402",
|
|
120
|
+
doc: "Wire the x402 agent-wallet pay rail backing `ctx.x402` — a payment-enabled `fetch` that answers `402` challenges under a mandatory spend policy (ActionCtx-only; spends real funds).",
|
|
121
|
+
method: "x402"
|
|
122
|
+
},
|
|
123
|
+
contextProperty: "x402",
|
|
124
|
+
key: "x402",
|
|
125
|
+
moduleSpecifier: "@lunora/x402/pay"
|
|
126
|
+
},
|
|
127
|
+
// Pipelines is its own `@lunora/bindings/pipelines` subpath (distinct from
|
|
128
|
+
// `/analytics`), so a real import is a clean signal that won't be flipped by a
|
|
129
|
+
// plain analytics import; `ctx.pipelines` reads flip it too.
|
|
130
|
+
{
|
|
131
|
+
contextProperty: "pipelines",
|
|
132
|
+
key: "pipelines",
|
|
133
|
+
moduleSpecifier: "@lunora/bindings/pipelines",
|
|
134
|
+
// `ctx.pipelines` — Pipelines (R2-backed) ingestion sink. ActionCtx ONLY
|
|
135
|
+
// (write-only fire-and-forget, but external I/O — kept off query/mutation).
|
|
136
|
+
serverCtxField: {
|
|
137
|
+
field: `
|
|
138
|
+
/** Pipelines ingestion sink (durable, R2-backed). Fire-and-forget and batched; do not read it back in-handler. */
|
|
139
|
+
readonly pipelines: import("@lunora/bindings/pipelines").PipelineClient;`,
|
|
140
|
+
tier: "action"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
appMethod: {
|
|
145
|
+
configKey: "r2sql",
|
|
146
|
+
doc: "Wire the R2 SQL client backing `ctx.r2sql` — build it with `createR2Sql({ accountId, apiToken, bucket })` (defaults to env `R2_SQL_TOKEN` / `R2_SQL_ACCOUNT_ID` / `R2_SQL_BUCKET`).",
|
|
147
|
+
method: "r2sql"
|
|
148
|
+
},
|
|
149
|
+
contextProperty: "r2sql",
|
|
150
|
+
key: "r2sql",
|
|
151
|
+
moduleSpecifier: "@lunora/bindings/r2sql",
|
|
152
|
+
// `ctx.r2sql` — R2 SQL (serverless query engine over Apache Iceberg tables).
|
|
153
|
+
// ActionCtx ONLY: external REST I/O, non-deterministic, and non-reactive
|
|
154
|
+
// (reads are not tracked by Lunora live queries).
|
|
155
|
+
serverCtxField: {
|
|
156
|
+
field: `
|
|
157
|
+
/**
|
|
158
|
+
* R2 SQL over Apache Iceberg tables (window functions, DISTINCT, set operations). Non-deterministic — available only in actions. Reads here are NOT tracked by Lunora live queries.
|
|
159
|
+
*/
|
|
160
|
+
readonly r2sql: import("@lunora/bindings/r2sql").R2SqlClient;`,
|
|
161
|
+
tier: "action"
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{ contextProperty: "scheduler", key: "scheduler", moduleSpecifier: "@lunora/scheduler" },
|
|
165
|
+
{ contextProperty: "storage", key: "storage", moduleSpecifier: "@lunora/storage" },
|
|
166
|
+
{
|
|
167
|
+
appMethod: { configKey: "vectors", doc: "Wire the Vectorize index map backing `ctx.vectors`.", method: "vectors" },
|
|
168
|
+
contextProperty: "vectors",
|
|
169
|
+
key: "vectors",
|
|
170
|
+
moduleSpecifier: "@lunora/bindings/vectors"
|
|
171
|
+
},
|
|
172
|
+
{ contextProperty: "workflows", key: "workflows", moduleSpecifier: "@lunora/workflow" }
|
|
173
|
+
];
|
|
174
|
+
const CAPABILITIES = CAPABILITY_ROWS;
|
|
175
|
+
const SERVER_CTX_FIELDS = new Map(
|
|
176
|
+
CAPABILITIES.flatMap((capability) => capability.serverCtxField ? [[capability.key, capability.serverCtxField]] : [])
|
|
177
|
+
);
|
|
178
|
+
const APP_METHOD_CAPABILITIES = CAPABILITY_ROWS.flatMap(
|
|
179
|
+
(capability) => "appMethod" in capability ? [{ appMethod: capability.appMethod, key: capability.key }] : []
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const LITERAL_VALUE_RE = /^(?:"(?:[^"\\]|\\(?:["\\/bfnrt]|u[0-9A-Fa-f]{4}))*"|'[^'\\]*'|-?\d+(?:\.\d+)?|true|false|null)$/u;
|
|
183
|
+
|
|
184
|
+
const PASS_THROUGH_KINDS = /* @__PURE__ */ new Set(["any", "bigint", "boolean", "date", "id", "null", "number", "storage", "string", "timestamp"]);
|
|
185
|
+
const hasColumnModifier = (node) => node.column !== void 0;
|
|
186
|
+
const emitScalarGuard = (kind, inExpr) => {
|
|
187
|
+
switch (kind) {
|
|
188
|
+
case "bigint": {
|
|
189
|
+
return `if (typeof ${inExpr} !== "bigint") return DEFER;
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
case "boolean": {
|
|
193
|
+
return `if (typeof ${inExpr} !== "boolean") return DEFER;
|
|
194
|
+
`;
|
|
195
|
+
}
|
|
196
|
+
case "date":
|
|
197
|
+
case "number":
|
|
198
|
+
case "timestamp": {
|
|
199
|
+
return `if (typeof ${inExpr} !== "number" || !Number.isFinite(${inExpr})) return DEFER;
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
case "null": {
|
|
203
|
+
return `if (${inExpr} !== null) return DEFER;
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
206
|
+
// string / id / storage all parse as a bare string at runtime.
|
|
207
|
+
default: {
|
|
208
|
+
return `if (typeof ${inExpr} !== "string") return DEFER;
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const compileLiteral = (node, inExpr) => {
|
|
214
|
+
const literal = node.literalValue?.trim();
|
|
215
|
+
if (literal === void 0 || !LITERAL_VALUE_RE.test(literal)) {
|
|
216
|
+
return void 0;
|
|
217
|
+
}
|
|
218
|
+
return { out: inExpr, pre: `if (${inExpr} !== ${literal}) return DEFER;
|
|
219
|
+
` };
|
|
220
|
+
};
|
|
221
|
+
const compileNode = (node, inExpr, context) => {
|
|
222
|
+
if (hasColumnModifier(node)) {
|
|
223
|
+
return void 0;
|
|
224
|
+
}
|
|
225
|
+
if (node.hasRefinement || node.sourceText !== void 0) {
|
|
226
|
+
return void 0;
|
|
227
|
+
}
|
|
228
|
+
if (PASS_THROUGH_KINDS.has(node.kind)) {
|
|
229
|
+
if (node.kind === "any") {
|
|
230
|
+
return { out: inExpr, pre: "" };
|
|
231
|
+
}
|
|
232
|
+
return { out: inExpr, pre: emitScalarGuard(node.kind, inExpr) };
|
|
233
|
+
}
|
|
234
|
+
switch (node.kind) {
|
|
235
|
+
case "array": {
|
|
236
|
+
return compileArray(node, inExpr, context);
|
|
237
|
+
}
|
|
238
|
+
case "literal": {
|
|
239
|
+
return compileLiteral(node, inExpr);
|
|
240
|
+
}
|
|
241
|
+
case "object": {
|
|
242
|
+
return compileObject(node, inExpr, context);
|
|
243
|
+
}
|
|
244
|
+
default: {
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
const compileArray = (node, inExpr, context) => {
|
|
250
|
+
const { inner } = node;
|
|
251
|
+
if (!inner) {
|
|
252
|
+
return void 0;
|
|
253
|
+
}
|
|
254
|
+
const id = context.next();
|
|
255
|
+
const array = `__arr${String(id)}`;
|
|
256
|
+
const index = `__i${String(id)}`;
|
|
257
|
+
const element = `__e${String(id)}`;
|
|
258
|
+
const innerEmit = compileNode(inner, element, context);
|
|
259
|
+
if (!innerEmit) {
|
|
260
|
+
return void 0;
|
|
261
|
+
}
|
|
262
|
+
const pre = `if (!Array.isArray(${inExpr})) return DEFER;
|
|
263
|
+
const ${array} = new Array(${inExpr}.length);
|
|
264
|
+
for (let ${index} = 0; ${index} < ${inExpr}.length; ${index}++) {
|
|
265
|
+
const ${element} = ${inExpr}[${index}];
|
|
266
|
+
${innerEmit.pre}${array}[${index}] = ${innerEmit.out};
|
|
267
|
+
}
|
|
268
|
+
`;
|
|
269
|
+
return { out: array, pre };
|
|
270
|
+
};
|
|
271
|
+
const compileField = (key, node, access, context) => {
|
|
272
|
+
const keyLiteral = JSON.stringify(key);
|
|
273
|
+
if (node.kind === "optional") {
|
|
274
|
+
if (node.hasRefinement || node.sourceText !== void 0 || hasColumnModifier(node)) {
|
|
275
|
+
return void 0;
|
|
276
|
+
}
|
|
277
|
+
const { inner } = node;
|
|
278
|
+
if (!inner) {
|
|
279
|
+
return void 0;
|
|
280
|
+
}
|
|
281
|
+
const innerEmit = compileNode(inner, access, context);
|
|
282
|
+
if (!innerEmit) {
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
285
|
+
const id = context.next();
|
|
286
|
+
const has = `__has${String(id)}`;
|
|
287
|
+
const value = `__val${String(id)}`;
|
|
288
|
+
const pre = `let ${has} = false;
|
|
289
|
+
let ${value};
|
|
290
|
+
if (${access} !== undefined) {
|
|
291
|
+
${innerEmit.pre}${value} = ${innerEmit.out};
|
|
292
|
+
${has} = true;
|
|
293
|
+
}
|
|
294
|
+
`;
|
|
295
|
+
return { entry: `...(${has} ? { ${keyLiteral}: ${value} } : {})`, pre };
|
|
296
|
+
}
|
|
297
|
+
const emit = compileNode(node, access, context);
|
|
298
|
+
if (!emit) {
|
|
299
|
+
return void 0;
|
|
300
|
+
}
|
|
301
|
+
return { entry: `${keyLiteral}: ${emit.out}`, pre: emit.pre };
|
|
302
|
+
};
|
|
303
|
+
const compileFields = (shape, accessFor, context) => {
|
|
304
|
+
let pre = "";
|
|
305
|
+
const entries = [];
|
|
306
|
+
for (const key of Object.keys(shape)) {
|
|
307
|
+
const node = shape[key];
|
|
308
|
+
if (!node) {
|
|
309
|
+
return void 0;
|
|
310
|
+
}
|
|
311
|
+
const field = compileField(key, node, accessFor(key), context);
|
|
312
|
+
if (!field) {
|
|
313
|
+
return void 0;
|
|
314
|
+
}
|
|
315
|
+
pre += field.pre;
|
|
316
|
+
entries.push(field.entry);
|
|
317
|
+
}
|
|
318
|
+
return { entries: entries.join(", "), pre };
|
|
319
|
+
};
|
|
320
|
+
const compileObject = (node, inExpr, context) => {
|
|
321
|
+
const shape = node.shape ?? {};
|
|
322
|
+
const fields = compileFields(shape, (key) => `${inExpr}[${JSON.stringify(key)}]`, context);
|
|
323
|
+
if (!fields) {
|
|
324
|
+
return void 0;
|
|
325
|
+
}
|
|
326
|
+
const id = context.next();
|
|
327
|
+
const object = `__obj${String(id)}`;
|
|
328
|
+
const pre = `if (typeof ${inExpr} !== "object" || ${inExpr} === null || Array.isArray(${inExpr})) return DEFER;
|
|
329
|
+
${fields.pre}const ${object} = { ${fields.entries} };
|
|
330
|
+
`;
|
|
331
|
+
return { out: object, pre };
|
|
332
|
+
};
|
|
333
|
+
const compileArgsValidator = (args) => {
|
|
334
|
+
let counter = 0;
|
|
335
|
+
const context = {
|
|
336
|
+
next: () => {
|
|
337
|
+
counter += 1;
|
|
338
|
+
return counter;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
const fields = compileFields(args, (key) => `source[${JSON.stringify(key)}]`, context);
|
|
342
|
+
if (!fields) {
|
|
343
|
+
return void 0;
|
|
344
|
+
}
|
|
345
|
+
return `(source) => {
|
|
346
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) return DEFER;
|
|
347
|
+
${fields.pre}return { ${fields.entries} };
|
|
348
|
+
}`;
|
|
349
|
+
};
|
|
350
|
+
|
|
3
351
|
const GENERATED_HEADER = "// GENERATED by @lunora/codegen — do not edit.\n// Run `lunora codegen` to regenerate.\n\n";
|
|
4
352
|
const baseSpecifiers = (useUmbrella = false) => useUmbrella ? {
|
|
5
353
|
client: "lunorash/client",
|
|
6
354
|
do: "lunorash/do",
|
|
355
|
+
flags: "lunorash/flags",
|
|
7
356
|
server: "lunorash/server",
|
|
8
357
|
serverDataModel: "lunorash/server/data-model",
|
|
9
358
|
serverDrizzle: "lunorash/server/drizzle",
|
|
10
|
-
serverTypes: "lunorash/server/types"
|
|
359
|
+
serverTypes: "lunorash/server/types",
|
|
360
|
+
values: "lunorash/values"
|
|
11
361
|
} : {
|
|
12
362
|
client: "@lunora/client",
|
|
13
363
|
do: "@lunora/do",
|
|
364
|
+
flags: "@lunora/flags",
|
|
14
365
|
server: "@lunora/server",
|
|
15
366
|
serverDataModel: "@lunora/server/data-model",
|
|
16
367
|
serverDrizzle: "@lunora/server/drizzle",
|
|
17
|
-
serverTypes: "@lunora/server/types"
|
|
368
|
+
serverTypes: "@lunora/server/types",
|
|
369
|
+
values: "@lunora/values"
|
|
18
370
|
};
|
|
19
371
|
const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/u;
|
|
20
|
-
const LITERAL_VALUE_RE = /^(?:"[^"\\]*"|'[^'\\]*'|-?\d+(?:\.\d+)?|true|false|null)$/u;
|
|
21
372
|
const IMPORT_PATH_RE = /^[\w./-]+$/u;
|
|
22
373
|
const assertIdentifier = (value, context) => {
|
|
23
374
|
if (!IDENTIFIER_RE.test(value)) {
|
|
24
|
-
throw new
|
|
375
|
+
throw new LunoraError("INTERNAL", `@lunora/codegen: ${context} is not a valid JS identifier: ${JSON.stringify(value)}`);
|
|
25
376
|
}
|
|
26
377
|
};
|
|
27
378
|
const renderPropertyKey = (fieldName) => IDENTIFIER_RE.test(fieldName) ? fieldName : JSON.stringify(fieldName);
|
|
@@ -48,7 +399,10 @@ const literalToType = (value) => {
|
|
|
48
399
|
return "unknown";
|
|
49
400
|
}
|
|
50
401
|
if (!LITERAL_VALUE_RE.test(value)) {
|
|
51
|
-
throw new
|
|
402
|
+
throw new LunoraError(
|
|
403
|
+
"INTERNAL",
|
|
404
|
+
`@lunora/codegen: v.literal() argument is not a parseable string/number/boolean/null literal: ${JSON.stringify(value)}`
|
|
405
|
+
);
|
|
52
406
|
}
|
|
53
407
|
return value;
|
|
54
408
|
};
|
|
@@ -430,21 +784,37 @@ import type { InsertModel } from "./dataModel.js";
|
|
|
430
784
|
export const createSeedClient = (options?: SeedClientOptions): SeedClient<InsertModel> => createSeedClientBase<InsertModel>(schema, options);
|
|
431
785
|
`;
|
|
432
786
|
};
|
|
787
|
+
const emitCollections = (shapes, hasDatabase, useUmbrella = false) => {
|
|
788
|
+
if (shapes.length === 0 || !hasDatabase) {
|
|
789
|
+
return "";
|
|
790
|
+
}
|
|
791
|
+
const base = baseSpecifiers(useUmbrella);
|
|
792
|
+
const factories = shapes.map((shape) => {
|
|
793
|
+
assertIdentifier(shape.exportName, "shape export name");
|
|
794
|
+
return `/** Live collection for the \`${shape.exportName}\` replication shape — pass the shape's validated \`args\` (its partition selector). */
|
|
795
|
+
export const ${shape.exportName}Collection = (client: LunoraClient, args?: Record<string, unknown>): Collection<Row, string> =>
|
|
796
|
+
createCollection(lunoraCollectionOptions({ client, shape: { args, name: "${shape.exportName}" } }).config);`;
|
|
797
|
+
}).join("\n\n");
|
|
798
|
+
return `${GENERATED_HEADER}import type { LunoraClient } from "${base.client}";
|
|
799
|
+
import { lunoraCollectionOptions } from "@lunora/db/collections";
|
|
800
|
+
import type { Row } from "@lunora/db";
|
|
801
|
+
import type { Collection } from "@tanstack/db";
|
|
802
|
+
import { createCollection } from "@tanstack/db";
|
|
803
|
+
|
|
804
|
+
${factories}
|
|
805
|
+
`;
|
|
806
|
+
};
|
|
433
807
|
const moduleAlias = (filePath, index) => `lunora_${sanitizeNamespace(filePath)}_${String(index)}`;
|
|
434
808
|
const CALL_REGISTERED_HELPER = `const callRegistered = async <R>(context: CallerCtx, functionPath: string, args: Record<string, unknown> | undefined): Promise<R> => {
|
|
435
809
|
const registered = LUNORA_FUNCTIONS[functionPath];
|
|
436
810
|
|
|
437
811
|
if (!registered) {
|
|
438
|
-
throw
|
|
439
|
-
name: "LunoraError",
|
|
440
|
-
code: "FUNCTION_NOT_FOUND",
|
|
441
|
-
status: 404,
|
|
442
|
-
});
|
|
812
|
+
throw new LunoraError("FUNCTION_NOT_FOUND", \`function not registered: \${functionPath}\`);
|
|
443
813
|
}
|
|
444
814
|
|
|
445
815
|
return (await registered.handler(context, args ?? {})) as R;
|
|
446
816
|
};`;
|
|
447
|
-
const renderFunctionRegistry = (functions, migrations) => {
|
|
817
|
+
const renderFunctionRegistry = (functions, migrations, mutators = [], shapes = []) => {
|
|
448
818
|
const aliasByPath = /* @__PURE__ */ new Map();
|
|
449
819
|
const registerPath = (filePath) => {
|
|
450
820
|
if (!aliasByPath.has(filePath)) {
|
|
@@ -457,9 +827,15 @@ const renderFunctionRegistry = (functions, migrations) => {
|
|
|
457
827
|
for (const migration of migrations) {
|
|
458
828
|
registerPath(migration.filePath);
|
|
459
829
|
}
|
|
830
|
+
for (const mutator of mutators) {
|
|
831
|
+
registerPath(mutator.filePath);
|
|
832
|
+
}
|
|
833
|
+
for (const shape of shapes) {
|
|
834
|
+
registerPath(shape.filePath);
|
|
835
|
+
}
|
|
460
836
|
const importLines = [...aliasByPath.entries()].map(([filePath, alias]) => {
|
|
461
837
|
if (!IMPORT_PATH_RE.test(filePath)) {
|
|
462
|
-
throw new
|
|
838
|
+
throw new LunoraError("INTERNAL", `@lunora/codegen: refusing to emit import for unsafe file path: ${JSON.stringify(filePath)}`);
|
|
463
839
|
}
|
|
464
840
|
return `import * as ${alias} from "../${filePath}.js";`;
|
|
465
841
|
}).join("\n");
|
|
@@ -469,15 +845,37 @@ const renderFunctionRegistry = (functions, migrations) => {
|
|
|
469
845
|
const migrationEntries = migrations.map(
|
|
470
846
|
(migration) => ` ${JSON.stringify(migration.id)}: ${aliasByPath.get(migration.filePath) ?? ""}.${migration.exportName} as unknown as RegisteredDataMigration,`
|
|
471
847
|
).join("\n");
|
|
848
|
+
const mutatorEntries = mutators.map(
|
|
849
|
+
(mutator) => ` "${sanitizeNamespace(mutator.filePath)}:${mutator.exportName}": ${aliasByPath.get(mutator.filePath) ?? ""}.${mutator.exportName} as unknown as RegisteredLunoraFunction,`
|
|
850
|
+
).join("\n");
|
|
851
|
+
const shapeEntries = shapes.map((shape) => ` "${shape.exportName}": ${aliasByPath.get(shape.filePath) ?? ""}.${shape.exportName} as unknown as RegisteredShape,`).join("\n");
|
|
852
|
+
const mutatorPaths = mutators.map((mutator) => `${sanitizeNamespace(mutator.filePath)}:${mutator.exportName}`);
|
|
853
|
+
const combinedDispatch = [dispatchEntries, mutatorEntries].filter((entries) => entries.length > 0).join("\n");
|
|
854
|
+
const installLines = functions.map((definition) => {
|
|
855
|
+
if (definition.lifecycle || Object.keys(definition.args).length === 0) {
|
|
856
|
+
return void 0;
|
|
857
|
+
}
|
|
858
|
+
const compiled = compileArgsValidator(definition.args);
|
|
859
|
+
if (compiled === void 0) {
|
|
860
|
+
return void 0;
|
|
861
|
+
}
|
|
862
|
+
const alias = aliasByPath.get(definition.filePath) ?? "";
|
|
863
|
+
return `installCompiledValidatorMap(${alias}.${definition.exportName}.args, ${compiled});`;
|
|
864
|
+
}).filter((line) => line !== void 0).join("\n");
|
|
472
865
|
return {
|
|
473
|
-
dispatchBody:
|
|
474
|
-
${
|
|
866
|
+
dispatchBody: combinedDispatch.length > 0 ? `
|
|
867
|
+
${combinedDispatch}
|
|
475
868
|
` : "",
|
|
476
869
|
importBlock: importLines.length > 0 ? `${importLines}
|
|
477
870
|
|
|
478
871
|
` : "",
|
|
872
|
+
installBlock: installLines,
|
|
479
873
|
migrationBody: migrationEntries.length > 0 ? `
|
|
480
874
|
${migrationEntries}
|
|
875
|
+
` : "",
|
|
876
|
+
mutatorPaths,
|
|
877
|
+
shapeBody: shapeEntries.length > 0 ? `
|
|
878
|
+
${shapeEntries}
|
|
481
879
|
` : ""
|
|
482
880
|
};
|
|
483
881
|
};
|
|
@@ -531,15 +929,21 @@ const buildStorageBucketNames = (schema, ruleBuckets = []) => {
|
|
|
531
929
|
};
|
|
532
930
|
const emitServer = ({
|
|
533
931
|
containers = [],
|
|
932
|
+
env,
|
|
933
|
+
hasAccessFacade = false,
|
|
534
934
|
hasAi = false,
|
|
535
935
|
hasAnalytics = false,
|
|
536
936
|
hasBrowser = false,
|
|
937
|
+
hasFlags = false,
|
|
537
938
|
hasHyperdrive = false,
|
|
538
939
|
hasImages = false,
|
|
539
940
|
hasKv = false,
|
|
540
941
|
hasPayments = false,
|
|
541
942
|
hasPipelines = false,
|
|
542
943
|
hasR2sql = false,
|
|
944
|
+
hasX402 = false,
|
|
945
|
+
identity,
|
|
946
|
+
queues = [],
|
|
543
947
|
schema,
|
|
544
948
|
storageRuleBuckets = [],
|
|
545
949
|
useUmbrella = false,
|
|
@@ -555,6 +959,10 @@ const emitServer = ({
|
|
|
555
959
|
` : "";
|
|
556
960
|
const paymentsActionField = hasPayments ? `
|
|
557
961
|
readonly payments: LunoraPayment;` : "";
|
|
962
|
+
const x402TypeImport = hasX402 ? `import type { X402Pay } from "@lunora/x402/pay";
|
|
963
|
+
` : "";
|
|
964
|
+
const x402ActionField = hasX402 ? `
|
|
965
|
+
readonly x402: X402Pay;` : "";
|
|
558
966
|
const containersTypeImport = containers.length > 0 ? `import type { ContainerAccessor } from "@lunora/container";
|
|
559
967
|
` : "";
|
|
560
968
|
const containersActionField = containers.length > 0 ? `
|
|
@@ -573,6 +981,11 @@ const emitServer = ({
|
|
|
573
981
|
assertIdentifier(workflow.bindingName, `workflow binding "${workflow.bindingName}"`);
|
|
574
982
|
return ` /** Workflow binding for the \`${workflow.exportName}\` workflow. */
|
|
575
983
|
readonly ${workflow.bindingName}?: unknown;`;
|
|
984
|
+
}),
|
|
985
|
+
...queues.map((queue) => {
|
|
986
|
+
assertIdentifier(queue.bindingName, `queue binding "${queue.bindingName}"`);
|
|
987
|
+
return ` /** Queue producer binding for the \`${queue.exportName}\` queue. */
|
|
988
|
+
readonly ${queue.bindingName}?: unknown;`;
|
|
576
989
|
})
|
|
577
990
|
].join("\n");
|
|
578
991
|
const envBlock = `
|
|
@@ -594,30 +1007,20 @@ ${envBindingFields}` : ""}
|
|
|
594
1007
|
|
|
595
1008
|
/** Alias for {@link CloudflareBindings} — the typed shape of \`env\`. */
|
|
596
1009
|
export type Env = CloudflareBindings;`;
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
/** Analytics Engine telemetry sink. Fire-and-forget and sampled; do not read it back in-handler. */
|
|
612
|
-
readonly analytics: import("@lunora/analytics").AnalyticsClient;` : "";
|
|
613
|
-
const pipelinesActionField = hasPipelines ? `
|
|
614
|
-
/** Pipelines ingestion sink (durable, R2-backed). Fire-and-forget and batched; do not read it back in-handler. */
|
|
615
|
-
readonly pipelines: import("@lunora/pipelines").PipelineClient;` : "";
|
|
616
|
-
const r2sqlActionField = hasR2sql ? `
|
|
617
|
-
/**
|
|
618
|
-
* R2 SQL over Apache Iceberg tables (window functions, DISTINCT, set operations). Non-deterministic — available only in actions. Reads here are NOT tracked by Lunora live queries.
|
|
619
|
-
*/
|
|
620
|
-
readonly r2sql: import("@lunora/r2sql").R2SqlClient;` : "";
|
|
1010
|
+
const serverCapabilityField = (key, enabled) => enabled ? SERVER_CTX_FIELDS.get(key)?.field ?? "" : "";
|
|
1011
|
+
const kvContextField = serverCapabilityField("kv", hasKv);
|
|
1012
|
+
const analyticsContextField = serverCapabilityField("analytics", hasAnalytics);
|
|
1013
|
+
const hyperdriveActionField = serverCapabilityField("hyperdrive", hasHyperdrive);
|
|
1014
|
+
const browserActionField = serverCapabilityField("browser", hasBrowser);
|
|
1015
|
+
const imagesActionField = serverCapabilityField("images", hasImages);
|
|
1016
|
+
const pipelinesActionField = serverCapabilityField("pipelines", hasPipelines);
|
|
1017
|
+
const r2sqlActionField = serverCapabilityField("r2sql", hasR2sql);
|
|
1018
|
+
const accessContextField = hasAccessFacade ? `
|
|
1019
|
+
/** Verified Cloudflare Access identity — a synchronous facade over the resolved claims (email / groups / hasGroup / claims). Anonymous when no Access token is present. */
|
|
1020
|
+
readonly access: import("@lunora/cloudflare-access/context").AccessFacade;` : "";
|
|
1021
|
+
const flagsContextField = hasFlags ? `
|
|
1022
|
+
/** Feature-flag evaluation (OpenFeature). Reads are memoized per request; evaluations never throw — a provider error resolves to the supplied default. */
|
|
1023
|
+
readonly flags: import("${base.flags}").LunoraFlags;` : "";
|
|
621
1024
|
const hasWorkflows = workflows.length > 0;
|
|
622
1025
|
const workflowsTypeImport = hasWorkflows ? `import type { WorkflowHandle } from "@lunora/workflow";
|
|
623
1026
|
import type * as lunoraWorkflowDefinitions from "../workflows.js";
|
|
@@ -638,6 +1041,45 @@ ${workflows.map((workflow) => ` get(name: ${JSON.stringify(workflow.exportNam
|
|
|
638
1041
|
const workflowsOmit = hasWorkflows ? ` | "workflows"` : "";
|
|
639
1042
|
const workflowsContextField = hasWorkflows ? `
|
|
640
1043
|
readonly workflows: LunoraWorkflows;` : "";
|
|
1044
|
+
const hasQueues = queues.length > 0;
|
|
1045
|
+
const queuesTypeImport = hasQueues ? `import type { QueueProducer } from "@lunora/queue";
|
|
1046
|
+
import type * as lunoraQueueDefinitions from "../queues.js";
|
|
1047
|
+
` : "";
|
|
1048
|
+
const queuesTypeBlock = hasQueues ? `
|
|
1049
|
+
|
|
1050
|
+
/** Message body type carried by a \`defineQueue\` definition (its phantom \`__lunoraBody\`). */
|
|
1051
|
+
type QueueBodyOf<Definition> = Definition extends { __lunoraBody?: infer Body } ? (unknown extends Body ? unknown : NonNullable<Body>) : unknown;
|
|
1052
|
+
|
|
1053
|
+
/** This project's declared queues, addressable from \`ctx.queues\` by their \`lunora/queues.ts\` export name. */
|
|
1054
|
+
export interface LunoraQueues {
|
|
1055
|
+
${queues.map((queue) => ` readonly ${queue.exportName}: QueueProducer<QueueBodyOf<typeof lunoraQueueDefinitions.${queue.exportName}>>;`).join("\n")}
|
|
1056
|
+
}` : "";
|
|
1057
|
+
const queuesContextField = hasQueues ? `
|
|
1058
|
+
readonly queues: LunoraQueues;` : "";
|
|
1059
|
+
const identityTypeImport = identity ? `import type { InferIdentity } from "${base.server}";
|
|
1060
|
+
import type * as lunoraIdentityContract from "../identity.js";
|
|
1061
|
+
` : "";
|
|
1062
|
+
const identityTypeBlock = identity ? `
|
|
1063
|
+
|
|
1064
|
+
/** This app's declared identity claim contract (\`defineIdentity\` in \`lunora/identity.ts\`) — the typed shape of \`ctx.auth.getIdentity()\`, the RLS policy \`ctx.auth.identity\`, and the \`authorizeShard\`/\`authorizeFanOut\` identity argument. */
|
|
1065
|
+
export type Identity = InferIdentity<typeof lunoraIdentityContract.${identity.exportName}>;
|
|
1066
|
+
|
|
1067
|
+
/** \`ctx.auth\` narrowed so \`getIdentity()\` resolves the declared {@link Identity} contract instead of the untyped claim bag. */
|
|
1068
|
+
type NarrowedAuth = Omit<QueryCtxBase["auth"], "getIdentity"> & { getIdentity: () => Promise<Identity | null> };` : "";
|
|
1069
|
+
const authOmit = identity ? ` | "auth"` : "";
|
|
1070
|
+
const authContextField = identity ? `
|
|
1071
|
+
readonly auth: NarrowedAuth;` : "";
|
|
1072
|
+
const policyIdentityArgument = identity ? ", Identity" : "";
|
|
1073
|
+
const envTypeImport = env ? `import type * as lunoraEnvContract from "../env.js";
|
|
1074
|
+
` : "";
|
|
1075
|
+
const envTypeBlock = env ? `
|
|
1076
|
+
|
|
1077
|
+
/** This app's declared env contract (\`defineEnv\` in \`lunora/env.ts\`) — the validated, coercion-aware shape of \`ctx.env\`. */
|
|
1078
|
+
export type LunoraEnv = ReturnType<typeof lunoraEnvContract.${env.exportName}>;` : "";
|
|
1079
|
+
const envOmit = env ? ` | "env"` : "";
|
|
1080
|
+
const envContextField = env ? `
|
|
1081
|
+
/** Validated, typed environment declared by \`defineEnv\` in \`lunora/env.ts\` — parsed & coercion-aware config values (\`ctx.env.STRIPE_KEY\`); a missing or invalid value throws at read time. */
|
|
1082
|
+
readonly env: LunoraEnv;` : "";
|
|
641
1083
|
const server = `${GENERATED_HEADER}import { createPolicyDsl, initLunora, v as vBase } from "${base.server}";
|
|
642
1084
|
import type {
|
|
643
1085
|
ActionBuilder,
|
|
@@ -659,11 +1101,11 @@ import type {
|
|
|
659
1101
|
} from "${base.server}";
|
|
660
1102
|
|
|
661
1103
|
import type { DataModel, DatabaseReaderFacade, DatabaseWriterFacade, Doc, Id as IdOfTable, OrmReader, OrmWriter, Relations, TableName } from "./dataModel.js";
|
|
662
|
-
${aiTypeImport}${paymentsTypeImport}${containersTypeImport}${workflowsTypeImport}
|
|
1104
|
+
${aiTypeImport}${paymentsTypeImport}${x402TypeImport}${containersTypeImport}${workflowsTypeImport}${queuesTypeImport}${identityTypeImport}${envTypeImport}
|
|
663
1105
|
export type { DataModel, Doc, Id, TableName } from "./dataModel.js";
|
|
664
1106
|
|
|
665
1107
|
/** Storage buckets this schema declares (\`v.storage("name")\`), narrowing \`ctx.storage.bucket(name)\`. */
|
|
666
|
-
export type StorageBucketName = ${storageBucketUnion};${envBlock}${workflowsTypeBlock}
|
|
1108
|
+
export type StorageBucketName = ${storageBucketUnion};${envBlock}${workflowsTypeBlock}${queuesTypeBlock}${identityTypeBlock}${envTypeBlock}
|
|
667
1109
|
|
|
668
1110
|
/**
|
|
669
1111
|
* Project-typed contexts. The base contexts from \`@lunora/server\` are
|
|
@@ -692,22 +1134,22 @@ type TypedTableQuery = (<T extends TableName>(table: T) => TableReader<Doc<T>>)
|
|
|
692
1134
|
*/
|
|
693
1135
|
type TypedTableGet = <T extends TableName>(id: IdOfTable<T>) => Promise<Doc<T> | null>;
|
|
694
1136
|
|
|
695
|
-
export interface QueryCtx extends Omit<QueryCtxBase, "db" | "storage"> {
|
|
1137
|
+
export interface QueryCtx extends Omit<QueryCtxBase, "db" | "storage"${authOmit}${envOmit}> {
|
|
696
1138
|
readonly db: Omit<DatabaseReader, "query" | "get"> & DatabaseReaderFacade & { query: TypedTableQuery; get: TypedTableGet };
|
|
697
1139
|
readonly orm: OrmReader;
|
|
698
|
-
readonly storage: ReadOnlyStorage<StorageBucketName>;${kvContextField}${analyticsContextField}
|
|
1140
|
+
readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}${envContextField}${authContextField}
|
|
699
1141
|
}
|
|
700
1142
|
|
|
701
|
-
export interface MutationCtx extends Omit<MutationCtxBase, "db" | "storage"${workflowsOmit}> {
|
|
1143
|
+
export interface MutationCtx extends Omit<MutationCtxBase, "db" | "storage"${workflowsOmit}${authOmit}${envOmit}> {
|
|
702
1144
|
readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
|
|
703
1145
|
readonly orm: OrmWriter;
|
|
704
|
-
readonly storage: ReadOnlyStorage<StorageBucketName>;${kvContextField}${analyticsContextField}${workflowsContextField}
|
|
1146
|
+
readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}${envContextField}${workflowsContextField}${queuesContextField}${authContextField}
|
|
705
1147
|
}
|
|
706
1148
|
|
|
707
|
-
export interface ActionCtx extends Omit<ActionCtxBase, "db" | "storage"${workflowsOmit}> {
|
|
1149
|
+
export interface ActionCtx extends Omit<ActionCtxBase, "db" | "storage"${workflowsOmit}${authOmit}${envOmit}> {
|
|
708
1150
|
readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
|
|
709
1151
|
readonly orm: OrmWriter;
|
|
710
|
-
readonly storage: StorageBase<StorageBucketName>;${aiActionField}${paymentsActionField}${containersActionField}${kvContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${r2sqlActionField}${workflowsContextField}
|
|
1152
|
+
readonly storage: StorageBase<StorageBucketName>;${accessContextField}${aiActionField}${paymentsActionField}${x402ActionField}${containersActionField}${kvContextField}${flagsContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${r2sqlActionField}${envContextField}${workflowsContextField}${queuesContextField}${authContextField}
|
|
711
1153
|
}
|
|
712
1154
|
|
|
713
1155
|
/**
|
|
@@ -747,7 +1189,7 @@ export const internalAction = lunoraBuilders.internalAction as unknown as Intern
|
|
|
747
1189
|
* Runtime-identical to \`@lunora/server\`'s \`definePolicy\`; only the types narrow,
|
|
748
1190
|
* so the \`rls()\` chain discovers a policy authored either way the same.
|
|
749
1191
|
*/
|
|
750
|
-
export const definePolicy = createPolicyDsl<DataModel, Relations>();
|
|
1192
|
+
export const definePolicy = createPolicyDsl<DataModel, Relations${policyIdentityArgument}>();
|
|
751
1193
|
|
|
752
1194
|
/**
|
|
753
1195
|
* The validator builder \`v\`, with \`v.id(...)\` constrained to THIS schema's
|
|
@@ -777,10 +1219,40 @@ const renderLifecycleManifest = (functions) => {
|
|
|
777
1219
|
}
|
|
778
1220
|
return manifest;
|
|
779
1221
|
};
|
|
780
|
-
const emitFunctions = (functions, migrations = []) => {
|
|
1222
|
+
const emitFunctions = (functions, migrations = [], useUmbrella = false, mutators = [], shapes = []) => {
|
|
781
1223
|
const hasFunctions = functions.length > 0;
|
|
782
|
-
const
|
|
1224
|
+
const base = baseSpecifiers(useUmbrella);
|
|
1225
|
+
const { dispatchBody, importBlock, installBlock, migrationBody, mutatorPaths, shapeBody } = renderFunctionRegistry(functions, migrations, mutators, shapes);
|
|
783
1226
|
const lifecycleHooks = renderLifecycleManifest(functions);
|
|
1227
|
+
const shapeTypeImport = shapes.length > 0 ? `import type { RegisteredShape } from "${base.server}";
|
|
1228
|
+
` : "";
|
|
1229
|
+
const shapeRegistry = shapes.length > 0 ? `
|
|
1230
|
+
/**
|
|
1231
|
+
* Replication-shape registry — one entry per \`defineShape\` in \`lunora/shapes.ts\`.
|
|
1232
|
+
* The generated ShardDO's \`resolveShape\` override looks a shape up by name and
|
|
1233
|
+
* evaluates its trusted \`compileWhere(ctx, args)\` to authorize + scope a
|
|
1234
|
+
* \`shape_subscribe\` (reads-as-permissions).
|
|
1235
|
+
*/
|
|
1236
|
+
export const LUNORA_SHAPES: Record<string, RegisteredShape> = {${shapeBody}};
|
|
1237
|
+
` : "";
|
|
1238
|
+
const mutatorPathsRegistry = mutatorPaths.length > 0 ? `
|
|
1239
|
+
/**
|
|
1240
|
+
* Custom-mutator function paths — the \`LUNORA_FUNCTIONS\` keys the generated
|
|
1241
|
+
* ShardDO's \`isCustomMutator\` override routes through the client-watermark push
|
|
1242
|
+
* protocol (\`x-lunora-client-id\`/\`x-lunora-client-seq\` ordering).
|
|
1243
|
+
*/
|
|
1244
|
+
export const LUNORA_MUTATOR_PATHS: ReadonlySet<string> = new Set([${mutatorPaths.map((path) => JSON.stringify(path)).join(", ")}]);
|
|
1245
|
+
` : "";
|
|
1246
|
+
const compiledArgsImport = installBlock.length > 0 ? `import { DEFER_VALIDATION as DEFER, installCompiledValidatorMap } from "${base.values}";
|
|
1247
|
+
` : "";
|
|
1248
|
+
const compiledArgsInstall = installBlock.length > 0 ? `
|
|
1249
|
+
/**
|
|
1250
|
+
* AOT-compiled argument validators (Worker-safe, no \`eval\`). Each is installed
|
|
1251
|
+
* onto its function's live \`.args\` object and consulted by the interpreted
|
|
1252
|
+
* parser as a zero-allocation fast path; anything it can't model is deferred.
|
|
1253
|
+
*/
|
|
1254
|
+
${installBlock}
|
|
1255
|
+
` : "";
|
|
784
1256
|
const caller = renderCaller(functions);
|
|
785
1257
|
const callerTypes = caller.types ? `
|
|
786
1258
|
${caller.types}
|
|
@@ -795,7 +1267,8 @@ ${caller.implementation}
|
|
|
795
1267
|
const callRegisteredHelper = hasFunctions ? `${CALL_REGISTERED_HELPER}
|
|
796
1268
|
|
|
797
1269
|
` : "";
|
|
798
|
-
return `${GENERATED_HEADER}${importBlock}import
|
|
1270
|
+
return `${GENERATED_HEADER}${importBlock}${compiledArgsImport}${shapeTypeImport}import { LunoraError } from "${base.server}";
|
|
1271
|
+
import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
|
|
799
1272
|
${dataModelImport}
|
|
800
1273
|
/**
|
|
801
1274
|
* Single registered function, narrowed to the shape \`handleRpc\` needs.
|
|
@@ -821,7 +1294,7 @@ export interface RegisteredLunoraFunction {
|
|
|
821
1294
|
* emits (\`api[namespace][fn].__lunoraRef === "namespace:fn"\`).
|
|
822
1295
|
*/
|
|
823
1296
|
export const LUNORA_FUNCTIONS: Record<string, RegisteredLunoraFunction> = {${dispatchBody}};
|
|
824
|
-
|
|
1297
|
+
${compiledArgsInstall}${shapeRegistry}${mutatorPathsRegistry}
|
|
825
1298
|
/**
|
|
826
1299
|
* Connection-lifecycle manifest: the function paths the generated ShardDO
|
|
827
1300
|
* dispatches when a client's WebSocket connects (\`connect\`) or disconnects
|
|
@@ -843,11 +1316,7 @@ export const dispatchLunoraFunction = async (functionPath: string, context: unkn
|
|
|
843
1316
|
const registered = LUNORA_FUNCTIONS[functionPath];
|
|
844
1317
|
|
|
845
1318
|
if (!registered || registered.visibility === "internal") {
|
|
846
|
-
throw
|
|
847
|
-
name: "LunoraError",
|
|
848
|
-
code: "FUNCTION_NOT_FOUND",
|
|
849
|
-
status: 404,
|
|
850
|
-
});
|
|
1319
|
+
throw new LunoraError("FUNCTION_NOT_FOUND", \`function not registered: \${functionPath}\`);
|
|
851
1320
|
}
|
|
852
1321
|
|
|
853
1322
|
return registered.handler(context, args);
|
|
@@ -1042,7 +1511,7 @@ const emitKvFragments = (hasKv) => {
|
|
|
1042
1511
|
kv?: (env: Record<string, unknown>) => KVNamespaceLike;`,
|
|
1043
1512
|
contextField: `
|
|
1044
1513
|
kv,`,
|
|
1045
|
-
importLines: [`import type { Kv, KVNamespaceLike } from "@lunora/kv";`, `import { createKv } from "@lunora/kv";`],
|
|
1514
|
+
importLines: [`import type { Kv, KVNamespaceLike } from "@lunora/bindings/kv";`, `import { createKv } from "@lunora/bindings/kv";`],
|
|
1046
1515
|
stub: `
|
|
1047
1516
|
const kvStub: Kv = {
|
|
1048
1517
|
delete: async () => {
|
|
@@ -1067,6 +1536,131 @@ const kvStub: Kv = {
|
|
|
1067
1536
|
`
|
|
1068
1537
|
};
|
|
1069
1538
|
};
|
|
1539
|
+
const emitFlagsFragments = (hasFlags, flagsSpecifier) => {
|
|
1540
|
+
if (!hasFlags) {
|
|
1541
|
+
return EMPTY_HELPER_FRAGMENTS;
|
|
1542
|
+
}
|
|
1543
|
+
return {
|
|
1544
|
+
build: `
|
|
1545
|
+
const flags: import("${flagsSpecifier}").LunoraFlags = createFlags({
|
|
1546
|
+
hooks: flagsConfig.hooks,
|
|
1547
|
+
logger: flagsConfig.logger,
|
|
1548
|
+
provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
|
|
1549
|
+
targetingKey: () => flagsConfig.identify?.({ identity: identity ?? null, userId: userId ?? null }),
|
|
1550
|
+
});
|
|
1551
|
+
`,
|
|
1552
|
+
configField: `
|
|
1553
|
+
flags?: (env: Record<string, unknown>) => import("${flagsSpecifier}").Provider;`,
|
|
1554
|
+
contextField: `
|
|
1555
|
+
flags,`,
|
|
1556
|
+
importLines: [`import { createFlags } from "${flagsSpecifier}";`, `import flagsConfig from "../flags.js";`],
|
|
1557
|
+
stub: ""
|
|
1558
|
+
};
|
|
1559
|
+
};
|
|
1560
|
+
const emitEnvFragments = (env) => {
|
|
1561
|
+
if (!env) {
|
|
1562
|
+
return EMPTY_HELPER_FRAGMENTS;
|
|
1563
|
+
}
|
|
1564
|
+
return {
|
|
1565
|
+
build: `
|
|
1566
|
+
const envConfig = lunoraEnvContract.${env.exportName}(env);
|
|
1567
|
+
`,
|
|
1568
|
+
configField: "",
|
|
1569
|
+
contextField: `
|
|
1570
|
+
env: envConfig,`,
|
|
1571
|
+
importLines: [`import * as lunoraEnvContract from "../env.js";`],
|
|
1572
|
+
stub: ""
|
|
1573
|
+
};
|
|
1574
|
+
};
|
|
1575
|
+
const emitAccessFragments = (hasAccessFacade) => {
|
|
1576
|
+
if (!hasAccessFacade) {
|
|
1577
|
+
return EMPTY_HELPER_FRAGMENTS;
|
|
1578
|
+
}
|
|
1579
|
+
return {
|
|
1580
|
+
build: `
|
|
1581
|
+
const access = accessFacade(identity, userId);
|
|
1582
|
+
`,
|
|
1583
|
+
configField: "",
|
|
1584
|
+
contextField: `
|
|
1585
|
+
access,`,
|
|
1586
|
+
importLines: [`import { accessFacade } from "@lunora/cloudflare-access/context";`],
|
|
1587
|
+
stub: ""
|
|
1588
|
+
};
|
|
1589
|
+
};
|
|
1590
|
+
const emitFlagsOverrides = (flagKeys, hasFlags, flagsSpecifier) => {
|
|
1591
|
+
if (!hasFlags) {
|
|
1592
|
+
return { constant: "", evaluateOverride: "", subscriptionOverride: "" };
|
|
1593
|
+
}
|
|
1594
|
+
const clientBuild = (targetingKey) => `
|
|
1595
|
+
const env = (this.env ?? {}) as Record<string, unknown>;
|
|
1596
|
+
const flags: import("${flagsSpecifier}").LunoraFlags = createFlags({
|
|
1597
|
+
hooks: flagsConfig.hooks,
|
|
1598
|
+
logger: flagsConfig.logger,
|
|
1599
|
+
provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
|
|
1600
|
+
targetingKey: ${targetingKey},
|
|
1601
|
+
});`;
|
|
1602
|
+
const constant = `
|
|
1603
|
+
/** Statically-discovered feature flags (\`ctx.flags.<type>("key")\` reads) served via \`__lunora_admin__:listFlags\` + the reactive \`__lunora_flags__:\` channel. */
|
|
1604
|
+
const LUNORA_FLAG_KEYS: ReadonlyArray<{ key: string; type: "boolean" | "number" | "object" | "string" }> = ${JSON.stringify(flagKeys, void 0, 4)};
|
|
1605
|
+
`;
|
|
1606
|
+
const evaluateOverride = `
|
|
1607
|
+
protected override async evaluateFlags(context?: Record<string, unknown>): Promise<FlagsResult> {${clientBuild("undefined")}
|
|
1608
|
+
const evalContext = context as import("${flagsSpecifier}").EvaluationContext | undefined;
|
|
1609
|
+
const evaluations: FlagsResult["flags"] = [];
|
|
1610
|
+
|
|
1611
|
+
for (const entry of LUNORA_FLAG_KEYS) {
|
|
1612
|
+
// eslint-disable-next-line no-await-in-loop -- flags evaluate sequentially; each shares the single memoized provider client
|
|
1613
|
+
const details =
|
|
1614
|
+
entry.type === "boolean"
|
|
1615
|
+
? await flags.details.boolean(entry.key, false, evalContext)
|
|
1616
|
+
: entry.type === "number"
|
|
1617
|
+
? await flags.details.number(entry.key, 0, evalContext)
|
|
1618
|
+
: entry.type === "string"
|
|
1619
|
+
? await flags.details.string(entry.key, "", evalContext)
|
|
1620
|
+
: await flags.details.object(entry.key, {}, evalContext);
|
|
1621
|
+
|
|
1622
|
+
evaluations.push({ errorCode: details.errorCode, key: entry.key, reason: details.reason, type: entry.type, value: details.value, variant: details.variant });
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
return { configured: true, flags: evaluations };
|
|
1626
|
+
}
|
|
1627
|
+
`;
|
|
1628
|
+
const subscriptionOverride = `
|
|
1629
|
+
protected override runFlagSubscriptionRead(_functionPath: string, args: Record<string, unknown>, identity?: { identity?: Record<string, unknown>; userId?: string }): Promise<unknown> {${clientBuild("() => flagsConfig.identify?.({ identity: identity?.identity ?? null, userId: identity?.userId ?? null })")}
|
|
1630
|
+
const key = typeof args.key === "string" ? args.key : "";
|
|
1631
|
+
|
|
1632
|
+
// SECURITY: the reactive channel is public (any socket, no auth). Serve
|
|
1633
|
+
// ONLY statically-discovered flag keys — an arbitrary client-supplied key
|
|
1634
|
+
// would let a subscriber probe the value of internal/unreleased flags the
|
|
1635
|
+
// app never exposes. Unknown key ⇒ the "nothing to deliver" sentinel.
|
|
1636
|
+
// eslint-disable-next-line unicorn/no-null -- the base hook's "nothing to deliver" sentinel
|
|
1637
|
+
if (key.length === 0 || !LUNORA_FLAG_KEYS.some((entry) => entry.key === key)) {
|
|
1638
|
+
return Promise.resolve(null);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// SECURITY: evaluate under the socket's server-verified identity ONLY
|
|
1642
|
+
// (the targetingKey resolved above). Client-supplied targeting context is
|
|
1643
|
+
// NOT honored on this public channel — otherwise a subscriber could spoof
|
|
1644
|
+
// targeting attributes (e.g. plan/role) to unlock a flag gated on them.
|
|
1645
|
+
const context = undefined;
|
|
1646
|
+
|
|
1647
|
+
if (args.type === "number") {
|
|
1648
|
+
return flags.number(key, typeof args.default === "number" ? args.default : 0, context);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
if (args.type === "string") {
|
|
1652
|
+
return flags.string(key, typeof args.default === "string" ? args.default : "", context);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (args.type === "object") {
|
|
1656
|
+
return flags.object(key, (args.default ?? {}) as import("${flagsSpecifier}").JsonValue, context);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
return flags.boolean(key, typeof args.default === "boolean" ? args.default : false, context);
|
|
1660
|
+
}
|
|
1661
|
+
`;
|
|
1662
|
+
return { constant, evaluateOverride, subscriptionOverride };
|
|
1663
|
+
};
|
|
1070
1664
|
const emitAnalyticsFragments = (hasAnalytics) => {
|
|
1071
1665
|
if (!hasAnalytics) {
|
|
1072
1666
|
return EMPTY_HELPER_FRAGMENTS;
|
|
@@ -1082,8 +1676,8 @@ const emitAnalyticsFragments = (hasAnalytics) => {
|
|
|
1082
1676
|
contextField: `
|
|
1083
1677
|
analytics,`,
|
|
1084
1678
|
importLines: [
|
|
1085
|
-
`import type { AnalyticsClient, AnalyticsEngineDatasetLike } from "@lunora/analytics";`,
|
|
1086
|
-
`import { createAnalytics } from "@lunora/analytics";`
|
|
1679
|
+
`import type { AnalyticsClient, AnalyticsEngineDatasetLike } from "@lunora/bindings/analytics";`,
|
|
1680
|
+
`import { createAnalytics } from "@lunora/bindings/analytics";`
|
|
1087
1681
|
],
|
|
1088
1682
|
stub: `
|
|
1089
1683
|
const analyticsStub: AnalyticsClient = {
|
|
@@ -1112,7 +1706,7 @@ const emitImagesFragments = (hasImages) => {
|
|
|
1112
1706
|
// ActionCtx-only: woven onto the action ctx object, never query/mutation.
|
|
1113
1707
|
contextField: `
|
|
1114
1708
|
images,`,
|
|
1115
|
-
importLines: [`import type { Images, ImagesBindingLike } from "@lunora/images";`, `import { createImages } from "@lunora/images";`],
|
|
1709
|
+
importLines: [`import type { Images, ImagesBindingLike } from "@lunora/bindings/images";`, `import { createImages } from "@lunora/bindings/images";`],
|
|
1116
1710
|
stub: `
|
|
1117
1711
|
const imagesStub: Images = {
|
|
1118
1712
|
info: async () => {
|
|
@@ -1211,10 +1805,10 @@ const emitR2sqlFragments = (hasR2sql) => {
|
|
|
1211
1805
|
// ActionCtx-only: attached via the \`ctx.r2sql = r2sql\` assignment in the
|
|
1212
1806
|
// \`isAction\` block, never the every-ctx object literal.
|
|
1213
1807
|
contextField: "",
|
|
1214
|
-
importLines: [`import type { R2SqlClient } from "@lunora/r2sql";`, `import { createR2Sql } from "@lunora/r2sql";`],
|
|
1808
|
+
importLines: [`import type { R2SqlClient } from "@lunora/bindings/r2sql";`, `import { createR2Sql } from "@lunora/bindings/r2sql";`],
|
|
1215
1809
|
// The stub is typed `R2SqlClient`, so TS flags a missing method at build
|
|
1216
1810
|
// time — but it must stay structurally in sync with that interface
|
|
1217
|
-
// (`@lunora/r2sql` client.ts) when a method is added there.
|
|
1811
|
+
// (`@lunora/bindings/r2sql` client.ts) when a method is added there.
|
|
1218
1812
|
stub: `
|
|
1219
1813
|
const r2sqlStub: R2SqlClient = {
|
|
1220
1814
|
describe: async () => {
|
|
@@ -1239,17 +1833,46 @@ const r2sqlStub: R2SqlClient = {
|
|
|
1239
1833
|
`
|
|
1240
1834
|
};
|
|
1241
1835
|
};
|
|
1242
|
-
const
|
|
1836
|
+
const emitPipelinesFragments = (hasPipelines) => {
|
|
1837
|
+
if (!hasPipelines) {
|
|
1838
|
+
return EMPTY_HELPER_FRAGMENTS;
|
|
1839
|
+
}
|
|
1840
|
+
const pipelinesMissing = `throw new Error("ctx.pipelines: no Pipelines binding found. Add a \\\`pipelines\\\` binding (env.PIPELINES) to wrangler.jsonc, or pass \\\`pipelines\\\` to createShardDO().");`;
|
|
1841
|
+
return {
|
|
1842
|
+
build: `
|
|
1843
|
+
const pipelinesBinding = config.pipelines?.(env) ?? (env as Record<string, unknown>).PIPELINES;
|
|
1844
|
+
const pipelines: PipelineClient = pipelinesBinding ? createPipelines({ binding: pipelinesBinding as PipelineBindingLike }) : pipelinesStub;
|
|
1845
|
+
`,
|
|
1846
|
+
configField: `
|
|
1847
|
+
pipelines?: (env: Record<string, unknown>) => PipelineBindingLike;`,
|
|
1848
|
+
// ActionCtx-only: attached via the \`ctx.pipelines = pipelines\` assignment
|
|
1849
|
+
// in the \`isAction\` block, never the every-ctx object literal.
|
|
1850
|
+
contextField: "",
|
|
1851
|
+
importLines: [
|
|
1852
|
+
`import type { PipelineBindingLike, PipelineClient } from "@lunora/bindings/pipelines";`,
|
|
1853
|
+
`import { createPipelines } from "@lunora/bindings/pipelines";`
|
|
1854
|
+
],
|
|
1855
|
+
stub: `
|
|
1856
|
+
const pipelinesStub: PipelineClient = {
|
|
1857
|
+
send: async () => {
|
|
1858
|
+
${pipelinesMissing}
|
|
1859
|
+
},
|
|
1860
|
+
};
|
|
1861
|
+
`
|
|
1862
|
+
};
|
|
1863
|
+
};
|
|
1864
|
+
const emitContainers = (containers, jurisdiction) => {
|
|
1243
1865
|
if (containers.length === 0) {
|
|
1244
1866
|
return "";
|
|
1245
1867
|
}
|
|
1868
|
+
const jurisdictionArgument = jurisdiction ? `, ${JSON.stringify(jurisdiction)}` : "";
|
|
1246
1869
|
const classes = containers.map((container) => {
|
|
1247
1870
|
assertIdentifier(container.exportName, `container export "${container.exportName}"`);
|
|
1248
1871
|
assertIdentifier(container.className, `container class "${container.className}"`);
|
|
1249
1872
|
return `/** Container DO for the \`${container.exportName}\` definition (binding \`${container.bindingName}\`). */
|
|
1250
1873
|
export class ${container.className} extends LunoraContainer {
|
|
1251
1874
|
public constructor(ctx: ConstructorParameters<typeof LunoraContainer>[0], env: Record<string, unknown>) {
|
|
1252
|
-
super(ctx, env, ${container.exportName}, "${container.exportName}");
|
|
1875
|
+
super(ctx, env, ${container.exportName}, "${container.exportName}"${jurisdictionArgument});
|
|
1253
1876
|
}
|
|
1254
1877
|
}
|
|
1255
1878
|
`;
|
|
@@ -1261,14 +1884,21 @@ export class ${container.className} extends LunoraContainer {
|
|
|
1261
1884
|
* requires each \`containers[].class_name\` to be exported by the worker:
|
|
1262
1885
|
*
|
|
1263
1886
|
* \`export * from "./lunora/_generated/containers.js";\`
|
|
1887
|
+
*
|
|
1888
|
+
* \`ContainerProxy\` is re-exported alongside them: the egress-interception path
|
|
1889
|
+
* (\`allowedHosts\`/\`deniedHosts\`/\`interceptHttps\` and the runtime
|
|
1890
|
+
* \`handle.egress\` controls) routes container outbound traffic through this
|
|
1891
|
+
* WorkerEntrypoint, so it too must be exported by the deployed worker.
|
|
1264
1892
|
*/
|
|
1265
|
-
import LunoraContainer from "@lunora/container/do";
|
|
1893
|
+
import { LunoraContainer } from "@lunora/container/do";
|
|
1266
1894
|
|
|
1267
1895
|
import { ${imports} } from "../containers.js";
|
|
1268
1896
|
|
|
1897
|
+
export { ContainerProxy } from "@lunora/container/do";
|
|
1898
|
+
|
|
1269
1899
|
${classes}`;
|
|
1270
1900
|
};
|
|
1271
|
-
const emitContainerFragments = (containers) => {
|
|
1901
|
+
const emitContainerFragments = (containers, jurisdiction) => {
|
|
1272
1902
|
if (containers.length === 0) {
|
|
1273
1903
|
return { build: "", contextField: "", importLines: [], specs: "" };
|
|
1274
1904
|
}
|
|
@@ -1281,8 +1911,11 @@ const emitContainerFragments = (containers) => {
|
|
|
1281
1911
|
return ` { binding: "${container.bindingName}", exportName: "${container.exportName}"${maxInstances} },`;
|
|
1282
1912
|
}).join("\n");
|
|
1283
1913
|
return {
|
|
1914
|
+
// Schema `.jurisdiction("…")` pins every container DO this shard reaches
|
|
1915
|
+
// to the data-residency region; the arg is omitted when undeclared so
|
|
1916
|
+
// existing generated output is unchanged.
|
|
1284
1917
|
build: `
|
|
1285
|
-
const containers = createContainerContext(env, LUNORA_CONTAINERS);
|
|
1918
|
+
const containers = createContainerContext(env, LUNORA_CONTAINERS${jurisdiction ? `, ${JSON.stringify(jurisdiction)}` : ""});
|
|
1286
1919
|
`,
|
|
1287
1920
|
contextField: `
|
|
1288
1921
|
containers,`,
|
|
@@ -1335,6 +1968,32 @@ type WorkflowOutputOf<Definition> = Definition extends { __output?: infer Output
|
|
|
1335
1968
|
|
|
1336
1969
|
${classes}`;
|
|
1337
1970
|
};
|
|
1971
|
+
const emitQueues = (queues) => {
|
|
1972
|
+
const pushQueues = queues.filter((queue) => queue.mode === "push");
|
|
1973
|
+
if (pushQueues.length === 0) {
|
|
1974
|
+
return "";
|
|
1975
|
+
}
|
|
1976
|
+
for (const queue of pushQueues) {
|
|
1977
|
+
assertIdentifier(queue.exportName, `queue export "${queue.exportName}"`);
|
|
1978
|
+
}
|
|
1979
|
+
const imports = pushQueues.map((queue) => queue.exportName).join(", ");
|
|
1980
|
+
const entries = pushQueues.map((queue) => ` ${JSON.stringify(queue.name)}: { definition: ${queue.exportName}, exportName: ${JSON.stringify(queue.exportName)} },`).join("\n");
|
|
1981
|
+
return `${GENERATED_HEADER}/**
|
|
1982
|
+
* Push-consumer registry for the queues declared in \`lunora/queues.ts\`. The
|
|
1983
|
+
* composed worker's \`queue(batch, env, ctx)\` entry routes each delivered batch
|
|
1984
|
+
* by \`batch.queue\` to the matching \`defineQueue\` handler. Wired automatically
|
|
1985
|
+
* by \`defineApp\` — you don't import this directly.
|
|
1986
|
+
*/
|
|
1987
|
+
import type { QueueRegistry } from "@lunora/queue";
|
|
1988
|
+
|
|
1989
|
+
import { ${imports} } from "../queues.js";
|
|
1990
|
+
|
|
1991
|
+
/** Stable wrangler queue name → { definition, exportName } for batch routing. */
|
|
1992
|
+
export const LUNORA_QUEUE_REGISTRY: QueueRegistry = {
|
|
1993
|
+
${entries}
|
|
1994
|
+
};
|
|
1995
|
+
`;
|
|
1996
|
+
};
|
|
1338
1997
|
const emitWorkflowFragments = (workflows) => {
|
|
1339
1998
|
if (workflows.length === 0) {
|
|
1340
1999
|
return { build: "", contextField: "", importLines: [], specs: "" };
|
|
@@ -1360,6 +2019,31 @@ ${specEntries}
|
|
|
1360
2019
|
`
|
|
1361
2020
|
};
|
|
1362
2021
|
};
|
|
2022
|
+
const emitQueueFragments = (queues) => {
|
|
2023
|
+
if (queues.length === 0) {
|
|
2024
|
+
return { build: "", contextField: "", importLines: [], specs: "" };
|
|
2025
|
+
}
|
|
2026
|
+
for (const queue of queues) {
|
|
2027
|
+
assertIdentifier(queue.exportName, `queue export "${queue.exportName}"`);
|
|
2028
|
+
assertIdentifier(queue.bindingName, `queue binding "${queue.bindingName}"`);
|
|
2029
|
+
}
|
|
2030
|
+
const specEntries = queues.map((queue) => ` { binding: "${queue.bindingName}", exportName: "${queue.exportName}", name: ${JSON.stringify(queue.name)} },`).join("\n");
|
|
2031
|
+
return {
|
|
2032
|
+
build: `
|
|
2033
|
+
const queues = createQueueContext(env, LUNORA_QUEUES);
|
|
2034
|
+
`,
|
|
2035
|
+
contextField: `
|
|
2036
|
+
queues,`,
|
|
2037
|
+
importLines: [`import type { QueueBindingSpec } from "@lunora/queue";`, `import { createQueueContext } from "@lunora/queue";`],
|
|
2038
|
+
// eslint-disable-next-line no-secrets/no-secrets -- the emitted readonly-array type annotation is dense generated TS, not a credential
|
|
2039
|
+
specs: `
|
|
2040
|
+
/** Wiring specs for \`ctx.queues\` (codegen-derived from \`lunora/queues.ts\`). */
|
|
2041
|
+
const LUNORA_QUEUES: ReadonlyArray<QueueBindingSpec> = [
|
|
2042
|
+
${specEntries}
|
|
2043
|
+
];
|
|
2044
|
+
`
|
|
2045
|
+
};
|
|
2046
|
+
};
|
|
1363
2047
|
const emitWorkflowsMetadataFragments = (workflows) => {
|
|
1364
2048
|
if (workflows.length === 0) {
|
|
1365
2049
|
return { constant: "", override: "" };
|
|
@@ -1386,6 +2070,33 @@ const LUNORA_WORKFLOWS_INFO: WorkflowsResult = ${JSON.stringify(metadata, void 0
|
|
|
1386
2070
|
`
|
|
1387
2071
|
};
|
|
1388
2072
|
};
|
|
2073
|
+
const emitQueuesMetadataFragments = (queues) => {
|
|
2074
|
+
if (queues.length === 0) {
|
|
2075
|
+
return { constant: "", override: "" };
|
|
2076
|
+
}
|
|
2077
|
+
const metadata = {
|
|
2078
|
+
queues: queues.map((queue) => {
|
|
2079
|
+
return {
|
|
2080
|
+
binding: queue.bindingName,
|
|
2081
|
+
...queue.tuning.deadLetterQueue === void 0 ? {} : { deadLetterQueue: queue.tuning.deadLetterQueue },
|
|
2082
|
+
exportName: queue.exportName,
|
|
2083
|
+
mode: queue.mode,
|
|
2084
|
+
name: queue.name
|
|
2085
|
+
};
|
|
2086
|
+
})
|
|
2087
|
+
};
|
|
2088
|
+
return {
|
|
2089
|
+
constant: `
|
|
2090
|
+
/** Read-only declared-queue metadata (discovered from \`lunora/queues.ts\`) served via \`__lunora_admin__:listQueues\` for the studio's queues view. */
|
|
2091
|
+
const LUNORA_QUEUES_INFO: QueuesResult = ${JSON.stringify(metadata, void 0, 4)};
|
|
2092
|
+
`,
|
|
2093
|
+
override: `
|
|
2094
|
+
protected override queuesMetadata(): QueuesResult {
|
|
2095
|
+
return LUNORA_QUEUES_INFO;
|
|
2096
|
+
}
|
|
2097
|
+
`
|
|
2098
|
+
};
|
|
2099
|
+
};
|
|
1389
2100
|
const emitPaymentFragments = (hasPayments) => {
|
|
1390
2101
|
if (!hasPayments) {
|
|
1391
2102
|
return { build: "", configField: "", contextField: "", imports: [], stub: "" };
|
|
@@ -1441,13 +2152,41 @@ const paymentStub: LunoraPayment = {
|
|
|
1441
2152
|
`
|
|
1442
2153
|
};
|
|
1443
2154
|
};
|
|
1444
|
-
const
|
|
2155
|
+
const emitX402Fragments = (hasX402) => {
|
|
2156
|
+
if (!hasX402) {
|
|
2157
|
+
return { build: "", configField: "", contextField: "", imports: [], stub: "" };
|
|
2158
|
+
}
|
|
2159
|
+
return {
|
|
2160
|
+
imports: [`import type { X402Pay, X402PayConfig } from "@lunora/x402/pay";`, `import { lazyX402Pay } from "@lunora/x402/pay";`],
|
|
2161
|
+
// Built lazily off `secrets` (the Secrets Store facade already in scope) and
|
|
2162
|
+
// the `config.x402` thunk over env; falls back to `x402Stub`.
|
|
2163
|
+
build: `
|
|
2164
|
+
const x402: X402Pay = config.x402
|
|
2165
|
+
? lazyX402Pay(config.x402(env), { getSecret: (name: string) => secrets.get(name) })
|
|
2166
|
+
: x402Stub;
|
|
2167
|
+
`,
|
|
2168
|
+
configField: `
|
|
2169
|
+
x402?: (env: Record<string, unknown>) => X402PayConfig;`,
|
|
2170
|
+
contextField: `
|
|
2171
|
+
x402,`,
|
|
2172
|
+
stub: `
|
|
2173
|
+
const x402Stub: X402Pay = {
|
|
2174
|
+
fetch: () => {
|
|
2175
|
+
throw new Error("ctx.x402: no pay rail configured. Pass \\\`x402\\\` to createShardDO().");
|
|
2176
|
+
},
|
|
2177
|
+
} as unknown as X402Pay;
|
|
2178
|
+
`
|
|
2179
|
+
};
|
|
2180
|
+
};
|
|
2181
|
+
const buildDoTypeImports = (hasVectors, hasWorkflows, hasQueues, hasFlags) => [
|
|
1445
2182
|
"AdvisoryFinding",
|
|
1446
2183
|
"DatabaseWriterLike",
|
|
1447
2184
|
"DataMigrationLike",
|
|
2185
|
+
...hasFlags ? ["FlagsResult"] : [],
|
|
1448
2186
|
"LogSink",
|
|
1449
2187
|
"MaskPoliciesResult",
|
|
1450
2188
|
"MigrationRunResult",
|
|
2189
|
+
...hasQueues ? ["QueuesResult"] : [],
|
|
1451
2190
|
"RunShardApplyCdcArgs",
|
|
1452
2191
|
"RunShardMigrationArgs",
|
|
1453
2192
|
"RlsPoliciesResult",
|
|
@@ -1469,36 +2208,53 @@ const buildDoTypeImports = (hasVectors, hasWorkflows) => [
|
|
|
1469
2208
|
const emitShard = ({
|
|
1470
2209
|
advisories = [],
|
|
1471
2210
|
containers = [],
|
|
2211
|
+
env,
|
|
2212
|
+
flagKeys = [],
|
|
2213
|
+
hasAccessFacade = false,
|
|
1472
2214
|
hasAi = false,
|
|
1473
2215
|
hasAnalytics = false,
|
|
1474
2216
|
hasBrowser = false,
|
|
2217
|
+
hasFlags = false,
|
|
1475
2218
|
hasHyperdrive = false,
|
|
1476
2219
|
hasImages = false,
|
|
1477
2220
|
hasKv = false,
|
|
1478
2221
|
hasPayments = false,
|
|
2222
|
+
hasPipelines = false,
|
|
1479
2223
|
hasR2sql = false,
|
|
2224
|
+
hasX402 = false,
|
|
1480
2225
|
maskMetadata,
|
|
2226
|
+
mutators = [],
|
|
2227
|
+
queues = [],
|
|
1481
2228
|
rlsMetadata,
|
|
1482
2229
|
schema,
|
|
2230
|
+
shapes = [],
|
|
1483
2231
|
storageRules,
|
|
1484
2232
|
studioFeatures,
|
|
1485
2233
|
useUmbrella = false,
|
|
1486
2234
|
workflows = []
|
|
1487
2235
|
}) => {
|
|
1488
2236
|
const base = baseSpecifiers(useUmbrella);
|
|
2237
|
+
const hasMutators = mutators.length > 0;
|
|
2238
|
+
const hasShapes = shapes.length > 0;
|
|
1489
2239
|
const { build: aiBuild, configField: aiConfigField, contextField: aiContextField, stub: aiStub } = emitAiFragments(hasAi);
|
|
2240
|
+
const accessFragments = emitAccessFragments(hasAccessFacade);
|
|
1490
2241
|
const kvFragments = emitKvFragments(hasKv);
|
|
2242
|
+
const flagsFragments = emitFlagsFragments(hasFlags, base.flags);
|
|
2243
|
+
const flagsOverrides = emitFlagsOverrides(flagKeys, hasFlags, base.flags);
|
|
2244
|
+
const envFragments = emitEnvFragments(env);
|
|
1491
2245
|
const analyticsFragments = emitAnalyticsFragments(hasAnalytics);
|
|
1492
2246
|
const imagesFragments = emitImagesFragments(hasImages);
|
|
1493
2247
|
const hyperdriveFragments = emitHyperdriveFragments(hasHyperdrive);
|
|
1494
2248
|
const browserFragments = emitBrowserFragments(hasBrowser);
|
|
1495
2249
|
const r2sqlFragments = emitR2sqlFragments(hasR2sql);
|
|
2250
|
+
const pipelinesFragments = emitPipelinesFragments(hasPipelines);
|
|
2251
|
+
const { build: queuesBuild, contextField: queuesContextField, importLines: queueImportLines, specs: queueSpecs } = emitQueueFragments(queues);
|
|
1496
2252
|
const {
|
|
1497
2253
|
build: containersBuild,
|
|
1498
2254
|
contextField: containersContextField,
|
|
1499
2255
|
importLines: containerImportLines,
|
|
1500
2256
|
specs: containerSpecs
|
|
1501
|
-
} = emitContainerFragments(containers);
|
|
2257
|
+
} = emitContainerFragments(containers, schema.jurisdiction);
|
|
1502
2258
|
const {
|
|
1503
2259
|
build: workflowsBuild,
|
|
1504
2260
|
contextField: workflowsContextField,
|
|
@@ -1512,25 +2268,35 @@ const emitShard = ({
|
|
|
1512
2268
|
imports: paymentsImports,
|
|
1513
2269
|
stub: paymentStub
|
|
1514
2270
|
} = emitPaymentFragments(hasPayments);
|
|
2271
|
+
const { build: x402Build, configField: x402ConfigField, imports: x402Imports, stub: x402Stub } = emitX402Fragments(hasX402);
|
|
1515
2272
|
const advisoryData = advisories;
|
|
1516
2273
|
const rlsData = rlsMetadata ?? { policies: [], roles: [] };
|
|
1517
2274
|
const maskData = maskMetadata ?? { columns: [] };
|
|
1518
2275
|
const storageRulesData = storageRules ?? { rules: [] };
|
|
1519
2276
|
const studioFeaturesData = studioFeatures ?? {
|
|
2277
|
+
analytics: false,
|
|
2278
|
+
auth: false,
|
|
2279
|
+
containers: false,
|
|
2280
|
+
flags: false,
|
|
2281
|
+
kv: false,
|
|
1520
2282
|
mail: false,
|
|
1521
2283
|
payments: false,
|
|
2284
|
+
queues: false,
|
|
1522
2285
|
scheduler: false,
|
|
1523
2286
|
storage: false,
|
|
1524
2287
|
vectors: false,
|
|
1525
2288
|
workflows: false
|
|
1526
2289
|
};
|
|
1527
2290
|
const { constant: workflowsMetadataConst, override: workflowsMetadataOverride } = emitWorkflowsMetadataFragments(workflows);
|
|
2291
|
+
const { constant: queuesMetadataConst, override: queuesMetadataOverride } = emitQueuesMetadataFragments(queues);
|
|
1528
2292
|
const hasVectors = schema.vectorIndexes.length > 0;
|
|
1529
2293
|
const hasGlobalTables = schema.tables.some((table) => table.shardMode === "global");
|
|
1530
2294
|
const hasHyperdriveGlobal = schema.tables.some((table) => table.shardMode === "global" && table.globalBackend === "hyperdrive");
|
|
1531
2295
|
const hasD1Global = schema.tables.some((table) => table.shardMode === "global" && table.globalBackend !== "hyperdrive");
|
|
2296
|
+
const hasSourcedTables = schema.tables.some((table) => table.externalSource !== void 0);
|
|
1532
2297
|
if (hasD1Global && hasHyperdriveGlobal) {
|
|
1533
|
-
throw new
|
|
2298
|
+
throw new LunoraError(
|
|
2299
|
+
"INTERNAL",
|
|
1534
2300
|
'lunora codegen: mixing `.global()` (D1) and `.global({ backend: "hyperdrive" })` tables in one app is not supported yet — use a single global backend.'
|
|
1535
2301
|
);
|
|
1536
2302
|
}
|
|
@@ -1539,41 +2305,115 @@ const emitShard = ({
|
|
|
1539
2305
|
const tableIndexes = buildTableIndexes(schema);
|
|
1540
2306
|
const tableColumns = buildTableColumns(schema);
|
|
1541
2307
|
const storageColumns = buildStorageColumns(schema);
|
|
1542
|
-
const doTypeImports = buildDoTypeImports(hasVectors, workflows.length > 0);
|
|
2308
|
+
const doTypeImports = buildDoTypeImports(hasVectors, workflows.length > 0, queues.length > 0, hasFlags);
|
|
2309
|
+
if (hasShapes) {
|
|
2310
|
+
doTypeImports.push("WhereInput");
|
|
2311
|
+
}
|
|
1543
2312
|
const relationFanout = emitRelationFanout(hasGlobalTables);
|
|
2313
|
+
const shapeResolveOverride = hasShapes ? `
|
|
2314
|
+
protected override resolveShape(name: string, args: Record<string, unknown>, identity?: { identity?: Record<string, unknown>; userId?: string }): { columns?: readonly string[]; effectiveWhere?: WhereInput; global?: boolean; table: string } | undefined {
|
|
2315
|
+
const shape = LUNORA_SHAPES[name];
|
|
2316
|
+
|
|
2317
|
+
if (!shape) {
|
|
2318
|
+
return undefined;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
this.ensureMigrated();
|
|
2322
|
+
|
|
2323
|
+
// Trusted ctx from the socket's OWN verified identity — the client
|
|
2324
|
+
// supplies only the shape name + args; \`compileWhere\` validates the
|
|
2325
|
+
// args then runs the shape's \`where(ctx, args)\` under that identity,
|
|
2326
|
+
// so which rows replicate is a server decision (reads-as-permissions).
|
|
2327
|
+
const ctx = this.buildCtx({ functionPath: \`__shape__:\${name}\`, identity });
|
|
2328
|
+
const shapeWhere = shape.compileWhere(ctx, args) as unknown as WhereInput;
|
|
2329
|
+
|
|
2330
|
+
// AND-compose with the table's RLS read base-where. A shape runs no
|
|
2331
|
+
// procedure, so the \`.use(rls(...))\` middleware never fires; without
|
|
2332
|
+
// this merge its reads would bypass every read policy on the table
|
|
2333
|
+
// (rows the caller can't see would replicate). \`composeShapeReadWhere\`
|
|
2334
|
+
// evaluates the table's read policies under this same trusted ctx —
|
|
2335
|
+
// exactly the request-time path — and fails closed under a
|
|
2336
|
+
// \`.rls("required")\` schema for a non-\`.public()\`, policy-less table.
|
|
2337
|
+
const effectiveWhere = composeShapeReadWhere(LUNORA_RLS_READ_REGISTRY, {
|
|
2338
|
+
ctx,
|
|
2339
|
+
identity: identity?.identity ?? null,
|
|
2340
|
+
rlsRequired: (schema as unknown as { rlsMode?: string }).rlsMode === "required",
|
|
2341
|
+
roles: (ctx as { auth?: { roles?: readonly string[] } }).auth?.roles ?? [],
|
|
2342
|
+
shapeWhere,
|
|
2343
|
+
table: shape.table,
|
|
2344
|
+
tablePublic: (schema as unknown as { tables: Record<string, { isPublic?: boolean }> }).tables[shape.table]?.isPublic === true,
|
|
2345
|
+
userId: identity?.userId ?? null,
|
|
2346
|
+
});
|
|
2347
|
+
|
|
2348
|
+
// A live shape pokes only from its OWN shard's op-log, so a \`where()\`
|
|
2349
|
+
// that joins to a \`.shardBy()\` table (rows in another DO) is rejected
|
|
2350
|
+
// here — the first point the compiled predicate + shard modes are both
|
|
2351
|
+
// known. Remedy: denormalize, or move the joined table to \`.global()\`.
|
|
2352
|
+
assertShapeShardable(effectiveWhere, schema as unknown as SchemaLike, shape.table);
|
|
2353
|
+
|
|
2354
|
+
// A \`.global()\` table lives in D1 (no per-DO op-log): flag it so the
|
|
2355
|
+
// base serves it through the latency-tiered poll path (\`readGlobalShapeRows\`)
|
|
2356
|
+
// instead of the CDC poke path.
|
|
2357
|
+
const isGlobal = (schema as unknown as SchemaLike).tables[shape.table]?.shardMode?.kind === "global";
|
|
2358
|
+
|
|
2359
|
+
return { columns: shape.columns, effectiveWhere, global: isGlobal, table: shape.table };
|
|
2360
|
+
}
|
|
2361
|
+
` : "";
|
|
2362
|
+
const customMutatorOverride = hasMutators ? `
|
|
2363
|
+
protected override isCustomMutator(functionPath: string): boolean {
|
|
2364
|
+
return LUNORA_MUTATOR_PATHS.has(functionPath);
|
|
2365
|
+
}
|
|
2366
|
+
` : "";
|
|
2367
|
+
const shapeReadRegistryConst = hasShapes ? `
|
|
2368
|
+
/** Per-table RLS read policies (hoisted from \`.use(rls(...))\` chains) the shape resolver AND-merges into each \`defineShape\` predicate so partial replication honours read policies. */
|
|
2369
|
+
const LUNORA_RLS_READ_REGISTRY = buildRlsReadRegistry(Object.values(LUNORA_FUNCTIONS));
|
|
2370
|
+
` : "";
|
|
2371
|
+
const shapeGuardImport = hasShapes ? "assertShapeShardable, " : "";
|
|
1544
2372
|
const importLines = [
|
|
1545
2373
|
`import type { ${doTypeImports.join(", ")} } from "${base.do}";`,
|
|
1546
|
-
`import { applyCdcChanges, createShardCtxDb, runDataMigration, runShardMigrations, ${relationFanout.importFragment}ShardDO as ShardDOBase } from "${base.do}";`,
|
|
1547
|
-
|
|
2374
|
+
`import { applyCdcChanges, ${shapeGuardImport}createShardCtxDb, ${hasSourcedTables ? "isSourceDue, pullExternalSourceTick, " : ""}runDataMigration, runShardMigrations, ${relationFanout.importFragment}ShardDO as ShardDOBase } from "${base.do}";`,
|
|
2375
|
+
...hasSourcedTables ? [`import type { ExternalSourceLike, SourceClientLike } from "${base.do}";`] : [],
|
|
2376
|
+
// `asBucketStorage` (the bucket-aware `ctx.storage` wrapper) and
|
|
2377
|
+
// `createSecrets` (the `ctx.secrets` core built-in) live in
|
|
1548
2378
|
// `@lunora/server`, the single source — imported here rather than stamped
|
|
1549
|
-
// inline into every generated shard.
|
|
1550
|
-
|
|
2379
|
+
// inline into every generated shard. With shapes, also pull the RLS
|
|
2380
|
+
// read-registry builder + `composeShapeReadWhere` so `resolveShape` can
|
|
2381
|
+
// AND-merge a shape's predicate with the table's read base-where.
|
|
2382
|
+
hasShapes ? `import { asBucketStorage, buildRlsReadRegistry, composeShapeReadWhere, createSecrets, LunoraError } from "${base.server}";` : `import { asBucketStorage, createSecrets, LunoraError } from "${base.server}";`
|
|
1551
2383
|
];
|
|
1552
2384
|
if (hasTables) {
|
|
1553
2385
|
importLines.push(`import { bindOrm, bindTableFacade } from "${base.server}";`);
|
|
1554
2386
|
}
|
|
1555
2387
|
if (hasVectors) {
|
|
1556
2388
|
importLines.push(
|
|
1557
|
-
`import type { SchemaLike as VectorSchemaLike, VectorizeIndexLike, VectorSearchLike } from "@lunora/vectors";`,
|
|
1558
|
-
`import { createContextVectors, createVectors, createVectorSyncHook } from "@lunora/vectors";`
|
|
2389
|
+
`import type { SchemaLike as VectorSchemaLike, VectorizeIndexLike, VectorSearchLike } from "@lunora/bindings/vectors";`,
|
|
2390
|
+
`import { createContextVectors, createVectors, createVectorSyncHook } from "@lunora/bindings/vectors";`
|
|
1559
2391
|
);
|
|
1560
2392
|
}
|
|
1561
2393
|
if (hasAi) {
|
|
1562
2394
|
importLines.push(`import type { AiBindingLike, LunoraAi } from "@lunora/ai";`, `import { createAi } from "@lunora/ai";`);
|
|
1563
2395
|
}
|
|
1564
2396
|
importLines.push(
|
|
2397
|
+
...accessFragments.importLines,
|
|
1565
2398
|
...kvFragments.importLines,
|
|
2399
|
+
...flagsFragments.importLines,
|
|
2400
|
+
...envFragments.importLines,
|
|
1566
2401
|
...analyticsFragments.importLines,
|
|
1567
2402
|
...imagesFragments.importLines,
|
|
1568
2403
|
...hyperdriveFragments.importLines,
|
|
1569
2404
|
...browserFragments.importLines,
|
|
1570
2405
|
...r2sqlFragments.importLines,
|
|
2406
|
+
...pipelinesFragments.importLines,
|
|
1571
2407
|
...containerImportLines,
|
|
1572
2408
|
...workflowImportLines,
|
|
2409
|
+
...queueImportLines,
|
|
1573
2410
|
...paymentsImports,
|
|
2411
|
+
...x402Imports,
|
|
1574
2412
|
``,
|
|
1575
2413
|
`import schema from "../schema.js";`,
|
|
1576
|
-
|
|
2414
|
+
// Local-first sync registries are pulled in alongside the function table
|
|
2415
|
+
// only when the project declares them, so the import list stays minimal.
|
|
2416
|
+
`import { ${["LUNORA_FUNCTIONS", "LUNORA_LIFECYCLE_HOOKS", "LUNORA_MIGRATIONS", ...hasMutators ? ["LUNORA_MUTATOR_PATHS"] : [], ...hasShapes ? ["LUNORA_SHAPES"] : []].join(", ")} } from "./functions.js";`
|
|
1577
2417
|
);
|
|
1578
2418
|
const vectorsConfigField = hasVectors ? `
|
|
1579
2419
|
vectors?: (env: Record<string, unknown>) => Record<string, VectorizeIndexLike>;` : "";
|
|
@@ -1581,6 +2421,8 @@ const emitShard = ({
|
|
|
1581
2421
|
d1?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;` : "";
|
|
1582
2422
|
const hyperdriveGlobalConfigField = hasHyperdriveGlobal ? `
|
|
1583
2423
|
hyperdriveGlobal?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;` : "";
|
|
2424
|
+
const sourceClientConfigField = hasSourcedTables ? `
|
|
2425
|
+
sourceClient?: (env: Record<string, unknown>, binding: string) => { query: <Row = Record<string, unknown>>(text: string, params?: readonly unknown[]) => Promise<Row[]> } | undefined;` : "";
|
|
1584
2426
|
const globalDatabaseStub = hasGlobalTables ? `
|
|
1585
2427
|
const globalDbStub: DatabaseWriterLike = {
|
|
1586
2428
|
aggregate: async () => {
|
|
@@ -1698,6 +2540,144 @@ const vectorsStub: VectorSearchLike = {
|
|
|
1698
2540
|
const bindTableHelper = "";
|
|
1699
2541
|
const globalDatabaseThunk = hasHyperdriveGlobal ? "config.hyperdriveGlobal" : "config.d1";
|
|
1700
2542
|
const globalDatabaseLine = hasGlobalTables ? ` const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, { identity, userId }) ?? globalDbStub;
|
|
2543
|
+
` : "";
|
|
2544
|
+
const globalShapeReaderOverride = hasShapes && hasGlobalTables ? `
|
|
2545
|
+
protected override async readGlobalShapeRows(resolved: { columns?: readonly string[]; effectiveWhere?: WhereInput; global?: boolean; table: string }, identity?: { identity?: Record<string, unknown>; userId?: string }): Promise<Array<{ doc: Record<string, unknown>; id: string }>> {
|
|
2546
|
+
const env = this.env as Record<string, unknown>;
|
|
2547
|
+
const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, identity) ?? globalDbStub;
|
|
2548
|
+
const rows: Array<{ doc: Record<string, unknown>; id: string }> = [];
|
|
2549
|
+
|
|
2550
|
+
let cursor: null | string = null;
|
|
2551
|
+
|
|
2552
|
+
// Drain every page of the global membership so the seed/diff sees the
|
|
2553
|
+
// full rowset (D1 \`findMany\` is paginated). Stop one row past the cap:
|
|
2554
|
+
// a broad \`.global()\` shape would otherwise materialize an unbounded
|
|
2555
|
+
// array before the caller's \`withinGlobalShapeBound\` check rejects it,
|
|
2556
|
+
// so bail early and let the caller fail it closed.
|
|
2557
|
+
do {
|
|
2558
|
+
// eslint-disable-next-line no-await-in-loop -- sequential page drain to assemble the full membership
|
|
2559
|
+
const page = await globalDb.findMany(resolved.table, { cursor, where: resolved.effectiveWhere });
|
|
2560
|
+
|
|
2561
|
+
for (const doc of page.page) {
|
|
2562
|
+
rows.push({ doc, id: String((doc as { _id?: unknown })._id) });
|
|
2563
|
+
|
|
2564
|
+
if (rows.length > ShardDOBase.GLOBAL_SHAPE_MAX_ROWS) {
|
|
2565
|
+
return rows;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
cursor = page.isDone ? null : page.continueCursor;
|
|
2570
|
+
} while (cursor !== null);
|
|
2571
|
+
|
|
2572
|
+
return rows;
|
|
2573
|
+
}
|
|
2574
|
+
` : "";
|
|
2575
|
+
const sourceClientCacheConst = hasSourcedTables ? `
|
|
2576
|
+
const sourceClientCache = new WeakMap<object, Map<string, SourceClientLike>>();
|
|
2577
|
+
const sourcePollAtCache = new WeakMap<object, Map<string, number>>();
|
|
2578
|
+
` : "";
|
|
2579
|
+
const externalSourceOverride = hasSourcedTables ? `
|
|
2580
|
+
protected override async pollExternalSources(): Promise<number> {
|
|
2581
|
+
const env = (this.env ?? {}) as Record<string, unknown>;
|
|
2582
|
+
const sourced = Object.entries((schema as unknown as SchemaLike).tables)
|
|
2583
|
+
.map(([table, definition]) => [table, (definition as { externalSource?: ExternalSourceLike }).externalSource] as const)
|
|
2584
|
+
.filter((entry): entry is [string, ExternalSourceLike] => entry[1] !== undefined);
|
|
2585
|
+
|
|
2586
|
+
if (sourced.length === 0) {
|
|
2587
|
+
return 0;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
const shardKey = this.currentShardKey();
|
|
2591
|
+
const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
|
|
2592
|
+
const writer = createShardCtxDb({
|
|
2593
|
+
broadcast: (delta) => {
|
|
2594
|
+
this.recordChangedTable(delta.table);
|
|
2595
|
+
},
|
|
2596
|
+
cdc: config.cdc ?? false,
|
|
2597
|
+
scheduler,
|
|
2598
|
+
schema: schema as unknown as SchemaLike,
|
|
2599
|
+
sql: this.sql as SqlExec,
|
|
2600
|
+
});
|
|
2601
|
+
|
|
2602
|
+
let clients = sourceClientCache.get(this);
|
|
2603
|
+
|
|
2604
|
+
if (clients === undefined) {
|
|
2605
|
+
clients = new Map();
|
|
2606
|
+
sourceClientCache.set(this, clients);
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
let polledAt = sourcePollAtCache.get(this);
|
|
2610
|
+
|
|
2611
|
+
if (polledAt === undefined) {
|
|
2612
|
+
polledAt = new Map();
|
|
2613
|
+
sourcePollAtCache.set(this, polledAt);
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
const now = Date.now();
|
|
2617
|
+
// \`active\` counts non-manual sources: while > 0 the alarm re-arms; a
|
|
2618
|
+
// manual-only schema returns 0 so the shared alarm goes idle.
|
|
2619
|
+
let active = 0;
|
|
2620
|
+
|
|
2621
|
+
for (const [table, source] of sourced) {
|
|
2622
|
+
if (source.refresh === "manual") {
|
|
2623
|
+
continue;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
active += 1;
|
|
2627
|
+
|
|
2628
|
+
if (!isSourceDue(source.refresh, polledAt.get(table), now)) {
|
|
2629
|
+
continue;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
try {
|
|
2633
|
+
let client = clients.get(source.binding);
|
|
2634
|
+
|
|
2635
|
+
if (client === undefined) {
|
|
2636
|
+
client = config.sourceClient?.(env, source.binding);
|
|
2637
|
+
|
|
2638
|
+
if (client !== undefined) {
|
|
2639
|
+
clients.set(source.binding, client);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
if (client === undefined) {
|
|
2644
|
+
// No SqlClient resolved for this binding (host never wired
|
|
2645
|
+
// \`config.sourceClient\`, or wired it wrong). Surface it in the
|
|
2646
|
+
// Logs panel and stamp \`polledAt\` so a persistent misconfig backs
|
|
2647
|
+
// off to \`refresh.everyMs\` instead of retrying every alarm tick.
|
|
2648
|
+
this.recordExternalSourceError(table, new Error(\`external-source: no sourceClient resolved for binding "\${source.binding}"\`));
|
|
2649
|
+
polledAt.set(table, now);
|
|
2650
|
+
continue;
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
// eslint-disable-next-line no-await-in-loop -- one sourced table at a time; slices are independent but small and sequential keeps the writer transaction simple
|
|
2654
|
+
await pullExternalSourceTick(this.sql as SqlExec, writer, client, table, source, shardKey);
|
|
2655
|
+
polledAt.set(table, now);
|
|
2656
|
+
} catch (error) {
|
|
2657
|
+
this.recordExternalSourceError(table, error);
|
|
2658
|
+
// Stamp on failure too, so a persistently failing source throttles
|
|
2659
|
+
// to \`refresh.everyMs\` rather than being hammered every tick.
|
|
2660
|
+
polledAt.set(table, now);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
return active;
|
|
2665
|
+
}
|
|
2666
|
+
` : "";
|
|
2667
|
+
const sourceConstructorOverride = hasSourcedTables ? `
|
|
2668
|
+
public constructor(state: ShardDOState, env: unknown) {
|
|
2669
|
+
super(state, env);
|
|
2670
|
+
|
|
2671
|
+
const autoSourced = Object.values((schema as unknown as SchemaLike).tables).some((definition) => {
|
|
2672
|
+
const source = (definition as { externalSource?: ExternalSourceLike }).externalSource;
|
|
2673
|
+
|
|
2674
|
+
return source !== undefined && source.refresh !== "manual";
|
|
2675
|
+
});
|
|
2676
|
+
|
|
2677
|
+
if (autoSourced) {
|
|
2678
|
+
void this.scheduleSourcePoll();
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
1701
2681
|
` : "";
|
|
1702
2682
|
const facadeBlock = hasTables ? `
|
|
1703
2683
|
const facade = db as unknown as Record<string, ReturnType<typeof bindTableFacade>>;
|
|
@@ -1711,18 +2691,24 @@ ${schema.tables.map(
|
|
|
1711
2691
|
(table) => ` facade[${JSON.stringify(table.name)}] = bindTableFacade(db, ${JSON.stringify(table.name)});`
|
|
1712
2692
|
).join("\n")}
|
|
1713
2693
|
` : "";
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1716
|
-
|
|
2694
|
+
const secretsBuild = `
|
|
2695
|
+
const secrets = createSecrets(env);
|
|
2696
|
+
`;
|
|
2697
|
+
const everyContextBuild = `${accessFragments.build}${kvFragments.build}${flagsFragments.build}${analyticsFragments.build}${envFragments.build}${secretsBuild}`;
|
|
2698
|
+
const everyContextField = `${accessFragments.contextField}${kvFragments.contextField}${flagsFragments.contextField}${analyticsFragments.contextField}${envFragments.contextField}
|
|
2699
|
+
secrets,`;
|
|
2700
|
+
const actionOnlyHasAny = hasImages || hasHyperdrive || hasBrowser || hasR2sql || hasPipelines || hasX402;
|
|
1717
2701
|
const actionOnlyBlock = actionOnlyHasAny ? `
|
|
1718
2702
|
// ActionCtx-only helpers (external, non-deterministic I/O): constructed
|
|
1719
2703
|
// and attached only for an \`action\` so query/mutation ctx never carry them.
|
|
1720
2704
|
if (isAction) {
|
|
1721
|
-
${imagesFragments.build}${hyperdriveFragments.build}${browserFragments.build}${r2sqlFragments.build}${[
|
|
2705
|
+
${imagesFragments.build}${hyperdriveFragments.build}${browserFragments.build}${r2sqlFragments.build}${pipelinesFragments.build}${x402Build}${[
|
|
1722
2706
|
...hasImages ? [" ctx.images = images;"] : [],
|
|
1723
2707
|
...hasHyperdrive ? [" ctx.sql = sql;"] : [],
|
|
1724
2708
|
...hasBrowser ? [" ctx.browser = browser;"] : [],
|
|
1725
|
-
...hasR2sql ? [" ctx.r2sql = r2sql;"] : []
|
|
2709
|
+
...hasR2sql ? [" ctx.r2sql = r2sql;"] : [],
|
|
2710
|
+
...hasPipelines ? [" ctx.pipelines = pipelines;"] : [],
|
|
2711
|
+
...hasX402 ? [" ctx.x402 = x402;"] : []
|
|
1726
2712
|
].join("\n")}
|
|
1727
2713
|
}
|
|
1728
2714
|
` : "";
|
|
@@ -1762,14 +2748,14 @@ const LUNORA_STORAGE_RULES: StorageRulesResult = ${JSON.stringify(storageRulesDa
|
|
|
1762
2748
|
|
|
1763
2749
|
/** Which optional package-backed features this app wires up (discovered from imports / \`ctx.*\` reads / schema signals) served via \`__lunora_admin__:studioFeatures\` so the studio hides nav pages whose package isn't enabled. */
|
|
1764
2750
|
const LUNORA_STUDIO_FEATURES: StudioFeaturesResult = ${JSON.stringify(studioFeaturesData, void 0, 4)};
|
|
1765
|
-
${workflowsMetadataConst}${containerSpecs}${workflowSpecs}
|
|
2751
|
+
${flagsOverrides.constant}${shapeReadRegistryConst}${workflowsMetadataConst}${queuesMetadataConst}${containerSpecs}${workflowSpecs}${queueSpecs}
|
|
1766
2752
|
export interface ShardDOConfig {
|
|
1767
2753
|
/** Opt into change-data-capture: records a post-image to \`__cdc_log\` on every write (backs streaming export + replay-PITR). */
|
|
1768
2754
|
cdc?: boolean;
|
|
1769
2755
|
/** Optional telemetry sink. When supplied, each \`ctx.log.*\` call is forwarded to \`sink.onLog\`. Pass the SAME sink you give \`createWorker({ observability })\` (which drives \`onRpc\`) to route both RPC and log events. */
|
|
1770
2756
|
observability?: (env: Record<string, unknown>) => LogSink | undefined;
|
|
1771
2757
|
scheduler?: (env: Record<string, unknown>) => unknown;
|
|
1772
|
-
storage?: (env: Record<string, unknown>) => unknown;${vectorsConfigField}${aiConfigField}${kvFragments.configField}${analyticsFragments.configField}${imagesFragments.configField}${hyperdriveFragments.configField}${browserFragments.configField}${r2sqlFragments.configField}${paymentsConfigField}${d1ConfigField}${hyperdriveGlobalConfigField}
|
|
2758
|
+
storage?: (env: Record<string, unknown>) => unknown;${vectorsConfigField}${aiConfigField}${kvFragments.configField}${flagsFragments.configField}${analyticsFragments.configField}${imagesFragments.configField}${hyperdriveFragments.configField}${browserFragments.configField}${r2sqlFragments.configField}${pipelinesFragments.configField}${paymentsConfigField}${x402ConfigField}${d1ConfigField}${hyperdriveGlobalConfigField}${sourceClientConfigField}
|
|
1773
2759
|
}
|
|
1774
2760
|
|
|
1775
2761
|
const schedulerStub = {
|
|
@@ -1807,7 +2793,7 @@ const storageStub = {
|
|
|
1807
2793
|
throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
|
|
1808
2794
|
},
|
|
1809
2795
|
};
|
|
1810
|
-
${globalDatabaseStub}${vectorsStub}${aiStub}${kvFragments.stub}${analyticsFragments.stub}${imagesFragments.stub}${hyperdriveFragments.stub}${browserFragments.stub}${r2sqlFragments.stub}${paymentStub}${bindTableHelper}
|
|
2796
|
+
${globalDatabaseStub}${sourceClientCacheConst}${vectorsStub}${aiStub}${kvFragments.stub}${flagsFragments.stub}${analyticsFragments.stub}${imagesFragments.stub}${hyperdriveFragments.stub}${browserFragments.stub}${r2sqlFragments.stub}${pipelinesFragments.stub}${paymentStub}${x402Stub}${bindTableHelper}
|
|
1811
2797
|
// Bound in-process \`ctx.run*\` composition depth so a self- or cyclically-
|
|
1812
2798
|
// referencing call fails loudly with a clear error instead of overflowing the
|
|
1813
2799
|
// stack. Tracked across the awaited handler chain (one DO invocation is
|
|
@@ -1827,11 +2813,7 @@ const dispatchRun = async (expected: FunctionKind, functionPath: string, args: R
|
|
|
1827
2813
|
}
|
|
1828
2814
|
|
|
1829
2815
|
if (runDepth >= MAX_RUN_DEPTH) {
|
|
1830
|
-
throw
|
|
1831
|
-
name: "LunoraError",
|
|
1832
|
-
code: "RUN_DEPTH_EXCEEDED",
|
|
1833
|
-
status: 500,
|
|
1834
|
-
});
|
|
2816
|
+
throw new LunoraError("RUN_DEPTH_EXCEEDED", \`ctx.run*: composition depth limit (\${MAX_RUN_DEPTH}) exceeded — likely a cyclic runQuery/runMutation\`);
|
|
1835
2817
|
}
|
|
1836
2818
|
|
|
1837
2819
|
runDepth += 1;
|
|
@@ -1848,7 +2830,7 @@ const dispatchRun = async (expected: FunctionKind, functionPath: string, args: R
|
|
|
1848
2830
|
* from the worker entry so wrangler binds it by name.
|
|
1849
2831
|
*/
|
|
1850
2832
|
export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOState, env: unknown) => ShardDOBase =>
|
|
1851
|
-
class extends ShardDOBase {
|
|
2833
|
+
class extends ShardDOBase {${sourceConstructorOverride}
|
|
1852
2834
|
private migrated = false;
|
|
1853
2835
|
|
|
1854
2836
|
public override async handleRpc(functionPath: string, args: Record<string, unknown>): Promise<unknown> {
|
|
@@ -1859,11 +2841,7 @@ export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOSt
|
|
|
1859
2841
|
// \`isSystemDispatch()\`). A client RPC never carries that flag, so its
|
|
1860
2842
|
// internals stay not-found and never leak across the external boundary.
|
|
1861
2843
|
if (!registered || (registered.visibility === "internal" && !this.isSystemDispatch())) {
|
|
1862
|
-
throw
|
|
1863
|
-
name: "LunoraError",
|
|
1864
|
-
code: "FUNCTION_NOT_FOUND",
|
|
1865
|
-
status: 404,
|
|
1866
|
-
});
|
|
2844
|
+
throw new LunoraError("FUNCTION_NOT_FOUND", \`function not registered: \${functionPath}\`);
|
|
1867
2845
|
}
|
|
1868
2846
|
|
|
1869
2847
|
this.ensureMigrated();
|
|
@@ -1877,8 +2855,20 @@ export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOSt
|
|
|
1877
2855
|
// do external I/O that can't be rolled back, so both dispatch directly.
|
|
1878
2856
|
// \`ctx.run*\` composition runs inside this span (it never re-enters
|
|
1879
2857
|
// handleRpc); runInTransaction's own guard rejects accidental nesting.
|
|
2858
|
+
//
|
|
2859
|
+
// The replay bookkeeping (idempotency dedup row + custom-mutator
|
|
2860
|
+
// watermark advance) commits INSIDE this span via
|
|
2861
|
+
// \`commitMutationBookkeeping\`, so the writes, the dedup row, and the
|
|
2862
|
+
// watermark are atomic — a crash can't leave the writes durable without
|
|
2863
|
+
// the replay guard.
|
|
1880
2864
|
if (registered.kind === "mutation") {
|
|
1881
|
-
return this.runInTransaction(() =>
|
|
2865
|
+
return this.runInTransaction(async () => {
|
|
2866
|
+
const result = await registered.handler(ctx, args);
|
|
2867
|
+
|
|
2868
|
+
this.commitMutationBookkeeping(result);
|
|
2869
|
+
|
|
2870
|
+
return result;
|
|
2871
|
+
});
|
|
1882
2872
|
}
|
|
1883
2873
|
|
|
1884
2874
|
return registered.handler(ctx, args);
|
|
@@ -1916,7 +2906,7 @@ ${relationFanout.override}
|
|
|
1916
2906
|
iterator: (signal) => (registered.handler as (context: unknown, args: Record<string, unknown>, signal: AbortSignal) => AsyncIterable<unknown>)(this.buildCtx({ functionPath }), args, signal),
|
|
1917
2907
|
};
|
|
1918
2908
|
}
|
|
1919
|
-
|
|
2909
|
+
${customMutatorOverride}${shapeResolveOverride}${globalShapeReaderOverride}${externalSourceOverride}
|
|
1920
2910
|
protected override lifecycleHookPaths(event: "connect" | "disconnect"): readonly string[] {
|
|
1921
2911
|
return LUNORA_LIFECYCLE_HOOKS[event];
|
|
1922
2912
|
}
|
|
@@ -1952,7 +2942,7 @@ ${relationFanout.override}
|
|
|
1952
2942
|
protected override studioFeatures(): StudioFeaturesResult {
|
|
1953
2943
|
return LUNORA_STUDIO_FEATURES;
|
|
1954
2944
|
}
|
|
1955
|
-
${workflowsMetadataOverride}
|
|
2945
|
+
${flagsOverrides.evaluateOverride}${flagsOverrides.subscriptionOverride}${workflowsMetadataOverride}${queuesMetadataOverride}
|
|
1956
2946
|
protected override advisories(): AdvisoryFinding[] {
|
|
1957
2947
|
return LUNORA_ADVISORIES;
|
|
1958
2948
|
}
|
|
@@ -1961,11 +2951,7 @@ ${workflowsMetadataOverride}
|
|
|
1961
2951
|
const migration = LUNORA_MIGRATIONS[args.id];
|
|
1962
2952
|
|
|
1963
2953
|
if (!migration) {
|
|
1964
|
-
throw
|
|
1965
|
-
name: "LunoraError",
|
|
1966
|
-
code: "MIGRATION_NOT_FOUND",
|
|
1967
|
-
status: 404,
|
|
1968
|
-
});
|
|
2954
|
+
throw new LunoraError("MIGRATION_NOT_FOUND", \`data migration "\${args.id}" is not registered\`, { status: 404 });
|
|
1969
2955
|
}
|
|
1970
2956
|
|
|
1971
2957
|
this.ensureMigrated();
|
|
@@ -2001,18 +2987,14 @@ ${workflowsMetadataOverride}
|
|
|
2001
2987
|
const definition = (schema as unknown as SchemaLike).tables[args.table];
|
|
2002
2988
|
|
|
2003
2989
|
if (!definition) {
|
|
2004
|
-
throw
|
|
2990
|
+
throw new LunoraError("UNKNOWN_TABLE", \`unknown table: \${args.table}\`, { status: 404 });
|
|
2005
2991
|
}
|
|
2006
2992
|
|
|
2007
2993
|
// \`.global()\` tables live in D1, not this DO's SQLite — editing them
|
|
2008
2994
|
// here would corrupt nothing but would fail confusingly, so reject up
|
|
2009
2995
|
// front with a clear code the studio can surface.
|
|
2010
2996
|
if (definition.shardMode?.kind === "global") {
|
|
2011
|
-
throw
|
|
2012
|
-
name: "LunoraError",
|
|
2013
|
-
code: "GLOBAL_TABLE_NOT_EDITABLE",
|
|
2014
|
-
status: 400,
|
|
2015
|
-
});
|
|
2997
|
+
throw new LunoraError("GLOBAL_TABLE_NOT_EDITABLE", \`table "\${args.table}" is global; edit it through D1, not the shard\`, { status: 400 });
|
|
2016
2998
|
}
|
|
2017
2999
|
|
|
2018
3000
|
this.ensureMigrated();
|
|
@@ -2056,17 +3038,13 @@ ${workflowsMetadataOverride}
|
|
|
2056
3038
|
const definition = (schema as unknown as SchemaLike).tables[table];
|
|
2057
3039
|
|
|
2058
3040
|
if (!definition) {
|
|
2059
|
-
throw
|
|
3041
|
+
throw new LunoraError("UNKNOWN_TABLE", \`unknown table: \${table}\`, { status: 404 });
|
|
2060
3042
|
}
|
|
2061
3043
|
|
|
2062
3044
|
// \`.global()\` tables live in D1, not this DO's SQLite — the same
|
|
2063
3045
|
// guard \`runShardWrite\` applies to single-row edits.
|
|
2064
3046
|
if (definition.shardMode?.kind === "global") {
|
|
2065
|
-
throw
|
|
2066
|
-
name: "LunoraError",
|
|
2067
|
-
code: "GLOBAL_TABLE_NOT_EDITABLE",
|
|
2068
|
-
status: 400,
|
|
2069
|
-
});
|
|
3047
|
+
throw new LunoraError("GLOBAL_TABLE_NOT_EDITABLE", \`table "\${table}" is global; edit it through D1, not the shard\`, { status: 400 });
|
|
2070
3048
|
}
|
|
2071
3049
|
|
|
2072
3050
|
this.ensureMigrated();
|
|
@@ -2107,7 +3085,7 @@ ${workflowsMetadataOverride}
|
|
|
2107
3085
|
// \`rankBefore\` is optional on \`DatabaseWriterLike\` (the D1 twin omits it),
|
|
2108
3086
|
// but the shard writer from \`createShardCtxDb\` always defines it.
|
|
2109
3087
|
if (!writer.rankBefore) {
|
|
2110
|
-
throw
|
|
3088
|
+
throw new LunoraError("NOT_IMPLEMENTED", "rankBefore is unavailable on the shard writer", { status: 500 });
|
|
2111
3089
|
}
|
|
2112
3090
|
|
|
2113
3091
|
return writer.rankBefore(args.table, args.index, {
|
|
@@ -2137,7 +3115,7 @@ ${workflowsMetadataOverride}
|
|
|
2137
3115
|
// directions live in the schema's rankIndex, so the shard reads them itself;
|
|
2138
3116
|
// \`args.directions\` is only the coordinator's comparator hint and isn't forwarded.
|
|
2139
3117
|
if (!writer.rankPageRows) {
|
|
2140
|
-
throw
|
|
3118
|
+
throw new LunoraError("NOT_IMPLEMENTED", "rankPage is unavailable on the shard writer", { status: 500 });
|
|
2141
3119
|
}
|
|
2142
3120
|
|
|
2143
3121
|
return writer.rankPageRows(args.table, args.index, {
|
|
@@ -2186,7 +3164,7 @@ ${workflowsMetadataOverride}
|
|
|
2186
3164
|
// dispatch path) fall back to the per-request fields as before.
|
|
2187
3165
|
const userId = options.identity ? options.identity.userId : this.getCurrentUserId();
|
|
2188
3166
|
const identity = options.identity ? options.identity.identity : this.getCurrentIdentity();
|
|
2189
|
-
${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}
|
|
3167
|
+
${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}${queuesBuild}
|
|
2190
3168
|
const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
|
|
2191
3169
|
// Build the storage adapter once and share it between \`ctx.storage\`
|
|
2192
3170
|
// and \`ctx.db.system._storage\` so both read the same R2 binding. The
|
|
@@ -2228,7 +3206,7 @@ ${facadeBlock}${paymentsBuild}
|
|
|
2228
3206
|
log,
|
|
2229
3207
|
now,${ormContextField}
|
|
2230
3208
|
scheduler,
|
|
2231
|
-
storage,${vectorsContextField}${aiContextField}${everyContextField}${paymentsContextField}${containersContextField}${workflowsContextField}
|
|
3209
|
+
storage,${vectorsContextField}${aiContextField}${everyContextField}${paymentsContextField}${containersContextField}${workflowsContextField}${queuesContextField}
|
|
2232
3210
|
};
|
|
2233
3211
|
${isActionLine}${actionOnlyBlock}
|
|
2234
3212
|
ctx.runAction = (reference: FunctionReference, fnArgs: Record<string, unknown>) => dispatchRun("action", reference.__lunoraRef, fnArgs, ctx);
|
|
@@ -2492,4 +3470,4 @@ const emitWranglerCronTriggers = (crons) => {
|
|
|
2492
3470
|
return triggers;
|
|
2493
3471
|
};
|
|
2494
3472
|
|
|
2495
|
-
export {
|
|
3473
|
+
export { APP_METHOD_CAPABILITIES as A, CAPABILITIES as C, GENERATED_HEADER as G, emitCollections as a, emitContainers as b, emitCrons as c, emitDataModel as d, emitApi as e, emitDrizzleSchema as f, emitFunctions as g, emitServer as h, emitShard as i, emitVectors as j, emitWorkflows as k, emitWranglerCronTriggers as l, buildStorageColumns as m, emitQueues as n, emitSeed as o };
|