@auto-engineer/server-generator-apollo-emmett 1.86.0 → 1.88.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 +1 -1
- package/.turbo/turbo-test.log +5 -5
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +40 -0
- package/dist/src/codegen/extract/messages.d.ts +1 -1
- package/dist/src/codegen/extract/messages.d.ts.map +1 -1
- package/dist/src/codegen/extract/projection.d.ts +1 -1
- package/dist/src/codegen/extract/projection.d.ts.map +1 -1
- package/dist/src/codegen/extract/projection.js +14 -1
- package/dist/src/codegen/extract/projection.js.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +306 -0
- package/dist/src/codegen/templates/command/decide.specs.ts +4 -4
- package/dist/src/codegen/templates/command/decide.specs.ts.ejs +27 -1
- package/dist/src/codegen/templates/command/decide.ts.ejs +1 -1
- package/dist/src/codegen/templates/query/projection.specs.specs.ts +174 -1
- package/dist/src/codegen/templates/query/projection.specs.ts.ejs +38 -12
- package/dist/src/commands/generate-server.d.ts +7 -1
- package/dist/src/commands/generate-server.d.ts.map +1 -1
- package/dist/src/index.d.ts +7 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +5 -1
- package/package.json +4 -4
- package/src/codegen/extract/messages.ts +1 -1
- package/src/codegen/extract/projection.ts +13 -3
- package/src/codegen/scaffoldFromSchema.ts +1 -1
- package/src/codegen/templates/command/decide.specs.specs.ts +306 -0
- package/src/codegen/templates/command/decide.specs.ts +4 -4
- package/src/codegen/templates/command/decide.specs.ts.ejs +27 -1
- package/src/codegen/templates/command/decide.ts.ejs +1 -1
- package/src/codegen/templates/query/projection.specs.specs.ts +174 -1
- package/src/codegen/templates/query/projection.specs.ts.ejs +38 -12
package/ketchup-plan.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
# Ketchup Plan:
|
|
1
|
+
# Ketchup Plan: Fix generator template bugs found in typical server
|
|
2
2
|
|
|
3
3
|
## TODO
|
|
4
4
|
|
|
5
|
+
- [x] Burst 8: Return array `idField` natively from extraction (d4633c71)
|
|
6
|
+
- [x] Burst 9: Fix `findOne` fallback with safe value resolution (8d3ee9cd)
|
|
7
|
+
- [x] Burst 10: Derive stable `metadata.now` with narrow trigger
|
|
8
|
+
|
|
5
9
|
## DONE
|
|
6
10
|
|
|
7
11
|
- [x] Burst 1: Slim ReadModel to find + findOne, update EJS template API docs, update all inline snapshots
|
package/package.json
CHANGED
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"uuid": "^11.0.0",
|
|
33
33
|
"web-streams-polyfill": "^4.1.0",
|
|
34
34
|
"zod": "^3.22.4",
|
|
35
|
-
"@auto-engineer/narrative": "1.
|
|
36
|
-
"@auto-engineer/message-bus": "1.
|
|
35
|
+
"@auto-engineer/narrative": "1.88.0",
|
|
36
|
+
"@auto-engineer/message-bus": "1.88.0"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"typescript": "^5.8.3",
|
|
45
45
|
"vitest": "^3.2.4",
|
|
46
46
|
"tsx": "^4.19.2",
|
|
47
|
-
"@auto-engineer/cli": "1.
|
|
47
|
+
"@auto-engineer/cli": "1.88.0"
|
|
48
48
|
},
|
|
49
|
-
"version": "1.
|
|
49
|
+
"version": "1.88.0",
|
|
50
50
|
"scripts": {
|
|
51
51
|
"generate:server": "tsx src/cli/index.ts",
|
|
52
52
|
"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",
|
|
@@ -2,7 +2,7 @@ import type { Slice } from '@auto-engineer/narrative';
|
|
|
2
2
|
|
|
3
3
|
interface ProjectionOrigin {
|
|
4
4
|
type: 'projection';
|
|
5
|
-
idField?: string;
|
|
5
|
+
idField?: string | string[];
|
|
6
6
|
name?: string;
|
|
7
7
|
singleton?: boolean;
|
|
8
8
|
}
|
|
@@ -40,8 +40,18 @@ function extractProjectionField<K extends keyof ProjectionOrigin>(slice: Slice,
|
|
|
40
40
|
return undefined;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function extractProjectionIdField(slice: Slice): string | undefined {
|
|
44
|
-
|
|
43
|
+
export function extractProjectionIdField(slice: Slice): string | string[] | undefined {
|
|
44
|
+
if (!('server' in slice)) return undefined;
|
|
45
|
+
const dataSource = slice.server?.data?.items?.[0];
|
|
46
|
+
if (!hasOrigin(dataSource)) return undefined;
|
|
47
|
+
|
|
48
|
+
const origin = dataSource.origin;
|
|
49
|
+
if (isProjectionOrigin(origin)) {
|
|
50
|
+
const { idField } = origin;
|
|
51
|
+
if (typeof idField === 'string') return idField;
|
|
52
|
+
if (Array.isArray(idField) && idField.length > 0) return idField;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
export function extractProjectionName(slice: Slice): string | undefined {
|
|
@@ -547,7 +547,7 @@ async function prepareTemplateData(
|
|
|
547
547
|
events: Message[],
|
|
548
548
|
states: Message[],
|
|
549
549
|
commandSchemasByName: Record<string, Message>,
|
|
550
|
-
projectionIdField: string | undefined,
|
|
550
|
+
projectionIdField: string | string[] | undefined,
|
|
551
551
|
projectionSingleton: boolean | undefined,
|
|
552
552
|
allMessages?: MessageDefinition[],
|
|
553
553
|
integrations?: Model['integrations'],
|
|
@@ -822,4 +822,310 @@ describe('spec.ts.ejs', () => {
|
|
|
822
822
|
expect(decideFile?.contents).toContain('Business rules:');
|
|
823
823
|
expect(decideFile?.contents).toContain('Host can only approve pending bookings');
|
|
824
824
|
});
|
|
825
|
+
|
|
826
|
+
it('should pin metadata.now when Then event has a derived ISO date not in command fields or Given', async () => {
|
|
827
|
+
const spec: SpecsSchema = {
|
|
828
|
+
variant: 'specs',
|
|
829
|
+
narratives: [
|
|
830
|
+
{
|
|
831
|
+
name: 'Record flow',
|
|
832
|
+
slices: [
|
|
833
|
+
{
|
|
834
|
+
type: 'command',
|
|
835
|
+
name: 'Update record',
|
|
836
|
+
client: { specs: [] },
|
|
837
|
+
server: {
|
|
838
|
+
description: '',
|
|
839
|
+
specs: [
|
|
840
|
+
{
|
|
841
|
+
type: 'gherkin',
|
|
842
|
+
feature: 'Update record',
|
|
843
|
+
rules: [
|
|
844
|
+
{
|
|
845
|
+
name: 'Should update personal record',
|
|
846
|
+
examples: [
|
|
847
|
+
{
|
|
848
|
+
name: 'Record updated with derived date',
|
|
849
|
+
steps: [
|
|
850
|
+
{
|
|
851
|
+
keyword: 'When',
|
|
852
|
+
text: 'UpdateRecord',
|
|
853
|
+
docString: { recordId: 'r1', value: 100 },
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
keyword: 'Then',
|
|
857
|
+
text: 'RecordUpdated',
|
|
858
|
+
docString: { recordId: 'r1', value: 100, date: '2024-01-20' },
|
|
859
|
+
},
|
|
860
|
+
],
|
|
861
|
+
},
|
|
862
|
+
],
|
|
863
|
+
},
|
|
864
|
+
],
|
|
865
|
+
},
|
|
866
|
+
],
|
|
867
|
+
},
|
|
868
|
+
},
|
|
869
|
+
],
|
|
870
|
+
},
|
|
871
|
+
],
|
|
872
|
+
messages: [
|
|
873
|
+
{
|
|
874
|
+
type: 'command',
|
|
875
|
+
name: 'UpdateRecord',
|
|
876
|
+
fields: [
|
|
877
|
+
{ name: 'recordId', type: 'string', required: true },
|
|
878
|
+
{ name: 'value', type: 'number', required: true },
|
|
879
|
+
],
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
type: 'event',
|
|
883
|
+
name: 'RecordUpdated',
|
|
884
|
+
source: 'internal',
|
|
885
|
+
fields: [
|
|
886
|
+
{ name: 'recordId', type: 'string', required: true },
|
|
887
|
+
{ name: 'value', type: 'number', required: true },
|
|
888
|
+
{ name: 'date', type: 'string', required: true },
|
|
889
|
+
],
|
|
890
|
+
},
|
|
891
|
+
],
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
895
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
896
|
+
|
|
897
|
+
expect(specFile?.contents).toContain("metadata: { now: new Date('2024-01-20') }");
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it('should keep new Date() when all date fields are in command schema', async () => {
|
|
901
|
+
const spec: SpecsSchema = {
|
|
902
|
+
variant: 'specs',
|
|
903
|
+
narratives: [
|
|
904
|
+
{
|
|
905
|
+
name: 'Schedule flow',
|
|
906
|
+
slices: [
|
|
907
|
+
{
|
|
908
|
+
type: 'command',
|
|
909
|
+
name: 'Schedule event',
|
|
910
|
+
client: { specs: [] },
|
|
911
|
+
server: {
|
|
912
|
+
description: '',
|
|
913
|
+
specs: [
|
|
914
|
+
{
|
|
915
|
+
type: 'gherkin',
|
|
916
|
+
feature: 'Schedule event',
|
|
917
|
+
rules: [
|
|
918
|
+
{
|
|
919
|
+
name: 'Should schedule',
|
|
920
|
+
examples: [
|
|
921
|
+
{
|
|
922
|
+
name: 'Event scheduled with user-provided date',
|
|
923
|
+
steps: [
|
|
924
|
+
{
|
|
925
|
+
keyword: 'When',
|
|
926
|
+
text: 'ScheduleEvent',
|
|
927
|
+
docString: { eventId: 'e1', scheduledDate: '2024-03-15' },
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
keyword: 'Then',
|
|
931
|
+
text: 'EventScheduled',
|
|
932
|
+
docString: { eventId: 'e1', scheduledDate: '2024-03-15' },
|
|
933
|
+
},
|
|
934
|
+
],
|
|
935
|
+
},
|
|
936
|
+
],
|
|
937
|
+
},
|
|
938
|
+
],
|
|
939
|
+
},
|
|
940
|
+
],
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
],
|
|
944
|
+
},
|
|
945
|
+
],
|
|
946
|
+
messages: [
|
|
947
|
+
{
|
|
948
|
+
type: 'command',
|
|
949
|
+
name: 'ScheduleEvent',
|
|
950
|
+
fields: [
|
|
951
|
+
{ name: 'eventId', type: 'string', required: true },
|
|
952
|
+
{ name: 'scheduledDate', type: 'string', required: true },
|
|
953
|
+
],
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
type: 'event',
|
|
957
|
+
name: 'EventScheduled',
|
|
958
|
+
source: 'internal',
|
|
959
|
+
fields: [
|
|
960
|
+
{ name: 'eventId', type: 'string', required: true },
|
|
961
|
+
{ name: 'scheduledDate', type: 'string', required: true },
|
|
962
|
+
],
|
|
963
|
+
},
|
|
964
|
+
],
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
968
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
969
|
+
|
|
970
|
+
expect(specFile?.contents).toContain('metadata: { now: new Date() }');
|
|
971
|
+
expect(specFile?.contents).not.toContain("new Date('2024-03-15')");
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
it('should keep new Date() when date value also appears in Given events', async () => {
|
|
975
|
+
const spec: SpecsSchema = {
|
|
976
|
+
variant: 'specs',
|
|
977
|
+
narratives: [
|
|
978
|
+
{
|
|
979
|
+
name: 'Renewal flow',
|
|
980
|
+
slices: [
|
|
981
|
+
{
|
|
982
|
+
type: 'command',
|
|
983
|
+
name: 'Renew subscription',
|
|
984
|
+
client: { specs: [] },
|
|
985
|
+
server: {
|
|
986
|
+
description: '',
|
|
987
|
+
specs: [
|
|
988
|
+
{
|
|
989
|
+
type: 'gherkin',
|
|
990
|
+
feature: 'Renew subscription',
|
|
991
|
+
rules: [
|
|
992
|
+
{
|
|
993
|
+
name: 'Should renew',
|
|
994
|
+
examples: [
|
|
995
|
+
{
|
|
996
|
+
name: 'Renewal carries forward existing date',
|
|
997
|
+
steps: [
|
|
998
|
+
{
|
|
999
|
+
keyword: 'Given',
|
|
1000
|
+
text: 'SubscriptionCreated',
|
|
1001
|
+
docString: { subId: 's1', startDate: '2024-01-20' },
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
keyword: 'When',
|
|
1005
|
+
text: 'RenewSubscription',
|
|
1006
|
+
docString: { subId: 's1' },
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
keyword: 'Then',
|
|
1010
|
+
text: 'SubscriptionRenewed',
|
|
1011
|
+
docString: { subId: 's1', startDate: '2024-01-20' },
|
|
1012
|
+
},
|
|
1013
|
+
],
|
|
1014
|
+
},
|
|
1015
|
+
],
|
|
1016
|
+
},
|
|
1017
|
+
],
|
|
1018
|
+
},
|
|
1019
|
+
],
|
|
1020
|
+
},
|
|
1021
|
+
},
|
|
1022
|
+
],
|
|
1023
|
+
},
|
|
1024
|
+
],
|
|
1025
|
+
messages: [
|
|
1026
|
+
{
|
|
1027
|
+
type: 'command',
|
|
1028
|
+
name: 'RenewSubscription',
|
|
1029
|
+
fields: [{ name: 'subId', type: 'string', required: true }],
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
type: 'event',
|
|
1033
|
+
name: 'SubscriptionCreated',
|
|
1034
|
+
source: 'internal',
|
|
1035
|
+
fields: [
|
|
1036
|
+
{ name: 'subId', type: 'string', required: true },
|
|
1037
|
+
{ name: 'startDate', type: 'string', required: true },
|
|
1038
|
+
],
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
type: 'event',
|
|
1042
|
+
name: 'SubscriptionRenewed',
|
|
1043
|
+
source: 'internal',
|
|
1044
|
+
fields: [
|
|
1045
|
+
{ name: 'subId', type: 'string', required: true },
|
|
1046
|
+
{ name: 'startDate', type: 'string', required: true },
|
|
1047
|
+
],
|
|
1048
|
+
},
|
|
1049
|
+
],
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
1053
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
1054
|
+
|
|
1055
|
+
expect(specFile?.contents).toContain('metadata: { now: new Date() }');
|
|
1056
|
+
expect(specFile?.contents).not.toContain("new Date('2024-01-20')");
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
it('should keep new Date() when multiple candidate dates create ambiguity', async () => {
|
|
1060
|
+
const spec: SpecsSchema = {
|
|
1061
|
+
variant: 'specs',
|
|
1062
|
+
narratives: [
|
|
1063
|
+
{
|
|
1064
|
+
name: 'Contract flow',
|
|
1065
|
+
slices: [
|
|
1066
|
+
{
|
|
1067
|
+
type: 'command',
|
|
1068
|
+
name: 'Sign contract',
|
|
1069
|
+
client: { specs: [] },
|
|
1070
|
+
server: {
|
|
1071
|
+
description: '',
|
|
1072
|
+
specs: [
|
|
1073
|
+
{
|
|
1074
|
+
type: 'gherkin',
|
|
1075
|
+
feature: 'Sign contract',
|
|
1076
|
+
rules: [
|
|
1077
|
+
{
|
|
1078
|
+
name: 'Should sign',
|
|
1079
|
+
examples: [
|
|
1080
|
+
{
|
|
1081
|
+
name: 'Contract signed with two derived dates',
|
|
1082
|
+
steps: [
|
|
1083
|
+
{
|
|
1084
|
+
keyword: 'When',
|
|
1085
|
+
text: 'SignContract',
|
|
1086
|
+
docString: { contractId: 'c1' },
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
keyword: 'Then',
|
|
1090
|
+
text: 'ContractSigned',
|
|
1091
|
+
docString: { contractId: 'c1', signedDate: '2024-01-20', expiresOn: '2024-06-30' },
|
|
1092
|
+
},
|
|
1093
|
+
],
|
|
1094
|
+
},
|
|
1095
|
+
],
|
|
1096
|
+
},
|
|
1097
|
+
],
|
|
1098
|
+
},
|
|
1099
|
+
],
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
],
|
|
1103
|
+
},
|
|
1104
|
+
],
|
|
1105
|
+
messages: [
|
|
1106
|
+
{
|
|
1107
|
+
type: 'command',
|
|
1108
|
+
name: 'SignContract',
|
|
1109
|
+
fields: [{ name: 'contractId', type: 'string', required: true }],
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
type: 'event',
|
|
1113
|
+
name: 'ContractSigned',
|
|
1114
|
+
source: 'internal',
|
|
1115
|
+
fields: [
|
|
1116
|
+
{ name: 'contractId', type: 'string', required: true },
|
|
1117
|
+
{ name: 'signedDate', type: 'string', required: true },
|
|
1118
|
+
{ name: 'expiresOn', type: 'string', required: true },
|
|
1119
|
+
],
|
|
1120
|
+
},
|
|
1121
|
+
],
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
1125
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
1126
|
+
|
|
1127
|
+
expect(specFile?.contents).toContain('metadata: { now: new Date() }');
|
|
1128
|
+
expect(specFile?.contents).not.toContain("new Date('2024-01-20')");
|
|
1129
|
+
expect(specFile?.contents).not.toContain("new Date('2024-06-30')");
|
|
1130
|
+
});
|
|
825
1131
|
});
|
|
@@ -96,7 +96,7 @@ describe('decide.ts.ejs', () => {
|
|
|
96
96
|
*
|
|
97
97
|
* You should:
|
|
98
98
|
* - Validate the command input fields
|
|
99
|
-
* - Inspect the current domain \`
|
|
99
|
+
* - Inspect the current domain \`_state\` to determine if the command is allowed
|
|
100
100
|
* - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
|
|
101
101
|
* ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
|
|
102
102
|
* - If valid, return one or more events with the correct structure
|
|
@@ -229,7 +229,7 @@ describe('decide.ts.ejs', () => {
|
|
|
229
229
|
*
|
|
230
230
|
* You should:
|
|
231
231
|
* - Validate the command input fields
|
|
232
|
-
* - Inspect the current domain \`
|
|
232
|
+
* - Inspect the current domain \`_state\` to determine if the command is allowed
|
|
233
233
|
* - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
|
|
234
234
|
* ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
|
|
235
235
|
* - If valid, return one or more events with the correct structure
|
|
@@ -382,7 +382,7 @@ describe('decide.ts.ejs', () => {
|
|
|
382
382
|
*
|
|
383
383
|
* You should:
|
|
384
384
|
* - Validate the command input fields
|
|
385
|
-
* - Inspect the current domain \`
|
|
385
|
+
* - Inspect the current domain \`_state\` to determine if the command is allowed
|
|
386
386
|
* - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
|
|
387
387
|
* ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
|
|
388
388
|
* - If valid, return one or more events with the correct structure
|
|
@@ -579,7 +579,7 @@ describe('decide.ts.ejs', () => {
|
|
|
579
579
|
*
|
|
580
580
|
* You should:
|
|
581
581
|
* - Validate the command input fields
|
|
582
|
-
* - Inspect the current domain \`
|
|
582
|
+
* - Inspect the current domain \`_state\` to determine if the command is allowed
|
|
583
583
|
* - Use \`products\` (integration result) to enrich or filter the output
|
|
584
584
|
* - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
|
|
585
585
|
* ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
|
|
@@ -53,6 +53,31 @@ for (const [importPath, eventTypes] of testEventsByPath.entries()) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
const uniqueEventTypes = Array.from(new Set(allEvents.map(e => e?.type).filter(Boolean))).sort();
|
|
56
|
+
|
|
57
|
+
function findDerivedDateValue(eventResults, commandSchema, givenEvents) {
|
|
58
|
+
const commandFields = new Set(commandSchema?.fields?.map(f => f.name) || []);
|
|
59
|
+
const givenValues = new Set();
|
|
60
|
+
for (const g of givenEvents || []) {
|
|
61
|
+
for (const val of Object.values(g.exampleData || {})) {
|
|
62
|
+
if (typeof val === 'string') givenValues.add(val);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const candidates = new Set();
|
|
66
|
+
for (const e of eventResults) {
|
|
67
|
+
const data = e.exampleData || {};
|
|
68
|
+
for (const [key, val] of Object.entries(data)) {
|
|
69
|
+
if (
|
|
70
|
+
!commandFields.has(key) &&
|
|
71
|
+
typeof val === 'string' &&
|
|
72
|
+
/^\d{4}-\d{2}-\d{2}$/.test(val) &&
|
|
73
|
+
!givenValues.has(val)
|
|
74
|
+
) {
|
|
75
|
+
candidates.add(val);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return candidates.size === 1 ? [...candidates][0] : null;
|
|
80
|
+
}
|
|
56
81
|
_%>
|
|
57
82
|
import { describe, it } from 'vitest';
|
|
58
83
|
import { DeciderSpecification } from '@event-driven-io/emmett';
|
|
@@ -97,7 +122,8 @@ describe('<%= ruleDescription %>', () => {
|
|
|
97
122
|
.when({
|
|
98
123
|
type: '<%= example.commandRef %>',
|
|
99
124
|
data: <%- formatDataObject(example.exampleData, schema) %>,
|
|
100
|
-
|
|
125
|
+
<% const derivedDate = findDerivedDateValue(eventResults, schema, gwt.given); -%>
|
|
126
|
+
metadata: { now: <%= derivedDate ? `new Date('${derivedDate}')` : 'new Date()' %> },
|
|
101
127
|
})
|
|
102
128
|
<% if (errorResult) { %>
|
|
103
129
|
.thenThrows((err) => err instanceof <%= errorResult.errorType %> && err.message === '<%= errorResult.message || '' %>');
|
|
@@ -60,7 +60,7 @@ case '<%= command %>': {
|
|
|
60
60
|
*
|
|
61
61
|
* You should:
|
|
62
62
|
* - Validate the command input fields
|
|
63
|
-
* - Inspect the current domain `
|
|
63
|
+
* - Inspect the current domain `_state` to determine if the command is allowed
|
|
64
64
|
<% if (integrationReturnType) { -%>
|
|
65
65
|
* - Use `<%= camelCase(integrationReturnType) %>` (integration result) to enrich or filter the output
|
|
66
66
|
<% } -%>
|
|
@@ -1022,7 +1022,7 @@ describe('projection.specs.ts.ejs', () => {
|
|
|
1022
1022
|
.then(async (state) => {
|
|
1023
1023
|
const document = await state.database
|
|
1024
1024
|
.collection<UserProject>('UserProjectsProjection')
|
|
1025
|
-
.findOne((doc) => doc.
|
|
1025
|
+
.findOne((doc) => doc.userId === 'user_123' && doc.projectId === 'proj_456');
|
|
1026
1026
|
|
|
1027
1027
|
const expected: UserProject = {
|
|
1028
1028
|
userId: 'user_123',
|
|
@@ -2093,4 +2093,177 @@ describe('projection.specs.ts.ejs', () => {
|
|
|
2093
2093
|
expect(specFile?.contents).toContain("service: 'haircut'");
|
|
2094
2094
|
expect(specFile?.contents).not.toContain('"[{\\"appointmentId\\"');
|
|
2095
2095
|
});
|
|
2096
|
+
|
|
2097
|
+
it('should resolve idField value from Given/When events when not in Then state data', async () => {
|
|
2098
|
+
const spec: SpecsSchema = {
|
|
2099
|
+
variant: 'specs',
|
|
2100
|
+
narratives: [
|
|
2101
|
+
{
|
|
2102
|
+
name: 'fitness-flow',
|
|
2103
|
+
slices: [
|
|
2104
|
+
{
|
|
2105
|
+
type: 'command',
|
|
2106
|
+
name: 'log-workout',
|
|
2107
|
+
stream: 'workouts-${memberId}',
|
|
2108
|
+
client: { specs: [] },
|
|
2109
|
+
server: {
|
|
2110
|
+
description: '',
|
|
2111
|
+
specs: [
|
|
2112
|
+
{
|
|
2113
|
+
type: 'gherkin',
|
|
2114
|
+
feature: 'Log workout',
|
|
2115
|
+
rules: [
|
|
2116
|
+
{
|
|
2117
|
+
name: 'Should log',
|
|
2118
|
+
examples: [
|
|
2119
|
+
{
|
|
2120
|
+
name: 'Logs workout',
|
|
2121
|
+
steps: [
|
|
2122
|
+
{
|
|
2123
|
+
keyword: 'When',
|
|
2124
|
+
text: 'LogWorkout',
|
|
2125
|
+
docString: { memberId: 'mem_001', caloriesBurned: 250 },
|
|
2126
|
+
},
|
|
2127
|
+
{
|
|
2128
|
+
keyword: 'Then',
|
|
2129
|
+
text: 'WorkoutRecorded',
|
|
2130
|
+
docString: { memberId: 'mem_001', caloriesBurned: 250 },
|
|
2131
|
+
},
|
|
2132
|
+
],
|
|
2133
|
+
},
|
|
2134
|
+
],
|
|
2135
|
+
},
|
|
2136
|
+
],
|
|
2137
|
+
},
|
|
2138
|
+
],
|
|
2139
|
+
},
|
|
2140
|
+
},
|
|
2141
|
+
{
|
|
2142
|
+
type: 'query',
|
|
2143
|
+
name: 'view-workout-summary',
|
|
2144
|
+
stream: 'workouts',
|
|
2145
|
+
client: { specs: [] },
|
|
2146
|
+
server: {
|
|
2147
|
+
description: '',
|
|
2148
|
+
data: {
|
|
2149
|
+
items: [
|
|
2150
|
+
{
|
|
2151
|
+
target: { type: 'State', name: 'WorkoutSummary' },
|
|
2152
|
+
origin: { type: 'projection', name: 'WorkoutSummaryProjection', idField: 'memberId' },
|
|
2153
|
+
},
|
|
2154
|
+
],
|
|
2155
|
+
},
|
|
2156
|
+
specs: [
|
|
2157
|
+
{
|
|
2158
|
+
type: 'gherkin',
|
|
2159
|
+
feature: 'View workout summary',
|
|
2160
|
+
rules: [
|
|
2161
|
+
{
|
|
2162
|
+
name: 'Should summarize workouts',
|
|
2163
|
+
examples: [
|
|
2164
|
+
{
|
|
2165
|
+
name: 'Summary after workout recorded',
|
|
2166
|
+
steps: [
|
|
2167
|
+
{
|
|
2168
|
+
keyword: 'When',
|
|
2169
|
+
text: 'WorkoutRecorded',
|
|
2170
|
+
docString: { memberId: 'mem_001', caloriesBurned: 250 },
|
|
2171
|
+
},
|
|
2172
|
+
{
|
|
2173
|
+
keyword: 'Then',
|
|
2174
|
+
text: 'WorkoutSummary',
|
|
2175
|
+
docString: { totalCalories: 250, totalSessions: 1 },
|
|
2176
|
+
},
|
|
2177
|
+
],
|
|
2178
|
+
},
|
|
2179
|
+
],
|
|
2180
|
+
},
|
|
2181
|
+
],
|
|
2182
|
+
},
|
|
2183
|
+
],
|
|
2184
|
+
},
|
|
2185
|
+
},
|
|
2186
|
+
],
|
|
2187
|
+
},
|
|
2188
|
+
],
|
|
2189
|
+
messages: [
|
|
2190
|
+
{
|
|
2191
|
+
type: 'command',
|
|
2192
|
+
name: 'LogWorkout',
|
|
2193
|
+
fields: [
|
|
2194
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
2195
|
+
{ name: 'caloriesBurned', type: 'number', required: true },
|
|
2196
|
+
],
|
|
2197
|
+
},
|
|
2198
|
+
{
|
|
2199
|
+
type: 'event',
|
|
2200
|
+
name: 'WorkoutRecorded',
|
|
2201
|
+
source: 'internal',
|
|
2202
|
+
fields: [
|
|
2203
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
2204
|
+
{ name: 'caloriesBurned', type: 'number', required: true },
|
|
2205
|
+
],
|
|
2206
|
+
},
|
|
2207
|
+
{
|
|
2208
|
+
type: 'state',
|
|
2209
|
+
name: 'WorkoutSummary',
|
|
2210
|
+
fields: [
|
|
2211
|
+
{ name: 'totalCalories', type: 'number', required: true },
|
|
2212
|
+
{ name: 'totalSessions', type: 'number', required: true },
|
|
2213
|
+
],
|
|
2214
|
+
},
|
|
2215
|
+
],
|
|
2216
|
+
} as SpecsSchema;
|
|
2217
|
+
|
|
2218
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
2219
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('view-workout-summary/projection.specs.ts'));
|
|
2220
|
+
|
|
2221
|
+
expect(specFile?.contents).toMatchInlineSnapshot(`
|
|
2222
|
+
"import { describe, it, beforeEach, expect } from 'vitest';
|
|
2223
|
+
import { InMemoryProjectionSpec } from '@event-driven-io/emmett';
|
|
2224
|
+
import { projection } from './projection';
|
|
2225
|
+
import type { WorkoutRecorded } from '../log-workout/events';
|
|
2226
|
+
import { WorkoutSummary } from './state';
|
|
2227
|
+
|
|
2228
|
+
type ProjectionEvent = WorkoutRecorded;
|
|
2229
|
+
|
|
2230
|
+
describe('Should summarize workouts', () => {
|
|
2231
|
+
let given: InMemoryProjectionSpec<ProjectionEvent>;
|
|
2232
|
+
|
|
2233
|
+
beforeEach(() => {
|
|
2234
|
+
given = InMemoryProjectionSpec.for({ projection });
|
|
2235
|
+
});
|
|
2236
|
+
|
|
2237
|
+
it('Summary after workout recorded', () =>
|
|
2238
|
+
given([])
|
|
2239
|
+
.when([
|
|
2240
|
+
{
|
|
2241
|
+
type: 'WorkoutRecorded',
|
|
2242
|
+
data: {
|
|
2243
|
+
memberId: 'mem_001',
|
|
2244
|
+
caloriesBurned: 250,
|
|
2245
|
+
},
|
|
2246
|
+
metadata: {
|
|
2247
|
+
streamName: 'workouts',
|
|
2248
|
+
streamPosition: 1n,
|
|
2249
|
+
globalPosition: 1n,
|
|
2250
|
+
},
|
|
2251
|
+
},
|
|
2252
|
+
])
|
|
2253
|
+
.then(async (state) => {
|
|
2254
|
+
const document = await state.database
|
|
2255
|
+
.collection<WorkoutSummary>('WorkoutSummaryProjection')
|
|
2256
|
+
.findOne((doc) => doc.memberId === 'mem_001');
|
|
2257
|
+
|
|
2258
|
+
const expected: WorkoutSummary = {
|
|
2259
|
+
totalCalories: 250,
|
|
2260
|
+
totalSessions: 1,
|
|
2261
|
+
};
|
|
2262
|
+
|
|
2263
|
+
expect(document).toMatchObject(expected);
|
|
2264
|
+
}));
|
|
2265
|
+
});
|
|
2266
|
+
"
|
|
2267
|
+
`);
|
|
2268
|
+
});
|
|
2096
2269
|
});
|