eva4j 1.0.11 β†’ 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/AGENTS.md +441 -14
  2. package/DOMAIN_YAML_GUIDE.md +425 -21
  3. package/FUTURE_FEATURES.md +315 -115
  4. package/QUICK_REFERENCE.md +101 -153
  5. package/README.md +77 -70
  6. package/bin/eva4j.js +57 -1
  7. package/config/defaults.json +3 -0
  8. package/docs/commands/GENERATE_ENTITIES.md +662 -1968
  9. package/docs/commands/GENERATE_HTTP_EXCHANGE.md +274 -450
  10. package/docs/commands/GENERATE_KAFKA_EVENT.md +219 -498
  11. package/docs/commands/GENERATE_KAFKA_LISTENER.md +18 -18
  12. package/docs/commands/GENERATE_RECORD.md +335 -311
  13. package/docs/commands/GENERATE_TEMPORAL_ACTIVITY.md +174 -0
  14. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +237 -0
  15. package/docs/commands/GENERATE_USECASE.md +216 -282
  16. package/docs/commands/INDEX.md +36 -7
  17. package/examples/doctor-evaluation.yaml +3 -3
  18. package/examples/domain-audit-complete.yaml +2 -2
  19. package/examples/domain-collections.yaml +2 -2
  20. package/examples/domain-ecommerce.yaml +2 -2
  21. package/examples/domain-events.yaml +201 -0
  22. package/examples/domain-field-visibility.yaml +11 -5
  23. package/examples/domain-multi-aggregate.yaml +12 -6
  24. package/examples/domain-one-to-many.yaml +1 -1
  25. package/examples/domain-one-to-one.yaml +1 -1
  26. package/examples/domain-secondary-onetomany.yaml +1 -1
  27. package/examples/domain-secondary-onetoone.yaml +1 -1
  28. package/examples/domain-simple.yaml +1 -1
  29. package/examples/domain-soft-delete.yaml +3 -3
  30. package/examples/domain-transitions.yaml +1 -1
  31. package/examples/domain-value-objects.yaml +1 -1
  32. package/package.json +2 -2
  33. package/src/commands/add-kafka-client.js +3 -1
  34. package/src/commands/add-temporal-client.js +286 -0
  35. package/src/commands/generate-entities.js +75 -4
  36. package/src/commands/generate-kafka-event.js +273 -89
  37. package/src/commands/generate-temporal-activity.js +228 -0
  38. package/src/commands/generate-temporal-flow.js +216 -0
  39. package/src/generators/module-generator.js +1 -0
  40. package/src/generators/shared-generator.js +26 -0
  41. package/src/utils/yaml-to-entity.js +93 -4
  42. package/templates/aggregate/AggregateRepository.java.ejs +3 -2
  43. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +15 -7
  44. package/templates/aggregate/AggregateRoot.java.ejs +38 -2
  45. package/templates/aggregate/DomainEntity.java.ejs +6 -2
  46. package/templates/aggregate/DomainEventHandler.java.ejs +62 -0
  47. package/templates/aggregate/DomainEventRecord.java.ejs +50 -0
  48. package/templates/aggregate/JpaAggregateRoot.java.ejs +3 -1
  49. package/templates/aggregate/JpaEntity.java.ejs +3 -1
  50. package/templates/base/docker/kafka-services.yaml.ejs +2 -2
  51. package/templates/base/docker/temporal-services.yaml.ejs +29 -0
  52. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +9 -0
  53. package/templates/base/resources/parameters/local/temporal.yaml.ejs +9 -0
  54. package/templates/base/resources/parameters/production/temporal.yaml.ejs +9 -0
  55. package/templates/base/resources/parameters/test/temporal.yaml.ejs +9 -0
  56. package/templates/base/root/AGENTS.md.ejs +916 -51
  57. package/templates/crud/Controller.java.ejs +36 -6
  58. package/templates/crud/ListQuery.java.ejs +6 -2
  59. package/templates/crud/ListQueryHandler.java.ejs +24 -10
  60. package/templates/crud/UpdateCommand.java.ejs +52 -0
  61. package/templates/crud/UpdateCommandHandler.java.ejs +105 -0
  62. package/templates/kafka-event/DomainEventHandlerMethod.ejs +1 -0
  63. package/templates/kafka-event/Event.java.ejs +23 -0
  64. package/templates/shared/application/dtos/PagedResponse.java.ejs +30 -0
  65. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +104 -0
  66. package/templates/shared/domain/DomainEvent.java.ejs +40 -0
  67. package/templates/shared/interfaces/HeavyActivity.java.ejs +4 -0
  68. package/templates/shared/interfaces/LightActivity.java.ejs +4 -0
  69. package/templates/temporal-activity/ActivityImpl.java.ejs +14 -0
  70. package/templates/temporal-activity/ActivityInterface.java.ejs +11 -0
  71. package/templates/temporal-flow/WorkFlowImpl.java.ejs +64 -0
  72. package/templates/temporal-flow/WorkFlowInterface.java.ejs +19 -0
  73. package/templates/temporal-flow/WorkFlowService.java.ejs +49 -0
@@ -1,2215 +1,909 @@
1
1
  # Command `generate entities` (alias: `g entities`)
2
2
 
3
- ## πŸ“‹ Description
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
+ ---
4
24
 
5
- Generates complete domain model from a YAML definition file, including entities, value objects, enums, JPA mappings, repositories, and CRUD operations with CQRS pattern.
25
+ ## 1. Description and purpose
6
26
 
7
- ## 🎯 Purpose
27
+ `generate entities` is the core command of eva4j. From a `domain.yaml` file, it generates the complete hexagonal architecture for the module:
8
28
 
9
- Automate the creation of domain models with full hexagonal architecture implementation, eliminating repetitive coding and ensuring consistency across all layers (domain, application, infrastructure).
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
10
32
 
11
- ## πŸ“ Syntax
33
+ The generator understands relationships, auditing, field visibility, validations, state transitions, and domain events.
34
+
35
+ ---
36
+
37
+ ## 2. Syntax and YAML location
12
38
 
13
39
  ```bash
14
- eva4j generate entities <aggregate-name>
15
- eva4j g entities <aggregate-name> # Short alias
40
+ eva generate entities <module>
41
+ eva g entities <module> # short alias
16
42
  ```
17
43
 
18
44
  ### Parameters
19
45
 
20
46
  | Parameter | Required | Description |
21
47
  |-----------|----------|-------------|
22
- | `aggregate-name` | Yes | Name of the aggregate (must match YAML file name) |
23
-
24
- ## πŸ“„ YAML File Structure
25
-
26
- The command expects a YAML file at `examples/<aggregate-name>.yaml` with the following structure:
27
-
28
- ```yaml
29
- module: <module-name> # Target module for generation
30
-
31
- aggregates:
32
- - name: <AggregateName>
33
- tableName: <table_name>
34
- auditable: true|false
35
-
36
- entities:
37
- - name: <EntityName>
38
- isRoot: true|false
39
- tableName: <table_name>
40
- fields:
41
- - name: <fieldName>
42
- type: <JavaType|ValueObject|Enum>
43
- validations:
44
- - <@Annotation>
45
- relationships:
46
- - type: OneToMany|ManyToOne|OneToOne|ManyToMany
47
- target: <TargetEntity>
48
- mappedBy: <fieldName> # For inverse side
49
- cascade: ALL|PERSIST|MERGE
50
- fetch: LAZY|EAGER
51
-
52
- valueObjects:
53
- - name: <ValueObjectName>
54
- fields:
55
- - name: <fieldName>
56
- type: <JavaType>
57
-
58
- enums:
59
- - name: <EnumName>
60
- values:
61
- - VALUE1
62
- - VALUE2
63
- ```
48
+ | `<module>` | Yes | Module name (must already exist in the project) |
64
49
 
65
- ## πŸ’‘ Examples
50
+ ### Options
66
51
 
67
- ### Example 1: Simple Customer Aggregate
52
+ | Option | Description |
53
+ |--------|-------------|
54
+ | `--force` | Overwrite files that have developer changes |
68
55
 
69
- **File:** `examples/customer.yaml`
56
+ ### YAML location
70
57
 
71
- ```yaml
72
- module: customer
58
+ The file is read from:
73
59
 
74
- aggregates:
75
- - name: Customer
76
- tableName: customers
77
- auditable: true
78
-
79
- entities:
80
- - name: customer
81
- isRoot: true
82
- fields:
83
- - name: id
84
- type: Long
85
- - name: firstName
86
- type: String
87
- validations:
88
- - "@NotBlank"
89
- - "@Size(max = 100)"
90
- - name: email
91
- type: String
92
- validations:
93
- - "@Email"
94
- - name: status
95
- type: CustomerStatus
96
60
  ```
97
-
98
- **Generate:**
99
- ```bash
100
- eva4j g entities customer
61
+ src/main/java/<package>/<module>/domain.yaml
101
62
  ```
102
63
 
103
- ### Example 2: Complex Order with Relations
64
+ > The generator detects developer changes via checksums. If a file was manually modified, it is **not overwritten** unless you use `--force`.
104
65
 
105
- **File:** `examples/order.yaml`
66
+ ---
106
67
 
107
- ```yaml
108
- module: order
68
+ ## 3. Base domain.yaml structure
109
69
 
110
- aggregates:
111
- - name: Order
112
- tableName: orders
113
- auditable: true
114
-
115
- entities:
116
- - name: order
117
- isRoot: true
118
- fields:
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
119
81
  - name: id
120
- type: Long
121
- - name: orderNumber
122
82
  type: String
123
- - name: totalAmount
124
- type: BigDecimal
125
83
  - name: status
126
- type: OrderStatus
127
- relationships:
84
+ type: OrderStatus # Reference to enum or VO
85
+ relationships: # JPA relationships (optional)
128
86
  - type: OneToMany
129
87
  target: OrderItem
130
88
  mappedBy: order
131
- cascade: ALL
89
+ cascade: [PERSIST, MERGE, REMOVE]
132
90
  fetch: LAZY
133
-
134
- - name: orderItem
135
- isRoot: false
91
+
92
+ - name: OrderItem # Secondary entity (no isRoot or isRoot: false)
136
93
  tableName: order_items
137
94
  fields:
138
95
  - name: id
139
96
  type: Long
140
97
  - name: quantity
141
98
  type: Integer
142
- - name: unitPrice
99
+
100
+ valueObjects: # Aggregate Value Objects
101
+ - name: Money
102
+ fields:
103
+ - name: amount
143
104
  type: BigDecimal
144
- relationships:
145
- - type: ManyToOne
146
- target: Order
147
- fetch: LAZY
148
-
149
- enums:
105
+ - name: currency
106
+ type: String
107
+
108
+ enums: # Aggregate enums
150
109
  - name: OrderStatus
151
- values:
152
- - PENDING
153
- - CONFIRMED
154
- - SHIPPED
155
- - DELIVERED
156
- - CANCELLED
157
- ```
110
+ values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
158
111
 
159
- **Generate:**
160
- ```bash
161
- eva4j g entities order
112
+ events: # Domain events (optional)
113
+ - name: OrderPlaced
114
+ fields:
115
+ - name: customerId
116
+ type: String
162
117
  ```
163
118
 
164
- ### Example 3: With Value Objects
119
+ > **Supported synonyms**: `fields` = `properties`; `target` = `targetEntity`
165
120
 
