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.
@@ -1,5 +1,7 @@
1
1
  import { escapeXml, escapeXmlText, outputJson } from '../../lib/format.js';
2
2
  import { findElementOrExit } from '../../lib/element-lookup.js';
3
+ import { findChapterForSlice, getChapterHierarchy } from '../../lib/chapter-utils.js';
4
+ import { getInboundFlows, getOutboundFlows, getFlowsForElement, } from '../../lib/flow-utils.js';
3
5
  function formatFieldValues(values) {
4
6
  if (!values || Object.keys(values).length === 0)
5
7
  return '';
@@ -102,9 +104,91 @@ function findActorForScreen(model, screen) {
102
104
  }
103
105
  return null;
104
106
  }
107
+ function formatChapterXml(hierarchy, indent) {
108
+ let xml = `${indent}<chapter id="${hierarchy.id}" name="${escapeXml(hierarchy.name)}"`;
109
+ if (hierarchy.parent) {
110
+ xml += '>\n';
111
+ xml += formatChapterXml(hierarchy.parent, indent + ' ');
112
+ xml += `${indent}</chapter>\n`;
113
+ }
114
+ else {
115
+ xml += '/>\n';
116
+ }
117
+ return xml;
118
+ }
119
+ // Format inline flow annotations for a component
120
+ function formatFlowAnnotationsXml(elementId, incoming, outgoing, componentIds, indent) {
121
+ let xml = '';
122
+ // Flows coming into this element
123
+ const flowsFrom = incoming.map(f => {
124
+ const external = !componentIds.has(f.source.id);
125
+ return external
126
+ ? `${escapeXml(f.source.name)} (${f.source.type}) [external]`
127
+ : `${escapeXml(f.source.name)} (${f.source.type})`;
128
+ });
129
+ if (flowsFrom.length > 0) {
130
+ xml += `${indent}<flows-from>${flowsFrom.join(', ')}</flows-from>\n`;
131
+ }
132
+ // Flows going out of this element
133
+ const flowsTo = outgoing.map(f => {
134
+ const external = !componentIds.has(f.target.id);
135
+ return external
136
+ ? `${escapeXml(f.target.name)} (${f.target.type}) [external]`
137
+ : `${escapeXml(f.target.name)} (${f.target.type})`;
138
+ });
139
+ if (flowsTo.length > 0) {
140
+ xml += `${indent}<flows-to>${flowsTo.join(', ')}</flows-to>\n`;
141
+ }
142
+ return xml;
143
+ }
144
+ // Format inbound flows section
145
+ function formatInboundFlowsXml(flows) {
146
+ if (flows.length === 0)
147
+ return '';
148
+ let xml = ' <inbound-flows>\n';
149
+ for (const flow of flows) {
150
+ xml += ` <flow type="${flow.flowType}">\n`;
151
+ const sourceSliceInfo = flow.sourceSlice ? ` slice="${escapeXml(flow.sourceSlice.name)}"` : '';
152
+ xml += ` <from${sourceSliceInfo}>${escapeXml(flow.source.name)} (${flow.source.type})</from>\n`;
153
+ xml += ` <to>${escapeXml(flow.target.name)} (${flow.target.type})</to>\n`;
154
+ if (flow.fieldMappings.length > 0) {
155
+ xml += ' <mappings>\n';
156
+ for (const mapping of flow.fieldMappings) {
157
+ xml += ` <map from="${escapeXml(mapping.from)}" to="${escapeXml(mapping.to)}"/>\n`;
158
+ }
159
+ xml += ' </mappings>\n';
160
+ }
161
+ xml += ' </flow>\n';
162
+ }
163
+ xml += ' </inbound-flows>\n';
164
+ return xml;
165
+ }
166
+ // Format outbound flows section
167
+ function formatOutboundFlowsXml(flows) {
168
+ if (flows.length === 0)
169
+ return '';
170
+ let xml = ' <outbound-flows>\n';
171
+ for (const flow of flows) {
172
+ xml += ` <flow type="${flow.flowType}">\n`;
173
+ xml += ` <from>${escapeXml(flow.source.name)} (${flow.source.type})</from>\n`;
174
+ const targetSliceInfo = flow.targetSlice ? ` slice="${escapeXml(flow.targetSlice.name)}"` : '';
175
+ xml += ` <to${targetSliceInfo}>${escapeXml(flow.target.name)} (${flow.target.type})</to>\n`;
176
+ if (flow.fieldMappings.length > 0) {
177
+ xml += ' <mappings>\n';
178
+ for (const mapping of flow.fieldMappings) {
179
+ xml += ` <map from="${escapeXml(mapping.from)}" to="${escapeXml(mapping.to)}"/>\n`;
180
+ }
181
+ xml += ' </mappings>\n';
182
+ }
183
+ xml += ' </flow>\n';
184
+ }
185
+ xml += ' </outbound-flows>\n';
186
+ return xml;
187
+ }
105
188
  function formatSliceXml(model, slice) {
106
189
  const components = getSliceComponents(model, slice);
107
190
  const scenarios = [...model.scenarios.values()].filter(s => s.sliceId === slice.id);
191
+ const chapter = findChapterForSlice(model, slice);
108
192
  const componentIds = new Set();
109
193
  components.commands.forEach(c => componentIds.add(c.id));
110
194
  components.events.forEach(e => componentIds.add(e.id));
@@ -113,6 +197,9 @@ function formatSliceXml(model, slice) {
113
197
  components.processors.forEach(p => componentIds.add(p.id));
114
198
  const flows = [...model.flows.values()].filter(f => componentIds.has(f.sourceId) || componentIds.has(f.targetId));
115
199
  const internalFlows = flows.filter(f => componentIds.has(f.sourceId) && componentIds.has(f.targetId));
200
+ // Get inbound and outbound flows for the slice
201
+ const inboundFlows = getInboundFlows(model, slice);
202
+ const outboundFlows = getOutboundFlows(model, slice);
116
203
  function getName(id) {
117
204
  return (model.commands.get(id)?.name ??
118
205
  model.events.get(id)?.name ??
@@ -122,6 +209,9 @@ function formatSliceXml(model, slice) {
122
209
  id);
123
210
  }
124
211
  let xml = `<slice id="${slice.id}" name="${escapeXml(slice.name)}" status="${slice.status}">\n`;
212
+ if (chapter) {
213
+ xml += formatChapterXml(getChapterHierarchy(model, chapter), ' ');
214
+ }
125
215
  xml += ' <components>\n';
126
216
  for (const screen of components.screens) {
127
217
  // Check if this is a linked copy
@@ -137,6 +227,9 @@ function formatSliceXml(model, slice) {
137
227
  const actor = findActorForScreen(model, screen);
138
228
  const actorAttr = actor ? ` actor="${escapeXml(actor.name)}"` : '';
139
229
  xml += ` <screen id="${screen.id}" name="${escapeXml(screen.name)}"${copyAttr}${originAttr}${actorAttr}>\n`;
230
+ // Add flow annotations
231
+ const screenFlows = getFlowsForElement(model, screen.id);
232
+ xml += formatFlowAnnotationsXml(screen.id, screenFlows.incoming, screenFlows.outgoing, componentIds, ' ');
140
233
  if (screen.fields.length > 0) {
141
234
  xml += ' <fields>\n';
142
235
  for (const field of screen.fields) {
@@ -148,6 +241,9 @@ function formatSliceXml(model, slice) {
148
241
  }
149
242
  for (const command of components.commands) {
150
243
  xml += ` <command id="${command.id}" name="${escapeXml(command.name)}">\n`;
244
+ // Add flow annotations
245
+ const commandFlows = getFlowsForElement(model, command.id);
246
+ xml += formatFlowAnnotationsXml(command.id, commandFlows.incoming, commandFlows.outgoing, componentIds, ' ');
151
247
  if (command.fields.length > 0) {
152
248
  xml += ' <fields>\n';
153
249
  for (const field of command.fields) {
@@ -179,6 +275,9 @@ function formatSliceXml(model, slice) {
179
275
  const aggregate = findAggregateForEvent(model, event);
180
276
  const aggregateAttr = aggregate ? ` aggregate="${escapeXml(aggregate.name)}"` : '';
181
277
  xml += ` <event id="${event.id}" name="${escapeXml(event.name)}"${copyAttr}${originAttr}${aggregateAttr}>\n`;
278
+ // Add flow annotations
279
+ const eventFlows = getFlowsForElement(model, event.id);
280
+ xml += formatFlowAnnotationsXml(event.id, eventFlows.incoming, eventFlows.outgoing, componentIds, ' ');
182
281
  if (event.fields.length > 0) {
183
282
  xml += ' <fields>\n';
184
283
  for (const field of event.fields) {
@@ -210,6 +309,9 @@ function formatSliceXml(model, slice) {
210
309
  }
211
310
  }
212
311
  xml += ` <read-model id="${readModel.id}" name="${escapeXml(readModel.name)}"${copyAttr}${originAttr}>\n`;
312
+ // Add flow annotations
313
+ const rmFlows = getFlowsForElement(model, readModel.id);
314
+ xml += formatFlowAnnotationsXml(readModel.id, rmFlows.incoming, rmFlows.outgoing, componentIds, ' ');
213
315
  if (readModel.fields.length > 0) {
214
316
  xml += ' <fields>\n';
215
317
  for (const field of readModel.fields) {
@@ -221,6 +323,9 @@ function formatSliceXml(model, slice) {
221
323
  }
222
324
  for (const processor of components.processors) {
223
325
  xml += ` <processor id="${processor.id}" name="${escapeXml(processor.name)}">\n`;
326
+ // Add flow annotations
327
+ const procFlows = getFlowsForElement(model, processor.id);
328
+ xml += formatFlowAnnotationsXml(processor.id, procFlows.incoming, procFlows.outgoing, componentIds, ' ');
224
329
  if (processor.fields.length > 0) {
225
330
  xml += ' <fields>\n';
226
331
  for (const field of processor.fields) {
@@ -297,6 +402,9 @@ function formatSliceXml(model, slice) {
297
402
  }
298
403
  xml += ' </scenarios>\n';
299
404
  }
405
+ // Add inbound and outbound flows sections
406
+ xml += formatInboundFlowsXml(inboundFlows);
407
+ xml += formatOutboundFlowsXml(outboundFlows);
300
408
  xml += '</slice>';
301
409
  return xml;
302
410
  }
@@ -321,6 +429,7 @@ function fieldToJson(field) {
321
429
  function formatSliceJson(model, slice) {
322
430
  const components = getSliceComponents(model, slice);
323
431
  const scenarios = [...model.scenarios.values()].filter(s => s.sliceId === slice.id);
432
+ const chapter = findChapterForSlice(model, slice);
324
433
  const componentIds = new Set();
325
434
  components.commands.forEach(c => componentIds.add(c.id));
326
435
  components.events.forEach(e => componentIds.add(e.id));
@@ -329,6 +438,9 @@ function formatSliceJson(model, slice) {
329
438
  components.processors.forEach(p => componentIds.add(p.id));
330
439
  const flows = [...model.flows.values()].filter(f => componentIds.has(f.sourceId) || componentIds.has(f.targetId));
331
440
  const internalFlows = flows.filter(f => componentIds.has(f.sourceId) && componentIds.has(f.targetId));
441
+ // Get inbound and outbound flows for the slice
442
+ const inboundFlows = getInboundFlows(model, slice);
443
+ const outboundFlows = getOutboundFlows(model, slice);
332
444
  function getName(id) {
333
445
  return (model.commands.get(id)?.name ??
334
446
  model.events.get(id)?.name ??
@@ -341,6 +453,7 @@ function formatSliceJson(model, slice) {
341
453
  id: slice.id,
342
454
  name: slice.name,
343
455
  status: slice.status,
456
+ ...(chapter && { chapter: getChapterHierarchy(model, chapter) }),
344
457
  components: {
345
458
  screens: components.screens.map(screen => {
346
459
  const screenObj = {
@@ -357,13 +470,47 @@ function formatSliceJson(model, slice) {
357
470
  const actor = findActorForScreen(model, screen);
358
471
  if (actor)
359
472
  screenObj.actor = actor.name;
473
+ // Add flow annotations
474
+ const screenFlows = getFlowsForElement(model, screen.id);
475
+ if (screenFlows.incoming.length > 0) {
476
+ screenObj.flowsFrom = screenFlows.incoming.map(f => ({
477
+ element: f.source.name,
478
+ type: f.source.type,
479
+ external: !componentIds.has(f.source.id)
480
+ }));
481
+ }
482
+ if (screenFlows.outgoing.length > 0) {
483
+ screenObj.flowsTo = screenFlows.outgoing.map(f => ({
484
+ element: f.target.name,
485
+ type: f.target.type,
486
+ external: !componentIds.has(f.target.id)
487
+ }));
488
+ }
360
489
  return screenObj;
361
490
  }),
362
- commands: components.commands.map(cmd => ({
363
- id: cmd.id,
364
- name: cmd.name,
365
- fields: cmd.fields.map(fieldToJson)
366
- })),
491
+ commands: components.commands.map(cmd => {
492
+ const cmdObj = {
493
+ id: cmd.id,
494
+ name: cmd.name,
495
+ fields: cmd.fields.map(fieldToJson)
496
+ };
497
+ const cmdFlows = getFlowsForElement(model, cmd.id);
498
+ if (cmdFlows.incoming.length > 0) {
499
+ cmdObj.flowsFrom = cmdFlows.incoming.map(f => ({
500
+ element: f.source.name,
501
+ type: f.source.type,
502
+ external: !componentIds.has(f.source.id)
503
+ }));
504
+ }
505
+ if (cmdFlows.outgoing.length > 0) {
506
+ cmdObj.flowsTo = cmdFlows.outgoing.map(f => ({
507
+ element: f.target.name,
508
+ type: f.target.type,
509
+ external: !componentIds.has(f.target.id)
510
+ }));
511
+ }
512
+ return cmdObj;
513
+ }),
367
514
  events: components.events.map(event => {
368
515
  const eventObj = {
369
516
  id: event.id,
@@ -379,6 +526,22 @@ function formatSliceJson(model, slice) {
379
526
  const aggregate = findAggregateForEvent(model, event);
380
527
  if (aggregate)
381
528
  eventObj.aggregate = aggregate.name;
529
+ // Add flow annotations
530
+ const eventFlows = getFlowsForElement(model, event.id);
531
+ if (eventFlows.incoming.length > 0) {
532
+ eventObj.flowsFrom = eventFlows.incoming.map(f => ({
533
+ element: f.source.name,
534
+ type: f.source.type,
535
+ external: !componentIds.has(f.source.id)
536
+ }));
537
+ }
538
+ if (eventFlows.outgoing.length > 0) {
539
+ eventObj.flowsTo = eventFlows.outgoing.map(f => ({
540
+ element: f.target.name,
541
+ type: f.target.type,
542
+ external: !componentIds.has(f.target.id)
543
+ }));
544
+ }
382
545
  return eventObj;
383
546
  }),
384
547
  readModels: components.readModels.map(rm => {
@@ -393,13 +556,47 @@ function formatSliceJson(model, slice) {
393
556
  if (originSlice)
394
557
  rmObj.originSlice = originSlice.name;
395
558
  }
559
+ // Add flow annotations
560
+ const rmFlows = getFlowsForElement(model, rm.id);
561
+ if (rmFlows.incoming.length > 0) {
562
+ rmObj.flowsFrom = rmFlows.incoming.map(f => ({
563
+ element: f.source.name,
564
+ type: f.source.type,
565
+ external: !componentIds.has(f.source.id)
566
+ }));
567
+ }
568
+ if (rmFlows.outgoing.length > 0) {
569
+ rmObj.flowsTo = rmFlows.outgoing.map(f => ({
570
+ element: f.target.name,
571
+ type: f.target.type,
572
+ external: !componentIds.has(f.target.id)
573
+ }));
574
+ }
396
575
  return rmObj;
397
576
  }),
398
- processors: components.processors.map(proc => ({
399
- id: proc.id,
400
- name: proc.name,
401
- fields: proc.fields.map(fieldToJson)
402
- }))
577
+ processors: components.processors.map(proc => {
578
+ const procObj = {
579
+ id: proc.id,
580
+ name: proc.name,
581
+ fields: proc.fields.map(fieldToJson)
582
+ };
583
+ const procFlows = getFlowsForElement(model, proc.id);
584
+ if (procFlows.incoming.length > 0) {
585
+ procObj.flowsFrom = procFlows.incoming.map(f => ({
586
+ element: f.source.name,
587
+ type: f.source.type,
588
+ external: !componentIds.has(f.source.id)
589
+ }));
590
+ }
591
+ if (procFlows.outgoing.length > 0) {
592
+ procObj.flowsTo = procFlows.outgoing.map(f => ({
593
+ element: f.target.name,
594
+ type: f.target.type,
595
+ external: !componentIds.has(f.target.id)
596
+ }));
597
+ }
598
+ return procObj;
599
+ })
403
600
  }
404
601
  };
405
602
  if (internalFlows.length > 0) {
@@ -460,6 +657,41 @@ function formatSliceJson(model, slice) {
460
657
  return scenarioObj;
461
658
  });
462
659
  }
660
+ // Add inbound and outbound flows
661
+ if (inboundFlows.length > 0) {
662
+ result.inboundFlows = inboundFlows.map(f => ({
663
+ type: f.flowType,
664
+ from: {
665
+ element: f.source.name,
666
+ type: f.source.type,
667
+ ...(f.sourceSlice && { slice: f.sourceSlice.name })
668
+ },
669
+ to: {
670
+ element: f.target.name,
671
+ type: f.target.type
672
+ },
673
+ ...(f.fieldMappings.length > 0 && {
674
+ mappings: f.fieldMappings.map(m => ({ from: m.from, to: m.to }))
675
+ })
676
+ }));
677
+ }
678
+ if (outboundFlows.length > 0) {
679
+ result.outboundFlows = outboundFlows.map(f => ({
680
+ type: f.flowType,
681
+ from: {
682
+ element: f.source.name,
683
+ type: f.source.type
684
+ },
685
+ to: {
686
+ element: f.target.name,
687
+ type: f.target.type,
688
+ ...(f.targetSlice && { slice: f.targetSlice.name })
689
+ },
690
+ ...(f.fieldMappings.length > 0 && {
691
+ mappings: f.fieldMappings.map(m => ({ from: m.from, to: m.to }))
692
+ })
693
+ }));
694
+ }
463
695
  return result;
464
696
  }
465
697
  export function showSlice(model, name, format) {
@@ -8,5 +8,7 @@ export declare function updateField(model: EventModel, filePath: string, options
8
8
  command?: string;
9
9
  event?: string;
10
10
  readModel?: string;
11
+ screen?: string;
12
+ processor?: string;
11
13
  }, fieldName: string, updates: UpdateOptions): void;
12
14
  export {};
@@ -33,13 +33,13 @@ function createUpdatedField(original, updates) {
33
33
  }
34
34
  export function updateField(model, filePath, options, fieldName, updates) {
35
35
  // Determine which entity type
36
- const entityCount = [options.command, options.event, options.readModel].filter(Boolean).length;
36
+ const entityCount = [options.command, options.event, options.readModel, options.screen, options.processor].filter(Boolean).length;
37
37
  if (entityCount === 0) {
38
- console.error('Error: Must specify one of --command, --event, or --read-model');
38
+ console.error('Error: Must specify one of --command, --event, --read-model, --screen, or --processor');
39
39
  process.exit(1);
40
40
  }
41
41
  if (entityCount > 1) {
42
- console.error('Error: Can only specify one of --command, --event, or --read-model');
42
+ console.error('Error: Can only specify one of --command, --event, --read-model, --screen, or --processor');
43
43
  process.exit(1);
44
44
  }
45
45
  if (options.command) {
@@ -51,6 +51,12 @@ export function updateField(model, filePath, options, fieldName, updates) {
51
51
  else if (options.readModel) {
52
52
  updateReadModelField(model, filePath, options.readModel, fieldName, updates);
53
53
  }
54
+ else if (options.screen) {
55
+ updateScreenField(model, filePath, options.screen, fieldName, updates);
56
+ }
57
+ else if (options.processor) {
58
+ updateProcessorField(model, filePath, options.processor, fieldName, updates);
59
+ }
54
60
  }
55
61
  function updateCommandField(model, filePath, commandName, fieldName, updates) {
56
62
  const command = findElementOrExit(model.commands, commandName, 'command');
@@ -124,6 +130,54 @@ function updateReadModelField(model, filePath, readModelName, fieldName, updates
124
130
  console.log(`Updated field "${field.name}" on read model "${readModel.name}"`);
125
131
  logUpdates(updates);
126
132
  }
133
+ function updateScreenField(model, filePath, screenName, fieldName, updates) {
134
+ const screen = findElementOrExit(model.screens, screenName, 'screen');
135
+ const field = findFieldByName(screen.fields, fieldName);
136
+ if (!field) {
137
+ console.error(`Error: Field "${fieldName}" not found on screen "${screen.name}"`);
138
+ if (screen.fields.length > 0) {
139
+ console.error('Available fields:');
140
+ for (const f of screen.fields) {
141
+ console.error(` - ${f.name}`);
142
+ }
143
+ }
144
+ process.exit(1);
145
+ }
146
+ const updatedField = createUpdatedField(field, updates);
147
+ appendEvent(filePath, {
148
+ type: 'ScreenFieldAdjusted',
149
+ screenId: screen.id,
150
+ fieldId: field.id,
151
+ field: updatedField,
152
+ timestamp: Date.now(),
153
+ });
154
+ console.log(`Updated field "${field.name}" on screen "${screen.name}"`);
155
+ logUpdates(updates);
156
+ }
157
+ function updateProcessorField(model, filePath, processorName, fieldName, updates) {
158
+ const processor = findElementOrExit(model.processors, processorName, 'processor');
159
+ const field = findFieldByName(processor.fields, fieldName);
160
+ if (!field) {
161
+ console.error(`Error: Field "${fieldName}" not found on processor "${processor.name}"`);
162
+ if (processor.fields.length > 0) {
163
+ console.error('Available fields:');
164
+ for (const f of processor.fields) {
165
+ console.error(` - ${f.name}`);
166
+ }
167
+ }
168
+ process.exit(1);
169
+ }
170
+ const updatedField = createUpdatedField(field, updates);
171
+ appendEvent(filePath, {
172
+ type: 'ProcessorFieldAdjusted',
173
+ processorId: processor.id,
174
+ fieldId: field.id,
175
+ field: updatedField,
176
+ timestamp: Date.now(),
177
+ });
178
+ console.log(`Updated field "${field.name}" on processor "${processor.name}"`);
179
+ logUpdates(updates);
180
+ }
127
181
  function logUpdates(updates) {
128
182
  if (updates.optional !== undefined) {
129
183
  console.log(` isOptional: ${updates.optional}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eventmodeler",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "description": "CLI tool for interacting with Event Model files - query, update, and export event models from the terminal",
5
5
  "type": "module",
6
6
  "bin": {