eventmodeler 0.2.3 → 0.2.5
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 +63 -0
- package/dist/lib/element-lookup.d.ts +47 -0
- package/dist/lib/element-lookup.js +86 -0
- package/dist/lib/slice-utils.d.ts +83 -0
- package/dist/lib/slice-utils.js +135 -0
- package/dist/slices/add-field/index.js +4 -33
- package/dist/slices/add-scenario/index.js +15 -77
- package/dist/slices/create-automation-slice/index.d.ts +2 -0
- package/dist/slices/create-automation-slice/index.js +217 -0
- package/dist/slices/create-flow/index.d.ts +2 -0
- package/dist/slices/create-flow/index.js +177 -0
- package/dist/slices/create-state-change-slice/index.d.ts +2 -0
- package/dist/slices/create-state-change-slice/index.js +239 -0
- package/dist/slices/create-state-view-slice/index.d.ts +2 -0
- package/dist/slices/create-state-view-slice/index.js +120 -0
- package/dist/slices/list-chapters/index.js +2 -2
- package/dist/slices/list-commands/index.js +2 -2
- package/dist/slices/list-events/index.js +3 -2
- package/dist/slices/list-slices/index.js +2 -2
- package/dist/slices/mark-slice-status/index.js +2 -11
- package/dist/slices/remove-field/index.js +4 -33
- package/dist/slices/remove-scenario/index.js +45 -11
- package/dist/slices/show-actor/index.js +2 -11
- package/dist/slices/show-aggregate-completeness/index.js +2 -11
- package/dist/slices/show-chapter/index.js +6 -14
- package/dist/slices/show-command/index.js +4 -12
- package/dist/slices/show-completeness/index.js +378 -32
- package/dist/slices/show-event/index.js +4 -12
- package/dist/slices/show-slice/index.js +14 -17
- package/dist/slices/update-field/index.js +4 -33
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -31,6 +31,10 @@ import { mapFields } from './slices/map-fields/index.js';
|
|
|
31
31
|
import { updateField } from './slices/update-field/index.js';
|
|
32
32
|
import { showAggregateCompleteness, listAggregates } from './slices/show-aggregate-completeness/index.js';
|
|
33
33
|
import { showActor, listActors } from './slices/show-actor/index.js';
|
|
34
|
+
import { createStateChangeSlice } from './slices/create-state-change-slice/index.js';
|
|
35
|
+
import { createAutomationSlice } from './slices/create-automation-slice/index.js';
|
|
36
|
+
import { createStateViewSlice } from './slices/create-state-view-slice/index.js';
|
|
37
|
+
import { createFlow } from './slices/create-flow/index.js';
|
|
34
38
|
const args = process.argv.slice(2);
|
|
35
39
|
function getNamedArg(argList, ...names) {
|
|
36
40
|
for (let i = 0; i < argList.length; i++) {
|
|
@@ -87,6 +91,15 @@ COMMANDS:
|
|
|
87
91
|
update field --command|--event|--read-model <name> --field <name> [--optional true|false] [--generated true|false]
|
|
88
92
|
Update field properties
|
|
89
93
|
|
|
94
|
+
create state-change-slice --xml <data>
|
|
95
|
+
Create a state-change slice (Screen → Command → Event)
|
|
96
|
+
create automation-slice --xml <data>
|
|
97
|
+
Create an automation slice (Processor → Command → Event)
|
|
98
|
+
create state-view-slice --xml <data>
|
|
99
|
+
Create a state-view slice (Read Model)
|
|
100
|
+
create flow --from <source> --to <target>
|
|
101
|
+
Create a flow between elements (Event→ReadModel, ReadModel→Screen/Processor)
|
|
102
|
+
|
|
90
103
|
summary Show model summary statistics
|
|
91
104
|
|
|
92
105
|
export json Export entire model as JSON
|
|
@@ -413,6 +426,56 @@ async function main() {
|
|
|
413
426
|
process.exit(1);
|
|
414
427
|
}
|
|
415
428
|
break;
|
|
429
|
+
case 'create':
|
|
430
|
+
switch (subcommand) {
|
|
431
|
+
case 'state-change-slice': {
|
|
432
|
+
const xmlArg = getNamedArg(filteredArgs, '--xml');
|
|
433
|
+
if (!xmlArg) {
|
|
434
|
+
console.error('Error: --xml is required');
|
|
435
|
+
console.error('Usage: eventmodeler create state-change-slice --xml \'<state-change-slice>...</state-change-slice>\'');
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
createStateChangeSlice(model, filePath, xmlArg);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
case 'automation-slice': {
|
|
442
|
+
const xmlArg = getNamedArg(filteredArgs, '--xml');
|
|
443
|
+
if (!xmlArg) {
|
|
444
|
+
console.error('Error: --xml is required');
|
|
445
|
+
console.error('Usage: eventmodeler create automation-slice --xml \'<automation-slice>...</automation-slice>\'');
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
createAutomationSlice(model, filePath, xmlArg);
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case 'state-view-slice': {
|
|
452
|
+
const xmlArg = getNamedArg(filteredArgs, '--xml');
|
|
453
|
+
if (!xmlArg) {
|
|
454
|
+
console.error('Error: --xml is required');
|
|
455
|
+
console.error('Usage: eventmodeler create state-view-slice --xml \'<state-view-slice>...</state-view-slice>\'');
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
createStateViewSlice(model, filePath, xmlArg);
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
case 'flow': {
|
|
462
|
+
const fromArg = getNamedArg(filteredArgs, '--from');
|
|
463
|
+
const toArg = getNamedArg(filteredArgs, '--to');
|
|
464
|
+
if (!fromArg || !toArg) {
|
|
465
|
+
console.error('Error: --from and --to are required');
|
|
466
|
+
console.error('Usage: eventmodeler create flow --from <source> --to <target>');
|
|
467
|
+
console.error('Valid flows: Event→ReadModel, ReadModel→Screen, ReadModel→Processor');
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
createFlow(model, filePath, fromArg, toArg);
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
default:
|
|
474
|
+
console.error(`Unknown create target: ${subcommand}`);
|
|
475
|
+
console.error('Valid targets: state-change-slice, automation-slice, state-view-slice, flow');
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
break;
|
|
416
479
|
default:
|
|
417
480
|
console.error(`Unknown command: ${command}`);
|
|
418
481
|
printHelp();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element lookup utilities for CLI commands.
|
|
3
|
+
* Provides exact-match lookup with UUID disambiguation for duplicate names.
|
|
4
|
+
*/
|
|
5
|
+
export type LookupResult<T> = {
|
|
6
|
+
success: true;
|
|
7
|
+
element: T;
|
|
8
|
+
} | {
|
|
9
|
+
success: false;
|
|
10
|
+
error: 'not_found' | 'ambiguous';
|
|
11
|
+
matches: T[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Find an element by exact name (case-insensitive) or UUID.
|
|
15
|
+
* - If search starts with "id:", treats rest as UUID/UUID prefix
|
|
16
|
+
* - Otherwise, performs case-insensitive exact name match
|
|
17
|
+
* - Returns error if multiple elements have the same name
|
|
18
|
+
*/
|
|
19
|
+
export declare function findElement<T extends {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
}>(elements: Map<string, T> | T[], search: string): LookupResult<T>;
|
|
23
|
+
/**
|
|
24
|
+
* Format an element name with its ID for display
|
|
25
|
+
*/
|
|
26
|
+
export declare function formatElementWithId<T extends {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
}>(element: T, truncateId?: boolean): string;
|
|
30
|
+
/**
|
|
31
|
+
* Print error message for lookup failures and exit
|
|
32
|
+
*/
|
|
33
|
+
export declare function handleLookupError<T extends {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
}>(search: string, elementType: string, result: {
|
|
37
|
+
success: false;
|
|
38
|
+
error: 'not_found' | 'ambiguous';
|
|
39
|
+
matches: T[];
|
|
40
|
+
}, allElements: Map<string, T> | T[]): never;
|
|
41
|
+
/**
|
|
42
|
+
* Convenience function: find element or exit with error
|
|
43
|
+
*/
|
|
44
|
+
export declare function findElementOrExit<T extends {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
}>(elements: Map<string, T> | T[], search: string, elementType: string): T;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element lookup utilities for CLI commands.
|
|
3
|
+
* Provides exact-match lookup with UUID disambiguation for duplicate names.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Find an element by exact name (case-insensitive) or UUID.
|
|
7
|
+
* - If search starts with "id:", treats rest as UUID/UUID prefix
|
|
8
|
+
* - Otherwise, performs case-insensitive exact name match
|
|
9
|
+
* - Returns error if multiple elements have the same name
|
|
10
|
+
*/
|
|
11
|
+
export function findElement(elements, search) {
|
|
12
|
+
const elementArray = Array.isArray(elements) ? elements : [...elements.values()];
|
|
13
|
+
// Check for UUID lookup (id:prefix or full UUID format)
|
|
14
|
+
if (search.startsWith('id:')) {
|
|
15
|
+
const idSearch = search.slice(3).toLowerCase();
|
|
16
|
+
const match = elementArray.find(e => e.id.toLowerCase().startsWith(idSearch));
|
|
17
|
+
if (match) {
|
|
18
|
+
return { success: true, element: match };
|
|
19
|
+
}
|
|
20
|
+
return { success: false, error: 'not_found', matches: [] };
|
|
21
|
+
}
|
|
22
|
+
// Check if the search looks like a UUID (contains dashes in UUID pattern)
|
|
23
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
|
+
if (uuidPattern.test(search)) {
|
|
25
|
+
const match = elementArray.find(e => e.id.toLowerCase() === search.toLowerCase());
|
|
26
|
+
if (match) {
|
|
27
|
+
return { success: true, element: match };
|
|
28
|
+
}
|
|
29
|
+
return { success: false, error: 'not_found', matches: [] };
|
|
30
|
+
}
|
|
31
|
+
// Case-insensitive exact name match
|
|
32
|
+
const searchLower = search.toLowerCase();
|
|
33
|
+
const matches = elementArray.filter(e => e.name.toLowerCase() === searchLower);
|
|
34
|
+
if (matches.length === 1) {
|
|
35
|
+
return { success: true, element: matches[0] };
|
|
36
|
+
}
|
|
37
|
+
if (matches.length > 1) {
|
|
38
|
+
return { success: false, error: 'ambiguous', matches };
|
|
39
|
+
}
|
|
40
|
+
return { success: false, error: 'not_found', matches: [] };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Format an element name with its ID for display
|
|
44
|
+
*/
|
|
45
|
+
export function formatElementWithId(element, truncateId = true) {
|
|
46
|
+
const id = truncateId ? element.id.slice(0, 8) : element.id;
|
|
47
|
+
return `"${element.name}" (id: ${id})`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Print error message for lookup failures and exit
|
|
51
|
+
*/
|
|
52
|
+
export function handleLookupError(search, elementType, result, allElements) {
|
|
53
|
+
const elementArray = Array.isArray(allElements) ? allElements : [...allElements.values()];
|
|
54
|
+
if (result.error === 'ambiguous') {
|
|
55
|
+
console.error(`Error: Multiple ${elementType}s found with name "${search}"`);
|
|
56
|
+
console.error('Please specify using the element ID:');
|
|
57
|
+
for (const el of result.matches) {
|
|
58
|
+
console.error(` - ${formatElementWithId(el, false)}`);
|
|
59
|
+
}
|
|
60
|
+
console.error('');
|
|
61
|
+
console.error(`Usage: eventmodeler <command> "id:${result.matches[0].id.slice(0, 8)}"`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.error(`Error: ${elementType.charAt(0).toUpperCase() + elementType.slice(1)} not found: "${search}"`);
|
|
65
|
+
if (elementArray.length > 0) {
|
|
66
|
+
console.error(`Available ${elementType}s:`);
|
|
67
|
+
for (const el of elementArray) {
|
|
68
|
+
console.error(` - ${formatElementWithId(el)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.error(`No ${elementType}s exist in the model.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Convenience function: find element or exit with error
|
|
79
|
+
*/
|
|
80
|
+
export function findElementOrExit(elements, search, elementType) {
|
|
81
|
+
const result = findElement(elements, search);
|
|
82
|
+
if (!result.success) {
|
|
83
|
+
handleLookupError(search, elementType, result, elements);
|
|
84
|
+
}
|
|
85
|
+
return result.element;
|
|
86
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { EventModel, Field, FieldMapping, Slice } from '../types.js';
|
|
2
|
+
export declare const STATE_CHANGE_SLICE: {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
screen: {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
offsetX: number;
|
|
9
|
+
offsetY: number;
|
|
10
|
+
};
|
|
11
|
+
command: {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
offsetX: number;
|
|
15
|
+
offsetY: number;
|
|
16
|
+
};
|
|
17
|
+
event: {
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
offsetX: number;
|
|
21
|
+
offsetY: number;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export declare const AUTOMATION_SLICE: {
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
processor: {
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
offsetX: number;
|
|
31
|
+
offsetY: number;
|
|
32
|
+
};
|
|
33
|
+
command: {
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
offsetX: number;
|
|
37
|
+
offsetY: number;
|
|
38
|
+
};
|
|
39
|
+
event: {
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
offsetX: number;
|
|
43
|
+
offsetY: number;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export declare const STATE_VIEW_SLICE: {
|
|
47
|
+
width: number;
|
|
48
|
+
height: number;
|
|
49
|
+
readModel: {
|
|
50
|
+
width: number;
|
|
51
|
+
height: number;
|
|
52
|
+
offsetX: number;
|
|
53
|
+
offsetY: number;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
export declare const SLICE_GAP = 20;
|
|
57
|
+
export interface PositionReference {
|
|
58
|
+
after?: string;
|
|
59
|
+
before?: string;
|
|
60
|
+
}
|
|
61
|
+
export declare function findSliceByName(model: EventModel, name: string): Slice | undefined;
|
|
62
|
+
export declare function validateSliceNameUnique(model: EventModel, name: string): void;
|
|
63
|
+
export declare function calculateSlicePosition(model: EventModel, sliceWidth: number, ref: PositionReference): {
|
|
64
|
+
x: number;
|
|
65
|
+
y: number;
|
|
66
|
+
};
|
|
67
|
+
export declare function getSlicesToShift(model: EventModel, insertX: number, shiftAmount: number): Array<{
|
|
68
|
+
sliceId: string;
|
|
69
|
+
newX: number;
|
|
70
|
+
currentY: number;
|
|
71
|
+
}>;
|
|
72
|
+
export declare function inferFieldMappings(sourceFields: Field[], targetFields: Field[]): FieldMapping[];
|
|
73
|
+
export interface FieldInput {
|
|
74
|
+
name: string;
|
|
75
|
+
type: string;
|
|
76
|
+
isList?: boolean;
|
|
77
|
+
isGenerated?: boolean;
|
|
78
|
+
isOptional?: boolean;
|
|
79
|
+
isUserInput?: boolean;
|
|
80
|
+
subfields?: FieldInput[];
|
|
81
|
+
}
|
|
82
|
+
export declare function parseFieldsFromXml(xml: string): FieldInput[];
|
|
83
|
+
export declare function fieldInputToField(input: FieldInput): Field;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Fixed dimensions from requirements
|
|
2
|
+
export const STATE_CHANGE_SLICE = {
|
|
3
|
+
width: 560,
|
|
4
|
+
height: 1000,
|
|
5
|
+
screen: { width: 180, height: 120, offsetX: 40, offsetY: 120 },
|
|
6
|
+
command: { width: 160, height: 100, offsetX: 180, offsetY: 460 },
|
|
7
|
+
event: { width: 160, height: 100, offsetX: 360, offsetY: 860 },
|
|
8
|
+
};
|
|
9
|
+
export const AUTOMATION_SLICE = {
|
|
10
|
+
width: 560,
|
|
11
|
+
height: 1000,
|
|
12
|
+
processor: { width: 120, height: 120, offsetX: 40, offsetY: 120 },
|
|
13
|
+
command: { width: 160, height: 100, offsetX: 180, offsetY: 460 },
|
|
14
|
+
event: { width: 160, height: 100, offsetX: 360, offsetY: 860 },
|
|
15
|
+
};
|
|
16
|
+
export const STATE_VIEW_SLICE = {
|
|
17
|
+
width: 380,
|
|
18
|
+
height: 1000,
|
|
19
|
+
readModel: { width: 160, height: 100, offsetX: 110, offsetY: 460 },
|
|
20
|
+
};
|
|
21
|
+
export const SLICE_GAP = 20;
|
|
22
|
+
export function findSliceByName(model, name) {
|
|
23
|
+
const nameLower = name.toLowerCase();
|
|
24
|
+
return [...model.slices.values()].find(s => s.name.toLowerCase() === nameLower || s.name.toLowerCase().includes(nameLower));
|
|
25
|
+
}
|
|
26
|
+
export function validateSliceNameUnique(model, name) {
|
|
27
|
+
const existing = findSliceByName(model, name);
|
|
28
|
+
if (existing && existing.name.toLowerCase() === name.toLowerCase()) {
|
|
29
|
+
throw new Error(`Slice "${name}" already exists`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function calculateSlicePosition(model, sliceWidth, ref) {
|
|
33
|
+
const slices = [...model.slices.values()];
|
|
34
|
+
if (slices.length === 0) {
|
|
35
|
+
return { x: 0, y: 0 };
|
|
36
|
+
}
|
|
37
|
+
if (ref.after) {
|
|
38
|
+
const refSlice = findSliceByName(model, ref.after);
|
|
39
|
+
if (!refSlice) {
|
|
40
|
+
const available = slices.map(s => s.name).join(', ');
|
|
41
|
+
throw new Error(`Slice "${ref.after}" not found. Available: ${available}`);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
x: refSlice.position.x + refSlice.size.width + SLICE_GAP,
|
|
45
|
+
y: refSlice.position.y
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (ref.before) {
|
|
49
|
+
const refSlice = findSliceByName(model, ref.before);
|
|
50
|
+
if (!refSlice) {
|
|
51
|
+
const available = slices.map(s => s.name).join(', ');
|
|
52
|
+
throw new Error(`Slice "${ref.before}" not found. Available: ${available}`);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
x: refSlice.position.x,
|
|
56
|
+
y: refSlice.position.y
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Default: append to end (right of rightmost slice)
|
|
60
|
+
const rightmost = slices.reduce((max, s) => Math.max(max, s.position.x + s.size.width), 0);
|
|
61
|
+
const avgY = slices.reduce((sum, s) => sum + s.position.y, 0) / slices.length;
|
|
62
|
+
return {
|
|
63
|
+
x: rightmost + SLICE_GAP,
|
|
64
|
+
y: avgY
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function getSlicesToShift(model, insertX, shiftAmount) {
|
|
68
|
+
return [...model.slices.values()]
|
|
69
|
+
.filter(s => s.position.x >= insertX)
|
|
70
|
+
.map(s => ({
|
|
71
|
+
sliceId: s.id,
|
|
72
|
+
newX: s.position.x + shiftAmount,
|
|
73
|
+
currentY: s.position.y
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
export function inferFieldMappings(sourceFields, targetFields) {
|
|
77
|
+
const mappings = [];
|
|
78
|
+
for (const targetField of targetFields) {
|
|
79
|
+
// Skip generated fields - they have no source
|
|
80
|
+
if (targetField.isGenerated)
|
|
81
|
+
continue;
|
|
82
|
+
// Find source field with matching name (case-insensitive)
|
|
83
|
+
const sourceField = sourceFields.find(sf => sf.name.toLowerCase() === targetField.name.toLowerCase());
|
|
84
|
+
if (sourceField) {
|
|
85
|
+
mappings.push({
|
|
86
|
+
sourceFieldId: sourceField.id,
|
|
87
|
+
targetFieldId: targetField.id
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return mappings;
|
|
92
|
+
}
|
|
93
|
+
function getAttr(attrs, name) {
|
|
94
|
+
const match = attrs.match(new RegExp(`${name}="([^"]*)"`));
|
|
95
|
+
return match ? match[1] : undefined;
|
|
96
|
+
}
|
|
97
|
+
export function parseFieldsFromXml(xml) {
|
|
98
|
+
const fields = [];
|
|
99
|
+
// Match top-level field elements only (not nested inside other fields)
|
|
100
|
+
const fieldMatches = xml.matchAll(/<field([^>]*?)(?:\/>|>([\s\S]*?)<\/field>)/g);
|
|
101
|
+
for (const match of fieldMatches) {
|
|
102
|
+
const attrs = match[1];
|
|
103
|
+
const content = match[2];
|
|
104
|
+
const name = getAttr(attrs, 'name');
|
|
105
|
+
const type = getAttr(attrs, 'type');
|
|
106
|
+
if (!name || !type)
|
|
107
|
+
continue;
|
|
108
|
+
const field = {
|
|
109
|
+
name,
|
|
110
|
+
type,
|
|
111
|
+
isList: getAttr(attrs, 'list') === 'true',
|
|
112
|
+
isGenerated: getAttr(attrs, 'generated') === 'true',
|
|
113
|
+
isOptional: getAttr(attrs, 'optional') === 'true',
|
|
114
|
+
isUserInput: getAttr(attrs, 'user-input') === 'true',
|
|
115
|
+
};
|
|
116
|
+
// Parse nested subfields for Custom type
|
|
117
|
+
if (type === 'Custom' && content) {
|
|
118
|
+
field.subfields = parseFieldsFromXml(content);
|
|
119
|
+
}
|
|
120
|
+
fields.push(field);
|
|
121
|
+
}
|
|
122
|
+
return fields;
|
|
123
|
+
}
|
|
124
|
+
export function fieldInputToField(input) {
|
|
125
|
+
return {
|
|
126
|
+
id: crypto.randomUUID(),
|
|
127
|
+
name: input.name,
|
|
128
|
+
fieldType: input.type,
|
|
129
|
+
isList: input.isList ?? false,
|
|
130
|
+
isGenerated: input.isGenerated ?? false,
|
|
131
|
+
isOptional: input.isOptional,
|
|
132
|
+
isUserInput: input.isUserInput,
|
|
133
|
+
subfields: input.subfields?.map(fieldInputToField),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto';
|
|
2
2
|
import { appendEvent } from '../../lib/file-loader.js';
|
|
3
|
+
import { findElementOrExit } from '../../lib/element-lookup.js';
|
|
3
4
|
const validFieldTypes = ['UUID', 'Boolean', 'Double', 'Decimal', 'Date', 'DateTime', 'Long', 'Int', 'String', 'Custom'];
|
|
4
5
|
function parseJsonInput(input) {
|
|
5
6
|
return JSON.parse(input);
|
|
@@ -103,17 +104,7 @@ export function addField(model, filePath, options, input) {
|
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
function addFieldToCommand(model, filePath, commandName, fieldInput) {
|
|
106
|
-
const
|
|
107
|
-
const commands = [...model.commands.values()];
|
|
108
|
-
const command = commands.find(c => c.name.toLowerCase() === nameLower || c.name.toLowerCase().includes(nameLower));
|
|
109
|
-
if (!command) {
|
|
110
|
-
console.error(`Error: Command not found: ${commandName}`);
|
|
111
|
-
console.error('Available commands:');
|
|
112
|
-
for (const c of commands) {
|
|
113
|
-
console.error(` - ${c.name}`);
|
|
114
|
-
}
|
|
115
|
-
process.exit(1);
|
|
116
|
-
}
|
|
107
|
+
const command = findElementOrExit(model.commands, commandName, 'command');
|
|
117
108
|
// Check for duplicate field name
|
|
118
109
|
if (command.fields.some(f => f.name.toLowerCase() === fieldInput.name.toLowerCase())) {
|
|
119
110
|
console.error(`Error: Field "${fieldInput.name}" already exists on command "${command.name}"`);
|
|
@@ -129,17 +120,7 @@ function addFieldToCommand(model, filePath, commandName, fieldInput) {
|
|
|
129
120
|
console.log(`Added field "${field.name}" to command "${command.name}"`);
|
|
130
121
|
}
|
|
131
122
|
function addFieldToEvent(model, filePath, eventName, fieldInput) {
|
|
132
|
-
const
|
|
133
|
-
const events = [...model.events.values()];
|
|
134
|
-
const event = events.find(e => e.name.toLowerCase() === nameLower || e.name.toLowerCase().includes(nameLower));
|
|
135
|
-
if (!event) {
|
|
136
|
-
console.error(`Error: Event not found: ${eventName}`);
|
|
137
|
-
console.error('Available events:');
|
|
138
|
-
for (const e of events) {
|
|
139
|
-
console.error(` - ${e.name}`);
|
|
140
|
-
}
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
123
|
+
const event = findElementOrExit(model.events, eventName, 'event');
|
|
143
124
|
// Check for duplicate field name
|
|
144
125
|
if (event.fields.some(f => f.name.toLowerCase() === fieldInput.name.toLowerCase())) {
|
|
145
126
|
console.error(`Error: Field "${fieldInput.name}" already exists on event "${event.name}"`);
|
|
@@ -155,17 +136,7 @@ function addFieldToEvent(model, filePath, eventName, fieldInput) {
|
|
|
155
136
|
console.log(`Added field "${field.name}" to event "${event.name}"`);
|
|
156
137
|
}
|
|
157
138
|
function addFieldToReadModel(model, filePath, readModelName, fieldInput) {
|
|
158
|
-
const
|
|
159
|
-
const readModels = [...model.readModels.values()];
|
|
160
|
-
const readModel = readModels.find(rm => rm.name.toLowerCase() === nameLower || rm.name.toLowerCase().includes(nameLower));
|
|
161
|
-
if (!readModel) {
|
|
162
|
-
console.error(`Error: Read model not found: ${readModelName}`);
|
|
163
|
-
console.error('Available read models:');
|
|
164
|
-
for (const rm of readModels) {
|
|
165
|
-
console.error(` - ${rm.name}`);
|
|
166
|
-
}
|
|
167
|
-
process.exit(1);
|
|
168
|
-
}
|
|
139
|
+
const readModel = findElementOrExit(model.readModels, readModelName, 'read model');
|
|
169
140
|
// Check for duplicate field name
|
|
170
141
|
if (readModel.fields.some(f => f.name.toLowerCase() === fieldInput.name.toLowerCase())) {
|
|
171
142
|
console.error(`Error: Field "${fieldInput.name}" already exists on read model "${readModel.name}"`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto';
|
|
2
2
|
import { appendEvent } from '../../lib/file-loader.js';
|
|
3
|
+
import { findElementOrExit } from '../../lib/element-lookup.js';
|
|
3
4
|
function parseJsonInput(input) {
|
|
4
5
|
return JSON.parse(input);
|
|
5
6
|
}
|
|
@@ -144,27 +145,12 @@ export function addScenario(model, filePath, sliceName, input) {
|
|
|
144
145
|
process.exit(1);
|
|
145
146
|
}
|
|
146
147
|
// Find slice by name
|
|
147
|
-
const
|
|
148
|
-
const sliceNameLower = sliceName.toLowerCase();
|
|
149
|
-
const slice = slices.find(s => s.name.toLowerCase() === sliceNameLower || s.name.toLowerCase().includes(sliceNameLower));
|
|
150
|
-
if (!slice) {
|
|
151
|
-
console.error(`Error: Slice not found: ${sliceName}`);
|
|
152
|
-
console.error('Available slices:');
|
|
153
|
-
for (const s of slices) {
|
|
154
|
-
console.error(` - ${s.name}`);
|
|
155
|
-
}
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
148
|
+
const slice = findElementOrExit(model.slices, sliceName, 'slice');
|
|
158
149
|
// Resolve event references in given
|
|
159
150
|
const givenEvents = [];
|
|
160
151
|
if (scenarioInput.given) {
|
|
161
152
|
for (const g of scenarioInput.given) {
|
|
162
|
-
const event =
|
|
163
|
-
if (!event) {
|
|
164
|
-
console.error(`Error: Event not found in "given": ${g.event}`);
|
|
165
|
-
listAvailableEvents(model);
|
|
166
|
-
process.exit(1);
|
|
167
|
-
}
|
|
153
|
+
const event = findElementOrExit(model.events, g.event, 'event');
|
|
168
154
|
givenEvents.push({
|
|
169
155
|
eventStickyId: event.id,
|
|
170
156
|
fieldValues: g.fieldValues,
|
|
@@ -174,12 +160,7 @@ export function addScenario(model, filePath, sliceName, input) {
|
|
|
174
160
|
// Resolve command reference in when
|
|
175
161
|
let whenCommand = null;
|
|
176
162
|
if (scenarioInput.when) {
|
|
177
|
-
const command =
|
|
178
|
-
if (!command) {
|
|
179
|
-
console.error(`Error: Command not found in "when": ${scenarioInput.when.command}`);
|
|
180
|
-
listAvailableCommands(model);
|
|
181
|
-
process.exit(1);
|
|
182
|
-
}
|
|
163
|
+
const command = findElementOrExit(model.commands, scenarioInput.when.command, 'command');
|
|
183
164
|
whenCommand = {
|
|
184
165
|
commandStickyId: command.id,
|
|
185
166
|
fieldValues: scenarioInput.when.fieldValues,
|
|
@@ -195,12 +176,7 @@ export function addScenario(model, filePath, sliceName, input) {
|
|
|
195
176
|
then.expectedEvents = [];
|
|
196
177
|
if (scenarioInput.then.events) {
|
|
197
178
|
for (const e of scenarioInput.then.events) {
|
|
198
|
-
const event =
|
|
199
|
-
if (!event) {
|
|
200
|
-
console.error(`Error: Event not found in "then.events": ${e.event}`);
|
|
201
|
-
listAvailableEvents(model);
|
|
202
|
-
process.exit(1);
|
|
203
|
-
}
|
|
179
|
+
const event = findElementOrExit(model.events, e.event, 'event');
|
|
204
180
|
then.expectedEvents.push({
|
|
205
181
|
eventStickyId: event.id,
|
|
206
182
|
fieldValues: e.fieldValues,
|
|
@@ -213,21 +189,11 @@ export function addScenario(model, filePath, sliceName, input) {
|
|
|
213
189
|
console.error('Error: readModelAssertion requires a readModel name');
|
|
214
190
|
process.exit(1);
|
|
215
191
|
}
|
|
216
|
-
const readModel =
|
|
217
|
-
if (!readModel) {
|
|
218
|
-
console.error(`Error: Read model not found: ${scenarioInput.then.readModel}`);
|
|
219
|
-
listAvailableReadModels(model);
|
|
220
|
-
process.exit(1);
|
|
221
|
-
}
|
|
192
|
+
const readModel = findElementOrExit(model.readModels, scenarioInput.then.readModel, 'read model');
|
|
222
193
|
const assertionGivenEvents = [];
|
|
223
194
|
if (scenarioInput.then.givenEvents) {
|
|
224
195
|
for (const g of scenarioInput.then.givenEvents) {
|
|
225
|
-
const event =
|
|
226
|
-
if (!event) {
|
|
227
|
-
console.error(`Error: Event not found in "then.givenEvents": ${g.event}`);
|
|
228
|
-
listAvailableEvents(model);
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
196
|
+
const event = findElementOrExit(model.events, g.event, 'event');
|
|
231
197
|
assertionGivenEvents.push({
|
|
232
198
|
eventStickyId: event.id,
|
|
233
199
|
fieldValues: g.fieldValues,
|
|
@@ -241,15 +207,20 @@ export function addScenario(model, filePath, sliceName, input) {
|
|
|
241
207
|
};
|
|
242
208
|
}
|
|
243
209
|
// Calculate position below slice
|
|
210
|
+
// Scenarios are tall (GWT rows with 100px stickies + labels + padding = ~400px+)
|
|
211
|
+
// so we need adequate spacing between them
|
|
212
|
+
const SCENARIO_GAP = 20; // Match slice gap
|
|
213
|
+
const SCENARIO_ESTIMATED_HEIGHT = 450; // Approximate rendered height for positioning
|
|
244
214
|
const existingScenarios = [...model.scenarios.values()].filter(s => s.sliceId === slice.id);
|
|
245
215
|
const sliceBottom = slice.position.y + slice.size.height;
|
|
246
216
|
let positionY;
|
|
247
217
|
if (existingScenarios.length === 0) {
|
|
248
|
-
positionY = sliceBottom +
|
|
218
|
+
positionY = sliceBottom + SCENARIO_GAP;
|
|
249
219
|
}
|
|
250
220
|
else {
|
|
251
|
-
|
|
252
|
-
|
|
221
|
+
// Use estimated height since stored height (80px) doesn't reflect actual rendered size
|
|
222
|
+
const lowestY = Math.max(...existingScenarios.map(s => s.position.y + SCENARIO_ESTIMATED_HEIGHT));
|
|
223
|
+
positionY = lowestY + SCENARIO_GAP;
|
|
253
224
|
}
|
|
254
225
|
const position = {
|
|
255
226
|
x: slice.position.x + 10,
|
|
@@ -304,36 +275,3 @@ export function addScenario(model, filePath, sliceName, input) {
|
|
|
304
275
|
});
|
|
305
276
|
console.log(`Added scenario "${scenarioInput.name}" to slice "${slice.name}"`);
|
|
306
277
|
}
|
|
307
|
-
function findEventByName(model, name) {
|
|
308
|
-
const nameLower = name.toLowerCase();
|
|
309
|
-
const events = [...model.events.values()];
|
|
310
|
-
return events.find(e => e.name.toLowerCase() === nameLower || e.name.toLowerCase().includes(nameLower));
|
|
311
|
-
}
|
|
312
|
-
function findCommandByName(model, name) {
|
|
313
|
-
const nameLower = name.toLowerCase();
|
|
314
|
-
const commands = [...model.commands.values()];
|
|
315
|
-
return commands.find(c => c.name.toLowerCase() === nameLower || c.name.toLowerCase().includes(nameLower));
|
|
316
|
-
}
|
|
317
|
-
function findReadModelByName(model, name) {
|
|
318
|
-
const nameLower = name.toLowerCase();
|
|
319
|
-
const readModels = [...model.readModels.values()];
|
|
320
|
-
return readModels.find(rm => rm.name.toLowerCase() === nameLower || rm.name.toLowerCase().includes(nameLower));
|
|
321
|
-
}
|
|
322
|
-
function listAvailableEvents(model) {
|
|
323
|
-
console.error('Available events:');
|
|
324
|
-
for (const e of model.events.values()) {
|
|
325
|
-
console.error(` - ${e.name}`);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
function listAvailableCommands(model) {
|
|
329
|
-
console.error('Available commands:');
|
|
330
|
-
for (const c of model.commands.values()) {
|
|
331
|
-
console.error(` - ${c.name}`);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
function listAvailableReadModels(model) {
|
|
335
|
-
console.error('Available read models:');
|
|
336
|
-
for (const rm of model.readModels.values()) {
|
|
337
|
-
console.error(` - ${rm.name}`);
|
|
338
|
-
}
|
|
339
|
-
}
|