166
- **File:** `examples/evaluation.yaml`
121
+ ### The `id` field rule
167
122
 
168
- ```yaml
169
- module: evaluation
123
+ Every entity **must** have a field named exactly `id`:
170
124
 
171
- aggregates:
172
- - name: Evaluation
173
- tableName: evaluations
174
-
175
- entities:
176
- - name: evaluation
177
- isRoot: true
178
- fields:
179
- - name: id
180
- type: String
181
- - name: score
182
- type: Integer
183
- relationships:
184
- - type: OneToMany
185
- target: EvaluationDoctor
186
- cascade: ALL
187
-
188
- - name: evaluationDoctor
189
- isRoot: false
190
- fields:
191
- - name: id
192
- type: Long
193
- - name: degrees
194
- type: List<Degrees>
195
-
196
- valueObjects:
197
- - name: Degrees
198
- fields:
199
- - name: title
200
- type: String
201
- - name: institution
202
- type: String
203
- - name: year
204
- type: Integer
205
- - name: typeDegrees
206
- type: TypeDegrees
207
-
208
- enums:
209
- - name: TypeDegrees
210
- values:
211
- - BACHELOR
212
- - MASTER
213
- - PHD
214
- ```
125
+ | `id` type | Generated strategy |
126
+ |-----------|--------------------|
127
+ | `String` | `@GeneratedValue(strategy = GenerationType.UUID)` |
128
+ | `Long` | `@GeneratedValue(strategy = GenerationType.IDENTITY)` |
215
129
 
216
- **Generate:**
217
- ```bash
218
- eva4j g entities evaluation
219
- ```
130
+ ---
220
131
 
221
- ## πŸ“¦ Generated Code Structure
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` |
222
150
 
223
- ```
224
- src/main/java/com/example/project/<module>/
225
- β”œβ”€β”€ domain/
226
- β”‚ β”œβ”€β”€ models/
227
- β”‚ β”‚ β”œβ”€β”€ Customer.java # Domain entity (root)
228
- β”‚ β”‚ β”œβ”€β”€ OrderItem.java # Domain entity (secondary)
229
- β”‚ β”‚ β”œβ”€β”€ valueobjects/
230
- β”‚ β”‚ β”‚ └── Degrees.java # Value object
231
- β”‚ β”‚ └── enums/
232
- β”‚ β”‚ └── OrderStatus.java # Enum
233
- β”‚ └── repositories/
234
- β”‚ └── CustomerRepository.java # Repository port (interface)
235
- β”‚
236
- β”œβ”€β”€ application/
237
- β”‚ β”œβ”€β”€ commands/
238
- β”‚ β”‚ β”œβ”€β”€ CreateCustomerCommand.java # Create command
239
- β”‚ β”‚ └── CreateCustomerCommandHandler.java # Command handler
240
- β”‚ β”œβ”€β”€ queries/
241
- β”‚ β”‚ β”œβ”€β”€ GetCustomerQuery.java # Get query
242
- β”‚ β”‚ β”œβ”€β”€ GetCustomerQueryHandler.java # Get handler
243
- β”‚ β”‚ β”œβ”€β”€ ListCustomersQuery.java # List query
244
- β”‚ β”‚ └── ListCustomersQueryHandler.java # List handler
245
- β”‚ β”œβ”€β”€ dtos/
246
- β”‚ β”‚ β”œβ”€β”€ CreateCustomerDto.java # Create DTO
247
- β”‚ β”‚ β”œβ”€β”€ CreateOrderItemDto.java # Nested entity DTO
248
- β”‚ β”‚ └── CustomerResponseDto.java # Response DTO
249
- β”‚ └── mappers/
250
- β”‚ └── CustomerApplicationMapper.java # Application mapper (Command/DTO β†’ Domain)
251
- β”‚
252
- └── infrastructure/
253
- β”œβ”€β”€ database/
254
- β”‚ β”œβ”€β”€ entities/
255
- β”‚ β”‚ β”œβ”€β”€ CustomerJpa.java # JPA entity (root)
256
- β”‚ β”‚ β”œβ”€β”€ OrderItemJpa.java # JPA entity (secondary)
257
- β”‚ β”‚ └── valueobjects/
258
- β”‚ β”‚ └── DegreesJpa.java # JPA value object
259
- β”‚ β”œβ”€β”€ repositories/
260
- β”‚ β”‚ β”œβ”€β”€ CustomerJpaRepository.java # Spring Data repository
261
- β”‚ β”‚ └── CustomerRepositoryImpl.java # Repository implementation
262
- β”‚ └── mappers/
263
- β”‚ └── CustomerMapper.java # Infrastructure mapper (Domain ↔ JPA)
264
- └── rest/
265
- └── controllers/
266
- └── CustomerController.java # REST controller with CRUD endpoints
267
- ```
151
+ ---
268
152
 
269
- ## ✨ Features
270
-
271
- ### 1. Domain Layer (Pure Business Logic)
272
- - βœ… **Entities** - Aggregate root and secondary entities
273
- - βœ… **Value Objects** - Immutable value types with `@Embedded` support
274
- - βœ… **Enums** - Type-safe enumerations
275
- - βœ… **Repository Interfaces** - Ports for persistence
276
-
277
- ### 2. Application Layer (Use Cases - CQRS)
278
- - βœ… **Commands** - `CreateCustomerCommand` with validation
279
- - βœ… **CommandHandlers** - Business logic orchestration
280
- - βœ… **Queries** - `GetCustomerQuery`, `ListCustomersQuery`
281
- - βœ… **QueryHandlers** - Read operations with pagination
282
- - βœ… **DTOs** - Request/Response data transfer objects
283
- - βœ… **Application Mappers** - Command/DTO β†’ Domain transformations
284
-
285
- ### 3. Infrastructure Layer (Technical Details)
286
- - βœ… **JPA Entities** - Persistence annotations (`@Entity`, `@Table`)
287
- - βœ… **JPA Repositories** - Spring Data JPA implementation
288
- - βœ… **Infrastructure Mappers** - Domain ↔ JPA bidirectional mapping
289
- - βœ… **REST Controllers** - CRUD endpoints (`POST`, `GET`, `GET list`)
290
-
291
- ### 4. Advanced Capabilities
292
- - βœ… **Relationships** - OneToMany, ManyToOne, OneToOne, ManyToMany
293
- - βœ… **Nested Entities** - Secondary entities with their own relationships
294
- - βœ… **Value Object Collections** - `List<ValueObject>` with `@ElementCollection`
295
- - βœ… **Auditing** - `@CreatedDate`, `@LastModifiedDate` when `auditable: true`
296
- - βœ… **Cascade Operations** - Configurable cascade types
297
- - βœ… **Fetch Strategies** - LAZY/EAGER configuration
298
- - βœ… **Validations** - Bean Validation annotations
299
- - βœ… **Pagination** - Built-in pagination support for list queries
300
-
301
- ## πŸ”„ Supported Relationships
302
-
303
- ### OneToMany / ManyToOne (Bidirectional)
153
+ ## 5. Field properties
304
154
 
305
155
  ```yaml
306
- # Parent entity
307
- entities:
308
- - name: order
309
- relationships:
310
- - type: OneToMany
311
- target: OrderItem
312
- mappedBy: order # Field in OrderItem that owns the relationship
313
- cascade: ALL
314
- fetch: LAZY
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
+ ```
315
168
 
316
- # Child entity
317
- - name: orderItem
318
- relationships:
319
- - type: ManyToOne
320
- target: Order
321
- fetch: LAZY
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
322
187
  ```
323
188
 
324
- ### OneToOne
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`.
325
194
 
326
195
  ```yaml
327
- entities:
328
- - name: user
329
- relationships:
330
- - type: OneToOne
331
- target: UserProfile
332
- cascade: ALL
196
+ fields:
197
+ - name: passwordHash
198
+ type: String
199
+ hidden: true # do not expose in API
200
+ ```
333
201
 
334
- - name: userProfile
335
- relationships:
336
- - type: OneToOne
337
- target: User
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)"
338
212
  ```
339
213
 
340
- ### ManyToMany
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.
341
217
 
342
218
  ```yaml
343
- entities:
344
- - name: student
345
- relationships:
346
- - type: ManyToMany
347
- target: Course
348
- cascade: PERSIST
219
+ fields:
220
+ - name: customerId
221
+ type: String
222
+ reference:
223
+ aggregate: Customer
224
+ module: customers
225
+ ```
349
226
 
350
- - name: course
351
- relationships:
352
- - type: ManyToMany
353
- target: Student
354
- mappedBy: courses
227
+ Generated in the domain entity:
228
+
229
+ ```java
230
+ /** @see customers.Customer */
231
+ private String customerId;
355
232
  ```
356
233
 
357
- ### Relations Between Secondary Entities
234
+ ---
358
235
 
359
- ```yaml
360
- entities:
361
- - name: evaluationDoctor
362
- relationships:
363
- - type: OneToMany
364
- target: EvaluationBranch # Another secondary entity
365
- cascade: ALL
236
+ ## 6. JSR-303 Validations
366
237
 
367
- - name: evaluationBranch
368
- relationships:
369
- - type: ManyToOne
370
- target: EvaluationDoctor
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
371
250
  ```
372
251
 
373
- ## 🎯 Supported Data Types
252
+ Auto-generates import: `import jakarta.validation.constraints.*;`
374
253
 
375
- ### Primitive Types
376
- - `String`, `Integer`, `Long`, `Double`, `Float`, `Boolean`
377
- - `BigDecimal`, `BigInteger`
378
- - `LocalDate`, `LocalDateTime`, `LocalTime`
379
- - `ZonedDateTime`, `Instant`
254
+ ### Supported parameters
380
255
 
381
- ### Collections
382
- - `List<ValueObject>` - Generates `@ElementCollection`
383
- - `List<Entity>` - Generates `@OneToMany`
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`) |
384
267
 
385
- ### Custom Types
386
- - Value Objects (defined in `valueObjects` section)
387
- - Enums (defined in `enums` section)
268
+ ### Examples by type
388
269
 
389
- ## πŸš€ Next Steps
270
+ ```yaml
271
+ # @NotBlank
272
+ - type: NotBlank
273
+ message: "Field is required"
390
274
 
391
- After generating entities:
275
+ # @NotNull
276
+ - type: NotNull
392
277
 
393
- 1. **Review generated code:**
394
- ```bash
395
- # Check domain models
396
- cat src/main/java/com/example/project/<module>/domain/models/*.java
397
- ```
278
+ # @Size
279
+ - type: Size
280
+ min: 2
281
+ max: 255
398
282
 
399
- 2. **Add business logic:**
400
- - Edit domain entities to add business methods
401
- - Implement domain validations
402
- - Add domain events if needed
283
+ # @Email
284
+ - type: Email
403
285
 
404
- 3. **Test the API:**
405
- ```bash
406
- ./gradlew bootRun
407
- # POST http://localhost:8080/api/<module>/<entity>
408
- # GET http://localhost:8080/api/<module>/<entity>/{id}
409
- # GET http://localhost:8080/api/<module>/<entity>
410
- ```
286
+ # @Min / @Max (for numeric fields)
287
+ - type: Min
288
+ value: 1
289
+ - type: Max
290
+ value: 999
411
291
 
