@runium/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.
@@ -0,0 +1,767 @@
1
+ import {
2
+ ProjectActionType,
3
+ ProjectTaskRestartPolicyType,
4
+ ProjectTaskStartMode,
5
+ ProjectTaskType,
6
+ ProjectTriggerType,
7
+ } from './project-config';
8
+ import { TaskStatus } from './task';
9
+ import { RuniumError } from './error';
10
+
11
+ interface ProjectSchema {
12
+ properties: {
13
+ tasks: {
14
+ items: {
15
+ oneOf: { $ref: string }[];
16
+ };
17
+ };
18
+ };
19
+ required?: string[];
20
+ $defs: Record<string, ProjectSchemaTask | ProjectSchemaAction | unknown> & {
21
+ Runium_Action: {
22
+ oneOf: { $ref: string }[];
23
+ };
24
+ Runium_Trigger: {
25
+ oneOf: { $ref: string }[];
26
+ };
27
+ };
28
+ }
29
+
30
+ interface ProjectSchemaTask {
31
+ properties: {
32
+ type: {
33
+ const: string;
34
+ };
35
+ };
36
+ }
37
+
38
+ interface ProjectSchemaAction {
39
+ properties: {
40
+ type: {
41
+ const?: string;
42
+ enum?: string[];
43
+ };
44
+ };
45
+ }
46
+
47
+ export interface ProjectSchemaExtensionProject {
48
+ properties: unknown;
49
+ required?: string[];
50
+ }
51
+
52
+ export interface ProjectSchemaExtensionTask {
53
+ type: string;
54
+ options: unknown;
55
+ }
56
+
57
+ export interface ProjectSchemaExtensionAction {
58
+ type: string;
59
+ payload?: unknown;
60
+ }
61
+
62
+ export interface ProjectSchemaExtensionTrigger {
63
+ type: string;
64
+ payload?: unknown;
65
+ }
66
+
67
+ export interface ProjectSchemaExtension {
68
+ project?: ProjectSchemaExtensionProject;
69
+ tasks?: Record<string, ProjectSchemaExtensionTask>;
70
+ definitions?: Record<string, unknown>;
71
+ actions?: Record<string, ProjectSchemaExtensionAction>;
72
+ triggers?: Record<string, ProjectSchemaExtensionTrigger>;
73
+ }
74
+
75
+ export enum ProjectSchemaErrorCode {
76
+ ACTION_TYPE_ALREADY_USED = 'project-schema-action-type-already-used',
77
+ TASK_TYPE_ALREADY_USED = 'project-schema-task-type-already-used',
78
+ TRIGGER_TYPE_ALREADY_USED = 'project-schema-trigger-type-already-used',
79
+ }
80
+
81
+ export const ID_REGEX = /^[a-zA-Z0-9_-]+$/;
82
+
83
+ /**
84
+ * Task common properties
85
+ */
86
+ const TASK_COMMON_PROPERTIES = {
87
+ id: {
88
+ type: 'string',
89
+ pattern: ID_REGEX.source,
90
+ },
91
+ name: {
92
+ type: 'string',
93
+ },
94
+ type: {
95
+ type: 'string',
96
+ },
97
+ mode: {
98
+ $ref: '#/$defs/Runium_TaskStartMode',
99
+ },
100
+ dependencies: {
101
+ type: 'array',
102
+ items: {
103
+ $ref: '#/$defs/Runium_TaskDependency',
104
+ },
105
+ },
106
+ handlers: {
107
+ type: 'array',
108
+ items: {
109
+ $ref: '#/$defs/Runium_TaskHandler',
110
+ },
111
+ },
112
+ restart: {
113
+ $ref: '#/$defs/Runium_TaskRestartPolicy',
114
+ },
115
+ };
116
+
117
+ /**
118
+ * Trigger common properties
119
+ */
120
+ const TRIGGER_COMMON_PROPERTIES = {
121
+ id: {
122
+ type: 'string',
123
+ pattern: ID_REGEX.source,
124
+ },
125
+ action: {
126
+ $ref: '#/$defs/Runium_Action',
127
+ },
128
+ disabled: {
129
+ type: 'boolean',
130
+ },
131
+ };
132
+
133
+ /**
134
+ * Create task schema
135
+ * @param type
136
+ * @param options
137
+ */
138
+ function createTaskSchema(type: string, options: unknown): object {
139
+ return {
140
+ type: 'object',
141
+ properties: {
142
+ ...structuredClone(TASK_COMMON_PROPERTIES),
143
+ type: {
144
+ const: type,
145
+ },
146
+ options: {
147
+ ...structuredClone(options as object),
148
+ },
149
+ },
150
+ required: ['id', 'options'],
151
+ additionalProperties: false,
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Create action schema
157
+ * @param type
158
+ * @param payload
159
+ */
160
+ function createActionSchema(type: string, payload?: unknown): object {
161
+ const payloadProp = payload ? { payload } : {};
162
+ const payloadRequired = payload ? ['payload'] : [];
163
+ return {
164
+ type: 'object',
165
+ properties: {
166
+ type: {
167
+ type: 'string',
168
+ const: type,
169
+ },
170
+ ...payloadProp,
171
+ },
172
+ required: ['type', ...payloadRequired],
173
+ additionalProperties: false,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Create trigger schema
179
+ * @param type
180
+ * @param payload
181
+ */
182
+ function createTriggerSchema(type: string, payload?: unknown): object {
183
+ const payloadProp = payload ? { payload } : {};
184
+ const payloadRequired = payload ? ['payload'] : [];
185
+ return {
186
+ type: 'object',
187
+ properties: {
188
+ ...structuredClone(TRIGGER_COMMON_PROPERTIES),
189
+ type: {
190
+ type: 'string',
191
+ const: type,
192
+ },
193
+ ...payloadProp,
194
+ },
195
+ required: ['id', 'type', 'action', ...payloadRequired],
196
+ additionalProperties: false,
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Get project schema
202
+ */
203
+ export function getProjectSchema(): object {
204
+ const schema = {
205
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
206
+ $id: 'https://example.com/schemas/project.json',
207
+ title: 'Project',
208
+ type: 'object',
209
+ properties: {
210
+ id: {
211
+ type: 'string',
212
+ pattern: ID_REGEX.source,
213
+ },
214
+ name: {
215
+ type: 'string',
216
+ },
217
+ tasks: {
218
+ type: 'array',
219
+ items: {
220
+ oneOf: [
221
+ {
222
+ $ref: '#/$defs/Runium_TaskConfig',
223
+ },
224
+ ],
225
+ },
226
+ minItems: 1,
227
+ uniqueItemProperties: ['id'],
228
+ },
229
+ triggers: {
230
+ type: 'array',
231
+ items: {
232
+ $ref: '#/$defs/Runium_Trigger',
233
+ },
234
+ uniqueItemProperties: ['id'],
235
+ },
236
+ },
237
+ required: ['id', 'tasks'],
238
+ additionalProperties: false,
239
+ $defs: {
240
+ Runium_EnvValue: {
241
+ type: ['string', 'number', 'boolean'],
242
+ },
243
+ Runium_Env: {
244
+ type: 'object',
245
+ additionalProperties: {
246
+ $ref: '#/$defs/Runium_EnvValue',
247
+ },
248
+ },
249
+ Runium_TaskStartMode: {
250
+ type: 'string',
251
+ enum: Object.values(ProjectTaskStartMode),
252
+ },
253
+ Runium_TaskHandler: {
254
+ type: 'object',
255
+ properties: {
256
+ action: {
257
+ $ref: '#/$defs/Runium_Action',
258
+ },
259
+ condition: {
260
+ $ref: '#/$defs/Runium_TaskStateCondition',
261
+ },
262
+ },
263
+ required: ['action', 'condition'],
264
+ additionalProperties: false,
265
+ },
266
+ Runium_TaskStateCondition: {
267
+ oneOf: [
268
+ {
269
+ type: 'string',
270
+ },
271
+ {
272
+ type: 'boolean',
273
+ },
274
+ {
275
+ $ref: '#/$defs/Runium_TaskState',
276
+ },
277
+ ],
278
+ },
279
+ Runium_TaskState: {
280
+ type: 'object',
281
+ properties: {
282
+ status: {
283
+ $ref: '#/$defs/Runium_TaskStatus',
284
+ },
285
+ iteration: {
286
+ type: 'number',
287
+ minimum: 0,
288
+ },
289
+ exitCode: {
290
+ type: 'number',
291
+ minimum: 0,
292
+ },
293
+ reason: {
294
+ type: 'string',
295
+ },
296
+ },
297
+ additionalProperties: false,
298
+ },
299
+ Runium_TaskStatus: {
300
+ type: 'string',
301
+ enum: Object.values(TaskStatus),
302
+ },
303
+ Runium_TaskDependency: {
304
+ type: 'object',
305
+ properties: {
306
+ taskId: {
307
+ type: 'string',
308
+ },
309
+ condition: {
310
+ $ref: '#/$defs/Runium_TaskStateCondition',
311
+ },
312
+ },
313
+ required: ['taskId', 'condition'],
314
+ additionalProperties: false,
315
+ },
316
+ Runium_Trigger: {
317
+ oneOf: [
318
+ {
319
+ $ref: '#/$defs/Runium_TriggerEvent',
320
+ },
321
+ {
322
+ $ref: '#/$defs/Runium_TriggerInterval',
323
+ },
324
+ {
325
+ $ref: '#/$defs/Runium_TriggerTimeout',
326
+ },
327
+ ],
328
+ },
329
+ Runium_TriggerEvent: {
330
+ type: 'object',
331
+ properties: {
332
+ ...structuredClone(TRIGGER_COMMON_PROPERTIES),
333
+ type: {
334
+ const: ProjectTriggerType.EVENT,
335
+ },
336
+ event: {
337
+ type: 'string',
338
+ },
339
+ },
340
+ required: ['id', 'type', 'event', 'action'],
341
+ additionalProperties: false,
342
+ },
343
+ Runium_TriggerInterval: {
344
+ type: 'object',
345
+ properties: {
346
+ ...structuredClone(TRIGGER_COMMON_PROPERTIES),
347
+ type: {
348
+ const: ProjectTriggerType.INTERVAL,
349
+ },
350
+ interval: {
351
+ type: 'number',
352
+ minimum: 0,
353
+ },
354
+ },
355
+ required: ['id', 'type', 'interval', 'action'],
356
+ additionalProperties: false,
357
+ },
358
+ Runium_TriggerTimeout: {
359
+ type: 'object',
360
+ properties: {
361
+ ...structuredClone(TRIGGER_COMMON_PROPERTIES),
362
+ type: {
363
+ const: ProjectTriggerType.TIMEOUT,
364
+ },
365
+ timeout: {
366
+ type: 'number',
367
+ minimum: 0,
368
+ },
369
+ },
370
+ required: ['id', 'type', 'timeout', 'action'],
371
+ additionalProperties: false,
372
+ },
373
+ Runium_Action: {
374
+ oneOf: [
375
+ {
376
+ $ref: '#/$defs/Runium_ActionEmitEvent',
377
+ },
378
+ {
379
+ $ref: '#/$defs/Runium_ActionProcessTask',
380
+ },
381
+ {
382
+ $ref: '#/$defs/Runium_ActionStopProject',
383
+ },
384
+ {
385
+ $ref: '#/$defs/Runium_ActionToggleTrigger',
386
+ },
387
+ ],
388
+ },
389
+ Runium_ActionEmitEvent: {
390
+ type: 'object',
391
+ properties: {
392
+ type: {
393
+ type: 'string',
394
+ const: ProjectActionType.EMIT_EVENT,
395
+ },
396
+ event: {
397
+ type: 'string',
398
+ },
399
+ },
400
+ required: ['type', 'event'],
401
+ additionalProperties: false,
402
+ },
403
+ Runium_ActionProcessTask: {
404
+ type: 'object',
405
+ properties: {
406
+ type: {
407
+ type: 'string',
408
+ enum: [
409
+ ProjectActionType.START_TASK,
410
+ ProjectActionType.RESTART_TASK,
411
+ ProjectActionType.STOP_TASK,
412
+ ],
413
+ },
414
+ taskId: {
415
+ type: 'string',
416
+ },
417
+ },
418
+ required: ['type', 'taskId'],
419
+ additionalProperties: false,
420
+ },
421
+ Runium_ActionStopProject: {
422
+ type: 'object',
423
+ properties: {
424
+ type: {
425
+ type: 'string',
426
+ const: ProjectActionType.STOP_PROJECT,
427
+ },
428
+ },
429
+ required: ['type'],
430
+ additionalProperties: false,
431
+ },
432
+ Runium_ActionToggleTrigger: {
433
+ type: 'object',
434
+ properties: {
435
+ type: {
436
+ type: 'string',
437
+ enum: [
438
+ ProjectActionType.ENABLE_TRIGGER,
439
+ ProjectActionType.DISABLE_TRIGGER,
440
+ ],
441
+ },
442
+ triggerId: {
443
+ type: 'string',
444
+ },
445
+ },
446
+ required: ['type', 'triggerId'],
447
+ additionalProperties: false,
448
+ },
449
+ Runium_TaskConfig: {
450
+ ...createTaskSchema(ProjectTaskType.DEFAULT, {
451
+ $ref: '#/$defs/Runium_TaskOptions',
452
+ }),
453
+ },
454
+ Runium_TaskOptions: {
455
+ type: 'object',
456
+ properties: {
457
+ command: {
458
+ type: 'string',
459
+ },
460
+ arguments: {
461
+ type: 'array',
462
+ items: {
463
+ type: 'string',
464
+ },
465
+ },
466
+ shell: {
467
+ type: 'boolean',
468
+ },
469
+ cwd: {
470
+ type: 'string',
471
+ },
472
+ env: {
473
+ $ref: '#/$defs/Runium_Env',
474
+ },
475
+ ttl: {
476
+ type: 'number',
477
+ },
478
+ log: {
479
+ $ref: '#/$defs/Runium_TaskLog',
480
+ },
481
+ stopSignal: {
482
+ type: 'string',
483
+ },
484
+ },
485
+ required: ['command'],
486
+ additionalProperties: false,
487
+ },
488
+ Runium_TaskRestartPolicy: {
489
+ oneOf: [
490
+ {
491
+ $ref: '#/$defs/Runium_TaskRestartPolicyAlways',
492
+ },
493
+ {
494
+ $ref: '#/$defs/Runium_TaskRestartPolicyOnFailure',
495
+ },
496
+ ],
497
+ },
498
+ Runium_TaskRestartPolicyAlways: {
499
+ type: 'object',
500
+ required: ['policy'],
501
+ properties: {
502
+ policy: {
503
+ const: ProjectTaskRestartPolicyType.ALWAYS,
504
+ },
505
+ delay: {
506
+ type: 'number',
507
+ },
508
+ },
509
+ additionalProperties: false,
510
+ },
511
+ Runium_TaskRestartPolicyOnFailure: {
512
+ type: 'object',
513
+ required: ['policy'],
514
+ properties: {
515
+ policy: {
516
+ const: ProjectTaskRestartPolicyType.ON_FAILURE,
517
+ },
518
+ delay: {
519
+ type: 'number',
520
+ },
521
+ maxRetries: {
522
+ type: 'number',
523
+ },
524
+ },
525
+ additionalProperties: false,
526
+ },
527
+ Runium_TaskLog: {
528
+ type: 'object',
529
+ properties: {
530
+ stdout: {
531
+ type: ['string', 'null'],
532
+ },
533
+ stderr: {
534
+ type: ['string', 'null'],
535
+ },
536
+ },
537
+ additionalProperties: false,
538
+ },
539
+ },
540
+ };
541
+
542
+ return Object.freeze(schema as object);
543
+ }
544
+
545
+ /**
546
+ * Extend project schema
547
+ * @param schema
548
+ * @param extensions
549
+ */
550
+ export function extendProjectSchema(
551
+ schema: object,
552
+ extensions: ProjectSchemaExtension
553
+ ): object {
554
+ let result = structuredClone(schema) as ProjectSchema;
555
+
556
+ // extend project properties
557
+ if (extensions.project) {
558
+ result = extendProjectPropertiesSchema(result, extensions.project);
559
+ }
560
+
561
+ // extend definitions
562
+ if (extensions.definitions) {
563
+ result = extendDefinitionsSchema(result, extensions.definitions);
564
+ }
565
+
566
+ // extend tasks
567
+ if (extensions.tasks) {
568
+ result = extendTasksSchema(result, extensions.tasks);
569
+ }
570
+
571
+ // extend actions
572
+ if (extensions.actions) {
573
+ result = extendActionsSchema(result, extensions.actions);
574
+ }
575
+
576
+ // extend triggers
577
+ if (extensions.triggers) {
578
+ result = extendTriggersSchema(result, extensions.triggers);
579
+ }
580
+
581
+ return Object.freeze(result as object);
582
+ }
583
+
584
+ /**
585
+ * Extend project properties schema
586
+ * @param schema
587
+ * @param extension
588
+ */
589
+ function extendProjectPropertiesSchema(
590
+ schema: ProjectSchema,
591
+ extension: ProjectSchemaExtension['project']
592
+ ): ProjectSchema {
593
+ if (extension) {
594
+ schema.properties = {
595
+ ...(extension.properties || {}),
596
+ ...schema.properties,
597
+ };
598
+ schema.required = Array.from(
599
+ new Set([...(schema.required ?? []), ...(extension.required ?? [])])
600
+ );
601
+ }
602
+ return schema;
603
+ }
604
+
605
+ /**
606
+ * Extend definitions schema
607
+ * @param schema
608
+ * @param extension
609
+ */
610
+ function extendDefinitionsSchema(
611
+ schema: ProjectSchema,
612
+ extension: ProjectSchemaExtension['definitions']
613
+ ): ProjectSchema {
614
+ if (extension) {
615
+ schema.$defs = {
616
+ ...(extension as Record<string, unknown>),
617
+ ...schema.$defs,
618
+ };
619
+ }
620
+ return schema;
621
+ }
622
+
623
+ /**
624
+ * Extend tasks schema
625
+ * @param schema
626
+ * @param extension
627
+ */
628
+ function extendTasksSchema(
629
+ schema: ProjectSchema,
630
+ extension: ProjectSchemaExtension['tasks']
631
+ ): ProjectSchema {
632
+ if (extension) {
633
+ // check types uniqueness
634
+ const taskTypes = new Set(
635
+ schema.properties.tasks.items.oneOf.map(task => {
636
+ const taskDef = task.$ref.split('/').pop() || '';
637
+ return (
638
+ (schema.$defs[taskDef] as ProjectSchemaTask)?.properties?.type
639
+ ?.const || ProjectTaskType.DEFAULT
640
+ );
641
+ })
642
+ );
643
+
644
+ const tasks: Record<string, object> = {};
645
+ for (const [key, value] of Object.entries(extension)) {
646
+ if (taskTypes.has(value.type)) {
647
+ throw new RuniumError(
648
+ `Task type "${value.type}" already used in project schema`,
649
+ ProjectSchemaErrorCode.TASK_TYPE_ALREADY_USED,
650
+ {
651
+ type: value.type,
652
+ }
653
+ );
654
+ }
655
+ tasks[key] = createTaskSchema(value.type, value.options);
656
+
657
+ schema.properties.tasks.items.oneOf.push({
658
+ $ref: `#/$defs/${key}`,
659
+ });
660
+
661
+ taskTypes.add(value.type);
662
+ }
663
+ schema.$defs = {
664
+ ...tasks,
665
+ ...schema.$defs,
666
+ };
667
+ }
668
+ return schema;
669
+ }
670
+
671
+ /**
672
+ * Extend actions schema
673
+ * @param schema
674
+ * @param extension
675
+ */
676
+ function extendActionsSchema(
677
+ schema: ProjectSchema,
678
+ extension: ProjectSchemaExtension['actions']
679
+ ): ProjectSchema {
680
+ if (extension) {
681
+ // check types uniqueness
682
+ const actionTypes = new Set(
683
+ schema.$defs.Runium_Action.oneOf
684
+ .map(action => {
685
+ const actionDef = action.$ref.split('/').pop() || '';
686
+ const actionType = (schema.$defs[actionDef] as ProjectSchemaAction)
687
+ ?.properties?.type;
688
+ return actionType?.enum || [actionType?.const];
689
+ })
690
+ .flat()
691
+ );
692
+
693
+ const actions: Record<string, object> = {};
694
+ for (const [key, value] of Object.entries(extension)) {
695
+ if (actionTypes.has(value.type)) {
696
+ throw new RuniumError(
697
+ `Action type "${value.type}" already used in project schema`,
698
+ ProjectSchemaErrorCode.ACTION_TYPE_ALREADY_USED,
699
+ {
700
+ type: value.type,
701
+ }
702
+ );
703
+ }
704
+
705
+ actions[key] = createActionSchema(value.type, value.payload);
706
+
707
+ schema.$defs.Runium_Action.oneOf.push({
708
+ $ref: `#/$defs/${key}`,
709
+ });
710
+
711
+ actionTypes.add(value.type);
712
+ }
713
+ schema.$defs = {
714
+ ...actions,
715
+ ...schema.$defs,
716
+ };
717
+ }
718
+ return schema;
719
+ }
720
+
721
+ /**
722
+ * Extend triggers schema
723
+ * @param schema
724
+ * @param extension
725
+ */
726
+ function extendTriggersSchema(
727
+ schema: ProjectSchema,
728
+ extension: ProjectSchemaExtension['triggers']
729
+ ): ProjectSchema {
730
+ if (extension) {
731
+ // check types uniqueness
732
+ const triggerTypes = new Set(
733
+ schema.$defs.Runium_Trigger.oneOf.map(trigger => {
734
+ const triggerDef = trigger.$ref.split('/').pop() || '';
735
+ const triggerType = (schema.$defs[triggerDef] as ProjectSchemaAction)
736
+ ?.properties?.type;
737
+ return triggerType?.const;
738
+ })
739
+ );
740
+
741
+ const triggers: Record<string, object> = {};
742
+ for (const [key, value] of Object.entries(extension)) {
743
+ if (triggerTypes.has(value.type)) {
744
+ throw new RuniumError(
745
+ `Trigger type "${value.type}" already used in project schema`,
746
+ ProjectSchemaErrorCode.TRIGGER_TYPE_ALREADY_USED,
747
+ {
748
+ type: value.type,
749
+ }
750
+ );
751
+ }
752
+
753
+ triggers[key] = createTriggerSchema(value.type, value.payload);
754
+
755
+ schema.$defs.Runium_Trigger.oneOf.push({
756
+ $ref: `#/$defs/${key}`,
757
+ });
758
+
759
+ triggerTypes.add(value.type);
760
+ }
761
+ schema.$defs = {
762
+ ...triggers,
763
+ ...schema.$defs,
764
+ };
765
+ }
766
+ return schema;
767
+ }