@zodmire/core 0.1.0 → 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 +38 -6
- package/mod.mjs +1611 -264
- package/package.json +2 -2
package/mod.mjs
CHANGED
|
@@ -63,7 +63,7 @@ const VENDORED_FILE_MANIFEST = [
|
|
|
63
63
|
sourceRelativePath: "packages/config/composition_config.ts"
|
|
64
64
|
}
|
|
65
65
|
];
|
|
66
|
-
/** Rewrite
|
|
66
|
+
/** Rewrite `npm:zod@*` imports to bare `"zod"` for end-user consumption. */
|
|
67
67
|
function rewriteImports(content) {
|
|
68
68
|
return content.replace(/["']npm:zod@[^"']*["']/g, "\"zod\"");
|
|
69
69
|
}
|
|
@@ -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
|
}
|
|
@@ -2069,8 +2232,11 @@ function readModelPortBaseFileName(readModelName) {
|
|
|
2069
2232
|
function resolvedReadModelName$2(query) {
|
|
2070
2233
|
return query.readSide?.readModelName ?? query.readModelName ?? query.name;
|
|
2071
2234
|
}
|
|
2235
|
+
function queryOutputExportName$1(query) {
|
|
2236
|
+
return query.outputSchemaExportName?.trim() || void 0;
|
|
2237
|
+
}
|
|
2072
2238
|
function queryReadModelPortTypeName$1(query) {
|
|
2073
|
-
return `${readModelContractName$
|
|
2239
|
+
return `${readModelContractName$2(resolvedReadModelName$2(query))}RepositoryPort`;
|
|
2074
2240
|
}
|
|
2075
2241
|
function queryReadModelPortFileName(query) {
|
|
2076
2242
|
return `read-models/${readModelPortBaseFileName(resolvedReadModelName$2(query))}.repository.port.ts`;
|
|
@@ -2130,11 +2296,11 @@ ${query.outputFields.map((f) => {
|
|
|
2130
2296
|
function createListQueryHandlerStub(_spec, query) {
|
|
2131
2297
|
const handlerName = `${pascalCase(query.name)}Handler`;
|
|
2132
2298
|
const queryTypeName = `${pascalCase(query.name)}Query`;
|
|
2133
|
-
const outputContractName = queryOutputContractName$
|
|
2299
|
+
const outputContractName = queryOutputContractName$1(query);
|
|
2134
2300
|
const readModelPortType = queryReadModelPortTypeName$1(query);
|
|
2135
2301
|
const readModelVariable = queryReadModelVariableName$1(query);
|
|
2136
2302
|
const readModelMethodName = queryReadModelMethodName$1(query);
|
|
2137
|
-
const viewFileBase = queryViewFileBase$
|
|
2303
|
+
const viewFileBase = queryViewFileBase$1(query);
|
|
2138
2304
|
return `import type { ${queryTypeName} } from "./${kebabCase(query.name)}.query.ts";
|
|
2139
2305
|
import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";
|
|
2140
2306
|
import type { ${readModelPortType} } from "../ports/${queryReadModelPortFileName(query)}";
|
|
@@ -2162,12 +2328,14 @@ function createHandlerDepsStub(spec) {
|
|
|
2162
2328
|
const repoPort = findRepositoryPort(spec);
|
|
2163
2329
|
const repoTypeName = repoPort ? repositoryPortTypeName(repoPort.name) : `${aggregatePascal}Repository`;
|
|
2164
2330
|
return `import type { Transaction } from "../../../../lib/transaction.ts";
|
|
2331
|
+
import type { AggregateEventTracker } from "../../../shared-kernel/events/aggregate-event-tracker.ts";
|
|
2165
2332
|
import type { EventCollector } from "../../../shared-kernel/events/event-collector.ts";
|
|
2166
2333
|
import type { ${repoTypeName} } from "./ports/${repoPort ? repositoryPortFileName(repoPort.name) : `${kebabCase(spec.aggregate.name)}-repository.port.ts`}";
|
|
2167
2334
|
|
|
2168
2335
|
export type ${contextPascal}CommandHandlerDeps = {
|
|
2169
2336
|
tx: Transaction;
|
|
2170
2337
|
eventCollector: EventCollector;
|
|
2338
|
+
aggregateEventTracker: AggregateEventTracker;
|
|
2171
2339
|
repos: {
|
|
2172
2340
|
${aggregateVar}s: ${repoTypeName};
|
|
2173
2341
|
};
|
|
@@ -2193,7 +2361,7 @@ ${(query.inputFields ?? []).map((f) => {
|
|
|
2193
2361
|
`;
|
|
2194
2362
|
}
|
|
2195
2363
|
function createViewContractStub(query) {
|
|
2196
|
-
return `export interface ${queryOutputContractName$
|
|
2364
|
+
return `export interface ${queryOutputContractName$1(query)} {
|
|
2197
2365
|
${query.outputFields.map((f) => {
|
|
2198
2366
|
const opt = f.optional ? "?" : "";
|
|
2199
2367
|
return ` readonly ${camelCase(f.name)}${opt}: ${formatFieldType(f)};`;
|
|
@@ -2201,11 +2369,14 @@ ${query.outputFields.map((f) => {
|
|
|
2201
2369
|
}
|
|
2202
2370
|
`;
|
|
2203
2371
|
}
|
|
2204
|
-
function queryOutputContractName$
|
|
2205
|
-
const
|
|
2206
|
-
|
|
2372
|
+
function queryOutputContractName$1(query) {
|
|
2373
|
+
const outputExportName = queryOutputExportName$1(query);
|
|
2374
|
+
if (outputExportName) return pascalCase(outputExportName);
|
|
2375
|
+
return readModelContractName$2(resolvedReadModelName$2(query));
|
|
2207
2376
|
}
|
|
2208
|
-
function queryViewFileBase$
|
|
2377
|
+
function queryViewFileBase$1(query) {
|
|
2378
|
+
const outputExportName = queryOutputExportName$1(query);
|
|
2379
|
+
if (outputExportName) return kebabCase(outputExportName).replace(/-view$/, "");
|
|
2209
2380
|
return kebabCase(resolvedReadModelName$2(query)).replace(/-view$/, "");
|
|
2210
2381
|
}
|
|
2211
2382
|
function createCommandStub(spec, command) {
|
|
@@ -2254,7 +2425,8 @@ function buildCreateHandlerBody(spec, command) {
|
|
|
2254
2425
|
const assignedValue = createPropAssignments.get(fieldName) ?? (commandInputFieldNames.has(fieldName) ? commandInputField?.optional && !field.optional ? `command.payload.${fieldName} ?? ${defaultValue}` : `command.payload.${fieldName}` : defaultValue);
|
|
2255
2426
|
if (assignedValue !== void 0) propsLines.push(` ${fieldName}: ${assignedValue},`);
|
|
2256
2427
|
}
|
|
2257
|
-
const
|
|
2428
|
+
const defaultIdBrand = `${pascalCase(spec.aggregate.name)}Id`;
|
|
2429
|
+
const idTypeName = hasCustomIdType ? pascalCase(idType) : `BrandedId<"${defaultIdBrand}">`;
|
|
2258
2430
|
const creationEventName = command.emits?.[0];
|
|
2259
2431
|
const creationEvent = creationEventName ? spec.domainEvents.find((e) => pascalCase(e.name) === pascalCase(creationEventName)) : void 0;
|
|
2260
2432
|
const aggregateFieldNames = new Set((spec.aggregate.fields ?? []).map((field) => camelCase(field.name)));
|
|
@@ -2285,7 +2457,7 @@ function buildCreateHandlerBody(spec, command) {
|
|
|
2285
2457
|
const payloadName = `${pascalCase(creationEventName)}Payload`;
|
|
2286
2458
|
imports.push(`import type { ${payloadName} } from "../../domain/events/${kebabCase(creationEventName)}.event.ts";`);
|
|
2287
2459
|
}
|
|
2288
|
-
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())`;
|
|
2289
2461
|
let creationEventLines = "";
|
|
2290
2462
|
if (creationEvent) {
|
|
2291
2463
|
const payloadName = `${pascalCase(creationEventName)}Payload`;
|
|
@@ -2304,7 +2476,7 @@ function buildCreateHandlerBody(spec, command) {
|
|
|
2304
2476
|
else if (fieldName === "recordedBy" || fieldName === "correlationId" || fieldName === "causationId") {
|
|
2305
2477
|
const metadataExpr = fieldName === "recordedBy" ? f.type === "string" ? "eventContext.recordedBy.value" : "eventContext.recordedBy" : `eventContext.${fieldName}`;
|
|
2306
2478
|
payloadFields.push(` ${fieldName}: ${metadataExpr},`);
|
|
2307
|
-
} else payloadFields.push(` ${fieldName}:
|
|
2479
|
+
} else payloadFields.push(` ${fieldName}: /* TODO: unmapped ${payloadName} field "${fieldName}" */ undefined as never,`);
|
|
2308
2480
|
}
|
|
2309
2481
|
creationEventLines = `
|
|
2310
2482
|
deps.eventCollector.collect([
|
|
@@ -2381,7 +2553,7 @@ function buildMutationHandlerBody(spec, command) {
|
|
|
2381
2553
|
}
|
|
2382
2554
|
imports.push(`import type { ${notFoundErrorType} } from "../../domain/errors/${aggregateDir}-application-errors.ts";`);
|
|
2383
2555
|
if (hasCustomIdType && idCreatorFn && idType) imports.push(`import { ${idCreatorFn} } from "../../../../shared-kernel/entity-ids/${kebabCase(idType)}.ts";`);
|
|
2384
|
-
const domainMethodArg = cmdEmitsEvents ? "command.payload
|
|
2556
|
+
const domainMethodArg = cmdEmitsEvents ? "command.payload, eventContext" : "command.payload";
|
|
2385
2557
|
const loadByField = command.loadBy?.startsWith("input.") ? command.loadBy.slice(6) : aggregateId;
|
|
2386
2558
|
const brandedIdExpr = hasCustomIdType && idCreatorFn ? `${idCreatorFn}(command.payload.${loadByField})` : `command.payload.${loadByField}`;
|
|
2387
2559
|
let bodyLines;
|
|
@@ -2393,7 +2565,7 @@ function buildMutationHandlerBody(spec, command) {
|
|
|
2393
2565
|
` const result = ${aggregateVar}.${methodName}(${domainMethodArg});`,
|
|
2394
2566
|
` if (!result.ok) return result;`,
|
|
2395
2567
|
` await deps.repos.${camelCase(spec.aggregate.name)}s.save(${aggregateVar}, loadedVersion, deps.tx);`,
|
|
2396
|
-
` deps.
|
|
2568
|
+
` deps.aggregateEventTracker.track(${aggregateVar});`,
|
|
2397
2569
|
` return ok(undefined);`
|
|
2398
2570
|
];
|
|
2399
2571
|
else bodyLines = [
|
|
@@ -2403,7 +2575,7 @@ function buildMutationHandlerBody(spec, command) {
|
|
|
2403
2575
|
` const loadedVersion = ${aggregateVar}.version;`,
|
|
2404
2576
|
` ${aggregateVar}.${methodName}(${domainMethodArg});`,
|
|
2405
2577
|
` await deps.repos.${camelCase(spec.aggregate.name)}s.save(${aggregateVar}, loadedVersion, deps.tx);`,
|
|
2406
|
-
` deps.
|
|
2578
|
+
` deps.aggregateEventTracker.track(${aggregateVar});`,
|
|
2407
2579
|
` return ok(undefined);`
|
|
2408
2580
|
];
|
|
2409
2581
|
const handlerErrorTypeExport = hasPreconditions ? `export type ${handlerErrorType} =\n | ${errorTypes.join("\n | ")};\n` : `export type ${handlerErrorType} = ${notFoundErrorType};\n`;
|
|
@@ -2436,11 +2608,14 @@ export async function ${handlerFnName}(
|
|
|
2436
2608
|
command: ${commandTypeName},
|
|
2437
2609
|
deps: ${depsTypeName},
|
|
2438
2610
|
eventContext: EventContext,
|
|
2439
|
-
): Promise<
|
|
2611
|
+
): Promise<never> {
|
|
2440
2612
|
void command;
|
|
2441
2613
|
void deps;
|
|
2442
2614
|
void eventContext;
|
|
2443
|
-
|
|
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
|
+
);
|
|
2444
2619
|
}
|
|
2445
2620
|
`;
|
|
2446
2621
|
}
|
|
@@ -2457,12 +2632,12 @@ export interface ${queryName} {
|
|
|
2457
2632
|
}
|
|
2458
2633
|
function buildQueryHandlerBody(_spec, query) {
|
|
2459
2634
|
const queryTypeName = `${pascalCase(query.name)}Query`;
|
|
2460
|
-
const outputContractName = queryOutputContractName$
|
|
2635
|
+
const outputContractName = queryOutputContractName$1(query);
|
|
2461
2636
|
const readModelPortType = queryReadModelPortTypeName$1(query);
|
|
2462
2637
|
const handlerName = `${pascalCase(query.name)}Handler`;
|
|
2463
2638
|
const readModelVariable = queryReadModelVariableName$1(query);
|
|
2464
2639
|
const readModelMethodName = queryReadModelMethodName$1(query);
|
|
2465
|
-
const viewFileBase = queryViewFileBase$
|
|
2640
|
+
const viewFileBase = queryViewFileBase$1(query);
|
|
2466
2641
|
return `import type { ${queryTypeName} } from "./${kebabCase(query.name)}.query.ts";
|
|
2467
2642
|
import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";
|
|
2468
2643
|
import type { ${readModelPortType} } from "../ports/${queryReadModelPortFileName(query)}";
|
|
@@ -2483,12 +2658,12 @@ export class ${handlerName} {
|
|
|
2483
2658
|
function createQueryHandlerStub(_spec, query) {
|
|
2484
2659
|
if (query.outputFields && query.outputFields.length > 0) return buildQueryHandlerBody(_spec, query);
|
|
2485
2660
|
const queryTypeName = `${pascalCase(query.name)}Query`;
|
|
2486
|
-
const outputContractName = queryOutputContractName$
|
|
2661
|
+
const outputContractName = queryOutputContractName$1(query);
|
|
2487
2662
|
const readModelPortType = queryReadModelPortTypeName$1(query);
|
|
2488
2663
|
const handlerName = `${pascalCase(query.name)}Handler`;
|
|
2489
2664
|
const readModelVariable = queryReadModelVariableName$1(query);
|
|
2490
2665
|
const readModelMethodName = queryReadModelMethodName$1(query);
|
|
2491
|
-
const viewFileBase = queryViewFileBase$
|
|
2666
|
+
const viewFileBase = queryViewFileBase$1(query);
|
|
2492
2667
|
return `import type { ${queryTypeName} } from "./${kebabCase(query.name)}.query.ts";
|
|
2493
2668
|
import type { ${outputContractName} } from "../contracts/${viewFileBase}.view.ts";
|
|
2494
2669
|
import type { ${readModelPortType} } from "../ports/${queryReadModelPortFileName(query)}";
|
|
@@ -2551,46 +2726,71 @@ function groupQueriesByReadModel$1(queries) {
|
|
|
2551
2726
|
}));
|
|
2552
2727
|
}
|
|
2553
2728
|
function createReadModelRepositoryPortStub(readModelName, queries) {
|
|
2554
|
-
const portTypeName = `${readModelContractName$
|
|
2729
|
+
const portTypeName = `${readModelContractName$2(readModelName)}RepositoryPort`;
|
|
2555
2730
|
const importLines = /* @__PURE__ */ new Map();
|
|
2556
2731
|
let needsPaginatedResult = false;
|
|
2557
|
-
importLines.set("tx", `import type { Transaction } from "
|
|
2732
|
+
importLines.set("tx", `import type { Transaction } from "../../../../../../lib/transaction.ts";`);
|
|
2558
2733
|
const queryMethodLines = queries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((query) => {
|
|
2559
2734
|
const queryTypeName = `${pascalCase(query.name)}Query`;
|
|
2560
|
-
const outputContractName = queryOutputContractName$
|
|
2561
|
-
const viewFileBase = queryViewFileBase$
|
|
2735
|
+
const outputContractName = queryOutputContractName$1(query);
|
|
2736
|
+
const viewFileBase = queryViewFileBase$1(query);
|
|
2562
2737
|
const returnType = query.queryKind === "list" ? `PaginatedResult<${outputContractName}>` : outputContractName;
|
|
2563
2738
|
if (query.queryKind === "list") needsPaginatedResult = true;
|
|
2564
|
-
importLines.set(`query:${queryTypeName}`, `import type { ${queryTypeName} } from "
|
|
2565
|
-
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";`);
|
|
2566
2741
|
return ` ${queryReadModelMethodName$1(query)}(query: ${queryTypeName}, tx: Transaction): Promise<${returnType}>;`;
|
|
2567
2742
|
});
|
|
2568
|
-
if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "
|
|
2743
|
+
if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "../../../../../../lib/pagination.ts";`);
|
|
2569
2744
|
return `${[...importLines.values()].join("\n")}
|
|
2570
2745
|
|
|
2571
2746
|
// Read-model repository port. Query handlers share this contract by read-model, not by query.
|
|
2572
2747
|
export interface ${portTypeName} {
|
|
2573
|
-
// Query methods
|
|
2574
2748
|
${queryMethodLines.join("\n")}
|
|
2575
|
-
|
|
2576
|
-
// Projector methods
|
|
2577
|
-
// TODO: add typed row write methods when projector/repository generation lands.
|
|
2578
|
-
|
|
2579
|
-
// Rebuild methods
|
|
2580
|
-
// TODO: add rebuild helpers when projection rebuild scaffolding lands.
|
|
2581
2749
|
}
|
|
2582
2750
|
`;
|
|
2583
2751
|
}
|
|
2584
2752
|
function createReadModelPortsIndexStub(queries) {
|
|
2585
2753
|
return `${groupQueriesByReadModel$1(queries).map(({ readModelName }) => {
|
|
2586
|
-
return `export type { ${`${readModelContractName$
|
|
2754
|
+
return `export type { ${`${readModelContractName$2(readModelName)}RepositoryPort`} } from "./${readModelPortBaseFileName(readModelName)}.repository.port.ts";`;
|
|
2587
2755
|
}).join("\n")}\n`;
|
|
2588
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
|
+
}
|
|
2589
2789
|
function inputContractFileName(commandOrQueryName) {
|
|
2590
2790
|
return `${kebabCase(commandOrQueryName)}.input`;
|
|
2591
2791
|
}
|
|
2592
2792
|
function viewContractFileName(query) {
|
|
2593
|
-
return `${
|
|
2793
|
+
return `${queryViewFileBase$1(query)}.view`;
|
|
2594
2794
|
}
|
|
2595
2795
|
function createHandlerMapStub(spec) {
|
|
2596
2796
|
const contextPascal = pascalCase(spec.context.name);
|
|
@@ -2613,7 +2813,11 @@ ${mapEntries.join("\n")}
|
|
|
2613
2813
|
}
|
|
2614
2814
|
function createContextHandlerDepsStub(contextSpec) {
|
|
2615
2815
|
const contextPascal = pascalCase(contextSpec.context.name);
|
|
2616
|
-
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
|
+
];
|
|
2617
2821
|
const repoEntries = [];
|
|
2618
2822
|
const aclEntries = [];
|
|
2619
2823
|
for (const agg of contextSpec.aggregates) {
|
|
@@ -2639,6 +2843,7 @@ ${aclEntries.join("\n")}
|
|
|
2639
2843
|
export type ${contextPascal}CommandHandlerDeps = {
|
|
2640
2844
|
tx: Transaction;
|
|
2641
2845
|
eventCollector: EventCollector;
|
|
2846
|
+
aggregateEventTracker: AggregateEventTracker;
|
|
2642
2847
|
repos: {
|
|
2643
2848
|
${repoEntries.join("\n")}
|
|
2644
2849
|
};
|
|
@@ -2682,9 +2887,9 @@ function createPublishedReadBoundaryStub(contextSpec) {
|
|
|
2682
2887
|
const queryHandlerName = `${pascalCase(query.name)}Handler`;
|
|
2683
2888
|
const handlerVarName = `${camelCase(query.name)}Handler`;
|
|
2684
2889
|
const queryTypeName = `${pascalCase(query.name)}Query`;
|
|
2685
|
-
const outputContractName = queryOutputContractName$
|
|
2890
|
+
const outputContractName = queryOutputContractName$1(query);
|
|
2686
2891
|
const queryFileName = kebabCase(query.name);
|
|
2687
|
-
const outputFileName = queryViewFileBase$
|
|
2892
|
+
const outputFileName = queryViewFileBase$1(query);
|
|
2688
2893
|
const readModelDepName = queryReadModelVariableName$1(query);
|
|
2689
2894
|
const readModelTypeName = queryReadModelPortTypeName$1(query);
|
|
2690
2895
|
imports.set(`handler:${queryHandlerName}`, `import { ${queryHandlerName} } from "./queries/${queryFileName}.handler.ts";`);
|
|
@@ -2766,16 +2971,25 @@ function buildV5ApplicationArtifacts(spec, options) {
|
|
|
2766
2971
|
}
|
|
2767
2972
|
function buildV5ApplicationContextArtifacts(contextSpec) {
|
|
2768
2973
|
const modulePath = normalizeModulePath(contextSpec.context.modulePath);
|
|
2769
|
-
|
|
2974
|
+
const artifacts = [
|
|
2770
2975
|
createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/ports/read-models/index.ts"), createReadModelPortsIndexStub(contextSpec.queries)),
|
|
2771
2976
|
createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/handler-deps.ts"), createContextHandlerDepsStub(contextSpec)),
|
|
2772
2977
|
createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/read-boundary.ts"), createPublishedReadBoundaryStub(contextSpec)),
|
|
2773
2978
|
createGeneratedArtifact(buildArtifactPath("core/contexts", modulePath, "application/handler-map.ts"), createContextHandlerMapStub(contextSpec))
|
|
2774
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;
|
|
2775
2986
|
}
|
|
2776
2987
|
|
|
2777
2988
|
//#endregion
|
|
2778
2989
|
//#region packages/core/generators/infrastructure.ts
|
|
2990
|
+
function resolveDrizzlePersistence(infrastructureStrategy) {
|
|
2991
|
+
return infrastructureStrategy?.persistence === "mysql" ? "mysql" : "postgres";
|
|
2992
|
+
}
|
|
2779
2993
|
function isDecimalNumberField(field) {
|
|
2780
2994
|
if (field.type !== "number") return false;
|
|
2781
2995
|
const explicitColumnType = field.columnType?.toLowerCase();
|
|
@@ -2790,10 +3004,12 @@ function isDecimalNumberField(field) {
|
|
|
2790
3004
|
const fieldName = snakeCase(field.name);
|
|
2791
3005
|
return /(^|_)(cpk|ppk|ucl|lcl)(_|$)/.test(fieldName) || /(percent|percentage|ratio|rate|average|avg|variance|yield|scrap|drift)/.test(fieldName);
|
|
2792
3006
|
}
|
|
2793
|
-
function drizzleColumnBuilder(field, columnName) {
|
|
3007
|
+
function drizzleColumnBuilder(field, columnName, persistence = "postgres") {
|
|
2794
3008
|
let builder;
|
|
2795
|
-
if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0)
|
|
2796
|
-
|
|
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) {
|
|
2797
3013
|
case "number":
|
|
2798
3014
|
builder = isDecimalNumberField(field) ? `real("${columnName}")` : `integer("${columnName}")`;
|
|
2799
3015
|
break;
|
|
@@ -2900,12 +3116,12 @@ function relativePrefixFromRepositoryDirectory(modulePath) {
|
|
|
2900
3116
|
...modulePath.split("/").filter(Boolean)
|
|
2901
3117
|
].map(() => "..").join("/");
|
|
2902
3118
|
}
|
|
2903
|
-
function readModelContractName$
|
|
3119
|
+
function readModelContractName$1(readModelName) {
|
|
2904
3120
|
const baseName = pascalCase(readModelName);
|
|
2905
3121
|
return baseName.endsWith("View") ? baseName : `${baseName}View`;
|
|
2906
3122
|
}
|
|
2907
3123
|
function readModelRepositoryClassName(readModelName) {
|
|
2908
|
-
return `Drizzle${readModelContractName$
|
|
3124
|
+
return `Drizzle${readModelContractName$1(readModelName)}Repository`;
|
|
2909
3125
|
}
|
|
2910
3126
|
function readModelRepositoryFileBase$1(readModelName) {
|
|
2911
3127
|
return kebabCase(readModelName).replace(/-view$/, "");
|
|
@@ -2913,13 +3129,29 @@ function readModelRepositoryFileBase$1(readModelName) {
|
|
|
2913
3129
|
function readModelTableConstName(readModelName) {
|
|
2914
3130
|
return `${camelCase(readModelName)}Table`;
|
|
2915
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
|
+
}
|
|
2916
3141
|
function resolvedReadModelName$1(query) {
|
|
2917
3142
|
return query.readSide?.readModelName ?? query.readModelName ?? query.name;
|
|
2918
3143
|
}
|
|
2919
|
-
function
|
|
2920
|
-
return
|
|
3144
|
+
function queryOutputExportName(query) {
|
|
3145
|
+
return query.outputSchemaExportName?.trim() || void 0;
|
|
2921
3146
|
}
|
|
2922
|
-
function
|
|
3147
|
+
function queryOutputContractName(query) {
|
|
3148
|
+
const outputExportName = queryOutputExportName(query);
|
|
3149
|
+
if (outputExportName) return pascalCase(outputExportName);
|
|
3150
|
+
return readModelContractName$1(resolvedReadModelName$1(query));
|
|
3151
|
+
}
|
|
3152
|
+
function queryViewFileBase(query) {
|
|
3153
|
+
const outputExportName = queryOutputExportName(query);
|
|
3154
|
+
if (outputExportName) return kebabCase(outputExportName).replace(/-view$/, "");
|
|
2923
3155
|
return readModelRepositoryFileBase$1(resolvedReadModelName$1(query));
|
|
2924
3156
|
}
|
|
2925
3157
|
function queryReadModelMethodName(query) {
|
|
@@ -2947,13 +3179,13 @@ const RESERVED_AGGREGATE_FIELD_NAMES = new Set(["version"]);
|
|
|
2947
3179
|
function filterManagedAggregateFields(fields) {
|
|
2948
3180
|
return fields.filter((field) => !RESERVED_AGGREGATE_FIELD_NAMES.has(camelCase(field.name)));
|
|
2949
3181
|
}
|
|
2950
|
-
function buildTableBlock(tableName, tableConstName, fields, idField, fkColumn, isAggregateRoot = false) {
|
|
3182
|
+
function buildTableBlock(tableName, tableConstName, fields, idField, fkColumn, isAggregateRoot = false, persistence = "postgres") {
|
|
2951
3183
|
const columnDefs = [];
|
|
2952
3184
|
const emittedColumns = /* @__PURE__ */ new Set();
|
|
2953
3185
|
for (const field of fields) {
|
|
2954
3186
|
const snakeName = snakeCase(field.name);
|
|
2955
3187
|
if (emittedColumns.has(snakeName)) continue;
|
|
2956
|
-
const columnDef = field.name === idField ? `text("${snakeName}").primaryKey()` : drizzleColumnBuilder(field, snakeName);
|
|
3188
|
+
const columnDef = field.name === idField ? `text("${snakeName}").primaryKey()` : drizzleColumnBuilder(field, snakeName, persistence);
|
|
2957
3189
|
columnDefs.push(` ${snakeName}: ${columnDef},`);
|
|
2958
3190
|
emittedColumns.add(snakeName);
|
|
2959
3191
|
}
|
|
@@ -2962,16 +3194,16 @@ function buildTableBlock(tableName, tableConstName, fields, idField, fkColumn, i
|
|
|
2962
3194
|
emittedColumns.add(fkColumn.name);
|
|
2963
3195
|
}
|
|
2964
3196
|
if (isAggregateRoot) columnDefs.push(` version: integer("version").default(0).notNull(),`);
|
|
2965
|
-
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});`;
|
|
2966
3198
|
}
|
|
2967
|
-
function createDrizzleTableDefinition(spec) {
|
|
3199
|
+
function createDrizzleTableDefinition(spec, persistence = "postgres") {
|
|
2968
3200
|
const fields = filterManagedAggregateFields(spec.aggregate.fields);
|
|
2969
3201
|
if (!fields || fields.length === 0) return null;
|
|
2970
3202
|
const children = spec.aggregate.children ?? [];
|
|
2971
3203
|
const aggScalarFields = scalarFields(fields, children);
|
|
2972
3204
|
const emittedTableNames = /* @__PURE__ */ new Set();
|
|
2973
3205
|
const blocks = [];
|
|
2974
|
-
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));
|
|
2975
3207
|
emittedTableNames.add(spec.aggregate.tableName);
|
|
2976
3208
|
const aggregate = {
|
|
2977
3209
|
name: spec.aggregate.name,
|
|
@@ -2988,13 +3220,13 @@ function createDrizzleTableDefinition(spec) {
|
|
|
2988
3220
|
blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, {
|
|
2989
3221
|
name: fkColName,
|
|
2990
3222
|
type: "text"
|
|
2991
|
-
}));
|
|
3223
|
+
}, false, persistence));
|
|
2992
3224
|
emittedTableNames.add(entity.tableName);
|
|
2993
3225
|
});
|
|
2994
3226
|
for (const entity of spec.entities ?? []) {
|
|
2995
3227
|
if (!entity.tableName || emittedTableNames.has(entity.tableName)) continue;
|
|
2996
3228
|
const entityScalarFields = scalarFields(entity.fields, entity.children ?? []);
|
|
2997
|
-
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));
|
|
2998
3230
|
emittedTableNames.add(entity.tableName);
|
|
2999
3231
|
}
|
|
3000
3232
|
const flatScalarFields = [
|
|
@@ -3002,12 +3234,12 @@ function createDrizzleTableDefinition(spec) {
|
|
|
3002
3234
|
...flattenEntities(aggregate).map((entity) => scalarFields(entity.fields, entity.children)),
|
|
3003
3235
|
...(spec.entities ?? []).map((entity) => scalarFields(entity.fields, entity.children ?? []))
|
|
3004
3236
|
].flat();
|
|
3005
|
-
const
|
|
3006
|
-
if (flatScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field)))
|
|
3007
|
-
if (flatScalarFields.some((field) => isDecimalNumberField(field)))
|
|
3008
|
-
if (flatScalarFields.some((field) => field.type === "boolean"))
|
|
3009
|
-
if (flatScalarFields.some((field) => field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0))
|
|
3010
|
-
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`;
|
|
3011
3243
|
}
|
|
3012
3244
|
function buildCodecBlock(_spec, entityName, fields, options) {
|
|
3013
3245
|
const varName = camelCase(entityName);
|
|
@@ -3078,6 +3310,280 @@ function createPersistenceEqualityBlock(name, fields, options) {
|
|
|
3078
3310
|
return `// Compiled from zx.deepEqual.writeable at generator time using the canonical ${pascalCase(name)} persistence row shape.
|
|
3079
3311
|
export const ${equalityName} = ${compiledEquality};`;
|
|
3080
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
|
+
}
|
|
3081
3587
|
function childTableVar(spec, tableName) {
|
|
3082
3588
|
const child = (spec.aggregate.children ?? []).find((c) => c.tableName === tableName);
|
|
3083
3589
|
return child ? `${camelCase(child.name)}Table` : `${camelCase(tableName)}Table`;
|
|
@@ -3735,29 +4241,39 @@ function buildV5InfrastructureArtifacts(spec, options) {
|
|
|
3735
4241
|
const modulePath = normalizeModulePath(spec.context.modulePath);
|
|
3736
4242
|
const scopeKey = sliceArtifactOwnership(modulePath);
|
|
3737
4243
|
const artifacts = [];
|
|
3738
|
-
|
|
3739
|
-
|
|
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));
|
|
3740
4248
|
if (tableContent !== null) {
|
|
3741
4249
|
const tableLogicalPath = `infrastructure/persistence/${modulePath}/tables.ts`;
|
|
3742
4250
|
artifacts.push(createGeneratedArtifact(tableLogicalPath, tableContent, scopeKey));
|
|
3743
4251
|
}
|
|
3744
4252
|
}
|
|
3745
|
-
const repoArtifacts = spec.adapters.
|
|
4253
|
+
const repoArtifacts = spec.adapters.flatMap((adapter) => {
|
|
3746
4254
|
const port = spec.ports.find((p) => p.name === adapter.port);
|
|
3747
4255
|
if (port === void 0) return [];
|
|
3748
|
-
|
|
3749
|
-
|
|
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 [];
|
|
3750
4266
|
});
|
|
3751
4267
|
artifacts.push(...repoArtifacts);
|
|
3752
4268
|
return artifacts.sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
|
|
3753
4269
|
}
|
|
3754
|
-
function createContextDrizzleTableDefinition(contextSpec) {
|
|
4270
|
+
function createContextDrizzleTableDefinition(contextSpec, persistence = "postgres") {
|
|
3755
4271
|
const blocks = [];
|
|
3756
4272
|
const emittedTableNames = /* @__PURE__ */ new Set();
|
|
3757
4273
|
let usesInteger = false;
|
|
3758
4274
|
let usesReal = false;
|
|
3759
4275
|
let usesBoolean = false;
|
|
3760
|
-
let
|
|
4276
|
+
let usesJson = false;
|
|
3761
4277
|
for (const agg of contextSpec.aggregates) {
|
|
3762
4278
|
const fields = filterManagedAggregateFields(agg.fields);
|
|
3763
4279
|
if (!fields || fields.length === 0) continue;
|
|
@@ -3766,8 +4282,8 @@ function createContextDrizzleTableDefinition(contextSpec) {
|
|
|
3766
4282
|
if (aggScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
|
|
3767
4283
|
if (aggScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
|
|
3768
4284
|
if (aggScalarFields.some((field) => field.type === "boolean")) usesBoolean = true;
|
|
3769
|
-
if (aggScalarFields.some((field) => field.array))
|
|
3770
|
-
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));
|
|
3771
4287
|
emittedTableNames.add(agg.tableName);
|
|
3772
4288
|
walkEntityTree({
|
|
3773
4289
|
name: agg.name,
|
|
@@ -3782,12 +4298,12 @@ function createContextDrizzleTableDefinition(contextSpec) {
|
|
|
3782
4298
|
if (entityScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
|
|
3783
4299
|
if (entityScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
|
|
3784
4300
|
if (entityScalarFields.some((field) => field.type === "boolean")) usesBoolean = true;
|
|
3785
|
-
if (entityScalarFields.some((field) => field.array))
|
|
4301
|
+
if (entityScalarFields.some((field) => field.array)) usesJson = true;
|
|
3786
4302
|
const fkColName = snakeCase("idField" in ctx.parent ? ctx.parent.idField : agg.idField);
|
|
3787
4303
|
blocks.push(buildTableBlock(entity.tableName, `${camelCase(entity.name)}Table`, entityScalarFields, entity.idField, {
|
|
3788
4304
|
name: fkColName,
|
|
3789
4305
|
type: "text"
|
|
3790
|
-
}));
|
|
4306
|
+
}, false, persistence));
|
|
3791
4307
|
emittedTableNames.add(entity.tableName);
|
|
3792
4308
|
});
|
|
3793
4309
|
}
|
|
@@ -3797,21 +4313,22 @@ function createContextDrizzleTableDefinition(contextSpec) {
|
|
|
3797
4313
|
if (entityScalarFields.some((field) => field.type === "number" && !isDecimalNumberField(field))) usesInteger = true;
|
|
3798
4314
|
if (entityScalarFields.some((field) => isDecimalNumberField(field))) usesReal = true;
|
|
3799
4315
|
if (entityScalarFields.some((field) => field.type === "boolean")) usesBoolean = true;
|
|
3800
|
-
if (entityScalarFields.some((field) => field.array))
|
|
3801
|
-
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));
|
|
3802
4318
|
emittedTableNames.add(entity.tableName);
|
|
3803
4319
|
}
|
|
3804
4320
|
if (blocks.length === 0) return null;
|
|
3805
|
-
const
|
|
3806
|
-
if (usesInteger)
|
|
3807
|
-
if (usesReal)
|
|
3808
|
-
if (usesBoolean)
|
|
3809
|
-
if (
|
|
3810
|
-
return `import { ${
|
|
3811
|
-
}
|
|
3812
|
-
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";
|
|
3813
4330
|
const importTokens = new Set([
|
|
3814
|
-
|
|
4331
|
+
tableBuilder,
|
|
3815
4332
|
"primaryKey",
|
|
3816
4333
|
"text"
|
|
3817
4334
|
]);
|
|
@@ -3819,8 +4336,8 @@ function createReadModelTableDefinition(readModel) {
|
|
|
3819
4336
|
if (field.type === "number" && !isDecimalNumberField(field)) importTokens.add("integer");
|
|
3820
4337
|
if (isDecimalNumberField(field)) importTokens.add("real");
|
|
3821
4338
|
if (field.type === "boolean") importTokens.add("boolean");
|
|
3822
|
-
if (field.array || field.type === "object" && field.nestedFields && field.nestedFields.length > 0) importTokens.add("jsonb");
|
|
3823
|
-
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)},`;
|
|
3824
4341
|
});
|
|
3825
4342
|
if (readModel.indexes.length > 0) importTokens.add("index");
|
|
3826
4343
|
if (readModel.uniqueConstraints.length > 0) importTokens.add("uniqueIndex");
|
|
@@ -3829,9 +4346,9 @@ function createReadModelTableDefinition(readModel) {
|
|
|
3829
4346
|
for (const uniqueConstraint of readModel.uniqueConstraints) constraintLines.push(` ${camelCase(uniqueConstraint.name)}: uniqueIndex("${uniqueConstraint.name}").on(${uniqueConstraint.fields.map((field) => `table.${snakeCase(field)}`).join(", ")}),`);
|
|
3830
4347
|
const tableConstName = readModelTableConstName(readModel.name);
|
|
3831
4348
|
const typeName = pascalCase(readModel.name);
|
|
3832
|
-
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"}";
|
|
3833
4350
|
|
|
3834
|
-
export const ${tableConstName} =
|
|
4351
|
+
export const ${tableConstName} = ${tableBuilder}("${readModel.tableName}", {
|
|
3835
4352
|
${columnLines.join("\n")}
|
|
3836
4353
|
}, (table) => ({
|
|
3837
4354
|
${constraintLines.join("\n")}
|
|
@@ -3876,22 +4393,139 @@ function buildReadModelCompositionArtifacts(contextSpecs) {
|
|
|
3876
4393
|
};
|
|
3877
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)];
|
|
3878
4395
|
}
|
|
3879
|
-
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 = {}) {
|
|
3880
4503
|
const modulePath = normalizeModulePath(contextSpec.context.modulePath);
|
|
3881
4504
|
const scopeKey = sliceArtifactOwnership(modulePath);
|
|
3882
4505
|
const artifacts = [];
|
|
3883
4506
|
const readModels = contextSpec.readModels ?? [];
|
|
3884
|
-
const tableContent = createContextDrizzleTableDefinition(contextSpec);
|
|
4507
|
+
const tableContent = createContextDrizzleTableDefinition(contextSpec, resolveDrizzlePersistence(options.infrastructureStrategy));
|
|
3885
4508
|
if (tableContent !== null) artifacts.push(createGeneratedArtifact(`infrastructure/persistence/${modulePath}/tables.ts`, tableContent, scopeKey));
|
|
3886
4509
|
if (readModels.length > 0) {
|
|
3887
|
-
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));
|
|
3888
4511
|
artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/drizzle/schema.ts`, createReadModelContextSchemaBarrel({
|
|
3889
4512
|
...contextSpec,
|
|
3890
4513
|
readModels
|
|
3891
4514
|
}), scopeKey));
|
|
3892
4515
|
artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/tables.ts`, createReadModelCompatibilityBarrel(), scopeKey));
|
|
3893
4516
|
} else artifacts.push(createGeneratedArtifact(`infrastructure/view-models/${modulePath}/tables.ts`, createViewModelTablesSkeleton(contextSpec), scopeKey));
|
|
3894
|
-
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
|
+
}
|
|
3895
4529
|
return artifacts;
|
|
3896
4530
|
}
|
|
3897
4531
|
function createViewModelTablesSkeleton(contextSpec) {
|
|
@@ -3906,7 +4540,7 @@ function createViewModelTablesSkeleton(contextSpec) {
|
|
|
3906
4540
|
for (const { readModelName, queries } of groupQueriesByReadModel(contextSpec.queries)) {
|
|
3907
4541
|
lines.push(` ${camelCase(readModelName)}: {`);
|
|
3908
4542
|
lines.push(` readModel: "${readModelName}",`);
|
|
3909
|
-
lines.push(` contract: "${readModelContractName$
|
|
4543
|
+
lines.push(` contract: "${readModelContractName$1(readModelName)}",`);
|
|
3910
4544
|
lines.push(` servesQueries: [${queries.map((query) => `"${query.name}"`).join(", ")}],`);
|
|
3911
4545
|
lines.push(` suggestedTableName: "${snakeCase(modulePath).replaceAll("/", "_")}_${snakeCase(readModelRepositoryFileBase$1(readModelName))}",`);
|
|
3912
4546
|
lines.push(` },`);
|
|
@@ -3915,40 +4549,33 @@ function createViewModelTablesSkeleton(contextSpec) {
|
|
|
3915
4549
|
lines.push(``);
|
|
3916
4550
|
return lines.join("\n");
|
|
3917
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
|
+
}
|
|
3918
4562
|
function createReadModelRepositorySkeleton(modulePath, readModelName, queries) {
|
|
3919
4563
|
const className = readModelRepositoryClassName(readModelName);
|
|
3920
|
-
const portTypeName = `${readModelContractName$
|
|
4564
|
+
const portTypeName = `${readModelContractName$1(readModelName)}RepositoryPort`;
|
|
3921
4565
|
const fileBase = readModelRepositoryFileBase$1(readModelName);
|
|
3922
4566
|
const importLines = /* @__PURE__ */ new Map();
|
|
3923
4567
|
let needsPaginatedResult = false;
|
|
3924
4568
|
importLines.set("tx", `import type { Transaction } from "../../../../../lib/transaction.ts";`);
|
|
3925
4569
|
importLines.set("port", `import type { ${portTypeName} } from "../../../../../core/contexts/${modulePath}/application/ports/read-models/${fileBase}.repository.port.ts";`);
|
|
3926
4570
|
const methodBlocks = queries.slice().sort((left, right) => left.name.localeCompare(right.name)).map((query) => {
|
|
3927
|
-
|
|
4571
|
+
queryReadModelMethodName(query);
|
|
3928
4572
|
const queryTypeName = `${pascalCase(query.name)}Query`;
|
|
3929
|
-
const outputTypeName = queryOutputContractName
|
|
3930
|
-
const viewFileBase = queryViewFileBase
|
|
4573
|
+
const outputTypeName = queryOutputContractName(query);
|
|
4574
|
+
const viewFileBase = queryViewFileBase(query);
|
|
3931
4575
|
importLines.set(`query:${queryTypeName}`, `import type { ${queryTypeName} } from "../../../../../core/contexts/${modulePath}/application/queries/${kebabCase(query.name)}.query.ts";`);
|
|
3932
4576
|
importLines.set(`view:${outputTypeName}`, `import type { ${outputTypeName} } from "../../../../../core/contexts/${modulePath}/application/contracts/${viewFileBase}.view.ts";`);
|
|
3933
|
-
if (query.queryKind === "list")
|
|
3934
|
-
|
|
3935
|
-
return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<PaginatedResult<${outputTypeName}>> {
|
|
3936
|
-
void this.db;
|
|
3937
|
-
void tx;
|
|
3938
|
-
const { page = 1, pageSize = 20 } = query.payload;
|
|
3939
|
-
return { items: [], total: 0, page, pageSize };
|
|
3940
|
-
}`;
|
|
3941
|
-
}
|
|
3942
|
-
return ` async ${methodName}(query: ${queryTypeName}, tx: Transaction): Promise<${outputTypeName}> {
|
|
3943
|
-
void this.db;
|
|
3944
|
-
void query;
|
|
3945
|
-
void tx;
|
|
3946
|
-
return {
|
|
3947
|
-
${query.outputFields.map((field) => {
|
|
3948
|
-
return ` ${camelCase(field.name)}: ${placeholderValueForField(field)}, // TODO: map from projection row`;
|
|
3949
|
-
}).join("\n")}
|
|
3950
|
-
};
|
|
3951
|
-
}`;
|
|
4577
|
+
if (query.queryKind === "list") needsPaginatedResult = true;
|
|
4578
|
+
return createSingleReadModelMethodSkeleton(query, readModelName);
|
|
3952
4579
|
});
|
|
3953
4580
|
if (needsPaginatedResult) importLines.set("pagination", `import type { PaginatedResult } from "../../../../../lib/pagination.ts";`);
|
|
3954
4581
|
return `${[...importLines.values()].join("\n")}
|
|
@@ -3961,76 +4588,273 @@ ${methodBlocks.join("\n\n")}
|
|
|
3961
4588
|
}
|
|
3962
4589
|
`;
|
|
3963
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")}
|
|
3964
4625
|
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
}
|
|
3971
|
-
function readModelRepositoryPortName(readModelName) {
|
|
3972
|
-
return `${readModelContractName$1(readModelName)}RepositoryPort`;
|
|
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")}
|
|
3973
4631
|
}
|
|
3974
|
-
|
|
3975
|
-
return kebabCase(readModelName).replace(/-view$/, "");
|
|
4632
|
+
`;
|
|
3976
4633
|
}
|
|
3977
|
-
function
|
|
3978
|
-
|
|
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;
|
|
3979
4661
|
}
|
|
3980
|
-
function
|
|
3981
|
-
|
|
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
|
+
}
|
|
4806
|
+
|
|
4807
|
+
//#endregion
|
|
4808
|
+
//#region packages/core/generators/projections.ts
|
|
4809
|
+
function projectionWritePortName(projectionName) {
|
|
4810
|
+
return `${pascalCase(projectionName)}WritePort`;
|
|
4811
|
+
}
|
|
4812
|
+
function projectionWritePortVariableName(projectionName) {
|
|
4813
|
+
return camelCase(projectionWritePortName(projectionName));
|
|
3982
4814
|
}
|
|
3983
|
-
function
|
|
3984
|
-
return
|
|
4815
|
+
function projectionFileBase(projectionName) {
|
|
4816
|
+
return kebabCase(projectionName);
|
|
4817
|
+
}
|
|
4818
|
+
function projectionVariableName(projectionName) {
|
|
4819
|
+
return camelCase(projectionName);
|
|
3985
4820
|
}
|
|
3986
4821
|
function projectionSourceHandlerName(eventName) {
|
|
3987
4822
|
return `on${pascalCase(eventName)}`;
|
|
3988
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
|
+
}
|
|
3989
4834
|
function buildProjectionSourceMethod(projection, source) {
|
|
3990
4835
|
const methodName = projectionSourceHandlerName(source.eventName);
|
|
3991
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.`;
|
|
3992
|
-
|
|
3993
|
-
return await this.${source.mutation.handlerName}(event, tx);
|
|
3994
|
-
}
|
|
3995
|
-
|
|
3996
|
-
protected async ${source.mutation.handlerName}(event: unknown, tx: Transaction): Promise<void> {
|
|
3997
|
-
void this.${readModelRepositoryVariableName(projection.readModelName)};
|
|
3998
|
-
void event;
|
|
3999
|
-
void tx;
|
|
4000
|
-
${roleComment}
|
|
4001
|
-
// Custom projection hook "${source.mutation.handlerName}" for ${source.contextName}.${source.aggregateName}.${source.eventName}.
|
|
4002
|
-
// TODO: implement custom projector logic against the generated read-model repository port.
|
|
4003
|
-
}`;
|
|
4004
|
-
const setFields = source.mutation.kind === "delete" ? "" : Object.entries(source.mutation.set ?? {}).map(([fieldName, valueSource]) => {
|
|
4005
|
-
if (valueSource.kind === "event-field") return ` // ${fieldName} <- event.${valueSource.field}`;
|
|
4006
|
-
if (valueSource.kind === "literal") return ` // ${fieldName} <- ${JSON.stringify(valueSource.value)}`;
|
|
4007
|
-
return ` // ${fieldName} <- ${valueSource.expression}`;
|
|
4008
|
-
}).join("\n");
|
|
4009
|
-
const matchOnComment = `"${source.mutation.matchOn.join("\", \"")}"`;
|
|
4010
|
-
return ` async ${methodName}(event: unknown, tx: Transaction): Promise<void> {
|
|
4011
|
-
void this.${readModelRepositoryVariableName(projection.readModelName)};
|
|
4012
|
-
void event;
|
|
4013
|
-
void tx;
|
|
4837
|
+
return ` async ${methodName}(event: ${projectionSourceEventType(source)}, tx: Transaction): Promise<void> {
|
|
4014
4838
|
${roleComment}
|
|
4015
|
-
|
|
4016
|
-
// Match on: [${matchOnComment}]
|
|
4017
|
-
${setFields}
|
|
4018
|
-
// TODO: call generated projector-facing repository methods when write helpers land.
|
|
4839
|
+
return await this.${projectionWritePortVariableName(projection.name)}.${methodName}(event, tx);
|
|
4019
4840
|
}`;
|
|
4020
4841
|
}
|
|
4021
4842
|
function buildProjectorArtifact(modulePath, projection) {
|
|
4022
4843
|
const projectorClassName = projection.name;
|
|
4023
|
-
const
|
|
4024
|
-
const
|
|
4844
|
+
const writePortName = projectionWritePortName(projection.name);
|
|
4845
|
+
const writePortVar = projectionWritePortVariableName(projection.name);
|
|
4025
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)}";`);
|
|
4026
4848
|
const methods = projection.sources.slice().sort((left, right) => left.eventName.localeCompare(right.eventName)).map((source) => buildProjectionSourceMethod(projection, source)).join("\n\n");
|
|
4027
|
-
return createGeneratedArtifact(`core/contexts/${modulePath}/application/projections/${fileBase}.projector.ts`, `import type {
|
|
4028
|
-
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")}
|
|
4029
4853
|
|
|
4030
4854
|
// Auto-generated projector skeleton for the "${projection.readModelName}" read model.
|
|
4031
4855
|
export class ${projectorClassName} {
|
|
4032
4856
|
constructor(
|
|
4033
|
-
private readonly ${
|
|
4857
|
+
private readonly ${writePortVar}: ${writePortName},
|
|
4034
4858
|
) {}
|
|
4035
4859
|
|
|
4036
4860
|
${methods}
|
|
@@ -4043,20 +4867,53 @@ function buildProjectionRebuildArtifact(modulePath, projection) {
|
|
|
4043
4867
|
const projectorClassName = projection.name;
|
|
4044
4868
|
const rebuildFunctionName = `rebuild${pascalCase(projection.name)}`;
|
|
4045
4869
|
const batchSize = projection.rebuild.batchSize ?? 500;
|
|
4046
|
-
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";
|
|
4047
4871
|
import type { ${projectorClassName} } from "./${fileBase}.projector.ts";
|
|
4048
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
|
+
|
|
4049
4889
|
export type ${pascalCase(projection.name)}RebuildDeps = {
|
|
4050
4890
|
projector: ${projectorClassName};
|
|
4891
|
+
eventSource: ProjectionRebuildEventSource;
|
|
4051
4892
|
transaction: Transaction;
|
|
4052
4893
|
};
|
|
4053
4894
|
|
|
4054
|
-
// Auto-generated rebuild
|
|
4895
|
+
// Auto-generated rebuild runner for the "${projection.readModelName}" read model.
|
|
4055
4896
|
export async function ${rebuildFunctionName}(deps: ${pascalCase(projection.name)}RebuildDeps): Promise<void> {
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
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
|
+
}
|
|
4060
4917
|
}
|
|
4061
4918
|
`, contextArtifactOwnership(modulePath));
|
|
4062
4919
|
}
|
|
@@ -4074,7 +4931,8 @@ function buildProjectionSubscriptionsArtifact(contextSpec) {
|
|
|
4074
4931
|
const depsTypeName = `${pascalCase(contextSpec.context.name)}ProjectionSubscriptionDeps`;
|
|
4075
4932
|
const builderName = `build${pascalCase(contextSpec.context.name)}ProjectionSubscriptions`;
|
|
4076
4933
|
const dependencyLines = contextSpec.projections.map((projection) => ` ${projectionVariableName(projection.name)}: ${projection.name};`);
|
|
4077
|
-
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";`));
|
|
4078
4936
|
const subscriptionEntries = contextSpec.projections.flatMap((projection) => projection.sources.map((source) => {
|
|
4079
4937
|
const subscriptionName = projection.subscription?.subscriptionName ?? projection.name;
|
|
4080
4938
|
const consumerGroup = projection.subscription?.consumerGroup;
|
|
@@ -4084,11 +4942,13 @@ function buildProjectionSubscriptionsArtifact(contextSpec) {
|
|
|
4084
4942
|
subscriptionName: "${subscriptionName}",
|
|
4085
4943
|
eventName: "${source.contextName}.${source.aggregateName}.${source.eventName}",
|
|
4086
4944
|
role: "${source.role}",${consumerGroupLine}
|
|
4087
|
-
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),
|
|
4088
4946
|
}`;
|
|
4089
4947
|
}));
|
|
4090
|
-
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";
|
|
4091
4950
|
${importLines.join("\n")}
|
|
4951
|
+
${[...new Set(eventImports)].join("\n")}
|
|
4092
4952
|
|
|
4093
4953
|
export type ProjectionSubscription = {
|
|
4094
4954
|
projectionName: string;
|
|
@@ -4096,7 +4956,7 @@ export type ProjectionSubscription = {
|
|
|
4096
4956
|
eventName: string;
|
|
4097
4957
|
role: "primary" | "enrichment";
|
|
4098
4958
|
consumerGroup?: string;
|
|
4099
|
-
handle: (event: unknown
|
|
4959
|
+
handle: (event: EventEnvelope<unknown>, tx: Transaction) => Promise<void>;
|
|
4100
4960
|
};
|
|
4101
4961
|
|
|
4102
4962
|
export type ${depsTypeName} = {
|
|
@@ -4372,6 +5232,34 @@ export class EventCollector {
|
|
|
4372
5232
|
}
|
|
4373
5233
|
`;
|
|
4374
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
|
+
}
|
|
4375
5263
|
function generateDomainErrors() {
|
|
4376
5264
|
return `export type { DomainError } from '../../../lib/domain-error.ts';
|
|
4377
5265
|
export type { ApplicationError } from '../../../lib/application-error.ts';
|
|
@@ -4452,6 +5340,7 @@ function buildV5SharedKernelArtifacts(spec) {
|
|
|
4452
5340
|
const base = [
|
|
4453
5341
|
createGeneratedArtifact("core/shared-kernel/result.ts", generateResultReExport(), ownership),
|
|
4454
5342
|
createGeneratedArtifact("core/shared-kernel/events/event-collector.ts", generateEventCollector(), ownership),
|
|
5343
|
+
createGeneratedArtifact("core/shared-kernel/events/aggregate-event-tracker.ts", generateAggregateEventTracker(), ownership),
|
|
4455
5344
|
createGeneratedArtifact("core/shared-kernel/events/event-context.ts", generateEventContext(), ownership),
|
|
4456
5345
|
createGeneratedArtifact("core/shared-kernel/errors/domain-errors.ts", generateDomainErrors(), ownership),
|
|
4457
5346
|
createGeneratedArtifact("core/shared-kernel/trpc-error-mapper.ts", generateTrpcErrorMapper(), ownership),
|
|
@@ -4910,7 +5799,11 @@ function createApplicationError() {
|
|
|
4910
5799
|
}
|
|
4911
5800
|
`;
|
|
4912
5801
|
}
|
|
4913
|
-
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
|
+
`;
|
|
4914
5807
|
return `import type { PgTransaction } from 'drizzle-orm/pg-core';
|
|
4915
5808
|
|
|
4916
5809
|
export type Transaction = PgTransaction<any, any, any>;
|
|
@@ -5004,7 +5897,7 @@ export type ListResult<T> = {
|
|
|
5004
5897
|
};
|
|
5005
5898
|
`;
|
|
5006
5899
|
}
|
|
5007
|
-
function buildV5LibArtifacts() {
|
|
5900
|
+
function buildV5LibArtifacts(infrastructureStrategy) {
|
|
5008
5901
|
const ownership = sharedArtifactOwnership("lib");
|
|
5009
5902
|
return [
|
|
5010
5903
|
createGeneratedArtifact("lib/branded-id.ts", createBrandedId(), ownership),
|
|
@@ -5016,7 +5909,7 @@ function buildV5LibArtifacts() {
|
|
|
5016
5909
|
createGeneratedArtifact("lib/result.ts", createResult(), ownership),
|
|
5017
5910
|
createGeneratedArtifact("lib/domain-error.ts", createDomainError(), ownership),
|
|
5018
5911
|
createGeneratedArtifact("lib/application-error.ts", createApplicationError(), ownership),
|
|
5019
|
-
createGeneratedArtifact("lib/transaction.ts", createTransaction(), ownership),
|
|
5912
|
+
createGeneratedArtifact("lib/transaction.ts", createTransaction(infrastructureStrategy), ownership),
|
|
5020
5913
|
createGeneratedArtifact("lib/concurrency-conflict-error.ts", createConcurrencyConflictError(), ownership),
|
|
5021
5914
|
createGeneratedArtifact("lib/pagination.ts", createPagination(), ownership)
|
|
5022
5915
|
];
|
|
@@ -5126,26 +6019,29 @@ function buildConsumerAclArtifacts(contextSpec) {
|
|
|
5126
6019
|
|
|
5127
6020
|
//#endregion
|
|
5128
6021
|
//#region packages/core/orchestrator.ts
|
|
5129
|
-
function generateContextArtifacts(contextSpec) {
|
|
6022
|
+
function generateContextArtifacts(contextSpec, options = {}) {
|
|
5130
6023
|
const perAggregateArtifacts = sliceContextIntoAggregateViews(contextSpec).flatMap((view) => {
|
|
5131
6024
|
const ownership = sliceArtifactOwnership(`${view.context.modulePath}/${kebabCase(view.aggregate.name)}`);
|
|
5132
6025
|
return [
|
|
5133
6026
|
...applyOwnershipIfMissing(buildV5DomainArtifacts(view), ownership),
|
|
5134
6027
|
...applyOwnershipIfMissing(buildV5ApplicationArtifacts(view, { skipContextWideArtifacts: true }), ownership),
|
|
5135
|
-
...buildV5InfrastructureArtifacts(view, {
|
|
6028
|
+
...buildV5InfrastructureArtifacts(view, {
|
|
6029
|
+
skipContextWideArtifacts: true,
|
|
6030
|
+
infrastructureStrategy: options.infrastructureStrategy
|
|
6031
|
+
}),
|
|
5136
6032
|
...applyOwnershipIfMissing(buildV5TestArtifacts(view), ownership)
|
|
5137
6033
|
];
|
|
5138
6034
|
});
|
|
5139
6035
|
const ctxOwnership = contextArtifactOwnership(contextSpec.context.modulePath);
|
|
5140
6036
|
const infraOwnership = sliceArtifactOwnership(contextSpec.context.modulePath);
|
|
5141
6037
|
const perContextArtifacts = [
|
|
5142
|
-
...buildV5LibArtifacts(),
|
|
6038
|
+
...buildV5LibArtifacts(options.infrastructureStrategy),
|
|
5143
6039
|
...buildV5SharedKernelArtifacts(contextSpec),
|
|
5144
6040
|
...buildV5PortArtifacts(),
|
|
5145
6041
|
...applyOwnershipIfMissing(buildV5ApplicationContextArtifacts(contextSpec), ctxOwnership),
|
|
5146
6042
|
...applyOwnershipIfMissing(buildConsumerAclArtifacts(contextSpec), ctxOwnership),
|
|
5147
6043
|
...applyOwnershipIfMissing(buildProjectionArtifacts(contextSpec), ctxOwnership),
|
|
5148
|
-
...applyOwnershipIfMissing(buildV5InfrastructureContextArtifacts(contextSpec), infraOwnership),
|
|
6044
|
+
...applyOwnershipIfMissing(buildV5InfrastructureContextArtifacts(contextSpec, { infrastructureStrategy: options.infrastructureStrategy }), infraOwnership),
|
|
5149
6045
|
...applyOwnershipIfMissing(buildV5PresentationArtifacts(contextSpec), ctxOwnership),
|
|
5150
6046
|
...applyOwnershipIfMissing(buildV5RouteArtifacts(contextSpec), ctxOwnership)
|
|
5151
6047
|
];
|
|
@@ -5752,6 +6648,7 @@ function createIntegrationEvent() {
|
|
|
5752
6648
|
|
|
5753
6649
|
export interface IntegrationEvent<TPayload> {
|
|
5754
6650
|
readonly type: string;
|
|
6651
|
+
readonly version: number;
|
|
5755
6652
|
readonly payload: TPayload;
|
|
5756
6653
|
readonly eventContext: EventContext;
|
|
5757
6654
|
}
|
|
@@ -5787,6 +6684,20 @@ export class DrizzleTransactionManager implements TransactionManager {
|
|
|
5787
6684
|
}
|
|
5788
6685
|
`;
|
|
5789
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
|
+
}
|
|
5790
6701
|
function createTransactionAbortError() {
|
|
5791
6702
|
return `export class TransactionAbortError extends Error {
|
|
5792
6703
|
constructor(public readonly innerError: unknown) {
|
|
@@ -5797,11 +6708,13 @@ function createTransactionAbortError() {
|
|
|
5797
6708
|
`;
|
|
5798
6709
|
}
|
|
5799
6710
|
function createHandlerDeps() {
|
|
5800
|
-
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";
|
|
5801
6713
|
|
|
5802
6714
|
export type HandlerDeps = {
|
|
5803
6715
|
readonly tx: unknown;
|
|
5804
6716
|
readonly eventCollector: EventCollector;
|
|
6717
|
+
readonly aggregateEventTracker: AggregateEventTracker;
|
|
5805
6718
|
readonly repos: Record<string, unknown>;
|
|
5806
6719
|
readonly acls: Record<string, unknown>;
|
|
5807
6720
|
};
|
|
@@ -5813,6 +6726,7 @@ function buildCompositionTypeArtifacts() {
|
|
|
5813
6726
|
createGeneratedArtifact("infrastructure/messaging/integration-event.ts", createIntegrationEvent(), ownership),
|
|
5814
6727
|
createGeneratedArtifact("infrastructure/unit-of-work/transaction-manager.ts", createTransactionManager(), ownership),
|
|
5815
6728
|
createGeneratedArtifact("infrastructure/unit-of-work/drizzle-transaction-manager.ts", createDrizzleTransactionManager(), ownership),
|
|
6729
|
+
createGeneratedArtifact("infrastructure/unit-of-work/mikroorm-transaction-manager.ts", createMikroOrmTransactionManager(), ownership),
|
|
5816
6730
|
createGeneratedArtifact("infrastructure/unit-of-work/transaction-abort-error.ts", createTransactionAbortError(), ownership),
|
|
5817
6731
|
createGeneratedArtifact("infrastructure/unit-of-work/handler-deps.ts", createHandlerDeps(), ownership)
|
|
5818
6732
|
].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
|
|
@@ -5825,6 +6739,7 @@ function createCommandBusImpl() {
|
|
|
5825
6739
|
import type { TransactionManager } from "../unit-of-work/transaction-manager.ts";
|
|
5826
6740
|
import { TransactionAbortError } from "../unit-of-work/transaction-abort-error.ts";
|
|
5827
6741
|
import type { HandlerDeps } from "../unit-of-work/handler-deps.ts";
|
|
6742
|
+
import { AggregateEventTracker } from "../../core/shared-kernel/events/aggregate-event-tracker.ts";
|
|
5828
6743
|
import { EventCollector } from "../../core/shared-kernel/events/event-collector.ts";
|
|
5829
6744
|
import type { CommandEnvelope } from "../../core/shared-kernel/commands/command-envelope.ts";
|
|
5830
6745
|
import type { EventContext } from "../../core/shared-kernel/events/event-context.ts";
|
|
@@ -5836,6 +6751,9 @@ import type { EventEnvelope } from "../../lib/event-envelope.ts";
|
|
|
5836
6751
|
type OutboxWriter = {
|
|
5837
6752
|
append(tx: unknown, events: EventEnvelope<unknown>[]): Promise<void>;
|
|
5838
6753
|
};
|
|
6754
|
+
type OutboxRelay = {
|
|
6755
|
+
afterCommit(): void;
|
|
6756
|
+
};
|
|
5839
6757
|
type RepositoryFactory = {
|
|
5840
6758
|
create(tx: unknown): Record<string, unknown>;
|
|
5841
6759
|
};
|
|
@@ -5855,6 +6773,8 @@ type InternalHandlerFn = (
|
|
|
5855
6773
|
) => Promise<Result<unknown, unknown>>;
|
|
5856
6774
|
|
|
5857
6775
|
export class CommandBusImpl implements CommandBusPort {
|
|
6776
|
+
private relay: OutboxRelay | undefined;
|
|
6777
|
+
|
|
5858
6778
|
constructor(
|
|
5859
6779
|
private readonly txManager: TransactionManager,
|
|
5860
6780
|
private readonly repoFactory: RepositoryFactory,
|
|
@@ -5862,6 +6782,10 @@ export class CommandBusImpl implements CommandBusPort {
|
|
|
5862
6782
|
private readonly outboxWriter: OutboxWriter,
|
|
5863
6783
|
) {}
|
|
5864
6784
|
|
|
6785
|
+
setRelay(relay: OutboxRelay): void {
|
|
6786
|
+
this.relay = relay;
|
|
6787
|
+
}
|
|
6788
|
+
|
|
5865
6789
|
// Internal handler type: receives deps injected by withUnitOfWork.
|
|
5866
6790
|
// This is different from TestCommandBus which registers pre-bound
|
|
5867
6791
|
// handlers with signature (command, eventContext) — deps closed over
|
|
@@ -5889,9 +6813,11 @@ export class CommandBusImpl implements CommandBusPort {
|
|
|
5889
6813
|
): Promise<TResult> {
|
|
5890
6814
|
const { handler, contextKey: commandContext } = this.resolveHandler(command.type);
|
|
5891
6815
|
try {
|
|
5892
|
-
|
|
6816
|
+
const result = await this.withUnitOfWork(commandContext, (deps) =>
|
|
5893
6817
|
handler(command, deps, eventContext)
|
|
5894
6818
|
) as TResult;
|
|
6819
|
+
this.relay?.afterCommit();
|
|
6820
|
+
return result;
|
|
5895
6821
|
} catch (error) {
|
|
5896
6822
|
if (error instanceof TransactionAbortError) {
|
|
5897
6823
|
// Reconstitute the failure Result that triggered rollback.
|
|
@@ -5922,16 +6848,24 @@ export class CommandBusImpl implements CommandBusPort {
|
|
|
5922
6848
|
work: (deps: HandlerDeps) => Promise<Result<T, unknown>>
|
|
5923
6849
|
): Promise<Result<T, unknown>> {
|
|
5924
6850
|
return this.txManager.withTransaction(async (tx) => {
|
|
6851
|
+
const aggregateEventTracker = new AggregateEventTracker();
|
|
5925
6852
|
const eventCollector = new EventCollector();
|
|
5926
6853
|
const repos = this.repoFactory.create(tx);
|
|
5927
6854
|
const acls = this.aclFactory.create(tx, commandContext);
|
|
5928
6855
|
|
|
5929
|
-
const result = await work({
|
|
6856
|
+
const result = await work({
|
|
6857
|
+
tx,
|
|
6858
|
+
eventCollector,
|
|
6859
|
+
aggregateEventTracker,
|
|
6860
|
+
repos,
|
|
6861
|
+
acls,
|
|
6862
|
+
});
|
|
5930
6863
|
|
|
5931
6864
|
if (!result.ok) {
|
|
5932
6865
|
throw new TransactionAbortError(result.error);
|
|
5933
6866
|
}
|
|
5934
6867
|
|
|
6868
|
+
aggregateEventTracker.releaseInto(eventCollector);
|
|
5935
6869
|
const events = eventCollector.drain();
|
|
5936
6870
|
|
|
5937
6871
|
if (events.length > 0) {
|
|
@@ -5976,16 +6910,38 @@ function buildCompositionBusArtifacts() {
|
|
|
5976
6910
|
|
|
5977
6911
|
//#endregion
|
|
5978
6912
|
//#region packages/core/generators/composition_outbox.ts
|
|
5979
|
-
function createOutboxTable() {
|
|
5980
|
-
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";
|
|
5981
6932
|
|
|
5982
6933
|
export const outboxEvents = pgTable("outbox_events", {
|
|
5983
6934
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
5984
6935
|
eventType: varchar("event_type", { length: 255 }).notNull(),
|
|
6936
|
+
eventVersion: integer("event_version").notNull(),
|
|
5985
6937
|
payload: jsonb("payload").notNull(),
|
|
5986
6938
|
metadata: jsonb("metadata").notNull(),
|
|
5987
6939
|
occurredAt: timestamp("occurred_at").notNull().defaultNow(),
|
|
5988
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"),
|
|
5989
6945
|
});
|
|
5990
6946
|
`;
|
|
5991
6947
|
}
|
|
@@ -6001,6 +6957,7 @@ export class OutboxWriter {
|
|
|
6001
6957
|
for (const event of events) {
|
|
6002
6958
|
await db.insert(outboxEvents).values({
|
|
6003
6959
|
eventType: event.eventType,
|
|
6960
|
+
eventVersion: event.eventVersion,
|
|
6004
6961
|
payload: event.payload,
|
|
6005
6962
|
metadata: {
|
|
6006
6963
|
correlationId: event.correlationId,
|
|
@@ -6015,9 +6972,221 @@ export class OutboxWriter {
|
|
|
6015
6972
|
}
|
|
6016
6973
|
`;
|
|
6017
6974
|
}
|
|
6018
|
-
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) {
|
|
6019
7182
|
const ownership = sharedArtifactOwnership("composition");
|
|
6020
|
-
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));
|
|
6021
7190
|
}
|
|
6022
7191
|
|
|
6023
7192
|
//#endregion
|
|
@@ -6105,34 +7274,53 @@ function renderMappedType(fields) {
|
|
|
6105
7274
|
${Object.entries(fields).map(([fieldName, expr]) => ` readonly ${camelCase(fieldName)}: ${mappingValueType(expr)};`).join("\n")}
|
|
6106
7275
|
}`;
|
|
6107
7276
|
}
|
|
6108
|
-
function
|
|
6109
|
-
|
|
6110
|
-
|
|
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
|
+
}
|
|
6111
7290
|
}
|
|
6112
|
-
function
|
|
6113
|
-
|
|
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;
|
|
7301
|
+
}
|
|
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}>`;
|
|
6114
7307
|
}
|
|
6115
7308
|
function readResponseFieldType(acl, mapping) {
|
|
6116
7309
|
if (mapping.kind === "const") return mappingValueType({ const: mapping.value });
|
|
6117
|
-
const viewTypeName = sourceQueryViewTypeName(acl);
|
|
6118
7310
|
if (acl.sourceQuery.queryKind === "list") {
|
|
6119
|
-
if (mapping.sourcePath === "result.items") return
|
|
7311
|
+
if (mapping.sourcePath === "result.items") return formatReadAclListItemType(acl.sourceQuery.outputFields);
|
|
6120
7312
|
if (mapping.sourcePath === "result.total" || mapping.sourcePath === "result.page" || mapping.sourcePath === "result.pageSize") return "number";
|
|
6121
7313
|
}
|
|
6122
7314
|
const sourceFieldName = mapping.sourcePath.replace(/^result\./, "");
|
|
6123
7315
|
const sourceField = acl.sourceQuery.outputFields.find((field) => camelCase(field.name) === camelCase(sourceFieldName));
|
|
6124
|
-
return sourceField ?
|
|
7316
|
+
return sourceField ? formatReadAclFieldType(sourceField) : "unknown";
|
|
6125
7317
|
}
|
|
6126
7318
|
function buildReadAclContractContent(acl) {
|
|
6127
7319
|
normalizeModulePath(acl.consumerContext.modulePath);
|
|
6128
|
-
const sourceModulePath = normalizeModulePath(acl.sourceContext.modulePath);
|
|
6129
7320
|
const requestTypeName = aclRequestTypeName(acl.port);
|
|
6130
7321
|
const responseTypeName = aclResponseTypeName(acl.port);
|
|
6131
|
-
const viewTypeName = sourceQueryViewTypeName(acl);
|
|
6132
|
-
const viewFileName = sourceQueryViewFileName(acl);
|
|
6133
7322
|
const responseLines = acl.responseMappings.map((mapping) => ` readonly ${camelCase(mapping.targetPath)}: ${readResponseFieldType(acl, mapping)};`);
|
|
6134
7323
|
return `import type { Transaction } from "../../../../../lib/transaction.ts";
|
|
6135
|
-
import type { ${viewTypeName} } from "../../../${sourceModulePath}/application/contracts/${viewFileName}";
|
|
6136
7324
|
|
|
6137
7325
|
export type ${requestTypeName} = ${renderMappedType(Object.fromEntries(acl.requestMappings.map((mapping) => [mapping.targetPath, mapping.kind === "from" ? { from: mapping.sourcePath } : { const: mapping.value }])))};
|
|
6138
7326
|
|
|
@@ -6421,6 +7609,24 @@ function buildCompositionAclArtifacts(compositionSpec) {
|
|
|
6421
7609
|
].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
|
|
6422
7610
|
}
|
|
6423
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
|
+
|
|
6424
7630
|
//#endregion
|
|
6425
7631
|
//#region packages/core/generators/composition_container.ts
|
|
6426
7632
|
/**
|
|
@@ -6432,6 +7638,8 @@ function buildCompositionContainerArtifacts(compositionSpec, contextSpecs) {
|
|
|
6432
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));
|
|
6433
7639
|
}
|
|
6434
7640
|
function buildContainerContent(compositionSpec, contextSpecs) {
|
|
7641
|
+
assertSupportedContainerStrategy(compositionSpec);
|
|
7642
|
+
const runtime = resolveContainerRuntime(compositionSpec);
|
|
6435
7643
|
const readContexts = uniqueReadContexts(compositionSpec.acls);
|
|
6436
7644
|
const readContextNames = buildCollisionSafeModulePathNames(readContexts.map((context) => context.modulePath));
|
|
6437
7645
|
const commandContextNames = buildCollisionSafeModulePathNames(contextSpecs.map((context) => context.context.modulePath));
|
|
@@ -6441,7 +7649,10 @@ function buildContainerContent(compositionSpec, contextSpecs) {
|
|
|
6441
7649
|
``,
|
|
6442
7650
|
`import { CommandBusImpl } from "../messaging/command-bus.impl.ts";`,
|
|
6443
7651
|
`import { QueryBusImpl } from "../messaging/query-bus.impl.ts";`,
|
|
6444
|
-
`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";`,
|
|
6445
7656
|
`import { OutboxWriter } from "../outbox/outbox-writer.ts";`,
|
|
6446
7657
|
`import { AclFactory } from "./acl-factory.ts";`,
|
|
6447
7658
|
`import { RepositoryFactory } from "./repo-factory.ts";`,
|
|
@@ -6460,7 +7671,8 @@ function buildContainerContent(compositionSpec, contextSpecs) {
|
|
|
6460
7671
|
for (const query of uniqueQueriesByReadModel(contextSpec.queries)) {
|
|
6461
7672
|
const repoClassName = queryReadModelRepositoryClassName(query);
|
|
6462
7673
|
const repoAlias = queryReadModelRepositoryAlias(boundaryPlan.symbolStem, query);
|
|
6463
|
-
|
|
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";`);
|
|
6464
7676
|
}
|
|
6465
7677
|
}
|
|
6466
7678
|
for (const spec of contextSpecs) {
|
|
@@ -6484,15 +7696,16 @@ function buildContainerContent(compositionSpec, contextSpecs) {
|
|
|
6484
7696
|
` readonly commandBus: CommandBusPort;`,
|
|
6485
7697
|
` readonly queryBus: QueryBusPort;`,
|
|
6486
7698
|
` readonly integrationEventSubscriptions: EventSubscription[];`,
|
|
6487
|
-
` readonly
|
|
7699
|
+
` readonly outboxPoller: OutboxPoller;`,
|
|
7700
|
+
...runtime.containerFields.map((field) => ` readonly ${field}: unknown;`),
|
|
6488
7701
|
`};`
|
|
6489
7702
|
];
|
|
6490
7703
|
const createContainerLines = [
|
|
6491
7704
|
``,
|
|
6492
|
-
`export function createContainer(deps: {
|
|
7705
|
+
`export function createContainer(deps: { ${runtime.dependencyFields.map((field) => `${field}: unknown`).join("; ")} }): Container {`,
|
|
6493
7706
|
` const outboxWriter = new OutboxWriter();`,
|
|
6494
7707
|
` const repoFactory = new RepositoryFactory();`,
|
|
6495
|
-
` const txManager = new
|
|
7708
|
+
` const txManager = new ${runtime.transactionManagerClassName}(${runtime.transactionDependencyRef});`
|
|
6496
7709
|
];
|
|
6497
7710
|
for (const context of readContexts) {
|
|
6498
7711
|
const contextSpec = contextSpecs.find((spec) => spec.context.modulePath.toLowerCase() === context.modulePath.toLowerCase());
|
|
@@ -6500,7 +7713,7 @@ function buildContainerContent(compositionSpec, contextSpecs) {
|
|
|
6500
7713
|
const boundaryPlan = readContextNames.get(context.modulePath);
|
|
6501
7714
|
const boundaryVar = `${boundaryPlan.propertyStem}PublishedReadBoundary`;
|
|
6502
7715
|
const boundaryAlias = `create${boundaryPlan.symbolStem}PublishedReadBoundary`;
|
|
6503
|
-
const boundaryDeps = buildPublishedReadBoundaryDeps(contextSpec, boundaryPlan.symbolStem);
|
|
7716
|
+
const boundaryDeps = buildPublishedReadBoundaryDeps(contextSpec, boundaryPlan.symbolStem, runtime.readRepositoryDependencyRef);
|
|
6504
7717
|
createContainerLines.push(` const ${boundaryVar} = ${boundaryAlias}(${boundaryDeps});`);
|
|
6505
7718
|
}
|
|
6506
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);`, ` },`, ` };`);
|
|
@@ -6531,7 +7744,9 @@ function buildContainerContent(compositionSpec, contextSpecs) {
|
|
|
6531
7744
|
createContainerLines.push(` const integrationEventSubscriptions: EventSubscription[] = [];`);
|
|
6532
7745
|
}
|
|
6533
7746
|
createContainerLines.push(``);
|
|
6534
|
-
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(", ")} };`);
|
|
6535
7750
|
createContainerLines.push(`}`);
|
|
6536
7751
|
createContainerLines.push(``);
|
|
6537
7752
|
return [
|
|
@@ -6540,11 +7755,50 @@ function buildContainerContent(compositionSpec, contextSpecs) {
|
|
|
6540
7755
|
...createContainerLines
|
|
6541
7756
|
].join("\n");
|
|
6542
7757
|
}
|
|
6543
|
-
function buildPublishedReadBoundaryDeps(spec, contextSymbolStem) {
|
|
7758
|
+
function buildPublishedReadBoundaryDeps(spec, contextSymbolStem, readRepositoryDependencyRef) {
|
|
6544
7759
|
const entries = /* @__PURE__ */ new Map();
|
|
6545
|
-
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})`);
|
|
6546
7761
|
return `{ ${[...entries.values()].join(", ")} }`;
|
|
6547
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
|
+
}
|
|
6548
7802
|
function resolvedReadModelName(query) {
|
|
6549
7803
|
return query.readSide?.readModelName ?? query.readModelName ?? query.name;
|
|
6550
7804
|
}
|
|
@@ -6563,23 +7817,17 @@ function uniqueQueriesByReadModel(queries) {
|
|
|
6563
7817
|
}
|
|
6564
7818
|
return [...queriesByReadModel.values()].sort((left, right) => resolvedReadModelName(left).localeCompare(resolvedReadModelName(right)));
|
|
6565
7819
|
}
|
|
6566
|
-
function queryOutputContractName(query) {
|
|
6567
|
-
return readModelContractName(resolvedReadModelName(query));
|
|
6568
|
-
}
|
|
6569
|
-
function queryViewFileBase(query) {
|
|
6570
|
-
return readModelRepositoryFileBase(resolvedReadModelName(query));
|
|
6571
|
-
}
|
|
6572
7820
|
function queryReadModelPortTypeName(query) {
|
|
6573
|
-
return `${
|
|
7821
|
+
return `${readModelContractName(resolvedReadModelName(query))}RepositoryPort`;
|
|
6574
7822
|
}
|
|
6575
7823
|
function queryReadModelVariableName(query) {
|
|
6576
7824
|
return camelCase(queryReadModelPortTypeName(query));
|
|
6577
7825
|
}
|
|
6578
7826
|
function queryReadModelRepositoryClassName(query) {
|
|
6579
|
-
return `Drizzle${
|
|
7827
|
+
return `Drizzle${readModelContractName(resolvedReadModelName(query))}Repository`;
|
|
6580
7828
|
}
|
|
6581
7829
|
function queryReadModelRepositoryAlias(contextSymbolStem, query) {
|
|
6582
|
-
return `${contextSymbolStem}${
|
|
7830
|
+
return `${contextSymbolStem}${readModelContractName(resolvedReadModelName(query))}Repository`;
|
|
6583
7831
|
}
|
|
6584
7832
|
function uniqueReadContexts(acls) {
|
|
6585
7833
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -6594,24 +7842,31 @@ function uniqueReadContexts(acls) {
|
|
|
6594
7842
|
return contexts;
|
|
6595
7843
|
}
|
|
6596
7844
|
function buildRepoFactoryContent(contextSpecs) {
|
|
6597
|
-
const repositoryDefs = contextSpecs.flatMap((spec) => spec.aggregates.map((aggregate) =>
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
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
|
+
}));
|
|
6604
7859
|
assertNoSameContextRepoFileCollisions(repositoryDefs);
|
|
6605
7860
|
const namingPlan = buildRepositoryNamingPlan(repositoryDefs);
|
|
6606
7861
|
const lines = [`// Auto-generated repository factory — do not edit by hand`, ``];
|
|
6607
7862
|
for (const repositoryDef of repositoryDefs) {
|
|
6608
|
-
|
|
7863
|
+
repositoryDef.aggregateName;
|
|
6609
7864
|
const modulePath = normalizeModulePath(repositoryDef.modulePath);
|
|
6610
|
-
const repoClass =
|
|
7865
|
+
const repoClass = repositoryDef.aggregateAliasBase;
|
|
6611
7866
|
const { repoAlias } = namingPlan.get(repositoryKey(repositoryDef));
|
|
6612
7867
|
const repoFile = repositoryDef.repoFile;
|
|
6613
|
-
if (repoAlias === repoClass) lines.push(`import { ${repoClass} } from "
|
|
6614
|
-
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}";`);
|
|
6615
7870
|
}
|
|
6616
7871
|
lines.push(``);
|
|
6617
7872
|
lines.push(`export class RepositoryFactory {`);
|
|
@@ -6749,9 +8004,14 @@ export {};
|
|
|
6749
8004
|
const payloadTypeNames = [];
|
|
6750
8005
|
for (const evt of compositionSpec.crossContextEvents) {
|
|
6751
8006
|
const fields = getPayloadFields(evt.payloadSchema);
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
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
|
+
}
|
|
6755
8015
|
lines.push(`}`);
|
|
6756
8016
|
lines.push(``);
|
|
6757
8017
|
}
|
|
@@ -6762,18 +8022,20 @@ export {};
|
|
|
6762
8022
|
return lines.join("\n");
|
|
6763
8023
|
}
|
|
6764
8024
|
function buildHandlerFactoryContent(evt, sub) {
|
|
6765
|
-
const
|
|
8025
|
+
const fields = getPayloadFields(evt.payloadSchema);
|
|
8026
|
+
const payloadTypeName = integrationPayloadTypeName(evt);
|
|
8027
|
+
const payloadMappingLines = fields.map((f) => ` ${f.name}: event.payload.${f.name},`);
|
|
6766
8028
|
const commandType = `${pascalCase(sub.targetContext)}.${pascalCase(sub.action)}`;
|
|
6767
8029
|
return [
|
|
6768
8030
|
`// Auto-generated event handler factory — do not edit by hand`,
|
|
6769
8031
|
`import type { CommandBusPort } from "../../../../../core/ports/messaging/command-bus.port.ts";`,
|
|
6770
8032
|
`import type { IntegrationEvent, IntegrationEventHandler } from "../../../../../infrastructure/messaging/integration-event.ts";`,
|
|
6771
|
-
`import type { ${
|
|
8033
|
+
`import type { ${payloadTypeName} } from "../../../../shared-kernel/events/integration-events.types.ts";`,
|
|
6772
8034
|
``,
|
|
6773
8035
|
`export function ${sub.handlerFactoryName}(deps: {`,
|
|
6774
8036
|
` commandBus: CommandBusPort;`,
|
|
6775
|
-
`}): IntegrationEventHandler<${
|
|
6776
|
-
` return async (event: IntegrationEvent<${
|
|
8037
|
+
`}): IntegrationEventHandler<${payloadTypeName}> {`,
|
|
8038
|
+
` return async (event: IntegrationEvent<${payloadTypeName}>) => {`,
|
|
6777
8039
|
` return deps.commandBus.execute(`,
|
|
6778
8040
|
` {`,
|
|
6779
8041
|
` type: "${commandType}",`,
|
|
@@ -6792,30 +8054,114 @@ function buildSubscriptionRegistryContent(compositionSpec) {
|
|
|
6792
8054
|
const allSubs = [];
|
|
6793
8055
|
for (const evt of compositionSpec.crossContextEvents) for (const sub of evt.subscriptions) allSubs.push({
|
|
6794
8056
|
eventType: evt.eventType,
|
|
8057
|
+
payloadTypeName: integrationPayloadTypeName(evt),
|
|
6795
8058
|
sub
|
|
6796
8059
|
});
|
|
6797
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);
|
|
6798
8063
|
for (const { sub } of allSubs) {
|
|
6799
8064
|
const fileName = `${kebabCase(sub.handlerName)}.handler.ts`;
|
|
6800
8065
|
importLines.push(`// Handler: ${sub.handlerFactoryName} from core/contexts/${sub.targetModulePath}/application/event-handlers/${fileName}`);
|
|
6801
8066
|
}
|
|
6802
|
-
const depsEntries = allSubs.map(({ sub }) => ` ${camelCase(sub.handlerName)}: IntegrationEventHandler
|
|
8067
|
+
const depsEntries = allSubs.map(({ payloadTypeName, sub }) => ` ${camelCase(sub.handlerName)}: IntegrationEventHandler<${payloadTypeName}>;`);
|
|
6803
8068
|
const registryEntries = allSubs.map(({ eventType, sub }) => ` { eventType: "${eventType}", handler: deps.${camelCase(sub.handlerName)} },`);
|
|
6804
8069
|
const lines = [...importLines, ``];
|
|
6805
8070
|
if (depsEntries.length > 0) lines.push(`export function buildSubscriptionRegistry(deps: {`, ...depsEntries, `}): EventSubscription[] {`, ` return [`, ...registryEntries, ` ];`, `}`, ``);
|
|
6806
8071
|
else lines.push(`export function buildSubscriptionRegistry(_deps: Record<string, never>): EventSubscription[] {`, ` return [];`, `}`, ``);
|
|
6807
8072
|
return lines.join("\n");
|
|
6808
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
|
+
}
|
|
6809
8155
|
function getPayloadFields(payloadSchema) {
|
|
6810
8156
|
const shape = payloadSchema?.shape;
|
|
6811
8157
|
if (shape && typeof shape === "object") return Object.keys(shape).map((key) => {
|
|
6812
|
-
const
|
|
6813
|
-
|
|
6814
|
-
if (typeName === "number") tsType = "number";
|
|
6815
|
-
else if (typeName === "boolean") tsType = "boolean";
|
|
8158
|
+
const fieldSchema = shape[key];
|
|
8159
|
+
const rendered = renderPayloadSchemaType(fieldSchema);
|
|
6816
8160
|
return {
|
|
6817
8161
|
name: key,
|
|
6818
|
-
type:
|
|
8162
|
+
type: rendered.type,
|
|
8163
|
+
optional: rendered.optional,
|
|
8164
|
+
comment: rendered.comment
|
|
6819
8165
|
};
|
|
6820
8166
|
});
|
|
6821
8167
|
return [];
|
|
@@ -6840,7 +8186,7 @@ async function buildContextNormalizedSpecFromConfig(input) {
|
|
|
6840
8186
|
}))), config);
|
|
6841
8187
|
}
|
|
6842
8188
|
async function buildV5ContextArtifacts(input) {
|
|
6843
|
-
return generateContextArtifacts(await buildContextNormalizedSpecFromConfig(input));
|
|
8189
|
+
return generateContextArtifacts(await buildContextNormalizedSpecFromConfig(input), { infrastructureStrategy: input.infrastructureStrategy });
|
|
6844
8190
|
}
|
|
6845
8191
|
async function buildV5Artifacts(input) {
|
|
6846
8192
|
const spec = await buildNormalizedSpecFromConfig(input);
|
|
@@ -6865,8 +8211,9 @@ async function buildCompositionArtifacts(compositionConfigPath, contextSpecs) {
|
|
|
6865
8211
|
return mergeGeneratedArtifacts([
|
|
6866
8212
|
...buildCompositionTypeArtifacts(),
|
|
6867
8213
|
...buildCompositionBusArtifacts(),
|
|
6868
|
-
...buildCompositionOutboxArtifacts(),
|
|
8214
|
+
...buildCompositionOutboxArtifacts(compositionSpec.infrastructure),
|
|
6869
8215
|
...buildCompositionAclArtifacts(compositionSpec),
|
|
8216
|
+
...buildCompositionDependencyManifestArtifacts(compositionSpec),
|
|
6870
8217
|
...buildReadModelCompositionArtifacts(composedContextSpecs),
|
|
6871
8218
|
...buildCompositionContainerArtifacts(compositionSpec, composedContextSpecs),
|
|
6872
8219
|
...buildCompositionRouterArtifacts(compositionSpec, composedContextSpecs),
|
|
@@ -6875,4 +8222,4 @@ async function buildCompositionArtifacts(compositionConfigPath, contextSpecs) {
|
|
|
6875
8222
|
}
|
|
6876
8223
|
|
|
6877
8224
|
//#endregion
|
|
6878
|
-
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 };
|