@auto-engineer/server-generator-apollo-emmett 1.70.0 → 1.71.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 +31 -0
- package/dist/src/codegen/templates/react/react.specs.specs.ts +199 -0
- package/dist/src/codegen/templates/react/react.specs.ts +17 -24
- package/dist/src/codegen/templates/react/react.specs.ts.ejs +20 -0
- package/dist/src/codegen/templates/react/react.ts.ejs +54 -24
- package/dist/src/codegen/templates/react/react.ts.specs.ts +152 -0
- package/ketchup-plan.md +5 -2
- package/package.json +4 -4
- package/src/codegen/templates/react/react.specs.specs.ts +199 -0
- package/src/codegen/templates/react/react.specs.ts +17 -24
- package/src/codegen/templates/react/react.specs.ts.ejs +20 -0
- package/src/codegen/templates/react/react.ts.ejs +54 -24
- package/src/codegen/templates/react/react.ts.specs.ts +152 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @auto-engineer/server-generator-apollo-emmett@1.
|
|
2
|
+
> @auto-engineer/server-generator-apollo-emmett@1.71.0 build /home/runner/work/auto-engineer/auto-engineer/packages/server-generator-apollo-emmett
|
|
3
3
|
> 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
|
|
4
4
|
|
|
5
5
|
Fixed ESM imports in dist/
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
|
|
2
|
-
> @auto-engineer/server-generator-apollo-emmett@1.
|
|
2
|
+
> @auto-engineer/server-generator-apollo-emmett@1.70.0 test /home/runner/work/auto-engineer/auto-engineer/packages/server-generator-apollo-emmett
|
|
3
3
|
> vitest run --reporter=dot
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
[1m[46m RUN [49m[22m [36mv3.2.4 [39m[90m/home/runner/work/auto-engineer/auto-engineer/packages/server-generator-apollo-emmett[39m
|
|
7
7
|
|
|
8
|
-
[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[2m[90m-[39m[22m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m
|
|
8
|
+
[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[2m[90m-[39m[22m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m[33m[39m[32m·[39m
|
|
9
9
|
|
|
10
10
|
[2m Test Files [22m [1m[32m30 passed[39m[22m[2m | [22m[33m1 skipped[39m[90m (31)[39m
|
|
11
|
-
[2m Tests [22m [1m[
|
|
12
|
-
[2m Start at [22m 06:
|
|
13
|
-
[2m Duration [22m
|
|
11
|
+
[2m Tests [22m [1m[32m172 passed[39m[22m[2m | [22m[33m1 skipped[39m[90m (173)[39m
|
|
12
|
+
[2m Start at [22m 12:06:20
|
|
13
|
+
[2m Duration [22m 22.20s[2m (transform 3.79s, setup 0ms, collect 45.99s, tests 8.68s, environment 6ms, prepare 4.29s)[22m
|
|
14
14
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
|
|
2
|
-
> @auto-engineer/server-generator-apollo-emmett@1.
|
|
2
|
+
> @auto-engineer/server-generator-apollo-emmett@1.70.0 type-check /home/runner/work/auto-engineer/auto-engineer/packages/server-generator-apollo-emmett
|
|
3
3
|
> tsc --noEmit --project tsconfig.json
|
|
4
4
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @auto-engineer/server-generator-apollo-emmett
|
|
2
2
|
|
|
3
|
+
## 1.71.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`82543ae`](https://github.com/BeOnAuto/auto-engineer/commit/82543aec90a4398696e148f9ea26e5f4df51eb91) Thanks [@rami-hatoum](https://github.com/rami-hatoum)! - - **server-generator-apollo-emmett**: seed event store with Given state data in react specs template
|
|
8
|
+
|
|
9
|
+
- [`6a7a3f2`](https://github.com/BeOnAuto/auto-engineer/commit/6a7a3f237c1267e1835168afaccf21e482e48278) Thanks [@rami-hatoum](https://github.com/rami-hatoum)! - - **server-generator-apollo-emmett**: generate aggregateStream for Given states
|
|
10
|
+
- **server-generator-apollo-emmett**: add Burst 3 to ketchup plan for commandSender.send generation
|
|
11
|
+
- **server-generator-apollo-emmett**: mark all react Given states bursts done
|
|
12
|
+
|
|
13
|
+
- [`560d192`](https://github.com/BeOnAuto/auto-engineer/commit/560d19212be145f358ab2176c2424f6a8849070f) Thanks [@rami-hatoum](https://github.com/rami-hatoum)! - - **server-generator-apollo-emmett**: generate real commandSender.send() in react scaffold
|
|
14
|
+
|
|
15
|
+
- [`b5f823a`](https://github.com/BeOnAuto/auto-engineer/commit/b5f823a8b6e08c05518220aedad5b686e8bf37ac) Thanks [@rami-hatoum](https://github.com/rami-hatoum)! - - **server-generator-apollo-emmett**: generate aggregateStream for Given states
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [`dde21b9`](https://github.com/BeOnAuto/auto-engineer/commit/dde21b9f06c8aaba5d18cfa4f1a68eed3bf4c190) Thanks [@rami-hatoum](https://github.com/rami-hatoum)! - - **server-generator-apollo-emmett**: mark all react Given states bursts done
|
|
20
|
+
|
|
21
|
+
- [`28efa44`](https://github.com/BeOnAuto/auto-engineer/commit/28efa44f51373af11febb3fff00d85d1b3350784) Thanks [@rami-hatoum](https://github.com/rami-hatoum)! - - **server-generator-apollo-emmett**: mark Burst 3 done in ketchup plan
|
|
22
|
+
|
|
23
|
+
- [`8b1238a`](https://github.com/BeOnAuto/auto-engineer/commit/8b1238ab2c98ad7bf396987edabf642b43b730bd) Thanks [@rami-hatoum](https://github.com/rami-hatoum)! - - **server-generator-apollo-emmett**: add ketchup plan for react slice Given states fix
|
|
24
|
+
|
|
25
|
+
- [`e7a4ddf`](https://github.com/BeOnAuto/auto-engineer/commit/e7a4ddff607260651ecbf504a881a21db4657f19) Thanks [@github-actions[bot]](https://github.com/github-actions%5Bbot%5D)! - - **job-graph-processor**: increase time tolerance for parallel job dispatch test
|
|
26
|
+
- **root**: update @event-driven-io/emmett and @event-driven-io/emmett-sqlite versions to 0.42.0
|
|
27
|
+
- **ci**: remove --reporter=append to show pnpm install errors
|
|
28
|
+
- **generate-react-client**: update landing page header text
|
|
29
|
+
- **pipeline**: emit to event store before SSE broadcast in broadcastPipelineRunStarted
|
|
30
|
+
- Updated dependencies [[`82543ae`](https://github.com/BeOnAuto/auto-engineer/commit/82543aec90a4398696e148f9ea26e5f4df51eb91), [`dde21b9`](https://github.com/BeOnAuto/auto-engineer/commit/dde21b9f06c8aaba5d18cfa4f1a68eed3bf4c190), [`28efa44`](https://github.com/BeOnAuto/auto-engineer/commit/28efa44f51373af11febb3fff00d85d1b3350784), [`6a7a3f2`](https://github.com/BeOnAuto/auto-engineer/commit/6a7a3f237c1267e1835168afaccf21e482e48278), [`560d192`](https://github.com/BeOnAuto/auto-engineer/commit/560d19212be145f358ab2176c2424f6a8849070f), [`8b1238a`](https://github.com/BeOnAuto/auto-engineer/commit/8b1238ab2c98ad7bf396987edabf642b43b730bd), [`e7a4ddf`](https://github.com/BeOnAuto/auto-engineer/commit/e7a4ddff607260651ecbf504a881a21db4657f19), [`b5f823a`](https://github.com/BeOnAuto/auto-engineer/commit/b5f823a8b6e08c05518220aedad5b686e8bf37ac)]:
|
|
31
|
+
- @auto-engineer/message-bus@1.71.0
|
|
32
|
+
- @auto-engineer/narrative@1.71.0
|
|
33
|
+
|
|
3
34
|
## 1.70.0
|
|
4
35
|
|
|
5
36
|
### Minor Changes
|
|
@@ -368,4 +368,203 @@ describe('react.specs.ts.ejs (react slice)', () => {
|
|
|
368
368
|
expect(specFile?.contents).not.toContain("tipAmount: '5.00'");
|
|
369
369
|
expect(specFile?.contents).not.toContain('tipAmount: "5.00"');
|
|
370
370
|
});
|
|
371
|
+
|
|
372
|
+
it('should seed event store with Given state data via appendToStream', async () => {
|
|
373
|
+
const spec: SpecsSchema = {
|
|
374
|
+
variant: 'specs',
|
|
375
|
+
narratives: [
|
|
376
|
+
{
|
|
377
|
+
name: 'barbershop flow',
|
|
378
|
+
slices: [
|
|
379
|
+
{
|
|
380
|
+
type: 'command',
|
|
381
|
+
name: 'cancel appointment',
|
|
382
|
+
client: { specs: [] },
|
|
383
|
+
server: {
|
|
384
|
+
description: '',
|
|
385
|
+
specs: [
|
|
386
|
+
{
|
|
387
|
+
type: 'gherkin',
|
|
388
|
+
feature: 'Cancel appointment command',
|
|
389
|
+
rules: [
|
|
390
|
+
{
|
|
391
|
+
name: 'Should cancel appointment',
|
|
392
|
+
examples: [
|
|
393
|
+
{
|
|
394
|
+
name: 'Appointment cancelled',
|
|
395
|
+
steps: [
|
|
396
|
+
{
|
|
397
|
+
keyword: 'When',
|
|
398
|
+
text: 'CancelAppointment',
|
|
399
|
+
docString: { appointmentId: 'apt_001', barberId: 'barber_001' },
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
keyword: 'Then',
|
|
403
|
+
text: 'AppointmentCancelled',
|
|
404
|
+
docString: { appointmentId: 'apt_001', barberId: 'barber_001' },
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
type: 'react',
|
|
417
|
+
name: 'notify barber of cancellation',
|
|
418
|
+
server: {
|
|
419
|
+
description: 'Notifies barber when appointment is cancelled',
|
|
420
|
+
data: {
|
|
421
|
+
items: [
|
|
422
|
+
{
|
|
423
|
+
target: { type: 'Command', name: 'NotifyBarber' },
|
|
424
|
+
destination: { type: 'stream', pattern: 'barber-${barberId}' },
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
},
|
|
428
|
+
specs: [
|
|
429
|
+
{
|
|
430
|
+
type: 'gherkin',
|
|
431
|
+
feature: 'Notify barber of cancellation reaction',
|
|
432
|
+
rules: [
|
|
433
|
+
{
|
|
434
|
+
name: 'Should notify barber on cancellation',
|
|
435
|
+
examples: [
|
|
436
|
+
{
|
|
437
|
+
name: 'Barber notified after cancellation',
|
|
438
|
+
steps: [
|
|
439
|
+
{
|
|
440
|
+
keyword: 'Given',
|
|
441
|
+
text: 'Appointment',
|
|
442
|
+
docString: { barberId: 'barber_001', clientName: 'John' },
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
keyword: 'When',
|
|
446
|
+
text: 'AppointmentCancelled',
|
|
447
|
+
docString: { appointmentId: 'apt_001', barberId: 'barber_001' },
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
keyword: 'Then',
|
|
451
|
+
text: 'NotifyBarber',
|
|
452
|
+
docString: { barberId: 'barber_001', message: 'Appointment cancelled' },
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
},
|
|
460
|
+
],
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
messages: [
|
|
467
|
+
{
|
|
468
|
+
type: 'command',
|
|
469
|
+
name: 'CancelAppointment',
|
|
470
|
+
fields: [
|
|
471
|
+
{ name: 'appointmentId', type: 'string', required: true },
|
|
472
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
473
|
+
],
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
type: 'event',
|
|
477
|
+
name: 'AppointmentCancelled',
|
|
478
|
+
source: 'internal',
|
|
479
|
+
fields: [
|
|
480
|
+
{ name: 'appointmentId', type: 'string', required: true },
|
|
481
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
482
|
+
],
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
type: 'command',
|
|
486
|
+
name: 'NotifyBarber',
|
|
487
|
+
fields: [
|
|
488
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
489
|
+
{ name: 'message', type: 'string', required: true },
|
|
490
|
+
],
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
type: 'state',
|
|
494
|
+
name: 'Appointment',
|
|
495
|
+
fields: [
|
|
496
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
497
|
+
{ name: 'clientName', type: 'string', required: true },
|
|
498
|
+
],
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
504
|
+
|
|
505
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('notify-barber-of-cancellation/react.specs.ts'));
|
|
506
|
+
expect(specFile?.contents).toMatchInlineSnapshot(`
|
|
507
|
+
"import { describe, it, beforeEach } from 'vitest';
|
|
508
|
+
import 'reflect-metadata';
|
|
509
|
+
import { getInMemoryEventStore, type InMemoryEventStore, type CommandSender } from '@event-driven-io/emmett';
|
|
510
|
+
import { type ReactorContext, ReactorSpecification } from '../../../shared';
|
|
511
|
+
import { react } from './react';
|
|
512
|
+
import type { AppointmentCancelled } from '../cancel-appointment/events';
|
|
513
|
+
import type { NotifyBarber } from './commands';
|
|
514
|
+
|
|
515
|
+
type ReactorEvent = AppointmentCancelled;
|
|
516
|
+
type ReactorCommand = NotifyBarber;
|
|
517
|
+
|
|
518
|
+
describe('Should notify barber on cancellation', () => {
|
|
519
|
+
let eventStore: InMemoryEventStore;
|
|
520
|
+
let given: ReactorSpecification<ReactorEvent, ReactorCommand, ReactorContext>;
|
|
521
|
+
let messageBus: CommandSender;
|
|
522
|
+
|
|
523
|
+
beforeEach(() => {
|
|
524
|
+
eventStore = getInMemoryEventStore({});
|
|
525
|
+
given = ReactorSpecification.for<ReactorEvent, ReactorCommand, ReactorContext>(
|
|
526
|
+
() => react({ eventStore, commandSender: messageBus, database: eventStore.database }),
|
|
527
|
+
(commandSender) => {
|
|
528
|
+
messageBus = commandSender;
|
|
529
|
+
return {
|
|
530
|
+
eventStore,
|
|
531
|
+
commandSender,
|
|
532
|
+
database: eventStore.database,
|
|
533
|
+
};
|
|
534
|
+
},
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('Barber notified after cancellation', async () => {
|
|
539
|
+
await eventStore.appendToStream('Appointment-barber_001', [
|
|
540
|
+
{
|
|
541
|
+
type: 'AppointmentInitialized',
|
|
542
|
+
data: {
|
|
543
|
+
barberId: 'barber_001',
|
|
544
|
+
clientName: 'John',
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
]);
|
|
548
|
+
await given([])
|
|
549
|
+
.when({
|
|
550
|
+
type: 'AppointmentCancelled',
|
|
551
|
+
data: {
|
|
552
|
+
appointmentId: 'apt_001',
|
|
553
|
+
barberId: 'barber_001',
|
|
554
|
+
},
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
.then({
|
|
558
|
+
type: 'NotifyBarber',
|
|
559
|
+
kind: 'Command',
|
|
560
|
+
data: {
|
|
561
|
+
barberId: 'barber_001',
|
|
562
|
+
message: 'Appointment cancelled',
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
"
|
|
568
|
+
`);
|
|
569
|
+
});
|
|
371
570
|
});
|
|
@@ -248,36 +248,28 @@ describe('handle.ts.ejs (react slice)', () => {
|
|
|
248
248
|
/**
|
|
249
249
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
250
250
|
*
|
|
251
|
-
* -
|
|
252
|
-
* -
|
|
253
|
-
* - Send one or more commands via: commandSender.send({...})
|
|
254
|
-
* - Optionally return a MessageHandlerResult for SKIP or error cases.
|
|
251
|
+
* - Review the generated send call below and adjust if needed.
|
|
252
|
+
* - Add business logic (validation, conditional sends) as required.
|
|
255
253
|
*/
|
|
256
254
|
|
|
257
255
|
// Event (BookingRequested) fields: bookingId: string, hostId: string, message: string
|
|
258
256
|
|
|
259
257
|
// Command (NotifyHost) fields: hostId: string, notificationType: string, priority: string, channels: string[], message: string, actionRequired: boolean
|
|
260
258
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// kind: 'Command',
|
|
274
|
-
// data: {
|
|
275
|
-
// // Map event fields to command fields here
|
|
276
|
-
// // e.g., userId: event.data.userId,
|
|
277
|
-
// },
|
|
278
|
-
// });
|
|
259
|
+
await commandSender.send({
|
|
260
|
+
type: 'NotifyHost',
|
|
261
|
+
kind: 'Command',
|
|
262
|
+
data: {
|
|
263
|
+
hostId: event.data.hostId,
|
|
264
|
+
notificationType: undefined, // TODO: source unknown
|
|
265
|
+
priority: undefined, // TODO: source unknown
|
|
266
|
+
channels: undefined, // TODO: source unknown
|
|
267
|
+
message: event.data.message,
|
|
268
|
+
actionRequired: undefined, // TODO: source unknown
|
|
269
|
+
},
|
|
270
|
+
});
|
|
279
271
|
|
|
280
|
-
|
|
272
|
+
return;
|
|
281
273
|
},
|
|
282
274
|
});
|
|
283
275
|
"
|
|
@@ -560,6 +552,7 @@ describe('handle.ts.ejs (react slice)', () => {
|
|
|
560
552
|
expect(reactFile?.contents).toContain("canHandle: ['PaymentProcessed']");
|
|
561
553
|
expect(reactFile?.contents).toContain("from '../../order-management/process-payment/events'");
|
|
562
554
|
expect(reactFile?.contents).not.toContain('ReactToPaymentProcessed');
|
|
563
|
-
expect(reactFile?.contents).toContain('
|
|
555
|
+
expect(reactFile?.contents).not.toContain('readable from database');
|
|
556
|
+
expect(reactFile?.contents).not.toContain('aggregateStream');
|
|
564
557
|
});
|
|
565
558
|
});
|
|
@@ -115,6 +115,26 @@ describe('<%= ruleDescription %>', () => {
|
|
|
115
115
|
`should send ${thenCommands.map(c => c.commandRef).join(', ')} when ${exampleEvent.eventRef} is received`;
|
|
116
116
|
%>
|
|
117
117
|
it('<%= description %>', async () => {
|
|
118
|
+
<%
|
|
119
|
+
const givenStates = testCase.given.filter(g =>
|
|
120
|
+
messages.some(m => m.type === 'state' && m.name === g.eventRef)
|
|
121
|
+
);
|
|
122
|
+
for (const gs of givenStates) {
|
|
123
|
+
const stateSchema = states.find(s => s.type === gs.eventRef);
|
|
124
|
+
const eventDef = messages.find(m => m.name === exampleEvent.eventRef);
|
|
125
|
+
const stateFieldNames = (stateSchema?.fields || []).map(f => f.name);
|
|
126
|
+
const eventFieldNames = (eventDef?.fields || []).map(f => f.name);
|
|
127
|
+
const linkingField = stateFieldNames.find(f => eventFieldNames.includes(f));
|
|
128
|
+
if (linkingField) {
|
|
129
|
+
const linkingValue = gs.exampleData[linkingField];
|
|
130
|
+
-%>
|
|
131
|
+
await eventStore.appendToStream('<%= gs.eventRef %>-<%= linkingValue %>', [{
|
|
132
|
+
type: '<%= gs.eventRef %>Initialized',
|
|
133
|
+
data: <%- formatDataObject(gs.exampleData, stateSchema) %>
|
|
134
|
+
}]);
|
|
135
|
+
<% }
|
|
136
|
+
}
|
|
137
|
+
-%>
|
|
118
138
|
await given([])
|
|
119
139
|
.when({
|
|
120
140
|
type: '<%= exampleEvent.eventRef %>',
|
|
@@ -42,17 +42,15 @@ eachMessage: async (event, context): Promise<MessageHandlerResult> => {
|
|
|
42
42
|
/**
|
|
43
43
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
44
44
|
*
|
|
45
|
-
* -
|
|
46
|
-
* -
|
|
47
|
-
* - Send one or more commands via: commandSender.send({...})
|
|
48
|
-
* - Optionally return a MessageHandlerResult for SKIP or error cases.
|
|
49
|
-
<% if (states.length > 0) { -%>
|
|
50
|
-
* - Context state available: <%= states.map(s => pascalCase(s.type)).join(', ') %> (readable from database)
|
|
51
|
-
<% } -%>
|
|
45
|
+
* - Review the generated send call below and adjust if needed.
|
|
46
|
+
* - Add business logic (validation, conditional sends) as required.
|
|
52
47
|
*/
|
|
53
48
|
<%
|
|
54
49
|
const eventDef = messages.find(m => m.name === eventType);
|
|
55
50
|
const commandDef = messages.find(m => m.name === commandType && m.type === 'command');
|
|
51
|
+
const commandFields = (commandDef?.fields || []);
|
|
52
|
+
const eventFieldSet = new Set((eventDef?.fields || []).map(f => f.name));
|
|
53
|
+
const stateFieldSources = {};
|
|
56
54
|
-%>
|
|
57
55
|
<% if (eventDef?.fields?.length) { %>
|
|
58
56
|
// Event (<%= eventType %>) fields: <%= eventDef.fields.map(f => f.name + ': ' + (f.tsType || f.type)).join(', ') %>
|
|
@@ -60,26 +58,58 @@ const commandDef = messages.find(m => m.name === commandType && m.type === 'comm
|
|
|
60
58
|
<% if (commandDef?.fields?.length) { %>
|
|
61
59
|
// Command (<%= commandType %>) fields: <%= commandDef.fields.map(f => f.name + ': ' + (f.tsType || f.type)).join(', ') %>
|
|
62
60
|
<% } -%>
|
|
61
|
+
<% if (states.length > 0) {
|
|
62
|
+
let hasAggregateStream = false;
|
|
63
|
+
for (const state of states) {
|
|
64
|
+
const stateFieldNames = state.fields.map(f => f.name);
|
|
65
|
+
const eventFieldNames = (eventDef?.fields || []).map(f => f.name);
|
|
66
|
+
const linkingField = stateFieldNames.find(f => eventFieldNames.includes(f));
|
|
67
|
+
if (linkingField) {
|
|
68
|
+
hasAggregateStream = true;
|
|
69
|
+
const varName = camelCase(state.type);
|
|
70
|
+
for (const f of state.fields) {
|
|
71
|
+
if (!stateFieldSources[f.name]) stateFieldSources[f.name] = varName;
|
|
72
|
+
}
|
|
73
|
+
-%>
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
const { state: <%= varName %> } = await eventStore.aggregateStream(
|
|
76
|
+
'<%= state.type %>-' + event.data.<%= linkingField %>,
|
|
77
|
+
{
|
|
78
|
+
evolve: (currentState, evt) => ({ ...currentState, ...evt.data }),
|
|
79
|
+
initialState: () => ({}),
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
// <%= state.type %> fields: <%= state.fields.map(f => f.name).join(', ') %>
|
|
83
|
+
|
|
84
|
+
<% }
|
|
85
|
+
}
|
|
86
|
+
if (!hasAggregateStream) {
|
|
87
|
+
-%>
|
|
65
88
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// type: 'SKIP',
|
|
70
|
-
// reason: 'Condition not met',
|
|
71
|
-
// };
|
|
72
|
-
// }
|
|
89
|
+
<% }
|
|
90
|
+
} else {
|
|
91
|
+
-%>
|
|
73
92
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
<% } -%>
|
|
94
|
+
await commandSender.send({
|
|
95
|
+
type: '<%= commandType %>',
|
|
96
|
+
kind: 'Command',
|
|
97
|
+
data: {
|
|
98
|
+
<% for (const field of commandFields) {
|
|
99
|
+
const fieldName = field.name;
|
|
100
|
+
if (eventFieldSet.has(fieldName)) {
|
|
101
|
+
-%>
|
|
102
|
+
<%= fieldName %>: event.data.<%= fieldName %>,
|
|
103
|
+
<% } else if (stateFieldSources[fieldName]) { -%>
|
|
104
|
+
<%= fieldName %>: <%= stateFieldSources[fieldName] %>.<%= fieldName %>,
|
|
105
|
+
<% } else { -%>
|
|
106
|
+
<%= fieldName %>: undefined, // TODO: source unknown
|
|
107
|
+
<% }
|
|
108
|
+
}
|
|
109
|
+
-%>
|
|
110
|
+
},
|
|
111
|
+
});
|
|
82
112
|
|
|
83
|
-
|
|
113
|
+
return;
|
|
84
114
|
},
|
|
85
115
|
});
|
|
@@ -190,6 +190,11 @@ describe('react.ts.ejs', () => {
|
|
|
190
190
|
// Should contain field-type hints for event and command
|
|
191
191
|
expect(reactFile?.contents).toContain('// Event (OrderPlaced) fields:');
|
|
192
192
|
expect(reactFile?.contents).toContain('// Command (SendConfirmation) fields:');
|
|
193
|
+
|
|
194
|
+
expect(reactFile?.contents).toContain('commandSender.send({');
|
|
195
|
+
expect(reactFile?.contents).toContain('event.data.orderId');
|
|
196
|
+
expect(reactFile?.contents).toContain('undefined, // TODO: source unknown');
|
|
197
|
+
expect(reactFile?.contents).not.toContain('throw new IllegalStateError');
|
|
193
198
|
});
|
|
194
199
|
|
|
195
200
|
it('should skip react slice with empty examples without throwing', async () => {
|
|
@@ -410,4 +415,151 @@ export type BarberNotified = Event<
|
|
|
410
415
|
"
|
|
411
416
|
`);
|
|
412
417
|
});
|
|
418
|
+
|
|
419
|
+
it('should generate aggregateStream when Given states exist', async () => {
|
|
420
|
+
const spec: SpecsSchema = {
|
|
421
|
+
variant: 'specs',
|
|
422
|
+
narratives: [
|
|
423
|
+
{
|
|
424
|
+
name: 'barbershop flow',
|
|
425
|
+
slices: [
|
|
426
|
+
{
|
|
427
|
+
type: 'command',
|
|
428
|
+
name: 'cancel appointment',
|
|
429
|
+
client: { specs: [] },
|
|
430
|
+
server: {
|
|
431
|
+
description: '',
|
|
432
|
+
specs: [
|
|
433
|
+
{
|
|
434
|
+
type: 'gherkin',
|
|
435
|
+
feature: 'Cancel appointment command',
|
|
436
|
+
rules: [
|
|
437
|
+
{
|
|
438
|
+
name: 'Should cancel appointment',
|
|
439
|
+
examples: [
|
|
440
|
+
{
|
|
441
|
+
name: 'Appointment cancelled',
|
|
442
|
+
steps: [
|
|
443
|
+
{
|
|
444
|
+
keyword: 'When',
|
|
445
|
+
text: 'CancelAppointment',
|
|
446
|
+
docString: { appointmentId: 'apt_001', barberId: 'barber_001' },
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
keyword: 'Then',
|
|
450
|
+
text: 'AppointmentCancelled',
|
|
451
|
+
docString: { appointmentId: 'apt_001', barberId: 'barber_001' },
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
},
|
|
459
|
+
],
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
type: 'react',
|
|
464
|
+
name: 'notify barber of cancellation',
|
|
465
|
+
server: {
|
|
466
|
+
description: 'Notifies barber when appointment is cancelled',
|
|
467
|
+
data: {
|
|
468
|
+
items: [
|
|
469
|
+
{
|
|
470
|
+
target: { type: 'Command', name: 'NotifyBarber' },
|
|
471
|
+
destination: { type: 'stream', pattern: 'barber-${barberId}' },
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
},
|
|
475
|
+
specs: [
|
|
476
|
+
{
|
|
477
|
+
type: 'gherkin',
|
|
478
|
+
feature: 'Notify barber of cancellation reaction',
|
|
479
|
+
rules: [
|
|
480
|
+
{
|
|
481
|
+
name: 'Should notify barber on cancellation',
|
|
482
|
+
examples: [
|
|
483
|
+
{
|
|
484
|
+
name: 'Barber notified after cancellation',
|
|
485
|
+
steps: [
|
|
486
|
+
{
|
|
487
|
+
keyword: 'Given',
|
|
488
|
+
text: 'Appointment',
|
|
489
|
+
docString: { barberId: 'barber_001', clientName: 'John' },
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
keyword: 'When',
|
|
493
|
+
text: 'AppointmentCancelled',
|
|
494
|
+
docString: { appointmentId: 'apt_001', barberId: 'barber_001' },
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
keyword: 'Then',
|
|
498
|
+
text: 'NotifyBarber',
|
|
499
|
+
docString: { barberId: 'barber_001', message: 'Appointment cancelled' },
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
},
|
|
512
|
+
],
|
|
513
|
+
messages: [
|
|
514
|
+
{
|
|
515
|
+
type: 'command',
|
|
516
|
+
name: 'CancelAppointment',
|
|
517
|
+
fields: [
|
|
518
|
+
{ name: 'appointmentId', type: 'string', required: true },
|
|
519
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
520
|
+
],
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
type: 'event',
|
|
524
|
+
name: 'AppointmentCancelled',
|
|
525
|
+
source: 'internal',
|
|
526
|
+
fields: [
|
|
527
|
+
{ name: 'appointmentId', type: 'string', required: true },
|
|
528
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
529
|
+
],
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
type: 'command',
|
|
533
|
+
name: 'NotifyBarber',
|
|
534
|
+
fields: [
|
|
535
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
536
|
+
{ name: 'message', type: 'string', required: true },
|
|
537
|
+
],
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
type: 'state',
|
|
541
|
+
name: 'Appointment',
|
|
542
|
+
fields: [
|
|
543
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
544
|
+
{ name: 'clientName', type: 'string', required: true },
|
|
545
|
+
],
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
551
|
+
const reactFile = plans.find((p) => p.outputPath.endsWith('notify-barber-of-cancellation/react.ts'));
|
|
552
|
+
|
|
553
|
+
expect(reactFile?.contents).toContain('aggregateStream');
|
|
554
|
+
expect(reactFile?.contents).toContain('event.data.barberId');
|
|
555
|
+
expect(reactFile?.contents).toContain('appointment');
|
|
556
|
+
expect(reactFile?.contents).not.toContain('readable from database');
|
|
557
|
+
|
|
558
|
+
expect(reactFile?.contents).toContain('commandSender.send({');
|
|
559
|
+
expect(reactFile?.contents).toContain("type: 'NotifyBarber'");
|
|
560
|
+
expect(reactFile?.contents).toContain('event.data.barberId');
|
|
561
|
+
expect(reactFile?.contents).toContain('undefined, // TODO: source unknown');
|
|
562
|
+
expect(reactFile?.contents).not.toContain('throw new IllegalStateError');
|
|
563
|
+
expect(reactFile?.contents).not.toContain('// Example:');
|
|
564
|
+
});
|
|
413
565
|
});
|
package/ketchup-plan.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
# Ketchup Plan: Fix
|
|
1
|
+
# Ketchup Plan: Fix react slice scaffold ignoring Given states
|
|
2
2
|
|
|
3
3
|
## TODO
|
|
4
4
|
|
|
5
5
|
## DONE
|
|
6
6
|
|
|
7
|
-
- [x] Burst
|
|
7
|
+
- [x] Burst 3: Generate real commandSender.send() in react.ts.ejs with field-source mapping (89610315)
|
|
8
|
+
|
|
9
|
+
- [x] Burst 1: Fix react.specs.ts.ejs — seed event store with Given state data via appendToStream (0afa65ca)
|
|
10
|
+
- [x] Burst 2: Fix react.ts.ejs — generate aggregateStream pattern when Given states exist (8380e5a2)
|
package/package.json
CHANGED
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"uuid": "^11.0.0",
|
|
33
33
|
"web-streams-polyfill": "^4.1.0",
|
|
34
34
|
"zod": "^3.22.4",
|
|
35
|
-
"@auto-engineer/
|
|
36
|
-
"@auto-engineer/
|
|
35
|
+
"@auto-engineer/message-bus": "1.71.0",
|
|
36
|
+
"@auto-engineer/narrative": "1.71.0"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"typescript": "^5.8.3",
|
|
45
45
|
"vitest": "^3.2.4",
|
|
46
46
|
"tsx": "^4.19.2",
|
|
47
|
-
"@auto-engineer/cli": "1.
|
|
47
|
+
"@auto-engineer/cli": "1.71.0"
|
|
48
48
|
},
|
|
49
|
-
"version": "1.
|
|
49
|
+
"version": "1.71.0",
|
|
50
50
|
"scripts": {
|
|
51
51
|
"generate:server": "tsx src/cli/index.ts",
|
|
52
52
|
"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",
|
|
@@ -368,4 +368,203 @@ describe('react.specs.ts.ejs (react slice)', () => {
|
|
|
368
368
|
expect(specFile?.contents).not.toContain("tipAmount: '5.00'");
|
|
369
369
|
expect(specFile?.contents).not.toContain('tipAmount: "5.00"');
|
|
370
370
|
});
|
|
371
|
+
|
|
372
|
+
it('should seed event store with Given state data via appendToStream', async () => {
|
|
373
|
+
const spec: SpecsSchema = {
|
|
374
|
+
variant: 'specs',
|
|
375
|
+
narratives: [
|
|
376
|
+
{
|
|
377
|
+
name: 'barbershop flow',
|
|
378
|
+
slices: [
|
|
379
|
+
{
|
|
380
|
+
type: 'command',
|
|
381
|
+
name: 'cancel appointment',
|
|
382
|
+
client: { specs: [] },
|
|
383
|
+
server: {
|
|
384
|
+
description: '',
|
|
385
|
+
specs: [
|
|
386
|
+
{
|
|
387
|
+
type: 'gherkin',
|
|
388
|
+
feature: 'Cancel appointment command',
|
|
389
|
+
rules: [
|
|
390
|
+
{
|
|
391
|
+
name: 'Should cancel appointment',
|
|
392
|
+
examples: [
|
|
393
|
+
{
|
|
394
|
+
name: 'Appointment cancelled',
|
|
395
|
+
steps: [
|
|
396
|
+
{
|
|
397
|
+
keyword: 'When',
|
|
398
|
+
text: 'CancelAppointment',
|
|
399
|
+
docString: { appointmentId: 'apt_001', barberId: 'barber_001' },
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
keyword: 'Then',
|
|
403
|
+
text: 'AppointmentCancelled',
|
|
404
|
+
docString: { appointmentId: 'apt_001', barberId: 'barber_001' },
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
type: 'react',
|
|
417
|
+
name: 'notify barber of cancellation',
|
|
418
|
+
server: {
|
|
419
|
+
description: 'Notifies barber when appointment is cancelled',
|
|
420
|
+
data: {
|
|
421
|
+
items: [
|
|
422
|
+
{
|
|
423
|
+
target: { type: 'Command', name: 'NotifyBarber' },
|
|
424
|
+
destination: { type: 'stream', pattern: 'barber-${barberId}' },
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
},
|
|
428
|
+
specs: [
|
|
429
|
+
{
|
|
430
|
+
type: 'gherkin',
|
|
431
|
+
feature: 'Notify barber of cancellation reaction',
|
|
432
|
+
rules: [
|
|
433
|
+
{
|
|
434
|
+
name: 'Should notify barber on cancellation',
|
|
435
|
+
examples: [
|
|
436
|
+
{
|
|
437
|
+
name: 'Barber notified after cancellation',
|
|
438
|
+
steps: [
|
|
439
|
+
{
|
|
440
|
+
keyword: 'Given',
|
|
441
|
+
text: 'Appointment',
|
|
442
|
+
docString: { barberId: 'barber_001', clientName: 'John' },
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
keyword: 'When',
|
|
446
|
+
text: 'AppointmentCancelled',
|
|
447
|
+
docString: { appointmentId: 'apt_001', barberId: 'barber_001' },
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
keyword: 'Then',
|
|
451
|
+
text: 'NotifyBarber',
|
|
452
|
+
docString: { barberId: 'barber_001', message: 'Appointment cancelled' },
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
},
|
|
460
|
+
],
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
messages: [
|
|
467
|
+
{
|
|
468
|
+
type: 'command',
|
|
469
|
+
name: 'CancelAppointment',
|
|
470
|
+
fields: [
|
|
471
|
+
{ name: 'appointmentId', type: 'string', required: true },
|
|
472
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
473
|
+
],
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
type: 'event',
|
|
477
|
+
name: 'AppointmentCancelled',
|
|
478
|
+
source: 'internal',
|
|
479
|
+
fields: [
|
|
480
|
+
{ name: 'appointmentId', type: 'string', required: true },
|
|
481
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
482
|
+
],
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
type: 'command',
|
|
486
|
+
name: 'NotifyBarber',
|
|
487
|
+
fields: [
|
|
488
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
489
|
+
{ name: 'message', type: 'string', required: true },
|
|
490
|
+
],
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
type: 'state',
|
|
494
|
+
name: 'Appointment',
|
|
495
|
+
fields: [
|
|
496
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
497
|
+
{ name: 'clientName', type: 'string', required: true },
|
|
498
|
+
],
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
504
|
+
|
|
505
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('notify-barber-of-cancellation/react.specs.ts'));
|
|
506
|
+
expect(specFile?.contents).toMatchInlineSnapshot(`
|
|
507
|
+
"import { describe, it, beforeEach } from 'vitest';
|
|
508
|
+
import 'reflect-metadata';
|
|
509
|
+
import { getInMemoryEventStore, type InMemoryEventStore, type CommandSender } from '@event-driven-io/emmett';
|
|
510
|
+
import { type ReactorContext, ReactorSpecification } from '../../../shared';
|
|
511
|
+
import { react } from './react';
|
|
512
|
+
import type { AppointmentCancelled } from '../cancel-appointment/events';
|
|
513
|
+
import type { NotifyBarber } from './commands';
|
|
514
|
+
|
|
515
|
+
type ReactorEvent = AppointmentCancelled;
|
|
516
|
+
type ReactorCommand = NotifyBarber;
|
|
517
|
+
|
|
518
|
+
describe('Should notify barber on cancellation', () => {
|
|
519
|
+
let eventStore: InMemoryEventStore;
|
|
520
|
+
let given: ReactorSpecification<ReactorEvent, ReactorCommand, ReactorContext>;
|
|
521
|
+
let messageBus: CommandSender;
|
|
522
|
+
|
|
523
|
+
beforeEach(() => {
|
|
524
|
+
eventStore = getInMemoryEventStore({});
|
|
525
|
+
given = ReactorSpecification.for<ReactorEvent, ReactorCommand, ReactorContext>(
|
|
526
|
+
() => react({ eventStore, commandSender: messageBus, database: eventStore.database }),
|
|
527
|
+
(commandSender) => {
|
|
528
|
+
messageBus = commandSender;
|
|
529
|
+
return {
|
|
530
|
+
eventStore,
|
|
531
|
+
commandSender,
|
|
532
|
+
database: eventStore.database,
|
|
533
|
+
};
|
|
534
|
+
},
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('Barber notified after cancellation', async () => {
|
|
539
|
+
await eventStore.appendToStream('Appointment-barber_001', [
|
|
540
|
+
{
|
|
541
|
+
type: 'AppointmentInitialized',
|
|
542
|
+
data: {
|
|
543
|
+
barberId: 'barber_001',
|
|
544
|
+
clientName: 'John',
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
]);
|
|
548
|
+
await given([])
|
|
549
|
+
.when({
|
|
550
|
+
type: 'AppointmentCancelled',
|
|
551
|
+
data: {
|
|
552
|
+
appointmentId: 'apt_001',
|
|
553
|
+
barberId: 'barber_001',
|
|
554
|
+
},
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
.then({
|
|
558
|
+
type: 'NotifyBarber',
|
|
559
|
+
kind: 'Command',
|
|
560
|
+
data: {
|
|
561
|
+
barberId: 'barber_001',
|
|
562
|
+
message: 'Appointment cancelled',
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
"
|
|
568
|
+
`);
|
|
569
|
+
});
|
|
371
570
|
});
|
|
@@ -248,36 +248,28 @@ describe('handle.ts.ejs (react slice)', () => {
|
|
|
248
248
|
/**
|
|
249
249
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
250
250
|
*
|
|
251
|
-
* -
|
|
252
|
-
* -
|
|
253
|
-
* - Send one or more commands via: commandSender.send({...})
|
|
254
|
-
* - Optionally return a MessageHandlerResult for SKIP or error cases.
|
|
251
|
+
* - Review the generated send call below and adjust if needed.
|
|
252
|
+
* - Add business logic (validation, conditional sends) as required.
|
|
255
253
|
*/
|
|
256
254
|
|
|
257
255
|
// Event (BookingRequested) fields: bookingId: string, hostId: string, message: string
|
|
258
256
|
|
|
259
257
|
// Command (NotifyHost) fields: hostId: string, notificationType: string, priority: string, channels: string[], message: string, actionRequired: boolean
|
|
260
258
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// kind: 'Command',
|
|
274
|
-
// data: {
|
|
275
|
-
// // Map event fields to command fields here
|
|
276
|
-
// // e.g., userId: event.data.userId,
|
|
277
|
-
// },
|
|
278
|
-
// });
|
|
259
|
+
await commandSender.send({
|
|
260
|
+
type: 'NotifyHost',
|
|
261
|
+
kind: 'Command',
|
|
262
|
+
data: {
|
|
263
|
+
hostId: event.data.hostId,
|
|
264
|
+
notificationType: undefined, // TODO: source unknown
|
|
265
|
+
priority: undefined, // TODO: source unknown
|
|
266
|
+
channels: undefined, // TODO: source unknown
|
|
267
|
+
message: event.data.message,
|
|
268
|
+
actionRequired: undefined, // TODO: source unknown
|
|
269
|
+
},
|
|
270
|
+
});
|
|
279
271
|
|
|
280
|
-
|
|
272
|
+
return;
|
|
281
273
|
},
|
|
282
274
|
});
|
|
283
275
|
"
|
|
@@ -560,6 +552,7 @@ describe('handle.ts.ejs (react slice)', () => {
|
|
|
560
552
|
expect(reactFile?.contents).toContain("canHandle: ['PaymentProcessed']");
|
|
561
553
|
expect(reactFile?.contents).toContain("from '../../order-management/process-payment/events'");
|
|
562
554
|
expect(reactFile?.contents).not.toContain('ReactToPaymentProcessed');
|
|
563
|
-
expect(reactFile?.contents).toContain('
|
|
555
|
+
expect(reactFile?.contents).not.toContain('readable from database');
|
|
556
|
+
expect(reactFile?.contents).not.toContain('aggregateStream');
|
|
564
557
|
});
|
|
565
558
|
});
|
|
@@ -115,6 +115,26 @@ describe('<%= ruleDescription %>', () => {
|
|
|
115
115
|
`should send ${thenCommands.map(c => c.commandRef).join(', ')} when ${exampleEvent.eventRef} is received`;
|
|
116
116
|
%>
|
|
117
117
|
it('<%= description %>', async () => {
|
|
118
|
+
<%
|
|
119
|
+
const givenStates = testCase.given.filter(g =>
|
|
120
|
+
messages.some(m => m.type === 'state' && m.name === g.eventRef)
|
|
121
|
+
);
|
|
122
|
+
for (const gs of givenStates) {
|
|
123
|
+
const stateSchema = states.find(s => s.type === gs.eventRef);
|
|
124
|
+
const eventDef = messages.find(m => m.name === exampleEvent.eventRef);
|
|
125
|
+
const stateFieldNames = (stateSchema?.fields || []).map(f => f.name);
|
|
126
|
+
const eventFieldNames = (eventDef?.fields || []).map(f => f.name);
|
|
127
|
+
const linkingField = stateFieldNames.find(f => eventFieldNames.includes(f));
|
|
128
|
+
if (linkingField) {
|
|
129
|
+
const linkingValue = gs.exampleData[linkingField];
|
|
130
|
+
-%>
|
|
131
|
+
await eventStore.appendToStream('<%= gs.eventRef %>-<%= linkingValue %>', [{
|
|
132
|
+
type: '<%= gs.eventRef %>Initialized',
|
|
133
|
+
data: <%- formatDataObject(gs.exampleData, stateSchema) %>
|
|
134
|
+
}]);
|
|
135
|
+
<% }
|
|
136
|
+
}
|
|
137
|
+
-%>
|
|
118
138
|
await given([])
|
|
119
139
|
.when({
|
|
120
140
|
type: '<%= exampleEvent.eventRef %>',
|
|
@@ -42,17 +42,15 @@ eachMessage: async (event, context): Promise<MessageHandlerResult> => {
|
|
|
42
42
|
/**
|
|
43
43
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
44
44
|
*
|
|
45
|
-
* -
|
|
46
|
-
* -
|
|
47
|
-
* - Send one or more commands via: commandSender.send({...})
|
|
48
|
-
* - Optionally return a MessageHandlerResult for SKIP or error cases.
|
|
49
|
-
<% if (states.length > 0) { -%>
|
|
50
|
-
* - Context state available: <%= states.map(s => pascalCase(s.type)).join(', ') %> (readable from database)
|
|
51
|
-
<% } -%>
|
|
45
|
+
* - Review the generated send call below and adjust if needed.
|
|
46
|
+
* - Add business logic (validation, conditional sends) as required.
|
|
52
47
|
*/
|
|
53
48
|
<%
|
|
54
49
|
const eventDef = messages.find(m => m.name === eventType);
|
|
55
50
|
const commandDef = messages.find(m => m.name === commandType && m.type === 'command');
|
|
51
|
+
const commandFields = (commandDef?.fields || []);
|
|
52
|
+
const eventFieldSet = new Set((eventDef?.fields || []).map(f => f.name));
|
|
53
|
+
const stateFieldSources = {};
|
|
56
54
|
-%>
|
|
57
55
|
<% if (eventDef?.fields?.length) { %>
|
|
58
56
|
// Event (<%= eventType %>) fields: <%= eventDef.fields.map(f => f.name + ': ' + (f.tsType || f.type)).join(', ') %>
|
|
@@ -60,26 +58,58 @@ const commandDef = messages.find(m => m.name === commandType && m.type === 'comm
|
|
|
60
58
|
<% if (commandDef?.fields?.length) { %>
|
|
61
59
|
// Command (<%= commandType %>) fields: <%= commandDef.fields.map(f => f.name + ': ' + (f.tsType || f.type)).join(', ') %>
|
|
62
60
|
<% } -%>
|
|
61
|
+
<% if (states.length > 0) {
|
|
62
|
+
let hasAggregateStream = false;
|
|
63
|
+
for (const state of states) {
|
|
64
|
+
const stateFieldNames = state.fields.map(f => f.name);
|
|
65
|
+
const eventFieldNames = (eventDef?.fields || []).map(f => f.name);
|
|
66
|
+
const linkingField = stateFieldNames.find(f => eventFieldNames.includes(f));
|
|
67
|
+
if (linkingField) {
|
|
68
|
+
hasAggregateStream = true;
|
|
69
|
+
const varName = camelCase(state.type);
|
|
70
|
+
for (const f of state.fields) {
|
|
71
|
+
if (!stateFieldSources[f.name]) stateFieldSources[f.name] = varName;
|
|
72
|
+
}
|
|
73
|
+
-%>
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
const { state: <%= varName %> } = await eventStore.aggregateStream(
|
|
76
|
+
'<%= state.type %>-' + event.data.<%= linkingField %>,
|
|
77
|
+
{
|
|
78
|
+
evolve: (currentState, evt) => ({ ...currentState, ...evt.data }),
|
|
79
|
+
initialState: () => ({}),
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
// <%= state.type %> fields: <%= state.fields.map(f => f.name).join(', ') %>
|
|
83
|
+
|
|
84
|
+
<% }
|
|
85
|
+
}
|
|
86
|
+
if (!hasAggregateStream) {
|
|
87
|
+
-%>
|
|
65
88
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// type: 'SKIP',
|
|
70
|
-
// reason: 'Condition not met',
|
|
71
|
-
// };
|
|
72
|
-
// }
|
|
89
|
+
<% }
|
|
90
|
+
} else {
|
|
91
|
+
-%>
|
|
73
92
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
<% } -%>
|
|
94
|
+
await commandSender.send({
|
|
95
|
+
type: '<%= commandType %>',
|
|
96
|
+
kind: 'Command',
|
|
97
|
+
data: {
|
|
98
|
+
<% for (const field of commandFields) {
|
|
99
|
+
const fieldName = field.name;
|
|
100
|
+
if (eventFieldSet.has(fieldName)) {
|
|
101
|
+
-%>
|
|
102
|
+
<%= fieldName %>: event.data.<%= fieldName %>,
|
|
103
|
+
<% } else if (stateFieldSources[fieldName]) { -%>
|
|
104
|
+
<%= fieldName %>: <%= stateFieldSources[fieldName] %>.<%= fieldName %>,
|
|
105
|
+
<% } else { -%>
|
|
106
|
+
<%= fieldName %>: undefined, // TODO: source unknown
|
|
107
|
+
<% }
|
|
108
|
+
}
|
|
109
|
+
-%>
|
|
110
|
+
},
|
|
111
|
+
});
|
|
82
112
|
|
|
83
|
-
|
|
113
|
+
return;
|
|
84
114
|
},
|
|
85
115
|
});
|
|
@@ -190,6 +190,11 @@ describe('react.ts.ejs', () => {
|
|
|
190
190
|
// Should contain field-type hints for event and command
|
|
191
191
|
expect(reactFile?.contents).toContain('// Event (OrderPlaced) fields:');
|
|
192
192
|
expect(reactFile?.contents).toContain('// Command (SendConfirmation) fields:');
|
|
193
|
+
|
|
194
|
+
expect(reactFile?.contents).toContain('commandSender.send({');
|
|
195
|
+
expect(reactFile?.contents).toContain('event.data.orderId');
|
|
196
|
+
expect(reactFile?.contents).toContain('undefined, // TODO: source unknown');
|
|
197
|
+
expect(reactFile?.contents).not.toContain('throw new IllegalStateError');
|
|
193
198
|
});
|
|
194
199
|
|
|
195
200
|
it('should skip react slice with empty examples without throwing', async () => {
|
|
@@ -410,4 +415,151 @@ export type BarberNotified = Event<
|
|
|
410
415
|
"
|
|
411
416
|
`);
|
|
412
417
|
});
|
|
418
|
+
|
|
419
|
+
it('should generate aggregateStream when Given states exist', async () => {
|
|
420
|
+
const spec: SpecsSchema = {
|
|
421
|
+
variant: 'specs',
|
|
422
|
+
narratives: [
|
|
423
|
+
{
|
|
424
|
+
name: 'barbershop flow',
|
|
425
|
+
slices: [
|
|
426
|
+
{
|
|
427
|
+
type: 'command',
|
|
428
|
+
name: 'cancel appointment',
|
|
429
|
+
client: { specs: [] },
|
|
430
|
+
server: {
|
|
431
|
+
description: '',
|
|
432
|
+
specs: [
|
|
433
|
+
{
|
|
434
|
+
type: 'gherkin',
|
|
435
|
+
feature: 'Cancel appointment command',
|
|
436
|
+
rules: [
|
|
437
|
+
{
|
|
438
|
+
name: 'Should cancel appointment',
|
|
439
|
+
examples: [
|
|
440
|
+
{
|
|
441
|
+
name: 'Appointment cancelled',
|
|
442
|
+
steps: [
|
|
443
|
+
{
|
|
444
|
+
keyword: 'When',
|
|
445
|
+
text: 'CancelAppointment',
|
|
446
|
+
docString: { appointmentId: 'apt_001', barberId: 'barber_001' },
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
keyword: 'Then',
|
|
450
|
+
text: 'AppointmentCancelled',
|
|
451
|
+
docString: { appointmentId: 'apt_001', barberId: 'barber_001' },
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
},
|
|
459
|
+
],
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
type: 'react',
|
|
464
|
+
name: 'notify barber of cancellation',
|
|
465
|
+
server: {
|
|
466
|
+
description: 'Notifies barber when appointment is cancelled',
|
|
467
|
+
data: {
|
|
468
|
+
items: [
|
|
469
|
+
{
|
|
470
|
+
target: { type: 'Command', name: 'NotifyBarber' },
|
|
471
|
+
destination: { type: 'stream', pattern: 'barber-${barberId}' },
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
},
|
|
475
|
+
specs: [
|
|
476
|
+
{
|
|
477
|
+
type: 'gherkin',
|
|
478
|
+
feature: 'Notify barber of cancellation reaction',
|
|
479
|
+
rules: [
|
|
480
|
+
{
|
|
481
|
+
name: 'Should notify barber on cancellation',
|
|
482
|
+
examples: [
|
|
483
|
+
{
|
|
484
|
+
name: 'Barber notified after cancellation',
|
|
485
|
+
steps: [
|
|
486
|
+
{
|
|
487
|
+
keyword: 'Given',
|
|
488
|
+
text: 'Appointment',
|
|
489
|
+
docString: { barberId: 'barber_001', clientName: 'John' },
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
keyword: 'When',
|
|
493
|
+
text: 'AppointmentCancelled',
|
|
494
|
+
docString: { appointmentId: 'apt_001', barberId: 'barber_001' },
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
keyword: 'Then',
|
|
498
|
+
text: 'NotifyBarber',
|
|
499
|
+
docString: { barberId: 'barber_001', message: 'Appointment cancelled' },
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
},
|
|
512
|
+
],
|
|
513
|
+
messages: [
|
|
514
|
+
{
|
|
515
|
+
type: 'command',
|
|
516
|
+
name: 'CancelAppointment',
|
|
517
|
+
fields: [
|
|
518
|
+
{ name: 'appointmentId', type: 'string', required: true },
|
|
519
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
520
|
+
],
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
type: 'event',
|
|
524
|
+
name: 'AppointmentCancelled',
|
|
525
|
+
source: 'internal',
|
|
526
|
+
fields: [
|
|
527
|
+
{ name: 'appointmentId', type: 'string', required: true },
|
|
528
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
529
|
+
],
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
type: 'command',
|
|
533
|
+
name: 'NotifyBarber',
|
|
534
|
+
fields: [
|
|
535
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
536
|
+
{ name: 'message', type: 'string', required: true },
|
|
537
|
+
],
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
type: 'state',
|
|
541
|
+
name: 'Appointment',
|
|
542
|
+
fields: [
|
|
543
|
+
{ name: 'barberId', type: 'string', required: true },
|
|
544
|
+
{ name: 'clientName', type: 'string', required: true },
|
|
545
|
+
],
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
551
|
+
const reactFile = plans.find((p) => p.outputPath.endsWith('notify-barber-of-cancellation/react.ts'));
|
|
552
|
+
|
|
553
|
+
expect(reactFile?.contents).toContain('aggregateStream');
|
|
554
|
+
expect(reactFile?.contents).toContain('event.data.barberId');
|
|
555
|
+
expect(reactFile?.contents).toContain('appointment');
|
|
556
|
+
expect(reactFile?.contents).not.toContain('readable from database');
|
|
557
|
+
|
|
558
|
+
expect(reactFile?.contents).toContain('commandSender.send({');
|
|
559
|
+
expect(reactFile?.contents).toContain("type: 'NotifyBarber'");
|
|
560
|
+
expect(reactFile?.contents).toContain('event.data.barberId');
|
|
561
|
+
expect(reactFile?.contents).toContain('undefined, // TODO: source unknown');
|
|
562
|
+
expect(reactFile?.contents).not.toContain('throw new IllegalStateError');
|
|
563
|
+
expect(reactFile?.contents).not.toContain('// Example:');
|
|
564
|
+
});
|
|
413
565
|
});
|