@zodmire/core 0.1.1 → 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 +37 -5
  2. package/mod.mjs +1579 -248
  3. package/package.json +2 -2
package/mod.mjs CHANGED
@@ -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
  }
@@ -2073,7 +2236,7 @@ function queryOutputExportName$1(query) {
2073
2236
  return query.outputSchemaExportName?.trim() || void 0;
2074
2237
  }
2075
2238
  function queryReadModelPortTypeName$1(query) {
2076
- return `${readModelContractName$3(resolvedReadModelName$2(query))}RepositoryPort`;
2239
+ return `${readModelContractName$2(resolvedReadModelName$2(query))}RepositoryPort`;
2077
2240
  }
2078
2241
  function queryReadModelPortFileName(query) {
2079
2242
  return `read-models/${readModelPortBaseFileName(resolvedReadModelName$2(query))}.repository.port.ts`;
@@ -2137,7 +2300,7 @@ function createListQueryHandlerStub(_spec, query) {
2137
2300
  const readModelPortType = queryReadModelPortTypeName$1(query);
2138
2301
  const readModelVariable = queryReadModelVariableName$1(query);
2139
2302
  const readModelMethodName = queryReadModelMethodName$1(query);
2140
- const viewFileBase = queryViewFileBase$2(query);
2303
+ const viewFileBase = queryViewFileBase$1(query);
2141
2304
  return `import type { ${queryTypeName} } from "./${kebabCase(query.name)}.query.ts";
2142
2305
  import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";
2143
2306
  import type { ${readModelPortType} } from "../ports/${queryReadModelPortFileName(query)}";
@@ -2165,12 +2328,14 @@ function createHandlerDepsStub(spec) {
2165
2328
  const repoPort = findRepositoryPort(spec);
2166
2329
  const repoTypeName = repoPort ? repositoryPortTypeName(repoPort.name) : `${aggregatePascal}Repository`;
2167
2330
  return `import type { Transaction } from "../../../../lib/transaction.ts";
2331
+ import type { AggregateEventTracker } from "../../../shared-kernel/events/aggregate-event-tracker.ts";
2168
2332
  import type { EventCollector } from "../../../shared-kernel/events/event-collector.ts";
2169
2333
  import type { ${repoTypeName} } from "./ports/${repoPort ? repositoryPortFileName(repoPort.name) : `${kebabCase(spec.aggregate.name)}-repository.port.ts`}";
2170
2334
 
2171
2335
  export type ${contextPascal}CommandHandlerDeps = {
2172
2336
  tx: Transaction;
2173
2337
  eventCollector: EventCollector;
2338
+ aggregateEventTracker: AggregateEventTracker;
2174
2339
  repos: {
2175
2340
  ${aggregateVar}s: ${repoTypeName};
2176
2341
  };
@@ -2207,9 +2372,9 @@ ${query.outputFields.map((f) => {
2207
2372
  function queryOutputContractName$1(query) {
2208
2373
  const outputExportName = queryOutputExportName$1(query);
2209
2374
  if (outputExportName) return pascalCase(outputExportName);
2210
- return readModelContractName$3(resolvedReadModelName$2(query));
2375
+ return readModelContractName$2(resolvedReadModelName$2(query));
2211
2376
  }
2212
- function queryViewFileBase$2(query) {
2377
+ function queryViewFileBase$1(query) {
2213
2378
  const outputExportName = queryOutputExportName$1(query);
2214
2379
  if (outputExportName) return kebabCase(outputExportName).replace(/-view$/, "");
2215
2380
  return kebabCase(resolvedReadModelName$2(query)).replace(/-view$/, "");
@@ -2260,7 +2425,8 @@ function buildCreateHandlerBody(spec, command) {
2260
2425
  const assignedValue = createPropAssignments.get(fieldName) ?? (commandInputFieldNames.has(fieldName) ? commandInputField?.optional && !field.optional ? `command.payload.${fieldName} ?? ${defaultValue}` : `command.payload.${fieldName}` : defaultValue);
2261
2426
  if (assignedValue !== void 0) propsLines.push(` ${fieldName}: ${assignedValue},`);
2262
2427
  }
2263
- const idTypeName = hasCustomIdType ? pascalCase(idType) : `BrandedId<"string">`;
2428
+ const defaultIdBrand = `${pascalCase(spec.aggregate.name)}Id`;
2429
+ const idTypeName = hasCustomIdType ? pascalCase(idType) : `BrandedId<"${defaultIdBrand}">`;
2264
2430
  const creationEventName = command.emits?.[0];
2265
2431
  const creationEvent = creationEventName ? spec.domainEvents.find((e) => pascalCase(e.name) === pascalCase(creationEventName)) : void 0;
2266
2432
  const aggregateFieldNames = new Set((spec.aggregate.fields ?? []).map((field) => camelCase(field.name)));
@@ -2291,7 +2457,7 @@ function buildCreateHandlerBody(spec, command) {
2291
2457
  const payloadName = `${pascalCase(creationEventName)}Payload`;
2292
2458
  imports.push(`import type { ${payloadName} } from "../../domain/events/${kebabCase(creationEventName)}.event.ts";`);
2293
2459
  }
2294
- 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())`;
2295
2461
  let creationEventLines = "";
2296
2462
  if (creationEvent) {
2297
2463
  const payloadName = `${pascalCase(creationEventName)}Payload`;
@@ -2310,7 +2476,7 @@ function buildCreateHandlerBody(spec, command) {
2310
2476
  else if (fieldName === "recordedBy" || fieldName === "correlationId" || fieldName === "causationId") {
2311
2477
  const metadataExpr = fieldName === "recordedBy" ? f.type === "string" ? "eventContext.recordedBy.value" : "eventContext.recordedBy" : `eventContext.${fieldName}`;
2312
2478
  payloadFields.push(` ${fieldName}: ${metadataExpr},`);
2313
- } else payloadFields.push(` ${fieldName}: undefined as unknown as ${payloadName}["${fieldName}"],`);
2479
+ } else payloadFields.push(` ${fieldName}: /* TODO: unmapped ${payloadName} field "${fieldName}" */ undefined as never,`);
2314
2480
  }
2315
2481
  creationEventLines = `
2316
2482
  deps.eventCollector.collect([
@@ -2387,7 +2553,7 @@ function buildMutationHandlerBody(spec, command) {
2387
2553
  }
2388
2554
  imports.push(`import type { ${notFoundErrorType} } from "../../domain/errors/${aggregateDir}-application-errors.ts";`);
2389
2555
  if (hasCustomIdType && idCreatorFn && idType) imports.push(`import { ${idCreatorFn} } from "../../../../shared-kernel/entity-ids/${kebabCase(idType)}.ts";`);
2390
- const domainMethodArg = cmdEmitsEvents ? "command.payload as any, eventContext" : "command.payload as any";
2556
+ const domainMethodArg = cmdEmitsEvents ? "command.payload, eventContext" : "command.payload";
2391
2557
  const loadByField = command.loadBy?.startsWith("input.") ? command.loadBy.slice(6) : aggregateId;
2392
2558
  const brandedIdExpr = hasCustomIdType && idCreatorFn ? `${idCreatorFn}(command.payload.${loadByField})` : `command.payload.${loadByField}`;
2393
2559
  let bodyLines;
@@ -2399,7 +2565,7 @@ function buildMutationHandlerBody(spec, command) {
2399
2565
  ` const result = ${aggregateVar}.${methodName}(${domainMethodArg});`,
2400
2566
  ` if (!result.ok) return result;`,
2401
2567
  ` await deps.repos.${camelCase(spec.aggregate.name)}s.save(${aggregateVar}, loadedVersion, deps.tx);`,
2402
- ` deps.eventCollector.collect(${aggregateVar}.pullDomainEvents());`,
2568
+ ` deps.aggregateEventTracker.track(${aggregateVar});`,
2403
2569
  ` return ok(undefined);`
2404
2570
  ];
2405
2571
  else bodyLines = [
@@ -2409,7 +2575,7 @@ function buildMutationHandlerBody(spec, command) {
2409
2575
  ` const loadedVersion = ${aggregateVar}.version;`,
2410
2576
  ` ${aggregateVar}.${methodName}(${domainMethodArg});`,
2411
2577
  ` await deps.repos.${camelCase(spec.aggregate.name)}s.save(${aggregateVar}, loadedVersion, deps.tx);`,
2412
- ` deps.eventCollector.collect(${aggregateVar}.pullDomainEvents());`,
2578
+ ` deps.aggregateEventTracker.track(${aggregateVar});`,
2413
2579
  ` return ok(undefined);`
2414
2580
  ];
2415
2581
  const handlerErrorTypeExport = hasPreconditions ? `export type ${handlerErrorType} =\n | ${errorTypes.join("\n | ")};\n` : `export type ${handlerErrorType} = ${notFoundErrorType};\n`;
@@ -2442,11 +2608,14 @@ export async function ${handlerFnName}(
2442
2608
  command: ${commandTypeName},
2443
2609
  deps: ${depsTypeName},
2444
2610
  eventContext: EventContext,
2445
- ): Promise<void> {
2611
+ ): Promise<never> {
2446
2612
  void command;
2447
2613
  void deps;
2448
2614
  void eventContext;
2449
- // 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
+ );
2450
2619
  }
2451
2620
  `;
2452
2621
  }
@@ -2468,7 +2637,7 @@ function buildQueryHandlerBody(_spec, query) {
2468
2637
  const handlerName = `${pascalCase(query.name)}Handler`;
2469
2638
  const readModelVariable = queryReadModelVariableName$1(query);
2470
2639
  const readModelMethodName = queryReadModelMethodName$1(query);
2471
- const viewFileBase = queryViewFileBase$2(query);
2640
+ const viewFileBase = queryViewFileBase$1(query);
2472
2641
  return `import type { ${queryTypeName} } from "./${kebabCase(query.name)}.query.ts";
2473
2642
  import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";
2474
2643
  import type { ${readModelPortType} } from "../ports/${queryReadModelPortFileName(query)}";
@@ -2494,7 +2663,7 @@ function createQueryHandlerStub(_spec, query) {
2494
2663
  const handlerName = `${pascalCase(query.name)}Handler`;
2495
2664
  const readModelVariable = queryReadModelVariableName$1(query);
2496
2665
  const readModelMethodName = queryReadModelMethodName$1(query);
2497
- const viewFileBase = queryViewFileBase$2(query);
2666
+ const viewFileBase = queryViewFileBase$1(query);
2498
2667
  return `import type { ${queryTypeName} } from "./${kebabCase(query.name)}.query.ts";
2499
2668
  import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";
2500
2669
  import type { ${readModelPortType} } from "../ports/${queryReadModelPortFileName(query)}";
@@ -2557,46 +2726,71 @@ function groupQueriesByReadModel$1(queries) {
2557
2726
  }));
2558
2727
  }
2559
2728
  function createReadModelRepositoryPortStub(readModelName, queries) {
2560
- const portTypeName = `${readModelContractName$3(readModelName)}RepositoryPort`;
2729
+ const portTypeName = `${readModelContractName$2(readModelName)}RepositoryPort`;
2561
2730
  const importLines = /* @__PURE__ */ new Map();
2562
2731
  let needsPaginatedResult = false;
2563
- importLines.set("tx", `import type { Transaction } from "../../../../../lib/transaction.ts";`);
2732
+ importLines.set("tx", `import type { Transaction } from "../../../../../../lib/transaction.ts";`);
2564
2733
  const queryMethodLines = queries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((query) => {
2565
2734
  const queryTypeName = `${pascalCase(query.name)}Query`;
2566
2735
  const outputContractName = queryOutputContractName$1(query);
2567
- const viewFileBase = queryViewFileBase$2(query);
2736
+ const viewFileBase = queryViewFileBase$1(query);
2568
2737
  const returnType = query.queryKind === "list" ? `PaginatedResult<${outputContractName}>` : outputContractName;
2569
2738
  if (query.queryKind === "list") needsPaginatedResult = true;
2570
- importLines.set(`query:${queryTypeName}`, `import type { ${queryTypeName} } from "../queries/${kebabCase(query.name)}.query.ts";`);
2571
- 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";`);
2572
2741
  return ` ${queryReadModelMethodName$1(query)}(query: ${queryTypeName}, tx: Transaction): Promise<${returnType}>;`;
2573
2742
  });
2574
- 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";`);
2575
2744
  return `${[...importLines.values()].join("\n")}
2576
2745
 
2577
2746
  // Read-model repository port. Query handlers share this contract by read-model, not by query.
2578
2747
  export interface ${portTypeName} {
2579
- // Query methods
2580
2748
  ${queryMethodLines.join("\n")}
2581
-
2582
- // Projector methods
2583
- // TODO: add typed row write methods when projector/repository generation lands.
2584
-
2585
- // Rebuild methods
2586
- // TODO: add rebuild helpers when projection rebuild scaffolding lands.
2587
2749
  }
2588
2750
  `;
2589
2751
  }
2590
2752
  function createReadModelPortsIndexStub(queries) {
2591
2753
  return `${groupQueriesByReadModel$1(queries).map(({ readModelName }) => {
2592
- 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";`;
2593
2755
  }).join("\n")}\n`;
2594
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
+ }
2595
2789
  function inputContractFileName(commandOrQueryName) {
2596
2790
  return `${kebabCase(commandOrQueryName)}.input`;
2597
2791
  }
2598
2792
  function viewContractFileName(query) {
2599
- return `${queryViewFileBase$2(query)}.view`;
2793
+ return `${queryViewFileBase$1(query)}.view`;
2600
2794
  }
2601
2795
  function createHandlerMapStub(spec) {
2602
2796
  const contextPascal = pascalCase(spec.context.name);
@@ -2619,7 +2813,11 @@ ${mapEntries.join("\n")}
2619
2813
  }
2620
2814
  function createContextHandlerDepsStub(contextSpec) {
2621
2815
  const contextPascal = pascalCase(contextSpec.context.name);
2622
- 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
+ ];
2623
2821
  const repoEntries = [];
2624
2822
  const aclEntries = [];
2625
2823
  for (const agg of contextSpec.aggregates) {
@@ -2645,6 +2843,7 @@ ${aclEntries.join("\n")}
2645
2843
  export type ${contextPascal}CommandHandlerDeps = {
2646
2844
  tx: Transaction;
2647
2845
  eventCollector: EventCollector;
2846
+ aggregateEventTracker: AggregateEventTracker;
2648
2847
  repos: {
2649
2848
  ${repoEntries.join("\n")}
2650
2849
  };
@@ -2690,7 +2889,7 @@ function createPublishedReadBoundaryStub(contextSpec) {
2690
2889
  const queryTypeName = `${pascalCase(query.name)}Query`;
2691
2890
  const outputContractName = queryOutputContractName$1(query);
2692
2891
  const queryFileName = kebabCase(query.name);
2693
- const outputFileName = queryViewFileBase$2(query);
2892
+ const outputFileName = queryViewFileBase$1(query);
2694
2893
  const readModelDepName = queryReadModelVariableName$1(query);
2695
2894
  const readModelTypeName = queryReadModelPortTypeName$1(query);
2696
2895
  imports.set(`handler:${queryHandlerName}`, `import { ${queryHandlerName} } from "./queries/${queryFileName}.handler.ts";`);
@@ -2772,16 +2971,25 @@ function buildV5ApplicationArtifacts(spec, options) {
2772
2971
  }
2773
2972
  function buildV5ApplicationContextArtifacts(contextSpec) {
2774
2973
  const modulePath = normalizeModulePath(contextSpec.context.modulePath);
2775
- return [
2974
+ const artifacts = [
2776
2975
  createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/ports/read-models/index.ts"), createReadModelPortsIndexStub(contextSpec.queries)),
2777
2976
  createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/handler-deps.ts"), createContextHandlerDepsStub(contextSpec)),
2778
2977
  createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/read-boundary.ts"), createPublishedReadBoundaryStub(contextSpec)),
2779
2978
  createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/handler-map.ts"), createContextHandlerMapStub(contextSpec))
2780
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;
2781
2986
  }
2782
2987
 
2783
2988
  //#endregion
2784
2989
  //#region packages/core/generators/infrastructure.ts
2990
+ function resolveDrizzlePersistence(infrastructureStrategy) {
2991
+ return infrastructureStrategy?.persistence === "mysql" ? "mysql" : "postgres";
2992
+ }
2785
2993
  function isDecimalNumberField(field) {
2786
2994
  if (field.type !== "number") return false;
2787
2995
  const explicitColumnType = field.columnType?.toLowerCase();
@@ -2796,10 +3004,12 @@ function isDecimalNumberField(field) {
2796
3004
  const fieldName = snakeCase(field.name);
2797
3005
  return /(^|_)(cpk|ppk|ucl|lcl)(_|$)/.test(fieldName) || /(percent|percentage|ratio|rate|average|avg|variance|yield|scrap|drift)/.test(fieldName);
2798
3006
  }
2799
- function drizzleColumnBuilder(field, columnName) {
3007
+ function drizzleColumnBuilder(field, columnName, persistence = "postgres") {
2800
3008
  let builder;
2801
- if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0) builder = `jsonb("${columnName}").$type<${formatTsFieldType(field)}>()`;
2802
- 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) {
2803
3013
  case "number":
2804
3014
  builder = isDecimalNumberField(field) ? `real("${columnName}")` : `integer("${columnName}")`;
2805
3015
  break;
@@ -2906,12 +3116,12 @@ function relativePrefixFromRepositoryDirectory(modulePath) {
2906
3116
  ...modulePath.split("/").filter(Boolean)
2907
3117
  ].map(() => "..").join("/");
2908
3118
  }
2909
- function readModelContractName$2(readModelName) {
3119
+ function readModelContractName$1(readModelName) {
2910
3120
  const baseName = pascalCase(readModelName);
2911
3121
  return baseName.endsWith("View") ? baseName : `${baseName}View`;
2912
3122
  }
2913
3123
  function readModelRepositoryClassName(readModelName) {
2914
- return `Drizzle${readModelContractName$2(readModelName)}Repository`;
3124
+ return `Drizzle${readModelContractName$1(readModelName)}Repository`;
2915
3125
  }
2916
3126
  function readModelRepositoryFileBase$1(readModelName) {
2917
3127
  return kebabCase(readModelName).replace(/-view$/, "");
@@ -2919,6 +3129,15 @@ function readModelRepositoryFileBase$1(readModelName) {
2919
3129
  function readModelTableConstName(readModelName) {
2920
3130
  return `${camelCase(readModelName)}Table`;
2921
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
+ }
2922
3141
  function resolvedReadModelName$1(query) {
2923
3142
  return query.readSide?.readModelName ?? query.readModelName ?? query.name;
2924
3143
  }
@@ -2928,9 +3147,9 @@ function queryOutputExportName(query) {
2928
3147
  function queryOutputContractName(query) {
2929
3148
  const outputExportName = queryOutputExportName(query);
2930
3149
  if (outputExportName) return pascalCase(outputExportName);
2931
- return readModelContractName$2(resolvedReadModelName$1(query));
3150
+ return readModelContractName$1(resolvedReadModelName$1(query));
2932
3151
  }
2933
- function queryViewFileBase$1(query) {
3152
+ function queryViewFileBase(query) {
2934
3153
  const outputExportName = queryOutputExportName(query);
2935
3154
  if (outputExportName) return kebabCase(outputExportName).replace(/-view$/, "");
2936
3155
  return readModelRepositoryFileBase$1(resolvedReadModelName$1(query));
@@ -2960,13 +3179,13 @@ const RESERVED_AGGREGATE_FIELD_NAMES = new Set(["version"]);
2960
3179
  function filterManagedAggregateFields(fields) {
2961
3180
  return fields.filter((field) => !RESERVED_AGGREGATE_FIELD_NAMES.has(camelCase(field.name)));
2962
3181
  }
2963
- function buildTableBlock(tableName, tableConstName, fields, idField, fkColumn, isAggregateRoot = false) {
3182
+ function buildTableBlock(tableName, tableConstName, fields, idField, fkColumn, isAggregateRoot = false, persistence = "postgres") {
2964
3183
  const columnDefs = [];
2965
3184
  const emittedColumns = /* @__PURE__ */ new Set();
2966
3185
  for (const field of fields) {
2967
3186
  const snakeName = snakeCase(field.name);
2968
3187
  if (emittedColumns.has(snakeName)) continue;
2969
- const columnDef = field.name === idField ? `text("${snakeName}").primaryKey()` : drizzleColumnBuilder(field, snakeName);
3188
+ const columnDef = field.name === idField ? `text("${snakeName}").primaryKey()` : drizzleColumnBuilder(field, snakeName, persistence);
2970
3189
  columnDefs.push(` ${snakeName}: ${columnDef},`);
2971
3190
  emittedColumns.add(snakeName);
2972
3191
  }
@@ -2975,16 +3194,16 @@ function buildTableBlock(tableName, tableConstName, fields, idField, fkColumn, i
2975
3194
  emittedColumns.add(fkColumn.name);
2976
3195
  }
2977
3196
  if (isAggregateRoot) columnDefs.push(` version: integer("version").default(0).notNull(),`);
2978
- 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});`;
2979
3198
  }
2980
- function createDrizzleTableDefinition(spec) {
3199
+ function createDrizzleTableDefinition(spec, persistence = "postgres") {
2981
3200
  const fields = filterManagedAggregateFields(spec.aggregate.fields);
2982
3201
  if (!fields || fields.length === 0) return null;
2983
3202
  const children = spec.aggregate.children ?? [];
2984
3203
  const aggScalarFields = scalarFields(fields, children);
2985
3204
  const emittedTableNames = /* @__PURE__ */ new Set();
2986
3205
  const blocks = [];
2987
- 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));
2988
3207
  emittedTableNames.add(spec.aggregate.tableName);
2989
3208
  const aggregate = {
2990
3209
  name: spec.aggregate.name,
@@ -3001,13 +3220,13 @@ function createDrizzleTableDefinition(spec) {
3001
3220
  blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, {
3002
3221
  name: fkColName,
3003
3222
  type: "text"
3004
- }));
3223
+ }, false, persistence));
3005
3224
  emittedTableNames.add(entity.tableName);
