eventmodeler 0.2.9 → 0.2.10
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 +10 -4
- 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.js +5 -0
- 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/show-chapter/index.js +162 -2
- package/dist/slices/show-slice/index.js +242 -10
- 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>
|
|
@@ -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);
|
|
@@ -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) {
|
|
@@ -90,6 +90,11 @@ export function addField(model, filePath, options, input) {
|
|
|
90
90
|
if (!validFieldTypes.includes(fieldInput.type)) {
|
|
91
91
|
console.error(`Error: Invalid field type: ${fieldInput.type}`);
|
|
92
92
|
console.error(`Valid types: ${validFieldTypes.join(', ')}`);
|
|
93
|
+
const typeLower = fieldInput.type.toLowerCase();
|
|
94
|
+
if (typeLower === 'list' || typeLower === 'array') {
|
|
95
|
+
console.error(`\nHint: To make a field a list/array, use isList="true" with a valid type:`);
|
|
96
|
+
console.error(` <field name="items" type="String" isList="true"/>`);
|
|
97
|
+
}
|
|
93
98
|
process.exit(1);
|
|
94
99
|
}
|
|
95
100
|
// Find entity and add field
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { outputJson } from '../../lib/format.js';
|
|
2
2
|
import { findElementOrExit } from '../../lib/element-lookup.js';
|
|
3
|
+
import { findChapterForSlice, getChapterHierarchy } from '../../lib/chapter-utils.js';
|
|
3
4
|
// Get components inside a slice by checking if center point is within bounds
|
|
4
5
|
function getSliceComponents(model, slice) {
|
|
5
6
|
const bounds = {
|
|
@@ -71,18 +72,6 @@ function findActorForScreen(model, screen) {
|
|
|
71
72
|
}
|
|
72
73
|
return undefined;
|
|
73
74
|
}
|
|
74
|
-
// Find which chapter contains a slice (based on horizontal center)
|
|
75
|
-
function findChapterForSlice(model, slice) {
|
|
76
|
-
const sliceCenterX = slice.position.x + slice.size.width / 2;
|
|
77
|
-
for (const chapter of model.chapters.values()) {
|
|
78
|
-
const chapterLeft = chapter.position.x;
|
|
79
|
-
const chapterRight = chapter.position.x + chapter.size.width;
|
|
80
|
-
if (sliceCenterX >= chapterLeft && sliceCenterX <= chapterRight) {
|
|
81
|
-
return chapter;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
75
|
// Convert Field to JSON-friendly format
|
|
87
76
|
function fieldToJson(field) {
|
|
88
77
|
const result = {
|
|
@@ -351,7 +340,7 @@ export function codegenSlice(model, sliceName) {
|
|
|
351
340
|
id: slice.id,
|
|
352
341
|
name: slice.name,
|
|
353
342
|
},
|
|
354
|
-
...(chapter && { chapter:
|
|
343
|
+
...(chapter && { chapter: getChapterHierarchy(model, chapter) }),
|
|
355
344
|
elements: {
|
|
356
345
|
readModels: components.readModels.map(rm => ({
|
|
357
346
|
id: rm.id,
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { EventModel } from '../../types.js';
|
|
2
2
|
import { type OutputFormat } from '../../lib/format.js';
|
|
3
|
-
export declare function listSlices(model: EventModel, format: OutputFormat): void;
|
|
3
|
+
export declare function listSlices(model: EventModel, format: OutputFormat, chapterName?: string): void;
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import { escapeXml, outputJson } from '../../lib/format.js';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { findElementOrExit } from '../../lib/element-lookup.js';
|
|
3
|
+
function getSlicesUnderChapter(model, chapter) {
|
|
4
|
+
const chapterLeft = chapter.position.x;
|
|
5
|
+
const chapterRight = chapter.position.x + chapter.size.width;
|
|
6
|
+
return [...model.slices.values()].filter(slice => {
|
|
7
|
+
const sliceCenterX = slice.position.x + slice.size.width / 2;
|
|
8
|
+
return sliceCenterX >= chapterLeft && sliceCenterX <= chapterRight;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export function listSlices(model, format, chapterName) {
|
|
12
|
+
let slices = [...model.slices.values()];
|
|
13
|
+
// Filter by chapter if specified
|
|
14
|
+
if (chapterName) {
|
|
15
|
+
const chapter = findElementOrExit(model.chapters, chapterName, 'chapter');
|
|
16
|
+
slices = getSlicesUnderChapter(model, chapter);
|
|
17
|
+
}
|
|
4
18
|
const sorted = [...slices].sort((a, b) => a.position.x - b.position.x);
|
|
5
19
|
if (format === 'json') {
|
|
6
20
|
outputJson({
|
|
21
|
+
...(chapterName && { chapter: chapterName }),
|
|
7
22
|
slices: sorted.map(s => ({ id: s.id, name: s.name, status: s.status }))
|
|
8
23
|
});
|
|
9
24
|
return;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { escapeXml, outputJson } from '../../lib/format.js';
|
|
2
2
|
import { findElementOrExit } from '../../lib/element-lookup.js';
|
|
3
|
+
import { getChapterHierarchy, findChapterForSlice } from '../../lib/chapter-utils.js';
|
|
4
|
+
import { findSliceToSliceFlows, findChapterInboundFlows, findChapterOutboundFlows, } from '../../lib/flow-utils.js';
|
|
3
5
|
function getSlicesUnderChapter(model, chapter) {
|
|
4
6
|
// A slice is "under" a chapter if its horizontal center falls within the chapter's x range
|
|
5
7
|
const chapterLeft = chapter.position.x;
|
|
@@ -9,18 +11,149 @@ function getSlicesUnderChapter(model, chapter) {
|
|
|
9
11
|
return sliceCenterX >= chapterLeft && sliceCenterX <= chapterRight;
|
|
10
12
|
}).sort((a, b) => a.position.x - b.position.x);
|
|
11
13
|
}
|
|
14
|
+
function formatSliceFlowXml(sliceFlow) {
|
|
15
|
+
let xml = ` <slice-flow from="${escapeXml(sliceFlow.fromSlice.name)}" to="${escapeXml(sliceFlow.toSlice.name)}">\n`;
|
|
16
|
+
for (const flow of sliceFlow.flows) {
|
|
17
|
+
xml += ` <via type="${flow.flowType}">\n`;
|
|
18
|
+
xml += ` <source element="${escapeXml(flow.source.name)}" type="${flow.source.type}"/>\n`;
|
|
19
|
+
xml += ` <target element="${escapeXml(flow.target.name)}" type="${flow.target.type}"/>\n`;
|
|
20
|
+
if (flow.fieldMappings.length > 0) {
|
|
21
|
+
xml += ' <mappings>\n';
|
|
22
|
+
for (const mapping of flow.fieldMappings) {
|
|
23
|
+
xml += ` <map from="${escapeXml(mapping.from)}" to="${escapeXml(mapping.to)}"/>\n`;
|
|
24
|
+
}
|
|
25
|
+
xml += ' </mappings>\n';
|
|
26
|
+
}
|
|
27
|
+
xml += ' </via>\n';
|
|
28
|
+
}
|
|
29
|
+
xml += ' </slice-flow>\n';
|
|
30
|
+
return xml;
|
|
31
|
+
}
|
|
32
|
+
function formatExternalFlowXml(flow, direction, model) {
|
|
33
|
+
let xml = ` <flow ${direction === 'inbound' ? `to-slice="${escapeXml(flow.targetSlice?.name ?? 'unknown')}"` : `from-slice="${escapeXml(flow.sourceSlice?.name ?? 'unknown')}"`} type="${flow.flowType}">\n`;
|
|
34
|
+
// For inbound: source is external, for outbound: target is external
|
|
35
|
+
if (direction === 'inbound') {
|
|
36
|
+
// Find the chapter of the external source
|
|
37
|
+
const sourceSlice = flow.sourceSlice;
|
|
38
|
+
let chapterInfo = '';
|
|
39
|
+
if (sourceSlice) {
|
|
40
|
+
const sourceSliceObj = model.slices.get(sourceSlice.id);
|
|
41
|
+
if (sourceSliceObj) {
|
|
42
|
+
const sourceChapter = findChapterForSlice(model, sourceSliceObj);
|
|
43
|
+
if (sourceChapter) {
|
|
44
|
+
chapterInfo = ` chapter="${escapeXml(sourceChapter.name)}"`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
xml += ` <from external="true"${chapterInfo}>${escapeXml(flow.source.name)} (${flow.source.type})</from>\n`;
|
|
49
|
+
xml += ` <to>${escapeXml(flow.target.name)} (${flow.target.type})</to>\n`;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
xml += ` <from>${escapeXml(flow.source.name)} (${flow.source.type})</from>\n`;
|
|
53
|
+
// Find the chapter of the external target
|
|
54
|
+
const targetSlice = flow.targetSlice;
|
|
55
|
+
let chapterInfo = '';
|
|
56
|
+
if (targetSlice) {
|
|
57
|
+
const targetSliceObj = model.slices.get(targetSlice.id);
|
|
58
|
+
if (targetSliceObj) {
|
|
59
|
+
const targetChapter = findChapterForSlice(model, targetSliceObj);
|
|
60
|
+
if (targetChapter) {
|
|
61
|
+
chapterInfo = ` chapter="${escapeXml(targetChapter.name)}"`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
xml += ` <to external="true"${chapterInfo}>${escapeXml(flow.target.name)} (${flow.target.type})</to>\n`;
|
|
66
|
+
}
|
|
67
|
+
if (flow.fieldMappings.length > 0) {
|
|
68
|
+
xml += ' <mappings>\n';
|
|
69
|
+
for (const mapping of flow.fieldMappings) {
|
|
70
|
+
xml += ` <map from="${escapeXml(mapping.from)}" to="${escapeXml(mapping.to)}"/>\n`;
|
|
71
|
+
}
|
|
72
|
+
xml += ' </mappings>\n';
|
|
73
|
+
}
|
|
74
|
+
xml += ' </flow>\n';
|
|
75
|
+
return xml;
|
|
76
|
+
}
|
|
12
77
|
export function showChapter(model, name, format) {
|
|
13
78
|
const chapter = findElementOrExit(model.chapters, name, 'chapter');
|
|
14
79
|
const slices = getSlicesUnderChapter(model, chapter);
|
|
80
|
+
// Get flow information
|
|
81
|
+
const sliceToSliceFlows = findSliceToSliceFlows(model, slices);
|
|
82
|
+
const chapterInboundFlows = findChapterInboundFlows(model, slices);
|
|
83
|
+
const chapterOutboundFlows = findChapterOutboundFlows(model, slices);
|
|
15
84
|
if (format === 'json') {
|
|
16
|
-
|
|
85
|
+
const result = {
|
|
17
86
|
id: chapter.id,
|
|
18
87
|
name: chapter.name,
|
|
88
|
+
parent: chapter ? getChapterHierarchy(model, chapter).parent : undefined,
|
|
19
89
|
slices: slices.map(s => ({ id: s.id, name: s.name, status: s.status }))
|
|
20
|
-
}
|
|
90
|
+
};
|
|
91
|
+
// Add flow graph if there are slice-to-slice flows
|
|
92
|
+
if (sliceToSliceFlows.length > 0) {
|
|
93
|
+
result.flowGraph = sliceToSliceFlows.map(sf => ({
|
|
94
|
+
from: sf.fromSlice.name,
|
|
95
|
+
to: sf.toSlice.name,
|
|
96
|
+
flows: sf.flows.map(f => ({
|
|
97
|
+
type: f.flowType,
|
|
98
|
+
source: { element: f.source.name, type: f.source.type },
|
|
99
|
+
target: { element: f.target.name, type: f.target.type },
|
|
100
|
+
...(f.fieldMappings.length > 0 && {
|
|
101
|
+
mappings: f.fieldMappings.map(m => ({ from: m.from, to: m.to }))
|
|
102
|
+
})
|
|
103
|
+
}))
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
// Add external dependencies
|
|
107
|
+
if (chapterInboundFlows.length > 0 || chapterOutboundFlows.length > 0) {
|
|
108
|
+
result.externalDependencies = {
|
|
109
|
+
...(chapterInboundFlows.length > 0 && {
|
|
110
|
+
inbound: chapterInboundFlows.map(f => {
|
|
111
|
+
const sourceSliceObj = f.sourceSlice ? model.slices.get(f.sourceSlice.id) : null;
|
|
112
|
+
const sourceChapter = sourceSliceObj ? findChapterForSlice(model, sourceSliceObj) : null;
|
|
113
|
+
return {
|
|
114
|
+
toSlice: f.targetSlice?.name,
|
|
115
|
+
type: f.flowType,
|
|
116
|
+
from: {
|
|
117
|
+
element: f.source.name,
|
|
118
|
+
type: f.source.type,
|
|
119
|
+
slice: f.sourceSlice?.name,
|
|
120
|
+
chapter: sourceChapter?.name
|
|
121
|
+
},
|
|
122
|
+
to: { element: f.target.name, type: f.target.type },
|
|
123
|
+
...(f.fieldMappings.length > 0 && {
|
|
124
|
+
mappings: f.fieldMappings.map(m => ({ from: m.from, to: m.to }))
|
|
125
|
+
})
|
|
126
|
+
};
|
|
127
|
+
})
|
|
128
|
+
}),
|
|
129
|
+
...(chapterOutboundFlows.length > 0 && {
|
|
130
|
+
outbound: chapterOutboundFlows.map(f => {
|
|
131
|
+
const targetSliceObj = f.targetSlice ? model.slices.get(f.targetSlice.id) : null;
|
|
132
|
+
const targetChapter = targetSliceObj ? findChapterForSlice(model, targetSliceObj) : null;
|
|
133
|
+
return {
|
|
134
|
+
fromSlice: f.sourceSlice?.name,
|
|
135
|
+
type: f.flowType,
|
|
136
|
+
from: { element: f.source.name, type: f.source.type },
|
|
137
|
+
to: {
|
|
138
|
+
element: f.target.name,
|
|
139
|
+
type: f.target.type,
|
|
140
|
+
slice: f.targetSlice?.name,
|
|
141
|
+
chapter: targetChapter?.name
|
|
142
|
+
},
|
|
143
|
+
...(f.fieldMappings.length > 0 && {
|
|
144
|
+
mappings: f.fieldMappings.map(m => ({ from: m.from, to: m.to }))
|
|
145
|
+
})
|
|
146
|
+
};
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
outputJson(result);
|
|
21
152
|
return;
|
|
22
153
|
}
|
|
154
|
+
// XML format
|
|
23
155
|
console.log(`<chapter id="${chapter.id}" name="${escapeXml(chapter.name)}">`);
|
|
156
|
+
// Slices section
|
|
24
157
|
if (slices.length === 0) {
|
|
25
158
|
console.log(' <slices/>');
|
|
26
159
|
}
|
|
@@ -31,5 +164,32 @@ export function showChapter(model, name, format) {
|
|
|
31
164
|
}
|
|
32
165
|
console.log(' </slices>');
|
|
33
166
|
}
|
|
167
|
+
// Flow graph section
|
|
168
|
+
if (sliceToSliceFlows.length > 0) {
|
|
169
|
+
console.log(' <flow-graph>');
|
|
170
|
+
for (const sliceFlow of sliceToSliceFlows) {
|
|
171
|
+
process.stdout.write(formatSliceFlowXml(sliceFlow));
|
|
172
|
+
}
|
|
173
|
+
console.log(' </flow-graph>');
|
|
174
|
+
}
|
|
175
|
+
// External dependencies section
|
|
176
|
+
if (chapterInboundFlows.length > 0 || chapterOutboundFlows.length > 0) {
|
|
177
|
+
console.log(' <external-dependencies>');
|
|
178
|
+
if (chapterInboundFlows.length > 0) {
|
|
179
|
+
console.log(' <inbound>');
|
|
180
|
+
for (const flow of chapterInboundFlows) {
|
|
181
|
+
process.stdout.write(formatExternalFlowXml(flow, 'inbound', model));
|
|
182
|
+
}
|
|
183
|
+
console.log(' </inbound>');
|
|
184
|
+
}
|
|
185
|
+
if (chapterOutboundFlows.length > 0) {
|
|
186
|
+
console.log(' <outbound>');
|
|
187
|
+
for (const flow of chapterOutboundFlows) {
|
|
188
|
+
process.stdout.write(formatExternalFlowXml(flow, 'outbound', model));
|
|
189
|
+
}
|
|
190
|
+
console.log(' </outbound>');
|
|
191
|
+
}
|
|
192
|
+
console.log(' </external-dependencies>');
|
|
193
|
+
}
|
|
34
194
|
console.log('</chapter>');
|
|
35
195
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { escapeXml, escapeXmlText, outputJson } from '../../lib/format.js';
|
|
2
2
|
import { findElementOrExit } from '../../lib/element-lookup.js';
|
|
3
|
+
import { findChapterForSlice, getChapterHierarchy } from '../../lib/chapter-utils.js';
|
|
4
|
+
import { getInboundFlows, getOutboundFlows, getFlowsForElement, } from '../../lib/flow-utils.js';
|
|
3
5
|
function formatFieldValues(values) {
|
|
4
6
|
if (!values || Object.keys(values).length === 0)
|
|
5
7
|
return '';
|
|
@@ -102,9 +104,91 @@ function findActorForScreen(model, screen) {
|
|
|
102
104
|
}
|
|
103
105
|
return null;
|
|
104
106
|
}
|
|
107
|
+
function formatChapterXml(hierarchy, indent) {
|
|
108
|
+
let xml = `${indent}<chapter id="${hierarchy.id}" name="${escapeXml(hierarchy.name)}"`;
|
|
109
|
+
if (hierarchy.parent) {
|
|
110
|
+
xml += '>\n';
|
|
111
|
+
xml += formatChapterXml(hierarchy.parent, indent + ' ');
|
|
112
|
+
xml += `${indent}</chapter>\n`;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
xml += '/>\n';
|
|
116
|
+
}
|
|
117
|
+
return xml;
|
|
118
|
+
}
|
|
119
|
+
// Format inline flow annotations for a component
|
|
120
|
+
function formatFlowAnnotationsXml(elementId, incoming, outgoing, componentIds, indent) {
|
|
121
|
+
let xml = '';
|
|
122
|
+
// Flows coming into this element
|
|
123
|
+
const flowsFrom = incoming.map(f => {
|
|
124
|
+
const external = !componentIds.has(f.source.id);
|
|
125
|
+
return external
|
|
126
|
+
? `${escapeXml(f.source.name)} (${f.source.type}) [external]`
|
|
127
|
+
: `${escapeXml(f.source.name)} (${f.source.type})`;
|
|
128
|
+
});
|
|
129
|
+
if (flowsFrom.length > 0) {
|
|
130
|
+
xml += `${indent}<flows-from>${flowsFrom.join(', ')}</flows-from>\n`;
|
|
131
|
+
}
|
|
132
|
+
// Flows going out of this element
|
|
133
|
+
const flowsTo = outgoing.map(f => {
|
|
134
|
+
const external = !componentIds.has(f.target.id);
|
|
135
|
+
return external
|
|
136
|
+
? `${escapeXml(f.target.name)} (${f.target.type}) [external]`
|
|
137
|
+
: `${escapeXml(f.target.name)} (${f.target.type})`;
|
|
138
|
+
});
|
|
139
|
+
if (flowsTo.length > 0) {
|
|
140
|
+
xml += `${indent}<flows-to>${flowsTo.join(', ')}</flows-to>\n`;
|
|
141
|
+
}
|
|
142
|
+
return xml;
|
|
143
|
+
}
|
|
144
|
+
// Format inbound flows section
|
|
145
|
+
function formatInboundFlowsXml(flows) {
|
|
146
|
+
if (flows.length === 0)
|
|
147
|
+
return '';
|
|
148
|
+
let xml = ' <inbound-flows>\n';
|
|
149
|
+
for (const flow of flows) {
|
|
150
|
+
xml += ` <flow type="${flow.flowType}">\n`;
|
|
151
|
+
const sourceSliceInfo = flow.sourceSlice ? ` slice="${escapeXml(flow.sourceSlice.name)}"` : '';
|
|
152
|
+
xml += ` <from${sourceSliceInfo}>${escapeXml(flow.source.name)} (${flow.source.type})</from>\n`;
|
|
153
|
+
xml += ` <to>${escapeXml(flow.target.name)} (${flow.target.type})</to>\n`;
|
|
154
|
+
if (flow.fieldMappings.length > 0) {
|
|
155
|
+
xml += ' <mappings>\n';
|
|
156
|
+
for (const mapping of flow.fieldMappings) {
|
|
157
|
+
xml += ` <map from="${escapeXml(mapping.from)}" to="${escapeXml(mapping.to)}"/>\n`;
|
|
158
|
+
}
|
|
159
|
+
xml += ' </mappings>\n';
|
|
160
|
+
}
|
|
161
|
+
xml += ' </flow>\n';
|
|
162
|
+
}
|
|
163
|
+
xml += ' </inbound-flows>\n';
|
|
164
|
+
return xml;
|
|
165
|
+
}
|
|
166
|
+
// Format outbound flows section
|
|
167
|
+
function formatOutboundFlowsXml(flows) {
|
|
168
|
+
if (flows.length === 0)
|
|
169
|
+
return '';
|
|
170
|
+
let xml = ' <outbound-flows>\n';
|
|
171
|
+
for (const flow of flows) {
|
|
172
|
+
xml += ` <flow type="${flow.flowType}">\n`;
|
|
173
|
+
xml += ` <from>${escapeXml(flow.source.name)} (${flow.source.type})</from>\n`;
|
|
174
|
+
const targetSliceInfo = flow.targetSlice ? ` slice="${escapeXml(flow.targetSlice.name)}"` : '';
|
|
175
|
+
xml += ` <to${targetSliceInfo}>${escapeXml(flow.target.name)} (${flow.target.type})</to>\n`;
|
|
176
|
+
if (flow.fieldMappings.length > 0) {
|
|
177
|
+
xml += ' <mappings>\n';
|
|
178
|
+
for (const mapping of flow.fieldMappings) {
|
|
179
|
+
xml += ` <map from="${escapeXml(mapping.from)}" to="${escapeXml(mapping.to)}"/>\n`;
|
|
180
|
+
}
|
|
181
|
+
xml += ' </mappings>\n';
|
|
182
|
+
}
|
|
183
|
+
xml += ' </flow>\n';
|
|
184
|
+
}
|
|
185
|
+
xml += ' </outbound-flows>\n';
|
|
186
|
+
return xml;
|
|
187
|
+
}
|
|
105
188
|
function formatSliceXml(model, slice) {
|
|
106
189
|
const components = getSliceComponents(model, slice);
|
|
107
190
|
const scenarios = [...model.scenarios.values()].filter(s => s.sliceId === slice.id);
|
|
191
|
+
const chapter = findChapterForSlice(model, slice);
|
|
108
192
|
const componentIds = new Set();
|
|
109
193
|
components.commands.forEach(c => componentIds.add(c.id));
|
|
110
194
|
components.events.forEach(e => componentIds.add(e.id));
|
|
@@ -113,6 +197,9 @@ function formatSliceXml(model, slice) {
|
|
|
113
197
|
components.processors.forEach(p => componentIds.add(p.id));
|
|
114
198
|
const flows = [...model.flows.values()].filter(f => componentIds.has(f.sourceId) || componentIds.has(f.targetId));
|
|
115
199
|
const internalFlows = flows.filter(f => componentIds.has(f.sourceId) && componentIds.has(f.targetId));
|
|
200
|
+
// Get inbound and outbound flows for the slice
|
|
201
|
+
const inboundFlows = getInboundFlows(model, slice);
|
|
202
|
+
const outboundFlows = getOutboundFlows(model, slice);
|
|
116
203
|
function getName(id) {
|
|
117
204
|
return (model.commands.get(id)?.name ??
|
|
118
205
|
model.events.get(id)?.name ??
|
|
@@ -122,6 +209,9 @@ function formatSliceXml(model, slice) {
|
|
|
122
209
|
id);
|
|
123
210
|
}
|
|
124
211
|
let xml = `<slice id="${slice.id}" name="${escapeXml(slice.name)}" status="${slice.status}">\n`;
|
|
212
|
+
if (chapter) {
|
|
213
|
+
xml += formatChapterXml(getChapterHierarchy(model, chapter), ' ');
|
|
214
|
+
}
|
|
125
215
|
xml += ' <components>\n';
|
|
126
216
|
for (const screen of components.screens) {
|
|
127
217
|
// Check if this is a linked copy
|
|
@@ -137,6 +227,9 @@ function formatSliceXml(model, slice) {
|
|
|
137
227
|
const actor = findActorForScreen(model, screen);
|
|
138
228
|
const actorAttr = actor ? ` actor="${escapeXml(actor.name)}"` : '';
|
|
139
229
|
xml += ` <screen id="${screen.id}" name="${escapeXml(screen.name)}"${copyAttr}${originAttr}${actorAttr}>\n`;
|
|
230
|
+
// Add flow annotations
|
|
231
|
+
const screenFlows = getFlowsForElement(model, screen.id);
|
|
232
|
+
xml += formatFlowAnnotationsXml(screen.id, screenFlows.incoming, screenFlows.outgoing, componentIds, ' ');
|
|
140
233
|
if (screen.fields.length > 0) {
|
|
141
234
|
xml += ' <fields>\n';
|
|
142
235
|
for (const field of screen.fields) {
|
|
@@ -148,6 +241,9 @@ function formatSliceXml(model, slice) {
|
|
|
148
241
|
}
|
|
149
242
|
for (const command of components.commands) {
|
|
150
243
|
xml += ` <command id="${command.id}" name="${escapeXml(command.name)}">\n`;
|
|
244
|
+
// Add flow annotations
|
|
245
|
+
const commandFlows = getFlowsForElement(model, command.id);
|
|
246
|
+
xml += formatFlowAnnotationsXml(command.id, commandFlows.incoming, commandFlows.outgoing, componentIds, ' ');
|
|
151
247
|
if (command.fields.length > 0) {
|
|
152
248
|
xml += ' <fields>\n';
|
|
153
249
|
for (const field of command.fields) {
|
|
@@ -179,6 +275,9 @@ function formatSliceXml(model, slice) {
|
|
|
179
275
|
const aggregate = findAggregateForEvent(model, event);
|
|
180
276
|
const aggregateAttr = aggregate ? ` aggregate="${escapeXml(aggregate.name)}"` : '';
|
|
181
277
|
xml += ` <event id="${event.id}" name="${escapeXml(event.name)}"${copyAttr}${originAttr}${aggregateAttr}>\n`;
|
|
278
|
+
// Add flow annotations
|
|
279
|
+
const eventFlows = getFlowsForElement(model, event.id);
|
|
280
|
+
xml += formatFlowAnnotationsXml(event.id, eventFlows.incoming, eventFlows.outgoing, componentIds, ' ');
|
|
182
281
|
if (event.fields.length > 0) {
|
|
183
282
|
xml += ' <fields>\n';
|
|
184
283
|
for (const field of event.fields) {
|
|
@@ -210,6 +309,9 @@ function formatSliceXml(model, slice) {
|
|
|
210
309
|
}
|
|
211
310
|
}
|
|
212
311
|
xml += ` <read-model id="${readModel.id}" name="${escapeXml(readModel.name)}"${copyAttr}${originAttr}>\n`;
|
|
312
|
+
// Add flow annotations
|
|
313
|
+
const rmFlows = getFlowsForElement(model, readModel.id);
|
|
314
|
+
xml += formatFlowAnnotationsXml(readModel.id, rmFlows.incoming, rmFlows.outgoing, componentIds, ' ');
|
|
213
315
|
if (readModel.fields.length > 0) {
|
|
214
316
|
xml += ' <fields>\n';
|
|
215
317
|
for (const field of readModel.fields) {
|
|
@@ -221,6 +323,9 @@ function formatSliceXml(model, slice) {
|
|
|
221
323
|
}
|
|
222
324
|
for (const processor of components.processors) {
|
|
223
325
|
xml += ` <processor id="${processor.id}" name="${escapeXml(processor.name)}">\n`;
|
|
326
|
+
// Add flow annotations
|
|
327
|
+
const procFlows = getFlowsForElement(model, processor.id);
|
|
328
|
+
xml += formatFlowAnnotationsXml(processor.id, procFlows.incoming, procFlows.outgoing, componentIds, ' ');
|
|
224
329
|
if (processor.fields.length > 0) {
|
|
225
330
|
xml += ' <fields>\n';
|
|
226
331
|
for (const field of processor.fields) {
|
|
@@ -297,6 +402,9 @@ function formatSliceXml(model, slice) {
|
|
|
297
402
|
}
|
|
298
403
|
xml += ' </scenarios>\n';
|
|
299
404
|
}
|
|
405
|
+
// Add inbound and outbound flows sections
|
|
406
|
+
xml += formatInboundFlowsXml(inboundFlows);
|
|
407
|
+
xml += formatOutboundFlowsXml(outboundFlows);
|
|
300
408
|
xml += '</slice>';
|
|
301
409
|
return xml;
|
|
302
410
|
}
|
|
@@ -321,6 +429,7 @@ function fieldToJson(field) {
|
|
|
321
429
|
function formatSliceJson(model, slice) {
|
|
322
430
|
const components = getSliceComponents(model, slice);
|
|
323
431
|
const scenarios = [...model.scenarios.values()].filter(s => s.sliceId === slice.id);
|
|
432
|
+
const chapter = findChapterForSlice(model, slice);
|
|
324
433
|
const componentIds = new Set();
|
|
325
434
|
components.commands.forEach(c => componentIds.add(c.id));
|
|
326
435
|
components.events.forEach(e => componentIds.add(e.id));
|
|
@@ -329,6 +438,9 @@ function formatSliceJson(model, slice) {
|
|
|
329
438
|
components.processors.forEach(p => componentIds.add(p.id));
|
|
330
439
|
const flows = [...model.flows.values()].filter(f => componentIds.has(f.sourceId) || componentIds.has(f.targetId));
|
|
331
440
|
const internalFlows = flows.filter(f => componentIds.has(f.sourceId) && componentIds.has(f.targetId));
|
|
441
|
+
// Get inbound and outbound flows for the slice
|
|
442
|
+
const inboundFlows = getInboundFlows(model, slice);
|
|
443
|
+
const outboundFlows = getOutboundFlows(model, slice);
|
|
332
444
|
function getName(id) {
|
|
333
445
|
return (model.commands.get(id)?.name ??
|
|
334
446
|
model.events.get(id)?.name ??
|
|
@@ -341,6 +453,7 @@ function formatSliceJson(model, slice) {
|
|
|
341
453
|
id: slice.id,
|
|
342
454
|
name: slice.name,
|
|
343
455
|
status: slice.status,
|
|
456
|
+
...(chapter && { chapter: getChapterHierarchy(model, chapter) }),
|
|
344
457
|
components: {
|
|
345
458
|
screens: components.screens.map(screen => {
|
|
346
459
|
const screenObj = {
|
|
@@ -357,13 +470,47 @@ function formatSliceJson(model, slice) {
|
|
|
357
470
|
const actor = findActorForScreen(model, screen);
|
|
358
471
|
if (actor)
|
|
359
472
|
screenObj.actor = actor.name;
|
|
473
|
+
// Add flow annotations
|
|
474
|
+
const screenFlows = getFlowsForElement(model, screen.id);
|
|
475
|
+
if (screenFlows.incoming.length > 0) {
|
|
476
|
+
screenObj.flowsFrom = screenFlows.incoming.map(f => ({
|
|
477
|
+
element: f.source.name,
|
|
478
|
+
type: f.source.type,
|
|
479
|
+
external: !componentIds.has(f.source.id)
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
482
|
+
if (screenFlows.outgoing.length > 0) {
|
|
483
|
+
screenObj.flowsTo = screenFlows.outgoing.map(f => ({
|
|
484
|
+
element: f.target.name,
|
|
485
|
+
type: f.target.type,
|
|
486
|
+
external: !componentIds.has(f.target.id)
|
|
487
|
+
}));
|
|
488
|
+
}
|
|
360
489
|
return screenObj;
|
|
361
490
|
}),
|
|
362
|
-
commands: components.commands.map(cmd =>
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
491
|
+
commands: components.commands.map(cmd => {
|
|
492
|
+
const cmdObj = {
|
|
493
|
+
id: cmd.id,
|
|
494
|
+
name: cmd.name,
|
|
495
|
+
fields: cmd.fields.map(fieldToJson)
|
|
496
|
+
};
|
|
497
|
+
const cmdFlows = getFlowsForElement(model, cmd.id);
|
|
498
|
+
if (cmdFlows.incoming.length > 0) {
|
|
499
|
+
cmdObj.flowsFrom = cmdFlows.incoming.map(f => ({
|
|
500
|
+
element: f.source.name,
|
|
501
|
+
type: f.source.type,
|
|
502
|
+
external: !componentIds.has(f.source.id)
|
|
503
|
+
}));
|
|
504
|
+
}
|
|
505
|
+
if (cmdFlows.outgoing.length > 0) {
|
|
506
|
+
cmdObj.flowsTo = cmdFlows.outgoing.map(f => ({
|
|
507
|
+
element: f.target.name,
|
|
508
|
+
type: f.target.type,
|
|
509
|
+
external: !componentIds.has(f.target.id)
|
|
510
|
+
}));
|
|
511
|
+
}
|
|
512
|
+
return cmdObj;
|
|
513
|
+
}),
|
|
367
514
|
events: components.events.map(event => {
|
|
368
515
|
const eventObj = {
|
|
369
516
|
id: event.id,
|
|
@@ -379,6 +526,22 @@ function formatSliceJson(model, slice) {
|
|
|
379
526
|
const aggregate = findAggregateForEvent(model, event);
|
|
380
527
|
if (aggregate)
|
|
381
528
|
eventObj.aggregate = aggregate.name;
|
|
529
|
+
// Add flow annotations
|
|
530
|
+
const eventFlows = getFlowsForElement(model, event.id);
|
|
531
|
+
if (eventFlows.incoming.length > 0) {
|
|
532
|
+
eventObj.flowsFrom = eventFlows.incoming.map(f => ({
|
|
533
|
+
element: f.source.name,
|
|
534
|
+
type: f.source.type,
|
|
535
|
+
external: !componentIds.has(f.source.id)
|
|
536
|
+
}));
|
|
537
|
+
}
|
|
538
|
+
if (eventFlows.outgoing.length > 0) {
|
|
539
|
+
eventObj.flowsTo = eventFlows.outgoing.map(f => ({
|
|
540
|
+
element: f.target.name,
|
|
541
|
+
type: f.target.type,
|
|
542
|
+
external: !componentIds.has(f.target.id)
|
|
543
|
+
}));
|
|
544
|
+
}
|
|
382
545
|
return eventObj;
|
|
383
546
|
}),
|
|
384
547
|
readModels: components.readModels.map(rm => {
|
|
@@ -393,13 +556,47 @@ function formatSliceJson(model, slice) {
|
|
|
393
556
|
if (originSlice)
|
|
394
557
|
rmObj.originSlice = originSlice.name;
|
|
395
558
|
}
|
|
559
|
+
// Add flow annotations
|
|
560
|
+
const rmFlows = getFlowsForElement(model, rm.id);
|
|
561
|
+
if (rmFlows.incoming.length > 0) {
|
|
562
|
+
rmObj.flowsFrom = rmFlows.incoming.map(f => ({
|
|
563
|
+
element: f.source.name,
|
|
564
|
+
type: f.source.type,
|
|
565
|
+
external: !componentIds.has(f.source.id)
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
if (rmFlows.outgoing.length > 0) {
|
|
569
|
+
rmObj.flowsTo = rmFlows.outgoing.map(f => ({
|
|
570
|
+
element: f.target.name,
|
|
571
|
+
type: f.target.type,
|
|
572
|
+
external: !componentIds.has(f.target.id)
|
|
573
|
+
}));
|
|
574
|
+
}
|
|
396
575
|
return rmObj;
|
|
397
576
|
}),
|
|
398
|
-
processors: components.processors.map(proc =>
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
577
|
+
processors: components.processors.map(proc => {
|
|
578
|
+
const procObj = {
|
|
579
|
+
id: proc.id,
|
|
580
|
+
name: proc.name,
|
|
581
|
+
fields: proc.fields.map(fieldToJson)
|
|
582
|
+
};
|
|
583
|
+
const procFlows = getFlowsForElement(model, proc.id);
|
|
584
|
+
if (procFlows.incoming.length > 0) {
|
|
585
|
+
procObj.flowsFrom = procFlows.incoming.map(f => ({
|
|
586
|
+
element: f.source.name,
|
|
587
|
+
type: f.source.type,
|
|
588
|
+
external: !componentIds.has(f.source.id)
|
|
589
|
+
}));
|
|
590
|
+
}
|
|
591
|
+
if (procFlows.outgoing.length > 0) {
|
|
592
|
+
procObj.flowsTo = procFlows.outgoing.map(f => ({
|
|
593
|
+
element: f.target.name,
|
|
594
|
+
type: f.target.type,
|
|
595
|
+
external: !componentIds.has(f.target.id)
|
|
596
|
+
}));
|
|
597
|
+
}
|
|
598
|
+
return procObj;
|
|
599
|
+
})
|
|
403
600
|
}
|
|
404
601
|
};
|
|
405
602
|
if (internalFlows.length > 0) {
|
|
@@ -460,6 +657,41 @@ function formatSliceJson(model, slice) {
|
|
|
460
657
|
return scenarioObj;
|
|
461
658
|
});
|
|
462
659
|
}
|
|
660
|
+
// Add inbound and outbound flows
|
|
661
|
+
if (inboundFlows.length > 0) {
|
|
662
|
+
result.inboundFlows = inboundFlows.map(f => ({
|
|
663
|
+
type: f.flowType,
|
|
664
|
+
from: {
|
|
665
|
+
element: f.source.name,
|
|
666
|
+
type: f.source.type,
|
|
667
|
+
...(f.sourceSlice && { slice: f.sourceSlice.name })
|
|
668
|
+
},
|
|
669
|
+
to: {
|
|
670
|
+
element: f.target.name,
|
|
671
|
+
type: f.target.type
|
|
672
|
+
},
|
|
673
|
+
...(f.fieldMappings.length > 0 && {
|
|
674
|
+
mappings: f.fieldMappings.map(m => ({ from: m.from, to: m.to }))
|
|
675
|
+
})
|
|
676
|
+
}));
|
|
677
|
+
}
|
|
678
|
+
if (outboundFlows.length > 0) {
|
|
679
|
+
result.outboundFlows = outboundFlows.map(f => ({
|
|
680
|
+
type: f.flowType,
|
|
681
|
+
from: {
|
|
682
|
+
element: f.source.name,
|
|
683
|
+
type: f.source.type
|
|
684
|
+
},
|
|
685
|
+
to: {
|
|
686
|
+
element: f.target.name,
|
|
687
|
+
type: f.target.type,
|
|
688
|
+
...(f.targetSlice && { slice: f.targetSlice.name })
|
|
689
|
+
},
|
|
690
|
+
...(f.fieldMappings.length > 0 && {
|
|
691
|
+
mappings: f.fieldMappings.map(m => ({ from: m.from, to: m.to }))
|
|
692
|
+
})
|
|
693
|
+
}));
|
|
694
|
+
}
|
|
463
695
|
return result;
|
|
464
696
|
}
|
|
465
697
|
export function showSlice(model, name, format) {
|