eventmodeler 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -68,13 +68,13 @@ function createFieldFromInput(input) {
68
68
  }
69
69
  export function addField(model, filePath, options, input) {
70
70
  // Determine which entity type
71
- const entityCount = [options.command, options.event, options.readModel].filter(Boolean).length;
71
+ const entityCount = [options.command, options.event, options.readModel, options.screen, options.processor].filter(Boolean).length;
72
72
  if (entityCount === 0) {
73
- console.error('Error: Must specify one of --command, --event, or --read-model');
73
+ console.error('Error: Must specify one of --command, --event, --read-model, --screen, or --processor');
74
74
  process.exit(1);
75
75
  }
76
76
  if (entityCount > 1) {
77
- console.error('Error: Can only specify one of --command, --event, or --read-model');
77
+ console.error('Error: Can only specify one of --command, --event, --read-model, --screen, or --processor');
78
78
  process.exit(1);
79
79
  }
80
80
  // Parse input
@@ -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
@@ -102,6 +107,12 @@ export function addField(model, filePath, options, input) {
102
107
  else if (options.readModel) {
103
108
  addFieldToReadModel(model, filePath, options.readModel, fieldInput);
104
109
  }
110
+ else if (options.screen) {
111
+ addFieldToScreen(model, filePath, options.screen, fieldInput);
112
+ }
113
+ else if (options.processor) {
114
+ addFieldToProcessor(model, filePath, options.processor, fieldInput);
115
+ }
105
116
  }
106
117
  function addFieldToCommand(model, filePath, commandName, fieldInput) {
107
118
  const command = findElementOrExit(model.commands, commandName, 'command');
@@ -151,3 +162,35 @@ function addFieldToReadModel(model, filePath, readModelName, fieldInput) {
151
162
  });
152
163
  console.log(`Added field "${field.name}" to read model "${readModel.name}"`);
153
164
  }
165
+ function addFieldToScreen(model, filePath, screenName, fieldInput) {
166
+ const screen = findElementOrExit(model.screens, screenName, 'screen');
167
+ // Check for duplicate field name
168
+ if (screen.fields.some(f => f.name.toLowerCase() === fieldInput.name.toLowerCase())) {
169
+ console.error(`Error: Field "${fieldInput.name}" already exists on screen "${screen.name}"`);
170
+ process.exit(1);
171
+ }
172
+ const field = createFieldFromInput(fieldInput);
173
+ appendEvent(filePath, {
174
+ type: 'ScreenFieldAdded',
175
+ screenId: screen.id,
176
+ field,
177
+ timestamp: Date.now(),
178
+ });
179
+ console.log(`Added field "${field.name}" to screen "${screen.name}"`);
180
+ }
181
+ function addFieldToProcessor(model, filePath, processorName, fieldInput) {
182
+ const processor = findElementOrExit(model.processors, processorName, 'processor');
183
+ // Check for duplicate field name
184
+ if (processor.fields.some(f => f.name.toLowerCase() === fieldInput.name.toLowerCase())) {
185
+ console.error(`Error: Field "${fieldInput.name}" already exists on processor "${processor.name}"`);
186
+ process.exit(1);
187
+ }
188
+ const field = createFieldFromInput(fieldInput);
189
+ appendEvent(filePath, {
190
+ type: 'ProcessorFieldAdded',
191
+ processorId: processor.id,
192
+ field,
193
+ timestamp: Date.now(),
194
+ });
195
+ console.log(`Added field "${field.name}" to processor "${processor.name}"`);
196
+ }
@@ -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: { id: chapter.id, name: chapter.name } }),
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
- export function listSlices(model, format) {
3
- const slices = [...model.slices.values()];
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;
@@ -3,4 +3,6 @@ export declare function removeField(model: EventModel, filePath: string, options
3
3
  command?: string;
4
4
  event?: string;
5
5
  readModel?: string;
6
+ screen?: string;
7
+ processor?: string;
6
8
  }, fieldName: string): void;
@@ -2,13 +2,13 @@ import { appendEvent } from '../../lib/file-loader.js';
2
2
  import { findElementOrExit } from '../../lib/element-lookup.js';
