eventmodeler 0.2.9 → 0.3.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 +25 -13
- package/dist/lib/chapter-utils.d.ts +13 -0
- package/dist/lib/chapter-utils.js +71 -0
- package/dist/lib/flow-utils.d.ts +52 -0
- package/dist/lib/flow-utils.js +305 -0
- package/dist/lib/slice-utils.js +17 -4
- package/dist/slices/add-field/index.d.ts +2 -0
- package/dist/slices/add-field/index.js +46 -3
- package/dist/slices/codegen-slice/index.js +2 -13
- package/dist/slices/list-slices/index.d.ts +1 -1
- package/dist/slices/list-slices/index.js +17 -2
- package/dist/slices/remove-field/index.d.ts +2 -0
- package/dist/slices/remove-field/index.js +59 -3
- package/dist/slices/show-chapter/index.js +162 -2
- package/dist/slices/show-slice/index.js +242 -10
- package/dist/slices/update-field/index.d.ts +2 -0
- package/dist/slices/update-field/index.js +57 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -54,17 +54,19 @@ USAGE:
|
|
|
54
54
|
eventmodeler <command> [options] Run a CLI command
|
|
55
55
|
|
|
56
56
|
COMMANDS:
|
|
57
|
-
list slices
|
|
57
|
+
list slices [--chapter <name>] List all slices (optionally filtered by chapter)
|
|
58
58
|
list events List all events
|
|
59
59
|
list commands List all commands
|
|
60
60
|
list chapters List all chapters
|
|
61
61
|
list aggregates List all aggregates
|
|
62
62
|
list actors List all actors
|
|
63
63
|
|
|
64
|
-
show slice <name> Show
|
|
64
|
+
show slice <name> Show slice with components, flow annotations, and
|
|
65
|
+
inbound/outbound dependencies
|
|
65
66
|
show event <name> Show detailed XML view of an event
|
|
66
67
|
show command <name> Show detailed XML view of a command
|
|
67
|
-
show chapter <name> Show chapter with
|
|
68
|
+
show chapter <name> Show chapter with slices, flow graph, and
|
|
69
|
+
external dependencies
|
|
68
70
|
show completeness <name> Show field mapping completeness for any element
|
|
69
71
|
show model-completeness Show completeness of all flows in the model
|
|
70
72
|
show aggregate-completeness <name>
|
|
@@ -78,18 +80,18 @@ COMMANDS:
|
|
|
78
80
|
|
|
79
81
|
add scenario --slice <name> --json|--xml <data>
|
|
80
82
|
Add a scenario to a slice
|
|
81
|
-
add field --command|--event|--read-model <name> --json|--xml <data>
|
|
83
|
+
add field --command|--event|--read-model|--screen|--processor <name> --json|--xml <data>
|
|
82
84
|
Add a field to an entity
|
|
83
85
|
|
|
84
86
|
remove scenario <name> [--slice <name>]
|
|
85
87
|
Remove a scenario by name
|
|
86
|
-
remove field --command|--event|--read-model <name> --field <name>
|
|
88
|
+
remove field --command|--event|--read-model|--screen|--processor <name> --field <name>
|
|
87
89
|
Remove a field from an entity
|
|
88
90
|
|
|
89
91
|
map fields --flow <source→target> --json|--xml <mappings>
|
|
90
92
|
Set field mappings on a flow
|
|
91
93
|
|
|
92
|
-
update field --command|--event|--read-model <name> --field <name> [--optional true|false] [--generated true|false]
|
|
94
|
+
update field --command|--event|--read-model|--screen|--processor <name> --field <name> [--optional true|false] [--generated true|false]
|
|
93
95
|
Update field properties
|
|
94
96
|
|
|
95
97
|
create state-change-slice --xml <data>
|
|
@@ -134,6 +136,7 @@ EXAMPLES:
|
|
|
134
136
|
async function main() {
|
|
135
137
|
let fileArg = null;
|
|
136
138
|
let formatArg = null;
|
|
139
|
+
let chapterArg = null;
|
|
137
140
|
const filteredArgs = [];
|
|
138
141
|
for (let i = 0; i < args.length; i++) {
|
|
139
142
|
if (args[i] === '-f' || args[i] === '--file') {
|
|
@@ -142,6 +145,9 @@ async function main() {
|
|
|
142
145
|
else if (args[i] === '--format') {
|
|
143
146
|
formatArg = args[++i];
|
|
144
147
|
}
|
|
148
|
+
else if (args[i] === '--chapter') {
|
|
149
|
+
chapterArg = args[++i];
|
|
150
|
+
}
|
|
145
151
|
else if (args[i] === '-h' || args[i] === '--help') {
|
|
146
152
|
printHelp();
|
|
147
153
|
process.exit(0);
|
|
@@ -183,7 +189,7 @@ async function main() {
|
|
|
183
189
|
case 'list':
|
|
184
190
|
switch (subcommand) {
|
|
185
191
|
case 'slices':
|
|
186
|
-
listSlices(model, format);
|
|
192
|
+
listSlices(model, format, chapterArg ?? undefined);
|
|
187
193
|
break;
|
|
188
194
|
case 'events':
|
|
189
195
|
listEvents(model, format);
|
|
@@ -326,15 +332,17 @@ async function main() {
|
|
|
326
332
|
const commandArg = getNamedArg(filteredArgs, '--command');
|
|
327
333
|
const eventArg = getNamedArg(filteredArgs, '--event');
|
|
328
334
|
const readModelArg = getNamedArg(filteredArgs, '--read-model');
|
|
335
|
+
const screenArg = getNamedArg(filteredArgs, '--screen');
|
|
336
|
+
const processorArg = getNamedArg(filteredArgs, '--processor');
|
|
329
337
|
const jsonArg = getNamedArg(filteredArgs, '--json');
|
|
330
338
|
const xmlArg = getNamedArg(filteredArgs, '--xml');
|
|
331
339
|
const inputData = jsonArg ?? xmlArg;
|
|
332
340
|
if (!inputData) {
|
|
333
341
|
console.error('Error: --json or --xml is required');
|
|
334
|
-
console.error('Usage: eventmodeler add field --command|--event|--read-model <name> --json|--xml <data>');
|
|
342
|
+
console.error('Usage: eventmodeler add field --command|--event|--read-model|--screen|--processor <name> --json|--xml <data>');
|
|
335
343
|
process.exit(1);
|
|
336
344
|
}
|
|
337
|
-
addField(model, filePath, { command: commandArg, event: eventArg, readModel: readModelArg }, inputData);
|
|
345
|
+
addField(model, filePath, { command: commandArg, event: eventArg, readModel: readModelArg, screen: screenArg, processor: processorArg }, inputData);
|
|
338
346
|
break;
|
|
339
347
|
}
|
|
340
348
|
default:
|
|
@@ -359,13 +367,15 @@ async function main() {
|
|
|
359
367
|
const commandArg = getNamedArg(filteredArgs, '--command');
|
|
360
368
|
const eventArg = getNamedArg(filteredArgs, '--event');
|
|
361
369
|
const readModelArg = getNamedArg(filteredArgs, '--read-model');
|
|
370
|
+
const screenArg = getNamedArg(filteredArgs, '--screen');
|
|
371
|
+
const processorArg = getNamedArg(filteredArgs, '--processor');
|
|
362
372
|
const fieldArg = getNamedArg(filteredArgs, '--field');
|
|
363
373
|
if (!fieldArg) {
|
|
364
374
|
console.error('Error: --field is required');
|
|
365
|
-
console.error('Usage: eventmodeler remove field --command|--event|--read-model <name> --field <field-name>');
|
|
375
|
+
console.error('Usage: eventmodeler remove field --command|--event|--read-model|--screen|--processor <name> --field <field-name>');
|
|
366
376
|
process.exit(1);
|
|
367
377
|
}
|
|
368
|
-
removeField(model, filePath, { command: commandArg, event: eventArg, readModel: readModelArg }, fieldArg);
|
|
378
|
+
removeField(model, filePath, { command: commandArg, event: eventArg, readModel: readModelArg, screen: screenArg, processor: processorArg }, fieldArg);
|
|
369
379
|
break;
|
|
370
380
|
}
|
|
371
381
|
default:
|
|
@@ -406,13 +416,15 @@ async function main() {
|
|
|
406
416
|
const commandArg = getNamedArg(filteredArgs, '--command');
|
|
407
417
|
const eventArg = getNamedArg(filteredArgs, '--event');
|
|
408
418
|
const readModelArg = getNamedArg(filteredArgs, '--read-model');
|
|
419
|
+
const screenArg = getNamedArg(filteredArgs, '--screen');
|
|
420
|
+
const processorArg = getNamedArg(filteredArgs, '--processor');
|
|
409
421
|
const fieldArg = getNamedArg(filteredArgs, '--field');
|
|
410
422
|
const optionalArg = getNamedArg(filteredArgs, '--optional');
|
|
411
423
|
const generatedArg = getNamedArg(filteredArgs, '--generated');
|
|
412
424
|
const typeArg = getNamedArg(filteredArgs, '--type');
|
|
413
425
|
if (!fieldArg) {
|
|
414
426
|
console.error('Error: --field is required');
|
|
415
|
-
console.error('Usage: eventmodeler update field --command|--event|--read-model <name> --field <field-name> [--optional true|false] [--generated true|false]');
|
|
427
|
+
console.error('Usage: eventmodeler update field --command|--event|--read-model|--screen|--processor <name> --field <field-name> [--optional true|false] [--generated true|false]');
|
|
416
428
|
process.exit(1);
|
|
417
429
|
}
|
|
418
430
|
const updates = {};
|
|
@@ -429,7 +441,7 @@ async function main() {
|
|
|
429
441
|
console.error('Error: Must specify at least one update (--optional, --generated, or --type)');
|
|
430
442
|
process.exit(1);
|
|
431
443
|
}
|
|
432
|
-
updateField(model, filePath, { command: commandArg, event: eventArg, readModel: readModelArg }, fieldArg, updates);
|
|
444
|
+
updateField(model, filePath, { command: commandArg, event: eventArg, readModel: readModelArg, screen: screenArg, processor: processorArg }, fieldArg, updates);
|
|
433
445
|
break;
|
|
434
446
|
}
|
|
435
447
|
default:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Chapter, EventModel, Slice } from '../types.js';
|
|
2
|
+
export interface ChapterWithHierarchy {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
parent?: ChapterWithHierarchy;
|
|
6
|
+
}
|
|
7
|
+
export declare function findParentChapter(model: EventModel, chapter: Chapter): Chapter | null;
|
|
8
|
+
export declare function getChapterHierarchy(model: EventModel, chapter: Chapter): ChapterWithHierarchy;
|
|
9
|
+
export declare function hierarchyToArray(hierarchy: ChapterWithHierarchy): Array<{
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function findChapterForSlice(model: EventModel, slice: Slice): Chapter | null;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Check if two chapters have X-overlap
|
|
2
|
+
function hasXOverlap(parent, child) {
|
|
3
|
+
const parentLeft = parent.position.x;
|
|
4
|
+
const parentRight = parent.position.x + parent.size.width;
|
|
5
|
+
const childLeft = child.position.x;
|
|
6
|
+
const childRight = child.position.x + child.size.width;
|
|
7
|
+
return childLeft < parentRight && childRight > parentLeft;
|
|
8
|
+
}
|
|
9
|
+
// Find the immediate parent of a chapter (nearest chapter above with X-overlap)
|
|
10
|
+
export function findParentChapter(model, chapter) {
|
|
11
|
+
let bestParent = null;
|
|
12
|
+
let bestParentY = -Infinity;
|
|
13
|
+
for (const candidate of model.chapters.values()) {
|
|
14
|
+
if (candidate.id === chapter.id)
|
|
15
|
+
continue;
|
|
16
|
+
// Parent must be above (lower y value)
|
|
17
|
+
if (candidate.position.y >= chapter.position.y)
|
|
18
|
+
continue;
|
|
19
|
+
// Must have X-overlap
|
|
20
|
+
if (!hasXOverlap(candidate, chapter))
|
|
21
|
+
continue;
|
|
22
|
+
// Take the closest parent (highest y value that's still above)
|
|
23
|
+
if (candidate.position.y > bestParentY) {
|
|
24
|
+
bestParent = candidate;
|
|
25
|
+
bestParentY = candidate.position.y;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return bestParent;
|
|
29
|
+
}
|
|
30
|
+
// Build full hierarchy for a chapter (from root to this chapter)
|
|
31
|
+
export function getChapterHierarchy(model, chapter) {
|
|
32
|
+
const parent = findParentChapter(model, chapter);
|
|
33
|
+
const result = {
|
|
34
|
+
id: chapter.id,
|
|
35
|
+
name: chapter.name,
|
|
36
|
+
};
|
|
37
|
+
if (parent) {
|
|
38
|
+
result.parent = getChapterHierarchy(model, parent);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
// Convert hierarchy to array (root first)
|
|
43
|
+
export function hierarchyToArray(hierarchy) {
|
|
44
|
+
const result = [];
|
|
45
|
+
function collect(h) {
|
|
46
|
+
if (h.parent)
|
|
47
|
+
collect(h.parent);
|
|
48
|
+
result.push({ id: h.id, name: h.name });
|
|
49
|
+
}
|
|
50
|
+
collect(hierarchy);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
// Find which chapter contains a slice (based on horizontal center)
|
|
54
|
+
// Returns the deepest (highest y-value) chapter that contains the slice
|
|
55
|
+
export function findChapterForSlice(model, slice) {
|
|
56
|
+
const sliceCenterX = slice.position.x + slice.size.width / 2;
|
|
57
|
+
let bestChapter = null;
|
|
58
|
+
let bestY = -Infinity;
|
|
59
|
+
for (const chapter of model.chapters.values()) {
|
|
60
|
+
const chapterLeft = chapter.position.x;
|
|
61
|
+
const chapterRight = chapter.position.x + chapter.size.width;
|
|
62
|
+
if (sliceCenterX >= chapterLeft && sliceCenterX <= chapterRight) {
|
|
63
|
+
// Take the deepest (highest y) chapter
|
|
64
|
+
if (chapter.position.y > bestY) {
|
|
65
|
+
bestChapter = chapter;
|
|
66
|
+
bestY = chapter.position.y;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return bestChapter;
|
|
71
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { EventModel, Slice } from '../types.js';
|
|
2
|
+
export type ElementType = 'screen' | 'command' | 'event' | 'read-model' | 'processor';
|
|
3
|
+
export interface ElementInfo {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
type: ElementType;
|
|
7
|
+
}
|
|
8
|
+
export interface FieldMappingInfo {
|
|
9
|
+
from: string;
|
|
10
|
+
to: string;
|
|
11
|
+
}
|
|
12
|
+
export interface FlowInfo {
|
|
13
|
+
flowId: string;
|
|
14
|
+
flowType: string;
|
|
15
|
+
source: ElementInfo;
|
|
16
|
+
target: ElementInfo;
|
|
17
|
+
fieldMappings: FieldMappingInfo[];
|
|
18
|
+
}
|
|
19
|
+
export interface SliceFlowInfo extends FlowInfo {
|
|
20
|
+
sourceSlice?: {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
targetSlice?: {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare function getElementInfo(model: EventModel, id: string): ElementInfo | null;
|
|
30
|
+
export declare function getSliceComponentIds(model: EventModel, slice: Slice): Set<string>;
|
|
31
|
+
export declare function findSliceForComponent(model: EventModel, componentId: string): Slice | null;
|
|
32
|
+
export declare function getInboundFlows(model: EventModel, slice: Slice): SliceFlowInfo[];
|
|
33
|
+
export declare function getOutboundFlows(model: EventModel, slice: Slice): SliceFlowInfo[];
|
|
34
|
+
export declare function getInternalFlows(model: EventModel, slice: Slice): FlowInfo[];
|
|
35
|
+
export declare function getFlowsForElement(model: EventModel, elementId: string): {
|
|
36
|
+
incoming: FlowInfo[];
|
|
37
|
+
outgoing: FlowInfo[];
|
|
38
|
+
};
|
|
39
|
+
export interface SliceToSliceFlow {
|
|
40
|
+
fromSlice: {
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
};
|
|
44
|
+
toSlice: {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
};
|
|
48
|
+
flows: SliceFlowInfo[];
|
|
49
|
+
}
|
|
50
|
+
export declare function findSliceToSliceFlows(model: EventModel, slices: Slice[]): SliceToSliceFlow[];
|
|
51
|
+
export declare function findChapterInboundFlows(model: EventModel, slices: Slice[]): SliceFlowInfo[];
|
|
52
|
+
export declare function findChapterOutboundFlows(model: EventModel, slices: Slice[]): SliceFlowInfo[];
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// Get element info (name and type) by ID
|
|
2
|
+
export function getElementInfo(model, id) {
|
|
3
|
+
const screen = model.screens.get(id);
|
|
4
|
+
if (screen)
|
|
5
|
+
return { id, name: screen.name, type: 'screen' };
|
|
6
|
+
const command = model.commands.get(id);
|
|
7
|
+
if (command)
|
|
8
|
+
return { id, name: command.name, type: 'command' };
|
|
9
|
+
const event = model.events.get(id);
|
|
10
|
+
if (event)
|
|
11
|
+
return { id, name: event.name, type: 'event' };
|
|
12
|
+
const readModel = model.readModels.get(id);
|
|
13
|
+
if (readModel)
|
|
14
|
+
return { id, name: readModel.name, type: 'read-model' };
|
|
15
|
+
const processor = model.processors.get(id);
|
|
16
|
+
if (processor)
|
|
17
|
+
return { id, name: processor.name, type: 'processor' };
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
// Get fields for an element
|
|
21
|
+
function getElementFields(model, id) {
|
|
22
|
+
return (model.screens.get(id)?.fields ??
|
|
23
|
+
model.commands.get(id)?.fields ??
|
|
24
|
+
model.events.get(id)?.fields ??
|
|
25
|
+
model.readModels.get(id)?.fields ??
|
|
26
|
+
model.processors.get(id)?.fields ??
|
|
27
|
+
[]);
|
|
28
|
+
}
|
|
29
|
+
// Find a field by ID in a fields array (recursive for subfields)
|
|
30
|
+
function findFieldById(fields, fieldId) {
|
|
31
|
+
for (const field of fields) {
|
|
32
|
+
if (field.id === fieldId)
|
|
33
|
+
return field;
|
|
34
|
+
if (field.subfields) {
|
|
35
|
+
const found = findFieldById(field.subfields, fieldId);
|
|
36
|
+
if (found)
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
// Enrich field mappings with field names
|
|
43
|
+
function enrichFieldMappings(model, flow) {
|
|
44
|
+
const sourceFields = getElementFields(model, flow.sourceId);
|
|
45
|
+
const targetFields = getElementFields(model, flow.targetId);
|
|
46
|
+
return flow.fieldMappings.map(mapping => {
|
|
47
|
+
const sourceField = findFieldById(sourceFields, mapping.sourceFieldId);
|
|
48
|
+
const targetField = findFieldById(targetFields, mapping.targetFieldId);
|
|
49
|
+
return {
|
|
50
|
+
from: sourceField?.name ?? 'unknown',
|
|
51
|
+
to: targetField?.name ?? 'unknown',
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Check if an element's center point is within a slice's bounds
|
|
56
|
+
function isElementInSlice(slice, position, width, height) {
|
|
57
|
+
const centerX = position.x + width / 2;
|
|
58
|
+
const centerY = position.y + height / 2;
|
|
59
|
+
return (centerX >= slice.position.x &&
|
|
60
|
+
centerX <= slice.position.x + slice.size.width &&
|
|
61
|
+
centerY >= slice.position.y &&
|
|
62
|
+
centerY <= slice.position.y + slice.size.height);
|
|
63
|
+
}
|
|
64
|
+
// Get all component IDs in a slice
|
|
65
|
+
export function getSliceComponentIds(model, slice) {
|
|
66
|
+
const ids = new Set();
|
|
67
|
+
for (const screen of model.screens.values()) {
|
|
68
|
+
if (isElementInSlice(slice, screen.position, screen.width, screen.height)) {
|
|
69
|
+
ids.add(screen.id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const command of model.commands.values()) {
|
|
73
|
+
if (isElementInSlice(slice, command.position, command.width, command.height)) {
|
|
74
|
+
ids.add(command.id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const event of model.events.values()) {
|
|
78
|
+
if (isElementInSlice(slice, event.position, event.width, event.height)) {
|
|
79
|
+
ids.add(event.id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const readModel of model.readModels.values()) {
|
|
83
|
+
if (isElementInSlice(slice, readModel.position, readModel.width, readModel.height)) {
|
|
84
|
+
ids.add(readModel.id);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const processor of model.processors.values()) {
|
|
88
|
+
if (isElementInSlice(slice, processor.position, processor.width, processor.height)) {
|
|
89
|
+
ids.add(processor.id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return ids;
|
|
93
|
+
}
|
|
94
|
+
// Find which slice contains a component
|
|
95
|
+
export function findSliceForComponent(model, componentId) {
|
|
96
|
+
// Get element position and size
|
|
97
|
+
let position = null;
|
|
98
|
+
let width = 0;
|
|
99
|
+
let height = 0;
|
|
100
|
+
const screen = model.screens.get(componentId);
|
|
101
|
+
if (screen) {
|
|
102
|
+
position = screen.position;
|
|
103
|
+
width = screen.width;
|
|
104
|
+
height = screen.height;
|
|
105
|
+
}
|
|
106
|
+
const command = model.commands.get(componentId);
|
|
107
|
+
if (command) {
|
|
108
|
+
position = command.position;
|
|
109
|
+
width = command.width;
|
|
110
|
+
height = command.height;
|
|
111
|
+
}
|
|
112
|
+
const event = model.events.get(componentId);
|
|
113
|
+
if (event) {
|
|
114
|
+
position = event.position;
|
|
115
|
+
width = event.width;
|
|
116
|
+
height = event.height;
|
|
117
|
+
}
|
|
118
|
+
const readModel = model.readModels.get(componentId);
|
|
119
|
+
if (readModel) {
|
|
120
|
+
position = readModel.position;
|
|
121
|
+
width = readModel.width;
|
|
122
|
+
height = readModel.height;
|
|
123
|
+
}
|
|
124
|
+
const processor = model.processors.get(componentId);
|
|
125
|
+
if (processor) {
|
|
126
|
+
position = processor.position;
|
|
127
|
+
width = processor.width;
|
|
128
|
+
height = processor.height;
|
|
129
|
+
}
|
|
130
|
+
if (!position)
|
|
131
|
+
return null;
|
|
132
|
+
for (const slice of model.slices.values()) {
|
|
133
|
+
if (isElementInSlice(slice, position, width, height)) {
|
|
134
|
+
return slice;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
// Build FlowInfo from a Flow
|
|
140
|
+
function buildFlowInfo(model, flow) {
|
|
141
|
+
const source = getElementInfo(model, flow.sourceId);
|
|
142
|
+
const target = getElementInfo(model, flow.targetId);
|
|
143
|
+
if (!source || !target)
|
|
144
|
+
return null;
|
|
145
|
+
return {
|
|
146
|
+
flowId: flow.id,
|
|
147
|
+
flowType: flow.flowType,
|
|
148
|
+
source,
|
|
149
|
+
target,
|
|
150
|
+
fieldMappings: enrichFieldMappings(model, flow),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Build SliceFlowInfo from a Flow (includes slice info for source/target)
|
|
154
|
+
function buildSliceFlowInfo(model, flow) {
|
|
155
|
+
const base = buildFlowInfo(model, flow);
|
|
156
|
+
if (!base)
|
|
157
|
+
return null;
|
|
158
|
+
const sourceSlice = findSliceForComponent(model, flow.sourceId);
|
|
159
|
+
const targetSlice = findSliceForComponent(model, flow.targetId);
|
|
160
|
+
return {
|
|
161
|
+
...base,
|
|
162
|
+
sourceSlice: sourceSlice ? { id: sourceSlice.id, name: sourceSlice.name } : undefined,
|
|
163
|
+
targetSlice: targetSlice ? { id: targetSlice.id, name: targetSlice.name } : undefined,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// Get flows entering a slice (target in slice, source outside)
|
|
167
|
+
export function getInboundFlows(model, slice) {
|
|
168
|
+
const componentIds = getSliceComponentIds(model, slice);
|
|
169
|
+
const flows = [];
|
|
170
|
+
for (const flow of model.flows.values()) {
|
|
171
|
+
if (componentIds.has(flow.targetId) && !componentIds.has(flow.sourceId)) {
|
|
172
|
+
const flowInfo = buildSliceFlowInfo(model, flow);
|
|
173
|
+
if (flowInfo)
|
|
174
|
+
flows.push(flowInfo);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return flows;
|
|
178
|
+
}
|
|
179
|
+
// Get flows leaving a slice (source in slice, target outside)
|
|
180
|
+
export function getOutboundFlows(model, slice) {
|
|
181
|
+
const componentIds = getSliceComponentIds(model, slice);
|
|
182
|
+
const flows = [];
|
|
183
|
+
for (const flow of model.flows.values()) {
|
|
184
|
+
if (componentIds.has(flow.sourceId) && !componentIds.has(flow.targetId)) {
|
|
185
|
+
const flowInfo = buildSliceFlowInfo(model, flow);
|
|
186
|
+
if (flowInfo)
|
|
187
|
+
flows.push(flowInfo);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return flows;
|
|
191
|
+
}
|
|
192
|
+
// Get flows within a slice (both source and target in slice)
|
|
193
|
+
export function getInternalFlows(model, slice) {
|
|
194
|
+
const componentIds = getSliceComponentIds(model, slice);
|
|
195
|
+
const flows = [];
|
|
196
|
+
for (const flow of model.flows.values()) {
|
|
197
|
+
if (componentIds.has(flow.sourceId) && componentIds.has(flow.targetId)) {
|
|
198
|
+
const flowInfo = buildFlowInfo(model, flow);
|
|
199
|
+
if (flowInfo)
|
|
200
|
+
flows.push(flowInfo);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return flows;
|
|
204
|
+
}
|
|
205
|
+
// Get all flows for a specific element (both directions)
|
|
206
|
+
export function getFlowsForElement(model, elementId) {
|
|
207
|
+
const incoming = [];
|
|
208
|
+
const outgoing = [];
|
|
209
|
+
for (const flow of model.flows.values()) {
|
|
210
|
+
if (flow.targetId === elementId) {
|
|
211
|
+
const flowInfo = buildFlowInfo(model, flow);
|
|
212
|
+
if (flowInfo)
|
|
213
|
+
incoming.push(flowInfo);
|
|
214
|
+
}
|
|
215
|
+
if (flow.sourceId === elementId) {
|
|
216
|
+
const flowInfo = buildFlowInfo(model, flow);
|
|
217
|
+
if (flowInfo)
|
|
218
|
+
outgoing.push(flowInfo);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { incoming, outgoing };
|
|
222
|
+
}
|
|
223
|
+
export function findSliceToSliceFlows(model, slices) {
|
|
224
|
+
const sliceMap = new Map(slices.map(s => [s.id, s]));
|
|
225
|
+
const sliceComponentMap = new Map();
|
|
226
|
+
// Build component ID sets for each slice
|
|
227
|
+
for (const slice of slices) {
|
|
228
|
+
sliceComponentMap.set(slice.id, getSliceComponentIds(model, slice));
|
|
229
|
+
}
|
|
230
|
+
// Group flows by slice pair
|
|
231
|
+
const flowsBySlicePair = new Map();
|
|
232
|
+
for (const flow of model.flows.values()) {
|
|
233
|
+
let sourceSliceId = null;
|
|
234
|
+
let targetSliceId = null;
|
|
235
|
+
// Find which slice contains source and target
|
|
236
|
+
for (const [sliceId, componentIds] of sliceComponentMap.entries()) {
|
|
237
|
+
if (componentIds.has(flow.sourceId))
|
|
238
|
+
sourceSliceId = sliceId;
|
|
239
|
+
if (componentIds.has(flow.targetId))
|
|
240
|
+
targetSliceId = sliceId;
|
|
241
|
+
}
|
|
242
|
+
// Only include flows between different slices in our set
|
|
243
|
+
if (sourceSliceId && targetSliceId && sourceSliceId !== targetSliceId) {
|
|
244
|
+
const key = `${sourceSliceId}:${targetSliceId}`;
|
|
245
|
+
const flowInfo = buildSliceFlowInfo(model, flow);
|
|
246
|
+
if (flowInfo) {
|
|
247
|
+
if (!flowsBySlicePair.has(key)) {
|
|
248
|
+
flowsBySlicePair.set(key, []);
|
|
249
|
+
}
|
|
250
|
+
flowsBySlicePair.get(key).push(flowInfo);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Convert to result format
|
|
255
|
+
const result = [];
|
|
256
|
+
for (const [key, flows] of flowsBySlicePair.entries()) {
|
|
257
|
+
const [fromId, toId] = key.split(':');
|
|
258
|
+
const fromSlice = sliceMap.get(fromId);
|
|
259
|
+
const toSlice = sliceMap.get(toId);
|
|
260
|
+
if (fromSlice && toSlice) {
|
|
261
|
+
result.push({
|
|
262
|
+
fromSlice: { id: fromSlice.id, name: fromSlice.name },
|
|
263
|
+
toSlice: { id: toSlice.id, name: toSlice.name },
|
|
264
|
+
flows,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
// Find flows entering a chapter (target in chapter slices, source outside)
|
|
271
|
+
export function findChapterInboundFlows(model, slices) {
|
|
272
|
+
const chapterComponentIds = new Set();
|
|
273
|
+
for (const slice of slices) {
|
|
274
|
+
for (const id of getSliceComponentIds(model, slice)) {
|
|
275
|
+
chapterComponentIds.add(id);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const flows = [];
|
|
279
|
+
for (const flow of model.flows.values()) {
|
|
280
|
+
if (chapterComponentIds.has(flow.targetId) && !chapterComponentIds.has(flow.sourceId)) {
|
|
281
|
+
const flowInfo = buildSliceFlowInfo(model, flow);
|
|
282
|
+
if (flowInfo)
|
|
283
|
+
flows.push(flowInfo);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return flows;
|
|
287
|
+
}
|
|
288
|
+
// Find flows leaving a chapter (source in chapter slices, target outside)
|
|
289
|
+
export function findChapterOutboundFlows(model, slices) {
|
|
290
|
+
const chapterComponentIds = new Set();
|
|
291
|
+
for (const slice of slices) {
|
|
292
|
+
for (const id of getSliceComponentIds(model, slice)) {
|
|
293
|
+
chapterComponentIds.add(id);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const flows = [];
|
|
297
|
+
for (const flow of model.flows.values()) {
|
|
298
|
+
if (chapterComponentIds.has(flow.sourceId) && !chapterComponentIds.has(flow.targetId)) {
|
|
299
|
+
const flowInfo = buildSliceFlowInfo(model, flow);
|
|
300
|
+
if (flowInfo)
|
|
301
|
+
flows.push(flowInfo);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return flows;
|
|
305
|
+
}
|
package/dist/lib/slice-utils.js
CHANGED
|
@@ -91,6 +91,17 @@ export function inferFieldMappings(sourceFields, targetFields) {
|
|
|
91
91
|
}
|
|
92
92
|
return mappings;
|
|
93
93
|
}
|
|
94
|
+
const VALID_FIELD_TYPES = ['UUID', 'Boolean', 'Double', 'Decimal', 'Date', 'DateTime', 'Long', 'Int', 'String', 'Custom'];
|
|
95
|
+
function validateFieldType(type, fieldName) {
|
|
96
|
+
if (!VALID_FIELD_TYPES.includes(type)) {
|
|
97
|
+
const typeLower = type.toLowerCase();
|
|
98
|
+
let hint = '';
|
|
99
|
+
if (typeLower === 'list' || typeLower === 'array') {
|
|
100
|
+
hint = `\n\nHint: To make a field a list/array, use isList="true" with a valid type:\n <field name="${fieldName}" type="String" isList="true"/>`;
|
|
101
|
+
}
|
|
102
|
+
throw new Error(`Invalid field type "${type}" for field "${fieldName}". Valid types: ${VALID_FIELD_TYPES.join(', ')}${hint}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
94
105
|
function getAttr(attrs, name) {
|
|
95
106
|
const match = attrs.match(new RegExp(`${name}="([^"]*)"`));
|
|
96
107
|
return match ? match[1] : undefined;
|
|
@@ -106,13 +117,15 @@ export function parseFieldsFromXml(xml) {
|
|
|
106
117
|
const type = getAttr(attrs, 'type');
|
|
107
118
|
if (!name || !type)
|
|
108
119
|
continue;
|
|
120
|
+
// Validate the field type
|
|
121
|
+
validateFieldType(type, name);
|
|
109
122
|
const field = {
|
|
110
123
|
name,
|
|
111
124
|
type,
|
|
112
|
-
isList: getAttr(attrs, '
|
|
113
|
-
isGenerated: getAttr(attrs, '
|
|
114
|
-
isOptional: getAttr(attrs, '
|
|
115
|
-
isUserInput: getAttr(attrs, '
|
|
125
|
+
isList: getAttr(attrs, 'isList') === 'true',
|
|
126
|
+
isGenerated: getAttr(attrs, 'isGenerated') === 'true',
|
|
127
|
+
isOptional: getAttr(attrs, 'isOptional') === 'true',
|
|
128
|
+
isUserInput: getAttr(attrs, 'isUserInput') === 'true',
|
|
116
129
|
};
|
|
117
130
|
// Parse nested subfields for Custom type
|
|
118
131
|
if (type === 'Custom' && content) {
|