eventmodeler 0.1.0 → 0.2.0
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 +213 -6
- package/dist/projection.js +101 -0
- package/dist/slices/add-field/index.d.ts +6 -0
- package/dist/slices/add-field/index.js +182 -0
- package/dist/slices/add-scenario/index.d.ts +2 -0
- package/dist/slices/add-scenario/index.js +339 -0
- package/dist/slices/export-eventmodel-to-json/index.js +62 -3
- package/dist/slices/list-events/index.js +58 -2
- package/dist/slices/map-fields/index.d.ts +2 -0
- package/dist/slices/map-fields/index.js +216 -0
- package/dist/slices/remove-field/index.d.ts +6 -0
- package/dist/slices/remove-field/index.js +127 -0
- package/dist/slices/remove-scenario/index.d.ts +2 -0
- package/dist/slices/remove-scenario/index.js +39 -0
- package/dist/slices/show-actor/index.d.ts +3 -0
- package/dist/slices/show-actor/index.js +89 -0
- package/dist/slices/show-aggregate-completeness/index.d.ts +3 -0
- package/dist/slices/show-aggregate-completeness/index.js +139 -0
- package/dist/slices/show-command/index.js +22 -2
- package/dist/slices/show-completeness/index.d.ts +2 -0
- package/dist/slices/show-completeness/index.js +180 -0
- package/dist/slices/show-event/index.js +20 -1
- package/dist/slices/show-model-summary/index.js +2 -0
- package/dist/slices/show-slice/index.js +144 -3
- package/dist/slices/update-field/index.d.ts +12 -0
- package/dist/slices/update-field/index.js +166 -0
- package/dist/types.d.ts +33 -0
- package/package.json +1 -1
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import { appendEvent } from '../../lib/file-loader.js';
|
|
3
|
+
function parseJsonInput(input) {
|
|
4
|
+
return JSON.parse(input);
|
|
5
|
+
}
|
|
6
|
+
function parseXmlInput(input) {
|
|
7
|
+
// Simple XML parser for scenario format
|
|
8
|
+
const getAttr = (tag, attr) => {
|
|
9
|
+
const match = tag.match(new RegExp(`${attr}="([^"]*)"`));
|
|
10
|
+
return match ? match[1] : undefined;
|
|
11
|
+
};
|
|
12
|
+
const getContent = (xml, tagName) => {
|
|
13
|
+
const match = xml.match(new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)</${tagName}>`));
|
|
14
|
+
return match ? match[1].trim() : undefined;
|
|
15
|
+
};
|
|
16
|
+
const scenarioMatch = input.match(/<scenario([^>]*)>/);
|
|
17
|
+
if (!scenarioMatch) {
|
|
18
|
+
throw new Error('Invalid XML: missing <scenario> tag');
|
|
19
|
+
}
|
|
20
|
+
const name = getAttr(scenarioMatch[1], 'name');
|
|
21
|
+
if (!name) {
|
|
22
|
+
throw new Error('Invalid XML: scenario must have a name attribute');
|
|
23
|
+
}
|
|
24
|
+
const description = getAttr(scenarioMatch[1], 'description');
|
|
25
|
+
// Parse given events
|
|
26
|
+
const givenContent = getContent(input, 'given');
|
|
27
|
+
const given = [];
|
|
28
|
+
if (givenContent) {
|
|
29
|
+
const eventMatches = givenContent.matchAll(/<event([^>]*?)(?:\/>|>([\s\S]*?)<\/event>)/g);
|
|
30
|
+
for (const match of eventMatches) {
|
|
31
|
+
const eventName = getAttr(match[1], 'name');
|
|
32
|
+
if (eventName) {
|
|
33
|
+
const fieldValues = {};
|
|
34
|
+
if (match[2]) {
|
|
35
|
+
const fieldMatches = match[2].matchAll(/<field name="([^"]*)"[^>]*>([^<]*)<\/field>/g);
|
|
36
|
+
for (const fieldMatch of fieldMatches) {
|
|
37
|
+
fieldValues[fieldMatch[1]] = parseFieldValue(fieldMatch[2]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
given.push({ event: eventName, fieldValues: Object.keys(fieldValues).length > 0 ? fieldValues : undefined });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Parse when command
|
|
45
|
+
const whenContent = getContent(input, 'when');
|
|
46
|
+
let when;
|
|
47
|
+
if (whenContent) {
|
|
48
|
+
const commandMatch = whenContent.match(/<command([^>]*?)(?:\/>|>([\s\S]*?)<\/command>)/);
|
|
49
|
+
if (commandMatch) {
|
|
50
|
+
const commandName = getAttr(commandMatch[1], 'name');
|
|
51
|
+
if (commandName) {
|
|
52
|
+
const fieldValues = {};
|
|
53
|
+
if (commandMatch[2]) {
|
|
54
|
+
const fieldMatches = commandMatch[2].matchAll(/<field name="([^"]*)"[^>]*>([^<]*)<\/field>/g);
|
|
55
|
+
for (const fieldMatch of fieldMatches) {
|
|
56
|
+
fieldValues[fieldMatch[1]] = parseFieldValue(fieldMatch[2]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
when = { command: commandName, fieldValues: Object.keys(fieldValues).length > 0 ? fieldValues : undefined };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Parse then
|
|
64
|
+
const thenMatch = input.match(/<then([^>]*)(?:\/>|>([\s\S]*?)<\/then>)/);
|
|
65
|
+
if (!thenMatch) {
|
|
66
|
+
throw new Error('Invalid XML: missing <then> tag');
|
|
67
|
+
}
|
|
68
|
+
const thenType = getAttr(thenMatch[1], 'type');
|
|
69
|
+
if (!thenType) {
|
|
70
|
+
throw new Error('Invalid XML: <then> must have a type attribute');
|
|
71
|
+
}
|
|
72
|
+
const then = { type: thenType };
|
|
73
|
+
if (thenType === 'error') {
|
|
74
|
+
then.errorType = getAttr(thenMatch[1], 'errorType');
|
|
75
|
+
then.errorMessage = thenMatch[2]?.trim();
|
|
76
|
+
}
|
|
77
|
+
else if (thenType === 'events' && thenMatch[2]) {
|
|
78
|
+
then.events = [];
|
|
79
|
+
const eventMatches = thenMatch[2].matchAll(/<event([^>]*?)(?:\/>|>([\s\S]*?)<\/event>)/g);
|
|
80
|
+
for (const match of eventMatches) {
|
|
81
|
+
const eventName = getAttr(match[1], 'name');
|
|
82
|
+
if (eventName) {
|
|
83
|
+
const fieldValues = {};
|
|
84
|
+
if (match[2]) {
|
|
85
|
+
const fieldMatches = match[2].matchAll(/<field name="([^"]*)"[^>]*>([^<]*)<\/field>/g);
|
|
86
|
+
for (const fieldMatch of fieldMatches) {
|
|
87
|
+
fieldValues[fieldMatch[1]] = parseFieldValue(fieldMatch[2]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
then.events.push({ event: eventName, fieldValues: Object.keys(fieldValues).length > 0 ? fieldValues : undefined });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (thenType === 'readModelAssertion' && thenMatch[2]) {
|
|
95
|
+
const rmMatch = thenMatch[2].match(/<read-model([^>]*?)(?:\/>|>([\s\S]*?)<\/read-model>)/);
|
|
96
|
+
if (rmMatch) {
|
|
97
|
+
then.readModel = getAttr(rmMatch[1], 'name');
|
|
98
|
+
then.expected = {};
|
|
99
|
+
if (rmMatch[2]) {
|
|
100
|
+
const fieldMatches = rmMatch[2].matchAll(/<field name="([^"]*)"[^>]*>([^<]*)<\/field>/g);
|
|
101
|
+
for (const fieldMatch of fieldMatches) {
|
|
102
|
+
then.expected[fieldMatch[1]] = parseFieldValue(fieldMatch[2]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { name, description, given: given.length > 0 ? given : undefined, when, then };
|
|
108
|
+
}
|
|
109
|
+
function parseFieldValue(value) {
|
|
110
|
+
// Try to parse as JSON for complex values, otherwise return as string
|
|
111
|
+
const trimmed = value.trim();
|
|
112
|
+
if (trimmed === 'true')
|
|
113
|
+
return true;
|
|
114
|
+
if (trimmed === 'false')
|
|
115
|
+
return false;
|
|
116
|
+
if (trimmed === 'null')
|
|
117
|
+
return null;
|
|
118
|
+
if (/^-?\d+$/.test(trimmed))
|
|
119
|
+
return parseInt(trimmed, 10);
|
|
120
|
+
if (/^-?\d+\.\d+$/.test(trimmed))
|
|
121
|
+
return parseFloat(trimmed);
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(trimmed);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return trimmed;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function parseInput(input) {
|
|
130
|
+
const trimmed = input.trim();
|
|
131
|
+
if (trimmed.startsWith('<')) {
|
|
132
|
+
return parseXmlInput(trimmed);
|
|
133
|
+
}
|
|
134
|
+
return parseJsonInput(trimmed);
|
|
135
|
+
}
|
|
136
|
+
export function addScenario(model, filePath, sliceName, input) {
|
|
137
|
+
// Parse input
|
|
138
|
+
let scenarioInput;
|
|
139
|
+
try {
|
|
140
|
+
scenarioInput = parseInput(input);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
console.error(`Error: Invalid input format: ${err.message}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
// Find slice by name
|
|
147
|
+
const slices = [...model.slices.values()];
|
|
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
|
+
}
|
|
158
|
+
// Resolve event references in given
|
|
159
|
+
const givenEvents = [];
|
|
160
|
+
if (scenarioInput.given) {
|
|
161
|
+
for (const g of scenarioInput.given) {
|
|
162
|
+
const event = findEventByName(model, g.event);
|
|
163
|
+
if (!event) {
|
|
164
|
+
console.error(`Error: Event not found in "given": ${g.event}`);
|
|
165
|
+
listAvailableEvents(model);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
givenEvents.push({
|
|
169
|
+
eventStickyId: event.id,
|
|
170
|
+
fieldValues: g.fieldValues,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Resolve command reference in when
|
|
175
|
+
let whenCommand = null;
|
|
176
|
+
if (scenarioInput.when) {
|
|
177
|
+
const command = findCommandByName(model, scenarioInput.when.command);
|
|
178
|
+
if (!command) {
|
|
179
|
+
console.error(`Error: Command not found in "when": ${scenarioInput.when.command}`);
|
|
180
|
+
listAvailableCommands(model);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
whenCommand = {
|
|
184
|
+
commandStickyId: command.id,
|
|
185
|
+
fieldValues: scenarioInput.when.fieldValues,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// Resolve then clause
|
|
189
|
+
const then = { type: scenarioInput.then.type };
|
|
190
|
+
if (scenarioInput.then.type === 'error') {
|
|
191
|
+
then.errorMessage = scenarioInput.then.errorMessage;
|
|
192
|
+
then.errorType = scenarioInput.then.errorType;
|
|
193
|
+
}
|
|
194
|
+
else if (scenarioInput.then.type === 'events') {
|
|
195
|
+
then.expectedEvents = [];
|
|
196
|
+
if (scenarioInput.then.events) {
|
|
197
|
+
for (const e of scenarioInput.then.events) {
|
|
198
|
+
const event = findEventByName(model, e.event);
|
|
199
|
+
if (!event) {
|
|
200
|
+
console.error(`Error: Event not found in "then.events": ${e.event}`);
|
|
201
|
+
listAvailableEvents(model);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
then.expectedEvents.push({
|
|
205
|
+
eventStickyId: event.id,
|
|
206
|
+
fieldValues: e.fieldValues,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else if (scenarioInput.then.type === 'readModelAssertion') {
|
|
212
|
+
if (!scenarioInput.then.readModel) {
|
|
213
|
+
console.error('Error: readModelAssertion requires a readModel name');
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
const readModel = findReadModelByName(model, scenarioInput.then.readModel);
|
|
217
|
+
if (!readModel) {
|
|
218
|
+
console.error(`Error: Read model not found: ${scenarioInput.then.readModel}`);
|
|
219
|
+
listAvailableReadModels(model);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
const assertionGivenEvents = [];
|
|
223
|
+
if (scenarioInput.then.givenEvents) {
|
|
224
|
+
for (const g of scenarioInput.then.givenEvents) {
|
|
225
|
+
const event = findEventByName(model, g.event);
|
|
226
|
+
if (!event) {
|
|
227
|
+
console.error(`Error: Event not found in "then.givenEvents": ${g.event}`);
|
|
228
|
+
listAvailableEvents(model);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
assertionGivenEvents.push({
|
|
232
|
+
eventStickyId: event.id,
|
|
233
|
+
fieldValues: g.fieldValues,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
then.readModelAssertion = {
|
|
238
|
+
readModelStickyId: readModel.id,
|
|
239
|
+
givenEvents: assertionGivenEvents,
|
|
240
|
+
expectedFieldValues: scenarioInput.then.expected ?? {},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
// Calculate position below slice
|
|
244
|
+
const existingScenarios = [...model.scenarios.values()].filter(s => s.sliceId === slice.id);
|
|
245
|
+
const sliceBottom = slice.position.y + slice.size.height;
|
|
246
|
+
let positionY;
|
|
247
|
+
if (existingScenarios.length === 0) {
|
|
248
|
+
positionY = sliceBottom + 20;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const lowestY = Math.max(...existingScenarios.map(s => s.position.y + s.height));
|
|
252
|
+
positionY = lowestY + 10;
|
|
253
|
+
}
|
|
254
|
+
const position = {
|
|
255
|
+
x: slice.position.x + 10,
|
|
256
|
+
y: positionY,
|
|
257
|
+
};
|
|
258
|
+
// Generate scenario ID
|
|
259
|
+
const scenarioId = crypto.randomUUID();
|
|
260
|
+
// Append ScenarioCreated event
|
|
261
|
+
appendEvent(filePath, {
|
|
262
|
+
type: 'ScenarioCreated',
|
|
263
|
+
scenarioId,
|
|
264
|
+
sliceId: slice.id,
|
|
265
|
+
name: scenarioInput.name,
|
|
266
|
+
position,
|
|
267
|
+
width: 200,
|
|
268
|
+
height: 80,
|
|
269
|
+
timestamp: Date.now(),
|
|
270
|
+
});
|
|
271
|
+
// Append description update if provided
|
|
272
|
+
if (scenarioInput.description) {
|
|
273
|
+
appendEvent(filePath, {
|
|
274
|
+
type: 'ScenarioDescriptionUpdated',
|
|
275
|
+
scenarioId,
|
|
276
|
+
description: scenarioInput.description,
|
|
277
|
+
timestamp: Date.now(),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
// Append given events update if provided
|
|
281
|
+
if (givenEvents.length > 0) {
|
|
282
|
+
appendEvent(filePath, {
|
|
283
|
+
type: 'ScenarioGivenEventsUpdated',
|
|
284
|
+
scenarioId,
|
|
285
|
+
givenEvents,
|
|
286
|
+
timestamp: Date.now(),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
// Append when command update if provided
|
|
290
|
+
if (whenCommand) {
|
|
291
|
+
appendEvent(filePath, {
|
|
292
|
+
type: 'ScenarioWhenCommandUpdated',
|
|
293
|
+
scenarioId,
|
|
294
|
+
whenCommand,
|
|
295
|
+
timestamp: Date.now(),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
// Append then update
|
|
299
|
+
appendEvent(filePath, {
|
|
300
|
+
type: 'ScenarioThenUpdated',
|
|
301
|
+
scenarioId,
|
|
302
|
+
then,
|
|
303
|
+
timestamp: Date.now(),
|
|
304
|
+
});
|
|
305
|
+
console.log(`Added scenario "${scenarioInput.name}" to slice "${slice.name}"`);
|
|
306
|
+
}
|
|
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
|
+
}
|
|
@@ -141,6 +141,60 @@ function determineSliceType(commands, events, processors) {
|
|
|
141
141
|
// Otherwise it's a state view (read-only)
|
|
142
142
|
return 'STATE_VIEW';
|
|
143
143
|
}
|
|
144
|
+
// Check if two rectangles overlap
|
|
145
|
+
function rectanglesOverlap(r1, r2) {
|
|
146
|
+
return (r1.x < r2.x + r2.width &&
|
|
147
|
+
r1.x + r1.width > r2.x &&
|
|
148
|
+
r1.y < r2.y + r2.height &&
|
|
149
|
+
r1.y + r1.height > r2.y);
|
|
150
|
+
}
|
|
151
|
+
// Find aggregates that overlap with the slice
|
|
152
|
+
function getAggregatesForSlice(model, slice) {
|
|
153
|
+
const sliceRect = {
|
|
154
|
+
x: slice.position.x,
|
|
155
|
+
y: slice.position.y,
|
|
156
|
+
width: slice.size.width,
|
|
157
|
+
height: slice.size.height,
|
|
158
|
+
};
|
|
159
|
+
const aggregateNames = [];
|
|
160
|
+
for (const aggregate of model.aggregates.values()) {
|
|
161
|
+
const aggRect = {
|
|
162
|
+
x: aggregate.position.x,
|
|
163
|
+
y: aggregate.position.y,
|
|
164
|
+
width: aggregate.size.width,
|
|
165
|
+
height: aggregate.size.height,
|
|
166
|
+
};
|
|
167
|
+
if (rectanglesOverlap(sliceRect, aggRect)) {
|
|
168
|
+
aggregateNames.push(aggregate.name);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return aggregateNames;
|
|
172
|
+
}
|
|
173
|
+
// Find actors that overlap with the slice
|
|
174
|
+
function getActorsForSlice(model, slice) {
|
|
175
|
+
const sliceRect = {
|
|
176
|
+
x: slice.position.x,
|
|
177
|
+
y: slice.position.y,
|
|
178
|
+
width: slice.size.width,
|
|
179
|
+
height: slice.size.height,
|
|
180
|
+
};
|
|
181
|
+
const actors = [];
|
|
182
|
+
for (const actor of model.actors.values()) {
|
|
183
|
+
const actorRect = {
|
|
184
|
+
x: actor.position.x,
|
|
185
|
+
y: actor.position.y,
|
|
186
|
+
width: actor.size.width,
|
|
187
|
+
height: actor.size.height,
|
|
188
|
+
};
|
|
189
|
+
if (rectanglesOverlap(sliceRect, actorRect)) {
|
|
190
|
+
actors.push({
|
|
191
|
+
name: actor.name,
|
|
192
|
+
authRequired: false, // Default - we don't track this property yet
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return actors;
|
|
197
|
+
}
|
|
144
198
|
export function exportEventmodelToJson(model) {
|
|
145
199
|
const specSlices = [];
|
|
146
200
|
// Sort slices by position
|
|
@@ -157,18 +211,21 @@ export function exportEventmodelToJson(model) {
|
|
|
157
211
|
commands.push(cmd);
|
|
158
212
|
}
|
|
159
213
|
}
|
|
214
|
+
// Filter out linked copies - only export originals
|
|
160
215
|
for (const evt of model.events.values()) {
|
|
161
|
-
if (isInSlice(slice, evt.position, evt.width, evt.height)) {
|
|
216
|
+
if (!evt.originalNodeId && isInSlice(slice, evt.position, evt.width, evt.height)) {
|
|
162
217
|
events.push(evt);
|
|
163
218
|
}
|
|
164
219
|
}
|
|
220
|
+
// Filter out linked copies - only export originals
|
|
165
221
|
for (const rm of model.readModels.values()) {
|
|
166
|
-
if (isInSlice(slice, rm.position, rm.width, rm.height)) {
|
|
222
|
+
if (!rm.originalNodeId && isInSlice(slice, rm.position, rm.width, rm.height)) {
|
|
167
223
|
readModels.push(rm);
|
|
168
224
|
}
|
|
169
225
|
}
|
|
226
|
+
// Filter out linked copies - only export originals
|
|
170
227
|
for (const scr of model.screens.values()) {
|
|
171
|
-
if (isInSlice(slice, scr.position, scr.width, scr.height)) {
|
|
228
|
+
if (!scr.originalNodeId && isInSlice(slice, scr.position, scr.width, scr.height)) {
|
|
172
229
|
screens.push(scr);
|
|
173
230
|
}
|
|
174
231
|
}
|
|
@@ -236,6 +293,8 @@ export function exportEventmodelToJson(model) {
|
|
|
236
293
|
],
|
|
237
294
|
})),
|
|
238
295
|
tables: [],
|
|
296
|
+
actors: getActorsForSlice(model, slice),
|
|
297
|
+
aggregates: getAggregatesForSlice(model, slice),
|
|
239
298
|
specifications: scenarios.map(scenario => {
|
|
240
299
|
const givenSteps = scenario.givenEvents.map(given => {
|
|
241
300
|
const evt = model.events.get(given.eventStickyId);
|
|
@@ -6,16 +6,72 @@ function escapeXml(str) {
|
|
|
6
6
|
.replace(/"/g, '"')
|
|
7
7
|
.replace(/'/g, ''');
|
|
8
8
|
}
|
|
9
|
+
// Find which aggregate an event belongs to (center point inside aggregate bounds)
|
|
10
|
+
function findAggregateForEvent(model, event) {
|
|
11
|
+
const centerX = event.position.x + event.width / 2;
|
|
12
|
+
const centerY = event.position.y + event.height / 2;
|
|
13
|
+
for (const aggregate of model.aggregates.values()) {
|
|
14
|
+
const bounds = {
|
|
15
|
+
left: aggregate.position.x,
|
|
16
|
+
right: aggregate.position.x + aggregate.size.width,
|
|
17
|
+
top: aggregate.position.y,
|
|
18
|
+
bottom: aggregate.position.y + aggregate.size.height,
|
|
19
|
+
};
|
|
20
|
+
if (centerX >= bounds.left && centerX <= bounds.right && centerY >= bounds.top && centerY <= bounds.bottom) {
|
|
21
|
+
return aggregate;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
// Find which slice contains an event
|
|
27
|
+
function findSliceForEvent(model, event) {
|
|
28
|
+
for (const slice of model.slices.values()) {
|
|
29
|
+
const centerX = event.position.x + event.width / 2;
|
|
30
|
+
const centerY = event.position.y + event.height / 2;
|
|
31
|
+
if (centerX >= slice.position.x &&
|
|
32
|
+
centerX <= slice.position.x + slice.size.width &&
|
|
33
|
+
centerY >= slice.position.y &&
|
|
34
|
+
centerY <= slice.position.y + slice.size.height) {
|
|
35
|
+
return slice.name;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
9
40
|
export function listEvents(model) {
|
|
10
41
|
const events = [...model.events.values()];
|
|
11
42
|
if (events.length === 0) {
|
|
12
43
|
console.log('<events/>');
|
|
13
44
|
return;
|
|
14
45
|
}
|
|
15
|
-
|
|
46
|
+
// Sort: originals first, then copies; alphabetically within each group
|
|
47
|
+
const sorted = [...events].sort((a, b) => {
|
|
48
|
+
// Originals before copies
|
|
49
|
+
if (!a.originalNodeId && b.originalNodeId)
|
|
50
|
+
return -1;
|
|
51
|
+
if (a.originalNodeId && !b.originalNodeId)
|
|
52
|
+
return 1;
|
|
53
|
+
// Alphabetically by name
|
|
54
|
+
return a.name.localeCompare(b.name);
|
|
55
|
+
});
|
|
16
56
|
console.log('<events>');
|
|
17
57
|
for (const evt of sorted) {
|
|
18
|
-
|
|
58
|
+
const aggregate = findAggregateForEvent(model, evt);
|
|
59
|
+
const aggregateAttr = aggregate ? ` aggregate="${escapeXml(aggregate.name)}"` : '';
|
|
60
|
+
if (evt.originalNodeId) {
|
|
61
|
+
// This is a linked copy - show origin info
|
|
62
|
+
const original = model.events.get(evt.originalNodeId);
|
|
63
|
+
const originSlice = original ? findSliceForEvent(model, original) : null;
|
|
64
|
+
const originAttr = originSlice ? ` origin-slice="${escapeXml(originSlice)}"` : '';
|
|
65
|
+
console.log(` <event name="${escapeXml(evt.name)}" fields="${evt.fields.length}"${aggregateAttr} linked-copy="true"${originAttr}/>`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// This is an original - show copy count
|
|
69
|
+
const copyCount = evt.canonicalId
|
|
70
|
+
? [...model.events.values()].filter(e => e.canonicalId === evt.canonicalId && e.originalNodeId).length
|
|
71
|
+
: 0;
|
|
72
|
+
const copiesAttr = copyCount > 0 ? ` copies="${copyCount}"` : '';
|
|
73
|
+
console.log(` <event name="${escapeXml(evt.name)}" fields="${evt.fields.length}"${aggregateAttr}${copiesAttr}/>`);
|
|
74
|
+
}
|
|
19
75
|
}
|
|
20
76
|
console.log('</events>');
|
|
21
77
|
}
|