@zodmire/core 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mod.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as mergeGeneratedArtifacts, c as sliceArtifactOwnership, i as inferArtifactOwnership, l as withArtifactOwnership, n as contextArtifactOwnership, o as resolveArtifactOwnership, r as createGeneratedArtifact, s as sharedArtifactOwnership, t as applyOwnershipIfMissing } from "./artifacts-CUzHfc15.mjs";
1
+ import { a as mergeGeneratedArtifacts, c as sliceArtifactOwnership, i as inferArtifactOwnership, l as userArtifactOwnership, n as contextArtifactOwnership, o as resolveArtifactOwnership, r as createGeneratedArtifact, s as sharedArtifactOwnership, t as applyOwnershipIfMissing, u as withArtifactOwnership } from "./artifacts-D0E751FX.mjs";
2
2
  import { zx } from "@traversable/zod";
3
3
  import { z } from "zod";
4
4
  import { isAbsolute, join, normalize, relative, resolve } from "node:path";
@@ -107,10 +107,83 @@ const ZOD_TYPE_MAP = {
107
107
  nan: "number",
108
108
  literal: "literal"
109
109
  };
110
+ function isRecord(value) {
111
+ return typeof value === "object" && value !== null;
112
+ }
113
+ function getZodDef(schema) {
114
+ if (!isRecord(schema)) return void 0;
115
+ const rawZod = schema["_zod"];
116
+ if (!isRecord(rawZod)) return void 0;
117
+ const def = rawZod["def"];
118
+ return isRecord(def) ? def : void 0;
119
+ }
110
120
  function isZodSchemaLike(schema) {
111
- if (!schema || typeof schema !== "object") return false;
112
- if (!("_zod" in schema)) return false;
113
- return typeof schema._zod?.def?.type === "string";
121
+ return typeof getZodDef(schema)?.type === "string";
122
+ }
123
+ function deriveLegacyQueryOutputMeta(meta) {
124
+ const metaRecord = meta;
125
+ const readModelName = typeof metaRecord["readModelName"] === "string" ? metaRecord["readModelName"] : void 0;
126
+ const targetAggregate = typeof metaRecord["targetAggregate"] === "string" ? metaRecord["targetAggregate"] : void 0;
127
+ const computedFields = metaRecord["computedFields"];
128
+ return {
129
+ ...readModelName !== void 0 ? { readModelName } : {},
130
+ ...targetAggregate !== void 0 ? { targetAggregate } : {},
131
+ ...computedFields !== void 0 ? { computedFields } : {}
132
+ };
133
+ }
134
+ function deriveLegacyQueryInputMeta(meta) {
135
+ const metaRecord = meta;
136
+ const readSide = metaRecord["readSide"];
137
+ const readSideReadModelName = isRecord(readSide) && typeof readSide["readModelName"] === "string" ? readSide["readModelName"] : void 0;
138
+ const legacyReadModelName = typeof metaRecord["readModelName"] === "string" ? metaRecord["readModelName"] : void 0;
139
+ const computedFields = metaRecord["computedFields"];
140
+ return {
141
+ ...readSideReadModelName !== void 0 ? { readModelName: readSideReadModelName } : legacyReadModelName !== void 0 ? { readModelName: legacyReadModelName } : {},
142
+ ...computedFields !== void 0 ? { computedFields } : {}
143
+ };
144
+ }
145
+ const SCALAR_TYPE_MAP = {
146
+ string: {
147
+ kind: "scalar",
148
+ scalar: "string"
149
+ },
150
+ number: {
151
+ kind: "scalar",
152
+ scalar: "number"
153
+ },
154
+ boolean: {
155
+ kind: "scalar",
156
+ scalar: "boolean"
157
+ },
158
+ date: {
159
+ kind: "scalar",
160
+ scalar: "date"
161
+ },
162
+ bigint: {
163
+ kind: "scalar",
164
+ scalar: "bigint"
165
+ }
166
+ };
167
+ function deriveScalarDescriptor(mapped) {
168
+ return SCALAR_TYPE_MAP[mapped] ?? {
169
+ kind: "unknown",
170
+ rawType: mapped
171
+ };
172
+ }
173
+ function extractEnumValues(schema) {
174
+ const zodDef = getZodDef(schema);
175
+ if (!zodDef) return void 0;
176
+ const entries = zodDef.entries;
177
+ if (Array.isArray(entries)) return entries.filter((e) => typeof e === "string");
178
+ const values = zodDef.values;
179
+ if (Array.isArray(values)) return values.filter((v) => typeof v === "string");
180
+ }
181
+ function extractLiteralValue(schema) {
182
+ const zodDef = getZodDef(schema);
183
+ if (!zodDef) return null;
184
+ const value = zodDef.value;
185
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
186
+ return null;
114
187
  }
