@zodmire/core 0.1.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/mod.d.mts +38 -6
  2. package/mod.mjs +1611 -264
  3. package/package.json +2 -2
package/mod.mjs CHANGED
@@ -63,7 +63,7 @@ const VENDORED_FILE_MANIFEST = [
63
63
  sourceRelativePath: "packages/config/composition_config.ts"
64
64
  }
65
65
  ];
66
- /** Rewrite Deno-style `npm:zod@*` imports to bare `"zod"` for end-user consumption. */
66
+ /** Rewrite `npm:zod@*` imports to bare `"zod"` for end-user consumption. */
67
67
  function rewriteImports(content) {
68
68
  return content.replace(/["']npm:zod@[^"']*["']/g, "\"zod\"");
69
69
  }
@@ -160,6 +160,44 @@ function createReadField(exportName) {
160
160
  sourceSchema: rawSchema,
161
161
  objectShape: def.shape
162
162
  };
163
+ case zx.tagged("intersection")(rawSchema): {
164
+ const left = def.left;
165
+ const right = def.right;
166
+ if (left?.objectShape && right?.objectShape) return {
167
+ type: "object",
168
+ optional: false,
169
+ array: false,
170
+ sourceSchema: rawSchema,
171
+ objectShape: {
172
+ ...left.objectShape,
173
+ ...right.objectShape
174
+ }
175
+ };
176
+ return {
177
+ type: "intersection",
178
+ optional: false,
179
+ array: false,
180
+ sourceSchema: rawSchema
181
+ };
182
+ }
183
+ case zx.tagged("union")(rawSchema): return {
184
+ type: "union",
185
+ optional: false,
186
+ array: false,
187
+ sourceSchema: rawSchema
188
+ };
189
+ case zx.tagged("record")(rawSchema): return {
190
+ type: "record",
191
+ optional: false,
192
+ array: false,
193
+ sourceSchema: rawSchema
194
+ };
195
+ case zx.tagged("tuple")(rawSchema): return {
196
+ type: "tuple",
197
+ optional: false,
198
+ array: false,
199
+ sourceSchema: rawSchema
200
+ };
163
201
  case zx.tagged("string")(rawSchema):
164
202
  case zx.tagged("number")(rawSchema):
165
203
  case zx.tagged("int")(rawSchema):
@@ -470,6 +508,64 @@ function mergeSchemaReadResults(results) {
470
508
 
471
509
  //#endregion
472
510
  //#region packages/core/normalizer.ts
511
+ function generatedCapability() {
512
+ return {
513
+ status: "generated",
514
+ reasons: []
515
+ };
516
+ }
517
+ function disabledCapability(reason) {
518
+ return {
519
+ status: "disabled",
520
+ reasons: [reason]
521
+ };
522
+ }
523
+ function runtimeThrowCapability(reasons) {
524
+ return {
525
+ status: "runtime-throw",
526
+ reasons
527
+ };
528
+ }
529
+ function canMapReadModelOutput$1(readModel, outputFields) {
530
+ const readModelFields = new Set(readModel.fields.map((field) => camelCase(field.name)));
531
+ return outputFields.every((field) => readModelFields.has(camelCase(field.name)));
532
+ }
533
+ function resolveGeneratedFindByIdLookupField(readModel, query) {
534
+ if (readModel.primaryKey.length !== 1) return null;
535
+ const primaryKeyField = camelCase(readModel.primaryKey[0]);
536
+ const matchingField = (query.inputFields ?? []).find((field) => camelCase(field.name) === primaryKeyField);
537
+ if (matchingField) return camelCase(matchingField.name);
538
+ if ((query.inputFields ?? []).length === 1) return camelCase(query.inputFields[0].name);
539
+ return null;
540
+ }
541
+ function deriveCommandCapability(command) {
542
+ const reasons = [];
543
+ if (!command.effects || command.effects.length === 0) reasons.push("command does not declare any effects");
544
+ if (command.commandKind === "update" && !command.loadBy) reasons.push("mutation commands require loadBy");
545
+ return reasons.length === 0 ? generatedCapability() : runtimeThrowCapability(reasons);
546
+ }
547
+ function deriveQueryCapability(query, readModelByName) {
548
+ const reasons = [];
549
+ if (!query.readModelName) reasons.push("query does not declare readModelName");
550
+ const readModel = query.readModelName ? readModelByName.get(query.readModelName) : void 0;
551
+ if (!readModel) reasons.push(`read-model "${query.readModelName ?? query.name}" is missing`);
552
+ else {
553
+ if (!canMapReadModelOutput$1(readModel, query.outputFields)) reasons.push("output fields do not map directly to the read-model");
554
+ if (query.queryKind === "findById" && !resolveGeneratedFindByIdLookupField(readModel, query)) reasons.push("read-model does not expose a stable findById lookup field");
555
+ }
556
+ return reasons.length === 0 ? generatedCapability() : runtimeThrowCapability(reasons);
557
+ }
558
+ function deriveProjectionCapabilities(projection, readModelByName) {
559
+ const writeReasons = [];
560
+ if (!readModelByName.has(projection.readModelName)) writeReasons.push(`read-model "${projection.readModelName}" is missing`);
561
+ for (const source of projection.sources) if (source.mutation.kind === "custom") writeReasons.push(`source "${source.contextName}.${source.aggregateName}.${source.eventName}" requires custom mutation handler "${source.mutation.handlerName}"`);
562
+ const writeModel = writeReasons.length === 0 ? generatedCapability() : runtimeThrowCapability(writeReasons);
563
+ return {
564
+ projector: generatedCapability(),
565
+ writeModel,
566
+ rebuild: !projection.rebuild?.enabled ? disabledCapability("projection rebuild is disabled") : writeModel.status === "generated" ? generatedCapability() : runtimeThrowCapability([...writeModel.reasons])
567
+ };
568
+ }
473
569
  function buildContextNormalizedSpec(schemaResult, config) {
474
570
  if (schemaResult.aggregates.length === 0) throw new Error(`[normalizer] No aggregate found in SchemaReadResult. Context "${config.name}" requires at least one aggregate.`);
475
571
  const aggregateNames = new Set(schemaResult.aggregates.map((a) => a.name));
@@ -653,7 +749,8 @@ function buildContextNormalizedSpec(schemaResult, config) {
653
749
  sources: projection.sources.map((source) => ({
654
750
  ...source,
655
751
  locality: source.contextName === config.name ? "local" : "external"
656
- }))
752
+ })),
753
+ capabilities: deriveProjectionCapabilities(projection, readModelByName)
657
754
  };
658
755
  });
659
756
  return {
@@ -685,9 +782,11 @@ function buildContextNormalizedSpec(schemaResult, config) {
685
782
  loadBy: cmd.loadBy,
686
783
  preconditions: cmd.preconditions,
687
784
  emits: cmd.emits,
688
- effects: cmd.effects
785
+ effects: cmd.effects,
786
+ capability: deriveCommandCapability(cmd)
689
787
  })),
