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.
Files changed (32) hide show
  1. package/dist/index.js +63 -0
  2. package/dist/lib/element-lookup.d.ts +47 -0
  3. package/dist/lib/element-lookup.js +86 -0
  4. package/dist/lib/slice-utils.d.ts +83 -0
  5. package/dist/lib/slice-utils.js +135 -0
  6. package/dist/projection.js +132 -0
  7. package/dist/slices/add-field/index.js +4 -33
  8. package/dist/slices/add-scenario/index.js +7 -74
  9. package/dist/slices/create-automation-slice/index.d.ts +2 -0
  10. package/dist/slices/create-automation-slice/index.js +217 -0
  11. package/dist/slices/create-flow/index.d.ts +2 -0
  12. package/dist/slices/create-flow/index.js +177 -0
  13. package/dist/slices/create-state-change-slice/index.d.ts +2 -0
  14. package/dist/slices/create-state-change-slice/index.js +239 -0
  15. package/dist/slices/create-state-view-slice/index.d.ts +2 -0
  16. package/dist/slices/create-state-view-slice/index.js +120 -0
  17. package/dist/slices/list-chapters/index.js +2 -2
  18. package/dist/slices/list-commands/index.js +2 -2
  19. package/dist/slices/list-events/index.js +3 -2
  20. package/dist/slices/list-slices/index.js +2 -2
  21. package/dist/slices/mark-slice-status/index.js +2 -11
  22. package/dist/slices/remove-field/index.js +4 -33
  23. package/dist/slices/remove-scenario/index.js +45 -11
  24. package/dist/slices/show-actor/index.js +2 -11
  25. package/dist/slices/show-aggregate-completeness/index.js +2 -11
  26. package/dist/slices/show-chapter/index.js +6 -14
  27. package/dist/slices/show-command/index.js +4 -12
  28. package/dist/slices/show-completeness/index.js +108 -19
  29. package/dist/slices/show-event/index.js +4 -12
  30. package/dist/slices/show-slice/index.js +14 -17
  31. package/dist/slices/update-field/index.js +4 -33
  32. 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
+ }
@@ -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 nameLower = commandName.toLowerCase();
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 nameLower = eventName.toLowerCase();
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 nameLower = readModelName.toLowerCase();
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}"`);