@auto-engineer/server-generator-apollo-emmett 0.10.5 → 0.11.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 (51) hide show
  1. package/.turbo/turbo-build.log +6 -0
  2. package/.turbo/turbo-format.log +5 -0
  3. package/.turbo/turbo-lint.log +4 -0
  4. package/.turbo/turbo-test.log +22 -0
  5. package/.turbo/turbo-type-check.log +5 -0
  6. package/CHANGELOG.md +12 -0
  7. package/dist/src/codegen/scaffoldFromSchema.query-slice-register.specs.d.ts +2 -0
  8. package/dist/src/codegen/scaffoldFromSchema.query-slice-register.specs.d.ts.map +1 -0
  9. package/dist/src/codegen/scaffoldFromSchema.query-slice-register.specs.js +168 -0
  10. package/dist/src/codegen/scaffoldFromSchema.query-slice-register.specs.js.map +1 -0
  11. package/dist/src/codegen/templates/query/projection.specs.ts +1 -1
  12. package/dist/src/codegen/templates/query/projection.specs.ts.ejs +2 -0
  13. package/dist/src/codegen/templates/query/query.resolver.specs.ts +190 -5
  14. package/dist/src/codegen/templates/query/query.resolver.ts.ejs +31 -9
  15. package/dist/src/codegen/templates/react/react.specs.specs.ts +1 -1
  16. package/dist/src/codegen/templates/react/react.specs.ts +4 -4
  17. package/dist/src/codegen/templates/react/react.specs.ts.ejs +1 -1
  18. package/dist/src/codegen/templates/react/react.ts.ejs +4 -4
  19. package/dist/src/codegen/templates/react/register.specs.ts +2 -2
  20. package/dist/src/codegen/templates/react/register.ts.ejs +2 -2
  21. package/dist/src/commands/generate-server.d.ts.map +1 -1
  22. package/dist/src/commands/generate-server.js +3 -0
  23. package/dist/src/commands/generate-server.js.map +1 -1
  24. package/dist/src/domain/shared/ReadModel.d.ts +2 -2
  25. package/dist/src/domain/shared/ReadModel.d.ts.map +1 -1
  26. package/dist/src/domain/shared/ReadModel.js +2 -2
  27. package/dist/src/domain/shared/ReadModel.js.map +1 -1
  28. package/dist/src/domain/shared/ReadModel.ts +3 -3
  29. package/dist/src/domain/shared/types.d.ts +5 -3
  30. package/dist/src/domain/shared/types.d.ts.map +1 -1
  31. package/dist/src/domain/shared/types.js.map +1 -1
  32. package/dist/src/domain/shared/types.ts +5 -3
  33. package/dist/src/server.js +54 -7
  34. package/dist/src/server.js.map +1 -1
  35. package/dist/src/server.ts +53 -15
  36. package/dist/tsconfig.tsbuildinfo +1 -1
  37. package/package.json +8 -5
  38. package/src/codegen/templates/query/projection.specs.ts +1 -1
  39. package/src/codegen/templates/query/projection.specs.ts.ejs +2 -0
  40. package/src/codegen/templates/query/query.resolver.specs.ts +190 -5
  41. package/src/codegen/templates/query/query.resolver.ts.ejs +31 -9
  42. package/src/codegen/templates/react/react.specs.specs.ts +1 -1
  43. package/src/codegen/templates/react/react.specs.ts +4 -4
  44. package/src/codegen/templates/react/react.specs.ts.ejs +1 -1
  45. package/src/codegen/templates/react/react.ts.ejs +4 -4
  46. package/src/codegen/templates/react/register.specs.ts +2 -2
  47. package/src/codegen/templates/react/register.ts.ejs +2 -2
  48. package/src/commands/generate-server.ts +3 -0
  49. package/src/domain/shared/ReadModel.ts +3 -3
  50. package/src/domain/shared/types.ts +5 -3
  51. package/src/server.ts +53 -15
package/package.json CHANGED
@@ -11,8 +11,10 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@event-driven-io/emmett": "^0.38.2",
14
+ "@event-driven-io/emmett-sqlite": "^0.38.5",
14
15
  "apollo-server": "^3.13.0",
15
16
  "apollo-server-express": "^3.13.0",
