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 +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 +144 -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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
function escapeXml(str) {
|
|
2
|
+
return str
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"');
|
|
7
|
+
}
|
|
8
|
+
function flattenFields(fields, prefix = '') {
|
|
9
|
+
const result = [];
|
|
10
|
+
for (const field of fields) {
|
|
11
|
+
const path = prefix ? `${prefix}.${field.name}` : field.name;
|
|
12
|
+
result.push({ id: field.id, path, field });
|
|
13
|
+
if (field.fieldType === 'Custom' && field.subfields) {
|
|
14
|
+
result.push(...flattenFields(field.subfields, path));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
function calculateFlowCompleteness(model, flow, targetFields, sourceFields) {
|
|
20
|
+
const flatSource = flattenFields(sourceFields);
|
|
21
|
+
const flatTarget = flattenFields(targetFields);
|
|
22
|
+
const manualMappings = flow.fieldMappings ?? [];
|
|
23
|
+
// Get source entity name
|
|
24
|
+
let sourceName = 'Unknown';
|
|
25
|
+
let sourceType = 'unknown';
|
|
26
|
+
const event = model.events.get(flow.sourceId);
|
|
27
|
+
const readModel = model.readModels.get(flow.sourceId);
|
|
28
|
+
const command = model.commands.get(flow.sourceId);
|
|
29
|
+
const screen = model.screens.get(flow.sourceId);
|
|
30
|
+
const processor = model.processors.get(flow.sourceId);
|
|
31
|
+
if (event) {
|
|
32
|
+
sourceName = event.name;
|
|
33
|
+
sourceType = 'event';
|
|
34
|
+
}
|
|
35
|
+
else if (readModel) {
|
|
36
|
+
sourceName = readModel.name;
|
|
37
|
+
sourceType = 'read-model';
|
|
38
|
+
}
|
|
39
|
+
else if (command) {
|
|
40
|
+
sourceName = command.name;
|
|
41
|
+
sourceType = 'command';
|
|
42
|
+
}
|
|
43
|
+
else if (screen) {
|
|
44
|
+
sourceName = screen.name;
|
|
45
|
+
sourceType = 'screen';
|
|
46
|
+
}
|
|
47
|
+
else if (processor) {
|
|
48
|
+
sourceName = processor.name;
|
|
49
|
+
sourceType = 'processor';
|
|
50
|
+
}
|
|
51
|
+
const fieldStatuses = flatTarget.map(({ id, path, field }) => {
|
|
52
|
+
// 1. Check if generated
|
|
53
|
+
if (field.isGenerated) {
|
|
54
|
+
return { fieldId: id, fieldName: path, status: 'generated' };
|
|
55
|
+
}
|
|
56
|
+
// 2. Check if user input
|
|
57
|
+
if (field.isUserInput) {
|
|
58
|
+
return { fieldId: id, fieldName: path, status: 'user-input' };
|
|
59
|
+
}
|
|
60
|
+
// 3. Check manual mapping
|
|
61
|
+
const manualMapping = manualMappings.find(m => m.targetFieldId === id);
|
|
62
|
+
if (manualMapping) {
|
|
63
|
+
const sourceField = flatSource.find(sf => sf.id === manualMapping.sourceFieldId);
|
|
64
|
+
return {
|
|
65
|
+
fieldId: id,
|
|
66
|
+
fieldName: path,
|
|
67
|
+
status: 'satisfied',
|
|
68
|
+
sourceFieldId: manualMapping.sourceFieldId,
|
|
69
|
+
sourceFieldName: sourceField?.path,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// 4. Check auto-mapping by path
|
|
73
|
+
const autoMatch = flatSource.find(sf => sf.path === path);
|
|
74
|
+
if (autoMatch) {
|
|
75
|
+
return {
|
|
76
|
+
fieldId: id,
|
|
77
|
+
fieldName: path,
|
|
78
|
+
status: 'satisfied',
|
|
79
|
+
sourceFieldId: autoMatch.id,
|
|
80
|
+
sourceFieldName: autoMatch.path,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// 5. Not satisfied - check if optional
|
|
84
|
+
if (field.isOptional) {
|
|
85
|
+
return { fieldId: id, fieldName: path, status: 'optional-missing' };
|
|
86
|
+
}
|
|
87
|
+
// 6. Unsatisfied required field
|
|
88
|
+
return { fieldId: id, fieldName: path, status: 'unsatisfied' };
|
|
89
|
+
});
|
|
90
|
+
const hasUnsatisfied = fieldStatuses.some(s => s.status === 'unsatisfied');
|
|
91
|
+
const hasOptionalMissing = fieldStatuses.some(s => s.status === 'optional-missing');
|
|
92
|
+
return {
|
|
93
|
+
flowId: flow.id,
|
|
94
|
+
sourceName,
|
|
95
|
+
sourceType,
|
|
96
|
+
isComplete: !hasUnsatisfied,
|
|
97
|
+
hasWarnings: hasOptionalMissing,
|
|
98
|
+
targetFields: fieldStatuses,
|
|
99
|
+
sourceFields: flatSource.map(sf => ({
|
|
100
|
+
id: sf.id,
|
|
101
|
+
name: sf.path,
|
|
102
|
+
type: sf.field.fieldType,
|
|
103
|
+
isList: sf.field.isList,
|
|
104
|
+
})),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function showCompleteness(model, readModelName) {
|
|
108
|
+
// Find the read model
|
|
109
|
+
const nameLower = readModelName.toLowerCase();
|
|
110
|
+
const readModels = [...model.readModels.values()];
|
|
111
|
+
const readModel = readModels.find(rm => rm.name.toLowerCase() === nameLower || rm.name.toLowerCase().includes(nameLower));
|
|
112
|
+
if (!readModel) {
|
|
113
|
+
console.error(`Error: Read model not found: ${readModelName}`);
|
|
114
|
+
console.error('Available read models:');
|
|
115
|
+
for (const rm of readModels) {
|
|
116
|
+
console.error(` - ${rm.name}`);
|
|
117
|
+
}
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
// Find all flows into this read model (EventToReadModel flows)
|
|
121
|
+
const incomingFlows = [...model.flows.values()].filter(f => f.targetId === readModel.id && f.flowType === 'EventToReadModel');
|
|
122
|
+
if (incomingFlows.length === 0) {
|
|
123
|
+
console.log(`<completeness readModel="${escapeXml(readModel.name)}">`);
|
|
124
|
+
console.log(' <no-flows>No events flow into this read model</no-flows>');
|
|
125
|
+
console.log('</completeness>');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Calculate completeness for each flow
|
|
129
|
+
const completenessResults = [];
|
|
130
|
+
for (const flow of incomingFlows) {
|
|
131
|
+
const sourceEvent = model.events.get(flow.sourceId);
|
|
132
|
+
if (!sourceEvent)
|
|
133
|
+
continue;
|
|
134
|
+
const result = calculateFlowCompleteness(model, flow, readModel.fields, sourceEvent.fields);
|
|
135
|
+
completenessResults.push(result);
|
|
136
|
+
}
|
|
137
|
+
// Output XML
|
|
138
|
+
const overallComplete = completenessResults.every(r => r.isComplete);
|
|
139
|
+
const overallStatus = overallComplete ? 'complete' : 'incomplete';
|
|
140
|
+
console.log(`<completeness readModel="${escapeXml(readModel.name)}" status="${overallStatus}">`);
|
|
141
|
+
for (const result of completenessResults) {
|
|
142
|
+
const flowStatus = result.isComplete ? 'complete' : 'incomplete';
|
|
143
|
+
const warningAttr = result.hasWarnings ? ' hasWarnings="true"' : '';
|
|
144
|
+
console.log(` <flow id="${result.flowId}" from="${escapeXml(result.sourceName)}" type="${result.sourceType}" status="${flowStatus}"${warningAttr}>`);
|
|
145
|
+
// Target fields
|
|
146
|
+
console.log(' <target-fields>');
|
|
147
|
+
for (const field of result.targetFields) {
|
|
148
|
+
if (field.sourceFieldName) {
|
|
149
|
+
console.log(` <field name="${escapeXml(field.fieldName)}" status="${field.status}" source="${escapeXml(field.sourceFieldName)}"/>`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
console.log(` <field name="${escapeXml(field.fieldName)}" status="${field.status}"/>`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
console.log(' </target-fields>');
|
|
156
|
+
// Available source fields (for AI to see what's available)
|
|
157
|
+
console.log(' <available-source-fields>');
|
|
158
|
+
for (const sf of result.sourceFields) {
|
|
159
|
+
const listAttr = sf.isList ? ' isList="true"' : '';
|
|
160
|
+
console.log(` <field id="${sf.id}" name="${escapeXml(sf.name)}" type="${sf.type}"${listAttr}/>`);
|
|
161
|
+
}
|
|
162
|
+
console.log(' </available-source-fields>');
|
|
163
|
+
console.log(' </flow>');
|
|
164
|
+
}
|
|
165
|
+
// Also show the read model's fields with their IDs (needed for mapping)
|
|
166
|
+
console.log(' <read-model-fields>');
|
|
167
|
+
const flatTargetFields = flattenFields(readModel.fields);
|
|
168
|
+
for (const { id, path, field } of flatTargetFields) {
|
|
169
|
+
const attrs = [`id="${id}"`, `name="${escapeXml(path)}"`, `type="${field.fieldType}"`];
|
|
170
|
+
if (field.isList)
|
|
171
|
+
attrs.push('isList="true"');
|
|
172
|
+
if (field.isOptional)
|
|
173
|
+
attrs.push('isOptional="true"');
|
|
174
|
+
if (field.isGenerated)
|
|
175
|
+
attrs.push('isGenerated="true"');
|
|
176
|
+
console.log(` <field ${attrs.join(' ')}/>`);
|
|
177
|
+
}
|
|
178
|
+
console.log(' </read-model-fields>');
|
|
179
|
+
console.log('</completeness>');
|
|
180
|
+
}
|
|
@@ -27,8 +27,27 @@ function formatFieldXml(field, indent) {
|
|
|
27
27
|
}
|
|
28
28
|
return `${indent}<field ${attrs.join(' ')}/>\n`;
|
|
29
29
|
}
|
|
30
|
+
// Find which aggregate an event belongs to (center point inside aggregate bounds)
|
|
31
|
+
function findAggregateForEvent(model, event) {
|
|
32
|
+
const centerX = event.position.x + event.width / 2;
|
|
33
|
+
const centerY = event.position.y + event.height / 2;
|
|
34
|
+
for (const aggregate of model.aggregates.values()) {
|
|
35
|
+
const bounds = {
|
|
36
|
+
left: aggregate.position.x,
|
|
37
|
+
right: aggregate.position.x + aggregate.size.width,
|
|
38
|
+
top: aggregate.position.y,
|
|
39
|
+
bottom: aggregate.position.y + aggregate.size.height,
|
|
40
|
+
};
|
|
41
|
+
if (centerX >= bounds.left && centerX <= bounds.right && centerY >= bounds.top && centerY <= bounds.bottom) {
|
|
42
|
+
return aggregate;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
30
47
|
function formatEventXml(model, event) {
|
|
31
|
-
|
|
48
|
+
const aggregate = findAggregateForEvent(model, event);
|
|
49
|
+
const aggregateAttr = aggregate ? ` aggregate="${escapeXml(aggregate.name)}"` : '';
|
|
50
|
+
let xml = `<event name="${escapeXml(event.name)}"${aggregateAttr}>\n`;
|
|
32
51
|
if (event.fields.length > 0) {
|
|
33
52
|
xml += ' <fields>\n';
|
|
34
53
|
for (const field of event.fields) {
|
|
@@ -14,6 +14,8 @@ export function showModelSummary(model) {
|
|
|
14
14
|
console.log(` <read-models count="${model.readModels.size}"/>`);
|
|
15
15
|
console.log(` <screens count="${model.screens.size}"/>`);
|
|
16
16
|
console.log(` <processors count="${model.processors.size}"/>`);
|
|
17
|
+
console.log(` <aggregates count="${model.aggregates.size}"/>`);
|
|
18
|
+
console.log(` <actors count="${model.actors.size}"/>`);
|
|
17
19
|
console.log(` <scenarios count="${model.scenarios.size}"/>`);
|
|
18
20
|
console.log(` <flows count="${model.flows.size}"/>`);
|
|
19
21
|
console.log('</model>');
|
|
@@ -50,6 +50,8 @@ function getSliceComponents(model, slice) {
|
|
|
50
50
|
const centerY = pos.y + height / 2;
|
|
51
51
|
return centerX >= bounds.left && centerX <= bounds.right && centerY >= bounds.top && centerY <= bounds.bottom;
|
|
52
52
|
}
|
|
53
|
+
// Include all elements in the slice (including linked copies)
|
|
54
|
+
// Linked copies will be marked as such in the output
|
|
53
55
|
return {
|
|
54
56
|
commands: [...model.commands.values()].filter(c => isInSlice(c.position, c.width, c.height)),
|
|
55
57
|
events: [...model.events.values()].filter(e => isInSlice(e.position, e.width, e.height)),
|
|
@@ -58,9 +60,100 @@ function getSliceComponents(model, slice) {
|
|
|
58
60
|
processors: [...model.processors.values()].filter(p => isInSlice(p.position, p.width, p.height)),
|
|
59
61
|
};
|
|
60
62
|
}
|
|
63
|
+
// Find which slice contains the original of a linked copy
|
|
64
|
+
function findSliceForNode(model, nodeId) {
|
|
65
|
+
const event = model.events.get(nodeId);
|
|
66
|
+
const readModel = model.readModels.get(nodeId);
|
|
67
|
+
const screen = model.screens.get(nodeId);
|
|
68
|
+
const node = event ?? readModel ?? screen;
|
|
69
|
+
if (!node)
|
|
70
|
+
return null;
|
|
71
|
+
for (const slice of model.slices.values()) {
|
|
72
|
+
const centerX = node.position.x + node.width / 2;
|
|
73
|
+
const centerY = node.position.y + node.height / 2;
|
|
74
|
+
if (centerX >= slice.position.x &&
|
|
75
|
+
centerX <= slice.position.x + slice.size.width &&
|
|
76
|
+
centerY >= slice.position.y &&
|
|
77
|
+
centerY <= slice.position.y + slice.size.height) {
|
|
78
|
+
return slice;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
// Find which aggregate an event belongs to (center point inside aggregate bounds)
|
|
84
|
+
function findAggregateForEvent(model, event) {
|
|
85
|
+
const centerX = event.position.x + event.width / 2;
|
|
86
|
+
const centerY = event.position.y + event.height / 2;
|
|
87
|
+
for (const aggregate of model.aggregates.values()) {
|
|
88
|
+
if (centerX >= aggregate.position.x &&
|
|
89
|
+
centerX <= aggregate.position.x + aggregate.size.width &&
|
|
90
|
+
centerY >= aggregate.position.y &&
|
|
91
|
+
centerY <= aggregate.position.y + aggregate.size.height) {
|
|
92
|
+
return aggregate;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
// Find which actor a screen belongs to (center point inside actor bounds)
|
|
98
|
+
function findActorForScreen(model, screen) {
|
|
99
|
+
const centerX = screen.position.x + screen.width / 2;
|
|
100
|
+
const centerY = screen.position.y + screen.height / 2;
|
|
101
|
+
for (const actor of model.actors.values()) {
|
|
102
|
+
if (centerX >= actor.position.x &&
|
|
103
|
+
centerX <= actor.position.x + actor.size.width &&
|
|
104
|
+
centerY >= actor.position.y &&
|
|
105
|
+
centerY <= actor.position.y + actor.size.height) {
|
|
106
|
+
return actor;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
// Check if two rectangles overlap
|
|
112
|
+
function rectanglesOverlap(r1, r2) {
|
|
113
|
+
return (r1.x < r2.x + r2.width &&
|
|
114
|
+
r1.x + r1.width > r2.x &&
|
|
115
|
+
r1.y < r2.y + r2.height &&
|
|
116
|
+
r1.y + r1.height > r2.y);
|
|
117
|
+
}
|
|
118
|
+
function getAggregatesForSlice(model, slice) {
|
|
119
|
+
const sliceRect = {
|
|
120
|
+
x: slice.position.x,
|
|
121
|
+
y: slice.position.y,
|
|
122
|
+
width: slice.size.width,
|
|
123
|
+
height: slice.size.height,
|
|
124
|
+
};
|
|
125
|
+
return [...model.aggregates.values()].filter(aggregate => {
|
|
126
|
+
const aggRect = {
|
|
127
|
+
x: aggregate.position.x,
|
|
128
|
+
y: aggregate.position.y,
|
|
129
|
+
width: aggregate.size.width,
|
|
130
|
+
height: aggregate.size.height,
|
|
131
|
+
};
|
|
132
|
+
return rectanglesOverlap(sliceRect, aggRect);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function getActorsForSlice(model, slice) {
|
|
136
|
+
const sliceRect = {
|
|
137
|
+
x: slice.position.x,
|
|
138
|
+
y: slice.position.y,
|
|
139
|
+
width: slice.size.width,
|
|
140
|
+
height: slice.size.height,
|
|
141
|
+
};
|
|
142
|
+
return [...model.actors.values()].filter(actor => {
|
|
143
|
+
const actorRect = {
|
|
144
|
+
x: actor.position.x,
|
|
145
|
+
y: actor.position.y,
|
|
146
|
+
width: actor.size.width,
|
|
147
|
+
height: actor.size.height,
|
|
148
|
+
};
|
|
149
|
+
return rectanglesOverlap(sliceRect, actorRect);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
61
152
|
function formatSliceXml(model, slice) {
|
|
62
153
|
const components = getSliceComponents(model, slice);
|
|
63
154
|
const scenarios = [...model.scenarios.values()].filter(s => s.sliceId === slice.id);
|
|
155
|
+
const aggregates = getAggregatesForSlice(model, slice);
|
|
156
|
+
const actors = getActorsForSlice(model, slice);
|
|
64
157
|
const componentIds = new Set();
|
|
65
158
|
components.commands.forEach(c => componentIds.add(c.id));
|
|
66
159
|
components.events.forEach(e => componentIds.add(e.id));
|
|
@@ -80,7 +173,19 @@ function formatSliceXml(model, slice) {
|
|
|
80
173
|
let xml = `<slice name="${escapeXml(slice.name)}" status="${slice.status}">\n`;
|
|
81
174
|
xml += ' <components>\n';
|
|
82
175
|
for (const screen of components.screens) {
|
|
83
|
-
|
|
176
|
+
// Check if this is a linked copy
|
|
177
|
+
const copyAttr = screen.originalNodeId ? ' linked-copy="true"' : '';
|
|
178
|
+
let originAttr = '';
|
|
179
|
+
if (screen.originalNodeId) {
|
|
180
|
+
const originSlice = findSliceForNode(model, screen.originalNodeId);
|
|
181
|
+
if (originSlice) {
|
|
182
|
+
originAttr = ` origin-slice="${escapeXml(originSlice.name)}"`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Check which actor this screen belongs to
|
|
186
|
+
const actor = findActorForScreen(model, screen);
|
|
187
|
+
const actorAttr = actor ? ` actor="${escapeXml(actor.name)}"` : '';
|
|
188
|
+
xml += ` <screen name="${escapeXml(screen.name)}"${copyAttr}${originAttr}${actorAttr}>\n`;
|
|
84
189
|
if (screen.fields.length > 0) {
|
|
85
190
|
xml += ' <fields>\n';
|
|
86
191
|
for (const field of screen.fields) {
|
|
@@ -110,7 +215,19 @@ function formatSliceXml(model, slice) {
|
|
|
110
215
|
xml += ' </command>\n';
|
|
111
216
|
}
|
|
112
217
|
for (const event of components.events) {
|
|
113
|
-
|
|
218
|
+
// Check if this is a linked copy
|
|
219
|
+
const copyAttr = event.originalNodeId ? ' linked-copy="true"' : '';
|
|
220
|
+
let originAttr = '';
|
|
221
|
+
if (event.originalNodeId) {
|
|
222
|
+
const originSlice = findSliceForNode(model, event.originalNodeId);
|
|
223
|
+
if (originSlice) {
|
|
224
|
+
originAttr = ` origin-slice="${escapeXml(originSlice.name)}"`;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Check which aggregate this event belongs to
|
|
228
|
+
const aggregate = findAggregateForEvent(model, event);
|
|
229
|
+
const aggregateAttr = aggregate ? ` aggregate="${escapeXml(aggregate.name)}"` : '';
|
|
230
|
+
xml += ` <event name="${escapeXml(event.name)}"${copyAttr}${originAttr}${aggregateAttr}>\n`;
|
|
114
231
|
if (event.fields.length > 0) {
|
|
115
232
|
xml += ' <fields>\n';
|
|
116
233
|
for (const field of event.fields) {
|
|
@@ -132,7 +249,16 @@ function formatSliceXml(model, slice) {
|
|
|
132
249
|
xml += ' </event>\n';
|
|
133
250
|
}
|
|
134
251
|
for (const readModel of components.readModels) {
|
|
135
|
-
|
|
252
|
+
// Check if this is a linked copy
|
|
253
|
+
const copyAttr = readModel.originalNodeId ? ' linked-copy="true"' : '';
|
|
254
|
+
let originAttr = '';
|
|
255
|
+
if (readModel.originalNodeId) {
|
|
256
|
+
const originSlice = findSliceForNode(model, readModel.originalNodeId);
|
|
257
|
+
if (originSlice) {
|
|
258
|
+
originAttr = ` origin-slice="${escapeXml(originSlice.name)}"`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
xml += ` <read-model name="${escapeXml(readModel.name)}"${copyAttr}${originAttr}>\n`;
|
|
136
262
|
if (readModel.fields.length > 0) {
|
|
137
263
|
xml += ' <fields>\n';
|
|
138
264
|
for (const field of readModel.fields) {
|
|
@@ -154,6 +280,21 @@ function formatSliceXml(model, slice) {
|
|
|
154
280
|
xml += ' </processor>\n';
|
|
155
281
|
}
|
|
156
282
|
xml += ' </components>\n';
|
|
283
|
+
if (aggregates.length > 0) {
|
|
284
|
+
xml += ' <aggregates>\n';
|
|
285
|
+
for (const agg of aggregates) {
|
|
286
|
+
const idField = agg.aggregateIdFieldName ? ` id-field="${escapeXml(agg.aggregateIdFieldName)}"` : '';
|
|
287
|
+
xml += ` <aggregate name="${escapeXml(agg.name)}"${idField}/>\n`;
|
|
288
|
+
}
|
|
289
|
+
xml += ' </aggregates>\n';
|
|
290
|
+
}
|
|
291
|
+
if (actors.length > 0) {
|
|
292
|
+
xml += ' <actors>\n';
|
|
293
|
+
for (const actor of actors) {
|
|
294
|
+
xml += ` <actor name="${escapeXml(actor.name)}"/>\n`;
|
|
295
|
+
}
|
|
296
|
+
xml += ' </actors>\n';
|
|
297
|
+
}
|
|
157
298
|
if (internalFlows.length > 0) {
|
|
158
299
|
xml += ' <information-flow>\n';
|
|
159
300
|
for (const flow of internalFlows) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EventModel } from '../../types.js';
|
|
2
|
+
interface UpdateOptions {
|
|
3
|
+
optional?: boolean;
|
|
4
|
+
generated?: boolean;
|
|
5
|
+
type?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function updateField(model: EventModel, filePath: string, options: {
|
|
8
|
+
command?: string;
|
|
9
|
+
event?: string;
|
|
10
|
+
readModel?: string;
|
|
11
|
+
}, fieldName: string, updates: UpdateOptions): void;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { appendEvent } from '../../lib/file-loader.js';
|
|
2
|
+
function findFieldByName(fields, fieldName) {
|
|
3
|
+
const nameLower = fieldName.toLowerCase();
|
|
4
|
+
for (const field of fields) {
|
|
5
|
+
if (field.name.toLowerCase() === nameLower) {
|
|
6
|
+
return field;
|
|
7
|
+
}
|
|
8
|
+
// Check subfields for Custom types
|
|
9
|
+
if (field.fieldType === 'Custom' && field.subfields) {
|
|
10
|
+
// Check for dot notation path (e.g., "address.street")
|
|
11
|
+
if (fieldName.toLowerCase().startsWith(field.name.toLowerCase() + '.')) {
|
|
12
|
+
const subPath = fieldName.slice(field.name.length + 1);
|
|
13
|
+
const subField = findFieldByName(field.subfields, subPath);
|
|
14
|
+
if (subField)
|
|
15
|
+
return subField;
|
|
16
|
+
}
|
|
17
|
+
// Also check direct match in subfields
|
|
18
|
+
const subField = findFieldByName(field.subfields, fieldName);
|
|
19
|
+
if (subField)
|
|
20
|
+
return subField;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
function createUpdatedField(original, updates) {
|
|
26
|
+
return {
|
|
27
|
+
...original,
|
|
28
|
+
isOptional: updates.optional !== undefined ? updates.optional : original.isOptional,
|
|
29
|
+
isGenerated: updates.generated !== undefined ? updates.generated : original.isGenerated,
|
|
30
|
+
fieldType: updates.type !== undefined ? updates.type : original.fieldType,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function updateField(model, filePath, options, fieldName, updates) {
|
|
34
|
+
// Determine which entity type
|
|
35
|
+
const entityCount = [options.command, options.event, options.readModel].filter(Boolean).length;
|
|
36
|
+
if (entityCount === 0) {
|
|
37
|
+
console.error('Error: Must specify one of --command, --event, or --read-model');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
if (entityCount > 1) {
|
|
41
|
+
console.error('Error: Can only specify one of --command, --event, or --read-model');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (options.command) {
|
|
45
|
+
updateCommandField(model, filePath, options.command, fieldName, updates);
|
|
46
|
+
}
|
|
47
|
+
else if (options.event) {
|
|
48
|
+
updateEventField(model, filePath, options.event, fieldName, updates);
|
|
49
|
+
}
|
|
50
|
+
else if (options.readModel) {
|
|
51
|
+
updateReadModelField(model, filePath, options.readModel, fieldName, updates);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function updateCommandField(model, filePath, commandName, fieldName, updates) {
|
|
55
|
+
const nameLower = commandName.toLowerCase();
|
|
56
|
+
const commands = [...model.commands.values()];
|
|
57
|
+
const command = commands.find(c => c.name.toLowerCase() === nameLower || c.name.toLowerCase().includes(nameLower));
|
|
58
|
+
if (!command) {
|
|
59
|
+
console.error(`Error: Command not found: ${commandName}`);
|
|
60
|
+
console.error('Available commands:');
|
|
61
|
+
for (const c of commands) {
|
|
62
|
+
console.error(` - ${c.name}`);
|
|
63
|
+
}
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
const field = findFieldByName(command.fields, fieldName);
|
|
67
|
+
if (!field) {
|
|
68
|
+
console.error(`Error: Field "${fieldName}" not found on command "${command.name}"`);
|
|
69
|
+
if (command.fields.length > 0) {
|
|
70
|
+
console.error('Available fields:');
|
|
71
|
+
for (const f of command.fields) {
|
|
72
|
+
console.error(` - ${f.name}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const updatedField = createUpdatedField(field, updates);
|
|
78
|
+
appendEvent(filePath, {
|
|
79
|
+
type: 'CommandFieldAdjusted',
|
|
80
|
+
commandStickyId: command.id,
|
|
81
|
+
fieldId: field.id,
|
|
82
|
+
field: updatedField,
|
|
83
|
+
timestamp: Date.now(),
|
|
84
|
+
});
|
|
85
|
+
console.log(`Updated field "${field.name}" on command "${command.name}"`);
|
|
86
|
+
logUpdates(updates);
|
|
87
|
+
}
|
|
88
|
+
function updateEventField(model, filePath, eventName, fieldName, updates) {
|
|
89
|
+
const nameLower = eventName.toLowerCase();
|
|
90
|
+
const events = [...model.events.values()];
|
|
91
|
+
const event = events.find(e => e.name.toLowerCase() === nameLower || e.name.toLowerCase().includes(nameLower));
|
|
92
|
+
if (!event) {
|
|
93
|
+
console.error(`Error: Event not found: ${eventName}`);
|
|
94
|
+
console.error('Available events:');
|
|
95
|
+
for (const e of events) {
|
|
96
|
+
console.error(` - ${e.name}`);
|
|
97
|
+
}
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
const field = findFieldByName(event.fields, fieldName);
|
|
101
|
+
if (!field) {
|
|
102
|
+
console.error(`Error: Field "${fieldName}" not found on event "${event.name}"`);
|
|
103
|
+
if (event.fields.length > 0) {
|
|
104
|
+
console.error('Available fields:');
|
|
105
|
+
for (const f of event.fields) {
|
|
106
|
+
console.error(` - ${f.name}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
const updatedField = createUpdatedField(field, updates);
|
|
112
|
+
appendEvent(filePath, {
|
|
113
|
+
type: 'EventFieldAdjusted',
|
|
114
|
+
eventStickyId: event.id,
|
|
115
|
+
fieldId: field.id,
|
|
116
|
+
field: updatedField,
|
|
117
|
+
timestamp: Date.now(),
|
|
118
|
+
});
|
|
119
|
+
console.log(`Updated field "${field.name}" on event "${event.name}"`);
|
|
120
|
+
logUpdates(updates);
|
|
121
|
+
}
|
|
122
|
+
function updateReadModelField(model, filePath, readModelName, fieldName, updates) {
|
|
123
|
+
const nameLower = readModelName.toLowerCase();
|
|
124
|
+
const readModels = [...model.readModels.values()];
|
|
125
|
+
const readModel = readModels.find(rm => rm.name.toLowerCase() === nameLower || rm.name.toLowerCase().includes(nameLower));
|
|
126
|
+
if (!readModel) {
|
|
127
|
+
console.error(`Error: Read model not found: ${readModelName}`);
|
|
128
|
+
console.error('Available read models:');
|
|
129
|
+
for (const rm of readModels) {
|
|
130
|
+
console.error(` - ${rm.name}`);
|
|
131
|
+
}
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
const field = findFieldByName(readModel.fields, fieldName);
|
|
135
|
+
if (!field) {
|
|
136
|
+
console.error(`Error: Field "${fieldName}" not found on read model "${readModel.name}"`);
|
|
137
|
+
if (readModel.fields.length > 0) {
|
|
138
|
+
console.error('Available fields:');
|
|
139
|
+
for (const f of readModel.fields) {
|
|
140
|
+
console.error(` - ${f.name}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
const updatedField = createUpdatedField(field, updates);
|
|
146
|
+
appendEvent(filePath, {
|
|
147
|
+
type: 'ReadModelFieldAdjusted',
|
|
148
|
+
readModelStickyId: readModel.id,
|
|
149
|
+
fieldId: field.id,
|
|
150
|
+
field: updatedField,
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
});
|
|
153
|
+
console.log(`Updated field "${field.name}" on read model "${readModel.name}"`);
|
|
154
|
+
logUpdates(updates);
|
|
155
|
+
}
|
|
156
|
+
function logUpdates(updates) {
|
|
157
|
+
if (updates.optional !== undefined) {
|
|
158
|
+
console.log(` isOptional: ${updates.optional}`);
|
|
159
|
+
}
|
|
160
|
+
if (updates.generated !== undefined) {
|
|
161
|
+
console.log(` isGenerated: ${updates.generated}`);
|
|
162
|
+
}
|
|
163
|
+
if (updates.type !== undefined) {
|
|
164
|
+
console.log(` fieldType: ${updates.type}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -56,6 +56,7 @@ export interface EventSticky {
|
|
|
56
56
|
width: number;
|
|
57
57
|
height: number;
|
|
58
58
|
canonicalId?: string;
|
|
59
|
+
originalNodeId?: string;
|
|
59
60
|
}
|
|
60
61
|
export interface ReadModelSticky {
|
|
61
62
|
id: string;
|
|
@@ -68,6 +69,7 @@ export interface ReadModelSticky {
|
|
|
68
69
|
width: number;
|
|
69
70
|
height: number;
|
|
70
71
|
canonicalId?: string;
|
|
72
|
+
originalNodeId?: string;
|
|
71
73
|
}
|
|
72
74
|
export interface Screen {
|
|
73
75
|
id: string;
|
|
@@ -80,6 +82,7 @@ export interface Screen {
|
|
|
80
82
|
width: number;
|
|
81
83
|
height: number;
|
|
82
84
|
canonicalId?: string;
|
|
85
|
+
originalNodeId?: string;
|
|
83
86
|
}
|
|
84
87
|
export interface Processor {
|
|
85
88
|
id: string;
|
|
@@ -118,6 +121,34 @@ export interface Chapter {
|
|
|
118
121
|
height: number;
|
|
119
122
|
};
|
|
120
123
|
}
|
|
124
|
+
export interface Aggregate {
|
|
125
|
+
id: string;
|
|
126
|
+
name: string;
|
|
127
|
+
position: {
|
|
128
|
+
x: number;
|
|
129
|
+
y: number;
|
|
130
|
+
};
|
|
131
|
+
size: {
|
|
132
|
+
width: number;
|
|
133
|
+
height: number;
|
|
134
|
+
};
|
|
135
|
+
eventIds: string[];
|
|
136
|
+
aggregateIdFieldName?: string;
|
|
137
|
+
aggregateIdFieldType?: Field['fieldType'];
|
|
138
|
+
}
|
|
139
|
+
export interface Actor {
|
|
140
|
+
id: string;
|
|
141
|
+
name: string;
|
|
142
|
+
position: {
|
|
143
|
+
x: number;
|
|
144
|
+
y: number;
|
|
145
|
+
};
|
|
146
|
+
size: {
|
|
147
|
+
width: number;
|
|
148
|
+
height: number;
|
|
149
|
+
};
|
|
150
|
+
screenIds: string[];
|
|
151
|
+
}
|
|
121
152
|
export interface Scenario {
|
|
122
153
|
id: string;
|
|
123
154
|
sliceId: string;
|
|
@@ -151,6 +182,8 @@ export interface EventModel {
|
|
|
151
182
|
processors: Map<string, Processor>;
|
|
152
183
|
slices: Map<string, Slice>;
|
|
153
184
|
chapters: Map<string, Chapter>;
|
|
185
|
+
aggregates: Map<string, Aggregate>;
|
|
186
|
+
actors: Map<string, Actor>;
|
|
154
187
|
scenarios: Map<string, Scenario>;
|
|
155
188
|
flows: Map<string, Flow>;
|
|
156
189
|
}
|