@auto-engineer/server-generator-apollo-emmett 1.88.0 → 1.90.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +5 -5
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +90 -0
  5. package/dist/src/codegen/extract/slice-normalizer.d.ts.map +1 -1
  6. package/dist/src/codegen/extract/slice-normalizer.js +14 -0
  7. package/dist/src/codegen/extract/slice-normalizer.js.map +1 -1
  8. package/dist/src/codegen/extract/type-helpers.d.ts +10 -0
  9. package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -1
  10. package/dist/src/codegen/extract/type-helpers.js +17 -0
  11. package/dist/src/codegen/extract/type-helpers.js.map +1 -1
  12. package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
  13. package/dist/src/codegen/scaffoldFromSchema.js +6 -4
  14. package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
  15. package/dist/src/codegen/templates/command/decide.specs.specs.ts +293 -34
  16. package/dist/src/codegen/templates/command/decide.specs.ts +34 -14
  17. package/dist/src/codegen/templates/command/decide.specs.ts.ejs +47 -14
  18. package/dist/src/codegen/templates/command/decide.ts.ejs +32 -4
  19. package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +72 -1
  20. package/dist/src/codegen/templates/command/mutation.resolver.ts.ejs +1 -1
  21. package/dist/src/codegen/templates/query/projection.specs.specs.ts +124 -0
  22. package/dist/src/codegen/templates/query/projection.specs.ts +20 -0
  23. package/dist/src/codegen/templates/query/projection.specs.ts.ejs +5 -1
  24. package/dist/src/codegen/templates/query/projection.ts.ejs +5 -0
  25. package/dist/src/codegen/templates/query/query.resolver.ts.ejs +1 -1
  26. package/dist/src/codegen/templates/react/react.specs.specs.ts +115 -0
  27. package/dist/src/codegen/templates/react/react.specs.ts +9 -2
  28. package/dist/src/codegen/templates/react/react.specs.ts.ejs +1 -3
  29. package/dist/src/codegen/templates/react/react.ts.ejs +22 -9
  30. package/dist/src/codegen/templates/react/react.ts.specs.ts +253 -0
  31. package/dist/src/codegen/templates/react/register.specs.ts +27 -23
  32. package/dist/src/codegen/templates/react/register.ts.ejs +5 -1
  33. package/dist/tsconfig.tsbuildinfo +1 -1
  34. package/ketchup-plan.md +12 -3
  35. package/package.json +4 -4
  36. package/src/codegen/extract/slice-normalizer.specs.ts +83 -0
  37. package/src/codegen/extract/slice-normalizer.ts +15 -0
  38. package/src/codegen/extract/type-helpers.specs.ts +77 -1
  39. package/src/codegen/extract/type-helpers.ts +23 -0
  40. package/src/codegen/formatTsValueSimple.specs.ts +8 -0
  41. package/src/codegen/scaffoldFromSchema.ts +7 -3
  42. package/src/codegen/templates/command/decide.specs.specs.ts +293 -34
  43. package/src/codegen/templates/command/decide.specs.ts +34 -14
  44. package/src/codegen/templates/command/decide.specs.ts.ejs +47 -14
  45. package/src/codegen/templates/command/decide.ts.ejs +32 -4
  46. package/src/codegen/templates/command/mutation.resolver.specs.ts +72 -1
  47. package/src/codegen/templates/command/mutation.resolver.ts.ejs +1 -1
  48. package/src/codegen/templates/query/projection.specs.specs.ts +124 -0
  49. package/src/codegen/templates/query/projection.specs.ts +20 -0
  50. package/src/codegen/templates/query/projection.specs.ts.ejs +5 -1
  51. package/src/codegen/templates/query/projection.ts.ejs +5 -0
  52. package/src/codegen/templates/query/query.resolver.ts.ejs +1 -1
  53. package/src/codegen/templates/react/react.specs.specs.ts +115 -0
  54. package/src/codegen/templates/react/react.specs.ts +9 -2
  55. package/src/codegen/templates/react/react.specs.ts.ejs +1 -3
  56. package/src/codegen/templates/react/react.ts.ejs +22 -9
  57. package/src/codegen/templates/react/react.ts.specs.ts +253 -0
  58. package/src/codegen/templates/react/register.specs.ts +27 -23
  59. package/src/codegen/templates/react/register.ts.ejs +5 -1
  60. package/dist/src/codegen/extract/graphql.d.ts +0 -14
  61. package/dist/src/codegen/extract/graphql.d.ts.map +0 -1
  62. package/dist/src/codegen/extract/graphql.js +0 -81
  63. package/dist/src/codegen/extract/graphql.js.map +0 -1
  64. package/src/codegen/extract/graphql.ts +0 -103
