@scenarist/core 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +755 -28
- package/dist/adapters/in-memory-registry.d.ts +18 -0
- package/dist/adapters/in-memory-registry.d.ts.map +1 -0
- package/dist/adapters/in-memory-registry.js +25 -0
- package/dist/adapters/in-memory-sequence-tracker.d.ts +28 -0
- package/dist/adapters/in-memory-sequence-tracker.d.ts.map +1 -0
- package/dist/adapters/in-memory-sequence-tracker.js +82 -0
- package/dist/adapters/in-memory-state-manager.d.ts +24 -0
- package/dist/adapters/in-memory-state-manager.d.ts.map +1 -0
- package/dist/adapters/in-memory-state-manager.js +81 -0
- package/dist/adapters/in-memory-store.d.ts +18 -0
- package/dist/adapters/in-memory-store.d.ts.map +1 -0
- package/dist/adapters/in-memory-store.js +25 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +4 -0
- package/dist/constants/headers.d.ts +10 -0
- package/dist/constants/headers.d.ts.map +1 -0
- package/dist/constants/headers.js +9 -0
- package/dist/constants/index.d.ts +2 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/contracts/framework-adapter.d.ts +118 -0
- package/dist/contracts/framework-adapter.d.ts.map +1 -0
- package/dist/contracts/framework-adapter.js +1 -0
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +1 -0
- package/dist/domain/config-builder.d.ts +9 -0
- package/dist/domain/config-builder.d.ts.map +1 -0
- package/dist/domain/config-builder.js +20 -0
- package/dist/domain/index.d.ts +5 -0
- package/dist/domain/index.d.ts.map +1 -0
- package/dist/domain/index.js +4 -0
- package/dist/domain/path-extraction.d.ts +17 -0
- package/dist/domain/path-extraction.d.ts.map +1 -0
- package/dist/domain/path-extraction.js +60 -0
- package/dist/domain/regex-matching.d.ts +20 -0
- package/dist/domain/regex-matching.d.ts.map +1 -0
- package/dist/domain/regex-matching.js +27 -0
- package/dist/domain/response-selector.d.ts +22 -0
- package/dist/domain/response-selector.d.ts.map +1 -0
- package/dist/domain/response-selector.js +337 -0
- package/dist/domain/scenario-manager.d.ts +20 -0
- package/dist/domain/scenario-manager.d.ts.map +1 -0
- package/dist/domain/scenario-manager.js +90 -0
- package/dist/domain/template-replacement.d.ts +11 -0
- package/dist/domain/template-replacement.d.ts.map +1 -0
- package/dist/domain/template-replacement.js +94 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/ports/driven/request-context.d.ts +43 -0
- package/dist/ports/driven/request-context.d.ts.map +1 -0
- package/dist/ports/driven/request-context.js +1 -0
- package/dist/ports/driven/response-selector.d.ts +34 -0
- package/dist/ports/driven/response-selector.d.ts.map +1 -0
- package/dist/ports/driven/response-selector.js +9 -0
- package/dist/ports/driven/scenario-registry.d.ts +46 -0
- package/dist/ports/driven/scenario-registry.d.ts.map +1 -0
- package/dist/ports/driven/scenario-registry.js +1 -0
- package/dist/ports/driven/scenario-store.d.ts +33 -0
- package/dist/ports/driven/scenario-store.d.ts.map +1 -0
- package/dist/ports/driven/scenario-store.js +1 -0
- package/dist/ports/driven/sequence-tracker.d.ts +49 -0
- package/dist/ports/driven/sequence-tracker.d.ts.map +1 -0
- package/dist/ports/driven/sequence-tracker.js +1 -0
- package/dist/ports/driven/state-manager.d.ts +56 -0
- package/dist/ports/driven/state-manager.d.ts.map +1 -0
- package/dist/ports/driven/state-manager.js +1 -0
- package/dist/ports/driving/scenario-manager.d.ts +99 -0
- package/dist/ports/driving/scenario-manager.d.ts.map +1 -0
- package/dist/ports/driving/scenario-manager.js +1 -0
- package/dist/ports/index.d.ts +8 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +1 -0
- package/dist/schemas/index.d.ts +18 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +17 -0
- package/dist/schemas/match-criteria.d.ts +27 -0
- package/dist/schemas/match-criteria.d.ts.map +1 -0
- package/dist/schemas/match-criteria.js +71 -0
- package/dist/schemas/scenario-definition.d.ts +276 -0
- package/dist/schemas/scenario-definition.d.ts.map +1 -0
- package/dist/schemas/scenario-definition.js +78 -0
- package/dist/schemas/scenario-requests.d.ts +33 -0
- package/dist/schemas/scenario-requests.d.ts.map +1 -0
- package/dist/schemas/scenario-requests.js +29 -0
- package/dist/schemas/scenarios-object.d.ts +91 -0
- package/dist/schemas/scenarios-object.d.ts.map +1 -0
- package/dist/schemas/scenarios-object.js +17 -0
- package/dist/types/config.d.ts +70 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/scenario.d.ts +141 -0
- package/dist/types/scenario.d.ts.map +1 -0
- package/dist/types/scenario.js +1 -0
- package/package.json +67 -7
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ScenarioRegistry } from '../ports/index.js';
|
|
2
|
+
import type { ScenaristScenario } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* In-memory implementation of ScenarioRegistry using a Map.
|
|
5
|
+
* Suitable for single-process testing and development.
|
|
6
|
+
*
|
|
7
|
+
* For distributed testing across multiple processes,
|
|
8
|
+
* consider implementing a Redis-based or database-backed registry.
|
|
9
|
+
*/
|
|
10
|
+
export declare class InMemoryScenarioRegistry implements ScenarioRegistry {
|
|
11
|
+
private readonly registry;
|
|
12
|
+
register(definition: ScenaristScenario): void;
|
|
13
|
+
get(id: string): ScenaristScenario | undefined;
|
|
14
|
+
has(id: string): boolean;
|
|
15
|
+
list(): ReadonlyArray<ScenaristScenario>;
|
|
16
|
+
unregister(id: string): void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=in-memory-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-registry.d.ts","sourceRoot":"","sources":["../../src/adapters/in-memory-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D;;;;;;GAMG;AACH,qBAAa,wBAAyB,YAAW,gBAAgB;IAC/D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwC;IAEjE,QAAQ,CAAC,UAAU,EAAE,iBAAiB,GAAG,IAAI;IAI7C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAI9C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIxB,IAAI,IAAI,aAAa,CAAC,iBAAiB,CAAC;IAIxC,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;CAG7B"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory implementation of ScenarioRegistry using a Map.
|
|
3
|
+
* Suitable for single-process testing and development.
|
|
4
|
+
*
|
|
5
|
+
* For distributed testing across multiple processes,
|
|
6
|
+
* consider implementing a Redis-based or database-backed registry.
|
|
7
|
+
*/
|
|
8
|
+
export class InMemoryScenarioRegistry {
|
|
9
|
+
registry = new Map();
|
|
10
|
+
register(definition) {
|
|
11
|
+
this.registry.set(definition.id, definition);
|
|
12
|
+
}
|
|
13
|
+
get(id) {
|
|
14
|
+
return this.registry.get(id);
|
|
15
|
+
}
|
|
16
|
+
has(id) {
|
|
17
|
+
return this.registry.has(id);
|
|
18
|
+
}
|
|
19
|
+
list() {
|
|
20
|
+
return Array.from(this.registry.values());
|
|
21
|
+
}
|
|
22
|
+
unregister(id) {
|
|
23
|
+
this.registry.delete(id);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SequenceTracker, SequencePosition } from '../ports/driven/sequence-tracker.js';
|
|
2
|
+
/**
|
|
3
|
+
* In-memory implementation of SequenceTracker.
|
|
4
|
+
*
|
|
5
|
+
* Tracks sequence positions using a Map in memory.
|
|
6
|
+
* Suitable for single-process testing scenarios.
|
|
7
|
+
*
|
|
8
|
+
* For distributed testing across multiple processes, use a Redis-based
|
|
9
|
+
* implementation instead.
|
|
10
|
+
*/
|
|
11
|
+
export declare class InMemorySequenceTracker implements SequenceTracker {
|
|
12
|
+
private readonly positions;
|
|
13
|
+
constructor();
|
|
14
|
+
/**
|
|
15
|
+
* Generate key for tracking sequence position.
|
|
16
|
+
* Format: `${testId}:${scenarioId}:${mockIndex}`
|
|
17
|
+
*/
|
|
18
|
+
private getKey;
|
|
19
|
+
getPosition(testId: string, scenarioId: string, mockIndex: number): SequencePosition;
|
|
20
|
+
advance(testId: string, scenarioId: string, mockIndex: number, totalResponses: number, repeatMode: 'last' | 'cycle' | 'none'): void;
|
|
21
|
+
reset(testId: string): void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create an in-memory sequence tracker.
|
|
25
|
+
* Factory function following the pattern established for other domain services.
|
|
26
|
+
*/
|
|
27
|
+
export declare const createInMemorySequenceTracker: () => SequenceTracker;
|
|
28
|
+
//# sourceMappingURL=in-memory-sequence-tracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-sequence-tracker.d.ts","sourceRoot":"","sources":["../../src/adapters/in-memory-sequence-tracker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EACjB,MAAM,qCAAqC,CAAC;AAE7C;;;;;;;;GAQG;AACH,qBAAa,uBAAwB,YAAW,eAAe;IAC7D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;;IAM1D;;;OAGG;IACH,OAAO,CAAC,MAAM;IAId,WAAW,CACT,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAChB,gBAAgB;IAYnB,OAAO,CACL,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GACpC,IAAI;IA0CP,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;CAM5B;AAED;;;GAGG;AACH,eAAO,MAAM,6BAA6B,QAAO,eAEhD,CAAC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory implementation of SequenceTracker.
|
|
3
|
+
*
|
|
4
|
+
* Tracks sequence positions using a Map in memory.
|
|
5
|
+
* Suitable for single-process testing scenarios.
|
|
6
|
+
*
|
|
7
|
+
* For distributed testing across multiple processes, use a Redis-based
|
|
8
|
+
* implementation instead.
|
|
9
|
+
*/
|
|
10
|
+
export class InMemorySequenceTracker {
|
|
11
|
+
positions;
|
|
12
|
+
constructor() {
|
|
13
|
+
this.positions = new Map();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Generate key for tracking sequence position.
|
|
17
|
+
* Format: `${testId}:${scenarioId}:${mockIndex}`
|
|
18
|
+
*/
|
|
19
|
+
getKey(testId, scenarioId, mockIndex) {
|
|
20
|
+
return `${testId}:${scenarioId}:${mockIndex}`;
|
|
21
|
+
}
|
|
22
|
+
getPosition(testId, scenarioId, mockIndex) {
|
|
23
|
+
const key = this.getKey(testId, scenarioId, mockIndex);
|
|
24
|
+
const existing = this.positions.get(key);
|
|
25
|
+
// If no position exists yet, start at 0
|
|
26
|
+
if (!existing) {
|
|
27
|
+
return { position: 0, exhausted: false };
|
|
28
|
+
}
|
|
29
|
+
return existing;
|
|
30
|
+
}
|
|
31
|
+
advance(testId, scenarioId, mockIndex, totalResponses, repeatMode) {
|
|
32
|
+
const key = this.getKey(testId, scenarioId, mockIndex);
|
|
33
|
+
const current = this.getPosition(testId, scenarioId, mockIndex);
|
|
34
|
+
const nextPosition = current.position + 1;
|
|
35
|
+
// Handle different repeat modes
|
|
36
|
+
if (nextPosition >= totalResponses) {
|
|
37
|
+
switch (repeatMode) {
|
|
38
|
+
case 'last':
|
|
39
|
+
// Stay at last position (don't increment beyond last)
|
|
40
|
+
this.positions.set(key, {
|
|
41
|
+
position: totalResponses - 1,
|
|
42
|
+
exhausted: false,
|
|
43
|
+
});
|
|
44
|
+
break;
|
|
45
|
+
case 'cycle':
|
|
46
|
+
// Wrap back to first position
|
|
47
|
+
this.positions.set(key, {
|
|
48
|
+
position: 0,
|
|
49
|
+
exhausted: false,
|
|
50
|
+
});
|
|
51
|
+
break;
|
|
52
|
+
case 'none':
|
|
53
|
+
// Mark as exhausted
|
|
54
|
+
this.positions.set(key, {
|
|
55
|
+
position: totalResponses,
|
|
56
|
+
exhausted: true,
|
|
57
|
+
});
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Normal advancement (not at end yet)
|
|
63
|
+
this.positions.set(key, {
|
|
64
|
+
position: nextPosition,
|
|
65
|
+
exhausted: false,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
reset(testId) {
|
|
70
|
+
const prefix = `${testId}:`;
|
|
71
|
+
Array.from(this.positions.keys())
|
|
72
|
+
.filter(key => key.startsWith(prefix))
|
|
73
|
+
.forEach(key => this.positions.delete(key));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create an in-memory sequence tracker.
|
|
78
|
+
* Factory function following the pattern established for other domain services.
|
|
79
|
+
*/
|
|
80
|
+
export const createInMemorySequenceTracker = () => {
|
|
81
|
+
return new InMemorySequenceTracker();
|
|
82
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { StateManager } from '../ports/driven/state-manager.js';
|
|
2
|
+
/**
|
|
3
|
+
* In-memory implementation of StateManager port.
|
|
4
|
+
* Fast, single-process state storage for stateful mocks.
|
|
5
|
+
*
|
|
6
|
+
* For distributed testing across multiple processes,
|
|
7
|
+
* consider implementing a Redis-based or database-backed state manager.
|
|
8
|
+
*/
|
|
9
|
+
export declare class InMemoryStateManager implements StateManager {
|
|
10
|
+
private readonly storage;
|
|
11
|
+
get(testId: string, key: string): unknown;
|
|
12
|
+
set(testId: string, key: string, value: unknown): void;
|
|
13
|
+
getAll(testId: string): Record<string, unknown>;
|
|
14
|
+
reset(testId: string): void;
|
|
15
|
+
private getOrCreateTestState;
|
|
16
|
+
private setNestedValue;
|
|
17
|
+
private getNestedValue;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Factory function for creating InMemoryStateManager instances.
|
|
21
|
+
* Consistent with existing adapter factory pattern.
|
|
22
|
+
*/
|
|
23
|
+
export declare const createInMemoryStateManager: () => StateManager;
|
|
24
|
+
//# sourceMappingURL=in-memory-state-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-state-manager.d.ts","sourceRoot":"","sources":["../../src/adapters/in-memory-state-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAErE;;;;;;GAMG;AACH,qBAAa,oBAAqB,YAAW,YAAY;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8C;IAEtE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO;IAUzC,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAyBtD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAI/C,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI3B,OAAO,CAAC,oBAAoB;IAS5B,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,cAAc;CAavB;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,QAAO,YAE7C,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory implementation of StateManager port.
|
|
3
|
+
* Fast, single-process state storage for stateful mocks.
|
|
4
|
+
*
|
|
5
|
+
* For distributed testing across multiple processes,
|
|
6
|
+
* consider implementing a Redis-based or database-backed state manager.
|
|
7
|
+
*/
|
|
8
|
+
export class InMemoryStateManager {
|
|
9
|
+
storage = new Map();
|
|
10
|
+
get(testId, key) {
|
|
11
|
+
const testState = this.storage.get(testId);
|
|
12
|
+
if (!testState) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const path = key.split('.');
|
|
16
|
+
return this.getNestedValue(testState, path);
|
|
17
|
+
}
|
|
18
|
+
set(testId, key, value) {
|
|
19
|
+
const testState = this.getOrCreateTestState(testId);
|
|
20
|
+
// Guard: Normal set (no array syntax)
|
|
21
|
+
if (!key.endsWith('[]')) {
|
|
22
|
+
const path = key.split('.');
|
|
23
|
+
this.setNestedValue(testState, path, value);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Array append syntax
|
|
27
|
+
const actualKey = key.slice(0, -2);
|
|
28
|
+
const path = actualKey.split('.');
|
|
29
|
+
const currentValue = this.get(testId, actualKey);
|
|
30
|
+
// Guard: If current value is already an array, append immutably
|
|
31
|
+
if (Array.isArray(currentValue)) {
|
|
32
|
+
this.setNestedValue(testState, path, [...currentValue, value]);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Create new array (either undefined or non-array value)
|
|
36
|
+
this.setNestedValue(testState, path, [value]);
|
|
37
|
+
}
|
|
38
|
+
getAll(testId) {
|
|
39
|
+
return this.storage.get(testId) ?? {};
|
|
40
|
+
}
|
|
41
|
+
reset(testId) {
|
|
42
|
+
this.storage.delete(testId);
|
|
43
|
+
}
|
|
44
|
+
getOrCreateTestState(testId) {
|
|
45
|
+
let testState = this.storage.get(testId);
|
|
46
|
+
if (!testState) {
|
|
47
|
+
testState = {};
|
|
48
|
+
this.storage.set(testId, testState);
|
|
49
|
+
}
|
|
50
|
+
return testState;
|
|
51
|
+
}
|
|
52
|
+
setNestedValue(obj, path, value) {
|
|
53
|
+
if (path.length === 1) {
|
|
54
|
+
obj[path[0]] = value;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const key = path[0];
|
|
58
|
+
if (typeof obj[key] !== 'object' || obj[key] === null || Array.isArray(obj[key])) {
|
|
59
|
+
obj[key] = {};
|
|
60
|
+
}
|
|
61
|
+
this.setNestedValue(obj[key], path.slice(1), value);
|
|
62
|
+
}
|
|
63
|
+
getNestedValue(obj, path) {
|
|
64
|
+
if (path.length === 1) {
|
|
65
|
+
return obj[path[0]];
|
|
66
|
+
}
|
|
67
|
+
const key = path[0];
|
|
68
|
+
const nested = obj[key];
|
|
69
|
+
if (typeof nested !== 'object' || nested === null || Array.isArray(nested)) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return this.getNestedValue(nested, path.slice(1));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Factory function for creating InMemoryStateManager instances.
|
|
77
|
+
* Consistent with existing adapter factory pattern.
|
|
78
|
+
*/
|
|
79
|
+
export const createInMemoryStateManager = () => {
|
|
80
|
+
return new InMemoryStateManager();
|
|
81
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ScenarioStore } from '../ports/index.js';
|
|
2
|
+
import type { ActiveScenario } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* In-memory implementation of ScenarioStore using a Map.
|
|
5
|
+
* Suitable for single-process testing.
|
|
6
|
+
*
|
|
7
|
+
* For distributed testing across multiple processes,
|
|
8
|
+
* consider implementing a Redis-based store.
|
|
9
|
+
*/
|
|
10
|
+
export declare class InMemoryScenarioStore implements ScenarioStore {
|
|
11
|
+
private readonly store;
|
|
12
|
+
set(testId: string, scenario: ActiveScenario): void;
|
|
13
|
+
get(testId: string): ActiveScenario | undefined;
|
|
14
|
+
has(testId: string): boolean;
|
|
15
|
+
delete(testId: string): void;
|
|
16
|
+
clear(): void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=in-memory-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-store.d.ts","sourceRoot":"","sources":["../../src/adapters/in-memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;;;;GAMG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IACzD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAE3D,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,IAAI;IAInD,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAI/C,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAI5B,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI5B,KAAK,IAAI,IAAI;CAGd"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory implementation of ScenarioStore using a Map.
|
|
3
|
+
* Suitable for single-process testing.
|
|
4
|
+
*
|
|
5
|
+
* For distributed testing across multiple processes,
|
|
6
|
+
* consider implementing a Redis-based store.
|
|
7
|
+
*/
|
|
8
|
+
export class InMemoryScenarioStore {
|
|
9
|
+
store = new Map();
|
|
10
|
+
set(testId, scenario) {
|
|
11
|
+
this.store.set(testId, scenario);
|
|
12
|
+
}
|
|
13
|
+
get(testId) {
|
|
14
|
+
return this.store.get(testId);
|
|
15
|
+
}
|
|
16
|
+
has(testId) {
|
|
17
|
+
return this.store.has(testId);
|
|
18
|
+
}
|
|
19
|
+
delete(testId) {
|
|
20
|
+
this.store.delete(testId);
|
|
21
|
+
}
|
|
22
|
+
clear() {
|
|
23
|
+
this.store.clear();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { InMemoryScenarioRegistry } from './in-memory-registry.js';
|
|
2
|
+
export { InMemoryScenarioStore } from './in-memory-store.js';
|
|
3
|
+
export { InMemorySequenceTracker, createInMemorySequenceTracker } from './in-memory-sequence-tracker.js';
|
|
4
|
+
export { InMemoryStateManager, createInMemoryStateManager } from './in-memory-state-manager.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AACzG,OAAO,EAAE,oBAAoB,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { InMemoryScenarioRegistry } from './in-memory-registry.js';
|
|
2
|
+
export { InMemoryScenarioStore } from './in-memory-store.js';
|
|
3
|
+
export { InMemorySequenceTracker, createInMemorySequenceTracker } from './in-memory-sequence-tracker.js';
|
|
4
|
+
export { InMemoryStateManager, createInMemoryStateManager } from './in-memory-state-manager.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The standard header name used for test ID isolation.
|
|
3
|
+
*
|
|
4
|
+
* This header is used by Scenarist to identify which test a request belongs to,
|
|
5
|
+
* enabling concurrent tests with different backend states.
|
|
6
|
+
*
|
|
7
|
+
* This is a fixed constant - not configurable by users.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SCENARIST_TEST_ID_HEADER = "x-scenarist-test-id";
|
|
10
|
+
//# sourceMappingURL=headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/constants/headers.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The standard header name used for test ID isolation.
|
|
3
|
+
*
|
|
4
|
+
* This header is used by Scenarist to identify which test a request belongs to,
|
|
5
|
+
* enabling concurrent tests with different backend states.
|
|
6
|
+
*
|
|
7
|
+
* This is a fixed constant - not configurable by users.
|
|
8
|
+
*/
|
|
9
|
+
export const SCENARIST_TEST_ID_HEADER = 'x-scenarist-test-id';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/constants/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SCENARIST_TEST_ID_HEADER } from './headers.js';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ScenaristScenario, ActiveScenario, ScenaristResult, ScenaristConfigInput, ScenaristConfig, ScenaristScenarios, ScenarioIds } from '../types/index.js';
|
|
2
|
+
import type { ScenarioRegistry } from '../ports/driven/scenario-registry.js';
|
|
3
|
+
import type { ScenarioStore } from '../ports/driven/scenario-store.js';
|
|
4
|
+
/**
|
|
5
|
+
* Base configuration options that all framework adapters must support.
|
|
6
|
+
*
|
|
7
|
+
* Extends ScenaristConfigInput (the core config) with adapter-specific options
|
|
8
|
+
* for registry and store injection.
|
|
9
|
+
*
|
|
10
|
+
* Framework adapters can extend this further with framework-specific options:
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // Express adapter - uses base options directly
|
|
15
|
+
* export type ExpressAdapterOptions<T extends ScenaristScenarios = ScenaristScenarios> =
|
|
16
|
+
* BaseAdapterOptions<T>;
|
|
17
|
+
*
|
|
18
|
+
* // Fastify adapter - adds framework-specific options
|
|
19
|
+
* export type FastifyAdapterOptions<T extends ScenaristScenarios = ScenaristScenarios> =
|
|
20
|
+
* BaseAdapterOptions<T> & {
|
|
21
|
+
* readonly logLevel?: 'debug' | 'info' | 'warn' | 'error';
|
|
22
|
+
* };
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export type BaseAdapterOptions<T extends ScenaristScenarios = ScenaristScenarios> = ScenaristConfigInput<T> & {
|
|
26
|
+
/**
|
|
27
|
+
* Custom scenario registry implementation.
|
|
28
|
+
*
|
|
29
|
+
* If not provided, InMemoryScenarioRegistry will be used.
|
|
30
|
+
*/
|
|
31
|
+
readonly registry?: ScenarioRegistry;
|
|
32
|
+
/**
|
|
33
|
+
* Custom scenario store implementation.
|
|
34
|
+
*
|
|
35
|
+
* If not provided, InMemoryScenarioStore will be used.
|
|
36
|
+
*/
|
|
37
|
+
readonly store?: ScenarioStore;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The contract that all Scenarist adapters must satisfy.
|
|
41
|
+
*
|
|
42
|
+
* This ensures consistent API across Express, Fastify, Next.js, etc.
|
|
43
|
+
*
|
|
44
|
+
* @template TMiddleware - Framework-specific middleware type
|
|
45
|
+
* - Express: Router
|
|
46
|
+
* - Fastify: FastifyPluginCallback
|
|
47
|
+
* - Next.js: NextMiddleware
|
|
48
|
+
* @template TScenarios - Scenarios object for type-safe scenario IDs
|
|
49
|
+
*/
|
|
50
|
+
export type ScenaristAdapter<TMiddleware = unknown, TScenarios extends ScenaristScenarios = ScenaristScenarios> = {
|
|
51
|
+
/**
|
|
52
|
+
* Resolved configuration.
|
|
53
|
+
*
|
|
54
|
+
* Use this to access configured endpoints in tests.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* import { SCENARIST_TEST_ID_HEADER } from '@scenarist/core';
|
|
59
|
+
*
|
|
60
|
+
* await request(app)
|
|
61
|
+
* .post(scenarist.config.endpoints.setScenario)
|
|
62
|
+
* .set(SCENARIST_TEST_ID_HEADER, 'test-123')
|
|
63
|
+
* .send({ scenario: 'cartWithState' });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
readonly config: ScenaristConfig;
|
|
67
|
+
/**
|
|
68
|
+
* Framework-specific middleware/plugin.
|
|
69
|
+
*
|
|
70
|
+
* - Express: app.use(scenarist.middleware)
|
|
71
|
+
* - Fastify: app.register(scenarist.middleware)
|
|
72
|
+
* - Next.js: export const middleware = scenarist.middleware
|
|
73
|
+
*/
|
|
74
|
+
readonly middleware: TMiddleware;
|
|
75
|
+
/**
|
|
76
|
+
* Switch active scenario for a test ID.
|
|
77
|
+
*
|
|
78
|
+
* Scenario IDs are type-safe based on the scenarios object passed during creation.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* const result = scenarist.switchScenario('test-123', 'cartWithState');
|
|
83
|
+
* // TypeScript ensures 'cartWithState' is a valid scenario ID
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
readonly switchScenario: (testId: string, scenarioId: ScenarioIds<TScenarios>) => ScenaristResult<void, Error>;
|
|
87
|
+
/**
|
|
88
|
+
* Get the active scenario for a test ID.
|
|
89
|
+
*/
|
|
90
|
+
readonly getActiveScenario: (testId: string) => ActiveScenario | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Get a scenario definition by ID.
|
|
93
|
+
*
|
|
94
|
+
* Scenario IDs are type-safe based on the scenarios object.
|
|
95
|
+
*/
|
|
96
|
+
readonly getScenarioById: (scenarioId: ScenarioIds<TScenarios>) => ScenaristScenario | undefined;
|
|
97
|
+
/**
|
|
98
|
+
* List all registered scenarios.
|
|
99
|
+
*/
|
|
100
|
+
readonly listScenarios: () => ReadonlyArray<ScenaristScenario>;
|
|
101
|
+
/**
|
|
102
|
+
* Clear the active scenario for a test ID.
|
|
103
|
+
*/
|
|
104
|
+
readonly clearScenario: (testId: string) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Start the MSW server.
|
|
107
|
+
*
|
|
108
|
+
* Call in beforeAll() hook.
|
|
109
|
+
*/
|
|
110
|
+
readonly start: () => void;
|
|
111
|
+
/**
|
|
112
|
+
* Stop the MSW server.
|
|
113
|
+
*
|
|
114
|
+
* Call in afterAll() hook.
|
|
115
|
+
*/
|
|
116
|
+
readonly stop: () => Promise<void>;
|
|
117
|
+
};
|
|
118
|
+
//# sourceMappingURL=framework-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framework-adapter.d.ts","sourceRoot":"","sources":["../../src/contracts/framework-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,oBAAoB,EACpB,eAAe,EACf,kBAAkB,EAClB,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,kBAAkB,CAAC,CAAC,SAAS,kBAAkB,GAAG,kBAAkB,IAC9E,oBAAoB,CAAC,CAAC,CAAC,GAAG;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAErC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;CAChC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,gBAAgB,CAC1B,WAAW,GAAG,OAAO,EACrB,UAAU,SAAS,kBAAkB,GAAG,kBAAkB,IACxD;IACF;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IAEjC;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC;IAEjC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,CACvB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,WAAW,CAAC,UAAU,CAAC,KAChC,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAElC;;OAEG;IACH,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,cAAc,GAAG,SAAS,CAAC;IAE3E;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,CACxB,UAAU,EAAE,WAAW,CAAC,UAAU,CAAC,KAChC,iBAAiB,GAAG,SAAS,CAAC;IAEnC;;OAEG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,aAAa,CAAC,iBAAiB,CAAC,CAAC;IAE/D;;OAEG;IACH,QAAQ,CAAC,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAEjD;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;IAE3B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contracts/index.ts"],"names":[],"mappings":"AAGA,YAAY,EACV,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ScenaristConfig, ScenaristConfigInput, ScenaristScenarios } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build a complete config from partial user input.
|
|
4
|
+
* Applies sensible defaults for missing values.
|
|
5
|
+
*
|
|
6
|
+
* **Validation:** Ensures scenarios object has a 'default' key.
|
|
7
|
+
*/
|
|
8
|
+
export declare const buildConfig: <T extends ScenaristScenarios>(input: ScenaristConfigInput<T>) => ScenaristConfig;
|
|
9
|
+
//# sourceMappingURL=config-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-builder.d.ts","sourceRoot":"","sources":["../../src/domain/config-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAGnG;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,kBAAkB,EACtD,OAAO,oBAAoB,CAAC,CAAC,CAAC,KAC7B,eAaF,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ScenariosObjectSchema } from '../schemas/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build a complete config from partial user input.
|
|
4
|
+
* Applies sensible defaults for missing values.
|
|
5
|
+
*
|
|
6
|
+
* **Validation:** Ensures scenarios object has a 'default' key.
|
|
7
|
+
*/
|
|
8
|
+
export const buildConfig = (input) => {
|
|
9
|
+
// Validate scenarios object has 'default' key (trust boundary)
|
|
10
|
+
ScenariosObjectSchema.parse(input.scenarios);
|
|
11
|
+
return {
|
|
12
|
+
enabled: input.enabled,
|
|
13
|
+
strictMode: input.strictMode ?? false,
|
|
14
|
+
endpoints: {
|
|
15
|
+
setScenario: input.endpoints?.setScenario ?? '/__scenario__',
|
|
16
|
+
getScenario: input.endpoints?.getScenario ?? '/__scenario__',
|
|
17
|
+
},
|
|
18
|
+
defaultTestId: input.defaultTestId ?? 'default-test',
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createScenarioManager } from './scenario-manager.js';
|
|
2
|
+
export { buildConfig } from './config-builder.js';
|
|
3
|
+
export { createResponseSelector } from './response-selector.js';
|
|
4
|
+
export { matchesRegex } from './regex-matching.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/domain/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HttpRequestContext } from '../types/scenario.js';
|
|
2
|
+
/**
|
|
3
|
+
* Extracts a value from HttpRequestContext based on a path expression.
|
|
4
|
+
*
|
|
5
|
+
* Supported path prefixes:
|
|
6
|
+
* - `body.field` - Extract from request body
|
|
7
|
+
* - `headers.field` - Extract from request headers
|
|
8
|
+
* - `query.field` - Extract from query parameters
|
|
9
|
+
*
|
|
10
|
+
* Supports nested paths: `body.user.profile.name`
|
|
11
|
+
*
|
|
12
|
+
* @param context - HTTP request context
|
|
13
|
+
* @param path - Path expression (e.g., 'body.userId', 'headers.x-session-id')
|
|
14
|
+
* @returns Extracted value, or undefined if path not found
|
|
15
|
+
*/
|
|
16
|
+
export declare const extractFromPath: (context: HttpRequestContext, path: string) => unknown;
|
|
17
|
+
//# sourceMappingURL=path-extraction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-extraction.d.ts","sourceRoot":"","sources":["../../src/domain/path-extraction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,kBAAkB,EAAE,MAAM,MAAM,KAAG,OA+B3E,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts a value from HttpRequestContext based on a path expression.
|
|
3
|
+
*
|
|
4
|
+
* Supported path prefixes:
|
|
5
|
+
* - `body.field` - Extract from request body
|
|
6
|
+
* - `headers.field` - Extract from request headers
|
|
7
|
+
* - `query.field` - Extract from query parameters
|
|
8
|
+
*
|
|
9
|
+
* Supports nested paths: `body.user.profile.name`
|
|
10
|
+
*
|
|
11
|
+
* @param context - HTTP request context
|
|
12
|
+
* @param path - Path expression (e.g., 'body.userId', 'headers.x-session-id')
|
|
13
|
+
* @returns Extracted value, or undefined if path not found
|
|
14
|
+
*/
|
|
15
|
+
export const extractFromPath = (context, path) => {
|
|
16
|
+
const segments = path.split('.');
|
|
17
|
+
// Guard: Need at least 2 segments (prefix.field)
|
|
18
|
+
if (segments.length < 2) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const prefix = segments[0];
|
|
22
|
+
// Guard: Must be valid prefix
|
|
23
|
+
if (prefix !== 'body' && prefix !== 'headers' && prefix !== 'query') {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const remainingPath = segments.slice(1);
|
|
27
|
+
const sourceMap = {
|
|
28
|
+
body: context.body,
|
|
29
|
+
headers: context.headers,
|
|
30
|
+
query: context.query,
|
|
31
|
+
};
|
|
32
|
+
const source = sourceMap[prefix];
|
|
33
|
+
// Guard: Source must exist
|
|
34
|
+
if (source === undefined || source === null) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return traversePath(source, remainingPath);
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Traverses a nested object path.
|
|
41
|
+
*
|
|
42
|
+
* @param obj - Object to traverse
|
|
43
|
+
* @param path - Path segments to follow
|
|
44
|
+
* @returns Value at path, or undefined if not found
|
|
45
|
+
*/
|
|
46
|
+
const traversePath = (obj, path) => {
|
|
47
|
+
// Guard: Empty path means we've reached the value
|
|
48
|
+
if (path.length === 0) {
|
|
49
|
+
return obj;
|
|
50
|
+
}
|
|
51
|
+
// Guard: Can only traverse objects
|
|
52
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const key = path[0];
|
|
56
|
+
const record = obj;
|
|
57
|
+
const value = record[key];
|
|
58
|
+
// Recursively traverse remaining path
|
|
59
|
+
return traversePath(value, path.slice(1));
|
|
60
|
+
};
|