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,180 @@
1
+ function escapeXml(str) {
2
+ return str
3
+ .replace(/&/g, '&')
4
+ .replace(/</g, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/"/g, '&quot;');
7
+ }
8
+ function flattenFields(fields, prefix = '') {
9
+ const result = [];
10
+ for (const field of fields) {
11
+ const path = prefix ? `${prefix}.${field.name}` : field.name;
12
+ result.push({ id: field.id, path, field });
13
+ if (field.fieldType === 'Custom' && field.subfields) {
14
+ result.push(...flattenFields(field.subfields, path));
15
+ }
16
+ }
17
+ return result;
18
+ }
19
+ function calculateFlowCompleteness(model, flow, targetFields, sourceFields) {
20
+ const flatSource = flattenFields(sourceFields);
21
+ const flatTarget = flattenFields(targetFields);
22
+ const manualMappings = flow.fieldMappings ?? [];
23
+ // Get source entity name
24
+ let sourceName = 'Unknown';
25
+ let sourceType = 'unknown';
26
+ const event = model.events.get(flow.sourceId);
27
+ const readModel = model.readModels.get(flow.sourceId);
28
+ const command = model.commands.get(flow.sourceId);
29
+ const screen = model.screens.get(flow.sourceId);
30
+ const processor = model.processors.get(flow.sourceId);
31
+ if (event) {
32
+ sourceName = event.name;
33
+ sourceType = 'event';
34
+ }
35
+ else if (readModel) {
36
+ sourceName = readModel.name;
37
+ sourceType = 'read-model';
38
+ }
39
+ else if (command) {
40
+ sourceName = command.name;
41
+ sourceType = 'command';
42
+ }
43
+ else if (screen) {
44
+ sourceName = screen.name;
45
+ sourceType = 'screen';
46
+ }
47
+ else if (processor) {
48
+ sourceName = processor.name;
49
+ sourceType = 'processor';
50
+ }
51
+ const fieldStatuses = flatTarget.map(({ id, path, field }) => {
52
+ // 1. Check if generated
53
+ if (field.isGenerated) {
54
+ return { fieldId: id, fieldName: path, status: 'generated' };
55
+ }
56
+ // 2. Check if user input
57
+ if (field.isUserInput) {
58
+ return { fieldId: id, fieldName: path, status: 'user-input' };
59
+ }
60
+ // 3. Check manual mapping
61
+ const manualMapping = manualMappings.find(m => m.targetFieldId === id);
62
+ if (manualMapping) {
63
+ const sourceField = flatSource.find(sf => sf.id === manualMapping.sourceFieldId);
64
+ return {
65
+ fieldId: id,
66
+ fieldName: path,
67
+ status: 'satisfied',
68
+ sourceFieldId: manualMapping.sourceFieldId,
69
+ sourceFieldName: sourceField?.path,
70
+ };
71
+ }
72
+ // 4. Check auto-mapping by path
73
+ const autoMatch = flatSource.find(sf => sf.path === path);
74
+ if (autoMatch) {
75
+ return {
76
+ fieldId: id,
77
+ fieldName: path,
78
+ status: 'satisfied',
79
+ sourceFieldId: autoMatch.id,
80
+ sourceFieldName: autoMatch.path,
81
+ };
82
+ }
83
+ // 5. Not satisfied - check if optional
84
+ if (field.isOptional) {
85
+ return { fieldId: id, fieldName: path, status: 'optional-missing' };
86
+ }
87
+ // 6. Unsatisfied required field
88
+ return { fieldId: id, fieldName: path, status: 'unsatisfied' };
89
+ });
90
+ const hasUnsatisfied = fieldStatuses.some(s => s.status === 'unsatisfied');
91
+ const hasOptionalMissing = fieldStatuses.some(s => s.status === 'optional-missing');
92
+ return {
93
+ flowId: flow.id,
94
+ sourceName,
95
+ sourceType,
96
+ isComplete: !hasUnsatisfied,
97
+ hasWarnings: hasOptionalMissing,
98
+ targetFields: fieldStatuses,
99
+ sourceFields: flatSource.map(sf => ({
100
+ id: sf.id,
101
+ name: sf.path,
102
+ type: sf.field.fieldType,
103
+ isList: sf.field.isList,
104
+ })),
105
+ };
106
+ }
107
+ export function showCompleteness(model, readModelName) {
108
+ // Find the read model
109
+ const nameLower = readModelName.toLowerCase();
110
+ const readModels = [...model.readModels.values()];
111
+ const readModel = readModels.find(rm => rm.name.toLowerCase() === nameLower || rm.name.toLowerCase().includes(nameLower));
112
+ if (!readModel) {
113
+ console.error(`Error: Read model not found: ${readModelName}`);
114
+ console.error('Available read models:');
115
+ for (const rm of readModels) {
116
+ console.error(` - ${rm.name}`);
117
+ }
118
+ process.exit(1);
119
+ }
120
+ // Find all flows into this read model (EventToReadModel flows)
121
+ const incomingFlows = [...model.flows.values()].filter(f => f.targetId === readModel.id && f.flowType === 'EventToReadModel');
122
+ if (incomingFlows.length === 0) {
123
+ console.log(`<completeness readModel="${escapeXml(readModel.name)}">`);
124
+ console.log(' <no-flows>No events flow into this read model</no-flows>');
125
+ console.log('</completeness>');
126
+ return;
127
+ }
128
+ // Calculate completeness for each flow
129
+ const completenessResults = [];
130
+ for (const flow of incomingFlows) {
131
+ const sourceEvent = model.events.get(flow.sourceId);
132
+ if (!sourceEvent)
133
+ continue;
134
+ const result = calculateFlowCompleteness(model, flow, readModel.fields, sourceEvent.fields);
135
+ completenessResults.push(result);
136
+ }
137
+ // Output XML
138
+ const overallComplete = completenessResults.every(r => r.isComplete);
139
+ const overallStatus = overallComplete ? 'complete' : 'incomplete';
140
+ console.log(`<completeness readModel="${escapeXml(readModel.name)}" status="${overallStatus}">`);
141
+ for (const result of completenessResults) {
142
+ const flowStatus = result.isComplete ? 'complete' : 'incomplete';
143
+ const warningAttr = result.hasWarnings ? ' hasWarnings="true"' : '';
144
+ console.log(` <flow id="${result.flowId}" from="${escapeXml(result.sourceName)}" type="${result.sourceType}" status="${flowStatus}"${warningAttr}>`);
145
+ // Target fields
146
+ console.log(' <target-fields>');
147
+ for (const field of result.targetFields) {
148
+ if (field.sourceFieldName) {
149
+ console.log(` <field name="${escapeXml(field.fieldName)}" status="${field.status}" source="${escapeXml(field.sourceFieldName)}"/>`);
150
+ }
151
+ else {
152
+ console.log(` <field name="${escapeXml(field.fieldName)}" status="${field.status}"/>`);
153
+ }
154
+ }
155
+ console.log(' </target-fields>');
156
+ // Available source fields (for AI to see what's available)
157
+ console.log(' <available-source-fields>');
158
+ for (const sf of result.sourceFields) {
159
+ const listAttr = sf.isList ? ' isList="true"' : '';
160
+ console.log(` <field id="${sf.id}" name="${escapeXml(sf.name)}" type="${sf.type}"${listAttr}/>`);
161
+ }
162
+ console.log(' </available-source-fields>');
163
+ console.log(' </flow>');
164
+ }
165
+ // Also show the read model's fields with their IDs (needed for mapping)
166
+ console.log(' <read-model-fields>');
167
+ const flatTargetFields = flattenFields(readModel.fields);
168
+ for (const { id, path, field } of flatTargetFields) {
169
+ const attrs = [`id="${id}"`, `name="${escapeXml(path)}"`, `type="${field.fieldType}"`];
170
+ if (field.isList)
171
+ attrs.push('isList="true"');
172
+ if (field.isOptional)
173
+ attrs.push('isOptional="true"');
174
+ if (field.isGenerated)
175
+ attrs.push('isGenerated="true"');
176
+ console.log(` <field ${attrs.join(' ')}/>`);
177
+ }
178
+ console.log(' </read-model-fields>');
179
+ console.log('</completeness>');
180
+ }
@@ -27,8 +27,27 @@ 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 formatEventXml(model, event) {
31
- let xml = `<event name="${escapeXml(event.name)}">\n`;
48
+ const aggregate = findAggregateForEvent(model, event);
49
+ const aggregateAttr = aggregate ? ` aggregate="${escapeXml(aggregate.name)}"` : '';
50
+ let xml = `<event name="${escapeXml(event.name)}"${aggregateAttr}>\n`;
32
51
  if (event.fields.length > 0) {
33
52
  xml += ' <fields>\n';
34
53
  for (const field of event.fields) {
@@ -14,6 +14,8 @@ export function showModelSummary(model) {
14
14
  console.log(` <read-models count="${model.readModels.size}"/>`);
15
15
  console.log(` <screens count="${model.screens.size}"/>`);
16
16
  console.log(` <processors count="${model.processors.size}"/>`);
17
+ console.log(` <aggregates count="${model.aggregates.size}"/>`);
18
+ console.log(` <actors count="${model.actors.size}"/>`);
17
19
  console.log(` <scenarios count="${model.scenarios.size}"/>`);
18
20
  console.log(` <flows count="${model.flows.size}"/>`);
19
21
  console.log('</model>');
@@ -50,6 +50,8 @@ function getSliceComponents(model, slice) {
50
50
  const centerY = pos.y + height / 2;
51
51
  return centerX >= bounds.left && centerX <= bounds.right && centerY >= bounds.top && centerY <= bounds.bottom;
52
52
  }
53
+ // Include all elements in the slice (including linked copies)
54
+ // Linked copies will be marked as such in the output
53
55
  return {
54
56
  commands: [...model.commands.values()].filter(c => isInSlice(c.position, c.width, c.height)),
55
57
  events: [...model.events.values()].filter(e => isInSlice(e.position, e.width, e.height)),
@@ -58,9 +60,100 @@ function getSliceComponents(model, slice) {
58
60
  processors: [...model.processors.values()].filter(p => isInSlice(p.position, p.width, p.height)),
59
61
  };
60
62
  }
63
+ // Find which slice contains the original of a linked copy
64
+ function findSliceForNode(model, nodeId) {
65
+ const event = model.events.get(nodeId);
66
+ const readModel = model.readModels.get(nodeId);
67
+ const screen = model.screens.get(nodeId);
68
+ const node = event ?? readModel ?? screen;
69
+ if (!node)
70
+ return null;
71
+ for (const slice of model.slices.values()) {
72
+ const centerX = node.position.x + node.width / 2;
73
+ const centerY = node.position.y + node.height / 2;
74
+ if (centerX >= slice.position.x &&
75
+ centerX <= slice.position.x + slice.size.width &&
76
+ centerY >= slice.position.y &&
77
+ centerY <= slice.position.y + slice.size.height) {
78
+ return slice;
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+ // Find which aggregate an event belongs to (center point inside aggregate bounds)
84
+ function findAggregateForEvent(model, event) {
85
+ const centerX = event.position.x + event.width / 2;
86
+ const centerY = event.position.y + event.height / 2;
87
+ for (const aggregate of model.aggregates.values()) {
88
+ if (centerX >= aggregate.position.x &&
89
+ centerX <= aggregate.position.x + aggregate.size.width &&
90
+ centerY >= aggregate.position.y &&
91
+ centerY <= aggregate.position.y + aggregate.size.height) {
92
+ return aggregate;
93
+ }
94
+ }
95
+ return null;
96
+ }
97
+ // Find which actor a screen belongs to (center point inside actor bounds)
98
+ function findActorForScreen(model, screen) {
99
+ const centerX = screen.position.x + screen.width / 2;
100
+ const centerY = screen.position.y + screen.height / 2;
101
+ for (const actor of model.actors.values()) {
102
+ if (centerX >= actor.position.x &&
103
+ centerX <= actor.position.x + actor.size.width &&
104
+ centerY >= actor.position.y &&
105
+ centerY <= actor.position.y + actor.size.height) {
106
+ return actor;
107
+ }
108
+ }
109
+ return null;
110
+ }
111
+ // Check if two rectangles overlap
112
+ function rectanglesOverlap(r1, r2) {
113
+ return (r1.x < r2.x + r2.width &&
114
+ r1.x + r1.width > r2.x &&
115
+ r1.y < r2.y + r2.height &&
116
+ r1.y + r1.height > r2.y);
117
+ }
118
+ function getAggregatesForSlice(model, slice) {
119
+ const sliceRect = {
120
+ x: slice.position.x,
121
+ y: slice.position.y,
122
+ width: slice.size.width,
123
+ height: slice.size.height,
124
+ };
125
+ return [...model.aggregates.values()].filter(aggregate => {
126
+ const aggRect = {
127
+ x: aggregate.position.x,
128
+ y: aggregate.position.y,
129
+ width: aggregate.size.width,
130
+ height: aggregate.size.height,
131
+ };
132
+ return rectanglesOverlap(sliceRect, aggRect);
133
+ });
134
+ }
135
+ function getActorsForSlice(model, slice) {
136
+ const sliceRect = {
137
+ x: slice.position.x,
138
+ y: slice.position.y,
139
+ width: slice.size.width,
140
+ height: slice.size.height,
141
+ };
142
+ return [...model.actors.values()].filter(actor => {
143
+ const actorRect = {
144
+ x: actor.position.x,
145
+ y: actor.position.y,
146
+ width: actor.size.width,
147
+ height: actor.size.height,
148
+ };
149
+ return rectanglesOverlap(sliceRect, actorRect);
150
+ });
151
+ }
61
152
  function formatSliceXml(model, slice) {
62
153
  const components = getSliceComponents(model, slice);
63
154
  const scenarios = [...model.scenarios.values()].filter(s => s.sliceId === slice.id);
155
+ const aggregates = getAggregatesForSlice(model, slice);
156
+ const actors = getActorsForSlice(model, slice);
64
157
  const componentIds = new Set();
65
158
  components.commands.forEach(c => componentIds.add(c.id));
66
159
  components.events.forEach(e => componentIds.add(e.id));
@@ -80,7 +173,19 @@ function formatSliceXml(model, slice) {
80
173
  let xml = `<slice name="${escapeXml(slice.name)}" status="${slice.status}">\n`;
81
174
  xml += ' <components>\n';
82
175
  for (const screen of components.screens) {
83
- xml += ` <screen name="${escapeXml(screen.name)}">\n`;
176
+ // Check if this is a linked copy
177
+ const copyAttr = screen.originalNodeId ? ' linked-copy="true"' : '';
178
+ let originAttr = '';
179
+ if (screen.originalNodeId) {
180
+ const originSlice = findSliceForNode(model, screen.originalNodeId);
181
+ if (originSlice) {
182
+ originAttr = ` origin-slice="${escapeXml(originSlice.name)}"`;
183
+ }
184
+ }
185
+ // Check which actor this screen belongs to
186
+ const actor = findActorForScreen(model, screen);
187
+ const actorAttr = actor ? ` actor="${escapeXml(actor.name)}"` : '';
188
+ xml += ` <screen name="${escapeXml(screen.name)}"${copyAttr}${originAttr}${actorAttr}>\n`;
84
189
  if (screen.fields.length > 0) {
85
190
  xml += ' <fields>\n';
86
191
  for (const field of screen.fields) {
@@ -110,7 +215,19 @@ function formatSliceXml(model, slice) {
110
215
  xml += ' </command>\n';
111
216
  }
112
217
  for (const event of components.events) {
113
- xml += ` <event name="${escapeXml(event.name)}">\n`;
218
+ // Check if this is a linked copy
219
+ const copyAttr = event.originalNodeId ? ' linked-copy="true"' : '';
220
+ let originAttr = '';
221
+ if (event.originalNodeId) {
222
+ const originSlice = findSliceForNode(model, event.originalNodeId);
223
+ if (originSlice) {
224
+ originAttr = ` origin-slice="${escapeXml(originSlice.name)}"`;
225
+ }
226
+ }
227
+ // Check which aggregate this event belongs to
228
+ const aggregate = findAggregateForEvent(model, event);
229
+ const aggregateAttr = aggregate ? ` aggregate="${escapeXml(aggregate.name)}"` : '';
230
+ xml += ` <event name="${escapeXml(event.name)}"${copyAttr}${originAttr}${aggregateAttr}>\n`;
114
231
  if (event.fields.length > 0) {
115
232
  xml += ' <fields>\n';
116
233
  for (const field of event.fields) {
@@ -132,7 +249,16 @@ function formatSliceXml(model, slice) {
132
249
  xml += ' </event>\n';
133
250
  }
134
251
  for (const readModel of components.readModels) {
135
- xml += ` <read-model name="${escapeXml(readModel.name)}">\n`;
252
+ // Check if this is a linked copy
253
+ const copyAttr = readModel.originalNodeId ? ' linked-copy="true"' : '';
254
+ let originAttr = '';
255
+ if (readModel.originalNodeId) {
256
+ const originSlice = findSliceForNode(model, readModel.originalNodeId);
257
+ if (originSlice) {
258
+ originAttr = ` origin-slice="${escapeXml(originSlice.name)}"`;
259
+ }
260
+ }
261
+ xml += ` <read-model name="${escapeXml(readModel.name)}"${copyAttr}${originAttr}>\n`;
136
262
  if (readModel.fields.length > 0) {
137
263
  xml += ' <fields>\n';
138
264
  for (const field of readModel.fields) {
@@ -154,6 +280,21 @@ function formatSliceXml(model, slice) {
154
280
  xml += ' </processor>\n';
155
281
  }
156
282
  xml += ' </components>\n';
283
+ if (aggregates.length > 0) {
284
+ xml += ' <aggregates>\n';
285
+ for (const agg of aggregates) {
286
+ const idField = agg.aggregateIdFieldName ? ` id-field="${escapeXml(agg.aggregateIdFieldName)}"` : '';
287
+ xml += ` <aggregate name="${escapeXml(agg.name)}"${idField}/>\n`;
288
+ }
289
+ xml += ' </aggregates>\n';
290
+ }
291
+ if (actors.length > 0) {
292
+ xml += ' <actors>\n';
293
+ for (const actor of actors) {
294
+ xml += ` <actor name="${escapeXml(actor.name)}"/>\n`;
295
+ }
296
+ xml += ' </actors>\n';
297
+ }
157
298
  if (internalFlows.length > 0) {
158
299
  xml += ' <information-flow>\n';
159
300
  for (const flow of internalFlows) {
@@ -0,0 +1,12 @@
1
+ import type { EventModel } from '../../types.js';
2
+ interface UpdateOptions {
3
+ optional?: boolean;
4
+ generated?: boolean;
5
+ type?: string;
6
+ }
7
+ export declare function updateField(model: EventModel, filePath: string, options: {
8
+ command?: string;
9
+ event?: string;
10
+ readModel?: string;
11
+ }, fieldName: string, updates: UpdateOptions): void;
12
+ export {};
@@ -0,0 +1,166 @@
1
+ import { appendEvent } from '../../lib/file-loader.js';
2
+ function findFieldByName(fields, fieldName) {
3
+ const nameLower = fieldName.toLowerCase();
4
+ for (const field of fields) {
5
+ if (field.name.toLowerCase() === nameLower) {
6
+ return field;
7
+ }
8
+ // Check subfields for Custom types
9
+ if (field.fieldType === 'Custom' && field.subfields) {
10
+ // Check for dot notation path (e.g., "address.street")
11
+ if (fieldName.toLowerCase().startsWith(field.name.toLowerCase() + '.')) {
12
+ const subPath = fieldName.slice(field.name.length + 1);
13
+ const subField = findFieldByName(field.subfields, subPath);
14
+ if (subField)
15
+ return subField;
16
+ }
17
+ // Also check direct match in subfields
18
+ const subField = findFieldByName(field.subfields, fieldName);
19
+ if (subField)
20
+ return subField;
21
+ }
22
+ }
23
+ return undefined;
24
+ }
25
+ function createUpdatedField(original, updates) {
26
+ return {
27
+ ...original,
28
+ isOptional: updates.optional !== undefined ? updates.optional : original.isOptional,
29
+ isGenerated: updates.generated !== undefined ? updates.generated : original.isGenerated,
30
+ fieldType: updates.type !== undefined ? updates.type : original.fieldType,
31
+ };
32
+ }
33
+ export function updateField(model, filePath, options, fieldName, updates) {
34
+ // Determine which entity type
35
+ const entityCount = [options.command, options.event, options.readModel].filter(Boolean).length;
36
+ if (entityCount === 0) {
37
+ console.error('Error: Must specify one of --command, --event, or --read-model');
38
+ process.exit(1);
39
+ }
40
+ if (entityCount > 1) {
41
+ console.error('Error: Can only specify one of --command, --event, or --read-model');
42
+ process.exit(1);
43
+ }
44
+ if (options.command) {
45
+ updateCommandField(model, filePath, options.command, fieldName, updates);
46
+ }
47
+ else if (options.event) {
48
+ updateEventField(model, filePath, options.event, fieldName, updates);
49
+ }
50
+ else if (options.readModel) {
51
+ updateReadModelField(model, filePath, options.readModel, fieldName, updates);
52
+ }
53
+ }
54
+ function updateCommandField(model, filePath, commandName, fieldName, updates) {
55
+ const nameLower = commandName.toLowerCase();
56
+ const commands = [...model.commands.values()];
57
+ const command = commands.find(c => c.name.toLowerCase() === nameLower || c.name.toLowerCase().includes(nameLower));
58
+ if (!command) {
59
+ console.error(`Error: Command not found: ${commandName}`);
60
+ console.error('Available commands:');
61
+ for (const c of commands) {
62
+ console.error(` - ${c.name}`);
63
+ }
64
+ process.exit(1);
65
+ }
66
+ const field = findFieldByName(command.fields, fieldName);
67
+ if (!field) {
68
+ console.error(`Error: Field "${fieldName}" not found on command "${command.name}"`);
69
+ if (command.fields.length > 0) {
70
+ console.error('Available fields:');
71
+ for (const f of command.fields) {
72
+ console.error(` - ${f.name}`);
73
+ }
74
+ }
75
+ process.exit(1);
76
+ }
77
+ const updatedField = createUpdatedField(field, updates);
78
+ appendEvent(filePath, {
79
+ type: 'CommandFieldAdjusted',
80
+ commandStickyId: command.id,
81
+ fieldId: field.id,
82
+ field: updatedField,
83
+ timestamp: Date.now(),
84
+ });
85
+ console.log(`Updated field "${field.name}" on command "${command.name}"`);
86
+ logUpdates(updates);
87
+ }
88
+ function updateEventField(model, filePath, eventName, fieldName, updates) {
89
+ const nameLower = eventName.toLowerCase();
90
+ const events = [...model.events.values()];
91
+ const event = events.find(e => e.name.toLowerCase() === nameLower || e.name.toLowerCase().includes(nameLower));
92
+ if (!event) {
93
+ console.error(`Error: Event not found: ${eventName}`);
94
+ console.error('Available events:');
95
+ for (const e of events) {
96
+ console.error(` - ${e.name}`);
97
+ }
98
+ process.exit(1);
99
+ }
100
+ const field = findFieldByName(event.fields, fieldName);
101
+ if (!field) {
102
+ console.error(`Error: Field "${fieldName}" not found on event "${event.name}"`);
103
+ if (event.fields.length > 0) {
104
+ console.error('Available fields:');
105
+ for (const f of event.fields) {
106
+ console.error(` - ${f.name}`);
107
+ }
108
+ }
109
+ process.exit(1);
110
+ }
111
+ const updatedField = createUpdatedField(field, updates);
112
+ appendEvent(filePath, {
113
+ type: 'EventFieldAdjusted',
114
+ eventStickyId: event.id,
115
+ fieldId: field.id,
116
+ field: updatedField,
117
+ timestamp: Date.now(),
118
+ });
119
+ console.log(`Updated field "${field.name}" on event "${event.name}"`);
120
+ logUpdates(updates);
121
+ }
122
+ function updateReadModelField(model, filePath, readModelName, fieldName, updates) {
123
+ const nameLower = readModelName.toLowerCase();
124
+ const readModels = [...model.readModels.values()];
125
+ const readModel = readModels.find(rm => rm.name.toLowerCase() === nameLower || rm.name.toLowerCase().includes(nameLower));
126
+ if (!readModel) {
127
+ console.error(`Error: Read model not found: ${readModelName}`);
128
+ console.error('Available read models:');
129
+ for (const rm of readModels) {
130
+ console.error(` - ${rm.name}`);
131
+ }
132
+ process.exit(1);
133
+ }
134
+ const field = findFieldByName(readModel.fields, fieldName);
135
+ if (!field) {
136
+ console.error(`Error: Field "${fieldName}" not found on read model "${readModel.name}"`);
137
+ if (readModel.fields.length > 0) {
138
+ console.error('Available fields:');
139
+ for (const f of readModel.fields) {
140
+ console.error(` - ${f.name}`);
141
+ }
142
+ }
143
+ process.exit(1);
144
+ }
145
+ const updatedField = createUpdatedField(field, updates);
146
+ appendEvent(filePath, {
147
+ type: 'ReadModelFieldAdjusted',
148
+ readModelStickyId: readModel.id,
149
+ fieldId: field.id,
150
+ field: updatedField,
151
+ timestamp: Date.now(),
152
+ });
153
+ console.log(`Updated field "${field.name}" on read model "${readModel.name}"`);
154
+ logUpdates(updates);
155
+ }
156
+ function logUpdates(updates) {
157
+ if (updates.optional !== undefined) {
158
+ console.log(` isOptional: ${updates.optional}`);
159
+ }
160
+ if (updates.generated !== undefined) {
161
+ console.log(` isGenerated: ${updates.generated}`);
162
+ }
163
+ if (updates.type !== undefined) {
164
+ console.log(` fieldType: ${updates.type}`);
165
+ }
166
+ }
package/dist/types.d.ts CHANGED
@@ -56,6 +56,7 @@ export interface EventSticky {
56
56
  width: number;
57
57
  height: number;
58
58
  canonicalId?: string;
59
+ originalNodeId?: string;
59
60
  }
60
61
  export interface ReadModelSticky {
61
62
  id: string;
@@ -68,6 +69,7 @@ export interface ReadModelSticky {
68
69
  width: number;
69
70
  height: number;
70
71
  canonicalId?: string;
72
+ originalNodeId?: string;
71
73
  }
72
74
  export interface Screen {
73
75
  id: string;
@@ -80,6 +82,7 @@ export interface Screen {
80
82
  width: number;
81
83
  height: number;
82
84
  canonicalId?: string;
85
+ originalNodeId?: string;
83
86
  }
84
87
  export interface Processor {
85
88
  id: string;
@@ -118,6 +121,34 @@ export interface Chapter {
118
121
  height: number;
119
122
  };
120
123
  }
124
+ export interface Aggregate {
125
+ id: string;
126
+ name: string;
127
+ position: {
128
+ x: number;
129
+ y: number;
130
+ };
131
+ size: {
132
+ width: number;
133
+ height: number;
134
+ };
135
+ eventIds: string[];
136
+ aggregateIdFieldName?: string;
137
+ aggregateIdFieldType?: Field['fieldType'];
138
+ }
139
+ export interface Actor {
140
+ id: string;
141
+ name: string;
142
+ position: {
143
+ x: number;
144
+ y: number;
145
+ };
146
+ size: {
147
+ width: number;
148
+ height: number;
149
+ };
150
+ screenIds: string[];
151
+ }
121
152
  export interface Scenario {
122
153
  id: string;
123
154
  sliceId: string;
@@ -151,6 +182,8 @@ export interface EventModel {
151
182
  processors: Map<string, Processor>;
152
183
  slices: Map<string, Slice>;
153
184
  chapters: Map<string, Chapter>;
185
+ aggregates: Map<string, Aggregate>;
186
+ actors: Map<string, Actor>;
154
187
  scenarios: Map<string, Scenario>;
155
188
  flows: Map<string, Flow>;
156
189
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eventmodeler",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "CLI tool for interacting with Event Model files - query, update, and export event models from the terminal",
5
5
  "type": "module",
6
6
  "bin": {