690
788
  queries: schemaResult.queries.map((q) => {
789
+ const capability = deriveQueryCapability(q, readModelByName);
691
790
  const readSide = {
692
791
  readModelName: q.readModelName ?? pascalCase(q.name),
693
792
  searchFields: [],
@@ -746,7 +845,8 @@ function buildContextNormalizedSpec(schemaResult, config) {
746
845
  pagination: { style: "offset" },
747
846
  filters: q.filters,
748
847
  sorting: q.sorting,
749
- computedFields: resolvedComputed
848
+ computedFields: resolvedComputed,
849
+ capability
750
850
  };
751
851
  }
752
852
  return {
@@ -758,7 +858,8 @@ function buildContextNormalizedSpec(schemaResult, config) {
758
858
  outputSchemaPath: q.outputSchemaPath,
759
859
  outputSchemaExportName: q.outputSchemaExportName,
760
860
  readModelName: q.readModelName,
761
- readSide
861
+ readSide,
862
+ capability
762
863
  };
763
864
  }),
764
865
  readModels: normalizedReadModels,
@@ -826,6 +927,7 @@ function walkEntityTree(aggregate, visitor) {
826
927
  //#region packages/core/composition_normalizer.ts
827
928
  function buildNormalizedCompositionSpec(config, contextSpecs) {
828
929
  assertUniqueCanonicalContextSpecs(contextSpecs);
930
+ const infrastructure = resolveInfrastructureStrategy(config.infrastructure);
829
931
  const resolveProvidedContextRef = createContextRefResolver(contextSpecs);
830
932
  const resolvedContexts = [];
831
933
  const resolvedContextSpecs = [];
@@ -884,9 +986,60 @@ function buildNormalizedCompositionSpec(config, contextSpecs) {
884
986
  contexts: resolvedContexts,
885
987
  crossContextEvents,
886
988
  acls,
989
+ infrastructure,
887
990
  materialization: { targetRoot: config.materialization.targetRoot }
888
991
  };
889
992
  }
993
+ function resolveInfrastructureStrategy(input) {
994
+ const supportedTuples = [
995
+ {
996
+ architecture: "physical-cqrs",
997
+ persistence: "postgres",
998
+ orm: "drizzle"
999
+ },
1000
+ {
1001
+ architecture: "logical-cqrs",
1002
+ persistence: "postgres",
1003
+ orm: "drizzle"
1004
+ },
1005
+ {
1006
+ architecture: "logical-cqrs",
1007
+ persistence: "mysql",
1008
+ orm: "drizzle"
1009
+ },
1010
+ {
1011
+ architecture: "physical-cqrs",
1012
+ persistence: "mysql",
1013
+ orm: "drizzle"
1014
+ },
1015
+ {
1016
+ architecture: "physical-cqrs",
1017
+ persistence: "postgres",
1018
+ orm: "mikroorm"
1019
+ },
1020
+ {
1021
+ architecture: "logical-cqrs",
1022
+ persistence: "postgres",
1023
+ orm: "mikroorm"
1024
+ },
1025
+ {
1026
+ architecture: "physical-cqrs",
1027
+ persistence: "mysql",
1028
+ orm: "mikroorm"
1029
+ },
1030
+ {
1031
+ architecture: "logical-cqrs",
1032
+ persistence: "mysql",
1033
+ orm: "mikroorm"
1034
+ }
1035
+ ];
1036
+ if (!supportedTuples.some((tuple) => tuple.architecture === input.architecture && tuple.persistence === input.persistence && tuple.orm === input.orm)) throw new Error(`[composition] Unsupported infrastructure strategy:\narchitecture=${input.architecture}, persistence=${input.persistence}, orm=${input.orm}\n\nCurrently supported strategies:\n` + supportedTuples.map((tuple) => `- ${tuple.architecture} + ${tuple.persistence} + ${tuple.orm}`).join("\n"));
1037
+ return {
1038
+ architecture: input.architecture,
1039
+ persistence: input.persistence,
1040
+ orm: input.orm
1041
+ };
1042
+ }
890
1043
  function assertUniqueCanonicalContextSpecs(contextSpecs) {
891
1044
  const seenModulePaths = /* @__PURE__ */ new Map();
892
1045
  for (const spec of contextSpecs) {
@@ -1140,6 +1293,10 @@ function aggregateVariableName$1(spec) {
1140
1293
  function aggregateIdPropertyName$1(spec) {
1141
1294
  return `${aggregateVariableName$1(spec)}Id`;
1142
1295
  }
1296
+ function defaultIdBrandName(name) {
1297
+ const base = pascalCase(name);
1298
+ return base.endsWith("Id") ? base : `${base}Id`;
1299
+ }
1143
1300
  function formatFieldType$1(field, indent = 0) {
1144
1301
  if (field.type === "object" && field.nestedFields && field.nestedFields.length > 0) {
1145
1302
  const objectType = `{\n${field.nestedFields.map((nestedField) => {
@@ -1367,6 +1524,7 @@ function renderEffectLines(effects, inputVar, spec, indent, selfRef = "this", is
1367
1524
  const aggregateFieldsByName = new Map(filterManagedAggregateFields$1(spec.aggregate.fields ?? []).map((field) => [camelCase(field.name), field]));
1368
1525
  let lastPushedEntityVar = null;
1369
1526
  let lastPushedEntityIdField = null;
1527
+ let lastPushedEntityTypeName = null;
1370
1528
  const idFieldName = spec.aggregate.idField ? camelCase(spec.aggregate.idField) : null;
1371
1529
  const lines = [];
1372
1530
  for (const effect of effects) switch (effect.kind) {
@@ -1397,11 +1555,11 @@ function renderEffectLines(effects, inputVar, spec, indent, selfRef = "this", is
1397
1555
  }
1398
1556
  return `${fieldName}: ${aggregateAccess}`;
1399
1557
  }
1400
- if (lastPushedEntityVar && !aggregateFieldNames.has(fieldName)) return `${fieldName}: (${lastPushedEntityVar} as any).${fieldName}`;
1558
+ if (lastPushedEntityVar && !aggregateFieldNames.has(fieldName)) return `${fieldName}: (${lastPushedEntityVar} as ${lastPushedEntityTypeName ?? "Record<string, unknown>"}).${fieldName}`;
1401
1559
  if (availableInputFields.has(fieldName)) return `${fieldName}: ${inputVar}.${fieldName}`;
1402
1560
  if (fieldName === "occurredAt" || fieldName.endsWith("At")) return `${fieldName}: ${f.type === "date" ? "new Date()" : "new Date().toISOString()"}`;
1403
1561
  if (fieldName === "recordedBy" || fieldName.endsWith("By")) return `${fieldName}: ${f.type === "string" ? "eventContext.recordedBy.value" : "eventContext.recordedBy"}`;
1404
- return `${fieldName}: undefined as unknown as ${eventName}Payload["${fieldName}"]`;
1562
+ return `${fieldName}: /* TODO: unmapped ${eventName} payload field "${fieldName}" */ undefined as never`;
1405
1563
  }).join(", ")} }` : "{}";
1406
1564
  lines.push(`${indent}${selfRef}.raise(createDomainEvent<${eventName}Payload>({`, `${indent} eventType: "${contextName}.${eventName}",`, `${indent} eventVersion: 1,`, `${indent} entityType: "${aggregateName}",`, `${indent} entityId: ${selfRef}.id,`, `${indent} contextName: "${contextName}",`, `${indent} recordedBy: eventContext.recordedBy,`, `${indent} correlationId: eventContext.correlationId,`, `${indent} causationId: eventContext.causationId,`, `${indent} payload: ${payloadObj},`, `${indent}}));`);
1407
1565
  break;
@@ -1420,6 +1578,7 @@ function renderEffectLines(effects, inputVar, spec, indent, selfRef = "this", is
1420
1578
  const entityVar = camelCase(targetChild.name);
1421
1579
  lastPushedEntityVar = entityVar;
1422
1580
  lastPushedEntityIdField = targetChild.idField;
1581
+ lastPushedEntityTypeName = entityName;
1423
1582
  lines.push(`${indent}const ${entityVar} = ${entityName}.create({`, `${indent} ${camelCase(targetChild.idField)}: ${createIdFn}(crypto.randomUUID()),`, `${indent} ...${inputVar} as any,`, `${indent}});`, `${indent}${selfRef}.${camelCase(effect.target)}.push(${entityVar});`);
1424
1583
  } else lines.push(`${indent}${selfRef}.${camelCase(effect.target)}.push(${renderEffectValue(effect.value, inputVar, selfRef)});`);
1425
1584
  break;
@@ -1520,10 +1679,10 @@ function createAggregateStub(spec) {
1520
1679
  return ` readonly ${camelCase(child.collectionFieldName)}: ${entityName}[] = [];`;
1521
1680
  });
1522
1681
  const constructorAssignments = nonIdFields.map((f) => ` this.${camelCase(f.name)} = props.${camelCase(f.name)};`).join("\n");
1523
- const idTypeStr = idType ? `BrandedId<"${idType}">` : `BrandedId<string>`;
1682
+ const idTypeStr = `BrandedId<"${idType && idType !== "string" ? pascalCase(idType) : defaultIdBrandName(spec.aggregate.name)}">`;
1524
1683
  let createBody;
1525
1684
  if (createCmd?.effects?.length) createBody = ` const instance = new ${aggregateClassName}(id, 0, props);
1526
- ${renderEffectLines(createCmd.effects, "props", spec, " ", "instance", true).join("\n")}
1685
+ ${renderEffectLines(createCmd.effects.filter((effect) => effect.kind !== "set-field" || effect.value !== `input.${effect.target}`), "props", spec, " ", "instance", true).join("\n")}
1527
1686
  return instance;`;
1528
1687
  else createBody = ` return new ${aggregateClassName}(id, 0, props);`;
1529
1688
  const fieldTypeMap = /* @__PURE__ */ new Map();
@@ -1601,9 +1760,8 @@ ${methodLines.join("\n")}
1601
1760
  const opt = f.optional ? "?" : "";
1602
1761
  persistenceLines.push(` readonly ${camelCase(f.name)}${opt}: ${formatPersistenceFieldType(f)};`);
1603
1762
  }
1604
- const fromPersistenceFields = nonIdFields.map((f) => ` ${camelCase(f.name)}: state.${camelCase(f.name)},`);
1605
- for (const child of spec.aggregate.children ?? []) fromPersistenceFields.push(` ${camelCase(child.collectionFieldName)}: state.${camelCase(child.collectionFieldName)},`);
1606
- const fromPersistenceAssignment = fromPersistenceFields.length > 0 ? ` Object.assign(instance, {\n${fromPersistenceFields.join("\n")}\n });` : "";
1763
+ const fromPersistenceProps = nonIdFields.map((f) => ` ${camelCase(f.name)}: state.${camelCase(f.name)},`).join("\n");
1764
+ const collectionAssignments = (spec.aggregate.children ?? []).map((child) => ` instance.${camelCase(child.collectionFieldName)}.push(...state.${camelCase(child.collectionFieldName)});`).join("\n");
1607
1765
  return `${imports}
1608
1766
 
1609
1767
  export interface ${aggregateClassName}Props {
@@ -1644,8 +1802,11 @@ ${createBody}
1644
1802
  rehydrationStateName,
1645
1803
  children: spec.aggregate.children ?? []
1646
1804
  })}(state);
1647
- const instance = new ${aggregateClassName}(state.${camelCase(idField)}, state.version, state as unknown as ${aggregateClassName}Props);
1648
- ${fromPersistenceAssignment}
1805
+ const props: ${aggregateClassName}Props = {
1806
+ ${fromPersistenceProps}
1807
+ };
1808
+ const instance = new ${aggregateClassName}(state.${camelCase(idField)}, state.version, props);
1809
+ ${collectionAssignments}
1649
1810
  return instance;
1650
1811
  }${mutationMethods.join("")}
1651
1812
  }
@@ -1657,7 +1818,7 @@ function createEntityStub(entity, depthFromDomain = 1) {
1657
1818
  const idField = entity.idField;
1658
1819
  const idType = entity.idType;
1659
1820
  const nonIdFields = fields.filter((f) => f.name !== idField);
1660
- const idTypeStr = idType && idType !== "string" ? `BrandedId<"${idType}">` : `BrandedId<string>`;
1821
+ const idTypeStr = `BrandedId<"${idType && idType !== "string" ? pascalCase(idType) : defaultIdBrandName(entity.name)}">`;
1661
1822
  const libRelPath = "../../../../../lib";
1662
1823
  const imports = [
1663
1824
  `import { Entity } from "${libRelPath}/entity.base.ts";`,
@@ -1703,9 +1864,8 @@ function createEntityStub(entity, depthFromDomain = 1) {
1703
1864
  const opt = f.optional ? "?" : "";
1704
1865
  persistenceLines.push(` readonly ${camelCase(f.name)}${opt}: ${formatPersistenceFieldType(f)};`);
1705
1866
  }
1706
- const entityFromPersistenceFields = nonIdFields.map((f) => ` ${camelCase(f.name)}: state.${camelCase(f.name)},`);
1707
- for (const child of children) entityFromPersistenceFields.push(` ${camelCase(child.collectionFieldName)}: state.${camelCase(child.collectionFieldName)},`);
1708
- const entityFromPersistenceAssignment = entityFromPersistenceFields.length > 0 ? ` Object.assign(instance, {\n${entityFromPersistenceFields.join("\n")}\n });` : "";
1867
+ const entityFromPersistenceProps = nonIdFields.map((f) => ` ${camelCase(f.name)}: state.${camelCase(f.name)},`).join("\n");
1868
+ const entityCollectionAssignments = children.map((child) => ` instance.${camelCase(child.collectionFieldName)}.push(...state.${camelCase(child.collectionFieldName)});`).join("\n");
1709
1869
  return `${imports.join("\n")}
1710
1870
 
1711
1871
  export interface ${entityName}Props {
@@ -1746,8 +1906,11 @@ ${constructorAssignments}
1746
1906
  rehydrationStateName,
1747
1907
  children: entity.children ?? []
1748
1908
  })}(state);
1749
- const instance = new ${entityName}(state.${camelCase(idField)}, state as unknown as ${entityName}Props);
1750
- ${entityFromPersistenceAssignment}
1909
+ const props: ${entityName}Props = {
1910
+ ${entityFromPersistenceProps}
1911
+ };
1912
+ const instance = new ${entityName}(state.${camelCase(idField)}, props);
1913
+ ${entityCollectionAssignments}
1751
1914
  return instance;
1752
1915
  }
1753
1916
  }
@@ -2059,7 +2222,7 @@ function listQuerySortFieldTypeName$1(query) {
2059
2222
  function listQueryRowTypeName$1(query) {
2060
2223
  return `${listQueryTypeBaseName$1(query)}Row`;
2061
2224
  }
2062
- function readModelContractName$3(readModelName) {
2225
+ function readModelContractName$2(readModelName) {
2063
2226
  const baseName = pascalCase(readModelName);
2064
2227
  return baseName.endsWith("View") ? baseName : `${baseName}View`;
2065
2228
  }
@@ -2069,8 +2232,11 @@ function readModelPortBaseFileName(readModelName) {
2069
2232
  function resolvedReadModelName$2(query) {
2070
2233
  return query.readSide?.readModelName ?? query.readModelName ?? query.name;
2071
2234
  }
2235
+ function queryOutputExportName$1(query) {
2236
+ return query.outputSchemaExportName?.trim() || void 0;
2237
+ }
2072
2238
  function queryReadModelPortTypeName$1(query) {
2073
- return `${readModelContractName$3(resolvedReadModelName$2(query))}RepositoryPort`;
2239
+ return `${readModelContractName$2(resolvedReadModelName$2(query))}RepositoryPort`;
2074
2240
  }
2075
2241
  function queryReadModelPortFileName(query) {
2076
2242
  return `read-models/${readModelPortBaseFileName(resolvedReadModelName$2(query))}.repository.port.ts`;
@@ -2130,11 +2296,11 @@ ${query.outputFields.map((f) => {
2130
2296
  function createListQueryHandlerStub(_spec, query) {
2131
2297
  const handlerName = `${pascalCase(query.name)}Handler`;
2132
2298
  const queryTypeName = `${pascalCase(query.name)}Query`;
2133
- const outputContractName = queryOutputContractName$2(query);
2299
+ const outputContractName = queryOutputContractName$1(query);
2134
2300
  const readModelPortType = queryReadModelPortTypeName$1(query);
2135
2301
  const readModelVariable = queryReadModelVariableName$1(query);
2136
2302
  const readModelMethodName = queryReadModelMethodName$1(query);
2137
- const viewFileBase = queryViewFileBase$2(query);
2303
+ const viewFileBase = queryViewFileBase$1(query);
2138
2304
  return `import type { ${queryTypeName} } from "./${kebabCase(query.name)}.query.ts";
2139
2305
  import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";
2140
2306
  import type { ${readModelPortType} } from "../ports/${queryReadModelPortFileName(query)}";
@@ -2162,12 +2328,14 @@ function createHandlerDepsStub(spec) {
2162
2328
  const repoPort = findRepositoryPort(spec);
2163
2329
  const repoTypeName = repoPort ? repositoryPortTypeName(repoPort.name) : `${aggregatePascal}Repository`;
2164
2330
  return `import type { Transaction } from "../../../../lib/transaction.ts";
2331
+ import type { AggregateEventTracker } from "../../../shared-kernel/events/aggregate-event-tracker.ts";
2165
2332
  import type { EventCollector } from "../../../shared-kernel/events/event-collector.ts";
2166
2333
  import type { ${repoTypeName} } from "./ports/${repoPort ? repositoryPortFileName(repoPort.name) : `${kebabCase(spec.aggregate.name)}-repository.port.ts`}";
2167
2334
 
2168
2335
  export type ${contextPascal}CommandHandlerDeps = {
2169
2336
  tx: Transaction;
2170
2337
  eventCollector: EventCollector;
2338
+ aggregateEventTracker: AggregateEventTracker;
2171
2339
  repos: {
2172
2340
  ${aggregateVar}s: ${repoTypeName};
2173
2341
  };
@@ -2193,7 +2361,7 @@ ${(query.inputFields ?? []).map((f) => {
2193
2361
  `;
2194
2362
  }
2195
2363
  function createViewContractStub(query) {
2196
- return `export interface ${queryOutputContractName$2(query)} {
2364
+ return `export interface ${queryOutputContractName$1(query)} {
2197
2365
  ${query.outputFields.map((f) => {
2198
2366
  const opt = f.optional ? "?" : "";
2199
2367
  return ` readonly ${camelCase(f.name)}${opt}: ${formatFieldType(f)};`;
@@ -2201,11 +2369,14 @@ ${query.outputFields.map((f) => {
2201
2369
  }
2202
2370
  `;
2203
2371
  }
2204
- function queryOutputContractName$2(query) {
2205
- const baseName = pascalCase(resolvedReadModelName$2(query));
2206
- return baseName.endsWith("View") ? baseName : `${baseName}View`;
2372
+ function queryOutputContractName$1(query) {
2373
+ const outputExportName = queryOutputExportName$1(query);
2374
+ if (outputExportName) return pascalCase(outputExportName);
2375
+ return readModelContractName$2(resolvedReadModelName$2(query));
2207
2376
  }
2208
- function queryViewFileBase$2(query) {
2377
+ function queryViewFileBase$1(query) {
2378
+ const outputExportName = queryOutputExportName$1(query);
2379
+ if (outputExportName) return kebabCase(outputExportName).replace(/-view$/, "");
2209
2380
  return kebabCase(resolvedReadModelName$2(query)).replace(/-view$/, "");
2210
2381
  }
2211
2382
  function createCommandStub(spec, command) {
@@ -2254,7 +2425,8 @@ function buildCreateHandlerBody(spec, command) {
2254
2425
  const assignedValue = createPropAssignments.get(fieldName) ?? (commandInputFieldNames.has(fieldName) ? commandInputField?.optional && !field.optional ? `command.payload.${fieldName} ?? ${defaultValue}` : `command.payload.${fieldName}` : defaultValue);
2255
2426
  if (assignedValue !== void 0) propsLines.push(` ${fieldName}: ${assignedValue},`);
2256
2427
  }
2257
- const idTypeName = hasCustomIdType ? pascalCase(idType) : `BrandedId<"string">`;
2428
+ const defaultIdBrand = `${pascalCase(spec.aggregate.name)}Id`;
2429
+ const idTypeName = hasCustomIdType ? pascalCase(idType) : `BrandedId<"${defaultIdBrand}">`;
2258
2430
  const creationEventName = command.emits?.[0];
2259
2431
  const creationEvent = creationEventName ? spec.domainEvents.find((e) => pascalCase(e.name) === pascalCase(creationEventName)) : void 0;
2260
2432
  const aggregateFieldNames = new Set((spec.aggregate.fields ?? []).map((field) => camelCase(field.name)));
@@ -2285,7 +2457,7 @@ function buildCreateHandlerBody(spec, command) {
2285
2457
  const payloadName = `${pascalCase(creationEventName)}Payload`;
2286
2458
  imports.push(`import type { ${payloadName} } from "../../domain/events/${kebabCase(creationEventName)}.event.ts";`);
2287
2459
  }
2288
- const idExpr = hasCustomIdType ? `create${pascalCase(idType)}(crypto.randomUUID())` : `createBrandedId("${idType || "string"}", crypto.randomUUID())`;
2460
+ const idExpr = hasCustomIdType ? `create${pascalCase(idType)}(crypto.randomUUID())` : `createBrandedId("${hasCustomIdType ? idType : defaultIdBrand}", crypto.randomUUID())`;
2289
2461
  let creationEventLines = "";
2290
2462
  if (creationEvent) {
2291
2463
  const payloadName = `${pascalCase(creationEventName)}Payload`;
@@ -2304,7 +2476,7 @@ function buildCreateHandlerBody(spec, command) {
2304
2476
  else if (fieldName === "recordedBy" || fieldName === "correlationId" || fieldName === "causationId") {
2305
2477
  const metadataExpr = fieldName === "recordedBy" ? f.type === "string" ? "eventContext.recordedBy.value" : "eventContext.recordedBy" : `eventContext.${fieldName}`;
2306
2478
  payloadFields.push(` ${fieldName}: ${metadataExpr},`);
2307
- } else payloadFields.push(` ${fieldName}: undefined as unknown as ${payloadName}["${fieldName}"],`);
2479
+ } else payloadFields.push(` ${fieldName}: /* TODO: unmapped ${payloadName} field "${fieldName}" */ undefined as never,`);
2308
2480
  }
2309
2481
  creationEventLines = `
2310
2482
  deps.eventCollector.collect([
@@ -2381,7 +2553,7 @@ function buildMutationHandlerBody(spec, command) {
2381
2553
  }
2382
2554
  imports.push(`import type { ${notFoundErrorType} } from "../../domain/errors/${aggregateDir}-application-errors.ts";`);
2383
2555
  if (hasCustomIdType && idCreatorFn && idType) imports.push(`import { ${idCreatorFn} } from "../../../../shared-kernel/entity-ids/${kebabCase(idType)}.ts";`);
2384
- const domainMethodArg = cmdEmitsEvents ? "command.payload as any, eventContext" : "command.payload as any";
2556
+ const domainMethodArg = cmdEmitsEvents ? "command.payload, eventContext" : "command.payload";
2385
2557
  const loadByField = command.loadBy?.startsWith("input.") ? command.loadBy.slice(6) : aggregateId;
2386
2558
  const brandedIdExpr = hasCustomIdType && idCreatorFn ? `${idCreatorFn}(command.payload.${loadByField})` : `command.payload.${loadByField}`;
2387
2559
  let bodyLines;
@@ -2393,7 +2565,7 @@ function buildMutationHandlerBody(spec, command) {
2393
2565
  ` const result = ${aggregateVar}.${methodName}(${domainMethodArg});`,
2394
2566
  ` if (!result.ok) return result;`,
2395
2567
  ` await deps.repos.${camelCase(spec.aggregate.name)}s.save(${aggregateVar}, loadedVersion, deps.tx);`,
2396
- ` deps.eventCollector.collect(${aggregateVar}.pullDomainEvents());`,
2568
+ ` deps.aggregateEventTracker.track(${aggregateVar});`,
2397
2569
  ` return ok(undefined);`
2398
2570
  ];
2399
2571
  else bodyLines = [
@@ -2403,7 +2575,7 @@ function buildMutationHandlerBody(spec, command) {
2403
2575
  ` const loadedVersion = ${aggregateVar}.version;`,
2404
2576
  ` ${aggregateVar}.${methodName}(${domainMethodArg});`,
2405
2577
  ` await deps.repos.${camelCase(spec.aggregate.name)}s.save(${aggregateVar}, loadedVersion, deps.tx);`,
2406
- ` deps.eventCollector.collect(${aggregateVar}.pullDomainEvents());`,
2578
+ ` deps.aggregateEventTracker.track(${aggregateVar});`,
2407
2579
  ` return ok(undefined);`
2408
2580
  ];
2409
2581
  const handlerErrorTypeExport = hasPreconditions ? `export type ${handlerErrorType} =\n | ${errorTypes.join("\n | ")};\n` : `export type ${handlerErrorType} = ${notFoundErrorType};\n`;
@@ -2436,11 +2608,14 @@ export async function ${handlerFnName}(
2436
2608
  command: ${commandTypeName},
2437
2609
  deps: ${depsTypeName},
2438
2610
  eventContext: EventContext,
2439
- ): Promise<void> {
2611
+ ): Promise<never> {
2440
2612
  void command;
2441
2613
  void deps;
2442
2614
  void eventContext;
2443
- // TODO: load aggregate, execute domain operation, save, pull events
2615
+ throw new Error(
2616
+ "[application_generator] Command \\"${spec.context.name}.${command.name}\\" cannot be fully generated. " +
2617
+ "Expected a create command with effects or a mutation command with both effects and loadBy.",
2618
+ );
2444
2619
  }
2445
2620
  `;
2446
2621
  }
@@ -2457,12 +2632,12 @@ export interface ${queryName} {
2457
2632
  }
2458
2633
  function buildQueryHandlerBody(_spec, query) {
2459
2634
  const queryTypeName = `${pascalCase(query.name)}Query`;
2460
- const outputContractName = queryOutputContractName$2(query);
2635
+ const outputContractName = queryOutputContractName$1(query);
2461
2636
  const readModelPortType = queryReadModelPortTypeName$1(query);
2462
2637
  const handlerName = `${pascalCase(query.name)}Handler`;
2463
2638
  const readModelVariable = queryReadModelVariableName$1(query);
2464
2639
  const readModelMethodName = queryReadModelMethodName$1(query);
2465
- const viewFileBase = queryViewFileBase$2(query);
2640
+ const viewFileBase = queryViewFileBase$1(query);
2466
2641
  return `import type { ${queryTypeName} } from "./${kebabCase(query.name)}.query.ts";
2467
2642
  import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";
2468
2643
  import type { ${readModelPortType} } from "../ports/${queryReadModelPortFileName(query)}";
@@ -2483,12 +2658,12 @@ export class ${handlerName} {
2483
2658
  function createQueryHandlerStub(_spec, query) {
2484
2659
  if (query.outputFields && query.outputFields.length > 0) return buildQueryHandlerBody(_spec, query);
2485
2660
  const queryTypeName = `${pascalCase(query.name)}Query`;
2486
- const outputContractName = queryOutputContractName$2(query);
2661
+ const outputContractName = queryOutputContractName$1(query);
2487
2662
  const readModelPortType = queryReadModelPortTypeName$1(query);
2488
2663
  const handlerName = `${pascalCase(query.name)}Handler`;
2489
2664
  const readModelVariable = queryReadModelVariableName$1(query);
2490
2665
  const readModelMethodName = queryReadModelMethodName$1(query);
2491
- const viewFileBase = queryViewFileBase$2(query);
2666
+ const viewFileBase = queryViewFileBase$1(query);
2492
2667
  return `import type { ${queryTypeName} } from "./${kebabCase(query.name)}.query.ts";
2493
2668
  import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";
2494
2669
  import type { ${readModelPortType} } from "../ports/${queryReadModelPortFileName(query)}";
@@ -2551,46 +2726,71 @@ function groupQueriesByReadModel$1(queries) {
2551
2726
  }));
2552
2727
  }
2553
2728
  function createReadModelRepositoryPortStub(readModelName, queries) {
2554
- const portTypeName = `${readModelContractName$3(readModelName)}RepositoryPort`;
2729
+ const portTypeName = `${readModelContractName$2(readModelName)}RepositoryPort`;
2555
2730
  const importLines = /* @__PURE__ */ new Map();
2556
2731
  let needsPaginatedResult = false;
2557
- importLines.set("tx", `import type { Transaction } from "../../../../../lib/transaction.ts";`);
2732
+ importLines.set("tx", `import type { Transaction } from "../../../../../../lib/transaction.ts";`);
2558
2733
  const queryMethodLines = queries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((query) => {
2559
2734
  const queryTypeName = `${pascalCase(query.name)}Query`;
2560
- const outputContractName = queryOutputContractName$2(query);
2561
- const viewFileBase = queryViewFileBase$2(query);
2735
+ const outputContractName = queryOutputContractName$1(query);
2736
+ const viewFileBase = queryViewFileBase$1(query);
2562
2737
  const returnType = query.queryKind === "list" ? `PaginatedResult<${outputContractName}>` : outputContractName;
2563
2738
  if (query.queryKind === "list") needsPaginatedResult = true;
2564
- importLines.set(`query:${queryTypeName}`, `import type { ${queryTypeName} } from "../queries/${kebabCase(query.name)}.query.ts";`);
2565
- importLines.set(`view:${outputContractName}`, `import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";`);
2739
+ importLines.set(`query:${queryTypeName}`, `import type { ${queryTypeName} } from "../../queries/${kebabCase(query.name)}.query.ts";`);
2740
+ importLines.set(`view:${outputContractName}`, `import type { ${outputContractName} } from "../../contracts/${viewFileBase}.view.ts";`);
2566
2741
  return ` ${queryReadModelMethodName$1(query)}(query: ${queryTypeName}, tx: Transaction): Promise<${returnType}>;`;
2567
2742
  });
2568
- if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "../../../../../lib/pagination.ts";`);
2743
+ if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "../../../../../../lib/pagination.ts";`);
2569
2744
  return `${[...importLines.values()].join("\n")}
2570
2745
 
2571
2746
  // Read-model repository port. Query handlers share this contract by read-model, not by query.
2572
2747
  export interface ${portTypeName} {
2573
- // Query methods
2574
2748
  ${queryMethodLines.join("\n")}
2575
-
2576
- // Projector methods
2577
- // TODO: add typed row write methods when projector/repository generation lands.
2578
-
2579
- // Rebuild methods
2580
- // TODO: add rebuild helpers when projection rebuild scaffolding lands.
2581
2749
  }
2582
2750
  `;
2583
2751
  }
2584
2752
  function createReadModelPortsIndexStub(queries) {
2585
2753
  return `${groupQueriesByReadModel$1(queries).map(({ readModelName }) => {
2586
- return `export type { ${`${readModelContractName$3(readModelName)}RepositoryPort`} } from "./${readModelPortBaseFileName(readModelName)}.repository.port.ts";`;
2754
+ return `export type { ${`${readModelContractName$2(readModelName)}RepositoryPort`} } from "./${readModelPortBaseFileName(readModelName)}.repository.port.ts";`;
2587
2755
  }).join("\n")}\n`;
2588
2756
  }
2757
+ function projectionWritePortName$2(projectionName) {
2758
+ return `${pascalCase(projectionName)}WritePort`;
2759
+ }
2760
+ function projectionWritePortFileName(projectionName) {
2761
+ return `${kebabCase(projectionName)}.projection-write.port.ts`;
2762
+ }
2763
+ function projectionSourcePayloadTypeName$1(source) {
2764
+ return `${pascalCase(source.eventName)}Payload`;
2765
+ }
2766
+ function projectionSourcePayloadImportPath$1(source) {
2767
+ return `../../../${normalizeModulePath(source.contextName)}/domain/events/${kebabCase(source.eventName)}.event.ts`;
2768
+ }
2769
+ function projectionSourceEventType$1(source) {
2770
+ const payloadType = projectionSourcePayloadTypeName$1(source);
2771
+ return `EventEnvelope<${payloadType}> | ${payloadType}`;
2772
+ }
2773
+ function createProjectionWritePortStub(projection) {
2774
+ const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionSourcePayloadTypeName$1(source)} } from "${projectionSourcePayloadImportPath$1(source)}";`);
2775
+ const methodLines = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => ` on${pascalCase(source.eventName)}(event: ${projectionSourceEventType$1(source)}, tx: Transaction): Promise<void>;`);
2776
+ return `import type { EventEnvelope } from "../../../../../../lib/event-envelope.ts";
2777
+ import type { Transaction } from "../../../../../../lib/transaction.ts";
2778
+ ${payloadImports.join("\n")}
2779
+
2780
+ // Projector-facing write port for the "${projection.readModelName}" read model.
2781
+ export interface ${projectionWritePortName$2(projection.name)} {
2782
+ ${methodLines.join("\n")}
2783
+ }
2784
+ `;
2785
+ }
2786
+ function createProjectionWritePortsIndexStub(projections) {
2787
+ return `${projections.slice().sort((left, right) => left.name.localeCompare(right.name)).map((projection) => `export type { ${projectionWritePortName$2(projection.name)} } from "./${projectionWritePortFileName(projection.name)}";`).join("\n")}\n`;
2788
+ }
2589
2789
  function inputContractFileName(commandOrQueryName) {
2590
2790
  return `${kebabCase(commandOrQueryName)}.input`;
2591
2791
  }
2592
2792
  function viewContractFileName(query) {
2593
- return `${kebabCase(resolvedReadModelName$2(query)).replace(/-view$/, "")}.view`;
2793
+ return `${queryViewFileBase$1(query)}.view`;
2594
2794
  }
2595
2795
  function createHandlerMapStub(spec) {
2596
2796
  const contextPascal = pascalCase(spec.context.name);
@@ -2613,7 +2813,11 @@ ${mapEntries.join("\n")}
2613
2813
  }
2614
2814
  function createContextHandlerDepsStub(contextSpec) {
2615
2815
  const contextPascal = pascalCase(contextSpec.context.name);
2616
- const importLines = [`import type { Transaction } from "../../../../lib/transaction.ts";`, `import type { EventCollector } from "../../../shared-kernel/events/event-collector.ts";`];
2816
+ const importLines = [
2817
+ `import type { Transaction } from "../../../../lib/transaction.ts";`,
2818
+ `import type { AggregateEventTracker } from "../../../shared-kernel/events/aggregate-event-tracker.ts";`,
2819
+ `import type { EventCollector } from "../../../shared-kernel/events/event-collector.ts";`
2820
+ ];
2617
2821
  const repoEntries = [];
2618
2822
  const aclEntries = [];
2619
2823
  for (const agg of contextSpec.aggregates) {
@@ -2639,6 +2843,7 @@ ${aclEntries.join("\n")}
2639
2843
  export type ${contextPascal}CommandHandlerDeps = {
2640
2844
  tx: Transaction;
2641
2845
  eventCollector: EventCollector;
2846
+ aggregateEventTracker: AggregateEventTracker;
2642
2847
  repos: {
2643
2848
  ${repoEntries.join("\n")}
2644
2849
  };
@@ -2682,9 +2887,9 @@ function createPublishedReadBoundaryStub(contextSpec) {
2682
2887
  const queryHandlerName = `${pascalCase(query.name)}Handler`;
2683
2888
  const handlerVarName = `${camelCase(query.name)}Handler`;
2684
2889
  const queryTypeName = `${pascalCase(query.name)}Query`;
2685
- const outputContractName = queryOutputContractName$2(query);
2890
+ const outputContractName = queryOutputContractName$1(query);
2686
2891
  const queryFileName = kebabCase(query.name);
2687
- const outputFileName = queryViewFileBase$2(query);
2892
+ const outputFileName = queryViewFileBase$1(query);
2688
2893
  const readModelDepName = queryReadModelVariableName$1(query);
2689
2894
  const readModelTypeName = queryReadModelPortTypeName$1(query);
2690
2895
  imports.set(`handler:${queryHandlerName}`, `import { ${queryHandlerName} } from "./queries/${queryFileName}.handler.ts";`);
@@ -2766,16 +2971,25 @@ function buildV5ApplicationArtifacts(spec, options) {
2766
2971
  }
2767
2972
  function buildV5ApplicationContextArtifacts(contextSpec) {
2768
2973
  const modulePath = normalizeModulePath(contextSpec.context.modulePath);
2769
- return [
2974
+ const artifacts = [
2770
2975
  createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/ports/read-models/index.ts"), createReadModelPortsIndexStub(contextSpec.queries)),
2771
2976
  createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/handler-deps.ts"), createContextHandlerDepsStub(contextSpec)),
2772
2977
  createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/read-boundary.ts"), createPublishedReadBoundaryStub(contextSpec)),
2773
2978
  createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/handler-map.ts"), createContextHandlerMapStub(contextSpec))
2774
2979
  ];
2980
+ const projections = contextSpec.projections ?? [];
2981
+ if (projections.length > 0) {
2982
+ artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/ports/projections/index.ts"), createProjectionWritePortsIndexStub(projections)));
2983
+ for (const projection of projections) artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, `application/ports/projections/${projectionWritePortFileName(projection.name)}`), createProjectionWritePortStub(projection)));
2984
+ }
2985
+ return artifacts;
2775
2986
  }
2776
2987
 
2777
2988
  //#endregion
2778
2989
  //#region packages/core/generators/infrastructure.ts
2990
+ function resolveDrizzlePersistence(infrastructureStrategy) {
2991
+ return infrastructureStrategy?.persistence === "mysql" ? "mysql" : "postgres";
2992
+ }
2779
2993
  function isDecimalNumberField(field) {
2780
2994
  if (field.type !== "number") return false;
2781
2995
  const explicitColumnType = field.columnType?.toLowerCase();
@@ -2790,10 +3004,12 @@ function isDecimalNumberField(field) {
2790
3004
  const fieldName = snakeCase(field.name);
2791
3005
  return /(^|_)(cpk|ppk|ucl|lcl)(_|$)/.test(fieldName) || /(percent|percentage|ratio|rate|average|avg|variance|yield|scrap|drift)/.test(fieldName);
2792
3006
  }
2793
- function drizzleColumnBuilder(field, columnName) {
3007
+ function drizzleColumnBuilder(field, columnName, persistence = "postgres") {
2794
3008
  let builder;
2795
- if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0) builder = `jsonb("${columnName}").$type<${formatTsFieldType(field)}>()`;
2796
- else switch (field.type) {
3009
+ if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0) {
3010
+ const jsonType = formatTsFieldType(field);
3011
+ builder = persistence === "mysql" ? `json("${columnName}").$type<${jsonType}>()` : `jsonb("${columnName}").$type<${jsonType}>()`;
3012
+ } else switch (field.type) {
2797
3013
  case "number":
2798
3014
  builder = isDecimalNumberField(field) ? `real("${columnName}")` : `integer("${columnName}")`;
2799
3015
  break;
@@ -2900,12 +3116,12 @@ function relativePrefixFromRepositoryDirectory(modulePath) {
2900
3116
  ...modulePath.split("/").filter(Boolean)
2901
3117
  ].map(() => "..").join("/");
2902
3118
  }
2903
- function readModelContractName$2(readModelName) {
3119
+ function readModelContractName$1(readModelName) {
2904
3120
  const baseName = pascalCase(readModelName);
2905
3121
  return baseName.endsWith("View") ? baseName : `${baseName}View`;
2906
3122
  }
2907
3123
  function readModelRepositoryClassName(readModelName) {
2908
- return `Drizzle${readModelContractName$2(readModelName)}Repository`;
3124
+ return `Drizzle${readModelContractName$1(readModelName)}Repository`;
2909
3125
  }
2910
3126
  function readModelRepositoryFileBase$1(readModelName) {
2911
3127
  return kebabCase(readModelName).replace(/-view$/, "");
@@ -2913,13 +3129,29 @@ function readModelRepositoryFileBase$1(readModelName) {
2913
3129
  function readModelTableConstName(readModelName) {
2914
3130
  return `${camelCase(readModelName)}Table`;
2915
3131
  }
3132
+ function mikroOrmRepositoryClassName(aggregateName) {
3133
+ return `MikroOrm${pascalCase(aggregateName)}Repository`;
3134
+ }
3135
+ function mikroOrmEntityClassName(name) {
3136
+ return `${pascalCase(name)}Record`;
3137
+ }
3138
+ function mikroOrmEntitySchemaConstName(name) {
3139
+ return `${pascalCase(name)}Schema`;
3140
+ }
2916
3141
  function resolvedReadModelName$1(query) {
2917
3142
  return query.readSide?.readModelName ?? query.readModelName ?? query.name;
2918
3143
  }
2919
- function queryOutputContractName$1(query) {
2920
- return readModelContractName$2(resolvedReadModelName$1(query));
3144
+ function queryOutputExportName(query) {
3145
+ return query.outputSchemaExportName?.trim() || void 0;
2921
3146
  }
2922
- function queryViewFileBase$1(query) {
3147
+ function queryOutputContractName(query) {
3148
+ const outputExportName = queryOutputExportName(query);
3149
+ if (outputExportName) return pascalCase(outputExportName);
3150
+ return readModelContractName$1(resolvedReadModelName$1(query));
3151
+ }
3152
+ function queryViewFileBase(query) {
3153
+ const outputExportName = queryOutputExportName(query);
3154
+ if (outputExportName) return kebabCase(outputExportName).replace(/-view$/, "");
2923
3155
  return readModelRepositoryFileBase$1(resolvedReadModelName$1(query));
2924
3156
  }
2925
3157
  function queryReadModelMethodName(query) {
@@ -2947,13 +3179,13 @@ const RESERVED_AGGREGATE_FIELD_NAMES = new Set(["version"]);
2947
3179
  function filterManagedAggregateFields(fields) {
2948
3180
  return fields.filter((field) => !RESERVED_AGGREGATE_FIELD_NAMES.has(camelCase(field.name)));
2949
3181
  }
2950
- function buildTableBlock(tableName, tableConstName, fields, idField, fkColumn, isAggregateRoot = false) {
3182
+ function buildTableBlock(tableName, tableConstName, fields, idField, fkColumn, isAggregateRoot = false, persistence = "postgres") {
2951
3183
  const columnDefs = [];
2952
3184
  const emittedColumns = /* @__PURE__ */ new Set();
2953
3185
  for (const field of fields) {
2954
3186
  const snakeName = snakeCase(field.name);
2955
3187
  if (emittedColumns.has(snakeName)) continue;
2956
- const columnDef = field.name === idField ? `text("${snakeName}").primaryKey()` : drizzleColumnBuilder(field, snakeName);
3188
+ const columnDef = field.name === idField ? `text("${snakeName}").primaryKey()` : drizzleColumnBuilder(field, snakeName, persistence);
2957
3189
  columnDefs.push(` ${snakeName}: ${columnDef},`);
2958
3190
  emittedColumns.add(snakeName);
2959
3191
  }
@@ -2962,16 +3194,16 @@ function buildTableBlock(tableName, tableConstName, fields, idField, fkColumn, i
2962
3194
  emittedColumns.add(fkColumn.name);
2963
3195
  }
2964
3196
  if (isAggregateRoot) columnDefs.push(` version: integer("version").default(0).notNull(),`);
2965
- return `export const ${tableConstName} = pgTable("${tableName}", {\n${columnDefs.join("\n")}\n});`;
3197
+ return `export const ${tableConstName} = ${persistence === "mysql" ? "mysqlTable" : "pgTable"}("${tableName}", {\n${columnDefs.join("\n")}\n});`;
2966
3198
  }
2967
- function createDrizzleTableDefinition(spec) {
3199
+ function createDrizzleTableDefinition(spec, persistence = "postgres") {
2968
3200
  const fields = filterManagedAggregateFields(spec.aggregate.fields);
2969
3201
  if (!fields || fields.length === 0) return null;
2970
3202
  const children = spec.aggregate.children ?? [];
2971
3203
  const aggScalarFields = scalarFields(fields, children);
2972
3204
  const emittedTableNames = /* @__PURE__ */ new Set();
2973
3205
  const blocks = [];
2974
- blocks.push(buildTableBlock(spec.aggregate.tableName, `${camelCase(spec.aggregate.name)}Table`, aggScalarFields, spec.aggregate.idField, void 0, true));
3206
+ blocks.push(buildTableBlock(spec.aggregate.tableName, `${camelCase(spec.aggregate.name)}Table`, aggScalarFields, spec.aggregate.idField, void 0, true, persistence));
2975
3207
  emittedTableNames.add(spec.aggregate.tableName);
2976
3208
  const aggregate = {
2977
3209
  name: spec.aggregate.name,
@@ -2988,13 +3220,13 @@ function createDrizzleTableDefinition(spec) {
2988
3220
  blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, {
2989
3221
  name: fkColName,
2990
3222
  type: "text"
2991
- }));
3223
+ }, false, persistence));
2992
3224
  emittedTableNames.add(entity.tableName);
2993
3225
  });
2994
3226
  for (const entity of spec.entities ?? []) {
2995
3227
  if (!entity.tableName || emittedTableNames.has(entity.tableName)) continue;
2996
3228
  const entityScalarFields = scalarFields(entity.fields, entity.children ?? []);
2997
- blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField));
3229
+ blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, void 0, false, persistence));
2998
3230
  emittedTableNames.add(entity.tableName);
2999
3231
  }
3000
3232
  const flatScalarFields = [
@@ -3002,12 +3234,12 @@ function createDrizzleTableDefinition(spec) {
3002
3234
  ...flattenEntities(aggregate).map((entity) => scalarFields(entity.fields, entity.children)),
3003
3235
  ...(spec.entities ?? []).map((entity) => scalarFields(entity.fields, entity.children ?? []))
3004
3236
  ].flat();
3005
- const pgCoreImportTokens = ["pgTable", "text"];
3006
- if (flatScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) pgCoreImportTokens.push("integer");
3007
- if (flatScalarFields.some((field) => isDecimalNumberField(field))) pgCoreImportTokens.push("real");
3008
- if (flatScalarFields.some((field) => field.type === "boolean")) pgCoreImportTokens.push("boolean");
3009
- if (flatScalarFields.some((field) => field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0)) pgCoreImportTokens.push("jsonb");
3010
- return `import { ${pgCoreImportTokens.join(", ")} } from "drizzle-orm/pg-core";\n\n${blocks.join("\n\n")}\n`;
3237
+ const coreImportTokens = [persistence === "mysql" ? "mysqlTable" : "pgTable", "text"];
3238
+ if (flatScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) coreImportTokens.push("integer");
3239
+ if (flatScalarFields.some((field) => isDecimalNumberField(field))) coreImportTokens.push("real");
3240
+ if (flatScalarFields.some((field) => field.type === "boolean")) coreImportTokens.push("boolean");
3241
+ if (flatScalarFields.some((field) => field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0)) coreImportTokens.push(persistence === "mysql" ? "json" : "jsonb");
3242
+ return `import { ${coreImportTokens.join(", ")} } from "${persistence === "mysql" ? "drizzle-orm/mysql-core" : "drizzle-orm/pg-core"}";\n\n${blocks.join("\n\n")}\n`;
3011
3243
  }
3012
3244
  function buildCodecBlock(_spec, entityName, fields, options) {
3013
3245
  const varName = camelCase(entityName);
@@ -3078,6 +3310,280 @@ function createPersistenceEqualityBlock(name, fields, options) {
3078
3310
  return `// Compiled from zx.deepEqual.writeable at generator time using the canonical ${pascalCase(name)} persistence row shape.
3079
3311
  export const ${equalityName} = ${compiledEquality};`;
3080
3312
  }
3313
+ function buildMikroOrmRootNode(spec) {
3314
+ const buildChild = (entity, parent) => {
3315
+ const node = {
3316
+ name: entity.name,
3317
+ className: mikroOrmEntityClassName(entity.name),
3318
+ schemaConstName: mikroOrmEntitySchemaConstName(entity.name),
3319
+ fields: entity.fields,
3320
+ scalarFields: scalarFields(entity.fields, entity.children),
3321
+ idField: entity.idField,
3322
+ idType: entity.idType,
3323
+ tableName: entity.tableName,
3324
+ collectionFieldName: entity.collectionFieldName,
3325
+ parent: {
3326
+ className: parent.className,
3327
+ propertyName: camelCase(parent.name),
3328
+ fkFieldName: snakeCase(parent.idField)
3329
+ },
3330
+ children: [],
3331
+ isAggregateRoot: false
3332
+ };
3333
+ node.children = (entity.children ?? []).map((child) => buildChild(child, node));
3334
+ return node;
3335
+ };
3336
+ const rootNode = {
3337
+ name: spec.aggregate.name,
3338
+ className: mikroOrmEntityClassName(spec.aggregate.name),
3339
+ schemaConstName: mikroOrmEntitySchemaConstName(spec.aggregate.name),
3340
+ fields: filterManagedAggregateFields(spec.aggregate.fields),
3341
+ scalarFields: scalarFields(filterManagedAggregateFields(spec.aggregate.fields), spec.aggregate.children ?? []),
3342
+ idField: spec.aggregate.idField,
3343
+ idType: spec.aggregate.idType,
3344
+ tableName: spec.aggregate.tableName,
3345
+ children: [],
3346
+ isAggregateRoot: true
3347
+ };
3348
+ rootNode.children = (spec.aggregate.children ?? []).map((child) => buildChild(child, rootNode));
3349
+ return rootNode;
3350
+ }
3351
+ function flattenMikroOrmNodes(rootNode) {
3352
+ const nodes = [rootNode];
3353
+ const queue = [...rootNode.children];
3354
+ while (queue.length > 0) {
3355
+ const node = queue.shift();
3356
+ nodes.push(node);
3357
+ queue.push(...node.children);
3358
+ }
3359
+ return nodes;
3360
+ }
3361
+ function mikroOrmClassFieldType(field) {
3362
+ if (field.type === "object" && field.nestedFields && field.nestedFields.length > 0) return formatTsFieldType(field);
3363
+ switch (field.type) {
3364
+ case "number": return field.array ? "number[]" : "number";
3365
+ case "boolean": return field.array ? "boolean[]" : "boolean";
3366
+ default: return field.array ? "string[]" : "string";
3367
+ }
3368
+ }
3369
+ function mikroOrmPropertyType(field) {
3370
+ if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0) return "json";
3371
+ switch (field.type) {
3372
+ case "number": return "number";
3373
+ case "boolean": return "boolean";
3374
+ default: return "string";
3375
+ }
3376
+ }
3377
+ function createMikroOrmEntitySchemaArtifactContent(spec) {
3378
+ const nodes = flattenMikroOrmNodes(buildMikroOrmRootNode(spec));
3379
+ const contextPrefix = pascalCase(spec.context.name);
3380
+ const imports = [`import { Cascade, EntitySchema${nodes.some((node) => node.children.length > 0) ? ", Collection" : ""} } from "@mikro-orm/core";`];
3381
+ const classBlocks = nodes.map((node) => {
3382
+ const lines = [`export class ${node.className} {`];
3383
+ for (const field of node.scalarFields) lines.push(` ${camelCase(field.name)}${field.optional ? "?" : "!"}: ${mikroOrmClassFieldType(field)};`);
3384
+ if (node.isAggregateRoot) lines.push(` version!: number;`);
3385
+ if (node.parent) lines.push(` ${node.parent.propertyName}!: ${node.parent.className};`);
3386
+ for (const child of node.children) {
3387
+ const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
3388
+ lines.push(` ${collectionName} = new Collection<${child.className}>(this);`);
3389
+ }
3390
+ lines.push(`}`);
3391
+ return lines.join("\n");
3392
+ });
3393
+ const schemaBlocks = nodes.map((node) => {
3394
+ const propertyLines = [];
3395
+ for (const field of node.scalarFields) {
3396
+ const options = [`type: "${mikroOrmPropertyType(field)}"`, `fieldName: "${snakeCase(field.name)}"`];
3397
+ if (camelCase(field.name) === camelCase(node.idField)) options.push(`primary: true`);
3398
+ if (field.optional) options.push(`nullable: true`);
3399
+ propertyLines.push(` ${camelCase(field.name)}: { ${options.join(", ")} },`);
3400
+ }
3401
+ if (node.isAggregateRoot) propertyLines.push(` version: { type: "number", fieldName: "version" },`);
3402
+ if (node.parent) propertyLines.push(` ${node.parent.propertyName}: { kind: "m:1", entity: () => ${node.parent.className}, fieldName: "${node.parent.fkFieldName}" },`);
3403
+ for (const child of node.children) {
3404
+ const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
3405
+ propertyLines.push(` ${collectionName}: { kind: "1:m", entity: () => ${child.className}, mappedBy: "${child.parent?.propertyName}", cascade: [Cascade.PERSIST, Cascade.REMOVE], orphanRemoval: true },`);
3406
+ }
3407
+ return `export const ${node.schemaConstName} = new EntitySchema<${node.className}>({
3408
+ name: "${contextPrefix}${node.className}",
3409
+ class: ${node.className},
3410
+ tableName: "${node.tableName}",
3411
+ properties: {
3412
+ ${propertyLines.join("\n")}
3413
+ },
3414
+ });`;
3415
+ });
3416
+ return `${imports.join("\n")}
3417
+
3418
+ ${classBlocks.join("\n\n")}
3419
+
3420
+ ${schemaBlocks.join("\n\n")}
3421
+
3422
+ export const ${camelCase(spec.aggregate.name)}MikroOrmEntities = [${nodes.map((node) => node.schemaConstName).join(", ")}] as const;
3423
+ `;
3424
+ }
3425
+ function serializeDomainFieldValue(field, sourceExpr) {
3426
+ if (field.type === "date" || field.type === "Date") return `${sourceExpr} instanceof Date ? ${sourceExpr}.toISOString() : String(${sourceExpr})`;
3427
+ return sourceExpr;
3428
+ }
3429
+ function deserializePersistenceFieldValue(field, sourceExpr) {
3430
+ if (field.type === "date" || field.type === "Date") return `${sourceExpr} ? new Date(${sourceExpr}) : ${sourceExpr}`;
3431
+ return sourceExpr;
3432
+ }
3433
+ function rehydrateIdExpression(node, sourceExpr) {
3434
+ if (node.idType && node.idType !== "string") return `create${pascalCase(node.idType)}(${sourceExpr})`;
3435
+ return `createBrandedId("string", ${sourceExpr})`;
3436
+ }
3437
+ function buildMikroOrmNodeHelperBlock(spec, node) {
3438
+ const domainTypeName = node.isAggregateRoot ? `${pascalCase(spec.aggregate.name)}Aggregate` : pascalCase(node.name);
3439
+ const domainVar = "source";
3440
+ const recordVar = "target";
3441
+ const scalarAssignmentLines = node.scalarFields.map((field) => {
3442
+ const propertyName = camelCase(field.name);
3443
+ return ` ${recordVar}.${propertyName} = ${camelCase(field.name) === camelCase(node.idField) ? `String(${domainVar}.id.value)` : serializeDomainFieldValue(field, `${domainVar}.${propertyName}`)};`;
3444
+ });
3445
+ if (node.isAggregateRoot) scalarAssignmentLines.push(` ${recordVar}.version = ${domainVar}.version;`);
3446
+ const createChildLines = node.children.map((child) => {
3447
+ const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
3448
+ return ` ${recordVar}.${collectionName}.set(${domainVar}.${collectionName}.map((item) => create${child.className}FromDomain(item, ${recordVar})));`;
3449
+ });
3450
+ const syncChildLines = node.children.map((child) => {
3451
+ const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
3452
+ const childIdField = camelCase(child.idField);
3453
+ return ` {
3454
+ const currentById = new Map(${recordVar}.${collectionName}.getItems().map((item: ${child.className}) => [String(item.${childIdField}), item] as const));
3455
+ const nextItems = ${domainVar}.${collectionName}.map((item) => {
3456
+ const existing = currentById.get(String(item.id.value));
3457
+ if (existing) {
3458
+ sync${child.className}FromDomain(existing, item, ${recordVar});
3459
+ return existing;
3460
+ }
3461
+ return create${child.className}FromDomain(item, ${recordVar});
3462
+ });
3463
+ ${recordVar}.${collectionName}.set(nextItems);
3464
+ }`;
3465
+ });
3466
+ const parentParam = node.parent ? `, parent: ${node.parent.className}` : "";
3467
+ const parentAssignment = node.parent ? ` record.${node.parent.propertyName} = parent;\n` : "";
3468
+ const parentSyncParam = node.parent ? `, parent?: ${node.parent.className}` : "";
3469
+ const parentSyncAssignment = node.parent ? ` if (parent) {\n ${recordVar}.${node.parent.propertyName} = parent;\n }\n` : "";
3470
+ const domainScalarLines = node.scalarFields.map((field) => {
3471
+ const propertyName = camelCase(field.name);
3472
+ return ` ${propertyName}: ${camelCase(field.name) === camelCase(node.idField) ? rehydrateIdExpression(node, `${recordVar}.${propertyName}`) : deserializePersistenceFieldValue(field, `${recordVar}.${propertyName}`)},`;
3473
+ });
3474
+ if (node.isAggregateRoot) domainScalarLines.push(` version: ${recordVar}.version,`);
3475
+ for (const child of node.children) {
3476
+ const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
3477
+ domainScalarLines.push(` ${collectionName}: ${recordVar}.${collectionName}.getItems().map((item: ${child.className}) => map${child.className}ToDomain(item)),`);
3478
+ }
3479
+ return `function create${node.className}FromDomain(source: ${domainTypeName}${parentParam}): ${node.className} {
3480
+ const record = new ${node.className}();
3481
+ ${parentAssignment}${scalarAssignmentLines.join("\n")}
3482
+ ${createChildLines.join("\n")}
3483
+ return record;
3484
+ }
3485
+
3486
+ function sync${node.className}FromDomain(target: ${node.className}, source: ${domainTypeName}${parentSyncParam}): void {
3487
+ ${parentSyncAssignment}${scalarAssignmentLines.join("\n")}
3488
+ ${syncChildLines.join("\n")}
3489
+ }
3490
+
3491
+ function map${node.className}ToDomain(target: ${node.className}): ${domainTypeName} {
3492
+ return ${domainTypeName}.fromPersistence({
3493
+ ${domainScalarLines.join("\n")}
3494
+ });
3495
+ }`;
3496
+ }
3497
+ function createMikroOrmRepository(spec, portName) {
3498
+ const modulePath = normalizeModulePath(spec.context.modulePath);
3499
+ const relativePrefix = relativePrefixFromRepositoryDirectory(modulePath);
3500
+ const aggregateName = pascalCase(spec.aggregate.name);
3501
+ const aggregateDir = kebabCase(spec.aggregate.name);
3502
+ const repositoryPortType = repositoryPortTypeName(portName);
3503
+ const repositoryClassName = mikroOrmRepositoryClassName(spec.aggregate.name);
3504
+ const rootNode = buildMikroOrmRootNode(spec);
3505
+ const nodes = flattenMikroOrmNodes(rootNode);
3506
+ const needsBrandedIdHelper = nodes.some((node) => !node.idType || node.idType === "string");
3507
+ const customIdImports = /* @__PURE__ */ new Map();
3508
+ for (const node of nodes) if (node.idType && node.idType !== "string") customIdImports.set(node.idType, `import { create${pascalCase(node.idType)} } from "${relativePrefix}/core/shared-kernel/entity-ids/${kebabCase(node.idType)}";`);
3509
+ const notFoundError = (spec.aggregate.applicationErrors ?? [])[0];
3510
+ const notFoundImports = notFoundError ? (() => {
3511
+ const typeName = `${pascalCase(notFoundError.aggregateName)}NotFoundError`;
3512
+ return `import { ${notFoundError.factoryName}, type ${typeName} } from "${relativePrefix}/core/contexts/${modulePath}/domain/errors/${aggregateDir}-application-errors";\n`;
3513
+ })() : "";
3514
+ const notFoundTypeName = notFoundError ? `${pascalCase(notFoundError.aggregateName)}NotFoundError` : "Error";
3515
+ const notFoundFactory = notFoundError?.factoryName ?? "createNotFoundError";
3516
+ const rootIdParamName = `${camelCase(spec.aggregate.name)}Id`;
3517
+ const rootIdTypeName = rootNode.idType && rootNode.idType !== "string" ? pascalCase(rootNode.idType) : "string";
3518
+ const populatePaths = nodes.filter((node) => !node.isAggregateRoot).map((node) => {
3519
+ const segments = [];
3520
+ let current = node;
3521
+ while (current && !current.isAggregateRoot) {
3522
+ segments.unshift(camelCase(current.collectionFieldName ?? current.name));
3523
+ current = current.parent ? nodes.find((candidate) => candidate.className === current.parent?.className) : void 0;
3524
+ }
3525
+ return segments.join(".");
3526
+ }).filter((value, index, array) => value.length > 0 && array.indexOf(value) === index).sort();
3527
+ return `import type { Result } from "${relativePrefix}/lib/result";
3528
+ import { ok, err } from "${relativePrefix}/lib/result";
3529
+ import type { Transaction } from "${relativePrefix}/lib/transaction";
3530
+ import { ConcurrencyConflictError } from "${relativePrefix}/lib/concurrency-conflict-error";
3531
+ import type { ${repositoryPortType} } from "${relativePrefix}/core/contexts/${modulePath}/application/ports/${kebabCase(portName)}.port";
3532
+ ${needsBrandedIdHelper ? `import { createBrandedId } from "${relativePrefix}/lib/branded-id";\n` : ""}${[...customIdImports.values()].join("\n")}${customIdImports.size > 0 ? "\n" : ""}${notFoundImports}import { ${aggregateName}Aggregate } from "${relativePrefix}/core/contexts/${modulePath}/domain/aggregates/${aggregateDir}/${aggregateDir}.aggregate";
3533
+ ${nodes.filter((node) => !node.isAggregateRoot).map((node) => `import { ${pascalCase(node.name)}Entity as ${pascalCase(node.name)} } from "${relativePrefix}/core/contexts/${modulePath}/domain/entities/${kebabCase(node.name)}.entity";`).join("\n")}${nodes.some((node) => !node.isAggregateRoot) ? "\n" : ""}import { ${nodes.map((node) => node.className).join(", ")} } from "../../entities/${modulePath}/${kebabCase(spec.aggregate.name)}.entity-schema.ts";
3534
+
3535
+ const TOUCHED_AGGREGATES_KEY = "__zodmireTouchedAggregates";
3536
+
3537
+ function markTouchedAggregate(tx: unknown, aggregateType: string, aggregateId: string): void {
3538
+ const runtimeTx = tx as Record<string, unknown>;
3539
+ const touched = (runtimeTx[TOUCHED_AGGREGATES_KEY] ??= new Map<string, Set<string>>()) as Map<string, Set<string>>;
3540
+ const ids = touched.get(aggregateType) ?? new Set<string>();
3541
+ ids.add(aggregateId);
3542
+ touched.set(aggregateType, ids);
3543
+ }
3544
+
3545
+ ${nodes.slice().reverse().map((node) => buildMikroOrmNodeHelperBlock(spec, node)).join("\n\n")}
3546
+
3547
+ export class ${repositoryClassName} implements ${repositoryPortType} {
3548
+ async findById(${rootIdParamName}: ${rootIdTypeName}, tx: Transaction): Promise<Result<${aggregateName}Aggregate, ${notFoundTypeName}>> {
3549
+ const em = tx as any;
3550
+ const record = await em.findOne(${rootNode.className}, { ${camelCase(rootNode.idField)}: ${rootNode.idType && rootNode.idType !== "string" ? `${rootIdParamName}.value` : rootIdParamName} }, { populate: ${populatePaths.length > 0 ? `[${populatePaths.map((path) => `"${path}"`).join(", ")}]` : "[]"} });
3551
+ if (!record) {
3552
+ return err(${notFoundFactory}(String(${rootNode.idType && rootNode.idType !== "string" ? `${rootIdParamName}.value` : rootIdParamName})));
3553
+ }
3554
+
3555
+ return ok(map${rootNode.className}ToDomain(record));
3556
+ }
3557
+
3558
+ async create(${camelCase(spec.aggregate.name)}: ${aggregateName}Aggregate, tx: Transaction): Promise<void> {
3559
+ const em = tx as any;
3560
+ const rootRecord = create${rootNode.className}FromDomain(${camelCase(spec.aggregate.name)});
3561
+ markTouchedAggregate(tx, "${aggregateName}", String(${camelCase(spec.aggregate.name)}.id.value));
3562
+ em.persist(rootRecord);
3563
+ await em.flush();
3564
+ }
3565
+
3566
+ async save(${camelCase(spec.aggregate.name)}: ${aggregateName}Aggregate, expectedVersion: number, tx: Transaction): Promise<void> {
3567
+ const em = tx as any;
3568
+ const current = await em.findOne(${rootNode.className}, { ${camelCase(rootNode.idField)}: String(${camelCase(spec.aggregate.name)}.id.value) }, { populate: ${populatePaths.length > 0 ? `[${populatePaths.map((path) => `"${path}"`).join(", ")}]` : "[]"} });
3569
+
3570
+ if (!current || current.version !== expectedVersion) {
3571
+ throw new ConcurrencyConflictError({
3572
+ aggregateType: "${aggregateName}",
3573
+ aggregateId: String(${camelCase(spec.aggregate.name)}.id.value),
3574
+ expectedVersion,
3575
+ actualVersion: current?.version,
3576
+ });
3577
+ }
3578
+
3579
+ sync${rootNode.className}FromDomain(current, ${camelCase(spec.aggregate.name)});
3580
+ markTouchedAggregate(tx, "${aggregateName}", String(${camelCase(spec.aggregate.name)}.id.value));
3581
+ em.persist(current);
3582
+ await em.flush();
3583
+ }
3584
+ }
3585
+ `;
3586
+ }
3081
3587
  function childTableVar(spec, tableName) {
3082
3588
  const child = (spec.aggregate.children ?? []).find((c) => c.tableName === tableName);
3083
3589
  return child ? `${camelCase(child.name)}Table` : `${camelCase(tableName)}Table`;
@@ -3735,29 +4241,39 @@ function buildV5InfrastructureArtifacts(spec, options) {
3735
4241
  const modulePath = normalizeModulePath(spec.context.modulePath);
3736
4242
  const scopeKey = sliceArtifactOwnership(modulePath);
3737
4243
  const artifacts = [];
3738
- if (!options?.skipContextWideArtifacts) {
3739
- const tableContent = createDrizzleTableDefinition(spec);
4244
+ const hasDrizzleRepositoryAdapter = spec.adapters.some((adapter) => adapter.kind === "drizzle-repository");
4245
+ spec.adapters.some((adapter) => adapter.kind === "mikroorm-repository");
4246
+ if (!options?.skipContextWideArtifacts && hasDrizzleRepositoryAdapter) {
4247
+ const tableContent = createDrizzleTableDefinition(spec, resolveDrizzlePersistence(options?.infrastructureStrategy));
3740
4248
  if (tableContent !== null) {
3741
4249
  const tableLogicalPath = `infrastructure/persistence/${modulePath}/tables.ts`;
3742
4250
  artifacts.push(createGeneratedArtifact(tableLogicalPath, tableContent, scopeKey));
3743
4251
  }
3744
4252
  }
3745
- const repoArtifacts = spec.adapters.filter((adapter) => adapter.kind === "drizzle-repository").flatMap((adapter) => {
4253
+ const repoArtifacts = spec.adapters.flatMap((adapter) => {
3746
4254
  const port = spec.ports.find((p) => p.name === adapter.port);
3747
4255
  if (port === void 0) return [];
3748
- const logicalPath = buildArtifactPath("infrastructure/persistence/drizzle/repositories", modulePath, `drizzle-${kebabCase(spec.aggregate.name)}.repository.ts`);
3749
- return [createGeneratedArtifact(buildArtifactPath("infrastructure/persistence/drizzle/repositories", modulePath, `${kebabCase(spec.aggregate.name)}.deep-equal.ts`), createPersistenceEqualityArtifactContent(spec), scopeKey), createGeneratedArtifact(logicalPath, createDrizzleRepositoryWithCodec(spec, port.name), scopeKey)];
4256
+ if (adapter.kind === "drizzle-repository") {
4257
+ const logicalPath = buildArtifactPath("infrastructure/persistence/drizzle/repositories", modulePath, `drizzle-${kebabCase(spec.aggregate.name)}.repository.ts`);
4258
+ return [createGeneratedArtifact(buildArtifactPath("infrastructure/persistence/drizzle/repositories", modulePath, `${kebabCase(spec.aggregate.name)}.deep-equal.ts`), createPersistenceEqualityArtifactContent(spec), scopeKey), createGeneratedArtifact(logicalPath, createDrizzleRepositoryWithCodec(spec, port.name), scopeKey)];
4259
+ }
4260
+ if (adapter.kind === "mikroorm-repository") {
4261
+ const schemaLogicalPath = buildArtifactPath("infrastructure/persistence/mikroorm/entities", modulePath, `${kebabCase(spec.aggregate.name)}.entity-schema.ts`);
4262
+ const logicalPath = buildArtifactPath("infrastructure/persistence/mikroorm/repositories", modulePath, `mikroorm-${kebabCase(spec.aggregate.name)}.repository.ts`);
4263
+ return [createGeneratedArtifact(schemaLogicalPath, createMikroOrmEntitySchemaArtifactContent(spec), scopeKey), createGeneratedArtifact(logicalPath, createMikroOrmRepository(spec, port.name), scopeKey)];
4264
+ }
4265
+ return [];
3750
4266
  });
3751
4267
  artifacts.push(...repoArtifacts);
3752
4268
  return artifacts.sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
3753
4269
  }
3754
- function createContextDrizzleTableDefinition(contextSpec) {
4270
+ function createContextDrizzleTableDefinition(contextSpec, persistence = "postgres") {
3755
4271
  const blocks = [];
3756
4272
  const emittedTableNames = /* @__PURE__ */ new Set();
3757
4273
  let usesInteger = false;
3758
4274
  let usesReal = false;
3759
4275
  let usesBoolean = false;
3760
- let usesJsonb = false;
4276
+ let usesJson = false;
3761
4277
  for (const agg of contextSpec.aggregates) {
3762
4278
  const fields = filterManagedAggregateFields(agg.fields);
3763
4279
  if (!fields || fields.length === 0) continue;
@@ -3766,8 +4282,8 @@ function createContextDrizzleTableDefinition(contextSpec) {
3766
4282
  if (aggScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
3767
4283
  if (aggScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
3768
4284
  if (aggScalarFields.some((field) => field.type === "boolean")) usesBoolean = true;
3769
- if (aggScalarFields.some((field) => field.array)) usesJsonb = true;
3770
- blocks.push(buildTableBlock(agg.tableName, `${camelCase(agg.name)}Table`, aggScalarFields, agg.idField, void 0, true));
4285
+ if (aggScalarFields.some((field) => field.array)) usesJson = true;
4286
+ blocks.push(buildTableBlock(agg.tableName, `${camelCase(agg.name)}Table`, aggScalarFields, agg.idField, void 0, true, persistence));
3771
4287
  emittedTableNames.add(agg.tableName);
3772
4288
  walkEntityTree({
3773
4289
  name: agg.name,
@@ -3782,12 +4298,12 @@ function createContextDrizzleTableDefinition(contextSpec) {
3782
4298
  if (entityScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
3783
4299
  if (entityScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
3784
4300
  if (entityScalarFields.some((field) => field.type === "boolean")) usesBoolean = true;
3785
- if (entityScalarFields.some((field) => field.array)) usesJsonb = true;
4301
+ if (entityScalarFields.some((field) => field.array)) usesJson = true;
3786
4302
  const fkColName = snakeCase("idField" in ctx.parent ? ctx.parent.idField : agg.idField);
3787
4303
  blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, {
3788
4304
  name: fkColName,
3789
4305
  type: "text"
3790
- }));
4306
+ }, false, persistence));
3791
4307
  emittedTableNames.add(entity.tableName);
3792
4308
  });
3793
4309
  }
@@ -3797,21 +4313,22 @@ function createContextDrizzleTableDefinition(contextSpec) {
3797
4313
  if (entityScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
3798
4314
  if (entityScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
3799
4315
  if (entityScalarFields.some((field) => field.type === "boolean")) usesBoolean = true;
3800
- if (entityScalarFields.some((field) => field.array)) usesJsonb = true;
3801
- blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField));
4316
+ if (entityScalarFields.some((field) => field.array)) usesJson = true;
4317
+ blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, void 0, false, persistence));
3802
4318
  emittedTableNames.add(entity.tableName);
3803
4319
  }
3804
4320
  if (blocks.length === 0) return null;
3805
- const pgCoreImportTokens = ["pgTable", "text"];
3806
- if (usesInteger) pgCoreImportTokens.push("integer");
3807
- if (usesReal) pgCoreImportTokens.push("real");
3808
- if (usesBoolean) pgCoreImportTokens.push("boolean");
3809
- if (usesJsonb) pgCoreImportTokens.push("jsonb");
3810
- return `import { ${pgCoreImportTokens.join(", ")} } from "drizzle-orm/pg-core";\n\n${blocks.join("\n\n")}\n`;
3811
- }
3812
- function createReadModelTableDefinition(readModel) {
4321
+ const coreImportTokens = [persistence === "mysql" ? "mysqlTable" : "pgTable", "text"];
4322
+ if (usesInteger) coreImportTokens.push("integer");
4323
+ if (usesReal) coreImportTokens.push("real");
4324
+ if (usesBoolean) coreImportTokens.push("boolean");
4325
+ if (usesJson) coreImportTokens.push(persistence === "mysql" ? "json" : "jsonb");
4326
+ return `import { ${coreImportTokens.join(", ")} } from "${persistence === "mysql" ? "drizzle-orm/mysql-core" : "drizzle-orm/pg-core"}";\n\n${blocks.join("\n\n")}\n`;
4327
+ }
4328
+ function createReadModelTableDefinition(readModel, persistence = "postgres") {
4329
+ const tableBuilder = persistence === "mysql" ? "mysqlTable" : "pgTable";
3813
4330
  const importTokens = new Set([
3814
- "pgTable",
4331
+ tableBuilder,
3815
4332
  "primaryKey",
3816
4333
  "text"
3817
4334
  ]);
@@ -3819,8 +4336,8 @@ function createReadModelTableDefinition(readModel) {
3819
4336
  if (field.type === "number" && !isDecimalNumberField(field)) importTokens.add("integer");
3820
4337
  if (isDecimalNumberField(field)) importTokens.add("real");
3821
4338
  if (field.type === "boolean") importTokens.add("boolean");
3822
- if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0) importTokens.add("jsonb");
3823
- return ` ${snakeCase(field.name)}: ${drizzleColumnBuilder(field, snakeCase(field.name))},`;
4339
+ if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0) importTokens.add(persistence === "mysql" ? "json" : "jsonb");
4340
+ return ` ${snakeCase(field.name)}: ${drizzleColumnBuilder(field, snakeCase(field.name), persistence)},`;
3824
4341
  });
3825
4342
  if (readModel.indexes.length > 0) importTokens.add("index");
3826
4343
  if (readModel.uniqueConstraints.length > 0) importTokens.add("uniqueIndex");
@@ -3829,9 +4346,9 @@ function createReadModelTableDefinition(readModel) {
3829
4346
  for (const uniqueConstraint of readModel.uniqueConstraints) constraintLines.push(` ${camelCase(uniqueConstraint.name)}: uniqueIndex("${uniqueConstraint.name}").on(${uniqueConstraint.fields.map((field) => `table.${snakeCase(field)}`).join(", ")}),`);
3830
4347
  const tableConstName = readModelTableConstName(readModel.name);
3831
4348
  const typeName = pascalCase(readModel.name);
3832
- return `import { ${[...importTokens].sort().join(", ")} } from "drizzle-orm/pg-core";
4349
+ return `import { ${[...importTokens].sort().join(", ")} } from "${persistence === "mysql" ? "drizzle-orm/mysql-core" : "drizzle-orm/pg-core"}";
3833
4350
 
3834
- export const ${tableConstName} = pgTable("${readModel.tableName}", {
4351
+ export const ${tableConstName} = ${tableBuilder}("${readModel.tableName}", {
3835
4352
  ${columnLines.join("\n")}
3836
4353
  }, (table) => ({
3837
4354
  ${constraintLines.join("\n")}
@@ -3876,22 +4393,139 @@ function buildReadModelCompositionArtifacts(contextSpecs) {
3876
4393
  };
3877
4394
  return [createGeneratedArtifact("infrastructure/view-models/drizzle/schema.ts", schemaContent, ownership), createGeneratedArtifact("infrastructure/view-models/drizzle/schema.manifest.json", `${JSON.stringify(manifest, null, 2)}\n`, ownership)];
3878
4395
  }
3879
- function buildV5InfrastructureContextArtifacts(contextSpec) {
4396
+ function projectionWriterClassName(projectionName) {
4397
+ const baseName = pascalCase(projectionName);
4398
+ return `MikroOrm${baseName.endsWith("Projection") ? baseName : `${baseName}Projection`}Writer`;
4399
+ }
4400
+ function drizzleProjectionWriterClassName(projectionName) {
4401
+ const baseName = pascalCase(projectionName);
4402
+ return `Drizzle${baseName.endsWith("Projection") ? baseName : `${baseName}Projection`}Writer`;
4403
+ }
4404
+ function projectionWritePortName$1(projectionName) {
4405
+ return `${pascalCase(projectionName)}WritePort`;
4406
+ }
4407
+ function projectionWritePortImportPath(modulePath, projectionName) {
4408
+ return `../../../../../core/contexts/${modulePath}/application/ports/projections/${kebabCase(projectionName)}.projection-write.port.ts`;
4409
+ }
4410
+ function projectionWriterPayloadTypeName(source) {
4411
+ return `${pascalCase(source.eventName)}Payload`;
4412
+ }
4413
+ function projectionWriterPayloadImportPath(source) {
4414
+ return `../../../../../core/contexts/${normalizeModulePath(source.contextName)}/domain/events/${kebabCase(source.eventName)}.event.ts`;
4415
+ }
4416
+ function projectionWriterEventType(source) {
4417
+ const payloadType = projectionWriterPayloadTypeName(source);
4418
+ return `EventEnvelope<${payloadType}> | ${payloadType}`;
4419
+ }
4420
+ function buildProjectionWriterMethod(readModel, source, executorVariable, executorExpression, persistence = "postgres") {
4421
+ const methodName = `on${pascalCase(source.eventName)}`;
4422
+ const eventType = projectionWriterEventType(source);
4423
+ const payloadLine = ` const payload = (((event as any)?.payload ?? event) ?? {}) as Record<string, unknown>;`;
4424
+ const executorLine = ` const ${executorVariable} = ${executorExpression};`;
4425
+ if (source.mutation.kind === "delete") {
4426
+ const whereClause = source.mutation.matchOn.map((field) => `${snakeCase(field)} = ?`).join(" and ");
4427
+ const deleteParams = source.mutation.matchOn.map((field) => `payload.${field}`).join(", ");
4428
+ return ` async ${methodName}(event: ${eventType}, tx: Transaction): Promise<void> {
4429
+ ${payloadLine}
4430
+ ${executorLine}
4431
+ await ${executorVariable}.execute(
4432
+ ${JSON.stringify(`delete from ${readModel.tableName} where ${whereClause}`)},
4433
+ [${deleteParams}],
4434
+ );
4435
+ }`;
4436
+ }
4437
+ const setEntries = Object.entries(source.mutation.set ?? {});
4438
+ const insertColumns = setEntries.map(([fieldName]) => snakeCase(fieldName));
4439
+ const insertParams = setEntries.map(([, mapping]) => buildProjectionMutationValueExpression(mapping));
4440
+ const conflictColumns = source.mutation.matchOn.map((field) => snakeCase(field));
4441
+ const updateAssignments = persistence === "mysql" ? insertColumns.map((column) => `${column} = values(${column})`) : insertColumns.map((column) => `${column} = excluded.${column}`);
4442
+ if (source.mutation.kind === "upsert") {
4443
+ const upsertClause = persistence === "mysql" ? `on duplicate key update ${updateAssignments.join(", ")}` : `on conflict (${conflictColumns.join(", ")}) do update set ${updateAssignments.join(", ")}`;
4444
+ return ` async ${methodName}(event: ${eventType}, tx: Transaction): Promise<void> {
4445
+ ${payloadLine}
4446
+ ${executorLine}
4447
+ await ${executorVariable}.execute(
4448
+ ${JSON.stringify(`insert into ${readModel.tableName} (${insertColumns.join(", ")}) values (${insertColumns.map(() => "?").join(", ")}) ${upsertClause}`)},
4449
+ [${insertParams.join(", ")}],
4450
+ );
4451
+ }`;
4452
+ }
4453
+ const updateAssignmentsSql = insertColumns.map((column) => `${column} = ?`).join(", ");
4454
+ const whereClause = source.mutation.matchOn.map((field) => `${snakeCase(field)} = ?`).join(" and ");
4455
+ const whereParams = source.mutation.matchOn.map((field) => `payload.${field}`);
4456
+ return ` async ${methodName}(event: ${eventType}, tx: Transaction): Promise<void> {
4457
+ ${payloadLine}
4458
+ ${executorLine}
4459
+ await ${executorVariable}.execute(
4460
+ ${JSON.stringify(`update ${readModel.tableName} set ${updateAssignmentsSql} where ${whereClause}`)},
4461
+ [${[...insertParams, ...whereParams].join(", ")}],
4462
+ );
4463
+ }`;
4464
+ }
4465
+ function buildProjectionMutationValueExpression(mapping) {
4466
+ if (mapping.kind === "event-field") return `payload.${mapping.field}`;
4467
+ if (mapping.kind === "literal") return JSON.stringify(mapping.value);
4468
+ return `(${mapping.expression})`;
4469
+ }
4470
+ function createMikroOrmProjectionWriterArtifact(modulePath, projection, readModel, persistence = "postgres") {
4471
+ const className = projectionWriterClassName(projection.name);
4472
+ const writePortName = projectionWritePortName$1(projection.name);
4473
+ const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionWriterPayloadTypeName(source)} } from "${projectionWriterPayloadImportPath(source)}";`);
4474
+ const methodBlocks = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => buildProjectionWriterMethod(readModel, source, "connection", "(tx as any).getConnection()", persistence)).join("\n\n");
4475
+ return createGeneratedArtifact(`infrastructure/persistence/mikroorm/projection-writers/${modulePath}/${kebabCase(projection.name)}.projection-writer.ts`, `import type { EventEnvelope } from "../../../../../lib/event-envelope.ts";
4476
+ import type { Transaction } from "../../../../../lib/transaction.ts";
4477
+ import type { ${writePortName} } from "${projectionWritePortImportPath(modulePath, projection.name)}";
4478
+ ${payloadImports.join("\n")}
4479
+
4480
+ // Generated tx-scoped MikroORM projection writer for "${projection.readModelName}".
4481
+ export class ${className} implements ${writePortName} {
4482
+ ${methodBlocks}
4483
+ }
4484
+ `, sliceArtifactOwnership(modulePath));
4485
+ }
4486
+ function createDrizzleProjectionWriterArtifact(modulePath, projection, readModel, persistence = "postgres") {
4487
+ const className = drizzleProjectionWriterClassName(projection.name);
4488
+ const writePortName = projectionWritePortName$1(projection.name);
4489
+ const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionWriterPayloadTypeName(source)} } from "${projectionWriterPayloadImportPath(source)}";`);
4490
+ const methodBlocks = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => buildProjectionWriterMethod(readModel, source, "executor", "tx as any", persistence)).join("\n\n");
4491
+ return createGeneratedArtifact(`infrastructure/persistence/drizzle/projection-writers/${modulePath}/${kebabCase(projection.name)}.projection-writer.ts`, `import type { EventEnvelope } from "../../../../../lib/event-envelope.ts";
4492
+ import type { Transaction } from "../../../../../lib/transaction.ts";
4493
+ import type { ${writePortName} } from "${projectionWritePortImportPath(modulePath, projection.name)}";
4494
+ ${payloadImports.join("\n")}
4495
+
4496
+ // Generated tx-scoped Drizzle projection writer for "${projection.readModelName}".
4497
+ export class ${className} implements ${writePortName} {
4498
+ ${methodBlocks}
4499
+ }
4500
+ `, sliceArtifactOwnership(modulePath));
4501
+ }
4502
+ function buildV5InfrastructureContextArtifacts(contextSpec, options = {}) {
3880
4503
  const modulePath = normalizeModulePath(contextSpec.context.modulePath);
3881
4504
  const scopeKey = sliceArtifactOwnership(modulePath);
3882
4505
  const artifacts = [];
3883
4506
  const readModels = contextSpec.readModels ?? [];
3884
- const tableContent = createContextDrizzleTableDefinition(contextSpec);
4507
+ const tableContent = createContextDrizzleTableDefinition(contextSpec, resolveDrizzlePersistence(options.infrastructureStrategy));
3885
4508
  if (tableContent !== null) artifacts.push(createGeneratedArtifact(`infrastructure/persistence/${modulePath}/tables.ts`, tableContent, scopeKey));
3886
4509
  if (readModels.length > 0) {
3887
- for (const readModel of readModels.slice().sort((left, right) => left.name.localeCompare(right.name))) artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/drizzle/tables/${readModelRepositoryFileBase$1(readModel.name)}.table.ts`, createReadModelTableDefinition(readModel), scopeKey));
4510
+ for (const readModel of readModels.slice().sort((left, right) => left.name.localeCompare(right.name))) artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/drizzle/tables/${readModelRepositoryFileBase$1(readModel.name)}.table.ts`, createReadModelTableDefinition(readModel, resolveDrizzlePersistence(options.infrastructureStrategy)), scopeKey));
3888
4511
  artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/drizzle/schema.ts`, createReadModelContextSchemaBarrel({
3889
4512
  ...contextSpec,
3890
4513
  readModels
3891
4514
  }), scopeKey));
3892
4515
  artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/tables.ts`, createReadModelCompatibilityBarrel(), scopeKey));
3893
4516
  } else artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/tables.ts`, createViewModelTablesSkeleton(contextSpec), scopeKey));
3894
- for (const { readModelName, queries } of groupQueriesByReadModel(contextSpec.queries)) artifacts.push(createGeneratedArtifact(`infrastructure/view-models/drizzle/repositories/${modulePath}/${readModelRepositoryFileBase$1(readModelName)}.repository.ts`, createReadModelRepositorySkeleton(modulePath, readModelName, queries), scopeKey));
4517
+ for (const { readModelName, queries } of groupQueriesByReadModel(contextSpec.queries)) artifacts.push(createGeneratedArtifact(`infrastructure/view-models/drizzle/repositories/${modulePath}/${readModelRepositoryFileBase$1(readModelName)}.repository.ts`, createReadModelRepositoryContent(modulePath, readModelName, queries, readModels, options.infrastructureStrategy), scopeKey));
4518
+ const projections = contextSpec.projections ?? [];
4519
+ if (options.infrastructureStrategy?.orm === "mikroorm") for (const projection of projections) {
4520
+ const readModel = readModels.find((candidate) => candidate.name === projection.readModelName);
4521
+ if (!readModel) continue;
4522
+ artifacts.push(createMikroOrmProjectionWriterArtifact(modulePath, projection, readModel, resolveDrizzlePersistence(options.infrastructureStrategy)));
4523
+ }
4524
+ else if (options.infrastructureStrategy?.orm === "drizzle") for (const projection of projections) {
4525
+ const readModel = readModels.find((candidate) => candidate.name === projection.readModelName);
4526
+ if (!readModel) throw new Error(`[infrastructure_generator] Projection "${projection.name}" cannot be fully generated because read-model "${projection.readModelName}" was not found.`);
4527
+ artifacts.push(createDrizzleProjectionWriterArtifact(modulePath, projection, readModel, resolveDrizzlePersistence(options.infrastructureStrategy)));
4528
+ }
3895
4529
  return artifacts;
3896
4530
  }
