eva4j 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/AGENTS.md +314 -10
  2. package/COMMAND_EVALUATION.md +15 -16
  3. package/DOMAIN_YAML_GUIDE.md +576 -10
  4. package/FUTURE_FEATURES.md +1627 -1168
  5. package/README.md +318 -13
  6. package/bin/eva4j.js +34 -0
  7. package/config/defaults.json +1 -0
  8. package/design-system.md +797 -0
  9. package/docs/commands/EVALUATE_SYSTEM.md +994 -0
  10. package/docs/commands/GENERATE_ENTITIES.md +795 -6
  11. package/docs/commands/INDEX.md +10 -1
  12. package/examples/domain-endpoints-relations.yaml +353 -0
  13. package/examples/domain-endpoints-versioned.yaml +144 -0
  14. package/examples/domain-endpoints.yaml +135 -0
  15. package/examples/domain-events.yaml +166 -20
  16. package/examples/domain-listeners.yaml +212 -0
  17. package/examples/domain-one-to-many.yaml +1 -0
  18. package/examples/domain-one-to-one.yaml +1 -0
  19. package/examples/domain-ports.yaml +414 -0
  20. package/examples/domain-soft-delete.yaml +47 -44
  21. package/examples/system/notification.yaml +147 -0
  22. package/examples/system/product.yaml +185 -0
  23. package/examples/system/system.yaml +112 -0
  24. package/examples/system-report.html +971 -0
  25. package/examples/system.yaml +332 -0
  26. package/package.json +2 -1
  27. package/src/commands/build.js +714 -0
  28. package/src/commands/create.js +7 -3
  29. package/src/commands/detach.js +1 -0
  30. package/src/commands/evaluate-system.js +610 -0
  31. package/src/commands/generate-entities.js +1331 -49
  32. package/src/commands/generate-http-exchange.js +2 -0
  33. package/src/commands/generate-kafka-event.js +98 -11
  34. package/src/generators/base-generator.js +8 -1
  35. package/src/generators/postman-generator.js +188 -0
  36. package/src/generators/shared-generator.js +10 -0
  37. package/src/utils/config-manager.js +54 -0
  38. package/src/utils/context-builder.js +1 -0
  39. package/src/utils/domain-diagram.js +192 -0
  40. package/src/utils/domain-validator.js +970 -0
  41. package/src/utils/fake-data.js +376 -0
  42. package/src/utils/naming.js +3 -2
  43. package/src/utils/system-validator.js +434 -0
  44. package/src/utils/yaml-to-entity.js +302 -8
  45. package/templates/aggregate/AggregateMapper.java.ejs +3 -2
  46. package/templates/aggregate/AggregateRepository.java.ejs +8 -2
  47. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +13 -3
  48. package/templates/aggregate/AggregateRoot.java.ejs +60 -2
  49. package/templates/aggregate/DomainEventHandler.java.ejs +27 -20
  50. package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
  51. package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
  52. package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
  53. package/templates/aggregate/JpaRepository.java.ejs +5 -0
  54. package/templates/base/gradle/build.gradle.ejs +3 -2
  55. package/templates/base/root/AGENTS.md.ejs +306 -45
  56. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1663 -0
  57. package/templates/base/root/skill-build-system-yaml.ejs +1446 -0
  58. package/templates/base/root/system.yaml.ejs +97 -0
  59. package/templates/crud/ApplicationMapper.java.ejs +4 -0
  60. package/templates/crud/Controller.java.ejs +4 -4
  61. package/templates/crud/CreateCommand.java.ejs +4 -0
  62. package/templates/crud/CreateItemDto.java.ejs +4 -0
  63. package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
  64. package/templates/crud/DeleteCommandHandler.java.ejs +10 -2
  65. package/templates/crud/EndpointsController.java.ejs +178 -0
  66. package/templates/crud/FindByQuery.java.ejs +17 -0
  67. package/templates/crud/FindByQueryHandler.java.ejs +57 -0
  68. package/templates/crud/ListQuery.java.ejs +1 -1
  69. package/templates/crud/ListQueryHandler.java.ejs +8 -8
  70. package/templates/crud/ScaffoldCommand.java.ejs +12 -0
  71. package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
  72. package/templates/crud/ScaffoldQuery.java.ejs +13 -0
  73. package/templates/crud/ScaffoldQueryHandler.java.ejs +41 -0
  74. package/templates/crud/SubEntityAddCommand.java.ejs +21 -0
  75. package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
  76. package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
  77. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
  78. package/templates/crud/TransitionCommand.java.ejs +9 -0
  79. package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
  80. package/templates/crud/UpdateCommand.java.ejs +4 -0
  81. package/templates/evaluate/report.html.ejs +1363 -0
  82. package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
  83. package/templates/kafka-event/Event.java.ejs +16 -0
  84. package/templates/kafka-listener/KafkaController.java.ejs +1 -1
  85. package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
  86. package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
  87. package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
  88. package/templates/kafka-listener/ListenerCommandHandler.java.ejs +23 -0
  89. package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
  90. package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
  91. package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
  92. package/templates/mock/MockEvent.java.ejs +10 -0
  93. package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
  94. package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
  95. package/templates/mock/SpringEventListener.java.ejs +61 -0
  96. package/templates/ports/PortDomainModel.java.ejs +35 -0
  97. package/templates/ports/PortFeignAdapter.java.ejs +67 -0
  98. package/templates/ports/PortFeignClient.java.ejs +45 -0
  99. package/templates/ports/PortFeignConfig.java.ejs +24 -0
  100. package/templates/ports/PortInterface.java.ejs +45 -0
  101. package/templates/ports/PortNestedType.java.ejs +28 -0
  102. package/templates/ports/PortRequestDto.java.ejs +30 -0
  103. package/templates/ports/PortResponseDto.java.ejs +28 -0
  104. package/templates/postman/Collection.json.ejs +1 -1
  105. package/templates/postman/UnifiedCollection.json.ejs +185 -0
  106. package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