412
- 4. **Extend functionality:**
413
- ```bash
414
- eva4j g usecase UpdateCustomer --type command
415
- eva4j g usecase DeleteCustomer --type command
416
- ```
292
+ # @Pattern
293
+ - type: Pattern
294
+ regexp: "^[A-Z]{2}[0-9]{6}$"
295
+ message: "Invalid format"
417
296
 
418
- ## ⚠️ Prerequisites
297
+ # @DecimalMin / @DecimalMax
298
+ - type: DecimalMin
299
+ min: "0.01"
300
+ inclusive: true
301
+ - type: DecimalMax
302
+ max: "9999.99"
419
303
 
420
- - Be in a project created with `eva4j create`
421
- - Module must exist (created with `eva4j add module`)
422
- - YAML file must exist at `examples/<aggregate-name>.yaml`
304
+ # @Digits
305
+ - type: Digits
306
+ integer: 6
307
+ fraction: 2
308
+ ```
423
309
 
424
- ## πŸ” Validations
310
+ ---
425
311
 
426
- The command validates:
427
- - βœ… Valid eva4j project
428
- - βœ… Target module exists
429
- - βœ… YAML file exists and is valid
430
- - βœ… No syntax errors in YAML
431
- - βœ… Entity names are unique
432
- - βœ… Relationship targets exist
433
- - βœ… Field types are valid
312
+ ## 7. Auditing
434
313
 
435
- ## πŸ“š See Also
314
+ ### Syntax
436
315
 
437
- - [DOMAIN_YAML_GUIDE.md](../../DOMAIN_YAML_GUIDE.md) - Complete YAML syntax reference
438
- - [add-module](./ADD_MODULE.md) - Create modules
439
- - [generate-usecase](./GENERATE_USECASE.md) - Add more use cases
316
+ ```yaml
317
+ # New (recommended)
318
+ audit:
319
+ enabled: true # adds createdAt, updatedAt
320
+ trackUser: true # also adds createdBy, updatedBy
440
321
 
441
- ## πŸ› Troubleshooting
322
+ # Legacy (equivalent to audit.enabled: true, trackUser: false)
323
+ auditable: true
324
+ ```
442
325
 
443
- **Error: "YAML file not found"**
444
- - Solution: Create `examples/<aggregate-name>.yaml` file first
326
+ ### Generated JPA inheritance
445
327
 
446
- **Error: "Module does not exist"**
447
- - Solution: Run `eva4j add module <module-name>` first
328
+ | Configuration | JPA base class |
329
+ |---------------|----------------|
330
+ | No auditing | no inheritance |
331
+ | `audit.enabled: true` | `extends AuditableEntity` |
332
+ | `audit.trackUser: true` | `extends FullAuditableEntity` |
448
333
 
449
- **Error: "Invalid relationship target"**
450
- - Solution: Ensure the target entity is defined in the same aggregate
334
+ ### Generated fields
451
335
 
452
- **Import errors after generation**
453
- - Solution: This has been fixed in recent versions. Make sure you're using eva4j 1.0.3+
454
- - If still happening, check that field types match defined ValueObjects/Enums
336
+ | Field | `audit.enabled` | `audit.trackUser` | In ResponseDto |
337
+ |-------|-----------------|-------------------|----------------|
338
+ | `createdAt` | βœ… | βœ… | βœ… |
339
+ | `updatedAt` | βœ… | βœ… | βœ… |
340
+ | `createdBy` | ❌ | βœ… | ❌ |
341
+ | `updatedBy` | ❌ | βœ… | ❌ |
455
342
 
456
- **Compilation errors with List<ValueObject>**
457
- - Solution: Updated in latest version to use `List<ValueObjectJpa>` in JPA entities
458
- - Mapper name: `OrderMapper.java`
459
- - File organization
460
- - Generated code references
343
+ > `createdBy` and `updatedBy` are administrative metadata: they are never exposed in response DTOs.
461
344
 
462
- ---
345
+ ### Infrastructure generated with `trackUser: true`
463
346
 
464
- ## Entities
347
+ When `trackUser` is enabled, eva4j automatically generates:
465
348
 
466
- ### Root Entity (Aggregate Root)
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 |
467
354
 
468
- The root entity is the entry point to the aggregate. All operations must go through it.
355
+ `Application.java` is configured with `@EnableJpaAuditing(auditorAwareRef = "auditorProvider")`.
469
356
 
470
- **⚠️ Important**: The root entity is defined within the `entities` array with `isRoot: true`.
357
+ ### Example
471
358
 
472
359
  ```yaml
473
- aggregates:
360
+ entities:
474
361
  - name: Order
475
- entities:
476
- - name: order # Entity name (camelCase or snake_case)
477
- isRoot: true # ← REQUIRED to mark the root
478
- tableName: orders # Table name in DB (optional)
479
-
480
- fields:
481
- - name: id
482
- type: String # String generates UUID, Long generates IDENTITY
483
-
484
- - name: orderNumber
485
- type: String
486
-
487
- - name: status
488
- type: OrderStatus # Reference to an enum
489
-
490
- - name: totalAmount
491
- type: Money # Reference to a value object
492
-
493
- - name: createdAt
494
- type: LocalDateTime
495
-
496
- relationships:
497
- - type: OneToMany
498
- target: OrderItem
499
- mappedBy: order
500
- cascade: [PERSIST, MERGE, REMOVE]
501
- fetch: LAZY
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
502
372
  ```
503
373
 
504
- ### Secondary Entities
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
505
392
 
506
- Entities that belong to the aggregate but are not the root. They are defined in the same `entities` array **without** `isRoot` (or with `isRoot: false`).
393
+ When you define `OneToMany` with `mappedBy`, eva4j automatically generates `@ManyToOne` in the target JPA entity. **Defining both sides is not required.**
507
394
 
508
395
  ```yaml
509
- aggregates:
396
+ # βœ… Only this is needed
397
+ entities:
510
398
  - name: Order
511
- entities:
512
- # ... root entity order with isRoot: true ...
513
-
514
- - name: orderItem # ← Secondary entity
515
- tableName: order_items
516
- # Without isRoot or isRoot: false = secondary
517
-
518
- fields:
519
- - name: id
520
- type: Long
521
-
522
- - name: productId
523
- type: String
524
-
525
- - name: quantity
526
- type: Integer
527
-
528
- - name: unitPrice
529
- type: Money
530
-
531
- relationships:
532
- - type: ManyToOne
533
- target: Order
534
- joinColumn: order_id
535
- fetch: LAZY
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;
536
411
  ```
537
412
 
538
- ### Fields
413
+ > If you define `ManyToOne` manually, that definition takes priority over auto-generation.
539
414
 
540
- #### Syntax
415
+ ### OneToMany
541
416
 
542
417
  ```yaml
543
- fields:
544
- - name: fieldName # Field name (camelCase) - REQUIRED
545
- type: String # Java data type - REQUIRED
418
+ relationships:
419
+ - type: OneToMany
420
+ target: OrderItem
421
+ mappedBy: order
422
+ cascade: [PERSIST, MERGE, REMOVE]
423
+ fetch: LAZY
546
424
  ```
547
425
 
548
- **Supported properties:**
549
- - `name`: Field name (required)
550
- - `type`: Java data type (required)
426
+ Generated in domain:
551
427
 
552
- #### Automatic Type Detection
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
+ ```
553
433
 
554
- eva4j automatically detects field types based **only** on `type`:
434
+ ### ManyToOne (manual, when you need a specific FK)
555
435
 
556
- **βœ… Value Objects** - Automatically detected
557
436
  ```yaml
558
- fields:
559
- - name: totalAmount
560
- type: Money # If Money is in valueObjects β†’ automatic @Embedded
437
+ relationships:
438
+ - type: ManyToOne
439
+ target: Order
440
+ joinColumn: fk_order_uuid
441
+ fetch: LAZY
561
442
  ```
562
443
 
563
- **βœ… Enums** - Automatically detected
564
- ```yaml
565
- fields:
566
- - name: status
567
- type: OrderStatus # If OrderStatus is in enums β†’ @Enumerated(STRING)
568
- ```
444
+ ### OneToOne
569
445
 
570
- **βœ… Primitive types**
571
446
  ```yaml
572
- fields:
573
- - name: name
574
- type: String # β†’ VARCHAR
575
- - name: age
576
- type: Integer # β†’ INTEGER
577
- - name: price
578
- type: BigDecimal # β†’ DECIMAL
579
- ```
447
+ # Inverse side (with mappedBy)
448
+ relationships:
449
+ - type: OneToOne
450
+ target: OrderSummary
451
+ mappedBy: order
452
+ cascade: [PERSIST, MERGE]
453
+ fetch: LAZY
580
454
 
581
- **βœ… Date types** - Automatically imported
582
- ```yaml
583
- fields:
584
- - name: createdAt
585
- type: LocalDateTime # β†’ timestamp + import java.time.LocalDateTime
455
+ # Owner side (with FK)
456
+ relationships:
457
+ - type: OneToOne
458
+ target: Order
459
+ joinColumn: order_id
460
+ fetch: LAZY
586
461
  ```
587
462
 
588
- **βœ… Collections** - Automatic @ElementCollection
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
+
589
474
  ```yaml
590
- fields:
591
- - name: tags
592
- type: List<String> # β†’ @ElementCollection with secondary table
593
- ```
475
+ # Child has no meaning without parent β†’ include REMOVE
476
+ cascade: [PERSIST, MERGE, REMOVE]
594
477
 
595
- #### ❌ NO need to specify
478
+ # Child has an independent lifecycle
479
+ cascade: [PERSIST, MERGE]
480
+ ```
596
481
 
597
- eva4j automatically generates the correct JPA annotations:
598
- - `@Embedded` for Value Objects
599
- - `@Enumerated(EnumType.STRING)` for Enums
600
- - `@ElementCollection` for lists
601
- - Required imports
482
+ ---
602
483
 
603
- #### ⚠️ MANDATORY RULE: `id` Field
484
+ ## 9. Value Objects
604
485
 
605
- **All entities MUST have a field named exactly `id`.**
486
+ Immutable objects that represent domain concepts without their own identity.
606
487
 
607
488
  ```yaml
608
- # βœ… CORRECT - All entities have 'id'
609
- entities:
610
- - name: order
611
- isRoot: true
612
- fields:
613
- - name: id # ← REQUIRED
614
- type: String # String = UUID, Long = IDENTITY
615
- - name: orderNumber
616
- type: String
617
-
618
- - name: orderItem
489
+ valueObjects:
490
+ - name: Money
619
491
  fields:
620
- - name: id # ← REQUIRED also in secondary entities
621
- type: Long
622
- - name: productId
492
+ - name: amount
493
+ type: BigDecimal
494
+ - name: currency
623
495
  type: String
624
496
  ```
625
497
 
626
- **Reasons:**
627
- - βœ… JPA requires `@Id` in all entities
628
- - βœ… Eva4j automatically generates `@Id` and `@GeneratedValue` for the `id` field
629
- - βœ… Clear and consistent convention across the domain
498
+ Generates:
630
499
 
631
- **Supported types for `id`:**
632
- - `String` β†’ Generates `@GeneratedValue(strategy = GenerationType.UUID)`
633
- - `Long` β†’ Generates `@GeneratedValue(strategy = GenerationType.IDENTITY)`
500
+ - `Money.java` – immutable domain class with constructor, getters, `equals()`, `hashCode()`
501
+ - `MoneyJpa.java` – `@Embeddable` with Lombok
634
502
 
635
- **❌ INCORRECT:**
636
- ```yaml
637
- # ❌ Without 'id' field - Application will fail
638
- fields:
639
- - name: orderNumber
640
- type: String
641
- # ← Missing 'id' field
503
+ Usage in a field:
642
504
 
