@zodmire/core 0.1.2 → 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,12 +223,17 @@ 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,
@@ -165,6 +244,7 @@ function createReadField(exportName) {
165
244
  const right = def.right;
166
245
  if (left?.objectShape && right?.objectShape) return {
167
246
  type: "object",
247
+ typeDescriptor: { kind: "object" },
168
248
  optional: false,
169
249
  array: false,
170
250
  sourceSchema: rawSchema,
@@ -175,6 +255,7 @@ function createReadField(exportName) {
175
255
  };
176
256
  return {
177
257
  type: "intersection",
258
+ typeDescriptor: { kind: "intersection" },
178
259
  optional: false,
179
260
  array: false,
180
261
  sourceSchema: rawSchema
@@ -182,18 +263,21 @@ function createReadField(exportName) {
182
263
  }
183
264
  case zx.tagged("union")(rawSchema): return {
184
265
  type: "union",
266
+ typeDescriptor: { kind: "union" },
185
267
  optional: false,
186
268
  array: false,
187
269
  sourceSchema: rawSchema
188
270
  };
189
271
  case zx.tagged("record")(rawSchema): return {
190
272
  type: "record",
273
+ typeDescriptor: { kind: "record" },
191
274
  optional: false,
192
275
  array: false,
193
276
  sourceSchema: rawSchema
194
277
  };
195
278
  case zx.tagged("tuple")(rawSchema): return {
196
279
  type: "tuple",
280
+ typeDescriptor: { kind: "tuple" },
197
281
  optional: false,
198
282
  array: false,
199
283
  sourceSchema: rawSchema
@@ -211,9 +295,22 @@ function createReadField(exportName) {
211
295
  case zx.tagged("any")(rawSchema):
212
296
  case zx.tagged("unknown")(rawSchema):
213
297
  case zx.tagged("never")(rawSchema):
214
- 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
+ }
215
308
  case zx.tagged("literal")(rawSchema): return {
216
- 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
+ },
217
314
  optional: false,
218
315
  array: false,
219
316
  sourceSchema: rawSchema
@@ -238,8 +335,14 @@ function toIntrospectedField(name, read, registry) {
238
335
  optional: read.optional,
239
336
  array: read.array
240
337
  };
338
+ if (read.typeDescriptor) field.typeDescriptor = read.typeDescriptor;
241
339
  const fieldMeta = registry.get(read.sourceSchema);
242
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
+ };
243
346
  if (read.objectShape) field.nestedFields = Object.entries(read.objectShape).map(([childName, childRead]) => toIntrospectedField(childName, childRead, registry));
244
347
  return field;
245
348
  }
@@ -327,7 +430,7 @@ function introspectSchema(schema, registry, exportName) {
327
430
  };
328
431
  }
329
432
  function deriveSchemaName(schema) {
330
- const typeName = schema?.constructor?.name;
433
+ const typeName = isRecord(schema) && "constructor" in schema ? schema.constructor?.name : void 0;
331
434
  if (typeName && typeName !== "Object" && typeName !== "ZodObject") return typeName.replace(/Schema$/, "");
332
435
  return "UnnamedEntity";
333
436
  }
@@ -347,14 +450,12 @@ async function readSchemaFile(filePath, registry) {
347
450
  const queryOutputs = /* @__PURE__ */ new Map();
348
451
  const schemaExportNames = /* @__PURE__ */ new Map();
349
452
  for (const [exportName, exported] of Object.entries(mod)) {
350
- if (!exported || typeof exported !== "object") continue;
351
- if (!("_zod" in exported)) continue;
453
+ if (!isZodSchemaLike(exported)) continue;
352
454
  schemaExportNames.set(exported, exportName);
353
455
  }
354
456
  const childEntitySchemas = /* @__PURE__ */ new Set();
355
457
  for (const [exportName, exported] of Object.entries(mod)) {
356
- if (!exported || typeof exported !== "object") continue;
357
- if (!("_zod" in exported)) continue;
458
+ if (!isZodSchemaLike(exported)) continue;
358
459
  const meta = registry.get(exported);
359
460
  if (!meta) continue;
360
461
  switch (meta.kind) {
@@ -403,29 +504,27 @@ async function readSchemaFile(filePath, registry) {
403
504
  }
404
505
  case "query-input": {
405
506
  const fields = introspectObjectShape(exported, registry, exportName);
406
- const legacyMeta = meta;
507
+ const legacyMeta = deriveLegacyQueryInputMeta(meta);
407
508
  queryInputs.set(meta.queryName, {
408
509
  fields,
409
510
  targetAggregate: meta.targetAggregate,
410
- readModelName: meta.readSide?.readModelName ?? legacyMeta.readModelName,
411
- queryKind: "queryKind" in meta ? meta.queryKind : "findById",
412
- pagination: "pagination" in meta ? meta.pagination : void 0,
413
- filters: "filters" in meta ? meta.filters : void 0,
414
- sorting: "sorting" in meta ? meta.sorting : void 0,
415
- 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
416
517
  });
417
518
  break;
418
519
  }
419
520
  case "query-output": {
420
521
  const fields = introspectObjectShape(exported, registry, exportName);
421
- const legacyMeta = meta;
522
+ const legacyMeta = deriveLegacyQueryOutputMeta(meta);
422
523
  queryOutputs.set(meta.queryName, {
423
524
  fields,
424
525
  outputSchemaPath: filePath,
425
526
  outputSchemaExportName: exportName,
426
- ...legacyMeta.readModelName !== void 0 ? { readModelName: legacyMeta.readModelName } : {},
427
- ...legacyMeta.targetAggregate !== void 0 ? { targetAggregate: legacyMeta.targetAggregate } : {},
428
- ...legacyMeta.computedFields !== void 0 ? { computedFields: legacyMeta.computedFields } : {}
527
+ ...legacyMeta
429
528
  });
430
529
  break;
431
530
  }
@@ -460,8 +559,7 @@ async function readSchemaFile(filePath, registry) {
460
559
  }
461
560
  }
462
561
  for (const [exportName, exported] of Object.entries(mod)) {
463
- if (!exported || typeof exported !== "object") continue;
464
- if (!("_zod" in exported)) continue;
562
+ if (!isZodSchemaLike(exported)) continue;
465
563
  const meta = registry.get(exported);
466
564
  if (meta?.kind !== "entity") continue;
467
565
  if (childEntitySchemas.has(exported)) continue;
@@ -566,8 +664,17 @@ function deriveProjectionCapabilities(projection, readModelByName) {
566
664
  rebuild: !projection.rebuild?.enabled ? disabledCapability("projection rebuild is disabled") : writeModel.status === "generated" ? generatedCapability() : runtimeThrowCapability([...writeModel.reasons])
567
665
  };
568
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
+ }
569
674
  function buildContextNormalizedSpec(schemaResult, config) {
570
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);
571
678
  const aggregateNames = new Set(schemaResult.aggregates.map((a) => a.name));
572
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(", ")}`);
573
680
  for (const q of schemaResult.queries) {
@@ -581,7 +688,6 @@ function buildContextNormalizedSpec(schemaResult, config) {
581
688
  }
582
689
  for (const q of schemaResult.queries) if (q.queryKind === "list") {
583
690
  if (!q.pagination) throw new Error(`[normalizer] List query "${q.name}" is missing pagination config`);
584
- if (q.pagination.style === "cursor") throw new Error(`[normalizer] List query "${q.name}" uses cursor pagination which is not supported in v12`);
585
691
  if (!q.filters) throw new Error(`[normalizer] List query "${q.name}" is missing filters config`);
586
692
  if (!q.sorting) throw new Error(`[normalizer] List query "${q.name}" is missing sorting config`);
587
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`);
@@ -790,7 +896,7 @@ function buildContextNormalizedSpec(schemaResult, config) {
790
896
  const readSide = {
791
897
  readModelName: q.readModelName ?? pascalCase(q.name),
792
898
  searchFields: [],
793
- pagination: q.pagination?.style === "offset" ? { style: "offset" } : void 0,
899
+ pagination: q.pagination,
794
900
  filters: q.filters,
795
901
  sorting: q.sorting?.sortableFields ? q.sorting : void 0
796
902
  };
@@ -842,7 +948,7 @@ function buildContextNormalizedSpec(schemaResult, config) {
842
948
  outputSchemaExportName: q.outputSchemaExportName,
843
949
  readModelName: q.readModelName,
844
950
  readSide,
845
- pagination: { style: "offset" },
951
+ pagination: q.pagination ?? { style: "offset" },
846
952
  filters: q.filters,
847
953
  sorting: q.sorting,
848
954
  computedFields: resolvedComputed,
@@ -873,7 +979,9 @@ function buildContextNormalizedSpec(schemaResult, config) {
873
979
  adapters: config.adapters,
874
980
  acls: config.acls,
875
981
  presentation: config.presentation,
876
- materialization: config.materialization
982
+ materialization: config.materialization,
983
+ writeModelStrategy,
984
+ readModelStrategy
877
985
  };
878
986
  }
879
987
  function buildNormalizedSpec(schemaResult, config) {
@@ -891,7 +999,9 @@ function buildNormalizedSpec(schemaResult, config) {
891
999
  ports: contextSpec.ports,
892
1000
  adapters: contextSpec.adapters,
893
1001
  presentation: contextSpec.presentation,
894
- materialization: contextSpec.materialization
1002
+ materialization: contextSpec.materialization,
1003
+ writeModelStrategy: contextSpec.writeModelStrategy,
1004
+ readModelStrategy: contextSpec.readModelStrategy
895
1005
  };
896
1006
  }
897
1007
 
@@ -925,15 +1035,25 @@ function walkEntityTree(aggregate, visitor) {
925
1035
 
926
1036
  //#endregion
927
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
+ }
928
1047
  function buildNormalizedCompositionSpec(config, contextSpecs) {
929
1048
  assertUniqueCanonicalContextSpecs(contextSpecs);
930
1049
  const infrastructure = resolveInfrastructureStrategy(config.infrastructure);
931
1050
  const resolveProvidedContextRef = createContextRefResolver(contextSpecs);
1051
+ const configuredContextRefs = getConfiguredContextRefs(config);
932
1052
  const resolvedContexts = [];
933
1053
  const resolvedContextSpecs = [];
934
1054
  const seenResolvedModulePaths = /* @__PURE__ */ new Set();
935
1055
  const seenResolvedDisplayNames = /* @__PURE__ */ new Map();
936
- for (const ctxRef of config.contexts) {
1056
+ for (const ctxRef of configuredContextRefs) {
937
1057
  const spec = resolveProvidedContextRef(ctxRef, "Context");
938
1058
  const canonicalModulePath = spec.context.modulePath.toLowerCase();
939
1059
  if (seenResolvedModulePaths.has(canonicalModulePath)) throw new Error(`[composition_normalizer] Context "${ctxRef}" resolves to duplicate composed context "${spec.context.name}" (${spec.context.modulePath})`);
@@ -953,11 +1073,11 @@ function buildNormalizedCompositionSpec(config, contextSpecs) {
953
1073
  validateExternalProjectionSources(resolvedContextSpecs, resolveCompositionContextRef, resolvedContexts.map((context) => context.modulePath));
954
1074
  const crossContextEvents = [];
955
1075
  for (const evt of config.crossContextEvents) {
956
- const sourceSpec = resolveCompositionMemberContextRef(resolveCompositionContextRef, evt.sourceContext, `sourceContext`, `event "${evt.eventType}"`, config.contexts);
1076
+ const sourceSpec = resolveCompositionMemberContextRef(resolveCompositionContextRef, evt.sourceContext, `sourceContext`, `event "${evt.eventType}"`, configuredContextRefs);
957
1077
  const [, eventNamePart] = evt.eventType.split(".");
958
1078
  const resolvedSubs = [];
959
1079
  for (const sub of evt.subscriptions) {
960
- 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);
961
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.`);
962
1082
  const commandNames = targetSpec.commands.map((c) => c.name);
963
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(", ")}`);
@@ -1280,6 +1400,8 @@ function sliceContextIntoAggregateViews(contextSpec) {
1280
1400
  adapters,
1281
1401
  presentation: contextSpec.presentation,
1282
1402
  materialization: contextSpec.materialization,
1403
+ writeModelStrategy: contextSpec.writeModelStrategy,
1404
+ readModelStrategy: contextSpec.readModelStrategy,
1283
1405
  externalValueObjects: externalVos
1284
1406
  };
1285
1407
  });
@@ -1293,10 +1415,55 @@ function aggregateVariableName$1(spec) {
1293
1415
  function aggregateIdPropertyName$1(spec) {
1294
1416
  return `${aggregateVariableName$1(spec)}Id`;
1295
1417
  }
1296
- function defaultIdBrandName(name) {
1418
+ function defaultIdBrandName$1(name) {
1297
1419
  const base = pascalCase(name);
1298
1420
  return base.endsWith("Id") ? base : `${base}Id`;
1299
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
+ }
1300
1467
  function formatFieldType$1(field, indent = 0) {
1301
1468
  if (field.type === "object" && field.nestedFields && field.nestedFields.length > 0) {
1302
1469
  const objectType = `{\n${field.nestedFields.map((nestedField) => {
@@ -1353,6 +1520,10 @@ function getRehydrationGuardName(shape) {
1353
1520
  function annotateCompiledPredicateParameters$1(source) {
1354
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) =>");
1355
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
+ }
1356
1527
  function getRehydrationFieldSpecsName(shape) {
1357
1528
  return `${pascalCase(shape.name)}RehydrationFieldSpecs`;
1358
1529
  }
@@ -1515,9 +1686,9 @@ function renderEffectValue(value, inputVar, selfRef = "this") {
1515
1686
  if (value === "now" || value === "now()") return "new Date()";
1516
1687
  if (value.startsWith("'") && value.endsWith("'")) return `"${value.slice(1, -1)}"`;
1517
1688
  if (/^\d+$/.test(value)) return value;
1518
- 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)}`);
1519
1690
  }
1520
- 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()) {
1521
1692
  const contextName = spec.context.name;
1522
1693
  const aggregateName = pascalCase(spec.aggregate.name);
1523
1694
  const aggregateFieldNames = new Set(filterManagedAggregateFields$1(spec.aggregate.fields ?? []).map((f) => camelCase(f.name)));
@@ -1525,6 +1696,7 @@ function renderEffectLines(effects, inputVar, spec, indent, selfRef = "this", is
1525
1696
  let lastPushedEntityVar = null;
1526
1697
  let lastPushedEntityIdField = null;
1527
1698
  let lastPushedEntityTypeName = null;
1699
+ let lastPushedEntityFields = null;
1528
1700
  const idFieldName = spec.aggregate.idField ? camelCase(spec.aggregate.idField) : null;
1529
1701
  const lines = [];
1530
1702
  for (const effect of effects) switch (effect.kind) {
@@ -1533,7 +1705,11 @@ function renderEffectLines(effects, inputVar, spec, indent, selfRef = "this", is
1533
1705
  if (RESERVED_AGGREGATE_FIELD_NAMES$2.has(camelCase(effect.target))) break;
1534
1706
  const targetField = aggregateFieldsByName.get(camelCase(effect.target));
1535
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);
1536
- 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};`);
1537
1713
  break;
1538
1714
  case "raise-event": {
1539
1715
  if (isCreate) break;
@@ -1546,29 +1722,42 @@ function renderEffectLines(effects, inputVar, spec, indent, selfRef = "this", is
1546
1722
  if (lastPushedEntityIdField && fieldName === camelCase(lastPushedEntityIdField)) return `${fieldName}: ${lastPushedEntityVar}.id.value`;
1547
1723
  if (aggregateFieldNames.has(fieldName)) {
1548
1724
  const aggregateField = aggregateFieldsByName.get(fieldName);
1549
- const aggregateAccess = `${selfRef}.${fieldName}`;
1550
- 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);
1551
1727
  if (aggregateField?.optional) {
1552
- if (availableInputFields.has(fieldName)) return `${fieldName}: ${aggregateAccess} ?? ${inputVar}.${fieldName}`;
1553
- 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}"]`;
1554
1730
  return `${fieldName}: ${aggregateAccess} as ${eventName}Payload["${fieldName}"]`;
1555
1731
  }
1556
1732
  return `${fieldName}: ${aggregateAccess}`;
1557
1733
  }
1558
- if (lastPushedEntityVar && !aggregateFieldNames.has(fieldName)) return `${fieldName}: (${lastPushedEntityVar} as ${lastPushedEntityTypeName ?? "Record<string, unknown>"}).${fieldName}`;
1559
- if (availableInputFields.has(fieldName)) return `${fieldName}: ${inputVar}.${fieldName}`;
1560
- if (fieldName === "occurredAt" || fieldName.endsWith("At")) return `${fieldName}: ${f.type === "date" ? "new Date()" : "new Date().toISOString()"}`;
1561
- if (fieldName === "recordedBy" || fieldName.endsWith("By")) return `${fieldName}: ${f.type === "string" ? "eventContext.recordedBy.value" : "eventContext.recordedBy"}`;
1562
- return `${fieldName}: /* TODO: unmapped ${eventName} payload field "${fieldName}" */ undefined as never`;
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}" */`;
1563
1746
  }).join(", ")} }` : "{}";
1564
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}}));`);
1565
1748
  break;