@@ -194,6 +194,8 @@ describe('react.ts.ejs', () => {
194
194
  expect(reactFile?.contents).toContain('commandSender.send({');
195
195
  expect(reactFile?.contents).toContain('event.data.orderId');
196
196
  expect(reactFile?.contents).toContain('undefined, // TODO: source unknown');
197
+ expect(reactFile?.contents).toContain('NEVER hardcode values copied from test assertions');
198
+ expect(reactFile?.contents).toContain('Preserve all import paths');
197
199
  expect(reactFile?.contents).not.toContain('throw new IllegalStateError');
198
200
  });
199
201
 
@@ -551,6 +553,8 @@ export type BarberNotified = Event<
551
553
  const reactFile = plans.find((p) => p.outputPath.endsWith('notify-barber-of-cancellation/react.ts'));
552
554
 
553
555
  expect(reactFile?.contents).toContain('aggregateStream');
556
+ expect(reactFile?.contents).toContain('Record<string, unknown>');
557
+ expect(reactFile?.contents).toContain('Do NOT modify or remove aggregateStream');
554
558
  expect(reactFile?.contents).toContain('event.data.barberId');
555
559
  expect(reactFile?.contents).toContain('appointment');
556
560
  expect(reactFile?.contents).not.toContain('readable from database');
@@ -562,4 +566,253 @@ export type BarberNotified = Event<
562
566
  expect(reactFile?.contents).not.toContain('throw new IllegalStateError');
563
567
  expect(reactFile?.contents).not.toContain('// Example:');
564
568
  });