643
- # ❌ Different name - Won't work
644
- fields:
645
- - name: orderId # ← Must be named exactly 'id'
646
- type: String
505
+ ```yaml
506
+ - name: totalAmount
507
+ type: Money # automatically detected as @Embedded
647
508
  ```
648
509
 
649
- **πŸ’‘ Business Identifiers:**
650
-
651
- If you need a business identifier in addition to the technical ID:
510
+ ### List of Value Objects
652
511
 
653
512
  ```yaml
654
- fields:
655
- - name: id # ← Technical ID (required)
656
- type: String
657
- - name: orderNumber # ← Business ID (optional)
658
- type: String
659
- - name: invoiceNumber # ← Another business identifier
660
- type: String
661
- ```
662
-
663
- ---
664
-
665
- #### Correct Examples
666
-
667
- ```yaml
668
- # Value Object
669
- fields:
670
- - name: totalAmount
671
- type: Money # βœ… Sufficient - eva4j automatically detects
672
-
673
- # Enum
674
- fields:
675
- - name: status
676
- type: OrderStatus # βœ… Sufficient - eva4j automatically detects
677
-
678
- # Primitive type
679
- fields:
680
- - name: description
681
- type: String # βœ… Basic type
682
-
683
- # Collection
684
- fields:
685
- - name: tags
686
- type: List<String> # βœ… Automatic @ElementCollection
687
- ```
688
-
689
- ---
690
-
691
- ### Automatic Auditing
692
-
693
- eva4j supports automatic entity auditing using the `auditable` property. When set to `true`, the entity will automatically include creation and modification date fields.
694
-
695
- #### Syntax
696
-
697
- ```yaml
698
- entities:
699
- - name: order
700
- isRoot: true
701
- auditable: true # ← Activates automatic auditing
702
- fields:
703
- - name: orderNumber
704
- type: String
705
- ```
706
-
707
- #### What `auditable: true` Generates
708
-
709
- **In the domain entity (`Order.java`):**
710
- ```java
711
- public class Order {
712
- private String orderNumber;
713
- private LocalDateTime createdAt; // ← Automatically added
714
- private LocalDateTime updatedAt; // ← Automatically added
715
-
716
- // getters/setters automatically generated
717
- }
718
- ```
719
-
720
- **In the JPA entity (`OrderJpa.java`):**
721
- ```java
722
- @Entity
723
- @Table(name = "orders")
724
- public class OrderJpa extends AuditableEntity { // ← Extends base class
725
- @Id
726
- @GeneratedValue(strategy = GenerationType.UUID)
727
- private String orderNumber;
728
-
729
- // createdAt/updatedAt fields inherited from AuditableEntity
730
- }
731
- ```
732
-
733
- **Generated base class (`AuditableEntity.java`):**
734
- ```java
735
- @MappedSuperclass
736
- @EntityListeners(AuditingEntityListener.class)
737
- public abstract class AuditableEntity {
738
-
739
- @CreatedDate
740
- @Column(name = "created_at", nullable = false, updatable = false)
741
- private LocalDateTime createdAt;
742
-
743
- @LastModifiedDate
744
- @Column(name = "updated_at", nullable = false)
745
- private LocalDateTime updatedAt;
746
-
747
- // getters/setters
748
- }
749
- ```
750
-
751
- #### Features
752
-
753
- βœ… **Fully automatic**: Timestamps update without additional code
754
- βœ… **Entity level**: Can be enabled for specific entities
755
- βœ… **Spring Data JPA**: Uses `@CreatedDate` and `@LastModifiedDate`
756
- βœ… **Mapper included**: Audit fields are automatically mapped between domain and JPA
757
-
758
- #### Required Configuration
759
-
760
- The Spring Boot application already has JPA auditing enabled in the main class:
761
-
762
- ```java
763
- @SpringBootApplication
764
- @EnableJpaAuditing // ← Already configured by eva4j
765
- public class Application {
766
- public static void main(String[] args) {
767
- SpringApplication.run(Application.class, args);
768
- }
769
- }
770
- ```
771
-
772
- #### Complete Example
773
-
774
- ```yaml
775
- aggregates:
776
- - name: Product
777
- entities:
778
- - name: product
779
- isRoot: true
780
- auditable: true # ← Enables auditing
781
- fields:
782
- - name: productId
783
- type: String
784
- - name: name
785
- type: String
786
- - name: price
787
- type: BigDecimal
788
- # createdAt and updatedAt are automatically added
789
-
790
- - name: review
791
- auditable: true # ← Secondary entities can also have auditing
792
- fields:
793
- - name: reviewId
794
- type: Long
795
- - name: comment
796
- type: String
797
- relationships:
798
- - type: ManyToOne
799
- target: product
800
- fetch: LAZY
801
- joinColumn: product_id
802
- ```
803
-
804
- **Resultado en la tabla:**
805
- ```sql
806
- CREATE TABLE products (
807
- product_id VARCHAR(36) PRIMARY KEY,
808
- name VARCHAR(255),
809
- price DECIMAL(19,2),
810
- created_at TIMESTAMP NOT NULL, -- ← AutomΓ‘tico
811
- updated_at TIMESTAMP NOT NULL -- ← AutomΓ‘tico
812
- );
813
-
814
- CREATE TABLE reviews (
815
- review_id BIGINT PRIMARY KEY AUTO_INCREMENT,
816
- comment TEXT,
817
- product_id VARCHAR(36),
818
- created_at TIMESTAMP NOT NULL, -- ← AutomΓ‘tico
819
- updated_at TIMESTAMP NOT NULL, -- ← AutomΓ‘tico
820
- FOREIGN KEY (product_id) REFERENCES products(product_id)
821
- );
822
- ```
823
-
824
- #### Notas importantes
825
-
826
- - βœ… `auditable` es **opcional** - por defecto es `false`
827
- - βœ… Puede usarse en **entidad raΓ­z** o **entidades secundarias**
828
- - βœ… Los campos `createdAt` y `updatedAt` **no deben** definirse manualmente en `fields`
829
- - βœ… El tipo es siempre `LocalDateTime`
830
- - ❌ **No incluye** auditoría de usuario (createdBy/updatedBy) - ver [FUTURE_FEATURES.md](FUTURE_FEATURES.md) para esa funcionalidad
831
-
832
- ---
833
-
834
- ## Value Objects
835
-
836
- Los Value Objects son objetos inmutables que representan conceptos del dominio sin identidad propia.
837
-
838
- ### DefiniciΓ³n bΓ‘sica
839
-
840
- ```yaml
841
- valueObjects:
842
- - name: Money
843
- fields:
844
- - name: amount
845
- type: BigDecimal
846
-
847
- - name: currency
848
- type: String
849
- ```
850
-
851
- ### Generated Value Object (Domain)
852
-
853
- ```java
854
- public class Money {
855
- private final BigDecimal amount;
856
- private final String currency;
857
-
858
- public Money(BigDecimal amount, String currency) {
859
- this.amount = amount;
860
- this.currency = currency;
861
- }
862
-
863
- // Getters
864
- public BigDecimal getAmount() { return amount; }
865
- public String getCurrency() { return currency; }
866
-
867
- // equals() and hashCode() based on all fields
868
- }
869
- ```
870
-
871
- ### Value Object JPA (@Embeddable)
872
-
873
- ```java
874
- @Embeddable
875
- public class MoneyJpa {
876
- private BigDecimal amount;
877
- private String currency;
878
-
879
- // Constructor, getters, setters (Lombok)
880
- }
881
- ```
882
-
883
- ### Usage in Entities
884
-
885
- ```yaml
886
- fields:
887
- - name: totalAmount
888
- type: Money # Automatically detected as VO
889
- ```
890
-
891
- Generates in JPA:
892
- ```java
893
- @Embedded
894
- private MoneyJpa totalAmount;
895
- ```
896
-
897
- ### Example: Complex Value Object
898
-
899
- ```yaml
900
- valueObjects:
901
- - name: Address
902
- fields:
903
- - name: street
904
- type: String
905
-
906
- - name: city
907
- type: String
908
-
909
- - name: state
910
- type: String
911
-
912
- - name: zipCode
913
- type: String
914
-
915
- - name: country
916
- type: String
917
- ```
918
-
919
- ---
920
-
921
- ## Enums
922
-
923
- ### Definition
924
-
925
- ```yaml
926
- enums:
927
- - name: OrderStatus
928
- values:
929
- - PENDING
930
- - CONFIRMED
931
- - SHIPPED
932
- - DELIVERED
933
- - CANCELLED
934
- ```
935
-
936
- ### Generated Enum
937
-
938
- ```java
939
- package com.example.myapp.order.domain.models.enums;
940
-
941
- public enum OrderStatus {
942
- PENDING,
943
- CONFIRMED,
944
- SHIPPED,
945
- DELIVERED,
946
- CANCELLED
947
- }
948
- ```
949
-
950
- ### Uso en entidades
951
-
952
- ```yaml
953
- fields:
954
- - name: status
955
- type: OrderStatus # Se detecta y se importa automΓ‘ticamente
956
- ```
957
-
958
- Genera en JPA:
959
- ```java
960
- @Enumerated(EnumType.STRING)
961
- private OrderStatus status;
962
- ```
963
-
964
- ### MΓΊltiples enums
965
-
966
- ```yaml
967
- enums:
968
- - name: OrderStatus
969
- values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
970
-
971
- - name: PaymentMethod
972
- values: [CREDIT_CARD, DEBIT_CARD, CASH, BANK_TRANSFER]
973
-
974
- - name: ShippingMethod
975
- values: [STANDARD, EXPRESS, OVERNIGHT]
976
- ```
977
-
978
- ---
979
-
980
- ## Relaciones
981
-
982
- eva4j soporta relaciones JPA bidireccionales completas con generaciΓ³n automΓ‘tica del lado inverso.
983
-
984
- ### 🎯 Relaciones Bidireccionales AutomÑticas
985
-
986
- **Característica clave**: Cuando defines una relación OneToMany con `mappedBy`, eva4j genera AUTOMÁTICAMENTE la relación inversa ManyToOne en la entidad target.
987
-
988
- **Solo necesitas definir UN lado:**
989
-
990
- ```yaml
991
- entities:
992
- - name: order
993
- isRoot: true
994
- relationships:
995
- - type: OneToMany
996
- target: OrderItem
997
- mappedBy: order # ← eva4j crea automΓ‘ticamente ManyToOne en OrderItem
998
- cascade: [PERSIST, MERGE]
999
- fetch: LAZY
1000
- ```
1001
-
1002
- **eva4j genera automΓ‘ticamente en OrderItem:**
1003
-
1004
- ```java
1005
- // OrderItemJpa.java (automatically generated)
1006
- @ManyToOne(fetch = FetchType.LAZY)
1007
- @JoinColumn(name = "order_id")
1008
- private OrderJpa order;
1009
- ```
1010
-
1011
- **Ventajas:**
1012
- - βœ… No necesitas definir ambos lados manualmente
1013
- - βœ… Evita inconsistencias entre relaciones
1014
- - βœ… JPA persiste correctamente la relaciΓ³n bidireccional
1015
- - βœ… Menos cΓ³digo YAML, misma funcionalidad
1016
-
1017
- **Nota**: Si defines manualmente ambos lados en el YAML, la definiciΓ³n manual tiene prioridad sobre la autogeneraciΓ³n.
1018
-
1019
- ---
1020
-
1021
- ### OneToMany (Uno a Muchos)
1022
-
1023
- **DefiniciΓ³n en la entidad que tiene la colecciΓ³n:**
1024
-
1025
- ```yaml
1026
- entities:
1027
- - name: order
1028
- isRoot: true
1029
- relationships:
1030
- - type: OneToMany
1031
- target: OrderItem # Entidad relacionada
1032
- mappedBy: order # Campo en OrderItem que apunta a Order
1033
- cascade: [PERSIST, MERGE, REMOVE]
1034
- fetch: LAZY
1035
- ```
1036
-
1037
- **Genera en dominio:**
1038
- ```java
1039
- private List<OrderItem> orderItems = new ArrayList<>();
1040
-
1041
- public void addOrderItem(OrderItem orderItem) {
1042
- this.orderItems.add(orderItem);
1043
- }
1044
-
1045
- public void removeOrderItem(OrderItem orderItem) {
1046
- this.orderItems.remove(orderItem);
1047
- }
1048
- ```
1049
-
1050
- **Genera en JPA:**
1051
- ```java
1052
- @OneToMany(mappedBy = "order", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}, fetch = FetchType.LAZY)
1053
- @Builder.Default
1054
- private List<OrderItemJpa> orderItems = new ArrayList<>();
1055
- ```
1056
-
1057
- **Genera automΓ‘ticamente en OrderItem (lado inverso):**
1058
- ```java
1059
- @ManyToOne(fetch = FetchType.LAZY)
1060
- @JoinColumn(name = "order_id") // Inferido desde mappedBy
1061
- private OrderJpa order;
1062
- ```
1063
-
1064
- ### ManyToOne (Muchos a Uno)
1065
-
1066
- **DefiniciΓ³n manual (opcional si ya usaste mappedBy en OneToMany):**
1067
-
1068
- ```yaml
1069
- entities:
1070
- - name: orderItem
1071
- # Sin isRoot = entidad secundaria
1072
- relationships:
1073
- - type: ManyToOne
1074
- target: Order
1075
- joinColumn: order_id # Columna FK en la tabla
1076
- fetch: LAZY
1077
- ```
1078
-
1079
- **Genera en JPA:**
1080
- ```java
1081
- @ManyToOne(fetch = FetchType.LAZY)
1082
- @JoinColumn(name = "order_id")
1083
- private OrderJpa order;
1084
- ```
1085
-
1086
- **πŸ’‘ Tip**: Si ya definiste `OneToMany` con `mappedBy` en Order, NO necesitas definir manualmente el `ManyToOne` en OrderItem. eva4j lo genera automΓ‘ticamente.
1087
-
1088
- ---
1089
-
1090
- ### ⚠️ REGLA CRÍTICA: Relaciones Bidireccionales
1091
-
1092
- **Para relaciones bidireccionales OneToMany/ManyToOne:**
1093
-
1094
- #### βœ… CORRECTO - Solo definir en la entidad raΓ­z
1095
-
1096
- ```yaml
1097
- entities:
1098
- - name: invoice
1099
- isRoot: true
1100
- relationships:
1101
- - type: OneToMany
1102
- target: InvoiceItem
1103
- mappedBy: invoice # ← Solo esta definiciΓ³n
1104
- cascade: [PERSIST, MERGE, REMOVE]
1105
- fetch: LAZY
1106
-
1107
- - name: invoiceItem
1108
- fields:
1109
- - name: id
1110
- type: Long
1111
- # ← SIN relationships definidas
1112
- # Eva4j genera automΓ‘ticamente el ManyToOne en InvoiceItemJpa
1113
- ```
1114
-
1115
- **Resultado generado:**
1116
- ```java
1117
- // InvoiceJpa.java
1118
- @OneToMany(mappedBy = "invoice", cascade = {...})
1119
- private List<InvoiceItemJpa> invoiceItems;
1120
-
1121
- // InvoiceItemJpa.java (automatically generated)
1122
- @ManyToOne(fetch = FetchType.LAZY)
1123
- @JoinColumn(name = "invoice_id")
1124
- private InvoiceJpa invoice;
1125
- ```
1126
-
1127
- #### ❌ INCORRECTO - Definir en ambos lados
1128
-
1129
- ```yaml
1130
- entities:
1131
- - name: invoice
1132
- isRoot: true
1133
- relationships:
1134
- - type: OneToMany
1135
- target: InvoiceItem
1136
- mappedBy: invoice # ← Primera definiciΓ³n
1137
-
1138
- - name: invoiceItem
1139
- relationships:
1140
- - type: ManyToOne # ← ❌ DUPLICADO - CausarΓ‘ error
1141
- target: Invoice
1142
- joinColumn: invoice_id
1143
- ```
1144
-
1145
- **Problema:** Genera DOS relaciones `@ManyToOne` en `InvoiceItemJpa`, ambas mapeando a `invoice_id`:
1146
-
1147
- ```java
1148
- // InvoiceItemJpa.java (INCORRECTO - Duplicado)
1149
- @ManyToOne
1150
- @JoinColumn(name = "invoice_id")
1151
- private InvoiceJpa invoice; // ← Del mappedBy
1152
-
1153
- @ManyToOne
1154
- @JoinColumn(name = "invoice_id")
1155
- private InvoiceJpa invoices; // ← Del ManyToOne explΓ­cito
1156
-
1157
- // Error de Hibernate:
1158
- // "Column 'invoice_id' is duplicated in mapping"
1159
- ```
1160
-
1161
- #### πŸ“‹ Regla de Oro
1162
-
1163
- | Escenario | Definir en RaΓ­z | Definir en Secundaria | Eva4j Genera |
1164
- |-----------|-----------------|----------------------|-------------|
1165
- | **Bidireccional** | `OneToMany` con `mappedBy` | ❌ NADA | `@OneToMany` en raíz + `@ManyToOne` en JPA de secundaria |
1166
- | **Unidireccional** | Opcional | `ManyToOne` con `joinColumn` | Solo lo definido |
1167
-
1168
- #### πŸ’‘ SeparaciΓ³n Dominio/Persistencia
1169
-
1170
- **Importante:** Eva4j sigue correctamente DDD:
1171
-
1172
- - **Capa de Dominio:** Las entidades secundarias NO tienen referencia a la raΓ­z
1173
- ```java
1174
- // InvoiceItem.java (dominio puro)
1175
- public class InvoiceItem {
1176
- private Long id;
1177
- private String description;
1178
- // ← SIN private Invoice invoice
1179
- }
1180
- ```
1181
-
1182
- - **Capa de Persistencia (JPA):** Solo aquΓ­ existe la relaciΓ³n
1183
- ```java
1184
- // InvoiceItemJpa.java (persistencia)
1185
- public class InvoiceItemJpa {
1186
- private Long id;
1187
-
1188
- @ManyToOne
1189
- @JoinColumn(name = "invoice_id")
1190
- private InvoiceJpa invoice; // ← Solo en capa JPA
1191
- }
1192
- ```
1193
-
1194
- **Ventajas:**
1195
- - βœ… Sin dependencias circulares en dominio
1196
- - βœ… Modelo de dominio mΓ‘s simple
1197
- - βœ… RelaciΓ³n bidireccional solo donde se necesita (persistencia)
1198
- - βœ… Cumple principios de DDD y arquitectura hexagonal
1199
-
1200
- ---
1201
-
1202
- ### OneToOne (Uno a Uno)
1203
-
1204
- **Bidireccional con mappedBy:**
1205
-
1206
- ```yaml
1207
- entities:
1208
- - name: order
1209
- isRoot: true
1210
- relationships:
1211
- - type: OneToOne
1212
- target: OrderSummary
1213
- mappedBy: order
1214
- cascade: [PERSIST, MERGE]
1215
- fetch: LAZY
1216
- ```
1217
-
1218
- **Sin mappedBy (owner):**
1219
-
1220
- ```yaml
1221
- entities:
1222
- - name: orderSummary
1223
- relationships:
1224
- - type: OneToOne
1225
- target: Order
1226
- joinColumn: order_id
1227
- fetch: LAZY
1228
- ```
1229
-
1230
- ### Relationship Options
1231
-
1232
- | Option | Values | Description |
1233
- |--------|--------|-------------|
1234
- | `type` | OneToMany, ManyToOne, OneToOne, ManyToMany | Relationship type |
1235
- | `target` | EntityName | Related entity |
1236
- | `mappedBy` | fieldName | For the inverse side of the relationship |
1237
- | `joinColumn` | column_name | FK column name |
1238
- | `cascade` | [PERSIST, MERGE, REMOVE, REFRESH, DETACH, ALL] | Cascade operations |
1239
- | `fetch` | LAZY, EAGER | Loading strategy |
1240
-
1241
- ---
1242
-
1243
- ### πŸ”₯ Cascade Options (Cascade Operations)
1244
-
1245
- The `cascade` options determine which operations on the parent are automatically propagated to related entities.
1246
-
1247
- #### **⚠️ IMPORTANT: Cascade and Persistence**
1248
-
1249
- If you DON'T define `cascade`, related entities will **NOT be persisted automatically**. This is the most common error:
1250
-
1251
- ```yaml
1252
- # ❌ BAD - OrderItems will NOT be saved in DB
1253
- relationships:
1254
- - type: OneToMany
1255
- target: OrderItem
1256
- mappedBy: order
1257
- cascade: [] # ← Empty array = no cascade
1258
- fetch: LAZY
1259
-
1260
- # βœ… GOOD - OrderItems are saved automatically with Order
1261
- relationships:
1262
- - type: OneToMany
1263
- target: OrderItem
1264
- mappedBy: order
1265
- cascade: [PERSIST, MERGE, REMOVE] # ← Required to persist
1266
- fetch: LAZY
1267
- ```
1268
-
1269
- #### **Cascade Options:**
1270
-
1271
- | Option | Description | When to use? |
1272
- |--------|-------------|--------------|
1273
- | `PERSIST` | When saving the parent, saves new children | βœ… **Always in OneToMany** to create items |
1274
- | `MERGE` | When updating the parent, updates children | βœ… **Always in OneToMany** to edit items |
1275
- | `REMOVE` | When deleting the parent, deletes children | βœ… If children don't make sense without the parent |
1276
- | `REFRESH` | When refreshing the parent, refreshes children | ⚠️ Rarely needed |
1277
- | `DETACH` | When detaching the parent, detaches children | ⚠️ Rarely needed |
1278
- | `ALL` | All of the above operations | ⚠️ Only if you're sure |
1279
-
1280
- #### **Recommended Configurations:**
1281
-
1282
- ```yaml
1283
- # 🎯 RECOMMENDED for OneToMany (Order β†’ OrderItem)
1284
- relationships:
1285
- - type: OneToMany
1286
- target: OrderItem
1287
- mappedBy: order
1288
- cascade: [PERSIST, MERGE, REMOVE] # ← Creates, updates and deletes items
1289
- fetch: LAZY
1290
-
1291
- # 🎯 RECOMMENDED for entities with independent lifecycle
1292
- relationships:
1293
- - type: OneToMany
1294
- target: OrderItem
1295
- mappedBy: order
1296
- cascade: [PERSIST, MERGE] # ← Without REMOVE, items persist
1297
- fetch: LAZY
1298
-
1299
- # ⚠️ CAREFUL with ALL - includes REMOVE
1300
- relationships:
1301
- - type: OneToMany
1302
- target: OrderItem
1303
- mappedBy: order
1304
- cascade: [ALL] # ← Deleting Order removes all OrderItems
1305
- fetch: LAZY
1306
-
1307
- # ❌ AVOID empty array if you want to persist children
1308
- relationships:
1309
- - type: OneToMany
1310
- target: OrderItem
1311
- mappedBy: order
1312
- cascade: [] # ← Requires manually saving OrderItem
1313
- fetch: LAZY
1314
- ```
1315
-
1316
- #### **What happens without Cascade?**
1317
-
1318
- ```yaml
1319
- # Without cascade: [PERSIST]
1320
- cascade: []
1321
-
1322
- # Behavior:
1323
- order.addOrderItem(item);
1324
- repository.save(order); // ❌ Order is saved, OrderItem is NOT
1325
- ```
1326
-
1327
- ```yaml
1328
- # With cascade: [PERSIST, MERGE]
1329
- cascade: [PERSIST, MERGE]
1330
-
1331
- # Behavior:
1332
- order.addOrderItem(item);
1333
- repository.save(order); // βœ… Order and OrderItem are saved automatically
1334
- ```
1335
-
1336
- ---
1337
-
1338
- ### πŸš€ Fetch Options (Loading Strategy)
1339
-
1340
- The `fetch` options determine WHEN related entities are loaded from the database.
1341
-
1342
- #### **Fetch Options:**
1343
-
1344
- | Option | Description | Behavior | When to use? |
1345
- |--------|-------------|----------|--------------|
1346
- | `LAZY` | Load on demand (when accessed) | Only fetches parent initially | βœ… **Recommended by default** |
1347
- | `EAGER` | Immediate load (always) | Fetches parent + children in same query | ⚠️ Only if you ALWAYS need children |
1348
-
1349
- #### **LAZY Example (Recommended):**
1350
-
1351
- ```yaml
1352
- relationships:
1353
- - type: OneToMany
1354
- target: OrderItem
1355
- mappedBy: order
1356
- cascade: [PERSIST, MERGE]
1357
- fetch: LAZY # ← Loads items only when accessed
1358
- ```
1359
-
1360
- **Generated SQL:**
1361
- ```sql
1362
- -- First query: Only fetches Order
1363
- SELECT * FROM orders WHERE id = ?
1364
-
1365
- -- Second query: Only if you access order.getOrderItems()
1366
- SELECT * FROM order_items WHERE order_id = ?
1367
- ```
1368
-
1369
- **βœ… Advantages:**
1370
- - Better initial performance
1371
- - Only loads what you need
1372
- - Avoids loading unnecessary data
1373
-
1374
- **⚠️ Disadvantage:**
1375
- - Can cause N+1 queries if you don't use `JOIN FETCH`
1376
-
1377
- #### **Ejemplo EAGER (Usar con cuidado):**
1378
-
1379
- ```yaml
1380
- relationships:
1381
- - type: OneToMany
1382
- target: OrderItem
1383
- mappedBy: order
1384
- cascade: [PERSIST, MERGE]
1385
- fetch: EAGER # ← Always loads items with Order
1386
- ```
1387
-
1388
- **Generated SQL:**
1389
- ```sql
1390
- -- Single query: Fetches Order + OrderItems
1391
- SELECT o.*, i.*
1392
- FROM orders o
1393
- LEFT JOIN order_items i ON i.order_id = o.id
1394
- WHERE o.id = ?
1395
- ```
1396
-
1397
- **βœ… Advantage:**
1398
- - Single SQL query
1399
- - Data available immediately
1400
-
1401
- **❌ Disadvantages:**
1402
- - Loads data even if unused
1403
- - Heavier queries
1404
- - Can cause performance issues
1405
-
1406
- #### **Recommended Configurations by Type:**
1407
-
1408
- ```yaml
1409
- # OneToMany: ALWAYS LAZY
1410
- relationships:
1411
- - type: OneToMany
1412
- target: OrderItem
1413
- mappedBy: order
1414
- cascade: [PERSIST, MERGE]
1415
- fetch: LAZY # ← Avoids loading all items always
1416
-
1417
- # ManyToOne: LAZY by default, EAGER only if always needed
1418
- relationships:
1419
- - type: ManyToOne
1420
- target: Customer
1421
- joinColumn: customer_id
1422
- fetch: LAZY # ← LAZY by default
1423
-
1424
- # OneToOne: LAZY if optional, EAGER if always exists
1425
- relationships:
1426
- - type: OneToOne
1427
- target: OrderSummary
1428
- mappedBy: order
1429
- cascade: [PERSIST, MERGE]
1430
- fetch: LAZY # ← LAZY if not always used
1431
- ```
1432
-
1433
- #### **N+1 Problem and how to solve it:**
1434
-
1435
- **Problem:**
1436
- ```java
1437
- // With LAZY fetch
1438
- List<Order> orders = orderRepository.findAll(); // 1 query
1439
- orders.forEach(order -> {
1440
- order.getOrderItems().forEach(item -> { // N queries (one per Order)
1441
- System.out.println(item.getProductName());
1442
- });
1443
- });
1444
- // Total: 1 + N queries = N+1 problem
1445
- ```
1446
-
1447
- **Solution - Use JOIN FETCH in queries:**
1448
- ```java
1449
- @Query("SELECT o FROM OrderJpa o LEFT JOIN FETCH o.orderItems WHERE o.id = :id")
1450
- OrderJpa findByIdWithItems(@Param("id") String id);
1451
- ```
1452
-
1453
- ---
1454
-
1455
- ### When to manually define inverse relationships?
1456
-
1457
- #### ❌ You DON'T need to define ManyToOne if:
1458
-
1459
- You already defined `OneToMany` with `mappedBy` on the "parent" side. eva4j automatically generates the inverse relationship.
1460
-
1461
- **Example - Only define OneToMany:**
1462
-
1463
- ```yaml
1464
- # βœ… SUFFICIENT: Only define this in Order
1465
- entities:
1466
- - name: order
1467
- isRoot: true
1468
- relationships:
1469
- - type: OneToMany
1470
- target: OrderItem
1471
- mappedBy: order # ← eva4j generates ManyToOne automatically
1472
- cascade: [PERSIST, MERGE, REMOVE]
1473
- fetch: LAZY
1474
-
1475
- # ❌ DON'T NEED this in OrderItem (generated automatically)
1476
- # - name: orderItem
1477
- # relationships:
1478
- # - type: ManyToOne
1479
- # target: Order
1480
- # joinColumn: order_id
1481
- # fetch: LAZY
1482
- ```
1483
-
1484
- **Result:** Complete bidirectional relationship with FK `order_id` generated automatically.
1485
-
1486
- **βœ… Advantages:**
1487
- - Less YAML code (only define one side)
1488
- - No duplication or inconsistencies
1489
- - Works the same as defining both sides
1490
- - FK inferred automatically: `{mappedBy}_id`
1491
-
1492
- ---
1493
-
1494
- #### βœ… You SHOULD define ManyToOne manually if:
1495
-
1496
- ##### 1. **You need a specific FK column name**
1497
-
1498
- ```yaml
1499
- # Define both sides to control FK name
1500
- entities:
1501
- - name: order
1502
- isRoot: true
1503
- relationships:
1504
- - type: OneToMany
1505
- target: OrderItem
1506
- mappedBy: order
1507
- cascade: [PERSIST, MERGE]
1508
- fetch: LAZY
1509
-
1510
- - name: orderItem
1511
- relationships:
1512
- - type: ManyToOne
1513
- target: Order
1514
- joinColumn: fk_pedido_uuid # ← Custom name
1515
- fetch: LAZY
1516
- ```
1517
-
1518
- **When to use:**
1519
- - Your DB has specific conventions (`fk_*`, prefixes, etc.)
1520
- - Need to maintain compatibility with existing schema
1521
- - Migration from another tool/framework
1522
-
1523
- ---
1524
-
1525
- ##### 2. **Multiple FKs to the same entity**
1526
-
1527
- ```yaml
1528
- # Transaction has 'from' and 'to' Account
1529
- entities:
1530
- - name: transaction
1531
- tableName: transactions
1532
-
1533
- fields:
1534
- - name: id
1535
- type: String
1536
- - name: amount
1537
- type: BigDecimal
1538
-
1539
- relationships:
1540
- # First relationship
1541
- - type: ManyToOne
1542
- target: Account
1543
- joinColumn: from_account_id # ← Explicit name required
1544
- fetch: LAZY
1545
-
1546
- # Second relationship to same entity
1547
- - type: ManyToOne
1548
- target: Account
1549
- joinColumn: to_account_id # ← Different FK name
1550
- fetch: LAZY
1551
- ```
1552
-
1553
- **When to use:**
1554
- - Self-relationships (category tree, org chart)
1555
- - Multiple relationships to same type (from/to, parent/child)
1556
- - Can't use `mappedBy` (which one would it be?)
1557
-
1558
- ---
1559
-
1560
- ##### 3. **Unidirectional relationship (no inverse side)**
1561
-
1562
- ```yaml
1563
- # OrderItem needs Product, but Product DOESN'T need OrderItems
1564
- entities:
1565
- - name: orderItem
1566
- relationships:
1567
- - type: ManyToOne
1568
- target: Product # Product has NO List<OrderItem>
1569
- joinColumn: product_id
1570
- fetch: LAZY
1571
-
1572
- # In Product DON'T define OneToMany
1573
- - name: product
1574
- isRoot: true
1575
- fields:
1576
- - name: id
1577
- type: String
1578
- - name: name
1579
- type: String
1580
- # No relationships to OrderItem
1581
- ```
1582
-
1583
- **When to use:**
1584
- - Performance: avoid loading unnecessary collections
1585
- - Product is not part of Order aggregate
1586
- - Only need navigation in one direction
1587
-
1588
- ---
1589
-
1590
- #### πŸ“Š Quick Comparison
1591
-
1592
- | Scenario | Define ManyToOne? | Why? |
1593
- |----------|-------------------|------|
1594
- | Standard relationship with `mappedBy` | ❌ No | eva4j generates it automatically |
1595
- | FK with custom name | βœ… Yes | To control `joinColumn` |
1596
- | Multiple FKs to same entity | βœ… Yes | Need explicit names |
1597
- | Unidirectional relationship | βœ… Yes | No inverse side (`mappedBy`) |
1598
- | Specific DB conventions | βœ… Yes | To comply with standards |
1599
- | Simple standard case | ❌ No | Let eva4j generate it |
1600
-
1601
- ---
1602
-
1603
- #### ⚠️ Error Común
1604
-
1605
- **NO hagas esto:**
1606
-
1607
- ```yaml
1608
- # ❌ INCORRECTO: Inconsistencia entre ambos lados
1609
- entities:
1610
- - name: order
1611
- isRoot: true
1612
- relationships:
1613
- - type: OneToMany
1614
- target: OrderItem
1615
- mappedBy: order # ← Espera campo "order" en OrderItem
1616
- fetch: LAZY
1617
-
1618
- - name: orderItem
1619
- relationships:
1620
- - type: ManyToOne
1621
- target: Order
1622
- joinColumn: pedido_id # ← Pero la FK se llama diferente
1623
- fetch: LAZY
513
+ - name: addresses
514
+ type: List<Address>
1624
515
  ```
