eventmodeler 0.2.3 → 0.2.4
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 +63 -0
- package/dist/lib/element-lookup.d.ts +47 -0
- package/dist/lib/element-lookup.js +86 -0
- package/dist/lib/slice-utils.d.ts +83 -0
- package/dist/lib/slice-utils.js +135 -0
- package/dist/projection.js +132 -0
- package/dist/slices/add-field/index.js +4 -33
- package/dist/slices/add-scenario/index.js +7 -74
- package/dist/slices/create-automation-slice/index.d.ts +2 -0
- package/dist/slices/create-automation-slice/index.js +217 -0
- package/dist/slices/create-flow/index.d.ts +2 -0
- package/dist/slices/create-flow/index.js +177 -0
- package/dist/slices/create-state-change-slice/index.d.ts +2 -0
- package/dist/slices/create-state-change-slice/index.js +239 -0
- package/dist/slices/create-state-view-slice/index.d.ts +2 -0
- package/dist/slices/create-state-view-slice/index.js +120 -0
- package/dist/slices/list-chapters/index.js +2 -2
- package/dist/slices/list-commands/index.js +2 -2
- package/dist/slices/list-events/index.js +3 -2
- package/dist/slices/list-slices/index.js +2 -2
- package/dist/slices/mark-slice-status/index.js +2 -11
- package/dist/slices/remove-field/index.js +4 -33
- package/dist/slices/remove-scenario/index.js +45 -11
- package/dist/slices/show-actor/index.js +2 -11
- package/dist/slices/show-aggregate-completeness/index.js +2 -11
- package/dist/slices/show-chapter/index.js +6 -14
- package/dist/slices/show-command/index.js +4 -12
- package/dist/slices/show-completeness/index.js +108 -19
- package/dist/slices/show-event/index.js +4 -12
- package/dist/slices/show-slice/index.js +14 -17
- package/dist/slices/update-field/index.js +4 -33
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto';
|
|
2
2
|
import { appendEvent } from '../../lib/file-loader.js';
|
|
3
|
+
import { findElementOrExit } from '../../lib/element-lookup.js';
|
|
3
4
|
function parseJsonInput(input) {
|
|
4
5
|
return JSON.parse(input);
|
|
5
6
|
}
|
|
@@ -144,27 +145,12 @@ export function addScenario(model, filePath, sliceName, input) {
|
|
|
144
145
|
process.exit(1);
|
|
145
146
|
}
|
|
146
147
|
// Find slice by name
|
|
147
|
-
const
|
|
148
|
-
const sliceNameLower = sliceName.toLowerCase();
|
|
149
|
-
const slice = slices.find(s => s.name.toLowerCase() === sliceNameLower || s.name.toLowerCase().includes(sliceNameLower));
|
|
150
|
-
if (!slice) {
|
|
151
|
-
console.error(`Error: Slice not found: ${sliceName}`);
|
|
152
|
-
console.error('Available slices:');
|
|
153
|
-
for (const s of slices) {
|
|
154
|
-
console.error(` - ${s.name}`);
|
|
155
|
-
}
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
148
|
+
const slice = findElementOrExit(model.slices, sliceName, 'slice');
|
|
158
149
|
// Resolve event references in given
|
|
159
150
|
const givenEvents = [];
|
|
160
151
|
if (scenarioInput.given) {
|
|
161
152
|
for (const g of scenarioInput.given) {
|
|
162
|
-
const event =
|
|
163
|
-
if (!event) {
|
|
164
|
-
console.error(`Error: Event not found in "given": ${g.event}`);
|
|
165
|
-
listAvailableEvents(model);
|
|
166
|
-
process.exit(1);
|
|
167
|
-
}
|
|
153
|
+
const event = findElementOrExit(model.events, g.event, 'event');
|
|
168
154
|
givenEvents.push({
|
|
169
155
|
eventStickyId: event.id,
|
|
170
156
|
fieldValues: g.fieldValues,
|
|
@@ -174,12 +160,7 @@ export function addScenario(model, filePath, sliceName, input) {
|
|
|
174
160
|
// Resolve command reference in when
|
|
175
161
|
let whenCommand = null;
|
|
176
162
|
if (scenarioInput.when) {
|
|
177
|
-
const command =
|
|
178
|
-
if (!command) {
|
|
179
|
-
console.error(`Error: Command not found in "when": ${scenarioInput.when.command}`);
|
|
180
|
-
listAvailableCommands(model);
|
|
181
|
-
process.exit(1);
|
|
182
|
-
}
|
|
163
|
+
const command = findElementOrExit(model.commands, scenarioInput.when.command, 'command');
|
|
183
164
|
whenCommand = {
|
|
184
165
|
commandStickyId: command.id,
|
|
185
166
|
fieldValues: scenarioInput.when.fieldValues,
|
|
@@ -195,12 +176,7 @@ export function addScenario(model, filePath, sliceName, input) {
|
|
|
195
176
|
then.expectedEvents = [];
|
|
196
177
|
if (scenarioInput.then.events) {
|
|
197
178
|
for (const e of scenarioInput.then.events) {
|
|
198
|
-
const event =
|
|
199
|
-
if (!event) {
|
|
200
|
-
console.error(`Error: Event not found in "then.events": ${e.event}`);
|
|
201
|
-
listAvailableEvents(model);
|
|
202
|
-
process.exit(1);
|
|
203
|
-
}
|
|
179
|
+
const event = findElementOrExit(model.events, e.event, 'event');
|
|
204
180
|
then.expectedEvents.push({
|
|
205
181
|
eventStickyId: event.id,
|
|
206
182
|
fieldValues: e.fieldValues,
|
|
@@ -213,21 +189,11 @@ export function addScenario(model, filePath, sliceName, input) {
|
|
|
213
189
|
console.error('Error: readModelAssertion requires a readModel name');
|
|
214
190
|
process.exit(1);
|
|
215
191
|
}
|
|
216
|
-
const readModel =
|
|
217
|
-
if (!readModel) {
|
|
218
|
-
console.error(`Error: Read model not found: ${scenarioInput.then.readModel}`);
|
|
219
|
-
listAvailableReadModels(model);
|
|
220
|
-
process.exit(1);
|
|
221
|
-
}
|
|
192
|
+
const readModel = findElementOrExit(model.readModels, scenarioInput.then.readModel, 'read model');
|
|
222
193
|
const assertionGivenEvents = [];
|
|
223
194
|
if (scenarioInput.then.givenEvents) {
|
|
224
195
|
for (const g of scenarioInput.then.givenEvents) {
|
|
225
|
-
const event =
|
|
226
|
-
if (!event) {
|
|
227
|
-
console.error(`Error: Event not found in "then.givenEvents": ${g.event}`);
|
|
228
|
-
listAvailableEvents(model);
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
196
|
+
const event = findElementOrExit(model.events, g.event, 'event');
|
|
231
197
|
assertionGivenEvents.push({
|
|
232
198
|
eventStickyId: event.id,
|
|
233
199
|
fieldValues: g.fieldValues,
|
|
@@ -304,36 +270,3 @@ export function addScenario(model, filePath, sliceName, input) {
|
|
|
304
270
|
});
|
|
305
271
|
console.log(`Added scenario "${scenarioInput.name}" to slice "${slice.name}"`);
|
|
306
272
|
}
|
|
307
|
-
function findEventByName(model, name) {
|
|
308
|
-
const nameLower = name.toLowerCase();
|
|
309
|
-
const events = [...model.events.values()];
|
|
310
|
-
return events.find(e => e.name.toLowerCase() === nameLower || e.name.toLowerCase().includes(nameLower));
|
|
311
|
-
}
|
|
312
|
-
function findCommandByName(model, name) {
|
|
313
|
-
const nameLower = name.toLowerCase();
|
|
314
|
-
const commands = [...model.commands.values()];
|
|
315
|
-
return commands.find(c => c.name.toLowerCase() === nameLower || c.name.toLowerCase().includes(nameLower));
|
|
316
|
-
}
|
|
317
|
-
function findReadModelByName(model, name) {
|
|
318
|
-
const nameLower = name.toLowerCase();
|
|
319
|
-
const readModels = [...model.readModels.values()];
|
|
320
|
-
return readModels.find(rm => rm.name.toLowerCase() === nameLower || rm.name.toLowerCase().includes(nameLower));
|
|
321
|
-
}
|
|
322
|
-
function listAvailableEvents(model) {
|
|
323
|
-
console.error('Available events:');
|
|
324
|
-
for (const e of model.events.values()) {
|
|
325
|
-
console.error(` - ${e.name}`);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
function listAvailableCommands(model) {
|
|
329
|
-
console.error('Available commands:');
|
|
330
|
-
for (const c of model.commands.values()) {
|
|
331
|
-
console.error(` - ${c.name}`);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
function listAvailableReadModels(model) {
|
|
335
|
-
console.error('Available read models:');
|
|
336
|
-
for (const rm of model.readModels.values()) {
|
|
337
|
-
console.error(` - ${rm.name}`);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { appendEvent } from '../../lib/file-loader.js';
|
|
2
|
+
import { AUTOMATION_SLICE, SLICE_GAP, calculateSlicePosition, validateSliceNameUnique, inferFieldMappings, parseFieldsFromXml, getSlicesToShift, fieldInputToField, } from '../../lib/slice-utils.js';
|
|
3
|
+
function getAttr(attrs, name) {
|
|
4
|
+
const match = attrs.match(new RegExp(`${name}="([^"]*)"`));
|
|
5
|
+
return match ? match[1] : undefined;
|
|
6
|
+
}
|
|
7
|
+
function parseXmlInput(xml) {
|
|
8
|
+
// Parse <automation-slice name="..." after="..." before="...">
|
|
9
|
+
const sliceMatch = xml.match(/<automation-slice([^>]*)>/);
|
|
10
|
+
if (!sliceMatch) {
|
|
11
|
+
throw new Error('Invalid XML: missing <automation-slice> tag');
|
|
12
|
+
}
|
|
13
|
+
const sliceName = getAttr(sliceMatch[1], 'name');
|
|
14
|
+
if (!sliceName) {
|
|
15
|
+
throw new Error('Invalid XML: automation-slice must have a name attribute');
|
|
16
|
+
}
|
|
17
|
+
const after = getAttr(sliceMatch[1], 'after');
|
|
18
|
+
const before = getAttr(sliceMatch[1], 'before');
|
|
19
|
+
// Parse processor (no fields)
|
|
20
|
+
const processorMatch = xml.match(/<processor([^>]*?)(?:\/>|>([\s\S]*?)<\/processor>)/);
|
|
21
|
+
if (!processorMatch) {
|
|
22
|
+
throw new Error('Invalid XML: missing <processor> element');
|
|
23
|
+
}
|
|
24
|
+
const processorName = getAttr(processorMatch[1], 'name');
|
|
25
|
+
if (!processorName) {
|
|
26
|
+
throw new Error('Invalid XML: processor must have a name attribute');
|
|
27
|
+
}
|
|
28
|
+
// Parse command
|
|
29
|
+
const commandMatch = xml.match(/<command([^>]*)>([\s\S]*?)<\/command>/);
|
|
30
|
+
if (!commandMatch) {
|
|
31
|
+
throw new Error('Invalid XML: missing <command> element');
|
|
32
|
+
}
|
|
33
|
+
const commandName = getAttr(commandMatch[1], 'name');
|
|
34
|
+
if (!commandName) {
|
|
35
|
+
throw new Error('Invalid XML: command must have a name attribute');
|
|
36
|
+
}
|
|
37
|
+
const commandFields = parseFieldsFromXml(commandMatch[2]);
|
|
38
|
+
// Parse event
|
|
39
|
+
const eventMatch = xml.match(/<event([^>]*)>([\s\S]*?)<\/event>/);
|
|
40
|
+
if (!eventMatch) {
|
|
41
|
+
throw new Error('Invalid XML: missing <event> element');
|
|
42
|
+
}
|
|
43
|
+
const eventName = getAttr(eventMatch[1], 'name');
|
|
44
|
+
if (!eventName) {
|
|
45
|
+
throw new Error('Invalid XML: event must have a name attribute');
|
|
46
|
+
}
|
|
47
|
+
const eventFields = parseFieldsFromXml(eventMatch[2]);
|
|
48
|
+
return {
|
|
49
|
+
sliceName,
|
|
50
|
+
after,
|
|
51
|
+
before,
|
|
52
|
+
processor: { name: processorName },
|
|
53
|
+
command: { name: commandName, fields: commandFields },
|
|
54
|
+
event: { name: eventName, fields: eventFields },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function createAutomationSlice(model, filePath, xmlInput) {
|
|
58
|
+
// Parse input
|
|
59
|
+
let input;
|
|
60
|
+
try {
|
|
61
|
+
input = parseXmlInput(xmlInput);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error(`Error: ${err.message}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
// Validate slice name is unique
|
|
68
|
+
try {
|
|
69
|
+
validateSliceNameUnique(model, input.sliceName);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.error(`Error: ${err.message}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
// Calculate position
|
|
76
|
+
let slicePosition;
|
|
77
|
+
try {
|
|
78
|
+
slicePosition = calculateSlicePosition(model, AUTOMATION_SLICE.width, {
|
|
79
|
+
after: input.after,
|
|
80
|
+
before: input.before,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
console.error(`Error: ${err.message}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
// Handle "before" positioning - shift existing slices
|
|
88
|
+
if (input.before) {
|
|
89
|
+
const shiftAmount = AUTOMATION_SLICE.width + SLICE_GAP;
|
|
90
|
+
const toShift = getSlicesToShift(model, slicePosition.x, shiftAmount);
|
|
91
|
+
for (const { sliceId, newX, currentY } of toShift) {
|
|
92
|
+
appendEvent(filePath, {
|
|
93
|
+
type: 'SliceMoved',
|
|
94
|
+
sliceId,
|
|
95
|
+
position: { x: newX, y: currentY },
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Generate IDs
|
|
101
|
+
const sliceId = crypto.randomUUID();
|
|
102
|
+
const processorId = crypto.randomUUID();
|
|
103
|
+
const commandId = crypto.randomUUID();
|
|
104
|
+
const eventId = crypto.randomUUID();
|
|
105
|
+
const processorToCommandFlowId = crypto.randomUUID();
|
|
106
|
+
const commandToEventFlowId = crypto.randomUUID();
|
|
107
|
+
// Convert field inputs to fields with IDs
|
|
108
|
+
const commandFields = input.command.fields.map(fieldInputToField);
|
|
109
|
+
const eventFields = input.event.fields.map(fieldInputToField);
|
|
110
|
+
// Infer field mappings (processor has no fields, so only command->event)
|
|
111
|
+
const commandToEventMappings = inferFieldMappings(commandFields, eventFields);
|
|
112
|
+
// Calculate absolute positions for components within the slice
|
|
113
|
+
const processorPosition = {
|
|
114
|
+
x: slicePosition.x + AUTOMATION_SLICE.processor.offsetX,
|
|
115
|
+
y: slicePosition.y + AUTOMATION_SLICE.processor.offsetY,
|
|
116
|
+
};
|
|
117
|
+
const commandPosition = {
|
|
118
|
+
x: slicePosition.x + AUTOMATION_SLICE.command.offsetX,
|
|
119
|
+
y: slicePosition.y + AUTOMATION_SLICE.command.offsetY,
|
|
120
|
+
};
|
|
121
|
+
const eventPosition = {
|
|
122
|
+
x: slicePosition.x + AUTOMATION_SLICE.event.offsetX,
|
|
123
|
+
y: slicePosition.y + AUTOMATION_SLICE.event.offsetY,
|
|
124
|
+
};
|
|
125
|
+
// Emit atomic events in sequence
|
|
126
|
+
// 1. Create the slice
|
|
127
|
+
appendEvent(filePath, {
|
|
128
|
+
type: 'SlicePlaced',
|
|
129
|
+
sliceId,
|
|
130
|
+
name: input.sliceName,
|
|
131
|
+
position: slicePosition,
|
|
132
|
+
size: { width: AUTOMATION_SLICE.width, height: AUTOMATION_SLICE.height },
|
|
133
|
+
timestamp: Date.now(),
|
|
134
|
+
});
|
|
135
|
+
// 2. Create the processor
|
|
136
|
+
appendEvent(filePath, {
|
|
137
|
+
type: 'ProcessorPlaced',
|
|
138
|
+
processorId,
|
|
139
|
+
name: input.processor.name,
|
|
140
|
+
position: processorPosition,
|
|
141
|
+
width: AUTOMATION_SLICE.processor.width,
|
|
142
|
+
height: AUTOMATION_SLICE.processor.height,
|
|
143
|
+
timestamp: Date.now(),
|
|
144
|
+
});
|
|
145
|
+
// 3. Create the command
|
|
146
|
+
appendEvent(filePath, {
|
|
147
|
+
type: 'CommandStickyPlaced',
|
|
148
|
+
commandStickyId: commandId,
|
|
149
|
+
name: input.command.name,
|
|
150
|
+
position: commandPosition,
|
|
151
|
+
width: AUTOMATION_SLICE.command.width,
|
|
152
|
+
height: AUTOMATION_SLICE.command.height,
|
|
153
|
+
timestamp: Date.now(),
|
|
154
|
+
});
|
|
155
|
+
// 4. Add command fields
|
|
156
|
+
for (const field of commandFields) {
|
|
157
|
+
appendEvent(filePath, {
|
|
158
|
+
type: 'CommandFieldAdded',
|
|
159
|
+
commandStickyId: commandId,
|
|
160
|
+
field,
|
|
161
|
+
timestamp: Date.now(),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// 5. Create the event
|
|
165
|
+
appendEvent(filePath, {
|
|
166
|
+
type: 'EventStickyPlaced',
|
|
167
|
+
eventStickyId: eventId,
|
|
168
|
+
name: input.event.name,
|
|
169
|
+
position: eventPosition,
|
|
170
|
+
width: AUTOMATION_SLICE.event.width,
|
|
171
|
+
height: AUTOMATION_SLICE.event.height,
|
|
172
|
+
timestamp: Date.now(),
|
|
173
|
+
});
|
|
174
|
+
// 6. Add event fields
|
|
175
|
+
for (const field of eventFields) {
|
|
176
|
+
appendEvent(filePath, {
|
|
177
|
+
type: 'EventFieldAdded',
|
|
178
|
+
eventStickyId: eventId,
|
|
179
|
+
field,
|
|
180
|
+
timestamp: Date.now(),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// 7. Create processor -> command flow
|
|
184
|
+
appendEvent(filePath, {
|
|
185
|
+
type: 'ProcessorToCommandFlowSpecified',
|
|
186
|
+
flowId: processorToCommandFlowId,
|
|
187
|
+
processorId,
|
|
188
|
+
commandStickyId: commandId,
|
|
189
|
+
sourceHandle: 'bottom-source',
|
|
190
|
+
targetHandle: 'top-target',
|
|
191
|
+
timestamp: Date.now(),
|
|
192
|
+
});
|
|
193
|
+
// 8. Create command -> event flow
|
|
194
|
+
appendEvent(filePath, {
|
|
195
|
+
type: 'CommandToEventFlowSpecified',
|
|
196
|
+
flowId: commandToEventFlowId,
|
|
197
|
+
commandStickyId: commandId,
|
|
198
|
+
eventStickyId: eventId,
|
|
199
|
+
sourceHandle: 'bottom-source',
|
|
200
|
+
targetHandle: 'top-target',
|
|
201
|
+
timestamp: Date.now(),
|
|
202
|
+
});
|
|
203
|
+
// 9. Add command -> event field mappings
|
|
204
|
+
if (commandToEventMappings.length > 0) {
|
|
205
|
+
appendEvent(filePath, {
|
|
206
|
+
type: 'FieldMappingSpecified',
|
|
207
|
+
flowId: commandToEventFlowId,
|
|
208
|
+
mappings: commandToEventMappings,
|
|
209
|
+
timestamp: Date.now(),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
console.log(`Created automation slice "${input.sliceName}"`);
|
|
213
|
+
console.log(` Processor: ${input.processor.name}`);
|
|
214
|
+
console.log(` Command: ${input.command.name} (${commandFields.length} fields)`);
|
|
215
|
+
console.log(` Event: ${input.event.name} (${eventFields.length} fields)`);
|
|
216
|
+
console.log(` Command -> Event mappings: ${commandToEventMappings.length}`);
|
|
217
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { appendEvent } from '../../lib/file-loader.js';
|
|
2
|
+
import { findElement } from '../../lib/element-lookup.js';
|
|
3
|
+
function findSource(model, name) {
|
|
4
|
+
// Try events first
|
|
5
|
+
const eventResult = findElement(model.events, name);
|
|
6
|
+
if (eventResult.success) {
|
|
7
|
+
return { type: 'event', element: eventResult.element };
|
|
8
|
+
}
|
|
9
|
+
// Try read models
|
|
10
|
+
const readModelResult = findElement(model.readModels, name);
|
|
11
|
+
if (readModelResult.success) {
|
|
12
|
+
return { type: 'readModel', element: readModelResult.element };
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
function findTarget(model, name) {
|
|
17
|
+
// Try read models first
|
|
18
|
+
const readModelResult = findElement(model.readModels, name);
|
|
19
|
+
if (readModelResult.success) {
|
|
20
|
+
return { type: 'readModel', element: readModelResult.element };
|
|
21
|
+
}
|
|
22
|
+
// Try screens
|
|
23
|
+
const screenResult = findElement(model.screens, name);
|
|
24
|
+
if (screenResult.success) {
|
|
25
|
+
return { type: 'screen', element: screenResult.element };
|
|
26
|
+
}
|
|
27
|
+
// Try processors
|
|
28
|
+
const processorResult = findElement(model.processors, name);
|
|
29
|
+
if (processorResult.success) {
|
|
30
|
+
return { type: 'processor', element: processorResult.element };
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function listAvailableSources(model) {
|
|
35
|
+
const sources = [];
|
|
36
|
+
for (const event of model.events.values()) {
|
|
37
|
+
sources.push(` - Event: "${event.name}"`);
|
|
38
|
+
}
|
|
39
|
+
for (const rm of model.readModels.values()) {
|
|
40
|
+
sources.push(` - ReadModel: "${rm.name}"`);
|
|
41
|
+
}
|
|
42
|
+
return sources;
|
|
43
|
+
}
|
|
44
|
+
function listAvailableTargets(model) {
|
|
45
|
+
const targets = [];
|
|
46
|
+
for (const rm of model.readModels.values()) {
|
|
47
|
+
targets.push(` - ReadModel: "${rm.name}"`);
|
|
48
|
+
}
|
|
49
|
+
for (const screen of model.screens.values()) {
|
|
50
|
+
targets.push(` - Screen: "${screen.name}"`);
|
|
51
|
+
}
|
|
52
|
+
for (const proc of model.processors.values()) {
|
|
53
|
+
targets.push(` - Processor: "${proc.name}"`);
|
|
54
|
+
}
|
|
55
|
+
return targets;
|
|
56
|
+
}
|
|
57
|
+
function flowExists(model, sourceId, targetId) {
|
|
58
|
+
for (const flow of model.flows.values()) {
|
|
59
|
+
if (flow.sourceId === sourceId && flow.targetId === targetId) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
export function createFlow(model, filePath, fromName, toName) {
|
|
66
|
+
// Find source element
|
|
67
|
+
const source = findSource(model, fromName);
|
|
68
|
+
if (!source) {
|
|
69
|
+
console.error(`Error: Source not found: "${fromName}"`);
|
|
70
|
+
console.error('Valid sources are events or read models.');
|
|
71
|
+
const available = listAvailableSources(model);
|
|
72
|
+
if (available.length > 0) {
|
|
73
|
+
console.error('Available sources:');
|
|
74
|
+
available.forEach(s => console.error(s));
|
|
75
|
+
}
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
// Find target element
|
|
79
|
+
const target = findTarget(model, toName);
|
|
80
|
+
if (!target) {
|
|
81
|
+
console.error(`Error: Target not found: "${toName}"`);
|
|
82
|
+
console.error('Valid targets are read models, screens, or processors.');
|
|
83
|
+
const available = listAvailableTargets(model);
|
|
84
|
+
if (available.length > 0) {
|
|
85
|
+
console.error('Available targets:');
|
|
86
|
+
available.forEach(t => console.error(t));
|
|
87
|
+
}
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
// Validate the flow combination
|
|
91
|
+
const flowId = crypto.randomUUID();
|
|
92
|
+
let event;
|
|
93
|
+
if (source.type === 'event' && target.type === 'readModel') {
|
|
94
|
+
// Event → ReadModel (event top → read model bottom)
|
|
95
|
+
if (flowExists(model, source.element.id, target.element.id)) {
|
|
96
|
+
console.error(`Error: Flow already exists from "${source.element.name}" to "${target.element.name}"`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
event = {
|
|
100
|
+
type: 'EventToReadModelFlowSpecified',
|
|
101
|
+
flowId,
|
|
102
|
+
eventStickyId: source.element.id,
|
|
103
|
+
readModelStickyId: target.element.id,
|
|
104
|
+
sourceHandle: 'top-source',
|
|
105
|
+
targetHandle: 'bottom-target',
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
};
|
|
108
|
+
appendEvent(filePath, event);
|
|
109
|
+
console.log(`Created flow: Event "${source.element.name}" → ReadModel "${target.element.name}"`);
|
|
110
|
+
}
|
|
111
|
+
else if (source.type === 'readModel' && target.type === 'screen') {
|
|
112
|
+
// ReadModel → Screen (read model top → screen bottom)
|
|
113
|
+
if (flowExists(model, source.element.id, target.element.id)) {
|
|
114
|
+
console.error(`Error: Flow already exists from "${source.element.name}" to "${target.element.name}"`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
event = {
|
|
118
|
+
type: 'ReadModelToScreenFlowSpecified',
|
|
119
|
+
flowId,
|
|
120
|
+
readModelStickyId: source.element.id,
|
|
121
|
+
screenId: target.element.id,
|
|
122
|
+
sourceHandle: 'top-source',
|
|
123
|
+
targetHandle: 'bottom-target',
|
|
124
|
+
timestamp: Date.now(),
|
|
125
|
+
};
|
|
126
|
+
appendEvent(filePath, event);
|
|
127
|
+
console.log(`Created flow: ReadModel "${source.element.name}" → Screen "${target.element.name}"`);
|
|
128
|
+
}
|
|
129
|
+
else if (source.type === 'readModel' && target.type === 'processor') {
|
|
130
|
+
// ReadModel → Processor (read model top → processor bottom)
|
|
131
|
+
if (flowExists(model, source.element.id, target.element.id)) {
|
|
132
|
+
console.error(`Error: Flow already exists from "${source.element.name}" to "${target.element.name}"`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
event = {
|
|
136
|
+
type: 'ReadModelToProcessorFlowSpecified',
|
|
137
|
+
flowId,
|
|
138
|
+
readModelStickyId: source.element.id,
|
|
139
|
+
processorId: target.element.id,
|
|
140
|
+
sourceHandle: 'top-source',
|
|
141
|
+
targetHandle: 'bottom-target',
|
|
142
|
+
timestamp: Date.now(),
|
|
143
|
+
};
|
|
144
|
+
appendEvent(filePath, event);
|
|
145
|
+
console.log(`Created flow: ReadModel "${source.element.name}" → Processor "${target.element.name}"`);
|
|
146
|
+
}
|
|
147
|
+
else if (source.type === 'event' && target.type === 'screen') {
|
|
148
|
+
console.error('Error: Cannot create flow directly from Event to Screen.');
|
|
149
|
+
console.error('Events flow to ReadModels, which then flow to Screens.');
|
|
150
|
+
console.error('Create a ReadModel first, then:');
|
|
151
|
+
console.error(` 1. eventmodeler create flow --from "${source.element.name}" --to "<ReadModelName>"`);
|
|
152
|
+
console.error(` 2. eventmodeler create flow --from "<ReadModelName>" --to "${target.element.name}"`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
else if (source.type === 'event' && target.type === 'processor') {
|
|
156
|
+
console.error('Error: Cannot create flow directly from Event to Processor.');
|
|
157
|
+
console.error('Events flow to ReadModels, which then flow to Processors.');
|
|
158
|
+
console.error('Create a ReadModel first, then:');
|
|
159
|
+
console.error(` 1. eventmodeler create flow --from "${source.element.name}" --to "<ReadModelName>"`);
|
|
160
|
+
console.error(` 2. eventmodeler create flow --from "<ReadModelName>" --to "${target.element.name}"`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
else if (source.type === 'readModel' && target.type === 'readModel') {
|
|
164
|
+
console.error('Error: Cannot create flow from ReadModel to ReadModel.');
|
|
165
|
+
console.error('ReadModels receive data from Events and provide data to Screens or Processors.');
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.error(`Error: Invalid flow combination: ${source.type} → ${target.type}`);
|
|
170
|
+
console.error('Valid combinations:');
|
|
171
|
+
console.error(' - Event → ReadModel');
|
|
172
|
+
console.error(' - ReadModel → Screen');
|
|
173
|
+
console.error(' - ReadModel → Processor');
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
console.log(`Use 'eventmodeler map fields --flow "${fromName}→${toName}"' to set field mappings.`);
|
|
177
|
+
}
|