3
3
  export function removeField(model, filePath, options, fieldName) {
4
4
  // Determine which entity type
5
- const entityCount = [options.command, options.event, options.readModel].filter(Boolean).length;
5
+ const entityCount = [options.command, options.event, options.readModel, options.screen, options.processor].filter(Boolean).length;
6
6
  if (entityCount === 0) {
7
- console.error('Error: Must specify one of --command, --event, or --read-model');
7
+ console.error('Error: Must specify one of --command, --event, --read-model, --screen, or --processor');
8
8
  process.exit(1);
9
9
  }
10
10
  if (entityCount > 1) {
11
- console.error('Error: Can only specify one of --command, --event, or --read-model');
11
+ console.error('Error: Can only specify one of --command, --event, --read-model, --screen, or --processor');
12
12
  process.exit(1);
13
13
  }
14
14
  if (options.command) {
@@ -20,6 +20,12 @@ export function removeField(model, filePath, options, fieldName) {
20
20
  else if (options.readModel) {
21
21
  removeFieldFromReadModel(model, filePath, options.readModel, fieldName);
22
22
  }
23
+ else if (options.screen) {
24
+ removeFieldFromScreen(model, filePath, options.screen, fieldName);
25
+ }
26
+ else if (options.processor) {
27
+ removeFieldFromProcessor(model, filePath, options.processor, fieldName);
28
+ }
23
29
  }
24
30
  function removeFieldFromCommand(model, filePath, commandName, fieldName) {
25
31
  const command = findElementOrExit(model.commands, commandName, 'command');
@@ -96,3 +102,53 @@ function removeFieldFromReadModel(model, filePath, readModelName, fieldName) {
96
102
  });
97
103
  console.log(`Removed field "${field.name}" from read model "${readModel.name}"`);
98
104
  }
105
+ function removeFieldFromScreen(model, filePath, screenName, fieldName) {
106
+ const screen = findElementOrExit(model.screens, screenName, 'screen');
107
+ const fieldNameLower = fieldName.toLowerCase();
108
+ const field = screen.fields.find(f => f.name.toLowerCase() === fieldNameLower);
109
+ if (!field) {
110
+ console.error(`Error: Field "${fieldName}" not found on screen "${screen.name}"`);
111
+ if (screen.fields.length > 0) {
112
+ console.error('Available fields:');
113
+ for (const f of screen.fields) {
114
+ console.error(` - ${f.name} (${f.fieldType})`);
115
+ }
116
+ }
117
+ else {
118
+ console.error('This screen has no fields.');
119
+ }
120
+ process.exit(1);
121
+ }
122
+ appendEvent(filePath, {
123
+ type: 'ScreenFieldRemoved',
124
+ screenId: screen.id,
125
+ fieldId: field.id,
126
+ timestamp: Date.now(),
127
+ });
128
+ console.log(`Removed field "${field.name}" from screen "${screen.name}"`);
129
+ }
130
+ function removeFieldFromProcessor(model, filePath, processorName, fieldName) {
131
+ const processor = findElementOrExit(model.processors, processorName, 'processor');
132
+ const fieldNameLower = fieldName.toLowerCase();
133
+ const field = processor.fields.find(f => f.name.toLowerCase() === fieldNameLower);
134
+ if (!field) {
135
+ console.error(`Error: Field "${fieldName}" not found on processor "${processor.name}"`);
136
+ if (processor.fields.length > 0) {
137
+ console.error('Available fields:');
138
+ for (const f of processor.fields) {
139
+ console.error(` - ${f.name} (${f.fieldType})`);
140
+ }
141
+ }
142
+ else {
143
+ console.error('This processor has no fields.');
144
+ }
145
+ process.exit(1);
146
+ }
147
+ appendEvent(filePath, {
148
+ type: 'ProcessorFieldRemoved',
149
+ processorId: processor.id,
150
+ fieldId: field.id,
151
+ timestamp: Date.now(),
152
+ });
153
+ console.log(`Removed field "${field.name}" from processor "${processor.name}"`);
154
+ }
@@ -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
- outputJson({
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
  }