@auto-engineer/server-generator-apollo-emmett 1.124.0 → 1.125.1

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 (52) 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 +72 -0
  5. package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -1
  6. package/dist/src/codegen/extract/type-helpers.js +7 -5
  7. package/dist/src/codegen/extract/type-helpers.js.map +1 -1
  8. package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
  9. package/dist/src/codegen/scaffoldFromSchema.js +19 -1
  10. package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
  11. package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +0 -1
  12. package/dist/src/codegen/templates/query/query.resolver.specs.ts +8 -9
  13. package/dist/src/codegen/templates/query/query.resolver.ts.ejs +9 -3
  14. package/dist/src/codegen/templates/query/state.specs.ts +63 -0
  15. package/dist/src/codegen/templates/query/state.ts.ejs +1 -1
  16. package/dist/src/codegen/templates/react/react.specs.specs.ts +3 -3
  17. package/dist/src/codegen/templates/react/react.specs.ts +2 -2
  18. package/dist/src/codegen/templates/react/react.specs.ts.ejs +2 -2
  19. package/dist/src/codegen/templates/react/react.ts.ejs +138 -64
  20. package/dist/src/codegen/templates/react/react.ts.specs.ts +243 -1
  21. package/dist/src/codegen/templates/react/register.specs.ts +281 -14
  22. package/dist/src/codegen/templates/react/register.ts.ejs +100 -48
  23. package/dist/src/commands/generate-server.d.ts +1 -0
  24. package/dist/src/commands/generate-server.d.ts.map +1 -1
  25. package/dist/src/commands/generate-server.js +18 -0
  26. package/dist/src/commands/generate-server.js.map +1 -1
  27. package/dist/src/domain/shared/reactorSpecification.d.ts +5 -5
  28. package/dist/src/domain/shared/reactorSpecification.d.ts.map +1 -1
  29. package/dist/src/domain/shared/reactorSpecification.js +1 -2
  30. package/dist/src/domain/shared/reactorSpecification.js.map +1 -1
  31. package/dist/src/domain/shared/reactorSpecification.ts +7 -10
  32. package/dist/tsconfig.tsbuildinfo +1 -1
  33. package/ketchup-plan.md +4 -30
  34. package/package.json +4 -4
  35. package/src/codegen/extract/type-helpers.specs.ts +50 -1
  36. package/src/codegen/extract/type-helpers.ts +6 -3
  37. package/src/codegen/scaffoldFromSchema.ts +21 -1
  38. package/src/codegen/templates/command/mutation.resolver.specs.ts +0 -1
  39. package/src/codegen/templates/query/query.resolver.specs.ts +8 -9
  40. package/src/codegen/templates/query/query.resolver.ts.ejs +9 -3
  41. package/src/codegen/templates/query/state.specs.ts +63 -0
  42. package/src/codegen/templates/query/state.ts.ejs +1 -1
  43. package/src/codegen/templates/react/react.specs.specs.ts +3 -3
  44. package/src/codegen/templates/react/react.specs.ts +2 -2
  45. package/src/codegen/templates/react/react.specs.ts.ejs +2 -2
  46. package/src/codegen/templates/react/react.ts.ejs +138 -64
  47. package/src/codegen/templates/react/react.ts.specs.ts +243 -1
  48. package/src/codegen/templates/react/register.specs.ts +281 -14
  49. package/src/codegen/templates/react/register.ts.ejs +100 -48
  50. package/src/commands/generate-server.specs.ts +32 -0
  51. package/src/commands/generate-server.ts +20 -0
  52. package/src/domain/shared/reactorSpecification.ts +7 -10
