@protobase/workflows 0.0.0-stage → 0.68.0
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/LICENSE +202 -0
- package/dist/definition/check-definition.d.ts +21 -0
- package/dist/definition/check-definition.js +128 -0
- package/dist/definition/check-reads.js +46 -0
- package/dist/definition/check-schema.js +74 -0
- package/dist/definition/check-trigger.d.ts +7 -0
- package/dist/definition/check-trigger.js +27 -0
- package/dist/definition/expression-fields.js +68 -0
- package/dist/definition/expressions.js +35 -0
- package/dist/definition/graph.js +39 -0
- package/dist/definition/names.js +14 -0
- package/dist/definition/problem.d.ts +13 -0
- package/dist/definition/problem.js +11 -0
- package/dist/definition/reads.js +81 -0
- package/dist/definition/schema.d.ts +259 -0
- package/dist/definition/schema.js +258 -0
- package/dist/definition/types.d.ts +92 -0
- package/dist/definition/visible.js +52 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +16 -0
- package/dist/limits.d.ts +39 -0
- package/dist/limits.js +40 -0
- package/dist/nesting.d.ts +7 -0
- package/dist/nesting.js +19 -0
- package/dist/plugin/connection-error.d.ts +10 -0
- package/dist/plugin/connection-error.js +15 -0
- package/dist/plugin/context.d.ts +99 -0
- package/dist/plugin/define-plugin.d.ts +7 -0
- package/dist/plugin/define-plugin.js +23 -0
- package/dist/plugin/fetch-errors.d.ts +14 -0
- package/dist/plugin/fetch-errors.js +18 -0
- package/dist/plugin/file-value.d.ts +5 -0
- package/dist/plugin/file-value.js +9 -0
- package/dist/plugin/index.d.ts +8 -0
- package/dist/plugin/index.js +6 -0
- package/dist/plugin/load-plugins.d.ts +10 -0
- package/dist/plugin/load-plugins.js +22 -0
- package/dist/plugin/step-handlers.d.ts +12 -0
- package/dist/plugin/step-handlers.js +33 -0
- package/dist/plugin/trigger-checks.d.ts +6 -0
- package/dist/plugin/trigger-checks.js +28 -0
- package/dist/plugin/type-catalog.d.ts +19 -0
- package/dist/plugin/type-catalog.js +26 -0
- package/dist/plugin/type-list.d.ts +26 -0
- package/dist/plugin/type-list.js +26 -0
- package/dist/plugin/types.d.ts +123 -0
- package/dist/run/context.d.ts +18 -0
- package/dist/run/errors.d.ts +20 -0
- package/dist/run/errors.js +39 -0
- package/dist/run/evaluate.js +59 -0
- package/dist/run/fan-out.js +39 -0
- package/dist/run/json-bytes.js +5 -0
- package/dist/run/port.d.ts +105 -0
- package/dist/run/position.js +54 -0
- package/dist/run/recovery.js +76 -0
- package/dist/run/redact.d.ts +16 -0
- package/dist/run/redact.js +39 -0
- package/dist/run/rerun.d.ts +18 -0
- package/dist/run/rerun.js +62 -0
- package/dist/run/result-states.js +115 -0
- package/dist/run/runner.d.ts +29 -0
- package/dist/run/runner.js +60 -0
- package/dist/run/scope.js +127 -0
- package/dist/run/segment.d.ts +35 -0
- package/dist/run/segment.js +58 -0
- package/dist/run/simple-states.js +108 -0
- package/dist/run/step-input.js +22 -0
- package/dist/run/task.js +39 -0
- package/package.json +34 -4
- package/README.md +0 -3
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//#region src/definition/types.d.ts
|
|
2
|
+
/** A value written in a definition: JSON, where a string `{% ... %}` is a JSONata expression evaluated when the state runs. */
|
|
3
|
+
export type Template = unknown;
|
|
4
|
+
export type Retrier = {
|
|
5
|
+
ErrorEquals: string[];
|
|
6
|
+
IntervalSeconds?: number;
|
|
7
|
+
MaxAttempts?: number;
|
|
8
|
+
BackoffRate?: number;
|
|
9
|
+
MaxDelaySeconds?: number;
|
|
10
|
+
};
|
|
11
|
+
export type Catcher = {
|
|
12
|
+
ErrorEquals: string[];
|
|
13
|
+
Next: string;
|
|
14
|
+
Output?: Template;
|
|
15
|
+
Assign?: Record<string, Template>;
|
|
16
|
+
};
|
|
17
|
+
type Common = {
|
|
18
|
+
Comment?: string;
|
|
19
|
+
Output?: Template;
|
|
20
|
+
Assign?: Record<string, Template>;
|
|
21
|
+
QueryLanguage?: 'JSONata';
|
|
22
|
+
};
|
|
23
|
+
type Flow = {
|
|
24
|
+
Next?: string;
|
|
25
|
+
End?: boolean;
|
|
26
|
+
};
|
|
27
|
+
export type PassState = Common & Flow & {
|
|
28
|
+
Type: 'Pass';
|
|
29
|
+
};
|
|
30
|
+
export type TaskState = Common & Flow & {
|
|
31
|
+
Type: 'Task';
|
|
32
|
+
Resource: string;
|
|
33
|
+
Arguments?: Template;
|
|
34
|
+
TimeoutSeconds?: number;
|
|
35
|
+
Retry?: Retrier[];
|
|
36
|
+
Catch?: Catcher[];
|
|
37
|
+
};
|
|
38
|
+
export type ChoiceRule = {
|
|
39
|
+
Condition: string;
|
|
40
|
+
Next: string;
|
|
41
|
+
Comment?: string;
|
|
42
|
+
Output?: Template;
|
|
43
|
+
Assign?: Record<string, Template>;
|
|
44
|
+
};
|
|
45
|
+
export type ChoiceState = Common & {
|
|
46
|
+
Type: 'Choice';
|
|
47
|
+
Choices: ChoiceRule[];
|
|
48
|
+
Default?: string;
|
|
49
|
+
};
|
|
50
|
+
export type WaitState = Common & Flow & {
|
|
51
|
+
Type: 'Wait';
|
|
52
|
+
Seconds?: number | string;
|
|
53
|
+
Timestamp?: string;
|
|
54
|
+
};
|
|
55
|
+
export type SucceedState = Omit<Common, 'Assign'> & {
|
|
56
|
+
Type: 'Succeed';
|
|
57
|
+
};
|
|
58
|
+
export type FailState = Omit<Common, 'Output' | 'Assign'> & {
|
|
59
|
+
Type: 'Fail';
|
|
60
|
+
Error?: string;
|
|
61
|
+
Cause?: string;
|
|
62
|
+
};
|
|
63
|
+
export type MapState = Common & Flow & {
|
|
64
|
+
Type: 'Map';
|
|
65
|
+
Items: Template;
|
|
66
|
+
ItemProcessor: Machine;
|
|
67
|
+
MaxConcurrency?: number;
|
|
68
|
+
Retry?: Retrier[];
|
|
69
|
+
Catch?: Catcher[];
|
|
70
|
+
};
|
|
71
|
+
export type State = PassState | TaskState | ChoiceState | WaitState | SucceedState | FailState | MapState;
|
|
72
|
+
/** States and where they start: a whole state machine, or a Map's item processor. */
|
|
73
|
+
export type Machine = {
|
|
74
|
+
StartAt: string;
|
|
75
|
+
States: Record<string, State>;
|
|
76
|
+
Comment?: string;
|
|
77
|
+
};
|
|
78
|
+
/** The state machine of a workflow: the States Language with JSONata expressions, in the named-reads profile. */
|
|
79
|
+
export type StateMachine = Machine & {
|
|
80
|
+
QueryLanguage: 'JSONata';
|
|
81
|
+
};
|
|
82
|
+
/** What starts a workflow's runs: one key, `manual` or a trigger type `<plugin>:<key>`, holding its input. */
|
|
83
|
+
export type Trigger = {
|
|
84
|
+
manual?: Record<string, unknown>;
|
|
85
|
+
[type: `${string}:${string}`]: Record<string, unknown>;
|
|
86
|
+
};
|
|
87
|
+
/** A workflow definition: its trigger, and the state machine a run runs from `StartAt`. */
|
|
88
|
+
export type Definition = {
|
|
89
|
+
trigger: Trigger;
|
|
90
|
+
stateMachine: StateMachine;
|
|
91
|
+
};
|
|
92
|
+
//#endregion
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { edges } from "./graph.js";
|
|
2
|
+
//#region src/definition/visible.ts
|
|
3
|
+
const passedOn = (state) => {
|
|
4
|
+
const own = Object.keys("Assign" in state && state.Assign || {});
|
|
5
|
+
const along = [];
|
|
6
|
+
if ("Next" in state && state.Next !== void 0) along.push({
|
|
7
|
+
target: state.Next,
|
|
8
|
+
names: own
|
|
9
|
+
});
|
|
10
|
+
if (state.Type === "Choice") {
|
|
11
|
+
for (const rule of state.Choices) along.push({
|
|
12
|
+
target: rule.Next,
|
|
13
|
+
names: Object.keys(rule.Assign ?? state.Assign ?? {})
|
|
14
|
+
});
|
|
15
|
+
if (state.Default !== void 0) along.push({
|
|
16
|
+
target: state.Default,
|
|
17
|
+
names: own
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if ("Catch" in state) for (const catcher of state.Catch ?? []) along.push({
|
|
21
|
+
target: catcher.Next,
|
|
22
|
+
names: Object.keys(catcher.Assign ?? {})
|
|
23
|
+
});
|
|
24
|
+
return along;
|
|
25
|
+
};
|
|
26
|
+
const reachedFrom = (machine, start) => {
|
|
27
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28
|
+
const stack = [start];
|
|
29
|
+
while (stack.length > 0) {
|
|
30
|
+
const name = stack.pop();
|
|
31
|
+
const state = machine.States[name];
|
|
32
|
+
if (seen.has(name) || !state) continue;
|
|
33
|
+
seen.add(name);
|
|
34
|
+
stack.push(...edges(state).map((edge) => edge.target));
|
|
35
|
+
}
|
|
36
|
+
return seen;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The variables each state of a machine may read: those of the outer scopes, and each one assigned on an edge some path
|
|
40
|
+
* from `StartAt` takes before it. A state's own Assign takes effect from the next state on, and only along the edges
|
|
41
|
+
* that assign it: a Choice rule's along its Next, a catcher's along its own.
|
|
42
|
+
*/
|
|
43
|
+
const visibleVariables = (machine, outer) => {
|
|
44
|
+
const visible = new Map(Object.keys(machine.States).map((name) => [name, new Set(outer)]));
|
|
45
|
+
for (const state of Object.values(machine.States)) for (const { target, names } of passedOn(state)) {
|
|
46
|
+
if (names.length === 0) continue;
|
|
47
|
+
for (const later of reachedFrom(machine, target)) for (const variable of names) visible.get(later)?.add(variable);
|
|
48
|
+
}
|
|
49
|
+
return visible;
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
export { visibleVariables };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Problem, problemText } from "./definition/problem.js";
|
|
2
|
+
import { Catcher, ChoiceRule, ChoiceState, Definition, Machine, MapState, Retrier, State, StateMachine, TaskState, Trigger } from "./definition/types.js";
|
|
3
|
+
import { TriggerCheck } from "./definition/check-trigger.js";
|
|
4
|
+
import { CheckOptions, checkDefinition } from "./definition/check-definition.js";
|
|
5
|
+
import { definitionSchema } from "./definition/schema.js";
|
|
6
|
+
import { limits } from "./limits.js";
|
|
7
|
+
import { nestingDepth } from "./nesting.js";
|
|
8
|
+
import { ErrorOutput, StepError } from "./run/errors.js";
|
|
9
|
+
import { Redactor, redacted, secretRedactor, shortestSecret } from "./run/redact.js";
|
|
10
|
+
import { LoadedRun, RunChange, RunStatus, RunStore, SavedStep, SegmentQueue, StepLog, StepRecord, StepStatus, TaskContext, TaskHandler, TaskHandlers } from "./run/port.js";
|
|
11
|
+
import { EarlierStep, rerunSteps } from "./run/rerun.js";
|
|
12
|
+
import { Runner, RunnerPort, createRunner } from "./run/runner.js";
|
|
13
|
+
import { SegmentInput, runSegment } from "./run/segment.js";
|
|
14
|
+
import { LoadedPlugin, loadPlugins } from "./plugin/load-plugins.js";
|
|
15
|
+
import { StepWiring, stepHandlers } from "./plugin/step-handlers.js";
|
|
16
|
+
import { triggerChecks } from "./plugin/trigger-checks.js";
|
|
17
|
+
import { TypeEntry, Unavailable, typeList } from "./plugin/type-list.js";
|
|
18
|
+
import { CatalogEntry, TypeCatalog, typeCatalog } from "./plugin/type-catalog.js";
|
|
19
|
+
export { type CatalogEntry, type Catcher, type CheckOptions, type ChoiceRule, type ChoiceState, type Definition, type EarlierStep, type ErrorOutput, type LoadedPlugin, type LoadedRun, type Machine, type MapState, type Problem, type Redactor, type Retrier, type RunChange, type RunStatus, type RunStore, type Runner, type RunnerPort, type SavedStep, type SegmentInput, type SegmentQueue, type State, type StateMachine, StepError, type StepLog, type StepRecord, type StepStatus, type StepWiring, type TaskContext, type TaskHandler, type TaskHandlers, type TaskState, type Trigger, type TriggerCheck, type TypeCatalog, type TypeEntry, type Unavailable, checkDefinition, createRunner, definitionSchema, limits, loadPlugins, nestingDepth, problemText, redacted, rerunSteps, runSegment, secretRedactor, shortestSecret, stepHandlers, triggerChecks, typeCatalog, typeList };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { limits } from "./limits.js";
|
|
2
|
+
import { definitionSchema } from "./definition/schema.js";
|
|
3
|
+
import { problemText } from "./definition/problem.js";
|
|
4
|
+
import { nestingDepth } from "./nesting.js";
|
|
5
|
+
import { checkDefinition } from "./definition/check-definition.js";
|
|
6
|
+
import { StepError } from "./run/errors.js";
|
|
7
|
+
import { redacted, secretRedactor, shortestSecret } from "./run/redact.js";
|
|
8
|
+
import { rerunSteps } from "./run/rerun.js";
|
|
9
|
+
import { runSegment } from "./run/segment.js";
|
|
10
|
+
import { createRunner } from "./run/runner.js";
|
|
11
|
+
import { loadPlugins } from "./plugin/load-plugins.js";
|
|
12
|
+
import { stepHandlers } from "./plugin/step-handlers.js";
|
|
13
|
+
import { triggerChecks } from "./plugin/trigger-checks.js";
|
|
14
|
+
import { typeList } from "./plugin/type-list.js";
|
|
15
|
+
import { typeCatalog } from "./plugin/type-catalog.js";
|
|
16
|
+
export { StepError, checkDefinition, createRunner, definitionSchema, limits, loadPlugins, nestingDepth, problemText, redacted, rerunSteps, runSegment, secretRedactor, shortestSecret, stepHandlers, triggerChecks, typeCatalog, typeList };
|
package/dist/limits.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/limits.d.ts
|
|
2
|
+
/** The fixed limits of workflows, checked when a definition is saved or while it runs; each fails closed. */
|
|
3
|
+
export declare const limits: {
|
|
4
|
+
/** A definition's size as JSON, and the length of one expression's source. */
|
|
5
|
+
readonly definitionBytes: number;
|
|
6
|
+
/** How deeply a definition's arrays and objects nest, so no walk of it overflows the call stack. */
|
|
7
|
+
readonly definitionDepth: 256;
|
|
8
|
+
readonly expressionLength: 4096;
|
|
9
|
+
/** States in one definition, the ones inside Maps counted. */
|
|
10
|
+
readonly states: 100;
|
|
11
|
+
/** The longest a Task may run before it fails with `States.Timeout`, and its default. */
|
|
12
|
+
readonly taskTimeoutSeconds: 300;
|
|
13
|
+
readonly defaultTaskTimeoutSeconds: 300;
|
|
14
|
+
/** The longest wait one Wait state may give in seconds: 366 days. */
|
|
15
|
+
readonly waitSeconds: number;
|
|
16
|
+
/** Retries of one retrier after the first try, and the longest wait between two. */
|
|
17
|
+
readonly retryAttempts: 19;
|
|
18
|
+
readonly retryDelaySeconds: number;
|
|
19
|
+
/** Items a Map runs at once; 0 or none means this limit. */
|
|
20
|
+
readonly mapConcurrency: 20;
|
|
21
|
+
/** Items one Map takes. */
|
|
22
|
+
readonly mapItems: 10000;
|
|
23
|
+
/** Steps one run may take, every attempt and every Map item's steps counted. */
|
|
24
|
+
readonly runSteps: 50000;
|
|
25
|
+
/** The size of each value a step saves (its input, output and variables), as JSON. */
|
|
26
|
+
readonly valueBytes: number;
|
|
27
|
+
/** Log lines one step keeps, and the length of each. */
|
|
28
|
+
readonly logLines: 100;
|
|
29
|
+
readonly logLineLength: 1000;
|
|
30
|
+
/** How long one expression may evaluate, and how deep it may nest. */
|
|
31
|
+
readonly expressionMs: 1000;
|
|
32
|
+
readonly expressionDepth: 300;
|
|
33
|
+
/** How long one segment runs steps before it hands the rest of the run to the next segment. */
|
|
34
|
+
readonly segmentMs: 60000;
|
|
35
|
+
/** A run's input: its size as JSON and how deeply its arrays and objects nest. */
|
|
36
|
+
readonly inputBytes: number;
|
|
37
|
+
readonly inputDepth: 64;
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
package/dist/limits.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/limits.ts
|
|
2
|
+
/** The fixed limits of workflows, checked when a definition is saved or while it runs; each fails closed. */
|
|
3
|
+
const limits = {
|
|
4
|
+
/** A definition's size as JSON, and the length of one expression's source. */
|
|
5
|
+
definitionBytes: 262144,
|
|
6
|
+
/** How deeply a definition's arrays and objects nest, so no walk of it overflows the call stack. */
|
|
7
|
+
definitionDepth: 256,
|
|
8
|
+
expressionLength: 4096,
|
|
9
|
+
/** States in one definition, the ones inside Maps counted. */
|
|
10
|
+
states: 100,
|
|
11
|
+
/** The longest a Task may run before it fails with `States.Timeout`, and its default. */
|
|
12
|
+
taskTimeoutSeconds: 300,
|
|
13
|
+
defaultTaskTimeoutSeconds: 300,
|
|
14
|
+
/** The longest wait one Wait state may give in seconds: 366 days. */
|
|
15
|
+
waitSeconds: 31622400,
|
|
16
|
+
/** Retries of one retrier after the first try, and the longest wait between two. */
|
|
17
|
+
retryAttempts: 19,
|
|
18
|
+
retryDelaySeconds: 86400,
|
|
19
|
+
/** Items a Map runs at once; 0 or none means this limit. */
|
|
20
|
+
mapConcurrency: 20,
|
|
21
|
+
/** Items one Map takes. */
|
|
22
|
+
mapItems: 1e4,
|
|
23
|
+
/** Steps one run may take, every attempt and every Map item's steps counted. */
|
|
24
|
+
runSteps: 5e4,
|
|
25
|
+
/** The size of each value a step saves (its input, output and variables), as JSON. */
|
|
26
|
+
valueBytes: 1048576,
|
|
27
|
+
/** Log lines one step keeps, and the length of each. */
|
|
28
|
+
logLines: 100,
|
|
29
|
+
logLineLength: 1e3,
|
|
30
|
+
/** How long one expression may evaluate, and how deep it may nest. */
|
|
31
|
+
expressionMs: 1e3,
|
|
32
|
+
expressionDepth: 300,
|
|
33
|
+
/** How long one segment runs steps before it hands the rest of the run to the next segment. */
|
|
34
|
+
segmentMs: 6e4,
|
|
35
|
+
/** A run's input: its size as JSON and how deeply its arrays and objects nest. */
|
|
36
|
+
inputBytes: 1048576,
|
|
37
|
+
inputDepth: 64
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { limits };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/nesting.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* How deeply a value's arrays and objects nest, walked with a stack of its own so no value can overflow the call stack;
|
|
4
|
+
* it stops counting once past `most`, returning `most + 1`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const nestingDepth: (value: unknown, most: number) => number;
|
|
7
|
+
//#endregion
|
package/dist/nesting.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/nesting.ts
|
|
2
|
+
/**
|
|
3
|
+
* How deeply a value's arrays and objects nest, walked with a stack of its own so no value can overflow the call stack;
|
|
4
|
+
* it stops counting once past `most`, returning `most + 1`.
|
|
5
|
+
*/
|
|
6
|
+
const nestingDepth = (value, most) => {
|
|
7
|
+
let deepest = 0;
|
|
8
|
+
const stack = [[value, 0]];
|
|
9
|
+
while (stack.length > 0) {
|
|
10
|
+
const [item, depth] = stack.pop();
|
|
11
|
+
if (typeof item !== "object" || item === null) continue;
|
|
12
|
+
if (depth + 1 > most) return most + 1;
|
|
13
|
+
deepest = Math.max(deepest, depth + 1);
|
|
14
|
+
for (const child of Object.values(item)) stack.push([child, depth + 1]);
|
|
15
|
+
}
|
|
16
|
+
return deepest;
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { nestingDepth };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/plugin/connection-error.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Why a connection gave no token: `not_connected` when the workflow has no sign-in for it, `link_expired` when the
|
|
4
|
+
* provider refused it, so its owner must sign in again. A trigger that throws it waits for that sign-in.
|
|
5
|
+
*/
|
|
6
|
+
export declare class ConnectionError extends Error {
|
|
7
|
+
readonly code: 'not_connected' | 'link_expired';
|
|
8
|
+
constructor(code: 'not_connected' | 'link_expired', message: string);
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/plugin/connection-error.ts
|
|
2
|
+
/**
|
|
3
|
+
* Why a connection gave no token: `not_connected` when the workflow has no sign-in for it, `link_expired` when the
|
|
4
|
+
* provider refused it, so its owner must sign in again. A trigger that throws it waits for that sign-in.
|
|
5
|
+
*/
|
|
6
|
+
var ConnectionError = class extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.name = "ConnectionError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { ConnectionError };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//#region src/plugin/context.d.ts
|
|
2
|
+
/** A JSON Schema (draft-07), as plain data: what a step or trigger takes and gives. */
|
|
3
|
+
export type JsonSchema = Record<string, unknown>;
|
|
4
|
+
/** What `fetch` takes: a request's method, headers and body, and the most of the answer's body it reads. */
|
|
5
|
+
export type PluginFetchInit = {
|
|
6
|
+
method?: string;
|
|
7
|
+
headers?: HeadersInit;
|
|
8
|
+
body?: string | Uint8Array;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
/** The most bytes of the answer's body read off the wire; a longer body fails. Default and maximum 10 MiB. */
|
|
11
|
+
maxBytes?: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The only way a plugin sends a request: it resolves the host once, refuses a private, loopback or link-local address
|
|
15
|
+
* (unless the app allows them), connects to the address it checked, follows no redirect and reads at most `maxBytes`
|
|
16
|
+
* of the body as sent, never decoding a `Content-Encoding`. The answer's body is already read.
|
|
17
|
+
*/
|
|
18
|
+
export type PluginFetch = (url: string, init?: PluginFetchInit) => Promise<Response>;
|
|
19
|
+
/** A file of the run: the handle the API gives for a file field. */
|
|
20
|
+
export type FileValue = {
|
|
21
|
+
uri: string;
|
|
22
|
+
name: string;
|
|
23
|
+
type: string;
|
|
24
|
+
size: number;
|
|
25
|
+
};
|
|
26
|
+
/** The run's own files: a step keeps what it downloaded and reads what an earlier step kept, never other files. */
|
|
27
|
+
export type PluginFiles = {
|
|
28
|
+
keep(bytes: Uint8Array, name: string, type: string): Promise<FileValue>;
|
|
29
|
+
/** The bytes of a file of this run; `field` names the argument in the error when it is not one. */
|
|
30
|
+
read(value: unknown, field: string): Promise<{
|
|
31
|
+
name: string;
|
|
32
|
+
type?: string;
|
|
33
|
+
bytes: Uint8Array;
|
|
34
|
+
}>;
|
|
35
|
+
};
|
|
36
|
+
type Key = string | number | (string | number)[];
|
|
37
|
+
type Record_ = Record<string, unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* The app's records, read and written as the user who saved the version the run runs, with the roles they hold now,
|
|
40
|
+
* through the API: its access rules, tenant scope, validation and write hooks apply. A refusal fails the step with
|
|
41
|
+
* `Records.Refused`. A create in a retry of a step after its records committed creates none again and returns them.
|
|
42
|
+
*/
|
|
43
|
+
export type PluginRecords = {
|
|
44
|
+
create(resource: string, data: Record_): Promise<Record_>;
|
|
45
|
+
/** Several records in one batch, all or none. */
|
|
46
|
+
createMany(resource: string, data: Record_[]): Promise<Record_[]>;
|
|
47
|
+
update(resource: string, key: Key, data: Record_): Promise<Record_>;
|
|
48
|
+
get(resource: string, key: Key): Promise<Record_>;
|
|
49
|
+
list(resource: string, query: {
|
|
50
|
+
filter?: string;
|
|
51
|
+
order_by?: string;
|
|
52
|
+
limit?: number;
|
|
53
|
+
}): Promise<Record_[]>;
|
|
54
|
+
};
|
|
55
|
+
/** One message from the app's own sender. */
|
|
56
|
+
export type PluginMailMessage = {
|
|
57
|
+
to: string;
|
|
58
|
+
subject: string;
|
|
59
|
+
text: string;
|
|
60
|
+
html?: string;
|
|
61
|
+
headers?: Record<string, string>;
|
|
62
|
+
attachments?: {
|
|
63
|
+
filename: string;
|
|
64
|
+
content: Uint8Array;
|
|
65
|
+
contentType?: string;
|
|
66
|
+
}[];
|
|
67
|
+
};
|
|
68
|
+
export type PluginMail = {
|
|
69
|
+
send(message: PluginMailMessage): Promise<void>;
|
|
70
|
+
};
|
|
71
|
+
/** The sign-in a step or trigger declared, bound by its workflow; it names no other. */
|
|
72
|
+
export type PluginConnection = {
|
|
73
|
+
/** A current access token; throws `ConnectionError` when the workflow has no working sign-in for it. */
|
|
74
|
+
accessToken(): Promise<string>;
|
|
75
|
+
};
|
|
76
|
+
/** What the app gives every hook of a plugin: its guarded requests, records, files and mail. */
|
|
77
|
+
export type PluginServices = {
|
|
78
|
+
fetch: PluginFetch;
|
|
79
|
+
records: PluginRecords;
|
|
80
|
+
files: PluginFiles;
|
|
81
|
+
/** Undefined when the app sends no mail. */
|
|
82
|
+
mail: PluginMail | undefined;
|
|
83
|
+
};
|
|
84
|
+
/** What a step's `run` gets: its checked input, the app's services, and the step's own run details. */
|
|
85
|
+
export type StepContext<Input = Record<string, unknown>> = PluginServices & {
|
|
86
|
+
input: Input;
|
|
87
|
+
runId: string;
|
|
88
|
+
/** The same for every retry of one visit of this step in this run; a later visit of the step, such as in a loop, gets a new one. */
|
|
89
|
+
idempotencyKey: string;
|
|
90
|
+
/** Aborted at the step's timeout, or when the worker stops. */
|
|
91
|
+
signal: AbortSignal;
|
|
92
|
+
/** Adds a line to the step's logs, secrets blanked out. */
|
|
93
|
+
log: (message: string) => void;
|
|
94
|
+
/** The text with the app's secrets blanked out, for a step that cuts text short. */
|
|
95
|
+
redact: (text: string) => string;
|
|
96
|
+
/** Undefined unless the step declares a `connection`. */
|
|
97
|
+
connection: PluginConnection | undefined;
|
|
98
|
+
};
|
|
99
|
+
//#endregion
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Plugin } from "./types.js";
|
|
2
|
+
//#region src/plugin/define-plugin.d.ts
|
|
3
|
+
/** The version of this interface: releases add optional keys to it and never break it. */
|
|
4
|
+
export declare const pluginApi = 1;
|
|
5
|
+
/** Declares a workflow plugin, checking its names: a type is `<name>:<key>`, such as `app:http` or `acme:new_order`. */
|
|
6
|
+
export declare const definePlugin: <const P extends Plugin>(plugin: P) => P;
|
|
7
|
+
//#endregion
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/plugin/define-plugin.ts
|
|
2
|
+
/** The version of this interface: releases add optional keys to it and never break it. */
|
|
3
|
+
const pluginApi = 1;
|
|
4
|
+
const pluginName = /^[a-z][a-z0-9_]*$/;
|
|
5
|
+
const typeKey = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$/;
|
|
6
|
+
/** Throws when `plugin` breaks a naming rule, each error starting with `where`. */
|
|
7
|
+
const checkPlugin = (plugin, where = `Workflow plugin "${plugin.name}"`) => {
|
|
8
|
+
if (!pluginName.test(plugin.name)) throw new Error(`${where}: a plugin's name is lowercase letters, digits and "_"`);
|
|
9
|
+
const invalid = [...Object.keys(plugin.triggers ?? {}), ...Object.keys(plugin.steps ?? {})].find((key) => !typeKey.test(key));
|
|
10
|
+
if (invalid !== void 0) throw new Error(`${where}: "${invalid}" is not a type key; use lowercase words joined by "_" or "."`);
|
|
11
|
+
const shared = Object.keys(plugin.steps ?? {}).find((key) => Object.hasOwn(plugin.triggers ?? {}, key));
|
|
12
|
+
if (shared !== void 0) throw new Error(`${where}: "${shared}" is both a step and a trigger; give each type its own key`);
|
|
13
|
+
const connections = Object.keys(plugin.connections ?? {});
|
|
14
|
+
if (connections.includes("")) throw new Error(`${where}: a connection's name is not empty`);
|
|
15
|
+
for (const [key, type] of [...Object.entries(plugin.triggers ?? {}), ...Object.entries(plugin.steps ?? {})]) if (type.connection !== void 0 && !connections.includes(type.connection)) throw new Error(`${where}: ${key} uses the connection "${type.connection}", which the plugin does not declare`);
|
|
16
|
+
};
|
|
17
|
+
/** Declares a workflow plugin, checking its names: a type is `<name>:<key>`, such as `app:http` or `acme:new_order`. */
|
|
18
|
+
const definePlugin = (plugin) => {
|
|
19
|
+
checkPlugin(plugin);
|
|
20
|
+
return plugin;
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { checkPlugin, definePlugin, pluginApi };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/plugin/fetch-errors.d.ts
|
|
2
|
+
/** `fetch` refused the URL before sending: its scheme, or a host that is not a public address. Names the host only. */
|
|
3
|
+
export declare class AddressRefusedError extends Error {
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
/** The answer's body is longer than the `maxBytes` `fetch` reads. */
|
|
7
|
+
export declare class ResponseTooLargeError extends Error {
|
|
8
|
+
readonly maxBytes: number;
|
|
9
|
+
name: string;
|
|
10
|
+
constructor(maxBytes: number);
|
|
11
|
+
}
|
|
12
|
+
/** The most a plugin's `fetch` reads of a body, and its default. */
|
|
13
|
+
export declare const fetchMaxBytes: number;
|
|
14
|
+
//#endregion
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/plugin/fetch-errors.ts
|
|
2
|
+
/** `fetch` refused the URL before sending: its scheme, or a host that is not a public address. Names the host only. */
|
|
3
|
+
var AddressRefusedError = class extends Error {
|
|
4
|
+
name = "AddressRefusedError";
|
|
5
|
+
};
|
|
6
|
+
/** The answer's body is longer than the `maxBytes` `fetch` reads. */
|
|
7
|
+
var ResponseTooLargeError = class extends Error {
|
|
8
|
+
maxBytes;
|
|
9
|
+
name = "ResponseTooLargeError";
|
|
10
|
+
constructor(maxBytes) {
|
|
11
|
+
super(`The answer's body is longer than ${maxBytes} bytes`);
|
|
12
|
+
this.maxBytes = maxBytes;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/** The most a plugin's `fetch` reads of a body, and its default. */
|
|
16
|
+
const fetchMaxBytes = 10485760;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { AddressRefusedError, ResponseTooLargeError, fetchMaxBytes };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { FileValue } from "./context.js";
|
|
2
|
+
//#region src/plugin/file-value.d.ts
|
|
3
|
+
/** Whether a value has a file value's shape, all four fields; an object with only a `uri` is other data. */
|
|
4
|
+
export declare const isFileValue: (value: unknown) => value is FileValue;
|
|
5
|
+
//#endregion
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/plugin/file-value.ts
|
|
2
|
+
/** Whether a value has a file value's shape, all four fields; an object with only a `uri` is other data. */
|
|
3
|
+
const isFileValue = (value) => {
|
|
4
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
5
|
+
const { uri, name, type, size } = value;
|
|
6
|
+
return typeof uri === "string" && typeof name === "string" && typeof type === "string" && typeof size === "number";
|
|
7
|
+
};
|
|
8
|
+
//#endregion
|
|
9
|
+
export { isFileValue };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { StepError } from "../run/errors.js";
|
|
2
|
+
import { FileValue, JsonSchema, PluginConnection, PluginFetch, PluginFetchInit, PluginFiles, PluginMail, PluginMailMessage, PluginRecords, PluginServices, StepContext } from "./context.js";
|
|
3
|
+
import { ConnectionDeclaration, Plugin, PollingTrigger, ReadyTrigger, StepType, TriggerContext, TriggerItem, TriggerType, WebhookTrigger } from "./types.js";
|
|
4
|
+
import { ConnectionError } from "./connection-error.js";
|
|
5
|
+
import { definePlugin, pluginApi } from "./define-plugin.js";
|
|
6
|
+
import { AddressRefusedError, ResponseTooLargeError, fetchMaxBytes } from "./fetch-errors.js";
|
|
7
|
+
import { isFileValue } from "./file-value.js";
|
|
8
|
+
export { AddressRefusedError, type ConnectionDeclaration, ConnectionError, type FileValue, type JsonSchema, type Plugin, type PluginConnection, type PluginFetch, type PluginFetchInit, type PluginFiles, type PluginMail, type PluginMailMessage, type PluginRecords, type PluginServices, type PollingTrigger, type ReadyTrigger, ResponseTooLargeError, type StepContext, StepError, type StepType, type TriggerContext, type TriggerItem, type TriggerType, type WebhookTrigger, definePlugin, fetchMaxBytes, isFileValue, pluginApi };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { StepError } from "../run/errors.js";
|
|
2
|
+
import { definePlugin, pluginApi } from "./define-plugin.js";
|
|
3
|
+
import { ConnectionError } from "./connection-error.js";
|
|
4
|
+
import { AddressRefusedError, ResponseTooLargeError, fetchMaxBytes } from "./fetch-errors.js";
|
|
5
|
+
import { isFileValue } from "./file-value.js";
|
|
6
|
+
export { AddressRefusedError, ConnectionError, ResponseTooLargeError, StepError, definePlugin, fetchMaxBytes, isFileValue, pluginApi };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Plugin } from "./types.js";
|
|
2
|
+
//#region src/plugin/load-plugins.d.ts
|
|
3
|
+
/** A plugin and where it came from, such as a package name or a file, which an error names. */
|
|
4
|
+
export type LoadedPlugin = {
|
|
5
|
+
plugin: Plugin;
|
|
6
|
+
source: string;
|
|
7
|
+
};
|
|
8
|
+
/** The plugins an app runs, each checked as definePlugin checks it, refusing two of one name, which would give two types one id. */
|
|
9
|
+
export declare const loadPlugins: (loaded: readonly LoadedPlugin[]) => Plugin[];
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { checkPlugin } from "./define-plugin.js";
|
|
2
|
+
//#region src/plugin/load-plugins.ts
|
|
3
|
+
const record = (value) => value === void 0 || typeof value === "object" && value !== null;
|
|
4
|
+
const isPlugin = (value) => {
|
|
5
|
+
if (typeof value !== "object" || value === null) return false;
|
|
6
|
+
const plugin = value;
|
|
7
|
+
return typeof plugin.name === "string" && typeof plugin.label === "string" && record(plugin.steps) && record(plugin.triggers) && record(plugin.connections);
|
|
8
|
+
};
|
|
9
|
+
/** The plugins an app runs, each checked as definePlugin checks it, refusing two of one name, which would give two types one id. */
|
|
10
|
+
const loadPlugins = (loaded) => {
|
|
11
|
+
const seen = /* @__PURE__ */ new Map();
|
|
12
|
+
for (const { plugin, source } of loaded) {
|
|
13
|
+
if (!isPlugin(plugin)) throw new Error(`${source} does not export a workflow plugin as its default; export default definePlugin({ ... })`);
|
|
14
|
+
checkPlugin(plugin, `Workflow plugin "${plugin.name}" of ${source}`);
|
|
15
|
+
const first = seen.get(plugin.name);
|
|
16
|
+
if (first !== void 0) throw new Error(`Two workflow plugins are named "${plugin.name}": ${first} and ${source}`);
|
|
17
|
+
seen.set(plugin.name, source);
|
|
18
|
+
}
|
|
19
|
+
return loaded.map(({ plugin }) => plugin);
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { loadPlugins };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TaskContext, TaskHandlers } from "../run/port.js";
|
|
2
|
+
import { PluginConnection, PluginServices } from "./context.js";
|
|
3
|
+
import { Plugin } from "./types.js";
|
|
4
|
+
//#region src/plugin/step-handlers.d.ts
|
|
5
|
+
/** What the app gives each step: its services for this run, and the connection a step declared. */
|
|
6
|
+
export type StepWiring = {
|
|
7
|
+
services: (task: TaskContext) => PluginServices;
|
|
8
|
+
connection: (stepId: string, name: string, task: TaskContext) => PluginConnection;
|
|
9
|
+
};
|
|
10
|
+
/** The runner's step types from the plugins' steps, each checking its input against its schema before it runs. */
|
|
11
|
+
export declare const stepHandlers: (plugins: readonly Plugin[], wiring: StepWiring) => TaskHandlers;
|
|
12
|
+
//#endregion
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { StepError } from "../run/errors.js";
|
|
2
|
+
import { Ajv } from "ajv";
|
|
3
|
+
//#region src/plugin/step-handlers.ts
|
|
4
|
+
const ajv = new Ajv({
|
|
5
|
+
allErrors: true,
|
|
6
|
+
strict: false
|
|
7
|
+
});
|
|
8
|
+
const handler = (id, type, wiring) => {
|
|
9
|
+
const validate = ajv.compile(type.input);
|
|
10
|
+
const prefix = type.errorPrefix ?? "Step";
|
|
11
|
+
return async (input, task) => {
|
|
12
|
+
if (!validate(input)) {
|
|
13
|
+
const problems = (validate.errors ?? []).map((error) => `${error.instancePath.slice(1).replaceAll("/", ".") || "Arguments"} ${error.message}`);
|
|
14
|
+
throw new StepError(`${prefix}.InvalidArguments`, problems.join("; "));
|
|
15
|
+
}
|
|
16
|
+
const connection = type.connection === void 0 ? void 0 : wiring.connection(id, type.connection, task);
|
|
17
|
+
const { runId, idempotencyKey, signal, log, redact } = task;
|
|
18
|
+
return type.run({
|
|
19
|
+
...wiring.services(task),
|
|
20
|
+
input,
|
|
21
|
+
runId,
|
|
22
|
+
idempotencyKey,
|
|
23
|
+
signal,
|
|
24
|
+
log,
|
|
25
|
+
redact,
|
|
26
|
+
connection
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
/** The runner's step types from the plugins' steps, each checking its input against its schema before it runs. */
|
|
31
|
+
const stepHandlers = (plugins, wiring) => Object.fromEntries(plugins.flatMap((plugin) => Object.entries(plugin.steps ?? {}).map(([key, type]) => [`${plugin.name}:${key}`, handler(`${plugin.name}:${key}`, type, wiring)])));
|
|
32
|
+
//#endregion
|
|
33
|
+
export { stepHandlers };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { TriggerCheck } from "../definition/check-trigger.js";
|
|
2
|
+
import { Plugin } from "./types.js";
|
|
3
|
+
//#region src/plugin/trigger-checks.d.ts
|
|
4
|
+
/** Each trigger type of `plugins` by id, with the check of its input a save runs: its schema, then its own check. */
|
|
5
|
+
export declare const triggerChecks: (plugins: readonly Plugin[]) => Record<string, TriggerCheck>;
|
|
6
|
+
//#endregion
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Ajv } from "ajv";
|
|
2
|
+
//#region src/plugin/trigger-checks.ts
|
|
3
|
+
const ajv = new Ajv({
|
|
4
|
+
allErrors: true,
|
|
5
|
+
strict: false
|
|
6
|
+
});
|
|
7
|
+
const problem = (error) => {
|
|
8
|
+
const path = error.instancePath.slice(1).split("/").filter(Boolean);
|
|
9
|
+
if (error.keyword === "required") return {
|
|
10
|
+
field: [...path, String(error.params.missingProperty)].join("."),
|
|
11
|
+
message: "is missing"
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
...path.length > 0 && { field: path.join(".") },
|
|
15
|
+
message: error.message ?? "is not valid"
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
/** Each trigger type of `plugins` by id, with the check of its input a save runs: its schema, then its own check. */
|
|
19
|
+
const triggerChecks = (plugins) => Object.fromEntries(plugins.flatMap((plugin) => Object.entries(plugin.triggers ?? {}).map(([key, type]) => {
|
|
20
|
+
const validate = ajv.compile(type.input);
|
|
21
|
+
const check = (input) => {
|
|
22
|
+
if (!validate(input)) return (validate.errors ?? []).map(problem);
|
|
23
|
+
return type.check?.(input) ?? [];
|
|
24
|
+
};
|
|
25
|
+
return [`${plugin.name}:${key}`, check];
|
|
26
|
+
})));
|
|
27
|
+
//#endregion
|
|
28
|
+
export { triggerChecks };
|