3006
3225
  });
3007
3226
  for (const entity of spec.entities ?? []) {
3008
3227
  if (!entity.tableName || emittedTableNames.has(entity.tableName)) continue;
3009
3228
  const entityScalarFields = scalarFields(entity.fields, entity.children ?? []);
3010
- 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));
3011
3230
  emittedTableNames.add(entity.tableName);
3012
3231
  }
3013
3232
  const flatScalarFields = [
@@ -3015,12 +3234,12 @@ function createDrizzleTableDefinition(spec) {
3015
3234
  ...flattenEntities(aggregate).map((entity) => scalarFields(entity.fields, entity.children)),
3016
3235
  ...(spec.entities ?? []).map((entity) => scalarFields(entity.fields, entity.children ?? []))
3017
3236
  ].flat();
3018
- const pgCoreImportTokens = ["pgTable", "text"];
3019
- if (flatScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) pgCoreImportTokens.push("integer");
3020
- if (flatScalarFields.some((field) => isDecimalNumberField(field))) pgCoreImportTokens.push("real");
3021
- if (flatScalarFields.some((field) => field.type === "boolean")) pgCoreImportTokens.push("boolean");
3022
- if (flatScalarFields.some((field) => field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0)) pgCoreImportTokens.push("jsonb");
3023
- 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`;
3024
3243
  }
3025
3244
  function buildCodecBlock(_spec, entityName, fields, options) {
3026
3245
  const varName = camelCase(entityName);
@@ -3091,6 +3310,280 @@ function createPersistenceEqualityBlock(name, fields, options) {
3091
3310
  return `// Compiled from zx.deepEqual.writeable at generator time using the canonical ${pascalCase(name)} persistence row shape.
3092
3311
  export const ${equalityName} = ${compiledEquality};`;
3093
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
+ }
3094
3587
  function childTableVar(spec, tableName) {
3095
3588
  const child = (spec.aggregate.children ?? []).find((c) => c.tableName === tableName);
3096
3589
  return child ? `${camelCase(child.name)}Table` : `${camelCase(tableName)}Table`;
@@ -3748,29 +4241,39 @@ function buildV5InfrastructureArtifacts(spec, options) {
3748
4241
  const modulePath = normalizeModulePath(spec.context.modulePath);
3749
4242
  const scopeKey = sliceArtifactOwnership(modulePath);
3750
4243
  const artifacts = [];
3751
- if (!options?.skipContextWideArtifacts) {
3752
- 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));
3753
4248
  if (tableContent !== null) {
3754
4249
  const tableLogicalPath = `infrastructure/persistence/${modulePath}/tables.ts`;
3755
4250
  artifacts.push(createGeneratedArtifact(tableLogicalPath, tableContent, scopeKey));
3756
4251
  }
3757
4252
  }
3758
- const repoArtifacts = spec.adapters.filter((adapter) => adapter.kind === "drizzle-repository").flatMap((adapter) => {
4253
+ const repoArtifacts = spec.adapters.flatMap((adapter) => {
3759
4254
  const port = spec.ports.find((p) => p.name === adapter.port);
3760
4255
  if (port === void 0) return [];
3761
- const logicalPath = buildArtifactPath("infrastructure/persistence/drizzle/repositories", modulePath, `drizzle-${kebabCase(spec.aggregate.name)}.repository.ts`);
3762
- 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 [];
3763
4266
  });
3764
4267
  artifacts.push(...repoArtifacts);
3765
4268
  return artifacts.sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
3766
4269
  }
3767
- function createContextDrizzleTableDefinition(contextSpec) {
4270
+ function createContextDrizzleTableDefinition(contextSpec, persistence = "postgres") {
3768
4271
  const blocks = [];
3769
4272
  const emittedTableNames = /* @__PURE__ */ new Set();
3770
4273
  let usesInteger = false;
3771
4274
  let usesReal = false;
3772
4275
  let usesBoolean = false;
3773
- let usesJsonb = false;
4276
+ let usesJson = false;
3774
4277
  for (const agg of contextSpec.aggregates) {
3775
4278
  const fields = filterManagedAggregateFields(agg.fields);
3776
4279
  if (!fields || fields.length === 0) continue;
@@ -3779,8 +4282,8 @@ function createContextDrizzleTableDefinition(contextSpec) {
3779
4282
  if (aggScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
3780
4283
  if (aggScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
3781
4284
  if (aggScalarFields.some((field) => field.type === "boolean")) usesBoolean = true;
3782
- if (aggScalarFields.some((field) => field.array)) usesJsonb = true;
3783
- 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));
3784
4287
  emittedTableNames.add(agg.tableName);
3785
4288
  walkEntityTree({
3786
4289
  name: agg.name,
@@ -3795,12 +4298,12 @@ function createContextDrizzleTableDefinition(contextSpec) {
3795
4298
  if (entityScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
3796
4299
  if (entityScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
3797
4300
  if (entityScalarFields.some((field) => field.type === "boolean")) usesBoolean = true;
3798
- if (entityScalarFields.some((field) => field.array)) usesJsonb = true;
4301
+ if (entityScalarFields.some((field) => field.array)) usesJson = true;
3799
4302
  const fkColName = snakeCase("idField" in ctx.parent ? ctx.parent.idField : agg.idField);
3800
4303
  blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, {
3801
4304
  name: fkColName,
3802
4305
  type: "text"
3803
- }));
4306
+ }, false, persistence));
3804
4307
  emittedTableNames.add(entity.tableName);
3805
4308
  });
3806
4309
  }
@@ -3810,21 +4313,22 @@ function createContextDrizzleTableDefinition(contextSpec) {
3810
4313
  if (entityScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
3811
4314
  if (entityScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
3812
4315
  if (entityScalarFields.some((field) => field.type === "boolean")) usesBoolean = true;
3813
- if (entityScalarFields.some((field) => field.array)) usesJsonb = true;
3814
- 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));
3815
4318
  emittedTableNames.add(entity.tableName);
3816
4319
  }
3817
4320
  if (blocks.length === 0) return null;
3818
- const pgCoreImportTokens = ["pgTable", "text"];
3819
- if (usesInteger) pgCoreImportTokens.push("integer");
3820
- if (usesReal) pgCoreImportTokens.push("real");
3821
- if (usesBoolean) pgCoreImportTokens.push("boolean");
3822
- if (usesJsonb) pgCoreImportTokens.push("jsonb");
3823
- return `import { ${pgCoreImportTokens.join(", ")} } from "drizzle-orm/pg-core";\n\n${blocks.join("\n\n")}\n`;
3824
- }
3825
- 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";
3826
4330
  const importTokens = new Set([
3827
- "pgTable",
4331
+ tableBuilder,
3828
4332
  "primaryKey",
3829
4333
  "text"
3830
4334
  ]);
@@ -3832,8 +4336,8 @@ function createReadModelTableDefinition(readModel) {
3832
4336
  if (field.type === "number" && !isDecimalNumberField(field)) importTokens.add("integer");
3833
4337
  if (isDecimalNumberField(field)) importTokens.add("real");
3834
4338
  if (field.type === "boolean") importTokens.add("boolean");
3835
- if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0) importTokens.add("jsonb");
3836
- 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)},`;
3837
4341
  });