@@ -0,0 +1,30 @@
1
+ package <%= packageName %>.<%= moduleName %>.application.dtos;
2
+ <% const needsBigDecimal = bodyFields && bodyFields.some(f => f.javaType === 'BigDecimal'); %>
3
+ <% const needsLocalDate = bodyFields && bodyFields.some(f => ['LocalDate','LocalDateTime','LocalTime'].includes(f.javaType)); %>
4
+ <% const needsInstant = bodyFields && bodyFields.some(f => f.javaType === 'Instant'); %>
5
+ <% const needsUUID = bodyFields && bodyFields.some(f => f.javaType === 'UUID'); %>
6
+ <% const needsList = bodyFields && bodyFields.some(f => f.javaType && f.javaType.startsWith('List')); %>
7
+ <% if (needsBigDecimal) { %>
8
+ import java.math.BigDecimal;
9
+ <% } %><% if (needsLocalDate) { %>
10
+ import java.time.LocalDate;
11
+ import java.time.LocalDateTime;
12
+ import java.time.LocalTime;
13
+ <% } %><% if (needsInstant) { %>
14
+ import java.time.Instant;
15
+ <% } %><% if (needsUUID) { %>
16
+ import java.util.UUID;
17
+ <% } %><% if (needsList) { %>
18
+ import java.util.List;
19
+ <% } %>
20
+ <% (nestedTypes || []).forEach(nt => { %>import <%= packageName %>.<%= moduleName %>.application.dtos.<%= nt.name %>;
21
+ <% }); %>
22
+ public record <%= dtoName %>(
23
+ <% if (bodyFields && bodyFields.length > 0) { %>
24
+ <% bodyFields.forEach((f, i) => { %>
25
+ <%- f.javaType %> <%= f.name %><%= i < bodyFields.length - 1 ? ',' : '' %>
26
+ <% }); %>
27
+ <% } else { %>
28
+ // TODO: Add request body fields
29
+ <% } %>
30
+ ) {}
@@ -0,0 +1,28 @@
1
+ package <%= packageName %>.<%= moduleName %>.infrastructure.adapters.<%= adapterPackage %>;
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
+ <% const needsList = fields && fields.some(f => f.javaType && f.javaType.startsWith('List')); %>
7
+ <% if (needsBigDecimal) { %>
8
+ import java.math.BigDecimal;
9
+ <% } %><% if (needsLocalDate) { %>
10
+ import java.time.LocalDate;
11
+ import java.time.LocalDateTime;
12
+ import java.time.LocalTime;
13
+ <% } %><% if (needsInstant) { %>
14
+ import java.time.Instant;
15
+ <% } %><% if (needsUUID) { %>
16
+ import java.util.UUID;
17
+ <% } %><% if (needsList) { %>
18
+ import java.util.List;
19
+ <% } %>
20
+ public record <%= dtoName %>(
21
+ <% if (fields && fields.length > 0) { %>
22
+ <% fields.forEach((f, i) => { %>
23
+ <%- f.javaType %> <%= f.name %><%= i < fields.length - 1 ? ',' : '' %>
24
+ <% }); %>
25
+ <% } else { %>
26
+ // TODO: Add response fields
27
+ <% } %>
28
+ ) {}
@@ -250,7 +250,7 @@ function generateCreateBody() {
250
250
  "response": []
251
251
  },
