eva4j 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/AGENTS.md +314 -10
  2. package/COMMAND_EVALUATION.md +15 -16
  3. package/DOMAIN_YAML_GUIDE.md +576 -10
  4. package/FUTURE_FEATURES.md +1627 -1168
  5. package/README.md +318 -13
  6. package/bin/eva4j.js +34 -0
  7. package/config/defaults.json +1 -0
  8. package/design-system.md +797 -0
  9. package/docs/commands/EVALUATE_SYSTEM.md +994 -0
  10. package/docs/commands/GENERATE_ENTITIES.md +795 -6
  11. package/docs/commands/INDEX.md +10 -1
  12. package/examples/domain-endpoints-relations.yaml +353 -0
  13. package/examples/domain-endpoints-versioned.yaml +144 -0
  14. package/examples/domain-endpoints.yaml +135 -0
  15. package/examples/domain-events.yaml +166 -20
  16. package/examples/domain-listeners.yaml +212 -0
  17. package/examples/domain-one-to-many.yaml +1 -0
  18. package/examples/domain-one-to-one.yaml +1 -0
  19. package/examples/domain-ports.yaml +414 -0
  20. package/examples/domain-soft-delete.yaml +47 -44
  21. package/examples/system/notification.yaml +147 -0
  22. package/examples/system/product.yaml +185 -0
  23. package/examples/system/system.yaml +112 -0
  24. package/examples/system-report.html +971 -0
  25. package/examples/system.yaml +332 -0
  26. package/package.json +2 -1
  27. package/src/commands/build.js +714 -0
  28. package/src/commands/create.js +7 -3
  29. package/src/commands/detach.js +1 -0
  30. package/src/commands/evaluate-system.js +610 -0
  31. package/src/commands/generate-entities.js +1331 -49
  32. package/src/commands/generate-http-exchange.js +2 -0
  33. package/src/commands/generate-kafka-event.js +98 -11
  34. package/src/generators/base-generator.js +8 -1
  35. package/src/generators/postman-generator.js +188 -0
  36. package/src/generators/shared-generator.js +10 -0
  37. package/src/utils/config-manager.js +54 -0
  38. package/src/utils/context-builder.js +1 -0
  39. package/src/utils/domain-diagram.js +192 -0
  40. package/src/utils/domain-validator.js +970 -0
  41. package/src/utils/fake-data.js +376 -0
  42. package/src/utils/naming.js +3 -2
  43. package/src/utils/system-validator.js +434 -0
  44. package/src/utils/yaml-to-entity.js +302 -8
  45. package/templates/aggregate/AggregateMapper.java.ejs +3 -2
  46. package/templates/aggregate/AggregateRepository.java.ejs +8 -2
  47. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +13 -3
  48. package/templates/aggregate/AggregateRoot.java.ejs +60 -2
  49. package/templates/aggregate/DomainEventHandler.java.ejs +27 -20
  50. package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
  51. package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
  52. package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
  53. package/templates/aggregate/JpaRepository.java.ejs +5 -0
  54. package/templates/base/gradle/build.gradle.ejs +3 -2
  55. package/templates/base/root/AGENTS.md.ejs +306 -45
  56. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1663 -0
  57. package/templates/base/root/skill-build-system-yaml.ejs +1446 -0
  58. package/templates/base/root/system.yaml.ejs +97 -0
  59. package/templates/crud/ApplicationMapper.java.ejs +4 -0
  60. package/templates/crud/Controller.java.ejs +4 -4
  61. package/templates/crud/CreateCommand.java.ejs +4 -0
  62. package/templates/crud/CreateItemDto.java.ejs +4 -0
  63. package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
  64. package/templates/crud/DeleteCommandHandler.java.ejs +10 -2
  65. package/templates/crud/EndpointsController.java.ejs +178 -0
  66. package/templates/crud/FindByQuery.java.ejs +17 -0
  67. package/templates/crud/FindByQueryHandler.java.ejs +57 -0
  68. package/templates/crud/ListQuery.java.ejs +1 -1
  69. package/templates/crud/ListQueryHandler.java.ejs +8 -8
  70. package/templates/crud/ScaffoldCommand.java.ejs +12 -0
  71. package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
  72. package/templates/crud/ScaffoldQuery.java.ejs +13 -0
  73. package/templates/crud/ScaffoldQueryHandler.java.ejs +41 -0
  74. package/templates/crud/SubEntityAddCommand.java.ejs +21 -0
  75. package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
  76. package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
  77. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
  78. package/templates/crud/TransitionCommand.java.ejs +9 -0
  79. package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
  80. package/templates/crud/UpdateCommand.java.ejs +4 -0
  81. package/templates/evaluate/report.html.ejs +1363 -0
  82. package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
  83. package/templates/kafka-event/Event.java.ejs +16 -0
  84. package/templates/kafka-listener/KafkaController.java.ejs +1 -1
  85. package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
  86. package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
  87. package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
  88. package/templates/kafka-listener/ListenerCommandHandler.java.ejs +23 -0
  89. package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
  90. package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
  91. package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
  92. package/templates/mock/MockEvent.java.ejs +10 -0
  93. package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
  94. package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
  95. package/templates/mock/SpringEventListener.java.ejs +61 -0
  96. package/templates/ports/PortDomainModel.java.ejs +35 -0
  97. package/templates/ports/PortFeignAdapter.java.ejs +67 -0
  98. package/templates/ports/PortFeignClient.java.ejs +45 -0
  99. package/templates/ports/PortFeignConfig.java.ejs +24 -0
  100. package/templates/ports/PortInterface.java.ejs +45 -0
  101. package/templates/ports/PortNestedType.java.ejs +28 -0
  102. package/templates/ports/PortRequestDto.java.ejs +30 -0
  103. package/templates/ports/PortResponseDto.java.ejs +28 -0
  104. package/templates/postman/Collection.json.ejs +1 -1
  105. package/templates/postman/UnifiedCollection.json.ejs +185 -0
  106. package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
