eventmodeler 0.1.0 → 0.2.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.
package/dist/index.js CHANGED
@@ -15,7 +15,24 @@ import { openApp } from './slices/open-app/index.js';
15
15
  import { search } from './slices/search/index.js';
16
16
  import { listChapters } from './slices/list-chapters/index.js';
17
17
  import { showChapter } from './slices/show-chapter/index.js';
18
+ import { addScenario } from './slices/add-scenario/index.js';
19
+ import { addField } from './slices/add-field/index.js';
20
+ import { removeScenario } from './slices/remove-scenario/index.js';
21
+ import { removeField } from './slices/remove-field/index.js';
22
+ import { showCompleteness } from './slices/show-completeness/index.js';
23
+ import { mapFields } from './slices/map-fields/index.js';
24
+ import { updateField } from './slices/update-field/index.js';
25
+ import { showAggregateCompleteness, listAggregates } from './slices/show-aggregate-completeness/index.js';
26
+ import { showActor, listActors } from './slices/show-actor/index.js';
18
27
  const args = process.argv.slice(2);
28
+ function getNamedArg(argList, ...names) {
29
+ for (let i = 0; i < argList.length; i++) {
30
+ if (names.includes(argList[i]) && i + 1 < argList.length) {
31
+ return argList[i + 1];
32
+ }
33
+ }
34
+ return undefined;
35
+ }
19
36
  function printHelp() {
20
37
  console.log(`
21
38
  eventmodeler - CLI tool for interacting with Event Model files
@@ -29,30 +46,57 @@ COMMANDS:
29
46
  list events List all events
30
47
  list commands List all commands
31
48
  list chapters List all chapters
49
+ list aggregates List all aggregates
50
+ list actors List all actors
32
51
 
33
52
  show slice <name> Show detailed XML view of a slice
34
53
  show event <name> Show detailed XML view of an event
35
54
  show command <name> Show detailed XML view of a command
36
55
  show chapter <name> Show chapter with its slices
56
+ show completeness <read-model> Show field mapping completeness status
57
+ show aggregate-completeness <name>
58
+ Show if events in aggregate have the ID field
59
+ show actor <name> Show actor with its screens
37
60
 
38
61
  search <term> Search for entities by name
39
62
 
40
63
  mark <slice-name> <status> Mark a slice's status
41
64
  Status: created | in-progress | blocked | done
42
65
 
66
+ add scenario --slice <name> --json|--xml <data>
67
+ Add a scenario to a slice
68
+ add field --command|--event|--read-model <name> --json|--xml <data>
69
+ Add a field to an entity
70
+
71
+ remove scenario <name> [--slice <name>]
72
+ Remove a scenario by name
73
+ remove field --command|--event|--read-model <name> --field <name>
74
+ Remove a field from an entity
75
+
76
+ map fields --flow <source→target> --json|--xml <mappings>
77
+ Set field mappings on a flow
78
+
79
+ update field --command|--event|--read-model <name> --field <name> [--optional true|false] [--generated true|false]
80
+ Update field properties
81
+
43
82
  summary Show model summary statistics
44
83
 
45
84
  export json Export entire model as JSON
46
85
 
47
86
  OPTIONS:
48
- -f, --file <path> Path to .eventmodeler file (default: auto-detect)
87
+ -f, --file <path> Path to .eventmodel file (default: auto-detect)
49
88
  -h, --help Show this help message
50
89
 
51
90
  EXAMPLES:
52
91
  eventmodeler list slices
53
92
  eventmodeler show slice "Place Order"
54
93
  eventmodeler mark "Place Order" done
55
- eventmodeler summary -f ./my-model.eventmodeler
94
+ eventmodeler add scenario --slice "Place Order" --json '{"name": "Happy path", "then": {"type": "events", "events": []}}'
95
+ eventmodeler add field --event "OrderPlaced" --json '{"name": "orderId", "type": "UUID"}'
96
+ eventmodeler remove field --event "OrderPlaced" --field "orderId"
97
+ eventmodeler show completeness "OrderSummary"
98
+ eventmodeler map fields --flow "OrderPlaced→OrderSummary" --json '[{"from": "total", "to": "totalAmount"}]'
99
+ eventmodeler update field --read-model "OrderSummary" --field "notes" --optional true
56
100
  `);
57
101
  }