@@ -240,12 +240,14 @@ describe('register.ts.ejs (react slice)', () => {
240
240
  /**
241
241
  * ## IMPLEMENTATION INSTRUCTIONS ##
242
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.
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
- // await messageBus.send({
258
- // type: 'NotifyHost',
259
- // kind: 'Command',
260
- // data: {
261
- // // Map event fields to command fields here
262
- // // e.g., userId: event.data.userId,
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
  });
@@ -1,62 +1,114 @@
1
1
  <%
2
- const specs = slice.server?.specs;
3
- const firstExample = specs?.rules?.flatMap(rule => rule.examples)[0] ?? null;
4
- const gwt = firstExample ? {
5
- given: firstExample.given,
6
- when: firstExample.when,
7
- then: firstExample.then
8
- } : null;
9
- const when = Array.isArray(gwt?.when) ? gwt.when[0] : gwt?.when;
10
- const then = Array.isArray(gwt?.then) ? gwt.then[0] : gwt?.then;
2
+ if (!eventCommandPairs || eventCommandPairs.length === 0) {
3
+ throw new Error(`register.ts.ejs: slice "${slice.name}" has no event→command pairs — check specs`);
4
+ }
11
5
 
12
- const eventType = when?.eventRef;
13
- const commandType = then?.commandRef;
14
- if (!eventType) throw new Error(
15
- `register.ts.ejs: slice "${slice.name}" has no event in .when() check specs`
16
- );
17
- const event = events.find(e => e.type === eventType);
18
- const isCrossFlow = event?.sourceFlowName && event.sourceFlowName !== flowName;
19
- const eventImportBase = isCrossFlow
20
- ? `../../${toKebabCase(event.sourceFlowName)}/${toKebabCase(event.sourceSliceName)}`
21
- : event?.sourceSliceName ? `../${toKebabCase(event.sourceSliceName)}` : '.';
6
+ const importGroups = new Map();
7
+ for (const pair of eventCommandPairs) {
8
+ const event = events.find(e => e.type === pair.eventType);
9
+ const isCrossFlow = event?.sourceFlowName && event.sourceFlowName !== flowName;
10
+ const importBase = isCrossFlow
11
+ ? `../../${toKebabCase(event.sourceFlowName)}/${toKebabCase(event.sourceSliceName)}`
12
+ : event?.sourceSliceName ? `../${toKebabCase(event.sourceSliceName)}` : '.';
13
+ if (!importGroups.has(importBase)) importGroups.set(importBase, []);
14
+ const typeName = pascalCase(pair.eventType);
15
+ if (!importGroups.get(importBase).includes(typeName)) {
16
+ importGroups.get(importBase).push(typeName);
17
+ }
18
+ }
22
19
  %>
23
20
  import { type CommandSender, type EventSubscription, type EventStore } from '@event-driven-io/emmett';
24
- import type { <%= pascalCase(eventType) %> } from '<%= eventImportBase %>/events';
25
-
21
+ <% for (const [importBase, types] of importGroups) { -%>
22
+ import type { <%= types.join(', ') %> } from '<%= importBase %>/events';
23
+ <% } %>
26
24
  export async function register(
27
25
  messageBus: CommandSender & EventSubscription,
28
26
  eventStore: EventStore
29
27
  ) {
28
+ <% for (const pair of eventCommandPairs) {
29
+ const eventDef = messages.find(m => m.name === pair.eventType);
30
+ const commandDef = messages.find(m => m.name === pair.commandType && m.type === 'command');
31
+ const commandFields = (commandDef?.fields || []);
32
+ const eventFieldSet = new Set((eventDef?.fields || []).map(f => f.name));
33
+ const stateFieldSources = {};
34
+ -%>
30
35
  messageBus.subscribe(
31
- async (event: <%= pascalCase(eventType) %>) => {
32
- /**
33
- * ## IMPLEMENTATION INSTRUCTIONS ##
34
- *
35
- * - Replace the placeholder logic with the real implementation.
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.
41
- *
42
- * CONSTRAINTS:
43
- * - NEVER use `as SomeType` type assertions. Use typed variable declarations.
44
- * - Only reference fields that exist on the event type (<%= pascalCase(eventType) %>). Check the import above.
45
- * - Do NOT modify the messageBus.subscribe() call, function signature, or import statements.
46
- * - When event.data contains nested arrays/objects, iterate them. Do NOT cast to primitive types.
47
- */
36
+ async (event: <%= pascalCase(pair.eventType) %>) => {
37
+ /**
38
+ * ## IMPLEMENTATION INSTRUCTIONS ##
39
+ *
40
+ * Complete the command send below. Field sources are pre-classified:
41
+ * - `event.data.<field>` already wired from the triggering event.
42
+ * - `<stateVar>.<field>` already wired from loaded aggregate state.
43
+ * - `undefined, // TODO: source unknown` MUST be dynamically derived:
44
+ * compute from event.data, loaded state, or runtime logic.
45
+ * NEVER hardcode values copied from test assertions.
46
+ *
47
+ * Preserve all import paths above — they are generated from the model.
48
+ *
49
+ * CONSTRAINTS:
50
+ * - NEVER use `as SomeType` type assertions. Use typed variable declarations.
51
+ * - Only reference fields that exist on the event type (<%= pascalCase(pair.eventType) %>). Check the import above.
52
+ * - Do NOT modify the messageBus.subscribe() call, function signature, or import statements.
53
+ * - When event.data contains nested arrays/objects, iterate them. Do NOT cast to primitive types.
54
+ */
55
+ <% if (eventDef?.fields?.length) { %>
56
+ // Event (<%= pair.eventType %>) fields: <%= eventDef.fields.map(f => f.name + ': ' + (f.tsType || f.type)).join(', ') %>
57
+ <% } -%>
58
+ <% if (commandDef?.fields?.length) { %>
59
+ // Command (<%= pair.commandType %>) fields: <%= commandDef.fields.map(f => f.name + ': ' + (f.tsType || f.type)).join(', ') %>
60
+ <% } -%>
61
+ <% if (states.length > 0) {
62
+ for (const state of states) {
63
+ const linkingField = findPrimitiveLinkingField(state.fields, eventDef?.fields || []);
64
+ if (linkingField) {
65
+ const varName = camelCase(state.type);
66
+ for (const f of state.fields) {
67
+ if (!stateFieldSources[f.name]) stateFieldSources[f.name] = varName;
68
+ }
69
+ const stateUsedByCommand = commandFields.some(f =>
70
+ !eventFieldSet.has(f.name) && state.fields.some(sf => sf.name === f.name)
71
+ );
72
+ const varPrefix = stateUsedByCommand ? '' : '_';
73
+ -%>
74
+
75
+ const { state: <%= varPrefix %><%= varName %> } = await eventStore.aggregateStream(
76
+ `<%= state.type %>-${event.data.<%= linkingField %>}`,
77
+ {
78
+ evolve: (currentState: Record<string, unknown>, evt: { type: string; data: Record<string, unknown> }) => ({ ...currentState, ...evt.data }),
79
+ initialState: (): Record<string, unknown> => ({}),
80
+ },
81
+ );
82
+ // <%= state.type %> fields: <%= state.fields.map(f => f.name).join(', ') %>
48
83
 
49
- // await messageBus.send({
50
- // type: '<%= commandType %>',
51
- // kind: 'Command',
52
- // data: {
53
- // // Map event fields to command fields here
54
- // // e.g., userId: event.data.userId,
55
- // },
56
- // });
84
+ <% }
85
+ }
86
+ }
87
+ -%>
57
88
 
58
- return;
89
+ await messageBus.send({
90
+ type: '<%= pair.commandType %>',
91
+ kind: 'Command',
92
+ data: {
93
+ <% for (const field of commandFields) {
94
+ const fieldName = field.name;
95
+ if (eventFieldSet.has(fieldName)) {
96
+ -%>
97
+ <%= fieldName %>: event.data.<%= fieldName %>,
98
+ <% } else if (stateFieldSources[fieldName]) { -%>
99
+ <%= fieldName %>: <%= stateFieldSources[fieldName] %>.<%= fieldName %>,
100
+ <% } else { -%>
101
+ <%= fieldName %>: undefined, // TODO: source unknown
102
+ <% }
103
+ }
104
+ -%>
105
+ },
106
+ });
107
+
108
+ return;
59
109
  },
60
- '<%= eventType %>'
110
+ '<%= pair.eventType %>'
61
111
  );
112
+
113
+ <% } -%>
62
114
  }