17
+ "better-sqlite3": "^12.4.1",
16
18
  "change-case": "^5.4.4",
17
19
  "ejs": "^3.1.10",
18
20
  "execa": "^9.6.0",
@@ -23,13 +25,14 @@
23
25
  "graphql-scalars": "^1.24.2",
24
26
  "prettier": "^3.6.1",
25
27
  "reflect-metadata": "^0.2.2",
28
+ "sqlite3": "^5.1.7",
26
29
  "type-fest": "^4.41.0",
27
30
  "type-graphql": "^2.0.0-rc.2",
28
31
  "graphql-type-json": "^0.3.2",
29
- "uuid": "^10.0.0",
32
+ "uuid": "^11.0.0",
30
33
  "web-streams-polyfill": "^4.1.0",
31
- "@auto-engineer/flow": "0.10.5",
32
- "@auto-engineer/message-bus": "0.10.5"
34
+ "@auto-engineer/flow": "0.11.0",
35
+ "@auto-engineer/message-bus": "0.11.0"
33
36
  },
34
37
  "publishConfig": {
35
38
  "access": "public"
@@ -37,9 +40,9 @@
37
40
  "devDependencies": {
38
41
  "@types/ejs": "^3.1.5",
39
42
  "@types/fs-extra": "^11.0.4",
40
- "@auto-engineer/cli": "0.10.5"
43
+ "@auto-engineer/cli": "0.11.0"
41
44
  },
42
- "version": "0.10.5",
45
+ "version": "0.11.0",
43
46
  "scripts": {
44
47
  "generate:server": "tsx src/cli/index.ts",
45
48
  "build": "tsc && tsx ../../scripts/fix-esm-imports.ts && rm -rf dist/src/codegen/templates && mkdir -p dist/src/codegen && cp -r src/codegen/templates dist/src/codegen/templates && cp src/server.ts dist/src && cp -r src/utils dist/src && cp -r src/domain dist/src",
@@ -339,7 +339,7 @@ describe('projection.ts.ejs', () => {
339
339
  @Ctx() ctx: GraphQLContext,
340
340
  @Arg('sessionId', () => ID, { nullable: true }) sessionId?: string,
341
341
  ): Promise<Wishlist[]> {
342
- const model = new ReadModel<Wishlist>(ctx.eventStore, 'WishlistProjection');
342
+ const model = new ReadModel<Wishlist>(ctx.database, 'WishlistProjection');
343
343
 
344
344
  // ## IMPLEMENTATION INSTRUCTIONS ##
345
345
  // You can query the projection using the ReadModel API:
@@ -176,6 +176,8 @@ if (idField.includes('-')) {
176
176
  formattedValue = `'${value}'`;
177
177
  } else if (tsType === 'number' || tsType === 'boolean') {
178
178
  formattedValue = String(value);
179
+ } else if (tsType === 'Date') {
180
+ formattedValue = `new Date('${value}')`;
179
181
  } else if (Array.isArray(value)) {
180
182
  formattedValue = JSON.stringify(value);
181
183
  } else {
@@ -67,7 +67,7 @@ describe('query.resolver.ts.ejs', () => {
67
67
  const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
68
68
 
69
69
  expect(resolverFile?.contents).toMatchInlineSnapshot(`
70
- "import { Query, Resolver, Arg, Ctx, ObjectType, Field } from 'type-graphql';
70
+ "import { Query, Resolver, Arg, Ctx, ObjectType, Field, Float } from 'type-graphql';
71
71
  import { type GraphQLContext, ReadModel } from '../../../shared';
72
72
 
73
73
  @ObjectType()
@@ -99,7 +99,7 @@ describe('query.resolver.ts.ejs', () => {
99
99
  @Arg('maxPrice', () => Float, { nullable: true }) maxPrice?: number,
100
100
  @Arg('minGuests', () => Float, { nullable: true }) minGuests?: number,
101
101
  ): Promise<AvailableListings[]> {
102
- const model = new ReadModel<AvailableListings>(ctx.eventStore, 'AvailablePropertiesProjection');
102
+ const model = new ReadModel<AvailableListings>(ctx.database, 'AvailablePropertiesProjection');
103
103
 
104
104
  // ## IMPLEMENTATION INSTRUCTIONS ##
105
105
  // You can query the projection using the ReadModel API:
@@ -190,7 +190,8 @@ describe('query.resolver.ts.ejs', () => {
190
190
  const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
191
191
 
192
192
  expect(resolverFile?.contents).toMatchInlineSnapshot(`
193
- "import { Query, Resolver, Arg, Ctx, ObjectType, Field, ID } from 'type-graphql';
193
+ "import { Query, Resolver, Arg, Ctx, ObjectType, Field, ID, Float } from 'type-graphql';
194
+ import { GraphQLJSON } from 'graphql-type-json';
194
195
  import { type GraphQLContext, ReadModel } from '../../../shared';
195
196
 
196
197
  @ObjectType()
@@ -226,7 +227,7 @@ describe('query.resolver.ts.ejs', () => {
226
227
  @Ctx() ctx: GraphQLContext,
227
228
  @Arg('sessionId', () => ID, { nullable: true }) sessionId?: string,
228
229
  ): Promise<SuggestedItems[]> {
229
- const model = new ReadModel<SuggestedItems>(ctx.eventStore, 'SuggestedItemsProjection');
230
+ const model = new ReadModel<SuggestedItems>(ctx.database, 'SuggestedItemsProjection');
230
231
 
231
232
  // ## IMPLEMENTATION INSTRUCTIONS ##
232
233
  // You can query the projection using the ReadModel API:
@@ -400,7 +401,7 @@ describe('query.resolver.ts.ejs', () => {
400
401
  @Ctx() ctx: GraphQLContext,
401
402
  @Arg('participantId', () => ID, { nullable: true }) participantId?: string,
402
403
  ): Promise<QuestionnaireProgress[]> {
403
- const model = new ReadModel<QuestionnaireProgress>(ctx.eventStore, 'Questionnaires');
404
+ const model = new ReadModel<QuestionnaireProgress>(ctx.database, 'Questionnaires');
404
405
 
405
406
  // ## IMPLEMENTATION INSTRUCTIONS ##
406
407
  // You can query the projection using the ReadModel API:
@@ -422,4 +423,188 @@ describe('query.resolver.ts.ejs', () => {
422
423
  "
423
424
  `);
424
425
  });
426
+ it('should import Float when Float fields are used', async () => {
427
+ const spec: SpecsSchema = {
428
+ variant: 'specs',
429
+ flows: [
430
+ {
431
+ name: 'product-flow',
432
+ slices: [
433
+ {
434
+ type: 'query',
435
+ name: 'get-product-price',
436
+ request: `
437
+ query GetProductPrice($productId: ID!) {
438
+ productPrice(productId: $productId) {
439
+ productId
440
+ price
441
+ discount
442
+ }
443
+ }
444
+ `,
445
+ client: {
446
+ description: '',
447
+ },
448
+ server: {
449
+ description: '',
450
+ data: [
451
+ {
452
+ origin: {
453
+ type: 'projection',
454
+ idField: 'productId',
455
+ name: 'ProductPricesProjection',
456
+ },
457
+ target: {
458
+ type: 'State',
459
+ name: 'ProductPrice',
460
+ },
461
+ },
462
+ ],
463
+ specs: { name: '', rules: [] },
464
+ },
465
+ },
466
+ ],
467
+ },
468
+ ],
469
+ messages: [
470
+ {
471
+ type: 'state',
472
+ name: 'ProductPrice',
473
+ fields: [
474
+ { name: 'productId', type: 'string', required: true },
475
+ { name: 'price', type: 'number', required: true },
476
+ { name: 'discount', type: 'number', required: true },
477
+ ],
478
+ },
479
+ ],
480
+ };
481
+
482
+ const plans = await generateScaffoldFilePlans(spec.flows, spec.messages, undefined, 'src/domain/flows');
483
+ const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
484
+
485
+ expect(resolverFile?.contents).toContain(
486
+ "import { Query, Resolver, Arg, Ctx, ObjectType, Field, ID, Float } from 'type-graphql';",
487
+ );
488
+ expect(resolverFile?.contents).toContain('@Field(() => Float)');
489
+ });
490
+ it('should import Float when array of numbers is used', async () => {
491
+ const spec: SpecsSchema = {
492
+ variant: 'specs',
493
+ flows: [
494
+ {
495
+ name: 'stats-flow',
496
+ slices: [
497
+ {
498
+ type: 'query',
499
+ name: 'get-stats',
500
+ request: `
501
+ query GetStats($userId: ID!) {
502
+ stats(userId: $userId) {
503
+ userId
504
+ scores
505
+ }
506
+ }
507
+ `,
508
+ client: {
509
+ description: '',
510
+ },
511
+ server: {
512
+ description: '',
513
+ data: [
514
+ {
515
+ origin: {
516
+ type: 'projection',
517
+ idField: 'userId',
518
+ name: 'StatsProjection',
519
+ },
520
+ target: {
521
+ type: 'State',
522
+ name: 'Stats',
523
+ },
524
+ },
525
+ ],
526
+ specs: { name: '', rules: [] },
527
+ },
528
+ },
529
+ ],
530
+ },
531
+ ],
532
+ messages: [
533
+ {
534
+ type: 'state',
535
+ name: 'Stats',
536
+ fields: [
537
+ { name: 'userId', type: 'string', required: true },
538
+ { name: 'scores', type: 'Array<number>', required: true },
539
+ ],
540
+ },
541
+ ],
542
+ };
543
+
544
+ const plans = await generateScaffoldFilePlans(spec.flows, spec.messages, undefined, 'src/domain/flows');
545
+ const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
546
+
547
+ expect(resolverFile?.contents).toContain('Float');
548
+ expect(resolverFile?.contents).toContain('@Field(() => [Float])');
549
+ });
550
+ it('should import Float when Float query arg is used', async () => {
551
+ const spec: SpecsSchema = {
552
+ variant: 'specs',
553
+ flows: [
554
+ {
555
+ name: 'search-flow',
556
+ slices: [
557
+ {
558
+ type: 'query',
559
+ name: 'search-products',
560
+ request: `
561
+ query SearchProducts($minPrice: Float, $maxPrice: Float) {
562
+ searchProducts(minPrice: $minPrice, maxPrice: $maxPrice) {
563
+ productId
564
+ name
565
+ }
566
+ }
567
+ `,
568
+ client: {
569
+ description: '',
570
+ },
571
+ server: {
572
+ description: '',
573
+ data: [
574
+ {
575
+ origin: {
576
+ type: 'projection',
577
+ idField: 'productId',
578
+ name: 'ProductsProjection',
579
+ },
580
+ target: {
581
+ type: 'State',
582
+ name: 'Product',
583
+ },
584
+ },
585
+ ],
586
+ specs: { name: '', rules: [] },
587
+ },
588
+ },
589
+ ],
590
+ },
591
+ ],
592
+ messages: [
593
+ {
594
+ type: 'state',
595
+ name: 'Product',
596
+ fields: [
597
+ { name: 'productId', type: 'string', required: true },
598
+ { name: 'name', type: 'string', required: true },
599
+ ],
600
+ },
601
+ ],
602
+ };
603
+
604
+ const plans = await generateScaffoldFilePlans(spec.flows, spec.messages, undefined, 'src/domain/flows');
605
+ const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
606
+
607
+ expect(resolverFile?.contents).toContain('Float');
608
+ expect(resolverFile?.contents).toContain("@Arg('minPrice', () => Float");
609
+ });
425
610
  });
@@ -20,23 +20,46 @@ function baseTs(ts) {
20
20
  }
21
21
  function fieldUsesDate(ts) {
22
22
  const b = baseTs(ts);
23
- if (b === 'Date') return true;
23
+ const gqlType = graphqlType(b);
24
+ if (gqlType.includes('GraphQLISODateTime')) return true;
24
25
  if (isInlineObject(b) || isInlineObjectArray(b)) return /:\s*Date\b/.test(b);
25
26
  return false;
26
27
  }
27
28
  function fieldUsesJSON(ts) {
28
29
  const b = baseTs(ts);
29
- if (b === 'unknown' || b === 'any' || b === 'object') return true;
30
+ const gqlType = graphqlType(b);
31
+ if (gqlType.includes('GraphQLJSON') || gqlType.includes('JSON')) return true;
30
32
  if (isInlineObject(b) || isInlineObjectArray(b)) return /:\s*(unknown|any|object)\b/.test(b);
31
33
  return false;
32
34
  }
35
+ function fieldUsesFloat(ts) {
36
+ const b = baseTs(ts);
37
+ const gqlType = graphqlType(b);
38
+ if (gqlType.includes('Float')) return true;
39
+ if (isInlineObject(b) || isInlineObjectArray(b)) {
40
+ const inner = b.trim().startsWith('Array<')
41
+ ? b.trim().replace(/^Array<\{/, '{').replace(/}>$/, '}')
42
+ : b.trim().replace(/\[\]$/, '');
43
+ const match = inner.match(/^\{([\s\S]*)\}$/);
44
+ const body = match ? match[1] : '';
45
+ const rawFields = body.split(/[,;]\s*/).filter(Boolean);
46
+ return rawFields.some(f => {
47
+ const parts = f.split(':');
48
+ const type = parts.slice(1).join(':').trim();
49
+ return type && graphqlType(type).includes('Float');
50
+ });
51
+ }
52
+ return false;
53
+ }
33
54
 
34
55
  const messageFields = message?.fields ?? [];
35
56
  const usesDate = messageFields.some(f => fieldUsesDate(f.type)) ||
36
- (parsedRequest?.args ?? []).some(a => baseTs(a.tsType) === 'Date');
37
- const usesJSON = messageFields.some(f => fieldUsesJSON(f.type));
57
+ (parsedRequest?.args ?? []).some(a => fieldUsesDate(a.tsType));
58
+ const usesJSON = messageFields.some(f => fieldUsesJSON(f.type)) ||
59
+ (parsedRequest?.args ?? []).some(a => fieldUsesJSON(a.tsType));
60
+ const usesFloat = messageFields.some(f => fieldUsesFloat(f.type)) ||
61
+ (parsedRequest?.args ?? []).some(a => fieldUsesFloat(a.tsType));
38
62
 
39
- // Collect embedded types up-front so we can emit them before the parent
40
63
  const embeddedTypes = [];
41
64
  for (const field of messageFields) {
42
65
  const tsType = field.type ?? 'string';
@@ -48,13 +71,12 @@ for (const field of messageFields) {
48
71
  }
49
72
  }
50
73
  %>
51
- import { Query, Resolver, Arg, Ctx, ObjectType, Field<% if (usesID) { %>, ID<% } %><% if (usesDate) { %>, GraphQLISODateTime<% } %> } from 'type-graphql';
74
+ import { Query, Resolver, Arg, Ctx, ObjectType, Field<% if (usesID) { %>, ID<% } %><% if (usesFloat) { %>, Float<% } %><% if (usesDate) { %>, GraphQLISODateTime<% } %> } from 'type-graphql';
52
75
  <% if (usesJSON) { %>import { GraphQLJSON } from 'graphql-type-json';
53
76
  <% } %>import { type GraphQLContext, ReadModel } from '../../../shared';
54
77
 
55
- <% // Emit embedded types FIRST — this matches your snapshot order
78
+ <%
56
79
  for (const { typeName, tsType } of embeddedTypes) {
57
- // Extract inner "{ ... }" whether Array<{...}> or "{...}[]"
58
80
  const inner = tsType.trim().startsWith('Array<')
59
81
  ? tsType.trim().replace(/^Array<\{/, '{').replace(/}>$/, '}')
60
82
  : tsType.trim().replace(/\[\]$/, '');
@@ -114,7 +136,7 @@ async <%= queryName %>(
114
136
  %> @Arg('<%= arg.name %>', () => <%= gqlType %>, { nullable: true }) <%= arg.name %>?: <%= tsType %><%= i < parsedRequest.args.length - 1 ? ',' : '' %>
115
137
  <% } } %>
116
138
  ): Promise<<%= viewType %>[]> {
117
- const model = new ReadModel<<%= viewType %>>(ctx.eventStore, '<%= projectionType %>');
139
+ const model = new ReadModel<<%= viewType %>>(ctx.database, '<%= projectionType %>');
118
140
 
119
141
  // ## IMPLEMENTATION INSTRUCTIONS ##
120
142
  // You can query the projection using the ReadModel API:
@@ -184,7 +184,7 @@ describe('react.specs.ts.ejs (react slice)', () => {
184
184
  beforeEach(() => {
185
185
  eventStore = getInMemoryEventStore({});
186
186
  given = ReactorSpecification.for<ReactorEvent, ReactorCommand, ReactorContext>(
187
- () => react({ eventStore, commandSender: messageBus }),
187
+ () => react({ eventStore, commandSender: messageBus, database: eventStore.database }),
188
188
  (commandSender) => {
189
189
  messageBus = commandSender;
190
190
  return {
@@ -223,12 +223,12 @@ describe('handle.ts.ejs (react slice)', () => {
223
223
  import type { BookingRequested } from '../guest-submits-booking-request/events';
224
224
  import type { ReactorContext } from '../../../shared';
225
225
 
226
- export const react = ({ eventStore, commandSender }: ReactorContext) =>
226
+ export const react = ({ eventStore, commandSender, database }: ReactorContext) =>
227
227
  inMemoryReactor<BookingRequested>({
228
228
  processorId: 'manage-bookings-send-notification-to-host',
229
229
  canHandle: ['BookingRequested'],
230
230
  connectionOptions: {
231
- database: eventStore.database,
231
+ database,
232
232
  },
233
233
  eachMessage: async (event, context): Promise<MessageHandlerResult> => {
234
234
  /**
@@ -236,7 +236,7 @@ describe('handle.ts.ejs (react slice)', () => {
236
236
  *
237
237
  * - Inspect event data to determine if the command should be sent.
238
238
  * - Replace the placeholder logic and \\\`throw\\\` below with real implementation.
239
- * - Send one or more commands via: context.commandSender.send({...})
239
+ * - Send one or more commands via: commandSender.send({...})
240
240
  * - Optionally return a MessageHandlerResult for SKIP or error cases.
241
241
  */
242
242
 
@@ -250,7 +250,7 @@ describe('handle.ts.ejs (react slice)', () => {
250
250
  // };
251
251
  // }
252
252
 
253
- // await context.commandSender.send({
253
+ // await commandSender.send({
254
254
  // type: 'NotifyHost',
255
255
  // kind: 'Command',
256
256
  // data: {
@@ -87,7 +87,7 @@ describe('<%= ruleDescription %>', () => {
87
87
  beforeEach(() => {
88
88
  eventStore = getInMemoryEventStore({});
89
89
  given = ReactorSpecification.for<ReactorEvent, ReactorCommand, ReactorContext>(
90
- () => react({ eventStore, commandSender: messageBus }),
90
+ () => react({ eventStore, commandSender: messageBus, database: eventStore.database }),
91
91
  (commandSender) => {
92
92
  messageBus = commandSender;
93
93
  return {
@@ -22,12 +22,12 @@ IllegalStateError,
22
22
  import type { <%= pascalCase(eventType) %> } from '../<%= toKebabCase(event?.sourceSliceName ?? 'unknown') %>/events';
23
23
  import type { ReactorContext } from '../../../shared';
24
24
 
25
- export const react = ({ eventStore, commandSender }: ReactorContext) =>
25
+ export const react = ({ eventStore, commandSender, database }: ReactorContext) =>
26
26
  inMemoryReactor<<%= pascalCase(eventType) %>>({
27
27
  processorId: '<%= toKebabCase(flowName) %>-<%= toKebabCase(slice.name) %>',
28
28
  canHandle: ['<%= eventType %>'],
29
29
  connectionOptions: {
30
- database: eventStore.database,
30
+ database,
31
31
  },
32
32
  eachMessage: async (event, context): Promise<MessageHandlerResult> => {
33
33
  /**
@@ -35,7 +35,7 @@ eachMessage: async (event, context): Promise<MessageHandlerResult> => {
35
35
  *
36
36
  * - Inspect event data to determine if the command should be sent.
37
37
  * - Replace the placeholder logic and \`throw\` below with real implementation.
38
- * - Send one or more commands via: context.commandSender.send({...})
38
+ * - Send one or more commands via: commandSender.send({...})
39
39
  * - Optionally return a MessageHandlerResult for SKIP or error cases.
40
40
  */
41
41
 
@@ -49,7 +49,7 @@ eachMessage: async (event, context): Promise<MessageHandlerResult> => {
49
49
  // };
50
50
  // }
51
51
 
52
- // await context.commandSender.send({
52
+ // await commandSender.send({
53
53
  // type: '<%= commandType %>',
54
54
  // kind: 'Command',
55
55
  // data: {
@@ -219,10 +219,10 @@ describe('register.ts.ejs (react slice)', () => {
219
219
  const registerFile = plans.find((p) => p.outputPath.endsWith('send-notification-to-host/register.ts'));
220
220
 
221
221
  expect(registerFile?.contents).toMatchInlineSnapshot(`
222
- "import { type CommandSender, type EventSubscription, type InMemoryEventStore } from '@event-driven-io/emmett';
222
+ "import { type CommandSender, type EventSubscription, type EventStore } from '@event-driven-io/emmett';
223
223
  import type { BookingRequested } from '../guest-submits-booking-request/events';
224
224
 
225
- export async function register(messageBus: CommandSender & EventSubscription, eventStore: InMemoryEventStore) {
225
+ export async function register(messageBus: CommandSender & EventSubscription, eventStore: EventStore) {
226
226
  messageBus.subscribe(async (event: BookingRequested) => {
227
227
  /**
228
228
  * ## IMPLEMENTATION INSTRUCTIONS ##
@@ -14,12 +14,12 @@ const eventType = when?.eventRef;
14
14
  const commandType = then?.commandRef;
15
15
  const event = events.find(e => e.type === eventType);
16
16
  %>
17
- import { type CommandSender, type EventSubscription, type InMemoryEventStore } from '@event-driven-io/emmett';
17
+ import { type CommandSender, type EventSubscription, type EventStore } from '@event-driven-io/emmett';
18
18
  import type { <%= pascalCase(eventType) %> } from '../<%= toKebabCase(event?.sourceSliceName ?? 'unknown') %>/events';
19
19
 
20
20
  export async function register(
21
21
  messageBus: CommandSender & EventSubscription,
22
- eventStore: InMemoryEventStore
22
+ eventStore: EventStore
23
23
  ) {
24
24
  messageBus.subscribe(
25
25
  async (event: <%= pascalCase(eventType) %>) => {
@@ -415,9 +415,11 @@ async function writePackage(dest: string): Promise<void> {
415
415
  'type-check': 'tsc --noEmit',
416
416
  test: 'vitest run',
417
417
  dev: 'tsx --no-deprecation src/server.ts',
418
+ postinstall: 'npm rebuild sqlite3 2>/dev/null || true',
418
419
  },
419
420
  dependencies: resolveDeps([
420
421
  '@event-driven-io/emmett',
422
+ '@event-driven-io/emmett-sqlite',
421
423
  'type-graphql',
422
424
  'graphql-type-json',
423
425
  'graphql',
@@ -426,6 +428,7 @@ async function writePackage(dest: string): Promise<void> {
426
428
  'zod',
427
429
  'apollo-server',
428
430
  'uuid',
431
+ 'sqlite3',
429
432
  ]),
430
433
  devDependencies: resolveDeps(['typescript', 'vitest', 'tsx']),
431
434
  } as const;
@@ -1,10 +1,10 @@
1
- import type { InMemoryEventStore } from '@event-driven-io/emmett';
1
+ import type { InMemoryDatabase } from '@event-driven-io/emmett';
2
2
 
3
3
  export class ReadModel<T extends Record<string, unknown>> {
4
4
  private collection;
5
5
 
6
- constructor(eventStore: InMemoryEventStore, collectionName: string) {
7
- this.collection = eventStore.database.collection<T>(collectionName);
6
+ constructor(database: InMemoryDatabase, collectionName: string) {
7
+ this.collection = database.collection<T>(collectionName);
8
8
  }
9
9
 
10
10
  async getAll(): Promise<T[]> {
@@ -1,15 +1,17 @@
1
- import { CommandSender, InMemoryEventStore } from '@event-driven-io/emmett';
1
+ import { CommandSender, EventStore, type InMemoryDatabase } from '@event-driven-io/emmett';
2
2
  import { Field, ObjectType } from 'type-graphql';
3
3
 
4
4
  export interface ReactorContext {
5
- eventStore: InMemoryEventStore;
5
+ eventStore: EventStore;
6
6
  commandSender: CommandSender;
7
+ database: InMemoryDatabase;
7
8
  [key: string]: unknown;
8
9
  }
9
10
 
10
11
  export interface GraphQLContext {
11
- eventStore: InMemoryEventStore;
12
+ eventStore: EventStore;
12
13
  messageBus: CommandSender;
14
+ database: InMemoryDatabase;
13
15
  }
14
16
 
15
17
  @ObjectType()
package/src/server.ts CHANGED
@@ -2,38 +2,76 @@ import 'reflect-metadata';
2
2
  import { ApolloServer } from 'apollo-server';
3
3
  import { buildSchema } from 'type-graphql';
4
4
  import { loadProjections, loadRegisterFiles, loadResolvers } from './utils';
5
- import {
6
- getInMemoryEventStore,
7
- getInMemoryMessageBus,
8
- projections,
9
- forwardToMessageBus,
10
- } from '@event-driven-io/emmett';
5
+ import { getInMemoryMessageBus, getInMemoryDatabase, handleInMemoryProjections } from '@event-driven-io/emmett';
6
+ import { getSQLiteEventStore } from '@event-driven-io/emmett-sqlite';
11
7
 
12
8
  async function start() {
13
9
  const loadedProjections = await loadProjections('src/domain/flows/**/projection.{ts,js}');
14
10
  const registrations = await loadRegisterFiles('src/domain/flows/**/register.{ts,js}');
15
11
 
16
12
  const messageBus = getInMemoryMessageBus();
13
+ const database = getInMemoryDatabase();
17
14
 
18
- const eventStore = getInMemoryEventStore({
19
- projections: projections.inline(loadedProjections),
20
- hooks: {
21
- onAfterCommit: forwardToMessageBus(messageBus),
22
- },
15
+ const eventStore = getSQLiteEventStore({
16
+ fileName: './event-store.sqlite',
17
+ schema: { autoMigration: 'CreateOrUpdate' },
23
18
  });
24
-
19
+ try {
20
+ await eventStore.readStream('__init__');
21
+ } catch {
22
+ // Expected on fresh DB - schema gets created on first operation
23
+ }
25
24
  await Promise.all(registrations.map((r) => r.register(messageBus, eventStore)));
26
-
25
+ const consumer = eventStore.consumer();
26
+ consumer.processor({
27
+ processorId: 'projection-updater',
28
+ startFrom: 'BEGINNING',
29
+ eachMessage: async (event) => {
30
+ await handleInMemoryProjections({
31
+ projections: loadedProjections,
32
+ database,
33
+ events: [event],
34
+ });
35
+ },
36
+ });
37
+ consumer.processor({
38
+ processorId: 'forward-to-message-bus',
39
+ startFrom: 'BEGINNING',
40
+ eachMessage: async (evt) => {
41
+ await messageBus.publish(evt);
42
+ },
43
+ });
44
+ consumer.start().catch((err) => {
45
+ console.error('Consumer crashed:', err);
46
+ process.exit(1);
47
+ });
48
+ const shutdown = async () => {
49
+ console.log('Shutting down...');
50
+ try {
51
+ await consumer.stop?.();
52
+ } catch {
53
+ /* empty */
54
+ }
55
+ try {
56
+ await consumer.close?.();
57
+ } catch {
58
+ /* empty */
59
+ }
60
+ process.exit(0);
61
+ };
62
+ process.on('SIGINT', () => void shutdown());
63
+ process.on('SIGTERM', () => void shutdown());
27
64
  const resolvers = await loadResolvers('src/domain/flows/**/*.resolver.{ts,js}');
65
+ type ResolverClass = new (...args: unknown[]) => unknown;
28
66
  const schema = await buildSchema({
29
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
- resolvers: resolvers as unknown as [(...args: any[]) => any, ...Array<(...args: any[]) => any>],
67
+ resolvers: resolvers as unknown as [ResolverClass, ...ResolverClass[]],
31
68
  });
32
69
  const server = new ApolloServer({
33
70
  schema,
34
71
  context: () => ({
35
72
  eventStore,
36
73
  messageBus,
74
+ database,
37
75
  }),
38
76
  });
39
77
  const { url } = await server.listen({ port: 4000 });