58
102
  async function main() {
@@ -79,8 +123,8 @@ async function main() {
79
123
  }
80
124
  const filePath = fileArg ?? await findEventModelFile();
81
125
  if (!filePath) {
82
- console.error('Error: No .eventmodeler file found in current directory.');
83
- console.error('Use -f <path> to specify a file or run in a directory with an .eventmodeler file.');
126
+ console.error('Error: No .eventmodel file found in current directory.');
127
+ console.error('Use -f <path> to specify a file or run in a directory with an .eventmodel file.');
84
128
  process.exit(1);
85
129
  }
86
130
  if (!fs.existsSync(filePath)) {
@@ -103,9 +147,15 @@ async function main() {
103
147
  case 'chapters':
104
148
  listChapters(model);
105
149
  break;
150
+ case 'aggregates':
151
+ listAggregates(model);
152
+ break;
153
+ case 'actors':
154
+ listActors(model);
155
+ break;
106
156
  default:
107
157
  console.error(`Unknown list target: ${subcommand}`);
108
- console.error('Valid targets: slices, events, commands, chapters');
158
+ console.error('Valid targets: slices, events, commands, chapters, aggregates, actors');
109
159
  process.exit(1);
110
160
  }
111
161
  break;
@@ -139,9 +189,30 @@ async function main() {
139
189
  }
140
190
  showChapter(model, target);
141
191
  break;
192
+ case 'completeness':
193
+ if (!target) {
194
+ console.error('Usage: eventmodeler show completeness <read-model-name>');
195
+ process.exit(1);
196
+ }
197
+ showCompleteness(model, target);
198
+ break;
199
+ case 'aggregate-completeness':
200
+ if (!target) {
201
+ console.error('Usage: eventmodeler show aggregate-completeness <aggregate-name>');
202
+ process.exit(1);
203
+ }
204
+ showAggregateCompleteness(model, target);
205
+ break;
206
+ case 'actor':
207
+ if (!target) {
208
+ console.error('Usage: eventmodeler show actor <actor-name>');
209
+ process.exit(1);
210
+ }
211
+ showActor(model, target);
212
+ break;
142
213
  default:
143
214
  console.error(`Unknown show target: ${subcommand}`);
144
- console.error('Valid targets: slice, event, command, chapter');
215
+ console.error('Valid targets: slice, event, command, chapter, completeness, aggregate-completeness, actor');
145
216
  process.exit(1);
146
217
  }
147
218
  break;
@@ -174,6 +245,142 @@ async function main() {
174
245
  process.exit(1);
175
246
  }
176
247
  break;
248
+ case 'add':
249
+ switch (subcommand) {
250
+ case 'scenario': {
251
+ const sliceArg = getNamedArg(filteredArgs, '--slice');
252
+ const jsonArg = getNamedArg(filteredArgs, '--json');
253
+ const xmlArg = getNamedArg(filteredArgs, '--xml');
254
+ const inputData = jsonArg ?? xmlArg;
255
+ if (!sliceArg) {
256
+ console.error('Error: --slice is required');
257
+ console.error('Usage: eventmodeler add scenario --slice <name> --json|--xml <data>');
258
+ process.exit(1);
259
+ }
260
+ if (!inputData) {
261
+ console.error('Error: --json or --xml is required');
262
+ console.error('Usage: eventmodeler add scenario --slice <name> --json|--xml <data>');
263
+ process.exit(1);
264
+ }
265
+ addScenario(model, filePath, sliceArg, inputData);
266
+ break;
267
+ }
268
+ case 'field': {
269
+ const commandArg = getNamedArg(filteredArgs, '--command');
270
+ const eventArg = getNamedArg(filteredArgs, '--event');
271
+ const readModelArg = getNamedArg(filteredArgs, '--read-model');
272
+ const jsonArg = getNamedArg(filteredArgs, '--json');
273
+ const xmlArg = getNamedArg(filteredArgs, '--xml');
274
+ const inputData = jsonArg ?? xmlArg;
275
+ if (!inputData) {
276
+ console.error('Error: --json or --xml is required');
277
+ console.error('Usage: eventmodeler add field --command|--event|--read-model <name> --json|--xml <data>');
278
+ process.exit(1);
279
+ }
280
+ addField(model, filePath, { command: commandArg, event: eventArg, readModel: readModelArg }, inputData);
281
+ break;
282
+ }
283
+ default:
284
+ console.error(`Unknown add target: ${subcommand}`);
285
+ console.error('Valid targets: scenario, field');
286
+ process.exit(1);
287
+ }
288
+ break;
289
+ case 'remove':
290
+ switch (subcommand) {
291
+ case 'scenario': {
292
+ const scenarioName = target;
293
+ const sliceArg = getNamedArg(filteredArgs, '--slice');
294
+ if (!scenarioName) {
295
+ console.error('Usage: eventmodeler remove scenario <name> [--slice <slice-name>]');
296
+ process.exit(1);
297
+ }
298
+ removeScenario(model, filePath, scenarioName, sliceArg);
299
+ break;
300
+ }
301
+ case 'field': {
302
+ const commandArg = getNamedArg(filteredArgs, '--command');
303
+ const eventArg = getNamedArg(filteredArgs, '--event');
304
+ const readModelArg = getNamedArg(filteredArgs, '--read-model');
305
+ const fieldArg = getNamedArg(filteredArgs, '--field');
306
+ if (!fieldArg) {
307
+ console.error('Error: --field is required');
308
+ console.error('Usage: eventmodeler remove field --command|--event|--read-model <name> --field <field-name>');
309
+ process.exit(1);
310
+ }
311
+ removeField(model, filePath, { command: commandArg, event: eventArg, readModel: readModelArg }, fieldArg);
312
+ break;
313
+ }
314
+ default:
315
+ console.error(`Unknown remove target: ${subcommand}`);
316
+ console.error('Valid targets: scenario, field');
317
+ process.exit(1);
318
+ }
319
+ break;
320
+ case 'map':
321
+ switch (subcommand) {
322
+ case 'fields': {
323
+ const flowArg = getNamedArg(filteredArgs, '--flow');
324
+ const jsonArg = getNamedArg(filteredArgs, '--json');
325
+ const xmlArg = getNamedArg(filteredArgs, '--xml');
326
+ const inputData = jsonArg ?? xmlArg;
327
+ if (!flowArg) {
328
+ console.error('Error: --flow is required');
329
+ console.error('Usage: eventmodeler map fields --flow <source→target> --json|--xml <mappings>');
330
+ process.exit(1);
331
+ }
332
+ if (!inputData) {
333
+ console.error('Error: --json or --xml is required');
334
+ console.error('Usage: eventmodeler map fields --flow <source→target> --json|--xml <mappings>');
335
+ process.exit(1);
336
+ }
337
+ mapFields(model, filePath, flowArg, inputData);
338
+ break;
339
+ }
340
+ default:
341
+ console.error(`Unknown map target: ${subcommand}`);
342
+ console.error('Valid targets: fields');
343
+ process.exit(1);
344
+ }
345
+ break;
346
+ case 'update':
347
+ switch (subcommand) {
348
+ case 'field': {
349
+ const commandArg = getNamedArg(filteredArgs, '--command');
350
+ const eventArg = getNamedArg(filteredArgs, '--event');
351
+ const readModelArg = getNamedArg(filteredArgs, '--read-model');
352
+ const fieldArg = getNamedArg(filteredArgs, '--field');
353
+ const optionalArg = getNamedArg(filteredArgs, '--optional');
354
+ const generatedArg = getNamedArg(filteredArgs, '--generated');
355
+ const typeArg = getNamedArg(filteredArgs, '--type');
356
+ if (!fieldArg) {
357
+ console.error('Error: --field is required');
358
+ console.error('Usage: eventmodeler update field --command|--event|--read-model <name> --field <field-name> [--optional true|false] [--generated true|false]');
359
+ process.exit(1);
360
+ }
361
+ const updates = {};
362
+ if (optionalArg !== undefined) {
363
+ updates.optional = optionalArg === 'true';
364
+ }
365
+ if (generatedArg !== undefined) {
366
+ updates.generated = generatedArg === 'true';
367
+ }
368
+ if (typeArg !== undefined) {
369
+ updates.type = typeArg;
370
+ }
371
+ if (Object.keys(updates).length === 0) {
372
+ console.error('Error: Must specify at least one update (--optional, --generated, or --type)');
373
+ process.exit(1);
374
+ }
375
+ updateField(model, filePath, { command: commandArg, event: eventArg, readModel: readModelArg }, fieldArg, updates);
376
+ break;
377
+ }
378
+ default:
379
+ console.error(`Unknown update target: ${subcommand}`);
380
+ console.error('Valid targets: field');
381
+ process.exit(1);
382
+ }
383
+ break;
177
384
  default:
178
385
  console.error(`Unknown command: ${command}`);
179
386
  printHelp();
@@ -11,6 +11,8 @@ export function createEmptyModel() {
11
11
  chapters: new Map(),
12
12
  scenarios: new Map(),
13
13
  flows: new Map(),
14
+ aggregates: new Map(),
15
+ actors: new Map(),
14
16
  };
15
17
  }
