eventmodeler 0.4.6 → 0.4.7
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/api/index.d.ts +3 -0
- package/dist/api/index.js +3 -0
- package/dist/eventmodeler.js +3 -2
- package/dist/index.js +58 -13
- package/dist/slices/guide/guides/codegen.d.ts +5 -0
- package/dist/slices/guide/guides/codegen.js +339 -0
- package/dist/slices/guide/guides/connect-slices.d.ts +5 -0
- package/dist/slices/guide/guides/connect-slices.js +227 -0
- package/dist/slices/guide/guides/create-slices.d.ts +5 -0
- package/dist/slices/guide/guides/create-slices.js +303 -0
- package/dist/slices/guide/guides/explore.d.ts +5 -0
- package/dist/slices/guide/guides/explore.js +227 -0
- package/dist/slices/guide/guides/information-flow.d.ts +5 -0
- package/dist/slices/guide/guides/information-flow.js +316 -0
- package/dist/slices/guide/guides/scenarios.d.ts +5 -0
- package/dist/slices/guide/guides/scenarios.js +269 -0
- package/dist/slices/guide/index.d.ts +1 -0
- package/dist/slices/guide/index.js +40 -0
- package/dist/slices/open-app/index.js +1 -1
- package/package.json +5 -10
package/dist/api/index.d.ts
CHANGED
|
@@ -143,6 +143,9 @@ export declare function removeScenario(modelId: string, scenarioName: string, sl
|
|
|
143
143
|
export declare function createFlow(modelId: string, fromName: string, toName: string): Promise<{
|
|
144
144
|
success: boolean;
|
|
145
145
|
}>;
|
|
146
|
+
export declare function removeFlow(modelId: string, fromName: string, toName: string): Promise<{
|
|
147
|
+
flowId: string;
|
|
148
|
+
}>;
|
|
146
149
|
export interface CompoundFieldInput {
|
|
147
150
|
name: string;
|
|
148
151
|
fieldType: string;
|
package/dist/api/index.js
CHANGED
|
@@ -206,6 +206,9 @@ export async function removeScenario(modelId, scenarioName, sliceName) {
|
|
|
206
206
|
export async function createFlow(modelId, fromName, toName) {
|
|
207
207
|
return cliPost('/cli/create-flow', { modelId, fromName, toName });
|
|
208
208
|
}
|
|
209
|
+
export async function removeFlow(modelId, fromName, toName) {
|
|
210
|
+
return cliPost('/cli/remove-flow', { modelId, fromName, toName });
|
|
211
|
+
}
|
|
209
212
|
export async function createStateChangeSlice(modelId, input) {
|
|
210
213
|
return cliPost('/cli/create-state-change-slice', { modelId, ...input });
|
|
211
214
|
}
|
package/dist/eventmodeler.js
CHANGED
|
@@ -4413,7 +4413,8 @@ EXAMPLES:
|
|
|
4413
4413
|
process.exit(1);
|
|
4414
4414
|
}
|
|
4415
4415
|
{
|
|
4416
|
-
const
|
|
4416
|
+
const searchTerm = target ? `${subcommand} ${target}` : subcommand;
|
|
4417
|
+
const response = await search(modelId, searchTerm);
|
|
4417
4418
|
if (format === "json") {
|
|
4418
4419
|
console.log(JSON.stringify(response, null, 2));
|
|
4419
4420
|
} else {
|
|
@@ -4441,7 +4442,7 @@ EXAMPLES:
|
|
|
4441
4442
|
attrs["@_parent-chapter-name"] = r.parentChapterName;
|
|
4442
4443
|
return ` ${xb.build({ [tag]: attrs }).trim()}`;
|
|
4443
4444
|
});
|
|
4444
|
-
const wrapper = xb.build({ "search-results": { "@_query":
|
|
4445
|
+
const wrapper = xb.build({ "search-results": { "@_query": searchTerm, "@_count": String(response.results.length) } });
|
|
4445
4446
|
const open = wrapper.replace(/\/>$/, ">").trim();
|
|
4446
4447
|
console.log([open, ...results, "</search-results>"].join(`
|
|
4447
4448
|
`));
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ const VERSION = packageJson.version;
|
|
|
10
10
|
// Utilities
|
|
11
11
|
import { openApp } from './slices/open-app/index.js';
|
|
12
12
|
import { parseScenarioInput } from './slices/add-scenario/index.js';
|
|
13
|
+
import { runGuide } from './slices/guide/index.js';
|
|
13
14
|
import { login } from './slices/login/index.js';
|
|
14
15
|
import { logout } from './slices/logout/index.js';
|
|
15
16
|
import { whoami } from './slices/whoami/index.js';
|
|
@@ -124,6 +125,10 @@ COMMANDS:
|
|
|
124
125
|
|
|
125
126
|
export json Export entire model as JSON
|
|
126
127
|
|
|
128
|
+
GUIDES:
|
|
129
|
+
guide List available guides for AI agents and users
|
|
130
|
+
guide <name> Read a conceptual guide (no auth required)
|
|
131
|
+
|
|
127
132
|
OPTIONS:
|
|
128
133
|
--format <xml|json> Output format (default: xml, or from config)
|
|
129
134
|
-h, --help Show this help message
|
|
@@ -277,6 +282,10 @@ EXAMPLES:
|
|
|
277
282
|
await init();
|
|
278
283
|
return;
|
|
279
284
|
}
|
|
285
|
+
if (command === 'guide') {
|
|
286
|
+
runGuide(subcommand);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
280
289
|
const projectConfig = loadProjectConfig();
|
|
281
290
|
if (!projectConfig || projectConfig.type !== 'cloud') {
|
|
282
291
|
console.error('Error: No project configured.');
|
|
@@ -1084,7 +1093,8 @@ EXAMPLES:
|
|
|
1084
1093
|
process.exit(1);
|
|
1085
1094
|
}
|
|
1086
1095
|
{
|
|
1087
|
-
const
|
|
1096
|
+
const searchTerm = target ? `${subcommand} ${target}` : subcommand;
|
|
1097
|
+
const response = await api.search(modelId, searchTerm);
|
|
1088
1098
|
if (format === 'json') {
|
|
1089
1099
|
console.log(JSON.stringify(response, null, 2));
|
|
1090
1100
|
}
|
|
@@ -1113,7 +1123,7 @@ EXAMPLES:
|
|
|
1113
1123
|
attrs['@_parent-chapter-name'] = r.parentChapterName;
|
|
1114
1124
|
return ` ${xb.build({ [tag]: attrs }).trim()}`;
|
|
1115
1125
|
});
|
|
1116
|
-
const wrapper = xb.build({ 'search-results': { '@_query':
|
|
1126
|
+
const wrapper = xb.build({ 'search-results': { '@_query': searchTerm, '@_count': String(response.results.length) } });
|
|
1117
1127
|
// Replace self-closing tag with children
|
|
1118
1128
|
const open = wrapper.replace(/\/>$/, '>').trim();
|
|
1119
1129
|
console.log([open, ...results, '</search-results>'].join('\n'));
|
|
@@ -1505,18 +1515,19 @@ EXAMPLES:
|
|
|
1505
1515
|
{
|
|
1506
1516
|
// Parse field data
|
|
1507
1517
|
let field;
|
|
1518
|
+
const toApiFieldFromJson = (f) => ({
|
|
1519
|
+
name: f.name,
|
|
1520
|
+
fieldType: f.type ?? f.fieldType ?? 'String',
|
|
1521
|
+
isList: f.isList,
|
|
1522
|
+
isOptional: f.isOptional,
|
|
1523
|
+
isGenerated: f.isGenerated,
|
|
1524
|
+
isUserInput: f.isUserInput,
|
|
1525
|
+
subfields: f.subfields?.map(toApiFieldFromJson),
|
|
1526
|
+
});
|
|
1508
1527
|
try {
|
|
1509
1528
|
if (jsonArg) {
|
|
1510
1529
|
const parsed = JSON.parse(inputData);
|
|
1511
|
-
field =
|
|
1512
|
-
name: parsed.name,
|
|
1513
|
-
fieldType: parsed.type ?? parsed.fieldType ?? 'String',
|
|
1514
|
-
isList: parsed.isList,
|
|
1515
|
-
isOptional: parsed.isOptional,
|
|
1516
|
-
isGenerated: parsed.isGenerated,
|
|
1517
|
-
isUserInput: parsed.isUserInput,
|
|
1518
|
-
subfields: parsed.subfields,
|
|
1519
|
-
};
|
|
1530
|
+
field = toApiFieldFromJson(parsed);
|
|
1520
1531
|
}
|
|
1521
1532
|
else {
|
|
1522
1533
|
const parser = await import('./lib/slice-utils.js');
|
|
@@ -1562,6 +1573,7 @@ USAGE:
|
|
|
1562
1573
|
TYPES:
|
|
1563
1574
|
scenario Remove a scenario from a slice
|
|
1564
1575
|
field Remove a field from an entity
|
|
1576
|
+
flow Remove a flow between elements
|
|
1565
1577
|
|
|
1566
1578
|
Run "eventmodeler remove <type> --help" for type-specific help.
|
|
1567
1579
|
`);
|
|
@@ -1646,9 +1658,39 @@ EXAMPLES:
|
|
|
1646
1658
|
}
|
|
1647
1659
|
break;
|
|
1648
1660
|
}
|
|
1661
|
+
case 'flow': {
|
|
1662
|
+
if (helpRequested) {
|
|
1663
|
+
console.log(`
|
|
1664
|
+
eventmodeler remove flow - Remove a flow between elements
|
|
1665
|
+
|
|
1666
|
+
USAGE:
|
|
1667
|
+
eventmodeler remove flow --from <source> --to <target>
|
|
1668
|
+
|
|
1669
|
+
OPTIONS:
|
|
1670
|
+
--from <name> Source element name (required)
|
|
1671
|
+
--to <name> Target element name (required)
|
|
1672
|
+
|
|
1673
|
+
EXAMPLES:
|
|
1674
|
+
eventmodeler remove flow --from OrderPlaced --to OrderSummary
|
|
1675
|
+
eventmodeler remove flow --from "Order Summary" --to "Order Dashboard Screen"
|
|
1676
|
+
`);
|
|
1677
|
+
process.exit(0);
|
|
1678
|
+
}
|
|
1679
|
+
const fromArg = getNamedArg(filteredArgs, '--from');
|
|
1680
|
+
const toArg = getNamedArg(filteredArgs, '--to');
|
|
1681
|
+
if (!fromArg || !toArg) {
|
|
1682
|
+
console.error('Error: --from and --to are required');
|
|
1683
|
+
console.error('Usage: eventmodeler remove flow --from <source> --to <target>');
|
|
1684
|
+
console.error('Run "eventmodeler remove flow --help" for more information.');
|
|
1685
|
+
process.exit(1);
|
|
1686
|
+
}
|
|
1687
|
+
await api.removeFlow(modelId, fromArg, toArg);
|
|
1688
|
+
console.log(`Removed flow from "${fromArg}" to "${toArg}"`);
|
|
1689
|
+
break;
|
|
1690
|
+
}
|
|
1649
1691
|
default:
|
|
1650
1692
|
console.error(`Unknown remove target: ${subcommand}`);
|
|
1651
|
-
console.error('Valid targets: scenario, field');
|
|
1693
|
+
console.error('Valid targets: scenario, field, flow');
|
|
1652
1694
|
console.error('Run "eventmodeler remove --help" for more information.');
|
|
1653
1695
|
process.exit(1);
|
|
1654
1696
|
}
|
|
@@ -1791,7 +1833,7 @@ OPTIONS:
|
|
|
1791
1833
|
<element-name> Name of the command, event, read model, screen, or processor
|
|
1792
1834
|
|
|
1793
1835
|
REQUIRED:
|
|
1794
|
-
--field <name> Field to update
|
|
1836
|
+
--field <name> Field to update (use dot-notation for nested subfields: "parent.child")
|
|
1795
1837
|
|
|
1796
1838
|
UPDATE OPTIONS (at least one required):
|
|
1797
1839
|
--optional true|false Set isOptional flag
|
|
@@ -1816,6 +1858,9 @@ EXAMPLES:
|
|
|
1816
1858
|
|
|
1817
1859
|
# Change field type
|
|
1818
1860
|
eventmodeler update field OrderPlaced --field amount --type Decimal
|
|
1861
|
+
|
|
1862
|
+
# Update a nested subfield using dot-notation
|
|
1863
|
+
eventmodeler update field OrderPlaced --field "shippingAddress.zipCode" --optional true
|
|
1819
1864
|
`);
|
|
1820
1865
|
process.exit(0);
|
|
1821
1866
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const meta: {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
};
|
|
5
|
+
export declare const content = "\n# Building Code Generators for Event Models\n\nThe `eventmodeler codegen` commands output structured JSON that code generators can consume:\n- `eventmodeler codegen slice \"<slice-name>\"` for per-slice generation\n- `eventmodeler codegen events [--chapter \"<chapter-name>\"]` for deduplicated event scaffolding across slices\n\n## The Codegen Pipeline\n\nPer-slice pipeline:\n\n```\neventmodeler codegen slice \"Place Order\" | your-generator > output-files\n```\n\nEvents-only pipeline:\n\n```\neventmodeler codegen events --chapter \"Register Products\" | your-events-generator > events.kt\n```\n\nYour generator receives JSON on stdin and produces code files.\n\n## Understanding the Input\n\nRun codegen to see what you'll receive:\n\n```bash\neventmodeler codegen slice \"Place Order\"\n```\n\n### Top-Level Structure\n\n```json\n{\n \"sliceType\": \"STATE_CHANGE | STATE_VIEW | AUTOMATION\",\n \"slice\": { \"id\": \"...\", \"name\": \"Place Order\" },\n \"chapter\": { \"name\": \"...\", \"hierarchy\": [\"Parent\", \"Child\"] },\n \"elements\": { ... },\n \"inboundDependencies\": [ ... ],\n \"internalFlows\": [ ... ],\n \"scenarios\": [ ... ]\n}\n```\n\n### Events-Only Structure (`codegen events`)\n\n```json\n{\n \"chapter\": { \"name\": \"Register Products\", \"parent\": { \"name\": \"Configuration\" } },\n \"sliceCount\": 4,\n \"eventCount\": 9,\n \"slices\": [\n { \"id\": \"...\", \"name\": \"Place Product\", \"sliceType\": \"STATE_CHANGE\" }\n ],\n \"events\": [\n {\n \"id\": \"...\",\n \"name\": \"ProductRegistered\",\n \"fields\": [ ... ],\n \"aggregate\": \"Product\",\n \"sourceSlices\": [\"Place Product\", \"Import Product\"]\n }\n ]\n}\n```\n\nUse `codegen events` when generating shared event artifacts in one pass (for a chapter or entire model).\n\n### Slice Types and What to Generate\n\n| sliceType | Components | Generate |\n|-----------|------------|----------|\n| `STATE_CHANGE` | Screen, Command, Event | API endpoint, command handler, event, tests |\n| `STATE_VIEW` | ReadModel | Projection/denormalizer, query endpoint |\n| `AUTOMATION` | Processor, ReadModel, Command, Event | Event handler, saga/process manager |\n\n## Building a Generator: Step by Step\n\n### 1. Parse the Input\n\n```typescript\n// generator.ts\nimport { stdin } from 'process';\n\ninterface CodegenInput {\n sliceType: 'STATE_CHANGE' | 'STATE_VIEW' | 'AUTOMATION';\n slice: { id: string; name: string };\n elements: {\n commands: Array<{ name: string; fields: Field[] }>;\n events: Array<{ name: string; fields: Field[]; aggregate?: string }>;\n readModels: Array<{ name: string; fields: Field[] }>;\n screens: Array<{ name: string; fields: Field[] }>;\n processors: Array<{ name: string }>;\n };\n inboundDependencies: InboundDependency[];\n internalFlows: Flow[];\n scenarios: Scenario[];\n}\n\nasync function readInput(): Promise<CodegenInput> {\n const chunks: Buffer[] = [];\n for await (const chunk of stdin) {\n chunks.push(chunk);\n }\n return JSON.parse(Buffer.concat(chunks).toString());\n}\n```\n\n### 2. Route by Slice Type\n\n```typescript\nasync function main() {\n const input = await readInput();\n\n switch (input.sliceType) {\n case 'STATE_CHANGE':\n generateStateChangeSlice(input);\n break;\n case 'STATE_VIEW':\n generateStateViewSlice(input);\n break;\n case 'AUTOMATION':\n generateAutomationSlice(input);\n break;\n }\n}\n```\n\n### 3. Generate from Elements\n\n**Commands \u2192 DTOs/Request types:**\n```typescript\nfunction generateCommand(cmd: Command): string {\n const fields = cmd.fields\n .map(f => ` ${f.name}: ${mapType(f.type)}${f.optional ? ' | null' : ''};`)\n .join('\\n');\n\n return `export interface ${cmd.name}Command {\\n${fields}\\n}`;\n}\n```\n\n**Events \u2192 Event types with metadata:**\n```typescript\nfunction generateEvent(evt: Event): string {\n const fields = evt.fields.map(f => {\n const generated = f.generated ? ' // generated' : '';\n return ` ${f.name}: ${mapType(f.type)};${generated}`;\n }).join('\\n');\n\n return `export interface ${evt.name}Event {\\n type: '${evt.name}';\\n${fields}\\n}`;\n}\n```\n\n**ReadModels \u2192 Projection types:**\n```typescript\nfunction generateReadModel(rm: ReadModel): string {\n const fields = rm.fields\n .map(f => ` ${f.name}: ${mapType(f.type)}${f.optional ? ' | null' : ''};`)\n .join('\\n');\n\n return `export interface ${rm.name} {\\n${fields}\\n}`;\n}\n```\n\n### 4. Use Field Mappings for Projections\n\nThe `inboundDependencies` show how external events map to read model fields:\n\n```typescript\nfunction generateProjection(input: CodegenInput): string {\n const rm = input.elements.readModels[0];\n const handlers: string[] = [];\n\n for (const dep of input.inboundDependencies) {\n if (dep.target.name === rm.name) {\n const mappings = dep.fieldMappings\n .map(m => ` state.${m.targetFieldName} = event.${m.sourceFieldName};`)\n .join('\\n');\n\n handlers.push(`\n on${dep.source.name}(state: ${rm.name}, event: ${dep.source.name}Event) {\n${mappings}\n }`);\n }\n }\n\n return `export class ${rm.name}Projection {\\n${handlers.join('\\n')}\\n}`;\n}\n```\n\n### 5. Generate Tests from Scenarios\n\nScenarios are BDD specs that become tests. There are two scenario patterns:\n\n**State-Change Scenarios** (whenCommand \u2192 then events/error):\n- `whenCommand`: The command being tested\n- `then.type: 'events'`: Expected events produced\n- `then.type: 'error'`: Expected error\n\n**Automation Scenarios** (whenEvents \u2192 then command):\n- `whenEvents`: The event(s) that trigger the processor\n- `then.type: 'command'`: The command the processor should issue\n\n```typescript\nfunction generateTests(input: CodegenInput): string {\n return input.scenarios.map(scenario => {\n const givenSetup = scenario.given\n .map(g => ` await given(new ${g.eventName}(${JSON.stringify(g.fieldValues)}));`)\n .join('\\n');\n\n // State-change scenarios: when is a command\n let whenAction = '';\n if (scenario.whenCommand) {\n whenAction = ` await when(new ${scenario.whenCommand.commandName}(${JSON.stringify(scenario.whenCommand.fieldValues)}));`;\n }\n // Automation scenarios: when is event(s)\n if (scenario.whenEvents?.length > 0) {\n whenAction = scenario.whenEvents\n .map(e => ` await whenEvent(new ${e.eventName}(${JSON.stringify(e.fieldValues)}));`)\n .join('\\n');\n }\n\n let thenAssertion: string;\n if (scenario.then.type === 'events') {\n const expected = scenario.then.expectedEvents!\n .map(e => `new ${e.eventName}(${JSON.stringify(e.fieldValues)})`)\n .join(', ');\n thenAssertion = ` await thenExpect([${expected}]);`;\n } else if (scenario.then.type === 'command') {\n const cmd = scenario.then.expectedCommand!;\n thenAssertion = ` await thenExpectCommand(new ${cmd.commandName}(${JSON.stringify(cmd.fieldValues)}));`;\n } else if (scenario.then.type === 'error') {\n thenAssertion = ` await thenExpectError('${scenario.then.errorType}', '${scenario.then.errorMessage}');`;\n } else {\n thenAssertion = ` await thenExpectState(${JSON.stringify(scenario.then.expectedFieldValues)});`;\n }\n\n return `\n test('${scenario.name}', async () => {\n${givenSetup}\n${whenAction}\n${thenAssertion}\n });`;\n }).join('\\n');\n}\n```\n\n## Type Mapping\n\nMap event model types to your target language:\n\n```typescript\nfunction mapType(fieldType: string, isList?: boolean): string {\n const baseType = {\n 'UUID': 'string',\n 'String': 'string',\n 'Int': 'number',\n 'Long': 'number',\n 'Double': 'number',\n 'Decimal': 'Decimal', // or 'number' or 'BigNumber'\n 'Boolean': 'boolean',\n 'Date': 'Date',\n 'DateTime': 'Date',\n 'Custom': 'object', // handle subfields separately\n }[fieldType] ?? 'unknown';\n\n return isList ? `${baseType}[]` : baseType;\n}\n```\n\nFor Custom types with subfields, generate inline or separate types:\n\n```typescript\nfunction generateFieldType(field: Field): string {\n if (field.type === 'Custom' && field.subfields) {\n const nested = field.subfields\n .map(sf => `${sf.name}: ${mapType(sf.type)}`)\n .join('; ');\n const type = `{ ${nested} }`;\n return field.list ? `Array<${type}>` : type;\n }\n return mapType(field.type, field.list);\n}\n```\n\n## Handling Aggregates\n\nEvents include their aggregate name when inside one:\n\n```typescript\nfunction generateEventHandler(input: CodegenInput): string {\n const event = input.elements.events[0];\n const aggregate = event.aggregate;\n\n if (aggregate) {\n return `\nexport class ${aggregate}Aggregate {\n apply(event: ${event.name}Event) {\n // Update aggregate state from event\n }\n}`;\n }\n // ...\n}\n```\n\n## Running Your Generator\n\n```bash\n# Single slice\neventmodeler codegen slice \"Place Order\" | node generator.js\n\n# All slices (bash loop)\nfor slice in $(eventmodeler list slices --format json | node -e 'const fs=require(\"fs\");const d=JSON.parse(fs.readFileSync(0,\"utf8\"));for(const s of (d.slices||[])) console.log(s.name)'); do\n eventmodeler codegen slice \"$slice\" | node generator.js\ndone\n\n# Chapter-scoped events scaffolding\neventmodeler codegen events --chapter \"Register Products\" | node generate-events.js\n\n# Whole-model events scaffolding\neventmodeler codegen events | node generate-events.js\n```\n\n## Best Practices\n\n1. **Handle all slice types** - Your generator should handle STATE_CHANGE, STATE_VIEW, and AUTOMATION\n2. **Use field mappings** - They define the projection logic between events and read models\n3. **Generate tests from scenarios** - Scenarios are executable specifications\n4. **Respect field attributes** - `generated` fields don't need input, `optional` fields can be null\n5. **Use aggregate info** - Events in aggregates should be handled by that aggregate\n6. **Handle Custom types recursively** - Subfields can themselves be Custom types\n7. **Make it idempotent** - Running twice should produce the same output\n";
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
name: 'codegen',
|
|
3
|
+
description: 'Build code generators that consume eventmodeler codegen output',
|
|
4
|
+
};
|
|
5
|
+
export const content = `
|
|
6
|
+
# Building Code Generators for Event Models
|
|
7
|
+
|
|
8
|
+
The \`eventmodeler codegen\` commands output structured JSON that code generators can consume:
|
|
9
|
+
- \`eventmodeler codegen slice "<slice-name>"\` for per-slice generation
|
|
10
|
+
- \`eventmodeler codegen events [--chapter "<chapter-name>"]\` for deduplicated event scaffolding across slices
|
|
11
|
+
|
|
12
|
+
## The Codegen Pipeline
|
|
13
|
+
|
|
14
|
+
Per-slice pipeline:
|
|
15
|
+
|
|
16
|
+
\`\`\`
|
|
17
|
+
eventmodeler codegen slice "Place Order" | your-generator > output-files
|
|
18
|
+
\`\`\`
|
|
19
|
+
|
|
20
|
+
Events-only pipeline:
|
|
21
|
+
|
|
22
|
+
\`\`\`
|
|
23
|
+
eventmodeler codegen events --chapter "Register Products" | your-events-generator > events.kt
|
|
24
|
+
\`\`\`
|
|
25
|
+
|
|
26
|
+
Your generator receives JSON on stdin and produces code files.
|
|
27
|
+
|
|
28
|
+
## Understanding the Input
|
|
29
|
+
|
|
30
|
+
Run codegen to see what you'll receive:
|
|
31
|
+
|
|
32
|
+
\`\`\`bash
|
|
33
|
+
eventmodeler codegen slice "Place Order"
|
|
34
|
+
\`\`\`
|
|
35
|
+
|
|
36
|
+
### Top-Level Structure
|
|
37
|
+
|
|
38
|
+
\`\`\`json
|
|
39
|
+
{
|
|
40
|
+
"sliceType": "STATE_CHANGE | STATE_VIEW | AUTOMATION",
|
|
41
|
+
"slice": { "id": "...", "name": "Place Order" },
|
|
42
|
+
"chapter": { "name": "...", "hierarchy": ["Parent", "Child"] },
|
|
43
|
+
"elements": { ... },
|
|
44
|
+
"inboundDependencies": [ ... ],
|
|
45
|
+
"internalFlows": [ ... ],
|
|
46
|
+
"scenarios": [ ... ]
|
|
47
|
+
}
|
|
48
|
+
\`\`\`
|
|
49
|
+
|
|
50
|
+
### Events-Only Structure (\`codegen events\`)
|
|
51
|
+
|
|
52
|
+
\`\`\`json
|
|
53
|
+
{
|
|
54
|
+
"chapter": { "name": "Register Products", "parent": { "name": "Configuration" } },
|
|
55
|
+
"sliceCount": 4,
|
|
56
|
+
"eventCount": 9,
|
|
57
|
+
"slices": [
|
|
58
|
+
{ "id": "...", "name": "Place Product", "sliceType": "STATE_CHANGE" }
|
|
59
|
+
],
|
|
60
|
+
"events": [
|
|
61
|
+
{
|
|
62
|
+
"id": "...",
|
|
63
|
+
"name": "ProductRegistered",
|
|
64
|
+
"fields": [ ... ],
|
|
65
|
+
"aggregate": "Product",
|
|
66
|
+
"sourceSlices": ["Place Product", "Import Product"]
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
\`\`\`
|
|
71
|
+
|
|
72
|
+
Use \`codegen events\` when generating shared event artifacts in one pass (for a chapter or entire model).
|
|
73
|
+
|
|
74
|
+
### Slice Types and What to Generate
|
|
75
|
+
|
|
76
|
+
| sliceType | Components | Generate |
|
|
77
|
+
|-----------|------------|----------|
|
|
78
|
+
| \`STATE_CHANGE\` | Screen, Command, Event | API endpoint, command handler, event, tests |
|
|
79
|
+
| \`STATE_VIEW\` | ReadModel | Projection/denormalizer, query endpoint |
|
|
80
|
+
| \`AUTOMATION\` | Processor, ReadModel, Command, Event | Event handler, saga/process manager |
|
|
81
|
+
|
|
82
|
+
## Building a Generator: Step by Step
|
|
83
|
+
|
|
84
|
+
### 1. Parse the Input
|
|
85
|
+
|
|
86
|
+
\`\`\`typescript
|
|
87
|
+
// generator.ts
|
|
88
|
+
import { stdin } from 'process';
|
|
89
|
+
|
|
90
|
+
interface CodegenInput {
|
|
91
|
+
sliceType: 'STATE_CHANGE' | 'STATE_VIEW' | 'AUTOMATION';
|
|
92
|
+
slice: { id: string; name: string };
|
|
93
|
+
elements: {
|
|
94
|
+
commands: Array<{ name: string; fields: Field[] }>;
|
|
95
|
+
events: Array<{ name: string; fields: Field[]; aggregate?: string }>;
|
|
96
|
+
readModels: Array<{ name: string; fields: Field[] }>;
|
|
97
|
+
screens: Array<{ name: string; fields: Field[] }>;
|
|
98
|
+
processors: Array<{ name: string }>;
|
|
99
|
+
};
|
|
100
|
+
inboundDependencies: InboundDependency[];
|
|
101
|
+
internalFlows: Flow[];
|
|
102
|
+
scenarios: Scenario[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function readInput(): Promise<CodegenInput> {
|
|
106
|
+
const chunks: Buffer[] = [];
|
|
107
|
+
for await (const chunk of stdin) {
|
|
108
|
+
chunks.push(chunk);
|
|
109
|
+
}
|
|
110
|
+
return JSON.parse(Buffer.concat(chunks).toString());
|
|
111
|
+
}
|
|
112
|
+
\`\`\`
|
|
113
|
+
|
|
114
|
+
### 2. Route by Slice Type
|
|
115
|
+
|
|
116
|
+
\`\`\`typescript
|
|
117
|
+
async function main() {
|
|
118
|
+
const input = await readInput();
|
|
119
|
+
|
|
120
|
+
switch (input.sliceType) {
|
|
121
|
+
case 'STATE_CHANGE':
|
|
122
|
+
generateStateChangeSlice(input);
|
|
123
|
+
break;
|
|
124
|
+
case 'STATE_VIEW':
|
|
125
|
+
generateStateViewSlice(input);
|
|
126
|
+
break;
|
|
127
|
+
case 'AUTOMATION':
|
|
128
|
+
generateAutomationSlice(input);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
\`\`\`
|
|
133
|
+
|
|
134
|
+
### 3. Generate from Elements
|
|
135
|
+
|
|
136
|
+
**Commands → DTOs/Request types:**
|
|
137
|
+
\`\`\`typescript
|
|
138
|
+
function generateCommand(cmd: Command): string {
|
|
139
|
+
const fields = cmd.fields
|
|
140
|
+
.map(f => \` \${f.name}: \${mapType(f.type)}\${f.optional ? ' | null' : ''};\`)
|
|
141
|
+
.join('\\n');
|
|
142
|
+
|
|
143
|
+
return \`export interface \${cmd.name}Command {\\n\${fields}\\n}\`;
|
|
144
|
+
}
|
|
145
|
+
\`\`\`
|
|
146
|
+
|
|
147
|
+
**Events → Event types with metadata:**
|
|
148
|
+
\`\`\`typescript
|
|
149
|
+
function generateEvent(evt: Event): string {
|
|
150
|
+
const fields = evt.fields.map(f => {
|
|
151
|
+
const generated = f.generated ? ' // generated' : '';
|
|
152
|
+
return \` \${f.name}: \${mapType(f.type)};\${generated}\`;
|
|
153
|
+
}).join('\\n');
|
|
154
|
+
|
|
155
|
+
return \`export interface \${evt.name}Event {\\n type: '\${evt.name}';\\n\${fields}\\n}\`;
|
|
156
|
+
}
|
|
157
|
+
\`\`\`
|
|
158
|
+
|
|
159
|
+
**ReadModels → Projection types:**
|
|
160
|
+
\`\`\`typescript
|
|
161
|
+
function generateReadModel(rm: ReadModel): string {
|
|
162
|
+
const fields = rm.fields
|
|
163
|
+
.map(f => \` \${f.name}: \${mapType(f.type)}\${f.optional ? ' | null' : ''};\`)
|
|
164
|
+
.join('\\n');
|
|
165
|
+
|
|
166
|
+
return \`export interface \${rm.name} {\\n\${fields}\\n}\`;
|
|
167
|
+
}
|
|
168
|
+
\`\`\`
|
|
169
|
+
|
|
170
|
+
### 4. Use Field Mappings for Projections
|
|
171
|
+
|
|
172
|
+
The \`inboundDependencies\` show how external events map to read model fields:
|
|
173
|
+
|
|
174
|
+
\`\`\`typescript
|
|
175
|
+
function generateProjection(input: CodegenInput): string {
|
|
176
|
+
const rm = input.elements.readModels[0];
|
|
177
|
+
const handlers: string[] = [];
|
|
178
|
+
|
|
179
|
+
for (const dep of input.inboundDependencies) {
|
|
180
|
+
if (dep.target.name === rm.name) {
|
|
181
|
+
const mappings = dep.fieldMappings
|
|
182
|
+
.map(m => \` state.\${m.targetFieldName} = event.\${m.sourceFieldName};\`)
|
|
183
|
+
.join('\\n');
|
|
184
|
+
|
|
185
|
+
handlers.push(\`
|
|
186
|
+
on\${dep.source.name}(state: \${rm.name}, event: \${dep.source.name}Event) {
|
|
187
|
+
\${mappings}
|
|
188
|
+
}\`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return \`export class \${rm.name}Projection {\\n\${handlers.join('\\n')}\\n}\`;
|
|
193
|
+
}
|
|
194
|
+
\`\`\`
|
|
195
|
+
|
|
196
|
+
### 5. Generate Tests from Scenarios
|
|
197
|
+
|
|
198
|
+
Scenarios are BDD specs that become tests. There are two scenario patterns:
|
|
199
|
+
|
|
200
|
+
**State-Change Scenarios** (whenCommand → then events/error):
|
|
201
|
+
- \`whenCommand\`: The command being tested
|
|
202
|
+
- \`then.type: 'events'\`: Expected events produced
|
|
203
|
+
- \`then.type: 'error'\`: Expected error
|
|
204
|
+
|
|
205
|
+
**Automation Scenarios** (whenEvents → then command):
|
|
206
|
+
- \`whenEvents\`: The event(s) that trigger the processor
|
|
207
|
+
- \`then.type: 'command'\`: The command the processor should issue
|
|
208
|
+
|
|
209
|
+
\`\`\`typescript
|
|
210
|
+
function generateTests(input: CodegenInput): string {
|
|
211
|
+
return input.scenarios.map(scenario => {
|
|
212
|
+
const givenSetup = scenario.given
|
|
213
|
+
.map(g => \` await given(new \${g.eventName}(\${JSON.stringify(g.fieldValues)}));\`)
|
|
214
|
+
.join('\\n');
|
|
215
|
+
|
|
216
|
+
// State-change scenarios: when is a command
|
|
217
|
+
let whenAction = '';
|
|
218
|
+
if (scenario.whenCommand) {
|
|
219
|
+
whenAction = \` await when(new \${scenario.whenCommand.commandName}(\${JSON.stringify(scenario.whenCommand.fieldValues)}));\`;
|
|
220
|
+
}
|
|
221
|
+
// Automation scenarios: when is event(s)
|
|
222
|
+
if (scenario.whenEvents?.length > 0) {
|
|
223
|
+
whenAction = scenario.whenEvents
|
|
224
|
+
.map(e => \` await whenEvent(new \${e.eventName}(\${JSON.stringify(e.fieldValues)}));\`)
|
|
225
|
+
.join('\\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let thenAssertion: string;
|
|
229
|
+
if (scenario.then.type === 'events') {
|
|
230
|
+
const expected = scenario.then.expectedEvents!
|
|
231
|
+
.map(e => \`new \${e.eventName}(\${JSON.stringify(e.fieldValues)})\`)
|
|
232
|
+
.join(', ');
|
|
233
|
+
thenAssertion = \` await thenExpect([\${expected}]);\`;
|
|
234
|
+
} else if (scenario.then.type === 'command') {
|
|
235
|
+
const cmd = scenario.then.expectedCommand!;
|
|
236
|
+
thenAssertion = \` await thenExpectCommand(new \${cmd.commandName}(\${JSON.stringify(cmd.fieldValues)}));\`;
|
|
237
|
+
} else if (scenario.then.type === 'error') {
|
|
238
|
+
thenAssertion = \` await thenExpectError('\${scenario.then.errorType}', '\${scenario.then.errorMessage}');\`;
|
|
239
|
+
} else {
|
|
240
|
+
thenAssertion = \` await thenExpectState(\${JSON.stringify(scenario.then.expectedFieldValues)});\`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return \`
|
|
244
|
+
test('\${scenario.name}', async () => {
|
|
245
|
+
\${givenSetup}
|
|
246
|
+
\${whenAction}
|
|
247
|
+
\${thenAssertion}
|
|
248
|
+
});\`;
|
|
249
|
+
}).join('\\n');
|
|
250
|
+
}
|
|
251
|
+
\`\`\`
|
|
252
|
+
|
|
253
|
+
## Type Mapping
|
|
254
|
+
|
|
255
|
+
Map event model types to your target language:
|
|
256
|
+
|
|
257
|
+
\`\`\`typescript
|
|
258
|
+
function mapType(fieldType: string, isList?: boolean): string {
|
|
259
|
+
const baseType = {
|
|
260
|
+
'UUID': 'string',
|
|
261
|
+
'String': 'string',
|
|
262
|
+
'Int': 'number',
|
|
263
|
+
'Long': 'number',
|
|
264
|
+
'Double': 'number',
|
|
265
|
+
'Decimal': 'Decimal', // or 'number' or 'BigNumber'
|
|
266
|
+
'Boolean': 'boolean',
|
|
267
|
+
'Date': 'Date',
|
|
268
|
+
'DateTime': 'Date',
|
|
269
|
+
'Custom': 'object', // handle subfields separately
|
|
270
|
+
}[fieldType] ?? 'unknown';
|
|
271
|
+
|
|
272
|
+
return isList ? \`\${baseType}[]\` : baseType;
|
|
273
|
+
}
|
|
274
|
+
\`\`\`
|
|
275
|
+
|
|
276
|
+
For Custom types with subfields, generate inline or separate types:
|
|
277
|
+
|
|
278
|
+
\`\`\`typescript
|
|
279
|
+
function generateFieldType(field: Field): string {
|
|
280
|
+
if (field.type === 'Custom' && field.subfields) {
|
|
281
|
+
const nested = field.subfields
|
|
282
|
+
.map(sf => \`\${sf.name}: \${mapType(sf.type)}\`)
|
|
283
|
+
.join('; ');
|
|
284
|
+
const type = \`{ \${nested} }\`;
|
|
285
|
+
return field.list ? \`Array<\${type}>\` : type;
|
|
286
|
+
}
|
|
287
|
+
return mapType(field.type, field.list);
|
|
288
|
+
}
|
|
289
|
+
\`\`\`
|
|
290
|
+
|
|
291
|
+
## Handling Aggregates
|
|
292
|
+
|
|
293
|
+
Events include their aggregate name when inside one:
|
|
294
|
+
|
|
295
|
+
\`\`\`typescript
|
|
296
|
+
function generateEventHandler(input: CodegenInput): string {
|
|
297
|
+
const event = input.elements.events[0];
|
|
298
|
+
const aggregate = event.aggregate;
|
|
299
|
+
|
|
300
|
+
if (aggregate) {
|
|
301
|
+
return \`
|
|
302
|
+
export class \${aggregate}Aggregate {
|
|
303
|
+
apply(event: \${event.name}Event) {
|
|
304
|
+
// Update aggregate state from event
|
|
305
|
+
}
|
|
306
|
+
}\`;
|
|
307
|
+
}
|
|
308
|
+
// ...
|
|
309
|
+
}
|
|
310
|
+
\`\`\`
|
|
311
|
+
|
|
312
|
+
## Running Your Generator
|
|
313
|
+
|
|
314
|
+
\`\`\`bash
|
|
315
|
+
# Single slice
|
|
316
|
+
eventmodeler codegen slice "Place Order" | node generator.js
|
|
317
|
+
|
|
318
|
+
# All slices (bash loop)
|
|
319
|
+
for slice in $(eventmodeler list slices --format json | node -e 'const fs=require("fs");const d=JSON.parse(fs.readFileSync(0,"utf8"));for(const s of (d.slices||[])) console.log(s.name)'); do
|
|
320
|
+
eventmodeler codegen slice "$slice" | node generator.js
|
|
321
|
+
done
|
|
322
|
+
|
|
323
|
+
# Chapter-scoped events scaffolding
|
|
324
|
+
eventmodeler codegen events --chapter "Register Products" | node generate-events.js
|
|
325
|
+
|
|
326
|
+
# Whole-model events scaffolding
|
|
327
|
+
eventmodeler codegen events | node generate-events.js
|
|
328
|
+
\`\`\`
|
|
329
|
+
|
|
330
|
+
## Best Practices
|
|
331
|
+
|
|
332
|
+
1. **Handle all slice types** - Your generator should handle STATE_CHANGE, STATE_VIEW, and AUTOMATION
|
|
333
|
+
2. **Use field mappings** - They define the projection logic between events and read models
|
|
334
|
+
3. **Generate tests from scenarios** - Scenarios are executable specifications
|
|
335
|
+
4. **Respect field attributes** - \`generated\` fields don't need input, \`optional\` fields can be null
|
|
336
|
+
5. **Use aggregate info** - Events in aggregates should be handled by that aggregate
|
|
337
|
+
6. **Handle Custom types recursively** - Subfields can themselves be Custom types
|
|
338
|
+
7. **Make it idempotent** - Running twice should produce the same output
|
|
339
|
+
`;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const meta: {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
};
|
|
5
|
+
export declare const content = "\n# Connecting Slices\n\nAfter creating slices, you need to connect them with flows to show how data moves through the system. This creates the complete picture of your event model.\n\n## The Data Flow Pattern\n\nIn event modeling, data flows in a specific pattern:\n\n```\n[State-Change Slice] [State-View Slice] [Next Slice]\n\n Screen ReadModel Screen\n | ^ | ^\n v | v |\n Command | - - - - - - - - - - - - - +\n | | OR\n v | Processor\n Event - - - - - - - - - - - - - - - + ^\n |\n + - - - - - - - - - - -+\n```\n\n**Key flows:**\n1. **Event \u2192 ReadModel**: Events project their data into read models\n2. **ReadModel \u2192 Screen**: Read models provide data to screens (for user viewing)\n3. **ReadModel \u2192 Processor**: Read models provide data to processors (for automation)\n\nWithin-slice flows (Screen\u2192Command, Command\u2192Event, Processor\u2192Command) are created automatically when you create a slice.\n\n## Command Syntax\n\n```bash\neventmodeler create flow --from \"<source>\" --to \"<target>\"\n```\n\n## Valid Flow Combinations\n\n| Source | Target | Use Case |\n|--------|--------|----------|\n| Event | ReadModel | Project event data into a view |\n| ReadModel | Screen | Provide data for user display |\n| ReadModel | Processor | Provide data for automation logic |\n\n## Workflow: Connecting a Complete Model\n\n### 1. Create Your Slices First\n\n```bash\n# User places an order\neventmodeler create state-change-slice --xml '<state-change-slice name=\"Place Order\">\n <screen name=\"Checkout\">...</screen>\n <command name=\"PlaceOrder\">...</command>\n <event name=\"OrderPlaced\">...</event>\n</state-change-slice>'\n\n# View for order status\neventmodeler create state-view-slice --xml '<state-view-slice name=\"View Order Status\" after=\"Place Order\">\n <read-model name=\"OrderStatus\">...</read-model>\n</state-view-slice>'\n\n# System automatically fulfills order (includes its own read model)\neventmodeler create automation-slice --xml '<automation-slice name=\"Auto Fulfill\" after=\"View Order Status\">\n <read-model name=\"OrderReadyToFulfill\">\n <field name=\"orderId\" type=\"UUID\"/>\n <field name=\"isPaid\" type=\"Boolean\"/>\n </read-model>\n <processor name=\"Fulfillment Processor\"/>\n <command name=\"FulfillOrder\">...</command>\n <event name=\"OrderFulfilled\">...</event>\n</automation-slice>'\n```\n\n### 2. Connect Events to Read Models\n\nEvents project their data into read models:\n\n```bash\n# OrderPlaced feeds into OrderStatus view\neventmodeler create flow --from \"OrderPlaced\" --to \"OrderStatus\"\n\n# OrderFulfilled also updates OrderStatus view\neventmodeler create flow --from \"OrderFulfilled\" --to \"OrderStatus\"\n```\n\n### 3. Connect Events to Automation Read Models\n\nAutomation slices have their own internal read model. Connect external events to feed it:\n\n```bash\n# OrderPlaced feeds the automation's read model\neventmodeler create flow --from \"OrderPlaced\" --to \"OrderReadyToFulfill\"\n\n# PaymentReceived also feeds the automation's read model\neventmodeler create flow --from \"PaymentReceived\" --to \"OrderReadyToFulfill\"\n```\n\nNote: The flow from ReadModel \u2192 Processor is internal to the automation slice and created automatically.\n\n### 4. Connect Read Models to Screens\n\nState-view read models provide data to screens:\n\n```bash\n# OrderStatus provides data to a screen in another slice\neventmodeler create flow --from \"OrderStatus\" --to \"Order Details Screen\"\n```\n\n### 5. Set Up Field Mappings\n\nAfter creating flows, map the fields:\n\n```bash\neventmodeler map fields --flow \"OrderPlaced\u2192OrderStatus\" --xml '\n <mapping from=\"orderId\" to=\"orderId\"/>\n <mapping from=\"customerId\" to=\"customerId\"/>\n <mapping from=\"placedAt\" to=\"placedAt\"/>\n'\n```\n\n### 6. Verify Completeness\n\nCheck that all flows have proper field mappings:\n\n```bash\neventmodeler show model-completeness\neventmodeler show completeness \"OrderStatus\"\n```\n\n## Handling Duplicate Names with IDs\n\nWhen multiple elements share the same name (e.g., linked copies of events or read models), you'll need to use element IDs instead of names.\n\n### Finding Element IDs\n\n```bash\n# List events with their IDs\neventmodeler list events\n# Output:\n# <events>\n# <event id=\"abc12345-...\" name=\"OrderPlaced\" fields=\"4\"/>\n# <event id=\"def67890-...\" name=\"OrderPlaced\" fields=\"4\"/> <!-- linked copy -->\n# </events>\n```\n\n### Using IDs in Commands\n\nPrefix the ID (or ID prefix) with `id:`:\n\n```bash\n# Create flow using ID prefix (first 8 characters is usually enough)\neventmodeler create flow --from \"id:abc12345\" --to \"OrderStatus\"\n\n# Map fields using ID prefix\neventmodeler map fields --flow \"id:abc12345\u2192OrderStatus\" --xml '\n <mapping from=\"orderId\" to=\"orderId\"/>\n'\n```\n\n### When the CLI Finds Duplicates\n\nIf you use a name that matches multiple elements, the CLI will show you the options:\n\n```bash\neventmodeler create flow --from \"OrderPlaced\" --to \"OrderStatus\"\n# Error: Multiple events found with name \"OrderPlaced\"\n# Please specify using the element ID:\n# - \"OrderPlaced\" (id: abc12345-1234-5678-9abc-def012345678)\n# - \"OrderPlaced\" (id: def67890-1234-5678-9abc-def012345678)\n#\n# Usage: eventmodeler create flow --from \"id:abc12345\" --to \"OrderStatus\"\n```\n\n## Common Patterns\n\n### Pattern 1: Event Sourced View\nMultiple events feed into one read model:\n\n```bash\neventmodeler create flow --from \"OrderPlaced\" --to \"OrderSummary\"\neventmodeler create flow --from \"OrderShipped\" --to \"OrderSummary\"\neventmodeler create flow --from \"OrderDelivered\" --to \"OrderSummary\"\n```\n\n### Pattern 2: Automation Triggered by Events\nEvents feed an automation slice's internal read model:\n\n```bash\neventmodeler create flow --from \"OrderPlaced\" --to \"OrderReadyForShipment\"\neventmodeler create flow --from \"PaymentReceived\" --to \"OrderReadyForShipment\"\n```\n\n### Pattern 3: User Dashboard\nA read model feeds a screen for user viewing:\n\n```bash\neventmodeler create flow --from \"UserDashboard\" --to \"Dashboard Screen\"\n```\n\n## Error Handling\n\nThe CLI prevents invalid flows:\n\n```bash\n# This will error - can't go directly from Event to Screen\neventmodeler create flow --from \"OrderPlaced\" --to \"Checkout Screen\"\n# Error: Cannot create flow directly from Event to Screen.\n# Events flow to ReadModels, which then flow to Screens.\n\n# This will error - can't go from ReadModel to ReadModel\neventmodeler create flow --from \"OrderStatus\" --to \"CustomerProfile\"\n# Error: Cannot create flow from ReadModel to ReadModel.\n```\n\n## Best Practices\n\n1. **Create all slices first** - Easier to see the full picture before connecting\n2. **Connect events to read models** - Every event should feed at least one read model\n3. **Think about what triggers what** - Automation slices need data from read models\n4. **Map fields after creating flows** - Use `map fields` to complete the data flow\n5. **Verify completeness** - Run `show model-completeness` to find missing mappings\n6. **Use IDs for duplicates** - When names aren't unique, use `id:` prefix with element IDs\n";
|