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.
Files changed (44) hide show
  1. package/AGENTS.md +51 -9
  2. package/DOMAIN_YAML_GUIDE.md +150 -0
  3. package/bin/eva4j.js +31 -1
  4. package/design-system.md +797 -0
  5. package/docs/commands/EVALUATE_SYSTEM.md +542 -0
  6. package/docs/commands/GENERATE_ENTITIES.md +196 -0
  7. package/docs/commands/INDEX.md +10 -1
  8. package/examples/domain-endpoints-relations.yaml +353 -0
  9. package/examples/domain-endpoints-versioned.yaml +144 -0
  10. package/examples/domain-endpoints.yaml +135 -0
  11. package/examples/system.yaml +289 -0
  12. package/package.json +1 -1
  13. package/src/commands/create.js +6 -3
  14. package/src/commands/evaluate-system.js +384 -0
  15. package/src/commands/generate-entities.js +677 -14
  16. package/src/commands/generate-kafka-event.js +59 -5
  17. package/src/commands/generate-system.js +243 -0
  18. package/src/generators/base-generator.js +9 -1
  19. package/src/utils/naming.js +3 -2
  20. package/src/utils/system-validator.js +314 -0
  21. package/src/utils/yaml-to-entity.js +31 -2
  22. package/templates/aggregate/AggregateRepository.java.ejs +5 -0
  23. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +9 -0
  24. package/templates/aggregate/DomainEventHandler.java.ejs +24 -20
  25. package/templates/aggregate/JpaRepository.java.ejs +5 -0
  26. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1103 -0
  27. package/templates/base/root/skill-build-domain-yaml.ejs +292 -0
  28. package/templates/base/root/skill-build-system-yaml.ejs +252 -0
  29. package/templates/base/root/system.yaml.ejs +97 -0
  30. package/templates/crud/EndpointsController.java.ejs +178 -0
  31. package/templates/crud/FindByQuery.java.ejs +17 -0
  32. package/templates/crud/FindByQueryHandler.java.ejs +57 -0
  33. package/templates/crud/ScaffoldCommand.java.ejs +12 -0
  34. package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
  35. package/templates/crud/ScaffoldQuery.java.ejs +12 -0
  36. package/templates/crud/ScaffoldQueryHandler.java.ejs +40 -0
  37. package/templates/crud/SubEntityAddCommand.java.ejs +17 -0
  38. package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
  39. package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
  40. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
  41. package/templates/crud/TransitionCommand.java.ejs +9 -0
  42. package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
  43. package/templates/evaluate/report.html.ejs +971 -0
  44. 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` |