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,216 @@
|
|
|
1
|
+
import { appendEvent } from '../../lib/file-loader.js';
|
|
2
|
+
function flattenFields(fields, prefix = '') {
|
|
3
|
+
const result = [];
|
|
4
|
+
for (const field of fields) {
|
|
5
|
+
const path = prefix ? `${prefix}.${field.name}` : field.name;
|
|
6
|
+
result.push({ id: field.id, path, field });
|
|
7
|
+
if (field.fieldType === 'Custom' && field.subfields) {
|
|
8
|
+
result.push(...flattenFields(field.subfields, path));
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
function parseJsonInput(input) {
|
|
14
|
+
const parsed = JSON.parse(input);
|
|
15
|
+
if (!Array.isArray(parsed)) {
|
|
16
|
+
return [parsed];
|
|
17
|
+
}
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
function parseXmlInput(input) {
|
|
21
|
+
const mappings = [];
|
|
22
|
+
const mappingMatches = input.matchAll(/<mapping[^>]*from="([^"]*)"[^>]*to="([^"]*)"[^>]*\/?>/g);
|
|
23
|
+
for (const match of mappingMatches) {
|
|
24
|
+
mappings.push({ from: match[1], to: match[2] });
|
|
25
|
+
}
|
|
26
|
+
// Also try alternate attribute order
|
|
27
|
+
const altMatches = input.matchAll(/<mapping[^>]*to="([^"]*)"[^>]*from="([^"]*)"[^>]*\/?>/g);
|
|
28
|
+
for (const match of altMatches) {
|
|
29
|
+
// Check if this mapping was already added
|
|
30
|
+
const exists = mappings.some(m => m.from === match[2] && m.to === match[1]);
|
|
31
|
+
if (!exists) {
|
|
32
|
+
mappings.push({ from: match[2], to: match[1] });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (mappings.length === 0) {
|
|
36
|
+
throw new Error('No <mapping from="..." to="..."/> elements found in XML');
|
|
37
|
+
}
|
|
38
|
+
return mappings;
|
|
39
|
+
}
|
|
40
|
+
function parseInput(input) {
|
|
41
|
+
const trimmed = input.trim();
|
|
42
|
+
if (trimmed.startsWith('<')) {
|
|
43
|
+
return parseXmlInput(trimmed);
|
|
44
|
+
}
|
|
45
|
+
return parseJsonInput(trimmed);
|
|
46
|
+
}
|
|
47
|
+
export function mapFields(model, filePath, flowIdentifier, input) {
|
|
48
|
+
// Parse the mapping input
|
|
49
|
+
let mappingInputs;
|
|
50
|
+
try {
|
|
51
|
+
mappingInputs = parseInput(input);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
console.error(`Error: Invalid input format: ${err.message}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
// Find the flow - can be identified by:
|
|
58
|
+
// 1. Flow ID directly
|
|
59
|
+
// 2. "SourceName→TargetName" format (using → or ->)
|
|
60
|
+
let flow = model.flows.get(flowIdentifier);
|
|
61
|
+
if (!flow) {
|
|
62
|
+
// Try to parse as "Source→Target" or "Source->Target"
|
|
63
|
+
const arrowMatch = flowIdentifier.match(/^(.+?)(?:→|->)(.+)$/);
|
|
64
|
+
if (arrowMatch) {
|
|
65
|
+
const sourceName = arrowMatch[1].trim().toLowerCase();
|
|
66
|
+
const targetName = arrowMatch[2].trim().toLowerCase();
|
|
67
|
+
// Find matching flow
|
|
68
|
+
for (const f of model.flows.values()) {
|
|
69
|
+
let sourceEntityName = '';
|
|
70
|
+
let targetEntityName = '';
|
|
71
|
+
// Get source name
|
|
72
|
+
const sourceEvent = model.events.get(f.sourceId);
|
|
73
|
+
const sourceReadModel = model.readModels.get(f.sourceId);
|
|
74
|
+
const sourceScreen = model.screens.get(f.sourceId);
|
|
75
|
+
const sourceProcessor = model.processors.get(f.sourceId);
|
|
76
|
+
const sourceCommand = model.commands.get(f.sourceId);
|
|
77
|
+
if (sourceEvent)
|
|
78
|
+
sourceEntityName = sourceEvent.name.toLowerCase();
|
|
79
|
+
else if (sourceReadModel)
|
|
80
|
+
sourceEntityName = sourceReadModel.name.toLowerCase();
|
|
81
|
+
else if (sourceScreen)
|
|
82
|
+
sourceEntityName = sourceScreen.name.toLowerCase();
|
|
83
|
+
else if (sourceProcessor)
|
|
84
|
+
sourceEntityName = sourceProcessor.name.toLowerCase();
|
|
85
|
+
else if (sourceCommand)
|
|
86
|
+
sourceEntityName = sourceCommand.name.toLowerCase();
|
|
87
|
+
// Get target name
|
|
88
|
+
const targetEvent = model.events.get(f.targetId);
|
|
89
|
+
const targetReadModel = model.readModels.get(f.targetId);
|
|
90
|
+
const targetScreen = model.screens.get(f.targetId);
|
|
91
|
+
const targetProcessor = model.processors.get(f.targetId);
|
|
92
|
+
const targetCommand = model.commands.get(f.targetId);
|
|
93
|
+
if (targetEvent)
|
|
94
|
+
targetEntityName = targetEvent.name.toLowerCase();
|
|
95
|
+
else if (targetReadModel)
|
|
96
|
+
targetEntityName = targetReadModel.name.toLowerCase();
|
|
97
|
+
else if (targetScreen)
|
|
98
|
+
targetEntityName = targetScreen.name.toLowerCase();
|
|
99
|
+
else if (targetProcessor)
|
|
100
|
+
targetEntityName = targetProcessor.name.toLowerCase();
|
|
101
|
+
else if (targetCommand)
|
|
102
|
+
targetEntityName = targetCommand.name.toLowerCase();
|
|
103
|
+
// Check for match (exact or partial)
|
|
104
|
+
const sourceMatches = sourceEntityName === sourceName || sourceEntityName.includes(sourceName);
|
|
105
|
+
const targetMatches = targetEntityName === targetName || targetEntityName.includes(targetName);
|
|
106
|
+
if (sourceMatches && targetMatches) {
|
|
107
|
+
flow = f;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!flow) {
|
|
114
|
+
console.error(`Error: Flow not found: ${flowIdentifier}`);
|
|
115
|
+
console.error('Available flows:');
|
|
116
|
+
for (const f of model.flows.values()) {
|
|
117
|
+
const source = getEntityName(model, f.sourceId);
|
|
118
|
+
const target = getEntityName(model, f.targetId);
|
|
119
|
+
console.error(` - ${source}→${target} (${f.id})`);
|
|
120
|
+
}
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
// Get source and target fields
|
|
124
|
+
const sourceFields = getEntityFields(model, flow.sourceId);
|
|
125
|
+
const targetFields = getEntityFields(model, flow.targetId);
|
|
126
|
+
const flatSource = flattenFields(sourceFields);
|
|
127
|
+
const flatTarget = flattenFields(targetFields);
|
|
128
|
+
// Resolve mapping inputs to field IDs
|
|
129
|
+
const mappings = [];
|
|
130
|
+
for (const m of mappingInputs) {
|
|
131
|
+
// Find source field (by name/path or ID)
|
|
132
|
+
const sourceField = flatSource.find(sf => sf.id === m.from || sf.path.toLowerCase() === m.from.toLowerCase());
|
|
133
|
+
if (!sourceField) {
|
|
134
|
+
console.error(`Error: Source field not found: ${m.from}`);
|
|
135
|
+
console.error('Available source fields:');
|
|
136
|
+
for (const sf of flatSource) {
|
|
137
|
+
console.error(` - ${sf.path} (${sf.id})`);
|
|
138
|
+
}
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
// Find target field (by name/path or ID)
|
|
142
|
+
const targetField = flatTarget.find(tf => tf.id === m.to || tf.path.toLowerCase() === m.to.toLowerCase());
|
|
143
|
+
if (!targetField) {
|
|
144
|
+
console.error(`Error: Target field not found: ${m.to}`);
|
|
145
|
+
console.error('Available target fields:');
|
|
146
|
+
for (const tf of flatTarget) {
|
|
147
|
+
console.error(` - ${tf.path} (${tf.id})`);
|
|
148
|
+
}
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
mappings.push({
|
|
152
|
+
sourceFieldId: sourceField.id,
|
|
153
|
+
targetFieldId: targetField.id,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// Get existing mappings and merge
|
|
157
|
+
const existingMappings = flow.fieldMappings ?? [];
|
|
158
|
+
const mergedMappings = [...existingMappings];
|
|
159
|
+
for (const newMapping of mappings) {
|
|
160
|
+
// Remove any existing mapping for the same target field
|
|
161
|
+
const existingIndex = mergedMappings.findIndex(m => m.targetFieldId === newMapping.targetFieldId);
|
|
162
|
+
if (existingIndex >= 0) {
|
|
163
|
+
mergedMappings.splice(existingIndex, 1);
|
|
164
|
+
}
|
|
165
|
+
mergedMappings.push(newMapping);
|
|
166
|
+
}
|
|
167
|
+
// Append the event
|
|
168
|
+
appendEvent(filePath, {
|
|
169
|
+
type: 'FieldMappingSpecified',
|
|
170
|
+
flowId: flow.id,
|
|
171
|
+
mappings: mergedMappings,
|
|
172
|
+
timestamp: Date.now(),
|
|
173
|
+
});
|
|
174
|
+
const sourceName = getEntityName(model, flow.sourceId);
|
|
175
|
+
const targetName = getEntityName(model, flow.targetId);
|
|
176
|
+
console.log(`Mapped ${mappings.length} field(s) on flow ${sourceName}→${targetName}`);
|
|
177
|
+
for (const m of mappingInputs) {
|
|
178
|
+
console.log(` ${m.from} → ${m.to}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function getEntityName(model, entityId) {
|
|
182
|
+
const event = model.events.get(entityId);
|
|
183
|
+
if (event)
|
|
184
|
+
return event.name;
|
|
185
|
+
const readModel = model.readModels.get(entityId);
|
|
186
|
+
if (readModel)
|
|
187
|
+
return readModel.name;
|
|
188
|
+
const command = model.commands.get(entityId);
|
|
189
|
+
if (command)
|
|
190
|
+
return command.name;
|
|
191
|
+
const screen = model.screens.get(entityId);
|
|
192
|
+
if (screen)
|
|
193
|
+
return screen.name;
|
|
194
|
+
const processor = model.processors.get(entityId);
|
|
195
|
+
if (processor)
|
|
196
|
+
return processor.name;
|
|
197
|
+
return entityId;
|
|
198
|
+
}
|
|
199
|
+
function getEntityFields(model, entityId) {
|
|
200
|
+
const event = model.events.get(entityId);
|
|
201
|
+
if (event)
|
|
202
|
+
return event.fields;
|
|
203
|
+
const readModel = model.readModels.get(entityId);
|
|
204
|
+
if (readModel)
|
|
205
|
+
return readModel.fields;
|
|
206
|
+
const command = model.commands.get(entityId);
|
|
207
|
+
if (command)
|
|
208
|
+
return command.fields;
|
|
209
|
+
const screen = model.screens.get(entityId);
|
|
210
|
+
if (screen)
|
|
211
|
+
return screen.fields;
|
|
212
|
+
const processor = model.processors.get(entityId);
|
|
213
|
+
if (processor)
|
|
214
|
+
return processor.fields;
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { appendEvent } from '../../lib/file-loader.js';
|
|
2
|
+
export function removeField(model, filePath, options, fieldName) {
|
|
3
|
+
// Determine which entity type
|
|
4
|
+
const entityCount = [options.command, options.event, options.readModel].filter(Boolean).length;
|
|
5
|
+
if (entityCount === 0) {
|
|
6
|
+
console.error('Error: Must specify one of --command, --event, or --read-model');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
if (entityCount > 1) {
|
|
10
|
+
console.error('Error: Can only specify one of --command, --event, or --read-model');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
if (options.command) {
|
|
14
|
+
removeFieldFromCommand(model, filePath, options.command, fieldName);
|
|
15
|
+
}
|
|
16
|
+
else if (options.event) {
|
|
17
|
+
removeFieldFromEvent(model, filePath, options.event, fieldName);
|
|
18
|
+
}
|
|
19
|
+
else if (options.readModel) {
|
|
20
|
+
removeFieldFromReadModel(model, filePath, options.readModel, fieldName);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function removeFieldFromCommand(model, filePath, commandName, fieldName) {
|
|
24
|
+
const nameLower = commandName.toLowerCase();
|
|
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
|
+
}
|
|
35
|
+
const fieldNameLower = fieldName.toLowerCase();
|
|
36
|
+
const field = command.fields.find(f => f.name.toLowerCase() === fieldNameLower);
|
|
37
|
+
if (!field) {
|
|
38
|
+
console.error(`Error: Field "${fieldName}" not found on command "${command.name}"`);
|
|
39
|
+
if (command.fields.length > 0) {
|
|
40
|
+
console.error('Available fields:');
|
|
41
|
+
for (const f of command.fields) {
|
|
42
|
+
console.error(` - ${f.name} (${f.fieldType})`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.error('This command has no fields.');
|
|
47
|
+
}
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
appendEvent(filePath, {
|
|
51
|
+
type: 'CommandFieldRemoved',
|
|
52
|
+
commandStickyId: command.id,
|
|
53
|
+
fieldId: field.id,
|
|
54
|
+
timestamp: Date.now(),
|
|
55
|
+
});
|
|
56
|
+
console.log(`Removed field "${field.name}" from command "${command.name}"`);
|
|
57
|
+
}
|
|
58
|
+
function removeFieldFromEvent(model, filePath, eventName, fieldName) {
|
|
59
|
+
const nameLower = eventName.toLowerCase();
|
|
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
|
+
}
|
|
70
|
+
const fieldNameLower = fieldName.toLowerCase();
|
|
71
|
+
const field = event.fields.find(f => f.name.toLowerCase() === fieldNameLower);
|
|
72
|
+
if (!field) {
|
|
73
|
+
console.error(`Error: Field "${fieldName}" not found on event "${event.name}"`);
|
|
74
|
+
if (event.fields.length > 0) {
|
|
75
|
+
console.error('Available fields:');
|
|
76
|
+
for (const f of event.fields) {
|
|
77
|
+
console.error(` - ${f.name} (${f.fieldType})`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
console.error('This event has no fields.');
|
|
82
|
+
}
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
appendEvent(filePath, {
|
|
86
|
+
type: 'EventFieldRemoved',
|
|
87
|
+
eventStickyId: event.id,
|
|
88
|
+
fieldId: field.id,
|
|
89
|
+
timestamp: Date.now(),
|
|
90
|
+
});
|
|
91
|
+
console.log(`Removed field "${field.name}" from event "${event.name}"`);
|
|
92
|
+
}
|
|
93
|
+
function removeFieldFromReadModel(model, filePath, readModelName, fieldName) {
|
|
94
|
+
const nameLower = readModelName.toLowerCase();
|
|
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
|
+
}
|
|
105
|
+
const fieldNameLower = fieldName.toLowerCase();
|
|
106
|
+
const field = readModel.fields.find(f => f.name.toLowerCase() === fieldNameLower);
|
|
107
|
+
if (!field) {
|
|
108
|
+
console.error(`Error: Field "${fieldName}" not found on read model "${readModel.name}"`);
|
|
109
|
+
if (readModel.fields.length > 0) {
|
|
110
|
+
console.error('Available fields:');
|
|
111
|
+
for (const f of readModel.fields) {
|
|
112
|
+
console.error(` - ${f.name} (${f.fieldType})`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.error('This read model has no fields.');
|
|
117
|
+
}
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
appendEvent(filePath, {
|
|
121
|
+
type: 'ReadModelFieldRemoved',
|
|
122
|
+
readModelStickyId: readModel.id,
|
|
123
|
+
fieldId: field.id,
|
|
124
|
+
timestamp: Date.now(),
|
|
125
|
+
});
|
|
126
|
+
console.log(`Removed field "${field.name}" from read model "${readModel.name}"`);
|
|
127
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { appendEvent } from '../../lib/file-loader.js';
|
|
2
|
+
export function removeScenario(model, filePath, scenarioName, sliceName) {
|
|
3
|
+
const scenarioNameLower = scenarioName.toLowerCase();
|
|
4
|
+
// Find scenarios matching the name
|
|
5
|
+
let matchingScenarios = [...model.scenarios.values()].filter(s => s.name.toLowerCase() === scenarioNameLower || s.name.toLowerCase().includes(scenarioNameLower));
|
|
6
|
+
// If slice name provided, filter to that slice
|
|
7
|
+
if (sliceName && matchingScenarios.length > 0) {
|
|
8
|
+
const sliceNameLower = sliceName.toLowerCase();
|
|
9
|
+
const slice = [...model.slices.values()].find(s => s.name.toLowerCase() === sliceNameLower || s.name.toLowerCase().includes(sliceNameLower));
|
|
10
|
+
if (slice) {
|
|
11
|
+
matchingScenarios = matchingScenarios.filter(s => s.sliceId === slice.id);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (matchingScenarios.length === 0) {
|
|
15
|
+
console.error(`Error: Scenario not found: ${scenarioName}`);
|
|
16
|
+
console.error('Available scenarios:');
|
|
17
|
+
for (const s of model.scenarios.values()) {
|
|
18
|
+
const slice = model.slices.get(s.sliceId);
|
|
19
|
+
console.error(` - "${s.name}" (in slice "${slice?.name ?? 'unknown'}")`);
|
|
20
|
+
}
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
if (matchingScenarios.length > 1) {
|
|
24
|
+
console.error(`Error: Multiple scenarios match "${scenarioName}". Use --slice to disambiguate:`);
|
|
25
|
+
for (const s of matchingScenarios) {
|
|
26
|
+
const slice = model.slices.get(s.sliceId);
|
|
27
|
+
console.error(` - "${s.name}" in slice "${slice?.name ?? 'unknown'}"`);
|
|
28
|
+
}
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const scenario = matchingScenarios[0];
|
|
32
|
+
const slice = model.slices.get(scenario.sliceId);
|
|
33
|
+
appendEvent(filePath, {
|
|
34
|
+
type: 'ScenarioRemoved',
|
|
35
|
+
scenarioId: scenario.id,
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
});
|
|
38
|
+
console.log(`Removed scenario "${scenario.name}" from slice "${slice?.name ?? 'unknown'}"`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
function escapeXml(str) {
|
|
2
|
+
return str
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"');
|
|
7
|
+
}
|
|
8
|
+
// Get screens whose center point is inside the actor bounds
|
|
9
|
+
function getScreensInActor(model, actor) {
|
|
10
|
+
const bounds = {
|
|
11
|
+
left: actor.position.x,
|
|
12
|
+
right: actor.position.x + actor.size.width,
|
|
13
|
+
top: actor.position.y,
|
|
14
|
+
bottom: actor.position.y + actor.size.height,
|
|
15
|
+
};
|
|
16
|
+
function isInActor(pos, width, height) {
|
|
17
|
+
const centerX = pos.x + width / 2;
|
|
18
|
+
const centerY = pos.y + height / 2;
|
|
19
|
+
return centerX >= bounds.left && centerX <= bounds.right && centerY >= bounds.top && centerY <= bounds.bottom;
|
|
20
|
+
}
|
|
21
|
+
return [...model.screens.values()].filter(s => isInActor(s.position, s.width, s.height));
|
|
22
|
+
}
|
|
23
|
+
// Find which slice contains a screen
|
|
24
|
+
function findSliceForScreen(model, screen) {
|
|
25
|
+
for (const slice of model.slices.values()) {
|
|
26
|
+
const centerX = screen.position.x + screen.width / 2;
|
|
27
|
+
const centerY = screen.position.y + screen.height / 2;
|
|
28
|
+
if (centerX >= slice.position.x &&
|
|
29
|
+
centerX <= slice.position.x + slice.size.width &&
|
|
30
|
+
centerY >= slice.position.y &&
|
|
31
|
+
centerY <= slice.position.y + slice.size.height) {
|
|
32
|
+
return slice.name;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
export function showActor(model, actorName) {
|
|
38
|
+
// Find the actor
|
|
39
|
+
const nameLower = actorName.toLowerCase();
|
|
40
|
+
const actors = [...model.actors.values()];
|
|
41
|
+
const actor = actors.find(a => a.name.toLowerCase() === nameLower || a.name.toLowerCase().includes(nameLower));
|
|
42
|
+
if (!actor) {
|
|
43
|
+
console.error(`Error: Actor not found: ${actorName}`);
|
|
44
|
+
console.error('Available actors:');
|
|
45
|
+
for (const a of actors) {
|
|
46
|
+
console.error(` - ${a.name}`);
|
|
47
|
+
}
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
// Get screens dynamically based on position (center point inside actor bounds)
|
|
51
|
+
const screensInActor = getScreensInActor(model, actor);
|
|
52
|
+
console.log(`<actor id="${actor.id}" name="${escapeXml(actor.name)}">`);
|
|
53
|
+
console.log(` <screens-count>${screensInActor.length}</screens-count>`);
|
|
54
|
+
if (screensInActor.length > 0) {
|
|
55
|
+
console.log(' <screens>');
|
|
56
|
+
for (const screen of screensInActor) {
|
|
57
|
+
const fieldCount = screen.fields.length;
|
|
58
|
+
if (screen.originalNodeId) {
|
|
59
|
+
// This is a linked copy - show origin info
|
|
60
|
+
const original = model.screens.get(screen.originalNodeId);
|
|
61
|
+
const originSlice = original ? findSliceForScreen(model, original) : null;
|
|
62
|
+
const originAttr = originSlice ? ` origin-slice="${escapeXml(originSlice)}"` : '';
|
|
63
|
+
console.log(` <screen id="${screen.id}" name="${escapeXml(screen.name)}" fields="${fieldCount}" linked-copy="true"${originAttr}/>`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.log(` <screen id="${screen.id}" name="${escapeXml(screen.name)}" fields="${fieldCount}"/>`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
console.log(' </screens>');
|
|
70
|
+
}
|
|
71
|
+
console.log('</actor>');
|
|
72
|
+
}
|
|
73
|
+
export function listActors(model) {
|
|
74
|
+
const actors = [...model.actors.values()];
|
|
75
|
+
if (actors.length === 0) {
|
|
76
|
+
console.log('<actors/>');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
console.log('<actors>');
|
|
80
|
+
for (const actor of actors) {
|
|
81
|
+
// Calculate screens dynamically based on position
|
|
82
|
+
const screensInActor = getScreensInActor(model, actor);
|
|
83
|
+
const originalCount = screensInActor.filter(s => !s.originalNodeId).length;
|
|
84
|
+
const copyCount = screensInActor.filter(s => s.originalNodeId).length;
|
|
85
|
+
const copiesAttr = copyCount > 0 ? ` copies="${copyCount}"` : '';
|
|
86
|
+
console.log(` <actor id="${actor.id}" name="${escapeXml(actor.name)}" screens="${originalCount}"${copiesAttr}/>`);
|
|
87
|
+
}
|
|
88
|
+
console.log('</actors>');
|
|
89
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
function escapeXml(str) {
|
|
2
|
+
return str
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"');
|
|
7
|
+
}
|
|
8
|
+
function findMatchingField(fields, targetName, targetType) {
|
|
9
|
+
for (const field of fields) {
|
|
10
|
+
// Check if name and type match
|
|
11
|
+
if (field.name.toLowerCase() === targetName.toLowerCase() && field.fieldType === targetType) {
|
|
12
|
+
return field;
|
|
13
|
+
}
|
|
14
|
+
// Check nested subfields
|
|
15
|
+
if (field.fieldType === 'Custom' && field.subfields) {
|
|
16
|
+
const nested = findMatchingField(field.subfields, targetName, targetType);
|
|
17
|
+
if (nested)
|
|
18
|
+
return nested;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
// Get events whose center point is inside the aggregate bounds
|
|
24
|
+
// Only returns original events, not linked copies (linked copies have originalNodeId set)
|
|
25
|
+
function getEventsInAggregate(model, aggregate) {
|
|
26
|
+
const bounds = {
|
|
27
|
+
left: aggregate.position.x,
|
|
28
|
+
right: aggregate.position.x + aggregate.size.width,
|
|
29
|
+
top: aggregate.position.y,
|
|
30
|
+
bottom: aggregate.position.y + aggregate.size.height,
|
|
31
|
+
};
|
|
32
|
+
function isInAggregate(pos, width, height) {
|
|
33
|
+
const centerX = pos.x + width / 2;
|
|
34
|
+
const centerY = pos.y + height / 2;
|
|
35
|
+
return centerX >= bounds.left && centerX <= bounds.right && centerY >= bounds.top && centerY <= bounds.bottom;
|
|
36
|
+
}
|
|
37
|
+
// Filter out linked copies - only return original events
|
|
38
|
+
// Linked copies have originalNodeId set, originals do not
|
|
39
|
+
return [...model.events.values()].filter(e => !e.originalNodeId && isInAggregate(e.position, e.width, e.height));
|
|
40
|
+
}
|
|
41
|
+
export function showAggregateCompleteness(model, aggregateName) {
|
|
42
|
+
// Find the aggregate
|
|
43
|
+
const nameLower = aggregateName.toLowerCase();
|
|
44
|
+
const aggregates = [...model.aggregates.values()];
|
|
45
|
+
const aggregate = aggregates.find(a => a.name.toLowerCase() === nameLower || a.name.toLowerCase().includes(nameLower));
|
|
46
|
+
if (!aggregate) {
|
|
47
|
+
console.error(`Error: Aggregate not found: ${aggregateName}`);
|
|
48
|
+
console.error('Available aggregates:');
|
|
49
|
+
for (const a of aggregates) {
|
|
50
|
+
console.error(` - ${a.name}`);
|
|
51
|
+
}
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// Get events dynamically based on position (center point inside aggregate bounds)
|
|
55
|
+
const eventsInAggregate = getEventsInAggregate(model, aggregate);
|
|
56
|
+
// Check if aggregate has ID field configured
|
|
57
|
+
if (!aggregate.aggregateIdFieldName || !aggregate.aggregateIdFieldType) {
|
|
58
|
+
console.log(`<aggregate-completeness aggregate="${escapeXml(aggregate.name)}" status="unconfigured">`);
|
|
59
|
+
console.log(` <events-count>${eventsInAggregate.length}</events-count>`);
|
|
60
|
+
console.log(' <events>');
|
|
61
|
+
for (const event of eventsInAggregate) {
|
|
62
|
+
console.log(` <event id="${event.id}" name="${escapeXml(event.name)}"/>`);
|
|
63
|
+
}
|
|
64
|
+
console.log(' </events>');
|
|
65
|
+
console.log(' <message>Aggregate ID field not configured. Set aggregateIdFieldName and aggregateIdFieldType.</message>');
|
|
66
|
+
console.log('</aggregate-completeness>');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const idFieldName = aggregate.aggregateIdFieldName;
|
|
70
|
+
const idFieldType = aggregate.aggregateIdFieldType;
|
|
71
|
+
if (eventsInAggregate.length === 0) {
|
|
72
|
+
console.log(`<aggregate-completeness aggregate="${escapeXml(aggregate.name)}" status="empty">`);
|
|
73
|
+
console.log(` <id-field name="${escapeXml(idFieldName)}" type="${idFieldType}"/>`);
|
|
74
|
+
console.log(' <message>No events in this aggregate.</message>');
|
|
75
|
+
console.log('</aggregate-completeness>');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Check each event for the ID field
|
|
79
|
+
const eventResults = [];
|
|
80
|
+
for (const event of eventsInAggregate) {
|
|
81
|
+
const matchingField = findMatchingField(event.fields, idFieldName, idFieldType);
|
|
82
|
+
eventResults.push({
|
|
83
|
+
eventId: event.id,
|
|
84
|
+
eventName: event.name,
|
|
85
|
+
hasIdField: !!matchingField,
|
|
86
|
+
matchingField: matchingField ? {
|
|
87
|
+
fieldId: matchingField.id,
|
|
88
|
+
fieldName: matchingField.name,
|
|
89
|
+
fieldType: matchingField.fieldType,
|
|
90
|
+
} : undefined,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const completeEvents = eventResults.filter(e => e.hasIdField);
|
|
94
|
+
const incompleteEvents = eventResults.filter(e => !e.hasIdField);
|
|
95
|
+
const isComplete = incompleteEvents.length === 0;
|
|
96
|
+
const overallStatus = isComplete ? 'complete' : 'incomplete';
|
|
97
|
+
const completionPercent = eventResults.length > 0
|
|
98
|
+
? Math.round((completeEvents.length / eventResults.length) * 100)
|
|
99
|
+
: 100;
|
|
100
|
+
console.log(`<aggregate-completeness aggregate="${escapeXml(aggregate.name)}" status="${overallStatus}" completion="${completionPercent}%">`);
|
|
101
|
+
console.log(` <id-field name="${escapeXml(idFieldName)}" type="${idFieldType}"/>`);
|
|
102
|
+
console.log(` <summary total="${eventResults.length}" complete="${completeEvents.length}" incomplete="${incompleteEvents.length}"/>`);
|
|
103
|
+
if (incompleteEvents.length > 0) {
|
|
104
|
+
console.log(' <incomplete-events>');
|
|
105
|
+
for (const event of incompleteEvents) {
|
|
106
|
+
console.log(` <event id="${event.eventId}" name="${escapeXml(event.eventName)}"/>`);
|
|
107
|
+
}
|
|
108
|
+
console.log(' </incomplete-events>');
|
|
109
|
+
}
|
|
110
|
+
if (completeEvents.length > 0) {
|
|
111
|
+
console.log(' <complete-events>');
|
|
112
|
+
for (const event of completeEvents) {
|
|
113
|
+
const fieldAttr = event.matchingField
|
|
114
|
+
? ` field-id="${event.matchingField.fieldId}"`
|
|
115
|
+
: '';
|
|
116
|
+
console.log(` <event id="${event.eventId}" name="${escapeXml(event.eventName)}"${fieldAttr}/>`);
|
|
117
|
+
}
|
|
118
|
+
console.log(' </complete-events>');
|
|
119
|
+
}
|
|
120
|
+
console.log('</aggregate-completeness>');
|
|
121
|
+
}
|
|
122
|
+
export function listAggregates(model) {
|
|
123
|
+
const aggregates = [...model.aggregates.values()];
|
|
124
|
+
if (aggregates.length === 0) {
|
|
125
|
+
console.log('<aggregates/>');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
console.log('<aggregates>');
|
|
129
|
+
for (const aggregate of aggregates) {
|
|
130
|
+
// Calculate events dynamically based on position
|
|
131
|
+
const eventsInAggregate = getEventsInAggregate(model, aggregate);
|
|
132
|
+
const eventCount = eventsInAggregate.length;
|
|
133
|
+
const idFieldAttr = aggregate.aggregateIdFieldName
|
|
134
|
+
? ` idField="${escapeXml(aggregate.aggregateIdFieldName)}" idType="${aggregate.aggregateIdFieldType ?? 'Unknown'}"`
|
|
135
|
+
: '';
|
|
136
|
+
console.log(` <aggregate id="${aggregate.id}" name="${escapeXml(aggregate.name)}" events="${eventCount}"${idFieldAttr}/>`);
|
|
137
|
+
}
|
|
138
|
+
console.log('</aggregates>');
|
|
139
|
+
}
|
|
@@ -27,6 +27,23 @@ function formatFieldXml(field, indent) {
|
|
|
27
27
|
}
|
|
28
28
|
return `${indent}<field ${attrs.join(' ')}/>\n`;
|
|
29
29
|
}
|
|
30
|
+
// Find which aggregate an event belongs to (center point inside aggregate bounds)
|
|
31
|
+
function findAggregateForEvent(model, event) {
|
|
32
|
+
const centerX = event.position.x + event.width / 2;
|
|
33
|
+
const centerY = event.position.y + event.height / 2;
|
|
34
|
+
for (const aggregate of model.aggregates.values()) {
|
|
35
|
+
const bounds = {
|
|
36
|
+
left: aggregate.position.x,
|
|
37
|
+
right: aggregate.position.x + aggregate.size.width,
|
|
38
|
+
top: aggregate.position.y,
|
|
39
|
+
bottom: aggregate.position.y + aggregate.size.height,
|
|
40
|
+
};
|
|
41
|
+
if (centerX >= bounds.left && centerX <= bounds.right && centerY >= bounds.top && centerY <= bounds.bottom) {
|
|
42
|
+
return aggregate;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
30
47
|
function formatCommandXml(model, command) {
|
|
31
48
|
let xml = `<command name="${escapeXml(command.name)}">\n`;
|
|
32
49
|
if (command.fields.length > 0) {
|
|
@@ -54,8 +71,11 @@ function formatCommandXml(model, command) {
|
|
|
54
71
|
xml += ' <produces>\n';
|
|
55
72
|
for (const flow of outgoingFlows) {
|
|
56
73
|
const target = model.events.get(flow.targetId);
|
|
57
|
-
if (target)
|
|
58
|
-
|
|
74
|
+
if (target) {
|
|
75
|
+
const aggregate = findAggregateForEvent(model, target);
|
|
76
|
+
const aggregateAttr = aggregate ? ` aggregate="${escapeXml(aggregate.name)}"` : '';
|
|
77
|
+
xml += ` <event name="${escapeXml(target.name)}"${aggregateAttr}/>\n`;
|
|
78
|
+
}
|
|
59
79
|
}
|
|
60
80
|
xml += ' </produces>\n';
|
|
61
81
|
}
|