252
252
  {
253
- "name": "Get All <%= aggregateName %>s",
253
+ "name": "Get All <%= aggregateNamePlural %>",
254
254
  "request": {
255
255
  "method": "GET",
256
256
  "header": [],
@@ -0,0 +1,185 @@
1
+ <%
2
+ /**
3
+ * UnifiedCollection.json.ejs
4
+ *
5
+ * Generates a single Postman 2.1.0 collection covering ALL modules in the
6
+ * system. The hierarchy is:
7
+ *
8
+ * {SystemName} API
9
+ * └── {ModuleName}
10
+ * └── {AggregateName}
11
+ * ├── POST Create {Agg}
12
+ * ├── GET Get {Agg} by ID
13
+ * ├── GET Get All {Agg}s
14
+ * ├── PUT Update {Agg}
15
+ * ├── DELETE Delete {Agg}
16
+ * └── PUT {Custom} {Agg} (confirm, cancel, …)
17
+ *
18
+ * Context expected:
19
+ * - systemName : string
20
+ * - collectionId : string (UUID)
21
+ * - port : number
22
+ * - modules : Array<ModuleCtx>
23
+ *
24
+ * ModuleCtx:
25
+ * - name : string (module name, e.g. "product")
26
+ * - aggregates : Array<AggregateCtx>
27
+ *
28
+ * AggregateCtx:
29
+ * - name : string (PascalCase, e.g. "Product")
30
+ * - trackUser : boolean
31
+ * - idType : string ("String" | "UUID" | "Long")
32
+ * - exampleId : string
33
+ * - operations : Array<OperationCtx> (when endpoints: section exists)
34
+ * - defaultCrud : boolean (true when no endpoints: section)
35
+ * - bodies : { [useCase]: object } (pre-generated JSON bodies keyed by useCase)
36
+ *
37
+ * OperationCtx:
38
+ * - useCase : string
39
+ * - method : string (GET, POST, PUT, PATCH, DELETE)
40
+ * - path : string (e.g. "/{id}" or "/publish")
41
+ * - basePath : string (e.g. "/products")
42
+ * - version : string (e.g. "v1")
43
+ */
44
+
45
+ function buildUrl(port, version, basePath, opPath) {
46
+ // Normalise: remove leading slash from basePath if present, and from opPath
47
+ let bPath = basePath.replace(/^\//, '');
48
+ let oPath = opPath.replace(/^\//, '');
49
+
50
+ // Build full path segments
51
+ const segments = ['api', version];
52
+ if (bPath) segments.push(...bPath.split('/').filter(Boolean));
53
+ if (oPath) segments.push(...oPath.split('/').filter(Boolean));
54
+
55
+ const raw = 'http://localhost:' + port + '/' + segments.join('/');
56
+ return { raw, segments, port: String(port) };
57
+ }
58
+
59
+ function hasBody(method) {
60
+ return method === 'POST' || method === 'PUT' || method === 'PATCH';
61
+ }
62
+
63
+ // ----- Build the items tree ------------------------------------------------
64
+ const moduleItems = [];
65
+
66
+ for (const mod of modules) {
67
+ const aggregateItems = [];
68
+
69
+ for (const agg of mod.aggregates) {
70
+ const requestItems = [];
71
+
72
+ if (agg.operations && agg.operations.length > 0) {
73
+ // Endpoint-driven: use declared operations
74
+ for (const op of agg.operations) {
75
+ const url = buildUrl(port, op.version, op.basePath, op.path);
76
+ // Replace {id} with example ID
77
+ const rawUrl = url.raw.replace(/\{id\}/g, agg.exampleId);
78
+ const pathSegments = url.segments.map(s => s === '{id}' ? agg.exampleId : s);
79
+
80
+ const headers = [{ key: 'Content-Type', value: 'application/json', type: 'text' }];
81
+ if (agg.trackUser) {
82
+ headers.push({ key: 'X-User', value: 'system', type: 'text' });
83
+ }
84
+
85
+ const item = {
86
+ name: op.useCase,
87
+ request: {
88
+ method: op.method,
89
+ header: headers,
90
+ url: {
91
+ raw: rawUrl,
92
+ protocol: 'http',
93
+ host: ['localhost'],
94
+ port: String(port),
95
+ path: pathSegments
96
+ }
97
+ },
98
+ response: []
99
+ };
100
+
101
+ if (hasBody(op.method) && agg.bodies[op.useCase]) {
102
+ item.request.body = {
103
+ mode: 'raw',
104
+ raw: JSON.stringify(agg.bodies[op.useCase], null, 2),
105
+ options: { raw: { language: 'json' } }
106
+ };
107
+ }
108
+
109
+ requestItems.push(item);
110
+ }
111
+ } else if (agg.defaultCrud) {
112
+ // No endpoints section → generate 5 standard CRUD requests
113
+ const resource = agg.resourceNameKebab;
114
+ const crudOps = [
115
+ { useCase: 'Create ' + agg.name, method: 'POST', path: '' },
116
+ { useCase: 'Get ' + agg.name + ' by ID', method: 'GET', path: '/' + agg.exampleId },
117
+ { useCase: 'Get All ' + agg.name + 's', method: 'GET', path: '' },
118
+ { useCase: 'Update ' + agg.name, method: 'PUT', path: '/' + agg.exampleId },
119
+ { useCase: 'Delete ' + agg.name, method: 'DELETE', path: '/' + agg.exampleId },
120
+ ];
121
+
122
+ for (const op of crudOps) {
123
+ const url = buildUrl(port, 'v1', resource, op.path);
124
+
125
+ const headers = [{ key: 'Content-Type', value: 'application/json', type: 'text' }];
126
+ if (agg.trackUser) {
127
+ headers.push({ key: 'X-User', value: 'system', type: 'text' });
128
+ }
129
+
130
+ const item = {
131
+ name: op.useCase,
132
+ request: {
133
+ method: op.method,
134
+ header: headers,
135
+ url: {
136
+ raw: url.raw,
137
+ protocol: 'http',
138
+ host: ['localhost'],
139
+ port: String(port),
140
+ path: url.segments
141
+ }
142
+ },
143
+ response: []
144
+ };
145
+
146
+ if (hasBody(op.method) && agg.bodies['default']) {
147
+ item.request.body = {
148
+ mode: 'raw',
149
+ raw: JSON.stringify(agg.bodies['default'], null, 2),
150
+ options: { raw: { language: 'json' } }
151
+ };
152
+ }
153
+
154
+ requestItems.push(item);
155
+ }
156
+ }
157
+
158
+ if (requestItems.length > 0) {
159
+ aggregateItems.push({
160
+ name: agg.name,
161
+ item: requestItems
162
+ });
163
+ }
164
+ }
165
+
166
+ if (aggregateItems.length > 0) {
167
+ moduleItems.push({
168
+ name: mod.name,
169
+ item: aggregateItems
170
+ });
171
+ }
172
+ }
173
+
174
+ const collection = {
175
+ info: {
176
+ _postman_id: collectionId,
177
+ name: systemName + ' API',
178
+ description: 'Auto-generated unified API collection for all modules in ' + systemName + '.\nGenerated by eva4j with realistic fake data.',
179
+ schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
180
+ version: '1.0.0'
181
+ },
182
+ item: moduleItems
183
+ };
184
+ %>
185
+ <%- JSON.stringify(collection, null, 2) %>
@@ -0,0 +1,109 @@
1
+ package <%= packageName %>.shared.infrastructure.configurations.eventPublicationConfig;
2
+
3
+ import java.util.Map;
4
+ import javax.sql.DataSource;
5
+
6
+ import org.hibernate.boot.model.relational.Namespace;
7
+ import org.hibernate.boot.model.relational.Sequence;
8
+ import org.hibernate.mapping.Table;
9
+ import org.hibernate.tool.schema.spi.SchemaFilter;
10
+ import org.hibernate.tool.schema.spi.SchemaFilterProvider;
11
+ import org.slf4j.Logger;
12
+ import org.slf4j.LoggerFactory;
13
+ import org.springframework.boot.ApplicationRunner;
14
+ import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
15
+ import org.springframework.context.annotation.Bean;
16
+ import org.springframework.context.annotation.Configuration;
17
+
18
+ /**
19
+ * Fixes Spring Modulith JPA event_publication table columns.
20
+ * <p>
21
+ * Hibernate maps the internal {@code JpaEventPublication} String fields
22
+ * as {@code varchar(255)} by default, which is too short for serialised
23
+ * domain event payloads.
24
+ * <p>
25
+ * This config does two things:
26
+ * <ol>
27
+ * <li>Excludes the {@code event_publication} table from Hibernate DDL auto
28
+ * so it never tries to ALTER columns back to {@code varchar(255)}.</li>
29
+ * <li>Runs an {@link ApplicationRunner} that ensures the columns are {@code TEXT}.</li>
30
+ * </ol>
31
+ */
32
+ @Configuration
33
+ public class EventPublicationSchemaConfig implements HibernatePropertiesCustomizer {
34
+
35
+ private static final Logger log = LoggerFactory.getLogger(EventPublicationSchemaConfig.class);
36
+
37
+ @Override
38
+ public void customize(Map<String, Object> hibernateProperties) {
39
+ hibernateProperties.put(
40
+ "hibernate.hbm2ddl.schema_filter_provider",
41
+ new EventPublicationFilterProvider()
42
+ );
43
+ }
44
+
45
+ @Bean
46
+ ApplicationRunner eventPublicationSchemaFix(DataSource dataSource) {
47
+ return args -> {
48
+ try (var conn = dataSource.getConnection()) {
49
+ var db = conn.getMetaData().getDatabaseProductName().toLowerCase();
50
+ String[] statements;
51
+
52
+ if (db.contains("postgre")) {
53
+ statements = new String[]{
54
+ "CREATE TABLE IF NOT EXISTS event_publication (id UUID NOT NULL PRIMARY KEY, completion_date TIMESTAMP, event_type TEXT NOT NULL, listener_id TEXT NOT NULL, publication_date TIMESTAMP, serialized_event TEXT NOT NULL)",
55
+ "ALTER TABLE event_publication ALTER COLUMN serialized_event TYPE TEXT",
56
+ "ALTER TABLE event_publication ALTER COLUMN event_type TYPE TEXT",
57
+ "ALTER TABLE event_publication ALTER COLUMN listener_id TYPE TEXT"
58
+ };
59
+ } else if (db.contains("mysql") || db.contains("maria")) {
60
+ statements = new String[]{
61
+ "CREATE TABLE IF NOT EXISTS event_publication (id VARCHAR(36) NOT NULL PRIMARY KEY, completion_date DATETIME, event_type TEXT NOT NULL, listener_id TEXT NOT NULL, publication_date DATETIME, serialized_event TEXT NOT NULL)",
62
+ "ALTER TABLE event_publication MODIFY COLUMN serialized_event TEXT",
63
+ "ALTER TABLE event_publication MODIFY COLUMN event_type TEXT",
64
+ "ALTER TABLE event_publication MODIFY COLUMN listener_id TEXT"
65
+ };
66
+ } else {
67
+ // H2 and other databases
68
+ statements = new String[]{
69
+ "CREATE TABLE IF NOT EXISTS event_publication (id UUID NOT NULL PRIMARY KEY, completion_date TIMESTAMP, event_type TEXT NOT NULL, listener_id TEXT NOT NULL, publication_date TIMESTAMP, serialized_event TEXT NOT NULL)",
70
+ "ALTER TABLE IF EXISTS event_publication ALTER COLUMN serialized_event TEXT",
71
+ "ALTER TABLE IF EXISTS event_publication ALTER COLUMN event_type TEXT",
72
+ "ALTER TABLE IF EXISTS event_publication ALTER COLUMN listener_id TEXT"
73
+ };
74
+ }
75
+
76
+ for (var sql : statements) {
77
+ try (var stmt = conn.createStatement()) {
78
+ stmt.execute(sql);
79
+ } catch (Exception e) {
80
+ // Column already TEXT or statement not applicable — safe to ignore
81
+ }
82
+ }
83
+ log.debug("event_publication columns verified (TEXT)");
84
+ }
85
+ };
86
+ }
87
+
88
+ // ── Hibernate Schema Filter ────────────────────────────────────────────
89
+ // Excludes event_publication from ddl-auto so Hibernate never tries
90
+ // to ALTER its columns back to varchar(255).
91
+
92
+ static class EventPublicationFilterProvider implements SchemaFilterProvider {
93
+ private static final SchemaFilter FILTER = new EventPublicationFilter();
94
+
95
+ @Override public SchemaFilter getCreateFilter() { return FILTER; }
96
+ @Override public SchemaFilter getDropFilter() { return FILTER; }
97
+ @Override public SchemaFilter getMigrateFilter() { return FILTER; }
98
+ @Override public SchemaFilter getValidateFilter() { return FILTER; }
99
+ @Override public SchemaFilter getTruncatorFilter() { return FILTER; }
100
+ }
101
+
102
+ static class EventPublicationFilter implements SchemaFilter {
103
+ @Override public boolean includeNamespace(Namespace namespace) { return true; }
104
+ @Override public boolean includeSequence(Sequence sequence) { return true; }
105
+ @Override public boolean includeTable(Table table) {
106
+ return !"event_publication".equalsIgnoreCase(table.getName());
107
+ }
108
+ }
109
+ }