@lunora/codegen 1.0.0-alpha.3 → 1.0.0-alpha.30
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 +1084 -25
- package/dist/index.d.ts +1084 -25
- package/dist/index.mjs +29 -23
- package/dist/packem_shared/{CONTAINERS_FILENAME-0K-pjNb8.mjs → CONTAINERS_FILENAME-DjpXMqhp.mjs} +1 -1
- package/dist/packem_shared/{CodegenDiagnosticError-54jWDxA9.mjs → CodegenDiagnosticError-DyQ5FwkM.mjs} +7 -5
- package/dist/packem_shared/FLAGS_FILENAME-BjpS5w08.mjs +139 -0
- package/dist/packem_shared/{GENERATED_HEADER-CuLyj0WQ.mjs → GENERATED_HEADER-f6nMvllu.mjs} +853 -65
- package/dist/packem_shared/MUTATORS_FILENAME-BZOfUhlY.mjs +81 -0
- package/dist/packem_shared/{OPENRPC_VERSION-BGUrsrt_.mjs → OPENRPC_VERSION-u5SqDDGk.mjs} +1 -1
- package/dist/packem_shared/QUEUES_FILENAME-BF0iUmx7.mjs +119 -0
- package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-Cpfn80SH.mjs +3481 -0
- package/dist/packem_shared/{SCHEMA_SNAPSHOT_VERSION-DzLDbWk3.mjs → SCHEMA_SNAPSHOT_VERSION-D0ARY6rL.mjs} +18 -2
- package/dist/packem_shared/SHAPES_FILENAME-DOhPGi-6.mjs +94 -0
- package/dist/packem_shared/{WORKFLOWS_FILENAME-DRDQdhfq.mjs → WORKFLOWS_FILENAME-CCC_42o3.mjs} +42 -2
- package/dist/packem_shared/{buildOpenApiDocument-CXwnbp4b.mjs → buildOpenApiDocument-AgKWmlry.mjs} +1 -1
- package/dist/packem_shared/{discoverAuthApiCalls-C35R6z0T.mjs → discoverAuthApiCalls-URDoTADN.mjs} +1 -1
- package/dist/packem_shared/{discoverCrons-BL6iGuJ3.mjs → discoverCrons-DdZOeroL.mjs} +20 -17
- package/dist/packem_shared/{discoverFunctions-DEgAcRuD.mjs → discoverFunctions-BJdF7lRs.mjs} +63 -9
- package/dist/packem_shared/{discoverHttpRoutes-C978pBiG.mjs → discoverHttpRoutes-CcwP-Rkr.mjs} +2 -2
- package/dist/packem_shared/{discoverInserts-CRQdXvHO.mjs → discoverInserts-BpFhM32L.mjs} +24 -2
- package/dist/packem_shared/{discoverMaskProcedures-B64zA740.mjs → discoverMaskProcedures-DT-v8wvj.mjs} +58 -2
- package/dist/packem_shared/{discoverMigrations-Doj_-BAA.mjs → discoverMigrations-cG1idBVM.mjs} +10 -4
- package/dist/packem_shared/{discoverNondeterministicCalls-4KiPQxQU.mjs → discoverNondeterministicCalls-CEOjmfk4.mjs} +1 -1
- package/dist/packem_shared/{discoverQueries-BkIi0dBD.mjs → discoverQueries-DeVBQVAG.mjs} +1 -1
- package/dist/packem_shared/{discoverR2sqlCalls-BpDqvcUn.mjs → discoverR2sqlCalls-CWCAU-xJ.mjs} +1 -1
- package/dist/packem_shared/{discoverRlsMetadata-DpRB1HMe.mjs → discoverRlsMetadata-BL0oukaK.mjs} +1 -1
- package/dist/packem_shared/{discoverSchema-BBulgGbH.mjs → discoverSchema-CeXJWVKV.mjs} +349 -17
- package/dist/packem_shared/{discoverStorageRulesMetadata-DAqJUxUv.mjs → discoverStorageRulesMetadata-CFHByjIl.mjs} +1 -1
- package/dist/packem_shared/{emitApp-CzSxVDaG.mjs → emitApp-DKt3rPtr.mjs} +84 -8
- package/dist/packem_shared/{formatAdvisories-8NIv1k0I.mjs → formatAdvisories-C9wNBNvL.mjs} +47 -3
- package/dist/packem_shared/{parse-validator-tuQtHrsr.mjs → parse-validator-D6zI2i85.mjs} +5 -3
- package/dist/packem_shared/redact-oTmsol5A.mjs +33 -0
- package/package.json +9 -7
- package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-BQ_kCZ81.mjs +0 -903
|
@@ -1,27 +1,200 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
1
2
|
import { s as sanitizeNamespace } from './paths-BRd6JHuF.mjs';
|
|
2
3
|
|
|
4
|
+
const LITERAL_VALUE_RE = /^(?:"[^"\\]*"|'[^'\\]*'|-?\d+(?:\.\d+)?|true|false|null)$/u;
|
|
5
|
+
|
|
6
|
+
const PASS_THROUGH_KINDS = /* @__PURE__ */ new Set(["any", "bigint", "boolean", "date", "id", "null", "number", "storage", "string", "timestamp"]);
|
|
7
|
+
const hasColumnModifier = (node) => node.column !== void 0;
|
|
8
|
+
const emitScalarGuard = (kind, inExpr) => {
|
|
9
|
+
switch (kind) {
|
|
10
|
+
case "bigint": {
|
|
11
|
+
return `if (typeof ${inExpr} !== "bigint") return DEFER;
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
14
|
+
case "boolean": {
|
|
15
|
+
return `if (typeof ${inExpr} !== "boolean") return DEFER;
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
18
|
+
case "date":
|
|
19
|
+
case "number":
|
|
20
|
+
case "timestamp": {
|
|
21
|
+
return `if (typeof ${inExpr} !== "number" || !Number.isFinite(${inExpr})) return DEFER;
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
24
|
+
case "null": {
|
|
25
|
+
return `if (${inExpr} !== null) return DEFER;
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
// string / id / storage all parse as a bare string at runtime.
|
|
29
|
+
default: {
|
|
30
|
+
return `if (typeof ${inExpr} !== "string") return DEFER;
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const compileLiteral = (node, inExpr) => {
|
|
36
|
+
const literal = node.literalValue?.trim();
|
|
37
|
+
if (literal === void 0 || !LITERAL_VALUE_RE.test(literal)) {
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
return { out: inExpr, pre: `if (${inExpr} !== ${literal}) return DEFER;
|
|
41
|
+
` };
|
|
42
|
+
};
|
|
43
|
+
const compileNode = (node, inExpr, context) => {
|
|
44
|
+
if (hasColumnModifier(node)) {
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
if (node.hasRefinement || node.sourceText !== void 0) {
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
if (PASS_THROUGH_KINDS.has(node.kind)) {
|
|
51
|
+
if (node.kind === "any") {
|
|
52
|
+
return { out: inExpr, pre: "" };
|
|
53
|
+
}
|
|
54
|
+
return { out: inExpr, pre: emitScalarGuard(node.kind, inExpr) };
|
|
55
|
+
}
|
|
56
|
+
switch (node.kind) {
|
|
57
|
+
case "array": {
|
|
58
|
+
return compileArray(node, inExpr, context);
|
|
59
|
+
}
|
|
60
|
+
case "literal": {
|
|
61
|
+
return compileLiteral(node, inExpr);
|
|
62
|
+
}
|
|
63
|
+
case "object": {
|
|
64
|
+
return compileObject(node, inExpr, context);
|
|
65
|
+
}
|
|
66
|
+
default: {
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const compileArray = (node, inExpr, context) => {
|
|
72
|
+
const { inner } = node;
|
|
73
|
+
if (!inner) {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
const id = context.next();
|
|
77
|
+
const array = `__arr${String(id)}`;
|
|
78
|
+
const index = `__i${String(id)}`;
|
|
79
|
+
const element = `__e${String(id)}`;
|
|
80
|
+
const innerEmit = compileNode(inner, element, context);
|
|
81
|
+
if (!innerEmit) {
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
const pre = `if (!Array.isArray(${inExpr})) return DEFER;
|
|
85
|
+
const ${array} = new Array(${inExpr}.length);
|
|
86
|
+
for (let ${index} = 0; ${index} < ${inExpr}.length; ${index}++) {
|
|
87
|
+
const ${element} = ${inExpr}[${index}];
|
|
88
|
+
${innerEmit.pre}${array}[${index}] = ${innerEmit.out};
|
|
89
|
+
}
|
|
90
|
+
`;
|
|
91
|
+
return { out: array, pre };
|
|
92
|
+
};
|
|
93
|
+
const compileField = (key, node, access, context) => {
|
|
94
|
+
const keyLiteral = JSON.stringify(key);
|
|
95
|
+
if (node.kind === "optional") {
|
|
96
|
+
if (node.hasRefinement || node.sourceText !== void 0 || hasColumnModifier(node)) {
|
|
97
|
+
return void 0;
|
|
98
|
+
}
|
|
99
|
+
const { inner } = node;
|
|
100
|
+
if (!inner) {
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
const innerEmit = compileNode(inner, access, context);
|
|
104
|
+
if (!innerEmit) {
|
|
105
|
+
return void 0;
|
|
106
|
+
}
|
|
107
|
+
const id = context.next();
|
|
108
|
+
const has = `__has${String(id)}`;
|
|
109
|
+
const value = `__val${String(id)}`;
|
|
110
|
+
const pre = `let ${has} = false;
|
|
111
|
+
let ${value};
|
|
112
|
+
if (${access} !== undefined) {
|
|
113
|
+
${innerEmit.pre}${value} = ${innerEmit.out};
|
|
114
|
+
${has} = true;
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
117
|
+
return { entry: `...(${has} ? { ${keyLiteral}: ${value} } : {})`, pre };
|
|
118
|
+
}
|
|
119
|
+
const emit = compileNode(node, access, context);
|
|
120
|
+
if (!emit) {
|
|
121
|
+
return void 0;
|
|
122
|
+
}
|
|
123
|
+
return { entry: `${keyLiteral}: ${emit.out}`, pre: emit.pre };
|
|
124
|
+
};
|
|
125
|
+
const compileFields = (shape, accessFor, context) => {
|
|
126
|
+
let pre = "";
|
|
127
|
+
const entries = [];
|
|
128
|
+
for (const key of Object.keys(shape)) {
|
|
129
|
+
const node = shape[key];
|
|
130
|
+
if (!node) {
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
const field = compileField(key, node, accessFor(key), context);
|
|
134
|
+
if (!field) {
|
|
135
|
+
return void 0;
|
|
136
|
+
}
|
|
137
|
+
pre += field.pre;
|
|
138
|
+
entries.push(field.entry);
|
|
139
|
+
}
|
|
140
|
+
return { entries: entries.join(", "), pre };
|
|
141
|
+
};
|
|
142
|
+
const compileObject = (node, inExpr, context) => {
|
|
143
|
+
const shape = node.shape ?? {};
|
|
144
|
+
const fields = compileFields(shape, (key) => `${inExpr}[${JSON.stringify(key)}]`, context);
|
|
145
|
+
if (!fields) {
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
const id = context.next();
|
|
149
|
+
const object = `__obj${String(id)}`;
|
|
150
|
+
const pre = `if (typeof ${inExpr} !== "object" || ${inExpr} === null || Array.isArray(${inExpr})) return DEFER;
|
|
151
|
+
${fields.pre}const ${object} = { ${fields.entries} };
|
|
152
|
+
`;
|
|
153
|
+
return { out: object, pre };
|
|
154
|
+
};
|
|
155
|
+
const compileArgsValidator = (args) => {
|
|
156
|
+
let counter = 0;
|
|
157
|
+
const context = {
|
|
158
|
+
next: () => {
|
|
159
|
+
counter += 1;
|
|
160
|
+
return counter;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const fields = compileFields(args, (key) => `source[${JSON.stringify(key)}]`, context);
|
|
164
|
+
if (!fields) {
|
|
165
|
+
return void 0;
|
|
166
|
+
}
|
|
167
|
+
return `(source) => {
|
|
168
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) return DEFER;
|
|
169
|
+
${fields.pre}return { ${fields.entries} };
|
|
170
|
+
}`;
|
|
171
|
+
};
|
|
172
|
+
|
|
3
173
|
const GENERATED_HEADER = "// GENERATED by @lunora/codegen — do not edit.\n// Run `lunora codegen` to regenerate.\n\n";
|
|
4
174
|
const baseSpecifiers = (useUmbrella = false) => useUmbrella ? {
|
|
5
175
|
client: "lunorash/client",
|
|
6
176
|
do: "lunorash/do",
|
|
177
|
+
flags: "lunorash/flags",
|
|
7
178
|
server: "lunorash/server",
|
|
8
179
|
serverDataModel: "lunorash/server/data-model",
|
|
9
180
|
serverDrizzle: "lunorash/server/drizzle",
|
|
10
|
-
serverTypes: "lunorash/server/types"
|
|
181
|
+
serverTypes: "lunorash/server/types",
|
|
182
|
+
values: "lunorash/values"
|
|
11
183
|
} : {
|
|
12
184
|
client: "@lunora/client",
|
|
13
185
|
do: "@lunora/do",
|
|
186
|
+
flags: "@lunora/flags",
|
|
14
187
|
server: "@lunora/server",
|
|
15
188
|
serverDataModel: "@lunora/server/data-model",
|
|
16
189
|
serverDrizzle: "@lunora/server/drizzle",
|
|
17
|
-
serverTypes: "@lunora/server/types"
|
|
190
|
+
serverTypes: "@lunora/server/types",
|
|
191
|
+
values: "@lunora/values"
|
|
18
192
|
};
|
|
19
193
|
const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/u;
|
|
20
|
-
const LITERAL_VALUE_RE = /^(?:"[^"\\]*"|'[^'\\]*'|-?\d+(?:\.\d+)?|true|false|null)$/u;
|
|
21
194
|
const IMPORT_PATH_RE = /^[\w./-]+$/u;
|
|
22
195
|
const assertIdentifier = (value, context) => {
|
|
23
196
|
if (!IDENTIFIER_RE.test(value)) {
|
|
24
|
-
throw new
|
|
197
|
+
throw new LunoraError("INTERNAL", `@lunora/codegen: ${context} is not a valid JS identifier: ${JSON.stringify(value)}`);
|
|
25
198
|
}
|
|
26
199
|
};
|
|
27
200
|
const renderPropertyKey = (fieldName) => IDENTIFIER_RE.test(fieldName) ? fieldName : JSON.stringify(fieldName);
|
|
@@ -48,7 +221,10 @@ const literalToType = (value) => {
|
|
|
48
221
|
return "unknown";
|
|
49
222
|
}
|
|
50
223
|
if (!LITERAL_VALUE_RE.test(value)) {
|
|
51
|
-
throw new
|
|
224
|
+
throw new LunoraError(
|
|
225
|
+
"INTERNAL",
|
|
226
|
+
`@lunora/codegen: v.literal() argument is not a parseable string/number/boolean/null literal: ${JSON.stringify(value)}`
|
|
227
|
+
);
|
|
52
228
|
}
|
|
53
229
|
return value;
|
|
54
230
|
};
|
|
@@ -430,6 +606,26 @@ import type { InsertModel } from "./dataModel.js";
|
|
|
430
606
|
export const createSeedClient = (options?: SeedClientOptions): SeedClient<InsertModel> => createSeedClientBase<InsertModel>(schema, options);
|
|
431
607
|
`;
|
|
432
608
|
};
|
|
609
|
+
const emitCollections = (shapes, hasDatabase, useUmbrella = false) => {
|
|
610
|
+
if (shapes.length === 0 || !hasDatabase) {
|
|
611
|
+
return "";
|
|
612
|
+
}
|
|
613
|
+
const base = baseSpecifiers(useUmbrella);
|
|
614
|
+
const factories = shapes.map((shape) => {
|
|
615
|
+
assertIdentifier(shape.exportName, "shape export name");
|
|
616
|
+
return `/** Live collection for the \`${shape.exportName}\` replication shape — pass the shape's validated \`args\` (its partition selector). */
|
|
617
|
+
export const ${shape.exportName}Collection = (client: LunoraClient, args?: Record<string, unknown>): Collection<Row, string> =>
|
|
618
|
+
createCollection(lunoraCollectionOptions({ client, shape: { args, name: "${shape.exportName}" } }).config);`;
|
|
619
|
+
}).join("\n\n");
|
|
620
|
+
return `${GENERATED_HEADER}import type { LunoraClient } from "${base.client}";
|
|
621
|
+
import { lunoraCollectionOptions } from "@lunora/db/collections";
|
|
622
|
+
import type { Row } from "@lunora/db";
|
|
623
|
+
import type { Collection } from "@tanstack/db";
|
|
624
|
+
import { createCollection } from "@tanstack/db";
|
|
625
|
+
|
|
626
|
+
${factories}
|
|
627
|
+
`;
|
|
628
|
+
};
|
|
433
629
|
const moduleAlias = (filePath, index) => `lunora_${sanitizeNamespace(filePath)}_${String(index)}`;
|
|
434
630
|
const CALL_REGISTERED_HELPER = `const callRegistered = async <R>(context: CallerCtx, functionPath: string, args: Record<string, unknown> | undefined): Promise<R> => {
|
|
435
631
|
const registered = LUNORA_FUNCTIONS[functionPath];
|
|
@@ -444,7 +640,7 @@ const CALL_REGISTERED_HELPER = `const callRegistered = async <R>(context: Caller
|
|
|
444
640
|
|
|
445
641
|
return (await registered.handler(context, args ?? {})) as R;
|
|
446
642
|
};`;
|
|
447
|
-
const renderFunctionRegistry = (functions, migrations) => {
|
|
643
|
+
const renderFunctionRegistry = (functions, migrations, mutators = [], shapes = []) => {
|
|
448
644
|
const aliasByPath = /* @__PURE__ */ new Map();
|
|
449
645
|
const registerPath = (filePath) => {
|
|
450
646
|
if (!aliasByPath.has(filePath)) {
|
|
@@ -457,9 +653,15 @@ const renderFunctionRegistry = (functions, migrations) => {
|
|
|
457
653
|
for (const migration of migrations) {
|
|
458
654
|
registerPath(migration.filePath);
|
|
459
655
|
}
|
|
656
|
+
for (const mutator of mutators) {
|
|
657
|
+
registerPath(mutator.filePath);
|
|
658
|
+
}
|
|
659
|
+
for (const shape of shapes) {
|
|
660
|
+
registerPath(shape.filePath);
|
|
661
|
+
}
|
|
460
662
|
const importLines = [...aliasByPath.entries()].map(([filePath, alias]) => {
|
|
461
663
|
if (!IMPORT_PATH_RE.test(filePath)) {
|
|
462
|
-
throw new
|
|
664
|
+
throw new LunoraError("INTERNAL", `@lunora/codegen: refusing to emit import for unsafe file path: ${JSON.stringify(filePath)}`);
|
|
463
665
|
}
|
|
464
666
|
return `import * as ${alias} from "../${filePath}.js";`;
|
|
465
667
|
}).join("\n");
|
|
@@ -469,15 +671,37 @@ const renderFunctionRegistry = (functions, migrations) => {
|
|
|
469
671
|
const migrationEntries = migrations.map(
|
|
470
672
|
(migration) => ` ${JSON.stringify(migration.id)}: ${aliasByPath.get(migration.filePath) ?? ""}.${migration.exportName} as unknown as RegisteredDataMigration,`
|
|
471
673
|
).join("\n");
|
|
674
|
+
const mutatorEntries = mutators.map(
|
|
675
|
+
(mutator) => ` "${sanitizeNamespace(mutator.filePath)}:${mutator.exportName}": ${aliasByPath.get(mutator.filePath) ?? ""}.${mutator.exportName} as unknown as RegisteredLunoraFunction,`
|
|
676
|
+
).join("\n");
|
|
677
|
+
const shapeEntries = shapes.map((shape) => ` "${shape.exportName}": ${aliasByPath.get(shape.filePath) ?? ""}.${shape.exportName} as unknown as RegisteredShape,`).join("\n");
|
|
678
|
+
const mutatorPaths = mutators.map((mutator) => `${sanitizeNamespace(mutator.filePath)}:${mutator.exportName}`);
|
|
679
|
+
const combinedDispatch = [dispatchEntries, mutatorEntries].filter((entries) => entries.length > 0).join("\n");
|
|
680
|
+
const installLines = functions.map((definition) => {
|
|
681
|
+
if (definition.lifecycle || Object.keys(definition.args).length === 0) {
|
|
682
|
+
return void 0;
|
|
683
|
+
}
|
|
684
|
+
const compiled = compileArgsValidator(definition.args);
|
|
685
|
+
if (compiled === void 0) {
|
|
686
|
+
return void 0;
|
|
687
|
+
}
|
|
688
|
+
const alias = aliasByPath.get(definition.filePath) ?? "";
|
|
689
|
+
return `installCompiledValidatorMap(${alias}.${definition.exportName}.args, ${compiled});`;
|
|
690
|
+
}).filter((line) => line !== void 0).join("\n");
|
|
472
691
|
return {
|
|
473
|
-
dispatchBody:
|
|
474
|
-
${
|
|
692
|
+
dispatchBody: combinedDispatch.length > 0 ? `
|
|
693
|
+
${combinedDispatch}
|
|
475
694
|
` : "",
|
|
476
695
|
importBlock: importLines.length > 0 ? `${importLines}
|
|
477
696
|
|
|
478
697
|
` : "",
|
|
698
|
+
installBlock: installLines,
|
|
479
699
|
migrationBody: migrationEntries.length > 0 ? `
|
|
480
700
|
${migrationEntries}
|
|
701
|
+
` : "",
|
|
702
|
+
mutatorPaths,
|
|
703
|
+
shapeBody: shapeEntries.length > 0 ? `
|
|
704
|
+
${shapeEntries}
|
|
481
705
|
` : ""
|
|
482
706
|
};
|
|
483
707
|
};
|
|
@@ -531,15 +755,19 @@ const buildStorageBucketNames = (schema, ruleBuckets = []) => {
|
|
|
531
755
|
};
|
|
532
756
|
const emitServer = ({
|
|
533
757
|
containers = [],
|
|
758
|
+
hasAccessFacade = false,
|
|
534
759
|
hasAi = false,
|
|
535
760
|
hasAnalytics = false,
|
|
536
761
|
hasBrowser = false,
|
|
762
|
+
hasFlags = false,
|
|
537
763
|
hasHyperdrive = false,
|
|
538
764
|
hasImages = false,
|
|
539
765
|
hasKv = false,
|
|
540
766
|
hasPayments = false,
|
|
541
767
|
hasPipelines = false,
|
|
542
768
|
hasR2sql = false,
|
|
769
|
+
identity,
|
|
770
|
+
queues = [],
|
|
543
771
|
schema,
|
|
544
772
|
storageRuleBuckets = [],
|
|
545
773
|
useUmbrella = false,
|
|
@@ -573,6 +801,11 @@ const emitServer = ({
|
|
|
573
801
|
assertIdentifier(workflow.bindingName, `workflow binding "${workflow.bindingName}"`);
|
|
574
802
|
return ` /** Workflow binding for the \`${workflow.exportName}\` workflow. */
|
|
575
803
|
readonly ${workflow.bindingName}?: unknown;`;
|
|
804
|
+
}),
|
|
805
|
+
...queues.map((queue) => {
|
|
806
|
+
assertIdentifier(queue.bindingName, `queue binding "${queue.bindingName}"`);
|
|
807
|
+
return ` /** Queue producer binding for the \`${queue.exportName}\` queue. */
|
|
808
|
+
readonly ${queue.bindingName}?: unknown;`;
|
|
576
809
|
})
|
|
577
810
|
].join("\n");
|
|
578
811
|
const envBlock = `
|
|
@@ -595,7 +828,13 @@ ${envBindingFields}` : ""}
|
|
|
595
828
|
/** Alias for {@link CloudflareBindings} — the typed shape of \`env\`. */
|
|
596
829
|
export type Env = CloudflareBindings;`;
|
|
597
830
|
const kvContextField = hasKv ? `
|
|
598
|
-
readonly kv: import("@lunora/kv").Kv;` : "";
|
|
831
|
+
readonly kv: import("@lunora/bindings/kv").Kv;` : "";
|
|
832
|
+
const accessContextField = hasAccessFacade ? `
|
|
833
|
+
/** Verified Cloudflare Access identity — a synchronous facade over the resolved claims (email / groups / hasGroup / claims). Anonymous when no Access token is present. */
|
|
834
|
+
readonly access: import("@lunora/cloudflare-access/context").AccessFacade;` : "";
|
|
835
|
+
const flagsContextField = hasFlags ? `
|
|
836
|
+
/** Feature-flag evaluation (OpenFeature). Reads are memoized per request; evaluations never throw — a provider error resolves to the supplied default. */
|
|
837
|
+
readonly flags: import("${base.flags}").LunoraFlags;` : "";
|
|
599
838
|
const hyperdriveActionField = hasHyperdrive ? `
|
|
600
839
|
/**
|
|
601
840
|
* 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.
|
|
@@ -606,18 +845,18 @@ export type Env = CloudflareBindings;`;
|
|
|
606
845
|
readonly browser: import("@lunora/browser").Browser;` : "";
|
|
607
846
|
const imagesActionField = hasImages ? `
|
|
608
847
|
/** Cloudflare Images transforms (resize/format/optimize). Non-deterministic — available only in actions. */
|
|
609
|
-
readonly images: import("@lunora/images").Images;` : "";
|
|
848
|
+
readonly images: import("@lunora/bindings/images").Images;` : "";
|
|
610
849
|
const analyticsContextField = hasAnalytics ? `
|
|
611
850
|
/** Analytics Engine telemetry sink. Fire-and-forget and sampled; do not read it back in-handler. */
|
|
612
|
-
readonly analytics: import("@lunora/analytics").AnalyticsClient;` : "";
|
|
851
|
+
readonly analytics: import("@lunora/bindings/analytics").AnalyticsClient;` : "";
|
|
613
852
|
const pipelinesActionField = hasPipelines ? `
|
|
614
853
|
/** Pipelines ingestion sink (durable, R2-backed). Fire-and-forget and batched; do not read it back in-handler. */
|
|
615
|
-
readonly pipelines: import("@lunora/pipelines").PipelineClient;` : "";
|
|
854
|
+
readonly pipelines: import("@lunora/bindings/pipelines").PipelineClient;` : "";
|
|
616
855
|
const r2sqlActionField = hasR2sql ? `
|
|
617
856
|
/**
|
|
618
857
|
* R2 SQL over Apache Iceberg tables (window functions, DISTINCT, set operations). Non-deterministic — available only in actions. Reads here are NOT tracked by Lunora live queries.
|
|
619
858
|
*/
|
|
620
|
-
readonly r2sql: import("@lunora/r2sql").R2SqlClient;` : "";
|
|
859
|
+
readonly r2sql: import("@lunora/bindings/r2sql").R2SqlClient;` : "";
|
|
621
860
|
const hasWorkflows = workflows.length > 0;
|
|
622
861
|
const workflowsTypeImport = hasWorkflows ? `import type { WorkflowHandle } from "@lunora/workflow";
|
|
623
862
|
import type * as lunoraWorkflowDefinitions from "../workflows.js";
|
|
@@ -638,6 +877,35 @@ ${workflows.map((workflow) => ` get(name: ${JSON.stringify(workflow.exportNam
|
|
|
638
877
|
const workflowsOmit = hasWorkflows ? ` | "workflows"` : "";
|
|
639
878
|
const workflowsContextField = hasWorkflows ? `
|
|
640
879
|
readonly workflows: LunoraWorkflows;` : "";
|
|
880
|
+
const hasQueues = queues.length > 0;
|
|
881
|
+
const queuesTypeImport = hasQueues ? `import type { QueueProducer } from "@lunora/queue";
|
|
882
|
+
import type * as lunoraQueueDefinitions from "../queues.js";
|
|
883
|
+
` : "";
|
|
884
|
+
const queuesTypeBlock = hasQueues ? `
|
|
885
|
+
|
|
886
|
+
/** Message body type carried by a \`defineQueue\` definition (its phantom \`__lunoraBody\`). */
|
|
887
|
+
type QueueBodyOf<Definition> = Definition extends { __lunoraBody?: infer Body } ? (unknown extends Body ? unknown : NonNullable<Body>) : unknown;
|
|
888
|
+
|
|
889
|
+
/** This project's declared queues, addressable from \`ctx.queues\` by their \`lunora/queues.ts\` export name. */
|
|
890
|
+
export interface LunoraQueues {
|
|
891
|
+
${queues.map((queue) => ` readonly ${queue.exportName}: QueueProducer<QueueBodyOf<typeof lunoraQueueDefinitions.${queue.exportName}>>;`).join("\n")}
|
|
892
|
+
}` : "";
|
|
893
|
+
const queuesContextField = hasQueues ? `
|
|
894
|
+
readonly queues: LunoraQueues;` : "";
|
|
895
|
+
const identityTypeImport = identity ? `import type { InferIdentity } from "${base.server}";
|
|
896
|
+
import type * as lunoraIdentityContract from "../identity.js";
|
|
897
|
+
` : "";
|
|
898
|
+
const identityTypeBlock = identity ? `
|
|
899
|
+
|
|
900
|
+
/** 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. */
|
|
901
|
+
export type Identity = InferIdentity<typeof lunoraIdentityContract.${identity.exportName}>;
|
|
902
|
+
|
|
903
|
+
/** \`ctx.auth\` narrowed so \`getIdentity()\` resolves the declared {@link Identity} contract instead of the untyped claim bag. */
|
|
904
|
+
type NarrowedAuth = Omit<QueryCtxBase["auth"], "getIdentity"> & { getIdentity: () => Promise<Identity | null> };` : "";
|
|
905
|
+
const authOmit = identity ? ` | "auth"` : "";
|
|
906
|
+
const authContextField = identity ? `
|
|
907
|
+
readonly auth: NarrowedAuth;` : "";
|
|
908
|
+
const policyIdentityArgument = identity ? ", Identity" : "";
|
|
641
909
|
const server = `${GENERATED_HEADER}import { createPolicyDsl, initLunora, v as vBase } from "${base.server}";
|
|
642
910
|
import type {
|
|
643
911
|
ActionBuilder,
|
|
@@ -659,11 +927,11 @@ import type {
|
|
|
659
927
|
} from "${base.server}";
|
|
660
928
|
|
|
661
929
|
import type { DataModel, DatabaseReaderFacade, DatabaseWriterFacade, Doc, Id as IdOfTable, OrmReader, OrmWriter, Relations, TableName } from "./dataModel.js";
|
|
662
|
-
${aiTypeImport}${paymentsTypeImport}${containersTypeImport}${workflowsTypeImport}
|
|
930
|
+
${aiTypeImport}${paymentsTypeImport}${containersTypeImport}${workflowsTypeImport}${queuesTypeImport}${identityTypeImport}
|
|
663
931
|
export type { DataModel, Doc, Id, TableName } from "./dataModel.js";
|
|
664
932
|
|
|
665
933
|
/** Storage buckets this schema declares (\`v.storage("name")\`), narrowing \`ctx.storage.bucket(name)\`. */
|
|
666
|
-
export type StorageBucketName = ${storageBucketUnion};${envBlock}${workflowsTypeBlock}
|
|
934
|
+
export type StorageBucketName = ${storageBucketUnion};${envBlock}${workflowsTypeBlock}${queuesTypeBlock}${identityTypeBlock}
|
|
667
935
|
|
|
668
936
|
/**
|
|
669
937
|
* Project-typed contexts. The base contexts from \`@lunora/server\` are
|
|
@@ -692,22 +960,22 @@ type TypedTableQuery = (<T extends TableName>(table: T) => TableReader<Doc<T>>)
|
|
|
692
960
|
*/
|
|
693
961
|
type TypedTableGet = <T extends TableName>(id: IdOfTable<T>) => Promise<Doc<T> | null>;
|
|
694
962
|
|
|
695
|
-
export interface QueryCtx extends Omit<QueryCtxBase, "db" | "storage"> {
|
|
963
|
+
export interface QueryCtx extends Omit<QueryCtxBase, "db" | "storage"${authOmit}> {
|
|
696
964
|
readonly db: Omit<DatabaseReader, "query" | "get"> & DatabaseReaderFacade & { query: TypedTableQuery; get: TypedTableGet };
|
|
697
965
|
readonly orm: OrmReader;
|
|
698
|
-
readonly storage: ReadOnlyStorage<StorageBucketName>;${kvContextField}${analyticsContextField}
|
|
966
|
+
readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}${authContextField}
|
|
699
967
|
}
|
|
700
968
|
|
|
701
|
-
export interface MutationCtx extends Omit<MutationCtxBase, "db" | "storage"${workflowsOmit}> {
|
|
969
|
+
export interface MutationCtx extends Omit<MutationCtxBase, "db" | "storage"${workflowsOmit}${authOmit}> {
|
|
702
970
|
readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
|
|
703
971
|
readonly orm: OrmWriter;
|
|
704
|
-
readonly storage: ReadOnlyStorage<StorageBucketName>;${kvContextField}${analyticsContextField}${workflowsContextField}
|
|
972
|
+
readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}${workflowsContextField}${queuesContextField}${authContextField}
|
|
705
973
|
}
|
|
706
974
|
|
|
707
|
-
export interface ActionCtx extends Omit<ActionCtxBase, "db" | "storage"${workflowsOmit}> {
|
|
975
|
+
export interface ActionCtx extends Omit<ActionCtxBase, "db" | "storage"${workflowsOmit}${authOmit}> {
|
|
708
976
|
readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
|
|
709
977
|
readonly orm: OrmWriter;
|
|
710
|
-
readonly storage: StorageBase<StorageBucketName>;${aiActionField}${paymentsActionField}${containersActionField}${kvContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${r2sqlActionField}${workflowsContextField}
|
|
978
|
+
readonly storage: StorageBase<StorageBucketName>;${accessContextField}${aiActionField}${paymentsActionField}${containersActionField}${kvContextField}${flagsContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${r2sqlActionField}${workflowsContextField}${queuesContextField}${authContextField}
|
|
711
979
|
}
|
|
712
980
|
|
|
713
981
|
/**
|
|
@@ -747,7 +1015,7 @@ export const internalAction = lunoraBuilders.internalAction as unknown as Intern
|
|
|
747
1015
|
* Runtime-identical to \`@lunora/server\`'s \`definePolicy\`; only the types narrow,
|
|
748
1016
|
* so the \`rls()\` chain discovers a policy authored either way the same.
|
|
749
1017
|
*/
|
|
750
|
-
export const definePolicy = createPolicyDsl<DataModel, Relations>();
|
|
1018
|
+
export const definePolicy = createPolicyDsl<DataModel, Relations${policyIdentityArgument}>();
|
|
751
1019
|
|
|
752
1020
|
/**
|
|
753
1021
|
* The validator builder \`v\`, with \`v.id(...)\` constrained to THIS schema's
|
|
@@ -777,10 +1045,40 @@ const renderLifecycleManifest = (functions) => {
|
|
|
777
1045
|
}
|
|
778
1046
|
return manifest;
|
|
779
1047
|
};
|
|
780
|
-
const emitFunctions = (functions, migrations = []) => {
|
|
1048
|
+
const emitFunctions = (functions, migrations = [], useUmbrella = false, mutators = [], shapes = []) => {
|
|
781
1049
|
const hasFunctions = functions.length > 0;
|
|
782
|
-
const
|
|
1050
|
+
const base = baseSpecifiers(useUmbrella);
|
|
1051
|
+
const { dispatchBody, importBlock, installBlock, migrationBody, mutatorPaths, shapeBody } = renderFunctionRegistry(functions, migrations, mutators, shapes);
|
|
783
1052
|
const lifecycleHooks = renderLifecycleManifest(functions);
|
|
1053
|
+
const shapeTypeImport = shapes.length > 0 ? `import type { RegisteredShape } from "${base.server}";
|
|
1054
|
+
` : "";
|
|
1055
|
+
const shapeRegistry = shapes.length > 0 ? `
|
|
1056
|
+
/**
|
|
1057
|
+
* Replication-shape registry — one entry per \`defineShape\` in \`lunora/shapes.ts\`.
|
|
1058
|
+
* The generated ShardDO's \`resolveShape\` override looks a shape up by name and
|
|
1059
|
+
* evaluates its trusted \`compileWhere(ctx, args)\` to authorize + scope a
|
|
1060
|
+
* \`shape_subscribe\` (reads-as-permissions).
|
|
1061
|
+
*/
|
|
1062
|
+
export const LUNORA_SHAPES: Record<string, RegisteredShape> = {${shapeBody}};
|
|
1063
|
+
` : "";
|
|
1064
|
+
const mutatorPathsRegistry = mutatorPaths.length > 0 ? `
|
|
1065
|
+
/**
|
|
1066
|
+
* Custom-mutator function paths — the \`LUNORA_FUNCTIONS\` keys the generated
|
|
1067
|
+
* ShardDO's \`isCustomMutator\` override routes through the client-watermark push
|
|
1068
|
+
* protocol (\`x-lunora-client-id\`/\`x-lunora-client-seq\` ordering).
|
|
1069
|
+
*/
|
|
1070
|
+
export const LUNORA_MUTATOR_PATHS: ReadonlySet<string> = new Set([${mutatorPaths.map((path) => JSON.stringify(path)).join(", ")}]);
|
|
1071
|
+
` : "";
|
|
1072
|
+
const compiledArgsImport = installBlock.length > 0 ? `import { DEFER_VALIDATION as DEFER, installCompiledValidatorMap } from "${base.values}";
|
|
1073
|
+
` : "";
|
|
1074
|
+
const compiledArgsInstall = installBlock.length > 0 ? `
|
|
1075
|
+
/**
|
|
1076
|
+
* AOT-compiled argument validators (Worker-safe, no \`eval\`). Each is installed
|
|
1077
|
+
* onto its function's live \`.args\` object and consulted by the interpreted
|
|
1078
|
+
* parser as a zero-allocation fast path; anything it can't model is deferred.
|
|
1079
|
+
*/
|
|
1080
|
+
${installBlock}
|
|
1081
|
+
` : "";
|
|
784
1082
|
const caller = renderCaller(functions);
|
|
785
1083
|
const callerTypes = caller.types ? `
|
|
786
1084
|
${caller.types}
|
|
@@ -795,7 +1093,7 @@ ${caller.implementation}
|
|
|
795
1093
|
const callRegisteredHelper = hasFunctions ? `${CALL_REGISTERED_HELPER}
|
|
796
1094
|
|
|
797
1095
|
` : "";
|
|
798
|
-
return `${GENERATED_HEADER}${importBlock}import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
|
|
1096
|
+
return `${GENERATED_HEADER}${importBlock}${compiledArgsImport}${shapeTypeImport}import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
|
|
799
1097
|
${dataModelImport}
|
|
800
1098
|
/**
|
|
801
1099
|
* Single registered function, narrowed to the shape \`handleRpc\` needs.
|
|
@@ -821,7 +1119,7 @@ export interface RegisteredLunoraFunction {
|
|
|
821
1119
|
* emits (\`api[namespace][fn].__lunoraRef === "namespace:fn"\`).
|
|
822
1120
|
*/
|
|
823
1121
|
export const LUNORA_FUNCTIONS: Record<string, RegisteredLunoraFunction> = {${dispatchBody}};
|
|
824
|
-
|
|
1122
|
+
${compiledArgsInstall}${shapeRegistry}${mutatorPathsRegistry}
|
|
825
1123
|
/**
|
|
826
1124
|
* Connection-lifecycle manifest: the function paths the generated ShardDO
|
|
827
1125
|
* dispatches when a client's WebSocket connects (\`connect\`) or disconnects
|
|
@@ -1042,7 +1340,7 @@ const emitKvFragments = (hasKv) => {
|
|
|
1042
1340
|
kv?: (env: Record<string, unknown>) => KVNamespaceLike;`,
|
|
1043
1341
|
contextField: `
|
|
1044
1342
|
kv,`,
|
|
1045
|
-
importLines: [`import type { Kv, KVNamespaceLike } from "@lunora/kv";`, `import { createKv } from "@lunora/kv";`],
|
|
1343
|
+
importLines: [`import type { Kv, KVNamespaceLike } from "@lunora/bindings/kv";`, `import { createKv } from "@lunora/bindings/kv";`],
|
|
1046
1344
|
stub: `
|
|
1047
1345
|
const kvStub: Kv = {
|
|
1048
1346
|
delete: async () => {
|
|
@@ -1067,6 +1365,116 @@ const kvStub: Kv = {
|
|
|
1067
1365
|
`
|
|
1068
1366
|
};
|
|
1069
1367
|
};
|
|
1368
|
+
const emitFlagsFragments = (hasFlags, flagsSpecifier) => {
|
|
1369
|
+
if (!hasFlags) {
|
|
1370
|
+
return EMPTY_HELPER_FRAGMENTS;
|
|
1371
|
+
}
|
|
1372
|
+
return {
|
|
1373
|
+
build: `
|
|
1374
|
+
const flags: import("${flagsSpecifier}").LunoraFlags = createFlags({
|
|
1375
|
+
hooks: flagsConfig.hooks,
|
|
1376
|
+
logger: flagsConfig.logger,
|
|
1377
|
+
provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
|
|
1378
|
+
targetingKey: () => flagsConfig.identify?.({ identity: identity ?? null, userId: userId ?? null }),
|
|
1379
|
+
});
|
|
1380
|
+
`,
|
|
1381
|
+
configField: `
|
|
1382
|
+
flags?: (env: Record<string, unknown>) => import("${flagsSpecifier}").Provider;`,
|
|
1383
|
+
contextField: `
|
|
1384
|
+
flags,`,
|
|
1385
|
+
importLines: [`import { createFlags } from "${flagsSpecifier}";`, `import flagsConfig from "../flags.js";`],
|
|
1386
|
+
stub: ""
|
|
1387
|
+
};
|
|
1388
|
+
};
|
|
1389
|
+
const emitAccessFragments = (hasAccessFacade) => {
|
|
1390
|
+
if (!hasAccessFacade) {
|
|
1391
|
+
return EMPTY_HELPER_FRAGMENTS;
|
|
1392
|
+
}
|
|
1393
|
+
return {
|
|
1394
|
+
build: `
|
|
1395
|
+
const access = accessFacade(identity, userId);
|
|
1396
|
+
`,
|
|
1397
|
+
configField: "",
|
|
1398
|
+
contextField: `
|
|
1399
|
+
access,`,
|
|
1400
|
+
importLines: [`import { accessFacade } from "@lunora/cloudflare-access/context";`],
|
|
1401
|
+
stub: ""
|
|
1402
|
+
};
|
|
1403
|
+
};
|
|
1404
|
+
const emitFlagsOverrides = (flagKeys, hasFlags, flagsSpecifier) => {
|
|
1405
|
+
if (!hasFlags) {
|
|
1406
|
+
return { constant: "", evaluateOverride: "", subscriptionOverride: "" };
|
|
1407
|
+
}
|
|
1408
|
+
const clientBuild = (targetingKey) => `
|
|
1409
|
+
const env = (this.env ?? {}) as Record<string, unknown>;
|
|
1410
|
+
const flags: import("${flagsSpecifier}").LunoraFlags = createFlags({
|
|
1411
|
+
hooks: flagsConfig.hooks,
|
|
1412
|
+
logger: flagsConfig.logger,
|
|
1413
|
+
provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
|
|
1414
|
+
targetingKey: ${targetingKey},
|
|
1415
|
+
});`;
|
|
1416
|
+
const constant = `
|
|
1417
|
+
/** Statically-discovered feature flags (\`ctx.flags.<type>("key")\` reads) served via \`__lunora_admin__:listFlags\` + the reactive \`__lunora_flags__:\` channel. */
|
|
1418
|
+
const LUNORA_FLAG_KEYS: ReadonlyArray<{ key: string; type: "boolean" | "number" | "object" | "string" }> = ${JSON.stringify(flagKeys, void 0, 4)};
|
|
1419
|
+
`;
|
|
1420
|
+
const evaluateOverride = `
|
|
1421
|
+
protected override async evaluateFlags(context?: Record<string, unknown>): Promise<FlagsResult> {${clientBuild("undefined")}
|
|
1422
|
+
const evalContext = context as import("${flagsSpecifier}").EvaluationContext | undefined;
|
|
1423
|
+
const evaluations: FlagsResult["flags"] = [];
|
|
1424
|
+
|
|
1425
|
+
for (const entry of LUNORA_FLAG_KEYS) {
|
|
1426
|
+
// eslint-disable-next-line no-await-in-loop -- flags evaluate sequentially; each shares the single memoized provider client
|
|
1427
|
+
const details =
|
|
1428
|
+
entry.type === "boolean"
|
|
1429
|
+
? await flags.details.boolean(entry.key, false, evalContext)
|
|
1430
|
+
: entry.type === "number"
|
|
1431
|
+
? await flags.details.number(entry.key, 0, evalContext)
|
|
1432
|
+
: entry.type === "string"
|
|
1433
|
+
? await flags.details.string(entry.key, "", evalContext)
|
|
1434
|
+
: await flags.details.object(entry.key, {}, evalContext);
|
|
1435
|
+
|
|
1436
|
+
evaluations.push({ errorCode: details.errorCode, key: entry.key, reason: details.reason, type: entry.type, value: details.value, variant: details.variant });
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
return { configured: true, flags: evaluations };
|
|
1440
|
+
}
|
|
1441
|
+
`;
|
|
1442
|
+
const subscriptionOverride = `
|
|
1443
|
+
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 })")}
|
|
1444
|
+
const key = typeof args.key === "string" ? args.key : "";
|
|
1445
|
+
|
|
1446
|
+
// SECURITY: the reactive channel is public (any socket, no auth). Serve
|
|
1447
|
+
// ONLY statically-discovered flag keys — an arbitrary client-supplied key
|
|
1448
|
+
// would let a subscriber probe the value of internal/unreleased flags the
|
|
1449
|
+
// app never exposes. Unknown key ⇒ the "nothing to deliver" sentinel.
|
|
1450
|
+
// eslint-disable-next-line unicorn/no-null -- the base hook's "nothing to deliver" sentinel
|
|
1451
|
+
if (key.length === 0 || !LUNORA_FLAG_KEYS.some((entry) => entry.key === key)) {
|
|
1452
|
+
return Promise.resolve(null);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// SECURITY: evaluate under the socket's server-verified identity ONLY
|
|
1456
|
+
// (the targetingKey resolved above). Client-supplied targeting context is
|
|
1457
|
+
// NOT honored on this public channel — otherwise a subscriber could spoof
|
|
1458
|
+
// targeting attributes (e.g. plan/role) to unlock a flag gated on them.
|
|
1459
|
+
const context = undefined;
|
|
1460
|
+
|
|
1461
|
+
if (args.type === "number") {
|
|
1462
|
+
return flags.number(key, typeof args.default === "number" ? args.default : 0, context);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
if (args.type === "string") {
|
|
1466
|
+
return flags.string(key, typeof args.default === "string" ? args.default : "", context);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
if (args.type === "object") {
|
|
1470
|
+
return flags.object(key, (args.default ?? {}) as import("${flagsSpecifier}").JsonValue, context);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
return flags.boolean(key, typeof args.default === "boolean" ? args.default : false, context);
|
|
1474
|
+
}
|
|
1475
|
+
`;
|
|
1476
|
+
return { constant, evaluateOverride, subscriptionOverride };
|
|
1477
|
+
};
|
|
1070
1478
|
const emitAnalyticsFragments = (hasAnalytics) => {
|
|
1071
1479
|
if (!hasAnalytics) {
|
|
1072
1480
|
return EMPTY_HELPER_FRAGMENTS;
|
|
@@ -1082,8 +1490,8 @@ const emitAnalyticsFragments = (hasAnalytics) => {
|
|
|
1082
1490
|
contextField: `
|
|
1083
1491
|
analytics,`,
|
|
1084
1492
|
importLines: [
|
|
1085
|
-
`import type { AnalyticsClient, AnalyticsEngineDatasetLike } from "@lunora/analytics";`,
|
|
1086
|
-
`import { createAnalytics } from "@lunora/analytics";`
|
|
1493
|
+
`import type { AnalyticsClient, AnalyticsEngineDatasetLike } from "@lunora/bindings/analytics";`,
|
|
1494
|
+
`import { createAnalytics } from "@lunora/bindings/analytics";`
|
|
1087
1495
|
],
|
|
1088
1496
|
stub: `
|
|
1089
1497
|
const analyticsStub: AnalyticsClient = {
|
|
@@ -1112,7 +1520,7 @@ const emitImagesFragments = (hasImages) => {
|
|
|
1112
1520
|
// ActionCtx-only: woven onto the action ctx object, never query/mutation.
|
|
1113
1521
|
contextField: `
|
|
1114
1522
|
images,`,
|
|
1115
|
-
importLines: [`import type { Images, ImagesBindingLike } from "@lunora/images";`, `import { createImages } from "@lunora/images";`],
|
|
1523
|
+
importLines: [`import type { Images, ImagesBindingLike } from "@lunora/bindings/images";`, `import { createImages } from "@lunora/bindings/images";`],
|
|
1116
1524
|
stub: `
|
|
1117
1525
|
const imagesStub: Images = {
|
|
1118
1526
|
info: async () => {
|
|
@@ -1211,10 +1619,10 @@ const emitR2sqlFragments = (hasR2sql) => {
|
|
|
1211
1619
|
// ActionCtx-only: attached via the \`ctx.r2sql = r2sql\` assignment in the
|
|
1212
1620
|
// \`isAction\` block, never the every-ctx object literal.
|
|
1213
1621
|
contextField: "",
|
|
1214
|
-
importLines: [`import type { R2SqlClient } from "@lunora/r2sql";`, `import { createR2Sql } from "@lunora/r2sql";`],
|
|
1622
|
+
importLines: [`import type { R2SqlClient } from "@lunora/bindings/r2sql";`, `import { createR2Sql } from "@lunora/bindings/r2sql";`],
|
|
1215
1623
|
// The stub is typed `R2SqlClient`, so TS flags a missing method at build
|
|
1216
1624
|
// time — but it must stay structurally in sync with that interface
|
|
1217
|
-
// (`@lunora/r2sql` client.ts) when a method is added there.
|
|
1625
|
+
// (`@lunora/bindings/r2sql` client.ts) when a method is added there.
|
|
1218
1626
|
stub: `
|
|
1219
1627
|
const r2sqlStub: R2SqlClient = {
|
|
1220
1628
|
describe: async () => {
|
|
@@ -1239,17 +1647,46 @@ const r2sqlStub: R2SqlClient = {
|
|
|
1239
1647
|
`
|
|
1240
1648
|
};
|
|
1241
1649
|
};
|
|
1242
|
-
const
|
|
1650
|
+
const emitPipelinesFragments = (hasPipelines) => {
|
|
1651
|
+
if (!hasPipelines) {
|
|
1652
|
+
return EMPTY_HELPER_FRAGMENTS;
|
|
1653
|
+
}
|
|
1654
|
+
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().");`;
|
|
1655
|
+
return {
|
|
1656
|
+
build: `
|
|
1657
|
+
const pipelinesBinding = config.pipelines?.(env) ?? (env as Record<string, unknown>).PIPELINES;
|
|
1658
|
+
const pipelines: PipelineClient = pipelinesBinding ? createPipelines({ binding: pipelinesBinding as PipelineBindingLike }) : pipelinesStub;
|
|
1659
|
+
`,
|
|
1660
|
+
configField: `
|
|
1661
|
+
pipelines?: (env: Record<string, unknown>) => PipelineBindingLike;`,
|
|
1662
|
+
// ActionCtx-only: attached via the \`ctx.pipelines = pipelines\` assignment
|
|
1663
|
+
// in the \`isAction\` block, never the every-ctx object literal.
|
|
1664
|
+
contextField: "",
|
|
1665
|
+
importLines: [
|
|
1666
|
+
`import type { PipelineBindingLike, PipelineClient } from "@lunora/bindings/pipelines";`,
|
|
1667
|
+
`import { createPipelines } from "@lunora/bindings/pipelines";`
|
|
1668
|
+
],
|
|
1669
|
+
stub: `
|
|
1670
|
+
const pipelinesStub: PipelineClient = {
|
|
1671
|
+
send: async () => {
|
|
1672
|
+
${pipelinesMissing}
|
|
1673
|
+
},
|
|
1674
|
+
};
|
|
1675
|
+
`
|
|
1676
|
+
};
|
|
1677
|
+
};
|
|
1678
|
+
const emitContainers = (containers, jurisdiction) => {
|
|
1243
1679
|
if (containers.length === 0) {
|
|
1244
1680
|
return "";
|
|
1245
1681
|
}
|
|
1682
|
+
const jurisdictionArgument = jurisdiction ? `, ${JSON.stringify(jurisdiction)}` : "";
|
|
1246
1683
|
const classes = containers.map((container) => {
|
|
1247
1684
|
assertIdentifier(container.exportName, `container export "${container.exportName}"`);
|
|
1248
1685
|
assertIdentifier(container.className, `container class "${container.className}"`);
|
|
1249
1686
|
return `/** Container DO for the \`${container.exportName}\` definition (binding \`${container.bindingName}\`). */
|
|
1250
1687
|
export class ${container.className} extends LunoraContainer {
|
|
1251
1688
|
public constructor(ctx: ConstructorParameters<typeof LunoraContainer>[0], env: Record<string, unknown>) {
|
|
1252
|
-
super(ctx, env, ${container.exportName}, "${container.exportName}");
|
|
1689
|
+
super(ctx, env, ${container.exportName}, "${container.exportName}"${jurisdictionArgument});
|
|
1253
1690
|
}
|
|
1254
1691
|
}
|
|
1255
1692
|
`;
|
|
@@ -1261,14 +1698,21 @@ export class ${container.className} extends LunoraContainer {
|
|
|
1261
1698
|
* requires each \`containers[].class_name\` to be exported by the worker:
|
|
1262
1699
|
*
|
|
1263
1700
|
* \`export * from "./lunora/_generated/containers.js";\`
|
|
1701
|
+
*
|
|
1702
|
+
* \`ContainerProxy\` is re-exported alongside them: the egress-interception path
|
|
1703
|
+
* (\`allowedHosts\`/\`deniedHosts\`/\`interceptHttps\` and the runtime
|
|
1704
|
+
* \`handle.egress\` controls) routes container outbound traffic through this
|
|
1705
|
+
* WorkerEntrypoint, so it too must be exported by the deployed worker.
|
|
1264
1706
|
*/
|
|
1265
|
-
import LunoraContainer from "@lunora/container/do";
|
|
1707
|
+
import { LunoraContainer } from "@lunora/container/do";
|
|
1266
1708
|
|
|
1267
1709
|
import { ${imports} } from "../containers.js";
|
|
1268
1710
|
|
|
1711
|
+
export { ContainerProxy } from "@lunora/container/do";
|
|
1712
|
+
|
|
1269
1713
|
${classes}`;
|
|
1270
1714
|
};
|
|
1271
|
-
const emitContainerFragments = (containers) => {
|
|
1715
|
+
const emitContainerFragments = (containers, jurisdiction) => {
|
|
1272
1716
|
if (containers.length === 0) {
|
|
1273
1717
|
return { build: "", contextField: "", importLines: [], specs: "" };
|
|
1274
1718
|
}
|
|
@@ -1281,8 +1725,11 @@ const emitContainerFragments = (containers) => {
|
|
|
1281
1725
|
return ` { binding: "${container.bindingName}", exportName: "${container.exportName}"${maxInstances} },`;
|
|
1282
1726
|
}).join("\n");
|
|
1283
1727
|
return {
|
|
1728
|
+
// Schema `.jurisdiction("…")` pins every container DO this shard reaches
|
|
1729
|
+
// to the data-residency region; the arg is omitted when undeclared so
|
|
1730
|
+
// existing generated output is unchanged.
|
|
1284
1731
|
build: `
|
|
1285
|
-
const containers = createContainerContext(env, LUNORA_CONTAINERS);
|
|
1732
|
+
const containers = createContainerContext(env, LUNORA_CONTAINERS${jurisdiction ? `, ${JSON.stringify(jurisdiction)}` : ""});
|
|
1286
1733
|
`,
|
|
1287
1734
|
contextField: `
|
|
1288
1735
|
containers,`,
|
|
@@ -1335,6 +1782,32 @@ type WorkflowOutputOf<Definition> = Definition extends { __output?: infer Output
|
|
|
1335
1782
|
|
|
1336
1783
|
${classes}`;
|
|
1337
1784
|
};
|
|
1785
|
+
const emitQueues = (queues) => {
|
|
1786
|
+
const pushQueues = queues.filter((queue) => queue.mode === "push");
|
|
1787
|
+
if (pushQueues.length === 0) {
|
|
1788
|
+
return "";
|
|
1789
|
+
}
|
|
1790
|
+
for (const queue of pushQueues) {
|
|
1791
|
+
assertIdentifier(queue.exportName, `queue export "${queue.exportName}"`);
|
|
1792
|
+
}
|
|
1793
|
+
const imports = pushQueues.map((queue) => queue.exportName).join(", ");
|
|
1794
|
+
const entries = pushQueues.map((queue) => ` ${JSON.stringify(queue.name)}: { definition: ${queue.exportName}, exportName: ${JSON.stringify(queue.exportName)} },`).join("\n");
|
|
1795
|
+
return `${GENERATED_HEADER}/**
|
|
1796
|
+
* Push-consumer registry for the queues declared in \`lunora/queues.ts\`. The
|
|
1797
|
+
* composed worker's \`queue(batch, env, ctx)\` entry routes each delivered batch
|
|
1798
|
+
* by \`batch.queue\` to the matching \`defineQueue\` handler. Wired automatically
|
|
1799
|
+
* by \`defineApp\` — you don't import this directly.
|
|
1800
|
+
*/
|
|
1801
|
+
import type { QueueRegistry } from "@lunora/queue";
|
|
1802
|
+
|
|
1803
|
+
import { ${imports} } from "../queues.js";
|
|
1804
|
+
|
|
1805
|
+
/** Stable wrangler queue name → { definition, exportName } for batch routing. */
|
|
1806
|
+
export const LUNORA_QUEUE_REGISTRY: QueueRegistry = {
|
|
1807
|
+
${entries}
|
|
1808
|
+
};
|
|
1809
|
+
`;
|
|
1810
|
+
};
|
|
1338
1811
|
const emitWorkflowFragments = (workflows) => {
|
|
1339
1812
|
if (workflows.length === 0) {
|
|
1340
1813
|
return { build: "", contextField: "", importLines: [], specs: "" };
|
|
@@ -1360,6 +1833,31 @@ ${specEntries}
|
|
|
1360
1833
|
`
|
|
1361
1834
|
};
|
|
1362
1835
|
};
|
|
1836
|
+
const emitQueueFragments = (queues) => {
|
|
1837
|
+
if (queues.length === 0) {
|
|
1838
|
+
return { build: "", contextField: "", importLines: [], specs: "" };
|
|
1839
|
+
}
|
|
1840
|
+
for (const queue of queues) {
|
|
1841
|
+
assertIdentifier(queue.exportName, `queue export "${queue.exportName}"`);
|
|
1842
|
+
assertIdentifier(queue.bindingName, `queue binding "${queue.bindingName}"`);
|
|
1843
|
+
}
|
|
1844
|
+
const specEntries = queues.map((queue) => ` { binding: "${queue.bindingName}", exportName: "${queue.exportName}", name: ${JSON.stringify(queue.name)} },`).join("\n");
|
|
1845
|
+
return {
|
|
1846
|
+
build: `
|
|
1847
|
+
const queues = createQueueContext(env, LUNORA_QUEUES);
|
|
1848
|
+
`,
|
|
1849
|
+
contextField: `
|
|
1850
|
+
queues,`,
|
|
1851
|
+
importLines: [`import type { QueueBindingSpec } from "@lunora/queue";`, `import { createQueueContext } from "@lunora/queue";`],
|
|
1852
|
+
// eslint-disable-next-line no-secrets/no-secrets -- the emitted readonly-array type annotation is dense generated TS, not a credential
|
|
1853
|
+
specs: `
|
|
1854
|
+
/** Wiring specs for \`ctx.queues\` (codegen-derived from \`lunora/queues.ts\`). */
|
|
1855
|
+
const LUNORA_QUEUES: ReadonlyArray<QueueBindingSpec> = [
|
|
1856
|
+
${specEntries}
|
|
1857
|
+
];
|
|
1858
|
+
`
|
|
1859
|
+
};
|
|
1860
|
+
};
|
|
1363
1861
|
const emitWorkflowsMetadataFragments = (workflows) => {
|
|
1364
1862
|
if (workflows.length === 0) {
|
|
1365
1863
|
return { constant: "", override: "" };
|
|
@@ -1386,6 +1884,33 @@ const LUNORA_WORKFLOWS_INFO: WorkflowsResult = ${JSON.stringify(metadata, void 0
|
|
|
1386
1884
|
`
|
|
1387
1885
|
};
|
|
1388
1886
|
};
|
|
1887
|
+
const emitQueuesMetadataFragments = (queues) => {
|
|
1888
|
+
if (queues.length === 0) {
|
|
1889
|
+
return { constant: "", override: "" };
|
|
1890
|
+
}
|
|
1891
|
+
const metadata = {
|
|
1892
|
+
queues: queues.map((queue) => {
|
|
1893
|
+
return {
|
|
1894
|
+
binding: queue.bindingName,
|
|
1895
|
+
...queue.tuning.deadLetterQueue === void 0 ? {} : { deadLetterQueue: queue.tuning.deadLetterQueue },
|
|
1896
|
+
exportName: queue.exportName,
|
|
1897
|
+
mode: queue.mode,
|
|
1898
|
+
name: queue.name
|
|
1899
|
+
};
|
|
1900
|
+
})
|
|
1901
|
+
};
|
|
1902
|
+
return {
|
|
1903
|
+
constant: `
|
|
1904
|
+
/** Read-only declared-queue metadata (discovered from \`lunora/queues.ts\`) served via \`__lunora_admin__:listQueues\` for the studio's queues view. */
|
|
1905
|
+
const LUNORA_QUEUES_INFO: QueuesResult = ${JSON.stringify(metadata, void 0, 4)};
|
|
1906
|
+
`,
|
|
1907
|
+
override: `
|
|
1908
|
+
protected override queuesMetadata(): QueuesResult {
|
|
1909
|
+
return LUNORA_QUEUES_INFO;
|
|
1910
|
+
}
|
|
1911
|
+
`
|
|
1912
|
+
};
|
|
1913
|
+
};
|
|
1389
1914
|
const emitPaymentFragments = (hasPayments) => {
|
|
1390
1915
|
if (!hasPayments) {
|
|
1391
1916
|
return { build: "", configField: "", contextField: "", imports: [], stub: "" };
|
|
@@ -1441,13 +1966,15 @@ const paymentStub: LunoraPayment = {
|
|
|
1441
1966
|
`
|
|
1442
1967
|
};
|
|
1443
1968
|
};
|
|
1444
|
-
const buildDoTypeImports = (hasVectors, hasWorkflows) => [
|
|
1969
|
+
const buildDoTypeImports = (hasVectors, hasWorkflows, hasQueues, hasFlags) => [
|
|
1445
1970
|
"AdvisoryFinding",
|
|
1446
1971
|
"DatabaseWriterLike",
|
|
1447
1972
|
"DataMigrationLike",
|
|
1973
|
+
...hasFlags ? ["FlagsResult"] : [],
|
|
1448
1974
|
"LogSink",
|
|
1449
1975
|
"MaskPoliciesResult",
|
|
1450
1976
|
"MigrationRunResult",
|
|
1977
|
+
...hasQueues ? ["QueuesResult"] : [],
|
|
1451
1978
|
"RunShardApplyCdcArgs",
|
|
1452
1979
|
"RunShardMigrationArgs",
|
|
1453
1980
|
"RlsPoliciesResult",
|
|
@@ -1469,36 +1996,50 @@ const buildDoTypeImports = (hasVectors, hasWorkflows) => [
|
|
|
1469
1996
|
const emitShard = ({
|
|
1470
1997
|
advisories = [],
|
|
1471
1998
|
containers = [],
|
|
1999
|
+
flagKeys = [],
|
|
2000
|
+
hasAccessFacade = false,
|
|
1472
2001
|
hasAi = false,
|
|
1473
2002
|
hasAnalytics = false,
|
|
1474
2003
|
hasBrowser = false,
|
|
2004
|
+
hasFlags = false,
|
|
1475
2005
|
hasHyperdrive = false,
|
|
1476
2006
|
hasImages = false,
|
|
1477
2007
|
hasKv = false,
|
|
1478
2008
|
hasPayments = false,
|
|
2009
|
+
hasPipelines = false,
|
|
1479
2010
|
hasR2sql = false,
|
|
1480
2011
|
maskMetadata,
|
|
2012
|
+
mutators = [],
|
|
2013
|
+
queues = [],
|
|
1481
2014
|
rlsMetadata,
|
|
1482
2015
|
schema,
|
|
2016
|
+
shapes = [],
|
|
1483
2017
|
storageRules,
|
|
1484
2018
|
studioFeatures,
|
|
1485
2019
|
useUmbrella = false,
|
|
1486
2020
|
workflows = []
|
|
1487
2021
|
}) => {
|
|
1488
2022
|
const base = baseSpecifiers(useUmbrella);
|
|
2023
|
+
const hasMutators = mutators.length > 0;
|
|
2024
|
+
const hasShapes = shapes.length > 0;
|
|
1489
2025
|
const { build: aiBuild, configField: aiConfigField, contextField: aiContextField, stub: aiStub } = emitAiFragments(hasAi);
|
|
2026
|
+
const accessFragments = emitAccessFragments(hasAccessFacade);
|
|
1490
2027
|
const kvFragments = emitKvFragments(hasKv);
|
|
2028
|
+
const flagsFragments = emitFlagsFragments(hasFlags, base.flags);
|
|
2029
|
+
const flagsOverrides = emitFlagsOverrides(flagKeys, hasFlags, base.flags);
|
|
1491
2030
|
const analyticsFragments = emitAnalyticsFragments(hasAnalytics);
|
|
1492
2031
|
const imagesFragments = emitImagesFragments(hasImages);
|
|
1493
2032
|
const hyperdriveFragments = emitHyperdriveFragments(hasHyperdrive);
|
|
1494
2033
|
const browserFragments = emitBrowserFragments(hasBrowser);
|
|
1495
2034
|
const r2sqlFragments = emitR2sqlFragments(hasR2sql);
|
|
2035
|
+
const pipelinesFragments = emitPipelinesFragments(hasPipelines);
|
|
2036
|
+
const { build: queuesBuild, contextField: queuesContextField, importLines: queueImportLines, specs: queueSpecs } = emitQueueFragments(queues);
|
|
1496
2037
|
const {
|
|
1497
2038
|
build: containersBuild,
|
|
1498
2039
|
contextField: containersContextField,
|
|
1499
2040
|
importLines: containerImportLines,
|
|
1500
2041
|
specs: containerSpecs
|
|
1501
|
-
} = emitContainerFragments(containers);
|
|
2042
|
+
} = emitContainerFragments(containers, schema.jurisdiction);
|
|
1502
2043
|
const {
|
|
1503
2044
|
build: workflowsBuild,
|
|
1504
2045
|
contextField: workflowsContextField,
|
|
@@ -1517,20 +2058,29 @@ const emitShard = ({
|
|
|
1517
2058
|
const maskData = maskMetadata ?? { columns: [] };
|
|
1518
2059
|
const storageRulesData = storageRules ?? { rules: [] };
|
|
1519
2060
|
const studioFeaturesData = studioFeatures ?? {
|
|
2061
|
+
analytics: false,
|
|
2062
|
+
auth: false,
|
|
2063
|
+
containers: false,
|
|
2064
|
+
flags: false,
|
|
2065
|
+
kv: false,
|
|
1520
2066
|
mail: false,
|
|
1521
2067
|
payments: false,
|
|
2068
|
+
queues: false,
|
|
1522
2069
|
scheduler: false,
|
|
1523
2070
|
storage: false,
|
|
1524
2071
|
vectors: false,
|
|
1525
2072
|
workflows: false
|
|
1526
2073
|
};
|
|
1527
2074
|
const { constant: workflowsMetadataConst, override: workflowsMetadataOverride } = emitWorkflowsMetadataFragments(workflows);
|
|
2075
|
+
const { constant: queuesMetadataConst, override: queuesMetadataOverride } = emitQueuesMetadataFragments(queues);
|
|
1528
2076
|
const hasVectors = schema.vectorIndexes.length > 0;
|
|
1529
2077
|
const hasGlobalTables = schema.tables.some((table) => table.shardMode === "global");
|
|
1530
2078
|
const hasHyperdriveGlobal = schema.tables.some((table) => table.shardMode === "global" && table.globalBackend === "hyperdrive");
|
|
1531
2079
|
const hasD1Global = schema.tables.some((table) => table.shardMode === "global" && table.globalBackend !== "hyperdrive");
|
|
2080
|
+
const hasSourcedTables = schema.tables.some((table) => table.externalSource !== void 0);
|
|
1532
2081
|
if (hasD1Global && hasHyperdriveGlobal) {
|
|
1533
|
-
throw new
|
|
2082
|
+
throw new LunoraError(
|
|
2083
|
+
"INTERNAL",
|
|
1534
2084
|
'lunora codegen: mixing `.global()` (D1) and `.global({ backend: "hyperdrive" })` tables in one app is not supported yet — use a single global backend.'
|
|
1535
2085
|
);
|
|
1536
2086
|
}
|
|
@@ -1539,41 +2089,113 @@ const emitShard = ({
|
|
|
1539
2089
|
const tableIndexes = buildTableIndexes(schema);
|
|
1540
2090
|
const tableColumns = buildTableColumns(schema);
|
|
1541
2091
|
const storageColumns = buildStorageColumns(schema);
|
|
1542
|
-
const doTypeImports = buildDoTypeImports(hasVectors, workflows.length > 0);
|
|
2092
|
+
const doTypeImports = buildDoTypeImports(hasVectors, workflows.length > 0, queues.length > 0, hasFlags);
|
|
2093
|
+
if (hasShapes) {
|
|
2094
|
+
doTypeImports.push("WhereInput");
|
|
2095
|
+
}
|
|
1543
2096
|
const relationFanout = emitRelationFanout(hasGlobalTables);
|
|
2097
|
+
const shapeResolveOverride = hasShapes ? `
|
|
2098
|
+
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 {
|
|
2099
|
+
const shape = LUNORA_SHAPES[name];
|
|
2100
|
+
|
|
2101
|
+
if (!shape) {
|
|
2102
|
+
return undefined;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
this.ensureMigrated();
|
|
2106
|
+
|
|
2107
|
+
// Trusted ctx from the socket's OWN verified identity — the client
|
|
2108
|
+
// supplies only the shape name + args; \`compileWhere\` validates the
|
|
2109
|
+
// args then runs the shape's \`where(ctx, args)\` under that identity,
|
|
2110
|
+
// so which rows replicate is a server decision (reads-as-permissions).
|
|
2111
|
+
const ctx = this.buildCtx({ functionPath: \`__shape__:\${name}\`, identity });
|
|
2112
|
+
const shapeWhere = shape.compileWhere(ctx, args) as unknown as WhereInput;
|
|
2113
|
+
|
|
2114
|
+
// AND-compose with the table's RLS read base-where. A shape runs no
|
|
2115
|
+
// procedure, so the \`.use(rls(...))\` middleware never fires; without
|
|
2116
|
+
// this merge its reads would bypass every read policy on the table
|
|
2117
|
+
// (rows the caller can't see would replicate). \`composeShapeReadWhere\`
|
|
2118
|
+
// evaluates the table's read policies under this same trusted ctx —
|
|
2119
|
+
// exactly the request-time path — and fails closed under a
|
|
2120
|
+
// \`.rls("required")\` schema for a non-\`.public()\`, policy-less table.
|
|
2121
|
+
const effectiveWhere = composeShapeReadWhere(LUNORA_RLS_READ_REGISTRY, {
|
|
2122
|
+
ctx,
|
|
2123
|
+
identity: identity?.identity ?? null,
|
|
2124
|
+
rlsRequired: (schema as unknown as { rlsMode?: string }).rlsMode === "required",
|
|
2125
|
+
roles: (ctx as { auth?: { roles?: readonly string[] } }).auth?.roles ?? [],
|
|
2126
|
+
shapeWhere,
|
|
2127
|
+
table: shape.table,
|
|
2128
|
+
tablePublic: (schema as unknown as { tables: Record<string, { isPublic?: boolean }> }).tables[shape.table]?.isPublic === true,
|
|
2129
|
+
userId: identity?.userId ?? null,
|
|
2130
|
+
});
|
|
2131
|
+
|
|
2132
|
+
// A live shape pokes only from its OWN shard's op-log, so a \`where()\`
|
|
2133
|
+
// that joins to a \`.shardBy()\` table (rows in another DO) is rejected
|
|
2134
|
+
// here — the first point the compiled predicate + shard modes are both
|
|
2135
|
+
// known. Remedy: denormalize, or move the joined table to \`.global()\`.
|
|
2136
|
+
assertShapeShardable(effectiveWhere, schema as unknown as SchemaLike, shape.table);
|
|
2137
|
+
|
|
2138
|
+
// A \`.global()\` table lives in D1 (no per-DO op-log): flag it so the
|
|
2139
|
+
// base serves it through the latency-tiered poll path (\`readGlobalShapeRows\`)
|
|
2140
|
+
// instead of the CDC poke path.
|
|
2141
|
+
const isGlobal = (schema as unknown as SchemaLike).tables[shape.table]?.shardMode?.kind === "global";
|
|
2142
|
+
|
|
2143
|
+
return { columns: shape.columns, effectiveWhere, global: isGlobal, table: shape.table };
|
|
2144
|
+
}
|
|
2145
|
+
` : "";
|
|
2146
|
+
const customMutatorOverride = hasMutators ? `
|
|
2147
|
+
protected override isCustomMutator(functionPath: string): boolean {
|
|
2148
|
+
return LUNORA_MUTATOR_PATHS.has(functionPath);
|
|
2149
|
+
}
|
|
2150
|
+
` : "";
|
|
2151
|
+
const shapeReadRegistryConst = hasShapes ? `
|
|
2152
|
+
/** 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. */
|
|
2153
|
+
const LUNORA_RLS_READ_REGISTRY = buildRlsReadRegistry(Object.values(LUNORA_FUNCTIONS));
|
|
2154
|
+
` : "";
|
|
2155
|
+
const shapeGuardImport = hasShapes ? "assertShapeShardable, " : "";
|
|
1544
2156
|
const importLines = [
|
|
1545
2157
|
`import type { ${doTypeImports.join(", ")} } from "${base.do}";`,
|
|
1546
|
-
`import { applyCdcChanges, createShardCtxDb, runDataMigration, runShardMigrations, ${relationFanout.importFragment}ShardDO as ShardDOBase } from "${base.do}";`,
|
|
1547
|
-
|
|
2158
|
+
`import { applyCdcChanges, ${shapeGuardImport}createShardCtxDb, ${hasSourcedTables ? "isSourceDue, pullExternalSourceTick, " : ""}runDataMigration, runShardMigrations, ${relationFanout.importFragment}ShardDO as ShardDOBase } from "${base.do}";`,
|
|
2159
|
+
...hasSourcedTables ? [`import type { ExternalSourceLike, SourceClientLike } from "${base.do}";`] : [],
|
|
2160
|
+
// `asBucketStorage` (the bucket-aware `ctx.storage` wrapper) and
|
|
2161
|
+
// `createSecrets` (the `ctx.secrets` core built-in) live in
|
|
1548
2162
|
// `@lunora/server`, the single source — imported here rather than stamped
|
|
1549
|
-
// inline into every generated shard.
|
|
1550
|
-
|
|
2163
|
+
// inline into every generated shard. With shapes, also pull the RLS
|
|
2164
|
+
// read-registry builder + `composeShapeReadWhere` so `resolveShape` can
|
|
2165
|
+
// AND-merge a shape's predicate with the table's read base-where.
|
|
2166
|
+
hasShapes ? `import { asBucketStorage, buildRlsReadRegistry, composeShapeReadWhere, createSecrets } from "${base.server}";` : `import { asBucketStorage, createSecrets } from "${base.server}";`
|
|
1551
2167
|
];
|
|
1552
2168
|
if (hasTables) {
|
|
1553
2169
|
importLines.push(`import { bindOrm, bindTableFacade } from "${base.server}";`);
|
|
1554
2170
|
}
|
|
1555
2171
|
if (hasVectors) {
|
|
1556
2172
|
importLines.push(
|
|
1557
|
-
`import type { SchemaLike as VectorSchemaLike, VectorizeIndexLike, VectorSearchLike } from "@lunora/vectors";`,
|
|
1558
|
-
`import { createContextVectors, createVectors, createVectorSyncHook } from "@lunora/vectors";`
|
|
2173
|
+
`import type { SchemaLike as VectorSchemaLike, VectorizeIndexLike, VectorSearchLike } from "@lunora/bindings/vectors";`,
|
|
2174
|
+
`import { createContextVectors, createVectors, createVectorSyncHook } from "@lunora/bindings/vectors";`
|
|
1559
2175
|
);
|
|
1560
2176
|
}
|
|
1561
2177
|
if (hasAi) {
|
|
1562
2178
|
importLines.push(`import type { AiBindingLike, LunoraAi } from "@lunora/ai";`, `import { createAi } from "@lunora/ai";`);
|
|
1563
2179
|
}
|
|
1564
2180
|
importLines.push(
|
|
2181
|
+
...accessFragments.importLines,
|
|
1565
2182
|
...kvFragments.importLines,
|
|
2183
|
+
...flagsFragments.importLines,
|
|
1566
2184
|
...analyticsFragments.importLines,
|
|
1567
2185
|
...imagesFragments.importLines,
|
|
1568
2186
|
...hyperdriveFragments.importLines,
|
|
1569
2187
|
...browserFragments.importLines,
|
|
1570
2188
|
...r2sqlFragments.importLines,
|
|
2189
|
+
...pipelinesFragments.importLines,
|
|
1571
2190
|
...containerImportLines,
|
|
1572
2191
|
...workflowImportLines,
|
|
2192
|
+
...queueImportLines,
|
|
1573
2193
|
...paymentsImports,
|
|
1574
2194
|
``,
|
|
1575
2195
|
`import schema from "../schema.js";`,
|
|
1576
|
-
|
|
2196
|
+
// Local-first sync registries are pulled in alongside the function table
|
|
2197
|
+
// only when the project declares them, so the import list stays minimal.
|
|
2198
|
+
`import { ${["LUNORA_FUNCTIONS", "LUNORA_LIFECYCLE_HOOKS", "LUNORA_MIGRATIONS", ...hasMutators ? ["LUNORA_MUTATOR_PATHS"] : [], ...hasShapes ? ["LUNORA_SHAPES"] : []].join(", ")} } from "./functions.js";`
|
|
1577
2199
|
);
|
|
1578
2200
|
const vectorsConfigField = hasVectors ? `
|
|
1579
2201
|
vectors?: (env: Record<string, unknown>) => Record<string, VectorizeIndexLike>;` : "";
|
|
@@ -1581,6 +2203,8 @@ const emitShard = ({
|
|
|
1581
2203
|
d1?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;` : "";
|
|
1582
2204
|
const hyperdriveGlobalConfigField = hasHyperdriveGlobal ? `
|
|
1583
2205
|
hyperdriveGlobal?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;` : "";
|
|
2206
|
+
const sourceClientConfigField = hasSourcedTables ? `
|
|
2207
|
+
sourceClient?: (env: Record<string, unknown>, binding: string) => { query: <Row = Record<string, unknown>>(text: string, params?: readonly unknown[]) => Promise<Row[]> } | undefined;` : "";
|
|
1584
2208
|
const globalDatabaseStub = hasGlobalTables ? `
|
|
1585
2209
|
const globalDbStub: DatabaseWriterLike = {
|
|
1586
2210
|
aggregate: async () => {
|
|
@@ -1698,6 +2322,144 @@ const vectorsStub: VectorSearchLike = {
|
|
|
1698
2322
|
const bindTableHelper = "";
|
|
1699
2323
|
const globalDatabaseThunk = hasHyperdriveGlobal ? "config.hyperdriveGlobal" : "config.d1";
|
|
1700
2324
|
const globalDatabaseLine = hasGlobalTables ? ` const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, { identity, userId }) ?? globalDbStub;
|
|
2325
|
+
` : "";
|
|
2326
|
+
const globalShapeReaderOverride = hasShapes && hasGlobalTables ? `
|
|
2327
|
+
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 }>> {
|
|
2328
|
+
const env = this.env as Record<string, unknown>;
|
|
2329
|
+
const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, identity) ?? globalDbStub;
|
|
2330
|
+
const rows: Array<{ doc: Record<string, unknown>; id: string }> = [];
|
|
2331
|
+
|
|
2332
|
+
let cursor: null | string = null;
|
|
2333
|
+
|
|
2334
|
+
// Drain every page of the global membership so the seed/diff sees the
|
|
2335
|
+
// full rowset (D1 \`findMany\` is paginated). Stop one row past the cap:
|
|
2336
|
+
// a broad \`.global()\` shape would otherwise materialize an unbounded
|
|
2337
|
+
// array before the caller's \`withinGlobalShapeBound\` check rejects it,
|
|
2338
|
+
// so bail early and let the caller fail it closed.
|
|
2339
|
+
do {
|
|
2340
|
+
// eslint-disable-next-line no-await-in-loop -- sequential page drain to assemble the full membership
|
|
2341
|
+
const page = await globalDb.findMany(resolved.table, { cursor, where: resolved.effectiveWhere });
|
|
2342
|
+
|
|
2343
|
+
for (const doc of page.page) {
|
|
2344
|
+
rows.push({ doc, id: String((doc as { _id?: unknown })._id) });
|
|
2345
|
+
|
|
2346
|
+
if (rows.length > ShardDOBase.GLOBAL_SHAPE_MAX_ROWS) {
|
|
2347
|
+
return rows;
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
cursor = page.isDone ? null : page.continueCursor;
|
|
2352
|
+
} while (cursor !== null);
|
|
2353
|
+
|
|
2354
|
+
return rows;
|
|
2355
|
+
}
|
|
2356
|
+
` : "";
|
|
2357
|
+
const sourceClientCacheConst = hasSourcedTables ? `
|
|
2358
|
+
const sourceClientCache = new WeakMap<object, Map<string, SourceClientLike>>();
|
|
2359
|
+
const sourcePollAtCache = new WeakMap<object, Map<string, number>>();
|
|
2360
|
+
` : "";
|
|
2361
|
+
const externalSourceOverride = hasSourcedTables ? `
|
|
2362
|
+
protected override async pollExternalSources(): Promise<number> {
|
|
2363
|
+
const env = (this.env ?? {}) as Record<string, unknown>;
|
|
2364
|
+
const sourced = Object.entries((schema as unknown as SchemaLike).tables)
|
|
2365
|
+
.map(([table, definition]) => [table, (definition as { externalSource?: ExternalSourceLike }).externalSource] as const)
|
|
2366
|
+
.filter((entry): entry is [string, ExternalSourceLike] => entry[1] !== undefined);
|
|
2367
|
+
|
|
2368
|
+
if (sourced.length === 0) {
|
|
2369
|
+
return 0;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
const shardKey = this.currentShardKey();
|
|
2373
|
+
const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
|
|
2374
|
+
const writer = createShardCtxDb({
|
|
2375
|
+
broadcast: (delta) => {
|
|
2376
|
+
this.recordChangedTable(delta.table);
|
|
2377
|
+
},
|
|
2378
|
+
cdc: config.cdc ?? false,
|
|
2379
|
+
scheduler,
|
|
2380
|
+
schema: schema as unknown as SchemaLike,
|
|
2381
|
+
sql: this.sql as SqlExec,
|
|
2382
|
+
});
|
|
2383
|
+
|
|
2384
|
+
let clients = sourceClientCache.get(this);
|
|
2385
|
+
|
|
2386
|
+
if (clients === undefined) {
|
|
2387
|
+
clients = new Map();
|
|
2388
|
+
sourceClientCache.set(this, clients);
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
let polledAt = sourcePollAtCache.get(this);
|
|
2392
|
+
|
|
2393
|
+
if (polledAt === undefined) {
|
|
2394
|
+
polledAt = new Map();
|
|
2395
|
+
sourcePollAtCache.set(this, polledAt);
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
const now = Date.now();
|
|
2399
|
+
// \`active\` counts non-manual sources: while > 0 the alarm re-arms; a
|
|
2400
|
+
// manual-only schema returns 0 so the shared alarm goes idle.
|
|
2401
|
+
let active = 0;
|
|
2402
|
+
|
|
2403
|
+
for (const [table, source] of sourced) {
|
|
2404
|
+
if (source.refresh === "manual") {
|
|
2405
|
+
continue;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
active += 1;
|
|
2409
|
+
|
|
2410
|
+
if (!isSourceDue(source.refresh, polledAt.get(table), now)) {
|
|
2411
|
+
continue;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
try {
|
|
2415
|
+
let client = clients.get(source.binding);
|
|
2416
|
+
|
|
2417
|
+
if (client === undefined) {
|
|
2418
|
+
client = config.sourceClient?.(env, source.binding);
|
|
2419
|
+
|
|
2420
|
+
if (client !== undefined) {
|
|
2421
|
+
clients.set(source.binding, client);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
if (client === undefined) {
|
|
2426
|
+
// No SqlClient resolved for this binding (host never wired
|
|
2427
|
+
// \`config.sourceClient\`, or wired it wrong). Surface it in the
|
|
2428
|
+
// Logs panel and stamp \`polledAt\` so a persistent misconfig backs
|
|
2429
|
+
// off to \`refresh.everyMs\` instead of retrying every alarm tick.
|
|
2430
|
+
this.recordExternalSourceError(table, new Error(\`external-source: no sourceClient resolved for binding "\${source.binding}"\`));
|
|
2431
|
+
polledAt.set(table, now);
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// 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
|
|
2436
|
+
await pullExternalSourceTick(this.sql as SqlExec, writer, client, table, source, shardKey);
|
|
2437
|
+
polledAt.set(table, now);
|
|
2438
|
+
} catch (error) {
|
|
2439
|
+
this.recordExternalSourceError(table, error);
|
|
2440
|
+
// Stamp on failure too, so a persistently failing source throttles
|
|
2441
|
+
// to \`refresh.everyMs\` rather than being hammered every tick.
|
|
2442
|
+
polledAt.set(table, now);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
return active;
|
|
2447
|
+
}
|
|
2448
|
+
` : "";
|
|
2449
|
+
const sourceConstructorOverride = hasSourcedTables ? `
|
|
2450
|
+
public constructor(state: ShardDOState, env: unknown) {
|
|
2451
|
+
super(state, env);
|
|
2452
|
+
|
|
2453
|
+
const autoSourced = Object.values((schema as unknown as SchemaLike).tables).some((definition) => {
|
|
2454
|
+
const source = (definition as { externalSource?: ExternalSourceLike }).externalSource;
|
|
2455
|
+
|
|
2456
|
+
return source !== undefined && source.refresh !== "manual";
|
|
2457
|
+
});
|
|
2458
|
+
|
|
2459
|
+
if (autoSourced) {
|
|
2460
|
+
void this.scheduleSourcePoll();
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
1701
2463
|
` : "";
|
|
1702
2464
|
const facadeBlock = hasTables ? `
|
|
1703
2465
|
const facade = db as unknown as Record<string, ReturnType<typeof bindTableFacade>>;
|
|
@@ -1711,18 +2473,23 @@ ${schema.tables.map(
|
|
|
1711
2473
|
(table) => ` facade[${JSON.stringify(table.name)}] = bindTableFacade(db, ${JSON.stringify(table.name)});`
|
|
1712
2474
|
).join("\n")}
|
|
1713
2475
|
` : "";
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1716
|
-
|
|
2476
|
+
const secretsBuild = `
|
|
2477
|
+
const secrets = createSecrets(env);
|
|
2478
|
+
`;
|
|
2479
|
+
const everyContextBuild = `${accessFragments.build}${kvFragments.build}${flagsFragments.build}${analyticsFragments.build}${secretsBuild}`;
|
|
2480
|
+
const everyContextField = `${accessFragments.contextField}${kvFragments.contextField}${flagsFragments.contextField}${analyticsFragments.contextField}
|
|
2481
|
+
secrets,`;
|
|
2482
|
+
const actionOnlyHasAny = hasImages || hasHyperdrive || hasBrowser || hasR2sql || hasPipelines;
|
|
1717
2483
|
const actionOnlyBlock = actionOnlyHasAny ? `
|
|
1718
2484
|
// ActionCtx-only helpers (external, non-deterministic I/O): constructed
|
|
1719
2485
|
// and attached only for an \`action\` so query/mutation ctx never carry them.
|
|
1720
2486
|
if (isAction) {
|
|
1721
|
-
${imagesFragments.build}${hyperdriveFragments.build}${browserFragments.build}${r2sqlFragments.build}${[
|
|
2487
|
+
${imagesFragments.build}${hyperdriveFragments.build}${browserFragments.build}${r2sqlFragments.build}${pipelinesFragments.build}${[
|
|
1722
2488
|
...hasImages ? [" ctx.images = images;"] : [],
|
|
1723
2489
|
...hasHyperdrive ? [" ctx.sql = sql;"] : [],
|
|
1724
2490
|
...hasBrowser ? [" ctx.browser = browser;"] : [],
|
|
1725
|
-
...hasR2sql ? [" ctx.r2sql = r2sql;"] : []
|
|
2491
|
+
...hasR2sql ? [" ctx.r2sql = r2sql;"] : [],
|
|
2492
|
+
...hasPipelines ? [" ctx.pipelines = pipelines;"] : []
|
|
1726
2493
|
].join("\n")}
|
|
1727
2494
|
}
|
|
1728
2495
|
` : "";
|
|
@@ -1762,14 +2529,14 @@ const LUNORA_STORAGE_RULES: StorageRulesResult = ${JSON.stringify(storageRulesDa
|
|
|
1762
2529
|
|
|
1763
2530
|
/** Which optional package-backed features this app wires up (discovered from imports / \`ctx.*\` reads / schema signals) served via \`__lunora_admin__:studioFeatures\` so the studio hides nav pages whose package isn't enabled. */
|
|
1764
2531
|
const LUNORA_STUDIO_FEATURES: StudioFeaturesResult = ${JSON.stringify(studioFeaturesData, void 0, 4)};
|
|
1765
|
-
${workflowsMetadataConst}${containerSpecs}${workflowSpecs}
|
|
2532
|
+
${flagsOverrides.constant}${shapeReadRegistryConst}${workflowsMetadataConst}${queuesMetadataConst}${containerSpecs}${workflowSpecs}${queueSpecs}
|
|
1766
2533
|
export interface ShardDOConfig {
|
|
1767
2534
|
/** Opt into change-data-capture: records a post-image to \`__cdc_log\` on every write (backs streaming export + replay-PITR). */
|
|
1768
2535
|
cdc?: boolean;
|
|
1769
2536
|
/** Optional telemetry sink. When supplied, each \`ctx.log.*\` call is forwarded to \`sink.onLog\`. Pass the SAME sink you give \`createWorker({ observability })\` (which drives \`onRpc\`) to route both RPC and log events. */
|
|
1770
2537
|
observability?: (env: Record<string, unknown>) => LogSink | undefined;
|
|
1771
2538
|
scheduler?: (env: Record<string, unknown>) => unknown;
|
|
1772
|
-
storage?: (env: Record<string, unknown>) => unknown;${vectorsConfigField}${aiConfigField}${kvFragments.configField}${analyticsFragments.configField}${imagesFragments.configField}${hyperdriveFragments.configField}${browserFragments.configField}${r2sqlFragments.configField}${paymentsConfigField}${d1ConfigField}${hyperdriveGlobalConfigField}
|
|
2539
|
+
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}
|
|
1773
2540
|
}
|
|
1774
2541
|
|
|
1775
2542
|
const schedulerStub = {
|
|
@@ -1807,7 +2574,7 @@ const storageStub = {
|
|
|
1807
2574
|
throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
|
|
1808
2575
|
},
|
|
1809
2576
|
};
|
|
1810
|
-
${globalDatabaseStub}${vectorsStub}${aiStub}${kvFragments.stub}${analyticsFragments.stub}${imagesFragments.stub}${hyperdriveFragments.stub}${browserFragments.stub}${r2sqlFragments.stub}${paymentStub}${bindTableHelper}
|
|
2577
|
+
${globalDatabaseStub}${sourceClientCacheConst}${vectorsStub}${aiStub}${kvFragments.stub}${flagsFragments.stub}${analyticsFragments.stub}${imagesFragments.stub}${hyperdriveFragments.stub}${browserFragments.stub}${r2sqlFragments.stub}${pipelinesFragments.stub}${paymentStub}${bindTableHelper}
|
|
1811
2578
|
// Bound in-process \`ctx.run*\` composition depth so a self- or cyclically-
|
|
1812
2579
|
// referencing call fails loudly with a clear error instead of overflowing the
|
|
1813
2580
|
// stack. Tracked across the awaited handler chain (one DO invocation is
|
|
@@ -1848,7 +2615,7 @@ const dispatchRun = async (expected: FunctionKind, functionPath: string, args: R
|
|
|
1848
2615
|
* from the worker entry so wrangler binds it by name.
|
|
1849
2616
|
*/
|
|
1850
2617
|
export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOState, env: unknown) => ShardDOBase =>
|
|
1851
|
-
class extends ShardDOBase {
|
|
2618
|
+
class extends ShardDOBase {${sourceConstructorOverride}
|
|
1852
2619
|
private migrated = false;
|
|
1853
2620
|
|
|
1854
2621
|
public override async handleRpc(functionPath: string, args: Record<string, unknown>): Promise<unknown> {
|
|
@@ -1877,8 +2644,20 @@ export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOSt
|
|
|
1877
2644
|
// do external I/O that can't be rolled back, so both dispatch directly.
|
|
1878
2645
|
// \`ctx.run*\` composition runs inside this span (it never re-enters
|
|
1879
2646
|
// handleRpc); runInTransaction's own guard rejects accidental nesting.
|
|
2647
|
+
//
|
|
2648
|
+
// The replay bookkeeping (idempotency dedup row + custom-mutator
|
|
2649
|
+
// watermark advance) commits INSIDE this span via
|
|
2650
|
+
// \`commitMutationBookkeeping\`, so the writes, the dedup row, and the
|
|
2651
|
+
// watermark are atomic — a crash can't leave the writes durable without
|
|
2652
|
+
// the replay guard.
|
|
1880
2653
|
if (registered.kind === "mutation") {
|
|
1881
|
-
return this.runInTransaction(() =>
|
|
2654
|
+
return this.runInTransaction(async () => {
|
|
2655
|
+
const result = await registered.handler(ctx, args);
|
|
2656
|
+
|
|
2657
|
+
this.commitMutationBookkeeping(result);
|
|
2658
|
+
|
|
2659
|
+
return result;
|
|
2660
|
+
});
|
|
1882
2661
|
}
|
|
1883
2662
|
|
|
1884
2663
|
return registered.handler(ctx, args);
|
|
@@ -1916,7 +2695,7 @@ ${relationFanout.override}
|
|
|
1916
2695
|
iterator: (signal) => (registered.handler as (context: unknown, args: Record<string, unknown>, signal: AbortSignal) => AsyncIterable<unknown>)(this.buildCtx({ functionPath }), args, signal),
|
|
1917
2696
|
};
|
|
1918
2697
|
}
|
|
1919
|
-
|
|
2698
|
+
${customMutatorOverride}${shapeResolveOverride}${globalShapeReaderOverride}${externalSourceOverride}
|
|
1920
2699
|
protected override lifecycleHookPaths(event: "connect" | "disconnect"): readonly string[] {
|
|
1921
2700
|
return LUNORA_LIFECYCLE_HOOKS[event];
|
|
1922
2701
|
}
|
|
@@ -1952,7 +2731,7 @@ ${relationFanout.override}
|
|
|
1952
2731
|
protected override studioFeatures(): StudioFeaturesResult {
|
|
1953
2732
|
return LUNORA_STUDIO_FEATURES;
|
|
1954
2733
|
}
|
|
1955
|
-
${workflowsMetadataOverride}
|
|
2734
|
+
${flagsOverrides.evaluateOverride}${flagsOverrides.subscriptionOverride}${workflowsMetadataOverride}${queuesMetadataOverride}
|
|
1956
2735
|
protected override advisories(): AdvisoryFinding[] {
|
|
1957
2736
|
return LUNORA_ADVISORIES;
|
|
1958
2737
|
}
|
|
@@ -2186,7 +2965,7 @@ ${workflowsMetadataOverride}
|
|
|
2186
2965
|
// dispatch path) fall back to the per-request fields as before.
|
|
2187
2966
|
const userId = options.identity ? options.identity.userId : this.getCurrentUserId();
|
|
2188
2967
|
const identity = options.identity ? options.identity.identity : this.getCurrentIdentity();
|
|
2189
|
-
${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}
|
|
2968
|
+
${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}${queuesBuild}
|
|
2190
2969
|
const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
|
|
2191
2970
|
// Build the storage adapter once and share it between \`ctx.storage\`
|
|
2192
2971
|
// and \`ctx.db.system._storage\` so both read the same R2 binding. The
|
|
@@ -2209,6 +2988,14 @@ ${facadeBlock}${paymentsBuild}
|
|
|
2209
2988
|
warn: (...args: unknown[]) => { this.recordUserLog(logFunctionPath, "warn", args, observability); },
|
|
2210
2989
|
};
|
|
2211
2990
|
|
|
2991
|
+
// \`ctx.now\`: the wall-clock instant (epoch ms) this function began,
|
|
2992
|
+
// captured ONCE so the whole handler body sees a single stable value.
|
|
2993
|
+
// Query/mutation handlers must be deterministic (they may be re-run on
|
|
2994
|
+
// OCC retry / subscription re-eval), so they must read time through
|
|
2995
|
+
// \`ctx.now\` instead of \`Date.now()\` — the \`nondeterministic_query_mutation\`
|
|
2996
|
+
// advisor flags the latter. Actions may still use ambient \`Date.now()\`.
|
|
2997
|
+
const now = Date.now();
|
|
2998
|
+
|
|
2212
2999
|
const ctx: Record<string, unknown> = {
|
|
2213
3000
|
auth: {
|
|
2214
3001
|
getIdentity: async () => identity ?? null,
|
|
@@ -2217,9 +3004,10 @@ ${facadeBlock}${paymentsBuild}
|
|
|
2217
3004
|
db,
|
|
2218
3005
|
fetch: globalThis.fetch.bind(globalThis),
|
|
2219
3006
|
ip: this.getCurrentIp(),
|
|
2220
|
-
log
|
|
3007
|
+
log,
|
|
3008
|
+
now,${ormContextField}
|
|
2221
3009
|
scheduler,
|
|
2222
|
-
storage,${vectorsContextField}${aiContextField}${everyContextField}${paymentsContextField}${containersContextField}${workflowsContextField}
|
|
3010
|
+
storage,${vectorsContextField}${aiContextField}${everyContextField}${paymentsContextField}${containersContextField}${workflowsContextField}${queuesContextField}
|
|
2223
3011
|
};
|
|
2224
3012
|
${isActionLine}${actionOnlyBlock}
|
|
2225
3013
|
ctx.runAction = (reference: FunctionReference, fnArgs: Record<string, unknown>) => dispatchRun("action", reference.__lunoraRef, fnArgs, ctx);
|
|
@@ -2483,4 +3271,4 @@ const emitWranglerCronTriggers = (crons) => {
|
|
|
2483
3271
|
return triggers;
|
|
2484
3272
|
};
|
|
2485
3273
|
|
|
2486
|
-
export { GENERATED_HEADER, buildStorageColumns, emitApi, emitContainers, emitCrons, emitDataModel, emitDrizzleSchema, emitFunctions, emitSeed, emitServer, emitShard, emitVectors, emitWorkflows, emitWranglerCronTriggers };
|
|
3274
|
+
export { GENERATED_HEADER, buildStorageColumns, emitApi, emitCollections, emitContainers, emitCrons, emitDataModel, emitDrizzleSchema, emitFunctions, emitQueues, emitSeed, emitServer, emitShard, emitVectors, emitWorkflows, emitWranglerCronTriggers };
|