@auto-engineer/server-generator-apollo-emmett 0.10.4 → 0.11.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.
Files changed (104) hide show
  1. package/.turbo/turbo-build.log +6 -0
  2. package/.turbo/turbo-format.log +5 -0
  3. package/.turbo/turbo-lint.log +4 -0
  4. package/.turbo/turbo-test.log +22 -0
  5. package/.turbo/turbo-type-check.log +5 -0
  6. package/CHANGELOG.md +20 -0
  7. package/dist/src/codegen/extract/events.d.ts +2 -2
  8. package/dist/src/codegen/extract/events.d.ts.map +1 -1
  9. package/dist/src/codegen/extract/events.js +16 -6
  10. package/dist/src/codegen/extract/events.js.map +1 -1
  11. package/dist/src/codegen/extract/gwt.js +7 -22
  12. package/dist/src/codegen/extract/gwt.js.map +1 -1
  13. package/dist/src/codegen/extract/imports.d.ts +29 -0
  14. package/dist/src/codegen/extract/imports.d.ts.map +1 -0
  15. package/dist/src/codegen/extract/imports.js +55 -0
  16. package/dist/src/codegen/extract/imports.js.map +1 -0
  17. package/dist/src/codegen/extract/index.d.ts +1 -0
  18. package/dist/src/codegen/extract/index.d.ts.map +1 -1
  19. package/dist/src/codegen/extract/index.js +1 -0
  20. package/dist/src/codegen/extract/index.js.map +1 -1
  21. package/dist/src/codegen/extract/messages.d.ts.map +1 -1
  22. package/dist/src/codegen/extract/messages.js +33 -7
  23. package/dist/src/codegen/extract/messages.js.map +1 -1
  24. package/dist/src/codegen/extract/query.d.ts +3 -1
  25. package/dist/src/codegen/extract/query.d.ts.map +1 -1
  26. package/dist/src/codegen/extract/query.js +12 -12
  27. package/dist/src/codegen/extract/query.js.map +1 -1
  28. package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
  29. package/dist/src/codegen/scaffoldFromSchema.js +9 -1
  30. package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
  31. package/dist/src/codegen/templates/command/decide.specs.specs.ts +235 -8
  32. package/dist/src/codegen/templates/command/decide.specs.ts +8 -8
  33. package/dist/src/codegen/templates/command/decide.specs.ts.ejs +95 -30
  34. package/dist/src/codegen/templates/command/decide.ts.ejs +2 -2
  35. package/dist/src/codegen/templates/command/events.ts.ejs +2 -2
  36. package/dist/src/codegen/templates/command/evolve.ts.ejs +3 -3
  37. package/dist/src/codegen/templates/command/handle.specs.ts +6 -6
  38. package/dist/src/codegen/templates/command/handle.ts.ejs +3 -3
  39. package/dist/src/codegen/templates/query/projection.specs.specs.ts +623 -0
  40. package/dist/src/codegen/templates/query/projection.specs.ts +1 -1
  41. package/dist/src/codegen/templates/query/projection.specs.ts.ejs +176 -52
  42. package/dist/src/codegen/templates/query/projection.ts.ejs +30 -29
  43. package/dist/src/codegen/templates/query/query.resolver.specs.ts +190 -5
  44. package/dist/src/codegen/templates/query/query.resolver.ts.ejs +31 -9
  45. package/dist/src/codegen/templates/react/react.specs.specs.ts +8 -5
  46. package/dist/src/codegen/templates/react/react.specs.ts +4 -4
  47. package/dist/src/codegen/templates/react/react.specs.ts.ejs +118 -67
  48. package/dist/src/codegen/templates/react/react.ts.ejs +4 -4
  49. package/dist/src/codegen/templates/react/register.specs.ts +2 -2
  50. package/dist/src/codegen/templates/react/register.ts.ejs +2 -2
  51. package/dist/src/codegen/types.d.ts +2 -0
  52. package/dist/src/codegen/types.d.ts.map +1 -1
  53. package/dist/src/commands/generate-server.d.ts.map +1 -1
  54. package/dist/src/commands/generate-server.js +3 -0
  55. package/dist/src/commands/generate-server.js.map +1 -1
  56. package/dist/src/domain/shared/ReadModel.d.ts +2 -2
  57. package/dist/src/domain/shared/ReadModel.d.ts.map +1 -1
  58. package/dist/src/domain/shared/ReadModel.js +2 -2
  59. package/dist/src/domain/shared/ReadModel.js.map +1 -1
  60. package/dist/src/domain/shared/ReadModel.ts +3 -3
  61. package/dist/src/domain/shared/types.d.ts +5 -3
  62. package/dist/src/domain/shared/types.d.ts.map +1 -1
  63. package/dist/src/domain/shared/types.js.map +1 -1
  64. package/dist/src/domain/shared/types.ts +5 -3
  65. package/dist/src/server.js +54 -7
  66. package/dist/src/server.js.map +1 -1
  67. package/dist/src/server.ts +53 -15
  68. package/dist/tsconfig.tsbuildinfo +1 -1
  69. package/package.json +8 -5
  70. package/src/codegen/extract/events.ts +20 -3
  71. package/src/codegen/extract/gwt.ts +10 -26
  72. package/src/codegen/extract/imports.ts +71 -0
  73. package/src/codegen/extract/index.ts +1 -0
  74. package/src/codegen/extract/messages.ts +34 -7
  75. package/src/codegen/extract/query.ts +17 -19
  76. package/src/codegen/scaffoldFromSchema.ts +13 -0
  77. package/src/codegen/templates/command/decide.specs.specs.ts +235 -8
  78. package/src/codegen/templates/command/decide.specs.ts +8 -8
  79. package/src/codegen/templates/command/decide.specs.ts.ejs +95 -30
  80. package/src/codegen/templates/command/decide.ts.ejs +2 -2
  81. package/src/codegen/templates/command/events.ts.ejs +2 -2
  82. package/src/codegen/templates/command/evolve.ts.ejs +3 -3
  83. package/src/codegen/templates/command/handle.specs.ts +6 -6
  84. package/src/codegen/templates/command/handle.ts.ejs +3 -3
  85. package/src/codegen/templates/query/projection.specs.specs.ts +623 -0
  86. package/src/codegen/templates/query/projection.specs.ts +1 -1
  87. package/src/codegen/templates/query/projection.specs.ts.ejs +176 -52
  88. package/src/codegen/templates/query/projection.ts.ejs +30 -29
  89. package/src/codegen/templates/query/query.resolver.specs.ts +190 -5
  90. package/src/codegen/templates/query/query.resolver.ts.ejs +31 -9
  91. package/src/codegen/templates/react/react.specs.specs.ts +8 -5
  92. package/src/codegen/templates/react/react.specs.ts +4 -4
  93. package/src/codegen/templates/react/react.specs.ts.ejs +118 -67
  94. package/src/codegen/templates/react/react.ts.ejs +4 -4
  95. package/src/codegen/templates/react/register.specs.ts +2 -2
  96. package/src/codegen/templates/react/register.ts.ejs +2 -2
  97. package/src/codegen/types.ts +2 -0
  98. package/src/commands/generate-server.ts +3 -0
  99. package/src/domain/shared/ReadModel.ts +3 -3
  100. package/src/domain/shared/types.ts +5 -3
  101. package/src/server.ts +53 -15
  102. package/dist/src/codegen/templates/query/projection.specs.specs..ts +0 -296
  103. package/src/codegen/scaffoldFromSchema.query-slice-register.specs.ts +0 -179
  104. package/src/codegen/templates/query/projection.specs.specs..ts +0 -296
