@output.ai/core 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ 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, METADATA_ACCESS_SYMBOL } from '#consts';
|
|
6
|
-
import { deepMerge,
|
|
6
|
+
import { deepMerge, setMetadata, toUrlSafeBase64 } from '#utils';
|
|
7
7
|
import { FatalError, ValidationError } from '#errors';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -53,10 +53,10 @@ const defaultOptions = {
|
|
|
53
53
|
disableTrace: false
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
export function workflow( { name, description, inputSchema, outputSchema, fn, options } ) {
|
|
56
|
+
export function workflow( { name, description, inputSchema, outputSchema, fn, options = {} } ) {
|
|
57
57
|
validateWorkflow( { name, description, inputSchema, outputSchema, fn, options } );
|
|
58
58
|
|
|
59
|
-
const activityOptions =
|
|
59
|
+
const { disableTrace, activityOptions } = deepMerge( defaultOptions, options );
|
|
60
60
|
const steps = proxyActivities( activityOptions );
|
|
61
61
|
|
|
62
62
|
/**
|
|
@@ -71,7 +71,7 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
71
71
|
if ( !inWorkflowContext() ) {
|
|
72
72
|
validateWithSchema( inputSchema, input, `Workflow ${name} input` );
|
|
73
73
|
const context = Context.build( { workflowId: 'test-workflow', continueAsNew: async () => {}, isContinueAsNewSuggested: () => false } );
|
|
74
|
-
const output = await fn( input, deepMerge( context, extra.context
|
|
74
|
+
const output = await fn( input, deepMerge( context, extra.context ) );
|
|
75
75
|
validateWithSchema( outputSchema, output, `Workflow ${name} output` );
|
|
76
76
|
return output;
|
|
77
77
|
}
|
|
@@ -89,7 +89,7 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
89
89
|
const executionContext = memo.executionContext ?? {
|
|
90
90
|
workflowId,
|
|
91
91
|
workflowName: name,
|
|
92
|
-
disableTrace
|
|
92
|
+
disableTrace,
|
|
93
93
|
startTime: startTime.getTime()
|
|
94
94
|
};
|
|
95
95
|
|
|
@@ -131,7 +131,7 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
131
131
|
executionContext,
|
|
132
132
|
parentId: workflowId,
|
|
133
133
|
// new configuration for activities of the child workflow, this will be omitted so it will use what that workflow have defined
|
|
134
|
-
...( extra?.options?.activityOptions && { activityOptions:
|
|
134
|
+
...( extra?.options?.activityOptions && { activityOptions: deepMerge( activityOptions, extra.options.activityOptions ) } )
|
|
135
135
|
}
|
|
136
136
|
} )
|
|
137
137
|
}, input, context );
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const traceDestinationsStepMock = vi.fn().mockResolvedValue( { local: '/tmp/trace' } );
|
|
5
|
+
const proxyActivitiesMock = vi.fn( () => new Proxy( {}, {
|
|
6
|
+
get: ( _, prop ) => {
|
|
7
|
+
if ( prop === '__internal#getTraceDestinations' ) {
|
|
8
|
+
return traceDestinationsStepMock;
|
|
9
|
+
}
|
|
10
|
+
return vi.fn();
|
|
11
|
+
}
|
|
12
|
+
} ) );
|
|
13
|
+
const workflowInfoMock = vi.fn( () => ( {
|
|
14
|
+
workflowId: 'wf-test-123',
|
|
15
|
+
workflowType: 'test_wf',
|
|
16
|
+
memo: {},
|
|
17
|
+
startTime: new Date( '2025-01-01T00:00:00Z' ),
|
|
18
|
+
continueAsNewSuggested: false
|
|
19
|
+
} ) );
|
|
20
|
+
|
|
21
|
+
vi.mock( '@temporalio/workflow', () => ( {
|
|
22
|
+
proxyActivities: proxyActivitiesMock,
|
|
23
|
+
inWorkflowContext: () => true,
|
|
24
|
+
executeChild: vi.fn(),
|
|
25
|
+
workflowInfo: workflowInfoMock,
|
|
26
|
+
uuid4: () => 'uuid-mock',
|
|
27
|
+
ParentClosePolicy: { TERMINATE: 'TERMINATE', ABANDON: 'ABANDON' },
|
|
28
|
+
continueAsNew: vi.fn()
|
|
29
|
+
} ) );
|
|
30
|
+
|
|
31
|
+
vi.mock( '#consts', () => ( {
|
|
32
|
+
SHARED_STEP_PREFIX: '__shared',
|
|
33
|
+
ACTIVITY_GET_TRACE_DESTINATIONS: '__internal#getTraceDestinations',
|
|
34
|
+
METADATA_ACCESS_SYMBOL: Symbol( 'metadata' )
|
|
35
|
+
} ) );
|
|
36
|
+
|
|
37
|
+
describe( 'workflow()', () => {
|
|
38
|
+
beforeEach( () => {
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
workflowInfoMock.mockReturnValue( {
|
|
41
|
+
workflowId: 'wf-test-123',
|
|
42
|
+
workflowType: 'test_wf',
|
|
43
|
+
memo: {},
|
|
44
|
+
startTime: new Date( '2025-01-01T00:00:00Z' ),
|
|
45
|
+
continueAsNewSuggested: false
|
|
46
|
+
} );
|
|
47
|
+
} );
|
|
48
|
+
|
|
49
|
+
it( 'does not throw when options is omitted (disableTrace defaults to false)', async () => {
|
|
50
|
+
const { workflow } = await import( './workflow.js' );
|
|
51
|
+
|
|
52
|
+
const wf = workflow( {
|
|
53
|
+
name: 'no_options_wf',
|
|
54
|
+
description: 'Workflow without options',
|
|
55
|
+
inputSchema: z.object( { value: z.string() } ),
|
|
56
|
+
outputSchema: z.object( { value: z.string() } ),
|
|
57
|
+
fn: async ( { value } ) => ( { value } )
|
|
58
|
+
} );
|
|
59
|
+
|
|
60
|
+
const result = await wf( { value: 'hello' } );
|
|
61
|
+
expect( result.output ).toEqual( { value: 'hello' } );
|
|
62
|
+
} );
|
|
63
|
+
|
|
64
|
+
it( 'respects disableTrace: true when options is provided', async () => {
|
|
65
|
+
const { workflow } = await import( './workflow.js' );
|
|
66
|
+
|
|
67
|
+
const wf = workflow( {
|
|
68
|
+
name: 'trace_disabled_wf',
|
|
69
|
+
description: 'Workflow with tracing disabled',
|
|
70
|
+
inputSchema: z.object( { value: z.string() } ),
|
|
71
|
+
outputSchema: z.object( { value: z.string() } ),
|
|
72
|
+
options: { disableTrace: true },
|
|
73
|
+
fn: async ( { value } ) => ( { value } )
|
|
74
|
+
} );
|
|
75
|
+
|
|
76
|
+
const result = await wf( { value: 'hello' } );
|
|
77
|
+
expect( result.output ).toEqual( { value: 'hello' } );
|
|
78
|
+
} );
|
|
79
|
+
} );
|
package/src/utils/index.d.ts
CHANGED
|
@@ -33,14 +33,6 @@ export function throws( error: Error ): void;
|
|
|
33
33
|
*/
|
|
34
34
|
export function setMetadata( target: object, value: object ): void;
|
|
35
35
|
|
|
36
|
-
/**
|
|
37
|
-
* Merge two temporal activity options
|
|
38
|
-
*/
|
|
39
|
-
export function mergeActivityOptions(
|
|
40
|
-
base?: import( '@temporalio/workflow' ).ActivityOptions,
|
|
41
|
-
ext?: import( '@temporalio/workflow' ).ActivityOptions
|
|
42
|
-
): import( '@temporalio/workflow' ).ActivityOptions;
|
|
43
|
-
|
|
44
36
|
/** Represents a {Response} serialized to plain object */
|
|
45
37
|
export type SerializedFetchResponse = {
|
|
46
38
|
/** The response url */
|
package/src/utils/utils.js
CHANGED
|
@@ -37,17 +37,6 @@ export const throws = e => {
|
|
|
37
37
|
export const setMetadata = ( target, values ) =>
|
|
38
38
|
Object.defineProperty( target, METADATA_ACCESS_SYMBOL, { value: values, writable: false, enumerable: false, configurable: false } );
|
|
39
39
|
|
|
40
|
-
/**
|
|
41
|
-
* Merge two temporal activity options
|
|
42
|
-
* @param {import('@temporalio/workflow').ActivityOptions} base
|
|
43
|
-
* @param {import('@temporalio/workflow').ActivityOptions} ext
|
|
44
|
-
* @returns {import('@temporalio/workflow').ActivityOptions}
|
|
45
|
-
*/
|
|
46
|
-
export const mergeActivityOptions = ( base = {}, ext = {} ) =>
|
|
47
|
-
Object.entries( ext ).reduce( ( options, [ k, v ] ) =>
|
|
48
|
-
Object.assign( options, { [k]: typeof v === 'object' ? mergeActivityOptions( options[k], v ) : v } )
|
|
49
|
-
, clone( base ) );
|
|
50
|
-
|
|
51
40
|
/**
|
|
52
41
|
* Returns true if string value is stringbool and true
|
|
53
42
|
* @param {string} v
|
|
@@ -179,6 +168,7 @@ export const shuffleArray = arr => arr
|
|
|
179
168
|
* - Object "b" fields that don't exist on object "a" will be created;
|
|
180
169
|
* - Object "a" fields that don't exist on object "b" will not be touched;
|
|
181
170
|
*
|
|
171
|
+
* If "b" isn't an object, a new object equal to "a" is returned
|
|
182
172
|
*
|
|
183
173
|
* @param {object} a - The base object
|
|
184
174
|
* @param {object} b - The target object
|
|
@@ -189,7 +179,7 @@ export const deepMerge = ( a, b ) => {
|
|
|
189
179
|
throw new Error( 'Parameter "a" is not an object.' );
|
|
190
180
|
}
|
|
191
181
|
if ( !isPlainObject( b ) ) {
|
|
192
|
-
|
|
182
|
+
return clone( a );
|
|
193
183
|
}
|
|
194
184
|
return Object.entries( b ).reduce( ( obj, [ k, v ] ) =>
|
|
195
185
|
Object.assign( obj, { [k]: isPlainObject( v ) && isPlainObject( a[k] ) ? deepMerge( a[k], v ) : v } )
|
package/src/utils/utils.spec.js
CHANGED
|
@@ -2,7 +2,6 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { Readable } from 'node:stream';
|
|
3
3
|
import {
|
|
4
4
|
clone,
|
|
5
|
-
mergeActivityOptions,
|
|
6
5
|
serializeBodyAndInferContentType,
|
|
7
6
|
serializeFetchResponse,
|
|
8
7
|
deepMerge,
|
|
@@ -223,50 +222,6 @@ describe( 'serializeBodyAndInferContentType', () => {
|
|
|
223
222
|
} );
|
|
224
223
|
} );
|
|
225
224
|
|
|
226
|
-
describe( 'mergeActivityOptions', () => {
|
|
227
|
-
it( 'recursively merges nested objects', () => {
|
|
228
|
-
const base = {
|
|
229
|
-
taskQueue: 'q1',
|
|
230
|
-
retry: { maximumAttempts: 3, backoffCoefficient: 2 }
|
|
231
|
-
};
|
|
232
|
-
const ext = {
|
|
233
|
-
retry: { maximumAttempts: 5, initialInterval: '1s' }
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const result = mergeActivityOptions( base, ext );
|
|
237
|
-
|
|
238
|
-
expect( result ).toEqual( {
|
|
239
|
-
taskQueue: 'q1',
|
|
240
|
-
retry: { maximumAttempts: 5, backoffCoefficient: 2, initialInterval: '1s' }
|
|
241
|
-
} );
|
|
242
|
-
} );
|
|
243
|
-
|
|
244
|
-
it( 'omitted properties in second do not overwrite first', () => {
|
|
245
|
-
const base = {
|
|
246
|
-
taskQueue: 'q2',
|
|
247
|
-
retry: { initialInterval: '2s', backoffCoefficient: 2 }
|
|
248
|
-
};
|
|
249
|
-
const ext = {
|
|
250
|
-
retry: { backoffCoefficient: 3 }
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const result = mergeActivityOptions( base, ext );
|
|
254
|
-
|
|
255
|
-
expect( result.retry.initialInterval ).toBe( '2s' );
|
|
256
|
-
expect( result.retry.backoffCoefficient ).toBe( 3 );
|
|
257
|
-
expect( result.taskQueue ).toBe( 'q2' );
|
|
258
|
-
} );
|
|
259
|
-
|
|
260
|
-
it( 'handles omitted second argument by returning a clone', () => {
|
|
261
|
-
const base = { taskQueue: 'q3', retry: { maximumAttempts: 2 } };
|
|
262
|
-
|
|
263
|
-
const result = mergeActivityOptions( base );
|
|
264
|
-
|
|
265
|
-
expect( result ).toEqual( base );
|
|
266
|
-
expect( result ).not.toBe( base );
|
|
267
|
-
} );
|
|
268
|
-
} );
|
|
269
|
-
|
|
270
225
|
describe( 'deepMerge', () => {
|
|
271
226
|
it( 'Overwrites properties in object "a"', () => {
|
|
272
227
|
const a = {
|
|
@@ -316,13 +271,43 @@ describe( 'deepMerge', () => {
|
|
|
316
271
|
} );
|
|
317
272
|
} );
|
|
318
273
|
|
|
319
|
-
it( '
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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 );
|
|
326
311
|
expect( () => deepMerge( class Foo {}, class Foo {} ) ).toThrow( Error );
|
|
327
312
|
expect( () => deepMerge( Number.constructor, Number.constructor ) ).toThrow( Error );
|
|
328
313
|
expect( () => deepMerge( Number.constructor.prototype, Number.constructor.prototype ) ).toThrow( Error );
|
|
@@ -377,6 +362,8 @@ describe( 'isPlainObject', () => {
|
|
|
377
362
|
} );
|
|
378
363
|
|
|
379
364
|
it( 'Returns false for primitives', () => {
|
|
365
|
+
expect( isPlainObject( null ) ).toBe( false );
|
|
366
|
+
expect( isPlainObject( undefined ) ).toBe( false );
|
|
380
367
|
expect( isPlainObject( false ) ).toBe( false );
|
|
381
368
|
expect( isPlainObject( true ) ).toBe( false );
|
|
382
369
|
expect( isPlainObject( 1 ) ).toBe( false );
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
|
|
2
2
|
import { workflowInfo, proxySinks, ApplicationFailure, ContinueAsNew } from '@temporalio/workflow';
|
|
3
3
|
import { memoToHeaders } from '../sandboxed_utils.js';
|
|
4
|
-
import {
|
|
4
|
+
import { deepMerge } from '#utils';
|
|
5
5
|
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
6
6
|
// this is a dynamic generated file with activity configs overwrites
|
|
7
7
|
import stepOptions from '../temp/__activity_options.js';
|
|
@@ -22,7 +22,7 @@ class HeadersInjectionInterceptor {
|
|
|
22
22
|
// apply per-invocation options passed as second argument by rewritten calls
|
|
23
23
|
const options = stepOptions[input.activityType];
|
|
24
24
|
if ( options ) {
|
|
25
|
-
input.options =
|
|
25
|
+
input.options = deepMerge( memo.activityOptions, options );
|
|
26
26
|
}
|
|
27
27
|
return next( input );
|
|
28
28
|
}
|