@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.
- package/.turbo/turbo-build.log +6 -0
- package/.turbo/turbo-format.log +5 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +22 -0
- package/.turbo/turbo-type-check.log +5 -0
- package/CHANGELOG.md +20 -0
- package/dist/src/codegen/extract/events.d.ts +2 -2
- package/dist/src/codegen/extract/events.d.ts.map +1 -1
- package/dist/src/codegen/extract/events.js +16 -6
- package/dist/src/codegen/extract/events.js.map +1 -1
- package/dist/src/codegen/extract/gwt.js +7 -22
- package/dist/src/codegen/extract/gwt.js.map +1 -1
- package/dist/src/codegen/extract/imports.d.ts +29 -0
- package/dist/src/codegen/extract/imports.d.ts.map +1 -0
- package/dist/src/codegen/extract/imports.js +55 -0
- package/dist/src/codegen/extract/imports.js.map +1 -0
- package/dist/src/codegen/extract/index.d.ts +1 -0
- package/dist/src/codegen/extract/index.d.ts.map +1 -1
- package/dist/src/codegen/extract/index.js +1 -0
- package/dist/src/codegen/extract/index.js.map +1 -1
- package/dist/src/codegen/extract/messages.d.ts.map +1 -1
- package/dist/src/codegen/extract/messages.js +33 -7
- package/dist/src/codegen/extract/messages.js.map +1 -1
- package/dist/src/codegen/extract/query.d.ts +3 -1
- package/dist/src/codegen/extract/query.d.ts.map +1 -1
- package/dist/src/codegen/extract/query.js +12 -12
- package/dist/src/codegen/extract/query.js.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +9 -1
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +235 -8
- package/dist/src/codegen/templates/command/decide.specs.ts +8 -8
- package/dist/src/codegen/templates/command/decide.specs.ts.ejs +95 -30
- package/dist/src/codegen/templates/command/decide.ts.ejs +2 -2
- package/dist/src/codegen/templates/command/events.ts.ejs +2 -2
- package/dist/src/codegen/templates/command/evolve.ts.ejs +3 -3
- package/dist/src/codegen/templates/command/handle.specs.ts +6 -6
- package/dist/src/codegen/templates/command/handle.ts.ejs +3 -3
- package/dist/src/codegen/templates/query/projection.specs.specs.ts +623 -0
- package/dist/src/codegen/templates/query/projection.specs.ts +1 -1
- package/dist/src/codegen/templates/query/projection.specs.ts.ejs +176 -52
- package/dist/src/codegen/templates/query/projection.ts.ejs +30 -29
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +190 -5
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +31 -9
- package/dist/src/codegen/templates/react/react.specs.specs.ts +8 -5
- package/dist/src/codegen/templates/react/react.specs.ts +4 -4
- package/dist/src/codegen/templates/react/react.specs.ts.ejs +118 -67
- package/dist/src/codegen/templates/react/react.ts.ejs +4 -4
- package/dist/src/codegen/templates/react/register.specs.ts +2 -2
- package/dist/src/codegen/templates/react/register.ts.ejs +2 -2
- package/dist/src/codegen/types.d.ts +2 -0
- package/dist/src/codegen/types.d.ts.map +1 -1
- package/dist/src/commands/generate-server.d.ts.map +1 -1
- package/dist/src/commands/generate-server.js +3 -0
- package/dist/src/commands/generate-server.js.map +1 -1
- package/dist/src/domain/shared/ReadModel.d.ts +2 -2
- package/dist/src/domain/shared/ReadModel.d.ts.map +1 -1
- package/dist/src/domain/shared/ReadModel.js +2 -2
- package/dist/src/domain/shared/ReadModel.js.map +1 -1
- package/dist/src/domain/shared/ReadModel.ts +3 -3
- package/dist/src/domain/shared/types.d.ts +5 -3
- package/dist/src/domain/shared/types.d.ts.map +1 -1
- package/dist/src/domain/shared/types.js.map +1 -1
- package/dist/src/domain/shared/types.ts +5 -3
- package/dist/src/server.js +54 -7
- package/dist/src/server.js.map +1 -1
- package/dist/src/server.ts +53 -15
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -5
- package/src/codegen/extract/events.ts +20 -3
- package/src/codegen/extract/gwt.ts +10 -26
- package/src/codegen/extract/imports.ts +71 -0
- package/src/codegen/extract/index.ts +1 -0
- package/src/codegen/extract/messages.ts +34 -7
- package/src/codegen/extract/query.ts +17 -19
- package/src/codegen/scaffoldFromSchema.ts +13 -0
- package/src/codegen/templates/command/decide.specs.specs.ts +235 -8
- package/src/codegen/templates/command/decide.specs.ts +8 -8
- package/src/codegen/templates/command/decide.specs.ts.ejs +95 -30
- package/src/codegen/templates/command/decide.ts.ejs +2 -2
- package/src/codegen/templates/command/events.ts.ejs +2 -2
- package/src/codegen/templates/command/evolve.ts.ejs +3 -3
- package/src/codegen/templates/command/handle.specs.ts +6 -6
- package/src/codegen/templates/command/handle.ts.ejs +3 -3
- package/src/codegen/templates/query/projection.specs.specs.ts +623 -0
- package/src/codegen/templates/query/projection.specs.ts +1 -1
- package/src/codegen/templates/query/projection.specs.ts.ejs +176 -52
- package/src/codegen/templates/query/projection.ts.ejs +30 -29
- package/src/codegen/templates/query/query.resolver.specs.ts +190 -5
- package/src/codegen/templates/query/query.resolver.ts.ejs +31 -9
- package/src/codegen/templates/react/react.specs.specs.ts +8 -5
- package/src/codegen/templates/react/react.specs.ts +4 -4
- package/src/codegen/templates/react/react.specs.ts.ejs +118 -67
- package/src/codegen/templates/react/react.ts.ejs +4 -4
- package/src/codegen/templates/react/register.specs.ts +2 -2
- package/src/codegen/templates/react/register.ts.ejs +2 -2
- package/src/codegen/types.ts +2 -0
- package/src/commands/generate-server.ts +3 -0
- package/src/domain/shared/ReadModel.ts +3 -3
- package/src/domain/shared/types.ts +5 -3
- package/src/server.ts +53 -15
- package/dist/src/codegen/templates/query/projection.specs.specs..ts +0 -296
- package/src/codegen/scaffoldFromSchema.query-slice-register.specs.ts +0 -179
- 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": "^
|
|
32
|
+
"uuid": "^11.0.0",
|
|
30
33
|
"web-streams-polyfill": "^4.1.0",
|
|
31
|
-
"@auto-engineer/
|
|
32
|
-
"@auto-engineer/
|
|
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.
|
|
43
|
+
"@auto-engineer/cli": "0.11.0"
|
|
41
44
|
},
|
|
42
|
-
"version": "0.
|
|
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
|
-
|
|
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(
|
|
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
|
|
58
|
-
const successfulData = findSuccessfulExampleData(
|
|
63
|
+
const conditions = mapping[command];
|
|
64
|
+
const successfulData = findSuccessfulExampleData(conditions);
|
|
59
65
|
|
|
60
|
-
enhancedMapping[command] =
|
|
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
|
+
}
|
|
@@ -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
|
|
137
|
-
? gwt.
|
|
137
|
+
const eventsFromGiven = Array.isArray(gwt.given)
|
|
138
|
+
? gwt.given.filter((item): item is EventExample => 'eventRef' in item)
|
|
138
139
|
: [];
|
|
139
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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('
|
|
105
|
-
|
|
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('
|
|
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
|
-
|
|
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('
|
|
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
|
});
|