eventmodeler 0.1.0 → 0.2.1
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 +213 -6
- package/dist/projection.js +101 -0
- package/dist/slices/add-field/index.d.ts +6 -0
- package/dist/slices/add-field/index.js +182 -0
- package/dist/slices/add-scenario/index.d.ts +2 -0
- package/dist/slices/add-scenario/index.js +339 -0
- package/dist/slices/export-eventmodel-to-json/index.js +62 -3
- package/dist/slices/list-events/index.js +58 -2
- package/dist/slices/map-fields/index.d.ts +2 -0
- package/dist/slices/map-fields/index.js +216 -0
- package/dist/slices/remove-field/index.d.ts +6 -0
- package/dist/slices/remove-field/index.js +127 -0
- package/dist/slices/remove-scenario/index.d.ts +2 -0
- package/dist/slices/remove-scenario/index.js +39 -0
- package/dist/slices/show-actor/index.d.ts +3 -0
- package/dist/slices/show-actor/index.js +89 -0
- package/dist/slices/show-aggregate-completeness/index.d.ts +3 -0
- package/dist/slices/show-aggregate-completeness/index.js +139 -0
- package/dist/slices/show-command/index.js +22 -2
- package/dist/slices/show-completeness/index.d.ts +2 -0
- package/dist/slices/show-completeness/index.js +180 -0
- package/dist/slices/show-event/index.js +20 -1
- package/dist/slices/show-model-summary/index.js +2 -0
- package/dist/slices/show-slice/index.js +86 -3
- package/dist/slices/update-field/index.d.ts +12 -0
- package/dist/slices/update-field/index.js +166 -0
- package/dist/types.d.ts +33 -0
- package/package.json +1 -1
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 .
|
|
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
|
|
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 .
|
|
83
|
-
console.error('Use -f <path> to specify a file or run in a directory with an .
|
|
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();
|
package/dist/projection.js
CHANGED
|
@@ -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,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
|
+
}
|