@zodmire/core 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mod.d.mts +37 -5
- package/mod.mjs +1579 -248
- package/package.json +2 -2
package/mod.mjs
CHANGED
|
@@ -160,6 +160,44 @@ function createReadField(exportName) {
|
|
|
160
160
|
sourceSchema: rawSchema,
|
|
161
161
|
objectShape: def.shape
|
|
162
162
|
};
|
|
163
|
+
case zx.tagged("intersection")(rawSchema): {
|
|
164
|
+
const left = def.left;
|
|
165
|
+
const right = def.right;
|
|
166
|
+
if (left?.objectShape && right?.objectShape) return {
|
|
167
|
+
type: "object",
|
|
168
|
+
optional: false,
|
|
169
|
+
array: false,
|
|
170
|
+
sourceSchema: rawSchema,
|
|
171
|
+
objectShape: {
|
|
172
|
+
...left.objectShape,
|
|
173
|
+
...right.objectShape
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
type: "intersection",
|
|
178
|
+
optional: false,
|
|
179
|
+
array: false,
|
|
180
|
+
sourceSchema: rawSchema
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
case zx.tagged("union")(rawSchema): return {
|
|
184
|
+
type: "union",
|
|
185
|
+
optional: false,
|
|
186
|
+
array: false,
|
|
187
|
+
sourceSchema: rawSchema
|
|
188
|
+
};
|
|
189
|
+
case zx.tagged("record")(rawSchema): return {
|
|
190
|
+
type: "record",
|
|
191
|
+
optional: false,
|
|
192
|
+
array: false,
|
|
193
|
+
sourceSchema: rawSchema
|
|
194
|
+
};
|
|
195
|
+
case zx.tagged("tuple")(rawSchema): return {
|
|
196
|
+
type: "tuple",
|
|
197
|
+
optional: false,
|
|
198
|
+
array: false,
|
|
199
|
+
sourceSchema: rawSchema
|
|
200
|
+
};
|
|
163
201
|
case zx.tagged("string")(rawSchema):
|
|
164
202
|
case zx.tagged("number")(rawSchema):
|
|
165
203
|
case zx.tagged("int")(rawSchema):
|
|
@@ -470,6 +508,64 @@ function mergeSchemaReadResults(results) {
|
|
|
470
508
|
|
|
471
509
|
//#endregion
|
|
472
510
|
//#region packages/core/normalizer.ts
|
|
511
|
+
function generatedCapability() {
|
|
512
|
+
return {
|
|
513
|
+
status: "generated",
|
|
514
|
+
reasons: []
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function disabledCapability(reason) {
|
|
518
|
+
return {
|
|
519
|
+
status: "disabled",
|
|
520
|
+
reasons: [reason]
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function runtimeThrowCapability(reasons) {
|
|
524
|
+
return {
|
|
525
|
+
status: "runtime-throw",
|
|
526
|
+
reasons
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function canMapReadModelOutput$1(readModel, outputFields) {
|
|
530
|
+
const readModelFields = new Set(readModel.fields.map((field) => camelCase(field.name)));
|
|
531
|
+
return outputFields.every((field) => readModelFields.has(camelCase(field.name)));
|
|
532
|
+
}
|
|
533
|
+
function resolveGeneratedFindByIdLookupField(readModel, query) {
|
|
534
|
+
if (readModel.primaryKey.length !== 1) return null;
|
|
535
|
+
const primaryKeyField = camelCase(readModel.primaryKey[0]);
|
|
536
|
+
const matchingField = (query.inputFields ?? []).find((field) => camelCase(field.name) === primaryKeyField);
|
|
537
|
+
if (matchingField) return camelCase(matchingField.name);
|
|
538
|
+
if ((query.inputFields ?? []).length === 1) return camelCase(query.inputFields[0].name);
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
function deriveCommandCapability(command) {
|
|
542
|
+
const reasons = [];
|
|
543
|
+
if (!command.effects || command.effects.length === 0) reasons.push("command does not declare any effects");
|
|
544
|
+
if (command.commandKind === "update" && !command.loadBy) reasons.push("mutation commands require loadBy");
|
|
545
|
+
return reasons.length === 0 ? generatedCapability() : runtimeThrowCapability(reasons);
|
|
546
|
+
}
|
|
547
|
+
function deriveQueryCapability(query, readModelByName) {
|
|
548
|
+
const reasons = [];
|
|
549
|
+
if (!query.readModelName) reasons.push("query does not declare readModelName");
|
|
550
|
+
const readModel = query.readModelName ? readModelByName.get(query.readModelName) : void 0;
|
|
551
|
+
if (!readModel) reasons.push(`read-model "${query.readModelName ?? query.name}" is missing`);
|
|
552
|
+
else {
|
|
553
|
+
if (!canMapReadModelOutput$1(readModel, query.outputFields)) reasons.push("output fields do not map directly to the read-model");
|
|
554
|
+
if (query.queryKind === "findById" && !resolveGeneratedFindByIdLookupField(readModel, query)) reasons.push("read-model does not expose a stable findById lookup field");
|
|
555
|
+
}
|
|
556
|
+
return reasons.length === 0 ? generatedCapability() : runtimeThrowCapability(reasons);
|
|
557
|
+
}
|
|
558
|
+
function deriveProjectionCapabilities(projection, readModelByName) {
|
|
559
|
+
const writeReasons = [];
|
|
560
|
+
if (!readModelByName.has(projection.readModelName)) writeReasons.push(`read-model "${projection.readModelName}" is missing`);
|
|
561
|
+
for (const source of projection.sources) if (source.mutation.kind === "custom") writeReasons.push(`source "${source.contextName}.${source.aggregateName}.${source.eventName}" requires custom mutation handler "${source.mutation.handlerName}"`);
|
|
562
|
+
const writeModel = writeReasons.length === 0 ? generatedCapability() : runtimeThrowCapability(writeReasons);
|
|
563
|
+
return {
|
|
564
|
+
projector: generatedCapability(),
|
|
565
|
+
writeModel,
|
|
566
|
+
rebuild: !projection.rebuild?.enabled ? disabledCapability("projection rebuild is disabled") : writeModel.status === "generated" ? generatedCapability() : runtimeThrowCapability([...writeModel.reasons])
|
|
567
|
+
};
|
|
568
|
+
}
|
|
473
569
|
function buildContextNormalizedSpec(schemaResult, config) {
|
|
474
570
|
if (schemaResult.aggregates.length === 0) throw new Error(`[normalizer] No aggregate found in SchemaReadResult. Context "${config.name}" requires at least one aggregate.`);
|
|
475
571
|
const aggregateNames = new Set(schemaResult.aggregates.map((a) => a.name));
|
|
@@ -653,7 +749,8 @@ function buildContextNormalizedSpec(schemaResult, config) {
|
|
|
653
749
|
sources: projection.sources.map((source) => ({
|
|
654
750
|
...source,
|
|
655
751
|
locality: source.contextName === config.name ? "local" : "external"
|
|
656
|
-
}))
|
|
752
|
+
})),
|
|
753
|
+
capabilities: deriveProjectionCapabilities(projection, readModelByName)
|
|
657
754
|
};
|
|
658
755
|
});
|
|
659
756
|
return {
|
|
@@ -685,9 +782,11 @@ function buildContextNormalizedSpec(schemaResult, config) {
|
|
|
685
782
|
loadBy: cmd.loadBy,
|
|
686
783
|
preconditions: cmd.preconditions,
|
|
687
784
|
emits: cmd.emits,
|
|
688
|
-
effects: cmd.effects
|
|
785
|
+
effects: cmd.effects,
|
|
786
|
+
capability: deriveCommandCapability(cmd)
|
|
689
787
|
})),
|
|
690
788
|
queries: schemaResult.queries.map((q) => {
|
|
789
|
+
const capability = deriveQueryCapability(q, readModelByName);
|
|
691
790
|
const readSide = {
|
|
692
791
|
readModelName: q.readModelName ?? pascalCase(q.name),
|
|
693
792
|
searchFields: [],
|
|
@@ -746,7 +845,8 @@ function buildContextNormalizedSpec(schemaResult, config) {
|
|
|
746
845
|
pagination: { style: "offset" },
|
|
747
846
|
filters: q.filters,
|
|
748
847
|
sorting: q.sorting,
|
|
749
|
-
computedFields: resolvedComputed
|
|
848
|
+
computedFields: resolvedComputed,
|
|
849
|
+
capability
|
|
750
850
|
};
|
|
751
851
|
}
|
|
752
852
|
return {
|
|
@@ -758,7 +858,8 @@ function buildContextNormalizedSpec(schemaResult, config) {
|
|
|
758
858
|
outputSchemaPath: q.outputSchemaPath,
|
|
759
859
|
outputSchemaExportName: q.outputSchemaExportName,
|
|
760
860
|
readModelName: q.readModelName,
|
|
761
|
-
readSide
|
|
861
|
+
readSide,
|
|
862
|
+
capability
|
|
762
863
|
};
|
|
763
864
|
}),
|
|
764
865
|
readModels: normalizedReadModels,
|
|
@@ -826,6 +927,7 @@ function walkEntityTree(aggregate, visitor) {
|
|
|
826
927
|
//#region packages/core/composition_normalizer.ts
|
|
827
928
|
function buildNormalizedCompositionSpec(config, contextSpecs) {
|
|
828
929
|
assertUniqueCanonicalContextSpecs(contextSpecs);
|
|
930
|
+
const infrastructure = resolveInfrastructureStrategy(config.infrastructure);
|
|
829
931
|
const resolveProvidedContextRef = createContextRefResolver(contextSpecs);
|
|
830
932
|
const resolvedContexts = [];
|
|
831
933
|
const resolvedContextSpecs = [];
|
|
@@ -884,9 +986,60 @@ function buildNormalizedCompositionSpec(config, contextSpecs) {
|
|
|
884
986
|
contexts: resolvedContexts,
|
|
885
987
|
crossContextEvents,
|
|
886
988
|
acls,
|
|
989
|
+
infrastructure,
|
|
887
990
|
materialization: { targetRoot: config.materialization.targetRoot }
|
|
888
991
|
};
|
|
889
992
|
}
|
|
993
|
+
function resolveInfrastructureStrategy(input) {
|
|
994
|
+
const supportedTuples = [
|
|
995
|
+
{
|
|
996
|
+
architecture: "physical-cqrs",
|
|
997
|
+
persistence: "postgres",
|
|
998
|
+
orm: "drizzle"
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
architecture: "logical-cqrs",
|
|
1002
|
+
persistence: "postgres",
|
|
1003
|
+
orm: "drizzle"
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
architecture: "logical-cqrs",
|
|
1007
|
+
persistence: "mysql",
|
|
1008
|
+
orm: "drizzle"
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
architecture: "physical-cqrs",
|
|
1012
|
+
persistence: "mysql",
|
|
1013
|
+
orm: "drizzle"
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
architecture: "physical-cqrs",
|
|
1017
|
+
persistence: "postgres",
|
|
1018
|
+
orm: "mikroorm"
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
architecture: "logical-cqrs",
|
|
1022
|
+
persistence: "postgres",
|
|
1023
|
+
orm: "mikroorm"
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
architecture: "physical-cqrs",
|
|
1027
|
+
persistence: "mysql",
|
|
1028
|
+
orm: "mikroorm"
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
architecture: "logical-cqrs",
|
|
1032
|
+
persistence: "mysql",
|
|
1033
|
+
orm: "mikroorm"
|
|
1034
|
+
}
|
|
1035
|
+
];
|
|
1036
|
+
if (!supportedTuples.some((tuple) => tuple.architecture === input.architecture && tuple.persistence === input.persistence && tuple.orm === input.orm)) throw new Error(`[composition] Unsupported infrastructure strategy:\narchitecture=${input.architecture}, persistence=${input.persistence}, orm=${input.orm}\n\nCurrently supported strategies:\n` + supportedTuples.map((tuple) => `- ${tuple.architecture} + ${tuple.persistence} + ${tuple.orm}`).join("\n"));
|
|
1037
|
+
return {
|
|
1038
|
+
architecture: input.architecture,
|
|
1039
|
+
persistence: input.persistence,
|
|
1040
|
+
orm: input.orm
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
890
1043
|
function assertUniqueCanonicalContextSpecs(contextSpecs) {
|
|
891
1044
|
const seenModulePaths = /* @__PURE__ */ new Map();
|
|
892
1045
|
for (const spec of contextSpecs) {
|
|
@@ -1140,6 +1293,10 @@ function aggregateVariableName$1(spec) {
|
|
|
1140
1293
|
function aggregateIdPropertyName$1(spec) {
|
|
1141
1294
|
return `${aggregateVariableName$1(spec)}Id`;
|
|
1142
1295
|
}
|
|
1296
|
+
function defaultIdBrandName(name) {
|
|
1297
|
+
const base = pascalCase(name);
|
|
1298
|
+
return base.endsWith("Id") ? base : `${base}Id`;
|
|
1299
|
+
}
|
|
1143
1300
|
function formatFieldType$1(field, indent = 0) {
|
|
1144
1301
|
if (field.type === "object" && field.nestedFields && field.nestedFields.length > 0) {
|
|
1145
1302
|
const objectType = `{\n${field.nestedFields.map((nestedField) => {
|
|
@@ -1367,6 +1524,7 @@ function renderEffectLines(effects, inputVar, spec, indent, selfRef = "this", is
|
|
|
1367
1524
|
const aggregateFieldsByName = new Map(filterManagedAggregateFields$1(spec.aggregate.fields ?? []).map((field) => [camelCase(field.name), field]));
|
|
1368
1525
|
let lastPushedEntityVar = null;
|
|
1369
1526
|
let lastPushedEntityIdField = null;
|
|
1527
|
+
let lastPushedEntityTypeName = null;
|
|
1370
1528
|
const idFieldName = spec.aggregate.idField ? camelCase(spec.aggregate.idField) : null;
|
|
1371
1529
|
const lines = [];
|
|
1372
1530
|
for (const effect of effects) switch (effect.kind) {
|
|
@@ -1397,11 +1555,11 @@ function renderEffectLines(effects, inputVar, spec, indent, selfRef = "this", is
|
|
|
1397
1555
|
}
|
|
1398
1556
|
return `${fieldName}: ${aggregateAccess}`;
|
|
1399
1557
|
}
|
|
1400
|
-
if (lastPushedEntityVar && !aggregateFieldNames.has(fieldName)) return `${fieldName}: (${lastPushedEntityVar} as
|
|
1558
|
+
if (lastPushedEntityVar && !aggregateFieldNames.has(fieldName)) return `${fieldName}: (${lastPushedEntityVar} as ${lastPushedEntityTypeName ?? "Record<string, unknown>"}).${fieldName}`;
|
|
1401
1559
|
if (availableInputFields.has(fieldName)) return `${fieldName}: ${inputVar}.${fieldName}`;
|
|
1402
1560
|
if (fieldName === "occurredAt" || fieldName.endsWith("At")) return `${fieldName}: ${f.type === "date" ? "new Date()" : "new Date().toISOString()"}`;
|
|
1403
1561
|
if (fieldName === "recordedBy" || fieldName.endsWith("By")) return `${fieldName}: ${f.type === "string" ? "eventContext.recordedBy.value" : "eventContext.recordedBy"}`;
|
|
1404
|
-
return `${fieldName}:
|
|
1562
|
+
return `${fieldName}: /* TODO: unmapped ${eventName} payload field "${fieldName}" */ undefined as never`;
|
|
1405
1563
|
}).join(", ")} }` : "{}";
|
|
1406
1564
|
lines.push(`${indent}${selfRef}.raise(createDomainEvent<${eventName}Payload>({`, `${indent} eventType: "${contextName}.${eventName}",`, `${indent} eventVersion: 1,`, `${indent} entityType: "${aggregateName}",`, `${indent} entityId: ${selfRef}.id,`, `${indent} contextName: "${contextName}",`, `${indent} recordedBy: eventContext.recordedBy,`, `${indent} correlationId: eventContext.correlationId,`, `${indent} causationId: eventContext.causationId,`, `${indent} payload: ${payloadObj},`, `${indent}}));`);
|
|
1407
1565
|
break;
|
|
@@ -1420,6 +1578,7 @@ function renderEffectLines(effects, inputVar, spec, indent, selfRef = "this", is
|
|
|
1420
1578
|
const entityVar = camelCase(targetChild.name);
|
|
1421
1579
|
lastPushedEntityVar = entityVar;
|
|
1422
1580
|
lastPushedEntityIdField = targetChild.idField;
|
|
1581
|
+
lastPushedEntityTypeName = entityName;
|
|
1423
1582
|
lines.push(`${indent}const ${entityVar} = ${entityName}.create({`, `${indent} ${camelCase(targetChild.idField)}: ${createIdFn}(crypto.randomUUID()),`, `${indent} ...${inputVar} as any,`, `${indent}});`, `${indent}${selfRef}.${camelCase(effect.target)}.push(${entityVar});`);
|
|
1424
1583
|
} else lines.push(`${indent}${selfRef}.${camelCase(effect.target)}.push(${renderEffectValue(effect.value, inputVar, selfRef)});`);
|
|
1425
1584
|
break;
|
|
@@ -1520,10 +1679,10 @@ function createAggregateStub(spec) {
|
|
|
1520
1679
|
return ` readonly ${camelCase(child.collectionFieldName)}: ${entityName}[] = [];`;
|
|
1521
1680
|
});
|
|
1522
1681
|
const constructorAssignments = nonIdFields.map((f) => ` this.${camelCase(f.name)} = props.${camelCase(f.name)};`).join("\n");
|
|
1523
|
-
const idTypeStr =
|
|
1682
|
+
const idTypeStr = `BrandedId<"${idType && idType !== "string" ? pascalCase(idType) : defaultIdBrandName(spec.aggregate.name)}">`;
|
|
1524
1683
|
let createBody;
|
|
1525
1684
|
if (createCmd?.effects?.length) createBody = ` const instance = new ${aggregateClassName}(id, 0, props);
|
|
1526
|
-
${renderEffectLines(createCmd.effects, "props", spec, " ", "instance", true).join("\n")}
|
|
1685
|
+
${renderEffectLines(createCmd.effects.filter((effect) => effect.kind !== "set-field" || effect.value !== `input.${effect.target}`), "props", spec, " ", "instance", true).join("\n")}
|
|
1527
1686
|
return instance;`;
|
|
1528
1687
|
else createBody = ` return new ${aggregateClassName}(id, 0, props);`;
|
|
1529
1688
|
const fieldTypeMap = /* @__PURE__ */ new Map();
|
|
@@ -1601,9 +1760,8 @@ ${methodLines.join("\n")}
|
|
|
1601
1760
|
const opt = f.optional ? "?" : "";
|
|
1602
1761
|
persistenceLines.push(` readonly ${camelCase(f.name)}${opt}: ${formatPersistenceFieldType(f)};`);
|
|
1603
1762
|
}
|
|
1604
|
-
const
|
|
1605
|
-
|
|
1606
|
-
const fromPersistenceAssignment = fromPersistenceFields.length > 0 ? ` Object.assign(instance, {\n${fromPersistenceFields.join("\n")}\n });` : "";
|
|
1763
|
+
const fromPersistenceProps = nonIdFields.map((f) => ` ${camelCase(f.name)}: state.${camelCase(f.name)},`).join("\n");
|
|
1764
|
+
const collectionAssignments = (spec.aggregate.children ?? []).map((child) => ` instance.${camelCase(child.collectionFieldName)}.push(...state.${camelCase(child.collectionFieldName)});`).join("\n");
|
|
1607
1765
|
return `${imports}
|
|
1608
1766
|
|
|
1609
1767
|
export interface ${aggregateClassName}Props {
|
|
@@ -1644,8 +1802,11 @@ ${createBody}
|
|
|
1644
1802
|
rehydrationStateName,
|
|
1645
1803
|
children: spec.aggregate.children ?? []
|
|
1646
1804
|
})}(state);
|
|
1647
|
-
const
|
|
1648
|
-
${
|
|
1805
|
+
const props: ${aggregateClassName}Props = {
|
|
1806
|
+
${fromPersistenceProps}
|
|
1807
|
+
};
|
|
1808
|
+
const instance = new ${aggregateClassName}(state.${camelCase(idField)}, state.version, props);
|
|
1809
|
+
${collectionAssignments}
|
|
1649
1810
|
return instance;
|
|
1650
1811
|
}${mutationMethods.join("")}
|
|
1651
1812
|
}
|
|
@@ -1657,7 +1818,7 @@ function createEntityStub(entity, depthFromDomain = 1) {
|
|
|
1657
1818
|
const idField = entity.idField;
|
|
1658
1819
|
const idType = entity.idType;
|
|
1659
1820
|
const nonIdFields = fields.filter((f) => f.name !== idField);
|
|
1660
|
-
const idTypeStr = idType && idType !== "string" ?
|
|
1821
|
+
const idTypeStr = `BrandedId<"${idType && idType !== "string" ? pascalCase(idType) : defaultIdBrandName(entity.name)}">`;
|
|
1661
1822
|
const libRelPath = "../../../../../lib";
|
|
1662
1823
|
const imports = [
|
|
1663
1824
|
`import { Entity } from "${libRelPath}/entity.base.ts";`,
|
|
@@ -1703,9 +1864,8 @@ function createEntityStub(entity, depthFromDomain = 1) {
|
|
|
1703
1864
|
const opt = f.optional ? "?" : "";
|
|
1704
1865
|
persistenceLines.push(` readonly ${camelCase(f.name)}${opt}: ${formatPersistenceFieldType(f)};`);
|
|
1705
1866
|
}
|
|
1706
|
-
const
|
|
1707
|
-
|
|
1708
|
-
const entityFromPersistenceAssignment = entityFromPersistenceFields.length > 0 ? ` Object.assign(instance, {\n${entityFromPersistenceFields.join("\n")}\n });` : "";
|
|
1867
|
+
const entityFromPersistenceProps = nonIdFields.map((f) => ` ${camelCase(f.name)}: state.${camelCase(f.name)},`).join("\n");
|
|
1868
|
+
const entityCollectionAssignments = children.map((child) => ` instance.${camelCase(child.collectionFieldName)}.push(...state.${camelCase(child.collectionFieldName)});`).join("\n");
|
|
1709
1869
|
return `${imports.join("\n")}
|
|
1710
1870
|
|
|
1711
1871
|
export interface ${entityName}Props {
|
|
@@ -1746,8 +1906,11 @@ ${constructorAssignments}
|
|
|
1746
1906
|
rehydrationStateName,
|
|
1747
1907
|
children: entity.children ?? []
|
|
1748
1908
|
})}(state);
|
|
1749
|
-
const
|
|
1750
|
-
${
|
|
1909
|
+
const props: ${entityName}Props = {
|
|
1910
|
+
${entityFromPersistenceProps}
|
|
1911
|
+
};
|
|
1912
|
+
const instance = new ${entityName}(state.${camelCase(idField)}, props);
|
|
1913
|
+
${entityCollectionAssignments}
|
|
1751
1914
|
return instance;
|
|
1752
1915
|
}
|
|
1753
1916
|
}
|
|
@@ -2059,7 +2222,7 @@ function listQuerySortFieldTypeName$1(query) {
|
|
|
2059
2222
|
function listQueryRowTypeName$1(query) {
|
|
2060
2223
|
return `${listQueryTypeBaseName$1(query)}Row`;
|
|
2061
2224
|
}
|
|
2062
|
-
function readModelContractName$
|
|
2225
|
+
function readModelContractName$2(readModelName) {
|
|
2063
2226
|
const baseName = pascalCase(readModelName);
|
|
2064
2227
|
return baseName.endsWith("View") ? baseName : `${baseName}View`;
|
|
2065
2228
|
}
|
|
@@ -2073,7 +2236,7 @@ function queryOutputExportName$1(query) {
|
|
|
2073
2236
|
return query.outputSchemaExportName?.trim() || void 0;
|
|
2074
2237
|
}
|
|
2075
2238
|
function queryReadModelPortTypeName$1(query) {
|
|
2076
|
-
return `${readModelContractName$
|
|
2239
|
+
return `${readModelContractName$2(resolvedReadModelName$2(query))}RepositoryPort`;
|
|
2077
2240
|
}
|
|
2078
2241
|
function queryReadModelPortFileName(query) {
|
|
2079
2242
|
return `read-models/${readModelPortBaseFileName(resolvedReadModelName$2(query))}.repository.port.ts`;
|
|
@@ -2137,7 +2300,7 @@ function createListQueryHandlerStub(_spec, query) {
|
|
|
2137
2300
|
const readModelPortType = queryReadModelPortTypeName$1(query);
|
|
2138
2301
|
const readModelVariable = queryReadModelVariableName$1(query);
|
|
2139
2302
|
const readModelMethodName = queryReadModelMethodName$1(query);
|
|
2140
|
-
const viewFileBase = queryViewFileBase$
|
|
2303
|
+
const viewFileBase = queryViewFileBase$1(query);
|
|
2141
2304
|
return `import type { ${queryTypeName} } from "./${kebabCase(query.name)}.query.ts";
|
|
2142
2305
|
import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";
|
|
2143
2306
|
import type { ${readModelPortType} } from "../ports/${queryReadModelPortFileName(query)}";
|
|
@@ -2165,12 +2328,14 @@ function createHandlerDepsStub(spec) {
|
|
|
2165
2328
|
const repoPort = findRepositoryPort(spec);
|
|
2166
2329
|
const repoTypeName = repoPort ? repositoryPortTypeName(repoPort.name) : `${aggregatePascal}Repository`;
|
|
2167
2330
|
return `import type { Transaction } from "../../../../lib/transaction.ts";
|
|
2331
|
+
import type { AggregateEventTracker } from "../../../shared-kernel/events/aggregate-event-tracker.ts";
|
|
2168
2332
|
import type { EventCollector } from "../../../shared-kernel/events/event-collector.ts";
|
|
2169
2333
|
import type { ${repoTypeName} } from "./ports/${repoPort ? repositoryPortFileName(repoPort.name) : `${kebabCase(spec.aggregate.name)}-repository.port.ts`}";
|
|
2170
2334
|
|
|
2171
2335
|
export type ${contextPascal}CommandHandlerDeps = {
|
|
2172
2336
|
tx: Transaction;
|
|
2173
2337
|
eventCollector: EventCollector;
|
|
2338
|
+
aggregateEventTracker: AggregateEventTracker;
|
|
2174
2339
|
repos: {
|
|
2175
2340
|
${aggregateVar}s: ${repoTypeName};
|
|
2176
2341
|
};
|
|
@@ -2207,9 +2372,9 @@ ${query.outputFields.map((f) => {
|
|
|
2207
2372
|
function queryOutputContractName$1(query) {
|
|
2208
2373
|
const outputExportName = queryOutputExportName$1(query);
|
|
2209
2374
|
if (outputExportName) return pascalCase(outputExportName);
|
|
2210
|
-
return readModelContractName$
|
|
2375
|
+
return readModelContractName$2(resolvedReadModelName$2(query));
|
|
2211
2376
|
}
|
|
2212
|
-
function queryViewFileBase$
|
|
2377
|
+
function queryViewFileBase$1(query) {
|
|
2213
2378
|
const outputExportName = queryOutputExportName$1(query);
|
|
2214
2379
|
if (outputExportName) return kebabCase(outputExportName).replace(/-view$/, "");
|
|
2215
2380
|
return kebabCase(resolvedReadModelName$2(query)).replace(/-view$/, "");
|
|
@@ -2260,7 +2425,8 @@ function buildCreateHandlerBody(spec, command) {
|
|
|
2260
2425
|
const assignedValue = createPropAssignments.get(fieldName) ?? (commandInputFieldNames.has(fieldName) ? commandInputField?.optional && !field.optional ? `command.payload.${fieldName} ?? ${defaultValue}` : `command.payload.${fieldName}` : defaultValue);
|
|
2261
2426
|
if (assignedValue !== void 0) propsLines.push(` ${fieldName}: ${assignedValue},`);
|
|
2262
2427
|
}
|
|
2263
|
-
const
|
|
2428
|
+
const defaultIdBrand = `${pascalCase(spec.aggregate.name)}Id`;
|
|
2429
|
+
const idTypeName = hasCustomIdType ? pascalCase(idType) : `BrandedId<"${defaultIdBrand}">`;
|
|
2264
2430
|
const creationEventName = command.emits?.[0];
|
|
2265
2431
|
const creationEvent = creationEventName ? spec.domainEvents.find((e) => pascalCase(e.name) === pascalCase(creationEventName)) : void 0;
|
|
2266
2432
|
const aggregateFieldNames = new Set((spec.aggregate.fields ?? []).map((field) => camelCase(field.name)));
|
|
@@ -2291,7 +2457,7 @@ function buildCreateHandlerBody(spec, command) {
|
|
|
2291
2457
|
const payloadName = `${pascalCase(creationEventName)}Payload`;
|
|
2292
2458
|
imports.push(`import type { ${payloadName} } from "../../domain/events/${kebabCase(creationEventName)}.event.ts";`);
|
|
2293
2459
|
}
|
|
2294
|
-
const idExpr = hasCustomIdType ? `create${pascalCase(idType)}(crypto.randomUUID())` : `createBrandedId("${idType
|
|
2460
|
+
const idExpr = hasCustomIdType ? `create${pascalCase(idType)}(crypto.randomUUID())` : `createBrandedId("${hasCustomIdType ? idType : defaultIdBrand}", crypto.randomUUID())`;
|
|
2295
2461
|
let creationEventLines = "";
|
|
2296
2462
|
if (creationEvent) {
|
|
2297
2463
|
const payloadName = `${pascalCase(creationEventName)}Payload`;
|
|
@@ -2310,7 +2476,7 @@ function buildCreateHandlerBody(spec, command) {
|
|
|
2310
2476
|
else if (fieldName === "recordedBy" || fieldName === "correlationId" || fieldName === "causationId") {
|
|
2311
2477
|
const metadataExpr = fieldName === "recordedBy" ? f.type === "string" ? "eventContext.recordedBy.value" : "eventContext.recordedBy" : `eventContext.${fieldName}`;
|
|
2312
2478
|
payloadFields.push(` ${fieldName}: ${metadataExpr},`);
|
|
2313
|
-
} else payloadFields.push(` ${fieldName}:
|
|
2479
|
+
} else payloadFields.push(` ${fieldName}: /* TODO: unmapped ${payloadName} field "${fieldName}" */ undefined as never,`);
|
|
2314
2480
|
}
|
|
2315
2481
|
creationEventLines = `
|
|
2316
2482
|
deps.eventCollector.collect([
|
|
@@ -2387,7 +2553,7 @@ function buildMutationHandlerBody(spec, command) {
|
|
|
2387
2553
|
}
|
|
2388
2554
|
imports.push(`import type { ${notFoundErrorType} } from "../../domain/errors/${aggregateDir}-application-errors.ts";`);
|
|
2389
2555
|
if (hasCustomIdType && idCreatorFn && idType) imports.push(`import { ${idCreatorFn} } from "../../../../shared-kernel/entity-ids/${kebabCase(idType)}.ts";`);
|
|
2390
|
-
const domainMethodArg = cmdEmitsEvents ? "command.payload
|
|
2556
|
+
const domainMethodArg = cmdEmitsEvents ? "command.payload, eventContext" : "command.payload";
|
|
2391
2557
|
const loadByField = command.loadBy?.startsWith("input.") ? command.loadBy.slice(6) : aggregateId;
|
|
2392
2558
|
const brandedIdExpr = hasCustomIdType && idCreatorFn ? `${idCreatorFn}(command.payload.${loadByField})` : `command.payload.${loadByField}`;
|
|
2393
2559
|
let bodyLines;
|
|
@@ -2399,7 +2565,7 @@ function buildMutationHandlerBody(spec, command) {
|
|
|
2399
2565
|
` const result = ${aggregateVar}.${methodName}(${domainMethodArg});`,
|
|
2400
2566
|
` if (!result.ok) return result;`,
|
|
2401
2567
|
` await deps.repos.${camelCase(spec.aggregate.name)}s.save(${aggregateVar}, loadedVersion, deps.tx);`,
|
|
2402
|
-
` deps.
|
|
2568
|
+
` deps.aggregateEventTracker.track(${aggregateVar});`,
|
|
2403
2569
|
` return ok(undefined);`
|
|
2404
2570
|
];
|
|
2405
2571
|
else bodyLines = [
|
|
@@ -2409,7 +2575,7 @@ function buildMutationHandlerBody(spec, command) {
|
|
|
2409
2575
|
` const loadedVersion = ${aggregateVar}.version;`,
|
|
2410
2576
|
` ${aggregateVar}.${methodName}(${domainMethodArg});`,
|
|
2411
2577
|
` await deps.repos.${camelCase(spec.aggregate.name)}s.save(${aggregateVar}, loadedVersion, deps.tx);`,
|
|
2412
|
-
` deps.
|
|
2578
|
+
` deps.aggregateEventTracker.track(${aggregateVar});`,
|
|
2413
2579
|
` return ok(undefined);`
|
|
2414
2580
|
];
|
|
2415
2581
|
const handlerErrorTypeExport = hasPreconditions ? `export type ${handlerErrorType} =\n | ${errorTypes.join("\n | ")};\n` : `export type ${handlerErrorType} = ${notFoundErrorType};\n`;
|
|
@@ -2442,11 +2608,14 @@ export async function ${handlerFnName}(
|
|
|
2442
2608
|
command: ${commandTypeName},
|
|
2443
2609
|
deps: ${depsTypeName},
|
|
2444
2610
|
eventContext: EventContext,
|
|
2445
|
-
): Promise<
|
|
2611
|
+
): Promise<never> {
|
|
2446
2612
|
void command;
|
|
2447
2613
|
void deps;
|
|
2448
2614
|
void eventContext;
|
|
2449
|
-
|
|
2615
|
+
throw new Error(
|
|
2616
|
+
"[application_generator] Command \\"${spec.context.name}.${command.name}\\" cannot be fully generated. " +
|
|
2617
|
+
"Expected a create command with effects or a mutation command with both effects and loadBy.",
|
|
2618
|
+
);
|
|
2450
2619
|
}
|
|
2451
2620
|
`;
|
|
2452
2621
|
}
|
|
@@ -2468,7 +2637,7 @@ function buildQueryHandlerBody(_spec, query) {
|
|
|
2468
2637
|
const handlerName = `${pascalCase(query.name)}Handler`;
|
|
2469
2638
|
const readModelVariable = queryReadModelVariableName$1(query);
|
|
2470
2639
|
const readModelMethodName = queryReadModelMethodName$1(query);
|
|
2471
|
-
const viewFileBase = queryViewFileBase$
|
|
2640
|
+
const viewFileBase = queryViewFileBase$1(query);
|
|
2472
2641
|
return `import type { ${queryTypeName} } from "./${kebabCase(query.name)}.query.ts";
|
|
2473
2642
|
import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";
|
|
2474
2643
|
import type { ${readModelPortType} } from "../ports/${queryReadModelPortFileName(query)}";
|
|
@@ -2494,7 +2663,7 @@ function createQueryHandlerStub(_spec, query) {
|
|
|
2494
2663
|
const handlerName = `${pascalCase(query.name)}Handler`;
|
|
2495
2664
|
const readModelVariable = queryReadModelVariableName$1(query);
|
|
2496
2665
|
const readModelMethodName = queryReadModelMethodName$1(query);
|
|
2497
|
-
const viewFileBase = queryViewFileBase$
|
|
2666
|
+
const viewFileBase = queryViewFileBase$1(query);
|
|
2498
2667
|
return `import type { ${queryTypeName} } from "./${kebabCase(query.name)}.query.ts";
|
|
2499
2668
|
import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";
|
|
2500
2669
|
import type { ${readModelPortType} } from "../ports/${queryReadModelPortFileName(query)}";
|
|
@@ -2557,46 +2726,71 @@ function groupQueriesByReadModel$1(queries) {
|
|
|
2557
2726
|
}));
|
|
2558
2727
|
}
|
|
2559
2728
|
function createReadModelRepositoryPortStub(readModelName, queries) {
|
|
2560
|
-
const portTypeName = `${readModelContractName$
|
|
2729
|
+
const portTypeName = `${readModelContractName$2(readModelName)}RepositoryPort`;
|
|
2561
2730
|
const importLines = /* @__PURE__ */ new Map();
|
|
2562
2731
|
let needsPaginatedResult = false;
|
|
2563
|
-
importLines.set("tx", `import type { Transaction } from "
|
|
2732
|
+
importLines.set("tx", `import type { Transaction } from "../../../../../../lib/transaction.ts";`);
|
|
2564
2733
|
const queryMethodLines = queries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((query) => {
|
|
2565
2734
|
const queryTypeName = `${pascalCase(query.name)}Query`;
|
|
2566
2735
|
const outputContractName = queryOutputContractName$1(query);
|
|
2567
|
-
const viewFileBase = queryViewFileBase$
|
|
2736
|
+
const viewFileBase = queryViewFileBase$1(query);
|
|
2568
2737
|
const returnType = query.queryKind === "list" ? `PaginatedResult<${outputContractName}>` : outputContractName;
|
|
2569
2738
|
if (query.queryKind === "list") needsPaginatedResult = true;
|
|
2570
|
-
importLines.set(`query:${queryTypeName}`, `import type { ${queryTypeName} } from "
|
|
2571
|
-
importLines.set(`view:${outputContractName}`, `import type { ${outputContractName} } from "
|
|
2739
|
+
importLines.set(`query:${queryTypeName}`, `import type { ${queryTypeName} } from "../../queries/${kebabCase(query.name)}.query.ts";`);
|
|
2740
|
+
importLines.set(`view:${outputContractName}`, `import type { ${outputContractName} } from "../../contracts/${viewFileBase}.view.ts";`);
|
|
2572
2741
|
return ` ${queryReadModelMethodName$1(query)}(query: ${queryTypeName}, tx: Transaction): Promise<${returnType}>;`;
|
|
2573
2742
|
});
|
|
2574
|
-
if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "
|
|
2743
|
+
if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "../../../../../../lib/pagination.ts";`);
|
|
2575
2744
|
return `${[...importLines.values()].join("\n")}
|
|
2576
2745
|
|
|
2577
2746
|
// Read-model repository port. Query handlers share this contract by read-model, not by query.
|
|
2578
2747
|
export interface ${portTypeName} {
|
|
2579
|
-
// Query methods
|
|
2580
2748
|
${queryMethodLines.join("\n")}
|
|
2581
|
-
|
|
2582
|
-
// Projector methods
|
|
2583
|
-
// TODO: add typed row write methods when projector/repository generation lands.
|
|
2584
|
-
|
|
2585
|
-
// Rebuild methods
|
|
2586
|
-
// TODO: add rebuild helpers when projection rebuild scaffolding lands.
|
|
2587
2749
|
}
|
|
2588
2750
|
`;
|
|
2589
2751
|
}
|
|
2590
2752
|
function createReadModelPortsIndexStub(queries) {
|
|
2591
2753
|
return `${groupQueriesByReadModel$1(queries).map(({ readModelName }) => {
|
|
2592
|
-
return `export type { ${`${readModelContractName$
|
|
2754
|
+
return `export type { ${`${readModelContractName$2(readModelName)}RepositoryPort`} } from "./${readModelPortBaseFileName(readModelName)}.repository.port.ts";`;
|
|
2593
2755
|
}).join("\n")}\n`;
|
|
2594
2756
|
}
|
|
2757
|
+
function projectionWritePortName$2(projectionName) {
|
|
2758
|
+
return `${pascalCase(projectionName)}WritePort`;
|
|
2759
|
+
}
|
|
2760
|
+
function projectionWritePortFileName(projectionName) {
|
|
2761
|
+
return `${kebabCase(projectionName)}.projection-write.port.ts`;
|
|
2762
|
+
}
|
|
2763
|
+
function projectionSourcePayloadTypeName$1(source) {
|
|
2764
|
+
return `${pascalCase(source.eventName)}Payload`;
|
|
2765
|
+
}
|
|
2766
|
+
function projectionSourcePayloadImportPath$1(source) {
|
|
2767
|
+
return `../../../${normalizeModulePath(source.contextName)}/domain/events/${kebabCase(source.eventName)}.event.ts`;
|
|
2768
|
+
}
|
|
2769
|
+
function projectionSourceEventType$1(source) {
|
|
2770
|
+
const payloadType = projectionSourcePayloadTypeName$1(source);
|
|
2771
|
+
return `EventEnvelope<${payloadType}> | ${payloadType}`;
|
|
2772
|
+
}
|
|
2773
|
+
function createProjectionWritePortStub(projection) {
|
|
2774
|
+
const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionSourcePayloadTypeName$1(source)} } from "${projectionSourcePayloadImportPath$1(source)}";`);
|
|
2775
|
+
const methodLines = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => ` on${pascalCase(source.eventName)}(event: ${projectionSourceEventType$1(source)}, tx: Transaction): Promise<void>;`);
|
|
2776
|
+
return `import type { EventEnvelope } from "../../../../../../lib/event-envelope.ts";
|
|
2777
|
+
import type { Transaction } from "../../../../../../lib/transaction.ts";
|
|
2778
|
+
${payloadImports.join("\n")}
|
|
2779
|
+
|
|
2780
|
+
// Projector-facing write port for the "${projection.readModelName}" read model.
|
|
2781
|
+
export interface ${projectionWritePortName$2(projection.name)} {
|
|
2782
|
+
${methodLines.join("\n")}
|
|
2783
|
+
}
|
|
2784
|
+
`;
|
|
2785
|
+
}
|
|
2786
|
+
function createProjectionWritePortsIndexStub(projections) {
|
|
2787
|
+
return `${projections.slice().sort((left, right) => left.name.localeCompare(right.name)).map((projection) => `export type { ${projectionWritePortName$2(projection.name)} } from "./${projectionWritePortFileName(projection.name)}";`).join("\n")}\n`;
|
|
2788
|
+
}
|
|
2595
2789
|
function inputContractFileName(commandOrQueryName) {
|
|
2596
2790
|
return `${kebabCase(commandOrQueryName)}.input`;
|
|
2597
2791
|
}
|
|
2598
2792
|
function viewContractFileName(query) {
|
|
2599
|
-
return `${queryViewFileBase$
|
|
2793
|
+
return `${queryViewFileBase$1(query)}.view`;
|
|
2600
2794
|
}
|
|
2601
2795
|
function createHandlerMapStub(spec) {
|
|
2602
2796
|
const contextPascal = pascalCase(spec.context.name);
|
|
@@ -2619,7 +2813,11 @@ ${mapEntries.join("\n")}
|
|
|
2619
2813
|
}
|
|
2620
2814
|
function createContextHandlerDepsStub(contextSpec) {
|
|
2621
2815
|
const contextPascal = pascalCase(contextSpec.context.name);
|
|
2622
|
-
const importLines = [
|
|
2816
|
+
const importLines = [
|
|
2817
|
+
`import type { Transaction } from "../../../../lib/transaction.ts";`,
|
|
2818
|
+
`import type { AggregateEventTracker } from "../../../shared-kernel/events/aggregate-event-tracker.ts";`,
|
|
2819
|
+
`import type { EventCollector } from "../../../shared-kernel/events/event-collector.ts";`
|
|
2820
|
+
];
|
|
2623
2821
|
const repoEntries = [];
|
|
2624
2822
|
const aclEntries = [];
|
|
2625
2823
|
for (const agg of contextSpec.aggregates) {
|
|
@@ -2645,6 +2843,7 @@ ${aclEntries.join("\n")}
|
|
|
2645
2843
|
export type ${contextPascal}CommandHandlerDeps = {
|
|
2646
2844
|
tx: Transaction;
|
|
2647
2845
|
eventCollector: EventCollector;
|
|
2846
|
+
aggregateEventTracker: AggregateEventTracker;
|
|
2648
2847
|
repos: {
|
|
2649
2848
|
${repoEntries.join("\n")}
|
|
2650
2849
|
};
|
|
@@ -2690,7 +2889,7 @@ function createPublishedReadBoundaryStub(contextSpec) {
|
|
|
2690
2889
|
const queryTypeName = `${pascalCase(query.name)}Query`;
|
|
2691
2890
|
const outputContractName = queryOutputContractName$1(query);
|
|
2692
2891
|
const queryFileName = kebabCase(query.name);
|
|
2693
|
-
const outputFileName = queryViewFileBase$
|
|
2892
|
+
const outputFileName = queryViewFileBase$1(query);
|
|
2694
2893
|
const readModelDepName = queryReadModelVariableName$1(query);
|
|
2695
2894
|
const readModelTypeName = queryReadModelPortTypeName$1(query);
|
|
2696
2895
|
imports.set(`handler:${queryHandlerName}`, `import { ${queryHandlerName} } from "./queries/${queryFileName}.handler.ts";`);
|
|
@@ -2772,16 +2971,25 @@ function buildV5ApplicationArtifacts(spec, options) {
|
|
|
2772
2971
|
}
|
|
2773
2972
|
function buildV5ApplicationContextArtifacts(contextSpec) {
|
|
2774
2973
|
const modulePath = normalizeModulePath(contextSpec.context.modulePath);
|
|
2775
|
-
|
|
2974
|
+
const artifacts = [
|
|
2776
2975
|
createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/ports/read-models/index.ts"), createReadModelPortsIndexStub(contextSpec.queries)),
|
|
2777
2976
|
createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/handler-deps.ts"), createContextHandlerDepsStub(contextSpec)),
|
|
2778
2977
|
createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/read-boundary.ts"), createPublishedReadBoundaryStub(contextSpec)),
|
|
2779
2978
|
createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/handler-map.ts"), createContextHandlerMapStub(contextSpec))
|
|
2780
2979
|
];
|
|
2980
|
+
const projections = contextSpec.projections ?? [];
|
|
2981
|
+
if (projections.length > 0) {
|
|
2982
|
+
artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/ports/projections/index.ts"), createProjectionWritePortsIndexStub(projections)));
|
|
2983
|
+
for (const projection of projections) artifacts.push(createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, `application/ports/projections/${projectionWritePortFileName(projection.name)}`), createProjectionWritePortStub(projection)));
|
|
2984
|
+
}
|
|
2985
|
+
return artifacts;
|
|
2781
2986
|
}
|
|
2782
2987
|
|
|
2783
2988
|
//#endregion
|
|
2784
2989
|
//#region packages/core/generators/infrastructure.ts
|
|
2990
|
+
function resolveDrizzlePersistence(infrastructureStrategy) {
|
|
2991
|
+
return infrastructureStrategy?.persistence === "mysql" ? "mysql" : "postgres";
|
|
2992
|
+
}
|
|
2785
2993
|
function isDecimalNumberField(field) {
|
|
2786
2994
|
if (field.type !== "number") return false;
|
|
2787
2995
|
const explicitColumnType = field.columnType?.toLowerCase();
|
|
@@ -2796,10 +3004,12 @@ function isDecimalNumberField(field) {
|
|
|
2796
3004
|
const fieldName = snakeCase(field.name);
|
|
2797
3005
|
return /(^|_)(cpk|ppk|ucl|lcl)(_|$)/.test(fieldName) || /(percent|percentage|ratio|rate|average|avg|variance|yield|scrap|drift)/.test(fieldName);
|
|
2798
3006
|
}
|
|
2799
|
-
function drizzleColumnBuilder(field, columnName) {
|
|
3007
|
+
function drizzleColumnBuilder(field, columnName, persistence = "postgres") {
|
|
2800
3008
|
let builder;
|
|
2801
|
-
if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0)
|
|
2802
|
-
|
|
3009
|
+
if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0) {
|
|
3010
|
+
const jsonType = formatTsFieldType(field);
|
|
3011
|
+
builder = persistence === "mysql" ? `json("${columnName}").$type<${jsonType}>()` : `jsonb("${columnName}").$type<${jsonType}>()`;
|
|
3012
|
+
} else switch (field.type) {
|
|
2803
3013
|
case "number":
|
|
2804
3014
|
builder = isDecimalNumberField(field) ? `real("${columnName}")` : `integer("${columnName}")`;
|
|
2805
3015
|
break;
|
|
@@ -2906,12 +3116,12 @@ function relativePrefixFromRepositoryDirectory(modulePath) {
|
|
|
2906
3116
|
...modulePath.split("/").filter(Boolean)
|
|
2907
3117
|
].map(() => "..").join("/");
|
|
2908
3118
|
}
|
|
2909
|
-
function readModelContractName$
|
|
3119
|
+
function readModelContractName$1(readModelName) {
|
|
2910
3120
|
const baseName = pascalCase(readModelName);
|
|
2911
3121
|
return baseName.endsWith("View") ? baseName : `${baseName}View`;
|
|
2912
3122
|
}
|
|
2913
3123
|
function readModelRepositoryClassName(readModelName) {
|
|
2914
|
-
return `Drizzle${readModelContractName$
|
|
3124
|
+
return `Drizzle${readModelContractName$1(readModelName)}Repository`;
|
|
2915
3125
|
}
|
|
2916
3126
|
function readModelRepositoryFileBase$1(readModelName) {
|
|
2917
3127
|
return kebabCase(readModelName).replace(/-view$/, "");
|
|
@@ -2919,6 +3129,15 @@ function readModelRepositoryFileBase$1(readModelName) {
|
|
|
2919
3129
|
function readModelTableConstName(readModelName) {
|
|
2920
3130
|
return `${camelCase(readModelName)}Table`;
|
|
2921
3131
|
}
|
|
3132
|
+
function mikroOrmRepositoryClassName(aggregateName) {
|
|
3133
|
+
return `MikroOrm${pascalCase(aggregateName)}Repository`;
|
|
3134
|
+
}
|
|
3135
|
+
function mikroOrmEntityClassName(name) {
|
|
3136
|
+
return `${pascalCase(name)}Record`;
|
|
3137
|
+
}
|
|
3138
|
+
function mikroOrmEntitySchemaConstName(name) {
|
|
3139
|
+
return `${pascalCase(name)}Schema`;
|
|
3140
|
+
}
|
|
2922
3141
|
function resolvedReadModelName$1(query) {
|
|
2923
3142
|
return query.readSide?.readModelName ?? query.readModelName ?? query.name;
|
|
2924
3143
|
}
|
|
@@ -2928,9 +3147,9 @@ function queryOutputExportName(query) {
|
|
|
2928
3147
|
function queryOutputContractName(query) {
|
|
2929
3148
|
const outputExportName = queryOutputExportName(query);
|
|
2930
3149
|
if (outputExportName) return pascalCase(outputExportName);
|
|
2931
|
-
return readModelContractName$
|
|
3150
|
+
return readModelContractName$1(resolvedReadModelName$1(query));
|
|
2932
3151
|
}
|
|
2933
|
-
function queryViewFileBase
|
|
3152
|
+
function queryViewFileBase(query) {
|
|
2934
3153
|
const outputExportName = queryOutputExportName(query);
|
|
2935
3154
|
if (outputExportName) return kebabCase(outputExportName).replace(/-view$/, "");
|
|
2936
3155
|
return readModelRepositoryFileBase$1(resolvedReadModelName$1(query));
|
|
@@ -2960,13 +3179,13 @@ const RESERVED_AGGREGATE_FIELD_NAMES = new Set(["version"]);
|
|
|
2960
3179
|
function filterManagedAggregateFields(fields) {
|
|
2961
3180
|
return fields.filter((field) => !RESERVED_AGGREGATE_FIELD_NAMES.has(camelCase(field.name)));
|
|
2962
3181
|
}
|
|
2963
|
-
function buildTableBlock(tableName, tableConstName, fields, idField, fkColumn, isAggregateRoot = false) {
|
|
3182
|
+
function buildTableBlock(tableName, tableConstName, fields, idField, fkColumn, isAggregateRoot = false, persistence = "postgres") {
|
|
2964
3183
|
const columnDefs = [];
|
|
2965
3184
|
const emittedColumns = /* @__PURE__ */ new Set();
|
|
2966
3185
|
for (const field of fields) {
|
|
2967
3186
|
const snakeName = snakeCase(field.name);
|
|
2968
3187
|
if (emittedColumns.has(snakeName)) continue;
|
|
2969
|
-
const columnDef = field.name === idField ? `text("${snakeName}").primaryKey()` : drizzleColumnBuilder(field, snakeName);
|
|
3188
|
+
const columnDef = field.name === idField ? `text("${snakeName}").primaryKey()` : drizzleColumnBuilder(field, snakeName, persistence);
|
|
2970
3189
|
columnDefs.push(` ${snakeName}: ${columnDef},`);
|
|
2971
3190
|
emittedColumns.add(snakeName);
|
|
2972
3191
|
}
|
|
@@ -2975,16 +3194,16 @@ function buildTableBlock(tableName, tableConstName, fields, idField, fkColumn, i
|
|
|
2975
3194
|
emittedColumns.add(fkColumn.name);
|
|
2976
3195
|
}
|
|
2977
3196
|
if (isAggregateRoot) columnDefs.push(` version: integer("version").default(0).notNull(),`);
|
|
2978
|
-
return `export const ${tableConstName} = pgTable("${tableName}", {\n${columnDefs.join("\n")}\n});`;
|
|
3197
|
+
return `export const ${tableConstName} = ${persistence === "mysql" ? "mysqlTable" : "pgTable"}("${tableName}", {\n${columnDefs.join("\n")}\n});`;
|
|
2979
3198
|
}
|
|
2980
|
-
function createDrizzleTableDefinition(spec) {
|
|
3199
|
+
function createDrizzleTableDefinition(spec, persistence = "postgres") {
|
|
2981
3200
|
const fields = filterManagedAggregateFields(spec.aggregate.fields);
|
|
2982
3201
|
if (!fields || fields.length === 0) return null;
|
|
2983
3202
|
const children = spec.aggregate.children ?? [];
|
|
2984
3203
|
const aggScalarFields = scalarFields(fields, children);
|
|
2985
3204
|
const emittedTableNames = /* @__PURE__ */ new Set();
|
|
2986
3205
|
const blocks = [];
|
|
2987
|
-
blocks.push(buildTableBlock(spec.aggregate.tableName, `${camelCase(spec.aggregate.name)}Table`, aggScalarFields, spec.aggregate.idField, void 0, true));
|
|
3206
|
+
blocks.push(buildTableBlock(spec.aggregate.tableName, `${camelCase(spec.aggregate.name)}Table`, aggScalarFields, spec.aggregate.idField, void 0, true, persistence));
|
|
2988
3207
|
emittedTableNames.add(spec.aggregate.tableName);
|
|
2989
3208
|
const aggregate = {
|
|
2990
3209
|
name: spec.aggregate.name,
|
|
@@ -3001,13 +3220,13 @@ function createDrizzleTableDefinition(spec) {
|
|
|
3001
3220
|
blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, {
|
|
3002
3221
|
name: fkColName,
|
|
3003
3222
|
type: "text"
|
|
3004
|
-
}));
|
|
3223
|
+
}, false, persistence));
|
|
3005
3224
|
emittedTableNames.add(entity.tableName);
|
|
3006
3225
|
});
|
|
3007
3226
|
for (const entity of spec.entities ?? []) {
|
|
3008
3227
|
if (!entity.tableName || emittedTableNames.has(entity.tableName)) continue;
|
|
3009
3228
|
const entityScalarFields = scalarFields(entity.fields, entity.children ?? []);
|
|
3010
|
-
blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField));
|
|
3229
|
+
blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, void 0, false, persistence));
|
|
3011
3230
|
emittedTableNames.add(entity.tableName);
|
|
3012
3231
|
}
|
|
3013
3232
|
const flatScalarFields = [
|
|
@@ -3015,12 +3234,12 @@ function createDrizzleTableDefinition(spec) {
|
|
|
3015
3234
|
...flattenEntities(aggregate).map((entity) => scalarFields(entity.fields, entity.children)),
|
|
3016
3235
|
...(spec.entities ?? []).map((entity) => scalarFields(entity.fields, entity.children ?? []))
|
|
3017
3236
|
].flat();
|
|
3018
|
-
const
|
|
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 { ${
|
|
3237
|
+
const coreImportTokens = [persistence === "mysql" ? "mysqlTable" : "pgTable", "text"];
|
|
3238
|
+
if (flatScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) coreImportTokens.push("integer");
|
|
3239
|
+
if (flatScalarFields.some((field) => isDecimalNumberField(field))) coreImportTokens.push("real");
|
|
3240
|
+
if (flatScalarFields.some((field) => field.type === "boolean")) coreImportTokens.push("boolean");
|
|
3241
|
+
if (flatScalarFields.some((field) => field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0)) coreImportTokens.push(persistence === "mysql" ? "json" : "jsonb");
|
|
3242
|
+
return `import { ${coreImportTokens.join(", ")} } from "${persistence === "mysql" ? "drizzle-orm/mysql-core" : "drizzle-orm/pg-core"}";\n\n${blocks.join("\n\n")}\n`;
|
|
3024
3243
|
}
|
|
3025
3244
|
function buildCodecBlock(_spec, entityName, fields, options) {
|
|
3026
3245
|
const varName = camelCase(entityName);
|
|
@@ -3091,6 +3310,280 @@ function createPersistenceEqualityBlock(name, fields, options) {
|
|
|
3091
3310
|
return `// Compiled from zx.deepEqual.writeable at generator time using the canonical ${pascalCase(name)} persistence row shape.
|
|
3092
3311
|
export const ${equalityName} = ${compiledEquality};`;
|
|
3093
3312
|
}
|
|
3313
|
+
function buildMikroOrmRootNode(spec) {
|
|
3314
|
+
const buildChild = (entity, parent) => {
|
|
3315
|
+
const node = {
|
|
3316
|
+
name: entity.name,
|
|
3317
|
+
className: mikroOrmEntityClassName(entity.name),
|
|
3318
|
+
schemaConstName: mikroOrmEntitySchemaConstName(entity.name),
|
|
3319
|
+
fields: entity.fields,
|
|
3320
|
+
scalarFields: scalarFields(entity.fields, entity.children),
|
|
3321
|
+
idField: entity.idField,
|
|
3322
|
+
idType: entity.idType,
|
|
3323
|
+
tableName: entity.tableName,
|
|
3324
|
+
collectionFieldName: entity.collectionFieldName,
|
|
3325
|
+
parent: {
|
|
3326
|
+
className: parent.className,
|
|
3327
|
+
propertyName: camelCase(parent.name),
|
|
3328
|
+
fkFieldName: snakeCase(parent.idField)
|
|
3329
|
+
},
|
|
3330
|
+
children: [],
|
|
3331
|
+
isAggregateRoot: false
|
|
3332
|
+
};
|
|
3333
|
+
node.children = (entity.children ?? []).map((child) => buildChild(child, node));
|
|
3334
|
+
return node;
|
|
3335
|
+
};
|
|
3336
|
+
const rootNode = {
|
|
3337
|
+
name: spec.aggregate.name,
|
|
3338
|
+
className: mikroOrmEntityClassName(spec.aggregate.name),
|
|
3339
|
+
schemaConstName: mikroOrmEntitySchemaConstName(spec.aggregate.name),
|
|
3340
|
+
fields: filterManagedAggregateFields(spec.aggregate.fields),
|
|
3341
|
+
scalarFields: scalarFields(filterManagedAggregateFields(spec.aggregate.fields), spec.aggregate.children ?? []),
|
|
3342
|
+
idField: spec.aggregate.idField,
|
|
3343
|
+
idType: spec.aggregate.idType,
|
|
3344
|
+
tableName: spec.aggregate.tableName,
|
|
3345
|
+
children: [],
|
|
3346
|
+
isAggregateRoot: true
|
|
3347
|
+
};
|
|
3348
|
+
rootNode.children = (spec.aggregate.children ?? []).map((child) => buildChild(child, rootNode));
|
|
3349
|
+
return rootNode;
|
|
3350
|
+
}
|
|
3351
|
+
function flattenMikroOrmNodes(rootNode) {
|
|
3352
|
+
const nodes = [rootNode];
|
|
3353
|
+
const queue = [...rootNode.children];
|
|
3354
|
+
while (queue.length > 0) {
|
|
3355
|
+
const node = queue.shift();
|
|
3356
|
+
nodes.push(node);
|
|
3357
|
+
queue.push(...node.children);
|
|
3358
|
+
}
|
|
3359
|
+
return nodes;
|
|
3360
|
+
}
|
|
3361
|
+
function mikroOrmClassFieldType(field) {
|
|
3362
|
+
if (field.type === "object" && field.nestedFields && field.nestedFields.length > 0) return formatTsFieldType(field);
|
|
3363
|
+
switch (field.type) {
|
|
3364
|
+
case "number": return field.array ? "number[]" : "number";
|
|
3365
|
+
case "boolean": return field.array ? "boolean[]" : "boolean";
|
|
3366
|
+
default: return field.array ? "string[]" : "string";
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
function mikroOrmPropertyType(field) {
|
|
3370
|
+
if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0) return "json";
|
|
3371
|
+
switch (field.type) {
|
|
3372
|
+
case "number": return "number";
|
|
3373
|
+
case "boolean": return "boolean";
|
|
3374
|
+
default: return "string";
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
function createMikroOrmEntitySchemaArtifactContent(spec) {
|
|
3378
|
+
const nodes = flattenMikroOrmNodes(buildMikroOrmRootNode(spec));
|
|
3379
|
+
const contextPrefix = pascalCase(spec.context.name);
|
|
3380
|
+
const imports = [`import { Cascade, EntitySchema${nodes.some((node) => node.children.length > 0) ? ", Collection" : ""} } from "@mikro-orm/core";`];
|
|
3381
|
+
const classBlocks = nodes.map((node) => {
|
|
3382
|
+
const lines = [`export class ${node.className} {`];
|
|
3383
|
+
for (const field of node.scalarFields) lines.push(` ${camelCase(field.name)}${field.optional ? "?" : "!"}: ${mikroOrmClassFieldType(field)};`);
|
|
3384
|
+
if (node.isAggregateRoot) lines.push(` version!: number;`);
|
|
3385
|
+
if (node.parent) lines.push(` ${node.parent.propertyName}!: ${node.parent.className};`);
|
|
3386
|
+
for (const child of node.children) {
|
|
3387
|
+
const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
|
|
3388
|
+
lines.push(` ${collectionName} = new Collection<${child.className}>(this);`);
|
|
3389
|
+
}
|
|
3390
|
+
lines.push(`}`);
|
|
3391
|
+
return lines.join("\n");
|
|
3392
|
+
});
|
|
3393
|
+
const schemaBlocks = nodes.map((node) => {
|
|
3394
|
+
const propertyLines = [];
|
|
3395
|
+
for (const field of node.scalarFields) {
|
|
3396
|
+
const options = [`type: "${mikroOrmPropertyType(field)}"`, `fieldName: "${snakeCase(field.name)}"`];
|
|
3397
|
+
if (camelCase(field.name) === camelCase(node.idField)) options.push(`primary: true`);
|
|
3398
|
+
if (field.optional) options.push(`nullable: true`);
|
|
3399
|
+
propertyLines.push(` ${camelCase(field.name)}: { ${options.join(", ")} },`);
|
|
3400
|
+
}
|
|
3401
|
+
if (node.isAggregateRoot) propertyLines.push(` version: { type: "number", fieldName: "version" },`);
|
|
3402
|
+
if (node.parent) propertyLines.push(` ${node.parent.propertyName}: { kind: "m:1", entity: () => ${node.parent.className}, fieldName: "${node.parent.fkFieldName}" },`);
|
|
3403
|
+
for (const child of node.children) {
|
|
3404
|
+
const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
|
|
3405
|
+
propertyLines.push(` ${collectionName}: { kind: "1:m", entity: () => ${child.className}, mappedBy: "${child.parent?.propertyName}", cascade: [Cascade.PERSIST, Cascade.REMOVE], orphanRemoval: true },`);
|
|
3406
|
+
}
|
|
3407
|
+
return `export const ${node.schemaConstName} = new EntitySchema<${node.className}>({
|
|
3408
|
+
name: "${contextPrefix}${node.className}",
|
|
3409
|
+
class: ${node.className},
|
|
3410
|
+
tableName: "${node.tableName}",
|
|
3411
|
+
properties: {
|
|
3412
|
+
${propertyLines.join("\n")}
|
|
3413
|
+
},
|
|
3414
|
+
});`;
|
|
3415
|
+
});
|
|
3416
|
+
return `${imports.join("\n")}
|
|
3417
|
+
|
|
3418
|
+
${classBlocks.join("\n\n")}
|
|
3419
|
+
|
|
3420
|
+
${schemaBlocks.join("\n\n")}
|
|
3421
|
+
|
|
3422
|
+
export const ${camelCase(spec.aggregate.name)}MikroOrmEntities = [${nodes.map((node) => node.schemaConstName).join(", ")}] as const;
|
|
3423
|
+
`;
|
|
3424
|
+
}
|
|
3425
|
+
function serializeDomainFieldValue(field, sourceExpr) {
|
|
3426
|
+
if (field.type === "date" || field.type === "Date") return `${sourceExpr} instanceof Date ? ${sourceExpr}.toISOString() : String(${sourceExpr})`;
|
|
3427
|
+
return sourceExpr;
|
|
3428
|
+
}
|
|
3429
|
+
function deserializePersistenceFieldValue(field, sourceExpr) {
|
|
3430
|
+
if (field.type === "date" || field.type === "Date") return `${sourceExpr} ? new Date(${sourceExpr}) : ${sourceExpr}`;
|
|
3431
|
+
return sourceExpr;
|
|
3432
|
+
}
|
|
3433
|
+
function rehydrateIdExpression(node, sourceExpr) {
|
|
3434
|
+
if (node.idType && node.idType !== "string") return `create${pascalCase(node.idType)}(${sourceExpr})`;
|
|
3435
|
+
return `createBrandedId("string", ${sourceExpr})`;
|
|
3436
|
+
}
|
|
3437
|
+
function buildMikroOrmNodeHelperBlock(spec, node) {
|
|
3438
|
+
const domainTypeName = node.isAggregateRoot ? `${pascalCase(spec.aggregate.name)}Aggregate` : pascalCase(node.name);
|
|
3439
|
+
const domainVar = "source";
|
|
3440
|
+
const recordVar = "target";
|
|
3441
|
+
const scalarAssignmentLines = node.scalarFields.map((field) => {
|
|
3442
|
+
const propertyName = camelCase(field.name);
|
|
3443
|
+
return ` ${recordVar}.${propertyName} = ${camelCase(field.name) === camelCase(node.idField) ? `String(${domainVar}.id.value)` : serializeDomainFieldValue(field, `${domainVar}.${propertyName}`)};`;
|
|
3444
|
+
});
|
|
3445
|
+
if (node.isAggregateRoot) scalarAssignmentLines.push(` ${recordVar}.version = ${domainVar}.version;`);
|
|
3446
|
+
const createChildLines = node.children.map((child) => {
|
|
3447
|
+
const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
|
|
3448
|
+
return ` ${recordVar}.${collectionName}.set(${domainVar}.${collectionName}.map((item) => create${child.className}FromDomain(item, ${recordVar})));`;
|
|
3449
|
+
});
|
|
3450
|
+
const syncChildLines = node.children.map((child) => {
|
|
3451
|
+
const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
|
|
3452
|
+
const childIdField = camelCase(child.idField);
|
|
3453
|
+
return ` {
|
|
3454
|
+
const currentById = new Map(${recordVar}.${collectionName}.getItems().map((item: ${child.className}) => [String(item.${childIdField}), item] as const));
|
|
3455
|
+
const nextItems = ${domainVar}.${collectionName}.map((item) => {
|
|
3456
|
+
const existing = currentById.get(String(item.id.value));
|
|
3457
|
+
if (existing) {
|
|
3458
|
+
sync${child.className}FromDomain(existing, item, ${recordVar});
|
|
3459
|
+
return existing;
|
|
3460
|
+
}
|
|
3461
|
+
return create${child.className}FromDomain(item, ${recordVar});
|
|
3462
|
+
});
|
|
3463
|
+
${recordVar}.${collectionName}.set(nextItems);
|
|
3464
|
+
}`;
|
|
3465
|
+
});
|
|
3466
|
+
const parentParam = node.parent ? `, parent: ${node.parent.className}` : "";
|
|
3467
|
+
const parentAssignment = node.parent ? ` record.${node.parent.propertyName} = parent;\n` : "";
|
|
3468
|
+
const parentSyncParam = node.parent ? `, parent?: ${node.parent.className}` : "";
|
|
3469
|
+
const parentSyncAssignment = node.parent ? ` if (parent) {\n ${recordVar}.${node.parent.propertyName} = parent;\n }\n` : "";
|
|
3470
|
+
const domainScalarLines = node.scalarFields.map((field) => {
|
|
3471
|
+
const propertyName = camelCase(field.name);
|
|
3472
|
+
return ` ${propertyName}: ${camelCase(field.name) === camelCase(node.idField) ? rehydrateIdExpression(node, `${recordVar}.${propertyName}`) : deserializePersistenceFieldValue(field, `${recordVar}.${propertyName}`)},`;
|
|
3473
|
+
});
|
|
3474
|
+
if (node.isAggregateRoot) domainScalarLines.push(` version: ${recordVar}.version,`);
|
|
3475
|
+
for (const child of node.children) {
|
|
3476
|
+
const collectionName = camelCase(child.collectionFieldName ?? `${child.name}s`);
|
|
3477
|
+
domainScalarLines.push(` ${collectionName}: ${recordVar}.${collectionName}.getItems().map((item: ${child.className}) => map${child.className}ToDomain(item)),`);
|
|
3478
|
+
}
|
|
3479
|
+
return `function create${node.className}FromDomain(source: ${domainTypeName}${parentParam}): ${node.className} {
|
|
3480
|
+
const record = new ${node.className}();
|
|
3481
|
+
${parentAssignment}${scalarAssignmentLines.join("\n")}
|
|
3482
|
+
${createChildLines.join("\n")}
|
|
3483
|
+
return record;
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
function sync${node.className}FromDomain(target: ${node.className}, source: ${domainTypeName}${parentSyncParam}): void {
|
|
3487
|
+
${parentSyncAssignment}${scalarAssignmentLines.join("\n")}
|
|
3488
|
+
${syncChildLines.join("\n")}
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
function map${node.className}ToDomain(target: ${node.className}): ${domainTypeName} {
|
|
3492
|
+
return ${domainTypeName}.fromPersistence({
|
|
3493
|
+
${domainScalarLines.join("\n")}
|
|
3494
|
+
});
|
|
3495
|
+
}`;
|
|
3496
|
+
}
|
|
3497
|
+
function createMikroOrmRepository(spec, portName) {
|
|
3498
|
+
const modulePath = normalizeModulePath(spec.context.modulePath);
|
|
3499
|
+
const relativePrefix = relativePrefixFromRepositoryDirectory(modulePath);
|
|
3500
|
+
const aggregateName = pascalCase(spec.aggregate.name);
|
|
3501
|
+
const aggregateDir = kebabCase(spec.aggregate.name);
|
|
3502
|
+
const repositoryPortType = repositoryPortTypeName(portName);
|
|
3503
|
+
const repositoryClassName = mikroOrmRepositoryClassName(spec.aggregate.name);
|
|
3504
|
+
const rootNode = buildMikroOrmRootNode(spec);
|
|
3505
|
+
const nodes = flattenMikroOrmNodes(rootNode);
|
|
3506
|
+
const needsBrandedIdHelper = nodes.some((node) => !node.idType || node.idType === "string");
|
|
3507
|
+
const customIdImports = /* @__PURE__ */ new Map();
|
|
3508
|
+
for (const node of nodes) if (node.idType && node.idType !== "string") customIdImports.set(node.idType, `import { create${pascalCase(node.idType)} } from "${relativePrefix}/core/shared-kernel/entity-ids/${kebabCase(node.idType)}";`);
|
|
3509
|
+
const notFoundError = (spec.aggregate.applicationErrors ?? [])[0];
|
|
3510
|
+
const notFoundImports = notFoundError ? (() => {
|
|
3511
|
+
const typeName = `${pascalCase(notFoundError.aggregateName)}NotFoundError`;
|
|
3512
|
+
return `import { ${notFoundError.factoryName}, type ${typeName} } from "${relativePrefix}/core/contexts/${modulePath}/domain/errors/${aggregateDir}-application-errors";\n`;
|
|
3513
|
+
})() : "";
|
|
3514
|
+
const notFoundTypeName = notFoundError ? `${pascalCase(notFoundError.aggregateName)}NotFoundError` : "Error";
|
|
3515
|
+
const notFoundFactory = notFoundError?.factoryName ?? "createNotFoundError";
|
|
3516
|
+
const rootIdParamName = `${camelCase(spec.aggregate.name)}Id`;
|
|
3517
|
+
const rootIdTypeName = rootNode.idType && rootNode.idType !== "string" ? pascalCase(rootNode.idType) : "string";
|
|
3518
|
+
const populatePaths = nodes.filter((node) => !node.isAggregateRoot).map((node) => {
|
|
3519
|
+
const segments = [];
|
|
3520
|
+
let current = node;
|
|
3521
|
+
while (current && !current.isAggregateRoot) {
|
|
3522
|
+
segments.unshift(camelCase(current.collectionFieldName ?? current.name));
|
|
3523
|
+
current = current.parent ? nodes.find((candidate) => candidate.className === current.parent?.className) : void 0;
|
|
3524
|
+
}
|
|
3525
|
+
return segments.join(".");
|
|
3526
|
+
}).filter((value, index, array) => value.length > 0 && array.indexOf(value) === index).sort();
|
|
3527
|
+
return `import type { Result } from "${relativePrefix}/lib/result";
|
|
3528
|
+
import { ok, err } from "${relativePrefix}/lib/result";
|
|
3529
|
+
import type { Transaction } from "${relativePrefix}/lib/transaction";
|
|
3530
|
+
import { ConcurrencyConflictError } from "${relativePrefix}/lib/concurrency-conflict-error";
|
|
3531
|
+
import type { ${repositoryPortType} } from "${relativePrefix}/core/contexts/${modulePath}/application/ports/${kebabCase(portName)}.port";
|
|
3532
|
+
${needsBrandedIdHelper ? `import { createBrandedId } from "${relativePrefix}/lib/branded-id";\n` : ""}${[...customIdImports.values()].join("\n")}${customIdImports.size > 0 ? "\n" : ""}${notFoundImports}import { ${aggregateName}Aggregate } from "${relativePrefix}/core/contexts/${modulePath}/domain/aggregates/${aggregateDir}/${aggregateDir}.aggregate";
|
|
3533
|
+
${nodes.filter((node) => !node.isAggregateRoot).map((node) => `import { ${pascalCase(node.name)}Entity as ${pascalCase(node.name)} } from "${relativePrefix}/core/contexts/${modulePath}/domain/entities/${kebabCase(node.name)}.entity";`).join("\n")}${nodes.some((node) => !node.isAggregateRoot) ? "\n" : ""}import { ${nodes.map((node) => node.className).join(", ")} } from "../../entities/${modulePath}/${kebabCase(spec.aggregate.name)}.entity-schema.ts";
|
|
3534
|
+
|
|
3535
|
+
const TOUCHED_AGGREGATES_KEY = "__zodmireTouchedAggregates";
|
|
3536
|
+
|
|
3537
|
+
function markTouchedAggregate(tx: unknown, aggregateType: string, aggregateId: string): void {
|
|
3538
|
+
const runtimeTx = tx as Record<string, unknown>;
|
|
3539
|
+
const touched = (runtimeTx[TOUCHED_AGGREGATES_KEY] ??= new Map<string, Set<string>>()) as Map<string, Set<string>>;
|
|
3540
|
+
const ids = touched.get(aggregateType) ?? new Set<string>();
|
|
3541
|
+
ids.add(aggregateId);
|
|
3542
|
+
touched.set(aggregateType, ids);
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
${nodes.slice().reverse().map((node) => buildMikroOrmNodeHelperBlock(spec, node)).join("\n\n")}
|
|
3546
|
+
|
|
3547
|
+
export class ${repositoryClassName} implements ${repositoryPortType} {
|
|
3548
|
+
async findById(${rootIdParamName}: ${rootIdTypeName}, tx: Transaction): Promise<Result<${aggregateName}Aggregate, ${notFoundTypeName}>> {
|
|
3549
|
+
const em = tx as any;
|
|
3550
|
+
const record = await em.findOne(${rootNode.className}, { ${camelCase(rootNode.idField)}: ${rootNode.idType && rootNode.idType !== "string" ? `${rootIdParamName}.value` : rootIdParamName} }, { populate: ${populatePaths.length > 0 ? `[${populatePaths.map((path) => `"${path}"`).join(", ")}]` : "[]"} });
|
|
3551
|
+
if (!record) {
|
|
3552
|
+
return err(${notFoundFactory}(String(${rootNode.idType && rootNode.idType !== "string" ? `${rootIdParamName}.value` : rootIdParamName})));
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3555
|
+
return ok(map${rootNode.className}ToDomain(record));
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
async create(${camelCase(spec.aggregate.name)}: ${aggregateName}Aggregate, tx: Transaction): Promise<void> {
|
|
3559
|
+
const em = tx as any;
|
|
3560
|
+
const rootRecord = create${rootNode.className}FromDomain(${camelCase(spec.aggregate.name)});
|
|
3561
|
+
markTouchedAggregate(tx, "${aggregateName}", String(${camelCase(spec.aggregate.name)}.id.value));
|
|
3562
|
+
em.persist(rootRecord);
|
|
3563
|
+
await em.flush();
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
async save(${camelCase(spec.aggregate.name)}: ${aggregateName}Aggregate, expectedVersion: number, tx: Transaction): Promise<void> {
|
|
3567
|
+
const em = tx as any;
|
|
3568
|
+
const current = await em.findOne(${rootNode.className}, { ${camelCase(rootNode.idField)}: String(${camelCase(spec.aggregate.name)}.id.value) }, { populate: ${populatePaths.length > 0 ? `[${populatePaths.map((path) => `"${path}"`).join(", ")}]` : "[]"} });
|
|
3569
|
+
|
|
3570
|
+
if (!current || current.version !== expectedVersion) {
|
|
3571
|
+
throw new ConcurrencyConflictError({
|
|
3572
|
+
aggregateType: "${aggregateName}",
|
|
3573
|
+
aggregateId: String(${camelCase(spec.aggregate.name)}.id.value),
|
|
3574
|
+
expectedVersion,
|
|
3575
|
+
actualVersion: current?.version,
|
|
3576
|
+
});
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
sync${rootNode.className}FromDomain(current, ${camelCase(spec.aggregate.name)});
|
|
3580
|
+
markTouchedAggregate(tx, "${aggregateName}", String(${camelCase(spec.aggregate.name)}.id.value));
|
|
3581
|
+
em.persist(current);
|
|
3582
|
+
await em.flush();
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
`;
|
|
3586
|
+
}
|
|
3094
3587
|
function childTableVar(spec, tableName) {
|
|
3095
3588
|
const child = (spec.aggregate.children ?? []).find((c) => c.tableName === tableName);
|
|
3096
3589
|
return child ? `${camelCase(child.name)}Table` : `${camelCase(tableName)}Table`;
|
|
@@ -3748,29 +4241,39 @@ function buildV5InfrastructureArtifacts(spec, options) {
|
|
|
3748
4241
|
const modulePath = normalizeModulePath(spec.context.modulePath);
|
|
3749
4242
|
const scopeKey = sliceArtifactOwnership(modulePath);
|
|
3750
4243
|
const artifacts = [];
|
|
3751
|
-
|
|
3752
|
-
|
|
4244
|
+
const hasDrizzleRepositoryAdapter = spec.adapters.some((adapter) => adapter.kind === "drizzle-repository");
|
|
4245
|
+
spec.adapters.some((adapter) => adapter.kind === "mikroorm-repository");
|
|
4246
|
+
if (!options?.skipContextWideArtifacts && hasDrizzleRepositoryAdapter) {
|
|
4247
|
+
const tableContent = createDrizzleTableDefinition(spec, resolveDrizzlePersistence(options?.infrastructureStrategy));
|
|
3753
4248
|
if (tableContent !== null) {
|
|
3754
4249
|
const tableLogicalPath = `infrastructure/persistence/${modulePath}/tables.ts`;
|
|
3755
4250
|
artifacts.push(createGeneratedArtifact(tableLogicalPath, tableContent, scopeKey));
|
|
3756
4251
|
}
|
|
3757
4252
|
}
|
|
3758
|
-
const repoArtifacts = spec.adapters.
|
|
4253
|
+
const repoArtifacts = spec.adapters.flatMap((adapter) => {
|
|
3759
4254
|
const port = spec.ports.find((p) => p.name === adapter.port);
|
|
3760
4255
|
if (port === void 0) return [];
|
|
3761
|
-
|
|
3762
|
-
|
|
4256
|
+
if (adapter.kind === "drizzle-repository") {
|
|
4257
|
+
const logicalPath = buildArtifactPath("infrastructure/persistence/drizzle/repositories", modulePath, `drizzle-${kebabCase(spec.aggregate.name)}.repository.ts`);
|
|
4258
|
+
return [createGeneratedArtifact(buildArtifactPath("infrastructure/persistence/drizzle/repositories", modulePath, `${kebabCase(spec.aggregate.name)}.deep-equal.ts`), createPersistenceEqualityArtifactContent(spec), scopeKey), createGeneratedArtifact(logicalPath, createDrizzleRepositoryWithCodec(spec, port.name), scopeKey)];
|
|
4259
|
+
}
|
|
4260
|
+
if (adapter.kind === "mikroorm-repository") {
|
|
4261
|
+
const schemaLogicalPath = buildArtifactPath("infrastructure/persistence/mikroorm/entities", modulePath, `${kebabCase(spec.aggregate.name)}.entity-schema.ts`);
|
|
4262
|
+
const logicalPath = buildArtifactPath("infrastructure/persistence/mikroorm/repositories", modulePath, `mikroorm-${kebabCase(spec.aggregate.name)}.repository.ts`);
|
|
4263
|
+
return [createGeneratedArtifact(schemaLogicalPath, createMikroOrmEntitySchemaArtifactContent(spec), scopeKey), createGeneratedArtifact(logicalPath, createMikroOrmRepository(spec, port.name), scopeKey)];
|
|
4264
|
+
}
|
|
4265
|
+
return [];
|
|
3763
4266
|
});
|
|
3764
4267
|
artifacts.push(...repoArtifacts);
|
|
3765
4268
|
return artifacts.sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
|
|
3766
4269
|
}
|
|
3767
|
-
function createContextDrizzleTableDefinition(contextSpec) {
|
|
4270
|
+
function createContextDrizzleTableDefinition(contextSpec, persistence = "postgres") {
|
|
3768
4271
|
const blocks = [];
|
|
3769
4272
|
const emittedTableNames = /* @__PURE__ */ new Set();
|
|
3770
4273
|
let usesInteger = false;
|
|
3771
4274
|
let usesReal = false;
|
|
3772
4275
|
let usesBoolean = false;
|
|
3773
|
-
let
|
|
4276
|
+
let usesJson = false;
|
|
3774
4277
|
for (const agg of contextSpec.aggregates) {
|
|
3775
4278
|
const fields = filterManagedAggregateFields(agg.fields);
|
|
3776
4279
|
if (!fields || fields.length === 0) continue;
|
|
@@ -3779,8 +4282,8 @@ function createContextDrizzleTableDefinition(contextSpec) {
|
|
|
3779
4282
|
if (aggScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
|
|
3780
4283
|
if (aggScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
|
|
3781
4284
|
if (aggScalarFields.some((field) => field.type === "boolean")) usesBoolean = true;
|
|
3782
|
-
if (aggScalarFields.some((field) => field.array))
|
|
3783
|
-
blocks.push(buildTableBlock(agg.tableName, `${camelCase(agg.name)}Table`, aggScalarFields, agg.idField, void 0, true));
|
|
4285
|
+
if (aggScalarFields.some((field) => field.array)) usesJson = true;
|
|
4286
|
+
blocks.push(buildTableBlock(agg.tableName, `${camelCase(agg.name)}Table`, aggScalarFields, agg.idField, void 0, true, persistence));
|
|
3784
4287
|
emittedTableNames.add(agg.tableName);
|
|
3785
4288
|
walkEntityTree({
|
|
3786
4289
|
name: agg.name,
|
|
@@ -3795,12 +4298,12 @@ function createContextDrizzleTableDefinition(contextSpec) {
|
|
|
3795
4298
|
if (entityScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
|
|
3796
4299
|
if (entityScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
|
|
3797
4300
|
if (entityScalarFields.some((field) => field.type === "boolean")) usesBoolean = true;
|
|
3798
|
-
if (entityScalarFields.some((field) => field.array))
|
|
4301
|
+
if (entityScalarFields.some((field) => field.array)) usesJson = true;
|
|
3799
4302
|
const fkColName = snakeCase("idField" in ctx.parent ? ctx.parent.idField : agg.idField);
|
|
3800
4303
|
blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, {
|
|
3801
4304
|
name: fkColName,
|
|
3802
4305
|
type: "text"
|
|
3803
|
-
}));
|
|
4306
|
+
}, false, persistence));
|
|
3804
4307
|
emittedTableNames.add(entity.tableName);
|
|
3805
4308
|
});
|
|
3806
4309
|
}
|
|
@@ -3810,21 +4313,22 @@ function createContextDrizzleTableDefinition(contextSpec) {
|
|
|
3810
4313
|
if (entityScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
|
|
3811
4314
|
if (entityScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
|
|
3812
4315
|
if (entityScalarFields.some((field) => field.type === "boolean")) usesBoolean = true;
|
|
3813
|
-
if (entityScalarFields.some((field) => field.array))
|
|
3814
|
-
blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField));
|
|
4316
|
+
if (entityScalarFields.some((field) => field.array)) usesJson = true;
|
|
4317
|
+
blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, void 0, false, persistence));
|
|
3815
4318
|
emittedTableNames.add(entity.tableName);
|
|
3816
4319
|
}
|
|
3817
4320
|
if (blocks.length === 0) return null;
|
|
3818
|
-
const
|
|
3819
|
-
if (usesInteger)
|
|
3820
|
-
if (usesReal)
|
|
3821
|
-
if (usesBoolean)
|
|
3822
|
-
if (
|
|
3823
|
-
return `import { ${
|
|
3824
|
-
}
|
|
3825
|
-
function createReadModelTableDefinition(readModel) {
|
|
4321
|
+
const coreImportTokens = [persistence === "mysql" ? "mysqlTable" : "pgTable", "text"];
|
|
4322
|
+
if (usesInteger) coreImportTokens.push("integer");
|
|
4323
|
+
if (usesReal) coreImportTokens.push("real");
|
|
4324
|
+
if (usesBoolean) coreImportTokens.push("boolean");
|
|
4325
|
+
if (usesJson) coreImportTokens.push(persistence === "mysql" ? "json" : "jsonb");
|
|
4326
|
+
return `import { ${coreImportTokens.join(", ")} } from "${persistence === "mysql" ? "drizzle-orm/mysql-core" : "drizzle-orm/pg-core"}";\n\n${blocks.join("\n\n")}\n`;
|
|
4327
|
+
}
|
|
4328
|
+
function createReadModelTableDefinition(readModel, persistence = "postgres") {
|
|
4329
|
+
const tableBuilder = persistence === "mysql" ? "mysqlTable" : "pgTable";
|
|
3826
4330
|
const importTokens = new Set([
|
|
3827
|
-
|
|
4331
|
+
tableBuilder,
|
|
3828
4332
|
"primaryKey",
|
|
3829
4333
|
"text"
|
|
3830
4334
|
]);
|
|
@@ -3832,8 +4336,8 @@ function createReadModelTableDefinition(readModel) {
|
|
|
3832
4336
|
if (field.type === "number" && !isDecimalNumberField(field)) importTokens.add("integer");
|
|
3833
4337
|
if (isDecimalNumberField(field)) importTokens.add("real");
|
|
3834
4338
|
if (field.type === "boolean") importTokens.add("boolean");
|
|
3835
|
-
if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0) importTokens.add("jsonb");
|
|
3836
|
-
return ` ${snakeCase(field.name)}: ${drizzleColumnBuilder(field, snakeCase(field.name))},`;
|
|
4339
|
+
if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0) importTokens.add(persistence === "mysql" ? "json" : "jsonb");
|
|
4340
|
+
return ` ${snakeCase(field.name)}: ${drizzleColumnBuilder(field, snakeCase(field.name), persistence)},`;
|
|
3837
4341
|
});
|
|
3838
4342
|
if (readModel.indexes.length > 0) importTokens.add("index");
|
|
3839
4343
|
if (readModel.uniqueConstraints.length > 0) importTokens.add("uniqueIndex");
|
|
@@ -3842,9 +4346,9 @@ function createReadModelTableDefinition(readModel) {
|
|
|
3842
4346
|
for (const uniqueConstraint of readModel.uniqueConstraints) constraintLines.push(` ${camelCase(uniqueConstraint.name)}: uniqueIndex("${uniqueConstraint.name}").on(${uniqueConstraint.fields.map((field) => `table.${snakeCase(field)}`).join(", ")}),`);
|
|
3843
4347
|
const tableConstName = readModelTableConstName(readModel.name);
|
|
3844
4348
|
const typeName = pascalCase(readModel.name);
|
|
3845
|
-
return `import { ${[...importTokens].sort().join(", ")} } from "drizzle-orm/pg-core";
|
|
4349
|
+
return `import { ${[...importTokens].sort().join(", ")} } from "${persistence === "mysql" ? "drizzle-orm/mysql-core" : "drizzle-orm/pg-core"}";
|
|
3846
4350
|
|
|
3847
|
-
export const ${tableConstName} =
|
|
4351
|
+
export const ${tableConstName} = ${tableBuilder}("${readModel.tableName}", {
|
|
3848
4352
|
${columnLines.join("\n")}
|
|
3849
4353
|
}, (table) => ({
|
|
3850
4354
|
${constraintLines.join("\n")}
|
|
@@ -3889,22 +4393,139 @@ function buildReadModelCompositionArtifacts(contextSpecs) {
|
|
|
3889
4393
|
};
|
|
3890
4394
|
return [createGeneratedArtifact("infrastructure/view-models/drizzle/schema.ts", schemaContent, ownership), createGeneratedArtifact("infrastructure/view-models/drizzle/schema.manifest.json", `${JSON.stringify(manifest, null, 2)}\n`, ownership)];
|
|
3891
4395
|
}
|
|
3892
|
-
function
|
|
4396
|
+
function projectionWriterClassName(projectionName) {
|
|
4397
|
+
const baseName = pascalCase(projectionName);
|
|
4398
|
+
return `MikroOrm${baseName.endsWith("Projection") ? baseName : `${baseName}Projection`}Writer`;
|
|
4399
|
+
}
|
|
4400
|
+
function drizzleProjectionWriterClassName(projectionName) {
|
|
4401
|
+
const baseName = pascalCase(projectionName);
|
|
4402
|
+
return `Drizzle${baseName.endsWith("Projection") ? baseName : `${baseName}Projection`}Writer`;
|
|
4403
|
+
}
|
|
4404
|
+
function projectionWritePortName$1(projectionName) {
|
|
4405
|
+
return `${pascalCase(projectionName)}WritePort`;
|
|
4406
|
+
}
|
|
4407
|
+
function projectionWritePortImportPath(modulePath, projectionName) {
|
|
4408
|
+
return `../../../../../core/contexts/${modulePath}/application/ports/projections/${kebabCase(projectionName)}.projection-write.port.ts`;
|
|
4409
|
+
}
|
|
4410
|
+
function projectionWriterPayloadTypeName(source) {
|
|
4411
|
+
return `${pascalCase(source.eventName)}Payload`;
|
|
4412
|
+
}
|
|
4413
|
+
function projectionWriterPayloadImportPath(source) {
|
|
4414
|
+
return `../../../../../core/contexts/${normalizeModulePath(source.contextName)}/domain/events/${kebabCase(source.eventName)}.event.ts`;
|
|
4415
|
+
}
|
|
4416
|
+
function projectionWriterEventType(source) {
|
|
4417
|
+
const payloadType = projectionWriterPayloadTypeName(source);
|
|
4418
|
+
return `EventEnvelope<${payloadType}> | ${payloadType}`;
|
|
4419
|
+
}
|
|
4420
|
+
function buildProjectionWriterMethod(readModel, source, executorVariable, executorExpression, persistence = "postgres") {
|
|
4421
|
+
const methodName = `on${pascalCase(source.eventName)}`;
|
|
4422
|
+
const eventType = projectionWriterEventType(source);
|
|
4423
|
+
const payloadLine = ` const payload = (((event as any)?.payload ?? event) ?? {}) as Record<string, unknown>;`;
|
|
4424
|
+
const executorLine = ` const ${executorVariable} = ${executorExpression};`;
|
|
4425
|
+
if (source.mutation.kind === "delete") {
|
|
4426
|
+
const whereClause = source.mutation.matchOn.map((field) => `${snakeCase(field)} = ?`).join(" and ");
|
|
4427
|
+
const deleteParams = source.mutation.matchOn.map((field) => `payload.${field}`).join(", ");
|
|
4428
|
+
return ` async ${methodName}(event: ${eventType}, tx: Transaction): Promise<void> {
|
|
4429
|
+
${payloadLine}
|
|
4430
|
+
${executorLine}
|
|
4431
|
+
await ${executorVariable}.execute(
|
|
4432
|
+
${JSON.stringify(`delete from ${readModel.tableName} where ${whereClause}`)},
|
|
4433
|
+
[${deleteParams}],
|
|
4434
|
+
);
|
|
4435
|
+
}`;
|
|
4436
|
+
}
|
|
4437
|
+
const setEntries = Object.entries(source.mutation.set ?? {});
|
|
4438
|
+
const insertColumns = setEntries.map(([fieldName]) => snakeCase(fieldName));
|
|
4439
|
+
const insertParams = setEntries.map(([, mapping]) => buildProjectionMutationValueExpression(mapping));
|
|
4440
|
+
const conflictColumns = source.mutation.matchOn.map((field) => snakeCase(field));
|
|
4441
|
+
const updateAssignments = persistence === "mysql" ? insertColumns.map((column) => `${column} = values(${column})`) : insertColumns.map((column) => `${column} = excluded.${column}`);
|
|
4442
|
+
if (source.mutation.kind === "upsert") {
|
|
4443
|
+
const upsertClause = persistence === "mysql" ? `on duplicate key update ${updateAssignments.join(", ")}` : `on conflict (${conflictColumns.join(", ")}) do update set ${updateAssignments.join(", ")}`;
|
|
4444
|
+
return ` async ${methodName}(event: ${eventType}, tx: Transaction): Promise<void> {
|
|
4445
|
+
${payloadLine}
|
|
4446
|
+
${executorLine}
|
|
4447
|
+
await ${executorVariable}.execute(
|
|
4448
|
+
${JSON.stringify(`insert into ${readModel.tableName} (${insertColumns.join(", ")}) values (${insertColumns.map(() => "?").join(", ")}) ${upsertClause}`)},
|
|
4449
|
+
[${insertParams.join(", ")}],
|
|
4450
|
+
);
|
|
4451
|
+
}`;
|
|
4452
|
+
}
|
|
4453
|
+
const updateAssignmentsSql = insertColumns.map((column) => `${column} = ?`).join(", ");
|
|
4454
|
+
const whereClause = source.mutation.matchOn.map((field) => `${snakeCase(field)} = ?`).join(" and ");
|
|
4455
|
+
const whereParams = source.mutation.matchOn.map((field) => `payload.${field}`);
|
|
4456
|
+
return ` async ${methodName}(event: ${eventType}, tx: Transaction): Promise<void> {
|
|
4457
|
+
${payloadLine}
|
|
4458
|
+
${executorLine}
|
|
4459
|
+
await ${executorVariable}.execute(
|
|
4460
|
+
${JSON.stringify(`update ${readModel.tableName} set ${updateAssignmentsSql} where ${whereClause}`)},
|
|
4461
|
+
[${[...insertParams, ...whereParams].join(", ")}],
|
|
4462
|
+
);
|
|
4463
|
+
}`;
|
|
4464
|
+
}
|
|
4465
|
+
function buildProjectionMutationValueExpression(mapping) {
|
|
4466
|
+
if (mapping.kind === "event-field") return `payload.${mapping.field}`;
|
|
4467
|
+
if (mapping.kind === "literal") return JSON.stringify(mapping.value);
|
|
4468
|
+
return `(${mapping.expression})`;
|
|
4469
|
+
}
|
|
4470
|
+
function createMikroOrmProjectionWriterArtifact(modulePath, projection, readModel, persistence = "postgres") {
|
|
4471
|
+
const className = projectionWriterClassName(projection.name);
|
|
4472
|
+
const writePortName = projectionWritePortName$1(projection.name);
|
|
4473
|
+
const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionWriterPayloadTypeName(source)} } from "${projectionWriterPayloadImportPath(source)}";`);
|
|
4474
|
+
const methodBlocks = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => buildProjectionWriterMethod(readModel, source, "connection", "(tx as any).getConnection()", persistence)).join("\n\n");
|
|
4475
|
+
return createGeneratedArtifact(`infrastructure/persistence/mikroorm/projection-writers/${modulePath}/${kebabCase(projection.name)}.projection-writer.ts`, `import type { EventEnvelope } from "../../../../../lib/event-envelope.ts";
|
|
4476
|
+
import type { Transaction } from "../../../../../lib/transaction.ts";
|
|
4477
|
+
import type { ${writePortName} } from "${projectionWritePortImportPath(modulePath, projection.name)}";
|
|
4478
|
+
${payloadImports.join("\n")}
|
|
4479
|
+
|
|
4480
|
+
// Generated tx-scoped MikroORM projection writer for "${projection.readModelName}".
|
|
4481
|
+
export class ${className} implements ${writePortName} {
|
|
4482
|
+
${methodBlocks}
|
|
4483
|
+
}
|
|
4484
|
+
`, sliceArtifactOwnership(modulePath));
|
|
4485
|
+
}
|
|
4486
|
+
function createDrizzleProjectionWriterArtifact(modulePath, projection, readModel, persistence = "postgres") {
|
|
4487
|
+
const className = drizzleProjectionWriterClassName(projection.name);
|
|
4488
|
+
const writePortName = projectionWritePortName$1(projection.name);
|
|
4489
|
+
const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionWriterPayloadTypeName(source)} } from "${projectionWriterPayloadImportPath(source)}";`);
|
|
4490
|
+
const methodBlocks = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => buildProjectionWriterMethod(readModel, source, "executor", "tx as any", persistence)).join("\n\n");
|
|
4491
|
+
return createGeneratedArtifact(`infrastructure/persistence/drizzle/projection-writers/${modulePath}/${kebabCase(projection.name)}.projection-writer.ts`, `import type { EventEnvelope } from "../../../../../lib/event-envelope.ts";
|
|
4492
|
+
import type { Transaction } from "../../../../../lib/transaction.ts";
|
|
4493
|
+
import type { ${writePortName} } from "${projectionWritePortImportPath(modulePath, projection.name)}";
|
|
4494
|
+
${payloadImports.join("\n")}
|
|
4495
|
+
|
|
4496
|
+
// Generated tx-scoped Drizzle projection writer for "${projection.readModelName}".
|
|
4497
|
+
export class ${className} implements ${writePortName} {
|
|
4498
|
+
${methodBlocks}
|
|
4499
|
+
}
|
|
4500
|
+
`, sliceArtifactOwnership(modulePath));
|
|
4501
|
+
}
|
|
4502
|
+
function buildV5InfrastructureContextArtifacts(contextSpec, options = {}) {
|
|
3893
4503
|
const modulePath = normalizeModulePath(contextSpec.context.modulePath);
|
|
3894
4504
|
const scopeKey = sliceArtifactOwnership(modulePath);
|
|
3895
4505
|
const artifacts = [];
|
|
3896
4506
|
const readModels = contextSpec.readModels ?? [];
|
|
3897
|
-
const tableContent = createContextDrizzleTableDefinition(contextSpec);
|
|
4507
|
+
const tableContent = createContextDrizzleTableDefinition(contextSpec, resolveDrizzlePersistence(options.infrastructureStrategy));
|
|
3898
4508
|
if (tableContent !== null) artifacts.push(createGeneratedArtifact(`infrastructure/persistence/${modulePath}/tables.ts`, tableContent, scopeKey));
|
|
3899
4509
|
if (readModels.length > 0) {
|
|
3900
|
-
for (const readModel of readModels.slice().sort((left, right) => left.name.localeCompare(right.name))) artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/drizzle/tables/${readModelRepositoryFileBase$1(readModel.name)}.table.ts`, createReadModelTableDefinition(readModel), scopeKey));
|
|
4510
|
+
for (const readModel of readModels.slice().sort((left, right) => left.name.localeCompare(right.name))) artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/drizzle/tables/${readModelRepositoryFileBase$1(readModel.name)}.table.ts`, createReadModelTableDefinition(readModel, resolveDrizzlePersistence(options.infrastructureStrategy)), scopeKey));
|
|
3901
4511
|
artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/drizzle/schema.ts`, createReadModelContextSchemaBarrel({
|
|
3902
4512
|
...contextSpec,
|
|
3903
4513
|
readModels
|
|
3904
4514
|
}), scopeKey));
|
|
3905
4515
|
artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/tables.ts`, createReadModelCompatibilityBarrel(), scopeKey));
|
|
3906
4516
|
} else artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/tables.ts`, createViewModelTablesSkeleton(contextSpec), scopeKey));
|
|
3907
|
-
for (const { readModelName, queries } of groupQueriesByReadModel(contextSpec.queries)) artifacts.push(createGeneratedArtifact(`infrastructure/view-models/drizzle/repositories/${modulePath}/${readModelRepositoryFileBase$1(readModelName)}.repository.ts`,
|
|
4517
|
+
for (const { readModelName, queries } of groupQueriesByReadModel(contextSpec.queries)) artifacts.push(createGeneratedArtifact(`infrastructure/view-models/drizzle/repositories/${modulePath}/${readModelRepositoryFileBase$1(readModelName)}.repository.ts`, createReadModelRepositoryContent(modulePath, readModelName, queries, readModels, options.infrastructureStrategy), scopeKey));
|
|
4518
|
+
const projections = contextSpec.projections ?? [];
|
|
4519
|
+
if (options.infrastructureStrategy?.orm === "mikroorm") for (const projection of projections) {
|
|
4520
|
+
const readModel = readModels.find((candidate) => candidate.name === projection.readModelName);
|
|
4521
|
+
if (!readModel) continue;
|
|
4522
|
+
artifacts.push(createMikroOrmProjectionWriterArtifact(modulePath, projection, readModel, resolveDrizzlePersistence(options.infrastructureStrategy)));
|
|
4523
|
+
}
|
|
4524
|
+
else if (options.infrastructureStrategy?.orm === "drizzle") for (const projection of projections) {
|
|
4525
|
+
const readModel = readModels.find((candidate) => candidate.name === projection.readModelName);
|
|
4526
|
+
if (!readModel) throw new Error(`[infrastructure_generator] Projection "${projection.name}" cannot be fully generated because read-model "${projection.readModelName}" was not found.`);
|
|
4527
|
+
artifacts.push(createDrizzleProjectionWriterArtifact(modulePath, projection, readModel, resolveDrizzlePersistence(options.infrastructureStrategy)));
|
|
4528
|
+
}
|
|
3908
4529
|
return artifacts;
|
|
3909
4530
|
}
|
|
3910
4531
|
function createViewModelTablesSkeleton(contextSpec) {
|
|
@@ -3919,7 +4540,7 @@ function createViewModelTablesSkeleton(contextSpec) {
|
|
|
3919
4540
|
for (const { readModelName, queries } of groupQueriesByReadModel(contextSpec.queries)) {
|
|
3920
4541
|
lines.push(` ${camelCase(readModelName)}: {`);
|
|
3921
4542
|
lines.push(` readModel: "${readModelName}",`);
|
|
3922
|
-
lines.push(` contract: "${readModelContractName$
|
|
4543
|
+
lines.push(` contract: "${readModelContractName$1(readModelName)}",`);
|
|
3923
4544
|
lines.push(` servesQueries: [${queries.map((query) => `"${query.name}"`).join(", ")}],`);
|
|
3924
4545
|
lines.push(` suggestedTableName: "${snakeCase(modulePath).replaceAll("/", "_")}_${snakeCase(readModelRepositoryFileBase$1(readModelName))}",`);
|
|
3925
4546
|
lines.push(` },`);
|
|
@@ -3928,40 +4549,33 @@ function createViewModelTablesSkeleton(contextSpec) {
|
|
|
3928
4549
|
lines.push(``);
|
|
3929
4550
|
return lines.join("\n");
|
|
3930
4551
|
}
|
|
4552
|
+
function createReadModelRepositoryContent(modulePath, readModelName, queries, readModels, infrastructureStrategy) {
|
|
4553
|
+
if (!supportsGeneratedReadModelQueries(infrastructureStrategy)) return createReadModelRepositorySkeleton(modulePath, readModelName, queries);
|
|
4554
|
+
const readModel = readModels.find((candidate) => candidate.name === readModelName);
|
|
4555
|
+
if (!readModel) return createReadModelRepositorySkeleton(modulePath, readModelName, queries);
|
|
4556
|
+
return createSupportedReadModelRepository(modulePath, readModel, queries);
|
|
4557
|
+
}
|
|
4558
|
+
function supportsGeneratedReadModelQueries(infrastructureStrategy) {
|
|
4559
|
+
if (!infrastructureStrategy) return true;
|
|
4560
|
+
return (infrastructureStrategy.architecture === "physical-cqrs" || infrastructureStrategy.architecture === "logical-cqrs") && (infrastructureStrategy.persistence === "postgres" || infrastructureStrategy.persistence === "mysql");
|
|
4561
|
+
}
|
|
3931
4562
|
function createReadModelRepositorySkeleton(modulePath, readModelName, queries) {
|
|
3932
4563
|
const className = readModelRepositoryClassName(readModelName);
|
|
3933
|
-
const portTypeName = `${readModelContractName$
|
|
4564
|
+
const portTypeName = `${readModelContractName$1(readModelName)}RepositoryPort`;
|
|
3934
4565
|
const fileBase = readModelRepositoryFileBase$1(readModelName);
|
|
3935
4566
|
const importLines = /* @__PURE__ */ new Map();
|
|
3936
4567
|
let needsPaginatedResult = false;
|
|
3937
4568
|
importLines.set("tx", `import type { Transaction } from "../../../../../lib/transaction.ts";`);
|
|
3938
4569
|
importLines.set("port", `import type { ${portTypeName} } from "../../../../../core/contexts/${modulePath}/application/ports/read-models/${fileBase}.repository.port.ts";`);
|
|
3939
4570
|
const methodBlocks = queries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((query) => {
|
|
3940
|
-
|
|
4571
|
+
queryReadModelMethodName(query);
|
|
3941
4572
|
const queryTypeName = `${pascalCase(query.name)}Query`;
|
|
3942
4573
|
const outputTypeName = queryOutputContractName(query);
|
|
3943
|
-
const viewFileBase = queryViewFileBase
|
|
4574
|
+
const viewFileBase = queryViewFileBase(query);
|
|
3944
4575
|
importLines.set(`query:${queryTypeName}`, `import type { ${queryTypeName} } from "../../../../../core/contexts/${modulePath}/application/queries/${kebabCase(query.name)}.query.ts";`);
|
|
3945
4576
|
importLines.set(`view:${outputTypeName}`, `import type { ${outputTypeName} } from "../../../../../core/contexts/${modulePath}/application/contracts/${viewFileBase}.view.ts";`);
|
|
3946
|
-
if (query.queryKind === "list")
|
|
3947
|
-
|
|
3948
|
-
return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<PaginatedResult<${outputTypeName}>> {
|
|
3949
|
-
void this.db;
|
|
3950
|
-
void tx;
|
|
3951
|
-
const { page = 1, pageSize = 20 } = query.payload;
|
|
3952
|
-
return { items: [], total: 0, page, pageSize };
|
|
3953
|
-
}`;
|
|
3954
|
-
}
|
|
3955
|
-
return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<${outputTypeName}> {
|
|
3956
|
-
void this.db;
|
|
3957
|
-
void query;
|
|
3958
|
-
void tx;
|
|
3959
|
-
return {
|
|
3960
|
-
${query.outputFields.map((field) => {
|
|
3961
|
-
return ` ${camelCase(field.name)}: ${placeholderValueForField(field)}, // TODO: map from projection row`;
|
|
3962
|
-
}).join("\n")}
|
|
3963
|
-
};
|
|
3964
|
-
}`;
|
|
4577
|
+
if (query.queryKind === "list") needsPaginatedResult = true;
|
|
4578
|
+
return createSingleReadModelMethodSkeleton(query, readModelName);
|
|
3965
4579
|
});
|
|
3966
4580
|
if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "../../../../../lib/pagination.ts";`);
|
|
3967
4581
|
return `${[...importLines.values()].join("\n")}
|
|
@@ -3974,18 +4588,229 @@ ${methodBlocks.join("\n\n")}
|
|
|
3974
4588
|
}
|
|
3975
4589
|
`;
|
|
3976
4590
|
}
|
|
4591
|
+
function createSupportedReadModelRepository(modulePath, readModel, queries) {
|
|
4592
|
+
const className = readModelRepositoryClassName(readModel.name);
|
|
4593
|
+
const portTypeName = `${readModelContractName$1(readModel.name)}RepositoryPort`;
|
|
4594
|
+
const fileBase = readModelRepositoryFileBase$1(readModel.name);
|
|
4595
|
+
const importLines = /* @__PURE__ */ new Map();
|
|
4596
|
+
const drizzleImports = /* @__PURE__ */ new Set();
|
|
4597
|
+
let needsPaginatedResult = false;
|
|
4598
|
+
let needsSqlType = false;
|
|
4599
|
+
let needsTableImport = false;
|
|
4600
|
+
importLines.set("tx", `import type { Transaction } from "../../../../../lib/transaction.ts";`);
|
|
4601
|
+
importLines.set("port", `import type { ${portTypeName} } from "../../../../../core/contexts/${modulePath}/application/ports/read-models/${fileBase}.repository.port.ts";`);
|
|
4602
|
+
const methodBlocks = queries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((query) => {
|
|
4603
|
+
const queryTypeName = `${pascalCase(query.name)}Query`;
|
|
4604
|
+
const outputTypeName = queryOutputContractName(query);
|
|
4605
|
+
const viewFileBase = queryViewFileBase(query);
|
|
4606
|
+
importLines.set(`query:${queryTypeName}`, `import type { ${queryTypeName} } from "../../../../../core/contexts/${modulePath}/application/queries/${kebabCase(query.name)}.query.ts";`);
|
|
4607
|
+
importLines.set(`view:${outputTypeName}`, `import type { ${outputTypeName} } from "../../../../../core/contexts/${modulePath}/application/contracts/${viewFileBase}.view.ts";`);
|
|
4608
|
+
const generatedMethod = createGeneratedReadModelMethod(readModel, query, drizzleImports);
|
|
4609
|
+
if (generatedMethod !== null) {
|
|
4610
|
+
needsTableImport = true;
|
|
4611
|
+
if (query.queryKind === "list") {
|
|
4612
|
+
needsPaginatedResult = true;
|
|
4613
|
+
needsSqlType = needsSqlType || generatedMethod.includes("SQL[]");
|
|
4614
|
+
}
|
|
4615
|
+
return generatedMethod;
|
|
4616
|
+
}
|
|
4617
|
+
if (query.queryKind === "list") needsPaginatedResult = true;
|
|
4618
|
+
return createSingleReadModelMethodSkeleton(query, readModel.name);
|
|
4619
|
+
});
|
|
4620
|
+
if (needsTableImport) importLines.set("table", `import { ${readModelTableConstName(readModel.name)} } from "../../../${modulePath}/drizzle/tables/${fileBase}.table.ts";`);
|
|
4621
|
+
if (drizzleImports.size > 0) importLines.set("drizzle", `import { ${[...drizzleImports].sort().join(", ")} } from "drizzle-orm";`);
|
|
4622
|
+
if (needsSqlType) importLines.set("sql", `import type { SQL } from "drizzle-orm";`);
|
|
4623
|
+
if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "../../../../../lib/pagination.ts";`);
|
|
4624
|
+
return `${[...importLines.values()].join("\n")}
|
|
4625
|
+
|
|
4626
|
+
// Generated read-model repository for the supported infrastructure strategy.
|
|
4627
|
+
export class ${className} implements ${portTypeName} {
|
|
4628
|
+
constructor(private readonly db: unknown) {}
|
|
4629
|
+
|
|
4630
|
+
${methodBlocks.join("\n\n")}
|
|
4631
|
+
}
|
|
4632
|
+
`;
|
|
4633
|
+
}
|
|
4634
|
+
function createGeneratedReadModelMethod(readModel, query, drizzleImports) {
|
|
4635
|
+
if (!canMapReadModelOutput(readModel, query.outputFields)) return null;
|
|
4636
|
+
if (query.queryKind === "findById") {
|
|
4637
|
+
const lookupField = resolveFindByIdLookupField(readModel, query);
|
|
4638
|
+
if (!lookupField) return null;
|
|
4639
|
+
drizzleImports.add("eq");
|
|
4640
|
+
return createFindByIdReadModelMethod(readModel, query, lookupField);
|
|
4641
|
+
}
|
|
4642
|
+
drizzleImports.add("asc");
|
|
4643
|
+
drizzleImports.add("count");
|
|
4644
|
+
drizzleImports.add("desc");
|
|
4645
|
+
const filterData = buildReadModelFilterData(readModel, query, readModelTableConstName(readModel.name));
|
|
4646
|
+
if (filterData.lines.length > 0) drizzleImports.add("and");
|
|
4647
|
+
for (const drizzleImport of filterData.imports) drizzleImports.add(drizzleImport);
|
|
4648
|
+
return createListReadModelMethod(readModel, query, filterData.lines);
|
|
4649
|
+
}
|
|
4650
|
+
function canMapReadModelOutput(readModel, outputFields) {
|
|
4651
|
+
const readModelFields = new Set(readModel.fields.map((field) => camelCase(field.name)));
|
|
4652
|
+
return outputFields.every((field) => readModelFields.has(camelCase(field.name)));
|
|
4653
|
+
}
|
|
4654
|
+
function resolveFindByIdLookupField(readModel, query) {
|
|
4655
|
+
if (readModel.primaryKey.length !== 1) return null;
|
|
4656
|
+
const primaryKeyField = camelCase(readModel.primaryKey[0]);
|
|
4657
|
+
const matchingField = query.inputFields.find((field) => camelCase(field.name) === primaryKeyField);
|
|
4658
|
+
if (matchingField) return camelCase(matchingField.name);
|
|
4659
|
+
if (query.inputFields.length === 1) return camelCase(query.inputFields[0].name);
|
|
4660
|
+
return null;
|
|
4661
|
+
}
|
|
4662
|
+
function createFindByIdReadModelMethod(readModel, query, lookupField) {
|
|
4663
|
+
const methodName = queryReadModelMethodName(query);
|
|
4664
|
+
const queryTypeName = `${pascalCase(query.name)}Query`;
|
|
4665
|
+
const outputTypeName = queryOutputContractName(query);
|
|
4666
|
+
const tableConstName = readModelTableConstName(readModel.name);
|
|
4667
|
+
return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<${outputTypeName}> {
|
|
4668
|
+
void this.db;
|
|
4669
|
+
const rows = await tx.select()
|
|
4670
|
+
.from(${tableConstName})
|
|
4671
|
+
.where(eq(${tableConstName}.${snakeCase(readModel.primaryKey[0])}, query.payload.${lookupField}))
|
|
4672
|
+
.limit(1);
|
|
4673
|
+
|
|
4674
|
+
const row = rows[0];
|
|
4675
|
+
if (!row) {
|
|
4676
|
+
throw new Error("${outputTypeName} row not found");
|
|
4677
|
+
}
|
|
4678
|
+
|
|
4679
|
+
return {
|
|
4680
|
+
${buildReadModelOutputMapping(query.outputFields, "row")}
|
|
4681
|
+
};
|
|
4682
|
+
}`;
|
|
4683
|
+
}
|
|
4684
|
+
function createListReadModelMethod(readModel, query, filterLines) {
|
|
4685
|
+
const methodName = queryReadModelMethodName(query);
|
|
4686
|
+
const queryTypeName = `${pascalCase(query.name)}Query`;
|
|
4687
|
+
const outputTypeName = queryOutputContractName(query);
|
|
4688
|
+
const tableConstName = readModelTableConstName(readModel.name);
|
|
4689
|
+
const payloadDestructure = filterLines.length > 0 ? `page = 1, pageSize = 20, filters, sortBy, sortDirection` : `page = 1, pageSize = 20, sortBy, sortDirection`;
|
|
4690
|
+
const whereBlock = filterLines.length > 0 ? ` const conditions: SQL[] = [];
|
|
4691
|
+
${filterLines.join("\n")}
|
|
4692
|
+
|
|
4693
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;` : ` const where = undefined;`;
|
|
4694
|
+
const sortFieldEntries = query.sorting.sortableFields.filter((field) => readModel.fields.some((candidate) => camelCase(candidate.name) === camelCase(field))).map((field) => ` ${camelCase(field)}: ${tableConstName}.${snakeCase(field)},`);
|
|
4695
|
+
const defaultSortField = camelCase(query.sorting.defaultSort.field);
|
|
4696
|
+
return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<PaginatedResult<${outputTypeName}>> {
|
|
4697
|
+
void this.db;
|
|
4698
|
+
const { ${payloadDestructure} } = query.payload;
|
|
4699
|
+
|
|
4700
|
+
${whereBlock}
|
|
4701
|
+
const sortColumn = (({
|
|
4702
|
+
${sortFieldEntries.join("\n")}
|
|
4703
|
+
} as Record<string, unknown>)[sortBy ?? "${defaultSortField}"] ?? ${tableConstName}.${snakeCase(query.sorting.defaultSort.field)}) as import("drizzle-orm").AnyColumn;
|
|
4704
|
+
const orderBy = sortDirection === "desc"
|
|
4705
|
+
? [desc(sortColumn)]
|
|
4706
|
+
: [asc(sortColumn)];
|
|
4707
|
+
|
|
4708
|
+
const [rows, countResult] = await Promise.all([
|
|
4709
|
+
tx.select()
|
|
4710
|
+
.from(${tableConstName})
|
|
4711
|
+
.where(where)
|
|
4712
|
+
.orderBy(...orderBy)
|
|
4713
|
+
.limit(pageSize)
|
|
4714
|
+
.offset((page - 1) * pageSize),
|
|
4715
|
+
tx.select({ count: count() }).from(${tableConstName}).where(where),
|
|
4716
|
+
]);
|
|
4717
|
+
|
|
4718
|
+
return {
|
|
4719
|
+
items: rows.map((row) => ({
|
|
4720
|
+
${buildReadModelOutputMapping(query.outputFields, "row")}
|
|
4721
|
+
})),
|
|
4722
|
+
total: Number(countResult[0]?.count ?? 0),
|
|
4723
|
+
page,
|
|
4724
|
+
pageSize,
|
|
4725
|
+
};
|
|
4726
|
+
}`;
|
|
4727
|
+
}
|
|
4728
|
+
function buildReadModelFilterData(readModel, query, tableConstName) {
|
|
4729
|
+
const readModelFields = new Set(readModel.fields.map((field) => camelCase(field.name)));
|
|
4730
|
+
const lines = [];
|
|
4731
|
+
const imports = /* @__PURE__ */ new Set();
|
|
4732
|
+
for (const [field, operators] of Object.entries(query.filters)) {
|
|
4733
|
+
const normalizedField = camelCase(field);
|
|
4734
|
+
if (!readModelFields.has(normalizedField)) continue;
|
|
4735
|
+
const columnRef = `${tableConstName}.${snakeCase(field)}`;
|
|
4736
|
+
for (const operator of operators) switch (operator) {
|
|
4737
|
+
case "eq":
|
|
4738
|
+
imports.add("eq");
|
|
4739
|
+
lines.push(` if (filters?.${normalizedField}?.eq != null) conditions.push(eq(${columnRef}, filters.${normalizedField}.eq));`);
|
|
4740
|
+
break;
|
|
4741
|
+
case "in":
|
|
4742
|
+
imports.add("inArray");
|
|
4743
|
+
lines.push(` if (filters?.${normalizedField}?.in) conditions.push(inArray(${columnRef}, filters.${normalizedField}.in));`);
|
|
4744
|
+
break;
|
|
4745
|
+
case "gt":
|
|
4746
|
+
imports.add("gt");
|
|
4747
|
+
lines.push(` if (filters?.${normalizedField}?.gt != null) conditions.push(gt(${columnRef}, filters.${normalizedField}.gt));`);
|
|
4748
|
+
break;
|
|
4749
|
+
case "gte":
|
|
4750
|
+
imports.add("gte");
|
|
4751
|
+
lines.push(` if (filters?.${normalizedField}?.gte != null) conditions.push(gte(${columnRef}, filters.${normalizedField}.gte));`);
|
|
4752
|
+
break;
|
|
4753
|
+
case "lt":
|
|
4754
|
+
imports.add("lt");
|
|
4755
|
+
lines.push(` if (filters?.${normalizedField}?.lt != null) conditions.push(lt(${columnRef}, filters.${normalizedField}.lt));`);
|
|
4756
|
+
break;
|
|
4757
|
+
case "lte":
|
|
4758
|
+
imports.add("lte");
|
|
4759
|
+
lines.push(` if (filters?.${normalizedField}?.lte != null) conditions.push(lte(${columnRef}, filters.${normalizedField}.lte));`);
|
|
4760
|
+
break;
|
|
4761
|
+
case "contains":
|
|
4762
|
+
imports.add("like");
|
|
4763
|
+
lines.push(` if (filters?.${normalizedField}?.contains) conditions.push(like(${columnRef}, \`%\${filters.${normalizedField}.contains}%\`));`);
|
|
4764
|
+
break;
|
|
4765
|
+
case "startsWith":
|
|
4766
|
+
imports.add("like");
|
|
4767
|
+
lines.push(` if (filters?.${normalizedField}?.startsWith) conditions.push(like(${columnRef}, \`\${filters.${normalizedField}.startsWith}%\`));`);
|
|
4768
|
+
break;
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
return {
|
|
4772
|
+
lines,
|
|
4773
|
+
imports
|
|
4774
|
+
};
|
|
4775
|
+
}
|
|
4776
|
+
function buildReadModelOutputMapping(outputFields, rowVar) {
|
|
4777
|
+
return outputFields.map((field) => {
|
|
4778
|
+
const outputField = camelCase(field.name);
|
|
4779
|
+
const rowField = snakeCase(field.name);
|
|
4780
|
+
return ` ${outputField}: ${field.optional ? `${rowVar}.${rowField} ?? undefined` : `${rowVar}.${rowField}`},`;
|
|
4781
|
+
}).join("\n");
|
|
4782
|
+
}
|
|
4783
|
+
function createSingleReadModelMethodSkeleton(query, readModelName) {
|
|
4784
|
+
const methodName = queryReadModelMethodName(query);
|
|
4785
|
+
const queryTypeName = `${pascalCase(query.name)}Query`;
|
|
4786
|
+
const outputTypeName = queryOutputContractName(query);
|
|
4787
|
+
const targetName = readModelName ?? resolvedReadModelName$1(query);
|
|
4788
|
+
if (query.queryKind === "list") return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<PaginatedResult<${outputTypeName}>> {
|
|
4789
|
+
void this.db;
|
|
4790
|
+
void tx;
|
|
4791
|
+
throw new Error(
|
|
4792
|
+
"[infrastructure_generator] Query \\"${query.name}\\" for read-model \\"${targetName}\\" cannot be fully generated. " +
|
|
4793
|
+
"Ensure output fields map directly to read-model fields and list-query metadata is fully declared.",
|
|
4794
|
+
);
|
|
4795
|
+
}`;
|
|
4796
|
+
return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<${outputTypeName}> {
|
|
4797
|
+
void this.db;
|
|
4798
|
+
void query;
|
|
4799
|
+
void tx;
|
|
4800
|
+
throw new Error(
|
|
4801
|
+
"[infrastructure_generator] Query \\"${query.name}\\" for read-model \\"${targetName}\\" cannot be fully generated. " +
|
|
4802
|
+
"Ensure output fields map directly to read-model fields and findById queries resolve a stable lookup field.",
|
|
4803
|
+
);
|
|
4804
|
+
}`;
|
|
4805
|
+
}
|
|
3977
4806
|
|
|
3978
4807
|
//#endregion
|
|
3979
4808
|
//#region packages/core/generators/projections.ts
|
|
3980
|
-
function
|
|
3981
|
-
|
|
3982
|
-
return baseName.endsWith("View") ? baseName : `${baseName}View`;
|
|
4809
|
+
function projectionWritePortName(projectionName) {
|
|
4810
|
+
return `${pascalCase(projectionName)}WritePort`;
|
|
3983
4811
|
}
|
|
3984
|
-
function
|
|
3985
|
-
return
|
|
3986
|
-
}
|
|
3987
|
-
function readModelRepositoryPortFileBase(readModelName) {
|
|
3988
|
-
return kebabCase(readModelName).replace(/-view$/, "");
|
|
4812
|
+
function projectionWritePortVariableName(projectionName) {
|
|
4813
|
+
return camelCase(projectionWritePortName(projectionName));
|
|
3989
4814
|
}
|
|
3990
4815
|
function projectionFileBase(projectionName) {
|
|
3991
4816
|
return kebabCase(projectionName);
|
|
@@ -3993,57 +4818,43 @@ function projectionFileBase(projectionName) {
|
|
|
3993
4818
|
function projectionVariableName(projectionName) {
|
|
3994
4819
|
return camelCase(projectionName);
|
|
3995
4820
|
}
|
|
3996
|
-
function readModelRepositoryVariableName(readModelName) {
|
|
3997
|
-
return camelCase(readModelRepositoryPortName(readModelName));
|
|
3998
|
-
}
|
|
3999
4821
|
function projectionSourceHandlerName(eventName) {
|
|
4000
4822
|
return `on${pascalCase(eventName)}`;
|
|
4001
4823
|
}
|
|
4824
|
+
function projectionSourcePayloadTypeName(source) {
|
|
4825
|
+
return `${pascalCase(source.eventName)}Payload`;
|
|
4826
|
+
}
|
|
4827
|
+
function projectionSourcePayloadImportPath(currentModulePath, source) {
|
|
4828
|
+
return `../../${normalizeModulePath(source.contextName)}/domain/events/${kebabCase(source.eventName)}.event.ts`;
|
|
4829
|
+
}
|
|
4830
|
+
function projectionSourceEventType(source) {
|
|
4831
|
+
const payloadType = projectionSourcePayloadTypeName(source);
|
|
4832
|
+
return `EventEnvelope<${payloadType}> | ${payloadType}`;
|
|
4833
|
+
}
|
|
4002
4834
|
function buildProjectionSourceMethod(projection, source) {
|
|
4003
4835
|
const methodName = projectionSourceHandlerName(source.eventName);
|
|
4004
4836
|
const roleComment = source.role === "primary" ? `// Primary source: this event defines row lifecycle for the "${projection.readModelName}" read model.` : `// Enrichment source: update existing rows only; this event must not redefine ownership.`;
|
|
4005
|
-
|
|
4006
|
-
return await this.${source.mutation.handlerName}(event, tx);
|
|
4007
|
-
}
|
|
4008
|
-
|
|
4009
|
-
protected async ${source.mutation.handlerName}(event: unknown, tx: Transaction): Promise<void> {
|
|
4010
|
-
void this.${readModelRepositoryVariableName(projection.readModelName)};
|
|
4011
|
-
void event;
|
|
4012
|
-
void tx;
|
|
4013
|
-
${roleComment}
|
|
4014
|
-
// Custom projection hook "${source.mutation.handlerName}" for ${source.contextName}.${source.aggregateName}.${source.eventName}.
|
|
4015
|
-
// TODO: implement custom projector logic against the generated read-model repository port.
|
|
4016
|
-
}`;
|
|
4017
|
-
const setFields = source.mutation.kind === "delete" ? "" : Object.entries(source.mutation.set ?? {}).map(([fieldName, valueSource]) => {
|
|
4018
|
-
if (valueSource.kind === "event-field") return ` // ${fieldName} <- event.${valueSource.field}`;
|
|
4019
|
-
if (valueSource.kind === "literal") return ` // ${fieldName} <- ${JSON.stringify(valueSource.value)}`;
|
|
4020
|
-
return ` // ${fieldName} <- ${valueSource.expression}`;
|
|
4021
|
-
}).join("\n");
|
|
4022
|
-
const matchOnComment = `"${source.mutation.matchOn.join("\", \"")}"`;
|
|
4023
|
-
return ` async ${methodName}(event: unknown, tx: Transaction): Promise<void> {
|
|
4024
|
-
void this.${readModelRepositoryVariableName(projection.readModelName)};
|
|
4025
|
-
void event;
|
|
4026
|
-
void tx;
|
|
4837
|
+
return ` async ${methodName}(event: ${projectionSourceEventType(source)}, tx: Transaction): Promise<void> {
|
|
4027
4838
|
${roleComment}
|
|
4028
|
-
|
|
4029
|
-
// Match on: [${matchOnComment}]
|
|
4030
|
-
${setFields}
|
|
4031
|
-
// TODO: call generated projector-facing repository methods when write helpers land.
|
|
4839
|
+
return await this.${projectionWritePortVariableName(projection.name)}.${methodName}(event, tx);
|
|
4032
4840
|
}`;
|
|
4033
4841
|
}
|
|
4034
4842
|
function buildProjectorArtifact(modulePath, projection) {
|
|
4035
4843
|
const projectorClassName = projection.name;
|
|
4036
|
-
const
|
|
4037
|
-
const
|
|
4844
|
+
const writePortName = projectionWritePortName(projection.name);
|
|
4845
|
+
const writePortVar = projectionWritePortVariableName(projection.name);
|
|
4038
4846
|
const fileBase = projectionFileBase(projection.name);
|
|
4847
|
+
const payloadImports = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => `import type { ${projectionSourcePayloadTypeName(source)} } from "${projectionSourcePayloadImportPath(modulePath, source)}";`);
|
|
4039
4848
|
const methods = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => buildProjectionSourceMethod(projection, source)).join("\n\n");
|
|
4040
|
-
return createGeneratedArtifact(`core/contexts/${modulePath}/application/projections/${fileBase}.projector.ts`, `import type {
|
|
4041
|
-
import type {
|
|
4849
|
+
return createGeneratedArtifact(`core/contexts/${modulePath}/application/projections/${fileBase}.projector.ts`, `import type { EventEnvelope } from "../../../../../lib/event-envelope.ts";
|
|
4850
|
+
import type { Transaction } from "../../../../../lib/transaction.ts";
|
|
4851
|
+
import type { ${writePortName} } from "../ports/projections/${projectionFileBase(projection.name)}.projection-write.port.ts";
|
|
4852
|
+
${payloadImports.join("\n")}
|
|
4042
4853
|
|
|
4043
4854
|
// Auto-generated projector skeleton for the "${projection.readModelName}" read model.
|
|
4044
4855
|
export class ${projectorClassName} {
|
|
4045
4856
|
constructor(
|
|
4046
|
-
private readonly ${
|
|
4857
|
+
private readonly ${writePortVar}: ${writePortName},
|
|
4047
4858
|
) {}
|
|
4048
4859
|
|
|
4049
4860
|
${methods}
|
|
@@ -4056,20 +4867,53 @@ function buildProjectionRebuildArtifact(modulePath, projection) {
|
|
|
4056
4867
|
const projectorClassName = projection.name;
|
|
4057
4868
|
const rebuildFunctionName = `rebuild${pascalCase(projection.name)}`;
|
|
4058
4869
|
const batchSize = projection.rebuild.batchSize ?? 500;
|
|
4059
|
-
return createGeneratedArtifact(`core/contexts/${modulePath}/application/projections/${fileBase}.rebuild.ts`, `import type { Transaction } from "
|
|
4870
|
+
return createGeneratedArtifact(`core/contexts/${modulePath}/application/projections/${fileBase}.rebuild.ts`, `import type { Transaction } from "../../../../../lib/transaction.ts";
|
|
4060
4871
|
import type { ${projectorClassName} } from "./${fileBase}.projector.ts";
|
|
4061
4872
|
|
|
4873
|
+
export type ProjectionRebuildBatch = {
|
|
4874
|
+
readonly events: Array<{
|
|
4875
|
+
readonly type: string;
|
|
4876
|
+
readonly payload?: unknown;
|
|
4877
|
+
}>;
|
|
4878
|
+
};
|
|
4879
|
+
|
|
4880
|
+
export interface ProjectionRebuildEventSource {
|
|
4881
|
+
stream(args: {
|
|
4882
|
+
projectionName: string;
|
|
4883
|
+
strategy: string;
|
|
4884
|
+
batchSize: number;
|
|
4885
|
+
tx: Transaction;
|
|
4886
|
+
}): AsyncIterable<ProjectionRebuildBatch>;
|
|
4887
|
+
}
|
|
4888
|
+
|
|
4062
4889
|
export type ${pascalCase(projection.name)}RebuildDeps = {
|
|
4063
4890
|
projector: ${projectorClassName};
|
|
4891
|
+
eventSource: ProjectionRebuildEventSource;
|
|
4064
4892
|
transaction: Transaction;
|
|
4065
4893
|
};
|
|
4066
4894
|
|
|
4067
|
-
// Auto-generated rebuild
|
|
4895
|
+
// Auto-generated rebuild runner for the "${projection.readModelName}" read model.
|
|
4068
4896
|
export async function ${rebuildFunctionName}(deps: ${pascalCase(projection.name)}RebuildDeps): Promise<void> {
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4897
|
+
const batchSize = ${batchSize};
|
|
4898
|
+
|
|
4899
|
+
for await (
|
|
4900
|
+
const batch of deps.eventSource.stream({
|
|
4901
|
+
projectionName: "${projection.name}",
|
|
4902
|
+
strategy: "${projection.rebuild.strategy}",
|
|
4903
|
+
batchSize,
|
|
4904
|
+
tx: deps.transaction,
|
|
4905
|
+
})
|
|
4906
|
+
) {
|
|
4907
|
+
for (const event of batch.events) {
|
|
4908
|
+
switch (event.type) {
|
|
4909
|
+
${projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => ` case "${source.contextName}.${source.aggregateName}.${source.eventName}":
|
|
4910
|
+
await deps.projector.${projectionSourceHandlerName(source.eventName)}(event, deps.transaction);
|
|
4911
|
+
break;`).join("\n")}
|
|
4912
|
+
default:
|
|
4913
|
+
break;
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4073
4917
|
}
|
|
4074
4918
|
`, contextArtifactOwnership(modulePath));
|
|
4075
4919
|
}
|
|
@@ -4087,7 +4931,8 @@ function buildProjectionSubscriptionsArtifact(contextSpec) {
|
|
|
4087
4931
|
const depsTypeName = `${pascalCase(contextSpec.context.name)}ProjectionSubscriptionDeps`;
|
|
4088
4932
|
const builderName = `build${pascalCase(contextSpec.context.name)}ProjectionSubscriptions`;
|
|
4089
4933
|
const dependencyLines = contextSpec.projections.map((projection) => ` ${projectionVariableName(projection.name)}: ${projection.name};`);
|
|
4090
|
-
const importLines = contextSpec.projections.map((projection) => `import type { ${projection.name} } from "
|
|
4934
|
+
const importLines = contextSpec.projections.map((projection) => `import type { ${projection.name} } from "../../../core/contexts/${modulePath}/application/projections/${projectionFileBase(projection.name)}.projector.ts";`);
|
|
4935
|
+
const eventImports = contextSpec.projections.flatMap((projection) => projection.sources.map((source) => `import type { ${projectionSourcePayloadTypeName(source)} } from "../../../core/contexts/${normalizeModulePath(source.contextName)}/domain/events/${kebabCase(source.eventName)}.event.ts";`));
|
|
4091
4936
|
const subscriptionEntries = contextSpec.projections.flatMap((projection) => projection.sources.map((source) => {
|
|
4092
4937
|
const subscriptionName = projection.subscription?.subscriptionName ?? projection.name;
|
|
4093
4938
|
const consumerGroup = projection.subscription?.consumerGroup;
|
|
@@ -4097,11 +4942,13 @@ function buildProjectionSubscriptionsArtifact(contextSpec) {
|
|
|
4097
4942
|
subscriptionName: "${subscriptionName}",
|
|
4098
4943
|
eventName: "${source.contextName}.${source.aggregateName}.${source.eventName}",
|
|
4099
4944
|
role: "${source.role}",${consumerGroupLine}
|
|
4100
|
-
handle: (event, tx) => deps.${projectionVariableName(projection.name)}.${projectionSourceHandlerName(source.eventName)}(event, tx),
|
|
4945
|
+
handle: (event: ${projectionSourceEventType(source)}, tx) => deps.${projectionVariableName(projection.name)}.${projectionSourceHandlerName(source.eventName)}(event, tx),
|
|
4101
4946
|
}`;
|
|
4102
4947
|
}));
|
|
4103
|
-
return createGeneratedArtifact(`infrastructure/messaging/projection-subscriptions/${modulePath}.ts`, `import type {
|
|
4948
|
+
return createGeneratedArtifact(`infrastructure/messaging/projection-subscriptions/${modulePath}.ts`, `import type { EventEnvelope } from "../../../lib/event-envelope.ts";
|
|
4949
|
+
import type { Transaction } from "../../../lib/transaction.ts";
|
|
4104
4950
|
${importLines.join("\n")}
|
|
4951
|
+
${[...new Set(eventImports)].join("\n")}
|
|
4105
4952
|
|
|
4106
4953
|
export type ProjectionSubscription = {
|
|
4107
4954
|
projectionName: string;
|
|
@@ -4109,7 +4956,7 @@ export type ProjectionSubscription = {
|
|
|
4109
4956
|
eventName: string;
|
|
4110
4957
|
role: "primary" | "enrichment";
|
|
4111
4958
|
consumerGroup?: string;
|
|
4112
|
-
handle: (event: unknown
|
|
4959
|
+
handle: (event: EventEnvelope<unknown>, tx: Transaction) => Promise<void>;
|
|
4113
4960
|
};
|
|
4114
4961
|
|
|
4115
4962
|
export type ${depsTypeName} = {
|
|
@@ -4385,6 +5232,34 @@ export class EventCollector {
|
|
|
4385
5232
|
}
|
|
4386
5233
|
`;
|
|
4387
5234
|
}
|
|
5235
|
+
function generateAggregateEventTracker() {
|
|
5236
|
+
return `import type { EventEnvelope } from "../../../lib/event-envelope.ts";
|
|
5237
|
+
import { EventCollector } from "./event-collector.ts";
|
|
5238
|
+
|
|
5239
|
+
type EventSourceAggregate = {
|
|
5240
|
+
pullDomainEvents(): EventEnvelope<unknown>[];
|
|
5241
|
+
};
|
|
5242
|
+
|
|
5243
|
+
export class AggregateEventTracker {
|
|
5244
|
+
private readonly tracked = new Set<EventSourceAggregate>();
|
|
5245
|
+
|
|
5246
|
+
track(aggregate: EventSourceAggregate): void {
|
|
5247
|
+
this.tracked.add(aggregate);
|
|
5248
|
+
}
|
|
5249
|
+
|
|
5250
|
+
releaseInto(eventCollector: EventCollector): void {
|
|
5251
|
+
for (const aggregate of this.tracked) {
|
|
5252
|
+
eventCollector.collect(aggregate.pullDomainEvents());
|
|
5253
|
+
}
|
|
5254
|
+
this.tracked.clear();
|
|
5255
|
+
}
|
|
5256
|
+
|
|
5257
|
+
reset(): void {
|
|
5258
|
+
this.tracked.clear();
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
`;
|
|
5262
|
+
}
|
|
4388
5263
|
function generateDomainErrors() {
|
|
4389
5264
|
return `export type { DomainError } from '../../../lib/domain-error.ts';
|
|
4390
5265
|
export type { ApplicationError } from '../../../lib/application-error.ts';
|
|
@@ -4465,6 +5340,7 @@ function buildV5SharedKernelArtifacts(spec) {
|
|
|
4465
5340
|
const base = [
|
|
4466
5341
|
createGeneratedArtifact("core/shared-kernel/result.ts", generateResultReExport(), ownership),
|
|
4467
5342
|
createGeneratedArtifact("core/shared-kernel/events/event-collector.ts", generateEventCollector(), ownership),
|
|
5343
|
+
createGeneratedArtifact("core/shared-kernel/events/aggregate-event-tracker.ts", generateAggregateEventTracker(), ownership),
|
|
4468
5344
|
createGeneratedArtifact("core/shared-kernel/events/event-context.ts", generateEventContext(), ownership),
|
|
4469
5345
|
createGeneratedArtifact("core/shared-kernel/errors/domain-errors.ts", generateDomainErrors(), ownership),
|
|
4470
5346
|
createGeneratedArtifact("core/shared-kernel/trpc-error-mapper.ts", generateTrpcErrorMapper(), ownership),
|
|
@@ -4923,7 +5799,11 @@ function createApplicationError() {
|
|
|
4923
5799
|
}
|
|
4924
5800
|
`;
|
|
4925
5801
|
}
|
|
4926
|
-
function createTransaction() {
|
|
5802
|
+
function createTransaction(infrastructureStrategy) {
|
|
5803
|
+
if (infrastructureStrategy?.persistence === "mysql") return `import type { MySqlTransaction } from "drizzle-orm/mysql-core";
|
|
5804
|
+
|
|
5805
|
+
export type Transaction = MySqlTransaction<any, any, any, any>;
|
|
5806
|
+
`;
|
|
4927
5807
|
return `import type { PgTransaction } from 'drizzle-orm/pg-core';
|
|
4928
5808
|
|
|
4929
5809
|
export type Transaction = PgTransaction<any, any, any>;
|
|
@@ -5017,7 +5897,7 @@ export type ListResult<T> = {
|
|
|
5017
5897
|
};
|
|
5018
5898
|
`;
|
|
5019
5899
|
}
|
|
5020
|
-
function buildV5LibArtifacts() {
|
|
5900
|
+
function buildV5LibArtifacts(infrastructureStrategy) {
|
|
5021
5901
|
const ownership = sharedArtifactOwnership("lib");
|
|
5022
5902
|
return [
|
|
5023
5903
|
createGeneratedArtifact("lib/branded-id.ts", createBrandedId(), ownership),
|
|
@@ -5029,7 +5909,7 @@ function buildV5LibArtifacts() {
|
|
|
5029
5909
|
createGeneratedArtifact("lib/result.ts", createResult(), ownership),
|
|
5030
5910
|
createGeneratedArtifact("lib/domain-error.ts", createDomainError(), ownership),
|
|
5031
5911
|
createGeneratedArtifact("lib/application-error.ts", createApplicationError(), ownership),
|
|
5032
|
-
createGeneratedArtifact("lib/transaction.ts", createTransaction(), ownership),
|
|
5912
|
+
createGeneratedArtifact("lib/transaction.ts", createTransaction(infrastructureStrategy), ownership),
|
|
5033
5913
|
createGeneratedArtifact("lib/concurrency-conflict-error.ts", createConcurrencyConflictError(), ownership),
|
|
5034
5914
|
createGeneratedArtifact("lib/pagination.ts", createPagination(), ownership)
|
|
5035
5915
|
];
|
|
@@ -5139,26 +6019,29 @@ function buildConsumerAclArtifacts(contextSpec) {
|
|
|
5139
6019
|
|
|
5140
6020
|
//#endregion
|
|
5141
6021
|
//#region packages/core/orchestrator.ts
|
|
5142
|
-
function generateContextArtifacts(contextSpec) {
|
|
6022
|
+
function generateContextArtifacts(contextSpec, options = {}) {
|
|
5143
6023
|
const perAggregateArtifacts = sliceContextIntoAggregateViews(contextSpec).flatMap((view) => {
|
|
5144
6024
|
const ownership = sliceArtifactOwnership(`${view.context.modulePath}/${kebabCase(view.aggregate.name)}`);
|
|
5145
6025
|
return [
|
|
5146
6026
|
...applyOwnershipIfMissing(buildV5DomainArtifacts(view), ownership),
|
|
5147
6027
|
...applyOwnershipIfMissing(buildV5ApplicationArtifacts(view, { skipContextWideArtifacts: true }), ownership),
|
|
5148
|
-
...buildV5InfrastructureArtifacts(view, {
|
|
6028
|
+
...buildV5InfrastructureArtifacts(view, {
|
|
6029
|
+
skipContextWideArtifacts: true,
|
|
6030
|
+
infrastructureStrategy: options.infrastructureStrategy
|
|
6031
|
+
}),
|
|
5149
6032
|
...applyOwnershipIfMissing(buildV5TestArtifacts(view), ownership)
|
|
5150
6033
|
];
|
|
5151
6034
|
});
|
|
5152
6035
|
const ctxOwnership = contextArtifactOwnership(contextSpec.context.modulePath);
|
|
5153
6036
|
const infraOwnership = sliceArtifactOwnership(contextSpec.context.modulePath);
|
|
5154
6037
|
const perContextArtifacts = [
|
|
5155
|
-
...buildV5LibArtifacts(),
|
|
6038
|
+
...buildV5LibArtifacts(options.infrastructureStrategy),
|
|
5156
6039
|
...buildV5SharedKernelArtifacts(contextSpec),
|
|
5157
6040
|
...buildV5PortArtifacts(),
|
|
5158
6041
|
...applyOwnershipIfMissing(buildV5ApplicationContextArtifacts(contextSpec), ctxOwnership),
|
|
5159
6042
|
...applyOwnershipIfMissing(buildConsumerAclArtifacts(contextSpec), ctxOwnership),
|
|
5160
6043
|
...applyOwnershipIfMissing(buildProjectionArtifacts(contextSpec), ctxOwnership),
|
|
5161
|
-
...applyOwnershipIfMissing(buildV5InfrastructureContextArtifacts(contextSpec), infraOwnership),
|
|
6044
|
+
...applyOwnershipIfMissing(buildV5InfrastructureContextArtifacts(contextSpec, { infrastructureStrategy: options.infrastructureStrategy }), infraOwnership),
|
|
5162
6045
|
...applyOwnershipIfMissing(buildV5PresentationArtifacts(contextSpec), ctxOwnership),
|
|
5163
6046
|
...applyOwnershipIfMissing(buildV5RouteArtifacts(contextSpec), ctxOwnership)
|
|
5164
6047
|
];
|
|
@@ -5765,6 +6648,7 @@ function createIntegrationEvent() {
|
|
|
5765
6648
|
|
|
5766
6649
|
export interface IntegrationEvent<TPayload> {
|
|
5767
6650
|
readonly type: string;
|
|
6651
|
+
readonly version: number;
|
|
5768
6652
|
readonly payload: TPayload;
|
|
5769
6653
|
readonly eventContext: EventContext;
|
|
5770
6654
|
}
|
|
@@ -5800,6 +6684,20 @@ export class DrizzleTransactionManager implements TransactionManager {
|
|
|
5800
6684
|
}
|
|
5801
6685
|
`;
|
|
5802
6686
|
}
|
|
6687
|
+
function createMikroOrmTransactionManager() {
|
|
6688
|
+
return `import type { TransactionManager } from "./transaction-manager.ts";
|
|
6689
|
+
|
|
6690
|
+
export class MikroOrmTransactionManager implements TransactionManager {
|
|
6691
|
+
constructor(private readonly em: unknown) {}
|
|
6692
|
+
|
|
6693
|
+
async withTransaction<T>(work: (tx: unknown) => Promise<T>): Promise<T> {
|
|
6694
|
+
return (this.em as any).transactional(async (tx: unknown) => {
|
|
6695
|
+
return work(tx);
|
|
6696
|
+
});
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
6699
|
+
`;
|
|
6700
|
+
}
|
|
5803
6701
|
function createTransactionAbortError() {
|
|
5804
6702
|
return `export class TransactionAbortError extends Error {
|
|
5805
6703
|
constructor(public readonly innerError: unknown) {
|
|
@@ -5810,11 +6708,13 @@ function createTransactionAbortError() {
|
|
|
5810
6708
|
`;
|
|
5811
6709
|
}
|
|
5812
6710
|
function createHandlerDeps() {
|
|
5813
|
-
return `import type {
|
|
6711
|
+
return `import type { AggregateEventTracker } from "../../core/shared-kernel/events/aggregate-event-tracker.ts";
|
|
6712
|
+
import type { EventCollector } from "../../core/shared-kernel/events/event-collector.ts";
|
|
5814
6713
|
|
|
5815
6714
|
export type HandlerDeps = {
|
|
5816
6715
|
readonly tx: unknown;
|
|
5817
6716
|
readonly eventCollector: EventCollector;
|
|
6717
|
+
readonly aggregateEventTracker: AggregateEventTracker;
|
|
5818
6718
|
readonly repos: Record<string, unknown>;
|
|
5819
6719
|
readonly acls: Record<string, unknown>;
|
|
5820
6720
|
};
|
|
@@ -5826,6 +6726,7 @@ function buildCompositionTypeArtifacts() {
|
|
|
5826
6726
|
createGeneratedArtifact("infrastructure/messaging/integration-event.ts", createIntegrationEvent(), ownership),
|
|
5827
6727
|
createGeneratedArtifact("infrastructure/unit-of-work/transaction-manager.ts", createTransactionManager(), ownership),
|
|
5828
6728
|
createGeneratedArtifact("infrastructure/unit-of-work/drizzle-transaction-manager.ts", createDrizzleTransactionManager(), ownership),
|
|
6729
|
+
createGeneratedArtifact("infrastructure/unit-of-work/mikroorm-transaction-manager.ts", createMikroOrmTransactionManager(), ownership),
|
|
5829
6730
|
createGeneratedArtifact("infrastructure/unit-of-work/transaction-abort-error.ts", createTransactionAbortError(), ownership),
|
|
5830
6731
|
createGeneratedArtifact("infrastructure/unit-of-work/handler-deps.ts", createHandlerDeps(), ownership)
|
|
5831
6732
|
].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
|
|
@@ -5838,6 +6739,7 @@ function createCommandBusImpl() {
|
|
|
5838
6739
|
import type { TransactionManager } from "../unit-of-work/transaction-manager.ts";
|
|
5839
6740
|
import { TransactionAbortError } from "../unit-of-work/transaction-abort-error.ts";
|
|
5840
6741
|
import type { HandlerDeps } from "../unit-of-work/handler-deps.ts";
|
|
6742
|
+
import { AggregateEventTracker } from "../../core/shared-kernel/events/aggregate-event-tracker.ts";
|
|
5841
6743
|
import { EventCollector } from "../../core/shared-kernel/events/event-collector.ts";
|
|
5842
6744
|
import type { CommandEnvelope } from "../../core/shared-kernel/commands/command-envelope.ts";
|
|
5843
6745
|
import type { EventContext } from "../../core/shared-kernel/events/event-context.ts";
|
|
@@ -5849,6 +6751,9 @@ import type { EventEnvelope } from "../../lib/event-envelope.ts";
|
|
|
5849
6751
|
type OutboxWriter = {
|
|
5850
6752
|
append(tx: unknown, events: EventEnvelope<unknown>[]): Promise<void>;
|
|
5851
6753
|
};
|
|
6754
|
+
type OutboxRelay = {
|
|
6755
|
+
afterCommit(): void;
|
|
6756
|
+
};
|
|
5852
6757
|
type RepositoryFactory = {
|
|
5853
6758
|
create(tx: unknown): Record<string, unknown>;
|
|
5854
6759
|
};
|
|
@@ -5868,6 +6773,8 @@ type InternalHandlerFn = (
|
|
|
5868
6773
|
) => Promise<Result<unknown, unknown>>;
|
|
5869
6774
|
|
|
5870
6775
|
export class CommandBusImpl implements CommandBusPort {
|
|
6776
|
+
private relay: OutboxRelay | undefined;
|
|
6777
|
+
|
|
5871
6778
|
constructor(
|
|
5872
6779
|
private readonly txManager: TransactionManager,
|
|
5873
6780
|
private readonly repoFactory: RepositoryFactory,
|
|
@@ -5875,6 +6782,10 @@ export class CommandBusImpl implements CommandBusPort {
|
|
|
5875
6782
|
private readonly outboxWriter: OutboxWriter,
|
|
5876
6783
|
) {}
|
|
5877
6784
|
|
|
6785
|
+
setRelay(relay: OutboxRelay): void {
|
|
6786
|
+
this.relay = relay;
|
|
6787
|
+
}
|
|
6788
|
+
|
|
5878
6789
|
// Internal handler type: receives deps injected by withUnitOfWork.
|
|
5879
6790
|
// This is different from TestCommandBus which registers pre-bound
|
|
5880
6791
|
// handlers with signature (command, eventContext) — deps closed over
|
|
@@ -5902,9 +6813,11 @@ export class CommandBusImpl implements CommandBusPort {
|
|
|
5902
6813
|
): Promise<TResult> {
|
|
5903
6814
|
const { handler, contextKey: commandContext } = this.resolveHandler(command.type);
|
|
5904
6815
|
try {
|
|
5905
|
-
|
|
6816
|
+
const result = await this.withUnitOfWork(commandContext, (deps) =>
|
|
5906
6817
|
handler(command, deps, eventContext)
|
|
5907
6818
|
) as TResult;
|
|
6819
|
+
this.relay?.afterCommit();
|
|
6820
|
+
return result;
|
|
5908
6821
|
} catch (error) {
|
|
5909
6822
|
if (error instanceof TransactionAbortError) {
|
|
5910
6823
|
// Reconstitute the failure Result that triggered rollback.
|
|
@@ -5935,16 +6848,24 @@ export class CommandBusImpl implements CommandBusPort {
|
|
|
5935
6848
|
work: (deps: HandlerDeps) => Promise<Result<T, unknown>>
|
|
5936
6849
|
): Promise<Result<T, unknown>> {
|
|
5937
6850
|
return this.txManager.withTransaction(async (tx) => {
|
|
6851
|
+
const aggregateEventTracker = new AggregateEventTracker();
|
|
5938
6852
|
const eventCollector = new EventCollector();
|
|
5939
6853
|
const repos = this.repoFactory.create(tx);
|
|
5940
6854
|
const acls = this.aclFactory.create(tx, commandContext);
|
|
5941
6855
|
|
|
5942
|
-
const result = await work({
|
|
6856
|
+
const result = await work({
|
|
6857
|
+
tx,
|
|
6858
|
+
eventCollector,
|
|
6859
|
+
aggregateEventTracker,
|
|
6860
|
+
repos,
|
|
6861
|
+
acls,
|
|
6862
|
+
});
|
|
5943
6863
|
|
|
5944
6864
|
if (!result.ok) {
|
|
5945
6865
|
throw new TransactionAbortError(result.error);
|
|
5946
6866
|
}
|
|
5947
6867
|
|
|
6868
|
+
aggregateEventTracker.releaseInto(eventCollector);
|
|
5948
6869
|
const events = eventCollector.drain();
|
|
5949
6870
|
|
|
5950
6871
|
if (events.length > 0) {
|
|
@@ -5989,16 +6910,38 @@ function buildCompositionBusArtifacts() {
|
|
|
5989
6910
|
|
|
5990
6911
|
//#endregion
|
|
5991
6912
|
//#region packages/core/generators/composition_outbox.ts
|
|
5992
|
-
function createOutboxTable() {
|
|
5993
|
-
return `import {
|
|
6913
|
+
function createOutboxTable(infrastructureStrategy) {
|
|
6914
|
+
if (infrastructureStrategy?.persistence === "mysql") return `import { sql } from "drizzle-orm";
|
|
6915
|
+
import { int, json, mysqlTable, timestamp, varchar } from "drizzle-orm/mysql-core";
|
|
6916
|
+
|
|
6917
|
+
export const outboxEvents = mysqlTable("outbox_events", {
|
|
6918
|
+
id: varchar("id", { length: 36 }).primaryKey().default(sql\`(uuid())\`),
|
|
6919
|
+
eventType: varchar("event_type", { length: 255 }).notNull(),
|
|
6920
|
+
eventVersion: int("event_version").notNull(),
|
|
6921
|
+
payload: json("payload").notNull(),
|
|
6922
|
+
metadata: json("metadata").notNull(),
|
|
6923
|
+
occurredAt: timestamp("occurred_at").notNull().defaultNow(),
|
|
6924
|
+
status: varchar("status", { length: 50 }).notNull().default("pending"),
|
|
6925
|
+
attempts: int("attempts").notNull().default(0),
|
|
6926
|
+
processedAt: timestamp("processed_at"),
|
|
6927
|
+
failedAt: timestamp("failed_at"),
|
|
6928
|
+
error: json("error"),
|
|
6929
|
+
});
|
|
6930
|
+
`;
|
|
6931
|
+
return `import { pgTable, uuid, varchar, jsonb, timestamp, integer } from "drizzle-orm/pg-core";
|
|
5994
6932
|
|
|
5995
6933
|
export const outboxEvents = pgTable("outbox_events", {
|
|
5996
6934
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
5997
6935
|
eventType: varchar("event_type", { length: 255 }).notNull(),
|
|
6936
|
+
eventVersion: integer("event_version").notNull(),
|
|
5998
6937
|
payload: jsonb("payload").notNull(),
|
|
5999
6938
|
metadata: jsonb("metadata").notNull(),
|
|
6000
6939
|
occurredAt: timestamp("occurred_at").notNull().defaultNow(),
|
|
6001
6940
|
status: varchar("status", { length: 50 }).notNull().default("pending"),
|
|
6941
|
+
attempts: integer("attempts").notNull().default(0),
|
|
6942
|
+
processedAt: timestamp("processed_at"),
|
|
6943
|
+
failedAt: timestamp("failed_at"),
|
|
6944
|
+
error: jsonb("error"),
|
|
6002
6945
|
});
|
|
6003
6946
|
`;
|
|
6004
6947
|
}
|
|
@@ -6014,6 +6957,7 @@ export class OutboxWriter {
|
|
|
6014
6957
|
for (const event of events) {
|
|
6015
6958
|
await db.insert(outboxEvents).values({
|
|
6016
6959
|
eventType: event.eventType,
|
|
6960
|
+
eventVersion: event.eventVersion,
|
|
6017
6961
|
payload: event.payload,
|
|
6018
6962
|
metadata: {
|
|
6019
6963
|
correlationId: event.correlationId,
|
|
@@ -6028,9 +6972,221 @@ export class OutboxWriter {
|
|
|
6028
6972
|
}
|
|
6029
6973
|
`;
|
|
6030
6974
|
}
|
|
6031
|
-
function
|
|
6975
|
+
function createOutboxDispatcher() {
|
|
6976
|
+
return `import { asc, eq } from "drizzle-orm";
|
|
6977
|
+
import { outboxEvents } from "./outbox.table.ts";
|
|
6978
|
+
import type {
|
|
6979
|
+
EventSubscription,
|
|
6980
|
+
IntegrationEvent,
|
|
6981
|
+
} from "../messaging/integration-event.ts";
|
|
6982
|
+
|
|
6983
|
+
export type OutboxDispatcherConfig = {
|
|
6984
|
+
readonly maxAttempts?: number;
|
|
6985
|
+
readonly batchSize?: number;
|
|
6986
|
+
};
|
|
6987
|
+
|
|
6988
|
+
type OutboxRow = typeof outboxEvents.$inferSelect;
|
|
6989
|
+
|
|
6990
|
+
type OutboxMetadata = {
|
|
6991
|
+
correlationId: string;
|
|
6992
|
+
causationId: string;
|
|
6993
|
+
recordedBy: string;
|
|
6994
|
+
occurredAt: string;
|
|
6995
|
+
};
|
|
6996
|
+
|
|
6997
|
+
type HandlerError = {
|
|
6998
|
+
handler: string;
|
|
6999
|
+
error: string;
|
|
7000
|
+
};
|
|
7001
|
+
|
|
7002
|
+
export class OutboxDispatcher {
|
|
7003
|
+
private readonly maxAttempts: number;
|
|
7004
|
+
private readonly batchSize: number;
|
|
7005
|
+
private readonly handlersByEventType: Map<string, EventSubscription[]>;
|
|
7006
|
+
|
|
7007
|
+
constructor(
|
|
7008
|
+
private readonly db: unknown,
|
|
7009
|
+
subscriptions: EventSubscription[],
|
|
7010
|
+
config?: OutboxDispatcherConfig,
|
|
7011
|
+
) {
|
|
7012
|
+
this.maxAttempts = config?.maxAttempts ?? 5;
|
|
7013
|
+
this.batchSize = config?.batchSize ?? 100;
|
|
7014
|
+
|
|
7015
|
+
const index = new Map<string, EventSubscription[]>();
|
|
7016
|
+
for (const subscription of subscriptions) {
|
|
7017
|
+
const existing = index.get(subscription.eventType) ?? [];
|
|
7018
|
+
existing.push(subscription);
|
|
7019
|
+
index.set(subscription.eventType, existing);
|
|
7020
|
+
}
|
|
7021
|
+
this.handlersByEventType = index;
|
|
7022
|
+
}
|
|
7023
|
+
|
|
7024
|
+
async dispatchPending(): Promise<{ processed: number; failed: number }> {
|
|
7025
|
+
const drizzle = this.db as any;
|
|
7026
|
+
let processed = 0;
|
|
7027
|
+
let failed = 0;
|
|
7028
|
+
|
|
7029
|
+
const rows: OutboxRow[] = await drizzle
|
|
7030
|
+
.select()
|
|
7031
|
+
.from(outboxEvents)
|
|
7032
|
+
.where(eq(outboxEvents.status, "pending"))
|
|
7033
|
+
.orderBy(asc(outboxEvents.occurredAt))
|
|
7034
|
+
.limit(this.batchSize);
|
|
7035
|
+
|
|
7036
|
+
for (const row of rows) {
|
|
7037
|
+
const ok = await this.dispatchRow(row);
|
|
7038
|
+
if (ok) {
|
|
7039
|
+
processed += 1;
|
|
7040
|
+
} else {
|
|
7041
|
+
failed += 1;
|
|
7042
|
+
}
|
|
7043
|
+
}
|
|
7044
|
+
|
|
7045
|
+
return { processed, failed };
|
|
7046
|
+
}
|
|
7047
|
+
|
|
7048
|
+
private async dispatchRow(row: OutboxRow): Promise<boolean> {
|
|
7049
|
+
const handlers = this.handlersByEventType.get(row.eventType);
|
|
7050
|
+
if (!handlers || handlers.length === 0) {
|
|
7051
|
+
await this.updateStatus(row.id, "processed", { processedAt: new Date() });
|
|
7052
|
+
return true;
|
|
7053
|
+
}
|
|
7054
|
+
|
|
7055
|
+
const metadata = (row.metadata ?? {}) as Partial<OutboxMetadata>;
|
|
7056
|
+
const event: IntegrationEvent<unknown> = {
|
|
7057
|
+
type: row.eventType,
|
|
7058
|
+
version: row.eventVersion,
|
|
7059
|
+
payload: row.payload,
|
|
7060
|
+
eventContext: {
|
|
7061
|
+
correlationId: metadata.correlationId as any,
|
|
7062
|
+
causationId: metadata.causationId as any,
|
|
7063
|
+
recordedBy: metadata.recordedBy as any,
|
|
7064
|
+
},
|
|
7065
|
+
};
|
|
7066
|
+
|
|
7067
|
+
const errors: HandlerError[] = [];
|
|
7068
|
+
for (const subscription of handlers) {
|
|
7069
|
+
try {
|
|
7070
|
+
await subscription.handler(event);
|
|
7071
|
+
} catch (error) {
|
|
7072
|
+
errors.push({
|
|
7073
|
+
handler: subscription.handler.name || subscription.eventType,
|
|
7074
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7075
|
+
});
|
|
7076
|
+
}
|
|
7077
|
+
}
|
|
7078
|
+
|
|
7079
|
+
if (errors.length === 0) {
|
|
7080
|
+
await this.updateStatus(row.id, "processed", {
|
|
7081
|
+
processedAt: new Date(),
|
|
7082
|
+
error: null,
|
|
7083
|
+
});
|
|
7084
|
+
return true;
|
|
7085
|
+
}
|
|
7086
|
+
|
|
7087
|
+
const nextAttempts = (row.attempts ?? 0) + 1;
|
|
7088
|
+
if (nextAttempts >= this.maxAttempts) {
|
|
7089
|
+
await this.updateStatus(row.id, "failed", {
|
|
7090
|
+
attempts: nextAttempts,
|
|
7091
|
+
failedAt: new Date(),
|
|
7092
|
+
error: errors,
|
|
7093
|
+
});
|
|
7094
|
+
} else {
|
|
7095
|
+
await this.updateStatus(row.id, "pending", {
|
|
7096
|
+
attempts: nextAttempts,
|
|
7097
|
+
error: errors,
|
|
7098
|
+
});
|
|
7099
|
+
}
|
|
7100
|
+
|
|
7101
|
+
return false;
|
|
7102
|
+
}
|
|
7103
|
+
|
|
7104
|
+
private async updateStatus(
|
|
7105
|
+
id: string,
|
|
7106
|
+
status: "pending" | "processed" | "failed",
|
|
7107
|
+
fields: Record<string, unknown>,
|
|
7108
|
+
): Promise<void> {
|
|
7109
|
+
const drizzle = this.db as any;
|
|
7110
|
+
await drizzle
|
|
7111
|
+
.update(outboxEvents)
|
|
7112
|
+
.set({ status, ...fields })
|
|
7113
|
+
.where(eq(outboxEvents.id, id));
|
|
7114
|
+
}
|
|
7115
|
+
}
|
|
7116
|
+
`;
|
|
7117
|
+
}
|
|
7118
|
+
function createOutboxRelay() {
|
|
7119
|
+
return `import type { OutboxDispatcher } from "./outbox-dispatcher.ts";
|
|
7120
|
+
|
|
7121
|
+
export class OutboxRelay {
|
|
7122
|
+
constructor(private readonly dispatcher: OutboxDispatcher) {}
|
|
7123
|
+
|
|
7124
|
+
afterCommit(): void {
|
|
7125
|
+
void this.dispatcher.dispatchPending().catch(() => {
|
|
7126
|
+
// Swallowed - the poller retries on the next cycle.
|
|
7127
|
+
});
|
|
7128
|
+
}
|
|
7129
|
+
}
|
|
7130
|
+
`;
|
|
7131
|
+
}
|
|
7132
|
+
function createOutboxPoller() {
|
|
7133
|
+
return `import type { OutboxDispatcher } from "./outbox-dispatcher.ts";
|
|
7134
|
+
|
|
7135
|
+
export type OutboxPollerConfig = {
|
|
7136
|
+
readonly intervalMs?: number;
|
|
7137
|
+
};
|
|
7138
|
+
|
|
7139
|
+
export class OutboxPoller {
|
|
7140
|
+
private handle: ReturnType<typeof setInterval> | null = null;
|
|
7141
|
+
private polling = false;
|
|
7142
|
+
private readonly intervalMs: number;
|
|
7143
|
+
|
|
7144
|
+
constructor(
|
|
7145
|
+
private readonly dispatcher: OutboxDispatcher,
|
|
7146
|
+
config?: OutboxPollerConfig,
|
|
7147
|
+
) {
|
|
7148
|
+
this.intervalMs = config?.intervalMs ?? 5_000;
|
|
7149
|
+
}
|
|
7150
|
+
|
|
7151
|
+
start(): void {
|
|
7152
|
+
if (this.handle) return;
|
|
7153
|
+
void this.poll();
|
|
7154
|
+
this.handle = setInterval(() => void this.poll(), this.intervalMs);
|
|
7155
|
+
}
|
|
7156
|
+
|
|
7157
|
+
stop(): void {
|
|
7158
|
+
if (!this.handle) return;
|
|
7159
|
+
clearInterval(this.handle);
|
|
7160
|
+
this.handle = null;
|
|
7161
|
+
}
|
|
7162
|
+
|
|
7163
|
+
get running(): boolean {
|
|
7164
|
+
return this.handle !== null;
|
|
7165
|
+
}
|
|
7166
|
+
|
|
7167
|
+
private async poll(): Promise<void> {
|
|
7168
|
+
if (this.polling) return;
|
|
7169
|
+
this.polling = true;
|
|
7170
|
+
try {
|
|
7171
|
+
await this.dispatcher.dispatchPending();
|
|
7172
|
+
} catch {
|
|
7173
|
+
// Swallowed - the next cycle retries.
|
|
7174
|
+
} finally {
|
|
7175
|
+
this.polling = false;
|
|
7176
|
+
}
|
|
7177
|
+
}
|
|
7178
|
+
}
|
|
7179
|
+
`;
|
|
7180
|
+
}
|
|
7181
|
+
function buildCompositionOutboxArtifacts(infrastructureStrategy) {
|
|
6032
7182
|
const ownership = sharedArtifactOwnership("composition");
|
|
6033
|
-
return [
|
|
7183
|
+
return [
|
|
7184
|
+
createGeneratedArtifact("infrastructure/outbox/outbox-dispatcher.ts", createOutboxDispatcher(), ownership),
|
|
7185
|
+
createGeneratedArtifact("infrastructure/outbox/outbox-poller.ts", createOutboxPoller(), ownership),
|
|
7186
|
+
createGeneratedArtifact("infrastructure/outbox/outbox-relay.ts", createOutboxRelay(), ownership),
|
|
7187
|
+
createGeneratedArtifact("infrastructure/outbox/outbox.table.ts", createOutboxTable(infrastructureStrategy), ownership),
|
|
7188
|
+
createGeneratedArtifact("infrastructure/outbox/outbox-writer.ts", createOutboxWriter(), ownership)
|
|
7189
|
+
].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
|
|
6034
7190
|
}
|
|
6035
7191
|
|
|
6036
7192
|
//#endregion
|
|
@@ -6118,38 +7274,53 @@ function renderMappedType(fields) {
|
|
|
6118
7274
|
${Object.entries(fields).map(([fieldName, expr]) => ` readonly ${camelCase(fieldName)}: ${mappingValueType(expr)};`).join("\n")}
|
|
6119
7275
|
}`;
|
|
6120
7276
|
}
|
|
6121
|
-
function
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
7277
|
+
function mapReadAclFieldTypeToTS(type) {
|
|
7278
|
+
switch (type) {
|
|
7279
|
+
case "string": return "string";
|
|
7280
|
+
case "number": return "number";
|
|
7281
|
+
case "boolean": return "boolean";
|
|
7282
|
+
case "date": return "string";
|
|
7283
|
+
case "enum": return "string";
|
|
7284
|
+
case "union": return "string | number | boolean | null";
|
|
7285
|
+
case "record": return "Record<string, unknown>";
|
|
7286
|
+
case "tuple": return "[unknown, ...unknown[]]";
|
|
7287
|
+
case "intersection": return "Record<string, unknown>";
|
|
7288
|
+
default: return "unknown";
|
|
7289
|
+
}
|
|
7290
|
+
}
|
|
7291
|
+
function formatReadAclFieldType(field, indent = 0) {
|
|
7292
|
+
if (field.type === "object" && field.nestedFields && field.nestedFields.length > 0) {
|
|
7293
|
+
const objectType = `{\n${field.nestedFields.map((nestedField) => {
|
|
7294
|
+
const opt = nestedField.optional ? "?" : "";
|
|
7295
|
+
return `${" ".repeat(indent + 1)}readonly ${camelCase(nestedField.name)}${opt}: ${formatReadAclFieldType(nestedField, indent + 1)};`;
|
|
7296
|
+
}).join("\n")}\n${" ".repeat(indent)}}`;
|
|
7297
|
+
return field.array ? `Array<${objectType}>` : objectType;
|
|
7298
|
+
}
|
|
7299
|
+
const base = mapReadAclFieldTypeToTS(field.type);
|
|
7300
|
+
return field.array ? `${base}[]` : base;
|
|
6126
7301
|
}
|
|
6127
|
-
function
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
7302
|
+
function formatReadAclListItemType(outputFields) {
|
|
7303
|
+
return `Array<{\n${outputFields.map((field) => {
|
|
7304
|
+
const opt = field.optional ? "?" : "";
|
|
7305
|
+
return ` readonly ${camelCase(field.name)}${opt}: ${formatReadAclFieldType(field, 1)};`;
|
|
7306
|
+
}).join("\n")}\n}>`;
|
|
6131
7307
|
}
|
|
6132
7308
|
function readResponseFieldType(acl, mapping) {
|
|
6133
7309
|
if (mapping.kind === "const") return mappingValueType({ const: mapping.value });
|
|
6134
|
-
const viewTypeName = sourceQueryViewTypeName(acl);
|
|
6135
7310
|
if (acl.sourceQuery.queryKind === "list") {
|
|
6136
|
-
if (mapping.sourcePath === "result.items") return
|
|
7311
|
+
if (mapping.sourcePath === "result.items") return formatReadAclListItemType(acl.sourceQuery.outputFields);
|
|
6137
7312
|
if (mapping.sourcePath === "result.total" || mapping.sourcePath === "result.page" || mapping.sourcePath === "result.pageSize") return "number";
|
|
6138
7313
|
}
|
|
6139
7314
|
const sourceFieldName = mapping.sourcePath.replace(/^result\./, "");
|
|
6140
7315
|
const sourceField = acl.sourceQuery.outputFields.find((field) => camelCase(field.name) === camelCase(sourceFieldName));
|
|
6141
|
-
return sourceField ?
|
|
7316
|
+
return sourceField ? formatReadAclFieldType(sourceField) : "unknown";
|
|
6142
7317
|
}
|
|
6143
7318
|
function buildReadAclContractContent(acl) {
|
|
6144
7319
|
normalizeModulePath(acl.consumerContext.modulePath);
|
|
6145
|
-
const sourceModulePath = normalizeModulePath(acl.sourceContext.modulePath);
|
|
6146
7320
|
const requestTypeName = aclRequestTypeName(acl.port);
|
|
6147
7321
|
const responseTypeName = aclResponseTypeName(acl.port);
|
|
6148
|
-
const viewTypeName = sourceQueryViewTypeName(acl);
|
|
6149
|
-
const viewFileName = sourceQueryViewFileName(acl);
|
|
6150
7322
|
const responseLines = acl.responseMappings.map((mapping) => ` readonly ${camelCase(mapping.targetPath)}: ${readResponseFieldType(acl, mapping)};`);
|
|
6151
7323
|
return `import type { Transaction } from "../../../../../lib/transaction.ts";
|
|
6152
|
-
import type { ${viewTypeName} } from "../../../${sourceModulePath}/application/contracts/${viewFileName}";
|
|
6153
7324
|
|
|
6154
7325
|
export type ${requestTypeName} = ${renderMappedType(Object.fromEntries(acl.requestMappings.map((mapping) => [mapping.targetPath, mapping.kind === "from" ? { from: mapping.sourcePath } : { const: mapping.value }])))};
|
|
6155
7326
|
|
|
@@ -6438,6 +7609,24 @@ function buildCompositionAclArtifacts(compositionSpec) {
|
|
|
6438
7609
|
].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
|
|
6439
7610
|
}
|
|
6440
7611
|
|
|
7612
|
+
//#endregion
|
|
7613
|
+
//#region packages/core/generators/composition_dependencies.ts
|
|
7614
|
+
function resolveRuntimeDependencies(compositionSpec) {
|
|
7615
|
+
const dependencies = new Set(["drizzle-orm"]);
|
|
7616
|
+
if (compositionSpec.infrastructure.persistence === "postgres") dependencies.add("postgres");
|
|
7617
|
+
else if (compositionSpec.infrastructure.persistence === "mysql") dependencies.add("mysql2");
|
|
7618
|
+
if (compositionSpec.infrastructure.orm === "mikroorm") dependencies.add("@mikro-orm/core");
|
|
7619
|
+
return [...dependencies].sort((left, right) => left.localeCompare(right));
|
|
7620
|
+
}
|
|
7621
|
+
function buildCompositionDependencyManifestArtifacts(compositionSpec) {
|
|
7622
|
+
const manifest = {
|
|
7623
|
+
kind: "zodmire-dependency-manifest",
|
|
7624
|
+
infrastructure: compositionSpec.infrastructure,
|
|
7625
|
+
dependencies: resolveRuntimeDependencies(compositionSpec)
|
|
7626
|
+
};
|
|
7627
|
+
return [createGeneratedArtifact("codegen/dependency-manifest.json", `${JSON.stringify(manifest, null, 2)}\n`, sharedArtifactOwnership("composition"))];
|
|
7628
|
+
}
|
|
7629
|
+
|
|
6441
7630
|
//#endregion
|
|
6442
7631
|
//#region packages/core/generators/composition_container.ts
|
|
6443
7632
|
/**
|
|
@@ -6449,6 +7638,8 @@ function buildCompositionContainerArtifacts(compositionSpec, contextSpecs) {
|
|
|
6449
7638
|
return [createGeneratedArtifact("infrastructure/di/container.ts", buildContainerContent(compositionSpec, contextSpecs), ownership), createGeneratedArtifact("infrastructure/di/repo-factory.ts", buildRepoFactoryContent(contextSpecs), ownership)].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
|
|
6450
7639
|
}
|
|
6451
7640
|
function buildContainerContent(compositionSpec, contextSpecs) {
|
|
7641
|
+
assertSupportedContainerStrategy(compositionSpec);
|
|
7642
|
+
const runtime = resolveContainerRuntime(compositionSpec);
|
|
6452
7643
|
const readContexts = uniqueReadContexts(compositionSpec.acls);
|
|
6453
7644
|
const readContextNames = buildCollisionSafeModulePathNames(readContexts.map((context) => context.modulePath));
|
|
6454
7645
|
const commandContextNames = buildCollisionSafeModulePathNames(contextSpecs.map((context) => context.context.modulePath));
|
|
@@ -6458,7 +7649,10 @@ function buildContainerContent(compositionSpec, contextSpecs) {
|
|
|
6458
7649
|
``,
|
|
6459
7650
|
`import { CommandBusImpl } from "../messaging/command-bus.impl.ts";`,
|
|
6460
7651
|
`import { QueryBusImpl } from "../messaging/query-bus.impl.ts";`,
|
|
6461
|
-
`import {
|
|
7652
|
+
`import { ${runtime.transactionManagerClassName} } from "../unit-of-work/${runtime.transactionManagerFileBase}.ts";`,
|
|
7653
|
+
`import { OutboxDispatcher } from "../outbox/outbox-dispatcher.ts";`,
|
|
7654
|
+
`import { OutboxPoller } from "../outbox/outbox-poller.ts";`,
|
|
7655
|
+
`import { OutboxRelay } from "../outbox/outbox-relay.ts";`,
|
|
6462
7656
|
`import { OutboxWriter } from "../outbox/outbox-writer.ts";`,
|
|
6463
7657
|
`import { AclFactory } from "./acl-factory.ts";`,
|
|
6464
7658
|
`import { RepositoryFactory } from "./repo-factory.ts";`,
|
|
@@ -6477,7 +7671,8 @@ function buildContainerContent(compositionSpec, contextSpecs) {
|
|
|
6477
7671
|
for (const query of uniqueQueriesByReadModel(contextSpec.queries)) {
|
|
6478
7672
|
const repoClassName = queryReadModelRepositoryClassName(query);
|
|
6479
7673
|
const repoAlias = queryReadModelRepositoryAlias(boundaryPlan.symbolStem, query);
|
|
6480
|
-
|
|
7674
|
+
const repoFileBase = readModelRepositoryFileBase(resolvedReadModelName(query));
|
|
7675
|
+
importLines.push(repoAlias === repoClassName ? `import { ${repoClassName} } from "../view-models/drizzle/repositories/${modulePath}/${repoFileBase}.repository.ts";` : `import { ${repoClassName} as ${repoAlias} } from "../view-models/drizzle/repositories/${modulePath}/${repoFileBase}.repository.ts";`);
|
|
6481
7676
|
}
|
|
6482
7677
|
}
|
|
6483
7678
|
for (const spec of contextSpecs) {
|
|
@@ -6501,15 +7696,16 @@ function buildContainerContent(compositionSpec, contextSpecs) {
|
|
|
6501
7696
|
` readonly commandBus: CommandBusPort;`,
|
|
6502
7697
|
` readonly queryBus: QueryBusPort;`,
|
|
6503
7698
|
` readonly integrationEventSubscriptions: EventSubscription[];`,
|
|
6504
|
-
` readonly
|
|
7699
|
+
` readonly outboxPoller: OutboxPoller;`,
|
|
7700
|
+
...runtime.containerFields.map((field) => ` readonly ${field}: unknown;`),
|
|
6505
7701
|
`};`
|
|
6506
7702
|
];
|
|
6507
7703
|
const createContainerLines = [
|
|
6508
7704
|
``,
|
|
6509
|
-
`export function createContainer(deps: {
|
|
7705
|
+
`export function createContainer(deps: { ${runtime.dependencyFields.map((field) => `${field}: unknown`).join("; ")} }): Container {`,
|
|
6510
7706
|
` const outboxWriter = new OutboxWriter();`,
|
|
6511
7707
|
` const repoFactory = new RepositoryFactory();`,
|
|
6512
|
-
` const txManager = new
|
|
7708
|
+
` const txManager = new ${runtime.transactionManagerClassName}(${runtime.transactionDependencyRef});`
|
|
6513
7709
|
];
|
|
6514
7710
|
for (const context of readContexts) {
|
|
6515
7711
|
const contextSpec = contextSpecs.find((spec) => spec.context.modulePath.toLowerCase() === context.modulePath.toLowerCase());
|
|
@@ -6517,7 +7713,7 @@ function buildContainerContent(compositionSpec, contextSpecs) {
|
|
|
6517
7713
|
const boundaryPlan = readContextNames.get(context.modulePath);
|
|
6518
7714
|
const boundaryVar = `${boundaryPlan.propertyStem}PublishedReadBoundary`;
|
|
6519
7715
|
const boundaryAlias = `create${boundaryPlan.symbolStem}PublishedReadBoundary`;
|
|
6520
|
-
const boundaryDeps = buildPublishedReadBoundaryDeps(contextSpec, boundaryPlan.symbolStem);
|
|
7716
|
+
const boundaryDeps = buildPublishedReadBoundaryDeps(contextSpec, boundaryPlan.symbolStem, runtime.readRepositoryDependencyRef);
|
|
6521
7717
|
createContainerLines.push(` const ${boundaryVar} = ${boundaryAlias}(${boundaryDeps});`);
|
|
6522
7718
|
}
|
|
6523
7719
|
createContainerLines.push(` let commandBus: CommandBusImpl | undefined;`, ` const commandBusPort: CommandBusPort = {`, ` execute: (command, eventContext) => {`, ` if (!commandBus) {`, ` throw new Error("Command bus is not initialized");`, ` }`, ` return commandBus.execute(command, eventContext);`, ` },`, ` };`);
|
|
@@ -6548,7 +7744,9 @@ function buildContainerContent(compositionSpec, contextSpecs) {
|
|
|
6548
7744
|
createContainerLines.push(` const integrationEventSubscriptions: EventSubscription[] = [];`);
|
|
6549
7745
|
}
|
|
6550
7746
|
createContainerLines.push(``);
|
|
6551
|
-
createContainerLines.push(`
|
|
7747
|
+
createContainerLines.push(` const outboxDispatcher = new OutboxDispatcher(${runtime.transactionDependencyRef}, integrationEventSubscriptions);`, ` const outboxRelay = new OutboxRelay(outboxDispatcher);`, ` commandBus.setRelay(outboxRelay);`, ` const outboxPoller = new OutboxPoller(outboxDispatcher);`);
|
|
7748
|
+
createContainerLines.push(``);
|
|
7749
|
+
createContainerLines.push(` return { commandBus: commandBus!, queryBus, integrationEventSubscriptions, outboxPoller, ${runtime.containerFields.map((field) => `${field}: deps.${field}`).join(", ")} };`);
|
|
6552
7750
|
createContainerLines.push(`}`);
|
|
6553
7751
|
createContainerLines.push(``);
|
|
6554
7752
|
return [
|
|
@@ -6557,11 +7755,50 @@ function buildContainerContent(compositionSpec, contextSpecs) {
|
|
|
6557
7755
|
...createContainerLines
|
|
6558
7756
|
].join("\n");
|
|
6559
7757
|
}
|
|
6560
|
-
function buildPublishedReadBoundaryDeps(spec, contextSymbolStem) {
|
|
7758
|
+
function buildPublishedReadBoundaryDeps(spec, contextSymbolStem, readRepositoryDependencyRef) {
|
|
6561
7759
|
const entries = /* @__PURE__ */ new Map();
|
|
6562
|
-
for (const query of uniqueQueriesByReadModel(spec.queries)) entries.set(queryReadModelVariableName(query), `${queryReadModelVariableName(query)}: new ${queryReadModelRepositoryAlias(contextSymbolStem, query)}(
|
|
7760
|
+
for (const query of uniqueQueriesByReadModel(spec.queries)) entries.set(queryReadModelVariableName(query), `${queryReadModelVariableName(query)}: new ${queryReadModelRepositoryAlias(contextSymbolStem, query)}(${readRepositoryDependencyRef})`);
|
|
6563
7761
|
return `{ ${[...entries.values()].join(", ")} }`;
|
|
6564
7762
|
}
|
|
7763
|
+
function assertSupportedContainerStrategy(compositionSpec) {
|
|
7764
|
+
const { architecture, persistence, orm } = compositionSpec.infrastructure;
|
|
7765
|
+
if (!(architecture === "physical-cqrs" && persistence === "postgres" && (orm === "drizzle" || orm === "mikroorm") || architecture === "logical-cqrs" && (persistence === "postgres" && (orm === "drizzle" || orm === "mikroorm") || persistence === "mysql" && (orm === "drizzle" || orm === "mikroorm")) || architecture === "physical-cqrs" && persistence === "mysql" && (orm === "drizzle" || orm === "mikroorm"))) throw new Error(`[composition_container] Unsupported infrastructure strategy: ${architecture} + ${persistence} + ${orm}`);
|
|
7766
|
+
}
|
|
7767
|
+
function resolveContainerRuntime(compositionSpec) {
|
|
7768
|
+
const { architecture, orm } = compositionSpec.infrastructure;
|
|
7769
|
+
if (architecture === "physical-cqrs" && orm === "drizzle") return {
|
|
7770
|
+
transactionManagerClassName: "DrizzleTransactionManager",
|
|
7771
|
+
transactionManagerFileBase: "drizzle-transaction-manager",
|
|
7772
|
+
transactionDependencyRef: "deps.writeDb",
|
|
7773
|
+
readRepositoryDependencyRef: "deps.readDb",
|
|
7774
|
+
dependencyFields: ["writeDb", "readDb"],
|
|
7775
|
+
containerFields: ["writeDb", "readDb"]
|
|
7776
|
+
};
|
|
7777
|
+
if (architecture === "logical-cqrs" && orm === "drizzle") return {
|
|
7778
|
+
transactionManagerClassName: "DrizzleTransactionManager",
|
|
7779
|
+
transactionManagerFileBase: "drizzle-transaction-manager",
|
|
7780
|
+
transactionDependencyRef: "deps.db",
|
|
7781
|
+
readRepositoryDependencyRef: "deps.db",
|
|
7782
|
+
dependencyFields: ["db"],
|
|
7783
|
+
containerFields: ["db"]
|
|
7784
|
+
};
|
|
7785
|
+
if (architecture === "physical-cqrs" && orm === "mikroorm") return {
|
|
7786
|
+
transactionManagerClassName: "MikroOrmTransactionManager",
|
|
7787
|
+
transactionManagerFileBase: "mikroorm-transaction-manager",
|
|
7788
|
+
transactionDependencyRef: "deps.writeEm",
|
|
7789
|
+
readRepositoryDependencyRef: "deps.readDb",
|
|
7790
|
+
dependencyFields: ["writeEm", "readDb"],
|
|
7791
|
+
containerFields: ["writeEm", "readDb"]
|
|
7792
|
+
};
|
|
7793
|
+
return {
|
|
7794
|
+
transactionManagerClassName: "MikroOrmTransactionManager",
|
|
7795
|
+
transactionManagerFileBase: "mikroorm-transaction-manager",
|
|
7796
|
+
transactionDependencyRef: "deps.em",
|
|
7797
|
+
readRepositoryDependencyRef: "deps.db",
|
|
7798
|
+
dependencyFields: ["em", "db"],
|
|
7799
|
+
containerFields: ["em", "db"]
|
|
7800
|
+
};
|
|
7801
|
+
}
|
|
6565
7802
|
function resolvedReadModelName(query) {
|
|
6566
7803
|
return query.readSide?.readModelName ?? query.readModelName ?? query.name;
|
|
6567
7804
|
}
|
|
@@ -6580,11 +7817,6 @@ function uniqueQueriesByReadModel(queries) {
|
|
|
6580
7817
|
}
|
|
6581
7818
|
return [...queriesByReadModel.values()].sort((left, right) => resolvedReadModelName(left).localeCompare(resolvedReadModelName(right)));
|
|
6582
7819
|
}
|
|
6583
|
-
function queryViewFileBase(query) {
|
|
6584
|
-
const outputExportName = query.outputSchemaExportName?.trim();
|
|
6585
|
-
if (outputExportName) return kebabCase(outputExportName).replace(/-view$/, "");
|
|
6586
|
-
return readModelRepositoryFileBase(resolvedReadModelName(query));
|
|
6587
|
-
}
|
|
6588
7820
|
function queryReadModelPortTypeName(query) {
|
|
6589
7821
|
return `${readModelContractName(resolvedReadModelName(query))}RepositoryPort`;
|
|
6590
7822
|
}
|
|
@@ -6610,24 +7842,31 @@ function uniqueReadContexts(acls) {
|
|
|
6610
7842
|
return contexts;
|
|
6611
7843
|
}
|
|
6612
7844
|
function buildRepoFactoryContent(contextSpecs) {
|
|
6613
|
-
const repositoryDefs = contextSpecs.flatMap((spec) => spec.aggregates.map((aggregate) =>
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
7845
|
+
const repositoryDefs = contextSpecs.flatMap((spec) => spec.aggregates.map((aggregate) => {
|
|
7846
|
+
const repositoryPort = spec.ports.find((port) => port.kind === "repository" && port.target === aggregate.name);
|
|
7847
|
+
const isMikroOrm = (repositoryPort ? spec.adapters.find((candidate) => candidate.port === repositoryPort.name && (candidate.kind === "drizzle-repository" || candidate.kind === "mikroorm-repository")) : void 0)?.kind === "mikroorm-repository";
|
|
7848
|
+
const providerPrefix = isMikroOrm ? "mikroorm" : "drizzle";
|
|
7849
|
+
const repositoryClassPrefix = isMikroOrm ? "MikroOrm" : "Drizzle";
|
|
7850
|
+
return {
|
|
7851
|
+
aggregateName: aggregate.name,
|
|
7852
|
+
modulePath: spec.context.modulePath,
|
|
7853
|
+
repoFile: `${providerPrefix}-${kebabCase(aggregate.name)}.repository.ts`,
|
|
7854
|
+
repoImportBasePath: `../persistence/${providerPrefix}/repositories`,
|
|
7855
|
+
aggregateAliasBase: `${repositoryClassPrefix}${pascalCase(aggregate.name)}Repository`,
|
|
7856
|
+
aggregateFieldBase: `${camelCase(aggregate.name)}s`
|
|
7857
|
+
};
|
|
7858
|
+
}));
|
|
6620
7859
|
assertNoSameContextRepoFileCollisions(repositoryDefs);
|
|
6621
7860
|
const namingPlan = buildRepositoryNamingPlan(repositoryDefs);
|
|
6622
7861
|
const lines = [`// Auto-generated repository factory — do not edit by hand`, ``];
|
|
6623
7862
|
for (const repositoryDef of repositoryDefs) {
|
|
6624
|
-
|
|
7863
|
+
repositoryDef.aggregateName;
|
|
6625
7864
|
const modulePath = normalizeModulePath(repositoryDef.modulePath);
|
|
6626
|
-
const repoClass =
|
|
7865
|
+
const repoClass = repositoryDef.aggregateAliasBase;
|
|
6627
7866
|
const { repoAlias } = namingPlan.get(repositoryKey(repositoryDef));
|
|
6628
7867
|
const repoFile = repositoryDef.repoFile;
|
|
6629
|
-
if (repoAlias === repoClass) lines.push(`import { ${repoClass} } from "
|
|
6630
|
-
else lines.push(`import { ${repoClass} as ${repoAlias} } from "
|
|
7868
|
+
if (repoAlias === repoClass) lines.push(`import { ${repoClass} } from "${repositoryDef.repoImportBasePath}/${modulePath}/${repoFile}";`);
|
|
7869
|
+
else lines.push(`import { ${repoClass} as ${repoAlias} } from "${repositoryDef.repoImportBasePath}/${modulePath}/${repoFile}";`);
|
|
6631
7870
|
}
|
|
6632
7871
|
lines.push(``);
|
|
6633
7872
|
lines.push(`export class RepositoryFactory {`);
|
|
@@ -6765,9 +8004,14 @@ export {};
|
|
|
6765
8004
|
const payloadTypeNames = [];
|
|
6766
8005
|
for (const evt of compositionSpec.crossContextEvents) {
|
|
6767
8006
|
const fields = getPayloadFields(evt.payloadSchema);
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
8007
|
+
const payloadTypeName = integrationPayloadTypeName(evt);
|
|
8008
|
+
payloadTypeNames.push(payloadTypeName);
|
|
8009
|
+
lines.push(`export interface ${payloadTypeName} {`);
|
|
8010
|
+
for (const field of fields) {
|
|
8011
|
+
const opt = field.optional ? "?" : "";
|
|
8012
|
+
const comment = field.comment ? ` ${field.comment}` : "";
|
|
8013
|
+
lines.push(` readonly ${field.name}${opt}: ${field.type};${comment}`);
|
|
8014
|
+
}
|
|
6771
8015
|
lines.push(`}`);
|
|
6772
8016
|
lines.push(``);
|
|
6773
8017
|
}
|
|
@@ -6778,18 +8022,20 @@ export {};
|
|
|
6778
8022
|
return lines.join("\n");
|
|
6779
8023
|
}
|
|
6780
8024
|
function buildHandlerFactoryContent(evt, sub) {
|
|
6781
|
-
const
|
|
8025
|
+
const fields = getPayloadFields(evt.payloadSchema);
|
|
8026
|
+
const payloadTypeName = integrationPayloadTypeName(evt);
|
|
8027
|
+
const payloadMappingLines = fields.map((f) => ` ${f.name}: event.payload.${f.name},`);
|
|
6782
8028
|
const commandType = `${pascalCase(sub.targetContext)}.${pascalCase(sub.action)}`;
|
|
6783
8029
|
return [
|
|
6784
8030
|
`// Auto-generated event handler factory — do not edit by hand`,
|
|
6785
8031
|
`import type { CommandBusPort } from "../../../../../core/ports/messaging/command-bus.port.ts";`,
|
|
6786
8032
|
`import type { IntegrationEvent, IntegrationEventHandler } from "../../../../../infrastructure/messaging/integration-event.ts";`,
|
|
6787
|
-
`import type { ${
|
|
8033
|
+
`import type { ${payloadTypeName} } from "../../../../shared-kernel/events/integration-events.types.ts";`,
|
|
6788
8034
|
``,
|
|
6789
8035
|
`export function ${sub.handlerFactoryName}(deps: {`,
|
|
6790
8036
|
` commandBus: CommandBusPort;`,
|
|
6791
|
-
`}): IntegrationEventHandler<${
|
|
6792
|
-
` return async (event: IntegrationEvent<${
|
|
8037
|
+
`}): IntegrationEventHandler<${payloadTypeName}> {`,
|
|
8038
|
+
` return async (event: IntegrationEvent<${payloadTypeName}>) => {`,
|
|
6793
8039
|
` return deps.commandBus.execute(`,
|
|
6794
8040
|
` {`,
|
|
6795
8041
|
` type: "${commandType}",`,
|
|
@@ -6808,30 +8054,114 @@ function buildSubscriptionRegistryContent(compositionSpec) {
|
|
|
6808
8054
|
const allSubs = [];
|
|
6809
8055
|
for (const evt of compositionSpec.crossContextEvents) for (const sub of evt.subscriptions) allSubs.push({
|
|
6810
8056
|
eventType: evt.eventType,
|
|
8057
|
+
payloadTypeName: integrationPayloadTypeName(evt),
|
|
6811
8058
|
sub
|
|
6812
8059
|
});
|
|
6813
8060
|
const importLines = [`// Auto-generated subscription registry — do not edit by hand`, `import type { EventSubscription, IntegrationEventHandler } from "./integration-event.ts";`];
|
|
8061
|
+
const payloadTypeImports = [...new Set(allSubs.map(({ payloadTypeName }) => payloadTypeName))].sort((left, right) => left.localeCompare(right)).map((payloadTypeName) => `import type { ${payloadTypeName} } from "../../core/shared-kernel/events/integration-events.types.ts";`);
|
|
8062
|
+
importLines.push(...payloadTypeImports);
|
|
6814
8063
|
for (const { sub } of allSubs) {
|
|
6815
8064
|
const fileName = `${kebabCase(sub.handlerName)}.handler.ts`;
|
|
6816
8065
|
importLines.push(`// Handler: ${sub.handlerFactoryName} from core/contexts/${sub.targetModulePath}/application/event-handlers/${fileName}`);
|
|
6817
8066
|
}
|
|
6818
|
-
const depsEntries = allSubs.map(({ sub }) => ` ${camelCase(sub.handlerName)}: IntegrationEventHandler
|
|
8067
|
+
const depsEntries = allSubs.map(({ payloadTypeName, sub }) => ` ${camelCase(sub.handlerName)}: IntegrationEventHandler<${payloadTypeName}>;`);
|
|
6819
8068
|
const registryEntries = allSubs.map(({ eventType, sub }) => ` { eventType: "${eventType}", handler: deps.${camelCase(sub.handlerName)} },`);
|
|
6820
8069
|
const lines = [...importLines, ``];
|
|
6821
8070
|
if (depsEntries.length > 0) lines.push(`export function buildSubscriptionRegistry(deps: {`, ...depsEntries, `}): EventSubscription[] {`, ` return [`, ...registryEntries, ` ];`, `}`, ``);
|
|
6822
8071
|
else lines.push(`export function buildSubscriptionRegistry(_deps: Record<string, never>): EventSubscription[] {`, ` return [];`, `}`, ``);
|
|
6823
8072
|
return lines.join("\n");
|
|
6824
8073
|
}
|
|
8074
|
+
function integrationPayloadTypeName(evt) {
|
|
8075
|
+
return `${pascalCase(evt.sourceContext)}${evt.payloadTypeName}`;
|
|
8076
|
+
}
|
|
8077
|
+
function renderPayloadSchemaType(schema, indent = 0) {
|
|
8078
|
+
const raw = schema;
|
|
8079
|
+
const def = raw?._zod?.def;
|
|
8080
|
+
const typeName = typeof def?.type === "string" ? def.type : void 0;
|
|
8081
|
+
if (typeName === "optional") return {
|
|
8082
|
+
...renderPayloadSchemaType(def?.innerType, indent),
|
|
8083
|
+
optional: true
|
|
8084
|
+
};
|
|
8085
|
+
if (typeName === "nullable" || typeName === "readonly" || typeName === "default" || typeName === "catch" || typeName === "nonoptional" || typeName === "success" || typeName === "prefault") return renderPayloadSchemaType(def?.innerType, indent);
|
|
8086
|
+
if (typeName === "array") {
|
|
8087
|
+
const element = renderPayloadSchemaType(def?.element, indent);
|
|
8088
|
+
return {
|
|
8089
|
+
type: `Array<${element.type}>`,
|
|
8090
|
+
optional: false,
|
|
8091
|
+
comment: element.comment
|
|
8092
|
+
};
|
|
8093
|
+
}
|
|
8094
|
+
if (typeName === "object") {
|
|
8095
|
+
const shape = typeof raw?.shape === "object" && raw?.shape !== null ? raw.shape : typeof def?.shape === "object" && def?.shape !== null ? def.shape : {};
|
|
8096
|
+
return {
|
|
8097
|
+
type: `{\n${Object.entries(shape).map(([fieldName, fieldSchema]) => {
|
|
8098
|
+
const field = renderPayloadSchemaType(fieldSchema, indent + 1);
|
|
8099
|
+
const opt = field.optional ? "?" : "";
|
|
8100
|
+
const comment = field.comment ? ` ${field.comment}` : "";
|
|
8101
|
+
return `${" ".repeat(indent + 1)}readonly ${fieldName}${opt}: ${field.type};${comment}`;
|
|
8102
|
+
}).join("\n")}\n${" ".repeat(indent)}}`,
|
|
8103
|
+
optional: false
|
|
8104
|
+
};
|
|
8105
|
+
}
|
|
8106
|
+
if (typeName === "enum") {
|
|
8107
|
+
const entries = def?.entries;
|
|
8108
|
+
if (entries && typeof entries === "object") return {
|
|
8109
|
+
type: Object.values(entries).map((value) => JSON.stringify(String(value))).join(" | "),
|
|
8110
|
+
optional: false
|
|
8111
|
+
};
|
|
8112
|
+
return {
|
|
8113
|
+
type: "string",
|
|
8114
|
+
optional: false
|
|
8115
|
+
};
|
|
8116
|
+
}
|
|
8117
|
+
if (typeName === "record") {
|
|
8118
|
+
const valueShape = renderPayloadSchemaType(def?.valueType, indent);
|
|
8119
|
+
return {
|
|
8120
|
+
type: `Record<string, ${valueShape.type}>`,
|
|
8121
|
+
optional: false,
|
|
8122
|
+
comment: valueShape.comment
|
|
8123
|
+
};
|
|
8124
|
+
}
|
|
8125
|
+
if (typeName === "tuple") return {
|
|
8126
|
+
type: `[${(Array.isArray(def?.items) ? def.items : []).map((item) => renderPayloadSchemaType(item, indent).type).join(", ")}]`,
|
|
8127
|
+
optional: false
|
|
8128
|
+
};
|
|
8129
|
+
if (typeName === "union") return {
|
|
8130
|
+
type: (Array.isArray(def?.options) ? def.options : []).map((option) => renderPayloadSchemaType(option, indent).type).join(" | "),
|
|
8131
|
+
optional: false
|
|
8132
|
+
};
|
|
8133
|
+
if (typeName === "date") return {
|
|
8134
|
+
type: "string",
|
|
8135
|
+
optional: false,
|
|
8136
|
+
comment: "// ISO 8601"
|
|
8137
|
+
};
|
|
8138
|
+
if (typeName === "number" || typeName === "int" || typeName === "float") return {
|
|
8139
|
+
type: "number",
|
|
8140
|
+
optional: false
|
|
8141
|
+
};
|
|
8142
|
+
if (typeName === "boolean") return {
|
|
8143
|
+
type: "boolean",
|
|
8144
|
+
optional: false
|
|
8145
|
+
};
|
|
8146
|
+
if (typeName === "string") return {
|
|
8147
|
+
type: "string",
|
|
8148
|
+
optional: false
|
|
8149
|
+
};
|
|
8150
|
+
return {
|
|
8151
|
+
type: "string",
|
|
8152
|
+
optional: false
|
|
8153
|
+
};
|
|
8154
|
+
}
|
|
6825
8155
|
function getPayloadFields(payloadSchema) {
|
|
6826
8156
|
const shape = payloadSchema?.shape;
|
|
6827
8157
|
if (shape && typeof shape === "object") return Object.keys(shape).map((key) => {
|
|
6828
|
-
const
|
|
6829
|
-
|
|
6830
|
-
if (typeName === "number") tsType = "number";
|
|
6831
|
-
else if (typeName === "boolean") tsType = "boolean";
|
|
8158
|
+
const fieldSchema = shape[key];
|
|
8159
|
+
const rendered = renderPayloadSchemaType(fieldSchema);
|
|
6832
8160
|
return {
|
|
6833
8161
|
name: key,
|
|
6834
|
-
type:
|
|
8162
|
+
type: rendered.type,
|
|
8163
|
+
optional: rendered.optional,
|
|
8164
|
+
comment: rendered.comment
|
|
6835
8165
|
};
|
|
6836
8166
|
});
|
|
6837
8167
|
return [];
|
|
@@ -6856,7 +8186,7 @@ async function buildContextNormalizedSpecFromConfig(input) {
|
|
|
6856
8186
|
}))), config);
|
|
6857
8187
|
}
|
|
6858
8188
|
async function buildV5ContextArtifacts(input) {
|
|
6859
|
-
return generateContextArtifacts(await buildContextNormalizedSpecFromConfig(input));
|
|
8189
|
+
return generateContextArtifacts(await buildContextNormalizedSpecFromConfig(input), { infrastructureStrategy: input.infrastructureStrategy });
|
|
6860
8190
|
}
|
|
6861
8191
|
async function buildV5Artifacts(input) {
|
|
6862
8192
|
const spec = await buildNormalizedSpecFromConfig(input);
|
|
@@ -6881,8 +8211,9 @@ async function buildCompositionArtifacts(compositionConfigPath, contextSpecs) {
|
|
|
6881
8211
|
return mergeGeneratedArtifacts([
|
|
6882
8212
|
...buildCompositionTypeArtifacts(),
|
|
6883
8213
|
...buildCompositionBusArtifacts(),
|
|
6884
|
-
...buildCompositionOutboxArtifacts(),
|
|
8214
|
+
...buildCompositionOutboxArtifacts(compositionSpec.infrastructure),
|
|
6885
8215
|
...buildCompositionAclArtifacts(compositionSpec),
|
|
8216
|
+
...buildCompositionDependencyManifestArtifacts(compositionSpec),
|
|
6886
8217
|
...buildReadModelCompositionArtifacts(composedContextSpecs),
|
|
6887
8218
|
...buildCompositionContainerArtifacts(compositionSpec, composedContextSpecs),
|
|
6888
8219
|
...buildCompositionRouterArtifacts(compositionSpec, composedContextSpecs),
|
|
@@ -6891,4 +8222,4 @@ async function buildCompositionArtifacts(compositionConfigPath, contextSpecs) {
|
|
|
6891
8222
|
}
|
|
6892
8223
|
|
|
6893
8224
|
//#endregion
|
|
6894
|
-
export { GENERATED_READ_SIDE_SCHEMA_LOGICAL_PATH, SPEC_ARTIFACT_SCHEMA_VERSION, SPEC_DIFF_SCHEMA_VERSION, VENDORED_FILE_MANIFEST, applyOwnershipIfMissing, buildArtifactPath, buildCollisionSafeModulePathNames, buildCompositionAclArtifacts, buildCompositionArtifacts, buildCompositionBusArtifacts, buildCompositionContainerArtifacts, buildCompositionOutboxArtifacts, buildCompositionRouterArtifacts, buildCompositionSpecDiff, buildCompositionSubscriptionArtifacts, buildCompositionTypeArtifacts, buildConsumerAclArtifacts, buildContextInputChecks, buildContextNormalizedSpec, buildContextNormalizedSpecFromConfig, buildContextSpecDiff, buildDrizzleConfig, buildFileArtifactTags, buildMaterializationManifestTags, buildMigrationResultTags, buildNormalizedCompositionSpec, buildNormalizedSpec, buildNormalizedSpecFromConfig, buildNormalizedSpecTags, buildProjectionArtifacts, buildReadModelCompositionArtifacts, buildSpecDiffTags, buildSummaryTags, buildV5ApplicationArtifacts, buildV5ApplicationContextArtifacts, buildV5Artifacts, buildV5ContextArtifacts, buildV5DomainArtifacts, buildV5InfrastructureArtifacts, buildV5InfrastructureContextArtifacts, buildV5LibArtifacts, buildV5PortArtifacts, buildV5PresentationArtifacts, buildV5RouteArtifacts, buildV5SharedKernelArtifacts, buildV5TestArtifacts, buildVendoredFileTags, camelCase, collectOwnedValueObjects, compositionSpecDiffDataName, contextArtifactOwnership, contextSpecDiffDataName, contractFileName, createGeneratedArtifact, discoverReferencedVos, ensureContextSupportFilesExist, filterPerContextArtifactsForComposition, flattenEntities, generateContextArtifacts, inferArtifactOwnership, inferCodegenSupportPathForContext, inferCodegenSupportPathFromSchemaFile, inferReconcileScopes, introspectObjectShape, introspectSchema, isRegisteredEntity, kebabCase, loadContextConfig, mergeGeneratedArtifacts, mergeSchemaReadResults, normalizeModulePath, parseConnectionString, parseDrizzleKitOutput, pascalCase, prepareAllVendoredFiles, prepareVendoredFile, readSchemaFile, repositoryPortFileName, repositoryPortTypeName, resolveAbsoluteContextInputs, resolveArtifactOwnership, resolveContextInputs, resolveFactoryPath, resolveGeneratedMigrationSchemaPath, resolveMethodContextInputsForCheck, resolveSchemaFilePathsForContext, resolveVoOwner, rewriteImports, runMigration, serializeCompositionSpec, serializeContextSpec, sharedArtifactOwnership, sliceArtifactOwnership, sliceContextIntoAggregateViews, snakeCase, snakeUpperCase, toImportURL, toUpperSnakeCase, unwrapFieldType, validateContextImports, walkEntityTree, withArtifactOwnership, wordsFromName };
|
|
8225
|
+
export { GENERATED_READ_SIDE_SCHEMA_LOGICAL_PATH, SPEC_ARTIFACT_SCHEMA_VERSION, SPEC_DIFF_SCHEMA_VERSION, VENDORED_FILE_MANIFEST, applyOwnershipIfMissing, buildArtifactPath, buildCollisionSafeModulePathNames, buildCompositionAclArtifacts, buildCompositionArtifacts, buildCompositionBusArtifacts, buildCompositionContainerArtifacts, buildCompositionDependencyManifestArtifacts, buildCompositionOutboxArtifacts, buildCompositionRouterArtifacts, buildCompositionSpecDiff, buildCompositionSubscriptionArtifacts, buildCompositionTypeArtifacts, buildConsumerAclArtifacts, buildContextInputChecks, buildContextNormalizedSpec, buildContextNormalizedSpecFromConfig, buildContextSpecDiff, buildDrizzleConfig, buildFileArtifactTags, buildMaterializationManifestTags, buildMigrationResultTags, buildNormalizedCompositionSpec, buildNormalizedSpec, buildNormalizedSpecFromConfig, buildNormalizedSpecTags, buildProjectionArtifacts, buildReadModelCompositionArtifacts, buildSpecDiffTags, buildSummaryTags, buildV5ApplicationArtifacts, buildV5ApplicationContextArtifacts, buildV5Artifacts, buildV5ContextArtifacts, buildV5DomainArtifacts, buildV5InfrastructureArtifacts, buildV5InfrastructureContextArtifacts, buildV5LibArtifacts, buildV5PortArtifacts, buildV5PresentationArtifacts, buildV5RouteArtifacts, buildV5SharedKernelArtifacts, buildV5TestArtifacts, buildVendoredFileTags, camelCase, collectOwnedValueObjects, compositionSpecDiffDataName, contextArtifactOwnership, contextSpecDiffDataName, contractFileName, createGeneratedArtifact, discoverReferencedVos, ensureContextSupportFilesExist, filterPerContextArtifactsForComposition, flattenEntities, generateContextArtifacts, inferArtifactOwnership, inferCodegenSupportPathForContext, inferCodegenSupportPathFromSchemaFile, inferReconcileScopes, introspectObjectShape, introspectSchema, isRegisteredEntity, kebabCase, loadContextConfig, mergeGeneratedArtifacts, mergeSchemaReadResults, normalizeModulePath, parseConnectionString, parseDrizzleKitOutput, pascalCase, prepareAllVendoredFiles, prepareVendoredFile, readSchemaFile, repositoryPortFileName, repositoryPortTypeName, resolveAbsoluteContextInputs, resolveArtifactOwnership, resolveContextInputs, resolveFactoryPath, resolveGeneratedMigrationSchemaPath, resolveMethodContextInputsForCheck, resolveSchemaFilePathsForContext, resolveVoOwner, rewriteImports, runMigration, serializeCompositionSpec, serializeContextSpec, sharedArtifactOwnership, sliceArtifactOwnership, sliceContextIntoAggregateViews, snakeCase, snakeUpperCase, toImportURL, toUpperSnakeCase, unwrapFieldType, validateContextImports, walkEntityTree, withArtifactOwnership, wordsFromName };
|