3838
4342
  if (readModel.indexes.length > 0) importTokens.add("index");
3839
4343
  if (readModel.uniqueConstraints.length > 0) importTokens.add("uniqueIndex");
@@ -3842,9 +4346,9 @@ function createReadModelTableDefinition(readModel) {
3842
4346
  for (const uniqueConstraint of readModel.uniqueConstraints) constraintLines.push(` ${camelCase(uniqueConstraint.name)}: uniqueIndex("${uniqueConstraint.name}").on(${uniqueConstraint.fields.map((field) => `table.${snakeCase(field)}`).join(", ")}),`);
3843
4347
  const tableConstName = readModelTableConstName(readModel.name);
3844
4348
  const typeName = pascalCase(readModel.name);
3845
- 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"}";
3846
4350
 
3847
- export const ${tableConstName} = pgTable("${readModel.tableName}", {
4351
+ export const ${tableConstName} = ${tableBuilder}("${readModel.tableName}", {
3848
4352
  ${columnLines.join("\n")}
3849
4353
  }, (table) => ({
3850
4354
  ${constraintLines.join("\n")}
@@ -3889,22 +4393,139 @@ function buildReadModelCompositionArtifacts(contextSpecs) {
3889
4393
  };
3890
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)];
3891
4395
  }
3892
- 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 = {}) {
3893
4503
  const modulePath = normalizeModulePath(contextSpec.context.modulePath);
3894
4504
  const scopeKey = sliceArtifactOwnership(modulePath);
3895
4505
  const artifacts = [];
3896
4506
  const readModels = contextSpec.readModels ?? [];
3897
- const tableContent = createContextDrizzleTableDefinition(contextSpec);
4507
+ const tableContent = createContextDrizzleTableDefinition(contextSpec, resolveDrizzlePersistence(options.infrastructureStrategy));
3898
4508
  if (tableContent !== null) artifacts.push(createGeneratedArtifact(`infrastructure/persistence/${modulePath}/tables.ts`, tableContent, scopeKey));
3899
4509
  if (readModels.length > 0) {
3900
- 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));
3901
4511
  artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/drizzle/schema.ts`, createReadModelContextSchemaBarrel({
3902
4512
  ...contextSpec,
3903
4513
  readModels
3904
4514
  }), scopeKey));
3905
4515
  artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/tables.ts`, createReadModelCompatibilityBarrel(), scopeKey));
