@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/core",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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, mergeActivityOptions, setMetadata, toUrlSafeBase64 } from '#utils';
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 = mergeActivityOptions( defaultOptions.activityOptions, options?.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: options.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: mergeActivityOptions( activityOptions, extra.options.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
+ } );
@@ -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 */
@@ -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
- throw new Error( 'Parameter "b" is not an object.' );
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 } )
@@ -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( 'Throw error on non iterable object types', () => {
320
- expect( () => deepMerge( Function, Function ) ).toThrow( Error );
321
- expect( () => deepMerge( () => {}, () => {} ) ).toThrow( Error );
322
- expect( () => deepMerge( 'a', 'a' ) ).toThrow( Error );
323
- expect( () => deepMerge( true, true ) ).toThrow( Error );
324
- expect( () => deepMerge( /a/, /a/ ) ).toThrow( Error );
325
- expect( () => deepMerge( [], [] ) ).toThrow( Error );
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 { mergeActivityOptions } from '#utils';
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 = mergeActivityOptions( memo.activityOptions, options );
25
+ input.options = deepMerge( memo.activityOptions, options );
26
26
  }
27
27
  return next( input );
28
28
  }