eva4j 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +314 -10
- package/COMMAND_EVALUATION.md +15 -16
- package/DOMAIN_YAML_GUIDE.md +576 -10
- package/FUTURE_FEATURES.md +1627 -1168
- package/README.md +318 -13
- package/bin/eva4j.js +34 -0
- package/config/defaults.json +1 -0
- package/design-system.md +797 -0
- package/docs/commands/EVALUATE_SYSTEM.md +994 -0
- package/docs/commands/GENERATE_ENTITIES.md +795 -6
- package/docs/commands/INDEX.md +10 -1
- package/examples/domain-endpoints-relations.yaml +353 -0
- package/examples/domain-endpoints-versioned.yaml +144 -0
- package/examples/domain-endpoints.yaml +135 -0
- package/examples/domain-events.yaml +166 -20
- package/examples/domain-listeners.yaml +212 -0
- package/examples/domain-one-to-many.yaml +1 -0
- package/examples/domain-one-to-one.yaml +1 -0
- package/examples/domain-ports.yaml +414 -0
- package/examples/domain-soft-delete.yaml +47 -44
- package/examples/system/notification.yaml +147 -0
- package/examples/system/product.yaml +185 -0
- package/examples/system/system.yaml +112 -0
- package/examples/system-report.html +971 -0
- package/examples/system.yaml +332 -0
- package/package.json +2 -1
- package/src/commands/build.js +714 -0
- package/src/commands/create.js +7 -3
- package/src/commands/detach.js +1 -0
- package/src/commands/evaluate-system.js +610 -0
- package/src/commands/generate-entities.js +1331 -49
- package/src/commands/generate-http-exchange.js +2 -0
- package/src/commands/generate-kafka-event.js +98 -11
- package/src/generators/base-generator.js +8 -1
- package/src/generators/postman-generator.js +188 -0
- package/src/generators/shared-generator.js +10 -0
- package/src/utils/config-manager.js +54 -0
- package/src/utils/context-builder.js +1 -0
- package/src/utils/domain-diagram.js +192 -0
- package/src/utils/domain-validator.js +970 -0
- package/src/utils/fake-data.js +376 -0
- package/src/utils/naming.js +3 -2
- package/src/utils/system-validator.js +434 -0
- package/src/utils/yaml-to-entity.js +302 -8
- package/templates/aggregate/AggregateMapper.java.ejs +3 -2
- package/templates/aggregate/AggregateRepository.java.ejs +8 -2
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +13 -3
- package/templates/aggregate/AggregateRoot.java.ejs +60 -2
- package/templates/aggregate/DomainEventHandler.java.ejs +27 -20
- package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
- package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
- package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
- package/templates/aggregate/JpaRepository.java.ejs +5 -0
- package/templates/base/gradle/build.gradle.ejs +3 -2
- package/templates/base/root/AGENTS.md.ejs +306 -45
- package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1663 -0
- package/templates/base/root/skill-build-system-yaml.ejs +1446 -0
- package/templates/base/root/system.yaml.ejs +97 -0
- package/templates/crud/ApplicationMapper.java.ejs +4 -0
- package/templates/crud/Controller.java.ejs +4 -4
- package/templates/crud/CreateCommand.java.ejs +4 -0
- package/templates/crud/CreateItemDto.java.ejs +4 -0
- package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
- package/templates/crud/DeleteCommandHandler.java.ejs +10 -2
- package/templates/crud/EndpointsController.java.ejs +178 -0
- package/templates/crud/FindByQuery.java.ejs +17 -0
- package/templates/crud/FindByQueryHandler.java.ejs +57 -0
- package/templates/crud/ListQuery.java.ejs +1 -1
- package/templates/crud/ListQueryHandler.java.ejs +8 -8
- package/templates/crud/ScaffoldCommand.java.ejs +12 -0
- package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
- package/templates/crud/ScaffoldQuery.java.ejs +13 -0
- package/templates/crud/ScaffoldQueryHandler.java.ejs +41 -0
- package/templates/crud/SubEntityAddCommand.java.ejs +21 -0
- package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
- package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
- package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
- package/templates/crud/TransitionCommand.java.ejs +9 -0
- package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
- package/templates/crud/UpdateCommand.java.ejs +4 -0
- package/templates/evaluate/report.html.ejs +1363 -0
- package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
- package/templates/kafka-event/Event.java.ejs +16 -0
- package/templates/kafka-listener/KafkaController.java.ejs +1 -1
- package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
- package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
- package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
- package/templates/kafka-listener/ListenerCommandHandler.java.ejs +23 -0
- package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
- package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
- package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
- package/templates/mock/MockEvent.java.ejs +10 -0
- package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
- package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
- package/templates/mock/SpringEventListener.java.ejs +61 -0
- package/templates/ports/PortDomainModel.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +67 -0
- package/templates/ports/PortFeignClient.java.ejs +45 -0
- package/templates/ports/PortFeignConfig.java.ejs +24 -0
- package/templates/ports/PortInterface.java.ejs +45 -0
- package/templates/ports/PortNestedType.java.ejs +28 -0
- package/templates/ports/PortRequestDto.java.ejs +30 -0
- package/templates/ports/PortResponseDto.java.ejs +28 -0
- package/templates/postman/Collection.json.ejs +1 -1
- package/templates/postman/UnifiedCollection.json.ejs +185 -0
- package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
package/FUTURE_FEATURES.md
CHANGED
|
@@ -1,1168 +1,1627 @@
|
|
|
1
|
-
# Características Futuras - eva4j
|
|
2
|
-
|
|
3
|
-
Este documento describe las mejoras planificadas para futuras versiones de eva4j, organizadas por prioridad. Cada sección incluye el contexto DDD correspondiente, la sintaxis YAML propuesta y ejemplos del código que se generaría.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## � Tabla de Contenidos
|
|
8
|
-
|
|
9
|
-
### � Alta Prioridad
|
|
10
|
-
- [Domain Events](#1-domain-events)
|
|
11
|
-
- [Aggregate Boundaries por ID](#2-aggregate-boundaries-por-id)
|
|
12
|
-
- [Soft Delete Completo](#3-soft-delete-completo)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- [
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
public
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
public
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
public void
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
```java
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
int
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
```java
|
|
443
|
-
|
|
444
|
-
public
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
###
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
###
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
.
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
```
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
```
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
###
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
```
|
|
1087
|
-
|
|
1088
|
-
###
|
|
1089
|
-
|
|
1090
|
-
```
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
@
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1
|
+
# Características Futuras - eva4j
|
|
2
|
+
|
|
3
|
+
Este documento describe las mejoras planificadas para futuras versiones de eva4j, organizadas por prioridad. Cada sección incluye el contexto DDD correspondiente, la sintaxis YAML propuesta y ejemplos del código que se generaría.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## � Tabla de Contenidos
|
|
8
|
+
|
|
9
|
+
### � Alta Prioridad
|
|
10
|
+
- [Domain Events](#1-domain-events) ✅
|
|
11
|
+
- [Aggregate Boundaries por ID](#2-aggregate-boundaries-por-id) ✅
|
|
12
|
+
- [Soft Delete Completo](#3-soft-delete-completo) ✅
|
|
13
|
+
- [Transactional Outbox Pattern](#15-transactional-outbox-pattern)
|
|
14
|
+
|
|
15
|
+
### � Media Prioridad
|
|
16
|
+
- [Paginación en Queries](#4-paginación-en-queries) ✅
|
|
17
|
+
- [Optimistic Locking](#5-optimistic-locking)
|
|
18
|
+
- [Read Models Separados](#6-read-models-separados-proyecciones)
|
|
19
|
+
- [Enums con Comportamiento y Transiciones](#7-enums-con-comportamiento-y-transiciones) ✅
|
|
20
|
+
- [Políticas y Especificaciones](#8-políticas-y-especificaciones)
|
|
21
|
+
|
|
22
|
+
### � Tooling y Calidad
|
|
23
|
+
- [Validación de domain.yaml con JSON Schema](#9-validación-de-domainyaml-con-json-schema)
|
|
24
|
+
- [Generación Incremental / Diff](#10-generación-incremental--diff) ✅
|
|
25
|
+
- [Comando eva4j doctor](#11-comando-eva4j-doctor)
|
|
26
|
+
- [Tests Generados Completos](#12-tests-generados-completos)
|
|
27
|
+
- [Diagrama Mermaid desde domain.yaml](#13-diagrama-mermaid-desde-domainyaml-eva-g-diagram)
|
|
28
|
+
|
|
29
|
+
### 🚀 Prototyping
|
|
30
|
+
- [Mock Mode — `eva build --mock`](#17-mock-mode--eva-build---mock) ✅
|
|
31
|
+
|
|
32
|
+
### ✅ Implementado
|
|
33
|
+
- [Domain Events](#1-domain-events)
|
|
34
|
+
- [Aggregate Boundaries por ID](#2-aggregate-boundaries-por-id)
|
|
35
|
+
- [Soft Delete Completo](#3-soft-delete-completo)
|
|
36
|
+
- [Paginación en Queries](#4-paginación-en-queries)
|
|
37
|
+
- [Enums con Comportamiento y Transiciones](#7-enums-con-comportamiento-y-transiciones)
|
|
38
|
+
- [Generación Incremental / Diff](#10-generación-incremental--diff)
|
|
39
|
+
- [Auditoría de Tiempo y Usuario](#15-auditoría-implementada)
|
|
40
|
+
- [Validaciones JSR-303](#14-validaciones-jsr-303-implementado)
|
|
41
|
+
- [`defaultValue` para campos `readOnly`](#16-defaultvalue-para-campos-readonly-implementado)
|
|
42
|
+
- [Mock Mode (`eva build --mock`)](#17-mock-mode--eva-build---mock)
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## � ALTA PRIORIDAD
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 1. Domain Events ✅
|
|
51
|
+
|
|
52
|
+
### Descripción
|
|
53
|
+
|
|
54
|
+
Los **Domain Events** son el patrón más fundamental de DDD que actualmente falta en eva4j. Un evento de dominio representa algo significativo que ocurrió en el negocio — un hecho pasado, no una intención futura. Son esenciales para:
|
|
55
|
+
|
|
56
|
+
- Comunicar cambios entre agregados sin acoplamiento directo
|
|
57
|
+
- Disparar side effects (emails, notificaciones, actualizaciones de proyecciones)
|
|
58
|
+
- Construir sistemas eventualmente consistentes
|
|
59
|
+
|
|
60
|
+
Sin eventos de dominio, la comunicación entre agregados obliga a dependencias directas que violan los límites de los bounded contexts.
|
|
61
|
+
|
|
62
|
+
### Sintaxis Propuesta en domain.yaml
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
aggregates:
|
|
66
|
+
- name: Order
|
|
67
|
+
entities:
|
|
68
|
+
- name: order
|
|
69
|
+
isRoot: true
|
|
70
|
+
events:
|
|
71
|
+
- name: OrderPlaced
|
|
72
|
+
fields:
|
|
73
|
+
- name: orderId
|
|
74
|
+
type: String
|
|
75
|
+
- name: customerId
|
|
76
|
+
type: String
|
|
77
|
+
- name: totalAmount
|
|
78
|
+
type: BigDecimal
|
|
79
|
+
- name: OrderCancelled
|
|
80
|
+
fields:
|
|
81
|
+
- name: orderId
|
|
82
|
+
type: String
|
|
83
|
+
- name: reason
|
|
84
|
+
type: String
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Código Generado
|
|
88
|
+
|
|
89
|
+
#### Clase base DomainEvent
|
|
90
|
+
|
|
91
|
+
```java
|
|
92
|
+
// shared/domain/DomainEvent.java
|
|
93
|
+
public abstract class DomainEvent {
|
|
94
|
+
private final String eventId;
|
|
95
|
+
private final LocalDateTime occurredOn;
|
|
96
|
+
private final String aggregateId;
|
|
97
|
+
|
|
98
|
+
protected DomainEvent(String aggregateId) {
|
|
99
|
+
this.eventId = UUID.randomUUID().toString();
|
|
100
|
+
this.occurredOn = LocalDateTime.now();
|
|
101
|
+
this.aggregateId = aggregateId;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public String getEventId() { return eventId; }
|
|
105
|
+
public LocalDateTime getOccurredOn() { return occurredOn; }
|
|
106
|
+
public String getAggregateId() { return aggregateId; }
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Evento específico generado
|
|
111
|
+
|
|
112
|
+
```java
|
|
113
|
+
// domain/models/events/OrderPlacedEvent.java
|
|
114
|
+
public class OrderPlacedEvent extends DomainEvent {
|
|
115
|
+
private final String customerId;
|
|
116
|
+
private final BigDecimal totalAmount;
|
|
117
|
+
|
|
118
|
+
public OrderPlacedEvent(String orderId, String customerId, BigDecimal totalAmount) {
|
|
119
|
+
super(orderId);
|
|
120
|
+
this.customerId = customerId;
|
|
121
|
+
this.totalAmount = totalAmount;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public String getCustomerId() { return customerId; }
|
|
125
|
+
public BigDecimal getTotalAmount() { return totalAmount; }
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Raíz del agregado con eventos
|
|
130
|
+
|
|
131
|
+
```java
|
|
132
|
+
// domain/models/entities/Order.java
|
|
133
|
+
public class Order {
|
|
134
|
+
private List<DomainEvent> domainEvents = new ArrayList<>();
|
|
135
|
+
|
|
136
|
+
public List<DomainEvent> getDomainEvents() {
|
|
137
|
+
return Collections.unmodifiableList(domainEvents);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public void clearDomainEvents() {
|
|
141
|
+
domainEvents.clear();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public void place(String customerId, BigDecimal total) {
|
|
145
|
+
this.status = OrderStatus.PLACED;
|
|
146
|
+
domainEvents.add(new OrderPlacedEvent(this.id, customerId, total));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public void cancel(String reason) {
|
|
150
|
+
if (this.status == OrderStatus.DELIVERED) {
|
|
151
|
+
throw new IllegalStateException("Cannot cancel a delivered order");
|
|
152
|
+
}
|
|
153
|
+
this.status = OrderStatus.CANCELLED;
|
|
154
|
+
domainEvents.add(new OrderCancelledEvent(this.id, reason));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Publicación automática desde el repositorio
|
|
160
|
+
|
|
161
|
+
```java
|
|
162
|
+
@Override
|
|
163
|
+
public Order save(Order order) {
|
|
164
|
+
OrderJpa jpa = mapper.toJpa(order);
|
|
165
|
+
repository.save(jpa);
|
|
166
|
+
order.getDomainEvents().forEach(eventPublisher::publishEvent);
|
|
167
|
+
order.clearDomainEvents();
|
|
168
|
+
return mapper.toDomain(jpa);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### Listener en otro módulo (sin acoplamiento)
|
|
173
|
+
|
|
174
|
+
```java
|
|
175
|
+
@Component
|
|
176
|
+
public class OrderEventListener {
|
|
177
|
+
@EventListener
|
|
178
|
+
public void onOrderPlaced(OrderPlacedEvent event) {
|
|
179
|
+
// enviar email de confirmación, actualizar inventario, etc.
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@TransactionalEventListener(phase = AFTER_COMMIT)
|
|
183
|
+
public void onOrderCancelled(OrderCancelledEvent event) {
|
|
184
|
+
// proceso de reembolso, notificación al cliente
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 2. Aggregate Boundaries por ID ✅
|
|
192
|
+
|
|
193
|
+
### Descripción
|
|
194
|
+
|
|
195
|
+
Eva4j ya genera correctamente el patrón DDD de referencia por ID: los campos que apuntan a otro agregado se generan como tipos primitivos (`String`, `Long`, etc.) sin ningún `@ManyToOne` cruzado. Esta feature añade **declaración semántica explícita** mediante la propiedad `reference:` en el campo, que permite documentar la intención en el YAML y generar un comentario Javadoc en el código.
|
|
196
|
+
|
|
197
|
+
Sin `reference:`, un campo `customerId: String` es indistinguible de cualquier otro `String`. Con `reference:`, el generador sabe que es un puntero intencional al agregado `Customer` del módulo `customers`.
|
|
198
|
+
|
|
199
|
+
### Sintaxis
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
aggregates:
|
|
203
|
+
- name: Order
|
|
204
|
+
entities:
|
|
205
|
+
- name: order
|
|
206
|
+
isRoot: true
|
|
207
|
+
fields:
|
|
208
|
+
- name: id
|
|
209
|
+
type: String
|
|
210
|
+
- name: customerId
|
|
211
|
+
type: String
|
|
212
|
+
reference:
|
|
213
|
+
aggregate: Customer # Nombre del agregado (PascalCase) — obligatorio
|
|
214
|
+
module: customers # Módulo donde vive el agregado — opcional
|
|
215
|
+
- name: productId
|
|
216
|
+
type: String
|
|
217
|
+
reference:
|
|
218
|
+
aggregate: Product
|
|
219
|
+
module: catalog
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Comportamiento
|
|
223
|
+
|
|
224
|
+
- El tipo Java **no cambia** — sigue siendo `String`, `Long`, etc.
|
|
225
|
+
- JPA genera `@Column` normal — **sin** `@ManyToOne` ni `@JoinColumn`.
|
|
226
|
+
- Se genera un **comentario Javadoc** en la entidad de dominio y en la entidad JPA.
|
|
227
|
+
- `module:` es opcional: se puede omitir si el agregado referenciado está en el mismo módulo.
|
|
228
|
+
- Si `reference:` está malformado (falta `aggregate`), eva4j lanza un error descriptivo.
|
|
229
|
+
|
|
230
|
+
### Código Generado
|
|
231
|
+
|
|
232
|
+
```java
|
|
233
|
+
// domain/models/entities/Order.java
|
|
234
|
+
/** Cross-aggregate reference → Customer (module: customers) */
|
|
235
|
+
private String customerId;
|
|
236
|
+
|
|
237
|
+
/** Cross-aggregate reference → Product (module: catalog) */
|
|
238
|
+
private String productId;
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
```java
|
|
242
|
+
// infrastructure/database/entities/OrderJpa.java
|
|
243
|
+
@Column(name = "customer_id")
|
|
244
|
+
/** Cross-aggregate reference → Customer (module: customers) */
|
|
245
|
+
private String customerId;
|
|
246
|
+
|
|
247
|
+
@Column(name = "product_id")
|
|
248
|
+
/** Cross-aggregate reference → Product (module: catalog) */
|
|
249
|
+
private String productId;
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Archivos Modificados
|
|
253
|
+
|
|
254
|
+
| Archivo | Cambio |
|
|
255
|
+
|---|---|
|
|
256
|
+
| `src/utils/yaml-to-entity.js` | ✅ Destructura y valida `reference:` en `parseProperty()` |
|
|
257
|
+
| `templates/aggregate/AggregateRoot.java.ejs` | ✅ Genera comentario Javadoc en campos con `reference` |
|
|
258
|
+
| `templates/aggregate/JpaAggregateRoot.java.ejs` | ✅ Genera comentario Javadoc en campos con `reference` |
|
|
259
|
+
| `templates/aggregate/JpaEntity.java.ejs` | ✅ Genera comentario Javadoc en campos con `reference` |
|
|
260
|
+
| `examples/domain-multi-aggregate.yaml` | ✅ Actualizado con `reference:` en `productId` y `warehouseId` |
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## 3. Soft Delete Completo ✅
|
|
265
|
+
|
|
266
|
+
### Descripción
|
|
267
|
+
|
|
268
|
+
Implementado como `hasSoftDelete: true` en la entidad raíz del agregado. El generador inyecta automáticamente el campo `deletedAt`, añade `@SQLRestriction("deleted_at IS NULL")` en la entidad JPA para filtrar eliminados en todas las queries, genera `softDelete()` e `isDeleted()` en el dominio, y cambia el `DeleteCommandHandler` a borrado lógico.
|
|
269
|
+
|
|
270
|
+
### Sintaxis
|
|
271
|
+
|
|
272
|
+
```yaml
|
|
273
|
+
entities:
|
|
274
|
+
- name: product
|
|
275
|
+
isRoot: true
|
|
276
|
+
tableName: products
|
|
277
|
+
hasSoftDelete: true
|
|
278
|
+
audit:
|
|
279
|
+
enabled: true
|
|
280
|
+
fields:
|
|
281
|
+
- name: id
|
|
282
|
+
type: String
|
|
283
|
+
- name: name
|
|
284
|
+
type: String
|
|
285
|
+
- name: price
|
|
286
|
+
type: BigDecimal
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Archivos Modificados
|
|
290
|
+
|
|
291
|
+
| Archivo | Cambio |
|
|
292
|
+
|---|---|
|
|
293
|
+
| `src/utils/yaml-to-entity.js` | ✅ Parsea `hasSoftDelete`, inyecta `deletedAt` en campo de entidad |
|
|
294
|
+
| `src/commands/generate-entities.js` | ✅ Propaga `hasSoftDelete` a todos los contextos, excluye `deletedAt` de comandos/respuestas |
|
|
295
|
+
| `templates/aggregate/AggregateRoot.java.ejs` | ✅ Excluye `deletedAt` del constructor de creación, genera `softDelete()` e `isDeleted()` |
|
|
296
|
+
| `templates/aggregate/JpaAggregateRoot.java.ejs` | ✅ Añade `@SQLRestriction("deleted_at IS NULL")` e import condicional |
|
|
297
|
+
| `templates/aggregate/AggregateRepository.java.ejs` | ✅ Elimina `deleteById()` del puerto cuando hay soft delete |
|
|
298
|
+
| `templates/aggregate/AggregateRepositoryImpl.java.ejs` | ✅ Mismo cambio en la implementación |
|
|
299
|
+
| `templates/crud/DeleteCommandHandler.java.ejs` | ✅ Dos ramas: `findById→softDelete()→save()` vs `deleteById()` |
|
|
300
|
+
| `examples/domain-soft-delete.yaml` | ✅ Reescrito con sintaxis `hasSoftDelete: true` |
|
|
301
|
+
|
|
302
|
+
### Comportamiento generado
|
|
303
|
+
|
|
304
|
+
- `deletedAt` inyectado automáticamente — no declarar a mano en `fields:`
|
|
305
|
+
- `deletedAt` excluido de `CreateCommand`, `ResponseDto` (invisible en API)
|
|
306
|
+
- `GET /products` y `GET /products/{id}` nunca retornan registros eliminados
|
|
307
|
+
- `DELETE /products/{id}` sobre un registro ya eliminado retorna 404
|
|
308
|
+
- `deleteById()` eliminado del contrato del repositorio
|
|
309
|
+
|
|
310
|
+
### Scope excluido
|
|
311
|
+
|
|
312
|
+
- Endpoint de restauración (`PATCH /{id}/restore`) — pendiente de implementar como use case adicional
|
|
313
|
+
- Query param `includeDeleted=true` — requiere query nativa separada
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## � MEDIA PRIORIDAD
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## 4. Paginación en Queries ✅
|
|
322
|
+
|
|
323
|
+
### Descripción
|
|
324
|
+
|
|
325
|
+
Implementado como **paginación siempre activa** en todos los módulos generados. `GET /` ya no devuelve `List<T>` sin límite — devuelve un `PagedResponse<T>` propio con `content`, `page`, `size`, `totalElements` y `totalPages`. Sin flags ni configuración adicional en `domain.yaml`.
|
|
326
|
+
|
|
327
|
+
### Implementación Realizada
|
|
328
|
+
|
|
329
|
+
#### PagedResponse — `shared/application/dtos/PagedResponse.java`
|
|
330
|
+
|
|
331
|
+
Record genérico generado una vez por proyecto en la capa shared. Desacoplado de Spring Data `Page<T>` para no exponer internals de Spring en la API:
|
|
332
|
+
|
|
333
|
+
```java
|
|
334
|
+
public record PagedResponse<T>(
|
|
335
|
+
List<T> content,
|
|
336
|
+
int page,
|
|
337
|
+
int size,
|
|
338
|
+
long totalElements,
|
|
339
|
+
int totalPages
|
|
340
|
+
) {
|
|
341
|
+
public static <T> PagedResponse<T> of(
|
|
342
|
+
List<T> content, int page, int size, long totalElements) {
|
|
343
|
+
int totalPages = size == 0 ? 1 : (int) Math.ceil((double) totalElements / size);
|
|
344
|
+
return new PagedResponse<>(content, page, size, totalElements, totalPages);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
#### Query con parámetros de paginación
|
|
350
|
+
|
|
351
|
+
```java
|
|
352
|
+
public record FindAllOrdersQuery(
|
|
353
|
+
int page,
|
|
354
|
+
int size,
|
|
355
|
+
String sortBy,
|
|
356
|
+
String sortDirection
|
|
357
|
+
) implements Query<PagedResponse<OrderResponseDto>> {}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
#### Handler paginado
|
|
361
|
+
|
|
362
|
+
```java
|
|
363
|
+
public PagedResponse<OrderResponseDto> handle(FindAllOrdersQuery query) {
|
|
364
|
+
Sort sort = Sort.by(Sort.Direction.fromString(query.sortDirection()), query.sortBy());
|
|
365
|
+
Pageable pageable = PageRequest.of(query.page(), query.size(), sort);
|
|
366
|
+
Page<Order> page = repository.findAll(pageable);
|
|
367
|
+
List<OrderResponseDto> content = page.getContent().stream().map(mapper::toDto).toList();
|
|
368
|
+
return PagedResponse.of(content, page.getNumber(), page.getSize(), page.getTotalElements());
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
#### Endpoint REST
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
# Defaults: page=0, size=20, sortBy=id, sortDirection=ASC
|
|
376
|
+
GET /api/v1/orders?page=0&size=10&sortBy=createdAt&sortDirection=DESC
|
|
377
|
+
|
|
378
|
+
# Respuesta
|
|
379
|
+
{
|
|
380
|
+
"content": [...],
|
|
381
|
+
"page": 0,
|
|
382
|
+
"size": 10,
|
|
383
|
+
"totalElements": 87,
|
|
384
|
+
"totalPages": 9
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
#### Archivos modificados
|
|
389
|
+
|
|
390
|
+
| Archivo | Cambio |
|
|
391
|
+
|---|---|
|
|
392
|
+
| `templates/shared/application/dtos/PagedResponse.java.ejs` | ✅ Nuevo template shared |
|
|
393
|
+
| `src/generators/shared-generator.js` | ✅ Método `generatePagedResponse()` |
|
|
394
|
+
| `src/commands/generate-entities.js` | ✅ Llama `generatePagedResponse` en cada `g entities` |
|
|
395
|
+
| `templates/crud/ListQuery.java.ejs` | ✅ Parámetros de paginación |
|
|
396
|
+
| `templates/crud/ListQueryHandler.java.ejs` | ✅ `PageRequest` + `PagedResponse` |
|
|
397
|
+
| `templates/aggregate/AggregateRepository.java.ejs` | ✅ `Page<X> findAll(Pageable)` |
|
|
398
|
+
| `templates/aggregate/AggregateRepositoryImpl.java.ejs` | ✅ Implementación `jpaRepository.findAll(pageable).map(...)` |
|
|
399
|
+
| `templates/crud/Controller.java.ejs` | ✅ `@RequestParam` page/size/sortBy/sortDirection |
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## 5. Optimistic Locking
|
|
404
|
+
|
|
405
|
+
### Descripción
|
|
406
|
+
|
|
407
|
+
El **Optimistic Locking** previene la pérdida de actualizaciones cuando dos usuarios modifican el mismo registro simultáneamente. Sin él, la última escritura gana sin advertencia, causando pérdida de datos silenciosa.
|
|
408
|
+
|
|
409
|
+
### Sintaxis Propuesta
|
|
410
|
+
|
|
411
|
+
```yaml
|
|
412
|
+
entities:
|
|
413
|
+
- name: account
|
|
414
|
+
isRoot: true
|
|
415
|
+
audit:
|
|
416
|
+
enabled: true
|
|
417
|
+
optimisticLocking: true
|
|
418
|
+
fields:
|
|
419
|
+
- name: id
|
|
420
|
+
type: String
|
|
421
|
+
- name: balance
|
|
422
|
+
type: BigDecimal
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Código Generado
|
|
426
|
+
|
|
427
|
+
```java
|
|
428
|
+
@Entity
|
|
429
|
+
public class AccountJpa extends AuditableEntity {
|
|
430
|
+
@Id
|
|
431
|
+
private String id;
|
|
432
|
+
|
|
433
|
+
@Column(name = "balance")
|
|
434
|
+
private BigDecimal balance;
|
|
435
|
+
|
|
436
|
+
@Version
|
|
437
|
+
@Column(name = "version", nullable = false)
|
|
438
|
+
private Long version;
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
```java
|
|
443
|
+
// El UpdateCommand incluye la versión esperada
|
|
444
|
+
public record UpdateAccountCommand(
|
|
445
|
+
String id,
|
|
446
|
+
BigDecimal newBalance,
|
|
447
|
+
Long version // Si no coincide con la BD: HTTP 409 Conflict
|
|
448
|
+
) {}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
```java
|
|
452
|
+
// ControllerAdvice generado
|
|
453
|
+
@ExceptionHandler(ObjectOptimisticLockingFailureException.class)
|
|
454
|
+
public ResponseEntity<ErrorDto> handleOptimisticLock(ObjectOptimisticLockingFailureException ex) {
|
|
455
|
+
return ResponseEntity.status(HttpStatus.CONFLICT)
|
|
456
|
+
.body(new ErrorDto("CONFLICT", "The record was modified by another user. Please reload and retry."));
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## 6. Read Models Separados (Proyecciones)
|
|
463
|
+
|
|
464
|
+
### Descripción
|
|
465
|
+
|
|
466
|
+
En CQRS puro, el lado de lectura puede tener su propio modelo optimizado para consultas, independiente del modelo de escritura. Los `*ResponseDto` actuales son transformaciones directas del dominio, suficiente para casos simples pero insuficientes para reportes o vistas que joinean múltiples agregados.
|
|
467
|
+
|
|
468
|
+
### Sintaxis Propuesta
|
|
469
|
+
|
|
470
|
+
```yaml
|
|
471
|
+
aggregates:
|
|
472
|
+
- name: Order
|
|
473
|
+
readModels:
|
|
474
|
+
- name: OrderSummary
|
|
475
|
+
description: "Vista desnormalizada para listados"
|
|
476
|
+
fields:
|
|
477
|
+
- name: id
|
|
478
|
+
type: String
|
|
479
|
+
- name: orderNumber
|
|
480
|
+
type: String
|
|
481
|
+
- name: customerName
|
|
482
|
+
type: String
|
|
483
|
+
- name: totalAmount
|
|
484
|
+
type: BigDecimal
|
|
485
|
+
- name: itemCount
|
|
486
|
+
type: Integer
|
|
487
|
+
- name: status
|
|
488
|
+
type: OrderStatus
|
|
489
|
+
source: native_query
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Código Generado
|
|
493
|
+
|
|
494
|
+
```java
|
|
495
|
+
public interface OrderSummaryProjection {
|
|
496
|
+
String getId();
|
|
497
|
+
String getOrderNumber();
|
|
498
|
+
String getCustomerName();
|
|
499
|
+
BigDecimal getTotalAmount();
|
|
500
|
+
Integer getItemCount();
|
|
501
|
+
OrderStatus getStatus();
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
```java
|
|
506
|
+
@Query(value = """
|
|
507
|
+
SELECT
|
|
508
|
+
o.id,
|
|
509
|
+
o.order_number AS orderNumber,
|
|
510
|
+
c.name AS customerName,
|
|
511
|
+
o.total_amount AS totalAmount,
|
|
512
|
+
COUNT(i.id) AS itemCount,
|
|
513
|
+
o.status
|
|
514
|
+
FROM orders o
|
|
515
|
+
JOIN customers c ON c.id = o.customer_id
|
|
516
|
+
LEFT JOIN order_items i ON i.order_id = o.id
|
|
517
|
+
WHERE o.deleted = false
|
|
518
|
+
GROUP BY o.id, c.name
|
|
519
|
+
""", nativeQuery = true)
|
|
520
|
+
Page<OrderSummaryProjection> findOrderSummaries(Pageable pageable);
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
## 7. Enums con Comportamiento y Transiciones ✅
|
|
526
|
+
|
|
527
|
+
### Descripción
|
|
528
|
+
|
|
529
|
+
Los enums generados actualmente son solo listas de valores. En DDD, los enums frecuentemente encapsulan lógica de transición de estado — qué valores son válidos como siguiente estado, qué acciones se permiten. Esto elimina `if/switch` dispersos en el dominio.
|
|
530
|
+
|
|
531
|
+
### Sintaxis Propuesta
|
|
532
|
+
|
|
533
|
+
```yaml
|
|
534
|
+
enums:
|
|
535
|
+
- name: OrderStatus
|
|
536
|
+
withTransitions: true
|
|
537
|
+
values:
|
|
538
|
+
- DRAFT
|
|
539
|
+
- PLACED
|
|
540
|
+
- CONFIRMED
|
|
541
|
+
- SHIPPED
|
|
542
|
+
- DELIVERED
|
|
543
|
+
- CANCELLED
|
|
544
|
+
transitions:
|
|
545
|
+
DRAFT: [PLACED, CANCELLED]
|
|
546
|
+
PLACED: [CONFIRMED, CANCELLED]
|
|
547
|
+
CONFIRMED: [SHIPPED, CANCELLED]
|
|
548
|
+
SHIPPED: [DELIVERED]
|
|
549
|
+
DELIVERED: []
|
|
550
|
+
CANCELLED: []
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Código Generado
|
|
554
|
+
|
|
555
|
+
```java
|
|
556
|
+
public enum OrderStatus {
|
|
557
|
+
DRAFT(Set.of("PLACED", "CANCELLED")),
|
|
558
|
+
PLACED(Set.of("CONFIRMED", "CANCELLED")),
|
|
559
|
+
CONFIRMED(Set.of("SHIPPED", "CANCELLED")),
|
|
560
|
+
SHIPPED(Set.of("DELIVERED")),
|
|
561
|
+
DELIVERED(Set.of()),
|
|
562
|
+
CANCELLED(Set.of());
|
|
563
|
+
|
|
564
|
+
private final Set<String> allowedTransitions;
|
|
565
|
+
|
|
566
|
+
OrderStatus(Set<String> allowedTransitions) {
|
|
567
|
+
this.allowedTransitions = allowedTransitions;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
public boolean canTransitionTo(OrderStatus next) {
|
|
571
|
+
return allowedTransitions.contains(next.name());
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
public void validateTransitionTo(OrderStatus next) {
|
|
575
|
+
if (!canTransitionTo(next)) {
|
|
576
|
+
throw new IllegalStateException(
|
|
577
|
+
String.format("Cannot transition from %s to %s", this.name(), next.name())
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
```java
|
|
585
|
+
// Uso en entidad de dominio — declarativo, sin if/switch
|
|
586
|
+
public void confirm() {
|
|
587
|
+
this.status.validateTransitionTo(OrderStatus.CONFIRMED);
|
|
588
|
+
this.status = OrderStatus.CONFIRMED;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
public void ship() {
|
|
592
|
+
this.status.validateTransitionTo(OrderStatus.SHIPPED);
|
|
593
|
+
this.status = OrderStatus.SHIPPED;
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## 8. Políticas y Especificaciones
|
|
600
|
+
|
|
601
|
+
### Descripción
|
|
602
|
+
|
|
603
|
+
El **Specification Pattern** encapsula reglas de negocio complejas como objetos combinables. Es especialmente útil cuando las mismas reglas se aplican en múltiples lugares: validación al crear, filtrado en queries, reportes. Actualmente eva4j no genera ninguna infraestructura para este patrón.
|
|
604
|
+
|
|
605
|
+
### Sintaxis Propuesta
|
|
606
|
+
|
|
607
|
+
```yaml
|
|
608
|
+
aggregates:
|
|
609
|
+
- name: Order
|
|
610
|
+
specifications:
|
|
611
|
+
- name: OrderCanBeShipped
|
|
612
|
+
description: "Una orden puede enviarse si está confirmada y tiene dirección de envío"
|
|
613
|
+
- name: OrderIsOverdue
|
|
614
|
+
description: "Una orden está vencida si lleva más de 30 días en estado PLACED"
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### Código Generado
|
|
618
|
+
|
|
619
|
+
```java
|
|
620
|
+
public interface Specification<T> {
|
|
621
|
+
boolean isSatisfiedBy(T candidate);
|
|
622
|
+
|
|
623
|
+
default Specification<T> and(Specification<T> other) {
|
|
624
|
+
return candidate -> this.isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
default Specification<T> or(Specification<T> other) {
|
|
628
|
+
return candidate -> this.isSatisfiedBy(candidate) || other.isSatisfiedBy(candidate);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
default Specification<T> not() {
|
|
632
|
+
return candidate -> !this.isSatisfiedBy(candidate);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
```java
|
|
638
|
+
@Component
|
|
639
|
+
public class OrderCanBeShippedSpecification implements Specification<Order> {
|
|
640
|
+
@Override
|
|
641
|
+
public boolean isSatisfiedBy(Order order) {
|
|
642
|
+
return order.getStatus() == OrderStatus.CONFIRMED
|
|
643
|
+
&& order.getShippingAddress() != null;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
```java
|
|
649
|
+
@Component
|
|
650
|
+
public class ShipOrderCommandHandler {
|
|
651
|
+
private final OrderCanBeShippedSpecification canBeShipped;
|
|
652
|
+
|
|
653
|
+
public void handle(ShipOrderCommand command) {
|
|
654
|
+
Order order = orderRepository.findById(command.orderId()).orElseThrow();
|
|
655
|
+
if (!canBeShipped.isSatisfiedBy(order)) {
|
|
656
|
+
throw new OrderCannotBeShippedException(command.orderId());
|
|
657
|
+
}
|
|
658
|
+
order.ship();
|
|
659
|
+
orderRepository.save(order);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
## � TOOLING Y CALIDAD
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
## 9. Validación de domain.yaml con JSON Schema
|
|
671
|
+
|
|
672
|
+
### Descripción
|
|
673
|
+
|
|
674
|
+
Actualmente los errores en `domain.yaml` producen mensajes crípticos de Node.js en tiempo de ejecución. Un JSON Schema publicado permitiría validación inmediata en el editor (VS Code, IntelliJ) antes de ejecutar `eva4j g entities`, con autocompletado y documentación inline.
|
|
675
|
+
|
|
676
|
+
### Comportamiento Esperado
|
|
677
|
+
|
|
678
|
+
Con el schema configurado, el editor mostraría errores como:
|
|
679
|
+
|
|
680
|
+
```
|
|
681
|
+
domain.yaml:14:5 error Property "tipe" is not allowed. Did you mean "type"?
|
|
682
|
+
domain.yaml:28:9 error "audit.trackUser" requires "audit.enabled: true"
|
|
683
|
+
domain.yaml:41:7 error Relationship type "OneToFew" is not valid.
|
|
684
|
+
Expected one of: OneToOne, OneToMany, ManyToOne, ManyToMany
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### Implementación
|
|
688
|
+
|
|
689
|
+
```json
|
|
690
|
+
{
|
|
691
|
+
"": "http://json-schema.org/draft-07/schema#",
|
|
692
|
+
"title": "eva4j domain.yaml",
|
|
693
|
+
"type": "object",
|
|
694
|
+
"required": ["aggregates"],
|
|
695
|
+
"properties": {
|
|
696
|
+
"aggregates": {
|
|
697
|
+
"type": "array",
|
|
698
|
+
"items": {
|
|
699
|
+
"required": ["name", "entities"],
|
|
700
|
+
"properties": {
|
|
701
|
+
"name": { "type": "string", "pattern": "^[A-Z][a-zA-Z0-9]*$" },
|
|
702
|
+
"entities": { "type": "array" }
|
|
703
|
+
},
|
|
704
|
+
"additionalProperties": false
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
```json
|
|
712
|
+
// .vscode/settings.json (generado por eva4j create)
|
|
713
|
+
{
|
|
714
|
+
"yaml.schemas": {
|
|
715
|
+
"https://eva4j.dev/schemas/domain-yaml.json": "domain.yaml"
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
---
|
|
721
|
+
|
|
722
|
+
## 10. Generación Incremental / Diff ✅
|
|
723
|
+
|
|
724
|
+
### Descripción
|
|
725
|
+
|
|
726
|
+
Implementado como **safe mode con checksums SHA-256**. `eva4j g entities` (y `g usecase`, `g resource`) detecta si un archivo generado fue modificado manualmente después de su generación y lo omite automáticamente en re-ejecuciones. El flag `--force` permite sobreescribir cuando se desea regenerar intencionalmente.
|
|
727
|
+
|
|
728
|
+
### Implementación Realizada
|
|
729
|
+
|
|
730
|
+
#### ChecksumManager — `src/utils/checksum-manager.js`
|
|
731
|
+
|
|
732
|
+
Almacena hashes SHA-256 de cada archivo escrito en un archivo `.eva4j-checksums.json` por módulo (junto al `domain.yaml`). Métodos clave:
|
|
733
|
+
- `wasModified(destPath, generatedContent)` — compara hash en disco vs hash almacenado
|
|
734
|
+
- `recordWrite(destPath, content)` — registra hash del archivo recién escrito
|
|
735
|
+
- `save()` — persiste la base de datos de checksums
|
|
736
|
+
|
|
737
|
+
#### Safe mode en `renderAndWrite()` — `src/utils/template-engine.js`
|
|
738
|
+
|
|
739
|
+
```bash
|
|
740
|
+
# Comportamiento por defecto (safe mode)
|
|
741
|
+
eva4j g entities orders
|
|
742
|
+
|
|
743
|
+
# Output:
|
|
744
|
+
# ✅ Order.java -- regenerado (sin cambios previos)
|
|
745
|
+
# ✅ OrderJpa.java -- regenerado (sin cambios previos)
|
|
746
|
+
# ⚠️ SKIP OrderApplicationMapper.java -- omitido (modificado manualmente — use --force to overwrite)
|
|
747
|
+
# ⚠️ SKIP CreateOrderCommandHandler.java -- omitido (modificado manualmente)
|
|
748
|
+
|
|
749
|
+
# Con --force: sobreescribe todo
|
|
750
|
+
eva4j g entities orders --force
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
#### Comandos con safe mode integrado
|
|
754
|
+
|
|
755
|
+
| Comando | Estado |
|
|
756
|
+
|---|---|
|
|
757
|
+
| `eva4j g entities <module>` | ✅ Integrado |
|
|
758
|
+
| `eva4j g usecase <module> <name>` | ✅ Integrado |
|
|
759
|
+
| `eva4j g resource <module>` | ✅ Integrado |
|
|
760
|
+
| `eva4j create` / `eva4j add module` | ⚠️ Out of scope (archivos de scaffolding inicial, no se re-ejecutan) |
|
|
761
|
+
|
|
762
|
+
#### Nota sobre portabilidad
|
|
763
|
+
|
|
764
|
+
`.eva4j-checksums.json` está en `.gitignore` por diseño — es estado local de la máquina de desarrollo. En un `git clone` fresco, la primera re-ejecución regenerará todos los archivos (comportamiento correcto en ese contexto).
|
|
765
|
+
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
## 11. Comando `eva4j doctor`
|
|
769
|
+
|
|
770
|
+
### Descripción
|
|
771
|
+
|
|
772
|
+
Un comando de análisis estático que examina el código del proyecto y detecta violaciones de los patrones DDD que eva4j promueve. Útil para onboarding de equipos y revisiones de arquitectura.
|
|
773
|
+
|
|
774
|
+
### Uso
|
|
775
|
+
|
|
776
|
+
```bash
|
|
777
|
+
eva4j doctor
|
|
778
|
+
eva4j doctor --module orders
|
|
779
|
+
eva4j doctor --verbose
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
### Salida Esperada
|
|
783
|
+
|
|
784
|
+
```
|
|
785
|
+
� eva4j doctor — Analizando proyecto...
|
|
786
|
+
|
|
787
|
+
� Módulo: orders
|
|
788
|
+
|
|
789
|
+
❌ Order.java:45
|
|
790
|
+
Setter público detectado: setStatus(OrderStatus status)
|
|
791
|
+
Recomendación: Reemplazar con método de negocio: confirm(), cancel(), etc.
|
|
792
|
+
|
|
793
|
+
❌ OrderItemJpa.java:12
|
|
794
|
+
Falta @JoinColumn en relación inverse @ManyToOne
|
|
795
|
+
Recomendación: Agregar @JoinColumn(name = "order_id", nullable = false)
|
|
796
|
+
|
|
797
|
+
⚠️ CreateOrderCommandHandler.java:67
|
|
798
|
+
Lógica de negocio detectada fuera del dominio: totalAmount > 0
|
|
799
|
+
Recomendación: Mover validación a Order.place() como invariante de dominio
|
|
800
|
+
|
|
801
|
+
✅ OrderRepository.java — OK
|
|
802
|
+
✅ OrderMapper.java — OK
|
|
803
|
+
|
|
804
|
+
� Resultado: 2 errores, 1 advertencia, 2 archivos OK
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
### Reglas Implementadas
|
|
808
|
+
|
|
809
|
+
| Regla | Severidad | Descripción |
|
|
810
|
+
|---|---|---|
|
|
811
|
+
| No setters en dominio | ❌ Error | Detecta `set*` públicos en entidades de dominio |
|
|
812
|
+
| No constructor vacío en dominio | ❌ Error | Detecta `public Entity()` sin parámetros |
|
|
813
|
+
| Repositorio solo para raíz | ❌ Error | Detecta `Repository<SecondaryEntity>` |
|
|
814
|
+
| FK cross-aggregate | ⚠️ Warn | `@ManyToOne` a entidad de otro agregado |
|
|
815
|
+
| Lógica de negocio en handler | ⚠️ Warn | Condicionales complejos en CommandHandlers |
|
|
816
|
+
| Value Object mutable | ⚠️ Warn | Value Objects con setters o campos non-final |
|
|
817
|
+
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
## 12. Tests Generados Completos
|
|
821
|
+
|
|
822
|
+
### Descripción
|
|
823
|
+
|
|
824
|
+
Actualmente eva4j genera estructura de test básica. Para proyectos en producción, los tests deben cubrir invariantes de dominio, contrato de mappers y tests de integración de módulo con Spring Modulith.
|
|
825
|
+
|
|
826
|
+
### Tests de Dominio Generados
|
|
827
|
+
|
|
828
|
+
```java
|
|
829
|
+
class OrderTest {
|
|
830
|
+
|
|
831
|
+
@Test
|
|
832
|
+
@DisplayName("Should create order with valid data")
|
|
833
|
+
void shouldCreateOrder() {
|
|
834
|
+
Order order = new Order("ORD-001", "CUST-123");
|
|
835
|
+
assertThat(order.getOrderNumber()).isEqualTo("ORD-001");
|
|
836
|
+
assertThat(order.getStatus()).isEqualTo(OrderStatus.DRAFT);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
@Test
|
|
840
|
+
@DisplayName("Should not allow adding item to cancelled order")
|
|
841
|
+
void shouldRejectItemOnCancelledOrder() {
|
|
842
|
+
Order order = new Order("ORD-001", "CUST-123");
|
|
843
|
+
order.cancel("Test");
|
|
844
|
+
assertThatThrownBy(() -> order.addItem("PROD-1", 2, BigDecimal.TEN))
|
|
845
|
+
.isInstanceOf(IllegalStateException.class)
|
|
846
|
+
.hasMessageContaining("cancelled");
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
### Tests de Mapper (roundtrip)
|
|
852
|
+
|
|
853
|
+
```java
|
|
854
|
+
class OrderMapperTest {
|
|
855
|
+
private final OrderMapper mapper = new OrderMapper();
|
|
856
|
+
|
|
857
|
+
@Test
|
|
858
|
+
@DisplayName("Domain -> JPA -> Domain roundtrip preserves all fields")
|
|
859
|
+
void domainToJpaRoundtrip() {
|
|
860
|
+
Order original = new Order("id-1", "ORD-001", "CUST-123",
|
|
861
|
+
OrderStatus.DRAFT, LocalDateTime.now(), LocalDateTime.now());
|
|
862
|
+
OrderJpa jpa = mapper.toJpa(original);
|
|
863
|
+
Order restored = mapper.toDomain(jpa);
|
|
864
|
+
assertThat(restored.getId()).isEqualTo(original.getId());
|
|
865
|
+
assertThat(restored.getStatus()).isEqualTo(original.getStatus());
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
### Tests de Módulo con Spring Modulith
|
|
871
|
+
|
|
872
|
+
```java
|
|
873
|
+
@ApplicationModuleTest
|
|
874
|
+
class OrderModuleTest {
|
|
875
|
+
|
|
876
|
+
@Test
|
|
877
|
+
@DisplayName("Module is self-contained -- no illegal cross-module dependencies")
|
|
878
|
+
void moduleShouldBeValid(ApplicationModules modules) {
|
|
879
|
+
modules.verify();
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
@Test
|
|
883
|
+
@DisplayName("Create order publishes OrderPlacedEvent")
|
|
884
|
+
@Transactional
|
|
885
|
+
void shouldPublishOrderPlacedEvent(
|
|
886
|
+
@Autowired CreateOrderCommandHandler handler,
|
|
887
|
+
AssertablePublishedEvents events
|
|
888
|
+
) {
|
|
889
|
+
handler.handle(new CreateOrderCommand("ORD-001", "CUST-123"));
|
|
890
|
+
events.assertThat()
|
|
891
|
+
.contains(OrderPlacedEvent.class)
|
|
892
|
+
.matching(e -> e.getAggregateId().equals("ORD-001"));
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
## 13. Diagrama Mermaid desde domain.yaml (`eva g diagram`)
|
|
900
|
+
|
|
901
|
+
### Descripción
|
|
902
|
+
|
|
903
|
+
Generar automáticamente un diagrama **Mermaid `classDiagram`** a partir del `domain.yaml` de un módulo. El diagrama plasma visualmente la estructura del agregado: entidades, value objects, enums, relaciones y visibilidad de campos, usando estereotipos DDD.
|
|
904
|
+
|
|
905
|
+
El output es un archivo `.md` con el diagrama incrustado, renderizable nativamente en GitHub, VS Code (Markdown Preview), Notion y el reporte HTML de `eva evaluate system`.
|
|
906
|
+
|
|
907
|
+
### Comando propuesto
|
|
908
|
+
|
|
909
|
+
```bash
|
|
910
|
+
eva generate diagram <module>
|
|
911
|
+
eva g diagram <module> # alias corto
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
### Ejemplo de output para `domain-one-to-one.yaml`
|
|
915
|
+
|
|
916
|
+
```mermaid
|
|
917
|
+
classDiagram
|
|
918
|
+
namespace UserAggregate {
|
|
919
|
+
class User {
|
|
920
|
+
<<aggregate root>>
|
|
921
|
+
+String id
|
|
922
|
+
+String username
|
|
923
|
+
+String email
|
|
924
|
+
-String passwordHash
|
|
925
|
+
+UserStatus status
|
|
926
|
+
+LocalDateTime registrationDate
|
|
927
|
+
+LocalDateTime lastLogin
|
|
928
|
+
+LocalDateTime createdAt
|
|
929
|
+
+LocalDateTime updatedAt
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
class UserProfile {
|
|
933
|
+
<<entity>>
|
|
934
|
+
+Long id
|
|
935
|
+
+String firstName
|
|
936
|
+
+String lastName
|
|
937
|
+
+LocalDate dateOfBirth
|
|
938
|
+
+String phoneNumber
|
|
939
|
+
+String bio
|
|
940
|
+
+String avatarUrl
|
|
941
|
+
+Boolean isPublic
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
class Address {
|
|
945
|
+
<<value object>>
|
|
946
|
+
+String street
|
|
947
|
+
+String city
|
|
948
|
+
+String state
|
|
949
|
+
+String zipCode
|
|
950
|
+
+String country
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
class UserPreferences {
|
|
954
|
+
<<value object>>
|
|
955
|
+
+String language
|
|
956
|
+
+String timezone
|
|
957
|
+
+Boolean emailNotifications
|
|
958
|
+
+Boolean smsNotifications
|
|
959
|
+
+String theme
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
class UserStatus {
|
|
963
|
+
<<enum>>
|
|
964
|
+
ACTIVE
|
|
965
|
+
INACTIVE
|
|
966
|
+
SUSPENDED
|
|
967
|
+
PENDING_VERIFICATION
|
|
968
|
+
DELETED
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
User "1" --o "1" UserProfile : owns
|
|
973
|
+
UserProfile *-- Address : embedded
|
|
974
|
+
UserProfile *-- UserPreferences : embedded
|
|
975
|
+
User --> UserStatus : status
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
### Reglas de generación
|
|
979
|
+
|
|
980
|
+
| Elemento domain.yaml | Stereotipo Mermaid | Visibilidad de campos |
|
|
981
|
+
|---|---|---|
|
|
982
|
+
| `isRoot: true` | `<<aggregate root>>` | `+` público, `-` hidden |
|
|
983
|
+
| Entidad secundaria | `<<entity>>` | igual |
|
|
984
|
+
| `valueObjects[]` | `<<value object>>` | `+` todos |
|
|
985
|
+
| `enums[]` | `<<enum>>` | valores como líneas |
|
|
986
|
+
| `audit.enabled: true` | campos `createdAt`, `updatedAt` en la entidad | auto-añadidos |
|
|
987
|
+
| `audit.trackUser: true` | campos `createdBy`, `updatedBy` en la entidad | auto-añadidos |
|
|
988
|
+
| `hidden: true` | prefijo `-` (privado) en lugar de `+` | |
|
|
989
|
+
| `readOnly: true` | prefijo `~` (package) para indicar derivado | |
|
|
990
|
+
|
|
991
|
+
### Variante HTML (integración con `eva evaluate system`)
|
|
992
|
+
|
|
993
|
+
Otra opción es agregar un 5.º tab **"Dominio"** al reporte de `evaluate system` que renderice el diagrama Mermaid de cada módulo usando la librería [mermaid.js CDN](https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js), sin necesidad de un comando separado.
|
|
994
|
+
|
|
995
|
+
### Estado
|
|
996
|
+
|
|
997
|
+
✅ Implementado
|
|
998
|
+
|
|
999
|
+
---
|
|
1000
|
+
|
|
1001
|
+
## ✅ IMPLEMENTADO
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
## 15. Auditoría (Implementada)
|
|
1006
|
+
|
|
1007
|
+
| Característica | Sintaxis | Estado |
|
|
1008
|
+
|---|---|---|
|
|
1009
|
+
| Auditoría de tiempo | `audit: { enabled: true }` | ✅ Implementado |
|
|
1010
|
+
| Auditoría de usuario | `audit: { trackUser: true }` | ✅ Implementado |
|
|
1011
|
+
| `@EnableJpaAuditing` condicional | `auditorAwareRef` solo si `trackUser: true` | ✅ Implementado |
|
|
1012
|
+
| Regeneración de `Application.java` en `g entities` | Automático | ✅ Implementado |
|
|
1013
|
+
|
|
1014
|
+
Cuando `trackUser: true` se generan automáticamente: `UserContextFilter`, `UserContextHolder`, `AuditorAwareImpl` y la anotación `@EnableJpaAuditing(auditorAwareRef = "auditorProvider")` en `Application.java`.
|
|
1015
|
+
|
|
1016
|
+
Cuando solo `enabled: true` se genera `@EnableJpaAuditing` sin `auditorAwareRef`.
|
|
1017
|
+
|
|
1018
|
+
---
|
|
1019
|
+
|
|
1020
|
+
## 14. Validaciones JSR-303 (Implementado)
|
|
1021
|
+
|
|
1022
|
+
Generación automática de anotaciones Bean Validation en `Create*Command` y `Create*Dto`. Las validaciones **nunca** se generan en entidades de dominio ni en campos `readOnly: true`.
|
|
1023
|
+
|
|
1024
|
+
### Sintaxis
|
|
1025
|
+
|
|
1026
|
+
```yaml
|
|
1027
|
+
fields:
|
|
1028
|
+
- name: email
|
|
1029
|
+
type: String
|
|
1030
|
+
validations:
|
|
1031
|
+
- type: Email
|
|
1032
|
+
message: "Email inválido"
|
|
1033
|
+
- type: NotBlank
|
|
1034
|
+
- name: age
|
|
1035
|
+
type: Integer
|
|
1036
|
+
validations:
|
|
1037
|
+
- type: Min
|
|
1038
|
+
value: 18
|
|
1039
|
+
- type: Max
|
|
1040
|
+
value: 120
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
### Código Generado
|
|
1044
|
+
|
|
1045
|
+
```java
|
|
1046
|
+
@Email(message = "Email inválido")
|
|
1047
|
+
@NotBlank
|
|
1048
|
+
private String email;
|
|
1049
|
+
|
|
1050
|
+
@Min(value = 18)
|
|
1051
|
+
@Max(value = 120)
|
|
1052
|
+
private Integer age;
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
---
|
|
1056
|
+
|
|
1057
|
+
## 16. `defaultValue` para campos `readOnly` (Implementado)
|
|
1058
|
+
|
|
1059
|
+
Permite especificar un valor inicial para campos `readOnly` directamente en `domain.yaml`. El valor se emite en el **constructor de creación** de la entidad de dominio y como field initializer con `@Builder.Default` en la entidad JPA.
|
|
1060
|
+
|
|
1061
|
+
### Sintaxis
|
|
1062
|
+
|
|
1063
|
+
```yaml
|
|
1064
|
+
entities:
|
|
1065
|
+
- name: order
|
|
1066
|
+
fields:
|
|
1067
|
+
- name: status
|
|
1068
|
+
type: OrderStatus
|
|
1069
|
+
readOnly: true
|
|
1070
|
+
defaultValue: PENDING # Enum value
|
|
1071
|
+
|
|
1072
|
+
- name: totalAmount
|
|
1073
|
+
type: BigDecimal
|
|
1074
|
+
readOnly: true
|
|
1075
|
+
defaultValue: "0.00" # BigDecimal literal
|
|
1076
|
+
|
|
1077
|
+
- name: itemCount
|
|
1078
|
+
type: Integer
|
|
1079
|
+
readOnly: true
|
|
1080
|
+
defaultValue: 0 # Integer literal
|
|
1081
|
+
|
|
1082
|
+
- name: isActive
|
|
1083
|
+
type: Boolean
|
|
1084
|
+
readOnly: true
|
|
1085
|
+
defaultValue: true # Boolean literal
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
### Código Generado — Dominio
|
|
1089
|
+
|
|
1090
|
+
```java
|
|
1091
|
+
// Constructor de creación — defaultValue asignado automáticamente
|
|
1092
|
+
public Order(String orderNumber, String customerId) {
|
|
1093
|
+
this.orderNumber = orderNumber;
|
|
1094
|
+
this.customerId = customerId;
|
|
1095
|
+
// readOnly fields initialized with defaultValue:
|
|
1096
|
+
this.status = OrderStatus.PENDING;
|
|
1097
|
+
this.totalAmount = new BigDecimal("0.00");
|
|
1098
|
+
this.itemCount = 0;
|
|
1099
|
+
this.isActive = true;
|
|
1100
|
+
}
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
### Código Generado — JPA
|
|
1104
|
+
|
|
1105
|
+
```java
|
|
1106
|
+
@Builder.Default
|
|
1107
|
+
private OrderStatus status = OrderStatus.PENDING;
|
|
1108
|
+
|
|
1109
|
+
@Builder.Default
|
|
1110
|
+
private BigDecimal totalAmount = new BigDecimal("0.00");
|
|
1111
|
+
|
|
1112
|
+
@Builder.Default
|
|
1113
|
+
private Integer itemCount = 0;
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
### Tipos soportados
|
|
1117
|
+
|
|
1118
|
+
| Tipo Java | Ejemplo YAML | Java emitido |
|
|
1119
|
+
|-----------|-------------|---------------|
|
|
1120
|
+
| `String` | `defaultValue: hello` | `"hello"` |
|
|
1121
|
+
| `Integer` / `Long` | `defaultValue: 0` | `0` / `0L` |
|
|
1122
|
+
| `Boolean` | `defaultValue: false` | `false` |
|
|
1123
|
+
| `BigDecimal` | `defaultValue: "0.00"` | `new BigDecimal("0.00")` |
|
|
1124
|
+
| `LocalDateTime` | `defaultValue: now` | `LocalDateTime.now()` |
|
|
1125
|
+
| `LocalDate` | `defaultValue: now` | `LocalDate.now()` |
|
|
1126
|
+
| `Instant` | `defaultValue: now` | `Instant.now()` |
|
|
1127
|
+
| `UUID` | `defaultValue: random` | `UUID.randomUUID()` |
|
|
1128
|
+
| Enum | `defaultValue: ACTIVE` | `EnumType.ACTIVE` |
|
|
1129
|
+
|
|
1130
|
+
### Reglas
|
|
1131
|
+
|
|
1132
|
+
- `defaultValue` **solo es válido** en campos con `readOnly: true`. Si se usa en un campo no-readOnly, se emite un warning y se ignora.
|
|
1133
|
+
- El campo **sigue siendo readOnly** — no aparece en el constructor de negocio ni en `CreateDto`.
|
|
1134
|
+
- En campos con `autoInit` (enum con `initialValue`), `defaultValue` es ignorado — `autoInit` tiene precedencia.
|
|
1135
|
+
|
|
1136
|
+
### Archivos Modificados
|
|
1137
|
+
|
|
1138
|
+
| Archivo | Cambio |
|
|
1139
|
+
|---|---|
|
|
1140
|
+
| `src/utils/yaml-to-entity.js` | ✅ `computeJavaDefaultValue()` + `defaultValue` en `parseProperty()` |
|
|
1141
|
+
| `templates/aggregate/AggregateRoot.java.ejs` | ✅ Emite `this.field = defaultValue` en constructor de creación |
|
|
1142
|
+
| `templates/aggregate/DomainEntity.java.ejs` | ✅ Mismo cambio para entidades secundarias |
|
|
1143
|
+
| `templates/aggregate/JpaAggregateRoot.java.ejs` | ✅ `@Builder.Default` + field initializer |
|
|
1144
|
+
| `templates/aggregate/JpaEntity.java.ejs` | ✅ Mismo cambio para entidades JPA secundarias |
|
|
1145
|
+
| `examples/domain-field-visibility.yaml` | ✅ Ejemplos con `defaultValue` en campos readOnly |
|
|
1146
|
+
|
|
1147
|
+
---
|
|
1148
|
+
|
|
1149
|
+
## 15. Transactional Outbox Pattern
|
|
1150
|
+
|
|
1151
|
+
### Descripción
|
|
1152
|
+
|
|
1153
|
+
El **Transactional Outbox Pattern** es la evolución natural de los Domain Events implementados (ítem 1). Resuelve el caso donde el proceso muere después del commit de BD pero antes de que `ApplicationEventPublisher` llegue a publicar al broker externo — en ese escenario, el evento se pierde silenciosamente.
|
|
1154
|
+
|
|
1155
|
+
El patrón garantiza **at-least-once delivery**: los eventos son almacenados en la misma transacción que el agregado y un proceso separado los publica de forma resiliente.
|
|
1156
|
+
|
|
1157
|
+
Los Domain Events ya implementados (`ApplicationEventPublisher` + `@TransactionalEventListener(AFTER_COMMIT)`) son suficientes para la mayoría de sistemas. Esta feature es necesaria para dominios críticos: pagos, auditoría regulatoria, inventario en tiempo real.
|
|
1158
|
+
|
|
1159
|
+
**Nota:** El puerto `MessageBroker` ya generado no requiere cambios — solo se añade la capa de persistencia intermedia.
|
|
1160
|
+
|
|
1161
|
+
### Flujo del Patrón
|
|
1162
|
+
|
|
1163
|
+
```
|
|
1164
|
+
BD Transaction:
|
|
1165
|
+
→ INSERT INTO orders ...
|
|
1166
|
+
→ INSERT INTO outbox_events (type, payload, published=false) ← misma TX
|
|
1167
|
+
→ COMMIT
|
|
1168
|
+
|
|
1169
|
+
Proceso resiliente (polling o CDC con Debezium):
|
|
1170
|
+
→ SELECT * FROM outbox_events WHERE published = false
|
|
1171
|
+
→ Publica a Kafka / RabbitMQ / SNS
|
|
1172
|
+
→ UPDATE outbox_events SET published = true
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
### Sintaxis Propuesta en domain.yaml
|
|
1176
|
+
|
|
1177
|
+
```yaml
|
|
1178
|
+
aggregates:
|
|
1179
|
+
- name: Order
|
|
1180
|
+
events:
|
|
1181
|
+
- name: OrderPlaced
|
|
1182
|
+
kafka: true
|
|
1183
|
+
delivery: at-least-once # ← activa Outbox Pattern para este evento
|
|
1184
|
+
fields:
|
|
1185
|
+
- name: customerId
|
|
1186
|
+
type: String
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
### Código Generado (Outbox Table + Publisher)
|
|
1190
|
+
|
|
1191
|
+
```java
|
|
1192
|
+
@Entity
|
|
1193
|
+
@Table(name = "outbox_events")
|
|
1194
|
+
public class OutboxEvent {
|
|
1195
|
+
@Id
|
|
1196
|
+
private String id;
|
|
1197
|
+
private String aggregateType;
|
|
1198
|
+
private String aggregateId;
|
|
1199
|
+
private String eventType;
|
|
1200
|
+
@Column(columnDefinition = "TEXT")
|
|
1201
|
+
private String payload; // JSON serializado del evento
|
|
1202
|
+
private boolean published = false;
|
|
1203
|
+
private LocalDateTime createdAt;
|
|
1204
|
+
private LocalDateTime publishedAt;
|
|
1205
|
+
}
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
```java
|
|
1209
|
+
// OutboxEventPublisher — proceso de polling (cada 5s via @Scheduled)
|
|
1210
|
+
@Component
|
|
1211
|
+
public class OutboxEventPublisher {
|
|
1212
|
+
@Scheduled(fixedDelay = 5000)
|
|
1213
|
+
@Transactional
|
|
1214
|
+
public void publishPendingEvents() {
|
|
1215
|
+
List<OutboxEvent> pending = outboxRepository.findByPublishedFalse();
|
|
1216
|
+
pending.forEach(event -> {
|
|
1217
|
+
messageBroker.publishRaw(event.getEventType(), event.getPayload());
|
|
1218
|
+
event.markPublished();
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
```
|
|
1223
|
+
|
|
1224
|
+
### Prerrequisito
|
|
1225
|
+
|
|
1226
|
+
Domain Events (ítem 1) implementados y funcionando — este ítem solo añade persistencia intermedia, no reemplaza la arquitectura existente.
|
|
1227
|
+
|
|
1228
|
+
---
|
|
1229
|
+
|
|
1230
|
+
## 🚀 PROTOTYPING
|
|
1231
|
+
|
|
1232
|
+
---
|
|
1233
|
+
|
|
1234
|
+
## 17. Mock Mode — `eva build --mock` ✅
|
|
1235
|
+
|
|
1236
|
+
### El Problema
|
|
1237
|
+
|
|
1238
|
+
Hoy el pipeline de eva4j tiene un salto demasiado grande entre **especificación** y **ejecución**:
|
|
1239
|
+
|
|
1240
|
+
```
|
|
1241
|
+
system.yaml + domain.yaml ──────────────────→ Código Java completo
|
|
1242
|
+
(requiere JVM, DB, Kafka, config)
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
Este salto crea un **cuello de botella** que afecta a todo el equipo:
|
|
1246
|
+
|
|
1247
|
+
1. **Frontend bloqueado** — Los desarrolladores de UI no pueden construir pantallas hasta que el backend esté implementado, testeado y desplegado. Con un sistema de 5+ módulos, esto significa semanas de espera.
|
|
1248
|
+
2. **Iteración lenta del diseño** — Cada cambio en el contrato API (agregar un campo, renombrar un endpoint, cambiar un flujo de estados) requiere regenerar código Java, compilar, levantar la base de datos y verificar. Un ciclo de retroalimentación de minutos cuando debería ser segundos.
|
|
1249
|
+
3. **Infraestructura innecesaria en fase de diseño** — Para probar si los contratos entre módulos tienen sentido, no se necesita PostgreSQL, Kafka ni Docker. Se necesita un servidor que responda con datos realistas y respete las transiciones de estado.
|
|
1250
|
+
4. **Desacoplamiento temporal frontend/backend** — El equipo de frontend y el equipo de backend deberían poder trabajar en paralelo desde el día uno, no secuencialmente.
|
|
1251
|
+
|
|
1252
|
+
### La Visión
|
|
1253
|
+
|
|
1254
|
+
Introducir un **paso intermedio de prototipado** en el pipeline de eva4j que permita levantar un servidor funcional completo **sin infraestructura real**, directamente desde las especificaciones YAML:
|
|
1255
|
+
|
|
1256
|
+
```
|
|
1257
|
+
┌─ eva build --mock ────────────────────────┐
|
|
1258
|
+
system.yaml │ Spring Boot + H2 + Spring Events │
|
|
1259
|
+
+ domain.yaml → │ Endpoints REST con Swagger UI │ ← Frontend trabaja aquí
|
|
1260
|
+
│ State machines reales │
|
|
1261
|
+
│ Eventos fluyendo entre módulos │
|
|
1262
|
+
│ Datos mock pre-cargados │
|
|
1263
|
+
└──────────────────────────────────────────┘
|
|
1264
|
+
↓ (cuando el diseño estabiliza)
|
|
1265
|
+
┌─ eva build ──────────────────────────────┐
|
|
1266
|
+
│ Spring Boot + PostgreSQL + Kafka │ ← Backend real
|
|
1267
|
+
└──────────────────────────────────────────┘
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
El mock mode genera **el mismo código de dominio** que la versión de producción — entidades, state machines, validaciones, use cases — pero reemplaza los adaptadores de infraestructura pesada (Kafka, PostgreSQL) por alternativas ligeras (Spring Events, H2). El 90% del código que corre en mock **es el mismo** que correrá en producción.
|
|
1271
|
+
|
|
1272
|
+
### Por Qué Funciona: La Arquitectura Hexagonal Ya Lo Permite
|
|
1273
|
+
|
|
1274
|
+
La clave de esta feature es que **eva4j ya genera código con puertos y adaptadores**. El dominio nunca depende de la infraestructura. La cadena de eventos actual:
|
|
1275
|
+
|
|
1276
|
+
```
|
|
1277
|
+
entity.raise(DomainEvent)
|
|
1278
|
+
↓
|
|
1279
|
+
RepositoryImpl.save() → eventPublisher.publishEvent() ← Spring interno (ya existe)
|
|
1280
|
+
↓
|
|
1281
|
+
DomainEventHandler @TransactionalEventListener(AFTER_COMMIT)
|
|
1282
|
+
↓
|
|
1283
|
+
messageBroker.publish*(IntegrationEvent) ← Puerto abstracto (ya existe)
|
|
1284
|
+
↓
|
|
1285
|
+
KafkaMessageBroker → kafkaTemplate.send() ← Adaptador Kafka
|
|
1286
|
+
```
|
|
1287
|
+
|
|
1288
|
+
Para mock mode solo se necesita **otro adaptador** que reimplemente el mismo puerto:
|
|
1289
|
+
|
|
1290
|
+
```
|
|
1291
|
+
messageBroker.publish*(IntegrationEvent)
|
|
1292
|
+
↓
|
|
1293
|
+
InMemoryMessageBroker → applicationEventPublisher.publishEvent() ← Bus de Spring
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
Y en el lado consumidor, en vez de `@KafkaListener`:
|
|
1297
|
+
|
|
1298
|
+
```
|
|
1299
|
+
@EventListener(condition = "#event.topic == 'BIKE_RESERVED'")
|
|
1300
|
+
public void handle(MockEvent event) {
|
|
1301
|
+
var payload = objectMapper.convertValue(event.data(), BikeReservedIntegrationEvent.class);
|
|
1302
|
+
useCaseMediator.dispatch(new InitiatePaymentCommand(...));
|
|
1303
|
+
}
|
|
1304
|
+
```
|
|
1305
|
+
|
|
1306
|
+
**Es un swap de adaptadores.** El dominio, los use cases, los mappers, los DTOs, las validaciones — todo es idéntico.
|
|
1307
|
+
|
|
1308
|
+
### Comando Propuesto
|
|
1309
|
+
|
|
1310
|
+
```bash
|
|
1311
|
+
eva build --mock # Genera proyecto mock desde system/
|
|
1312
|
+
eva build --mock --port 3000 # Puerto custom
|
|
1313
|
+
eva build --mock --seed 20 # 20 entidades por módulo
|
|
1314
|
+
eva build --mock --dir ./my-system # Directorio de specs custom
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
### Qué Genera el Comando
|
|
1318
|
+
|
|
1319
|
+
El comando orquesta la siguiente secuencia internamente:
|
|
1320
|
+
|
|
1321
|
+
```
|
|
1322
|
+
1. Lee system/system.yaml
|
|
1323
|
+
2. eva create {name} --database h2 → Proyecto Spring Boot con H2
|
|
1324
|
+
3. Para cada módulo:
|
|
1325
|
+
a. eva add module {name}
|
|
1326
|
+
b. Copia system/{module}.yaml → src/.../domain.yaml
|
|
1327
|
+
c. eva g entities {module} --broker inMemory → Genera InMemoryMessageBroker en vez de Kafka
|
|
1328
|
+
4. Para cada listener en los domain.yaml:
|
|
1329
|
+
→ Genera SpringEventListener en vez de KafkaListener
|
|
1330
|
+
5. Genera MockDataSeeder.java → Datos fake pre-cargados
|
|
1331
|
+
6. Genera application-mock.yaml → Profile Spring con H2 + config mock
|
|
1332
|
+
```
|
|
1333
|
+
|
|
1334
|
+
### Artefactos Nuevos (3 Templates + 1 Comando)
|
|
1335
|
+
|
|
1336
|
+
El esfuerzo es bajo porque la mayoría de artefactos **ya existen**. Solo se necesitan:
|
|
1337
|
+
|
|
1338
|
+
| Artefacto | Tipo | Propósito |
|
|
1339
|
+
|---|---|---|
|
|
1340
|
+
| `templates/mock/InMemoryMessageBroker.java.ejs` | Template | Adaptador mock que reemplaza `KafkaMessageBroker` |
|
|
1341
|
+
| `templates/mock/SpringEventListener.java.ejs` | Template | Listener que reemplaza `@KafkaListener` |
|
|
1342
|
+
| `templates/mock/MockDataSeeder.java.ejs` | Template | `CommandLineRunner` que siembra datos mock al arranque |
|
|
1343
|
+
| `templates/mock/application-mock.yaml.ejs` | Template | Profile Spring con H2 + configuración mock |
|
|
1344
|
+
| `src/commands/build-mock.js` | Comando | Orquestador del flujo completo |
|
|
1345
|
+
|
|
1346
|
+
### Inventario: Qué Ya Existe vs Qué Falta
|
|
1347
|
+
|
|
1348
|
+
| Pieza | ¿Existe? | Mock Mode |
|
|
1349
|
+
|---|---|---|
|
|
1350
|
+
| Proyecto Spring Boot | `eva create` | ✅ Con `database: h2` |
|
|
1351
|
+
| Módulos | `eva add module` | ✅ Sin cambios |
|
|
1352
|
+
| Entidades, dominio, state machines | `eva g entities` | ✅ Sin cambios |
|
|
1353
|
+
| Endpoints REST + Swagger UI | `eva g entities` con `endpoints:` | ✅ Sin cambios |
|
|
1354
|
+
| Validaciones JSR-303 | Ya generadas | ✅ Sin cambios |
|
|
1355
|
+
| `ApplicationEventPublisher` en RepositoryImpl | Ya genera `eventPublisher.publishEvent()` | ✅ Sin cambios |
|
|
1356
|
+
| `DomainEventHandler` (`@TransactionalEventListener`) | Ya genera mapping Domain → Integration Event | ✅ Sin cambios |
|
|
1357
|
+
| Puerto `MessageBroker` (interfaz) | Ya genera `application/ports/MessageBroker.java` | ✅ Sin cambios |
|
|
1358
|
+
| **`InMemoryMessageBroker`** (adaptador mock) | **No existe** | 🆕 1 template |
|
|
1359
|
+
| **`SpringEventListener`** (reemplaza `@KafkaListener`) | **No existe** | 🆕 1 template |
|
|
1360
|
+
| **`MockDataSeeder`** (`CommandLineRunner`) | **No existe** | 🆕 1 template |
|
|
1361
|
+
| **Comando orquestador** | **No existe** | 🆕 1 comando |
|
|
1362
|
+
|
|
1363
|
+
De 12 piezas, **8 ya existen**. Solo se necesitan 4 artefactos nuevos.
|
|
1364
|
+
|
|
1365
|
+
---
|
|
1366
|
+
|
|
1367
|
+
### Detalle de Templates
|
|
1368
|
+
|
|
1369
|
+
#### 1. `InMemoryMessageBroker.java.ejs`
|
|
1370
|
+
|
|
1371
|
+
Implementa la misma interfaz `MessageBroker` que `KafkaMessageBroker`, pero re-publica al bus interno de Spring:
|
|
1372
|
+
|
|
1373
|
+
```java
|
|
1374
|
+
@Component("{moduleCamelCase}InMemoryMessageBroker")
|
|
1375
|
+
public class {ModulePascal}InMemoryMessageBroker implements MessageBroker {
|
|
1376
|
+
|
|
1377
|
+
private final ApplicationEventPublisher eventPublisher;
|
|
1378
|
+
private final ObjectMapper objectMapper;
|
|
1379
|
+
|
|
1380
|
+
// constructor...
|
|
1381
|
+
|
|
1382
|
+
@Override
|
|
1383
|
+
public void publish{EventName}({EventName}IntegrationEvent event) {
|
|
1384
|
+
// Envuelve en MockEvent para routing cross-módulo
|
|
1385
|
+
Map<String, Object> payload = objectMapper.convertValue(event, Map.class);
|
|
1386
|
+
eventPublisher.publishEvent(new MockEvent("{TOPIC_NAME}", payload));
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
`MockEvent` es un record genérico en `shared`:
|
|
1392
|
+
|
|
1393
|
+
```java
|
|
1394
|
+
public record MockEvent(String topic, Map<String, Object> data) {}
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
#### 2. `SpringEventListener.java.ejs`
|
|
1398
|
+
|
|
1399
|
+
Reemplaza `@KafkaListener`. Escucha `MockEvent` del bus de Spring y filtra por topic:
|
|
1400
|
+
|
|
1401
|
+
```java
|
|
1402
|
+
@Component
|
|
1403
|
+
@Profile("mock")
|
|
1404
|
+
public class {EventName}SpringListener {
|
|
1405
|
+
|
|
1406
|
+
private final UseCaseMediator useCaseMediator;
|
|
1407
|
+
private final ObjectMapper objectMapper;
|
|
1408
|
+
|
|
1409
|
+
// constructor...
|
|
1410
|
+
|
|
1411
|
+
@EventListener(condition = "#event.topic() == '{TOPIC_NAME}'")
|
|
1412
|
+
public void handle(MockEvent event) {
|
|
1413
|
+
var payload = objectMapper.convertValue(
|
|
1414
|
+
event.data(), {EventName}IntegrationEvent.class
|
|
1415
|
+
);
|
|
1416
|
+
useCaseMediator.dispatch(new {UseCase}Command(
|
|
1417
|
+
// map event fields → command fields
|
|
1418
|
+
));
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
Cada módulo mantiene su propio `IntegrationEvent` record (ya generado por `listeners[]`). La deserialización con `objectMapper.convertValue()` replica el mismo patrón que el `KafkaListener` real — los módulos siguen aislados.
|
|
1424
|
+
|
|
1425
|
+
#### 3. `MockDataSeeder.java.ejs`
|
|
1426
|
+
|
|
1427
|
+
Un `CommandLineRunner` activo solo con profile `mock` que siembra datos realistas usando los **Commands reales** del sistema:
|
|
1428
|
+
|
|
1429
|
+
```java
|
|
1430
|
+
@Component
|
|
1431
|
+
public class MockDataSeeder implements CommandLineRunner {
|
|
1432
|
+
|
|
1433
|
+
private final UseCaseMediator mediator;
|
|
1434
|
+
|
|
1435
|
+
@Override
|
|
1436
|
+
public void run(String... args) {
|
|
1437
|
+
log.info("🌱 Mock data seeder started");
|
|
1438
|
+
|
|
1439
|
+
// Orden topológico derivado de references + integrations
|
|
1440
|
+
seedStations(5);
|
|
1441
|
+
seedBikes(15);
|
|
1442
|
+
seedAccounts(10);
|
|
1443
|
+
seedReservations(8);
|
|
1444
|
+
|
|
1445
|
+
log.info("✅ Mock data seeding complete");
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
private void seedAccounts(int count) {
|
|
1449
|
+
for (int i = 0; i < count; i++) {
|
|
1450
|
+
mediator.dispatch(new CreateAccountCommand(
|
|
1451
|
+
"user" + i + "@example.com", // email
|
|
1452
|
+
"User " + i, // fullName
|
|
1453
|
+
"+1-555-" + String.format("%04d", i) // phone
|
|
1454
|
+
));
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
// ... métodos por módulo
|
|
1458
|
+
}
|
|
1459
|
+
```
|
|
1460
|
+
|
|
1461
|
+
**Principio clave:** Al usar `mediator.dispatch()` con los Commands reales, **toda la cadena de eventos se activa automáticamente**. Crear una reserva dispara `BikeReservedEvent` → `payments.InitiatePayment` → `PaymentApprovedEvent` → `reservations.ConfirmReservation`. El seeder no necesita orquestar nada — los eventos fluyen solos vía el `InMemoryMessageBroker`.
|
|
1462
|
+
|
|
1463
|
+
### Generación de Datos Mock: Derivación desde domain.yaml
|
|
1464
|
+
|
|
1465
|
+
La metadata ya presente en los domain.yaml es suficiente para generar valores fake inteligentes:
|
|
1466
|
+
|
|
1467
|
+
| Señal en el YAML | Estrategia de generación |
|
|
1468
|
+
|---|---|
|
|
1469
|
+
| `type: String` + nombre contiene `email` | `"user{i}@example.com"` |
|
|
1470
|
+
| `type: String` + nombre contiene `name`/`fullName` | `"Name {i}"` |
|
|
1471
|
+
| `type: String` + nombre contiene `phone` | `"+1-555-{i:04d}"` |
|
|
1472
|
+
| `type: String` + nombre contiene `url` | `"https://example.com/{name}/{i}"` |
|
|
1473
|
+
| `type: String` + nombre contiene `code` + `@Column(unique)` | `"CODE-{i}"` (garantiza unicidad) |
|
|
1474
|
+
| `type: String` + `reference: { aggregate: X }` | **ID de una entidad X ya creada** (round-robin) |
|
|
1475
|
+
| `type: BigDecimal` + validación `Positive` | `new BigDecimal("{10 + i * 5}")` |
|
|
1476
|
+
| `type: Integer` + validación `Min(value: N)` | `N + i` |
|
|
1477
|
+
| `type: LocalDateTime` + nombre contiene `At`/`Time` | `LocalDateTime.now().plusHours({i})` |
|
|
1478
|
+
| `type: {EnumName}` (coincide con `enums[]`) | Primer valor del enum (o random pick) |
|
|
1479
|
+
| `type: {ValueObject}` | Generación recursiva por fields del VO |
|
|
1480
|
+
| `readOnly: true` | **Omitir** — el dominio lo asigna (initialValue, defaultValue) |
|
|
1481
|
+
| `hidden: true` | Generar valor (ej: token) — no es visible en response pero sí en create |
|
|
1482
|
+
|
|
1483
|
+
Esta lógica reutiliza y extiende la función `generateDummyValue()` que ya existe en `templates/postman/Collection.json.ejs` para la colección Postman.
|
|
1484
|
+
|
|
1485
|
+
### Orden de Seeding: Resolución Topológica
|
|
1486
|
+
|
|
1487
|
+
El `system.yaml` ya define el grafo de dependencias implícitamente. El seeder calcula el orden correcto:
|
|
1488
|
+
|
|
1489
|
+
1. **`reference:` en fields** — `reservations.userId → accounts` implica que `accounts` se siembra antes.
|
|
1490
|
+
2. **`integrations.async[]`** — `BikeCreatedEvent: fleet → reservations` implica que `fleet` se siembra antes.
|
|
1491
|
+
3. **`integrations.sync[]`** — `reservations calls accounts` confirma la dependencia.
|
|
1492
|
+
|
|
1493
|
+
Resultado para el sistema de bicicletas:
|
|
1494
|
+
|
|
1495
|
+
```
|
|
1496
|
+
1. fleet (stations, bikes) ← sin dependencias
|
|
1497
|
+
2. accounts (accounts) ← sin dependencias
|
|
1498
|
+
3. reservations (reservations) ← depende de fleet + accounts
|
|
1499
|
+
4. payments (se crea vía eventos) ← no necesita seed directo
|
|
1500
|
+
```
|
|
1501
|
+
|
|
1502
|
+
### Datos Declarativos Opcionales: `mock-data.yaml`
|
|
1503
|
+
|
|
1504
|
+
Para equipos que necesitan datos específicos (un usuario "demo", un producto con ID conocido):
|
|
1505
|
+
|
|
1506
|
+
```yaml
|
|
1507
|
+
# mock-data.yaml — opcional, overrides los datos auto-generados
|
|
1508
|
+
seed:
|
|
1509
|
+
accounts:
|
|
1510
|
+
count: 10 # 10 cuentas totales
|
|
1511
|
+
data: # las primeras se crean con datos explícitos
|
|
1512
|
+
- email: "demo@example.com"
|
|
1513
|
+
fullName: "Demo User"
|
|
1514
|
+
phone: "+1234567890"
|
|
1515
|
+
# las restantes 9 se generan automáticamente
|
|
1516
|
+
|
|
1517
|
+
fleet:
|
|
1518
|
+
stations:
|
|
1519
|
+
count: 5
|
|
1520
|
+
bikes:
|
|
1521
|
+
count: 15
|
|
1522
|
+
|
|
1523
|
+
reservations:
|
|
1524
|
+
count: 8
|
|
1525
|
+
```
|
|
1526
|
+
|
|
1527
|
+
### Comportamiento del Sistema en Mock Mode
|
|
1528
|
+
|
|
1529
|
+
El frontend dev ejecuta `eva build --mock`, espera la compilación, y obtiene:
|
|
1530
|
+
|
|
1531
|
+
```
|
|
1532
|
+
$ cd generated-project && ./gradlew bootRun --args='--spring.profiles.active=mock'
|
|
1533
|
+
|
|
1534
|
+
🚀 test-eva started on port 8080 (profile: mock)
|
|
1535
|
+
|
|
1536
|
+
Modules: reservations, fleet, accounts, payments, notifications
|
|
1537
|
+
Database: H2 (in-memory, ddl-auto: create-drop)
|
|
1538
|
+
Events: Spring ApplicationEventPublisher (in-process)
|
|
1539
|
+
Swagger: http://localhost:8080/swagger-ui.html
|
|
1540
|
+
|
|
1541
|
+
🌱 Mock data seeded:
|
|
1542
|
+
5 stations, 15 bikes, 10 accounts, 8 reservations
|
|
1543
|
+
Events propagated: 8 BikeReserved → 8 PaymentApproved → 8 ReservationConfirmed
|
|
1544
|
+
```
|
|
1545
|
+
|
|
1546
|
+
#### El flujo completo que ve un frontend dev
|
|
1547
|
+
|
|
1548
|
+
```
|
|
1549
|
+
Frontend: POST /reservations { userId, bikeId, stationId, amount, scheduledPickupTime }
|
|
1550
|
+
↓
|
|
1551
|
+
Mock: crea reserva (status: PENDING_PAYMENT) → 201 Created
|
|
1552
|
+
↓ raise(BikeReservedEvent)
|
|
1553
|
+
↓ InMemoryMessageBroker → publishEvent(MockEvent("BIKE_RESERVED", payload))
|
|
1554
|
+
↓ SpringEventListener en payments → crea Payment (PENDING)
|
|
1555
|
+
↓ handler auto-aprueba → raise(PaymentApprovedEvent)
|
|
1556
|
+
↓ InMemoryMessageBroker → publishEvent(MockEvent("PAYMENT_APPROVED", payload))
|
|
1557
|
+
↓ SpringEventListener en reservations → confirm() → status: CONFIRMED
|
|
1558
|
+
|
|
1559
|
+
Frontend: GET /reservations/{id}
|
|
1560
|
+
→ { status: "CONFIRMED", ... } ← cambió automáticamente
|
|
1561
|
+
|
|
1562
|
+
Frontend: PUT /reservations/{id}/pickup
|
|
1563
|
+
→ { status: "IN_PROGRESS", ... }
|
|
1564
|
+
↓ state machine valida: CONFIRMED → IN_PROGRESS ✅
|
|
1565
|
+
|
|
1566
|
+
Frontend: PUT /reservations/{id}/return
|
|
1567
|
+
→ { status: "COMPLETED", ... }
|
|
1568
|
+
↓ raise(TripCompletedEvent) → accounts.HandleTripCompleted
|
|
1569
|
+
```
|
|
1570
|
+
|
|
1571
|
+
El frontend dev ve **exactamente el mismo comportamiento** que tendría el sistema real, incluyendo transiciones de estado asíncronas y reacciones cross-módulo.
|
|
1572
|
+
|
|
1573
|
+
### Transición de Mock a Producción
|
|
1574
|
+
|
|
1575
|
+
Cambiar de mock a producción **no requiere regenerar código de dominio**. Solo:
|
|
1576
|
+
|
|
1577
|
+
1. `system.yaml` → `database: postgresql` (en vez de `h2`)
|
|
1578
|
+
2. `eva add kafka-client` → instala adaptadores Kafka reales
|
|
1579
|
+
3. `eva g entities {module}` → regenera adaptadores (ya usa `--broker kafka` por defecto)
|
|
1580
|
+
4. Configurar `application-production.yaml` con URLs reales
|
|
1581
|
+
|
|
1582
|
+
El dominio, los use cases, los mappers, los DTOs, las validaciones — **todo permanece idéntico**.
|
|
1583
|
+
|
|
1584
|
+
### Dependencias Adicionales
|
|
1585
|
+
|
|
1586
|
+
Solo una librería nueva (opcional, para datos más realistas):
|
|
1587
|
+
|
|
1588
|
+
```gradle
|
|
1589
|
+
// build.gradle — solo en profile mock
|
|
1590
|
+
runtimeOnly 'net.datafaker:datafaker:2.4.2' // Sucesor de JavaFaker, mantenido activamente
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1593
|
+
Sin DataFaker, el seeder genera datos con patrones simples (`"User 0"`, `"user0@example.com"`). Con DataFaker, genera nombres, emails, teléfonos y direcciones realistas en cualquier locale.
|
|
1594
|
+
|
|
1595
|
+
### Estado
|
|
1596
|
+
|
|
1597
|
+
⏳ Pendiente de implementación
|
|
1598
|
+
|
|
1599
|
+
---
|
|
1600
|
+
|
|
1601
|
+
## Resumen de Prioridades
|
|
1602
|
+
|
|
1603
|
+
| # | Característica | Prioridad | Complejidad | Estado |
|
|
1604
|
+
|---|---|---|---|---|
|
|
1605
|
+
| 1 | Domain Events | Alta | Alta | ✅ Implementado |
|
|
1606
|
+
| 2 | Aggregate Boundaries por ID | Alta | Media | ✅ Implementado |
|
|
1607
|
+
| 3 | Soft Delete Completo | Alta | Baja | ✅ Implementado |
|
|
1608
|
+
| 4 | Paginación en Queries | Impl. | -- | ✅ Implementado |
|
|
1609
|
+
| 5 | Optimistic Locking | Media | Baja | Pendiente |
|
|
1610
|
+
| 6 | Read Models / Proyecciones | Media | Alta | Pendiente |
|
|
1611
|
+
| 7 | Enums con Transiciones | Impl. | -- | ✅ Implementado |
|
|
1612
|
+
| 8 | Specifications Pattern | Media | Media | Pendiente |
|
|
1613
|
+
| 9 | JSON Schema para domain.yaml | Tooling | Media | Pendiente |
|
|
1614
|
+
| 10 | Generacion Incremental | Tooling | -- | ✅ Implementado |
|
|
1615
|
+
| 11 | eva4j doctor | Tooling | Media | Pendiente |
|
|
1616
|
+
| 12 | Tests Completos | Tooling | Media | Pendiente |
|
|
1617
|
+
| 13 | Auditoria completa | Impl. | -- | ✅ Implementado |
|
|
1618
|
+
| 14 | Validaciones JSR-303 | Impl. | -- | ✅ Implementado |
|
|
1619
|
+
| 15 | Transactional Outbox Pattern | Alta | Alta | Pendiente |
|
|
1620
|
+
| 16 | `defaultValue` para campos `readOnly` | Impl. | -- | ✅ Implementado |
|
|
1621
|
+
| 17 | Mock Mode (`eva build --mock`) | Alta | Media | ✅ Implementado |
|
|
1622
|
+
|
|
1623
|
+
---
|
|
1624
|
+
|
|
1625
|
+
**Ultima actualizacion:** 2026-03-13
|
|
1626
|
+
**Version de eva4j:** 1.x
|
|
1627
|
+
**Estado:** Documento de planificacion y referencia
|