3906
4516
  } else artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/tables.ts`, createViewModelTablesSkeleton(contextSpec), scopeKey));
3907
- 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
+ }
3908
4529
  return artifacts;
3909
4530
  }
3910
4531
  function createViewModelTablesSkeleton(contextSpec) {
@@ -3919,7 +4540,7 @@ function createViewModelTablesSkeleton(contextSpec) {
3919
4540
  for (const { readModelName, queries } of groupQueriesByReadModel(contextSpec.queries)) {
3920
4541
  lines.push(` ${camelCase(readModelName)}: {`);
3921
4542
  lines.push(` readModel: "${readModelName}",`);
3922
- lines.push(` contract: "${readModelContractName$2(readModelName)}",`);
4543
+ lines.push(` contract: "${readModelContractName$1(readModelName)}",`);
3923
4544
  lines.push(` servesQueries: [${queries.map((query) => `"${query.name}"`).join(", ")}],`);
3924
4545
  lines.push(` suggestedTableName: "${snakeCase(modulePath).replaceAll("/", "_")}_${snakeCase(readModelRepositoryFileBase$1(readModelName))}",`);
3925
4546
  lines.push(` },`);
@@ -3928,40 +4549,33 @@ function createViewModelTablesSkeleton(contextSpec) {
3928
4549
  lines.push(``);
3929
4550
  return lines.join("\n");
3930
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
+ }
3931
4562
  function createReadModelRepositorySkeleton(modulePath, readModelName, queries) {
3932
4563
  const className = readModelRepositoryClassName(readModelName);
3933
- const portTypeName = `${readModelContractName$2(readModelName)}RepositoryPort`;
4564
+ const portTypeName = `${readModelContractName$1(readModelName)}RepositoryPort`;
3934
4565
  const fileBase = readModelRepositoryFileBase$1(readModelName);
3935
4566
  const importLines = /* @__PURE__ */ new Map();
3936
4567
  let needsPaginatedResult = false;
3937
4568
  importLines.set("tx", `import type { Transaction } from "../../../../../lib/transaction.ts";`);
3938
4569
  importLines.set("port", `import type { ${portTypeName} } from "../../../../../core/contexts/${modulePath}/application/ports/read-models/${fileBase}.repository.port.ts";`);
3939
4570
  const methodBlocks = queries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((query) => {
3940
- const methodName = queryReadModelMethodName(query);
4571
+ queryReadModelMethodName(query);
3941
4572
  const queryTypeName = `${pascalCase(query.name)}Query`;
3942
4573
  const outputTypeName = queryOutputContractName(query);
3943
- const viewFileBase = queryViewFileBase$1(query);
4574
+ const viewFileBase = queryViewFileBase(query);
3944
4575
  importLines.set(`query:${queryTypeName}`, `import type { ${queryTypeName} } from "../../../../../core/contexts/${modulePath}/application/queries/${kebabCase(query.name)}.query.ts";`);
3945
4576
  importLines.set(`view:${outputTypeName}`, `import type { ${outputTypeName} } from "../../../../../core/contexts/${modulePath}/application/contracts/${viewFileBase}.view.ts";`);
3946
- if (query.queryKind === "list") {
3947
- needsPaginatedResult = true;
3948
- return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<PaginatedResult<${outputTypeName}>> {
3949
- void this.db;
3950
- void tx;
3951
- const { page = 1, pageSize = 20 } = query.payload;
3952
- return { items: [], total: 0, page, pageSize };
3953
- }`;
3954
- }
3955
- return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<${outputTypeName}> {
3956
- void this.db;
3957
- void query;
3958
- void tx;
3959
- return {
3960
- ${query.outputFields.map((field) => {
3961
- return ` ${camelCase(field.name)}: ${placeholderValueForField(field)}, // TODO: map from projection row`;
3962
- }).join("\n")}
3963
- };
3964
- }`;
4577
+ if (query.queryKind === "list") needsPaginatedResult = true;
4578
+ return createSingleReadModelMethodSkeleton(query, readModelName);
3965
4579
  });
3966
4580
  if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "../../../../../lib/pagination.ts";`);
3967
4581
  return `${[...importLines.values()].join("\n")}
@@ -3974,18 +4588,229 @@ ${methodBlocks.join("\n\n")}
3974
4588
  }
3975
4589
  `;