16
18
  export function projectEvents(rawEvents) {
@@ -143,6 +145,7 @@ function applyEvent(model, event) {
143
145
  width: event.width,
144
146
  height: event.height,
145
147
  canonicalId: event.canonicalId,
148
+ originalNodeId: event.sourceEventStickyId,
146
149
  });
147
150
  // Also set canonical on source if not already set
148
151
  if (source && !source.canonicalId) {
@@ -217,6 +220,7 @@ function applyEvent(model, event) {
217
220
  width: event.width,
218
221
  height: event.height,
219
222
  canonicalId: event.canonicalId,
223
+ originalNodeId: event.sourceReadModelStickyId,
220
224
  });
221
225
  if (source && !source.canonicalId) {
222
226
  source.canonicalId = event.canonicalId;
@@ -290,6 +294,7 @@ function applyEvent(model, event) {
290
294
  width: event.width,
291
295
  height: event.height,
292
296
  canonicalId: event.canonicalId,
297
+ originalNodeId: event.sourceScreenId,
293
298
  });
294
299
  if (source && !source.canonicalId) {
295
300
  source.canonicalId = event.canonicalId;
@@ -462,6 +467,102 @@ function applyEvent(model, event) {
462
467
  nodeIds: [],
463
468
  });
464
469
  break;