1566
1749
  }
1567
1750
  case "increment":
1568
- 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
+ }
1569
1755
  break;
1570
1756
  case "decrement":
1571
- 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
+ }
1572
1761
  break;
1573
1762
  case "push-to-collection": {
1574
1763
  const targetChild = spec.aggregate.children?.find((c) => c.collectionFieldName === effect.target);
@@ -1576,10 +1765,19 @@ function renderEffectLines(effects, inputVar, spec, indent, selfRef = "this", is
1576
1765
  const entityName = pascalCase(targetChild.name);
1577
1766
  const createIdFn = `create${entityName}Id`;
1578
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
+ });
1579
1776
  lastPushedEntityVar = entityVar;
1580
1777
  lastPushedEntityIdField = targetChild.idField;
1581
1778
  lastPushedEntityTypeName = entityName;
1582
- lines.push(`${indent}const ${entityVar} = ${entityName}.create({`, `${indent} ${camelCase(targetChild.idField)}: ${createIdFn}(crypto.randomUUID()),`, `${indent} ...${inputVar} as any,`, `${indent}});`, `${indent}${selfRef}.${camelCase(effect.target)}.push(${entityVar});`);
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});`);
1583
1781
  } else lines.push(`${indent}${selfRef}.${camelCase(effect.target)}.push(${renderEffectValue(effect.value, inputVar, selfRef)});`);
1584
1782
  break;
1585
1783
  }
@@ -1594,11 +1792,16 @@ function createAggregateStub(spec) {
1594
1792
  const idField = spec.aggregate.idField;
1595
1793
  const idType = spec.aggregate.idType;
1596
1794
  const nonIdFields = fields.filter((f) => f.name !== idField);
1795
+ const aggregatePropFieldNames = new Set(nonIdFields.map((field) => camelCase(field.name)));
1597
1796
  const commands = spec.commands.filter((c) => c.targetAggregate === spec.aggregate.name);
1598
1797
  const createCmd = commands.find((c) => c.commandKind === "create" && c.effects?.length);
1599
1798
  const mutationCmds = commands.filter((c) => c.commandKind !== "create" && c.effects?.length);
1600
1799
  const mutatedFieldNames = /* @__PURE__ */ new Set();
1601
- 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) {
1602
1805
  for (const eff of cmd.effects) if (eff.kind === "set-field" || eff.kind === "increment" || eff.kind === "decrement") mutatedFieldNames.add(camelCase(eff.target));
1603
1806
  }
1604
1807
  const domainImports = [
@@ -1626,20 +1829,23 @@ function createAggregateStub(spec) {
1626
1829
  const eventImports = [];
1627
1830
  if (mutationRaisedEvents.size > 0) eventImports.push(`import { createDomainEvent } from "../../../../../../lib/domain-event.base.ts";`);
1628
1831
  if (needsEventContextImport) eventImports.push(`import type { EventContext } from "../../../../../shared-kernel/events/event-context.ts";`);
1832
+ let needsBrandedIdToString = false;
1629
1833
  for (const evtName of mutationRaisedEvents) {
1630
1834
  const payloadName = `${pascalCase(evtName)}Payload`;
1631
1835
  const eventFileName = kebabCase(evtName);
1632
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;
1633
1838
  }
1839
+ if (needsBrandedIdToString) domainImports[1] = `import { brandedIdToString, type BrandedId } from "../../../../../../lib/branded-id.ts";`;
1634
1840
  const invariantMap = /* @__PURE__ */ new Map();
1635
1841
  for (const inv of spec.aggregate.invariants ?? []) invariantMap.set(inv.name, inv);
1636
1842
  const domainErrorByInvariant = /* @__PURE__ */ new Map();
1637
1843
  for (const errDef of spec.aggregate.domainErrors ?? []) domainErrorByInvariant.set(errDef.invariantName, errDef);
1638
1844
  const referencedFactoryNames = /* @__PURE__ */ new Set();
1639
1845
  const referencedErrorTypeNames = /* @__PURE__ */ new Set();
1640
- let needsResultImport = false;
1846
+ let needsErrImport = false;
1641
1847
  for (const cmd of mutationCmds) if (cmd.preconditions?.length) {
1642
- needsResultImport = true;
1848
+ needsErrImport = true;
1643
1849
  for (const pre of cmd.preconditions) {
1644
1850
  const errDef = domainErrorByInvariant.get(pre);
1645
1851
  if (errDef) {
@@ -1649,7 +1855,8 @@ function createAggregateStub(spec) {
1649
1855
  }
1650
1856
  }
1651
1857
  const invariantImports = [];
1652
- 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";`);
1653
1860
  if (referencedFactoryNames.size > 0) {
1654
1861
  const aggregateDir = kebabCase(spec.aggregate.name);
1655
1862
  const imports = [...referencedFactoryNames, ...referencedErrorTypeNames].sort();
@@ -1672,21 +1879,41 @@ function createAggregateStub(spec) {
1672
1879
  const classFields = nonIdFields.map((f) => {
1673
1880
  const opt = f.optional ? "?" : "";
1674
1881
  const fieldName = camelCase(f.name);
1675
- return ` ${mutatedFieldNames.has(fieldName) ? "" : "readonly "}${fieldName}${opt}: ${formatFieldType$1(f)};`;
1882
+ return ` private ${mutatedFieldNames.has(fieldName) ? "" : "readonly "}${aggregateBackingFieldName(fieldName)}${opt}: ${formatFieldType$1(f)};`;
1676
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("");
1677
1892
  const collectionFields = (spec.aggregate.children ?? []).map((child) => {
1678
1893
  const entityName = pascalCase(child.name);
1679
1894
  return ` readonly ${camelCase(child.collectionFieldName)}: ${entityName}[] = [];`;
1680
1895
  });
1681
- const constructorAssignments = nonIdFields.map((f) => ` this.${camelCase(f.name)} = props.${camelCase(f.name)};`).join("\n");
1682
- const idTypeStr = `BrandedId<"${idType && idType !== "string" ? pascalCase(idType) : defaultIdBrandName(spec.aggregate.name)}">`;
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)}">`;
1683
1898
  let createBody;
1684
- if (createCmd?.effects?.length) createBody = ` const instance = new ${aggregateClassName}(id, 0, props);
1685
- ${renderEffectLines(createCmd.effects.filter((effect) => effect.kind !== "set-field" || effect.value !== `input.${effect.target}`), "props", spec, " ", "instance", true).join("\n")}
1686
- return instance;`;
1687
- 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));`;
1688
1911
  const fieldTypeMap = /* @__PURE__ */ new Map();
1689
- 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
+ }
1690
1917
  const mutationMethods = [];
1691
1918
  for (const cmd of mutationCmds) {
1692
1919
  const methodName = commandDomainMethodName$1(spec, cmd.name);
@@ -1707,24 +1934,22 @@ ${renderEffectLines(createCmd.effects.filter((effect) => effect.kind !== "set-fi
1707
1934
  if (cmd.lifecycle === "mutate") methodLines.push(` this.incrementVersion();`);
1708
1935
  const cmdEmitsEvents = (cmd.effects ?? []).some((e) => e.kind === "raise-event");
1709
1936
  const inputFields = /* @__PURE__ */ new Map();
1710
- for (const field of cmd.inputFields ?? []) inputFields.set(camelCase(field.name), formatFieldType$1(field));
1711
- for (const eff of cmd.effects ?? []) {
1712
- if (eff.value?.startsWith("input.")) {
1713
- const inputFieldName = camelCase(eff.value.slice(6));
1714
- const targetFieldName = camelCase(eff.target);
1715
- const fieldType = fieldTypeMap.get(targetFieldName) ?? "unknown";
1716
- inputFields.set(inputFieldName, fieldType);
1717
- }
1718
- if (eff.kind === "push-to-collection") {
1719
- const targetChild = spec.aggregate.children?.find((c) => c.collectionFieldName === eff.target);
1720
- if (targetChild) {
1721
- for (const f of targetChild.fields) if (f.name !== targetChild.idField) inputFields.set(camelCase(f.name), formatFieldType$1(f));
1722
- }
1723
- }
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
+ });
1724
1949
  }
1725
- 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>";
1726
1951
  const extraParams = cmdEmitsEvents ? `, eventContext: EventContext` : "";
1727
- 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]))));
1728
1953
  const inputParamName = methodLines.some((line) => /\binput\b/.test(line)) ? "input" : "_input";
1729
1954
  if (resolvedInvariants.length > 0) {
1730
1955
  const errorUnion = resolvedInvariants.map((inv) => `${pascalCase(inv.name)}Error`).join(" | ");
@@ -1762,7 +1987,7 @@ ${methodLines.join("\n")}
1762
1987
  }
1763
1988
  const fromPersistenceProps = nonIdFields.map((f) => ` ${camelCase(f.name)}: state.${camelCase(f.name)},`).join("\n");
1764
1989
  const collectionAssignments = (spec.aggregate.children ?? []).map((child) => ` instance.${camelCase(child.collectionFieldName)}.push(...state.${camelCase(child.collectionFieldName)});`).join("\n");
1765
- return `${imports}
1990
+ return pruneUnusedBrandedIdImport$1(`${imports}
1766
1991
 
1767
1992
  export interface ${aggregateClassName}Props {
1768
1993
  ${propsLines}
@@ -1788,9 +2013,9 @@ ${classFields}${collectionFields.length > 0 ? "\n" + collectionFields.join("\n")
1788
2013
  ${constructorAssignments}
1789
2014
  }
1790
2015
 
1791
- static create(id: ${idTypeStr}, props: ${aggregateClassName}Props): ${aggregateClassName} {
2016
+ static create(id: ${idTypeStr}, props: ${aggregateClassName}Props): Result<${aggregateClassName}, DomainError> {
1792
2017
  ${createBody}
1793
- }
2018
+ }${scalarGetters}
1794
2019
 
1795
2020
  static fromPersistence(state: ${rehydrationStateName}): ${aggregateClassName} {
1796
2021
  ${getRehydrationGuardName({
@@ -1810,15 +2035,15 @@ ${collectionAssignments}
1810
2035
  return instance;
1811
2036
  }${mutationMethods.join("")}
1812
2037
  }
1813
- `;
2038
+ `);
1814
2039
  }
1815
- function createEntityStub(entity, depthFromDomain = 1) {
2040
+ function createEntityStub(entity, _depthFromDomain = 1) {
1816
2041
  const entityName = `${pascalCase(entity.name)}Entity`;
1817
2042
  const fields = entity.fields;
1818
2043
  const idField = entity.idField;
1819
2044
  const idType = entity.idType;
1820
2045
  const nonIdFields = fields.filter((f) => f.name !== idField);
1821
- const idTypeStr = `BrandedId<"${idType && idType !== "string" ? pascalCase(idType) : defaultIdBrandName(entity.name)}">`;
2046
+ const idTypeStr = `BrandedId<"${idType && idType !== "string" ? pascalCase(idType) : defaultIdBrandName$1(entity.name)}">`;
1822
2047
  const libRelPath = "../../../../../lib";
1823
2048
  const imports = [
1824
2049
  `import { Entity } from "${libRelPath}/entity.base.ts";`,
@@ -2095,10 +2320,10 @@ function aggregateIdPropertyName(spec) {
2095
2320
  }
2096
2321
  function metadataFieldFallback(field) {
2097
2322
  const fieldName = camelCase(field.name);
2098
- 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";
2099
2324
  if (fieldName === "correlationId") return "eventContext.correlationId";
2100
2325
  if (fieldName === "causationId") return "eventContext.causationId";
2101
- 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()";
2102
2327
  }
2103
2328
  function findRepositoryPort(spec, targetAggregate) {
2104
2329
  const aggregateName = targetAggregate ?? ("aggregate" in spec ? spec.aggregate.name : void 0);
@@ -2135,7 +2360,7 @@ function formatFieldType(field, indent = 0) {
2135
2360
  function maybeUnwrapBrandedIdExpression(expression, targetField) {
2136
2361
  if (targetField.type !== "string") return expression;
2137
2362
  if (!camelCase(targetField.name).endsWith("Id")) return expression;
2138
- 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})`;
2139
2364
  return targetField.optional ? `${expression} === undefined ? undefined : (${unwrap})` : unwrap;
2140
2365
  }
