@auto-engineer/narrative 0.21.0 → 0.21.2

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 (44) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +9 -0
  3. package/dist/tsconfig.tsbuildinfo +1 -1
  4. package/package.json +4 -4
  5. package/.turbo/turbo-format.log +0 -4
  6. package/.turbo/turbo-lint.log +0 -4
  7. package/.turbo/turbo-test.log +0 -14
  8. package/.turbo/turbo-type-check.log +0 -5
  9. package/dist/src/commands/export-schema-runner.d.ts +0 -3
  10. package/dist/src/commands/export-schema-runner.d.ts.map +0 -1
  11. package/dist/src/commands/export-schema-runner.js +0 -53
  12. package/dist/src/commands/export-schema-runner.js.map +0 -1
  13. package/dist/src/fluent-builder.specs.d.ts +0 -2
  14. package/dist/src/fluent-builder.specs.d.ts.map +0 -1
  15. package/dist/src/fluent-builder.specs.js +0 -28
  16. package/dist/src/fluent-builder.specs.js.map +0 -1
  17. package/dist/src/getNarratives.cache.specs.d.ts +0 -2
  18. package/dist/src/getNarratives.cache.specs.d.ts.map +0 -1
  19. package/dist/src/getNarratives.cache.specs.js +0 -234
  20. package/dist/src/getNarratives.cache.specs.js.map +0 -1
  21. package/dist/src/getNarratives.specs.d.ts +0 -2
  22. package/dist/src/getNarratives.specs.d.ts.map +0 -1
  23. package/dist/src/getNarratives.specs.js +0 -1294
  24. package/dist/src/getNarratives.specs.js.map +0 -1
  25. package/dist/src/id/addAutoIds.specs.d.ts +0 -2
  26. package/dist/src/id/addAutoIds.specs.d.ts.map +0 -1
  27. package/dist/src/id/addAutoIds.specs.js +0 -265
  28. package/dist/src/id/addAutoIds.specs.js.map +0 -1
  29. package/dist/src/id/hasAllIds.specs.d.ts +0 -2
  30. package/dist/src/id/hasAllIds.specs.d.ts.map +0 -1
  31. package/dist/src/id/hasAllIds.specs.js +0 -231
  32. package/dist/src/id/hasAllIds.specs.js.map +0 -1
  33. package/dist/src/model-to-narrative.specs.d.ts +0 -2
  34. package/dist/src/model-to-narrative.specs.d.ts.map +0 -1
  35. package/dist/src/model-to-narrative.specs.js +0 -2378
  36. package/dist/src/model-to-narrative.specs.js.map +0 -1
  37. package/dist/src/narrative-context.specs.d.ts +0 -2
  38. package/dist/src/narrative-context.specs.d.ts.map +0 -1
  39. package/dist/src/narrative-context.specs.js +0 -185
  40. package/dist/src/narrative-context.specs.js.map +0 -1
  41. package/dist/src/transformers/narrative-to-model/type-inference.specs.d.ts +0 -2
  42. package/dist/src/transformers/narrative-to-model/type-inference.specs.d.ts.map +0 -1
  43. package/dist/src/transformers/narrative-to-model/type-inference.specs.js +0 -167
  44. package/dist/src/transformers/narrative-to-model/type-inference.specs.js.map +0 -1
