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.
Files changed (73) hide show
  1. package/AGENTS.md +441 -14
  2. package/DOMAIN_YAML_GUIDE.md +425 -21
  3. package/FUTURE_FEATURES.md +315 -115
  4. package/QUICK_REFERENCE.md +101 -153
  5. package/README.md +77 -70
  6. package/bin/eva4j.js +57 -1
  7. package/config/defaults.json +3 -0
  8. package/docs/commands/GENERATE_ENTITIES.md +662 -1968
  9. package/docs/commands/GENERATE_HTTP_EXCHANGE.md +274 -450
  10. package/docs/commands/GENERATE_KAFKA_EVENT.md +219 -498
  11. package/docs/commands/GENERATE_KAFKA_LISTENER.md +18 -18
  12. package/docs/commands/GENERATE_RECORD.md +335 -311
  13. package/docs/commands/GENERATE_TEMPORAL_ACTIVITY.md +174 -0
  14. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +237 -0
  15. package/docs/commands/GENERATE_USECASE.md +216 -282
  16. package/docs/commands/INDEX.md +36 -7
  17. package/examples/doctor-evaluation.yaml +3 -3
  18. package/examples/domain-audit-complete.yaml +2 -2
  19. package/examples/domain-collections.yaml +2 -2
  20. package/examples/domain-ecommerce.yaml +2 -2
  21. package/examples/domain-events.yaml +201 -0
  22. package/examples/domain-field-visibility.yaml +11 -5
  23. package/examples/domain-multi-aggregate.yaml +12 -6
  24. package/examples/domain-one-to-many.yaml +1 -1
  25. package/examples/domain-one-to-one.yaml +1 -1
  26. package/examples/domain-secondary-onetomany.yaml +1 -1
  27. package/examples/domain-secondary-onetoone.yaml +1 -1
  28. package/examples/domain-simple.yaml +1 -1
  29. package/examples/domain-soft-delete.yaml +3 -3
  30. package/examples/domain-transitions.yaml +1 -1
  31. package/examples/domain-value-objects.yaml +1 -1
  32. package/package.json +2 -2
  33. package/src/commands/add-kafka-client.js +3 -1
  34. package/src/commands/add-temporal-client.js +286 -0
  35. package/src/commands/generate-entities.js +75 -4
  36. package/src/commands/generate-kafka-event.js +273 -89
  37. package/src/commands/generate-temporal-activity.js +228 -0
  38. package/src/commands/generate-temporal-flow.js +216 -0
  39. package/src/generators/module-generator.js +1 -0
  40. package/src/generators/shared-generator.js +26 -0
  41. package/src/utils/yaml-to-entity.js +93 -4
  42. package/templates/aggregate/AggregateRepository.java.ejs +3 -2
  43. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +15 -7
  44. package/templates/aggregate/AggregateRoot.java.ejs +38 -2
  45. package/templates/aggregate/DomainEntity.java.ejs +6 -2
  46. package/templates/aggregate/DomainEventHandler.java.ejs +62 -0
  47. package/templates/aggregate/DomainEventRecord.java.ejs +50 -0
  48. package/templates/aggregate/JpaAggregateRoot.java.ejs +3 -1
  49. package/templates/aggregate/JpaEntity.java.ejs +3 -1
  50. package/templates/base/docker/kafka-services.yaml.ejs +2 -2
  51. package/templates/base/docker/temporal-services.yaml.ejs +29 -0
  52. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +9 -0
  53. package/templates/base/resources/parameters/local/temporal.yaml.ejs +9 -0
  54. package/templates/base/resources/parameters/production/temporal.yaml.ejs +9 -0
  55. package/templates/base/resources/parameters/test/temporal.yaml.ejs +9 -0
  56. package/templates/base/root/AGENTS.md.ejs +916 -51
  57. package/templates/crud/Controller.java.ejs +36 -6
  58. package/templates/crud/ListQuery.java.ejs +6 -2
  59. package/templates/crud/ListQueryHandler.java.ejs +24 -10
  60. package/templates/crud/UpdateCommand.java.ejs +52 -0
  61. package/templates/crud/UpdateCommandHandler.java.ejs +105 -0
  62. package/templates/kafka-event/DomainEventHandlerMethod.ejs +1 -0
  63. package/templates/kafka-event/Event.java.ejs +23 -0
  64. package/templates/shared/application/dtos/PagedResponse.java.ejs +30 -0
  65. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +104 -0
  66. package/templates/shared/domain/DomainEvent.java.ejs +40 -0
  67. package/templates/shared/interfaces/HeavyActivity.java.ejs +4 -0
  68. package/templates/shared/interfaces/LightActivity.java.ejs +4 -0
  69. package/templates/temporal-activity/ActivityImpl.java.ejs +14 -0
  70. package/templates/temporal-activity/ActivityInterface.java.ejs +11 -0
  71. package/templates/temporal-flow/WorkFlowImpl.java.ejs +64 -0
  72. package/templates/temporal-flow/WorkFlowInterface.java.ejs +19 -0
  73. 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 List<<%= aggregateName %>ResponseDto> findAll() {
54
- log.info("Finding all <%= aggregateName %>s");
55
- return useCaseMediator.dispatch(new FindAll<%= aggregateName %>sQuery());
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
- ) implements Query<List<<%= aggregateName %>ResponseDto>> {
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, List<<%= aggregateName %>ResponseDto>> {
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 List<<%= aggregateName %>ResponseDto> handle(FindAll<%= aggregateName %>sQuery query) {
35
- log.info("Handling FindAll<%= aggregateName %>sQuery");
36
-
37
- List<<%= aggregateName %>> entities = repository.findAll();
38
-
39
- log.info("FindAll<%= aggregateName %>s retrieved - total elements: {}", entities.size());
40
- return mapper.toDto(entities);
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,4 @@
1
+ package <%= packageName %>.shared.domain.interfaces;
2
+
3
+ public interface HeavyActivity {
4
+ }
@@ -0,0 +1,4 @@
1
+ package <%= packageName %>.shared.domain.interfaces;
2
+
3
+ public interface LightActivity {
4
+ }
@@ -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
+ }