115
188
  function unsupportedSchemaNodeError(exportName, nodeType) {
116
189
  const target = exportName ?? "<unknown export>";
@@ -140,6 +213,7 @@ function createReadField(exportName) {
140
213
  const inner = def.element;
141
214
  return {
142
215
  type: inner.type,
216
+ typeDescriptor: inner.typeDescriptor,
143
217
  optional: false,
144
218
  array: true,
145
219
  sourceSchema: rawSchema,
@@ -149,17 +223,65 @@ function createReadField(exportName) {
149
223
  }
150
224
  case zx.tagged("enum")(rawSchema): return {
151
225
  type: "enum",
226
+ typeDescriptor: {
227
+ kind: "enum",
228
+ values: extractEnumValues(rawSchema)
229
+ },
152
230
  optional: false,
153
231
  array: false,
154
232
  sourceSchema: rawSchema
155
233
  };
156
234
  case zx.tagged("object")(rawSchema): return {
157
235
  type: "object",
236
+ typeDescriptor: { kind: "object" },
158
237
  optional: false,
159
238
  array: false,
160
239
  sourceSchema: rawSchema,
161
240
  objectShape: def.shape
162
241
  };
242
+ case zx.tagged("intersection")(rawSchema): {
243
+ const left = def.left;
244
+ const right = def.right;
245
+ if (left?.objectShape && right?.objectShape) return {
246
+ type: "object",
247
+ typeDescriptor: { kind: "object" },
248
+ optional: false,
249
+ array: false,
250
+ sourceSchema: rawSchema,
251
+ objectShape: {
252
+ ...left.objectShape,
253
+ ...right.objectShape
254
+ }
255
+ };
256
+ return {
257
+ type: "intersection",
258
+ typeDescriptor: { kind: "intersection" },
259
+ optional: false,
260
+ array: false,
261
+ sourceSchema: rawSchema
262
+ };
263
+ }
264
+ case zx.tagged("union")(rawSchema): return {
265
+ type: "union",
266
+ typeDescriptor: { kind: "union" },
267
+ optional: false,
268
+ array: false,
269
+ sourceSchema: rawSchema
270
+ };
271
+ case zx.tagged("record")(rawSchema): return {
272
+ type: "record",
273
+ typeDescriptor: { kind: "record" },
274
+ optional: false,
275
+ array: false,
276
+ sourceSchema: rawSchema
277
+ };
278
+ case zx.tagged("tuple")(rawSchema): return {
279
+ type: "tuple",
280
+ typeDescriptor: { kind: "tuple" },
281
+ optional: false,
282
+ array: false,
283
+ sourceSchema: rawSchema
284
+ };
163
285
  case zx.tagged("string")(rawSchema):
164
286
  case zx.tagged("number")(rawSchema):
165
287
  case zx.tagged("int")(rawSchema):
@@ -173,9 +295,22 @@ function createReadField(exportName) {
173
295
  case zx.tagged("any")(rawSchema):
174
296
  case zx.tagged("unknown")(rawSchema):
175
297
  case zx.tagged("never")(rawSchema):
176
- case zx.tagged("nan")(rawSchema):
298
+ case zx.tagged("nan")(rawSchema): {
299
+ const mapped = ZOD_TYPE_MAP[raw._zod.def.type] ?? raw._zod.def.type;
300
+ return {
301
+ type: mapped,
302
+ typeDescriptor: deriveScalarDescriptor(mapped),
303
+ optional: false,
304
+ array: false,
305
+ sourceSchema: rawSchema
306
+ };
307
+ }
177
308
  case zx.tagged("literal")(rawSchema): return {
178
- type: ZOD_TYPE_MAP[raw._zod.def.type] ?? raw._zod.def.type,
309
+ type: "literal",
310
+ typeDescriptor: {
311
+ kind: "literal",
312
+ value: extractLiteralValue(rawSchema)
313
+ },
179
314
  optional: false,
180
315
  array: false,
181
316
  sourceSchema: rawSchema
@@ -200,8 +335,14 @@ function toIntrospectedField(name, read, registry) {
200
335
  optional: read.optional,
201
336
  array: read.array
202
337
  };
338
+ if (read.typeDescriptor) field.typeDescriptor = read.typeDescriptor;
203
339
  const fieldMeta = registry.get(read.sourceSchema);
204
340
  if (fieldMeta?.kind === "field" && fieldMeta.columnType) field.columnType = fieldMeta.columnType;
341
+ if (fieldMeta?.kind === "field" && fieldMeta.idType) field.typeDescriptor = {
342
+ kind: "branded",
343
+ brand: fieldMeta.idType,
344
+ underlying: read.type === "number" ? "number" : "string"
345
+ };
205
346
  if (read.objectShape) field.nestedFields = Object.entries(read.objectShape).map(([childName, childRead]) => toIntrospectedField(childName, childRead, registry));
206
347
  return field;
207
348
  }
@@ -289,7 +430,7 @@ function introspectSchema(schema, registry, exportName) {
289
430
  };
290
431
  }
291
432
  function deriveSchemaName(schema) {
292
- const typeName = schema?.constructor?.name;
433
+ const typeName = isRecord(schema) && "constructor" in schema ? schema.constructor?.name : void 0;
293
434
  if (typeName && typeName !== "Object" && typeName !== "ZodObject") return typeName.replace(/Schema$/, "");
294
435
  return "UnnamedEntity";
295
436
  }
@@ -309,14 +450,12 @@ async function readSchemaFile(filePath, registry) {
309
450
  const queryOutputs = /* @__PURE__ */ new Map();
310
451
  const schemaExportNames = /* @__PURE__ */ new Map();
311
452
  for (const [exportName, exported] of Object.entries(mod)) {
312
- if (!exported || typeof exported !== "object") continue;
313
- if (!("_zod" in exported)) continue;
453
+ if (!isZodSchemaLike(exported)) continue;
314
454
  schemaExportNames.set(exported, exportName);
315
455
  }
316
456
  const childEntitySchemas = /* @__PURE__ */ new Set();
317
457
  for (const [exportName, exported] of Object.entries(mod)) {
318
- if (!exported || typeof exported !== "object") continue;
319
- if (!("_zod" in exported)) continue;
458
+ if (!isZodSchemaLike(exported)) continue;
320
459
  const meta = registry.get(exported);
321
460
  if (!meta) continue;
322
461
  switch (meta.kind) {
@@ -365,29 +504,27 @@ async function readSchemaFile(filePath, registry) {
365
504
  }
366
505
  case "query-input": {
367
506
  const fields = introspectObjectShape(exported, registry, exportName);
368
- const legacyMeta = meta;
507
+ const legacyMeta = deriveLegacyQueryInputMeta(meta);
369
508
  queryInputs.set(meta.queryName, {
370
509
  fields,
371
510
  targetAggregate: meta.targetAggregate,
372
- readModelName: meta.readSide?.readModelName ?? legacyMeta.readModelName,
373
- queryKind: "queryKind" in meta ? meta.queryKind : "findById",
374
- pagination: "pagination" in meta ? meta.pagination : void 0,
375
- filters: "filters" in meta ? meta.filters : void 0,
376
- sorting: "sorting" in meta ? meta.sorting : void 0,
377
- computedFields: "computedFields" in meta ? meta.computedFields : legacyMeta.computedFields
511
+ readModelName: legacyMeta.readModelName,
512
+ queryKind: meta.queryKind,
513
+ pagination: meta.queryKind === "list" ? meta.pagination : void 0,
514
+ filters: meta.queryKind === "list" ? meta.filters : void 0,
515
+ sorting: meta.queryKind === "list" ? meta.sorting : void 0,
516
+ computedFields: legacyMeta.computedFields
378
517
  });
379
518
  break;
380
519
  }
381
520
  case "query-output": {
382
521
  const fields = introspectObjectShape(exported, registry, exportName);
383
- const legacyMeta = meta;
522
+ const legacyMeta = deriveLegacyQueryOutputMeta(meta);
384
523
  queryOutputs.set(meta.queryName, {
385
524
  fields,
386
525
  outputSchemaPath: filePath,
387
526
  outputSchemaExportName: exportName,
388
- ...legacyMeta.readModelName !== void 0 ? { readModelName: legacyMeta.readModelName } : {},
389
- ...legacyMeta.targetAggregate !== void 0 ? { targetAggregate: legacyMeta.targetAggregate } : {},
390
- ...legacyMeta.computedFields !== void 0 ? { computedFields: legacyMeta.computedFields } : {}
527
+ ...legacyMeta
391
528
  });
392
529
  break;
393
530
  }
@@ -422,8 +559,7 @@ async function readSchemaFile(filePath, registry) {
422
559
  }
423
560
  }
424
561
  for (const [exportName, exported] of Object.entries(mod)) {
425
- if (!exported || typeof exported !== "object") continue;
426
- if (!("_zod" in exported)) continue;
562
+ if (!isZodSchemaLike(exported)) continue;
427
563
  const meta = registry.get(exported);
428
564
  if (meta?.kind !== "entity") continue;
429
565
  if (childEntitySchemas.has(exported)) continue;
@@ -470,8 +606,75 @@ function mergeSchemaReadResults(results) {
470
606
 
471
607
  //#endregion
472
608
  //#region packages/core/normalizer.ts
609
+ function generatedCapability() {
610
+ return {
611
+ status: "generated",
612
+ reasons: []
613
+ };
614
+ }
615
+ function disabledCapability(reason) {
616
+ return {
617
+ status: "disabled",
618
+ reasons: [reason]
619
+ };
620
+ }
621
+ function runtimeThrowCapability(reasons) {
622
+ return {
623
+ status: "runtime-throw",
624
+ reasons
625
+ };
626
+ }
627
+ function canMapReadModelOutput$1(readModel, outputFields) {
628
+ const readModelFields = new Set(readModel.fields.map((field) => camelCase(field.name)));
629
+ return outputFields.every((field) => readModelFields.has(camelCase(field.name)));
630
+ }
631
+ function resolveGeneratedFindByIdLookupField(readModel, query) {
632
+ if (readModel.primaryKey.length !== 1) return null;
633
+ const primaryKeyField = camelCase(readModel.primaryKey[0]);
634
+ const matchingField = (query.inputFields ?? []).find((field) => camelCase(field.name) === primaryKeyField);
635
+ if (matchingField) return camelCase(matchingField.name);
636
+ if ((query.inputFields ?? []).length === 1) return camelCase(query.inputFields[0].name);
637
+ return null;
638
+ }
639
+ function deriveCommandCapability(command) {
640
+ const reasons = [];
641
+ if (!command.effects || command.effects.length === 0) reasons.push("command does not declare any effects");
642
+ if (command.commandKind === "update" && !command.loadBy) reasons.push("mutation commands require loadBy");
643
+ return reasons.length === 0 ? generatedCapability() : runtimeThrowCapability(reasons);
644
+ }
645
+ function deriveQueryCapability(query, readModelByName) {
646
+ const reasons = [];
647
+ if (!query.readModelName) reasons.push("query does not declare readModelName");
648
+ const readModel = query.readModelName ? readModelByName.get(query.readModelName) : void 0;
649
+ if (!readModel) reasons.push(`read-model "${query.readModelName ?? query.name}" is missing`);
650
+ else {
651
+ if (!canMapReadModelOutput$1(readModel, query.outputFields)) reasons.push("output fields do not map directly to the read-model");
652
+ if (query.queryKind === "findById" && !resolveGeneratedFindByIdLookupField(readModel, query)) reasons.push("read-model does not expose a stable findById lookup field");
653
+ }
654
+ return reasons.length === 0 ? generatedCapability() : runtimeThrowCapability(reasons);
655
+ }
656
+ function deriveProjectionCapabilities(projection, readModelByName) {
657
+ const writeReasons = [];
658
+ if (!readModelByName.has(projection.readModelName)) writeReasons.push(`read-model "${projection.readModelName}" is missing`);
659
+ 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}"`);
660
+ const writeModel = writeReasons.length === 0 ? generatedCapability() : runtimeThrowCapability(writeReasons);
661
+ return {
662
+ projector: generatedCapability(),
663
+ writeModel,
664
+ rebuild: !projection.rebuild?.enabled ? disabledCapability("projection rebuild is disabled") : writeModel.status === "generated" ? generatedCapability() : runtimeThrowCapability([...writeModel.reasons])
665
+ };
666
+ }
667
+ function resolveWriteModelStrategy(config) {
668
+ if (config.writeModelStrategy) return config.writeModelStrategy;
669
+ return config.adapters.some((adapter) => adapter.kind === "mikroorm-repository") ? "mikroorm" : "drizzle";
670
+ }
671
+ function resolveReadModelStrategy(config) {
672
+ return config.readModelStrategy ?? "drizzle";
673
+ }
473
674
  function buildContextNormalizedSpec(schemaResult, config) {
474
675
  if (schemaResult.aggregates.length === 0) throw new Error(`[normalizer] No aggregate found in SchemaReadResult. Context "${config.name}" requires at least one aggregate.`);
676
+ const writeModelStrategy = resolveWriteModelStrategy(config);
677
+ const readModelStrategy = resolveReadModelStrategy(config);
475
678
  const aggregateNames = new Set(schemaResult.aggregates.map((a) => a.name));
476
679
  for (const cmd of schemaResult.commands) if (!aggregateNames.has(cmd.targetAggregate)) throw new Error(`[normalizer] Command "${cmd.name}" targets aggregate "${cmd.targetAggregate}" which does not exist. Available aggregates: ${[...aggregateNames].join(", ")}`);
477
680
  for (const q of schemaResult.queries) {
@@ -485,7 +688,6 @@ function buildContextNormalizedSpec(schemaResult, config) {
485
688
  }
486
689
  for (const q of schemaResult.queries) if (q.queryKind === "list") {
487
690
  if (!q.pagination) throw new Error(`[normalizer] List query "${q.name}" is missing pagination config`);
488
- if (q.pagination.style === "cursor") throw new Error(`[normalizer] List query "${q.name}" uses cursor pagination which is not supported in v12`);
489
691
  if (!q.filters) throw new Error(`[normalizer] List query "${q.name}" is missing filters config`);
490
692
  if (!q.sorting) throw new Error(`[normalizer] List query "${q.name}" is missing sorting config`);
491
693
  if (!q.sorting.sortableFields.includes(q.sorting.defaultSort.field)) throw new Error(`[normalizer] List query "${q.name}": defaultSort.field "${q.sorting.defaultSort.field}" is not in sortableFields`);
@@ -653,7 +855,8 @@ function buildContextNormalizedSpec(schemaResult, config) {
653
855
  sources: projection.sources.map((source) => ({
654
856
  ...source,
655
857
  locality: source.contextName === config.name ? "local" : "external"
656
- }))
858
+ })),
859
+ capabilities: deriveProjectionCapabilities(projection, readModelByName)
657
860
  };
658
861
  });
659
862
  return {
@@ -685,13 +888,15 @@ function buildContextNormalizedSpec(schemaResult, config) {
685
888
  loadBy: cmd.loadBy,
686
889
  preconditions: cmd.preconditions,
687
890
  emits: cmd.emits,
688
- effects: cmd.effects
891
+ effects: cmd.effects,
892
+ capability: deriveCommandCapability(cmd)
689
893
  })),
690
894
  queries: schemaResult.queries.map((q) => {
895
+ const capability = deriveQueryCapability(q, readModelByName);
691
896
  const readSide = {
692
897
  readModelName: q.readModelName ?? pascalCase(q.name),
693
898
  searchFields: [],
694
- pagination: q.pagination?.style === "offset" ? { style: "offset" } : void 0,
899
+ pagination: q.pagination,
695
900
  filters: q.filters,
696
901
  sorting: q.sorting?.sortableFields ? q.sorting : void 0
697
902
  };
@@ -743,10 +948,11 @@ function buildContextNormalizedSpec(schemaResult, config) {
743
948
  outputSchemaExportName: q.outputSchemaExportName,
744
949
  readModelName: q.readModelName,
745
950
  readSide,
746
- pagination: { style: "offset" },
951
+ pagination: q.pagination ?? { style: "offset" },
747
952
  filters: q.filters,
748
953
  sorting: q.sorting,
749
- computedFields: resolvedComputed
954
+ computedFields: resolvedComputed,
955
+ capability
750
956
  };
751
957
  }
752
958
  return {
@@ -758,7 +964,8 @@ function buildContextNormalizedSpec(schemaResult, config) {
758
964
  outputSchemaPath: q.outputSchemaPath,
759
965
  outputSchemaExportName: q.outputSchemaExportName,
760
966
  readModelName: q.readModelName,
761
- readSide
967
+ readSide,
968
+ capability
762
969
  };
763
970
  }),
764
971
  readModels: normalizedReadModels,
@@ -772,7 +979,9 @@ function buildContextNormalizedSpec(schemaResult, config) {
772
979
  adapters: config.adapters,
773
980
  acls: config.acls,
774
981
  presentation: config.presentation,
775
- materialization: config.materialization
982
+ materialization: config.materialization,
983
+ writeModelStrategy,
984
+ readModelStrategy
776
985
  };
777
986
  }
778
987
  function buildNormalizedSpec(schemaResult, config) {
@@ -790,7 +999,9 @@ function buildNormalizedSpec(schemaResult, config) {
790
999
  ports: contextSpec.ports,
791
1000
  adapters: contextSpec.adapters,
792
1001
  presentation: contextSpec.presentation,
793
- materialization: contextSpec.materialization
1002
+ materialization: contextSpec.materialization,
1003
+ writeModelStrategy: contextSpec.writeModelStrategy,
1004
+ readModelStrategy: contextSpec.readModelStrategy
794
1005
  };
795
1006
  }
796
1007
 
@@ -824,14 +1035,25 @@ function walkEntityTree(aggregate, visitor) {
824
1035
 
825
1036
  //#endregion
826
1037
  //#region packages/core/composition_normalizer.ts
1038
+ function deriveContextRefFromConfigPath(contextConfigPath) {
1039
+ const normalized = contextConfigPath.replace(/\\/g, "/");
1040
+ return normalized.slice(normalized.lastIndexOf("/") + 1).replace(/\.context\.[cm]?ts$/i, "").replace(/\.[cm]?ts$/i, "");
1041
+ }
1042
+ function getConfiguredContextRefs(config) {
1043
+ const contextConfigs = Array.isArray(config.contextConfigs) ? config.contextConfigs : [];
1044
+ if (contextConfigs.length > 0) return contextConfigs.map((entry) => deriveContextRefFromConfigPath(entry.contextConfigPath));
1045
+ return Array.isArray(config.contexts) ? config.contexts : [];
1046
+ }
827
1047
  function buildNormalizedCompositionSpec(config, contextSpecs) {
828
1048
  assertUniqueCanonicalContextSpecs(contextSpecs);
1049
+ const infrastructure = resolveInfrastructureStrategy(config.infrastructure);
829
1050
  const resolveProvidedContextRef = createContextRefResolver(contextSpecs);
1051
+ const configuredContextRefs = getConfiguredContextRefs(config);
830
1052
  const resolvedContexts = [];
831
1053
  const resolvedContextSpecs = [];
832
1054
  const seenResolvedModulePaths = /* @__PURE__ */ new Set();
833
1055
  const seenResolvedDisplayNames = /* @__PURE__ */ new Map();
834
- for (const ctxRef of config.contexts) {
1056
+ for (const ctxRef of configuredContextRefs) {
835
1057
  const spec = resolveProvidedContextRef(ctxRef, "Context");
836
1058
  const canonicalModulePath = spec.context.modulePath.toLowerCase();
837
1059
  if (seenResolvedModulePaths.has(canonicalModulePath)) throw new Error(`[composition_normalizer] Context "${ctxRef}" resolves to duplicate composed context "${spec.context.name}" (${spec.context.modulePath})`);
@@ -851,11 +1073,11 @@ function buildNormalizedCompositionSpec(config, contextSpecs) {
851
1073
  validateExternalProjectionSources(resolvedContextSpecs, resolveCompositionContextRef, resolvedContexts.map((context) => context.modulePath));
852
1074
  const crossContextEvents = [];
853
1075
  for (const evt of config.crossContextEvents) {
854
- const sourceSpec = resolveCompositionMemberContextRef(resolveCompositionContextRef, evt.sourceContext, `sourceContext`, `event "${evt.eventType}"`, config.contexts);
1076
+ const sourceSpec = resolveCompositionMemberContextRef(resolveCompositionContextRef, evt.sourceContext, `sourceContext`, `event "${evt.eventType}"`, configuredContextRefs);
855
1077
  const [, eventNamePart] = evt.eventType.split(".");
856
1078
  const resolvedSubs = [];
857
1079
  for (const sub of evt.subscriptions) {
858
- const targetSpec = resolveCompositionMemberContextRef(resolveCompositionContextRef, sub.targetContext, `targetContext`, `subscription for event "${evt.eventType}"`, config.contexts);
1080
+ const targetSpec = resolveCompositionMemberContextRef(resolveCompositionContextRef, sub.targetContext, `targetContext`, `subscription for event "${evt.eventType}"`, configuredContextRefs);
859
1081
  if (sourceSpec.context.name.toLowerCase() === targetSpec.context.name.toLowerCase() && sourceSpec.context.modulePath.toLowerCase() === targetSpec.context.modulePath.toLowerCase()) throw new Error(`[composition_normalizer] sourceContext and targetContext are the same ("${evt.sourceContext}") in event "${evt.eventType}". Cross-context events must target a different context.`);
860
1082
  const commandNames = targetSpec.commands.map((c) => c.name);
861
1083
  if (!commandNames.includes(sub.action)) throw new Error(`[composition_normalizer] action "${sub.action}" in subscription for event "${evt.eventType}" does not match any command in target context "${sub.targetContext}". Available commands: ${commandNames.join(", ")}`);
@@ -884,9 +1106,60 @@ function buildNormalizedCompositionSpec(config, contextSpecs) {
884
1106
  contexts: resolvedContexts,
885
1107
  crossContextEvents,
886
1108
  acls,
1109
+ infrastructure,
887
1110
  materialization: { targetRoot: config.materialization.targetRoot }
888
1111
  };
889
1112
  }
1113
+ function resolveInfrastructureStrategy(input) {
1114
+ const supportedTuples = [
1115
+ {
1116
+ architecture: "physical-cqrs",
1117
+ persistence: "postgres",
1118
+ orm: "drizzle"
1119
+ },
1120
+ {
1121
+ architecture: "logical-cqrs",
1122
+ persistence: "postgres",
1123
+ orm: "drizzle"
1124
+ },
1125
+ {
1126
+ architecture: "logical-cqrs",
1127
+ persistence: "mysql",
1128
+ orm: "drizzle"
1129
+ },
1130
+ {
1131
+ architecture: "physical-cqrs",
1132
+ persistence: "mysql",
1133
+ orm: "drizzle"
1134
+ },
1135
+ {
1136
+ architecture: "physical-cqrs",
1137
+ persistence: "postgres",
1138
+ orm: "mikroorm"
1139
+ },
1140
+ {
1141
+ architecture: "logical-cqrs",
1142
+ persistence: "postgres",
1143
+ orm: "mikroorm"
1144
+ },
1145
+ {
1146
+ architecture: "physical-cqrs",
1147
+ persistence: "mysql",
1148
+ orm: "mikroorm"
1149
+ },
1150
+ {
1151
+ architecture: "logical-cqrs",
1152
+ persistence: "mysql",
1153
+ orm: "mikroorm"
1154
+ }
1155
+ ];
1156
+ 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"));
1157
+ return {
1158
+ architecture: input.architecture,
1159
+ persistence: input.persistence,
1160
+ orm: input.orm
1161
+ };
1162
+ }
890
1163
  function assertUniqueCanonicalContextSpecs(contextSpecs) {
891
1164
  const seenModulePaths = /* @__PURE__ */ new Map();
892
1165
  for (const spec of contextSpecs) {
@@ -1127,6 +1400,8 @@ function sliceContextIntoAggregateViews(contextSpec) {
1127
1400
  adapters,
1128
1401
  presentation: contextSpec.presentation,
1129
1402
  materialization: contextSpec.materialization,
1403
+ writeModelStrategy: contextSpec.writeModelStrategy,
1404
+ readModelStrategy: contextSpec.readModelStrategy,
1130
1405
  externalValueObjects: externalVos
1131
1406
  };
1132
1407
  });
@@ -1140,6 +1415,55 @@ function aggregateVariableName$1(spec) {
1140
1415
  function aggregateIdPropertyName$1(spec) {
1141
1416
  return `${aggregateVariableName$1(spec)}Id`;
1142
1417
  }
1418
+ function defaultIdBrandName$1(name) {
1419
+ const base = pascalCase(name);
1420
+ return base.endsWith("Id") ? base : `${base}Id`;
1421
+ }
1422
+ /**
1423
+ * If targetField is a string-typed field whose name ends with "Id",
1424
+ * the source expression likely holds a BrandedId object — unwrap it.
1425
+ */
1426
+ function maybeUnwrapBrandedId(expression, targetField, sourceOptional = false) {
1427
+ if (targetField.type !== "string") return expression;
1428
+ if (!camelCase(targetField.name).endsWith("Id")) return expression;
1429
+ if (sourceOptional) return `(() => { const value = ${expression}; return value == null ? undefined : brandedIdToString(value); })()`;
1430
+ return `brandedIdToString(${expression})`;
1431
+ }
1432
+ function metadataFieldFallback$1(field) {
1433
+ const fieldName = camelCase(field.name);
1434
+ if (fieldName === "recordedBy") return field.type === "string" ? "eventContext.recordedBy.value" : "eventContext.recordedBy";
1435
+ if (fieldName === "correlationId") return "eventContext.correlationId";
1436
+ if (fieldName === "causationId") return "eventContext.causationId";
1437
+ if (fieldName === "occurredAt" || fieldName === "createdAt") return field.type === "date" ? "new Date()" : "new Date().toISOString()";
1438
+ }
1439
+ function placeholderValueForField$2(field) {
1440
+ if (field.type === "object" && field.nestedFields && field.nestedFields.length > 0) {
1441
+ const nestedEntries = field.nestedFields.map((nestedField) => `${camelCase(nestedField.name)}: ${placeholderValueForField$2(nestedField)}`);
1442
+ const objectLiteral = nestedEntries.length > 0 ? `{ ${nestedEntries.join(", ")} }` : "{}";
1443
+ return field.array ? "[]" : objectLiteral;
1444
+ }
1445
+ if (field.array) return "[]";
1446
+ switch (field.type) {
1447
+ case "number": return "0";
1448
+ case "boolean": return "false";
1449
+ case "date": return "new Date()";
1450
+ default: return "\"\"";
1451
+ }
1452
+ }
1453
+ function fieldNameTokens(name) {
1454
+ return snakeCase(name).split("_").filter(Boolean);
1455
+ }
1456
+ function findSuffixMatchingField(targetFieldName, fields) {
1457
+ const targetTokens = fieldNameTokens(targetFieldName);
1458
+ return fields.filter((field) => {
1459
+ const candidateTokens = fieldNameTokens(field.name);
1460
+ if (candidateTokens.length < targetTokens.length) return false;
1461
+ return targetTokens.every((token, index) => candidateTokens[candidateTokens.length - targetTokens.length + index] === token);
1462
+ }).sort((left, right) => fieldNameTokens(left.name).length - fieldNameTokens(right.name).length)[0];
1463
+ }
1464
+ function aggregateBackingFieldName(name) {
1465
+ return `_${camelCase(name)}`;
1466
+ }
1143
1467
  function formatFieldType$1(field, indent = 0) {
1144
1468
  if (field.type === "object" && field.nestedFields && field.nestedFields.length > 0) {
1145
1469
  const objectType = `{\n${field.nestedFields.map((nestedField) => {
@@ -1196,6 +1520,10 @@ function getRehydrationGuardName(shape) {
1196
1520
  function annotateCompiledPredicateParameters$1(source) {
1197
1521
  return source.replace(/function check\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/g, "function check ($1: any)").replace(/\(\s*([A-Za-z_$][\w$]*)\s*\)\s*=>/g, "($1: any) =>");
1198
1522
  }
1523
+ function pruneUnusedBrandedIdImport$1(content) {
1524
+ if (content.includes("brandedIdToString(")) return content;
1525
+ return content.replace(`import { brandedIdToString, type BrandedId } from "../../../../../../lib/branded-id.ts";`, `import type { BrandedId } from "../../../../../../lib/branded-id.ts";`);
1526
+ }
1199
1527
  function getRehydrationFieldSpecsName(shape) {
1200
1528
  return `${pascalCase(shape.name)}RehydrationFieldSpecs`;
1201
1529
  }
@@ -1358,15 +1686,17 @@ function renderEffectValue(value, inputVar, selfRef = "this") {
1358
1686
  if (value === "now" || value === "now()") return "new Date()";
1359
1687
  if (value.startsWith("'") && value.endsWith("'")) return `"${value.slice(1, -1)}"`;
1360
1688
  if (/^\d+$/.test(value)) return value;
1361
- return value.replaceAll(/\binput\.([A-Za-z0-9_]+)/g, (_match, fieldName) => `${inputVar}.${camelCase(fieldName)}`).replaceAll(/\baggregate\.([A-Za-z0-9_]+)/g, (_match, fieldName) => `${selfRef}.${camelCase(fieldName)}`);
1689
+ return value.replace(/\binput\.([A-Za-z0-9_]+)/g, (_match, fieldName) => `${inputVar}.${camelCase(fieldName)}`).replace(/\baggregate\.([A-Za-z0-9_]+)/g, (_match, fieldName) => `${selfRef}.${camelCase(fieldName)}`);
1362
1690
  }
1363
- function renderEffectLines(effects, inputVar, spec, indent, selfRef = "this", isCreate = false, availableInputFields = /* @__PURE__ */ new Set()) {
1691
+ function renderEffectLines(effects, inputVar, spec, indent, selfRef = "this", isCreate = false, availableInputFields = /* @__PURE__ */ new Set(), inputFieldOptionality = /* @__PURE__ */ new Map()) {
1364
1692
  const contextName = spec.context.name;
1365
1693
  const aggregateName = pascalCase(spec.aggregate.name);
1366
1694
  const aggregateFieldNames = new Set(filterManagedAggregateFields$1(spec.aggregate.fields ?? []).map((f) => camelCase(f.name)));
1367
1695
  const aggregateFieldsByName = new Map(filterManagedAggregateFields$1(spec.aggregate.fields ?? []).map((field) => [camelCase(field.name), field]));
1368
1696
  let lastPushedEntityVar = null;
1369
1697
  let lastPushedEntityIdField = null;
1698
+ let lastPushedEntityTypeName = null;
1699
+ let lastPushedEntityFields = null;
1370
1700
  const idFieldName = spec.aggregate.idField ? camelCase(spec.aggregate.idField) : null;
1371
1701
  const lines = [];
1372
1702
  for (const effect of effects) switch (effect.kind) {
@@ -1375,7 +1705,11 @@ function renderEffectLines(effects, inputVar, spec, indent, selfRef = "this", is
1375
1705
  if (RESERVED_AGGREGATE_FIELD_NAMES$2.has(camelCase(effect.target))) break;
1376
1706
  const targetField = aggregateFieldsByName.get(camelCase(effect.target));
1377
1707
  const renderedValue = (effect.value === "now" || effect.value === "now()") && targetField ? targetField.type === "date" || targetField.type === "Date" ? "new Date()" : "new Date().toISOString()" : renderEffectValue(effect.value, inputVar, selfRef);
1378
- lines.push(`${indent}${selfRef}.${camelCase(effect.target)} = ${renderedValue};`);
1708
+ const targetAccessor = aggregateFieldNames.has(camelCase(effect.target)) ? `${selfRef}.${aggregateBackingFieldName(effect.target)}` : `${selfRef}.${camelCase(effect.target)}`;
1709
+ if (effect.value?.startsWith("input.") && inputFieldOptionality.get(camelCase(effect.value.slice(6)))) {
1710
+ const inputFieldName = camelCase(effect.value.slice(6));
1711
+ lines.push(`${indent}if (${inputVar}.${inputFieldName} !== undefined) {`, `${indent} ${targetAccessor} = ${renderedValue};`, `${indent}}`);
1712
+ } else lines.push(`${indent}${targetAccessor} = ${renderedValue};`);
1379
1713
  break;
1380
1714
  case "raise-event": {
1381
1715
  if (isCreate) break;
@@ -1388,29 +1722,42 @@ function renderEffectLines(effects, inputVar, spec, indent, selfRef = "this", is
1388
1722
  if (lastPushedEntityIdField && fieldName === camelCase(lastPushedEntityIdField)) return `${fieldName}: ${lastPushedEntityVar}.id.value`;
1389
1723
  if (aggregateFieldNames.has(fieldName)) {
1390
1724
  const aggregateField = aggregateFieldsByName.get(fieldName);
1391
- const aggregateAccess = `${selfRef}.${fieldName}`;
1392
- const metadataFallback = fieldName === "occurredAt" || fieldName.endsWith("At") ? f.type === "date" ? "new Date()" : "new Date().toISOString()" : fieldName === "recordedBy" || fieldName.endsWith("By") ? f.type === "string" ? "eventContext.recordedBy.value" : "eventContext.recordedBy" : void 0;
1725
+ const aggregateAccess = maybeUnwrapBrandedId(`${selfRef}.${fieldName}`, f, aggregateField?.optional ?? false);
1726
+ const metadataFallback = metadataFieldFallback$1(f);
1393
1727
  if (aggregateField?.optional) {
1394
- if (availableInputFields.has(fieldName)) return `${fieldName}: ${aggregateAccess} ?? ${inputVar}.${fieldName}`;
1395
- if (metadataFallback) return `${fieldName}: ${aggregateAccess} ?? ${metadataFallback}`;
1728
+ if (availableInputFields.has(fieldName)) return `${fieldName}: (${aggregateAccess} ?? ${maybeUnwrapBrandedId(`${inputVar}.${fieldName}`, f, inputFieldOptionality.get(fieldName) ?? false)}) as ${eventName}Payload["${fieldName}"]`;
1729
+ if (metadataFallback) return `${fieldName}: (${aggregateAccess} ?? ${metadataFallback}) as ${eventName}Payload["${fieldName}"]`;
1396
1730
  return `${fieldName}: ${aggregateAccess} as ${eventName}Payload["${fieldName}"]`;
1397
1731
  }
1398
1732
  return `${fieldName}: ${aggregateAccess}`;
1399
1733
  }
1400
- if (lastPushedEntityVar && !aggregateFieldNames.has(fieldName)) return `${fieldName}: (${lastPushedEntityVar} as any).${fieldName}`;
1401
- if (availableInputFields.has(fieldName)) return `${fieldName}: ${inputVar}.${fieldName}`;
1402
- if (fieldName === "occurredAt" || fieldName.endsWith("At")) return `${fieldName}: ${f.type === "date" ? "new Date()" : "new Date().toISOString()"}`;
1403
- 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}"]`;
1734
+ if (lastPushedEntityVar && !aggregateFieldNames.has(fieldName)) {
1735
+ const entityType = lastPushedEntityTypeName ?? "Record<string, unknown>";
1736
+ const matchedEntityField = lastPushedEntityFields ? findSuffixMatchingField(fieldName, lastPushedEntityFields) : void 0;
1737
+ if (matchedEntityField) return `${fieldName}: ${maybeUnwrapBrandedId(`(${lastPushedEntityVar} as ${entityType}).${camelCase(matchedEntityField.name)}`, f, matchedEntityField.optional)}${matchedEntityField.optional ? ` as ${eventName}Payload["${fieldName}"]` : ""}`;
1738
+ if (fieldName.endsWith("Id")) return `${fieldName}: ${lastPushedEntityVar}.id.value`;
1739
+ }
1740
+ if (availableInputFields.has(fieldName)) return `${fieldName}: ${maybeUnwrapBrandedId(`${inputVar}.${fieldName}`, f, inputFieldOptionality.get(fieldName) ?? false)} as ${eventName}Payload["${fieldName}"]`;
1741
+ const matchingEffect = effects.find((candidateEffect) => candidateEffect.kind === "set-field" && camelCase(candidateEffect.target) === fieldName);
1742
+ if (matchingEffect?.value) return `${fieldName}: ${renderEffectValue(matchingEffect.value, inputVar, selfRef)}`;
1743
+ const metadataFallback = metadataFieldFallback$1(f);
1744
+ if (metadataFallback) return `${fieldName}: ${metadataFallback}`;
1745
+ return `${fieldName}: undefined as unknown as ${eventName}Payload["${fieldName}"] /* TODO: unmapped ${eventName} payload field "${fieldName}" */`;
1405
1746
  }).join(", ")} }` : "{}";
1406
1747
  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
1748
  break;
1408
1749
  }
1409
1750
  case "increment":
1410
- lines.push(`${indent}${selfRef}.${camelCase(effect.target)} += ${effect.value ?? "1"};`);
1751
+ {
1752
+ const targetAccessor = aggregateFieldNames.has(camelCase(effect.target)) ? `${selfRef}.${aggregateBackingFieldName(effect.target)}` : `${selfRef}.${camelCase(effect.target)}`;
1753
+ lines.push(`${indent}${targetAccessor} += ${effect.value ?? "1"};`);
1754
+ }
1411
1755
  break;
1412
1756
  case "decrement":
1413
- lines.push(`${indent}${selfRef}.${camelCase(effect.target)} -= ${effect.value ?? "1"};`);
1757
+ {
1758
+ const targetAccessor = aggregateFieldNames.has(camelCase(effect.target)) ? `${selfRef}.${aggregateBackingFieldName(effect.target)}` : `${selfRef}.${camelCase(effect.target)}`;
1759
+ lines.push(`${indent}${targetAccessor} -= ${effect.value ?? "1"};`);
1760
+ }
1414
1761
  break;
1415
1762
  case "push-to-collection": {
1416
1763
  const targetChild = spec.aggregate.children?.find((c) => c.collectionFieldName === effect.target);
@@ -1418,9 +1765,19 @@ function renderEffectLines(effects, inputVar, spec, indent, selfRef = "this", is
1418
1765
  const entityName = pascalCase(targetChild.name);
1419
1766
  const createIdFn = `create${entityName}Id`;
1420
1767
  const entityVar = camelCase(targetChild.name);
1768
+ const childAssignmentLines = targetChild.fields.filter((field) => field.name !== targetChild.idField).flatMap((field) => {
1769
+ const childFieldName = camelCase(field.name);
1770
+ if (availableInputFields.has(childFieldName)) return [`${indent} ${childFieldName}: ${(inputFieldOptionality.get(childFieldName) ?? false) && !field.optional ? `${inputVar}.${childFieldName} ?? ${placeholderValueForField$2(field)}` : `${inputVar}.${childFieldName}`},`];
1771
+ const metadataFallback = metadataFieldFallback$1(field);
1772
+ if (metadataFallback) return [`${indent} ${childFieldName}: ${metadataFallback},`];
1773
+ if (field.optional) return [];
1774
+ return [`${indent} ${childFieldName}: undefined as unknown as ${formatFieldType$1(field)} /* TODO: unmapped ${entityName} field "${childFieldName}" */,`];
1775
+ });
1421
1776
  lastPushedEntityVar = entityVar;
1422
1777
  lastPushedEntityIdField = targetChild.idField;
1423
- 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});`);
1778
+ lastPushedEntityTypeName = entityName;
1779
+ lastPushedEntityFields = targetChild.fields;
1780
+ lines.push(`${indent}const ${entityVar} = ${entityName}.create({`, `${indent} ${camelCase(targetChild.idField)}: ${createIdFn}(crypto.randomUUID()),`, ...childAssignmentLines, `${indent}});`, `${indent}${selfRef}.${camelCase(effect.target)}.push(${entityVar});`);
1424
1781
  } else lines.push(`${indent}${selfRef}.${camelCase(effect.target)}.push(${renderEffectValue(effect.value, inputVar, selfRef)});`);
1425
1782
  break;
1426
1783
  }
@@ -1435,11 +1792,16 @@ function createAggregateStub(spec) {
1435
1792
  const idField = spec.aggregate.idField;
1436
1793
  const idType = spec.aggregate.idType;
1437
1794
  const nonIdFields = fields.filter((f) => f.name !== idField);
1795
+ const aggregatePropFieldNames = new Set(nonIdFields.map((field) => camelCase(field.name)));
1438
1796
  const commands = spec.commands.filter((c) => c.targetAggregate === spec.aggregate.name);
1439
1797
  const createCmd = commands.find((c) => c.commandKind === "create" && c.effects?.length);
1440
1798
  const mutationCmds = commands.filter((c) => c.commandKind !== "create" && c.effects?.length);
1441
1799
  const mutatedFieldNames = /* @__PURE__ */ new Set();
1442
- for (const cmd of commands) if (cmd.effects) {
1800
+ const createMutationEffects = createCmd ? createCmd.effects.filter((effect) => effect.kind !== "set-field" || effect.value !== `input.${effect.target}`) : [];
1801
+ for (const cmd of [...mutationCmds, ...createMutationEffects.length > 0 ? [{
1802
+ ...createCmd,
1803
+ effects: createMutationEffects
1804
+ }] : []]) if (cmd.effects) {
1443
1805
  for (const eff of cmd.effects) if (eff.kind === "set-field" || eff.kind === "increment" || eff.kind === "decrement") mutatedFieldNames.add(camelCase(eff.target));
1444
1806
  }
1445
1807
  const domainImports = [
@@ -1467,20 +1829,23 @@ function createAggregateStub(spec) {
1467
1829
  const eventImports = [];
1468
1830
  if (mutationRaisedEvents.size > 0) eventImports.push(`import { createDomainEvent } from "../../../../../../lib/domain-event.base.ts";`);
1469
1831
  if (needsEventContextImport) eventImports.push(`import type { EventContext } from "../../../../../shared-kernel/events/event-context.ts";`);
1832
+ let needsBrandedIdToString = false;
1470
1833
  for (const evtName of mutationRaisedEvents) {
1471
1834
  const payloadName = `${pascalCase(evtName)}Payload`;
1472
1835
  const eventFileName = kebabCase(evtName);
1473
1836
  eventImports.push(`import type { ${payloadName} } from "../../events/${eventFileName}.event.ts";`);
1837
+ if (spec.domainEvents.find((e) => pascalCase(e.name) === pascalCase(evtName))?.fields.some((f) => f.type === "string" && camelCase(f.name).endsWith("Id"))) needsBrandedIdToString = true;
1474
1838
  }
1839
+ if (needsBrandedIdToString) domainImports[1] = `import { brandedIdToString, type BrandedId } from "../../../../../../lib/branded-id.ts";`;
1475
1840
  const invariantMap = /* @__PURE__ */ new Map();
1476
1841
  for (const inv of spec.aggregate.invariants ?? []) invariantMap.set(inv.name, inv);
1477
1842
  const domainErrorByInvariant = /* @__PURE__ */ new Map();
1478
1843
  for (const errDef of spec.aggregate.domainErrors ?? []) domainErrorByInvariant.set(errDef.invariantName, errDef);
1479
1844
  const referencedFactoryNames = /* @__PURE__ */ new Set();
1480
1845
  const referencedErrorTypeNames = /* @__PURE__ */ new Set();
1481
- let needsResultImport = false;
1846
+ let needsErrImport = false;
1482
1847
  for (const cmd of mutationCmds) if (cmd.preconditions?.length) {
1483
- needsResultImport = true;
1848
+ needsErrImport = true;
1484
1849
  for (const pre of cmd.preconditions) {
1485
1850
  const errDef = domainErrorByInvariant.get(pre);
1486
1851
  if (errDef) {
@@ -1490,7 +1855,8 @@ function createAggregateStub(spec) {
1490
1855
  }
1491
1856
  }
1492
1857
  const invariantImports = [];
1493
- if (needsResultImport) invariantImports.push(`import { type Result, ok, err } from "../../../../../shared-kernel/result.ts";`);
1858
+ invariantImports.push(`import { type Result, ok${needsErrImport ? ", err" : ""} } from "../../../../../shared-kernel/result.ts";`);
1859
+ invariantImports.push(`import type { DomainError } from "../../../../../../lib/domain-error.ts";`);
1494
1860
  if (referencedFactoryNames.size > 0) {
1495
1861
  const aggregateDir = kebabCase(spec.aggregate.name);
1496
1862
  const imports = [...referencedFactoryNames, ...referencedErrorTypeNames].sort();
@@ -1513,21 +1879,41 @@ function createAggregateStub(spec) {
1513
1879
  const classFields = nonIdFields.map((f) => {
1514
1880
  const opt = f.optional ? "?" : "";
1515
1881
  const fieldName = camelCase(f.name);
1516
- return ` ${mutatedFieldNames.has(fieldName) ? "" : "readonly "}${fieldName}${opt}: ${formatFieldType$1(f)};`;
1882
+ return ` private ${mutatedFieldNames.has(fieldName) ? "" : "readonly "}${aggregateBackingFieldName(fieldName)}${opt}: ${formatFieldType$1(f)};`;
1517
1883
  }).join("\n");
1884
+ const scalarGetters = nonIdFields.map((f) => {
1885
+ const opt = f.optional ? " | undefined" : "";
1886
+ const fieldName = camelCase(f.name);
1887
+ return `
1888
+ get ${fieldName}(): ${formatFieldType$1(f)}${opt} {
1889
+ return this.${aggregateBackingFieldName(fieldName)};
1890
+ }`;
1891
+ }).join("");
1518
1892
  const collectionFields = (spec.aggregate.children ?? []).map((child) => {
1519
1893
  const entityName = pascalCase(child.name);
1520
1894
  return ` readonly ${camelCase(child.collectionFieldName)}: ${entityName}[] = [];`;
1521
1895
  });
1522
- const constructorAssignments = nonIdFields.map((f) => ` this.${camelCase(f.name)} = props.${camelCase(f.name)};`).join("\n");
1523
- const idTypeStr = idType ? `BrandedId<"${idType}">` : `BrandedId<string>`;
1896
+ const constructorAssignments = nonIdFields.map((f) => ` this.${aggregateBackingFieldName(f.name)} = props.${camelCase(f.name)};`).join("\n");
1897
+ const idTypeStr = `BrandedId<"${idType && idType !== "string" ? pascalCase(idType) : defaultIdBrandName$1(spec.aggregate.name)}">`;
1524
1898
  let createBody;
1525
- if (createCmd?.effects?.length) createBody = ` const instance = new ${aggregateClassName}(id, 0, props);
1526
- ${renderEffectLines(createCmd.effects, "props", spec, " ", "instance", true).join("\n")}
1527
- return instance;`;
1528
- else createBody = ` return new ${aggregateClassName}(id, 0, props);`;
1899
+ if (createCmd?.effects?.length) {
1900
+ const filteredCreateEffects = createCmd.effects.filter((effect) => effect.kind !== "set-field" || effect.value !== `input.${effect.target}`);
1901
+ const createInputFields = /* @__PURE__ */ new Set();
1902
+ for (const field of nonIdFields) createInputFields.add(camelCase(field.name));
1903
+ for (const eff of createCmd.effects) if (eff.value?.startsWith("input.")) {
1904
+ const inputFieldName = camelCase(eff.value.slice(6));
1905
+ if (aggregatePropFieldNames.has(inputFieldName)) createInputFields.add(inputFieldName);
1906
+ }
1907
+ createBody = ` const instance = new ${aggregateClassName}(id, 0, props);
1908
+ ${renderEffectLines(filteredCreateEffects, "props", spec, " ", "instance", true, createInputFields, new Map(nonIdFields.map((field) => [camelCase(field.name), field.optional]))).join("\n")}
1909
+ return ok(instance);`;
1910
+ } else createBody = ` return ok(new ${aggregateClassName}(id, 0, props));`;
1529
1911
  const fieldTypeMap = /* @__PURE__ */ new Map();
1530
- for (const f of fields ?? []) fieldTypeMap.set(camelCase(f.name), formatFieldType$1(f));
1912
+ const fieldOptionalMap = /* @__PURE__ */ new Map();
1913
+ for (const f of fields ?? []) {
1914
+ fieldTypeMap.set(camelCase(f.name), formatFieldType$1(f));
1915
+ fieldOptionalMap.set(camelCase(f.name), f.optional);
1916
+ }
1531
1917
  const mutationMethods = [];
1532
1918
  for (const cmd of mutationCmds) {
1533
1919
  const methodName = commandDomainMethodName$1(spec, cmd.name);
@@ -1548,24 +1934,22 @@ ${renderEffectLines(createCmd.effects, "props", spec, " ", "instance", true).
1548
1934
  if (cmd.lifecycle === "mutate") methodLines.push(` this.incrementVersion();`);
1549
1935
  const cmdEmitsEvents = (cmd.effects ?? []).some((e) => e.kind === "raise-event");
1550
1936
  const inputFields = /* @__PURE__ */ new Map();
1551
- for (const field of cmd.inputFields ?? []) inputFields.set(camelCase(field.name), formatFieldType$1(field));
1552
- for (const eff of cmd.effects ?? []) {
1553
- if (eff.value?.startsWith("input.")) {
1554
- const inputFieldName = camelCase(eff.value.slice(6));
1555
- const targetFieldName = camelCase(eff.target);
1556
- const fieldType = fieldTypeMap.get(targetFieldName) ?? "unknown";
1557
- inputFields.set(inputFieldName, fieldType);
1558
- }
1559
- if (eff.kind === "push-to-collection") {
1560
- const targetChild = spec.aggregate.children?.find((c) => c.collectionFieldName === eff.target);
1561
- if (targetChild) {
1562
- for (const f of targetChild.fields) if (f.name !== targetChild.idField) inputFields.set(camelCase(f.name), formatFieldType$1(f));
1563
- }
1564
- }
1937
+ for (const field of cmd.inputFields ?? []) inputFields.set(camelCase(field.name), {
1938
+ type: formatFieldType$1(field),
1939
+ optional: field.optional
1940
+ });
1941
+ for (const eff of cmd.effects ?? []) if (eff.value?.startsWith("input.")) {
1942
+ const inputFieldName = camelCase(eff.value.slice(6));
1943
+ const targetFieldName = camelCase(eff.target);
1944
+ const fieldType = fieldTypeMap.get(targetFieldName) ?? "unknown";
1945
+ if (!inputFields.has(inputFieldName)) inputFields.set(inputFieldName, {
1946
+ type: fieldType,
1947
+ optional: fieldOptionalMap.get(targetFieldName) ?? false
1948
+ });
1565
1949
  }
1566
- const inputType = inputFields.size > 0 ? `{ ${[...inputFields.entries()].map(([k, t]) => `${k}: ${t}`).join("; ")} }` : "Record<string, unknown>";
1950
+ const inputType = inputFields.size > 0 ? `{ ${[...inputFields.entries()].map(([k, value]) => `${k}${value.optional ? "?" : ""}: ${value.type}`).join("; ")} }` : "Record<string, unknown>";
1567
1951
  const extraParams = cmdEmitsEvents ? `, eventContext: EventContext` : "";
1568
- methodLines.push(...renderEffectLines(cmd.effects, "input", spec, " ", "this", false, new Set(inputFields.keys())));
1952
+ methodLines.push(...renderEffectLines(cmd.effects, "input", spec, " ", "this", false, new Set(inputFields.keys()), new Map([...inputFields.entries()].map(([name, meta]) => [name, meta.optional]))));
1569
1953
  const inputParamName = methodLines.some((line) => /\binput\b/.test(line)) ? "input" : "_input";
1570
1954
  if (resolvedInvariants.length > 0) {
1571
1955
  const errorUnion = resolvedInvariants.map((inv) => `${pascalCase(inv.name)}Error`).join(" | ");
@@ -1601,10 +1985,9 @@ ${methodLines.join("\n")}
1601
1985
  const opt = f.optional ? "?" : "";
1602
1986
  persistenceLines.push(` readonly ${camelCase(f.name)}${opt}: ${formatPersistenceFieldType(f)};`);
1603
1987
  }
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 });` : "";
1607
- return `${imports}
1988
+ const fromPersistenceProps = nonIdFields.map((f) => ` ${camelCase(f.name)}: state.${camelCase(f.name)},`).join("\n");
1989
+ const collectionAssignments = (spec.aggregate.children ?? []).map((child) => ` instance.${camelCase(child.collectionFieldName)}.push(...state.${camelCase(child.collectionFieldName)});`).join("\n");
1990
+ return pruneUnusedBrandedIdImport$1(`${imports}
1608
1991
 
1609
1992
  export interface ${aggregateClassName}Props {
1610
1993
  ${propsLines}
@@ -1630,9 +2013,9 @@ ${classFields}${collectionFields.length > 0 ? "\n" + collectionFields.join("\n")
1630
2013
  ${constructorAssignments}
1631
2014
  }
1632
2015
 
1633
- static create(id: ${idTypeStr}, props: ${aggregateClassName}Props): ${aggregateClassName} {
2016
+ static create(id: ${idTypeStr}, props: ${aggregateClassName}Props): Result<${aggregateClassName}, DomainError> {
1634
2017
  ${createBody}
1635
- }
2018
+ }${scalarGetters}
1636
2019
 
1637
2020
  static fromPersistence(state: ${rehydrationStateName}): ${aggregateClassName} {
1638
2021
  ${getRehydrationGuardName({
@@ -1644,20 +2027,23 @@ ${createBody}
1644
2027
  rehydrationStateName,
1645
2028
  children: spec.aggregate.children ?? []
1646
2029
  })}(state);
1647
- const instance = new ${aggregateClassName}(state.${camelCase(idField)}, state.version, state as unknown as ${aggregateClassName}Props);
1648
- ${fromPersistenceAssignment}
2030
+ const props: ${aggregateClassName}Props = {
2031
+ ${fromPersistenceProps}
2032
+ };
2033
+ const instance = new ${aggregateClassName}(state.${camelCase(idField)}, state.version, props);
2034
+ ${collectionAssignments}
1649
2035
  return instance;
1650
2036
  }${mutationMethods.join("")}
1651
2037
  }
1652
- `;
2038
+ `);
1653
2039
  }
1654
- function createEntityStub(entity, depthFromDomain = 1) {
2040
+ function createEntityStub(entity, _depthFromDomain = 1) {
1655
2041
  const entityName = `${pascalCase(entity.name)}Entity`;
1656
2042
  const fields = entity.fields;
1657
2043
  const idField = entity.idField;
1658
2044
  const idType = entity.idType;
1659
2045
  const nonIdFields = fields.filter((f) => f.name !== idField);
1660
- const idTypeStr = idType && idType !== "string" ? `BrandedId<"${idType}">` : `BrandedId<string>`;
2046
+ const idTypeStr = `BrandedId<"${idType && idType !== "string" ? pascalCase(idType) : defaultIdBrandName$1(entity.name)}">`;
1661
2047
  const libRelPath = "../../../../../lib";
1662
2048
  const imports = [
1663
2049
  `import { Entity } from "${libRelPath}/entity.base.ts";`,
@@ -1703,9 +2089,8 @@ function createEntityStub(entity, depthFromDomain = 1) {
1703
2089
  const opt = f.optional ? "?" : "";
1704
2090
  persistenceLines.push(` readonly ${camelCase(f.name)}${opt}: ${formatPersistenceFieldType(f)};`);
1705
2091
  }
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 });` : "";
2092
+ const entityFromPersistenceProps = nonIdFields.map((f) => ` ${camelCase(f.name)}: state.${camelCase(f.name)},`).join("\n");
2093
+ const entityCollectionAssignments = children.map((child) => ` instance.${camelCase(child.collectionFieldName)}.push(...state.${camelCase(child.collectionFieldName)});`).join("\n");
1709
2094
  return `${imports.join("\n")}
1710
2095
 
1711
2096
  export interface ${entityName}Props {
@@ -1746,8 +2131,11 @@ ${constructorAssignments}
1746
2131
  rehydrationStateName,
1747
2132
  children: entity.children ?? []
1748
2133
  })}(state);
1749
- const instance = new ${entityName}(state.${camelCase(idField)}, state as unknown as ${entityName}Props);
1750
- ${entityFromPersistenceAssignment}
2134
+ const props: ${entityName}Props = {
2135
+ ${entityFromPersistenceProps}
2136
+ };
2137
+ const instance = new ${entityName}(state.${camelCase(idField)}, props);
2138
+ ${entityCollectionAssignments}
1751
2139
  return instance;
1752
2140
  }
1753
2141
  }
@@ -1932,10 +2320,10 @@ function aggregateIdPropertyName(spec) {
1932
2320
  }
1933
2321
  function metadataFieldFallback(field) {
1934
2322
  const fieldName = camelCase(field.name);
1935
- if (fieldName === "recordedBy" || fieldName.endsWith("By")) return field.type === "string" ? "eventContext.recordedBy.value" : "eventContext.recordedBy";
2323
+ if (fieldName === "recordedBy") return field.type === "string" ? "eventContext.recordedBy.value" : "eventContext.recordedBy";
1936
2324
  if (fieldName === "correlationId") return "eventContext.correlationId";
1937
2325
  if (fieldName === "causationId") return "eventContext.causationId";
1938
- if (fieldName === "occurredAt" || fieldName.endsWith("At")) return field.type === "date" ? "new Date()" : "new Date().toISOString()";
2326
+ if (fieldName === "occurredAt" || fieldName === "createdAt") return field.type === "date" ? "new Date()" : "new Date().toISOString()";
1939
2327
  }
1940
2328
  function findRepositoryPort(spec, targetAggregate) {
1941
2329
  const aggregateName = targetAggregate ?? ("aggregate" in spec ? spec.aggregate.name : void 0);
@@ -1972,7 +2360,7 @@ function formatFieldType(field, indent = 0) {
1972
2360
  function maybeUnwrapBrandedIdExpression(expression, targetField) {
1973
2361
  if (targetField.type !== "string") return expression;
1974
2362
  if (!camelCase(targetField.name).endsWith("Id")) return expression;
1975
- const unwrap = `typeof ${expression} === "object" && ${expression} !== null && "value" in (${expression} as Record<string, unknown>) ? String((${expression} as { value: unknown }).value) : String(${expression})`;
2363
+ const unwrap = `brandedIdToString(${expression})`;
1976
2364
  return targetField.optional ? `${expression} === undefined ? undefined : (${unwrap})` : unwrap;
1977
2365
  }
1978
2366
  function projectValueToTargetField(expression, targetField, sourceField, depth = 0) {
@@ -1980,11 +2368,13 @@ function projectValueToTargetField(expression, targetField, sourceField, depth =
1980
2368
  const sourceNestedFields = new Map((sourceField?.nestedFields ?? []).map((nestedField) => [camelCase(nestedField.name), nestedField]));
1981
2369
  if (targetField.array) {
1982
2370
  const itemVar = `item${depth}`;
1983
- return `${expression}.map((${itemVar}: any) => ({ ${targetField.nestedFields.map((nestedField) => {
2371
+ const objectFields = targetField.nestedFields.map((nestedField) => {
1984
2372
  const nestedName = camelCase(nestedField.name);
1985
2373
  const nestedSourceField = sourceNestedFields.get(nestedName);
1986
2374
  return `${nestedName}: ${projectValueToTargetField(`${itemVar}.${nestedName}`, nestedField, nestedSourceField, depth + 1)}`;
1987
- }).join(", ")} }))`;
2375
+ });
2376
+ sourceField?.nestedFields ?? targetField.nestedFields;
2377
+ return `${expression}.map((${itemVar}) => ({ ${objectFields.join(", ")} }))`;
1988
2378
  }
1989
2379
  return `{ ${targetField.nestedFields.map((nestedField) => {
1990
2380
  const nestedName = camelCase(nestedField.name);
@@ -2037,7 +2427,11 @@ function coerceExpressionToFieldType(expression, targetField, sourceField) {
2037
2427
  return expression;
2038
2428
  }
2039
2429
  function renderInputExpression(value, inputRoot) {
2040
- return value.replaceAll(/\binput\.([A-Za-z0-9_]+)/g, (_match, fieldName) => `${inputRoot}.${camelCase(fieldName)}`);
2430
+ return value.replace(/\binput\.([A-Za-z0-9_]+)/g, (_match, fieldName) => `${inputRoot}.${camelCase(fieldName)}`);
2431
+ }
2432
+ function pruneUnusedBrandedIdImport(content) {
2433
+ if (content.includes("brandedIdToString(")) return content;
2434
+ return content.replace(`import { brandedIdToString, createBrandedId } from "../../../../../lib/branded-id.ts";`, `import { createBrandedId } from "../../../../../lib/branded-id.ts";`).replace(`import { brandedIdToString } from "../../../../../lib/branded-id.ts";\n`, "");
2041
2435
  }
2042
2436
  function inferFilterFieldType(query, fieldName) {
2043
2437
  switch (query.outputFields.find((f) => camelCase(f.name) === camelCase(fieldName))?.type ?? "string") {
@@ -2059,7 +2453,7 @@ function listQuerySortFieldTypeName$1(query) {
2059
2453
  function listQueryRowTypeName$1(query) {
2060
2454
  return `${listQueryTypeBaseName$1(query)}Row`;
2061
2455
  }
2062
- function readModelContractName$3(readModelName) {
2456
+ function readModelContractName$2(readModelName) {
2063
2457
  const baseName = pascalCase(readModelName);
2064
2458
  return baseName.endsWith("View") ? baseName : `${baseName}View`;
2065
2459
  }
@@ -2073,7 +2467,7 @@ function queryOutputExportName$1(query) {
2073
2467
  return query.outputSchemaExportName?.trim() || void 0;
2074
2468
  }
2075
2469
  function queryReadModelPortTypeName$1(query) {
2076
- return `${readModelContractName$3(resolvedReadModelName$2(query))}RepositoryPort`;
2470
+ return `${readModelContractName$2(resolvedReadModelName$2(query))}RepositoryPort`;
2077
2471
  }
2078
2472
  function queryReadModelPortFileName(query) {
2079
2473
  return `read-models/${readModelPortBaseFileName(resolvedReadModelName$2(query))}.repository.port.ts`;
@@ -2137,7 +2531,7 @@ function createListQueryHandlerStub(_spec, query) {
2137
2531
  const readModelPortType = queryReadModelPortTypeName$1(query);
2138
2532
  const readModelVariable = queryReadModelVariableName$1(query);
2139
2533
  const readModelMethodName = queryReadModelMethodName$1(query);
2140
- const viewFileBase = queryViewFileBase$2(query);
2534
+ const viewFileBase = queryViewFileBase$1(query);
2141
2535
  return `import type { ${queryTypeName} } from "./${kebabCase(query.name)}.query.ts";
2142
2536
  import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";
2143
2537
  import type { ${readModelPortType} } from "../ports/${queryReadModelPortFileName(query)}";
@@ -2160,17 +2554,17 @@ function createHandlerDepsStub(spec) {
2160
2554
  const contextPascal = pascalCase(spec.context.name);
2161
2555
  const aggregateVar = camelCase(spec.aggregate.name);
2162
2556
  const aggregatePascal = pascalCase(spec.aggregate.name);
2163
- const idType = spec.aggregate.idType;
2164
- idType && idType !== "string" && `${pascalCase(idType)}`;
2165
2557
  const repoPort = findRepositoryPort(spec);
2166
2558
  const repoTypeName = repoPort ? repositoryPortTypeName(repoPort.name) : `${aggregatePascal}Repository`;
2167
2559
  return `import type { Transaction } from "../../../../lib/transaction.ts";
2560
+ import type { AggregateEventTracker } from "../../../shared-kernel/events/aggregate-event-tracker.ts";
2168
2561
  import type { EventCollector } from "../../../shared-kernel/events/event-collector.ts";
2169
2562
  import type { ${repoTypeName} } from "./ports/${repoPort ? repositoryPortFileName(repoPort.name) : `${kebabCase(spec.aggregate.name)}-repository.port.ts`}";
2170
2563
 
2171
2564
  export type ${contextPascal}CommandHandlerDeps = {
2172
2565
  tx: Transaction;
2173
2566
  eventCollector: EventCollector;
2567
+ aggregateEventTracker: AggregateEventTracker;
2174
2568
  repos: {
2175
2569
  ${aggregateVar}s: ${repoTypeName};
2176
2570
  };
@@ -2207,9 +2601,9 @@ ${query.outputFields.map((f) => {
2207
2601
  function queryOutputContractName$1(query) {
2208
2602
  const outputExportName = queryOutputExportName$1(query);
2209
2603
  if (outputExportName) return pascalCase(outputExportName);
2210
- return readModelContractName$3(resolvedReadModelName$2(query));
2604
+ return readModelContractName$2(resolvedReadModelName$2(query));
2211
2605
  }
2212
- function queryViewFileBase$2(query) {
2606
+ function queryViewFileBase$1(query) {
2213
2607
  const outputExportName = queryOutputExportName$1(query);
2214
2608
  if (outputExportName) return kebabCase(outputExportName).replace(/-view$/, "");
2215
2609
  return kebabCase(resolvedReadModelName$2(query)).replace(/-view$/, "");
@@ -2260,7 +2654,8 @@ function buildCreateHandlerBody(spec, command) {
2260
2654
  const assignedValue = createPropAssignments.get(fieldName) ?? (commandInputFieldNames.has(fieldName) ? commandInputField?.optional && !field.optional ? `command.payload.${fieldName} ?? ${defaultValue}` : `command.payload.${fieldName}` : defaultValue);
2261
2655
  if (assignedValue !== void 0) propsLines.push(` ${fieldName}: ${assignedValue},`);
2262
2656
  }
2263
- const idTypeName = hasCustomIdType ? pascalCase(idType) : `BrandedId<"string">`;
2657
+ const defaultIdBrand = `${pascalCase(spec.aggregate.name)}Id`;
2658
+ const idTypeName = hasCustomIdType ? pascalCase(idType) : `BrandedId<"${defaultIdBrand}">`;
2264
2659
  const creationEventName = command.emits?.[0];
2265
2660
  const creationEvent = creationEventName ? spec.domainEvents.find((e) => pascalCase(e.name) === pascalCase(creationEventName)) : void 0;
2266
2661
  const aggregateFieldNames = new Set((spec.aggregate.fields ?? []).map((field) => camelCase(field.name)));
@@ -2287,18 +2682,20 @@ function buildCreateHandlerBody(spec, command) {
2287
2682
  imports.push(`import type { BrandedId } from "../../../../../lib/branded-id.ts";`);
2288
2683
  }
2289
2684
  if (creationEvent) {
2685
+ if (hasCustomIdType) imports.push(`import { brandedIdToString } from "../../../../../lib/branded-id.ts";`);
2686
+ else imports[imports.findIndex((entry) => entry === `import { createBrandedId } from "../../../../../lib/branded-id.ts";`)] = `import { brandedIdToString, createBrandedId } from "../../../../../lib/branded-id.ts";`;
2290
2687
  imports.push(`import { createDomainEvent } from "../../../../../lib/domain-event.base.ts";`);
2291
2688
  const payloadName = `${pascalCase(creationEventName)}Payload`;
2292
2689
  imports.push(`import type { ${payloadName} } from "../../domain/events/${kebabCase(creationEventName)}.event.ts";`);
2293
2690
  }
2294
- const idExpr = hasCustomIdType ? `create${pascalCase(idType)}(crypto.randomUUID())` : `createBrandedId("${idType || "string"}", crypto.randomUUID())`;
2691
+ const idExpr = hasCustomIdType ? `create${pascalCase(idType)}(crypto.randomUUID())` : `createBrandedId("${hasCustomIdType ? idType : defaultIdBrand}", crypto.randomUUID())`;
2295
2692
  let creationEventLines = "";
2296
2693
  if (creationEvent) {
2297
2694
  const payloadName = `${pascalCase(creationEventName)}Payload`;
2298
2695
  const payloadFields = [];
2299
2696
  for (const f of creationEvent.fields) {
2300
2697
  const fieldName = camelCase(f.name);
2301
- if (fieldName === camelCase(idField)) payloadFields.push(` ${fieldName}: id.value,`);
2698
+ if (fieldName === camelCase(idField) || fieldName.endsWith("Id") && camelCase(idField).toLowerCase().endsWith(fieldName.toLowerCase())) payloadFields.push(` ${fieldName}: id.value,`);
2302
2699
  else if (aggregateChildCollections.has(fieldName)) {
2303
2700
  const aggregateCollectionField = aggregateChildCollections.get(fieldName);
2304
2701
  payloadFields.push(` ${fieldName}: ${projectValueToTargetField(`${aggregateVar}.${fieldName}`, f, aggregateCollectionField)},`);
@@ -2307,10 +2704,11 @@ function buildCreateHandlerBody(spec, command) {
2307
2704
  const aggregateExpr = fieldName === "version" && (f.type === "string" || f.type === "enum") ? `String(${aggregateVar}.version)` : `${aggregateVar}.${fieldName}`;
2308
2705
  payloadFields.push(` ${fieldName}: ${projectValueToTargetField(aggregateExpr, f, aggregateField)},`);
2309
2706
  } else if (commandInputFieldNames.has(fieldName)) payloadFields.push(` ${fieldName}: ${projectValueToTargetField(`command.payload.${fieldName}`, f, commandInputFields.get(fieldName))},`);
2310
- else if (fieldName === "recordedBy" || fieldName === "correlationId" || fieldName === "causationId") {
2311
- const metadataExpr = fieldName === "recordedBy" ? f.type === "string" ? "eventContext.recordedBy.value" : "eventContext.recordedBy" : `eventContext.${fieldName}`;
2312
- payloadFields.push(` ${fieldName}: ${metadataExpr},`);
2313
- } else payloadFields.push(` ${fieldName}: undefined as unknown as ${payloadName}["${fieldName}"],`);
2707
+ else {
2708
+ const metadataExpr = metadataFieldFallback(f);
2709
+ if (metadataExpr) payloadFields.push(` ${fieldName}: ${metadataExpr},`);
2710
+ else payloadFields.push(` ${fieldName}: undefined /* TODO: unmapped ${payloadName} field "${fieldName}" */,`);
2711
+ }
2314
2712
  }
2315
2713
  creationEventLines = `
2316
2714
  deps.eventCollector.collect([
@@ -2331,9 +2729,10 @@ ${payloadFields.join("\n")}
2331
2729
  }
2332
2730
  const handlerErrorType = `${pascalCase(command.name)}HandlerError`;
2333
2731
  const eventContextParamName = [...propsLines, creationEventLines].some((line) => line.includes("eventContext")) ? "eventContext" : "_eventContext";
2334
- return `${imports.join("\n")}
2732
+ imports.push(`import type { DomainError } from "../../../../../lib/domain-error.ts";`);
2733
+ return pruneUnusedBrandedIdImport(`${imports.join("\n")}
2335
2734
 
2336
- export type ${handlerErrorType} = never;
2735
+ export type ${handlerErrorType} = DomainError;
2337
2736
 
2338
2737
  export async function ${handlerFnName}(
2339
2738
  command: ${commandTypeName},
@@ -2341,16 +2740,18 @@ export async function ${handlerFnName}(
2341
2740
  ${eventContextParamName}: EventContext,
2342
2741
  ): Promise<Result<{ ${idField}: ${idTypeName} }, ${handlerErrorType}>> {
2343
2742
  const id = ${idExpr};
2344
- const ${aggregateVar} = ${aggregateClassName}.create(
2743
+ const created = ${aggregateClassName}.create(
2345
2744
  id,
2346
2745
  {
2347
2746
  ${propsLines.join("\n")}
2348
2747
  },
2349
2748
  );
2749
+ if (!created.ok) return created;
2750
+ const ${aggregateVar} = created.value;
2350
2751
  await deps.repos.${camelCase(spec.aggregate.name)}s.create(${aggregateVar}, deps.tx);${creationEventLines}
2351
2752
  return ok({ ${idField}: ${aggregateVar}.id });
2352
2753
  }
2353
- `;
2754
+ `);
2354
2755
  }
2355
2756
  function buildMutationHandlerBody(spec, command) {
2356
2757
  const commandTypeName = `${pascalCase(command.name)}Command`;
@@ -2387,7 +2788,7 @@ function buildMutationHandlerBody(spec, command) {
2387
2788
  }
2388
2789
  imports.push(`import type { ${notFoundErrorType} } from "../../domain/errors/${aggregateDir}-application-errors.ts";`);
2389
2790
  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";
2791
+ const domainMethodArg = cmdEmitsEvents ? "command.payload, eventContext" : "command.payload";
2391
2792
  const loadByField = command.loadBy?.startsWith("input.") ? command.loadBy.slice(6) : aggregateId;
2392
2793
  const brandedIdExpr = hasCustomIdType && idCreatorFn ? `${idCreatorFn}(command.payload.${loadByField})` : `command.payload.${loadByField}`;
2393
2794
  let bodyLines;
@@ -2399,7 +2800,7 @@ function buildMutationHandlerBody(spec, command) {
2399
2800
  ` const result = ${aggregateVar}.${methodName}(${domainMethodArg});`,
2400
2801
  ` if (!result.ok) return result;`,
2401
2802
  ` await deps.repos.${camelCase(spec.aggregate.name)}s.save(${aggregateVar}, loadedVersion, deps.tx);`,
2402
- ` deps.eventCollector.collect(${aggregateVar}.pullDomainEvents());`,
2803
+ ` deps.aggregateEventTracker.track(${aggregateVar});`,
2403
2804
  ` return ok(undefined);`
2404
2805
  ];
2405
2806
  else bodyLines = [
@@ -2409,7 +2810,7 @@ function buildMutationHandlerBody(spec, command) {
2409
2810
  ` const loadedVersion = ${aggregateVar}.version;`,
2410
2811
  ` ${aggregateVar}.${methodName}(${domainMethodArg});`,
2411
2812
  ` await deps.repos.${camelCase(spec.aggregate.name)}s.save(${aggregateVar}, loadedVersion, deps.tx);`,
2412
- ` deps.eventCollector.collect(${aggregateVar}.pullDomainEvents());`,
2813
+ ` deps.aggregateEventTracker.track(${aggregateVar});`,
2413
2814
  ` return ok(undefined);`
2414
2815
  ];
2415
2816
  const handlerErrorTypeExport = hasPreconditions ? `export type ${handlerErrorType} =\n | ${errorTypes.join("\n | ")};\n` : `export type ${handlerErrorType} = ${notFoundErrorType};\n`;
@@ -2431,23 +2832,49 @@ function createCommandHandlerStub(spec, command) {
2431
2832
  if (command.commandKind === "create") return buildCreateHandlerBody(spec, command);
2432
2833
  if (command.loadBy) return buildMutationHandlerBody(spec, command);
2433
2834
  }
2835
+ `${pascalCase(command.name)}`;
2836
+ `${pascalCase(command.name)}`;
2837
+ `${pascalCase(spec.context.name)}`;
2838
+ const capabilityReasons = command.capability?.reasons ?? [];
2839
+ const diagnosticSuffix = capabilityReasons.length > 0 ? ` Reasons: ${capabilityReasons.join("; ")}.` : "";
2840
+ `${spec.context.name}${command.name}${diagnosticSuffix}`;
2841
+ return null;
2842
+ }
2843
+ function createUnsupportedCommandContract(spec, command) {
2434
2844
  const commandTypeName = `${pascalCase(command.name)}Command`;
2435
- const handlerFnName = `handle${pascalCase(command.name)}`;
2436
2845
  const depsTypeName = `${pascalCase(spec.context.name)}CommandHandlerDeps`;
2846
+ const contractName = `${pascalCase(command.name)}HandlerContract`;
2437
2847
  return `import type { ${commandTypeName} } from "../commands/${kebabCase(command.name)}.command.ts";
2438
2848
  import type { ${depsTypeName} } from "../handler-deps.ts";
2439
2849
  import type { EventContext } from "../../../../shared-kernel/events/event-context.ts";
2440
2850
 
2441
- export async function ${handlerFnName}(
2442
- command: ${commandTypeName},
2443
- deps: ${depsTypeName},
2444
- eventContext: EventContext,
2445
- ): Promise<void> {
2446
- void command;
2447
- void deps;
2448
- void eventContext;
2449
- // TODO: load aggregate, execute domain operation, save, pull events
2851
+ export interface ${contractName} {
2852
+ execute(
2853
+ command: ${commandTypeName},
2854
+ deps: ${depsTypeName},
2855
+ eventContext: EventContext,
2856
+ ): Promise<void>;
2857
+ }
2858
+ `;
2450
2859
  }
2860
+ function createUnsupportedCommandStub(spec, command) {
2861
+ const commandTypeName = `${pascalCase(command.name)}Command`;
2862
+ const depsTypeName = `${pascalCase(spec.context.name)}CommandHandlerDeps`;
2863
+ const contractName = `${pascalCase(command.name)}HandlerContract`;
2864
+ const handlerVarName = `${camelCase(command.name)}Handler`;
2865
+ return `import type { ${contractName} } from "../contracts/${kebabCase(command.name)}.handler.contract.ts";
2866
+ import type { ${commandTypeName} } from "../commands/${kebabCase(command.name)}.command.ts";
2867
+ import type { ${depsTypeName} } from "../handler-deps.ts";
2868
+ import type { EventContext } from "../../../../shared-kernel/events/event-context.ts";
2869
+
2870
+ export const ${handlerVarName}: ${contractName} = {
2871
+ async execute(command, deps, eventContext) {
2872
+ void command;
2873
+ void deps;
2874
+ void eventContext;
2875
+ throw new Error("TODO: implement ${pascalCase(command.name)} handler");
2876
+ },
2877
+ };
2451
2878
  `;
2452
2879
  }
2453
2880
  function createQueryStub(spec, query) {
@@ -2468,7 +2895,7 @@ function buildQueryHandlerBody(_spec, query) {
2468
2895
  const handlerName = `${pascalCase(query.name)}Handler`;
2469
2896
  const readModelVariable = queryReadModelVariableName$1(query);
2470
2897
  const readModelMethodName = queryReadModelMethodName$1(query);
2471
- const viewFileBase = queryViewFileBase$2(query);
2898
+ const viewFileBase = queryViewFileBase$1(query);
2472
2899
  return `import type { ${queryTypeName} } from "./${kebabCase(query.name)}.query.ts";
2473
2900
  import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";
2474
2901
  import type { ${readModelPortType} } from "../ports/${queryReadModelPortFileName(query)}";
@@ -2494,7 +2921,7 @@ function createQueryHandlerStub(_spec, query) {
2494
2921
  const handlerName = `${pascalCase(query.name)}Handler`;
2495
2922
  const readModelVariable = queryReadModelVariableName$1(query);
2496
2923
  const readModelMethodName = queryReadModelMethodName$1(query);
2497
- const viewFileBase = queryViewFileBase$2(query);
2924
+ const viewFileBase = queryViewFileBase$1(query);
2498
2925
  return `import type { ${queryTypeName} } from "./${kebabCase(query.name)}.query.ts";
2499
2926
  import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";
2500
2927
  import type { ${readModelPortType} } from "../ports/${queryReadModelPortFileName(query)}";
@@ -2557,46 +2984,80 @@ function groupQueriesByReadModel$1(queries) {
2557
2984
  }));
2558
2985
  }
2559
2986
  function createReadModelRepositoryPortStub(readModelName, queries) {
2560
- const portTypeName = `${readModelContractName$3(readModelName)}RepositoryPort`;
2987
+ const portTypeName = `${readModelContractName$2(readModelName)}RepositoryPort`;
2561
2988
  const importLines = /* @__PURE__ */ new Map();
2562
2989
  let needsPaginatedResult = false;
2563
- importLines.set("tx", `import type { Transaction } from "../../../../../lib/transaction.ts";`);
2990
+ importLines.set("tx", `import type { Transaction } from "../../../../../../lib/transaction.ts";`);
2564
2991
  const queryMethodLines = queries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((query) => {
2565
2992
  const queryTypeName = `${pascalCase(query.name)}Query`;
2566
2993
  const outputContractName = queryOutputContractName$1(query);
2567
- const viewFileBase = queryViewFileBase$2(query);
2994
+ const viewFileBase = queryViewFileBase$1(query);
2568
2995
  const returnType = query.queryKind === "list" ? `PaginatedResult<${outputContractName}>` : outputContractName;
2569
2996
  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";`);
2997
+ importLines.set(`query:${queryTypeName}`, `import type { ${queryTypeName} } from "../../queries/${kebabCase(query.name)}.query.ts";`);
2998
+ importLines.set(`view:${outputContractName}`, `import type { ${outputContractName} } from "../../contracts/${viewFileBase}.view.ts";`);
2572
2999
  return ` ${queryReadModelMethodName$1(query)}(query: ${queryTypeName}, tx: Transaction): Promise<${returnType}>;`;
2573
3000
  });
2574
- if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "../../../../../lib/pagination.ts";`);
3001
+ if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "../../../../../../lib/pagination.ts";`);
2575
3002
  return `${[...importLines.values()].join("\n")}
2576
3003
 
2577
3004
  // Read-model repository port. Query handlers share this contract by read-model, not by query.
2578
3005
  export interface ${portTypeName} {
2579
- // Query methods
2580
3006
  ${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
3007
  }
2588
3008
  `;
2589
3009
  }
2590
3010
  function createReadModelPortsIndexStub(queries) {
2591
3011
  return `${groupQueriesByReadModel$1(queries).map(({ readModelName }) => {
2592
- return `export type { ${`${readModelContractName$3(readModelName)}RepositoryPort`} } from "./${readModelPortBaseFileName(readModelName)}.repository.port.ts";`;
3012
+ return `export type { ${`${readModelContractName$2(readModelName)}RepositoryPort`} } from "./${readModelPortBaseFileName(readModelName)}.repository.port.ts";`;
2593
3013
  }).join("\n")}\n`;
2594
3014
  }
3015
+ function projectionWritePortName$2(projectionName) {
3016
+ return `${pascalCase(projectionName)}WritePort`;
3017
+ }
3018
+ function projectionWritePortFileName(projectionName) {
3019
+ return `${kebabCase(projectionName)}.projection-write.port.ts`;
3020
+ }
3021
+ function projectionSourcePayloadTypeName$1(source) {
3022
+ return `${pascalCase(trimProjectionSourceEventName$1(source.eventName))}Payload`;
3023
+ }
3024
+ function projectionSourcePayloadImportPath$1(currentModulePath, source) {
3025
+ const sourceModulePath = projectionSourceModulePath$2(source.contextName);
3026
+ const eventFile = `${kebabCase(trimProjectionSourceEventName$1(source.eventName))}.event.ts`;
3027
+ if (sourceModulePath === currentModulePath) return `../../../domain/events/${eventFile}`;
3028
+ return `../../../../${sourceModulePath}/domain/events/${eventFile}`;
3029
+ }
3030
+ function trimProjectionSourceEventName$1(eventName) {
3031
+ return eventName.endsWith("Event") ? eventName.slice(0, -5) : eventName;
3032
+ }
3033
+ function projectionSourceModulePath$2(contextName) {
3034
+ return normalizeModulePath(contextName.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2"));
3035
+ }
3036
+ function projectionSourceEventType$1(source) {
3037
+ const payloadType = projectionSourcePayloadTypeName$1(source);
3038
+ return `EventEnvelope<${payloadType}> | ${payloadType}`;
3039
+ }
3040
+ function createProjectionWritePortStub(currentModulePath, projection) {
3041
+ const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionSourcePayloadTypeName$1(source)} } from "${projectionSourcePayloadImportPath$1(currentModulePath, source)}";`);
3042
+ 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>;`);
3043
+ return `import type { EventEnvelope } from "../../../../../../lib/event-envelope.ts";
3044
+ import type { Transaction } from "../../../../../../lib/transaction.ts";
3045
+ ${payloadImports.join("\n")}
3046
+
3047
+ // Projector-facing write port for the "${projection.readModelName}" read model.
3048
+ export interface ${projectionWritePortName$2(projection.name)} {
3049
+ ${methodLines.join("\n")}
3050
+ }
3051
+ `;
3052
+ }
3053
+ function createProjectionWritePortsIndexStub(projections) {
3054
+ 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`;
3055
+ }
2595
3056
  function inputContractFileName(commandOrQueryName) {
2596
3057
  return `${kebabCase(commandOrQueryName)}.input`;
2597
3058
  }
2598
3059
  function viewContractFileName(query) {
2599
- return `${queryViewFileBase$2(query)}.view`;
3060
+ return `${queryViewFileBase$1(query)}.view`;
2600
3061
  }
2601
3062
  function createHandlerMapStub(spec) {
2602
3063
  const contextPascal = pascalCase(spec.context.name);
@@ -2619,7 +3080,11 @@ ${mapEntries.join("\n")}
2619
3080
  }
2620
3081
  function createContextHandlerDepsStub(contextSpec) {
2621
3082
  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";`];
3083
+ const importLines = [
3084
+ `import type { Transaction } from "../../../../lib/transaction.ts";`,
3085
+ `import type { AggregateEventTracker } from "../../../shared-kernel/events/aggregate-event-tracker.ts";`,
3086
+ `import type { EventCollector } from "../../../shared-kernel/events/event-collector.ts";`
3087
+ ];
2623
3088
  const repoEntries = [];
2624
3089
  const aclEntries = [];
2625
3090
  for (const agg of contextSpec.aggregates) {
@@ -2645,6 +3110,7 @@ ${aclEntries.join("\n")}
2645
3110
  export type ${contextPascal}CommandHandlerDeps = {
2646
3111
  tx: Transaction;
2647
3112
  eventCollector: EventCollector;
3113
+ aggregateEventTracker: AggregateEventTracker;
2648
3114
  repos: {
2649
3115
  ${repoEntries.join("\n")}
2650
3116
  };
@@ -2690,7 +3156,7 @@ function createPublishedReadBoundaryStub(contextSpec) {
2690
3156
  const queryTypeName = `${pascalCase(query.name)}Query`;
2691
3157
  const outputContractName = queryOutputContractName$1(query);
2692
3158
  const queryFileName = kebabCase(query.name);
2693
- const outputFileName = queryViewFileBase$2(query);
3159
+ const outputFileName = queryViewFileBase$1(query);
2694
3160
  const readModelDepName = queryReadModelVariableName$1(query);
2695
3161
  const readModelTypeName = queryReadModelPortTypeName$1(query);
2696
3162
  imports.set(`handler:${queryHandlerName}`, `import { ${queryHandlerName} } from "./queries/${queryFileName}.handler.ts";`);
@@ -2756,7 +3222,14 @@ function buildV5ApplicationArtifacts(spec, options) {
2756
3222
  for (const { readModelName, queries } of groupQueriesByReadModel$1(spec.queries)) artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, `application/ports/${`read-models/${readModelPortBaseFileName(readModelName)}.repository.port.ts`}`), createReadModelRepositoryPortStub(readModelName, queries)));
2757
3223
  for (const command of spec.commands) {
2758
3224
  artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, `application/commands/${kebabCase(command.name)}.command.ts`), createCommandStub(spec, command)));
2759
- if (findRepositoryPort(spec, command.targetAggregate)) artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, `application/handlers/${kebabCase(command.name)}.handler.ts`), createCommandHandlerStub(spec, command)));
3225
+ if (findRepositoryPort(spec, command.targetAggregate)) {
3226
+ const handlerBody = createCommandHandlerStub(spec, command);
3227
+ if (handlerBody !== null) artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, `application/handlers/${kebabCase(command.name)}.handler.ts`), handlerBody));
3228
+ else {
3229
+ artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, `application/contracts/${kebabCase(command.name)}.handler.contract.ts`), createUnsupportedCommandContract(spec, command)));
3230
+ artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, `application/handlers/${kebabCase(command.name)}.handler.ts`), createUnsupportedCommandStub(spec, command), userArtifactOwnership(`${spec.context.name}:${command.name}`)));
3231
+ }
3232
+ }
2760
3233
  }
2761
3234
  for (const query of spec.queries) if (query.queryKind === "list") {
2762
3235
  const listQuery = query;
@@ -2772,16 +3245,29 @@ function buildV5ApplicationArtifacts(spec, options) {
2772
3245
  }
2773
3246
  function buildV5ApplicationContextArtifacts(contextSpec) {
2774
3247
  const modulePath = normalizeModulePath(contextSpec.context.modulePath);
2775
- return [
3248
+ const artifacts = [
2776
3249
  createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/ports/read-models/index.ts"), createReadModelPortsIndexStub(contextSpec.queries)),
2777
3250
  createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/handler-deps.ts"), createContextHandlerDepsStub(contextSpec)),
2778
3251
  createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/read-boundary.ts"), createPublishedReadBoundaryStub(contextSpec)),
2779
3252
  createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/handler-map.ts"), createContextHandlerMapStub(contextSpec))
2780
3253
  ];
3254
+ const projections = contextSpec.projections ?? [];
3255
+ if (projections.length > 0) {
3256
+ artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/ports/projections/index.ts"), createProjectionWritePortsIndexStub(projections)));
3257
+ for (const projection of projections) artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, `application/ports/projections/${projectionWritePortFileName(projection.name)}`), createProjectionWritePortStub(modulePath, projection)));
3258
+ }
3259
+ return artifacts;
2781
3260
  }
2782
3261
 
2783
3262
  //#endregion
2784
3263
  //#region packages/core/generators/infrastructure.ts
3264
+ function defaultIdBrandName(name) {
3265
+ const base = pascalCase(name);
3266
+ return base.endsWith("Id") ? base : `${base}Id`;
3267
+ }
3268
+ function resolveDrizzlePersistence(infrastructureStrategy) {
3269
+ return infrastructureStrategy?.persistence === "mysql" ? "mysql" : "postgres";
3270
+ }
2785
3271
  function isDecimalNumberField(field) {
2786
3272
  if (field.type !== "number") return false;
2787
3273
  const explicitColumnType = field.columnType?.toLowerCase();
@@ -2796,10 +3282,12 @@ function isDecimalNumberField(field) {
2796
3282
  const fieldName = snakeCase(field.name);
2797
3283
  return /(^|_)(cpk|ppk|ucl|lcl)(_|$)/.test(fieldName) || /(percent|percentage|ratio|rate|average|avg|variance|yield|scrap|drift)/.test(fieldName);
2798
3284
  }
2799
- function drizzleColumnBuilder(field, columnName) {
3285
+ function drizzleColumnBuilder(field, columnName, persistence = "postgres") {
2800
3286
  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) {
3287
+ if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0) {
3288
+ const jsonType = formatTsFieldType(field);
3289
+ builder = persistence === "mysql" ? `json("${columnName}").$type<${jsonType}>()` : `jsonb("${columnName}").$type<${jsonType}>()`;
3290
+ } else switch (field.type) {
2803
3291
  case "number":
2804
3292
  builder = isDecimalNumberField(field) ? `real("${columnName}")` : `integer("${columnName}")`;
2805
3293
  break;
@@ -2906,12 +3394,12 @@ function relativePrefixFromRepositoryDirectory(modulePath) {
2906
3394
  ...modulePath.split("/").filter(Boolean)
2907
3395
  ].map(() => "..").join("/");
2908
3396
  }
2909
- function readModelContractName$2(readModelName) {
3397
+ function readModelContractName$1(readModelName) {
2910
3398
  const baseName = pascalCase(readModelName);
2911
3399
  return baseName.endsWith("View") ? baseName : `${baseName}View`;
2912
3400
  }
2913
3401
  function readModelRepositoryClassName(readModelName) {
2914
- return `Drizzle${readModelContractName$2(readModelName)}Repository`;
3402
+ return `Drizzle${readModelContractName$1(readModelName)}Repository`;
2915
3403
  }
2916
3404
  function readModelRepositoryFileBase$1(readModelName) {
2917
3405
  return kebabCase(readModelName).replace(/-view$/, "");
@@ -2919,6 +3407,15 @@ function readModelRepositoryFileBase$1(readModelName) {
2919
3407
  function readModelTableConstName(readModelName) {
2920
3408
  return `${camelCase(readModelName)}Table`;
2921
3409
  }
3410
+ function mikroOrmRepositoryClassName(aggregateName) {
3411
+ return `MikroOrm${pascalCase(aggregateName)}Repository`;
3412
+ }
3413
+ function mikroOrmEntityClassName(name) {
3414
+ return `${pascalCase(name)}Record`;
3415
+ }
3416
+ function mikroOrmEntitySchemaConstName(name) {
3417
+ return `${pascalCase(name)}Schema`;
3418
+ }
2922
3419
  function resolvedReadModelName$1(query) {
2923
3420
  return query.readSide?.readModelName ?? query.readModelName ?? query.name;
2924
3421
  }
@@ -2928,9 +3425,9 @@ function queryOutputExportName(query) {
2928
3425
  function queryOutputContractName(query) {
2929
3426
  const outputExportName = queryOutputExportName(query);
2930
3427
  if (outputExportName) return pascalCase(outputExportName);
2931
- return readModelContractName$2(resolvedReadModelName$1(query));
3428
+ return readModelContractName$1(resolvedReadModelName$1(query));
2932
3429
  }
2933
- function queryViewFileBase$1(query) {
3430
+ function queryViewFileBase(query) {
2934
3431
  const outputExportName = queryOutputExportName(query);
2935
3432
  if (outputExportName) return kebabCase(outputExportName).replace(/-view$/, "");
2936
3433
  return readModelRepositoryFileBase$1(resolvedReadModelName$1(query));
@@ -2960,13 +3457,13 @@ const RESERVED_AGGREGATE_FIELD_NAMES = new Set(["version"]);
2960
3457
  function filterManagedAggregateFields(fields) {
2961
3458
  return fields.filter((field) => !RESERVED_AGGREGATE_FIELD_NAMES.has(camelCase(field.name)));
2962
3459
  }
2963
- function buildTableBlock(tableName, tableConstName, fields, idField, fkColumn, isAggregateRoot = false) {
3460
+ function buildTableBlock(tableName, tableConstName, fields, idField, fkColumn, isAggregateRoot = false, persistence = "postgres") {
2964
3461
  const columnDefs = [];
2965
3462
  const emittedColumns = /* @__PURE__ */ new Set();
2966
3463
  for (const field of fields) {
2967
3464
  const snakeName = snakeCase(field.name);
2968
3465
  if (emittedColumns.has(snakeName)) continue;
2969
- const columnDef = field.name === idField ? `text("${snakeName}").primaryKey()` : drizzleColumnBuilder(field, snakeName);
3466
+ const columnDef = field.name === idField ? `text("${snakeName}").primaryKey()` : drizzleColumnBuilder(field, snakeName, persistence);
2970
3467
  columnDefs.push(` ${snakeName}: ${columnDef},`);
2971
3468
  emittedColumns.add(snakeName);
2972
3469
  }
@@ -2975,16 +3472,16 @@ function buildTableBlock(tableName, tableConstName, fields, idField, fkColumn, i
2975
3472
  emittedColumns.add(fkColumn.name);
2976
3473
  }
2977
3474
  if (isAggregateRoot) columnDefs.push(` version: integer("version").default(0).notNull(),`);
2978
- return `export const ${tableConstName} = pgTable("${tableName}", {\n${columnDefs.join("\n")}\n});`;
3475
+ return `export const ${tableConstName} = ${persistence === "mysql" ? "mysqlTable" : "pgTable"}("${tableName}", {\n${columnDefs.join("\n")}\n});`;
2979
3476
  }
2980
- function createDrizzleTableDefinition(spec) {
3477
+ function createDrizzleTableDefinition(spec, persistence = "postgres") {
2981
3478
  const fields = filterManagedAggregateFields(spec.aggregate.fields);
2982
3479
  if (!fields || fields.length === 0) return null;
2983
3480
  const children = spec.aggregate.children ?? [];
2984
3481
  const aggScalarFields = scalarFields(fields, children);
2985
3482
  const emittedTableNames = /* @__PURE__ */ new Set();
2986
3483
  const blocks = [];
2987
- blocks.push(buildTableBlock(spec.aggregate.tableName, `${camelCase(spec.aggregate.name)}Table`, aggScalarFields, spec.aggregate.idField, void 0, true));
3484
+ blocks.push(buildTableBlock(spec.aggregate.tableName, `${camelCase(spec.aggregate.name)}Table`, aggScalarFields, spec.aggregate.idField, void 0, true, persistence));
2988
3485
  emittedTableNames.add(spec.aggregate.tableName);
2989
3486
  const aggregate = {
2990
3487
  name: spec.aggregate.name,
@@ -3001,13 +3498,13 @@ function createDrizzleTableDefinition(spec) {
3001
3498
  blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, {
3002
3499
  name: fkColName,
3003
3500
  type: "text"
3004
- }));
3501
+ }, false, persistence));
3005
3502
  emittedTableNames.add(entity.tableName);
3006
3503
  });
3007
3504
  for (const entity of spec.entities ?? []) {
3008
3505
  if (!entity.tableName || emittedTableNames.has(entity.tableName)) continue;
3009
3506
  const entityScalarFields = scalarFields(entity.fields, entity.children ?? []);
3010
- blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField));
3507
+ blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, void 0, false, persistence));
3011
3508
  emittedTableNames.add(entity.tableName);
3012
3509
  }
3013
3510
  const flatScalarFields = [
@@ -3015,12 +3512,12 @@ function createDrizzleTableDefinition(spec) {
3015
3512
  ...flattenEntities(aggregate).map((entity) => scalarFields(entity.fields, entity.children)),
3016
3513
  ...(spec.entities ?? []).map((entity) => scalarFields(entity.fields, entity.children ?? []))
3017
3514
  ].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`;
3515
+ const coreImportTokens = [persistence === "mysql" ? "mysqlTable" : "pgTable", "text"];
3516
+ if (spec.aggregate !== void 0 || flatScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) coreImportTokens.push("integer");
3517
+ if (flatScalarFields.some((field) => isDecimalNumberField(field))) coreImportTokens.push("real");
3518
+ if (flatScalarFields.some((field) => field.type === "boolean")) coreImportTokens.push("boolean");
3519
+ if (flatScalarFields.some((field) => field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0)) coreImportTokens.push(persistence === "mysql" ? "json" : "jsonb");
3520
+ return `import { ${coreImportTokens.join(", ")} } from "${persistence === "mysql" ? "drizzle-orm/mysql-core" : "drizzle-orm/pg-core"}";\n\n${blocks.join("\n\n")}\n`;
3024
3521
  }
3025
3522
  function buildCodecBlock(_spec, entityName, fields, options) {
3026
3523
  const varName = camelCase(entityName);
@@ -3091,48 +3588,343 @@ function createPersistenceEqualityBlock(name, fields, options) {
3091
3588
  return `// Compiled from zx.deepEqual.writeable at generator time using the canonical ${pascalCase(name)} persistence row shape.
3092
3589
  export const ${equalityName} = ${compiledEquality};`;
3093
3590
  }
3094
- function childTableVar(spec, tableName) {
3095
- const child = (spec.aggregate.children ?? []).find((c) => c.tableName === tableName);
3096
- return child ? `${camelCase(child.name)}Table` : `${camelCase(tableName)}Table`;
3591
+ function buildMikroOrmRootNode(spec) {
3592
+ const buildChild = (entity, parent) => {
3593
+ const node = {
3594
+ name: entity.name,
3595
+ className: mikroOrmEntityClassName(entity.name),
3596
+ schemaConstName: mikroOrmEntitySchemaConstName(entity.name),
3597
+ fields: entity.fields,
3598
+ scalarFields: scalarFields(entity.fields, entity.children),
3599
+ idField: entity.idField,
3600
+ idType: entity.idType,
3601
+ tableName: entity.tableName,
3602
+ collectionFieldName: entity.collectionFieldName,
3603
+ parent: {
3604
+ className: parent.className,
3605
+ propertyName: camelCase(parent.name),
3606
+ fkFieldName: snakeCase(parent.idField)
3607
+ },
3608
+ children: [],
3609
+ isAggregateRoot: false
3610
+ };
3611
+ node.children = (entity.children ?? []).map((child) => buildChild(child, node));
3612
+ return node;
3613
+ };
3614
+ const rootNode = {
3615
+ name: spec.aggregate.name,
3616
+ className: mikroOrmEntityClassName(spec.aggregate.name),
3617
+ schemaConstName: mikroOrmEntitySchemaConstName(spec.aggregate.name),
3618
+ fields: filterManagedAggregateFields(spec.aggregate.fields),
3619
+ scalarFields: scalarFields(filterManagedAggregateFields(spec.aggregate.fields), spec.aggregate.children ?? []),
3620
+ idField: spec.aggregate.idField,
3621
+ idType: spec.aggregate.idType,
3622
+ tableName: spec.aggregate.tableName,
3623
+ children: [],
3624
+ isAggregateRoot: true
3625
+ };
3626
+ rootNode.children = (spec.aggregate.children ?? []).map((child) => buildChild(child, rootNode));
3627
+ return rootNode;
3097
3628
  }
3098
- function listQueryTypeBaseName(listQuery) {
3099
- return pascalCase(listQuery.name);
3629
+ function flattenMikroOrmNodes(rootNode) {
3630
+ const nodes = [rootNode];
3631
+ const queue = [...rootNode.children];
3632
+ while (queue.length > 0) {
3633
+ const node = queue.shift();
3634
+ nodes.push(node);
3635
+ queue.push(...node.children);
3636
+ }
3637
+ return nodes;
3100
3638
  }
3101
- function listQueryFiltersTypeName(listQuery) {
3102
- return `${listQueryTypeBaseName(listQuery)}Filters`;
3639
+ function mikroOrmClassFieldType(field) {
3640
+ if (field.type === "object" && field.nestedFields && field.nestedFields.length > 0) return formatTsFieldType(field);
3641
+ switch (field.type) {
3642
+ case "number": return field.array ? "number[]" : "number";
3643
+ case "boolean": return field.array ? "boolean[]" : "boolean";
3644
+ default: return field.array ? "string[]" : "string";
3645
+ }
3103
3646
  }
3104
- function listQuerySortFieldTypeName(listQuery) {
3105
- return `${listQueryTypeBaseName(listQuery)}SortField`;
3647
+ function mikroOrmPropertyType(field) {
3648
+ if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0) return "json";
3649
+ switch (field.type) {
3650
+ case "number": return "number";
3651
+ case "boolean": return "boolean";
3652
+ default: return "string";
3653
+ }
3106
3654
  }
3107
- function listQueryRowTypeName(listQuery) {
3108
- return `${listQueryTypeBaseName(listQuery)}Row`;
3655
+ function createMikroOrmEntitySchemaArtifactContent(spec) {
3656
+ const nodes = flattenMikroOrmNodes(buildMikroOrmRootNode(spec));
3657
+ const contextPrefix = pascalCase(spec.context.name);
3658
+ const needsCascade = nodes.some((node) => node.children.length > 0);
3659
+ const needsCollection = nodes.some((node) => node.children.length > 0);
3660
+ const mikroOrmImports = ["EntitySchema"];
3661
+ if (needsCascade) mikroOrmImports.unshift("Cascade");
3662
+ if (needsCollection) mikroOrmImports.push("Collection");
3663
+ const imports = [`import { ${mikroOrmImports.join(", ")} } from "@mikro-orm/core";`];
3664
+ const classBlocks = nodes.map((node) => {
3665
+ const lines = [`export class ${node.className} {`];
3666
+ for (const field of node.scalarFields) lines.push(` ${camelCase(field.name)}${field.optional ? "?" : "!"}: ${mikroOrmClassFieldType(field)};`);
3667
+ if (node.isAggregateRoot) lines.push(` version!: number;`);
3668
+ if (node.parent) lines.push(` ${node.parent.propertyName}!: ${node.parent.className};`);
3669
+ for (const child of node.children) {
3670
+ const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
3671
+ lines.push(` ${collectionName} = new Collection<${child.className}>(this);`);
3672
+ }
3673
+ lines.push(`}`);
3674
+ return lines.join("\n");
3675
+ });
3676
+ const schemaBlocks = nodes.map((node) => {
3677
+ const propertyLines = [];
3678
+ for (const field of node.scalarFields) {
3679
+ const options = [`type: "${mikroOrmPropertyType(field)}"`, `fieldName: "${snakeCase(field.name)}"`];
3680
+ if (camelCase(field.name) === camelCase(node.idField)) options.push(`primary: true`);
3681
+ if (field.optional) options.push(`nullable: true`);
3682
+ propertyLines.push(` ${camelCase(field.name)}: { ${options.join(", ")} },`);
3683
+ }
3684
+ if (node.isAggregateRoot) propertyLines.push(` version: { type: "number", fieldName: "version" },`);
3685
+ if (node.parent) propertyLines.push(` ${node.parent.propertyName}: { kind: "m:1", entity: () => ${node.parent.className}, fieldName: "${node.parent.fkFieldName}" },`);
3686
+ for (const child of node.children) {
3687
+ const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
3688
+ propertyLines.push(` ${collectionName}: { kind: "1:m", entity: () => ${child.className}, mappedBy: "${child.parent?.propertyName}", cascade: [Cascade.PERSIST, Cascade.REMOVE], orphanRemoval: true },`);
3689
+ }
3690
+ return `export const ${node.schemaConstName} = new EntitySchema<${node.className}>({
3691
+ name: "${contextPrefix}${node.className}",
3692
+ class: ${node.className},
3693
+ tableName: "${node.tableName}",
3694
+ properties: {
3695
+ ${propertyLines.join("\n")}
3696
+ },
3697
+ });`;
3698
+ });
3699
+ return `${imports.join("\n")}
3700
+
3701
+ ${classBlocks.join("\n\n")}
3702
+
3703
+ ${schemaBlocks.join("\n\n")}
3704
+
3705
+ export const ${camelCase(spec.aggregate.name)}MikroOrmEntities = [${nodes.map((node) => node.schemaConstName).join(", ")}] as const;
3706
+ `;
3109
3707
  }
3110
- function listQueryRepositoryMethodName(listQuery) {
3111
- return camelCase(listQuery.name);
3708
+ function serializeDomainFieldValue(field, sourceExpr) {
3709
+ if (field.type === "date" || field.type === "Date") return `${sourceExpr} instanceof Date ? ${sourceExpr}.toISOString() : String(${sourceExpr})`;
3710
+ return sourceExpr;
3112
3711
  }
3113
- function buildListMethod(spec, listQuery) {
3114
- const filtersTypeName = listQueryFiltersTypeName(listQuery);
3115
- const sortFieldTypeName = listQuerySortFieldTypeName(listQuery);
3116
- const rowTypeName = listQueryRowTypeName(listQuery);
3117
- const methodName = listQueryRepositoryMethodName(listQuery);
3118
- const tableVar = `${camelCase(spec.aggregate.name)}Table`;
3119
- const idField = spec.aggregate.idField;
3120
- const idColRef = `${tableVar}.${snakeCase(idField)}`;
3121
- const aggregateFieldNames = new Set([camelCase(idField), ...filterManagedAggregateFields(spec.aggregate.fields).map((f) => camelCase(f.name))]);
3122
- const aggregateFieldsByName = new Map(filterManagedAggregateFields(spec.aggregate.fields).map((field) => [camelCase(field.name), field]));
3123
- const filterLines = [];
3124
- for (const [field, operators] of Object.entries(listQuery.filters)) {
3125
- if (!aggregateFieldNames.has(camelCase(field))) continue;
3126
- const columnRef = `${tableVar}.${snakeCase(field)}`;
3127
- for (const op of operators) switch (op) {
3128
- case "eq":
3129
- filterLines.push(` if (filters?.${camelCase(field)}?.eq != null) conditions.push(eq(${columnRef}, filters.${camelCase(field)}.eq));`);
3130
- break;
3131
- case "in":
3132
- filterLines.push(` if (filters?.${camelCase(field)}?.in) conditions.push(inArray(${columnRef}, filters.${camelCase(field)}.in));`);
3133
- break;
3134
- case "gt":
3135
- filterLines.push(` if (filters?.${camelCase(field)}?.gt != null) conditions.push(gt(${columnRef}, filters.${camelCase(field)}.gt));`);
3712
+ function deserializePersistenceFieldValue(field, sourceExpr) {
3713
+ if (field.type === "date" || field.type === "Date") return `${sourceExpr} ? new Date(${sourceExpr}) : ${sourceExpr}`;
3714
+ return sourceExpr;
3715
+ }
3716
+ function rehydrateIdExpression(node, sourceExpr) {
3717
+ return `create${pascalCase(node.idType && node.idType !== "string" ? node.idType : defaultIdBrandName(node.name))}(${sourceExpr})`;
3718
+ }
3719
+ function buildMikroOrmNodeHelperBlock(spec, node, domainTypeNameOverride) {
3720
+ const domainTypeName = domainTypeNameOverride ?? (node.isAggregateRoot ? `${pascalCase(spec.aggregate.name)}Aggregate` : pascalCase(node.name));
3721
+ const domainVar = "source";
3722
+ const recordVar = "target";
3723
+ const scalarAssignmentLines = node.scalarFields.map((field) => {
3724
+ const propertyName = camelCase(field.name);
3725
+ return ` ${recordVar}.${propertyName} = ${camelCase(field.name) === camelCase(node.idField) ? `String(${domainVar}.id.value)` : serializeDomainFieldValue(field, `${domainVar}.${propertyName}`)};`;
3726
+ });
3727
+ if (node.isAggregateRoot) scalarAssignmentLines.push(` ${recordVar}.version = ${domainVar}.version;`);
3728
+ const createChildLines = node.children.map((child) => {
3729
+ const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
3730
+ return ` ${recordVar}.${collectionName}.set(${domainVar}.${collectionName}.map((item) => create${child.className}FromDomain(item, ${recordVar})));`;
3731
+ });
3732
+ const syncChildLines = node.children.map((child) => {
3733
+ const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
3734
+ const childIdField = camelCase(child.idField);
3735
+ return ` {
3736
+ const currentById = new Map<string, ${child.className}>(${recordVar}.${collectionName}.getItems().map((item: ${child.className}) => [String(item.${childIdField}), item] as const));
3737
+ const nextItems = ${domainVar}.${collectionName}.map((item) => {
3738
+ const existing = currentById.get(String(item.id.value));
3739
+ if (existing) {
3740
+ sync${child.className}FromDomain(existing, item, ${recordVar});
3741
+ return existing;
3742
+ }
3743
+ return create${child.className}FromDomain(item, ${recordVar});
3744
+ });
3745
+ ${recordVar}.${collectionName}.set(nextItems);
3746
+ }`;
3747
+ });
3748
+ const parentParam = node.parent ? `, parent: ${node.parent.className}` : "";
3749
+ const parentAssignment = node.parent ? ` ${recordVar}.${node.parent.propertyName} = parent;\n` : "";
3750
+ const parentSyncParam = node.parent ? `, parent?: ${node.parent.className}` : "";
3751
+ const parentSyncAssignment = node.parent ? ` if (parent) {\n ${recordVar}.${node.parent.propertyName} = parent;\n }\n` : "";
3752
+ const domainScalarLines = node.scalarFields.map((field) => {
3753
+ const propertyName = camelCase(field.name);
3754
+ return ` ${propertyName}: ${camelCase(field.name) === camelCase(node.idField) ? rehydrateIdExpression(node, `${recordVar}.${propertyName}`) : deserializePersistenceFieldValue(field, `${recordVar}.${propertyName}`)},`;
3755
+ });
3756
+ if (node.isAggregateRoot) domainScalarLines.push(` version: ${recordVar}.version,`);
3757
+ for (const child of node.children) {
3758
+ const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
3759
+ domainScalarLines.push(` ${collectionName}: ${recordVar}.${collectionName}.getItems().map((item: ${child.className}) => map${child.className}ToDomain(item)),`);
3760
+ }
3761
+ return `function create${node.className}FromDomain(source: ${domainTypeName}${parentParam}): ${node.className} {
3762
+ const ${recordVar} = new ${node.className}();
3763
+ ${parentAssignment}${scalarAssignmentLines.join("\n")}
3764
+ ${createChildLines.join("\n")}
3765
+ return ${recordVar};
3766
+ }
3767
+
3768
+ function sync${node.className}FromDomain(target: ${node.className}, source: ${domainTypeName}${parentSyncParam}): void {
3769
+ ${parentSyncAssignment}${scalarAssignmentLines.join("\n")}
3770
+ ${syncChildLines.join("\n")}
3771
+ }
3772
+
3773
+ function map${node.className}ToDomain(target: ${node.className}): ${domainTypeName} {
3774
+ return ${domainTypeName}.fromPersistence({
3775
+ ${domainScalarLines.join("\n")}
3776
+ });
3777
+ }`;
3778
+ }
3779
+ function createMikroOrmRepository(spec, portName) {
3780
+ const modulePath = normalizeModulePath(spec.context.modulePath);
3781
+ const relativePrefix = relativePrefixFromRepositoryDirectory(modulePath);
3782
+ const aggregateName = pascalCase(spec.aggregate.name);
3783
+ const aggregateDir = kebabCase(spec.aggregate.name);
3784
+ const repositoryPortType = repositoryPortTypeName(portName);
3785
+ const repositoryClassName = mikroOrmRepositoryClassName(spec.aggregate.name);
3786
+ const rootNode = buildMikroOrmRootNode(spec);
3787
+ const nodes = flattenMikroOrmNodes(rootNode);
3788
+ const customIdImports = /* @__PURE__ */ new Map();
3789
+ for (const node of nodes) {
3790
+ const idFactoryBase = node.idType && node.idType !== "string" ? node.idType : defaultIdBrandName(node.name);
3791
+ customIdImports.set(idFactoryBase, `import { create${pascalCase(idFactoryBase)} } from "${relativePrefix}/core/shared-kernel/entity-ids/${kebabCase(idFactoryBase)}";`);
3792
+ }
3793
+ const notFoundError = (spec.aggregate.applicationErrors ?? [])[0];
3794
+ const notFoundImports = notFoundError ? (() => {
3795
+ const typeName = `${pascalCase(notFoundError.aggregateName)}NotFoundError`;
3796
+ return `import { ${notFoundError.factoryName}, type ${typeName} } from "${relativePrefix}/core/contexts/${modulePath}/domain/errors/${aggregateDir}-application-errors";\n`;
3797
+ })() : "";
3798
+ const notFoundTypeName = notFoundError ? `${pascalCase(notFoundError.aggregateName)}NotFoundError` : "Error";
3799
+ const notFoundFactory = notFoundError?.factoryName ?? "createNotFoundError";
3800
+ const rootIdParamName = `${camelCase(spec.aggregate.name)}Id`;
3801
+ const rootIdTypeName = rootNode.idType && rootNode.idType !== "string" ? pascalCase(rootNode.idType) : "string";
3802
+ const populatePaths = nodes.filter((node) => !node.isAggregateRoot).map((node) => {
3803
+ const segments = [];
3804
+ let current = node;
3805
+ while (current && !current.isAggregateRoot) {
3806
+ segments.unshift(camelCase(current.collectionFieldName ?? current.name));
3807
+ const parentClassName = current.parent?.className;
3808
+ current = parentClassName ? nodes.find((candidate) => candidate.className === parentClassName) : void 0;
3809
+ }
3810
+ return segments.join(".");
3811
+ }).filter((value, index, array) => value.length > 0 && array.indexOf(value) === index).sort();
3812
+ const domainEntityImports = nodes.filter((node) => !node.isAggregateRoot).map((node) => {
3813
+ const domainAlias = `${pascalCase(node.name)}DomainEntity`;
3814
+ return `import { ${pascalCase(node.name)}Entity as ${domainAlias} } from "${relativePrefix}/core/contexts/${modulePath}/domain/entities/${kebabCase(node.name)}.entity";`;
3815
+ }).join("\n");
3816
+ const helperBlocks = nodes.slice().reverse().map((node) => {
3817
+ return buildMikroOrmNodeHelperBlock(spec, node, node.isAggregateRoot ? `${pascalCase(spec.aggregate.name)}Aggregate` : `${pascalCase(node.name)}DomainEntity`);
3818
+ }).join("\n\n");
3819
+ return `import type { Result } from "${relativePrefix}/lib/result";
3820
+ import { ok, err } from "${relativePrefix}/lib/result";
3821
+ import type { Transaction } from "${relativePrefix}/lib/transaction";
3822
+ import { ConcurrencyConflictError } from "${relativePrefix}/lib/concurrency-conflict-error";
3823
+ import type { ${repositoryPortType} } from "${relativePrefix}/core/contexts/${modulePath}/application/ports/${kebabCase(portName)}.port";
3824
+ ${[...customIdImports.values()].join("\n")}${customIdImports.size > 0 ? "\n" : ""}${notFoundImports}import { ${aggregateName}Aggregate } from "${relativePrefix}/core/contexts/${modulePath}/domain/aggregates/${aggregateDir}/${aggregateDir}.aggregate";
3825
+ ${domainEntityImports}${nodes.some((node) => !node.isAggregateRoot) ? "\n" : ""}import { ${nodes.map((node) => node.className).join(", ")} } from "../../entities/${modulePath}/${kebabCase(spec.aggregate.name)}.entity-schema.ts";
3826
+
3827
+ const TOUCHED_AGGREGATES_KEY = "__zodmireTouchedAggregates";
3828
+
3829
+ type MikroOrmEntityManager = {
3830
+ findOne<TRecord>(
3831
+ entity: new () => TRecord,
3832
+ where: Record<string, unknown>,
3833
+ options: { populate: string[] },
3834
+ ): Promise<TRecord | null>;
3835
+ persist(record: unknown): void | Promise<void>;
3836
+ };
3837
+
3838
+ function markTouchedAggregate(tx: unknown, aggregateType: string, aggregateId: string): void {
3839
+ const runtimeTx = tx as Record<string, unknown>;
3840
+ const touched = (runtimeTx[TOUCHED_AGGREGATES_KEY] ??= new Map<string, Set<string>>()) as Map<string, Set<string>>;
3841
+ const ids = touched.get(aggregateType) ?? new Set<string>();
3842
+ ids.add(aggregateId);
3843
+ touched.set(aggregateType, ids);
3844
+ }
3845
+
3846
+ ${helperBlocks}
3847
+
3848
+ export class ${repositoryClassName} implements ${repositoryPortType} {
3849
+ async findById(${rootIdParamName}: ${rootIdTypeName}, tx: Transaction): Promise<Result<${aggregateName}Aggregate, ${notFoundTypeName}>> {
3850
+ const em = tx as unknown as MikroOrmEntityManager;
3851
+ 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(", ")}]` : "[]"} });
3852
+ if (!record) {
3853
+ return err(${notFoundFactory}(String(${rootNode.idType && rootNode.idType !== "string" ? `${rootIdParamName}.value` : rootIdParamName})));
3854
+ }
3855
+
3856
+ return ok(map${rootNode.className}ToDomain(record));
3857
+ }
3858
+
3859
+ async create(${camelCase(spec.aggregate.name)}: ${aggregateName}Aggregate, tx: Transaction): Promise<void> {
3860
+ const em = tx as unknown as MikroOrmEntityManager;
3861
+ const rootRecord = create${rootNode.className}FromDomain(${camelCase(spec.aggregate.name)});
3862
+ markTouchedAggregate(tx, "${aggregateName}", String(${camelCase(spec.aggregate.name)}.id.value));
3863
+ await em.persist(rootRecord);
3864
+ }
3865
+
3866
+ async save(${camelCase(spec.aggregate.name)}: ${aggregateName}Aggregate, expectedVersion: number, tx: Transaction): Promise<void> {
3867
+ const em = tx as unknown as MikroOrmEntityManager;
3868
+ 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(", ")}]` : "[]"} });
3869
+
3870
+ if (!current || current.version !== expectedVersion) {
3871
+ throw new ConcurrencyConflictError({
3872
+ aggregateType: "${aggregateName}",
3873
+ aggregateId: String(${camelCase(spec.aggregate.name)}.id.value),
3874
+ expectedVersion,
3875
+ actualVersion: current?.version,
3876
+ });
3877
+ }
3878
+
3879
+ sync${rootNode.className}FromDomain(current, ${camelCase(spec.aggregate.name)});
3880
+ markTouchedAggregate(tx, "${aggregateName}", String(${camelCase(spec.aggregate.name)}.id.value));
3881
+ await em.persist(current);
3882
+ }
3883
+ }
3884
+ `;
3885
+ }
3886
+ function childTableVar(spec, tableName) {
3887
+ const child = (spec.aggregate.children ?? []).find((c) => c.tableName === tableName);
3888
+ return child ? `${camelCase(child.name)}Table` : `${camelCase(tableName)}Table`;
3889
+ }
3890
+ function listQueryTypeBaseName(listQuery) {
3891
+ return pascalCase(listQuery.name);
3892
+ }
3893
+ function listQueryFiltersTypeName(listQuery) {
3894
+ return `${listQueryTypeBaseName(listQuery)}Filters`;
3895
+ }
3896
+ function listQuerySortFieldTypeName(listQuery) {
3897
+ return `${listQueryTypeBaseName(listQuery)}SortField`;
3898
+ }
3899
+ function listQueryRowTypeName(listQuery) {
3900
+ return `${listQueryTypeBaseName(listQuery)}Row`;
3901
+ }
3902
+ function listQueryRepositoryMethodName(listQuery) {
3903
+ return camelCase(listQuery.name);
3904
+ }
3905
+ function buildListMethod(spec, listQuery) {
3906
+ const filtersTypeName = listQueryFiltersTypeName(listQuery);
3907
+ const sortFieldTypeName = listQuerySortFieldTypeName(listQuery);
3908
+ const rowTypeName = listQueryRowTypeName(listQuery);
3909
+ const methodName = listQueryRepositoryMethodName(listQuery);
3910
+ const tableVar = `${camelCase(spec.aggregate.name)}Table`;
3911
+ const idField = spec.aggregate.idField;
3912
+ const idColRef = `${tableVar}.${snakeCase(idField)}`;
3913
+ const aggregateFieldNames = new Set([camelCase(idField), ...filterManagedAggregateFields(spec.aggregate.fields).map((f) => camelCase(f.name))]);
3914
+ const aggregateFieldsByName = new Map(filterManagedAggregateFields(spec.aggregate.fields).map((field) => [camelCase(field.name), field]));
3915
+ const filterLines = [];
3916
+ for (const [field, operators] of Object.entries(listQuery.filters)) {
3917
+ if (!aggregateFieldNames.has(camelCase(field))) continue;
3918
+ const columnRef = `${tableVar}.${snakeCase(field)}`;
3919
+ for (const op of operators) switch (op) {
3920
+ case "eq":
3921
+ filterLines.push(` if (filters?.${camelCase(field)}?.eq != null) conditions.push(eq(${columnRef}, filters.${camelCase(field)}.eq));`);
3922
+ break;
3923
+ case "in":
3924
+ filterLines.push(` if (filters?.${camelCase(field)}?.in) conditions.push(inArray(${columnRef}, filters.${camelCase(field)}.in));`);
3925
+ break;
3926
+ case "gt":
3927
+ filterLines.push(` if (filters?.${camelCase(field)}?.gt != null) conditions.push(gt(${columnRef}, filters.${camelCase(field)}.gt));`);
3136
3928
  break;
3137
3929
  case "gte":
3138
3930
  filterLines.push(` if (filters?.${camelCase(field)}?.gte != null) conditions.push(gte(${columnRef}, filters.${camelCase(field)}.gte));`);
@@ -3353,7 +4145,6 @@ function buildNestedPersistenceEqualityHelpers(allEntities) {
3353
4145
  lines.push(``);
3354
4146
  for (const entity of allEntities) {
3355
4147
  const entityName = pascalCase(entity.name);
3356
- camelCase(entity.name);
3357
4148
  const entityIdSnake = snakeCase(entity.idField);
3358
4149
  lines.push(`function equal${entityName}PersistenceRows(currentRows: any[], expectedRows: any[]): boolean {`);
3359
4150
  lines.push(` return equalPersistenceRowSet(currentRows, expectedRows, (row: any) => String(row.${entityIdSnake}), equal${entityName}Persistence);`);
@@ -3528,7 +4319,6 @@ function buildBottomUpAssembly(spec, _allEntities) {
3528
4319
  camelCase(parent.name);
3529
4320
  const parentIdField = parent.idField;
3530
4321
  const fkCol = snakeCase(parentIdField);
3531
- entity.collectionFieldName;
3532
4322
  if (entity.children.length === 0) {
3533
4323
  lines.push(` const ${entityVar}ByParent = new Map<string, any[]>();`);
3534
4324
  lines.push(` for (const r of ${entityVar}Rows) {`);
@@ -3712,7 +4502,7 @@ function buildExpectedChildRowCollectionLines(lines, children, parentAccessor, p
3712
4502
  const childItemVar = `${childVar}Item`;
3713
4503
  const fkCol = snakeCase(parentIdField);
3714
4504
  lines.push(`${indent}for (const ${childItemVar} of ${parentAccessor}.${child.collectionFieldName}) {`);
3715
- lines.push(`${indent} ${childRowsVar}.push({ ...${childVar}Codec.encode(${childItemVar} as any), ${fkCol}: String(${parentAccessor}.id.value) });`);
4505
+ lines.push(`${indent} ${childRowsVar}.push({ ...${childVar}Codec.encode(${childItemVar}), ${fkCol}: String(${parentAccessor}.id.value) });`);
3716
4506
  if (child.children.length > 0) buildExpectedChildRowCollectionLines(lines, child.children, childItemVar, child.idField, indent + " ");
3717
4507
  lines.push(`${indent}}`);
3718
4508
  }
@@ -3725,7 +4515,6 @@ function buildChildSaveBlock(lines, children, parentAccessor, parentIdField, ind
3725
4515
  const childIdField = child.idField;
3726
4516
  const childIdSnake = snakeCase(childIdField);
3727
4517
  const fkCol = snakeCase(parentIdField);
3728
- parentAccessor.includes(".") ? `${parentAccessor}${childIdField}` : `${parentAccessor}`;
3729
4518
  const parentIdValue = parentAccessor.includes(".") ? `${parentAccessor}.${getSimpleIdAccessor(parentIdField)}` : `String(${parentAccessor}.id.value)`;
3730
4519
  const iterVar = `${childVar}Item`;
3731
4520
  lines.push(`${indent}// Upsert ${child.name} children + delete orphans`);
@@ -3734,7 +4523,7 @@ function buildChildSaveBlock(lines, children, parentAccessor, parentIdField, ind
3734
4523
  lines.push(`${indent} and(eq(${childTableVar}.${fkCol}, ${parentIdValue}), notInArray(${childTableVar}.${childIdSnake}, current${pascalCase(child.name)}Ids))`);
3735
4524
  lines.push(`${indent});`);
3736
4525
  lines.push(`${indent}for (const ${iterVar} of ${parentAccessor}.${collectionField}) {`);
3737
- lines.push(`${indent} const ${childVar}Row = { ...${childVar}Codec.encode(${iterVar} as any), ${fkCol}: ${parentIdValue} };`);
4526
+ lines.push(`${indent} const ${childVar}Row = { ...${childVar}Codec.encode(${iterVar}), ${fkCol}: ${parentIdValue} };`);
3738
4527
  lines.push(`${indent} await tx.insert(${childTableVar}).values(${childVar}Row).onConflictDoUpdate({ target: ${childTableVar}.${childIdSnake}, set: ${childVar}Row });`);
3739
4528
  if (child.children.length > 0) buildChildSaveBlock(lines, child.children, iterVar, childIdField, indent + " ");
3740
4529
  lines.push(`${indent}}`);
@@ -3748,39 +4537,51 @@ function buildV5InfrastructureArtifacts(spec, options) {
3748
4537
  const modulePath = normalizeModulePath(spec.context.modulePath);
3749
4538
  const scopeKey = sliceArtifactOwnership(modulePath);
3750
4539
  const artifacts = [];
3751
- if (!options?.skipContextWideArtifacts) {
3752
- const tableContent = createDrizzleTableDefinition(spec);
4540
+ const forceMikroOrmRepositories = options?.infrastructureStrategy?.orm === "mikroorm";
4541
+ const hasDrizzleRepositoryAdapter = spec.adapters.some((adapter) => adapter.kind === "drizzle-repository");
4542
+ const shouldGenerateDrizzleTables = !forceMikroOrmRepositories && hasDrizzleRepositoryAdapter;
4543
+ if (!options?.skipContextWideArtifacts && shouldGenerateDrizzleTables) {
4544
+ const tableContent = createDrizzleTableDefinition(spec, resolveDrizzlePersistence(options?.infrastructureStrategy));
3753
4545
  if (tableContent !== null) {
3754
4546
  const tableLogicalPath = `infrastructure/persistence/${modulePath}/tables.ts`;
3755
4547
  artifacts.push(createGeneratedArtifact(tableLogicalPath, tableContent, scopeKey));
3756
4548
  }
3757
4549
  }
3758
- const repoArtifacts = spec.adapters.filter((adapter) => adapter.kind === "drizzle-repository").flatMap((adapter) => {
3759
- const port = spec.ports.find((p) => p.name === adapter.port);
3760
- 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)];
4550
+ const repoArtifacts = spec.ports.filter((port) => port.kind === "repository" && port.target === spec.aggregate.name).flatMap((port) => {
4551
+ const adapter = spec.adapters.find((candidate) => candidate.port === port.name && (candidate.kind === "drizzle-repository" || candidate.kind === "mikroorm-repository"));
4552
+ const repositoryAdapterKind = forceMikroOrmRepositories ? "mikroorm-repository" : adapter?.kind;
4553
+ if (repositoryAdapterKind === "drizzle-repository") {
4554
+ const logicalPath = buildArtifactPath("infrastructure/persistence/drizzle/repositories", modulePath, `drizzle-${kebabCase(spec.aggregate.name)}.repository.ts`);
4555
+ 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)];
4556
+ }
4557
+ if (repositoryAdapterKind === "mikroorm-repository") {
4558
+ const schemaLogicalPath = buildArtifactPath("infrastructure/persistence/mikroorm/entities", modulePath, `${kebabCase(spec.aggregate.name)}.entity-schema.ts`);
4559
+ const logicalPath = buildArtifactPath("infrastructure/persistence/mikroorm/repositories", modulePath, `mikroorm-${kebabCase(spec.aggregate.name)}.repository.ts`);
4560
+ return [createGeneratedArtifact(schemaLogicalPath, createMikroOrmEntitySchemaArtifactContent(spec), scopeKey), createGeneratedArtifact(logicalPath, createMikroOrmRepository(spec, port.name), scopeKey)];
4561
+ }
4562
+ return [];
3763
4563
  });
3764
4564
  artifacts.push(...repoArtifacts);
3765
4565
  return artifacts.sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
3766
4566
  }
3767
- function createContextDrizzleTableDefinition(contextSpec) {
4567
+ function createContextDrizzleTableDefinition(contextSpec, persistence = "postgres") {
3768
4568
  const blocks = [];
3769
4569
  const emittedTableNames = /* @__PURE__ */ new Set();
3770
4570
  let usesInteger = false;
3771
4571
  let usesReal = false;
3772
4572
  let usesBoolean = false;
3773
- let usesJsonb = false;
4573
+ let usesJson = false;
3774
4574
  for (const agg of contextSpec.aggregates) {
3775
4575
  const fields = filterManagedAggregateFields(agg.fields);
3776
4576
  if (!fields || fields.length === 0) continue;
3777
4577
  const children = agg.children ?? [];
3778
4578
  const aggScalarFields = scalarFields(fields, children);
4579
+ usesInteger = true;
3779
4580
  if (aggScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
3780
4581
  if (aggScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
3781
4582
  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));
4583
+ if (aggScalarFields.some((field) => field.array)) usesJson = true;
4584
+ blocks.push(buildTableBlock(agg.tableName, `${camelCase(agg.name)}Table`, aggScalarFields, agg.idField, void 0, true, persistence));
3784
4585
  emittedTableNames.add(agg.tableName);
3785
4586
  walkEntityTree({
3786
4587
  name: agg.name,
@@ -3795,12 +4596,12 @@ function createContextDrizzleTableDefinition(contextSpec) {
3795
4596
  if (entityScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
3796
4597
  if (entityScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
3797
4598
  if (entityScalarFields.some((field) => field.type === "boolean")) usesBoolean = true;
3798
- if (entityScalarFields.some((field) => field.array)) usesJsonb = true;
4599
+ if (entityScalarFields.some((field) => field.array)) usesJson = true;
3799
4600
  const fkColName = snakeCase("idField" in ctx.parent ? ctx.parent.idField : agg.idField);
3800
4601
  blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, {
3801
4602
  name: fkColName,
3802
4603
  type: "text"
3803
- }));
4604
+ }, false, persistence));
3804
4605
  emittedTableNames.add(entity.tableName);
3805
4606
  });
3806
4607
  }
@@ -3810,21 +4611,22 @@ function createContextDrizzleTableDefinition(contextSpec) {
3810
4611
  if (entityScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
3811
4612
  if (entityScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
3812
4613
  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));
4614
+ if (entityScalarFields.some((field) => field.array)) usesJson = true;
4615
+ blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, void 0, false, persistence));
3815
4616
  emittedTableNames.add(entity.tableName);
3816
4617
  }
3817
4618
  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) {
4619
+ const coreImportTokens = [persistence === "mysql" ? "mysqlTable" : "pgTable", "text"];
4620
+ if (usesInteger) coreImportTokens.push("integer");
4621
+ if (usesReal) coreImportTokens.push("real");
4622
+ if (usesBoolean) coreImportTokens.push("boolean");
4623
+ if (usesJson) coreImportTokens.push(persistence === "mysql" ? "json" : "jsonb");
4624
+ return `import { ${coreImportTokens.join(", ")} } from "${persistence === "mysql" ? "drizzle-orm/mysql-core" : "drizzle-orm/pg-core"}";\n\n${blocks.join("\n\n")}\n`;
4625
+ }
4626
+ function createReadModelTableDefinition(readModel, persistence = "postgres") {
4627
+ const tableBuilder = persistence === "mysql" ? "mysqlTable" : "pgTable";
3826
4628
  const importTokens = new Set([
3827
- "pgTable",
4629
+ tableBuilder,
3828
4630
  "primaryKey",
3829
4631
  "text"
3830
4632
  ]);
@@ -3832,8 +4634,8 @@ function createReadModelTableDefinition(readModel) {
3832
4634
  if (field.type === "number" && !isDecimalNumberField(field)) importTokens.add("integer");
3833
4635
  if (isDecimalNumberField(field)) importTokens.add("real");
3834
4636
  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))},`;
4637
+ if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0) importTokens.add(persistence === "mysql" ? "json" : "jsonb");
4638
+ return ` ${snakeCase(field.name)}: ${drizzleColumnBuilder(field, snakeCase(field.name), persistence)},`;
3837
4639
  });
3838
4640
  if (readModel.indexes.length > 0) importTokens.add("index");
3839
4641
  if (readModel.uniqueConstraints.length > 0) importTokens.add("uniqueIndex");
@@ -3842,9 +4644,9 @@ function createReadModelTableDefinition(readModel) {
3842
4644
  for (const uniqueConstraint of readModel.uniqueConstraints) constraintLines.push(` ${camelCase(uniqueConstraint.name)}: uniqueIndex("${uniqueConstraint.name}").on(${uniqueConstraint.fields.map((field) => `table.${snakeCase(field)}`).join(", ")}),`);
3843
4645
  const tableConstName = readModelTableConstName(readModel.name);
3844
4646
  const typeName = pascalCase(readModel.name);
3845
- return `import { ${[...importTokens].sort().join(", ")} } from "drizzle-orm/pg-core";
4647
+ return `import { ${[...importTokens].sort().join(", ")} } from "${persistence === "mysql" ? "drizzle-orm/mysql-core" : "drizzle-orm/pg-core"}";
3846
4648
 
3847
- export const ${tableConstName} = pgTable("${readModel.tableName}", {
4649
+ export const ${tableConstName} = ${tableBuilder}("${readModel.tableName}", {
3848
4650
  ${columnLines.join("\n")}
3849
4651
  }, (table) => ({
3850
4652
  ${constraintLines.join("\n")}
@@ -3889,22 +4691,200 @@ function buildReadModelCompositionArtifacts(contextSpecs) {
3889
4691
  };
3890
4692
  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
4693
  }
3892
- function buildV5InfrastructureContextArtifacts(contextSpec) {
4694
+ function projectionWriterClassName(projectionName) {
4695
+ const baseName = pascalCase(projectionName);
4696
+ return `MikroOrm${baseName.endsWith("Projection") ? baseName : `${baseName}Projection`}Writer`;
4697
+ }
4698
+ function drizzleProjectionWriterClassName(projectionName) {
4699
+ const baseName = pascalCase(projectionName);
4700
+ return `Drizzle${baseName.endsWith("Projection") ? baseName : `${baseName}Projection`}Writer`;
4701
+ }
4702
+ function projectionWritePortName$1(projectionName) {
4703
+ return `${pascalCase(projectionName)}WritePort`;
4704
+ }
4705
+ function projectionWritePortImportPath(modulePath, projectionName) {
4706
+ return `../../../../../core/contexts/${modulePath}/application/ports/projections/${kebabCase(projectionName)}.projection-write.port.ts`;
4707
+ }
4708
+ function projectionWriterPayloadTypeName(source) {
4709
+ return `${pascalCase(trimEventNameSuffix(source.eventName))}Payload`;
4710
+ }
4711
+ function projectionWriterPayloadImportPath(source) {
4712
+ return `../../../../../core/contexts/${projectionSourceModulePath$1(source.contextName)}/domain/events/${kebabCase(trimEventNameSuffix(source.eventName))}.event.ts`;
4713
+ }
4714
+ function trimEventNameSuffix(eventName) {
4715
+ return eventName.endsWith("Event") ? eventName.slice(0, -5) : eventName;
4716
+ }
4717
+ function projectionSourceModulePath$1(contextName) {
4718
+ return normalizeModulePath(contextName.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2"));
4719
+ }
4720
+ function projectionWriterEventType(source) {
4721
+ const payloadType = projectionWriterPayloadTypeName(source);
4722
+ return `EventEnvelope<${payloadType}> | ${payloadType}`;
4723
+ }
4724
+ function buildProjectionWriterMethod(readModel, source, executorVariable, executorExpression, persistence = "postgres") {
4725
+ const methodName = `on${pascalCase(source.eventName)}`;
4726
+ const eventType = projectionWriterEventType(source);
4727
+ const payloadLine = ` const payload = extractProjectionPayload(event);`;
4728
+ const executorLine = ` const ${executorVariable} = ${executorExpression};`;
4729
+ const mutation = source.mutation;
4730
+ if (mutation.kind === "delete") {
4731
+ const whereClause = mutation.matchOn.map((field) => `${snakeCase(field)} = ?`).join(" and ");
4732
+ const deleteParams = mutation.matchOn.map((field) => `payload.${field}`).join(", ");
4733
+ return ` async ${methodName}(event: ${eventType}, tx: Transaction): Promise<void> {
4734
+ ${payloadLine}
4735
+ ${executorLine}
4736
+ await ${executorVariable}.execute(
4737
+ ${JSON.stringify(`delete from ${readModel.tableName} where ${whereClause}`)},
4738
+ [${deleteParams}],
4739
+ );
4740
+ }`;
4741
+ }
4742
+ if (mutation.kind === "custom") return ` async ${mutation.handlerName}(event: ${eventType}, tx: Transaction): Promise<void> {
4743
+ ${payloadLine}
4744
+ ${executorLine}
4745
+ // TODO: implement custom projection mutation
4746
+ throw new Error("Custom projection mutation not implemented");
4747
+ }`;
4748
+ const writeEntries = [...Object.entries(mutation.set ?? {})];
4749
+ for (const field of mutation.matchOn) if (!writeEntries.some(([fieldName]) => fieldName === field)) writeEntries.push([field, {
4750
+ kind: "event-field",
4751
+ field
4752
+ }]);
4753
+ const insertColumns = writeEntries.map(([fieldName]) => snakeCase(fieldName));
4754
+ const insertParams = writeEntries.map(([, mapping]) => buildProjectionMutationValueExpression(mapping));
4755
+ const conflictColumns = mutation.matchOn.map((field) => snakeCase(field));
4756
+ const updateAssignments = persistence === "mysql" ? insertColumns.map((column) => `${column} = values(${column})`) : insertColumns.map((column) => `${column} = excluded.${column}`);
4757
+ if (mutation.kind === "upsert") {
4758
+ const upsertClause = persistence === "mysql" ? `on duplicate key update ${updateAssignments.join(", ")}` : `on conflict (${conflictColumns.join(", ")}) do update set ${updateAssignments.join(", ")}`;
4759
+ return ` async ${methodName}(event: ${eventType}, tx: Transaction): Promise<void> {
4760
+ ${payloadLine}
4761
+ ${executorLine}
4762
+ await ${executorVariable}.execute(
4763
+ ${JSON.stringify(`insert into ${readModel.tableName} (${insertColumns.join(", ")}) values (${insertColumns.map(() => "?").join(", ")}) ${upsertClause}`)},
4764
+ [${insertParams.join(", ")}],
4765
+ );
4766
+ }`;
4767
+ }
4768
+ const _exhaustive = mutation;
4769
+ throw new Error(`Unsupported projection mutation kind: ${_exhaustive.kind}`);
4770
+ }
4771
+ function buildProjectionWriterCapabilityStubMethod(projection, source) {
4772
+ const methodName = `on${pascalCase(source.eventName)}`;
4773
+ const eventType = projectionWriterEventType(source);
4774
+ const reasons = projection.capabilities.writeModel.reasons.length > 0 ? ` Reasons: ${projection.capabilities.writeModel.reasons.join("; ")}.` : "";
4775
+ const diagnosticMessage = `[infrastructure_generator] Projection "${projection.name}" cannot be fully generated for read-model "${projection.readModelName}".${reasons}`;
4776
+ return ` async ${methodName}(event: ${eventType}, tx: Transaction): Promise<void> {
4777
+ void event;
4778
+ void tx;
4779
+ throw new Error(${JSON.stringify(diagnosticMessage)});
4780
+ }`;
4781
+ }
4782
+ function buildProjectionMutationValueExpression(mapping) {
4783
+ if (mapping.kind === "event-field") return `payload.${mapping.field}`;
4784
+ if (mapping.kind === "literal") return JSON.stringify(mapping.value);
4785
+ return `(${mapping.expression})`;
4786
+ }
4787
+ function createMikroOrmProjectionWriterArtifact(modulePath, projection, readModel, persistence = "postgres") {
4788
+ const className = projectionWriterClassName(projection.name);
4789
+ const writePortName = projectionWritePortName$1(projection.name);
4790
+ const writeCapability = projection.capabilities?.writeModel;
4791
+ const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionWriterPayloadTypeName(source)} } from "${projectionWriterPayloadImportPath(source)}";`);
4792
+ const methodBlocks = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => writeCapability?.status !== "runtime-throw" ? buildProjectionWriterMethod(readModel, source, "connection", "getMikroOrmConnection(tx)", persistence) : buildProjectionWriterCapabilityStubMethod(projection, source)).join("\n\n");
4793
+ return createGeneratedArtifact(`infrastructure/persistence/mikroorm/projection-writers/${modulePath}/${kebabCase(projection.name)}.projection-writer.ts`, `import type { EventEnvelope } from "../../../../../lib/event-envelope.ts";
4794
+ import type { Transaction } from "../../../../../lib/transaction.ts";
4795
+ import type { ${writePortName} } from "${projectionWritePortImportPath(modulePath, projection.name)}";
4796
+ ${payloadImports.join("\n")}
4797
+
4798
+ type SqlExecutor = {
4799
+ execute(sql: string, params: unknown[]): Promise<unknown>;
4800
+ };
4801
+
4802
+ function extractProjectionPayload<TPayload>(event: EventEnvelope<TPayload> | TPayload): Record<string, unknown> {
4803
+ if (typeof event === "object" && event !== null && "payload" in event) {
4804
+ return event.payload as Record<string, unknown>;
4805
+ }
4806
+ return (event ?? {}) as Record<string, unknown>;
4807
+ }
4808
+
4809
+ function getMikroOrmConnection(tx: Transaction): SqlExecutor {
4810
+ if (typeof tx === "object" && tx !== null) {
4811
+ const candidate = tx as { getConnection?: unknown };
4812
+ if (typeof candidate.getConnection === "function") {
4813
+ const connection = candidate.getConnection.call(tx);
4814
+ if (typeof connection === "object" && connection !== null) {
4815
+ const executor = connection as { execute?: unknown };
4816
+ if (typeof executor.execute === "function") {
4817
+ return connection as SqlExecutor;
4818
+ }
4819
+ }
4820
+ }
4821
+ }
4822
+ throw new Error("MikroORM projection writer requires a transaction with getConnection().");
4823
+ }
4824
+
4825
+ // Generated tx-scoped MikroORM projection writer for "${projection.readModelName}".
4826
+ export class ${className} implements ${writePortName} {
4827
+ ${methodBlocks}
4828
+ }
4829
+ `, sliceArtifactOwnership(modulePath));
4830
+ }
4831
+ function createDrizzleProjectionWriterArtifact(modulePath, projection, readModel, persistence = "postgres") {
4832
+ const className = drizzleProjectionWriterClassName(projection.name);
4833
+ const writePortName = projectionWritePortName$1(projection.name);
4834
+ const writeCapability = projection.capabilities?.writeModel;
4835
+ const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionWriterPayloadTypeName(source)} } from "${projectionWriterPayloadImportPath(source)}";`);
4836
+ const methodBlocks = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => writeCapability?.status !== "runtime-throw" ? buildProjectionWriterMethod(readModel, source, "executor", "tx as DrizzleSqlExecutor", persistence) : buildProjectionWriterCapabilityStubMethod(projection, source)).join("\n\n");
4837
+ return createGeneratedArtifact(`infrastructure/persistence/drizzle/projection-writers/${modulePath}/${kebabCase(projection.name)}.projection-writer.ts`, `import type { EventEnvelope } from "../../../../../lib/event-envelope.ts";
4838
+ import type { Transaction } from "../../../../../lib/transaction.ts";
4839
+ import type { ${writePortName} } from "${projectionWritePortImportPath(modulePath, projection.name)}";
4840
+ ${payloadImports.join("\n")}
4841
+
4842
+ type DrizzleSqlExecutor = {
4843
+ execute(sql: string, params: unknown[]): Promise<unknown>;
4844
+ };
4845
+
4846
+ function extractProjectionPayload<TPayload>(event: EventEnvelope<TPayload> | TPayload): Record<string, unknown> {
4847
+ if (typeof event === "object" && event !== null && "payload" in event) {
4848
+ return event.payload as Record<string, unknown>;
4849
+ }
4850
+ return (event ?? {}) as Record<string, unknown>;
4851
+ }
4852
+
4853
+ // Generated tx-scoped Drizzle projection writer for "${projection.readModelName}".
4854
+ export class ${className} implements ${writePortName} {
4855
+ ${methodBlocks}
4856
+ }
4857
+ `, sliceArtifactOwnership(modulePath));
4858
+ }
4859
+ function buildV5InfrastructureContextArtifacts(contextSpec, options = {}) {
3893
4860
  const modulePath = normalizeModulePath(contextSpec.context.modulePath);
3894
4861
  const scopeKey = sliceArtifactOwnership(modulePath);
3895
4862
  const artifacts = [];
3896
4863
  const readModels = contextSpec.readModels ?? [];
3897
- const tableContent = createContextDrizzleTableDefinition(contextSpec);
3898
- if (tableContent !== null) artifacts.push(createGeneratedArtifact(`infrastructure/persistence/${modulePath}/tables.ts`, tableContent, scopeKey));
4864
+ if (options.infrastructureStrategy?.orm !== "mikroorm") {
4865
+ const tableContent = createContextDrizzleTableDefinition(contextSpec, resolveDrizzlePersistence(options.infrastructureStrategy));
4866
+ if (tableContent !== null) artifacts.push(createGeneratedArtifact(`infrastructure/persistence/${modulePath}/tables.ts`, tableContent, scopeKey));
4867
+ }
3899
4868
  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));
4869
+ 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
4870
  artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/drizzle/schema.ts`, createReadModelContextSchemaBarrel({
3902
4871
  ...contextSpec,
3903
4872
  readModels
3904
4873
  }), scopeKey));
3905
4874
  artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/tables.ts`, createReadModelCompatibilityBarrel(), scopeKey));
3906
4875
  } 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));
4876
+ 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));
4877
+ const projections = contextSpec.projections ?? [];
4878
+ if (options.infrastructureStrategy?.orm === "mikroorm") for (const projection of projections) {
4879
+ const readModel = readModels.find((candidate) => candidate.name === projection.readModelName);
4880
+ if (!readModel) continue;
4881
+ artifacts.push(createMikroOrmProjectionWriterArtifact(modulePath, projection, readModel, resolveDrizzlePersistence(options.infrastructureStrategy)));
4882
+ }
4883
+ else if (options.infrastructureStrategy?.orm === "drizzle") for (const projection of projections) {
4884
+ const readModel = readModels.find((candidate) => candidate.name === projection.readModelName);
4885
+ if (!readModel) throw new Error(`[infrastructure_generator] Projection "${projection.name}" cannot be fully generated because read-model "${projection.readModelName}" was not found.`);
4886
+ artifacts.push(createDrizzleProjectionWriterArtifact(modulePath, projection, readModel, resolveDrizzlePersistence(options.infrastructureStrategy)));
4887
+ }
3908
4888
  return artifacts;
3909
4889
  }
3910
4890
  function createViewModelTablesSkeleton(contextSpec) {
@@ -3919,49 +4899,41 @@ function createViewModelTablesSkeleton(contextSpec) {
3919
4899
  for (const { readModelName, queries } of groupQueriesByReadModel(contextSpec.queries)) {
3920
4900
  lines.push(` ${camelCase(readModelName)}: {`);
3921
4901
  lines.push(` readModel: "${readModelName}",`);
3922
- lines.push(` contract: "${readModelContractName$2(readModelName)}",`);
4902
+ lines.push(` contract: "${readModelContractName$1(readModelName)}",`);
3923
4903
  lines.push(` servesQueries: [${queries.map((query) => `"${query.name}"`).join(", ")}],`);
3924
- lines.push(` suggestedTableName: "${snakeCase(modulePath).replaceAll("/", "_")}_${snakeCase(readModelRepositoryFileBase$1(readModelName))}",`);
4904
+ lines.push(` suggestedTableName: "${snakeCase(modulePath).replace(/\//g, "_")}_${snakeCase(readModelRepositoryFileBase$1(readModelName))}",`);
3925
4905
  lines.push(` },`);
3926
4906
  }
3927
4907
  lines.push(`} as const;`);
3928
4908
  lines.push(``);
3929
4909
  return lines.join("\n");
3930
4910
  }
4911
+ function createReadModelRepositoryContent(modulePath, readModelName, queries, readModels, infrastructureStrategy) {
4912
+ if (!supportsGeneratedReadModelQueries(infrastructureStrategy)) return createReadModelRepositorySkeleton(modulePath, readModelName, queries);
4913
+ const readModel = readModels.find((candidate) => candidate.name === readModelName);
4914
+ if (!readModel) return createReadModelRepositorySkeleton(modulePath, readModelName, queries);
4915
+ return createSupportedReadModelRepository(modulePath, readModel, queries);
4916
+ }
4917
+ function supportsGeneratedReadModelQueries(infrastructureStrategy) {
4918
+ if (!infrastructureStrategy) return true;
4919
+ return (infrastructureStrategy.architecture === "physical-cqrs" || infrastructureStrategy.architecture === "logical-cqrs") && (infrastructureStrategy.persistence === "postgres" || infrastructureStrategy.persistence === "mysql");
4920
+ }
3931
4921
  function createReadModelRepositorySkeleton(modulePath, readModelName, queries) {
3932
4922
  const className = readModelRepositoryClassName(readModelName);
3933
- const portTypeName = `${readModelContractName$2(readModelName)}RepositoryPort`;
4923
+ const portTypeName = `${readModelContractName$1(readModelName)}RepositoryPort`;
3934
4924
  const fileBase = readModelRepositoryFileBase$1(readModelName);
3935
4925
  const importLines = /* @__PURE__ */ new Map();
3936
4926
  let needsPaginatedResult = false;
3937
4927
  importLines.set("tx", `import type { Transaction } from "../../../../../lib/transaction.ts";`);
3938
4928
  importLines.set("port", `import type { ${portTypeName} } from "../../../../../core/contexts/${modulePath}/application/ports/read-models/${fileBase}.repository.port.ts";`);
3939
4929
  const methodBlocks = queries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((query) => {
3940
- const methodName = queryReadModelMethodName(query);
3941
4930
  const queryTypeName = `${pascalCase(query.name)}Query`;
3942
4931
  const outputTypeName = queryOutputContractName(query);
3943
- const viewFileBase = queryViewFileBase$1(query);
4932
+ const viewFileBase = queryViewFileBase(query);
3944
4933
  importLines.set(`query:${queryTypeName}`, `import type { ${queryTypeName} } from "../../../../../core/contexts/${modulePath}/application/queries/${kebabCase(query.name)}.query.ts";`);
3945
4934
  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
- }`;
4935
+ if (query.queryKind === "list") needsPaginatedResult = true;
4936
+ return createSingleReadModelMethodSkeleton(query, readModelName);
3965
4937
  });
3966
4938
  if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "../../../../../lib/pagination.ts";`);
3967
4939
  return `${[...importLines.values()].join("\n")}
@@ -3974,18 +4946,232 @@ ${methodBlocks.join("\n\n")}
3974
4946
  }
3975
4947
  `;
3976
4948
  }
4949
+ function createSupportedReadModelRepository(modulePath, readModel, queries) {
4950
+ const className = readModelRepositoryClassName(readModel.name);
4951
+ const portTypeName = `${readModelContractName$1(readModel.name)}RepositoryPort`;
4952
+ const fileBase = readModelRepositoryFileBase$1(readModel.name);
4953
+ const importLines = /* @__PURE__ */ new Map();
4954
+ const drizzleImports = /* @__PURE__ */ new Set();
4955
+ let needsPaginatedResult = false;
4956
+ let needsSqlType = false;
4957
+ let needsTableImport = false;
4958
+ importLines.set("tx", `import type { Transaction } from "../../../../../lib/transaction.ts";`);
4959
+ importLines.set("port", `import type { ${portTypeName} } from "../../../../../core/contexts/${modulePath}/application/ports/read-models/${fileBase}.repository.port.ts";`);
4960
+ const methodBlocks = queries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((query) => {
4961
+ const queryTypeName = `${pascalCase(query.name)}Query`;
4962
+ const outputTypeName = queryOutputContractName(query);
4963
+ const viewFileBase = queryViewFileBase(query);
4964
+ importLines.set(`query:${queryTypeName}`, `import type { ${queryTypeName} } from "../../../../../core/contexts/${modulePath}/application/queries/${kebabCase(query.name)}.query.ts";`);
4965
+ importLines.set(`view:${outputTypeName}`, `import type { ${outputTypeName} } from "../../../../../core/contexts/${modulePath}/application/contracts/${viewFileBase}.view.ts";`);
4966
+ const generatedMethod = createGeneratedReadModelMethod(readModel, query, drizzleImports);
4967
+ if (generatedMethod !== null) {
4968
+ needsTableImport = true;
4969
+ if (query.queryKind === "list") {
4970
+ needsPaginatedResult = true;
4971
+ needsSqlType = needsSqlType || generatedMethod.includes("SQL[]");
4972
+ }
4973
+ return generatedMethod;
4974
+ }
4975
+ if (query.queryKind === "list") needsPaginatedResult = true;
4976
+ return createSingleReadModelMethodSkeleton(query, readModel.name);
4977
+ });
4978
+ if (needsTableImport) importLines.set("table", `import { ${readModelTableConstName(readModel.name)} } from "../../../${modulePath}/drizzle/tables/${fileBase}.table.ts";`);
4979
+ if (drizzleImports.size > 0) importLines.set("drizzle", `import { ${[...drizzleImports].sort().join(", ")} } from "drizzle-orm";`);
4980
+ if (needsSqlType) importLines.set("sql", `import type { SQL } from "drizzle-orm";`);
4981
+ if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "../../../../../lib/pagination.ts";`);
4982
+ return `${[...importLines.values()].join("\n")}
4983
+
4984
+ // Generated read-model repository for the supported infrastructure strategy.
4985
+ export class ${className} implements ${portTypeName} {
4986
+ constructor(private readonly db: unknown) {}
4987
+
4988
+ ${methodBlocks.join("\n\n")}
4989
+ }
4990
+ `;
4991
+ }
4992
+ function createGeneratedReadModelMethod(readModel, query, drizzleImports) {
4993
+ if (!canMapReadModelOutput(readModel, query.outputFields)) return null;
4994
+ if (query.queryKind === "findById") {
4995
+ const lookupField = resolveFindByIdLookupField(readModel, query);
4996
+ if (!lookupField) return null;
4997
+ drizzleImports.add("eq");
4998
+ return createFindByIdReadModelMethod(readModel, query, lookupField);
4999
+ }
5000
+ drizzleImports.add("asc");
5001
+ drizzleImports.add("count");
5002
+ drizzleImports.add("desc");
5003
+ const filterData = buildReadModelFilterData(readModel, query, readModelTableConstName(readModel.name));
5004
+ if (filterData.lines.length > 0) drizzleImports.add("and");
5005
+ for (const drizzleImport of filterData.imports) drizzleImports.add(drizzleImport);
5006
+ return createListReadModelMethod(readModel, query, filterData.lines);
5007
+ }
5008
+ function canMapReadModelOutput(readModel, outputFields) {
5009
+ const readModelFields = new Set(readModel.fields.map((field) => camelCase(field.name)));
5010
+ return outputFields.every((field) => readModelFields.has(camelCase(field.name)));
5011
+ }
5012
+ function resolveFindByIdLookupField(readModel, query) {
5013
+ if (readModel.primaryKey.length !== 1) return null;
5014
+ const primaryKeyField = camelCase(readModel.primaryKey[0]);
5015
+ const matchingField = query.inputFields.find((field) => camelCase(field.name) === primaryKeyField);
5016
+ if (matchingField) return camelCase(matchingField.name);
5017
+ if (query.inputFields.length === 1) return camelCase(query.inputFields[0].name);
5018
+ return null;
5019
+ }
5020
+ function createFindByIdReadModelMethod(readModel, query, lookupField) {
5021
+ const methodName = queryReadModelMethodName(query);
5022
+ const queryTypeName = `${pascalCase(query.name)}Query`;
5023
+ const outputTypeName = queryOutputContractName(query);
5024
+ const tableConstName = readModelTableConstName(readModel.name);
5025
+ return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<${outputTypeName}> {
5026
+ void this.db;
5027
+ const rows = await tx.select()
5028
+ .from(${tableConstName})
5029
+ .where(eq(${tableConstName}.${snakeCase(readModel.primaryKey[0])}, query.payload.${lookupField}))
5030
+ .limit(1);
5031
+
5032
+ const row = rows[0];
5033
+ if (!row) {
5034
+ throw new Error("${outputTypeName} row not found");
5035
+ }
5036
+
5037
+ return {
5038
+ ${buildReadModelOutputMapping(query.outputFields, "row")}
5039
+ };
5040
+ }`;
5041
+ }
5042
+ function createListReadModelMethod(readModel, query, filterLines) {
5043
+ const methodName = queryReadModelMethodName(query);
5044
+ const queryTypeName = `${pascalCase(query.name)}Query`;
5045
+ const outputTypeName = queryOutputContractName(query);
5046
+ const tableConstName = readModelTableConstName(readModel.name);
5047
+ const payloadDestructure = filterLines.length > 0 ? `page = 1, pageSize = 20, filters, sortBy, sortDirection` : `page = 1, pageSize = 20, sortBy, sortDirection`;
5048
+ const whereBlock = filterLines.length > 0 ? ` const conditions: SQL[] = [];
5049
+ ${filterLines.join("\n")}
5050
+
5051
+ const where = conditions.length > 0 ? and(...conditions) : undefined;` : ` const where = undefined;`;
5052
+ const sortFieldEntries = query.sorting.sortableFields.filter((field) => readModel.fields.some((candidate) => camelCase(candidate.name) === camelCase(field))).map((field) => ` ${camelCase(field)}: ${tableConstName}.${snakeCase(field)},`);
5053
+ const defaultSortField = camelCase(query.sorting.defaultSort.field);
5054
+ return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<PaginatedResult<${outputTypeName}>> {
5055
+ void this.db;
5056
+ const { ${payloadDestructure} } = query.payload;
5057
+
5058
+ ${whereBlock}
5059
+ const sortColumn = (({
5060
+ ${sortFieldEntries.join("\n")}
5061
+ } as Record<string, unknown>)[sortBy ?? "${defaultSortField}"] ?? ${tableConstName}.${snakeCase(query.sorting.defaultSort.field)}) as import("drizzle-orm").AnyColumn;
5062
+ const orderBy = sortDirection === "desc"
5063
+ ? [desc(sortColumn)]
5064
+ : [asc(sortColumn)];
5065
+
5066
+ const [rows, countResult] = await Promise.all([
5067
+ tx.select()
5068
+ .from(${tableConstName})
5069
+ .where(where)
5070
+ .orderBy(...orderBy)
5071
+ .limit(pageSize)
5072
+ .offset((page - 1) * pageSize),
5073
+ tx.select({ count: count() }).from(${tableConstName}).where(where),
5074
+ ]);
5075
+
5076
+ return {
5077
+ items: rows.map((row) => ({
5078
+ ${buildReadModelOutputMapping(query.outputFields, "row")}
5079
+ })),
5080
+ total: Number(countResult[0]?.count ?? 0),
5081
+ page,
5082
+ pageSize,
5083
+ };
5084
+ }`;
5085
+ }
5086
+ function buildReadModelFilterData(readModel, query, tableConstName) {
5087
+ const readModelFields = new Set(readModel.fields.map((field) => camelCase(field.name)));
5088
+ const lines = [];
5089
+ const imports = /* @__PURE__ */ new Set();
5090
+ for (const [field, operators] of Object.entries(query.filters)) {
5091
+ const normalizedField = camelCase(field);
5092
+ if (!readModelFields.has(normalizedField)) continue;
5093
+ const columnRef = `${tableConstName}.${snakeCase(field)}`;
5094
+ for (const operator of operators) switch (operator) {
5095
+ case "eq":
5096
+ imports.add("eq");
5097
+ lines.push(` if (filters?.${normalizedField}?.eq != null) conditions.push(eq(${columnRef}, filters.${normalizedField}.eq));`);
5098
+ break;
5099
+ case "in":
5100
+ imports.add("inArray");
5101
+ lines.push(` if (filters?.${normalizedField}?.in) conditions.push(inArray(${columnRef}, filters.${normalizedField}.in));`);
5102
+ break;
5103
+ case "gt":
5104
+ imports.add("gt");
5105
+ lines.push(` if (filters?.${normalizedField}?.gt != null) conditions.push(gt(${columnRef}, filters.${normalizedField}.gt));`);
5106
+ break;
5107
+ case "gte":
5108
+ imports.add("gte");
5109
+ lines.push(` if (filters?.${normalizedField}?.gte != null) conditions.push(gte(${columnRef}, filters.${normalizedField}.gte));`);
5110
+ break;
5111
+ case "lt":
5112
+ imports.add("lt");
5113
+ lines.push(` if (filters?.${normalizedField}?.lt != null) conditions.push(lt(${columnRef}, filters.${normalizedField}.lt));`);
5114
+ break;
5115
+ case "lte":
5116
+ imports.add("lte");
5117
+ lines.push(` if (filters?.${normalizedField}?.lte != null) conditions.push(lte(${columnRef}, filters.${normalizedField}.lte));`);
5118
+ break;
5119
+ case "contains":
5120
+ imports.add("like");
5121
+ lines.push(` if (filters?.${normalizedField}?.contains) conditions.push(like(${columnRef}, \`%\${filters.${normalizedField}.contains}%\`));`);
5122
+ break;
5123
+ case "startsWith":
5124
+ imports.add("like");
5125
+ lines.push(` if (filters?.${normalizedField}?.startsWith) conditions.push(like(${columnRef}, \`\${filters.${normalizedField}.startsWith}%\`));`);
5126
+ break;
5127
+ }
5128
+ }
5129
+ return {
5130
+ lines,
5131
+ imports
5132
+ };
5133
+ }
5134
+ function buildReadModelOutputMapping(outputFields, rowVar) {
5135
+ return outputFields.map((field) => {
5136
+ const outputField = camelCase(field.name);
5137
+ const rowField = snakeCase(field.name);
5138
+ return ` ${outputField}: ${field.optional ? `${rowVar}.${rowField} ?? undefined` : `${rowVar}.${rowField}`},`;
5139
+ }).join("\n");
5140
+ }
5141
+ function createSingleReadModelMethodSkeleton(query, readModelName) {
5142
+ const methodName = queryReadModelMethodName(query);
5143
+ const queryTypeName = `${pascalCase(query.name)}Query`;
5144
+ const outputTypeName = queryOutputContractName(query);
5145
+ const targetName = readModelName ?? resolvedReadModelName$1(query);
5146
+ const capabilityReasons = query.capability?.reasons ?? [];
5147
+ const diagnosticReasons = capabilityReasons.length > 0 ? ` Reasons: ${capabilityReasons.join("; ")}.` : "";
5148
+ if (query.queryKind === "list") return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<PaginatedResult<${outputTypeName}>> {
5149
+ void this.db;
5150
+ void query;
5151
+ void tx;
5152
+ throw new Error(
5153
+ "[infrastructure_generator] Query \\"${query.name}\\" for read-model \\"${targetName}\\" cannot be fully generated. " +
5154
+ "Ensure output fields map directly to read-model fields and list-query metadata is fully declared." + ${JSON.stringify(diagnosticReasons)},
5155
+ );
5156
+ }`;
5157
+ return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<${outputTypeName}> {
5158
+ void this.db;
5159
+ void query;
5160
+ void tx;
5161
+ throw new Error(
5162
+ "[infrastructure_generator] Query \\"${query.name}\\" for read-model \\"${targetName}\\" cannot be fully generated. " +
5163
+ "Ensure output fields map directly to read-model fields and findById queries resolve a stable lookup field." + ${JSON.stringify(diagnosticReasons)},
5164
+ );
5165
+ }`;
5166
+ }
3977
5167
 
