@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.
- package/.turbo/turbo-build.log +6 -0
- package/.turbo/turbo-format.log +5 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +22 -0
- package/.turbo/turbo-type-check.log +5 -0
- package/CHANGELOG.md +12 -0
- package/dist/src/codegen/scaffoldFromSchema.query-slice-register.specs.d.ts +2 -0
- package/dist/src/codegen/scaffoldFromSchema.query-slice-register.specs.d.ts.map +1 -0
- package/dist/src/codegen/scaffoldFromSchema.query-slice-register.specs.js +168 -0
- package/dist/src/codegen/scaffoldFromSchema.query-slice-register.specs.js.map +1 -0
- package/dist/src/codegen/templates/query/projection.specs.ts +1 -1
- package/dist/src/codegen/templates/query/projection.specs.ts.ejs +2 -0
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +190 -5
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +31 -9
- package/dist/src/codegen/templates/react/react.specs.specs.ts +1 -1
- package/dist/src/codegen/templates/react/react.specs.ts +4 -4
- package/dist/src/codegen/templates/react/react.specs.ts.ejs +1 -1
- package/dist/src/codegen/templates/react/react.ts.ejs +4 -4
- package/dist/src/codegen/templates/react/register.specs.ts +2 -2
- package/dist/src/codegen/templates/react/register.ts.ejs +2 -2
- package/dist/src/commands/generate-server.d.ts.map +1 -1
- package/dist/src/commands/generate-server.js +3 -0
- package/dist/src/commands/generate-server.js.map +1 -1
- package/dist/src/domain/shared/ReadModel.d.ts +2 -2
- package/dist/src/domain/shared/ReadModel.d.ts.map +1 -1
- package/dist/src/domain/shared/ReadModel.js +2 -2
- package/dist/src/domain/shared/ReadModel.js.map +1 -1
- package/dist/src/domain/shared/ReadModel.ts +3 -3
- package/dist/src/domain/shared/types.d.ts +5 -3
- package/dist/src/domain/shared/types.d.ts.map +1 -1
- package/dist/src/domain/shared/types.js.map +1 -1
- package/dist/src/domain/shared/types.ts +5 -3
- package/dist/src/server.js +54 -7
- package/dist/src/server.js.map +1 -1
- package/dist/src/server.ts +53 -15
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -5
- package/src/codegen/templates/query/projection.specs.ts +1 -1
- package/src/codegen/templates/query/projection.specs.ts.ejs +2 -0
- package/src/codegen/templates/query/query.resolver.specs.ts +190 -5
- package/src/codegen/templates/query/query.resolver.ts.ejs +31 -9
- package/src/codegen/templates/react/react.specs.specs.ts +1 -1
- package/src/codegen/templates/react/react.specs.ts +4 -4
- package/src/codegen/templates/react/react.specs.ts.ejs +1 -1
- package/src/codegen/templates/react/react.ts.ejs +4 -4
- package/src/codegen/templates/react/register.specs.ts +2 -2
- package/src/codegen/templates/react/register.ts.ejs +2 -2
- package/src/commands/generate-server.ts +3 -0
- package/src/domain/shared/ReadModel.ts +3 -3
- package/src/domain/shared/types.ts +5 -3
- 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": "^
|
|
32
|
+
"uuid": "^11.0.0",
|
|
30
33
|
"web-streams-polyfill": "^4.1.0",
|
|
31
|
-
"@auto-engineer/flow": "0.
|
|
32
|
-
"@auto-engineer/message-bus": "0.
|
|
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.
|
|
43
|
+
"@auto-engineer/cli": "0.11.0"
|
|
41
44
|
},
|
|
42
|
-
"version": "0.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 =>
|
|
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
|
-
<%
|
|
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.
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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 {
|
|
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(
|
|
7
|
-
this.collection =
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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 });
|