2141
2366
  function projectValueToTargetField(expression, targetField, sourceField, depth = 0) {
@@ -2143,11 +2368,13 @@ function projectValueToTargetField(expression, targetField, sourceField, depth =
2143
2368
  const sourceNestedFields = new Map((sourceField?.nestedFields ?? []).map((nestedField) => [camelCase(nestedField.name), nestedField]));
2144
2369
  if (targetField.array) {
2145
2370
  const itemVar = `item${depth}`;
2146
- return `${expression}.map((${itemVar}: any) => ({ ${targetField.nestedFields.map((nestedField) => {
2371
+ const objectFields = targetField.nestedFields.map((nestedField) => {
2147
2372
  const nestedName = camelCase(nestedField.name);
2148
2373
  const nestedSourceField = sourceNestedFields.get(nestedName);
2149
2374
  return `${nestedName}: ${projectValueToTargetField(`${itemVar}.${nestedName}`, nestedField, nestedSourceField, depth + 1)}`;
2150
- }).join(", ")} }))`;
2375
+ });
2376
+ sourceField?.nestedFields ?? targetField.nestedFields;
2377
+ return `${expression}.map((${itemVar}) => ({ ${objectFields.join(", ")} }))`;
2151
2378
  }
2152
2379
  return `{ ${targetField.nestedFields.map((nestedField) => {
2153
2380
  const nestedName = camelCase(nestedField.name);
@@ -2200,7 +2427,11 @@ function coerceExpressionToFieldType(expression, targetField, sourceField) {
2200
2427
  return expression;
2201
2428
  }
2202
2429
  function renderInputExpression(value, inputRoot) {
2203
- 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`, "");
2204
2435
  }
2205
2436
  function inferFilterFieldType(query, fieldName) {
2206
2437
  switch (query.outputFields.find((f) => camelCase(f.name) === camelCase(fieldName))?.type ?? "string") {
@@ -2323,8 +2554,6 @@ function createHandlerDepsStub(spec) {
2323
2554
  const contextPascal = pascalCase(spec.context.name);
2324
2555
  const aggregateVar = camelCase(spec.aggregate.name);
2325
2556
  const aggregatePascal = pascalCase(spec.aggregate.name);
2326
- const idType = spec.aggregate.idType;
2327
- idType && idType !== "string" && `${pascalCase(idType)}`;
2328
2557
  const repoPort = findRepositoryPort(spec);
2329
2558
  const repoTypeName = repoPort ? repositoryPortTypeName(repoPort.name) : `${aggregatePascal}Repository`;
2330
2559
  return `import type { Transaction } from "../../../../lib/transaction.ts";
@@ -2453,6 +2682,8 @@ function buildCreateHandlerBody(spec, command) {
2453
2682
  imports.push(`import type { BrandedId } from "../../../../../lib/branded-id.ts";`);
2454
2683
  }
2455
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";`;
2456
2687
  imports.push(`import { createDomainEvent } from "../../../../../lib/domain-event.base.ts";`);
2457
2688
  const payloadName = `${pascalCase(creationEventName)}Payload`;
2458
2689
  imports.push(`import type { ${payloadName} } from "../../domain/events/${kebabCase(creationEventName)}.event.ts";`);
@@ -2464,7 +2695,7 @@ function buildCreateHandlerBody(spec, command) {
2464
2695
  const payloadFields = [];
2465
2696
  for (const f of creationEvent.fields) {
2466
2697
  const fieldName = camelCase(f.name);
2467
- 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,`);
2468
2699
  else if (aggregateChildCollections.has(fieldName)) {
2469
2700
  const aggregateCollectionField = aggregateChildCollections.get(fieldName);
2470
2701
  payloadFields.push(` ${fieldName}: ${projectValueToTargetField(`${aggregateVar}.${fieldName}`, f, aggregateCollectionField)},`);
@@ -2473,10 +2704,11 @@ function buildCreateHandlerBody(spec, command) {
2473
2704
  const aggregateExpr = fieldName === "version" && (f.type === "string" || f.type === "enum") ? `String(${aggregateVar}.version)` : `${aggregateVar}.${fieldName}`;
2474
2705
  payloadFields.push(` ${fieldName}: ${projectValueToTargetField(aggregateExpr, f, aggregateField)},`);
2475
2706
  } else if (commandInputFieldNames.has(fieldName)) payloadFields.push(` ${fieldName}: ${projectValueToTargetField(`command.payload.${fieldName}`, f, commandInputFields.get(fieldName))},`);
2476
- else if (fieldName === "recordedBy" || fieldName === "correlationId" || fieldName === "causationId") {
2477
- const metadataExpr = fieldName === "recordedBy" ? f.type === "string" ? "eventContext.recordedBy.value" : "eventContext.recordedBy" : `eventContext.${fieldName}`;
2478
- payloadFields.push(` ${fieldName}: ${metadataExpr},`);
2479
- } else payloadFields.push(` ${fieldName}: /* TODO: unmapped ${payloadName} field "${fieldName}" */ undefined as never,`);
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
+ }
2480
2712
  }
2481
2713
  creationEventLines = `
2482
2714
  deps.eventCollector.collect([
@@ -2497,9 +2729,10 @@ ${payloadFields.join("\n")}
2497
2729
  }
2498
2730
  const handlerErrorType = `${pascalCase(command.name)}HandlerError`;
2499
2731
  const eventContextParamName = [...propsLines, creationEventLines].some((line) => line.includes("eventContext")) ? "eventContext" : "_eventContext";
2500
- return `${imports.join("\n")}
2732
+ imports.push(`import type { DomainError } from "../../../../../lib/domain-error.ts";`);
2733
+ return pruneUnusedBrandedIdImport(`${imports.join("\n")}
2501
2734
 
2502
- export type ${handlerErrorType} = never;
2735
+ export type ${handlerErrorType} = DomainError;
2503
2736
 
2504
2737
  export async function ${handlerFnName}(
2505
2738
  command: ${commandTypeName},
@@ -2507,16 +2740,18 @@ export async function ${handlerFnName}(
2507
2740
  ${eventContextParamName}: EventContext,
2508
2741
  ): Promise<Result<{ ${idField}: ${idTypeName} }, ${handlerErrorType}>> {
2509
2742
  const id = ${idExpr};
2510
- const ${aggregateVar} = ${aggregateClassName}.create(
2743
+ const created = ${aggregateClassName}.create(
2511
2744
  id,
2512
2745
  {
2513
2746
  ${propsLines.join("\n")}
2514
2747
  },
2515
2748
  );
2749
+ if (!created.ok) return created;
2750
+ const ${aggregateVar} = created.value;
2516
2751
  await deps.repos.${camelCase(spec.aggregate.name)}s.create(${aggregateVar}, deps.tx);${creationEventLines}
2517
2752
  return ok({ ${idField}: ${aggregateVar}.id });
2518
2753
  }
2519
- `;
2754
+ `);
2520
2755
  }
2521
2756
  function buildMutationHandlerBody(spec, command) {
2522
2757
  const commandTypeName = `${pascalCase(command.name)}Command`;
@@ -2597,28 +2832,51 @@ function createCommandHandlerStub(spec, command) {
2597
2832
  if (command.commandKind === "create") return buildCreateHandlerBody(spec, command);
2598
2833
  if (command.loadBy) return buildMutationHandlerBody(spec, command);
2599
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) {
2600
2844
  const commandTypeName = `${pascalCase(command.name)}Command`;
2601
- const handlerFnName = `handle${pascalCase(command.name)}`;
2602
2845
  const depsTypeName = `${pascalCase(spec.context.name)}CommandHandlerDeps`;
2846
+ const contractName = `${pascalCase(command.name)}HandlerContract`;
2603
2847
  return `import type { ${commandTypeName} } from "../commands/${kebabCase(command.name)}.command.ts";
2604
2848
  import type { ${depsTypeName} } from "../handler-deps.ts";
2605
2849
  import type { EventContext } from "../../../../shared-kernel/events/event-context.ts";
2606
2850
 
2607
- export async function ${handlerFnName}(
2608
- command: ${commandTypeName},
2609
- deps: ${depsTypeName},
2610
- eventContext: EventContext,
2611
- ): Promise<never> {
2612
- void command;
2613
- void deps;
2614
- void eventContext;
2615
- throw new Error(
2616
- "[application_generator] Command \\"${spec.context.name}.${command.name}\\" cannot be fully generated. " +
2617
- "Expected a create command with effects or a mutation command with both effects and loadBy.",
2618
- );
2851
+ export interface ${contractName} {
2852
+ execute(
2853
+ command: ${commandTypeName},
2854
+ deps: ${depsTypeName},
2855
+ eventContext: EventContext,
2856
+ ): Promise<void>;
2619
2857
  }
2620
2858
  `;
2621
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
+ };
2878
+ `;
2879
+ }
2622
2880
  function createQueryStub(spec, query) {
2623
2881
  const queryName = `${pascalCase(query.name)}Query`;
2624
2882
  const inputContractName = `${pascalCase(query.name)}Input`;
@@ -2761,17 +3019,26 @@ function projectionWritePortFileName(projectionName) {
2761
3019
  return `${kebabCase(projectionName)}.projection-write.port.ts`;
2762
3020
  }
2763
3021
  function projectionSourcePayloadTypeName$1(source) {
2764
- return `${pascalCase(source.eventName)}Payload`;
3022
+ return `${pascalCase(trimProjectionSourceEventName$1(source.eventName))}Payload`;
2765
3023
  }
2766
- function projectionSourcePayloadImportPath$1(source) {
2767
- return `../../../${normalizeModulePath(source.contextName)}/domain/events/${kebabCase(source.eventName)}.event.ts`;
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"));
2768
3035
  }
2769
3036
  function projectionSourceEventType$1(source) {
2770
3037
  const payloadType = projectionSourcePayloadTypeName$1(source);
2771
3038
  return `EventEnvelope<${payloadType}> | ${payloadType}`;
2772
3039
  }
2773
- function createProjectionWritePortStub(projection) {
2774
- const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionSourcePayloadTypeName$1(source)} } from "${projectionSourcePayloadImportPath$1(source)}";`);
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)}";`);
2775
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>;`);
2776
3043
  return `import type { EventEnvelope } from "../../../../../../lib/event-envelope.ts";
2777
3044
  import type { Transaction } from "../../../../../../lib/transaction.ts";
@@ -2955,7 +3222,14 @@ function buildV5ApplicationArtifacts(spec, options) {
2955
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)));
2956
3223
  for (const command of spec.commands) {
2957
3224
  artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, `application/commands/${kebabCase(command.name)}.command.ts`), createCommandStub(spec, command)));
2958
- 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
+ }
2959
3233
  }
2960
3234
  for (const query of spec.queries) if (query.queryKind === "list") {
2961
3235
  const listQuery = query;
@@ -2980,13 +3254,17 @@ function buildV5ApplicationContextArtifacts(contextSpec) {
2980
3254
  const projections = contextSpec.projections ?? [];
2981
3255
  if (projections.length > 0) {
2982
3256
  artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/ports/projections/index.ts"), createProjectionWritePortsIndexStub(projections)));
2983
- for (const projection of projections) artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, `application/ports/projections/${projectionWritePortFileName(projection.name)}`), createProjectionWritePortStub(projection)));
3257
+ for (const projection of projections) artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, `application/ports/projections/${projectionWritePortFileName(projection.name)}`), createProjectionWritePortStub(modulePath, projection)));
2984
3258
  }
2985
3259
  return artifacts;
2986
3260
  }
2987
3261
 
2988
3262
  //#endregion
2989
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
+ }
2990
3268
  function resolveDrizzlePersistence(infrastructureStrategy) {
2991
3269
  return infrastructureStrategy?.persistence === "mysql" ? "mysql" : "postgres";
2992
3270
  }
@@ -3235,7 +3513,7 @@ function createDrizzleTableDefinition(spec, persistence = "postgres") {
3235
3513
  ...(spec.entities ?? []).map((entity) => scalarFields(entity.fields, entity.children ?? []))
3236
3514
  ].flat();
3237
3515
  const coreImportTokens = [persistence === "mysql" ? "mysqlTable" : "pgTable", "text"];
3238
- if (flatScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) coreImportTokens.push("integer");
3516
+ if (spec.aggregate !== void 0 || flatScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) coreImportTokens.push("integer");
3239
3517
  if (flatScalarFields.some((field) => isDecimalNumberField(field))) coreImportTokens.push("real");
3240
3518
  if (flatScalarFields.some((field) => field.type === "boolean")) coreImportTokens.push("boolean");
3241
3519
  if (flatScalarFields.some((field) => field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0)) coreImportTokens.push(persistence === "mysql" ? "json" : "jsonb");