569
+
570
+ it('should ignore event And-steps in react Then, using only commands', async () => {
571
+ const spec: SpecsSchema = {
572
+ variant: 'specs',
573
+ narratives: [
574
+ {
575
+ name: 'notification flow',
576
+ slices: [
577
+ {
578
+ type: 'command',
579
+ name: 'earn points',
580
+ client: { specs: [] },
581
+ server: {
582
+ description: '',
583
+ specs: [
584
+ {
585
+ type: 'gherkin',
586
+ feature: 'Earn points',
587
+ rules: [
588
+ {
589
+ name: 'Should earn',
590
+ examples: [
591
+ {
592
+ name: 'Points earned',
593
+ steps: [
594
+ { keyword: 'When', text: 'EarnPoints', docString: { memberId: 'm1', amount: 10 } },
595
+ { keyword: 'Then', text: 'PointsEarned', docString: { memberId: 'm1', amount: 10 } },
596
+ ],
597
+ },
598
+ ],
599
+ },
600
+ ],
601
+ },
602
+ ],
603
+ },
604
+ },
605
+ {
606
+ type: 'react',
607
+ name: 'notify milestone',
608
+ server: {
609
+ description: 'Notifies on milestone',
610
+ data: {
611
+ items: [
612
+ {
613
+ target: { type: 'Command', name: 'SendNotification' },
614
+ destination: { type: 'stream', pattern: 'notif-${memberId}' },
615
+ },
616
+ ],
617
+ },
618
+ specs: [
619
+ {
620
+ type: 'gherkin',
621
+ feature: 'Milestone notification',
622
+ rules: [
623
+ {
624
+ name: 'Should notify',
625
+ examples: [
626
+ {
627
+ name: 'Milestone reached',
628
+ steps: [
629
+ { keyword: 'When', text: 'PointsEarned', docString: { memberId: 'm1', amount: 10 } },
630
+ { keyword: 'Then', text: 'MilestoneNotified', docString: { memberId: 'm1' } },
631
+ { keyword: 'And', text: 'SendNotification', docString: { memberId: 'm1' } },
632
+ ],
633
+ },
634
+ ],
635
+ },
636
+ ],
637
+ },
638
+ ],
639
+ },
640
+ },
641
+ ],
642
+ },
643
+ ],
644
+ messages: [
645
+ {
646
+ type: 'command',
647
+ name: 'EarnPoints',
648
+ fields: [
649
+ { name: 'memberId', type: 'string', required: true },
650
+ { name: 'amount', type: 'number', required: true },
651
+ ],
652
+ },
653
+ {
654
+ type: 'event',
655
+ name: 'PointsEarned',
656
+ source: 'internal',
657
+ fields: [
658
+ { name: 'memberId', type: 'string', required: true },
659
+ { name: 'amount', type: 'number', required: true },
660
+ ],
661
+ },
662
+ {
663
+ type: 'command',
664
+ name: 'SendNotification',
665
+ fields: [{ name: 'memberId', type: 'string', required: true }],
666
+ },
667
+ {
668
+ type: 'event',
669
+ name: 'MilestoneNotified',
670
+ source: 'internal',
671
+ fields: [{ name: 'memberId', type: 'string', required: true }],
672
+ },
673
+ ],
674
+ };
675
+
676
+ const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
677
+ const reactFile = plans.find((p) => p.outputPath.endsWith('notify-milestone/react.ts'));
678
+
679
+ expect(reactFile?.contents).toContain("type: 'SendNotification'");
680
+ expect(reactFile?.contents).not.toContain('MilestoneNotified');
681
+ });
682
+
683
+ it('should use primitive linking field, skipping array/object fields', async () => {
684
+ const spec: SpecsSchema = {
685
+ variant: 'specs',
686
+ narratives: [
687
+ {
688
+ name: 'fitness flow',
689
+ slices: [
690
+ {
691
+ type: 'command',
692
+ name: 'log workout',
693
+ client: { specs: [] },
694
+ server: {
695
+ description: '',
696
+ specs: [
697
+ {
698
+ type: 'gherkin',
699
+ feature: 'Log workout',
700
+ rules: [
701
+ {
702
+ name: 'Should log workout',
703
+ examples: [
704
+ {
705
+ name: 'Workout logged',
706
+ steps: [
707
+ {
708
+ keyword: 'When',
709
+ text: 'LogWorkout',
710
+ docString: { memberId: 'm1', exercises: [{ exercise: 'squat', sets: 3 }] },
711
+ },
712
+ {
713
+ keyword: 'Then',
714
+ text: 'WorkoutLogged',
715
+ docString: { memberId: 'm1', exercises: [{ exercise: 'squat', sets: 3 }] },
716
+ },
717
+ ],
718
+ },
719
+ ],
720
+ },
721
+ ],
722
+ },
723
+ ],
724
+ },
725
+ },
726
+ {
727
+ type: 'react',
728
+ name: 'check record updates',
729
+ server: {
730
+ description: 'Updates records after workout',
731
+ data: {
732
+ items: [
733
+ {
734
+ target: { type: 'Command', name: 'UpdateRecord' },
735
+ destination: { type: 'stream', pattern: 'records-${memberId}' },
736
+ },
737
+ ],
738
+ },
739
+ specs: [
740
+ {
741
+ type: 'gherkin',
742
+ feature: 'Check record updates',
743
+ rules: [
744
+ {
745
+ name: 'Should update records',
746
+ examples: [
747
+ {
748
+ name: 'Record updated',
749
+ steps: [
750
+ {
751
+ keyword: 'Given',
752
+ text: 'WorkoutSummary',
753
+ docString: { memberId: 'm1', exercises: [{ exercise: 'squat', sets: 3 }] },
754
+ },
755
+ {
756
+ keyword: 'When',
757
+ text: 'WorkoutLogged',
758
+ docString: { memberId: 'm1', exercises: [{ exercise: 'squat', sets: 3 }] },
759
+ },
760
+ {
761
+ keyword: 'Then',
762
+ text: 'UpdateRecord',
763
+ docString: { memberId: 'm1' },
764
+ },
765
+ ],
766
+ },
767
+ ],
768
+ },
769
+ ],
770
+ },
771
+ ],
772
+ },
773
+ },
774
+ ],
775
+ },
776
+ ],
777
+ messages: [
778
+ {
779
+ type: 'command',
780
+ name: 'LogWorkout',
781
+ fields: [
782
+ { name: 'memberId', type: 'string', required: true },
783
+ { name: 'exercises', type: '{exercise:string,sets:number}[]', required: true },
784
+ ],
785
+ },
786
+ {
787
+ type: 'event',
788
+ name: 'WorkoutLogged',
789
+ source: 'internal',
790
+ fields: [
791
+ { name: 'memberId', type: 'string', required: true },
792
+ { name: 'exercises', type: '{exercise:string,sets:number}[]', required: true },
793
+ ],
794
+ },
795
+ {
796
+ type: 'command',
797
+ name: 'UpdateRecord',
798
+ fields: [{ name: 'memberId', type: 'string', required: true }],
799
+ },
800
+ {
801
+ type: 'state',
802
+ name: 'WorkoutSummary',
803
+ fields: [
804
+ { name: 'memberId', type: 'string', required: true },
805
+ { name: 'exercises', type: '{exercise:string,sets:number}[]', required: true },
806
+ ],
807
+ },
808
+ ],
809
+ };
810
+
811
+ const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
812
+ const reactFile = plans.find((p) => p.outputPath.endsWith('check-record-updates/react.ts'));
813
+
814
+ expect(reactFile?.contents).toContain("'WorkoutSummary-' + event.data.memberId");
815
+ expect(reactFile?.contents).toContain('aggregateStream');
816
+ expect(reactFile?.contents).not.toContain('event.data.exercises');
817
+ });
565
818
  });
