eva4j 1.0.16 → 1.0.17
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 +218 -5
- package/DOMAIN_YAML_GUIDE.md +185 -2
- package/FUTURE_FEATURES.md +33 -52
- package/docs/commands/EVALUATE_SYSTEM.md +18 -2
- package/examples/domain-events.yaml +26 -0
- package/examples/domain-read-models.yaml +113 -0
- package/package.json +1 -1
- package/read-model-spec.md +664 -0
- package/src/agents/design-reviewer.agent.md +3 -0
- package/src/commands/generate-entities.js +254 -10
- 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-record.js +2 -2
- package/src/commands/generate-resource.js +4 -1
- package/src/commands/generate-temporal-activity.js +4 -1
- package/src/commands/generate-temporal-flow.js +4 -1
- package/src/commands/generate-usecase.js +4 -1
- package/src/skills/build-system-yaml/SKILL.md +122 -1
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +205 -24
- package/src/skills/build-system-yaml/references/module-spec.md +33 -1
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
- package/src/utils/config-manager.js +4 -2
- package/src/utils/domain-validator.js +230 -14
- package/src/utils/naming.js +10 -0
- package/src/utils/validator.js +3 -1
- package/src/utils/yaml-to-entity.js +272 -3
- 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/JpaAggregateRoot.java.ejs +2 -2
- package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
- package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
- 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 +11 -0
- package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
- package/templates/read-model/ReadModelRepository.java.ejs +42 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +81 -0
- package/templates/read-model/ReadModelSyncHandler.java.ejs +52 -0
- package/test-c2010.js +49 -0
- package/test-update-compat.js +109 -0
- package/test-update-lifecycle.js +121 -0
|
@@ -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() {
|
|
@@ -68,9 +68,9 @@ if (audit && audit.trackUser) {
|
|
|
68
68
|
private <%- field.javaTypeJpa %> <%= field.name %> = new ArrayList<>();
|
|
69
69
|
<% } else { %>
|
|
70
70
|
<% if (index === 0) { %>@Id
|
|
71
|
-
<% if (field.javaType === 'String') { %>@GeneratedValue(strategy = GenerationType.UUID)
|
|
71
|
+
<% if (!hasCreateLifecycle) { %><% if (field.javaType === 'String') { %>@GeneratedValue(strategy = GenerationType.UUID)
|
|
72
72
|
<% } else if (field.javaType === 'Long' || field.javaType === 'Integer') { %>@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
73
|
-
<% } %><% } %><% if (field.annotations.length > 0) { %><% field.annotations.forEach(annotation => { %><%= annotation %>
|
|
73
|
+
<% } %><% } %><% } %><% if (field.annotations.length > 0) { %><% field.annotations.forEach(annotation => { %><%= annotation %>
|
|
74
74
|
<% }); %><% } %><% if (field.isEmbedded || field.isValueObject) { %><%
|
|
75
75
|
// Find the Value Object definition to get its fields
|
|
76
76
|
const vo = valueObjects && valueObjects.find(v => v.name === field.javaType);
|
|
@@ -2,9 +2,20 @@ package <%= packageName %>.<%= moduleName %>.application.usecases;
|
|
|
2
2
|
|
|
3
3
|
import <%= packageName %>.<%= moduleName %>.application.commands.Delete<%= aggregateName %>Command;
|
|
4
4
|
import <%= packageName %>.<%= moduleName %>.domain.repositories.<%= aggregateName %>Repository;
|
|
5
|
-
<%
|
|
5
|
+
<% const _deleteLifecycleEvents = (lifecycleEventsMap || {}).delete || []; %>
|
|
6
|
+
<% const _softDeleteLifecycleEvents2 = (lifecycleEventsMap || {}).softDelete || []; %>
|
|
7
|
+
<% if (hasSoftDelete || _deleteLifecycleEvents.length > 0) { %>
|
|
6
8
|
import <%= packageName %>.<%= moduleName %>.domain.models.entities.<%= aggregateName %>;
|
|
7
9
|
<% } %>
|
|
10
|
+
<% if (_deleteLifecycleEvents.length > 0) { %>
|
|
11
|
+
import <%= packageName %>.shared.domain.DomainEvent;
|
|
12
|
+
<% _deleteLifecycleEvents.forEach(function(evt) { %>
|
|
13
|
+
import <%= packageName %>.<%= moduleName %>.domain.models.events.<%= evt.name %>;
|
|
14
|
+
<% }); %>
|
|
15
|
+
<% if (_deleteLifecycleEvents.some(function(evt) { return evt.needsLocalDateTime; })) { %>
|
|
16
|
+
import java.time.LocalDateTime;
|
|
17
|
+
<% } %>
|
|
18
|
+
<% } %>
|
|
8
19
|
import <%= packageName %>.shared.domain.annotations.ApplicationComponent;
|
|
9
20
|
import <%= packageName %>.shared.domain.annotations.LogExceptions;
|
|
10
21
|
import <%= packageName %>.shared.domain.customExceptions.NotFoundException;
|
|
@@ -33,6 +44,13 @@ public class Delete<%= aggregateName %>CommandHandler implements CommandHandler<
|
|
|
33
44
|
.orElseThrow(() -> new NotFoundException("<%= aggregateName %> not found with id: " + command.id()));
|
|
34
45
|
entity.softDelete();
|
|
35
46
|
repository.save(entity);
|
|
47
|
+
<% } else if (_deleteLifecycleEvents.length > 0) { %>
|
|
48
|
+
<%= aggregateName %> entity = repository.findById(command.id())
|
|
49
|
+
.orElseThrow(() -> new NotFoundException("<%= aggregateName %> not found with id: " + command.id()));
|
|
50
|
+
<% _deleteLifecycleEvents.forEach(function(evt) { %>
|
|
51
|
+
entity.raise(new <%= evt.name %>(<%= evt.resolvedArgs.join(', ') %>));
|
|
52
|
+
<% }); %>
|
|
53
|
+
repository.delete(entity);
|
|
36
54
|
<% } else { %>
|
|
37
55
|
if (!repository.existsById(command.id())) {
|
|
38
56
|
throw new NotFoundException("<%= aggregateName %> not found with id: " + command.id());
|
|
@@ -6,6 +6,7 @@ import <%= packageName %>.<%= moduleName %>.domain.repositories.<%= aggregateNam
|
|
|
6
6
|
<% if ((commandFields || []).some(f => f.originalVoType)) { %>
|
|
7
7
|
import <%= packageName %>.<%= moduleName %>.application.mappers.<%= aggregateName %>ApplicationMapper;
|
|
8
8
|
<% } %>
|
|
9
|
+
<% const _updateLifecycleEvents = (lifecycleEventsMap || {}).update || []; %>
|
|
9
10
|
import <%= packageName %>.shared.domain.annotations.ApplicationComponent;
|
|
10
11
|
import <%= packageName %>.shared.domain.annotations.LogExceptions;
|
|
11
12
|
import <%= packageName %>.shared.domain.customExceptions.NotFoundException;
|
|
@@ -15,8 +16,13 @@ import org.springframework.transaction.annotation.Transactional;
|
|
|
15
16
|
/**
|
|
16
17
|
* Update<%= aggregateName %>CommandHandler — PATCH
|
|
17
18
|
*
|
|
19
|
+
<% if (_updateLifecycleEvents.length > 0) { %>
|
|
20
|
+
* Delegates field updates to the aggregate root's update() method,
|
|
21
|
+
* which raises domain events internally.
|
|
22
|
+
<% } else { %>
|
|
18
23
|
* Reconstructs the <%= aggregateName %> aggregate using the full constructor,
|
|
19
24
|
* merging each command field with the existing entity value.
|
|
25
|
+
<% } %>
|
|
20
26
|
* Non-null command fields override the current value; null fields are preserved
|
|
21
27
|
* from the loaded entity. No setters are required on the domain entity.
|
|
22
28
|
*/
|
|
@@ -52,12 +58,56 @@ public class Update<%= aggregateName %>CommandHandler implements CommandHandler<
|
|
|
52
58
|
const cmdFieldMap = {};
|
|
53
59
|
(commandFields || []).forEach(f => { cmdFieldMap[f.name] = f; });
|
|
54
60
|
|
|
61
|
+
function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
|
|
62
|
+
|
|
63
|
+
// Updatable fields: same filter as creationFields (exclude id, audit, deletedAt, readOnly, autoInit)
|
|
64
|
+
const updatableFields = (rootEntity.fields || []).filter(f =>
|
|
65
|
+
f.name !== 'id' && f.name !== 'createdAt' && f.name !== 'updatedAt' &&
|
|
66
|
+
f.name !== 'createdBy' && f.name !== 'updatedBy' && f.name !== 'deletedAt' && !f.readOnly && !f.autoInit
|
|
67
|
+
);
|
|
68
|
+
%>
|
|
69
|
+
<% if (_updateLifecycleEvents.length > 0) { %>
|
|
70
|
+
// Merge: use command value when non-null, otherwise preserve existing value.
|
|
71
|
+
// The aggregate root's update() method raises domain events internally.
|
|
72
|
+
existing.update(
|
|
73
|
+
<% updatableFields.forEach((field, idx) => { %>
|
|
74
|
+
<% const cmdField = cmdFieldMap[field.name]; %>
|
|
75
|
+
<% const getter = `existing.get${capitalize(field.name)}()`; %>
|
|
76
|
+
<% const comma = idx < updatableFields.length - 1 ? ',' : ''; %>
|
|
77
|
+
<% if (cmdField && cmdField.originalVoType) { %>
|
|
78
|
+
command.<%= field.name %>() != null
|
|
79
|
+
? mapper.to<%= cmdField.originalVoType %>(command.<%= field.name %>())
|
|
80
|
+
: <%= getter %><%= comma %>
|
|
81
|
+
<% } else if (cmdField) { %>
|
|
82
|
+
command.<%= field.name %>() != null ? command.<%= field.name %>() : <%= getter %><%= comma %>
|
|
83
|
+
<% } else { %>
|
|
84
|
+
<%= getter %><%= comma %> // preserved: <%= field.name %>
|
|
85
|
+
<% } %>
|
|
86
|
+
<% }); %>
|
|
87
|
+
);
|
|
88
|
+
<% if ((oneToManyRelationships || []).length > 0) { %>
|
|
89
|
+
|
|
90
|
+
// TODO: Collection relationships require an explicit replacement strategy.
|
|
91
|
+
// Choose the right approach for your domain and uncomment:
|
|
92
|
+
//
|
|
93
|
+
// Option A — Clear-and-replace (simple, loses existing entity ids):
|
|
94
|
+
// existing.get<collection>().clear();
|
|
95
|
+
// command.<collection>().forEach(dto -> entity.add<Item>(...));
|
|
96
|
+
//
|
|
97
|
+
// Option B — Merge by id (preserves audit trail, more complex):
|
|
98
|
+
// reconcile existing items against command items by id
|
|
99
|
+
<% (oneToManyRelationships || []).forEach(rel => { %>
|
|
100
|
+
// if (command.<%= rel.fieldName %>() != null) { /* replace <%= rel.fieldName %> */ }
|
|
101
|
+
<% }); %>
|
|
102
|
+
<% } %>
|
|
103
|
+
|
|
104
|
+
repository.save(existing);
|
|
105
|
+
<% } else { %>
|
|
106
|
+
<%
|
|
55
107
|
// oneToOneRelationships are non-collection non-inverse rels → appear in the full constructor
|
|
56
108
|
const ctorRels = oneToOneRelationships || [];
|
|
57
109
|
const totalArgs = (rootEntity.fields || []).length + ctorRels.length;
|
|
58
110
|
let argIdx = 0;
|
|
59
|
-
|
|
60
|
-
function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
|
|
61
111
|
%>
|
|
62
112
|
// Merge: use command value when non-null, otherwise preserve existing value.
|
|
63
113
|
// The full constructor is called so no setters are needed on the domain entity.
|
|
@@ -98,5 +148,6 @@ public class Update<%= aggregateName %>CommandHandler implements CommandHandler<
|
|
|
98
148
|
<% } %>
|
|
99
149
|
|
|
100
150
|
repository.save(updated);
|
|
151
|
+
<% } %>
|
|
101
152
|
}
|
|
102
153
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.domain.models.readmodels;
|
|
2
|
+
<% const needsBigDecimal = fields && fields.some(f => f.javaType === 'BigDecimal'); %>
|
|
3
|
+
<% const needsLocalDate = fields && fields.some(f => ['LocalDate','LocalDateTime','LocalTime'].includes(f.javaType)); %>
|
|
4
|
+
<% const needsInstant = fields && fields.some(f => f.javaType === 'Instant'); %>
|
|
5
|
+
<% const needsUUID = fields && fields.some(f => f.javaType === 'UUID'); %>
|
|
6
|
+
<% if (needsBigDecimal) { %>
|
|
7
|
+
import java.math.BigDecimal;
|
|
8
|
+
<% } %>
|
|
9
|
+
<% if (needsLocalDate) { %>
|
|
10
|
+
import java.time.LocalDate;
|
|
11
|
+
import java.time.LocalDateTime;
|
|
12
|
+
import java.time.LocalTime;
|
|
13
|
+
<% } %>
|
|
14
|
+
<% if (needsInstant) { %>
|
|
15
|
+
import java.time.Instant;
|
|
16
|
+
<% } %>
|
|
17
|
+
<% if (needsUUID) { %>
|
|
18
|
+
import java.util.UUID;
|
|
19
|
+
<% } %>
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Local Read Model — projection of data from <%= sourceModule %>/<%= sourceName %>.
|
|
23
|
+
* <p>
|
|
24
|
+
* This class is read-only from this module's perspective.
|
|
25
|
+
* The source module owns mutations; this projection is maintained
|
|
26
|
+
* via domain events (see syncedBy configuration in domain.yaml).
|
|
27
|
+
* <p>
|
|
28
|
+
* No business logic, no setters, no empty constructor.
|
|
29
|
+
*/
|
|
30
|
+
public class <%= domainClassName %> {
|
|
31
|
+
<% fields.forEach(field => { %>
|
|
32
|
+
private final <%- field.javaType %> <%= field.name %>;
|
|
33
|
+
<% }); %>
|
|
34
|
+
|
|
35
|
+
public <%= domainClassName %>(<%= fields.map(f => f.javaType + ' ' + f.name).join(', ') %>) {
|
|
36
|
+
<% fields.forEach(field => { %>
|
|
37
|
+
this.<%= field.name %> = <%= field.name %>;
|
|
38
|
+
<% }); %>
|
|
39
|
+
}
|
|
40
|
+
<% fields.forEach(field => { %>
|
|
41
|
+
|
|
42
|
+
public <%- field.javaType %> get<%= field.name.charAt(0).toUpperCase() + field.name.slice(1) %>() {
|
|
43
|
+
return this.<%= field.name %>;
|
|
44
|
+
}
|
|
45
|
+
<% }); %>
|
|
46
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.infrastructure.database.entities;
|
|
2
|
+
<% const needsBigDecimal = fields && fields.some(f => f.javaType === 'BigDecimal'); %>
|
|
3
|
+
<% const needsLocalDate = fields && fields.some(f => ['LocalDate','LocalDateTime','LocalTime'].includes(f.javaType)); %>
|
|
4
|
+
<% const needsInstant = fields && fields.some(f => f.javaType === 'Instant'); %>
|
|
5
|
+
<% const needsUUID = fields && fields.some(f => f.javaType === 'UUID'); %>
|
|
6
|
+
<% if (needsBigDecimal) { %>
|
|
7
|
+
import java.math.BigDecimal;
|
|
8
|
+
<% } %>
|
|
9
|
+
<% if (needsLocalDate || hasSoftDelete) { %>
|
|
10
|
+
import java.time.LocalDate;
|
|
11
|
+
import java.time.LocalDateTime;
|
|
12
|
+
import java.time.LocalTime;
|
|
13
|
+
<% } %>
|
|
14
|
+
<% if (needsInstant) { %>
|
|
15
|
+
import java.time.Instant;
|
|
16
|
+
<% } %>
|
|
17
|
+
<% if (needsUUID) { %>
|
|
18
|
+
import java.util.UUID;
|
|
19
|
+
<% } %>
|
|
20
|
+
|
|
21
|
+
import jakarta.persistence.Column;
|
|
22
|
+
import jakarta.persistence.Entity;
|
|
23
|
+
import jakarta.persistence.Id;
|
|
24
|
+
import jakarta.persistence.Table;
|
|
25
|
+
import lombok.AllArgsConstructor;
|
|
26
|
+
import lombok.Builder;
|
|
27
|
+
import lombok.Getter;
|
|
28
|
+
import lombok.NoArgsConstructor;
|
|
29
|
+
import lombok.Setter;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* JPA entity for the local read model — table: <%= tableName %>.
|
|
33
|
+
* Projection of data from <%= sourceModule %>/<%= sourceName %>.
|
|
34
|
+
* <p>
|
|
35
|
+
* No audit fields. The @Id mirrors the source entity's ID (not auto-generated).
|
|
36
|
+
*/
|
|
37
|
+
@Entity
|
|
38
|
+
@Table(name = "<%= tableName %>")
|
|
39
|
+
@Getter
|
|
40
|
+
@Setter
|
|
41
|
+
@NoArgsConstructor
|
|
42
|
+
@AllArgsConstructor
|
|
43
|
+
@Builder
|
|
44
|
+
public class <%= jpaEntityName %> {
|
|
45
|
+
|
|
46
|
+
@Id
|
|
47
|
+
private String id;
|
|
48
|
+
<% fields.filter(f => f.name !== 'id').forEach(field => { %>
|
|
49
|
+
|
|
50
|
+
@Column(name = "<%= field.name.replace(/([A-Z])/g, '_$1').toLowerCase() %>")
|
|
51
|
+
private <%- field.javaType %> <%= field.name %>;
|
|
52
|
+
<% }); %>
|
|
53
|
+
<% if (hasSoftDelete) { %>
|
|
54
|
+
|
|
55
|
+
@Column(name = "deleted_at")
|
|
56
|
+
private LocalDateTime deletedAt;
|
|
57
|
+
<% } %>
|
|
58
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.infrastructure.database.repositories;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.<%= moduleName %>.infrastructure.database.entities.<%= jpaEntityName %>;
|
|
4
|
+
import org.springframework.data.jpa.repository.JpaRepository;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Spring Data JPA repository for the <%= domainClassName %> read model.
|
|
8
|
+
* Table: <%= tableName %>.
|
|
9
|
+
*/
|
|
10
|
+
public interface <%= jpaRepositoryName %> extends JpaRepository<<%= jpaEntityName %>, String> {
|
|
11
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.infrastructure.kafkaListener;
|
|
2
|
+
<% const isUpsert = action === 'UPSERT'; %>
|
|
3
|
+
<% if (isUpsert) { %>
|
|
4
|
+
|
|
5
|
+
import <%= packageName %>.<%= moduleName %>.application.events.<%= integrationEventClassName %>;
|
|
6
|
+
<% } %>
|
|
7
|
+
import <%= packageName %>.<%= moduleName %>.application.usecases.<%= syncHandlerName %>;
|
|
8
|
+
import <%= packageName %>.shared.infrastructure.eventEnvelope.EventEnvelope;
|
|
9
|
+
|
|
10
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
11
|
+
import org.springframework.kafka.annotation.KafkaListener;
|
|
12
|
+
import org.springframework.kafka.support.Acknowledgment;
|
|
13
|
+
import org.springframework.stereotype.Component;
|
|
14
|
+
|
|
15
|
+
import java.util.Map;
|
|
16
|
+
<% if (isUpsert) { %>
|
|
17
|
+
<% const needsBigDecimal = fields && fields.some(f => f.javaType === 'BigDecimal'); %>
|
|
18
|
+
<% const needsLocalDate = fields && fields.some(f => ['LocalDate','LocalDateTime','LocalTime'].includes(f.javaType)); %>
|
|
19
|
+
<% const needsInstant = fields && fields.some(f => f.javaType === 'Instant'); %>
|
|
20
|
+
<% const needsUUID = fields && fields.some(f => f.javaType === 'UUID'); %>
|
|
21
|
+
<% if (needsBigDecimal) { %>import java.math.BigDecimal;
|
|
22
|
+
<% } %><% if (needsLocalDate) { %>import java.time.LocalDate;
|
|
23
|
+
import java.time.LocalDateTime;
|
|
24
|
+
import java.time.LocalTime;
|
|
25
|
+
<% } %><% if (needsInstant) { %>import java.time.Instant;
|
|
26
|
+
<% } %><% if (needsUUID) { %>import java.util.UUID;
|
|
27
|
+
<% } %>
|
|
28
|
+
<% } %>
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Kafka listener for read model sync — topic: <%= topicConstant %>.
|
|
32
|
+
* Delegates to <%= syncHandlerName %>.on<%= eventBaseName %>().
|
|
33
|
+
*/
|
|
34
|
+
@Component("<%= moduleName %>.<%= listenerClassName %>")
|
|
35
|
+
public class <%= listenerClassName %> {
|
|
36
|
+
|
|
37
|
+
private final <%= syncHandlerName %> syncHandler;
|
|
38
|
+
private final ObjectMapper objectMapper;
|
|
39
|
+
|
|
40
|
+
public <%= listenerClassName %>(<%= syncHandlerName %> syncHandler, ObjectMapper objectMapper) {
|
|
41
|
+
this.syncHandler = syncHandler;
|
|
42
|
+
this.objectMapper = objectMapper;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@KafkaListener(topics = "<%= topicSpringProperty %>", groupId = "${spring.application.name}-<%= moduleName %>-group")
|
|
46
|
+
public void handle(EventEnvelope<Map<String, Object>> event, Acknowledgment ack) {
|
|
47
|
+
<% if (isUpsert) { %>
|
|
48
|
+
<% (fields || []).forEach(f => { %>
|
|
49
|
+
<%- f.javaType %> <%= f.name %> = objectMapper.convertValue(event.data().get("<%= f.payloadKey || f.name %>"), <%- f.javaType %>.class);
|
|
50
|
+
<% }); %>
|
|
51
|
+
|
|
52
|
+
syncHandler.on<%= eventBaseName %>(new <%= integrationEventClassName %>(
|
|
53
|
+
<% (fields || []).forEach((f, i) => { %>
|
|
54
|
+
<%= f.name %><%= i < fields.length - 1 ? ',' : '' %>
|
|
55
|
+
<% }); %>
|
|
56
|
+
));
|
|
57
|
+
<% } else { %>
|
|
58
|
+
String id = objectMapper.convertValue(event.data().get("<%= (fields[0] && fields[0].payloadKey) || 'id' %>"), String.class);
|
|
59
|
+
syncHandler.on<%= eventBaseName %>(id);
|
|
60
|
+
<% } %>
|
|
61
|
+
|
|
62
|
+
ack.acknowledge();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.domain.repositories;
|
|
2
|
+
<% const needsBigDecimal = fields && fields.some(f => f.javaType === 'BigDecimal'); %>
|
|
3
|
+
<% const needsLocalDate = fields && fields.some(f => ['LocalDate','LocalDateTime','LocalTime'].includes(f.javaType)); %>
|
|
4
|
+
<% const needsInstant = fields && fields.some(f => f.javaType === 'Instant'); %>
|
|
5
|
+
<% const needsUUID = fields && fields.some(f => f.javaType === 'UUID'); %>
|
|
6
|
+
<% if (needsBigDecimal) { %>
|
|
7
|
+
import java.math.BigDecimal;
|
|
8
|
+
<% } %>
|
|
9
|
+
<% if (needsLocalDate) { %>
|
|
10
|
+
import java.time.LocalDate;
|
|
11
|
+
import java.time.LocalDateTime;
|
|
12
|
+
import java.time.LocalTime;
|
|
13
|
+
<% } %>
|
|
14
|
+
<% if (needsInstant) { %>
|
|
15
|
+
import java.time.Instant;
|
|
16
|
+
<% } %>
|
|
17
|
+
<% if (needsUUID) { %>
|
|
18
|
+
import java.util.UUID;
|
|
19
|
+
<% } %>
|
|
20
|
+
|
|
21
|
+
import <%= packageName %>.<%= moduleName %>.domain.models.readmodels.<%= domainClassName %>;
|
|
22
|
+
|
|
23
|
+
import java.util.Optional;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Repository interface for the <%= domainClassName %> read model.
|
|
27
|
+
* Provides read-only access and upsert/delete for event-driven sync.
|
|
28
|
+
*/
|
|
29
|
+
public interface <%= repositoryName %> {
|
|
30
|
+
|
|
31
|
+
void upsert(<%= domainClassName %> readModel);
|
|
32
|
+
|
|
33
|
+
void deleteById(String id);
|
|
34
|
+
<% if (hasSoftDelete) { %>
|
|
35
|
+
|
|
36
|
+
void softDeleteById(String id);
|
|
37
|
+
<% } %>
|
|
38
|
+
|
|
39
|
+
Optional<<%= domainClassName %>> findById(String id);
|
|
40
|
+
|
|
41
|
+
boolean existsById(String id);
|
|
42
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.infrastructure.database.repositories;
|
|
2
|
+
<% const needsBigDecimal = fields && fields.some(f => f.javaType === 'BigDecimal'); %>
|
|
3
|
+
<% const needsLocalDate = fields && fields.some(f => ['LocalDate','LocalDateTime','LocalTime'].includes(f.javaType)); %>
|
|
4
|
+
<% const needsInstant = fields && fields.some(f => f.javaType === 'Instant'); %>
|
|
5
|
+
<% const needsUUID = fields && fields.some(f => f.javaType === 'UUID'); %>
|
|
6
|
+
<% if (needsBigDecimal) { %>
|
|
7
|
+
import java.math.BigDecimal;
|
|
8
|
+
<% } %>
|
|
9
|
+
<% if (needsLocalDate || hasSoftDelete) { %>
|
|
10
|
+
import java.time.LocalDateTime;
|
|
11
|
+
<% } %>
|
|
12
|
+
<% if (needsInstant) { %>
|
|
13
|
+
import java.time.Instant;
|
|
14
|
+
<% } %>
|
|
15
|
+
<% if (needsUUID) { %>
|
|
16
|
+
import java.util.UUID;
|
|
17
|
+
<% } %>
|
|
18
|
+
|
|
19
|
+
import <%= packageName %>.<%= moduleName %>.domain.models.readmodels.<%= domainClassName %>;
|
|
20
|
+
import <%= packageName %>.<%= moduleName %>.domain.repositories.<%= repositoryName %>;
|
|
21
|
+
import <%= packageName %>.<%= moduleName %>.infrastructure.database.entities.<%= jpaEntityName %>;
|
|
22
|
+
import org.springframework.stereotype.Repository;
|
|
23
|
+
|
|
24
|
+
import java.util.Optional;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Repository implementation for the <%= domainClassName %> read model.
|
|
28
|
+
* Maps between domain read model and JPA entity.
|
|
29
|
+
*/
|
|
30
|
+
@Repository
|
|
31
|
+
public class <%= repositoryImplName %> implements <%= repositoryName %> {
|
|
32
|
+
|
|
33
|
+
private final <%= jpaRepositoryName %> jpaRepository;
|
|
34
|
+
|
|
35
|
+
public <%= repositoryImplName %>(<%= jpaRepositoryName %> jpaRepository) {
|
|
36
|
+
this.jpaRepository = jpaRepository;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Override
|
|
40
|
+
public void upsert(<%= domainClassName %> readModel) {
|
|
41
|
+
<%= jpaEntityName %> entity = <%= jpaEntityName %>.builder()
|
|
42
|
+
<% fields.forEach(field => { %>
|
|
43
|
+
.<%= field.name %>(readModel.get<%= field.name.charAt(0).toUpperCase() + field.name.slice(1) %>())
|
|
44
|
+
<% }); %>
|
|
45
|
+
.build();
|
|
46
|
+
jpaRepository.save(entity);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Override
|
|
50
|
+
public void deleteById(String id) {
|
|
51
|
+
jpaRepository.deleteById(id);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Override
|
|
55
|
+
public Optional<<%= domainClassName %>> findById(String id) {
|
|
56
|
+
return jpaRepository.findById(id).map(this::toDomain);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Override
|
|
60
|
+
public boolean existsById(String id) {
|
|
61
|
+
return jpaRepository.existsById(id);
|
|
62
|
+
}
|
|
63
|
+
<% if (hasSoftDelete) { %>
|
|
64
|
+
|
|
65
|
+
@Override
|
|
66
|
+
public void softDeleteById(String id) {
|
|
67
|
+
jpaRepository.findById(id).ifPresent(entity -> {
|
|
68
|
+
entity.setDeletedAt(LocalDateTime.now());
|
|
69
|
+
jpaRepository.save(entity);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
<% } %>
|
|
73
|
+
|
|
74
|
+
private <%= domainClassName %> toDomain(<%= jpaEntityName %> entity) {
|
|
75
|
+
return new <%= domainClassName %>(
|
|
76
|
+
<% fields.forEach((field, idx) => { %>
|
|
77
|
+
entity.get<%= field.name.charAt(0).toUpperCase() + field.name.slice(1) %>()<%= idx < fields.length - 1 ? ',' : '' %>
|
|
78
|
+
<% }); %>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.application.usecases;
|
|
2
|
+
<% const upsertEntries = syncedBy.filter(s => s.action === 'UPSERT'); %>
|
|
3
|
+
<% const upsertImports = [...new Set(upsertEntries.map(s => s.integrationEventClassName))]; %>
|
|
4
|
+
<% upsertImports.forEach(cls => { %>
|
|
5
|
+
import <%= packageName %>.<%= moduleName %>.application.events.<%= cls %>;
|
|
6
|
+
<% }); %>
|
|
7
|
+
import <%= packageName %>.<%= moduleName %>.domain.models.readmodels.<%= domainClassName %>;
|
|
8
|
+
import <%= packageName %>.<%= moduleName %>.domain.repositories.<%= repositoryName %>;
|
|
9
|
+
import <%= packageName %>.shared.domain.annotations.ApplicationComponent;
|
|
10
|
+
import <%= packageName %>.shared.domain.annotations.LogExceptions;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Sync handler for the <%= domainClassName %> read model.
|
|
14
|
+
* Maintains the local projection by processing events from <%= sourceModule %>/<%= sourceName %>.
|
|
15
|
+
* <p>
|
|
16
|
+
* One method per syncedBy event — Kafka listeners delegate directly to these methods.
|
|
17
|
+
*/
|
|
18
|
+
@ApplicationComponent
|
|
19
|
+
public class <%= syncHandlerName %> {
|
|
20
|
+
|
|
21
|
+
private final <%= repositoryName %> repository;
|
|
22
|
+
|
|
23
|
+
public <%= syncHandlerName %>(<%= repositoryName %> repository) {
|
|
24
|
+
this.repository = repository;
|
|
25
|
+
}
|
|
26
|
+
<% syncedBy.forEach(sync => { %>
|
|
27
|
+
<% if (sync.action === 'UPSERT') { %>
|
|
28
|
+
|
|
29
|
+
@LogExceptions
|
|
30
|
+
public void on<%= sync.eventBaseName %>(<%= sync.integrationEventClassName %> event) {
|
|
31
|
+
<%= domainClassName %> model = new <%= domainClassName %>(
|
|
32
|
+
<% fields.forEach((field, idx) => { %>
|
|
33
|
+
event.<%= field.name %>()<%= idx < fields.length - 1 ? ',' : '' %>
|
|
34
|
+
<% }); %>
|
|
35
|
+
);
|
|
36
|
+
repository.upsert(model);
|
|
37
|
+
}
|
|
38
|
+
<% } else if (sync.action === 'DELETE') { %>
|
|
39
|
+
|
|
40
|
+
@LogExceptions
|
|
41
|
+
public void on<%= sync.eventBaseName %>(String id) {
|
|
42
|
+
repository.deleteById(id);
|
|
43
|
+
}
|
|
44
|
+
<% } else if (sync.action === 'SOFT_DELETE') { %>
|
|
45
|
+
|
|
46
|
+
@LogExceptions
|
|
47
|
+
public void on<%= sync.eventBaseName %>(String id) {
|
|
48
|
+
repository.softDeleteById(id);
|
|
49
|
+
}
|
|
50
|
+
<% } %>
|
|
51
|
+
<% }); %>
|
|
52
|
+
}
|