@output.ai/core 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -59
- package/package.json +10 -3
- package/src/configs.js +1 -1
- package/src/consts.js +4 -3
- package/src/errors.js +11 -0
- package/src/index.d.ts +302 -30
- package/src/index.js +3 -2
- package/src/interface/metadata.js +3 -3
- package/src/interface/step.js +18 -4
- package/src/interface/utils.js +41 -4
- package/src/interface/utils.spec.js +71 -0
- package/src/interface/validations/ajv_provider.js +3 -0
- package/src/interface/validations/runtime.js +69 -0
- package/src/interface/validations/runtime.spec.js +50 -0
- package/src/interface/validations/static.js +67 -0
- package/src/interface/validations/static.spec.js +101 -0
- package/src/interface/webhook.js +15 -14
- package/src/interface/workflow.js +45 -40
- package/src/internal_activities/index.js +16 -5
- package/src/worker/catalog_workflow/catalog.js +105 -0
- package/src/worker/catalog_workflow/index.js +21 -0
- package/src/worker/catalog_workflow/index.spec.js +139 -0
- package/src/worker/catalog_workflow/workflow.js +13 -0
- package/src/worker/index.js +41 -5
- package/src/worker/interceptors/activity.js +3 -2
- package/src/worker/internal_utils.js +60 -0
- package/src/worker/internal_utils.spec.js +134 -0
- package/src/worker/loader.js +30 -44
- package/src/worker/loader.spec.js +68 -0
- package/src/worker/sinks.js +2 -1
- package/src/worker/tracer/index.js +35 -3
- package/src/worker/tracer/index.test.js +115 -0
- package/src/worker/tracer/tracer_tree.js +29 -5
- package/src/worker/tracer/tracer_tree.test.js +116 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +133 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +77 -0
- package/src/worker/webpack_loaders/workflow_rewriter/consts.js +3 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +58 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +129 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +70 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +33 -0
- package/src/worker/webpack_loaders/workflow_rewriter/tools.js +245 -0
- package/src/worker/webpack_loaders/workflow_rewriter/tools.spec.js +144 -0
- package/src/errors.d.ts +0 -3
- package/src/worker/temp/__workflows_entrypoint.js +0 -6
package/src/interface/utils.js
CHANGED
|
@@ -1,9 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Function rigged to return the folder path of the source of calls for the interface methods (step/workflow)
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT!!!
|
|
5
|
+
* If to refactor this, pay attention to the depth in the stack trace to extract the info.
|
|
6
|
+
* Currently it is 3:
|
|
7
|
+
* - 1st line is the name of the function;
|
|
8
|
+
* - 2nd line is this function;
|
|
9
|
+
* - 3rd line is step/workflow;
|
|
10
|
+
* - 4th line is caller;
|
|
11
|
+
*
|
|
12
|
+
* @returns {string} The folder path of the caller
|
|
13
|
+
*/
|
|
14
|
+
export const getInvocationDir = () => new Error()
|
|
5
15
|
.stack.split( '\n' )[3]
|
|
6
16
|
.split( ' ' )
|
|
7
17
|
.at( -1 )
|
|
8
18
|
.replace( /\((.+):\d+:\d+\)/, '$1' )
|
|
9
19
|
.split( '/' ).slice( 0, -1 ).join( '/' );
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* This mouthful function will invoke a function with given arguments, and validate its return
|
|
23
|
+
* using a given validator.
|
|
24
|
+
*
|
|
25
|
+
* It will preserver the execution model (asynchronous vs synchronous), so if the function is
|
|
26
|
+
* sync the validation happens here, if it is async (returns Promise) the validation is attached
|
|
27
|
+
* to a .then().
|
|
28
|
+
*
|
|
29
|
+
*
|
|
30
|
+
* @param {Function} fn - The function to execute
|
|
31
|
+
* @param {any} input - The payload to call the function
|
|
32
|
+
* @param {Function} validate - The validator function
|
|
33
|
+
* @returns {any} Function result (Promise or not)
|
|
34
|
+
*/
|
|
35
|
+
export const invokeFnAndValidateOutputPreservingExecutionModel = ( fn, input, validate ) => {
|
|
36
|
+
const uniformReturn = output => {
|
|
37
|
+
validate( output );
|
|
38
|
+
return output;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const output = fn( input );
|
|
42
|
+
if ( output?.constructor === Promise ) {
|
|
43
|
+
return output.then( resolvedOutput => uniformReturn( resolvedOutput ) );
|
|
44
|
+
}
|
|
45
|
+
return uniformReturn( output );
|
|
46
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { getInvocationDir, invokeFnAndValidateOutputPreservingExecutionModel } from './utils.js';
|
|
3
|
+
|
|
4
|
+
describe( 'interface/utils', () => {
|
|
5
|
+
describe( 'getInvocationDir', () => {
|
|
6
|
+
it( 'returns the caller directory from stack trace', () => {
|
|
7
|
+
const fakeCaller = '/tmp/project/src/caller/file.js';
|
|
8
|
+
const OriginalError = Error;
|
|
9
|
+
// Provide a deterministic stack shape for the function under test
|
|
10
|
+
// Lines: 1) Error, 2) getInvocationDir, 3) step/workflow, 4) actual caller
|
|
11
|
+
// Include typical V8 formatting with leading spaces and without function name
|
|
12
|
+
// for the caller line
|
|
13
|
+
|
|
14
|
+
Error = class extends OriginalError {
|
|
15
|
+
constructor( ...args ) {
|
|
16
|
+
super( ...args );
|
|
17
|
+
this.stack = [
|
|
18
|
+
'Error',
|
|
19
|
+
' at getInvocationDir (a:1:1)',
|
|
20
|
+
' at step (b:1:1)',
|
|
21
|
+
` at ${fakeCaller}:10:20`
|
|
22
|
+
].join( '\n' );
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
try {
|
|
26
|
+
const dir = getInvocationDir();
|
|
27
|
+
expect( dir ).toBe( '/tmp/project/src/caller' );
|
|
28
|
+
} finally {
|
|
29
|
+
|
|
30
|
+
Error = OriginalError;
|
|
31
|
+
}
|
|
32
|
+
} );
|
|
33
|
+
} );
|
|
34
|
+
|
|
35
|
+
describe( 'invokeFnAndValidateOutputPreservingExecutionModel', () => {
|
|
36
|
+
it( 'validates and returns sync output', () => {
|
|
37
|
+
const fn = vi.fn( x => x * 2 );
|
|
38
|
+
const validate = vi.fn();
|
|
39
|
+
const result = invokeFnAndValidateOutputPreservingExecutionModel( fn, 3, validate );
|
|
40
|
+
expect( result ).toBe( 6 );
|
|
41
|
+
expect( validate ).toHaveBeenCalledWith( 6 );
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
it( 'validates and returns async output preserving promise', async () => {
|
|
45
|
+
const fn = vi.fn( async x => x + 1 );
|
|
46
|
+
const validate = vi.fn();
|
|
47
|
+
const resultPromise = invokeFnAndValidateOutputPreservingExecutionModel( fn, 4, validate );
|
|
48
|
+
expect( resultPromise ).toBeInstanceOf( Promise );
|
|
49
|
+
const result = await resultPromise;
|
|
50
|
+
expect( result ).toBe( 5 );
|
|
51
|
+
expect( validate ).toHaveBeenCalledWith( 5 );
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
it( 'propagates validator errors (sync)', () => {
|
|
55
|
+
const fn = vi.fn( x => x );
|
|
56
|
+
const validate = vi.fn( () => {
|
|
57
|
+
throw new Error( 'invalid' );
|
|
58
|
+
} );
|
|
59
|
+
expect( () => invokeFnAndValidateOutputPreservingExecutionModel( fn, 'a', validate ) ).toThrow( 'invalid' );
|
|
60
|
+
} );
|
|
61
|
+
|
|
62
|
+
it( 'propagates validator errors (async)', async () => {
|
|
63
|
+
const fn = vi.fn( async x => x );
|
|
64
|
+
const validate = vi.fn( () => {
|
|
65
|
+
throw new Error( 'invalid' );
|
|
66
|
+
} );
|
|
67
|
+
await expect( invokeFnAndValidateOutputPreservingExecutionModel( fn, 'a', validate ) ).rejects.toThrow( 'invalid' );
|
|
68
|
+
} );
|
|
69
|
+
} );
|
|
70
|
+
} );
|
|
71
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { FatalError } from '#errors';
|
|
2
|
+
import { ajv } from './ajv_provider.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error type for when the input/output of a step/workflow doesn't match its input/output schema, respectively
|
|
6
|
+
*/
|
|
7
|
+
export class MismatchSchemaError extends FatalError {}
|
|
8
|
+
/**
|
|
9
|
+
* Error type for when the input of a step/workflow doesn't match its input schema
|
|
10
|
+
* @extends MismatchSchemaError
|
|
11
|
+
*/
|
|
12
|
+
export class InvalidInputError extends MismatchSchemaError {}
|
|
13
|
+
/**
|
|
14
|
+
* Error type for when the output of a step/workflow doesn't match its output schema
|
|
15
|
+
* @extends MismatchSchemaError
|
|
16
|
+
*/
|
|
17
|
+
export class InvalidOutputError extends MismatchSchemaError {}
|
|
18
|
+
|
|
19
|
+
const validate = ( ErrorClass, type, name, schema, payload ) => {
|
|
20
|
+
const validate = ajv.compile( schema );
|
|
21
|
+
const valid = validate( payload );
|
|
22
|
+
|
|
23
|
+
if ( !valid ) {
|
|
24
|
+
throw new ErrorClass( `Invalid input at ${type} "${name}": ${ajv.errorsText( validate.errors )}` );
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const validateInput = validate.bind( null, InvalidInputError );
|
|
29
|
+
const validateOutput = validate.bind( null, InvalidOutputError );
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validates step input
|
|
33
|
+
*
|
|
34
|
+
* @param {name} name - step's name
|
|
35
|
+
* @param {object} schema - step's input schema
|
|
36
|
+
* @param {any} - the input to validate
|
|
37
|
+
* @throws InvalidInputError
|
|
38
|
+
*/
|
|
39
|
+
export const validateStepInput = validateInput.bind( null, 'step' );
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validates step output
|
|
43
|
+
*
|
|
44
|
+
* @param {name} name - step's name
|
|
45
|
+
* @param {object} schema - step's output schema
|
|
46
|
+
* @param {any} - the output to validate
|
|
47
|
+
* @throws InvalidOutputError
|
|
48
|
+
*/
|
|
49
|
+
export const validateStepOutput = validateOutput.bind( null, 'step' );
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validates workflow input
|
|
53
|
+
*
|
|
54
|
+
* @param {name} name - workflow's name
|
|
55
|
+
* @param {object} schema - workflow's input schema
|
|
56
|
+
* @param {any} - the input to validate
|
|
57
|
+
* @throws InvalidInputError
|
|
58
|
+
*/
|
|
59
|
+
export const validateWorkflowInput = validateInput.bind( null, 'workflow' );
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validates workflow output
|
|
63
|
+
*
|
|
64
|
+
* @param {name} name - workflow's name
|
|
65
|
+
* @param {object} schema - workflow's output schema
|
|
66
|
+
* @param {any} - the output to validate
|
|
67
|
+
* @throws InvalidOutputError
|
|
68
|
+
*/
|
|
69
|
+
export const validateWorkflowOutput = validateOutput.bind( null, 'workflow' );
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateStepInput, validateWorkflowInput, InvalidInputError } from './runtime.js';
|
|
3
|
+
|
|
4
|
+
describe( 'Runtime validations spec', () => {
|
|
5
|
+
describe( 'validateStepInput', () => {
|
|
6
|
+
it( 'passes with matching payload', () => {
|
|
7
|
+
const schema = {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: { id: { type: 'string' }, count: { type: 'integer', minimum: 0 } },
|
|
10
|
+
required: [ 'id' ],
|
|
11
|
+
additionalProperties: false
|
|
12
|
+
};
|
|
13
|
+
expect( () => validateStepInput( 'myStep', schema, { id: 'x', count: 2 } ) ).not.toThrow();
|
|
14
|
+
} );
|
|
15
|
+
|
|
16
|
+
it( 'rejects invalid payload', () => {
|
|
17
|
+
const schema = {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: { id: { type: 'string' }, count: { type: 'integer', minimum: 0 } },
|
|
20
|
+
required: [ 'id' ],
|
|
21
|
+
additionalProperties: false
|
|
22
|
+
};
|
|
23
|
+
const error = new InvalidInputError( 'Invalid input at step "myStep": data must have required property \'id\'' );
|
|
24
|
+
expect( () => validateStepInput( 'myStep', schema, { count: -1 } ) ).toThrow( error );
|
|
25
|
+
} );
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
describe( 'validateWorkflowInput', () => {
|
|
29
|
+
it( 'passes with matching payload', () => {
|
|
30
|
+
const schema = {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: { name: { type: 'string' }, enabled: { type: 'boolean' } },
|
|
33
|
+
required: [ 'name' ],
|
|
34
|
+
additionalProperties: false
|
|
35
|
+
};
|
|
36
|
+
expect( () => validateWorkflowInput( 'myWorkflow', schema, { name: 'wf', enabled: true } ) ).not.toThrow();
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
it( 'rejects invalid payload', () => {
|
|
40
|
+
const schema = {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: { name: { type: 'string' }, enabled: { type: 'boolean' } },
|
|
43
|
+
required: [ 'name' ],
|
|
44
|
+
additionalProperties: false
|
|
45
|
+
};
|
|
46
|
+
const error = new InvalidInputError( 'Invalid input at workflow "myWorkflow": data must have required property \'name\'' );
|
|
47
|
+
expect( () => validateWorkflowInput( 'myWorkflow', schema, { enabled: 'yes' } ) ).toThrow( error );
|
|
48
|
+
} );
|
|
49
|
+
} );
|
|
50
|
+
} );
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ajv } from './ajv_provider.js';
|
|
2
|
+
import * as z from 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error is thrown when the definition of a step/workflow has problems
|
|
6
|
+
*/
|
|
7
|
+
export class StaticValidationError extends Error {};
|
|
8
|
+
|
|
9
|
+
// Custom validation for zod, to be used when validating JSONSchema def with ajv
|
|
10
|
+
const refineJsonSchema = ( value, ctx ) => {
|
|
11
|
+
if ( value && !ajv.validateSchema( value ) ) {
|
|
12
|
+
ctx.addIssue( {
|
|
13
|
+
code: 'invalid_format',
|
|
14
|
+
message: ajv.errorsText( ajv.errors )
|
|
15
|
+
} );
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const stepAndWorkflowSchema = z.object( {
|
|
20
|
+
name: z.string().regex( /^[a-z_][a-z0-9_]*$/i ),
|
|
21
|
+
description: z.string().optional(),
|
|
22
|
+
inputSchema: z.looseObject( {} ).optional().superRefine( refineJsonSchema ),
|
|
23
|
+
outputSchema: z.looseObject( {} ).optional().superRefine( refineJsonSchema ),
|
|
24
|
+
fn: z.function()
|
|
25
|
+
} );
|
|
26
|
+
|
|
27
|
+
const webhookSchema = z.object( {
|
|
28
|
+
url: z.url( { protocol: /^https?$/ } ),
|
|
29
|
+
payload: z.any().optional()
|
|
30
|
+
} );
|
|
31
|
+
|
|
32
|
+
const validateAgainstSchema = ( schema, args ) => {
|
|
33
|
+
const result = schema.safeParse( args );
|
|
34
|
+
if ( !result.success ) {
|
|
35
|
+
throw new StaticValidationError( z.prettifyError( result.error ) );
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate step payload
|
|
41
|
+
*
|
|
42
|
+
* @param {object} args - The step arguments
|
|
43
|
+
* @throws {StaticValidationError} Throws if args are invalid
|
|
44
|
+
*/
|
|
45
|
+
export function validateStep( args ) {
|
|
46
|
+
validateAgainstSchema( stepAndWorkflowSchema, args );
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate workflow payload
|
|
51
|
+
*
|
|
52
|
+
* @param {object} args - The workflow arguments
|
|
53
|
+
* @throws {StaticValidationError} Throws if args are invalid
|
|
54
|
+
*/
|
|
55
|
+
export function validateWorkflow( args ) {
|
|
56
|
+
validateAgainstSchema( stepAndWorkflowSchema, args );
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validate createWebhook payload
|
|
61
|
+
*
|
|
62
|
+
* @param {object} args - The createWebhook arguments
|
|
63
|
+
* @throws {StaticValidationError} Throws if args are invalid
|
|
64
|
+
*/
|
|
65
|
+
export function validateCreateWebhook( args ) {
|
|
66
|
+
validateAgainstSchema( webhookSchema, args );
|
|
67
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateStep, validateWorkflow, validateCreateWebhook, StaticValidationError } from './static.js';
|
|
3
|
+
|
|
4
|
+
const validArgs = Object.freeze( {
|
|
5
|
+
name: 'valid_name',
|
|
6
|
+
description: 'desc',
|
|
7
|
+
inputSchema: { type: 'object' },
|
|
8
|
+
outputSchema: { type: 'object' },
|
|
9
|
+
fn: () => {}
|
|
10
|
+
} );
|
|
11
|
+
|
|
12
|
+
describe( 'interface/validator', () => {
|
|
13
|
+
describe( 'validateStep', () => {
|
|
14
|
+
it( 'passes for valid args', () => {
|
|
15
|
+
expect( () => validateStep( { ...validArgs } ) ).not.toThrow();
|
|
16
|
+
} );
|
|
17
|
+
|
|
18
|
+
it( 'rejects missing name', () => {
|
|
19
|
+
const error = new StaticValidationError( '✖ Invalid input: expected string, received undefined\n → at name' );
|
|
20
|
+
expect( () => validateStep( { ...validArgs, name: undefined } ) ).toThrow( error );
|
|
21
|
+
} );
|
|
22
|
+
|
|
23
|
+
it( 'rejects non-string name', () => {
|
|
24
|
+
const error = new StaticValidationError( '✖ Invalid input: expected string, received number\n → at name' );
|
|
25
|
+
expect( () => validateStep( { ...validArgs, name: 123 } ) ).toThrow( error );
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
it( 'rejects invalid name pattern', () => {
|
|
29
|
+
const error = new StaticValidationError( '✖ Invalid string: must match pattern /^[a-z_][a-z0-9_]*$/i\n → at name' );
|
|
30
|
+
expect( () => validateStep( { ...validArgs, name: '-bad' } ) ).toThrow( error );
|
|
31
|
+
} );
|
|
32
|
+
|
|
33
|
+
it( 'rejects non-string description', () => {
|
|
34
|
+
const error = new StaticValidationError( '✖ Invalid input: expected string, received number\n → at description' );
|
|
35
|
+
expect( () => validateStep( { ...validArgs, description: 10 } ) ).toThrow( error );
|
|
36
|
+
} );
|
|
37
|
+
|
|
38
|
+
it( 'rejects non-object inputSchema', () => {
|
|
39
|
+
const error = new StaticValidationError( '✖ Invalid input: expected object, received string\n → at inputSchema' );
|
|
40
|
+
expect( () => validateStep( { ...validArgs, inputSchema: 'not-an-object' } ) ).toThrow( error );
|
|
41
|
+
} );
|
|
42
|
+
|
|
43
|
+
it( 'rejects invalid inputSchema structure', () => {
|
|
44
|
+
const error = new StaticValidationError( '✖ data/type must be equal to one of the allowed values, \
|
|
45
|
+
data/type must be array, data/type must match a schema in anyOf\n → at inputSchema' );
|
|
46
|
+
expect( () => validateStep( { ...validArgs, inputSchema: { type: 1 } } ) ).toThrow( error );
|
|
47
|
+
} );
|
|
48
|
+
|
|
49
|
+
it( 'rejects non-object outputSchema', () => {
|
|
50
|
+
const error = new StaticValidationError( '✖ Invalid input: expected object, received number\n → at outputSchema' );
|
|
51
|
+
expect( () => validateStep( { ...validArgs, outputSchema: 10 } ) ).toThrow( error );
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
it( 'rejects invalid outputSchema structure', () => {
|
|
55
|
+
const error = new StaticValidationError( '✖ data/type must be equal to one of the allowed values, \
|
|
56
|
+
data/type must be array, data/type must match a schema in anyOf\n → at outputSchema' );
|
|
57
|
+
expect( () => validateStep( { ...validArgs, outputSchema: { type: 1 } } ) ).toThrow( error );
|
|
58
|
+
} );
|
|
59
|
+
|
|
60
|
+
it( 'rejects missing fn', () => {
|
|
61
|
+
const error = new StaticValidationError( '✖ Invalid input: expected function, received undefined\n → at fn' );
|
|
62
|
+
expect( () => validateStep( { ...validArgs, fn: undefined } ) ).toThrow( error );
|
|
63
|
+
} );
|
|
64
|
+
|
|
65
|
+
it( 'rejects non-function fn', () => {
|
|
66
|
+
const error = new StaticValidationError( '✖ Invalid input: expected function, received string\n → at fn' );
|
|
67
|
+
expect( () => validateStep( { ...validArgs, fn: 'not-fn' } ) ).toThrow( error );
|
|
68
|
+
} );
|
|
69
|
+
} );
|
|
70
|
+
|
|
71
|
+
describe( 'validateWorkflow', () => {
|
|
72
|
+
it( 'passes for valid args', () => {
|
|
73
|
+
expect( () => validateWorkflow( { ...validArgs } ) ).not.toThrow();
|
|
74
|
+
} );
|
|
75
|
+
} );
|
|
76
|
+
|
|
77
|
+
describe( 'validate webhook', () => {
|
|
78
|
+
it( 'passes with valid http url', () => {
|
|
79
|
+
expect( () => validateCreateWebhook( { url: 'http://example.com' } ) ).not.toThrow();
|
|
80
|
+
} );
|
|
81
|
+
|
|
82
|
+
it( 'passes with valid https url', () => {
|
|
83
|
+
expect( () => validateCreateWebhook( { url: 'https://example.com/path?q=1' } ) ).not.toThrow();
|
|
84
|
+
} );
|
|
85
|
+
|
|
86
|
+
it( 'rejects missing url', () => {
|
|
87
|
+
const error = new StaticValidationError( '✖ Invalid input: expected string, received undefined\n → at url' );
|
|
88
|
+
expect( () => validateCreateWebhook( { } ) ).toThrow( error );
|
|
89
|
+
} );
|
|
90
|
+
|
|
91
|
+
it( 'rejects invalid scheme', () => {
|
|
92
|
+
const error = new StaticValidationError( '✖ Invalid URL\n → at url' );
|
|
93
|
+
expect( () => validateCreateWebhook( { url: 'ftp://example.com' } ) ).toThrow( error );
|
|
94
|
+
} );
|
|
95
|
+
|
|
96
|
+
it( 'rejects malformed url', () => {
|
|
97
|
+
const error = new StaticValidationError( '✖ Invalid URL\n → at url' );
|
|
98
|
+
expect( () => validateCreateWebhook( { url: 'http:////' } ) ).toThrow( error );
|
|
99
|
+
} );
|
|
100
|
+
} );
|
|
101
|
+
} );
|
package/src/interface/webhook.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
// THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
|
|
2
|
+
import { defineSignal, setHandler, proxyActivities, workflowInfo } from '@temporalio/workflow';
|
|
3
|
+
import { SEND_WEBHOOK_ACTIVITY_NAME } from '#consts';
|
|
4
|
+
import { validateCreateWebhook } from './validations/static.js';
|
|
2
5
|
|
|
3
|
-
export function createWebhook(
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if ( error ) {
|
|
7
|
-
throw new Error( 'Webhook call failed' );
|
|
8
|
-
}
|
|
6
|
+
export async function createWebhook( { url, payload } ) {
|
|
7
|
+
validateCreateWebhook( { url, payload } );
|
|
8
|
+
const workflowId = workflowInfo();
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
await proxyActivities( temporalActivityConfigs )[SEND_WEBHOOK_ACTIVITY_NAME]( { url, workflowId, payload } );
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
const resumeSignal = defineSignal( 'resume' );
|
|
13
|
+
|
|
14
|
+
return new Promise( resolve =>
|
|
15
|
+
setHandler( resumeSignal, responsePayload => {
|
|
16
|
+
resolve( responsePayload );
|
|
17
|
+
} )
|
|
18
|
+
);
|
|
18
19
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
|
|
2
|
-
import { proxyActivities,
|
|
3
|
-
import { getInvocationDir } from './utils.js';
|
|
4
|
-
import {
|
|
5
|
-
import { setName } from './metadata.js';
|
|
6
|
-
import { sendWebhookPostName } from '#consts';
|
|
2
|
+
import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, ApplicationFailure } from '@temporalio/workflow';
|
|
3
|
+
import { getInvocationDir, invokeFnAndValidateOutputPreservingExecutionModel } from './utils.js';
|
|
4
|
+
import { setMetadata } from './metadata.js';
|
|
7
5
|
import { FatalError, ValidationError } from '../errors.js';
|
|
6
|
+
import { validateWorkflow } from './validations/static.js';
|
|
7
|
+
import { validateWorkflowInput, validateWorkflowOutput } from './validations/runtime.js';
|
|
8
8
|
|
|
9
9
|
const temporalActivityConfigs = {
|
|
10
10
|
startToCloseTimeout: '20 minute',
|
|
@@ -17,53 +17,58 @@ const temporalActivityConfigs = {
|
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
export function workflow( { name, description
|
|
20
|
+
export function workflow( { name, description, inputSchema, outputSchema, fn } ) {
|
|
21
|
+
validateWorkflow( { name, description, inputSchema, outputSchema, fn } );
|
|
21
22
|
const workflowPath = getInvocationDir();
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
const steps = proxyActivities( temporalActivityConfigs );
|
|
25
|
+
|
|
24
26
|
const wrapper = async input => {
|
|
25
27
|
try {
|
|
26
|
-
|
|
28
|
+
if ( inputSchema ) {
|
|
29
|
+
validateWorkflowInput( name, inputSchema, input );
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// this returns a plain function, for example, in unit tests
|
|
33
|
+
if ( !inWorkflowContext() ) {
|
|
34
|
+
if ( outputSchema ) {
|
|
35
|
+
return invokeFnAndValidateOutputPreservingExecutionModel( fn, input, validateWorkflowOutput.bind( null, name, outputSchema ) );
|
|
36
|
+
}
|
|
37
|
+
return fn( input );
|
|
38
|
+
}
|
|
27
39
|
|
|
28
|
-
// enrich context information needed for tracking
|
|
29
40
|
Object.assign( workflowInfo().memo, { workflowPath } );
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
get( _target, name ) {
|
|
35
|
-
return proxies[`${workflowPath}#${name}`];
|
|
36
|
-
}
|
|
37
|
-
} ),
|
|
38
|
-
tools: {
|
|
39
|
-
webhook: createWebhook( workflowId, proxies[sendWebhookPostName] )
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
+
// binds the methods called in the code that Webpack loader will add, they will exposed via "this"
|
|
43
|
+
const boundFn = fn.bind( {
|
|
44
|
+
invokeStep: async ( stepName, input ) => steps[`${workflowPath}#${stepName}`]( input ),
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
startWorkflow: async ( name, input ) => {
|
|
47
|
+
const { memo, workflowId, workflowType } = workflowInfo();
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
// Checks if current memo has rootWorkflowId, which means current execution is already a child
|
|
50
|
+
// Then it sets the memory for the child execution passing along who's the original workflow is and its type
|
|
51
|
+
const workflowMemory = memo.rootWorkflowId ?
|
|
52
|
+
{ parentWorkflowId: workflowId, rootWorkflowType: memo.rootWorkflowType, rootWorkflowId: memo.rootWorkflowId } :
|
|
53
|
+
{ parentWorkflowId: workflowId, rootWorkflowId: workflowId, rootWorkflowType: workflowType };
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
return executeChild( name, { args: input ? [ input ] : [], memo: workflowMemory } );
|
|
56
|
+
}
|
|
57
|
+
} );
|
|
58
|
+
|
|
59
|
+
if ( outputSchema ) {
|
|
60
|
+
return invokeFnAndValidateOutputPreservingExecutionModel( boundFn, input, validateWorkflowOutput.bind( null, name, outputSchema ) );
|
|
61
|
+
}
|
|
62
|
+
return boundFn( input );
|
|
63
|
+
} catch ( error ) {
|
|
64
|
+
/*
|
|
65
|
+
* Any errors in the workflow will interrupt its execution since the workflow is designed to orchestrate and
|
|
66
|
+
* IOs should be made in steps
|
|
67
|
+
*/
|
|
68
|
+
throw new ApplicationFailure( error.message, error.constructor.name );
|
|
52
69
|
}
|
|
53
70
|
};
|
|
54
71
|
|
|
55
|
-
|
|
72
|
+
setMetadata( wrapper, { name, description, inputSchema, outputSchema } );
|
|
56
73
|
return wrapper;
|
|
57
74
|
};
|
|
58
|
-
|
|
59
|
-
export async function startWorkflow( name, { input } = {} ) {
|
|
60
|
-
const { memo, workflowId, workflowType } = workflowInfo();
|
|
61
|
-
|
|
62
|
-
// Checks if current memo has rootWorkflowId, which means current execution is already a child
|
|
63
|
-
// Then it sets the memory for the child execution passing along who's the original workflow is and its type
|
|
64
|
-
const workflowMemory = memo.rootWorkflowId ?
|
|
65
|
-
{ parentWorkflowId: workflowId, rootWorkflowType: memo.rootWorkflowType, rootWorkflowId: memo.rootWorkflowId } :
|
|
66
|
-
{ parentWorkflowId: workflowId, rootWorkflowId: workflowId, rootWorkflowType: workflowType };
|
|
67
|
-
|
|
68
|
-
return executeChild( name, { args: input ? [ input ] : [], memo: workflowMemory } );
|
|
69
|
-
}
|
|
@@ -1,17 +1,28 @@
|
|
|
1
|
-
import { fetch } from 'undici';
|
|
2
1
|
import { api as apiConfig } from '#configs';
|
|
2
|
+
import { FatalError } from '#errors';
|
|
3
3
|
|
|
4
|
-
export async
|
|
5
|
-
const
|
|
4
|
+
export const sendWebhookPost = async ( { url, workflowId, payload } ) => {
|
|
5
|
+
const request = fetch( url, {
|
|
6
6
|
method: 'POST',
|
|
7
7
|
headers: {
|
|
8
8
|
'Content-Type': 'application/json',
|
|
9
9
|
Authentication: `Basic ${apiConfig.authKey}`
|
|
10
10
|
},
|
|
11
|
-
body: JSON.stringify( { workflowId, payload } )
|
|
11
|
+
body: JSON.stringify( { workflowId, payload } ),
|
|
12
|
+
signal: AbortSignal.timeout( 5000 )
|
|
12
13
|
} );
|
|
13
14
|
|
|
15
|
+
const res = await ( async () => {
|
|
16
|
+
try {
|
|
17
|
+
return await request;
|
|
18
|
+
} catch {
|
|
19
|
+
throw new FatalError( 'Webhook fail: timeout' );
|
|
20
|
+
}
|
|
21
|
+
} )();
|
|
22
|
+
|
|
14
23
|
console.log( '[Core.SendWebhookPost]', res.status, res.statusText );
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
if ( !res.ok ) {
|
|
26
|
+
throw new FatalError( `Webhook fail: ${res.status}` );
|
|
27
|
+
}
|
|
17
28
|
};
|