@opscotch/resource-testkit 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/README.md +51 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/runtime/byte-context.d.ts +38 -0
- package/dist/runtime/byte-context.js +177 -0
- package/dist/runtime/context.d.ts +116 -0
- package/dist/runtime/context.js +408 -0
- package/dist/runtime/doc.d.ts +16 -0
- package/dist/runtime/doc.js +40 -0
- package/dist/runtime/run-resource.d.ts +15 -0
- package/dist/runtime/run-resource.js +33 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @opscotch/resource-testkit
|
|
2
|
+
|
|
3
|
+
`@opscotch/resource-testkit` is the npm-distributed JavaScript unit test runtime for Opscotch resource files.
|
|
4
|
+
|
|
5
|
+
It provides the same best-effort runtime shim used by the Docker-based Opscotch resource testkit, but packaged for local IDE use and direct Vitest/Jest-style test authoring.
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
This package is intended for unit testing Opscotch JavaScript resources.
|
|
10
|
+
|
|
11
|
+
It is not the production Opscotch agent runtime.
|
|
12
|
+
|
|
13
|
+
Some features are fully implemented in-memory for test realism, such as:
|
|
14
|
+
|
|
15
|
+
- `doc`
|
|
16
|
+
- `JavascriptContext` and `JavascriptStateContext` core flow
|
|
17
|
+
- byte-buffer handling
|
|
18
|
+
- send/metric/log recording
|
|
19
|
+
|
|
20
|
+
Some complex features remain stubbed or mock-first, such as:
|
|
21
|
+
|
|
22
|
+
- parts of `crypto()`
|
|
23
|
+
- parts of `files()`
|
|
24
|
+
- parts of `queue()`
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Install from npmjs:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -D @opscotch/resource-testkit@0.1.0 vitest typescript @types/node
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Example
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { createJavascriptContext, runResource } from '@opscotch/resource-testkit';
|
|
38
|
+
|
|
39
|
+
const context = createJavascriptContext({
|
|
40
|
+
body: '{"hello":"world"}',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await runResource({
|
|
44
|
+
resource: '/path/to/resource.js',
|
|
45
|
+
context,
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Publishing
|
|
50
|
+
|
|
51
|
+
This package is intended to be published to npmjs as a public scoped package.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createDocRuntime, type DocRuntime, } from './runtime/doc.js';
|
|
2
|
+
export { createByteContext, type ByteBufferHandle, type ByteContextRuntime, type ByteReaderRuntime, } from './runtime/byte-context.js';
|
|
3
|
+
export { createJavascriptContext, createAuthenticationJavascriptContext, createJavascriptStateContext, type ContextOptions, type JavascriptContextRuntime, type JavascriptStateRuntime, type StubPolicy, } from './runtime/context.js';
|
|
4
|
+
export { runResource, type RunResourceOptions, type RunResourceResult, } from './runtime/run-resource.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createDocRuntime, } from './runtime/doc.js';
|
|
2
|
+
export { createByteContext, } from './runtime/byte-context.js';
|
|
3
|
+
export { createJavascriptContext, createAuthenticationJavascriptContext, createJavascriptStateContext, } from './runtime/context.js';
|
|
4
|
+
export { runResource, } from './runtime/run-resource.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type ByteBufferHandle = string & {
|
|
2
|
+
readonly __brand: 'ByteBufferHandle';
|
|
3
|
+
};
|
|
4
|
+
export interface ByteReaderRuntime {
|
|
5
|
+
read(length: number): ByteBufferHandle;
|
|
6
|
+
readAll(): ByteBufferHandle;
|
|
7
|
+
available(): number;
|
|
8
|
+
}
|
|
9
|
+
export interface ByteContextRuntime {
|
|
10
|
+
create(size: number): ByteBufferHandle;
|
|
11
|
+
createFromByteArray(bytes: number[] | Uint8Array): ByteBufferHandle;
|
|
12
|
+
createFromString(value: string): ByteBufferHandle;
|
|
13
|
+
copy(source: ByteBufferHandle): ByteBufferHandle;
|
|
14
|
+
slice(source: ByteBufferHandle, offset: number, length: number): ByteBufferHandle;
|
|
15
|
+
split(buffer: ByteBufferHandle, delimiter: ByteBufferHandle, includeDelimiter: number): ByteBufferHandle[];
|
|
16
|
+
resize(buffer: ByteBufferHandle, newSize: number): ByteBufferHandle;
|
|
17
|
+
getSize(buffer: ByteBufferHandle): number;
|
|
18
|
+
toString(buffer: ByteBufferHandle): string;
|
|
19
|
+
writeByte(buffer: ByteBufferHandle, offset: number, value: number): void;
|
|
20
|
+
readByte(buffer: ByteBufferHandle, offset: number): number;
|
|
21
|
+
writeBytes(toBuffer: ByteBufferHandle, offset: number, sourceBuffer: ByteBufferHandle, sourceOffset: number, length: number): void;
|
|
22
|
+
concat(buffers: ByteBufferHandle[]): ByteBufferHandle;
|
|
23
|
+
hexToBinary(hex: string): ByteBufferHandle;
|
|
24
|
+
binaryToHex(buffer: ByteBufferHandle): string;
|
|
25
|
+
base64ToBinary(base64: string): ByteBufferHandle;
|
|
26
|
+
binaryToBase64(buffer: ByteBufferHandle): string;
|
|
27
|
+
hash(buffer: ByteBufferHandle): number;
|
|
28
|
+
sha256(buffer: ByteBufferHandle): ByteBufferHandle;
|
|
29
|
+
gzip(buffer: ByteBufferHandle): ByteBufferHandle;
|
|
30
|
+
gunzip(buffer: ByteBufferHandle): ByteBufferHandle;
|
|
31
|
+
zip(buffer: ByteBufferHandle): ByteBufferHandle;
|
|
32
|
+
unzip(buffer: ByteBufferHandle): ByteBufferHandle;
|
|
33
|
+
reader(buffer: ByteBufferHandle): ByteReaderRuntime;
|
|
34
|
+
release(buffers: ByteBufferHandle[]): void;
|
|
35
|
+
inspect(buffer: ByteBufferHandle): Uint8Array;
|
|
36
|
+
}
|
|
37
|
+
export declare function createByteContext(): ByteContextRuntime;
|
|
38
|
+
export declare function createRandomBufferHandle(byteContext: ByteContextRuntime, length: number): ByteBufferHandle;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { createHash, randomBytes as nodeRandomBytes } from 'node:crypto';
|
|
2
|
+
import { deflateSync, gunzipSync, gzipSync, inflateSync } from 'node:zlib';
|
|
3
|
+
export function createByteContext() {
|
|
4
|
+
let sequence = 0;
|
|
5
|
+
const buffers = new Map();
|
|
6
|
+
const nextHandle = () => `buf_${++sequence}`;
|
|
7
|
+
const put = (bytes) => {
|
|
8
|
+
const handle = nextHandle();
|
|
9
|
+
buffers.set(handle, bytes);
|
|
10
|
+
return handle;
|
|
11
|
+
};
|
|
12
|
+
const get = (handle) => {
|
|
13
|
+
const value = buffers.get(handle);
|
|
14
|
+
if (!value) {
|
|
15
|
+
throw new Error(`No buffer exists for key: ${handle}`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
const checkOffsets = (bytes, offset, length) => {
|
|
20
|
+
if (offset < 0 || offset > bytes.length || offset + length > bytes.length) {
|
|
21
|
+
throw new RangeError(`Invalid buffer offset (${offset}) and length (${length}) for buffer length (${bytes.length})`);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const toUint8Array = (value) => value instanceof Uint8Array ? new Uint8Array(value) : Uint8Array.from(value);
|
|
25
|
+
return {
|
|
26
|
+
create(size) {
|
|
27
|
+
return put(new Uint8Array(size));
|
|
28
|
+
},
|
|
29
|
+
createFromByteArray(bytes) {
|
|
30
|
+
return put(toUint8Array(bytes));
|
|
31
|
+
},
|
|
32
|
+
createFromString(value) {
|
|
33
|
+
return put(Buffer.from(value, 'utf8'));
|
|
34
|
+
},
|
|
35
|
+
copy(source) {
|
|
36
|
+
return put(new Uint8Array(get(source)));
|
|
37
|
+
},
|
|
38
|
+
slice(source, offset, length) {
|
|
39
|
+
const bytes = get(source);
|
|
40
|
+
checkOffsets(bytes, offset, length);
|
|
41
|
+
return put(bytes.slice(offset, offset + length));
|
|
42
|
+
},
|
|
43
|
+
split(buffer, delimiter, includeDelimiter) {
|
|
44
|
+
if (includeDelimiter < -1 || includeDelimiter > 1) {
|
|
45
|
+
throw new Error('includeDelimiter must be -1, 0, or 1');
|
|
46
|
+
}
|
|
47
|
+
const source = get(buffer);
|
|
48
|
+
const delim = get(delimiter);
|
|
49
|
+
if (delim.length === 0) {
|
|
50
|
+
return [put(source.slice())];
|
|
51
|
+
}
|
|
52
|
+
const matches = [];
|
|
53
|
+
for (let i = 0; i <= source.length - delim.length;) {
|
|
54
|
+
let matched = true;
|
|
55
|
+
for (let j = 0; j < delim.length; j += 1) {
|
|
56
|
+
if (source[i + j] !== delim[j]) {
|
|
57
|
+
matched = false;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (matched) {
|
|
62
|
+
matches.push(i);
|
|
63
|
+
i += delim.length;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
i += 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const parts = [];
|
|
70
|
+
let start = 0;
|
|
71
|
+
for (const match of matches) {
|
|
72
|
+
const end = includeDelimiter === -1 ? match + delim.length : match;
|
|
73
|
+
parts.push(put(source.slice(start, end)));
|
|
74
|
+
start = includeDelimiter === 1 ? match : match + delim.length;
|
|
75
|
+
}
|
|
76
|
+
parts.push(put(source.slice(start)));
|
|
77
|
+
return parts;
|
|
78
|
+
},
|
|
79
|
+
resize(buffer, newSize) {
|
|
80
|
+
const bytes = get(buffer);
|
|
81
|
+
const resized = new Uint8Array(newSize);
|
|
82
|
+
resized.set(bytes.slice(0, newSize));
|
|
83
|
+
return put(resized);
|
|
84
|
+
},
|
|
85
|
+
getSize(buffer) {
|
|
86
|
+
return get(buffer).length;
|
|
87
|
+
},
|
|
88
|
+
toString(buffer) {
|
|
89
|
+
return Buffer.from(get(buffer)).toString('utf8');
|
|
90
|
+
},
|
|
91
|
+
writeByte(buffer, offset, value) {
|
|
92
|
+
const bytes = get(buffer);
|
|
93
|
+
checkOffsets(bytes, offset, 1);
|
|
94
|
+
bytes[offset] = value & 0xff;
|
|
95
|
+
},
|
|
96
|
+
readByte(buffer, offset) {
|
|
97
|
+
return get(buffer)[offset] ?? 0;
|
|
98
|
+
},
|
|
99
|
+
writeBytes(toBuffer, offset, sourceBuffer, sourceOffset, length) {
|
|
100
|
+
const target = get(toBuffer);
|
|
101
|
+
const source = get(sourceBuffer);
|
|
102
|
+
checkOffsets(target, offset, length);
|
|
103
|
+
checkOffsets(source, sourceOffset, length);
|
|
104
|
+
target.set(source.slice(sourceOffset, sourceOffset + length), offset);
|
|
105
|
+
},
|
|
106
|
+
concat(inputBuffers) {
|
|
107
|
+
const joined = Buffer.concat(inputBuffers.map((buffer) => Buffer.from(get(buffer))));
|
|
108
|
+
return put(new Uint8Array(joined));
|
|
109
|
+
},
|
|
110
|
+
hexToBinary(hex) {
|
|
111
|
+
return put(new Uint8Array(Buffer.from(hex, 'hex')));
|
|
112
|
+
},
|
|
113
|
+
binaryToHex(buffer) {
|
|
114
|
+
return Buffer.from(get(buffer)).toString('hex');
|
|
115
|
+
},
|
|
116
|
+
base64ToBinary(base64) {
|
|
117
|
+
return put(new Uint8Array(Buffer.from(base64, 'base64')));
|
|
118
|
+
},
|
|
119
|
+
binaryToBase64(buffer) {
|
|
120
|
+
return Buffer.from(get(buffer)).toString('base64');
|
|
121
|
+
},
|
|
122
|
+
hash(buffer) {
|
|
123
|
+
const digest = createHash('sha256').update(get(buffer)).digest();
|
|
124
|
+
return digest.readUInt32BE(0);
|
|
125
|
+
},
|
|
126
|
+
sha256(buffer) {
|
|
127
|
+
return put(new Uint8Array(createHash('sha256').update(get(buffer)).digest()));
|
|
128
|
+
},
|
|
129
|
+
gzip(buffer) {
|
|
130
|
+
return put(new Uint8Array(gzipSync(get(buffer))));
|
|
131
|
+
},
|
|
132
|
+
gunzip(buffer) {
|
|
133
|
+
return put(new Uint8Array(gunzipSync(get(buffer))));
|
|
134
|
+
},
|
|
135
|
+
zip(buffer) {
|
|
136
|
+
return put(new Uint8Array(deflateSync(get(buffer))));
|
|
137
|
+
},
|
|
138
|
+
unzip(buffer) {
|
|
139
|
+
return put(new Uint8Array(inflateSync(get(buffer))));
|
|
140
|
+
},
|
|
141
|
+
reader(buffer) {
|
|
142
|
+
const bytes = get(buffer);
|
|
143
|
+
let offset = 0;
|
|
144
|
+
return {
|
|
145
|
+
read(length) {
|
|
146
|
+
const chunk = bytes.slice(offset, offset + length);
|
|
147
|
+
offset += chunk.length;
|
|
148
|
+
return put(chunk);
|
|
149
|
+
},
|
|
150
|
+
readAll() {
|
|
151
|
+
const chunk = bytes.slice(offset);
|
|
152
|
+
offset = bytes.length;
|
|
153
|
+
return put(chunk);
|
|
154
|
+
},
|
|
155
|
+
available() {
|
|
156
|
+
return bytes.length - offset;
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
release(inputBuffers) {
|
|
161
|
+
for (const handle of inputBuffers) {
|
|
162
|
+
const bytes = buffers.get(handle);
|
|
163
|
+
if (!bytes) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
bytes.fill(0);
|
|
167
|
+
buffers.delete(handle);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
inspect(buffer) {
|
|
171
|
+
return new Uint8Array(get(buffer));
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export function createRandomBufferHandle(byteContext, length) {
|
|
176
|
+
return byteContext.createFromByteArray(nodeRandomBytes(length));
|
|
177
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { type ByteContextRuntime, type ByteReaderRuntime } from './byte-context.js';
|
|
2
|
+
export type StubPolicy = 'warn' | 'throw';
|
|
3
|
+
type RuntimeFn = (...args: any[]) => unknown;
|
|
4
|
+
export interface JavascriptStateRuntime {
|
|
5
|
+
getMessageBodyAsString(): string | null;
|
|
6
|
+
getBody(): string | null;
|
|
7
|
+
getStepProperties(): Record<string, unknown>;
|
|
8
|
+
getProperty(key: string): unknown;
|
|
9
|
+
isErrored(): boolean;
|
|
10
|
+
getAllErrors(): string[];
|
|
11
|
+
hasUserErrors(): boolean;
|
|
12
|
+
hasSystemErrors(): boolean;
|
|
13
|
+
getUserErrors(): string[];
|
|
14
|
+
getSystemErrors(): string[];
|
|
15
|
+
getFirstError(errors: string[] | null | undefined): string | null;
|
|
16
|
+
}
|
|
17
|
+
export interface ContextOptions {
|
|
18
|
+
body?: string | null;
|
|
19
|
+
passedMessage?: string | null;
|
|
20
|
+
data?: Record<string, unknown> | null;
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
properties?: Record<string, unknown>;
|
|
23
|
+
stepProperties?: Record<string, unknown>;
|
|
24
|
+
persistedItems?: Record<string, string>;
|
|
25
|
+
timestamp?: number;
|
|
26
|
+
byteContext?: ByteContextRuntime;
|
|
27
|
+
stubPolicy?: StubPolicy;
|
|
28
|
+
sendToStep?: (call: SendToStepCall) => Partial<StateSnapshot> | JavascriptStateRuntime | void;
|
|
29
|
+
sendToStepAndForget?: (call: SendToStepCall) => void;
|
|
30
|
+
crypto?: Record<string, RuntimeFn>;
|
|
31
|
+
files?: Record<string, RuntimeFn>;
|
|
32
|
+
queue?: Record<string, RuntimeFn>;
|
|
33
|
+
restrictedData?: Record<string, string>;
|
|
34
|
+
authenticationProperties?: Record<string, Record<string, string>>;
|
|
35
|
+
}
|
|
36
|
+
export interface JavascriptContextRuntime extends JavascriptStateRuntime {
|
|
37
|
+
__warnings: string[];
|
|
38
|
+
__metrics: MetricRecord[];
|
|
39
|
+
__logs: string[];
|
|
40
|
+
__sendToStepCalls: SendToStepCall[];
|
|
41
|
+
__sendToStepAndForgetCalls: SendToStepCall[];
|
|
42
|
+
__splitReturnItems: string[];
|
|
43
|
+
__ended: boolean;
|
|
44
|
+
__url?: {
|
|
45
|
+
hostRef: string;
|
|
46
|
+
path: string;
|
|
47
|
+
};
|
|
48
|
+
__method?: string | null;
|
|
49
|
+
addUserError(error: string): void;
|
|
50
|
+
addSystemError(error: string): void;
|
|
51
|
+
getPassedMessageAsString(): string | null;
|
|
52
|
+
setBody(body: unknown): void;
|
|
53
|
+
setMessage(body: unknown): void;
|
|
54
|
+
setStream(reader: ByteReaderRuntime | null): void;
|
|
55
|
+
setStreamFromString(value: string): void;
|
|
56
|
+
getStream(): ByteReaderRuntime | null;
|
|
57
|
+
setHttpMultipartType(textPartName: string, contentType: string, binaryPartName: string, binaryContentType: string, binaryFileName: string): void;
|
|
58
|
+
sendToStep(routeName: string, body?: string | null, headers?: Record<string, string>): JavascriptStateRuntime;
|
|
59
|
+
sendToStep(deploymentAccessId: string, stepName: string, body?: string | null, headers?: Record<string, string>): JavascriptStateRuntime;
|
|
60
|
+
sendToStepAndForget(stepName: string, body?: string | null, headers?: Record<string, string>): void;
|
|
61
|
+
sendToStepAndForget(deploymentAccessId: string, stepName: string, body?: string | null, headers?: Record<string, string>): void;
|
|
62
|
+
sendMetric(...args: unknown[]): void;
|
|
63
|
+
diagnosticLog(message: string): void;
|
|
64
|
+
getHeader(name: string): string | null;
|
|
65
|
+
setHeader(name: string, value: string): void;
|
|
66
|
+
removeHeader(name: string): void;
|
|
67
|
+
removeAllHeaders(): void;
|
|
68
|
+
setData(data: string): void;
|
|
69
|
+
getData(key?: string): string | null;
|
|
70
|
+
getTimestamp(): number;
|
|
71
|
+
getTimestampManager(): {
|
|
72
|
+
get(key: string): number;
|
|
73
|
+
set(key: string, value: number | string): void;
|
|
74
|
+
firstTimestamp(): number | null;
|
|
75
|
+
minutesAgo(timestamp: number | string): number;
|
|
76
|
+
};
|
|
77
|
+
setProperty(key: string, value: unknown): void;
|
|
78
|
+
getProperty(key: string): unknown;
|
|
79
|
+
setPersistedItem(key: string, value: string): void;
|
|
80
|
+
getPersistedItem(key: string): string | null;
|
|
81
|
+
queue(): Record<string, RuntimeFn>;
|
|
82
|
+
counter(name: string, add: number): number;
|
|
83
|
+
setCounter(name: string, value: number): void;
|
|
84
|
+
bytes(): ByteContextRuntime;
|
|
85
|
+
crypto(): Record<string, RuntimeFn>;
|
|
86
|
+
files(id?: string): Record<string, RuntimeFn>;
|
|
87
|
+
getStepProperties(): Record<string, unknown>;
|
|
88
|
+
addSplitReturnItem(item: string): void;
|
|
89
|
+
setHttpMethod(method: string): void;
|
|
90
|
+
setUrl(hostRef: string, path: string): void;
|
|
91
|
+
end(): void;
|
|
92
|
+
sleep(ms: number): Promise<void>;
|
|
93
|
+
delta(key: string, currentValue: number): number;
|
|
94
|
+
regexMatch(regex: string, input: string): string[] | null;
|
|
95
|
+
mergeJsonStrings(one: string | null, two: string | null): string;
|
|
96
|
+
}
|
|
97
|
+
export interface MetricRecord {
|
|
98
|
+
args: unknown[];
|
|
99
|
+
}
|
|
100
|
+
export interface SendToStepCall {
|
|
101
|
+
deploymentAccessId?: string;
|
|
102
|
+
stepName: string;
|
|
103
|
+
body: string | null;
|
|
104
|
+
headers?: Record<string, string>;
|
|
105
|
+
}
|
|
106
|
+
interface StateSnapshot {
|
|
107
|
+
body: string | null;
|
|
108
|
+
properties: Record<string, unknown>;
|
|
109
|
+
stepProperties: Record<string, unknown>;
|
|
110
|
+
userErrors: string[];
|
|
111
|
+
systemErrors: string[];
|
|
112
|
+
}
|
|
113
|
+
export declare function createJavascriptStateContext(snapshot?: Partial<StateSnapshot>): JavascriptStateRuntime;
|
|
114
|
+
export declare function createJavascriptContext(options?: ContextOptions): JavascriptContextRuntime;
|
|
115
|
+
export declare function createAuthenticationJavascriptContext(options?: ContextOptions): JavascriptContextRuntime;
|
|
116
|
+
export {};
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
import { createByteContext, createRandomBufferHandle } from './byte-context.js';
|
|
3
|
+
const RESTRICTED_AUTH_METHODS = new Set([
|
|
4
|
+
'setStream',
|
|
5
|
+
'setStreamFromString',
|
|
6
|
+
'getStream',
|
|
7
|
+
'setHttpMultipartType',
|
|
8
|
+
'diagnostic',
|
|
9
|
+
'sendMetric',
|
|
10
|
+
'files',
|
|
11
|
+
'agentControl',
|
|
12
|
+
]);
|
|
13
|
+
const noopObject = () => ({});
|
|
14
|
+
export function createJavascriptStateContext(snapshot = {}) {
|
|
15
|
+
const state = {
|
|
16
|
+
body: snapshot.body ?? null,
|
|
17
|
+
properties: { ...(snapshot.properties ?? {}) },
|
|
18
|
+
stepProperties: { ...(snapshot.stepProperties ?? {}) },
|
|
19
|
+
userErrors: [...(snapshot.userErrors ?? [])],
|
|
20
|
+
systemErrors: [...(snapshot.systemErrors ?? [])],
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
getMessageBodyAsString() {
|
|
24
|
+
return state.body;
|
|
25
|
+
},
|
|
26
|
+
getBody() {
|
|
27
|
+
return state.body;
|
|
28
|
+
},
|
|
29
|
+
getStepProperties() {
|
|
30
|
+
return state.stepProperties;
|
|
31
|
+
},
|
|
32
|
+
getProperty(key) {
|
|
33
|
+
return state.properties[key];
|
|
34
|
+
},
|
|
35
|
+
isErrored() {
|
|
36
|
+
return state.userErrors.length > 0 || state.systemErrors.length > 0;
|
|
37
|
+
},
|
|
38
|
+
getAllErrors() {
|
|
39
|
+
return [...state.userErrors, ...state.systemErrors];
|
|
40
|
+
},
|
|
41
|
+
hasUserErrors() {
|
|
42
|
+
return state.userErrors.length > 0;
|
|
43
|
+
},
|
|
44
|
+
hasSystemErrors() {
|
|
45
|
+
return state.systemErrors.length > 0;
|
|
46
|
+
},
|
|
47
|
+
getUserErrors() {
|
|
48
|
+
return [...state.userErrors];
|
|
49
|
+
},
|
|
50
|
+
getSystemErrors() {
|
|
51
|
+
return [...state.systemErrors];
|
|
52
|
+
},
|
|
53
|
+
getFirstError(errors) {
|
|
54
|
+
return errors?.[0] ?? null;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function createJavascriptContext(options = {}) {
|
|
59
|
+
const warnings = [];
|
|
60
|
+
const metrics = [];
|
|
61
|
+
const logs = [];
|
|
62
|
+
const sendToStepCalls = [];
|
|
63
|
+
const sendToStepAndForgetCalls = [];
|
|
64
|
+
const byteContext = options.byteContext ?? createByteContext();
|
|
65
|
+
const stubPolicy = options.stubPolicy ?? 'warn';
|
|
66
|
+
const properties = { ...(options.properties ?? {}) };
|
|
67
|
+
const stepProperties = { ...(options.stepProperties ?? {}) };
|
|
68
|
+
const persistedItems = new Map(Object.entries(options.persistedItems ?? {}));
|
|
69
|
+
const headers = new Map(Object.entries(options.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
|
|
70
|
+
const counters = new Map();
|
|
71
|
+
const timestamps = new Map();
|
|
72
|
+
const splitReturnItems = [];
|
|
73
|
+
const data = { ...(options.data ?? {}) };
|
|
74
|
+
let body = options.body ?? null;
|
|
75
|
+
let passedMessage = options.passedMessage ?? null;
|
|
76
|
+
let currentStream = null;
|
|
77
|
+
let ended = false;
|
|
78
|
+
let url;
|
|
79
|
+
let method = null;
|
|
80
|
+
const userErrors = [];
|
|
81
|
+
const systemErrors = [];
|
|
82
|
+
let multipart;
|
|
83
|
+
const warnOrThrow = (name) => {
|
|
84
|
+
const message = `Stub invoked without explicit mock: ${name}`;
|
|
85
|
+
if (stubPolicy === 'throw') {
|
|
86
|
+
throw new Error(message);
|
|
87
|
+
}
|
|
88
|
+
warnings.push(message);
|
|
89
|
+
};
|
|
90
|
+
const createGenericStub = (namespace) => new Proxy(noopObject(), {
|
|
91
|
+
get(target, prop) {
|
|
92
|
+
if (typeof prop !== 'string') {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
if (prop in target) {
|
|
96
|
+
return target[prop];
|
|
97
|
+
}
|
|
98
|
+
return (...args) => {
|
|
99
|
+
warnOrThrow(`${namespace}.${prop}`);
|
|
100
|
+
if (namespace === 'crypto' && prop === 'randomBytes') {
|
|
101
|
+
return createRandomBufferHandle(byteContext, Number(args[0] ?? 0));
|
|
102
|
+
}
|
|
103
|
+
if (namespace === 'crypto' && prop === 'hash') {
|
|
104
|
+
const input = args[0];
|
|
105
|
+
return byteContext.sha256(input);
|
|
106
|
+
}
|
|
107
|
+
if (namespace === 'crypto' && prop === 'hmacSha256') {
|
|
108
|
+
const key = args[0];
|
|
109
|
+
const bytes = args[1];
|
|
110
|
+
const digest = createHmac('sha256', byteContext.inspect(key)).update(byteContext.inspect(bytes)).digest();
|
|
111
|
+
return byteContext.createFromByteArray(digest);
|
|
112
|
+
}
|
|
113
|
+
if (namespace === 'crypto' && prop === 'verifySignature') {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (namespace === 'crypto' && prop === 'registerKey') {
|
|
117
|
+
return 'stub-key-id';
|
|
118
|
+
}
|
|
119
|
+
if (namespace === 'crypto' && prop === 'generateKeyPair') {
|
|
120
|
+
return [byteContext.create(0), byteContext.create(0)];
|
|
121
|
+
}
|
|
122
|
+
if (namespace === 'crypto' && prop === 'passwordHash') {
|
|
123
|
+
return [byteContext.create(0), byteContext.create(0)];
|
|
124
|
+
}
|
|
125
|
+
if (namespace === 'files') {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
if (namespace === 'queue') {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
return byteContext.create(0);
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
const cryptoContext = Object.assign(createGenericStub('crypto'), options.crypto ?? {});
|
|
136
|
+
const filesContext = Object.assign(createGenericStub('files'), options.files ?? {});
|
|
137
|
+
const queueContext = Object.assign(createGenericStub('queue'), options.queue ?? {});
|
|
138
|
+
const runtime = {
|
|
139
|
+
__warnings: warnings,
|
|
140
|
+
__metrics: metrics,
|
|
141
|
+
__logs: logs,
|
|
142
|
+
__sendToStepCalls: sendToStepCalls,
|
|
143
|
+
__sendToStepAndForgetCalls: sendToStepAndForgetCalls,
|
|
144
|
+
__splitReturnItems: splitReturnItems,
|
|
145
|
+
__ended: ended,
|
|
146
|
+
getPassedMessageAsString() {
|
|
147
|
+
return passedMessage;
|
|
148
|
+
},
|
|
149
|
+
getMessageBodyAsString() {
|
|
150
|
+
return body;
|
|
151
|
+
},
|
|
152
|
+
getBody() {
|
|
153
|
+
return body ?? passedMessage;
|
|
154
|
+
},
|
|
155
|
+
setBody(value) {
|
|
156
|
+
body = value == null ? null : String(value);
|
|
157
|
+
},
|
|
158
|
+
setMessage(value) {
|
|
159
|
+
body = value == null ? null : String(value);
|
|
160
|
+
},
|
|
161
|
+
setStream(reader) {
|
|
162
|
+
currentStream = reader;
|
|
163
|
+
if (reader != null) {
|
|
164
|
+
body = null;
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
setStreamFromString(value) {
|
|
168
|
+
const handle = byteContext.createFromString(value);
|
|
169
|
+
currentStream = byteContext.reader(handle);
|
|
170
|
+
body = null;
|
|
171
|
+
},
|
|
172
|
+
getStream() {
|
|
173
|
+
return currentStream;
|
|
174
|
+
},
|
|
175
|
+
setHttpMultipartType(textPartName, contentType, binaryPartName, binaryContentType, binaryFileName) {
|
|
176
|
+
multipart = { textPartName, contentType, binaryPartName, binaryContentType, binaryFileName };
|
|
177
|
+
void multipart;
|
|
178
|
+
},
|
|
179
|
+
sendToStep: ((...args) => {
|
|
180
|
+
const call = normalizeSendCall(args);
|
|
181
|
+
sendToStepCalls.push(call);
|
|
182
|
+
const returned = options.sendToStep?.(call);
|
|
183
|
+
if (!returned) {
|
|
184
|
+
return createJavascriptStateContext({ body: call.body });
|
|
185
|
+
}
|
|
186
|
+
if (isJavascriptStateRuntime(returned)) {
|
|
187
|
+
return returned;
|
|
188
|
+
}
|
|
189
|
+
return createJavascriptStateContext(returned);
|
|
190
|
+
}),
|
|
191
|
+
sendToStepAndForget: ((...args) => {
|
|
192
|
+
const call = normalizeSendCall(args);
|
|
193
|
+
sendToStepAndForgetCalls.push(call);
|
|
194
|
+
options.sendToStepAndForget?.(call);
|
|
195
|
+
}),
|
|
196
|
+
sendMetric(...args) {
|
|
197
|
+
metrics.push({ args });
|
|
198
|
+
},
|
|
199
|
+
diagnosticLog(message) {
|
|
200
|
+
logs.push(message);
|
|
201
|
+
},
|
|
202
|
+
addUserError(error) {
|
|
203
|
+
userErrors.push(error);
|
|
204
|
+
},
|
|
205
|
+
addSystemError(error) {
|
|
206
|
+
systemErrors.push(error);
|
|
207
|
+
},
|
|
208
|
+
isErrored() {
|
|
209
|
+
return userErrors.length > 0 || systemErrors.length > 0;
|
|
210
|
+
},
|
|
211
|
+
getAllErrors() {
|
|
212
|
+
return [...userErrors, ...systemErrors];
|
|
213
|
+
},
|
|
214
|
+
hasUserErrors() {
|
|
215
|
+
return userErrors.length > 0;
|
|
216
|
+
},
|
|
217
|
+
hasSystemErrors() {
|
|
218
|
+
return systemErrors.length > 0;
|
|
219
|
+
},
|
|
220
|
+
getUserErrors() {
|
|
221
|
+
return [...userErrors];
|
|
222
|
+
},
|
|
223
|
+
getSystemErrors() {
|
|
224
|
+
return [...systemErrors];
|
|
225
|
+
},
|
|
226
|
+
getFirstError(errors) {
|
|
227
|
+
return errors?.[0] ?? null;
|
|
228
|
+
},
|
|
229
|
+
getHeader(name) {
|
|
230
|
+
return headers.get(name.toLowerCase()) ?? null;
|
|
231
|
+
},
|
|
232
|
+
setHeader(name, value) {
|
|
233
|
+
headers.set(name.toLowerCase(), value);
|
|
234
|
+
},
|
|
235
|
+
removeHeader(name) {
|
|
236
|
+
headers.delete(name.toLowerCase());
|
|
237
|
+
},
|
|
238
|
+
removeAllHeaders() {
|
|
239
|
+
headers.clear();
|
|
240
|
+
},
|
|
241
|
+
setData(rawData) {
|
|
242
|
+
Object.keys(data).forEach((key) => delete data[key]);
|
|
243
|
+
const parsed = JSON.parse(rawData);
|
|
244
|
+
Object.assign(data, parsed);
|
|
245
|
+
},
|
|
246
|
+
getData(key) {
|
|
247
|
+
if (key == null) {
|
|
248
|
+
return JSON.stringify(data);
|
|
249
|
+
}
|
|
250
|
+
const value = data[key];
|
|
251
|
+
if (value == null) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
255
|
+
},
|
|
256
|
+
getTimestamp() {
|
|
257
|
+
return options.timestamp ?? Date.now();
|
|
258
|
+
},
|
|
259
|
+
getTimestampManager() {
|
|
260
|
+
return {
|
|
261
|
+
get(key) {
|
|
262
|
+
return timestamps.get(key) ?? 0;
|
|
263
|
+
},
|
|
264
|
+
set(key, value) {
|
|
265
|
+
timestamps.set(key, Number(value));
|
|
266
|
+
},
|
|
267
|
+
firstTimestamp() {
|
|
268
|
+
const values = Array.from(timestamps.values()).filter((value) => Number.isFinite(value));
|
|
269
|
+
if (values.length === 0) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
return Math.min(...values);
|
|
273
|
+
},
|
|
274
|
+
minutesAgo(timestamp) {
|
|
275
|
+
const then = Number(timestamp);
|
|
276
|
+
return Math.floor((runtime.getTimestamp() - then) / 60000);
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
},
|
|
280
|
+
setProperty(key, value) {
|
|
281
|
+
properties[key] = value;
|
|
282
|
+
},
|
|
283
|
+
getProperty(key) {
|
|
284
|
+
return properties[key];
|
|
285
|
+
},
|
|
286
|
+
setPersistedItem(key, value) {
|
|
287
|
+
persistedItems.set(key, value);
|
|
288
|
+
},
|
|
289
|
+
getPersistedItem(key) {
|
|
290
|
+
return persistedItems.get(key) ?? null;
|
|
291
|
+
},
|
|
292
|
+
queue() {
|
|
293
|
+
return queueContext;
|
|
294
|
+
},
|
|
295
|
+
counter(name, add) {
|
|
296
|
+
const next = (counters.get(name) ?? 0) + add;
|
|
297
|
+
counters.set(name, next);
|
|
298
|
+
return next;
|
|
299
|
+
},
|
|
300
|
+
setCounter(name, value) {
|
|
301
|
+
counters.set(name, value);
|
|
302
|
+
},
|
|
303
|
+
bytes() {
|
|
304
|
+
return byteContext;
|
|
305
|
+
},
|
|
306
|
+
crypto() {
|
|
307
|
+
return cryptoContext;
|
|
308
|
+
},
|
|
309
|
+
files() {
|
|
310
|
+
return filesContext;
|
|
311
|
+
},
|
|
312
|
+
getStepProperties() {
|
|
313
|
+
return stepProperties;
|
|
314
|
+
},
|
|
315
|
+
addSplitReturnItem(item) {
|
|
316
|
+
splitReturnItems.push(item);
|
|
317
|
+
void splitReturnItems;
|
|
318
|
+
},
|
|
319
|
+
setHttpMethod(nextMethod) {
|
|
320
|
+
method = nextMethod.toUpperCase();
|
|
321
|
+
runtime.__method = method;
|
|
322
|
+
},
|
|
323
|
+
setUrl(hostRef, path) {
|
|
324
|
+
url = { hostRef, path };
|
|
325
|
+
runtime.__url = url;
|
|
326
|
+
},
|
|
327
|
+
end() {
|
|
328
|
+
ended = true;
|
|
329
|
+
runtime.__ended = true;
|
|
330
|
+
},
|
|
331
|
+
async sleep(ms) {
|
|
332
|
+
if (ms <= 0) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
336
|
+
},
|
|
337
|
+
delta(key, currentValue) {
|
|
338
|
+
const lookupKey = `DELTA-${key}`;
|
|
339
|
+
const previous = Number(persistedItems.get(lookupKey) ?? '0');
|
|
340
|
+
persistedItems.set(lookupKey, String(currentValue));
|
|
341
|
+
return currentValue - previous;
|
|
342
|
+
},
|
|
343
|
+
regexMatch(regex, input) {
|
|
344
|
+
const match = new RegExp(regex).exec(input);
|
|
345
|
+
return match ? Array.from(match) : null;
|
|
346
|
+
},
|
|
347
|
+
mergeJsonStrings(one, two) {
|
|
348
|
+
if (!one && !two) {
|
|
349
|
+
return '{}';
|
|
350
|
+
}
|
|
351
|
+
if (!one) {
|
|
352
|
+
return two ?? '{}';
|
|
353
|
+
}
|
|
354
|
+
if (!two) {
|
|
355
|
+
return one;
|
|
356
|
+
}
|
|
357
|
+
return JSON.stringify(deepMerge(JSON.parse(one), JSON.parse(two)));
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
return runtime;
|
|
361
|
+
}
|
|
362
|
+
export function createAuthenticationJavascriptContext(options = {}) {
|
|
363
|
+
const runtime = createJavascriptContext(options);
|
|
364
|
+
const restrictedData = new Map(Object.entries(options.restrictedData ?? {}));
|
|
365
|
+
const authProperties = new Map(Object.entries(options.authenticationProperties ?? {}).map(([step, values]) => [step, new Map(Object.entries(values))]));
|
|
366
|
+
const authRuntime = runtime;
|
|
367
|
+
authRuntime.getRestrictedDataFromHost = (host) => restrictedData.get(host) ?? null;
|
|
368
|
+
authRuntime.getAuthenticationPropertiesFromStep = (stepId, key) => authProperties.get(stepId)?.get(key) ?? null;
|
|
369
|
+
authRuntime.setAuthenticationPropertiesOnStep = (stepName, _expiresInMs, key, value) => {
|
|
370
|
+
if (!authProperties.has(stepName)) {
|
|
371
|
+
authProperties.set(stepName, new Map());
|
|
372
|
+
}
|
|
373
|
+
authProperties.get(stepName).set(key, value);
|
|
374
|
+
};
|
|
375
|
+
for (const method of RESTRICTED_AUTH_METHODS) {
|
|
376
|
+
authRuntime[method] = (() => {
|
|
377
|
+
throw new Error(`Operation not supported in authentication context: ${method}`);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return authRuntime;
|
|
381
|
+
}
|
|
382
|
+
function normalizeSendCall(args) {
|
|
383
|
+
if (args.length >= 3 && typeof args[2] !== 'object') {
|
|
384
|
+
const [deploymentAccessId, stepName, body, headers] = args;
|
|
385
|
+
return { deploymentAccessId, stepName, body: body ?? null, headers };
|
|
386
|
+
}
|
|
387
|
+
const [stepName, body, headers] = args;
|
|
388
|
+
return { stepName, body: body ?? null, headers };
|
|
389
|
+
}
|
|
390
|
+
function isJavascriptStateRuntime(value) {
|
|
391
|
+
return typeof value === 'object' && value != null && typeof value.getBody === 'function';
|
|
392
|
+
}
|
|
393
|
+
function deepMerge(left, right) {
|
|
394
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
395
|
+
return right;
|
|
396
|
+
}
|
|
397
|
+
if (!isObject(left) || !isObject(right)) {
|
|
398
|
+
return right;
|
|
399
|
+
}
|
|
400
|
+
const merged = { ...left };
|
|
401
|
+
for (const [key, value] of Object.entries(right)) {
|
|
402
|
+
merged[key] = key in merged ? deepMerge(merged[key], value) : value;
|
|
403
|
+
}
|
|
404
|
+
return merged;
|
|
405
|
+
}
|
|
406
|
+
function isObject(value) {
|
|
407
|
+
return typeof value === 'object' && value != null && !Array.isArray(value);
|
|
408
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type DocRunFunction = () => unknown | Promise<unknown>;
|
|
2
|
+
export interface DocRuntime {
|
|
3
|
+
inSchemaValue?: unknown;
|
|
4
|
+
outSchemaValue?: unknown;
|
|
5
|
+
dataSchemaValue?: unknown;
|
|
6
|
+
descriptionValue?: string;
|
|
7
|
+
runFunction?: DocRunFunction;
|
|
8
|
+
userErrors: boolean;
|
|
9
|
+
inSchema(schema: unknown): DocRuntime;
|
|
10
|
+
outSchema(schema: unknown): DocRuntime;
|
|
11
|
+
dataSchema(schema: unknown): DocRuntime;
|
|
12
|
+
run(run: unknown): DocRuntime;
|
|
13
|
+
description(description: string): DocRuntime;
|
|
14
|
+
asUserErrors(): DocRuntime;
|
|
15
|
+
}
|
|
16
|
+
export declare function createDocRuntime(): DocRuntime;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function createDocRuntime() {
|
|
2
|
+
const runtime = {
|
|
3
|
+
userErrors: false,
|
|
4
|
+
inSchemaValue: undefined,
|
|
5
|
+
outSchemaValue: undefined,
|
|
6
|
+
dataSchemaValue: undefined,
|
|
7
|
+
descriptionValue: undefined,
|
|
8
|
+
runFunction: undefined,
|
|
9
|
+
inSchema(schema) {
|
|
10
|
+
runtime.inSchemaValue = schema;
|
|
11
|
+
return runtime;
|
|
12
|
+
},
|
|
13
|
+
outSchema(schema) {
|
|
14
|
+
runtime.outSchemaValue = schema;
|
|
15
|
+
return runtime;
|
|
16
|
+
},
|
|
17
|
+
dataSchema(schema) {
|
|
18
|
+
runtime.dataSchemaValue = schema;
|
|
19
|
+
return runtime;
|
|
20
|
+
},
|
|
21
|
+
run(run) {
|
|
22
|
+
if (typeof run === 'function') {
|
|
23
|
+
runtime.runFunction = run;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
throw new TypeError('doc.run(...) expects a function');
|
|
27
|
+
}
|
|
28
|
+
return runtime;
|
|
29
|
+
},
|
|
30
|
+
description(description) {
|
|
31
|
+
runtime.descriptionValue = description;
|
|
32
|
+
return runtime;
|
|
33
|
+
},
|
|
34
|
+
asUserErrors() {
|
|
35
|
+
runtime.userErrors = true;
|
|
36
|
+
return runtime;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
return runtime;
|
|
40
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type DocRuntime } from './doc.js';
|
|
2
|
+
import { type ContextOptions, type JavascriptContextRuntime } from './context.js';
|
|
3
|
+
export interface RunResourceOptions {
|
|
4
|
+
resource: string;
|
|
5
|
+
context?: JavascriptContextRuntime;
|
|
6
|
+
contextOptions?: ContextOptions;
|
|
7
|
+
doc?: DocRuntime;
|
|
8
|
+
execute?: 'auto' | 'registration' | 'run';
|
|
9
|
+
}
|
|
10
|
+
export interface RunResourceResult {
|
|
11
|
+
context: JavascriptContextRuntime;
|
|
12
|
+
doc: DocRuntime;
|
|
13
|
+
returnValue: unknown;
|
|
14
|
+
}
|
|
15
|
+
export declare function runResource(options: RunResourceOptions): Promise<RunResourceResult>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import vm from 'node:vm';
|
|
3
|
+
import { createDocRuntime } from './doc.js';
|
|
4
|
+
import { createJavascriptContext, } from './context.js';
|
|
5
|
+
export async function runResource(options) {
|
|
6
|
+
const source = await fs.readFile(options.resource, 'utf8');
|
|
7
|
+
const context = options.context ?? createJavascriptContext(options.contextOptions);
|
|
8
|
+
const doc = options.doc ?? createDocRuntime();
|
|
9
|
+
const sandbox = {
|
|
10
|
+
context,
|
|
11
|
+
doc,
|
|
12
|
+
console,
|
|
13
|
+
JSON,
|
|
14
|
+
Buffer,
|
|
15
|
+
setTimeout,
|
|
16
|
+
clearTimeout,
|
|
17
|
+
};
|
|
18
|
+
const vmContext = vm.createContext(sandbox);
|
|
19
|
+
const script = new vm.Script(source, {
|
|
20
|
+
filename: options.resource,
|
|
21
|
+
});
|
|
22
|
+
let returnValue = script.runInContext(vmContext);
|
|
23
|
+
const execute = options.execute ?? 'auto';
|
|
24
|
+
const shouldRun = execute === 'run' || (execute === 'auto' && typeof doc.runFunction === 'function');
|
|
25
|
+
if (shouldRun && doc.runFunction) {
|
|
26
|
+
returnValue = await doc.runFunction();
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
context,
|
|
30
|
+
doc,
|
|
31
|
+
returnValue,
|
|
32
|
+
};
|
|
33
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opscotch/resource-testkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Unit test runtime shim for Opscotch JavaScript resources.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/opscotch/opscotch.git"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"registry": "https://registry.npmjs.org",
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"sync:source": "node ../../shared/opscotch-resource-testkit-source/sync-to-consumer.mjs ./src",
|
|
29
|
+
"build": "tsc -p tsconfig.build.json",
|
|
30
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
31
|
+
"prepack": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^24.6.0",
|
|
35
|
+
"typescript": "^5.9.3"
|
|
36
|
+
}
|
|
37
|
+
}
|