@onlineapps/cookbook-core 1.0.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/README.md +117 -0
- package/package.json +38 -0
- package/schemas/cookbook.relaxed.schema.json +351 -0
- package/schemas/cookbook.schema.json +482 -0
- package/schemas/cookbook.strict.schema.json +441 -0
- package/src/index.js +60 -0
- package/src/parser/cookbookParser.js +108 -0
- package/src/parser/cookbookValidator.js +569 -0
- package/src/parser.js +7 -0
- package/src/referenceValidator.js +317 -0
- package/src/schema.js +46 -0
- package/src/validator.js +17 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reference validation utilities for cookbook schemas.
|
|
5
|
+
* Validates that depends_on references exist and variable paths are valid.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract all step IDs from a cookbook recursively
|
|
10
|
+
* @param {Array} steps - Array of steps
|
|
11
|
+
* @param {Set} ids - Set to collect IDs
|
|
12
|
+
* @returns {Set} Set of all step IDs
|
|
13
|
+
*/
|
|
14
|
+
function collectAllStepIds(steps, ids = new Set()) {
|
|
15
|
+
if (!Array.isArray(steps)) return ids;
|
|
16
|
+
|
|
17
|
+
steps.forEach(step => {
|
|
18
|
+
if (step && typeof step === 'object' && step.id) {
|
|
19
|
+
ids.add(step.id);
|
|
20
|
+
|
|
21
|
+
// Recursively collect from nested structures
|
|
22
|
+
switch (step.type) {
|
|
23
|
+
case 'foreach':
|
|
24
|
+
if (step.body) collectAllStepIds(step.body, ids);
|
|
25
|
+
break;
|
|
26
|
+
|
|
27
|
+
case 'fork_join':
|
|
28
|
+
if (step.branches) collectAllStepIds(step.branches, ids);
|
|
29
|
+
break;
|
|
30
|
+
|
|
31
|
+
case 'switch':
|
|
32
|
+
if (step.cases && typeof step.cases === 'object') {
|
|
33
|
+
Object.values(step.cases).forEach(caseStep => {
|
|
34
|
+
if (caseStep) collectAllStepIds([caseStep], ids);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (step.default) collectAllStepIds([step.default], ids);
|
|
38
|
+
break;
|
|
39
|
+
|
|
40
|
+
case 'sub_workflow':
|
|
41
|
+
if (step.steps) collectAllStepIds(step.steps, ids);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return ids;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validate that all depends_on references point to existing step IDs
|
|
52
|
+
* @param {Object} cookbook - The cookbook to validate
|
|
53
|
+
* @throws {Error} If invalid references are found
|
|
54
|
+
*/
|
|
55
|
+
function validateDependsOnReferences(cookbook) {
|
|
56
|
+
if (!cookbook || !cookbook.steps) return;
|
|
57
|
+
|
|
58
|
+
// Collect all valid step IDs
|
|
59
|
+
const validIds = collectAllStepIds(cookbook.steps);
|
|
60
|
+
|
|
61
|
+
// Check all depends_on references
|
|
62
|
+
const checkDependsOn = (steps, path = 'steps') => {
|
|
63
|
+
if (!Array.isArray(steps)) return;
|
|
64
|
+
|
|
65
|
+
steps.forEach((step, index) => {
|
|
66
|
+
const stepPath = `${path}[${index}]`;
|
|
67
|
+
|
|
68
|
+
if (step.depends_on && Array.isArray(step.depends_on)) {
|
|
69
|
+
step.depends_on.forEach((depId, depIndex) => {
|
|
70
|
+
if (!validIds.has(depId)) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Invalid depends_on reference at ${stepPath}.depends_on[${depIndex}]: ` +
|
|
73
|
+
`Step ID '${depId}' does not exist`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check nested steps
|
|
80
|
+
switch (step.type) {
|
|
81
|
+
case 'foreach':
|
|
82
|
+
if (step.body) checkDependsOn(step.body, `${stepPath}.body`);
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
case 'fork_join':
|
|
86
|
+
if (step.branches) checkDependsOn(step.branches, `${stepPath}.branches`);
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case 'switch':
|
|
90
|
+
if (step.cases && typeof step.cases === 'object') {
|
|
91
|
+
Object.entries(step.cases).forEach(([caseKey, caseStep]) => {
|
|
92
|
+
checkDependsOn([caseStep], `${stepPath}.cases['${caseKey}']`);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (step.default) checkDependsOn([step.default], `${stepPath}.default`);
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case 'sub_workflow':
|
|
99
|
+
if (step.steps) checkDependsOn(step.steps, `${stepPath}.steps`);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
checkDependsOn(cookbook.steps);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Validate variable references in cookbook
|
|
110
|
+
* @param {Object} cookbook - The cookbook to validate
|
|
111
|
+
* @throws {Error} If invalid variable references are found
|
|
112
|
+
*/
|
|
113
|
+
function validateVariableReferences(cookbook) {
|
|
114
|
+
if (!cookbook || !cookbook.steps) return;
|
|
115
|
+
|
|
116
|
+
// Collect valid variable paths
|
|
117
|
+
const validPaths = new Set([
|
|
118
|
+
'$api_input',
|
|
119
|
+
'$global_error',
|
|
120
|
+
'$_loop_item',
|
|
121
|
+
'$_loop_index',
|
|
122
|
+
'$_parent',
|
|
123
|
+
'$_context'
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
// Add step output paths
|
|
127
|
+
const stepIds = collectAllStepIds(cookbook.steps);
|
|
128
|
+
stepIds.forEach(id => {
|
|
129
|
+
validPaths.add(`$steps.${id}`);
|
|
130
|
+
validPaths.add(`$.${id}`);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Check variable references in strings
|
|
134
|
+
const checkVariableRef = (value, path) => {
|
|
135
|
+
if (typeof value === 'string') {
|
|
136
|
+
// Find all variable references ($ followed by word characters or dots)
|
|
137
|
+
const matches = value.match(/\$[\w\.]+/g);
|
|
138
|
+
if (matches) {
|
|
139
|
+
matches.forEach(match => {
|
|
140
|
+
// Extract base path (before any property access)
|
|
141
|
+
const basePath = match.split('.')[0];
|
|
142
|
+
|
|
143
|
+
// Check if it's a step reference
|
|
144
|
+
if (match.startsWith('$steps.')) {
|
|
145
|
+
const stepId = match.split('.')[1];
|
|
146
|
+
if (!stepIds.has(stepId)) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Invalid variable reference at ${path}: ` +
|
|
149
|
+
`Step '${stepId}' does not exist in '${match}'`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
156
|
+
// Recursively check object properties
|
|
157
|
+
Object.entries(value).forEach(([key, val]) => {
|
|
158
|
+
checkVariableRef(val, `${path}.${key}`);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Check all string values in steps
|
|
164
|
+
const checkStep = (step, path) => {
|
|
165
|
+
if (!step || typeof step !== 'object') return;
|
|
166
|
+
|
|
167
|
+
// Check common string fields
|
|
168
|
+
['condition', 'expression', 'iterator', 'errorOutput'].forEach(field => {
|
|
169
|
+
if (step[field]) {
|
|
170
|
+
checkVariableRef(step[field], `${path}.${field}`);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Check input/output mappings
|
|
175
|
+
['input', 'output'].forEach(field => {
|
|
176
|
+
if (step[field]) {
|
|
177
|
+
checkVariableRef(step[field], `${path}.${field}`);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Check nested steps
|
|
182
|
+
switch (step.type) {
|
|
183
|
+
case 'foreach':
|
|
184
|
+
if (step.body) {
|
|
185
|
+
step.body.forEach((s, i) => checkStep(s, `${path}.body[${i}]`));
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
|
|
189
|
+
case 'fork_join':
|
|
190
|
+
if (step.branches) {
|
|
191
|
+
step.branches.forEach((s, i) => checkStep(s, `${path}.branches[${i}]`));
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
|
|
195
|
+
case 'switch':
|
|
196
|
+
if (step.cases) {
|
|
197
|
+
Object.entries(step.cases).forEach(([k, s]) => {
|
|
198
|
+
checkStep(s, `${path}.cases['${k}']`);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (step.default) {
|
|
202
|
+
checkStep(step.default, `${path}.default`);
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
case 'sub_workflow':
|
|
207
|
+
if (step.steps) {
|
|
208
|
+
step.steps.forEach((s, i) => checkStep(s, `${path}.steps[${i}]`));
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
cookbook.steps.forEach((step, index) => {
|
|
215
|
+
checkStep(step, `steps[${index}]`);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Validate error handler references
|
|
221
|
+
* @param {Object} cookbook - The cookbook to validate
|
|
222
|
+
* @throws {Error} If invalid error handler references are found
|
|
223
|
+
*/
|
|
224
|
+
function validateErrorHandlerReferences(cookbook) {
|
|
225
|
+
if (!cookbook || !cookbook.steps) return;
|
|
226
|
+
|
|
227
|
+
const validIds = collectAllStepIds(cookbook.steps);
|
|
228
|
+
|
|
229
|
+
const checkErrorHandler = (handler, path) => {
|
|
230
|
+
if (!handler || typeof handler !== 'object') return;
|
|
231
|
+
|
|
232
|
+
// Check fallbackStep reference
|
|
233
|
+
if (handler.fallbackStep && !validIds.has(handler.fallbackStep)) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`Invalid error handler reference at ${path}.fallbackStep: ` +
|
|
236
|
+
`Step ID '${handler.fallbackStep}' does not exist`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Check compensationStep reference
|
|
241
|
+
if (handler.compensationStep && !validIds.has(handler.compensationStep)) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`Invalid error handler reference at ${path}.compensationStep: ` +
|
|
244
|
+
`Step ID '${handler.compensationStep}' does not exist`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Check global error handler
|
|
250
|
+
if (cookbook.global_error_handler) {
|
|
251
|
+
checkErrorHandler(cookbook.global_error_handler, 'global_error_handler');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check step error handlers
|
|
255
|
+
const checkStep = (step, path) => {
|
|
256
|
+
if (!step || typeof step !== 'object') return;
|
|
257
|
+
|
|
258
|
+
if (step.on_error) {
|
|
259
|
+
checkErrorHandler(step.on_error, `${path}.on_error`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check nested steps
|
|
263
|
+
switch (step.type) {
|
|
264
|
+
case 'foreach':
|
|
265
|
+
if (step.body) {
|
|
266
|
+
step.body.forEach((s, i) => checkStep(s, `${path}.body[${i}]`));
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
|
|
270
|
+
case 'fork_join':
|
|
271
|
+
if (step.branches) {
|
|
272
|
+
step.branches.forEach((s, i) => checkStep(s, `${path}.branches[${i}]`));
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
|
|
276
|
+
case 'switch':
|
|
277
|
+
if (step.cases) {
|
|
278
|
+
Object.entries(step.cases).forEach(([k, s]) => {
|
|
279
|
+
checkStep(s, `${path}.cases['${k}']`);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (step.default) {
|
|
283
|
+
checkStep(step.default, `${path}.default`);
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
|
|
287
|
+
case 'sub_workflow':
|
|
288
|
+
if (step.steps) {
|
|
289
|
+
step.steps.forEach((s, i) => checkStep(s, `${path}.steps[${i}]`));
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
cookbook.steps.forEach((step, index) => {
|
|
296
|
+
checkStep(step, `steps[${index}]`);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Validate all references in a cookbook
|
|
302
|
+
* @param {Object} cookbook - The cookbook to validate
|
|
303
|
+
* @throws {Error} If any invalid references are found
|
|
304
|
+
*/
|
|
305
|
+
function validateAllReferences(cookbook) {
|
|
306
|
+
validateDependsOnReferences(cookbook);
|
|
307
|
+
validateVariableReferences(cookbook);
|
|
308
|
+
validateErrorHandlerReferences(cookbook);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = {
|
|
312
|
+
collectAllStepIds,
|
|
313
|
+
validateDependsOnReferences,
|
|
314
|
+
validateVariableReferences,
|
|
315
|
+
validateErrorHandlerReferences,
|
|
316
|
+
validateAllReferences
|
|
317
|
+
};
|
package/src/schema.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema access utilities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Load the cookbook JSON schema
|
|
12
|
+
* @param {string} mode - Schema mode: 'default', 'strict', 'relaxed'
|
|
13
|
+
* @returns {Object} The cookbook schema object
|
|
14
|
+
*/
|
|
15
|
+
function loadSchema(mode = 'default') {
|
|
16
|
+
let schemaFile;
|
|
17
|
+
switch (mode) {
|
|
18
|
+
case 'strict':
|
|
19
|
+
schemaFile = 'cookbook.strict.schema.json';
|
|
20
|
+
break;
|
|
21
|
+
case 'relaxed':
|
|
22
|
+
schemaFile = 'cookbook.relaxed.schema.json';
|
|
23
|
+
break;
|
|
24
|
+
default:
|
|
25
|
+
schemaFile = 'cookbook.schema.json';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const schemaPath = path.join(__dirname, '../schemas', schemaFile);
|
|
29
|
+
|
|
30
|
+
// Check if file exists
|
|
31
|
+
if (!fs.existsSync(schemaPath)) {
|
|
32
|
+
// Fall back to default schema
|
|
33
|
+
const defaultPath = path.join(__dirname, '../schemas/cookbook.schema.json');
|
|
34
|
+
return JSON.parse(fs.readFileSync(defaultPath, 'utf-8'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Pre-load the default schema
|
|
41
|
+
const CookbookSchema = loadSchema();
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
CookbookSchema,
|
|
45
|
+
loadSchema
|
|
46
|
+
};
|
package/src/validator.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Re-export validator functions from the parser module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
validateCookbook,
|
|
9
|
+
validateStep,
|
|
10
|
+
CookbookValidationError
|
|
11
|
+
} = require('./parser/cookbookValidator');
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
validateCookbook,
|
|
15
|
+
validateStep,
|
|
16
|
+
CookbookValidationError
|
|
17
|
+
};
|