3976
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")}
4625
+
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")}
4631
+ }
4632
+ `;
4633
+ }
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;
4661
+ }
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
+ }
3977
4806
 
3978
4807
  //#endregion
3979
4808
  //#region packages/core/generators/projections.ts
3980
- function readModelContractName$1(readModelName) {
3981
- const baseName = pascalCase(readModelName);
3982
- return baseName.endsWith("View") ? baseName : `${baseName}View`;
4809
+ function projectionWritePortName(projectionName) {
4810
+ return `${pascalCase(projectionName)}WritePort`;
3983
4811
  }
3984
- function readModelRepositoryPortName(readModelName) {
3985
- return `${readModelContractName$1(readModelName)}RepositoryPort`;
3986
- }
3987
- function readModelRepositoryPortFileBase(readModelName) {
3988
- return kebabCase(readModelName).replace(/-view$/, "");
4812
+ function projectionWritePortVariableName(projectionName) {
4813
+ return camelCase(projectionWritePortName(projectionName));
3989
4814
  }
3990
4815
  function projectionFileBase(projectionName) {
3991
4816
  return kebabCase(projectionName);
@@ -3993,57 +4818,43 @@ function projectionFileBase(projectionName) {
3993
4818
  function projectionVariableName(projectionName) {
3994
4819
  return camelCase(projectionName);
3995
4820
  }
3996
- function readModelRepositoryVariableName(readModelName) {
3997
- return camelCase(readModelRepositoryPortName(readModelName));
3998
- }
3999
4821
  function projectionSourceHandlerName(eventName) {
4000
4822
  return `on${pascalCase(eventName)}`;
4001
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
+ }
4002
4834
  function buildProjectionSourceMethod(projection, source) {
4003
4835
  const methodName = projectionSourceHandlerName(source.eventName);
4004
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.`;
4005
- if (source.mutation.kind === "custom") return ` async ${methodName}(event: unknown, tx: Transaction): Promise<void> {
4006
- return await this.${source.mutation.handlerName}(event, tx);
4007
- }
4008
-
4009
- protected async ${source.mutation.handlerName}(event: unknown, tx: Transaction): Promise<void> {
4010
- void this.${readModelRepositoryVariableName(projection.readModelName)};
4011
- void event;
4012
- void tx;
4013
- ${roleComment}
4014
- // Custom projection hook "${source.mutation.handlerName}" for ${source.contextName}.${source.aggregateName}.${source.eventName}.
4015
- // TODO: implement custom projector logic against the generated read-model repository port.
4016
- }`;
4017
- const setFields = source.mutation.kind === "delete" ? "" : Object.entries(source.mutation.set ?? {}).map(([fieldName, valueSource]) => {
4018
- if (valueSource.kind === "event-field") return ` // ${fieldName} <- event.${valueSource.field}`;
4019
- if (valueSource.kind === "literal") return ` // ${fieldName} <- ${JSON.stringify(valueSource.value)}`;
4020
- return ` // ${fieldName} <- ${valueSource.expression}`;
4021
- }).join("\n");
4022
- const matchOnComment = `"${source.mutation.matchOn.join("\", \"")}"`;
4023
- return ` async ${methodName}(event: unknown, tx: Transaction): Promise<void> {
4024
- void this.${readModelRepositoryVariableName(projection.readModelName)};
4025
- void event;
4026
- void tx;
4837
+ return ` async ${methodName}(event: ${projectionSourceEventType(source)}, tx: Transaction): Promise<void> {
4027
4838
  ${roleComment}
4028
- // Mutation kind: ${source.mutation.kind}
4029
- // Match on: [${matchOnComment}]
4030
- ${setFields}
4031
- // TODO: call generated projector-facing repository methods when write helpers land.
4839
+ return await this.${projectionWritePortVariableName(projection.name)}.${methodName}(event, tx);
4032
4840
  }`;
4033
4841
  }
4034
4842
  function buildProjectorArtifact(modulePath, projection) {
4035
4843
  const projectorClassName = projection.name;
4036
- const repositoryPortName = readModelRepositoryPortName(projection.readModelName);
4037
- const repositoryPortVar = readModelRepositoryVariableName(projection.readModelName);
4844
+ const writePortName = projectionWritePortName(projection.name);
4845
+ const writePortVar = projectionWritePortVariableName(projection.name);
4038
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)}";`);
4039
4848
  const methods = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => buildProjectionSourceMethod(projection, source)).join("\n\n");
4040
- return createGeneratedArtifact(`core/contexts/${modulePath}/application/projections/${fileBase}.projector.ts`, `import type { Transaction } from "../../../../lib/transaction.ts";
4041
- 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")}
4042
4853
 
4043
4854
  // Auto-generated projector skeleton for the "${projection.readModelName}" read model.
4044
4855
  export class ${projectorClassName} {
4045
4856
  constructor(
4046
- private readonly ${repositoryPortVar}: ${repositoryPortName},
4857
+ private readonly ${writePortVar}: ${writePortName},
4047
4858
  ) {}
4048
4859
 
4049
4860
  ${methods}
@@ -4056,20 +4867,53 @@ function buildProjectionRebuildArtifact(modulePath, projection) {
4056
4867
  const projectorClassName = projection.name;
4057
4868
  const rebuildFunctionName = `rebuild${pascalCase(projection.name)}`;
4058
4869
  const batchSize = projection.rebuild.batchSize ?? 500;
4059
- 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";
4060
4871
  import type { ${projectorClassName} } from "./${fileBase}.projector.ts";
4061
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
+
4062
4889
  export type ${pascalCase(projection.name)}RebuildDeps = {
4063
4890
  projector: ${projectorClassName};
4891
+ eventSource: ProjectionRebuildEventSource;
4064
4892
  transaction: Transaction;
4065
4893
  };
4066
4894
 
4067
- // Auto-generated rebuild scaffold for the "${projection.readModelName}" read model.
4895
+ // Auto-generated rebuild runner for the "${projection.readModelName}" read model.
4068
4896
  export async function ${rebuildFunctionName}(deps: ${pascalCase(projection.name)}RebuildDeps): Promise<void> {
4069
- void deps;
4070
- // Strategy: ${projection.rebuild.strategy}
4071
- // Batch size: ${batchSize}
4072
- // 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
+ }
4073
4917
  }
4074
4918
  `, contextArtifactOwnership(modulePath));
4075
4919
  }