470
+ // Aggregate events
471
+ case 'AggregatePlaced':
472
+ model.aggregates.set(event.aggregateId, {
473
+ id: event.aggregateId,
474
+ name: event.name,
475
+ position: event.position,
476
+ size: event.size,
477
+ eventIds: [],
478
+ aggregateIdFieldName: event.aggregateIdFieldName,
479
+ aggregateIdFieldType: event.aggregateIdFieldType,
480
+ });
481
+ break;
482
+ case 'EventsGroupedIntoAggregate':
483
+ model.aggregates.set(event.aggregateId, {
484
+ id: event.aggregateId,
485
+ name: event.name,
486
+ position: event.position,
487
+ size: event.size,
488
+ eventIds: event.eventIds,
489
+ aggregateIdFieldName: event.aggregateIdFieldName,
490
+ aggregateIdFieldType: event.aggregateIdFieldType,
491
+ });
492
+ break;
493
+ case 'AggregateMoved': {
494
+ const aggregate = model.aggregates.get(event.aggregateId);
495
+ if (aggregate)
496
+ aggregate.position = event.position;
497
+ break;
498
+ }
499
+ case 'AggregateResized': {
500
+ const aggregate = model.aggregates.get(event.aggregateId);
501
+ if (aggregate) {
502
+ aggregate.position = event.position;
503
+ aggregate.size = event.size;
504
+ }
505
+ break;
506
+ }
507
+ case 'AggregateRemoved':
508
+ model.aggregates.delete(event.aggregateId);
509
+ break;
510
+ case 'AggregateRenamed': {
511
+ const aggregate = model.aggregates.get(event.aggregateId);
512
+ if (aggregate)
513
+ aggregate.name = event.name;
514
+ break;
515
+ }
516
+ case 'AggregateIdFieldSet': {
517
+ const aggregate = model.aggregates.get(event.aggregateId);
518
+ if (aggregate) {
519
+ aggregate.aggregateIdFieldName = event.aggregateIdFieldName;
520
+ aggregate.aggregateIdFieldType = event.aggregateIdFieldType;
521
+ }
522
+ break;
523
+ }
524
+ // Actor events
525
+ case 'ActorPlaced':
526
+ model.actors.set(event.actorId, {
527
+ id: event.actorId,
528
+ name: event.name,
529
+ position: event.position,
530
+ size: event.size,
531
+ screenIds: [],
532
+ });
533
+ break;
534
+ case 'ScreensGroupedIntoActor':
535
+ model.actors.set(event.actorId, {
536
+ id: event.actorId,
537
+ name: event.name,
538
+ position: event.position,
539
+ size: event.size,
540
+ screenIds: event.screenIds,
541
+ });
542
+ break;
543
+ case 'ActorMoved': {
544
+ const actor = model.actors.get(event.actorId);
545
+ if (actor)
546
+ actor.position = event.position;
547
+ break;
548
+ }
549
+ case 'ActorResized': {
550
+ const actor = model.actors.get(event.actorId);
551
+ if (actor) {
552
+ actor.position = event.position;
553
+ actor.size = event.size;
554
+ }
555
+ break;
556
+ }
557
+ case 'ActorRemoved':
558
+ model.actors.delete(event.actorId);
559
+ break;
560
+ case 'ActorRenamed': {
561
+ const actor = model.actors.get(event.actorId);
562
+ if (actor)
563
+ actor.name = event.name;
564
+ break;
565
+ }
465
566
  // Chapter events
