@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.
@@ -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
+ };
@@ -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
+ };