@@ -4087,7 +4931,8 @@ function buildProjectionSubscriptionsArtifact(contextSpec) {
4087
4931
  const depsTypeName = `${pascalCase(contextSpec.context.name)}ProjectionSubscriptionDeps`;
4088
4932
  const builderName = `build${pascalCase(contextSpec.context.name)}ProjectionSubscriptions`;
4089
4933
  const dependencyLines = contextSpec.projections.map((projection) => ` ${projectionVariableName(projection.name)}: ${projection.name};`);
4090
- 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";`));
4091
4936
  const subscriptionEntries = contextSpec.projections.flatMap((projection) => projection.sources.map((source) => {
4092
4937
  const subscriptionName = projection.subscription?.subscriptionName ?? projection.name;
4093
4938
  const consumerGroup = projection.subscription?.consumerGroup;
@@ -4097,11 +4942,13 @@ function buildProjectionSubscriptionsArtifact(contextSpec) {
4097
4942
  subscriptionName: "${subscriptionName}",
4098
4943
  eventName: "${source.contextName}.${source.aggregateName}.${source.eventName}",
4099
4944
  role: "${source.role}",${consumerGroupLine}
4100
- 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),
4101
4946
  }`;
4102
4947
  }));
4103
- 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";
4104
4950
  ${importLines.join("\n")}
4951
+ ${[...new Set(eventImports)].join("\n")}
4105
4952
 
4106
4953
  export type ProjectionSubscription = {
4107
4954
  projectionName: string;
@@ -4109,7 +4956,7 @@ export type ProjectionSubscription = {
4109
4956
  eventName: string;
4110
4957
  role: "primary" | "enrichment";
4111
4958
  consumerGroup?: string;
4112
- handle: (event: unknown, tx: Transaction) => Promise<void>;
4959
+ handle: (event: EventEnvelope<unknown>, tx: Transaction) => Promise<void>;
4113
4960
  };
4114
4961
 
4115
4962
  export type ${depsTypeName} = {
@@ -4385,6 +5232,34 @@ export class EventCollector {
4385
5232
  }
4386
5233
  `;
4387
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
+ }
4388
5263
  function generateDomainErrors() {
4389
5264
  return `export type { DomainError } from '../../../lib/domain-error.ts';
4390
5265
  export type { ApplicationError } from '../../../lib/application-error.ts';
@@ -4465,6 +5340,7 @@ function buildV5SharedKernelArtifacts(spec) {
4465
5340
  const base = [
4466
5341
  createGeneratedArtifact("core/shared-kernel/result.ts", generateResultReExport(), ownership),
4467
5342
  createGeneratedArtifact("core/shared-kernel/events/event-collector.ts", generateEventCollector(), ownership),
5343
+ createGeneratedArtifact("core/shared-kernel/events/aggregate-event-tracker.ts", generateAggregateEventTracker(), ownership),
4468
5344
  createGeneratedArtifact("core/shared-kernel/events/event-context.ts", generateEventContext(), ownership),
4469
5345
  createGeneratedArtifact("core/shared-kernel/errors/domain-errors.ts", generateDomainErrors(), ownership),
4470
5346
  createGeneratedArtifact("core/shared-kernel/trpc-error-mapper.ts", generateTrpcErrorMapper(), ownership),
@@ -4923,7 +5799,11 @@ function createApplicationError() {
4923
5799
  }
4924
5800
  `;
4925
5801
  }
4926
- 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
+ `;
4927
5807
  return `import type { PgTransaction } from 'drizzle-orm/pg-core';
4928
5808
 
4929
5809
  export type Transaction = PgTransaction<any, any, any>;
@@ -5017,7 +5897,7 @@ export type ListResult<T> = {
5017
5897
  };
5018
5898
  `;
5019
5899
  }
5020
- function buildV5LibArtifacts() {
5900
+ function buildV5LibArtifacts(infrastructureStrategy) {
5021
5901
  const ownership = sharedArtifactOwnership("lib");
5022
5902
  return [
5023
5903
  createGeneratedArtifact("lib/branded-id.ts", createBrandedId(), ownership),
@@ -5029,7 +5909,7 @@ function buildV5LibArtifacts() {
5029
5909
  createGeneratedArtifact("lib/result.ts", createResult(), ownership),
5030
5910
  createGeneratedArtifact("lib/domain-error.ts", createDomainError(), ownership),
5031
5911
  createGeneratedArtifact("lib/application-error.ts", createApplicationError(), ownership),
5032
- createGeneratedArtifact("lib/transaction.ts", createTransaction(), ownership),
5912
+ createGeneratedArtifact("lib/transaction.ts", createTransaction(infrastructureStrategy), ownership),
5033
5913
  createGeneratedArtifact("lib/concurrency-conflict-error.ts", createConcurrencyConflictError(), ownership),
5034
5914
  createGeneratedArtifact("lib/pagination.ts", createPagination(), ownership)
5035
5915
  ];
@@ -5139,26 +6019,29 @@ function buildConsumerAclArtifacts(contextSpec) {
5139
6019
 
5140
6020
  //#endregion
5141
6021
  //#region packages/core/orchestrator.ts
5142
- function generateContextArtifacts(contextSpec) {
6022
+ function generateContextArtifacts(contextSpec, options = {}) {
5143
6023
  const perAggregateArtifacts = sliceContextIntoAggregateViews(contextSpec).flatMap((view) => {
5144
6024
  const ownership = sliceArtifactOwnership(`${view.context.modulePath}/${kebabCase(view.aggregate.name)}`);
5145
6025
  return [
5146
6026
  ...applyOwnershipIfMissing(buildV5DomainArtifacts(view), ownership),
5147
6027
  ...applyOwnershipIfMissing(buildV5ApplicationArtifacts(view, { skipContextWideArtifacts: true }), ownership),
5148
- ...buildV5InfrastructureArtifacts(view, { skipContextWideArtifacts: true }),
6028
+ ...buildV5InfrastructureArtifacts(view, {
6029
+ skipContextWideArtifacts: true,
6030
+ infrastructureStrategy: options.infrastructureStrategy
6031
+ }),
5149
6032
  ...applyOwnershipIfMissing(buildV5TestArtifacts(view), ownership)
5150
6033
  ];
5151
6034
  });
5152
6035
  const ctxOwnership = contextArtifactOwnership(contextSpec.context.modulePath);
5153
6036
  const infraOwnership = sliceArtifactOwnership(contextSpec.context.modulePath);
5154
6037
  const perContextArtifacts = [
5155
- ...buildV5LibArtifacts(),
6038
+ ...buildV5LibArtifacts(options.infrastructureStrategy),
5156
6039
  ...buildV5SharedKernelArtifacts(contextSpec),
5157
6040
  ...buildV5PortArtifacts(),
5158
6041
  ...applyOwnershipIfMissing(buildV5ApplicationContextArtifacts(contextSpec), ctxOwnership),
5159
6042
  ...applyOwnershipIfMissing(buildConsumerAclArtifacts(contextSpec), ctxOwnership),
5160
6043
  ...applyOwnershipIfMissing(buildProjectionArtifacts(contextSpec), ctxOwnership),
5161
- ...applyOwnershipIfMissing(buildV5InfrastructureContextArtifacts(contextSpec), infraOwnership),
6044
+ ...applyOwnershipIfMissing(buildV5InfrastructureContextArtifacts(contextSpec, { infrastructureStrategy: options.infrastructureStrategy }), infraOwnership),
5162
6045
  ...applyOwnershipIfMissing(buildV5PresentationArtifacts(contextSpec), ctxOwnership),
5163
6046
  ...applyOwnershipIfMissing(buildV5RouteArtifacts(contextSpec), ctxOwnership)
5164
6047
  ];
@@ -5765,6 +6648,7 @@ function createIntegrationEvent() {
5765
6648
 
5766
6649
  export interface IntegrationEvent<TPayload> {
5767
6650
  readonly type: string;
6651
+ readonly version: number;
5768
6652
  readonly payload: TPayload;
5769
6653
  readonly eventContext: EventContext;
5770
6654
  }
@@ -5800,6 +6684,20 @@ export class DrizzleTransactionManager implements TransactionManager {
5800
6684
  }
5801
6685
  `;
5802
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
+ }
5803
6701
  function createTransactionAbortError() {
5804
6702
  return `export class TransactionAbortError extends Error {
5805
6703
  constructor(public readonly innerError: unknown) {
@@ -5810,11 +6708,13 @@ function createTransactionAbortError() {
5810
6708
  `;
5811
6709
  }
5812
6710
  function createHandlerDeps() {
5813
- 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";
5814
6713
 
5815
6714
  export type HandlerDeps = {
5816
6715
  readonly tx: unknown;
5817
6716
  readonly eventCollector: EventCollector;
6717
+ readonly aggregateEventTracker: AggregateEventTracker;
5818
6718
  readonly repos: Record<string, unknown>;
5819
6719
  readonly acls: Record<string, unknown>;
5820
6720
  };
@@ -5826,6 +6726,7 @@ function buildCompositionTypeArtifacts() {
5826
6726
  createGeneratedArtifact("infrastructure/messaging/integration-event.ts", createIntegrationEvent(), ownership),
5827
6727
  createGeneratedArtifact("infrastructure/unit-of-work/transaction-manager.ts", createTransactionManager(), ownership),
5828
6728
  createGeneratedArtifact("infrastructure/unit-of-work/drizzle-transaction-manager.ts", createDrizzleTransactionManager(), ownership),
6729
+ createGeneratedArtifact("infrastructure/unit-of-work/mikroorm-transaction-manager.ts", createMikroOrmTransactionManager(), ownership),
5829
6730
  createGeneratedArtifact("infrastructure/unit-of-work/transaction-abort-error.ts", createTransactionAbortError(), ownership),
5830
6731
  createGeneratedArtifact("infrastructure/unit-of-work/handler-deps.ts", createHandlerDeps(), ownership)
5831
6732
  ].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
@@ -5838,6 +6739,7 @@ function createCommandBusImpl() {
5838
6739
  import type { TransactionManager } from "../unit-of-work/transaction-manager.ts";
5839
6740
  import { TransactionAbortError } from "../unit-of-work/transaction-abort-error.ts";
5840
6741
  import type { HandlerDeps } from "../unit-of-work/handler-deps.ts";
6742
+ import { AggregateEventTracker } from "../../core/shared-kernel/events/aggregate-event-tracker.ts";
5841
6743
  import { EventCollector } from "../../core/shared-kernel/events/event-collector.ts";
5842
6744
  import type { CommandEnvelope } from "../../core/shared-kernel/commands/command-envelope.ts";
5843
6745
  import type { EventContext } from "../../core/shared-kernel/events/event-context.ts";
@@ -5849,6 +6751,9 @@ import type { EventEnvelope } from "../../lib/event-envelope.ts";
5849
6751
  type OutboxWriter = {
5850
6752
  append(tx: unknown, events: EventEnvelope<unknown>[]): Promise<void>;
5851
6753
  };
6754
+ type OutboxRelay = {
6755
+ afterCommit(): void;
6756
+ };
5852
6757
  type RepositoryFactory = {
5853
6758
  create(tx: unknown): Record<string, unknown>;
5854
6759
  };
@@ -5868,6 +6773,8 @@ type InternalHandlerFn = (
5868
6773
  ) => Promise<Result<unknown, unknown>>;
5869
6774
 
5870
6775
  export class CommandBusImpl implements CommandBusPort {
6776
+ private relay: OutboxRelay | undefined;
6777
+
5871
6778
  constructor(
5872
6779
  private readonly txManager: TransactionManager,
5873
6780
  private readonly repoFactory: RepositoryFactory,
@@ -5875,6 +6782,10 @@ export class CommandBusImpl implements CommandBusPort {
5875
6782
  private readonly outboxWriter: OutboxWriter,
5876
6783
  ) {}
5877
6784
 
6785
+ setRelay(relay: OutboxRelay): void {
6786
+ this.relay = relay;
6787
+ }
6788
+
5878
6789
  // Internal handler type: receives deps injected by withUnitOfWork.
5879
6790
  // This is different from TestCommandBus which registers pre-bound
5880
6791
  // handlers with signature (command, eventContext) — deps closed over
@@ -5902,9 +6813,11 @@ export class CommandBusImpl implements CommandBusPort {
5902
6813
  ): Promise<TResult> {
5903
6814
  const { handler, contextKey: commandContext } = this.resolveHandler(command.type);
5904
6815
  try {
5905
- return await this.withUnitOfWork(commandContext, (deps) =>
6816
+ const result = await this.withUnitOfWork(commandContext, (deps) =>
5906
6817
  handler(command, deps, eventContext)
5907
6818
  ) as TResult;
6819
+ this.relay?.afterCommit();
6820
+ return result;
5908
6821
  } catch (error) {
5909
6822
  if (error instanceof TransactionAbortError) {
5910
6823
  // Reconstitute the failure Result that triggered rollback.
@@ -5935,16 +6848,24 @@ export class CommandBusImpl implements CommandBusPort {
5935
6848
  work: (deps: HandlerDeps) => Promise<Result<T, unknown>>
5936
6849
  ): Promise<Result<T, unknown>> {
5937
6850
  return this.txManager.withTransaction(async (tx) => {
6851
+ const aggregateEventTracker = new AggregateEventTracker();
5938
6852
  const eventCollector = new EventCollector();
5939
6853
  const repos = this.repoFactory.create(tx);
5940
6854
  const acls = this.aclFactory.create(tx, commandContext);
5941
6855
 
5942
- const result = await work({ tx, eventCollector, repos, acls });
6856
+ const result = await work({
6857
+ tx,
6858
+ eventCollector,
6859
+ aggregateEventTracker,
6860
+ repos,
6861
+ acls,
6862
+ });
5943
6863
 
5944
6864
  if (!result.ok) {
5945
6865
  throw new TransactionAbortError(result.error);
5946
6866
  }
5947
6867
 
6868
+ aggregateEventTracker.releaseInto(eventCollector);
5948
6869
  const events = eventCollector.drain();
5949
6870
 
5950
6871
  if (events.length > 0) {
@@ -5989,16 +6910,38 @@ function buildCompositionBusArtifacts() {
5989
6910
 
5990
6911
  //#endregion
5991
6912
  //#region packages/core/generators/composition_outbox.ts
5992
- function createOutboxTable() {
5993
- 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";
5994
6932
 
5995
6933
  export const outboxEvents = pgTable("outbox_events", {
5996
6934
  id: uuid("id").primaryKey().defaultRandom(),
5997
6935
  eventType: varchar("event_type", { length: 255 }).notNull(),
6936
+ eventVersion: integer("event_version").notNull(),
5998
6937
  payload: jsonb("payload").notNull(),
5999
6938
  metadata: jsonb("metadata").notNull(),
6000
6939
  occurredAt: timestamp("occurred_at").notNull().defaultNow(),
6001
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"),
6002
6945
  });
6003
6946
  `;
6004
6947
  }
@@ -6014,6 +6957,7 @@ export class OutboxWriter {
6014
6957
  for (const event of events) {
6015
6958
  await db.insert(outboxEvents).values({
6016
6959
  eventType: event.eventType,
6960
+ eventVersion: event.eventVersion,
6017
6961
  payload: event.payload,
6018
6962
  metadata: {
6019
6963
  correlationId: event.correlationId,
@@ -6028,9 +6972,221 @@ export class OutboxWriter {
6028
6972
  }
6029
6973
  `;
6030
6974
  }
6031
- 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) {
6032
7182
  const ownership = sharedArtifactOwnership("composition");
6033
- 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));
6034
7190
  }
6035
7191
 
6036
7192
  //#endregion
@@ -6118,38 +7274,53 @@ function renderMappedType(fields) {
6118
7274
  ${Object.entries(fields).map(([fieldName, expr]) => ` readonly ${camelCase(fieldName)}: ${mappingValueType(expr)};`).join("\n")}
6119
7275
  }`;
6120
7276
  }
6121
- function sourceQueryViewTypeName(acl) {
6122
- const outputExportName = acl.sourceQuery.outputSchemaExportName?.trim();
6123
- if (outputExportName) return pascalCase(outputExportName);
6124
- const baseName = acl.sourceQuery.readModelName ? pascalCase(acl.sourceQuery.readModelName) : pascalCase(acl.sourceQuery.name);
6125
- 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
+ }
7290
+ }
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;
6126
7301
  }
6127
- function sourceQueryViewFileName(acl) {
6128
- const outputExportName = acl.sourceQuery.outputSchemaExportName?.trim();
6129
- if (outputExportName) return `${kebabCase(outputExportName).replace(/-view$/, "")}.view.ts`;
6130
- return `${(acl.sourceQuery.readModelName ? kebabCase(acl.sourceQuery.readModelName) : kebabCase(acl.sourceQuery.name)).replace(/-view$/, "")}.view.ts`;
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}>`;
6131
7307
  }
6132
7308
  function readResponseFieldType(acl, mapping) {
6133
7309
  if (mapping.kind === "const") return mappingValueType({ const: mapping.value });
6134
- const viewTypeName = sourceQueryViewTypeName(acl);
6135
7310
  if (acl.sourceQuery.queryKind === "list") {
6136
- if (mapping.sourcePath === "result.items") return `${viewTypeName}[]`;
7311
+ if (mapping.sourcePath === "result.items") return formatReadAclListItemType(acl.sourceQuery.outputFields);
6137
7312
  if (mapping.sourcePath === "result.total" || mapping.sourcePath === "result.page" || mapping.sourcePath === "result.pageSize") return "number";
6138
7313
  }
6139
7314
  const sourceFieldName = mapping.sourcePath.replace(/^result\./, "");
6140
7315
  const sourceField = acl.sourceQuery.outputFields.find((field) => camelCase(field.name) === camelCase(sourceFieldName));
6141
- return sourceField ? `${viewTypeName}[${JSON.stringify(sourceField.name)}]` : "unknown";
7316
+ return sourceField ? formatReadAclFieldType(sourceField) : "unknown";
6142
7317
  }
6143
7318
  function buildReadAclContractContent(acl) {
6144
7319
  normalizeModulePath(acl.consumerContext.modulePath);
6145
- const sourceModulePath = normalizeModulePath(acl.sourceContext.modulePath);
6146
7320
  const requestTypeName = aclRequestTypeName(acl.port);
6147
7321
  const responseTypeName = aclResponseTypeName(acl.port);
6148
- const viewTypeName = sourceQueryViewTypeName(acl);
6149
- const viewFileName = sourceQueryViewFileName(acl);
6150
7322
  const responseLines = acl.responseMappings.map((mapping) => ` readonly ${camelCase(mapping.targetPath)}: ${readResponseFieldType(acl, mapping)};`);
6151
7323
  return `import type { Transaction } from "../../../../../lib/transaction.ts";
6152
- import type { ${viewTypeName} } from "../../../${sourceModulePath}/application/contracts/${viewFileName}";
6153
7324
 
6154
7325
  export type ${requestTypeName} = ${renderMappedType(Object.fromEntries(acl.requestMappings.map((mapping) => [mapping.targetPath, mapping.kind === "from" ? { from: mapping.sourcePath } : { const: mapping.value }])))};
6155
7326
 
@@ -6438,6 +7609,24 @@ function buildCompositionAclArtifacts(compositionSpec) {
6438
7609
  ].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
6439
7610
  }
6440
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
+
6441
7630
  //#endregion
6442
7631
  //#region packages/core/generators/composition_container.ts
6443
7632
  /**
@@ -6449,6 +7638,8 @@ function buildCompositionContainerArtifacts(compositionSpec, contextSpecs) {
6449
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));
6450
7639
  }
6451
7640
  function buildContainerContent(compositionSpec, contextSpecs) {
7641
+ assertSupportedContainerStrategy(compositionSpec);
7642
+ const runtime = resolveContainerRuntime(compositionSpec);
6452
7643
  const readContexts = uniqueReadContexts(compositionSpec.acls);
6453
7644
  const readContextNames = buildCollisionSafeModulePathNames(readContexts.map((context) => context.modulePath));
6454
7645
  const commandContextNames = buildCollisionSafeModulePathNames(contextSpecs.map((context) => context.context.modulePath));
@@ -6458,7 +7649,10 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6458
7649
  ``,
6459
7650
  `import { CommandBusImpl } from "../messaging/command-bus.impl.ts";`,
6460
7651
  `import { QueryBusImpl } from "../messaging/query-bus.impl.ts";`,
6461
- `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";`,
6462
7656
  `import { OutboxWriter } from "../outbox/outbox-writer.ts";`,
6463
7657
  `import { AclFactory } from "./acl-factory.ts";`,
6464
7658
  `import { RepositoryFactory } from "./repo-factory.ts";`,
@@ -6477,7 +7671,8 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6477
7671
  for (const query of uniqueQueriesByReadModel(contextSpec.queries)) {
6478
7672
  const repoClassName = queryReadModelRepositoryClassName(query);
6479
7673
  const repoAlias = queryReadModelRepositoryAlias(boundaryPlan.symbolStem, query);
6480
- 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";`);
6481
7676
  }
6482
7677
  }
6483
7678
  for (const spec of contextSpecs) {
@@ -6501,15 +7696,16 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6501
7696
  ` readonly commandBus: CommandBusPort;`,
6502
7697
  ` readonly queryBus: QueryBusPort;`,
6503
7698
  ` readonly integrationEventSubscriptions: EventSubscription[];`,
6504
- ` readonly db: unknown;`,
7699
+ ` readonly outboxPoller: OutboxPoller;`,
7700
+ ...runtime.containerFields.map((field) => ` readonly ${field}: unknown;`),
6505
7701
  `};`
6506
7702
  ];
6507
7703
  const createContainerLines = [
6508
7704
  ``,
6509
- `export function createContainer(deps: { db: unknown }): Container {`,
7705
+ `export function createContainer(deps: { ${runtime.dependencyFields.map((field) => `${field}: unknown`).join("; ")} }): Container {`,
6510
7706
  ` const outboxWriter = new OutboxWriter();`,
6511
7707
  ` const repoFactory = new RepositoryFactory();`,
6512
- ` const txManager = new DrizzleTransactionManager(deps.db);`
7708
+ ` const txManager = new ${runtime.transactionManagerClassName}(${runtime.transactionDependencyRef});`
6513
7709
  ];
6514
7710
  for (const context of readContexts) {
6515
7711
  const contextSpec = contextSpecs.find((spec) => spec.context.modulePath.toLowerCase() === context.modulePath.toLowerCase());
@@ -6517,7 +7713,7 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6517
7713
  const boundaryPlan = readContextNames.get(context.modulePath);
6518
7714
  const boundaryVar = `${boundaryPlan.propertyStem}PublishedReadBoundary`;
6519
7715
  const boundaryAlias = `create${boundaryPlan.symbolStem}PublishedReadBoundary`;
6520
- const boundaryDeps = buildPublishedReadBoundaryDeps(contextSpec, boundaryPlan.symbolStem);
7716
+ const boundaryDeps = buildPublishedReadBoundaryDeps(contextSpec, boundaryPlan.symbolStem, runtime.readRepositoryDependencyRef);
6521
7717
  createContainerLines.push(` const ${boundaryVar} = ${boundaryAlias}(${boundaryDeps});`);
6522
7718
  }
6523
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);`, ` },`, ` };`);
@@ -6548,7 +7744,9 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6548
7744
  createContainerLines.push(` const integrationEventSubscriptions: EventSubscription[] = [];`);
6549
7745
  }
6550
7746
  createContainerLines.push(``);
6551
- 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(", ")} };`);
6552
7750
  createContainerLines.push(`}`);
6553
7751
  createContainerLines.push(``);
6554
7752
  return [
@@ -6557,11 +7755,50 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6557
7755
  ...createContainerLines
6558
7756
  ].join("\n");
6559
7757
  }
6560
- function buildPublishedReadBoundaryDeps(spec, contextSymbolStem) {
7758
+ function buildPublishedReadBoundaryDeps(spec, contextSymbolStem, readRepositoryDependencyRef) {
6561
7759
  const entries = /* @__PURE__ */ new Map();
6562
- 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})`);
6563
7761
  return `{ ${[...entries.values()].join(", ")} }`;
6564
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
+ }
6565
7802
  function resolvedReadModelName(query) {
6566
7803
  return query.readSide?.readModelName ?? query.readModelName ?? query.name;
6567
7804
  }
@@ -6580,11 +7817,6 @@ function uniqueQueriesByReadModel(queries) {
6580
7817
  }
6581
7818
  return [...queriesByReadModel.values()].sort((left, right) => resolvedReadModelName(left).localeCompare(resolvedReadModelName(right)));
6582
7819
  }
6583
- function queryViewFileBase(query) {
6584
- const outputExportName = query.outputSchemaExportName?.trim();
6585
- if (outputExportName) return kebabCase(outputExportName).replace(/-view$/, "");
6586
- return readModelRepositoryFileBase(resolvedReadModelName(query));
6587
- }
6588
7820
  function queryReadModelPortTypeName(query) {
6589
7821
  return `${readModelContractName(resolvedReadModelName(query))}RepositoryPort`;
6590
7822
  }
@@ -6610,24 +7842,31 @@ function uniqueReadContexts(acls) {
6610
7842
  return contexts;
6611
7843
  }
6612
7844
  function buildRepoFactoryContent(contextSpecs) {
6613
- const repositoryDefs = contextSpecs.flatMap((spec) => spec.aggregates.map((aggregate) => ({
6614
- aggregateName: aggregate.name,
6615
- modulePath: spec.context.modulePath,
6616
- repoFile: `drizzle-${kebabCase(aggregate.name)}.repository.ts`,
6617
- aggregateAliasBase: `Drizzle${pascalCase(aggregate.name)}Repository`,
6618
- aggregateFieldBase: `${camelCase(aggregate.name)}s`
6619
- })));
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
+ }));
6620
7859
  assertNoSameContextRepoFileCollisions(repositoryDefs);
6621
7860
  const namingPlan = buildRepositoryNamingPlan(repositoryDefs);
6622
7861
  const lines = [`// Auto-generated repository factory — do not edit by hand`, ``];
6623
7862
  for (const repositoryDef of repositoryDefs) {
6624
- const aggName = repositoryDef.aggregateName;
7863
+ repositoryDef.aggregateName;
6625
7864
  const modulePath = normalizeModulePath(repositoryDef.modulePath);
6626
- const repoClass = `Drizzle${pascalCase(aggName)}Repository`;
7865
+ const repoClass = repositoryDef.aggregateAliasBase;
6627
7866
  const { repoAlias } = namingPlan.get(repositoryKey(repositoryDef));
6628
7867
  const repoFile = repositoryDef.repoFile;
6629
- if (repoAlias === repoClass) lines.push(`import { ${repoClass} } from "../persistence/drizzle/repositories/${modulePath}/${repoFile}";`);
6630
- 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}";`);
6631
7870
  }
6632
7871
  lines.push(``);
6633
7872
  lines.push(`export class RepositoryFactory {`);
@@ -6765,9 +8004,14 @@ export {};
6765
8004
  const payloadTypeNames = [];
6766
8005
  for (const evt of compositionSpec.crossContextEvents) {
6767
8006
  const fields = getPayloadFields(evt.payloadSchema);
6768
- payloadTypeNames.push(evt.payloadTypeName);
6769
- lines.push(`export interface ${evt.payloadTypeName} {`);
6770
- 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
+ }
6771
8015
  lines.push(`}`);
6772
8016
  lines.push(``);
6773
8017
  }
@@ -6778,18 +8022,20 @@ export {};
6778
8022
  return lines.join("\n");
6779
8023
  }
6780
8024
  function buildHandlerFactoryContent(evt, sub) {
6781
- 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},`);
6782
8028
  const commandType = `${pascalCase(sub.targetContext)}.${pascalCase(sub.action)}`;
6783
8029
  return [
6784
8030
  `// Auto-generated event handler factory — do not edit by hand`,
6785
8031
  `import type { CommandBusPort } from "../../../../../core/ports/messaging/command-bus.port.ts";`,
6786
8032
  `import type { IntegrationEvent, IntegrationEventHandler } from "../../../../../infrastructure/messaging/integration-event.ts";`,
6787
- `import type { ${evt.payloadTypeName} } from "../../../../shared-kernel/events/integration-events.types.ts";`,
8033
+ `import type { ${payloadTypeName} } from "../../../../shared-kernel/events/integration-events.types.ts";`,
6788
8034
  ``,
6789
8035
  `export function ${sub.handlerFactoryName}(deps: {`,
6790
8036
  ` commandBus: CommandBusPort;`,
6791
- `}): IntegrationEventHandler<${evt.payloadTypeName}> {`,
6792
- ` return async (event: IntegrationEvent<${evt.payloadTypeName}>) => {`,
8037
+ `}): IntegrationEventHandler<${payloadTypeName}> {`,
8038
+ ` return async (event: IntegrationEvent<${payloadTypeName}>) => {`,
6793
8039
  ` return deps.commandBus.execute(`,
6794
8040
  ` {`,
6795
8041
  ` type: "${commandType}",`,
@@ -6808,30 +8054,114 @@ function buildSubscriptionRegistryContent(compositionSpec) {
6808
8054
  const allSubs = [];
6809
8055
  for (const evt of compositionSpec.crossContextEvents) for (const sub of evt.subscriptions) allSubs.push({
6810
8056
  eventType: evt.eventType,
8057
+ payloadTypeName: integrationPayloadTypeName(evt),
6811
8058
  sub
6812
8059
  });
6813
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);
6814
8063
  for (const { sub } of allSubs) {
6815
8064
  const fileName = `${kebabCase(sub.handlerName)}.handler.ts`;
6816
8065
  importLines.push(`// Handler: ${sub.handlerFactoryName} from core/contexts/${sub.targetModulePath}/application/event-handlers/${fileName}`);
6817
8066
  }
6818
- const depsEntries = allSubs.map(({ sub }) => ` ${camelCase(sub.handlerName)}: IntegrationEventHandler<any>;`);
8067
+ const depsEntries = allSubs.map(({ payloadTypeName, sub }) => ` ${camelCase(sub.handlerName)}: IntegrationEventHandler<${payloadTypeName}>;`);
6819
8068
  const registryEntries = allSubs.map(({ eventType, sub }) => ` { eventType: "${eventType}", handler: deps.${camelCase(sub.handlerName)} },`);
6820
8069
  const lines = [...importLines, ``];
6821
8070
  if (depsEntries.length > 0) lines.push(`export function buildSubscriptionRegistry(deps: {`, ...depsEntries, `}): EventSubscription[] {`, ` return [`, ...registryEntries, ` ];`, `}`, ``);
6822
8071
  else lines.push(`export function buildSubscriptionRegistry(_deps: Record<string, never>): EventSubscription[] {`, ` return [];`, `}`, ``);
6823
8072
  return lines.join("\n");
6824
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
+ }
6825
8155
  function getPayloadFields(payloadSchema) {
6826
8156
  const shape = payloadSchema?.shape;
6827
8157
  if (shape && typeof shape === "object") return Object.keys(shape).map((key) => {
6828
- const typeName = ((shape[key]?._zod)?.def)?.type;
6829
- let tsType = "string";
6830
- if (typeName === "number") tsType = "number";
6831
- else if (typeName === "boolean") tsType = "boolean";
8158
+ const fieldSchema = shape[key];
8159
+ const rendered = renderPayloadSchemaType(fieldSchema);
6832
8160
  return {
6833
8161
  name: key,
6834
- type: tsType
8162
+ type: rendered.type,
8163
+ optional: rendered.optional,
8164
+ comment: rendered.comment
6835
8165
  };
6836
8166
  });
6837
8167
  return [];
@@ -6856,7 +8186,7 @@ async function buildContextNormalizedSpecFromConfig(input) {
6856
8186
  }))), config);
