@lunora/codegen 1.0.0-alpha.55 → 1.0.0-alpha.57
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/dist/index.d.mts +59 -5
- package/dist/index.d.ts +59 -5
- package/dist/index.mjs +1 -38
- package/dist/packem_shared/AGENTS_FILENAME-BesaDYkL.mjs +1 -0
- package/dist/packem_shared/CONTAINERS_FILENAME-CFwdyaCI.mjs +1 -0
- package/dist/packem_shared/CodegenDiagnosticError-DPezpZTz.mjs +1 -0
- package/dist/packem_shared/FLAGS_FILENAME-DW5oGq1q.mjs +1 -0
- package/dist/packem_shared/GENERATED_HEADER-hlHbtIBb.mjs +1 -0
- package/dist/packem_shared/LUNORA_ERROR_CODES-Um9hC1gr.mjs +1 -0
- package/dist/packem_shared/MUTATORS_FILENAME-BKKPROzu.mjs +1 -0
- package/dist/packem_shared/NOTIFY_FILENAME-C-c7Lg-U.mjs +1 -0
- package/dist/packem_shared/OPENRPC_VERSION-DWoqALFJ.mjs +3 -0
- package/dist/packem_shared/QUEUES_FILENAME-CSE6i-ld.mjs +1 -0
- package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-Cuf8KyVF.mjs +3 -0
- package/dist/packem_shared/SCHEMA_SNAPSHOT_VERSION-Bpf1FGMG.mjs +15 -0
- package/dist/packem_shared/SHAPES_FILENAME-DTkSJSsg.mjs +1 -0
- package/dist/packem_shared/WORKFLOWS_FILENAME-Bk5upkGM.mjs +1 -0
- package/dist/packem_shared/buildOpenApiDocument-BRCc5AIx.mjs +3 -0
- package/dist/packem_shared/discover-ast-CezKqEhE.mjs +1 -0
- package/dist/packem_shared/discoverAuthApiCalls-DOfSQqVK.mjs +1 -0
- package/dist/packem_shared/discoverCrons-B09_uEuL.mjs +1 -0
- package/dist/packem_shared/discoverFunctions-BdibLQmc.mjs +1 -0
- package/dist/packem_shared/discoverHttpRoutes-BOWz6y_n.mjs +1 -0
- package/dist/packem_shared/discoverInserts-BtteTyNy.mjs +1 -0
- package/dist/packem_shared/discoverMaskProcedures-COPmwMBm.mjs +1 -0
- package/dist/packem_shared/discoverMigrations-iGMlp9YJ.mjs +1 -0
- package/dist/packem_shared/discoverNondeterministicCalls-Dmgc4QBx.mjs +1 -0
- package/dist/packem_shared/discoverQueries-CcfuGKHh.mjs +1 -0
- package/dist/packem_shared/discoverR2sqlCalls-CjZKvg-N.mjs +1 -0
- package/dist/packem_shared/discoverRlsMetadata-BXmNOgry.mjs +1 -0
- package/dist/packem_shared/discoverSandboxUsage-CDdBqWcM.mjs +1 -0
- package/dist/packem_shared/discoverSchema-m2voTncu.mjs +1 -0
- package/dist/packem_shared/discoverStorageRulesMetadata-b71cJJHz.mjs +1 -0
- package/dist/packem_shared/emit-BYVxPDqO.mjs +2127 -0
- package/dist/packem_shared/emitApp-CHCOfQer.mjs +413 -0
- package/dist/packem_shared/formatAdvisories-DZvv-0MG.mjs +2 -0
- package/dist/packem_shared/parse-validator-CSDlMEvE.mjs +1 -0
- package/dist/packem_shared/paths-BmX5O1sG.mjs +1 -0
- package/dist/packem_shared/redact-CQ8-to6l.mjs +1 -0
- package/dist/packem_shared/schemaFromIr-R1ZFzVyy.mjs +1 -0
- package/package.json +3 -3
- package/dist/packem_shared/AGENTS_FILENAME-Cxx7wiSs.mjs +0 -158
- package/dist/packem_shared/CONTAINERS_FILENAME-DjpXMqhp.mjs +0 -224
- package/dist/packem_shared/CodegenDiagnosticError-DyQ5FwkM.mjs +0 -24
- package/dist/packem_shared/FLAGS_FILENAME-D5igzqwR.mjs +0 -139
- package/dist/packem_shared/GENERATED_HEADER-C8tAvxqP.mjs +0 -4
- package/dist/packem_shared/LUNORA_ERROR_CODES-DvTLozCu.mjs +0 -68
- package/dist/packem_shared/MUTATORS_FILENAME-BZOfUhlY.mjs +0 -81
- package/dist/packem_shared/NOTIFY_FILENAME-CK2IrsrP.mjs +0 -152
- package/dist/packem_shared/OPENRPC_VERSION-DvDChMhC.mjs +0 -60
- package/dist/packem_shared/QUEUES_FILENAME-Xv6Vy-Ku.mjs +0 -145
- package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-YAL6GrxI.mjs +0 -3597
- package/dist/packem_shared/SCHEMA_SNAPSHOT_VERSION-DGD73E_n.mjs +0 -252
- package/dist/packem_shared/SHAPES_FILENAME-DOhPGi-6.mjs +0 -94
- package/dist/packem_shared/WORKFLOWS_FILENAME-Dtuzvluc.mjs +0 -158
- package/dist/packem_shared/buildOpenApiDocument-DCQSLuFc.mjs +0 -262
- package/dist/packem_shared/discover-ast-CT6BgBr4.mjs +0 -13
- package/dist/packem_shared/discoverAuthApiCalls-Dhwgf4J4.mjs +0 -62
- package/dist/packem_shared/discoverCrons-UbQLLmMS.mjs +0 -286
- package/dist/packem_shared/discoverFunctions-COG_Xkyz.mjs +0 -544
- package/dist/packem_shared/discoverHttpRoutes-CVXWZkzo.mjs +0 -139
- package/dist/packem_shared/discoverInserts-DdWnXmLS.mjs +0 -61
- package/dist/packem_shared/discoverMaskProcedures-BG58_fw3.mjs +0 -273
- package/dist/packem_shared/discoverMigrations-Bw1U01zr.mjs +0 -97
- package/dist/packem_shared/discoverNondeterministicCalls-BbXEFZUT.mjs +0 -122
- package/dist/packem_shared/discoverQueries-CDmxu2ET.mjs +0 -62
- package/dist/packem_shared/discoverR2sqlCalls-Bn7QVv7J.mjs +0 -80
- package/dist/packem_shared/discoverRlsMetadata-D9uN35no.mjs +0 -280
- package/dist/packem_shared/discoverSandboxUsage-B5jDU6Bt.mjs +0 -38
- package/dist/packem_shared/discoverSchema-BJp5_T4o.mjs +0 -906
- package/dist/packem_shared/discoverStorageRulesMetadata-BqMhuEjo.mjs +0 -97
- package/dist/packem_shared/emit-DIbr7U31.mjs +0 -3884
- package/dist/packem_shared/emitApp-DbMM8iHe.mjs +0 -706
- package/dist/packem_shared/formatAdvisories-Cans5TWS.mjs +0 -131
- package/dist/packem_shared/parse-validator-BDGV8iue.mjs +0 -144
- package/dist/packem_shared/paths-BRd6JHuF.mjs +0 -11
- package/dist/packem_shared/redact-oTmsol5A.mjs +0 -33
- package/dist/packem_shared/schemaFromIr-DTYsLBaA.mjs +0 -57
|
@@ -0,0 +1,2127 @@
|
|
|
1
|
+
import{agentComponent as Je}from"@lunora/agent/component";import{LunoraError as X}from"@lunora/errors";import{n as $}from"./paths-BmX5O1sG.mjs";const Ve=[{contextProperty:"access",key:"access",moduleSpecifier:"@lunora/cloudflare-access"},{appMethod:{configKey:"ai",doc:"Override the Workers AI binding backing `ctx.ai` (defaults to `env.AI`).",method:"ai"},contextProperty:"ai",key:"ai",moduleSpecifier:"@lunora/ai"},{appMethod:{configKey:"analytics",doc:"Override the Analytics Engine dataset backing `ctx.analytics` (defaults to `env.ANALYTICS`).",method:"analytics"},contextProperty:"analytics",key:"analytics",moduleSpecifier:"@lunora/bindings/analytics",serverCtxField:{field:`
|
|
2
|
+
/** Analytics Engine telemetry sink. Fire-and-forget and sampled; do not read it back in-handler. */
|
|
3
|
+
readonly analytics: import("@lunora/bindings/analytics").AnalyticsClient;`,tier:"every"}},{appMethod:{configKey:"browser",doc:"Override the Browser Rendering binding backing `ctx.browser` (defaults to `env.BROWSER`).",method:"browser"},contextProperty:"browser",key:"browser",moduleSpecifier:"@lunora/browser",serverCtxField:{field:`
|
|
4
|
+
/** Browser Rendering (screenshots/PDF/scrape). Non-deterministic — available only in actions. */
|
|
5
|
+
readonly browser: import("@lunora/browser").Browser;`,tier:"action"}},{contextProperty:"containers",key:"container",moduleSpecifier:"@lunora/container"},{contextProperty:"flags",key:"flags",moduleSpecifier:"@lunora/flags"},{appMethod:{configKey:"sql",doc:"Wire the Hyperdrive SQL client backing `ctx.sql` — build it with `createHyperdrive` + `fromPostgresJs`/`fromNodePg`/`fromMysql2`.",method:"hyperdrive"},contextProperty:"sql",key:"hyperdrive",moduleSpecifier:"@lunora/hyperdrive",serverCtxField:{field:`
|
|
6
|
+
/**
|
|
7
|
+
* 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.
|
|
8
|
+
*/
|
|
9
|
+
readonly sql: import("@lunora/hyperdrive").SqlClient;`,tier:"action"}},{appMethod:{configKey:"images",doc:"Override the Images binding backing `ctx.images` (defaults to `env.IMAGES`).",method:"images"},contextProperty:"images",key:"images",moduleSpecifier:"@lunora/bindings/images",serverCtxField:{field:`
|
|
10
|
+
/** Cloudflare Images transforms (resize/format/optimize). Non-deterministic — available only in actions. */
|
|
11
|
+
readonly images: import("@lunora/bindings/images").Images;`,tier:"action"}},{appMethod:{configKey:"kv",doc:"Override the Workers KV binding backing `ctx.kv` (defaults to `env.KV`).",method:"kv"},contextProperty:"kv",key:"kv",moduleSpecifier:"@lunora/bindings/kv",serverCtxField:{field:`
|
|
12
|
+
readonly kv: import("@lunora/bindings/kv").Kv;`,tier:"every"}},{key:"mail",moduleSpecifier:"@lunora/mail"},{appMethod:{configKey:"payment",doc:"Wire the payment options backing `ctx.payments`.",method:"payment"},contextProperty:"payments",key:"payments",moduleSpecifier:"@lunora/payment"},{appMethod:{configKey:"x402",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).",method:"x402"},contextProperty:"x402",key:"x402",moduleSpecifier:"@lunora/x402/pay"},{contextProperty:"pipelines",key:"pipelines",moduleSpecifier:"@lunora/bindings/pipelines",serverCtxField:{field:`
|
|
13
|
+
/** Pipelines ingestion sink (durable, R2-backed). Fire-and-forget and batched; do not read it back in-handler. */
|
|
14
|
+
readonly pipelines: import("@lunora/bindings/pipelines").PipelineClient;`,tier:"action"}},{appMethod:{configKey:"r2sql",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`).",method:"r2sql"},contextProperty:"r2sql",key:"r2sql",moduleSpecifier:"@lunora/bindings/r2sql",serverCtxField:{field:`
|
|
15
|
+
/**
|
|
16
|
+
* 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.
|
|
17
|
+
*/
|
|
18
|
+
readonly r2sql: import("@lunora/bindings/r2sql").R2SqlClient;`,tier:"action"}},{contextProperty:"scheduler",key:"scheduler",moduleSpecifier:"@lunora/scheduler"},{contextProperty:"storage",key:"storage",moduleSpecifier:"@lunora/storage"},{appMethod:{configKey:"vectors",doc:"Wire the Vectorize index map backing `ctx.vectors`.",method:"vectors"},contextProperty:"vectors",key:"vectors",moduleSpecifier:"@lunora/bindings/vectors"},{contextProperty:"workflows",key:"workflows",moduleSpecifier:"@lunora/workflow"}],Wt=Ve,Ut=new Map(Wt.flatMap(e=>e.serverCtxField?[[e.key,e.serverCtxField]]:[])),Zn=Ve.flatMap(e=>"appMethod"in e?[{appMethod:e.appMethod,key:e.key}]:[]),He=/^(?:"(?:[^"\\]|\\(?:["\\/bfnrt]|u[0-9A-Fa-f]{4}))*"|'[^'\\]*'|-?\d+(?:\.\d+)?|true|false|null)$/u,Kt=new Set(["any","bigint","boolean","date","id","null","number","storage","string","timestamp"]),ze=e=>e.column!==void 0,Qt=(e,t)=>{switch(e){case"bigint":return`if (typeof ${t} !== "bigint") return DEFER;
|
|
19
|
+
`;case"boolean":return`if (typeof ${t} !== "boolean") return DEFER;
|
|
20
|
+
`;case"date":case"number":case"timestamp":return`if (typeof ${t} !== "number" || !Number.isFinite(${t})) return DEFER;
|
|
21
|
+
`;case"null":return`if (${t} !== null) return DEFER;
|
|
22
|
+
`;default:return`if (typeof ${t} !== "string") return DEFER;
|
|
23
|
+
`}},Gt=(e,t)=>{const n=e.literalValue?.trim();if(!(n===void 0||!He.test(n)))return{out:t,pre:`if (${t} !== ${n}) return DEFER;
|
|
24
|
+
`}},De=(e,t,n)=>{if(!ze(e)&&!(e.hasRefinement||e.sourceText!==void 0)){if(Kt.has(e.kind))return e.kind==="any"?{out:t,pre:""}:{out:t,pre:Qt(e.kind,t)};switch(e.kind){case"array":return Jt(e,t,n);case"literal":return Gt(e,t);case"object":return Ht(e,t,n);default:return}}},Jt=(e,t,n)=>{const{inner:a}=e;if(!a)return;const r=n.next(),o=`__arr${String(r)}`,s=`__i${String(r)}`,i=`__e${String(r)}`,l=De(a,i,n);if(!l)return;const u=`if (!Array.isArray(${t})) return DEFER;
|
|
25
|
+
const ${o} = new Array(${t}.length);
|
|
26
|
+
for (let ${s} = 0; ${s} < ${t}.length; ${s}++) {
|
|
27
|
+
const ${i} = ${t}[${s}];
|
|
28
|
+
${l.pre}${o}[${s}] = ${l.out};
|
|
29
|
+
}
|
|
30
|
+
`;return{out:o,pre:u}},Vt=(e,t,n,a)=>{const r=JSON.stringify(e);if(t.kind==="optional"){if(t.hasRefinement||t.sourceText!==void 0||ze(t))return;const{inner:s}=t;if(!s)return;const i=De(s,n,a);if(!i)return;const l=a.next(),u=`__has${String(l)}`,p=`__val${String(l)}`,b=`let ${u} = false;
|
|
31
|
+
let ${p};
|
|
32
|
+
if (${n} !== undefined) {
|
|
33
|
+
${i.pre}${p} = ${i.out};
|
|
34
|
+
${u} = true;
|
|
35
|
+
}
|
|
36
|
+
`;return{entry:`...(${u} ? { ${r}: ${p} } : {})`,pre:b}}const o=De(t,n,a);if(o)return{entry:`${r}: ${o.out}`,pre:o.pre}},Xe=(e,t,n)=>{let a="";const r=[];for(const o of Object.keys(e)){const s=e[o];if(!s)return;const i=Vt(o,s,t(o),n);if(!i)return;a+=i.pre,r.push(i.entry)}return{entries:r.join(", "),pre:a}},Ht=(e,t,n)=>{const a=e.shape??{},r=Xe(a,l=>`${t}[${JSON.stringify(l)}]`,n);if(!r)return;const o=n.next(),s=`__obj${String(o)}`,i=`if (typeof ${t} !== "object" || ${t} === null || Array.isArray(${t})) return DEFER;
|
|
37
|
+
${r.pre}const ${s} = { ${r.entries} };
|
|
38
|
+
`;return{out:s,pre:i}},zt=e=>{let t=0;const n=Xe(e,a=>`source[${JSON.stringify(a)}]`,{next:()=>(t+=1,t)});if(n)return`(source) => {
|
|
39
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) return DEFER;
|
|
40
|
+
${n.pre}return { ${n.entries} };
|
|
41
|
+
}`},k=`// GENERATED by @lunora/codegen — do not edit.
|
|
42
|
+
// Run \`lunora codegen\` to regenerate.
|
|
43
|
+
|
|
44
|
+
`,V=(e=!1)=>e?{client:"lunorash/client",do:"lunorash/do",flags:"lunorash/flags",server:"lunorash/server",serverDataModel:"lunorash/server/data-model",serverDrizzle:"lunorash/server/drizzle",serverTypes:"lunorash/server/types",values:"lunorash/values"}:{client:"@lunora/client",do:"@lunora/do",flags:"@lunora/flags",server:"@lunora/server",serverDataModel:"@lunora/server/data-model",serverDrizzle:"@lunora/server/drizzle",serverTypes:"@lunora/server/types",values:"@lunora/values"},Ye=/^[A-Za-z_$][\w$]*$/u,Xt=/^[\w./-]+$/u,m=(e,t)=>{if(!Ye.test(e))throw new X("INTERNAL",`@lunora/codegen: ${t} is not a valid JS identifier: ${JSON.stringify(e)}`)},Ce=e=>e.charAt(0).toUpperCase()+e.slice(1),O=e=>Ye.test(e)?e:JSON.stringify(e),Yt={any:"unknown",bigint:"bigint",boolean:"boolean",bytes:"ArrayBuffer",date:"number",from:"unknown",geoPoint:"{ lat: number; lng: number }",null:"null",number:"number",storage:"string",string:"string",timestamp:"number"},Zt=e=>{if(e===void 0)return"unknown";if(!He.test(e))throw new X("INTERNAL",`@lunora/codegen: v.literal() argument is not a parseable string/number/boolean/null literal: ${JSON.stringify(e)}`);return e},N=e=>{const t=Yt[e.kind];if(t!==void 0)return t;switch(e.kind){case"array":return`Array<${e.inner?N(e.inner):"unknown"}>`;case"id":{const n=e.tableName??"_unknown_";return m(n,"v.id() table name"),`Id<"${n}">`}case"literal":return Zt(e.literalValue);case"object":return e.shape?`{ ${Object.entries(e.shape).map(([n,a])=>`${O(n)}: ${N(a)}`).join("; ")} }`:"Record<string, unknown>";case"optional":return`${e.inner?N(e.inner):"unknown"} | undefined`;case"record":{const n=e.keyType?N(e.keyType):"string",a=e.valueType?N(e.valueType):"unknown";return`Record<${n}, ${a}>`}case"union":return e.members&&e.members.length>0?e.members.map(n=>N(n)).join(" | "):"never";default:return"unknown"}},he=e=>{const t=Object.entries(e);return t.length===0?"{}":`{ ${t.map(([n,a])=>{const r=O(n);if(a.kind==="optional"){const o=a.inner?N(a.inner):"unknown";return`${r}?: ${o}`}return`${r}: ${N(a)}`}).join("; ")} }`},en=e=>e.kind==="optional"||!!e.column?.hasDefault,tn=e=>{m(e.name,"table name");const t=Object.entries(e.shape).map(([a,r])=>{const o=r.kind==="optional"&&r.inner?r.inner:r,s=N(o),i=O(a);return en(r)?` ${i}?: ${s};`:` ${i}: ${s};`}).join(`
|
|
45
|
+
`),n=t?`
|
|
46
|
+
${t}
|
|
47
|
+
`:"";return`export interface Insert_${e.name} {
|
|
48
|
+
_id?: Id<"${e.name}">;
|
|
49
|
+
_creationTime?: number;${n}}`},er=(e,t=!1)=>{const n=V(t);for(const c of e.tables)m(c.name,"table name");const a=e.tables.map(c=>`"${c.name}"`).join(" | ")||"never",r=e.tables.filter(c=>c.extensionKey===void 0).map(c=>`"${c.name}"`).join(" | ")||"never",o=e.tables.map(c=>{const g=Object.entries(c.shape).map(([T,x])=>{const A=O(T);if(x.kind==="optional"){const _=x.inner?N(x.inner):"unknown";return` ${A}?: ${_};`}return` ${A}: ${N(x)};`}).join(`
|
|
50
|
+
`),f=g?`
|
|
51
|
+
${g}
|
|
52
|
+
`:"";return`export interface Doc_${c.name} {
|
|
53
|
+
_id: Id<"${c.name}">;
|
|
54
|
+
_creationTime: number;${f}}`}).join(`
|
|
55
|
+
|
|
56
|
+
`),s=e.tables.map(c=>` ${c.name}: Doc_${c.name};`).join(`
|
|
57
|
+
`),i=e.tables.map(c=>{const g=c.indexes.map(f=>JSON.stringify(f.name)).join(" | ");return` ${c.name}: ${g||"never"};`}).join(`
|
|
58
|
+
`),l=e.tables.map(c=>{const g=c.shardMode==="global"?"":c.searchIndexes.map(f=>JSON.stringify(f.name)).join(" | ");return` ${c.name}: ${g||"never"};`}).join(`
|
|
59
|
+
`),u=e.tables.map(c=>{const g=c.rankIndexes.map(f=>JSON.stringify(f.name)).join(" | ");return` ${c.name}: ${g||"never"};`}).join(`
|
|
60
|
+
`),p=e.tables.map(c=>{const g=c.shardMode==="global"?"":(c.geoIndexes??[]).map(f=>JSON.stringify(f.name)).join(" | ");return` ${c.name}: ${g||"never"};`}).join(`
|
|
61
|
+
`),b=e.vectorIndexes.map(c=>JSON.stringify(c.name)).join(" | ")||"never",w=e.tables.map(c=>tn(c)).join(`
|
|
62
|
+
|
|
63
|
+
`),v=e.tables.map(c=>` ${c.name}: Insert_${c.name};`).join(`
|
|
64
|
+
`),d=e.tables.map(c=>{const g=c.relations.map(f=>(m(f.table,"relation target table"),` ${O(f.name)}: ${f.kind==="one"?"OneRelation":"ManyRelation"}<"${f.table}">;`)).join(`
|
|
65
|
+
`);return g?` ${c.name}: {
|
|
66
|
+
${g}
|
|
67
|
+
};`:` ${c.name}: {};`}).join(`
|
|
68
|
+
`);return`${k}import type {
|
|
69
|
+
DatabaseReaderFacade as DatabaseReaderFacadeOf,
|
|
70
|
+
DatabaseWriterFacade as DatabaseWriterFacadeOf,
|
|
71
|
+
LoadWith as LoadWithOf,
|
|
72
|
+
TableReaderFacade as TableReaderFacadeOf,
|
|
73
|
+
TableWriterFacade as TableWriterFacadeOf,
|
|
74
|
+
WithArg as WithArgOf,
|
|
75
|
+
} from "${n.serverDataModel}";
|
|
76
|
+
|
|
77
|
+
export type {
|
|
78
|
+
AggregateOp,
|
|
79
|
+
GroupByEntry,
|
|
80
|
+
OrderBy,
|
|
81
|
+
QueryArgs,
|
|
82
|
+
QueryPage,
|
|
83
|
+
RankPage,
|
|
84
|
+
RankResult,
|
|
85
|
+
RestrictableQueryOptions,
|
|
86
|
+
RestrictableQueryOptionsOf,
|
|
87
|
+
SearchFilterBuilder,
|
|
88
|
+
SearchReader,
|
|
89
|
+
TableAggregateOptions,
|
|
90
|
+
TableAggregateOptionsOf,
|
|
91
|
+
TableGroupByOptions,
|
|
92
|
+
TableGroupByOptionsOf,
|
|
93
|
+
TableRankOptions,
|
|
94
|
+
TableRankPageOptions,
|
|
95
|
+
Where,
|
|
96
|
+
WhereOf,
|
|
97
|
+
WhereOperators,
|
|
98
|
+
} from "${n.serverDataModel}";
|
|
99
|
+
|
|
100
|
+
export type TableName = ${a};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The tables **this app declared** — every {@link TableName} except those an add-on
|
|
104
|
+
* contributed through \`defineSchema(...).extend(...)\`.
|
|
105
|
+
*
|
|
106
|
+
* An add-on's tables are real tables and stay in \`TableName\` (they are queryable,
|
|
107
|
+
* they appear in \`DataModel\`), but an app enumerating "my tables" should not have to
|
|
108
|
+
* know they exist: an account-deletion sweep, an export, a migration allowlist, or a
|
|
109
|
+
* helper generic over a table union is about the app's own data, and every add-on
|
|
110
|
+
* added or removed would otherwise silently change the correct answer.
|
|
111
|
+
*
|
|
112
|
+
* \`\`\`ts
|
|
113
|
+
* // Doesn't need to mention \`ratelimit_buckets\`, and won't drift when an add-on lands.
|
|
114
|
+
* const EXPORTED: readonly AppTableName[] = ["nodes", "tagColors"];
|
|
115
|
+
* \`\`\`
|
|
116
|
+
*/
|
|
117
|
+
export type AppTableName = ${r};
|
|
118
|
+
|
|
119
|
+
export type Id<TName extends string> = string & { readonly __table: TName };
|
|
120
|
+
|
|
121
|
+
${o}
|
|
122
|
+
|
|
123
|
+
export interface DataModel {
|
|
124
|
+
${s}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type Doc<T extends keyof DataModel> = DataModel[T];
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Per-table index name union. \`never\` for tables without secondary indexes.
|
|
131
|
+
* Used by \`TableReader.withIndex()\` to constrain callers to declared names.
|
|
132
|
+
*/
|
|
133
|
+
export interface IndexNamesByTable {
|
|
134
|
+
${i}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type IndexName<T extends keyof DataModel> = IndexNamesByTable[T];
|
|
138
|
+
|
|
139
|
+
/** Per-table search-index name union. \`never\` for tables without searchIndex. */
|
|
140
|
+
export interface SearchIndexNamesByTable {
|
|
141
|
+
${l}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export type SearchIndexName<T extends keyof DataModel> = SearchIndexNamesByTable[T];
|
|
145
|
+
|
|
146
|
+
/** Per-table rank-index name union. \`never\` for tables without a rankIndex. */
|
|
147
|
+
export interface RankIndexNamesByTable {
|
|
148
|
+
${u}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type RankIndexName<T extends keyof DataModel> = RankIndexNamesByTable[T];
|
|
152
|
+
|
|
153
|
+
/** Per-table geo-index name union. \`never\` for tables without a geoIndex. */
|
|
154
|
+
export interface GeoIndexNamesByTable {
|
|
155
|
+
${p}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type GeoIndexName<T extends keyof DataModel> = GeoIndexNamesByTable[T];
|
|
159
|
+
|
|
160
|
+
/** Union of declared vector index names. \`never\` when none are declared. */
|
|
161
|
+
export type VectorIndexName = ${b};
|
|
162
|
+
|
|
163
|
+
${w}
|
|
164
|
+
|
|
165
|
+
/** Per-table insert shape, accepted by \`ctx.db.<table>.insert(...)\`. */
|
|
166
|
+
export interface InsertModel {
|
|
167
|
+
${v}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export type Insert<T extends keyof DataModel> = InsertModel[T];
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Phantom relation descriptors. They carry the relation kind and target table
|
|
174
|
+
* as type parameters only — there is no runtime value — so the \`with\` argument
|
|
175
|
+
* and its return type can be inferred from {@link Relations}.
|
|
176
|
+
*/
|
|
177
|
+
export interface OneRelation<Target extends keyof DataModel> {
|
|
178
|
+
readonly __relationKind: "one";
|
|
179
|
+
readonly __target: Target;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface ManyRelation<Target extends keyof DataModel> {
|
|
183
|
+
readonly __relationKind: "many";
|
|
184
|
+
readonly __target: Target;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Per-table relation map keyed by accessor name. \`{}\` for tables with none. */
|
|
188
|
+
export interface Relations {
|
|
189
|
+
${d}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The \`with\` argument for table \`T\` — see \`@lunora/server/data-model\`. */
|
|
193
|
+
export type WithArg<T extends keyof DataModel> = WithArgOf<DataModel, Relations, T>;
|
|
194
|
+
|
|
195
|
+
/** \`Doc<T>\` narrowed to exactly the relations requested in the with-arg \`W\`. */
|
|
196
|
+
export type LoadWith<T extends keyof DataModel, W> = LoadWithOf<DataModel, Relations, T, W>;
|
|
197
|
+
|
|
198
|
+
/** Read-only typed table accessor exposed on \`QueryCtx.db.<table>\`. */
|
|
199
|
+
export type TableReaderFacade<T extends keyof DataModel> = TableReaderFacadeOf<DataModel, Relations, RankIndexNamesByTable, SearchIndexNamesByTable, T, GeoIndexNamesByTable>;
|
|
200
|
+
|
|
201
|
+
/** Read-write typed table accessor exposed on \`MutationCtx.db.<table>\` / \`ActionCtx.db.<table>\`. */
|
|
202
|
+
export type TableWriterFacade<T extends keyof DataModel> = TableWriterFacadeOf<DataModel, InsertModel, Relations, RankIndexNamesByTable, SearchIndexNamesByTable, T, GeoIndexNamesByTable>;
|
|
203
|
+
|
|
204
|
+
/** Per-table read facade — \`ctx.db.<table>\` on a \`QueryCtx\`. */
|
|
205
|
+
export type DatabaseReaderFacade = DatabaseReaderFacadeOf<DataModel, Relations, RankIndexNamesByTable, SearchIndexNamesByTable, GeoIndexNamesByTable>;
|
|
206
|
+
|
|
207
|
+
/** Per-table read-write facade — \`ctx.db.<table>\` on a \`MutationCtx\` / \`ActionCtx\`. */
|
|
208
|
+
export type DatabaseWriterFacade = DatabaseWriterFacadeOf<DataModel, InsertModel, Relations, RankIndexNamesByTable, SearchIndexNamesByTable, GeoIndexNamesByTable>;
|
|
209
|
+
|
|
210
|
+
/** Insert builder returned by \`ctx.orm.insert(table)\`. */
|
|
211
|
+
export interface OrmInsertBuilder<T extends keyof DataModel> {
|
|
212
|
+
values: (values: Insert<T>) => Promise<Id<T>>;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Replace builder returned by \`ctx.orm.replace(table, id)\` — swaps the whole document. */
|
|
216
|
+
export interface OrmReplaceBuilder<T extends keyof DataModel> {
|
|
217
|
+
with: (values: Insert<T>) => Promise<void>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Update builder returned by \`ctx.orm.update(table, id)\` — patches the named fields. */
|
|
221
|
+
export interface OrmUpdateBuilder<T extends keyof DataModel> {
|
|
222
|
+
set: (values: Partial<Insert<T>>) => Promise<void>;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Read-only ORM surface — \`ctx.orm\` on a \`QueryCtx\`. Mirrors \`ctx.db\` reads under a kitcn-style \`query\` namespace. */
|
|
226
|
+
export interface OrmReader {
|
|
227
|
+
query: DatabaseReaderFacade;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Read-write ORM surface — \`ctx.orm\` on a \`MutationCtx\` / \`ActionCtx\`. Writes are addressed by id, like \`ctx.db\`. */
|
|
231
|
+
export interface OrmWriter extends OrmReader {
|
|
232
|
+
delete: <T extends keyof DataModel>(table: T, id: Id<T>) => Promise<void>;
|
|
233
|
+
insert: <T extends keyof DataModel>(table: T) => OrmInsertBuilder<T>;
|
|
234
|
+
replace: <T extends keyof DataModel>(table: T, id: Id<T>) => OrmReplaceBuilder<T>;
|
|
235
|
+
update: <T extends keyof DataModel>(table: T, id: Id<T>) => OrmUpdateBuilder<T>;
|
|
236
|
+
}
|
|
237
|
+
`},nn=/import\("(?<spec>(?:\.\.?\/)*_generated\/[^"]+)"\)/gu,rn=/^(?:\.\.?\/)*_generated\//u,_e=e=>e.replaceAll(nn,(t,n)=>`import("${n.replace(rn,"./")}")`),Ie=e=>["Doc","Id"].filter(t=>new RegExp(String.raw`\b${t}<`,"u").test(e)),Qe=e=>{const t=new Map;for(const a of e){const r=t.get(a.filePath)??[];r.push(a),t.set(a.filePath,r)}const n=([a,r])=>{const o=r.map(s=>{const i=he(s.args),l=_e(s.returnType);return` ${s.exportName}: FunctionReference<"${s.kind}", ${i}, ${l}>;`}).join(`
|
|
238
|
+
`);return` ${O($(a))}: {
|
|
239
|
+
${o}
|
|
240
|
+
};`};return[...t.entries()].toSorted(([a],[r])=>a.localeCompare(r)).map(a=>n(a)).join(`
|
|
241
|
+
`)},an=(e,t)=>{if(e.length===0&&t.length===0)return{block:"",importLine:""};const n=[];let a="";if(e.length>0&&(a+="\n/** Params type carried by a `defineWorkflow` definition (its phantom `__params`). */\ntype WorkflowParamsOf<Definition> = Definition extends { __params?: infer Params } ? (unknown extends Params ? Record<string, unknown> : Params) : Record<string, unknown>;\n"),a+=`
|
|
242
|
+
/** A typed reference to a durable workflow, addressable for \`cronJobs()\` targets. Mirrors \`@lunora/scheduler\`'s \`WorkflowReference\` structurally. */
|
|
243
|
+
export interface WorkflowReference<Params = Record<string, unknown>> {
|
|
244
|
+
readonly isLunoraWorkflow: true;
|
|
245
|
+
readonly __params?: Params;
|
|
246
|
+
readonly binding: string;
|
|
247
|
+
readonly name: string;
|
|
248
|
+
}
|
|
249
|
+
`,e.length>0){const r=[...e].toSorted((i,l)=>i.exportName.localeCompare(l.exportName)),o=r.map(i=>` ${i.exportName}: WorkflowReference<WorkflowParamsOf<typeof lunoraWorkflowDefinitions.${i.exportName}>>;`).join(`
|
|
250
|
+
`),s=r.map(i=>` ${i.exportName}: { isLunoraWorkflow: true, binding: ${JSON.stringify(i.bindingName)}, name: ${JSON.stringify(i.exportName)} },`).join(`
|
|
251
|
+
`);a+=`
|
|
252
|
+
/** This project's durable workflows, addressable as typed references (e.g. \`crons.daily("digest", …, workflows.digestPipeline, params)\`). */
|
|
253
|
+
export interface WorkflowsRef {
|
|
254
|
+
${o}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export const workflows: WorkflowsRef = {
|
|
258
|
+
${s}
|
|
259
|
+
};
|
|
260
|
+
`,n.push(`import type * as lunoraWorkflowDefinitions from "../workflows.js";
|
|
261
|
+
`)}if(t.length>0){const r=[...t].toSorted((i,l)=>i.exportName.localeCompare(l.exportName)),o=r.map(i=>` ${i.exportName}: WorkflowReference<AgentRunInput>;`).join(`
|
|
262
|
+
`),s=r.map(i=>` ${i.exportName}: { isLunoraWorkflow: true, binding: ${JSON.stringify(i.bindingName)}, name: ${JSON.stringify(i.name)} },`).join(`
|
|
263
|
+
`);a+=`
|
|
264
|
+
/** This project's durable agents, addressable as typed references for \`cronJobs()\` — each fire starts a fresh agent run (e.g. \`crons.daily("sweep", …, agents.support, { input, threadKey })\`). */
|
|
265
|
+
export interface AgentsRef {
|
|
266
|
+
${o}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const agents: AgentsRef = {
|
|
270
|
+
${s}
|
|
271
|
+
};
|
|
272
|
+
`,n.push(`import type { AgentRunInput } from "@lunora/agent";
|
|
273
|
+
`)}return{block:a,importLine:n.join("")}},on=()=>Object.keys(Je().functions).toSorted((e,t)=>e.localeCompare(t)),Ze=e=>new Set(e.filter(t=>$(t.filePath)==="agents").map(t=>t.exportName)),sn=()=>new Set(Object.entries(Je().functions).filter(([,e])=>e.visibility==="internal").map(([e])=>e)),ln=(e,t)=>{const n={importLine:"",lines:"",prelude:""};if(e.length===0)return n;const a=Ze(t),r=sn(),o=t.find(i=>$(i.filePath)==="agents"&&r.has(i.exportName));if(o)throw new X("INTERNAL",`@lunora/codegen: "agents:${o.exportName}" is reserved for the durable agent loop's internal dispatch — rename the "${o.exportName}" export in ${o.filePath}`);const s=on().filter(i=>!a.has(i));return s.length===0?n:{importLine:`import { agentComponent } from "@lunora/agent/component";
|
|
274
|
+
`,lines:s.map(i=>` "agents:${i}": lunoraAgentRuntimeFunctions.${i} as unknown as RegisteredLunoraFunction,`).join(`
|
|
275
|
+
`),prelude:`
|
|
276
|
+
/**
|
|
277
|
+
* The \`@lunora/agent\` runtime component — auto-registered because \`lunora/agents.ts\`
|
|
278
|
+
* declares agents. The durable loop dispatches the internal mutations; clients
|
|
279
|
+
* subscribe to the public queries (\`agents:agentMessages\` is the live thread view).
|
|
280
|
+
*/
|
|
281
|
+
const lunoraAgentRuntimeFunctions = agentComponent().functions;
|
|
282
|
+
`}},dn=(e,t)=>{if(!e)return{importLine:"",lines:"",prelude:""};const n=t.find(a=>$(a.filePath)==="sandbox"&&a.exportName==="invoke");if(n)throw new X("INTERNAL",`@lunora/codegen: "sandbox:invoke" is reserved for the batteries-included sandbox tool dispatcher (browserTool/containerTool) — rename the "invoke" export in ${n.filePath}`);return{importLine:`import { sandboxComponent } from "@lunora/agent/component";
|
|
283
|
+
`,lines:' "sandbox:invoke": lunoraSandbox.invoke as unknown as RegisteredLunoraFunction,',prelude:"\n/**\n * The `@lunora/agent` sandbox dispatcher — auto-registered because `lunora/`\n * imports a sandbox tool (`browserTool`/`containerTool`). The batteries-included\n * tools dispatch to this internal action, which runs on an action ctx carrying\n * `ctx.browser` + `ctx.containers` (the durable tool step itself has neither).\n */\nconst lunoraSandbox = sandboxComponent();\n"}},cn=(e,t)=>{if(e.length===0)return[];const n=Ze(t),a=[{args:{key:{kind:"string"},limit:{inner:{kind:"number"},kind:"optional"}},exportName:"agentMessages",filePath:"agents",kind:"query",returnType:"Record<string, unknown>[]"},{args:{decision:{kind:"union",members:[{kind:"literal",literalValue:'"approve"'},{kind:"literal",literalValue:'"reject"'}]},instanceId:{kind:"string"},note:{inner:{kind:"string"},kind:"optional"},threadKey:{kind:"string"},toolCallId:{kind:"string"}},exportName:"agentResolveApproval",filePath:"agents",kind:"mutation",returnType:"{ resolved: boolean }"},{args:{agent:{kind:"string"},input:{kind:"string"},threadKey:{kind:"string"},title:{inner:{kind:"string"},kind:"optional"}},exportName:"agentRun",filePath:"agents",kind:"mutation",returnType:"{ id: string; threadKey: string }"},{args:{key:{kind:"string"}},exportName:"agentState",filePath:"agents",kind:"query",returnType:"Record<string, unknown> | undefined"},{args:{key:{kind:"string"}},exportName:"agentThread",filePath:"agents",kind:"query",returnType:"Record<string, unknown> | undefined"}];for(const r of e)r.voice&&a.push({args:{threadKey:{kind:"string"}},exportName:`${r.exportName}Voice`,filePath:"agents",kind:"stream",returnType:"Record<string, unknown>"});return a.filter(r=>!n.has(r.exportName))},un=e=>{const t=e.filter(s=>s.stream);if(t.length===0)return{block:"",body:""};const n=new Map;for(const s of t){const i=n.get(s.filePath)??[];i.push(s),n.set(s.filePath,i)}const a=[...n.entries()].toSorted(([s],[i])=>s.localeCompare(i)),r=a.map(([s,i])=>{const l=i.map(u=>{const p=_e(u.chunkType??"unknown");return` ${O(u.exportName)}: HttpStreamRef<${p}, ${he(u.searchParams)}, ${he(u.params)}>;`}).join(`
|
|
284
|
+
`);return` ${O($(s))}: {
|
|
285
|
+
${l}
|
|
286
|
+
};`}).join(`
|
|
287
|
+
`),o=a.map(([s,i])=>{const l=i.map(u=>` ${O(u.exportName)}: { method: ${JSON.stringify(u.method)}, path: ${JSON.stringify(u.path)} },`).join(`
|
|
288
|
+
`);return` ${O($(s))}: {
|
|
289
|
+
${l}
|
|
290
|
+
},`}).join(`
|
|
291
|
+
`);return{block:`
|
|
292
|
+
/** This project's HTTP-SSE stream routes (\`httpRoute.<verb>(path).stream()\`), addressable as typed references for \`client.httpStream\` / \`useHttpStream\`. */
|
|
293
|
+
export interface HttpStreamsRef {
|
|
294
|
+
${r}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export const httpStreams: HttpStreamsRef = {
|
|
298
|
+
${o}
|
|
299
|
+
};
|
|
300
|
+
`,body:r}},tr=e=>{const{agents:t=[],functions:n,httpRoutes:a=[],useUmbrella:r=!1,workflows:o=[]}=e,s=V(r),i=[...n.filter(x=>x.visibility!=="internal"),...cn(t,n)],l=n.filter(x=>x.visibility==="internal"),u=Qe(i),p=Qe(l),b=un(a),w=`${u}
|
|
301
|
+
${p}
|
|
302
|
+
${b.body}`,v=Ie(w),d=v.length>0?`
|
|
303
|
+
import type { ${v.join(", ")} } from "./dataModel.js";
|
|
304
|
+
`:"",c=u?`
|
|
305
|
+
${u}
|
|
306
|
+
`:"",g=p?`
|
|
307
|
+
${p}
|
|
308
|
+
`:"",f=an(o,t),T=b.block===""?"FunctionReference":"FunctionReference, HttpStreamRef";return`${k}import { anyApi } from "${s.serverTypes}";
|
|
309
|
+
import type { ${T} } from "${s.client}";
|
|
310
|
+
${f.importLine}${d}
|
|
311
|
+
export interface ApiTypes {${c}}
|
|
312
|
+
|
|
313
|
+
export const api = anyApi as unknown as ApiTypes;
|
|
314
|
+
|
|
315
|
+
/** Internal functions — callable only server-side via \`ctx.run*\`, never from a client. */
|
|
316
|
+
export interface InternalApiTypes {${g}}
|
|
317
|
+
|
|
318
|
+
export const internal = anyApi as unknown as InternalApiTypes;
|
|
319
|
+
${f.block}${b.block}`},nr=e=>e?`${k}import { createSeedClient as createSeedClientBase } from "@lunora/seed";
|
|
320
|
+
import type { SeedClient, SeedClientOptions } from "@lunora/seed";
|
|
321
|
+
|
|
322
|
+
import schema from "../schema.js";
|
|
323
|
+
import type { InsertModel } from "./dataModel.js";
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Schema-aware seed client with this project's \`InsertModel\` and runtime schema
|
|
327
|
+
* pre-bound. Each table is a method; call it with a count, a range, explicit
|
|
328
|
+
* partial rows, or per-field overrides. Foreign keys connect to rows seeded
|
|
329
|
+
* earlier in the run, and FK-parent tables are seeded automatically.
|
|
330
|
+
* @example
|
|
331
|
+
* const seed = createSeedClient({ seed: 1 });
|
|
332
|
+
* const { users } = await seed.users(5);
|
|
333
|
+
* const { posts } = await seed.posts((x) => x([10, 20]));
|
|
334
|
+
*/
|
|
335
|
+
export const createSeedClient = (options?: SeedClientOptions): SeedClient<InsertModel> => createSeedClientBase<InsertModel>(schema, options);
|
|
336
|
+
`:"",rr=(e,t,n=!1)=>{if(e.length===0||!t)return"";const a=V(n),r=l=>l.table===void 0?"Row":`Doc<${JSON.stringify(l.table)}> & Row`,o=e.map(l=>{m(l.exportName,"shape export name");const u=r(l),p=he(l.args),b=Object.keys(l.args).length>0,w=b?` args: ${p};`:` args?: ${p};`,v=b?`(args: ${p}) => void`:`(args?: ${p}) => void`,d=`${l.exportName}CollectionOptions`;return`/** Options for the \`${l.exportName}\` shape binding. */
|
|
337
|
+
export interface ${Ce(d)} {
|
|
338
|
+
${w}
|
|
339
|
+
/**
|
|
340
|
+
* Share the optimistic-overlay gate with other collections + mutators on this
|
|
341
|
+
* shard. Defaults to the shared per-shard registry, which is almost always what
|
|
342
|
+
* you want — pass one only to isolate this collection's gate.
|
|
343
|
+
*/
|
|
344
|
+
checkpoints?: CheckpointRegistry;
|
|
345
|
+
/** The Lunora client to subscribe through. */
|
|
346
|
+
client: LunoraClient;
|
|
347
|
+
/** Row key extractor — defaults to \`row._id\`. Override when the app keys rows by a natural column. */
|
|
348
|
+
getKey?: (row: ${u}) => string;
|
|
349
|
+
/** \`"eager"\` starts syncing at creation; \`"lazy"\` (default) on the first subscriber. */
|
|
350
|
+
load?: "eager" | "lazy";
|
|
351
|
+
/** Notified when the underlying subscription errors; the collection always leaves \`loading\`. */
|
|
352
|
+
onError?: (error: SubscriptionError) => void;
|
|
353
|
+
/** Routes the subscription to a shard's DO. **Required for a \`.shardBy()\` table** — without it the watermark lands in the default bucket. */
|
|
354
|
+
shardKey?: string;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Collection options for the \`${l.exportName}\` replication shape: \`config\` for
|
|
359
|
+
* \`createCollection\`, plus the \`checkpoints\` registry to hand \`bindMutators\` and
|
|
360
|
+
* \`scope\` to re-point the subscription.
|
|
361
|
+
*/
|
|
362
|
+
export const ${l.exportName}CollectionOptions = (options: ${Ce(d)}): LunoraCollectionOptions<${u}> =>
|
|
363
|
+
lunoraCollectionOptions<${u}>({
|
|
364
|
+
client: options.client,
|
|
365
|
+
...(options.checkpoints === undefined ? {} : { checkpoints: options.checkpoints }),
|
|
366
|
+
...(options.getKey === undefined ? {} : { getKey: options.getKey }),
|
|
367
|
+
...(options.load === undefined ? {} : { load: options.load }),
|
|
368
|
+
...(options.onError === undefined ? {} : { onError: options.onError }),
|
|
369
|
+
...(options.shardKey === undefined ? {} : { shardKey: options.shardKey }),
|
|
370
|
+
shape: { args: options.args, name: ${JSON.stringify(l.exportName)}, ...(options.shardKey === undefined ? {} : { shardKey: options.shardKey }) },
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
/** Live collection for the \`${l.exportName}\` shape, with its sync controls. */
|
|
374
|
+
export const ${l.exportName}Collection = (
|
|
375
|
+
options: ${Ce(d)},
|
|
376
|
+
): { checkpoints: CheckpointRegistry; collection: Collection<${u}, string>; scope: ${v} } => {
|
|
377
|
+
const { checkpoints, config, scope } = ${l.exportName}CollectionOptions(options);
|
|
378
|
+
|
|
379
|
+
return { checkpoints, collection: createCollection(config), scope };
|
|
380
|
+
};`}).join(`
|
|
381
|
+
|
|
382
|
+
`),s=Ie(o),i=s.length>0?`import type { ${s.join(", ")} } from "./dataModel.js";
|
|
383
|
+
`:"";return`${k}import type { LunoraClient, SubscriptionError } from "${a.client}";
|
|
384
|
+
import { lunoraCollectionOptions } from "@lunora/db/collections";
|
|
385
|
+
import type { CheckpointRegistry, LunoraCollectionOptions, Row } from "@lunora/db";
|
|
386
|
+
import type { Collection } from "@tanstack/db";
|
|
387
|
+
import { createCollection } from "@tanstack/db";
|
|
388
|
+
${i}
|
|
389
|
+
${o}
|
|
390
|
+
`},pn=(e,t)=>`lunora_${$(e)}_${String(t)}`,hn=`const callRegistered = async <R>(context: CallerCtx, functionPath: string, args: Record<string, unknown> | undefined): Promise<R> => {
|
|
391
|
+
const registered = LUNORA_FUNCTIONS[functionPath];
|
|
392
|
+
|
|
393
|
+
if (!registered) {
|
|
394
|
+
throw new LunoraError("FUNCTION_NOT_FOUND", \`function not registered: \${functionPath}\`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return (await registered.handler(context, args ?? {})) as R;
|
|
398
|
+
};`,gn=(e,t,n=[],a=[])=>{const r=new Map,o=d=>{r.has(d)||r.set(d,pn(d,r.size))};for(const d of e)o(d.filePath);for(const d of t)o(d.filePath);for(const d of n)o(d.filePath);for(const d of a)o(d.filePath);const s=[...r.entries()].map(([d,c])=>{if(!Xt.test(d))throw new X("INTERNAL",`@lunora/codegen: refusing to emit import for unsafe file path: ${JSON.stringify(d)}`);return`import * as ${c} from "../${d}.js";`}).join(`
|
|
399
|
+
`),i=e.map(d=>` "${$(d.filePath)}:${d.exportName}": ${r.get(d.filePath)??""}.${d.exportName} as unknown as RegisteredLunoraFunction,`).join(`
|
|
400
|
+
`),l=t.map(d=>` ${JSON.stringify(d.id)}: ${r.get(d.filePath)??""}.${d.exportName} as unknown as RegisteredDataMigration,`).join(`
|
|
401
|
+
`),u=n.map(d=>` "${$(d.filePath)}:${d.exportName}": ${r.get(d.filePath)??""}.${d.exportName} as unknown as RegisteredLunoraFunction,`).join(`
|
|
402
|
+
`),p=a.map(d=>` "${d.exportName}": ${r.get(d.filePath)??""}.${d.exportName} as unknown as RegisteredShape,`).join(`
|
|
403
|
+
`),b=n.map(d=>`${$(d.filePath)}:${d.exportName}`),w=[i,u].filter(d=>d.length>0).join(`
|
|
404
|
+
`),v=e.map(d=>{if(d.lifecycle||Object.keys(d.args).length===0)return;const c=zt(d.args);return c===void 0?void 0:`installCompiledValidatorMap(${r.get(d.filePath)??""}.${d.exportName}.args, ${c});`}).filter(d=>d!==void 0).join(`
|
|
405
|
+
`);return{dispatchBody:w.length>0?`
|
|
406
|
+
${w}
|
|
407
|
+
`:"",importBlock:s.length>0?`${s}
|
|
408
|
+
|
|
409
|
+
`:"",installBlock:v,migrationBody:l.length>0?`
|
|
410
|
+
${l}
|
|
411
|
+
`:"",mutatorPaths:b,shapeBody:p.length>0?`
|
|
412
|
+
${p}
|
|
413
|
+
`:""}},mn=e=>{const t=new Map;for(const r of e){const o=t.get(r.filePath)??[];o.push(r),t.set(r.filePath,o)}const n=[...t.entries()].toSorted(([r],[o])=>r.localeCompare(o)),a=n.map(([r,o])=>{const s=o.map(i=>{const l=he(i.args),u=l==="{}"?"?":"",p=_e(i.returnType);return i.kind==="stream"?` ${i.exportName}: (args${u}: ${l}) => Promise<AsyncIterable<${p}>>;`:` ${i.exportName}: (args${u}: ${l}) => Promise<${p}>;`}).join(`
|
|
414
|
+
`);return` ${O($(r))}: {
|
|
415
|
+
${s}
|
|
416
|
+
};`}).join(`
|
|
417
|
+
`);return{implementation:n.map(([r,o])=>{const s=$(r),i=o.map(l=>` ${l.exportName}: (args) => callRegistered(context, "${s}:${l.exportName}", args),`).join(`
|
|
418
|
+
`);return` ${O(s)}: {
|
|
419
|
+
${i}
|
|
420
|
+
},`}).join(`
|
|
421
|
+
`),types:a}},fn=(e,t=[])=>{const n=new Set;for(const a of e.tables)for(const r of Object.values(a.shape)){const o=r.kind==="optional"&&r.inner?r.inner:r;o.kind==="storage"&&typeof o.bucket=="string"&&o.bucket!==""&&n.add(o.bucket)}for(const a of t)a!==""&&a!=="default"&&n.add(a);return["default",...[...n].toSorted((a,r)=>a.localeCompare(r))]},ar=({agents:e=[],containers:t=[],env:n,hasAccessFacade:a=!1,hasAi:r=!1,hasAnalytics:o=!1,hasBrowser:s=!1,hasFlags:i=!1,hasHyperdrive:l=!1,hasImages:u=!1,hasKv:p=!1,hasNotify:b=!1,hasPayments:w=!1,hasPipelines:v=!1,hasR2sql:d=!1,hasX402:c=!1,identity:g,queues:f=[],schema:T,storageRuleBuckets:x=[],useUmbrella:A=!1,workflows:_=[]}={})=>{const y=V(A),Y=fn(T??{tables:[]},x).map(h=>JSON.stringify(h)).join(" | "),Z=r?`import type { LunoraAi } from "@lunora/ai";
|
|
422
|
+
`:"",ee=r?`
|
|
423
|
+
readonly ai: LunoraAi;`:"",I=w?`import type { LunoraPayment } from "@lunora/payment";
|
|
424
|
+
`:"",j=w?`
|
|
425
|
+
readonly payments: LunoraPayment;`:"",S=c?`import type { X402Pay } from "@lunora/x402/pay";
|
|
426
|
+
`:"",W=c?`
|
|
427
|
+
readonly x402: X402Pay;`:"",L=t.length>0?`import type { ContainerAccessor } from "@lunora/container";
|
|
428
|
+
`:"",te=t.length>0?`
|
|
429
|
+
readonly containers: {${t.map(h=>`
|
|
430
|
+
readonly ${h.exportName}: ContainerAccessor;`).join("")}
|
|
431
|
+
};`:"",H=[...r?[" /** Workers AI binding (the conventional `env.AI`), narrowing `ctx.ai`. */\n readonly AI?: unknown;"]:[],...t.map(h=>(m(h.bindingName,`container binding "${h.bindingName}"`),` /** Durable Object namespace for the \`${h.exportName}\` container. */
|
|
432
|
+
readonly ${h.bindingName}?: unknown;`)),..._.map(h=>(m(h.bindingName,`workflow binding "${h.bindingName}"`),` /** Workflow binding for the \`${h.exportName}\` workflow. */
|
|
433
|
+
readonly ${h.bindingName}?: unknown;`)),...f.map(h=>(m(h.bindingName,`queue binding "${h.bindingName}"`),` /** Queue producer binding for the \`${h.exportName}\` queue. */
|
|
434
|
+
readonly ${h.bindingName}?: unknown;`)),...e.map(h=>(m(h.bindingName,`agent binding "${h.bindingName}"`),` /** Workflow binding for the \`${h.exportName}\` agent. */
|
|
435
|
+
readonly ${h.bindingName}?: unknown;`))].join(`
|
|
436
|
+
`),C=`
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* This project's Cloudflare bindings, as far as codegen can discover them from
|
|
440
|
+
* \`lunora/\` source — the container/workflow Durable Object namespaces and the
|
|
441
|
+
* conventional Workers AI binding. Bindings the config layer reconciles from
|
|
442
|
+
* user-named wrangler config (R2/KV/D1/\`services\`/queues/…) aren't statically
|
|
443
|
+
* visible here, so the open index signature keeps them reachable (cast at the
|
|
444
|
+
* use site). Service bindings (\`env.<SERVICE>.fetch(...)\` / RPC stubs) live
|
|
445
|
+
* under this signature too — Lunora can't type a third-party worker's RPC
|
|
446
|
+
* surface, so treat them as \`Fetcher\`/\`Service<unknown>\` and cast.
|
|
447
|
+
*/
|
|
448
|
+
export interface CloudflareBindings {
|
|
449
|
+
readonly [binding: string]: unknown;${H?`
|
|
450
|
+
${H}`:""}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Alias for {@link CloudflareBindings} — the typed shape of \`env\`. */
|
|
454
|
+
export type Env = CloudflareBindings;`,P=(h,Te)=>Te?Ut.get(h)?.field??"":"",U=P("kv",p),E=P("analytics",o),K=P("hyperdrive",l),ne=P("browser",s),re=P("images",u),ae=P("pipelines",v),Q=P("r2sql",d),M=a?`
|
|
455
|
+
/** Verified Cloudflare Access identity — a synchronous facade over the resolved claims (email / groups / hasGroup / claims). Anonymous when no Access token is present. */
|
|
456
|
+
readonly access: import("@lunora/cloudflare-access/context").AccessFacade;`:"",q=i?`
|
|
457
|
+
/** Feature-flag evaluation (OpenFeature). Reads are memoized per request; evaluations never throw — a provider error resolves to the supplied default. */
|
|
458
|
+
readonly flags: import("${y.flags}").LunoraFlags;`:"",B=b?`
|
|
459
|
+
/** Multi-channel notifications (@lunora/notify): send / chat / inApp / webhook plus the push device sub-facade. Sends are external I/O — confine them to action handlers. */
|
|
460
|
+
readonly notify: import("@lunora/notify").LunoraNotify;
|
|
461
|
+
/** Device push sub-facade — the same object as ctx.notify.push (register / send / broadcast). Sends belong in action handlers. */
|
|
462
|
+
readonly push: import("@lunora/notify").LunoraPush;`:"",F=_.length>0,z=F?`import type { WorkflowHandle } from "@lunora/workflow";
|
|
463
|
+
import type * as lunoraWorkflowDefinitions from "../workflows.js";
|
|
464
|
+
`:"",ye=F?`
|
|
465
|
+
|
|
466
|
+
/** Params type carried by a \`defineWorkflow\` definition (its phantom \`__params\`). */
|
|
467
|
+
type WorkflowParamsOf<Definition> = Definition extends { __params?: infer Params }
|
|
468
|
+
? unknown extends Params
|
|
469
|
+
? Record<string, unknown>
|
|
470
|
+
: NonNullable<Params>
|
|
471
|
+
: Record<string, unknown>;
|
|
472
|
+
|
|
473
|
+
/** This project's declared workflows, addressable from \`ctx.workflows\` by their \`lunora/workflows.ts\` export name. */
|
|
474
|
+
export interface LunoraWorkflows {
|
|
475
|
+
${_.map(h=>` get(name: ${JSON.stringify(h.exportName)}): WorkflowHandle<WorkflowParamsOf<typeof lunoraWorkflowDefinitions.${h.exportName}>>;`).join(`
|
|
476
|
+
`)}
|
|
477
|
+
}`:"",ge=F?' | "workflows"':"",me=F?`
|
|
478
|
+
readonly workflows: LunoraWorkflows;`:"",oe=f.length>0,we=oe?`import type { QueueProducer } from "@lunora/queue";
|
|
479
|
+
import type * as lunoraQueueDefinitions from "../queues.js";
|
|
480
|
+
`:"",xe=oe?`
|
|
481
|
+
|
|
482
|
+
/** Message body type carried by a \`defineQueue\` definition (its phantom \`__lunoraBody\`). */
|
|
483
|
+
type QueueBodyOf<Definition> = Definition extends { __lunoraBody?: infer Body } ? (unknown extends Body ? unknown : NonNullable<Body>) : unknown;
|
|
484
|
+
|
|
485
|
+
/** This project's declared queues, addressable from \`ctx.queues\` by their \`lunora/queues.ts\` export name. */
|
|
486
|
+
export interface LunoraQueues {
|
|
487
|
+
${f.map(h=>` readonly ${h.exportName}: QueueProducer<QueueBodyOf<typeof lunoraQueueDefinitions.${h.exportName}>>;`).join(`
|
|
488
|
+
`)}
|
|
489
|
+
}`:"",fe=oe?`
|
|
490
|
+
readonly queues: LunoraQueues;`:"",ie=e.length>0,ke=ie?`import type { AgentHandle } from "@lunora/agent";
|
|
491
|
+
`:"",ve=ie?`
|
|
492
|
+
|
|
493
|
+
/** This project's declared agents, addressable from \`ctx.agents\` by their \`lunora/agents.ts\` export name. */
|
|
494
|
+
export interface LunoraAgents {
|
|
495
|
+
${e.map(h=>(m(h.exportName,`agent export "${h.exportName}"`),` readonly ${h.exportName}: AgentHandle;`)).join(`
|
|
496
|
+
`)}
|
|
497
|
+
}`:"",be=ie?`
|
|
498
|
+
readonly agents: LunoraAgents;`:"",$e=g?`import type { InferIdentity } from "${y.server}";
|
|
499
|
+
import type * as lunoraIdentityContract from "../identity.js";
|
|
500
|
+
`:"",Se=g?`
|
|
501
|
+
|
|
502
|
+
/** 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. */
|
|
503
|
+
export type Identity = InferIdentity<typeof lunoraIdentityContract.${g.exportName}>;
|
|
504
|
+
|
|
505
|
+
/** \`ctx.auth\` narrowed so \`getIdentity()\` resolves the declared {@link Identity} contract instead of the untyped claim bag. */
|
|
506
|
+
type NarrowedAuth = Omit<QueryCtxBase["auth"], "getIdentity"> & { getIdentity: () => Promise<Identity | null> };`:"",se=g?' | "auth"':"",le=g?`
|
|
507
|
+
readonly auth: NarrowedAuth;`:"",Re=g?", Identity":"",Ne=n?`import type * as lunoraEnvContract from "../env.js";
|
|
508
|
+
`:"",Oe=n?`
|
|
509
|
+
|
|
510
|
+
/** This app's declared env contract (\`defineEnv\` in \`lunora/env.ts\`) — the validated, coercion-aware shape of \`ctx.env\`. */
|
|
511
|
+
export type LunoraEnv = ReturnType<typeof lunoraEnvContract.${n.exportName}>;`:"",de=n?' | "env"':"",ce=n?"\n /** 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. */\n readonly env: LunoraEnv;":"";return`${k}import { createPolicyDsl, initLunora, v as vBase } from "${y.server}";
|
|
512
|
+
import type {
|
|
513
|
+
ActionBuilder,
|
|
514
|
+
ActionCtx as ActionCtxBase,
|
|
515
|
+
ColumnValidator,
|
|
516
|
+
DatabaseReader,
|
|
517
|
+
DatabaseWriter,
|
|
518
|
+
EmptyArgs,
|
|
519
|
+
InternalActionBuilder,
|
|
520
|
+
InternalMutationBuilder,
|
|
521
|
+
InternalQueryBuilder,
|
|
522
|
+
MutationBuilder,
|
|
523
|
+
MutationCtx as MutationCtxBase,
|
|
524
|
+
QueryBuilder,
|
|
525
|
+
QueryCtx as QueryCtxBase,
|
|
526
|
+
ReadOnlyStorage,
|
|
527
|
+
Storage as StorageBase,
|
|
528
|
+
TableReader,
|
|
529
|
+
} from "${y.server}";
|
|
530
|
+
|
|
531
|
+
import type { DataModel, DatabaseReaderFacade, DatabaseWriterFacade, Doc, Id as IdOfTable, OrmReader, OrmWriter, Relations, TableName } from "./dataModel.js";
|
|
532
|
+
${Z}${I}${S}${L}${z}${we}${ke}${$e}${Ne}
|
|
533
|
+
export type { AppTableName, DataModel, Doc, Id, TableName } from "./dataModel.js";
|
|
534
|
+
|
|
535
|
+
/** Storage buckets this schema declares (\`v.storage("name")\`), narrowing \`ctx.storage.bucket(name)\`. */
|
|
536
|
+
export type StorageBucketName = ${Y};${C}${ye}${xe}${ve}${Se}${Oe}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Project-typed contexts. The base contexts from \`@lunora/server\` are
|
|
540
|
+
* untyped against the schema; here \`db\` is widened to the generated per-table
|
|
541
|
+
* facade (\`ctx.db.<table>.findMany(...)\`) while keeping the legacy structural
|
|
542
|
+
* \`db.get\`/\`db.query\` surface for back-compat, and \`storage\` is narrowed so
|
|
543
|
+
* \`ctx.storage.bucket(name)\` autocompletes the declared buckets.
|
|
544
|
+
*/
|
|
545
|
+
/**
|
|
546
|
+
* The Convex-style fluent reader \`ctx.db.query(table)\`, bound to this schema:
|
|
547
|
+
* a table-name literal narrows the row type, so \`.collect()\` / \`.first()\` /
|
|
548
|
+
* \`.take()\` (and the \`.withIndex()\` / \`.filter()\` / \`.order()\` chain) resolve
|
|
549
|
+
* as \`Doc<table>\` with no \`as unknown as Doc<...>\` casts. The per-table accessor
|
|
550
|
+
* \`ctx.db.<table>\` stays available alongside it via the facade.
|
|
551
|
+
*
|
|
552
|
+
* Intersected with the wide \`(string) => TableReader\` signature so the bound
|
|
553
|
+
* \`ctx.db\` is still structurally assignable to schema-agnostic consumers that
|
|
554
|
+
* call \`db.query(someString)\` (e.g. \`@lunora/ratelimit\`'s \`createDbStore\`).
|
|
555
|
+
*/
|
|
556
|
+
type TypedTableQuery = (<T extends TableName>(table: T) => TableReader<Doc<T>>) & ((table: string) => TableReader);
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* The point read \`ctx.db.get(id)\`, bound to this schema: an \`Id<"table">\`
|
|
560
|
+
* carries its table name, so the resolved document is typed \`Doc<table>\` (or
|
|
561
|
+
* \`null\` when absent) with no \`as Doc<...>\` cast. Mirrors {@link TypedTableQuery}.
|
|
562
|
+
*/
|
|
563
|
+
type TypedTableGet = <T extends TableName>(id: IdOfTable<T>) => Promise<Doc<T> | null>;
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Resolves the \`asId\` table argument: a literal in {@link TableName} passes through, a
|
|
567
|
+
* genuinely-wide \`string\` (a computed name) passes through, and any OTHER literal
|
|
568
|
+
* collapses to \`never\` so it fails to typecheck.
|
|
569
|
+
*
|
|
570
|
+
* The \`string extends T\` arm is what distinguishes "the caller passed a computed
|
|
571
|
+
* string" from "the caller passed a misspelled literal" — only the wide \`string\` type
|
|
572
|
+
* is a supertype of \`string\`.
|
|
573
|
+
*/
|
|
574
|
+
type AsIdTable<T extends string> = T extends TableName ? T : string extends T ? T : never;
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* The id parse boundary \`ctx.db.asId(table, id)\`, bound to this schema: a misspelled
|
|
578
|
+
* table literal is a compile error rather than a call that hands back a confidently-
|
|
579
|
+
* branded id for a table that does not exist. Mirrors {@link TypedTableQuery} /
|
|
580
|
+
* {@link TypedTableGet}.
|
|
581
|
+
*
|
|
582
|
+
* Deliberately NOT an intersection with a wide \`(string, string) => string\` overload:
|
|
583
|
+
* overload resolution would fall through to it for a bad literal, silently restoring
|
|
584
|
+
* the very typo hole this narrowing exists to close. {@link AsIdTable} instead keeps a
|
|
585
|
+
* computed table name working — and keeps \`ctx.db\` structurally assignable to
|
|
586
|
+
* schema-agnostic consumers — without accepting an invalid literal.
|
|
587
|
+
*/
|
|
588
|
+
type TypedAsId = <T extends string>(tableName: AsIdTable<T>, id: string) => IdOfTable<T & TableName>;
|
|
589
|
+
|
|
590
|
+
export interface QueryCtx extends Omit<QueryCtxBase, "db" | "storage"${se}${de}> {
|
|
591
|
+
readonly db: Omit<DatabaseReader, "asId" | "query" | "get"> & DatabaseReaderFacade & { asId: TypedAsId; query: TypedTableQuery; get: TypedTableGet };
|
|
592
|
+
readonly orm: OrmReader;
|
|
593
|
+
readonly storage: ReadOnlyStorage<StorageBucketName>;${M}${U}${q}${B}${E}${ce}${le}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export interface MutationCtx extends Omit<MutationCtxBase, "db" | "storage"${ge}${se}${de}> {
|
|
597
|
+
readonly db: Omit<DatabaseWriter, "asId" | "query" | "get"> & DatabaseWriterFacade & { asId: TypedAsId; query: TypedTableQuery; get: TypedTableGet };
|
|
598
|
+
readonly orm: OrmWriter;
|
|
599
|
+
readonly storage: ReadOnlyStorage<StorageBucketName>;${M}${U}${q}${B}${E}${ce}${me}${fe}${be}${le}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export interface ActionCtx extends Omit<ActionCtxBase, "db" | "storage"${ge}${se}${de}> {
|
|
603
|
+
readonly db: Omit<DatabaseWriter, "asId" | "query" | "get"> & DatabaseWriterFacade & { asId: TypedAsId; query: TypedTableQuery; get: TypedTableGet };
|
|
604
|
+
readonly orm: OrmWriter;
|
|
605
|
+
readonly storage: StorageBase<StorageBucketName>;${M}${ee}${j}${W}${te}${U}${q}${B}${K}${ne}${re}${E}${ae}${Q}${ce}${me}${fe}${be}${le}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Procedure builders bound to this project's typed contexts. Chain
|
|
610
|
+
* \`.input({...})\` to declare args, \`.use(middleware)\` for rate limiting / RLS /
|
|
611
|
+
* masking (each middleware may refine \`ctx\`), then the terminal
|
|
612
|
+
* \`.query()\` / \`.mutation()\` / \`.action()\` with a \`({ ctx, args }) => ...\`
|
|
613
|
+
* handler. \`.output(validator)\` and (queries) \`.stream()\` are also available.
|
|
614
|
+
*/
|
|
615
|
+
const lunoraBuilders = initLunora.dataModel<DataModel>().create();
|
|
616
|
+
|
|
617
|
+
/** \`query\` builder bound to this project's typed {@link QueryCtx}. */
|
|
618
|
+
export const query = lunoraBuilders.query as unknown as QueryBuilder<QueryCtx, EmptyArgs>;
|
|
619
|
+
|
|
620
|
+
/** \`mutation\` builder bound to this project's typed {@link MutationCtx}. */
|
|
621
|
+
export const mutation = lunoraBuilders.mutation as unknown as MutationBuilder<MutationCtx, EmptyArgs>;
|
|
622
|
+
|
|
623
|
+
/** \`action\` builder bound to this project's typed {@link ActionCtx}. */
|
|
624
|
+
export const action = lunoraBuilders.action as unknown as ActionBuilder<ActionCtx, EmptyArgs>;
|
|
625
|
+
|
|
626
|
+
/** \`internalQuery\` builder bound to this project's typed {@link QueryCtx} — never exposed on \`api\`. */
|
|
627
|
+
export const internalQuery = lunoraBuilders.internalQuery as unknown as InternalQueryBuilder<QueryCtx, EmptyArgs>;
|
|
628
|
+
|
|
629
|
+
/** \`internalMutation\` builder bound to this project's typed {@link MutationCtx} — never exposed on \`api\`. */
|
|
630
|
+
export const internalMutation = lunoraBuilders.internalMutation as unknown as InternalMutationBuilder<MutationCtx, EmptyArgs>;
|
|
631
|
+
|
|
632
|
+
/** \`internalAction\` builder bound to this project's typed {@link ActionCtx} — never exposed on \`api\`. */
|
|
633
|
+
export const internalAction = lunoraBuilders.internalAction as unknown as InternalActionBuilder<ActionCtx, EmptyArgs>;
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* \`definePolicy\` bound to THIS schema's {@link DataModel} + {@link Relations}.
|
|
637
|
+
* Import it from here (\`./_generated/server\`) instead of \`@lunora/server\` to get
|
|
638
|
+
* a relation-aware RLS authoring surface: \`table\` autocompletes to a real table
|
|
639
|
+
* name and the \`when\` predicate type-checks against the table's columns **and**
|
|
640
|
+
* its declared relations — \`{ author: { is: {...} } }\`, \`{ posts: { some: {...} } }\`,
|
|
641
|
+
* etc. (the \`@lunora/do\` pre-resolver resolves relation predicates on reads).
|
|
642
|
+
* Runtime-identical to \`@lunora/server\`'s \`definePolicy\`; only the types narrow,
|
|
643
|
+
* so the \`rls()\` chain discovers a policy authored either way the same.
|
|
644
|
+
*/
|
|
645
|
+
export const definePolicy = createPolicyDsl<DataModel, Relations${Re}>();
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* The validator builder \`v\`, with \`v.id(...)\` constrained to THIS schema's
|
|
649
|
+
* table names — so the argument autocompletes and returns a precise
|
|
650
|
+
* \`Id<"table">\`. Identical to \`@lunora/server\`'s \`v\` at runtime; the only
|
|
651
|
+
* difference is the tightened \`id\` type.
|
|
652
|
+
*
|
|
653
|
+
* Import \`v\` from here (\`./_generated/server\`) in your queries/mutations/actions
|
|
654
|
+
* to get table-name autocomplete. \`lunora/schema.ts\` can't use it — it is the
|
|
655
|
+
* file that defines the table names, so they aren't known yet there; pass the
|
|
656
|
+
* names as string literals and the generated \`Id<"table">\` types still catch a
|
|
657
|
+
* typo wherever the id is used.
|
|
658
|
+
*/
|
|
659
|
+
export const v = vBase as unknown as Omit<typeof vBase, "id"> & {
|
|
660
|
+
id: <T extends TableName>(table: T) => ColumnValidator<IdOfTable<T>, IdOfTable<T>>;
|
|
661
|
+
};
|
|
662
|
+
`},bn=e=>{const t={connect:[],disconnect:[]};for(const n of e)n.lifecycle&&t[n.lifecycle].push(`${$(n.filePath)}:${n.exportName}`);return t},or=e=>{const{agents:t=[],functions:n,migrations:a=[],mutators:r=[],shapes:o=[],useUmbrella:s=!1,usesSandbox:i=!1}=e,l=n.length>0,u=V(s),{dispatchBody:p,importBlock:b,installBlock:w,migrationBody:v,mutatorPaths:d,shapeBody:c}=gn(n,a,r,o),g=ln(t,n),f=dn(i,n),T=[g.lines,f.lines].filter(C=>C.length>0).join(`
|
|
663
|
+
`);let x=p;T.length>0&&(x=p.length>0?`${p.trimEnd()}
|
|
664
|
+
${T}
|
|
665
|
+
`:`
|
|
666
|
+
${T}
|
|
667
|
+
`);const A=bn(n),_=o.length>0?`import type { RegisteredShape } from "${u.server}";
|
|
668
|
+
`:"",y=o.length>0?`
|
|
669
|
+
/**
|
|
670
|
+
* Replication-shape registry — one entry per \`defineShape\` in \`lunora/shapes.ts\`.
|
|
671
|
+
* The generated ShardDO's \`resolveShape\` override looks a shape up by name and
|
|
672
|
+
* evaluates its trusted \`compileWhere(ctx, args)\` to authorize + scope a
|
|
673
|
+
* \`shape_subscribe\` (reads-as-permissions).
|
|
674
|
+
*/
|
|
675
|
+
export const LUNORA_SHAPES: Record<string, RegisteredShape> = {${c}};
|
|
676
|
+
`:"",Y=d.length>0?`
|
|
677
|
+
/**
|
|
678
|
+
* Custom-mutator function paths — the \`LUNORA_FUNCTIONS\` keys the generated
|
|
679
|
+
* ShardDO's \`isCustomMutator\` override routes through the client-watermark push
|
|
680
|
+
* protocol (\`x-lunora-client-id\`/\`x-lunora-client-seq\` ordering).
|
|
681
|
+
*/
|
|
682
|
+
export const LUNORA_MUTATOR_PATHS: ReadonlySet<string> = new Set([${d.map(C=>JSON.stringify(C)).join(", ")}]);
|
|
683
|
+
`:"",Z=w.length>0?`import { DEFER_VALIDATION as DEFER, installCompiledValidatorMap } from "${u.values}";
|
|
684
|
+
`:"",ee=w.length>0?`
|
|
685
|
+
/**
|
|
686
|
+
* AOT-compiled argument validators (Worker-safe, no \`eval\`). Each is installed
|
|
687
|
+
* onto its function's live \`.args\` object and consulted by the interpreted
|
|
688
|
+
* parser as a zero-allocation fast path; anything it can't model is deferred.
|
|
689
|
+
*/
|
|
690
|
+
${w}
|
|
691
|
+
`:"",I=mn(n),j=I.types?`
|
|
692
|
+
${I.types}
|
|
693
|
+
`:"",S=I.implementation?`
|
|
694
|
+
${I.implementation}
|
|
695
|
+
`:"",W=Ie(I.types),L=W.length>0?`import type { ${W.join(", ")} } from "./dataModel.js";
|
|
696
|
+
`:"",te=l?"context":"_context",H=l?`${hn}
|
|
697
|
+
|
|
698
|
+
`:"";return`${k}${b}${g.importLine}${f.importLine}${Z}${_}import { LunoraError } from "${u.server}";
|
|
699
|
+
import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
|
|
700
|
+
${L}
|
|
701
|
+
/**
|
|
702
|
+
* Single registered function, narrowed to the shape \`handleRpc\` needs.
|
|
703
|
+
* The real argument validators / return types are checked elsewhere — at
|
|
704
|
+
* the dispatch site we erase to \`unknown\` so a single map can host every
|
|
705
|
+
* registered function.
|
|
706
|
+
*/
|
|
707
|
+
export interface RegisteredLunoraFunction {
|
|
708
|
+
kind: "action" | "mutation" | "query" | "stream";
|
|
709
|
+
args: Record<string, unknown>;
|
|
710
|
+
/**
|
|
711
|
+
* For \`"action" | "mutation" | "query"\` the handler is awaited and its result returned.
|
|
712
|
+
* For \`"stream"\` the handler returns an \`AsyncIterable\` synchronously and takes an
|
|
713
|
+
* \`AbortSignal\` as a third argument — the runtime drives it frame-by-frame.
|
|
714
|
+
*/
|
|
715
|
+
handler: ((context: unknown, args: Record<string, unknown>) => Promise<unknown> | unknown) | ((context: unknown, args: Record<string, unknown>, signal?: AbortSignal) => AsyncIterable<unknown>);
|
|
716
|
+
/** \`"internal"\` functions are rejected on the external RPC path; absence === public. */
|
|
717
|
+
visibility?: "internal" | "public";
|
|
718
|
+
}
|
|
719
|
+
${g.prelude}${f.prelude}
|
|
720
|
+
/**
|
|
721
|
+
* Static dispatch table. The key matches the \`__lunoraRef\` the client
|
|
722
|
+
* emits (\`api[namespace][fn].__lunoraRef === "namespace:fn"\`).
|
|
723
|
+
*/
|
|
724
|
+
export const LUNORA_FUNCTIONS: Record<string, RegisteredLunoraFunction> = {${x}};
|
|
725
|
+
${ee}${y}${Y}
|
|
726
|
+
/**
|
|
727
|
+
* Connection-lifecycle manifest: the function paths the generated ShardDO
|
|
728
|
+
* dispatches when a client's WebSocket connects (\`connect\`) or disconnects
|
|
729
|
+
* (\`disconnect\`). Each path also resolves through {@link LUNORA_FUNCTIONS}; the
|
|
730
|
+
* DO runs every hook under the socket's verified identity via system dispatch.
|
|
731
|
+
*/
|
|
732
|
+
export const LUNORA_LIFECYCLE_HOOKS: { connect: readonly string[]; disconnect: readonly string[] } = {
|
|
733
|
+
connect: [${A.connect.map(C=>JSON.stringify(C)).join(", ")}],
|
|
734
|
+
disconnect: [${A.disconnect.map(C=>JSON.stringify(C)).join(", ")}],
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Resolve and invoke a registered function from an external caller. Throws a
|
|
739
|
+
* LunoraError-shaped object (404) when the path is unknown — the runtime's
|
|
740
|
+
* structural error mapper turns that into the right HTTP status. Internal
|
|
741
|
+
* functions are treated as not-found so their existence never leaks to clients.
|
|
742
|
+
*/
|
|
743
|
+
export const dispatchLunoraFunction = async (functionPath: string, context: unknown, args: Record<string, unknown>): Promise<unknown> => {
|
|
744
|
+
const registered = LUNORA_FUNCTIONS[functionPath];
|
|
745
|
+
|
|
746
|
+
if (!registered || registered.visibility === "internal") {
|
|
747
|
+
throw new LunoraError("FUNCTION_NOT_FOUND", \`function not registered: \${functionPath}\`);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return registered.handler(context, args);
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* A handler context accepted by {@link createCaller}. The runtime context the
|
|
755
|
+
* shard DO builds is uniform across kinds, so any handler's \`ctx\` works here.
|
|
756
|
+
*/
|
|
757
|
+
export type CallerCtx = ActionCtx | MutationCtx | QueryCtx;
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Typed, in-process server-to-server caller. Every registered function — public
|
|
761
|
+
* *and* internal — is reachable; each call dispatches against the same shard
|
|
762
|
+
* with the supplied \`context\`, exactly like \`ctx.runQuery\`/\`runMutation\`/
|
|
763
|
+
* \`runAction\` but without re-stating the function path.
|
|
764
|
+
*/
|
|
765
|
+
export interface Caller {${j}}
|
|
766
|
+
|
|
767
|
+
${H}/** Build a {@link Caller} bound to \`context\` (typically a handler's \`ctx\`). */
|
|
768
|
+
export const createCaller = (${te}: CallerCtx): Caller => ({${S}});
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Single registered data migration, narrowed to the shape the per-shard runner
|
|
772
|
+
* consumes. \`up\`/\`down\` are erased to a structural transform; the authoring
|
|
773
|
+
* validation lives in \`defineMigration\`.
|
|
774
|
+
*/
|
|
775
|
+
export interface RegisteredDataMigration {
|
|
776
|
+
batchSize?: number;
|
|
777
|
+
down?: (document: Record<string, unknown>) => Record<string, unknown> | undefined;
|
|
778
|
+
id: string;
|
|
779
|
+
table: string;
|
|
780
|
+
up: (document: Record<string, unknown>) => Record<string, unknown> | undefined;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Registry of online data migrations keyed by \`defineMigration\`'s \`id\`. The
|
|
785
|
+
* shard DO's admin RPC and the CLI resolve migrations to run through this map.
|
|
786
|
+
*/
|
|
787
|
+
export const LUNORA_MIGRATIONS: Record<string, RegisteredDataMigration> = {${v}};
|
|
788
|
+
`},yn=e=>{const t={};for(const n of e.tables){const a={};for(const[r,o]of Object.entries(n.shape)){const s=o.kind==="optional"&&o.inner?o.inner:o;s.kind==="id"&&s.tableName!==void 0&&(a[r]=s.tableName)}Object.keys(a).length>0&&(t[n.name]=a)}return t},wn=e=>{const t={};for(const n of e.tables){const a=[];for(const[r,o]of Object.entries(n.shape))(o.kind==="optional"&&o.inner?o.inner:o).kind==="storage"&&a.push(r);a.length>0&&(t[n.name]=a)}return t},xn=e=>{const t={};for(const n of e.tables){const a=[...n.indexes.map(r=>({fields:[...r.fields],name:r.name,type:"index",...r.unique===!0?{unique:!0}:{}})),...n.searchIndexes.map(r=>({fields:[r.field,...r.filterFields??[]],name:r.name,type:"search"})),...(n.geoIndexes??[]).map(r=>({fields:[r.field],name:r.name,type:"geo"})),...n.rankIndexes.map(r=>({fields:r.sortBy.map(o=>o.field),name:r.name,type:"rank"})),...n.vectorIndexes.map(r=>({fields:r.field===void 0?[]:[r.field],name:r.name,type:"vector"}))];a.length>0&&(t[n.name]=a)}return t},kn=e=>{const t=[];for(const n of e.tables)!n.ttl||n.shardMode==="global"||t.push({...n.ttl.after===void 0?{}:{after:n.ttl.after},field:n.ttl.field,...n.softDelete?{softDeleteField:n.softDelete.field}:{},table:n.name});return t},vn=e=>{const t={};for(const n of e.tables){const a=[{name:"_id",optional:!1,pk:!0,type:"id"},{name:"_creationTime",optional:!1,type:"number"}];for(const[r,o]of Object.entries(n.shape)){const s=o.kind==="optional"||!!o.column?.hasDefault,i=o.kind==="optional"&&o.inner?o.inner:o,l={name:r,optional:s,type:i.kind};i.kind==="id"&&i.tableName!==void 0&&(l.ref=i.tableName),i.kind==="storage"&&(l.isStorage=!0),a.push(l)}t[n.name]=a}return t},$n=e=>e?{importFragment:"serveRelationFanout, ",override:`
|
|
789
|
+
protected override async runRelationFanoutRead(functionPath: string, args: Record<string, unknown>): Promise<unknown> {
|
|
790
|
+
this.ensureMigrated();
|
|
791
|
+
|
|
792
|
+
const { db } = this.buildCtx({ functionPath }) as { db: DatabaseWriterLike };
|
|
793
|
+
|
|
794
|
+
return serveRelationFanout(schema as unknown as SchemaLike, db, functionPath, args);
|
|
795
|
+
}
|
|
796
|
+
`}:{importFragment:"",override:""},Sn=e=>{if(!e)return{build:"",configField:"",contextField:"",stub:""};const t='throw new Error("ctx.ai: no AI binding found. Add an \\`ai\\` binding (env.AI) to wrangler.jsonc, or pass \\`ai\\` to createShardDO().");';return{build:`
|
|
797
|
+
const aiBinding = config.ai?.(env) ?? (env as Record<string, unknown>).AI;
|
|
798
|
+
// Correlate AI-Gateway-routed calls with the Lunora trace: thread the
|
|
799
|
+
// function path + trace id into createAi, which folds them into the
|
|
800
|
+
// gateway's native \`metadata\` only when a gateway is configured (absent
|
|
801
|
+
// otherwise). Mirror the tracer's anchor guard — a deferred subscription
|
|
802
|
+
// re-run must not borrow a concurrent dispatch's trace, so read
|
|
803
|
+
// \`getCurrentTrace()\` only on the synchronous (non-threaded-identity) path.
|
|
804
|
+
const aiTrace = options.identity ? undefined : this.getCurrentTrace();
|
|
805
|
+
const ai: LunoraAi = aiBinding
|
|
806
|
+
? createAi({ binding: aiBinding as AiBindingLike, env: env as Record<string, unknown>, metadata: { functionPath: options.functionPath, traceId: aiTrace?.traceId } })
|
|
807
|
+
: aiStub;
|
|
808
|
+
`,configField:`
|
|
809
|
+
ai?: (env: Record<string, unknown>) => AiBindingLike;`,contextField:`
|
|
810
|
+
ai,`,stub:`
|
|
811
|
+
const aiStub: LunoraAi = {
|
|
812
|
+
embeddingModel: () => {
|
|
813
|
+
${t}
|
|
814
|
+
},
|
|
815
|
+
model: () => {
|
|
816
|
+
${t}
|
|
817
|
+
},
|
|
818
|
+
run: async () => {
|
|
819
|
+
${t}
|
|
820
|
+
},
|
|
821
|
+
// workersai is a callable-with-properties; a bare throwing arrow isn't
|
|
822
|
+
// structurally assignable, so cast it. Never invoked (the stub throws first).
|
|
823
|
+
workersai: (() => {
|
|
824
|
+
${t}
|
|
825
|
+
}) as unknown as LunoraAi["workersai"],
|
|
826
|
+
};
|
|
827
|
+
`}},D={build:"",configField:"",contextField:"",importLines:[],stub:""},Rn=e=>{if(!e)return D;const t='throw new Error("ctx.kv: no KV binding found. Add a \\`kv_namespaces\\` binding (env.KV) to wrangler.jsonc, or pass \\`kv\\` to createShardDO().");';return{build:`
|
|
828
|
+
const kvBinding = config.kv?.(env) ?? (env as Record<string, unknown>).KV;
|
|
829
|
+
const kv: Kv = kvBinding ? createKv({ namespace: kvBinding as KVNamespaceLike }) : kvStub;
|
|
830
|
+
`,configField:`
|
|
831
|
+
kv?: (env: Record<string, unknown>) => KVNamespaceLike;`,contextField:`
|
|
832
|
+
kv,`,importLines:['import type { Kv, KVNamespaceLike } from "@lunora/bindings/kv";','import { createKv } from "@lunora/bindings/kv";'],stub:`
|
|
833
|
+
const kvStub: Kv = {
|
|
834
|
+
delete: async () => {
|
|
835
|
+
${t}
|
|
836
|
+
},
|
|
837
|
+
get: async () => {
|
|
838
|
+
${t}
|
|
839
|
+
},
|
|
840
|
+
getRaw: async () => {
|
|
841
|
+
${t}
|
|
842
|
+
},
|
|
843
|
+
getWithMetadata: async () => {
|
|
844
|
+
${t}
|
|
845
|
+
},
|
|
846
|
+
list: async () => {
|
|
847
|
+
${t}
|
|
848
|
+
},
|
|
849
|
+
put: async () => {
|
|
850
|
+
${t}
|
|
851
|
+
},
|
|
852
|
+
};
|
|
853
|
+
`}},Nn=(e,t)=>e?{build:`
|
|
854
|
+
const flags: import("${t}").LunoraFlags = createFlags({
|
|
855
|
+
hooks: flagsConfig.hooks,
|
|
856
|
+
logger: flagsConfig.logger,
|
|
857
|
+
provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
|
|
858
|
+
targetingKey: () => flagsConfig.identify?.({ identity: identity ?? null, userId: userId ?? null }),
|
|
859
|
+
});
|
|
860
|
+
`,configField:`
|
|
861
|
+
flags?: (env: Record<string, unknown>) => import("${t}").Provider;`,contextField:`
|
|
862
|
+
flags,`,importLines:[`import { createFlags } from "${t}";`,'import flagsConfig from "../flags.js";'],stub:""}:D,On=e=>e?{build:`
|
|
863
|
+
const { notify, push } = createNotify(notifyConfig, env, { log, metrics });
|
|
864
|
+
`,configField:"",contextField:`
|
|
865
|
+
notify,
|
|
866
|
+
push,`,importLines:['import { createNotify } from "@lunora/notify";','import notifyConfig from "../notify.js";'],stub:""}:D,Tn=e=>e?{build:`
|
|
867
|
+
const envConfig = lunoraEnvContract.${e.exportName}(env);
|
|
868
|
+
`,configField:"",contextField:`
|
|
869
|
+
env: envConfig,`,importLines:['import * as lunoraEnvContract from "../env.js";'],stub:""}:D,An=e=>e?{build:`
|
|
870
|
+
const access = accessFacade(identity, userId);
|
|
871
|
+
`,configField:"",contextField:`
|
|
872
|
+
access,`,importLines:['import { accessFacade } from "@lunora/cloudflare-access/context";'],stub:""}:D,Ln=(e,t,n)=>{if(!t)return{constant:"",evaluateOverride:"",subscriptionOverride:""};const a=i=>`
|
|
873
|
+
const env = (this.env ?? {}) as Record<string, unknown>;
|
|
874
|
+
const flags: import("${n}").LunoraFlags = createFlags({
|
|
875
|
+
hooks: flagsConfig.hooks,
|
|
876
|
+
logger: flagsConfig.logger,
|
|
877
|
+
provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
|
|
878
|
+
targetingKey: ${i},
|
|
879
|
+
});`,r=`
|
|
880
|
+
/** Statically-discovered feature flags (\`ctx.flags.<type>("key")\` reads) served via \`__lunora_admin__:listFlags\` + the reactive \`__lunora_flags__:\` channel. */
|
|
881
|
+
const LUNORA_FLAG_KEYS: ReadonlyArray<{ key: string; type: "boolean" | "number" | "object" | "string" }> = ${JSON.stringify(e,void 0,4)};
|
|
882
|
+
`,o=`
|
|
883
|
+
protected override async evaluateFlags(context?: Record<string, unknown>): Promise<FlagsResult> {${a("undefined")}
|
|
884
|
+
const evalContext = context as import("${n}").EvaluationContext | undefined;
|
|
885
|
+
const evaluations: FlagsResult["flags"] = [];
|
|
886
|
+
|
|
887
|
+
for (const entry of LUNORA_FLAG_KEYS) {
|
|
888
|
+
// eslint-disable-next-line no-await-in-loop -- flags evaluate sequentially; each shares the single memoized provider client
|
|
889
|
+
const details =
|
|
890
|
+
entry.type === "boolean"
|
|
891
|
+
? await flags.details.boolean(entry.key, false, evalContext)
|
|
892
|
+
: entry.type === "number"
|
|
893
|
+
? await flags.details.number(entry.key, 0, evalContext)
|
|
894
|
+
: entry.type === "string"
|
|
895
|
+
? await flags.details.string(entry.key, "", evalContext)
|
|
896
|
+
: await flags.details.object(entry.key, {}, evalContext);
|
|
897
|
+
|
|
898
|
+
evaluations.push({ errorCode: details.errorCode, key: entry.key, reason: details.reason, type: entry.type, value: details.value, variant: details.variant });
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return { configured: true, flags: evaluations };
|
|
902
|
+
}
|
|
903
|
+
`,s=`
|
|
904
|
+
protected override runFlagSubscriptionRead(_functionPath: string, args: Record<string, unknown>, identity?: { identity?: Record<string, unknown>; userId?: string }): Promise<unknown> {${a("() => flagsConfig.identify?.({ identity: identity?.identity ?? null, userId: identity?.userId ?? null })")}
|
|
905
|
+
const key = typeof args.key === "string" ? args.key : "";
|
|
906
|
+
|
|
907
|
+
// SECURITY: the reactive channel is public (any socket, no auth). Serve
|
|
908
|
+
// ONLY statically-discovered flag keys — an arbitrary client-supplied key
|
|
909
|
+
// would let a subscriber probe the value of internal/unreleased flags the
|
|
910
|
+
// app never exposes. Unknown key ⇒ the "nothing to deliver" sentinel.
|
|
911
|
+
// eslint-disable-next-line unicorn/no-null -- the base hook's "nothing to deliver" sentinel
|
|
912
|
+
if (key.length === 0 || !LUNORA_FLAG_KEYS.some((entry) => entry.key === key)) {
|
|
913
|
+
return Promise.resolve(null);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// SECURITY: evaluate under the socket's server-verified identity ONLY
|
|
917
|
+
// (the targetingKey resolved above). Client-supplied targeting context is
|
|
918
|
+
// NOT honored on this public channel — otherwise a subscriber could spoof
|
|
919
|
+
// targeting attributes (e.g. plan/role) to unlock a flag gated on them.
|
|
920
|
+
const context = undefined;
|
|
921
|
+
|
|
922
|
+
if (args.type === "number") {
|
|
923
|
+
return flags.number(key, typeof args.default === "number" ? args.default : 0, context);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (args.type === "string") {
|
|
927
|
+
return flags.string(key, typeof args.default === "string" ? args.default : "", context);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (args.type === "object") {
|
|
931
|
+
return flags.object(key, (args.default ?? {}) as import("${n}").JsonValue, context);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return flags.boolean(key, typeof args.default === "boolean" ? args.default : false, context);
|
|
935
|
+
}
|
|
936
|
+
`;return{constant:r,evaluateOverride:o,subscriptionOverride:s}},Cn=e=>{if(!e)return D;const t='throw new Error("ctx.analytics: no Analytics Engine binding found. Add an \\`analytics_engine_datasets\\` binding (env.ANALYTICS) to wrangler.jsonc, or pass \\`analytics\\` to createShardDO().");';return{build:`
|
|
937
|
+
const analyticsBinding = config.analytics?.(env) ?? (env as Record<string, unknown>).ANALYTICS;
|
|
938
|
+
const analytics: AnalyticsClient = analyticsBinding ? createAnalytics(analyticsBinding as AnalyticsEngineDatasetLike) : analyticsStub;
|
|
939
|
+
`,configField:`
|
|
940
|
+
analytics?: (env: Record<string, unknown>) => AnalyticsEngineDatasetLike;`,contextField:`
|
|
941
|
+
analytics,`,importLines:['import type { AnalyticsClient, AnalyticsEngineDatasetLike } from "@lunora/bindings/analytics";','import { createAnalytics } from "@lunora/bindings/analytics";'],stub:`
|
|
942
|
+
const analyticsStub: AnalyticsClient = {
|
|
943
|
+
track: () => {
|
|
944
|
+
${t}
|
|
945
|
+
},
|
|
946
|
+
writeDataPoint: () => {
|
|
947
|
+
${t}
|
|
948
|
+
},
|
|
949
|
+
};
|
|
950
|
+
`}},Dn=e=>{if(!e)return D;const t='throw new Error("ctx.images: no Images binding found. Add an \\`images\\` binding (env.IMAGES) to wrangler.jsonc, or pass \\`images\\` to createShardDO().");';return{build:`
|
|
951
|
+
const imagesBinding = config.images?.(env) ?? (env as Record<string, unknown>).IMAGES;
|
|
952
|
+
const images: Images = imagesBinding ? createImages({ binding: imagesBinding as ImagesBindingLike }) : imagesStub;
|
|
953
|
+
`,configField:`
|
|
954
|
+
images?: (env: Record<string, unknown>) => ImagesBindingLike;`,contextField:`
|
|
955
|
+
images,`,importLines:['import type { Images, ImagesBindingLike } from "@lunora/bindings/images";','import { createImages } from "@lunora/bindings/images";'],stub:`
|
|
956
|
+
const imagesStub: Images = {
|
|
957
|
+
info: async () => {
|
|
958
|
+
${t}
|
|
959
|
+
},
|
|
960
|
+
transform: async () => {
|
|
961
|
+
${t}
|
|
962
|
+
},
|
|
963
|
+
};
|
|
964
|
+
`}},_n=e=>e?{build:`
|
|
965
|
+
const sql: SqlClient = config.sql ? config.sql(env) : sqlStub;
|
|
966
|
+
`,configField:`
|
|
967
|
+
sql?: (env: Record<string, unknown>) => SqlClient;`,contextField:`
|
|
968
|
+
sql,`,importLines:['import type { SqlClient } from "@lunora/hyperdrive";'],stub:`
|
|
969
|
+
const sqlStub: SqlClient = {
|
|
970
|
+
query: async () => {
|
|
971
|
+
throw new Error("ctx.sql: provide a \\\`sql\\\` config thunk that builds a SqlClient from your driver, e.g. \\\`sql: (env) => fromPostgresJs(postgres(env.HYPERDRIVE.connectionString))\\\`.");
|
|
972
|
+
},
|
|
973
|
+
};
|
|
974
|
+
`}:D,In=e=>{if(!e)return D;const t="throw new Error(\"ctx.browser: provide a \\`browser\\` config thunk, e.g. \\`browser: (env) => createBrowser({ binding: env.BROWSER, launch })\\` with \\`import { launch } from '@cloudflare/playwright'\\`.\");";return{build:`
|
|
975
|
+
const browser: Browser = config.browser ? config.browser(env) : browserStub;
|
|
976
|
+
`,configField:`
|
|
977
|
+
browser?: (env: Record<string, unknown>) => Browser;`,contextField:`
|
|
978
|
+
browser,`,importLines:['import type { Browser } from "@lunora/browser";'],stub:`
|
|
979
|
+
const browserStub: Browser = {
|
|
980
|
+
content: async () => {
|
|
981
|
+
${t}
|
|
982
|
+
},
|
|
983
|
+
launch: async () => {
|
|
984
|
+
${t}
|
|
985
|
+
},
|
|
986
|
+
pdf: async () => {
|
|
987
|
+
${t}
|
|
988
|
+
},
|
|
989
|
+
scrape: async () => {
|
|
990
|
+
${t}
|
|
991
|
+
},
|
|
992
|
+
screenshot: async () => {
|
|
993
|
+
${t}
|
|
994
|
+
},
|
|
995
|
+
};
|
|
996
|
+
`}},Pn=e=>{if(!e)return D;const t='throw new Error("ctx.r2sql: no R2 SQL credentials found. Set \\`R2_SQL_TOKEN\\`, \\`R2_SQL_ACCOUNT_ID\\` (or \\`CLOUDFLARE_ACCOUNT_ID\\`), and \\`R2_SQL_BUCKET\\` in your env/.dev.vars, or pass an \\`r2sql\\` config thunk to createShardDO().");';return{build:`
|
|
997
|
+
const r2sqlEnv = env as Record<string, unknown>;
|
|
998
|
+
const r2sqlAccountId = (r2sqlEnv.R2_SQL_ACCOUNT_ID ?? r2sqlEnv.CLOUDFLARE_ACCOUNT_ID) as string | undefined;
|
|
999
|
+
const r2sqlToken = r2sqlEnv.R2_SQL_TOKEN as string | undefined;
|
|
1000
|
+
const r2sqlBucket = r2sqlEnv.R2_SQL_BUCKET as string | undefined;
|
|
1001
|
+
const r2sql: R2SqlClient = config.r2sql
|
|
1002
|
+
? config.r2sql(env)
|
|
1003
|
+
: r2sqlAccountId && r2sqlToken && r2sqlBucket
|
|
1004
|
+
? createR2Sql({ accountId: r2sqlAccountId, apiToken: r2sqlToken, bucket: r2sqlBucket })
|
|
1005
|
+
: r2sqlStub;
|
|
1006
|
+
`,configField:`
|
|
1007
|
+
r2sql?: (env: Record<string, unknown>) => R2SqlClient;`,contextField:"",importLines:['import type { R2SqlClient } from "@lunora/bindings/r2sql";','import { createR2Sql } from "@lunora/bindings/r2sql";'],stub:`
|
|
1008
|
+
const r2sqlStub: R2SqlClient = {
|
|
1009
|
+
describe: async () => {
|
|
1010
|
+
${t}
|
|
1011
|
+
},
|
|
1012
|
+
explain: async () => {
|
|
1013
|
+
${t}
|
|
1014
|
+
},
|
|
1015
|
+
from: () => {
|
|
1016
|
+
${t}
|
|
1017
|
+
},
|
|
1018
|
+
query: async () => {
|
|
1019
|
+
${t}
|
|
1020
|
+
},
|
|
1021
|
+
showDatabases: async () => {
|
|
1022
|
+
${t}
|
|
1023
|
+
},
|
|
1024
|
+
showTables: async () => {
|
|
1025
|
+
${t}
|
|
1026
|
+
},
|
|
1027
|
+
};
|
|
1028
|
+
`}},En=e=>e?{build:`
|
|
1029
|
+
const pipelinesBinding = config.pipelines?.(env) ?? (env as Record<string, unknown>).PIPELINES;
|
|
1030
|
+
const pipelines: PipelineClient = pipelinesBinding ? createPipelines({ binding: pipelinesBinding as PipelineBindingLike }) : pipelinesStub;
|
|
1031
|
+
`,configField:`
|
|
1032
|
+
pipelines?: (env: Record<string, unknown>) => PipelineBindingLike;`,contextField:"",importLines:['import type { PipelineBindingLike, PipelineClient } from "@lunora/bindings/pipelines";','import { createPipelines } from "@lunora/bindings/pipelines";'],stub:`
|
|
1033
|
+
const pipelinesStub: PipelineClient = {
|
|
1034
|
+
send: async () => {
|
|
1035
|
+
throw new Error("ctx.pipelines: no Pipelines binding found. Add a \\\`pipelines\\\` binding (env.PIPELINES) to wrangler.jsonc, or pass \\\`pipelines\\\` to createShardDO().");
|
|
1036
|
+
},
|
|
1037
|
+
};
|
|
1038
|
+
`}:D,ir=(e,t)=>{if(e.length===0)return"";const n=t?`, ${JSON.stringify(t)}`:"",a=e.map(o=>(m(o.exportName,`container export "${o.exportName}"`),m(o.className,`container class "${o.className}"`),`/** Container DO for the \`${o.exportName}\` definition (binding \`${o.bindingName}\`). */
|
|
1039
|
+
export class ${o.className} extends LunoraContainer {
|
|
1040
|
+
public constructor(ctx: ConstructorParameters<typeof LunoraContainer>[0], env: Record<string, unknown>) {
|
|
1041
|
+
super(ctx, env, ${o.exportName}, "${o.exportName}"${n});
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
`)).join(`
|
|
1045
|
+
`),r=e.map(o=>o.exportName).join(", ");return`${k}/**
|
|
1046
|
+
* Container-enabled Durable Object classes for the containers declared in
|
|
1047
|
+
* \`lunora/containers.ts\`. Re-export them from your worker entry — wrangler
|
|
1048
|
+
* requires each \`containers[].class_name\` to be exported by the worker:
|
|
1049
|
+
*
|
|
1050
|
+
* \`export * from "./lunora/_generated/containers.js";\`
|
|
1051
|
+
*
|
|
1052
|
+
* \`ContainerProxy\` is re-exported alongside them: the egress-interception path
|
|
1053
|
+
* (\`allowedHosts\`/\`deniedHosts\`/\`interceptHttps\` and the runtime
|
|
1054
|
+
* \`handle.egress\` controls) routes container outbound traffic through this
|
|
1055
|
+
* WorkerEntrypoint, so it too must be exported by the deployed worker.
|
|
1056
|
+
*/
|
|
1057
|
+
import { LunoraContainer } from "@lunora/container/do";
|
|
1058
|
+
|
|
1059
|
+
import { ${r} } from "../containers.js";
|
|
1060
|
+
|
|
1061
|
+
export { ContainerProxy } from "@lunora/container/do";
|
|
1062
|
+
|
|
1063
|
+
${a}`},Fn=(e,t)=>{if(e.length===0)return{build:"",contextField:"",importLines:[],specs:""};for(const a of e)m(a.exportName,`container export "${a.exportName}"`),m(a.bindingName,`container binding "${a.bindingName}"`);const n=e.map(a=>{const r=a.maxInstances===void 0?"":`, maxInstances: ${String(a.maxInstances)}`;return` { binding: "${a.bindingName}", exportName: "${a.exportName}"${r} },`}).join(`
|
|
1064
|
+
`);return{build:`
|
|
1065
|
+
const containers = createContainerContext(env, LUNORA_CONTAINERS, ${t?JSON.stringify(t):"undefined"}, this.getCurrentTraceparent());
|
|
1066
|
+
`,contextField:`
|
|
1067
|
+
containers,`,importLines:['import type { ContainerBindingSpec } from "@lunora/container";','import { createContainerContext } from "@lunora/container";'],specs:`
|
|
1068
|
+
/** Wiring specs for \`ctx.containers\` (codegen-derived from \`lunora/containers.ts\`). */
|
|
1069
|
+
const LUNORA_CONTAINERS: ReadonlyArray<ContainerBindingSpec> = [
|
|
1070
|
+
${n}
|
|
1071
|
+
];
|
|
1072
|
+
`}},sr=e=>{if(e.length===0)return"";const t=e.map(a=>(m(a.exportName,`workflow export "${a.exportName}"`),m(a.className,`workflow class "${a.className}"`),`/** WorkflowEntrypoint for the \`${a.exportName}\` definition (binding \`${a.bindingName}\`). */
|
|
1073
|
+
export class ${a.className} extends LunoraWorkflow<WorkflowParamsOf<typeof ${a.exportName}>, WorkflowOutputOf<typeof ${a.exportName}>> {
|
|
1074
|
+
public constructor(ctx: ConstructorParameters<typeof LunoraWorkflow>[0], env: Record<string, unknown>) {
|
|
1075
|
+
super(ctx, env, ${a.exportName}, "${a.exportName}");
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
`)).join(`
|
|
1079
|
+
`),n=e.map(a=>a.exportName).join(", ");return`${k}/**
|
|
1080
|
+
* WorkflowEntrypoint classes for the workflows declared in
|
|
1081
|
+
* \`lunora/workflows.ts\`. Re-export them from your worker entry — wrangler
|
|
1082
|
+
* requires each \`workflows[].class_name\` to be exported by the worker:
|
|
1083
|
+
*
|
|
1084
|
+
* \`export * from "./lunora/_generated/workflows.js";\`
|
|
1085
|
+
*/
|
|
1086
|
+
import LunoraWorkflow from "@lunora/workflow/do";
|
|
1087
|
+
|
|
1088
|
+
import { ${n} } from "../workflows.js";
|
|
1089
|
+
|
|
1090
|
+
/** Params type carried by a \`defineWorkflow\` definition (its phantom \`__params\`), so each entrypoint keeps the authored \`ctx.params\` type. */
|
|
1091
|
+
type WorkflowParamsOf<Definition> = Definition extends { __params?: infer Params }
|
|
1092
|
+
? unknown extends Params
|
|
1093
|
+
? Record<string, unknown>
|
|
1094
|
+
: NonNullable<Params>
|
|
1095
|
+
: Record<string, unknown>;
|
|
1096
|
+
|
|
1097
|
+
/** Output type carried by a \`defineWorkflow\` definition (its phantom \`__output\`), so each entrypoint keeps the authored return type. */
|
|
1098
|
+
type WorkflowOutputOf<Definition> = Definition extends { __output?: infer Output } ? Output : unknown;
|
|
1099
|
+
|
|
1100
|
+
${t}`},lr=e=>{if(e.length===0)return"";const t=e.filter(r=>r.voice).length>0,n=e.map(r=>{m(r.exportName,`agent export "${r.exportName}"`),m(r.className,`agent class "${r.className}"`);const o=`/** WorkflowEntrypoint for the \`${r.exportName}\` agent (binding \`${r.bindingName}\`). */
|
|
1101
|
+
export class ${r.className} extends LunoraWorkflow<AgentRunInput, AgentRunResult> {
|
|
1102
|
+
public constructor(ctx: ConstructorParameters<typeof LunoraWorkflow>[0], env: Record<string, unknown>) {
|
|
1103
|
+
super(ctx, env, compileAgentWorkflow(${r.exportName}, "${r.exportName}"), "${r.exportName}");
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
`;if(!r.voice)return o;const s=r.voiceClassName??"",i=r.voiceBindingName??"";m(s,`agent voice class "${s}"`);const l=`
|
|
1107
|
+
/** Voice-session Durable Object for the \`${r.exportName}\` agent (binding \`${i}\`). */
|
|
1108
|
+
export class ${s} extends VoiceSessionDO {
|
|
1109
|
+
public constructor(ctx: ConstructorParameters<typeof VoiceSessionDO>[0], env: Record<string, unknown>) {
|
|
1110
|
+
super(ctx, env, ${r.exportName}, "${r.exportName}");
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
`;return`${o}${l}`}).join(`
|
|
1114
|
+
`),a=e.map(r=>r.exportName).join(", ");return`${k}/**
|
|
1115
|
+
* WorkflowEntrypoint classes for the agents declared in \`lunora/agents.ts\`.
|
|
1116
|
+
* Each \`defineAgent\` compiles a replay-safe tool-loop onto a Cloudflare
|
|
1117
|
+
* Workflow. Re-export them from your worker entry — wrangler requires each
|
|
1118
|
+
* \`workflows[].class_name\` to be exported by the worker:
|
|
1119
|
+
*
|
|
1120
|
+
* \`export * from "./lunora/_generated/agents.js";\`${t?"\n *\n * Voice-enabled agents ALSO get a `VoiceSessionDO` subclass here — a real\n * Durable Object (unlike the Workflow), so wrangler needs both its\n * `durable_objects` binding and its `class_name` export (the same re-export\n * line covers it).":""}
|
|
1121
|
+
*/
|
|
1122
|
+
import LunoraWorkflow from "@lunora/workflow/do";
|
|
1123
|
+
${t?'import { compileAgentWorkflow, VoiceSessionDO } from "@lunora/agent";':'import { compileAgentWorkflow } from "@lunora/agent";'}
|
|
1124
|
+
import type { AgentRunInput, AgentRunResult } from "@lunora/agent";
|
|
1125
|
+
|
|
1126
|
+
import { ${a} } from "../agents.js";
|
|
1127
|
+
|
|
1128
|
+
${n}`},dr=e=>{const t=e.filter(r=>r.mode==="push");if(t.length===0)return"";for(const r of t)m(r.exportName,`queue export "${r.exportName}"`);const n=t.map(r=>r.exportName).join(", "),a=t.map(r=>` ${JSON.stringify(r.name)}: { definition: ${r.exportName}, exportName: ${JSON.stringify(r.exportName)} },`).join(`
|
|
1129
|
+
`);return`${k}/**
|
|
1130
|
+
* Push-consumer registry for the queues declared in \`lunora/queues.ts\`. The
|
|
1131
|
+
* composed worker's \`queue(batch, env, ctx)\` entry routes each delivered batch
|
|
1132
|
+
* by \`batch.queue\` to the matching \`defineQueue\` handler. Wired automatically
|
|
1133
|
+
* by \`defineApp\` — you don't import this directly.
|
|
1134
|
+
*/
|
|
1135
|
+
import type { QueueRegistry } from "@lunora/queue";
|
|
1136
|
+
|
|
1137
|
+
import { ${n} } from "../queues.js";
|
|
1138
|
+
|
|
1139
|
+
/** Stable wrangler queue name → { definition, exportName } for batch routing. */
|
|
1140
|
+
export const LUNORA_QUEUE_REGISTRY: QueueRegistry = {
|
|
1141
|
+
${a}
|
|
1142
|
+
};
|
|
1143
|
+
`},Mn=e=>{if(e.length===0)return{build:"",contextField:"",importLines:[],specs:""};for(const n of e)m(n.exportName,`workflow export "${n.exportName}"`),m(n.bindingName,`workflow binding "${n.bindingName}"`);const t=e.map(n=>` { binding: "${n.bindingName}", exportName: "${n.exportName}" },`).join(`
|
|
1144
|
+
`);return{build:`
|
|
1145
|
+
const workflows = createWorkflowContext(env, LUNORA_WORKFLOWS);
|
|
1146
|
+
`,contextField:`
|
|
1147
|
+
workflows,`,importLines:['import type { WorkflowBindingSpec } from "@lunora/workflow";','import { createWorkflowContext } from "@lunora/workflow";'],specs:`
|
|
1148
|
+
/** Wiring specs for \`ctx.workflows\` (codegen-derived from \`lunora/workflows.ts\`). */
|
|
1149
|
+
const LUNORA_WORKFLOWS: ReadonlyArray<WorkflowBindingSpec> = [
|
|
1150
|
+
${t}
|
|
1151
|
+
];
|
|
1152
|
+
`}},qn=e=>{if(e.length===0)return{build:"",contextField:"",importLines:[],specs:""};for(const n of e)m(n.exportName,`queue export "${n.exportName}"`),m(n.bindingName,`queue binding "${n.bindingName}"`);const t=e.map(n=>` { binding: "${n.bindingName}", exportName: "${n.exportName}", name: ${JSON.stringify(n.name)} },`).join(`
|
|
1153
|
+
`);return{build:`
|
|
1154
|
+
const queues = createQueueContext(env, LUNORA_QUEUES);
|
|
1155
|
+
`,contextField:`
|
|
1156
|
+
queues,`,importLines:['import type { QueueBindingSpec } from "@lunora/queue";','import { createQueueContext } from "@lunora/queue";'],specs:`
|
|
1157
|
+
/** Wiring specs for \`ctx.queues\` (codegen-derived from \`lunora/queues.ts\`). */
|
|
1158
|
+
const LUNORA_QUEUES: ReadonlyArray<QueueBindingSpec> = [
|
|
1159
|
+
${t}
|
|
1160
|
+
];
|
|
1161
|
+
`}},Bn=e=>{if(e.length===0)return{build:"",contextField:"",importLines:[],specs:""};for(const n of e)m(n.exportName,`agent export "${n.exportName}"`),m(n.bindingName,`agent binding "${n.bindingName}"`);const t=e.map(n=>` { binding: "${n.bindingName}", exportName: "${n.exportName}"${n.publicRun===!0?", publicRun: true":""} },`).join(`
|
|
1162
|
+
`);return{build:`
|
|
1163
|
+
const agents = createAgentContext(env, LUNORA_AGENTS);
|
|
1164
|
+
`,contextField:`
|
|
1165
|
+
agents,`,importLines:['import { createAgentContext } from "@lunora/agent";','import type { AgentBindingSpec } from "@lunora/agent";'],specs:`
|
|
1166
|
+
/** Wiring specs for \`ctx.agents\` (codegen-derived from \`lunora/agents.ts\`). */
|
|
1167
|
+
const LUNORA_AGENTS: ReadonlyArray<AgentBindingSpec> = [
|
|
1168
|
+
${t}
|
|
1169
|
+
];
|
|
1170
|
+
`}},jn=e=>{if(e.length===0)return{constant:"",override:""};const t={workflows:e.map(n=>({binding:n.bindingName,className:n.className,exportName:n.exportName,name:n.name}))};return{constant:`
|
|
1171
|
+
/** Read-only declared-workflow metadata (discovered from \`lunora/workflows.ts\`) served via \`__lunora_admin__:listWorkflows\` for the studio's workflows view. */
|
|
1172
|
+
const LUNORA_WORKFLOWS_INFO: WorkflowsResult = ${JSON.stringify(t,void 0,4)};
|
|
1173
|
+
`,override:`
|
|
1174
|
+
protected override workflowsMetadata(): WorkflowsResult {
|
|
1175
|
+
return LUNORA_WORKFLOWS_INFO;
|
|
1176
|
+
}
|
|
1177
|
+
`}},Wn=e=>{if(e.length===0)return{constant:"",override:""};const t={queues:e.map(n=>({binding:n.bindingName,...n.tuning.deadLetterQueue===void 0?{}:{deadLetterQueue:n.tuning.deadLetterQueue},exportName:n.exportName,mode:n.mode,name:n.name}))};return{constant:`
|
|
1178
|
+
/** Read-only declared-queue metadata (discovered from \`lunora/queues.ts\`) served via \`__lunora_admin__:listQueues\` for the studio's queues view. */
|
|
1179
|
+
const LUNORA_QUEUES_INFO: QueuesResult = ${JSON.stringify(t,void 0,4)};
|
|
1180
|
+
`,override:`
|
|
1181
|
+
protected override queuesMetadata(): QueuesResult {
|
|
1182
|
+
return LUNORA_QUEUES_INFO;
|
|
1183
|
+
}
|
|
1184
|
+
`}},Un=e=>{if(!e)return{build:"",configField:"",contextField:"",imports:[],stub:""};const t='throw new Error("ctx.payments: no payment configured. Pass \\`payment\\` to createShardDO().");';return{imports:['import type { LunoraDatabaseLike as LunoraPaymentDbLike, LunoraPayment, PaymentsFromContextOptions } from "@lunora/payment";','import { paymentsFromContext } from "@lunora/payment";'],build:`
|
|
1185
|
+
const payments: LunoraPayment = config.payment
|
|
1186
|
+
? paymentsFromContext({ auth: { userId: userId ?? null }, db: db as unknown as LunoraPaymentDbLike }, config.payment(env))
|
|
1187
|
+
: paymentStub;
|
|
1188
|
+
`,configField:`
|
|
1189
|
+
payment?: (env: Record<string, unknown>) => PaymentsFromContextOptions;`,contextField:`
|
|
1190
|
+
payments,`,stub:`
|
|
1191
|
+
const paymentStub: LunoraPayment = {
|
|
1192
|
+
attach: () => {
|
|
1193
|
+
${t}
|
|
1194
|
+
},
|
|
1195
|
+
cancelSubscription: () => {
|
|
1196
|
+
${t}
|
|
1197
|
+
},
|
|
1198
|
+
check: () => {
|
|
1199
|
+
${t}
|
|
1200
|
+
},
|
|
1201
|
+
createCheckout: () => {
|
|
1202
|
+
${t}
|
|
1203
|
+
},
|
|
1204
|
+
createPortalSession: () => {
|
|
1205
|
+
${t}
|
|
1206
|
+
},
|
|
1207
|
+
handleWebhook: () => {
|
|
1208
|
+
${t}
|
|
1209
|
+
},
|
|
1210
|
+
listBalances: () => {
|
|
1211
|
+
${t}
|
|
1212
|
+
},
|
|
1213
|
+
listSubscriptions: () => {
|
|
1214
|
+
${t}
|
|
1215
|
+
},
|
|
1216
|
+
track: () => {
|
|
1217
|
+
${t}
|
|
1218
|
+
},
|
|
1219
|
+
} as unknown as LunoraPayment;
|
|
1220
|
+
`}},Kn=e=>e?{imports:['import type { X402Pay, X402PayConfig } from "@lunora/x402/pay";','import { lazyX402Pay } from "@lunora/x402/pay";'],build:`
|
|
1221
|
+
const x402: X402Pay = config.x402
|
|
1222
|
+
? lazyX402Pay(config.x402(env), { getSecret: (name: string) => secrets.get(name) })
|
|
1223
|
+
: x402Stub;
|
|
1224
|
+
`,configField:`
|
|
1225
|
+
x402?: (env: Record<string, unknown>) => X402PayConfig;`,contextField:`
|
|
1226
|
+
x402,`,stub:`
|
|
1227
|
+
const x402Stub: X402Pay = {
|
|
1228
|
+
fetch: () => {
|
|
1229
|
+
throw new Error("ctx.x402: no pay rail configured. Pass \\\`x402\\\` to createShardDO().");
|
|
1230
|
+
},
|
|
1231
|
+
} as unknown as X402Pay;
|
|
1232
|
+
`}:{build:"",configField:"",contextField:"",imports:[],stub:""},Qn=(e,t,n,a)=>["AdvisoryFinding","DatabaseWriterLike","DataMigrationLike",...a?["FlagsResult"]:[],"LogSink","MaskPoliciesResult","MigrationRunResult",...n?["QueuesResult"]:[],"RunShardApplyCdcArgs","RunShardMigrationArgs","RlsPoliciesResult","RunShardRankBeforeArgs","RunShardRankPageArgs","RunShardWriteArgs","RunShardWriteResult","SchedulerLike","SchemaLike","ShardDOState","ShardRankPageResult","SqlExec","StorageRulesResult","StudioFeaturesResult","SystemReaderStorageLike",...t?["WorkflowsResult"]:[],...e?["WriteHook"]:[]],cr=({advisories:e=[],agents:t=[],containers:n=[],env:a,flagKeys:r=[],hasAccessFacade:o=!1,hasAi:s=!1,hasAnalytics:i=!1,hasBrowser:l=!1,hasFlags:u=!1,hasHyperdrive:p=!1,hasImages:b=!1,hasKv:w=!1,hasNotify:v=!1,hasPayments:d=!1,hasPipelines:c=!1,hasR2sql:g=!1,hasX402:f=!1,maskMetadata:T,mutators:x=[],queues:A=[],rlsMetadata:_,schema:y,shapes:Y=[],storageRules:Z,studioFeatures:ee,useUmbrella:I=!1,workflows:j=[]})=>{const S=V(I),W=x.length>0,L=Y.length>0,{build:te,configField:H,contextField:C,stub:P}=Sn(s),U=An(o),E=Rn(w),K=Nn(u,S.flags),ne=Ln(r,u,S.flags),re=On(v),ae=Tn(a),Q=Cn(i),M=Dn(b),q=_n(p),B=In(l),F=Pn(g),z=En(c),{build:ye,contextField:ge,importLines:me,specs:oe}=qn(A),{build:we,contextField:xe,importLines:fe,specs:ie}=Fn(n,y.jurisdiction),{build:ke,contextField:ve,importLines:be,specs:$e}=Mn(j),{build:Se,contextField:se,importLines:le,specs:Re}=Bn(t),{build:Ne,configField:Oe,contextField:de,imports:ce,stub:h}=Un(d),{build:Te,configField:et,imports:tt,stub:nt}=Kn(f),rt=e,at=_??{policies:[],roles:[]},ot=T??{columns:[]},it=Z??{rules:[]},st=ee??{analytics:!1,auth:!1,containers:!1,flags:!1,kv:!1,mail:!1,payments:!1,queues:!1,scheduler:!1,storage:!1,vectors:!1,workflows:!1},{constant:lt,override:dt}=jn(j),{constant:ct,override:ut}=Wn(A),G=y.vectorIndexes.length>0,ue=y.tables.some(R=>R.shardMode==="global"),Ae=y.tables.some(R=>R.shardMode==="global"&&R.globalBackend==="hyperdrive"),Ee=y.tables.some(R=>R.shardMode==="global"&&R.globalBackend!=="hyperdrive"),J=y.tables.some(R=>R.externalSource!==void 0);if(Ee&&Ae)throw new X("INTERNAL",'lunora codegen: mixing `.global()` (D1) and `.global({ backend: "hyperdrive" })` tables in one app is not supported yet — use a single global backend.');const Le=y.tables.length>0,pt=yn(y),ht=xn(y),gt=vn(y),mt=wn(y),Fe=kn(y),Me=Qn(G,j.length>0,A.length>0,u);L&&Me.push("WhereInput");const qe=$n(ue),ft=L?`
|
|
1233
|
+
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 {
|
|
1234
|
+
const shape = LUNORA_SHAPES[name];
|
|
1235
|
+
|
|
1236
|
+
if (!shape) {
|
|
1237
|
+
return undefined;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
this.ensureMigrated();
|
|
1241
|
+
|
|
1242
|
+
// Trusted ctx from the socket's OWN verified identity — the client
|
|
1243
|
+
// supplies only the shape name + args; \`compileWhere\` validates the
|
|
1244
|
+
// args then runs the shape's \`where(ctx, args)\` under that identity,
|
|
1245
|
+
// so which rows replicate is a server decision (reads-as-permissions).
|
|
1246
|
+
const ctx = this.buildCtx({ functionPath: \`__shape__:\${name}\`, identity });
|
|
1247
|
+
// \`ownerField\` resolves an \`owner: true\` shape: only here is the table's
|
|
1248
|
+
// \`.ownedBy(field)\` column in scope, so the DO hands it to the shape
|
|
1249
|
+
// rather than every shape restating the ownership check in its \`where\`.
|
|
1250
|
+
const shapeWhere = shape.compileWhere(ctx, args, {
|
|
1251
|
+
ownerField: (schema as unknown as { tables: Record<string, { ownerField?: string }> }).tables[shape.table]?.ownerField,
|
|
1252
|
+
}) as unknown as WhereInput;
|
|
1253
|
+
|
|
1254
|
+
// AND-compose with the table's RLS read base-where. A shape runs no
|
|
1255
|
+
// procedure, so the \`.use(rls(...))\` middleware never fires; without
|
|
1256
|
+
// this merge its reads would bypass every read policy on the table
|
|
1257
|
+
// (rows the caller can't see would replicate). \`composeShapeReadWhere\`
|
|
1258
|
+
// evaluates the table's read policies under this same trusted ctx —
|
|
1259
|
+
// exactly the request-time path — and fails closed under a
|
|
1260
|
+
// \`.rls("required")\` schema for a non-\`.public()\`, policy-less table.
|
|
1261
|
+
const effectiveWhere = composeShapeReadWhere(LUNORA_RLS_READ_REGISTRY, {
|
|
1262
|
+
ctx,
|
|
1263
|
+
identity: identity?.identity ?? null,
|
|
1264
|
+
rlsRequired: (schema as unknown as { rlsMode?: string }).rlsMode === "required",
|
|
1265
|
+
roles: (ctx as { auth?: { roles?: readonly string[] } }).auth?.roles ?? [],
|
|
1266
|
+
shapeWhere,
|
|
1267
|
+
table: shape.table,
|
|
1268
|
+
tablePublic: (schema as unknown as { tables: Record<string, { isPublic?: boolean }> }).tables[shape.table]?.isPublic === true,
|
|
1269
|
+
userId: identity?.userId ?? null,
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
// A live shape pokes only from its OWN shard's op-log, so a \`where()\`
|
|
1273
|
+
// that joins to a \`.shardBy()\` table (rows in another DO) is rejected
|
|
1274
|
+
// here — the first point the compiled predicate + shard modes are both
|
|
1275
|
+
// known. Remedy: denormalize, or move the joined table to \`.global()\`.
|
|
1276
|
+
assertShapeShardable(effectiveWhere, schema as unknown as SchemaLike, shape.table);
|
|
1277
|
+
|
|
1278
|
+
// A \`.global()\` table lives in D1 (no per-DO op-log): flag it so the
|
|
1279
|
+
// base serves it through the latency-tiered poll path (\`readGlobalShapeRows\`)
|
|
1280
|
+
// instead of the CDC poke path.
|
|
1281
|
+
const isGlobal = (schema as unknown as SchemaLike).tables[shape.table]?.shardMode?.kind === "global";
|
|
1282
|
+
|
|
1283
|
+
return { columns: shape.columns, effectiveWhere, global: isGlobal, table: shape.table };
|
|
1284
|
+
}
|
|
1285
|
+
`:"",bt=W?`
|
|
1286
|
+
protected override isCustomMutator(functionPath: string): boolean {
|
|
1287
|
+
return LUNORA_MUTATOR_PATHS.has(functionPath);
|
|
1288
|
+
}
|
|
1289
|
+
`:"",yt=L?"\n/** 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. */\nconst LUNORA_RLS_READ_REGISTRY = buildRlsReadRegistry(Object.values(LUNORA_FUNCTIONS));\n":"",wt=L?"assertShapeShardable, ":"",pe=[`import type { ${Me.join(", ")} } from "${S.do}";`,`import { applyCdcChanges, ${wt}createShardCtxDb, ${J?"isSourceDue, pullExternalSourceIncrementalTick, pullExternalSourceTick, ":""}runDataMigration, runShardMigrations, ${qe.importFragment}ShardDO as ShardDOBase } from "${S.do}";`,...J?[`import type { ExternalSourceLike, SourceClientLike } from "${S.do}";`]:[],L?`import { asBucketStorage, buildRlsReadRegistry, composeShapeReadWhere, createSecrets, LunoraError } from "${S.server}";`:`import { asBucketStorage, createSecrets, LunoraError } from "${S.server}";`];Le&&pe.push(`import { bindOrm, bindTableFacade } from "${S.server}";`),G&&pe.push('import type { SchemaLike as VectorSchemaLike, VectorizeIndexLike, VectorSearchLike } from "@lunora/bindings/vectors";','import { createContextVectors, createVectors, createVectorSyncHook } from "@lunora/bindings/vectors";'),s&&pe.push('import type { AiBindingLike, LunoraAi } from "@lunora/ai";','import { createAi } from "@lunora/ai";'),pe.push(...U.importLines,...E.importLines,...K.importLines,...re.importLines,...ae.importLines,...Q.importLines,...M.importLines,...q.importLines,...B.importLines,...F.importLines,...z.importLines,...fe,...be,...me,...le,...ce,...tt,"",'import schema from "../schema.js";',`import { ${["LUNORA_FUNCTIONS","LUNORA_LIFECYCLE_HOOKS","LUNORA_MIGRATIONS",...W?["LUNORA_MUTATOR_PATHS"]:[],...L?["LUNORA_SHAPES"]:[]].join(", ")} } from "./functions.js";`);const xt=G?`
|
|
1290
|
+
vectors?: (env: Record<string, unknown>) => Record<string, VectorizeIndexLike>;`:"",kt=Ee?`
|
|
1291
|
+
d1?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;`:"",vt=Ae?`
|
|
1292
|
+
hyperdriveGlobal?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;`:"",$t=J?`
|
|
1293
|
+
sourceClient?: (env: Record<string, unknown>, binding: string) => { query: <Row = Record<string, unknown>>(text: string, params?: readonly unknown[]) => Promise<Row[]> } | undefined;`:"",St=ue?'\nconst globalDbStub: DatabaseWriterLike = {\n aggregate: async () => {\n throw new Error("ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().");\n },\n count: async () => {\n throw new Error("ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().");\n },\n delete: async () => {\n throw new Error("ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().");\n },\n findFirst: async () => {\n throw new Error("ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().");\n },\n findFirstOrThrow: async () => {\n throw new Error("ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().");\n },\n findMany: async () => {\n throw new Error("ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().");\n },\n get: async () => {\n throw new Error("ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().");\n },\n groupBy: async () => {\n throw new Error("ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().");\n },\n insert: async () => {\n throw new Error("ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().");\n },\n normalizeId: () => {\n throw new Error("ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().");\n },\n patch: async () => {\n throw new Error("ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().");\n },\n query: () => {\n throw new Error("ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().");\n },\n rank: async () => {\n throw new Error("ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().");\n },\n rankPage: async () => {\n throw new Error("ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().");\n },\n replace: async () => {\n throw new Error("ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().");\n },\n};\n':"",Rt=G?`
|
|
1294
|
+
const vectorsStub: VectorSearchLike = {
|
|
1295
|
+
deleteByIds: async () => {
|
|
1296
|
+
throw new Error("ctx.vectors: no vectors configured. Pass \`vectors\` to createShardDO().");
|
|
1297
|
+
},
|
|
1298
|
+
getByIds: async () => {
|
|
1299
|
+
throw new Error("ctx.vectors: no vectors configured. Pass \`vectors\` to createShardDO().");
|
|
1300
|
+
},
|
|
1301
|
+
query: async () => {
|
|
1302
|
+
throw new Error("ctx.vectors: no vectors configured. Pass \`vectors\` to createShardDO().");
|
|
1303
|
+
},
|
|
1304
|
+
upsert: async () => {
|
|
1305
|
+
throw new Error("ctx.vectors: no vectors configured. Pass \`vectors\` to createShardDO().");
|
|
1306
|
+
},
|
|
1307
|
+
upsertNow: async () => {
|
|
1308
|
+
throw new Error("ctx.vectors: no vectors configured. Pass \`vectors\` to createShardDO().");
|
|
1309
|
+
},
|
|
1310
|
+
};
|
|
1311
|
+
`:"",Nt=G?`
|
|
1312
|
+
let vectors: VectorSearchLike;
|
|
1313
|
+
let onWrite: WriteHook | undefined;
|
|
1314
|
+
|
|
1315
|
+
if (config.vectors) {
|
|
1316
|
+
const lunora = createVectors({ indexes: config.vectors(env) });
|
|
1317
|
+
|
|
1318
|
+
vectors = createContextVectors(lunora);
|
|
1319
|
+
onWrite = createVectorSyncHook({ schema: schema as unknown as VectorSchemaLike, vectors });
|
|
1320
|
+
} else {
|
|
1321
|
+
vectors = vectorsStub;
|
|
1322
|
+
onWrite = undefined;
|
|
1323
|
+
}
|
|
1324
|
+
`:"",Be=ue?`
|
|
1325
|
+
globalDb,`:"",je=`
|
|
1326
|
+
auth: { identity: identity ?? null, userId: userId ?? null },`,Ot=G?`{${je}
|
|
1327
|
+
broadcast: (delta) => {
|
|
1328
|
+
this.recordChangedTable(delta.table);
|
|
1329
|
+
},
|
|
1330
|
+
cdc: config.cdc ?? false,
|
|
1331
|
+
enforceRls: true,
|
|
1332
|
+
onIndexUse: this.getCtxDbIndexUseHook(),
|
|
1333
|
+
onRead: options.onRead ?? this.getCtxDbReadHook(),
|
|
1334
|
+
onWrite,
|
|
1335
|
+
scheduler,
|
|
1336
|
+
schema: schema as unknown as SchemaLike,
|
|
1337
|
+
sql: this.sql as SqlExec,
|
|
1338
|
+
storage,${Be}
|
|
1339
|
+
}`:`{${je}
|
|
1340
|
+
broadcast: (delta) => {
|
|
1341
|
+
this.recordChangedTable(delta.table);
|
|
1342
|
+
},
|
|
1343
|
+
cdc: config.cdc ?? false,
|
|
1344
|
+
enforceRls: true,
|
|
1345
|
+
onIndexUse: this.getCtxDbIndexUseHook(),
|
|
1346
|
+
onRead: options.onRead ?? this.getCtxDbReadHook(),
|
|
1347
|
+
scheduler,
|
|
1348
|
+
schema: schema as unknown as SchemaLike,
|
|
1349
|
+
sql: this.sql as SqlExec,
|
|
1350
|
+
storage,${Be}
|
|
1351
|
+
}`,Tt=G?`
|
|
1352
|
+
vectors,`:"",At=Le?`
|
|
1353
|
+
orm: bindOrm(facade),`:"",Lt="",We=Ae?"config.hyperdriveGlobal":"config.d1",Ct=ue?` const globalDb: DatabaseWriterLike = ${We}?.(env, { identity, userId }) ?? globalDbStub;
|
|
1354
|
+
`:"",Dt=L&&ue?`
|
|
1355
|
+
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 }>> {
|
|
1356
|
+
const env = this.env as Record<string, unknown>;
|
|
1357
|
+
const globalDb: DatabaseWriterLike = ${We}?.(env, identity) ?? globalDbStub;
|
|
1358
|
+
const rows: Array<{ doc: Record<string, unknown>; id: string }> = [];
|
|
1359
|
+
|
|
1360
|
+
let cursor: null | string = null;
|
|
1361
|
+
|
|
1362
|
+
// Drain every page of the global membership so the seed/diff sees the
|
|
1363
|
+
// full rowset (D1 \`findMany\` is paginated). Stop one row past the cap:
|
|
1364
|
+
// a broad \`.global()\` shape would otherwise materialize an unbounded
|
|
1365
|
+
// array before the caller's \`withinGlobalShapeBound\` check rejects it,
|
|
1366
|
+
// so bail early and let the caller fail it closed.
|
|
1367
|
+
do {
|
|
1368
|
+
// eslint-disable-next-line no-await-in-loop -- sequential page drain to assemble the full membership
|
|
1369
|
+
const page = await globalDb.findMany(resolved.table, { cursor, where: resolved.effectiveWhere });
|
|
1370
|
+
|
|
1371
|
+
for (const doc of page.page) {
|
|
1372
|
+
rows.push({ doc, id: String((doc as { _id?: unknown })._id) });
|
|
1373
|
+
|
|
1374
|
+
if (rows.length > ShardDOBase.GLOBAL_SHAPE_MAX_ROWS) {
|
|
1375
|
+
return rows;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
cursor = page.isDone ? null : page.continueCursor;
|
|
1380
|
+
} while (cursor !== null);
|
|
1381
|
+
|
|
1382
|
+
return rows;
|
|
1383
|
+
}
|
|
1384
|
+
`:"",_t=J?`
|
|
1385
|
+
const sourceClientCache = new WeakMap<object, Map<string, SourceClientLike>>();
|
|
1386
|
+
const sourcePollAtCache = new WeakMap<object, Map<string, number>>();
|
|
1387
|
+
`:"",It=J?`
|
|
1388
|
+
protected override async pollExternalSources(): Promise<number | undefined> {
|
|
1389
|
+
const env = (this.env ?? {}) as Record<string, unknown>;
|
|
1390
|
+
const sourced = Object.entries((schema as unknown as SchemaLike).tables)
|
|
1391
|
+
.map(([table, definition]) => [table, (definition as { externalSource?: ExternalSourceLike }).externalSource] as const)
|
|
1392
|
+
.filter((entry): entry is [string, ExternalSourceLike] => entry[1] !== undefined);
|
|
1393
|
+
|
|
1394
|
+
if (sourced.length === 0) {
|
|
1395
|
+
return undefined;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
const shardKey = this.currentShardKey();
|
|
1399
|
+
const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
|
|
1400
|
+
const writer = createShardCtxDb({
|
|
1401
|
+
broadcast: (delta) => {
|
|
1402
|
+
this.recordChangedTable(delta.table);
|
|
1403
|
+
},
|
|
1404
|
+
cdc: config.cdc ?? false,
|
|
1405
|
+
scheduler,
|
|
1406
|
+
schema: schema as unknown as SchemaLike,
|
|
1407
|
+
sql: this.sql as SqlExec,
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
let clients = sourceClientCache.get(this);
|
|
1411
|
+
|
|
1412
|
+
if (clients === undefined) {
|
|
1413
|
+
clients = new Map();
|
|
1414
|
+
sourceClientCache.set(this, clients);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
let polledAt = sourcePollAtCache.get(this);
|
|
1418
|
+
|
|
1419
|
+
if (polledAt === undefined) {
|
|
1420
|
+
polledAt = new Map();
|
|
1421
|
+
sourcePollAtCache.set(this, polledAt);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const now = Date.now();
|
|
1425
|
+
// \`nextDueAt\` tracks the EARLIEST next-due timestamp across every
|
|
1426
|
+
// non-manual source; the shared alarm re-arms there instead of the
|
|
1427
|
+
// fixed 2 s global-shape floor, so a large \`refresh.everyMs\` actually
|
|
1428
|
+
// sleeps until it's due. Stays \`undefined\` when every source is
|
|
1429
|
+
// \`refresh: "manual"\`, so the shared alarm goes idle for this tier.
|
|
1430
|
+
let nextDueAt: number | undefined;
|
|
1431
|
+
|
|
1432
|
+
for (const [table, source] of sourced) {
|
|
1433
|
+
if (source.refresh === "manual") {
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
if (isSourceDue(source.refresh, polledAt.get(table), now)) {
|
|
1438
|
+
try {
|
|
1439
|
+
let client = clients.get(source.binding);
|
|
1440
|
+
|
|
1441
|
+
if (client === undefined) {
|
|
1442
|
+
client = config.sourceClient?.(env, source.binding);
|
|
1443
|
+
|
|
1444
|
+
if (client !== undefined) {
|
|
1445
|
+
clients.set(source.binding, client);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
if (client === undefined) {
|
|
1450
|
+
// No SqlClient resolved for this binding (host never wired
|
|
1451
|
+
// \`config.sourceClient\`, or wired it wrong). Surface it in the
|
|
1452
|
+
// Logs panel and stamp \`polledAt\` so a persistent misconfig backs
|
|
1453
|
+
// off to \`refresh.everyMs\` instead of retrying every alarm tick.
|
|
1454
|
+
this.recordExternalSourceError(table, new Error(\`external-source: no sourceClient resolved for binding "\${source.binding}"\`));
|
|
1455
|
+
} else if (source.mode === "incremental") {
|
|
1456
|
+
// Incremental (plan 136): pull only rows past the durable
|
|
1457
|
+
// watermark (or a full-pull seed/reconcile), upsert-only.
|
|
1458
|
+
// eslint-disable-next-line no-await-in-loop -- one sourced table at a time; sequential keeps the writer transaction simple
|
|
1459
|
+
await pullExternalSourceIncrementalTick(this.sql as SqlExec, writer, client, table, source, shardKey, now);
|
|
1460
|
+
} else {
|
|
1461
|
+
// 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
|
|
1462
|
+
await pullExternalSourceTick(this.sql as SqlExec, writer, client, table, source, shardKey);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// Timestamp AFTER the poll finishes, not the batch-start \`now\` — a
|
|
1466
|
+
// poll that outruns \`everyMs\` must not make \`nextDueAt\` (below)
|
|
1467
|
+
// stale-immediate and re-arm the alarm in a hammering loop.
|
|
1468
|
+
polledAt.set(table, Date.now());
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
this.recordExternalSourceError(table, error);
|
|
1471
|
+
// Stamp on failure too, so a persistently failing source throttles
|
|
1472
|
+
// to \`refresh.everyMs\` rather than being hammered every tick.
|
|
1473
|
+
polledAt.set(table, Date.now());
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// This source's own next-due time, read AFTER the poll-or-skip above
|
|
1478
|
+
// so a just-polled source reports \`now + everyMs\` (its FRESH due
|
|
1479
|
+
// time), not the stale pre-poll one. An omitted \`refresh\` (poll
|
|
1480
|
+
// every tick) is due again immediately.
|
|
1481
|
+
const sourceNextDueAt = source.refresh === undefined ? now : (polledAt.get(table) ?? now) + source.refresh.everyMs;
|
|
1482
|
+
|
|
1483
|
+
nextDueAt = nextDueAt === undefined ? sourceNextDueAt : Math.min(nextDueAt, sourceNextDueAt);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
return nextDueAt;
|
|
1487
|
+
}
|
|
1488
|
+
`:"",Ue=Fe.length>0,Pt=J||Ue?`
|
|
1489
|
+
public constructor(state: ShardDOState, env: unknown) {
|
|
1490
|
+
super(state, env);
|
|
1491
|
+
${J?`
|
|
1492
|
+
const autoSourced = Object.values((schema as unknown as SchemaLike).tables).some((definition) => {
|
|
1493
|
+
const source = (definition as { externalSource?: ExternalSourceLike }).externalSource;
|
|
1494
|
+
|
|
1495
|
+
return source !== undefined && source.refresh !== "manual";
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
if (autoSourced) {
|
|
1499
|
+
void this.scheduleSourcePoll();
|
|
1500
|
+
}
|
|
1501
|
+
`:""}${Ue?`
|
|
1502
|
+
if (LUNORA_TTL_SWEEPS.length > 0) {
|
|
1503
|
+
void this.scheduleTtlSweep();
|
|
1504
|
+
}
|
|
1505
|
+
`:""} }
|
|
1506
|
+
`:"",Et=Le?`
|
|
1507
|
+
const facade = db as unknown as Record<string, ReturnType<typeof bindTableFacade>>;
|
|
1508
|
+
${y.tables.map(R=>` facade[${JSON.stringify(R.name)}] = bindTableFacade(db, ${JSON.stringify(R.name)});`).join(`
|
|
1509
|
+
`)}
|
|
1510
|
+
`:"",Ft=`${U.build}${E.build}${K.build}${Q.build}${ae.build}
|
|
1511
|
+
const secrets = createSecrets(env);
|
|
1512
|
+
`,Mt=`${U.contextField}${E.contextField}${K.contextField}${re.contextField}${Q.contextField}${ae.contextField}
|
|
1513
|
+
secrets,`,qt=re.build,Ke=b||p||l||g||c||f,Bt=Ke?`
|
|
1514
|
+
// ActionCtx-only helpers (external, non-deterministic I/O): constructed
|
|
1515
|
+
// and attached only for an \`action\` so query/mutation ctx never carry them.
|
|
1516
|
+
if (isAction) {
|
|
1517
|
+
${M.build}${q.build}${B.build}${F.build}${z.build}${Te}${[...b?[" ctx.images = images;"]:[],...p?[" ctx.sql = sql;"]:[],...l?[" ctx.browser = browser;"]:[],...g?[" ctx.r2sql = r2sql;"]:[],...c?[" ctx.pipelines = pipelines;"]:[],...f?[" ctx.x402 = x402;"]:[]].join(`
|
|
1518
|
+
`)}
|
|
1519
|
+
}
|
|
1520
|
+
`:"",jt=Ke?` const isAction = LUNORA_FUNCTIONS[options.functionPath ?? ""]?.kind === "action";
|
|
1521
|
+
`:"";return`${k}${pe.join(`
|
|
1522
|
+
`)}
|
|
1523
|
+
|
|
1524
|
+
type FunctionKind = "action" | "mutation" | "query";
|
|
1525
|
+
|
|
1526
|
+
interface FunctionReference {
|
|
1527
|
+
__lunoraRef: string;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/** Foreign-key columns per table (\`v.id("target")\` fields) for the data browser. */
|
|
1531
|
+
const LUNORA_TABLE_REFS: Record<string, Record<string, string>> = ${JSON.stringify(pt,void 0,4)};
|
|
1532
|
+
|
|
1533
|
+
/** Declared indexes per table (secondary, search, geo, rank, vector) for the schema viewer. */
|
|
1534
|
+
const LUNORA_TABLE_INDEXES: Record<string, Array<{ fields: string[]; name: string; type: "geo" | "index" | "rank" | "search" | "vector"; unique?: boolean }>> = ${JSON.stringify(ht,void 0,4)};
|
|
1535
|
+
|
|
1536
|
+
/** Columns per table (typed, with PK/FK markers) for the studio's schema diagram, served via \`__lunora_admin__:describeTable\`. */
|
|
1537
|
+
const LUNORA_TABLE_COLUMNS: Record<string, Array<{ isStorage?: boolean; name: string; optional: boolean; pk?: boolean; ref?: string; type: string }>> = ${JSON.stringify(gt,void 0,4)};
|
|
1538
|
+
|
|
1539
|
+
/** Storage-key columns per table (\`v.storage(...)\` fields) for the file browser's records↔files join. */
|
|
1540
|
+
const LUNORA_STORAGE_COLUMNS: Record<string, string[]> = ${JSON.stringify(mt,void 0,4)};
|
|
1541
|
+
|
|
1542
|
+
/** Declarative TTL policies (\`.ttl(field, { after? })\`) the DO alarm sweep auto-expires rows for. */
|
|
1543
|
+
const LUNORA_TTL_SWEEPS: Array<{ after?: number; field: string; softDeleteField?: string; table: string }> = ${JSON.stringify(Fe,void 0,4)};
|
|
1544
|
+
|
|
1545
|
+
/** Static schema advisories (computed by @lunora/advisor at codegen time) served via \`__lunora_admin__:getAdvisories\`. */
|
|
1546
|
+
const LUNORA_ADVISORIES: AdvisoryFinding[] = ${JSON.stringify(rt,void 0,4)};
|
|
1547
|
+
|
|
1548
|
+
/** Read-only RLS metadata (policies + roles discovered from \`.use(rls(...))\` chains) served via \`__lunora_admin__:rlsPolicies\` for the studio's RLS inspector. */
|
|
1549
|
+
const LUNORA_RLS_METADATA: RlsPoliciesResult = ${JSON.stringify(at,void 0,4)};
|
|
1550
|
+
|
|
1551
|
+
/** Read-only masking metadata (table + column + strategy discovered from \`.use(mask(...))\` chains) served via \`__lunora_admin__:maskPolicies\` for the studio's data-browser mask preview. */
|
|
1552
|
+
const LUNORA_MASK_METADATA: MaskPoliciesResult = ${JSON.stringify(ot,void 0,4)};
|
|
1553
|
+
|
|
1554
|
+
/** Read-only storage access-rule metadata (discovered from \`.use(storageRules(...))\` chains) served via \`__lunora_admin__:storageRules\` for the studio's access-rules view. */
|
|
1555
|
+
const LUNORA_STORAGE_RULES: StorageRulesResult = ${JSON.stringify(it,void 0,4)};
|
|
1556
|
+
|
|
1557
|
+
/** 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. */
|
|
1558
|
+
const LUNORA_STUDIO_FEATURES: StudioFeaturesResult = ${JSON.stringify(st,void 0,4)};
|
|
1559
|
+
${ne.constant}${yt}${lt}${ct}${ie}${$e}${oe}${Re}
|
|
1560
|
+
export interface ShardDOConfig {
|
|
1561
|
+
/** Opt into change-data-capture: records a post-image to \`__cdc_log\` on every write (backs streaming export + replay-PITR). */
|
|
1562
|
+
cdc?: boolean;
|
|
1563
|
+
/** 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. */
|
|
1564
|
+
observability?: (env: Record<string, unknown>) => LogSink | undefined;
|
|
1565
|
+
scheduler?: (env: Record<string, unknown>) => unknown;
|
|
1566
|
+
storage?: (env: Record<string, unknown>) => unknown;${xt}${H}${E.configField}${K.configField}${Q.configField}${M.configField}${q.configField}${B.configField}${F.configField}${z.configField}${Oe}${et}${kt}${vt}${$t}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
const schedulerStub = {
|
|
1570
|
+
cancel: async () => {
|
|
1571
|
+
throw new Error("ctx.scheduler: no scheduler configured. Pass \`scheduler\` to createShardDO().");
|
|
1572
|
+
},
|
|
1573
|
+
runAfter: async () => {
|
|
1574
|
+
throw new Error("ctx.scheduler: no scheduler configured. Pass \`scheduler\` to createShardDO().");
|
|
1575
|
+
},
|
|
1576
|
+
runAt: async () => {
|
|
1577
|
+
throw new Error("ctx.scheduler: no scheduler configured. Pass \`scheduler\` to createShardDO().");
|
|
1578
|
+
},
|
|
1579
|
+
};
|
|
1580
|
+
|
|
1581
|
+
const storageStub = {
|
|
1582
|
+
delete: async () => {
|
|
1583
|
+
throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
|
|
1584
|
+
},
|
|
1585
|
+
download: async () => {
|
|
1586
|
+
throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
|
|
1587
|
+
},
|
|
1588
|
+
getMetadata: async () => {
|
|
1589
|
+
throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
|
|
1590
|
+
},
|
|
1591
|
+
getSignedUrl: async () => {
|
|
1592
|
+
throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
|
|
1593
|
+
},
|
|
1594
|
+
getUrl: () => {
|
|
1595
|
+
throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
|
|
1596
|
+
},
|
|
1597
|
+
list: async () => {
|
|
1598
|
+
throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
|
|
1599
|
+
},
|
|
1600
|
+
upload: async () => {
|
|
1601
|
+
throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
|
|
1602
|
+
},
|
|
1603
|
+
};
|
|
1604
|
+
${St}${_t}${Rt}${P}${E.stub}${K.stub}${Q.stub}${M.stub}${q.stub}${B.stub}${F.stub}${z.stub}${h}${nt}${Lt}
|
|
1605
|
+
// Bound in-process \`ctx.run*\` composition depth so a self- or cyclically-
|
|
1606
|
+
// referencing call fails loudly with a clear error instead of overflowing the
|
|
1607
|
+
// stack. Tracked across the awaited handler chain (one DO invocation is
|
|
1608
|
+
// single-threaded), so the counter reflects true nesting depth.
|
|
1609
|
+
const MAX_RUN_DEPTH = 32;
|
|
1610
|
+
let runDepth = 0;
|
|
1611
|
+
|
|
1612
|
+
const dispatchRun = async (expected: FunctionKind, functionPath: string, args: Record<string, unknown>, ctx: unknown): Promise<unknown> => {
|
|
1613
|
+
const registered = LUNORA_FUNCTIONS[functionPath];
|
|
1614
|
+
|
|
1615
|
+
if (!registered) {
|
|
1616
|
+
throw new Error(\`unknown function: \${functionPath}\`);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
if (registered.kind !== expected) {
|
|
1620
|
+
throw new Error(\`ctx.run\${expected[0]!.toUpperCase()}\${expected.slice(1)}: "\${functionPath}" is registered as a \${registered.kind}, not a \${expected}\`);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
if (runDepth >= MAX_RUN_DEPTH) {
|
|
1624
|
+
throw new LunoraError("RUN_DEPTH_EXCEEDED", \`ctx.run*: composition depth limit (\${MAX_RUN_DEPTH}) exceeded — likely a cyclic runQuery/runMutation\`);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
runDepth += 1;
|
|
1628
|
+
|
|
1629
|
+
try {
|
|
1630
|
+
return await registered.handler(ctx, args);
|
|
1631
|
+
} finally {
|
|
1632
|
+
runDepth -= 1;
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* Build the project's shard Durable Object. Export the result as \`ShardDO\`
|
|
1638
|
+
* from the worker entry so wrangler binds it by name.
|
|
1639
|
+
*/
|
|
1640
|
+
export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOState, env: unknown) => ShardDOBase =>
|
|
1641
|
+
class extends ShardDOBase {${Pt}
|
|
1642
|
+
private migrated = false;
|
|
1643
|
+
|
|
1644
|
+
public override async handleRpc(functionPath: string, args: Record<string, unknown>): Promise<unknown> {
|
|
1645
|
+
const registered = LUNORA_FUNCTIONS[functionPath];
|
|
1646
|
+
|
|
1647
|
+
// Internal functions are reachable server-side only: via \`ctx.run*\`
|
|
1648
|
+
// composition, or a trusted system dispatch (scheduler/cron, marked by
|
|
1649
|
+
// \`isSystemDispatch()\`). A client RPC never carries that flag, so its
|
|
1650
|
+
// internals stay not-found and never leak across the external boundary.
|
|
1651
|
+
if (!registered || (registered.visibility === "internal" && !this.isSystemDispatch())) {
|
|
1652
|
+
throw new LunoraError("FUNCTION_NOT_FOUND", \`function not registered: \${functionPath}\`);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
this.ensureMigrated();
|
|
1656
|
+
|
|
1657
|
+
const ctx = this.buildCtx({ functionPath });
|
|
1658
|
+
|
|
1659
|
+
// A mutation's writes must commit all-or-nothing: wrap its dispatch in
|
|
1660
|
+
// the DO's BEGIN/COMMIT span so any throw (a validator, an RLS denial,
|
|
1661
|
+
// an OCC conflict, a failed row mid-\`insertMany\`/\`patchMany\`) rolls
|
|
1662
|
+
// back every write the mutation made. Queries are read-only and actions
|
|
1663
|
+
// do external I/O that can't be rolled back, so both dispatch directly.
|
|
1664
|
+
// \`ctx.run*\` composition runs inside this span (it never re-enters
|
|
1665
|
+
// handleRpc); runInTransaction's own guard rejects accidental nesting.
|
|
1666
|
+
//
|
|
1667
|
+
// The replay bookkeeping (idempotency dedup row + custom-mutator
|
|
1668
|
+
// watermark advance) commits INSIDE this span via
|
|
1669
|
+
// \`commitMutationBookkeeping\`, so the writes, the dedup row, and the
|
|
1670
|
+
// watermark are atomic — a crash can't leave the writes durable without
|
|
1671
|
+
// the replay guard.
|
|
1672
|
+
if (registered.kind === "mutation") {
|
|
1673
|
+
return this.runInTransaction(async () => {
|
|
1674
|
+
const result = await registered.handler(ctx, args);
|
|
1675
|
+
|
|
1676
|
+
this.commitMutationBookkeeping(result);
|
|
1677
|
+
|
|
1678
|
+
return result;
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
return registered.handler(ctx, args);
|
|
1683
|
+
}
|
|
1684
|
+
${qe.override}
|
|
1685
|
+
protected override async executeSubscription(functionPath: string, args: Record<string, unknown>, identity?: { identity?: Record<string, unknown>; userId?: string }): Promise<{ result: unknown; tables: Set<string> } | null> {
|
|
1686
|
+
const registered = LUNORA_FUNCTIONS[functionPath];
|
|
1687
|
+
|
|
1688
|
+
if (!registered || registered.kind !== "query" || registered.visibility === "internal") {
|
|
1689
|
+
return null;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
this.ensureMigrated();
|
|
1693
|
+
|
|
1694
|
+
const tables = new Set<string>();
|
|
1695
|
+
// Identity is threaded EXPLICITLY from the (deferred/interleaved)
|
|
1696
|
+
// subscription caller — never read from the shared per-request field
|
|
1697
|
+
// here — so a concurrent RPC can't leak its identity into this re-run.
|
|
1698
|
+
const ctx = this.buildCtx({ functionPath, identity, onRead: (table) => tables.add(table) });
|
|
1699
|
+
const result = await registered.handler(ctx, args);
|
|
1700
|
+
|
|
1701
|
+
return { result, tables };
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
protected override executeStream(functionPath: string, args: Record<string, unknown>): null | { iterator: (signal: AbortSignal) => AsyncIterable<unknown> } {
|
|
1705
|
+
const registered = LUNORA_FUNCTIONS[functionPath];
|
|
1706
|
+
|
|
1707
|
+
if (!registered || registered.kind !== "stream" || registered.visibility === "internal") {
|
|
1708
|
+
return null;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
this.ensureMigrated();
|
|
1712
|
+
|
|
1713
|
+
return {
|
|
1714
|
+
iterator: (signal) => (registered.handler as (context: unknown, args: Record<string, unknown>, signal: AbortSignal) => AsyncIterable<unknown>)(this.buildCtx({ functionPath }), args, signal),
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
${bt}${ft}${Dt}${It}
|
|
1718
|
+
protected override lifecycleHookPaths(event: "connect" | "disconnect"): readonly string[] {
|
|
1719
|
+
return LUNORA_LIFECYCLE_HOOKS[event];
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
protected override tableRefs(table: string): Record<string, string> | undefined {
|
|
1723
|
+
return LUNORA_TABLE_REFS[table];
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
protected override tableIndexes(table: string): Array<{ fields: string[]; name: string; type: "geo" | "index" | "rank" | "search" | "vector"; unique?: boolean }> {
|
|
1727
|
+
return LUNORA_TABLE_INDEXES[table] ?? [];
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
protected override ttlSweeps(): ReadonlyArray<{ after?: number; field: string; softDeleteField?: string; table: string }> {
|
|
1731
|
+
return LUNORA_TTL_SWEEPS;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
protected override tableColumns(table: string): Array<{ isStorage?: boolean; name: string; optional: boolean; pk?: boolean; ref?: string; type: string }> {
|
|
1735
|
+
return LUNORA_TABLE_COLUMNS[table] ?? [];
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
protected override storageColumns(): Record<string, string[]> {
|
|
1739
|
+
return LUNORA_STORAGE_COLUMNS;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
protected override rlsMetadata(): RlsPoliciesResult {
|
|
1743
|
+
return LUNORA_RLS_METADATA;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
protected override maskMetadata(): MaskPoliciesResult {
|
|
1747
|
+
return LUNORA_MASK_METADATA;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
protected override storageRulesMetadata(): StorageRulesResult {
|
|
1751
|
+
return LUNORA_STORAGE_RULES;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
protected override studioFeatures(): StudioFeaturesResult {
|
|
1755
|
+
return LUNORA_STUDIO_FEATURES;
|
|
1756
|
+
}
|
|
1757
|
+
${ne.evaluateOverride}${ne.subscriptionOverride}${dt}${ut}
|
|
1758
|
+
protected override advisories(): AdvisoryFinding[] {
|
|
1759
|
+
return LUNORA_ADVISORIES;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
protected override async runShardDataMigration(args: RunShardMigrationArgs): Promise<MigrationRunResult> {
|
|
1763
|
+
const migration = LUNORA_MIGRATIONS[args.id];
|
|
1764
|
+
|
|
1765
|
+
if (!migration) {
|
|
1766
|
+
throw new LunoraError("MIGRATION_NOT_FOUND", \`data migration "\${args.id}" is not registered\`, { status: 404 });
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
this.ensureMigrated();
|
|
1770
|
+
|
|
1771
|
+
const env = (this.env ?? {}) as Record<string, unknown>;
|
|
1772
|
+
const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
|
|
1773
|
+
const writer = createShardCtxDb({
|
|
1774
|
+
broadcast: (delta) => {
|
|
1775
|
+
this.recordChangedTable(delta.table);
|
|
1776
|
+
},
|
|
1777
|
+
cdc: config.cdc ?? false,
|
|
1778
|
+
scheduler,
|
|
1779
|
+
schema: schema as unknown as SchemaLike,
|
|
1780
|
+
sql: this.sql as SqlExec,
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
return runDataMigration({
|
|
1784
|
+
batchSize: args.batchSize,
|
|
1785
|
+
direction: args.direction,
|
|
1786
|
+
dryRun: args.dryRun,
|
|
1787
|
+
maxBatches: args.maxBatches,
|
|
1788
|
+
migration: migration as unknown as DataMigrationLike,
|
|
1789
|
+
// Flush after each batch so live migrationStatus subscribers see
|
|
1790
|
+
// progress mid-run (the base method records the reserved state
|
|
1791
|
+
// table the raw-SQL progress write is invisible to the tracker).
|
|
1792
|
+
onBatch: () => this.flushMigrationProgress(),
|
|
1793
|
+
sql: this.sql as SqlExec,
|
|
1794
|
+
writer,
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
protected override async runShardWrite(args: RunShardWriteArgs): Promise<RunShardWriteResult> {
|
|
1799
|
+
const definition = (schema as unknown as SchemaLike).tables[args.table];
|
|
1800
|
+
|
|
1801
|
+
if (!definition) {
|
|
1802
|
+
throw new LunoraError("UNKNOWN_TABLE", \`unknown table: \${args.table}\`, { status: 404 });
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// \`.global()\` tables live in D1, not this DO's SQLite — editing them
|
|
1806
|
+
// here would corrupt nothing but would fail confusingly, so reject up
|
|
1807
|
+
// front with a clear code the studio can surface.
|
|
1808
|
+
if (definition.shardMode?.kind === "global") {
|
|
1809
|
+
throw new LunoraError("GLOBAL_TABLE_NOT_EDITABLE", \`table "\${args.table}" is global; edit it through D1, not the shard\`, { status: 400 });
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
this.ensureMigrated();
|
|
1813
|
+
|
|
1814
|
+
const env = (this.env ?? {}) as Record<string, unknown>;
|
|
1815
|
+
const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
|
|
1816
|
+
const writer = createShardCtxDb({
|
|
1817
|
+
broadcast: (delta) => {
|
|
1818
|
+
this.recordChangedTable(delta.table);
|
|
1819
|
+
},
|
|
1820
|
+
cdc: config.cdc ?? false,
|
|
1821
|
+
scheduler,
|
|
1822
|
+
schema: schema as unknown as SchemaLike,
|
|
1823
|
+
sql: this.sql as SqlExec,
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
if (args.op === "insert") {
|
|
1827
|
+
const id = await writer.insert(args.table, args.doc ?? {});
|
|
1828
|
+
|
|
1829
|
+
return { id, op: "insert" };
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
if (args.op === "delete") {
|
|
1833
|
+
await writer.delete(args.id ?? "");
|
|
1834
|
+
|
|
1835
|
+
return { id: args.id ?? null, op: "delete" };
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
if (args.op === "replace") {
|
|
1839
|
+
await writer.replace(args.id ?? "", args.doc ?? {});
|
|
1840
|
+
|
|
1841
|
+
return { id: args.id ?? null, op: "replace" };
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
await writer.patch(args.id ?? "", args.doc ?? {});
|
|
1845
|
+
|
|
1846
|
+
return { id: args.id ?? null, op: "patch" };
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
protected override async deleteRowThroughWriter(table: string, id: string): Promise<void> {
|
|
1850
|
+
const definition = (schema as unknown as SchemaLike).tables[table];
|
|
1851
|
+
|
|
1852
|
+
if (!definition) {
|
|
1853
|
+
throw new LunoraError("UNKNOWN_TABLE", \`unknown table: \${table}\`, { status: 404 });
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// \`.global()\` tables live in D1, not this DO's SQLite — the same
|
|
1857
|
+
// guard \`runShardWrite\` applies to single-row edits.
|
|
1858
|
+
if (definition.shardMode?.kind === "global") {
|
|
1859
|
+
throw new LunoraError("GLOBAL_TABLE_NOT_EDITABLE", \`table "\${table}" is global; edit it through D1, not the shard\`, { status: 400 });
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
this.ensureMigrated();
|
|
1863
|
+
|
|
1864
|
+
const env = (this.env ?? {}) as Record<string, unknown>;
|
|
1865
|
+
const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
|
|
1866
|
+
const writer = createShardCtxDb({
|
|
1867
|
+
broadcast: (delta) => {
|
|
1868
|
+
this.recordChangedTable(delta.table);
|
|
1869
|
+
},
|
|
1870
|
+
cdc: config.cdc ?? false,
|
|
1871
|
+
scheduler,
|
|
1872
|
+
schema: schema as unknown as SchemaLike,
|
|
1873
|
+
sql: this.sql as SqlExec,
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
// Routes through the writer (not raw SQL) so FTS / aggregate / rank
|
|
1877
|
+
// shadow tables stay in sync and \`onDelete\` cascades fire — the
|
|
1878
|
+
// bounded loop lives in the base \`runShardBulkDelete\`.
|
|
1879
|
+
await writer.delete(id);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
protected override async runShardRankBefore(args: RunShardRankBeforeArgs): Promise<{ before: number; total: number }> {
|
|
1883
|
+
this.ensureMigrated();
|
|
1884
|
+
|
|
1885
|
+
const env = (this.env ?? {}) as Record<string, unknown>;
|
|
1886
|
+
const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
|
|
1887
|
+
const writer = createShardCtxDb({
|
|
1888
|
+
broadcast: (delta) => {
|
|
1889
|
+
this.recordChangedTable(delta.table);
|
|
1890
|
+
},
|
|
1891
|
+
cdc: config.cdc ?? false,
|
|
1892
|
+
scheduler,
|
|
1893
|
+
schema: schema as unknown as SchemaLike,
|
|
1894
|
+
sql: this.sql as SqlExec,
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
// \`rankBefore\` is optional on \`DatabaseWriterLike\` (the D1 twin omits it),
|
|
1898
|
+
// but the shard writer from \`createShardCtxDb\` always defines it.
|
|
1899
|
+
if (!writer.rankBefore) {
|
|
1900
|
+
throw new LunoraError("NOT_IMPLEMENTED", "rankBefore is unavailable on the shard writer", { status: 500 });
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
return writer.rankBefore(args.table, args.index, {
|
|
1904
|
+
partitionKey: args.partitionKey,
|
|
1905
|
+
rowId: args.rowId,
|
|
1906
|
+
sortValues: args.sortValues,
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
protected override async runShardRankPage(args: RunShardRankPageArgs): Promise<ShardRankPageResult> {
|
|
1911
|
+
this.ensureMigrated();
|
|
1912
|
+
|
|
1913
|
+
const env = (this.env ?? {}) as Record<string, unknown>;
|
|
1914
|
+
const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
|
|
1915
|
+
const writer = createShardCtxDb({
|
|
1916
|
+
broadcast: (delta) => {
|
|
1917
|
+
this.recordChangedTable(delta.table);
|
|
1918
|
+
},
|
|
1919
|
+
cdc: config.cdc ?? false,
|
|
1920
|
+
scheduler,
|
|
1921
|
+
schema: schema as unknown as SchemaLike,
|
|
1922
|
+
sql: this.sql as SqlExec,
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
// \`rankPageRows\` is optional on \`DatabaseWriterLike\` (the D1 twin omits it),
|
|
1926
|
+
// but the shard writer from \`createShardCtxDb\` always defines it. The sort
|
|
1927
|
+
// directions live in the schema's rankIndex, so the shard reads them itself;
|
|
1928
|
+
// \`args.directions\` is only the coordinator's comparator hint and isn't forwarded.
|
|
1929
|
+
if (!writer.rankPageRows) {
|
|
1930
|
+
throw new LunoraError("NOT_IMPLEMENTED", "rankPage is unavailable on the shard writer", { status: 500 });
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
return writer.rankPageRows(args.table, args.index, {
|
|
1934
|
+
after: args.after,
|
|
1935
|
+
cursor: args.cursor,
|
|
1936
|
+
partitionKey: args.partitionKey,
|
|
1937
|
+
take: args.take,
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
protected override async runShardApplyCdc(args: RunShardApplyCdcArgs): Promise<{ applied: number }> {
|
|
1942
|
+
this.ensureMigrated();
|
|
1943
|
+
|
|
1944
|
+
const env = (this.env ?? {}) as Record<string, unknown>;
|
|
1945
|
+
const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
|
|
1946
|
+
const writer = createShardCtxDb({
|
|
1947
|
+
broadcast: (delta) => {
|
|
1948
|
+
this.recordChangedTable(delta.table);
|
|
1949
|
+
},
|
|
1950
|
+
cdc: config.cdc ?? false,
|
|
1951
|
+
scheduler,
|
|
1952
|
+
schema: schema as unknown as SchemaLike,
|
|
1953
|
+
sql: this.sql as SqlExec,
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1956
|
+
await applyCdcChanges(writer, args.changes);
|
|
1957
|
+
|
|
1958
|
+
return { applied: args.changes.length };
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
protected override ensureMigrated(): void {
|
|
1962
|
+
if (this.migrated) {
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
runShardMigrations(this.sql as SqlExec, schema as unknown as SchemaLike, { cdc: config.cdc ?? false });
|
|
1967
|
+
this.migrated = true;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
private buildCtx(options: { functionPath?: string; identity?: { identity?: Record<string, unknown>; userId?: string }; onRead?: (table: string) => void } = {}): unknown {
|
|
1971
|
+
const env = (this.env ?? {}) as Record<string, unknown>;
|
|
1972
|
+
// When the caller threads an explicit identity (subscription seed /
|
|
1973
|
+
// refresh — both run in deferred/interleaved contexts), use it by
|
|
1974
|
+
// value and NEVER read the shared per-request identity field, which a
|
|
1975
|
+
// concurrent RPC may have re-set. Otherwise (the synchronous RPC
|
|
1976
|
+
// dispatch path) fall back to the per-request fields as before.
|
|
1977
|
+
const userId = options.identity ? options.identity.userId : this.getCurrentUserId();
|
|
1978
|
+
const identity = options.identity ? options.identity.identity : this.getCurrentIdentity();
|
|
1979
|
+
${Nt}${te}${Ft}${we}${ke}${ye}${Se}
|
|
1980
|
+
const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
|
|
1981
|
+
// Build the storage adapter once and share it between \`ctx.storage\`
|
|
1982
|
+
// and \`ctx.db.system._storage\` so both read the same R2 binding. The
|
|
1983
|
+
// \`storageStub\` fallback satisfies SystemReaderStorageLike structurally
|
|
1984
|
+
// (its \`list\`/\`getMetadata\` throw the "no storage configured" error).
|
|
1985
|
+
const storage = asBucketStorage(config.storage?.(env) ?? storageStub) as unknown as SystemReaderStorageLike;
|
|
1986
|
+
// \`ctx.log\`: the DO base builds the attributed logger (structured
|
|
1987
|
+
// fields + \`.with(...)\` child + trace correlation) and routes each call
|
|
1988
|
+
// to the optional \`observability\` sink. It also buffers the line (studio
|
|
1989
|
+
// Logs panel) and emits a structured console event the dev-server formats.
|
|
1990
|
+
//
|
|
1991
|
+
// Resolved BEFORE \`ctx.db\` below, because the database is wrapped in
|
|
1992
|
+
// auto-instrumentation that needs the sink, the trace anchor, and the
|
|
1993
|
+
// dispatch's wide-event handle.
|
|
1994
|
+
const observability = config.observability?.(env);
|
|
1995
|
+
const logFunctionPath = options.functionPath ?? "";
|
|
1996
|
+
const log = this.makeLogger(logFunctionPath, observability);
|
|
1997
|
+
const traceAnchor = this.resolveDispatchAnchor(Boolean(options.identity));
|
|
1998
|
+
|
|
1999
|
+
// \`ctx.span\`: the handle onto the DISPATCH's own span — the wide-event
|
|
2000
|
+
// surface. Attributes attached here accumulate for the whole request
|
|
2001
|
+
// and are folded into the one span it already emits, instead of
|
|
2002
|
+
// becoming N separate log lines. Shares \`traceAnchor\` with \`ctx.trace\`
|
|
2003
|
+
// so both write to the same trace.
|
|
2004
|
+
const span = this.makeDispatchSpan(traceAnchor, observability);
|
|
2005
|
+
|
|
2006
|
+
${Ct} // \`ctx.db\`, wrapped in automatic instrumentation: by default this
|
|
2007
|
+
// adds aggregate counters (call count, total time, per-operation
|
|
2008
|
+
// breakdown) to the wide event rather than a span per call, so a
|
|
2009
|
+
// handler making hundreds of queries stays readable. See
|
|
2010
|
+
// \`instrumentDatabase\` for the \`"spans"\` / \`"off"\` levels.
|
|
2011
|
+
const db: DatabaseWriterLike = this.instrumentDb(createShardCtxDb(${Ot}), logFunctionPath, traceAnchor, observability);
|
|
2012
|
+
${Et}${Ne}
|
|
2013
|
+
|
|
2014
|
+
// \`ctx.trace\` / \`ctx.metrics\`: spans and measurements to the same sink.
|
|
2015
|
+
// The trace anchor is threaded explicitly for the same reason \`identity\`
|
|
2016
|
+
// is — a deferred caller (a subscription re-run) must not inherit the
|
|
2017
|
+
// writing mutation's trace from the shared per-request field.
|
|
2018
|
+
const trace = this.makeTracer(logFunctionPath, observability, traceAnchor);
|
|
2019
|
+
const metrics = this.makeMetrics(logFunctionPath, observability);
|
|
2020
|
+
${qt}
|
|
2021
|
+
// \`ctx.now\`: the wall-clock instant (epoch ms) this function began,
|
|
2022
|
+
// captured ONCE so the whole handler body sees a single stable value.
|
|
2023
|
+
// Query/mutation handlers must be deterministic (they may be re-run on
|
|
2024
|
+
// OCC retry / subscription re-eval), so they must read time through
|
|
2025
|
+
// \`ctx.now\` instead of \`Date.now()\` — the \`nondeterministic_query_mutation\`
|
|
2026
|
+
// advisor flags the latter. Actions may still use ambient \`Date.now()\`.
|
|
2027
|
+
const now = Date.now();
|
|
2028
|
+
|
|
2029
|
+
const ctx: Record<string, unknown> = {
|
|
2030
|
+
auth: {
|
|
2031
|
+
getIdentity: async () => identity ?? null,
|
|
2032
|
+
userId: userId ?? null,
|
|
2033
|
+
},
|
|
2034
|
+
db,
|
|
2035
|
+
// Instrumented \`fetch\`: a CLIENT span per outbound call plus W3C
|
|
2036
|
+
// \`traceparent\` propagation, so time spent in a downstream service
|
|
2037
|
+
// is visible and its spans join this trace. Degrades to the bare
|
|
2038
|
+
// global when no sink is configured.
|
|
2039
|
+
fetch: this.makeFetch(logFunctionPath, traceAnchor, observability),
|
|
2040
|
+
ip: this.getCurrentIp(),
|
|
2041
|
+
log,
|
|
2042
|
+
metrics,
|
|
2043
|
+
now,${At}
|
|
2044
|
+
scheduler,
|
|
2045
|
+
span,
|
|
2046
|
+
storage,
|
|
2047
|
+
trace,${Tt}${C}${Mt}${de}${xe}${ve}${ge}${se}
|
|
2048
|
+
};
|
|
2049
|
+
${jt}${Bt}
|
|
2050
|
+
ctx.runAction = (reference: FunctionReference, fnArgs: Record<string, unknown>) => dispatchRun("action", reference.__lunoraRef, fnArgs, ctx);
|
|
2051
|
+
ctx.runMutation = (reference: FunctionReference, fnArgs: Record<string, unknown>) => dispatchRun("mutation", reference.__lunoraRef, fnArgs, ctx);
|
|
2052
|
+
ctx.runQuery = (reference: FunctionReference, fnArgs: Record<string, unknown>) => dispatchRun("query", reference.__lunoraRef, fnArgs, ctx);
|
|
2053
|
+
|
|
2054
|
+
return ctx;
|
|
2055
|
+
}
|
|
2056
|
+
};
|
|
2057
|
+
`},Pe=e=>{switch(e.kind){case"array":case"object":case"record":case"union":return{builder:"text",mode:"json",notNull:!0,typeAnnotation:N(e)};case"bigint":return{builder:"blob",mode:"bigint",notNull:!0};case"boolean":return{builder:"integer",mode:"boolean",notNull:!0};case"bytes":return{builder:"blob",mode:"buffer",notNull:!0};case"date":case"timestamp":return{builder:"integer",notNull:!0};case"id":return{builder:"text",notNull:!0};case"literal":{const t=e.literalValue??"";return t.startsWith('"')||t.startsWith("'")?{builder:"text",notNull:!0}:t==="true"||t==="false"?{builder:"integer",mode:"boolean",notNull:!0}:t==="null"?{builder:"text",notNull:!0}:{builder:"real",notNull:!0}}case"null":return{builder:"text",notNull:!0};case"number":return{builder:"real",notNull:!0};case"optional":return{...e.inner?Pe(e.inner):{builder:"text",notNull:!1},notNull:!1};case"storage":case"string":return{builder:"text",notNull:!0};default:return{builder:"text",notNull:!0}}},Gn=(e,t,n)=>{m(e,"drizzle column name");const a=Pe(t),r=t.kind==="optional"&&t.inner?t.inner:t,o=a.mode?`, { mode: "${a.mode}" }`:"";let s=`${a.builder}("${e}"${o})`;return a.typeAnnotation&&(s+=`.$type<${a.typeAnnotation}>()`),r.kind==="id"&&r.tableName&&n.has(r.tableName)&&(s+=`.references(() => ${r.tableName}._id)`),a.notNull&&(s+=".notNull()"),s},Jn=e=>{m(e.name,"drizzle index name");const t=e.unique?"uniqueIndex":"index",n=e.fields.map(a=>(m(a,"drizzle index field"),`t.${a}`)).join(", ");return` ${e.name}: ${t}("${e.name}").on(${n}),`},Vn=(e,t)=>{m(e.name,"table name");const n=[' _id: text("_id").primaryKey(),',' _creationTime: integer("_creationTime").notNull(),',...Object.entries(e.shape).map(([r,o])=>` ${r}: ${Gn(r,o,t)},`)].join(`
|
|
2058
|
+
`),a=e.indexes.length>0?`, (t) => ({
|
|
2059
|
+
${e.indexes.map(r=>Jn(r)).join(`
|
|
2060
|
+
`)}
|
|
2061
|
+
})`:"";return`export const ${e.name} = sqliteTable("${e.name}", {
|
|
2062
|
+
${n}
|
|
2063
|
+
}${a});`},Hn=e=>{const t=new Set,n=new Set;t.add("text"),t.add("integer"),t.add("sqliteTable");for(const a of e){for(const r of Object.values(a.shape)){const o=Pe(r);t.add(o.builder)}for(const r of a.indexes)n.add(r.unique?"uniqueIndex":"index")}return{columns:[...t].toSorted((a,r)=>a.localeCompare(r)),indexes:[...n].toSorted((a,r)=>a.localeCompare(r))}},Ge=(e,t=!1)=>{if(e.length===0)return`${k}export {};
|
|
2064
|
+
`;const n=V(t),{columns:a,indexes:r}=Hn(e),o=[...r,...a].toSorted((l,u)=>l.localeCompare(u)),s=new Set(e.map(l=>l.name)),i=e.map(l=>Vn(l,s)).join(`
|
|
2065
|
+
|
|
2066
|
+
`);return`${k}import { ${o.join(", ")} } from "${n.serverDrizzle}";
|
|
2067
|
+
|
|
2068
|
+
${i}
|
|
2069
|
+
`},ur=(e,t=!1)=>{const n=e.tables.filter(r=>r.shardMode==="global"),a=e.tables.filter(r=>r.shardMode!=="global");return{global:Ge(n,t),shard:Ge(a,t)}},pr=e=>{const t=[],n=new Map;for(const i of e){const l=n.get(i.cron);l?l.push(i):(n.set(i.cron,[i]),t.push(i.cron))}const a=t.map(i=>` ${JSON.stringify(i)},`).join(`
|
|
2070
|
+
`),r=a.length>0?`
|
|
2071
|
+
${a}
|
|
2072
|
+
`:"",o=[...n.entries()].map(([i,l])=>{const u=l.map(p=>{const b=p.workflow?`workflow: ${JSON.stringify(p.workflow.binding)}`:`functionPath: ${JSON.stringify(p.functionPath)}`;return` { name: ${JSON.stringify(p.name)}, ${b}, args: ${JSON.stringify(p.args)} },`}).join(`
|
|
2073
|
+
`);return` ${JSON.stringify(i)}: [
|
|
2074
|
+
${u}
|
|
2075
|
+
],`}).join(`
|
|
2076
|
+
`),s=o.length>0?`
|
|
2077
|
+
${o}
|
|
2078
|
+
`:"";return`${k}/**
|
|
2079
|
+
* One scheduled cron invocation. Exactly one target is set: \`functionPath\` is
|
|
2080
|
+
* the \`namespace:fn\` dispatch ref (matches \`__lunoraRef\`), invoked on the
|
|
2081
|
+
* shard; \`workflow\` is a \`WORKFLOW_*\` binding name whose durable workflow is
|
|
2082
|
+
* started fresh per fire. \`args\` are forwarded verbatim (a workflow's become
|
|
2083
|
+
* its \`params\`).
|
|
2084
|
+
*/
|
|
2085
|
+
export interface LunoraCronJob {
|
|
2086
|
+
args: Record<string, unknown>;
|
|
2087
|
+
functionPath?: string;
|
|
2088
|
+
name: string;
|
|
2089
|
+
workflow?: string;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
/**
|
|
2093
|
+
* Deduplicated cron schedules — mirror of wrangler's \`triggers.crons\`. The
|
|
2094
|
+
* Lunora vite plugin keeps \`wrangler.jsonc\` in sync with this array.
|
|
2095
|
+
*/
|
|
2096
|
+
export const LUNORA_CRON_TRIGGERS: ReadonlyArray<string> = [${r}];
|
|
2097
|
+
|
|
2098
|
+
/**
|
|
2099
|
+
* Dispatcher map keyed by cron expression. The Worker's \`scheduled()\` handler
|
|
2100
|
+
* looks up \`event.cron\` here and dispatches every job in the list.
|
|
2101
|
+
*/
|
|
2102
|
+
export const LUNORA_CRONS: Record<string, ReadonlyArray<LunoraCronJob>> = {${s}};
|
|
2103
|
+
`},hr=e=>{const t=[...e].toSorted((a,r)=>a.name.localeCompare(r.name)).map(a=>{const r=[`name: ${JSON.stringify(a.name)}`,`table: ${JSON.stringify(a.table)}`];return a.field!==void 0&&r.push(`field: ${JSON.stringify(a.field)}`),a.dimensions!==void 0&&r.push(`dimensions: ${JSON.stringify(a.dimensions)}`),a.metric!==void 0&&r.push(`metric: ${JSON.stringify(a.metric)}`),a.metadata!==void 0&&r.push(`metadata: ${JSON.stringify(a.metadata)}`),` { ${r.join(", ")} },`}).join(`
|
|
2104
|
+
`),n=t.length>0?`
|
|
2105
|
+
${t}
|
|
2106
|
+
`:"";return`${k}/**
|
|
2107
|
+
* One vector index declared in \`schema.ts\` — an inline \`.vectorize()\` column
|
|
2108
|
+
* or a standalone \`defineVectorIndex()\`. \`field\` is the source column (absent
|
|
2109
|
+
* for a \`select\`-derived Shape B index); \`metadata\` lists the row fields
|
|
2110
|
+
* mirrored into Vectorize for filtering.
|
|
2111
|
+
*/
|
|
2112
|
+
export interface LunoraVectorIndex {
|
|
2113
|
+
dimensions?: number;
|
|
2114
|
+
field?: string;
|
|
2115
|
+
metadata?: ReadonlyArray<string>;
|
|
2116
|
+
metric?: "cosine" | "dot-product" | "euclidean";
|
|
2117
|
+
name: string;
|
|
2118
|
+
table: string;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
/**
|
|
2122
|
+
* Static registry of every vector index in the schema, sorted by name.
|
|
2123
|
+
* Vectorize cannot enumerate indexes at runtime, so the worker passes this to
|
|
2124
|
+
* \`createWorker({ vectorIntrospector })\` to back the studio's vector browser.
|
|
2125
|
+
*/
|
|
2126
|
+
export const LUNORA_VECTOR_INDEXES: ReadonlyArray<LunoraVectorIndex> = [${n}];
|
|
2127
|
+
`},gr=e=>{const t=new Set,n=[];for(const a of e)t.has(a.cron)||(t.add(a.cron),n.push(a.cron));return n};export{sr as D,lr as E,er as G,cr as K,ir as L,pr as X,hr as Y,gr as Z,dr as _,wn as a,nr as b,Zn as c,or as g,tr as n,rr as o,Wt as r,ar as u,k as w,ur as z};
|