@lunora/codegen 1.0.0-alpha.16 → 1.0.0-alpha.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +120 -3
- package/dist/index.d.ts +120 -3
- package/dist/index.mjs +8 -6
- package/dist/packem_shared/{GENERATED_HEADER-CkZUgM4_.mjs → GENERATED_HEADER-D-AbhmRh.mjs} +210 -20
- package/dist/packem_shared/MUTATORS_FILENAME-BhqdPtKp.mjs +81 -0
- package/dist/packem_shared/{OPENRPC_VERSION-VINwUAxf.mjs → OPENRPC_VERSION-DDT4mIzD.mjs} +1 -1
- package/dist/packem_shared/{SCHEMA_SNAPSHOT_FILENAME-DsBxQ5C_.mjs → SCHEMA_SNAPSHOT_FILENAME-ExbveiC6.mjs} +83 -7
- package/dist/packem_shared/SHAPES_FILENAME-ChV7MqgE.mjs +94 -0
- package/dist/packem_shared/{buildOpenApiDocument-BSoDzIn8.mjs → buildOpenApiDocument-DoK7_x1g.mjs} +1 -1
- package/dist/packem_shared/{emitApp-DGmlt5fq.mjs → emitApp-CV81L74c.mjs} +48 -6
- package/dist/packem_shared/{formatAdvisories-8NIv1k0I.mjs → formatAdvisories-LM8-Ixjw.mjs} +8 -2
- package/package.json +2 -2
|
@@ -600,6 +600,26 @@ import type { InsertModel } from "./dataModel.js";
|
|
|
600
600
|
export const createSeedClient = (options?: SeedClientOptions): SeedClient<InsertModel> => createSeedClientBase<InsertModel>(schema, options);
|
|
601
601
|
`;
|
|
602
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
|
+
};
|
|
603
623
|
const moduleAlias = (filePath, index) => `lunora_${sanitizeNamespace(filePath)}_${String(index)}`;
|
|
604
624
|
const CALL_REGISTERED_HELPER = `const callRegistered = async <R>(context: CallerCtx, functionPath: string, args: Record<string, unknown> | undefined): Promise<R> => {
|
|
605
625
|
const registered = LUNORA_FUNCTIONS[functionPath];
|
|
@@ -614,7 +634,7 @@ const CALL_REGISTERED_HELPER = `const callRegistered = async <R>(context: Caller
|
|
|
614
634
|
|
|
615
635
|
return (await registered.handler(context, args ?? {})) as R;
|
|
616
636
|
};`;
|
|
617
|
-
const renderFunctionRegistry = (functions, migrations) => {
|
|
637
|
+
const renderFunctionRegistry = (functions, migrations, mutators = [], shapes = []) => {
|
|
618
638
|
const aliasByPath = /* @__PURE__ */ new Map();
|
|
619
639
|
const registerPath = (filePath) => {
|
|
620
640
|
if (!aliasByPath.has(filePath)) {
|
|
@@ -627,6 +647,12 @@ const renderFunctionRegistry = (functions, migrations) => {
|
|
|
627
647
|
for (const migration of migrations) {
|
|
628
648
|
registerPath(migration.filePath);
|
|
629
649
|
}
|
|
650
|
+
for (const mutator of mutators) {
|
|
651
|
+
registerPath(mutator.filePath);
|
|
652
|
+
}
|
|
653
|
+
for (const shape of shapes) {
|
|
654
|
+
registerPath(shape.filePath);
|
|
655
|
+
}
|
|
630
656
|
const importLines = [...aliasByPath.entries()].map(([filePath, alias]) => {
|
|
631
657
|
if (!IMPORT_PATH_RE.test(filePath)) {
|
|
632
658
|
throw new Error(`@lunora/codegen: refusing to emit import for unsafe file path: ${JSON.stringify(filePath)}`);
|
|
@@ -639,6 +665,12 @@ const renderFunctionRegistry = (functions, migrations) => {
|
|
|
639
665
|
const migrationEntries = migrations.map(
|
|
640
666
|
(migration) => ` ${JSON.stringify(migration.id)}: ${aliasByPath.get(migration.filePath) ?? ""}.${migration.exportName} as unknown as RegisteredDataMigration,`
|
|
641
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");
|
|
642
674
|
const installLines = functions.map((definition) => {
|
|
643
675
|
if (definition.lifecycle || Object.keys(definition.args).length === 0) {
|
|
644
676
|
return void 0;
|
|
@@ -651,8 +683,8 @@ const renderFunctionRegistry = (functions, migrations) => {
|
|
|
651
683
|
return `installCompiledValidatorMap(${alias}.${definition.exportName}.args, ${compiled});`;
|
|
652
684
|
}).filter((line) => line !== void 0).join("\n");
|
|
653
685
|
return {
|
|
654
|
-
dispatchBody:
|
|
655
|
-
${
|
|
686
|
+
dispatchBody: combinedDispatch.length > 0 ? `
|
|
687
|
+
${combinedDispatch}
|
|
656
688
|
` : "",
|
|
657
689
|
importBlock: importLines.length > 0 ? `${importLines}
|
|
658
690
|
|
|
@@ -660,6 +692,10 @@ ${dispatchEntries}
|
|
|
660
692
|
installBlock: installLines,
|
|
661
693
|
migrationBody: migrationEntries.length > 0 ? `
|
|
662
694
|
${migrationEntries}
|
|
695
|
+
` : "",
|
|
696
|
+
mutatorPaths,
|
|
697
|
+
shapeBody: shapeEntries.length > 0 ? `
|
|
698
|
+
${shapeEntries}
|
|
663
699
|
` : ""
|
|
664
700
|
};
|
|
665
701
|
};
|
|
@@ -713,6 +749,7 @@ const buildStorageBucketNames = (schema, ruleBuckets = []) => {
|
|
|
713
749
|
};
|
|
714
750
|
const emitServer = ({
|
|
715
751
|
containers = [],
|
|
752
|
+
hasAccessFacade = false,
|
|
716
753
|
hasAi = false,
|
|
717
754
|
hasAnalytics = false,
|
|
718
755
|
hasBrowser = false,
|
|
@@ -785,6 +822,9 @@ ${envBindingFields}` : ""}
|
|
|
785
822
|
export type Env = CloudflareBindings;`;
|
|
786
823
|
const kvContextField = hasKv ? `
|
|
787
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;` : "";
|
|
788
828
|
const flagsContextField = hasFlags ? `
|
|
789
829
|
/** Feature-flag evaluation (OpenFeature). Reads are memoized per request; evaluations never throw — a provider error resolves to the supplied default. */
|
|
790
830
|
readonly flags: import("@lunora/flags").LunoraFlags;` : "";
|
|
@@ -902,19 +942,19 @@ type TypedTableGet = <T extends TableName>(id: IdOfTable<T>) => Promise<Doc<T> |
|
|
|
902
942
|
export interface QueryCtx extends Omit<QueryCtxBase, "db" | "storage"> {
|
|
903
943
|
readonly db: Omit<DatabaseReader, "query" | "get"> & DatabaseReaderFacade & { query: TypedTableQuery; get: TypedTableGet };
|
|
904
944
|
readonly orm: OrmReader;
|
|
905
|
-
readonly storage: ReadOnlyStorage<StorageBucketName>;${kvContextField}${flagsContextField}${analyticsContextField}
|
|
945
|
+
readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}
|
|
906
946
|
}
|
|
907
947
|
|
|
908
948
|
export interface MutationCtx extends Omit<MutationCtxBase, "db" | "storage"${workflowsOmit}> {
|
|
909
949
|
readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
|
|
910
950
|
readonly orm: OrmWriter;
|
|
911
|
-
readonly storage: ReadOnlyStorage<StorageBucketName>;${kvContextField}${flagsContextField}${analyticsContextField}${workflowsContextField}${queuesContextField}
|
|
951
|
+
readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}${workflowsContextField}${queuesContextField}
|
|
912
952
|
}
|
|
913
953
|
|
|
914
954
|
export interface ActionCtx extends Omit<ActionCtxBase, "db" | "storage"${workflowsOmit}> {
|
|
915
955
|
readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
|
|
916
956
|
readonly orm: OrmWriter;
|
|
917
|
-
readonly storage: StorageBase<StorageBucketName>;${aiActionField}${paymentsActionField}${containersActionField}${kvContextField}${flagsContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${r2sqlActionField}${workflowsContextField}${queuesContextField}
|
|
957
|
+
readonly storage: StorageBase<StorageBucketName>;${accessContextField}${aiActionField}${paymentsActionField}${containersActionField}${kvContextField}${flagsContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${r2sqlActionField}${workflowsContextField}${queuesContextField}
|
|
918
958
|
}
|
|
919
959
|
|
|
920
960
|
/**
|
|
@@ -984,11 +1024,30 @@ const renderLifecycleManifest = (functions) => {
|
|
|
984
1024
|
}
|
|
985
1025
|
return manifest;
|
|
986
1026
|
};
|
|
987
|
-
const emitFunctions = (functions, migrations = [], useUmbrella = false) => {
|
|
1027
|
+
const emitFunctions = (functions, migrations = [], useUmbrella = false, mutators = [], shapes = []) => {
|
|
988
1028
|
const hasFunctions = functions.length > 0;
|
|
989
1029
|
const base = baseSpecifiers(useUmbrella);
|
|
990
|
-
const { dispatchBody, importBlock, installBlock, migrationBody } = renderFunctionRegistry(functions, migrations);
|
|
1030
|
+
const { dispatchBody, importBlock, installBlock, migrationBody, mutatorPaths, shapeBody } = renderFunctionRegistry(functions, migrations, mutators, shapes);
|
|
991
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
|
+
` : "";
|
|
992
1051
|
const compiledArgsImport = installBlock.length > 0 ? `import { DEFER_VALIDATION as DEFER, installCompiledValidatorMap } from "${base.values}";
|
|
993
1052
|
` : "";
|
|
994
1053
|
const compiledArgsInstall = installBlock.length > 0 ? `
|
|
@@ -1013,7 +1072,7 @@ ${caller.implementation}
|
|
|
1013
1072
|
const callRegisteredHelper = hasFunctions ? `${CALL_REGISTERED_HELPER}
|
|
1014
1073
|
|
|
1015
1074
|
` : "";
|
|
1016
|
-
return `${GENERATED_HEADER}${importBlock}${compiledArgsImport}import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
|
|
1075
|
+
return `${GENERATED_HEADER}${importBlock}${compiledArgsImport}${shapeTypeImport}import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
|
|
1017
1076
|
${dataModelImport}
|
|
1018
1077
|
/**
|
|
1019
1078
|
* Single registered function, narrowed to the shape \`handleRpc\` needs.
|
|
@@ -1039,7 +1098,7 @@ export interface RegisteredLunoraFunction {
|
|
|
1039
1098
|
* emits (\`api[namespace][fn].__lunoraRef === "namespace:fn"\`).
|
|
1040
1099
|
*/
|
|
1041
1100
|
export const LUNORA_FUNCTIONS: Record<string, RegisteredLunoraFunction> = {${dispatchBody}};
|
|
1042
|
-
${compiledArgsInstall}
|
|
1101
|
+
${compiledArgsInstall}${shapeRegistry}${mutatorPathsRegistry}
|
|
1043
1102
|
/**
|
|
1044
1103
|
* Connection-lifecycle manifest: the function paths the generated ShardDO
|
|
1045
1104
|
* dispatches when a client's WebSocket connects (\`connect\`) or disconnects
|
|
@@ -1306,6 +1365,21 @@ const emitFlagsFragments = (hasFlags) => {
|
|
|
1306
1365
|
stub: ""
|
|
1307
1366
|
};
|
|
1308
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
|
+
};
|
|
1309
1383
|
const emitFlagsOverrides = (flagKeys, hasFlags) => {
|
|
1310
1384
|
if (!hasFlags) {
|
|
1311
1385
|
return { constant: "", evaluateOverride: "", subscriptionOverride: "" };
|
|
@@ -1899,6 +1973,7 @@ const emitShard = ({
|
|
|
1899
1973
|
advisories = [],
|
|
1900
1974
|
containers = [],
|
|
1901
1975
|
flagKeys = [],
|
|
1976
|
+
hasAccessFacade = false,
|
|
1902
1977
|
hasAi = false,
|
|
1903
1978
|
hasAnalytics = false,
|
|
1904
1979
|
hasBrowser = false,
|
|
@@ -1910,16 +1985,21 @@ const emitShard = ({
|
|
|
1910
1985
|
hasPipelines = false,
|
|
1911
1986
|
hasR2sql = false,
|
|
1912
1987
|
maskMetadata,
|
|
1988
|
+
mutators = [],
|
|
1913
1989
|
queues = [],
|
|
1914
1990
|
rlsMetadata,
|
|
1915
1991
|
schema,
|
|
1992
|
+
shapes = [],
|
|
1916
1993
|
storageRules,
|
|
1917
1994
|
studioFeatures,
|
|
1918
1995
|
useUmbrella = false,
|
|
1919
1996
|
workflows = []
|
|
1920
1997
|
}) => {
|
|
1921
1998
|
const base = baseSpecifiers(useUmbrella);
|
|
1999
|
+
const hasMutators = mutators.length > 0;
|
|
2000
|
+
const hasShapes = shapes.length > 0;
|
|
1922
2001
|
const { build: aiBuild, configField: aiConfigField, contextField: aiContextField, stub: aiStub } = emitAiFragments(hasAi);
|
|
2002
|
+
const accessFragments = emitAccessFragments(hasAccessFacade);
|
|
1923
2003
|
const kvFragments = emitKvFragments(hasKv);
|
|
1924
2004
|
const flagsFragments = emitFlagsFragments(hasFlags);
|
|
1925
2005
|
const flagsOverrides = emitFlagsOverrides(flagKeys, hasFlags);
|
|
@@ -1980,15 +2060,79 @@ const emitShard = ({
|
|
|
1980
2060
|
const tableColumns = buildTableColumns(schema);
|
|
1981
2061
|
const storageColumns = buildStorageColumns(schema);
|
|
1982
2062
|
const doTypeImports = buildDoTypeImports(hasVectors, workflows.length > 0, queues.length > 0, hasFlags);
|
|
2063
|
+
if (hasShapes) {
|
|
2064
|
+
doTypeImports.push("WhereInput");
|
|
2065
|
+
}
|
|
1983
2066
|
const relationFanout = emitRelationFanout(hasGlobalTables);
|
|
2067
|
+
const shapeResolveOverride = hasShapes ? `
|
|
2068
|
+
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 {
|
|
2069
|
+
const shape = LUNORA_SHAPES[name];
|
|
2070
|
+
|
|
2071
|
+
if (!shape) {
|
|
2072
|
+
return undefined;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
this.ensureMigrated();
|
|
2076
|
+
|
|
2077
|
+
// Trusted ctx from the socket's OWN verified identity — the client
|
|
2078
|
+
// supplies only the shape name + args; \`compileWhere\` validates the
|
|
2079
|
+
// args then runs the shape's \`where(ctx, args)\` under that identity,
|
|
2080
|
+
// so which rows replicate is a server decision (reads-as-permissions).
|
|
2081
|
+
const ctx = this.buildCtx({ functionPath: \`__shape__:\${name}\`, identity });
|
|
2082
|
+
const shapeWhere = shape.compileWhere(ctx, args) as unknown as WhereInput;
|
|
2083
|
+
|
|
2084
|
+
// AND-compose with the table's RLS read base-where. A shape runs no
|
|
2085
|
+
// procedure, so the \`.use(rls(...))\` middleware never fires; without
|
|
2086
|
+
// this merge its reads would bypass every read policy on the table
|
|
2087
|
+
// (rows the caller can't see would replicate). \`composeShapeReadWhere\`
|
|
2088
|
+
// evaluates the table's read policies under this same trusted ctx —
|
|
2089
|
+
// exactly the request-time path — and fails closed under a
|
|
2090
|
+
// \`.rls("required")\` schema for a non-\`.public()\`, policy-less table.
|
|
2091
|
+
const effectiveWhere = composeShapeReadWhere(LUNORA_RLS_READ_REGISTRY, {
|
|
2092
|
+
ctx,
|
|
2093
|
+
identity: identity?.identity ?? null,
|
|
2094
|
+
rlsRequired: (schema as unknown as { rlsMode?: string }).rlsMode === "required",
|
|
2095
|
+
roles: (ctx as { auth?: { roles?: readonly string[] } }).auth?.roles ?? [],
|
|
2096
|
+
shapeWhere,
|
|
2097
|
+
table: shape.table,
|
|
2098
|
+
tablePublic: (schema as unknown as { tables: Record<string, { isPublic?: boolean }> }).tables[shape.table]?.isPublic === true,
|
|
2099
|
+
userId: identity?.userId ?? null,
|
|
2100
|
+
});
|
|
2101
|
+
|
|
2102
|
+
// A live shape pokes only from its OWN shard's op-log, so a \`where()\`
|
|
2103
|
+
// that joins to a \`.shardBy()\` table (rows in another DO) is rejected
|
|
2104
|
+
// here — the first point the compiled predicate + shard modes are both
|
|
2105
|
+
// known. Remedy: denormalize, or move the joined table to \`.global()\`.
|
|
2106
|
+
assertShapeShardable(effectiveWhere, schema as unknown as SchemaLike, shape.table);
|
|
2107
|
+
|
|
2108
|
+
// A \`.global()\` table lives in D1 (no per-DO op-log): flag it so the
|
|
2109
|
+
// base serves it through the latency-tiered poll path (\`readGlobalShapeRows\`)
|
|
2110
|
+
// instead of the CDC poke path.
|
|
2111
|
+
const isGlobal = (schema as unknown as SchemaLike).tables[shape.table]?.shardMode?.kind === "global";
|
|
2112
|
+
|
|
2113
|
+
return { columns: shape.columns, effectiveWhere, global: isGlobal, table: shape.table };
|
|
2114
|
+
}
|
|
2115
|
+
` : "";
|
|
2116
|
+
const customMutatorOverride = hasMutators ? `
|
|
2117
|
+
protected override isCustomMutator(functionPath: string): boolean {
|
|
2118
|
+
return LUNORA_MUTATOR_PATHS.has(functionPath);
|
|
2119
|
+
}
|
|
2120
|
+
` : "";
|
|
2121
|
+
const shapeReadRegistryConst = hasShapes ? `
|
|
2122
|
+
/** 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. */
|
|
2123
|
+
const LUNORA_RLS_READ_REGISTRY = buildRlsReadRegistry(Object.values(LUNORA_FUNCTIONS));
|
|
2124
|
+
` : "";
|
|
2125
|
+
const shapeGuardImport = hasShapes ? "assertShapeShardable, " : "";
|
|
1984
2126
|
const importLines = [
|
|
1985
2127
|
`import type { ${doTypeImports.join(", ")} } from "${base.do}";`,
|
|
1986
|
-
`import { applyCdcChanges, createShardCtxDb, runDataMigration, runShardMigrations, ${relationFanout.importFragment}ShardDO as ShardDOBase } from "${base.do}";`,
|
|
2128
|
+
`import { applyCdcChanges, ${shapeGuardImport}createShardCtxDb, runDataMigration, runShardMigrations, ${relationFanout.importFragment}ShardDO as ShardDOBase } from "${base.do}";`,
|
|
1987
2129
|
// `asBucketStorage` (the bucket-aware `ctx.storage` wrapper) and
|
|
1988
2130
|
// `createSecrets` (the `ctx.secrets` core built-in) live in
|
|
1989
2131
|
// `@lunora/server`, the single source — imported here rather than stamped
|
|
1990
|
-
// inline into every generated shard.
|
|
1991
|
-
|
|
2132
|
+
// inline into every generated shard. With shapes, also pull the RLS
|
|
2133
|
+
// read-registry builder + `composeShapeReadWhere` so `resolveShape` can
|
|
2134
|
+
// AND-merge a shape's predicate with the table's read base-where.
|
|
2135
|
+
hasShapes ? `import { asBucketStorage, buildRlsReadRegistry, composeShapeReadWhere, createSecrets } from "${base.server}";` : `import { asBucketStorage, createSecrets } from "${base.server}";`
|
|
1992
2136
|
];
|
|
1993
2137
|
if (hasTables) {
|
|
1994
2138
|
importLines.push(`import { bindOrm, bindTableFacade } from "${base.server}";`);
|
|
@@ -2003,6 +2147,7 @@ const emitShard = ({
|
|
|
2003
2147
|
importLines.push(`import type { AiBindingLike, LunoraAi } from "@lunora/ai";`, `import { createAi } from "@lunora/ai";`);
|
|
2004
2148
|
}
|
|
2005
2149
|
importLines.push(
|
|
2150
|
+
...accessFragments.importLines,
|
|
2006
2151
|
...kvFragments.importLines,
|
|
2007
2152
|
...flagsFragments.importLines,
|
|
2008
2153
|
...analyticsFragments.importLines,
|
|
@@ -2017,7 +2162,9 @@ const emitShard = ({
|
|
|
2017
2162
|
...paymentsImports,
|
|
2018
2163
|
``,
|
|
2019
2164
|
`import schema from "../schema.js";`,
|
|
2020
|
-
|
|
2165
|
+
// Local-first sync registries are pulled in alongside the function table
|
|
2166
|
+
// only when the project declares them, so the import list stays minimal.
|
|
2167
|
+
`import { ${["LUNORA_FUNCTIONS", "LUNORA_LIFECYCLE_HOOKS", "LUNORA_MIGRATIONS", ...hasMutators ? ["LUNORA_MUTATOR_PATHS"] : [], ...hasShapes ? ["LUNORA_SHAPES"] : []].join(", ")} } from "./functions.js";`
|
|
2021
2168
|
);
|
|
2022
2169
|
const vectorsConfigField = hasVectors ? `
|
|
2023
2170
|
vectors?: (env: Record<string, unknown>) => Record<string, VectorizeIndexLike>;` : "";
|
|
@@ -2142,6 +2289,37 @@ const vectorsStub: VectorSearchLike = {
|
|
|
2142
2289
|
const bindTableHelper = "";
|
|
2143
2290
|
const globalDatabaseThunk = hasHyperdriveGlobal ? "config.hyperdriveGlobal" : "config.d1";
|
|
2144
2291
|
const globalDatabaseLine = hasGlobalTables ? ` const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, { identity, userId }) ?? globalDbStub;
|
|
2292
|
+
` : "";
|
|
2293
|
+
const globalShapeReaderOverride = hasShapes && hasGlobalTables ? `
|
|
2294
|
+
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 }>> {
|
|
2295
|
+
const env = this.env as Record<string, unknown>;
|
|
2296
|
+
const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, identity) ?? globalDbStub;
|
|
2297
|
+
const rows: Array<{ doc: Record<string, unknown>; id: string }> = [];
|
|
2298
|
+
|
|
2299
|
+
let cursor: null | string = null;
|
|
2300
|
+
|
|
2301
|
+
// Drain every page of the global membership so the seed/diff sees the
|
|
2302
|
+
// full rowset (D1 \`findMany\` is paginated). Stop one row past the cap:
|
|
2303
|
+
// a broad \`.global()\` shape would otherwise materialize an unbounded
|
|
2304
|
+
// array before the caller's \`withinGlobalShapeBound\` check rejects it,
|
|
2305
|
+
// so bail early and let the caller fail it closed.
|
|
2306
|
+
do {
|
|
2307
|
+
// eslint-disable-next-line no-await-in-loop -- sequential page drain to assemble the full membership
|
|
2308
|
+
const page = await globalDb.findMany(resolved.table, { cursor, where: resolved.effectiveWhere });
|
|
2309
|
+
|
|
2310
|
+
for (const doc of page.page) {
|
|
2311
|
+
rows.push({ doc, id: String((doc as { _id?: unknown })._id) });
|
|
2312
|
+
|
|
2313
|
+
if (rows.length > ShardDOBase.GLOBAL_SHAPE_MAX_ROWS) {
|
|
2314
|
+
return rows;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
cursor = page.isDone ? null : page.continueCursor;
|
|
2319
|
+
} while (cursor !== null);
|
|
2320
|
+
|
|
2321
|
+
return rows;
|
|
2322
|
+
}
|
|
2145
2323
|
` : "";
|
|
2146
2324
|
const facadeBlock = hasTables ? `
|
|
2147
2325
|
const facade = db as unknown as Record<string, ReturnType<typeof bindTableFacade>>;
|
|
@@ -2158,8 +2336,8 @@ ${schema.tables.map(
|
|
|
2158
2336
|
const secretsBuild = `
|
|
2159
2337
|
const secrets = createSecrets(env);
|
|
2160
2338
|
`;
|
|
2161
|
-
const everyContextBuild = `${kvFragments.build}${flagsFragments.build}${analyticsFragments.build}${secretsBuild}`;
|
|
2162
|
-
const everyContextField = `${kvFragments.contextField}${flagsFragments.contextField}${analyticsFragments.contextField}
|
|
2339
|
+
const everyContextBuild = `${accessFragments.build}${kvFragments.build}${flagsFragments.build}${analyticsFragments.build}${secretsBuild}`;
|
|
2340
|
+
const everyContextField = `${accessFragments.contextField}${kvFragments.contextField}${flagsFragments.contextField}${analyticsFragments.contextField}
|
|
2163
2341
|
secrets,`;
|
|
2164
2342
|
const actionOnlyHasAny = hasImages || hasHyperdrive || hasBrowser || hasR2sql || hasPipelines;
|
|
2165
2343
|
const actionOnlyBlock = actionOnlyHasAny ? `
|
|
@@ -2211,7 +2389,7 @@ const LUNORA_STORAGE_RULES: StorageRulesResult = ${JSON.stringify(storageRulesDa
|
|
|
2211
2389
|
|
|
2212
2390
|
/** 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. */
|
|
2213
2391
|
const LUNORA_STUDIO_FEATURES: StudioFeaturesResult = ${JSON.stringify(studioFeaturesData, void 0, 4)};
|
|
2214
|
-
${flagsOverrides.constant}${workflowsMetadataConst}${queuesMetadataConst}${containerSpecs}${workflowSpecs}${queueSpecs}
|
|
2392
|
+
${flagsOverrides.constant}${shapeReadRegistryConst}${workflowsMetadataConst}${queuesMetadataConst}${containerSpecs}${workflowSpecs}${queueSpecs}
|
|
2215
2393
|
export interface ShardDOConfig {
|
|
2216
2394
|
/** Opt into change-data-capture: records a post-image to \`__cdc_log\` on every write (backs streaming export + replay-PITR). */
|
|
2217
2395
|
cdc?: boolean;
|
|
@@ -2326,8 +2504,20 @@ export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOSt
|
|
|
2326
2504
|
// do external I/O that can't be rolled back, so both dispatch directly.
|
|
2327
2505
|
// \`ctx.run*\` composition runs inside this span (it never re-enters
|
|
2328
2506
|
// handleRpc); runInTransaction's own guard rejects accidental nesting.
|
|
2507
|
+
//
|
|
2508
|
+
// The replay bookkeeping (idempotency dedup row + custom-mutator
|
|
2509
|
+
// watermark advance) commits INSIDE this span via
|
|
2510
|
+
// \`commitMutationBookkeeping\`, so the writes, the dedup row, and the
|
|
2511
|
+
// watermark are atomic — a crash can't leave the writes durable without
|
|
2512
|
+
// the replay guard.
|
|
2329
2513
|
if (registered.kind === "mutation") {
|
|
2330
|
-
return this.runInTransaction(() =>
|
|
2514
|
+
return this.runInTransaction(async () => {
|
|
2515
|
+
const result = await registered.handler(ctx, args);
|
|
2516
|
+
|
|
2517
|
+
this.commitMutationBookkeeping(result);
|
|
2518
|
+
|
|
2519
|
+
return result;
|
|
2520
|
+
});
|
|
2331
2521
|
}
|
|
2332
2522
|
|
|
2333
2523
|
return registered.handler(ctx, args);
|
|
@@ -2365,7 +2555,7 @@ ${relationFanout.override}
|
|
|
2365
2555
|
iterator: (signal) => (registered.handler as (context: unknown, args: Record<string, unknown>, signal: AbortSignal) => AsyncIterable<unknown>)(this.buildCtx({ functionPath }), args, signal),
|
|
2366
2556
|
};
|
|
2367
2557
|
}
|
|
2368
|
-
|
|
2558
|
+
${customMutatorOverride}${shapeResolveOverride}${globalShapeReaderOverride}
|
|
2369
2559
|
protected override lifecycleHookPaths(event: "connect" | "disconnect"): readonly string[] {
|
|
2370
2560
|
return LUNORA_LIFECYCLE_HOOKS[event];
|
|
2371
2561
|
}
|
|
@@ -2941,4 +3131,4 @@ const emitWranglerCronTriggers = (crons) => {
|
|
|
2941
3131
|
return triggers;
|
|
2942
3132
|
};
|
|
2943
3133
|
|
|
2944
|
-
export { GENERATED_HEADER, buildStorageColumns, emitApi, emitContainers, emitCrons, emitDataModel, emitDrizzleSchema, emitFunctions, emitQueues, emitSeed, emitServer, emitShard, emitVectors, emitWorkflows, emitWranglerCronTriggers };
|
|
3134
|
+
export { GENERATED_HEADER, buildStorageColumns, emitApi, emitCollections, emitContainers, emitCrons, emitDataModel, emitDrizzleSchema, emitFunctions, emitQueues, emitSeed, emitServer, emitShard, emitVectors, emitWorkflows, emitWranglerCronTriggers };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { Node, SyntaxKind } from 'ts-morph';
|
|
4
|
+
import { diagnosticAt } from './CodegenDiagnosticError-DeblMkzO.mjs';
|
|
5
|
+
|
|
6
|
+
const MUTATORS_FILENAME = "mutators.ts";
|
|
7
|
+
const MUTATOR_MODULE_SPECIFIERS = /* @__PURE__ */ new Set(["@lunora/server", "lunorash/server"]);
|
|
8
|
+
const isDefineMutator = (identifier) => {
|
|
9
|
+
const symbol = identifier.getSymbol();
|
|
10
|
+
if (!symbol) {
|
|
11
|
+
return identifier.getText() === "defineMutator";
|
|
12
|
+
}
|
|
13
|
+
for (const declaration of symbol.getDeclarations()) {
|
|
14
|
+
if (!Node.isImportSpecifier(declaration)) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (!MUTATOR_MODULE_SPECIFIERS.has(declaration.getImportDeclaration().getModuleSpecifierValue())) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return declaration.getNameNode().getText() === "defineMutator";
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
};
|
|
24
|
+
const isMutatorNamespaceImport = (identifier) => {
|
|
25
|
+
const symbol = identifier.getSymbol();
|
|
26
|
+
if (!symbol) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
for (const declaration of symbol.getDeclarations()) {
|
|
30
|
+
if (!Node.isNamespaceImport(declaration)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const importDeclaration = declaration.getFirstAncestorByKind(SyntaxKind.ImportDeclaration);
|
|
34
|
+
return importDeclaration !== void 0 && MUTATOR_MODULE_SPECIFIERS.has(importDeclaration.getModuleSpecifierValue());
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
};
|
|
38
|
+
const isDefineMutatorCallee = (callee) => {
|
|
39
|
+
if (Node.isIdentifier(callee)) {
|
|
40
|
+
return isDefineMutator(callee);
|
|
41
|
+
}
|
|
42
|
+
if (Node.isPropertyAccessExpression(callee)) {
|
|
43
|
+
const object = callee.getExpression();
|
|
44
|
+
return callee.getName() === "defineMutator" && Node.isIdentifier(object) && isMutatorNamespaceImport(object);
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
};
|
|
48
|
+
const mutatorsFromSource = (source) => {
|
|
49
|
+
const mutators = [];
|
|
50
|
+
for (const declaration of source.getVariableDeclarations()) {
|
|
51
|
+
if (!declaration.isExported()) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const initializer = declaration.getInitializer();
|
|
55
|
+
if (initializer?.getKind() !== SyntaxKind.CallExpression) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const callee = initializer.getExpression();
|
|
59
|
+
if (!isDefineMutatorCallee(callee)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const nameNode = declaration.getNameNode();
|
|
63
|
+
if (!Node.isIdentifier(nameNode)) {
|
|
64
|
+
throw diagnosticAt(nameNode, "defineMutator exports must be plain named exports (no destructuring)");
|
|
65
|
+
}
|
|
66
|
+
mutators.push({ exportName: nameNode.getText(), filePath: "mutators" });
|
|
67
|
+
}
|
|
68
|
+
return mutators;
|
|
69
|
+
};
|
|
70
|
+
const discoverMutators = (project, lunoraDirectory) => {
|
|
71
|
+
const mutatorsPath = join(lunoraDirectory, MUTATORS_FILENAME);
|
|
72
|
+
if (!existsSync(mutatorsPath)) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const source = project.getSourceFile(mutatorsPath) ?? project.addSourceFileAtPath(mutatorsPath);
|
|
76
|
+
const mutators = mutatorsFromSource(source);
|
|
77
|
+
mutators.sort((a, b) => a.exportName.localeCompare(b.exportName));
|
|
78
|
+
return mutators;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export { MUTATORS_FILENAME, discoverMutators, isDefineMutatorCallee };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GENERATED_HEADER } from './GENERATED_HEADER-
|
|
1
|
+
import { GENERATED_HEADER } from './GENERATED_HEADER-D-AbhmRh.mjs';
|
|
2
2
|
import { s as sanitizeNamespace } from './paths-BRd6JHuF.mjs';
|
|
3
3
|
import { argsObjectSchema, LUNORA_ERROR_CODES } from './LUNORA_ERROR_CODES-CySpQPD3.mjs';
|
|
4
4
|
|