package/package.json CHANGED
@@ -11,8 +11,10 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@event-driven-io/emmett": "^0.38.2",
14
+ "@event-driven-io/emmett-sqlite": "^0.38.5",
14
15
  "apollo-server": "^3.13.0",
15
16
  "apollo-server-express": "^3.13.0",
17
+ "better-sqlite3": "^12.4.1",
16
18
  "change-case": "^5.4.4",
17
19
  "ejs": "^3.1.10",
18
20
  "execa": "^9.6.0",
@@ -23,13 +25,14 @@
23
25
  "graphql-scalars": "^1.24.2",
24
26
  "prettier": "^3.6.1",
25
27
  "reflect-metadata": "^0.2.2",
28
+ "sqlite3": "^5.1.7",
26
29
  "type-fest": "^4.41.0",
27
30
  "type-graphql": "^2.0.0-rc.2",
28
31
  "graphql-type-json": "^0.3.2",
29
- "uuid": "^10.0.0",
32
+ "uuid": "^11.0.0",
30
33
  "web-streams-polyfill": "^4.1.0",
31
- "@auto-engineer/message-bus": "0.10.4",
32
- "@auto-engineer/flow": "0.10.4"
34
+ "@auto-engineer/flow": "0.11.0",
35
+ "@auto-engineer/message-bus": "0.11.0"
33
36
  },