1625
516
 
1626
- **Problema:** `mappedBy: order` busca un campo llamado `order`, pero `pedido_id` no coincide con la convenciΓ³n de nombres.
517
+ Generates:
1627
518
 
1628
- **βœ… Soluciones:**
1629
-
1630
- **OpciΓ³n A - Deja que eva4j genere automΓ‘ticamente:**
1631
- ```yaml
1632
- # Solo define OneToMany, eva4j genera ManyToOne correctamente
1633
- entities:
1634
- - name: order
1635
- isRoot: true
1636
- relationships:
1637
- - type: OneToMany
1638
- target: OrderItem
1639
- mappedBy: order
1640
- fetch: LAZY
519
+ ```java
520
+ @ElementCollection
521
+ @CollectionTable(name = "entity_addresses", joinColumns = @JoinColumn(name = "entity_id"))
522
+ @Builder.Default
523
+ private List<AddressJpa> addresses = new ArrayList<>();
1641
524
  ```
1642
525
 
1643
- **OpciΓ³n B - Define ambos lados consistentemente:**
526
+ ---
527
+
528
+ ## 10. Enums and state transitions
529
+
530
+ ### Simple enum
531
+
1644
532
  ```yaml
1645
- entities:
1646
- - name: order
1647
- isRoot: true
1648
- relationships:
1649
- - type: OneToMany
1650
- target: OrderItem
1651
- mappedBy: pedido # ← Coincide con el nombre del campo
1652
- fetch: LAZY
1653
-
1654
- - name: orderItem
1655
- relationships:
1656
- - type: ManyToOne
1657
- target: Order
1658
- joinColumn: pedido_id
1659
- fetch: LAZY
533
+ enums:
534
+ - name: OrderStatus
535
+ values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
1660
536
  ```
1661
537
 
1662
- ---
538
+ Generates `OrderStatus.java` with the enumerated values. In JPA: `@Enumerated(EnumType.STRING)`.
1663
539
 
1664
- #### πŸ’‘ RecomendaciΓ³n General
540
+ ### Enum with state transitions
1665
541
 
1666
- **Para el 90% de los casos:**
542
+ Transitions generate business methods in the entity, validation logic in the enum, and prevent invalid states.
1667
543
 
1668
544
  ```yaml