@@ -17,6 +17,7 @@ import {
17
17
  emitSliceGenerationFailedForDuplicates,
18
18
  emitSliceGenerationFailedForFieldIssues,
19
19
  saveGenerationState,
20
+ writeBiomeConfig,
20
21
  writeHealthResolver,
21
22
  writeModelToDisk,
22
23
  } from './generate-server';
@@ -256,6 +257,37 @@ describe('writeHealthResolver', () => {
256
257
  });
257
258
  });
258
259
 
260
+ describe('writeBiomeConfig', () => {
261
+ let dir: string;
262
+
263
+ beforeEach(async () => {
264
+ dir = await mkdtemp(join(tmpdir(), 'biome-config-'));
265
+ });
266
+
267
+ afterEach(async () => {
268
+ await rm(dir, { recursive: true, force: true });
269
+ });
270
+
271
+ it('writes biome.json with decorator support and recommended linting', async () => {
272
+ await writeBiomeConfig(dir);
273
+
274
+ const content = JSON.parse(await readFile(join(dir, 'biome.json'), 'utf8'));
275
+ expect(content).toEqual({
276
+ javascript: {
277
+ parser: {
278
+ unsafeParameterDecoratorsEnabled: true,
279
+ },
280
+ },
281
+ linter: {
282
+ enabled: true,
283
+ rules: {
284
+ recommended: true,
285
+ },
286
+ },
287
+ });
288
+ });
289
+ });
290
+
259
291
  describe('emitSliceGenerationFailedForFieldIssues', () => {
260
292
  it('emits SliceGenerationFailed for command slice referencing message via gherkin step', () => {
261
293
  const fieldIssues = [
@@ -284,6 +284,9 @@ async function writeConfigurationFiles(serverDir: string): Promise<void> {
284
284
 
285
285
  debugFiles(`Writing vitest config... to ${serverDir}`);
286
286
  await writeVitestConfig(serverDir);
287
+
288
+ debugFiles(`Writing biome.json... to ${serverDir}`);
289
+ await writeBiomeConfig(serverDir);
287
290
  }
288
291
 
289
292
  function createServerSuccessEvent(command: GenerateServerCommand, serverDir: string): ServerGeneratedEvent {
@@ -757,4 +760,21 @@ export default defineConfig({
757
760
  await writeFile(path.join(dest, 'vitest.config.ts'), vitestConfig, 'utf-8');
758
761
  }
759
762
 
763
+ export async function writeBiomeConfig(dest: string): Promise<void> {
764
+ const biomeConfig = {
765
+ javascript: {
766
+ parser: {
767
+ unsafeParameterDecoratorsEnabled: true,
768
+ },
769
+ },
770
+ linter: {
771
+ enabled: true,
772
+ rules: {
773
+ recommended: true,
774
+ },
775
+ },
776
+ };
777
+ await fs.writeJson(path.join(dest, 'biome.json'), biomeConfig, { spaces: 2 });
778
+ }
779
+
760
780
  export default commandHandler;
@@ -4,7 +4,7 @@ import {
4
4
  type CommandSender,
5
5
  type ErrorConstructor,
6
6
  isErrorConstructor,
7
- type MessageProcessor,
7
+ type MessageHandlerResult,
8
8
  } from '@event-driven-io/emmett';
9
9
 
10
10
  type CommandCheck<CommandType> = (command: CommandType) => boolean;
@@ -22,7 +22,9 @@ interface ReactorSpecificationReturn<Event, Command, Context> {
22
22
  event: Event | Event[],
23
23
  context?: Context,
24
24
  ) => {
25
- then: (expectedCommand: Command | Command[] | CommandCheck<Command> | CommandCheck<Command[]>) => Promise<void>;
25
+ thenSends: (
26
+ expectedCommand: Command | Command[] | CommandCheck<Command> | CommandCheck<Command[]>,
27
+ ) => Promise<void>;
26
28
  thenNothingHappened: () => Promise<void>;
27
29
  thenThrows: <ErrorType extends Error = Error>(...args: Parameters<ThenThrows<ErrorType>>) => Promise<void>;
28
30
  };
@@ -60,12 +62,8 @@ function createMockCommandSender<Command>(): MockCommandSender<Command> {
60
62
  }
61
63
 
62
64
  type ReactorLike<Event, Context> =
63
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
- | { handle: (events: Event[], context: Context) => Promise<any> }
65
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
- | { eachMessage: (event: Event, context: Context) => Promise<any> }
67
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
- | MessageProcessor<any, any, any>;
65
+ | { handle(events: Event[], context: Context): MessageHandlerResult | Promise<MessageHandlerResult> }
66
+ | { eachMessage(event: Event, context: Context): MessageHandlerResult | Promise<MessageHandlerResult> };
69
67
 
70
68
  function reactorSpecificationFor<Event, Command, Context extends { commandSender: CommandSender }>(
71
69
  processorOrReactor: ReactorLike<Event, Context> | (() => ReactorLike<Event, Context>),
@@ -124,7 +122,7 @@ function reactorSpecificationFor<Event, Command, Context extends { commandSender
124
122
  };
125
123
 
126
124
  return {
127
- then: async (
125
+ thenSends: async (
128
126
  expectedCommand: Command | Command[] | CommandCheck<Command> | CommandCheck<Command[]>,
129
127
  ): Promise<void> => {
130
128
  try {
@@ -206,7 +204,6 @@ interface CommandWithMetadata {
206
204
  metadata?: Record<string, unknown>;
207
205
  }
208
206
 
209
- // eslint-disable-next-line complexity
210
207
  function assertCommandsMatch<Command>(actual: Command, expected: Command, index?: number): void {
211
208
  const actualCopy = { ...actual } as CommandWithMetadata & Record<string, unknown>;
212
209
  const expectedCopy = { ...expected } as CommandWithMetadata & Record<string, unknown>;