3978
5168
  //#endregion
3979
5169
  //#region packages/core/generators/projections.ts
3980
- function readModelContractName$1(readModelName) {
3981
- const baseName = pascalCase(readModelName);
3982
- return baseName.endsWith("View") ? baseName : `${baseName}View`;
5170
+ function projectionWritePortName(projectionName) {
5171
+ return `${pascalCase(projectionName)}WritePort`;
3983
5172
  }
3984
- function readModelRepositoryPortName(readModelName) {
3985
- return `${readModelContractName$1(readModelName)}RepositoryPort`;
3986
- }
3987
- function readModelRepositoryPortFileBase(readModelName) {
3988
- return kebabCase(readModelName).replace(/-view$/, "");
5173
+ function projectionWritePortVariableName(projectionName) {
5174
+ return camelCase(projectionWritePortName(projectionName));
3989
5175
  }
3990
5176
  function projectionFileBase(projectionName) {
3991
5177
  return kebabCase(projectionName);
@@ -3993,57 +5179,52 @@ function projectionFileBase(projectionName) {
3993
5179
  function projectionVariableName(projectionName) {
3994
5180
  return camelCase(projectionName);
3995
5181
  }
3996
- function readModelRepositoryVariableName(readModelName) {
3997
- return camelCase(readModelRepositoryPortName(readModelName));
3998
- }
3999
5182
  function projectionSourceHandlerName(eventName) {
4000
5183
  return `on${pascalCase(eventName)}`;
4001
5184
  }
5185
+ function projectionSourcePayloadTypeName(source) {
5186
+ return `${pascalCase(trimProjectionSourceEventName(source.eventName))}Payload`;
5187
+ }
5188
+ function projectionSourcePayloadImportPath(currentModulePath, source) {
5189
+ const sourceModulePath = projectionSourceModulePath(source.contextName);
5190
+ const eventFile = `${kebabCase(trimProjectionSourceEventName(source.eventName))}.event.ts`;
5191
+ if (sourceModulePath === currentModulePath) return `../../domain/events/${eventFile}`;
5192
+ return `../../../${sourceModulePath}/domain/events/${eventFile}`;
5193
+ }
5194
+ function trimProjectionSourceEventName(eventName) {
5195
+ return eventName.endsWith("Event") ? eventName.slice(0, -5) : eventName;
5196
+ }
5197
+ function projectionSourceModulePath(contextName) {
5198
+ return normalizeModulePath(contextName.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2"));
5199
+ }
5200
+ function projectionSourceEventType(source) {
5201
+ const payloadType = projectionSourcePayloadTypeName(source);
5202
+ return `EventEnvelope<${payloadType}> | ${payloadType}`;
5203
+ }
4002
5204
  function buildProjectionSourceMethod(projection, source) {
4003
5205
  const methodName = projectionSourceHandlerName(source.eventName);
4004
5206
  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;
5207
+ return ` async ${methodName}(event: ${projectionSourceEventType(source)}, tx: Transaction): Promise<void> {
4027
5208
  ${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.
5209
+ return await this.${projectionWritePortVariableName(projection.name)}.${methodName}(event, tx);
4032
5210
  }`;
4033
5211
  }
4034
5212
  function buildProjectorArtifact(modulePath, projection) {
4035
5213
  const projectorClassName = projection.name;
4036
- const repositoryPortName = readModelRepositoryPortName(projection.readModelName);
4037
- const repositoryPortVar = readModelRepositoryVariableName(projection.readModelName);
5214
+ const writePortName = projectionWritePortName(projection.name);
5215
+ const writePortVar = projectionWritePortVariableName(projection.name);
4038
5216
  const fileBase = projectionFileBase(projection.name);
5217
+ const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionSourcePayloadTypeName(source)} } from "${projectionSourcePayloadImportPath(modulePath, source)}";`);
4039
5218
  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";
5219
+ return createGeneratedArtifact(`core/contexts/${modulePath}/application/projections/${fileBase}.projector.ts`, `import type { EventEnvelope } from "../../../../../lib/event-envelope.ts";
5220
+ import type { Transaction } from "../../../../../lib/transaction.ts";
5221
+ import type { ${writePortName} } from "../ports/projections/${projectionFileBase(projection.name)}.projection-write.port.ts";
5222
+ ${payloadImports.join("\n")}
4042
5223
 
4043
5224
  // Auto-generated projector skeleton for the "${projection.readModelName}" read model.
4044
5225
  export class ${projectorClassName} {
4045
5226
  constructor(
4046
- private readonly ${repositoryPortVar}: ${repositoryPortName},
5227
+ private readonly ${writePortVar}: ${writePortName},
4047
5228
  ) {}
4048
5229
 
4049
5230
  ${methods}
@@ -4056,20 +5237,58 @@ function buildProjectionRebuildArtifact(modulePath, projection) {
4056
5237
  const projectorClassName = projection.name;
4057
5238
  const rebuildFunctionName = `rebuild${pascalCase(projection.name)}`;
4058
5239
  const batchSize = projection.rebuild.batchSize ?? 500;
4059
- return createGeneratedArtifact(`core/contexts/${modulePath}/application/projections/${fileBase}.rebuild.ts`, `import type { Transaction } from "../../../../lib/transaction.ts";
5240
+ const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionSourcePayloadTypeName(source)} } from "${projectionSourcePayloadImportPath(modulePath, source)}";`);
5241
+ return createGeneratedArtifact(`core/contexts/${modulePath}/application/projections/${fileBase}.rebuild.ts`, `import type { Transaction } from "../../../../../lib/transaction.ts";
4060
5242
  import type { ${projectorClassName} } from "./${fileBase}.projector.ts";
5243
+ ${payloadImports.join("\n")}
5244
+
5245
+ export type ProjectionRebuildBatch = {
5246
+ readonly events: Array<{
5247
+ readonly type: string;
5248
+ readonly payload?: unknown;
5249
+ }>;
5250
+ };
5251
+
5252
+ export interface ProjectionRebuildEventSource {
5253
+ stream(args: {
5254
+ projectionName: string;
5255
+ strategy: string;
5256
+ batchSize: number;
5257
+ tx: Transaction;
5258
+ }): AsyncIterable<ProjectionRebuildBatch>;
5259
+ }
4061
5260
 
4062
5261
  export type ${pascalCase(projection.name)}RebuildDeps = {
4063
5262
  projector: ${projectorClassName};
5263
+ eventSource: ProjectionRebuildEventSource;
4064
5264
  transaction: Transaction;
4065
5265
  };
4066
5266
 
4067
- // Auto-generated rebuild scaffold for the "${projection.readModelName}" read model.
5267
+ // Auto-generated rebuild runner for the "${projection.readModelName}" read model.
4068
5268
  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.
5269
+ const batchSize = ${batchSize};
5270
+
5271
+ for await (
5272
+ const batch of deps.eventSource.stream({
5273
+ projectionName: "${projection.name}",
5274
+ strategy: "${projection.rebuild.strategy}",
5275
+ batchSize,
5276
+ tx: deps.transaction,
5277
+ })
5278
+ ) {
5279
+ for (const event of batch.events) {
5280
+ switch (event.type) {
5281
+ ${projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => ` case "${source.contextName}.${source.aggregateName}.${source.eventName}":
5282
+ await deps.projector.${projectionSourceHandlerName(source.eventName)}(
5283
+ event.payload as ${projectionSourcePayloadTypeName(source)},
5284
+ deps.transaction,
5285
+ );
5286
+ break;`).join("\n")}
5287
+ default:
5288
+ break;
5289
+ }
5290
+ }
5291
+ }
4073
5292
  }
4074
5293
  `, contextArtifactOwnership(modulePath));
4075
5294
  }
@@ -4087,7 +5306,8 @@ function buildProjectionSubscriptionsArtifact(contextSpec) {
4087
5306
  const depsTypeName = `${pascalCase(contextSpec.context.name)}ProjectionSubscriptionDeps`;
4088
5307
  const builderName = `build${pascalCase(contextSpec.context.name)}ProjectionSubscriptions`;
4089
5308
  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";`);
5309
+ const importLines = contextSpec.projections.map((projection) => `import type { ${projection.name} } from "../../../core/contexts/${modulePath}/application/projections/${projectionFileBase(projection.name)}.projector.ts";`);
5310
+ const eventImports = contextSpec.projections.flatMap((projection) => projection.sources.map((source) => `import type { ${projectionSourcePayloadTypeName(source)} } from "../../../core/contexts/${projectionSourceModulePath(source.contextName)}/domain/events/${kebabCase(trimProjectionSourceEventName(source.eventName))}.event.ts";`));
4091
5311
  const subscriptionEntries = contextSpec.projections.flatMap((projection) => projection.sources.map((source) => {
4092
5312
  const subscriptionName = projection.subscription?.subscriptionName ?? projection.name;
4093
5313
  const consumerGroup = projection.subscription?.consumerGroup;
@@ -4097,11 +5317,13 @@ function buildProjectionSubscriptionsArtifact(contextSpec) {
4097
5317
  subscriptionName: "${subscriptionName}",
4098
5318
  eventName: "${source.contextName}.${source.aggregateName}.${source.eventName}",
4099
5319
  role: "${source.role}",${consumerGroupLine}
4100
- handle: (event, tx) => deps.${projectionVariableName(projection.name)}.${projectionSourceHandlerName(source.eventName)}(event, tx),
5320
+ handle: (event, tx) => deps.${projectionVariableName(projection.name)}.${projectionSourceHandlerName(source.eventName)}(event as ${projectionSourceEventType(source)}, tx),
4101
5321
  }`;
4102
5322
  }));
4103
- return createGeneratedArtifact(`infrastructure/messaging/projection-subscriptions/${modulePath}.ts`, `import type { Transaction } from "../../lib/transaction.ts";
5323
+ return createGeneratedArtifact(`infrastructure/messaging/projection-subscriptions/${modulePath}.ts`, `import type { EventEnvelope } from "../../../lib/event-envelope.ts";
5324
+ import type { Transaction } from "../../../lib/transaction.ts";
4104
5325
  ${importLines.join("\n")}
5326
+ ${[...new Set(eventImports)].join("\n")}
4105
5327
 
4106
5328
  export type ProjectionSubscription = {
4107
5329
  projectionName: string;
@@ -4109,7 +5331,7 @@ export type ProjectionSubscription = {
4109
5331
  eventName: string;
4110
5332
  role: "primary" | "enrichment";
4111
5333
  consumerGroup?: string;
4112
- handle: (event: unknown, tx: Transaction) => Promise<void>;
5334
+ handle: (event: EventEnvelope<unknown>, tx: Transaction) => Promise<void>;
4113
5335
  };
4114
5336
 
4115
5337
  export type ${depsTypeName} = {
@@ -4186,23 +5408,24 @@ function buildV5TestArtifacts(spec) {
4186
5408
  const failSupply = violating ? `{ ${violating.field}: "${violating.value}" }` : `{}`;
4187
5409
  const passSupply = passing ? `{ ${passing.field}: "${passing.value}" }` : violating ? `{ ${violating.field}: "active" }` : `{}`;
4188
5410
  const eventContextSetup = cmdHasEvents ? `\n const eventContext = createTestEventContext();` : "";
5411
+ const inputSetup = `\n const input = {} as Parameters<${aggPascal}Aggregate["${methodName}"]>[0];`;
4189
5412
  describes.push(`
4190
5413
  describe("${cmd.name} - ${inv.name} invariant", () => {
4191
5414
  it("rejects ${methodName} when ${inv.name} is violated", () => {
4192
5415
  const aggregate = ${aggPascal}Aggregate.fromPersistence(
4193
5416
  sampleValid(${aggPascal}RehydrationSchema, ${failSupply}),
4194
- );${eventContextSetup}
5417
+ );${inputSetup}${eventContextSetup}
4195
5418
  const result = aggregate.${methodName}(${methodCallArgs});
4196
- assert(!result.ok);
4197
- assert(result.error.type === "${errorTypeDiscriminator}");
5419
+ expect(result.ok).toBe(false);
5420
+ expect(result.error.type).toBe("${errorTypeDiscriminator}");
4198
5421
  });
4199
5422
 
4200
5423
  it("allows ${methodName} when ${inv.name} is satisfied", () => {
4201
5424
  const aggregate = ${aggPascal}Aggregate.fromPersistence(
4202
5425
  sampleValid(${aggPascal}RehydrationSchema, ${passSupply}),
4203
- );${eventContextSetup}
5426
+ );${inputSetup}${eventContextSetup}
4204
5427
  const result = aggregate.${methodName}(${methodCallArgs});
4205
- assert(result.ok);
5428
+ expect(result.ok).toBe(true);
4206
5429
  });
4207
5430
  });`);
4208
5431
  }
@@ -4212,20 +5435,21 @@ function buildV5TestArtifacts(spec) {
4212
5435
  const hasCustomIdType = idType && idType !== "string";
4213
5436
  if (createCommand) {
4214
5437
  let createIdExpr;
4215
- if (hasCustomIdType) {
4216
- const idTypePascal = pascalCase(idType);
4217
- extraImports.push(`import { create${idTypePascal} } from "../../../../../../shared-kernel/entity-ids/${kebabCase(idType)}.ts";`);
4218
- createIdExpr = `create${idTypePascal}(crypto.randomUUID())`;
4219
- } else {
4220
- extraImports.push(`import { createBrandedId } from "../../../../../../../lib/branded-id.ts";`);
4221
- createIdExpr = `createBrandedId("string", crypto.randomUUID())`;
4222
- }
5438
+ const idFactoryBase = hasCustomIdType ? idType : `${aggPascal}Id`;
5439
+ const idFactoryName = `create${pascalCase(idFactoryBase)}`;
5440
+ extraImports.push(`import { ${idFactoryName} } from "../../../../../../shared-kernel/entity-ids/${kebabCase(idFactoryBase)}.ts";`);
5441
+ createIdExpr = `${idFactoryName}(crypto.randomUUID())`;
4223
5442
  describes.push(`
4224
5443
  describe("version semantics", () => {
4225
5444
  it("should create with version 0", () => {
4226
5445
  const id = ${createIdExpr};
4227
- const aggregate = ${aggPascal}Aggregate.create(id, { ...input });
4228
- expect(aggregate.version).toBe(0);
5446
+ const props = {} as Parameters<typeof ${aggPascal}Aggregate.create>[1];
5447
+ const created = ${aggPascal}Aggregate.create(id, props);
5448
+ expect(created.ok).toBe(true);
5449
+ if (!created.ok) {
5450
+ throw new Error("expected aggregate creation to succeed");
5451
+ }
5452
+ expect(created.value.version).toBe(0);
4229
5453
  });
4230
5454
  });`);
4231
5455
  }
@@ -4246,13 +5470,14 @@ function buildV5TestArtifacts(spec) {
4246
5470
  break;
4247
5471
  }
4248
5472
  const eventContextSetup = cmdHasEvents ? `\n const eventContext = createTestEventContext();` : "";
5473
+ const inputSetup = `\n const input = {} as Parameters<${aggPascal}Aggregate["${methodName}"]>[0];`;
4249
5474
  describes.push(`
4250
5475
  describe("${cmd.name} - version semantics", () => {
4251
5476
  it("should increment version on successful ${methodName}", () => {
4252
5477
  const aggregate = ${aggPascal}Aggregate.fromPersistence(
4253
5478
  sampleValid(${aggPascal}RehydrationSchema, ${passSupply}),
4254
5479
  );
4255
- const initialVersion = aggregate.version;${eventContextSetup}
5480
+ const initialVersion = aggregate.version;${inputSetup}${eventContextSetup}
4256
5481
  const result = aggregate.${methodName}(${methodCallArgs});
4257
5482
  expect(result.ok).toBe(true);
4258
5483
  expect(aggregate.version).toBe(initialVersion + 1);
@@ -4262,7 +5487,7 @@ function buildV5TestArtifacts(spec) {
4262
5487
  const aggregate = ${aggPascal}Aggregate.fromPersistence(
4263
5488
  sampleValid(${aggPascal}RehydrationSchema, ${failSupply}),
4264
5489
  );
4265
- const initialVersion = aggregate.version;${eventContextSetup}
5490
+ const initialVersion = aggregate.version;${inputSetup}${eventContextSetup}
4266
5491
  const result = aggregate.${methodName}(${methodCallArgs});
4267
5492
  expect(result.ok).toBe(false);
4268
5493
  expect(aggregate.version).toBe(initialVersion);
@@ -4270,15 +5495,14 @@ function buildV5TestArtifacts(spec) {
4270
5495
  });`);
4271
5496
  }
4272
5497
  if (describes.length === 0) return artifacts;
4273
- const usesAssert = mutationCommandsWithPreconditions.length > 0;
4274
5498
  const usesSampleValid = mutationCommandsWithPreconditions.length > 0;
4275
- const usesInput = Boolean(createCommand);
4276
5499
  const testPath = `core/contexts/${modulePath}/domain/aggregates/${aggName}/__tests__/${aggName}.test.ts`;
4277
- const vitestImports = ["describe", "it"];
4278
- if (usesAssert) vitestImports.push("assert");
4279
- vitestImports.push("expect");
4280
5500
  const imports = [
4281
- `import { ${vitestImports.join(", ")} } from "vitest";`,
5501
+ `import { ${[
5502
+ "describe",
5503
+ "it",
5504
+ "expect"
5505
+ ].join(", ")} } from "vitest";`,
4282
5506
  ...usesSampleValid ? [`import type { z } from "zod";`] : [],
4283
5507
  ...usesSampleValid ? [`import { ${aggPascal}RehydrationSchema } from "../${aggName}.schema.ts";`] : [],
4284
5508
  `import { ${aggPascal}Aggregate } from "../${aggName}.aggregate.ts";`,
@@ -4290,11 +5514,7 @@ function sampleValid<T>(schema: z.ZodType<T>, overrides: Partial<T> = {}): T {
4290
5514
  void schema;
4291
5515
  return overrides as T;
4292
5516
  }` : "";
4293
- const inputStub = usesInput ? `
4294
-
4295
- // TODO: Provide a valid input object matching the command's input type
4296
- const input = {} as any;` : "";
4297
- const content = `${imports.join("\n")}${sampleValidHelper}${inputStub}
5517
+ const content = `${imports.join("\n")}${sampleValidHelper}
4298
5518
 
4299
5519
  describe("${aggPascal}Aggregate", () => {${describes.join("\n")}
4300
5520
  });
@@ -4320,7 +5540,7 @@ export class TestCommandBus implements CommandBusPort {
4320
5540
  type: TCommand["type"],
4321
5541
  handler: (command: TCommand, eventContext: EventContext) => Promise<Result<unknown, unknown>>,
4322
5542
  ): void {
4323
- this.handlers.set(type, handler as any);
5543
+ this.handlers.set(type, (command, eventContext) => handler(command as TCommand, eventContext));
4324
5544
  }
4325
5545
 
4326
5546
  async execute<TResult, TCommand extends CommandEnvelope>(
@@ -4385,6 +5605,34 @@ export class EventCollector {
4385
5605
  }
4386
5606
  `;
4387
5607
  }
5608
+ function generateAggregateEventTracker() {
5609
+ return `import type { EventEnvelope } from "../../../lib/event-envelope.ts";
5610
+ import { EventCollector } from "./event-collector.ts";
5611
+
5612
+ type EventSourceAggregate = {
5613
+ pullDomainEvents(): EventEnvelope<unknown>[];
5614
+ };
5615
+
5616
+ export class AggregateEventTracker {
5617
+ private readonly tracked = new Set<EventSourceAggregate>();
5618
+
5619
+ track(aggregate: EventSourceAggregate): void {
5620
+ this.tracked.add(aggregate);
5621
+ }
5622
+
5623
+ releaseInto(eventCollector: EventCollector): void {
5624
+ for (const aggregate of this.tracked) {
5625
+ eventCollector.collect(aggregate.pullDomainEvents());
5626
+ }
5627
+ this.tracked.clear();
5628
+ }
5629
+
5630
+ reset(): void {
5631
+ this.tracked.clear();
5632
+ }
5633
+ }
5634
+ `;
5635
+ }
4388
5636
  function generateDomainErrors() {
4389
5637
  return `export type { DomainError } from '../../../lib/domain-error.ts';
4390
5638
  export type { ApplicationError } from '../../../lib/application-error.ts';
@@ -4465,6 +5713,7 @@ function buildV5SharedKernelArtifacts(spec) {
4465
5713
  const base = [
4466
5714
  createGeneratedArtifact("core/shared-kernel/result.ts", generateResultReExport(), ownership),
4467
5715
  createGeneratedArtifact("core/shared-kernel/events/event-collector.ts", generateEventCollector(), ownership),
5716
+ createGeneratedArtifact("core/shared-kernel/events/aggregate-event-tracker.ts", generateAggregateEventTracker(), ownership),
4468
5717
  createGeneratedArtifact("core/shared-kernel/events/event-context.ts", generateEventContext(), ownership),
4469
5718
  createGeneratedArtifact("core/shared-kernel/errors/domain-errors.ts", generateDomainErrors(), ownership),
4470
5719
  createGeneratedArtifact("core/shared-kernel/trpc-error-mapper.ts", generateTrpcErrorMapper(), ownership),
@@ -4559,6 +5808,21 @@ function buildCommandPayloadCanonicalDefinition(command) {
4559
5808
  runtimeSchema: z.object(Object.fromEntries(fields.map((field) => [field.name, field.runtimeSchema])))
4560
5809
  };
4561
5810
  }
5811
+ function getQueryPayloadSchemaName(query) {
5812
+ return `${pascalCase(query.name)}QueryPayloadSchema`;
5813
+ }
5814
+ function getQueryPayloadFieldSpecsName(query) {
5815
+ return `${pascalCase(query.name)}QueryPayloadFieldSpecs`;
5816
+ }
5817
+ function buildQueryPayloadCanonicalDefinition(query) {
5818
+ const fields = query.inputFields.map((field) => buildCommandPayloadField(field));
5819
+ return {
5820
+ schemaName: getQueryPayloadSchemaName(query),
5821
+ fieldSpecsName: getQueryPayloadFieldSpecsName(query),
5822
+ fields,
5823
+ runtimeSchema: z.object(Object.fromEntries(fields.map((field) => [field.name, field.runtimeSchema])))
5824
+ };
5825
+ }
4562
5826
  function createCommandPayloadSchemaArtifactContent(definition) {
4563
5827
  const fieldSpecsLiteral = JSON.stringify(definition.fields.map((field) => ({
4564
5828
  name: field.name,
@@ -4594,28 +5858,36 @@ function createCommandProcedure(spec, procedure, command) {
4594
5858
  const commandTypeName = `${pascalCase(command.name)}Command`;
4595
5859
  const commandType = `${contextName}.${pascalCase(command.name)}`;
4596
5860
  const guardName = getCommandPayloadGuardName(command);
4597
- return ` async ${procedure.name}(input: any, ctx: any) {
5861
+ const payloadSchemaName = getCommandPayloadSchemaName(command);
5862
+ return ` ${procedure.name}: publicProcedure
5863
+ .input(${payloadSchemaName})
5864
+ .mutation(async ({ input, ctx }) => {
4598
5865
  ${guardName}(input);
4599
5866
  const command: ${commandTypeName} = { type: "${commandType}", payload: input };
4600
5867
  return await dispatchCommand(deps.commandBus, command, ctx);
4601
- }`;
5868
+ })`;
4602
5869
  }
4603
5870
  function createQueryProcedure(spec, procedure, query) {
4604
5871
  const queryTypeName = `${pascalCase(query.name)}Query`;
4605
- if (query.queryKind === "list") return ` async ${procedure.name}(input: any) {
5872
+ const payloadSchemaName = getQueryPayloadSchemaName(query);
5873
+ if (query.queryKind === "list") return ` ${procedure.name}: publicProcedure
5874
+ .input(${payloadSchemaName})
5875
+ .query(async ({ input }) => {
4606
5876
  const queryRequest: ${queryTypeName} = {
4607
5877
  type: "${pascalCase(spec.context.name)}.${pascalCase(query.name)}",
4608
5878
  payload: input,
4609
5879
  };
4610
5880
  return await deps.queryBus.execute(queryRequest);
4611
- }`;
4612
- return ` async ${procedure.name}(input: any) {
5881
+ })`;
5882
+ return ` ${procedure.name}: publicProcedure
5883
+ .input(${payloadSchemaName})
5884
+ .query(async ({ input }) => {
4613
5885
  const queryRequest: ${queryTypeName} = {
4614
5886
  type: "${pascalCase(spec.context.name)}.${pascalCase(query.name)}",
4615
5887
  payload: input,
4616
5888
  };
4617
5889
  return await deps.queryBus.execute(queryRequest);
4618
- }`;
5890
+ })`;
4619
5891
  }
4620
5892
  function buildProcedureBody(spec, procedure) {
4621
5893
  if (procedure.kind === "command") {
@@ -4632,7 +5904,8 @@ function collectImports(spec) {
4632
5904
  const imports = new Set([
4633
5905
  "import type { CommandBusPort } from \"../../../core/ports/messaging/command-bus.port.ts\";",
4634
5906
  "import type { QueryBusPort } from \"../../../core/ports/messaging/query-bus.port.ts\";",
4635
- "import { dispatchCommand } from \"../dispatch-command.ts\";"
5907
+ "import { dispatchCommand } from \"../dispatch-command.ts\";",
5908
+ "import { publicProcedure, router } from \"../trpc-init.ts\";"
4636
5909
  ]);
4637
5910
  for (const procedure of spec.presentation.procedures) {
4638
5911
  if (procedure.kind === "command") {
@@ -4642,12 +5915,14 @@ function collectImports(spec) {
4642
5915
  const commandFileName = kebabCase(command.name);
4643
5916
  imports.add(`import type { ${commandTypeName} } from "../../../core/contexts/${modulePath}/application/commands/${commandFileName}.command.ts";`);
4644
5917
  imports.add(`import { ${getCommandPayloadGuardName(command)} } from "../../../core/contexts/${modulePath}/application/commands/${commandFileName}.guards.ts";`);
5918
+ imports.add(`import { ${getCommandPayloadSchemaName(command)} } from "../../../core/contexts/${modulePath}/application/commands/${commandFileName}.schema.ts";`);
4645
5919
  continue;
4646
5920
  }
4647
5921
  const query = spec.queries.find((q) => q.name === procedure.operation);
4648
5922
  if (!query) continue;
4649
5923
  const queryFileName = kebabCase(query.name);
4650
5924
  imports.add(`import type { ${pascalCase(query.name)}Query } from "../../../core/contexts/${modulePath}/application/queries/${queryFileName}.query.ts";`);
5925
+ imports.add(`import { ${getQueryPayloadSchemaName(query)} } from "../../../core/contexts/${modulePath}/application/queries/${queryFileName}.schema.ts";`);
4651
5926
  }
4652
5927
  return [...imports].sort().join("\n");
4653
5928
  }
@@ -4660,9 +5935,9 @@ export function ${routerFactoryName}(deps: {
4660
5935
  commandBus: CommandBusPort;
4661
5936
  queryBus: QueryBusPort;
4662
5937
  }) {
4663
- return {
5938
+ return router({
4664
5939
  ${procedureBodies}
4665
- };
5940
+ });
4666
5941
  }
4667
5942
  `;
4668
5943
  }
@@ -4722,6 +5997,16 @@ export type TrpcContextRequirements = {
4722
5997
  };
4723
5998
  `, sharedArtifactOwnership("shared"));
4724
5999
  }
6000
+ function buildTrpcInitArtifact() {
6001
+ return createGeneratedArtifact("presentation/trpc/trpc-init.ts", `import { initTRPC } from "@trpc/server";
6002
+ import type { TrpcContextRequirements } from "./context-requirements.ts";
6003
+
6004
+ const t = initTRPC.context<TrpcContextRequirements>().create();
6005
+
6006
+ export const router = t.router;
6007
+ export const publicProcedure = t.procedure;
6008
+ `, sharedArtifactOwnership("shared"));
6009
+ }
4725
6010
  function buildV5PresentationArtifacts(spec) {
4726
6011
  if (spec.presentation.procedures.length === 0) return [];
4727
6012
  const logicalPath = buildArtifactPath("presentation/trpc/routers", "", `${kebabCase(spec.presentation.trpcRouter)}.router.ts`);
@@ -4730,11 +6015,17 @@ function buildV5PresentationArtifacts(spec) {
4730
6015
  const definition = buildCommandPayloadCanonicalDefinition(command);
4731
6016
  return [createGeneratedArtifact(buildArtifactPath("core/contexts", scopeKey, `application/commands/${kebabCase(command.name)}.schema.ts`), createCommandPayloadSchemaArtifactContent(definition), sliceArtifactOwnership(scopeKey)), createGeneratedArtifact(buildArtifactPath("core/contexts", scopeKey, `application/commands/${kebabCase(command.name)}.guards.ts`), createCommandPayloadGuardArtifactContent(command, definition), sliceArtifactOwnership(scopeKey))];
4732
6017
  });
6018
+ const querySupportArtifacts = spec.queries.map((query) => {
6019
+ const definition = buildQueryPayloadCanonicalDefinition(query);
6020
+ return createGeneratedArtifact(buildArtifactPath("core/contexts", scopeKey, `application/queries/${kebabCase(query.name)}.schema.ts`), createCommandPayloadSchemaArtifactContent(definition), sliceArtifactOwnership(scopeKey));
6021
+ });
4733
6022
  return [
4734
6023
  createGeneratedArtifact(logicalPath, createRouterStub(spec), sliceArtifactOwnership(scopeKey)),
4735
6024
  ...commandSupportArtifacts,
6025
+ ...querySupportArtifacts,
4736
6026
  buildDispatchCommandArtifact(),
4737
- buildContextRequirementsArtifact()
6027
+ buildContextRequirementsArtifact(),
6028
+ buildTrpcInitArtifact()
4738
6029
  ];
4739
6030
  }
4740
6031
 
@@ -4797,6 +6088,14 @@ export function fromPersistence<TBrand extends string>(
4797
6088
  ): BrandedId<TBrand> {
4798
6089
  return { value, _brand: brand } as BrandedId<TBrand>;
4799
6090
  }
6091
+
6092
+ export function brandedIdToString(
6093
+ value: BrandedId<string> | string,
6094
+ ): string {
6095
+ return typeof value === "object" && value !== null && "value" in value
6096
+ ? String(value.value)
6097
+ : String(value);
6098
+ }
4800
6099
  `;
4801
6100
  }
4802
6101
  function createEventEnvelope() {
@@ -4923,7 +6222,11 @@ function createApplicationError() {
4923
6222
  }
4924
6223
  `;
4925
6224
  }
4926
- function createTransaction() {
6225
+ function createTransaction(infrastructureStrategy) {
6226
+ if (infrastructureStrategy?.persistence === "mysql") return `import type { MySqlTransaction } from "drizzle-orm/mysql-core";
6227
+
6228
+ export type Transaction = MySqlTransaction<any, any, any, any>;
6229
+ `;
4927
6230
  return `import type { PgTransaction } from 'drizzle-orm/pg-core';
4928
6231
 
4929
6232
  export type Transaction = PgTransaction<any, any, any>;
@@ -5017,7 +6320,7 @@ export type ListResult<T> = {
5017
6320
  };
5018
6321
  `;
5019
6322
  }
5020
- function buildV5LibArtifacts() {
6323
+ function buildV5LibArtifacts(infrastructureStrategy) {
5021
6324
  const ownership = sharedArtifactOwnership("lib");
5022
6325
  return [
5023
6326
  createGeneratedArtifact("lib/branded-id.ts", createBrandedId(), ownership),
@@ -5029,7 +6332,7 @@ function buildV5LibArtifacts() {
5029
6332
  createGeneratedArtifact("lib/result.ts", createResult(), ownership),
5030
6333
  createGeneratedArtifact("lib/domain-error.ts", createDomainError(), ownership),
5031
6334
  createGeneratedArtifact("lib/application-error.ts", createApplicationError(), ownership),
5032
- createGeneratedArtifact("lib/transaction.ts", createTransaction(), ownership),
6335
+ createGeneratedArtifact("lib/transaction.ts", createTransaction(infrastructureStrategy), ownership),
5033
6336
  createGeneratedArtifact("lib/concurrency-conflict-error.ts", createConcurrencyConflictError(), ownership),
5034
6337
  createGeneratedArtifact("lib/pagination.ts", createPagination(), ownership)
5035
6338
  ];
@@ -5139,29 +6442,110 @@ function buildConsumerAclArtifacts(contextSpec) {
5139
6442
 
5140
6443
  //#endregion
5141
6444
  //#region packages/core/orchestrator.ts
5142
- function generateContextArtifacts(contextSpec) {
5143
- const perAggregateArtifacts = sliceContextIntoAggregateViews(contextSpec).flatMap((view) => {
5144
- const ownership = sliceArtifactOwnership(`${view.context.modulePath}/${kebabCase(view.aggregate.name)}`);
5145
- return [
5146
- ...applyOwnershipIfMissing(buildV5DomainArtifacts(view), ownership),
5147
- ...applyOwnershipIfMissing(buildV5ApplicationArtifacts(view, { skipContextWideArtifacts: true }), ownership),
5148
- ...buildV5InfrastructureArtifacts(view, { skipContextWideArtifacts: true }),
5149
- ...applyOwnershipIfMissing(buildV5TestArtifacts(view), ownership)
5150
- ];
5151
- });
5152
- const ctxOwnership = contextArtifactOwnership(contextSpec.context.modulePath);
5153
- const infraOwnership = sliceArtifactOwnership(contextSpec.context.modulePath);
5154
- const perContextArtifacts = [
5155
- ...buildV5LibArtifacts(),
5156
- ...buildV5SharedKernelArtifacts(contextSpec),
5157
- ...buildV5PortArtifacts(),
5158
- ...applyOwnershipIfMissing(buildV5ApplicationContextArtifacts(contextSpec), ctxOwnership),
5159
- ...applyOwnershipIfMissing(buildConsumerAclArtifacts(contextSpec), ctxOwnership),
5160
- ...applyOwnershipIfMissing(buildProjectionArtifacts(contextSpec), ctxOwnership),
5161
- ...applyOwnershipIfMissing(buildV5InfrastructureContextArtifacts(contextSpec), infraOwnership),
5162
- ...applyOwnershipIfMissing(buildV5PresentationArtifacts(contextSpec), ctxOwnership),
5163
- ...applyOwnershipIfMissing(buildV5RouteArtifacts(contextSpec), ctxOwnership)
5164
- ];
6445
+ function createContextGeneratorTarget(target) {
6446
+ return [target.id, target];
6447
+ }
6448
+ function aggregateSliceOwnership(view) {
6449
+ return sliceArtifactOwnership(`${view.context.modulePath}/${kebabCase(view.aggregate.name)}`);
6450
+ }
6451
+ function contextInfrastructureOwnership(contextSpec) {
6452
+ return sliceArtifactOwnership(contextSpec.context.modulePath);
6453
+ }
6454
+ function orderedTargets(registry, phase) {
6455
+ return [...registry.values()].filter((target) => target.phases.includes(phase)).sort((left, right) => left.order - right.order || left.id.localeCompare(right.id));
6456
+ }
6457
+ const DEFAULT_CONTEXT_GENERATOR_TARGET_REGISTRY = new Map([
6458
+ createContextGeneratorTarget({
6459
+ id: "domain",
6460
+ order: 10,
6461
+ phases: ["per-aggregate"],
6462
+ generateAggregate: (view) => applyOwnershipIfMissing(buildV5DomainArtifacts(view), aggregateSliceOwnership(view))
6463
+ }),
6464
+ createContextGeneratorTarget({
6465
+ id: "application",
6466
+ order: 20,
6467
+ phases: ["per-aggregate", "per-context"],
6468
+ generateAggregate: (view) => applyOwnershipIfMissing(buildV5ApplicationArtifacts(view, { skipContextWideArtifacts: true }), aggregateSliceOwnership(view)),
6469
+ generateContext: (contextSpec) => applyOwnershipIfMissing(buildV5ApplicationContextArtifacts(contextSpec), contextArtifactOwnership(contextSpec.context.modulePath))
6470
+ }),
6471
+ createContextGeneratorTarget({
6472
+ id: "infrastructure",
6473
+ order: 30,
6474
+ phases: ["per-aggregate", "per-context"],
6475
+ generateAggregate: (view, options) => buildV5InfrastructureArtifacts(view, {
6476
+ skipContextWideArtifacts: true,
6477
+ infrastructureStrategy: options.infrastructureStrategy
6478
+ }),
6479
+ generateContext: (contextSpec, options) => applyOwnershipIfMissing(buildV5InfrastructureContextArtifacts(contextSpec, { infrastructureStrategy: options.infrastructureStrategy }), contextInfrastructureOwnership(contextSpec))
6480
+ }),
6481
+ createContextGeneratorTarget({
6482
+ id: "tests",
6483
+ order: 40,
6484
+ phases: ["per-aggregate"],
6485
+ generateAggregate: (view) => applyOwnershipIfMissing(buildV5TestArtifacts(view), aggregateSliceOwnership(view))
6486
+ }),
6487
+ createContextGeneratorTarget({
6488
+ id: "lib",
6489
+ order: 50,
6490
+ phases: ["per-context"],
6491
+ generateContext: (_contextSpec, options) => buildV5LibArtifacts(options.infrastructureStrategy)
6492
+ }),
6493
+ createContextGeneratorTarget({
6494
+ id: "shared-kernel",
6495
+ order: 60,
6496
+ phases: ["per-context"],
6497
+ generateContext: (contextSpec) => buildV5SharedKernelArtifacts(contextSpec)
6498
+ }),
6499
+ createContextGeneratorTarget({
6500
+ id: "ports",
6501
+ order: 70,
6502
+ phases: ["per-context"],
6503
+ generateContext: () => buildV5PortArtifacts()
6504
+ }),
6505
+ createContextGeneratorTarget({
6506
+ id: "consumer-acls",
6507
+ order: 80,
6508
+ phases: ["per-context"],
6509
+ generateContext: (contextSpec) => applyOwnershipIfMissing(buildConsumerAclArtifacts(contextSpec), contextArtifactOwnership(contextSpec.context.modulePath))
6510
+ }),
6511
+ createContextGeneratorTarget({
6512
+ id: "projections",
6513
+ order: 90,
6514
+ phases: ["per-context"],
6515
+ generateContext: (contextSpec) => applyOwnershipIfMissing(buildProjectionArtifacts(contextSpec), contextArtifactOwnership(contextSpec.context.modulePath))
6516
+ }),
6517
+ createContextGeneratorTarget({
6518
+ id: "presentation",
6519
+ order: 100,
6520
+ phases: ["per-context"],
6521
+ generateContext: (contextSpec) => applyOwnershipIfMissing(buildV5PresentationArtifacts(contextSpec), contextArtifactOwnership(contextSpec.context.modulePath))
6522
+ }),
6523
+ createContextGeneratorTarget({
6524
+ id: "routes",
6525
+ order: 110,
6526
+ phases: ["per-context"],
6527
+ generateContext: (contextSpec) => applyOwnershipIfMissing(buildV5RouteArtifacts(contextSpec), contextArtifactOwnership(contextSpec.context.modulePath))
6528
+ })
6529
+ ]);
6530
+ function generateContextArtifacts(contextSpec, options = {}) {
6531
+ const generation = options.generation;
6532
+ let views = sliceContextIntoAggregateViews(contextSpec);
6533
+ if (generation?.include) {
6534
+ const includeSet = new Set(generation.include);
6535
+ views = views.filter((v) => includeSet.has(v.aggregate.name) || includeSet.has(kebabCase(v.aggregate.name)));
6536
+ }
6537
+ if (generation?.exclude) {
6538
+ const excludeSet = new Set(generation.exclude);
6539
+ views = views.filter((v) => !excludeSet.has(v.aggregate.name) && !excludeSet.has(kebabCase(v.aggregate.name)));
6540
+ }
6541
+ let registry = options.targetRegistry ?? DEFAULT_CONTEXT_GENERATOR_TARGET_REGISTRY;
6542
+ if (generation?.tests === false) {
6543
+ const filtered = new Map(registry);
6544
+ filtered.delete("tests");
6545
+ registry = filtered;
6546
+ }
6547
+ const perAggregateArtifacts = views.flatMap((view) => orderedTargets(registry, "per-aggregate").flatMap((target) => target.generateAggregate?.(view, options) ?? []));
6548
+ const perContextArtifacts = orderedTargets(registry, "per-context").flatMap((target) => target.generateContext?.(contextSpec, options) ?? []);
5165
6549
  return mergeGeneratedArtifacts([...perAggregateArtifacts, ...perContextArtifacts]).sort((a, b) => a.logicalPath.localeCompare(b.logicalPath));
5166
6550
  }
5167
6551
 
@@ -5765,6 +7149,7 @@ function createIntegrationEvent() {
5765
7149
 
5766
7150
  export interface IntegrationEvent<TPayload> {
5767
7151
  readonly type: string;
7152
+ readonly version: number;
5768
7153
  readonly payload: TPayload;
5769
7154
  readonly eventContext: EventContext;
5770
7155
  }
@@ -5775,28 +7160,66 @@ export type IntegrationEventHandler<TPayload> = (
5775
7160
 
5776
7161
  export type EventSubscription = {
5777
7162
  readonly eventType: string;
5778
- readonly handler: IntegrationEventHandler<unknown>;
7163
+ readonly handler: IntegrationEventHandler<any>;
5779
7164
  };
5780
7165
  `;
5781
7166
  }
5782
7167
  function createTransactionManager() {
5783
7168
  return `export interface TransactionManager {
5784
- withTransaction<T>(work: (tx: unknown) => Promise<T>): Promise<T>;
7169
+ withTransaction<TTx, T>(work: (tx: TTx) => Promise<T>): Promise<T>;
7170
+ flush(tx: unknown): Promise<void>;
5785
7171
  }
5786
7172
  `;
5787
7173
  }
5788
7174
  function createDrizzleTransactionManager() {
5789
7175
  return `import type { TransactionManager } from "./transaction-manager.ts";
5790
7176
 
7177
+ type DrizzleTransactionalDatabase = {
7178
+ transaction<T>(work: (tx: unknown) => Promise<T>): Promise<T>;
7179
+ };
7180
+
5791
7181
  export class DrizzleTransactionManager implements TransactionManager {
5792
- constructor(private readonly db: unknown) {}
7182
+ constructor(private readonly db: DrizzleTransactionalDatabase) {}
5793
7183
 
5794
- async withTransaction<T>(work: (tx: unknown) => Promise<T>): Promise<T> {
7184
+ async withTransaction<TTx, T>(work: (tx: TTx) => Promise<T>): Promise<T> {
5795
7185
  // Drizzle transaction wrapper
5796
- return (this.db as any).transaction(async (tx: unknown) => {
5797
- return work(tx);
7186
+ return this.db.transaction(async (tx: unknown) => {
7187
+ return work(tx as TTx);
7188
+ });
7189
+ }
7190
+
7191
+ async flush(_tx: unknown): Promise<void> {
7192
+ // Drizzle executes statements eagerly inside the transaction callback.
7193
+ }
7194
+ }
7195
+ `;
7196
+ }
7197
+ function createMikroOrmTransactionManager() {
7198
+ return `import type { TransactionManager } from "./transaction-manager.ts";
7199
+
7200
+ type MikroOrmTransactionalEntityManager = {
7201
+ transactional<T>(work: (tx: unknown) => Promise<T>): Promise<T>;
7202
+ };
7203
+
7204
+ export class MikroOrmTransactionManager implements TransactionManager {
7205
+ constructor(private readonly em: unknown) {}
7206
+
7207
+ async withTransaction<TTx, T>(work: (tx: TTx) => Promise<T>): Promise<T> {
7208
+ const transactionalEm = this.em as MikroOrmTransactionalEntityManager;
7209
+ return transactionalEm.transactional(async (tx: unknown) => {
7210
+ return work(tx as TTx);
5798
7211
  });
5799
7212
  }
7213
+
7214
+ async flush(tx: unknown): Promise<void> {
7215
+ const transactionalEm = tx as { flush?: () => Promise<void> };
7216
+ if (typeof transactionalEm.flush !== "function") {
7217
+ throw new Error(
7218
+ "[composition_types] MikroORM transaction context is missing flush()",
7219
+ );
7220
+ }
7221
+ await transactionalEm.flush();
7222
+ }
5800
7223
  }
5801
7224
  `;
5802
7225
  }
@@ -5810,11 +7233,13 @@ function createTransactionAbortError() {
5810
7233
  `;
5811
7234
  }
5812
7235
  function createHandlerDeps() {
5813
- return `import type { EventCollector } from "../../core/shared-kernel/events/event-collector.ts";
7236
+ return `import type { AggregateEventTracker } from "../../core/shared-kernel/events/aggregate-event-tracker.ts";
7237
+ import type { EventCollector } from "../../core/shared-kernel/events/event-collector.ts";
5814
7238
 
5815
7239
  export type HandlerDeps = {
5816
7240
  readonly tx: unknown;
5817
7241
  readonly eventCollector: EventCollector;
7242
+ readonly aggregateEventTracker: AggregateEventTracker;
5818
7243
  readonly repos: Record<string, unknown>;
5819
7244
  readonly acls: Record<string, unknown>;
5820
7245
  };
@@ -5826,6 +7251,7 @@ function buildCompositionTypeArtifacts() {
5826
7251
  createGeneratedArtifact("infrastructure/messaging/integration-event.ts", createIntegrationEvent(), ownership),
5827
7252
  createGeneratedArtifact("infrastructure/unit-of-work/transaction-manager.ts", createTransactionManager(), ownership),
5828
7253
  createGeneratedArtifact("infrastructure/unit-of-work/drizzle-transaction-manager.ts", createDrizzleTransactionManager(), ownership),
7254
+ createGeneratedArtifact("infrastructure/unit-of-work/mikroorm-transaction-manager.ts", createMikroOrmTransactionManager(), ownership),
5829
7255
  createGeneratedArtifact("infrastructure/unit-of-work/transaction-abort-error.ts", createTransactionAbortError(), ownership),
5830
7256
  createGeneratedArtifact("infrastructure/unit-of-work/handler-deps.ts", createHandlerDeps(), ownership)
5831
7257
  ].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
@@ -5838,6 +7264,7 @@ function createCommandBusImpl() {
5838
7264
  import type { TransactionManager } from "../unit-of-work/transaction-manager.ts";
5839
7265
  import { TransactionAbortError } from "../unit-of-work/transaction-abort-error.ts";
5840
7266
  import type { HandlerDeps } from "../unit-of-work/handler-deps.ts";
7267
+ import { AggregateEventTracker } from "../../core/shared-kernel/events/aggregate-event-tracker.ts";
5841
7268
  import { EventCollector } from "../../core/shared-kernel/events/event-collector.ts";
5842
7269
  import type { CommandEnvelope } from "../../core/shared-kernel/commands/command-envelope.ts";
5843
7270
  import type { EventContext } from "../../core/shared-kernel/events/event-context.ts";
@@ -5849,6 +7276,9 @@ import type { EventEnvelope } from "../../lib/event-envelope.ts";
5849
7276
  type OutboxWriter = {
5850
7277
  append(tx: unknown, events: EventEnvelope<unknown>[]): Promise<void>;
5851
7278
  };
7279
+ type OutboxRelay = {
7280
+ afterCommit(): void;
7281
+ };
5852
7282
  type RepositoryFactory = {
5853
7283
  create(tx: unknown): Record<string, unknown>;
5854
7284
  };
@@ -5868,6 +7298,8 @@ type InternalHandlerFn = (
5868
7298
  ) => Promise<Result<unknown, unknown>>;
5869
7299
 
5870
7300
  export class CommandBusImpl implements CommandBusPort {
7301
+ private relay: OutboxRelay | undefined;
7302
+
5871
7303
  constructor(
5872
7304
  private readonly txManager: TransactionManager,
5873
7305
  private readonly repoFactory: RepositoryFactory,
@@ -5875,6 +7307,10 @@ export class CommandBusImpl implements CommandBusPort {
5875
7307
  private readonly outboxWriter: OutboxWriter,
5876
7308
  ) {}
5877
7309
 
7310
+ setRelay(relay: OutboxRelay): void {
7311
+ this.relay = relay;
7312
+ }
7313
+
5878
7314
  // Internal handler type: receives deps injected by withUnitOfWork.
5879
7315
  // This is different from TestCommandBus which registers pre-bound
5880
7316
  // handlers with signature (command, eventContext) — deps closed over
@@ -5882,9 +7318,9 @@ export class CommandBusImpl implements CommandBusPort {
5882
7318
  // register() is NOT on CommandBusPort — it is implementation-specific.
5883
7319
  private handlers = new Map<string, RegisteredHandler>();
5884
7320
 
5885
- register<TCommand extends CommandEnvelope>(
7321
+ register<TCommand extends CommandEnvelope, TDeps extends HandlerDeps = HandlerDeps>(
5886
7322
  type: TCommand["type"],
5887
- handler: (command: TCommand, deps: HandlerDeps, eventContext: EventContext) => Promise<Result<unknown, unknown>>,
7323
+ handler: (command: TCommand, deps: TDeps, eventContext: EventContext) => Promise<Result<unknown, unknown>>,
5888
7324
  contextKey = this.resolveCommandContext(type),
5889
7325
  ): void {
5890
7326
  this.handlers.set(type, {
@@ -5902,9 +7338,11 @@ export class CommandBusImpl implements CommandBusPort {
5902
7338
  ): Promise<TResult> {
5903
7339
  const { handler, contextKey: commandContext } = this.resolveHandler(command.type);
5904
7340
  try {
5905
- return await this.withUnitOfWork(commandContext, (deps) =>
7341
+ const result = await this.withUnitOfWork(commandContext, (deps) =>
5906
7342
  handler(command, deps, eventContext)
5907
7343
  ) as TResult;
7344
+ this.relay?.afterCommit();
7345
+ return result;
5908
7346
  } catch (error) {
5909
7347
  if (error instanceof TransactionAbortError) {
5910
7348
  // Reconstitute the failure Result that triggered rollback.
@@ -5935,16 +7373,25 @@ export class CommandBusImpl implements CommandBusPort {
5935
7373
  work: (deps: HandlerDeps) => Promise<Result<T, unknown>>
5936
7374
  ): Promise<Result<T, unknown>> {
5937
7375
  return this.txManager.withTransaction(async (tx) => {
7376
+ const aggregateEventTracker = new AggregateEventTracker();
5938
7377
  const eventCollector = new EventCollector();
5939
7378
  const repos = this.repoFactory.create(tx);
5940
7379
  const acls = this.aclFactory.create(tx, commandContext);
5941
7380
 
5942
- const result = await work({ tx, eventCollector, repos, acls });
7381
+ const result = await work({
7382
+ tx,
7383
+ eventCollector,
7384
+ aggregateEventTracker,
7385
+ repos,
7386
+ acls,
7387
+ });
5943
7388
 
5944
7389
  if (!result.ok) {
5945
7390
  throw new TransactionAbortError(result.error);
5946
7391
  }
5947
7392
 
7393
+ await this.txManager.flush(tx);
7394
+ aggregateEventTracker.releaseInto(eventCollector);
5948
7395
  const events = eventCollector.drain();
5949
7396
 
5950
7397
  if (events.length > 0) {
@@ -5989,38 +7436,70 @@ function buildCompositionBusArtifacts() {
5989
7436
 
5990
7437
  //#endregion
5991
7438
  //#region packages/core/generators/composition_outbox.ts
5992
- function createOutboxTable() {
5993
- return `import { pgTable, uuid, varchar, jsonb, timestamp } from "drizzle-orm/pg-core";
7439
+ function createOutboxTable(infrastructureStrategy) {
7440
+ if (infrastructureStrategy?.persistence === "mysql") return `import { sql } from "drizzle-orm";
7441
+ import { index, int, json, mysqlTable, timestamp, varchar } from "drizzle-orm/mysql-core";
7442
+
7443
+ export const outboxEvents = mysqlTable("outbox_events", {
7444
+ id: varchar("id", { length: 36 }).primaryKey().default(sql\`(uuid())\`),
7445
+ eventType: varchar("event_type", { length: 255 }).notNull(),
7446
+ eventVersion: int("event_version").notNull(),
7447
+ payload: json("payload").notNull(),
7448
+ metadata: json("metadata").notNull(),
7449
+ occurredAt: timestamp("occurred_at").notNull().defaultNow(),
7450
+ status: varchar("status", { length: 50 }).notNull().default("pending"),
7451
+ attempts: int("attempts").notNull().default(0),
7452
+ processedAt: timestamp("processed_at"),
7453
+ failedAt: timestamp("failed_at"),
7454
+ error: json("error"),
7455
+ }, (table) => ({
7456
+ statusOccurredAtIdx: index("outbox_status_occurred_at_idx").on(table.status, table.occurredAt),
7457
+ }));
7458
+ `;
7459
+ return `import { index, pgTable, uuid, varchar, jsonb, timestamp, integer } from "drizzle-orm/pg-core";
5994
7460
 
5995
7461
  export const outboxEvents = pgTable("outbox_events", {
5996
7462
  id: uuid("id").primaryKey().defaultRandom(),
5997
7463
  eventType: varchar("event_type", { length: 255 }).notNull(),
7464
+ eventVersion: integer("event_version").notNull(),
5998
7465
  payload: jsonb("payload").notNull(),
5999
7466
  metadata: jsonb("metadata").notNull(),
6000
7467
  occurredAt: timestamp("occurred_at").notNull().defaultNow(),
6001
7468
  status: varchar("status", { length: 50 }).notNull().default("pending"),
6002
- });
7469
+ attempts: integer("attempts").notNull().default(0),
7470
+ processedAt: timestamp("processed_at"),
7471
+ failedAt: timestamp("failed_at"),
7472
+ error: jsonb("error"),
7473
+ }, (table) => ({
7474
+ statusOccurredAtIdx: index("outbox_status_occurred_at_idx").on(table.status, table.occurredAt),
7475
+ }));
6003
7476
  `;
6004
7477
  }
6005
7478
  function createOutboxWriter() {
6006
7479
  return `import { outboxEvents } from "./outbox.table.ts";
6007
7480
  import type { EventEnvelope } from "../../lib/event-envelope.ts";
6008
7481
 
7482
+ type OutboxInsertExecutor = {
7483
+ insert(table: typeof outboxEvents): {
7484
+ values(row: Record<string, unknown>): Promise<unknown>;
7485
+ };
7486
+ };
7487
+
6009
7488
  export class OutboxWriter {
6010
- async append(tx: unknown, events: EventEnvelope<unknown>[]): Promise<void> {
7489
+ async append(tx: OutboxInsertExecutor, events: EventEnvelope<unknown>[]): Promise<void> {
6011
7490
  if (events.length === 0) return;
6012
7491
 
6013
- const db = tx as any;
6014
7492
  for (const event of events) {
6015
- await db.insert(outboxEvents).values({
7493
+ await tx.insert(outboxEvents).values({
6016
7494
  eventType: event.eventType,
6017
- payload: event.payload,
6018
- metadata: {
7495
+ eventVersion: event.eventVersion,
7496
+ payload: JSON.stringify(event.payload),
7497
+ metadata: JSON.stringify({
6019
7498
  correlationId: event.correlationId,
6020
7499
  causationId: event.causationId,
6021
7500
  recordedBy: event.recordedBy,
6022
7501
  occurredAt: event.occurredAt,
6023
- },
7502
+ }),
6024
7503
  occurredAt: event.occurredAt,
6025
7504
  });
6026
7505
  }
@@ -6028,9 +7507,473 @@ export class OutboxWriter {
6028
7507
  }
6029
7508
  `;
6030
7509
  }
6031
- function buildCompositionOutboxArtifacts() {
7510
+ function createMikroOrmOutboxWriter(infrastructureStrategy) {
7511
+ return `import type { EventEnvelope } from "../../lib/event-envelope.ts";
7512
+
7513
+ type SqlConnection = {
7514
+ execute(sql: string, params?: unknown[]): Promise<unknown>;
7515
+ };
7516
+
7517
+ type MikroOrmOutboxExecutor = {
7518
+ getConnection(): SqlConnection;
7519
+ };
7520
+
7521
+ export class OutboxWriter {
7522
+ async append(tx: MikroOrmOutboxExecutor, events: EventEnvelope<unknown>[]): Promise<void> {
7523
+ if (events.length === 0) return;
7524
+
7525
+ const connection = tx.getConnection();
7526
+
7527
+ for (const event of events) {
7528
+ await connection.execute(
7529
+ "insert into outbox_events (id, event_type, event_version, payload, metadata, occurred_at, status, attempts) values (${infrastructureStrategy?.persistence === "mysql" ? "uuid()" : "gen_random_uuid()"}, ?, ?, ?, ?, ?, ?, ?)",
7530
+ [
7531
+ event.eventType,
7532
+ event.eventVersion,
7533
+ JSON.stringify(event.payload),
7534
+ JSON.stringify({
7535
+ correlationId: event.correlationId,
7536
+ causationId: event.causationId,
7537
+ recordedBy: event.recordedBy,
7538
+ occurredAt: event.occurredAt,
7539
+ }),
7540
+ event.occurredAt,
7541
+ "pending",
7542
+ 0,
7543
+ ],
7544
+ );
7545
+ }
7546
+ }
7547
+ }
7548
+ `;
7549
+ }
7550
+ function createOutboxDispatcher() {
7551
+ return `import { asc, eq } from "drizzle-orm";
7552
+ import { outboxEvents } from "./outbox.table.ts";
7553
+ import { createPersonnelId } from "../../core/shared-kernel/entity-ids/personnel-id.ts";
7554
+ import { createCorrelationId } from "../../core/shared-kernel/ids/correlation-id.ts";
7555
+ import { createEventId } from "../../core/shared-kernel/ids/event-id.ts";
7556
+ import type {
7557
+ EventSubscription,
7558
+ IntegrationEvent,
7559
+ } from "../messaging/integration-event.ts";
7560
+
7561
+ export type OutboxDispatcherConfig = {
7562
+ readonly maxAttempts?: number;
7563
+ readonly batchSize?: number;
7564
+ };
7565
+
7566
+ type OutboxRow = typeof outboxEvents.$inferSelect;
7567
+
7568
+ type OutboxMetadata = {
7569
+ correlationId: string;
7570
+ causationId: string;
7571
+ recordedBy: string;
7572
+ occurredAt: string;
7573
+ };
7574
+
7575
+ type HandlerError = {
7576
+ handler: string;
7577
+ error: string;
7578
+ };
7579
+
7580
+ type OutboxQueryExecutor = {
7581
+ select(): {
7582
+ from(table: typeof outboxEvents): {
7583
+ where(condition: unknown): {
7584
+ orderBy(ordering: unknown): {
7585
+ limit(count: number): Promise<OutboxRow[]>;
7586
+ };
7587
+ };
7588
+ };
7589
+ };
7590
+ update(table: typeof outboxEvents): {
7591
+ set(values: Record<string, unknown>): {
7592
+ where(condition: unknown): Promise<unknown>;
7593
+ };
7594
+ };
7595
+ };
7596
+
7597
+ function requiredMetadataValue(
7598
+ metadata: Partial<OutboxMetadata>,
7599
+ key: keyof OutboxMetadata,
7600
+ eventType: string,
7601
+ ): string {
7602
+ const value = metadata[key];
7603
+ if (typeof value !== "string" || value.length === 0) {
7604
+ throw new Error(\`Outbox event \${eventType} is missing metadata field: \${key}\`);
7605
+ }
7606
+ return value;
7607
+ }
7608
+
7609
+ export class OutboxDispatcher {
7610
+ private readonly maxAttempts: number;
7611
+ private readonly batchSize: number;
7612
+ private readonly handlersByEventType: Map<string, EventSubscription[]>;
7613
+
7614
+ constructor(
7615
+ private readonly db: OutboxQueryExecutor,
7616
+ subscriptions: EventSubscription[],
7617
+ config?: OutboxDispatcherConfig,
7618
+ ) {
7619
+ this.maxAttempts = config?.maxAttempts ?? 5;
7620
+ this.batchSize = config?.batchSize ?? 100;
7621
+
7622
+ const index = new Map<string, EventSubscription[]>();
7623
+ for (const subscription of subscriptions) {
7624
+ const existing = index.get(subscription.eventType) ?? [];
7625
+ existing.push(subscription);
7626
+ index.set(subscription.eventType, existing);
7627
+ }
7628
+ this.handlersByEventType = index;
7629
+ }
7630
+
7631
+ async dispatchPending(): Promise<{ processed: number; failed: number }> {
7632
+ let processed = 0;
7633
+ let failed = 0;
7634
+
7635
+ const rows: OutboxRow[] = await this.db
7636
+ .select()
7637
+ .from(outboxEvents)
7638
+ .where(eq(outboxEvents.status, "pending"))
7639
+ .orderBy(asc(outboxEvents.occurredAt))
7640
+ .limit(this.batchSize);
7641
+
7642
+ for (const row of rows) {
7643
+ const ok = await this.dispatchRow(row);
7644
+ if (ok) {
7645
+ processed += 1;
7646
+ } else {
7647
+ failed += 1;
7648
+ }
7649
+ }
7650
+
7651
+ return { processed, failed };
7652
+ }
7653
+
7654
+ private async dispatchRow(row: OutboxRow): Promise<boolean> {
7655
+ const handlers = this.handlersByEventType.get(row.eventType);
7656
+ if (!handlers || handlers.length === 0) {
7657
+ await this.updateStatus(row.id, "processed", { processedAt: new Date() });
7658
+ return true;
7659
+ }
7660
+
7661
+ const metadata = (row.metadata ?? {}) as Partial<OutboxMetadata>;
7662
+ const event: IntegrationEvent<unknown> = {
7663
+ type: row.eventType,
7664
+ version: row.eventVersion,
7665
+ payload: row.payload,
7666
+ eventContext: {
7667
+ correlationId: createCorrelationId(requiredMetadataValue(metadata, "correlationId", row.eventType)),
7668
+ causationId: createEventId(requiredMetadataValue(metadata, "causationId", row.eventType)),
7669
+ recordedBy: createPersonnelId(requiredMetadataValue(metadata, "recordedBy", row.eventType)),
7670
+ },
7671
+ };
7672
+
7673
+ const errors: HandlerError[] = [];
7674
+ for (const subscription of handlers) {
7675
+ try {
7676
+ await subscription.handler(event);
7677
+ } catch (error) {
7678
+ errors.push({
7679
+ handler: subscription.handler.name || subscription.eventType,
7680
+ error: error instanceof Error ? error.message : String(error),
7681
+ });
7682
+ }
7683
+ }
7684
+
7685
+ if (errors.length === 0) {
7686
+ await this.updateStatus(row.id, "processed", {
7687
+ processedAt: new Date(),
7688
+ error: null,
7689
+ });
7690
+ return true;
7691
+ }
7692
+
7693
+ const nextAttempts = (row.attempts ?? 0) + 1;
7694
+ if (nextAttempts >= this.maxAttempts) {
7695
+ await this.updateStatus(row.id, "failed", {
7696
+ attempts: nextAttempts,
7697
+ failedAt: new Date(),
7698
+ error: errors,
7699
+ });
7700
+ } else {
7701
+ await this.updateStatus(row.id, "pending", {
7702
+ attempts: nextAttempts,
7703
+ error: errors,
7704
+ });
7705
+ }
7706
+
7707
+ return false;
7708
+ }
7709
+
7710
+ private async updateStatus(
7711
+ id: string,
7712
+ status: "pending" | "processed" | "failed",
7713
+ fields: Record<string, unknown>,
7714
+ ): Promise<void> {
7715
+ await this.db
7716
+ .update(outboxEvents)
7717
+ .set({ status, ...fields })
7718
+ .where(eq(outboxEvents.id, id));
7719
+ }
7720
+ }
7721
+ `;
7722
+ }
7723
+ function createMikroOrmOutboxDispatcher() {
7724
+ return `import { createPersonnelId } from "../../core/shared-kernel/entity-ids/personnel-id.ts";
7725
+ import { createCorrelationId } from "../../core/shared-kernel/ids/correlation-id.ts";
7726
+ import { createEventId } from "../../core/shared-kernel/ids/event-id.ts";
7727
+ import type {
7728
+ EventSubscription,
7729
+ IntegrationEvent,
7730
+ } from "../messaging/integration-event.ts";
7731
+
7732
+ export type OutboxDispatcherConfig = {
7733
+ readonly maxAttempts?: number;
7734
+ readonly batchSize?: number;
7735
+ };
7736
+
7737
+ type OutboxRow = {
7738
+ id: string;
7739
+ event_type: string;
7740
+ event_version: number;
7741
+ payload: unknown;
7742
+ metadata: unknown;
7743
+ occurred_at: string | Date;
7744
+ status: string;
7745
+ attempts: number | null;
7746
+ processed_at: string | Date | null;
7747
+ failed_at: string | Date | null;
7748
+ error: unknown;
7749
+ };
7750
+
7751
+ type OutboxMetadata = {
7752
+ correlationId: string;
7753
+ causationId: string;
7754
+ recordedBy: string;
7755
+ occurredAt: string;
7756
+ };
7757
+
7758
+ type HandlerError = {
7759
+ handler: string;
7760
+ error: string;
7761
+ };
7762
+
7763
+ type SqlConnection = {
7764
+ execute<T = unknown>(sql: string, params?: unknown[]): Promise<T>;
7765
+ };
7766
+
7767
+ type MikroOrmOutboxExecutor = {
7768
+ getConnection(): SqlConnection;
7769
+ };
7770
+
7771
+ function requiredMetadataValue(
7772
+ metadata: Partial<OutboxMetadata>,
7773
+ key: keyof OutboxMetadata,
7774
+ eventType: string,
7775
+ ): string {
7776
+ const value = metadata[key];
7777
+ if (typeof value !== "string" || value.length === 0) {
7778
+ throw new Error(\`Outbox event \${eventType} is missing metadata field: \${key}\`);
7779
+ }
7780
+ return value;
7781
+ }
7782
+
7783
+ export class OutboxDispatcher {
7784
+ private readonly maxAttempts: number;
7785
+ private readonly batchSize: number;
7786
+ private readonly handlersByEventType: Map<string, EventSubscription[]>;
7787
+
7788
+ constructor(
7789
+ private readonly db: unknown,
7790
+ subscriptions: EventSubscription[],
7791
+ config?: OutboxDispatcherConfig,
7792
+ ) {
7793
+ this.maxAttempts = config?.maxAttempts ?? 5;
7794
+ this.batchSize = config?.batchSize ?? 100;
7795
+
7796
+ const index = new Map<string, EventSubscription[]>();
7797
+ for (const subscription of subscriptions) {
7798
+ const existing = index.get(subscription.eventType) ?? [];
7799
+ existing.push(subscription);
7800
+ index.set(subscription.eventType, existing);
7801
+ }
7802
+ this.handlersByEventType = index;
7803
+ }
7804
+
7805
+ async dispatchPending(): Promise<{ processed: number; failed: number }> {
7806
+ let processed = 0;
7807
+ let failed = 0;
7808
+
7809
+ const connection = (this.db as MikroOrmOutboxExecutor).getConnection();
7810
+ const rows = await connection.execute<OutboxRow[]>(
7811
+ "select id, event_type, event_version, payload, metadata, occurred_at, status, attempts, processed_at, failed_at, error from outbox_events where status = ? order by occurred_at asc limit ?",
7812
+ ["pending", this.batchSize],
7813
+ );
7814
+
7815
+ for (const row of rows) {
7816
+ const ok = await this.dispatchRow(row);
7817
+ if (ok) {
7818
+ processed += 1;
7819
+ } else {
7820
+ failed += 1;
7821
+ }
7822
+ }
7823
+
7824
+ return { processed, failed };
7825
+ }
7826
+
7827
+ private async dispatchRow(row: OutboxRow): Promise<boolean> {
7828
+ const handlers = this.handlersByEventType.get(row.event_type);
7829
+ if (!handlers || handlers.length === 0) {
7830
+ await this.updateStatus(row.id, "processed", { processedAt: new Date() });
7831
+ return true;
7832
+ }
7833
+
7834
+ const metadata = ((row.metadata ?? {}) as Partial<OutboxMetadata>);
7835
+ const event: IntegrationEvent<unknown> = {
7836
+ type: row.event_type,
7837
+ version: row.event_version,
7838
+ payload: row.payload,
7839
+ eventContext: {
7840
+ correlationId: createCorrelationId(requiredMetadataValue(metadata, "correlationId", row.event_type)),
7841
+ causationId: createEventId(requiredMetadataValue(metadata, "causationId", row.event_type)),
7842
+ recordedBy: createPersonnelId(requiredMetadataValue(metadata, "recordedBy", row.event_type)),
7843
+ },
7844
+ };
7845
+
7846
+ const errors: HandlerError[] = [];
7847
+ for (const subscription of handlers) {
7848
+ try {
7849
+ await subscription.handler(event);
7850
+ } catch (error) {
7851
+ errors.push({
7852
+ handler: subscription.handler.name || subscription.eventType,
7853
+ error: error instanceof Error ? error.message : String(error),
7854
+ });
7855
+ }
7856
+ }
7857
+
7858
+ if (errors.length === 0) {
7859
+ await this.updateStatus(row.id, "processed", {
7860
+ processedAt: new Date(),
7861
+ error: null,
7862
+ });
7863
+ return true;
7864
+ }
7865
+
7866
+ const nextAttempts = (row.attempts ?? 0) + 1;
7867
+ if (nextAttempts >= this.maxAttempts) {
7868
+ await this.updateStatus(row.id, "failed", {
7869
+ attempts: nextAttempts,
7870
+ failedAt: new Date(),
7871
+ error: errors,
7872
+ });
7873
+ } else {
7874
+ await this.updateStatus(row.id, "pending", {
7875
+ attempts: nextAttempts,
7876
+ error: errors,
7877
+ });
7878
+ }
7879
+
7880
+ return false;
7881
+ }
7882
+
7883
+ private async updateStatus(
7884
+ id: string,
7885
+ status: "pending" | "processed" | "failed",
7886
+ fields: Record<string, unknown>,
7887
+ ): Promise<void> {
7888
+ const connection = (this.db as MikroOrmOutboxExecutor).getConnection();
7889
+ await connection.execute(
7890
+ "update outbox_events set status = ?, attempts = ?, processed_at = ?, failed_at = ?, error = ? where id = ?",
7891
+ [
7892
+ status,
7893
+ fields.attempts ?? 0,
7894
+ fields.processedAt ?? null,
7895
+ fields.failedAt ?? null,
7896
+ fields.error ?? null,
7897
+ id,
7898
+ ],
7899
+ );
7900
+ }
7901
+ }
7902
+ `;
7903
+ }
7904
+ function createOutboxRelay() {
7905
+ return `import type { OutboxDispatcher } from "./outbox-dispatcher.ts";
7906
+
7907
+ export class OutboxRelay {
7908
+ constructor(private readonly dispatcher: OutboxDispatcher) {}
7909
+
7910
+ afterCommit(): void {
7911
+ void this.dispatcher.dispatchPending().catch(() => {
7912
+ // Swallowed - the poller retries on the next cycle.
7913
+ });
7914
+ }
7915
+ }
7916
+ `;
7917
+ }
7918
+ function createOutboxPoller() {
7919
+ return `import type { OutboxDispatcher } from "./outbox-dispatcher.ts";
7920
+
7921
+ export type OutboxPollerConfig = {
7922
+ readonly intervalMs?: number;
7923
+ };
7924
+
7925
+ export class OutboxPoller {
7926
+ private handle: ReturnType<typeof setInterval> | null = null;
7927
+ private polling = false;
7928
+ private readonly intervalMs: number;
7929
+
7930
+ constructor(
7931
+ private readonly dispatcher: OutboxDispatcher,
7932
+ config?: OutboxPollerConfig,
7933
+ ) {
7934
+ this.intervalMs = config?.intervalMs ?? 5_000;
7935
+ }
7936
+
7937
+ start(): void {
7938
+ if (this.handle) return;
7939
+ void this.poll();
7940
+ this.handle = setInterval(() => void this.poll(), this.intervalMs);
7941
+ }
7942
+
7943
+ stop(): void {
7944
+ if (!this.handle) return;
7945
+ clearInterval(this.handle);
7946
+ this.handle = null;
7947
+ }
7948
+
7949
+ get running(): boolean {
7950
+ return this.handle !== null;
7951
+ }
7952
+
7953
+ private async poll(): Promise<void> {
7954
+ if (this.polling) return;
7955
+ this.polling = true;
7956
+ try {
7957
+ await this.dispatcher.dispatchPending();
7958
+ } catch {
7959
+ // Swallowed - the next cycle retries.
7960
+ } finally {
7961
+ this.polling = false;
7962
+ }
7963
+ }
7964
+ }
7965
+ `;
7966
+ }
7967
+ function buildCompositionOutboxArtifacts(infrastructureStrategy) {
6032
7968
  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));
7969
+ const usesMikroOrm = infrastructureStrategy?.orm === "mikroorm";
7970
+ return [
7971
+ createGeneratedArtifact("infrastructure/outbox/outbox-dispatcher.ts", usesMikroOrm ? createMikroOrmOutboxDispatcher() : createOutboxDispatcher(), ownership),
7972
+ createGeneratedArtifact("infrastructure/outbox/outbox-poller.ts", createOutboxPoller(), ownership),
7973
+ createGeneratedArtifact("infrastructure/outbox/outbox-relay.ts", createOutboxRelay(), ownership),
7974
+ createGeneratedArtifact("infrastructure/outbox/outbox.table.ts", createOutboxTable(infrastructureStrategy), ownership),
7975
+ createGeneratedArtifact("infrastructure/outbox/outbox-writer.ts", usesMikroOrm ? createMikroOrmOutboxWriter(infrastructureStrategy) : createOutboxWriter(), ownership)
7976
+ ].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
6034
7977
  }
6035
7978
 
6036
7979
  //#endregion
@@ -6118,38 +8061,53 @@ function renderMappedType(fields) {
6118
8061
  ${Object.entries(fields).map(([fieldName, expr]) => ` readonly ${camelCase(fieldName)}: ${mappingValueType(expr)};`).join("\n")}
6119
8062
  }`;
6120
8063
  }
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`;
8064
+ function mapReadAclFieldTypeToTS(type) {
8065
+ switch (type) {
8066
+ case "string": return "string";
8067
+ case "number": return "number";
8068
+ case "boolean": return "boolean";
8069
+ case "date": return "string";
8070
+ case "enum": return "string";
8071
+ case "union": return "string | number | boolean | null";
8072
+ case "record": return "Record<string, unknown>";
8073
+ case "tuple": return "[unknown, ...unknown[]]";
8074
+ case "intersection": return "Record<string, unknown>";
8075
+ default: return "unknown";
8076
+ }
6126
8077
  }
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`;
8078
+ function formatReadAclFieldType(field, indent = 0) {
8079
+ if (field.type === "object" && field.nestedFields && field.nestedFields.length > 0) {
8080
+ const objectType = `{\n${field.nestedFields.map((nestedField) => {
8081
+ const opt = nestedField.optional ? "?" : "";
8082
+ return `${" ".repeat(indent + 1)}readonly ${camelCase(nestedField.name)}${opt}: ${formatReadAclFieldType(nestedField, indent + 1)};`;
8083
+ }).join("\n")}\n${" ".repeat(indent)}}`;
8084
+ return field.array ? `Array<${objectType}>` : objectType;
8085
+ }
8086
+ const base = mapReadAclFieldTypeToTS(field.type);
8087
+ return field.array ? `${base}[]` : base;
8088
+ }
8089
+ function formatReadAclListItemType(outputFields) {
8090
+ return `Array<{\n${outputFields.map((field) => {
8091
+ const opt = field.optional ? "?" : "";
8092
+ return ` readonly ${camelCase(field.name)}${opt}: ${formatReadAclFieldType(field, 1)};`;
8093
+ }).join("\n")}\n}>`;
6131
8094
  }
6132
8095
  function readResponseFieldType(acl, mapping) {
6133
8096
  if (mapping.kind === "const") return mappingValueType({ const: mapping.value });
6134
- const viewTypeName = sourceQueryViewTypeName(acl);
6135
8097
  if (acl.sourceQuery.queryKind === "list") {
6136
- if (mapping.sourcePath === "result.items") return `${viewTypeName}[]`;
8098
+ if (mapping.sourcePath === "result.items") return formatReadAclListItemType(acl.sourceQuery.outputFields);
6137
8099
  if (mapping.sourcePath === "result.total" || mapping.sourcePath === "result.page" || mapping.sourcePath === "result.pageSize") return "number";
6138
8100
  }
6139
8101
  const sourceFieldName = mapping.sourcePath.replace(/^result\./, "");
6140
8102
  const sourceField = acl.sourceQuery.outputFields.find((field) => camelCase(field.name) === camelCase(sourceFieldName));
6141
- return sourceField ? `${viewTypeName}[${JSON.stringify(sourceField.name)}]` : "unknown";
8103
+ return sourceField ? formatReadAclFieldType(sourceField) : "unknown";
6142
8104
  }
6143
8105
  function buildReadAclContractContent(acl) {
6144
8106
  normalizeModulePath(acl.consumerContext.modulePath);
6145
- const sourceModulePath = normalizeModulePath(acl.sourceContext.modulePath);
6146
8107
  const requestTypeName = aclRequestTypeName(acl.port);
6147
8108
  const responseTypeName = aclResponseTypeName(acl.port);
6148
- const viewTypeName = sourceQueryViewTypeName(acl);
6149
- const viewFileName = sourceQueryViewFileName(acl);
6150
8109
  const responseLines = acl.responseMappings.map((mapping) => ` readonly ${camelCase(mapping.targetPath)}: ${readResponseFieldType(acl, mapping)};`);
6151
8110
  return `import type { Transaction } from "../../../../../lib/transaction.ts";
6152
- import type { ${viewTypeName} } from "../../../${sourceModulePath}/application/contracts/${viewFileName}";
6153
8111
 
6154
8112
  export type ${requestTypeName} = ${renderMappedType(Object.fromEntries(acl.requestMappings.map((mapping) => [mapping.targetPath, mapping.kind === "from" ? { from: mapping.sourcePath } : { const: mapping.value }])))};
6155
8113
 
@@ -6194,20 +8152,51 @@ function buildAclContractContent(acl) {
6194
8152
  }
6195
8153
  function buildReadRequestAssignmentLines(acl) {
6196
8154
  const sourceInputFields = acl.sourceQuery.inputFields ?? [];
6197
- return acl.requestMappings.map((mapping) => {
8155
+ const directAssignments = [];
8156
+ const filterAssignments = [];
8157
+ for (const mapping of acl.requestMappings) {
6198
8158
  const valueExpr = mapping.kind === "from" ? `input.${mapping.sourcePath.replace(/^input\./, "")}` : JSON.stringify(mapping.value);
6199
8159
  const exactField = sourceInputFields.find((field) => camelCase(field.name) === camelCase(mapping.targetPath));
6200
- return ` ${exactField ? camelCase(exactField.name) : sourceInputFields.length === 1 ? camelCase(sourceInputFields[0].name) : camelCase(mapping.targetPath)}: ${valueExpr},`;
6201
- });
8160
+ if (exactField) {
8161
+ directAssignments.push(` ${camelCase(exactField.name)}: ${valueExpr},`);
8162
+ continue;
8163
+ }
8164
+ if (acl.sourceQuery.queryKind === "list" && acl.sourceQuery.filters[camelCase(mapping.targetPath)]?.includes("eq")) {
8165
+ filterAssignments.push(` ${camelCase(mapping.targetPath)}: { eq: ${valueExpr} },`);
8166
+ continue;
8167
+ }
8168
+ const targetKey = sourceInputFields.length === 1 ? camelCase(sourceInputFields[0].name) : camelCase(mapping.targetPath);
8169
+ directAssignments.push(` ${targetKey}: ${valueExpr},`);
8170
+ }
8171
+ if (filterAssignments.length > 0) directAssignments.push(` filters: {`, ...filterAssignments, ` },`);
8172
+ return directAssignments;
6202
8173
  }
6203
8174
  function buildReadResponseAssignmentLines(acl, responseTypeName) {
6204
8175
  return acl.responseMappings.map((mapping) => {
6205
8176
  if (mapping.kind === "const") return ` ${camelCase(mapping.targetPath)}: ${JSON.stringify(mapping.value)},`;
6206
8177
  if (mapping.sourcePath === "result.items" || mapping.sourcePath === "result.total" || mapping.sourcePath === "result.page" || mapping.sourcePath === "result.pageSize") return ` ${camelCase(mapping.targetPath)}: ${mapping.sourcePath.replace(/^result\./, "result.")} as ${responseTypeName}[${JSON.stringify(camelCase(mapping.targetPath))}],`;
8178
+ if (acl.sourceQuery.queryKind === "list") {
8179
+ const sourceFieldName = mapping.sourcePath.replace(/^result\./, "");
8180
+ const sourceField = acl.sourceQuery.outputFields.find((field) => camelCase(field.name) === camelCase(sourceFieldName));
8181
+ if (sourceField) return ` ${camelCase(mapping.targetPath)}: result.items[0]?.${camelCase(sourceField.name)} as ${responseTypeName}[${JSON.stringify(camelCase(mapping.targetPath))}],`;
8182
+ }
6207
8183
  const sourceFieldName = mapping.sourcePath.replace(/^result\./, "");
6208
- return ` ${camelCase(mapping.targetPath)}: resultRecord[${JSON.stringify(sourceFieldName)}] as ${responseTypeName}[${JSON.stringify(camelCase(mapping.targetPath))}],`;
8184
+ return ` ${camelCase(mapping.targetPath)}: (result as unknown as Record<string, unknown>)[${JSON.stringify(sourceFieldName)}] as ${responseTypeName}[${JSON.stringify(camelCase(mapping.targetPath))}],`;
6209
8185
  });
6210
8186
  }
8187
+ function unsupportedReadAclReasons(acl) {
8188
+ const sourceInputFields = acl.sourceQuery.inputFields ?? [];
8189
+ const reasons = [];
8190
+ for (const mapping of acl.requestMappings) {
8191
+ if (mapping.kind === "const") continue;
8192
+ const targetField = camelCase(mapping.targetPath);
8193
+ const matchesInput = sourceInputFields.some((field) => camelCase(field.name) === targetField);
8194
+ const matchesEqFilter = acl.sourceQuery.queryKind === "list" && acl.sourceQuery.filters[targetField]?.includes("eq");
8195
+ const supportsNonListFallback = acl.sourceQuery.queryKind !== "list";
8196
+ if (!matchesInput && !matchesEqFilter && !supportsNonListFallback) reasons.push(`request field "${mapping.targetPath}" does not map to source query input or eq-filter shape`);
8197
+ }
8198
+ return reasons;
8199
+ }
6211
8200
  function buildReadAdapterContent(acl, sourceContextNames) {
6212
8201
  const consumerModulePath = normalizeModulePath(acl.consumerContext.modulePath);
6213
8202
  const sourceModulePath = normalizeModulePath(acl.sourceContext.modulePath);
@@ -6223,7 +8212,36 @@ function buildReadAdapterContent(acl, sourceContextNames) {
6223
8212
  const queryFileName = `${kebabCase(acl.sourceQuery.name)}.query.ts`;
6224
8213
  const queryTypeLiteral = `${pascalCase(acl.sourceContext.name)}.${pascalCase(acl.sourceQuery.name)}`;
6225
8214
  const usesInput = acl.requestMappings.some((mapping) => mapping.kind === "from" && mapping.sourcePath.startsWith("input."));
6226
- const needsResultRecord = acl.responseMappings.some((mapping) => mapping.kind === "from" && mapping.sourcePath !== "result.items" && mapping.sourcePath !== "result.total" && mapping.sourcePath !== "result.page" && mapping.sourcePath !== "result.pageSize");
8215
+ const unsupportedReasons = unsupportedReadAclReasons(acl);
8216
+ const diagnosticReasonSuffix = unsupportedReasons.length > 0 ? ` Reasons: ${unsupportedReasons.join("; ")}.` : "";
8217
+ if (unsupportedReasons.length > 0) return `// Auto-generated composition ACL adapter — do not edit by hand
8218
+ import type { Transaction } from "../../../lib/transaction.ts";
8219
+ import type { TransactionManager } from "../../unit-of-work/transaction-manager.ts";
8220
+ import type {
8221
+ ${portTypeName},
8222
+ ${requestTypeName},
8223
+ ${responseTypeName},
8224
+ } from "../../../core/contexts/${consumerModulePath}/application/acls/${kebabCase(stripAclPortSuffix(acl.port))}.acl.ts";
8225
+ import type { ${boundaryTypeName} } from "../../../core/contexts/${sourceModulePath}/application/read-boundary.ts";
8226
+
8227
+ export class ${adapterClassName} implements ${portTypeName} {
8228
+ constructor(
8229
+ private readonly ${boundaryVarName}: ${boundaryTypeName},
8230
+ private readonly transactionManager: TransactionManager,
8231
+ ) {}
8232
+
8233
+ async execute(
8234
+ input: ${requestTypeName},
8235
+ tx?: Transaction,
8236
+ ): Promise<${responseTypeName}> {
8237
+ void input;
8238
+ void tx;
8239
+ void this.${boundaryVarName};
8240
+ void this.transactionManager;
8241
+ throw new Error(${JSON.stringify(`${acl.name} ACL cannot be fully generated.${diagnosticReasonSuffix}`)});
8242
+ }
8243
+ }
8244
+ `;
6227
8245
  return `// Auto-generated composition ACL adapter — do not edit by hand
6228
8246
  import type { Transaction } from "../../../lib/transaction.ts";
6229
8247
  import type { TransactionManager } from "../../unit-of-work/transaction-manager.ts";
@@ -6248,16 +8266,13 @@ export class ${adapterClassName} implements ${portTypeName} {
6248
8266
  ${usesInput ? "" : "void input;"}
6249
8267
  try {
6250
8268
  const executeWithTransaction = async (currentTx: Transaction) => {
6251
- const result = await this.${boundaryVarName}.${boundaryMethodName}(
6252
- {
6253
- type: "${queryTypeLiteral}",
6254
- payload: {
8269
+ const query: ${queryEnvelopeTypeName} = {
8270
+ type: "${queryTypeLiteral}",
8271
+ payload: {
6255
8272
  ${buildReadRequestAssignmentLines(acl).join("\n")}
6256
- },
6257
- } as unknown as ${queryEnvelopeTypeName},
6258
- currentTx,
6259
- );
6260
- ${needsResultRecord ? `const resultRecord = result as unknown as Record<string, unknown>;` : ""}
8273
+ },
8274
+ };
8275
+ const result = await this.${boundaryVarName}.${boundaryMethodName}(query, currentTx);
6261
8276
 
6262
8277
  return {
6263
8278
  ${buildReadResponseAssignmentLines(acl, responseTypeName).join("\n")}
@@ -6268,8 +8283,9 @@ ${buildReadResponseAssignmentLines(acl, responseTypeName).join("\n")}
6268
8283
  return await executeWithTransaction(tx);
6269
8284
  }
6270
8285
 
6271
- return await this.transactionManager.withTransaction(async (runtimeTx) =>
6272
- executeWithTransaction(runtimeTx as Transaction)
8286
+ return await this.transactionManager.withTransaction<Transaction, ${responseTypeName}>(
8287
+ async (runtimeTx) =>
8288
+ executeWithTransaction(runtimeTx)
6273
8289
  );
6274
8290
  } catch (_error) {
6275
8291
  throw new Error("${acl.name} ACL failed", { cause: _error });
@@ -6291,6 +8307,7 @@ function buildWriteAdapterContent(acl) {
6291
8307
  const commandTypeLiteral = `${pascalCase(acl.targetContext.name)}.${pascalCase(acl.targetCommand.name)}`;
6292
8308
  const responseMappings = acl.kind === "ack" ? acl.acknowledgeMappings : acl.responseMappings;
6293
8309
  const responseBody = acl.kind === "ack" ? ` return;\n` : ` return {\n${toTargetAssignmentLines(responseMappings, "result").join("\n")}\n };\n`;
8310
+ const resultValueLine = acl.kind === "ack" ? "" : `\n const result = executionResult.value;`;
6294
8311
  return `// Auto-generated composition ACL adapter — do not edit by hand
6295
8312
  import type { EventContext } from "../../../core/shared-kernel/events/event-context.ts";
6296
8313
  import type { Result } from "../../../core/shared-kernel/result.ts";
@@ -6313,7 +8330,7 @@ export class ${adapterClassName} implements ${portTypeName} {
6313
8330
  ): Promise<${responseTypeName}> {
6314
8331
  try {
6315
8332
  const executionResult = await this.commandBus.execute<
6316
- Result<Record<string, unknown>, unknown>,
8333
+ Result<${responseTypeName}, unknown>,
6317
8334
  ${commandEnvelopeTypeName}
6318
8335
  >(
6319
8336
  {
@@ -6328,8 +8345,7 @@ ${toTargetAssignmentLines(acl.requestMappings, "input").join("\n")}
6328
8345
  if (!executionResult.ok) {
6329
8346
  throw executionResult.error;
6330
8347
  }
6331
-
6332
- const result = executionResult.value as Record<string, unknown>;
8348
+ ${resultValueLine}
6333
8349
  ${responseBody}
6334
8350
  } catch (_error) {
6335
8351
  throw new Error("${acl.name} ACL failed", { cause: _error });
@@ -6438,6 +8454,25 @@ function buildCompositionAclArtifacts(compositionSpec) {
6438
8454
  ].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
6439
8455
  }
6440
8456
 
8457
+ //#endregion
8458
+ //#region packages/core/generators/composition_dependencies.ts
8459
+ function resolveRuntimeDependencies(compositionSpec) {
8460
+ const dependencies = new Set(["drizzle-orm"]);
8461
+ if (compositionSpec.infrastructure.persistence === "postgres") dependencies.add("postgres");
8462
+ else if (compositionSpec.infrastructure.persistence === "mysql") dependencies.add("mysql2");
8463
+ if (compositionSpec.infrastructure.orm === "mikroorm") dependencies.add("@mikro-orm/core");
8464
+ return [...dependencies].sort((left, right) => left.localeCompare(right));
8465
+ }
8466
+ function buildCompositionDependencyManifestArtifacts(compositionSpec) {
8467
+ const dependencyArrayLiteral = `[${resolveRuntimeDependencies(compositionSpec).map((dependency) => JSON.stringify(dependency)).join(", ")}]`;
8468
+ return [createGeneratedArtifact("codegen/dependency-manifest.json", `{
8469
+ "kind": "zodmire-dependency-manifest",
8470
+ "infrastructure": ${JSON.stringify(compositionSpec.infrastructure, null, 2).replace(/\n/g, "\n ")},
8471
+ "dependencies": ${dependencyArrayLiteral}
8472
+ }
8473
+ `, sharedArtifactOwnership("composition"))];
8474
+ }
8475
+
6441
8476
  //#endregion
6442
8477
  //#region packages/core/generators/composition_container.ts
6443
8478
  /**
@@ -6446,9 +8481,11 @@ function buildCompositionAclArtifacts(compositionSpec) {
6446
8481
  */
6447
8482
  function buildCompositionContainerArtifacts(compositionSpec, contextSpecs) {
6448
8483
  const ownership = sharedArtifactOwnership("composition");
6449
- 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));
8484
+ return [createGeneratedArtifact("infrastructure/di/container.ts", buildContainerContent(compositionSpec, contextSpecs), ownership), createGeneratedArtifact("infrastructure/di/repo-factory.ts", buildRepoFactoryContent(compositionSpec, contextSpecs), ownership)].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
6450
8485
  }
6451
8486
  function buildContainerContent(compositionSpec, contextSpecs) {
8487
+ assertSupportedContainerStrategy(compositionSpec);
8488
+ const runtime = resolveContainerRuntime(compositionSpec);
6452
8489
  const readContexts = uniqueReadContexts(compositionSpec.acls);
6453
8490
  const readContextNames = buildCollisionSafeModulePathNames(readContexts.map((context) => context.modulePath));
6454
8491
  const commandContextNames = buildCollisionSafeModulePathNames(contextSpecs.map((context) => context.context.modulePath));
@@ -6458,7 +8495,10 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6458
8495
  ``,
6459
8496
  `import { CommandBusImpl } from "../messaging/command-bus.impl.ts";`,
6460
8497
  `import { QueryBusImpl } from "../messaging/query-bus.impl.ts";`,
6461
- `import { DrizzleTransactionManager } from "../unit-of-work/drizzle-transaction-manager.ts";`,
8498
+ `import { ${runtime.transactionManagerClassName} } from "../unit-of-work/${runtime.transactionManagerFileBase}.ts";`,
8499
+ `import { OutboxDispatcher } from "../outbox/outbox-dispatcher.ts";`,
8500
+ `import { OutboxPoller } from "../outbox/outbox-poller.ts";`,
8501
+ `import { OutboxRelay } from "../outbox/outbox-relay.ts";`,
6462
8502
  `import { OutboxWriter } from "../outbox/outbox-writer.ts";`,
6463
8503
  `import { AclFactory } from "./acl-factory.ts";`,
6464
8504
  `import { RepositoryFactory } from "./repo-factory.ts";`,
@@ -6477,7 +8517,8 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6477
8517
  for (const query of uniqueQueriesByReadModel(contextSpec.queries)) {
6478
8518
  const repoClassName = queryReadModelRepositoryClassName(query);
6479
8519
  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";`);
8520
+ const repoFileBase = readModelRepositoryFileBase(resolvedReadModelName(query));
8521
+ 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
8522
  }
6482
8523
  }
6483
8524
  for (const spec of contextSpecs) {
@@ -6501,15 +8542,16 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6501
8542
  ` readonly commandBus: CommandBusPort;`,
6502
8543
  ` readonly queryBus: QueryBusPort;`,
6503
8544
  ` readonly integrationEventSubscriptions: EventSubscription[];`,
6504
- ` readonly db: unknown;`,
8545
+ ` readonly outboxPoller: OutboxPoller;`,
8546
+ ...runtime.containerFields.map((field) => ` readonly ${field}: unknown;`),
6505
8547
  `};`
6506
8548
  ];
6507
8549
  const createContainerLines = [
6508
8550
  ``,
6509
- `export function createContainer(deps: { db: unknown }): Container {`,
8551
+ `export function createContainer(deps: { ${runtime.dependencyFields.map((field) => `${field}: unknown`).join("; ")} }): Container {`,
6510
8552
  ` const outboxWriter = new OutboxWriter();`,
6511
8553
  ` const repoFactory = new RepositoryFactory();`,
6512
- ` const txManager = new DrizzleTransactionManager(deps.db);`
8554
+ ` const txManager = new ${runtime.transactionManagerClassName}(${runtime.transactionDependencyRef});`
6513
8555
  ];
6514
8556
  for (const context of readContexts) {
6515
8557
  const contextSpec = contextSpecs.find((spec) => spec.context.modulePath.toLowerCase() === context.modulePath.toLowerCase());
@@ -6517,7 +8559,7 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6517
8559
  const boundaryPlan = readContextNames.get(context.modulePath);
6518
8560
  const boundaryVar = `${boundaryPlan.propertyStem}PublishedReadBoundary`;
6519
8561
  const boundaryAlias = `create${boundaryPlan.symbolStem}PublishedReadBoundary`;
6520
- const boundaryDeps = buildPublishedReadBoundaryDeps(contextSpec, boundaryPlan.symbolStem);
8562
+ const boundaryDeps = buildPublishedReadBoundaryDeps(contextSpec, boundaryPlan.symbolStem, runtime.readRepositoryDependencyRef);
6521
8563
  createContainerLines.push(` const ${boundaryVar} = ${boundaryAlias}(${boundaryDeps});`);
6522
8564
  }
6523
8565
  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);`, ` },`, ` };`);
@@ -6529,7 +8571,7 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6529
8571
  for (const command of spec.commands) {
6530
8572
  const commandType = `${ctxName}.${pascalCase(command.name)}`;
6531
8573
  const handlerAlias = `${contextPlan.symbolStem}${pascalCase(command.name)}Handler`;
6532
- createContainerLines.push(` commandBus.register("${commandType}", ${handlerAlias} as any, "${contextPlan.pathSegment}");`);
8574
+ createContainerLines.push(` commandBus.register("${commandType}", ${handlerAlias}, "${contextPlan.pathSegment}");`);
6533
8575
  }
6534
8576
  }
6535
8577
  createContainerLines.push(``);
@@ -6548,7 +8590,9 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6548
8590
  createContainerLines.push(` const integrationEventSubscriptions: EventSubscription[] = [];`);
6549
8591
  }
6550
8592
  createContainerLines.push(``);
6551
- createContainerLines.push(` return { commandBus: commandBus!, queryBus, integrationEventSubscriptions, db: deps.db };`);
8593
+ createContainerLines.push(` const outboxDispatcher = new OutboxDispatcher(${runtime.transactionDependencyRef}, integrationEventSubscriptions);`, ` const outboxRelay = new OutboxRelay(outboxDispatcher);`, ` commandBus.setRelay(outboxRelay);`, ` const outboxPoller = new OutboxPoller(outboxDispatcher);`);
8594
+ createContainerLines.push(``);
8595
+ createContainerLines.push(` return { commandBus: commandBus!, queryBus, integrationEventSubscriptions, outboxPoller, ${runtime.containerFields.map((field) => `${field}: deps.${field}`).join(", ")} };`);
6552
8596
  createContainerLines.push(`}`);
6553
8597
  createContainerLines.push(``);
6554
8598
  return [
@@ -6557,11 +8601,50 @@ function buildContainerContent(compositionSpec, contextSpecs) {
6557
8601
  ...createContainerLines
6558
8602
  ].join("\n");
6559
8603
  }
6560
- function buildPublishedReadBoundaryDeps(spec, contextSymbolStem) {
8604
+ function buildPublishedReadBoundaryDeps(spec, contextSymbolStem, readRepositoryDependencyRef) {
6561
8605
  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)`);
8606
+ for (const query of uniqueQueriesByReadModel(spec.queries)) entries.set(queryReadModelVariableName(query), `${queryReadModelVariableName(query)}: new ${queryReadModelRepositoryAlias(contextSymbolStem, query)}(${readRepositoryDependencyRef})`);
6563
8607
  return `{ ${[...entries.values()].join(", ")} }`;
6564
8608
  }
8609
+ function assertSupportedContainerStrategy(compositionSpec) {
8610
+ const { architecture, persistence, orm } = compositionSpec.infrastructure;
8611
+ 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}`);
8612
+ }
8613
+ function resolveContainerRuntime(compositionSpec) {
8614
+ const { architecture, orm } = compositionSpec.infrastructure;
8615
+ if (architecture === "physical-cqrs" && orm === "drizzle") return {
8616
+ transactionManagerClassName: "DrizzleTransactionManager",
8617
+ transactionManagerFileBase: "drizzle-transaction-manager",
8618
+ transactionDependencyRef: "deps.writeDb",
8619
+ readRepositoryDependencyRef: "deps.readDb",
8620
+ dependencyFields: ["writeDb", "readDb"],
8621
+ containerFields: ["writeDb", "readDb"]
8622
+ };
8623
+ if (architecture === "logical-cqrs" && orm === "drizzle") return {
8624
+ transactionManagerClassName: "DrizzleTransactionManager",
8625
+ transactionManagerFileBase: "drizzle-transaction-manager",
8626
+ transactionDependencyRef: "deps.db",
8627
+ readRepositoryDependencyRef: "deps.db",
8628
+ dependencyFields: ["db"],
8629
+ containerFields: ["db"]
8630
+ };
8631
+ if (architecture === "physical-cqrs" && orm === "mikroorm") return {
8632
+ transactionManagerClassName: "MikroOrmTransactionManager",
8633
+ transactionManagerFileBase: "mikroorm-transaction-manager",
8634
+ transactionDependencyRef: "deps.writeEm",
8635
+ readRepositoryDependencyRef: "deps.readDb",
8636
+ dependencyFields: ["writeEm", "readDb"],
8637
+ containerFields: ["writeEm", "readDb"]
8638
+ };
8639
+ return {
8640
+ transactionManagerClassName: "MikroOrmTransactionManager",
8641
+ transactionManagerFileBase: "mikroorm-transaction-manager",
8642
+ transactionDependencyRef: "deps.em",
8643
+ readRepositoryDependencyRef: "deps.db",
8644
+ dependencyFields: ["em", "db"],
8645
+ containerFields: ["em", "db"]
8646
+ };
8647
+ }
6565
8648
  function resolvedReadModelName(query) {
6566
8649
  return query.readSide?.readModelName ?? query.readModelName ?? query.name;
6567
8650
  }
@@ -6580,11 +8663,6 @@ function uniqueQueriesByReadModel(queries) {
6580
8663
  }
6581
8664
  return [...queriesByReadModel.values()].sort((left, right) => resolvedReadModelName(left).localeCompare(resolvedReadModelName(right)));
6582
8665
  }
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
8666
  function queryReadModelPortTypeName(query) {
6589
8667
  return `${readModelContractName(resolvedReadModelName(query))}RepositoryPort`;
6590
8668
  }
@@ -6609,25 +8687,34 @@ function uniqueReadContexts(acls) {
6609
8687
  }
6610
8688
  return contexts;
6611
8689
  }
6612
- 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
- })));
8690
+ function buildRepoFactoryContent(compositionSpec, contextSpecs) {
8691
+ const forceMikroOrmRepositories = compositionSpec.infrastructure.orm === "mikroorm";
8692
+ const repositoryDefs = contextSpecs.flatMap((spec) => spec.aggregates.map((aggregate) => {
8693
+ const repositoryPort = spec.ports.find((port) => port.kind === "repository" && port.target === aggregate.name);
8694
+ const adapter = repositoryPort ? spec.adapters.find((candidate) => candidate.port === repositoryPort.name && (candidate.kind === "drizzle-repository" || candidate.kind === "mikroorm-repository")) : void 0;
8695
+ const isMikroOrm = forceMikroOrmRepositories || adapter?.kind === "mikroorm-repository";
8696
+ const providerPrefix = isMikroOrm ? "mikroorm" : "drizzle";
8697
+ const repositoryClassPrefix = isMikroOrm ? "MikroOrm" : "Drizzle";
8698
+ return {
8699
+ aggregateName: aggregate.name,
8700
+ modulePath: spec.context.modulePath,
8701
+ repoFile: `${providerPrefix}-${kebabCase(aggregate.name)}.repository.ts`,
8702
+ repoImportBasePath: `../persistence/${providerPrefix}/repositories`,
8703
+ aggregateAliasBase: `${repositoryClassPrefix}${pascalCase(aggregate.name)}Repository`,
8704
+ aggregateFieldBase: `${camelCase(aggregate.name)}s`
8705
+ };
8706
+ }));
6620
8707
  assertNoSameContextRepoFileCollisions(repositoryDefs);
6621
8708
  const namingPlan = buildRepositoryNamingPlan(repositoryDefs);
6622
8709
  const lines = [`// Auto-generated repository factory — do not edit by hand`, ``];
6623
8710
  for (const repositoryDef of repositoryDefs) {
6624
- const aggName = repositoryDef.aggregateName;
8711
+ repositoryDef.aggregateName;
6625
8712
  const modulePath = normalizeModulePath(repositoryDef.modulePath);
6626
- const repoClass = `Drizzle${pascalCase(aggName)}Repository`;
8713
+ const repoClass = repositoryDef.aggregateAliasBase;
6627
8714
  const { repoAlias } = namingPlan.get(repositoryKey(repositoryDef));
6628
8715
  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}";`);
8716
+ if (repoAlias === repoClass) lines.push(`import { ${repoClass} } from "${repositoryDef.repoImportBasePath}/${modulePath}/${repoFile}";`);
8717
+ else lines.push(`import { ${repoClass} as ${repoAlias} } from "${repositoryDef.repoImportBasePath}/${modulePath}/${repoFile}";`);
6631
8718
  }
6632
8719
  lines.push(``);
6633
8720
  lines.push(`export class RepositoryFactory {`);
@@ -6712,7 +8799,8 @@ function buildRootRouterContent(contextSpecs) {
6712
8799
  const lines = [
6713
8800
  `// Auto-generated root tRPC router — do not edit by hand`,
6714
8801
  ``,
6715
- `import type { Container } from "../../infrastructure/di/container.ts";`
8802
+ `import type { Container } from "../../infrastructure/di/container.ts";`,
8803
+ `import { router } from "./trpc-init.ts";`
6716
8804
  ];
6717
8805
  for (const spec of contextSpecs) {
6718
8806
  const routerName = spec.presentation.trpcRouter;
@@ -6722,7 +8810,7 @@ function buildRootRouterContent(contextSpecs) {
6722
8810
  }
6723
8811
  lines.push(``);
6724
8812
  lines.push(`export function createRootRouter(container: Container) {`);
6725
- lines.push(` return {`);
8813
+ lines.push(` return router({`);
6726
8814
  for (const spec of contextSpecs) {
6727
8815
  const routerName = spec.presentation.trpcRouter;
6728
8816
  const key = camelCase(routerName);
@@ -6732,7 +8820,7 @@ function buildRootRouterContent(contextSpecs) {
6732
8820
  lines.push(` queryBus: container.queryBus,`);
6733
8821
  lines.push(` }),`);
6734
8822
  }
6735
- lines.push(` };`);
8823
+ lines.push(` });`);
6736
8824
  lines.push(`}`);
6737
8825
  lines.push(``);
6738
8826
  return lines.join("\n");
@@ -6765,9 +8853,14 @@ export {};
6765
8853
  const payloadTypeNames = [];
6766
8854
  for (const evt of compositionSpec.crossContextEvents) {
6767
8855
  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};`);
8856
+ const payloadTypeName = integrationPayloadTypeName(evt);
8857
+ payloadTypeNames.push(payloadTypeName);
8858
+ lines.push(`export interface ${payloadTypeName} {`);
8859
+ for (const field of fields) {
8860
+ const opt = field.optional ? "?" : "";
8861
+ const comment = field.comment ? ` ${field.comment}` : "";
8862
+ lines.push(` readonly ${field.name}${opt}: ${field.type};${comment}`);
8863
+ }
6771
8864
  lines.push(`}`);
6772
8865
  lines.push(``);
6773
8866
  }
@@ -6778,18 +8871,20 @@ export {};
6778
8871
  return lines.join("\n");
6779
8872
  }
6780
8873
  function buildHandlerFactoryContent(evt, sub) {
6781
- const payloadMappingLines = getPayloadFields(evt.payloadSchema).map((f) => ` ${f.name}: event.payload.${f.name},`);
8874
+ const fields = getPayloadFields(evt.payloadSchema);
8875
+ const payloadTypeName = integrationPayloadTypeName(evt);
8876
+ const payloadMappingLines = fields.map((f) => ` ${f.name}: event.payload.${f.name},`);
6782
8877
  const commandType = `${pascalCase(sub.targetContext)}.${pascalCase(sub.action)}`;
6783
8878
  return [
6784
8879
  `// Auto-generated event handler factory — do not edit by hand`,
6785
8880
  `import type { CommandBusPort } from "../../../../../core/ports/messaging/command-bus.port.ts";`,
6786
8881
  `import type { IntegrationEvent, IntegrationEventHandler } from "../../../../../infrastructure/messaging/integration-event.ts";`,
6787
- `import type { ${evt.payloadTypeName} } from "../../../../shared-kernel/events/integration-events.types.ts";`,
8882
+ `import type { ${payloadTypeName} } from "../../../../shared-kernel/events/integration-events.types.ts";`,
6788
8883
  ``,
6789
8884
  `export function ${sub.handlerFactoryName}(deps: {`,
6790
8885
  ` commandBus: CommandBusPort;`,
6791
- `}): IntegrationEventHandler<${evt.payloadTypeName}> {`,
6792
- ` return async (event: IntegrationEvent<${evt.payloadTypeName}>) => {`,
8886
+ `}): IntegrationEventHandler<${payloadTypeName}> {`,
8887
+ ` return async (event: IntegrationEvent<${payloadTypeName}>) => {`,
6793
8888
  ` return deps.commandBus.execute(`,
6794
8889
  ` {`,
6795
8890
  ` type: "${commandType}",`,
@@ -6808,30 +8903,114 @@ function buildSubscriptionRegistryContent(compositionSpec) {
6808
8903
  const allSubs = [];
6809
8904
  for (const evt of compositionSpec.crossContextEvents) for (const sub of evt.subscriptions) allSubs.push({
6810
8905
  eventType: evt.eventType,
8906
+ payloadTypeName: integrationPayloadTypeName(evt),
6811
8907
  sub
6812
8908
  });
6813
8909
  const importLines = [`// Auto-generated subscription registry — do not edit by hand`, `import type { EventSubscription, IntegrationEventHandler } from "./integration-event.ts";`];
8910
+ 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";`);
8911
+ importLines.push(...payloadTypeImports);
6814
8912
  for (const { sub } of allSubs) {
6815
8913
  const fileName = `${kebabCase(sub.handlerName)}.handler.ts`;
6816
8914
  importLines.push(`// Handler: ${sub.handlerFactoryName} from core/contexts/${sub.targetModulePath}/application/event-handlers/${fileName}`);
6817
8915
  }
6818
- const depsEntries = allSubs.map(({ sub }) => ` ${camelCase(sub.handlerName)}: IntegrationEventHandler<any>;`);
8916
+ const depsEntries = allSubs.map(({ payloadTypeName, sub }) => ` ${camelCase(sub.handlerName)}: IntegrationEventHandler<${payloadTypeName}>;`);
6819
8917
  const registryEntries = allSubs.map(({ eventType, sub }) => ` { eventType: "${eventType}", handler: deps.${camelCase(sub.handlerName)} },`);
6820
8918
  const lines = [...importLines, ``];
6821
8919
  if (depsEntries.length > 0) lines.push(`export function buildSubscriptionRegistry(deps: {`, ...depsEntries, `}): EventSubscription[] {`, ` return [`, ...registryEntries, ` ];`, `}`, ``);
6822
8920
  else lines.push(`export function buildSubscriptionRegistry(_deps: Record<string, never>): EventSubscription[] {`, ` return [];`, `}`, ``);
6823
8921
  return lines.join("\n");
6824
8922
  }
8923
+ function integrationPayloadTypeName(evt) {
8924
+ return `${pascalCase(evt.sourceContext)}${evt.payloadTypeName}`;
8925
+ }
8926
+ function renderPayloadSchemaType(schema, indent = 0) {
8927
+ const raw = schema;
8928
+ const def = raw?._zod?.def;
8929
+ const typeName = typeof def?.type === "string" ? def.type : void 0;
8930
+ if (typeName === "optional") return {
8931
+ ...renderPayloadSchemaType(def?.innerType, indent),
8932
+ optional: true
8933
+ };
8934
+ if (typeName === "nullable" || typeName === "readonly" || typeName === "default" || typeName === "catch" || typeName === "nonoptional" || typeName === "success" || typeName === "prefault") return renderPayloadSchemaType(def?.innerType, indent);
8935
+ if (typeName === "array") {
8936
+ const element = renderPayloadSchemaType(def?.element, indent);
8937
+ return {
8938
+ type: `Array<${element.type}>`,
8939
+ optional: false,
8940
+ comment: element.comment
8941
+ };
8942
+ }
8943
+ if (typeName === "object") {
8944
+ const shape = typeof raw?.shape === "object" && raw?.shape !== null ? raw.shape : typeof def?.shape === "object" && def?.shape !== null ? def.shape : {};
8945
+ return {
8946
+ type: `{\n${Object.entries(shape).map(([fieldName, fieldSchema]) => {
8947
+ const field = renderPayloadSchemaType(fieldSchema, indent + 1);
8948
+ const opt = field.optional ? "?" : "";
8949
+ const comment = field.comment ? ` ${field.comment}` : "";
8950
+ return `${" ".repeat(indent + 1)}readonly ${fieldName}${opt}: ${field.type};${comment}`;
8951
+ }).join("\n")}\n${" ".repeat(indent)}}`,
8952
+ optional: false
8953
+ };
8954
+ }
8955
+ if (typeName === "enum") {
8956
+ const entries = def?.entries;
8957
+ if (entries && typeof entries === "object") return {
8958
+ type: Object.values(entries).map((value) => JSON.stringify(String(value))).join(" | "),
8959
+ optional: false
8960
+ };
8961
+ return {
8962
+ type: "string",
8963
+ optional: false
8964
+ };
8965
+ }
8966
+ if (typeName === "record") {
8967
+ const valueShape = renderPayloadSchemaType(def?.valueType, indent);
8968
+ return {
8969
+ type: `Record<string, ${valueShape.type}>`,
8970
+ optional: false,
8971
+ comment: valueShape.comment
8972
+ };
8973
+ }
8974
+ if (typeName === "tuple") return {
8975
+ type: `[${(Array.isArray(def?.items) ? def.items : []).map((item) => renderPayloadSchemaType(item, indent).type).join(", ")}]`,
8976
+ optional: false
8977
+ };
8978
+ if (typeName === "union") return {
8979
+ type: (Array.isArray(def?.options) ? def.options : []).map((option) => renderPayloadSchemaType(option, indent).type).join(" | "),
8980
+ optional: false
8981
+ };
8982
+ if (typeName === "date") return {
8983
+ type: "string",
8984
+ optional: false,
8985
+ comment: "// ISO 8601"
8986
+ };
8987
+ if (typeName === "number" || typeName === "int" || typeName === "float") return {
8988
+ type: "number",
8989
+ optional: false
8990
+ };
8991
+ if (typeName === "boolean") return {
8992
+ type: "boolean",
8993
+ optional: false
8994
+ };
8995
+ if (typeName === "string") return {
8996
+ type: "string",
8997
+ optional: false
8998
+ };
8999
+ return {
9000
+ type: "string",
9001
+ optional: false
9002
+ };
9003
+ }
6825
9004
  function getPayloadFields(payloadSchema) {
6826
9005
  const shape = payloadSchema?.shape;
6827
9006
  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";
9007
+ const fieldSchema = shape[key];
9008
+ const rendered = renderPayloadSchemaType(fieldSchema);
6832
9009
  return {
6833
9010
  name: key,
6834
- type: tsType
9011
+ type: rendered.type,
9012
+ optional: rendered.optional,
9013
+ comment: rendered.comment
6835
9014
  };
6836
9015
  });
6837
9016
  return [];
@@ -6856,7 +9035,10 @@ async function buildContextNormalizedSpecFromConfig(input) {
6856
9035
  }))), config);
6857
9036
  }
6858
9037
  async function buildV5ContextArtifacts(input) {
6859
- return generateContextArtifacts(await buildContextNormalizedSpecFromConfig(input));
9038
+ return generateContextArtifacts(await buildContextNormalizedSpecFromConfig(input), {
9039
+ infrastructureStrategy: input.infrastructureStrategy,
9040
+ generation: input.generation
9041
+ });
6860
9042
  }
6861
9043
  async function buildV5Artifacts(input) {
6862
9044
  const spec = await buildNormalizedSpecFromConfig(input);
@@ -6881,8 +9063,9 @@ async function buildCompositionArtifacts(compositionConfigPath, contextSpecs) {
6881
9063
  return mergeGeneratedArtifacts([
6882
9064
  ...buildCompositionTypeArtifacts(),
6883
9065
  ...buildCompositionBusArtifacts(),
6884
- ...buildCompositionOutboxArtifacts(),
9066
+ ...buildCompositionOutboxArtifacts(compositionSpec.infrastructure),
6885
9067
  ...buildCompositionAclArtifacts(compositionSpec),
9068
+ ...buildCompositionDependencyManifestArtifacts(compositionSpec),
6886
9069
  ...buildReadModelCompositionArtifacts(composedContextSpecs),
6887
9070
  ...buildCompositionContainerArtifacts(compositionSpec, composedContextSpecs),
6888
9071
  ...buildCompositionRouterArtifacts(compositionSpec, composedContextSpecs),
@@ -6891,4 +9074,4 @@ async function buildCompositionArtifacts(compositionConfigPath, contextSpecs) {
6891
9074
  }
6892
9075
 
6893
9076
  //#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 };
9077
+ export { DEFAULT_CONTEXT_GENERATOR_TARGET_REGISTRY, 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, userArtifactOwnership, validateContextImports, walkEntityTree, withArtifactOwnership, wordsFromName };