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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +238 -0
  3. package/dist/generators/plantuml.d.ts +17 -0
  4. package/dist/generators/plantuml.d.ts.map +1 -0
  5. package/dist/generators/plantuml.js +244 -0
  6. package/dist/generators/plantuml.js.map +1 -0
  7. package/dist/index.d.ts +7 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +897 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/model/parser.d.ts +51 -0
  12. package/dist/model/parser.d.ts.map +1 -0
  13. package/dist/model/parser.js +934 -0
  14. package/dist/model/parser.js.map +1 -0
  15. package/dist/model/types.d.ts +121 -0
  16. package/dist/model/types.d.ts.map +1 -0
  17. package/dist/model/types.js +12 -0
  18. package/dist/model/types.js.map +1 -0
  19. package/dist/model/validation.d.ts +36 -0
  20. package/dist/model/validation.d.ts.map +1 -0
  21. package/dist/model/validation.js +411 -0
  22. package/dist/model/validation.js.map +1 -0
  23. package/dist/model/writer.d.ts +30 -0
  24. package/dist/model/writer.d.ts.map +1 -0
  25. package/dist/model/writer.js +305 -0
  26. package/dist/model/writer.js.map +1 -0
  27. package/dist/tools/aggregate-tools.d.ts +295 -0
  28. package/dist/tools/aggregate-tools.d.ts.map +1 -0
  29. package/dist/tools/aggregate-tools.js +965 -0
  30. package/dist/tools/aggregate-tools.js.map +1 -0
  31. package/dist/tools/context-tools.d.ts +60 -0
  32. package/dist/tools/context-tools.d.ts.map +1 -0
  33. package/dist/tools/context-tools.js +166 -0
  34. package/dist/tools/context-tools.js.map +1 -0
  35. package/dist/tools/generation-tools.d.ts +29 -0
  36. package/dist/tools/generation-tools.d.ts.map +1 -0
  37. package/dist/tools/generation-tools.js +62 -0
  38. package/dist/tools/generation-tools.js.map +1 -0
  39. package/dist/tools/model-tools.d.ts +72 -0
  40. package/dist/tools/model-tools.d.ts.map +1 -0
  41. package/dist/tools/model-tools.js +186 -0
  42. package/dist/tools/model-tools.js.map +1 -0
  43. package/dist/tools/query-tools.d.ts +170 -0
  44. package/dist/tools/query-tools.d.ts.map +1 -0
  45. package/dist/tools/query-tools.js +322 -0
  46. package/dist/tools/query-tools.js.map +1 -0
  47. package/dist/tools/relationship-tools.d.ts +68 -0
  48. package/dist/tools/relationship-tools.d.ts.map +1 -0
  49. package/dist/tools/relationship-tools.js +178 -0
  50. package/dist/tools/relationship-tools.js.map +1 -0
  51. 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