eva4j 1.0.16 → 1.0.18
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/AGENTS.md +220 -5
- package/DOMAIN_YAML_GUIDE.md +188 -3
- package/FUTURE_FEATURES.md +33 -52
- package/QUICK_REFERENCE.md +8 -4
- package/bin/eva4j.js +70 -2
- package/config/defaults.json +1 -0
- package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
- package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
- package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
- package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
- package/docs/commands/EVALUATE_SYSTEM.md +290 -10
- package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
- package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
- package/docs/commands/INDEX.md +27 -3
- package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
- package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
- package/docs/prototype/system/RISKS.md +277 -0
- package/docs/prototype/system/customers.yaml +133 -0
- package/docs/prototype/system/inventory.yaml +109 -0
- package/docs/prototype/system/notifications.yaml +131 -0
- package/docs/prototype/system/orders.yaml +241 -0
- package/docs/prototype/system/payments.yaml +256 -0
- package/docs/prototype/system/products.yaml +168 -0
- package/docs/prototype/system/system.yaml +269 -0
- package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
- package/examples/domain-events.yaml +26 -0
- package/examples/domain-read-models.yaml +113 -0
- package/examples/system/customer.yaml +89 -0
- package/examples/system/orders.yaml +119 -0
- package/examples/system/product.yaml +27 -0
- package/examples/system/system.yaml +80 -0
- package/package.json +1 -1
- package/read-model-spec.md +664 -0
- package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
- package/src/agents/design-gap-analyst.agent.md +383 -0
- package/src/agents/design-reviewer-temporal.agent.md +412 -0
- package/src/agents/design-reviewer.agent.md +34 -5
- package/src/agents/implement-use-cases.prompt.md +179 -0
- package/src/agents/ux-gap-analyst.agent.md +412 -0
- package/src/commands/add-rabbitmq-client.js +261 -0
- package/src/commands/add-temporal-client.js +22 -2
- package/src/commands/build.js +267 -11
- package/src/commands/evaluate-system.js +700 -13
- package/src/commands/generate-entities.js +560 -24
- package/src/commands/generate-http-exchange.js +3 -0
- package/src/commands/generate-kafka-event.js +3 -0
- package/src/commands/generate-kafka-listener.js +3 -0
- package/src/commands/generate-rabbitmq-event.js +665 -0
- package/src/commands/generate-rabbitmq-listener.js +205 -0
- package/src/commands/generate-record.js +2 -2
- package/src/commands/generate-resource.js +4 -1
- package/src/commands/generate-temporal-activity.js +970 -33
- package/src/commands/generate-temporal-flow.js +98 -38
- package/src/commands/generate-temporal-system.js +708 -0
- package/src/commands/generate-usecase.js +4 -1
- package/src/skills/build-system-yaml/SKILL.md +343 -2
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
- package/src/skills/build-system-yaml/references/module-spec.md +90 -9
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
- package/src/skills/build-temporal-system/SKILL.md +752 -0
- package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
- package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
- package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
- package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
- package/src/skills/implement-use-case/SKILL.md +350 -0
- package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
- package/src/skills/requirements-elicitation/SKILL.md +228 -0
- package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
- package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
- package/src/utils/bounded-context-diagram.js +844 -0
- package/src/utils/config-manager.js +4 -2
- package/src/utils/domain-validator.js +495 -17
- package/src/utils/naming.js +20 -0
- package/src/utils/system-validator.js +169 -11
- package/src/utils/system-yaml-parser.js +318 -0
- package/src/utils/temporal-validator.js +497 -0
- package/src/utils/validator.js +3 -1
- package/src/utils/yaml-to-entity.js +281 -9
- package/templates/aggregate/AggregateRepository.java.ejs +4 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
- package/templates/aggregate/AggregateRoot.java.ejs +38 -4
- package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
- package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
- package/templates/aggregate/JpaEntity.java.ejs +2 -2
- package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
- package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
- package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
- package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
- package/templates/base/root/AGENTS.md.ejs +1 -1
- package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
- package/templates/crud/EndpointsController.java.ejs +1 -1
- package/templates/crud/ScaffoldCommand.java.ejs +5 -2
- package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
- package/templates/crud/ScaffoldQuery.java.ejs +5 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
- package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
- package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
- package/templates/evaluate/report.html.ejs +1447 -90
- package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
- package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
- package/templates/ports/PortAclMapper.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +7 -22
- package/templates/ports/PortFeignClient.java.ejs +4 -0
- package/templates/ports/PortResponseDto.java.ejs +1 -1
- package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
- package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
- package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
- package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
- package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
- package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
- package/templates/read-model/ReadModelDomain.java.ejs +46 -0
- package/templates/read-model/ReadModelJpa.java.ejs +58 -0
- package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
- package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
- package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
- package/templates/read-model/ReadModelRepository.java.ejs +42 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
- package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
- package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
- package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
- package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
- package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
- package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/NestedType.java.ejs +12 -0
- package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
- package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
- package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
- package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
- package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
- package/COMMAND_EVALUATION.md +0 -911
|
@@ -49,12 +49,16 @@ async function parseDomainYaml(yamlPath, packageName = '', moduleName = '') {
|
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
const ports = parsePorts(domainData, moduleName);
|
|
53
|
+
const readModels = parseReadModels(domainData, moduleName, ports);
|
|
54
|
+
|
|
52
55
|
return {
|
|
53
56
|
aggregates,
|
|
54
57
|
allEnums: extractAllEnums(domainData.aggregates),
|
|
55
58
|
endpoints,
|
|
56
59
|
listeners,
|
|
57
|
-
ports
|
|
60
|
+
ports,
|
|
61
|
+
readModels
|
|
58
62
|
};
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -108,7 +112,9 @@ function parseAggregate(aggregateData) {
|
|
|
108
112
|
name: eventName,
|
|
109
113
|
fieldName: toCamelCase(eventName),
|
|
110
114
|
fields: eventFields,
|
|
111
|
-
triggers: event.triggers || []
|
|
115
|
+
triggers: event.triggers || [],
|
|
116
|
+
lifecycle: event.lifecycle || null,
|
|
117
|
+
notifies: event.notifies || []
|
|
112
118
|
};
|
|
113
119
|
});
|
|
114
120
|
|
|
@@ -122,6 +128,17 @@ function parseAggregate(aggregateData) {
|
|
|
122
128
|
});
|
|
123
129
|
});
|
|
124
130
|
|
|
131
|
+
// Build lifecycle map: { create → [event, ...], update → [...], delete → [...], softDelete → [...] }
|
|
132
|
+
// Used by templates to emit raise() calls at CRUD lifecycle points.
|
|
133
|
+
const VALID_LIFECYCLE_VALUES = ['create', 'update', 'delete', 'softDelete'];
|
|
134
|
+
const lifecycleEventsMap = {};
|
|
135
|
+
domainEvents.forEach(event => {
|
|
136
|
+
if (event.lifecycle && VALID_LIFECYCLE_VALUES.includes(event.lifecycle)) {
|
|
137
|
+
if (!lifecycleEventsMap[event.lifecycle]) lifecycleEventsMap[event.lifecycle] = [];
|
|
138
|
+
lifecycleEventsMap[event.lifecycle].push(event);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
125
142
|
return {
|
|
126
143
|
name: toPascalCase(name),
|
|
127
144
|
packageName: aggregateData.package || '',
|
|
@@ -132,6 +149,7 @@ function parseAggregate(aggregateData) {
|
|
|
132
149
|
allEntities: parsedEntities,
|
|
133
150
|
domainEvents,
|
|
134
151
|
triggeredEventsMap,
|
|
152
|
+
lifecycleEventsMap,
|
|
135
153
|
enums: aggregateEnums
|
|
136
154
|
};
|
|
137
155
|
}
|
|
@@ -675,14 +693,16 @@ function generateAggregateMethods(root, secondaryEntities) {
|
|
|
675
693
|
}
|
|
676
694
|
|
|
677
695
|
// remove method
|
|
696
|
+
const secondaryIdField = secondaryEntity ? secondaryEntity.fields.find(f => f.name === 'id') : null;
|
|
697
|
+
const secondaryIdType = secondaryIdField ? secondaryIdField.javaType : 'String';
|
|
678
698
|
methods.push({
|
|
679
|
-
name: `remove${toPascalCase(singularName)}`,
|
|
699
|
+
name: `remove${toPascalCase(singularName)}ById`,
|
|
680
700
|
returnType: 'void',
|
|
681
701
|
parameters: [{
|
|
682
|
-
name: '
|
|
683
|
-
type:
|
|
702
|
+
name: 'itemId',
|
|
703
|
+
type: secondaryIdType
|
|
684
704
|
}],
|
|
685
|
-
body: `this.${rel.fieldName}.removeIf(item -> item.getId().equals(
|
|
705
|
+
body: `this.${rel.fieldName}.removeIf(item -> item.getId().equals(itemId));`
|
|
686
706
|
});
|
|
687
707
|
|
|
688
708
|
// get unmodifiable collection
|
|
@@ -1060,8 +1080,8 @@ function parseListeners(domainData) {
|
|
|
1060
1080
|
// Normalise: strip trailing 'Event' suffix for class naming, re-add it consistently
|
|
1061
1081
|
const baseName = eventName.endsWith('Event') ? eventName.slice(0, -5) : eventName;
|
|
1062
1082
|
const integrationEventClassName = `${baseName}IntegrationEvent`;
|
|
1063
|
-
//
|
|
1064
|
-
const listenerClassName = `${baseName}
|
|
1083
|
+
// Generic suffix — broker-specific suffix applied in generate-entities.js
|
|
1084
|
+
const listenerClassName = `${baseName}Listener`;
|
|
1065
1085
|
const useCaseName = toPascalCase(listener.useCase);
|
|
1066
1086
|
const commandClassName = `${useCaseName}Command`;
|
|
1067
1087
|
const topic = listener.topic || null;
|
|
@@ -1253,6 +1273,255 @@ function parsePorts(domainData, moduleName = '') {
|
|
|
1253
1273
|
});
|
|
1254
1274
|
}
|
|
1255
1275
|
|
|
1276
|
+
/**
|
|
1277
|
+
* Derive a Kafka topic name from an event class name.
|
|
1278
|
+
* Strips trailing 'Event' suffix, then converts to SCREAMING_SNAKE_CASE.
|
|
1279
|
+
* e.g. 'ProductCreatedEvent' → 'PRODUCT_CREATED'
|
|
1280
|
+
* 'OrderCancelled' → 'ORDER_CANCELLED'
|
|
1281
|
+
* @param {string} eventName - PascalCase event name
|
|
1282
|
+
* @returns {string} SCREAMING_SNAKE_CASE topic name
|
|
1283
|
+
*/
|
|
1284
|
+
function deriveTopicFromEventName(eventName) {
|
|
1285
|
+
const base = eventName.endsWith('Event') ? eventName.slice(0, -5) : eventName;
|
|
1286
|
+
// PascalCase → SCREAMING_SNAKE_CASE
|
|
1287
|
+
return base
|
|
1288
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
1289
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1_$2')
|
|
1290
|
+
.toUpperCase();
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* Parse the optional readModels section from domain.yaml.
|
|
1295
|
+
* Declares local read model projections maintained via domain events
|
|
1296
|
+
* from external bounded contexts.
|
|
1297
|
+
* @param {Object} domainData - Raw parsed YAML data
|
|
1298
|
+
* @param {string} moduleName - Current module name
|
|
1299
|
+
* @param {Array} ports - Parsed ports (for RM-009 warning)
|
|
1300
|
+
* @returns {Array} Parsed read models array (empty if not declared)
|
|
1301
|
+
*/
|
|
1302
|
+
function parseReadModels(domainData, moduleName = '', ports = []) {
|
|
1303
|
+
if (!domainData.readModels || !Array.isArray(domainData.readModels)) return [];
|
|
1304
|
+
|
|
1305
|
+
const validActions = ['UPSERT', 'DELETE', 'SOFT_DELETE'];
|
|
1306
|
+
|
|
1307
|
+
return domainData.readModels.map(rm => {
|
|
1308
|
+
const name = toPascalCase(rm.name);
|
|
1309
|
+
|
|
1310
|
+
// RM-001: name must end with 'ReadModel'
|
|
1311
|
+
if (!name.endsWith('ReadModel')) {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
`[RM-001] Read model "${name}": name must end with "ReadModel" suffix. ` +
|
|
1314
|
+
`Rename to "${name}ReadModel".`
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// RM-002: tableName must start with 'rm_'
|
|
1319
|
+
if (!rm.tableName || !rm.tableName.startsWith('rm_')) {
|
|
1320
|
+
throw new Error(
|
|
1321
|
+
`[RM-002] Read model "${name}": tableName must start with "rm_" prefix. ` +
|
|
1322
|
+
`Got: "${rm.tableName || '(empty)'}".`
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Source is required
|
|
1327
|
+
if (!rm.source || !rm.source.module || !rm.source.aggregate) {
|
|
1328
|
+
throw new Error(
|
|
1329
|
+
`[RM-010] Read model "${name}": source.module and source.aggregate are required.`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// RM-010: source.module must differ from current module
|
|
1334
|
+
if (rm.source.module === moduleName || toKebabCase(rm.source.module) === toKebabCase(moduleName)) {
|
|
1335
|
+
throw new Error(
|
|
1336
|
+
`[RM-010] Read model "${name}": source.module ("${rm.source.module}") is the same as the current module. ` +
|
|
1337
|
+
`Read models are for cross-module projections only.`
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Parse fields
|
|
1342
|
+
const fields = (rm.fields || []).map(f => ({
|
|
1343
|
+
name: toCamelCase(f.name),
|
|
1344
|
+
javaType: f.type
|
|
1345
|
+
}));
|
|
1346
|
+
|
|
1347
|
+
// RM-004: fields must include an 'id' field
|
|
1348
|
+
if (!fields.some(f => f.name === 'id')) {
|
|
1349
|
+
throw new Error(
|
|
1350
|
+
`[RM-004] Read model "${name}": fields must include an "id" field.`
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// Parse syncedBy
|
|
1355
|
+
const syncedBy = rm.syncedBy || [];
|
|
1356
|
+
|
|
1357
|
+
// RM-005: syncedBy must have at least one entry
|
|
1358
|
+
if (syncedBy.length === 0) {
|
|
1359
|
+
throw new Error(
|
|
1360
|
+
`[RM-005] Read model "${name}": syncedBy must have at least one entry.`
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Derive the source aggregate's id key for payload extraction
|
|
1365
|
+
// e.g., source.aggregate = 'Product' → sourceIdKey = 'productId'
|
|
1366
|
+
const sourceBase = rm.source.aggregate.charAt(0).toLowerCase() + rm.source.aggregate.slice(1);
|
|
1367
|
+
const sourceIdKey = sourceBase + 'Id';
|
|
1368
|
+
|
|
1369
|
+
const parsedSyncedBy = syncedBy.map(sync => {
|
|
1370
|
+
const eventName = toPascalCase(sync.event);
|
|
1371
|
+
const action = (sync.action || '').toUpperCase();
|
|
1372
|
+
|
|
1373
|
+
// RM-006: action must be valid
|
|
1374
|
+
if (!validActions.includes(action)) {
|
|
1375
|
+
throw new Error(
|
|
1376
|
+
`[RM-006] Read model "${name}", event "${eventName}": ` +
|
|
1377
|
+
`syncedBy action must be one of ${validActions.join(', ')}. Got: "${action}".`
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const baseName = eventName.endsWith('Event') ? eventName.slice(0, -5) : eventName;
|
|
1382
|
+
const topic = sync.topic || deriveTopicFromEventName(eventName);
|
|
1383
|
+
const topicKey = topic.toLowerCase().replace(/_/g, '-');
|
|
1384
|
+
|
|
1385
|
+
return {
|
|
1386
|
+
event: eventName,
|
|
1387
|
+
eventBaseName: baseName,
|
|
1388
|
+
action,
|
|
1389
|
+
integrationEventClassName: `${baseName}IntegrationEvent`,
|
|
1390
|
+
listenerClassName: `${baseName}ReadModelListener`,
|
|
1391
|
+
topicConstant: topic,
|
|
1392
|
+
topicKey,
|
|
1393
|
+
topicSpringProperty: `\${topics.${topicKey}}`,
|
|
1394
|
+
topicVariableName: toCamelCase(topicKey.replace(/-/g, '_')),
|
|
1395
|
+
// UPSERT needs all readModel fields; DELETE/SOFT_DELETE only need the id
|
|
1396
|
+
// payloadKey maps readModel field names to producer payload keys
|
|
1397
|
+
// e.g., readModel 'id' → payload 'productId' (convention {entityName}Id)
|
|
1398
|
+
fields: action === 'UPSERT'
|
|
1399
|
+
? fields.map(f => ({ ...f, payloadKey: f.name === 'id' ? sourceIdKey : f.name }))
|
|
1400
|
+
: [{ name: 'id', javaType: 'String', payloadKey: sourceIdKey }]
|
|
1401
|
+
};
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
const hasSoftDelete = parsedSyncedBy.some(s => s.action === 'SOFT_DELETE');
|
|
1405
|
+
const sourceName = toPascalCase(rm.source.aggregate);
|
|
1406
|
+
const sourceModule = rm.source.module;
|
|
1407
|
+
|
|
1408
|
+
// RM-009: warn if ports still has sync calls to the same source module
|
|
1409
|
+
const conflictingPorts = ports.filter(p =>
|
|
1410
|
+
p.target && (p.target === sourceModule || toKebabCase(p.target) === toKebabCase(sourceModule))
|
|
1411
|
+
);
|
|
1412
|
+
if (conflictingPorts.length > 0) {
|
|
1413
|
+
const serviceNames = conflictingPorts.map(p => p.serviceName).join(', ');
|
|
1414
|
+
console.warn(
|
|
1415
|
+
`⚠️ [RM-009] Read model "${name}": ports: section still contains sync calls ` +
|
|
1416
|
+
`to module "${sourceModule}" (services: ${serviceNames}). Consider removing them ` +
|
|
1417
|
+
`since the read model provides local access to that data.`
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
return {
|
|
1422
|
+
name,
|
|
1423
|
+
sourceName,
|
|
1424
|
+
sourceModule,
|
|
1425
|
+
tableName: rm.tableName,
|
|
1426
|
+
fields,
|
|
1427
|
+
syncedBy: parsedSyncedBy,
|
|
1428
|
+
hasSoftDelete,
|
|
1429
|
+
// Derived class names
|
|
1430
|
+
domainClassName: name,
|
|
1431
|
+
jpaEntityName: `${name}Jpa`,
|
|
1432
|
+
jpaRepositoryName: `${name}JpaRepository`,
|
|
1433
|
+
repositoryName: `${name}Repository`,
|
|
1434
|
+
repositoryImplName: `${name}RepositoryImpl`,
|
|
1435
|
+
syncHandlerName: `Sync${sourceName}ReadModelHandler`
|
|
1436
|
+
};
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/**
|
|
1441
|
+
* Resolve event constructor arguments for a domain event given entity context.
|
|
1442
|
+
* Replicates the same resolution logic used in AggregateRoot.java.ejs for transitions.
|
|
1443
|
+
*
|
|
1444
|
+
* @param {Object} event - Parsed domain event { name, fields }
|
|
1445
|
+
* @param {string} entityName - PascalCase aggregate root name (e.g. 'Product')
|
|
1446
|
+
* @param {Array} entityFields - Parsed fields of the root entity
|
|
1447
|
+
* @param {Array} valueObjects - Parsed value objects of the aggregate
|
|
1448
|
+
* @param {string} prefix - Expression prefix for getters (e.g. 'this' or 'updated' or 'entity')
|
|
1449
|
+
* @returns {Object} { args: string[], needsLocalDateTime: boolean, needsUUID: boolean }
|
|
1450
|
+
*/
|
|
1451
|
+
function resolveEventArgs(event, entityName, entityFields, valueObjects = [], prefix = 'this') {
|
|
1452
|
+
const entityBase = entityName.charAt(0).toLowerCase() + entityName.slice(1);
|
|
1453
|
+
const args = [`${prefix}.getId()`];
|
|
1454
|
+
let needsLocalDateTime = false;
|
|
1455
|
+
let needsUUID = false;
|
|
1456
|
+
|
|
1457
|
+
(event.fields || []).forEach(ef => {
|
|
1458
|
+
// Skip {entityName}Id — already provided as aggregateId in the DomainEvent constructor
|
|
1459
|
+
if (ef.name === entityBase + 'Id') return;
|
|
1460
|
+
|
|
1461
|
+
const matched = entityFields.find(f => f.name === ef.name);
|
|
1462
|
+
if (matched) {
|
|
1463
|
+
if (matched.javaType === ef.javaType) {
|
|
1464
|
+
const cap = ef.name.charAt(0).toUpperCase() + ef.name.slice(1);
|
|
1465
|
+
args.push(`${prefix}.get${cap}()`);
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
// Type mismatch: entity field may be a VO wrapping the expected primitive/type
|
|
1469
|
+
const vo = (valueObjects || []).find(v => v.name === matched.javaType);
|
|
1470
|
+
if (vo) {
|
|
1471
|
+
const voSub = vo.fields.find(voF => voF.name === ef.name && voF.javaType === ef.javaType)
|
|
1472
|
+
|| vo.fields.find(voF => voF.javaType === ef.javaType);
|
|
1473
|
+
if (voSub) {
|
|
1474
|
+
const oCap = ef.name.charAt(0).toUpperCase() + ef.name.slice(1);
|
|
1475
|
+
const sCap = voSub.name.charAt(0).toUpperCase() + voSub.name.slice(1);
|
|
1476
|
+
args.push(`${prefix}.get${oCap}().get${sCap}()`);
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
// Enum → String: convert via .name()
|
|
1481
|
+
if (matched.isEnum && ef.javaType === 'String') {
|
|
1482
|
+
const cap = ef.name.charAt(0).toUpperCase() + ef.name.slice(1);
|
|
1483
|
+
args.push(`${prefix}.get${cap}().name()`);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
args.push(`null /* TODO: provide ${ef.name} (entity returns ${matched.javaType}, expected ${ef.javaType}) */`);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
if (ef.name.endsWith('At') && ef.javaType === 'LocalDateTime') {
|
|
1490
|
+
args.push('LocalDateTime.now()');
|
|
1491
|
+
needsLocalDateTime = true;
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
args.push(`null /* TODO: provide ${ef.name} */`);
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
return { args, needsLocalDateTime, needsUUID };
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/**
|
|
1501
|
+
* Pre-compute resolved arguments for all lifecycle events.
|
|
1502
|
+
* Returns an enriched lifecycleEventsMap where each event has a `resolvedArgs` array.
|
|
1503
|
+
*
|
|
1504
|
+
* @param {Object} lifecycleEventsMap - { create: [event, ...], update: [...], ... }
|
|
1505
|
+
* @param {string} entityName - PascalCase aggregate root name
|
|
1506
|
+
* @param {Array} entityFields - Parsed fields of the root entity
|
|
1507
|
+
* @param {Array} valueObjects - Parsed value objects of the aggregate
|
|
1508
|
+
* @returns {Object} enriched map: { create: [{ ...event, resolvedArgs, needsLocalDateTime }], ... }
|
|
1509
|
+
*/
|
|
1510
|
+
function resolveLifecycleEventArgs(lifecycleEventsMap, entityName, entityFields, valueObjects = []) {
|
|
1511
|
+
const resolved = {};
|
|
1512
|
+
// Each lifecycle type uses a different variable name in the generated code
|
|
1513
|
+
// update uses 'this' because raise() is now inside the entity's update() method
|
|
1514
|
+
const prefixMap = { create: 'this', update: 'this', delete: 'entity', softDelete: 'this' };
|
|
1515
|
+
for (const [lifecycle, events] of Object.entries(lifecycleEventsMap)) {
|
|
1516
|
+
const prefix = prefixMap[lifecycle] || 'this';
|
|
1517
|
+
resolved[lifecycle] = events.map(event => {
|
|
1518
|
+
const { args, needsLocalDateTime } = resolveEventArgs(event, entityName, entityFields, valueObjects, prefix);
|
|
1519
|
+
return { ...event, resolvedArgs: args, needsLocalDateTime };
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
return resolved;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1256
1525
|
module.exports = {
|
|
1257
1526
|
parseDomainYaml,
|
|
1258
1527
|
parseAggregate,
|
|
@@ -1263,5 +1532,8 @@ module.exports = {
|
|
|
1263
1532
|
generateValidationImports,
|
|
1264
1533
|
generateAggregateMethodImports,
|
|
1265
1534
|
parseListeners,
|
|
1266
|
-
parsePorts
|
|
1535
|
+
parsePorts,
|
|
1536
|
+
parseReadModels,
|
|
1537
|
+
resolveEventArgs,
|
|
1538
|
+
resolveLifecycleEventArgs
|
|
1267
1539
|
};
|
|
@@ -20,6 +20,10 @@ public interface <%= rootEntity.name %>Repository {
|
|
|
20
20
|
boolean existsById(<%= rootEntity.fields[0].javaType %> id);
|
|
21
21
|
<% if (!hasSoftDelete) { %>
|
|
22
22
|
void deleteById(<%= rootEntity.fields[0].javaType %> id);
|
|
23
|
+
<% if (hasDeleteLifecycle) { %>
|
|
24
|
+
|
|
25
|
+
void delete(<%= rootEntity.name %> <%= rootEntity.fieldName %>);
|
|
26
|
+
<% } %>
|
|
23
27
|
<% } %>
|
|
24
28
|
<% if (findByOps && findByOps.length > 0) { %>
|
|
25
29
|
<% findByOps.forEach(function(op) { %>
|
|
@@ -58,6 +58,14 @@ public class <%= rootEntity.name %>RepositoryImpl implements <%= rootEntity.name
|
|
|
58
58
|
public void deleteById(<%= rootEntity.fields[0].javaType %> id) {
|
|
59
59
|
jpaRepository.deleteById(id);
|
|
60
60
|
}
|
|
61
|
+
<% if (hasDeleteLifecycle) { %>
|
|
62
|
+
|
|
63
|
+
@Override
|
|
64
|
+
public void delete(<%= rootEntity.name %> <%= rootEntity.fieldName %>) {
|
|
65
|
+
<%= rootEntity.fieldName %>.pullDomainEvents().forEach(eventPublisher::publishEvent);
|
|
66
|
+
jpaRepository.deleteById(<%= rootEntity.fieldName %>.getId());
|
|
67
|
+
}
|
|
68
|
+
<% } %>
|
|
61
69
|
<% } %>
|
|
62
70
|
<% if (findByOps && findByOps.length > 0) { %>
|
|
63
71
|
<% findByOps.forEach(function(op) { %>
|
|
@@ -19,9 +19,14 @@ import <%= packageName %>.<%= moduleName %>.domain.models.events.<%= event.name
|
|
|
19
19
|
<% }); %>
|
|
20
20
|
<% } %>
|
|
21
21
|
<%
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
})
|
|
22
|
+
const _allLifecycleEvents = Object.values(lifecycleEventsMap || {}).flat();
|
|
23
|
+
const _needsLocalDateTimeImport = (
|
|
24
|
+
Object.values(triggeredEventsMap || {}).flat().some(function(ev) {
|
|
25
|
+
return (ev.fields || []).some(function(ef) { return ef.name.endsWith('At') && ef.javaType === 'LocalDateTime'; });
|
|
26
|
+
}) ||
|
|
27
|
+
_allLifecycleEvents.some(function(ev) { return ev.needsLocalDateTime; })
|
|
28
|
+
) && !imports.some(function(imp) { return imp.includes('LocalDateTime'); });
|
|
29
|
+
const _hasCreateLifecycle = ((lifecycleEventsMap || {}).create || []).length > 0;
|
|
25
30
|
%>
|
|
26
31
|
<% if (_needsLocalDateTimeImport) { %>
|
|
27
32
|
import java.time.LocalDateTime;
|
|
@@ -36,6 +41,7 @@ import java.util.List;
|
|
|
36
41
|
*/
|
|
37
42
|
public class <%= name %> {
|
|
38
43
|
<% if (domainEvents && domainEvents.length > 0) { %>
|
|
44
|
+
<% const _needsPublicRaise = ((lifecycleEventsMap || {}).delete || []).length > 0; %>
|
|
39
45
|
|
|
40
46
|
// ─── Domain Events ──────────────────────────────────────────────────────────
|
|
41
47
|
private final List<DomainEvent> _domainEvents = new ArrayList<>();
|
|
@@ -44,7 +50,7 @@ public class <%= name %> {
|
|
|
44
50
|
* Registers a domain event to be published after successful persistence.
|
|
45
51
|
* Call this inside business methods when something significant happens.
|
|
46
52
|
*/
|
|
47
|
-
protected void raise(DomainEvent event) {
|
|
53
|
+
<%= _needsPublicRaise ? 'public' : 'protected' %> void raise(DomainEvent event) {
|
|
48
54
|
_domainEvents.add(event);
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -93,9 +99,13 @@ public class <%= name %> {
|
|
|
93
99
|
<% const creationFields = fields.filter(f => f.name !== 'id' && f.name !== 'createdAt' && f.name !== 'updatedAt' && f.name !== 'createdBy' && f.name !== 'updatedBy' && f.name !== 'deletedAt' && !f.readOnly && !f.autoInit); %>
|
|
94
100
|
<% const autoInitFields = fields.filter(f => f.autoInit); %>
|
|
95
101
|
<% const defaultValueFields = fields.filter(f => f.readOnly && !f.autoInit && f.javaDefaultValue); %>
|
|
102
|
+
<% const _createLifecycleEvents = (lifecycleEventsMap || {}).create || []; %>
|
|
96
103
|
<% if (creationFields.length > 0 || autoInitFields.length > 0 || defaultValueFields.length > 0) { %>
|
|
97
104
|
// Constructor for new entity creation (without id, audit fields, readOnly and auto-initialized fields)
|
|
98
105
|
public <%= name %>(<% let paramIdx = 0; %><% creationFields.forEach((field, idx) => { %><% if (paramIdx > 0) { %>, <% } %><%- field.javaType %> <%= field.name %><% paramIdx++; %><% }); %>) {
|
|
106
|
+
<% if (_createLifecycleEvents.length > 0) { %>
|
|
107
|
+
this.id = java.util.UUID.randomUUID().toString();
|
|
108
|
+
<% } %>
|
|
99
109
|
<% creationFields.forEach(field => { %>
|
|
100
110
|
this.<%= field.name %> = <%= field.name %>;
|
|
101
111
|
<% }); %>
|
|
@@ -104,6 +114,26 @@ public class <%= name %> {
|
|
|
104
114
|
<% }); %>
|
|
105
115
|
<% defaultValueFields.forEach(field => { %>
|
|
106
116
|
this.<%= field.name %> = <%- field.javaDefaultValue %>;
|
|
117
|
+
<% }); %>
|
|
118
|
+
<% _createLifecycleEvents.forEach(function(evt) { %>
|
|
119
|
+
raise(new <%= evt.name %>(<%= evt.resolvedArgs.join(', ') %>));
|
|
120
|
+
<% }); %>
|
|
121
|
+
}
|
|
122
|
+
<% } %>
|
|
123
|
+
<% const _updateLifecycleEvents = (lifecycleEventsMap || {}).update || []; %>
|
|
124
|
+
<% if (_updateLifecycleEvents.length > 0 && creationFields.length > 0) { %>
|
|
125
|
+
|
|
126
|
+
// ─── Update Method (lifecycle: update) ───────────────────────────────────
|
|
127
|
+
/**
|
|
128
|
+
* Updates the entity fields and raises domain event(s).
|
|
129
|
+
* Receives final (already-merged) values — PATCH semantics are resolved by the caller.
|
|
130
|
+
*/
|
|
131
|
+
public void update(<% creationFields.forEach((field, idx) => { %><%- field.javaType %> <%= field.name %><%= idx < creationFields.length - 1 ? ', ' : '' %><% }); %>) {
|
|
132
|
+
<% creationFields.forEach(field => { %>
|
|
133
|
+
this.<%= field.name %> = <%= field.name %>;
|
|
134
|
+
<% }); %>
|
|
135
|
+
<% _updateLifecycleEvents.forEach(function(evt) { %>
|
|
136
|
+
raise(new <%= evt.name %>(<%= evt.resolvedArgs.join(', ') %>));
|
|
107
137
|
<% }); %>
|
|
108
138
|
}
|
|
109
139
|
<% } %>
|
|
@@ -243,6 +273,7 @@ public class <%= name %> {
|
|
|
243
273
|
<% }); %>
|
|
244
274
|
<% } %>
|
|
245
275
|
<% if (hasSoftDelete) { %>
|
|
276
|
+
<% const _softDeleteLifecycleEvents = (lifecycleEventsMap || {}).softDelete || []; %>
|
|
246
277
|
|
|
247
278
|
// ─── Soft Delete ─────────────────────────────────────────────────────────
|
|
248
279
|
/**
|
|
@@ -254,6 +285,9 @@ public class <%= name %> {
|
|
|
254
285
|
throw new IllegalStateException("<%= name %> is already deleted");
|
|
255
286
|
}
|
|
256
287
|
this.deletedAt = java.time.LocalDateTime.now();
|
|
288
|
+
<% _softDeleteLifecycleEvents.forEach(function(evt) { %>
|
|
289
|
+
raise(new <%= evt.name %>(<%= evt.resolvedArgs.join(', ') %>));
|
|
290
|
+
<% }); %>
|
|
257
291
|
}
|
|
258
292
|
|
|
259
293
|
public boolean isDeleted() {
|
|
@@ -3,67 +3,161 @@ package <%= packageName %>.<%= moduleName %>.application.usecases;
|
|
|
3
3
|
import <%= packageName %>.shared.domain.annotations.ApplicationComponent;
|
|
4
4
|
import org.springframework.transaction.event.TransactionPhase;
|
|
5
5
|
import org.springframework.transaction.event.TransactionalEventListener;
|
|
6
|
-
<% domainEvents.forEach(event => {
|
|
6
|
+
<% domainEvents.forEach(event => { -%>
|
|
7
7
|
import <%= packageName %>.<%= moduleName %>.domain.models.events.<%= event.name %>;
|
|
8
|
-
<% });
|
|
9
|
-
<% if (broker) {
|
|
10
|
-
<% domainEvents.forEach(event => {
|
|
8
|
+
<% }); -%>
|
|
9
|
+
<% if (broker) { -%>
|
|
10
|
+
<% domainEvents.forEach(event => { -%>
|
|
11
11
|
import <%= packageName %>.<%= moduleName %>.application.events.<%= event.integrationEventClassName %>;
|
|
12
|
-
<% });
|
|
12
|
+
<% }); -%>
|
|
13
13
|
import <%= packageName %>.<%= moduleName %>.application.ports.MessageBroker;
|
|
14
|
-
<% }
|
|
14
|
+
<% } -%>
|
|
15
|
+
<%
|
|
16
|
+
// Collect unique workflow flowPascal names from events with notifies (Temporal bridge)
|
|
17
|
+
// flowPascal = workflow name with "Workflow" suffix stripped (e.g. PlaceOrderWorkflow → PlaceOrder)
|
|
18
|
+
// The generated service class is {flowPascal}WorkFlowService (e.g. PlaceOrderWorkFlowService)
|
|
19
|
+
const _flowPascalSet = new Set();
|
|
20
|
+
const _inputClassSet = new Set();
|
|
21
|
+
let _needsLocalDateTimeImport = false;
|
|
22
|
+
if (typeof temporal !== 'undefined' && temporal) {
|
|
23
|
+
domainEvents.forEach(event => {
|
|
24
|
+
const _evFieldNames = new Set((event.fields || []).map(f => f.name));
|
|
25
|
+
const _aggrIdWf = aggregateName.charAt(0).toLowerCase() + aggregateName.slice(1) + 'Id';
|
|
26
|
+
(event.notifies || []).forEach(n => {
|
|
27
|
+
if (n.workflow && n.flowPascal) _flowPascalSet.add(n.flowPascal);
|
|
28
|
+
if (n.inputFields && n.inputFields.length > 0 && n.flowPascal) {
|
|
29
|
+
_inputClassSet.add(n.flowPascal + 'Input');
|
|
30
|
+
// Check if any unresolvable field uses LocalDateTime.now()
|
|
31
|
+
n.inputFields.forEach(f => {
|
|
32
|
+
if (f.name !== _aggrIdWf && !_evFieldNames.has(f.name)
|
|
33
|
+
&& f.name.endsWith('At') && f.javaType === 'LocalDateTime') {
|
|
34
|
+
_needsLocalDateTimeImport = true;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const _flowPascals = Array.from(_flowPascalSet);
|
|
42
|
+
const _hasTemporalBridge = _flowPascals.length > 0;
|
|
43
|
+
-%>
|
|
44
|
+
<% if (_hasTemporalBridge) { -%>
|
|
45
|
+
<% if (_needsLocalDateTimeImport) { -%>
|
|
46
|
+
import java.time.LocalDateTime;
|
|
47
|
+
<% } -%>
|
|
48
|
+
<% _flowPascals.forEach(fp => { -%>
|
|
49
|
+
import <%= packageName %>.<%= moduleName %>.application.usecases.<%= fp %>WorkFlowService;
|
|
50
|
+
<% }); -%>
|
|
51
|
+
<% _inputClassSet.forEach(cls => { -%>
|
|
52
|
+
import <%= packageName %>.<%= moduleName %>.application.usecases.<%= cls %>;
|
|
53
|
+
<% }); -%>
|
|
54
|
+
<% } -%>
|
|
15
55
|
|
|
16
56
|
/**
|
|
17
57
|
* <%= aggregateName %>DomainEventHandler — Domain Event Bridge
|
|
18
58
|
*
|
|
19
|
-
* Connects the internal Spring event bus (ApplicationEventPublisher) with
|
|
20
|
-
* external messaging port (MessageBroker)
|
|
59
|
+
* Connects the internal Spring event bus (ApplicationEventPublisher) with<% if (broker && _hasTemporalBridge) { %>
|
|
60
|
+
* the external messaging port (MessageBroker) and Temporal workflow orchestration.<% } else if (broker) { %>
|
|
61
|
+
* the external messaging port (MessageBroker).<% } else if (_hasTemporalBridge) { %>
|
|
62
|
+
* the Temporal workflow orchestration layer.<% } else { %>
|
|
63
|
+
* external side effects.<% } %>
|
|
21
64
|
*
|
|
22
65
|
* Architecture:
|
|
23
66
|
* AggregateRepositoryImpl.save()
|
|
24
67
|
* → eventPublisher.publishEvent(domainEvent) [internal Spring bus]
|
|
25
68
|
* → @TransactionalEventListener(AFTER_COMMIT) [this class]
|
|
69
|
+
<% if (broker) { -%>
|
|
26
70
|
* → messageBroker.publish*(integrationEvent) [port — broker-agnostic]
|
|
71
|
+
<% } -%>
|
|
72
|
+
<% if (_hasTemporalBridge) { -%>
|
|
73
|
+
* → workFlowService.startAsync(workflowId) [Temporal workflow launch]
|
|
74
|
+
<% } -%>
|
|
27
75
|
*
|
|
28
76
|
* AFTER_COMMIT guarantees that external events are published only when the
|
|
29
77
|
* database transaction committed successfully, preventing ghost events from
|
|
30
78
|
* rolled-back operations.
|
|
31
79
|
*
|
|
32
80
|
* Domain Events (domain/models/events/) — internal signals scoped to this bounded context.
|
|
81
|
+
<% if (broker) { -%>
|
|
33
82
|
* Integration Events (application/events/) — broker-facing projections; changing broker
|
|
34
83
|
* technology (Kafka → RabbitMQ → SNS) only requires changing the MessageBroker adapter.
|
|
35
84
|
* Domain Events — and therefore this class — never need modification.
|
|
85
|
+
<% } -%>
|
|
36
86
|
*/
|
|
37
87
|
@ApplicationComponent
|
|
38
88
|
public class <%= aggregateName %>DomainEventHandler {
|
|
39
|
-
<%
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
89
|
+
<%
|
|
90
|
+
// Determine constructor dependencies
|
|
91
|
+
const _deps = [];
|
|
92
|
+
if (broker) _deps.push({ type: 'MessageBroker', name: 'messageBroker' });
|
|
93
|
+
_flowPascals.forEach(fp => {
|
|
94
|
+
const svcType = fp + 'WorkFlowService';
|
|
95
|
+
const svcName = fp.charAt(0).toLowerCase() + fp.slice(1) + 'WorkFlowService';
|
|
96
|
+
_deps.push({ type: svcType, name: svcName });
|
|
97
|
+
});
|
|
98
|
+
-%>
|
|
99
|
+
<% if (_deps.length > 0) { %>
|
|
100
|
+
<% _deps.forEach(dep => { -%>
|
|
101
|
+
private final <%= dep.type %> <%= dep.name %>;
|
|
102
|
+
<% }); %>
|
|
103
|
+
public <%= aggregateName %>DomainEventHandler(<%= _deps.map(d => d.type + ' ' + d.name).join(', ') %>) {
|
|
104
|
+
<% _deps.forEach(dep => { -%>
|
|
105
|
+
this.<%= dep.name %> = <%= dep.name %>;
|
|
106
|
+
<% }); -%>
|
|
45
107
|
}
|
|
46
|
-
<% }
|
|
47
|
-
<% domainEvents.forEach(event => {
|
|
108
|
+
<% } -%>
|
|
109
|
+
<% domainEvents.forEach(event => {
|
|
110
|
+
const _eventNotifies = (typeof temporal !== 'undefined' && temporal)
|
|
111
|
+
? (event.notifies || []).filter(n => n.workflow)
|
|
112
|
+
: [];
|
|
113
|
+
const _hasBrokerAction = !!broker;
|
|
114
|
+
const _hasWorkflowAction = _eventNotifies.length > 0;
|
|
115
|
+
-%>
|
|
48
116
|
|
|
49
117
|
/**
|
|
50
|
-
* Handles {@link <%= event.name %>} after the wrapping transaction commits.<% if (!
|
|
118
|
+
* Handles {@link <%= event.name %>} after the wrapping transaction commits.<% if (!_hasBrokerAction && !_hasWorkflowAction) { %>
|
|
51
119
|
* <p>
|
|
52
120
|
* TODO: Implement the side effect for this event (e.g., send notification,
|
|
53
121
|
* update a read model, trigger a saga step, etc.).<% } %>
|
|
54
122
|
*/
|
|
55
123
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
|
56
124
|
public void on<%= event.name %>(<%= event.name %> event) {
|
|
57
|
-
|
|
58
|
-
<%
|
|
125
|
+
<% if (_hasBrokerAction) {
|
|
59
126
|
const _aggrIdField = aggregateName.charAt(0).toLowerCase() + aggregateName.slice(1) + 'Id';
|
|
60
|
-
|
|
127
|
+
-%>
|
|
61
128
|
messageBroker.publish<%= event.integrationEventClassName %>(new <%= event.integrationEventClassName %>(<% if (event.fields && event.fields.length > 0) { event.fields.forEach(function(field, idx) { %><%= field.name === _aggrIdField ? 'event.getAggregateId()' : 'event.get' + field.name.charAt(0).toUpperCase() + field.name.slice(1) + '()' %><%= idx < event.fields.length - 1 ? ', ' : '' %><% }); } %>));
|
|
62
|
-
|
|
129
|
+
<% } -%>
|
|
130
|
+
<% if (_hasWorkflowAction) { _eventNotifies.forEach(n => {
|
|
131
|
+
const _svcName = n.flowPascal.charAt(0).toLowerCase() + n.flowPascal.slice(1) + 'WorkFlowService';
|
|
132
|
+
const _aggrIdFieldWf = aggregateName.charAt(0).toLowerCase() + aggregateName.slice(1) + 'Id';
|
|
133
|
+
const _eventFieldNames = new Set((event.fields || []).map(f => f.name));
|
|
134
|
+
const _hasTypedInput = n.inputFields && n.inputFields.length > 0;
|
|
135
|
+
-%>
|
|
136
|
+
<% if (_hasTypedInput) { -%>
|
|
137
|
+
<%= _svcName %>.startAsync(
|
|
138
|
+
event.getAggregateId(),
|
|
139
|
+
new <%= n.flowPascal %>Input(<% n.inputFields.forEach(function(inputF, idx) {
|
|
140
|
+
let _arg;
|
|
141
|
+
if (inputF.name === _aggrIdFieldWf) {
|
|
142
|
+
_arg = 'event.getAggregateId()';
|
|
143
|
+
} else if (_eventFieldNames.has(inputF.name)) {
|
|
144
|
+
_arg = 'event.get' + inputF.name.charAt(0).toUpperCase() + inputF.name.slice(1) + '()';
|
|
145
|
+
} else if (inputF.name.endsWith('At') && inputF.javaType === 'LocalDateTime') {
|
|
146
|
+
_arg = 'LocalDateTime.now()';
|
|
147
|
+
} else {
|
|
148
|
+
_arg = 'null /* TODO: provide ' + inputF.name + ' */';
|
|
149
|
+
}
|
|
150
|
+
%><%= idx > 0 ? ', ' : '' %><%= _arg %><% }); %>)
|
|
151
|
+
);
|
|
152
|
+
<% } else { -%>
|
|
153
|
+
<%= _svcName %>.startAsync(event.getAggregateId());
|
|
154
|
+
<% } -%>
|
|
155
|
+
<% }); } -%>
|
|
156
|
+
<% if (!_hasBrokerAction && !_hasWorkflowAction) { -%>
|
|
63
157
|
// TODO: handle <%= event.name %> — add your side-effect logic here
|
|
64
158
|
// e.g.: notificationService.notify(event);
|
|
65
159
|
// readModelUpdater.on(event);
|
|
66
|
-
|
|
160
|
+
<% } -%>
|
|
67
161
|
}
|
|
68
162
|
<% }); %>
|
|
69
163
|
}
|