eventmodeler 0.2.5 → 0.2.7

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,399 @@
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
+ // Find which chapter contains a slice (based on horizontal center)
75
+ function findChapterForSlice(model, slice) {
76
+ const sliceCenterX = slice.position.x + slice.size.width / 2;
77
+ for (const chapter of model.chapters.values()) {
78
+ const chapterLeft = chapter.position.x;
79
+ const chapterRight = chapter.position.x + chapter.size.width;
80
+ if (sliceCenterX >= chapterLeft && sliceCenterX <= chapterRight) {
81
+ return chapter;
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+ // Convert Field to JSON-friendly format
87
+ function fieldToJson(field) {
88
+ const result = {
89
+ name: field.name,
90
+ type: field.fieldType,
91
+ };
92
+ if (field.isList)
93
+ result.list = true;
94
+ if (field.isGenerated)
95
+ result.generated = true;
96
+ if (field.isOptional)
97
+ result.optional = true;
98
+ if (field.isUserInput)
99
+ result.userInput = true;
100
+ if (field.subfields && field.subfields.length > 0) {
101
+ result.subfields = field.subfields.map(fieldToJson);
102
+ }
103
+ return result;
104
+ }
105
+ // Determine slice type based on components, returns null for invalid configurations
106
+ function determineSliceType(hasProcessors, hasCommands, hasEvents, hasReadModels, hasScreens) {
107
+ // AUTOMATION: Must have a Processor (and typically ReadModel, Command, Event)
108
+ if (hasProcessors)
109
+ return 'AUTOMATION';
110
+ // STATE_CHANGE: Must have both Command and Event (typically with Screen)
111
+ if (hasCommands && hasEvents)
112
+ return 'STATE_CHANGE';
113
+ // STATE_VIEW: Must have at least one ReadModel
114
+ if (hasReadModels && !hasCommands && !hasEvents)
115
+ return 'STATE_VIEW';
116
+ // Invalid configurations:
117
+ // - Empty slice
118
+ // - Only Command (no Event)
119
+ // - Only Event (no Command)
120
+ // - Only Screen (no Command)
121
+ return null;
122
+ }
123
+ // Get element type for an ID
124
+ function getElementType(model, id) {
125
+ if (model.commands.has(id))
126
+ return 'COMMAND';
127
+ if (model.events.has(id))
128
+ return 'EVENT';
129
+ if (model.readModels.has(id))
130
+ return 'READMODEL';
131
+ if (model.screens.has(id))
132
+ return 'SCREEN';
133
+ if (model.processors.has(id))
134
+ return 'PROCESSOR';
135
+ return null;
136
+ }
137
+ // Get element name for an ID
138
+ function getElementName(model, id) {
139
+ return (model.commands.get(id)?.name ??
140
+ model.events.get(id)?.name ??
141
+ model.readModels.get(id)?.name ??
142
+ model.screens.get(id)?.name ??
143
+ model.processors.get(id)?.name ??
144
+ 'Unknown');
145
+ }
146
+ // Get element fields for an ID
147
+ function getElementFields(model, id) {
148
+ return (model.commands.get(id)?.fields ??
149
+ model.events.get(id)?.fields ??
150
+ model.readModels.get(id)?.fields ??
151
+ model.screens.get(id)?.fields ??
152
+ model.processors.get(id)?.fields ??
153
+ []);
154
+ }
155
+ // Find a field by ID within an element's fields
156
+ function findFieldById(fields, fieldId) {
157
+ for (const field of fields) {
158
+ if (field.id === fieldId)
159
+ return field;
160
+ if (field.subfields) {
161
+ const found = findFieldById(field.subfields, fieldId);
162
+ if (found)
163
+ return found;
164
+ }
165
+ }
166
+ return undefined;
167
+ }
168
+ // Get element reference (type, id, name)
169
+ function getElementRef(model, id) {
170
+ return {
171
+ type: getElementType(model, id) ?? 'EVENT',
172
+ id,
173
+ name: getElementName(model, id),
174
+ };
175
+ }
176
+ // Get element with full schema
177
+ function getElementWithSchema(model, id) {
178
+ const type = getElementType(model, id) ?? 'EVENT';
179
+ const name = getElementName(model, id);
180
+ const fields = getElementFields(model, id).map(fieldToJson);
181
+ const result = { type, id, name, fields };
182
+ // Add aggregate info for events
183
+ const event = model.events.get(id);
184
+ if (event) {
185
+ const aggregate = findAggregateForEvent(model, event);
186
+ if (aggregate)
187
+ result.aggregate = aggregate;
188
+ const originSlice = findSliceForNode(model, id);
189
+ if (originSlice)
190
+ result.originSlice = originSlice.name;
191
+ }
192
+ // Add actor info for screens
193
+ const screen = model.screens.get(id);
194
+ if (screen) {
195
+ const actor = findActorForScreen(model, screen);
196
+ if (actor)
197
+ result.actor = actor;
198
+ }
199
+ return result;
200
+ }
201
+ // Enrich field mappings with field names
202
+ function enrichFieldMappings(model, flow) {
203
+ const sourceFields = getElementFields(model, flow.sourceId);
204
+ const targetFields = getElementFields(model, flow.targetId);
205
+ return flow.fieldMappings.map(mapping => {
206
+ const sourceField = findFieldById(sourceFields, mapping.sourceFieldId);
207
+ const targetField = findFieldById(targetFields, mapping.targetFieldId);
208
+ return {
209
+ sourceFieldId: mapping.sourceFieldId,
210
+ sourceFieldName: sourceField?.name ?? 'unknown',
211
+ targetFieldId: mapping.targetFieldId,
212
+ targetFieldName: targetField?.name ?? 'unknown',
213
+ };
214
+ });
215
+ }
216
+ // Get inbound dependencies (flows where target is in slice but source is not)
217
+ function getInboundDependencies(model, componentIds) {
218
+ return [...model.flows.values()]
219
+ .filter(f => componentIds.has(f.targetId) && !componentIds.has(f.sourceId))
220
+ .map(flow => ({
221
+ flowId: flow.id,
222
+ flowType: flow.flowType,
223
+ source: getElementWithSchema(model, flow.sourceId),
224
+ target: getElementRef(model, flow.targetId),
225
+ fieldMappings: enrichFieldMappings(model, flow),
226
+ }));
227
+ }
228
+ // Get internal flows (both source and target are in slice)
229
+ function getInternalFlows(model, componentIds) {
230
+ return [...model.flows.values()]
231
+ .filter(f => componentIds.has(f.sourceId) && componentIds.has(f.targetId))
232
+ .map(flow => ({
233
+ flowId: flow.id,
234
+ flowType: flow.flowType,
235
+ source: getElementRef(model, flow.sourceId),
236
+ target: getElementRef(model, flow.targetId),
237
+ fieldMappings: enrichFieldMappings(model, flow),
238
+ }));
239
+ }
240
+ // Format scenario then clause
241
+ function formatScenarioThen(model, then) {
242
+ if (then.type === 'error') {
243
+ return {
244
+ type: 'error',
245
+ errorType: then.errorType,
246
+ errorMessage: then.errorMessage,
247
+ };
248
+ }
249
+ if (then.type === 'events' && then.expectedEvents) {
250
+ return {
251
+ type: 'events',
252
+ expectedEvents: then.expectedEvents.map(expected => {
253
+ const event = model.events.get(expected.eventStickyId);
254
+ return {
255
+ eventId: expected.eventStickyId,
256
+ eventName: event?.name ?? 'UnknownEvent',
257
+ eventSchema: { fields: event?.fields.map(fieldToJson) ?? [] },
258
+ fieldValues: expected.fieldValues ?? {},
259
+ };
260
+ }),
261
+ };
262
+ }
263
+ if (then.type === 'readModelAssertion' && then.readModelAssertion) {
264
+ const rm = model.readModels.get(then.readModelAssertion.readModelStickyId);
265
+ return {
266
+ type: 'readModelAssertion',
267
+ readModelName: rm?.name ?? 'UnknownReadModel',
268
+ expectedFieldValues: then.readModelAssertion.expectedFieldValues,
269
+ };
270
+ }
271
+ // Default fallback
272
+ return { type: 'events', expectedEvents: [] };
273
+ }
274
+ // Format scenarios with resolved schemas
275
+ function formatScenarios(model, scenarios) {
276
+ return scenarios.map(scenario => ({
277
+ id: scenario.id,
278
+ name: scenario.name,
279
+ description: scenario.description,
280
+ given: scenario.givenEvents.map(ref => {
281
+ const event = model.events.get(ref.eventStickyId);
282
+ const originSlice = findSliceForNode(model, ref.eventStickyId);
283
+ return {
284
+ eventId: ref.eventStickyId,
285
+ eventName: event?.name ?? 'UnknownEvent',
286
+ originSlice: originSlice?.name ?? null,
287
+ eventSchema: { fields: event?.fields.map(fieldToJson) ?? [] },
288
+ fieldValues: ref.fieldValues ?? {},
289
+ };
290
+ }),
291
+ when: scenario.whenCommand
292
+ ? {
293
+ commandId: scenario.whenCommand.commandStickyId,
294
+ commandName: model.commands.get(scenario.whenCommand.commandStickyId)?.name ?? 'UnknownCommand',
295
+ fieldValues: scenario.whenCommand.fieldValues ?? {},
296
+ }
297
+ : null,
298
+ then: formatScenarioThen(model, scenario.then),
299
+ }));
300
+ }
301
+ export function codegenSlice(model, sliceName) {
302
+ // 1. Find the slice
303
+ const slice = findElementOrExit(model.slices, sliceName, 'slice');
304
+ // 2. Get components inside slice
305
+ const components = getSliceComponents(model, slice);
306
+ // 3. Build component ID set
307
+ const componentIds = new Set();
308
+ components.commands.forEach(c => componentIds.add(c.id));
309
+ components.events.forEach(e => componentIds.add(e.id));
310
+ components.readModels.forEach(rm => componentIds.add(rm.id));
311
+ components.screens.forEach(s => componentIds.add(s.id));
312
+ components.processors.forEach(p => componentIds.add(p.id));
313
+ // 4. Determine slice type
314
+ const sliceType = determineSliceType(components.processors.length > 0, components.commands.length > 0, components.events.length > 0, components.readModels.length > 0, components.screens.length > 0);
315
+ // Validate slice type - exit with error for invalid configurations
316
+ if (sliceType === null) {
317
+ const hasAny = componentIds.size > 0;
318
+ if (!hasAny) {
319
+ console.error(`Error: Slice "${slice.name}" is empty - no elements found inside slice bounds`);
320
+ }
321
+ else {
322
+ const parts = [];
323
+ if (components.commands.length > 0)
324
+ parts.push(`${components.commands.length} command(s)`);
325
+ if (components.events.length > 0)
326
+ parts.push(`${components.events.length} event(s)`);
327
+ if (components.screens.length > 0)
328
+ parts.push(`${components.screens.length} screen(s)`);
329
+ if (components.readModels.length > 0)
330
+ parts.push(`${components.readModels.length} read model(s)`);
331
+ console.error(`Error: Slice "${slice.name}" has invalid configuration for codegen: ${parts.join(', ')}`);
332
+ console.error('Valid configurations:');
333
+ console.error(' AUTOMATION: Processor + ReadModel + Command + Event');
334
+ console.error(' STATE_CHANGE: Screen + Command + Event');
335
+ console.error(' STATE_VIEW: ReadModel only');
336
+ }
337
+ process.exit(1);
338
+ }
339
+ // 5. Get inbound dependencies with full schemas
340
+ const inboundDependencies = getInboundDependencies(model, componentIds);
341
+ // 6. Get internal flows with field mappings
342
+ const internalFlows = getInternalFlows(model, componentIds);
343
+ // 7. Get scenarios for this slice
344
+ const scenarios = [...model.scenarios.values()].filter(s => s.sliceId === slice.id);
345
+ // 8. Find which chapter this slice belongs to
346
+ const chapter = findChapterForSlice(model, slice);
347
+ // 9. Build output
348
+ const output = {
349
+ sliceType,
350
+ slice: {
351
+ id: slice.id,
352
+ name: slice.name,
353
+ },
354
+ ...(chapter && { chapter: { id: chapter.id, name: chapter.name } }),
355
+ elements: {
356
+ readModels: components.readModels.map(rm => ({
357
+ id: rm.id,
358
+ name: rm.name,
359
+ fields: rm.fields.map(fieldToJson),
360
+ })),
361
+ processors: components.processors.map(p => ({
362
+ id: p.id,
363
+ name: p.name,
364
+ })),
365
+ commands: components.commands.map(cmd => ({
366
+ id: cmd.id,
367
+ name: cmd.name,
368
+ fields: cmd.fields.map(fieldToJson),
369
+ })),
370
+ events: components.events.map(evt => {
371
+ const result = {
372
+ id: evt.id,
373
+ name: evt.name,
374
+ fields: evt.fields.map(fieldToJson),
375
+ };
376
+ const aggregate = findAggregateForEvent(model, evt);
377
+ if (aggregate)
378
+ result.aggregate = aggregate;
379
+ return result;
380
+ }),
381
+ screens: components.screens.map(scr => {
382
+ const result = {
383
+ id: scr.id,
384
+ name: scr.name,
385
+ fields: scr.fields.map(fieldToJson),
386
+ };
387
+ const actor = findActorForScreen(model, scr);
388
+ if (actor)
389
+ result.actor = actor;
390
+ return result;
391
+ }),
392
+ },
393
+ inboundDependencies,
394
+ internalFlows,
395
+ scenarios: formatScenarios(model, scenarios),
396
+ };
397
+ // 10. Output JSON
398
+ outputJson(output);
399
+ }
@@ -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.7",
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": {