1669
- # βœ… MEJOR PRÁCTICA: Solo define OneToMany
1670
- entities:
1671
- - name: order
1672
- isRoot: true
1673
- relationships:
1674
- - type: OneToMany
1675
- target: OrderItem
1676
- mappedBy: order
1677
- cascade: [PERSIST, MERGE, REMOVE]
1678
- fetch: LAZY
1679
-
1680
- # NO definas ManyToOne en OrderItem
1681
- # eva4j lo genera automΓ‘ticamente con:
1682
- # - @JoinColumn(name = "order_id")
1683
- # - @ManyToOne(fetch = FetchType.LAZY)
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
1684
560
  ```
1685
561
 
1686
- **Solo define ambos lados cuando necesites control especΓ­fico.**
562
+ #### What is generated in the Enum
1687
563
 
1688
- ---
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));
1689
569
 
1690
- ## Tipos de Datos
570
+ public boolean canTransitionTo(OrderStatus next) {
571
+ return VALID_TRANSITIONS.getOrDefault(this, List.of()).contains(next);
572
+ }
1691
573
 
1692
- ### Tipos primitivos Java
574
+ public OrderStatus transitionTo(OrderStatus next) {
575
+ if (!canTransitionTo(next)) {
576
+ throw new InvalidStateTransitionException(this, next);
577
+ }
578
+ return next;
579
+ }
580
+ ```
1693
581
 