@@ -3377,7 +3655,12 @@ function mikroOrmPropertyType(field) {
3377
3655
  function createMikroOrmEntitySchemaArtifactContent(spec) {
3378
3656
  const nodes = flattenMikroOrmNodes(buildMikroOrmRootNode(spec));
3379
3657
  const contextPrefix = pascalCase(spec.context.name);
3380
- const imports = [`import { Cascade, EntitySchema${nodes.some((node) => node.children.length > 0) ? ", Collection" : ""} } from "@mikro-orm/core";`];
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";`];
3381
3664
  const classBlocks = nodes.map((node) => {
3382
3665
  const lines = [`export class ${node.className} {`];
3383
3666
  for (const field of node.scalarFields) lines.push(` ${camelCase(field.name)}${field.optional ? "?" : "!"}: ${mikroOrmClassFieldType(field)};`);
@@ -3431,11 +3714,10 @@ function deserializePersistenceFieldValue(field, sourceExpr) {
3431
3714
  return sourceExpr;
3432
3715
  }
3433
3716
  function rehydrateIdExpression(node, sourceExpr) {
3434
- if (node.idType && node.idType !== "string") return `create${pascalCase(node.idType)}(${sourceExpr})`;
3435
- return `createBrandedId("string", ${sourceExpr})`;
3717
+ return `create${pascalCase(node.idType && node.idType !== "string" ? node.idType : defaultIdBrandName(node.name))}(${sourceExpr})`;
3436
3718
  }
3437
- function buildMikroOrmNodeHelperBlock(spec, node) {
3438
- const domainTypeName = node.isAggregateRoot ? `${pascalCase(spec.aggregate.name)}Aggregate` : pascalCase(node.name);
3719
+ function buildMikroOrmNodeHelperBlock(spec, node, domainTypeNameOverride) {
3720
+ const domainTypeName = domainTypeNameOverride ?? (node.isAggregateRoot ? `${pascalCase(spec.aggregate.name)}Aggregate` : pascalCase(node.name));
3439
3721
  const domainVar = "source";
3440
3722
  const recordVar = "target";
3441
3723
  const scalarAssignmentLines = node.scalarFields.map((field) => {
@@ -3451,7 +3733,7 @@ function buildMikroOrmNodeHelperBlock(spec, node) {
3451
3733
  const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
3452
3734
  const childIdField = camelCase(child.idField);
3453
3735
  return ` {
3454
- const currentById = new Map(${recordVar}.${collectionName}.getItems().map((item: ${child.className}) => [String(item.${childIdField}), item] as const));
3736
+ const currentById = new Map<string, ${child.className}>(${recordVar}.${collectionName}.getItems().map((item: ${child.className}) => [String(item.${childIdField}), item] as const));
3455
3737
  const nextItems = ${domainVar}.${collectionName}.map((item) => {
3456
3738
  const existing = currentById.get(String(item.id.value));
3457
3739
  if (existing) {
@@ -3464,7 +3746,7 @@ function buildMikroOrmNodeHelperBlock(spec, node) {
3464
3746
  }`;
3465
3747
  });
3466
3748
  const parentParam = node.parent ? `, parent: ${node.parent.className}` : "";
3467
- const parentAssignment = node.parent ? ` record.${node.parent.propertyName} = parent;\n` : "";
3749
+ const parentAssignment = node.parent ? ` ${recordVar}.${node.parent.propertyName} = parent;\n` : "";
3468
3750
  const parentSyncParam = node.parent ? `, parent?: ${node.parent.className}` : "";
3469
3751
  const parentSyncAssignment = node.parent ? ` if (parent) {\n ${recordVar}.${node.parent.propertyName} = parent;\n }\n` : "";
3470
3752
  const domainScalarLines = node.scalarFields.map((field) => {
@@ -3477,10 +3759,10 @@ function buildMikroOrmNodeHelperBlock(spec, node) {
3477
3759
  domainScalarLines.push(` ${collectionName}: ${recordVar}.${collectionName}.getItems().map((item: ${child.className}) => map${child.className}ToDomain(item)),`);
3478
3760
  }
3479
3761
  return `function create${node.className}FromDomain(source: ${domainTypeName}${parentParam}): ${node.className} {
3480
- const record = new ${node.className}();
3762
+ const ${recordVar} = new ${node.className}();
3481
3763
  ${parentAssignment}${scalarAssignmentLines.join("\n")}
3482
3764
  ${createChildLines.join("\n")}
3483
- return record;
3765
+ return ${recordVar};
3484
3766
  }
3485
3767
 
3486
3768
  function sync${node.className}FromDomain(target: ${node.className}, source: ${domainTypeName}${parentSyncParam}): void {
@@ -3503,9 +3785,11 @@ function createMikroOrmRepository(spec, portName) {
3503
3785
  const repositoryClassName = mikroOrmRepositoryClassName(spec.aggregate.name);
3504
3786
  const rootNode = buildMikroOrmRootNode(spec);
3505
3787
  const nodes = flattenMikroOrmNodes(rootNode);
3506
- const needsBrandedIdHelper = nodes.some((node) => !node.idType || node.idType === "string");
3507
3788
  const customIdImports = /* @__PURE__ */ new Map();
3508
- for (const node of nodes) if (node.idType && node.idType !== "string") customIdImports.set(node.idType, `import { create${pascalCase(node.idType)} } from "${relativePrefix}/core/shared-kernel/entity-ids/${kebabCase(node.idType)}";`);
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
+ }
3509
3793
  const notFoundError = (spec.aggregate.applicationErrors ?? [])[0];
3510
3794
  const notFoundImports = notFoundError ? (() => {
3511
3795
  const typeName = `${pascalCase(notFoundError.aggregateName)}NotFoundError`;
@@ -3520,20 +3804,37 @@ function createMikroOrmRepository(spec, portName) {
3520
3804
  let current = node;
3521
3805
  while (current && !current.isAggregateRoot) {
3522
3806
  segments.unshift(camelCase(current.collectionFieldName ?? current.name));
3523
- current = current.parent ? nodes.find((candidate) => candidate.className === current.parent?.className) : void 0;
3807
+ const parentClassName = current.parent?.className;
3808
+ current = parentClassName ? nodes.find((candidate) => candidate.className === parentClassName) : void 0;
3524
3809
  }
3525
3810
  return segments.join(".");
3526
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");
3527
3819
  return `import type { Result } from "${relativePrefix}/lib/result";
3528
3820
  import { ok, err } from "${relativePrefix}/lib/result";
3529
3821
  import type { Transaction } from "${relativePrefix}/lib/transaction";
3530
3822
  import { ConcurrencyConflictError } from "${relativePrefix}/lib/concurrency-conflict-error";
3531
3823
  import type { ${repositoryPortType} } from "${relativePrefix}/core/contexts/${modulePath}/application/ports/${kebabCase(portName)}.port";
3532
- ${needsBrandedIdHelper ? `import { createBrandedId } from "${relativePrefix}/lib/branded-id";\n` : ""}${[...customIdImports.values()].join("\n")}${customIdImports.size > 0 ? "\n" : ""}${notFoundImports}import { ${aggregateName}Aggregate } from "${relativePrefix}/core/contexts/${modulePath}/domain/aggregates/${aggregateDir}/${aggregateDir}.aggregate";
3533
- ${nodes.filter((node) => !node.isAggregateRoot).map((node) => `import { ${pascalCase(node.name)}Entity as ${pascalCase(node.name)} } from "${relativePrefix}/core/contexts/${modulePath}/domain/entities/${kebabCase(node.name)}.entity";`).join("\n")}${nodes.some((node) => !node.isAggregateRoot) ? "\n" : ""}import { ${nodes.map((node) => node.className).join(", ")} } from "../../entities/${modulePath}/${kebabCase(spec.aggregate.name)}.entity-schema.ts";
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";
3534
3826
 
3535
3827
  const TOUCHED_AGGREGATES_KEY = "__zodmireTouchedAggregates";
3536
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
+
3537
3838
  function markTouchedAggregate(tx: unknown, aggregateType: string, aggregateId: string): void {
3538
3839
  const runtimeTx = tx as Record<string, unknown>;
3539
3840
  const touched = (runtimeTx[TOUCHED_AGGREGATES_KEY] ??= new Map<string, Set<string>>()) as Map<string, Set<string>>;
@@ -3542,11 +3843,11 @@ function markTouchedAggregate(tx: unknown, aggregateType: string, aggregateId: s
3542
3843
  touched.set(aggregateType, ids);
3543
3844
  }
3544
3845
 
3545
- ${nodes.slice().reverse().map((node) => buildMikroOrmNodeHelperBlock(spec, node)).join("\n\n")}
3846
+ ${helperBlocks}
3546
3847
 
3547
3848
  export class ${repositoryClassName} implements ${repositoryPortType} {
3548
3849
  async findById(${rootIdParamName}: ${rootIdTypeName}, tx: Transaction): Promise<Result<${aggregateName}Aggregate, ${notFoundTypeName}>> {
3549
- const em = tx as any;
3850
+ const em = tx as unknown as MikroOrmEntityManager;
3550
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(", ")}]` : "[]"} });
3551
3852
  if (!record) {
3552
3853
  return err(${notFoundFactory}(String(${rootNode.idType && rootNode.idType !== "string" ? `${rootIdParamName}.value` : rootIdParamName})));
@@ -3556,15 +3857,14 @@ export class ${repositoryClassName} implements ${repositoryPortType} {
3556
3857
  }
3557
3858
 
3558
3859
  async create(${camelCase(spec.aggregate.name)}: ${aggregateName}Aggregate, tx: Transaction): Promise<void> {
3559
- const em = tx as any;
3860
+ const em = tx as unknown as MikroOrmEntityManager;
3560
3861
  const rootRecord = create${rootNode.className}FromDomain(${camelCase(spec.aggregate.name)});
3561
3862
  markTouchedAggregate(tx, "${aggregateName}", String(${camelCase(spec.aggregate.name)}.id.value));
3562
- em.persist(rootRecord);
3563
- await em.flush();
3863
+ await em.persist(rootRecord);
3564
3864
  }
3565
3865
 
3566
3866
  async save(${camelCase(spec.aggregate.name)}: ${aggregateName}Aggregate, expectedVersion: number, tx: Transaction): Promise<void> {
3567
- const em = tx as any;
3867
+ const em = tx as unknown as MikroOrmEntityManager;
3568
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(", ")}]` : "[]"} });
3569
3869
 
3570
3870
  if (!current || current.version !== expectedVersion) {
@@ -3578,8 +3878,7 @@ export class ${repositoryClassName} implements ${repositoryPortType} {
3578
3878
 
3579
3879
  sync${rootNode.className}FromDomain(current, ${camelCase(spec.aggregate.name)});
3580
3880
  markTouchedAggregate(tx, "${aggregateName}", String(${camelCase(spec.aggregate.name)}.id.value));
3581
- em.persist(current);
3582
- await em.flush();
3881
+ await em.persist(current);
3583
3882
  }
3584
3883
  }
3585
3884
  `;
@@ -3846,7 +4145,6 @@ function buildNestedPersistenceEqualityHelpers(allEntities) {
3846
4145
  lines.push(``);
3847
4146
  for (const entity of allEntities) {
3848
4147
  const entityName = pascalCase(entity.name);
3849
- camelCase(entity.name);
3850
4148
  const entityIdSnake = snakeCase(entity.idField);
3851
4149
  lines.push(`function equal${entityName}PersistenceRows(currentRows: any[], expectedRows: any[]): boolean {`);
3852
4150
  lines.push(` return equalPersistenceRowSet(currentRows, expectedRows, (row: any) => String(row.${entityIdSnake}), equal${entityName}Persistence);`);
@@ -4021,7 +4319,6 @@ function buildBottomUpAssembly(spec, _allEntities) {
4021
4319
  camelCase(parent.name);
4022
4320
  const parentIdField = parent.idField;
4023
4321
  const fkCol = snakeCase(parentIdField);
4024
- entity.collectionFieldName;
4025
4322
  if (entity.children.length === 0) {
4026
4323
  lines.push(` const ${entityVar}ByParent = new Map<string, any[]>();`);
4027
4324
  lines.push(` for (const r of ${entityVar}Rows) {`);
@@ -4205,7 +4502,7 @@ function buildExpectedChildRowCollectionLines(lines, children, parentAccessor, p
4205
4502
  const childItemVar = `${childVar}Item`;
4206
4503
  const fkCol = snakeCase(parentIdField);
4207
4504
  lines.push(`${indent}for (const ${childItemVar} of ${parentAccessor}.${child.collectionFieldName}) {`);
4208
- 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) });`);
4209
4506
  if (child.children.length > 0) buildExpectedChildRowCollectionLines(lines, child.children, childItemVar, child.idField, indent + " ");
4210
4507
  lines.push(`${indent}}`);
4211
4508
  }
@@ -4218,7 +4515,6 @@ function buildChildSaveBlock(lines, children, parentAccessor, parentIdField, ind
4218
4515
  const childIdField = child.idField;
4219
4516
  const childIdSnake = snakeCase(childIdField);
4220
4517
  const fkCol = snakeCase(parentIdField);
4221
- parentAccessor.includes(".") ? `${parentAccessor}${childIdField}` : `${parentAccessor}`;
4222
4518
  const parentIdValue = parentAccessor.includes(".") ? `${parentAccessor}.${getSimpleIdAccessor(parentIdField)}` : `String(${parentAccessor}.id.value)`;
4223
4519
  const iterVar = `${childVar}Item`;
4224
4520
  lines.push(`${indent}// Upsert ${child.name} children + delete orphans`);
@@ -4227,7 +4523,7 @@ function buildChildSaveBlock(lines, children, parentAccessor, parentIdField, ind
4227
4523
  lines.push(`${indent} and(eq(${childTableVar}.${fkCol}, ${parentIdValue}), notInArray(${childTableVar}.${childIdSnake}, current${pascalCase(child.name)}Ids))`);
4228
4524
  lines.push(`${indent});`);
4229
4525
  lines.push(`${indent}for (const ${iterVar} of ${parentAccessor}.${collectionField}) {`);
4230
- 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} };`);
4231
4527
  lines.push(`${indent} await tx.insert(${childTableVar}).values(${childVar}Row).onConflictDoUpdate({ target: ${childTableVar}.${childIdSnake}, set: ${childVar}Row });`);
4232
4528
  if (child.children.length > 0) buildChildSaveBlock(lines, child.children, iterVar, childIdField, indent + " ");
4233
4529
  lines.push(`${indent}}`);
@@ -4241,23 +4537,24 @@ function buildV5InfrastructureArtifacts(spec, options) {
4241
4537
  const modulePath = normalizeModulePath(spec.context.modulePath);
4242
4538
  const scopeKey = sliceArtifactOwnership(modulePath);
4243
4539
  const artifacts = [];
4540
+ const forceMikroOrmRepositories = options?.infrastructureStrategy?.orm === "mikroorm";
4244
4541
  const hasDrizzleRepositoryAdapter = spec.adapters.some((adapter) => adapter.kind === "drizzle-repository");
4245
- spec.adapters.some((adapter) => adapter.kind === "mikroorm-repository");
4246
- if (!options?.skipContextWideArtifacts && hasDrizzleRepositoryAdapter) {
4542
+ const shouldGenerateDrizzleTables = !forceMikroOrmRepositories && hasDrizzleRepositoryAdapter;
4543
+ if (!options?.skipContextWideArtifacts && shouldGenerateDrizzleTables) {
4247
4544
  const tableContent = createDrizzleTableDefinition(spec, resolveDrizzlePersistence(options?.infrastructureStrategy));
4248
4545
  if (tableContent !== null) {
4249
4546
  const tableLogicalPath = `infrastructure/persistence/${modulePath}/tables.ts`;
4250
4547
  artifacts.push(createGeneratedArtifact(tableLogicalPath, tableContent, scopeKey));
4251
4548
  }
4252
4549
  }
4253
- const repoArtifacts = spec.adapters.flatMap((adapter) => {
4254
- const port = spec.ports.find((p) => p.name === adapter.port);
4255
- if (port === void 0) return [];
4256
- if (adapter.kind === "drizzle-repository") {
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") {
4257
4554
  const logicalPath = buildArtifactPath("infrastructure/persistence/drizzle/repositories", modulePath, `drizzle-${kebabCase(spec.aggregate.name)}.repository.ts`);
4258
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)];
4259
4556
  }
4260
- if (adapter.kind === "mikroorm-repository") {
4557
+ if (repositoryAdapterKind === "mikroorm-repository") {
4261
4558
  const schemaLogicalPath = buildArtifactPath("infrastructure/persistence/mikroorm/entities", modulePath, `${kebabCase(spec.aggregate.name)}.entity-schema.ts`);
4262
4559
  const logicalPath = buildArtifactPath("infrastructure/persistence/mikroorm/repositories", modulePath, `mikroorm-${kebabCase(spec.aggregate.name)}.repository.ts`);
4263
4560
  return [createGeneratedArtifact(schemaLogicalPath, createMikroOrmEntitySchemaArtifactContent(spec), scopeKey), createGeneratedArtifact(logicalPath, createMikroOrmRepository(spec, port.name), scopeKey)];
@@ -4279,6 +4576,7 @@ function createContextDrizzleTableDefinition(contextSpec, persistence = "postgre
4279
4576
  if (!fields || fields.length === 0) continue;
4280
4577
  const children = agg.children ?? [];
4281
4578
  const aggScalarFields = scalarFields(fields, children);
4579
+ usesInteger = true;
4282
4580
  if (aggScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
4283
4581
  if (aggScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
4284
4582
  if (aggScalarFields.some((field) => field.type === "boolean")) usesBoolean = true;
@@ -4408,10 +4706,16 @@ function projectionWritePortImportPath(modulePath, projectionName) {
4408
4706
  return `../../../../../core/contexts/${modulePath}/application/ports/projections/${kebabCase(projectionName)}.projection-write.port.ts`;
4409
4707
  }
4410
4708
  function projectionWriterPayloadTypeName(source) {
4411
- return `${pascalCase(source.eventName)}Payload`;
4709
+ return `${pascalCase(trimEventNameSuffix(source.eventName))}Payload`;
4412
4710
  }
4413
4711
  function projectionWriterPayloadImportPath(source) {
4414
- return `../../../../../core/contexts/${normalizeModulePath(source.contextName)}/domain/events/${kebabCase(source.eventName)}.event.ts`;
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"));
4415
4719
  }
4416
4720
  function projectionWriterEventType(source) {
4417
4721
  const payloadType = projectionWriterPayloadTypeName(source);
@@ -4420,11 +4724,12 @@ function projectionWriterEventType(source) {
4420
4724
  function buildProjectionWriterMethod(readModel, source, executorVariable, executorExpression, persistence = "postgres") {
4421
4725
  const methodName = `on${pascalCase(source.eventName)}`;
4422
4726
  const eventType = projectionWriterEventType(source);
4423
- const payloadLine = ` const payload = (((event as any)?.payload ?? event) ?? {}) as Record<string, unknown>;`;
4727
+ const payloadLine = ` const payload = extractProjectionPayload(event);`;
4424
4728
  const executorLine = ` const ${executorVariable} = ${executorExpression};`;
4425
- if (source.mutation.kind === "delete") {
4426
- const whereClause = source.mutation.matchOn.map((field) => `${snakeCase(field)} = ?`).join(" and ");
4427
- const deleteParams = source.mutation.matchOn.map((field) => `payload.${field}`).join(", ");
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(", ");
4428
4733
  return ` async ${methodName}(event: ${eventType}, tx: Transaction): Promise<void> {
4429
4734
  ${payloadLine}
4430
4735
  ${executorLine}
@@ -4434,12 +4739,22 @@ ${executorLine}
4434
4739
  );
4435
4740
  }`;
4436
4741
  }
4437
- const setEntries = Object.entries(source.mutation.set ?? {});
4438
- const insertColumns = setEntries.map(([fieldName]) => snakeCase(fieldName));
4439
- const insertParams = setEntries.map(([, mapping]) => buildProjectionMutationValueExpression(mapping));
4440
- const conflictColumns = source.mutation.matchOn.map((field) => snakeCase(field));
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));
4441
4756
  const updateAssignments = persistence === "mysql" ? insertColumns.map((column) => `${column} = values(${column})`) : insertColumns.map((column) => `${column} = excluded.${column}`);
4442
- if (source.mutation.kind === "upsert") {
4757
+ if (mutation.kind === "upsert") {
4443
4758
  const upsertClause = persistence === "mysql" ? `on duplicate key update ${updateAssignments.join(", ")}` : `on conflict (${conflictColumns.join(", ")}) do update set ${updateAssignments.join(", ")}`;
4444
4759
  return ` async ${methodName}(event: ${eventType}, tx: Transaction): Promise<void> {
4445
4760
  ${payloadLine}
@@ -4450,16 +4765,18 @@ ${executorLine}
4450
4765
  );
4451
4766
  }`;
4452
4767
  }
4453
- const updateAssignmentsSql = insertColumns.map((column) => `${column} = ?`).join(", ");
4454
- const whereClause = source.mutation.matchOn.map((field) => `${snakeCase(field)} = ?`).join(" and ");
4455
- const whereParams = source.mutation.matchOn.map((field) => `payload.${field}`);
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}`;
4456
4776
  return ` async ${methodName}(event: ${eventType}, tx: Transaction): Promise<void> {
4457
- ${payloadLine}
4458
- ${executorLine}
4459
- await ${executorVariable}.execute(
4460
- ${JSON.stringify(`update ${readModel.tableName} set ${updateAssignmentsSql} where ${whereClause}`)},
4461
- [${[...insertParams, ...whereParams].join(", ")}],
4462
- );
4777
+ void event;
4778
+ void tx;
4779
+ throw new Error(${JSON.stringify(diagnosticMessage)});
4463
4780
  }`;
4464
4781
  }
4465
4782
  function buildProjectionMutationValueExpression(mapping) {
@@ -4470,13 +4787,41 @@ function buildProjectionMutationValueExpression(mapping) {
4470
4787
  function createMikroOrmProjectionWriterArtifact(modulePath, projection, readModel, persistence = "postgres") {
4471
4788
  const className = projectionWriterClassName(projection.name);
4472
4789
  const writePortName = projectionWritePortName$1(projection.name);
4790
+ const writeCapability = projection.capabilities?.writeModel;
4473
4791
  const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionWriterPayloadTypeName(source)} } from "${projectionWriterPayloadImportPath(source)}";`);
4474
- const methodBlocks = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => buildProjectionWriterMethod(readModel, source, "connection", "(tx as any).getConnection()", persistence)).join("\n\n");
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");
4475
4793
  return createGeneratedArtifact(`infrastructure/persistence/mikroorm/projection-writers/${modulePath}/${kebabCase(projection.name)}.projection-writer.ts`, `import type { EventEnvelope } from "../../../../../lib/event-envelope.ts";
4476
4794
  import type { Transaction } from "../../../../../lib/transaction.ts";
4477
4795
  import type { ${writePortName} } from "${projectionWritePortImportPath(modulePath, projection.name)}";
4478
4796
  ${payloadImports.join("\n")}
4479
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
+
4480
4825
  // Generated tx-scoped MikroORM projection writer for "${projection.readModelName}".
4481
4826
  export class ${className} implements ${writePortName} {
4482
4827
  ${methodBlocks}
@@ -4486,13 +4831,25 @@ ${methodBlocks}
4486
4831
  function createDrizzleProjectionWriterArtifact(modulePath, projection, readModel, persistence = "postgres") {
4487
4832
  const className = drizzleProjectionWriterClassName(projection.name);
4488
4833
  const writePortName = projectionWritePortName$1(projection.name);
4834
+ const writeCapability = projection.capabilities?.writeModel;
4489
4835
  const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionWriterPayloadTypeName(source)} } from "${projectionWriterPayloadImportPath(source)}";`);
4490
- const methodBlocks = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => buildProjectionWriterMethod(readModel, source, "executor", "tx as any", persistence)).join("\n\n");
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");
4491
4837
  return createGeneratedArtifact(`infrastructure/persistence/drizzle/projection-writers/${modulePath}/${kebabCase(projection.name)}.projection-writer.ts`, `import type { EventEnvelope } from "../../../../../lib/event-envelope.ts";
4492
4838
  import type { Transaction } from "../../../../../lib/transaction.ts";
4493
4839
  import type { ${writePortName} } from "${projectionWritePortImportPath(modulePath, projection.name)}";
4494
4840
  ${payloadImports.join("\n")}
4495
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
+
4496
4853
  // Generated tx-scoped Drizzle projection writer for "${projection.readModelName}".
4497
4854
  export class ${className} implements ${writePortName} {
4498
4855
  ${methodBlocks}
@@ -4504,8 +4861,10 @@ function buildV5InfrastructureContextArtifacts(contextSpec, options = {}) {
4504
4861
  const scopeKey = sliceArtifactOwnership(modulePath);
4505
4862
  const artifacts = [];
4506
4863
  const readModels = contextSpec.readModels ?? [];
4507
- const tableContent = createContextDrizzleTableDefinition(contextSpec, resolveDrizzlePersistence(options.infrastructureStrategy));
4508
- 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
+ }
4509
4868
  if (readModels.length > 0) {
4510
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));
4511
4870
  artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/drizzle/schema.ts`, createReadModelContextSchemaBarrel({
@@ -4542,7 +4901,7 @@ function createViewModelTablesSkeleton(contextSpec) {
4542
4901
  lines.push(` readModel: "${readModelName}",`);
4543
4902
  lines.push(` contract: "${readModelContractName$1(readModelName)}",`);
4544
4903
  lines.push(` servesQueries: [${queries.map((query) => `"${query.name}"`).join(", ")}],`);
4545
- lines.push(` suggestedTableName: "${snakeCase(modulePath).replaceAll("/", "_")}_${snakeCase(readModelRepositoryFileBase$1(readModelName))}",`);
4904
+ lines.push(` suggestedTableName: "${snakeCase(modulePath).replace(/\//g, "_")}_${snakeCase(readModelRepositoryFileBase$1(readModelName))}",`);
4546
4905
  lines.push(` },`);
4547
4906
  }
4548
4907
  lines.push(`} as const;`);
@@ -4568,7 +4927,6 @@ function createReadModelRepositorySkeleton(modulePath, readModelName, queries) {
4568
4927
  importLines.set("tx", `import type { Transaction } from "../../../../../lib/transaction.ts";`);
4569
4928
  importLines.set("port", `import type { ${portTypeName} } from "../../../../../core/contexts/${modulePath}/application/ports/read-models/${fileBase}.repository.port.ts";`);
4570
4929
  const methodBlocks = queries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((query) => {
4571
- queryReadModelMethodName(query);
4572
4930
  const queryTypeName = `${pascalCase(query.name)}Query`;
4573
4931
  const outputTypeName = queryOutputContractName(query);
4574
4932
  const viewFileBase = queryViewFileBase(query);
@@ -4785,12 +5143,15 @@ function createSingleReadModelMethodSkeleton(query, readModelName) {
4785
5143
  const queryTypeName = `${pascalCase(query.name)}Query`;
4786
5144
  const outputTypeName = queryOutputContractName(query);
4787
5145
  const targetName = readModelName ?? resolvedReadModelName$1(query);
5146
+ const capabilityReasons = query.capability?.reasons ?? [];
5147
+ const diagnosticReasons = capabilityReasons.length > 0 ? ` Reasons: ${capabilityReasons.join("; ")}.` : "";
4788
5148
  if (query.queryKind === "list") return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<PaginatedResult<${outputTypeName}>> {
4789
5149
  void this.db;
5150
+ void query;
4790
5151
  void tx;
4791
5152
  throw new Error(
4792
5153
  "[infrastructure_generator] Query \\"${query.name}\\" for read-model \\"${targetName}\\" cannot be fully generated. " +
4793
- "Ensure output fields map directly to read-model fields and list-query metadata is fully declared.",
5154
+ "Ensure output fields map directly to read-model fields and list-query metadata is fully declared." + ${JSON.stringify(diagnosticReasons)},
4794
5155
  );
4795
5156
  }`;
4796
5157
  return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<${outputTypeName}> {
@@ -4799,7 +5160,7 @@ function createSingleReadModelMethodSkeleton(query, readModelName) {
4799
5160
  void tx;
4800
5161
  throw new Error(
4801
5162
  "[infrastructure_generator] Query \\"${query.name}\\" for read-model \\"${targetName}\\" cannot be fully generated. " +
4802
- "Ensure output fields map directly to read-model fields and findById queries resolve a stable lookup field.",
5163
+ "Ensure output fields map directly to read-model fields and findById queries resolve a stable lookup field." + ${JSON.stringify(diagnosticReasons)},
4803
5164
  );
4804
5165
  }`;
4805
5166
  }
@@ -4822,10 +5183,19 @@ function projectionSourceHandlerName(eventName) {
4822
5183
  return `on${pascalCase(eventName)}`;
4823
5184
  }
4824
5185
  function projectionSourcePayloadTypeName(source) {
4825
- return `${pascalCase(source.eventName)}Payload`;
5186
+ return `${pascalCase(trimProjectionSourceEventName(source.eventName))}Payload`;
4826
5187
  }
4827
5188
  function projectionSourcePayloadImportPath(currentModulePath, source) {
4828
- return `../../${normalizeModulePath(source.contextName)}/domain/events/${kebabCase(source.eventName)}.event.ts`;
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"));
4829
5199
  }
4830
5200
  function projectionSourceEventType(source) {
4831
5201
  const payloadType = projectionSourcePayloadTypeName(source);
@@ -4867,8 +5237,10 @@ function buildProjectionRebuildArtifact(modulePath, projection) {
4867
5237
  const projectorClassName = projection.name;
4868
5238
  const rebuildFunctionName = `rebuild${pascalCase(projection.name)}`;
4869
5239
  const batchSize = projection.rebuild.batchSize ?? 500;
5240
+ const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionSourcePayloadTypeName(source)} } from "${projectionSourcePayloadImportPath(modulePath, source)}";`);
4870
5241
  return createGeneratedArtifact(`core/contexts/${modulePath}/application/projections/${fileBase}.rebuild.ts`, `import type { Transaction } from "../../../../../lib/transaction.ts";
4871
5242
  import type { ${projectorClassName} } from "./${fileBase}.projector.ts";
5243
+ ${payloadImports.join("\n")}
4872
5244
 
4873
5245
  export type ProjectionRebuildBatch = {
4874
5246
  readonly events: Array<{
@@ -4907,7 +5279,10 @@ export async function ${rebuildFunctionName}(deps: ${pascalCase(projection.name)
4907
5279
  for (const event of batch.events) {
4908
5280
  switch (event.type) {
4909
5281
  ${projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => ` case "${source.contextName}.${source.aggregateName}.${source.eventName}":
4910
- await deps.projector.${projectionSourceHandlerName(source.eventName)}(event, deps.transaction);
5282
+ await deps.projector.${projectionSourceHandlerName(source.eventName)}(
5283
+ event.payload as ${projectionSourcePayloadTypeName(source)},
5284
+ deps.transaction,
5285
+ );
4911
5286
  break;`).join("\n")}
4912
5287
  default:
4913
5288
  break;
@@ -4932,7 +5307,7 @@ function buildProjectionSubscriptionsArtifact(contextSpec) {
4932
5307
  const builderName = `build${pascalCase(contextSpec.context.name)}ProjectionSubscriptions`;
4933
5308
  const dependencyLines = contextSpec.projections.map((projection) => ` ${projectionVariableName(projection.name)}: ${projection.name};`);
4934
5309
  const importLines = contextSpec.projections.map((projection) => `import type { ${projection.name} } from "../../../core/contexts/${modulePath}/application/projections/${projectionFileBase(projection.name)}.projector.ts";`);
4935
- const eventImports = contextSpec.projections.flatMap((projection) => projection.sources.map((source) => `import type { ${projectionSourcePayloadTypeName(source)} } from "../../../core/contexts/${normalizeModulePath(source.contextName)}/domain/events/${kebabCase(source.eventName)}.event.ts";`));
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";`));
4936
5311
  const subscriptionEntries = contextSpec.projections.flatMap((projection) => projection.sources.map((source) => {
4937
5312
  const subscriptionName = projection.subscription?.subscriptionName ?? projection.name;
4938
5313
  const consumerGroup = projection.subscription?.consumerGroup;
@@ -4942,7 +5317,7 @@ function buildProjectionSubscriptionsArtifact(contextSpec) {
4942
5317
  subscriptionName: "${subscriptionName}",
4943
5318
  eventName: "${source.contextName}.${source.aggregateName}.${source.eventName}",
4944
5319
  role: "${source.role}",${consumerGroupLine}
4945
- handle: (event: ${projectionSourceEventType(source)}, 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),
4946
5321
  }`;
4947
5322
  }));
4948
5323
  return createGeneratedArtifact(`infrastructure/messaging/projection-subscriptions/${modulePath}.ts`, `import type { EventEnvelope } from "../../../lib/event-envelope.ts";
@@ -5033,23 +5408,24 @@ function buildV5TestArtifacts(spec) {
5033
5408
  const failSupply = violating ? `{ ${violating.field}: "${violating.value}" }` : `{}`;
5034
5409
  const passSupply = passing ? `{ ${passing.field}: "${passing.value}" }` : violating ? `{ ${violating.field}: "active" }` : `{}`;
5035
5410
  const eventContextSetup = cmdHasEvents ? `\n const eventContext = createTestEventContext();` : "";
5411
+ const inputSetup = `\n const input = {} as Parameters<${aggPascal}Aggregate["${methodName}"]>[0];`;
5036
5412
  describes.push(`
5037
5413
  describe("${cmd.name} - ${inv.name} invariant", () => {
5038
5414
  it("rejects ${methodName} when ${inv.name} is violated", () => {
5039
5415
  const aggregate = ${aggPascal}Aggregate.fromPersistence(
5040
5416
  sampleValid(${aggPascal}RehydrationSchema, ${failSupply}),
5041
- );${eventContextSetup}
5417
+ );${inputSetup}${eventContextSetup}
5042
5418
  const result = aggregate.${methodName}(${methodCallArgs});
5043
- assert(!result.ok);
5044
- assert(result.error.type === "${errorTypeDiscriminator}");
5419
+ expect(result.ok).toBe(false);
5420
+ expect(result.error.type).toBe("${errorTypeDiscriminator}");
5045
5421
  });
5046
5422
 
5047
5423
  it("allows ${methodName} when ${inv.name} is satisfied", () => {
5048
5424
  const aggregate = ${aggPascal}Aggregate.fromPersistence(
5049
5425
  sampleValid(${aggPascal}RehydrationSchema, ${passSupply}),
5050
- );${eventContextSetup}
5426
+ );${inputSetup}${eventContextSetup}
5051
5427
  const result = aggregate.${methodName}(${methodCallArgs});
5052
- assert(result.ok);
5428
+ expect(result.ok).toBe(true);
5053
5429
  });
5054
5430
  });`);
5055
5431
  }
@@ -5059,20 +5435,21 @@ function buildV5TestArtifacts(spec) {
5059
5435
  const hasCustomIdType = idType && idType !== "string";
5060
5436
  if (createCommand) {
5061
5437
  let createIdExpr;
5062
- if (hasCustomIdType) {
5063
- const idTypePascal = pascalCase(idType);
5064
- extraImports.push(`import { create${idTypePascal} } from "../../../../../../shared-kernel/entity-ids/${kebabCase(idType)}.ts";`);
5065
- createIdExpr = `create${idTypePascal}(crypto.randomUUID())`;
5066
- } else {
5067
- extraImports.push(`import { createBrandedId } from "../../../../../../../lib/branded-id.ts";`);
5068
- createIdExpr = `createBrandedId("string", crypto.randomUUID())`;
5069
- }
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())`;
5070
5442
  describes.push(`
5071
5443
  describe("version semantics", () => {
5072
5444
  it("should create with version 0", () => {
5073
5445
  const id = ${createIdExpr};
5074
- const aggregate = ${aggPascal}Aggregate.create(id, { ...input });
5075
- 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);
5076
5453
  });
5077
5454
  });`);
5078
5455
  }
@@ -5093,13 +5470,14 @@ function buildV5TestArtifacts(spec) {
5093
5470
  break;
5094
5471
  }
5095
5472
  const eventContextSetup = cmdHasEvents ? `\n const eventContext = createTestEventContext();` : "";
5473
+ const inputSetup = `\n const input = {} as Parameters<${aggPascal}Aggregate["${methodName}"]>[0];`;
5096
5474
  describes.push(`
5097
5475
  describe("${cmd.name} - version semantics", () => {
5098
5476
  it("should increment version on successful ${methodName}", () => {
5099
5477
  const aggregate = ${aggPascal}Aggregate.fromPersistence(
5100
5478
  sampleValid(${aggPascal}RehydrationSchema, ${passSupply}),
5101
5479
  );
5102
- const initialVersion = aggregate.version;${eventContextSetup}
5480
+ const initialVersion = aggregate.version;${inputSetup}${eventContextSetup}
5103
5481
  const result = aggregate.${methodName}(${methodCallArgs});
5104
5482
  expect(result.ok).toBe(true);
5105
5483
  expect(aggregate.version).toBe(initialVersion + 1);
@@ -5109,7 +5487,7 @@ function buildV5TestArtifacts(spec) {
5109
5487
  const aggregate = ${aggPascal}Aggregate.fromPersistence(
5110
5488
  sampleValid(${aggPascal}RehydrationSchema, ${failSupply}),
5111
5489
  );
5112
- const initialVersion = aggregate.version;${eventContextSetup}
5490
+ const initialVersion = aggregate.version;${inputSetup}${eventContextSetup}
5113
5491
  const result = aggregate.${methodName}(${methodCallArgs});
5114
5492
  expect(result.ok).toBe(false);
5115
5493
  expect(aggregate.version).toBe(initialVersion);
@@ -5117,15 +5495,14 @@ function buildV5TestArtifacts(spec) {
5117
5495
  });`);
5118
5496
  }
5119
5497
  if (describes.length === 0) return artifacts;
5120
- const usesAssert = mutationCommandsWithPreconditions.length > 0;
5121
5498
  const usesSampleValid = mutationCommandsWithPreconditions.length > 0;
5122
- const usesInput = Boolean(createCommand);
5123
5499
  const testPath = `core/contexts/${modulePath}/domain/aggregates/${aggName}/__tests__/${aggName}.test.ts`;
5124
- const vitestImports = ["describe", "it"];
5125
- if (usesAssert) vitestImports.push("assert");
5126
- vitestImports.push("expect");
5127
5500
  const imports = [
5128
- `import { ${vitestImports.join(", ")} } from "vitest";`,
5501
+ `import { ${[
5502
+ "describe",
5503
+ "it",
5504
+ "expect"
5505
+ ].join(", ")} } from "vitest";`,
5129
5506
  ...usesSampleValid ? [`import type { z } from "zod";`] : [],
5130
5507
  ...usesSampleValid ? [`import { ${aggPascal}RehydrationSchema } from "../${aggName}.schema.ts";`] : [],
5131
5508
  `import { ${aggPascal}Aggregate } from "../${aggName}.aggregate.ts";`,
@@ -5137,11 +5514,7 @@ function sampleValid<T>(schema: z.ZodType<T>, overrides: Partial<T> = {}): T {
5137
5514
  void schema;
5138
5515
  return overrides as T;
5139
5516
  }` : "";
5140
- const inputStub = usesInput ? `
5141
-
5142
- // TODO: Provide a valid input object matching the command's input type
5143
- const input = {} as any;` : "";
5144
- const content = `${imports.join("\n")}${sampleValidHelper}${inputStub}
5517
+ const content = `${imports.join("\n")}${sampleValidHelper}
5145
5518
 
5146
5519
  describe("${aggPascal}Aggregate", () => {${describes.join("\n")}
5147
5520
  });
@@ -5167,7 +5540,7 @@ export class TestCommandBus implements CommandBusPort {
5167
5540
  type: TCommand["type"],
5168
5541
  handler: (command: TCommand, eventContext: EventContext) => Promise<Result<unknown, unknown>>,
5169
5542
  ): void {
5170
- this.handlers.set(type, handler as any);
5543
+ this.handlers.set(type, (command, eventContext) => handler(command as TCommand, eventContext));
5171
5544
  }
5172
5545
 
5173
5546
  async execute<TResult, TCommand extends CommandEnvelope>(
@@ -5435,6 +5808,21 @@ function buildCommandPayloadCanonicalDefinition(command) {
5435
5808
  runtimeSchema: z.object(Object.fromEntries(fields.map((field) => [field.name, field.runtimeSchema])))
5436
5809
  };
5437
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
+ }
5438
5826
  function createCommandPayloadSchemaArtifactContent(definition) {
5439
5827
  const fieldSpecsLiteral = JSON.stringify(definition.fields.map((field) => ({
5440
5828
  name: field.name,
@@ -5470,28 +5858,36 @@ function createCommandProcedure(spec, procedure, command) {
5470
5858
  const commandTypeName = `${pascalCase(command.name)}Command`;
5471
5859
  const commandType = `${contextName}.${pascalCase(command.name)}`;
5472
5860
  const guardName = getCommandPayloadGuardName(command);
5473
- 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 }) => {
5474
5865
  ${guardName}(input);
5475
5866
  const command: ${commandTypeName} = { type: "${commandType}", payload: input };
5476
5867
  return await dispatchCommand(deps.commandBus, command, ctx);
5477
- }`;
5868
+ })`;
5478
5869
  }
5479
5870
  function createQueryProcedure(spec, procedure, query) {
5480
5871
  const queryTypeName = `${pascalCase(query.name)}Query`;
5481
- 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 }) => {
5482
5876
  const queryRequest: ${queryTypeName} = {
5483
5877
  type: "${pascalCase(spec.context.name)}.${pascalCase(query.name)}",
5484
5878
  payload: input,
5485
5879
  };
5486
5880
  return await deps.queryBus.execute(queryRequest);
5487
- }`;
5488
- return ` async ${procedure.name}(input: any) {
5881
+ })`;
5882
+ return ` ${procedure.name}: publicProcedure
5883
+ .input(${payloadSchemaName})
5884
+ .query(async ({ input }) => {
5489
5885
  const queryRequest: ${queryTypeName} = {
5490
5886
  type: "${pascalCase(spec.context.name)}.${pascalCase(query.name)}",
5491
5887
  payload: input,
5492
5888
  };
5493
5889
  return await deps.queryBus.execute(queryRequest);
5494
- }`;
5890
+ })`;
5495
5891
  }
5496
5892
  function buildProcedureBody(spec, procedure) {
5497
5893
  if (procedure.kind === "command") {
@@ -5508,7 +5904,8 @@ function collectImports(spec) {
5508
5904
  const imports = new Set([
5509
5905
  "import type { CommandBusPort } from \"../../../core/ports/messaging/command-bus.port.ts\";",
5510
5906
  "import type { QueryBusPort } from \"../../../core/ports/messaging/query-bus.port.ts\";",
5511
- "import { dispatchCommand } from \"../dispatch-command.ts\";"
5907
+ "import { dispatchCommand } from \"../dispatch-command.ts\";",
5908
+ "import { publicProcedure, router } from \"../trpc-init.ts\";"
5512
5909
  ]);
5513
5910
  for (const procedure of spec.presentation.procedures) {
5514
5911
  if (procedure.kind === "command") {
@@ -5518,12 +5915,14 @@ function collectImports(spec) {
5518
5915
  const commandFileName = kebabCase(command.name);
5519
5916
  imports.add(`import type { ${commandTypeName} } from "../../../core/contexts/${modulePath}/application/commands/${commandFileName}.command.ts";`);
5520
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";`);
5521
5919
  continue;
5522
5920
  }
5523
5921
  const query = spec.queries.find((q) => q.name === procedure.operation);
5524
5922
  if (!query) continue;
5525
5923
  const queryFileName = kebabCase(query.name);
5526
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";`);
5527
5926
  }
5528
5927
  return [...imports].sort().join("\n");
5529
5928
  }
@@ -5536,9 +5935,9 @@ export function ${routerFactoryName}(deps: {
5536
5935
  commandBus: CommandBusPort;
5537
5936
  queryBus: QueryBusPort;
5538
5937
  }) {
5539
- return {
5938
+ return router({
5540
5939
  ${procedureBodies}
5541
- };
5940
+ });
5542
5941
  }
5543
5942
  `;
5544
5943
  }
@@ -5598,6 +5997,16 @@ export type TrpcContextRequirements = {
5598
5997
  };
5599
5998
  `, sharedArtifactOwnership("shared"));
5600
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
+ }
5601
6010
  function buildV5PresentationArtifacts(spec) {
5602
6011
  if (spec.presentation.procedures.length === 0) return [];
5603
6012
  const logicalPath = buildArtifactPath("presentation/trpc/routers", "", `${kebabCase(spec.presentation.trpcRouter)}.router.ts`);
@@ -5606,11 +6015,17 @@ function buildV5PresentationArtifacts(spec) {
5606
6015
  const definition = buildCommandPayloadCanonicalDefinition(command);
5607
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))];
5608
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
+ });
5609
6022
  return [
5610
6023
  createGeneratedArtifact(logicalPath, createRouterStub(spec), sliceArtifactOwnership(scopeKey)),
5611
6024
  ...commandSupportArtifacts,
6025
+ ...querySupportArtifacts,
5612
6026
  buildDispatchCommandArtifact(),
5613
- buildContextRequirementsArtifact()
6027
+ buildContextRequirementsArtifact(),
6028
+ buildTrpcInitArtifact()
5614
6029
  ];
5615
6030
  }
5616
6031
 
@@ -5673,6 +6088,14 @@ export function fromPersistence<TBrand extends string>(
5673
6088
  ): BrandedId<TBrand> {
5674
6089
  return { value, _brand: brand } as BrandedId<TBrand>;
5675
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
+ }
5676
6099
  `;
5677
6100
  }
5678
6101
  function createEventEnvelope() {
@@ -6019,32 +6442,110 @@ function buildConsumerAclArtifacts(contextSpec) {
6019
6442
 
6020
6443
  //#endregion
6021
6444
  //#region packages/core/orchestrator.ts
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
+ ]);
6022
6530
  function generateContextArtifacts(contextSpec, options = {}) {
6023
- const perAggregateArtifacts = sliceContextIntoAggregateViews(contextSpec).flatMap((view) => {
6024
- const ownership = sliceArtifactOwnership(`${view.context.modulePath}/${kebabCase(view.aggregate.name)}`);
6025
- return [
6026
- ...applyOwnershipIfMissing(buildV5DomainArtifacts(view), ownership),
6027
- ...applyOwnershipIfMissing(buildV5ApplicationArtifacts(view, { skipContextWideArtifacts: true }), ownership),
6028
- ...buildV5InfrastructureArtifacts(view, {
6029
- skipContextWideArtifacts: true,
6030
- infrastructureStrategy: options.infrastructureStrategy
6031
- }),
6032
- ...applyOwnershipIfMissing(buildV5TestArtifacts(view), ownership)
6033
- ];
6034
- });
6035
- const ctxOwnership = contextArtifactOwnership(contextSpec.context.modulePath);
6036
- const infraOwnership = sliceArtifactOwnership(contextSpec.context.modulePath);
6037
- const perContextArtifacts = [
6038
- ...buildV5LibArtifacts(options.infrastructureStrategy),
6039
- ...buildV5SharedKernelArtifacts(contextSpec),
6040
- ...buildV5PortArtifacts(),
6041
- ...applyOwnershipIfMissing(buildV5ApplicationContextArtifacts(contextSpec), ctxOwnership),
6042
- ...applyOwnershipIfMissing(buildConsumerAclArtifacts(contextSpec), ctxOwnership),
6043
- ...applyOwnershipIfMissing(buildProjectionArtifacts(contextSpec), ctxOwnership),
6044
- ...applyOwnershipIfMissing(buildV5InfrastructureContextArtifacts(contextSpec, { infrastructureStrategy: options.infrastructureStrategy }), infraOwnership),
6045
- ...applyOwnershipIfMissing(buildV5PresentationArtifacts(contextSpec), ctxOwnership),
6046
- ...applyOwnershipIfMissing(buildV5RouteArtifacts(contextSpec), ctxOwnership)
6047
- ];
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) ?? []);
6048
6549
  return mergeGeneratedArtifacts([...perAggregateArtifacts, ...perContextArtifacts]).sort((a, b) => a.logicalPath.localeCompare(b.logicalPath));
6049
6550
  }
6050
6551
 
@@ -6659,42 +7160,66 @@ export type IntegrationEventHandler<TPayload> = (
6659
7160
 
6660
7161
  export type EventSubscription = {
6661
7162
  readonly eventType: string;
6662
- readonly handler: IntegrationEventHandler<unknown>;
7163
+ readonly handler: IntegrationEventHandler<any>;
6663
7164
  };
6664
7165
  `;
6665
7166
  }
6666
7167
  function createTransactionManager() {
6667
7168
  return `export interface TransactionManager {
6668
- 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>;
6669
7171
  }
6670
7172
  `;
6671
7173
  }
6672
7174
  function createDrizzleTransactionManager() {
6673
7175
  return `import type { TransactionManager } from "./transaction-manager.ts";
6674
7176
 
7177
+ type DrizzleTransactionalDatabase = {
7178
+ transaction<T>(work: (tx: unknown) => Promise<T>): Promise<T>;
7179
+ };
7180
+
6675
7181
  export class DrizzleTransactionManager implements TransactionManager {
6676
- constructor(private readonly db: unknown) {}
7182
+ constructor(private readonly db: DrizzleTransactionalDatabase) {}
6677
7183
 
6678
- async withTransaction<T>(work: (tx: unknown) => Promise<T>): Promise<T> {
7184
+ async withTransaction<TTx, T>(work: (tx: TTx) => Promise<T>): Promise<T> {
6679
7185
  // Drizzle transaction wrapper
6680
- return (this.db as any).transaction(async (tx: unknown) => {
6681
- return work(tx);
7186
+ return this.db.transaction(async (tx: unknown) => {
7187
+ return work(tx as TTx);
6682
7188
  });
6683
7189
  }
7190
+
7191
+ async flush(_tx: unknown): Promise<void> {
7192
+ // Drizzle executes statements eagerly inside the transaction callback.
7193
+ }
6684
7194
  }
6685
7195
  `;
6686
7196
  }
6687
7197
  function createMikroOrmTransactionManager() {
6688
7198
  return `import type { TransactionManager } from "./transaction-manager.ts";
6689
7199
 
7200
+ type MikroOrmTransactionalEntityManager = {
7201
+ transactional<T>(work: (tx: unknown) => Promise<T>): Promise<T>;
7202
+ };
7203
+
6690
7204
  export class MikroOrmTransactionManager implements TransactionManager {
6691
7205
  constructor(private readonly em: unknown) {}
6692
7206
 
6693
- async withTransaction<T>(work: (tx: unknown) => Promise<T>): Promise<T> {
6694
- return (this.em as any).transactional(async (tx: unknown) => {
6695
- return work(tx);
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);
6696
7211
  });
6697
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
+ }
6698
7223
  }
6699
7224
  `;
6700
7225
  }
@@ -6793,9 +7318,9 @@ export class CommandBusImpl implements CommandBusPort {
6793
7318
  // register() is NOT on CommandBusPort — it is implementation-specific.
6794
7319
  private handlers = new Map<string, RegisteredHandler>();
6795
7320
 
6796
- register<TCommand extends CommandEnvelope>(
7321
+ register<TCommand extends CommandEnvelope, TDeps extends HandlerDeps = HandlerDeps>(
6797
7322
  type: TCommand["type"],
6798
- handler: (command: TCommand, deps: HandlerDeps, eventContext: EventContext) => Promise<Result<unknown, unknown>>,
7323
+ handler: (command: TCommand, deps: TDeps, eventContext: EventContext) => Promise<Result<unknown, unknown>>,
6799
7324
  contextKey = this.resolveCommandContext(type),
6800
7325
  ): void {
6801
7326
  this.handlers.set(type, {
@@ -6865,6 +7390,7 @@ export class CommandBusImpl implements CommandBusPort {
6865
7390
  throw new TransactionAbortError(result.error);
6866
7391
  }
6867
7392
 
7393
+ await this.txManager.flush(tx);
6868
7394
  aggregateEventTracker.releaseInto(eventCollector);
6869
7395
  const events = eventCollector.drain();
6870
7396
 
@@ -6912,7 +7438,7 @@ function buildCompositionBusArtifacts() {
6912
7438
  //#region packages/core/generators/composition_outbox.ts
6913
7439
  function createOutboxTable(infrastructureStrategy) {
6914
7440
  if (infrastructureStrategy?.persistence === "mysql") return `import { sql } from "drizzle-orm";
6915
- import { int, json, mysqlTable, timestamp, varchar } from "drizzle-orm/mysql-core";
7441
+ import { index, int, json, mysqlTable, timestamp, varchar } from "drizzle-orm/mysql-core";
6916
7442
 
6917
7443
  export const outboxEvents = mysqlTable("outbox_events", {
6918
7444
  id: varchar("id", { length: 36 }).primaryKey().default(sql\`(uuid())\`),
@@ -6926,9 +7452,11 @@ export const outboxEvents = mysqlTable("outbox_events", {
6926
7452
  processedAt: timestamp("processed_at"),
6927
7453
  failedAt: timestamp("failed_at"),
6928
7454
  error: json("error"),
6929
- });
7455
+ }, (table) => ({
7456
+ statusOccurredAtIdx: index("outbox_status_occurred_at_idx").on(table.status, table.occurredAt),
7457
+ }));
6930
7458
  `;
6931
- return `import { pgTable, uuid, varchar, jsonb, timestamp, integer } from "drizzle-orm/pg-core";
7459
+ return `import { index, pgTable, uuid, varchar, jsonb, timestamp, integer } from "drizzle-orm/pg-core";
6932
7460
 
6933
7461
  export const outboxEvents = pgTable("outbox_events", {
6934
7462
  id: uuid("id").primaryKey().defaultRandom(),
@@ -6942,29 +7470,36 @@ export const outboxEvents = pgTable("outbox_events", {
6942
7470
  processedAt: timestamp("processed_at"),
6943
7471
  failedAt: timestamp("failed_at"),
6944
7472
  error: jsonb("error"),
6945
- });
7473
+ }, (table) => ({
7474
+ statusOccurredAtIdx: index("outbox_status_occurred_at_idx").on(table.status, table.occurredAt),
7475
+ }));
6946
7476
  `;
6947
7477
  }
6948
7478
  function createOutboxWriter() {
6949
7479
  return `import { outboxEvents } from "./outbox.table.ts";
6950
7480
  import type { EventEnvelope } from "../../lib/event-envelope.ts";
6951
7481
 
7482
+ type OutboxInsertExecutor = {
7483
+ insert(table: typeof outboxEvents): {
7484
+ values(row: Record<string, unknown>): Promise<unknown>;
7485
+ };
7486
+ };
7487
+
6952
7488
  export class OutboxWriter {
6953
- async append(tx: unknown, events: EventEnvelope<unknown>[]): Promise<void> {
7489
+ async append(tx: OutboxInsertExecutor, events: EventEnvelope<unknown>[]): Promise<void> {
6954
7490
  if (events.length === 0) return;
6955
7491
 
6956
- const db = tx as any;
6957
7492
  for (const event of events) {
6958
- await db.insert(outboxEvents).values({
7493
+ await tx.insert(outboxEvents).values({
6959
7494
  eventType: event.eventType,
6960
7495
  eventVersion: event.eventVersion,
6961
- payload: event.payload,
6962
- metadata: {
7496
+ payload: JSON.stringify(event.payload),
7497
+ metadata: JSON.stringify({
6963
7498
  correlationId: event.correlationId,
6964
7499
  causationId: event.causationId,
6965
7500
  recordedBy: event.recordedBy,
6966
7501
  occurredAt: event.occurredAt,
6967
- },
7502
+ }),
6968
7503
  occurredAt: event.occurredAt,
6969
7504
  });
6970
7505
  }
@@ -6972,9 +7507,52 @@ export class OutboxWriter {
6972
7507
  }
6973
7508
  `;
6974
7509
  }
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
+ }
6975
7550
  function createOutboxDispatcher() {
6976
7551
  return `import { asc, eq } from "drizzle-orm";
6977
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";
6978
7556
  import type {
6979
7557
  EventSubscription,
6980
7558
  IntegrationEvent,
@@ -6999,13 +7577,42 @@ type HandlerError = {
6999
7577
  error: string;
7000
7578
  };
7001
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
+
7002
7609
  export class OutboxDispatcher {
7003
7610
  private readonly maxAttempts: number;
7004
7611
  private readonly batchSize: number;
7005
7612
  private readonly handlersByEventType: Map<string, EventSubscription[]>;
7006
7613
 
7007
7614
  constructor(
7008
- private readonly db: unknown,
7615
+ private readonly db: OutboxQueryExecutor,
7009
7616
  subscriptions: EventSubscription[],
7010
7617
  config?: OutboxDispatcherConfig,
7011
7618
  ) {
@@ -7022,11 +7629,10 @@ export class OutboxDispatcher {
7022
7629
  }
7023
7630
 
7024
7631
  async dispatchPending(): Promise<{ processed: number; failed: number }> {
7025
- const drizzle = this.db as any;
7026
7632
  let processed = 0;
7027
7633
  let failed = 0;
7028
7634
 
7029
- const rows: OutboxRow[] = await drizzle
7635
+ const rows: OutboxRow[] = await this.db
7030
7636
  .select()
7031
7637
  .from(outboxEvents)
7032
7638
  .where(eq(outboxEvents.status, "pending"))
@@ -7058,9 +7664,9 @@ export class OutboxDispatcher {
7058
7664
  version: row.eventVersion,
7059
7665
  payload: row.payload,
7060
7666
  eventContext: {
7061
- correlationId: metadata.correlationId as any,
7062
- causationId: metadata.causationId as any,
7063
- recordedBy: metadata.recordedBy as any,
7667
+ correlationId: createCorrelationId(requiredMetadataValue(metadata, "correlationId", row.eventType)),
7668
+ causationId: createEventId(requiredMetadataValue(metadata, "causationId", row.eventType)),
7669
+ recordedBy: createPersonnelId(requiredMetadataValue(metadata, "recordedBy", row.eventType)),
7064
7670
  },
7065
7671
  };
7066
7672
 
@@ -7106,8 +7712,7 @@ export class OutboxDispatcher {
7106
7712
  status: "pending" | "processed" | "failed",
7107
7713
  fields: Record<string, unknown>,
7108
7714
  ): Promise<void> {
7109
- const drizzle = this.db as any;
7110
- await drizzle
7715
+ await this.db
7111
7716
  .update(outboxEvents)
7112
7717
  .set({ status, ...fields })
7113
7718
  .where(eq(outboxEvents.id, id));
@@ -7115,6 +7720,187 @@ export class OutboxDispatcher {
7115
7720
  }
7116
7721
  `;
7117
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
+ }
7118
7904
  function createOutboxRelay() {
7119
7905
  return `import type { OutboxDispatcher } from "./outbox-dispatcher.ts";
7120
7906
 
@@ -7180,12 +7966,13 @@ export class OutboxPoller {
7180
7966
  }
7181
7967
  function buildCompositionOutboxArtifacts(infrastructureStrategy) {
7182
7968
  const ownership = sharedArtifactOwnership("composition");
7969
+ const usesMikroOrm = infrastructureStrategy?.orm === "mikroorm";
7183
7970
  return [
7184
- createGeneratedArtifact("infrastructure/outbox/outbox-dispatcher.ts", createOutboxDispatcher(), ownership),
7971
+ createGeneratedArtifact("infrastructure/outbox/outbox-dispatcher.ts", usesMikroOrm ? createMikroOrmOutboxDispatcher() : createOutboxDispatcher(), ownership),
7185
7972
  createGeneratedArtifact("infrastructure/outbox/outbox-poller.ts", createOutboxPoller(), ownership),
7186
7973
  createGeneratedArtifact("infrastructure/outbox/outbox-relay.ts", createOutboxRelay(), ownership),
7187
7974
  createGeneratedArtifact("infrastructure/outbox/outbox.table.ts", createOutboxTable(infrastructureStrategy), ownership),
7188
- createGeneratedArtifact("infrastructure/outbox/outbox-writer.ts", createOutboxWriter(), ownership)
7975
+ createGeneratedArtifact("infrastructure/outbox/outbox-writer.ts", usesMikroOrm ? createMikroOrmOutboxWriter(infrastructureStrategy) : createOutboxWriter(), ownership)
7189
7976
  ].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
7190
7977
  }
7191
7978
 
@@ -7365,20 +8152,51 @@ function buildAclContractContent(acl) {
7365
8152
  }
7366
8153
  function buildReadRequestAssignmentLines(acl) {
7367
8154
  const sourceInputFields = acl.sourceQuery.inputFields ?? [];
7368
- return acl.requestMappings.map((mapping) => {
8155
+ const directAssignments = [];
8156
+ const filterAssignments = [];
8157
+ for (const mapping of acl.requestMappings) {
7369
8158
  const valueExpr = mapping.kind === "from" ? `input.${mapping.sourcePath.replace(/^input\./, "")}` : JSON.stringify(mapping.value);
7370
8159
  const exactField = sourceInputFields.find((field) => camelCase(field.name) === camelCase(mapping.targetPath));
7371
- return ` ${exactField ? camelCase(exactField.name) : sourceInputFields.length === 1 ? camelCase(sourceInputFields[0].name) : camelCase(mapping.targetPath)}: ${valueExpr},`;
7372
- });
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;
7373
8173
  }
7374
8174
  function buildReadResponseAssignmentLines(acl, responseTypeName) {
7375
8175
  return acl.responseMappings.map((mapping) => {
7376
8176
  if (mapping.kind === "const") return ` ${camelCase(mapping.targetPath)}: ${JSON.stringify(mapping.value)},`;
7377
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
+ }
7378
8183
  const sourceFieldName = mapping.sourcePath.replace(/^result\./, "");
7379
- 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))}],`;
7380
8185
  });
7381
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
+ }
7382
8200
  function buildReadAdapterContent(acl, sourceContextNames) {
7383
8201
  const consumerModulePath = normalizeModulePath(acl.consumerContext.modulePath);
7384
8202
  const sourceModulePath = normalizeModulePath(acl.sourceContext.modulePath);
@@ -7394,7 +8212,36 @@ function buildReadAdapterContent(acl, sourceContextNames) {
7394
8212
  const queryFileName = `${kebabCase(acl.sourceQuery.name)}.query.ts`;
7395
8213
  const queryTypeLiteral = `${pascalCase(acl.sourceContext.name)}.${pascalCase(acl.sourceQuery.name)}`;
7396
8214
  const usesInput = acl.requestMappings.some((mapping) => mapping.kind === "from" && mapping.sourcePath.startsWith("input."));
7397
- 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
+ `;
7398
8245
  return `// Auto-generated composition ACL adapter — do not edit by hand
7399
8246
  import type { Transaction } from "../../../lib/transaction.ts";
7400
8247
  import type { TransactionManager } from "../../unit-of-work/transaction-manager.ts";
@@ -7419,16 +8266,13 @@ export class ${adapterClassName} implements ${portTypeName} {
7419
8266
  ${usesInput ? "" : "void input;"}
7420
8267
  try {
7421
8268
  const executeWithTransaction = async (currentTx: Transaction) => {
7422
- const result = await this.${boundaryVarName}.${boundaryMethodName}(
7423
- {
7424
- type: "${queryTypeLiteral}",
7425
- payload: {
8269
+ const query: ${queryEnvelopeTypeName} = {
8270
+ type: "${queryTypeLiteral}",
8271
+ payload: {
7426
8272
  ${buildReadRequestAssignmentLines(acl).join("\n")}
7427
- },
7428
- } as unknown as ${queryEnvelopeTypeName},
7429
- currentTx,
7430
- );
7431
- ${needsResultRecord ? `const resultRecord = result as unknown as Record<string, unknown>;` : ""}
8273
+ },
8274
+ };
8275
+ const result = await this.${boundaryVarName}.${boundaryMethodName}(query, currentTx);
7432
8276
 
7433
8277
  return {
7434
8278
  ${buildReadResponseAssignmentLines(acl, responseTypeName).join("\n")}
@@ -7439,8 +8283,9 @@ ${buildReadResponseAssignmentLines(acl, responseTypeName).join("\n")}
7439
8283
  return await executeWithTransaction(tx);
7440
8284
  }
7441
8285
 
7442
- return await this.transactionManager.withTransaction(async (runtimeTx) =>
7443
- executeWithTransaction(runtimeTx as Transaction)
8286
+ return await this.transactionManager.withTransaction<Transaction, ${responseTypeName}>(
8287
+ async (runtimeTx) =>
8288
+ executeWithTransaction(runtimeTx)
7444
8289
  );
7445
8290
  } catch (_error) {
7446
8291
  throw new Error("${acl.name} ACL failed", { cause: _error });
@@ -7462,6 +8307,7 @@ function buildWriteAdapterContent(acl) {
7462
8307
  const commandTypeLiteral = `${pascalCase(acl.targetContext.name)}.${pascalCase(acl.targetCommand.name)}`;
7463
8308
  const responseMappings = acl.kind === "ack" ? acl.acknowledgeMappings : acl.responseMappings;
7464
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;`;
7465
8311
  return `// Auto-generated composition ACL adapter — do not edit by hand
7466
8312
  import type { EventContext } from "../../../core/shared-kernel/events/event-context.ts";
7467
8313
  import type { Result } from "../../../core/shared-kernel/result.ts";
@@ -7484,7 +8330,7 @@ export class ${adapterClassName} implements ${portTypeName} {
7484
8330
  ): Promise<${responseTypeName}> {
7485
8331
  try {
7486
8332
  const executionResult = await this.commandBus.execute<
7487
- Result<Record<string, unknown>, unknown>,
8333
+ Result<${responseTypeName}, unknown>,
7488
8334
  ${commandEnvelopeTypeName}
7489
8335
  >(
7490
8336
  {
@@ -7499,8 +8345,7 @@ ${toTargetAssignmentLines(acl.requestMappings, "input").join("\n")}
7499
8345
  if (!executionResult.ok) {
7500
8346
  throw executionResult.error;
7501
8347
  }
7502
-
7503
- const result = executionResult.value as Record<string, unknown>;
8348
+ ${resultValueLine}
7504
8349
  ${responseBody}
7505
8350
  } catch (_error) {
7506
8351
  throw new Error("${acl.name} ACL failed", { cause: _error });
@@ -7619,12 +8464,13 @@ function resolveRuntimeDependencies(compositionSpec) {
7619
8464
  return [...dependencies].sort((left, right) => left.localeCompare(right));
7620
8465
  }
7621
8466
  function buildCompositionDependencyManifestArtifacts(compositionSpec) {
7622
- const manifest = {
7623
- kind: "zodmire-dependency-manifest",
7624
- infrastructure: compositionSpec.infrastructure,
7625
- dependencies: resolveRuntimeDependencies(compositionSpec)
7626
- };
7627
- return [createGeneratedArtifact("codegen/dependency-manifest.json", `${JSON.stringify(manifest, null, 2)}\n`, sharedArtifactOwnership("composition"))];
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"))];
7628
8474
  }
7629
8475
 
7630
8476
  //#endregion
@@ -7635,7 +8481,7 @@ function buildCompositionDependencyManifestArtifacts(compositionSpec) {
7635
8481
  */
7636
8482
  function buildCompositionContainerArtifacts(compositionSpec, contextSpecs) {
7637
8483
  const ownership = sharedArtifactOwnership("composition");
7638
- return [createGeneratedArtifact("infrastructure/di/container.ts", buildContainerContent(compositionSpec, contextSpecs), ownership), createGeneratedArtifact("infrastructure/di/repo-factory.ts", buildRepoFactoryContent(contextSpecs), ownership)].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
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));
7639
8485
  }
7640
8486
  function buildContainerContent(compositionSpec, contextSpecs) {
7641
8487
  assertSupportedContainerStrategy(compositionSpec);
@@ -7725,7 +8571,7 @@ function buildContainerContent(compositionSpec, contextSpecs) {
7725
8571
  for (const command of spec.commands) {
7726
8572
  const commandType = `${ctxName}.${pascalCase(command.name)}`;
7727
8573
  const handlerAlias = `${contextPlan.symbolStem}${pascalCase(command.name)}Handler`;
7728
- createContainerLines.push(` commandBus.register("${commandType}", ${handlerAlias} as any, "${contextPlan.pathSegment}");`);
8574
+ createContainerLines.push(` commandBus.register("${commandType}", ${handlerAlias}, "${contextPlan.pathSegment}");`);
7729
8575
  }
7730
8576
  }
7731
8577
  createContainerLines.push(``);
@@ -7841,10 +8687,12 @@ function uniqueReadContexts(acls) {
7841
8687
  }
7842
8688
  return contexts;
7843
8689
  }
7844
- function buildRepoFactoryContent(contextSpecs) {
8690
+ function buildRepoFactoryContent(compositionSpec, contextSpecs) {
8691
+ const forceMikroOrmRepositories = compositionSpec.infrastructure.orm === "mikroorm";
7845
8692
  const repositoryDefs = contextSpecs.flatMap((spec) => spec.aggregates.map((aggregate) => {
7846
8693
  const repositoryPort = spec.ports.find((port) => port.kind === "repository" && port.target === aggregate.name);
7847
- const isMikroOrm = (repositoryPort ? spec.adapters.find((candidate) => candidate.port === repositoryPort.name && (candidate.kind === "drizzle-repository" || candidate.kind === "mikroorm-repository")) : void 0)?.kind === "mikroorm-repository";
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";
7848
8696
  const providerPrefix = isMikroOrm ? "mikroorm" : "drizzle";
7849
8697
  const repositoryClassPrefix = isMikroOrm ? "MikroOrm" : "Drizzle";
7850
8698
  return {
@@ -7951,7 +8799,8 @@ function buildRootRouterContent(contextSpecs) {
7951
8799
  const lines = [
7952
8800
  `// Auto-generated root tRPC router — do not edit by hand`,
7953
8801
  ``,
7954
- `import type { Container } from "../../infrastructure/di/container.ts";`
8802
+ `import type { Container } from "../../infrastructure/di/container.ts";`,
8803
+ `import { router } from "./trpc-init.ts";`
7955
8804
  ];
7956
8805
  for (const spec of contextSpecs) {
7957
8806
  const routerName = spec.presentation.trpcRouter;
@@ -7961,7 +8810,7 @@ function buildRootRouterContent(contextSpecs) {
7961
8810
  }
7962
8811
  lines.push(``);
7963
8812
  lines.push(`export function createRootRouter(container: Container) {`);
7964
- lines.push(` return {`);
8813
+ lines.push(` return router({`);
7965
8814
  for (const spec of contextSpecs) {
7966
8815
  const routerName = spec.presentation.trpcRouter;
7967
8816
  const key = camelCase(routerName);
@@ -7971,7 +8820,7 @@ function buildRootRouterContent(contextSpecs) {
7971
8820
  lines.push(` queryBus: container.queryBus,`);
7972
8821
  lines.push(` }),`);
7973
8822
  }
7974
- lines.push(` };`);
8823
+ lines.push(` });`);
7975
8824
  lines.push(`}`);
7976
8825
  lines.push(``);
7977
8826
  return lines.join("\n");
@@ -8186,7 +9035,10 @@ async function buildContextNormalizedSpecFromConfig(input) {
8186
9035
  }))), config);
8187
9036
  }
8188
9037
  async function buildV5ContextArtifacts(input) {
8189
- return generateContextArtifacts(await buildContextNormalizedSpecFromConfig(input), { infrastructureStrategy: input.infrastructureStrategy });
9038
+ return generateContextArtifacts(await buildContextNormalizedSpecFromConfig(input), {
9039
+ infrastructureStrategy: input.infrastructureStrategy,
9040
+ generation: input.generation
9041
+ });
8190
9042
  }
8191
9043
  async function buildV5Artifacts(input) {
8192
9044
  const spec = await buildNormalizedSpecFromConfig(input);
@@ -8222,4 +9074,4 @@ async function buildCompositionArtifacts(compositionConfigPath, contextSpecs) {
8222
9074
  }
8223
9075
 
8224
9076
  //#endregion
8225
- export { GENERATED_READ_SIDE_SCHEMA_LOGICAL_PATH, SPEC_ARTIFACT_SCHEMA_VERSION, SPEC_DIFF_SCHEMA_VERSION, VENDORED_FILE_MANIFEST, applyOwnershipIfMissing, buildArtifactPath, buildCollisionSafeModulePathNames, buildCompositionAclArtifacts, buildCompositionArtifacts, buildCompositionBusArtifacts, buildCompositionContainerArtifacts, buildCompositionDependencyManifestArtifacts, buildCompositionOutboxArtifacts, buildCompositionRouterArtifacts, buildCompositionSpecDiff, buildCompositionSubscriptionArtifacts, buildCompositionTypeArtifacts, buildConsumerAclArtifacts, buildContextInputChecks, buildContextNormalizedSpec, buildContextNormalizedSpecFromConfig, buildContextSpecDiff, buildDrizzleConfig, buildFileArtifactTags, buildMaterializationManifestTags, buildMigrationResultTags, buildNormalizedCompositionSpec, buildNormalizedSpec, buildNormalizedSpecFromConfig, buildNormalizedSpecTags, buildProjectionArtifacts, buildReadModelCompositionArtifacts, buildSpecDiffTags, buildSummaryTags, buildV5ApplicationArtifacts, buildV5ApplicationContextArtifacts, buildV5Artifacts, buildV5ContextArtifacts, buildV5DomainArtifacts, buildV5InfrastructureArtifacts, buildV5InfrastructureContextArtifacts, buildV5LibArtifacts, buildV5PortArtifacts, buildV5PresentationArtifacts, buildV5RouteArtifacts, buildV5SharedKernelArtifacts, buildV5TestArtifacts, buildVendoredFileTags, camelCase, collectOwnedValueObjects, compositionSpecDiffDataName, contextArtifactOwnership, contextSpecDiffDataName, contractFileName, createGeneratedArtifact, discoverReferencedVos, ensureContextSupportFilesExist, filterPerContextArtifactsForComposition, flattenEntities, generateContextArtifacts, inferArtifactOwnership, inferCodegenSupportPathForContext, inferCodegenSupportPathFromSchemaFile, inferReconcileScopes, introspectObjectShape, introspectSchema, isRegisteredEntity, kebabCase, loadContextConfig, mergeGeneratedArtifacts, mergeSchemaReadResults, normalizeModulePath, parseConnectionString, parseDrizzleKitOutput, pascalCase, prepareAllVendoredFiles, prepareVendoredFile, readSchemaFile, repositoryPortFileName, repositoryPortTypeName, resolveAbsoluteContextInputs, resolveArtifactOwnership, resolveContextInputs, resolveFactoryPath, resolveGeneratedMigrationSchemaPath, resolveMethodContextInputsForCheck, resolveSchemaFilePathsForContext, resolveVoOwner, rewriteImports, runMigration, serializeCompositionSpec, serializeContextSpec, sharedArtifactOwnership, sliceArtifactOwnership, sliceContextIntoAggregateViews, snakeCase, snakeUpperCase, toImportURL, toUpperSnakeCase, unwrapFieldType, validateContextImports, walkEntityTree, withArtifactOwnership, wordsFromName };
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 };