@@ -1,2378 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import schema from './samples/seasonal-assistant.schema.json.js';
3
- import { modelToNarrative } from './transformers/model-to-narrative/index.js';
4
- describe('modelToNarrative', () => {
5
- it('should create a full flow DSL from a model', async () => {
6
- const code = await modelToNarrative(schema);
7
- expect(code).toEqual(`import {
8
- command,
9
- data,
10
- example,
11
- gql,
12
- narrative,
13
- query,
14
- react,
15
- rule,
16
- should,
17
- sink,
18
- source,
19
- specs,
20
- } from '@auto-engineer/narrative';
21
- import type { Command, Event, State } from '@auto-engineer/narrative';
22
- import { AI, ProductCatalog } from '../server/src/integrations.js';
23
- import type { Products } from '../server/src/integrations.js';
24
- type EnterShoppingCriteria = Command<
25
- 'EnterShoppingCriteria',
26
- {
27
- sessionId: string;
28
- criteria: string;
29
- }
30
- >;
31
- type ShoppingCriteriaEntered = Event<
32
- 'ShoppingCriteriaEntered',
33
- {
34
- sessionId: string;
35
- criteria: string;
36
- }
37
- >;
38
- type SuggestShoppingItems = Command<
39
- 'SuggestShoppingItems',
40
- {
41
- sessionId: string;
42
- prompt: string;
43
- }
44
- >;
45
- type ShoppingItemsSuggested = Event<
46
- 'ShoppingItemsSuggested',
47
- {
48
- sessionId: string;
49
- suggestedItems: {
50
- productId: string;
51
- name: string;
52
- quantity: number;
53
- reason: string;
54
- }[];
55
- }
56
- >;
57
- type SuggestedItems = State<
58
- 'SuggestedItems',
59
- {
60
- sessionId: string;
61
- items: {
62
- productId: string;
63
- name: string;
64
- quantity: number;
65
- reason: string;
66
- }[];
67
- }
68
- >;
69
- type AddItemsToCart = Command<
70
- 'AddItemsToCart',
71
- {
72
- sessionId: string;
73
- items: {
74
- productId: string;
75
- quantity: number;
76
- }[];
77
- }
78
- >;
79
- type ItemsAddedToCart = Event<
80
- 'ItemsAddedToCart',
81
- {
82
- sessionId: string;
83
- items: {
84
- productId: string;
85
- quantity: number;
86
- }[];
87
- }
88
- >;
89
- narrative('Seasonal Assistant', () => {
90
- command('enters shopping criteria into assistant')
91
- .client(() => {
92
- specs('Assistant Chat Interface', () => {
93
- should('allow shopper to describe their shopping needs in natural language');
94
- should('provide a text input for entering criteria');
95
- should('show examples of what to include (age, interests, budget)');
96
- should('show a button to submit the criteria');
97
- should('generate a persisted session id for a visit');
98
- should('show the header on top of the page');
99
- });
100
- })
101
- .request(
102
- gql(\`mutation EnterShoppingCriteria($input: EnterShoppingCriteriaInput!) {
103
- enterShoppingCriteria(input: $input) {
104
- success
105
- error {
106
- type
107
- message
108
- }
109
- }
110
- }\`),
111
- )
112
- .server(() => {
113
- data([sink().event('ShoppingCriteriaEntered').toStream('shopping-session-\${sessionId}')]);
114
- specs('When shopper submits criteria, a shopping session is started', () => {
115
- rule('Valid criteria should start a shopping session', () => {
116
- example('User submits shopping criteria for children')
117
- .when<EnterShoppingCriteria>({
118
- sessionId: 'shopper-123',
119
- criteria:
120
- 'I need back-to-school items for my 7-year-old daughter who loves soccer and crafts, and my 12-year-old son who is into computers and Magic the Gathering.',
121
- })
122
- .then<ShoppingCriteriaEntered>({
123
- sessionId: 'shopper-123',
124
- criteria:
125
- 'I need back-to-school items for my 7-year-old daughter who loves soccer and crafts, and my 12-year-old son who is into computers and Magic the Gathering.',
126
- });
127
- });
128
- });
129
- });
130
- react('creates a chat session').server(() => {
131
- specs('When shopping criteria are entered, request wishlist creation', () => {
132
- rule('Shopping criteria should trigger item suggestion', () => {
133
- example('Criteria entered triggers wishlist creation')
134
- .when<ShoppingCriteriaEntered>({
135
- sessionId: 'session-abc',
136
- criteria:
137
- 'I need back-to-school items for my 7-year-old daughter who loves soccer and crafts, and my 12-year-old son who is into computers and Magic the Gathering.',
138
- })
139
- .then<SuggestShoppingItems>({
140
- sessionId: 'session-abc',
141
- prompt:
142
- 'I need back-to-school items for my 7-year-old daughter who loves soccer and crafts, and my 12-year-old son who is into computers and Magic the Gathering.',
143
- });
144
- });
145
- });
146
- });
147
- command('selects items relevant to the shopping criteria').server(() => {
148
- data([
149
- sink()
150
- .command('SuggestShoppingItems')
151
- .toIntegration(AI, 'DoChat', 'command')
152
- .withState(source().state('Products').fromIntegration(ProductCatalog))
153
- .additionalInstructions(
154
- 'add the following to the DoChat: schemaName: Products, systemPrompt: use the PRODUCT_CATALOGUE_PRODUCTS MCP tool to get product data',
155
- ),
156
- sink().event('ShoppingItemsSuggested').toStream('shopping-session-\${sessionId}'),
157
- ]);
158
- specs('When chat is triggered, AI suggests items based on product catalog', () => {
159
- rule('AI should suggest relevant items from available products', () => {
160
- example('Product catalog with matching items generates suggestions')
161
- .given<Products>({
162
- products: [
163
- {
164
- productId: 'prod-soccer-ball',
165
- name: 'Super Soccer Ball',
166
- category: 'Sports',
167
- price: 10,
168
- tags: ['soccer', 'sports'],
169
- imageUrl: 'https://example.com/soccer-ball.jpg',
170
- },
171
- {
172
- productId: 'prod-craft-kit',
173
- name: 'Deluxe Craft Kit',
174
- category: 'Arts & Crafts',
175
- price: 25,
176
- tags: ['crafts', 'art', 'creative'],
177
- imageUrl: 'https://example.com/craft-kit.jpg',
178
- },
179
- {
180
- productId: 'prod-laptop-bag',
181
- name: 'Tech Laptop Backpack',
182
- category: 'School Supplies',
183
- price: 45,
184
- tags: ['computers', 'tech', 'school'],
185
- imageUrl: 'https://example.com/laptop-bag.jpg',
186
- },
187
- {
188
- productId: 'prod-mtg-starter',
189
- name: 'Magic the Gathering Starter Set',
190
- category: 'Games',
191
- price: 30,
192
- tags: ['magic', 'tcg', 'games'],
193
- imageUrl: 'https://example.com/mtg-starter.jpg',
194
- },
195
- ],
196
- })
197
- .when<SuggestShoppingItems>({
198
- sessionId: 'session-abc',
199
- prompt:
200
- 'I need back-to-school items for my 7-year-old daughter who loves soccer and crafts, and my 12-year-old son who is into computers and Magic the Gathering.',
201
- })
202
- .then<ShoppingItemsSuggested>({
203
- sessionId: 'session-abc',
204
- suggestedItems: [
205
- {
206
- productId: 'prod-soccer-ball',
207
- name: 'Super Soccer Ball',
208
- quantity: 1,
209
- reason: 'Perfect for your daughter who loves soccer',
210
- },
211
- {
212
- productId: 'prod-craft-kit',
213
- name: 'Deluxe Craft Kit',
214
- quantity: 1,
215
- reason: 'Great for creative activities and crafts',
216
- },
217
- {
218
- productId: 'prod-laptop-bag',
219
- name: 'Tech Laptop Backpack',
220
- quantity: 1,
221
- reason: "Essential for your son's school computer needs",
222
- },
223
- {
224
- productId: 'prod-mtg-starter',
225
- name: 'Magic the Gathering Starter Set',
226
- quantity: 1,
227
- reason: 'Ideal starter set for Magic the Gathering enthusiasts',
228
- },
229
- ],
230
- });
231
- });
232
- });
233
- });
234
- query('views suggested items')
235
- .client(() => {
236
- specs('Suggested Items Screen', () => {
237
- should('display all suggested items with names and reasons');
238
- should('show quantity selectors for each item');
239
- should('have an "Add to Cart" button for selected items');
240
- should('allow removing items from the suggestions');
241
- });
242
- })
243
- .request(
244
- gql(\`query GetSuggestedItems($sessionId: ID!) {
245
- suggestedItems(sessionId: $sessionId) {
246
- items {
247
- productId
248
- name
249
- quantity
250
- reason
251
- }
252
- }
253
- }\`),
254
- )
255
- .server(() => {
256
- data([source().state('SuggestedItems').fromProjection('SuggestedItemsProjection', 'sessionId')]);
257
- specs('Suggested items are available for viewing', () => {
258
- rule('Items should be available for viewing after suggestion', () => {
259
- example('Item becomes available after AI suggestion event')
260
- .when<ShoppingItemsSuggested>({
261
- sessionId: 'session-abc',
262
- suggestedItems: [
263
- {
264
- productId: 'prod-soccer-ball',
265
- name: 'Super Soccer Ball',
266
- quantity: 1,
267
- reason: 'Perfect for your daughter who loves soccer',
268
- },
269
- {
270
- productId: 'prod-craft-kit',
271
- name: 'Deluxe Craft Kit',
272
- quantity: 1,
273
- reason: 'Great for creative activities and crafts',
274
- },
275
- {
276
- productId: 'prod-laptop-bag',
277
- name: 'Tech Laptop Backpack',
278
- quantity: 1,
279
- reason: "Essential for your son's school computer needs",
280
- },
281
- {
282
- productId: 'prod-mtg-starter',
283
- name: 'Magic the Gathering Starter Set',
284
- quantity: 1,
285
- reason: 'Ideal starter set for Magic the Gathering enthusiasts',
286
- },
287
- ],
288
- })
289
- .then<SuggestedItems>({
290
- sessionId: 'session-abc',
291
- items: [
292
- {
293
- productId: 'prod-soccer-ball',
294
- name: 'Super Soccer Ball',
295
- quantity: 1,
296
- reason: 'Perfect for your daughter who loves soccer',
297
- },
298
- {
299
- productId: 'prod-craft-kit',
300
- name: 'Deluxe Craft Kit',
301
- quantity: 1,
302
- reason: 'Great for creative activities and crafts',
303
- },
304
- {
305
- productId: 'prod-laptop-bag',
306
- name: 'Tech Laptop Backpack',
307
- quantity: 1,
308
- reason: "Essential for your son's school computer needs",
309
- },
310
- {
311
- productId: 'prod-mtg-starter',
312
- name: 'Magic the Gathering Starter Set',
313
- quantity: 1,
314
- reason: 'Ideal starter set for Magic the Gathering enthusiasts',
315
- },
316
- ],
317
- });
318
- });
319
- });
320
- });
321
- command('accepts items and adds to their cart')
322
- .client(() => {
323
- specs('Suggested Items Screen', () => {
324
- should('allow selecting specific items to add');
325
- should('update quantities before adding to cart');
326
- should('provide feedback when items are added');
327
- });
328
- })
329
- .server(() => {
330
- data([sink().event('ItemsAddedToCart').toStream('shopping-session-\${sessionId}')]);
331
- specs('When shopper accepts items, they are added to cart', () => {
332
- rule('Accepted items should be added to the shopping cart', () => {
333
- example('User selects all suggested items for cart')
334
- .when<AddItemsToCart>({
335
- sessionId: 'session-abc',
336
- items: [
337
- { productId: 'prod-soccer-ball', quantity: 1 },
338
- { productId: 'prod-craft-kit', quantity: 1 },
339
- { productId: 'prod-laptop-bag', quantity: 1 },
340
- { productId: 'prod-mtg-starter', quantity: 1 },
341
- ],
342
- })
343
- .then<ItemsAddedToCart>({
344
- sessionId: 'session-abc',
345
- items: [
346
- { productId: 'prod-soccer-ball', quantity: 1 },
347
- { productId: 'prod-craft-kit', quantity: 1 },
348
- { productId: 'prod-laptop-bag', quantity: 1 },
349
- { productId: 'prod-mtg-starter', quantity: 1 },
350
- ],
351
- });
352
- });
353
- });
354
- });
355
- });
356
- `);
357
- });
358
- it('should handle experience slices in model to flow conversion', async () => {
359
- const experienceModel = {
360
- variant: 'specs',
361
- narratives: [
362
- {
363
- name: 'Test Experience Flow',
364
- id: 'TEST-001',
365
- slices: [
366
- {
367
- name: 'Homepage',
368
- id: 'EXP-001',
369
- type: 'experience',
370
- client: {
371
- specs: {
372
- name: '',
373
- rules: ['show a hero section with a welcome message', 'allow user to start the questionnaire'],
374
- },
375
- },
376
- },
377
- ],
378
- },
379
- ],
380
- messages: [],
381
- integrations: [],
382
- };
383
- const code = await modelToNarrative(experienceModel);
384
- expect(code).toEqual(`import { experience, narrative, should, specs } from '@auto-engineer/narrative';
385
- narrative('Test Experience Flow', 'TEST-001', () => {
386
- experience('Homepage', 'EXP-001').client(() => {
387
- specs(() => {
388
- should('show a hero section with a welcome message');
389
- should('allow user to start the questionnaire');
390
- });
391
- });
392
- });
393
- `);
394
- });
395
- it('should handle flows and slices without IDs', async () => {
396
- const modelWithoutIds = {
397
- variant: 'specs',
398
- narratives: [
399
- {
400
- name: 'Test Flow without IDs',
401
- // id: undefined - no ID
402
- slices: [
403
- {
404
- name: 'Homepage',
405
- // id: undefined - no ID
406
- type: 'experience',
407
- client: {
408
- specs: {
409
- name: 'Homepage specs',
410
- rules: ['show welcome message', 'display navigation'],
411
- },
412
- },
413
- },
414
- ],
415
- },
416
- ],
417
- messages: [],
418
- integrations: [],
419
- };
420
- const code = await modelToNarrative(modelWithoutIds);
421
- expect(code).toEqual(`import { experience, narrative, should, specs } from '@auto-engineer/narrative';
422
- narrative('Test Flow without IDs', () => {
423
- experience('Homepage').client(() => {
424
- specs('Homepage specs', () => {
425
- should('show welcome message');
426
- should('display navigation');
427
- });
428
- });
429
- });
430
- `);
431
- });
432
- it('should include flow and slice IDs in generated code', async () => {
433
- const modelWithIds = {
434
- variant: 'specs',
435
- narratives: [
436
- {
437
- name: 'Test Flow with IDs',
438
- id: 'FLOW-123',
439
- slices: [
440
- {
441
- name: 'Homepage',
442
- id: 'SLICE-ABC',
443
- type: 'experience',
444
- client: {
445
- specs: {
446
- name: 'Homepage specs',
447
- rules: ['show welcome message', 'display navigation'],
448
- },
449
- },
450
- },
451
- {
452
- name: 'view products',
453
- id: 'SLICE-XYZ',
454
- type: 'query',
455
- client: {
456
- description: 'Product query client',
457
- specs: {
458
- name: 'Product list specs',
459
- rules: ['display all products', 'allow filtering'],
460
- },
461
- },
462
- server: {
463
- description: 'Product query server',
464
- specs: {
465
- name: 'Product data specs',
466
- rules: [],
467
- },
468
- },
469
- },
470
- ],
471
- },
472
- ],
473
- messages: [],
474
- integrations: [],
475
- };
476
- const code = await modelToNarrative(modelWithIds);
477
- expect(code).toEqual(`import { experience, narrative, query, should, specs } from '@auto-engineer/narrative';
478
- narrative('Test Flow with IDs', 'FLOW-123', () => {
479
- experience('Homepage', 'SLICE-ABC').client(() => {
480
- specs('Homepage specs', () => {
481
- should('show welcome message');
482
- should('display navigation');
483
- });
484
- });
485
- query('view products', 'SLICE-XYZ')
486
- .client(() => {
487
- specs('Product list specs', () => {
488
- should('display all products');
489
- should('allow filtering');
490
- });
491
- })
492
- .server(() => {
493
- specs('Product data specs', () => {});
494
- });
495
- });
496
- `);
497
- });
498
- it('should include rule IDs in server specs when present', async () => {
499
- const modelWithRuleIds = {
500
- variant: 'specs',
501
- narratives: [
502
- {
503
- name: 'Test Flow with Rule IDs',
504
- id: 'FLOW-456',
505
- slices: [
506
- {
507
- name: 'process command',
508
- id: 'SLICE-789',
509
- type: 'command',
510
- client: {
511
- description: 'Command processing client',
512
- },
513
- server: {
514
- description: 'Command processing server',
515
- specs: {
516
- name: 'Command Processing',
517
- rules: [
518
- {
519
- id: 'RULE-ABC',
520
- description: 'Valid commands should be processed',
521
- examples: [
522
- {
523
- description: 'User submits valid command',
524
- when: {
525
- commandRef: 'ProcessCommand',
526
- exampleData: { id: 'cmd-123', action: 'create' },
527
- },
528
- then: [
529
- {
530
- eventRef: 'CommandProcessed',
531
- exampleData: { id: 'cmd-123', status: 'success' },
532
- },
533
- ],
534
- },
535
- ],
536
- },
537
- ],
538
- },
539
- },
540
- },
541
- ],
542
- },
543
- ],
544
- messages: [
545
- {
546
- type: 'command',
547
- name: 'ProcessCommand',
548
- fields: [
549
- { name: 'id', type: 'string', required: true },
550
- { name: 'action', type: 'string', required: true },
551
- ],
552
- metadata: { version: 1 },
553
- },
554
- {
555
- type: 'event',
556
- name: 'CommandProcessed',
557
- fields: [
558
- { name: 'id', type: 'string', required: true },
559
- { name: 'status', type: 'string', required: true },
560
- ],
561
- source: 'external',
562
- metadata: { version: 1 },
563
- },
564
- ],
565
- integrations: [],
566
- };
567
- const code = await modelToNarrative(modelWithRuleIds);
568
- expect(code).toEqual(`import { command, example, narrative, rule, specs } from '@auto-engineer/narrative';
569
- import type { Command, Event } from '@auto-engineer/narrative';
570
- type ProcessCommand = Command<
571
- 'ProcessCommand',
572
- {
573
- id: string;
574
- action: string;
575
- }
576
- >;
577
- type CommandProcessed = Event<
578
- 'CommandProcessed',
579
- {
580
- id: string;
581
- status: string;
582
- }
583
- >;
584
- narrative('Test Flow with Rule IDs', 'FLOW-456', () => {
585
- command('process command', 'SLICE-789').server(() => {
586
- specs('Command Processing', () => {
587
- rule('Valid commands should be processed', 'RULE-ABC', () => {
588
- example('User submits valid command')
589
- .when<ProcessCommand>({ id: 'cmd-123', action: 'create' })
590
- .then<CommandProcessed>({ id: 'cmd-123', status: 'success' });
591
- });
592
- });
593
- });
594
- });
595
- `);
596
- });
597
- it('should correctly resolve Date types in messages', async () => {
598
- const modelWithDateTypes = {
599
- variant: 'specs',
600
- narratives: [
601
- {
602
- name: 'Questionnaire Flow',
603
- id: 'QUEST-001',
604
- slices: [],
605
- },
606
- ],
607
- messages: [
608
- {
609
- type: 'event',
610
- name: 'QuestionnaireLinkSent',
611
- fields: [
612
- { name: 'questionnaireId', type: 'string', required: true },
613
- { name: 'participantId', type: 'string', required: true },
614
- { name: 'link', type: 'string', required: true },
615
- { name: 'sentAt', type: 'Date', required: true },
616
- ],
617
- source: 'external',
618
- metadata: { version: 1 },
619
- },
620
- {
621
- type: 'event',
622
- name: 'QuestionAnswered',
623
- fields: [
624
- { name: 'questionnaireId', type: 'string', required: true },
625
- { name: 'participantId', type: 'string', required: true },
626
- { name: 'questionId', type: 'string', required: true },
627
- { name: 'answer', type: 'unknown', required: true },
628
- { name: 'savedAt', type: 'Date', required: true },
629
- ],
630
- source: 'external',
631
- metadata: { version: 1 },
632
- },
633
- ],
634
- integrations: [],
635
- };
636
- const code = await modelToNarrative(modelWithDateTypes);
637
- expect(code).toEqual(`import { narrative } from '@auto-engineer/narrative';
638
- import type { Event } from '@auto-engineer/narrative';
639
- type QuestionnaireLinkSent = Event<
640
- 'QuestionnaireLinkSent',
641
- {
642
- questionnaireId: string;
643
- participantId: string;
644
- link: string;
645
- sentAt: Date;
646
- }
647
- >;
648
- type QuestionAnswered = Event<
649
- 'QuestionAnswered',
650
- {
651
- questionnaireId: string;
652
- participantId: string;
653
- questionId: string;
654
- answer: unknown;
655
- savedAt: Date;
656
- }
657
- >;
658
- narrative('Questionnaire Flow', 'QUEST-001', () => {});
659
- `);
660
- });
661
- it('should generate browser-compatible imports without mixing values and types', async () => {
662
- const questionnairesModel = {
663
- variant: 'specs',
664
- narratives: [
665
- {
666
- name: 'Questionnaires',
667
- id: 'AUTO-Q9m2Kp4Lx',
668
- slices: [
669
- {
670
- name: 'Homepage',
671
- id: 'AUTO-H1a4Bn6Cy',
672
- type: 'experience',
673
- client: {
674
- specs: {
675
- name: '',
676
- rules: ['show a hero section with a welcome message', 'allow user to start the questionnaire'],
677
- },
678
- },
679
- },
680
- {
681
- name: 'views the questionnaire',
682
- id: 'AUTO-V7n8Rq5M',
683
- type: 'query',
684
- client: {
685
- description: '',
686
- specs: {
687
- name: 'Questionnaire Progress',
688
- rules: [
689
- 'focus on the current question based on the progress state',
690
- 'display the list of answered questions',
691
- 'display the list of remaining questions',
692
- 'show a progress indicator that is always visible as the user scrolls',
693
- ],
694
- },
695
- },
696
- request: 'query QuestionnaireProgress($participantId: ID!) {\n questionnaireProgress(participantId: $participantId) {\n questionnaireId\n participantId\n currentQuestionId\n remainingQuestions\n status\n answers {\n questionId\n value\n }\n }\n}',
697
- server: {
698
- description: '',
699
- data: [
700
- {
701
- target: {
702
- type: 'State',
703
- name: 'QuestionnaireProgress',
704
- },
705
- origin: {
706
- type: 'projection',
707
- name: 'Questionnaires',
708
- idField: 'questionnaire-participantId',
709
- },
710
- },
711
- ],
712
- specs: {
713
- name: '',
714
- rules: [
715
- {
716
- id: 'AUTO-r1A3Bp9W',
717
- description: 'questionnaires show current progress',
718
- examples: [
719
- {
720
- description: 'a question has already been answered',
721
- given: [
722
- {
723
- eventRef: 'QuestionnaireLinkSent',
724
- exampleData: {
725
- questionnaireId: 'q-001',
726
- participantId: 'participant-abc',
727
- link: 'https://app.example.com/q/q-001?participant=participant-abc',
728
- sentAt: new Date('2030-01-01T09:00:00.000Z'),
729
- },
730
- },
731
- ],
732
- when: {
733
- exampleData: {
734
- questionnaireId: 'q-001',
735
- participantId: 'participant-abc',
736
- questionId: 'q1',
737
- answer: 'Yes',
738
- savedAt: new Date('2030-01-01T09:05:00.000Z'),
739
- },
740
- eventRef: 'QuestionAnswered',
741
- },
742
- then: [
743
- {
744
- stateRef: 'QuestionnaireProgress',
745
- exampleData: {
746
- questionnaireId: 'q-001',
747
- participantId: 'participant-abc',
748
- status: 'in_progress',
749
- currentQuestionId: 'q2',
750
- remainingQuestions: ['q2', 'q3'],
751
- answers: [
752
- {
753
- questionId: 'q1',
754
- value: 'Yes',
755
- },
756
- ],
757
- },
758
- },
759
- ],
760
- },
761
- ],
762
- },
763
- ],
764
- },
765
- },
766
- },
767
- ],
768
- },
769
- ],
770
- messages: [
771
- {
772
- type: 'event',
773
- name: 'QuestionnaireLinkSent',
774
- fields: [
775
- {
776
- name: 'questionnaireId',
777
- type: 'string',
778
- required: true,
779
- },
780
- {
781
- name: 'participantId',
782
- type: 'string',
783
- required: true,
784
- },
785
- {
786
- name: 'link',
787
- type: 'string',
788
- required: true,
789
- },
790
- {
791
- name: 'sentAt',
792
- type: 'Date',
793
- required: true,
794
- },
795
- ],
796
- source: 'internal',
797
- metadata: {
798
- version: 1,
799
- },
800
- },
801
- {
802
- type: 'event',
803
- name: 'QuestionAnswered',
804
- fields: [
805
- {
806
- name: 'questionnaireId',
807
- type: 'string',
808
- required: true,
809
- },
810
- {
811
- name: 'participantId',
812
- type: 'string',
813
- required: true,
814
- },
815
- {
816
- name: 'questionId',
817
- type: 'string',
818
- required: true,
819
- },
820
- {
821
- name: 'answer',
822
- type: 'unknown',
823
- required: true,
824
- },
825
- {
826
- name: 'savedAt',
827
- type: 'Date',
828
- required: true,
829
- },
830
- ],
831
- source: 'internal',
832
- metadata: {
833
- version: 1,
834
- },
835
- },
836
- {
837
- type: 'state',
838
- name: 'QuestionnaireProgress',
839
- fields: [
840
- {
841
- name: 'questionnaireId',
842
- type: 'string',
843
- required: true,
844
- },
845
- {
846
- name: 'participantId',
847
- type: 'string',
848
- required: true,
849
- },
850
- {
851
- name: 'status',
852
- type: '"in_progress" | "ready_to_submit" | "submitted"',
853
- required: true,
854
- },
855
- {
856
- name: 'currentQuestionId',
857
- type: 'string | null',
858
- required: true,
859
- },
860
- {
861
- name: 'remainingQuestions',
862
- type: 'Array<string>',
863
- required: true,
864
- },
865
- {
866
- name: 'answers',
867
- type: 'Array<{ questionId: string; value: unknown }>',
868
- required: true,
869
- },
870
- ],
871
- metadata: {
872
- version: 1,
873
- },
874
- },
875
- ],
876
- integrations: [],
877
- };
878
- const code = await modelToNarrative(questionnairesModel);
879
- expect(code).toEqual(`import {
880
- data,
881
- example,
882
- experience,
883
- gql,
884
- narrative,
885
- query,
886
- rule,
887
- should,
888
- source,
889
- specs,
890
- } from '@auto-engineer/narrative';
891
- import type { Event, State } from '@auto-engineer/narrative';
892
- type QuestionnaireLinkSent = Event<
893
- 'QuestionnaireLinkSent',
894
- {
895
- questionnaireId: string;
896
- participantId: string;
897
- link: string;
898
- sentAt: Date;
899
- }
900
- >;
901
- type QuestionAnswered = Event<
902
- 'QuestionAnswered',
903
- {
904
- questionnaireId: string;
905
- participantId: string;
906
- questionId: string;
907
- answer: unknown;
908
- savedAt: Date;
909
- }
910
- >;
911
- type QuestionnaireProgress = State<
912
- 'QuestionnaireProgress',
913
- {
914
- questionnaireId: string;
915
- participantId: string;
916
- status: 'in_progress' | 'ready_to_submit' | 'submitted';
917
- currentQuestionId: string | null;
918
- remainingQuestions: string[];
919
- answers: {
920
- questionId: string;
921
- value: unknown;
922
- }[];
923
- }
924
- >;
925
- narrative('Questionnaires', 'AUTO-Q9m2Kp4Lx', () => {
926
- experience('Homepage', 'AUTO-H1a4Bn6Cy').client(() => {
927
- specs(() => {
928
- should('show a hero section with a welcome message');
929
- should('allow user to start the questionnaire');
930
- });
931
- });
932
- query('views the questionnaire', 'AUTO-V7n8Rq5M')
933
- .client(() => {
934
- specs('Questionnaire Progress', () => {
935
- should('focus on the current question based on the progress state');
936
- should('display the list of answered questions');
937
- should('display the list of remaining questions');
938
- should('show a progress indicator that is always visible as the user scrolls');
939
- });
940
- })
941
- .request(
942
- gql(\`query QuestionnaireProgress($participantId: ID!) {
943
- questionnaireProgress(participantId: $participantId) {
944
- questionnaireId
945
- participantId
946
- currentQuestionId
947
- remainingQuestions
948
- status
949
- answers {
950
- questionId
951
- value
952
- }
953
- }
954
- }\`),
955
- )
956
- .server(() => {
957
- data([source().state('QuestionnaireProgress').fromProjection('Questionnaires', 'questionnaire-participantId')]);
958
- specs(() => {
959
- rule('questionnaires show current progress', 'AUTO-r1A3Bp9W', () => {
960
- example('a question has already been answered')
961
- .given<QuestionnaireLinkSent>({
962
- questionnaireId: 'q-001',
963
- participantId: 'participant-abc',
964
- link: 'https://app.example.com/q/q-001?participant=participant-abc',
965
- sentAt: new Date('2030-01-01T09:00:00.000Z'),
966
- })
967
- .when<QuestionAnswered>({
968
- questionnaireId: 'q-001',
969
- participantId: 'participant-abc',
970
- questionId: 'q1',
971
- answer: 'Yes',
972
- savedAt: new Date('2030-01-01T09:05:00.000Z'),
973
- })
974
- .then<QuestionnaireProgress>({
975
- questionnaireId: 'q-001',
976
- participantId: 'participant-abc',
977
- status: 'in_progress',
978
- currentQuestionId: 'q2',
979
- remainingQuestions: ['q2', 'q3'],
980
- answers: [{ questionId: 'q1', value: 'Yes' }],
981
- });
982
- });
983
- });
984
- });
985
- });
986
- `);
987
- });
988
- it('should consolidate duplicate rules with multiple examples into single rule blocks', async () => {
989
- const modelWithDuplicateRules = {
990
- variant: 'specs',
991
- narratives: [
992
- {
993
- name: 'Test Flow',
994
- id: 'TEST-FLOW',
995
- slices: [
996
- {
997
- name: 'test slice',
998
- id: 'TEST-SLICE',
999
- type: 'query',
1000
- client: {
1001
- description: 'Test client for duplicate rules',
1002
- },
1003
- server: {
1004
- description: 'Test server for duplicate rules',
1005
- specs: {
1006
- name: 'Test Rules',
1007
- rules: [
1008
- {
1009
- id: 'AUTO-r1A3Bp9W',
1010
- description: 'questionnaires show current progress',
1011
- examples: [
1012
- {
1013
- description: 'a question has already been answered',
1014
- given: [
1015
- {
1016
- eventRef: 'QuestionnaireLinkSent',
1017
- exampleData: {
1018
- questionnaireId: 'q-001',
1019
- participantId: 'participant-abc',
1020
- },
1021
- },
1022
- ],
1023
- when: {
1024
- eventRef: 'QuestionAnswered',
1025
- exampleData: {
1026
- questionnaireId: 'q-001',
1027
- questionId: 'q1',
1028
- answer: 'Yes',
1029
- },
1030
- },
1031
- then: [
1032
- {
1033
- stateRef: 'QuestionnaireProgress',
1034
- exampleData: {
1035
- questionnaireId: 'q-001',
1036
- status: 'in_progress',
1037
- },
1038
- },
1039
- ],
1040
- },
1041
- {
1042
- description: 'no questions have been answered yet',
1043
- given: [
1044
- {
1045
- eventRef: 'QuestionnaireLinkSent',
1046
- exampleData: {
1047
- questionnaireId: 'q-001',
1048
- participantId: 'participant-abc',
1049
- },
1050
- },
1051
- ],
1052
- when: {
1053
- eventRef: 'QuestionnaireLinkSent',
1054
- exampleData: {},
1055
- },
1056
- then: [
1057
- {
1058
- stateRef: 'QuestionnaireProgress',
1059
- exampleData: {
1060
- questionnaireId: 'q-001',
1061
- status: 'in_progress',
1062
- },
1063
- },
1064
- ],
1065
- },
1066
- ],
1067
- },
1068
- ],
1069
- },
1070
- },
1071
- },
1072
- ],
1073
- },
1074
- ],
1075
- messages: [
1076
- {
1077
- type: 'event',
1078
- name: 'QuestionnaireLinkSent',
1079
- fields: [
1080
- { name: 'questionnaireId', type: 'string', required: true },
1081
- { name: 'participantId', type: 'string', required: true },
1082
- ],
1083
- source: 'internal',
1084
- metadata: { version: 1 },
1085
- },
1086
- {
1087
- type: 'event',
1088
- name: 'QuestionAnswered',
1089
- fields: [
1090
- { name: 'questionnaireId', type: 'string', required: true },
1091
- { name: 'questionId', type: 'string', required: true },
1092
- { name: 'answer', type: 'unknown', required: true },
1093
- ],
1094
- source: 'internal',
1095
- metadata: { version: 1 },
1096
- },
1097
- {
1098
- type: 'state',
1099
- name: 'QuestionnaireProgress',
1100
- fields: [
1101
- { name: 'questionnaireId', type: 'string', required: true },
1102
- { name: 'status', type: 'string', required: true },
1103
- ],
1104
- metadata: { version: 1 },
1105
- },
1106
- ],
1107
- integrations: [],
1108
- };
1109
- const code = await modelToNarrative(modelWithDuplicateRules);
1110
- expect(code).toEqual(`import { example, narrative, query, rule, specs } from '@auto-engineer/narrative';
1111
- import type { Event, State } from '@auto-engineer/narrative';
1112
- type QuestionnaireLinkSent = Event<
1113
- 'QuestionnaireLinkSent',
1114
- {
1115
- questionnaireId: string;
1116
- participantId: string;
1117
- }
1118
- >;
1119
- type QuestionAnswered = Event<
1120
- 'QuestionAnswered',
1121
- {
1122
- questionnaireId: string;
1123
- questionId: string;
1124
- answer: unknown;
1125
- }
1126
- >;
1127
- type QuestionnaireProgress = State<
1128
- 'QuestionnaireProgress',
1129
- {
1130
- questionnaireId: string;
1131
- status: string;
1132
- }
1133
- >;
1134
- narrative('Test Flow', 'TEST-FLOW', () => {
1135
- query('test slice', 'TEST-SLICE').server(() => {
1136
- specs('Test Rules', () => {
1137
- rule('questionnaires show current progress', 'AUTO-r1A3Bp9W', () => {
1138
- example('a question has already been answered')
1139
- .given<QuestionnaireLinkSent>({ questionnaireId: 'q-001', participantId: 'participant-abc' })
1140
- .when<QuestionAnswered>({ questionnaireId: 'q-001', questionId: 'q1', answer: 'Yes' })
1141
- .then<QuestionnaireProgress>({ questionnaireId: 'q-001', status: 'in_progress' });
1142
- example('no questions have been answered yet')
1143
- .given<QuestionnaireLinkSent>({ questionnaireId: 'q-001', participantId: 'participant-abc' })
1144
- .when<QuestionnaireLinkSent>({})
1145
- .then<QuestionnaireProgress>({ questionnaireId: 'q-001', status: 'in_progress' });
1146
- });
1147
- });
1148
- });
1149
- });
1150
- `);
1151
- });
1152
- it('should chain multiple given examples with .and() syntax', async () => {
1153
- const modelWithMultiGiven = {
1154
- variant: 'specs',
1155
- narratives: [
1156
- {
1157
- name: 'Multi Given Flow',
1158
- id: 'MULTI-GIVEN',
1159
- slices: [
1160
- {
1161
- name: 'multi given slice',
1162
- id: 'MULTI-SLICE',
1163
- type: 'query',
1164
- client: {
1165
- description: 'Multi given client',
1166
- },
1167
- server: {
1168
- description: 'Multi given server rules',
1169
- specs: {
1170
- name: 'Multi Given Rules',
1171
- rules: [
1172
- {
1173
- id: 'AUTO-MultiGiven',
1174
- description: 'all questions have been answered',
1175
- examples: [
1176
- {
1177
- description: 'questionnaire with multiple events',
1178
- given: [
1179
- {
1180
- stateRef: 'QuestionnaireConfig',
1181
- exampleData: {
1182
- questionnaireId: 'q-001',
1183
- numberOfQuestions: 3,
1184
- },
1185
- },
1186
- {
1187
- eventRef: 'QuestionnaireLinkSent',
1188
- exampleData: {
1189
- questionnaireId: 'q-001',
1190
- participantId: 'participant-abc',
1191
- link: 'https://example.com/q/q-001',
1192
- sentAt: new Date('2030-01-01T09:00:00.000Z'),
1193
- },
1194
- },
1195
- {
1196
- eventRef: 'QuestionAnswered',
1197
- exampleData: {
1198
- questionnaireId: 'q-001',
1199
- participantId: 'participant-abc',
1200
- questionId: 'q1',
1201
- answer: 'Yes',
1202
- savedAt: new Date('2030-01-01T09:05:00.000Z'),
1203
- },
1204
- },
1205
- {
1206
- eventRef: 'QuestionAnswered',
1207
- exampleData: {
1208
- questionnaireId: 'q-001',
1209
- participantId: 'participant-abc',
1210
- questionId: 'q2',
1211
- answer: 'No',
1212
- savedAt: new Date('2030-01-01T09:10:00.000Z'),
1213
- },
1214
- },
1215
- ],
1216
- when: {
1217
- eventRef: 'QuestionAnswered',
1218
- exampleData: {
1219
- questionnaireId: 'q-001',
1220
- participantId: 'participant-abc',
1221
- questionId: 'q3',
1222
- answer: 'Maybe',
1223
- savedAt: new Date('2030-01-01T09:15:00.000Z'),
1224
- },
1225
- },
1226
- then: [
1227
- {
1228
- stateRef: 'QuestionnaireProgress',
1229
- exampleData: {
1230
- questionnaireId: 'q-001',
1231
- participantId: 'participant-abc',
1232
- status: 'ready_to_submit',
1233
- currentQuestionId: null,
1234
- remainingQuestions: [],
1235
- answers: [
1236
- { questionId: 'q1', value: 'Yes' },
1237
- { questionId: 'q2', value: 'No' },
1238
- ],
1239
- },
1240
- },
1241
- ],
1242
- },
1243
- ],
1244
- },
1245
- ],
1246
- },
1247
- },
1248
- },
1249
- ],
1250
- },
1251
- ],
1252
- messages: [
1253
- {
1254
- type: 'state',
1255
- name: 'QuestionnaireConfig',
1256
- fields: [
1257
- { name: 'questionnaireId', type: 'string', required: true },
1258
- { name: 'numberOfQuestions', type: 'number', required: true },
1259
- ],
1260
- metadata: { version: 1 },
1261
- },
1262
- {
1263
- type: 'event',
1264
- name: 'QuestionnaireLinkSent',
1265
- fields: [
1266
- { name: 'questionnaireId', type: 'string', required: true },
1267
- { name: 'participantId', type: 'string', required: true },
1268
- { name: 'link', type: 'string', required: true },
1269
- { name: 'sentAt', type: 'Date', required: true },
1270
- ],
1271
- source: 'internal',
1272
- metadata: { version: 1 },
1273
- },
1274
- {
1275
- type: 'event',
1276
- name: 'QuestionAnswered',
1277
- fields: [
1278
- { name: 'questionnaireId', type: 'string', required: true },
1279
- { name: 'participantId', type: 'string', required: true },
1280
- { name: 'questionId', type: 'string', required: true },
1281
- { name: 'answer', type: 'unknown', required: true },
1282
- { name: 'savedAt', type: 'Date', required: true },
1283
- ],
1284
- source: 'internal',
1285
- metadata: { version: 1 },
1286
- },
1287
- {
1288
- type: 'state',
1289
- name: 'QuestionnaireProgress',
1290
- fields: [
1291
- { name: 'questionnaireId', type: 'string', required: true },
1292
- { name: 'participantId', type: 'string', required: true },
1293
- { name: 'status', type: '"in_progress" | "ready_to_submit" | "submitted"', required: true },
1294
- { name: 'currentQuestionId', type: 'string | null', required: true },
1295
- { name: 'remainingQuestions', type: 'Array<string>', required: true },
1296
- { name: 'answers', type: 'Array<{ questionId: string; value: unknown }>', required: true },
1297
- ],
1298
- metadata: { version: 1 },
1299
- },
1300
- ],
1301
- integrations: [],
1302
- };
1303
- const code = await modelToNarrative(modelWithMultiGiven);
1304
- expect(code).toEqual(`import { example, narrative, query, rule, specs } from '@auto-engineer/narrative';
1305
- import type { Event, State } from '@auto-engineer/narrative';
1306
- type QuestionnaireConfig = State<
1307
- 'QuestionnaireConfig',
1308
- {
1309
- questionnaireId: string;
1310
- numberOfQuestions: number;
1311
- }
1312
- >;
1313
- type QuestionnaireLinkSent = Event<
1314
- 'QuestionnaireLinkSent',
1315
- {
1316
- questionnaireId: string;
1317
- participantId: string;
1318
- link: string;
1319
- sentAt: Date;
1320
- }
1321
- >;
1322
- type QuestionAnswered = Event<
1323
- 'QuestionAnswered',
1324
- {
1325
- questionnaireId: string;
1326
- participantId: string;
1327
- questionId: string;
1328
- answer: unknown;
1329
- savedAt: Date;
1330
- }
1331
- >;
1332
- type QuestionnaireProgress = State<
1333
- 'QuestionnaireProgress',
1334
- {
1335
- questionnaireId: string;
1336
- participantId: string;
1337
- status: 'in_progress' | 'ready_to_submit' | 'submitted';
1338
- currentQuestionId: string | null;
1339
- remainingQuestions: string[];
1340
- answers: {
1341
- questionId: string;
1342
- value: unknown;
1343
- }[];
1344
- }
1345
- >;
1346
- narrative('Multi Given Flow', 'MULTI-GIVEN', () => {
1347
- query('multi given slice', 'MULTI-SLICE').server(() => {
1348
- specs('Multi Given Rules', () => {
1349
- rule('all questions have been answered', 'AUTO-MultiGiven', () => {
1350
- example('questionnaire with multiple events')
1351
- .given<QuestionnaireConfig>({ questionnaireId: 'q-001', numberOfQuestions: 3 })
1352
- .and<QuestionnaireLinkSent>({
1353
- questionnaireId: 'q-001',
1354
- participantId: 'participant-abc',
1355
- link: 'https://example.com/q/q-001',
1356
- sentAt: new Date('2030-01-01T09:00:00.000Z'),
1357
- })
1358
- .and<QuestionAnswered>({
1359
- questionnaireId: 'q-001',
1360
- participantId: 'participant-abc',
1361
- questionId: 'q1',
1362
- answer: 'Yes',
1363
- savedAt: new Date('2030-01-01T09:05:00.000Z'),
1364
- })
1365
- .and<QuestionAnswered>({
1366
- questionnaireId: 'q-001',
1367
- participantId: 'participant-abc',
1368
- questionId: 'q2',
1369
- answer: 'No',
1370
- savedAt: new Date('2030-01-01T09:10:00.000Z'),
1371
- })
1372
- .when<QuestionAnswered>({
1373
- questionnaireId: 'q-001',
1374
- participantId: 'participant-abc',
1375
- questionId: 'q3',
1376
- answer: 'Maybe',
1377
- savedAt: new Date('2030-01-01T09:15:00.000Z'),
1378
- })
1379
- .then<QuestionnaireProgress>({
1380
- questionnaireId: 'q-001',
1381
- participantId: 'participant-abc',
1382
- status: 'ready_to_submit',
1383
- currentQuestionId: null,
1384
- remainingQuestions: [],
1385
- answers: [
1386
- { questionId: 'q1', value: 'Yes' },
1387
- { questionId: 'q2', value: 'No' },
1388
- ],
1389
- });
1390
- });
1391
- });
1392
- });
1393
- });
1394
- `);
1395
- });
1396
- it('should generate types for states referenced in data origins', async () => {
1397
- const modelWithReferencedStates = {
1398
- variant: 'specs',
1399
- narratives: [
1400
- {
1401
- name: 'Referenced States Flow',
1402
- id: 'REF-STATES',
1403
- slices: [
1404
- {
1405
- name: 'query with database states',
1406
- id: 'REF-SLICE',
1407
- type: 'query',
1408
- client: {
1409
- description: 'Client for referenced states',
1410
- },
1411
- server: {
1412
- description: 'Server for referenced states',
1413
- data: [
1414
- {
1415
- target: {
1416
- type: 'State',
1417
- name: 'QuestionnaireProgress',
1418
- },
1419
- origin: {
1420
- type: 'projection',
1421
- name: 'QuestionnaireProjection',
1422
- idField: 'participantId',
1423
- },
1424
- },
1425
- {
1426
- target: {
1427
- type: 'State',
1428
- name: 'QuestionnaireConfig',
1429
- },
1430
- origin: {
1431
- type: 'database',
1432
- collection: 'ConfigStore',
1433
- query: { questionnaireId: '$questionnaireId' },
1434
- },
1435
- },
1436
- ],
1437
- specs: {
1438
- name: 'Database State Rules',
1439
- rules: [
1440
- {
1441
- id: 'AUTO-RefState',
1442
- description: 'questionnaire config is available when referenced',
1443
- examples: [
1444
- {
1445
- description: 'config from database is accessible',
1446
- given: [
1447
- {
1448
- stateRef: 'QuestionnaireConfig',
1449
- exampleData: {
1450
- questionnaireId: 'q-001',
1451
- numberOfQuestions: 5,
1452
- title: 'Customer Satisfaction Survey',
1453
- },
1454
- },
1455
- ],
1456
- when: {
1457
- eventRef: 'QuestionnaireProgress',
1458
- exampleData: {},
1459
- },
1460
- then: [
1461
- {
1462
- stateRef: 'QuestionnaireProgress',
1463
- exampleData: {
1464
- questionnaireId: 'q-001',
1465
- participantId: 'participant-abc',
1466
- status: 'in_progress',
1467
- totalQuestions: 5,
1468
- },
1469
- },
1470
- ],
1471
- },
1472
- ],
1473
- },
1474
- ],
1475
- },
1476
- },
1477
- },
1478
- ],
1479
- },
1480
- ],
1481
- messages: [
1482
- {
1483
- type: 'state',
1484
- name: 'QuestionnaireProgress',
1485
- fields: [
1486
- { name: 'questionnaireId', type: 'string', required: true },
1487
- { name: 'participantId', type: 'string', required: true },
1488
- { name: 'status', type: 'string', required: true },
1489
- { name: 'totalQuestions', type: 'number', required: true },
1490
- ],
1491
- metadata: { version: 1 },
1492
- },
1493
- {
1494
- type: 'state',
1495
- name: 'QuestionnaireConfig',
1496
- fields: [
1497
- { name: 'questionnaireId', type: 'string', required: true },
1498
- { name: 'numberOfQuestions', type: 'number', required: true },
1499
- { name: 'title', type: 'string', required: true },
1500
- ],
1501
- metadata: { version: 1 },
1502
- },
1503
- ],
1504
- integrations: [],
1505
- };
1506
- const code = await modelToNarrative(modelWithReferencedStates);
1507
- expect(code)
1508
- .toEqual(`import { data, example, narrative, query, rule, source, specs } from '@auto-engineer/narrative';
1509
- import type { State } from '@auto-engineer/narrative';
1510
- type QuestionnaireProgress = State<
1511
- 'QuestionnaireProgress',
1512
- {
1513
- questionnaireId: string;
1514
- participantId: string;
1515
- status: string;
1516
- totalQuestions: number;
1517
- }
1518
- >;
1519
- type QuestionnaireConfig = State<
1520
- 'QuestionnaireConfig',
1521
- {
1522
- questionnaireId: string;
1523
- numberOfQuestions: number;
1524
- title: string;
1525
- }
1526
- >;
1527
- narrative('Referenced States Flow', 'REF-STATES', () => {
1528
- query('query with database states', 'REF-SLICE').server(() => {
1529
- data([
1530
- source().state('QuestionnaireProgress').fromProjection('QuestionnaireProjection', 'participantId'),
1531
- source().state('QuestionnaireConfig').fromDatabase('ConfigStore', { questionnaireId: '$questionnaireId' }),
1532
- ]);
1533
- specs('Database State Rules', () => {
1534
- rule('questionnaire config is available when referenced', 'AUTO-RefState', () => {
1535
- example('config from database is accessible')
1536
- .given<QuestionnaireConfig>({
1537
- questionnaireId: 'q-001',
1538
- numberOfQuestions: 5,
1539
- title: 'Customer Satisfaction Survey',
1540
- })
1541
- .when<QuestionnaireProgress>({})
1542
- .then<QuestionnaireProgress>({
1543
- questionnaireId: 'q-001',
1544
- participantId: 'participant-abc',
1545
- status: 'in_progress',
1546
- totalQuestions: 5,
1547
- });
1548
- });
1549
- });
1550
- });
1551
- });
1552
- `);
1553
- });
1554
- it('should generate new Date() constructors for Date fields', async () => {
1555
- const modelWithDateFields = {
1556
- variant: 'specs',
1557
- narratives: [
1558
- {
1559
- name: 'Date Handling Flow',
1560
- id: 'DATE-FLOW',
1561
- slices: [
1562
- {
1563
- name: 'date handling slice',
1564
- id: 'DATE-SLICE',
1565
- type: 'query',
1566
- client: {
1567
- description: 'Date client',
1568
- },
1569
- server: {
1570
- description: 'Date server with Date fields',
1571
- specs: {
1572
- name: 'Date Field Rules',
1573
- rules: [
1574
- {
1575
- id: 'AUTO-DateRule',
1576
- description: 'handles Date fields correctly',
1577
- examples: [
1578
- {
1579
- description: 'event with Date fields',
1580
- given: [
1581
- {
1582
- eventRef: 'TimestampedEvent',
1583
- exampleData: {
1584
- id: 'event-123',
1585
- sentAt: new Date('2030-01-01T09:00:00.000Z'),
1586
- savedAt: new Date('2030-01-01T09:05:00.000Z'),
1587
- attemptedAt: '2030-01-01T09:10:00.000Z',
1588
- submittedAt: '2030-01-01T09:15:00.000Z',
1589
- },
1590
- },
1591
- ],
1592
- when: {
1593
- eventRef: 'ProcessEvent',
1594
- exampleData: {
1595
- processedAt: '2030-01-01T10:00:00.000Z',
1596
- },
1597
- },
1598
- then: [
1599
- {
1600
- stateRef: 'ProcessState',
1601
- exampleData: {
1602
- id: 'state-123',
1603
- completedAt: '2030-01-01T11:00:00.000Z',
1604
- status: 'completed',
1605
- },
1606
- },
1607
- ],
1608
- },
1609
- ],
1610
- },
1611
- ],
1612
- },
1613
- },
1614
- },
1615
- ],
1616
- },
1617
- ],
1618
- messages: [
1619
- {
1620
- type: 'event',
1621
- name: 'TimestampedEvent',
1622
- fields: [
1623
- { name: 'id', type: 'string', required: true },
1624
- { name: 'sentAt', type: 'Date', required: true },
1625
- { name: 'savedAt', type: 'Date', required: true },
1626
- { name: 'attemptedAt', type: 'Date', required: true },
1627
- { name: 'submittedAt', type: 'Date', required: true },
1628
- ],
1629
- source: 'internal',
1630
- metadata: { version: 1 },
1631
- },
1632
- {
1633
- type: 'event',
1634
- name: 'ProcessEvent',
1635
- fields: [{ name: 'processedAt', type: 'Date', required: true }],
1636
- source: 'internal',
1637
- metadata: { version: 1 },
1638
- },
1639
- {
1640
- type: 'state',
1641
- name: 'ProcessState',
1642
- fields: [
1643
- { name: 'id', type: 'string', required: true },
1644
- { name: 'completedAt', type: 'Date', required: true },
1645
- { name: 'status', type: 'string', required: true },
1646
- ],
1647
- metadata: { version: 1 },
1648
- },
1649
- ],
1650
- integrations: [],
1651
- };
1652
- const code = await modelToNarrative(modelWithDateFields);
1653
- expect(code).toEqual(`import { example, narrative, query, rule, specs } from '@auto-engineer/narrative';
1654
- import type { Event, State } from '@auto-engineer/narrative';
1655
- type TimestampedEvent = Event<
1656
- 'TimestampedEvent',
1657
- {
1658
- id: string;
1659
- sentAt: Date;
1660
- savedAt: Date;
1661
- attemptedAt: Date;
1662
- submittedAt: Date;
1663
- }
1664
- >;
1665
- type ProcessEvent = Event<
1666
- 'ProcessEvent',
1667
- {
1668
- processedAt: Date;
1669
- }
1670
- >;
1671
- type ProcessState = State<
1672
- 'ProcessState',
1673
- {
1674
- id: string;
1675
- completedAt: Date;
1676
- status: string;
1677
- }
1678
- >;
1679
- narrative('Date Handling Flow', 'DATE-FLOW', () => {
1680
- query('date handling slice', 'DATE-SLICE').server(() => {
1681
- specs('Date Field Rules', () => {
1682
- rule('handles Date fields correctly', 'AUTO-DateRule', () => {
1683
- example('event with Date fields')
1684
- .given<TimestampedEvent>({
1685
- id: 'event-123',
1686
- sentAt: new Date('2030-01-01T09:00:00.000Z'),
1687
- savedAt: new Date('2030-01-01T09:05:00.000Z'),
1688
- attemptedAt: new Date('2030-01-01T09:10:00.000Z'),
1689
- submittedAt: new Date('2030-01-01T09:15:00.000Z'),
1690
- })
1691
- .when<ProcessEvent>({ processedAt: new Date('2030-01-01T10:00:00.000Z') })
1692
- .then<ProcessState>({
1693
- id: 'state-123',
1694
- completedAt: new Date('2030-01-01T11:00:00.000Z'),
1695
- status: 'completed',
1696
- });
1697
- });
1698
- });
1699
- });
1700
- });
1701
- `);
1702
- });
1703
- it('should generate multiple flows when multiple flows have the same sourceFile', async () => {
1704
- const modelWithMultipleFlowsSameSource = {
1705
- variant: 'specs',
1706
- narratives: [
1707
- {
1708
- name: 'Home Screen',
1709
- sourceFile: '/path/to/homepage.narrative.ts',
1710
- slices: [
1711
- {
1712
- name: 'Active Surveys Summary',
1713
- id: 'AUTO-aifPcU3hw',
1714
- type: 'experience',
1715
- client: {
1716
- specs: {
1717
- name: '',
1718
- rules: ['show active surveys summary'],
1719
- },
1720
- },
1721
- },
1722
- ],
1723
- },
1724
- {
1725
- name: 'Create Survey',
1726
- sourceFile: '/path/to/homepage.narrative.ts',
1727
- slices: [
1728
- {
1729
- name: 'Create Survey Form',
1730
- id: 'AUTO-MPviTMrQC',
1731
- type: 'experience',
1732
- client: {
1733
- specs: {
1734
- name: '',
1735
- rules: ['allow entering survey title'],
1736
- },
1737
- },
1738
- },
1739
- ],
1740
- },
1741
- {
1742
- name: 'Response Analytics',
1743
- sourceFile: '/path/to/homepage.narrative.ts',
1744
- slices: [
1745
- {
1746
- name: 'Response Rate Charts',
1747
- id: 'AUTO-eME978Euk',
1748
- type: 'experience',
1749
- client: {
1750
- specs: {
1751
- name: '',
1752
- rules: ['show daily response rate charts'],
1753
- },
1754
- },
1755
- },
1756
- ],
1757
- },
1758
- ],
1759
- messages: [],
1760
- integrations: [],
1761
- };
1762
- const code = await modelToNarrative(modelWithMultipleFlowsSameSource);
1763
- expect(code).toEqual(`import { experience, narrative, should, specs } from '@auto-engineer/narrative';
1764
- narrative('Home Screen', () => {
1765
- experience('Active Surveys Summary', 'AUTO-aifPcU3hw').client(() => {
1766
- specs(() => {
1767
- should('show active surveys summary');
1768
- });
1769
- });
1770
- });
1771
- narrative('Create Survey', () => {
1772
- experience('Create Survey Form', 'AUTO-MPviTMrQC').client(() => {
1773
- specs(() => {
1774
- should('allow entering survey title');
1775
- });
1776
- });
1777
- });
1778
- narrative('Response Analytics', () => {
1779
- experience('Response Rate Charts', 'AUTO-eME978Euk').client(() => {
1780
- specs(() => {
1781
- should('show daily response rate charts');
1782
- });
1783
- });
1784
- });
1785
- `);
1786
- });
1787
- it('should omit .when({}) when given has multiple items and when is empty', async () => {
1788
- const modelWithEmptyWhen = {
1789
- variant: 'specs',
1790
- narratives: [
1791
- {
1792
- name: 'Todo List Summary',
1793
- id: 'TODO-001',
1794
- slices: [
1795
- {
1796
- name: 'views completion summary',
1797
- id: 'SUMMARY-001',
1798
- type: 'query',
1799
- client: {
1800
- description: 'Summary view client',
1801
- },
1802
- server: {
1803
- description: 'Summary calculation server',
1804
- specs: {
1805
- name: 'Summary Statistics',
1806
- rules: [
1807
- {
1808
- id: 'RULE-SUMMARY',
1809
- description: 'summary shows overall todo list statistics',
1810
- examples: [
1811
- {
1812
- description: 'calculates summary from multiple todos',
1813
- given: [
1814
- {
1815
- eventRef: 'TodoAdded',
1816
- exampleData: {
1817
- todoId: 'todo-001',
1818
- description: 'Buy groceries',
1819
- status: 'pending',
1820
- addedAt: new Date('2030-01-01T09:00:00.000Z'),
1821
- },
1822
- },
1823
- {
1824
- eventRef: 'TodoAdded',
1825
- exampleData: {
1826
- todoId: 'todo-002',
1827
- description: 'Write report',
1828
- status: 'pending',
1829
- addedAt: new Date('2030-01-01T09:10:00.000Z'),
1830
- },
1831
- },
1832
- {
1833
- eventRef: 'TodoMarkedInProgress',
1834
- exampleData: {
1835
- todoId: 'todo-001',
1836
- markedAt: new Date('2030-01-01T10:00:00.000Z'),
1837
- },
1838
- },
1839
- {
1840
- eventRef: 'TodoMarkedComplete',
1841
- exampleData: {
1842
- todoId: 'todo-002',
1843
- completedAt: new Date('2030-01-01T11:00:00.000Z'),
1844
- },
1845
- },
1846
- ],
1847
- when: {
1848
- eventRef: '',
1849
- exampleData: {},
1850
- },
1851
- then: [
1852
- {
1853
- stateRef: 'TodoListSummary',
1854
- exampleData: {
1855
- summaryId: 'main-summary',
1856
- totalTodos: 2,
1857
- pendingCount: 0,
1858
- inProgressCount: 1,
1859
- completedCount: 1,
1860
- completionPercentage: 50,
1861
- },
1862
- },
1863
- ],
1864
- },
1865
- ],
1866
- },
1867
- ],
1868
- },
1869
- },
1870
- },
1871
- ],
1872
- },
1873
- ],
1874
- messages: [
1875
- {
1876
- type: 'event',
1877
- name: 'TodoAdded',
1878
- fields: [
1879
- { name: 'todoId', type: 'string', required: true },
1880
- { name: 'description', type: 'string', required: true },
1881
- { name: 'status', type: 'string', required: true },
1882
- { name: 'addedAt', type: 'Date', required: true },
1883
- ],
1884
- source: 'internal',
1885
- metadata: { version: 1 },
1886
- },
1887
- {
1888
- type: 'event',
1889
- name: 'TodoMarkedInProgress',
1890
- fields: [
1891
- { name: 'todoId', type: 'string', required: true },
1892
- { name: 'markedAt', type: 'Date', required: true },
1893
- ],
1894
- source: 'internal',
1895
- metadata: { version: 1 },
1896
- },
1897
- {
1898
- type: 'event',
1899
- name: 'TodoMarkedComplete',
1900
- fields: [
1901
- { name: 'todoId', type: 'string', required: true },
1902
- { name: 'completedAt', type: 'Date', required: true },
1903
- ],
1904
- source: 'internal',
1905
- metadata: { version: 1 },
1906
- },
1907
- {
1908
- type: 'state',
1909
- name: 'TodoListSummary',
1910
- fields: [
1911
- { name: 'summaryId', type: 'string', required: true },
1912
- { name: 'totalTodos', type: 'number', required: true },
1913
- { name: 'pendingCount', type: 'number', required: true },
1914
- { name: 'inProgressCount', type: 'number', required: true },
1915
- { name: 'completedCount', type: 'number', required: true },
1916
- { name: 'completionPercentage', type: 'number', required: true },
1917
- ],
1918
- metadata: { version: 1 },
1919
- },
1920
- ],
1921
- integrations: [],
1922
- };
1923
- const code = await modelToNarrative(modelWithEmptyWhen);
1924
- expect(code).toEqual(`import { example, narrative, query, rule, specs } from '@auto-engineer/narrative';
1925
- import type { Event, State } from '@auto-engineer/narrative';
1926
- type TodoAdded = Event<
1927
- 'TodoAdded',
1928
- {
1929
- todoId: string;
1930
- description: string;
1931
- status: string;
1932
- addedAt: Date;
1933
- }
1934
- >;
1935
- type TodoMarkedInProgress = Event<
1936
- 'TodoMarkedInProgress',
1937
- {
1938
- todoId: string;
1939
- markedAt: Date;
1940
- }
1941
- >;
1942
- type TodoMarkedComplete = Event<
1943
- 'TodoMarkedComplete',
1944
- {
1945
- todoId: string;
1946
- completedAt: Date;
1947
- }
1948
- >;
1949
- type TodoListSummary = State<
1950
- 'TodoListSummary',
1951
- {
1952
- summaryId: string;
1953
- totalTodos: number;
1954
- pendingCount: number;
1955
- inProgressCount: number;
1956
- completedCount: number;
1957
- completionPercentage: number;
1958
- }
1959
- >;
1960
- narrative('Todo List Summary', 'TODO-001', () => {
1961
- query('views completion summary', 'SUMMARY-001').server(() => {
1962
- specs('Summary Statistics', () => {
1963
- rule('summary shows overall todo list statistics', 'RULE-SUMMARY', () => {
1964
- example('calculates summary from multiple todos')
1965
- .given<TodoAdded>({
1966
- todoId: 'todo-001',
1967
- description: 'Buy groceries',
1968
- status: 'pending',
1969
- addedAt: new Date('2030-01-01T09:00:00.000Z'),
1970
- })
1971
- .and<TodoAdded>({
1972
- todoId: 'todo-002',
1973
- description: 'Write report',
1974
- status: 'pending',
1975
- addedAt: new Date('2030-01-01T09:10:00.000Z'),
1976
- })
1977
- .and<TodoMarkedInProgress>({ todoId: 'todo-001', markedAt: new Date('2030-01-01T10:00:00.000Z') })
1978
- .and<TodoMarkedComplete>({ todoId: 'todo-002', completedAt: new Date('2030-01-01T11:00:00.000Z') })
1979
- .then<TodoListSummary>({
1980
- summaryId: 'main-summary',
1981
- totalTodos: 2,
1982
- pendingCount: 0,
1983
- inProgressCount: 1,
1984
- completedCount: 1,
1985
- completionPercentage: 50,
1986
- });
1987
- });
1988
- });
1989
- });
1990
- });
1991
- `);
1992
- expect(code).not.toContain('.when({})');
1993
- expect(code).not.toContain('.when<');
1994
- });
1995
- describe('projection DSL generation', () => {
1996
- it('should generate fromSingletonProjection for singleton projections', async () => {
1997
- const modelWithSingletonProjection = {
1998
- variant: 'specs',
1999
- narratives: [
2000
- {
2001
- name: 'Todo Summary Flow',
2002
- id: 'TODO-SUMMARY',
2003
- slices: [
2004
- {
2005
- name: 'views todo summary',
2006
- id: 'SUMMARY-SLICE',
2007
- type: 'query',
2008
- client: {
2009
- description: 'Summary client',
2010
- },
2011
- server: {
2012
- description: 'Summary server',
2013
- data: [
2014
- {
2015
- target: {
2016
- type: 'State',
2017
- name: 'TodoListSummary',
2018
- },
2019
- origin: {
2020
- type: 'projection',
2021
- name: 'TodoSummary',
2022
- singleton: true,
2023
- },
2024
- },
2025
- ],
2026
- specs: {
2027
- name: 'Summary Rules',
2028
- rules: [],
2029
- },
2030
- },
2031
- },
2032
- ],
2033
- },
2034
- ],
2035
- messages: [
2036
- {
2037
- type: 'state',
2038
- name: 'TodoListSummary',
2039
- fields: [
2040
- { name: 'summaryId', type: 'string', required: true },
2041
- { name: 'totalTodos', type: 'number', required: true },
2042
- ],
2043
- metadata: { version: 1 },
2044
- },
2045
- ],
2046
- integrations: [],
2047
- };
2048
- const code = await modelToNarrative(modelWithSingletonProjection);
2049
- expect(code).toEqual(`import { data, narrative, query, source, specs } from '@auto-engineer/narrative';
2050
- import type { State } from '@auto-engineer/narrative';
2051
- type TodoListSummary = State<
2052
- 'TodoListSummary',
2053
- {
2054
- summaryId: string;
2055
- totalTodos: number;
2056
- }
2057
- >;
2058
- narrative('Todo Summary Flow', 'TODO-SUMMARY', () => {
2059
- query('views todo summary', 'SUMMARY-SLICE').server(() => {
2060
- data([source().state('TodoListSummary').fromSingletonProjection('TodoSummary')]);
2061
- specs('Summary Rules', () => {});
2062
- });
2063
- });
2064
- `);
2065
- });
2066
- it('should generate fromProjection with single idField for regular projections', async () => {
2067
- const modelWithRegularProjection = {
2068
- variant: 'specs',
2069
- narratives: [
2070
- {
2071
- name: 'Todo Flow',
2072
- id: 'TODO-FLOW',
2073
- slices: [
2074
- {
2075
- name: 'views todo',
2076
- id: 'TODO-SLICE',
2077
- type: 'query',
2078
- client: {
2079
- description: 'Todo client',
2080
- },
2081
- server: {
2082
- description: 'Todo server',
2083
- data: [
2084
- {
2085
- target: {
2086
- type: 'State',
2087
- name: 'TodoState',
2088
- },
2089
- origin: {
2090
- type: 'projection',
2091
- name: 'Todos',
2092
- idField: 'todoId',
2093
- },
2094
- },
2095
- ],
2096
- specs: {
2097
- name: 'Todo Rules',
2098
- rules: [],
2099
- },
2100
- },
2101
- },
2102
- ],
2103
- },
2104
- ],
2105
- messages: [
2106
- {
2107
- type: 'state',
2108
- name: 'TodoState',
2109
- fields: [
2110
- { name: 'todoId', type: 'string', required: true },
2111
- { name: 'description', type: 'string', required: true },
2112
- ],
2113
- metadata: { version: 1 },
2114
- },
2115
- ],
2116
- integrations: [],
2117
- };
2118
- const code = await modelToNarrative(modelWithRegularProjection);
2119
- expect(code).toEqual(`import { data, narrative, query, source, specs } from '@auto-engineer/narrative';
2120
- import type { State } from '@auto-engineer/narrative';
2121
- type TodoState = State<
2122
- 'TodoState',
2123
- {
2124
- todoId: string;
2125
- description: string;
2126
- }
2127
- >;
2128
- narrative('Todo Flow', 'TODO-FLOW', () => {
2129
- query('views todo', 'TODO-SLICE').server(() => {
2130
- data([source().state('TodoState').fromProjection('Todos', 'todoId')]);
2131
- specs('Todo Rules', () => {});
2132
- });
2133
- });
2134
- `);
2135
- });
2136
- it('should generate fromCompositeProjection with array idField for composite key projections', async () => {
2137
- const modelWithCompositeProjection = {
2138
- variant: 'specs',
2139
- narratives: [
2140
- {
2141
- name: 'User Project Flow',
2142
- id: 'USER-PROJECT-FLOW',
2143
- slices: [
2144
- {
2145
- name: 'views user project',
2146
- id: 'USER-PROJECT-SLICE',
2147
- type: 'query',
2148
- client: {
2149
- description: 'User project client',
2150
- },
2151
- server: {
2152
- description: 'User project server',
2153
- data: [
2154
- {
2155
- target: {
2156
- type: 'State',
2157
- name: 'UserProjectState',
2158
- },
2159
- origin: {
2160
- type: 'projection',
2161
- name: 'UserProjects',
2162
- idField: ['userId', 'projectId'],
2163
- },
2164
- },
2165
- ],
2166
- specs: {
2167
- name: 'User Project Rules',
2168
- rules: [],
2169
- },
2170
- },
2171
- },
2172
- ],
2173
- },
2174
- ],
2175
- messages: [
2176
- {
2177
- type: 'state',
2178
- name: 'UserProjectState',
2179
- fields: [
2180
- { name: 'userId', type: 'string', required: true },
2181
- { name: 'projectId', type: 'string', required: true },
2182
- { name: 'role', type: 'string', required: true },
2183
- ],
2184
- metadata: { version: 1 },
2185
- },
2186
- ],
2187
- integrations: [],
2188
- };
2189
- const code = await modelToNarrative(modelWithCompositeProjection);
2190
- expect(code).toEqual(`import { data, narrative, query, source, specs } from '@auto-engineer/narrative';
2191
- import type { State } from '@auto-engineer/narrative';
2192
- type UserProjectState = State<
2193
- 'UserProjectState',
2194
- {
2195
- userId: string;
2196
- projectId: string;
2197
- role: string;
2198
- }
2199
- >;
2200
- narrative('User Project Flow', 'USER-PROJECT-FLOW', () => {
2201
- query('views user project', 'USER-PROJECT-SLICE').server(() => {
2202
- data([source().state('UserProjectState').fromCompositeProjection('UserProjects', ['userId', 'projectId'])]);
2203
- specs('User Project Rules', () => {});
2204
- });
2205
- });
2206
- `);
2207
- });
2208
- it('should generate all three projection types in a single narrative', async () => {
2209
- const modelWithAllProjectionTypes = {
2210
- variant: 'specs',
2211
- narratives: [
2212
- {
2213
- name: 'All Projection Types',
2214
- id: 'ALL-PROJ',
2215
- slices: [
2216
- {
2217
- name: 'views summary',
2218
- id: 'SUMMARY-SLICE',
2219
- type: 'query',
2220
- client: {
2221
- description: 'Summary client',
2222
- },
2223
- server: {
2224
- description: 'Summary server',
2225
- data: [
2226
- {
2227
- target: {
2228
- type: 'State',
2229
- name: 'TodoListSummary',
2230
- },
2231
- origin: {
2232
- type: 'projection',
2233
- name: 'TodoSummary',
2234
- singleton: true,
2235
- },
2236
- },
2237
- ],
2238
- specs: {
2239
- name: 'Summary Rules',
2240
- rules: [],
2241
- },
2242
- },
2243
- },
2244
- {
2245
- name: 'views todo',
2246
- id: 'TODO-SLICE',
2247
- type: 'query',
2248
- client: {
2249
- description: 'Todo client',
2250
- },
2251
- server: {
2252
- description: 'Todo server',
2253
- data: [
2254
- {
2255
- target: {
2256
- type: 'State',
2257
- name: 'TodoState',
2258
- },
2259
- origin: {
2260
- type: 'projection',
2261
- name: 'Todos',
2262
- idField: 'todoId',
2263
- },
2264
- },
2265
- ],
2266
- specs: {
2267
- name: 'Todo Rules',
2268
- rules: [],
2269
- },
2270
- },
2271
- },
2272
- {
2273
- name: 'views user project todos',
2274
- id: 'USER-PROJECT-SLICE',
2275
- type: 'query',
2276
- client: {
2277
- description: 'User project client',
2278
- },
2279
- server: {
2280
- description: 'User project server',
2281
- data: [
2282
- {
2283
- target: {
2284
- type: 'State',
2285
- name: 'UserProjectTodos',
2286
- },
2287
- origin: {
2288
- type: 'projection',
2289
- name: 'UserProjectTodos',
2290
- idField: ['userId', 'projectId'],
2291
- },
2292
- },
2293
- ],
2294
- specs: {
2295
- name: 'User Project Rules',
2296
- rules: [],
2297
- },
2298
- },
2299
- },
2300
- ],
2301
- },
2302
- ],
2303
- messages: [
2304
- {
2305
- type: 'state',
2306
- name: 'TodoListSummary',
2307
- fields: [
2308
- { name: 'summaryId', type: 'string', required: true },
2309
- { name: 'totalTodos', type: 'number', required: true },
2310
- ],
2311
- metadata: { version: 1 },
2312
- },
2313
- {
2314
- type: 'state',
2315
- name: 'TodoState',
2316
- fields: [
2317
- { name: 'todoId', type: 'string', required: true },
2318
- { name: 'description', type: 'string', required: true },
2319
- ],
2320
- metadata: { version: 1 },
2321
- },
2322
- {
2323
- type: 'state',
2324
- name: 'UserProjectTodos',
2325
- fields: [
2326
- { name: 'userId', type: 'string', required: true },
2327
- { name: 'projectId', type: 'string', required: true },
2328
- { name: 'todos', type: 'Array<string>', required: true },
2329
- ],
2330
- metadata: { version: 1 },
2331
- },
2332
- ],
2333
- integrations: [],
2334
- };
2335
- const code = await modelToNarrative(modelWithAllProjectionTypes);
2336
- expect(code).toEqual(`import { data, narrative, query, source, specs } from '@auto-engineer/narrative';
2337
- import type { State } from '@auto-engineer/narrative';
2338
- type TodoListSummary = State<
2339
- 'TodoListSummary',
2340
- {
2341
- summaryId: string;
2342
- totalTodos: number;
2343
- }
2344
- >;
2345
- type TodoState = State<
2346
- 'TodoState',
2347
- {
2348
- todoId: string;
2349
- description: string;
2350
- }
2351
- >;
2352
- type UserProjectTodos = State<
2353
- 'UserProjectTodos',
2354
- {
2355
- userId: string;
2356
- projectId: string;
2357
- todos: string[];
2358
- }
2359
- >;
2360
- narrative('All Projection Types', 'ALL-PROJ', () => {
2361
- query('views summary', 'SUMMARY-SLICE').server(() => {
2362
- data([source().state('TodoListSummary').fromSingletonProjection('TodoSummary')]);
2363
- specs('Summary Rules', () => {});
2364
- });
2365
- query('views todo', 'TODO-SLICE').server(() => {
2366
- data([source().state('TodoState').fromProjection('Todos', 'todoId')]);
2367
- specs('Todo Rules', () => {});
2368
- });
2369
- query('views user project todos', 'USER-PROJECT-SLICE').server(() => {
2370
- data([source().state('UserProjectTodos').fromCompositeProjection('UserProjectTodos', ['userId', 'projectId'])]);
2371
- specs('User Project Rules', () => {});
2372
- });
2373
- });
2374
- `);
2375
- });
2376
- });
2377
- });
2378
- //# sourceMappingURL=model-to-narrative.specs.js.map