@auto-engineer/narrative 0.18.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/dist/src/data-narrative-builders.d.ts +13 -8
  4. package/dist/src/data-narrative-builders.d.ts.map +1 -1
  5. package/dist/src/data-narrative-builders.js +47 -20
  6. package/dist/src/data-narrative-builders.js.map +1 -1
  7. package/dist/src/id/addAutoIds.d.ts.map +1 -1
  8. package/dist/src/id/addAutoIds.js +18 -0
  9. package/dist/src/id/addAutoIds.js.map +1 -1
  10. package/dist/src/id/hasAllIds.d.ts.map +1 -1
  11. package/dist/src/id/hasAllIds.js +13 -1
  12. package/dist/src/id/hasAllIds.js.map +1 -1
  13. package/dist/src/schema.d.ts +231 -0
  14. package/dist/src/schema.d.ts.map +1 -1
  15. package/dist/src/schema.js +2 -0
  16. package/dist/src/schema.js.map +1 -1
  17. package/dist/src/transformers/model-to-narrative/generators/flow.d.ts.map +1 -1
  18. package/dist/src/transformers/model-to-narrative/generators/flow.js +10 -5
  19. package/dist/src/transformers/model-to-narrative/generators/flow.js.map +1 -1
  20. package/dist/src/types.d.ts +2 -0
  21. package/dist/src/types.d.ts.map +1 -1
  22. package/dist/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +4 -4
  24. package/src/data-narrative-builders.ts +57 -20
  25. package/src/getNarratives.specs.ts +69 -0
  26. package/src/id/addAutoIds.specs.ts +268 -0
  27. package/src/id/addAutoIds.ts +19 -0
  28. package/src/id/hasAllIds.specs.ts +223 -0
  29. package/src/id/hasAllIds.ts +13 -1
  30. package/src/model-to-narrative.specs.ts +176 -0
  31. package/src/schema.ts +2 -0
  32. package/src/transformers/model-to-narrative/generators/flow.ts +16 -4
  33. package/src/types.ts +2 -0
@@ -77,11 +77,30 @@ function processClientSpecs(slice: Slice): Slice {
77
77
  return modifiedSlice;
78
78
  }
79
79
 
80
+ function processDataItems(slice: Slice): Slice {
81
+ if (!('server' in slice) || !slice.server?.data || !Array.isArray(slice.server.data)) return slice;
82
+
83
+ const modifiedSlice = structuredClone(slice);
84
+ if ('server' in modifiedSlice && modifiedSlice.server?.data && Array.isArray(modifiedSlice.server.data)) {
85
+ modifiedSlice.server.data = modifiedSlice.server.data.map((item) => {
86
+ const itemCopy = { ...item };
87
+ ensureId(itemCopy);
88
+ if ('destination' in itemCopy && itemCopy._withState) {
89
+ itemCopy._withState = { ...itemCopy._withState };
90
+ ensureId(itemCopy._withState);
91
+ }
92
+ return itemCopy;
93
+ });
94
+ }
95
+ return modifiedSlice;
96
+ }
97
+
80
98
  function processSlice(slice: Slice): Slice {
81
99
  let sliceCopy = { ...slice };
82
100
  ensureId(sliceCopy);
83
101
  sliceCopy = processServerSpecs(sliceCopy);
84
102
  sliceCopy = processClientSpecs(sliceCopy);
103
+ sliceCopy = processDataItems(sliceCopy);
85
104
  return sliceCopy;
86
105
  }
87
106
 
@@ -451,6 +451,229 @@ describe('hasAllIds', () => {
451
451
  expect(hasAllIds(model)).toBe(true);
452
452
  });
453
453
 
