@lunora/codegen 1.0.0-alpha.2 → 1.0.0-alpha.20
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/__assets__/package-og.svg +1 -1
- package/dist/index.d.mts +444 -16
- package/dist/index.d.ts +444 -16
- package/dist/index.mjs +28 -22
- package/dist/packem_shared/{CONTAINERS_FILENAME-0K-pjNb8.mjs → CONTAINERS_FILENAME-DlP6YM_Q.mjs} +1 -1
- package/dist/packem_shared/{CodegenDiagnosticError-54jWDxA9.mjs → CodegenDiagnosticError-DeblMkzO.mjs} +3 -2
- package/dist/packem_shared/FLAGS_FILENAME-B1vE0LIo.mjs +139 -0
- package/dist/packem_shared/{emitApi-hRVC-kE7.mjs → GENERATED_HEADER-CI272wRm.mjs} +873 -54
- package/dist/packem_shared/LUNORA_SOLUTION_RULES-BTejmZp-.mjs +131 -0
- package/dist/packem_shared/MUTATORS_FILENAME-BhqdPtKp.mjs +81 -0
- package/dist/packem_shared/{buildOpenRpcDocument-BZGY1-jT.mjs → OPENRPC_VERSION-t5pV2NIN.mjs} +1 -1
- package/dist/packem_shared/QUEUES_FILENAME-B5_eWCRe.mjs +119 -0
- package/dist/packem_shared/{createCodegenProject-DGJm0_Pk.mjs → SCHEMA_SNAPSHOT_FILENAME-BMez-jgr.mjs} +187 -43
- package/dist/packem_shared/{buildSchemaSnapshot-DzLDbWk3.mjs → SCHEMA_SNAPSHOT_VERSION-wWGP2_5E.mjs} +12 -0
- package/dist/packem_shared/SHAPES_FILENAME-ChV7MqgE.mjs +94 -0
- package/dist/packem_shared/{discoverWorkflows-DRDQdhfq.mjs → WORKFLOWS_FILENAME-CCisG0Vy.mjs} +42 -2
- package/dist/packem_shared/{buildOpenApiDocument-yHVN66Xd.mjs → buildOpenApiDocument-Dx5QftUn.mjs} +1 -1
- package/dist/packem_shared/{discoverAuthApiCalls-C35R6z0T.mjs → discoverAuthApiCalls-CoirYbg6.mjs} +1 -1
- package/dist/packem_shared/{discoverCrons-BL6iGuJ3.mjs → discoverCrons-Cev7RRAf.mjs} +20 -17
- package/dist/packem_shared/{discoverFunctions-DEgAcRuD.mjs → discoverFunctions-BWMczzBx.mjs} +7 -2
- package/dist/packem_shared/{discoverHttpRoutes-C978pBiG.mjs → discoverHttpRoutes-CfP6cMzt.mjs} +2 -2
- package/dist/packem_shared/{discoverInserts-CRQdXvHO.mjs → discoverInserts-C7zxXkUf.mjs} +24 -2
- package/dist/packem_shared/{discoverMaskProcedures-B64zA740.mjs → discoverMaskProcedures-Cm81kwrb.mjs} +1 -1
- package/dist/packem_shared/{discoverMigrations-Doj_-BAA.mjs → discoverMigrations-Bi5nJ0mJ.mjs} +10 -4
- package/dist/packem_shared/{discoverNondeterministicCalls-4KiPQxQU.mjs → discoverNondeterministicCalls-C4M8AXmQ.mjs} +1 -1
- package/dist/packem_shared/{discoverQueries-BkIi0dBD.mjs → discoverQueries-B0wGT-xe.mjs} +1 -1
- package/dist/packem_shared/discoverR2sqlCalls-BVNMd428.mjs +80 -0
- package/dist/packem_shared/{discoverRlsMetadata-DpRB1HMe.mjs → discoverRlsMetadata-BS9GOGC5.mjs} +1 -1
- package/dist/packem_shared/{discoverSchema-BBulgGbH.mjs → discoverSchema-KYq55qu3.mjs} +338 -16
- package/dist/packem_shared/{discoverStorageRulesMetadata-DAqJUxUv.mjs → discoverStorageRulesMetadata-Da8BKXcI.mjs} +1 -1
- package/dist/packem_shared/{emitApp-CzZ6GbrD.mjs → emitApp-cOSypPuO.mjs} +74 -8
- package/dist/packem_shared/{lintSchema-DicbOHvH.mjs → formatAdvisories-DdjK7sgh.mjs} +15 -2
- package/dist/packem_shared/{parse-validator-tuQtHrsr.mjs → parse-validator-Cabb60UV.mjs} +2 -1
- package/package.json +8 -7
|
@@ -1,5 +1,174 @@
|
|
|
1
1
|
import { s as sanitizeNamespace } from './paths-BRd6JHuF.mjs';
|
|
2
2
|
|
|
3
|
+
const LITERAL_VALUE_RE = /^(?:"[^"\\]*"|'[^'\\]*'|-?\d+(?:\.\d+)?|true|false|null)$/u;
|
|
4
|
+
|
|
5
|
+
const PASS_THROUGH_KINDS = /* @__PURE__ */ new Set(["any", "bigint", "boolean", "date", "id", "null", "number", "storage", "string", "timestamp"]);
|
|
6
|
+
const hasColumnModifier = (node) => node.column !== void 0;
|
|
7
|
+
const emitScalarGuard = (kind, inExpr) => {
|
|
8
|
+
switch (kind) {
|
|
9
|
+
case "bigint": {
|
|
10
|
+
return `if (typeof ${inExpr} !== "bigint") return DEFER;
|
|
11
|
+
`;
|
|
12
|
+
}
|
|
13
|
+
case "boolean": {
|
|
14
|
+
return `if (typeof ${inExpr} !== "boolean") return DEFER;
|
|
15
|
+
`;
|
|
16
|
+
}
|
|
17
|
+
case "date":
|
|
18
|
+
case "number":
|
|
19
|
+
case "timestamp": {
|
|
20
|
+
return `if (typeof ${inExpr} !== "number" || !Number.isFinite(${inExpr})) return DEFER;
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
case "null": {
|
|
24
|
+
return `if (${inExpr} !== null) return DEFER;
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
// string / id / storage all parse as a bare string at runtime.
|
|
28
|
+
default: {
|
|
29
|
+
return `if (typeof ${inExpr} !== "string") return DEFER;
|
|
30
|
+
`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const compileLiteral = (node, inExpr) => {
|
|
35
|
+
const literal = node.literalValue?.trim();
|
|
36
|
+
if (literal === void 0 || !LITERAL_VALUE_RE.test(literal)) {
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
return { out: inExpr, pre: `if (${inExpr} !== ${literal}) return DEFER;
|
|
40
|
+
` };
|
|
41
|
+
};
|
|
42
|
+
const compileNode = (node, inExpr, context) => {
|
|
43
|
+
if (hasColumnModifier(node)) {
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
if (node.hasRefinement || node.sourceText !== void 0) {
|
|
47
|
+
return void 0;
|
|
48
|
+
}
|
|
49
|
+
if (PASS_THROUGH_KINDS.has(node.kind)) {
|
|
50
|
+
if (node.kind === "any") {
|
|
51
|
+
return { out: inExpr, pre: "" };
|
|
52
|
+
}
|
|
53
|
+
return { out: inExpr, pre: emitScalarGuard(node.kind, inExpr) };
|
|
54
|
+
}
|
|
55
|
+
switch (node.kind) {
|
|
56
|
+
case "array": {
|
|
57
|
+
return compileArray(node, inExpr, context);
|
|
58
|
+
}
|
|
59
|
+
case "literal": {
|
|
60
|
+
return compileLiteral(node, inExpr);
|
|
61
|
+
}
|
|
62
|
+
case "object": {
|
|
63
|
+
return compileObject(node, inExpr, context);
|
|
64
|
+
}
|
|
65
|
+
default: {
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const compileArray = (node, inExpr, context) => {
|
|
71
|
+
const { inner } = node;
|
|
72
|
+
if (!inner) {
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
const id = context.next();
|
|
76
|
+
const array = `__arr${String(id)}`;
|
|
77
|
+
const index = `__i${String(id)}`;
|
|
78
|
+
const element = `__e${String(id)}`;
|
|
79
|
+
const innerEmit = compileNode(inner, element, context);
|
|
80
|
+
if (!innerEmit) {
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
const pre = `if (!Array.isArray(${inExpr})) return DEFER;
|
|
84
|
+
const ${array} = new Array(${inExpr}.length);
|
|
85
|
+
for (let ${index} = 0; ${index} < ${inExpr}.length; ${index}++) {
|
|
86
|
+
const ${element} = ${inExpr}[${index}];
|
|
87
|
+
${innerEmit.pre}${array}[${index}] = ${innerEmit.out};
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
return { out: array, pre };
|
|
91
|
+
};
|
|
92
|
+
const compileField = (key, node, access, context) => {
|
|
93
|
+
const keyLiteral = JSON.stringify(key);
|
|
94
|
+
if (node.kind === "optional") {
|
|
95
|
+
if (node.hasRefinement || node.sourceText !== void 0 || hasColumnModifier(node)) {
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
const { inner } = node;
|
|
99
|
+
if (!inner) {
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
const innerEmit = compileNode(inner, access, context);
|
|
103
|
+
if (!innerEmit) {
|
|
104
|
+
return void 0;
|
|
105
|
+
}
|
|
106
|
+
const id = context.next();
|
|
107
|
+
const has = `__has${String(id)}`;
|
|
108
|
+
const value = `__val${String(id)}`;
|
|
109
|
+
const pre = `let ${has} = false;
|
|
110
|
+
let ${value};
|
|
111
|
+
if (${access} !== undefined) {
|
|
112
|
+
${innerEmit.pre}${value} = ${innerEmit.out};
|
|
113
|
+
${has} = true;
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
return { entry: `...(${has} ? { ${keyLiteral}: ${value} } : {})`, pre };
|
|
117
|
+
}
|
|
118
|
+
const emit = compileNode(node, access, context);
|
|
119
|
+
if (!emit) {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
return { entry: `${keyLiteral}: ${emit.out}`, pre: emit.pre };
|
|
123
|
+
};
|
|
124
|
+
const compileFields = (shape, accessFor, context) => {
|
|
125
|
+
let pre = "";
|
|
126
|
+
const entries = [];
|
|
127
|
+
for (const key of Object.keys(shape)) {
|
|
128
|
+
const node = shape[key];
|
|
129
|
+
if (!node) {
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
132
|
+
const field = compileField(key, node, accessFor(key), context);
|
|
133
|
+
if (!field) {
|
|
134
|
+
return void 0;
|
|
135
|
+
}
|
|
136
|
+
pre += field.pre;
|
|
137
|
+
entries.push(field.entry);
|
|
138
|
+
}
|
|
139
|
+
return { entries: entries.join(", "), pre };
|
|
140
|
+
};
|
|
141
|
+
const compileObject = (node, inExpr, context) => {
|
|
142
|
+
const shape = node.shape ?? {};
|
|
143
|
+
const fields = compileFields(shape, (key) => `${inExpr}[${JSON.stringify(key)}]`, context);
|
|
144
|
+
if (!fields) {
|
|
145
|
+
return void 0;
|
|
146
|
+
}
|
|
147
|
+
const id = context.next();
|
|
148
|
+
const object = `__obj${String(id)}`;
|
|
149
|
+
const pre = `if (typeof ${inExpr} !== "object" || ${inExpr} === null || Array.isArray(${inExpr})) return DEFER;
|
|
150
|
+
${fields.pre}const ${object} = { ${fields.entries} };
|
|
151
|
+
`;
|
|
152
|
+
return { out: object, pre };
|
|
153
|
+
};
|
|
154
|
+
const compileArgsValidator = (args) => {
|
|
155
|
+
let counter = 0;
|
|
156
|
+
const context = {
|
|
157
|
+
next: () => {
|
|
158
|
+
counter += 1;
|
|
159
|
+
return counter;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const fields = compileFields(args, (key) => `source[${JSON.stringify(key)}]`, context);
|
|
163
|
+
if (!fields) {
|
|
164
|
+
return void 0;
|
|
165
|
+
}
|
|
166
|
+
return `(source) => {
|
|
167
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) return DEFER;
|
|
168
|
+
${fields.pre}return { ${fields.entries} };
|
|
169
|
+
}`;
|
|
170
|
+
};
|
|
171
|
+
|
|
3
172
|
const GENERATED_HEADER = "// GENERATED by @lunora/codegen — do not edit.\n// Run `lunora codegen` to regenerate.\n\n";
|
|
4
173
|
const baseSpecifiers = (useUmbrella = false) => useUmbrella ? {
|
|
5
174
|
client: "lunorash/client",
|
|
@@ -7,17 +176,18 @@ const baseSpecifiers = (useUmbrella = false) => useUmbrella ? {
|
|
|
7
176
|
server: "lunorash/server",
|
|
8
177
|
serverDataModel: "lunorash/server/data-model",
|
|
9
178
|
serverDrizzle: "lunorash/server/drizzle",
|
|
10
|
-
serverTypes: "lunorash/server/types"
|
|
179
|
+
serverTypes: "lunorash/server/types",
|
|
180
|
+
values: "lunorash/values"
|
|
11
181
|
} : {
|
|
12
182
|
client: "@lunora/client",
|
|
13
183
|
do: "@lunora/do",
|
|
14
184
|
server: "@lunora/server",
|
|
15
185
|
serverDataModel: "@lunora/server/data-model",
|
|
16
186
|
serverDrizzle: "@lunora/server/drizzle",
|
|
17
|
-
serverTypes: "@lunora/server/types"
|
|
187
|
+
serverTypes: "@lunora/server/types",
|
|
188
|
+
values: "@lunora/values"
|
|
18
189
|
};
|
|
19
190
|
const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/u;
|
|
20
|
-
const LITERAL_VALUE_RE = /^(?:"[^"\\]*"|'[^'\\]*'|-?\d+(?:\.\d+)?|true|false|null)$/u;
|
|
21
191
|
const IMPORT_PATH_RE = /^[\w./-]+$/u;
|
|
22
192
|
const assertIdentifier = (value, context) => {
|
|
23
193
|
if (!IDENTIFIER_RE.test(value)) {
|
|
@@ -430,6 +600,26 @@ import type { InsertModel } from "./dataModel.js";
|
|
|
430
600
|
export const createSeedClient = (options?: SeedClientOptions): SeedClient<InsertModel> => createSeedClientBase<InsertModel>(schema, options);
|
|
431
601
|
`;
|
|
432
602
|
};
|
|
603
|
+
const emitCollections = (shapes, hasDatabase, useUmbrella = false) => {
|
|
604
|
+
if (shapes.length === 0 || !hasDatabase) {
|
|
605
|
+
return "";
|
|
606
|
+
}
|
|
607
|
+
const base = baseSpecifiers(useUmbrella);
|
|
608
|
+
const factories = shapes.map((shape) => {
|
|
609
|
+
assertIdentifier(shape.exportName, "shape export name");
|
|
610
|
+
return `/** Live collection for the \`${shape.exportName}\` replication shape — pass the shape's validated \`args\` (its partition selector). */
|
|
611
|
+
export const ${shape.exportName}Collection = (client: LunoraClient, args?: Record<string, unknown>): Collection<Row, string> =>
|
|
612
|
+
createCollection(lunoraCollectionOptions({ client, shape: { args, name: "${shape.exportName}" } }).config);`;
|
|
613
|
+
}).join("\n\n");
|
|
614
|
+
return `${GENERATED_HEADER}import type { LunoraClient } from "${base.client}";
|
|
615
|
+
import { lunoraCollectionOptions } from "@lunora/db/collections";
|
|
616
|
+
import type { Row } from "@lunora/db";
|
|
617
|
+
import type { Collection } from "@tanstack/db";
|
|
618
|
+
import { createCollection } from "@tanstack/db";
|
|
619
|
+
|
|
620
|
+
${factories}
|
|
621
|
+
`;
|
|
622
|
+
};
|
|
433
623
|
const moduleAlias = (filePath, index) => `lunora_${sanitizeNamespace(filePath)}_${String(index)}`;
|
|
434
624
|
const CALL_REGISTERED_HELPER = `const callRegistered = async <R>(context: CallerCtx, functionPath: string, args: Record<string, unknown> | undefined): Promise<R> => {
|
|
435
625
|
const registered = LUNORA_FUNCTIONS[functionPath];
|
|
@@ -444,7 +634,7 @@ const CALL_REGISTERED_HELPER = `const callRegistered = async <R>(context: Caller
|
|
|
444
634
|
|
|
445
635
|
return (await registered.handler(context, args ?? {})) as R;
|
|
446
636
|
};`;
|
|
447
|
-
const renderFunctionRegistry = (functions, migrations) => {
|
|
637
|
+
const renderFunctionRegistry = (functions, migrations, mutators = [], shapes = []) => {
|
|
448
638
|
const aliasByPath = /* @__PURE__ */ new Map();
|
|
449
639
|
const registerPath = (filePath) => {
|
|
450
640
|
if (!aliasByPath.has(filePath)) {
|
|
@@ -457,6 +647,12 @@ const renderFunctionRegistry = (functions, migrations) => {
|
|
|
457
647
|
for (const migration of migrations) {
|
|
458
648
|
registerPath(migration.filePath);
|
|
459
649
|
}
|
|
650
|
+
for (const mutator of mutators) {
|
|
651
|
+
registerPath(mutator.filePath);
|
|
652
|
+
}
|
|
653
|
+
for (const shape of shapes) {
|
|
654
|
+
registerPath(shape.filePath);
|
|
655
|
+
}
|
|
460
656
|
const importLines = [...aliasByPath.entries()].map(([filePath, alias]) => {
|
|
461
657
|
if (!IMPORT_PATH_RE.test(filePath)) {
|
|
462
658
|
throw new Error(`@lunora/codegen: refusing to emit import for unsafe file path: ${JSON.stringify(filePath)}`);
|
|
@@ -469,15 +665,37 @@ const renderFunctionRegistry = (functions, migrations) => {
|
|
|
469
665
|
const migrationEntries = migrations.map(
|
|
470
666
|
(migration) => ` ${JSON.stringify(migration.id)}: ${aliasByPath.get(migration.filePath) ?? ""}.${migration.exportName} as unknown as RegisteredDataMigration,`
|
|
471
667
|
).join("\n");
|
|
668
|
+
const mutatorEntries = mutators.map(
|
|
669
|
+
(mutator) => ` "${sanitizeNamespace(mutator.filePath)}:${mutator.exportName}": ${aliasByPath.get(mutator.filePath) ?? ""}.${mutator.exportName} as unknown as RegisteredLunoraFunction,`
|
|
670
|
+
).join("\n");
|
|
671
|
+
const shapeEntries = shapes.map((shape) => ` "${shape.exportName}": ${aliasByPath.get(shape.filePath) ?? ""}.${shape.exportName} as unknown as RegisteredShape,`).join("\n");
|
|
672
|
+
const mutatorPaths = mutators.map((mutator) => `${sanitizeNamespace(mutator.filePath)}:${mutator.exportName}`);
|
|
673
|
+
const combinedDispatch = [dispatchEntries, mutatorEntries].filter((entries) => entries.length > 0).join("\n");
|
|
674
|
+
const installLines = functions.map((definition) => {
|
|
675
|
+
if (definition.lifecycle || Object.keys(definition.args).length === 0) {
|
|
676
|
+
return void 0;
|
|
677
|
+
}
|
|
678
|
+
const compiled = compileArgsValidator(definition.args);
|
|
679
|
+
if (compiled === void 0) {
|
|
680
|
+
return void 0;
|
|
681
|
+
}
|
|
682
|
+
const alias = aliasByPath.get(definition.filePath) ?? "";
|
|
683
|
+
return `installCompiledValidatorMap(${alias}.${definition.exportName}.args, ${compiled});`;
|
|
684
|
+
}).filter((line) => line !== void 0).join("\n");
|
|
472
685
|
return {
|
|
473
|
-
dispatchBody:
|
|
474
|
-
${
|
|
686
|
+
dispatchBody: combinedDispatch.length > 0 ? `
|
|
687
|
+
${combinedDispatch}
|
|
475
688
|
` : "",
|
|
476
689
|
importBlock: importLines.length > 0 ? `${importLines}
|
|
477
690
|
|
|
478
691
|
` : "",
|
|
692
|
+
installBlock: installLines,
|
|
479
693
|
migrationBody: migrationEntries.length > 0 ? `
|
|
480
694
|
${migrationEntries}
|
|
695
|
+
` : "",
|
|
696
|
+
mutatorPaths,
|
|
697
|
+
shapeBody: shapeEntries.length > 0 ? `
|
|
698
|
+
${shapeEntries}
|
|
481
699
|
` : ""
|
|
482
700
|
};
|
|
483
701
|
};
|
|
@@ -531,14 +749,18 @@ const buildStorageBucketNames = (schema, ruleBuckets = []) => {
|
|
|
531
749
|
};
|
|
532
750
|
const emitServer = ({
|
|
533
751
|
containers = [],
|
|
752
|
+
hasAccessFacade = false,
|
|
534
753
|
hasAi = false,
|
|
535
754
|
hasAnalytics = false,
|
|
536
755
|
hasBrowser = false,
|
|
756
|
+
hasFlags = false,
|
|
537
757
|
hasHyperdrive = false,
|
|
538
758
|
hasImages = false,
|
|
539
759
|
hasKv = false,
|
|
540
760
|
hasPayments = false,
|
|
541
761
|
hasPipelines = false,
|
|
762
|
+
hasR2sql = false,
|
|
763
|
+
queues = [],
|
|
542
764
|
schema,
|
|
543
765
|
storageRuleBuckets = [],
|
|
544
766
|
useUmbrella = false,
|
|
@@ -572,6 +794,11 @@ const emitServer = ({
|
|
|
572
794
|
assertIdentifier(workflow.bindingName, `workflow binding "${workflow.bindingName}"`);
|
|
573
795
|
return ` /** Workflow binding for the \`${workflow.exportName}\` workflow. */
|
|
574
796
|
readonly ${workflow.bindingName}?: unknown;`;
|
|
797
|
+
}),
|
|
798
|
+
...queues.map((queue) => {
|
|
799
|
+
assertIdentifier(queue.bindingName, `queue binding "${queue.bindingName}"`);
|
|
800
|
+
return ` /** Queue producer binding for the \`${queue.exportName}\` queue. */
|
|
801
|
+
readonly ${queue.bindingName}?: unknown;`;
|
|
575
802
|
})
|
|
576
803
|
].join("\n");
|
|
577
804
|
const envBlock = `
|
|
@@ -594,7 +821,13 @@ ${envBindingFields}` : ""}
|
|
|
594
821
|
/** Alias for {@link CloudflareBindings} — the typed shape of \`env\`. */
|
|
595
822
|
export type Env = CloudflareBindings;`;
|
|
596
823
|
const kvContextField = hasKv ? `
|
|
597
|
-
readonly kv: import("@lunora/kv").Kv;` : "";
|
|
824
|
+
readonly kv: import("@lunora/bindings/kv").Kv;` : "";
|
|
825
|
+
const accessContextField = hasAccessFacade ? `
|
|
826
|
+
/** Verified Cloudflare Access identity — a synchronous facade over the resolved claims (email / groups / hasGroup / claims). Anonymous when no Access token is present. */
|
|
827
|
+
readonly access: import("@lunora/cloudflare-access/context").AccessFacade;` : "";
|
|
828
|
+
const flagsContextField = hasFlags ? `
|
|
829
|
+
/** Feature-flag evaluation (OpenFeature). Reads are memoized per request; evaluations never throw — a provider error resolves to the supplied default. */
|
|
830
|
+
readonly flags: import("@lunora/flags").LunoraFlags;` : "";
|
|
598
831
|
const hyperdriveActionField = hasHyperdrive ? `
|
|
599
832
|
/**
|
|
600
833
|
* 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.
|
|
@@ -605,13 +838,18 @@ export type Env = CloudflareBindings;`;
|
|
|
605
838
|
readonly browser: import("@lunora/browser").Browser;` : "";
|
|
606
839
|
const imagesActionField = hasImages ? `
|
|
607
840
|
/** Cloudflare Images transforms (resize/format/optimize). Non-deterministic — available only in actions. */
|
|
608
|
-
readonly images: import("@lunora/images").Images;` : "";
|
|
841
|
+
readonly images: import("@lunora/bindings/images").Images;` : "";
|
|
609
842
|
const analyticsContextField = hasAnalytics ? `
|
|
610
843
|
/** Analytics Engine telemetry sink. Fire-and-forget and sampled; do not read it back in-handler. */
|
|
611
|
-
readonly analytics: import("@lunora/analytics").AnalyticsClient;` : "";
|
|
844
|
+
readonly analytics: import("@lunora/bindings/analytics").AnalyticsClient;` : "";
|
|
612
845
|
const pipelinesActionField = hasPipelines ? `
|
|
613
846
|
/** Pipelines ingestion sink (durable, R2-backed). Fire-and-forget and batched; do not read it back in-handler. */
|
|
614
|
-
readonly pipelines: import("@lunora/pipelines").PipelineClient;` : "";
|
|
847
|
+
readonly pipelines: import("@lunora/bindings/pipelines").PipelineClient;` : "";
|
|
848
|
+
const r2sqlActionField = hasR2sql ? `
|
|
849
|
+
/**
|
|
850
|
+
* 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.
|
|
851
|
+
*/
|
|
852
|
+
readonly r2sql: import("@lunora/bindings/r2sql").R2SqlClient;` : "";
|
|
615
853
|
const hasWorkflows = workflows.length > 0;
|
|
616
854
|
const workflowsTypeImport = hasWorkflows ? `import type { WorkflowHandle } from "@lunora/workflow";
|
|
617
855
|
import type * as lunoraWorkflowDefinitions from "../workflows.js";
|
|
@@ -632,6 +870,21 @@ ${workflows.map((workflow) => ` get(name: ${JSON.stringify(workflow.exportNam
|
|
|
632
870
|
const workflowsOmit = hasWorkflows ? ` | "workflows"` : "";
|
|
633
871
|
const workflowsContextField = hasWorkflows ? `
|
|
634
872
|
readonly workflows: LunoraWorkflows;` : "";
|
|
873
|
+
const hasQueues = queues.length > 0;
|
|
874
|
+
const queuesTypeImport = hasQueues ? `import type { QueueProducer } from "@lunora/queue";
|
|
875
|
+
import type * as lunoraQueueDefinitions from "../queues.js";
|
|
876
|
+
` : "";
|
|
877
|
+
const queuesTypeBlock = hasQueues ? `
|
|
878
|
+
|
|
879
|
+
/** Message body type carried by a \`defineQueue\` definition (its phantom \`__lunoraBody\`). */
|
|
880
|
+
type QueueBodyOf<Definition> = Definition extends { __lunoraBody?: infer Body } ? (unknown extends Body ? unknown : NonNullable<Body>) : unknown;
|
|
881
|
+
|
|
882
|
+
/** This project's declared queues, addressable from \`ctx.queues\` by their \`lunora/queues.ts\` export name. */
|
|
883
|
+
export interface LunoraQueues {
|
|
884
|
+
${queues.map((queue) => ` readonly ${queue.exportName}: QueueProducer<QueueBodyOf<typeof lunoraQueueDefinitions.${queue.exportName}>>;`).join("\n")}
|
|
885
|
+
}` : "";
|
|
886
|
+
const queuesContextField = hasQueues ? `
|
|
887
|
+
readonly queues: LunoraQueues;` : "";
|
|
635
888
|
const server = `${GENERATED_HEADER}import { createPolicyDsl, initLunora, v as vBase } from "${base.server}";
|
|
636
889
|
import type {
|
|
637
890
|
ActionBuilder,
|
|
@@ -653,11 +906,11 @@ import type {
|
|
|
653
906
|
} from "${base.server}";
|
|
654
907
|
|
|
655
908
|
import type { DataModel, DatabaseReaderFacade, DatabaseWriterFacade, Doc, Id as IdOfTable, OrmReader, OrmWriter, Relations, TableName } from "./dataModel.js";
|
|
656
|
-
${aiTypeImport}${paymentsTypeImport}${containersTypeImport}${workflowsTypeImport}
|
|
909
|
+
${aiTypeImport}${paymentsTypeImport}${containersTypeImport}${workflowsTypeImport}${queuesTypeImport}
|
|
657
910
|
export type { DataModel, Doc, Id, TableName } from "./dataModel.js";
|
|
658
911
|
|
|
659
912
|
/** Storage buckets this schema declares (\`v.storage("name")\`), narrowing \`ctx.storage.bucket(name)\`. */
|
|
660
|
-
export type StorageBucketName = ${storageBucketUnion};${envBlock}${workflowsTypeBlock}
|
|
913
|
+
export type StorageBucketName = ${storageBucketUnion};${envBlock}${workflowsTypeBlock}${queuesTypeBlock}
|
|
661
914
|
|
|
662
915
|
/**
|
|
663
916
|
* Project-typed contexts. The base contexts from \`@lunora/server\` are
|
|
@@ -689,19 +942,19 @@ type TypedTableGet = <T extends TableName>(id: IdOfTable<T>) => Promise<Doc<T> |
|
|
|
689
942
|
export interface QueryCtx extends Omit<QueryCtxBase, "db" | "storage"> {
|
|
690
943
|
readonly db: Omit<DatabaseReader, "query" | "get"> & DatabaseReaderFacade & { query: TypedTableQuery; get: TypedTableGet };
|
|
691
944
|
readonly orm: OrmReader;
|
|
692
|
-
readonly storage: ReadOnlyStorage<StorageBucketName>;${kvContextField}${analyticsContextField}
|
|
945
|
+
readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}
|
|
693
946
|
}
|
|
694
947
|
|
|
695
948
|
export interface MutationCtx extends Omit<MutationCtxBase, "db" | "storage"${workflowsOmit}> {
|
|
696
949
|
readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
|
|
697
950
|
readonly orm: OrmWriter;
|
|
698
|
-
readonly storage: ReadOnlyStorage<StorageBucketName>;${kvContextField}${analyticsContextField}${workflowsContextField}
|
|
951
|
+
readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}${workflowsContextField}${queuesContextField}
|
|
699
952
|
}
|
|
700
953
|
|
|
701
954
|
export interface ActionCtx extends Omit<ActionCtxBase, "db" | "storage"${workflowsOmit}> {
|
|
702
955
|
readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
|
|
703
956
|
readonly orm: OrmWriter;
|
|
704
|
-
readonly storage: StorageBase<StorageBucketName>;${aiActionField}${paymentsActionField}${containersActionField}${kvContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${workflowsContextField}
|
|
957
|
+
readonly storage: StorageBase<StorageBucketName>;${accessContextField}${aiActionField}${paymentsActionField}${containersActionField}${kvContextField}${flagsContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${r2sqlActionField}${workflowsContextField}${queuesContextField}
|
|
705
958
|
}
|
|
706
959
|
|
|
707
960
|
/**
|
|
@@ -771,10 +1024,40 @@ const renderLifecycleManifest = (functions) => {
|
|
|
771
1024
|
}
|
|
772
1025
|
return manifest;
|
|
773
1026
|
};
|
|
774
|
-
const emitFunctions = (functions, migrations = []) => {
|
|
1027
|
+
const emitFunctions = (functions, migrations = [], useUmbrella = false, mutators = [], shapes = []) => {
|
|
775
1028
|
const hasFunctions = functions.length > 0;
|
|
776
|
-
const
|
|
1029
|
+
const base = baseSpecifiers(useUmbrella);
|
|
1030
|
+
const { dispatchBody, importBlock, installBlock, migrationBody, mutatorPaths, shapeBody } = renderFunctionRegistry(functions, migrations, mutators, shapes);
|
|
777
1031
|
const lifecycleHooks = renderLifecycleManifest(functions);
|
|
1032
|
+
const shapeTypeImport = shapes.length > 0 ? `import type { RegisteredShape } from "${base.server}";
|
|
1033
|
+
` : "";
|
|
1034
|
+
const shapeRegistry = shapes.length > 0 ? `
|
|
1035
|
+
/**
|
|
1036
|
+
* Replication-shape registry — one entry per \`defineShape\` in \`lunora/shapes.ts\`.
|
|
1037
|
+
* The generated ShardDO's \`resolveShape\` override looks a shape up by name and
|
|
1038
|
+
* evaluates its trusted \`compileWhere(ctx, args)\` to authorize + scope a
|
|
1039
|
+
* \`shape_subscribe\` (reads-as-permissions).
|
|
1040
|
+
*/
|
|
1041
|
+
export const LUNORA_SHAPES: Record<string, RegisteredShape> = {${shapeBody}};
|
|
1042
|
+
` : "";
|
|
1043
|
+
const mutatorPathsRegistry = mutatorPaths.length > 0 ? `
|
|
1044
|
+
/**
|
|
1045
|
+
* Custom-mutator function paths — the \`LUNORA_FUNCTIONS\` keys the generated
|
|
1046
|
+
* ShardDO's \`isCustomMutator\` override routes through the client-watermark push
|
|
1047
|
+
* protocol (\`x-lunora-client-id\`/\`x-lunora-client-seq\` ordering).
|
|
1048
|
+
*/
|
|
1049
|
+
export const LUNORA_MUTATOR_PATHS: ReadonlySet<string> = new Set([${mutatorPaths.map((path) => JSON.stringify(path)).join(", ")}]);
|
|
1050
|
+
` : "";
|
|
1051
|
+
const compiledArgsImport = installBlock.length > 0 ? `import { DEFER_VALIDATION as DEFER, installCompiledValidatorMap } from "${base.values}";
|
|
1052
|
+
` : "";
|
|
1053
|
+
const compiledArgsInstall = installBlock.length > 0 ? `
|
|
1054
|
+
/**
|
|
1055
|
+
* AOT-compiled argument validators (Worker-safe, no \`eval\`). Each is installed
|
|
1056
|
+
* onto its function's live \`.args\` object and consulted by the interpreted
|
|
1057
|
+
* parser as a zero-allocation fast path; anything it can't model is deferred.
|
|
1058
|
+
*/
|
|
1059
|
+
${installBlock}
|
|
1060
|
+
` : "";
|
|
778
1061
|
const caller = renderCaller(functions);
|
|
779
1062
|
const callerTypes = caller.types ? `
|
|
780
1063
|
${caller.types}
|
|
@@ -789,7 +1072,7 @@ ${caller.implementation}
|
|
|
789
1072
|
const callRegisteredHelper = hasFunctions ? `${CALL_REGISTERED_HELPER}
|
|
790
1073
|
|
|
791
1074
|
` : "";
|
|
792
|
-
return `${GENERATED_HEADER}${importBlock}import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
|
|
1075
|
+
return `${GENERATED_HEADER}${importBlock}${compiledArgsImport}${shapeTypeImport}import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
|
|
793
1076
|
${dataModelImport}
|
|
794
1077
|
/**
|
|
795
1078
|
* Single registered function, narrowed to the shape \`handleRpc\` needs.
|
|
@@ -815,7 +1098,7 @@ export interface RegisteredLunoraFunction {
|
|
|
815
1098
|
* emits (\`api[namespace][fn].__lunoraRef === "namespace:fn"\`).
|
|
816
1099
|
*/
|
|
817
1100
|
export const LUNORA_FUNCTIONS: Record<string, RegisteredLunoraFunction> = {${dispatchBody}};
|
|
818
|
-
|
|
1101
|
+
${compiledArgsInstall}${shapeRegistry}${mutatorPathsRegistry}
|
|
819
1102
|
/**
|
|
820
1103
|
* Connection-lifecycle manifest: the function paths the generated ShardDO
|
|
821
1104
|
* dispatches when a client's WebSocket connects (\`connect\`) or disconnects
|
|
@@ -1036,7 +1319,7 @@ const emitKvFragments = (hasKv) => {
|
|
|
1036
1319
|
kv?: (env: Record<string, unknown>) => KVNamespaceLike;`,
|
|
1037
1320
|
contextField: `
|
|
1038
1321
|
kv,`,
|
|
1039
|
-
importLines: [`import type { Kv, KVNamespaceLike } from "@lunora/kv";`, `import { createKv } from "@lunora/kv";`],
|
|
1322
|
+
importLines: [`import type { Kv, KVNamespaceLike } from "@lunora/bindings/kv";`, `import { createKv } from "@lunora/bindings/kv";`],
|
|
1040
1323
|
stub: `
|
|
1041
1324
|
const kvStub: Kv = {
|
|
1042
1325
|
delete: async () => {
|
|
@@ -1061,6 +1344,113 @@ const kvStub: Kv = {
|
|
|
1061
1344
|
`
|
|
1062
1345
|
};
|
|
1063
1346
|
};
|
|
1347
|
+
const emitFlagsFragments = (hasFlags) => {
|
|
1348
|
+
if (!hasFlags) {
|
|
1349
|
+
return EMPTY_HELPER_FRAGMENTS;
|
|
1350
|
+
}
|
|
1351
|
+
return {
|
|
1352
|
+
build: `
|
|
1353
|
+
const flags: import("@lunora/flags").LunoraFlags = createFlags({
|
|
1354
|
+
hooks: flagsConfig.hooks,
|
|
1355
|
+
logger: flagsConfig.logger,
|
|
1356
|
+
provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
|
|
1357
|
+
targetingKey: () => flagsConfig.identify?.({ identity: identity ?? null, userId: userId ?? null }),
|
|
1358
|
+
});
|
|
1359
|
+
`,
|
|
1360
|
+
configField: `
|
|
1361
|
+
flags?: (env: Record<string, unknown>) => import("@lunora/flags").Provider;`,
|
|
1362
|
+
contextField: `
|
|
1363
|
+
flags,`,
|
|
1364
|
+
importLines: [`import { createFlags } from "@lunora/flags";`, `import flagsConfig from "../flags.js";`],
|
|
1365
|
+
stub: ""
|
|
1366
|
+
};
|
|
1367
|
+
};
|
|
1368
|
+
const emitAccessFragments = (hasAccessFacade) => {
|
|
1369
|
+
if (!hasAccessFacade) {
|
|
1370
|
+
return EMPTY_HELPER_FRAGMENTS;
|
|
1371
|
+
}
|
|
1372
|
+
return {
|
|
1373
|
+
build: `
|
|
1374
|
+
const access = accessFacade(identity, userId);
|
|
1375
|
+
`,
|
|
1376
|
+
configField: "",
|
|
1377
|
+
contextField: `
|
|
1378
|
+
access,`,
|
|
1379
|
+
importLines: [`import { accessFacade } from "@lunora/cloudflare-access/context";`],
|
|
1380
|
+
stub: ""
|
|
1381
|
+
};
|
|
1382
|
+
};
|
|
1383
|
+
const emitFlagsOverrides = (flagKeys, hasFlags) => {
|
|
1384
|
+
if (!hasFlags) {
|
|
1385
|
+
return { constant: "", evaluateOverride: "", subscriptionOverride: "" };
|
|
1386
|
+
}
|
|
1387
|
+
const clientBuild = (targetingKey) => `
|
|
1388
|
+
const env = (this.env ?? {}) as Record<string, unknown>;
|
|
1389
|
+
const flags: import("@lunora/flags").LunoraFlags = createFlags({
|
|
1390
|
+
hooks: flagsConfig.hooks,
|
|
1391
|
+
logger: flagsConfig.logger,
|
|
1392
|
+
provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
|
|
1393
|
+
targetingKey: ${targetingKey},
|
|
1394
|
+
});`;
|
|
1395
|
+
const constant = `
|
|
1396
|
+
/** Statically-discovered feature flags (\`ctx.flags.<type>("key")\` reads) served via \`__lunora_admin__:listFlags\` + the reactive \`__lunora_flags__:\` channel. */
|
|
1397
|
+
const LUNORA_FLAG_KEYS: ReadonlyArray<{ key: string; type: "boolean" | "number" | "object" | "string" }> = ${JSON.stringify(flagKeys, void 0, 4)};
|
|
1398
|
+
`;
|
|
1399
|
+
const evaluateOverride = `
|
|
1400
|
+
protected override async evaluateFlags(context?: Record<string, unknown>): Promise<FlagsResult> {${clientBuild("undefined")}
|
|
1401
|
+
const evalContext = context as import("@lunora/flags").EvaluationContext | undefined;
|
|
1402
|
+
const evaluations: FlagsResult["flags"] = [];
|
|
1403
|
+
|
|
1404
|
+
for (const entry of LUNORA_FLAG_KEYS) {
|
|
1405
|
+
// eslint-disable-next-line no-await-in-loop -- flags evaluate sequentially; each shares the single memoized provider client
|
|
1406
|
+
const details =
|
|
1407
|
+
entry.type === "boolean"
|
|
1408
|
+
? await flags.details.boolean(entry.key, false, evalContext)
|
|
1409
|
+
: entry.type === "number"
|
|
1410
|
+
? await flags.details.number(entry.key, 0, evalContext)
|
|
1411
|
+
: entry.type === "string"
|
|
1412
|
+
? await flags.details.string(entry.key, "", evalContext)
|
|
1413
|
+
: await flags.details.object(entry.key, {}, evalContext);
|
|
1414
|
+
|
|
1415
|
+
evaluations.push({ errorCode: details.errorCode, key: entry.key, reason: details.reason, type: entry.type, value: details.value, variant: details.variant });
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
return { configured: true, flags: evaluations };
|
|
1419
|
+
}
|
|
1420
|
+
`;
|
|
1421
|
+
const subscriptionOverride = `
|
|
1422
|
+
protected override runFlagSubscriptionRead(_functionPath: string, args: Record<string, unknown>, identity?: { identity?: Record<string, unknown>; userId?: string }): Promise<unknown> {${clientBuild("() => flagsConfig.identify?.({ identity: identity?.identity ?? null, userId: identity?.userId ?? null })")}
|
|
1423
|
+
const key = typeof args.key === "string" ? args.key : "";
|
|
1424
|
+
const rawContext = typeof args.context === "object" && args.context !== null ? (args.context as Record<string, unknown>) : undefined;
|
|
1425
|
+
// The reactive channel is public (any socket, no auth required): never
|
|
1426
|
+
// let a subscriber-supplied targetingKey override the verified identity
|
|
1427
|
+
// resolved above, or a client could read another user's flags. Other
|
|
1428
|
+
// (non-identity) targeting attributes pass through unchanged.
|
|
1429
|
+
const { targetingKey: _clientTargetingKey, ...safeContext } = rawContext ?? {};
|
|
1430
|
+
const context = (rawContext === undefined ? undefined : safeContext) as import("@lunora/flags").EvaluationContext | undefined;
|
|
1431
|
+
|
|
1432
|
+
// eslint-disable-next-line unicorn/no-null -- the base hook's "nothing to deliver" sentinel
|
|
1433
|
+
if (key.length === 0) {
|
|
1434
|
+
return Promise.resolve(null);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
if (args.type === "number") {
|
|
1438
|
+
return flags.number(key, typeof args.default === "number" ? args.default : 0, context);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
if (args.type === "string") {
|
|
1442
|
+
return flags.string(key, typeof args.default === "string" ? args.default : "", context);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
if (args.type === "object") {
|
|
1446
|
+
return flags.object(key, (args.default ?? {}) as import("@lunora/flags").JsonValue, context);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
return flags.boolean(key, typeof args.default === "boolean" ? args.default : false, context);
|
|
1450
|
+
}
|
|
1451
|
+
`;
|
|
1452
|
+
return { constant, evaluateOverride, subscriptionOverride };
|
|
1453
|
+
};
|
|
1064
1454
|
const emitAnalyticsFragments = (hasAnalytics) => {
|
|
1065
1455
|
if (!hasAnalytics) {
|
|
1066
1456
|
return EMPTY_HELPER_FRAGMENTS;
|
|
@@ -1076,8 +1466,8 @@ const emitAnalyticsFragments = (hasAnalytics) => {
|
|
|
1076
1466
|
contextField: `
|
|
1077
1467
|
analytics,`,
|
|
1078
1468
|
importLines: [
|
|
1079
|
-
`import type { AnalyticsClient, AnalyticsEngineDatasetLike } from "@lunora/analytics";`,
|
|
1080
|
-
`import { createAnalytics } from "@lunora/analytics";`
|
|
1469
|
+
`import type { AnalyticsClient, AnalyticsEngineDatasetLike } from "@lunora/bindings/analytics";`,
|
|
1470
|
+
`import { createAnalytics } from "@lunora/bindings/analytics";`
|
|
1081
1471
|
],
|
|
1082
1472
|
stub: `
|
|
1083
1473
|
const analyticsStub: AnalyticsClient = {
|
|
@@ -1106,7 +1496,7 @@ const emitImagesFragments = (hasImages) => {
|
|
|
1106
1496
|
// ActionCtx-only: woven onto the action ctx object, never query/mutation.
|
|
1107
1497
|
contextField: `
|
|
1108
1498
|
images,`,
|
|
1109
|
-
importLines: [`import type { Images, ImagesBindingLike } from "@lunora/images";`, `import { createImages } from "@lunora/images";`],
|
|
1499
|
+
importLines: [`import type { Images, ImagesBindingLike } from "@lunora/bindings/images";`, `import { createImages } from "@lunora/bindings/images";`],
|
|
1110
1500
|
stub: `
|
|
1111
1501
|
const imagesStub: Images = {
|
|
1112
1502
|
info: async () => {
|
|
@@ -1183,17 +1573,96 @@ const browserStub: Browser = {
|
|
|
1183
1573
|
`
|
|
1184
1574
|
};
|
|
1185
1575
|
};
|
|
1186
|
-
const
|
|
1576
|
+
const emitR2sqlFragments = (hasR2sql) => {
|
|
1577
|
+
if (!hasR2sql) {
|
|
1578
|
+
return EMPTY_HELPER_FRAGMENTS;
|
|
1579
|
+
}
|
|
1580
|
+
const r2sqlMissing = `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().");`;
|
|
1581
|
+
return {
|
|
1582
|
+
build: `
|
|
1583
|
+
const r2sqlEnv = env as Record<string, unknown>;
|
|
1584
|
+
const r2sqlAccountId = (r2sqlEnv.R2_SQL_ACCOUNT_ID ?? r2sqlEnv.CLOUDFLARE_ACCOUNT_ID) as string | undefined;
|
|
1585
|
+
const r2sqlToken = r2sqlEnv.R2_SQL_TOKEN as string | undefined;
|
|
1586
|
+
const r2sqlBucket = r2sqlEnv.R2_SQL_BUCKET as string | undefined;
|
|
1587
|
+
const r2sql: R2SqlClient = config.r2sql
|
|
1588
|
+
? config.r2sql(env)
|
|
1589
|
+
: r2sqlAccountId && r2sqlToken && r2sqlBucket
|
|
1590
|
+
? createR2Sql({ accountId: r2sqlAccountId, apiToken: r2sqlToken, bucket: r2sqlBucket })
|
|
1591
|
+
: r2sqlStub;
|
|
1592
|
+
`,
|
|
1593
|
+
configField: `
|
|
1594
|
+
r2sql?: (env: Record<string, unknown>) => R2SqlClient;`,
|
|
1595
|
+
// ActionCtx-only: attached via the \`ctx.r2sql = r2sql\` assignment in the
|
|
1596
|
+
// \`isAction\` block, never the every-ctx object literal.
|
|
1597
|
+
contextField: "",
|
|
1598
|
+
importLines: [`import type { R2SqlClient } from "@lunora/bindings/r2sql";`, `import { createR2Sql } from "@lunora/bindings/r2sql";`],
|
|
1599
|
+
// The stub is typed `R2SqlClient`, so TS flags a missing method at build
|
|
1600
|
+
// time — but it must stay structurally in sync with that interface
|
|
1601
|
+
// (`@lunora/bindings/r2sql` client.ts) when a method is added there.
|
|
1602
|
+
stub: `
|
|
1603
|
+
const r2sqlStub: R2SqlClient = {
|
|
1604
|
+
describe: async () => {
|
|
1605
|
+
${r2sqlMissing}
|
|
1606
|
+
},
|
|
1607
|
+
explain: async () => {
|
|
1608
|
+
${r2sqlMissing}
|
|
1609
|
+
},
|
|
1610
|
+
from: () => {
|
|
1611
|
+
${r2sqlMissing}
|
|
1612
|
+
},
|
|
1613
|
+
query: async () => {
|
|
1614
|
+
${r2sqlMissing}
|
|
1615
|
+
},
|
|
1616
|
+
showDatabases: async () => {
|
|
1617
|
+
${r2sqlMissing}
|
|
1618
|
+
},
|
|
1619
|
+
showTables: async () => {
|
|
1620
|
+
${r2sqlMissing}
|
|
1621
|
+
},
|
|
1622
|
+
};
|
|
1623
|
+
`
|
|
1624
|
+
};
|
|
1625
|
+
};
|
|
1626
|
+
const emitPipelinesFragments = (hasPipelines) => {
|
|
1627
|
+
if (!hasPipelines) {
|
|
1628
|
+
return EMPTY_HELPER_FRAGMENTS;
|
|
1629
|
+
}
|
|
1630
|
+
const pipelinesMissing = `throw new Error("ctx.pipelines: no Pipelines binding found. Add a \\\`pipelines\\\` binding (env.PIPELINES) to wrangler.jsonc, or pass \\\`pipelines\\\` to createShardDO().");`;
|
|
1631
|
+
return {
|
|
1632
|
+
build: `
|
|
1633
|
+
const pipelinesBinding = config.pipelines?.(env) ?? (env as Record<string, unknown>).PIPELINES;
|
|
1634
|
+
const pipelines: PipelineClient = pipelinesBinding ? createPipelines({ binding: pipelinesBinding as PipelineBindingLike }) : pipelinesStub;
|
|
1635
|
+
`,
|
|
1636
|
+
configField: `
|
|
1637
|
+
pipelines?: (env: Record<string, unknown>) => PipelineBindingLike;`,
|
|
1638
|
+
// ActionCtx-only: attached via the \`ctx.pipelines = pipelines\` assignment
|
|
1639
|
+
// in the \`isAction\` block, never the every-ctx object literal.
|
|
1640
|
+
contextField: "",
|
|
1641
|
+
importLines: [
|
|
1642
|
+
`import type { PipelineBindingLike, PipelineClient } from "@lunora/bindings/pipelines";`,
|
|
1643
|
+
`import { createPipelines } from "@lunora/bindings/pipelines";`
|
|
1644
|
+
],
|
|
1645
|
+
stub: `
|
|
1646
|
+
const pipelinesStub: PipelineClient = {
|
|
1647
|
+
send: async () => {
|
|
1648
|
+
${pipelinesMissing}
|
|
1649
|
+
},
|
|
1650
|
+
};
|
|
1651
|
+
`
|
|
1652
|
+
};
|
|
1653
|
+
};
|
|
1654
|
+
const emitContainers = (containers, jurisdiction) => {
|
|
1187
1655
|
if (containers.length === 0) {
|
|
1188
1656
|
return "";
|
|
1189
1657
|
}
|
|
1658
|
+
const jurisdictionArgument = jurisdiction ? `, ${JSON.stringify(jurisdiction)}` : "";
|
|
1190
1659
|
const classes = containers.map((container) => {
|
|
1191
1660
|
assertIdentifier(container.exportName, `container export "${container.exportName}"`);
|
|
1192
1661
|
assertIdentifier(container.className, `container class "${container.className}"`);
|
|
1193
1662
|
return `/** Container DO for the \`${container.exportName}\` definition (binding \`${container.bindingName}\`). */
|
|
1194
1663
|
export class ${container.className} extends LunoraContainer {
|
|
1195
1664
|
public constructor(ctx: ConstructorParameters<typeof LunoraContainer>[0], env: Record<string, unknown>) {
|
|
1196
|
-
super(ctx, env, ${container.exportName}, "${container.exportName}");
|
|
1665
|
+
super(ctx, env, ${container.exportName}, "${container.exportName}"${jurisdictionArgument});
|
|
1197
1666
|
}
|
|
1198
1667
|
}
|
|
1199
1668
|
`;
|
|
@@ -1205,14 +1674,21 @@ export class ${container.className} extends LunoraContainer {
|
|
|
1205
1674
|
* requires each \`containers[].class_name\` to be exported by the worker:
|
|
1206
1675
|
*
|
|
1207
1676
|
* \`export * from "./lunora/_generated/containers.js";\`
|
|
1677
|
+
*
|
|
1678
|
+
* \`ContainerProxy\` is re-exported alongside them: the egress-interception path
|
|
1679
|
+
* (\`allowedHosts\`/\`deniedHosts\`/\`interceptHttps\` and the runtime
|
|
1680
|
+
* \`handle.egress\` controls) routes container outbound traffic through this
|
|
1681
|
+
* WorkerEntrypoint, so it too must be exported by the deployed worker.
|
|
1208
1682
|
*/
|
|
1209
|
-
import LunoraContainer from "@lunora/container/do";
|
|
1683
|
+
import { LunoraContainer } from "@lunora/container/do";
|
|
1210
1684
|
|
|
1211
1685
|
import { ${imports} } from "../containers.js";
|
|
1212
1686
|
|
|
1687
|
+
export { ContainerProxy } from "@lunora/container/do";
|
|
1688
|
+
|
|
1213
1689
|
${classes}`;
|
|
1214
1690
|
};
|
|
1215
|
-
const emitContainerFragments = (containers) => {
|
|
1691
|
+
const emitContainerFragments = (containers, jurisdiction) => {
|
|
1216
1692
|
if (containers.length === 0) {
|
|
1217
1693
|
return { build: "", contextField: "", importLines: [], specs: "" };
|
|
1218
1694
|
}
|
|
@@ -1225,8 +1701,11 @@ const emitContainerFragments = (containers) => {
|
|
|
1225
1701
|
return ` { binding: "${container.bindingName}", exportName: "${container.exportName}"${maxInstances} },`;
|
|
1226
1702
|
}).join("\n");
|
|
1227
1703
|
return {
|
|
1704
|
+
// Schema `.jurisdiction("…")` pins every container DO this shard reaches
|
|
1705
|
+
// to the data-residency region; the arg is omitted when undeclared so
|
|
1706
|
+
// existing generated output is unchanged.
|
|
1228
1707
|
build: `
|
|
1229
|
-
const containers = createContainerContext(env, LUNORA_CONTAINERS);
|
|
1708
|
+
const containers = createContainerContext(env, LUNORA_CONTAINERS${jurisdiction ? `, ${JSON.stringify(jurisdiction)}` : ""});
|
|
1230
1709
|
`,
|
|
1231
1710
|
contextField: `
|
|
1232
1711
|
containers,`,
|
|
@@ -1279,6 +1758,32 @@ type WorkflowOutputOf<Definition> = Definition extends { __output?: infer Output
|
|
|
1279
1758
|
|
|
1280
1759
|
${classes}`;
|
|
1281
1760
|
};
|
|
1761
|
+
const emitQueues = (queues) => {
|
|
1762
|
+
const pushQueues = queues.filter((queue) => queue.mode === "push");
|
|
1763
|
+
if (pushQueues.length === 0) {
|
|
1764
|
+
return "";
|
|
1765
|
+
}
|
|
1766
|
+
for (const queue of pushQueues) {
|
|
1767
|
+
assertIdentifier(queue.exportName, `queue export "${queue.exportName}"`);
|
|
1768
|
+
}
|
|
1769
|
+
const imports = pushQueues.map((queue) => queue.exportName).join(", ");
|
|
1770
|
+
const entries = pushQueues.map((queue) => ` ${JSON.stringify(queue.name)}: { definition: ${queue.exportName}, exportName: ${JSON.stringify(queue.exportName)} },`).join("\n");
|
|
1771
|
+
return `${GENERATED_HEADER}/**
|
|
1772
|
+
* Push-consumer registry for the queues declared in \`lunora/queues.ts\`. The
|
|
1773
|
+
* composed worker's \`queue(batch, env, ctx)\` entry routes each delivered batch
|
|
1774
|
+
* by \`batch.queue\` to the matching \`defineQueue\` handler. Wired automatically
|
|
1775
|
+
* by \`defineApp\` — you don't import this directly.
|
|
1776
|
+
*/
|
|
1777
|
+
import type { QueueRegistry } from "@lunora/queue";
|
|
1778
|
+
|
|
1779
|
+
import { ${imports} } from "../queues.js";
|
|
1780
|
+
|
|
1781
|
+
/** Stable wrangler queue name → { definition, exportName } for batch routing. */
|
|
1782
|
+
export const LUNORA_QUEUE_REGISTRY: QueueRegistry = {
|
|
1783
|
+
${entries}
|
|
1784
|
+
};
|
|
1785
|
+
`;
|
|
1786
|
+
};
|
|
1282
1787
|
const emitWorkflowFragments = (workflows) => {
|
|
1283
1788
|
if (workflows.length === 0) {
|
|
1284
1789
|
return { build: "", contextField: "", importLines: [], specs: "" };
|
|
@@ -1304,6 +1809,31 @@ ${specEntries}
|
|
|
1304
1809
|
`
|
|
1305
1810
|
};
|
|
1306
1811
|
};
|
|
1812
|
+
const emitQueueFragments = (queues) => {
|
|
1813
|
+
if (queues.length === 0) {
|
|
1814
|
+
return { build: "", contextField: "", importLines: [], specs: "" };
|
|
1815
|
+
}
|
|
1816
|
+
for (const queue of queues) {
|
|
1817
|
+
assertIdentifier(queue.exportName, `queue export "${queue.exportName}"`);
|
|
1818
|
+
assertIdentifier(queue.bindingName, `queue binding "${queue.bindingName}"`);
|
|
1819
|
+
}
|
|
1820
|
+
const specEntries = queues.map((queue) => ` { binding: "${queue.bindingName}", exportName: "${queue.exportName}", name: ${JSON.stringify(queue.name)} },`).join("\n");
|
|
1821
|
+
return {
|
|
1822
|
+
build: `
|
|
1823
|
+
const queues = createQueueContext(env, LUNORA_QUEUES);
|
|
1824
|
+
`,
|
|
1825
|
+
contextField: `
|
|
1826
|
+
queues,`,
|
|
1827
|
+
importLines: [`import type { QueueBindingSpec } from "@lunora/queue";`, `import { createQueueContext } from "@lunora/queue";`],
|
|
1828
|
+
// eslint-disable-next-line no-secrets/no-secrets -- the emitted readonly-array type annotation is dense generated TS, not a credential
|
|
1829
|
+
specs: `
|
|
1830
|
+
/** Wiring specs for \`ctx.queues\` (codegen-derived from \`lunora/queues.ts\`). */
|
|
1831
|
+
const LUNORA_QUEUES: ReadonlyArray<QueueBindingSpec> = [
|
|
1832
|
+
${specEntries}
|
|
1833
|
+
];
|
|
1834
|
+
`
|
|
1835
|
+
};
|
|
1836
|
+
};
|
|
1307
1837
|
const emitWorkflowsMetadataFragments = (workflows) => {
|
|
1308
1838
|
if (workflows.length === 0) {
|
|
1309
1839
|
return { constant: "", override: "" };
|
|
@@ -1330,6 +1860,33 @@ const LUNORA_WORKFLOWS_INFO: WorkflowsResult = ${JSON.stringify(metadata, void 0
|
|
|
1330
1860
|
`
|
|
1331
1861
|
};
|
|
1332
1862
|
};
|
|
1863
|
+
const emitQueuesMetadataFragments = (queues) => {
|
|
1864
|
+
if (queues.length === 0) {
|
|
1865
|
+
return { constant: "", override: "" };
|
|
1866
|
+
}
|
|
1867
|
+
const metadata = {
|
|
1868
|
+
queues: queues.map((queue) => {
|
|
1869
|
+
return {
|
|
1870
|
+
binding: queue.bindingName,
|
|
1871
|
+
...queue.tuning.deadLetterQueue === void 0 ? {} : { deadLetterQueue: queue.tuning.deadLetterQueue },
|
|
1872
|
+
exportName: queue.exportName,
|
|
1873
|
+
mode: queue.mode,
|
|
1874
|
+
name: queue.name
|
|
1875
|
+
};
|
|
1876
|
+
})
|
|
1877
|
+
};
|
|
1878
|
+
return {
|
|
1879
|
+
constant: `
|
|
1880
|
+
/** Read-only declared-queue metadata (discovered from \`lunora/queues.ts\`) served via \`__lunora_admin__:listQueues\` for the studio's queues view. */
|
|
1881
|
+
const LUNORA_QUEUES_INFO: QueuesResult = ${JSON.stringify(metadata, void 0, 4)};
|
|
1882
|
+
`,
|
|
1883
|
+
override: `
|
|
1884
|
+
protected override queuesMetadata(): QueuesResult {
|
|
1885
|
+
return LUNORA_QUEUES_INFO;
|
|
1886
|
+
}
|
|
1887
|
+
`
|
|
1888
|
+
};
|
|
1889
|
+
};
|
|
1333
1890
|
const emitPaymentFragments = (hasPayments) => {
|
|
1334
1891
|
if (!hasPayments) {
|
|
1335
1892
|
return { build: "", configField: "", contextField: "", imports: [], stub: "" };
|
|
@@ -1385,13 +1942,15 @@ const paymentStub: LunoraPayment = {
|
|
|
1385
1942
|
`
|
|
1386
1943
|
};
|
|
1387
1944
|
};
|
|
1388
|
-
const buildDoTypeImports = (hasVectors, hasWorkflows) => [
|
|
1945
|
+
const buildDoTypeImports = (hasVectors, hasWorkflows, hasQueues, hasFlags) => [
|
|
1389
1946
|
"AdvisoryFinding",
|
|
1390
1947
|
"DatabaseWriterLike",
|
|
1391
1948
|
"DataMigrationLike",
|
|
1949
|
+
...hasFlags ? ["FlagsResult"] : [],
|
|
1392
1950
|
"LogSink",
|
|
1393
1951
|
"MaskPoliciesResult",
|
|
1394
1952
|
"MigrationRunResult",
|
|
1953
|
+
...hasQueues ? ["QueuesResult"] : [],
|
|
1395
1954
|
"RunShardApplyCdcArgs",
|
|
1396
1955
|
"RunShardMigrationArgs",
|
|
1397
1956
|
"RlsPoliciesResult",
|
|
@@ -1413,34 +1972,50 @@ const buildDoTypeImports = (hasVectors, hasWorkflows) => [
|
|
|
1413
1972
|
const emitShard = ({
|
|
1414
1973
|
advisories = [],
|
|
1415
1974
|
containers = [],
|
|
1975
|
+
flagKeys = [],
|
|
1976
|
+
hasAccessFacade = false,
|
|
1416
1977
|
hasAi = false,
|
|
1417
1978
|
hasAnalytics = false,
|
|
1418
1979
|
hasBrowser = false,
|
|
1980
|
+
hasFlags = false,
|
|
1419
1981
|
hasHyperdrive = false,
|
|
1420
1982
|
hasImages = false,
|
|
1421
1983
|
hasKv = false,
|
|
1422
1984
|
hasPayments = false,
|
|
1985
|
+
hasPipelines = false,
|
|
1986
|
+
hasR2sql = false,
|
|
1423
1987
|
maskMetadata,
|
|
1988
|
+
mutators = [],
|
|
1989
|
+
queues = [],
|
|
1424
1990
|
rlsMetadata,
|
|
1425
1991
|
schema,
|
|
1992
|
+
shapes = [],
|
|
1426
1993
|
storageRules,
|
|
1427
1994
|
studioFeatures,
|
|
1428
1995
|
useUmbrella = false,
|
|
1429
1996
|
workflows = []
|
|
1430
1997
|
}) => {
|
|
1431
1998
|
const base = baseSpecifiers(useUmbrella);
|
|
1999
|
+
const hasMutators = mutators.length > 0;
|
|
2000
|
+
const hasShapes = shapes.length > 0;
|
|
1432
2001
|
const { build: aiBuild, configField: aiConfigField, contextField: aiContextField, stub: aiStub } = emitAiFragments(hasAi);
|
|
2002
|
+
const accessFragments = emitAccessFragments(hasAccessFacade);
|
|
1433
2003
|
const kvFragments = emitKvFragments(hasKv);
|
|
2004
|
+
const flagsFragments = emitFlagsFragments(hasFlags);
|
|
2005
|
+
const flagsOverrides = emitFlagsOverrides(flagKeys, hasFlags);
|
|
1434
2006
|
const analyticsFragments = emitAnalyticsFragments(hasAnalytics);
|
|
1435
2007
|
const imagesFragments = emitImagesFragments(hasImages);
|
|
1436
2008
|
const hyperdriveFragments = emitHyperdriveFragments(hasHyperdrive);
|
|
1437
2009
|
const browserFragments = emitBrowserFragments(hasBrowser);
|
|
2010
|
+
const r2sqlFragments = emitR2sqlFragments(hasR2sql);
|
|
2011
|
+
const pipelinesFragments = emitPipelinesFragments(hasPipelines);
|
|
2012
|
+
const { build: queuesBuild, contextField: queuesContextField, importLines: queueImportLines, specs: queueSpecs } = emitQueueFragments(queues);
|
|
1438
2013
|
const {
|
|
1439
2014
|
build: containersBuild,
|
|
1440
2015
|
contextField: containersContextField,
|
|
1441
2016
|
importLines: containerImportLines,
|
|
1442
2017
|
specs: containerSpecs
|
|
1443
|
-
} = emitContainerFragments(containers);
|
|
2018
|
+
} = emitContainerFragments(containers, schema.jurisdiction);
|
|
1444
2019
|
const {
|
|
1445
2020
|
build: workflowsBuild,
|
|
1446
2021
|
contextField: workflowsContextField,
|
|
@@ -1459,18 +2034,22 @@ const emitShard = ({
|
|
|
1459
2034
|
const maskData = maskMetadata ?? { columns: [] };
|
|
1460
2035
|
const storageRulesData = storageRules ?? { rules: [] };
|
|
1461
2036
|
const studioFeaturesData = studioFeatures ?? {
|
|
2037
|
+
flags: false,
|
|
1462
2038
|
mail: false,
|
|
1463
2039
|
payments: false,
|
|
2040
|
+
queues: false,
|
|
1464
2041
|
scheduler: false,
|
|
1465
2042
|
storage: false,
|
|
1466
2043
|
vectors: false,
|
|
1467
2044
|
workflows: false
|
|
1468
2045
|
};
|
|
1469
2046
|
const { constant: workflowsMetadataConst, override: workflowsMetadataOverride } = emitWorkflowsMetadataFragments(workflows);
|
|
2047
|
+
const { constant: queuesMetadataConst, override: queuesMetadataOverride } = emitQueuesMetadataFragments(queues);
|
|
1470
2048
|
const hasVectors = schema.vectorIndexes.length > 0;
|
|
1471
2049
|
const hasGlobalTables = schema.tables.some((table) => table.shardMode === "global");
|
|
1472
2050
|
const hasHyperdriveGlobal = schema.tables.some((table) => table.shardMode === "global" && table.globalBackend === "hyperdrive");
|
|
1473
2051
|
const hasD1Global = schema.tables.some((table) => table.shardMode === "global" && table.globalBackend !== "hyperdrive");
|
|
2052
|
+
const hasSourcedTables = schema.tables.some((table) => table.externalSource !== void 0);
|
|
1474
2053
|
if (hasD1Global && hasHyperdriveGlobal) {
|
|
1475
2054
|
throw new Error(
|
|
1476
2055
|
'lunora codegen: mixing `.global()` (D1) and `.global({ backend: "hyperdrive" })` tables in one app is not supported yet — use a single global backend.'
|
|
@@ -1481,40 +2060,113 @@ const emitShard = ({
|
|
|
1481
2060
|
const tableIndexes = buildTableIndexes(schema);
|
|
1482
2061
|
const tableColumns = buildTableColumns(schema);
|
|
1483
2062
|
const storageColumns = buildStorageColumns(schema);
|
|
1484
|
-
const doTypeImports = buildDoTypeImports(hasVectors, workflows.length > 0);
|
|
2063
|
+
const doTypeImports = buildDoTypeImports(hasVectors, workflows.length > 0, queues.length > 0, hasFlags);
|
|
2064
|
+
if (hasShapes) {
|
|
2065
|
+
doTypeImports.push("WhereInput");
|
|
2066
|
+
}
|
|
1485
2067
|
const relationFanout = emitRelationFanout(hasGlobalTables);
|
|
2068
|
+
const shapeResolveOverride = hasShapes ? `
|
|
2069
|
+
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 {
|
|
2070
|
+
const shape = LUNORA_SHAPES[name];
|
|
2071
|
+
|
|
2072
|
+
if (!shape) {
|
|
2073
|
+
return undefined;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
this.ensureMigrated();
|
|
2077
|
+
|
|
2078
|
+
// Trusted ctx from the socket's OWN verified identity — the client
|
|
2079
|
+
// supplies only the shape name + args; \`compileWhere\` validates the
|
|
2080
|
+
// args then runs the shape's \`where(ctx, args)\` under that identity,
|
|
2081
|
+
// so which rows replicate is a server decision (reads-as-permissions).
|
|
2082
|
+
const ctx = this.buildCtx({ functionPath: \`__shape__:\${name}\`, identity });
|
|
2083
|
+
const shapeWhere = shape.compileWhere(ctx, args) as unknown as WhereInput;
|
|
2084
|
+
|
|
2085
|
+
// AND-compose with the table's RLS read base-where. A shape runs no
|
|
2086
|
+
// procedure, so the \`.use(rls(...))\` middleware never fires; without
|
|
2087
|
+
// this merge its reads would bypass every read policy on the table
|
|
2088
|
+
// (rows the caller can't see would replicate). \`composeShapeReadWhere\`
|
|
2089
|
+
// evaluates the table's read policies under this same trusted ctx —
|
|
2090
|
+
// exactly the request-time path — and fails closed under a
|
|
2091
|
+
// \`.rls("required")\` schema for a non-\`.public()\`, policy-less table.
|
|
2092
|
+
const effectiveWhere = composeShapeReadWhere(LUNORA_RLS_READ_REGISTRY, {
|
|
2093
|
+
ctx,
|
|
2094
|
+
identity: identity?.identity ?? null,
|
|
2095
|
+
rlsRequired: (schema as unknown as { rlsMode?: string }).rlsMode === "required",
|
|
2096
|
+
roles: (ctx as { auth?: { roles?: readonly string[] } }).auth?.roles ?? [],
|
|
2097
|
+
shapeWhere,
|
|
2098
|
+
table: shape.table,
|
|
2099
|
+
tablePublic: (schema as unknown as { tables: Record<string, { isPublic?: boolean }> }).tables[shape.table]?.isPublic === true,
|
|
2100
|
+
userId: identity?.userId ?? null,
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
// A live shape pokes only from its OWN shard's op-log, so a \`where()\`
|
|
2104
|
+
// that joins to a \`.shardBy()\` table (rows in another DO) is rejected
|
|
2105
|
+
// here — the first point the compiled predicate + shard modes are both
|
|
2106
|
+
// known. Remedy: denormalize, or move the joined table to \`.global()\`.
|
|
2107
|
+
assertShapeShardable(effectiveWhere, schema as unknown as SchemaLike, shape.table);
|
|
2108
|
+
|
|
2109
|
+
// A \`.global()\` table lives in D1 (no per-DO op-log): flag it so the
|
|
2110
|
+
// base serves it through the latency-tiered poll path (\`readGlobalShapeRows\`)
|
|
2111
|
+
// instead of the CDC poke path.
|
|
2112
|
+
const isGlobal = (schema as unknown as SchemaLike).tables[shape.table]?.shardMode?.kind === "global";
|
|
2113
|
+
|
|
2114
|
+
return { columns: shape.columns, effectiveWhere, global: isGlobal, table: shape.table };
|
|
2115
|
+
}
|
|
2116
|
+
` : "";
|
|
2117
|
+
const customMutatorOverride = hasMutators ? `
|
|
2118
|
+
protected override isCustomMutator(functionPath: string): boolean {
|
|
2119
|
+
return LUNORA_MUTATOR_PATHS.has(functionPath);
|
|
2120
|
+
}
|
|
2121
|
+
` : "";
|
|
2122
|
+
const shapeReadRegistryConst = hasShapes ? `
|
|
2123
|
+
/** 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. */
|
|
2124
|
+
const LUNORA_RLS_READ_REGISTRY = buildRlsReadRegistry(Object.values(LUNORA_FUNCTIONS));
|
|
2125
|
+
` : "";
|
|
2126
|
+
const shapeGuardImport = hasShapes ? "assertShapeShardable, " : "";
|
|
1486
2127
|
const importLines = [
|
|
1487
2128
|
`import type { ${doTypeImports.join(", ")} } from "${base.do}";`,
|
|
1488
|
-
`import { applyCdcChanges, createShardCtxDb, runDataMigration, runShardMigrations, ${relationFanout.importFragment}ShardDO as ShardDOBase } from "${base.do}";`,
|
|
1489
|
-
|
|
2129
|
+
`import { applyCdcChanges, ${shapeGuardImport}createShardCtxDb, ${hasSourcedTables ? "isSourceDue, pullExternalSourceTick, " : ""}runDataMigration, runShardMigrations, ${relationFanout.importFragment}ShardDO as ShardDOBase } from "${base.do}";`,
|
|
2130
|
+
...hasSourcedTables ? [`import type { ExternalSourceLike, SourceClientLike } from "${base.do}";`] : [],
|
|
2131
|
+
// `asBucketStorage` (the bucket-aware `ctx.storage` wrapper) and
|
|
2132
|
+
// `createSecrets` (the `ctx.secrets` core built-in) live in
|
|
1490
2133
|
// `@lunora/server`, the single source — imported here rather than stamped
|
|
1491
|
-
// inline into every generated shard.
|
|
1492
|
-
|
|
2134
|
+
// inline into every generated shard. With shapes, also pull the RLS
|
|
2135
|
+
// read-registry builder + `composeShapeReadWhere` so `resolveShape` can
|
|
2136
|
+
// AND-merge a shape's predicate with the table's read base-where.
|
|
2137
|
+
hasShapes ? `import { asBucketStorage, buildRlsReadRegistry, composeShapeReadWhere, createSecrets } from "${base.server}";` : `import { asBucketStorage, createSecrets } from "${base.server}";`
|
|
1493
2138
|
];
|
|
1494
2139
|
if (hasTables) {
|
|
1495
2140
|
importLines.push(`import { bindOrm, bindTableFacade } from "${base.server}";`);
|
|
1496
2141
|
}
|
|
1497
2142
|
if (hasVectors) {
|
|
1498
2143
|
importLines.push(
|
|
1499
|
-
`import type { SchemaLike as VectorSchemaLike, VectorizeIndexLike, VectorSearchLike } from "@lunora/vectors";`,
|
|
1500
|
-
`import { createContextVectors, createVectors, createVectorSyncHook } from "@lunora/vectors";`
|
|
2144
|
+
`import type { SchemaLike as VectorSchemaLike, VectorizeIndexLike, VectorSearchLike } from "@lunora/bindings/vectors";`,
|
|
2145
|
+
`import { createContextVectors, createVectors, createVectorSyncHook } from "@lunora/bindings/vectors";`
|
|
1501
2146
|
);
|
|
1502
2147
|
}
|
|
1503
2148
|
if (hasAi) {
|
|
1504
2149
|
importLines.push(`import type { AiBindingLike, LunoraAi } from "@lunora/ai";`, `import { createAi } from "@lunora/ai";`);
|
|
1505
2150
|
}
|
|
1506
2151
|
importLines.push(
|
|
2152
|
+
...accessFragments.importLines,
|
|
1507
2153
|
...kvFragments.importLines,
|
|
2154
|
+
...flagsFragments.importLines,
|
|
1508
2155
|
...analyticsFragments.importLines,
|
|
1509
2156
|
...imagesFragments.importLines,
|
|
1510
2157
|
...hyperdriveFragments.importLines,
|
|
1511
2158
|
...browserFragments.importLines,
|
|
2159
|
+
...r2sqlFragments.importLines,
|
|
2160
|
+
...pipelinesFragments.importLines,
|
|
1512
2161
|
...containerImportLines,
|
|
1513
2162
|
...workflowImportLines,
|
|
2163
|
+
...queueImportLines,
|
|
1514
2164
|
...paymentsImports,
|
|
1515
2165
|
``,
|
|
1516
2166
|
`import schema from "../schema.js";`,
|
|
1517
|
-
|
|
2167
|
+
// Local-first sync registries are pulled in alongside the function table
|
|
2168
|
+
// only when the project declares them, so the import list stays minimal.
|
|
2169
|
+
`import { ${["LUNORA_FUNCTIONS", "LUNORA_LIFECYCLE_HOOKS", "LUNORA_MIGRATIONS", ...hasMutators ? ["LUNORA_MUTATOR_PATHS"] : [], ...hasShapes ? ["LUNORA_SHAPES"] : []].join(", ")} } from "./functions.js";`
|
|
1518
2170
|
);
|
|
1519
2171
|
const vectorsConfigField = hasVectors ? `
|
|
1520
2172
|
vectors?: (env: Record<string, unknown>) => Record<string, VectorizeIndexLike>;` : "";
|
|
@@ -1522,6 +2174,8 @@ const emitShard = ({
|
|
|
1522
2174
|
d1?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;` : "";
|
|
1523
2175
|
const hyperdriveGlobalConfigField = hasHyperdriveGlobal ? `
|
|
1524
2176
|
hyperdriveGlobal?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;` : "";
|
|
2177
|
+
const sourceClientConfigField = hasSourcedTables ? `
|
|
2178
|
+
sourceClient?: (env: Record<string, unknown>, binding: string) => { query: <Row = Record<string, unknown>>(text: string, params?: readonly unknown[]) => Promise<Row[]> } | undefined;` : "";
|
|
1525
2179
|
const globalDatabaseStub = hasGlobalTables ? `
|
|
1526
2180
|
const globalDbStub: DatabaseWriterLike = {
|
|
1527
2181
|
aggregate: async () => {
|
|
@@ -1639,6 +2293,144 @@ const vectorsStub: VectorSearchLike = {
|
|
|
1639
2293
|
const bindTableHelper = "";
|
|
1640
2294
|
const globalDatabaseThunk = hasHyperdriveGlobal ? "config.hyperdriveGlobal" : "config.d1";
|
|
1641
2295
|
const globalDatabaseLine = hasGlobalTables ? ` const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, { identity, userId }) ?? globalDbStub;
|
|
2296
|
+
` : "";
|
|
2297
|
+
const globalShapeReaderOverride = hasShapes && hasGlobalTables ? `
|
|
2298
|
+
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 }>> {
|
|
2299
|
+
const env = this.env as Record<string, unknown>;
|
|
2300
|
+
const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, identity) ?? globalDbStub;
|
|
2301
|
+
const rows: Array<{ doc: Record<string, unknown>; id: string }> = [];
|
|
2302
|
+
|
|
2303
|
+
let cursor: null | string = null;
|
|
2304
|
+
|
|
2305
|
+
// Drain every page of the global membership so the seed/diff sees the
|
|
2306
|
+
// full rowset (D1 \`findMany\` is paginated). Stop one row past the cap:
|
|
2307
|
+
// a broad \`.global()\` shape would otherwise materialize an unbounded
|
|
2308
|
+
// array before the caller's \`withinGlobalShapeBound\` check rejects it,
|
|
2309
|
+
// so bail early and let the caller fail it closed.
|
|
2310
|
+
do {
|
|
2311
|
+
// eslint-disable-next-line no-await-in-loop -- sequential page drain to assemble the full membership
|
|
2312
|
+
const page = await globalDb.findMany(resolved.table, { cursor, where: resolved.effectiveWhere });
|
|
2313
|
+
|
|
2314
|
+
for (const doc of page.page) {
|
|
2315
|
+
rows.push({ doc, id: String((doc as { _id?: unknown })._id) });
|
|
2316
|
+
|
|
2317
|
+
if (rows.length > ShardDOBase.GLOBAL_SHAPE_MAX_ROWS) {
|
|
2318
|
+
return rows;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
cursor = page.isDone ? null : page.continueCursor;
|
|
2323
|
+
} while (cursor !== null);
|
|
2324
|
+
|
|
2325
|
+
return rows;
|
|
2326
|
+
}
|
|
2327
|
+
` : "";
|
|
2328
|
+
const sourceClientCacheConst = hasSourcedTables ? `
|
|
2329
|
+
const sourceClientCache = new WeakMap<object, Map<string, SourceClientLike>>();
|
|
2330
|
+
const sourcePollAtCache = new WeakMap<object, Map<string, number>>();
|
|
2331
|
+
` : "";
|
|
2332
|
+
const externalSourceOverride = hasSourcedTables ? `
|
|
2333
|
+
protected override async pollExternalSources(): Promise<number> {
|
|
2334
|
+
const env = (this.env ?? {}) as Record<string, unknown>;
|
|
2335
|
+
const sourced = Object.entries((schema as unknown as SchemaLike).tables)
|
|
2336
|
+
.map(([table, definition]) => [table, (definition as { externalSource?: ExternalSourceLike }).externalSource] as const)
|
|
2337
|
+
.filter((entry): entry is [string, ExternalSourceLike] => entry[1] !== undefined);
|
|
2338
|
+
|
|
2339
|
+
if (sourced.length === 0) {
|
|
2340
|
+
return 0;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
const shardKey = this.currentShardKey();
|
|
2344
|
+
const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
|
|
2345
|
+
const writer = createShardCtxDb({
|
|
2346
|
+
broadcast: (delta) => {
|
|
2347
|
+
this.recordChangedTable(delta.table);
|
|
2348
|
+
},
|
|
2349
|
+
cdc: config.cdc ?? false,
|
|
2350
|
+
scheduler,
|
|
2351
|
+
schema: schema as unknown as SchemaLike,
|
|
2352
|
+
sql: this.sql as SqlExec,
|
|
2353
|
+
});
|
|
2354
|
+
|
|
2355
|
+
let clients = sourceClientCache.get(this);
|
|
2356
|
+
|
|
2357
|
+
if (clients === undefined) {
|
|
2358
|
+
clients = new Map();
|
|
2359
|
+
sourceClientCache.set(this, clients);
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
let polledAt = sourcePollAtCache.get(this);
|
|
2363
|
+
|
|
2364
|
+
if (polledAt === undefined) {
|
|
2365
|
+
polledAt = new Map();
|
|
2366
|
+
sourcePollAtCache.set(this, polledAt);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
const now = Date.now();
|
|
2370
|
+
// \`active\` counts non-manual sources: while > 0 the alarm re-arms; a
|
|
2371
|
+
// manual-only schema returns 0 so the shared alarm goes idle.
|
|
2372
|
+
let active = 0;
|
|
2373
|
+
|
|
2374
|
+
for (const [table, source] of sourced) {
|
|
2375
|
+
if (source.refresh === "manual") {
|
|
2376
|
+
continue;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
active += 1;
|
|
2380
|
+
|
|
2381
|
+
if (!isSourceDue(source.refresh, polledAt.get(table), now)) {
|
|
2382
|
+
continue;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
try {
|
|
2386
|
+
let client = clients.get(source.binding);
|
|
2387
|
+
|
|
2388
|
+
if (client === undefined) {
|
|
2389
|
+
client = config.sourceClient?.(env, source.binding);
|
|
2390
|
+
|
|
2391
|
+
if (client !== undefined) {
|
|
2392
|
+
clients.set(source.binding, client);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
if (client === undefined) {
|
|
2397
|
+
// No SqlClient resolved for this binding (host never wired
|
|
2398
|
+
// \`config.sourceClient\`, or wired it wrong). Surface it in the
|
|
2399
|
+
// Logs panel and stamp \`polledAt\` so a persistent misconfig backs
|
|
2400
|
+
// off to \`refresh.everyMs\` instead of retrying every alarm tick.
|
|
2401
|
+
this.recordExternalSourceError(table, new Error(\`external-source: no sourceClient resolved for binding "\${source.binding}"\`));
|
|
2402
|
+
polledAt.set(table, now);
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
// 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
|
|
2407
|
+
await pullExternalSourceTick(this.sql as SqlExec, writer, client, table, source, shardKey);
|
|
2408
|
+
polledAt.set(table, now);
|
|
2409
|
+
} catch (error) {
|
|
2410
|
+
this.recordExternalSourceError(table, error);
|
|
2411
|
+
// Stamp on failure too, so a persistently failing source throttles
|
|
2412
|
+
// to \`refresh.everyMs\` rather than being hammered every tick.
|
|
2413
|
+
polledAt.set(table, now);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
return active;
|
|
2418
|
+
}
|
|
2419
|
+
` : "";
|
|
2420
|
+
const sourceConstructorOverride = hasSourcedTables ? `
|
|
2421
|
+
public constructor(state: ShardDOState, env: unknown) {
|
|
2422
|
+
super(state, env);
|
|
2423
|
+
|
|
2424
|
+
const autoSourced = Object.values((schema as unknown as SchemaLike).tables).some((definition) => {
|
|
2425
|
+
const source = (definition as { externalSource?: ExternalSourceLike }).externalSource;
|
|
2426
|
+
|
|
2427
|
+
return source !== undefined && source.refresh !== "manual";
|
|
2428
|
+
});
|
|
2429
|
+
|
|
2430
|
+
if (autoSourced) {
|
|
2431
|
+
void this.scheduleSourcePoll();
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
1642
2434
|
` : "";
|
|
1643
2435
|
const facadeBlock = hasTables ? `
|
|
1644
2436
|
const facade = db as unknown as Record<string, ReturnType<typeof bindTableFacade>>;
|
|
@@ -1652,17 +2444,23 @@ ${schema.tables.map(
|
|
|
1652
2444
|
(table) => ` facade[${JSON.stringify(table.name)}] = bindTableFacade(db, ${JSON.stringify(table.name)});`
|
|
1653
2445
|
).join("\n")}
|
|
1654
2446
|
` : "";
|
|
1655
|
-
const
|
|
1656
|
-
|
|
1657
|
-
|
|
2447
|
+
const secretsBuild = `
|
|
2448
|
+
const secrets = createSecrets(env);
|
|
2449
|
+
`;
|
|
2450
|
+
const everyContextBuild = `${accessFragments.build}${kvFragments.build}${flagsFragments.build}${analyticsFragments.build}${secretsBuild}`;
|
|
2451
|
+
const everyContextField = `${accessFragments.contextField}${kvFragments.contextField}${flagsFragments.contextField}${analyticsFragments.contextField}
|
|
2452
|
+
secrets,`;
|
|
2453
|
+
const actionOnlyHasAny = hasImages || hasHyperdrive || hasBrowser || hasR2sql || hasPipelines;
|
|
1658
2454
|
const actionOnlyBlock = actionOnlyHasAny ? `
|
|
1659
2455
|
// ActionCtx-only helpers (external, non-deterministic I/O): constructed
|
|
1660
2456
|
// and attached only for an \`action\` so query/mutation ctx never carry them.
|
|
1661
2457
|
if (isAction) {
|
|
1662
|
-
${imagesFragments.build}${hyperdriveFragments.build}${browserFragments.build}${[
|
|
2458
|
+
${imagesFragments.build}${hyperdriveFragments.build}${browserFragments.build}${r2sqlFragments.build}${pipelinesFragments.build}${[
|
|
1663
2459
|
...hasImages ? [" ctx.images = images;"] : [],
|
|
1664
2460
|
...hasHyperdrive ? [" ctx.sql = sql;"] : [],
|
|
1665
|
-
...hasBrowser ? [" ctx.browser = browser;"] : []
|
|
2461
|
+
...hasBrowser ? [" ctx.browser = browser;"] : [],
|
|
2462
|
+
...hasR2sql ? [" ctx.r2sql = r2sql;"] : [],
|
|
2463
|
+
...hasPipelines ? [" ctx.pipelines = pipelines;"] : []
|
|
1666
2464
|
].join("\n")}
|
|
1667
2465
|
}
|
|
1668
2466
|
` : "";
|
|
@@ -1702,14 +2500,14 @@ const LUNORA_STORAGE_RULES: StorageRulesResult = ${JSON.stringify(storageRulesDa
|
|
|
1702
2500
|
|
|
1703
2501
|
/** 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. */
|
|
1704
2502
|
const LUNORA_STUDIO_FEATURES: StudioFeaturesResult = ${JSON.stringify(studioFeaturesData, void 0, 4)};
|
|
1705
|
-
${workflowsMetadataConst}${containerSpecs}${workflowSpecs}
|
|
2503
|
+
${flagsOverrides.constant}${shapeReadRegistryConst}${workflowsMetadataConst}${queuesMetadataConst}${containerSpecs}${workflowSpecs}${queueSpecs}
|
|
1706
2504
|
export interface ShardDOConfig {
|
|
1707
2505
|
/** Opt into change-data-capture: records a post-image to \`__cdc_log\` on every write (backs streaming export + replay-PITR). */
|
|
1708
2506
|
cdc?: boolean;
|
|
1709
2507
|
/** 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. */
|
|
1710
2508
|
observability?: (env: Record<string, unknown>) => LogSink | undefined;
|
|
1711
2509
|
scheduler?: (env: Record<string, unknown>) => unknown;
|
|
1712
|
-
storage?: (env: Record<string, unknown>) => unknown;${vectorsConfigField}${aiConfigField}${kvFragments.configField}${analyticsFragments.configField}${imagesFragments.configField}${hyperdriveFragments.configField}${browserFragments.configField}${paymentsConfigField}${d1ConfigField}${hyperdriveGlobalConfigField}
|
|
2510
|
+
storage?: (env: Record<string, unknown>) => unknown;${vectorsConfigField}${aiConfigField}${kvFragments.configField}${flagsFragments.configField}${analyticsFragments.configField}${imagesFragments.configField}${hyperdriveFragments.configField}${browserFragments.configField}${r2sqlFragments.configField}${pipelinesFragments.configField}${paymentsConfigField}${d1ConfigField}${hyperdriveGlobalConfigField}${sourceClientConfigField}
|
|
1713
2511
|
}
|
|
1714
2512
|
|
|
1715
2513
|
const schedulerStub = {
|
|
@@ -1747,7 +2545,7 @@ const storageStub = {
|
|
|
1747
2545
|
throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
|
|
1748
2546
|
},
|
|
1749
2547
|
};
|
|
1750
|
-
${globalDatabaseStub}${vectorsStub}${aiStub}${kvFragments.stub}${analyticsFragments.stub}${imagesFragments.stub}${hyperdriveFragments.stub}${browserFragments.stub}${paymentStub}${bindTableHelper}
|
|
2548
|
+
${globalDatabaseStub}${sourceClientCacheConst}${vectorsStub}${aiStub}${kvFragments.stub}${flagsFragments.stub}${analyticsFragments.stub}${imagesFragments.stub}${hyperdriveFragments.stub}${browserFragments.stub}${r2sqlFragments.stub}${pipelinesFragments.stub}${paymentStub}${bindTableHelper}
|
|
1751
2549
|
// Bound in-process \`ctx.run*\` composition depth so a self- or cyclically-
|
|
1752
2550
|
// referencing call fails loudly with a clear error instead of overflowing the
|
|
1753
2551
|
// stack. Tracked across the awaited handler chain (one DO invocation is
|
|
@@ -1788,7 +2586,7 @@ const dispatchRun = async (expected: FunctionKind, functionPath: string, args: R
|
|
|
1788
2586
|
* from the worker entry so wrangler binds it by name.
|
|
1789
2587
|
*/
|
|
1790
2588
|
export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOState, env: unknown) => ShardDOBase =>
|
|
1791
|
-
class extends ShardDOBase {
|
|
2589
|
+
class extends ShardDOBase {${sourceConstructorOverride}
|
|
1792
2590
|
private migrated = false;
|
|
1793
2591
|
|
|
1794
2592
|
public override async handleRpc(functionPath: string, args: Record<string, unknown>): Promise<unknown> {
|
|
@@ -1817,8 +2615,20 @@ export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOSt
|
|
|
1817
2615
|
// do external I/O that can't be rolled back, so both dispatch directly.
|
|
1818
2616
|
// \`ctx.run*\` composition runs inside this span (it never re-enters
|
|
1819
2617
|
// handleRpc); runInTransaction's own guard rejects accidental nesting.
|
|
2618
|
+
//
|
|
2619
|
+
// The replay bookkeeping (idempotency dedup row + custom-mutator
|
|
2620
|
+
// watermark advance) commits INSIDE this span via
|
|
2621
|
+
// \`commitMutationBookkeeping\`, so the writes, the dedup row, and the
|
|
2622
|
+
// watermark are atomic — a crash can't leave the writes durable without
|
|
2623
|
+
// the replay guard.
|
|
1820
2624
|
if (registered.kind === "mutation") {
|
|
1821
|
-
return this.runInTransaction(() =>
|
|
2625
|
+
return this.runInTransaction(async () => {
|
|
2626
|
+
const result = await registered.handler(ctx, args);
|
|
2627
|
+
|
|
2628
|
+
this.commitMutationBookkeeping(result);
|
|
2629
|
+
|
|
2630
|
+
return result;
|
|
2631
|
+
});
|
|
1822
2632
|
}
|
|
1823
2633
|
|
|
1824
2634
|
return registered.handler(ctx, args);
|
|
@@ -1856,7 +2666,7 @@ ${relationFanout.override}
|
|
|
1856
2666
|
iterator: (signal) => (registered.handler as (context: unknown, args: Record<string, unknown>, signal: AbortSignal) => AsyncIterable<unknown>)(this.buildCtx({ functionPath }), args, signal),
|
|
1857
2667
|
};
|
|
1858
2668
|
}
|
|
1859
|
-
|
|
2669
|
+
${customMutatorOverride}${shapeResolveOverride}${globalShapeReaderOverride}${externalSourceOverride}
|
|
1860
2670
|
protected override lifecycleHookPaths(event: "connect" | "disconnect"): readonly string[] {
|
|
1861
2671
|
return LUNORA_LIFECYCLE_HOOKS[event];
|
|
1862
2672
|
}
|
|
@@ -1892,7 +2702,7 @@ ${relationFanout.override}
|
|
|
1892
2702
|
protected override studioFeatures(): StudioFeaturesResult {
|
|
1893
2703
|
return LUNORA_STUDIO_FEATURES;
|
|
1894
2704
|
}
|
|
1895
|
-
${workflowsMetadataOverride}
|
|
2705
|
+
${flagsOverrides.evaluateOverride}${flagsOverrides.subscriptionOverride}${workflowsMetadataOverride}${queuesMetadataOverride}
|
|
1896
2706
|
protected override advisories(): AdvisoryFinding[] {
|
|
1897
2707
|
return LUNORA_ADVISORIES;
|
|
1898
2708
|
}
|
|
@@ -2126,7 +2936,7 @@ ${workflowsMetadataOverride}
|
|
|
2126
2936
|
// dispatch path) fall back to the per-request fields as before.
|
|
2127
2937
|
const userId = options.identity ? options.identity.userId : this.getCurrentUserId();
|
|
2128
2938
|
const identity = options.identity ? options.identity.identity : this.getCurrentIdentity();
|
|
2129
|
-
${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}
|
|
2939
|
+
${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}${queuesBuild}
|
|
2130
2940
|
const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
|
|
2131
2941
|
// Build the storage adapter once and share it between \`ctx.storage\`
|
|
2132
2942
|
// and \`ctx.db.system._storage\` so both read the same R2 binding. The
|
|
@@ -2149,6 +2959,14 @@ ${facadeBlock}${paymentsBuild}
|
|
|
2149
2959
|
warn: (...args: unknown[]) => { this.recordUserLog(logFunctionPath, "warn", args, observability); },
|
|
2150
2960
|
};
|
|
2151
2961
|
|
|
2962
|
+
// \`ctx.now\`: the wall-clock instant (epoch ms) this function began,
|
|
2963
|
+
// captured ONCE so the whole handler body sees a single stable value.
|
|
2964
|
+
// Query/mutation handlers must be deterministic (they may be re-run on
|
|
2965
|
+
// OCC retry / subscription re-eval), so they must read time through
|
|
2966
|
+
// \`ctx.now\` instead of \`Date.now()\` — the \`nondeterministic_query_mutation\`
|
|
2967
|
+
// advisor flags the latter. Actions may still use ambient \`Date.now()\`.
|
|
2968
|
+
const now = Date.now();
|
|
2969
|
+
|
|
2152
2970
|
const ctx: Record<string, unknown> = {
|
|
2153
2971
|
auth: {
|
|
2154
2972
|
getIdentity: async () => identity ?? null,
|
|
@@ -2157,9 +2975,10 @@ ${facadeBlock}${paymentsBuild}
|
|
|
2157
2975
|
db,
|
|
2158
2976
|
fetch: globalThis.fetch.bind(globalThis),
|
|
2159
2977
|
ip: this.getCurrentIp(),
|
|
2160
|
-
log
|
|
2978
|
+
log,
|
|
2979
|
+
now,${ormContextField}
|
|
2161
2980
|
scheduler,
|
|
2162
|
-
storage,${vectorsContextField}${aiContextField}${everyContextField}${paymentsContextField}${containersContextField}${workflowsContextField}
|
|
2981
|
+
storage,${vectorsContextField}${aiContextField}${everyContextField}${paymentsContextField}${containersContextField}${workflowsContextField}${queuesContextField}
|
|
2163
2982
|
};
|
|
2164
2983
|
${isActionLine}${actionOnlyBlock}
|
|
2165
2984
|
ctx.runAction = (reference: FunctionReference, fnArgs: Record<string, unknown>) => dispatchRun("action", reference.__lunoraRef, fnArgs, ctx);
|
|
@@ -2423,4 +3242,4 @@ const emitWranglerCronTriggers = (crons) => {
|
|
|
2423
3242
|
return triggers;
|
|
2424
3243
|
};
|
|
2425
3244
|
|
|
2426
|
-
export { GENERATED_HEADER, buildStorageColumns, emitApi, emitContainers, emitCrons, emitDataModel, emitDrizzleSchema, emitFunctions, emitSeed, emitServer, emitShard, emitVectors, emitWorkflows, emitWranglerCronTriggers };
|
|
3245
|
+
export { GENERATED_HEADER, buildStorageColumns, emitApi, emitCollections, emitContainers, emitCrons, emitDataModel, emitDrizzleSchema, emitFunctions, emitQueues, emitSeed, emitServer, emitShard, emitVectors, emitWorkflows, emitWranglerCronTriggers };
|