eventmodeler 0.2.1 → 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/README.md +175 -0
- package/dist/index.js +114 -19
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/config.js +26 -0
- package/dist/lib/element-lookup.d.ts +47 -0
- package/dist/lib/element-lookup.js +86 -0
- package/dist/lib/format.d.ts +3 -0
- package/dist/lib/format.js +11 -0
- package/dist/lib/slice-utils.d.ts +83 -0
- package/dist/lib/slice-utils.js +135 -0
- package/dist/projection.js +161 -35
- 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.d.ts +2 -1
- package/dist/slices/list-chapters/index.js +11 -12
- package/dist/slices/list-commands/index.d.ts +2 -1
- package/dist/slices/list-commands/index.js +10 -11
- package/dist/slices/list-events/index.d.ts +2 -1
- package/dist/slices/list-events/index.js +36 -15
- package/dist/slices/list-slices/index.d.ts +2 -1
- package/dist/slices/list-slices/index.js +10 -11
- 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/search/index.d.ts +2 -1
- package/dist/slices/search/index.js +148 -21
- package/dist/slices/show-actor/index.d.ts +3 -2
- package/dist/slices/show-actor/index.js +46 -20
- package/dist/slices/show-aggregate-completeness/index.d.ts +3 -2
- package/dist/slices/show-aggregate-completeness/index.js +62 -20
- package/dist/slices/show-chapter/index.d.ts +2 -1
- package/dist/slices/show-chapter/index.js +14 -22
- package/dist/slices/show-command/index.d.ts +2 -1
- package/dist/slices/show-command/index.js +54 -19
- package/dist/slices/show-completeness/index.d.ts +3 -1
- package/dist/slices/show-completeness/index.js +313 -31
- package/dist/slices/show-event/index.d.ts +2 -1
- package/dist/slices/show-event/index.js +44 -20
- package/dist/slices/show-model-summary/index.d.ts +2 -1
- package/dist/slices/show-model-summary/index.js +18 -9
- package/dist/slices/show-slice/index.d.ts +2 -1
- package/dist/slices/show-slice/index.js +174 -24
- package/dist/slices/update-field/index.js +4 -33
- package/package.json +5 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { appendEvent } from '../../lib/file-loader.js';
|
|
2
|
+
import { STATE_CHANGE_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 <state-change-slice name="..." after="..." before="...">
|
|
9
|
+
const sliceMatch = xml.match(/<state-change-slice([^>]*)>/);
|
|
10
|
+
if (!sliceMatch) {
|
|
11
|
+
throw new Error('Invalid XML: missing <state-change-slice> tag');
|
|
12
|
+
}
|
|
13
|
+
const sliceName = getAttr(sliceMatch[1], 'name');
|
|
14
|
+
if (!sliceName) {
|
|
15
|
+
throw new Error('Invalid XML: state-change-slice must have a name attribute');
|
|
16
|
+
}
|
|
17
|
+
const after = getAttr(sliceMatch[1], 'after');
|
|
18
|
+
const before = getAttr(sliceMatch[1], 'before');
|
|
19
|
+
// Parse screen
|
|
20
|
+
const screenMatch = xml.match(/<screen([^>]*)>([\s\S]*?)<\/screen>/);
|
|
21
|
+
if (!screenMatch) {
|
|
22
|
+
throw new Error('Invalid XML: missing <screen> element');
|
|
23
|
+
}
|
|
24
|
+
const screenName = getAttr(screenMatch[1], 'name');
|
|
25
|
+
if (!screenName) {
|
|
26
|
+
throw new Error('Invalid XML: screen must have a name attribute');
|
|
27
|
+
}
|
|
28
|
+
const screenFields = parseFieldsFromXml(screenMatch[2]);
|
|
29
|
+
// Parse command
|
|
30
|
+
const commandMatch = xml.match(/<command([^>]*)>([\s\S]*?)<\/command>/);
|
|
31
|
+
if (!commandMatch) {
|
|
32
|
+
throw new Error('Invalid XML: missing <command> element');
|
|
33
|
+
}
|
|
34
|
+
const commandName = getAttr(commandMatch[1], 'name');
|
|
35
|
+
if (!commandName) {
|
|
36
|
+
throw new Error('Invalid XML: command must have a name attribute');
|
|
37
|
+
}
|
|
38
|
+
const commandFields = parseFieldsFromXml(commandMatch[2]);
|
|
39
|
+
// Parse event
|
|
40
|
+
const eventMatch = xml.match(/<event([^>]*)>([\s\S]*?)<\/event>/);
|
|
41
|
+
if (!eventMatch) {
|
|
42
|
+
throw new Error('Invalid XML: missing <event> element');
|
|
43
|
+
}
|
|
44
|
+
const eventName = getAttr(eventMatch[1], 'name');
|
|
45
|
+
if (!eventName) {
|
|
46
|
+
throw new Error('Invalid XML: event must have a name attribute');
|
|
47
|
+
}
|
|
48
|
+
const eventFields = parseFieldsFromXml(eventMatch[2]);
|
|
49
|
+
return {
|
|
50
|
+
sliceName,
|
|
51
|
+
after,
|
|
52
|
+
before,
|
|
53
|
+
screen: { name: screenName, fields: screenFields },
|
|
54
|
+
command: { name: commandName, fields: commandFields },
|
|
55
|
+
event: { name: eventName, fields: eventFields },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function createStateChangeSlice(model, filePath, xmlInput) {
|
|
59
|
+
// Parse input
|
|
60
|
+
let input;
|
|
61
|
+
try {
|
|
62
|
+
input = parseXmlInput(xmlInput);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
console.error(`Error: ${err.message}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
// Validate slice name is unique
|
|
69
|
+
try {
|
|
70
|
+
validateSliceNameUnique(model, input.sliceName);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error(`Error: ${err.message}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
// Calculate position
|
|
77
|
+
let slicePosition;
|
|
78
|
+
try {
|
|
79
|
+
slicePosition = calculateSlicePosition(model, STATE_CHANGE_SLICE.width, {
|
|
80
|
+
after: input.after,
|
|
81
|
+
before: input.before,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.error(`Error: ${err.message}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
// Handle "before" positioning - shift existing slices
|
|
89
|
+
if (input.before) {
|
|
90
|
+
const shiftAmount = STATE_CHANGE_SLICE.width + SLICE_GAP;
|
|
91
|
+
const toShift = getSlicesToShift(model, slicePosition.x, shiftAmount);
|
|
92
|
+
for (const { sliceId, newX, currentY } of toShift) {
|
|
93
|
+
appendEvent(filePath, {
|
|
94
|
+
type: 'SliceMoved',
|
|
95
|
+
sliceId,
|
|
96
|
+
position: { x: newX, y: currentY },
|
|
97
|
+
timestamp: Date.now(),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Generate IDs
|
|
102
|
+
const sliceId = crypto.randomUUID();
|
|
103
|
+
const screenId = crypto.randomUUID();
|
|
104
|
+
const commandId = crypto.randomUUID();
|
|
105
|
+
const eventId = crypto.randomUUID();
|
|
106
|
+
const screenToCommandFlowId = crypto.randomUUID();
|
|
107
|
+
const commandToEventFlowId = crypto.randomUUID();
|
|
108
|
+
// Convert field inputs to fields with IDs
|
|
109
|
+
const screenFields = input.screen.fields.map(fieldInputToField);
|
|
110
|
+
const commandFields = input.command.fields.map(fieldInputToField);
|
|
111
|
+
const eventFields = input.event.fields.map(fieldInputToField);
|
|
112
|
+
// Infer field mappings
|
|
113
|
+
const screenToCommandMappings = inferFieldMappings(screenFields, commandFields);
|
|
114
|
+
const commandToEventMappings = inferFieldMappings(commandFields, eventFields);
|
|
115
|
+
// Calculate absolute positions for components within the slice
|
|
116
|
+
const screenPosition = {
|
|
117
|
+
x: slicePosition.x + STATE_CHANGE_SLICE.screen.offsetX,
|
|
118
|
+
y: slicePosition.y + STATE_CHANGE_SLICE.screen.offsetY,
|
|
119
|
+
};
|
|
120
|
+
const commandPosition = {
|
|
121
|
+
x: slicePosition.x + STATE_CHANGE_SLICE.command.offsetX,
|
|
122
|
+
y: slicePosition.y + STATE_CHANGE_SLICE.command.offsetY,
|
|
123
|
+
};
|
|
124
|
+
const eventPosition = {
|
|
125
|
+
x: slicePosition.x + STATE_CHANGE_SLICE.event.offsetX,
|
|
126
|
+
y: slicePosition.y + STATE_CHANGE_SLICE.event.offsetY,
|
|
127
|
+
};
|
|
128
|
+
// Emit atomic events in sequence
|
|
129
|
+
// 1. Create the slice
|
|
130
|
+
appendEvent(filePath, {
|
|
131
|
+
type: 'SlicePlaced',
|
|
132
|
+
sliceId,
|
|
133
|
+
name: input.sliceName,
|
|
134
|
+
position: slicePosition,
|
|
135
|
+
size: { width: STATE_CHANGE_SLICE.width, height: STATE_CHANGE_SLICE.height },
|
|
136
|
+
timestamp: Date.now(),
|
|
137
|
+
});
|
|
138
|
+
// 2. Create the screen
|
|
139
|
+
appendEvent(filePath, {
|
|
140
|
+
type: 'ScreenPlaced',
|
|
141
|
+
screenId,
|
|
142
|
+
name: input.screen.name,
|
|
143
|
+
position: screenPosition,
|
|
144
|
+
width: STATE_CHANGE_SLICE.screen.width,
|
|
145
|
+
height: STATE_CHANGE_SLICE.screen.height,
|
|
146
|
+
timestamp: Date.now(),
|
|
147
|
+
});
|
|
148
|
+
// 3. Add screen fields
|
|
149
|
+
for (const field of screenFields) {
|
|
150
|
+
appendEvent(filePath, {
|
|
151
|
+
type: 'ScreenFieldAdded',
|
|
152
|
+
screenId,
|
|
153
|
+
field,
|
|
154
|
+
timestamp: Date.now(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// 4. Create the command
|
|
158
|
+
appendEvent(filePath, {
|
|
159
|
+
type: 'CommandStickyPlaced',
|
|
160
|
+
commandStickyId: commandId,
|
|
161
|
+
name: input.command.name,
|
|
162
|
+
position: commandPosition,
|
|
163
|
+
width: STATE_CHANGE_SLICE.command.width,
|
|
164
|
+
height: STATE_CHANGE_SLICE.command.height,
|
|
165
|
+
timestamp: Date.now(),
|
|
166
|
+
});
|
|
167
|
+
// 5. Add command fields
|
|
168
|
+
for (const field of commandFields) {
|
|
169
|
+
appendEvent(filePath, {
|
|
170
|
+
type: 'CommandFieldAdded',
|
|
171
|
+
commandStickyId: commandId,
|
|
172
|
+
field,
|
|
173
|
+
timestamp: Date.now(),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// 6. Create the event
|
|
177
|
+
appendEvent(filePath, {
|
|
178
|
+
type: 'EventStickyPlaced',
|
|
179
|
+
eventStickyId: eventId,
|
|
180
|
+
name: input.event.name,
|
|
181
|
+
position: eventPosition,
|
|
182
|
+
width: STATE_CHANGE_SLICE.event.width,
|
|
183
|
+
height: STATE_CHANGE_SLICE.event.height,
|
|
184
|
+
timestamp: Date.now(),
|
|
185
|
+
});
|
|
186
|
+
// 7. Add event fields
|
|
187
|
+
for (const field of eventFields) {
|
|
188
|
+
appendEvent(filePath, {
|
|
189
|
+
type: 'EventFieldAdded',
|
|
190
|
+
eventStickyId: eventId,
|
|
191
|
+
field,
|
|
192
|
+
timestamp: Date.now(),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
// 8. Create screen -> command flow
|
|
196
|
+
appendEvent(filePath, {
|
|
197
|
+
type: 'ScreenToCommandFlowSpecified',
|
|
198
|
+
flowId: screenToCommandFlowId,
|
|
199
|
+
screenId,
|
|
200
|
+
commandStickyId: commandId,
|
|
201
|
+
sourceHandle: 'bottom-source',
|
|
202
|
+
targetHandle: 'top-target',
|
|
203
|
+
timestamp: Date.now(),
|
|
204
|
+
});
|
|
205
|
+
// 9. Add screen -> command field mappings
|
|
206
|
+
if (screenToCommandMappings.length > 0) {
|
|
207
|
+
appendEvent(filePath, {
|
|
208
|
+
type: 'FieldMappingSpecified',
|
|
209
|
+
flowId: screenToCommandFlowId,
|
|
210
|
+
mappings: screenToCommandMappings,
|
|
211
|
+
timestamp: Date.now(),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// 10. Create command -> event flow
|
|
215
|
+
appendEvent(filePath, {
|
|
216
|
+
type: 'CommandToEventFlowSpecified',
|
|
217
|
+
flowId: commandToEventFlowId,
|
|
218
|
+
commandStickyId: commandId,
|
|
219
|
+
eventStickyId: eventId,
|
|
220
|
+
sourceHandle: 'bottom-source',
|
|
221
|
+
targetHandle: 'top-target',
|
|
222
|
+
timestamp: Date.now(),
|
|
223
|
+
});
|
|
224
|
+
// 11. Add command -> event field mappings
|
|
225
|
+
if (commandToEventMappings.length > 0) {
|
|
226
|
+
appendEvent(filePath, {
|
|
227
|
+
type: 'FieldMappingSpecified',
|
|
228
|
+
flowId: commandToEventFlowId,
|
|
229
|
+
mappings: commandToEventMappings,
|
|
230
|
+
timestamp: Date.now(),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
console.log(`Created state-change slice "${input.sliceName}"`);
|
|
234
|
+
console.log(` Screen: ${input.screen.name} (${screenFields.length} fields)`);
|
|
235
|
+
console.log(` Command: ${input.command.name} (${commandFields.length} fields)`);
|
|
236
|
+
console.log(` Event: ${input.event.name} (${eventFields.length} fields)`);
|
|
237
|
+
console.log(` Screen -> Command mappings: ${screenToCommandMappings.length}`);
|
|
238
|
+
console.log(` Command -> Event mappings: ${commandToEventMappings.length}`);
|
|
239
|
+
}
|