1694
- | YAML | Java | JPA | Observaciones |
1695
- |------|------|-----|---------------|
1696
- | `String` | String | VARCHAR | En ID genera UUID |
1697
- | `Integer` | Integer | INTEGER | En ID genera IDENTITY |
1698
- | `Long` | Long | BIGINT | En ID genera IDENTITY |
1699
- | `Double` | Double | DOUBLE | - |
1700
- | `Float` | Float | FLOAT | - |
1701
- | `Boolean` | Boolean | BOOLEAN | - |
1702
- | `BigDecimal` | BigDecimal | DECIMAL | Importa automΓ‘ticamente |
582
+ #### What is generated in the aggregate root
1703
583
 
1704
- ### Tipos de fecha/hora
584
+ One method per transition, plus `is*()` and `can*()` helpers:
1705
585
 
1706
- | YAML | Java | Importa automΓ‘ticamente |
1707
- |------|------|------------------------|
1708
- | `LocalDate` | LocalDate | java.time.LocalDate |
1709
- | `LocalDateTime` | LocalDateTime | java.time.LocalDateTime |
1710
- | `LocalTime` | LocalTime | java.time.LocalTime |
586
+ ```java
587
+ public void confirm() {
588
+ this.status = this.status.transitionTo(OrderStatus.CONFIRMED);
589
+ }
1711
590
 
1712
- ### Tipos especiales
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
+ }
1713
597
 
1714
- | YAML | Java | Uso |
1715
- |------|------|-----|
1716
- | `UUID` | UUID | IDs ΓΊnicos |
1717
- | Cualquier Enum | Enum personalizado | Estados, tipos |
1718
- | Cualquier VO | Value Object | Conceptos de dominio |
598
+ public boolean isPending() { return this.status == OrderStatus.PENDING; }
599
+ public boolean canConfirm() { return this.status.canTransitionTo(OrderStatus.CONFIRMED); }
600
+ ```
1719
601
 
1720
- ### Colecciones
602
+ ### `initialValue`
1721
603
 
1722
- #### Lista de primitivos
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`).
1723
605
 
1724
606
  ```yaml
1725
- fields:
1726
- - name: tags
1727
- type: List<String>
607
+ enums:
608
+ - name: OrderStatus
609
+ initialValue: PENDING
1728
610
  ```
1729
611
 
1730
- Genera:
1731
- ```java
1732
- @ElementCollection
1733
- @CollectionTable(name = "order_tags", joinColumns = @JoinColumn(name = "order_id"))
1734
- @Column(name = "tags")
1735
- @Builder.Default
1736
- private List<String> tags = new ArrayList<>();
1737
- ```
612
+ ### `guard`
1738
613
 
1739
- #### Lista de Value Objects
614
+ Java condition evaluated in the transition method. If the expression is `true`, a `BusinessException` is thrown.
1740
615
 
1741
616
  ```yaml
1742
- fields:
1743
- - name: addresses
1744
- type: List<Address> # Address es un VO definido
1745
- ```
1746
-
1747
- Genera:
1748
- ```java
1749
- @ElementCollection
1750
- @CollectionTable(name = "customer_addresses", joinColumns = @JoinColumn(name = "customer_id"))
1751
- @Builder.Default
1752
- private List<AddressJpa> addresses = new ArrayList<>();
617
+ - from: [PENDING, CONFIRMED]
618
+ to: CANCELLED
619
+ method: cancel
620
+ guard: "this.totalAmount.compareTo(BigDecimal.ZERO) == 0"
1753
621
  ```
1754
622
 
1755
623
  ---
1756
624
 
1757
- ## Ejemplos Completos
625
+ ## 11. Domain events
1758
626
 
1759
- ### Ejemplo 1: E-Commerce (Order)
627
+ Events are declared under the aggregate (at the same level as `entities:`, `enums:`, `valueObjects:`).
1760
628
 
1761
629
  ```yaml
1762
630
  aggregates:
1763
631
  - name: Order
1764
- entities:
1765
- - name: order
1766
- isRoot: true
1767
- tableName: orders
1768
-
632
+ events:
633
+ - name: OrderPlaced
1769
634
  fields:
1770
- - name: id
1771
- type: String
1772
-
1773
- - name: orderNumber
1774
- type: String
1775
-
1776
635
  - name: customerId
1777
636
  type: String
1778
-
1779
- - name: status
1780
- type: OrderStatus
1781
-
1782
637
  - name: totalAmount
1783
- type: Money
1784
-
1785
- - name: shippingAddress
1786
- type: Address
1787
-
1788
- - name: createdAt
1789
- type: LocalDateTime
1790
-
1791
- - name: updatedAt
1792
- type: LocalDateTime
1793
-
1794
- relationships:
1795
- - type: OneToMany
1796
- target: OrderItem
1797
- mappedBy: order
1798
- cascade: [PERSIST, MERGE, REMOVE]
1799
- fetch: LAZY
1800
-
1801
- - name: orderItem
1802
- tableName: order_items
1803
-
1804
- fields:
1805
- - name: id
1806
- type: Long
1807
-
1808
- - name: productId
1809
- type: String
1810
-
1811
- - name: productName
1812
- type: String
1813
-
1814
- - name: quantity
1815
- type: Integer
1816
-
1817
- - name: unitPrice
1818
- type: Money
1819
-
1820
- - name: subtotal
1821
- type: Money
1822
-
1823
- relationships:
1824
- - type: ManyToOne
1825
- target: Order
1826
- joinColumn: order_id
1827
- fetch: LAZY
1828
-
1829
- valueObjects:
1830
- - name: Money
1831
- fields:
1832
- - name: amount
1833
638
  type: BigDecimal
1834
- - name: currency
1835
- type: String
1836
-
1837
- - name: Address
639
+ - name: OrderCancelled
1838
640
  fields:
1839
- - name: street
1840
- type: String
1841
- - name: city
1842
- type: String
1843
- - name: state
1844
- type: String
1845
- - name: zipCode
641
+ - name: reason
1846
642
  type: String
1847
- - name: country
1848
- type: String
1849
-
1850
- enums:
1851
- - name: OrderStatus
1852
- values:
1853
- - PENDING
1854
- - CONFIRMED
1855
- - PROCESSING
1856
- - SHIPPED
1857
- - DELIVERED
1858
- - CANCELLED
1859
- - REFUNDED
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
+ }
1860
696
  ```
1861
697
 
1862
- ### Ejemplo 2: Blog (Post)
698
+ ---
699
+
700
+ ## 12. Multiple aggregates
701
+
702
+ A `domain.yaml` can contain multiple aggregates. Each one generates its own set of files.
1863
703
 
1864
704
  ```yaml
1865
705
  aggregates:
1866
- - name: Post
706
+ - name: Customer
1867
707
  entities:
1868
- - name: post
708
+ - name: Customer
1869
709
  isRoot: true
1870
- tableName: posts
1871
-
1872
710
  fields:
1873
711
  - name: id
1874
- type: Long
1875
-
1876
- - name: title
1877
- type: String
1878
-
1879
- - name: slug
1880
712
  type: String
1881
-
1882
- - name: content
1883
- type: String
1884
-
1885
- - name: authorId
713
+ - name: email
1886
714
  type: String
1887
-
1888
- - name: status
1889
- type: PostStatus
1890
-
1891
- - name: publishedAt
1892
- type: LocalDateTime
1893
-
1894
- - name: tags
1895
- type: List<String>
1896
-
1897
- - name: metadata
1898
- type: PostMetadata
1899
-
1900
- relationships:
1901
- - type: OneToMany
1902
- target: Comment
1903
- mappedBy: post
1904
- cascade: [PERSIST, MERGE, REMOVE]
1905
- fetch: LAZY
1906
-
1907
- - name: comment
1908
- tableName: comments
1909
-
715
+
716
+ - name: Product
717
+ entities:
718
+ - name: Product
719
+ isRoot: true
1910
720
  fields:
1911
721
  - name: id
1912
- type: Long
1913
-
1914
- - name: authorId
1915
722
  type: String
1916
-
1917
- - name: authorName
1918
- type: String
1919
-
1920
- - name: content
723
+ - name: name
1921
724
  type: String
1922
-
1923
- - name: createdAt
1924
- type: LocalDateTime
1925
-
1926
- - name: approved
1927
- type: Boolean
1928
-
1929
- relationships:
1930
- - type: ManyToOne
1931
- target: Post
1932
- joinColumn: post_id
1933
- fetch: LAZY
1934
-
1935
- valueObjects:
1936
- - name: PostMetadata
1937
- fields:
1938
- - name: viewCount
1939
- type: Integer
1940
- - name: likeCount
1941
- type: Integer
1942
- - name: shareCount
1943
- type: Integer
1944
-
1945
725
  enums:
1946
- - name: PostStatus
1947
- values: [DRAFT, PUBLISHED, ARCHIVED, DELETED]
726
+ - name: ProductCategory
727
+ values: [ELECTRONICS, CLOTHING, FOOD]
1948
728
  ```
1949
729
 
1950
- ### Ejemplo 3: Banking (Account)
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
1951
777
 
1952
778
  ```yaml
1953
779
  aggregates:
1954
- - name: Account
780
+ - name: Order
1955
781
  entities:
1956
- - name: account
782
+ - name: Order
1957
783
  isRoot: true
1958
- tableName: accounts
1959
-
784
+ tableName: orders
785
+ audit:
786
+ enabled: true
1960
787
  fields:
1961
788
  - name: id
1962
789
  type: String
1963
-
1964
- - name: accountNumber
1965
- type: String
1966
-
1967
790
  - name: customerId
1968
791
  type: String
1969
-
1970
- - name: accountType
1971
- type: AccountType
1972
-
1973
- - name: balance
1974
- type: Money
1975
-
792
+ reference:
793
+ aggregate: Customer
794
+ module: customers
1976
795
  - name: status
1977
- type: AccountStatus
1978
-
1979
- - name: openedAt
1980
- type: LocalDate
1981
-
796
+ type: OrderStatus
797
+ - name: totalAmount
798
+ type: BigDecimal
799
+ readOnly: true
1982
800
  relationships:
1983
801
  - type: OneToMany
1984
- target: Transaction
1985
- mappedBy: account
1986
- cascade: [PERSIST, MERGE]
802
+ target: OrderItem
803
+ mappedBy: order
804
+ cascade: [PERSIST, MERGE, REMOVE]
1987
805
  fetch: LAZY
1988
-
1989
- - name: transaction
1990
- tableName: transactions
1991
-
806
+
807
+ - name: OrderItem
808
+ tableName: order_items
1992
809
  fields:
1993
810
  - name: id
811
+ type: Long
812
+ - name: productId
1994
813
  type: String
1995
-
1996
- - name: transactionNumber
1997
- type: String
1998
-
1999
- - name: type
2000
- type: TransactionType
2001
-
2002
- - name: amount
2003
- type: Money
2004
-
2005
- - name: description
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
2006
842
  type: String
2007
-
2008
- - name: timestamp
2009
- type: LocalDateTime
2010
-
2011
- - name: balanceAfter
2012
- type: Money
2013
-
2014
- relationships:
2015
- - type: ManyToOne
2016
- target: Account
2017
- joinColumn: account_id
2018
- fetch: LAZY
2019
-
2020
- valueObjects:
2021
- - name: Money
843
+ - name: OrderCancelled
2022
844
  fields:
2023
- - name: amount
2024
- type: BigDecimal
2025
- - name: currency
845
+ - name: reason
2026
846
  type: String
2027
-
2028
- enums:
2029
- - name: AccountType
2030
- values: [CHECKING, SAVINGS, INVESTMENT, CREDIT]
2031
-
2032
- - name: AccountStatus
2033
- values: [ACTIVE, INACTIVE, SUSPENDED, CLOSED]
2034
-
2035
- - name: TransactionType
2036
- values: [DEPOSIT, WITHDRAWAL, TRANSFER, FEE, INTEREST]
2037
847
  ```
2038
848
 
2039
- ### Ejemplo 4: MΓΊltiples Agregados en un mΓ³dulo
849
+ ### Example 2: User with auditing and a sensitive field
2040
850
 
2041
851
  ```yaml
2042
852
  aggregates:
2043
- - name: Customer
853
+ - name: User
2044
854
  entities:
2045
- - name: customer
855
+ - name: User
2046
856
  isRoot: true
857
+ tableName: users
858
+ audit:
859
+ enabled: true
860
+ trackUser: true
2047
861
  fields:
2048
862
  - name: id
2049
863
  type: String
2050
- - name: name
2051
- type: String
2052
- - name: email
2053
- type: String
2054
- - name: phone
864
+ - name: username
2055
865
  type: String
2056
- - name: registeredAt
2057
- type: LocalDateTime
2058
-
2059
- valueObjects:
2060
- - name: ContactInfo
2061
- fields:
866
+ validations:
867
+ - type: NotBlank
868
+ - type: Size
869
+ min: 3
870
+ max: 50
2062
871
  - name: email
2063
872
  type: String
2064
- - name: phone
2065
- type: String
2066
-
2067
- - name: Product
2068
- entities:
2069
- - name: product
2070
- isRoot: true
2071
- fields:
2072
- - name: id
2073
- type: String
2074
- - name: name
2075
- type: String
2076
- - name: description
2077
- type: String
2078
- - name: price
2079
- type: Money
2080
- - name: stock
2081
- type: Integer
2082
- - name: category
2083
- type: ProductCategory
2084
-
2085
- valueObjects:
2086
- - name: Money
2087
- fields:
2088
- - name: amount
2089
- type: BigDecimal
2090
- - name: currency
2091
- type: String
2092
-
2093
- enums:
2094
- - name: ProductCategory
2095
- values: [ELECTRONICS, CLOTHING, FOOD, BOOKS, TOYS]
2096
- ```
2097
-
2098
- ---
2099
-
2100
- ## Comando de GeneraciΓ³n
2101
-
2102
- ```bash
2103
- # Generar todas las entidades del mΓ³dulo
2104
- eva4j generate entities <module-name>
2105
- ```
2106
-
2107
- ### Salida generada
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
2108
884
 
885
+ enums:
886
+ - name: UserRole
887
+ values: [ADMIN, USER, MODERATOR]
2109
888
  ```
2110
- βœ“ Found 1 aggregate(s) and 1 enum(s)
2111
-
2112
- πŸ“¦ Aggregates to generate:
2113
- β”œβ”€β”€ Order (Root: Order)
2114
- β”‚ β”œβ”€β”€ OrderItem
2115
- β”‚ └── Money (VO)
2116
-
2117
- β ‹ Generating files...
2118
-
2119
- βœ… Successfully generated 13 files for module 'order'
2120
-
2121
- πŸ“ Generated Files:
2122
- βœ“ Enum: OrderStatus
2123
- βœ“ Domain Entity: Order
2124
- βœ“ JPA Entity: OrderJpa
2125
- βœ“ Domain Entity: OrderItem
2126
- βœ“ JPA Entity: OrderItemJpa
2127
- βœ“ Domain VO: Money
2128
- βœ“ JPA VO: MoneyJpa
2129
- βœ“ Mapper: OrderMapper
2130
- βœ“ Repository: OrderRepository
2131
- βœ“ JPA Repository: OrderJpaRepository
2132
- βœ“ Repository Impl: OrderRepositoryImpl
2133
- ```
2134
-
2135
- ---
2136
-
2137
- ## Tips y Mejores PrΓ‘cticas
2138
-
2139
- ### βœ… Hacer
2140
-
2141
- 1. **Usa nombres descriptivos**: `orderNumber` en lugar de `number`
2142
- 2. **PascalCase para tipos**: `OrderStatus`, `Money`, `Address`
2143
- 3. **camelCase para campos**: `totalAmount`, `createdAt`
2144
- 4. **snake_case para tablas**: `order_items`, `customer_addresses`
2145
- 5. **Define IDs apropiados**: String para UUIDs, Long para secuencias
2146
- 6. **Usa Value Objects**: Para conceptos cohesivos (Money, Address)
2147
- 7. **Cascade apropiado**: PERSIST, MERGE para agregados; evita ALL
2148
-
2149
- ### ❌ Evitar
2150
-
2151
- 1. **Don't use Long for UUIDs**: Use String
2152
- 2. **Don't create bidirectional relationships without mappedBy**: Define the owner
2153
- 3. **Don't use EAGER without reason**: LAZY is better for performance
2154
- 4. **Don't mix concepts**: One aggregate = one transaction
2155
- 5. **Don't use @Column in domain.yaml**: It's for JPA, generated automatically
2156
-
2157
- ---
2158
-
2159
- ## Current Support and Limitations
2160
-
2161
- ### βœ… Supported
2162
-
2163
- - Aggregates with root and secondary entities
2164
- - Embedded Value Objects
2165
- - Enums with values
2166
- - OneToMany, ManyToOne, OneToOne relationships
2167
- - Java primitive and date types
2168
- - Collections of primitives and VOs
2169
- - IDs: String (UUID), Long/Integer (IDENTITY)
2170
- - Custom Cascade and Fetch
2171
-
2172
- ### 🚧 Coming Soon
2173
-
2174
- - JSR-303 validations
2175
- - Automatic auditing
2176
- - Soft delete
2177
- - Custom query methods
2178
- - Indexes and constraints
2179
- - Entity inheritance
2180
-
2181
- ---
2182
-
2183
- ## Frequently Asked Questions
2184
-
2185
- **Q: Can I have multiple aggregates in one domain.yaml?**
2186
- A: Yes, define multiple entries in the `aggregates` array.
2187
-
2188
- **Q: How do I reference an enum from another aggregate?**
2189
- A: Enums are global to the module, just use the name: `type: OrderStatus`
2190
-
2191
- **Q: Can I use a VO in multiple aggregates?**
2192
- A: Yes, but you must define it in each aggregate (for now).
2193
-
2194
- **Q: What happens if I regenerate the code?**
2195
- A: Files are overwritten. Modify only in templates, not in generated code.
2196
-
2197
- **Q: Can I customize generated entities?**
2198
- A: Yes, modify the templates in `templates/aggregate/`.
2199
889
 
2200
890
  ---
2201
891
 
2202
- ## Additional Resources
892
+ ## 15. Prerequisites and common errors
2203
893
 
2204
- - [Implementation Guide](IMPLEMENTATION_SUMMARY.md)
2205
- - [Testing Guide](TESTING_GUIDE.md)
2206
- - [Quick Reference](QUICK_REFERENCE.md)
2207
- - [DDD Documentation](https://martinfowler.com/bliki/DomainDrivenDesign.html)
894
+ ### Prerequisites
2208
895
 
2209
- ---
896
+ - Project created with `eva create`
897
+ - Existing module (`eva add module <module>`)
898
+ - `domain.yaml` file at `src/main/java/<package>/<module>/`
2210
899
 
2211
- **Ready to start?** Create your `domain.yaml` and run:
900
+ ### Common errors
2212
901
 
2213
- ```bash
2214
- eva4j generate entities <your-module>
2215
- ```
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 |