@output.ai/core 0.2.1 → 0.2.3
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/package.json +1 -1
- package/src/index.d.ts +36 -4
- package/src/interface/evaluator.js +3 -0
- package/src/interface/workflow.js +44 -12
- package/src/utils/index.d.ts +34 -4
- package/src/utils/utils.js +35 -0
- package/src/utils/utils.spec.js +153 -2
- package/src/worker/webpack_loaders/consts.js +0 -13
- package/src/worker/webpack_loaders/tools.js +1 -6
- package/src/worker/webpack_loaders/tools.spec.js +0 -44
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +3 -19
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +0 -44
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -2,6 +2,18 @@ import type { z } from 'zod';
|
|
|
2
2
|
import type { ActivityOptions } from '@temporalio/workflow';
|
|
3
3
|
import type { SerializedFetchResponse } from './utils/index.d.ts';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Similar to `Partial<T>` but applies to nested properties recursively, creating a deep optional variant of `T`:
|
|
7
|
+
* - Objects: All properties become optional, recursively.
|
|
8
|
+
* - Functions: Preserved as‑is (only the property itself becomes optional).
|
|
9
|
+
* - Primitives: Returned unchanged.
|
|
10
|
+
* Useful for config overrides with strong IntelliSense on nested fields and methods.
|
|
11
|
+
*/
|
|
12
|
+
type DeepPartial<T> =
|
|
13
|
+
T extends ( ...args: any[] ) => unknown ? T : // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
14
|
+
T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } :
|
|
15
|
+
T;
|
|
16
|
+
|
|
5
17
|
/**
|
|
6
18
|
* Expose z from Zod as a convenience.
|
|
7
19
|
*/
|
|
@@ -211,7 +223,7 @@ export declare function step<
|
|
|
211
223
|
*
|
|
212
224
|
* Allows overriding Temporal Activity options for this workflow.
|
|
213
225
|
*/
|
|
214
|
-
export type WorkflowInvocationConfiguration = {
|
|
226
|
+
export type WorkflowInvocationConfiguration<Context extends WorkflowContext = WorkflowContext> = {
|
|
215
227
|
|
|
216
228
|
/**
|
|
217
229
|
* Native Temporal Activity options
|
|
@@ -223,6 +235,11 @@ export type WorkflowInvocationConfiguration = {
|
|
|
223
235
|
* Detached workflows called without explicitly awaiting the result are "fire-and-forget" and may outlive the parent.
|
|
224
236
|
*/
|
|
225
237
|
detached?: boolean
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Allow to overwrite properties of the "context" of workflows when called in tests environments.
|
|
241
|
+
*/
|
|
242
|
+
context?: DeepPartial<Context>
|
|
226
243
|
};
|
|
227
244
|
|
|
228
245
|
/**
|
|
@@ -265,7 +282,8 @@ export type WorkflowContext<
|
|
|
265
282
|
( input: z.infer<InputSchema> ) => ( OutputSchema extends AnyZodSchema ? z.infer<OutputSchema> : void ) :
|
|
266
283
|
() => ( OutputSchema extends AnyZodSchema ? z.infer<OutputSchema> : void ),
|
|
267
284
|
|
|
268
|
-
/**
|
|
285
|
+
/**
|
|
286
|
+
* Indicates whether the Temporal runtime suggests continuing this workflow as new.
|
|
269
287
|
*
|
|
270
288
|
* Use this to decide whether to `continueAsNew` before long waits or at loop boundaries.
|
|
271
289
|
* Prefer returning the `continueAsNew(...)` call immediately when this becomes `true`.
|
|
@@ -275,6 +293,18 @@ export type WorkflowContext<
|
|
|
275
293
|
* @returns True if a continue-as-new is suggested for the current run; otherwise false.
|
|
276
294
|
*/
|
|
277
295
|
isContinueAsNewSuggested: () => boolean
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Information about the workflow execution
|
|
300
|
+
*/
|
|
301
|
+
info: {
|
|
302
|
+
/**
|
|
303
|
+
* Internal Temporal workflow id.
|
|
304
|
+
*
|
|
305
|
+
* @see {@link https://docs.temporal.io/workflow-execution/workflowid-runid#workflow-id}
|
|
306
|
+
*/
|
|
307
|
+
workflowId: string
|
|
278
308
|
}
|
|
279
309
|
};
|
|
280
310
|
|
|
@@ -310,8 +340,10 @@ export type WorkflowFunction<
|
|
|
310
340
|
*/
|
|
311
341
|
export type WorkflowFunctionWrapper<WorkflowFunction> =
|
|
312
342
|
[Parameters<WorkflowFunction>[0]] extends [undefined | null] ?
|
|
313
|
-
( input?: undefined | null, config?: WorkflowInvocationConfiguration ) =>
|
|
314
|
-
|
|
343
|
+
( input?: undefined | null, config?: WorkflowInvocationConfiguration<Parameters<WorkflowFunction>[1]> ) =>
|
|
344
|
+
ReturnType<WorkflowFunction> :
|
|
345
|
+
( input: Parameters<WorkflowFunction>[0], config?: WorkflowInvocationConfiguration<Parameters<WorkflowFunction>[1]> ) =>
|
|
346
|
+
ReturnType<WorkflowFunction>;
|
|
315
347
|
|
|
316
348
|
/**
|
|
317
349
|
* Creates a workflow.
|
|
@@ -81,6 +81,7 @@ export class EvaluationStringResult extends EvaluationResult {
|
|
|
81
81
|
* @param {number} args.confidence - The confidence on the evaluation
|
|
82
82
|
* @param {string} [args.reasoning] - The reasoning behind the result
|
|
83
83
|
*/
|
|
84
|
+
// eslint-disable-next-line no-useless-constructor
|
|
84
85
|
constructor( args ) {
|
|
85
86
|
super( args );
|
|
86
87
|
}
|
|
@@ -103,6 +104,7 @@ export class EvaluationBooleanResult extends EvaluationResult {
|
|
|
103
104
|
* @param {number} args.confidence - The confidence on the evaluation
|
|
104
105
|
* @param {string} [args.reasoning] - The reasoning behind the result
|
|
105
106
|
*/
|
|
107
|
+
// eslint-disable-next-line no-useless-constructor
|
|
106
108
|
constructor( args ) {
|
|
107
109
|
super( args );
|
|
108
110
|
}
|
|
@@ -125,6 +127,7 @@ export class EvaluationNumberResult extends EvaluationResult {
|
|
|
125
127
|
* @param {number} args.confidence - The confidence on the evaluation
|
|
126
128
|
* @param {string} [args.reasoning] - The reasoning behind the result
|
|
127
129
|
*/
|
|
130
|
+
// eslint-disable-next-line no-useless-constructor
|
|
128
131
|
constructor( args ) {
|
|
129
132
|
super( args );
|
|
130
133
|
}
|
|
@@ -3,9 +3,41 @@ import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, uuid4,
|
|
|
3
3
|
import { validateWorkflow } from './validations/static.js';
|
|
4
4
|
import { validateWithSchema } from './validations/runtime.js';
|
|
5
5
|
import { SHARED_STEP_PREFIX, ACTIVITY_GET_TRACE_DESTINATIONS } from '#consts';
|
|
6
|
-
import { mergeActivityOptions, resolveInvocationDir, setMetadata } from '#utils';
|
|
6
|
+
import { deepMerge, mergeActivityOptions, resolveInvocationDir, setMetadata } from '#utils';
|
|
7
7
|
import { FatalError, ValidationError } from '#errors';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Context instance builder
|
|
11
|
+
*/
|
|
12
|
+
class Context {
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds a new context instance
|
|
16
|
+
* @param {object} options - Arguments to build a new context instance
|
|
17
|
+
* @param {string} workflowId
|
|
18
|
+
* @param {function} continueAsNew
|
|
19
|
+
* @param {function} isContinueAsNewSuggested
|
|
20
|
+
* @returns {object} context
|
|
21
|
+
*/
|
|
22
|
+
static build( { workflowId, continueAsNew, isContinueAsNewSuggested } ) {
|
|
23
|
+
return {
|
|
24
|
+
/**
|
|
25
|
+
* Control namespace: This object adds functions to interact with Temporal flow mechanisms
|
|
26
|
+
*/
|
|
27
|
+
control: {
|
|
28
|
+
continueAsNew,
|
|
29
|
+
isContinueAsNewSuggested
|
|
30
|
+
},
|
|
31
|
+
/**
|
|
32
|
+
* Info namespace: abstracts workflowInfo()
|
|
33
|
+
*/
|
|
34
|
+
info: {
|
|
35
|
+
workflowId
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
9
41
|
const defaultActivityOptions = {
|
|
10
42
|
startToCloseTimeout: '20m',
|
|
11
43
|
retry: {
|
|
@@ -24,26 +56,26 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
24
56
|
const activityOptions = mergeActivityOptions( defaultActivityOptions, options );
|
|
25
57
|
const steps = proxyActivities( activityOptions );
|
|
26
58
|
|
|
27
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Wraps the `fn` function of the workflow
|
|
61
|
+
*
|
|
62
|
+
* @param {unknown} input - The input, must match the inputSchema
|
|
63
|
+
* @param {object} extra - Workflow configurations (received directly only in unit tests)
|
|
64
|
+
* @returns {unknown} The result, will match the outputSchema
|
|
65
|
+
*/
|
|
66
|
+
const wrapper = async ( input, extra = {} ) => {
|
|
28
67
|
// this returns a plain function, for example, in unit tests
|
|
29
68
|
if ( !inWorkflowContext() ) {
|
|
30
69
|
validateWithSchema( inputSchema, input, `Workflow ${name} input` );
|
|
31
|
-
const
|
|
70
|
+
const context = Context.build( { workflowId: 'test-workflow', continueAsNew: async () => {}, isContinueAsNewSuggested: () => false } );
|
|
71
|
+
const output = await fn( input, deepMerge( context, extra.context ?? {} ) );
|
|
32
72
|
validateWithSchema( outputSchema, output, `Workflow ${name} output` );
|
|
33
73
|
return output;
|
|
34
74
|
}
|
|
35
75
|
|
|
36
76
|
const { workflowId, memo, startTime } = workflowInfo();
|
|
37
77
|
|
|
38
|
-
|
|
39
|
-
- Control namespace: This object adds functions to interact with Temporal flow mechanisms
|
|
40
|
-
*/
|
|
41
|
-
const context = {
|
|
42
|
-
control: {
|
|
43
|
-
isContinueAsNewSuggested: () => workflowInfo().continueAsNewSuggested,
|
|
44
|
-
continueAsNew
|
|
45
|
-
}
|
|
46
|
-
};
|
|
78
|
+
const context = Context.build( { workflowId, continueAsNew, isContinueAsNewSuggested: () => workflowInfo().continueAsNewSuggested } );
|
|
47
79
|
|
|
48
80
|
// Root workflows will not have the execution context yet, since it is set here.
|
|
49
81
|
const isRoot = !memo.executionContext;
|
package/src/utils/index.d.ts
CHANGED
|
@@ -63,18 +63,48 @@ export type SerializedFetchResponse = {
|
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* Consumes
|
|
66
|
+
* Consumes an HTTP `Response` and serializes it to a plain object.
|
|
67
|
+
*
|
|
68
|
+
* @param response - The response to serialize.
|
|
69
|
+
* @returns SerializedFetchResponse
|
|
67
70
|
*/
|
|
68
71
|
export function serializeFetchResponse( response: Response ): SerializedFetchResponse;
|
|
69
72
|
|
|
70
73
|
export type SerializedBodyAndContentType = {
|
|
71
|
-
/** The body
|
|
74
|
+
/** The body as a string when possible; otherwise the original value */
|
|
72
75
|
body: string | unknown,
|
|
73
|
-
/** The inferred
|
|
76
|
+
/** The inferred `Content-Type` header value, if any */
|
|
74
77
|
contentType: string | undefined
|
|
75
78
|
};
|
|
76
79
|
|
|
77
80
|
/**
|
|
78
|
-
*
|
|
81
|
+
* Serializes a payload for use as a fetch POST body and infers its `Content-Type`.
|
|
82
|
+
*
|
|
83
|
+
* @param body - The payload to serialize.
|
|
84
|
+
* @returns The serialized body and inferred `Content-Type`.
|
|
79
85
|
*/
|
|
80
86
|
export function serializeBodyAndInferContentType( body: unknown ): SerializedBodyAndContentType;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns true if the value is a plain object:
|
|
90
|
+
* - `{}`
|
|
91
|
+
* - `new Object()`
|
|
92
|
+
* - `Object.create(null)`
|
|
93
|
+
*
|
|
94
|
+
* @param object - The value to check.
|
|
95
|
+
* @returns Whether the value is a plain object.
|
|
96
|
+
*/
|
|
97
|
+
export function isPlainObject( object: unknown ): boolean;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Creates a new object by merging object `b` onto object `a`, biased toward `b`:
|
|
101
|
+
* - Fields in `b` overwrite fields in `a`.
|
|
102
|
+
* - Fields in `b` that don't exist in `a` are created.
|
|
103
|
+
* - Fields in `a` that don't exist in `b` are left unchanged.
|
|
104
|
+
*
|
|
105
|
+
* @param a - The base object.
|
|
106
|
+
* @param b - The overriding object.
|
|
107
|
+
* @throws {Error} If either `a` or `b` is not a plain object.
|
|
108
|
+
* @returns A new merged object.
|
|
109
|
+
*/
|
|
110
|
+
export function deepMerge( a: object, b: object ): object;
|
package/src/utils/utils.js
CHANGED
|
@@ -7,6 +7,18 @@ import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
|
7
7
|
*/
|
|
8
8
|
export const clone = v => JSON.parse( JSON.stringify( v ) );
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Detect a JS plain object.
|
|
12
|
+
*
|
|
13
|
+
* @param {unknown} v
|
|
14
|
+
* @returns {boolean}
|
|
15
|
+
*/
|
|
16
|
+
export const isPlainObject = v =>
|
|
17
|
+
typeof v === 'object' &&
|
|
18
|
+
!Array.isArray( v ) &&
|
|
19
|
+
v !== null &&
|
|
20
|
+
[ Object.prototype, null ].includes( Object.getPrototypeOf( v ) );
|
|
21
|
+
|
|
10
22
|
/**
|
|
11
23
|
* Throw given error
|
|
12
24
|
* @param {Error} e
|
|
@@ -149,3 +161,26 @@ export const serializeBodyAndInferContentType = payload => {
|
|
|
149
161
|
|
|
150
162
|
return { body: String( payload ), contentType: 'text/plain; charset=UTF-8' };
|
|
151
163
|
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Creates a new object merging object "b" onto object "a" biased to "b":
|
|
167
|
+
* - Object "b" will overwrite fields on object "a";
|
|
168
|
+
* - Object "b" fields that don't exist on object "a" will be created;
|
|
169
|
+
* - Object "a" fields that don't exist on object "b" will not be touched;
|
|
170
|
+
*
|
|
171
|
+
*
|
|
172
|
+
* @param {object} a - The base object
|
|
173
|
+
* @param {object} b - The target object
|
|
174
|
+
* @returns {object} A new object
|
|
175
|
+
*/
|
|
176
|
+
export const deepMerge = ( a, b ) => {
|
|
177
|
+
if ( !isPlainObject( a ) ) {
|
|
178
|
+
throw new Error( 'Parameter "a" is not an object.' );
|
|
179
|
+
}
|
|
180
|
+
if ( !isPlainObject( b ) ) {
|
|
181
|
+
throw new Error( 'Parameter "b" is not an object.' );
|
|
182
|
+
}
|
|
183
|
+
return Object.entries( b ).reduce( ( obj, [ k, v ] ) =>
|
|
184
|
+
Object.assign( obj, { [k]: isPlainObject( v ) && isPlainObject( a[k] ) ? deepMerge( a[k], v ) : v } )
|
|
185
|
+
, clone( a ) );
|
|
186
|
+
};
|
package/src/utils/utils.spec.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { Readable } from 'node:stream';
|
|
3
|
-
import { clone, mergeActivityOptions, serializeBodyAndInferContentType, serializeFetchResponse } from './utils.js';
|
|
4
|
-
// Response is available globally in Node 18+ (undici)
|
|
3
|
+
import { clone, mergeActivityOptions, serializeBodyAndInferContentType, serializeFetchResponse, deepMerge, isPlainObject } from './utils.js';
|
|
5
4
|
|
|
6
5
|
describe( 'clone', () => {
|
|
7
6
|
it( 'produces a deep copy without shared references', () => {
|
|
@@ -260,3 +259,155 @@ describe( 'mergeActivityOptions', () => {
|
|
|
260
259
|
} );
|
|
261
260
|
} );
|
|
262
261
|
|
|
262
|
+
describe( 'deepMerge', () => {
|
|
263
|
+
it( 'Overwrites properties in object "a"', () => {
|
|
264
|
+
const a = {
|
|
265
|
+
a: 1,
|
|
266
|
+
b: {
|
|
267
|
+
c: 2
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
const b = {
|
|
271
|
+
a: false,
|
|
272
|
+
b: {
|
|
273
|
+
c: true
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
expect( deepMerge( a, b ) ).toEqual( {
|
|
277
|
+
a: false,
|
|
278
|
+
b: {
|
|
279
|
+
c: true
|
|
280
|
+
}
|
|
281
|
+
} );
|
|
282
|
+
} );
|
|
283
|
+
|
|
284
|
+
it( 'Adds properties existing in "b" but absent in "a"', () => {
|
|
285
|
+
const a = {
|
|
286
|
+
a: 1
|
|
287
|
+
};
|
|
288
|
+
const b = {
|
|
289
|
+
a: false,
|
|
290
|
+
b: true
|
|
291
|
+
};
|
|
292
|
+
expect( deepMerge( a, b ) ).toEqual( {
|
|
293
|
+
a: false,
|
|
294
|
+
b: true
|
|
295
|
+
} );
|
|
296
|
+
} );
|
|
297
|
+
|
|
298
|
+
it( 'Keep extra properties in "a"', () => {
|
|
299
|
+
const a = {
|
|
300
|
+
a: 1
|
|
301
|
+
};
|
|
302
|
+
const b = {
|
|
303
|
+
b: true
|
|
304
|
+
};
|
|
305
|
+
expect( deepMerge( a, b ) ).toEqual( {
|
|
306
|
+
a: 1,
|
|
307
|
+
b: true
|
|
308
|
+
} );
|
|
309
|
+
} );
|
|
310
|
+
|
|
311
|
+
it( 'Throw error on non iterable object types', () => {
|
|
312
|
+
expect( () => deepMerge( Function, Function ) ).toThrow( Error );
|
|
313
|
+
expect( () => deepMerge( () => {}, () => {} ) ).toThrow( Error );
|
|
314
|
+
expect( () => deepMerge( 'a', 'a' ) ).toThrow( Error );
|
|
315
|
+
expect( () => deepMerge( true, true ) ).toThrow( Error );
|
|
316
|
+
expect( () => deepMerge( /a/, /a/ ) ).toThrow( Error );
|
|
317
|
+
expect( () => deepMerge( [], [] ) ).toThrow( Error );
|
|
318
|
+
expect( () => deepMerge( class Foo {}, class Foo {} ) ).toThrow( Error );
|
|
319
|
+
expect( () => deepMerge( Number.constructor, Number.constructor ) ).toThrow( Error );
|
|
320
|
+
expect( () => deepMerge( Number.constructor.prototype, Number.constructor.prototype ) ).toThrow( Error );
|
|
321
|
+
} );
|
|
322
|
+
} );
|
|
323
|
+
|
|
324
|
+
describe( 'isPlainObject', () => {
|
|
325
|
+
it( 'Detects plain objects', () => {
|
|
326
|
+
expect( isPlainObject( {} ) ).toBe( true );
|
|
327
|
+
expect( isPlainObject( { a: 1 } ) ).toBe( true );
|
|
328
|
+
expect( isPlainObject( new Object() ) ).toBe( true );
|
|
329
|
+
expect( isPlainObject( new Object( { foo: 'bar' } ) ) ).toBe( true );
|
|
330
|
+
expect( isPlainObject( Object.create( {}.constructor.prototype ) ) ).toBe( true );
|
|
331
|
+
expect( isPlainObject( Object.create( Object.prototype ) ) ).toBe( true );
|
|
332
|
+
} );
|
|
333
|
+
|
|
334
|
+
it( 'Detects plain objects with different prototypes than Object.prototype', () => {
|
|
335
|
+
// Object with null prototype
|
|
336
|
+
expect( isPlainObject( Object.create( null ) ) ).toBe( true );
|
|
337
|
+
} );
|
|
338
|
+
|
|
339
|
+
it( 'Detects non plain objects that had their __proto__ mutated to Object.prototype or null', () => {
|
|
340
|
+
class Foo {}
|
|
341
|
+
const x = new Foo();
|
|
342
|
+
x.__proto__ = Object.prototype;
|
|
343
|
+
expect( isPlainObject( x ) ).toBe( true );
|
|
344
|
+
|
|
345
|
+
const y = new Foo();
|
|
346
|
+
y.__proto__ = null;
|
|
347
|
+
expect( isPlainObject( y ) ).toBe( true );
|
|
348
|
+
} );
|
|
349
|
+
|
|
350
|
+
it( 'Returns false for object which the prototype is not Object.prototype or null', () => {
|
|
351
|
+
// Object which the prototype is a plain {}
|
|
352
|
+
expect( isPlainObject( Object.create( {} ) ) ).toBe( false );
|
|
353
|
+
// Object which prototype is a another object with null prototype
|
|
354
|
+
expect( isPlainObject( Object.create( Object.create( null ) ) ) ).toBe( false );
|
|
355
|
+
} );
|
|
356
|
+
|
|
357
|
+
it( 'Returns false for functions', () => {
|
|
358
|
+
expect( isPlainObject( Function ) ).toBe( false );
|
|
359
|
+
expect( isPlainObject( () => {} ) ).toBe( false );
|
|
360
|
+
expect( isPlainObject( class Foo {} ) ).toBe( false );
|
|
361
|
+
expect( isPlainObject( Number.constructor ) ).toBe( false );
|
|
362
|
+
expect( isPlainObject( Number.constructor.prototype ) ).toBe( false );
|
|
363
|
+
} );
|
|
364
|
+
|
|
365
|
+
it( 'Returns false for arrays', () => {
|
|
366
|
+
expect( isPlainObject( [ 1, 2, 3 ] ) ).toBe( false );
|
|
367
|
+
expect( isPlainObject( [] ) ).toBe( false );
|
|
368
|
+
expect( isPlainObject( Array( 3 ) ) ).toBe( false );
|
|
369
|
+
} );
|
|
370
|
+
|
|
371
|
+
it( 'Returns false for primitives', () => {
|
|
372
|
+
expect( isPlainObject( false ) ).toBe( false );
|
|
373
|
+
expect( isPlainObject( true ) ).toBe( false );
|
|
374
|
+
expect( isPlainObject( 1 ) ).toBe( false );
|
|
375
|
+
expect( isPlainObject( 0 ) ).toBe( false );
|
|
376
|
+
expect( isPlainObject( '' ) ).toBe( false );
|
|
377
|
+
expect( isPlainObject( 'foo' ) ).toBe( false );
|
|
378
|
+
expect( isPlainObject( Symbol( 'foo' ) ) ).toBe( false );
|
|
379
|
+
expect( isPlainObject( Symbol.for( 'foo' ) ) ).toBe( false );
|
|
380
|
+
} );
|
|
381
|
+
|
|
382
|
+
it( 'Returns true for built in objects', () => {
|
|
383
|
+
expect( isPlainObject( Math ) ).toBe( true );
|
|
384
|
+
expect( isPlainObject( JSON ) ).toBe( true );
|
|
385
|
+
} );
|
|
386
|
+
|
|
387
|
+
it( 'Returns false for built in types', () => {
|
|
388
|
+
expect( isPlainObject( String ) ).toBe( false );
|
|
389
|
+
expect( isPlainObject( Number ) ).toBe( false );
|
|
390
|
+
expect( isPlainObject( Date ) ).toBe( false );
|
|
391
|
+
} );
|
|
392
|
+
|
|
393
|
+
it( 'Returns false for other instance where prototype is not object or null', () => {
|
|
394
|
+
expect( isPlainObject( /foo/ ) ).toBe( false );
|
|
395
|
+
expect( isPlainObject( new RegExp( 'foo' ) ) ).toBe( false );
|
|
396
|
+
expect( isPlainObject( new Date() ) ).toBe( false );
|
|
397
|
+
class Foo {}
|
|
398
|
+
expect( isPlainObject( new Foo() ) ).toBe( false );
|
|
399
|
+
expect( isPlainObject( Object.create( ( class Foo {} ).prototype ) ) ).toBe( false );
|
|
400
|
+
} );
|
|
401
|
+
|
|
402
|
+
it( 'Returns false if tries to change the prototype to simulate an object', () => {
|
|
403
|
+
function Bar() {}
|
|
404
|
+
Bar.prototype = Object.create( null );
|
|
405
|
+
expect( isPlainObject( new Bar() ) ).toBe( false );
|
|
406
|
+
} );
|
|
407
|
+
|
|
408
|
+
it( 'Returns false if object proto was mutated to anything else than object or null', () => {
|
|
409
|
+
const zum = {};
|
|
410
|
+
zum.__proto__ = Number.prototype;
|
|
411
|
+
expect( isPlainObject( zum ) ).toBe( false );
|
|
412
|
+
} );
|
|
413
|
+
} );
|
|
@@ -9,19 +9,6 @@ export const ComponentFile = {
|
|
|
9
9
|
WORKFLOW: 'workflow'
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export const EXTRANEOUS_FILE = 'extraneous';
|
|
13
|
-
export const ExtraneousFileList = [
|
|
14
|
-
'types',
|
|
15
|
-
'consts',
|
|
16
|
-
'constants',
|
|
17
|
-
'vars',
|
|
18
|
-
'variables',
|
|
19
|
-
'utils',
|
|
20
|
-
'tools',
|
|
21
|
-
'functions',
|
|
22
|
-
'shared'
|
|
23
|
-
];
|
|
24
|
-
|
|
25
12
|
export const CoreModule = {
|
|
26
13
|
LOCAL: 'local_core',
|
|
27
14
|
NPM: '@output.ai/core'
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
isExportDefaultDeclaration,
|
|
25
25
|
isFunctionDeclaration
|
|
26
26
|
} from '@babel/types';
|
|
27
|
-
import { ComponentFile,
|
|
27
|
+
import { ComponentFile, NodeType } from './consts.js';
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Resolve a relative module specifier against a base directory.
|
|
@@ -138,8 +138,6 @@ export const isWorkflowPath = value => /(^|\/)workflow\.js$/.test( value );
|
|
|
138
138
|
*/
|
|
139
139
|
export const isTypesPath = value => /(^|\/)types\.js$/.test( value );
|
|
140
140
|
|
|
141
|
-
export const isExtraneousFile = value => ExtraneousFileList.map( t => new RegExp( `(^|\/)${t}\\.js$` ) ).find( rx => rx.test( value ) );
|
|
142
|
-
|
|
143
141
|
/**
|
|
144
142
|
* Determine file kind based on its path.
|
|
145
143
|
* @param {string} filename
|
|
@@ -158,9 +156,6 @@ export const getFileKind = path => {
|
|
|
158
156
|
if ( isWorkflowPath( path ) ) {
|
|
159
157
|
return ComponentFile.WORKFLOW;
|
|
160
158
|
}
|
|
161
|
-
if ( isExtraneousFile( path ) ) {
|
|
162
|
-
return EXTRANEOUS_FILE;
|
|
163
|
-
}
|
|
164
159
|
return null;
|
|
165
160
|
};
|
|
166
161
|
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
buildSharedStepsNameMap,
|
|
21
21
|
buildWorkflowNameMap,
|
|
22
22
|
buildEvaluatorsNameMap,
|
|
23
|
-
isExtraneousFile,
|
|
24
23
|
getFileKind
|
|
25
24
|
} from './tools.js';
|
|
26
25
|
|
|
@@ -170,49 +169,6 @@ describe( 'workflow_rewriter tools', () => {
|
|
|
170
169
|
expect( isEvaluatorsPath( 'steps.js' ) ).toBe( false );
|
|
171
170
|
} );
|
|
172
171
|
|
|
173
|
-
it( 'isExtraneousFile: returns truthy for extraneous files (types/consts/constants/vars/variables)', () => {
|
|
174
|
-
const ok = [
|
|
175
|
-
'types.js',
|
|
176
|
-
'./types.js',
|
|
177
|
-
'/a/b/types.js',
|
|
178
|
-
'consts.js',
|
|
179
|
-
'./consts.js',
|
|
180
|
-
'/a/b/consts.js',
|
|
181
|
-
'constants.js',
|
|
182
|
-
'./constants.js',
|
|
183
|
-
'/a/b/constants.js',
|
|
184
|
-
'vars.js',
|
|
185
|
-
'./vars.js',
|
|
186
|
-
'/a/b/vars.js',
|
|
187
|
-
'variables.js',
|
|
188
|
-
'./variables.js',
|
|
189
|
-
'/a/b/variables.js'
|
|
190
|
-
];
|
|
191
|
-
for ( const p of ok ) {
|
|
192
|
-
expect( Boolean( isExtraneousFile( p ) ) ).toBe( true );
|
|
193
|
-
}
|
|
194
|
-
} );
|
|
195
|
-
|
|
196
|
-
it( 'isExtraneousFile: returns falsy for non-extraneous or non-.js files', () => {
|
|
197
|
-
const bad = [
|
|
198
|
-
'types.ts',
|
|
199
|
-
'/a/b/types.ts',
|
|
200
|
-
'types.mjs',
|
|
201
|
-
'variables.jsx',
|
|
202
|
-
'variables.mjs',
|
|
203
|
-
'myconstants.js',
|
|
204
|
-
'steps.js',
|
|
205
|
-
'evaluators.js',
|
|
206
|
-
'shared_steps.js',
|
|
207
|
-
'workflow.js',
|
|
208
|
-
'typess.js',
|
|
209
|
-
'/a/b/c/variables.json'
|
|
210
|
-
];
|
|
211
|
-
for ( const p of bad ) {
|
|
212
|
-
expect( isExtraneousFile( p ) ).toBeFalsy();
|
|
213
|
-
}
|
|
214
|
-
} );
|
|
215
|
-
|
|
216
172
|
it( 'createThisMethodCall: builds this.method(\'name\', ...args) call', () => {
|
|
217
173
|
const call = createThisMethodCall( 'invoke', 'n', [ t.numericLiteral( 1 ), t.identifier( 'x' ) ] );
|
|
218
174
|
expect( t.isCallExpression( call ) ).toBe( true );
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import traverseModule from '@babel/traverse';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { parse, toAbsolutePath, getFileKind } from '../tools.js';
|
|
4
|
-
import { ComponentFile
|
|
4
|
+
import { ComponentFile } from '../consts.js';
|
|
5
5
|
import {
|
|
6
6
|
isCallExpression,
|
|
7
7
|
isFunctionExpression,
|
|
@@ -17,19 +17,6 @@ import {
|
|
|
17
17
|
// Handle CJS/ESM interop for Babel packages when executed as a webpack loader
|
|
18
18
|
const traverse = traverseModule.default ?? traverseModule;
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Check if workflow dependencies
|
|
22
|
-
*/
|
|
23
|
-
const validateWorkflowImports = ( { specifier, filename } ) => {
|
|
24
|
-
const isCore = Object.values( CoreModule ).includes( specifier );
|
|
25
|
-
const isComponent = Object.values( ComponentFile ).includes( getFileKind( specifier ) );
|
|
26
|
-
const isAllowedExtraneous = getFileKind( specifier ) === EXTRANEOUS_FILE;
|
|
27
|
-
if ( !isCore && !isComponent && !isAllowedExtraneous ) {
|
|
28
|
-
throw new Error( `Invalid dependency in workflow.js: '${specifier}'. \
|
|
29
|
-
Only components (${Object.values( ComponentFile ) } ), @output.ai/core, or whitelisted (${ExtraneousFileList}) imports are allowed in ${filename}` );
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
|
|
33
20
|
/**
|
|
34
21
|
* Check if evaluators, steps or shared_steps import invalid dependencies
|
|
35
22
|
*/
|
|
@@ -41,12 +28,10 @@ Steps, shared_steps, evaluators or workflows are not allowed dependencies in ${f
|
|
|
41
28
|
};
|
|
42
29
|
|
|
43
30
|
/**
|
|
44
|
-
* Validate import for evaluators, steps, shared_steps
|
|
31
|
+
* Validate import for evaluators, steps, shared_steps
|
|
45
32
|
*/
|
|
46
33
|
const executeImportValidations = ( { fileKind, specifier, filename } ) => {
|
|
47
|
-
if ( fileKind
|
|
48
|
-
validateWorkflowImports( { fileKind, specifier, filename } );
|
|
49
|
-
} else if ( Object.values( ComponentFile ).includes( fileKind ) ) {
|
|
34
|
+
if ( Object.values( ComponentFile ).includes( fileKind ) && fileKind !== ComponentFile.WORKFLOW ) {
|
|
50
35
|
validateStepEvaluatorImports( { fileKind, specifier, filename } );
|
|
51
36
|
}
|
|
52
37
|
};
|
|
@@ -62,7 +47,6 @@ const executeImportValidations = ( { fileKind, specifier, filename } ) => {
|
|
|
62
47
|
* - shared_steps.js: may not import evaluators.js, steps.js, shared_steps.js, workflow.js
|
|
63
48
|
* - steps.js: at each step().fn body: calling any evaluator, step, shared_step or workflow is forbidden
|
|
64
49
|
* - steps.js: may not import evaluators.js, steps.js, shared_steps.js, workflow.js
|
|
65
|
-
* - workflow.js: may only import components: evaluators.js, steps.js, shared_steps.js, workflow.js; and files: types.js or `@output.ai/core`
|
|
66
50
|
*
|
|
67
51
|
* @param {string|Buffer} source
|
|
68
52
|
* @param {any} inputMap
|
|
@@ -36,13 +36,6 @@ describe( 'workflow_validator loader', () => {
|
|
|
36
36
|
rmSync( dir, { recursive: true, force: true } );
|
|
37
37
|
} );
|
|
38
38
|
|
|
39
|
-
it( 'workflow.js: rejects external dependencies', async () => {
|
|
40
|
-
const dir = mkdtempSync( join( tmpdir(), 'wf-reject-' ) );
|
|
41
|
-
const src = 'import x from "./foo.js";';
|
|
42
|
-
await expect( runLoader( join( dir, 'workflow.js' ), src ) ).rejects.toThrow( /Invalid (import|dependency) in workflow\.js/ );
|
|
43
|
-
rmSync( dir, { recursive: true, force: true } );
|
|
44
|
-
} );
|
|
45
|
-
|
|
46
39
|
it( 'workflow.js: allows imports from @output.ai/core and local_core', async () => {
|
|
47
40
|
const dir = mkdtempSync( join( tmpdir(), 'wf-allow-external-' ) );
|
|
48
41
|
const src = [
|
|
@@ -125,24 +118,6 @@ describe( 'workflow_validator loader', () => {
|
|
|
125
118
|
rmSync( dir, { recursive: true, force: true } );
|
|
126
119
|
} );
|
|
127
120
|
|
|
128
|
-
it( 'workflow.js: allows require from steps/shared_steps/evaluators/workflow; rejects others', async () => {
|
|
129
|
-
const dir = mkdtempSync( join( tmpdir(), 'wf-req-' ) );
|
|
130
|
-
writeFileSync( join( dir, 'steps.js' ), 'export const S = step({ name: "s" })\n' );
|
|
131
|
-
writeFileSync( join( dir, 'shared_steps.js' ), 'export const SS = step({ name: "ss" })\n' );
|
|
132
|
-
writeFileSync( join( dir, 'evaluators.js' ), 'export const E = evaluator({ name: "e" })\n' );
|
|
133
|
-
writeFileSync( join( dir, 'workflow.js' ), 'export default workflow({ name: "w" })\n' );
|
|
134
|
-
const ok = [
|
|
135
|
-
'const { S } = require("./steps.js");',
|
|
136
|
-
'const { SS } = require("./shared_steps.js");',
|
|
137
|
-
'const { E } = require("./evaluators.js");',
|
|
138
|
-
'const W = require("./workflow.js");'
|
|
139
|
-
].join( '\n' );
|
|
140
|
-
await expect( runLoader( join( dir, 'workflow.js' ), ok ) ).resolves.toBeTruthy();
|
|
141
|
-
const bad = 'const X = require("./util.js");';
|
|
142
|
-
await expect( runLoader( join( dir, 'workflow.js' ), bad ) ).rejects.toThrow( /Invalid (require|dependency) in workflow\.js/ );
|
|
143
|
-
rmSync( dir, { recursive: true, force: true } );
|
|
144
|
-
} );
|
|
145
|
-
|
|
146
121
|
it( 'steps.js: rejects importing shared_steps/evaluators/workflow variants', async () => {
|
|
147
122
|
const dir = mkdtempSync( join( tmpdir(), 'steps-reject2-' ) );
|
|
148
123
|
await expect( runLoader( join( dir, 'steps.js' ), 'import { SS } from "./shared_steps.js";' ) )
|
|
@@ -210,25 +185,6 @@ describe( 'workflow_validator loader', () => {
|
|
|
210
185
|
rmSync( dir, { recursive: true, force: true } );
|
|
211
186
|
} );
|
|
212
187
|
|
|
213
|
-
it( 'workflow.js: allows importing ./types.js and bare types', async () => {
|
|
214
|
-
const dir = mkdtempSync( join( tmpdir(), 'wf-types-allow-' ) );
|
|
215
|
-
writeFileSync( join( dir, 'types.js' ), 'export const T = {}\n' );
|
|
216
|
-
const src1 = 'import { T } from "./types.js";';
|
|
217
|
-
await expect( runLoader( join( dir, 'workflow.js' ), src1 ) ).resolves.toBeTruthy();
|
|
218
|
-
rmSync( dir, { recursive: true, force: true } );
|
|
219
|
-
} );
|
|
220
|
-
|
|
221
|
-
it( 'workflow.js: allows importing extraneous files (consts/constants/vars/variables)', async () => {
|
|
222
|
-
const dir = mkdtempSync( join( tmpdir(), 'wf-extra-allow-' ) );
|
|
223
|
-
const bases = [ 'consts', 'constants', 'vars', 'variables' ];
|
|
224
|
-
for ( const base of bases ) {
|
|
225
|
-
writeFileSync( join( dir, `${base}.js` ), 'export const X = 1\n' );
|
|
226
|
-
const src = `import x from "./${base}.js";`;
|
|
227
|
-
await expect( runLoader( join( dir, 'workflow.js' ), src ) ).resolves.toBeTruthy();
|
|
228
|
-
}
|
|
229
|
-
rmSync( dir, { recursive: true, force: true } );
|
|
230
|
-
} );
|
|
231
|
-
|
|
232
188
|
it( 'steps.js: rejects require of steps/shared_steps/evaluators/workflow; allows other require', async () => {
|
|
233
189
|
const dir = mkdtempSync( join( tmpdir(), 'steps-require-' ) );
|
|
234
190
|
await expect( runLoader( join( dir, 'steps.js' ), 'const { S } = require("./steps.js");' ) )
|