3897
4531
  function createViewModelTablesSkeleton(contextSpec) {
@@ -3906,7 +4540,7 @@ function createViewModelTablesSkeleton(contextSpec) {
3906
4540
  for (const { readModelName, queries } of groupQueriesByReadModel(contextSpec.queries)) {
3907
4541
  lines.push(` ${camelCase(readModelName)}: {`);
3908
4542
  lines.push(` readModel: "${readModelName}",`);
3909
- lines.push(` contract: "${readModelContractName$2(readModelName)}",`);
4543
+ lines.push(` contract: "${readModelContractName$1(readModelName)}",`);
3910
4544
  lines.push(` servesQueries: [${queries.map((query) => `"${query.name}"`).join(", ")}],`);
3911
4545
  lines.push(` suggestedTableName: "${snakeCase(modulePath).replaceAll("/", "_")}_${snakeCase(readModelRepositoryFileBase$1(readModelName))}",`);
3912
4546
  lines.push(` },`);
@@ -3915,40 +4549,33 @@ function createViewModelTablesSkeleton(contextSpec) {
3915
4549
  lines.push(``);
3916
4550
  return lines.join("\n");
3917
4551
  }
4552
+ function createReadModelRepositoryContent(modulePath, readModelName, queries, readModels, infrastructureStrategy) {
4553
+ if (!supportsGeneratedReadModelQueries(infrastructureStrategy)) return createReadModelRepositorySkeleton(modulePath, readModelName, queries);
4554
+ const readModel = readModels.find((candidate) => candidate.name === readModelName);
4555
+ if (!readModel) return createReadModelRepositorySkeleton(modulePath, readModelName, queries);
4556
+ return createSupportedReadModelRepository(modulePath, readModel, queries);
4557
+ }
4558
+ function supportsGeneratedReadModelQueries(infrastructureStrategy) {
4559
+ if (!infrastructureStrategy) return true;
4560
+ return (infrastructureStrategy.architecture === "physical-cqrs" || infrastructureStrategy.architecture === "logical-cqrs") && (infrastructureStrategy.persistence === "postgres" || infrastructureStrategy.persistence === "mysql");
4561
+ }
3918
4562
  function createReadModelRepositorySkeleton(modulePath, readModelName, queries) {
3919
4563
  const className = readModelRepositoryClassName(readModelName);
3920
- const portTypeName = `${readModelContractName$2(readModelName)}RepositoryPort`;
4564
+ const portTypeName = `${readModelContractName$1(readModelName)}RepositoryPort`;
3921
4565
  const fileBase = readModelRepositoryFileBase$1(readModelName);
3922
4566
  const importLines = /* @__PURE__ */ new Map();
3923
4567
  let needsPaginatedResult = false;
3924
4568
  importLines.set("tx", `import type { Transaction } from "../../../../../lib/transaction.ts";`);
3925
4569
  importLines.set("port", `import type { ${portTypeName} } from "../../../../../core/contexts/${modulePath}/application/ports/read-models/${fileBase}.repository.port.ts";`);
3926
4570
  const methodBlocks = queries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((query) => {
3927
- const methodName = queryReadModelMethodName(query);
4571
+ queryReadModelMethodName(query);
3928
4572
  const queryTypeName = `${pascalCase(query.name)}Query`;
3929
- const outputTypeName = queryOutputContractName$1(query);
3930
- const viewFileBase = queryViewFileBase$1(query);
4573
+ const outputTypeName = queryOutputContractName(query);
4574
+ const viewFileBase = queryViewFileBase(query);
3931
4575
  importLines.set(`query:${queryTypeName}`, `import type { ${queryTypeName} } from "../../../../../core/contexts/${modulePath}/application/queries/${kebabCase(query.name)}.query.ts";`);
3932
4576
  importLines.set(`view:${outputTypeName}`, `import type { ${outputTypeName} } from "../../../../../core/contexts/${modulePath}/application/contracts/${viewFileBase}.view.ts";`);
3933
- if (query.queryKind === "list") {
3934
- needsPaginatedResult = true;
3935
- return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<PaginatedResult<${outputTypeName}>> {
3936
- void this.db;
3937
- void tx;
3938
- const { page = 1, pageSize = 20 } = query.payload;
3939
- return { items: [], total: 0, page, pageSize };
3940
- }`;
3941
- }
3942
- return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<${outputTypeName}> {
3943
- void this.db;
3944
- void query;
3945
- void tx;
3946
- return {
3947
- ${query.outputFields.map((field) => {
3948
- return ` ${camelCase(field.name)}: ${placeholderValueForField(field)}, // TODO: map from projection row`;
3949
- }).join("\n")}
3950
- };
3951
- }`;
4577
+ if (query.queryKind === "list") needsPaginatedResult = true;
4578
+ return createSingleReadModelMethodSkeleton(query, readModelName);
3952
4579
  });
3953
4580
  if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "../../../../../lib/pagination.ts";`);
3954
4581
  return `${[...importLines.values()].join("\n")}
@@ -3961,76 +4588,273 @@ ${methodBlocks.join("\n\n")}
3961
4588
  }
3962
4589
  `;
3963
4590
  }
