eventmodeler 0.1.0 → 0.2.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.
@@ -0,0 +1,216 @@
1
+ import { appendEvent } from '../../lib/file-loader.js';
2
+ function flattenFields(fields, prefix = '') {
3
+ const result = [];
4
+ for (const field of fields) {
5
+ const path = prefix ? `${prefix}.${field.name}` : field.name;
6
+ result.push({ id: field.id, path, field });
7
+ if (field.fieldType === 'Custom' && field.subfields) {
8
+ result.push(...flattenFields(field.subfields, path));
9
+ }
10
+ }
11
+ return result;
12
+ }
13
+ function parseJsonInput(input) {
14
+ const parsed = JSON.parse(input);
15
+ if (!Array.isArray(parsed)) {
16
+ return [parsed];
17
+ }
18
+ return parsed;
19
+ }
20
+ function parseXmlInput(input) {
21
+ const mappings = [];
22
+ const mappingMatches = input.matchAll(/<mapping[^>]*from="([^"]*)"[^>]*to="([^"]*)"[^>]*\/?>/g);
23
+ for (const match of mappingMatches) {
24
+ mappings.push({ from: match[1], to: match[2] });
25
+ }
26
+ // Also try alternate attribute order
27
+ const altMatches = input.matchAll(/<mapping[^>]*to="([^"]*)"[^>]*from="([^"]*)"[^>]*\/?>/g);
28
+ for (const match of altMatches) {
29
+ // Check if this mapping was already added
30
+ const exists = mappings.some(m => m.from === match[2] && m.to === match[1]);
31
+ if (!exists) {
32
+ mappings.push({ from: match[2], to: match[1] });
33
+ }
34
+ }
35
+ if (mappings.length === 0) {
36
+ throw new Error('No <mapping from="..." to="..."/> elements found in XML');
37
+ }
38
+ return mappings;
39
+ }
40
+ function parseInput(input) {
41
+ const trimmed = input.trim();
42
+ if (trimmed.startsWith('<')) {
43
+ return parseXmlInput(trimmed);
44
+ }
45
+ return parseJsonInput(trimmed);
46
+ }
47
+ export function mapFields(model, filePath, flowIdentifier, input) {
48
+ // Parse the mapping input
49
+ let mappingInputs;
50
+ try {
51
+ mappingInputs = parseInput(input);
52
+ }
53
+ catch (err) {
54
+ console.error(`Error: Invalid input format: ${err.message}`);
55
+ process.exit(1);
56
+ }
57
+ // Find the flow - can be identified by:
58
+ // 1. Flow ID directly
59
+ // 2. "SourceName→TargetName" format (using → or ->)
60
+ let flow = model.flows.get(flowIdentifier);
61
+ if (!flow) {
62
+ // Try to parse as "Source→Target" or "Source->Target"
63
+ const arrowMatch = flowIdentifier.match(/^(.+?)(?:→|->)(.+)$/);
64
+ if (arrowMatch) {
65
+ const sourceName = arrowMatch[1].trim().toLowerCase();
66
+ const targetName = arrowMatch[2].trim().toLowerCase();
67
+ // Find matching flow
68
+ for (const f of model.flows.values()) {
69
+ let sourceEntityName = '';
70
+ let targetEntityName = '';
71
+ // Get source name
72
+ const sourceEvent = model.events.get(f.sourceId);
73
+ const sourceReadModel = model.readModels.get(f.sourceId);
74
+ const sourceScreen = model.screens.get(f.sourceId);
75
+ const sourceProcessor = model.processors.get(f.sourceId);
76
+ const sourceCommand = model.commands.get(f.sourceId);
77
+ if (sourceEvent)
78
+ sourceEntityName = sourceEvent.name.toLowerCase();
79
+ else if (sourceReadModel)
80
+ sourceEntityName = sourceReadModel.name.toLowerCase();
81
+ else if (sourceScreen)
82
+ sourceEntityName = sourceScreen.name.toLowerCase();
83
+ else if (sourceProcessor)
84
+ sourceEntityName = sourceProcessor.name.toLowerCase();
85
+ else if (sourceCommand)
86
+ sourceEntityName = sourceCommand.name.toLowerCase();
87
+ // Get target name
88
+ const targetEvent = model.events.get(f.targetId);
89
+ const targetReadModel = model.readModels.get(f.targetId);
90
+ const targetScreen = model.screens.get(f.targetId);
91
+ const targetProcessor = model.processors.get(f.targetId);
92
+ const targetCommand = model.commands.get(f.targetId);
93
+ if (targetEvent)
94
+ targetEntityName = targetEvent.name.toLowerCase();
95
+ else if (targetReadModel)
96
+ targetEntityName = targetReadModel.name.toLowerCase();
97
+ else if (targetScreen)
98
+ targetEntityName = targetScreen.name.toLowerCase();
99
+ else if (targetProcessor)
100
+ targetEntityName = targetProcessor.name.toLowerCase();
101
+ else if (targetCommand)
102
+ targetEntityName = targetCommand.name.toLowerCase();
103
+ // Check for match (exact or partial)
104
+ const sourceMatches = sourceEntityName === sourceName || sourceEntityName.includes(sourceName);
105
+ const targetMatches = targetEntityName === targetName || targetEntityName.includes(targetName);
106
+ if (sourceMatches && targetMatches) {
107
+ flow = f;
108
+ break;
109
+ }
110
+ }
111
+ }
112
+ }
113
+ if (!flow) {
114
+ console.error(`Error: Flow not found: ${flowIdentifier}`);
115
+ console.error('Available flows:');
116
+ for (const f of model.flows.values()) {
117
+ const source = getEntityName(model, f.sourceId);
118
+ const target = getEntityName(model, f.targetId);
119
+ console.error(` - ${source}→${target} (${f.id})`);
120
+ }
121
+ process.exit(1);
122
+ }
123
+ // Get source and target fields
124
+ const sourceFields = getEntityFields(model, flow.sourceId);
125
+ const targetFields = getEntityFields(model, flow.targetId);
126
+ const flatSource = flattenFields(sourceFields);
127
+ const flatTarget = flattenFields(targetFields);
128
+ // Resolve mapping inputs to field IDs
129
+ const mappings = [];
130
+ for (const m of mappingInputs) {
131
+ // Find source field (by name/path or ID)
132
+ const sourceField = flatSource.find(sf => sf.id === m.from || sf.path.toLowerCase() === m.from.toLowerCase());
133
+ if (!sourceField) {
134
+ console.error(`Error: Source field not found: ${m.from}`);
135
+ console.error('Available source fields:');
136
+ for (const sf of flatSource) {
137
+ console.error(` - ${sf.path} (${sf.id})`);
138
+ }
139
+ process.exit(1);
140
+ }
141
+ // Find target field (by name/path or ID)
142
+ const targetField = flatTarget.find(tf => tf.id === m.to || tf.path.toLowerCase() === m.to.toLowerCase());
143
+ if (!targetField) {
144
+ console.error(`Error: Target field not found: ${m.to}`);
145
+ console.error('Available target fields:');
146
+ for (const tf of flatTarget) {
147
+ console.error(` - ${tf.path} (${tf.id})`);
148
+ }
149
+ process.exit(1);
150
+ }
151
+ mappings.push({
152
+ sourceFieldId: sourceField.id,
153
+ targetFieldId: targetField.id,
154
+ });
155
+ }
156
+ // Get existing mappings and merge
157
+ const existingMappings = flow.fieldMappings ?? [];
158
+ const mergedMappings = [...existingMappings];
159
+ for (const newMapping of mappings) {
160
+ // Remove any existing mapping for the same target field
161
+ const existingIndex = mergedMappings.findIndex(m => m.targetFieldId === newMapping.targetFieldId);
162
+ if (existingIndex >= 0) {
163
+ mergedMappings.splice(existingIndex, 1);
164
+ }
165
+ mergedMappings.push(newMapping);
166
+ }
167
+ // Append the event
168
+ appendEvent(filePath, {
169
+ type: 'FieldMappingSpecified',
170
+ flowId: flow.id,
171
+ mappings: mergedMappings,
172
+ timestamp: Date.now(),
173
+ });
174
+ const sourceName = getEntityName(model, flow.sourceId);
175
+ const targetName = getEntityName(model, flow.targetId);
176
+ console.log(`Mapped ${mappings.length} field(s) on flow ${sourceName}→${targetName}`);
177
+ for (const m of mappingInputs) {
178
+ console.log(` ${m.from} → ${m.to}`);
179
+ }
180
+ }
181
+ function getEntityName(model, entityId) {
182
+ const event = model.events.get(entityId);
183
+ if (event)
184
+ return event.name;
185
+ const readModel = model.readModels.get(entityId);
186
+ if (readModel)
187
+ return readModel.name;
188
+ const command = model.commands.get(entityId);
189
+ if (command)
190
+ return command.name;
191
+ const screen = model.screens.get(entityId);
192
+ if (screen)
193
+ return screen.name;
194
+ const processor = model.processors.get(entityId);
195
+ if (processor)
196
+ return processor.name;
197
+ return entityId;
198
+ }
199
+ function getEntityFields(model, entityId) {
200
+ const event = model.events.get(entityId);
201
+ if (event)
202
+ return event.fields;
203
+ const readModel = model.readModels.get(entityId);
204
+ if (readModel)
205
+ return readModel.fields;
206
+ const command = model.commands.get(entityId);
207
+ if (command)
208
+ return command.fields;
209
+ const screen = model.screens.get(entityId);
210
+ if (screen)
211
+ return screen.fields;
212
+ const processor = model.processors.get(entityId);
213
+ if (processor)
214
+ return processor.fields;
215
+ return [];
216
+ }
@@ -0,0 +1,6 @@
1
+ import type { EventModel } from '../../types.js';
2
+ export declare function removeField(model: EventModel, filePath: string, options: {
3
+ command?: string;
4
+ event?: string;
5
+ readModel?: string;
6
+ }, fieldName: string): void;
@@ -0,0 +1,127 @@
1
+ import { appendEvent } from '../../lib/file-loader.js';
2
+ export function removeField(model, filePath, options, fieldName) {
3
+ // Determine which entity type
4
+ const entityCount = [options.command, options.event, options.readModel].filter(Boolean).length;
5
+ if (entityCount === 0) {
6
+ console.error('Error: Must specify one of --command, --event, or --read-model');
7
+ process.exit(1);
8
+ }
9
+ if (entityCount > 1) {
10
+ console.error('Error: Can only specify one of --command, --event, or --read-model');
11
+ process.exit(1);
12
+ }
13
+ if (options.command) {
14
+ removeFieldFromCommand(model, filePath, options.command, fieldName);
15
+ }
16
+ else if (options.event) {
17
+ removeFieldFromEvent(model, filePath, options.event, fieldName);
18
+ }
19
+ else if (options.readModel) {
20
+ removeFieldFromReadModel(model, filePath, options.readModel, fieldName);
21
+ }
22
+ }
23
+ function removeFieldFromCommand(model, filePath, commandName, fieldName) {
24
+ const nameLower = commandName.toLowerCase();
25
+ const commands = [...model.commands.values()];
26
+ const command = commands.find(c => c.name.toLowerCase() === nameLower || c.name.toLowerCase().includes(nameLower));
27
+ if (!command) {
28
+ console.error(`Error: Command not found: ${commandName}`);
29
+ console.error('Available commands:');
30
+ for (const c of commands) {
31
+ console.error(` - ${c.name}`);
32
+ }
33
+ process.exit(1);
34
+ }
35
+ const fieldNameLower = fieldName.toLowerCase();
36
+ const field = command.fields.find(f => f.name.toLowerCase() === fieldNameLower);
37
+ if (!field) {
38
+ console.error(`Error: Field "${fieldName}" not found on command "${command.name}"`);
39
+ if (command.fields.length > 0) {
40
+ console.error('Available fields:');
41
+ for (const f of command.fields) {
42
+ console.error(` - ${f.name} (${f.fieldType})`);
43
+ }
44
+ }
45
+ else {
46
+ console.error('This command has no fields.');
47
+ }
48
+ process.exit(1);
49
+ }
50
+ appendEvent(filePath, {
51
+ type: 'CommandFieldRemoved',
52
+ commandStickyId: command.id,
53
+ fieldId: field.id,
54
+ timestamp: Date.now(),
55
+ });
56
+ console.log(`Removed field "${field.name}" from command "${command.name}"`);
57
+ }
58
+ function removeFieldFromEvent(model, filePath, eventName, fieldName) {
59
+ const nameLower = eventName.toLowerCase();
60
+ const events = [...model.events.values()];
61
+ const event = events.find(e => e.name.toLowerCase() === nameLower || e.name.toLowerCase().includes(nameLower));
62
+ if (!event) {
63
+ console.error(`Error: Event not found: ${eventName}`);
64
+ console.error('Available events:');
65
+ for (const e of events) {
66
+ console.error(` - ${e.name}`);
67
+ }
68
+ process.exit(1);
69
+ }
70
+ const fieldNameLower = fieldName.toLowerCase();
71
+ const field = event.fields.find(f => f.name.toLowerCase() === fieldNameLower);
72
+ if (!field) {
73
+ console.error(`Error: Field "${fieldName}" not found on event "${event.name}"`);
74
+ if (event.fields.length > 0) {
75
+ console.error('Available fields:');
76
+ for (const f of event.fields) {
77
+ console.error(` - ${f.name} (${f.fieldType})`);
78
+ }
79
+ }
80
+ else {
81
+ console.error('This event has no fields.');
82
+ }
83
+ process.exit(1);
84
+ }
85
+ appendEvent(filePath, {
86
+ type: 'EventFieldRemoved',
87
+ eventStickyId: event.id,
88
+ fieldId: field.id,
89
+ timestamp: Date.now(),
90
+ });
91
+ console.log(`Removed field "${field.name}" from event "${event.name}"`);
92
+ }
93
+ function removeFieldFromReadModel(model, filePath, readModelName, fieldName) {
94
+ const nameLower = readModelName.toLowerCase();
95
+ const readModels = [...model.readModels.values()];
96
+ const readModel = readModels.find(rm => rm.name.toLowerCase() === nameLower || rm.name.toLowerCase().includes(nameLower));
97
+ if (!readModel) {
98
+ console.error(`Error: Read model not found: ${readModelName}`);
99
+ console.error('Available read models:');
100
+ for (const rm of readModels) {
101
+ console.error(` - ${rm.name}`);
102
+ }
103
+ process.exit(1);
104
+ }
105
+ const fieldNameLower = fieldName.toLowerCase();
106
+ const field = readModel.fields.find(f => f.name.toLowerCase() === fieldNameLower);
107
+ if (!field) {
108
+ console.error(`Error: Field "${fieldName}" not found on read model "${readModel.name}"`);
109
+ if (readModel.fields.length > 0) {
110
+ console.error('Available fields:');
111
+ for (const f of readModel.fields) {
112
+ console.error(` - ${f.name} (${f.fieldType})`);
113
+ }
114
+ }
115
+ else {
116
+ console.error('This read model has no fields.');
117
+ }
118
+ process.exit(1);
119
+ }
120
+ appendEvent(filePath, {
121
+ type: 'ReadModelFieldRemoved',
122
+ readModelStickyId: readModel.id,
123
+ fieldId: field.id,
124
+ timestamp: Date.now(),
125
+ });
126
+ console.log(`Removed field "${field.name}" from read model "${readModel.name}"`);
127
+ }
@@ -0,0 +1,2 @@
1
+ import type { EventModel } from '../../types.js';
2
+ export declare function removeScenario(model: EventModel, filePath: string, scenarioName: string, sliceName?: string): void;
@@ -0,0 +1,39 @@
1
+ import { appendEvent } from '../../lib/file-loader.js';
2
+ export function removeScenario(model, filePath, scenarioName, sliceName) {
3
+ const scenarioNameLower = scenarioName.toLowerCase();
4
+ // Find scenarios matching the name
5
+ let matchingScenarios = [...model.scenarios.values()].filter(s => s.name.toLowerCase() === scenarioNameLower || s.name.toLowerCase().includes(scenarioNameLower));
6
+ // If slice name provided, filter to that slice
7
+ if (sliceName && matchingScenarios.length > 0) {
8
+ const sliceNameLower = sliceName.toLowerCase();
9
+ const slice = [...model.slices.values()].find(s => s.name.toLowerCase() === sliceNameLower || s.name.toLowerCase().includes(sliceNameLower));
10
+ if (slice) {
11
+ matchingScenarios = matchingScenarios.filter(s => s.sliceId === slice.id);
12
+ }
13
+ }
14
+ if (matchingScenarios.length === 0) {
15
+ console.error(`Error: Scenario not found: ${scenarioName}`);
16
+ console.error('Available scenarios:');
17
+ for (const s of model.scenarios.values()) {
18
+ const slice = model.slices.get(s.sliceId);
19
+ console.error(` - "${s.name}" (in slice "${slice?.name ?? 'unknown'}")`);
20
+ }
21
+ process.exit(1);
22
+ }
23
+ if (matchingScenarios.length > 1) {
24
+ console.error(`Error: Multiple scenarios match "${scenarioName}". Use --slice to disambiguate:`);
25
+ for (const s of matchingScenarios) {
26
+ const slice = model.slices.get(s.sliceId);
27
+ console.error(` - "${s.name}" in slice "${slice?.name ?? 'unknown'}"`);
28
+ }
29
+ process.exit(1);
30
+ }
31
+ const scenario = matchingScenarios[0];
32
+ const slice = model.slices.get(scenario.sliceId);
33
+ appendEvent(filePath, {
34
+ type: 'ScenarioRemoved',
35
+ scenarioId: scenario.id,
36
+ timestamp: Date.now(),
37
+ });
38
+ console.log(`Removed scenario "${scenario.name}" from slice "${slice?.name ?? 'unknown'}"`);
39
+ }
@@ -0,0 +1,3 @@
1
+ import type { EventModel } from '../../types.js';
2
+ export declare function showActor(model: EventModel, actorName: string): void;
3
+ export declare function listActors(model: EventModel): void;
@@ -0,0 +1,89 @@
1
+ function escapeXml(str) {
2
+ return str
3
+ .replace(/&/g, '&amp;')
4
+ .replace(/</g, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/"/g, '&quot;');
7
+ }
8
+ // Get screens whose center point is inside the actor bounds
9
+ function getScreensInActor(model, actor) {
10
+ const bounds = {
11
+ left: actor.position.x,
12
+ right: actor.position.x + actor.size.width,
13
+ top: actor.position.y,
14
+ bottom: actor.position.y + actor.size.height,
15
+ };
16
+ function isInActor(pos, width, height) {
17
+ const centerX = pos.x + width / 2;
18
+ const centerY = pos.y + height / 2;
19
+ return centerX >= bounds.left && centerX <= bounds.right && centerY >= bounds.top && centerY <= bounds.bottom;
20
+ }
21
+ return [...model.screens.values()].filter(s => isInActor(s.position, s.width, s.height));
22
+ }
23
+ // Find which slice contains a screen
24
+ function findSliceForScreen(model, screen) {
25
+ for (const slice of model.slices.values()) {
26
+ const centerX = screen.position.x + screen.width / 2;
27
+ const centerY = screen.position.y + screen.height / 2;
28
+ if (centerX >= slice.position.x &&
29
+ centerX <= slice.position.x + slice.size.width &&
30
+ centerY >= slice.position.y &&
31
+ centerY <= slice.position.y + slice.size.height) {
32
+ return slice.name;
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ export function showActor(model, actorName) {
38
+ // Find the actor
39
+ const nameLower = actorName.toLowerCase();
40
+ const actors = [...model.actors.values()];
41
+ const actor = actors.find(a => a.name.toLowerCase() === nameLower || a.name.toLowerCase().includes(nameLower));
42
+ if (!actor) {
43
+ console.error(`Error: Actor not found: ${actorName}`);
44
+ console.error('Available actors:');
45
+ for (const a of actors) {
46
+ console.error(` - ${a.name}`);
47
+ }
48
+ process.exit(1);
49
+ }
50
+ // Get screens dynamically based on position (center point inside actor bounds)
51
+ const screensInActor = getScreensInActor(model, actor);
52
+ console.log(`<actor id="${actor.id}" name="${escapeXml(actor.name)}">`);
53
+ console.log(` <screens-count>${screensInActor.length}</screens-count>`);
54
+ if (screensInActor.length > 0) {
55
+ console.log(' <screens>');
56
+ for (const screen of screensInActor) {
57
+ const fieldCount = screen.fields.length;
58
+ if (screen.originalNodeId) {
59
+ // This is a linked copy - show origin info
60
+ const original = model.screens.get(screen.originalNodeId);
61
+ const originSlice = original ? findSliceForScreen(model, original) : null;
62
+ const originAttr = originSlice ? ` origin-slice="${escapeXml(originSlice)}"` : '';
63
+ console.log(` <screen id="${screen.id}" name="${escapeXml(screen.name)}" fields="${fieldCount}" linked-copy="true"${originAttr}/>`);
64
+ }
65
+ else {
66
+ console.log(` <screen id="${screen.id}" name="${escapeXml(screen.name)}" fields="${fieldCount}"/>`);
67
+ }
68
+ }
69
+ console.log(' </screens>');
70
+ }
71
+ console.log('</actor>');
72
+ }
73
+ export function listActors(model) {
74
+ const actors = [...model.actors.values()];
75
+ if (actors.length === 0) {
76
+ console.log('<actors/>');
77
+ return;
78
+ }
79
+ console.log('<actors>');
80
+ for (const actor of actors) {
81
+ // Calculate screens dynamically based on position
82
+ const screensInActor = getScreensInActor(model, actor);
83
+ const originalCount = screensInActor.filter(s => !s.originalNodeId).length;
84
+ const copyCount = screensInActor.filter(s => s.originalNodeId).length;
85
+ const copiesAttr = copyCount > 0 ? ` copies="${copyCount}"` : '';
86
+ console.log(` <actor id="${actor.id}" name="${escapeXml(actor.name)}" screens="${originalCount}"${copiesAttr}/>`);
87
+ }
88
+ console.log('</actors>');
89
+ }
@@ -0,0 +1,3 @@
1
+ import type { EventModel } from '../../types.js';
2
+ export declare function showAggregateCompleteness(model: EventModel, aggregateName: string): void;
3
+ export declare function listAggregates(model: EventModel): void;
@@ -0,0 +1,139 @@
1
+ function escapeXml(str) {
2
+ return str
3
+ .replace(/&/g, '&amp;')
4
+ .replace(/</g, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/"/g, '&quot;');
7
+ }
8
+ function findMatchingField(fields, targetName, targetType) {
9
+ for (const field of fields) {
10
+ // Check if name and type match
11
+ if (field.name.toLowerCase() === targetName.toLowerCase() && field.fieldType === targetType) {
12
+ return field;
13
+ }
14
+ // Check nested subfields
15
+ if (field.fieldType === 'Custom' && field.subfields) {
16
+ const nested = findMatchingField(field.subfields, targetName, targetType);
17
+ if (nested)
18
+ return nested;
19
+ }
20
+ }
21
+ return undefined;
22
+ }
23
+ // Get events whose center point is inside the aggregate bounds
24
+ // Only returns original events, not linked copies (linked copies have originalNodeId set)
25
+ function getEventsInAggregate(model, aggregate) {
26
+ const bounds = {
27
+ left: aggregate.position.x,
28
+ right: aggregate.position.x + aggregate.size.width,
29
+ top: aggregate.position.y,
30
+ bottom: aggregate.position.y + aggregate.size.height,
31
+ };
32
+ function isInAggregate(pos, width, height) {
33
+ const centerX = pos.x + width / 2;
34
+ const centerY = pos.y + height / 2;
35
+ return centerX >= bounds.left && centerX <= bounds.right && centerY >= bounds.top && centerY <= bounds.bottom;
36
+ }
37
+ // Filter out linked copies - only return original events
38
+ // Linked copies have originalNodeId set, originals do not
39
+ return [...model.events.values()].filter(e => !e.originalNodeId && isInAggregate(e.position, e.width, e.height));
40
+ }
41
+ export function showAggregateCompleteness(model, aggregateName) {
42
+ // Find the aggregate
43
+ const nameLower = aggregateName.toLowerCase();
44
+ const aggregates = [...model.aggregates.values()];
45
+ const aggregate = aggregates.find(a => a.name.toLowerCase() === nameLower || a.name.toLowerCase().includes(nameLower));
46
+ if (!aggregate) {
47
+ console.error(`Error: Aggregate not found: ${aggregateName}`);
48
+ console.error('Available aggregates:');
49
+ for (const a of aggregates) {
50
+ console.error(` - ${a.name}`);
51
+ }
52
+ process.exit(1);
53
+ }
54
+ // Get events dynamically based on position (center point inside aggregate bounds)
55
+ const eventsInAggregate = getEventsInAggregate(model, aggregate);
56
+ // Check if aggregate has ID field configured
57
+ if (!aggregate.aggregateIdFieldName || !aggregate.aggregateIdFieldType) {
58
+ console.log(`<aggregate-completeness aggregate="${escapeXml(aggregate.name)}" status="unconfigured">`);
59
+ console.log(` <events-count>${eventsInAggregate.length}</events-count>`);
60
+ console.log(' <events>');
61
+ for (const event of eventsInAggregate) {
62
+ console.log(` <event id="${event.id}" name="${escapeXml(event.name)}"/>`);
63
+ }
64
+ console.log(' </events>');
65
+ console.log(' <message>Aggregate ID field not configured. Set aggregateIdFieldName and aggregateIdFieldType.</message>');
66
+ console.log('</aggregate-completeness>');
67
+ return;
68
+ }
69
+ const idFieldName = aggregate.aggregateIdFieldName;
70
+ const idFieldType = aggregate.aggregateIdFieldType;
71
+ if (eventsInAggregate.length === 0) {
72
+ console.log(`<aggregate-completeness aggregate="${escapeXml(aggregate.name)}" status="empty">`);
73
+ console.log(` <id-field name="${escapeXml(idFieldName)}" type="${idFieldType}"/>`);
74
+ console.log(' <message>No events in this aggregate.</message>');
75
+ console.log('</aggregate-completeness>');
76
+ return;
77
+ }
78
+ // Check each event for the ID field
79
+ const eventResults = [];
80
+ for (const event of eventsInAggregate) {
81
+ const matchingField = findMatchingField(event.fields, idFieldName, idFieldType);
82
+ eventResults.push({
83
+ eventId: event.id,
84
+ eventName: event.name,
85
+ hasIdField: !!matchingField,
86
+ matchingField: matchingField ? {
87
+ fieldId: matchingField.id,
88
+ fieldName: matchingField.name,
89
+ fieldType: matchingField.fieldType,
90
+ } : undefined,
91
+ });
92
+ }
93
+ const completeEvents = eventResults.filter(e => e.hasIdField);
94
+ const incompleteEvents = eventResults.filter(e => !e.hasIdField);
95
+ const isComplete = incompleteEvents.length === 0;
96
+ const overallStatus = isComplete ? 'complete' : 'incomplete';
97
+ const completionPercent = eventResults.length > 0
98
+ ? Math.round((completeEvents.length / eventResults.length) * 100)
99
+ : 100;
100
+ console.log(`<aggregate-completeness aggregate="${escapeXml(aggregate.name)}" status="${overallStatus}" completion="${completionPercent}%">`);
101
+ console.log(` <id-field name="${escapeXml(idFieldName)}" type="${idFieldType}"/>`);
102
+ console.log(` <summary total="${eventResults.length}" complete="${completeEvents.length}" incomplete="${incompleteEvents.length}"/>`);
103
+ if (incompleteEvents.length > 0) {
104
+ console.log(' <incomplete-events>');
105
+ for (const event of incompleteEvents) {
106
+ console.log(` <event id="${event.eventId}" name="${escapeXml(event.eventName)}"/>`);
107
+ }
108
+ console.log(' </incomplete-events>');
109
+ }
110
+ if (completeEvents.length > 0) {
111
+ console.log(' <complete-events>');
112
+ for (const event of completeEvents) {
113
+ const fieldAttr = event.matchingField
114
+ ? ` field-id="${event.matchingField.fieldId}"`
115
+ : '';
116
+ console.log(` <event id="${event.eventId}" name="${escapeXml(event.eventName)}"${fieldAttr}/>`);
117
+ }
118
+ console.log(' </complete-events>');
119
+ }
120
+ console.log('</aggregate-completeness>');
121
+ }
122
+ export function listAggregates(model) {
123
+ const aggregates = [...model.aggregates.values()];
124
+ if (aggregates.length === 0) {
125
+ console.log('<aggregates/>');
126
+ return;
127
+ }
128
+ console.log('<aggregates>');
129
+ for (const aggregate of aggregates) {
130
+ // Calculate events dynamically based on position
131
+ const eventsInAggregate = getEventsInAggregate(model, aggregate);
132
+ const eventCount = eventsInAggregate.length;
133
+ const idFieldAttr = aggregate.aggregateIdFieldName
134
+ ? ` idField="${escapeXml(aggregate.aggregateIdFieldName)}" idType="${aggregate.aggregateIdFieldType ?? 'Unknown'}"`
135
+ : '';
136
+ console.log(` <aggregate id="${aggregate.id}" name="${escapeXml(aggregate.name)}" events="${eventCount}"${idFieldAttr}/>`);
137
+ }
138
+ console.log('</aggregates>');
139
+ }
@@ -27,6 +27,23 @@ function formatFieldXml(field, indent) {
27
27
  }
28
28
  return `${indent}<field ${attrs.join(' ')}/>\n`;
29
29
  }
30
+ // Find which aggregate an event belongs to (center point inside aggregate bounds)
31
+ function findAggregateForEvent(model, event) {
32
+ const centerX = event.position.x + event.width / 2;
33
+ const centerY = event.position.y + event.height / 2;
34
+ for (const aggregate of model.aggregates.values()) {
35
+ const bounds = {
36
+ left: aggregate.position.x,
37
+ right: aggregate.position.x + aggregate.size.width,
38
+ top: aggregate.position.y,
39
+ bottom: aggregate.position.y + aggregate.size.height,
40
+ };
41
+ if (centerX >= bounds.left && centerX <= bounds.right && centerY >= bounds.top && centerY <= bounds.bottom) {
42
+ return aggregate;
43
+ }
44
+ }
45
+ return null;
46
+ }
30
47
  function formatCommandXml(model, command) {
31
48
  let xml = `<command name="${escapeXml(command.name)}">\n`;
32
49
  if (command.fields.length > 0) {
@@ -54,8 +71,11 @@ function formatCommandXml(model, command) {
54
71
  xml += ' <produces>\n';
55
72
  for (const flow of outgoingFlows) {
56
73
  const target = model.events.get(flow.targetId);
57
- if (target)
58
- xml += ` <event name="${escapeXml(target.name)}"/>\n`;
74
+ if (target) {
75
+ const aggregate = findAggregateForEvent(model, target);
76
+ const aggregateAttr = aggregate ? ` aggregate="${escapeXml(aggregate.name)}"` : '';
77
+ xml += ` <event name="${escapeXml(target.name)}"${aggregateAttr}/>\n`;
78
+ }
59
79
  }
60
80
  xml += ' </produces>\n';
61
81
  }
@@ -0,0 +1,2 @@
1
+ import type { EventModel } from '../../types.js';
2
+ export declare function showCompleteness(model: EventModel, readModelName: string): void;