466
567
  case 'ChapterPlaced':
467
568
  model.chapters.set(event.chapterId, {
@@ -0,0 +1,6 @@
1
+ import type { EventModel } from '../../types.js';
2
+ export declare function addField(model: EventModel, filePath: string, options: {
3
+ command?: string;
4
+ event?: string;
5
+ readModel?: string;
6
+ }, input: string): void;
@@ -0,0 +1,182 @@
1
+ import * as crypto from 'node:crypto';
2
+ import { appendEvent } from '../../lib/file-loader.js';
3
+ const validFieldTypes = ['UUID', 'Boolean', 'Double', 'Decimal', 'Date', 'DateTime', 'Long', 'Int', 'String', 'Custom'];
4
+ function parseJsonInput(input) {
5
+ return JSON.parse(input);
6
+ }
7
+ function parseXmlInput(input) {
8
+ const getAttr = (tag, attr) => {
9
+ const match = tag.match(new RegExp(`${attr}="([^"]*)"`));
10
+ return match ? match[1] : undefined;
11
+ };
12
+ const getBoolAttr = (tag, attr) => {
13
+ const value = getAttr(tag, attr);
14
+ if (value === 'true')
15
+ return true;
16
+ if (value === 'false')
17
+ return false;
18
+ return undefined;
19
+ };
20
+ const fieldMatch = input.match(/<field([^>]*?)(?:\/>|>([\s\S]*?)<\/field>)/);
21
+ if (!fieldMatch) {
22
+ throw new Error('Invalid XML: missing <field> tag');
23
+ }
24
+ const name = getAttr(fieldMatch[1], 'name');
25
+ if (!name) {
26
+ throw new Error('Invalid XML: field must have a name attribute');
27
+ }
28
+ const type = getAttr(fieldMatch[1], 'type');
29
+ if (!type) {
30
+ throw new Error('Invalid XML: field must have a type attribute');
31
+ }
32
+ const fieldInput = {
33
+ name,
34
+ type,
35
+ isList: getBoolAttr(fieldMatch[1], 'isList'),
36
+ isGenerated: getBoolAttr(fieldMatch[1], 'isGenerated'),
37
+ isOptional: getBoolAttr(fieldMatch[1], 'isOptional'),
38
+ };
39
+ // Parse nested subfields for Custom type
40
+ if (type === 'Custom' && fieldMatch[2]) {
41
+ fieldInput.subfields = [];
42
+ const subfieldMatches = fieldMatch[2].matchAll(/<field([^>]*?)(?:\/>|>([\s\S]*?)<\/field>)/g);
43
+ for (const match of subfieldMatches) {
44
+ const subfieldXml = match[0];
45
+ fieldInput.subfields.push(parseXmlInput(subfieldXml));
46
+ }
47
+ }
48
+ return fieldInput;
49
+ }
50
+ function parseInput(input) {
51
+ const trimmed = input.trim();
52
+ if (trimmed.startsWith('<')) {
53
+ return parseXmlInput(trimmed);
54
+ }
55
+ return parseJsonInput(trimmed);
56
+ }
57
+ function createFieldFromInput(input) {
58
+ return {
59
+ id: crypto.randomUUID(),
60
+ name: input.name,
61
+ fieldType: input.type,
62
+ isList: input.isList ?? false,
63
+ isGenerated: input.isGenerated ?? false,
64
+ isOptional: input.isOptional,
65
+ subfields: input.subfields?.map(createFieldFromInput),
66
+ };
67
+ }
68
+ export function addField(model, filePath, options, input) {
69
+ // Determine which entity type
70
+ const entityCount = [options.command, options.event, options.readModel].filter(Boolean).length;
71
+ if (entityCount === 0) {
72
+ console.error('Error: Must specify one of --command, --event, or --read-model');
73
+ process.exit(1);
74
+ }
75
+ if (entityCount > 1) {
76
+ console.error('Error: Can only specify one of --command, --event, or --read-model');
77
+ process.exit(1);
78
+ }
79
+ // Parse input
80
+ let fieldInput;
81
+ try {
82
+ fieldInput = parseInput(input);
83
+ }
84
+ catch (err) {
85
+ console.error(`Error: Invalid input format: ${err.message}`);
86
+ process.exit(1);
87
+ }
88
+ // Validate field type
89
+ if (!validFieldTypes.includes(fieldInput.type)) {
90
+ console.error(`Error: Invalid field type: ${fieldInput.type}`);
91
+ console.error(`Valid types: ${validFieldTypes.join(', ')}`);
92
+ process.exit(1);
93
+ }
94
+ // Find entity and add field
95
+ if (options.command) {
96
+ addFieldToCommand(model, filePath, options.command, fieldInput);
97
+ }
98
+ else if (options.event) {
99
+ addFieldToEvent(model, filePath, options.event, fieldInput);
100
+ }
101
+ else if (options.readModel) {
102
+ addFieldToReadModel(model, filePath, options.readModel, fieldInput);
103
+ }
104
+ }
105
+ 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
+ }
117
+ // Check for duplicate field name
118
+ if (command.fields.some(f => f.name.toLowerCase() === fieldInput.name.toLowerCase())) {
119
+ console.error(`Error: Field "${fieldInput.name}" already exists on command "${command.name}"`);
120
+ process.exit(1);
121
+ }
122
+ const field = createFieldFromInput(fieldInput);
123
+ appendEvent(filePath, {
124
+ type: 'CommandFieldAdded',
125
+ commandStickyId: command.id,
126
+ field,
127
+ timestamp: Date.now(),
128
+ });
129
+ console.log(`Added field "${field.name}" to command "${command.name}"`);
130
+ }
131
+ 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
+ }
143
+ // Check for duplicate field name
144
+ if (event.fields.some(f => f.name.toLowerCase() === fieldInput.name.toLowerCase())) {
145
+ console.error(`Error: Field "${fieldInput.name}" already exists on event "${event.name}"`);
146
+ process.exit(1);
147
+ }
148
+ const field = createFieldFromInput(fieldInput);
149
+ appendEvent(filePath, {
150
+ type: 'EventFieldAdded',
151
+ eventStickyId: event.id,
152
+ field,
153
+ timestamp: Date.now(),
154
+ });
155
+ console.log(`Added field "${field.name}" to event "${event.name}"`);
156
+ }
157
+ 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
+ }
169
+ // Check for duplicate field name
170
+ if (readModel.fields.some(f => f.name.toLowerCase() === fieldInput.name.toLowerCase())) {
171
+ console.error(`Error: Field "${fieldInput.name}" already exists on read model "${readModel.name}"`);
172
+ process.exit(1);
173
+ }
174
+ const field = createFieldFromInput(fieldInput);
175
+ appendEvent(filePath, {
176
+ type: 'ReadModelFieldAdded',
177
+ readModelStickyId: readModel.id,
178
+ field,
179
+ timestamp: Date.now(),
180
+ });
181
+ console.log(`Added field "${field.name}" to read model "${readModel.name}"`);
182
+ }
@@ -0,0 +1,2 @@
1
+ import type { EventModel } from '../../types.js';
2
+ export declare function addScenario(model: EventModel, filePath: string, sliceName: string, input: string): void;