@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.
@@ -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: dispatchEntries.length > 0 ? `
655
- ${dispatchEntries}
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
- `import { asBucketStorage, createSecrets } from "${base.server}";`
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
- `import { LUNORA_FUNCTIONS, LUNORA_LIFECYCLE_HOOKS, LUNORA_MIGRATIONS } from "./functions.js";`
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(() => registered.handler(ctx, args));
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-CkZUgM4_.mjs';
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