34
37
  "publishConfig": {
35
38
  "access": "public"
@@ -37,9 +40,9 @@
37
40
  "devDependencies": {
38
41
  "@types/ejs": "^3.1.5",
39
42
  "@types/fs-extra": "^11.0.4",
40
- "@auto-engineer/cli": "0.10.4"
43
+ "@auto-engineer/cli": "0.11.0"
41
44
  },
42
- "version": "0.10.4",
45
+ "version": "0.11.0",
43
46
  "scripts": {
44
47
  "generate:server": "tsx src/cli/index.ts",
45
48
  "build": "tsc && tsx ../../scripts/fix-esm-imports.ts && rm -rf dist/src/codegen/templates && mkdir -p dist/src/codegen && cp -r src/codegen/templates dist/src/codegen/templates && cp src/server.ts dist/src && cp -r src/utils dist/src && cp -r src/domain dist/src",
@@ -7,20 +7,35 @@ function createEventMessage(
7
7
  eventRef: string | undefined,
8
8
  source: 'given' | 'then' | 'when',
9
9
  allMessages: MessageDefinition[],
10
+ currentSliceName?: string,
11
+ currentFlowName?: string,
10
12
  ): Message | undefined {
11
13
  if (eventRef == null) return undefined;
12
14
  const fields = extractFieldsFromMessage(eventRef, 'event', allMessages);
13
- return { type: eventRef, fields, source };
15
+ const messageDef = allMessages.find((m) => m.type === 'event' && m.name === eventRef);
16
+ const metadata = messageDef?.metadata as { sourceFlowName?: string; sourceSliceName?: string } | undefined;
17
+ const sourceFlowName = metadata?.sourceFlowName ?? currentFlowName;
18
+ const sourceSliceName = metadata?.sourceSliceName ?? currentSliceName;
19
+
20
+ return {
21
+ type: eventRef,
22
+ fields,
23
+ source,
24
+ sourceFlowName,
25
+ sourceSliceName,
26
+ };
14
27
  }
15
28
 
16
29
  export function extractEventsFromThen(
17
30
  thenItems: Array<EventExample | { errorType: string; message?: string }>,
18
31
  allMessages: MessageDefinition[],
32
+ currentSliceName?: string,
33
+ currentFlowName?: string,
19
34
  ): Message[] {
20
35
  return thenItems
21
36
  .map((then): Message | undefined => {
22
37
  if (!('eventRef' in then)) return undefined;
23
- return createEventMessage(then.eventRef, 'then', allMessages);
38
+ return createEventMessage(then.eventRef, 'then', allMessages, currentSliceName, currentFlowName);
24
39
  })
25
40
  .filter((event): event is Message => event !== undefined);
26
41
  }
@@ -28,11 +43,13 @@ export function extractEventsFromThen(
28
43
  export function extractEventsFromGiven(
29
44
  givenEvents: EventExample[] | undefined,
30
45
  allMessages: MessageDefinition[],
46
+ currentSliceName?: string,
47
+ currentFlowName?: string,
31
48
  ): Message[] {
32
49
  if (!givenEvents) return [];
33
50
 
34
51
  return givenEvents
35
- .map((given) => createEventMessage(given.eventRef, 'given', allMessages))
52
+ .map((given) => createEventMessage(given.eventRef, 'given', allMessages, currentSliceName, currentFlowName))
36
53
  .filter((event): event is Message => event !== undefined);
37
54
  }
38
55
 
@@ -21,12 +21,16 @@ function extractGwtSpecs(slice: Slice) {
21
21
  given: example.given,
22
22
  when: example.when,
23
23
  then: example.then,
24
+ description: example.description,
25
+ ruleDescription: rule.description,
24
26
  })),
25
27
  )
26
28
  : [];
27
29
  }
28
30
 