@@ -0,0 +1,376 @@
1
+ 'use strict';
2
+
3
+ const { faker } = require('@faker-js/faker');
4
+
5
+ // Set locale to English (US)
6
+ faker.locale = 'en_US';
7
+
8
+ // Deterministic seed for reproducible output
9
+ const DEFAULT_SEED = 42;
10
+
11
+ /**
12
+ * Initialize faker with a seed for reproducibility.
13
+ * @param {number} seed
14
+ */
15
+ function initSeed(seed = DEFAULT_SEED) {
16
+ faker.seed(seed);
17
+ }
18
+
19
+ // ── Field-name heuristics (order matters — first match wins) ────────────────
20
+
21
+ const NAME_HEURISTICS = [
22
+ // Identity / person
23
+ { pattern: /^(first[-_]?name)$/i, gen: () => faker.person.firstName() },
24
+ { pattern: /^(last[-_]?name|surname)$/i, gen: () => faker.person.lastName() },
25
+ { pattern: /^(full[-_]?name|customer[-_]?name|user[-_]?name|recipient[-_]?name|author[-_]?name|owner[-_]?name)$/i, gen: () => faker.person.fullName() },
26
+ { pattern: /^(username|login|nick[-_]?name)$/i, gen: () => faker.internet.username() },
27
+ { pattern: /^(name)$/i, gen: () => faker.commerce.productName() },
28
+
29
+ // Contact
30
+ { pattern: /email/i, gen: () => faker.internet.email() },
31
+ { pattern: /^(phone|telephone|mobile|cell|fax)/i, gen: () => faker.phone.number({ style: 'international' }) },
32
+
33
+ // Location / address
34
+ { pattern: /^(street|street[-_]?address)$/i, gen: () => faker.location.streetAddress() },
35
+ { pattern: /^(city|town)$/i, gen: () => faker.location.city() },
36
+ { pattern: /^(state|province|region)$/i, gen: () => faker.location.state() },
37
+ { pattern: /^(zip[-_]?code|postal[-_]?code)$/i, gen: () => faker.location.zipCode() },
38
+ { pattern: /^(country)$/i, gen: () => faker.location.country() },
39
+ { pattern: /^(address)$/i, gen: () => faker.location.streetAddress(true) },
40
+ { pattern: /^(latitude|lat)$/i, gen: () => faker.location.latitude() },
41
+ { pattern: /^(longitude|lng|lon)$/i, gen: () => faker.location.longitude() },
42
+
43
+ // Financial / commerce
44
+ { pattern: /^(price|amount|total|subtotal|tax|discount|balance|cost|fee|salary|income)$/i, gen: () => faker.commerce.price({ min: 5, max: 500 }) },
45
+ { pattern: /^(currency)$/i, gen: () => faker.finance.currencyCode() },
46
+ { pattern: /^(iban)$/i, gen: () => faker.finance.iban() },
47
+ { pattern: /^(credit[-_]?card|card[-_]?number)$/i, gen: () => faker.finance.creditCardNumber() },
48
+
49
+ // Text / content
50
+ { pattern: /^(title|subject|headline)$/i, gen: () => faker.commerce.productName() + ' ' + faker.word.noun() },
51
+ { pattern: /^(description|summary|bio|about|notes|comment|message|body|content)$/i, gen: () => faker.commerce.productDescription() },
52
+ { pattern: /^(slug)$/i, gen: () => faker.helpers.slugify(faker.commerce.productName()).toLowerCase() },
53
+
54
+ // Web
55
+ { pattern: /^(url|website|homepage|link|image[-_]?url|avatar[-_]?url|photo[-_]?url|logo[-_]?url)$/i, gen: () => faker.internet.url() },
56
+ { pattern: /^(ip|ip[-_]?address)$/i, gen: () => faker.internet.ip() },
57
+ { pattern: /^(domain|host)$/i, gen: () => faker.internet.domainName() },
58
+
59
+ // Identifiers / codes
60
+ { pattern: /^(order[-_]?number|invoice[-_]?number|reference[-_]?number|tracking[-_]?number)$/i, gen: () => `ORD-${faker.string.alphanumeric(6).toUpperCase()}` },
61
+ { pattern: /^(sku|product[-_]?code|item[-_]?code|barcode)$/i, gen: () => faker.string.alphanumeric(8).toUpperCase() },
62
+ { pattern: /^(code|token|key)$/i, gen: () => faker.string.alphanumeric(10).toUpperCase() },
63
+
64
+ // Company
65
+ { pattern: /^(company|company[-_]?name|organization|brand)$/i, gen: () => faker.company.name() },
66
+ { pattern: /^(department)$/i, gen: () => faker.commerce.department() },
67
+ { pattern: /^(job[-_]?title|position|role)$/i, gen: () => faker.person.jobTitle() },
68
+
69
+ // Dates (as strings for JSON)
70
+ { pattern: /^(birth[-_]?date|date[-_]?of[-_]?birth|dob)$/i, gen: () => faker.date.birthdate({ min: 18, max: 65, mode: 'age' }).toISOString().split('T')[0] },
71
+
72
+ // Quantity / counts
73
+ { pattern: /^(quantity|qty|count|stock|units|capacity|size|weight|height|width|length|duration|age|rating|score|priority|order|rank|level|version)$/i, gen: () => faker.number.int({ min: 1, max: 100 }) },
74
+
75
+ // Boolean semantic
76
+ { pattern: /^(is[-_]?active|active|enabled|verified|published|available|visible|approved|completed|deleted|archived)$/i, gen: () => faker.datatype.boolean() },
77
+
78
+ // Color
79
+ { pattern: /^(color|colour)$/i, gen: () => faker.color.human() },
80
+
81
+ // Catch-all for *Id fields that reference another aggregate
82
+ { pattern: /Id$/, gen: () => faker.string.uuid() },
83
+ ];
84
+
85
+ // ── Type-based fallbacks ────────────────────────────────────────────────────
86
+
87
+ const TYPE_GENERATORS = {
88
+ 'String': () => faker.word.words(3),
89
+ 'Integer': () => faker.number.int({ min: 1, max: 1000 }),
90
+ 'int': () => faker.number.int({ min: 1, max: 1000 }),
91
+ 'Long': () => faker.number.int({ min: 10000, max: 99999 }),
92
+ 'long': () => faker.number.int({ min: 10000, max: 99999 }),
93
+ 'Double': () => parseFloat(faker.finance.amount({ min: 1, max: 999, dec: 2 })),
94
+ 'double': () => parseFloat(faker.finance.amount({ min: 1, max: 999, dec: 2 })),
95
+ 'Float': () => parseFloat(faker.finance.amount({ min: 1, max: 999, dec: 2 })),
96
+ 'float': () => parseFloat(faker.finance.amount({ min: 1, max: 999, dec: 2 })),
97
+ 'BigDecimal': () => faker.commerce.price({ min: 5, max: 500 }),
98
+ 'Boolean': () => faker.datatype.boolean(),
99
+ 'boolean': () => faker.datatype.boolean(),
100
+ 'LocalDate': () => faker.date.recent({ days: 30 }).toISOString().split('T')[0],
101
+ 'LocalDateTime': () => faker.date.recent({ days: 30 }).toISOString().replace('Z', ''),
102
+ 'LocalTime': () => faker.date.recent().toISOString().split('T')[1].substring(0, 8),
103
+ 'Instant': () => faker.date.recent({ days: 30 }).toISOString(),
104
+ 'UUID': () => faker.string.uuid(),
105
+ };
106
+
107
+ // ── Validation-aware overrides ──────────────────────────────────────────────
108
+
109
+ function applyValidationOverrides(field, baseValue) {
110
+ const annotations = field.validationAnnotations || [];
111
+ if (annotations.length === 0) return baseValue;
112
+
113
+ let value = baseValue;
114
+
115
+ for (const ann of annotations) {
116
+ // @Email — always override to guarantee valid email format
117
+ if (/@Email/i.test(ann)) return faker.internet.email();
118
+
119
+ // @Future / @Past — always override to guarantee temporal constraint
120
+ if (/@Future/i.test(ann)) return faker.date.future({ years: 1 }).toISOString().replace('Z', '');
121
+ if (/@Past/i.test(ann)) return faker.date.past({ years: 2 }).toISOString().replace('Z', '');
122
+
123
+ // @Pattern — always override (heuristic can't know regex)
124
+ const patternMatch = ann.match(/@Pattern\s*\(.*?regexp\s*=\s*"([^"]+)"/);
125
+ if (patternMatch) {
126
+ const re = patternMatch[1];
127
+ if (/^\^\[0-9\]|^\^\\d/.test(re)) return faker.string.numeric(6);
128
+ if (/^\^\[A-Z\]/.test(re)) return faker.string.alpha({ length: 8, casing: 'upper' });
129
+ return faker.string.alphanumeric(8);
130
+ }
131
+
132
+ // @NotBlank / @NotNull / @NotEmpty — ensure non-empty; keep base if already non-empty
133
+ if (/@NotBlank|@NotEmpty|@NotNull/i.test(ann)) {
134
+ if (value === null || value === undefined || value === '') {
135
+ value = `example_${field.name}`;
136
+ }
137
+ continue; // don't return — check other annotations too
138
+ }
139
+
140
+ // @Positive / @PositiveOrZero / @Negative — constrain if base is numeric, else override
141
+ if (/@PositiveOrZero/i.test(ann)) {
142
+ if (typeof value === 'number') { value = Math.abs(value); }
143
+ else { value = faker.number.int({ min: 0, max: 500 }); }
144
+ continue;
145
+ }
146
+ if (/@Positive/i.test(ann)) {
147
+ if (typeof value === 'number') { value = Math.max(1, Math.abs(value)); }
148
+ else if (typeof value === 'string' && !isNaN(parseFloat(value))) {
149
+ const n = Math.max(1, Math.abs(parseFloat(value)));
150
+ value = String(n);
151
+ } else { value = faker.number.int({ min: 1, max: 500 }); }
152
+ continue;
153
+ }
154
+ if (/@Negative/i.test(ann)) {
155
+ if (typeof value === 'number') { value = -Math.abs(value || 1); }
156
+ else { value = faker.number.int({ min: -500, max: -1 }); }
157
+ continue;
158
+ }
159
+
160
+ // @Min(n) — if base is numeric and below min, adjust; otherwise generate
161
+ const minMatch = ann.match(/@Min\s*\(\s*(?:value\s*=\s*)?(\d+)/);
162
+ if (minMatch) {
163
+ const min = parseInt(minMatch[1], 10);
164
+ if (typeof value === 'number' && value < min) { value = faker.number.int({ min, max: min + 100 }); }
165
+ else if (typeof value !== 'number') { value = faker.number.int({ min, max: min + 100 }); }
166
+ continue;
167
+ }
168
+
169
+ // @Max(n) — if base is numeric and above max, adjust; otherwise generate
170
+ const maxMatch = ann.match(/@Max\s*\(\s*(?:value\s*=\s*)?(\d+)/);
171
+ if (maxMatch) {
172
+ const max = parseInt(maxMatch[1], 10);
173
+ if (typeof value === 'number' && value > max) { value = faker.number.int({ min: 0, max }); }
174
+ else if (typeof value !== 'number') { value = faker.number.int({ min: 0, max }); }
175
+ continue;
176
+ }
177
+
178
+ // @Size(min, max) — constrain string length; keep base if fits, else truncate/pad
179
+ const sizeMatch = ann.match(/@Size\s*\(.*?min\s*=\s*(\d+)(?:.*?max\s*=\s*(\d+))?/);
180
+ if (sizeMatch) {
181
+ const min = parseInt(sizeMatch[1], 10);
182
+ const max = sizeMatch[2] ? parseInt(sizeMatch[2], 10) : 255;
183
+ if (typeof value === 'string') {
184
+ if (value.length < min) {
185
+ // Pad to min length
186
+ value = value + faker.string.alpha(min - value.length);
187
+ } else if (value.length > max) {
188
+ // Truncate to max length
189
+ value = value.substring(0, max);
190
+ }
191
+ // else: value length is within range — keep it
192
+ } else {
193
+ value = faker.string.alpha({ length: { min: Math.max(min, 1), max: Math.min(max, 50) } });
194
+ }
195
+ continue;
196
+ }
197
+
198
+ // @Digits(integer, fraction) — format as decimal with given precision
199
+ const digitsMatch = ann.match(/@Digits\s*\(\s*integer\s*=\s*(\d+)(?:.*?fraction\s*=\s*(\d+))?/);
200
+ if (digitsMatch) {
201
+ const intPart = parseInt(digitsMatch[1], 10);
202
+ const fracPart = digitsMatch[2] ? parseInt(digitsMatch[2], 10) : 0;
203
+ const maxInt = Math.pow(10, Math.min(intPart, 6)) - 1;
204
+ const num = faker.number.float({ min: 1, max: maxInt, fractionDigits: fracPart });
205
+ value = fracPart > 0 ? num.toFixed(fracPart) : String(Math.floor(num));
206
+ continue;
207
+ }
208
+ }
209
+
210
+ return value;
211
+ }
212
+
213
+ // ── Core generation ─────────────────────────────────────────────────────────
214
+
215
+ /**
216
+ * Generate a realistic fake value for a domain field.
217
+ *
218
+ * @param {Object} field - Parsed field object from yaml-to-entity
219
+ * @param {Array} allEnums - All enum definitions available in the module
220
+ * @param {Array} valueObjects - All value object definitions in the aggregate
221
+ * @param {number} depth - Current recursion depth (for nested VOs)
222
+ * @returns {*} A JSON-compatible fake value
223
+ */
224
+ function generateFakeValue(field, allEnums = [], valueObjects = [], depth = 0) {
225
+ const MAX_DEPTH = 3;
226
+ if (depth >= MAX_DEPTH) return null;
227
+
228
+ // 1. Enum → random pick from values
229
+ if (field.isEnum) {
230
+ const enumDef = allEnums.find(e => e.name === field.javaType);
231
+ if (enumDef && enumDef.values && enumDef.values.length > 0) {
232
+ return faker.helpers.arrayElement(enumDef.values);
233
+ }
234
+ return 'EXAMPLE_VALUE';
235
+ }
236
+
237
+ // 2. Value Objects → recursively generate nested fields
238
+ if (field.isValueObject) {
239
+ const voDef = valueObjects.find(vo => vo.name === field.javaType);
240
+ if (voDef && voDef.fields) {
241
+ const obj = {};
242
+ voDef.fields.forEach(voField => {
243
+ obj[voField.name] = generateFakeValue(voField, allEnums, valueObjects, depth + 1);
244
+ });
245
+ return obj;
246
+ }
247
+ // Fallback: heuristic by VO name
248
+ return generateValueObjectFallback(field.javaType, field.name);
249
+ }
250
+
251
+ // 3. Collections
252
+ if (field.isCollection) {
253
+ return [faker.word.noun(), faker.word.noun()];
254
+ }
255
+
256
+ // 4. Field-name heuristic
257
+ for (const rule of NAME_HEURISTICS) {
258
+ if (rule.pattern.test(field.name)) {
259
+ const val = rule.gen();
260
+ return applyValidationOverrides(field, val);
261
+ }
262
+ }
263
+
264
+ // 5. Type-based fallback
265
+ const typeGen = TYPE_GENERATORS[field.javaType];
266
+ if (typeGen) {
267
+ return applyValidationOverrides(field, typeGen());
268
+ }
269
+
270
+ // 6. Absolute fallback
271
+ return applyValidationOverrides(field, `example_${field.name}`);
272
+ }
273
+
274
+ /**
275
+ * Fallback for value objects when no definition is found.
276
+ */
277
+ function generateValueObjectFallback(typeName, fieldName) {
278
+ const name = typeName.toLowerCase();
279
+ if (name.includes('money') || name.includes('price') || name.includes('amount')) {
280
+ return { amount: faker.commerce.price({ min: 5, max: 500 }), currency: faker.finance.currencyCode() };
281
+ }
282
+ if (name.includes('address')) {
283
+ return { street: faker.location.streetAddress(), city: faker.location.city(), state: faker.location.state(), zipCode: faker.location.zipCode(), country: faker.location.country() };
284
+ }
285
+ if (name.includes('email')) {
286
+ return { value: faker.internet.email() };
287
+ }
288
+ if (name.includes('phone')) {
289
+ return { value: faker.phone.number({ style: 'international' }) };
290
+ }
291
+ return { value: faker.word.words(2) };
292
+ }
293
+
294
+ /**
295
+ * Generate a full request body for an aggregate's command fields.
296
+ *
297
+ * @param {Array} commandFields - Writable fields (exclude id, audit, readOnly)
298
+ * @param {Array} oneToManyRelationships - Enriched relationships from enrichRelationshipsRecursively
299
+ * @param {Array} allEnums - Module enums
300
+ * @param {Array} valueObjects - Aggregate value objects
301
+ * @returns {Object} A JSON-serialisable body object
302
+ */
303
+ function generateFakeBody(commandFields, oneToManyRelationships = [], allEnums = [], valueObjects = []) {
304
+ const body = {};
305
+
306
+ commandFields.forEach(field => {
307
+ body[field.name] = generateFakeValue(field, allEnums, valueObjects);
308
+ });
309
+
310
+ if (oneToManyRelationships && oneToManyRelationships.length > 0) {
311
+ oneToManyRelationships.forEach(rel => {
312
+ const obj = generateFakeNestedObject(rel, allEnums, valueObjects, 0);
313
+ if (obj) {
314
+ body[rel.fieldName] = [obj];
315
+ }
316
+ });
317
+ }
318
+
319
+ return body;
320
+ }
321
+
322
+ /**
323
+ * Generate a fake object for a nested relationship.
324
+ */
325
+ function generateFakeNestedObject(rel, allEnums, valueObjects, depth) {
326
+ const MAX_DEPTH = 3;
327
+ if (depth >= MAX_DEPTH) return null;
328
+
329
+ const obj = {};
330
+
331
+ if (rel.fields && rel.fields.length > 0) {
332
+ rel.fields.filter(f => !f.readOnly && f.name !== 'id').forEach(field => {
333
+ obj[field.name] = generateFakeValue(field, allEnums, valueObjects, depth + 1);
334
+ });
335
+ }
336
+
337
+ // Recurse into nested OneToMany
338
+ if (rel.hasNestedRelationships && rel.nestedRelationships) {
339
+ rel.nestedRelationships.forEach(nestedRel => {
340
+ const nestedObj = generateFakeNestedObject(nestedRel, allEnums, valueObjects, depth + 1);
341
+ if (nestedObj) {
342
+ obj[nestedRel.fieldName] = [nestedObj];
343
+ }
344
+ });
345
+ }
346
+
347
+ // Nested OneToOne
348
+ if (rel.nestedOneToOneRelationships && rel.nestedOneToOneRelationships.length > 0) {
349
+ rel.nestedOneToOneRelationships.forEach(otoRel => {
350
+ const otoObj = {};
351
+ (otoRel.fields || []).filter(f => !f.readOnly && f.name !== 'id').forEach(field => {
352
+ otoObj[field.name] = generateFakeValue(field, allEnums, valueObjects, depth + 1);
353
+ });
354
+ obj[otoRel.fieldName] = otoObj;
355
+ });
356
+ }
357
+
358
+ return obj;
359
+ }
360
+
361
+ /**
362
+ * Generate example ID based on the id-field type.
363
+ */
364
+ function generateFakeId(idType) {
365
+ if (idType === 'UUID') return faker.string.uuid();
366
+ if (idType === 'Long' || idType === 'Integer') return String(faker.number.int({ min: 1, max: 999 }));
367
+ return faker.string.uuid();
368
+ }
369
+
370
+ module.exports = {
371
+ initSeed,
372
+ generateFakeValue,
373
+ generateFakeBody,
374
+ generateFakeNestedObject,
375
+ generateFakeId,
376
+ };
@@ -84,12 +84,13 @@ function getBaseEntity(hasSoftDelete, hasAudit) {
84
84
  /**
85
85
  * Convert artifact ID to valid Java package name
86
86
  * @param {string} artifactId - Artifact ID (e.g., my-project)
87
- * @returns {string} Valid package name (e.g., myproject)
87
+ * @returns {string} Valid package name (e.g., my_project)
88
88
  */
89
89
  function artifactIdToPackageName(artifactId) {
90
90
  return artifactId
91
91
  .toLowerCase()
92
- .replace(/[^a-z0-9]/g, '');
92
+ .replace(/-/g, '_')
93
+ .replace(/[^a-z0-9_]/g, '');
93
94
  }
94
95
 
95
96
  /**