@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/{artifacts-CUzHfc15.mjs → artifacts-D0E751FX.mjs} +7 -1
- package/{artifacts-QqCpfT50.d.mts → artifacts-lAG6nq88.d.mts} +3 -2
- package/materialize.d.mts +4 -1
- package/materialize.mjs +17 -13
- package/mod.d.mts +62 -5
- package/mod.mjs +1168 -316
- package/package.json +2 -2
package/mod.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as mergeGeneratedArtifacts, c as sliceArtifactOwnership, i as inferArtifactOwnership, l as
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
411
|
-
queryKind:
|
|
412
|
-
pagination: "
|
|
413
|
-
filters: "
|
|
414
|
-
sorting: "
|
|
415
|
-
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
|
|
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
|
|
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
|
|
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
|
|
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}"`,
|
|
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}"`,
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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))
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1846
|
+
let needsErrImport = false;
|
|
1641
1847
|
for (const cmd of mutationCmds) if (cmd.preconditions?.length) {
|
|
1642
|
-
|
|
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
|
-
|
|
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.${
|
|
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)
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
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
|
-
|
|
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),
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
if (
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
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,
|
|
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):
|
|
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,
|
|
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"
|
|
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
|
|
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 = `
|
|
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
|
-
|
|
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
|
-
})
|
|
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.
|
|
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
|
|
2477
|
-
const metadataExpr =
|
|
2478
|
-
payloadFields.push(` ${fieldName}: ${metadataExpr},`);
|
|
2479
|
-
|
|
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
|
-
|
|
2732
|
+
imports.push(`import type { DomainError } from "../../../../../lib/domain-error.ts";`);
|
|
2733
|
+
return pruneUnusedBrandedIdImport(`${imports.join("\n")}
|
|
2501
2734
|
|
|
2502
|
-
export type ${handlerErrorType} =
|
|
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
|
|
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
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
void
|
|
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
|
-
|
|
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))
|
|
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
|
|
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
|
-
|
|
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 ? `
|
|
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
|
|
3762
|
+
const ${recordVar} = new ${node.className}();
|
|
3481
3763
|
${parentAssignment}${scalarAssignmentLines.join("\n")}
|
|
3482
3764
|
${createChildLines.join("\n")}
|
|
3483
|
-
return
|
|
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)
|
|
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
|
-
|
|
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
|
-
${
|
|
3533
|
-
${
|
|
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
|
-
${
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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}
|
|
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
|
-
|
|
4246
|
-
if (!options?.skipContextWideArtifacts &&
|
|
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.
|
|
4254
|
-
const
|
|
4255
|
-
|
|
4256
|
-
if (
|
|
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 (
|
|
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/${
|
|
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 = (
|
|
4727
|
+
const payloadLine = ` const payload = extractProjectionPayload(event);`;
|
|
4424
4728
|
const executorLine = ` const ${executorVariable} = ${executorExpression};`;
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
const
|
|
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
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
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 (
|
|
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
|
|
4454
|
-
|
|
4455
|
-
|
|
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
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
4508
|
-
|
|
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).
|
|
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
|
-
|
|
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)}(
|
|
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/${
|
|
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
|
|
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
|
-
|
|
5044
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
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
|
|
5075
|
-
|
|
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 { ${
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
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<
|
|
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:
|
|
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:
|
|
7182
|
+
constructor(private readonly db: DrizzleTransactionalDatabase) {}
|
|
6677
7183
|
|
|
6678
|
-
async withTransaction<T>(work: (tx:
|
|
7184
|
+
async withTransaction<TTx, T>(work: (tx: TTx) => Promise<T>): Promise<T> {
|
|
6679
7185
|
// Drizzle transaction wrapper
|
|
6680
|
-
return
|
|
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:
|
|
6694
|
-
|
|
6695
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
7062
|
-
causationId: metadata
|
|
7063
|
-
recordedBy: metadata
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)}:
|
|
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
|
|
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
|
|
7423
|
-
{
|
|
7424
|
-
|
|
7425
|
-
payload: {
|
|
8269
|
+
const query: ${queryEnvelopeTypeName} = {
|
|
8270
|
+
type: "${queryTypeLiteral}",
|
|
8271
|
+
payload: {
|
|
7426
8272
|
${buildReadRequestAssignmentLines(acl).join("\n")}
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
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
|
|
7443
|
-
|
|
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
|
|
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
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
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}
|
|
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
|
|
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), {
|
|
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 };
|