29
- function buildCommandMapping(gwtSpecs: Array<{ given: unknown; when: unknown; then: unknown }>) {
31
+ function buildCommandMapping(
32
+ gwtSpecs: Array<{ given: unknown; when: unknown; then: unknown; description?: string; ruleDescription?: string }>,
33
+ ) {
30
34
  const mapping: Record<string, GwtCondition[]> = {};
31
35
 
32
36
  for (const gwt of gwtSpecs) {
@@ -43,6 +47,8 @@ function buildCommandMapping(gwtSpecs: Array<{ given: unknown; when: unknown; th
43
47
  given: gwt.given as Array<EventExample | StateExample> | undefined,
44
48
  when: gwt.when as CommandExample | EventExample[],
45
49
  then: gwt.then as Array<EventExample | StateExample | CommandExample | { errorType: string; message?: string }>,
50
+ description: gwt.description,
51
+ ruleDescription: gwt.ruleDescription,
46
52
  });
47
53
  }
48
54
  }
@@ -54,10 +60,10 @@ function enhanceMapping(mapping: Record<string, GwtCondition[]>) {
54
60
  const enhancedMapping: Record<string, (GwtCondition & { failingFields?: string[] })[]> = {};
55
61
 
56
62
  for (const command in mapping) {
57
- const merged = mergeGwtConditions(mapping[command]);
58
- const successfulData = findSuccessfulExampleData(merged);
63
+ const conditions = mapping[command];
64
+ const successfulData = findSuccessfulExampleData(conditions);
59
65
 
60
- enhancedMapping[command] = merged.map((gwt) => ({
66
+ enhancedMapping[command] = conditions.map((gwt) => ({
61
67
  ...gwt,
62
68
  failingFields: findFailingFields(gwt, successfulData),
63
69
  }));
@@ -66,28 +72,6 @@ function enhanceMapping(mapping: Record<string, GwtCondition[]>) {
66
72
  return enhancedMapping;
67
73
  }
68
74
 
69
- function mergeGwtConditions(gwts: GwtCondition[]): GwtCondition[] {
70
- const map = new Map<string, GwtCondition[]>();
71
-
72
- for (const gwt of gwts) {
73
- // Handle both single command and array of events in when clause
74
- const whenData = Array.isArray(gwt.when) ? gwt.when[0]?.exampleData : gwt.when.exampleData;
75
- const key = JSON.stringify(whenData ?? {});
76
- const existing = map.get(key) ?? [];
77
- map.set(key, [...existing, gwt]);
78
- }
79
-
80
- return Array.from(map.values()).map((conditions) => {
81
- const first = conditions[0];
82
- const combinedThen = conditions.flatMap((g) => g.then);
83
- return {
84
- given: conditions.flatMap((g) => g.given ?? []),
85
- when: first.when,
86
- then: combinedThen,
87
- };
88
- });
89
- }
90
-
91
75
  function findSuccessfulExampleData(gwts: GwtCondition[]): Record<string, unknown> {
92
76
  const successful = gwts.find((gwt) => gwt.then.some((t) => typeof t === 'object' && t !== null && 'eventRef' in t));
93
77
  const whenData = Array.isArray(successful?.when) ? successful?.when[0]?.exampleData : successful?.when?.exampleData;
@@ -0,0 +1,71 @@
1
+ import { toKebabCase } from '../utils/path';
2
+ import { Message } from '../types';
3
+
4
+ export interface ImportGroup {
5
+ importPath: string;
6
+ eventTypes: string[];
7
+ }
8
+
9
+ export interface CrossSliceImportContext {
10
+ currentSliceName: string;
11
+ events: Message[];
12
+ }
13
+
14
+ /**
15
+ * Groups events by their import paths, handling cross-slice imports correctly.
16
+ * Events from the current slice are imported from './events',
17
+ * while cross-slice events are imported from '../other-slice/events'.
18
+ */
19
+ export function groupEventImports(context: CrossSliceImportContext): ImportGroup[] {
20
+ const { currentSliceName, events } = context;
21
+ const importGroups = new Map<string, string[]>();
22
+
23
+ for (const event of events) {
24
+ if (!event.type) continue;
25
+ let importPath: string;
26
+ const isFromCurrentSlice =
27
+ event.source === 'then' || event.sourceSliceName === currentSliceName || event.sourceSliceName == null;
28
+
29
+ if (isFromCurrentSlice) {
30
+ importPath = './events';
31
+ } else {
32
+ importPath = `../${toKebabCase(event.sourceSliceName ?? currentSliceName)}/events`;
33
+ }
34
+ if (!importGroups.has(importPath)) {
35
+ importGroups.set(importPath, []);
36
+ }
37
+ const eventTypes = importGroups.get(importPath)!;
38
+ if (!eventTypes.includes(event.type)) {
39
+ eventTypes.push(event.type);
40
+ }
41
+ }
42
+ return Array.from(importGroups.entries()).map(([importPath, eventTypes]) => ({
43
+ importPath,
44
+ eventTypes: eventTypes.sort(),
45
+ }));
46
+ }
47
+
48
+ /**
49
+ * Filters events to only include those from the current slice (source === 'then').
50
+ * Used for generating local event definitions.
51
+ */
52
+ export function getLocalEvents(events: Message[]): Message[] {
53
+ return events.filter((event) => event.source === 'then');
54
+ }
55
+
56
+ /**
57
+ * Extracts all unique event types from a list of events.
58
+ */
59
+ export function getAllEventTypes(events: Message[]): string[] {
60
+ const eventTypes = events.map((event) => event.type).filter((type): type is string => Boolean(type));
61
+
62
+ return Array.from(new Set(eventTypes)).sort();
63
+ }
64
+
65
+ /**
66
+ * Creates a TypeScript union type string from event types.
67
+ */
68
+ export function createEventUnionType(events: Message[]): string {
69
+ const eventTypes = getAllEventTypes(events);
70
+ return eventTypes.length > 0 ? eventTypes.join(' | ') : 'never';
71
+ }
@@ -5,3 +5,4 @@ export * from './states';
5
5
  export * from './messages';
6
6
  export * from './query';
7
7
  export * from './projection';
8
+ export * from './imports';
@@ -2,6 +2,7 @@ import { extractCommandsFromGwt, extractCommandsFromThen } from './commands';
2
2
  import { CommandExample, ErrorExample, EventExample, Slice, StateExample } from '@auto-engineer/flow';
3
3
  import { Message, MessageDefinition } from '../types';
4
4
  import { extractEventsFromGiven, extractEventsFromThen, extractEventsFromWhen } from './events';
5
+ import { extractFieldsFromMessage } from './fields';
5
6
  import { extractProjectionIdField } from './projection';
6
7
  import { extractStatesFromData, extractStatesFromTarget } from './states';
7
8
  import createDebug from 'debug';
@@ -87,10 +88,10 @@ function extractMessagesForCommand(slice: Slice, allMessages: MessageDefinition[
87
88
 
88
89
  const events: Message[] = gwtSpecs.flatMap((gwt): Message[] => {
89
90
  const givenEventsOnly = gwt.given?.filter((item): item is EventExample => 'eventRef' in item);
90
- const givenEvents = extractEventsFromGiven(givenEventsOnly, allMessages);
91
+ const givenEvents = extractEventsFromGiven(givenEventsOnly, allMessages, slice.name);
91
92
 
92
93
  const thenEventsOnly = gwt.then.filter((item): item is EventExample => 'eventRef' in item);
93
- const thenEvents = extractEventsFromThen(thenEventsOnly, allMessages);
94
+ const thenEvents = extractEventsFromThen(thenEventsOnly, allMessages, slice.name);
94
95
  debugCommand(' GWT: given=%d events, then=%d events', givenEvents.length, thenEvents.length);
95
96
  return [...givenEvents, ...thenEvents];
96
97
  });
@@ -133,12 +134,39 @@ function extractMessagesForQuery(slice: Slice, allMessages: MessageDefinition[])
133
134
  debugQuery(' Projection ID field: %s', projectionIdField ?? 'none');
134
135
 
135
136
  const events: Message[] = gwtSpecs.flatMap((gwt) => {
136
- const eventsFromWhen = Array.isArray(gwt.when)
137
- ? gwt.when.filter((item): item is EventExample => 'eventRef' in item)
137
+ const eventsFromGiven = Array.isArray(gwt.given)
138
+ ? gwt.given.filter((item): item is EventExample => 'eventRef' in item)
138
139
  : [];
139
- return extractEventsFromGiven(eventsFromWhen, allMessages);
140
+ let eventsFromWhen: EventExample[] = [];
141
+ if (Array.isArray(gwt.when)) {
142
+ eventsFromWhen = gwt.when.filter((item): item is EventExample => 'eventRef' in item);
143
+ } else if (gwt.when != null && typeof gwt.when === 'object' && 'eventRef' in gwt.when) {
144
+ // when is a single object, convert to array
145
+ const whenItem = gwt.when as EventExample;
146
+ if (whenItem.eventRef && whenItem.eventRef.trim() !== '') {
147
+ eventsFromWhen = [whenItem];
148
+ }
149
+ }
150
+ const givenEvents = extractEventsFromGiven(eventsFromGiven, allMessages);
151
+ const whenEvents = eventsFromWhen
152
+ .map((eventExample) => {
153
+ const fields = extractFieldsFromMessage(eventExample.eventRef, 'event', allMessages);
154
+ const messageDef = allMessages.find((m) => m.type === 'event' && m.name === eventExample.eventRef);
155
+ const metadata = messageDef?.metadata as { sourceFlowName?: string; sourceSliceName?: string } | undefined;
156
+
157
+ return {
158
+ type: eventExample.eventRef,
159
+ fields,
160
+ source: 'when' as const, // Mark as 'when' source for query events
161
+ sourceFlowName: metadata?.sourceFlowName,
162
+ sourceSliceName: metadata?.sourceSliceName,
163
+ } as Message;
164
+ })
165
+ .filter((event): event is Message => event.type !== undefined);
166
+
167
+ return [...givenEvents, ...whenEvents];
140
168
  });
141
- debugQuery(' Extracted %d events from given', events.length);
169
+ debugQuery(' Extracted %d events total', events.length);
142
170
 
143
171
  const states: Message[] = extractStatesFromTarget(slice, allMessages);
144
172
  debugQuery(' Extracted %d states from target', states.length);
@@ -164,7 +192,6 @@ function extractMessagesForReact(slice: Slice, allMessages: MessageDefinition[])
164
192
  }
165
193
 
166
194
  const specs = slice.server?.specs;
167
- // Extract all examples from specs/rules structure
168
195
  const rules = specs?.rules;
169
196
  const gwtSpecs =
170
197
  Array.isArray(rules) && rules.length > 0
@@ -1,31 +1,29 @@
1
1
  import { EventExample, Slice } from '@auto-engineer/flow';
2
2
 
3
3
  interface QueryGwtCondition {
4
- given: EventExample[];
4
+ description: string;
5
+ given?: EventExample[];
6
+ when: EventExample[];
5
7
  then: Array<{ stateRef: string; exampleData: Record<string, unknown> }>;
6
8
  }
7
9
 
8
10
  export function buildQueryGwtMapping(slice: Slice): QueryGwtCondition[] {
9
- if (slice.type !== 'query') {
10
- return [];
11
- }
11
+ if (slice.type !== 'query') return [];
12
12
 
13
13
  const specs = slice.server?.specs;
14
14
  const rules = specs?.rules;
15
- const gwtSpecs =
16
- Array.isArray(rules) && rules.length > 0
17
- ? rules.flatMap((rule) =>
18
- rule.examples.map((example) => ({
19
- given: Array.isArray(example.when) ? example.when : [], // For query slices, when contains the given events
20
- then: example.then,
21
- })),
22
- )
23
- : [];
24
15
 
25
- return gwtSpecs.map((gwt) => ({
26
- given: gwt.given.filter((item): item is EventExample => 'eventRef' in item),
27
- then: gwt.then.filter(
28
- (item): item is { stateRef: string; exampleData: Record<string, unknown> } => 'stateRef' in item,
29
- ),
30
- }));
16
+ const examples = Array.isArray(rules) && rules.length > 0 ? rules.flatMap((rule) => rule.examples) : [];
17
+
18
+ return examples.map((ex) => {
19
+ const givenEvents = Array.isArray(ex.given) ? ex.given.filter((i): i is EventExample => 'eventRef' in i) : [];
20
+ const whenEvents = Array.isArray(ex.when) ? ex.when.filter((i): i is EventExample => 'eventRef' in i) : [];
21
+
22
+ return {
23
+ description: ex.description,
24
+ given: givenEvents.length > 0 ? givenEvents : undefined,
25
+ when: whenEvents,
26
+ then: ex.then.filter((i): i is { stateRef: string; exampleData: Record<string, unknown> } => 'stateRef' in i),
27
+ };
28
+ });
31
29
  }
@@ -21,6 +21,10 @@ import {
21
21
  buildQueryGwtMapping,
22
22
  extractMessagesFromSpecs,
23
23
  extractProjectionName,
24
+ groupEventImports,
25
+ getAllEventTypes,
26
+ getLocalEvents,
27
+ createEventUnionType,
24
28
  } from './extract';
25
29
 
26
30
  function extractGwtSpecs(slice: Slice) {
@@ -273,6 +277,11 @@ async function prepareTemplateData(
273
277
  const filteredCommands =
274
278
  allowedForSlice.size > 0 ? uniqueCommands.filter((c) => allowedForSlice.has(c.type)) : uniqueCommands;
275
279
 
280
+ const eventImportGroups = groupEventImports({ currentSliceName: slice.name, events });
281
+ const allEventTypesArray = getAllEventTypes(events);
282
+ const allEventTypes = createEventUnionType(events);
283
+ const localEvents = getLocalEvents(events);
284
+
276
285
  return {
277
286
  flowName: flow.name,
278
287
  sliceName: slice.name,
@@ -295,6 +304,10 @@ async function prepareTemplateData(
295
304
  ? allMessages.find((m) => m.name === slice.server?.data?.[0]?.target?.name)
296
305
  : undefined,
297
306
  integrations,
307
+ eventImportGroups,
308
+ allEventTypes,
309
+ allEventTypesArray,
310
+ localEvents,
298
311
  };
299
312
  }
300
313
 
@@ -99,16 +99,20 @@ describe('spec.ts.ejs', () => {
99
99
  import { DeciderSpecification } from '@event-driven-io/emmett';
100
100
  import { decide } from './decide';
101
101
  import { evolve } from './evolve';
102
- import { initialState } from './state';
102
+ import { initialState, State } from './state';
103
+ import type { ListingCreated } from './events';
104
+ import type { CreateListing } from './commands';
103
105
 
104
- describe('Host creates a listing | Create listing', () => {
105
- const given = DeciderSpecification.for({
106
+ describe('Should create listing successfully', () => {
107
+ type Events = ListingCreated;
108
+
109
+ const given = DeciderSpecification.for<CreateListing, Events, State>({
106
110
  decide,
107
111
  evolve,
108
112
  initialState,
109
113
  });
110
114
 
111
- it('should emit ListingCreated for valid CreateListing', () => {
115
+ it('User creates listing with valid data', () => {
112
116
  given([])
113
117
  .when({
114
118
  type: 'CreateListing',
@@ -237,16 +241,20 @@ describe('spec.ts.ejs', () => {
237
241
  import { DeciderSpecification } from '@event-driven-io/emmett';
238
242
  import { decide } from './decide';
239
243
  import { evolve } from './evolve';
240
- import { initialState } from './state';
244
+ import { initialState, State } from './state';
245
+ import type { ListingCreated, ListingRemoved } from './events';
246
+ import type { RemoveListing } from './commands';
247
+
248
+ describe('Should remove existing listing', () => {
249
+ type Events = ListingCreated | ListingRemoved;
241
250
 
242
- describe('Guest removes a listing | Remove listing', () => {
243
- const given = DeciderSpecification.for({
251
+ const given = DeciderSpecification.for<RemoveListing, Events, State>({
244
252
  decide,
245
253
  evolve,
246
254
  initialState,
247
255
  });
248
256
 
249
- it('should emit ListingRemoved for valid RemoveListing', () => {
257
+ it('Existing listing can be removed', () => {
250
258
  given([
251
259
  {
252
260
  type: 'ListingCreated',
@@ -280,4 +288,223 @@ describe('spec.ts.ejs', () => {
280
288
  "
281
289
  `);
282
290
  });
291
+
292
+ it('should generate separate tests for multiple examples with different scenarios', async () => {
293
+ const spec: SpecsSchema = {
294
+ variant: 'specs',
295
+ flows: [
296
+ {
297
+ name: 'Questionnaires',
298
+ slices: [
299
+ {
300
+ type: 'command',
301
+ name: 'submits a questionnaire answer',
302
+ client: { description: '' },
303
+ server: {
304
+ description: '',
305
+ specs: {
306
+ name: 'Answer question spec',
307
+ rules: [
308
+ {
309
+ description: 'answers are allowed while the questionnaire has not been submitted',
310
+ examples: [
311
+ {
312
+ description: 'no questions have been answered yet',
313
+ when: {
314
+ commandRef: 'AnswerQuestion',
315
+ exampleData: {
316
+ questionnaireId: 'q-001',
317
+ participantId: 'participant-abc',
318
+ questionId: 'q1',
319
+ answer: 'Yes',
320
+ },
321
+ },
322
+ then: [
323
+ {
324
+ eventRef: 'QuestionAnswered',
325
+ exampleData: {
326
+ questionnaireId: 'q-001',
327
+ participantId: 'participant-abc',
328
+ questionId: 'q1',
329
+ answer: 'Yes',
330
+ savedAt: '2030-01-01T09:05:00.000Z',
331
+ },
332
+ },
333
+ ],
334
+ },
335
+ {
336
+ description: 'all questions have already been answered and submitted',
337
+ given: [
338
+ {
339
+ eventRef: 'QuestionnaireSubmitted',
340
+ exampleData: {
341
+ questionnaireId: 'q-001',
342
+ participantId: 'participant-abc',
343
+ submittedAt: '2030-01-01T09:00:00.000Z',
344
+ },
345
+ },
346
+ ],
347
+ when: {
348
+ commandRef: 'AnswerQuestion',
349
+ exampleData: {
350
+ questionnaireId: 'q-001',
351
+ participantId: 'participant-abc',
352
+ questionId: 'q1',
353
+ answer: 'Yes',
354
+ },
355
+ },
356
+ then: [
357
+ {
358
+ eventRef: 'QuestionnaireEditRejected',
359
+ exampleData: {
360
+ questionnaireId: 'q-001',
361
+ participantId: 'participant-abc',
362
+ reason: 'Questionnaire already submitted',
363
+ attemptedAt: '2030-01-01T09:05:00.000Z',
364
+ },
365
+ },
366
+ ],
367
+ },
368
+ ],
369
+ },
370
+ ],
371
+ },
372
+ },
373
+ },
374
+ ],
375
+ },
376
+ ],
377
+ messages: [
378
+ {
379
+ type: 'command',
380
+ name: 'AnswerQuestion',
381
+ fields: [
382
+ { name: 'questionnaireId', type: 'string', required: true },
383
+ { name: 'participantId', type: 'string', required: true },
384
+ { name: 'questionId', type: 'string', required: true },
385
+ { name: 'answer', type: 'unknown', required: true },
386
+ ],
387
+ },
388
+ {
389
+ type: 'event',
390
+ name: 'QuestionAnswered',
391
+ source: 'internal',
392
+ fields: [
393
+ { name: 'questionnaireId', type: 'string', required: true },
394
+ { name: 'participantId', type: 'string', required: true },
395
+ { name: 'questionId', type: 'string', required: true },
396
+ { name: 'answer', type: 'unknown', required: true },
397
+ { name: 'savedAt', type: 'Date', required: true },
398
+ ],
399
+ },
400
+ {
401
+ type: 'event',
402
+ name: 'QuestionnaireSubmitted',
403
+ source: 'internal',
404
+ fields: [
405
+ { name: 'questionnaireId', type: 'string', required: true },
406
+ { name: 'participantId', type: 'string', required: true },
407
+ { name: 'submittedAt', type: 'Date', required: true },
408
+ ],
409
+ },
410
+ {
411
+ type: 'event',
412
+ name: 'QuestionnaireEditRejected',
413
+ source: 'internal',
414
+ fields: [
415
+ { name: 'questionnaireId', type: 'string', required: true },
416
+ { name: 'participantId', type: 'string', required: true },
417
+ { name: 'reason', type: 'string', required: true },
418
+ { name: 'attemptedAt', type: 'Date', required: true },
419
+ ],
420
+ },
421
+ ],
422
+ };
423
+
424
+ const plans = await generateScaffoldFilePlans(spec.flows, spec.messages, undefined, 'src/domain/flows');
425
+ const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
426
+
427
+ expect(specFile?.contents).toMatchInlineSnapshot(`
428
+ "import { describe, it } from 'vitest';
429
+ import { DeciderSpecification } from '@event-driven-io/emmett';
430
+ import { decide } from './decide';
431
+ import { evolve } from './evolve';
432
+ import { initialState, State } from './state';
433
+ import type { QuestionAnswered, QuestionnaireEditRejected, QuestionnaireSubmitted } from './events';
434
+ import type { AnswerQuestion } from './commands';
435
+
436
+ describe('answers are allowed while the questionnaire has not been submitted', () => {
437
+ type Events = QuestionAnswered | QuestionnaireEditRejected | QuestionnaireSubmitted;
438
+
439
+ const given = DeciderSpecification.for<AnswerQuestion, Events, State>({
440
+ decide,
441
+ evolve,
442
+ initialState,
443
+ });
444
+
445
+ it('no questions have been answered yet', () => {
446
+ given([])
447
+ .when({
448
+ type: 'AnswerQuestion',
449
+ data: {
450
+ questionnaireId: 'q-001',
451
+ participantId: 'participant-abc',
452
+ questionId: 'q1',
453
+ answer: 'Yes',
454
+ },
455
+ metadata: { now: new Date() },
456
+ })
457
+
458
+ .then([
459
+ {
460
+ type: 'QuestionAnswered',
461
+ data: {
462
+ questionnaireId: 'q-001',
463
+ participantId: 'participant-abc',
464
+ questionId: 'q1',
465
+ answer: 'Yes',
466
+ savedAt: new Date('2030-01-01T09:05:00.000Z'),
467
+ },
468
+ },
469
+ ]);
470
+ });
471
+
472
+ it('all questions have already been answered and submitted', () => {
473
+ given([
474
+ {
475
+ type: 'QuestionnaireSubmitted',
476
+ data: {
477
+ questionnaireId: 'q-001',
478
+ participantId: 'participant-abc',
479
+ submittedAt: new Date('2030-01-01T09:00:00.000Z'),
480
+ },
481
+ },
482
+ ])
483
+ .when({
484
+ type: 'AnswerQuestion',
485
+ data: {
486
+ questionnaireId: 'q-001',
487
+ participantId: 'participant-abc',
488
+ questionId: 'q1',
489
+ answer: 'Yes',
490
+ },
491
+ metadata: { now: new Date() },
492
+ })
493
+
494
+ .then([
495
+ {
496
+ type: 'QuestionnaireEditRejected',
497
+ data: {
498
+ questionnaireId: 'q-001',
499
+ participantId: 'participant-abc',
500
+ reason: 'Questionnaire already submitted',
501
+ attemptedAt: new Date('2030-01-01T09:05:00.000Z'),
502
+ },
503
+ },
504
+ ]);
505
+ });
506
+ });
507
+ "
508
+ `);
509
+ });
283
510
  });