@runium/core 0.0.9 → 0.1.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/index.js +594 -0
- package/package.json +11 -23
- package/.eslintrc.json +0 -30
- package/.prettierrc.json +0 -10
- package/README.md +0 -1
- package/src/error.ts +0 -22
- package/src/file.ts +0 -53
- package/src/index.ts +0 -8
- package/src/macros.ts +0 -91
- package/src/project-config.ts +0 -483
- package/src/project-schema.ts +0 -767
- package/src/project.ts +0 -748
- package/src/task.ts +0 -381
- package/src/trigger.ts +0 -170
- package/tsconfig.json +0 -29
- package/types/index.d.ts +0 -510
- package/types/package.json +0 -22
- package/vite.config.ts +0 -123
package/.prettierrc.json
DELETED
package/README.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# @runium/core
|
package/src/error.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runium error class
|
|
3
|
-
*/
|
|
4
|
-
export class RuniumError extends Error {
|
|
5
|
-
code: string;
|
|
6
|
-
|
|
7
|
-
payload: unknown;
|
|
8
|
-
|
|
9
|
-
constructor(message: string, code: string, payload: unknown = null) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.code = code;
|
|
12
|
-
this.payload = payload;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Checks if the given error is a RuniumError
|
|
18
|
-
* @param error
|
|
19
|
-
*/
|
|
20
|
-
export function isRuniumError(error: unknown): boolean {
|
|
21
|
-
return !!error && error instanceof RuniumError;
|
|
22
|
-
}
|
package/src/file.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { RuniumError } from './error';
|
|
3
|
-
|
|
4
|
-
export type JSONValue = string | number | boolean | JSONObject | JSONValue[];
|
|
5
|
-
|
|
6
|
-
export interface JSONObject {
|
|
7
|
-
[x: string]: JSONValue;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* File error codes
|
|
12
|
-
*/
|
|
13
|
-
export enum FileErrorCode {
|
|
14
|
-
READ_JSON = 'file-read-json',
|
|
15
|
-
WRITE_JSON = 'file-write-json',
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Reads a JSON file
|
|
20
|
-
* @param path
|
|
21
|
-
*/
|
|
22
|
-
export async function readJsonFile<T = JSONValue>(path: string): Promise<T> {
|
|
23
|
-
try {
|
|
24
|
-
const data = await readFile(path, { encoding: 'utf-8' });
|
|
25
|
-
return JSON.parse(data);
|
|
26
|
-
} catch (ex) {
|
|
27
|
-
throw new RuniumError(
|
|
28
|
-
`Can not read JSON file ${path}`,
|
|
29
|
-
FileErrorCode.READ_JSON,
|
|
30
|
-
{ path, original: ex }
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Writes a JSON file
|
|
37
|
-
* @param path
|
|
38
|
-
* @param data
|
|
39
|
-
*/
|
|
40
|
-
export async function writeJsonFile<T = JSONValue>(
|
|
41
|
-
path: string,
|
|
42
|
-
data: T
|
|
43
|
-
): Promise<void> {
|
|
44
|
-
try {
|
|
45
|
-
await writeFile(path, JSON.stringify(data, null, 2), { encoding: 'utf-8' });
|
|
46
|
-
} catch (ex) {
|
|
47
|
-
throw new RuniumError(
|
|
48
|
-
`Can not write JSON file ${path}`,
|
|
49
|
-
FileErrorCode.WRITE_JSON,
|
|
50
|
-
{ path, data, original: ex }
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
}
|
package/src/index.ts
DELETED
package/src/macros.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { RuniumError } from './error';
|
|
2
|
-
|
|
3
|
-
export type Macro = (...params: string[]) => string;
|
|
4
|
-
|
|
5
|
-
export type MacrosCollection = Record<string, Macro>;
|
|
6
|
-
|
|
7
|
-
const UNWRAP_MACROS_PATTERN = String.raw`"\$unwrap\((.*)\)"`;
|
|
8
|
-
|
|
9
|
-
export enum MacrosErrorCode {
|
|
10
|
-
MACRO_APPLY_ERROR = 'macro-apply-error',
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Apply unwrap macro
|
|
15
|
-
* @param value
|
|
16
|
-
*/
|
|
17
|
-
function unwrap(value: string): string {
|
|
18
|
-
const unwrapRegex = new RegExp(UNWRAP_MACROS_PATTERN, 'g');
|
|
19
|
-
|
|
20
|
-
return value.replace(unwrapRegex, (match: string, value: string) => {
|
|
21
|
-
return value.trim();
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Apply macros to a text
|
|
27
|
-
* @param text
|
|
28
|
-
* @param macros
|
|
29
|
-
*/
|
|
30
|
-
export function applyMacros(text: string, macros: MacrosCollection): string {
|
|
31
|
-
const macrosNames = Object.keys(macros);
|
|
32
|
-
|
|
33
|
-
const pattern = String.raw`\$(${macrosNames.join('|')})\(([^()]*(?:\([^()]*\)[^()]*)*)\)`;
|
|
34
|
-
|
|
35
|
-
const apply = (data: string): string => {
|
|
36
|
-
const macrosRegex = new RegExp(pattern, 'g');
|
|
37
|
-
|
|
38
|
-
let result = data;
|
|
39
|
-
|
|
40
|
-
let hasMacros = true;
|
|
41
|
-
while (hasMacros) {
|
|
42
|
-
hasMacros = false;
|
|
43
|
-
result = result.replace(
|
|
44
|
-
macrosRegex,
|
|
45
|
-
(match: string, type: string, args: string) => {
|
|
46
|
-
hasMacros = true;
|
|
47
|
-
|
|
48
|
-
const macroArgs: string[] = [];
|
|
49
|
-
let depth = 0;
|
|
50
|
-
let current = '';
|
|
51
|
-
|
|
52
|
-
for (const char of args) {
|
|
53
|
-
if (char === ',' && depth === 0) {
|
|
54
|
-
macroArgs.push(current.trim());
|
|
55
|
-
current = '';
|
|
56
|
-
} else {
|
|
57
|
-
if (char === '(') {
|
|
58
|
-
depth++;
|
|
59
|
-
}
|
|
60
|
-
if (char === ')') {
|
|
61
|
-
depth--;
|
|
62
|
-
}
|
|
63
|
-
current += char;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
macroArgs.push(current.trim());
|
|
67
|
-
|
|
68
|
-
const values = macroArgs.map(arg => {
|
|
69
|
-
return arg ? apply(arg) : '';
|
|
70
|
-
});
|
|
71
|
-
try {
|
|
72
|
-
return macros[type](...values);
|
|
73
|
-
} catch (ex) {
|
|
74
|
-
throw new RuniumError(
|
|
75
|
-
`Can not apply macro "${type}"`,
|
|
76
|
-
MacrosErrorCode.MACRO_APPLY_ERROR,
|
|
77
|
-
{ type, args: macroArgs, original: ex }
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return result;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
// process text
|
|
88
|
-
const processedText = macrosNames.length ? apply(text) : text;
|
|
89
|
-
// apply unwrap macros
|
|
90
|
-
return unwrap(processedText);
|
|
91
|
-
}
|
package/src/project-config.ts
DELETED
|
@@ -1,483 +0,0 @@
|
|
|
1
|
-
import Ajv from 'ajv/dist/2020.js';
|
|
2
|
-
import ajvKeywords from 'ajv-keywords';
|
|
3
|
-
import { RuniumTaskState, TaskOptions } from './task';
|
|
4
|
-
import { RuniumError } from './error';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Project task start mode
|
|
8
|
-
*/
|
|
9
|
-
export enum ProjectTaskStartMode {
|
|
10
|
-
IMMEDIATE = 'immediate',
|
|
11
|
-
DEFERRED = 'deferred',
|
|
12
|
-
IGNORE = 'ignore',
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Project action type
|
|
17
|
-
*/
|
|
18
|
-
export enum ProjectActionType {
|
|
19
|
-
EMIT_EVENT = 'emit-event',
|
|
20
|
-
START_TASK = 'start-task',
|
|
21
|
-
RESTART_TASK = 'restart-task',
|
|
22
|
-
STOP_TASK = 'stop-task',
|
|
23
|
-
STOP_PROJECT = 'stop-project',
|
|
24
|
-
ENABLE_TRIGGER = 'enable-trigger',
|
|
25
|
-
DISABLE_TRIGGER = 'disable-trigger',
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Project task state condition
|
|
30
|
-
*/
|
|
31
|
-
export type ProjectTaskStateCondition =
|
|
32
|
-
| string
|
|
33
|
-
| boolean
|
|
34
|
-
| Partial<RuniumTaskState>
|
|
35
|
-
| ((state: RuniumTaskState) => boolean);
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Project task handler
|
|
39
|
-
*/
|
|
40
|
-
export interface ProjectTaskHandler {
|
|
41
|
-
condition: ProjectTaskStateCondition;
|
|
42
|
-
action: ProjectAction;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Project task dependency
|
|
47
|
-
*/
|
|
48
|
-
export interface ProjectTaskDependency {
|
|
49
|
-
taskId: string;
|
|
50
|
-
condition: ProjectTaskStateCondition;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Project task type
|
|
55
|
-
*/
|
|
56
|
-
export enum ProjectTaskType {
|
|
57
|
-
DEFAULT = 'default',
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Project task config
|
|
62
|
-
*/
|
|
63
|
-
export interface ProjectTaskConfig<Options = unknown> {
|
|
64
|
-
id: string;
|
|
65
|
-
options: Options;
|
|
66
|
-
type?: ProjectTaskType | string;
|
|
67
|
-
name?: string;
|
|
68
|
-
mode?: ProjectTaskStartMode;
|
|
69
|
-
dependencies?: ProjectTaskDependency[];
|
|
70
|
-
handlers?: ProjectTaskHandler[];
|
|
71
|
-
restart?: ProjectTaskRestartPolicy;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Project default task config
|
|
76
|
-
*/
|
|
77
|
-
export interface ProjectDefaultTaskConfig
|
|
78
|
-
extends ProjectTaskConfig<TaskOptions> {
|
|
79
|
-
type?: ProjectTaskType.DEFAULT;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Project action emit event
|
|
84
|
-
*/
|
|
85
|
-
export interface ProjectActionEmitEvent {
|
|
86
|
-
type: ProjectActionType.EMIT_EVENT;
|
|
87
|
-
event: string;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Project action process task
|
|
92
|
-
*/
|
|
93
|
-
export interface ProjectActionProcessTask {
|
|
94
|
-
type:
|
|
95
|
-
| ProjectActionType.START_TASK
|
|
96
|
-
| ProjectActionType.RESTART_TASK
|
|
97
|
-
| ProjectActionType.STOP_TASK;
|
|
98
|
-
taskId: string;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Project action stop project
|
|
103
|
-
*/
|
|
104
|
-
export interface ProjectActionStopProject {
|
|
105
|
-
type: ProjectActionType.STOP_PROJECT;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Project action toggle trigger
|
|
110
|
-
*/
|
|
111
|
-
export interface ProjectActionToggleTrigger {
|
|
112
|
-
type: ProjectActionType.ENABLE_TRIGGER | ProjectActionType.DISABLE_TRIGGER;
|
|
113
|
-
triggerId: string;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Project custom action
|
|
118
|
-
*/
|
|
119
|
-
export interface ProjectCustomAction {
|
|
120
|
-
type: string;
|
|
121
|
-
payload?: unknown;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Project action
|
|
126
|
-
*/
|
|
127
|
-
export type ProjectAction =
|
|
128
|
-
| ProjectCustomAction
|
|
129
|
-
| ProjectActionEmitEvent
|
|
130
|
-
| ProjectActionProcessTask
|
|
131
|
-
| ProjectActionStopProject
|
|
132
|
-
| ProjectActionToggleTrigger;
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Project trigger type
|
|
136
|
-
*/
|
|
137
|
-
export enum ProjectTriggerType {
|
|
138
|
-
EVENT = 'event',
|
|
139
|
-
TIMEOUT = 'timeout',
|
|
140
|
-
INTERVAL = 'interval',
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Project trigger base
|
|
145
|
-
*/
|
|
146
|
-
interface ProjectTriggerBase {
|
|
147
|
-
id: string;
|
|
148
|
-
type: ProjectTriggerType | string;
|
|
149
|
-
action: ProjectAction;
|
|
150
|
-
disabled?: boolean;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Project trigger event
|
|
155
|
-
*/
|
|
156
|
-
export interface ProjectTriggerEvent extends ProjectTriggerBase {
|
|
157
|
-
type: ProjectTriggerType.EVENT;
|
|
158
|
-
event: string;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Project trigger interval
|
|
163
|
-
*/
|
|
164
|
-
export interface ProjectTriggerInterval extends ProjectTriggerBase {
|
|
165
|
-
type: ProjectTriggerType.INTERVAL;
|
|
166
|
-
interval: number;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Project trigger timeout
|
|
171
|
-
*/
|
|
172
|
-
export interface ProjectTriggerTimeout extends ProjectTriggerBase {
|
|
173
|
-
type: ProjectTriggerType.TIMEOUT;
|
|
174
|
-
timeout: number;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Project custom trigger
|
|
179
|
-
*/
|
|
180
|
-
export interface ProjectCustomTrigger extends ProjectTriggerBase {
|
|
181
|
-
type: string;
|
|
182
|
-
payload?: unknown;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Project trigger
|
|
187
|
-
*/
|
|
188
|
-
export type ProjectTrigger =
|
|
189
|
-
| ProjectTriggerEvent
|
|
190
|
-
| ProjectTriggerInterval
|
|
191
|
-
| ProjectTriggerTimeout
|
|
192
|
-
| ProjectCustomTrigger;
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Project task restart policy type
|
|
196
|
-
*/
|
|
197
|
-
export enum ProjectTaskRestartPolicyType {
|
|
198
|
-
ALWAYS = 'always',
|
|
199
|
-
ON_FAILURE = 'on-failure',
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Project task restart policy always
|
|
204
|
-
*/
|
|
205
|
-
export interface ProjectTaskRestartPolicyAlways {
|
|
206
|
-
policy: ProjectTaskRestartPolicyType.ALWAYS;
|
|
207
|
-
delay?: number;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Project task restart policy on failure
|
|
212
|
-
*/
|
|
213
|
-
export interface ProjectTaskRestartPolicyOnFailure {
|
|
214
|
-
policy: ProjectTaskRestartPolicyType.ON_FAILURE;
|
|
215
|
-
delay?: number;
|
|
216
|
-
maxRetries?: number;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Project task restart policy
|
|
221
|
-
*/
|
|
222
|
-
export type ProjectTaskRestartPolicy =
|
|
223
|
-
| ProjectTaskRestartPolicyAlways
|
|
224
|
-
| ProjectTaskRestartPolicyOnFailure;
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Project config
|
|
228
|
-
*/
|
|
229
|
-
export interface ProjectConfig {
|
|
230
|
-
id: string;
|
|
231
|
-
name?: string;
|
|
232
|
-
tasks: ProjectTaskConfig[];
|
|
233
|
-
triggers?: ProjectTrigger[];
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Project config error codes
|
|
238
|
-
*/
|
|
239
|
-
export enum ProjectConfigErrorCode {
|
|
240
|
-
INCORRECT_DATA = 'project-config-incorrect-data',
|
|
241
|
-
TASKS_CIRCULAR_DEPENDENCY = 'project-config-tasks-circular-dependency',
|
|
242
|
-
TASK_NOT_EXISTS = 'project-config-task-not-exists',
|
|
243
|
-
TRIGGER_NOT_EXISTS = 'project-config-trigger-not-exists',
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Process task action types
|
|
248
|
-
*/
|
|
249
|
-
const PROCESS_TASK_ACTION_TYPES = new Set<ProjectActionType>([
|
|
250
|
-
ProjectActionType.START_TASK,
|
|
251
|
-
ProjectActionType.RESTART_TASK,
|
|
252
|
-
ProjectActionType.STOP_TASK,
|
|
253
|
-
]);
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Toggle trigger action types
|
|
257
|
-
*/
|
|
258
|
-
const TOGGLE_TRIGGER_ACTION_TYPES = new Set<ProjectActionType>([
|
|
259
|
-
ProjectActionType.ENABLE_TRIGGER,
|
|
260
|
-
ProjectActionType.DISABLE_TRIGGER,
|
|
261
|
-
]);
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Runium action types
|
|
265
|
-
*/
|
|
266
|
-
const RUNIUM_ACTION_TYPES = new Set(Object.values(ProjectActionType));
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Runium trigger types
|
|
270
|
-
*/
|
|
271
|
-
const RUNIUM_TRIGGER_TYPES = new Set(Object.values(ProjectTriggerType));
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Is process task action guard
|
|
275
|
-
* @param action
|
|
276
|
-
*/
|
|
277
|
-
export function isProcessTaskAction(
|
|
278
|
-
action: ProjectAction
|
|
279
|
-
): action is ProjectActionProcessTask {
|
|
280
|
-
return PROCESS_TASK_ACTION_TYPES.has(action.type as ProjectActionType);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Is toggle trigger action guard
|
|
285
|
-
* @param action
|
|
286
|
-
*/
|
|
287
|
-
export function isToggleTriggerAction(
|
|
288
|
-
action: ProjectAction
|
|
289
|
-
): action is ProjectActionToggleTrigger {
|
|
290
|
-
return TOGGLE_TRIGGER_ACTION_TYPES.has(action.type as ProjectActionType);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Is custom action guard
|
|
295
|
-
* @param action
|
|
296
|
-
*/
|
|
297
|
-
export function isCustomAction(
|
|
298
|
-
action: ProjectAction
|
|
299
|
-
): action is ProjectCustomAction {
|
|
300
|
-
return !RUNIUM_ACTION_TYPES.has(action.type as ProjectActionType);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Is custom trigger guard
|
|
305
|
-
* @param trigger
|
|
306
|
-
*/
|
|
307
|
-
export function isCustomTrigger(
|
|
308
|
-
trigger: ProjectTrigger
|
|
309
|
-
): trigger is ProjectCustomTrigger {
|
|
310
|
-
return !RUNIUM_TRIGGER_TYPES.has(trigger.type as ProjectTriggerType);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const ajv = new Ajv({ allowUnionTypes: true, allErrors: true, verbose: true });
|
|
314
|
-
ajvKeywords(ajv, ['uniqueItemProperties']);
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Validate project schema
|
|
318
|
-
* @param project
|
|
319
|
-
* @param schema
|
|
320
|
-
*/
|
|
321
|
-
function validateSchema(project: ProjectConfig, schema: object): void {
|
|
322
|
-
const validate = ajv.compile(schema || {});
|
|
323
|
-
const result = validate(project);
|
|
324
|
-
if (!result) {
|
|
325
|
-
throw new RuniumError(
|
|
326
|
-
'Incorrect project config data',
|
|
327
|
-
ProjectConfigErrorCode.INCORRECT_DATA,
|
|
328
|
-
{
|
|
329
|
-
errors: validate.errors,
|
|
330
|
-
}
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Detect cycles in dependencies
|
|
337
|
-
* @param deps
|
|
338
|
-
*/
|
|
339
|
-
function detectCycles(deps: Map<string, string[]>) {
|
|
340
|
-
const visited: Set<string> = new Set();
|
|
341
|
-
const stack: Set<string> = new Set();
|
|
342
|
-
|
|
343
|
-
const dfs = (node: string) => {
|
|
344
|
-
if (stack.has(node)) {
|
|
345
|
-
const cycle = [...stack, node].join(' -> ');
|
|
346
|
-
throw new RuniumError(
|
|
347
|
-
`Project config tasks circular dependency detected ${cycle}`,
|
|
348
|
-
ProjectConfigErrorCode.TASKS_CIRCULAR_DEPENDENCY,
|
|
349
|
-
{
|
|
350
|
-
task: node,
|
|
351
|
-
dependencies: [...stack],
|
|
352
|
-
}
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
if (visited.has(node)) return;
|
|
356
|
-
|
|
357
|
-
stack.add(node);
|
|
358
|
-
for (const neighbor of deps.get(node) || []) {
|
|
359
|
-
dfs(neighbor);
|
|
360
|
-
}
|
|
361
|
-
stack.delete(node);
|
|
362
|
-
visited.add(node);
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
for (const node of deps.keys()) {
|
|
366
|
-
if (!visited.has(node)) {
|
|
367
|
-
dfs(node);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Validate existing elements
|
|
374
|
-
* @param project
|
|
375
|
-
*/
|
|
376
|
-
function validateExistingElements(project: ProjectConfig): void {
|
|
377
|
-
// task dependencies graph
|
|
378
|
-
const deps: Map<string, string[]> = new Map();
|
|
379
|
-
|
|
380
|
-
// task ids
|
|
381
|
-
const taskIds: Set<string> = new Set();
|
|
382
|
-
for (const task of project.tasks) {
|
|
383
|
-
taskIds.add(task.id);
|
|
384
|
-
deps.set(task.id, []);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const triggerIds: Set<string> = new Set(
|
|
388
|
-
project.triggers?.map(t => t.id) || []
|
|
389
|
-
);
|
|
390
|
-
|
|
391
|
-
// check every task dependencies, handler actions use existing task and existing triggers
|
|
392
|
-
for (const task of project.tasks) {
|
|
393
|
-
for (const dependency of task.dependencies || []) {
|
|
394
|
-
if (!taskIds.has(dependency.taskId)) {
|
|
395
|
-
throw new RuniumError(
|
|
396
|
-
`Task "${task.id}" depends on not existing task "${dependency.taskId}"`,
|
|
397
|
-
ProjectConfigErrorCode.TASK_NOT_EXISTS,
|
|
398
|
-
{
|
|
399
|
-
taskId: task.id,
|
|
400
|
-
dependency: { ...dependency },
|
|
401
|
-
scope: 'dependencies',
|
|
402
|
-
}
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
deps.get(dependency.taskId)!.push(task.id);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
for (const handler of task.handlers || []) {
|
|
409
|
-
if (
|
|
410
|
-
isProcessTaskAction(handler.action) &&
|
|
411
|
-
!taskIds.has(handler.action.taskId)
|
|
412
|
-
) {
|
|
413
|
-
throw new RuniumError(
|
|
414
|
-
`Task "${task.id}" handler action uses not existing task "${handler.action.taskId}"`,
|
|
415
|
-
ProjectConfigErrorCode.TASK_NOT_EXISTS,
|
|
416
|
-
{
|
|
417
|
-
taskId: task.id,
|
|
418
|
-
handler: { ...handler },
|
|
419
|
-
scope: 'handlers',
|
|
420
|
-
}
|
|
421
|
-
);
|
|
422
|
-
} else if (
|
|
423
|
-
isToggleTriggerAction(handler.action) &&
|
|
424
|
-
!triggerIds.has(handler.action.triggerId)
|
|
425
|
-
) {
|
|
426
|
-
throw new RuniumError(
|
|
427
|
-
`Task "${task.id}" handler action uses not existing trigger "${handler.action.triggerId}"`,
|
|
428
|
-
ProjectConfigErrorCode.TRIGGER_NOT_EXISTS,
|
|
429
|
-
{
|
|
430
|
-
taskId: task.id,
|
|
431
|
-
handler: { ...handler },
|
|
432
|
-
scope: 'handlers',
|
|
433
|
-
}
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// check project triggers use existing tasks and triggers
|
|
440
|
-
for (const trigger of project.triggers || []) {
|
|
441
|
-
if (
|
|
442
|
-
isProcessTaskAction(trigger.action) &&
|
|
443
|
-
!taskIds.has(trigger.action.taskId)
|
|
444
|
-
) {
|
|
445
|
-
throw new RuniumError(
|
|
446
|
-
`Trigger "${trigger.id}" action uses not existing task "${trigger.action.taskId}"`,
|
|
447
|
-
ProjectConfigErrorCode.TASK_NOT_EXISTS,
|
|
448
|
-
{
|
|
449
|
-
trigger: { ...trigger },
|
|
450
|
-
scope: 'triggers',
|
|
451
|
-
}
|
|
452
|
-
);
|
|
453
|
-
} else if (
|
|
454
|
-
isToggleTriggerAction(trigger.action) &&
|
|
455
|
-
!triggerIds.has(trigger.action.triggerId)
|
|
456
|
-
) {
|
|
457
|
-
throw new RuniumError(
|
|
458
|
-
`Trigger "${trigger.id}" action uses not existing trigger "${trigger.action.triggerId}"`,
|
|
459
|
-
ProjectConfigErrorCode.TRIGGER_NOT_EXISTS,
|
|
460
|
-
{
|
|
461
|
-
trigger: { ...trigger },
|
|
462
|
-
scope: 'triggers',
|
|
463
|
-
}
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// detect circular and non unique dependencies
|
|
469
|
-
detectCycles(deps);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Validate project
|
|
474
|
-
* @param project
|
|
475
|
-
* @param schema
|
|
476
|
-
*/
|
|
477
|
-
export function validateProject(project: ProjectConfig, schema: object): void {
|
|
478
|
-
// validate schema
|
|
479
|
-
validateSchema(project, schema);
|
|
480
|
-
|
|
481
|
-
// validate existing elements
|
|
482
|
-
validateExistingElements(project);
|
|
483
|
-
}
|