@auto-engineer/server-generator-apollo-emmett 1.154.0 → 1.156.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 +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +57 -0
- package/dist/src/codegen/extract/data-sink.d.ts +1 -0
- package/dist/src/codegen/extract/data-sink.d.ts.map +1 -1
- package/dist/src/codegen/extract/data-sink.js +3 -0
- package/dist/src/codegen/extract/data-sink.js.map +1 -1
- package/dist/src/codegen/extract/messages.d.ts +1 -0
- package/dist/src/codegen/extract/messages.d.ts.map +1 -1
- package/dist/src/codegen/extract/messages.js +1 -1
- package/dist/src/codegen/extract/messages.js.map +1 -1
- package/dist/src/codegen/extract/sibling-events.d.ts +8 -0
- package/dist/src/codegen/extract/sibling-events.d.ts.map +1 -0
- package/dist/src/codegen/extract/sibling-events.js +58 -0
- package/dist/src/codegen/extract/sibling-events.js.map +1 -0
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +37 -2
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +125 -0
- package/dist/src/codegen/templates/command/decide.specs.ts +258 -3
- package/dist/src/codegen/templates/command/decide.specs.ts.ejs +42 -14
- package/dist/src/codegen/templates/command/decide.ts.ejs +63 -16
- package/dist/src/codegen/templates/command/evolve.specs.ts +217 -39
- package/dist/src/codegen/templates/command/evolve.ts.ejs +7 -7
- package/dist/src/codegen/templates/command/handle.specs.ts +7 -4
- package/dist/src/codegen/templates/command/handle.ts.ejs +17 -5
- package/dist/src/codegen/templates/command/state.specs.ts +125 -0
- package/dist/src/codegen/templates/command/state.ts.ejs +10 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +3 -3
- package/package.json +4 -4
- package/src/codegen/extract/data-sink.specs.ts +23 -1
- package/src/codegen/extract/data-sink.ts +4 -0
- package/src/codegen/extract/messages.ts +1 -1
- package/src/codegen/extract/sibling-events.specs.ts +206 -0
- package/src/codegen/extract/sibling-events.ts +76 -0
- package/src/codegen/ketchup-plan.md +12 -0
- package/src/codegen/scaffoldFromSchema.ts +50 -0
- package/src/codegen/templates/command/decide.specs.specs.ts +125 -0
- package/src/codegen/templates/command/decide.specs.ts +258 -3
- package/src/codegen/templates/command/decide.specs.ts.ejs +42 -14
- package/src/codegen/templates/command/decide.ts.ejs +63 -16
- package/src/codegen/templates/command/evolve.specs.ts +217 -39
- package/src/codegen/templates/command/evolve.ts.ejs +7 -7
- package/src/codegen/templates/command/handle.specs.ts +7 -4
- package/src/codegen/templates/command/handle.ts.ejs +17 -5
- package/src/codegen/templates/command/state.specs.ts +125 -0
- package/src/codegen/templates/command/state.ts.ejs +10 -2
|
@@ -593,7 +593,7 @@ describe('decide.ts.ejs', () => {
|
|
|
593
593
|
import type { ItemsSuggested } from './events';
|
|
594
594
|
import type { Products } from '@auto-engineer/product-catalogue-integration';
|
|
595
595
|
|
|
596
|
-
export const decide = (command: SuggestItems, _state: State, products?: Products): ItemsSuggested => {
|
|
596
|
+
export const decide = (command: SuggestItems, _state: State, context?: { products?: Products }): ItemsSuggested => {
|
|
597
597
|
switch (command.type) {
|
|
598
598
|
case 'SuggestItems': {
|
|
599
599
|
/**
|
|
@@ -604,7 +604,7 @@ describe('decide.ts.ejs', () => {
|
|
|
604
604
|
* You should:
|
|
605
605
|
* - Validate the command input fields
|
|
606
606
|
* - NEVER use \`as SomeType\` type assertions — not \`as any\`, not \`as EventType\`, no casts at all. Use typed variable declarations.
|
|
607
|
-
* - Use \`products\` (integration result) to enrich or filter the output
|
|
607
|
+
* - Use \`context?.products\` (integration result) to enrich or filter the output
|
|
608
608
|
* - If invalid, throw one of the following domain errors: \`IllegalStateError\`
|
|
609
609
|
* ⚠️ Error constructors: IllegalStateError takes a string message
|
|
610
610
|
* - If valid, return one or more events with the correct structure
|
|
@@ -613,7 +613,7 @@ describe('decide.ts.ejs', () => {
|
|
|
613
613
|
* ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
|
|
614
614
|
*
|
|
615
615
|
* Integration result shape (Products):
|
|
616
|
-
* products?.data = {
|
|
616
|
+
* context?.products?.data = {
|
|
617
617
|
* products: Array<{
|
|
618
618
|
* id: string;
|
|
619
619
|
* name: string;
|
|
@@ -648,4 +648,259 @@ describe('decide.ts.ejs', () => {
|
|
|
648
648
|
"
|
|
649
649
|
`);
|
|
650
650
|
});
|
|
651
|
+
|
|
652
|
+
it('should generate context parameter and uuid assignment for stream uuid vars that are event fields', async () => {
|
|
653
|
+
const spec: SpecsSchema = {
|
|
654
|
+
variant: 'specs',
|
|
655
|
+
scenes: [
|
|
656
|
+
{
|
|
657
|
+
name: 'Fitness tracker',
|
|
658
|
+
moments: [
|
|
659
|
+
{
|
|
660
|
+
type: 'command',
|
|
661
|
+
name: 'Log workout',
|
|
662
|
+
stream: 'workouts-${workoutId}',
|
|
663
|
+
client: { specs: [] },
|
|
664
|
+
server: {
|
|
665
|
+
description: 'test',
|
|
666
|
+
specs: [
|
|
667
|
+
{
|
|
668
|
+
type: 'gherkin',
|
|
669
|
+
feature: 'Log workout',
|
|
670
|
+
rules: [
|
|
671
|
+
{
|
|
672
|
+
name: 'Should log workout',
|
|
673
|
+
examples: [
|
|
674
|
+
{
|
|
675
|
+
name: 'Workout logged',
|
|
676
|
+
steps: [
|
|
677
|
+
{
|
|
678
|
+
keyword: 'When',
|
|
679
|
+
text: 'LogWorkout',
|
|
680
|
+
docString: { exercise: 'squat', reps: 10 },
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
keyword: 'Then',
|
|
684
|
+
text: 'WorkoutLogged',
|
|
685
|
+
docString: { workoutId: 'wk-1', exercise: 'squat', reps: 10 },
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
},
|
|
689
|
+
],
|
|
690
|
+
},
|
|
691
|
+
],
|
|
692
|
+
},
|
|
693
|
+
],
|
|
694
|
+
data: {
|
|
695
|
+
items: [
|
|
696
|
+
{
|
|
697
|
+
target: { type: 'Event', name: 'WorkoutLogged' },
|
|
698
|
+
destination: { type: 'stream', pattern: 'workouts-${workoutId}' },
|
|
699
|
+
},
|
|
700
|
+
],
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
],
|
|
705
|
+
},
|
|
706
|
+
],
|
|
707
|
+
messages: [
|
|
708
|
+
{
|
|
709
|
+
type: 'command',
|
|
710
|
+
name: 'LogWorkout',
|
|
711
|
+
fields: [
|
|
712
|
+
{ name: 'exercise', type: 'string', required: true },
|
|
713
|
+
{ name: 'reps', type: 'number', required: true },
|
|
714
|
+
],
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
type: 'event',
|
|
718
|
+
name: 'WorkoutLogged',
|
|
719
|
+
source: 'internal',
|
|
720
|
+
fields: [
|
|
721
|
+
{ name: 'workoutId', type: 'string', required: true },
|
|
722
|
+
{ name: 'exercise', type: 'string', required: true },
|
|
723
|
+
{ name: 'reps', type: 'number', required: true },
|
|
724
|
+
],
|
|
725
|
+
},
|
|
726
|
+
],
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const { plans } = await generateScaffoldFilePlans(spec.scenes, spec.messages, undefined, 'src/domain/narratives');
|
|
730
|
+
const decideFile = plans.find((p) => p.outputPath.endsWith('decide.ts'));
|
|
731
|
+
|
|
732
|
+
expect(decideFile?.contents).toMatchInlineSnapshot(`
|
|
733
|
+
"import { randomUUID } from 'node:crypto';
|
|
734
|
+
import { IllegalStateError } from '@event-driven-io/emmett';
|
|
735
|
+
import type { State } from './state';
|
|
736
|
+
import type { LogWorkout } from './commands';
|
|
737
|
+
import type { WorkoutLogged } from './events';
|
|
738
|
+
|
|
739
|
+
export const decide = (
|
|
740
|
+
command: LogWorkout,
|
|
741
|
+
_state: State,
|
|
742
|
+
context?: { generated?: { workoutId: string } },
|
|
743
|
+
): WorkoutLogged => {
|
|
744
|
+
switch (command.type) {
|
|
745
|
+
case 'LogWorkout': {
|
|
746
|
+
/**
|
|
747
|
+
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
748
|
+
*
|
|
749
|
+
* This command can directly emit one or more events based on the input.
|
|
750
|
+
*
|
|
751
|
+
* You should:
|
|
752
|
+
* - Validate the command input fields
|
|
753
|
+
* - NEVER use \`as SomeType\` type assertions — not \`as any\`, not \`as EventType\`, no casts at all. Use typed variable declarations.
|
|
754
|
+
* - If invalid, throw one of the following domain errors: \`IllegalStateError\`
|
|
755
|
+
* ⚠️ Error constructors: IllegalStateError takes a string message
|
|
756
|
+
* - If valid, return one or more events with the correct structure
|
|
757
|
+
*
|
|
758
|
+
* - Only destructure/reference command.data fields that exist in the command type (imported from ./commands). Do NOT access fields not declared in the type, even if example data includes them.
|
|
759
|
+
* ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
|
|
760
|
+
|
|
761
|
+
* Business rules:
|
|
762
|
+
* - Should log workout
|
|
763
|
+
*/
|
|
764
|
+
|
|
765
|
+
const workoutId = context?.generated?.workoutId ?? randomUUID();
|
|
766
|
+
|
|
767
|
+
// IMPLEMENT: Use a typed variable to prevent type widening:
|
|
768
|
+
// const result: WorkoutLogged = {
|
|
769
|
+
// type: 'WorkoutLogged',
|
|
770
|
+
// data: { ...command.data, workoutId },
|
|
771
|
+
// };
|
|
772
|
+
// return result;
|
|
773
|
+
|
|
774
|
+
throw new IllegalStateError('Not yet implemented: ' + command.type);
|
|
775
|
+
}
|
|
776
|
+
default:
|
|
777
|
+
throw new IllegalStateError('Unexpected command type');
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
"
|
|
781
|
+
`);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it('should say Inspect _state for state-ref Given with preceding siblings', async () => {
|
|
785
|
+
const spec: SpecsSchema = {
|
|
786
|
+
variant: 'specs',
|
|
787
|
+
scenes: [
|
|
788
|
+
{
|
|
789
|
+
name: 'Gym session',
|
|
790
|
+
moments: [
|
|
791
|
+
{
|
|
792
|
+
type: 'command',
|
|
793
|
+
name: 'Start session',
|
|
794
|
+
client: { specs: [] },
|
|
795
|
+
server: {
|
|
796
|
+
description: '',
|
|
797
|
+
data: {
|
|
798
|
+
items: [
|
|
799
|
+
{
|
|
800
|
+
target: { type: 'Event', name: 'SessionStarted' },
|
|
801
|
+
destination: { type: 'stream', pattern: 'sessions-${id}' },
|
|
802
|
+
},
|
|
803
|
+
],
|
|
804
|
+
},
|
|
805
|
+
specs: [
|
|
806
|
+
{
|
|
807
|
+
type: 'gherkin',
|
|
808
|
+
feature: 'Start',
|
|
809
|
+
rules: [
|
|
810
|
+
{
|
|
811
|
+
name: 'Start',
|
|
812
|
+
examples: [
|
|
813
|
+
{
|
|
814
|
+
name: 'Start',
|
|
815
|
+
steps: [
|
|
816
|
+
{ keyword: 'When', text: 'StartSession', docString: { userId: 'u1' } },
|
|
817
|
+
{ keyword: 'Then', text: 'SessionStarted', docString: { id: 's1', userId: 'u1' } },
|
|
818
|
+
],
|
|
819
|
+
},
|
|
820
|
+
],
|
|
821
|
+
},
|
|
822
|
+
],
|
|
823
|
+
},
|
|
824
|
+
],
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
type: 'command',
|
|
829
|
+
name: 'Complete session',
|
|
830
|
+
client: { specs: [] },
|
|
831
|
+
server: {
|
|
832
|
+
description: '',
|
|
833
|
+
data: {
|
|
834
|
+
items: [
|
|
835
|
+
{
|
|
836
|
+
target: { type: 'Event', name: 'SessionCompleted' },
|
|
837
|
+
destination: { type: 'stream', pattern: 'sessions-${sessionId}' },
|
|
838
|
+
},
|
|
839
|
+
],
|
|
840
|
+
},
|
|
841
|
+
specs: [
|
|
842
|
+
{
|
|
843
|
+
type: 'gherkin',
|
|
844
|
+
feature: 'Complete',
|
|
845
|
+
rules: [
|
|
846
|
+
{
|
|
847
|
+
name: 'Complete',
|
|
848
|
+
examples: [
|
|
849
|
+
{
|
|
850
|
+
name: 'Complete',
|
|
851
|
+
steps: [
|
|
852
|
+
{ keyword: 'Given', text: 'ActiveSession', docString: { status: 'active' } },
|
|
853
|
+
{ keyword: 'When', text: 'CompleteSession', docString: { sessionId: 's1' } },
|
|
854
|
+
{
|
|
855
|
+
keyword: 'Then',
|
|
856
|
+
text: 'SessionCompleted',
|
|
857
|
+
docString: { sessionId: 's1', completedAt: '2024-01-15' },
|
|
858
|
+
},
|
|
859
|
+
],
|
|
860
|
+
},
|
|
861
|
+
],
|
|
862
|
+
},
|
|
863
|
+
],
|
|
864
|
+
},
|
|
865
|
+
],
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
],
|
|
869
|
+
},
|
|
870
|
+
],
|
|
871
|
+
messages: [
|
|
872
|
+
{ type: 'command', name: 'StartSession', fields: [{ name: 'userId', type: 'string', required: true }] },
|
|
873
|
+
{ type: 'command', name: 'CompleteSession', fields: [{ name: 'sessionId', type: 'string', required: true }] },
|
|
874
|
+
{
|
|
875
|
+
type: 'event',
|
|
876
|
+
name: 'SessionStarted',
|
|
877
|
+
source: 'internal',
|
|
878
|
+
fields: [
|
|
879
|
+
{ name: 'id', type: 'string', required: true },
|
|
880
|
+
{ name: 'userId', type: 'string', required: true },
|
|
881
|
+
],
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
type: 'event',
|
|
885
|
+
name: 'SessionCompleted',
|
|
886
|
+
source: 'internal',
|
|
887
|
+
fields: [
|
|
888
|
+
{ name: 'sessionId', type: 'string', required: true },
|
|
889
|
+
{ name: 'completedAt', type: 'string', required: true },
|
|
890
|
+
],
|
|
891
|
+
},
|
|
892
|
+
{ type: 'state', name: 'ActiveSession', fields: [{ name: 'status', type: 'string', required: true }] },
|
|
893
|
+
],
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
const { plans } = await generateScaffoldFilePlans(spec.scenes, spec.messages, undefined, 'src/domain/narratives');
|
|
897
|
+
const decideFile = plans.find(
|
|
898
|
+
(p) => p.outputPath.includes('complete-session') && p.outputPath.endsWith('decide.ts'),
|
|
899
|
+
);
|
|
900
|
+
const contents = decideFile?.contents ?? '';
|
|
901
|
+
|
|
902
|
+
expect(contents).toContain('requires evaluating prior state');
|
|
903
|
+
expect(contents).toContain('Inspect the current domain');
|
|
904
|
+
expect(contents).not.toContain('succeed from initial state');
|
|
905
|
+
});
|
|
651
906
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<%
|
|
2
2
|
const allEvents = [];
|
|
3
3
|
const ruleGroups = new Map();
|
|
4
|
+
const _stateRefGivenSeen = new Set();
|
|
4
5
|
for (const commandName in gwtMapping) {
|
|
5
6
|
const cases = gwtMapping[commandName];
|
|
6
7
|
for (const gwt of cases) {
|
|
@@ -12,8 +13,17 @@ for (const commandName in gwtMapping) {
|
|
|
12
13
|
if (gwt.given && gwt.given.length) {
|
|
13
14
|
for (const g of gwt.given) {
|
|
14
15
|
if (g.eventRef) {
|
|
15
|
-
const event =
|
|
16
|
-
if (event)
|
|
16
|
+
const event = allStreamEvents.find(e => e.type === g.eventRef);
|
|
17
|
+
if (event) {
|
|
18
|
+
allEvents.push(event);
|
|
19
|
+
} else if (precedingSiblingEvents.length > 0 && !_stateRefGivenSeen.has(g.eventRef)) {
|
|
20
|
+
_stateRefGivenSeen.add(g.eventRef);
|
|
21
|
+
for (const se of precedingSiblingEvents) {
|
|
22
|
+
if (!allEvents.some(e => e.type === se.type)) {
|
|
23
|
+
allEvents.push(se);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
17
27
|
}
|
|
18
28
|
}
|
|
19
29
|
}
|
|
@@ -33,7 +43,7 @@ const testEventsByPath = new Map();
|
|
|
33
43
|
|
|
34
44
|
for (const event of allEvents) {
|
|
35
45
|
if (!event.type) continue;
|
|
36
|
-
const importGroup =
|
|
46
|
+
const importGroup = allStreamEventImportGroups.find(group =>
|
|
37
47
|
group.eventTypes.includes(event.type)
|
|
38
48
|
);
|
|
39
49
|
|
|
@@ -64,7 +74,7 @@ for (const commandName in gwtMapping) {
|
|
|
64
74
|
const eventResults = gwt.then.filter(t => 'eventRef' in t);
|
|
65
75
|
const errorResult = gwt.then.find(t => 'errorType' in t);
|
|
66
76
|
if (errorResult) continue;
|
|
67
|
-
const givenEventRefs = (gwt.given || []).filter(g =>
|
|
77
|
+
const givenEventRefs = (gwt.given || []).filter(g => allStreamEvents.some(ev => ev.type === g.eventRef));
|
|
68
78
|
for (const e of eventResults) {
|
|
69
79
|
const dedupeKey = `${commandName}::${e.eventRef}`;
|
|
70
80
|
if (seenCommandEvents.has(dedupeKey)) continue;
|
|
@@ -116,12 +126,22 @@ describe('<%= escapeJsString(ruleDescription) %>', () => {
|
|
|
116
126
|
%>
|
|
117
127
|
it('<%= escapeJsString(testDescription) %>', () => {
|
|
118
128
|
given([
|
|
119
|
-
<%_
|
|
129
|
+
<%_
|
|
130
|
+
let givenEvents = (gwt.given || []).filter(g => allStreamEvents.some(e => e.type === g.eventRef));
|
|
131
|
+
const hasStateRefGiven = (gwt.given || []).some(g => !allStreamEvents.some(e => e.type === g.eventRef));
|
|
132
|
+
if (givenEvents.length === 0 && hasStateRefGiven && precedingSiblingEvents.length > 0) {
|
|
133
|
+
givenEvents = precedingSiblingEvents;
|
|
134
|
+
}
|
|
135
|
+
_%>
|
|
120
136
|
<%_ if (givenEvents.length) { _%>
|
|
121
|
-
<%- givenEvents.map(g =>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
137
|
+
<%- givenEvents.map(g => {
|
|
138
|
+
const schema = allStreamEvents.find(e => e.type === (g.eventRef || g.type));
|
|
139
|
+
const data = g.exampleData || siblingExampleData[g.type] || {};
|
|
140
|
+
return `{
|
|
141
|
+
type: '${g.eventRef || g.type}',
|
|
142
|
+
data: ${formatDataObject(data, schema)}
|
|
143
|
+
}`;
|
|
144
|
+
}).join(',\n ') %>
|
|
125
145
|
<%_ } _%>
|
|
126
146
|
])
|
|
127
147
|
.when({
|
|
@@ -171,7 +191,11 @@ describe('field completeness', () => {
|
|
|
171
191
|
const firstScenario = scenarios[0];
|
|
172
192
|
const gwt = firstScenario.gwt;
|
|
173
193
|
const schema = commandSchemasByName[cmdName];
|
|
174
|
-
|
|
194
|
+
let givenEvents = (gwt.given || []).filter(g => allStreamEvents.some(e => e.type === g.eventRef));
|
|
195
|
+
const hasStateRefGiven = (gwt.given || []).some(g => !allStreamEvents.some(e => e.type === g.eventRef));
|
|
196
|
+
if (givenEvents.length === 0 && hasStateRefGiven && precedingSiblingEvents.length > 0) {
|
|
197
|
+
givenEvents = precedingSiblingEvents;
|
|
198
|
+
}
|
|
175
199
|
const example = gwt.when;
|
|
176
200
|
const cmdFieldNames = firstScenario.cmdFieldNames;
|
|
177
201
|
const filteredCmdData = Object.fromEntries(
|
|
@@ -181,10 +205,14 @@ describe('field completeness', () => {
|
|
|
181
205
|
it('should include all required fields in <%= scenarios.map(s => s.eventRef).join(', ') %>', () => {
|
|
182
206
|
<%_ if (givenEvents.length) { _%>
|
|
183
207
|
const state = [
|
|
184
|
-
<%- givenEvents.map(g =>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
208
|
+
<%- givenEvents.map(g => {
|
|
209
|
+
const schema = allStreamEvents.find(e => e.type === (g.eventRef || g.type));
|
|
210
|
+
const data = g.exampleData || siblingExampleData[g.type] || {};
|
|
211
|
+
return `{
|
|
212
|
+
type: '${g.eventRef || g.type}' as const,
|
|
213
|
+
data: ${formatDataObject(data, schema)}
|
|
214
|
+
}`;
|
|
215
|
+
}).join(',\n ') %>
|
|
188
216
|
].reduce((s, e) => evolve(s, e), initialState());
|
|
189
217
|
<%_ } else { _%>
|
|
190
218
|
const state = initialState();
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const needsRandomUuidImport = (() => {
|
|
3
|
+
for (const cmdName of Object.keys(gwtMapping)) {
|
|
4
|
+
const cmdFieldSet = new Set((commandSchemasByName?.[cmdName]?.fields || []).map(f => f.name));
|
|
5
|
+
const scenarios = gwtMapping?.[cmdName] ?? [];
|
|
6
|
+
for (const v of (uuidVars || [])) {
|
|
7
|
+
if (cmdFieldSet.has(v)) continue;
|
|
8
|
+
for (const scenario of scenarios) {
|
|
9
|
+
for (const t of scenario.then.filter(t => 'eventRef' in t)) {
|
|
10
|
+
const eventMsg = (messages || []).find(m => m.name === t.eventRef && m.type === 'event');
|
|
11
|
+
if ((eventMsg?.fields || []).some(f => f.name === v)) return true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
})();
|
|
18
|
+
-%>
|
|
19
|
+
<% if (needsRandomUuidImport) { -%>
|
|
20
|
+
import { randomUUID } from 'node:crypto';
|
|
21
|
+
<% } -%>
|
|
1
22
|
import {
|
|
2
23
|
IllegalStateError<% if (usedErrors.includes('ValidationError')) { %>, ValidationError<% } %><% if (usedErrors.includes('NotFoundError')) { %>, NotFoundError<% } %>
|
|
3
24
|
} from '@event-driven-io/emmett';
|
|
@@ -16,6 +37,19 @@ const integrationReturnSystem = integration?._withState?.origin?.systems?.[0];
|
|
|
16
37
|
const integrationReturnImportSource = integrations?.find(i => i.name === integrationReturnSystem)?.source;
|
|
17
38
|
const integrationReturnFields = messages?.find(m => m.name === integrationReturnType && m.type === 'state')?.fields ?? [];
|
|
18
39
|
|
|
40
|
+
const hasContextGenerated = (uuidVars || []).length > 0;
|
|
41
|
+
const hasContextIntegration = !!integrationReturnType;
|
|
42
|
+
const hasContext = hasContextGenerated || hasContextIntegration;
|
|
43
|
+
const contextTypeParts = [];
|
|
44
|
+
if (hasContextGenerated) {
|
|
45
|
+
const generatedFields = (uuidVars || []).map(v => `${v}: string`).join('; ');
|
|
46
|
+
contextTypeParts.push(`generated?: { ${generatedFields} }`);
|
|
47
|
+
}
|
|
48
|
+
if (hasContextIntegration) {
|
|
49
|
+
contextTypeParts.push(`${camelCase(integrationReturnType)}?: ${integrationReturnType}`);
|
|
50
|
+
}
|
|
51
|
+
const contextTypeStr = contextTypeParts.join('; ');
|
|
52
|
+
|
|
19
53
|
function formatFieldDocLine(field) {
|
|
20
54
|
const optional = field.required ? '' : '?';
|
|
21
55
|
const name = `${field.name}${optional}`;
|
|
@@ -39,7 +73,7 @@ function formatFieldDocLine(field) {
|
|
|
39
73
|
|
|
40
74
|
export const decide = (
|
|
41
75
|
command: <%= Object.keys(gwtMapping).map(pascalCase).join(' | ') %>,
|
|
42
|
-
_state: State<%=
|
|
76
|
+
_state: State<%= hasContext ? `,\ncontext?: { ${contextTypeStr} }` : '' %>
|
|
43
77
|
): <%= uniqueEventTypes.length === 0
|
|
44
78
|
? 'never'
|
|
45
79
|
: uniqueEventTypes.length === 1
|
|
@@ -49,11 +83,13 @@ switch (command.type) {
|
|
|
49
83
|
<% for (const command of Object.keys(gwtMapping)) {
|
|
50
84
|
const scenarios = gwtMapping?.[command] ?? [];
|
|
51
85
|
const hasGivenEvents = scenarios.some(s =>
|
|
52
|
-
(s.given || []).some(g =>
|
|
86
|
+
(s.given || []).some(g => allStreamEvents.some(e => e.type === g.eventRef))
|
|
53
87
|
);
|
|
54
88
|
const hasGivenStates = scenarios.some(s =>
|
|
55
|
-
(s.given || []).some(g => !
|
|
89
|
+
(s.given || []).some(g => !allStreamEvents.some(e => e.type === g.eventRef))
|
|
56
90
|
);
|
|
91
|
+
const hasPrecedingSiblings = precedingSiblingEvents.length > 0;
|
|
92
|
+
const effectiveHasGivenEvents = hasGivenEvents || (hasGivenStates && hasPrecedingSiblings);
|
|
57
93
|
const fallbackEvents = scenarios.flatMap(s => s.then.filter(t => 'eventRef' in t));
|
|
58
94
|
const fallbackEventTypes = [...new Set(fallbackEvents.map(e => e.eventRef))];
|
|
59
95
|
-%>
|
|
@@ -61,22 +97,22 @@ case '<%= command %>': {
|
|
|
61
97
|
/**
|
|
62
98
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
63
99
|
*
|
|
64
|
-
* This command <%=
|
|
100
|
+
* This command <%= effectiveHasGivenEvents ? 'requires evaluating prior state to determine if it can proceed' : 'can directly emit one or more events based on the input' %>.
|
|
65
101
|
*
|
|
66
102
|
* You should:
|
|
67
103
|
* - Validate the command input fields
|
|
68
|
-
<% if (
|
|
104
|
+
<% if (effectiveHasGivenEvents) { -%>
|
|
69
105
|
* - Inspect the current domain `_state` to determine if the command is allowed
|
|
70
106
|
<% } -%>
|
|
71
107
|
* - NEVER use `as SomeType` type assertions — not `as any`, not `as EventType`, no casts at all. Use typed variable declarations.
|
|
72
|
-
<% if (
|
|
108
|
+
<% if (effectiveHasGivenEvents) { -%>
|
|
73
109
|
* - If State is a discriminated union, narrow with the discriminant: `if (_state.status !== 'active') throw new IllegalStateError('...');`
|
|
74
110
|
* After narrowing, access variant fields directly — TypeScript infers the correct type.
|
|
75
111
|
<% } else if (hasGivenStates) { -%>
|
|
76
112
|
* - The test uses given([]) with initialState(). Do not narrow or guard on _state — the command should succeed from initial state.
|
|
77
113
|
<% } -%>
|
|
78
114
|
<% if (integrationReturnType) { -%>
|
|
79
|
-
* - Use
|
|
115
|
+
* - Use `context?.<%= camelCase(integrationReturnType) %>` (integration result) to enrich or filter the output
|
|
80
116
|
<% } -%>
|
|
81
117
|
* - If invalid, throw one of the following domain errors: `IllegalStateError`<% if (usedErrors.includes('ValidationError')) { %>, `ValidationError`<% } %><% if (usedErrors.includes('NotFoundError')) { %>, `NotFoundError`<% } %>
|
|
82
118
|
* ⚠️ Error constructors: IllegalStateError takes a string message<% if (usedErrors.includes('ValidationError')) { %>, ValidationError takes a string message<% } %><% if (usedErrors.includes('NotFoundError')) { %>, NotFoundError takes { id, type, message? }<% } %>
|
|
@@ -87,7 +123,7 @@ case '<%= command %>': {
|
|
|
87
123
|
<% if (integrationReturnFields.length > 0) { -%>
|
|
88
124
|
*
|
|
89
125
|
* Integration result shape (<%= integrationReturnType %>):
|
|
90
|
-
*
|
|
126
|
+
* context?.<%= camelCase(integrationReturnType) %>?.data = {
|
|
91
127
|
<%= integrationReturnFields.flatMap(formatFieldDocLine).join('\n') %>
|
|
92
128
|
* }
|
|
93
129
|
<% } -%>
|
|
@@ -123,28 +159,39 @@ for (const scenario of scenarios) {
|
|
|
123
159
|
}
|
|
124
160
|
}
|
|
125
161
|
}
|
|
162
|
+
const uuidAssignments = (uuidVars || []).filter(v => nonCommandFields.some(f => f.name === v));
|
|
163
|
+
const filteredNonCommandFields = nonCommandFields.filter(f => !(uuidVars || []).includes(f.name));
|
|
126
164
|
const eventResults = scenarios.flatMap(s => s.then.filter(t => 'eventRef' in t));
|
|
127
|
-
|
|
128
|
-
(s.given || []).filter(g =>
|
|
165
|
+
let givenEventsForCmd = scenarios.flatMap(s =>
|
|
166
|
+
(s.given || []).filter(g => allStreamEvents.some(e => e.type === g.eventRef))
|
|
129
167
|
);
|
|
168
|
+
if (givenEventsForCmd.length === 0 && hasGivenStates && hasPrecedingSiblings) {
|
|
169
|
+
givenEventsForCmd = precedingSiblingEvents.map(e => ({
|
|
170
|
+
eventRef: e.type,
|
|
171
|
+
exampleData: siblingExampleData[e.type] || {},
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
130
174
|
const { fields: derivedDateFieldNames } = findDerivedDateInfo(eventResults, cmdFieldSet, givenEventsForCmd);
|
|
131
175
|
const derivedDateFields = new Set(derivedDateFieldNames);
|
|
132
176
|
const keepFieldNamesSet = buildKeepFieldNames(eventResults, cmdFieldSet, derivedDateFieldNames, givenEventsForCmd);
|
|
133
177
|
-%>
|
|
134
|
-
<%
|
|
178
|
+
<% for (const v of uuidAssignments) { -%>
|
|
179
|
+
const <%= v %> = context?.generated?.<%= v %> ?? randomUUID();
|
|
180
|
+
<% } -%>
|
|
181
|
+
<% if (filteredNonCommandFields.length > 0) { -%>
|
|
135
182
|
// ⚠️ REQUIRED: Your return value MUST include ALL fields defined in the event type.
|
|
136
183
|
// Tests use partial matching and may not check every field — passing tests does NOT mean all fields are present.
|
|
137
184
|
// Do NOT use 'as <%= fallbackEventTypes[0] ? pascalCase(fallbackEventTypes[0]) : 'EventType' %>' to silence missing fields.
|
|
138
185
|
//
|
|
139
186
|
// Fields from command input → use ...command.data or command.data.<fieldName>
|
|
140
187
|
// Fields NOT in command input → produce dynamically (never hardcode):
|
|
141
|
-
<% for (const f of
|
|
188
|
+
<% for (const f of filteredNonCommandFields) {
|
|
142
189
|
const isDerivedDate = derivedDateFields.has(f.name);
|
|
143
190
|
const isAssertedByTest = keepFieldNamesSet.has(f.name);
|
|
144
191
|
-%>
|
|
145
192
|
<% if (isDerivedDate) { -%>
|
|
146
193
|
// <%= f.name %>: <%= f.type || f.tsType %> — derive from command.metadata?.now (convert Date to ISO string, e.g., .toISOString().substring(0, 10))
|
|
147
|
-
<% } else if (
|
|
194
|
+
<% } else if (effectiveHasGivenEvents) { -%>
|
|
148
195
|
// <%= f.name %>: <%= f.type || f.tsType %> — derive from _state, generate at runtime (e.g., crypto.randomUUID()), or compute from command.data
|
|
149
196
|
<% } else if (!isAssertedByTest) { -%>
|
|
150
197
|
// <%= f.name %>: <%= f.type || f.tsType %> — not asserted by test; produce a valid runtime value (e.g., crypto.randomUUID() for IDs, '' for other strings)
|
|
@@ -152,14 +199,14 @@ const keepFieldNamesSet = buildKeepFieldNames(eventResults, cmdFieldSet, derived
|
|
|
152
199
|
// <%= f.name %>: <%= f.type || f.tsType %> — generate at runtime (e.g., crypto.randomUUID()), or compute from command.data
|
|
153
200
|
<% } -%>
|
|
154
201
|
<% } -%>
|
|
155
|
-
<% } else { -%>
|
|
202
|
+
<% } else if (uuidAssignments.length === 0) { -%>
|
|
156
203
|
// All event fields come from command input — use ...command.data to pass them through.
|
|
157
204
|
<% } -%>
|
|
158
205
|
|
|
159
206
|
// IMPLEMENT: Use a typed variable to prevent type widening:
|
|
160
207
|
// const result: <%= pascalCase(fallbackEventTypes[0] ?? 'TODO_EVENT_TYPE') %> = {
|
|
161
208
|
// type: '<%= fallbackEventTypes[0] %>',
|
|
162
|
-
// data: { ...command.data<%=
|
|
209
|
+
// data: { ...command.data<%= uuidAssignments.length > 0 ? `, ${uuidAssignments.join(', ')}` : '' %><%= filteredNonCommandFields.length > 0 ? `, /* + produce: ${filteredNonCommandFields.map(f => f.name).join(', ')} */` : '' %> },
|
|
163
210
|
// };
|
|
164
211
|
// return result;
|
|
165
212
|
|
|
@@ -169,4 +216,4 @@ throw new IllegalStateError('Not yet implemented: ' + command.type);
|
|
|
169
216
|
default:
|
|
170
217
|
throw new IllegalStateError('Unexpected command type');
|
|
171
218
|
}
|
|
172
|
-
};
|
|
219
|
+
};
|