eventmodeler 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ import { createStateChangeSlice } from './slices/create-state-change-slice/index
35
35
  import { createAutomationSlice } from './slices/create-automation-slice/index.js';
36
36
  import { createStateViewSlice } from './slices/create-state-view-slice/index.js';
37
37
  import { createFlow } from './slices/create-flow/index.js';
38
+ import { codegenSlice } from './slices/codegen-slice/index.js';
38
39
  const args = process.argv.slice(2);
39
40
  function getNamedArg(argList, ...names) {
40
41
  for (let i = 0; i < argList.length; i++) {
@@ -94,12 +95,14 @@ COMMANDS:
94
95
  create state-change-slice --xml <data>
95
96
  Create a state-change slice (Screen → Command → Event)
96
97
  create automation-slice --xml <data>
97
- Create an automation slice (Processor → Command → Event)
98
+ Create an automation slice (ReadModel → Processor → Command → Event)
98
99
  create state-view-slice --xml <data>
99
100
  Create a state-view slice (Read Model)
100
101
  create flow --from <source> --to <target>
101
102
  Create a flow between elements (Event→ReadModel, ReadModel→Screen/Processor)
102
103
 
104
+ codegen slice <name> Generate code-ready JSON for a slice (includes dependencies, mappings, scenarios)
105
+
103
106
  summary Show model summary statistics
104
107
 
105
108
  export json Export entire model as JSON
@@ -476,6 +479,24 @@ async function main() {
476
479
  process.exit(1);
477
480
  }
478
481
  break;
482
+ case 'codegen':
483
+ switch (subcommand) {
484
+ case 'slice': {
485
+ const sliceName = filteredArgs[2];
486
+ if (!sliceName) {
487
+ console.error('Error: slice name is required');
488
+ console.error('Usage: eventmodeler codegen slice <name>');
489
+ process.exit(1);
490
+ }
491
+ codegenSlice(model, sliceName);
492
+ break;
493
+ }
494
+ default:
495
+ console.error(`Unknown codegen target: ${subcommand}`);
496
+ console.error('Valid targets: slice');
497
+ process.exit(1);
498
+ }
499
+ break;
479
500
  default:
480
501
  console.error(`Unknown command: ${command}`);
481
502
  printHelp();
@@ -24,6 +24,12 @@ export declare const STATE_CHANGE_SLICE: {
24
24
  export declare const AUTOMATION_SLICE: {
25
25
  width: number;
26
26
  height: number;
27
+ readModel: {
28
+ width: number;
29
+ height: number;
30
+ offsetX: number;
31
+ offsetY: number;
32
+ };
27
33
  processor: {
28
34
  width: number;
29
35
  height: number;
@@ -7,11 +7,12 @@ export const STATE_CHANGE_SLICE = {
7
7
  event: { width: 160, height: 100, offsetX: 360, offsetY: 860 },
8
8
  };
9
9
  export const AUTOMATION_SLICE = {
10
- width: 560,
10
+ width: 880,
11
11
  height: 1000,
12
- processor: { width: 120, height: 120, offsetX: 40, offsetY: 120 },
13
- command: { width: 160, height: 100, offsetX: 180, offsetY: 460 },
14
- event: { width: 160, height: 100, offsetX: 360, offsetY: 860 },
12
+ readModel: { width: 160, height: 100, offsetX: 40, offsetY: 460 },
13
+ processor: { width: 120, height: 120, offsetX: 280, offsetY: 120 },
14
+ command: { width: 160, height: 100, offsetX: 440, offsetY: 460 },
15
+ event: { width: 160, height: 100, offsetX: 680, offsetY: 860 },
15
16
  };
16
17
  export const STATE_VIEW_SLICE = {
17
18
  width: 380,
@@ -0,0 +1,2 @@
1
+ import type { EventModel } from '../../types.js';
2
+ export declare function codegenSlice(model: EventModel, sliceName: string): void;
@@ -0,0 +1,384 @@
1
+ import { outputJson } from '../../lib/format.js';
2
+ import { findElementOrExit } from '../../lib/element-lookup.js';
3
+ // Get components inside a slice by checking if center point is within bounds
4
+ function getSliceComponents(model, slice) {
5
+ const bounds = {
6
+ left: slice.position.x,
7
+ right: slice.position.x + slice.size.width,
8
+ top: slice.position.y,
9
+ bottom: slice.position.y + slice.size.height,
10
+ };
11
+ function isInSlice(pos, width, height) {
12
+ const centerX = pos.x + width / 2;
13
+ const centerY = pos.y + height / 2;
14
+ return centerX >= bounds.left && centerX <= bounds.right && centerY >= bounds.top && centerY <= bounds.bottom;
15
+ }
16
+ return {
17
+ commands: [...model.commands.values()].filter(c => isInSlice(c.position, c.width, c.height)),
18
+ events: [...model.events.values()].filter(e => isInSlice(e.position, e.width, e.height)),
19
+ readModels: [...model.readModels.values()].filter(rm => isInSlice(rm.position, rm.width, rm.height)),
20
+ screens: [...model.screens.values()].filter(s => isInSlice(s.position, s.width, s.height)),
21
+ processors: [...model.processors.values()].filter(p => isInSlice(p.position, p.width, p.height)),
22
+ };
23
+ }
24
+ // Find which slice contains a node
25
+ function findSliceForNode(model, nodeId) {
26
+ const event = model.events.get(nodeId);
27
+ const readModel = model.readModels.get(nodeId);
28
+ const screen = model.screens.get(nodeId);
29
+ const command = model.commands.get(nodeId);
30
+ const processor = model.processors.get(nodeId);
31
+ const node = event ?? readModel ?? screen ?? command ?? processor;
32
+ if (!node)
33
+ return null;
34
+ for (const slice of model.slices.values()) {
35
+ const centerX = node.position.x + node.width / 2;
36
+ const centerY = node.position.y + node.height / 2;
37
+ if (centerX >= slice.position.x &&
38
+ centerX <= slice.position.x + slice.size.width &&
39
+ centerY >= slice.position.y &&
40
+ centerY <= slice.position.y + slice.size.height) {
41
+ return slice;
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+ // Find which aggregate an event belongs to
47
+ function findAggregateForEvent(model, event) {
48
+ const centerX = event.position.x + event.width / 2;
49
+ const centerY = event.position.y + event.height / 2;
50
+ for (const aggregate of model.aggregates.values()) {
51
+ if (centerX >= aggregate.position.x &&
52
+ centerX <= aggregate.position.x + aggregate.size.width &&
53
+ centerY >= aggregate.position.y &&
54
+ centerY <= aggregate.position.y + aggregate.size.height) {
55
+ return aggregate.name;
56
+ }
57
+ }
58
+ return undefined;
59
+ }
60
+ // Find which actor a screen belongs to
61
+ function findActorForScreen(model, screen) {
62
+ const centerX = screen.position.x + screen.width / 2;
63
+ const centerY = screen.position.y + screen.height / 2;
64
+ for (const actor of model.actors.values()) {
65
+ if (centerX >= actor.position.x &&
66
+ centerX <= actor.position.x + actor.size.width &&
67
+ centerY >= actor.position.y &&
68
+ centerY <= actor.position.y + actor.size.height) {
69
+ return actor.name;
70
+ }
71
+ }
72
+ return undefined;
73
+ }
74
+ // Convert Field to JSON-friendly format
75
+ function fieldToJson(field) {
76
+ const result = {
77
+ name: field.name,
78
+ type: field.fieldType,
79
+ };
80
+ if (field.isList)
81
+ result.list = true;
82
+ if (field.isGenerated)
83
+ result.generated = true;
84
+ if (field.isOptional)
85
+ result.optional = true;
86
+ if (field.isUserInput)
87
+ result.userInput = true;
88
+ if (field.subfields && field.subfields.length > 0) {
89
+ result.subfields = field.subfields.map(fieldToJson);
90
+ }
91
+ return result;
92
+ }
93
+ // Determine slice type based on components, returns null for invalid configurations
94
+ function determineSliceType(hasProcessors, hasCommands, hasEvents, hasReadModels, hasScreens) {
95
+ // AUTOMATION: Must have a Processor (and typically ReadModel, Command, Event)
96
+ if (hasProcessors)
97
+ return 'AUTOMATION';
98
+ // STATE_CHANGE: Must have both Command and Event (typically with Screen)
99
+ if (hasCommands && hasEvents)
100
+ return 'STATE_CHANGE';
101
+ // STATE_VIEW: Must have at least one ReadModel
102
+ if (hasReadModels && !hasCommands && !hasEvents)
103
+ return 'STATE_VIEW';
104
+ // Invalid configurations:
105
+ // - Empty slice
106
+ // - Only Command (no Event)
107
+ // - Only Event (no Command)
108
+ // - Only Screen (no Command)
109
+ return null;
110
+ }
111
+ // Get element type for an ID
112
+ function getElementType(model, id) {
113
+ if (model.commands.has(id))
114
+ return 'COMMAND';
115
+ if (model.events.has(id))
116
+ return 'EVENT';
117
+ if (model.readModels.has(id))
118
+ return 'READMODEL';
119
+ if (model.screens.has(id))
120
+ return 'SCREEN';
121
+ if (model.processors.has(id))
122
+ return 'PROCESSOR';
123
+ return null;
124
+ }
125
+ // Get element name for an ID
126
+ function getElementName(model, id) {
127
+ return (model.commands.get(id)?.name ??
128
+ model.events.get(id)?.name ??
129
+ model.readModels.get(id)?.name ??
130
+ model.screens.get(id)?.name ??
131
+ model.processors.get(id)?.name ??
132
+ 'Unknown');
133
+ }
134
+ // Get element fields for an ID
135
+ function getElementFields(model, id) {
136
+ return (model.commands.get(id)?.fields ??
137
+ model.events.get(id)?.fields ??
138
+ model.readModels.get(id)?.fields ??
139
+ model.screens.get(id)?.fields ??
140
+ model.processors.get(id)?.fields ??
141
+ []);
142
+ }
143
+ // Find a field by ID within an element's fields
144
+ function findFieldById(fields, fieldId) {
145
+ for (const field of fields) {
146
+ if (field.id === fieldId)
147
+ return field;
148
+ if (field.subfields) {
149
+ const found = findFieldById(field.subfields, fieldId);
150
+ if (found)
151
+ return found;
152
+ }
153
+ }
154
+ return undefined;
155
+ }
156
+ // Get element reference (type, id, name)
157
+ function getElementRef(model, id) {
158
+ return {
159
+ type: getElementType(model, id) ?? 'EVENT',
160
+ id,
161
+ name: getElementName(model, id),
162
+ };
163
+ }
164
+ // Get element with full schema
165
+ function getElementWithSchema(model, id) {
166
+ const type = getElementType(model, id) ?? 'EVENT';
167
+ const name = getElementName(model, id);
168
+ const fields = getElementFields(model, id).map(fieldToJson);
169
+ const result = { type, id, name, fields };
170
+ // Add aggregate info for events
171
+ const event = model.events.get(id);
172
+ if (event) {
173
+ const aggregate = findAggregateForEvent(model, event);
174
+ if (aggregate)
175
+ result.aggregate = aggregate;
176
+ const originSlice = findSliceForNode(model, id);
177
+ if (originSlice)
178
+ result.originSlice = originSlice.name;
179
+ }
180
+ // Add actor info for screens
181
+ const screen = model.screens.get(id);
182
+ if (screen) {
183
+ const actor = findActorForScreen(model, screen);
184
+ if (actor)
185
+ result.actor = actor;
186
+ }
187
+ return result;
188
+ }
189
+ // Enrich field mappings with field names
190
+ function enrichFieldMappings(model, flow) {
191
+ const sourceFields = getElementFields(model, flow.sourceId);
192
+ const targetFields = getElementFields(model, flow.targetId);
193
+ return flow.fieldMappings.map(mapping => {
194
+ const sourceField = findFieldById(sourceFields, mapping.sourceFieldId);
195
+ const targetField = findFieldById(targetFields, mapping.targetFieldId);
196
+ return {
197
+ sourceFieldId: mapping.sourceFieldId,
198
+ sourceFieldName: sourceField?.name ?? 'unknown',
199
+ targetFieldId: mapping.targetFieldId,
200
+ targetFieldName: targetField?.name ?? 'unknown',
201
+ };
202
+ });
203
+ }
204
+ // Get inbound dependencies (flows where target is in slice but source is not)
205
+ function getInboundDependencies(model, componentIds) {
206
+ return [...model.flows.values()]
207
+ .filter(f => componentIds.has(f.targetId) && !componentIds.has(f.sourceId))
208
+ .map(flow => ({
209
+ flowId: flow.id,
210
+ flowType: flow.flowType,
211
+ source: getElementWithSchema(model, flow.sourceId),
212
+ target: getElementRef(model, flow.targetId),
213
+ fieldMappings: enrichFieldMappings(model, flow),
214
+ }));
215
+ }
216
+ // Get internal flows (both source and target are in slice)
217
+ function getInternalFlows(model, componentIds) {
218
+ return [...model.flows.values()]
219
+ .filter(f => componentIds.has(f.sourceId) && componentIds.has(f.targetId))
220
+ .map(flow => ({
221
+ flowId: flow.id,
222
+ flowType: flow.flowType,
223
+ source: getElementRef(model, flow.sourceId),
224
+ target: getElementRef(model, flow.targetId),
225
+ fieldMappings: enrichFieldMappings(model, flow),
226
+ }));
227
+ }
228
+ // Format scenario then clause
229
+ function formatScenarioThen(model, then) {
230
+ if (then.type === 'error') {
231
+ return {
232
+ type: 'error',
233
+ errorType: then.errorType,
234
+ errorMessage: then.errorMessage,
235
+ };
236
+ }
237
+ if (then.type === 'events' && then.expectedEvents) {
238
+ return {
239
+ type: 'events',
240
+ expectedEvents: then.expectedEvents.map(expected => {
241
+ const event = model.events.get(expected.eventStickyId);
242
+ return {
243
+ eventId: expected.eventStickyId,
244
+ eventName: event?.name ?? 'UnknownEvent',
245
+ eventSchema: { fields: event?.fields.map(fieldToJson) ?? [] },
246
+ fieldValues: expected.fieldValues ?? {},
247
+ };
248
+ }),
249
+ };
250
+ }
251
+ if (then.type === 'readModelAssertion' && then.readModelAssertion) {
252
+ const rm = model.readModels.get(then.readModelAssertion.readModelStickyId);
253
+ return {
254
+ type: 'readModelAssertion',
255
+ readModelName: rm?.name ?? 'UnknownReadModel',
256
+ expectedFieldValues: then.readModelAssertion.expectedFieldValues,
257
+ };
258
+ }
259
+ // Default fallback
260
+ return { type: 'events', expectedEvents: [] };
261
+ }
262
+ // Format scenarios with resolved schemas
263
+ function formatScenarios(model, scenarios) {
264
+ return scenarios.map(scenario => ({
265
+ id: scenario.id,
266
+ name: scenario.name,
267
+ description: scenario.description,
268
+ given: scenario.givenEvents.map(ref => {
269
+ const event = model.events.get(ref.eventStickyId);
270
+ const originSlice = findSliceForNode(model, ref.eventStickyId);
271
+ return {
272
+ eventId: ref.eventStickyId,
273
+ eventName: event?.name ?? 'UnknownEvent',
274
+ originSlice: originSlice?.name ?? null,
275
+ eventSchema: { fields: event?.fields.map(fieldToJson) ?? [] },
276
+ fieldValues: ref.fieldValues ?? {},
277
+ };
278
+ }),
279
+ when: scenario.whenCommand
280
+ ? {
281
+ commandId: scenario.whenCommand.commandStickyId,
282
+ commandName: model.commands.get(scenario.whenCommand.commandStickyId)?.name ?? 'UnknownCommand',
283
+ fieldValues: scenario.whenCommand.fieldValues ?? {},
284
+ }
285
+ : null,
286
+ then: formatScenarioThen(model, scenario.then),
287
+ }));
288
+ }
289
+ export function codegenSlice(model, sliceName) {
290
+ // 1. Find the slice
291
+ const slice = findElementOrExit(model.slices, sliceName, 'slice');
292
+ // 2. Get components inside slice
293
+ const components = getSliceComponents(model, slice);
294
+ // 3. Build component ID set
295
+ const componentIds = new Set();
296
+ components.commands.forEach(c => componentIds.add(c.id));
297
+ components.events.forEach(e => componentIds.add(e.id));
298
+ components.readModels.forEach(rm => componentIds.add(rm.id));
299
+ components.screens.forEach(s => componentIds.add(s.id));
300
+ components.processors.forEach(p => componentIds.add(p.id));
301
+ // 4. Determine slice type
302
+ const sliceType = determineSliceType(components.processors.length > 0, components.commands.length > 0, components.events.length > 0, components.readModels.length > 0, components.screens.length > 0);
303
+ // Validate slice type - exit with error for invalid configurations
304
+ if (sliceType === null) {
305
+ const hasAny = componentIds.size > 0;
306
+ if (!hasAny) {
307
+ console.error(`Error: Slice "${slice.name}" is empty - no elements found inside slice bounds`);
308
+ }
309
+ else {
310
+ const parts = [];
311
+ if (components.commands.length > 0)
312
+ parts.push(`${components.commands.length} command(s)`);
313
+ if (components.events.length > 0)
314
+ parts.push(`${components.events.length} event(s)`);
315
+ if (components.screens.length > 0)
316
+ parts.push(`${components.screens.length} screen(s)`);
317
+ if (components.readModels.length > 0)
318
+ parts.push(`${components.readModels.length} read model(s)`);
319
+ console.error(`Error: Slice "${slice.name}" has invalid configuration for codegen: ${parts.join(', ')}`);
320
+ console.error('Valid configurations:');
321
+ console.error(' AUTOMATION: Processor + ReadModel + Command + Event');
322
+ console.error(' STATE_CHANGE: Screen + Command + Event');
323
+ console.error(' STATE_VIEW: ReadModel only');
324
+ }
325
+ process.exit(1);
326
+ }
327
+ // 5. Get inbound dependencies with full schemas
328
+ const inboundDependencies = getInboundDependencies(model, componentIds);
329
+ // 6. Get internal flows with field mappings
330
+ const internalFlows = getInternalFlows(model, componentIds);
331
+ // 7. Get scenarios for this slice
332
+ const scenarios = [...model.scenarios.values()].filter(s => s.sliceId === slice.id);
333
+ // 8. Build output
334
+ const output = {
335
+ sliceType,
336
+ slice: {
337
+ id: slice.id,
338
+ name: slice.name,
339
+ },
340
+ elements: {
341
+ readModels: components.readModels.map(rm => ({
342
+ id: rm.id,
343
+ name: rm.name,
344
+ fields: rm.fields.map(fieldToJson),
345
+ })),
346
+ processors: components.processors.map(p => ({
347
+ id: p.id,
348
+ name: p.name,
349
+ })),
350
+ commands: components.commands.map(cmd => ({
351
+ id: cmd.id,
352
+ name: cmd.name,
353
+ fields: cmd.fields.map(fieldToJson),
354
+ })),
355
+ events: components.events.map(evt => {
356
+ const result = {
357
+ id: evt.id,
358
+ name: evt.name,
359
+ fields: evt.fields.map(fieldToJson),
360
+ };
361
+ const aggregate = findAggregateForEvent(model, evt);
362
+ if (aggregate)
363
+ result.aggregate = aggregate;
364
+ return result;
365
+ }),
366
+ screens: components.screens.map(scr => {
367
+ const result = {
368
+ id: scr.id,
369
+ name: scr.name,
370
+ fields: scr.fields.map(fieldToJson),
371
+ };
372
+ const actor = findActorForScreen(model, scr);
373
+ if (actor)
374
+ result.actor = actor;
375
+ return result;
376
+ }),
377
+ },
378
+ inboundDependencies,
379
+ internalFlows,
380
+ scenarios: formatScenarios(model, scenarios),
381
+ };
382
+ // 9. Output JSON
383
+ outputJson(output);
384
+ }
@@ -16,6 +16,16 @@ function parseXmlInput(xml) {
16
16
  }
17
17
  const after = getAttr(sliceMatch[1], 'after');
18
18
  const before = getAttr(sliceMatch[1], 'before');
19
+ // Parse read-model
20
+ const readModelMatch = xml.match(/<read-model([^>]*)>([\s\S]*?)<\/read-model>/);
21
+ if (!readModelMatch) {
22
+ throw new Error('Invalid XML: missing <read-model> element');
23
+ }
24
+ const readModelName = getAttr(readModelMatch[1], 'name');
25
+ if (!readModelName) {
26
+ throw new Error('Invalid XML: read-model must have a name attribute');
27
+ }
28
+ const readModelFields = parseFieldsFromXml(readModelMatch[2]);
19
29
  // Parse processor (no fields)
20
30
  const processorMatch = xml.match(/<processor([^>]*?)(?:\/>|>([\s\S]*?)<\/processor>)/);
21
31
  if (!processorMatch) {
@@ -49,6 +59,7 @@ function parseXmlInput(xml) {
49
59
  sliceName,
50
60
  after,
51
61
  before,
62
+ readModel: { name: readModelName, fields: readModelFields },
52
63
  processor: { name: processorName },
53
64
  command: { name: commandName, fields: commandFields },
54
65
  event: { name: eventName, fields: eventFields },
@@ -99,17 +110,25 @@ export function createAutomationSlice(model, filePath, xmlInput) {
99
110
  }
100
111
  // Generate IDs
101
112
  const sliceId = crypto.randomUUID();
113
+ const readModelId = crypto.randomUUID();
102
114
  const processorId = crypto.randomUUID();
103
115
  const commandId = crypto.randomUUID();
104
116
  const eventId = crypto.randomUUID();
117
+ const readModelToProcessorFlowId = crypto.randomUUID();
105
118
  const processorToCommandFlowId = crypto.randomUUID();
106
119
  const commandToEventFlowId = crypto.randomUUID();
107
120
  // Convert field inputs to fields with IDs
121
+ const readModelFields = input.readModel.fields.map(fieldInputToField);
108
122
  const commandFields = input.command.fields.map(fieldInputToField);
109
123
  const eventFields = input.event.fields.map(fieldInputToField);
110
- // Infer field mappings (processor has no fields, so only command->event)
124
+ // Infer field mappings
125
+ const readModelToCommandMappings = inferFieldMappings(readModelFields, commandFields);
111
126
  const commandToEventMappings = inferFieldMappings(commandFields, eventFields);
112
127
  // Calculate absolute positions for components within the slice
128
+ const readModelPosition = {
129
+ x: slicePosition.x + AUTOMATION_SLICE.readModel.offsetX,
130
+ y: slicePosition.y + AUTOMATION_SLICE.readModel.offsetY,
131
+ };
113
132
  const processorPosition = {
114
133
  x: slicePosition.x + AUTOMATION_SLICE.processor.offsetX,
115
134
  y: slicePosition.y + AUTOMATION_SLICE.processor.offsetY,
@@ -132,7 +151,26 @@ export function createAutomationSlice(model, filePath, xmlInput) {
132
151
  size: { width: AUTOMATION_SLICE.width, height: AUTOMATION_SLICE.height },
133
152
  timestamp: Date.now(),
134
153
  });
135
- // 2. Create the processor
154
+ // 2. Create the read model
155
+ appendEvent(filePath, {
156
+ type: 'ReadModelStickyPlaced',
157
+ readModelStickyId: readModelId,
158
+ name: input.readModel.name,
159
+ position: readModelPosition,
160
+ width: AUTOMATION_SLICE.readModel.width,
161
+ height: AUTOMATION_SLICE.readModel.height,
162
+ timestamp: Date.now(),
163
+ });
164
+ // 3. Add read model fields
165
+ for (const field of readModelFields) {
166
+ appendEvent(filePath, {
167
+ type: 'ReadModelFieldAdded',
168
+ readModelStickyId: readModelId,
169
+ field,
170
+ timestamp: Date.now(),
171
+ });
172
+ }
173
+ // 4. Create the processor
136
174
  appendEvent(filePath, {
137
175
  type: 'ProcessorPlaced',
138
176
  processorId,
@@ -142,7 +180,7 @@ export function createAutomationSlice(model, filePath, xmlInput) {
142
180
  height: AUTOMATION_SLICE.processor.height,
143
181
  timestamp: Date.now(),
144
182
  });
145
- // 3. Create the command
183
+ // 5. Create the command
146
184
  appendEvent(filePath, {
147
185
  type: 'CommandStickyPlaced',
148
186
  commandStickyId: commandId,
@@ -152,7 +190,7 @@ export function createAutomationSlice(model, filePath, xmlInput) {
152
190
  height: AUTOMATION_SLICE.command.height,
153
191
  timestamp: Date.now(),
154
192
  });
155
- // 4. Add command fields
193
+ // 6. Add command fields
156
194
  for (const field of commandFields) {
157
195
  appendEvent(filePath, {
158
196
  type: 'CommandFieldAdded',
@@ -161,7 +199,7 @@ export function createAutomationSlice(model, filePath, xmlInput) {
161
199
  timestamp: Date.now(),
162
200
  });
163
201
  }
164
- // 5. Create the event
202
+ // 7. Create the event
165
203
  appendEvent(filePath, {
166
204
  type: 'EventStickyPlaced',
167
205
  eventStickyId: eventId,
@@ -171,7 +209,7 @@ export function createAutomationSlice(model, filePath, xmlInput) {
171
209
  height: AUTOMATION_SLICE.event.height,
172
210
  timestamp: Date.now(),
173
211
  });
174
- // 6. Add event fields
212
+ // 8. Add event fields
175
213
  for (const field of eventFields) {
176
214
  appendEvent(filePath, {
177
215
  type: 'EventFieldAdded',
@@ -180,7 +218,26 @@ export function createAutomationSlice(model, filePath, xmlInput) {
180
218
  timestamp: Date.now(),
181
219
  });
182
220
  }
183
- // 7. Create processor -> command flow
221
+ // 9. Create read model -> processor flow
222
+ appendEvent(filePath, {
223
+ type: 'ReadModelToProcessorFlowSpecified',
224
+ flowId: readModelToProcessorFlowId,
225
+ readModelStickyId: readModelId,
226
+ processorId,
227
+ sourceHandle: 'top-source',
228
+ targetHandle: 'bottom-target',
229
+ timestamp: Date.now(),
230
+ });
231
+ // 10. Add read model -> command field mappings
232
+ if (readModelToCommandMappings.length > 0) {
233
+ appendEvent(filePath, {
234
+ type: 'FieldMappingSpecified',
235
+ flowId: readModelToProcessorFlowId,
236
+ mappings: readModelToCommandMappings,
237
+ timestamp: Date.now(),
238
+ });
239
+ }
240
+ // 11. Create processor -> command flow
184
241
  appendEvent(filePath, {
185
242
  type: 'ProcessorToCommandFlowSpecified',
186
243
  flowId: processorToCommandFlowId,
@@ -190,7 +247,7 @@ export function createAutomationSlice(model, filePath, xmlInput) {
190
247
  targetHandle: 'top-target',
191
248
  timestamp: Date.now(),
192
249
  });
193
- // 8. Create command -> event flow
250
+ // 12. Create command -> event flow
194
251
  appendEvent(filePath, {
195
252
  type: 'CommandToEventFlowSpecified',
196
253
  flowId: commandToEventFlowId,
@@ -200,7 +257,7 @@ export function createAutomationSlice(model, filePath, xmlInput) {
200
257
  targetHandle: 'top-target',
201
258
  timestamp: Date.now(),
202
259
  });
203
- // 9. Add command -> event field mappings
260
+ // 13. Add command -> event field mappings
204
261
  if (commandToEventMappings.length > 0) {
205
262
  appendEvent(filePath, {
206
263
  type: 'FieldMappingSpecified',
@@ -210,8 +267,10 @@ export function createAutomationSlice(model, filePath, xmlInput) {
210
267
  });
211
268
  }
212
269
  console.log(`Created automation slice "${input.sliceName}"`);
270
+ console.log(` ReadModel: ${input.readModel.name} (${readModelFields.length} fields)`);
213
271
  console.log(` Processor: ${input.processor.name}`);
214
272
  console.log(` Command: ${input.command.name} (${commandFields.length} fields)`);
215
273
  console.log(` Event: ${input.event.name} (${eventFields.length} fields)`);
274
+ console.log(` ReadModel -> Command mappings: ${readModelToCommandMappings.length}`);
216
275
  console.log(` Command -> Event mappings: ${commandToEventMappings.length}`);
217
276
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eventmodeler",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
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": {