eva4j 1.0.13 → 1.0.15
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 +314 -10
- package/COMMAND_EVALUATION.md +15 -16
- package/DOMAIN_YAML_GUIDE.md +576 -10
- package/FUTURE_FEATURES.md +1627 -1168
- package/README.md +318 -13
- package/bin/eva4j.js +34 -0
- package/config/defaults.json +1 -0
- package/design-system.md +797 -0
- package/docs/commands/EVALUATE_SYSTEM.md +994 -0
- package/docs/commands/GENERATE_ENTITIES.md +795 -6
- package/docs/commands/INDEX.md +10 -1
- package/examples/domain-endpoints-relations.yaml +353 -0
- package/examples/domain-endpoints-versioned.yaml +144 -0
- package/examples/domain-endpoints.yaml +135 -0
- package/examples/domain-events.yaml +166 -20
- package/examples/domain-listeners.yaml +212 -0
- package/examples/domain-one-to-many.yaml +1 -0
- package/examples/domain-one-to-one.yaml +1 -0
- package/examples/domain-ports.yaml +414 -0
- package/examples/domain-soft-delete.yaml +47 -44
- package/examples/system/notification.yaml +147 -0
- package/examples/system/product.yaml +185 -0
- package/examples/system/system.yaml +112 -0
- package/examples/system-report.html +971 -0
- package/examples/system.yaml +332 -0
- package/package.json +2 -1
- package/src/commands/build.js +714 -0
- package/src/commands/create.js +7 -3
- package/src/commands/detach.js +1 -0
- package/src/commands/evaluate-system.js +610 -0
- package/src/commands/generate-entities.js +1331 -49
- package/src/commands/generate-http-exchange.js +2 -0
- package/src/commands/generate-kafka-event.js +98 -11
- package/src/generators/base-generator.js +8 -1
- package/src/generators/postman-generator.js +188 -0
- package/src/generators/shared-generator.js +10 -0
- package/src/utils/config-manager.js +54 -0
- package/src/utils/context-builder.js +1 -0
- package/src/utils/domain-diagram.js +192 -0
- package/src/utils/domain-validator.js +970 -0
- package/src/utils/fake-data.js +376 -0
- package/src/utils/naming.js +3 -2
- package/src/utils/system-validator.js +434 -0
- package/src/utils/yaml-to-entity.js +302 -8
- package/templates/aggregate/AggregateMapper.java.ejs +3 -2
- package/templates/aggregate/AggregateRepository.java.ejs +8 -2
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +13 -3
- package/templates/aggregate/AggregateRoot.java.ejs +60 -2
- package/templates/aggregate/DomainEventHandler.java.ejs +27 -20
- package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
- package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
- package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
- package/templates/aggregate/JpaRepository.java.ejs +5 -0
- package/templates/base/gradle/build.gradle.ejs +3 -2
- package/templates/base/root/AGENTS.md.ejs +306 -45
- package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1663 -0
- package/templates/base/root/skill-build-system-yaml.ejs +1446 -0
- package/templates/base/root/system.yaml.ejs +97 -0
- package/templates/crud/ApplicationMapper.java.ejs +4 -0
- package/templates/crud/Controller.java.ejs +4 -4
- package/templates/crud/CreateCommand.java.ejs +4 -0
- package/templates/crud/CreateItemDto.java.ejs +4 -0
- package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
- package/templates/crud/DeleteCommandHandler.java.ejs +10 -2
- package/templates/crud/EndpointsController.java.ejs +178 -0
- package/templates/crud/FindByQuery.java.ejs +17 -0
- package/templates/crud/FindByQueryHandler.java.ejs +57 -0
- package/templates/crud/ListQuery.java.ejs +1 -1
- package/templates/crud/ListQueryHandler.java.ejs +8 -8
- package/templates/crud/ScaffoldCommand.java.ejs +12 -0
- package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
- package/templates/crud/ScaffoldQuery.java.ejs +13 -0
- package/templates/crud/ScaffoldQueryHandler.java.ejs +41 -0
- package/templates/crud/SubEntityAddCommand.java.ejs +21 -0
- package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
- package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
- package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
- package/templates/crud/TransitionCommand.java.ejs +9 -0
- package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
- package/templates/crud/UpdateCommand.java.ejs +4 -0
- package/templates/evaluate/report.html.ejs +1363 -0
- package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
- package/templates/kafka-event/Event.java.ejs +16 -0
- package/templates/kafka-listener/KafkaController.java.ejs +1 -1
- package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
- package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
- package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
- package/templates/kafka-listener/ListenerCommandHandler.java.ejs +23 -0
- package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
- package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
- package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
- package/templates/mock/MockEvent.java.ejs +10 -0
- package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
- package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
- package/templates/mock/SpringEventListener.java.ejs +61 -0
- package/templates/ports/PortDomainModel.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +67 -0
- package/templates/ports/PortFeignClient.java.ejs +45 -0
- package/templates/ports/PortFeignConfig.java.ejs +24 -0
- package/templates/ports/PortInterface.java.ejs +45 -0
- package/templates/ports/PortNestedType.java.ejs +28 -0
- package/templates/ports/PortRequestDto.java.ejs +30 -0
- package/templates/ports/PortResponseDto.java.ejs +28 -0
- package/templates/postman/Collection.json.ejs +1 -1
- package/templates/postman/UnifiedCollection.json.ejs +185 -0
- package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const yaml = require('js-yaml');
|
|
2
2
|
const fs = require('fs-extra');
|
|
3
3
|
const pluralize = require('pluralize');
|
|
4
|
-
const { toPascalCase, toCamelCase, toSnakeCase } = require('./naming');
|
|
4
|
+
const { toPascalCase, toCamelCase, toSnakeCase, toKebabCase } = require('./naming');
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Parse domain.yaml and extract aggregates with entities and value objects
|
|
@@ -23,10 +23,38 @@ async function parseDomainYaml(yamlPath, packageName = '', moduleName = '') {
|
|
|
23
23
|
packageName,
|
|
24
24
|
moduleName
|
|
25
25
|
}));
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
const endpoints = parseEndpoints(domainData);
|
|
28
|
+
const listeners = parseListeners(domainData);
|
|
29
|
+
|
|
30
|
+
// ── C2-006: useCase name collision between endpoints and listeners ─────────
|
|
31
|
+
// Both sections generate a "{UseCase}Command.java". The generator processes
|
|
32
|
+
// listeners first, then endpoints — the endpoint run silently overwrites the
|
|
33
|
+
// listener command, leaving the KafkaListener dispatching a constructor that
|
|
34
|
+
// no longer exists → compile error.
|
|
35
|
+
const endpointUseCases = new Set(
|
|
36
|
+
endpoints ? endpoints.versions.flatMap(v => v.operations.map(op => op.useCase)) : []
|
|
37
|
+
);
|
|
38
|
+
const collisions = listeners
|
|
39
|
+
.map(l => l.useCase)
|
|
40
|
+
.filter(uc => endpointUseCases.has(uc));
|
|
41
|
+
if (collisions.length > 0) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`[C2-006] useCase name collision in domain.yaml:\n` +
|
|
44
|
+
collisions.map(uc =>
|
|
45
|
+
` - "${uc}" appears in both endpoints: (operation) and listeners: (useCase).\n` +
|
|
46
|
+
` Both would generate "${uc}Command.java" — the endpoint version overwrites the listener version.\n` +
|
|
47
|
+
` Fix: rename the listener useCase, e.g. "${uc.replace(/^Create/, 'Initialize')}".`
|
|
48
|
+
).join('\n')
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
return {
|
|
28
53
|
aggregates,
|
|
29
|
-
allEnums: extractAllEnums(domainData.aggregates)
|
|
54
|
+
allEnums: extractAllEnums(domainData.aggregates),
|
|
55
|
+
endpoints,
|
|
56
|
+
listeners,
|
|
57
|
+
ports: parsePorts(domainData, moduleName)
|
|
30
58
|
};
|
|
31
59
|
}
|
|
32
60
|
|
|
@@ -79,10 +107,21 @@ function parseAggregate(aggregateData) {
|
|
|
79
107
|
return {
|
|
80
108
|
name: eventName,
|
|
81
109
|
fieldName: toCamelCase(eventName),
|
|
82
|
-
fields: eventFields
|
|
110
|
+
fields: eventFields,
|
|
111
|
+
triggers: event.triggers || []
|
|
83
112
|
};
|
|
84
113
|
});
|
|
85
114
|
|
|
115
|
+
// Build inverse map: { methodName → [event, ...] }
|
|
116
|
+
// Used by the AggregateRoot template to emit raise() calls inside transition methods.
|
|
117
|
+
const triggeredEventsMap = {};
|
|
118
|
+
domainEvents.forEach(event => {
|
|
119
|
+
(event.triggers || []).forEach(method => {
|
|
120
|
+
if (!triggeredEventsMap[method]) triggeredEventsMap[method] = [];
|
|
121
|
+
triggeredEventsMap[method].push(event);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
86
125
|
return {
|
|
87
126
|
name: toPascalCase(name),
|
|
88
127
|
packageName: aggregateData.package || '',
|
|
@@ -91,7 +130,9 @@ function parseAggregate(aggregateData) {
|
|
|
91
130
|
valueObjects: parsedValueObjects,
|
|
92
131
|
aggregateMethods,
|
|
93
132
|
allEntities: parsedEntities,
|
|
94
|
-
domainEvents
|
|
133
|
+
domainEvents,
|
|
134
|
+
triggeredEventsMap,
|
|
135
|
+
enums: aggregateEnums
|
|
95
136
|
};
|
|
96
137
|
}
|
|
97
138
|
|
|
@@ -107,8 +148,16 @@ function parseAggregate(aggregateData) {
|
|
|
107
148
|
* @returns {Object} Parsed entity
|
|
108
149
|
*/
|
|
109
150
|
function parseEntity(entityData, aggregateName, packageName = '', moduleName = '', aggregateEnums = [], valueObjectNames = [], inverseRelationships = {}) {
|
|
110
|
-
const { name, isRoot = false, tableName, properties, fields: fieldsYaml, relationships = [], auditable = false, audit } = entityData;
|
|
151
|
+
const { name, isRoot = false, tableName, properties, fields: fieldsYaml, relationships = [], auditable = false, audit, hasSoftDelete = false } = entityData;
|
|
111
152
|
|
|
153
|
+
// Validate hasSoftDelete
|
|
154
|
+
if (hasSoftDelete !== undefined && typeof hasSoftDelete !== 'boolean') {
|
|
155
|
+
throw new Error(`Entity "${name}": hasSoftDelete must be a boolean (true/false)`);
|
|
156
|
+
}
|
|
157
|
+
if (hasSoftDelete === true && isRoot === false) {
|
|
158
|
+
console.warn(`⚠️ Entity "${name}": hasSoftDelete is only supported on the aggregate root (isRoot: true). It will be ignored for secondary entities.`);
|
|
159
|
+
}
|
|
160
|
+
|
|
112
161
|
// Accept both 'properties' and 'fields' field names
|
|
113
162
|
let entityFields = properties || fieldsYaml || [];
|
|
114
163
|
|
|
@@ -172,6 +221,15 @@ function parseEntity(entityData, aggregateName, packageName = '', moduleName = '
|
|
|
172
221
|
];
|
|
173
222
|
}
|
|
174
223
|
}
|
|
224
|
+
|
|
225
|
+
// Inject deletedAt field for soft-delete root entities
|
|
226
|
+
const effectiveSoftDelete = hasSoftDelete === true && isRoot === true;
|
|
227
|
+
if (effectiveSoftDelete) {
|
|
228
|
+
entityFields = [
|
|
229
|
+
...entityFields,
|
|
230
|
+
{ name: 'deletedAt', type: 'LocalDateTime' }
|
|
231
|
+
];
|
|
232
|
+
}
|
|
175
233
|
|
|
176
234
|
const className = toPascalCase(name);
|
|
177
235
|
const fieldName = toCamelCase(name);
|
|
@@ -204,6 +262,7 @@ function parseEntity(entityData, aggregateName, packageName = '', moduleName = '
|
|
|
204
262
|
fieldName,
|
|
205
263
|
tableName: table,
|
|
206
264
|
isRoot,
|
|
265
|
+
hasSoftDelete: effectiveSoftDelete,
|
|
207
266
|
auditable: auditable === true, // Legacy support
|
|
208
267
|
audit: auditConfig, // New audit configuration
|
|
209
268
|
fields,
|
|
@@ -233,7 +292,7 @@ function buildAnnotationString(validation) {
|
|
|
233
292
|
if (value !== undefined) params.push(`value = ${value}`);
|
|
234
293
|
if (min !== undefined) params.push(`min = ${min}`);
|
|
235
294
|
if (max !== undefined) params.push(`max = ${max}`);
|
|
236
|
-
if (regexp !== undefined) params.push(`regexp = "${regexp}"`);
|
|
295
|
+
if (regexp !== undefined) params.push(`regexp = "${regexp.replace(/\\/g, '\\\\')}"`);
|
|
237
296
|
if (integer !== undefined) params.push(`integer = ${integer}`);
|
|
238
297
|
if (fraction !== undefined) params.push(`fraction = ${fraction}`);
|
|
239
298
|
if (inclusive !== undefined) params.push(`inclusive = ${inclusive}`);
|
|
@@ -961,6 +1020,239 @@ function extractAllEnums(aggregates) {
|
|
|
961
1020
|
return Array.from(enumsMap.values());
|
|
962
1021
|
}
|
|
963
1022
|
|
|
1023
|
+
/**
|
|
1024
|
+
* Parse the optional endpoints section from domain.yaml.
|
|
1025
|
+
* When present, controls which use cases and versioned controllers are generated.
|
|
1026
|
+
* @param {Object} domainData - Raw parsed YAML data
|
|
1027
|
+
* @returns {Object|null} Parsed endpoints structure or null if not declared
|
|
1028
|
+
*/
|
|
1029
|
+
function parseEndpoints(domainData) {
|
|
1030
|
+
if (!domainData.endpoints) return null;
|
|
1031
|
+
const { basePath = '/', versions = [] } = domainData.endpoints;
|
|
1032
|
+
return {
|
|
1033
|
+
basePath,
|
|
1034
|
+
versions: versions.map(v => ({
|
|
1035
|
+
version: v.version,
|
|
1036
|
+
operations: (v.operations || []).map(op => {
|
|
1037
|
+
const method = (op.method || 'GET').toUpperCase();
|
|
1038
|
+
return {
|
|
1039
|
+
method,
|
|
1040
|
+
path: op.path || '/',
|
|
1041
|
+
description: op.description || '',
|
|
1042
|
+
useCase: toPascalCase(op.useCase),
|
|
1043
|
+
type: method === 'GET' ? 'query' : 'command'
|
|
1044
|
+
};
|
|
1045
|
+
})
|
|
1046
|
+
}))
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Parse the optional listeners section from domain.yaml.
|
|
1052
|
+
* Declares integration events this module CONSUMES from external producers.
|
|
1053
|
+
* @param {Object} domainData - Raw parsed YAML data
|
|
1054
|
+
* @returns {Array} Parsed listeners array (empty if not declared)
|
|
1055
|
+
*/
|
|
1056
|
+
function parseListeners(domainData) {
|
|
1057
|
+
if (!domainData.listeners || !Array.isArray(domainData.listeners)) return [];
|
|
1058
|
+
return domainData.listeners.map(listener => {
|
|
1059
|
+
const eventName = toPascalCase(listener.event);
|
|
1060
|
+
// Normalise: strip trailing 'Event' suffix for class naming, re-add it consistently
|
|
1061
|
+
const baseName = eventName.endsWith('Event') ? eventName.slice(0, -5) : eventName;
|
|
1062
|
+
const integrationEventClassName = `${baseName}IntegrationEvent`;
|
|
1063
|
+
// e.g. PaymentApprovedKafkaListener
|
|
1064
|
+
const listenerClassName = `${baseName}KafkaListener`;
|
|
1065
|
+
const useCaseName = toPascalCase(listener.useCase);
|
|
1066
|
+
const commandClassName = `${useCaseName}Command`;
|
|
1067
|
+
const topic = listener.topic || null;
|
|
1068
|
+
const fields = (listener.fields || []).map(f => ({
|
|
1069
|
+
name: toCamelCase(f.name),
|
|
1070
|
+
javaType: f.type
|
|
1071
|
+
}));
|
|
1072
|
+
const nestedTypes = (listener.nestedTypes || []).map(nt => ({
|
|
1073
|
+
name: toPascalCase(nt.name),
|
|
1074
|
+
fields: (nt.fields || []).map(f => ({
|
|
1075
|
+
name: toCamelCase(f.name),
|
|
1076
|
+
javaType: f.type
|
|
1077
|
+
}))
|
|
1078
|
+
}));
|
|
1079
|
+
return {
|
|
1080
|
+
event: eventName,
|
|
1081
|
+
baseName,
|
|
1082
|
+
producer: listener.producer || null,
|
|
1083
|
+
topic,
|
|
1084
|
+
useCase: useCaseName,
|
|
1085
|
+
commandClassName,
|
|
1086
|
+
integrationEventClassName,
|
|
1087
|
+
listenerClassName,
|
|
1088
|
+
fields,
|
|
1089
|
+
nestedTypes
|
|
1090
|
+
};
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Derive a domain model type name from a method name.
|
|
1096
|
+
* Strips common verb prefixes and 'ById/ByName/...' suffixes so that
|
|
1097
|
+
* 'findCustomerById' → 'Customer', 'processPayment' → 'Payment'.
|
|
1098
|
+
* @param {string} methodName camelCase method name
|
|
1099
|
+
* @returns {string} PascalCase domain model name
|
|
1100
|
+
*/
|
|
1101
|
+
function deriveDomainType(methodName) {
|
|
1102
|
+
let name = toPascalCase(methodName);
|
|
1103
|
+
|
|
1104
|
+
// Strip verb prefix
|
|
1105
|
+
name = name.replace(
|
|
1106
|
+
/^(Find|Get|Fetch|Search|Retrieve|List|Check|Process|Create|Update|Delete|Cancel|Submit|Execute)/,
|
|
1107
|
+
''
|
|
1108
|
+
);
|
|
1109
|
+
|
|
1110
|
+
// Strip trailing 'By{Something}' (e.g. ById, ByName, ByCode)
|
|
1111
|
+
name = name.replace(/By[A-Z][a-zA-Z0-9]*$/, '');
|
|
1112
|
+
|
|
1113
|
+
// Strip common informational suffixes
|
|
1114
|
+
name = name.replace(/(?:Status|Availability|All)$/, '');
|
|
1115
|
+
|
|
1116
|
+
// Fall back to full PascalCase method name if we stripped everything
|
|
1117
|
+
return name || toPascalCase(methodName);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Parse the optional ports section from domain.yaml.
|
|
1122
|
+
* Declares HTTP services this module CALLS synchronously (Feign clients).
|
|
1123
|
+
* Entries sharing the same service: are grouped into a single FeignClient.
|
|
1124
|
+
* @param {Object} domainData - Raw parsed YAML data
|
|
1125
|
+
* @param {string} moduleName - Module name (used for property key naming)
|
|
1126
|
+
* @returns {Array} Parsed port service groups (empty if not declared)
|
|
1127
|
+
*/
|
|
1128
|
+
function parsePorts(domainData, moduleName = '') {
|
|
1129
|
+
if (!domainData.ports || !Array.isArray(domainData.ports)) return [];
|
|
1130
|
+
|
|
1131
|
+
const serviceMap = new Map();
|
|
1132
|
+
|
|
1133
|
+
for (const entry of domainData.ports) {
|
|
1134
|
+
if (!entry.service || !entry.name) continue;
|
|
1135
|
+
|
|
1136
|
+
const serviceName = toPascalCase(entry.service);
|
|
1137
|
+
const methodName = toCamelCase(entry.name);
|
|
1138
|
+
const methodPascal = toPascalCase(entry.name);
|
|
1139
|
+
|
|
1140
|
+
// Parse HTTP verb + path
|
|
1141
|
+
const httpParts = (entry.http || 'GET /').trim().split(/\s+/);
|
|
1142
|
+
const httpVerb = (httpParts[0] || 'GET').toUpperCase();
|
|
1143
|
+
const httpPath = httpParts[1] || '/';
|
|
1144
|
+
|
|
1145
|
+
// Extract path variables: /screenings/{id}/seats → ['id']
|
|
1146
|
+
const pathVarMatches = httpPath.match(/\{(\w+)\}/g) || [];
|
|
1147
|
+
const pathVariables = pathVarMatches.map(pv => pv.slice(1, -1));
|
|
1148
|
+
|
|
1149
|
+
// Response fields
|
|
1150
|
+
const responseFields = (entry.fields || []).map(f => ({
|
|
1151
|
+
name: toCamelCase(f.name),
|
|
1152
|
+
javaType: f.type
|
|
1153
|
+
}));
|
|
1154
|
+
|
|
1155
|
+
// Body fields (POST/PUT/PATCH only)
|
|
1156
|
+
const bodyAllowed = httpVerb !== 'GET' && httpVerb !== 'DELETE';
|
|
1157
|
+
const bodyFields = bodyAllowed
|
|
1158
|
+
? (entry.body || []).map(f => ({ name: toCamelCase(f.name), javaType: f.type }))
|
|
1159
|
+
: [];
|
|
1160
|
+
|
|
1161
|
+
// nestedTypes per method
|
|
1162
|
+
const nestedTypes = (entry.nestedTypes || []).map(nt => ({
|
|
1163
|
+
name: toPascalCase(nt.name),
|
|
1164
|
+
fields: (nt.fields || []).map(f => ({
|
|
1165
|
+
name: toCamelCase(f.name),
|
|
1166
|
+
javaType: f.type
|
|
1167
|
+
}))
|
|
1168
|
+
}));
|
|
1169
|
+
|
|
1170
|
+
const returnList = entry.returnList === true;
|
|
1171
|
+
const hasResponse = responseFields.length > 0;
|
|
1172
|
+
const hasBody = bodyFields.length > 0;
|
|
1173
|
+
// ACL: infra DTO name (lives in infrastructure/adapters/{service}/)
|
|
1174
|
+
const infraDtoName = hasResponse ? `${methodPascal}Dto` : null;
|
|
1175
|
+
// Domain model type (lives in domain/models/)
|
|
1176
|
+
const domainType = hasResponse
|
|
1177
|
+
? (entry.domainType ? toPascalCase(entry.domainType) : deriveDomainType(entry.name))
|
|
1178
|
+
: null;
|
|
1179
|
+
// Keep responseDtoName as alias to infraDtoName for backward-compat
|
|
1180
|
+
const responseDtoName = infraDtoName;
|
|
1181
|
+
const requestDtoName = hasBody ? `${methodPascal}RequestDto` : null;
|
|
1182
|
+
|
|
1183
|
+
const method = {
|
|
1184
|
+
name: methodName,
|
|
1185
|
+
namePascal: methodPascal,
|
|
1186
|
+
httpVerb,
|
|
1187
|
+
httpPath,
|
|
1188
|
+
pathVariables,
|
|
1189
|
+
fields: responseFields,
|
|
1190
|
+
bodyFields,
|
|
1191
|
+
nestedTypes,
|
|
1192
|
+
returnList,
|
|
1193
|
+
hasResponse,
|
|
1194
|
+
hasBody,
|
|
1195
|
+
infraDtoName,
|
|
1196
|
+
domainType,
|
|
1197
|
+
responseDtoName,
|
|
1198
|
+
requestDtoName
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
if (!serviceMap.has(serviceName)) {
|
|
1202
|
+
serviceMap.set(serviceName, {
|
|
1203
|
+
serviceName,
|
|
1204
|
+
serviceNameCamelCase: toCamelCase(serviceName),
|
|
1205
|
+
target: entry.target || null,
|
|
1206
|
+
baseUrl: entry.baseUrl || null,
|
|
1207
|
+
methods: [],
|
|
1208
|
+
nestedTypes: [],
|
|
1209
|
+
domainModels: [] // ACL: unique domain models per service group
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const group = serviceMap.get(serviceName);
|
|
1214
|
+
group.methods.push(method);
|
|
1215
|
+
|
|
1216
|
+
// Keep baseUrl from the first entry that declares it
|
|
1217
|
+
if (entry.baseUrl && !group.baseUrl) {
|
|
1218
|
+
group.baseUrl = entry.baseUrl;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Deduplicate nestedTypes within the service group
|
|
1222
|
+
for (const nt of nestedTypes) {
|
|
1223
|
+
if (!group.nestedTypes.some(existing => existing.name === nt.name)) {
|
|
1224
|
+
group.nestedTypes.push(nt);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// ACL: collect unique domain models (by name) across all methods in this service
|
|
1229
|
+
if (method.domainType && method.hasResponse) {
|
|
1230
|
+
if (!group.domainModels.some(dm => dm.name === method.domainType)) {
|
|
1231
|
+
group.domainModels.push({
|
|
1232
|
+
name: method.domainType,
|
|
1233
|
+
fields: method.fields
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const moduleKebab = toKebabCase(moduleName);
|
|
1240
|
+
|
|
1241
|
+
return Array.from(serviceMap.values()).map(group => {
|
|
1242
|
+
const serviceKebab = toKebabCase(group.serviceName);
|
|
1243
|
+
return {
|
|
1244
|
+
...group,
|
|
1245
|
+
baseUrl: group.baseUrl || 'http://localhost:8080',
|
|
1246
|
+
baseUrlProperty: `${moduleKebab}.${serviceKebab}.base-url`,
|
|
1247
|
+
feignClientName: `${moduleKebab}-${serviceKebab}`,
|
|
1248
|
+
feignClientClassName: `${group.serviceName}FeignClient`,
|
|
1249
|
+
feignAdapterClassName: `${group.serviceName}FeignAdapter`,
|
|
1250
|
+
feignConfigClassName: `${group.serviceName}FeignConfig`,
|
|
1251
|
+
adapterPackage: group.serviceNameCamelCase
|
|
1252
|
+
};
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
|
|
964
1256
|
module.exports = {
|
|
965
1257
|
parseDomainYaml,
|
|
966
1258
|
parseAggregate,
|
|
@@ -969,5 +1261,7 @@ module.exports = {
|
|
|
969
1261
|
generateAggregateMethods,
|
|
970
1262
|
generateEntityImports,
|
|
971
1263
|
generateValidationImports,
|
|
972
|
-
generateAggregateMethodImports
|
|
1264
|
+
generateAggregateMethodImports,
|
|
1265
|
+
parseListeners,
|
|
1266
|
+
parsePorts
|
|
973
1267
|
};
|
|
@@ -82,8 +82,9 @@ public class <%= aggregateName %>Mapper {
|
|
|
82
82
|
<% } else if (!rel.isCollection && rel.type === 'OneToOne') { %>
|
|
83
83
|
// Map OneToOne relationship <%= rel.fieldName %>
|
|
84
84
|
if (domain.get<%= rel.fieldName.charAt(0).toUpperCase() + rel.fieldName.slice(1) %>() != null) {
|
|
85
|
-
<%= rel.target %>Jpa <%= rel.fieldName %>Jpa = toJpa<%= rel.target %>(domain.get<%= rel.fieldName.charAt(0).toUpperCase() + rel.fieldName.slice(1) %>()
|
|
86
|
-
|
|
85
|
+
<%= rel.target %>Jpa <%= rel.fieldName %>Jpa = toJpa<%= rel.target %>(domain.get<%= rel.fieldName.charAt(0).toUpperCase() + rel.fieldName.slice(1) %>());
|
|
86
|
+
<% if (rel.mappedBy) { %> <%= rel.fieldName %>Jpa.set<%= rel.mappedBy.charAt(0).toUpperCase() + rel.mappedBy.slice(1) %>(jpa);
|
|
87
|
+
<% } %> jpa.set<%= rel.fieldName.charAt(0).toUpperCase() + rel.fieldName.slice(1) %>(<%= rel.fieldName %>Jpa);
|
|
87
88
|
}
|
|
88
89
|
<% } else if (!rel.isCollection) { %>
|
|
89
90
|
jpa.set<%= rel.fieldName.charAt(0).toUpperCase() + rel.fieldName.slice(1) %>(toJpa<%= rel.target %>(domain.get<%= rel.fieldName.charAt(0).toUpperCase() + rel.fieldName.slice(1) %>()));
|
|
@@ -17,7 +17,13 @@ public interface <%= rootEntity.name %>Repository {
|
|
|
17
17
|
|
|
18
18
|
Page<<%= rootEntity.name %>> findAll(Pageable pageable);
|
|
19
19
|
|
|
20
|
-
void deleteById(<%= rootEntity.fields[0].javaType %> id);
|
|
21
|
-
|
|
22
20
|
boolean existsById(<%= rootEntity.fields[0].javaType %> id);
|
|
21
|
+
<% if (!hasSoftDelete) { %>
|
|
22
|
+
void deleteById(<%= rootEntity.fields[0].javaType %> id);
|
|
23
|
+
<% } %>
|
|
24
|
+
<% if (findByOps && findByOps.length > 0) { %>
|
|
25
|
+
<% findByOps.forEach(function(op) { %>
|
|
26
|
+
Page<<%= rootEntity.name %>> <%= op.jpaMethodName %>(<%= op.fieldJavaType %> <%= op.fieldName %>, Pageable pageable);
|
|
27
|
+
<% }); %>
|
|
28
|
+
<% } %>
|
|
23
29
|
}
|
|
@@ -49,13 +49,23 @@ public class <%= rootEntity.name %>RepositoryImpl implements <%= rootEntity.name
|
|
|
49
49
|
.map(mapper::toDomain);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
@Override
|
|
53
|
+
public boolean existsById(<%= rootEntity.fields[0].javaType %> id) {
|
|
54
|
+
return jpaRepository.existsById(id);
|
|
55
|
+
}
|
|
56
|
+
<% if (!hasSoftDelete) { %>
|
|
52
57
|
@Override
|
|
53
58
|
public void deleteById(<%= rootEntity.fields[0].javaType %> id) {
|
|
54
59
|
jpaRepository.deleteById(id);
|
|
55
60
|
}
|
|
56
|
-
|
|
61
|
+
<% } %>
|
|
62
|
+
<% if (findByOps && findByOps.length > 0) { %>
|
|
63
|
+
<% findByOps.forEach(function(op) { %>
|
|
57
64
|
@Override
|
|
58
|
-
public
|
|
59
|
-
return jpaRepository.
|
|
65
|
+
public Page<<%= rootEntity.name %>> <%= op.jpaMethodName %>(<%= op.fieldJavaType %> <%= op.fieldName %>, Pageable pageable) {
|
|
66
|
+
return jpaRepository.<%= op.jpaMethodName %>(<%= op.fieldName %>, pageable)
|
|
67
|
+
.map(mapper::toDomain);
|
|
60
68
|
}
|
|
69
|
+
<% }); %>
|
|
70
|
+
<% } %>
|
|
61
71
|
}
|
|
@@ -18,6 +18,14 @@ import <%= packageName %>.shared.domain.DomainEvent;
|
|
|
18
18
|
import <%= packageName %>.<%= moduleName %>.domain.models.events.<%= event.name %>;
|
|
19
19
|
<% }); %>
|
|
20
20
|
<% } %>
|
|
21
|
+
<%
|
|
22
|
+
const _needsLocalDateTimeImport = Object.values(triggeredEventsMap || {}).flat().some(function(ev) {
|
|
23
|
+
return (ev.fields || []).some(function(ef) { return ef.name.endsWith('At') && ef.javaType === 'LocalDateTime'; });
|
|
24
|
+
}) && !imports.some(function(imp) { return imp.includes('LocalDateTime'); });
|
|
25
|
+
%>
|
|
26
|
+
<% if (_needsLocalDateTimeImport) { %>
|
|
27
|
+
import java.time.LocalDateTime;
|
|
28
|
+
<% } %>
|
|
21
29
|
import java.util.ArrayList;
|
|
22
30
|
import java.util.Collections;
|
|
23
31
|
import java.util.List;
|
|
@@ -82,7 +90,7 @@ public class <%= name %> {
|
|
|
82
90
|
<% }); %>
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
<% const creationFields = fields.filter(f => f.name !== 'id' && f.name !== 'createdAt' && f.name !== 'updatedAt' && f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.readOnly && !f.autoInit); %>
|
|
93
|
+
<% 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); %>
|
|
86
94
|
<% const autoInitFields = fields.filter(f => f.autoInit); %>
|
|
87
95
|
<% const defaultValueFields = fields.filter(f => f.readOnly && !f.autoInit && f.javaDefaultValue); %>
|
|
88
96
|
<% if (creationFields.length > 0 || autoInitFields.length > 0 || defaultValueFields.length > 0) { %>
|
|
@@ -184,8 +192,40 @@ public class <%= name %> {
|
|
|
184
192
|
}
|
|
185
193
|
<% } %>
|
|
186
194
|
this.<%= field.name %> = this.<%= field.name %>.transitionTo(<%- field.javaType %>.<%= transition.to %>);
|
|
195
|
+
<%
|
|
196
|
+
const _triggeredEvents = (triggeredEventsMap || {})[methodName] || [];
|
|
197
|
+
_triggeredEvents.forEach(function(evt) {
|
|
198
|
+
const _entityBase = name.charAt(0).toLowerCase() + name.slice(1);
|
|
199
|
+
const _args = ['this.getId()'];
|
|
200
|
+
(evt.fields || []).forEach(function(ef) {
|
|
201
|
+
// Skip {entityName}Id — already provided as aggregateId in the DomainEvent constructor
|
|
202
|
+
if (ef.name === _entityBase + 'Id') { return; }
|
|
203
|
+
const _matched = fields.find(function(f) { return f.name === ef.name; });
|
|
204
|
+
if (_matched) {
|
|
205
|
+
if (_matched.javaType === ef.javaType) {
|
|
206
|
+
_args.push('this.get' + ef.name.charAt(0).toUpperCase() + ef.name.slice(1) + '()'); return;
|
|
207
|
+
}
|
|
208
|
+
// Type mismatch: entity field may be a VO wrapping the expected primitive/type
|
|
209
|
+
var _vo = (valueObjects || []).find(function(vo) { return vo.name === _matched.javaType; });
|
|
210
|
+
if (_vo) {
|
|
211
|
+
var _voSub = _vo.fields.find(function(voF) { return voF.name === ef.name && voF.javaType === ef.javaType; });
|
|
212
|
+
if (!_voSub) { _voSub = _vo.fields.find(function(voF) { return voF.javaType === ef.javaType; }); }
|
|
213
|
+
if (_voSub) {
|
|
214
|
+
var _oCap = ef.name.charAt(0).toUpperCase() + ef.name.slice(1);
|
|
215
|
+
var _sCap = _voSub.name.charAt(0).toUpperCase() + _voSub.name.slice(1);
|
|
216
|
+
_args.push('this.get' + _oCap + '().get' + _sCap + '()'); return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
_args.push('null /* TODO: provide ' + ef.name + ' (entity returns ' + _matched.javaType + ', expected ' + ef.javaType + ') */'); return;
|
|
220
|
+
}
|
|
221
|
+
if (ef.name.endsWith('At') && ef.javaType === 'LocalDateTime') { _args.push('LocalDateTime.now()'); return; }
|
|
222
|
+
_args.push('null /* TODO: provide ' + ef.name + ' */');
|
|
223
|
+
});
|
|
224
|
+
%>
|
|
225
|
+
raise(new <%= evt.name %>(<%= _args.join(', ') %>));
|
|
226
|
+
<% }); %>
|
|
187
227
|
}
|
|
188
|
-
<% })
|
|
228
|
+
<% });%>
|
|
189
229
|
|
|
190
230
|
// ─── State Query Helpers ─────────────────────────────────────────────────
|
|
191
231
|
<% meta.enumValues.forEach(value => { %>
|
|
@@ -202,4 +242,22 @@ public class <%= name %> {
|
|
|
202
242
|
<% }); %>
|
|
203
243
|
<% }); %>
|
|
204
244
|
<% } %>
|
|
245
|
+
<% if (hasSoftDelete) { %>
|
|
246
|
+
|
|
247
|
+
// ─── Soft Delete ─────────────────────────────────────────────────────────
|
|
248
|
+
/**
|
|
249
|
+
* Marks this entity as deleted by setting deletedAt timestamp.
|
|
250
|
+
* Use this instead of physical deletion to preserve records.
|
|
251
|
+
*/
|
|
252
|
+
public void softDelete() {
|
|
253
|
+
if (this.deletedAt != null) {
|
|
254
|
+
throw new IllegalStateException("<%= name %> is already deleted");
|
|
255
|
+
}
|
|
256
|
+
this.deletedAt = java.time.LocalDateTime.now();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
public boolean isDeleted() {
|
|
260
|
+
return this.deletedAt != null;
|
|
261
|
+
}
|
|
262
|
+
<% } %>
|
|
205
263
|
}
|
|
@@ -6,32 +6,37 @@ import org.springframework.transaction.event.TransactionalEventListener;
|
|
|
6
6
|
<% domainEvents.forEach(event => { %>
|
|
7
7
|
import <%= packageName %>.<%= moduleName %>.domain.models.events.<%= event.name %>;
|
|
8
8
|
<% }); %>
|
|
9
|
-
<% if (
|
|
10
|
-
|
|
9
|
+
<% if (broker) { %>
|
|
10
|
+
<% domainEvents.forEach(event => { %>
|
|
11
|
+
import <%= packageName %>.<%= moduleName %>.application.events.<%= event.integrationEventClassName %>;
|
|
12
|
+
<% }); %>
|
|
13
|
+
import <%= packageName %>.<%= moduleName %>.application.ports.MessageBroker;
|
|
11
14
|
<% } %>
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* <%= aggregateName %>DomainEventHandler — Domain Event Bridge
|
|
15
18
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
19
|
+
* Connects the internal Spring event bus (ApplicationEventPublisher) with the
|
|
20
|
+
* external messaging port (MessageBroker).
|
|
18
21
|
*
|
|
19
22
|
* Architecture:
|
|
20
23
|
* AggregateRepositoryImpl.save()
|
|
21
|
-
* → eventPublisher.publishEvent(domainEvent)
|
|
22
|
-
* → @TransactionalEventListener(AFTER_COMMIT)
|
|
23
|
-
* → messageBroker.
|
|
24
|
+
* → eventPublisher.publishEvent(domainEvent) [internal Spring bus]
|
|
25
|
+
* → @TransactionalEventListener(AFTER_COMMIT) [this class]
|
|
26
|
+
* → messageBroker.publish*(integrationEvent) [port — broker-agnostic]
|
|
24
27
|
*
|
|
25
|
-
* AFTER_COMMIT
|
|
26
|
-
* database transaction committed successfully, preventing ghost events
|
|
27
|
-
*
|
|
28
|
+
* AFTER_COMMIT guarantees that external events are published only when the
|
|
29
|
+
* database transaction committed successfully, preventing ghost events from
|
|
30
|
+
* rolled-back operations.
|
|
28
31
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
32
|
+
* Domain Events (domain/models/events/) — internal signals scoped to this bounded context.
|
|
33
|
+
* Integration Events (application/events/) — broker-facing projections; changing broker
|
|
34
|
+
* technology (Kafka → RabbitMQ → SNS) only requires changing the MessageBroker adapter.
|
|
35
|
+
* Domain Events — and therefore this class — never need modification.
|
|
31
36
|
*/
|
|
32
37
|
@ApplicationComponent
|
|
33
38
|
public class <%= aggregateName %>DomainEventHandler {
|
|
34
|
-
<% if (
|
|
39
|
+
<% if (broker) { %>
|
|
35
40
|
|
|
36
41
|
private final MessageBroker messageBroker;
|
|
37
42
|
|
|
@@ -42,16 +47,18 @@ public class <%= aggregateName %>DomainEventHandler {
|
|
|
42
47
|
<% domainEvents.forEach(event => { %>
|
|
43
48
|
|
|
44
49
|
/**
|
|
45
|
-
* Handles {@link <%= event.name %>} after the wrapping transaction commits
|
|
46
|
-
*
|
|
50
|
+
* Handles {@link <%= event.name %>} after the wrapping transaction commits.<% if (!broker) { %>
|
|
51
|
+
* <p>
|
|
47
52
|
* TODO: Implement the side effect for this event (e.g., send notification,
|
|
48
|
-
* update a read model, trigger a saga step, etc.)
|
|
49
|
-
* <% } %>
|
|
53
|
+
* update a read model, trigger a saga step, etc.).<% } %>
|
|
50
54
|
*/
|
|
51
55
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
|
52
|
-
public void on<%= event.name %>(<%=
|
|
53
|
-
<% if (
|
|
54
|
-
|
|
56
|
+
public void on<%= event.name %>(<%= event.name %> event) {
|
|
57
|
+
<% if (broker) { %>
|
|
58
|
+
<%
|
|
59
|
+
const _aggrIdField = aggregateName.charAt(0).toLowerCase() + aggregateName.slice(1) + 'Id';
|
|
60
|
+
%>
|
|
61
|
+
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 ? ', ' : '' %><% }); } %>));
|
|
55
62
|
<% } else { %>
|
|
56
63
|
// TODO: handle <%= event.name %> — add your side-effect logic here
|
|
57
64
|
// e.g.: notificationService.notify(event);
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
package <%= packageName %>.<%= moduleName %>.domain.models.events;
|
|
2
2
|
|
|
3
3
|
import <%= packageName %>.shared.domain.DomainEvent;
|
|
4
|
-
<%
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
<%
|
|
5
|
+
// Fields named {aggregateName}Id are excluded — the aggregate id is already
|
|
6
|
+
// available via getAggregateId() inherited from DomainEvent.
|
|
7
|
+
const _aggregateBase = aggregateName.charAt(0).toLowerCase() + aggregateName.slice(1);
|
|
8
|
+
const _ownFields = fields.filter(f => f.name !== _aggregateBase + 'Id');
|
|
9
|
+
%>
|
|
10
|
+
<% const needsBigDecimal = _ownFields.some(f => f.javaType === 'BigDecimal'); %>
|
|
11
|
+
<% const needsLocalDate = _ownFields.some(f => f.javaType === 'LocalDate' || f.javaType === 'LocalDateTime' || f.javaType === 'LocalTime'); %>
|
|
12
|
+
<% const needsUUID = _ownFields.some(f => f.javaType === 'UUID'); %>
|
|
13
|
+
<% const needsList = _ownFields.some(f => f.isCollection); %>
|
|
8
14
|
<% if (needsBigDecimal) { %>
|
|
9
15
|
import java.math.BigDecimal;
|
|
10
16
|
<% } %>
|
|
@@ -18,6 +24,15 @@ import java.util.UUID;
|
|
|
18
24
|
<% if (needsList) { %>
|
|
19
25
|
import java.util.List;
|
|
20
26
|
<% } %>
|
|
27
|
+
<%
|
|
28
|
+
const _STANDARD_DOMAIN_TYPES = new Set(['String','Integer','Long','Double','Float','Boolean','BigDecimal','LocalDate','LocalDateTime','LocalTime','Instant','UUID']);
|
|
29
|
+
const _customDomainElementTypes = _ownFields
|
|
30
|
+
? [...new Set(_ownFields.filter(f => f.isCollection && f.collectionElementType && !_STANDARD_DOMAIN_TYPES.has(f.collectionElementType)).map(f => f.collectionElementType))]
|
|
31
|
+
: [];
|
|
32
|
+
%>
|
|
33
|
+
<% _customDomainElementTypes.forEach(typeName => { %>
|
|
34
|
+
import <%= packageName %>.<%= moduleName %>.domain.models.events.<%- typeName %>;
|
|
35
|
+
<% }); %>
|
|
21
36
|
|
|
22
37
|
/**
|
|
23
38
|
* <%= name %> - Domain Event
|
|
@@ -25,23 +40,24 @@ import java.util.List;
|
|
|
25
40
|
* Raised when: TODO — describe the business fact this event represents.
|
|
26
41
|
* Aggregate: <%= aggregateName %>
|
|
27
42
|
*
|
|
43
|
+
* The aggregate id is available via {@link #getAggregateId()} (inherited from DomainEvent).
|
|
28
44
|
* This is a pure domain class. It carries no Spring or infrastructure dependencies.
|
|
29
45
|
* Publish it externally via <%= aggregateName %>DomainEventHandler (application layer).
|
|
30
46
|
*/
|
|
31
47
|
public final class <%= name %> extends DomainEvent {
|
|
32
48
|
|
|
33
|
-
<%
|
|
49
|
+
<% _ownFields.forEach(field => { %>
|
|
34
50
|
private final <%- field.javaType %> <%= field.name %>;
|
|
35
51
|
<% }); %>
|
|
36
52
|
|
|
37
|
-
public <%= name %>(String aggregateId<%
|
|
53
|
+
public <%= name %>(String aggregateId<% _ownFields.forEach(field => { %>, <%- field.javaType %> <%= field.name %><% }); %>) {
|
|
38
54
|
super(aggregateId);
|
|
39
|
-
<%
|
|
55
|
+
<% _ownFields.forEach(field => { %>
|
|
40
56
|
this.<%= field.name %> = <%= field.name %>;
|
|
41
57
|
<% }); %>
|
|
42
58
|
}
|
|
43
59
|
|
|
44
|
-
<%
|
|
60
|
+
<% _ownFields.forEach(field => { %>
|
|
45
61
|
public <%- field.javaType %> get<%= field.name.charAt(0).toUpperCase() + field.name.slice(1) %>() {
|
|
46
62
|
return <%= field.name %>;
|
|
47
63
|
}
|