@auto-engineer/server-generator-apollo-emmett 1.123.0 → 1.125.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 +99 -0
- package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -1
- package/dist/src/codegen/extract/type-helpers.js +7 -5
- 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 +19 -1
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/decide.specs.ts +14 -6
- package/dist/src/codegen/templates/command/decide.ts.ejs +5 -3
- package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +0 -1
- package/dist/src/codegen/templates/command/state.specs.ts +1 -1
- package/dist/src/codegen/templates/command/state.ts.ejs +1 -1
- package/dist/src/codegen/templates/query/projection.specs.specs.ts +2 -0
- package/dist/src/codegen/templates/query/projection.specs.ts +6 -0
- package/dist/src/codegen/templates/query/projection.specs.ts.ejs +4 -1
- package/dist/src/codegen/templates/query/projection.ts.ejs +2 -0
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +8 -9
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +9 -3
- package/dist/src/codegen/templates/react/react.specs.specs.ts +3 -3
- package/dist/src/codegen/templates/react/react.specs.ts +2 -2
- package/dist/src/codegen/templates/react/react.specs.ts.ejs +2 -2
- package/dist/src/codegen/templates/react/react.ts.ejs +138 -64
- package/dist/src/codegen/templates/react/react.ts.specs.ts +243 -1
- package/dist/src/codegen/templates/react/register.specs.ts +281 -14
- package/dist/src/codegen/templates/react/register.ts.ejs +100 -48
- package/dist/src/commands/generate-server.d.ts +1 -0
- package/dist/src/commands/generate-server.d.ts.map +1 -1
- package/dist/src/commands/generate-server.js +18 -0
- package/dist/src/commands/generate-server.js.map +1 -1
- package/dist/src/domain/shared/reactorSpecification.d.ts +5 -5
- package/dist/src/domain/shared/reactorSpecification.d.ts.map +1 -1
- package/dist/src/domain/shared/reactorSpecification.js +1 -2
- package/dist/src/domain/shared/reactorSpecification.js.map +1 -1
- package/dist/src/domain/shared/reactorSpecification.ts +7 -10
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +4 -30
- package/package.json +4 -4
- package/src/codegen/extract/type-helpers.specs.ts +50 -1
- package/src/codegen/extract/type-helpers.ts +6 -3
- package/src/codegen/scaffoldFromSchema.ts +21 -1
- package/src/codegen/templates/command/decide.specs.ts +14 -6
- package/src/codegen/templates/command/decide.ts.ejs +5 -3
- package/src/codegen/templates/command/mutation.resolver.specs.ts +0 -1
- package/src/codegen/templates/command/state.specs.ts +1 -1
- package/src/codegen/templates/command/state.ts.ejs +1 -1
- package/src/codegen/templates/query/projection.specs.specs.ts +2 -0
- package/src/codegen/templates/query/projection.specs.ts +6 -0
- package/src/codegen/templates/query/projection.specs.ts.ejs +4 -1
- package/src/codegen/templates/query/projection.ts.ejs +2 -0
- package/src/codegen/templates/query/query.resolver.specs.ts +8 -9
- package/src/codegen/templates/query/query.resolver.ts.ejs +9 -3
- package/src/codegen/templates/react/react.specs.specs.ts +3 -3
- package/src/codegen/templates/react/react.specs.ts +2 -2
- package/src/codegen/templates/react/react.specs.ts.ejs +2 -2
- package/src/codegen/templates/react/react.ts.ejs +138 -64
- package/src/codegen/templates/react/react.ts.specs.ts +243 -1
- package/src/codegen/templates/react/register.specs.ts +281 -14
- package/src/codegen/templates/react/register.ts.ejs +100 -48
- package/src/commands/generate-server.specs.ts +32 -0
- package/src/commands/generate-server.ts +20 -0
- package/src/domain/shared/reactorSpecification.ts +7 -10
|
@@ -762,8 +762,250 @@ export type BarberNotified = Event<
|
|
|
762
762
|
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
763
763
|
const reactFile = plans.find((p) => p.outputPath.endsWith('check-record-updates/react.ts'));
|
|
764
764
|
|
|
765
|
-
expect(reactFile?.contents).toContain(
|
|
765
|
+
expect(reactFile?.contents).toContain('`WorkoutSummary-${event.data.memberId}`');
|
|
766
766
|
expect(reactFile?.contents).toContain('aggregateStream');
|
|
767
767
|
expect(reactFile?.contents).not.toContain('event.data.exercises');
|
|
768
768
|
});
|
|
769
|
+
|
|
770
|
+
it('should scaffold handlers for multiple event→command pairs', async () => {
|
|
771
|
+
const spec: SpecsSchema = {
|
|
772
|
+
variant: 'specs',
|
|
773
|
+
narratives: [
|
|
774
|
+
{
|
|
775
|
+
name: 'fitness flow',
|
|
776
|
+
slices: [
|
|
777
|
+
{
|
|
778
|
+
type: 'command',
|
|
779
|
+
name: 'earn points',
|
|
780
|
+
client: { specs: [] },
|
|
781
|
+
server: {
|
|
782
|
+
description: '',
|
|
783
|
+
specs: [
|
|
784
|
+
{
|
|
785
|
+
type: 'gherkin',
|
|
786
|
+
feature: 'Earn points',
|
|
787
|
+
rules: [
|
|
788
|
+
{
|
|
789
|
+
name: 'Should earn',
|
|
790
|
+
examples: [
|
|
791
|
+
{
|
|
792
|
+
name: 'Points earned',
|
|
793
|
+
steps: [
|
|
794
|
+
{ keyword: 'When', text: 'EarnPoints', docString: { memberId: 'm1', amount: 10 } },
|
|
795
|
+
{ keyword: 'Then', text: 'PointsEarned', docString: { memberId: 'm1', amount: 10 } },
|
|
796
|
+
],
|
|
797
|
+
},
|
|
798
|
+
],
|
|
799
|
+
},
|
|
800
|
+
],
|
|
801
|
+
},
|
|
802
|
+
],
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
type: 'command',
|
|
807
|
+
name: 'update record',
|
|
808
|
+
client: { specs: [] },
|
|
809
|
+
server: {
|
|
810
|
+
description: '',
|
|
811
|
+
specs: [
|
|
812
|
+
{
|
|
813
|
+
type: 'gherkin',
|
|
814
|
+
feature: 'Update record',
|
|
815
|
+
rules: [
|
|
816
|
+
{
|
|
817
|
+
name: 'Should update',
|
|
818
|
+
examples: [
|
|
819
|
+
{
|
|
820
|
+
name: 'Record updated',
|
|
821
|
+
steps: [
|
|
822
|
+
{ keyword: 'When', text: 'UpdateRecord', docString: { memberId: 'm1' } },
|
|
823
|
+
{
|
|
824
|
+
keyword: 'Then',
|
|
825
|
+
text: 'PersonalRecordUpdated',
|
|
826
|
+
docString: { memberId: 'm1', record: 'bench' },
|
|
827
|
+
},
|
|
828
|
+
],
|
|
829
|
+
},
|
|
830
|
+
],
|
|
831
|
+
},
|
|
832
|
+
],
|
|
833
|
+
},
|
|
834
|
+
],
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
type: 'react',
|
|
839
|
+
name: 'award badges',
|
|
840
|
+
server: {
|
|
841
|
+
description: 'Awards badges based on points and records',
|
|
842
|
+
specs: [
|
|
843
|
+
{
|
|
844
|
+
type: 'gherkin',
|
|
845
|
+
feature: 'Award badges',
|
|
846
|
+
rules: [
|
|
847
|
+
{
|
|
848
|
+
name: 'Should award on points',
|
|
849
|
+
examples: [
|
|
850
|
+
{
|
|
851
|
+
name: 'Badge for points',
|
|
852
|
+
steps: [
|
|
853
|
+
{ keyword: 'When', text: 'PointsEarned', docString: { memberId: 'm1', amount: 100 } },
|
|
854
|
+
{
|
|
855
|
+
keyword: 'Then',
|
|
856
|
+
text: 'AwardBadge',
|
|
857
|
+
docString: { memberId: 'm1', badge: 'centurion' },
|
|
858
|
+
},
|
|
859
|
+
],
|
|
860
|
+
},
|
|
861
|
+
],
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
name: 'Should notify on record',
|
|
865
|
+
examples: [
|
|
866
|
+
{
|
|
867
|
+
name: 'Notification for record',
|
|
868
|
+
steps: [
|
|
869
|
+
{
|
|
870
|
+
keyword: 'When',
|
|
871
|
+
text: 'PersonalRecordUpdated',
|
|
872
|
+
docString: { memberId: 'm1', record: 'bench' },
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
keyword: 'Then',
|
|
876
|
+
text: 'SendRecordNotification',
|
|
877
|
+
docString: { memberId: 'm1', message: 'New record!' },
|
|
878
|
+
},
|
|
879
|
+
],
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
},
|
|
883
|
+
],
|
|
884
|
+
},
|
|
885
|
+
],
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
],
|
|
889
|
+
},
|
|
890
|
+
],
|
|
891
|
+
messages: [
|
|
892
|
+
{
|
|
893
|
+
type: 'command',
|
|
894
|
+
name: 'EarnPoints',
|
|
895
|
+
fields: [
|
|
896
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
897
|
+
{ name: 'amount', type: 'number', required: true },
|
|
898
|
+
],
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
type: 'event',
|
|
902
|
+
name: 'PointsEarned',
|
|
903
|
+
source: 'internal',
|
|
904
|
+
fields: [
|
|
905
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
906
|
+
{ name: 'amount', type: 'number', required: true },
|
|
907
|
+
],
|
|
908
|
+
},
|
|
909
|
+
{
|
|
910
|
+
type: 'command',
|
|
911
|
+
name: 'UpdateRecord',
|
|
912
|
+
fields: [{ name: 'memberId', type: 'string', required: true }],
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
type: 'event',
|
|
916
|
+
name: 'PersonalRecordUpdated',
|
|
917
|
+
source: 'internal',
|
|
918
|
+
fields: [
|
|
919
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
920
|
+
{ name: 'record', type: 'string', required: true },
|
|
921
|
+
],
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
type: 'command',
|
|
925
|
+
name: 'AwardBadge',
|
|
926
|
+
fields: [
|
|
927
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
928
|
+
{ name: 'badge', type: 'string', required: true },
|
|
929
|
+
],
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
type: 'command',
|
|
933
|
+
name: 'SendRecordNotification',
|
|
934
|
+
fields: [
|
|
935
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
936
|
+
{ name: 'message', type: 'string', required: true },
|
|
937
|
+
],
|
|
938
|
+
},
|
|
939
|
+
],
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
943
|
+
const reactFile = plans.find((p) => p.outputPath.endsWith('award-badges/react.ts'));
|
|
944
|
+
|
|
945
|
+
expect(reactFile?.contents).toMatchInlineSnapshot(`
|
|
946
|
+
"import { inMemoryReactor, type MessageHandlerResult } from '@event-driven-io/emmett';
|
|
947
|
+
import type { PointsEarned } from '../earn-points/events';
|
|
948
|
+
import type { PersonalRecordUpdated } from '../update-record/events';
|
|
949
|
+
import type { ReactorContext } from '../../../shared';
|
|
950
|
+
|
|
951
|
+
export const react = ({ eventStore, commandSender, database }: ReactorContext) =>
|
|
952
|
+
inMemoryReactor<PointsEarned | PersonalRecordUpdated>({
|
|
953
|
+
processorId: 'fitness-flow-award-badges',
|
|
954
|
+
canHandle: ['PointsEarned', 'PersonalRecordUpdated'],
|
|
955
|
+
connectionOptions: {
|
|
956
|
+
database,
|
|
957
|
+
},
|
|
958
|
+
eachMessage: async (event): Promise<MessageHandlerResult> => {
|
|
959
|
+
/**
|
|
960
|
+
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
961
|
+
*
|
|
962
|
+
* Complete the command send below. Field sources are pre-classified:
|
|
963
|
+
* - \`event.data.<field>\` → already wired from the triggering event.
|
|
964
|
+
* - \`<stateVar>.<field>\` → already wired from loaded aggregate state.
|
|
965
|
+
* - \`undefined, // TODO: source unknown\` → MUST be dynamically derived:
|
|
966
|
+
* compute from event.data, loaded state, or runtime logic.
|
|
967
|
+
* NEVER hardcode values copied from test assertions.
|
|
968
|
+
*
|
|
969
|
+
* Preserve all import paths above — they are generated from the model.
|
|
970
|
+
*
|
|
971
|
+
* CONSTRAINTS:
|
|
972
|
+
* - NEVER use \`as SomeType\` type assertions. Use typed variable declarations.
|
|
973
|
+
* - Only reference event.data fields listed in the "Event fields:" comment below. Check the type before accessing any field.
|
|
974
|
+
* - Do NOT modify the inMemoryReactor configuration, connectionOptions, import statements, or commandSender.send() call structure. Only fill in data fields marked TODO.
|
|
975
|
+
* - When event.data contains nested arrays/objects (e.g., exercises[].sets[]), iterate them to compute values. Do NOT cast arrays/objects to primitive types.
|
|
976
|
+
*
|
|
977
|
+
* Add business logic (validation, conditional sends) as needed.
|
|
978
|
+
*/
|
|
979
|
+
if (event.type === 'PointsEarned') {
|
|
980
|
+
// Event (PointsEarned) fields: memberId: string, amount: number
|
|
981
|
+
|
|
982
|
+
// Command (AwardBadge) fields: memberId: string, badge: string
|
|
983
|
+
await commandSender.send({
|
|
984
|
+
type: 'AwardBadge',
|
|
985
|
+
kind: 'Command',
|
|
986
|
+
data: {
|
|
987
|
+
memberId: event.data.memberId,
|
|
988
|
+
badge: undefined, // TODO: source unknown
|
|
989
|
+
},
|
|
990
|
+
});
|
|
991
|
+
} else if (event.type === 'PersonalRecordUpdated') {
|
|
992
|
+
// Event (PersonalRecordUpdated) fields: memberId: string, record: string
|
|
993
|
+
|
|
994
|
+
// Command (SendRecordNotification) fields: memberId: string, message: string
|
|
995
|
+
await commandSender.send({
|
|
996
|
+
type: 'SendRecordNotification',
|
|
997
|
+
kind: 'Command',
|
|
998
|
+
data: {
|
|
999
|
+
memberId: event.data.memberId,
|
|
1000
|
+
message: undefined, // TODO: source unknown
|
|
1001
|
+
},
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return;
|
|
1006
|
+
},
|
|
1007
|
+
});
|
|
1008
|
+
"
|
|
1009
|
+
`);
|
|
1010
|
+
});
|
|
769
1011
|
});
|
|
@@ -240,12 +240,14 @@ describe('register.ts.ejs (react slice)', () => {
|
|
|
240
240
|
/**
|
|
241
241
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
242
242
|
*
|
|
243
|
-
*
|
|
244
|
-
* -
|
|
245
|
-
* -
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
243
|
+
* Complete the command send below. Field sources are pre-classified:
|
|
244
|
+
* - \`event.data.<field>\` → already wired from the triggering event.
|
|
245
|
+
* - \`<stateVar>.<field>\` → already wired from loaded aggregate state.
|
|
246
|
+
* - \`undefined, // TODO: source unknown\` → MUST be dynamically derived:
|
|
247
|
+
* compute from event.data, loaded state, or runtime logic.
|
|
248
|
+
* NEVER hardcode values copied from test assertions.
|
|
249
|
+
*
|
|
250
|
+
* Preserve all import paths above — they are generated from the model.
|
|
249
251
|
*
|
|
250
252
|
* CONSTRAINTS:
|
|
251
253
|
* - NEVER use \`as SomeType\` type assertions. Use typed variable declarations.
|
|
@@ -254,14 +256,22 @@ describe('register.ts.ejs (react slice)', () => {
|
|
|
254
256
|
* - When event.data contains nested arrays/objects, iterate them. Do NOT cast to primitive types.
|
|
255
257
|
*/
|
|
256
258
|
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
259
|
+
// Event (BookingRequested) fields: bookingId: string, hostId: string, message: string
|
|
260
|
+
|
|
261
|
+
// Command (NotifyHost) fields: hostId: string, notificationType: string, priority: string, channels: string[], message: string, actionRequired: boolean
|
|
262
|
+
|
|
263
|
+
await messageBus.send({
|
|
264
|
+
type: 'NotifyHost',
|
|
265
|
+
kind: 'Command',
|
|
266
|
+
data: {
|
|
267
|
+
hostId: event.data.hostId,
|
|
268
|
+
notificationType: undefined, // TODO: source unknown
|
|
269
|
+
priority: undefined, // TODO: source unknown
|
|
270
|
+
channels: undefined, // TODO: source unknown
|
|
271
|
+
message: event.data.message,
|
|
272
|
+
actionRequired: undefined, // TODO: source unknown
|
|
273
|
+
},
|
|
274
|
+
});
|
|
265
275
|
|
|
266
276
|
return;
|
|
267
277
|
}, 'BookingRequested');
|
|
@@ -269,4 +279,261 @@ describe('register.ts.ejs (react slice)', () => {
|
|
|
269
279
|
"
|
|
270
280
|
`);
|
|
271
281
|
});
|
|
282
|
+
|
|
283
|
+
it('should scaffold multiple subscribe calls for multi-event reactor', async () => {
|
|
284
|
+
const spec: SpecsSchema = {
|
|
285
|
+
variant: 'specs',
|
|
286
|
+
narratives: [
|
|
287
|
+
{
|
|
288
|
+
name: 'fitness flow',
|
|
289
|
+
slices: [
|
|
290
|
+
{
|
|
291
|
+
type: 'command',
|
|
292
|
+
name: 'earn points',
|
|
293
|
+
client: { specs: [] },
|
|
294
|
+
server: {
|
|
295
|
+
description: '',
|
|
296
|
+
specs: [
|
|
297
|
+
{
|
|
298
|
+
type: 'gherkin',
|
|
299
|
+
feature: 'Earn points',
|
|
300
|
+
rules: [
|
|
301
|
+
{
|
|
302
|
+
name: 'Should earn',
|
|
303
|
+
examples: [
|
|
304
|
+
{
|
|
305
|
+
name: 'Points earned',
|
|
306
|
+
steps: [
|
|
307
|
+
{ keyword: 'When', text: 'EarnPoints', docString: { memberId: 'm1', amount: 10 } },
|
|
308
|
+
{ keyword: 'Then', text: 'PointsEarned', docString: { memberId: 'm1', amount: 10 } },
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
type: 'command',
|
|
320
|
+
name: 'update record',
|
|
321
|
+
client: { specs: [] },
|
|
322
|
+
server: {
|
|
323
|
+
description: '',
|
|
324
|
+
specs: [
|
|
325
|
+
{
|
|
326
|
+
type: 'gherkin',
|
|
327
|
+
feature: 'Update record',
|
|
328
|
+
rules: [
|
|
329
|
+
{
|
|
330
|
+
name: 'Should update',
|
|
331
|
+
examples: [
|
|
332
|
+
{
|
|
333
|
+
name: 'Record updated',
|
|
334
|
+
steps: [
|
|
335
|
+
{ keyword: 'When', text: 'UpdateRecord', docString: { memberId: 'm1' } },
|
|
336
|
+
{
|
|
337
|
+
keyword: 'Then',
|
|
338
|
+
text: 'PersonalRecordUpdated',
|
|
339
|
+
docString: { memberId: 'm1', record: 'bench' },
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
type: 'react',
|
|
352
|
+
name: 'award badges',
|
|
353
|
+
server: {
|
|
354
|
+
description: 'Awards badges based on points and records',
|
|
355
|
+
specs: [
|
|
356
|
+
{
|
|
357
|
+
type: 'gherkin',
|
|
358
|
+
feature: 'Award badges',
|
|
359
|
+
rules: [
|
|
360
|
+
{
|
|
361
|
+
name: 'Should award on points',
|
|
362
|
+
examples: [
|
|
363
|
+
{
|
|
364
|
+
name: 'Badge for points',
|
|
365
|
+
steps: [
|
|
366
|
+
{ keyword: 'When', text: 'PointsEarned', docString: { memberId: 'm1', amount: 100 } },
|
|
367
|
+
{
|
|
368
|
+
keyword: 'Then',
|
|
369
|
+
text: 'AwardBadge',
|
|
370
|
+
docString: { memberId: 'm1', badge: 'centurion' },
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: 'Should notify on record',
|
|
378
|
+
examples: [
|
|
379
|
+
{
|
|
380
|
+
name: 'Notification for record',
|
|
381
|
+
steps: [
|
|
382
|
+
{
|
|
383
|
+
keyword: 'When',
|
|
384
|
+
text: 'PersonalRecordUpdated',
|
|
385
|
+
docString: { memberId: 'm1', record: 'bench' },
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
keyword: 'Then',
|
|
389
|
+
text: 'SendRecordNotification',
|
|
390
|
+
docString: { memberId: 'm1', message: 'New record!' },
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
messages: [
|
|
405
|
+
{
|
|
406
|
+
type: 'command',
|
|
407
|
+
name: 'EarnPoints',
|
|
408
|
+
fields: [
|
|
409
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
410
|
+
{ name: 'amount', type: 'number', required: true },
|
|
411
|
+
],
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
type: 'event',
|
|
415
|
+
name: 'PointsEarned',
|
|
416
|
+
source: 'internal',
|
|
417
|
+
fields: [
|
|
418
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
419
|
+
{ name: 'amount', type: 'number', required: true },
|
|
420
|
+
],
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
type: 'command',
|
|
424
|
+
name: 'UpdateRecord',
|
|
425
|
+
fields: [{ name: 'memberId', type: 'string', required: true }],
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
type: 'event',
|
|
429
|
+
name: 'PersonalRecordUpdated',
|
|
430
|
+
source: 'internal',
|
|
431
|
+
fields: [
|
|
432
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
433
|
+
{ name: 'record', type: 'string', required: true },
|
|
434
|
+
],
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
type: 'command',
|
|
438
|
+
name: 'AwardBadge',
|
|
439
|
+
fields: [
|
|
440
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
441
|
+
{ name: 'badge', type: 'string', required: true },
|
|
442
|
+
],
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
type: 'command',
|
|
446
|
+
name: 'SendRecordNotification',
|
|
447
|
+
fields: [
|
|
448
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
449
|
+
{ name: 'message', type: 'string', required: true },
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
456
|
+
const registerFile = plans.find((p) => p.outputPath.endsWith('award-badges/register.ts'));
|
|
457
|
+
|
|
458
|
+
expect(registerFile?.contents).toMatchInlineSnapshot(`
|
|
459
|
+
"import { type CommandSender, type EventSubscription, type EventStore } from '@event-driven-io/emmett';
|
|
460
|
+
import type { PointsEarned } from '../earn-points/events';
|
|
461
|
+
import type { PersonalRecordUpdated } from '../update-record/events';
|
|
462
|
+
|
|
463
|
+
export async function register(messageBus: CommandSender & EventSubscription, eventStore: EventStore) {
|
|
464
|
+
messageBus.subscribe(async (event: PointsEarned) => {
|
|
465
|
+
/**
|
|
466
|
+
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
467
|
+
*
|
|
468
|
+
* Complete the command send below. Field sources are pre-classified:
|
|
469
|
+
* - \`event.data.<field>\` → already wired from the triggering event.
|
|
470
|
+
* - \`<stateVar>.<field>\` → already wired from loaded aggregate state.
|
|
471
|
+
* - \`undefined, // TODO: source unknown\` → MUST be dynamically derived:
|
|
472
|
+
* compute from event.data, loaded state, or runtime logic.
|
|
473
|
+
* NEVER hardcode values copied from test assertions.
|
|
474
|
+
*
|
|
475
|
+
* Preserve all import paths above — they are generated from the model.
|
|
476
|
+
*
|
|
477
|
+
* CONSTRAINTS:
|
|
478
|
+
* - NEVER use \`as SomeType\` type assertions. Use typed variable declarations.
|
|
479
|
+
* - Only reference fields that exist on the event type (PointsEarned). Check the import above.
|
|
480
|
+
* - Do NOT modify the messageBus.subscribe() call, function signature, or import statements.
|
|
481
|
+
* - When event.data contains nested arrays/objects, iterate them. Do NOT cast to primitive types.
|
|
482
|
+
*/
|
|
483
|
+
|
|
484
|
+
// Event (PointsEarned) fields: memberId: string, amount: number
|
|
485
|
+
|
|
486
|
+
// Command (AwardBadge) fields: memberId: string, badge: string
|
|
487
|
+
|
|
488
|
+
await messageBus.send({
|
|
489
|
+
type: 'AwardBadge',
|
|
490
|
+
kind: 'Command',
|
|
491
|
+
data: {
|
|
492
|
+
memberId: event.data.memberId,
|
|
493
|
+
badge: undefined, // TODO: source unknown
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
return;
|
|
498
|
+
}, 'PointsEarned');
|
|
499
|
+
|
|
500
|
+
messageBus.subscribe(async (event: PersonalRecordUpdated) => {
|
|
501
|
+
/**
|
|
502
|
+
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
503
|
+
*
|
|
504
|
+
* Complete the command send below. Field sources are pre-classified:
|
|
505
|
+
* - \`event.data.<field>\` → already wired from the triggering event.
|
|
506
|
+
* - \`<stateVar>.<field>\` → already wired from loaded aggregate state.
|
|
507
|
+
* - \`undefined, // TODO: source unknown\` → MUST be dynamically derived:
|
|
508
|
+
* compute from event.data, loaded state, or runtime logic.
|
|
509
|
+
* NEVER hardcode values copied from test assertions.
|
|
510
|
+
*
|
|
511
|
+
* Preserve all import paths above — they are generated from the model.
|
|
512
|
+
*
|
|
513
|
+
* CONSTRAINTS:
|
|
514
|
+
* - NEVER use \`as SomeType\` type assertions. Use typed variable declarations.
|
|
515
|
+
* - Only reference fields that exist on the event type (PersonalRecordUpdated). Check the import above.
|
|
516
|
+
* - Do NOT modify the messageBus.subscribe() call, function signature, or import statements.
|
|
517
|
+
* - When event.data contains nested arrays/objects, iterate them. Do NOT cast to primitive types.
|
|
518
|
+
*/
|
|
519
|
+
|
|
520
|
+
// Event (PersonalRecordUpdated) fields: memberId: string, record: string
|
|
521
|
+
|
|
522
|
+
// Command (SendRecordNotification) fields: memberId: string, message: string
|
|
523
|
+
|
|
524
|
+
await messageBus.send({
|
|
525
|
+
type: 'SendRecordNotification',
|
|
526
|
+
kind: 'Command',
|
|
527
|
+
data: {
|
|
528
|
+
memberId: event.data.memberId,
|
|
529
|
+
message: undefined, // TODO: source unknown
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
return;
|
|
534
|
+
}, 'PersonalRecordUpdated');
|
|
535
|
+
}
|
|
536
|
+
"
|
|
537
|
+
`);
|
|
538
|
+
});
|
|
272
539
|
});
|