eventmodeler 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/projection.js +132 -0
- package/dist/slices/add-field/index.js +4 -33
- package/dist/slices/add-scenario/index.js +7 -74
- package/dist/slices/create-automation-slice/index.d.ts +2 -0
- package/dist/slices/create-automation-slice/index.js +217 -0
- package/dist/slices/create-flow/index.d.ts +2 -0
- package/dist/slices/create-flow/index.js +177 -0
- package/dist/slices/create-state-change-slice/index.d.ts +2 -0
- package/dist/slices/create-state-change-slice/index.js +239 -0
- package/dist/slices/create-state-view-slice/index.d.ts +2 -0
- package/dist/slices/create-state-view-slice/index.js +120 -0
- package/dist/slices/list-chapters/index.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 +108 -19
- 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
|
+
}
|
package/dist/projection.js
CHANGED
|
@@ -824,6 +824,138 @@ function applyEvent(model, event) {
|
|
|
824
824
|
case 'ScreenDesignUpdated':
|
|
825
825
|
// Screen designs are visual-only, not relevant for CLI
|
|
826
826
|
break;
|
|
827
|
+
// Compound slice placement events - place entire slices atomically
|
|
828
|
+
case 'StateChangeSlicePlaced': {
|
|
829
|
+
// Create slice
|
|
830
|
+
model.slices.set(event.sliceId, {
|
|
831
|
+
id: event.sliceId,
|
|
832
|
+
name: event.sliceName,
|
|
833
|
+
status: 'created',
|
|
834
|
+
position: event.slicePosition,
|
|
835
|
+
size: event.sliceSize,
|
|
836
|
+
nodeIds: [event.screenId, event.commandId, event.eventId],
|
|
837
|
+
});
|
|
838
|
+
// Create screen
|
|
839
|
+
model.screens.set(event.screenId, {
|
|
840
|
+
id: event.screenId,
|
|
841
|
+
name: event.screenName,
|
|
842
|
+
fields: event.screenFields,
|
|
843
|
+
position: event.screenPosition,
|
|
844
|
+
width: event.screenSize.width,
|
|
845
|
+
height: event.screenSize.height,
|
|
846
|
+
});
|
|
847
|
+
// Create command
|
|
848
|
+
model.commands.set(event.commandId, {
|
|
849
|
+
id: event.commandId,
|
|
850
|
+
name: event.commandName,
|
|
851
|
+
fields: event.commandFields,
|
|
852
|
+
position: event.commandPosition,
|
|
853
|
+
width: event.commandSize.width,
|
|
854
|
+
height: event.commandSize.height,
|
|
855
|
+
});
|
|
856
|
+
// Create event
|
|
857
|
+
model.events.set(event.eventId, {
|
|
858
|
+
id: event.eventId,
|
|
859
|
+
name: event.eventName,
|
|
860
|
+
fields: event.eventFields,
|
|
861
|
+
position: event.eventPosition,
|
|
862
|
+
width: event.eventSize.width,
|
|
863
|
+
height: event.eventSize.height,
|
|
864
|
+
});
|
|
865
|
+
// Create screen->command flow
|
|
866
|
+
model.flows.set(event.screenToCommandFlowId, {
|
|
867
|
+
id: event.screenToCommandFlowId,
|
|
868
|
+
flowType: 'ScreenToCommand',
|
|
869
|
+
sourceId: event.screenId,
|
|
870
|
+
targetId: event.commandId,
|
|
871
|
+
fieldMappings: event.screenToCommandMappings,
|
|
872
|
+
});
|
|
873
|
+
// Create command->event flow
|
|
874
|
+
model.flows.set(event.commandToEventFlowId, {
|
|
875
|
+
id: event.commandToEventFlowId,
|
|
876
|
+
flowType: 'CommandToEvent',
|
|
877
|
+
sourceId: event.commandId,
|
|
878
|
+
targetId: event.eventId,
|
|
879
|
+
fieldMappings: event.commandToEventMappings,
|
|
880
|
+
});
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
case 'AutomationSlicePlaced': {
|
|
884
|
+
// Create slice
|
|
885
|
+
model.slices.set(event.sliceId, {
|
|
886
|
+
id: event.sliceId,
|
|
887
|
+
name: event.sliceName,
|
|
888
|
+
status: 'created',
|
|
889
|
+
position: event.slicePosition,
|
|
890
|
+
size: event.sliceSize,
|
|
891
|
+
nodeIds: [event.processorId, event.commandId, event.eventId],
|
|
892
|
+
});
|
|
893
|
+
// Create processor
|
|
894
|
+
model.processors.set(event.processorId, {
|
|
895
|
+
id: event.processorId,
|
|
896
|
+
name: event.processorName,
|
|
897
|
+
fields: [],
|
|
898
|
+
position: event.processorPosition,
|
|
899
|
+
width: event.processorSize.width,
|
|
900
|
+
height: event.processorSize.height,
|
|
901
|
+
});
|
|
902
|
+
// Create command
|
|
903
|
+
model.commands.set(event.commandId, {
|
|
904
|
+
id: event.commandId,
|
|
905
|
+
name: event.commandName,
|
|
906
|
+
fields: event.commandFields,
|
|
907
|
+
position: event.commandPosition,
|
|
908
|
+
width: event.commandSize.width,
|
|
909
|
+
height: event.commandSize.height,
|
|
910
|
+
});
|
|
911
|
+
// Create event
|
|
912
|
+
model.events.set(event.eventId, {
|
|
913
|
+
id: event.eventId,
|
|
914
|
+
name: event.eventName,
|
|
915
|
+
fields: event.eventFields,
|
|
916
|
+
position: event.eventPosition,
|
|
917
|
+
width: event.eventSize.width,
|
|
918
|
+
height: event.eventSize.height,
|
|
919
|
+
});
|
|
920
|
+
// Create processor->command flow
|
|
921
|
+
model.flows.set(event.processorToCommandFlowId, {
|
|
922
|
+
id: event.processorToCommandFlowId,
|
|
923
|
+
flowType: 'ProcessorToCommand',
|
|
924
|
+
sourceId: event.processorId,
|
|
925
|
+
targetId: event.commandId,
|
|
926
|
+
fieldMappings: [],
|
|
927
|
+
});
|
|
928
|
+
// Create command->event flow
|
|
929
|
+
model.flows.set(event.commandToEventFlowId, {
|
|
930
|
+
id: event.commandToEventFlowId,
|
|
931
|
+
flowType: 'CommandToEvent',
|
|
932
|
+
sourceId: event.commandId,
|
|
933
|
+
targetId: event.eventId,
|
|
934
|
+
fieldMappings: event.commandToEventMappings,
|
|
935
|
+
});
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
case 'StateViewSlicePlaced': {
|
|
939
|
+
// Create slice
|
|
940
|
+
model.slices.set(event.sliceId, {
|
|
941
|
+
id: event.sliceId,
|
|
942
|
+
name: event.sliceName,
|
|
943
|
+
status: 'created',
|
|
944
|
+
position: event.slicePosition,
|
|
945
|
+
size: event.sliceSize,
|
|
946
|
+
nodeIds: [event.readModelId],
|
|
947
|
+
});
|
|
948
|
+
// Create read model
|
|
949
|
+
model.readModels.set(event.readModelId, {
|
|
950
|
+
id: event.readModelId,
|
|
951
|
+
name: event.readModelName,
|
|
952
|
+
fields: event.readModelFields,
|
|
953
|
+
position: event.readModelPosition,
|
|
954
|
+
width: event.readModelSize.width,
|
|
955
|
+
height: event.readModelSize.height,
|
|
956
|
+
});
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
827
959
|
}
|
|
828
960
|
}
|
|
829
961
|
// Merge propagated fields with existing fields:
|
|
@@ -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}"`);
|