@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +25 -0
- package/dist/src/data-narrative-builders.d.ts +13 -8
- package/dist/src/data-narrative-builders.d.ts.map +1 -1
- package/dist/src/data-narrative-builders.js +47 -20
- package/dist/src/data-narrative-builders.js.map +1 -1
- package/dist/src/id/addAutoIds.d.ts.map +1 -1
- package/dist/src/id/addAutoIds.js +18 -0
- package/dist/src/id/addAutoIds.js.map +1 -1
- package/dist/src/id/hasAllIds.d.ts.map +1 -1
- package/dist/src/id/hasAllIds.js +13 -1
- package/dist/src/id/hasAllIds.js.map +1 -1
- package/dist/src/schema.d.ts +231 -0
- package/dist/src/schema.d.ts.map +1 -1
- package/dist/src/schema.js +2 -0
- package/dist/src/schema.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/flow.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/flow.js +10 -5
- package/dist/src/transformers/model-to-narrative/generators/flow.js.map +1 -1
- package/dist/src/types.d.ts +2 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/data-narrative-builders.ts +57 -20
- package/src/getNarratives.specs.ts +69 -0
- package/src/id/addAutoIds.specs.ts +268 -0
- package/src/id/addAutoIds.ts +19 -0
- package/src/id/hasAllIds.specs.ts +223 -0
- package/src/id/hasAllIds.ts +13 -1
- package/src/model-to-narrative.specs.ts +176 -0
- package/src/schema.ts +2 -0
- package/src/transformers/model-to-narrative/generators/flow.ts +16 -4
- package/src/types.ts +2 -0
package/package.json
CHANGED
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"typescript": "^5.9.2",
|
|
24
24
|
"zod": "^3.22.4",
|
|
25
25
|
"zod-to-json-schema": "^3.22.3",
|
|
26
|
-
"@auto-engineer/file-store": "0.
|
|
27
|
-
"@auto-engineer/id": "0.
|
|
28
|
-
"@auto-engineer/message-bus": "0.
|
|
26
|
+
"@auto-engineer/file-store": "0.19.1",
|
|
27
|
+
"@auto-engineer/id": "0.19.1",
|
|
28
|
+
"@auto-engineer/message-bus": "0.19.1"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^20.0.0",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
|
-
"version": "0.
|
|
38
|
+
"version": "0.19.1",
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsx scripts/build.ts",
|
|
41
41
|
"test": "vitest run --reporter=dot",
|
|
@@ -55,6 +55,7 @@ export interface FieldSelector {
|
|
|
55
55
|
abstract class MessageTargetBuilder<TResult> {
|
|
56
56
|
protected target: Partial<MessageTarget> = {};
|
|
57
57
|
protected instructions?: string;
|
|
58
|
+
protected itemId?: string;
|
|
58
59
|
|
|
59
60
|
fields(selector: FieldSelector): this {
|
|
60
61
|
this.target.fields = selector as Record<string, unknown>;
|
|
@@ -71,13 +72,15 @@ abstract class MessageTargetBuilder<TResult> {
|
|
|
71
72
|
|
|
72
73
|
// Event sink builder
|
|
73
74
|
export class EventSinkBuilder extends MessageTargetBuilder<DataSinkItem> {
|
|
74
|
-
constructor(name: string) {
|
|
75
|
+
constructor(name: string, id?: string) {
|
|
75
76
|
super();
|
|
76
77
|
this.target = { type: 'Event', name };
|
|
78
|
+
this.itemId = id;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
toStream(pattern: string): ChainableSink {
|
|
80
82
|
const sinkItem: DataSinkItem = {
|
|
83
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
81
84
|
target: this.target as MessageTarget,
|
|
82
85
|
destination: { type: 'stream', pattern },
|
|
83
86
|
__type: 'sink' as const,
|
|
@@ -88,6 +91,7 @@ export class EventSinkBuilder extends MessageTargetBuilder<DataSinkItem> {
|
|
|
88
91
|
|
|
89
92
|
toIntegration(...systems: Integration[]): ChainableSink {
|
|
90
93
|
const sinkItem: DataSinkItem = {
|
|
94
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
91
95
|
target: this.target as MessageTarget,
|
|
92
96
|
destination: {
|
|
93
97
|
type: 'integration',
|
|
@@ -101,6 +105,7 @@ export class EventSinkBuilder extends MessageTargetBuilder<DataSinkItem> {
|
|
|
101
105
|
|
|
102
106
|
toDatabase(collection: string): ChainableSink {
|
|
103
107
|
const sinkItem: DataSinkItem = {
|
|
108
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
104
109
|
target: this.target as MessageTarget,
|
|
105
110
|
destination: { type: 'database', collection },
|
|
106
111
|
__type: 'sink' as const,
|
|
@@ -111,6 +116,7 @@ export class EventSinkBuilder extends MessageTargetBuilder<DataSinkItem> {
|
|
|
111
116
|
|
|
112
117
|
toTopic(name: string): ChainableSink {
|
|
113
118
|
const sinkItem: DataSinkItem = {
|
|
119
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
114
120
|
target: this.target as MessageTarget,
|
|
115
121
|
destination: { type: 'topic', name },
|
|
116
122
|
__type: 'sink' as const,
|
|
@@ -128,9 +134,10 @@ export class EventSinkBuilder extends MessageTargetBuilder<DataSinkItem> {
|
|
|
128
134
|
export class CommandSinkBuilder extends MessageTargetBuilder<DataSinkItem> {
|
|
129
135
|
private stateSource?: DataSourceItem;
|
|
130
136
|
|
|
131
|
-
constructor(name: string) {
|
|
137
|
+
constructor(name: string, id?: string) {
|
|
132
138
|
super();
|
|
133
139
|
this.target = { type: 'Command', name };
|
|
140
|
+
this.itemId = id;
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
withState(source: DataSourceItem | ChainableSource): this {
|
|
@@ -144,6 +151,7 @@ export class CommandSinkBuilder extends MessageTargetBuilder<DataSinkItem> {
|
|
|
144
151
|
messageType: 'command' | 'query' | 'reaction',
|
|
145
152
|
): ChainableSink {
|
|
146
153
|
const sinkItem: DataSinkItem = {
|
|
154
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
147
155
|
target: this.target as MessageTarget,
|
|
148
156
|
destination: {
|
|
149
157
|
type: 'integration',
|
|
@@ -167,6 +175,7 @@ export class CommandSinkBuilder extends MessageTargetBuilder<DataSinkItem> {
|
|
|
167
175
|
|
|
168
176
|
hints(hint: string): ChainableSink {
|
|
169
177
|
const sinkItem: DataSinkItem = {
|
|
178
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
170
179
|
target: this.target as MessageTarget,
|
|
171
180
|
destination: { type: 'integration', systems: [] },
|
|
172
181
|
transform: hint,
|
|
@@ -179,6 +188,7 @@ export class CommandSinkBuilder extends MessageTargetBuilder<DataSinkItem> {
|
|
|
179
188
|
|
|
180
189
|
toDatabase(collection: string): ChainableSink {
|
|
181
190
|
const sinkItem: DataSinkItem = {
|
|
191
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
182
192
|
target: this.target as MessageTarget,
|
|
183
193
|
destination: { type: 'database', collection },
|
|
184
194
|
__type: 'sink' as const,
|
|
@@ -190,6 +200,7 @@ export class CommandSinkBuilder extends MessageTargetBuilder<DataSinkItem> {
|
|
|
190
200
|
|
|
191
201
|
toTopic(name: string): ChainableSink {
|
|
192
202
|
const sinkItem: DataSinkItem = {
|
|
203
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
193
204
|
target: this.target as MessageTarget,
|
|
194
205
|
destination: { type: 'topic', name },
|
|
195
206
|
__type: 'sink' as const,
|
|
@@ -206,13 +217,15 @@ export class CommandSinkBuilder extends MessageTargetBuilder<DataSinkItem> {
|
|
|
206
217
|
|
|
207
218
|
// State sink builder
|
|
208
219
|
export class StateSinkBuilder extends MessageTargetBuilder<DataSinkItem> {
|
|
209
|
-
constructor(name: string) {
|
|
220
|
+
constructor(name: string, id?: string) {
|
|
210
221
|
super();
|
|
211
222
|
this.target = { type: 'State', name };
|
|
223
|
+
this.itemId = id;
|
|
212
224
|
}
|
|
213
225
|
|
|
214
226
|
toDatabase(collection: string): ChainableSink {
|
|
215
227
|
const sinkItem: DataSinkItem = {
|
|
228
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
216
229
|
target: this.target as MessageTarget,
|
|
217
230
|
destination: { type: 'database', collection },
|
|
218
231
|
__type: 'sink' as const,
|
|
@@ -223,6 +236,7 @@ export class StateSinkBuilder extends MessageTargetBuilder<DataSinkItem> {
|
|
|
223
236
|
|
|
224
237
|
toStream(pattern: string): ChainableSink {
|
|
225
238
|
const sinkItem: DataSinkItem = {
|
|
239
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
226
240
|
target: this.target as MessageTarget,
|
|
227
241
|
destination: { type: 'stream', pattern },
|
|
228
242
|
__type: 'sink' as const,
|
|
@@ -238,13 +252,15 @@ export class StateSinkBuilder extends MessageTargetBuilder<DataSinkItem> {
|
|
|
238
252
|
|
|
239
253
|
// State source builder
|
|
240
254
|
export class StateSourceBuilder<S = unknown> extends MessageTargetBuilder<DataSourceItem> {
|
|
241
|
-
constructor(name: string) {
|
|
255
|
+
constructor(name: string, id?: string) {
|
|
242
256
|
super();
|
|
243
257
|
this.target = { type: 'State', name };
|
|
258
|
+
this.itemId = id;
|
|
244
259
|
}
|
|
245
260
|
|
|
246
261
|
fromSingletonProjection(name: string): ChainableSource {
|
|
247
262
|
const sourceItem: DataSourceItem = {
|
|
263
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
248
264
|
target: this.target as MessageTarget,
|
|
249
265
|
origin: { type: 'projection', name, singleton: true },
|
|
250
266
|
__type: 'source' as const,
|
|
@@ -259,6 +275,7 @@ export class StateSourceBuilder<S = unknown> extends MessageTargetBuilder<DataSo
|
|
|
259
275
|
: string,
|
|
260
276
|
>(name: string, idField: K): ChainableSource {
|
|
261
277
|
const sourceItem: DataSourceItem = {
|
|
278
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
262
279
|
target: this.target as MessageTarget,
|
|
263
280
|
origin: { type: 'projection', name, idField: idField as string },
|
|
264
281
|
__type: 'source' as const,
|
|
@@ -273,6 +290,7 @@ export class StateSourceBuilder<S = unknown> extends MessageTargetBuilder<DataSo
|
|
|
273
290
|
: string,
|
|
274
291
|
>(name: string, idFields: K[]): ChainableSource {
|
|
275
292
|
const sourceItem: DataSourceItem = {
|
|
293
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
276
294
|
target: this.target as MessageTarget,
|
|
277
295
|
origin: { type: 'projection', name, idField: idFields as string[] },
|
|
278
296
|
__type: 'source' as const,
|
|
@@ -283,6 +301,7 @@ export class StateSourceBuilder<S = unknown> extends MessageTargetBuilder<DataSo
|
|
|
283
301
|
|
|
284
302
|
fromReadModel(name: string): ChainableSource {
|
|
285
303
|
const sourceItem: DataSourceItem = {
|
|
304
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
286
305
|
target: this.target as MessageTarget,
|
|
287
306
|
origin: { type: 'readModel', name },
|
|
288
307
|
__type: 'source' as const,
|
|
@@ -293,6 +312,7 @@ export class StateSourceBuilder<S = unknown> extends MessageTargetBuilder<DataSo
|
|
|
293
312
|
|
|
294
313
|
fromDatabase(collection: string, query?: Record<string, unknown>): ChainableSource {
|
|
295
314
|
const sourceItem: DataSourceItem = {
|
|
315
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
296
316
|
target: this.target as MessageTarget,
|
|
297
317
|
origin: { type: 'database', collection, query },
|
|
298
318
|
__type: 'source' as const,
|
|
@@ -303,6 +323,7 @@ export class StateSourceBuilder<S = unknown> extends MessageTargetBuilder<DataSo
|
|
|
303
323
|
|
|
304
324
|
fromApi(endpoint: string, method?: string): ChainableSource {
|
|
305
325
|
const sourceItem: DataSourceItem = {
|
|
326
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
306
327
|
target: this.target as MessageTarget,
|
|
307
328
|
origin: { type: 'api', endpoint, method },
|
|
308
329
|
__type: 'source' as const,
|
|
@@ -313,6 +334,7 @@ export class StateSourceBuilder<S = unknown> extends MessageTargetBuilder<DataSo
|
|
|
313
334
|
|
|
314
335
|
fromIntegration(...systems: (Integration | string)[]): ChainableSource {
|
|
315
336
|
const sourceItem: DataSourceItem = {
|
|
337
|
+
...(this.itemId != null && this.itemId !== '' && { id: this.itemId }),
|
|
316
338
|
target: this.target as MessageTarget,
|
|
317
339
|
origin: createIntegrationOrigin(
|
|
318
340
|
systems.map((s) => (typeof s === 'string' ? s : integrationExportRegistry.getExportNameForIntegration(s))),
|
|
@@ -348,14 +370,20 @@ function isValidBuilderResult(obj: unknown): obj is BuilderResult {
|
|
|
348
370
|
}
|
|
349
371
|
|
|
350
372
|
export class DataSinkBuilder {
|
|
373
|
+
private readonly builderId?: string;
|
|
374
|
+
|
|
375
|
+
constructor(id?: string) {
|
|
376
|
+
this.builderId = id;
|
|
377
|
+
}
|
|
378
|
+
|
|
351
379
|
event(nameOrBuilder: string | BuilderResult): EventSinkBuilder {
|
|
352
380
|
if (typeof nameOrBuilder === 'string') {
|
|
353
|
-
return new EventSinkBuilder(nameOrBuilder);
|
|
381
|
+
return new EventSinkBuilder(nameOrBuilder, this.builderId);
|
|
354
382
|
}
|
|
355
383
|
|
|
356
384
|
// Handle event builder function
|
|
357
385
|
if (isValidBuilderResult(nameOrBuilder) && nameOrBuilder.__messageCategory === 'event') {
|
|
358
|
-
return new EventSinkBuilder(nameOrBuilder.type);
|
|
386
|
+
return new EventSinkBuilder(nameOrBuilder.type, this.builderId);
|
|
359
387
|
}
|
|
360
388
|
|
|
361
389
|
throw new Error('Invalid event parameter - must be a string or event builder function');
|
|
@@ -363,12 +391,12 @@ export class DataSinkBuilder {
|
|
|
363
391
|
|
|
364
392
|
command(nameOrBuilder: string | BuilderResult): CommandSinkBuilder {
|
|
365
393
|
if (typeof nameOrBuilder === 'string') {
|
|
366
|
-
return new CommandSinkBuilder(nameOrBuilder);
|
|
394
|
+
return new CommandSinkBuilder(nameOrBuilder, this.builderId);
|
|
367
395
|
}
|
|
368
396
|
|
|
369
397
|
// Handle command builder function
|
|
370
398
|
if (isValidBuilderResult(nameOrBuilder) && nameOrBuilder.__messageCategory === 'command') {
|
|
371
|
-
return new CommandSinkBuilder(nameOrBuilder.type);
|
|
399
|
+
return new CommandSinkBuilder(nameOrBuilder.type, this.builderId);
|
|
372
400
|
}
|
|
373
401
|
|
|
374
402
|
throw new Error('Invalid command parameter - must be a string or command builder function');
|
|
@@ -376,12 +404,12 @@ export class DataSinkBuilder {
|
|
|
376
404
|
|
|
377
405
|
state(nameOrBuilder: string | BuilderResult): StateSinkBuilder {
|
|
378
406
|
if (typeof nameOrBuilder === 'string') {
|
|
379
|
-
return new StateSinkBuilder(nameOrBuilder);
|
|
407
|
+
return new StateSinkBuilder(nameOrBuilder, this.builderId);
|
|
380
408
|
}
|
|
381
409
|
|
|
382
410
|
// Handle state builder function
|
|
383
411
|
if (isValidBuilderResult(nameOrBuilder) && nameOrBuilder.__messageCategory === 'state') {
|
|
384
|
-
return new StateSinkBuilder(nameOrBuilder.type);
|
|
412
|
+
return new StateSinkBuilder(nameOrBuilder.type, this.builderId);
|
|
385
413
|
}
|
|
386
414
|
|
|
387
415
|
throw new Error('Invalid state parameter - must be a string or state builder function');
|
|
@@ -389,16 +417,22 @@ export class DataSinkBuilder {
|
|
|
389
417
|
}
|
|
390
418
|
|
|
391
419
|
export class DataSourceBuilder {
|
|
420
|
+
private readonly builderId?: string;
|
|
421
|
+
|
|
422
|
+
constructor(id?: string) {
|
|
423
|
+
this.builderId = id;
|
|
424
|
+
}
|
|
425
|
+
|
|
392
426
|
state<S extends import('./types').State<string, DefaultRecord> = import('./types').State<string, DefaultRecord>>(
|
|
393
427
|
nameOrBuilder: string | BuilderResult,
|
|
394
428
|
): StateSourceBuilder<S> {
|
|
395
429
|
if (typeof nameOrBuilder === 'string') {
|
|
396
|
-
return new StateSourceBuilder<S>(nameOrBuilder);
|
|
430
|
+
return new StateSourceBuilder<S>(nameOrBuilder, this.builderId);
|
|
397
431
|
}
|
|
398
432
|
|
|
399
433
|
// Handle state builder function
|
|
400
434
|
if (isValidBuilderResult(nameOrBuilder) && nameOrBuilder.__messageCategory === 'state') {
|
|
401
|
-
return new StateSourceBuilder<S>(nameOrBuilder.type);
|
|
435
|
+
return new StateSourceBuilder<S>(nameOrBuilder.type, this.builderId);
|
|
402
436
|
}
|
|
403
437
|
|
|
404
438
|
throw new Error('Invalid state parameter - must be a string or state builder function');
|
|
@@ -406,11 +440,14 @@ export class DataSourceBuilder {
|
|
|
406
440
|
}
|
|
407
441
|
|
|
408
442
|
// Factory functions for cleaner API
|
|
409
|
-
export const sink = () => new DataSinkBuilder();
|
|
410
|
-
export const source = () => new DataSourceBuilder();
|
|
443
|
+
export const sink = (id?: string) => new DataSinkBuilder(id);
|
|
444
|
+
export const source = (id?: string) => new DataSourceBuilder(id);
|
|
411
445
|
|
|
412
446
|
// Type-safe sink function that accepts builder results
|
|
413
|
-
export function typedSink(
|
|
447
|
+
export function typedSink(
|
|
448
|
+
builderResult: BuilderResult,
|
|
449
|
+
id?: string,
|
|
450
|
+
): EventSinkBuilder | CommandSinkBuilder | StateSinkBuilder {
|
|
414
451
|
if (!isValidBuilderResult(builderResult)) {
|
|
415
452
|
throw new Error('Invalid builder result - must be from Events, Commands, or State builders');
|
|
416
453
|
}
|
|
@@ -419,11 +456,11 @@ export function typedSink(builderResult: BuilderResult): EventSinkBuilder | Comm
|
|
|
419
456
|
|
|
420
457
|
switch (__messageCategory) {
|
|
421
458
|
case 'event':
|
|
422
|
-
return new EventSinkBuilder(messageName);
|
|
459
|
+
return new EventSinkBuilder(messageName, id);
|
|
423
460
|
case 'command':
|
|
424
|
-
return new CommandSinkBuilder(messageName);
|
|
461
|
+
return new CommandSinkBuilder(messageName, id);
|
|
425
462
|
case 'state':
|
|
426
|
-
return new StateSinkBuilder(messageName);
|
|
463
|
+
return new StateSinkBuilder(messageName, id);
|
|
427
464
|
default: {
|
|
428
465
|
const category: never = __messageCategory;
|
|
429
466
|
throw new Error(`Unknown message category: ${String(category)}`);
|
|
@@ -434,7 +471,7 @@ export function typedSink(builderResult: BuilderResult): EventSinkBuilder | Comm
|
|
|
434
471
|
// Type-safe source function that accepts builder results
|
|
435
472
|
export function typedSource<
|
|
436
473
|
S extends import('./types').State<string, DefaultRecord> = import('./types').State<string, DefaultRecord>,
|
|
437
|
-
>(builderResult: BuilderResult): StateSourceBuilder<S> {
|
|
474
|
+
>(builderResult: BuilderResult, id?: string): StateSourceBuilder<S> {
|
|
438
475
|
if (!isValidBuilderResult(builderResult)) {
|
|
439
476
|
throw new Error('Invalid builder result - must be from State builders');
|
|
440
477
|
}
|
|
@@ -443,5 +480,5 @@ export function typedSource<
|
|
|
443
480
|
throw new Error('Source can only be created from State builders');
|
|
444
481
|
}
|
|
445
482
|
|
|
446
|
-
return new StateSourceBuilder<S>(builderResult.type);
|
|
483
|
+
return new StateSourceBuilder<S>(builderResult.type, id);
|
|
447
484
|
}
|
|
@@ -1712,4 +1712,73 @@ flow('All Projection Patterns', () => {
|
|
|
1712
1712
|
});
|
|
1713
1713
|
}
|
|
1714
1714
|
});
|
|
1715
|
+
|
|
1716
|
+
it('should capture optional id on data sink and source items', async () => {
|
|
1717
|
+
const memoryVfs = new InMemoryFileStore();
|
|
1718
|
+
|
|
1719
|
+
const flowContent = `
|
|
1720
|
+
import { flow, command, query, specs, rule, example, data, sink, source, type Event, type State } from '@auto-engineer/narrative';
|
|
1721
|
+
|
|
1722
|
+
type OrderPlaced = Event<'OrderPlaced', { orderId: string; amount: number }>;
|
|
1723
|
+
type OrderState = State<'OrderState', { orderId: string; status: string }>;
|
|
1724
|
+
|
|
1725
|
+
flow('Data Item IDs', () => {
|
|
1726
|
+
command('places order')
|
|
1727
|
+
.server(() => {
|
|
1728
|
+
specs(() => {
|
|
1729
|
+
rule('order is placed', () => {
|
|
1730
|
+
example('places order')
|
|
1731
|
+
.when({})
|
|
1732
|
+
.then<OrderPlaced>({ orderId: 'ord-001', amount: 100 });
|
|
1733
|
+
});
|
|
1734
|
+
});
|
|
1735
|
+
data([
|
|
1736
|
+
sink('SINK-001').event('OrderPlaced').toStream('orders-stream'),
|
|
1737
|
+
]);
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
query('views order status')
|
|
1741
|
+
.server(() => {
|
|
1742
|
+
specs(() => {
|
|
1743
|
+
rule('shows order status', () => {
|
|
1744
|
+
example('order status')
|
|
1745
|
+
.given<OrderPlaced>({ orderId: 'ord-001', amount: 100 })
|
|
1746
|
+
.when({})
|
|
1747
|
+
.then<OrderState>({ orderId: 'ord-001', status: 'pending' });
|
|
1748
|
+
});
|
|
1749
|
+
});
|
|
1750
|
+
data([
|
|
1751
|
+
source('SOURCE-001').state<OrderState>('OrderState').fromProjection('Orders', 'orderId'),
|
|
1752
|
+
]);
|
|
1753
|
+
});
|
|
1754
|
+
});
|
|
1755
|
+
`;
|
|
1756
|
+
|
|
1757
|
+
await memoryVfs.write('/test/data-ids.narrative.ts', new TextEncoder().encode(flowContent));
|
|
1758
|
+
|
|
1759
|
+
const flows = await getNarratives({ vfs: memoryVfs, root: '/test', pattern, fastFsScan: true });
|
|
1760
|
+
const model = flows.toModel();
|
|
1761
|
+
|
|
1762
|
+
const parseResult = modelSchema.safeParse(model);
|
|
1763
|
+
expect(parseResult.success).toBe(true);
|
|
1764
|
+
|
|
1765
|
+
const dataIdsFlow = model.narratives.find((f) => f.name === 'Data Item IDs');
|
|
1766
|
+
expect(dataIdsFlow).toBeDefined();
|
|
1767
|
+
|
|
1768
|
+
if (!dataIdsFlow) return;
|
|
1769
|
+
|
|
1770
|
+
const commandSlice = dataIdsFlow.slices.find((s) => s.name === 'places order');
|
|
1771
|
+
if (commandSlice?.type === 'command') {
|
|
1772
|
+
const sinkData = commandSlice.server.data;
|
|
1773
|
+
expect(sinkData).toHaveLength(1);
|
|
1774
|
+
expect(sinkData?.[0].id).toBe('SINK-001');
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
const querySlice = dataIdsFlow.slices.find((s) => s.name === 'views order status');
|
|
1778
|
+
if (querySlice?.type === 'query') {
|
|
1779
|
+
const sourceData = querySlice.server.data as DataSource[] | undefined;
|
|
1780
|
+
expect(sourceData).toHaveLength(1);
|
|
1781
|
+
expect(sourceData?.[0].id).toBe('SOURCE-001');
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1715
1784
|
});
|
|
@@ -669,6 +669,274 @@ describe('addAutoIds', () => {
|
|
|
669
669
|
}
|
|
670
670
|
});
|
|
671
671
|
|
|
672
|
+
describe('data item ID generation', () => {
|
|
673
|
+
const AUTO_ID_REGEX = /^[A-Za-z0-9_]{9}$/;
|
|
674
|
+
|
|
675
|
+
it('should assign ID to data sink without ID', () => {
|
|
676
|
+
const model: Model = {
|
|
677
|
+
variant: 'specs',
|
|
678
|
+
narratives: [
|
|
679
|
+
{
|
|
680
|
+
name: 'Test Flow',
|
|
681
|
+
id: 'FLOW-001',
|
|
682
|
+
slices: [
|
|
683
|
+
{
|
|
684
|
+
type: 'command',
|
|
685
|
+
name: 'Test Command',
|
|
686
|
+
id: 'SLICE-001',
|
|
687
|
+
client: { specs: [] },
|
|
688
|
+
server: {
|
|
689
|
+
description: 'Test server',
|
|
690
|
+
specs: [],
|
|
691
|
+
data: [
|
|
692
|
+
{
|
|
693
|
+
target: { type: 'Event', name: 'TestEvent' },
|
|
694
|
+
destination: { type: 'stream', pattern: 'test-stream' },
|
|
695
|
+
},
|
|
696
|
+
],
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
],
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
messages: [],
|
|
703
|
+
integrations: [],
|
|
704
|
+
modules: [],
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
const result = addAutoIds(model);
|
|
708
|
+
const slice = result.narratives[0].slices[0];
|
|
709
|
+
|
|
710
|
+
if ('server' in slice && slice.server?.data) {
|
|
711
|
+
expect(slice.server.data[0].id).toMatch(AUTO_ID_REGEX);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it('should assign ID to data source without ID', () => {
|
|
716
|
+
const model: Model = {
|
|
717
|
+
variant: 'specs',
|
|
718
|
+
narratives: [
|
|
719
|
+
{
|
|
720
|
+
name: 'Test Flow',
|
|
721
|
+
id: 'FLOW-001',
|
|
722
|
+
slices: [
|
|
723
|
+
{
|
|
724
|
+
type: 'query',
|
|
725
|
+
name: 'Test Query',
|
|
726
|
+
id: 'SLICE-001',
|
|
727
|
+
client: { specs: [] },
|
|
728
|
+
server: {
|
|
729
|
+
description: 'Test server',
|
|
730
|
+
specs: [],
|
|
731
|
+
data: [
|
|
732
|
+
{
|
|
733
|
+
target: { type: 'State', name: 'TestState' },
|
|
734
|
+
origin: { type: 'projection', name: 'TestProjection' },
|
|
735
|
+
},
|
|
736
|
+
],
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
],
|
|
740
|
+
},
|
|
741
|
+
],
|
|
742
|
+
messages: [],
|
|
743
|
+
integrations: [],
|
|
744
|
+
modules: [],
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
const result = addAutoIds(model);
|
|
748
|
+
const slice = result.narratives[0].slices[0];
|
|
749
|
+
|
|
750
|
+
if ('server' in slice && slice.server?.data) {
|
|
751
|
+
expect(slice.server.data[0].id).toMatch(AUTO_ID_REGEX);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it('should assign ID to nested _withState source without ID', () => {
|
|
756
|
+
const model: Model = {
|
|
757
|
+
variant: 'specs',
|
|
758
|
+
narratives: [
|
|
759
|
+
{
|
|
760
|
+
name: 'Test Flow',
|
|
761
|
+
id: 'FLOW-001',
|
|
762
|
+
slices: [
|
|
763
|
+
{
|
|
764
|
+
type: 'command',
|
|
765
|
+
name: 'Test Command',
|
|
766
|
+
id: 'SLICE-001',
|
|
767
|
+
client: { specs: [] },
|
|
768
|
+
server: {
|
|
769
|
+
description: 'Test server',
|
|
770
|
+
specs: [],
|
|
771
|
+
data: [
|
|
772
|
+
{
|
|
773
|
+
id: 'SINK-001',
|
|
774
|
+
target: { type: 'Command', name: 'TestCommand' },
|
|
775
|
+
destination: { type: 'stream', pattern: 'test-stream' },
|
|
776
|
+
_withState: {
|
|
777
|
+
target: { type: 'State', name: 'TestState' },
|
|
778
|
+
origin: { type: 'projection', name: 'TestProjection' },
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
],
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
],
|
|
785
|
+
},
|
|
786
|
+
],
|
|
787
|
+
messages: [],
|
|
788
|
+
integrations: [],
|
|
789
|
+
modules: [],
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const result = addAutoIds(model);
|
|
793
|
+
const slice = result.narratives[0].slices[0];
|
|
794
|
+
|
|
795
|
+
if ('server' in slice && slice.server?.data) {
|
|
796
|
+
const sink = slice.server.data[0];
|
|
797
|
+
expect(sink.id).toBe('SINK-001');
|
|
798
|
+
if ('destination' in sink && sink._withState) {
|
|
799
|
+
expect(sink._withState.id).toMatch(AUTO_ID_REGEX);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it('should preserve existing data item IDs', () => {
|
|
805
|
+
const model: Model = {
|
|
806
|
+
variant: 'specs',
|
|
807
|
+
narratives: [
|
|
808
|
+
{
|
|
809
|
+
name: 'Test Flow',
|
|
810
|
+
id: 'FLOW-001',
|
|
811
|
+
slices: [
|
|
812
|
+
{
|
|
813
|
+
type: 'react',
|
|
814
|
+
name: 'Test React',
|
|
815
|
+
id: 'SLICE-001',
|
|
816
|
+
server: {
|
|
817
|
+
specs: [],
|
|
818
|
+
data: [
|
|
819
|
+
{
|
|
820
|
+
id: 'EXISTING-SINK-001',
|
|
821
|
+
target: { type: 'Event', name: 'TestEvent' },
|
|
822
|
+
destination: { type: 'stream', pattern: 'test-stream' },
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
id: 'EXISTING-SOURCE-001',
|
|
826
|
+
target: { type: 'State', name: 'TestState' },
|
|
827
|
+
origin: { type: 'projection', name: 'TestProjection' },
|
|
828
|
+
},
|
|
829
|
+
],
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
],
|
|
833
|
+
},
|
|
834
|
+
],
|
|
835
|
+
messages: [],
|
|
836
|
+
integrations: [],
|
|
837
|
+
modules: [],
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
const result = addAutoIds(model);
|
|
841
|
+
const slice = result.narratives[0].slices[0];
|
|
842
|
+
|
|
843
|
+
if ('server' in slice && slice.server?.data) {
|
|
844
|
+
expect(slice.server.data[0].id).toBe('EXISTING-SINK-001');
|
|
845
|
+
expect(slice.server.data[1].id).toBe('EXISTING-SOURCE-001');
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
it('should not mutate original data items', () => {
|
|
850
|
+
const model: Model = {
|
|
851
|
+
variant: 'specs',
|
|
852
|
+
narratives: [
|
|
853
|
+
{
|
|
854
|
+
name: 'Test Flow',
|
|
855
|
+
id: 'FLOW-001',
|
|
856
|
+
slices: [
|
|
857
|
+
{
|
|
858
|
+
type: 'command',
|
|
859
|
+
name: 'Test Command',
|
|
860
|
+
id: 'SLICE-001',
|
|
861
|
+
client: { specs: [] },
|
|
862
|
+
server: {
|
|
863
|
+
description: 'Test server',
|
|
864
|
+
specs: [],
|
|
865
|
+
data: [
|
|
866
|
+
{
|
|
867
|
+
target: { type: 'Event', name: 'TestEvent' },
|
|
868
|
+
destination: { type: 'stream', pattern: 'test-stream' },
|
|
869
|
+
},
|
|
870
|
+
],
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
],
|
|
874
|
+
},
|
|
875
|
+
],
|
|
876
|
+
messages: [],
|
|
877
|
+
integrations: [],
|
|
878
|
+
modules: [],
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
const originalSlice = model.narratives[0].slices[0];
|
|
882
|
+
addAutoIds(model);
|
|
883
|
+
|
|
884
|
+
if ('server' in originalSlice && originalSlice.server?.data) {
|
|
885
|
+
expect(originalSlice.server.data[0].id).toBeUndefined();
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('should generate unique IDs for multiple data items', () => {
|
|
890
|
+
const model: Model = {
|
|
891
|
+
variant: 'specs',
|
|
892
|
+
narratives: [
|
|
893
|
+
{
|
|
894
|
+
name: 'Test Flow',
|
|
895
|
+
id: 'FLOW-001',
|
|
896
|
+
slices: [
|
|
897
|
+
{
|
|
898
|
+
type: 'react',
|
|
899
|
+
name: 'Test React',
|
|
900
|
+
id: 'SLICE-001',
|
|
901
|
+
server: {
|
|
902
|
+
specs: [],
|
|
903
|
+
data: [
|
|
904
|
+
{
|
|
905
|
+
target: { type: 'Event', name: 'Event1' },
|
|
906
|
+
destination: { type: 'stream', pattern: 'stream1' },
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
target: { type: 'Event', name: 'Event2' },
|
|
910
|
+
destination: { type: 'stream', pattern: 'stream2' },
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
target: { type: 'State', name: 'State1' },
|
|
914
|
+
origin: { type: 'projection', name: 'Proj1' },
|
|
915
|
+
},
|
|
916
|
+
],
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
],
|
|
920
|
+
},
|
|
921
|
+
],
|
|
922
|
+
messages: [],
|
|
923
|
+
integrations: [],
|
|
924
|
+
modules: [],
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
const result = addAutoIds(model);
|
|
928
|
+
const slice = result.narratives[0].slices[0];
|
|
929
|
+
|
|
930
|
+
if ('server' in slice && slice.server?.data) {
|
|
931
|
+
const ids = slice.server.data.map((d) => d.id);
|
|
932
|
+
expect(ids[0]).toMatch(AUTO_ID_REGEX);
|
|
933
|
+
expect(ids[1]).toMatch(AUTO_ID_REGEX);
|
|
934
|
+
expect(ids[2]).toMatch(AUTO_ID_REGEX);
|
|
935
|
+
expect(new Set(ids).size).toBe(3);
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
|
|
672
940
|
describe('module ID generation', () => {
|
|
673
941
|
const AUTO_ID_REGEX = /^[A-Za-z0-9_]{9}$/;
|
|
674
942
|
|