@output.ai/core 0.0.8 → 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 +6 -3
- package/src/consts.js +1 -0
- package/src/errors.js +11 -0
- package/src/interface/step.js +16 -2
- 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 +2 -0
- package/src/interface/workflow.js +42 -17
- package/src/worker/index.js +6 -2
- package/src/worker/interceptors/activity.js +3 -2
- package/src/worker/internal_utils.js +9 -3
- 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 +24 -8
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +3 -1
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +9 -3
- package/src/worker/webpack_loaders/workflow_rewriter/tools.js +30 -10
package/README.md
CHANGED
|
@@ -1,64 +1,100 @@
|
|
|
1
1
|
# Core
|
|
2
2
|
|
|
3
|
-
Provides tools to run a workflow, which is a well defined logical unit of work.
|
|
3
|
+
Provides tools to develop and run a workflow, which is a well defined logical unit of work.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Structure
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Workflows are defined using core functions "workflow" and "step", separate files:
|
|
8
8
|
|
|
9
|
-
```
|
|
10
|
-
|
|
9
|
+
```
|
|
10
|
+
└ workflows
|
|
11
|
+
└ example
|
|
12
|
+
├ workflow.ts|js <- workflow entry point
|
|
13
|
+
├ steps.ts|js <- file with each step of this workflow
|
|
14
|
+
└ prompt.prompt <- a prompt file
|
|
15
|
+
└ other-example
|
|
11
16
|
|
|
12
|
-
export const aSingleStep = step( {
|
|
13
|
-
name: 'aSingleStep',
|
|
14
|
-
fn: async (): Promise<string> => {
|
|
15
|
-
// do stuff
|
|
16
|
-
}
|
|
17
|
-
} );
|
|
18
17
|
```
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
Think that workflows is the orchestrator and steps are executors. So the workflow only call the steps and the steps call the IO operations, like APIs, DBs, LLMs, etc.
|
|
21
20
|
|
|
22
|
-
##
|
|
21
|
+
## Workflow code
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
### workflow.js
|
|
25
24
|
|
|
26
|
-
```
|
|
27
|
-
import { workflow } from '
|
|
28
|
-
import
|
|
29
|
-
import type { PromptWorkflowInput, PromptWorkflowOutput } from './types';
|
|
25
|
+
```js
|
|
26
|
+
import { workflow } from '@output.ai/workflow';
|
|
27
|
+
import { guessByName } from './steps.js';
|
|
30
28
|
|
|
31
29
|
export default workflow( {
|
|
32
|
-
name: '
|
|
33
|
-
description: '
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
name: 'guessMyProfession',
|
|
31
|
+
description: 'Guess a person profession by its name',
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
required: [ 'name' ],
|
|
35
|
+
properties: {
|
|
36
|
+
name: { type: 'string'}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
outputSchema: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
required: [ 'profession' ],
|
|
42
|
+
properties: {
|
|
43
|
+
profession: { type: 'string'}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
fn: async input => {
|
|
47
|
+
const profession = await guessByName( input.name );
|
|
48
|
+
return { profession };
|
|
38
49
|
}
|
|
39
|
-
}
|
|
50
|
+
})
|
|
40
51
|
```
|
|
41
52
|
|
|
42
|
-
|
|
53
|
+
### steps.js
|
|
43
54
|
|
|
44
|
-
|
|
55
|
+
```js
|
|
56
|
+
import { api } from './api.js'
|
|
45
57
|
|
|
46
|
-
|
|
58
|
+
export const guessByName = step( {
|
|
59
|
+
name: 'guessByName',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'string'
|
|
62
|
+
},
|
|
63
|
+
outputSchema: {
|
|
64
|
+
type: 'string'
|
|
65
|
+
},
|
|
66
|
+
fn: async name => {
|
|
67
|
+
const res = await api.consumer( name );
|
|
68
|
+
return res.body;
|
|
69
|
+
}
|
|
70
|
+
} )
|
|
71
|
+
```
|
|
47
72
|
|
|
48
|
-
|
|
73
|
+
## webhooks
|
|
49
74
|
|
|
50
|
-
|
|
75
|
+
workflows can call webhooks that will stop their execution until an answer is given back.
|
|
51
76
|
|
|
52
77
|
```js
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
78
|
+
import { workflow, createWebhook } from '@output.ai/workflow';
|
|
79
|
+
import { guessByName } from './steps.js';
|
|
80
|
+
|
|
81
|
+
export default workflow( {
|
|
82
|
+
...
|
|
83
|
+
fn: async input => {
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
const result = await createWebhook( {
|
|
87
|
+
url: 'http://xxx.xxx/feedback',
|
|
88
|
+
payload: {
|
|
89
|
+
progressSoFar: 'plenty'
|
|
90
|
+
}
|
|
91
|
+
} );
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
})
|
|
59
95
|
```
|
|
60
96
|
|
|
61
|
-
|
|
97
|
+
The url of the example will receive the payload, plus the workflowId:
|
|
62
98
|
|
|
63
99
|
```js
|
|
64
100
|
{
|
|
@@ -67,38 +103,28 @@ the url will receive the following payload:
|
|
|
67
103
|
}
|
|
68
104
|
```
|
|
69
105
|
|
|
70
|
-
To resume the workflow, a POST has to be made
|
|
106
|
+
To resume the workflow, a POST has to be made with a response payload and the workflowId.
|
|
71
107
|
|
|
72
|
-
|
|
108
|
+
- Production: `https://output-api-production.onrender.com/workflow/feedback`
|
|
109
|
+
- Staging: `https://output-api-staging.onrender.com/workflow/feedback`
|
|
110
|
+
- Local: `http://localhost:3001/workflow/feedback`
|
|
73
111
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
112
|
+
Example:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
POST http://locahost:3001/workflow/feedback
|
|
77
116
|
{
|
|
78
117
|
workflowId,
|
|
79
118
|
payload: {}
|
|
80
119
|
}
|
|
81
120
|
```
|
|
82
121
|
|
|
83
|
-
##
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
└ workflows
|
|
87
|
-
└ my workflow
|
|
88
|
-
├ index.ts // contains the workflow()
|
|
89
|
-
├ types.ts // contains input, output types, and other misc types
|
|
90
|
-
└ steps.ts // contains all steps
|
|
91
|
-
```
|
|
122
|
+
## Developing
|
|
92
123
|
|
|
93
|
-
|
|
124
|
+
To develop workflows you need the code, which will be called the worker, the API and the engine (Temporal).
|
|
94
125
|
|
|
95
|
-
|
|
126
|
+
After having the API and the engine running, to start the worker just run:
|
|
96
127
|
|
|
97
|
-
_package.json_
|
|
98
128
|
```js
|
|
99
|
-
|
|
100
|
-
"scripts": {
|
|
101
|
-
"start": "npx flow-worker"
|
|
102
|
-
},
|
|
103
|
-
...
|
|
129
|
+
`outputai`
|
|
104
130
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@output.ai/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "The core module of the output framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"worker": "node ./src/worker/index.js"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
|
-
"flow-worker": "./bin/worker.sh"
|
|
16
|
+
"flow-worker": "./bin/worker.sh",
|
|
17
|
+
"outputai": "./bin/worker.sh"
|
|
17
18
|
},
|
|
18
19
|
"repository": {
|
|
19
20
|
"type": "git",
|
|
@@ -25,7 +26,9 @@
|
|
|
25
26
|
"@babel/traverse": "7.25.9",
|
|
26
27
|
"@babel/types": "7.26.7",
|
|
27
28
|
"@temporalio/worker": "1.13.0",
|
|
28
|
-
"@temporalio/workflow": "1.13.0"
|
|
29
|
+
"@temporalio/workflow": "1.13.0",
|
|
30
|
+
"ajv": "8.17.1",
|
|
31
|
+
"zod": "4.1.9"
|
|
29
32
|
},
|
|
30
33
|
"imports": {
|
|
31
34
|
"#consts": "./src/consts.js",
|
package/src/consts.js
CHANGED
package/src/errors.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* These are errors exposed as tools for the user to break their flow
|
|
3
|
+
* They work in both steps and workflows
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Any generic fatal errors
|
|
8
|
+
*/
|
|
1
9
|
export class FatalError extends Error { }
|
|
2
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Any validation error
|
|
13
|
+
*/
|
|
3
14
|
export class ValidationError extends Error { }
|
package/src/interface/step.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { setMetadata } from './metadata.js';
|
|
2
|
+
import { validateStep } from './validations/static.js';
|
|
3
|
+
import { validateStepInput, validateStepOutput } from './validations/runtime.js';
|
|
4
|
+
import { invokeFnAndValidateOutputPreservingExecutionModel } from './utils.js';
|
|
2
5
|
|
|
3
6
|
export function step( { name, description, inputSchema, outputSchema, fn } ) {
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
validateStep( { name, description, inputSchema, outputSchema, fn } );
|
|
8
|
+
const wrapper = input => {
|
|
9
|
+
if ( inputSchema ) {
|
|
10
|
+
validateStepInput( name, inputSchema, input );
|
|
11
|
+
}
|
|
12
|
+
if ( !outputSchema ) {
|
|
13
|
+
return fn( input );
|
|
14
|
+
}
|
|
15
|
+
return invokeFnAndValidateOutputPreservingExecutionModel( fn, input, validateStepOutput.bind( null, name, outputSchema ) );
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
setMetadata( wrapper, { name, description, inputSchema, outputSchema } );
|
|
19
|
+
return wrapper;
|
|
6
20
|
};
|
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
|
+
};
|