@auto-engineer/server-generator-apollo-emmett 0.11.12 → 0.11.14

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/package.json CHANGED
@@ -31,8 +31,8 @@
31
31
  "graphql-type-json": "^0.3.2",
32
32
  "uuid": "^11.0.0",
33
33
  "web-streams-polyfill": "^4.1.0",
34
- "@auto-engineer/narrative": "0.11.12",
35
- "@auto-engineer/message-bus": "0.11.12"
34
+ "@auto-engineer/message-bus": "0.11.14",
35
+ "@auto-engineer/narrative": "0.11.14"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public"
@@ -43,9 +43,9 @@
43
43
  "typescript": "^5.8.3",
44
44
  "vitest": "^3.2.4",
45
45
  "tsx": "^4.19.2",
46
- "@auto-engineer/cli": "0.11.12"
46
+ "@auto-engineer/cli": "0.11.14"
47
47
  },
48
- "version": "0.11.12",
48
+ "version": "0.11.14",
49
49
  "scripts": {
50
50
  "generate:server": "tsx src/cli/index.ts",
51
51
  "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",
@@ -23,7 +23,7 @@ function createCommandMessage(
23
23
  export function extractCommandsFromGwt(
24
24
  gwtSpecs: Array<{
25
25
  given?: Array<EventExample | unknown>;
26
- when: CommandExample | EventExample | unknown[];
26
+ when?: CommandExample | EventExample | unknown[];
27
27
  then: Array<EventExample | unknown | { errorType: string; message?: string }>;
28
28
  }>,
29
29
  allMessages: MessageDefinition[],
@@ -86,7 +86,7 @@ function processCommandExample(
86
86
  export function extractCommandsFromThen(
87
87
  gwtSpecs: Array<{
88
88
  given?: Array<EventExample | unknown>;
89
- when: CommandExample | EventExample | unknown[];
89
+ when?: CommandExample | EventExample | unknown[];
90
90
  then: Array<EventExample | unknown | { errorType: string; message?: string }>;
91
91
  }>,
92
92
  allMessages: MessageDefinition[],
@@ -1,5 +1,5 @@
1
1
  import { extractCommandsFromGwt, extractCommandsFromThen } from './commands';
2
- import { CommandExample, ErrorExample, EventExample, Slice, StateExample } from '@auto-engineer/narrative';
2
+ import { CommandExample, EventExample, Slice } from '@auto-engineer/narrative';
3
3
  import { Message, MessageDefinition } from '../types';
4
4
  import { extractEventsFromGiven, extractEventsFromThen, extractEventsFromWhen } from './events';
5
5
  import { extractFieldsFromMessage } from './fields';
@@ -22,21 +22,10 @@ export interface ExtractedMessages {
22
22
  }
23
23
 
24
24
  export interface ReactGwtSpec {
25
- when: EventExample[];
25
+ when?: EventExample[];
26
26
  then: CommandExample[];
27
27
  }
28
28
 
29
- export interface CommandGwtSpec {
30
- given?: EventExample[];
31
- when: CommandExample;
32
- then: Array<EventExample | ErrorExample>;
33
- }
34
-
35
- export interface QueryGwtSpec {
36
- given: EventExample[];
37
- then: StateExample[];
38
- }
39
-
40
29
  const EMPTY_EXTRACTED_MESSAGES: ExtractedMessages = {
41
30
  commands: [],
42
31
  events: [],
@@ -634,7 +634,15 @@ function findCommandSource(flows: Narrative[], commandType: string): { flowName:
634
634
  })),
635
635
  )
636
636
  : [];
637
- if (gwtSpecs.some((g) => !Array.isArray(g.when) && 'commandRef' in g.when && g.when.commandRef === commandType)) {
637
+ if (
638
+ gwtSpecs.some(
639
+ (g) =>
640
+ g.when !== undefined &&
641
+ !Array.isArray(g.when) &&
642
+ 'commandRef' in g.when &&
643
+ g.when.commandRef === commandType,
644
+ )
645
+ ) {
638
646
  debugSlice(' Found command source in flow: %s, slice: %s', flow.name, slice.name);
639
647
  return { flowName: flow.name, sliceName: slice.name };
640
648
  }
@@ -620,4 +620,372 @@ describe('projection.specs.ts.ejs', () => {
620
620
  // canHandle must include BOTH events
621
621
  expect(projectionFile?.contents).toContain("canHandle: ['QuestionnaireLinkSent', 'QuestionAnswered']");
622
622
  });
623
+
624
+ it('should generate a valid test spec for singleton projection', async () => {
625
+ const spec: SpecsSchema = {
626
+ variant: 'specs',
627
+ narratives: [
628
+ {
629
+ name: 'todo-flow',
630
+ slices: [
631
+ {
632
+ type: 'command',
633
+ name: 'manage-todo',
634
+ stream: 'todo-${todoId}',
635
+ client: { description: '' },
636
+ server: {
637
+ description: '',
638
+ specs: {
639
+ name: 'Manage todo command',
640
+ rules: [
641
+ {
642
+ description: 'Should handle todo operations',
643
+ examples: [
644
+ {
645
+ description: 'User adds todo',
646
+ when: {
647
+ commandRef: 'AddTodo',
648
+ exampleData: {
649
+ todoId: 'todo_123',
650
+ title: 'Buy milk',
651
+ },
652
+ },
653
+ then: [
654
+ {
655
+ eventRef: 'TodoAdded',
656
+ exampleData: {
657
+ todoId: 'todo_123',
658
+ title: 'Buy milk',
659
+ },
660
+ },
661
+ ],
662
+ },
663
+ ],
664
+ },
665
+ ],
666
+ },
667
+ },
668
+ },
669
+ {
670
+ type: 'query',
671
+ name: 'view-summary',
672
+ stream: 'todos',
673
+ client: { description: '' },
674
+ server: {
675
+ description: '',
676
+ data: [
677
+ {
678
+ target: {
679
+ type: 'State',
680
+ name: 'TodoSummary',
681
+ },
682
+ origin: {
683
+ type: 'projection',
684
+ name: 'TodoSummaryProjection',
685
+ singleton: true,
686
+ },
687
+ },
688
+ ],
689
+ specs: {
690
+ name: 'View summary query',
691
+ rules: [
692
+ {
693
+ description: 'Should aggregate todo counts',
694
+ examples: [
695
+ {
696
+ description: 'Todo added updates count',
697
+ when: [
698
+ {
699
+ eventRef: 'TodoAdded',
700
+ exampleData: {
701
+ todoId: 'todo_123',
702
+ title: 'Buy milk',
703
+ },
704
+ },
705
+ ],
706
+ then: [
707
+ {
708
+ stateRef: 'TodoSummary',
709
+ exampleData: {
710
+ totalCount: 1,
711
+ },
712
+ },
713
+ ],
714
+ },
715
+ ],
716
+ },
717
+ ],
718
+ },
719
+ },
720
+ },
721
+ ],
722
+ },
723
+ ],
724
+ messages: [
725
+ {
726
+ type: 'command',
727
+ name: 'AddTodo',
728
+ fields: [
729
+ { name: 'todoId', type: 'string', required: true },
730
+ { name: 'title', type: 'string', required: true },
731
+ ],
732
+ },
733
+ {
734
+ type: 'event',
735
+ name: 'TodoAdded',
736
+ source: 'internal',
737
+ fields: [
738
+ { name: 'todoId', type: 'string', required: true },
739
+ { name: 'title', type: 'string', required: true },
740
+ ],
741
+ },
742
+ {
743
+ type: 'state',
744
+ name: 'TodoSummary',
745
+ fields: [{ name: 'totalCount', type: 'number', required: true }],
746
+ },
747
+ ],
748
+ } as SpecsSchema;
749
+
750
+ const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
751
+ const specFile = plans.find((p) => p.outputPath.endsWith('view-summary/projection.specs.ts'));
752
+
753
+ expect(specFile?.contents).toMatchInlineSnapshot(`
754
+ "import { describe, it, beforeEach, expect } from 'vitest';
755
+ import { InMemoryProjectionSpec } from '@event-driven-io/emmett';
756
+ import { projection } from './projection';
757
+ import type { TodoAdded } from '../manage-todo/events';
758
+ import { TodoSummary } from './state';
759
+
760
+ type ProjectionEvent = TodoAdded;
761
+
762
+ describe('Should aggregate todo counts', () => {
763
+ let given: InMemoryProjectionSpec<ProjectionEvent>;
764
+
765
+ beforeEach(() => {
766
+ given = InMemoryProjectionSpec.for({ projection });
767
+ });
768
+
769
+ it('Todo added updates count', () =>
770
+ given([])
771
+ .when([
772
+ {
773
+ type: 'TodoAdded',
774
+ data: {
775
+ todoId: 'todo_123',
776
+ title: 'Buy milk',
777
+ },
778
+ metadata: {
779
+ streamName: 'todos',
780
+ streamPosition: 1n,
781
+ globalPosition: 1n,
782
+ },
783
+ },
784
+ ])
785
+ .then(async (state) => {
786
+ const document = await state.database
787
+ .collection<TodoSummary>('TodoSummaryProjection')
788
+ .findOne((doc) => doc.id === 'test-id');
789
+
790
+ const expected: TodoSummary = {
791
+ totalCount: 1,
792
+ };
793
+
794
+ expect(document).toMatchObject(expected);
795
+ }));
796
+ });
797
+ "
798
+ `);
799
+ });
800
+
801
+ it('should generate a valid test spec for composite key projection', async () => {
802
+ const spec: SpecsSchema = {
803
+ variant: 'specs',
804
+ narratives: [
805
+ {
806
+ name: 'user-project-flow',
807
+ slices: [
808
+ {
809
+ type: 'command',
810
+ name: 'manage-user-project',
811
+ stream: 'user-project-${userId}-${projectId}',
812
+ client: { description: '' },
813
+ server: {
814
+ description: '',
815
+ specs: {
816
+ name: 'Manage user project command',
817
+ rules: [
818
+ {
819
+ description: 'Should handle user project operations',
820
+ examples: [
821
+ {
822
+ description: 'User joins project',
823
+ when: {
824
+ commandRef: 'JoinProject',
825
+ exampleData: {
826
+ userId: 'user_123',
827
+ projectId: 'proj_456',
828
+ role: 'developer',
829
+ },
830
+ },
831
+ then: [
832
+ {
833
+ eventRef: 'UserJoinedProject',
834
+ exampleData: {
835
+ userId: 'user_123',
836
+ projectId: 'proj_456',
837
+ role: 'developer',
838
+ },
839
+ },
840
+ ],
841
+ },
842
+ ],
843
+ },
844
+ ],
845
+ },
846
+ },
847
+ },
848
+ {
849
+ type: 'query',
850
+ name: 'view-user-projects',
851
+ stream: 'user-projects',
852
+ client: { description: '' },
853
+ server: {
854
+ description: '',
855
+ data: [
856
+ {
857
+ target: {
858
+ type: 'State',
859
+ name: 'UserProject',
860
+ },
861
+ origin: {
862
+ type: 'projection',
863
+ name: 'UserProjectsProjection',
864
+ idField: ['userId', 'projectId'],
865
+ },
866
+ },
867
+ ],
868
+ specs: {
869
+ name: 'View user projects query',
870
+ rules: [
871
+ {
872
+ description: 'Should track user project memberships',
873
+ examples: [
874
+ {
875
+ description: 'User joins project',
876
+ when: [
877
+ {
878
+ eventRef: 'UserJoinedProject',
879
+ exampleData: {
880
+ userId: 'user_123',
881
+ projectId: 'proj_456',
882
+ role: 'developer',
883
+ },
884
+ },
885
+ ],
886
+ then: [
887
+ {
888
+ stateRef: 'UserProject',
889
+ exampleData: {
890
+ userId: 'user_123',
891
+ projectId: 'proj_456',
892
+ role: 'developer',
893
+ },
894
+ },
895
+ ],
896
+ },
897
+ ],
898
+ },
899
+ ],
900
+ },
901
+ },
902
+ },
903
+ ],
904
+ },
905
+ ],
906
+ messages: [
907
+ {
908
+ type: 'command',
909
+ name: 'JoinProject',
910
+ fields: [
911
+ { name: 'userId', type: 'string', required: true },
912
+ { name: 'projectId', type: 'string', required: true },
913
+ { name: 'role', type: 'string', required: true },
914
+ ],
915
+ },
916
+ {
917
+ type: 'event',
918
+ name: 'UserJoinedProject',
919
+ source: 'internal',
920
+ fields: [
921
+ { name: 'userId', type: 'string', required: true },
922
+ { name: 'projectId', type: 'string', required: true },
923
+ { name: 'role', type: 'string', required: true },
924
+ ],
925
+ },
926
+ {
927
+ type: 'state',
928
+ name: 'UserProject',
929
+ fields: [
930
+ { name: 'userId', type: 'string', required: true },
931
+ { name: 'projectId', type: 'string', required: true },
932
+ { name: 'role', type: 'string', required: true },
933
+ ],
934
+ },
935
+ ],
936
+ } as SpecsSchema;
937
+
938
+ const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
939
+ const specFile = plans.find((p) => p.outputPath.endsWith('view-user-projects/projection.specs.ts'));
940
+
941
+ expect(specFile?.contents).toMatchInlineSnapshot(`
942
+ "import { describe, it, beforeEach, expect } from 'vitest';
943
+ import { InMemoryProjectionSpec } from '@event-driven-io/emmett';
944
+ import { projection } from './projection';
945
+ import type { UserJoinedProject } from '../manage-user-project/events';
946
+ import { UserProject } from './state';
947
+
948
+ type ProjectionEvent = UserJoinedProject;
949
+
950
+ describe('Should track user project memberships', () => {
951
+ let given: InMemoryProjectionSpec<ProjectionEvent>;
952
+
953
+ beforeEach(() => {
954
+ given = InMemoryProjectionSpec.for({ projection });
955
+ });
956
+
957
+ it('User joins project', () =>
958
+ given([])
959
+ .when([
960
+ {
961
+ type: 'UserJoinedProject',
962
+ data: {
963
+ userId: 'user_123',
964
+ projectId: 'proj_456',
965
+ role: 'developer',
966
+ },
967
+ metadata: {
968
+ streamName: 'user-projects',
969
+ streamPosition: 1n,
970
+ globalPosition: 1n,
971
+ },
972
+ },
973
+ ])
974
+ .then(async (state) => {
975
+ const document = await state.database
976
+ .collection<UserProject>('UserProjectsProjection')
977
+ .findOne((doc) => doc.id === 'test-id');
978
+
979
+ const expected: UserProject = {
980
+ userId: 'user_123',
981
+ projectId: 'proj_456',
982
+ role: 'developer',
983
+ };
984
+
985
+ expect(document).toMatchObject(expected);
986
+ }));
987
+ });
988
+ "
989
+ `);
990
+ });
623
991
  });