4591
+ function createSupportedReadModelRepository(modulePath, readModel, queries) {
4592
+ const className = readModelRepositoryClassName(readModel.name);
4593
+ const portTypeName = `${readModelContractName$1(readModel.name)}RepositoryPort`;
4594
+ const fileBase = readModelRepositoryFileBase$1(readModel.name);
4595
+ const importLines = /* @__PURE__ */ new Map();
4596
+ const drizzleImports = /* @__PURE__ */ new Set();
4597
+ let needsPaginatedResult = false;
4598
+ let needsSqlType = false;
4599
+ let needsTableImport = false;
4600
+ importLines.set("tx", `import type { Transaction } from "../../../../../lib/transaction.ts";`);
4601
+ importLines.set("port", `import type { ${portTypeName} } from "../../../../../core/contexts/${modulePath}/application/ports/read-models/${fileBase}.repository.port.ts";`);
4602
+ const methodBlocks = queries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((query) => {
4603
+ const queryTypeName = `${pascalCase(query.name)}Query`;
4604
+ const outputTypeName = queryOutputContractName(query);
4605
+ const viewFileBase = queryViewFileBase(query);
4606
+ importLines.set(`query:${queryTypeName}`, `import type { ${queryTypeName} } from "../../../../../core/contexts/${modulePath}/application/queries/${kebabCase(query.name)}.query.ts";`);
4607
+ importLines.set(`view:${outputTypeName}`, `import type { ${outputTypeName} } from "../../../../../core/contexts/${modulePath}/application/contracts/${viewFileBase}.view.ts";`);
4608
+ const generatedMethod = createGeneratedReadModelMethod(readModel, query, drizzleImports);
4609
+ if (generatedMethod !== null) {
4610
+ needsTableImport = true;
4611
+ if (query.queryKind === "list") {
4612
+ needsPaginatedResult = true;
4613
+ needsSqlType = needsSqlType || generatedMethod.includes("SQL[]");
4614
+ }
4615
+ return generatedMethod;
4616
+ }
4617
+ if (query.queryKind === "list") needsPaginatedResult = true;
4618
+ return createSingleReadModelMethodSkeleton(query, readModel.name);
4619
+ });
4620
+ if (needsTableImport) importLines.set("table", `import { ${readModelTableConstName(readModel.name)} } from "../../../${modulePath}/drizzle/tables/${fileBase}.table.ts";`);
4621
+ if (drizzleImports.size > 0) importLines.set("drizzle", `import { ${[...drizzleImports].sort().join(", ")} } from "drizzle-orm";`);
4622
+ if (needsSqlType) importLines.set("sql", `import type { SQL } from "drizzle-orm";`);
4623
+ if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "../../../../../lib/pagination.ts";`);
4624
+ return `${[...importLines.values()].join("\n")}
3964
4625
 
3965
- //#endregion
3966
- //#region packages/core/generators/projections.ts
3967
- function readModelContractName$1(readModelName) {
3968
- const baseName = pascalCase(readModelName);
3969
- return baseName.endsWith("View") ? baseName : `${baseName}View`;
3970
- }
3971
- function readModelRepositoryPortName(readModelName) {
3972
- return `${readModelContractName$1(readModelName)}RepositoryPort`;
4626
+ // Generated read-model repository for the supported infrastructure strategy.
4627
+ export class ${className} implements ${portTypeName} {
4628
+ constructor(private readonly db: unknown) {}
4629
+
4630
+ ${methodBlocks.join("\n\n")}
3973
4631
  }
3974
- function readModelRepositoryPortFileBase(readModelName) {
3975
- return kebabCase(readModelName).replace(/-view$/, "");
4632
+ `;
3976
4633
  }
3977
- function projectionFileBase(projectionName) {
3978
- return kebabCase(projectionName);
4634
+ function createGeneratedReadModelMethod(readModel, query, drizzleImports) {
4635
+ if (!canMapReadModelOutput(readModel, query.outputFields)) return null;
4636
+ if (query.queryKind === "findById") {
4637
+ const lookupField = resolveFindByIdLookupField(readModel, query);
4638
+ if (!lookupField) return null;
4639
+ drizzleImports.add("eq");
4640
+ return createFindByIdReadModelMethod(readModel, query, lookupField);
4641
+ }
4642
+ drizzleImports.add("asc");
4643
+ drizzleImports.add("count");
4644
+ drizzleImports.add("desc");
4645
+ const filterData = buildReadModelFilterData(readModel, query, readModelTableConstName(readModel.name));
4646
+ if (filterData.lines.length > 0) drizzleImports.add("and");
4647
+ for (const drizzleImport of filterData.imports) drizzleImports.add(drizzleImport);
4648
+ return createListReadModelMethod(readModel, query, filterData.lines);
4649
+ }
4650
+ function canMapReadModelOutput(readModel, outputFields) {
4651
+ const readModelFields = new Set(readModel.fields.map((field) => camelCase(field.name)));
4652
+ return outputFields.every((field) => readModelFields.has(camelCase(field.name)));
4653
+ }
4654
+ function resolveFindByIdLookupField(readModel, query) {
4655
+ if (readModel.primaryKey.length !== 1) return null;
4656
+ const primaryKeyField = camelCase(readModel.primaryKey[0]);
4657
+ const matchingField = query.inputFields.find((field) => camelCase(field.name) === primaryKeyField);
4658
+ if (matchingField) return camelCase(matchingField.name);
4659
+ if (query.inputFields.length === 1) return camelCase(query.inputFields[0].name);
4660
+ return null;
3979
4661
  }
3980
- function projectionVariableName(projectionName) {
3981
- return camelCase(projectionName);
4662
+ function createFindByIdReadModelMethod(readModel, query, lookupField) {
4663
+ const methodName = queryReadModelMethodName(query);
4664
+ const queryTypeName = `${pascalCase(query.name)}Query`;
4665
+ const outputTypeName = queryOutputContractName(query);
4666
+ const tableConstName = readModelTableConstName(readModel.name);
4667
+ return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<${outputTypeName}> {
4668
+ void this.db;
4669
+ const rows = await tx.select()
4670
+ .from(${tableConstName})
4671
+ .where(eq(${tableConstName}.${snakeCase(readModel.primaryKey[0])}, query.payload.${lookupField}))
4672
+ .limit(1);
4673
+
4674
+ const row = rows[0];
4675
+ if (!row) {
4676
+ throw new Error("${outputTypeName} row not found");
4677
+ }
4678
+
4679
+ return {
4680
+ ${buildReadModelOutputMapping(query.outputFields, "row")}
4681
+ };
4682
+ }`;
4683
+ }
4684
+ function createListReadModelMethod(readModel, query, filterLines) {
4685
+ const methodName = queryReadModelMethodName(query);
4686
+ const queryTypeName = `${pascalCase(query.name)}Query`;
4687
+ const outputTypeName = queryOutputContractName(query);
4688
+ const tableConstName = readModelTableConstName(readModel.name);
4689
+ const payloadDestructure = filterLines.length > 0 ? `page = 1, pageSize = 20, filters, sortBy, sortDirection` : `page = 1, pageSize = 20, sortBy, sortDirection`;
4690
+ const whereBlock = filterLines.length > 0 ? ` const conditions: SQL[] = [];
4691
+ ${filterLines.join("\n")}
4692
+
4693
+ const where = conditions.length > 0 ? and(...conditions) : undefined;` : ` const where = undefined;`;
4694
+ const sortFieldEntries = query.sorting.sortableFields.filter((field) => readModel.fields.some((candidate) => camelCase(candidate.name) === camelCase(field))).map((field) => ` ${camelCase(field)}: ${tableConstName}.${snakeCase(field)},`);
4695
+ const defaultSortField = camelCase(query.sorting.defaultSort.field);
4696
+ return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<PaginatedResult<${outputTypeName}>> {
4697
+ void this.db;
4698
+ const { ${payloadDestructure} } = query.payload;
4699
+
4700
+ ${whereBlock}
4701
+ const sortColumn = (({
4702
+ ${sortFieldEntries.join("\n")}
4703
+ } as Record<string, unknown>)[sortBy ?? "${defaultSortField}"] ?? ${tableConstName}.${snakeCase(query.sorting.defaultSort.field)}) as import("drizzle-orm").AnyColumn;
4704
+ const orderBy = sortDirection === "desc"
4705
+ ? [desc(sortColumn)]
4706
+ : [asc(sortColumn)];
4707
+
4708
+ const [rows, countResult] = await Promise.all([
4709
+ tx.select()
4710
+ .from(${tableConstName})
4711
+ .where(where)
4712
+ .orderBy(...orderBy)
4713
+ .limit(pageSize)
4714
+ .offset((page - 1) * pageSize),
4715
+ tx.select({ count: count() }).from(${tableConstName}).where(where),
4716
+ ]);
4717
+
4718
+ return {
4719
+ items: rows.map((row) => ({
4720
+ ${buildReadModelOutputMapping(query.outputFields, "row")}
4721
+ })),
4722
+ total: Number(countResult[0]?.count ?? 0),
4723
+ page,
4724
+ pageSize,
4725
+ };
4726
+ }`;
4727
+ }
4728
+ function buildReadModelFilterData(readModel, query, tableConstName) {
4729
+ const readModelFields = new Set(readModel.fields.map((field) => camelCase(field.name)));
4730
+ const lines = [];
4731
+ const imports = /* @__PURE__ */ new Set();
4732
+ for (const [field, operators] of Object.entries(query.filters)) {
4733
+ const normalizedField = camelCase(field);
4734
+ if (!readModelFields.has(normalizedField)) continue;
4735
+ const columnRef = `${tableConstName}.${snakeCase(field)}`;
4736
+ for (const operator of operators) switch (operator) {
4737
+ case "eq":
4738
+ imports.add("eq");
4739
+ lines.push(` if (filters?.${normalizedField}?.eq != null) conditions.push(eq(${columnRef}, filters.${normalizedField}.eq));`);
4740
+ break;
4741
+ case "in":
4742
+ imports.add("inArray");
4743
+ lines.push(` if (filters?.${normalizedField}?.in) conditions.push(inArray(${columnRef}, filters.${normalizedField}.in));`);
4744
+ break;
4745
+ case "gt":
4746
+ imports.add("gt");
4747
+ lines.push(` if (filters?.${normalizedField}?.gt != null) conditions.push(gt(${columnRef}, filters.${normalizedField}.gt));`);
4748
+ break;
4749
+ case "gte":
4750
+ imports.add("gte");
4751
+ lines.push(` if (filters?.${normalizedField}?.gte != null) conditions.push(gte(${columnRef}, filters.${normalizedField}.gte));`);
4752
+ break;
4753
+ case "lt":
4754
+ imports.add("lt");
4755
+ lines.push(` if (filters?.${normalizedField}?.lt != null) conditions.push(lt(${columnRef}, filters.${normalizedField}.lt));`);
4756
+ break;
4757
+ case "lte":
4758
+ imports.add("lte");
4759
+ lines.push(` if (filters?.${normalizedField}?.lte != null) conditions.push(lte(${columnRef}, filters.${normalizedField}.lte));`);
4760
+ break;
4761
+ case "contains":
4762
+ imports.add("like");
4763
+ lines.push(` if (filters?.${normalizedField}?.contains) conditions.push(like(${columnRef}, \`%\${filters.${normalizedField}.contains}%\`));`);
4764
+ break;
4765
+ case "startsWith":
4766
+ imports.add("like");
4767
+ lines.push(` if (filters?.${normalizedField}?.startsWith) conditions.push(like(${columnRef}, \`\${filters.${normalizedField}.startsWith}%\`));`);
4768
+ break;
4769
+ }
4770
+ }
4771
+ return {
4772
+ lines,
4773
+ imports
4774
+ };
4775
+ }
4776
+ function buildReadModelOutputMapping(outputFields, rowVar) {
4777
+ return outputFields.map((field) => {
4778
+ const outputField = camelCase(field.name);
4779
+ const rowField = snakeCase(field.name);
4780
+ return ` ${outputField}: ${field.optional ? `${rowVar}.${rowField} ?? undefined` : `${rowVar}.${rowField}`},`;
4781
+ }).join("\n");
4782
+ }
4783
+ function createSingleReadModelMethodSkeleton(query, readModelName) {
4784
+ const methodName = queryReadModelMethodName(query);
4785
+ const queryTypeName = `${pascalCase(query.name)}Query`;
4786
+ const outputTypeName = queryOutputContractName(query);
4787
+ const targetName = readModelName ?? resolvedReadModelName$1(query);
4788
+ if (query.queryKind === "list") return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<PaginatedResult<${outputTypeName}>> {
4789
+ void this.db;
4790
+ void tx;
4791
+ throw new Error(
4792
+ "[infrastructure_generator] Query \\"${query.name}\\" for read-model \\"${targetName}\\" cannot be fully generated. " +
4793
+ "Ensure output fields map directly to read-model fields and list-query metadata is fully declared.",
4794
+ );
4795
+ }`;
4796
+ return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<${outputTypeName}> {
4797
+ void this.db;
4798
+ void query;
4799
+ void tx;
4800
+ throw new Error(
4801
+ "[infrastructure_generator] Query \\"${query.name}\\" for read-model \\"${targetName}\\" cannot be fully generated. " +
4802
+ "Ensure output fields map directly to read-model fields and findById queries resolve a stable lookup field.",
4803
+ );
4804
+ }`;
4805
+ }
4806
+
4807
+ //#endregion
4808
+ //#region packages/core/generators/projections.ts
4809
+ function projectionWritePortName(projectionName) {
4810
+ return `${pascalCase(projectionName)}WritePort`;
4811
+ }
4812
+ function projectionWritePortVariableName(projectionName) {
4813
+ return camelCase(projectionWritePortName(projectionName));
3982
4814
  }
3983
- function readModelRepositoryVariableName(readModelName) {
3984
- return camelCase(readModelRepositoryPortName(readModelName));
4815
+ function projectionFileBase(projectionName) {
4816
+ return kebabCase(projectionName);
4817
+ }
4818
+ function projectionVariableName(projectionName) {
4819
+ return camelCase(projectionName);
3985
4820
  }
3986
4821
  function projectionSourceHandlerName(eventName) {
3987
4822
  return `on${pascalCase(eventName)}`;
3988
4823
  }
4824
+ function projectionSourcePayloadTypeName(source) {
4825
+ return `${pascalCase(source.eventName)}Payload`;
4826
+ }
4827
+ function projectionSourcePayloadImportPath(currentModulePath, source) {
4828
+ return `../../${normalizeModulePath(source.contextName)}/domain/events/${kebabCase(source.eventName)}.event.ts`;
4829
+ }
4830
+ function projectionSourceEventType(source) {
4831
+ const payloadType = projectionSourcePayloadTypeName(source);
4832
+ return `EventEnvelope<${payloadType}> | ${payloadType}`;
4833
+ }
3989
4834
  function buildProjectionSourceMethod(projection, source) {
3990
4835
  const methodName = projectionSourceHandlerName(source.eventName);
3991
4836
  const roleComment = source.role === "primary" ? `// Primary source: this event defines row lifecycle for the "${projection.readModelName}" read model.` : `// Enrichment source: update existing rows only; this event must not redefine ownership.`;
3992
- if (source.mutation.kind === "custom") return ` async ${methodName}(event: unknown, tx: Transaction): Promise<void> {
3993
- return await this.${source.mutation.handlerName}(event, tx);
3994
- }
3995
-
3996
- protected async ${source.mutation.handlerName}(event: unknown, tx: Transaction): Promise<void> {
3997
- void this.${readModelRepositoryVariableName(projection.readModelName)};
3998
- void event;
3999
- void tx;
4000
- ${roleComment}
4001
- // Custom projection hook "${source.mutation.handlerName}" for ${source.contextName}.${source.aggregateName}.${source.eventName}.
4002
- // TODO: implement custom projector logic against the generated read-model repository port.
4003
- }`;
4004
- const setFields = source.mutation.kind === "delete" ? "" : Object.entries(source.mutation.set ?? {}).map(([fieldName, valueSource]) => {
4005
- if (valueSource.kind === "event-field") return ` // ${fieldName} <- event.${valueSource.field}`;
4006
- if (valueSource.kind === "literal") return ` // ${fieldName} <- ${JSON.stringify(valueSource.value)}`;
4007
- return ` // ${fieldName} <- ${valueSource.expression}`;
4008
- }).join("\n");
4009
- const matchOnComment = `"${source.mutation.matchOn.join("\", \"")}"`;
4010
- return ` async ${methodName}(event: unknown, tx: Transaction): Promise<void> {
4011
- void this.${readModelRepositoryVariableName(projection.readModelName)};
4012
- void event;
4013
- void tx;
4837
+ return ` async ${methodName}(event: ${projectionSourceEventType(source)}, tx: Transaction): Promise<void> {
4014
4838
  ${roleComment}
4015
- // Mutation kind: ${source.mutation.kind}
4016
- // Match on: [${matchOnComment}]
4017
- ${setFields}
4018
- // TODO: call generated projector-facing repository methods when write helpers land.
4839
+ return await this.${projectionWritePortVariableName(projection.name)}.${methodName}(event, tx);
4019
4840
  }`;
4020
4841
  }
4021
4842
  function buildProjectorArtifact(modulePath, projection) {
4022
4843
  const projectorClassName = projection.name;
4023
- const repositoryPortName = readModelRepositoryPortName(projection.readModelName);
4024
- const repositoryPortVar = readModelRepositoryVariableName(projection.readModelName);
4844
+ const writePortName = projectionWritePortName(projection.name);
4845
+ const writePortVar = projectionWritePortVariableName(projection.name);
4025
4846
  const fileBase = projectionFileBase(projection.name);
4847
+ const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionSourcePayloadTypeName(source)} } from "${projectionSourcePayloadImportPath(modulePath, source)}";`);
4026
4848
  const methods = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => buildProjectionSourceMethod(projection, source)).join("\n\n");
4027
- return createGeneratedArtifact(`core/contexts/${modulePath}/application/projections/${fileBase}.projector.ts`, `import type { Transaction } from "../../../../lib/transaction.ts";
4028
- import type { ${repositoryPortName} } from "../ports/read-models/${readModelRepositoryPortFileBase(projection.readModelName)}.repository.port.ts";
4849
+ return createGeneratedArtifact(`core/contexts/${modulePath}/application/projections/${fileBase}.projector.ts`, `import type { EventEnvelope } from "../../../../../lib/event-envelope.ts";
4850
+ import type { Transaction } from "../../../../../lib/transaction.ts";
4851
+ import type { ${writePortName} } from "../ports/projections/${projectionFileBase(projection.name)}.projection-write.port.ts";
4852
+ ${payloadImports.join("\n")}
4029
4853
 
4030
4854
  // Auto-generated projector skeleton for the "${projection.readModelName}" read model.
4031
4855
  export class ${projectorClassName} {
4032
4856
  constructor(
4033
- private readonly ${repositoryPortVar}: ${repositoryPortName},
4857
+ private readonly ${writePortVar}: ${writePortName},
4034
4858
  ) {}
4035
4859
 
4036
4860
  ${methods}
@@ -4043,20 +4867,53 @@ function buildProjectionRebuildArtifact(modulePath, projection) {
4043
4867
  const projectorClassName = projection.name;
4044
4868
  const rebuildFunctionName = `rebuild${pascalCase(projection.name)}`;
4045
4869
  const batchSize = projection.rebuild.batchSize ?? 500;
4046
- return createGeneratedArtifact(`core/contexts/${modulePath}/application/projections/${fileBase}.rebuild.ts`, `import type { Transaction } from "../../../../lib/transaction.ts";
4870
+ return createGeneratedArtifact(`core/contexts/${modulePath}/application/projections/${fileBase}.rebuild.ts`, `import type { Transaction } from "../../../../../lib/transaction.ts";
4047
4871
  import type { ${projectorClassName} } from "./${fileBase}.projector.ts";
4048
4872
 
4873
+ export type ProjectionRebuildBatch = {
4874
+ readonly events: Array<{
4875
+ readonly type: string;
4876
+ readonly payload?: unknown;
4877
+ }>;
4878
+ };
4879
+
4880
+ export interface ProjectionRebuildEventSource {
4881
+ stream(args: {
4882
+ projectionName: string;
4883
+ strategy: string;
4884
+ batchSize: number;
4885
+ tx: Transaction;
4886
+ }): AsyncIterable<ProjectionRebuildBatch>;
4887
+ }
4888
+
4049
4889
  export type ${pascalCase(projection.name)}RebuildDeps = {
4050
4890
  projector: ${projectorClassName};
4891
+ eventSource: ProjectionRebuildEventSource;
4051
4892
  transaction: Transaction;
4052
4893
  };
4053
4894
 
4054
- // Auto-generated rebuild scaffold for the "${projection.readModelName}" read model.
4895
+ // Auto-generated rebuild runner for the "${projection.readModelName}" read model.
4055
4896
  export async function ${rebuildFunctionName}(deps: ${pascalCase(projection.name)}RebuildDeps): Promise<void> {
4056
- void deps;
4057
- // Strategy: ${projection.rebuild.strategy}
4058
- // Batch size: ${batchSize}
4059
- // TODO: replay authoritative events and route them through the projector in stable batches.
4897
+ const batchSize = ${batchSize};
4898
+
4899
+ for await (
4900
+ const batch of deps.eventSource.stream({
4901
+ projectionName: "${projection.name}",
4902
+ strategy: "${projection.rebuild.strategy}",
4903
+ batchSize,
4904
+ tx: deps.transaction,
4905
+ })
4906
+ ) {
4907
+ for (const event of batch.events) {
4908
+ switch (event.type) {
4909
+ ${projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => ` case "${source.contextName}.${source.aggregateName}.${source.eventName}":
4910
+ await deps.projector.${projectionSourceHandlerName(source.eventName)}(event, deps.transaction);
4911
+ break;`).join("\n")}
4912
+ default:
4913
+ break;
4914
+ }
4915
+ }
4916
+ }
4060
4917
  }
4061
4918
  `, contextArtifactOwnership(modulePath));
4062
4919
  }
@@ -4074,7 +4931,8 @@ function buildProjectionSubscriptionsArtifact(contextSpec) {
4074
4931
  const depsTypeName = `${pascalCase(contextSpec.context.name)}ProjectionSubscriptionDeps`;
4075
4932
  const builderName = `build${pascalCase(contextSpec.context.name)}ProjectionSubscriptions`;
4076
4933
  const dependencyLines = contextSpec.projections.map((projection) => ` ${projectionVariableName(projection.name)}: ${projection.name};`);
4077
- const importLines = contextSpec.projections.map((projection) => `import type { ${projection.name} } from "../../core/contexts/${modulePath}/application/projections/${projectionFileBase(projection.name)}.projector.ts";`);
4934
+ const importLines = contextSpec.projections.map((projection) => `import type { ${projection.name} } from "../../../core/contexts/${modulePath}/application/projections/${projectionFileBase(projection.name)}.projector.ts";`);
4935
+ const eventImports = contextSpec.projections.flatMap((projection) => projection.sources.map((source) => `import type { ${projectionSourcePayloadTypeName(source)} } from "../../../core/contexts/${normalizeModulePath(source.contextName)}/domain/events/${kebabCase(source.eventName)}.event.ts";`));
4078
4936
  const subscriptionEntries = contextSpec.projections.flatMap((projection) => projection.sources.map((source) => {
4079
4937
  const subscriptionName = projection.subscription?.subscriptionName ?? projection.name;
4080
4938
  const consumerGroup = projection.subscription?.consumerGroup;
@@ -4084,11 +4942,13 @@ function buildProjectionSubscriptionsArtifact(contextSpec) {
4084
4942
  subscriptionName: "${subscriptionName}",
4085
4943
  eventName: "${source.contextName}.${source.aggregateName}.${source.eventName}",
4086
4944
  role: "${source.role}",${consumerGroupLine}
4087
- handle: (event, tx) => deps.${projectionVariableName(projection.name)}.${projectionSourceHandlerName(source.eventName)}(event, tx),
4945
+ handle: (event: ${projectionSourceEventType(source)}, tx) => deps.${projectionVariableName(projection.name)}.${projectionSourceHandlerName(source.eventName)}(event, tx),
4088
4946
  }`;
4089
4947
  }));
4090
- return createGeneratedArtifact(`infrastructure/messaging/projection-subscriptions/${modulePath}.ts`, `import type { Transaction } from "../../lib/transaction.ts";
4948
+ return createGeneratedArtifact(`infrastructure/messaging/projection-subscriptions/${modulePath}.ts`, `import type { EventEnvelope } from "../../../lib/event-envelope.ts";
4949
+ import type { Transaction } from "../../../lib/transaction.ts";
4091
4950
  ${importLines.join("\n")}
4951
+ ${[...new Set(eventImports)].join("\n")}
4092
4952
 
4093
4953
  export type ProjectionSubscription = {
4094
4954
  projectionName: string;
@@ -4096,7 +4956,7 @@ export type ProjectionSubscription = {
4096
4956
  eventName: string;
4097
4957
  role: "primary" | "enrichment";
4098
4958
  consumerGroup?: string;
4099
- handle: (event: unknown, tx: Transaction) => Promise<void>;
4959
+ handle: (event: EventEnvelope<unknown>, tx: Transaction) => Promise<void>;
4100
4960
  };
4101
4961
 
4102
4962
  export type ${depsTypeName} = {
@@ -4372,6 +5232,34 @@ export class EventCollector {
4372
5232
  }
4373
5233
  `;
4374
5234
  }
5235
+ function generateAggregateEventTracker() {
5236
+ return `import type { EventEnvelope } from "../../../lib/event-envelope.ts";
5237
+ import { EventCollector } from "./event-collector.ts";
5238
+
5239
+ type EventSourceAggregate = {
5240
+ pullDomainEvents(): EventEnvelope<unknown>[];
5241
+ };
5242
+
5243
+ export class AggregateEventTracker {
5244
+ private readonly tracked = new Set<EventSourceAggregate>();
5245
+
5246
+ track(aggregate: EventSourceAggregate): void {
5247
+ this.tracked.add(aggregate);
5248
+ }
5249
+
5250
+ releaseInto(eventCollector: EventCollector): void {
5251
+ for (const aggregate of this.tracked) {
5252
+ eventCollector.collect(aggregate.pullDomainEvents());
5253
+ }
5254
+ this.tracked.clear();
5255
+ }
5256
+
5257
+ reset(): void {
5258
+ this.tracked.clear();
5259
+ }
5260
+ }
5261
+ `;
5262
+ }
4375
5263
  function generateDomainErrors() {
4376
5264
  return `export type { DomainError } from '../../../lib/domain-error.ts';
4377
5265
  export type { ApplicationError } from '../../../lib/application-error.ts';
@@ -4452,6 +5340,7 @@ function buildV5SharedKernelArtifacts(spec) {
4452
5340
  const base = [
4453
5341
  createGeneratedArtifact("core/shared-kernel/result.ts", generateResultReExport(), ownership),
4454
5342
  createGeneratedArtifact("core/shared-kernel/events/event-collector.ts", generateEventCollector(), ownership),
5343
+ createGeneratedArtifact("core/shared-kernel/events/aggregate-event-tracker.ts", generateAggregateEventTracker(), ownership),
4455
5344
  createGeneratedArtifact("core/shared-kernel/events/event-context.ts", generateEventContext(), ownership),
4456
5345
  createGeneratedArtifact("core/shared-kernel/errors/domain-errors.ts", generateDomainErrors(), ownership),
4457
5346
  createGeneratedArtifact("core/shared-kernel/trpc-error-mapper.ts", generateTrpcErrorMapper(), ownership),
@@ -4910,7 +5799,11 @@ function createApplicationError() {
4910
5799
  }
4911
5800
  `;
4912
5801
  }
4913
- function createTransaction() {
5802
+ function createTransaction(infrastructureStrategy) {
5803
+ if (infrastructureStrategy?.persistence === "mysql") return `import type { MySqlTransaction } from "drizzle-orm/mysql-core";
5804
+
5805
+ export type Transaction = MySqlTransaction<any, any, any, any>;
5806
+ `;
4914
5807
  return `import type { PgTransaction } from 'drizzle-orm/pg-core';
4915
5808
 
4916
5809
  export type Transaction = PgTransaction<any, any, any>;
@@ -5004,7 +5897,7 @@ export type ListResult<T> = {
5004
5897
  };
5005
5898
  `;
5006
5899
  }
5007
- function buildV5LibArtifacts() {
5900
+ function buildV5LibArtifacts(infrastructureStrategy) {
5008
5901
  const ownership = sharedArtifactOwnership("lib");
5009
5902
  return [
5010
5903
  createGeneratedArtifact("lib/branded-id.ts", createBrandedId(), ownership),
@@ -5016,7 +5909,7 @@ function buildV5LibArtifacts() {
5016
5909
  createGeneratedArtifact("lib/result.ts", createResult(), ownership),
5017
5910
  createGeneratedArtifact("lib/domain-error.ts", createDomainError(), ownership),
5018
5911
  createGeneratedArtifact("lib/application-error.ts", createApplicationError(), ownership),
5019
- createGeneratedArtifact("lib/transaction.ts", createTransaction(), ownership),
5912
+ createGeneratedArtifact("lib/transaction.ts", createTransaction(infrastructureStrategy), ownership),
5020
5913
  createGeneratedArtifact("lib/concurrency-conflict-error.ts", createConcurrencyConflictError(), ownership),
5021
5914
  createGeneratedArtifact("lib/pagination.ts", createPagination(), ownership)
5022
5915
  ];
@@ -5126,26 +6019,29 @@ function buildConsumerAclArtifacts(contextSpec) {
5126
6019
 
5127
6020
  //#endregion
5128
6021
  //#region packages/core/orchestrator.ts
5129
- function generateContextArtifacts(contextSpec) {
6022
+ function generateContextArtifacts(contextSpec, options = {}) {
5130
6023
  const perAggregateArtifacts = sliceContextIntoAggregateViews(contextSpec).flatMap((view) => {
5131
6024
  const ownership = sliceArtifactOwnership(`${view.context.modulePath}/${kebabCase(view.aggregate.name)}`);
5132
6025
  return [
5133
6026
  ...applyOwnershipIfMissing(buildV5DomainArtifacts(view), ownership),
5134
6027
  ...applyOwnershipIfMissing(buildV5ApplicationArtifacts(view, { skipContextWideArtifacts: true }), ownership),
5135
- ...buildV5InfrastructureArtifacts(view, { skipContextWideArtifacts: true }),
6028
+ ...buildV5InfrastructureArtifacts(view, {
6029
+ skipContextWideArtifacts: true,
6030
+ infrastructureStrategy: options.infrastructureStrategy
6031
+ }),
5136
6032
  ...applyOwnershipIfMissing(buildV5TestArtifacts(view), ownership)
5137
6033
  ];
5138
6034
  });
5139
6035
  const ctxOwnership = contextArtifactOwnership(contextSpec.context.modulePath);
5140
6036
  const infraOwnership = sliceArtifactOwnership(contextSpec.context.modulePath);
5141
6037
  const perContextArtifacts = [
5142
- ...buildV5LibArtifacts(),
6038
+ ...buildV5LibArtifacts(options.infrastructureStrategy),
5143
6039
  ...buildV5SharedKernelArtifacts(contextSpec),
5144
6040
  ...buildV5PortArtifacts(),
5145
6041
  ...applyOwnershipIfMissing(buildV5ApplicationContextArtifacts(contextSpec), ctxOwnership),
5146
6042
  ...applyOwnershipIfMissing(buildConsumerAclArtifacts(contextSpec), ctxOwnership),
5147
6043
  ...applyOwnershipIfMissing(buildProjectionArtifacts(contextSpec), ctxOwnership),
5148
- ...applyOwnershipIfMissing(buildV5InfrastructureContextArtifacts(contextSpec), infraOwnership),
6044
+ ...applyOwnershipIfMissing(buildV5InfrastructureContextArtifacts(contextSpec, { infrastructureStrategy: options.infrastructureStrategy }), infraOwnership),
5149
6045
  ...applyOwnershipIfMissing(buildV5PresentationArtifacts(contextSpec), ctxOwnership),
5150
6046
  ...applyOwnershipIfMissing(buildV5RouteArtifacts(contextSpec), ctxOwnership)
5151
6047
  ];
@@ -5752,6 +6648,7 @@ function createIntegrationEvent() {
5752
6648
 
5753
6649
  export interface IntegrationEvent<TPayload> {
5754
6650
  readonly type: string;
6651
+ readonly version: number;
5755
6652
  readonly payload: TPayload;
5756
6653
  readonly eventContext: EventContext;
5757
6654
  }
@@ -5787,6 +6684,20 @@ export class DrizzleTransactionManager implements TransactionManager {
5787
6684
  }
5788
6685
  `;
5789
6686
  }
6687
+ function createMikroOrmTransactionManager() {
6688
+ return `import type { TransactionManager } from "./transaction-manager.ts";
6689
+
6690
+ export class MikroOrmTransactionManager implements TransactionManager {
6691
+ constructor(private readonly em: unknown) {}
6692
+
6693
+ async withTransaction<T>(work: (tx: unknown) => Promise<T>): Promise<T> {
6694
+ return (this.em as any).transactional(async (tx: unknown) => {
6695
+ return work(tx);
6696
+ });
6697
+ }
6698
+ }
6699
+ `;
6700
+ }
5790
6701
  function createTransactionAbortError() {
5791
6702
  return `export class TransactionAbortError extends Error {
5792
6703
  constructor(public readonly innerError: unknown) {
@@ -5797,11 +6708,13 @@ function createTransactionAbortError() {
5797
6708
  `;
5798
6709
  }
5799
6710
  function createHandlerDeps() {
5800
- return `import type { EventCollector } from "../../core/shared-kernel/events/event-collector.ts";
6711
+ return `import type { AggregateEventTracker } from "../../core/shared-kernel/events/aggregate-event-tracker.ts";
6712
+ import type { EventCollector } from "../../core/shared-kernel/events/event-collector.ts";
5801
6713
 
5802
6714
  export type HandlerDeps = {
5803
6715
  readonly tx: unknown;
5804
6716
  readonly eventCollector: EventCollector;
6717
+ readonly aggregateEventTracker: AggregateEventTracker;
5805
6718
  readonly repos: Record<string, unknown>;
5806
6719
  readonly acls: Record<string, unknown>;
5807
6720
  };
@@ -5813,6 +6726,7 @@ function buildCompositionTypeArtifacts() {
5813
6726
  createGeneratedArtifact("infrastructure/messaging/integration-event.ts", createIntegrationEvent(), ownership),
5814
6727
  createGeneratedArtifact("infrastructure/unit-of-work/transaction-manager.ts", createTransactionManager(), ownership),
5815
6728
  createGeneratedArtifact("infrastructure/unit-of-work/drizzle-transaction-manager.ts", createDrizzleTransactionManager(), ownership),
6729
+ createGeneratedArtifact("infrastructure/unit-of-work/mikroorm-transaction-manager.ts", createMikroOrmTransactionManager(), ownership),
5816
6730
  createGeneratedArtifact("infrastructure/unit-of-work/transaction-abort-error.ts", createTransactionAbortError(), ownership),
5817
6731
  createGeneratedArtifact("infrastructure/unit-of-work/handler-deps.ts", createHandlerDeps(), ownership)
5818
6732
  ].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
@@ -5825,6 +6739,7 @@ function createCommandBusImpl() {
5825
6739
  import type { TransactionManager } from "../unit-of-work/transaction-manager.ts";
5826
6740
  import { TransactionAbortError } from "../unit-of-work/transaction-abort-error.ts";
5827
6741
  import type { HandlerDeps } from "../unit-of-work/handler-deps.ts";
6742
+ import { AggregateEventTracker } from "../../core/shared-kernel/events/aggregate-event-tracker.ts";
5828
6743
  import { EventCollector } from "../../core/shared-kernel/events/event-collector.ts";
5829
6744
  import type { CommandEnvelope } from "../../core/shared-kernel/commands/command-envelope.ts";
5830
6745
  import type { EventContext } from "../../core/shared-kernel/events/event-context.ts";
@@ -5836,6 +6751,9 @@ import type { EventEnvelope } from "../../lib/event-envelope.ts";
5836
6751
  type OutboxWriter = {
5837
6752
  append(tx: unknown, events: EventEnvelope<unknown>[]): Promise<void>;
5838
6753
  };
6754
+ type OutboxRelay = {
6755
+ afterCommit(): void;
6756
+ };
5839
6757
  type RepositoryFactory = {
5840
6758
  create(tx: unknown): Record<string, unknown>;
5841
6759
  };
@@ -5855,6 +6773,8 @@ type InternalHandlerFn = (
5855
6773
  ) => Promise<Result<unknown, unknown>>;
5856
6774
 
5857
6775
  export class CommandBusImpl implements CommandBusPort {
6776
+ private relay: OutboxRelay | undefined;
6777
+
5858
6778
  constructor(
5859
6779
  private readonly txManager: TransactionManager,
5860
6780
  private readonly repoFactory: RepositoryFactory,
@@ -5862,6 +6782,10 @@ export class CommandBusImpl implements CommandBusPort {
5862
6782
  private readonly outboxWriter: OutboxWriter,
5863
6783
  ) {}
5864
6784
 
6785
+ setRelay(relay: OutboxRelay): void {
6786
+ this.relay = relay;
6787
+ }
6788
+
5865
6789
  // Internal handler type: receives deps injected by withUnitOfWork.
5866
6790
  // This is different from TestCommandBus which registers pre-bound
5867
6791
  // handlers with signature (command, eventContext) — deps closed over
@@ -5889,9 +6813,11 @@ export class CommandBusImpl implements CommandBusPort {
5889
6813
  ): Promise<TResult> {
5890
6814
  const { handler, contextKey: commandContext } = this.resolveHandler(command.type);
5891
6815
  try {
5892
- return await this.withUnitOfWork(commandContext, (deps) =>
6816
+ const result = await this.withUnitOfWork(commandContext, (deps) =>
5893
6817
  handler(command, deps, eventContext)
5894
6818
  ) as TResult;
6819
+ this.relay?.afterCommit();
6820
+ return result;
5895
6821
  } catch (error) {
5896
6822
  if (error instanceof TransactionAbortError) {
5897
6823
  // Reconstitute the failure Result that triggered rollback.
@@ -5922,16 +6848,24 @@ export class CommandBusImpl implements CommandBusPort {
5922
6848
  work: (deps: HandlerDeps) => Promise<Result<T, unknown>>
5923
6849
  ): Promise<Result<T, unknown>> {
5924
6850
  return this.txManager.withTransaction(async (tx) => {
6851
+ const aggregateEventTracker = new AggregateEventTracker();
5925
6852
  const eventCollector = new EventCollector();
5926
6853
  const repos = this.repoFactory.create(tx);
5927
6854
  const acls = this.aclFactory.create(tx, commandContext);
5928
6855
 
5929
- const result = await work({ tx, eventCollector, repos, acls });
6856
+ const result = await work({
6857
+ tx,
6858
+ eventCollector,
6859
+ aggregateEventTracker,
6860
+ repos,
6861
+ acls,
6862
+ });
5930
6863
 
5931
6864
  if (!result.ok) {
5932
6865
  throw new TransactionAbortError(result.error);
5933
6866
  }
5934
6867
 
6868
+ aggregateEventTracker.releaseInto(eventCollector);
5935
6869
  const events = eventCollector.drain();
5936
6870
 
5937
6871
  if (events.length > 0) {
@@ -5976,16 +6910,38 @@ function buildCompositionBusArtifacts() {
5976
6910
 
5977
6911
  //#endregion
5978
6912
  //#region packages/core/generators/composition_outbox.ts
5979
- function createOutboxTable() {
5980
- return `import { pgTable, uuid, varchar, jsonb, timestamp } from "drizzle-orm/pg-core";
6913
+ function createOutboxTable(infrastructureStrategy) {
6914
+ if (infrastructureStrategy?.persistence === "mysql") return `import { sql } from "drizzle-orm";
6915
+ import { int, json, mysqlTable, timestamp, varchar } from "drizzle-orm/mysql-core";
6916
+
6917
+ export const outboxEvents = mysqlTable("outbox_events", {
6918
+ id: varchar("id", { length: 36 }).primaryKey().default(sql\`(uuid())\`),
6919
+ eventType: varchar("event_type", { length: 255 }).notNull(),
6920
+ eventVersion: int("event_version").notNull(),
6921
+ payload: json("payload").notNull(),
6922
+ metadata: json("metadata").notNull(),
6923
+ occurredAt: timestamp("occurred_at").notNull().defaultNow(),
6924
+ status: varchar("status", { length: 50 }).notNull().default("pending"),
6925
+ attempts: int("attempts").notNull().default(0),
6926
+ processedAt: timestamp("processed_at"),
6927
+ failedAt: timestamp("failed_at"),
6928
+ error: json("error"),
6929
+ });
6930
+ `;
6931
+ return `import { pgTable, uuid, varchar, jsonb, timestamp, integer } from "drizzle-orm/pg-core";
5981
6932
 
5982
6933
  export const outboxEvents = pgTable("outbox_events", {
5983
6934
  id: uuid("id").primaryKey().defaultRandom(),
5984
6935
  eventType: varchar("event_type", { length: 255 }).notNull(),
6936
+ eventVersion: integer("event_version").notNull(),
5985
6937
  payload: jsonb("payload").notNull(),
5986
6938
  metadata: jsonb("metadata").notNull(),
5987
6939
  occurredAt: timestamp("occurred_at").notNull().defaultNow(),
5988
6940
  status: varchar("status", { length: 50 }).notNull().default("pending"),
6941
+ attempts: integer("attempts").notNull().default(0),
6942
+ processedAt: timestamp("processed_at"),
6943
+ failedAt: timestamp("failed_at"),
6944
+ error: jsonb("error"),
5989
6945
  });
5990
6946
  `;
5991
6947
  }
@@ -6001,6 +6957,7 @@ export class OutboxWriter {
6001
6957
  for (const event of events) {
6002
6958
  await db.insert(outboxEvents).values({
6003
6959
  eventType: event.eventType,
6960
+ eventVersion: event.eventVersion,
6004
6961
  payload: event.payload,
6005
6962
  metadata: {
6006
6963
  correlationId: event.correlationId,
@@ -6015,9 +6972,221 @@ export class OutboxWriter {
6015
6972
  }
6016
6973
  `;
6017
6974
  }
6018
- function buildCompositionOutboxArtifacts() {
6975
+ function createOutboxDispatcher() {
6976
+ return `import { asc, eq } from "drizzle-orm";
6977
+ import { outboxEvents } from "./outbox.table.ts";
6978
+ import type {
6979
+ EventSubscription,
6980
+ IntegrationEvent,
6981
+ } from "../messaging/integration-event.ts";
6982
+
6983
+ export type OutboxDispatcherConfig = {
6984
+ readonly maxAttempts?: number;
6985
+ readonly batchSize?: number;
6986
+ };
6987
+
6988
+ type OutboxRow = typeof outboxEvents.$inferSelect;
6989
+
6990
+ type OutboxMetadata = {
6991
+ correlationId: string;
6992
+ causationId: string;
6993
+ recordedBy: string;
6994
+ occurredAt: string;
6995
+ };
6996
+
6997
+ type HandlerError = {
6998
+ handler: string;
6999
+ error: string;
7000
+ };
7001
+
7002
+ export class OutboxDispatcher {
7003
+ private readonly maxAttempts: number;
7004
+ private readonly batchSize: number;
7005
+ private readonly handlersByEventType: Map<string, EventSubscription[]>;
7006
+
7007
+ constructor(
7008
+ private readonly db: unknown,
7009
+ subscriptions: EventSubscription[],
7010
+ config?: OutboxDispatcherConfig,
7011
+ ) {
7012
+ this.maxAttempts = config?.maxAttempts ?? 5;
7013
+ this.batchSize = config?.batchSize ?? 100;
7014
+
7015
+ const index = new Map<string, EventSubscription[]>();
7016
+ for (const subscription of subscriptions) {
7017
+ const existing = index.get(subscription.eventType) ?? [];
7018
+ existing.push(subscription);
7019
+ index.set(subscription.eventType, existing);
7020
+ }
7021
+ this.handlersByEventType = index;
7022
+ }
7023
+
7024
+ async dispatchPending(): Promise<{ processed: number; failed: number }> {
7025
+ const drizzle = this.db as any;
7026
+ let processed = 0;
7027
+ let failed = 0;
7028
+
7029
+ const rows: OutboxRow[] = await drizzle
7030
+ .select()
7031
+ .from(outboxEvents)
7032
+ .where(eq(outboxEvents.status, "pending"))
7033
+ .orderBy(asc(outboxEvents.occurredAt))
7034
+ .limit(this.batchSize);
7035
+
7036
+ for (const row of rows) {
7037
+ const ok = await this.dispatchRow(row);
7038
+ if (ok) {
7039
+ processed += 1;
7040
+ } else {
7041
+ failed += 1;
7042
+ }
7043
+ }
7044
+
7045
+ return { processed, failed };
7046
+ }
7047
+
7048
+ private async dispatchRow(row: OutboxRow): Promise<boolean> {
7049
+ const handlers = this.handlersByEventType.get(row.eventType);
7050
+ if (!handlers || handlers.length === 0) {
7051
+ await this.updateStatus(row.id, "processed", { processedAt: new Date() });
7052
+ return true;
7053
+ }
7054
+
7055
+ const metadata = (row.metadata ?? {}) as Partial<OutboxMetadata>;
7056
+ const event: IntegrationEvent<unknown> = {
7057
+ type: row.eventType,
7058
+ version: row.eventVersion,
7059
+ payload: row.payload,
7060
+ eventContext: {
7061
+ correlationId: metadata.correlationId as any,
7062
+ causationId: metadata.causationId as any,
7063
+ recordedBy: metadata.recordedBy as any,
7064
+ },
7065
+ };
7066
+
7067
+ const errors: HandlerError[] = [];
7068
+ for (const subscription of handlers) {
7069
+ try {
7070
+ await subscription.handler(event);
7071
+ } catch (error) {
7072
+ errors.push({
7073
+ handler: subscription.handler.name || subscription.eventType,
7074
+ error: error instanceof Error ? error.message : String(error),
7075
+ });
7076
+ }
7077
+ }
7078
+
7079
+ if (errors.length === 0) {
7080
+ await this.updateStatus(row.id, "processed", {
7081
+ processedAt: new Date(),
7082
+ error: null,
7083
+ });
7084
+ return true;
7085
+ }
7086
+
7087
+ const nextAttempts = (row.attempts ?? 0) + 1;
7088
+ if (nextAttempts >= this.maxAttempts) {
7089
+ await this.updateStatus(row.id, "failed", {
7090
+ attempts: nextAttempts,
7091
+ failedAt: new Date(),
7092
+ error: errors,
7093
+ });
7094
+ } else {
7095
+ await this.updateStatus(row.id, "pending", {
7096
+ attempts: nextAttempts,
7097
+ error: errors,
7098
+ });
7099
+ }
7100
+
7101
+ return false;
7102
+ }
7103
+
7104
+ private async updateStatus(
7105
+ id: string,
7106
+ status: "pending" | "processed" | "failed",
7107
+ fields: Record<string, unknown>,
7108
+ ): Promise<void> {
7109
+ const drizzle = this.db as any;
7110
+ await drizzle
7111
+ .update(outboxEvents)
7112
+ .set({ status, ...fields })
7113
+ .where(eq(outboxEvents.id, id));
7114
+ }
7115
+ }
7116
+ `;
7117
+ }
7118
+ function createOutboxRelay() {
7119
+ return `import type { OutboxDispatcher } from "./outbox-dispatcher.ts";
7120
+
7121
+ export class OutboxRelay {
7122
+ constructor(private readonly dispatcher: OutboxDispatcher) {}
7123
+
7124
+ afterCommit(): void {
7125
+ void this.dispatcher.dispatchPending().catch(() => {
7126
+ // Swallowed - the poller retries on the next cycle.
7127
+ });
7128
+ }
7129
+ }
7130
+ `;
7131
+ }
7132
+ function createOutboxPoller() {
7133
+ return `import type { OutboxDispatcher } from "./outbox-dispatcher.ts";
7134
+
7135
+ export type OutboxPollerConfig = {
7136
+ readonly intervalMs?: number;
7137
+ };
7138
+
7139
+ export class OutboxPoller {
7140
+ private handle: ReturnType<typeof setInterval> | null = null;
7141
+ private polling = false;
7142
+ private readonly intervalMs: number;
7143
+
7144
+ constructor(
7145
+ private readonly dispatcher: OutboxDispatcher,
7146
+ config?: OutboxPollerConfig,
7147
+ ) {
7148
+ this.intervalMs = config?.intervalMs ?? 5_000;
7149
+ }
7150
+
7151
+ start(): void {
7152
+ if (this.handle) return;
7153
+ void this.poll();
7154
+ this.handle = setInterval(() => void this.poll(), this.intervalMs);
7155
+ }
7156
+
7157
+ stop(): void {
7158
+ if (!this.handle) return;
7159
+ clearInterval(this.handle);
7160
+ this.handle = null;
7161
+ }
7162
+
7163
+ get running(): boolean {
7164
+ return this.handle !== null;
7165
+ }
7166
+
7167
+ private async poll(): Promise<void> {
7168
+ if (this.polling) return;
7169
+ this.polling = true;
7170
+ try {
7171
+ await this.dispatcher.dispatchPending();
7172
+ } catch {
7173
+ // Swallowed - the next cycle retries.
7174
+ } finally {
7175
+ this.polling = false;
7176
+ }
7177
+ }
7178
+ }
7179
+ `;
7180
+ }
7181
+ function buildCompositionOutboxArtifacts(infrastructureStrategy) {
6019
7182
  const ownership = sharedArtifactOwnership("composition");
6020
- return [createGeneratedArtifact("infrastructure/outbox/outbox.table.ts", createOutboxTable(), ownership), createGeneratedArtifact("infrastructure/outbox/outbox-writer.ts", createOutboxWriter(), ownership)].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
7183
+ return [
7184
+ createGeneratedArtifact("infrastructure/outbox/outbox-dispatcher.ts", createOutboxDispatcher(), ownership),
7185
+ createGeneratedArtifact("infrastructure/outbox/outbox-poller.ts", createOutboxPoller(), ownership),
7186
+ createGeneratedArtifact("infrastructure/outbox/outbox-relay.ts", createOutboxRelay(), ownership),
7187
+ createGeneratedArtifact("infrastructure/outbox/outbox.table.ts", createOutboxTable(infrastructureStrategy), ownership),
7188
+ createGeneratedArtifact("infrastructure/outbox/outbox-writer.ts", createOutboxWriter(), ownership)
7189
+ ].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
6021
7190
  }
6022
7191
 
6023
7192
  //#endregion
@@ -6105,34 +7274,53 @@ function renderMappedType(fields) {
6105
7274
  ${Object.entries(fields).map(([fieldName, expr]) => ` readonly ${camelCase(fieldName)}: ${mappingValueType(expr)};`).join("\n")}
6106
7275
  }`;
6107
7276
  }
6108
- function sourceQueryViewTypeName(acl) {
6109
- const baseName = acl.sourceQuery.readModelName ? pascalCase(acl.sourceQuery.readModelName) : pascalCase(acl.sourceQuery.name);
6110
- return baseName.endsWith("View") ? baseName : `${baseName}View`;
7277
+ function mapReadAclFieldTypeToTS(type) {
7278
+ switch (type) {
7279
+ case "string": return "string";
7280
+ case "number": return "number";
7281
+ case "boolean": return "boolean";
7282
+ case "date": return "string";
7283
+ case "enum": return "string";
7284
+ case "union": return "string | number | boolean | null";
7285
+ case "record": return "Record<string, unknown>";
7286
+ case "tuple": return "[unknown, ...unknown[]]";
7287
+ case "intersection": return "Record<string, unknown>";
7288
+ default: return "unknown";
7289
+ }
6111
7290
  }
6112
- function sourceQueryViewFileName(acl) {
6113
- return `${(acl.sourceQuery.readModelName ? kebabCase(acl.sourceQuery.readModelName) : kebabCase(acl.sourceQuery.name)).replace(/-view$/, "")}.view.ts`;
7291
+ function formatReadAclFieldType(field, indent = 0) {
7292
+ if (field.type === "object" && field.nestedFields && field.nestedFields.length > 0) {
7293
+ const objectType = `{\n${field.nestedFields.map((nestedField) => {
7294
+ const opt = nestedField.optional ? "?" : "";
7295
+ return `${" ".repeat(indent + 1)}readonly ${camelCase(nestedField.name)}${opt}: ${formatReadAclFieldType(nestedField, indent + 1)};`;
7296
+ }).join("\n")}\n${" ".repeat(indent)}}`;
7297
+ return field.array ? `Array<${objectType}>` : objectType;
7298
+ }
7299
+ const base = mapReadAclFieldTypeToTS(field.type);
7300
+ return field.array ? `${base}[]` : base;
7301
+ }
7302
+ function formatReadAclListItemType(outputFields) {
7303
+ return `Array<{\n${outputFields.map((field) => {
7304
+ const opt = field.optional ? "?" : "";
7305
+ return ` readonly ${camelCase(field.name)}${opt}: ${formatReadAclFieldType(field, 1)};`;
7306
+ }).join("\n")}\n}>`;
6114
7307
  }
6115
7308
  function readResponseFieldType(acl, mapping) {
6116
7309
  if (mapping.kind === "const") return mappingValueType({ const: mapping.value });
6117
- const viewTypeName = sourceQueryViewTypeName(acl);
6118
7310
  if (acl.sourceQuery.queryKind === "list") {
6119
- if (mapping.sourcePath === "result.items") return `${viewTypeName}[]`;
7311
+ if (mapping.sourcePath === "result.items") return formatReadAclListItemType(acl.sourceQuery.outputFields);
6120
7312
  if (mapping.sourcePath === "result.total" || mapping.sourcePath === "result.page" || mapping.sourcePath === "result.pageSize") return "number";
6121
7313
  }
6122
7314
  const sourceFieldName = mapping.sourcePath.replace(/^result\./, "");
6123
7315
  const sourceField = acl.sourceQuery.outputFields.find((field) => camelCase(field.name) === camelCase(sourceFieldName));
6124
- return sourceField ? `${viewTypeName}[${JSON.stringify(sourceField.name)}]` : "unknown";
7316
+ return sourceField ? formatReadAclFieldType(sourceField) : "unknown";
6125
7317
  }
6126
7318
  function buildReadAclContractContent(acl) {
6127
7319
  normalizeModulePath(acl.consumerContext.modulePath);
6128
- const sourceModulePath = normalizeModulePath(acl.sourceContext.modulePath);
6129
7320
  const requestTypeName = aclRequestTypeName(acl.port);
6130
7321
  const responseTypeName = aclResponseTypeName(acl.port);
6131
- const viewTypeName = sourceQueryViewTypeName(acl);
6132
- const viewFileName = sourceQueryViewFileName(acl);
6133
7322
  const responseLines = acl.responseMappings.map((mapping) => ` readonly ${camelCase(mapping.targetPath)}: ${readResponseFieldType(acl, mapping)};`);
6134
7323
  return `import type { Transaction } from "../../../../../lib/transaction.ts";
6135
- import type { ${viewTypeName} } from "../../../${sourceModulePath}/application/contracts/${viewFileName}";
6136
7324
 
6137
7325
  export type ${requestTypeName} = ${renderMappedType(Object.fromEntries(acl.requestMappings.map((mapping) => [mapping.targetPath, mapping.kind === "from" ? { from: mapping.sourcePath } : { const: mapping.value }])))};
6138
7326
 
@@ -6421,6 +7609,24 @@ function buildCompositionAclArtifacts(compositionSpec) {
6421
7609
  ].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
6422
7610
  }
6423
7611
 
7612
+ //#endregion
7613
+ //#region packages/core/generators/composition_dependencies.ts
7614
+ function resolveRuntimeDependencies(compositionSpec) {
7615
+ const dependencies = new Set(["drizzle-orm"]);
7616
+ if (compositionSpec.infrastructure.persistence === "postgres") dependencies.add("postgres");
7617
+ else if (compositionSpec.infrastructure.persistence === "mysql") dependencies.add("mysql2");
7618
+ if (compositionSpec.infrastructure.orm === "mikroorm") dependencies.add("@mikro-orm/core");
7619
+ return [...dependencies].sort((left, right) => left.localeCompare(right));
7620
+ }
7621
+ function buildCompositionDependencyManifestArtifacts(compositionSpec) {
7622
+ const manifest = {
7623
+ kind: "zodmire-dependency-manifest",
7624
+ infrastructure: compositionSpec.infrastructure,
7625
+ dependencies: resolveRuntimeDependencies(compositionSpec)
7626
+ };
7627
+ return [createGeneratedArtifact("codegen/dependency-manifest.json", `${JSON.stringify(manifest, null, 2)}\n`, sharedArtifactOwnership("composition"))];
7628
+ }
7629
+
6424
7630
  //#endregion
6425
7631
  //#region packages/core/generators/composition_container.ts
6426
7632
  /**
@@ -6432,6 +7638,8 @@ function buildCompositionContainerArtifacts(compositionSpec, contextSpecs) {
6432
7638
  return [createGeneratedArtifact("infrastructure/di/container.ts", buildContainerContent(compositionSpec, contextSpecs), ownership), createGeneratedArtifact("infrastructure/di/repo-factory.ts", buildRepoFactoryContent(contextSpecs), ownership)].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
6433
7639
  }
6434
7640
  function buildContainerContent(compositionSpec, contextSpecs) {
7641
+ assertSupportedContainerStrategy(compositionSpec);
7642
+ const runtime = resolveContainerRuntime(compositionSpec);
6435
7643
  const readContexts = uniqueReadContexts(compositionSpec.acls);
6436
7644
  const readContextNames = buildCollisionSafeModulePathNames(readContexts.map((context) => context.modulePath));
6437
7645
  const commandContextNames = buildCollisionSafeModulePathNames(contextSpecs.map((context) => context.context.modulePath));
@@ -6441,7 +7649,10 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6441
7649
  ``,
6442
7650
  `import { CommandBusImpl } from "../messaging/command-bus.impl.ts";`,
6443
7651
  `import { QueryBusImpl } from "../messaging/query-bus.impl.ts";`,
6444
- `import { DrizzleTransactionManager } from "../unit-of-work/drizzle-transaction-manager.ts";`,
7652
+ `import { ${runtime.transactionManagerClassName} } from "../unit-of-work/${runtime.transactionManagerFileBase}.ts";`,
7653
+ `import { OutboxDispatcher } from "../outbox/outbox-dispatcher.ts";`,
7654
+ `import { OutboxPoller } from "../outbox/outbox-poller.ts";`,
7655
+ `import { OutboxRelay } from "../outbox/outbox-relay.ts";`,
6445
7656
  `import { OutboxWriter } from "../outbox/outbox-writer.ts";`,
6446
7657
  `import { AclFactory } from "./acl-factory.ts";`,
6447
7658
  `import { RepositoryFactory } from "./repo-factory.ts";`,
@@ -6460,7 +7671,8 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6460
7671
  for (const query of uniqueQueriesByReadModel(contextSpec.queries)) {
6461
7672
  const repoClassName = queryReadModelRepositoryClassName(query);
6462
7673
  const repoAlias = queryReadModelRepositoryAlias(boundaryPlan.symbolStem, query);
6463
- importLines.push(repoAlias === repoClassName ? `import { ${repoClassName} } from "../view-models/drizzle/repositories/${modulePath}/${queryViewFileBase(query)}.repository.ts";` : `import { ${repoClassName} as ${repoAlias} } from "../view-models/drizzle/repositories/${modulePath}/${queryViewFileBase(query)}.repository.ts";`);
7674
+ const repoFileBase = readModelRepositoryFileBase(resolvedReadModelName(query));
7675
+ importLines.push(repoAlias === repoClassName ? `import { ${repoClassName} } from "../view-models/drizzle/repositories/${modulePath}/${repoFileBase}.repository.ts";` : `import { ${repoClassName} as ${repoAlias} } from "../view-models/drizzle/repositories/${modulePath}/${repoFileBase}.repository.ts";`);
6464
7676
  }
6465
7677
  }
6466
7678
  for (const spec of contextSpecs) {
@@ -6484,15 +7696,16 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6484
7696
  ` readonly commandBus: CommandBusPort;`,
6485
7697
  ` readonly queryBus: QueryBusPort;`,
6486
7698
  ` readonly integrationEventSubscriptions: EventSubscription[];`,
6487
- ` readonly db: unknown;`,
7699
+ ` readonly outboxPoller: OutboxPoller;`,
7700
+ ...runtime.containerFields.map((field) => ` readonly ${field}: unknown;`),
6488
7701
  `};`
6489
7702
  ];
6490
7703
  const createContainerLines = [
6491
7704
  ``,
6492
- `export function createContainer(deps: { db: unknown }): Container {`,
7705
+ `export function createContainer(deps: { ${runtime.dependencyFields.map((field) => `${field}: unknown`).join("; ")} }): Container {`,
6493
7706
  ` const outboxWriter = new OutboxWriter();`,
6494
7707
  ` const repoFactory = new RepositoryFactory();`,
6495
- ` const txManager = new DrizzleTransactionManager(deps.db);`
7708
+ ` const txManager = new ${runtime.transactionManagerClassName}(${runtime.transactionDependencyRef});`
6496
7709
  ];
6497
7710
  for (const context of readContexts) {
6498
7711
  const contextSpec = contextSpecs.find((spec) => spec.context.modulePath.toLowerCase() === context.modulePath.toLowerCase());
@@ -6500,7 +7713,7 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6500
7713
  const boundaryPlan = readContextNames.get(context.modulePath);
6501
7714
  const boundaryVar = `${boundaryPlan.propertyStem}PublishedReadBoundary`;
6502
7715
  const boundaryAlias = `create${boundaryPlan.symbolStem}PublishedReadBoundary`;
6503
- const boundaryDeps = buildPublishedReadBoundaryDeps(contextSpec, boundaryPlan.symbolStem);
7716
+ const boundaryDeps = buildPublishedReadBoundaryDeps(contextSpec, boundaryPlan.symbolStem, runtime.readRepositoryDependencyRef);
6504
7717
  createContainerLines.push(` const ${boundaryVar} = ${boundaryAlias}(${boundaryDeps});`);
6505
7718
  }
6506
7719
  createContainerLines.push(` let commandBus: CommandBusImpl | undefined;`, ` const commandBusPort: CommandBusPort = {`, ` execute: (command, eventContext) => {`, ` if (!commandBus) {`, ` throw new Error("Command bus is not initialized");`, ` }`, ` return commandBus.execute(command, eventContext);`, ` },`, ` };`);
@@ -6531,7 +7744,9 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6531
7744
  createContainerLines.push(` const integrationEventSubscriptions: EventSubscription[] = [];`);
6532
7745
  }
6533
7746
  createContainerLines.push(``);
6534
- createContainerLines.push(` return { commandBus: commandBus!, queryBus, integrationEventSubscriptions, db: deps.db };`);
7747
+ createContainerLines.push(` const outboxDispatcher = new OutboxDispatcher(${runtime.transactionDependencyRef}, integrationEventSubscriptions);`, ` const outboxRelay = new OutboxRelay(outboxDispatcher);`, ` commandBus.setRelay(outboxRelay);`, ` const outboxPoller = new OutboxPoller(outboxDispatcher);`);
7748
+ createContainerLines.push(``);
7749
+ createContainerLines.push(` return { commandBus: commandBus!, queryBus, integrationEventSubscriptions, outboxPoller, ${runtime.containerFields.map((field) => `${field}: deps.${field}`).join(", ")} };`);
6535
7750
  createContainerLines.push(`}`);
6536
7751
  createContainerLines.push(``);
6537
7752
  return [
@@ -6540,11 +7755,50 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6540
7755
  ...createContainerLines
6541
7756
  ].join("\n");
6542
7757
  }
6543
- function buildPublishedReadBoundaryDeps(spec, contextSymbolStem) {
7758
+ function buildPublishedReadBoundaryDeps(spec, contextSymbolStem, readRepositoryDependencyRef) {
6544
7759
  const entries = /* @__PURE__ */ new Map();
6545
- for (const query of uniqueQueriesByReadModel(spec.queries)) entries.set(queryReadModelVariableName(query), `${queryReadModelVariableName(query)}: new ${queryReadModelRepositoryAlias(contextSymbolStem, query)}(deps.db)`);
7760
+ for (const query of uniqueQueriesByReadModel(spec.queries)) entries.set(queryReadModelVariableName(query), `${queryReadModelVariableName(query)}: new ${queryReadModelRepositoryAlias(contextSymbolStem, query)}(${readRepositoryDependencyRef})`);
6546
7761
  return `{ ${[...entries.values()].join(", ")} }`;
6547
7762
  }
7763
+ function assertSupportedContainerStrategy(compositionSpec) {
7764
+ const { architecture, persistence, orm } = compositionSpec.infrastructure;
7765
+ if (!(architecture === "physical-cqrs" && persistence === "postgres" && (orm === "drizzle" || orm === "mikroorm") || architecture === "logical-cqrs" && (persistence === "postgres" && (orm === "drizzle" || orm === "mikroorm") || persistence === "mysql" && (orm === "drizzle" || orm === "mikroorm")) || architecture === "physical-cqrs" && persistence === "mysql" && (orm === "drizzle" || orm === "mikroorm"))) throw new Error(`[composition_container] Unsupported infrastructure strategy: ${architecture} + ${persistence} + ${orm}`);
7766
+ }
7767
+ function resolveContainerRuntime(compositionSpec) {
7768
+ const { architecture, orm } = compositionSpec.infrastructure;
7769
+ if (architecture === "physical-cqrs" && orm === "drizzle") return {
7770
+ transactionManagerClassName: "DrizzleTransactionManager",
7771
+ transactionManagerFileBase: "drizzle-transaction-manager",
7772
+ transactionDependencyRef: "deps.writeDb",
7773
+ readRepositoryDependencyRef: "deps.readDb",
7774
+ dependencyFields: ["writeDb", "readDb"],
7775
+ containerFields: ["writeDb", "readDb"]
7776
+ };
7777
+ if (architecture === "logical-cqrs" && orm === "drizzle") return {
7778
+ transactionManagerClassName: "DrizzleTransactionManager",
7779
+ transactionManagerFileBase: "drizzle-transaction-manager",
7780
+ transactionDependencyRef: "deps.db",
7781
+ readRepositoryDependencyRef: "deps.db",
7782
+ dependencyFields: ["db"],
7783
+ containerFields: ["db"]
7784
+ };
7785
+ if (architecture === "physical-cqrs" && orm === "mikroorm") return {
7786
+ transactionManagerClassName: "MikroOrmTransactionManager",
7787
+ transactionManagerFileBase: "mikroorm-transaction-manager",
7788
+ transactionDependencyRef: "deps.writeEm",
7789
+ readRepositoryDependencyRef: "deps.readDb",
7790
+ dependencyFields: ["writeEm", "readDb"],
7791
+ containerFields: ["writeEm", "readDb"]
7792
+ };
7793
+ return {
7794
+ transactionManagerClassName: "MikroOrmTransactionManager",
7795
+ transactionManagerFileBase: "mikroorm-transaction-manager",
7796
+ transactionDependencyRef: "deps.em",
7797
+ readRepositoryDependencyRef: "deps.db",
7798
+ dependencyFields: ["em", "db"],
7799
+ containerFields: ["em", "db"]
7800
+ };
7801
+ }
6548
7802
  function resolvedReadModelName(query) {
6549
7803
  return query.readSide?.readModelName ?? query.readModelName ?? query.name;
6550
7804
  }
@@ -6563,23 +7817,17 @@ function uniqueQueriesByReadModel(queries) {
6563
7817
  }
6564
7818
  return [...queriesByReadModel.values()].sort((left, right) => resolvedReadModelName(left).localeCompare(resolvedReadModelName(right)));
6565
7819
  }
6566
- function queryOutputContractName(query) {
6567
- return readModelContractName(resolvedReadModelName(query));
6568
- }
6569
- function queryViewFileBase(query) {
6570
- return readModelRepositoryFileBase(resolvedReadModelName(query));
6571
- }
6572
7820
  function queryReadModelPortTypeName(query) {
6573
- return `${queryOutputContractName(query)}RepositoryPort`;
7821
+ return `${readModelContractName(resolvedReadModelName(query))}RepositoryPort`;
6574
7822
  }
6575
7823
  function queryReadModelVariableName(query) {
6576
7824
  return camelCase(queryReadModelPortTypeName(query));
6577
7825
  }
6578
7826
  function queryReadModelRepositoryClassName(query) {
6579
- return `Drizzle${queryOutputContractName(query)}Repository`;
7827
+ return `Drizzle${readModelContractName(resolvedReadModelName(query))}Repository`;
6580
7828
  }
6581
7829
  function queryReadModelRepositoryAlias(contextSymbolStem, query) {
6582
- return `${contextSymbolStem}${queryOutputContractName(query)}Repository`;
7830
+ return `${contextSymbolStem}${readModelContractName(resolvedReadModelName(query))}Repository`;
6583
7831
  }
6584
7832
  function uniqueReadContexts(acls) {
6585
7833
  const seen = /* @__PURE__ */ new Set();
@@ -6594,24 +7842,31 @@ function uniqueReadContexts(acls) {
6594
7842
  return contexts;
6595
7843
  }
6596
7844
  function buildRepoFactoryContent(contextSpecs) {
6597
- const repositoryDefs = contextSpecs.flatMap((spec) => spec.aggregates.map((aggregate) => ({
6598
- aggregateName: aggregate.name,
6599
- modulePath: spec.context.modulePath,
6600
- repoFile: `drizzle-${kebabCase(aggregate.name)}.repository.ts`,
6601
- aggregateAliasBase: `Drizzle${pascalCase(aggregate.name)}Repository`,
6602
- aggregateFieldBase: `${camelCase(aggregate.name)}s`
6603
- })));
7845
+ const repositoryDefs = contextSpecs.flatMap((spec) => spec.aggregates.map((aggregate) => {
7846
+ const repositoryPort = spec.ports.find((port) => port.kind === "repository" && port.target === aggregate.name);
7847
+ const isMikroOrm = (repositoryPort ? spec.adapters.find((candidate) => candidate.port === repositoryPort.name && (candidate.kind === "drizzle-repository" || candidate.kind === "mikroorm-repository")) : void 0)?.kind === "mikroorm-repository";
7848
+ const providerPrefix = isMikroOrm ? "mikroorm" : "drizzle";
7849
+ const repositoryClassPrefix = isMikroOrm ? "MikroOrm" : "Drizzle";
7850
+ return {
7851
+ aggregateName: aggregate.name,
7852
+ modulePath: spec.context.modulePath,
7853
+ repoFile: `${providerPrefix}-${kebabCase(aggregate.name)}.repository.ts`,
7854
+ repoImportBasePath: `../persistence/${providerPrefix}/repositories`,
7855
+ aggregateAliasBase: `${repositoryClassPrefix}${pascalCase(aggregate.name)}Repository`,
7856
+ aggregateFieldBase: `${camelCase(aggregate.name)}s`
7857
+ };
7858
+ }));
6604
7859
  assertNoSameContextRepoFileCollisions(repositoryDefs);
6605
7860
  const namingPlan = buildRepositoryNamingPlan(repositoryDefs);
6606
7861
  const lines = [`// Auto-generated repository factory — do not edit by hand`, ``];
6607
7862
  for (const repositoryDef of repositoryDefs) {
6608
- const aggName = repositoryDef.aggregateName;
7863
+ repositoryDef.aggregateName;
6609
7864
  const modulePath = normalizeModulePath(repositoryDef.modulePath);
6610
- const repoClass = `Drizzle${pascalCase(aggName)}Repository`;
7865
+ const repoClass = repositoryDef.aggregateAliasBase;
6611
7866
  const { repoAlias } = namingPlan.get(repositoryKey(repositoryDef));
6612
7867
  const repoFile = repositoryDef.repoFile;
6613
- if (repoAlias === repoClass) lines.push(`import { ${repoClass} } from "../persistence/drizzle/repositories/${modulePath}/${repoFile}";`);
6614
- else lines.push(`import { ${repoClass} as ${repoAlias} } from "../persistence/drizzle/repositories/${modulePath}/${repoFile}";`);
7868
+ if (repoAlias === repoClass) lines.push(`import { ${repoClass} } from "${repositoryDef.repoImportBasePath}/${modulePath}/${repoFile}";`);
7869
+ else lines.push(`import { ${repoClass} as ${repoAlias} } from "${repositoryDef.repoImportBasePath}/${modulePath}/${repoFile}";`);
6615
7870
  }
6616
7871
  lines.push(``);
6617
7872
  lines.push(`export class RepositoryFactory {`);
@@ -6749,9 +8004,14 @@ export {};
6749
8004
  const payloadTypeNames = [];
6750
8005
  for (const evt of compositionSpec.crossContextEvents) {
6751
8006
  const fields = getPayloadFields(evt.payloadSchema);
6752
- payloadTypeNames.push(evt.payloadTypeName);
6753
- lines.push(`export interface ${evt.payloadTypeName} {`);
6754
- for (const field of fields) lines.push(` readonly ${field.name}: ${field.type};`);
8007
+ const payloadTypeName = integrationPayloadTypeName(evt);
8008
+ payloadTypeNames.push(payloadTypeName);
8009
+ lines.push(`export interface ${payloadTypeName} {`);
8010
+ for (const field of fields) {
8011
+ const opt = field.optional ? "?" : "";
8012
+ const comment = field.comment ? ` ${field.comment}` : "";
8013
+ lines.push(` readonly ${field.name}${opt}: ${field.type};${comment}`);
8014
+ }
6755
8015
  lines.push(`}`);
6756
8016
  lines.push(``);
6757
8017
  }
@@ -6762,18 +8022,20 @@ export {};
6762
8022
  return lines.join("\n");
6763
8023
  }
6764
8024
  function buildHandlerFactoryContent(evt, sub) {
6765
- const payloadMappingLines = getPayloadFields(evt.payloadSchema).map((f) => ` ${f.name}: event.payload.${f.name},`);
8025
+ const fields = getPayloadFields(evt.payloadSchema);
8026
+ const payloadTypeName = integrationPayloadTypeName(evt);
8027
+ const payloadMappingLines = fields.map((f) => ` ${f.name}: event.payload.${f.name},`);
6766
8028
  const commandType = `${pascalCase(sub.targetContext)}.${pascalCase(sub.action)}`;
6767
8029
  return [
6768
8030
  `// Auto-generated event handler factory — do not edit by hand`,
6769
8031
  `import type { CommandBusPort } from "../../../../../core/ports/messaging/command-bus.port.ts";`,
6770
8032
  `import type { IntegrationEvent, IntegrationEventHandler } from "../../../../../infrastructure/messaging/integration-event.ts";`,
6771
- `import type { ${evt.payloadTypeName} } from "../../../../shared-kernel/events/integration-events.types.ts";`,
8033
+ `import type { ${payloadTypeName} } from "../../../../shared-kernel/events/integration-events.types.ts";`,
6772
8034
  ``,
6773
8035
  `export function ${sub.handlerFactoryName}(deps: {`,
6774
8036
  ` commandBus: CommandBusPort;`,
6775
- `}): IntegrationEventHandler<${evt.payloadTypeName}> {`,
6776
- ` return async (event: IntegrationEvent<${evt.payloadTypeName}>) => {`,
8037
+ `}): IntegrationEventHandler<${payloadTypeName}> {`,
8038
+ ` return async (event: IntegrationEvent<${payloadTypeName}>) => {`,
6777
8039
  ` return deps.commandBus.execute(`,
6778
8040
  ` {`,
6779
8041
  ` type: "${commandType}",`,
@@ -6792,30 +8054,114 @@ function buildSubscriptionRegistryContent(compositionSpec) {
6792
8054
  const allSubs = [];
6793
8055
  for (const evt of compositionSpec.crossContextEvents) for (const sub of evt.subscriptions) allSubs.push({
6794
8056
  eventType: evt.eventType,
8057
+ payloadTypeName: integrationPayloadTypeName(evt),
6795
8058
  sub
6796
8059
  });
6797
8060
  const importLines = [`// Auto-generated subscription registry — do not edit by hand`, `import type { EventSubscription, IntegrationEventHandler } from "./integration-event.ts";`];
8061
+ const payloadTypeImports = [...new Set(allSubs.map(({ payloadTypeName }) => payloadTypeName))].sort((left, right) => left.localeCompare(right)).map((payloadTypeName) => `import type { ${payloadTypeName} } from "../../core/shared-kernel/events/integration-events.types.ts";`);
8062
+ importLines.push(...payloadTypeImports);
6798
8063
  for (const { sub } of allSubs) {
6799
8064
  const fileName = `${kebabCase(sub.handlerName)}.handler.ts`;
6800
8065
  importLines.push(`// Handler: ${sub.handlerFactoryName} from core/contexts/${sub.targetModulePath}/application/event-handlers/${fileName}`);
6801
8066
  }
6802
- const depsEntries = allSubs.map(({ sub }) => ` ${camelCase(sub.handlerName)}: IntegrationEventHandler<any>;`);
8067
+ const depsEntries = allSubs.map(({ payloadTypeName, sub }) => ` ${camelCase(sub.handlerName)}: IntegrationEventHandler<${payloadTypeName}>;`);
6803
8068
  const registryEntries = allSubs.map(({ eventType, sub }) => ` { eventType: "${eventType}", handler: deps.${camelCase(sub.handlerName)} },`);
6804
8069
  const lines = [...importLines, ``];
6805
8070
  if (depsEntries.length > 0) lines.push(`export function buildSubscriptionRegistry(deps: {`, ...depsEntries, `}): EventSubscription[] {`, ` return [`, ...registryEntries, ` ];`, `}`, ``);
6806
8071
  else lines.push(`export function buildSubscriptionRegistry(_deps: Record<string, never>): EventSubscription[] {`, ` return [];`, `}`, ``);
6807
8072
  return lines.join("\n");
6808
8073
  }
8074
+ function integrationPayloadTypeName(evt) {
8075
+ return `${pascalCase(evt.sourceContext)}${evt.payloadTypeName}`;
8076
+ }
8077
+ function renderPayloadSchemaType(schema, indent = 0) {
8078
+ const raw = schema;
8079
+ const def = raw?._zod?.def;
8080
+ const typeName = typeof def?.type === "string" ? def.type : void 0;
8081
+ if (typeName === "optional") return {
8082
+ ...renderPayloadSchemaType(def?.innerType, indent),
8083
+ optional: true
8084
+ };
8085
+ if (typeName === "nullable" || typeName === "readonly" || typeName === "default" || typeName === "catch" || typeName === "nonoptional" || typeName === "success" || typeName === "prefault") return renderPayloadSchemaType(def?.innerType, indent);
8086
+ if (typeName === "array") {
8087
+ const element = renderPayloadSchemaType(def?.element, indent);
8088
+ return {
8089
+ type: `Array<${element.type}>`,
8090
+ optional: false,
8091
+ comment: element.comment
8092
+ };
8093
+ }
8094
+ if (typeName === "object") {
8095
+ const shape = typeof raw?.shape === "object" && raw?.shape !== null ? raw.shape : typeof def?.shape === "object" && def?.shape !== null ? def.shape : {};
8096
+ return {
8097
+ type: `{\n${Object.entries(shape).map(([fieldName, fieldSchema]) => {
8098
+ const field = renderPayloadSchemaType(fieldSchema, indent + 1);
8099
+ const opt = field.optional ? "?" : "";
8100
+ const comment = field.comment ? ` ${field.comment}` : "";
8101
+ return `${" ".repeat(indent + 1)}readonly ${fieldName}${opt}: ${field.type};${comment}`;
8102
+ }).join("\n")}\n${" ".repeat(indent)}}`,
8103
+ optional: false
8104
+ };
8105
+ }
8106
+ if (typeName === "enum") {
8107
+ const entries = def?.entries;
8108
+ if (entries && typeof entries === "object") return {
8109
+ type: Object.values(entries).map((value) => JSON.stringify(String(value))).join(" | "),
8110
+ optional: false
8111
+ };
8112
+ return {
8113
+ type: "string",
8114
+ optional: false
8115
+ };
8116
+ }
8117
+ if (typeName === "record") {
8118
+ const valueShape = renderPayloadSchemaType(def?.valueType, indent);
8119
+ return {
8120
+ type: `Record<string, ${valueShape.type}>`,
8121
+ optional: false,
8122
+ comment: valueShape.comment
8123
+ };
8124
+ }
8125
+ if (typeName === "tuple") return {
8126
+ type: `[${(Array.isArray(def?.items) ? def.items : []).map((item) => renderPayloadSchemaType(item, indent).type).join(", ")}]`,
8127
+ optional: false
8128
+ };
8129
+ if (typeName === "union") return {
8130
+ type: (Array.isArray(def?.options) ? def.options : []).map((option) => renderPayloadSchemaType(option, indent).type).join(" | "),
8131
+ optional: false
8132
+ };
8133
+ if (typeName === "date") return {
8134
+ type: "string",
8135
+ optional: false,
8136
+ comment: "// ISO 8601"
8137
+ };
8138
+ if (typeName === "number" || typeName === "int" || typeName === "float") return {
8139
+ type: "number",
8140
+ optional: false
8141
+ };
8142
+ if (typeName === "boolean") return {
8143
+ type: "boolean",
8144
+ optional: false
8145
+ };
8146
+ if (typeName === "string") return {
8147
+ type: "string",
8148
+ optional: false
8149
+ };
8150
+ return {
8151
+ type: "string",
8152
+ optional: false
8153
+ };
8154
+ }
6809
8155
  function getPayloadFields(payloadSchema) {
6810
8156
  const shape = payloadSchema?.shape;
6811
8157
  if (shape && typeof shape === "object") return Object.keys(shape).map((key) => {
6812
- const typeName = ((shape[key]?._zod)?.def)?.type;
6813
- let tsType = "string";
6814
- if (typeName === "number") tsType = "number";
6815
- else if (typeName === "boolean") tsType = "boolean";
8158
+ const fieldSchema = shape[key];
8159
+ const rendered = renderPayloadSchemaType(fieldSchema);
6816
8160
  return {
6817
8161
  name: key,
6818
- type: tsType
8162
+ type: rendered.type,
8163
+ optional: rendered.optional,
8164
+ comment: rendered.comment
6819
8165
  };
6820
8166
  });
6821
8167
  return [];
@@ -6840,7 +8186,7 @@ async function buildContextNormalizedSpecFromConfig(input) {
6840
8186
  }))), config);
6841
8187
  }
6842
8188
  async function buildV5ContextArtifacts(input) {
6843
- return generateContextArtifacts(await buildContextNormalizedSpecFromConfig(input));
8189
+ return generateContextArtifacts(await buildContextNormalizedSpecFromConfig(input), { infrastructureStrategy: input.infrastructureStrategy });
6844
8190
  }
6845
8191
  async function buildV5Artifacts(input) {
6846
8192
  const spec = await buildNormalizedSpecFromConfig(input);
@@ -6865,8 +8211,9 @@ async function buildCompositionArtifacts(compositionConfigPath, contextSpecs) {
6865
8211
  return mergeGeneratedArtifacts([
6866
8212
  ...buildCompositionTypeArtifacts(),
6867
8213
  ...buildCompositionBusArtifacts(),
6868
- ...buildCompositionOutboxArtifacts(),
8214
+ ...buildCompositionOutboxArtifacts(compositionSpec.infrastructure),
6869
8215
  ...buildCompositionAclArtifacts(compositionSpec),
8216
+ ...buildCompositionDependencyManifestArtifacts(compositionSpec),
6870
8217
  ...buildReadModelCompositionArtifacts(composedContextSpecs),
6871
8218
  ...buildCompositionContainerArtifacts(compositionSpec, composedContextSpecs),
6872
8219
  ...buildCompositionRouterArtifacts(compositionSpec, composedContextSpecs),
@@ -6875,4 +8222,4 @@ async function buildCompositionArtifacts(compositionConfigPath, contextSpecs) {
6875
8222
  }
6876
8223
 
6877
8224
  //#endregion
6878
- export { GENERATED_READ_SIDE_SCHEMA_LOGICAL_PATH, SPEC_ARTIFACT_SCHEMA_VERSION, SPEC_DIFF_SCHEMA_VERSION, VENDORED_FILE_MANIFEST, applyOwnershipIfMissing, buildArtifactPath, buildCollisionSafeModulePathNames, buildCompositionAclArtifacts, buildCompositionArtifacts, buildCompositionBusArtifacts, buildCompositionContainerArtifacts, buildCompositionOutboxArtifacts, buildCompositionRouterArtifacts, buildCompositionSpecDiff, buildCompositionSubscriptionArtifacts, buildCompositionTypeArtifacts, buildConsumerAclArtifacts, buildContextInputChecks, buildContextNormalizedSpec, buildContextNormalizedSpecFromConfig, buildContextSpecDiff, buildDrizzleConfig, buildFileArtifactTags, buildMaterializationManifestTags, buildMigrationResultTags, buildNormalizedCompositionSpec, buildNormalizedSpec, buildNormalizedSpecFromConfig, buildNormalizedSpecTags, buildProjectionArtifacts, buildReadModelCompositionArtifacts, buildSpecDiffTags, buildSummaryTags, buildV5ApplicationArtifacts, buildV5ApplicationContextArtifacts, buildV5Artifacts, buildV5ContextArtifacts, buildV5DomainArtifacts, buildV5InfrastructureArtifacts, buildV5InfrastructureContextArtifacts, buildV5LibArtifacts, buildV5PortArtifacts, buildV5PresentationArtifacts, buildV5RouteArtifacts, buildV5SharedKernelArtifacts, buildV5TestArtifacts, buildVendoredFileTags, camelCase, collectOwnedValueObjects, compositionSpecDiffDataName, contextArtifactOwnership, contextSpecDiffDataName, contractFileName, createGeneratedArtifact, discoverReferencedVos, ensureContextSupportFilesExist, filterPerContextArtifactsForComposition, flattenEntities, generateContextArtifacts, inferArtifactOwnership, inferCodegenSupportPathForContext, inferCodegenSupportPathFromSchemaFile, inferReconcileScopes, introspectObjectShape, introspectSchema, isRegisteredEntity, kebabCase, loadContextConfig, mergeGeneratedArtifacts, mergeSchemaReadResults, normalizeModulePath, parseConnectionString, parseDrizzleKitOutput, pascalCase, prepareAllVendoredFiles, prepareVendoredFile, readSchemaFile, repositoryPortFileName, repositoryPortTypeName, resolveAbsoluteContextInputs, resolveArtifactOwnership, resolveContextInputs, resolveFactoryPath, resolveGeneratedMigrationSchemaPath, resolveMethodContextInputsForCheck, resolveSchemaFilePathsForContext, resolveVoOwner, rewriteImports, runMigration, serializeCompositionSpec, serializeContextSpec, sharedArtifactOwnership, sliceArtifactOwnership, sliceContextIntoAggregateViews, snakeCase, snakeUpperCase, toImportURL, toUpperSnakeCase, unwrapFieldType, validateContextImports, walkEntityTree, withArtifactOwnership, wordsFromName };
8225
+ export { GENERATED_READ_SIDE_SCHEMA_LOGICAL_PATH, SPEC_ARTIFACT_SCHEMA_VERSION, SPEC_DIFF_SCHEMA_VERSION, VENDORED_FILE_MANIFEST, applyOwnershipIfMissing, buildArtifactPath, buildCollisionSafeModulePathNames, buildCompositionAclArtifacts, buildCompositionArtifacts, buildCompositionBusArtifacts, buildCompositionContainerArtifacts, buildCompositionDependencyManifestArtifacts, buildCompositionOutboxArtifacts, buildCompositionRouterArtifacts, buildCompositionSpecDiff, buildCompositionSubscriptionArtifacts, buildCompositionTypeArtifacts, buildConsumerAclArtifacts, buildContextInputChecks, buildContextNormalizedSpec, buildContextNormalizedSpecFromConfig, buildContextSpecDiff, buildDrizzleConfig, buildFileArtifactTags, buildMaterializationManifestTags, buildMigrationResultTags, buildNormalizedCompositionSpec, buildNormalizedSpec, buildNormalizedSpecFromConfig, buildNormalizedSpecTags, buildProjectionArtifacts, buildReadModelCompositionArtifacts, buildSpecDiffTags, buildSummaryTags, buildV5ApplicationArtifacts, buildV5ApplicationContextArtifacts, buildV5Artifacts, buildV5ContextArtifacts, buildV5DomainArtifacts, buildV5InfrastructureArtifacts, buildV5InfrastructureContextArtifacts, buildV5LibArtifacts, buildV5PortArtifacts, buildV5PresentationArtifacts, buildV5RouteArtifacts, buildV5SharedKernelArtifacts, buildV5TestArtifacts, buildVendoredFileTags, camelCase, collectOwnedValueObjects, compositionSpecDiffDataName, contextArtifactOwnership, contextSpecDiffDataName, contractFileName, createGeneratedArtifact, discoverReferencedVos, ensureContextSupportFilesExist, filterPerContextArtifactsForComposition, flattenEntities, generateContextArtifacts, inferArtifactOwnership, inferCodegenSupportPathForContext, inferCodegenSupportPathFromSchemaFile, inferReconcileScopes, introspectObjectShape, introspectSchema, isRegisteredEntity, kebabCase, loadContextConfig, mergeGeneratedArtifacts, mergeSchemaReadResults, normalizeModulePath, parseConnectionString, parseDrizzleKitOutput, pascalCase, prepareAllVendoredFiles, prepareVendoredFile, readSchemaFile, repositoryPortFileName, repositoryPortTypeName, resolveAbsoluteContextInputs, resolveArtifactOwnership, resolveContextInputs, resolveFactoryPath, resolveGeneratedMigrationSchemaPath, resolveMethodContextInputsForCheck, resolveSchemaFilePathsForContext, resolveVoOwner, rewriteImports, runMigration, serializeCompositionSpec, serializeContextSpec, sharedArtifactOwnership, sliceArtifactOwnership, sliceContextIntoAggregateViews, snakeCase, snakeUpperCase, toImportURL, toUpperSnakeCase, unwrapFieldType, validateContextImports, walkEntityTree, withArtifactOwnership, wordsFromName };