@auto-engineer/server-generator-apollo-emmett 1.87.0 → 1.89.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 +97 -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/extract/slice-normalizer.d.ts.map +1 -1
- package/dist/src/codegen/extract/slice-normalizer.js +14 -0
- package/dist/src/codegen/extract/slice-normalizer.js.map +1 -1
- package/dist/src/codegen/extract/type-helpers.d.ts +10 -0
- package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -1
- package/dist/src/codegen/extract/type-helpers.js +17 -0
- package/dist/src/codegen/extract/type-helpers.js.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +6 -4
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +599 -34
- package/dist/src/codegen/templates/command/decide.specs.ts +38 -18
- package/dist/src/codegen/templates/command/decide.specs.ts.ejs +65 -6
- package/dist/src/codegen/templates/command/decide.ts.ejs +33 -5
- package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +72 -1
- package/dist/src/codegen/templates/command/mutation.resolver.ts.ejs +1 -1
- package/dist/src/codegen/templates/query/projection.specs.specs.ts +298 -1
- package/dist/src/codegen/templates/query/projection.specs.ts +20 -0
- package/dist/src/codegen/templates/query/projection.specs.ts.ejs +43 -13
- package/dist/src/codegen/templates/query/projection.ts.ejs +5 -0
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +1 -1
- package/dist/src/codegen/templates/react/react.specs.specs.ts +115 -0
- package/dist/src/codegen/templates/react/react.specs.ts +9 -2
- package/dist/src/codegen/templates/react/react.specs.ts.ejs +1 -3
- package/dist/src/codegen/templates/react/react.ts.ejs +22 -9
- package/dist/src/codegen/templates/react/react.ts.specs.ts +253 -0
- package/dist/src/codegen/templates/react/register.specs.ts +27 -23
- package/dist/src/codegen/templates/react/register.ts.ejs +5 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +14 -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/extract/slice-normalizer.specs.ts +83 -0
- package/src/codegen/extract/slice-normalizer.ts +15 -0
- package/src/codegen/extract/type-helpers.specs.ts +77 -1
- package/src/codegen/extract/type-helpers.ts +23 -0
- package/src/codegen/formatTsValueSimple.specs.ts +8 -0
- package/src/codegen/scaffoldFromSchema.ts +8 -4
- package/src/codegen/templates/command/decide.specs.specs.ts +599 -34
- package/src/codegen/templates/command/decide.specs.ts +38 -18
- package/src/codegen/templates/command/decide.specs.ts.ejs +65 -6
- package/src/codegen/templates/command/decide.ts.ejs +33 -5
- package/src/codegen/templates/command/mutation.resolver.specs.ts +72 -1
- package/src/codegen/templates/command/mutation.resolver.ts.ejs +1 -1
- package/src/codegen/templates/query/projection.specs.specs.ts +298 -1
- package/src/codegen/templates/query/projection.specs.ts +20 -0
- package/src/codegen/templates/query/projection.specs.ts.ejs +43 -13
- package/src/codegen/templates/query/projection.ts.ejs +5 -0
- package/src/codegen/templates/query/query.resolver.ts.ejs +1 -1
- package/src/codegen/templates/react/react.specs.specs.ts +115 -0
- package/src/codegen/templates/react/react.specs.ts +9 -2
- package/src/codegen/templates/react/react.specs.ts.ejs +1 -3
- package/src/codegen/templates/react/react.ts.ejs +22 -9
- package/src/codegen/templates/react/react.ts.specs.ts +253 -0
- package/src/codegen/templates/react/register.specs.ts +27 -23
- package/src/codegen/templates/react/register.ts.ejs +5 -1
- package/dist/src/codegen/extract/graphql.d.ts +0 -14
- package/dist/src/codegen/extract/graphql.d.ts.map +0 -1
- package/dist/src/codegen/extract/graphql.js +0 -81
- package/dist/src/codegen/extract/graphql.js.map +0 -1
- package/src/codegen/extract/graphql.ts +0 -103
|
@@ -39,15 +39,30 @@ connectionOptions: {
|
|
|
39
39
|
database,
|
|
40
40
|
},
|
|
41
41
|
eachMessage: async (event, context): Promise<MessageHandlerResult> => {
|
|
42
|
+
<%
|
|
43
|
+
const eventDef = messages.find(m => m.name === eventType);
|
|
44
|
+
const commandDef = messages.find(m => m.name === commandType && m.type === 'command');
|
|
45
|
+
const willHaveAggregateStream = states.some(state =>
|
|
46
|
+
findPrimitiveLinkingField(state.fields, eventDef?.fields || []) !== undefined
|
|
47
|
+
);
|
|
48
|
+
-%>
|
|
42
49
|
/**
|
|
43
50
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
44
51
|
*
|
|
45
|
-
*
|
|
46
|
-
* -
|
|
52
|
+
* Complete the command send below. Field sources are pre-classified:
|
|
53
|
+
* - `event.data.<field>` → already wired from the triggering event.
|
|
54
|
+
* - `<stateVar>.<field>` → already wired from loaded aggregate state.
|
|
55
|
+
* - `undefined, // TODO: source unknown` → MUST be dynamically derived:
|
|
56
|
+
* compute from event.data, loaded state, or runtime logic.
|
|
57
|
+
* NEVER hardcode values copied from test assertions.
|
|
58
|
+
*
|
|
59
|
+
* Preserve all import paths above — they are generated from the model.
|
|
60
|
+
<% if (willHaveAggregateStream) { -%>
|
|
61
|
+
* Do NOT modify or remove aggregateStream calls — they load required state.
|
|
62
|
+
<% } -%>
|
|
63
|
+
* Add business logic (validation, conditional sends) as needed.
|
|
47
64
|
*/
|
|
48
65
|
<%
|
|
49
|
-
const eventDef = messages.find(m => m.name === eventType);
|
|
50
|
-
const commandDef = messages.find(m => m.name === commandType && m.type === 'command');
|
|
51
66
|
const commandFields = (commandDef?.fields || []);
|
|
52
67
|
const eventFieldSet = new Set((eventDef?.fields || []).map(f => f.name));
|
|
53
68
|
const stateFieldSources = {};
|
|
@@ -61,9 +76,7 @@ const stateFieldSources = {};
|
|
|
61
76
|
<% if (states.length > 0) {
|
|
62
77
|
let hasAggregateStream = false;
|
|
63
78
|
for (const state of states) {
|
|
64
|
-
const
|
|
65
|
-
const eventFieldNames = (eventDef?.fields || []).map(f => f.name);
|
|
66
|
-
const linkingField = stateFieldNames.find(f => eventFieldNames.includes(f));
|
|
79
|
+
const linkingField = findPrimitiveLinkingField(state.fields, eventDef?.fields || []);
|
|
67
80
|
if (linkingField) {
|
|
68
81
|
hasAggregateStream = true;
|
|
69
82
|
const varName = camelCase(state.type);
|
|
@@ -75,8 +88,8 @@ const stateFieldSources = {};
|
|
|
75
88
|
const { state: <%= varName %> } = await eventStore.aggregateStream(
|
|
76
89
|
'<%= state.type %>-' + event.data.<%= linkingField %>,
|
|
77
90
|
{
|
|
78
|
-
evolve: (currentState, evt) => ({ ...currentState, ...evt.data }),
|
|
79
|
-
initialState: () => ({}),
|
|
91
|
+
evolve: (currentState: Record<string, unknown>, evt: { type: string; data: Record<string, unknown> }) => ({ ...currentState, ...evt.data }),
|
|
92
|
+
initialState: (): Record<string, unknown> => ({}),
|
|
80
93
|
},
|
|
81
94
|
);
|
|
82
95
|
// <%= state.type %> fields: <%= state.fields.map(f => f.name).join(', ') %>
|
|
@@ -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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
|
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({
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export interface ParsedArg {
|
|
2
|
-
name: string;
|
|
3
|
-
tsType: string;
|
|
4
|
-
graphqlType: string;
|
|
5
|
-
nullable: boolean;
|
|
6
|
-
}
|
|
7
|
-
export interface ParsedGraphQlQuery {
|
|
8
|
-
queryName: string;
|
|
9
|
-
args: ParsedArg[];
|
|
10
|
-
returnType: string;
|
|
11
|
-
tsReturnType: string;
|
|
12
|
-
}
|
|
13
|
-
export declare function parseGraphQlRequest(request: string): ParsedGraphQlQuery;
|
|
14
|
-
//# sourceMappingURL=graphql.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../../../../src/codegen/extract/graphql.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AA8CD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,CAsCvE"}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { parse, print } from 'graphql';
|
|
2
|
-
function getTypeName(typeNode) {
|
|
3
|
-
if (typeNode.kind === 'NamedType') {
|
|
4
|
-
return { graphqlType: typeNode.name.value, nullable: true };
|
|
5
|
-
}
|
|
6
|
-
else if (typeNode.kind === 'NonNullType') {
|
|
7
|
-
const inner = getTypeName(typeNode.type);
|
|
8
|
-
return { ...inner, nullable: false };
|
|
9
|
-
}
|
|
10
|
-
else {
|
|
11
|
-
return getTypeName(typeNode.type);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
function graphqlToTs(type) {
|
|
15
|
-
switch (type) {
|
|
16
|
-
case 'String':
|
|
17
|
-
return 'string';
|
|
18
|
-
case 'Int':
|
|
19
|
-
case 'Float':
|
|
20
|
-
case 'Number':
|
|
21
|
-
return 'number';
|
|
22
|
-
case 'Boolean':
|
|
23
|
-
return 'boolean';
|
|
24
|
-
case 'Date':
|
|
25
|
-
return 'Date';
|
|
26
|
-
default:
|
|
27
|
-
return type;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function convertJsonAstToSdl(request) {
|
|
31
|
-
// Handle JSON-serialized AST
|
|
32
|
-
if (request.startsWith('{') && request.includes('"kind"')) {
|
|
33
|
-
try {
|
|
34
|
-
const ast = JSON.parse(request);
|
|
35
|
-
if (typeof ast === 'object' && ast !== null && 'kind' in ast && ast.kind === 'Document') {
|
|
36
|
-
// Convert AST to SDL string - cast is safe here as we've validated it's a Document
|
|
37
|
-
return print(ast);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
// If parsing fails, assume it's already a GraphQL string
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return request;
|
|
45
|
-
}
|
|
46
|
-
export function parseGraphQlRequest(request) {
|
|
47
|
-
const sdlRequest = convertJsonAstToSdl(request);
|
|
48
|
-
const ast = parse(sdlRequest);
|
|
49
|
-
const op = ast.definitions.find((d) => d.kind === 'OperationDefinition' && d.operation === 'query');
|
|
50
|
-
if (!op)
|
|
51
|
-
throw new Error('No query operation found');
|
|
52
|
-
const queryName = op.name?.value;
|
|
53
|
-
if (queryName == null)
|
|
54
|
-
throw new Error('Query must have a name');
|
|
55
|
-
const args = (op.variableDefinitions ?? []).map((def) => {
|
|
56
|
-
const varName = def.variable.name.value;
|
|
57
|
-
const { graphqlType, nullable } = getTypeName(def.type);
|
|
58
|
-
return {
|
|
59
|
-
name: varName,
|
|
60
|
-
graphqlType,
|
|
61
|
-
tsType: graphqlToTs(graphqlType),
|
|
62
|
-
nullable,
|
|
63
|
-
};
|
|
64
|
-
});
|
|
65
|
-
const field = op.selectionSet.selections[0];
|
|
66
|
-
if (field?.kind !== 'Field' || !field.name.value) {
|
|
67
|
-
throw new Error('Query selection must be a field');
|
|
68
|
-
}
|
|
69
|
-
const baseName = field.name.value;
|
|
70
|
-
const returnType = `${pascalCase(baseName)}View`;
|
|
71
|
-
return {
|
|
72
|
-
queryName: baseName,
|
|
73
|
-
args,
|
|
74
|
-
returnType,
|
|
75
|
-
tsReturnType: `${returnType}[]`,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
function pascalCase(input) {
|
|
79
|
-
return input.replace(/(^\w|_\w)/g, (match) => match.replace('_', '').toUpperCase());
|
|
80
|
-
}
|
|
81
|
-
//# sourceMappingURL=graphql.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"graphql.js","sourceRoot":"","sources":["../../../../src/codegen/extract/graphql.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,EAAE,KAAK,EAAiB,MAAM,SAAS,CAAC;AAgBpF,SAAS,WAAW,CAAC,QAAkB;IACrC,IAAI,QAAQ,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,EAAE,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC9D,CAAC;SAAM,IAAI,QAAQ,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;QAC3C,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACzC,OAAO,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IACvC,CAAC;SAAM,CAAC;QACN,OAAO,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAC;QAClB,KAAK,KAAK,CAAC;QACX,KAAK,OAAO,CAAC;QACb,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAC;QAClB,KAAK,SAAS;YACZ,OAAO,SAAS,CAAC;QACnB,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED,SAAS,mBAAmB,CAAC,OAAe;IAC1C,6BAA6B;IAC7B,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1D,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAY,CAAC;YAC3C,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBACxF,mFAAmF;gBACnF,OAAO,KAAK,CAAC,GAAkC,CAAC,CAAC;YACnD,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,yDAAyD;QAC3D,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,MAAM,UAAU,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAEhD,MAAM,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;IAC9B,MAAM,EAAE,GAAG,GAAG,CAAC,WAAW,CAAC,IAAI,CAC7B,CAAC,CAAC,EAAgC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,qBAAqB,IAAI,CAAC,CAAC,SAAS,KAAK,OAAO,CACjG,CAAC;IAEF,IAAI,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAErD,MAAM,SAAS,GAAG,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC;IACjC,IAAI,SAAS,IAAI,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAEjE,MAAM,IAAI,GAAgB,CAAC,EAAE,CAAC,mBAAmB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACnE,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;QACxC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxD,OAAO;YACL,IAAI,EAAE,OAAO;YACb,WAAW;YACX,MAAM,EAAE,WAAW,CAAC,WAAW,CAAC;YAChC,QAAQ;SACT,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC5C,IAAI,KAAK,EAAE,IAAI,KAAK,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QACjD,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACrD,CAAC;IAED,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;IAClC,MAAM,UAAU,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC;IAEjD,OAAO;QACL,SAAS,EAAE,QAAQ;QACnB,IAAI;QACJ,UAAU;QACV,YAAY,EAAE,GAAG,UAAU,IAAI;KAChC,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AACtF,CAAC"}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { type OperationDefinitionNode, parse, print, type TypeNode } from 'graphql';
|
|
2
|
-
|
|
3
|
-
export interface ParsedArg {
|
|
4
|
-
name: string;
|
|
5
|
-
tsType: string;
|
|
6
|
-
graphqlType: string;
|
|
7
|
-
nullable: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ParsedGraphQlQuery {
|
|
11
|
-
queryName: string;
|
|
12
|
-
args: ParsedArg[];
|
|
13
|
-
returnType: string;
|
|
14
|
-
tsReturnType: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function getTypeName(typeNode: TypeNode): { graphqlType: string; nullable: boolean } {
|
|
18
|
-
if (typeNode.kind === 'NamedType') {
|
|
19
|
-
return { graphqlType: typeNode.name.value, nullable: true };
|
|
20
|
-
} else if (typeNode.kind === 'NonNullType') {
|
|
21
|
-
const inner = getTypeName(typeNode.type);
|
|
22
|
-
return { ...inner, nullable: false };
|
|
23
|
-
} else {
|
|
24
|
-
return getTypeName(typeNode.type);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function graphqlToTs(type: string): string {
|
|
29
|
-
switch (type) {
|
|
30
|
-
case 'String':
|
|
31
|
-
return 'string';
|
|
32
|
-
case 'Int':
|
|
33
|
-
case 'Float':
|
|
34
|
-
case 'Number':
|
|
35
|
-
return 'number';
|
|
36
|
-
case 'Boolean':
|
|
37
|
-
return 'boolean';
|
|
38
|
-
case 'Date':
|
|
39
|
-
return 'Date';
|
|
40
|
-
default:
|
|
41
|
-
return type;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function convertJsonAstToSdl(request: string): string {
|
|
46
|
-
// Handle JSON-serialized AST
|
|
47
|
-
if (request.startsWith('{') && request.includes('"kind"')) {
|
|
48
|
-
try {
|
|
49
|
-
const ast = JSON.parse(request) as unknown;
|
|
50
|
-
if (typeof ast === 'object' && ast !== null && 'kind' in ast && ast.kind === 'Document') {
|
|
51
|
-
// Convert AST to SDL string - cast is safe here as we've validated it's a Document
|
|
52
|
-
return print(ast as Parameters<typeof print>[0]);
|
|
53
|
-
}
|
|
54
|
-
} catch {
|
|
55
|
-
// If parsing fails, assume it's already a GraphQL string
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return request;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function parseGraphQlRequest(request: string): ParsedGraphQlQuery {
|
|
62
|
-
const sdlRequest = convertJsonAstToSdl(request);
|
|
63
|
-
|
|
64
|
-
const ast = parse(sdlRequest);
|
|
65
|
-
const op = ast.definitions.find(
|
|
66
|
-
(d): d is OperationDefinitionNode => d.kind === 'OperationDefinition' && d.operation === 'query',
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
if (!op) throw new Error('No query operation found');
|
|
70
|
-
|
|
71
|
-
const queryName = op.name?.value;
|
|
72
|
-
if (queryName == null) throw new Error('Query must have a name');
|
|
73
|
-
|
|
74
|
-
const args: ParsedArg[] = (op.variableDefinitions ?? []).map((def) => {
|
|
75
|
-
const varName = def.variable.name.value;
|
|
76
|
-
const { graphqlType, nullable } = getTypeName(def.type);
|
|
77
|
-
return {
|
|
78
|
-
name: varName,
|
|
79
|
-
graphqlType,
|
|
80
|
-
tsType: graphqlToTs(graphqlType),
|
|
81
|
-
nullable,
|
|
82
|
-
};
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const field = op.selectionSet.selections[0];
|
|
86
|
-
if (field?.kind !== 'Field' || !field.name.value) {
|
|
87
|
-
throw new Error('Query selection must be a field');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const baseName = field.name.value;
|
|
91
|
-
const returnType = `${pascalCase(baseName)}View`;
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
queryName: baseName,
|
|
95
|
-
args,
|
|
96
|
-
returnType,
|
|
97
|
-
tsReturnType: `${returnType}[]`,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function pascalCase(input: string): string {
|
|
102
|
-
return input.replace(/(^\w|_\w)/g, (match) => match.replace('_', '').toUpperCase());
|
|
103
|
-
}
|