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,120 @@
|
|
|
1
|
+
import { appendEvent } from '../../lib/file-loader.js';
|
|
2
|
+
import { STATE_VIEW_SLICE, SLICE_GAP, calculateSlicePosition, validateSliceNameUnique, 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-view-slice name="..." after="..." before="...">
|
|
9
|
+
const sliceMatch = xml.match(/<state-view-slice([^>]*)>/);
|
|
10
|
+
if (!sliceMatch) {
|
|
11
|
+
throw new Error('Invalid XML: missing <state-view-slice> tag');
|
|
12
|
+
}
|
|
13
|
+
const sliceName = getAttr(sliceMatch[1], 'name');
|
|
14
|
+
if (!sliceName) {
|
|
15
|
+
throw new Error('Invalid XML: state-view-slice must have a name attribute');
|
|
16
|
+
}
|
|
17
|
+
const after = getAttr(sliceMatch[1], 'after');
|
|
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]);
|
|
29
|
+
return {
|
|
30
|
+
sliceName,
|
|
31
|
+
after,
|
|
32
|
+
before,
|
|
33
|
+
readModel: { name: readModelName, fields: readModelFields },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function createStateViewSlice(model, filePath, xmlInput) {
|
|
37
|
+
// Parse input
|
|
38
|
+
let input;
|
|
39
|
+
try {
|
|
40
|
+
input = parseXmlInput(xmlInput);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error(`Error: ${err.message}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
// Validate slice name is unique
|
|
47
|
+
try {
|
|
48
|
+
validateSliceNameUnique(model, input.sliceName);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
console.error(`Error: ${err.message}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// Calculate position
|
|
55
|
+
let slicePosition;
|
|
56
|
+
try {
|
|
57
|
+
slicePosition = calculateSlicePosition(model, STATE_VIEW_SLICE.width, {
|
|
58
|
+
after: input.after,
|
|
59
|
+
before: input.before,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.error(`Error: ${err.message}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
// Handle "before" positioning - shift existing slices
|
|
67
|
+
if (input.before) {
|
|
68
|
+
const shiftAmount = STATE_VIEW_SLICE.width + SLICE_GAP;
|
|
69
|
+
const toShift = getSlicesToShift(model, slicePosition.x, shiftAmount);
|
|
70
|
+
for (const { sliceId, newX, currentY } of toShift) {
|
|
71
|
+
appendEvent(filePath, {
|
|
72
|
+
type: 'SliceMoved',
|
|
73
|
+
sliceId,
|
|
74
|
+
position: { x: newX, y: currentY },
|
|
75
|
+
timestamp: Date.now(),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Generate IDs
|
|
80
|
+
const sliceId = crypto.randomUUID();
|
|
81
|
+
const readModelId = crypto.randomUUID();
|
|
82
|
+
// Convert field inputs to fields with IDs
|
|
83
|
+
const readModelFields = input.readModel.fields.map(fieldInputToField);
|
|
84
|
+
// Calculate absolute position for read model within the slice
|
|
85
|
+
const readModelPosition = {
|
|
86
|
+
x: slicePosition.x + STATE_VIEW_SLICE.readModel.offsetX,
|
|
87
|
+
y: slicePosition.y + STATE_VIEW_SLICE.readModel.offsetY,
|
|
88
|
+
};
|
|
89
|
+
// Emit atomic events in sequence
|
|
90
|
+
// 1. Create the slice
|
|
91
|
+
appendEvent(filePath, {
|
|
92
|
+
type: 'SlicePlaced',
|
|
93
|
+
sliceId,
|
|
94
|
+
name: input.sliceName,
|
|
95
|
+
position: slicePosition,
|
|
96
|
+
size: { width: STATE_VIEW_SLICE.width, height: STATE_VIEW_SLICE.height },
|
|
97
|
+
timestamp: Date.now(),
|
|
98
|
+
});
|
|
99
|
+
// 2. Create the read model
|
|
100
|
+
appendEvent(filePath, {
|
|
101
|
+
type: 'ReadModelStickyPlaced',
|
|
102
|
+
readModelStickyId: readModelId,
|
|
103
|
+
name: input.readModel.name,
|
|
104
|
+
position: readModelPosition,
|
|
105
|
+
width: STATE_VIEW_SLICE.readModel.width,
|
|
106
|
+
height: STATE_VIEW_SLICE.readModel.height,
|
|
107
|
+
timestamp: Date.now(),
|
|
108
|
+
});
|
|
109
|
+
// 3. Add read model fields
|
|
110
|
+
for (const field of readModelFields) {
|
|
111
|
+
appendEvent(filePath, {
|
|
112
|
+
type: 'ReadModelFieldAdded',
|
|
113
|
+
readModelStickyId: readModelId,
|
|
114
|
+
field,
|
|
115
|
+
timestamp: Date.now(),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
console.log(`Created state-view slice "${input.sliceName}"`);
|
|
119
|
+
console.log(` ReadModel: ${input.readModel.name} (${readModelFields.length} fields)`);
|
|
120
|
+
}
|
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
.replace(/&/g, '&')
|
|
4
|
-
.replace(/</g, '<')
|
|
5
|
-
.replace(/>/g, '>')
|
|
6
|
-
.replace(/"/g, '"')
|
|
7
|
-
.replace(/'/g, ''');
|
|
8
|
-
}
|
|
9
|
-
export function listChapters(model) {
|
|
1
|
+
import { escapeXml, outputJson } from '../../lib/format.js';
|
|
2
|
+
export function listChapters(model, format) {
|
|
10
3
|
const chapters = [...model.chapters.values()];
|
|
4
|
+
// Sort by x position (left to right on timeline)
|
|
5
|
+
const sorted = [...chapters].sort((a, b) => a.position.x - b.position.x);
|
|
6
|
+
if (format === 'json') {
|
|
7
|
+
outputJson({
|
|
8
|
+
chapters: sorted.map(ch => ({ id: ch.id, name: ch.name }))
|
|
9
|
+
});
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
11
12
|
if (chapters.length === 0) {
|
|
12
13
|
console.log('<chapters/>');
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
15
|
-
// Sort by x position (left to right on timeline)
|
|
16
|
-
const sorted = [...chapters].sort((a, b) => a.position.x - b.position.x);
|
|
17
16
|
console.log('<chapters>');
|
|
18
17
|
for (const chapter of sorted) {
|
|
19
|
-
console.log(` <chapter name="${escapeXml(chapter.name)}"/>`);
|
|
18
|
+
console.log(` <chapter id="${chapter.id}" name="${escapeXml(chapter.name)}"/>`);
|
|
20
19
|
}
|
|
21
20
|
console.log('</chapters>');
|
|
22
21
|
}
|
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
.replace(/&/g, '&')
|
|
4
|
-
.replace(/</g, '<')
|
|
5
|
-
.replace(/>/g, '>')
|
|
6
|
-
.replace(/"/g, '"')
|
|
7
|
-
.replace(/'/g, ''');
|
|
8
|
-
}
|
|
9
|
-
export function listCommands(model) {
|
|
1
|
+
import { escapeXml, outputJson } from '../../lib/format.js';
|
|
2
|
+
export function listCommands(model, format) {
|
|
10
3
|
const commands = [...model.commands.values()];
|
|
4
|
+
const sorted = [...commands].sort((a, b) => a.name.localeCompare(b.name));
|
|
5
|
+
if (format === 'json') {
|
|
6
|
+
outputJson({
|
|
7
|
+
commands: sorted.map(cmd => ({ id: cmd.id, name: cmd.name, fields: cmd.fields.length }))
|
|
8
|
+
});
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
11
|
if (commands.length === 0) {
|
|
12
12
|
console.log('<commands/>');
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
|
-
const sorted = [...commands].sort((a, b) => a.name.localeCompare(b.name));
|
|
16
15
|
console.log('<commands>');
|
|
17
16
|
for (const cmd of sorted) {
|
|
18
|
-
console.log(` <command name="${escapeXml(cmd.name)}" fields="${cmd.fields.length}"/>`);
|
|
17
|
+
console.log(` <command id="${cmd.id}" name="${escapeXml(cmd.name)}" fields="${cmd.fields.length}"/>`);
|
|
19
18
|
}
|
|
20
19
|
console.log('</commands>');
|
|
21
20
|
}
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
return str
|
|
3
|
-
.replace(/&/g, '&')
|
|
4
|
-
.replace(/</g, '<')
|
|
5
|
-
.replace(/>/g, '>')
|
|
6
|
-
.replace(/"/g, '"')
|
|
7
|
-
.replace(/'/g, ''');
|
|
8
|
-
}
|
|
1
|
+
import { escapeXml, outputJson } from '../../lib/format.js';
|
|
9
2
|
// Find which aggregate an event belongs to (center point inside aggregate bounds)
|
|
10
3
|
function findAggregateForEvent(model, event) {
|
|
11
4
|
const centerX = event.position.x + event.width / 2;
|
|
@@ -37,12 +30,8 @@ function findSliceForEvent(model, event) {
|
|
|
37
30
|
}
|
|
38
31
|
return null;
|
|
39
32
|
}
|
|
40
|
-
export function listEvents(model) {
|
|
33
|
+
export function listEvents(model, format) {
|
|
41
34
|
const events = [...model.events.values()];
|
|
42
|
-
if (events.length === 0) {
|
|
43
|
-
console.log('<events/>');
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
35
|
// Sort: originals first, then copies; alphabetically within each group
|
|
47
36
|
const sorted = [...events].sort((a, b) => {
|
|
48
37
|
// Originals before copies
|
|
@@ -53,6 +42,38 @@ export function listEvents(model) {
|
|
|
53
42
|
// Alphabetically by name
|
|
54
43
|
return a.name.localeCompare(b.name);
|
|
55
44
|
});
|
|
45
|
+
if (format === 'json') {
|
|
46
|
+
outputJson({
|
|
47
|
+
events: sorted.map(evt => {
|
|
48
|
+
const aggregate = findAggregateForEvent(model, evt);
|
|
49
|
+
const result = {
|
|
50
|
+
id: evt.id,
|
|
51
|
+
name: evt.name,
|
|
52
|
+
fields: evt.fields.length,
|
|
53
|
+
};
|
|
54
|
+
if (aggregate)
|
|
55
|
+
result.aggregate = aggregate.name;
|
|
56
|
+
if (evt.originalNodeId) {
|
|
57
|
+
result.linkedCopy = true;
|
|
58
|
+
const original = model.events.get(evt.originalNodeId);
|
|
59
|
+
const originSlice = original ? findSliceForEvent(model, original) : null;
|
|
60
|
+
if (originSlice)
|
|
61
|
+
result.originSlice = originSlice;
|
|
62
|
+
}
|
|
63
|
+
else if (evt.canonicalId) {
|
|
64
|
+
const copyCount = [...model.events.values()].filter(e => e.canonicalId === evt.canonicalId && e.originalNodeId).length;
|
|
65
|
+
if (copyCount > 0)
|
|
66
|
+
result.copies = copyCount;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
})
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (events.length === 0) {
|
|
74
|
+
console.log('<events/>');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
56
77
|
console.log('<events>');
|
|
57
78
|
for (const evt of sorted) {
|
|
58
79
|
const aggregate = findAggregateForEvent(model, evt);
|
|
@@ -62,7 +83,7 @@ export function listEvents(model) {
|
|
|
62
83
|
const original = model.events.get(evt.originalNodeId);
|
|
63
84
|
const originSlice = original ? findSliceForEvent(model, original) : null;
|
|
64
85
|
const originAttr = originSlice ? ` origin-slice="${escapeXml(originSlice)}"` : '';
|
|
65
|
-
console.log(` <event name="${escapeXml(evt.name)}" fields="${evt.fields.length}"${aggregateAttr} linked-copy="true"${originAttr}/>`);
|
|
86
|
+
console.log(` <event id="${evt.id}" name="${escapeXml(evt.name)}" fields="${evt.fields.length}"${aggregateAttr} linked-copy="true"${originAttr}/>`);
|
|
66
87
|
}
|
|
67
88
|
else {
|
|
68
89
|
// This is an original - show copy count
|
|
@@ -70,7 +91,7 @@ export function listEvents(model) {
|
|
|
70
91
|
? [...model.events.values()].filter(e => e.canonicalId === evt.canonicalId && e.originalNodeId).length
|
|
71
92
|
: 0;
|
|
72
93
|
const copiesAttr = copyCount > 0 ? ` copies="${copyCount}"` : '';
|
|
73
|
-
console.log(` <event name="${escapeXml(evt.name)}" fields="${evt.fields.length}"${aggregateAttr}${copiesAttr}/>`);
|
|
94
|
+
console.log(` <event id="${evt.id}" name="${escapeXml(evt.name)}" fields="${evt.fields.length}"${aggregateAttr}${copiesAttr}/>`);
|
|
74
95
|
}
|
|
75
96
|
}
|
|
76
97
|
console.log('</events>');
|
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
.replace(/&/g, '&')
|
|
4
|
-
.replace(/</g, '<')
|
|
5
|
-
.replace(/>/g, '>')
|
|
6
|
-
.replace(/"/g, '"')
|
|
7
|
-
.replace(/'/g, ''');
|
|
8
|
-
}
|
|
9
|
-
export function listSlices(model) {
|
|
1
|
+
import { escapeXml, outputJson } from '../../lib/format.js';
|
|
2
|
+
export function listSlices(model, format) {
|
|
10
3
|
const slices = [...model.slices.values()];
|
|
4
|
+
const sorted = [...slices].sort((a, b) => a.position.x - b.position.x);
|
|
5
|
+
if (format === 'json') {
|
|
6
|
+
outputJson({
|
|
7
|
+
slices: sorted.map(s => ({ id: s.id, name: s.name, status: s.status }))
|
|
8
|
+
});
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
11
|
if (slices.length === 0) {
|
|
12
12
|
console.log('<slices/>');
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
|
-
const sorted = [...slices].sort((a, b) => a.position.x - b.position.x);
|
|
16
15
|
console.log('<slices>');
|
|
17
16
|
for (const slice of sorted) {
|
|
18
|
-
console.log(` <slice name="${escapeXml(slice.name)}" status="${slice.status}"/>`);
|
|
17
|
+
console.log(` <slice id="${slice.id}" name="${escapeXml(slice.name)}" status="${slice.status}"/>`);
|
|
19
18
|
}
|
|
20
19
|
console.log('</slices>');
|
|
21
20
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { appendEvent } from '../../lib/file-loader.js';
|
|
2
|
+
import { findElementOrExit } from '../../lib/element-lookup.js';
|
|
2
3
|
const validStatuses = ['created', 'in-progress', 'blocked', 'done'];
|
|
3
4
|
const eventTypeMap = {
|
|
4
5
|
'created': 'SliceMarkedAsCreated',
|
|
@@ -13,17 +14,7 @@ export function markSliceStatus(model, filePath, sliceName, status) {
|
|
|
13
14
|
process.exit(1);
|
|
14
15
|
}
|
|
15
16
|
const newStatus = status;
|
|
16
|
-
const
|
|
17
|
-
const sliceNameLower = sliceName.toLowerCase();
|
|
18
|
-
const slice = slices.find(s => s.name.toLowerCase() === sliceNameLower || s.name.toLowerCase().includes(sliceNameLower));
|
|
19
|
-
if (!slice) {
|
|
20
|
-
console.error(`Error: Slice not found: ${sliceName}`);
|
|
21
|
-
console.error('Available slices:');
|
|
22
|
-
for (const s of slices) {
|
|
23
|
-
console.error(` - ${s.name}`);
|
|
24
|
-
}
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
17
|
+
const slice = findElementOrExit(model.slices, sliceName, 'slice');
|
|
27
18
|
if (slice.status === newStatus) {
|
|
28
19
|
console.log(`Slice "${slice.name}" is already marked as ${newStatus}`);
|
|
29
20
|
return;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { appendEvent } from '../../lib/file-loader.js';
|
|
2
|
+
import { findElementOrExit } from '../../lib/element-lookup.js';
|
|
2
3
|
export function removeField(model, filePath, options, fieldName) {
|
|
3
4
|
// Determine which entity type
|
|
4
5
|
const entityCount = [options.command, options.event, options.readModel].filter(Boolean).length;
|
|
@@ -21,17 +22,7 @@ export function removeField(model, filePath, options, fieldName) {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
function removeFieldFromCommand(model, filePath, commandName, fieldName) {
|
|
24
|
-
const
|
|
25
|
-
const commands = [...model.commands.values()];
|
|
26
|
-
const command = commands.find(c => c.name.toLowerCase() === nameLower || c.name.toLowerCase().includes(nameLower));
|
|
27
|
-
if (!command) {
|
|
28
|
-
console.error(`Error: Command not found: ${commandName}`);
|
|
29
|
-
console.error('Available commands:');
|
|
30
|
-
for (const c of commands) {
|
|
31
|
-
console.error(` - ${c.name}`);
|
|
32
|
-
}
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
25
|
+
const command = findElementOrExit(model.commands, commandName, 'command');
|
|
35
26
|
const fieldNameLower = fieldName.toLowerCase();
|
|
36
27
|
const field = command.fields.find(f => f.name.toLowerCase() === fieldNameLower);
|
|
37
28
|
if (!field) {
|
|
@@ -56,17 +47,7 @@ function removeFieldFromCommand(model, filePath, commandName, fieldName) {
|
|
|
56
47
|
console.log(`Removed field "${field.name}" from command "${command.name}"`);
|
|
57
48
|
}
|
|
58
49
|
function removeFieldFromEvent(model, filePath, eventName, fieldName) {
|
|
59
|
-
const
|
|
60
|
-
const events = [...model.events.values()];
|
|
61
|
-
const event = events.find(e => e.name.toLowerCase() === nameLower || e.name.toLowerCase().includes(nameLower));
|
|
62
|
-
if (!event) {
|
|
63
|
-
console.error(`Error: Event not found: ${eventName}`);
|
|
64
|
-
console.error('Available events:');
|
|
65
|
-
for (const e of events) {
|
|
66
|
-
console.error(` - ${e.name}`);
|
|
67
|
-
}
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
50
|
+
const event = findElementOrExit(model.events, eventName, 'event');
|
|
70
51
|
const fieldNameLower = fieldName.toLowerCase();
|
|
71
52
|
const field = event.fields.find(f => f.name.toLowerCase() === fieldNameLower);
|
|
72
53
|
if (!field) {
|
|
@@ -91,17 +72,7 @@ function removeFieldFromEvent(model, filePath, eventName, fieldName) {
|
|
|
91
72
|
console.log(`Removed field "${field.name}" from event "${event.name}"`);
|
|
92
73
|
}
|
|
93
74
|
function removeFieldFromReadModel(model, filePath, readModelName, fieldName) {
|
|
94
|
-
const
|
|
95
|
-
const readModels = [...model.readModels.values()];
|
|
96
|
-
const readModel = readModels.find(rm => rm.name.toLowerCase() === nameLower || rm.name.toLowerCase().includes(nameLower));
|
|
97
|
-
if (!readModel) {
|
|
98
|
-
console.error(`Error: Read model not found: ${readModelName}`);
|
|
99
|
-
console.error('Available read models:');
|
|
100
|
-
for (const rm of readModels) {
|
|
101
|
-
console.error(` - ${rm.name}`);
|
|
102
|
-
}
|
|
103
|
-
process.exit(1);
|
|
104
|
-
}
|
|
75
|
+
const readModel = findElementOrExit(model.readModels, readModelName, 'read model');
|
|
105
76
|
const fieldNameLower = fieldName.toLowerCase();
|
|
106
77
|
const field = readModel.fields.find(f => f.name.toLowerCase() === fieldNameLower);
|
|
107
78
|
if (!field) {
|
|
@@ -1,31 +1,65 @@
|
|
|
1
1
|
import { appendEvent } from '../../lib/file-loader.js';
|
|
2
|
+
import { findElement, formatElementWithId } from '../../lib/element-lookup.js';
|
|
2
3
|
export function removeScenario(model, filePath, scenarioName, sliceName) {
|
|
4
|
+
// Check for UUID lookup
|
|
5
|
+
if (scenarioName.startsWith('id:')) {
|
|
6
|
+
const idSearch = scenarioName.slice(3).toLowerCase();
|
|
7
|
+
const scenario = [...model.scenarios.values()].find(s => s.id.toLowerCase().startsWith(idSearch));
|
|
8
|
+
if (!scenario) {
|
|
9
|
+
console.error(`Error: Scenario not found with ID starting with: "${idSearch}"`);
|
|
10
|
+
console.error('Available scenarios:');
|
|
11
|
+
for (const s of model.scenarios.values()) {
|
|
12
|
+
const slice = model.slices.get(s.sliceId);
|
|
13
|
+
console.error(` - "${s.name}" (id: ${s.id.slice(0, 8)}) (in slice "${slice?.name ?? 'unknown'}")`);
|
|
14
|
+
}
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const slice = model.slices.get(scenario.sliceId);
|
|
18
|
+
appendEvent(filePath, {
|
|
19
|
+
type: 'ScenarioRemoved',
|
|
20
|
+
scenarioId: scenario.id,
|
|
21
|
+
timestamp: Date.now(),
|
|
22
|
+
});
|
|
23
|
+
console.log(`Removed scenario "${scenario.name}" from slice "${slice?.name ?? 'unknown'}"`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Case-insensitive exact name match
|
|
3
27
|
const scenarioNameLower = scenarioName.toLowerCase();
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// If slice name provided, filter to that slice
|
|
28
|
+
let matchingScenarios = [...model.scenarios.values()].filter(s => s.name.toLowerCase() === scenarioNameLower);
|
|
29
|
+
// If slice name provided, filter to that slice using exact match
|
|
7
30
|
if (sliceName && matchingScenarios.length > 0) {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
31
|
+
const sliceResult = findElement(model.slices, sliceName);
|
|
32
|
+
if (sliceResult.success) {
|
|
33
|
+
matchingScenarios = matchingScenarios.filter(s => s.sliceId === sliceResult.element.id);
|
|
34
|
+
}
|
|
35
|
+
else if (sliceResult.error === 'ambiguous') {
|
|
36
|
+
console.error(`Error: Multiple slices found with name "${sliceName}"`);
|
|
37
|
+
console.error('Please specify using the slice ID:');
|
|
38
|
+
for (const s of sliceResult.matches) {
|
|
39
|
+
console.error(` - ${formatElementWithId(s, false)}`);
|
|
40
|
+
}
|
|
41
|
+
process.exit(1);
|
|
12
42
|
}
|
|
43
|
+
// If slice not found, just proceed without filtering
|
|
13
44
|
}
|
|
14
45
|
if (matchingScenarios.length === 0) {
|
|
15
|
-
console.error(`Error: Scenario not found: ${scenarioName}`);
|
|
46
|
+
console.error(`Error: Scenario not found: "${scenarioName}"`);
|
|
16
47
|
console.error('Available scenarios:');
|
|
17
48
|
for (const s of model.scenarios.values()) {
|
|
18
49
|
const slice = model.slices.get(s.sliceId);
|
|
19
|
-
console.error(` - "${s.name}" (in slice "${slice?.name ?? 'unknown'}")`);
|
|
50
|
+
console.error(` - "${s.name}" (id: ${s.id.slice(0, 8)}) (in slice "${slice?.name ?? 'unknown'}")`);
|
|
20
51
|
}
|
|
21
52
|
process.exit(1);
|
|
22
53
|
}
|
|
23
54
|
if (matchingScenarios.length > 1) {
|
|
24
|
-
console.error(`Error: Multiple scenarios
|
|
55
|
+
console.error(`Error: Multiple scenarios found with name "${scenarioName}"`);
|
|
56
|
+
console.error('Please specify using the scenario ID or --slice to disambiguate:');
|
|
25
57
|
for (const s of matchingScenarios) {
|
|
26
58
|
const slice = model.slices.get(s.sliceId);
|
|
27
|
-
console.error(` - "${s.name}" in slice "${slice?.name ?? 'unknown'}"`);
|
|
59
|
+
console.error(` - "${s.name}" (id: ${s.id.slice(0, 8)}) (in slice "${slice?.name ?? 'unknown'}")`);
|
|
28
60
|
}
|
|
61
|
+
console.error('');
|
|
62
|
+
console.error(`Usage: eventmodeler remove scenario "id:${matchingScenarios[0].id.slice(0, 8)}"`);
|
|
29
63
|
process.exit(1);
|
|
30
64
|
}
|
|
31
65
|
const scenario = matchingScenarios[0];
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { EventModel } from '../../types.js';
|
|
2
|
-
|
|
2
|
+
import { type OutputFormat } from '../../lib/format.js';
|
|
3
|
+
export declare function search(model: EventModel, term: string, format: OutputFormat): void;
|