6857
8187
  }
6858
8188
  async function buildV5ContextArtifacts(input) {
6859
- return generateContextArtifacts(await buildContextNormalizedSpecFromConfig(input));
8189
+ return generateContextArtifacts(await buildContextNormalizedSpecFromConfig(input), { infrastructureStrategy: input.infrastructureStrategy });
6860
8190
  }
6861
8191
  async function buildV5Artifacts(input) {
6862
8192
  const spec = await buildNormalizedSpecFromConfig(input);
@@ -6881,8 +8211,9 @@ async function buildCompositionArtifacts(compositionConfigPath, contextSpecs) {
6881
8211
  return mergeGeneratedArtifacts([
6882
8212
  ...buildCompositionTypeArtifacts(),
6883
8213
  ...buildCompositionBusArtifacts(),
6884
- ...buildCompositionOutboxArtifacts(),
8214
+ ...buildCompositionOutboxArtifacts(compositionSpec.infrastructure),
6885
8215
  ...buildCompositionAclArtifacts(compositionSpec),
8216
+ ...buildCompositionDependencyManifestArtifacts(compositionSpec),
6886
8217
  ...buildReadModelCompositionArtifacts(composedContextSpecs),
6887
8218
  ...buildCompositionContainerArtifacts(compositionSpec, composedContextSpecs),
6888
8219
  ...buildCompositionRouterArtifacts(compositionSpec, composedContextSpecs),
@@ -6891,4 +8222,4 @@ async function buildCompositionArtifacts(compositionConfigPath, contextSpecs) {
6891
8222
  }
6892
8223
 
6893
8224
  //#endregion
6894
- 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 };