@@ -232,31 +232,35 @@ describe('register.ts.ejs (react slice)', () => {
232
232
  const registerFile = plans.find((p) => p.outputPath.endsWith('send-notification-to-host/register.ts'));
233
233
 
234
234
  expect(registerFile?.contents).toMatchInlineSnapshot(`
235
- "import { type CommandSender, type EventSubscription, type EventStore } from '@event-driven-io/emmett';
236
- import type { BookingRequested } from '../guest-submits-booking-request/events';
235
+ "import { type CommandSender, type EventSubscription, type EventStore } from '@event-driven-io/emmett';
236
+ import type { BookingRequested } from '../guest-submits-booking-request/events';
237
237
 
238
- export async function register(messageBus: CommandSender & EventSubscription, eventStore: EventStore) {
239
- messageBus.subscribe(async (event: BookingRequested) => {
240
- /**
241
- * ## IMPLEMENTATION INSTRUCTIONS ##
242
- *
243
- * - Replace the placeholder logic with the real implementation.
244
- * - Send one or more commands via: messageBus.send({...})
245
- */
238
+ export async function register(messageBus: CommandSender & EventSubscription, eventStore: EventStore) {
239
+ messageBus.subscribe(async (event: BookingRequested) => {
240
+ /**
241
+ * ## IMPLEMENTATION INSTRUCTIONS ##
242
+ *
243
+ * - Replace the placeholder logic with the real implementation.
244
+ * - Send one or more commands via: messageBus.send({...})
245
+ * - If calling eventStore.aggregateStream(), type the evolve callback:
246
+ * evolve: (s: Record<string, unknown>, e: { type: string; data: Record<string, unknown> }) => ...
247
+ * - NEVER hardcode values copied from test assertions.
248
+ * - Preserve all import paths above — they are generated from the model.
249
+ */
246
250
 
247
- // await messageBus.send({
248
- // type: 'NotifyHost',
249
- // kind: 'Command',
250
- // data: {
251
- // // Map event fields to command fields here
252
- // // e.g., userId: event.data.userId,
253
- // },
254
- // });
251
+ // await messageBus.send({
252
+ // type: 'NotifyHost',
253
+ // kind: 'Command',
254
+ // data: {
255
+ // // Map event fields to command fields here
256
+ // // e.g., userId: event.data.userId,
257
+ // },
258
+ // });
255
259
 
256
- return;
257
- }, 'BookingRequested');
258
- }
259
- "
260
- `);
260
+ return;
261
+ }, 'BookingRequested');
262
+ }
263
+ "
264
+ `);
261
265
  });
262
266
  });
@@ -32,8 +32,12 @@ async (event: <%= pascalCase(eventType) %>) => {
32
32
  /**
33
33
  * ## IMPLEMENTATION INSTRUCTIONS ##
34
34
  *
35
- * - Replace the placeholder logic with the real implementation.
35
+ * - Replace the placeholder logic with the real implementation.
36
36
  * - Send one or more commands via: messageBus.send({...})
37
+ * - If calling eventStore.aggregateStream(), type the evolve callback:
38
+ * evolve: (s: Record<string, unknown>, e: { type: string; data: Record<string, unknown> }) => ...
39
+ * - NEVER hardcode values copied from test assertions.
40
+ * - Preserve all import paths above — they are generated from the model.
37
41
  */
38
42
 
39
43
  // await messageBus.send({