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