context-mapper-mcp 1.0.0
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/LICENSE +21 -0
- package/README.md +238 -0
- package/dist/generators/plantuml.d.ts +17 -0
- package/dist/generators/plantuml.d.ts.map +1 -0
- package/dist/generators/plantuml.js +244 -0
- package/dist/generators/plantuml.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +897 -0
- package/dist/index.js.map +1 -0
- package/dist/model/parser.d.ts +51 -0
- package/dist/model/parser.d.ts.map +1 -0
- package/dist/model/parser.js +934 -0
- package/dist/model/parser.js.map +1 -0
- package/dist/model/types.d.ts +121 -0
- package/dist/model/types.d.ts.map +1 -0
- package/dist/model/types.js +12 -0
- package/dist/model/types.js.map +1 -0
- package/dist/model/validation.d.ts +36 -0
- package/dist/model/validation.d.ts.map +1 -0
- package/dist/model/validation.js +411 -0
- package/dist/model/validation.js.map +1 -0
- package/dist/model/writer.d.ts +30 -0
- package/dist/model/writer.d.ts.map +1 -0
- package/dist/model/writer.js +305 -0
- package/dist/model/writer.js.map +1 -0
- package/dist/tools/aggregate-tools.d.ts +295 -0
- package/dist/tools/aggregate-tools.d.ts.map +1 -0
- package/dist/tools/aggregate-tools.js +965 -0
- package/dist/tools/aggregate-tools.js.map +1 -0
- package/dist/tools/context-tools.d.ts +60 -0
- package/dist/tools/context-tools.d.ts.map +1 -0
- package/dist/tools/context-tools.js +166 -0
- package/dist/tools/context-tools.js.map +1 -0
- package/dist/tools/generation-tools.d.ts +29 -0
- package/dist/tools/generation-tools.d.ts.map +1 -0
- package/dist/tools/generation-tools.js +62 -0
- package/dist/tools/generation-tools.js.map +1 -0
- package/dist/tools/model-tools.d.ts +72 -0
- package/dist/tools/model-tools.d.ts.map +1 -0
- package/dist/tools/model-tools.js +186 -0
- package/dist/tools/model-tools.js.map +1 -0
- package/dist/tools/query-tools.d.ts +170 -0
- package/dist/tools/query-tools.d.ts.map +1 -0
- package/dist/tools/query-tools.js +322 -0
- package/dist/tools/query-tools.js.map +1 -0
- package/dist/tools/relationship-tools.d.ts +68 -0
- package/dist/tools/relationship-tools.d.ts.map +1 -0
- package/dist/tools/relationship-tools.js +178 -0
- package/dist/tools/relationship-tools.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregate Tools - CRUD operations for Aggregates, Entities, Value Objects, Events, Commands, Services
|
|
3
|
+
*/
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
import { getCurrentModel } from './model-tools.js';
|
|
6
|
+
import { sanitizeIdentifier, validateAttributes, isReservedDomainObjectName } from '../model/validation.js';
|
|
7
|
+
// Helper to find aggregate in a bounded context
|
|
8
|
+
function findAggregate(contextName, aggregateName) {
|
|
9
|
+
const model = getCurrentModel();
|
|
10
|
+
if (!model)
|
|
11
|
+
return null;
|
|
12
|
+
const bcIdx = model.boundedContexts.findIndex(bc => bc.name === contextName);
|
|
13
|
+
if (bcIdx === -1)
|
|
14
|
+
return null;
|
|
15
|
+
const bc = model.boundedContexts[bcIdx];
|
|
16
|
+
const agg = bc.aggregates.find(a => a.name === aggregateName);
|
|
17
|
+
if (agg)
|
|
18
|
+
return { aggregate: agg, contextIdx: bcIdx };
|
|
19
|
+
// Check modules
|
|
20
|
+
for (const mod of bc.modules) {
|
|
21
|
+
const modAgg = mod.aggregates.find(a => a.name === aggregateName);
|
|
22
|
+
if (modAgg)
|
|
23
|
+
return { aggregate: modAgg, contextIdx: bcIdx };
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Helper to check if a domain object name already exists in another bounded context.
|
|
29
|
+
* This prevents ambiguous type references (e.g., multiple "AgentId" definitions).
|
|
30
|
+
*
|
|
31
|
+
* @param name - The proposed name for the new domain object
|
|
32
|
+
* @param currentContext - The context where the object is being created (excluded from check)
|
|
33
|
+
* @returns Object with isDuplicate flag and location if found, or null if unique
|
|
34
|
+
*/
|
|
35
|
+
function checkForDuplicateDomainObjectName(name, currentContext) {
|
|
36
|
+
const model = getCurrentModel();
|
|
37
|
+
if (!model)
|
|
38
|
+
return { isDuplicate: false };
|
|
39
|
+
for (const bc of model.boundedContexts) {
|
|
40
|
+
// Skip the current context - duplicates within same context are handled elsewhere
|
|
41
|
+
if (bc.name === currentContext)
|
|
42
|
+
continue;
|
|
43
|
+
const allAggregates = [
|
|
44
|
+
...bc.aggregates,
|
|
45
|
+
...bc.modules.flatMap(m => m.aggregates),
|
|
46
|
+
];
|
|
47
|
+
for (const agg of allAggregates) {
|
|
48
|
+
// Check Value Objects
|
|
49
|
+
if (agg.valueObjects.some(vo => vo.name === name)) {
|
|
50
|
+
return {
|
|
51
|
+
isDuplicate: true,
|
|
52
|
+
existingLocation: `${bc.name}.${agg.name}`,
|
|
53
|
+
suggestion: `Use a unique prefixed name like '${currentContext.replace(/Platform$|Context$|Server$/g, '')}${name}' instead`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Check Entities
|
|
57
|
+
if (agg.entities.some(e => e.name === name)) {
|
|
58
|
+
return {
|
|
59
|
+
isDuplicate: true,
|
|
60
|
+
existingLocation: `${bc.name}.${agg.name}`,
|
|
61
|
+
suggestion: `Use a unique prefixed name like '${currentContext.replace(/Platform$|Context$|Server$/g, '')}${name}' instead`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Check Domain Events
|
|
65
|
+
if (agg.domainEvents.some(e => e.name === name)) {
|
|
66
|
+
return {
|
|
67
|
+
isDuplicate: true,
|
|
68
|
+
existingLocation: `${bc.name}.${agg.name}`,
|
|
69
|
+
suggestion: `Use a unique prefixed name like '${currentContext.replace(/Platform$|Context$|Server$/g, '')}${name}' instead`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Check Commands
|
|
73
|
+
if (agg.commands.some(c => c.name === name)) {
|
|
74
|
+
return {
|
|
75
|
+
isDuplicate: true,
|
|
76
|
+
existingLocation: `${bc.name}.${agg.name}`,
|
|
77
|
+
suggestion: `Use a unique prefixed name like '${currentContext.replace(/Platform$|Context$|Server$/g, '')}${name}' instead`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { isDuplicate: false };
|
|
83
|
+
}
|
|
84
|
+
export function createAggregate(params) {
|
|
85
|
+
const model = getCurrentModel();
|
|
86
|
+
if (!model) {
|
|
87
|
+
return { success: false, error: 'No model is currently loaded' };
|
|
88
|
+
}
|
|
89
|
+
const bc = model.boundedContexts.find(c => c.name === params.contextName);
|
|
90
|
+
if (!bc) {
|
|
91
|
+
return { success: false, error: `Bounded context '${params.contextName}' not found` };
|
|
92
|
+
}
|
|
93
|
+
const name = sanitizeIdentifier(params.name);
|
|
94
|
+
// Check if name is a reserved keyword (cannot be escaped for domain object names)
|
|
95
|
+
const reservedCheck = isReservedDomainObjectName(name);
|
|
96
|
+
if (reservedCheck.isReserved) {
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
error: `'${name}' is a reserved CML keyword and cannot be used as an Aggregate name. Try: ${reservedCheck.suggestion}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Check for duplicate name
|
|
103
|
+
if (bc.aggregates.some(a => a.name === name)) {
|
|
104
|
+
return { success: false, error: `Aggregate '${name}' already exists in context '${params.contextName}'` };
|
|
105
|
+
}
|
|
106
|
+
const agg = {
|
|
107
|
+
id: uuidv4(),
|
|
108
|
+
name,
|
|
109
|
+
responsibilities: params.responsibilities,
|
|
110
|
+
knowledgeLevel: params.knowledgeLevel,
|
|
111
|
+
entities: [],
|
|
112
|
+
valueObjects: [],
|
|
113
|
+
domainEvents: [],
|
|
114
|
+
commands: [],
|
|
115
|
+
services: [],
|
|
116
|
+
};
|
|
117
|
+
bc.aggregates.push(agg);
|
|
118
|
+
return {
|
|
119
|
+
success: true,
|
|
120
|
+
aggregate: {
|
|
121
|
+
id: agg.id,
|
|
122
|
+
name: agg.name,
|
|
123
|
+
contextName: bc.name,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export function updateAggregate(params) {
|
|
128
|
+
const result = findAggregate(params.contextName, params.aggregateName);
|
|
129
|
+
if (!result) {
|
|
130
|
+
return { success: false, error: `Aggregate '${params.aggregateName}' not found in context '${params.contextName}'` };
|
|
131
|
+
}
|
|
132
|
+
const { aggregate } = result;
|
|
133
|
+
if (params.newName) {
|
|
134
|
+
aggregate.name = sanitizeIdentifier(params.newName);
|
|
135
|
+
}
|
|
136
|
+
if (params.responsibilities !== undefined) {
|
|
137
|
+
aggregate.responsibilities = params.responsibilities.length > 0 ? params.responsibilities : undefined;
|
|
138
|
+
}
|
|
139
|
+
if (params.knowledgeLevel !== undefined) {
|
|
140
|
+
aggregate.knowledgeLevel = params.knowledgeLevel;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
success: true,
|
|
144
|
+
aggregate: {
|
|
145
|
+
id: aggregate.id,
|
|
146
|
+
name: aggregate.name,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function deleteAggregate(params) {
|
|
151
|
+
const model = getCurrentModel();
|
|
152
|
+
if (!model) {
|
|
153
|
+
return { success: false, error: 'No model is currently loaded' };
|
|
154
|
+
}
|
|
155
|
+
const bc = model.boundedContexts.find(c => c.name === params.contextName);
|
|
156
|
+
if (!bc) {
|
|
157
|
+
return { success: false, error: `Bounded context '${params.contextName}' not found` };
|
|
158
|
+
}
|
|
159
|
+
const idx = bc.aggregates.findIndex(a => a.name === params.aggregateName);
|
|
160
|
+
if (idx !== -1) {
|
|
161
|
+
bc.aggregates.splice(idx, 1);
|
|
162
|
+
return { success: true };
|
|
163
|
+
}
|
|
164
|
+
// Check modules
|
|
165
|
+
for (const mod of bc.modules) {
|
|
166
|
+
const modIdx = mod.aggregates.findIndex(a => a.name === params.aggregateName);
|
|
167
|
+
if (modIdx !== -1) {
|
|
168
|
+
mod.aggregates.splice(modIdx, 1);
|
|
169
|
+
return { success: true };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { success: false, error: `Aggregate '${params.aggregateName}' not found in context '${params.contextName}'` };
|
|
173
|
+
}
|
|
174
|
+
export function addEntity(params) {
|
|
175
|
+
const result = findAggregate(params.contextName, params.aggregateName);
|
|
176
|
+
if (!result) {
|
|
177
|
+
return { success: false, error: `Aggregate '${params.aggregateName}' not found in context '${params.contextName}'` };
|
|
178
|
+
}
|
|
179
|
+
const { aggregate } = result;
|
|
180
|
+
const name = sanitizeIdentifier(params.name);
|
|
181
|
+
// Check if name is a reserved keyword (cannot be escaped for domain object names)
|
|
182
|
+
const reservedCheck = isReservedDomainObjectName(name);
|
|
183
|
+
if (reservedCheck.isReserved) {
|
|
184
|
+
return {
|
|
185
|
+
success: false,
|
|
186
|
+
error: `'${name}' is a reserved CML keyword and cannot be used as an Entity name. Try: ${reservedCheck.suggestion}`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// Check for duplicate within aggregate
|
|
190
|
+
if (aggregate.entities.some(e => e.name === name)) {
|
|
191
|
+
return { success: false, error: `Entity '${name}' already exists in aggregate '${params.aggregateName}'` };
|
|
192
|
+
}
|
|
193
|
+
// Check for duplicate across other bounded contexts (prevents ambiguous type references)
|
|
194
|
+
const duplicateCheck = checkForDuplicateDomainObjectName(name, params.contextName);
|
|
195
|
+
if (duplicateCheck.isDuplicate) {
|
|
196
|
+
return {
|
|
197
|
+
success: false,
|
|
198
|
+
error: `Entity name '${name}' already exists in ${duplicateCheck.existingLocation}. ${duplicateCheck.suggestion}`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// Validate attribute types
|
|
202
|
+
if (params.attributes && params.attributes.length > 0) {
|
|
203
|
+
const attrValidation = validateAttributes(params.attributes, name);
|
|
204
|
+
if (!attrValidation.valid) {
|
|
205
|
+
return {
|
|
206
|
+
success: false,
|
|
207
|
+
error: attrValidation.errors.join('\n'),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const entity = {
|
|
212
|
+
id: uuidv4(),
|
|
213
|
+
name,
|
|
214
|
+
aggregateRoot: params.aggregateRoot,
|
|
215
|
+
attributes: (params.attributes || []).map(a => ({
|
|
216
|
+
name: a.name,
|
|
217
|
+
type: a.type,
|
|
218
|
+
key: a.key,
|
|
219
|
+
nullable: a.nullable,
|
|
220
|
+
})),
|
|
221
|
+
operations: [],
|
|
222
|
+
};
|
|
223
|
+
aggregate.entities.push(entity);
|
|
224
|
+
// Set as aggregate root if specified
|
|
225
|
+
if (params.aggregateRoot) {
|
|
226
|
+
aggregate.aggregateRoot = entity;
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
success: true,
|
|
230
|
+
entity: {
|
|
231
|
+
id: entity.id,
|
|
232
|
+
name: entity.name,
|
|
233
|
+
isAggregateRoot: !!entity.aggregateRoot,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
export function addValueObject(params) {
|
|
238
|
+
const result = findAggregate(params.contextName, params.aggregateName);
|
|
239
|
+
if (!result) {
|
|
240
|
+
return { success: false, error: `Aggregate '${params.aggregateName}' not found in context '${params.contextName}'` };
|
|
241
|
+
}
|
|
242
|
+
const { aggregate } = result;
|
|
243
|
+
const name = sanitizeIdentifier(params.name);
|
|
244
|
+
// Check if name is a reserved keyword (cannot be escaped for domain object names)
|
|
245
|
+
const reservedCheck = isReservedDomainObjectName(name);
|
|
246
|
+
if (reservedCheck.isReserved) {
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
error: `'${name}' is a reserved CML keyword and cannot be used as a Value Object name. Try: ${reservedCheck.suggestion}`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// Check for duplicate within aggregate
|
|
253
|
+
if (aggregate.valueObjects.some(vo => vo.name === name)) {
|
|
254
|
+
return { success: false, error: `Value object '${name}' already exists in aggregate '${params.aggregateName}'` };
|
|
255
|
+
}
|
|
256
|
+
// Check for duplicate across other bounded contexts (prevents ambiguous type references)
|
|
257
|
+
const duplicateCheck = checkForDuplicateDomainObjectName(name, params.contextName);
|
|
258
|
+
if (duplicateCheck.isDuplicate) {
|
|
259
|
+
return {
|
|
260
|
+
success: false,
|
|
261
|
+
error: `Value object name '${name}' already exists in ${duplicateCheck.existingLocation}. ${duplicateCheck.suggestion}`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
// Validate attribute types
|
|
265
|
+
if (params.attributes && params.attributes.length > 0) {
|
|
266
|
+
const attrValidation = validateAttributes(params.attributes, name);
|
|
267
|
+
if (!attrValidation.valid) {
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
error: attrValidation.errors.join('\n'),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const vo = {
|
|
275
|
+
id: uuidv4(),
|
|
276
|
+
name,
|
|
277
|
+
attributes: (params.attributes || []).map(a => ({
|
|
278
|
+
name: a.name,
|
|
279
|
+
type: a.type,
|
|
280
|
+
})),
|
|
281
|
+
};
|
|
282
|
+
aggregate.valueObjects.push(vo);
|
|
283
|
+
return {
|
|
284
|
+
success: true,
|
|
285
|
+
valueObject: {
|
|
286
|
+
id: vo.id,
|
|
287
|
+
name: vo.name,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
export function addIdentifier(params) {
|
|
292
|
+
const result = findAggregate(params.contextName, params.aggregateName);
|
|
293
|
+
if (!result) {
|
|
294
|
+
return { success: false, error: `Aggregate '${params.aggregateName}' not found in context '${params.contextName}'` };
|
|
295
|
+
}
|
|
296
|
+
const { aggregate } = result;
|
|
297
|
+
// Ensure name ends with "Id" for consistency
|
|
298
|
+
let name = sanitizeIdentifier(params.name);
|
|
299
|
+
if (!name.endsWith('Id')) {
|
|
300
|
+
name = name + 'Id';
|
|
301
|
+
}
|
|
302
|
+
// Capitalize first letter
|
|
303
|
+
name = name.charAt(0).toUpperCase() + name.slice(1);
|
|
304
|
+
// Check if Value Object already exists in this aggregate (return existing)
|
|
305
|
+
const existing = aggregate.valueObjects.find(vo => vo.name === name);
|
|
306
|
+
if (existing) {
|
|
307
|
+
return {
|
|
308
|
+
success: true,
|
|
309
|
+
identifier: {
|
|
310
|
+
id: existing.id,
|
|
311
|
+
name: existing.name,
|
|
312
|
+
type: `- ${existing.name}`, // Reference syntax
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
// Check for duplicate across other bounded contexts (prevents ambiguous type references)
|
|
317
|
+
const duplicateCheck = checkForDuplicateDomainObjectName(name, params.contextName);
|
|
318
|
+
if (duplicateCheck.isDuplicate) {
|
|
319
|
+
return {
|
|
320
|
+
success: false,
|
|
321
|
+
error: `Identifier name '${name}' already exists in ${duplicateCheck.existingLocation}. ${duplicateCheck.suggestion}`,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
// Create the ID Value Object with a single 'value' attribute
|
|
325
|
+
const vo = {
|
|
326
|
+
id: uuidv4(),
|
|
327
|
+
name,
|
|
328
|
+
attributes: [
|
|
329
|
+
{ name: 'value', type: 'String' },
|
|
330
|
+
],
|
|
331
|
+
};
|
|
332
|
+
aggregate.valueObjects.push(vo);
|
|
333
|
+
return {
|
|
334
|
+
success: true,
|
|
335
|
+
identifier: {
|
|
336
|
+
id: vo.id,
|
|
337
|
+
name: vo.name,
|
|
338
|
+
type: `- ${vo.name}`, // Reference syntax for CML
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
export function addDomainEvent(params) {
|
|
343
|
+
const result = findAggregate(params.contextName, params.aggregateName);
|
|
344
|
+
if (!result) {
|
|
345
|
+
return { success: false, error: `Aggregate '${params.aggregateName}' not found in context '${params.contextName}'` };
|
|
346
|
+
}
|
|
347
|
+
const { aggregate } = result;
|
|
348
|
+
const name = sanitizeIdentifier(params.name);
|
|
349
|
+
// Check if name is a reserved keyword (cannot be escaped for domain object names)
|
|
350
|
+
const reservedCheck = isReservedDomainObjectName(name);
|
|
351
|
+
if (reservedCheck.isReserved) {
|
|
352
|
+
return {
|
|
353
|
+
success: false,
|
|
354
|
+
error: `'${name}' is a reserved CML keyword and cannot be used as a Domain Event name. Try: ${reservedCheck.suggestion}`,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// Check for duplicate within aggregate
|
|
358
|
+
if (aggregate.domainEvents.some(e => e.name === name)) {
|
|
359
|
+
return { success: false, error: `Domain event '${name}' already exists in aggregate '${params.aggregateName}'` };
|
|
360
|
+
}
|
|
361
|
+
// Check for duplicate across other bounded contexts (prevents ambiguous type references)
|
|
362
|
+
const duplicateCheck = checkForDuplicateDomainObjectName(name, params.contextName);
|
|
363
|
+
if (duplicateCheck.isDuplicate) {
|
|
364
|
+
return {
|
|
365
|
+
success: false,
|
|
366
|
+
error: `Domain event name '${name}' already exists in ${duplicateCheck.existingLocation}. ${duplicateCheck.suggestion}`,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
// Validate attribute types
|
|
370
|
+
if (params.attributes && params.attributes.length > 0) {
|
|
371
|
+
const attrValidation = validateAttributes(params.attributes, name);
|
|
372
|
+
if (!attrValidation.valid) {
|
|
373
|
+
return {
|
|
374
|
+
success: false,
|
|
375
|
+
error: attrValidation.errors.join('\n'),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const event = {
|
|
380
|
+
id: uuidv4(),
|
|
381
|
+
name,
|
|
382
|
+
attributes: (params.attributes || []).map(a => ({
|
|
383
|
+
name: a.name,
|
|
384
|
+
type: a.type,
|
|
385
|
+
})),
|
|
386
|
+
};
|
|
387
|
+
aggregate.domainEvents.push(event);
|
|
388
|
+
return {
|
|
389
|
+
success: true,
|
|
390
|
+
domainEvent: {
|
|
391
|
+
id: event.id,
|
|
392
|
+
name: event.name,
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
export function addCommand(params) {
|
|
397
|
+
const result = findAggregate(params.contextName, params.aggregateName);
|
|
398
|
+
if (!result) {
|
|
399
|
+
return { success: false, error: `Aggregate '${params.aggregateName}' not found in context '${params.contextName}'` };
|
|
400
|
+
}
|
|
401
|
+
const { aggregate } = result;
|
|
402
|
+
const name = sanitizeIdentifier(params.name);
|
|
403
|
+
// Check if name is a reserved keyword (cannot be escaped for domain object names)
|
|
404
|
+
const reservedCheck = isReservedDomainObjectName(name);
|
|
405
|
+
if (reservedCheck.isReserved) {
|
|
406
|
+
return {
|
|
407
|
+
success: false,
|
|
408
|
+
error: `'${name}' is a reserved CML keyword and cannot be used as a Command name. Try: ${reservedCheck.suggestion}`,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
// Check for duplicate within aggregate
|
|
412
|
+
if (aggregate.commands.some(c => c.name === name)) {
|
|
413
|
+
return { success: false, error: `Command '${name}' already exists in aggregate '${params.aggregateName}'` };
|
|
414
|
+
}
|
|
415
|
+
// Check for duplicate across other bounded contexts (prevents ambiguous type references)
|
|
416
|
+
const duplicateCheck = checkForDuplicateDomainObjectName(name, params.contextName);
|
|
417
|
+
if (duplicateCheck.isDuplicate) {
|
|
418
|
+
return {
|
|
419
|
+
success: false,
|
|
420
|
+
error: `Command name '${name}' already exists in ${duplicateCheck.existingLocation}. ${duplicateCheck.suggestion}`,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
// Validate attribute types
|
|
424
|
+
if (params.attributes && params.attributes.length > 0) {
|
|
425
|
+
const attrValidation = validateAttributes(params.attributes, name);
|
|
426
|
+
if (!attrValidation.valid) {
|
|
427
|
+
return {
|
|
428
|
+
success: false,
|
|
429
|
+
error: attrValidation.errors.join('\n'),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const cmd = {
|
|
434
|
+
id: uuidv4(),
|
|
435
|
+
name,
|
|
436
|
+
attributes: (params.attributes || []).map(a => ({
|
|
437
|
+
name: a.name,
|
|
438
|
+
type: a.type,
|
|
439
|
+
})),
|
|
440
|
+
};
|
|
441
|
+
aggregate.commands.push(cmd);
|
|
442
|
+
return {
|
|
443
|
+
success: true,
|
|
444
|
+
command: {
|
|
445
|
+
id: cmd.id,
|
|
446
|
+
name: cmd.name,
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
export function addService(params) {
|
|
451
|
+
const result = findAggregate(params.contextName, params.aggregateName);
|
|
452
|
+
if (!result) {
|
|
453
|
+
return { success: false, error: `Aggregate '${params.aggregateName}' not found in context '${params.contextName}'` };
|
|
454
|
+
}
|
|
455
|
+
const { aggregate } = result;
|
|
456
|
+
const name = sanitizeIdentifier(params.name);
|
|
457
|
+
// Check for duplicate
|
|
458
|
+
if (aggregate.services.some(s => s.name === name)) {
|
|
459
|
+
return { success: false, error: `Service '${name}' already exists in aggregate '${params.aggregateName}'` };
|
|
460
|
+
}
|
|
461
|
+
const svc = {
|
|
462
|
+
id: uuidv4(),
|
|
463
|
+
name,
|
|
464
|
+
operations: (params.operations || []).map(op => ({
|
|
465
|
+
name: op.name,
|
|
466
|
+
returnType: op.returnType,
|
|
467
|
+
parameters: op.parameters || [],
|
|
468
|
+
})),
|
|
469
|
+
};
|
|
470
|
+
aggregate.services.push(svc);
|
|
471
|
+
return {
|
|
472
|
+
success: true,
|
|
473
|
+
service: {
|
|
474
|
+
id: svc.id,
|
|
475
|
+
name: svc.name,
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
export function deleteEntity(params) {
|
|
480
|
+
const result = findAggregate(params.contextName, params.aggregateName);
|
|
481
|
+
if (!result) {
|
|
482
|
+
return { success: false, error: `Aggregate '${params.aggregateName}' not found in context '${params.contextName}'` };
|
|
483
|
+
}
|
|
484
|
+
const { aggregate } = result;
|
|
485
|
+
const idx = aggregate.entities.findIndex(e => e.name === params.entityName);
|
|
486
|
+
if (idx === -1) {
|
|
487
|
+
return { success: false, error: `Entity '${params.entityName}' not found in aggregate '${params.aggregateName}'` };
|
|
488
|
+
}
|
|
489
|
+
// Check if it's the aggregate root
|
|
490
|
+
if (aggregate.aggregateRoot && aggregate.aggregateRoot.name === params.entityName) {
|
|
491
|
+
aggregate.aggregateRoot = undefined;
|
|
492
|
+
}
|
|
493
|
+
aggregate.entities.splice(idx, 1);
|
|
494
|
+
return { success: true };
|
|
495
|
+
}
|
|
496
|
+
export function deleteValueObject(params) {
|
|
497
|
+
const result = findAggregate(params.contextName, params.aggregateName);
|
|
498
|
+
if (!result) {
|
|
499
|
+
return { success: false, error: `Aggregate '${params.aggregateName}' not found in context '${params.contextName}'` };
|
|
500
|
+
}
|
|
501
|
+
const { aggregate } = result;
|
|
502
|
+
const idx = aggregate.valueObjects.findIndex(vo => vo.name === params.valueObjectName);
|
|
503
|
+
if (idx === -1) {
|
|
504
|
+
return { success: false, error: `Value object '${params.valueObjectName}' not found in aggregate '${params.aggregateName}'` };
|
|
505
|
+
}
|
|
506
|
+
aggregate.valueObjects.splice(idx, 1);
|
|
507
|
+
return { success: true };
|
|
508
|
+
}
|
|
509
|
+
export function deleteDomainEvent(params) {
|
|
510
|
+
const result = findAggregate(params.contextName, params.aggregateName);
|
|
511
|
+
if (!result) {
|
|
512
|
+
return { success: false, error: `Aggregate '${params.aggregateName}' not found in context '${params.contextName}'` };
|
|
513
|
+
}
|
|
514
|
+
const { aggregate } = result;
|
|
515
|
+
const idx = aggregate.domainEvents.findIndex(e => e.name === params.eventName);
|
|
516
|
+
if (idx === -1) {
|
|
517
|
+
return { success: false, error: `Domain event '${params.eventName}' not found in aggregate '${params.aggregateName}'` };
|
|
518
|
+
}
|
|
519
|
+
aggregate.domainEvents.splice(idx, 1);
|
|
520
|
+
return { success: true };
|
|
521
|
+
}
|
|
522
|
+
export function deleteCommand(params) {
|
|
523
|
+
const result = findAggregate(params.contextName, params.aggregateName);
|
|
524
|
+
if (!result) {
|
|
525
|
+
return { success: false, error: `Aggregate '${params.aggregateName}' not found in context '${params.contextName}'` };
|
|
526
|
+
}
|
|
527
|
+
const { aggregate } = result;
|
|
528
|
+
const idx = aggregate.commands.findIndex(c => c.name === params.commandName);
|
|
529
|
+
if (idx === -1) {
|
|
530
|
+
return { success: false, error: `Command '${params.commandName}' not found in aggregate '${params.aggregateName}'` };
|
|
531
|
+
}
|
|
532
|
+
aggregate.commands.splice(idx, 1);
|
|
533
|
+
return { success: true };
|
|
534
|
+
}
|
|
535
|
+
export function deleteService(params) {
|
|
536
|
+
const result = findAggregate(params.contextName, params.aggregateName);
|
|
537
|
+
if (!result) {
|
|
538
|
+
return { success: false, error: `Aggregate '${params.aggregateName}' not found in context '${params.contextName}'` };
|
|
539
|
+
}
|
|
540
|
+
const { aggregate } = result;
|
|
541
|
+
const idx = aggregate.services.findIndex(s => s.name === params.serviceName);
|
|
542
|
+
if (idx === -1) {
|
|
543
|
+
return { success: false, error: `Service '${params.serviceName}' not found in aggregate '${params.aggregateName}'` };
|
|
544
|
+
}
|
|
545
|
+
aggregate.services.splice(idx, 1);
|
|
546
|
+
return { success: true };
|
|
547
|
+
}
|
|
548
|
+
export function batchAddElements(params) {
|
|
549
|
+
const model = getCurrentModel();
|
|
550
|
+
if (!model) {
|
|
551
|
+
return {
|
|
552
|
+
success: false,
|
|
553
|
+
created: { entities: [], valueObjects: [], identifiers: [], domainEvents: [], commands: [], services: [] },
|
|
554
|
+
errors: [{ elementType: 'model', name: '', error: 'No model is currently loaded' }],
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
// Find the target aggregate
|
|
558
|
+
const aggregateResult = findAggregate(params.contextName, params.aggregateName);
|
|
559
|
+
if (!aggregateResult) {
|
|
560
|
+
return {
|
|
561
|
+
success: false,
|
|
562
|
+
created: { entities: [], valueObjects: [], identifiers: [], domainEvents: [], commands: [], services: [] },
|
|
563
|
+
errors: [{ elementType: 'aggregate', name: params.aggregateName, error: `Aggregate '${params.aggregateName}' not found in context '${params.contextName}'` }],
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
const { aggregate } = aggregateResult;
|
|
567
|
+
const failFast = params.failFast !== false; // Default to true
|
|
568
|
+
const validationErrors = [];
|
|
569
|
+
// ============================================
|
|
570
|
+
// PHASE 1: Validate ALL elements before mutations
|
|
571
|
+
// ============================================
|
|
572
|
+
// Track names we're about to create (for duplicate detection within batch)
|
|
573
|
+
const batchNames = new Set();
|
|
574
|
+
// Validate identifiers
|
|
575
|
+
const normalizedIdentifiers = [];
|
|
576
|
+
for (const identifier of params.identifiers || []) {
|
|
577
|
+
let name = sanitizeIdentifier(identifier.name);
|
|
578
|
+
if (!name.endsWith('Id')) {
|
|
579
|
+
name = name + 'Id';
|
|
580
|
+
}
|
|
581
|
+
name = name.charAt(0).toUpperCase() + name.slice(1);
|
|
582
|
+
// Check duplicate in batch
|
|
583
|
+
if (batchNames.has(name)) {
|
|
584
|
+
validationErrors.push({ elementType: 'identifier', name, error: `Duplicate name '${name}' within this batch` });
|
|
585
|
+
if (failFast)
|
|
586
|
+
break;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
// Check duplicate in aggregate (existing identifier returns success, so skip duplicate check for identifiers in aggregate)
|
|
590
|
+
// But check cross-context duplicates
|
|
591
|
+
const duplicateCheck = checkForDuplicateDomainObjectName(name, params.contextName);
|
|
592
|
+
if (duplicateCheck.isDuplicate) {
|
|
593
|
+
validationErrors.push({
|
|
594
|
+
elementType: 'identifier',
|
|
595
|
+
name,
|
|
596
|
+
error: `Identifier name '${name}' already exists in ${duplicateCheck.existingLocation}. ${duplicateCheck.suggestion}`,
|
|
597
|
+
});
|
|
598
|
+
if (failFast)
|
|
599
|
+
break;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
batchNames.add(name);
|
|
603
|
+
normalizedIdentifiers.push({ originalName: identifier.name, normalizedName: name });
|
|
604
|
+
}
|
|
605
|
+
// Validate value objects
|
|
606
|
+
if (validationErrors.length === 0 || !failFast) {
|
|
607
|
+
for (const vo of params.valueObjects || []) {
|
|
608
|
+
const name = sanitizeIdentifier(vo.name);
|
|
609
|
+
// Check reserved keyword
|
|
610
|
+
const reservedCheck = isReservedDomainObjectName(name);
|
|
611
|
+
if (reservedCheck.isReserved) {
|
|
612
|
+
validationErrors.push({
|
|
613
|
+
elementType: 'valueObject',
|
|
614
|
+
name,
|
|
615
|
+
error: `'${name}' is a reserved CML keyword and cannot be used as a Value Object name. Try: ${reservedCheck.suggestion}`,
|
|
616
|
+
});
|
|
617
|
+
if (failFast)
|
|
618
|
+
break;
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
// Check duplicate in batch
|
|
622
|
+
if (batchNames.has(name)) {
|
|
623
|
+
validationErrors.push({ elementType: 'valueObject', name, error: `Duplicate name '${name}' within this batch` });
|
|
624
|
+
if (failFast)
|
|
625
|
+
break;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
// Check duplicate in aggregate
|
|
629
|
+
if (aggregate.valueObjects.some(v => v.name === name)) {
|
|
630
|
+
validationErrors.push({ elementType: 'valueObject', name, error: `Value object '${name}' already exists in aggregate '${params.aggregateName}'` });
|
|
631
|
+
if (failFast)
|
|
632
|
+
break;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
// Check cross-context duplicate
|
|
636
|
+
const duplicateCheck = checkForDuplicateDomainObjectName(name, params.contextName);
|
|
637
|
+
if (duplicateCheck.isDuplicate) {
|
|
638
|
+
validationErrors.push({
|
|
639
|
+
elementType: 'valueObject',
|
|
640
|
+
name,
|
|
641
|
+
error: `Value object name '${name}' already exists in ${duplicateCheck.existingLocation}. ${duplicateCheck.suggestion}`,
|
|
642
|
+
});
|
|
643
|
+
if (failFast)
|
|
644
|
+
break;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
// Validate attributes
|
|
648
|
+
if (vo.attributes && vo.attributes.length > 0) {
|
|
649
|
+
const attrValidation = validateAttributes(vo.attributes, name);
|
|
650
|
+
if (!attrValidation.valid) {
|
|
651
|
+
validationErrors.push({ elementType: 'valueObject', name, error: attrValidation.errors.join('; ') });
|
|
652
|
+
if (failFast)
|
|
653
|
+
break;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
batchNames.add(name);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// Validate entities
|
|
661
|
+
if (validationErrors.length === 0 || !failFast) {
|
|
662
|
+
for (const entity of params.entities || []) {
|
|
663
|
+
const name = sanitizeIdentifier(entity.name);
|
|
664
|
+
// Check reserved keyword
|
|
665
|
+
const reservedCheck = isReservedDomainObjectName(name);
|
|
666
|
+
if (reservedCheck.isReserved) {
|
|
667
|
+
validationErrors.push({
|
|
668
|
+
elementType: 'entity',
|
|
669
|
+
name,
|
|
670
|
+
error: `'${name}' is a reserved CML keyword and cannot be used as an Entity name. Try: ${reservedCheck.suggestion}`,
|
|
671
|
+
});
|
|
672
|
+
if (failFast)
|
|
673
|
+
break;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
// Check duplicate in batch
|
|
677
|
+
if (batchNames.has(name)) {
|
|
678
|
+
validationErrors.push({ elementType: 'entity', name, error: `Duplicate name '${name}' within this batch` });
|
|
679
|
+
if (failFast)
|
|
680
|
+
break;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
// Check duplicate in aggregate
|
|
684
|
+
if (aggregate.entities.some(e => e.name === name)) {
|
|
685
|
+
validationErrors.push({ elementType: 'entity', name, error: `Entity '${name}' already exists in aggregate '${params.aggregateName}'` });
|
|
686
|
+
if (failFast)
|
|
687
|
+
break;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
// Check cross-context duplicate
|
|
691
|
+
const duplicateCheck = checkForDuplicateDomainObjectName(name, params.contextName);
|
|
692
|
+
if (duplicateCheck.isDuplicate) {
|
|
693
|
+
validationErrors.push({
|
|
694
|
+
elementType: 'entity',
|
|
695
|
+
name,
|
|
696
|
+
error: `Entity name '${name}' already exists in ${duplicateCheck.existingLocation}. ${duplicateCheck.suggestion}`,
|
|
697
|
+
});
|
|
698
|
+
if (failFast)
|
|
699
|
+
break;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
// Validate attributes
|
|
703
|
+
if (entity.attributes && entity.attributes.length > 0) {
|
|
704
|
+
const attrValidation = validateAttributes(entity.attributes, name);
|
|
705
|
+
if (!attrValidation.valid) {
|
|
706
|
+
validationErrors.push({ elementType: 'entity', name, error: attrValidation.errors.join('; ') });
|
|
707
|
+
if (failFast)
|
|
708
|
+
break;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
batchNames.add(name);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// Validate domain events
|
|
716
|
+
if (validationErrors.length === 0 || !failFast) {
|
|
717
|
+
for (const event of params.domainEvents || []) {
|
|
718
|
+
const name = sanitizeIdentifier(event.name);
|
|
719
|
+
// Check reserved keyword
|
|
720
|
+
const reservedCheck = isReservedDomainObjectName(name);
|
|
721
|
+
if (reservedCheck.isReserved) {
|
|
722
|
+
validationErrors.push({
|
|
723
|
+
elementType: 'domainEvent',
|
|
724
|
+
name,
|
|
725
|
+
error: `'${name}' is a reserved CML keyword and cannot be used as a Domain Event name. Try: ${reservedCheck.suggestion}`,
|
|
726
|
+
});
|
|
727
|
+
if (failFast)
|
|
728
|
+
break;
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
// Check duplicate in batch
|
|
732
|
+
if (batchNames.has(name)) {
|
|
733
|
+
validationErrors.push({ elementType: 'domainEvent', name, error: `Duplicate name '${name}' within this batch` });
|
|
734
|
+
if (failFast)
|
|
735
|
+
break;
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
// Check duplicate in aggregate
|
|
739
|
+
if (aggregate.domainEvents.some(e => e.name === name)) {
|
|
740
|
+
validationErrors.push({ elementType: 'domainEvent', name, error: `Domain event '${name}' already exists in aggregate '${params.aggregateName}'` });
|
|
741
|
+
if (failFast)
|
|
742
|
+
break;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
// Check cross-context duplicate
|
|
746
|
+
const duplicateCheck = checkForDuplicateDomainObjectName(name, params.contextName);
|
|
747
|
+
if (duplicateCheck.isDuplicate) {
|
|
748
|
+
validationErrors.push({
|
|
749
|
+
elementType: 'domainEvent',
|
|
750
|
+
name,
|
|
751
|
+
error: `Domain event name '${name}' already exists in ${duplicateCheck.existingLocation}. ${duplicateCheck.suggestion}`,
|
|
752
|
+
});
|
|
753
|
+
if (failFast)
|
|
754
|
+
break;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
// Validate attributes
|
|
758
|
+
if (event.attributes && event.attributes.length > 0) {
|
|
759
|
+
const attrValidation = validateAttributes(event.attributes, name);
|
|
760
|
+
if (!attrValidation.valid) {
|
|
761
|
+
validationErrors.push({ elementType: 'domainEvent', name, error: attrValidation.errors.join('; ') });
|
|
762
|
+
if (failFast)
|
|
763
|
+
break;
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
batchNames.add(name);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
// Validate commands
|
|
771
|
+
if (validationErrors.length === 0 || !failFast) {
|
|
772
|
+
for (const cmd of params.commands || []) {
|
|
773
|
+
const name = sanitizeIdentifier(cmd.name);
|
|
774
|
+
// Check reserved keyword
|
|
775
|
+
const reservedCheck = isReservedDomainObjectName(name);
|
|
776
|
+
if (reservedCheck.isReserved) {
|
|
777
|
+
validationErrors.push({
|
|
778
|
+
elementType: 'command',
|
|
779
|
+
name,
|
|
780
|
+
error: `'${name}' is a reserved CML keyword and cannot be used as a Command name. Try: ${reservedCheck.suggestion}`,
|
|
781
|
+
});
|
|
782
|
+
if (failFast)
|
|
783
|
+
break;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
// Check duplicate in batch
|
|
787
|
+
if (batchNames.has(name)) {
|
|
788
|
+
validationErrors.push({ elementType: 'command', name, error: `Duplicate name '${name}' within this batch` });
|
|
789
|
+
if (failFast)
|
|
790
|
+
break;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
// Check duplicate in aggregate
|
|
794
|
+
if (aggregate.commands.some(c => c.name === name)) {
|
|
795
|
+
validationErrors.push({ elementType: 'command', name, error: `Command '${name}' already exists in aggregate '${params.aggregateName}'` });
|
|
796
|
+
if (failFast)
|
|
797
|
+
break;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
// Check cross-context duplicate
|
|
801
|
+
const duplicateCheck = checkForDuplicateDomainObjectName(name, params.contextName);
|
|
802
|
+
if (duplicateCheck.isDuplicate) {
|
|
803
|
+
validationErrors.push({
|
|
804
|
+
elementType: 'command',
|
|
805
|
+
name,
|
|
806
|
+
error: `Command name '${name}' already exists in ${duplicateCheck.existingLocation}. ${duplicateCheck.suggestion}`,
|
|
807
|
+
});
|
|
808
|
+
if (failFast)
|
|
809
|
+
break;
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
// Validate attributes
|
|
813
|
+
if (cmd.attributes && cmd.attributes.length > 0) {
|
|
814
|
+
const attrValidation = validateAttributes(cmd.attributes, name);
|
|
815
|
+
if (!attrValidation.valid) {
|
|
816
|
+
validationErrors.push({ elementType: 'command', name, error: attrValidation.errors.join('; ') });
|
|
817
|
+
if (failFast)
|
|
818
|
+
break;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
batchNames.add(name);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
// Validate services (services don't have cross-context uniqueness requirements like other domain objects)
|
|
826
|
+
if (validationErrors.length === 0 || !failFast) {
|
|
827
|
+
for (const svc of params.services || []) {
|
|
828
|
+
const name = sanitizeIdentifier(svc.name);
|
|
829
|
+
// Check duplicate in aggregate
|
|
830
|
+
if (aggregate.services.some(s => s.name === name)) {
|
|
831
|
+
validationErrors.push({ elementType: 'service', name, error: `Service '${name}' already exists in aggregate '${params.aggregateName}'` });
|
|
832
|
+
if (failFast)
|
|
833
|
+
break;
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// If validation failed, return errors without making any changes
|
|
839
|
+
if (validationErrors.length > 0) {
|
|
840
|
+
return {
|
|
841
|
+
success: false,
|
|
842
|
+
created: { entities: [], valueObjects: [], identifiers: [], domainEvents: [], commands: [], services: [] },
|
|
843
|
+
errors: validationErrors,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
// ============================================
|
|
847
|
+
// PHASE 2: Create all elements (validation passed)
|
|
848
|
+
// ============================================
|
|
849
|
+
const created = {
|
|
850
|
+
entities: [],
|
|
851
|
+
valueObjects: [],
|
|
852
|
+
identifiers: [],
|
|
853
|
+
domainEvents: [],
|
|
854
|
+
commands: [],
|
|
855
|
+
services: [],
|
|
856
|
+
};
|
|
857
|
+
// Create identifiers first (as they may be referenced by other elements)
|
|
858
|
+
for (const { normalizedName } of normalizedIdentifiers) {
|
|
859
|
+
// Check if it already exists (return existing)
|
|
860
|
+
const existing = aggregate.valueObjects.find(vo => vo.name === normalizedName);
|
|
861
|
+
if (existing) {
|
|
862
|
+
created.identifiers.push({
|
|
863
|
+
id: existing.id,
|
|
864
|
+
name: existing.name,
|
|
865
|
+
type: `- ${existing.name}`,
|
|
866
|
+
});
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const vo = {
|
|
870
|
+
id: uuidv4(),
|
|
871
|
+
name: normalizedName,
|
|
872
|
+
attributes: [{ name: 'value', type: 'String' }],
|
|
873
|
+
};
|
|
874
|
+
aggregate.valueObjects.push(vo);
|
|
875
|
+
created.identifiers.push({
|
|
876
|
+
id: vo.id,
|
|
877
|
+
name: vo.name,
|
|
878
|
+
type: `- ${vo.name}`,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
// Create value objects
|
|
882
|
+
for (const voParams of params.valueObjects || []) {
|
|
883
|
+
const name = sanitizeIdentifier(voParams.name);
|
|
884
|
+
const vo = {
|
|
885
|
+
id: uuidv4(),
|
|
886
|
+
name,
|
|
887
|
+
attributes: (voParams.attributes || []).map(a => ({
|
|
888
|
+
name: a.name,
|
|
889
|
+
type: a.type,
|
|
890
|
+
})),
|
|
891
|
+
};
|
|
892
|
+
aggregate.valueObjects.push(vo);
|
|
893
|
+
created.valueObjects.push({ id: vo.id, name: vo.name });
|
|
894
|
+
}
|
|
895
|
+
// Create entities
|
|
896
|
+
for (const entityParams of params.entities || []) {
|
|
897
|
+
const name = sanitizeIdentifier(entityParams.name);
|
|
898
|
+
const entity = {
|
|
899
|
+
id: uuidv4(),
|
|
900
|
+
name,
|
|
901
|
+
aggregateRoot: entityParams.aggregateRoot,
|
|
902
|
+
attributes: (entityParams.attributes || []).map(a => ({
|
|
903
|
+
name: a.name,
|
|
904
|
+
type: a.type,
|
|
905
|
+
key: a.key,
|
|
906
|
+
nullable: a.nullable,
|
|
907
|
+
})),
|
|
908
|
+
operations: [],
|
|
909
|
+
};
|
|
910
|
+
aggregate.entities.push(entity);
|
|
911
|
+
if (entityParams.aggregateRoot) {
|
|
912
|
+
aggregate.aggregateRoot = entity;
|
|
913
|
+
}
|
|
914
|
+
created.entities.push({
|
|
915
|
+
id: entity.id,
|
|
916
|
+
name: entity.name,
|
|
917
|
+
isAggregateRoot: !!entity.aggregateRoot,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
// Create domain events
|
|
921
|
+
for (const eventParams of params.domainEvents || []) {
|
|
922
|
+
const name = sanitizeIdentifier(eventParams.name);
|
|
923
|
+
const event = {
|
|
924
|
+
id: uuidv4(),
|
|
925
|
+
name,
|
|
926
|
+
attributes: (eventParams.attributes || []).map(a => ({
|
|
927
|
+
name: a.name,
|
|
928
|
+
type: a.type,
|
|
929
|
+
})),
|
|
930
|
+
};
|
|
931
|
+
aggregate.domainEvents.push(event);
|
|
932
|
+
created.domainEvents.push({ id: event.id, name: event.name });
|
|
933
|
+
}
|
|
934
|
+
// Create commands
|
|
935
|
+
for (const cmdParams of params.commands || []) {
|
|
936
|
+
const name = sanitizeIdentifier(cmdParams.name);
|
|
937
|
+
const cmd = {
|
|
938
|
+
id: uuidv4(),
|
|
939
|
+
name,
|
|
940
|
+
attributes: (cmdParams.attributes || []).map(a => ({
|
|
941
|
+
name: a.name,
|
|
942
|
+
type: a.type,
|
|
943
|
+
})),
|
|
944
|
+
};
|
|
945
|
+
aggregate.commands.push(cmd);
|
|
946
|
+
created.commands.push({ id: cmd.id, name: cmd.name });
|
|
947
|
+
}
|
|
948
|
+
// Create services
|
|
949
|
+
for (const svcParams of params.services || []) {
|
|
950
|
+
const name = sanitizeIdentifier(svcParams.name);
|
|
951
|
+
const svc = {
|
|
952
|
+
id: uuidv4(),
|
|
953
|
+
name,
|
|
954
|
+
operations: (svcParams.operations || []).map(op => ({
|
|
955
|
+
name: op.name,
|
|
956
|
+
returnType: op.returnType,
|
|
957
|
+
parameters: op.parameters || [],
|
|
958
|
+
})),
|
|
959
|
+
};
|
|
960
|
+
aggregate.services.push(svc);
|
|
961
|
+
created.services.push({ id: svc.id, name: svc.name });
|
|
962
|
+
}
|
|
963
|
+
return { success: true, created };
|
|
964
|
+
}
|
|
965
|
+
//# sourceMappingURL=aggregate-tools.js.map
|