454
+ describe('data item ID validation', () => {
455
+ it('should return false when data sink is missing an ID', () => {
456
+ const model: Model = {
457
+ variant: 'specs',
458
+ narratives: [
459
+ {
460
+ name: 'Test Flow',
461
+ id: 'FLOW-001',
462
+ slices: [
463
+ {
464
+ type: 'command',
465
+ name: 'Test slice',
466
+ id: 'SLICE-001',
467
+ client: { specs: [] },
468
+ server: {
469
+ description: 'Test server',
470
+ specs: [],
471
+ data: [
472
+ {
473
+ __type: 'sink',
474
+ target: { type: 'Event', name: 'TestEvent' },
475
+ destination: { type: 'stream', pattern: 'test-stream' },
476
+ },
477
+ ],
478
+ },
479
+ },
480
+ ],
481
+ },
482
+ ],
483
+ messages: [],
484
+ integrations: [],
485
+ modules: [],
486
+ };
487
+ expect(hasAllIds(model)).toBe(false);
488
+ });
489
+
490
+ it('should return false when data source is missing an ID', () => {
491
+ const model: Model = {
492
+ variant: 'specs',
493
+ narratives: [
494
+ {
495
+ name: 'Test Flow',
496
+ id: 'FLOW-001',
497
+ slices: [
498
+ {
499
+ type: 'query',
500
+ name: 'Test slice',
501
+ id: 'SLICE-001',
502
+ client: { specs: [] },
503
+ server: {
504
+ description: 'Test server',
505
+ specs: [],
506
+ data: [
507
+ {
508
+ __type: 'source',
509
+ target: { type: 'State', name: 'TestState' },
510
+ origin: { type: 'projection', name: 'TestProjection' },
511
+ },
512
+ ],
513
+ },
514
+ },
515
+ ],
516
+ },
517
+ ],
518
+ messages: [],
519
+ integrations: [],
520
+ modules: [],
521
+ };
522
+ expect(hasAllIds(model)).toBe(false);
523
+ });
524
+
525
+ it('should return false when nested _withState source is missing an ID', () => {
526
+ const model: Model = {
527
+ variant: 'specs',
528
+ narratives: [
529
+ {
530
+ name: 'Test Flow',
531
+ id: 'FLOW-001',
532
+ slices: [
533
+ {
534
+ type: 'command',
535
+ name: 'Test slice',
536
+ id: 'SLICE-001',
537
+ client: { specs: [] },
538
+ server: {
539
+ description: 'Test server',
540
+ specs: [],
541
+ data: [
542
+ {
543
+ __type: 'sink',
544
+ id: 'SINK-001',
545
+ target: { type: 'Command', name: 'TestCommand' },
546
+ destination: { type: 'stream', pattern: 'test-stream' },
547
+ _withState: {
548
+ target: { type: 'State', name: 'TestState' },
549
+ origin: { type: 'projection', name: 'TestProjection' },
550
+ },
551
+ },
552
+ ],
553
+ },
554
+ },
555
+ ],
556
+ },
557
+ ],
558
+ messages: [],
559
+ integrations: [],
560
+ modules: [],
561
+ };
562
+ expect(hasAllIds(model)).toBe(false);
563
+ });
564
+
565
+ it('should return true when all data items have IDs', () => {
566
+ const model: Model = {
567
+ variant: 'specs',
568
+ narratives: [
569
+ {
570
+ name: 'Test Flow',
571
+ id: 'FLOW-001',
572
+ slices: [
573
+ {
574
+ type: 'command',
575
+ name: 'Test slice',
576
+ id: 'SLICE-001',
577
+ client: { specs: [] },
578
+ server: {
579
+ description: 'Test server',
580
+ specs: [],
581
+ data: [
582
+ {
583
+ __type: 'sink',
584
+ id: 'SINK-001',
585
+ target: { type: 'Event', name: 'TestEvent' },
586
+ destination: { type: 'stream', pattern: 'test-stream' },
587
+ },
588
+ {
589
+ __type: 'source',
590
+ id: 'SOURCE-001',
591
+ target: { type: 'State', name: 'TestState' },
592
+ origin: { type: 'projection', name: 'TestProjection' },
593
+ },
594
+ ],
595
+ },
596
+ },
597
+ ],
598
+ },
599
+ ],
600
+ messages: [],
601
+ integrations: [],
602
+ modules: [],
603
+ };
604
+ expect(hasAllIds(model)).toBe(true);
605
+ });
606
+
607
+ it('should return true when sink with _withState both have IDs', () => {
608
+ const model: Model = {
609
+ variant: 'specs',
610
+ narratives: [
611
+ {
612
+ name: 'Test Flow',
613
+ id: 'FLOW-001',
614
+ slices: [
615
+ {
616
+ type: 'command',
617
+ name: 'Test slice',
618
+ id: 'SLICE-001',
619
+ client: { specs: [] },
620
+ server: {
621
+ description: 'Test server',
622
+ specs: [],
623
+ data: [
624
+ {
625
+ __type: 'sink',
626
+ id: 'SINK-001',
627
+ target: { type: 'Command', name: 'TestCommand' },
628
+ destination: { type: 'stream', pattern: 'test-stream' },
629
+ _withState: {
630
+ id: 'SOURCE-001',
631
+ target: { type: 'State', name: 'TestState' },
632
+ origin: { type: 'projection', name: 'TestProjection' },
633
+ },
634
+ },
635
+ ],
636
+ },
637
+ },
638
+ ],
639
+ },
640
+ ],
641
+ messages: [],
642
+ integrations: [],
643
+ modules: [],
644
+ };
645
+ expect(hasAllIds(model)).toBe(true);
646
+ });
647
+
648
+ it('should return true when slice has no data array', () => {
649
+ const model: Model = {
650
+ variant: 'specs',
651
+ narratives: [
652
+ {
653
+ name: 'Test Flow',
654
+ id: 'FLOW-001',
655
+ slices: [
656
+ {
657
+ type: 'command',
658
+ name: 'Test slice',
659
+ id: 'SLICE-001',
660
+ client: { specs: [] },
661
+ server: {
662
+ description: 'Test server',
663
+ specs: [],
664
+ },
665
+ },
666
+ ],
667
+ },
668
+ ],
669
+ messages: [],
670
+ integrations: [],
671
+ modules: [],
672
+ };
673
+ expect(hasAllIds(model)).toBe(true);
674
+ });
675
+ });
676
+
454
677
  describe('module ID validation', () => {
455
678
  it('should return true when all modules have IDs', () => {
456
679
  const model: Model = {
@@ -40,8 +40,20 @@ function hasClientSpecIds(slice: Slice): boolean {
40
40
  return hasClientSpecNodeIds(slice.client.specs);
41
41
  }
42
42
 
43
+ function hasDataIds(slice: Slice): boolean {
44
+ if (!('server' in slice) || !slice.server?.data || !Array.isArray(slice.server.data)) return true;
45
+
46
+ return slice.server.data.every((item) => {
47
+ if (!hasValidId(item)) return false;
48
+ if ('destination' in item && item._withState) {
49
+ return hasValidId(item._withState);
50
+ }
51
+ return true;
52
+ });
53
+ }
54
+
43
55
  function hasSliceIds(slice: Slice): boolean {
44
- return hasValidId(slice) && hasServerSpecIds(slice) && hasClientSpecIds(slice);
56
+ return hasValidId(slice) && hasServerSpecIds(slice) && hasClientSpecIds(slice) && hasDataIds(slice);
45
57
  }
46
58
 
47
59
  function hasModuleIds(modules: Module[]): boolean {
@@ -2938,4 +2938,180 @@ narrative('All Projection Types', 'ALL-PROJ', () => {
2938
2938
  }
2939
2939
  });
2940
2940
  });
2941
+
2942
+ describe('data item IDs', () => {
2943
+ it('should generate sink id when provided', async () => {
2944
+ const modelWithSinkId: Model = {
2945
+ variant: 'specs',
2946
+ narratives: [
2947
+ {
2948
+ name: 'Order Flow',
2949
+ id: 'ORDER-FLOW',
2950
+ slices: [
2951
+ {
2952
+ name: 'places order',
2953
+ id: 'ORDER-SLICE',
2954
+ type: 'command',
2955
+ client: { specs: [] },
2956
+ server: {
2957
+ description: 'Order server',
2958
+ data: [
2959
+ {
2960
+ id: 'SINK-001',
2961
+ target: { type: 'Event', name: 'OrderPlaced' },
2962
+ destination: { type: 'stream', pattern: 'orders-stream' },
2963
+ },
2964
+ ],
2965
+ specs: [],
2966
+ },
2967
+ },
2968
+ ],
2969
+ },
2970
+ ],
2971
+ messages: [
2972
+ {
2973
+ type: 'event',
2974
+ source: 'internal',
2975
+ name: 'OrderPlaced',
2976
+ fields: [{ name: 'orderId', type: 'string', required: true }],
2977
+ },
2978
+ ],
2979
+ integrations: [],
2980
+ modules: [],
2981
+ };
2982
+
2983
+ const code = getCode(await modelToNarrative(modelWithSinkId));
2984
+ expect(code).toContain("sink('SINK-001').event('OrderPlaced').toStream('orders-stream')");
2985
+ });
2986
+
2987
+ it('should generate source id when provided', async () => {
2988
+ const modelWithSourceId: Model = {
2989
+ variant: 'specs',
2990
+ narratives: [
2991
+ {
2992
+ name: 'Order Flow',
2993
+ id: 'ORDER-FLOW',
2994
+ slices: [
2995
+ {
2996
+ name: 'views order',
2997
+ id: 'ORDER-SLICE',
2998
+ type: 'query',
2999
+ client: { specs: [] },
3000
+ server: {
3001
+ description: 'Order server',
3002
+ data: [
3003
+ {
3004
+ id: 'SOURCE-001',
3005
+ target: { type: 'State', name: 'OrderState' },
3006
+ origin: { type: 'projection', name: 'Orders', idField: 'orderId' },
3007
+ },
3008
+ ],
3009
+ specs: [],
3010
+ },
3011
+ },
3012
+ ],
3013
+ },
3014
+ ],
3015
+ messages: [
3016
+ {
3017
+ type: 'state',
3018
+ name: 'OrderState',
3019
+ fields: [{ name: 'orderId', type: 'string', required: true }],
3020
+ },
3021
+ ],
3022
+ integrations: [],
3023
+ modules: [],
3024
+ };
3025
+
3026
+ const code = getCode(await modelToNarrative(modelWithSourceId));
3027
+ expect(code).toContain("source('SOURCE-001').state('OrderState').fromProjection('Orders', 'orderId')");
3028
+ });
3029
+
3030
+ it('should not generate id when not provided', async () => {
3031
+ const modelWithoutId: Model = {
3032
+ variant: 'specs',
3033
+ narratives: [
3034
+ {
3035
+ name: 'Order Flow',
3036
+ id: 'ORDER-FLOW',
3037
+ slices: [
3038
+ {
3039
+ name: 'places order',
3040
+ id: 'ORDER-SLICE',
3041
+ type: 'command',
3042
+ client: { specs: [] },
3043
+ server: {
3044
+ description: 'Order server',
3045
+ data: [
3046
+ {
3047
+ target: { type: 'Event', name: 'OrderPlaced' },
3048
+ destination: { type: 'stream', pattern: 'orders-stream' },
3049
+ },
3050
+ ],
3051
+ specs: [],
3052
+ },
3053
+ },
3054
+ ],
3055
+ },
3056
+ ],
3057
+ messages: [
3058
+ {
3059
+ type: 'event',
3060
+ source: 'internal',
3061
+ name: 'OrderPlaced',
3062
+ fields: [{ name: 'orderId', type: 'string', required: true }],
3063
+ },
3064
+ ],
3065
+ integrations: [],
3066
+ modules: [],
3067
+ };
3068
+
3069
+ const code = getCode(await modelToNarrative(modelWithoutId));
3070
+ expect(code).toContain("sink().event('OrderPlaced').toStream('orders-stream')");
3071
+ expect(code).not.toContain("sink('')");
3072
+ });
3073
+
3074
+ it('should generate _additionalInstructions on source items', async () => {
3075
+ const modelWithSourceInstructions: Model = {
3076
+ variant: 'specs',
3077
+ narratives: [
3078
+ {
3079
+ name: 'Order Flow',
3080
+ id: 'ORDER-FLOW',
3081
+ slices: [
3082
+ {
3083
+ name: 'views order',
3084
+ id: 'ORDER-SLICE',
3085
+ type: 'query',
3086
+ client: { specs: [] },
3087
+ server: {
3088
+ description: 'Order server',
3089
+ data: [
3090
+ {
3091
+ target: { type: 'State', name: 'OrderState' },
3092
+ origin: { type: 'projection', name: 'Orders', idField: 'orderId' },
3093
+ _additionalInstructions: 'Filter by active orders only',
3094
+ },
3095
+ ],
3096
+ specs: [],
3097
+ },
3098
+ },
3099
+ ],
3100
+ },
3101
+ ],
3102
+ messages: [
3103
+ {
3104
+ type: 'state',
3105
+ name: 'OrderState',
3106
+ fields: [{ name: 'orderId', type: 'string', required: true }],
3107
+ },
3108
+ ],
3109
+ integrations: [],
3110
+ modules: [],
3111
+ };
3112
+
3113
+ const code = getCode(await modelToNarrative(modelWithSourceInstructions));
3114
+ expect(code).toContain(".additionalInstructions('Filter by active orders only')");
3115
+ });
3116
+ });
2941
3117
  });
package/src/schema.ts CHANGED
@@ -112,6 +112,7 @@ export const OriginSchema = z
112
112
 
113
113
  const DataSinkSchema = z
114
114
  .object({
115
+ id: z.string().optional().describe('Optional unique identifier for the data sink'),
115
116
  target: MessageTargetSchema,
116
117
  destination: DestinationSchema,
117
118
  transform: z.string().optional().describe('Optional transformation function name'),
@@ -125,6 +126,7 @@ const DataSinkSchema = z
125
126
 
126
127
  const DataSourceSchema = z
127
128
  .object({
129
+ id: z.string().optional().describe('Optional unique identifier for the data source'),
128
130
  target: MessageTargetSchema,
129
131
  origin: OriginSchema,
130
132
  transform: z.string().optional().describe('Optional transformation function name'),
@@ -84,13 +84,15 @@ function buildInitialChain(
84
84
  ts: typeof import('typescript'),
85
85
  f: tsNS.NodeFactory,
86
86
  target: DataSinkItem['target'] | DataSourceItem['target'],
87
+ id?: string,
87
88
  ): tsNS.Expression {
88
89
  const op = target.type === 'Event' ? 'event' : target.type === 'Command' ? 'command' : 'state';
90
+ const sinkOrSourceArgs: tsNS.Expression[] = id != null && id !== '' ? [f.createStringLiteral(id)] : [];
89
91
  return f.createCallExpression(
90
92
  f.createPropertyAccessExpression(
91
93
  target.type === 'State'
92
- ? f.createCallExpression(f.createIdentifier('source'), undefined, [])
93
- : f.createCallExpression(f.createIdentifier('sink'), undefined, []),
94
+ ? f.createCallExpression(f.createIdentifier('source'), undefined, sinkOrSourceArgs)
95
+ : f.createCallExpression(f.createIdentifier('sink'), undefined, sinkOrSourceArgs),
94
96
  ts.factory.createIdentifier(op),
95
97
  ),
96
98
  undefined,
@@ -309,7 +311,7 @@ function buildSingleDataItem(
309
311
  f: tsNS.NodeFactory,
310
312
  it: DataSinkItem | DataSourceItem,
311
313
  ): tsNS.Expression {
312
- let chain = buildInitialChain(ts, f, it.target);
314
+ let chain = buildInitialChain(ts, f, it.target, it.id);
313
315
 
314
316
  if ('destination' in it) {
315
317
  const sinkItem = it;
@@ -333,11 +335,13 @@ function buildSingleDataItem(
333
335
  }
334
336
  } else if ('origin' in it) {
335
337
  const sourceItem = it;
338
+ const sourceArgs: tsNS.Expression[] =
339
+ sourceItem.id != null && sourceItem.id !== '' ? [f.createStringLiteral(sourceItem.id)] : [];
336
340
  chain = f.createCallExpression(
337
341
  f.createPropertyAccessExpression(
338
342
  f.createCallExpression(
339
343
  f.createPropertyAccessExpression(
340
- f.createCallExpression(f.createIdentifier('source'), undefined, []),
344
+ f.createCallExpression(f.createIdentifier('source'), undefined, sourceArgs),
341
345
  f.createIdentifier('state'),
342
346
  ),
343
347
  undefined,
@@ -348,6 +352,14 @@ function buildSingleDataItem(
348
352
  undefined,
349
353
  buildOriginArgs(ts, f, sourceItem.origin),
350
354
  );
355
+
356
+ if (sourceItem._additionalInstructions != null && sourceItem._additionalInstructions !== '') {
357
+ chain = f.createCallExpression(
358
+ f.createPropertyAccessExpression(chain, f.createIdentifier('additionalInstructions')),
359
+ undefined,
360
+ [f.createStringLiteral(sourceItem._additionalInstructions)],
361
+ );
362
+ }
351
363
  }
352
364
 
353
365
  return chain;
package/src/types.ts CHANGED
@@ -130,6 +130,7 @@ export const createApiOrigin = (endpoint: string, method?: string): ApiOrigin =>
130
130
  export const createIntegrationOrigin = (systems: string[]): IntegrationOrigin => ({ type: 'integration', systems });
131
131
 
132
132
  export interface DataSink {
133
+ id?: string;
133
134
  target: MessageTarget;
134
135
  destination: Destination;
135
136
  transform?: string;
@@ -138,6 +139,7 @@ export interface DataSink {
138
139
  }
139
140
 
140
141
  export interface DataSource {
142
+ id?: string;
141
143
  target: MessageTarget;
142
144
  origin: Origin;
143
145
  transform?: string;