@outputai/core 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 +201 -0
- package/README.md +11 -0
- package/bin/healthcheck.mjs +36 -0
- package/bin/healthcheck.spec.js +90 -0
- package/bin/worker.sh +26 -0
- package/package.json +67 -0
- package/src/activity_integration/context.d.ts +27 -0
- package/src/activity_integration/context.js +17 -0
- package/src/activity_integration/context.spec.js +42 -0
- package/src/activity_integration/events.d.ts +7 -0
- package/src/activity_integration/events.js +10 -0
- package/src/activity_integration/index.d.ts +9 -0
- package/src/activity_integration/index.js +3 -0
- package/src/activity_integration/tracing.d.ts +32 -0
- package/src/activity_integration/tracing.js +37 -0
- package/src/async_storage.js +19 -0
- package/src/bus.js +3 -0
- package/src/consts.js +32 -0
- package/src/errors.d.ts +15 -0
- package/src/errors.js +14 -0
- package/src/hooks/index.d.ts +28 -0
- package/src/hooks/index.js +32 -0
- package/src/index.d.ts +49 -0
- package/src/index.js +4 -0
- package/src/interface/evaluation_result.d.ts +173 -0
- package/src/interface/evaluation_result.js +215 -0
- package/src/interface/evaluator.d.ts +70 -0
- package/src/interface/evaluator.js +34 -0
- package/src/interface/evaluator.spec.js +565 -0
- package/src/interface/index.d.ts +9 -0
- package/src/interface/index.js +26 -0
- package/src/interface/step.d.ts +138 -0
- package/src/interface/step.js +22 -0
- package/src/interface/types.d.ts +27 -0
- package/src/interface/validations/runtime.js +20 -0
- package/src/interface/validations/runtime.spec.js +29 -0
- package/src/interface/validations/schema_utils.js +8 -0
- package/src/interface/validations/schema_utils.spec.js +67 -0
- package/src/interface/validations/static.js +136 -0
- package/src/interface/validations/static.spec.js +366 -0
- package/src/interface/webhook.d.ts +84 -0
- package/src/interface/webhook.js +64 -0
- package/src/interface/webhook.spec.js +122 -0
- package/src/interface/workflow.d.ts +273 -0
- package/src/interface/workflow.js +128 -0
- package/src/interface/workflow.spec.js +467 -0
- package/src/interface/workflow_context.js +31 -0
- package/src/interface/workflow_utils.d.ts +76 -0
- package/src/interface/workflow_utils.js +50 -0
- package/src/interface/workflow_utils.spec.js +190 -0
- package/src/interface/zod_integration.spec.js +646 -0
- package/src/internal_activities/index.js +66 -0
- package/src/internal_activities/index.spec.js +102 -0
- package/src/logger.js +73 -0
- package/src/tracing/internal_interface.js +71 -0
- package/src/tracing/processors/local/index.js +111 -0
- package/src/tracing/processors/local/index.spec.js +149 -0
- package/src/tracing/processors/s3/configs.js +31 -0
- package/src/tracing/processors/s3/configs.spec.js +64 -0
- package/src/tracing/processors/s3/index.js +114 -0
- package/src/tracing/processors/s3/index.spec.js +153 -0
- package/src/tracing/processors/s3/redis_client.js +62 -0
- package/src/tracing/processors/s3/redis_client.spec.js +185 -0
- package/src/tracing/processors/s3/s3_client.js +27 -0
- package/src/tracing/processors/s3/s3_client.spec.js +62 -0
- package/src/tracing/tools/build_trace_tree.js +83 -0
- package/src/tracing/tools/build_trace_tree.spec.js +135 -0
- package/src/tracing/tools/utils.js +21 -0
- package/src/tracing/tools/utils.spec.js +14 -0
- package/src/tracing/trace_engine.js +97 -0
- package/src/tracing/trace_engine.spec.js +199 -0
- package/src/utils/index.d.ts +134 -0
- package/src/utils/index.js +2 -0
- package/src/utils/resolve_invocation_dir.js +34 -0
- package/src/utils/resolve_invocation_dir.spec.js +102 -0
- package/src/utils/utils.js +211 -0
- package/src/utils/utils.spec.js +448 -0
- package/src/worker/bundler_options.js +43 -0
- package/src/worker/catalog_workflow/catalog.js +114 -0
- package/src/worker/catalog_workflow/index.js +54 -0
- package/src/worker/catalog_workflow/index.spec.js +196 -0
- package/src/worker/catalog_workflow/workflow.js +24 -0
- package/src/worker/configs.js +49 -0
- package/src/worker/configs.spec.js +130 -0
- package/src/worker/index.js +89 -0
- package/src/worker/index.spec.js +177 -0
- package/src/worker/interceptors/activity.js +62 -0
- package/src/worker/interceptors/activity.spec.js +212 -0
- package/src/worker/interceptors/workflow.js +70 -0
- package/src/worker/interceptors/workflow.spec.js +167 -0
- package/src/worker/interceptors.js +10 -0
- package/src/worker/loader.js +151 -0
- package/src/worker/loader.spec.js +236 -0
- package/src/worker/loader_tools.js +132 -0
- package/src/worker/loader_tools.spec.js +156 -0
- package/src/worker/log_hooks.js +95 -0
- package/src/worker/log_hooks.spec.js +217 -0
- package/src/worker/sandboxed_utils.js +18 -0
- package/src/worker/shutdown.js +26 -0
- package/src/worker/shutdown.spec.js +82 -0
- package/src/worker/sinks.js +74 -0
- package/src/worker/start_catalog.js +36 -0
- package/src/worker/start_catalog.spec.js +118 -0
- package/src/worker/webpack_loaders/consts.js +9 -0
- package/src/worker/webpack_loaders/tools.js +548 -0
- package/src/worker/webpack_loaders/tools.spec.js +330 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +221 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +336 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +61 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +216 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +196 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +123 -0
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +205 -0
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +613 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import {
|
|
4
|
+
clone,
|
|
5
|
+
serializeBodyAndInferContentType,
|
|
6
|
+
serializeFetchResponse,
|
|
7
|
+
deepMerge,
|
|
8
|
+
isPlainObject,
|
|
9
|
+
toUrlSafeBase64
|
|
10
|
+
} from './utils.js';
|
|
11
|
+
|
|
12
|
+
describe( 'clone', () => {
|
|
13
|
+
it( 'produces a deep copy without shared references', () => {
|
|
14
|
+
const original = { a: 1, nested: { b: 2 } };
|
|
15
|
+
const copied = clone( original );
|
|
16
|
+
|
|
17
|
+
copied.nested.b = 3;
|
|
18
|
+
|
|
19
|
+
expect( original.nested.b ).toBe( 2 );
|
|
20
|
+
expect( copied.nested.b ).toBe( 3 );
|
|
21
|
+
expect( copied ).not.toBe( original );
|
|
22
|
+
} );
|
|
23
|
+
} );
|
|
24
|
+
|
|
25
|
+
describe( 'serializeFetchResponse', () => {
|
|
26
|
+
it( 'serializes JSON response body and flattens headers', async () => {
|
|
27
|
+
const payload = { a: 1, b: 'two' };
|
|
28
|
+
const response = new Response( JSON.stringify( payload ), {
|
|
29
|
+
status: 200,
|
|
30
|
+
statusText: 'OK',
|
|
31
|
+
headers: { 'content-type': 'application/json' }
|
|
32
|
+
} );
|
|
33
|
+
|
|
34
|
+
const result = await serializeFetchResponse( response );
|
|
35
|
+
expect( result.status ).toBe( 200 );
|
|
36
|
+
expect( result.ok ).toBe( true );
|
|
37
|
+
expect( result.statusText ).toBe( 'OK' );
|
|
38
|
+
expect( result.headers['content-type'] ).toContain( 'application/json' );
|
|
39
|
+
expect( result.body ).toEqual( payload );
|
|
40
|
+
} );
|
|
41
|
+
|
|
42
|
+
it( 'serializes text/* response via text()', async () => {
|
|
43
|
+
const bodyText = 'hello world';
|
|
44
|
+
const response = new Response( bodyText, {
|
|
45
|
+
status: 201,
|
|
46
|
+
statusText: 'Created',
|
|
47
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' }
|
|
48
|
+
} );
|
|
49
|
+
|
|
50
|
+
const result = await serializeFetchResponse( response );
|
|
51
|
+
expect( result.status ).toBe( 201 );
|
|
52
|
+
expect( result.ok ).toBe( true );
|
|
53
|
+
expect( result.statusText ).toBe( 'Created' );
|
|
54
|
+
expect( result.headers['content-type'] ).toContain( 'text/plain' );
|
|
55
|
+
expect( result.body ).toBe( bodyText );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
if ( typeof ReadableStream !== 'undefined' ) {
|
|
59
|
+
it( 'serializes ReadableStream body for text/* via text()', async () => {
|
|
60
|
+
const encoder = new TextEncoder();
|
|
61
|
+
const chunk = encoder.encode( 'streamed text' );
|
|
62
|
+
const stream = new ReadableStream( {
|
|
63
|
+
start( controller ) {
|
|
64
|
+
controller.enqueue( chunk );
|
|
65
|
+
controller.close();
|
|
66
|
+
}
|
|
67
|
+
} );
|
|
68
|
+
const response = new Response( stream, {
|
|
69
|
+
status: 200,
|
|
70
|
+
statusText: 'OK',
|
|
71
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' }
|
|
72
|
+
} );
|
|
73
|
+
|
|
74
|
+
const result = await serializeFetchResponse( response );
|
|
75
|
+
expect( result.status ).toBe( 200 );
|
|
76
|
+
expect( result.ok ).toBe( true );
|
|
77
|
+
expect( result.statusText ).toBe( 'OK' );
|
|
78
|
+
expect( result.headers['content-type'] ).toContain( 'text/plain' );
|
|
79
|
+
expect( result.body ).toBe( 'streamed text' );
|
|
80
|
+
} );
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
it( 'serializes non-text/non-json response as base64 from arrayBuffer()', async () => {
|
|
84
|
+
const bytes = Uint8Array.from( [ 0, 1, 2, 3 ] );
|
|
85
|
+
const response = new Response( bytes, {
|
|
86
|
+
status: 200,
|
|
87
|
+
statusText: 'OK',
|
|
88
|
+
headers: { 'content-type': 'application/octet-stream' }
|
|
89
|
+
} );
|
|
90
|
+
|
|
91
|
+
const result = await serializeFetchResponse( response );
|
|
92
|
+
expect( result.status ).toBe( 200 );
|
|
93
|
+
expect( result.ok ).toBe( true );
|
|
94
|
+
expect( result.statusText ).toBe( 'OK' );
|
|
95
|
+
expect( result.headers['content-type'] ).toBe( 'application/octet-stream' );
|
|
96
|
+
expect( result.body ).toBe( Buffer.from( bytes ).toString( 'base64' ) );
|
|
97
|
+
} );
|
|
98
|
+
|
|
99
|
+
it( 'defaults to base64 when content-type header is missing', async () => {
|
|
100
|
+
const bytes = Uint8Array.from( [ 0, 1, 2, 3 ] );
|
|
101
|
+
const response = new Response( bytes, { status: 200 } );
|
|
102
|
+
// No headers set; content-type resolves to ''
|
|
103
|
+
|
|
104
|
+
const result = await serializeFetchResponse( response );
|
|
105
|
+
expect( result.headers['content-type'] ?? '' ).toBe( '' );
|
|
106
|
+
expect( result.body ).toBe( Buffer.from( bytes ).toString( 'base64' ) );
|
|
107
|
+
} );
|
|
108
|
+
} );
|
|
109
|
+
|
|
110
|
+
describe( 'serializeBodyAndInferContentType', () => {
|
|
111
|
+
it( 'returns undefineds for null payload', () => {
|
|
112
|
+
const { body, contentType } = serializeBodyAndInferContentType( null );
|
|
113
|
+
expect( body ).toBeUndefined();
|
|
114
|
+
expect( contentType ).toBeUndefined();
|
|
115
|
+
} );
|
|
116
|
+
|
|
117
|
+
it( 'returns undefineds for undefined payload', () => {
|
|
118
|
+
const { body, contentType } = serializeBodyAndInferContentType( undefined );
|
|
119
|
+
expect( body ).toBeUndefined();
|
|
120
|
+
expect( contentType ).toBeUndefined();
|
|
121
|
+
} );
|
|
122
|
+
|
|
123
|
+
it( 'handles ArrayBuffer with octet-stream', () => {
|
|
124
|
+
const buf = new ArrayBuffer( 4 );
|
|
125
|
+
const { body, contentType } = serializeBodyAndInferContentType( buf );
|
|
126
|
+
expect( body ).toBe( buf );
|
|
127
|
+
expect( contentType ).toBe( 'application/octet-stream' );
|
|
128
|
+
} );
|
|
129
|
+
|
|
130
|
+
it( 'handles TypedArray with octet-stream', () => {
|
|
131
|
+
const view = new Uint8Array( [ 1, 2, 3 ] );
|
|
132
|
+
const { body, contentType } = serializeBodyAndInferContentType( view );
|
|
133
|
+
expect( body ).toBe( view );
|
|
134
|
+
expect( contentType ).toBe( 'application/octet-stream' );
|
|
135
|
+
} );
|
|
136
|
+
|
|
137
|
+
it( 'handles DataView with octet-stream', () => {
|
|
138
|
+
const ab = new ArrayBuffer( 2 );
|
|
139
|
+
const dv = new DataView( ab );
|
|
140
|
+
const { body, contentType } = serializeBodyAndInferContentType( dv );
|
|
141
|
+
expect( body ).toBe( dv );
|
|
142
|
+
expect( contentType ).toBe( 'application/octet-stream' );
|
|
143
|
+
} );
|
|
144
|
+
|
|
145
|
+
// Environment-provided web types
|
|
146
|
+
if ( typeof URLSearchParams !== 'undefined' ) {
|
|
147
|
+
it( 'passes through URLSearchParams without content type', () => {
|
|
148
|
+
const usp = new URLSearchParams( { a: '1', b: 'two' } );
|
|
149
|
+
const { body, contentType } = serializeBodyAndInferContentType( usp );
|
|
150
|
+
expect( body ).toBe( usp );
|
|
151
|
+
expect( contentType ).toBeUndefined();
|
|
152
|
+
} );
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if ( typeof FormData !== 'undefined' ) {
|
|
156
|
+
it( 'passes through FormData without content type', () => {
|
|
157
|
+
const fd = new FormData();
|
|
158
|
+
fd.append( 'a', '1' );
|
|
159
|
+
const { body, contentType } = serializeBodyAndInferContentType( fd );
|
|
160
|
+
expect( body ).toBe( fd );
|
|
161
|
+
expect( contentType ).toBeUndefined();
|
|
162
|
+
} );
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if ( typeof Blob !== 'undefined' ) {
|
|
166
|
+
it( 'passes through Blob without content type', () => {
|
|
167
|
+
const blob = new Blob( [ 'abc' ], { type: 'text/plain' } );
|
|
168
|
+
const { body, contentType } = serializeBodyAndInferContentType( blob );
|
|
169
|
+
expect( body ).toBe( blob );
|
|
170
|
+
expect( contentType ).toBeUndefined();
|
|
171
|
+
} );
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if ( typeof File !== 'undefined' ) {
|
|
175
|
+
it( 'passes through File without content type', () => {
|
|
176
|
+
const file = new File( [ 'abc' ], 'a.txt', { type: 'text/plain' } );
|
|
177
|
+
const { body, contentType } = serializeBodyAndInferContentType( file );
|
|
178
|
+
expect( body ).toBe( file );
|
|
179
|
+
expect( contentType ).toBeUndefined();
|
|
180
|
+
} );
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
it( 'passes through async iterator without content type', () => {
|
|
184
|
+
const asyncIter = ( async function *() {
|
|
185
|
+
yield 'chunk';
|
|
186
|
+
} )();
|
|
187
|
+
const { body, contentType } = serializeBodyAndInferContentType( asyncIter );
|
|
188
|
+
expect( typeof body[Symbol.asyncIterator] ).toBe( 'function' );
|
|
189
|
+
expect( contentType ).toBeUndefined();
|
|
190
|
+
} );
|
|
191
|
+
|
|
192
|
+
it( 'passes through Node Readable without content type', () => {
|
|
193
|
+
const readable = Readable.from( [ 'a', 'b' ] );
|
|
194
|
+
const { body, contentType } = serializeBodyAndInferContentType( readable );
|
|
195
|
+
expect( body ).toBe( readable );
|
|
196
|
+
expect( contentType ).toBeUndefined();
|
|
197
|
+
} );
|
|
198
|
+
|
|
199
|
+
it( 'serializes plain object as JSON with JSON content type', () => {
|
|
200
|
+
const input = { a: 1, b: 'two' };
|
|
201
|
+
const { body, contentType } = serializeBodyAndInferContentType( input );
|
|
202
|
+
expect( body ).toBe( JSON.stringify( input ) );
|
|
203
|
+
expect( contentType ).toBe( 'application/json; charset=UTF-8' );
|
|
204
|
+
} );
|
|
205
|
+
|
|
206
|
+
it( 'serializes string primitive with text/plain content type', () => {
|
|
207
|
+
const { body, contentType } = serializeBodyAndInferContentType( 'hello' );
|
|
208
|
+
expect( body ).toBe( 'hello' );
|
|
209
|
+
expect( contentType ).toBe( 'text/plain; charset=UTF-8' );
|
|
210
|
+
} );
|
|
211
|
+
|
|
212
|
+
it( 'serializes number primitive with text/plain content type', () => {
|
|
213
|
+
const { body, contentType } = serializeBodyAndInferContentType( 42 );
|
|
214
|
+
expect( body ).toBe( '42' );
|
|
215
|
+
expect( contentType ).toBe( 'text/plain; charset=UTF-8' );
|
|
216
|
+
} );
|
|
217
|
+
|
|
218
|
+
it( 'serializes boolean primitive with text/plain content type', () => {
|
|
219
|
+
const { body, contentType } = serializeBodyAndInferContentType( true );
|
|
220
|
+
expect( body ).toBe( 'true' );
|
|
221
|
+
expect( contentType ).toBe( 'text/plain; charset=UTF-8' );
|
|
222
|
+
} );
|
|
223
|
+
} );
|
|
224
|
+
|
|
225
|
+
describe( 'deepMerge', () => {
|
|
226
|
+
it( 'Overwrites properties in object "a"', () => {
|
|
227
|
+
const a = {
|
|
228
|
+
a: 1,
|
|
229
|
+
b: {
|
|
230
|
+
c: 2
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const b = {
|
|
234
|
+
a: false,
|
|
235
|
+
b: {
|
|
236
|
+
c: true
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
expect( deepMerge( a, b ) ).toEqual( {
|
|
240
|
+
a: false,
|
|
241
|
+
b: {
|
|
242
|
+
c: true
|
|
243
|
+
}
|
|
244
|
+
} );
|
|
245
|
+
} );
|
|
246
|
+
|
|
247
|
+
it( 'Adds properties existing in "b" but absent in "a"', () => {
|
|
248
|
+
const a = {
|
|
249
|
+
a: 1
|
|
250
|
+
};
|
|
251
|
+
const b = {
|
|
252
|
+
a: false,
|
|
253
|
+
b: true
|
|
254
|
+
};
|
|
255
|
+
expect( deepMerge( a, b ) ).toEqual( {
|
|
256
|
+
a: false,
|
|
257
|
+
b: true
|
|
258
|
+
} );
|
|
259
|
+
} );
|
|
260
|
+
|
|
261
|
+
it( 'Keep extra properties in "a"', () => {
|
|
262
|
+
const a = {
|
|
263
|
+
a: 1
|
|
264
|
+
};
|
|
265
|
+
const b = {
|
|
266
|
+
b: true
|
|
267
|
+
};
|
|
268
|
+
expect( deepMerge( a, b ) ).toEqual( {
|
|
269
|
+
a: 1,
|
|
270
|
+
b: true
|
|
271
|
+
} );
|
|
272
|
+
} );
|
|
273
|
+
|
|
274
|
+
it( 'Merge object is a clone', () => {
|
|
275
|
+
const a = {
|
|
276
|
+
a: 1
|
|
277
|
+
};
|
|
278
|
+
const b = {
|
|
279
|
+
b: 1
|
|
280
|
+
};
|
|
281
|
+
const result = deepMerge( a, b );
|
|
282
|
+
a.a = 2;
|
|
283
|
+
b.b = 2;
|
|
284
|
+
expect( result.a ).toEqual( 1 );
|
|
285
|
+
} );
|
|
286
|
+
|
|
287
|
+
it( 'Returns copy of "a" if "b" is not an object', () => {
|
|
288
|
+
const a = {
|
|
289
|
+
a: 1
|
|
290
|
+
};
|
|
291
|
+
expect( deepMerge( a, null ) ).toEqual( { a: 1 } );
|
|
292
|
+
expect( deepMerge( a, undefined ) ).toEqual( { a: 1 } );
|
|
293
|
+
} );
|
|
294
|
+
|
|
295
|
+
it( 'Copy of object "a" is a clone', () => {
|
|
296
|
+
const a = {
|
|
297
|
+
a: 1
|
|
298
|
+
};
|
|
299
|
+
const result = deepMerge( a, null );
|
|
300
|
+
a.a = 2;
|
|
301
|
+
expect( result.a ).toEqual( 1 );
|
|
302
|
+
} );
|
|
303
|
+
|
|
304
|
+
it( 'Throws when first argument is not a plain object', () => {
|
|
305
|
+
expect( () => deepMerge( Function ) ).toThrow( Error );
|
|
306
|
+
expect( () => deepMerge( () => {} ) ).toThrow( Error );
|
|
307
|
+
expect( () => deepMerge( 'a' ) ).toThrow( Error );
|
|
308
|
+
expect( () => deepMerge( true ) ).toThrow( Error );
|
|
309
|
+
expect( () => deepMerge( /a/ ) ).toThrow( Error );
|
|
310
|
+
expect( () => deepMerge( [] ) ).toThrow( Error );
|
|
311
|
+
expect( () => deepMerge( class Foo {}, class Foo {} ) ).toThrow( Error );
|
|
312
|
+
expect( () => deepMerge( Number.constructor, Number.constructor ) ).toThrow( Error );
|
|
313
|
+
expect( () => deepMerge( Number.constructor.prototype, Number.constructor.prototype ) ).toThrow( Error );
|
|
314
|
+
} );
|
|
315
|
+
} );
|
|
316
|
+
|
|
317
|
+
describe( 'isPlainObject', () => {
|
|
318
|
+
it( 'Detects plain objects', () => {
|
|
319
|
+
expect( isPlainObject( {} ) ).toBe( true );
|
|
320
|
+
expect( isPlainObject( { a: 1 } ) ).toBe( true );
|
|
321
|
+
expect( isPlainObject( new Object() ) ).toBe( true );
|
|
322
|
+
expect( isPlainObject( new Object( { foo: 'bar' } ) ) ).toBe( true );
|
|
323
|
+
expect( isPlainObject( Object.create( {}.constructor.prototype ) ) ).toBe( true );
|
|
324
|
+
expect( isPlainObject( Object.create( Object.prototype ) ) ).toBe( true );
|
|
325
|
+
} );
|
|
326
|
+
|
|
327
|
+
it( 'Detects plain objects with different prototypes than Object.prototype', () => {
|
|
328
|
+
// Object with null prototype
|
|
329
|
+
expect( isPlainObject( Object.create( null ) ) ).toBe( true );
|
|
330
|
+
} );
|
|
331
|
+
|
|
332
|
+
it( 'Detects non plain objects that had their __proto__ mutated to Object.prototype or null', () => {
|
|
333
|
+
class Foo {}
|
|
334
|
+
const x = new Foo();
|
|
335
|
+
x.__proto__ = Object.prototype;
|
|
336
|
+
expect( isPlainObject( x ) ).toBe( true );
|
|
337
|
+
|
|
338
|
+
const y = new Foo();
|
|
339
|
+
y.__proto__ = null;
|
|
340
|
+
expect( isPlainObject( y ) ).toBe( true );
|
|
341
|
+
} );
|
|
342
|
+
|
|
343
|
+
it( 'Returns false for object which the prototype is not Object.prototype or null', () => {
|
|
344
|
+
// Object which the prototype is a plain {}
|
|
345
|
+
expect( isPlainObject( Object.create( {} ) ) ).toBe( false );
|
|
346
|
+
// Object which prototype is a another object with null prototype
|
|
347
|
+
expect( isPlainObject( Object.create( Object.create( null ) ) ) ).toBe( false );
|
|
348
|
+
} );
|
|
349
|
+
|
|
350
|
+
it( 'Returns false for functions', () => {
|
|
351
|
+
expect( isPlainObject( Function ) ).toBe( false );
|
|
352
|
+
expect( isPlainObject( () => {} ) ).toBe( false );
|
|
353
|
+
expect( isPlainObject( class Foo {} ) ).toBe( false );
|
|
354
|
+
expect( isPlainObject( Number.constructor ) ).toBe( false );
|
|
355
|
+
expect( isPlainObject( Number.constructor.prototype ) ).toBe( false );
|
|
356
|
+
} );
|
|
357
|
+
|
|
358
|
+
it( 'Returns false for arrays', () => {
|
|
359
|
+
expect( isPlainObject( [ 1, 2, 3 ] ) ).toBe( false );
|
|
360
|
+
expect( isPlainObject( [] ) ).toBe( false );
|
|
361
|
+
expect( isPlainObject( Array( 3 ) ) ).toBe( false );
|
|
362
|
+
} );
|
|
363
|
+
|
|
364
|
+
it( 'Returns false for primitives', () => {
|
|
365
|
+
expect( isPlainObject( null ) ).toBe( false );
|
|
366
|
+
expect( isPlainObject( undefined ) ).toBe( false );
|
|
367
|
+
expect( isPlainObject( false ) ).toBe( false );
|
|
368
|
+
expect( isPlainObject( true ) ).toBe( false );
|
|
369
|
+
expect( isPlainObject( 1 ) ).toBe( false );
|
|
370
|
+
expect( isPlainObject( 0 ) ).toBe( false );
|
|
371
|
+
expect( isPlainObject( '' ) ).toBe( false );
|
|
372
|
+
expect( isPlainObject( 'foo' ) ).toBe( false );
|
|
373
|
+
expect( isPlainObject( Symbol( 'foo' ) ) ).toBe( false );
|
|
374
|
+
expect( isPlainObject( Symbol.for( 'foo' ) ) ).toBe( false );
|
|
375
|
+
} );
|
|
376
|
+
|
|
377
|
+
it( 'Returns true for built in objects', () => {
|
|
378
|
+
expect( isPlainObject( Math ) ).toBe( true );
|
|
379
|
+
expect( isPlainObject( JSON ) ).toBe( true );
|
|
380
|
+
} );
|
|
381
|
+
|
|
382
|
+
it( 'Returns false for built in types', () => {
|
|
383
|
+
expect( isPlainObject( String ) ).toBe( false );
|
|
384
|
+
expect( isPlainObject( Number ) ).toBe( false );
|
|
385
|
+
expect( isPlainObject( Date ) ).toBe( false );
|
|
386
|
+
} );
|
|
387
|
+
|
|
388
|
+
it( 'Returns false for other instance where prototype is not object or null', () => {
|
|
389
|
+
expect( isPlainObject( /foo/ ) ).toBe( false );
|
|
390
|
+
expect( isPlainObject( new RegExp( 'foo' ) ) ).toBe( false );
|
|
391
|
+
expect( isPlainObject( new Date() ) ).toBe( false );
|
|
392
|
+
class Foo {}
|
|
393
|
+
expect( isPlainObject( new Foo() ) ).toBe( false );
|
|
394
|
+
expect( isPlainObject( Object.create( ( class Foo {} ).prototype ) ) ).toBe( false );
|
|
395
|
+
} );
|
|
396
|
+
|
|
397
|
+
it( 'Returns false if tries to change the prototype to simulate an object', () => {
|
|
398
|
+
function Bar() {}
|
|
399
|
+
Bar.prototype = Object.create( null );
|
|
400
|
+
expect( isPlainObject( new Bar() ) ).toBe( false );
|
|
401
|
+
} );
|
|
402
|
+
|
|
403
|
+
it( 'Returns false if object proto was mutated to anything else than object or null', () => {
|
|
404
|
+
const zum = {};
|
|
405
|
+
zum.__proto__ = Number.prototype;
|
|
406
|
+
expect( isPlainObject( zum ) ).toBe( false );
|
|
407
|
+
} );
|
|
408
|
+
} );
|
|
409
|
+
|
|
410
|
+
describe( 'toUrlSafeBase64', () => {
|
|
411
|
+
const urlSafeAlphabet = /^[A-Za-z0-9_-]+$/;
|
|
412
|
+
|
|
413
|
+
it( 'returns a string for a valid UUID', () => {
|
|
414
|
+
const uuid = '550e8400-e29b-41d4-a716-446655440000';
|
|
415
|
+
expect( typeof toUrlSafeBase64( uuid ) ).toBe( 'string' );
|
|
416
|
+
expect( toUrlSafeBase64( uuid ).length ).toBeGreaterThan( 0 );
|
|
417
|
+
} );
|
|
418
|
+
|
|
419
|
+
it( 'output length is 21 or 22 for a standard UUID', () => {
|
|
420
|
+
const uuid = '550e8400-e29b-41d4-a716-446655440000';
|
|
421
|
+
const out = toUrlSafeBase64( uuid );
|
|
422
|
+
expect( out.length ).toBeGreaterThanOrEqual( 21 );
|
|
423
|
+
expect( out.length ).toBeLessThanOrEqual( 22 );
|
|
424
|
+
} );
|
|
425
|
+
|
|
426
|
+
it( 'output contains only url-safe alphabet characters', () => {
|
|
427
|
+
const uuid = '550e8400-e29b-41d4-a716-446655440000';
|
|
428
|
+
const out = toUrlSafeBase64( uuid );
|
|
429
|
+
expect( out ).toMatch( urlSafeAlphabet );
|
|
430
|
+
} );
|
|
431
|
+
|
|
432
|
+
it( 'is deterministic for the same UUID', () => {
|
|
433
|
+
const uuid = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
|
434
|
+
expect( toUrlSafeBase64( uuid ) ).toBe( toUrlSafeBase64( uuid ) );
|
|
435
|
+
} );
|
|
436
|
+
|
|
437
|
+
it( 'different UUIDs produce different strings', () => {
|
|
438
|
+
const a = toUrlSafeBase64( '550e8400-e29b-41d4-a716-446655440000' );
|
|
439
|
+
const b = toUrlSafeBase64( '6ba7b810-9dad-11d1-80b4-00c04fd430c8' );
|
|
440
|
+
expect( a ).not.toBe( b );
|
|
441
|
+
} );
|
|
442
|
+
|
|
443
|
+
it( 'strips hyphens and encodes hex (same as 32-char hex)', () => {
|
|
444
|
+
const withHyphens = '550e8400-e29b-41d4-a716-446655440000';
|
|
445
|
+
const hexOnly = '550e8400e29b41d4a716446655440000';
|
|
446
|
+
expect( toUrlSafeBase64( withHyphens ) ).toBe( toUrlSafeBase64( hexOnly ) );
|
|
447
|
+
} );
|
|
448
|
+
} );
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
5
|
+
const workerDir = __dirname; // sdk/core/src/worker
|
|
6
|
+
const interfaceDir = join( __dirname, '..', 'interface' );
|
|
7
|
+
|
|
8
|
+
export const webpackConfigHook = config => {
|
|
9
|
+
// Prefer the "output-workflow-bundle" export condition when resolving packages.
|
|
10
|
+
// Packages that transitively depend on Node.js built-ins (which can't exist in the
|
|
11
|
+
// Temporal workflow bundle) can provide an alternative entry point under this condition
|
|
12
|
+
// that excludes the offending code paths. Packages without this condition fall through
|
|
13
|
+
// to the standard "import" / "module" / "default" conditions as normal.
|
|
14
|
+
config.resolve = config.resolve ?? {};
|
|
15
|
+
config.resolve.conditionNames = [
|
|
16
|
+
'output-workflow-bundle',
|
|
17
|
+
...( config.resolve.conditionNames ?? [ 'import', 'module', 'webpack', 'default' ] )
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
config.module = config.module ?? { };
|
|
21
|
+
config.module.rules = config.module.rules ?? [];
|
|
22
|
+
|
|
23
|
+
// Validation loader (runs first)
|
|
24
|
+
config.module.rules.push( {
|
|
25
|
+
test: /\.js$/,
|
|
26
|
+
// Exclude node_modules and internal core worker files
|
|
27
|
+
exclude: resource => /node_modules/.test( resource ) || resource.startsWith( workerDir ) || resource.startsWith( interfaceDir ),
|
|
28
|
+
enforce: 'pre',
|
|
29
|
+
use: {
|
|
30
|
+
loader: join( __dirname, './webpack_loaders/workflow_validator/index.mjs' )
|
|
31
|
+
}
|
|
32
|
+
} );
|
|
33
|
+
// Use AST-based loader for rewriting steps/workflows
|
|
34
|
+
config.module.rules.push( {
|
|
35
|
+
test: /\.js$/,
|
|
36
|
+
// Exclude node_modules and internal core worker files
|
|
37
|
+
exclude: resource => /node_modules/.test( resource ) || resource.startsWith( workerDir ) || resource.startsWith( interfaceDir ),
|
|
38
|
+
use: {
|
|
39
|
+
loader: join( __dirname, './webpack_loaders/workflow_rewriter/index.mjs' )
|
|
40
|
+
}
|
|
41
|
+
} );
|
|
42
|
+
return config;
|
|
43
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents the collection of metadata from workflows and activities that a worker has.
|
|
3
|
+
*/
|
|
4
|
+
export class Catalog {
|
|
5
|
+
/**
|
|
6
|
+
* All workflows in the catalog
|
|
7
|
+
* @type {Array<CatalogWorkflow>}
|
|
8
|
+
*/
|
|
9
|
+
workflows;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this.workflows = [];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Add a workflow entry to the catalog.
|
|
17
|
+
*
|
|
18
|
+
* @param {CatalogWorkflow} workflow - Workflow to add.
|
|
19
|
+
* @returns {Catalog} This catalog instance (for chaining).
|
|
20
|
+
*/
|
|
21
|
+
addWorkflow( workflow ) {
|
|
22
|
+
this.workflows.push( workflow );
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Base type for catalog entries (workflows, activities).
|
|
29
|
+
*
|
|
30
|
+
* Encapsulates common descriptive fields and JSON schemas.
|
|
31
|
+
*/
|
|
32
|
+
class CatalogEntry {
|
|
33
|
+
/**
|
|
34
|
+
* Name of the entry. Only letters, numbers and _ allowed.
|
|
35
|
+
* @type {string}
|
|
36
|
+
*/
|
|
37
|
+
name;
|
|
38
|
+
/**
|
|
39
|
+
* Optional description.
|
|
40
|
+
* @type {string|undefined}
|
|
41
|
+
*/
|
|
42
|
+
description;
|
|
43
|
+
/**
|
|
44
|
+
* JSON schema describing the expected input.
|
|
45
|
+
* @type {object}
|
|
46
|
+
*/
|
|
47
|
+
inputSchema;
|
|
48
|
+
/**
|
|
49
|
+
* JSON schema describing the produced output.
|
|
50
|
+
* @type {object}
|
|
51
|
+
*/
|
|
52
|
+
outputSchema;
|
|
53
|
+
/**
|
|
54
|
+
* Absolute path of the entity in the file system.
|
|
55
|
+
* @type {string}
|
|
56
|
+
*/
|
|
57
|
+
path;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {Object} params - Entry parameters.
|
|
61
|
+
* @param {string} params.name - Name of the entry.
|
|
62
|
+
* @param {string} [params.description] - Optional description.
|
|
63
|
+
* @param {object} [params.inputSchema] - JSON schema describing the expected input.
|
|
64
|
+
* @param {object} [params.outputSchema] - JSON schema describing the produced output.
|
|
65
|
+
* @param {string} params.path - Absolute path of the entity in the file system.
|
|
66
|
+
*/
|
|
67
|
+
constructor( { name, description, inputSchema, outputSchema, path } ) {
|
|
68
|
+
this.name = name;
|
|
69
|
+
this.description = description;
|
|
70
|
+
this.inputSchema = inputSchema;
|
|
71
|
+
this.outputSchema = outputSchema;
|
|
72
|
+
this.path = path;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Describes a single activity within a workflow.
|
|
78
|
+
*
|
|
79
|
+
* @class
|
|
80
|
+
* @extends CatalogEntry
|
|
81
|
+
*/
|
|
82
|
+
export class CatalogActivity extends CatalogEntry {}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param { CatalogWorkflowOptions}
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Describes a single workflow within the catalog.
|
|
90
|
+
*
|
|
91
|
+
* @class
|
|
92
|
+
* @extends CatalogEntry
|
|
93
|
+
*/
|
|
94
|
+
export class CatalogWorkflow extends CatalogEntry {
|
|
95
|
+
/**
|
|
96
|
+
* Each activity of this workflow.
|
|
97
|
+
* @type {Array<CatalogActivity>}
|
|
98
|
+
*/
|
|
99
|
+
activities;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {Object} params - Entry parameters.
|
|
103
|
+
* @param {string} params.name - Name of the entry.
|
|
104
|
+
* @param {string} [params.description] - Optional description.
|
|
105
|
+
* @param {object} [params.inputSchema] - JSON schema describing the expected input.
|
|
106
|
+
* @param {object} [params.outputSchema] - JSON schema describing the produced output.
|
|
107
|
+
* @param {string} params.path - Absolute path of the entity in the file system.
|
|
108
|
+
* @param {Array<CatalogActivity>} params.activities - Each activity of this workflow
|
|
109
|
+
*/
|
|
110
|
+
constructor( { activities, ...args } ) {
|
|
111
|
+
super( args );
|
|
112
|
+
this.activities = activities;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
4
|
+
import { Catalog, CatalogActivity, CatalogWorkflow } from './catalog.js';
|
|
5
|
+
import { createChildLogger } from '#logger';
|
|
6
|
+
|
|
7
|
+
const log = createChildLogger( 'Catalog' );
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts a Zod schema to JSON Schema format.
|
|
11
|
+
*
|
|
12
|
+
* @param {any} schema - A zod schema
|
|
13
|
+
* @returns {object|null} JSON Schema object, or null if schema is invalid
|
|
14
|
+
*/
|
|
15
|
+
const convertToJsonSchema = schema => {
|
|
16
|
+
if ( !schema ) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
return z.toJSONSchema( schema );
|
|
22
|
+
} catch ( error ) {
|
|
23
|
+
log.warn( 'Invalid schema provided (expected Zod schema)', { error: error.message } );
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Converts the list of workflows and the activities into the catalog information.
|
|
30
|
+
*
|
|
31
|
+
* This has information of all workflows and their activities from this worker.
|
|
32
|
+
*
|
|
33
|
+
* @param {object[]} workflows - The workflows objects, as they are returned from the loader module
|
|
34
|
+
* @param {object} activities - The activities functions map with metadata, as they are returned from the loader module
|
|
35
|
+
* @returns {Catalog} An catalog instance
|
|
36
|
+
*/
|
|
37
|
+
export const createCatalog = ( { workflows, activities } ) =>
|
|
38
|
+
workflows.reduce( ( catalog, workflow ) =>
|
|
39
|
+
catalog.addWorkflow( new CatalogWorkflow( {
|
|
40
|
+
...workflow,
|
|
41
|
+
inputSchema: convertToJsonSchema( workflow.inputSchema ),
|
|
42
|
+
outputSchema: convertToJsonSchema( workflow.outputSchema ),
|
|
43
|
+
activities: Object.entries( activities )
|
|
44
|
+
.filter( ( [ k ] ) => k.startsWith( `${dirname( workflow.path )}#` ) )
|
|
45
|
+
.map( ( [ _, v ] ) => {
|
|
46
|
+
const metadata = v[METADATA_ACCESS_SYMBOL];
|
|
47
|
+
return new CatalogActivity( {
|
|
48
|
+
...metadata,
|
|
49
|
+
inputSchema: convertToJsonSchema( metadata.inputSchema ),
|
|
50
|
+
outputSchema: convertToJsonSchema( metadata.outputSchema )
|
|
51
|
+
} );
|
|
52
|
+
} )
|
|
53
|
+
} ) )
|
|
54
|
+
, new Catalog() );
|