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;
|
package/dist/lib/slice-utils.js
CHANGED
|
@@ -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:
|
|
10
|
+
width: 880,
|
|
11
11
|
height: 1000,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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,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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
}
|