eva4j 1.0.11 → 1.0.13
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 +441 -14
- package/DOMAIN_YAML_GUIDE.md +425 -21
- package/FUTURE_FEATURES.md +315 -115
- package/QUICK_REFERENCE.md +101 -153
- package/README.md +77 -70
- package/bin/eva4j.js +57 -1
- package/config/defaults.json +3 -0
- package/docs/commands/GENERATE_ENTITIES.md +662 -1968
- package/docs/commands/GENERATE_HTTP_EXCHANGE.md +274 -450
- package/docs/commands/GENERATE_KAFKA_EVENT.md +219 -498
- package/docs/commands/GENERATE_KAFKA_LISTENER.md +18 -18
- package/docs/commands/GENERATE_RECORD.md +335 -311
- package/docs/commands/GENERATE_TEMPORAL_ACTIVITY.md +174 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +237 -0
- package/docs/commands/GENERATE_USECASE.md +216 -282
- package/docs/commands/INDEX.md +36 -7
- package/examples/doctor-evaluation.yaml +3 -3
- package/examples/domain-audit-complete.yaml +2 -2
- package/examples/domain-collections.yaml +2 -2
- package/examples/domain-ecommerce.yaml +2 -2
- package/examples/domain-events.yaml +201 -0
- package/examples/domain-field-visibility.yaml +11 -5
- package/examples/domain-multi-aggregate.yaml +12 -6
- package/examples/domain-one-to-many.yaml +1 -1
- package/examples/domain-one-to-one.yaml +1 -1
- package/examples/domain-secondary-onetomany.yaml +1 -1
- package/examples/domain-secondary-onetoone.yaml +1 -1
- package/examples/domain-simple.yaml +1 -1
- package/examples/domain-soft-delete.yaml +3 -3
- package/examples/domain-transitions.yaml +1 -1
- package/examples/domain-value-objects.yaml +1 -1
- package/package.json +2 -2
- package/src/commands/add-kafka-client.js +3 -1
- package/src/commands/add-temporal-client.js +286 -0
- package/src/commands/generate-entities.js +75 -4
- package/src/commands/generate-kafka-event.js +273 -89
- package/src/commands/generate-temporal-activity.js +228 -0
- package/src/commands/generate-temporal-flow.js +216 -0
- package/src/generators/module-generator.js +1 -0
- package/src/generators/shared-generator.js +26 -0
- package/src/utils/yaml-to-entity.js +93 -4
- package/templates/aggregate/AggregateRepository.java.ejs +3 -2
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +15 -7
- package/templates/aggregate/AggregateRoot.java.ejs +38 -2
- package/templates/aggregate/DomainEntity.java.ejs +6 -2
- package/templates/aggregate/DomainEventHandler.java.ejs +62 -0
- package/templates/aggregate/DomainEventRecord.java.ejs +50 -0
- package/templates/aggregate/JpaAggregateRoot.java.ejs +3 -1
- package/templates/aggregate/JpaEntity.java.ejs +3 -1
- package/templates/base/docker/kafka-services.yaml.ejs +2 -2
- package/templates/base/docker/temporal-services.yaml.ejs +29 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +9 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +9 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +9 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +9 -0
- package/templates/base/root/AGENTS.md.ejs +916 -51
- package/templates/crud/Controller.java.ejs +36 -6
- package/templates/crud/ListQuery.java.ejs +6 -2
- package/templates/crud/ListQueryHandler.java.ejs +24 -10
- package/templates/crud/UpdateCommand.java.ejs +52 -0
- package/templates/crud/UpdateCommandHandler.java.ejs +105 -0
- package/templates/kafka-event/DomainEventHandlerMethod.ejs +1 -0
- package/templates/kafka-event/Event.java.ejs +23 -0
- package/templates/shared/application/dtos/PagedResponse.java.ejs +30 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +104 -0
- package/templates/shared/domain/DomainEvent.java.ejs +40 -0
- package/templates/shared/interfaces/HeavyActivity.java.ejs +4 -0
- package/templates/shared/interfaces/LightActivity.java.ejs +4 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +64 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +19 -0
- package/templates/temporal-flow/WorkFlowService.java.ejs +49 -0
|
@@ -2,9 +2,11 @@ package <%= packageName %>.<%= moduleName %>.infrastructure.rest.controllers.<%=
|
|
|
2
2
|
|
|
3
3
|
import <%= packageName %>.<%= moduleName %>.application.commands.Create<%= aggregateName %>Command;
|
|
4
4
|
import <%= packageName %>.<%= moduleName %>.application.commands.Delete<%= aggregateName %>Command;
|
|
5
|
+
import <%= packageName %>.<%= moduleName %>.application.commands.Update<%= aggregateName %>Command;
|
|
5
6
|
import <%= packageName %>.<%= moduleName %>.application.queries.Get<%= aggregateName %>Query;
|
|
6
7
|
import <%= packageName %>.<%= moduleName %>.application.queries.FindAll<%= aggregateName %>sQuery;
|
|
7
8
|
import <%= packageName %>.<%= moduleName %>.application.dtos.<%= aggregateName %>ResponseDto;
|
|
9
|
+
import <%= packageName %>.shared.application.dtos.PagedResponse;
|
|
8
10
|
import <%= packageName %>.shared.infrastructure.configurations.useCaseConfig.UseCaseMediator;
|
|
9
11
|
|
|
10
12
|
import io.swagger.v3.oas.annotations.Operation;
|
|
@@ -14,8 +16,6 @@ import lombok.extern.slf4j.Slf4j;
|
|
|
14
16
|
import org.springframework.http.HttpStatus;
|
|
15
17
|
import org.springframework.web.bind.annotation.*;
|
|
16
18
|
|
|
17
|
-
import java.util.List;
|
|
18
|
-
|
|
19
19
|
@RestController
|
|
20
20
|
@RequestMapping("/api/<%= apiVersion %>/<%= resourceNameKebab %>")
|
|
21
21
|
@Slf4j
|
|
@@ -49,10 +49,15 @@ public class <%= aggregateName %>Controller {
|
|
|
49
49
|
|
|
50
50
|
@GetMapping
|
|
51
51
|
@ResponseStatus(HttpStatus.OK)
|
|
52
|
-
@Operation(summary = "Get all <%= aggregateName %>s")
|
|
53
|
-
public
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
@Operation(summary = "Get all <%= aggregateName %>s (paginated)")
|
|
53
|
+
public PagedResponse<<%= aggregateName %>ResponseDto> findAll(
|
|
54
|
+
@RequestParam(defaultValue = "0") int page,
|
|
55
|
+
@RequestParam(defaultValue = "20") int size,
|
|
56
|
+
@RequestParam(defaultValue = "id") String sortBy,
|
|
57
|
+
@RequestParam(defaultValue = "ASC") String sortDirection) {
|
|
58
|
+
log.info("Finding all <%= aggregateName %>s — page={}, size={}, sortBy={}, sortDirection={}",
|
|
59
|
+
page, size, sortBy, sortDirection);
|
|
60
|
+
return useCaseMediator.dispatch(new FindAll<%= aggregateName %>sQuery(page, size, sortBy, sortDirection));
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
@DeleteMapping("/{id}")
|
|
@@ -62,4 +67,29 @@ public class <%= aggregateName %>Controller {
|
|
|
62
67
|
log.info("Deleting <%= aggregateName %>: {}", id);
|
|
63
68
|
useCaseMediator.dispatch(new Delete<%= aggregateName %>Command(id));
|
|
64
69
|
}
|
|
70
|
+
|
|
71
|
+
@PatchMapping("/{id}")
|
|
72
|
+
@ResponseStatus(HttpStatus.NO_CONTENT)
|
|
73
|
+
@Operation(summary = "Update <%= aggregateName %>")
|
|
74
|
+
public void update(
|
|
75
|
+
@PathVariable <%- idType %> id,
|
|
76
|
+
@RequestBody Update<%= aggregateName %>Command command) {
|
|
77
|
+
log.info("Updating <%= aggregateName %>: {}", id);
|
|
78
|
+
useCaseMediator.dispatch(new Update<%= aggregateName %>Command(
|
|
79
|
+
id<% if (commandFields && commandFields.length > 0) { %>,<% } %>
|
|
80
|
+
<% commandFields.forEach((field, idx) => { %>
|
|
81
|
+
command.<%= field.name %>()<% if (idx < commandFields.length - 1 || (oneToManyRelationships && oneToManyRelationships.length > 0) || (oneToOneRelationships && oneToOneRelationships.length > 0)) { %>,<% } %>
|
|
82
|
+
<% }); %>
|
|
83
|
+
<% if (oneToManyRelationships && oneToManyRelationships.length > 0) { %>
|
|
84
|
+
<% oneToManyRelationships.forEach((rel, idx) => { %>
|
|
85
|
+
command.<%= rel.fieldName %>()<% if (idx < oneToManyRelationships.length - 1 || (oneToOneRelationships && oneToOneRelationships.length > 0)) { %>,<% } %>
|
|
86
|
+
<% }); %>
|
|
87
|
+
<% } %>
|
|
88
|
+
<% if (oneToOneRelationships && oneToOneRelationships.length > 0) { %>
|
|
89
|
+
<% oneToOneRelationships.forEach((rel, idx) => { %>
|
|
90
|
+
command.<%= rel.fieldName %>()<% if (idx < oneToOneRelationships.length - 1) { %>,<% } %>
|
|
91
|
+
<% }); %>
|
|
92
|
+
<% } %>
|
|
93
|
+
));
|
|
94
|
+
}
|
|
65
95
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
package <%= packageName %>.<%= moduleName %>.application.queries;
|
|
2
2
|
|
|
3
|
-
import java.util.List;
|
|
4
3
|
import <%= packageName %>.<%= moduleName %>.application.dtos.<%= aggregateName %>ResponseDto;
|
|
4
|
+
import <%= packageName %>.shared.application.dtos.PagedResponse;
|
|
5
5
|
import <%= packageName %>.shared.domain.interfaces.Query;
|
|
6
6
|
|
|
7
7
|
public record FindAll<%= aggregateName %>sQuery(
|
|
8
|
-
|
|
8
|
+
int page,
|
|
9
|
+
int size,
|
|
10
|
+
String sortBy,
|
|
11
|
+
String sortDirection
|
|
12
|
+
) implements Query<PagedResponse<<%= aggregateName %>ResponseDto>> {
|
|
9
13
|
}
|
|
@@ -5,25 +5,30 @@ import <%= packageName %>.<%= moduleName %>.application.dtos.<%= aggregateName %
|
|
|
5
5
|
import <%= packageName %>.<%= moduleName %>.application.mappers.<%= aggregateName %>ApplicationMapper;
|
|
6
6
|
import <%= packageName %>.<%= moduleName %>.domain.models.entities.<%= aggregateName %>;
|
|
7
7
|
import <%= packageName %>.<%= moduleName %>.domain.repositories.<%= aggregateName %>Repository;
|
|
8
|
+
import <%= packageName %>.shared.application.dtos.PagedResponse;
|
|
8
9
|
import <%= packageName %>.shared.domain.annotations.ApplicationComponent;
|
|
9
10
|
import <%= packageName %>.shared.domain.interfaces.QueryHandler;
|
|
10
11
|
import lombok.extern.slf4j.Slf4j;
|
|
12
|
+
import org.springframework.data.domain.Page;
|
|
13
|
+
import org.springframework.data.domain.PageRequest;
|
|
14
|
+
import org.springframework.data.domain.Pageable;
|
|
15
|
+
import org.springframework.data.domain.Sort;
|
|
11
16
|
import org.springframework.transaction.annotation.Transactional;
|
|
12
17
|
|
|
13
18
|
import java.util.List;
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
21
|
* FindAll<%= aggregateName %>sQueryHandler
|
|
17
|
-
* Handles retrieval of all <%= aggregateName %>s
|
|
22
|
+
* Handles retrieval of all <%= aggregateName %>s (paginated)
|
|
18
23
|
*/
|
|
19
24
|
@Slf4j
|
|
20
25
|
@ApplicationComponent
|
|
21
|
-
public class FindAll<%= aggregateName %>sQueryHandler implements QueryHandler<FindAll<%= aggregateName %>sQuery,
|
|
26
|
+
public class FindAll<%= aggregateName %>sQueryHandler implements QueryHandler<FindAll<%= aggregateName %>sQuery, PagedResponse<<%= aggregateName %>ResponseDto>> {
|
|
22
27
|
|
|
23
28
|
private final <%= aggregateName %>Repository repository;
|
|
24
29
|
private final <%= aggregateName %>ApplicationMapper mapper;
|
|
25
30
|
|
|
26
|
-
public FindAll<%= aggregateName %>sQueryHandler(<%= aggregateName %>Repository repository,
|
|
31
|
+
public FindAll<%= aggregateName %>sQueryHandler(<%= aggregateName %>Repository repository,
|
|
27
32
|
<%= aggregateName %>ApplicationMapper mapper) {
|
|
28
33
|
this.repository = repository;
|
|
29
34
|
this.mapper = mapper;
|
|
@@ -31,12 +36,21 @@ public class FindAll<%= aggregateName %>sQueryHandler implements QueryHandler<Fi
|
|
|
31
36
|
|
|
32
37
|
@Override
|
|
33
38
|
@Transactional(readOnly = true)
|
|
34
|
-
public
|
|
35
|
-
log.info("Handling FindAll<%= aggregateName %>sQuery"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
public PagedResponse<<%= aggregateName %>ResponseDto> handle(FindAll<%= aggregateName %>sQuery query) {
|
|
40
|
+
log.info("Handling FindAll<%= aggregateName %>sQuery — page={}, size={}, sortBy={}, sortDirection={}",
|
|
41
|
+
query.page(), query.size(), query.sortBy(), query.sortDirection());
|
|
42
|
+
|
|
43
|
+
Sort sort = Sort.by(Sort.Direction.fromString(query.sortDirection()), query.sortBy());
|
|
44
|
+
Pageable pageable = PageRequest.of(query.page(), query.size(), sort);
|
|
45
|
+
|
|
46
|
+
Page<<%= aggregateName %>> page = repository.findAll(pageable);
|
|
47
|
+
List<<%= aggregateName %>ResponseDto> content = page.getContent().stream()
|
|
48
|
+
.map(mapper::toDto)
|
|
49
|
+
.toList();
|
|
50
|
+
|
|
51
|
+
log.info("FindAll<%= aggregateName %>s retrieved — page={}/{}, totalElements={}",
|
|
52
|
+
page.getNumber() + 1, page.getTotalPages(), page.getTotalElements());
|
|
53
|
+
|
|
54
|
+
return PagedResponse.of(content, page.getNumber(), page.getSize(), page.getTotalElements());
|
|
41
55
|
}
|
|
42
56
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.application.commands;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.shared.domain.interfaces.Command;
|
|
4
|
+
<% if (hasValueObjects) { %>
|
|
5
|
+
import <%= packageName %>.<%= moduleName %>.domain.models.valueObjects.*;
|
|
6
|
+
<% } %>
|
|
7
|
+
<% if (hasEnums) { %>
|
|
8
|
+
import <%= packageName %>.<%= moduleName %>.domain.models.enums.*;
|
|
9
|
+
<% } %>
|
|
10
|
+
<% if (oneToManyRelationships && oneToManyRelationships.length > 0) { %>
|
|
11
|
+
import java.util.List;
|
|
12
|
+
import jakarta.validation.Valid;
|
|
13
|
+
<% oneToManyRelationships.forEach(rel => { %>
|
|
14
|
+
import <%= packageName %>.<%= moduleName %>.application.dtos.Create<%= rel.targetEntityName %>Dto;
|
|
15
|
+
<% }); %>
|
|
16
|
+
<% } %>
|
|
17
|
+
<% if (oneToOneRelationships && oneToOneRelationships.length > 0) { %>
|
|
18
|
+
import jakarta.validation.Valid;
|
|
19
|
+
<% oneToOneRelationships.forEach(rel => { %>
|
|
20
|
+
import <%= packageName %>.<%= moduleName %>.application.dtos.Create<%= rel.targetEntityName %>Dto;
|
|
21
|
+
<% }); %>
|
|
22
|
+
<% } %>
|
|
23
|
+
<% imports.forEach(imp => { %>
|
|
24
|
+
<%- imp %>
|
|
25
|
+
<% }); %>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Update<%= aggregateName %>Command — PATCH
|
|
29
|
+
*
|
|
30
|
+
* All fields except {@code id} are nullable.
|
|
31
|
+
* The handler only applies fields that are non-null,
|
|
32
|
+
* following the partial-update (PATCH) semantics.
|
|
33
|
+
*/
|
|
34
|
+
public record Update<%= aggregateName %>Command(
|
|
35
|
+
<%- idType %> id<% if (commandFields && commandFields.length > 0) { %>,<% } %>
|
|
36
|
+
<% commandFields.forEach((field, idx) => { %>
|
|
37
|
+
<%- field.javaType %> <%= field.name %><% if (idx < commandFields.length - 1 || (oneToManyRelationships && oneToManyRelationships.length > 0) || (oneToOneRelationships && oneToOneRelationships.length > 0)) { %>,<% } %>
|
|
38
|
+
<% }); %>
|
|
39
|
+
<% if (oneToManyRelationships && oneToManyRelationships.length > 0) { %>
|
|
40
|
+
<% oneToManyRelationships.forEach((rel, idx) => { %>
|
|
41
|
+
@Valid
|
|
42
|
+
List<Create<%= rel.targetEntityName %>Dto> <%= rel.fieldName %><% if (idx < oneToManyRelationships.length - 1 || (oneToOneRelationships && oneToOneRelationships.length > 0)) { %>,<% } %>
|
|
43
|
+
<% }); %>
|
|
44
|
+
<% } %>
|
|
45
|
+
<% if (oneToOneRelationships && oneToOneRelationships.length > 0) { %>
|
|
46
|
+
<% oneToOneRelationships.forEach((rel, idx) => { %>
|
|
47
|
+
@Valid
|
|
48
|
+
Create<%= rel.targetEntityName %>Dto <%= rel.fieldName %><% if (idx < oneToOneRelationships.length - 1) { %>,<% } %>
|
|
49
|
+
<% }); %>
|
|
50
|
+
<% } %>
|
|
51
|
+
) implements Command {
|
|
52
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.application.usecases;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.<%= moduleName %>.application.commands.Update<%= aggregateName %>Command;
|
|
4
|
+
import <%= packageName %>.<%= moduleName %>.domain.models.entities.<%= aggregateName %>;
|
|
5
|
+
import <%= packageName %>.<%= moduleName %>.domain.repositories.<%= aggregateName %>Repository;
|
|
6
|
+
<% if ((commandFields || []).some(f => f.originalVoType)) { %>
|
|
7
|
+
import <%= packageName %>.<%= moduleName %>.application.mappers.<%= aggregateName %>ApplicationMapper;
|
|
8
|
+
<% } %>
|
|
9
|
+
import <%= packageName %>.shared.domain.annotations.ApplicationComponent;
|
|
10
|
+
import <%= packageName %>.shared.domain.customExceptions.NotFoundException;
|
|
11
|
+
import <%= packageName %>.shared.domain.interfaces.CommandHandler;
|
|
12
|
+
import lombok.extern.slf4j.Slf4j;
|
|
13
|
+
import org.springframework.transaction.annotation.Transactional;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Update<%= aggregateName %>CommandHandler — PATCH
|
|
17
|
+
*
|
|
18
|
+
* Reconstructs the <%= aggregateName %> aggregate using the full constructor,
|
|
19
|
+
* merging each command field with the existing entity value.
|
|
20
|
+
* Non-null command fields override the current value; null fields are preserved
|
|
21
|
+
* from the loaded entity. No setters are required on the domain entity.
|
|
22
|
+
*/
|
|
23
|
+
@Slf4j
|
|
24
|
+
@ApplicationComponent
|
|
25
|
+
public class Update<%= aggregateName %>CommandHandler implements CommandHandler<Update<%= aggregateName %>Command> {
|
|
26
|
+
|
|
27
|
+
private final <%= aggregateName %>Repository repository;
|
|
28
|
+
<% if ((commandFields || []).some(f => f.originalVoType)) { %>
|
|
29
|
+
private final <%= aggregateName %>ApplicationMapper mapper;
|
|
30
|
+
|
|
31
|
+
public Update<%= aggregateName %>CommandHandler(
|
|
32
|
+
<%= aggregateName %>Repository repository,
|
|
33
|
+
<%= aggregateName %>ApplicationMapper mapper) {
|
|
34
|
+
this.repository = repository;
|
|
35
|
+
this.mapper = mapper;
|
|
36
|
+
}
|
|
37
|
+
<% } else { %>
|
|
38
|
+
public Update<%= aggregateName %>CommandHandler(<%= aggregateName %>Repository repository) {
|
|
39
|
+
this.repository = repository;
|
|
40
|
+
}
|
|
41
|
+
<% } %>
|
|
42
|
+
|
|
43
|
+
@Override
|
|
44
|
+
@Transactional
|
|
45
|
+
public void handle(Update<%= aggregateName %>Command command) {
|
|
46
|
+
log.info("Handling Update<%= aggregateName %>Command for id: {}", command.id());
|
|
47
|
+
|
|
48
|
+
<%= aggregateName %> existing = repository
|
|
49
|
+
.findById(command.id())
|
|
50
|
+
.orElseThrow(() -> new NotFoundException("<%= aggregateName %> not found with id: " + command.id()));
|
|
51
|
+
|
|
52
|
+
<%
|
|
53
|
+
// Build a lookup map: field name → commandField (with originalVoType info)
|
|
54
|
+
const cmdFieldMap = {};
|
|
55
|
+
(commandFields || []).forEach(f => { cmdFieldMap[f.name] = f; });
|
|
56
|
+
|
|
57
|
+
// oneToOneRelationships are non-collection non-inverse rels → appear in the full constructor
|
|
58
|
+
const ctorRels = oneToOneRelationships || [];
|
|
59
|
+
const totalArgs = (rootEntity.fields || []).length + ctorRels.length;
|
|
60
|
+
let argIdx = 0;
|
|
61
|
+
|
|
62
|
+
function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
|
|
63
|
+
%>
|
|
64
|
+
// Merge: use command value when non-null, otherwise preserve existing value.
|
|
65
|
+
// The full constructor is called so no setters are needed on the domain entity.
|
|
66
|
+
<%= aggregateName %> updated = new <%= aggregateName %>(
|
|
67
|
+
<% (rootEntity.fields || []).forEach(field => { argIdx++; %>
|
|
68
|
+
<% const cmdField = cmdFieldMap[field.name]; %>
|
|
69
|
+
<% const getter = `existing.get${capitalize(field.name)}()`; %>
|
|
70
|
+
<% const comma = argIdx < totalArgs ? ',' : ''; %>
|
|
71
|
+
<% if (cmdField && cmdField.originalVoType) { %>
|
|
72
|
+
command.<%= field.name %>() != null
|
|
73
|
+
? mapper.to<%= cmdField.originalVoType %>(command.<%= field.name %>())
|
|
74
|
+
: <%= getter %><%= comma %>
|
|
75
|
+
<% } else if (cmdField) { %>
|
|
76
|
+
command.<%= field.name %>() != null ? command.<%= field.name %>() : <%= getter %><%= comma %>
|
|
77
|
+
<% } else { %>
|
|
78
|
+
<%= getter %><%= comma %> // preserved: <%= field.name %>
|
|
79
|
+
<% } %>
|
|
80
|
+
<% }); %>
|
|
81
|
+
<% ctorRels.forEach(rel => { argIdx++; %>
|
|
82
|
+
<% const comma = argIdx < totalArgs ? ',' : ''; %>
|
|
83
|
+
existing.get<%= capitalize(rel.fieldName) %>()<%= comma %> // preserved: <%= rel.fieldName %>
|
|
84
|
+
<% }); %>
|
|
85
|
+
);
|
|
86
|
+
<% if ((oneToManyRelationships || []).length > 0) { %>
|
|
87
|
+
|
|
88
|
+
// TODO: Collection relationships require an explicit replacement strategy.
|
|
89
|
+
// Choose the right approach for your domain and uncomment:
|
|
90
|
+
//
|
|
91
|
+
// Option A — Clear-and-replace (simple, loses existing entity ids):
|
|
92
|
+
// existing.get<collection>().clear();
|
|
93
|
+
// command.<collection>().forEach(dto -> entity.add<Item>(...));
|
|
94
|
+
//
|
|
95
|
+
// Option B — Merge by id (preserves audit trail, more complex):
|
|
96
|
+
// reconcile existing items against command items by id
|
|
97
|
+
<% (oneToManyRelationships || []).forEach(rel => { %>
|
|
98
|
+
// if (command.<%= rel.fieldName %>() != null) { /* replace <%= rel.fieldName %> */ }
|
|
99
|
+
<% }); %>
|
|
100
|
+
<% } %>
|
|
101
|
+
|
|
102
|
+
repository.save(updated);
|
|
103
|
+
log.info("<%= aggregateName %> updated successfully with id: {}", command.id());
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
messageBroker.publish<%= eventClassName %>(new <%= eventClassName %>(<% if (domainEventFields && domainEventFields.length > 0) { domainEventFields.forEach(function(field, idx) { %>event.get<%= field.name.charAt(0).toUpperCase() + field.name.slice(1) %>()<%= idx < domainEventFields.length - 1 ? ', ' : '' %><% }); } %>));
|
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
package <%= packageName %>.<%= moduleName %>.application.events;
|
|
2
|
+
<% const needsBigDecimal = eventFields && eventFields.some(f => f.javaType === 'BigDecimal'); %>
|
|
3
|
+
<% const needsLocalDate = eventFields && eventFields.some(f => f.javaType === 'LocalDate' || f.javaType === 'LocalDateTime' || f.javaType === 'LocalTime'); %>
|
|
4
|
+
<% const needsUUID = eventFields && eventFields.some(f => f.javaType === 'UUID'); %>
|
|
5
|
+
<% const needsList = eventFields && eventFields.some(f => f.isCollection); %>
|
|
6
|
+
<% if (needsBigDecimal) { %>
|
|
7
|
+
import java.math.BigDecimal;
|
|
8
|
+
<% } %>
|
|
9
|
+
<% if (needsLocalDate) { %>
|
|
10
|
+
import java.time.LocalDate;
|
|
11
|
+
import java.time.LocalDateTime;
|
|
12
|
+
<% } %>
|
|
13
|
+
<% if (needsUUID) { %>
|
|
14
|
+
import java.util.UUID;
|
|
15
|
+
<% } %>
|
|
16
|
+
<% if (needsList) { %>
|
|
17
|
+
import java.util.List;
|
|
18
|
+
<% } %>
|
|
2
19
|
|
|
3
20
|
public record <%= eventClassName %>(
|
|
21
|
+
<% if (eventFields && eventFields.length > 0) { %>
|
|
22
|
+
<% eventFields.forEach((field, idx) => { %>
|
|
23
|
+
<%- field.javaType %> <%= field.name %><%= idx < eventFields.length - 1 ? ',' : '' %>
|
|
24
|
+
<% }); %>
|
|
25
|
+
<% } else { %>
|
|
4
26
|
// TODO: Add your event fields here
|
|
5
27
|
// Example:
|
|
6
28
|
// Long id,
|
|
7
29
|
// String name,
|
|
8
30
|
// LocalDateTime createdAt
|
|
31
|
+
<% } %>
|
|
9
32
|
) {
|
|
10
33
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package <%= packageName %>.shared.application.dtos;
|
|
2
|
+
|
|
3
|
+
import java.util.List;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generic paginated response wrapper.
|
|
7
|
+
* Decouples API responses from Spring Data's Page internals.
|
|
8
|
+
*
|
|
9
|
+
* @param <T> the type of content elements
|
|
10
|
+
*/
|
|
11
|
+
public record PagedResponse<T>(
|
|
12
|
+
List<T> content,
|
|
13
|
+
int page,
|
|
14
|
+
int size,
|
|
15
|
+
long totalElements,
|
|
16
|
+
int totalPages
|
|
17
|
+
) {
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a PagedResponse from Spring Data's Page object via explicit parameters.
|
|
21
|
+
*/
|
|
22
|
+
public static <T> PagedResponse<T> of(
|
|
23
|
+
List<T> content,
|
|
24
|
+
int page,
|
|
25
|
+
int size,
|
|
26
|
+
long totalElements) {
|
|
27
|
+
int totalPages = size == 0 ? 1 : (int) Math.ceil((double) totalElements / size);
|
|
28
|
+
return new PagedResponse<>(content, page, size, totalElements, totalPages);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package <%= packageName %>.shared.infrastructure.configurations.temporalConfig;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.shared.domain.interfaces.HeavyActivity;
|
|
4
|
+
import <%= packageName %>.shared.domain.interfaces.LightActivity;
|
|
5
|
+
import io.temporal.client.WorkflowClient;
|
|
6
|
+
import io.temporal.serviceclient.WorkflowServiceStubs;
|
|
7
|
+
import io.temporal.worker.Worker;
|
|
8
|
+
import io.temporal.worker.WorkerFactory;
|
|
9
|
+
import io.temporal.worker.WorkerOptions;
|
|
10
|
+
import jakarta.annotation.PreDestroy;
|
|
11
|
+
import org.springframework.beans.factory.annotation.Value;
|
|
12
|
+
import org.springframework.context.annotation.Bean;
|
|
13
|
+
import org.springframework.context.annotation.Configuration;
|
|
14
|
+
|
|
15
|
+
import java.util.List;
|
|
16
|
+
|
|
17
|
+
@Configuration
|
|
18
|
+
public class TemporalConfig {
|
|
19
|
+
|
|
20
|
+
@Value("${temporal.service-url}")
|
|
21
|
+
private String temporalServiceUrl;
|
|
22
|
+
|
|
23
|
+
@Value("${temporal.namespace}")
|
|
24
|
+
private String namespace;
|
|
25
|
+
|
|
26
|
+
@Value("${temporal.flow-queue}")
|
|
27
|
+
private String flowQueue;
|
|
28
|
+
|
|
29
|
+
@Value("${temporal.number-flow-worker}")
|
|
30
|
+
private Integer numberFlowWorker;
|
|
31
|
+
|
|
32
|
+
@Value("${temporal.heavy-queue}")
|
|
33
|
+
private String heavyQueue;
|
|
34
|
+
|
|
35
|
+
@Value("${temporal.number-heavy-worker}")
|
|
36
|
+
private Integer numberHeavyWorker;
|
|
37
|
+
|
|
38
|
+
@Value("${temporal.light-queue}")
|
|
39
|
+
private String lightQueue;
|
|
40
|
+
|
|
41
|
+
@Value("${temporal.number-light-worker}")
|
|
42
|
+
private Integer numberLightWorker;
|
|
43
|
+
|
|
44
|
+
private WorkerFactory workerFactory;
|
|
45
|
+
|
|
46
|
+
@Bean
|
|
47
|
+
public WorkflowServiceStubs workflowServiceStubs() {
|
|
48
|
+
return WorkflowServiceStubs.newServiceStubs(
|
|
49
|
+
io.temporal.serviceclient.WorkflowServiceStubsOptions.newBuilder()
|
|
50
|
+
.setTarget(temporalServiceUrl)
|
|
51
|
+
.build()
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Bean
|
|
56
|
+
public WorkflowClient workflowClient(WorkflowServiceStubs serviceStubs) {
|
|
57
|
+
return WorkflowClient.newInstance(
|
|
58
|
+
serviceStubs,
|
|
59
|
+
io.temporal.client.WorkflowClientOptions.newBuilder()
|
|
60
|
+
.setNamespace(namespace)
|
|
61
|
+
.build()
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Bean
|
|
66
|
+
public WorkerFactory workerFactory(WorkflowClient client,
|
|
67
|
+
List<HeavyActivity> heavyActivities,
|
|
68
|
+
List<LightActivity> lightActivities) {
|
|
69
|
+
|
|
70
|
+
workerFactory = WorkerFactory.newInstance(client);
|
|
71
|
+
|
|
72
|
+
// 1. WORKER DE FLUJO (Orquestador)
|
|
73
|
+
WorkerOptions workflowOptions = WorkerOptions.newBuilder()
|
|
74
|
+
.setMaxConcurrentWorkflowTaskExecutionSize(numberFlowWorker)
|
|
75
|
+
.build();
|
|
76
|
+
Worker workflowWorker = workerFactory.newWorker(flowQueue, workflowOptions);
|
|
77
|
+
// TODO: register your workflow implementation types here
|
|
78
|
+
// workflowWorker.registerWorkflowImplementationTypes(MyWorkflowImpl.class);
|
|
79
|
+
|
|
80
|
+
// 2. WORKER PESADO (tareas complejas)
|
|
81
|
+
WorkerOptions heavyOptions = WorkerOptions.newBuilder()
|
|
82
|
+
.setMaxConcurrentActivityExecutionSize(numberHeavyWorker)
|
|
83
|
+
.build();
|
|
84
|
+
Worker heavyWorker = workerFactory.newWorker(heavyQueue, heavyOptions);
|
|
85
|
+
heavyWorker.registerActivitiesImplementations(heavyActivities.toArray());
|
|
86
|
+
|
|
87
|
+
// 3. WORKER LIGERO (tareas rápidas)
|
|
88
|
+
WorkerOptions lightOptions = WorkerOptions.newBuilder()
|
|
89
|
+
.setMaxConcurrentActivityExecutionSize(numberLightWorker)
|
|
90
|
+
.build();
|
|
91
|
+
Worker lightWorker = workerFactory.newWorker(lightQueue, lightOptions);
|
|
92
|
+
lightWorker.registerActivitiesImplementations(lightActivities.toArray());
|
|
93
|
+
|
|
94
|
+
workerFactory.start();
|
|
95
|
+
return workerFactory;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@PreDestroy
|
|
99
|
+
public void destroy() {
|
|
100
|
+
if (workerFactory != null) {
|
|
101
|
+
workerFactory.shutdown();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package <%= packageName %>.shared.domain;
|
|
2
|
+
|
|
3
|
+
import java.time.LocalDateTime;
|
|
4
|
+
import java.util.UUID;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DomainEvent - Base class for all domain events
|
|
8
|
+
*
|
|
9
|
+
* Domain events represent facts that occurred in the business domain.
|
|
10
|
+
* They are immutable, pure Java (no Spring or infrastructure dependencies).
|
|
11
|
+
*
|
|
12
|
+
* Usage: extend this class in your domain/models/events/ classes.
|
|
13
|
+
*
|
|
14
|
+
* NOTE: For guaranteed delivery (at-least-once semantics), consider implementing
|
|
15
|
+
* the Transactional Outbox Pattern on top of this mechanism.
|
|
16
|
+
*/
|
|
17
|
+
public abstract class DomainEvent {
|
|
18
|
+
|
|
19
|
+
private final String eventId;
|
|
20
|
+
private final LocalDateTime occurredOn;
|
|
21
|
+
private final String aggregateId;
|
|
22
|
+
|
|
23
|
+
protected DomainEvent(String aggregateId) {
|
|
24
|
+
this.eventId = UUID.randomUUID().toString();
|
|
25
|
+
this.occurredOn = LocalDateTime.now();
|
|
26
|
+
this.aggregateId = aggregateId;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public String getEventId() {
|
|
30
|
+
return eventId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public LocalDateTime getOccurredOn() {
|
|
34
|
+
return occurredOn;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public String getAggregateId() {
|
|
38
|
+
return aggregateId;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.infrastructure.adapters.activities;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.<%= moduleName %>.application.ports.<%= activityPascalCase %>Activity;
|
|
4
|
+
import <%= packageName %>.shared.domain.interfaces.<%= activityCategory %>;
|
|
5
|
+
import org.springframework.stereotype.Component;
|
|
6
|
+
|
|
7
|
+
@Component
|
|
8
|
+
public class <%= activityPascalCase %>ActivityImpl implements <%= activityPascalCase %>Activity, <%= activityCategory %> {
|
|
9
|
+
|
|
10
|
+
@Override
|
|
11
|
+
public void execute(String flowId) {
|
|
12
|
+
//todo: implement the logic
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.application.ports;
|
|
2
|
+
|
|
3
|
+
import io.temporal.activity.ActivityInterface;
|
|
4
|
+
import io.temporal.activity.ActivityMethod;
|
|
5
|
+
|
|
6
|
+
@ActivityInterface
|
|
7
|
+
public interface <%= activityPascalCase %>Activity {
|
|
8
|
+
|
|
9
|
+
@ActivityMethod(name = "<%= activityPascalCase %>")
|
|
10
|
+
void execute(String flowId);
|
|
11
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.application.usecases;
|
|
2
|
+
|
|
3
|
+
import io.temporal.activity.ActivityOptions;
|
|
4
|
+
import io.temporal.common.RetryOptions;
|
|
5
|
+
import io.temporal.workflow.Saga;
|
|
6
|
+
import io.temporal.workflow.SignalMethod;
|
|
7
|
+
import io.temporal.workflow.QueryMethod;
|
|
8
|
+
import io.temporal.workflow.Workflow;
|
|
9
|
+
|
|
10
|
+
import java.time.Duration;
|
|
11
|
+
|
|
12
|
+
public class <%= flowPascalCase %>WorkFlowImpl implements <%= flowPascalCase %>WorkFlow {
|
|
13
|
+
|
|
14
|
+
private Saga.Options sagaOptions = new Saga.Options.Builder()
|
|
15
|
+
.setParallelCompensation(false)
|
|
16
|
+
.build();
|
|
17
|
+
|
|
18
|
+
private Saga saga = new Saga(sagaOptions);
|
|
19
|
+
|
|
20
|
+
private final ActivityOptions lightActivityOptions = ActivityOptions.newBuilder()
|
|
21
|
+
.setStartToCloseTimeout(Duration.ofSeconds(30))
|
|
22
|
+
.setTaskQueue("LIGHT_TASK_QUEUE")
|
|
23
|
+
.setRetryOptions(
|
|
24
|
+
RetryOptions.newBuilder()
|
|
25
|
+
.setMaximumAttempts(2)
|
|
26
|
+
.setInitialInterval(Duration.ofSeconds(1))
|
|
27
|
+
.setMaximumInterval(Duration.ofSeconds(10))
|
|
28
|
+
.setBackoffCoefficient(2.0)
|
|
29
|
+
.build()
|
|
30
|
+
).build();
|
|
31
|
+
|
|
32
|
+
private final ActivityOptions heavyActivityOptions = ActivityOptions.newBuilder()
|
|
33
|
+
.setStartToCloseTimeout(Duration.ofSeconds(120))
|
|
34
|
+
.setTaskQueue("HEAVY_TASK_QUEUE")
|
|
35
|
+
.setRetryOptions(
|
|
36
|
+
RetryOptions.newBuilder()
|
|
37
|
+
.setMaximumAttempts(2)
|
|
38
|
+
.setInitialInterval(Duration.ofSeconds(1))
|
|
39
|
+
.setMaximumInterval(Duration.ofSeconds(10))
|
|
40
|
+
.setBackoffCoefficient(2.0)
|
|
41
|
+
.build()
|
|
42
|
+
).build();
|
|
43
|
+
|
|
44
|
+
@Override
|
|
45
|
+
public void start(String workFlowId) {
|
|
46
|
+
try {
|
|
47
|
+
//todo: workflow logic
|
|
48
|
+
} catch (Exception e) {
|
|
49
|
+
saga.compensate();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@SignalMethod
|
|
54
|
+
@Override
|
|
55
|
+
public void confirm() {
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@QueryMethod
|
|
60
|
+
@Override
|
|
61
|
+
public String getStatus() {
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.application.usecases;
|
|
2
|
+
|
|
3
|
+
import io.temporal.workflow.QueryMethod;
|
|
4
|
+
import io.temporal.workflow.SignalMethod;
|
|
5
|
+
import io.temporal.workflow.WorkflowInterface;
|
|
6
|
+
import io.temporal.workflow.WorkflowMethod;
|
|
7
|
+
|
|
8
|
+
@WorkflowInterface
|
|
9
|
+
public interface <%= flowPascalCase %>WorkFlow {
|
|
10
|
+
|
|
11
|
+
@WorkflowMethod
|
|
12
|
+
void start(String flowId);
|
|
13
|
+
|
|
14
|
+
@SignalMethod
|
|
15
|
+
void confirm();
|
|
16
|
+
|
|
17
|
+
@QueryMethod
|
|
18
|
+
String getStatus();
|
|
19
|
+
}
|