eva4j 1.0.13 → 1.0.14
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 +51 -9
- package/DOMAIN_YAML_GUIDE.md +150 -0
- package/bin/eva4j.js +31 -1
- package/design-system.md +797 -0
- package/docs/commands/EVALUATE_SYSTEM.md +542 -0
- package/docs/commands/GENERATE_ENTITIES.md +196 -0
- package/docs/commands/INDEX.md +10 -1
- package/examples/domain-endpoints-relations.yaml +353 -0
- package/examples/domain-endpoints-versioned.yaml +144 -0
- package/examples/domain-endpoints.yaml +135 -0
- package/examples/system.yaml +289 -0
- package/package.json +1 -1
- package/src/commands/create.js +6 -3
- package/src/commands/evaluate-system.js +384 -0
- package/src/commands/generate-entities.js +677 -14
- package/src/commands/generate-kafka-event.js +59 -5
- package/src/commands/generate-system.js +243 -0
- package/src/generators/base-generator.js +9 -1
- package/src/utils/naming.js +3 -2
- package/src/utils/system-validator.js +314 -0
- package/src/utils/yaml-to-entity.js +31 -2
- package/templates/aggregate/AggregateRepository.java.ejs +5 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +9 -0
- package/templates/aggregate/DomainEventHandler.java.ejs +24 -20
- package/templates/aggregate/JpaRepository.java.ejs +5 -0
- package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1103 -0
- package/templates/base/root/skill-build-domain-yaml.ejs +292 -0
- package/templates/base/root/skill-build-system-yaml.ejs +252 -0
- package/templates/base/root/system.yaml.ejs +97 -0
- package/templates/crud/EndpointsController.java.ejs +178 -0
- package/templates/crud/FindByQuery.java.ejs +17 -0
- package/templates/crud/FindByQueryHandler.java.ejs +57 -0
- package/templates/crud/ScaffoldCommand.java.ejs +12 -0
- package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
- package/templates/crud/ScaffoldQuery.java.ejs +12 -0
- package/templates/crud/ScaffoldQueryHandler.java.ejs +40 -0
- package/templates/crud/SubEntityAddCommand.java.ejs +17 -0
- package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
- package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
- package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
- package/templates/crud/TransitionCommand.java.ejs +9 -0
- package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
- package/templates/evaluate/report.html.ejs +971 -0
- package/templates/kafka-event/Event.java.ejs +7 -0
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
# Command `generate entities` (alias: `g entities`)
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Description and purpose](#1-description-and-purpose)
|
|
8
|
+
2. [Syntax and YAML location](#2-syntax-and-yaml-location)
|
|
9
|
+
3. [Base domain.yaml structure](#3-base-domainyaml-structure)
|
|
10
|
+
4. [Supported data types](#4-supported-data-types)
|
|
11
|
+
5. [Field properties](#5-field-properties)
|
|
12
|
+
6. [JSR-303 Validations](#6-jsr-303-validations)
|
|
13
|
+
7. [Auditing](#7-auditing)
|
|
14
|
+
8. [Relationships](#8-relationships)
|
|
15
|
+
9. [Value Objects](#9-value-objects)
|
|
16
|
+
10. [Enums and state transitions](#10-enums-and-state-transitions)
|
|
17
|
+
11. [Domain events](#11-domain-events)
|
|
18
|
+
12. [Multiple aggregates](#12-multiple-aggregates)
|
|
19
|
+
13. [Generated files](#13-generated-files)
|
|
20
|
+
14. [Complete examples](#14-complete-examples)
|
|
21
|
+
15. [Prerequisites and common errors](#15-prerequisites-and-common-errors)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 1. Description and purpose
|
|
26
|
+
|
|
27
|
+
`generate entities` is the core command of eva4j. From a `domain.yaml` file, it generates the complete hexagonal architecture for the module:
|
|
28
|
+
|
|
29
|
+
- **Domain layer** – Entities, Value Objects, Enums, repository interfaces
|
|
30
|
+
- **Application layer** – Commands, Queries, handlers, DTOs, mappers
|
|
31
|
+
- **Infrastructure layer** – JPA entities, Spring Data repositories, repository implementations, REST controllers
|
|
32
|
+
|
|
33
|
+
The generator understands relationships, auditing, field visibility, validations, state transitions, and domain events.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Syntax and YAML location
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
eva generate entities <module>
|
|
41
|
+
eva g entities <module> # short alias
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Parameters
|
|
45
|
+
|
|
46
|
+
| Parameter | Required | Description |
|
|
47
|
+
|-----------|----------|-------------|
|
|
48
|
+
| `<module>` | Yes | Module name (must already exist in the project) |
|
|
49
|
+
|
|
50
|
+
### Options
|
|
51
|
+
|
|
52
|
+
| Option | Description |
|
|
53
|
+
|--------|-------------|
|
|
54
|
+
| `--force` | Overwrite files that have developer changes |
|
|
55
|
+
|
|
56
|
+
### YAML location
|
|
57
|
+
|
|
58
|
+
The file is read from:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
src/main/java/<package>/<module>/domain.yaml
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> The generator detects developer changes via checksums. If a file was manually modified, it is **not overwritten** unless you use `--force`.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 3. Base domain.yaml structure
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
aggregates: # List of aggregates in the module
|
|
72
|
+
- name: Order # Aggregate name (PascalCase)
|
|
73
|
+
entities: # Entities of the aggregate
|
|
74
|
+
- name: Order # Entity name (PascalCase)
|
|
75
|
+
isRoot: true # true = aggregate root
|
|
76
|
+
tableName: orders # SQL table name (optional)
|
|
77
|
+
audit: # Auditing (optional)
|
|
78
|
+
enabled: true
|
|
79
|
+
trackUser: false
|
|
80
|
+
fields: # Entity fields
|
|
81
|
+
- name: id
|
|
82
|
+
type: String
|
|
83
|
+
- name: status
|
|
84
|
+
type: OrderStatus # Reference to enum or VO
|
|
85
|
+
relationships: # JPA relationships (optional)
|
|
86
|
+
- type: OneToMany
|
|
87
|
+
target: OrderItem
|
|
88
|
+
mappedBy: order
|
|
89
|
+
cascade: [PERSIST, MERGE, REMOVE]
|
|
90
|
+
fetch: LAZY
|
|
91
|
+
|
|
92
|
+
- name: OrderItem # Secondary entity (no isRoot or isRoot: false)
|
|
93
|
+
tableName: order_items
|
|
94
|
+
fields:
|
|
95
|
+
- name: id
|
|
96
|
+
type: Long
|
|
97
|
+
- name: quantity
|
|
98
|
+
type: Integer
|
|
99
|
+
|
|
100
|
+
valueObjects: # Aggregate Value Objects
|
|
101
|
+
- name: Money
|
|
102
|
+
fields:
|
|
103
|
+
- name: amount
|
|
104
|
+
type: BigDecimal
|
|
105
|
+
- name: currency
|
|
106
|
+
type: String
|
|
107
|
+
|
|
108
|
+
enums: # Aggregate enums
|
|
109
|
+
- name: OrderStatus
|
|
110
|
+
values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
|
|
111
|
+
|
|
112
|
+
events: # Domain events (optional)
|
|
113
|
+
- name: OrderPlaced
|
|
114
|
+
fields:
|
|
115
|
+
- name: customerId
|
|
116
|
+
type: String
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
> **Supported synonyms**: `fields` = `properties`; `target` = `targetEntity`
|
|
120
|
+
|
|
121
|
+
### The `id` field rule
|
|
122
|
+
|
|
123
|
+
Every entity **must** have a field named exactly `id`:
|
|
124
|
+
|
|
125
|
+
| `id` type | Generated strategy |
|
|
126
|
+
|-----------|--------------------|
|
|
127
|
+
| `String` | `@GeneratedValue(strategy = GenerationType.UUID)` |
|
|
128
|
+
| `Long` | `@GeneratedValue(strategy = GenerationType.IDENTITY)` |
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 4. Supported data types
|
|
133
|
+
|
|
134
|
+
| YAML type | Java type | Notes |
|
|
135
|
+
|-----------|-----------|-------|
|
|
136
|
+
| `String` | `String` | For `id` generates UUID |
|
|
137
|
+
| `Integer` | `Integer` | For `id` generates IDENTITY |
|
|
138
|
+
| `Long` | `Long` | For `id` generates IDENTITY |
|
|
139
|
+
| `Double` | `Double` | |
|
|
140
|
+
| `BigDecimal` | `BigDecimal` | |
|
|
141
|
+
| `Boolean` | `Boolean` | |
|
|
142
|
+
| `LocalDate` | `LocalDate` | Auto-imported |
|
|
143
|
+
| `LocalDateTime` | `LocalDateTime` | Auto-imported |
|
|
144
|
+
| `LocalTime` | `LocalTime` | Auto-imported |
|
|
145
|
+
| `UUID` | `UUID` | Auto-imported |
|
|
146
|
+
| `List<String>` | `List<String>` | `@ElementCollection` |
|
|
147
|
+
| `List<VO>` | `List<VoJpa>` | `@ElementCollection` |
|
|
148
|
+
| Enum name | Module enum | `@Enumerated(STRING)` |
|
|
149
|
+
| VO name | Value Object | `@Embedded` |
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 5. Field properties
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
fields:
|
|
157
|
+
- name: fieldName # camelCase, required
|
|
158
|
+
type: String # Java type, required
|
|
159
|
+
readOnly: false # default false
|
|
160
|
+
hidden: false # default false
|
|
161
|
+
validations: [] # JSR-303 annotations
|
|
162
|
+
annotations: [] # raw JPA annotations
|
|
163
|
+
reference: # semantic reference to another aggregate
|
|
164
|
+
aggregate: Customer
|
|
165
|
+
module: customers
|
|
166
|
+
enumValues: [] # inline enum (alternative to enums:)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Visibility matrix
|
|
170
|
+
|
|
171
|
+
| Field | Creation constructor | CreateDto/Command | Full constructor | ResponseDto |
|
|
172
|
+
|-------|---------------------|-------------------|------------------|-------------|
|
|
173
|
+
| normal | ✅ | ✅ | ✅ | ✅ |
|
|
174
|
+
| `readOnly: true` | ❌ | ❌ | ✅ | ✅ |
|
|
175
|
+
| `hidden: true` | ✅ | ✅ | ✅ | ❌ |
|
|
176
|
+
| `readOnly + hidden` | ❌ | ❌ | ✅ | ❌ |
|
|
177
|
+
|
|
178
|
+
### readOnly
|
|
179
|
+
|
|
180
|
+
Marks a field as calculated/derived: excluded from the business constructor and `CreateDto`/`CreateCommand`, but present in the full constructor (reconstruction from persistence) and in `ResponseDto`.
|
|
181
|
+
|
|
182
|
+
```yaml
|
|
183
|
+
fields:
|
|
184
|
+
- name: totalAmount
|
|
185
|
+
type: BigDecimal
|
|
186
|
+
readOnly: true # calculated from the sum of items
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
> When an enum has `initialValue`, the corresponding field is automatically treated as `readOnly`.
|
|
190
|
+
|
|
191
|
+
### hidden
|
|
192
|
+
|
|
193
|
+
Marks a field as sensitive: included on creation but does NOT appear in `ResponseDto`.
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
fields:
|
|
197
|
+
- name: passwordHash
|
|
198
|
+
type: String
|
|
199
|
+
hidden: true # do not expose in API
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### annotations (raw JPA)
|
|
203
|
+
|
|
204
|
+
Allows adding custom JPA annotations to the generated JPA entity.
|
|
205
|
+
|
|
206
|
+
```yaml
|
|
207
|
+
fields:
|
|
208
|
+
- name: email
|
|
209
|
+
type: String
|
|
210
|
+
annotations:
|
|
211
|
+
- "@Column(unique = true, nullable = false)"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### reference
|
|
215
|
+
|
|
216
|
+
Declares a semantic reference to a field in another aggregate. Generates a Javadoc comment indicating the relationship, without creating a code dependency.
|
|
217
|
+
|
|
218
|
+
```yaml
|
|
219
|
+
fields:
|
|
220
|
+
- name: customerId
|
|
221
|
+
type: String
|
|
222
|
+
reference:
|
|
223
|
+
aggregate: Customer
|
|
224
|
+
module: customers
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Generated in the domain entity:
|
|
228
|
+
|
|
229
|
+
```java
|
|
230
|
+
/** @see customers.Customer */
|
|
231
|
+
private String customerId;
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 6. JSR-303 Validations
|
|
237
|
+
|
|
238
|
+
Validations are declared on the field and applied to `CreateCommand` and `CreateDto`. They are **not** added to domain entities.
|
|
239
|
+
|
|
240
|
+
```yaml
|
|
241
|
+
fields:
|
|
242
|
+
- name: name
|
|
243
|
+
type: String
|
|
244
|
+
validations:
|
|
245
|
+
- type: NotBlank
|
|
246
|
+
message: "Name is required"
|
|
247
|
+
- type: Size
|
|
248
|
+
min: 2
|
|
249
|
+
max: 100
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Auto-generates import: `import jakarta.validation.constraints.*;`
|
|
253
|
+
|
|
254
|
+
### Supported parameters
|
|
255
|
+
|
|
256
|
+
| Parameter | Description |
|
|
257
|
+
|-----------|-------------|
|
|
258
|
+
| `type` | Annotation name without `@` (required) |
|
|
259
|
+
| `message` | Custom error message |
|
|
260
|
+
| `value` | Single value (for `@Min`, `@Max`) |
|
|
261
|
+
| `min` | Minimum value (for `@Size`, `@DecimalMin`) |
|
|
262
|
+
| `max` | Maximum value (for `@Size`, `@DecimalMax`) |
|
|
263
|
+
| `regexp` | Regular expression (for `@Pattern`) |
|
|
264
|
+
| `integer` | Integer digits (for `@Digits`) |
|
|
265
|
+
| `fraction` | Decimal digits (for `@Digits`) |
|
|
266
|
+
| `inclusive` | Inclusive boundary (for `@DecimalMin`, `@DecimalMax`) |
|
|
267
|
+
|
|
268
|
+
### Examples by type
|
|
269
|
+
|
|
270
|
+
```yaml
|
|
271
|
+
# @NotBlank
|
|
272
|
+
- type: NotBlank
|
|
273
|
+
message: "Field is required"
|
|
274
|
+
|
|
275
|
+
# @NotNull
|
|
276
|
+
- type: NotNull
|
|
277
|
+
|
|
278
|
+
# @Size
|
|
279
|
+
- type: Size
|
|
280
|
+
min: 2
|
|
281
|
+
max: 255
|
|
282
|
+
|
|
283
|
+
# @Email
|
|
284
|
+
- type: Email
|
|
285
|
+
|
|
286
|
+
# @Min / @Max (for numeric fields)
|
|
287
|
+
- type: Min
|
|
288
|
+
value: 1
|
|
289
|
+
- type: Max
|
|
290
|
+
value: 999
|
|
291
|
+
|
|
292
|
+
# @Pattern
|
|
293
|
+
- type: Pattern
|
|
294
|
+
regexp: "^[A-Z]{2}[0-9]{6}$"
|
|
295
|
+
message: "Invalid format"
|
|
296
|
+
|
|
297
|
+
# @DecimalMin / @DecimalMax
|
|
298
|
+
- type: DecimalMin
|
|
299
|
+
min: "0.01"
|
|
300
|
+
inclusive: true
|
|
301
|
+
- type: DecimalMax
|
|
302
|
+
max: "9999.99"
|
|
303
|
+
|
|
304
|
+
# @Digits
|
|
305
|
+
- type: Digits
|
|
306
|
+
integer: 6
|
|
307
|
+
fraction: 2
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## 7. Auditing
|
|
313
|
+
|
|
314
|
+
### Syntax
|
|
315
|
+
|
|
316
|
+
```yaml
|
|
317
|
+
# New (recommended)
|
|
318
|
+
audit:
|
|
319
|
+
enabled: true # adds createdAt, updatedAt
|
|
320
|
+
trackUser: true # also adds createdBy, updatedBy
|
|
321
|
+
|
|
322
|
+
# Legacy (equivalent to audit.enabled: true, trackUser: false)
|
|
323
|
+
auditable: true
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Generated JPA inheritance
|
|
327
|
+
|
|
328
|
+
| Configuration | JPA base class |
|
|
329
|
+
|---------------|----------------|
|
|
330
|
+
| No auditing | no inheritance |
|
|
331
|
+
| `audit.enabled: true` | `extends AuditableEntity` |
|
|
332
|
+
| `audit.trackUser: true` | `extends FullAuditableEntity` |
|
|
333
|
+
|
|
334
|
+
### Generated fields
|
|
335
|
+
|
|
336
|
+
| Field | `audit.enabled` | `audit.trackUser` | In ResponseDto |
|
|
337
|
+
|-------|-----------------|-------------------|----------------|
|
|
338
|
+
| `createdAt` | ✅ | ✅ | ✅ |
|
|
339
|
+
| `updatedAt` | ✅ | ✅ | ✅ |
|
|
340
|
+
| `createdBy` | ❌ | ✅ | ❌ |
|
|
341
|
+
| `updatedBy` | ❌ | ✅ | ❌ |
|
|
342
|
+
|
|
343
|
+
> `createdBy` and `updatedBy` are administrative metadata: they are never exposed in response DTOs.
|
|
344
|
+
|
|
345
|
+
### Infrastructure generated with `trackUser: true`
|
|
346
|
+
|
|
347
|
+
When `trackUser` is enabled, eva4j automatically generates:
|
|
348
|
+
|
|
349
|
+
| File | Purpose |
|
|
350
|
+
|------|---------|
|
|
351
|
+
| `UserContextHolder.java` | ThreadLocal for the current user |
|
|
352
|
+
| `UserContextFilter.java` | Captures the `X-User` header from each request |
|
|
353
|
+
| `AuditorAwareImpl.java` | Provides the current user to JPA Auditing |
|
|
354
|
+
|
|
355
|
+
`Application.java` is configured with `@EnableJpaAuditing(auditorAwareRef = "auditorProvider")`.
|
|
356
|
+
|
|
357
|
+
### Example
|
|
358
|
+
|
|
359
|
+
```yaml
|
|
360
|
+
entities:
|
|
361
|
+
- name: Order
|
|
362
|
+
isRoot: true
|
|
363
|
+
tableName: orders
|
|
364
|
+
audit:
|
|
365
|
+
enabled: true
|
|
366
|
+
trackUser: true
|
|
367
|
+
fields:
|
|
368
|
+
- name: id
|
|
369
|
+
type: String
|
|
370
|
+
- name: amount
|
|
371
|
+
type: BigDecimal
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
> Audit fields **must not be defined manually** in `fields:`; they are inherited from the JPA base class.
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## 8. Relationships
|
|
379
|
+
|
|
380
|
+
### Properties
|
|
381
|
+
|
|
382
|
+
| Property | Values | Description |
|
|
383
|
+
|----------|--------|-------------|
|
|
384
|
+
| `type` | `OneToMany`, `ManyToOne`, `OneToOne`, `ManyToMany` | Relationship type |
|
|
385
|
+
| `target` / `targetEntity` | Entity name | Related entity |
|
|
386
|
+
| `mappedBy` | field name | Inverse side of the relationship |
|
|
387
|
+
| `joinColumn` | column name | FK column name |
|
|
388
|
+
| `cascade` | array of `PERSIST`, `MERGE`, `REMOVE`, `REFRESH`, `DETACH`, `ALL` | Cascade operations |
|
|
389
|
+
| `fetch` | `LAZY` (default), `EAGER` | Loading strategy |
|
|
390
|
+
|
|
391
|
+
### Automatic inverse side generation
|
|
392
|
+
|
|
393
|
+
When you define `OneToMany` with `mappedBy`, eva4j automatically generates `@ManyToOne` in the target JPA entity. **Defining both sides is not required.**
|
|
394
|
+
|
|
395
|
+
```yaml
|
|
396
|
+
# ✅ Only this is needed
|
|
397
|
+
entities:
|
|
398
|
+
- name: Order
|
|
399
|
+
isRoot: true
|
|
400
|
+
relationships:
|
|
401
|
+
- type: OneToMany
|
|
402
|
+
target: OrderItem
|
|
403
|
+
mappedBy: order
|
|
404
|
+
cascade: [PERSIST, MERGE, REMOVE]
|
|
405
|
+
fetch: LAZY
|
|
406
|
+
|
|
407
|
+
# eva4j generates in OrderItemJpa:
|
|
408
|
+
# @ManyToOne(fetch = FetchType.LAZY)
|
|
409
|
+
# @JoinColumn(name = "order_id")
|
|
410
|
+
# private OrderJpa order;
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
> If you define `ManyToOne` manually, that definition takes priority over auto-generation.
|
|
414
|
+
|
|
415
|
+
### OneToMany
|
|
416
|
+
|
|
417
|
+
```yaml
|
|
418
|
+
relationships:
|
|
419
|
+
- type: OneToMany
|
|
420
|
+
target: OrderItem
|
|
421
|
+
mappedBy: order
|
|
422
|
+
cascade: [PERSIST, MERGE, REMOVE]
|
|
423
|
+
fetch: LAZY
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Generated in domain:
|
|
427
|
+
|
|
428
|
+
```java
|
|
429
|
+
private List<OrderItem> orderItems = new ArrayList<>();
|
|
430
|
+
public void addOrderItem(OrderItem item) { orderItems.add(item); }
|
|
431
|
+
public void removeOrderItem(OrderItem item) { orderItems.remove(item); }
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### ManyToOne (manual, when you need a specific FK)
|
|
435
|
+
|
|
436
|
+
```yaml
|
|
437
|
+
relationships:
|
|
438
|
+
- type: ManyToOne
|
|
439
|
+
target: Order
|
|
440
|
+
joinColumn: fk_order_uuid
|
|
441
|
+
fetch: LAZY
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### OneToOne
|
|
445
|
+
|
|
446
|
+
```yaml
|
|
447
|
+
# Inverse side (with mappedBy)
|
|
448
|
+
relationships:
|
|
449
|
+
- type: OneToOne
|
|
450
|
+
target: OrderSummary
|
|
451
|
+
mappedBy: order
|
|
452
|
+
cascade: [PERSIST, MERGE]
|
|
453
|
+
fetch: LAZY
|
|
454
|
+
|
|
455
|
+
# Owner side (with FK)
|
|
456
|
+
relationships:
|
|
457
|
+
- type: OneToOne
|
|
458
|
+
target: Order
|
|
459
|
+
joinColumn: order_id
|
|
460
|
+
fetch: LAZY
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### When to define ManyToOne manually
|
|
464
|
+
|
|
465
|
+
| Scenario | Define ManyToOne? |
|
|
466
|
+
|----------|------------------|
|
|
467
|
+
| Standard relationship with `mappedBy` | ❌ eva4j generates it |
|
|
468
|
+
| FK with custom name | ✅ Yes, to control `joinColumn` |
|
|
469
|
+
| Multiple FKs to the same entity | ✅ Yes, for distinct names |
|
|
470
|
+
| Unidirectional relationship (no inverse) | ✅ Yes |
|
|
471
|
+
|
|
472
|
+
### Recommended cascade
|
|
473
|
+
|
|
474
|
+
```yaml
|
|
475
|
+
# Child has no meaning without parent → include REMOVE
|
|
476
|
+
cascade: [PERSIST, MERGE, REMOVE]
|
|
477
|
+
|
|
478
|
+
# Child has an independent lifecycle
|
|
479
|
+
cascade: [PERSIST, MERGE]
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## 9. Value Objects
|
|
485
|
+
|
|
486
|
+
Immutable objects that represent domain concepts without their own identity.
|
|
487
|
+
|
|
488
|
+
```yaml
|
|
489
|
+
valueObjects:
|
|
490
|
+
- name: Money
|
|
491
|
+
fields:
|
|
492
|
+
- name: amount
|
|
493
|
+
type: BigDecimal
|
|
494
|
+
- name: currency
|
|
495
|
+
type: String
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Generates:
|
|
499
|
+
|
|
500
|
+
- `Money.java` – immutable domain class with constructor, getters, `equals()`, `hashCode()`
|
|
501
|
+
- `MoneyJpa.java` – `@Embeddable` with Lombok
|
|
502
|
+
|
|
503
|
+
Usage in a field:
|
|
504
|
+
|
|
505
|
+
```yaml
|
|
506
|
+
- name: totalAmount
|
|
507
|
+
type: Money # automatically detected as @Embedded
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### List of Value Objects
|
|
511
|
+
|
|
512
|
+
```yaml
|
|
513
|
+
- name: addresses
|
|
514
|
+
type: List<Address>
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Generates:
|
|
518
|
+
|
|
519
|
+
```java
|
|
520
|
+
@ElementCollection
|
|
521
|
+
@CollectionTable(name = "entity_addresses", joinColumns = @JoinColumn(name = "entity_id"))
|
|
522
|
+
@Builder.Default
|
|
523
|
+
private List<AddressJpa> addresses = new ArrayList<>();
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## 10. Enums and state transitions
|
|
529
|
+
|
|
530
|
+
### Simple enum
|
|
531
|
+
|
|
532
|
+
```yaml
|
|
533
|
+
enums:
|
|
534
|
+
- name: OrderStatus
|
|
535
|
+
values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
Generates `OrderStatus.java` with the enumerated values. In JPA: `@Enumerated(EnumType.STRING)`.
|
|
539
|
+
|
|
540
|
+
### Enum with state transitions
|
|
541
|
+
|
|
542
|
+
Transitions generate business methods in the entity, validation logic in the enum, and prevent invalid states.
|
|
543
|
+
|
|
544
|
+
```yaml
|
|
545
|
+
enums:
|
|
546
|
+
- name: OrderStatus
|
|
547
|
+
initialValue: PENDING # assigns an initial value; field becomes readOnly
|
|
548
|
+
values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
|
|
549
|
+
transitions:
|
|
550
|
+
- from: PENDING # can be a string or [array]
|
|
551
|
+
to: CONFIRMED
|
|
552
|
+
method: confirm # name of the method generated in the entity
|
|
553
|
+
- from: [PENDING, CONFIRMED]
|
|
554
|
+
to: CANCELLED
|
|
555
|
+
method: cancel
|
|
556
|
+
guard: "this.status == OrderStatus.DELIVERED" # throws BusinessException if true
|
|
557
|
+
- from: CONFIRMED
|
|
558
|
+
to: SHIPPED
|
|
559
|
+
method: ship
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
#### What is generated in the Enum
|
|
563
|
+
|
|
564
|
+
```java
|
|
565
|
+
private static final Map<OrderStatus, List<OrderStatus>> VALID_TRANSITIONS = Map.of(
|
|
566
|
+
PENDING, List.of(CONFIRMED, CANCELLED),
|
|
567
|
+
CONFIRMED, List.of(SHIPPED, CANCELLED),
|
|
568
|
+
SHIPPED, List.of(DELIVERED));
|
|
569
|
+
|
|
570
|
+
public boolean canTransitionTo(OrderStatus next) {
|
|
571
|
+
return VALID_TRANSITIONS.getOrDefault(this, List.of()).contains(next);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
public OrderStatus transitionTo(OrderStatus next) {
|
|
575
|
+
if (!canTransitionTo(next)) {
|
|
576
|
+
throw new InvalidStateTransitionException(this, next);
|
|
577
|
+
}
|
|
578
|
+
return next;
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
#### What is generated in the aggregate root
|
|
583
|
+
|
|
584
|
+
One method per transition, plus `is*()` and `can*()` helpers:
|
|
585
|
+
|
|
586
|
+
```java
|
|
587
|
+
public void confirm() {
|
|
588
|
+
this.status = this.status.transitionTo(OrderStatus.CONFIRMED);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
public void cancel() {
|
|
592
|
+
if (this.status == OrderStatus.DELIVERED) {
|
|
593
|
+
throw new BusinessException("Cannot cancel a delivered order");
|
|
594
|
+
}
|
|
595
|
+
this.status = this.status.transitionTo(OrderStatus.CANCELLED);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
public boolean isPending() { return this.status == OrderStatus.PENDING; }
|
|
599
|
+
public boolean canConfirm() { return this.status.canTransitionTo(OrderStatus.CONFIRMED); }
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### `initialValue`
|
|
603
|
+
|
|
604
|
+
Assigns a default value to the status field in the creation constructor. The field is automatically marked as `readOnly` (does not appear in `CreateDto`/`CreateCommand`).
|
|
605
|
+
|
|
606
|
+
```yaml
|
|
607
|
+
enums:
|
|
608
|
+
- name: OrderStatus
|
|
609
|
+
initialValue: PENDING
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### `guard`
|
|
613
|
+
|
|
614
|
+
Java condition evaluated in the transition method. If the expression is `true`, a `BusinessException` is thrown.
|
|
615
|
+
|
|
616
|
+
```yaml
|
|
617
|
+
- from: [PENDING, CONFIRMED]
|
|
618
|
+
to: CANCELLED
|
|
619
|
+
method: cancel
|
|
620
|
+
guard: "this.totalAmount.compareTo(BigDecimal.ZERO) == 0"
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## 11. Domain events
|
|
626
|
+
|
|
627
|
+
Events are declared under the aggregate (at the same level as `entities:`, `enums:`, `valueObjects:`).
|
|
628
|
+
|
|
629
|
+
```yaml
|
|
630
|
+
aggregates:
|
|
631
|
+
- name: Order
|
|
632
|
+
events:
|
|
633
|
+
- name: OrderPlaced
|
|
634
|
+
fields:
|
|
635
|
+
- name: customerId
|
|
636
|
+
type: String
|
|
637
|
+
- name: totalAmount
|
|
638
|
+
type: BigDecimal
|
|
639
|
+
- name: OrderCancelled
|
|
640
|
+
fields:
|
|
641
|
+
- name: reason
|
|
642
|
+
type: String
|
|
643
|
+
entities:
|
|
644
|
+
- name: Order
|
|
645
|
+
# ...
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### Generated files
|
|
649
|
+
|
|
650
|
+
| File | Description |
|
|
651
|
+
|------|-------------|
|
|
652
|
+
| `shared/domain/DomainEvent.java` | Abstract base class (generated once per project) |
|
|
653
|
+
| `domain/models/events/OrderPlaced.java` | Concrete event extending `DomainEvent` |
|
|
654
|
+
| `domain/models/events/OrderCancelled.java` | Concrete event |
|
|
655
|
+
| `raise()` / `pullDomainEvents()` in the aggregate root | Event infrastructure in the entity |
|
|
656
|
+
| `OrderRepositoryImpl.java` | Calls `eventPublisher.publishEvent()` when saving |
|
|
657
|
+
| `OrderDomainEventHandler.java` | Class with `@TransactionalEventListener` per event |
|
|
658
|
+
|
|
659
|
+
### Generated event
|
|
660
|
+
|
|
661
|
+
```java
|
|
662
|
+
public final class OrderPlaced extends DomainEvent {
|
|
663
|
+
private final String customerId;
|
|
664
|
+
private final BigDecimal totalAmount;
|
|
665
|
+
|
|
666
|
+
public OrderPlaced(String customerId, BigDecimal totalAmount) {
|
|
667
|
+
this.customerId = customerId;
|
|
668
|
+
this.totalAmount = totalAmount;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// getters
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### How to raise an event in the entity
|
|
676
|
+
|
|
677
|
+
```java
|
|
678
|
+
public class Order {
|
|
679
|
+
private final List<DomainEvent> domainEvents = new ArrayList<>();
|
|
680
|
+
|
|
681
|
+
public void place(String customerId, BigDecimal totalAmount) {
|
|
682
|
+
// business logic...
|
|
683
|
+
raise(new OrderPlaced(customerId, totalAmount));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
protected void raise(DomainEvent event) {
|
|
687
|
+
domainEvents.add(event);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
public List<DomainEvent> pullDomainEvents() {
|
|
691
|
+
List<DomainEvent> events = new ArrayList<>(domainEvents);
|
|
692
|
+
domainEvents.clear();
|
|
693
|
+
return events;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
---
|
|
699
|
+
|
|
700
|
+
## 12. Multiple aggregates
|
|
701
|
+
|
|
702
|
+
A `domain.yaml` can contain multiple aggregates. Each one generates its own set of files.
|
|
703
|
+
|
|
704
|
+
```yaml
|
|
705
|
+
aggregates:
|
|
706
|
+
- name: Customer
|
|
707
|
+
entities:
|
|
708
|
+
- name: Customer
|
|
709
|
+
isRoot: true
|
|
710
|
+
fields:
|
|
711
|
+
- name: id
|
|
712
|
+
type: String
|
|
713
|
+
- name: email
|
|
714
|
+
type: String
|
|
715
|
+
|
|
716
|
+
- name: Product
|
|
717
|
+
entities:
|
|
718
|
+
- name: Product
|
|
719
|
+
isRoot: true
|
|
720
|
+
fields:
|
|
721
|
+
- name: id
|
|
722
|
+
type: String
|
|
723
|
+
- name: name
|
|
724
|
+
type: String
|
|
725
|
+
enums:
|
|
726
|
+
- name: ProductCategory
|
|
727
|
+
values: [ELECTRONICS, CLOTHING, FOOD]
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
> Enums and Value Objects are local to the aggregate where they are defined. If two aggregates need the same VO, it must be declared in each one.
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
## 13. Generated files
|
|
735
|
+
|
|
736
|
+
For each aggregate, approximately the following files are generated:
|
|
737
|
+
|
|
738
|
+
| File | Layer | Description |
|
|
739
|
+
|------|-------|-------------|
|
|
740
|
+
| `{Root}.java` | Domain | Aggregate root entity |
|
|
741
|
+
| `{Entity}.java` | Domain | Secondary entities |
|
|
742
|
+
| `{Vo}.java` | Domain | Value Objects |
|
|
743
|
+
| `{Enum}.java` | Domain | Enums (with VALID_TRANSITIONS if transitions exist) |
|
|
744
|
+
| `{Root}Repository.java` | Domain | Repository interface (port) |
|
|
745
|
+
| `Create{Root}Command.java` | Application | Create command |
|
|
746
|
+
| `Create{Root}CommandHandler.java` | Application | Command handler |
|
|
747
|
+
| `Get{Root}Query.java` | Application | Get by ID query |
|
|
748
|
+
| `Get{Root}QueryHandler.java` | Application | Query handler |
|
|
749
|
+
| `List{Root}Query.java` | Application | Paginated list query |
|
|
750
|
+
| `List{Root}QueryHandler.java` | Application | List handler |
|
|
751
|
+
| `{Root}ResponseDto.java` | Application | Response DTO |
|
|
752
|
+
| `Create{Root}Dto.java` | Application | Create DTO |
|
|
753
|
+
| `{Root}ApplicationMapper.java` | Application | Mapper Command/DTO ↔ Domain |
|
|
754
|
+
| `{Root}Jpa.java` | Infrastructure | JPA entity |
|
|
755
|
+
| `{Entity}Jpa.java` | Infrastructure | Secondary JPA entities |
|
|
756
|
+
| `{Vo}Jpa.java` | Infrastructure | JPA Value Objects (@Embeddable) |
|
|
757
|
+
| `{Root}Mapper.java` | Infrastructure | Mapper Domain ↔ JPA |
|
|
758
|
+
| `{Root}JpaRepository.java` | Infrastructure | Spring Data repository |
|
|
759
|
+
| `{Root}RepositoryImpl.java` | Infrastructure | Repository implementation |
|
|
760
|
+
| `{Root}Controller.java` | Infrastructure | REST controller |
|
|
761
|
+
|
|
762
|
+
### Generated REST endpoints
|
|
763
|
+
|
|
764
|
+
| Method | Path | Description |
|
|
765
|
+
|--------|------|-------------|
|
|
766
|
+
| `POST` | `/api/{module}/{entity}` | Create |
|
|
767
|
+
| `GET` | `/api/{module}/{entity}/{id}` | Get by ID |
|
|
768
|
+
| `GET` | `/api/{module}/{entity}?page=0&size=20` | Paginated list |
|
|
769
|
+
| `PUT` | `/api/{module}/{entity}/{id}` | Update |
|
|
770
|
+
| `DELETE` | `/api/{module}/{entity}/{id}` | Delete |
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
774
|
+
## 14. Complete examples
|
|
775
|
+
|
|
776
|
+
### Example 1: Order with transitions and events
|
|
777
|
+
|
|
778
|
+
```yaml
|
|
779
|
+
aggregates:
|
|
780
|
+
- name: Order
|
|
781
|
+
entities:
|
|
782
|
+
- name: Order
|
|
783
|
+
isRoot: true
|
|
784
|
+
tableName: orders
|
|
785
|
+
audit:
|
|
786
|
+
enabled: true
|
|
787
|
+
fields:
|
|
788
|
+
- name: id
|
|
789
|
+
type: String
|
|
790
|
+
- name: customerId
|
|
791
|
+
type: String
|
|
792
|
+
reference:
|
|
793
|
+
aggregate: Customer
|
|
794
|
+
module: customers
|
|
795
|
+
- name: status
|
|
796
|
+
type: OrderStatus
|
|
797
|
+
- name: totalAmount
|
|
798
|
+
type: BigDecimal
|
|
799
|
+
readOnly: true
|
|
800
|
+
relationships:
|
|
801
|
+
- type: OneToMany
|
|
802
|
+
target: OrderItem
|
|
803
|
+
mappedBy: order
|
|
804
|
+
cascade: [PERSIST, MERGE, REMOVE]
|
|
805
|
+
fetch: LAZY
|
|
806
|
+
|
|
807
|
+
- name: OrderItem
|
|
808
|
+
tableName: order_items
|
|
809
|
+
fields:
|
|
810
|
+
- name: id
|
|
811
|
+
type: Long
|
|
812
|
+
- name: productId
|
|
813
|
+
type: String
|
|
814
|
+
- name: quantity
|
|
815
|
+
type: Integer
|
|
816
|
+
validations:
|
|
817
|
+
- type: Min
|
|
818
|
+
value: 1
|
|
819
|
+
- name: unitPrice
|
|
820
|
+
type: BigDecimal
|
|
821
|
+
|
|
822
|
+
enums:
|
|
823
|
+
- name: OrderStatus
|
|
824
|
+
initialValue: PENDING
|
|
825
|
+
values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
|
|
826
|
+
transitions:
|
|
827
|
+
- from: PENDING
|
|
828
|
+
to: CONFIRMED
|
|
829
|
+
method: confirm
|
|
830
|
+
- from: CONFIRMED
|
|
831
|
+
to: SHIPPED
|
|
832
|
+
method: ship
|
|
833
|
+
- from: [PENDING, CONFIRMED]
|
|
834
|
+
to: CANCELLED
|
|
835
|
+
method: cancel
|
|
836
|
+
guard: "this.status == OrderStatus.DELIVERED"
|
|
837
|
+
|
|
838
|
+
events:
|
|
839
|
+
- name: OrderPlaced
|
|
840
|
+
fields:
|
|
841
|
+
- name: customerId
|
|
842
|
+
type: String
|
|
843
|
+
- name: OrderCancelled
|
|
844
|
+
fields:
|
|
845
|
+
- name: reason
|
|
846
|
+
type: String
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
### Example 2: User with auditing and a sensitive field
|
|
850
|
+
|
|
851
|
+
```yaml
|
|
852
|
+
aggregates:
|
|
853
|
+
- name: User
|
|
854
|
+
entities:
|
|
855
|
+
- name: User
|
|
856
|
+
isRoot: true
|
|
857
|
+
tableName: users
|
|
858
|
+
audit:
|
|
859
|
+
enabled: true
|
|
860
|
+
trackUser: true
|
|
861
|
+
fields:
|
|
862
|
+
- name: id
|
|
863
|
+
type: String
|
|
864
|
+
- name: username
|
|
865
|
+
type: String
|
|
866
|
+
validations:
|
|
867
|
+
- type: NotBlank
|
|
868
|
+
- type: Size
|
|
869
|
+
min: 3
|
|
870
|
+
max: 50
|
|
871
|
+
- name: email
|
|
872
|
+
type: String
|
|
873
|
+
validations:
|
|
874
|
+
- type: Email
|
|
875
|
+
annotations:
|
|
876
|
+
- "@Column(unique = true)"
|
|
877
|
+
- name: passwordHash
|
|
878
|
+
type: String
|
|
879
|
+
hidden: true
|
|
880
|
+
- name: role
|
|
881
|
+
type: UserRole
|
|
882
|
+
- name: active
|
|
883
|
+
type: Boolean
|
|
884
|
+
|
|
885
|
+
enums:
|
|
886
|
+
- name: UserRole
|
|
887
|
+
values: [ADMIN, USER, MODERATOR]
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
---
|
|
891
|
+
|
|
892
|
+
## 15. Prerequisites and common errors
|
|
893
|
+
|
|
894
|
+
### Prerequisites
|
|
895
|
+
|
|
896
|
+
- Project created with `eva create`
|
|
897
|
+
- Existing module (`eva add module <module>`)
|
|
898
|
+
- `domain.yaml` file at `src/main/java/<package>/<module>/`
|
|
899
|
+
|
|
900
|
+
### Common errors
|
|
901
|
+
|
|
902
|
+
| Error | Cause | Solution |
|
|
903
|
+
|-------|-------|----------|
|
|
904
|
+
| `Module does not exist` | Module was not created | Run `eva add module <module>` |
|
|
905
|
+
| `YAML file not found` | No `domain.yaml` at the expected path | Check `src/main/java/<pkg>/<module>/domain.yaml` |
|
|
906
|
+
| `Invalid relationship target` | Target entity not defined in the same YAML | Define the target entity in the same `domain.yaml` |
|
|
907
|
+
| `Column 'x_id' is duplicated` | ManyToOne defined manually + auto-generated | Remove the manual ManyToOne; let eva4j generate it |
|
|
908
|
+
| File not regenerated | File was manually modified (checksum) | Use `--force` to overwrite |
|
|
909
|
+
| Import errors | Field `type` doesn't match name in `enums:` or `valueObjects:` | Verify names match exactly |
|
|
910
|
+
|
|
911
|
+
---
|
|
912
|
+
|
|
913
|
+
## 16. Declarative endpoints (`endpoints:`) — Use case patterns
|
|
914
|
+
|
|
915
|
+
When `domain.yaml` includes an `endpoints:` section, the generator examines each `useCase` name and classifies it semantically before generating code. This determines whether a full, working implementation or a scaffold stub is produced.
|
|
916
|
+
|
|
917
|
+
### 16.1 Pattern table
|
|
918
|
+
|
|
919
|
+
| Pattern | Category | Recognition condition | What is generated |
|
|
920
|
+
|---------|----------|----------------------|-------------------|
|
|
921
|
+
| `Create{Aggregate}` | **standard** | Exact string match | Full `CreateCommand` + `CreateCommandHandler` (`ApplicationMapper.fromCommand → save`) |
|
|
922
|
+
| `Update{Aggregate}` | **standard** | Exact string match | Full `UpdateCommand` + `UpdateCommandHandler` |
|
|
923
|
+
| `Delete{Aggregate}` | **standard** | Exact string match | Full `DeleteCommand` + `DeleteCommandHandler` |
|
|
924
|
+
| `Get{Aggregate}` | **standard** | Exact string match | Full `GetQuery` + `GetQueryHandler` (find + `mapper.toDto`) |
|
|
925
|
+
| `FindAll{Aggregate}s` | **standard** | Exact string match (trailing literal `s`) | Full `ListQuery` + `ListQueryHandler` (paginated) |
|
|
926
|
+
| `{MethodPascal}{Aggregate}` | **transition** | `MethodPascal` is `toPascalCase(transitions[n].method)` for any enum in the aggregate | Full `TransitionCommand(id)` + handler that calls `entity.{method}() → save()` |
|
|
927
|
+
| `Add{EntityName}` | **subEntityAdd** | `EntityName` is the `target` of a `OneToMany` relationship on the root | Full `AddCommand(id, entityFields…)` + handler that calls `entity.add{Entity}(new {Entity}(…)) → save()` |
|
|
928
|
+
| `Remove{EntityName}` | **subEntityRemove** | Same `target` from a `OneToMany` relationship | Full `RemoveCommand(id, itemId)` + handler that calls `entity.remove{Entity}ById(itemId) → save()` |
|
|
929
|
+
| `FindAll{Aggregate}sBy{FieldPascal}` | **findBy** | `FieldPascal` is `toPascalCase(fieldName)` for any field in the root entity | Full `FindByQuery` + `FindByQueryHandler` + `findBy{FieldPascal}` added to `{Aggregate}Repository`, `{Aggregate}RepositoryImpl`, and `{Aggregate}JpaRepository` |
|
|
930
|
+
| _anything else_ | **scaffold** | No pattern matched | `*Command(id)` or `*Query(id)` + handler that throws `UnsupportedOperationException` with a TODO comment |
|
|
931
|
+
|
|
932
|
+
> **Note on `FindAll{Aggregate}s`:** The trailing `s` is a literal character. For `Aggregate = Order` the standard name is `FindAllOrders` (not `FindAllOrder`). Irregular plurals are not supported — use the exact pattern or it will be classified as scaffold.
|
|
933
|
+
|
|
934
|
+
### 16.2 Transition pattern in detail
|
|
935
|
+
|
|
936
|
+
Enumerate state transitions in the `enums:` block using `transitions`:
|
|
937
|
+
|
|
938
|
+
```yaml
|
|
939
|
+
enums:
|
|
940
|
+
- name: OrderStatus
|
|
941
|
+
transitions:
|
|
942
|
+
- from: PENDING
|
|
943
|
+
to: CONFIRMED
|
|
944
|
+
method: confirm # → recognized as ConfirmOrder (PascalCase(confirm) + Order)
|
|
945
|
+
- from: [PENDING, CONFIRMED]
|
|
946
|
+
to: CANCELLED
|
|
947
|
+
method: cancel # → CancelOrder
|
|
948
|
+
- from: CONFIRMED
|
|
949
|
+
to: SHIPPED
|
|
950
|
+
method: ship # → ShipOrder
|
|
951
|
+
values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
Declare the corresponding use cases in `endpoints:`:
|
|
955
|
+
|
|
956
|
+
```yaml
|
|
957
|
+
endpoints:
|
|
958
|
+
basePath: /orders
|
|
959
|
+
versions:
|
|
960
|
+
- version: v1
|
|
961
|
+
operations:
|
|
962
|
+
- method: PUT
|
|
963
|
+
path: /{id}/confirm
|
|
964
|
+
useCase: ConfirmOrder # ← transition pattern: confirm + Order
|
|
965
|
+
type: command
|
|
966
|
+
- method: PUT
|
|
967
|
+
path: /{id}/cancel
|
|
968
|
+
useCase: CancelOrder
|
|
969
|
+
type: command
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
**Generated output:**
|
|
973
|
+
|
|
974
|
+
```java
|
|
975
|
+
// ConfirmOrderCommand.java
|
|
976
|
+
public record ConfirmOrderCommand(String id) implements Command {}
|
|
977
|
+
|
|
978
|
+
// ConfirmOrderCommandHandler.java
|
|
979
|
+
@Transactional
|
|
980
|
+
public void handle(ConfirmOrderCommand command) {
|
|
981
|
+
Order entity = repository.findById(command.id())
|
|
982
|
+
.orElseThrow(() -> new NotFoundException("Order not found with id: " + command.id()));
|
|
983
|
+
entity.confirm(); // ← the domain method from transitions[].method
|
|
984
|
+
repository.save(entity);
|
|
985
|
+
}
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
### 16.3 Sub-entity add/remove pattern in detail
|
|
989
|
+
|
|
990
|
+
Requirements:
|
|
991
|
+
- A `OneToMany` relationship must be declared on the root entity pointing to the target entity.
|
|
992
|
+
- The aggregate root must expose `add{EntityName}({EntityName} item)` and `remove{EntityName}ById(String id)` domain methods (generated automatically by `eva g entities`).
|
|
993
|
+
|
|
994
|
+
```yaml
|
|
995
|
+
# aggregates: section
|
|
996
|
+
relationships:
|
|
997
|
+
- type: OneToMany
|
|
998
|
+
target: OrderItem # ← entityName used to match Add/Remove pattern
|
|
999
|
+
fieldName: items
|
|
1000
|
+
...
|
|
1001
|
+
|
|
1002
|
+
# endpoints: section
|
|
1003
|
+
operations:
|
|
1004
|
+
- method: POST
|
|
1005
|
+
path: /{id}/items
|
|
1006
|
+
useCase: AddOrderItem # ← Add + OrderItem (target name)
|
|
1007
|
+
type: command
|
|
1008
|
+
- method: DELETE
|
|
1009
|
+
path: /{id}/items/{itemId}
|
|
1010
|
+
useCase: RemoveOrderItem # ← Remove + OrderItem
|
|
1011
|
+
type: command
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
**Generated output:**
|
|
1015
|
+
|
|
1016
|
+
```java
|
|
1017
|
+
// AddOrderItemCommand.java — fields taken from OrderItem (non-id, non-audit, non-readOnly)
|
|
1018
|
+
public record AddOrderItemCommand(
|
|
1019
|
+
String id,
|
|
1020
|
+
String productId,
|
|
1021
|
+
String productName,
|
|
1022
|
+
Integer quantity,
|
|
1023
|
+
BigDecimal unitPrice
|
|
1024
|
+
) implements Command {}
|
|
1025
|
+
|
|
1026
|
+
// AddOrderItemCommandHandler.java
|
|
1027
|
+
@Transactional
|
|
1028
|
+
public void handle(AddOrderItemCommand command) {
|
|
1029
|
+
Order entity = repository.findById(command.id()) ...;
|
|
1030
|
+
OrderItem item = new OrderItem(command.productId(), command.productName(),
|
|
1031
|
+
command.quantity(), command.unitPrice());
|
|
1032
|
+
entity.addOrderItem(item);
|
|
1033
|
+
repository.save(entity);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// RemoveOrderItemCommand.java
|
|
1037
|
+
public record RemoveOrderItemCommand(String id, String itemId) implements Command {}
|
|
1038
|
+
|
|
1039
|
+
// RemoveOrderItemCommandHandler.java
|
|
1040
|
+
@Transactional
|
|
1041
|
+
public void handle(RemoveOrderItemCommand command) {
|
|
1042
|
+
Order entity = repository.findById(command.id()) ...;
|
|
1043
|
+
entity.removeOrderItemById(command.itemId());
|
|
1044
|
+
repository.save(entity);
|
|
1045
|
+
}
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
### 16.4 FindBy pattern in detail
|
|
1049
|
+
|
|
1050
|
+
Strict pattern: `FindAll{Aggregate}sBy{FieldPascal}` — both parts are required.
|
|
1051
|
+
|
|
1052
|
+
When detected, the generator:
|
|
1053
|
+
1. Creates a paginated `FindBy{Field}Query` + `FindBy{Field}QueryHandler`.
|
|
1054
|
+
2. Re-generates `{Aggregate}Repository.java` (domain interface), `{Aggregate}JpaRepository.java`, and `{Aggregate}RepositoryImpl.java` with the `findBy{FieldPascal}(FieldType value, Pageable pageable)` method added. Checksum protection still applies — manually modified files are skipped unless `--force` is used.
|
|
1055
|
+
|
|
1056
|
+
```yaml
|
|
1057
|
+
# Root entity field
|
|
1058
|
+
fields:
|
|
1059
|
+
- name: customerId
|
|
1060
|
+
type: String
|
|
1061
|
+
|
|
1062
|
+
# Endpoint
|
|
1063
|
+
operations:
|
|
1064
|
+
- method: GET
|
|
1065
|
+
path: /customer/{customerId}
|
|
1066
|
+
useCase: FindAllOrdersByCustomerId # ← FindAll + Order + s + By + CustomerId
|
|
1067
|
+
type: query
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
**Generated output:**
|
|
1071
|
+
|
|
1072
|
+
```java
|
|
1073
|
+
// FindAllOrdersByCustomerIdQuery.java
|
|
1074
|
+
public record FindAllOrdersByCustomerIdQuery(
|
|
1075
|
+
String customerId, int page, int size, String sortBy, String sortDirection
|
|
1076
|
+
) implements Query<PagedResponse<OrderResponseDto>> {}
|
|
1077
|
+
|
|
1078
|
+
// OrderRepository.java (domain interface — method appended)
|
|
1079
|
+
Page<Order> findByCustomerId(String customerId, Pageable pageable);
|
|
1080
|
+
|
|
1081
|
+
// OrderJpaRepository.java (Spring Data JPA — method auto-implemented)
|
|
1082
|
+
Page<OrderJpa> findByCustomerId(String customerId, Pageable pageable);
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
### 16.5 Scaffold (fallback)
|
|
1086
|
+
|
|
1087
|
+
Any `useCase` name that does not match any pattern above becomes a scaffold. A scaffold generates:
|
|
1088
|
+
|
|
1089
|
+
- A minimal `{UseCase}Command(String id)` or `{UseCase}Query(String id)` record.
|
|
1090
|
+
- A handler that throws `UnsupportedOperationException` and includes a step-by-step TODO comment.
|
|
1091
|
+
|
|
1092
|
+
This is intentional: the developer fills in the custom business logic while the wiring (registration, mediator dispatch, controller method) is already in place.
|
|
1093
|
+
|
|
1094
|
+
### 16.6 Naming rules
|
|
1095
|
+
|
|
1096
|
+
| What | Convention | Example |
|
|
1097
|
+
|------|-----------|---------|
|
|
1098
|
+
| Aggregate name | PascalCase | `Order` |
|
|
1099
|
+
| Use case name in YAML | PascalCase | `ConfirmOrder`, `FindAllOrdersByCustomerId` |
|
|
1100
|
+
| Transition method in YAML | camelCase | `confirm`, `cancelOrder` |
|
|
1101
|
+
| Pattern `{MethodPascal}` | `toPascalCase(method)` | `confirm` → `Confirm` |
|
|
1102
|
+
| Pattern `{FieldPascal}` | `toPascalCase(fieldName)` | `customerId` → `CustomerId` |
|
|
1103
|
+
| Sub-entity target | PascalCase (must match entity `name:`) | `OrderItem` |
|