@onlineapps/cookbook-core 2.1.8 → 2.1.10
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/package.json
CHANGED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview
|
|
5
|
+
* Cookbook 2.1 Helper Functions
|
|
6
|
+
*
|
|
7
|
+
* STRICT RULES:
|
|
8
|
+
* - steps is ALWAYS an array
|
|
9
|
+
* - Access by index is FORBIDDEN - use findStep(stepId) instead
|
|
10
|
+
* - Reference steps ONLY by step_id
|
|
11
|
+
* - Iteration via forEach/map/filter ONLY
|
|
12
|
+
*
|
|
13
|
+
* These helpers enforce safe access patterns for cookbook steps.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* CookbookHelpers - Safe access to cookbook steps
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* const { findStep, forEachStep, getStepIds } = require('@onlineapps/cookbook-core');
|
|
21
|
+
*
|
|
22
|
+
* // Find step by step_id
|
|
23
|
+
* const step = findStep(cookbook.steps, 'my-step-id');
|
|
24
|
+
*
|
|
25
|
+
* // Iterate all steps
|
|
26
|
+
* forEachStep(cookbook.steps, (step, stepId) => { ... });
|
|
27
|
+
*
|
|
28
|
+
* // Get all step IDs in order
|
|
29
|
+
* const ids = getStepIds(cookbook.steps);
|
|
30
|
+
*/
|
|
31
|
+
class CookbookHelpers {
|
|
32
|
+
/**
|
|
33
|
+
* Find a step by its step_id
|
|
34
|
+
* This is the PRIMARY method for accessing steps - NEVER use index!
|
|
35
|
+
*
|
|
36
|
+
* @param {Array} steps - Array of step objects
|
|
37
|
+
* @param {string} stepId - The step_id to find
|
|
38
|
+
* @returns {Object|null} The step object or null if not found
|
|
39
|
+
* @throws {Error} If steps is not an array or stepId is not provided
|
|
40
|
+
*/
|
|
41
|
+
static findStep(steps, stepId) {
|
|
42
|
+
if (!stepId) {
|
|
43
|
+
throw new Error('[CookbookHelpers.findStep] stepId is required - never access by index!');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!Array.isArray(steps)) {
|
|
47
|
+
// Handle V2.0 object format for backwards compatibility during transition
|
|
48
|
+
if (steps && typeof steps === 'object') {
|
|
49
|
+
return steps[stepId] || null;
|
|
50
|
+
}
|
|
51
|
+
throw new Error('[CookbookHelpers.findStep] steps must be an array (V2.1 format)');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return steps.find(step => step.step_id === stepId) || null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Iterate over all steps with their step_ids
|
|
59
|
+
*
|
|
60
|
+
* @param {Array} steps - Array of step objects
|
|
61
|
+
* @param {Function} callback - Function(step, stepId, index) - index for internal use only!
|
|
62
|
+
* @throws {Error} If steps is not an array
|
|
63
|
+
*/
|
|
64
|
+
static forEachStep(steps, callback) {
|
|
65
|
+
if (!Array.isArray(steps)) {
|
|
66
|
+
// Handle V2.0 object format for backwards compatibility during transition
|
|
67
|
+
if (steps && typeof steps === 'object') {
|
|
68
|
+
Object.entries(steps).forEach(([stepId, step], index) => {
|
|
69
|
+
callback(step, stepId, index);
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
throw new Error('[CookbookHelpers.forEachStep] steps must be an array (V2.1 format)');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
steps.forEach((step, index) => {
|
|
77
|
+
callback(step, step.step_id, index);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Map over all steps
|
|
83
|
+
*
|
|
84
|
+
* @param {Array} steps - Array of step objects
|
|
85
|
+
* @param {Function} callback - Function(step, stepId) => newValue
|
|
86
|
+
* @returns {Array} Mapped values
|
|
87
|
+
* @throws {Error} If steps is not an array
|
|
88
|
+
*/
|
|
89
|
+
static mapSteps(steps, callback) {
|
|
90
|
+
if (!Array.isArray(steps)) {
|
|
91
|
+
// Handle V2.0 object format for backwards compatibility during transition
|
|
92
|
+
if (steps && typeof steps === 'object') {
|
|
93
|
+
return Object.entries(steps).map(([stepId, step]) => callback(step, stepId));
|
|
94
|
+
}
|
|
95
|
+
throw new Error('[CookbookHelpers.mapSteps] steps must be an array (V2.1 format)');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return steps.map(step => callback(step, step.step_id));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get all step IDs in their defined order
|
|
103
|
+
*
|
|
104
|
+
* @param {Array} steps - Array of step objects
|
|
105
|
+
* @returns {Array<string>} Array of step_ids in order
|
|
106
|
+
* @throws {Error} If steps is not an array
|
|
107
|
+
*/
|
|
108
|
+
static getStepIds(steps) {
|
|
109
|
+
if (!Array.isArray(steps)) {
|
|
110
|
+
// Handle V2.0 object format for backwards compatibility during transition
|
|
111
|
+
if (steps && typeof steps === 'object') {
|
|
112
|
+
return Object.keys(steps);
|
|
113
|
+
}
|
|
114
|
+
throw new Error('[CookbookHelpers.getStepIds] steps must be an array (V2.1 format)');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return steps.map(step => step.step_id);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get the first step (for workflow initiation)
|
|
122
|
+
*
|
|
123
|
+
* @param {Array} steps - Array of step objects
|
|
124
|
+
* @returns {Object|null} The first step or null if empty
|
|
125
|
+
* @throws {Error} If steps is not an array
|
|
126
|
+
*/
|
|
127
|
+
static getFirstStep(steps) {
|
|
128
|
+
if (!Array.isArray(steps)) {
|
|
129
|
+
// Handle V2.0 object format for backwards compatibility during transition
|
|
130
|
+
if (steps && typeof steps === 'object') {
|
|
131
|
+
const keys = Object.keys(steps);
|
|
132
|
+
return keys.length > 0 ? steps[keys[0]] : null;
|
|
133
|
+
}
|
|
134
|
+
throw new Error('[CookbookHelpers.getFirstStep] steps must be an array (V2.1 format)');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return steps.length > 0 ? steps[0] : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get the next step after a given step_id
|
|
142
|
+
*
|
|
143
|
+
* @param {Array} steps - Array of step objects
|
|
144
|
+
* @param {string} currentStepId - The current step_id
|
|
145
|
+
* @returns {Object|null} The next step or null if last/not found
|
|
146
|
+
* @throws {Error} If steps is not an array
|
|
147
|
+
*/
|
|
148
|
+
static getNextStep(steps, currentStepId) {
|
|
149
|
+
if (!Array.isArray(steps)) {
|
|
150
|
+
// Handle V2.0 object format for backwards compatibility during transition
|
|
151
|
+
if (steps && typeof steps === 'object') {
|
|
152
|
+
const keys = Object.keys(steps);
|
|
153
|
+
const currentIndex = keys.indexOf(currentStepId);
|
|
154
|
+
if (currentIndex === -1 || currentIndex >= keys.length - 1) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return steps[keys[currentIndex + 1]];
|
|
158
|
+
}
|
|
159
|
+
throw new Error('[CookbookHelpers.getNextStep] steps must be an array (V2.1 format)');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const currentIndex = steps.findIndex(step => step.step_id === currentStepId);
|
|
163
|
+
if (currentIndex === -1 || currentIndex >= steps.length - 1) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
return steps[currentIndex + 1];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get total step count
|
|
171
|
+
*
|
|
172
|
+
* @param {Array} steps - Array of step objects
|
|
173
|
+
* @returns {number} Number of steps
|
|
174
|
+
*/
|
|
175
|
+
static getStepCount(steps) {
|
|
176
|
+
if (!Array.isArray(steps)) {
|
|
177
|
+
// Handle V2.0 object format for backwards compatibility during transition
|
|
178
|
+
if (steps && typeof steps === 'object') {
|
|
179
|
+
return Object.keys(steps).length;
|
|
180
|
+
}
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return steps.length;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if a step_id exists
|
|
189
|
+
*
|
|
190
|
+
* @param {Array} steps - Array of step objects
|
|
191
|
+
* @param {string} stepId - The step_id to check
|
|
192
|
+
* @returns {boolean} True if step exists
|
|
193
|
+
*/
|
|
194
|
+
static hasStep(steps, stepId) {
|
|
195
|
+
return this.findStep(steps, stepId) !== null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Filter steps by a predicate
|
|
200
|
+
*
|
|
201
|
+
* @param {Array} steps - Array of step objects
|
|
202
|
+
* @param {Function} predicate - Function(step, stepId) => boolean
|
|
203
|
+
* @returns {Array} Filtered steps
|
|
204
|
+
*/
|
|
205
|
+
static filterSteps(steps, predicate) {
|
|
206
|
+
if (!Array.isArray(steps)) {
|
|
207
|
+
// Handle V2.0 object format for backwards compatibility during transition
|
|
208
|
+
if (steps && typeof steps === 'object') {
|
|
209
|
+
return Object.entries(steps)
|
|
210
|
+
.filter(([stepId, step]) => predicate(step, stepId))
|
|
211
|
+
.map(([_, step]) => step);
|
|
212
|
+
}
|
|
213
|
+
throw new Error('[CookbookHelpers.filterSteps] steps must be an array (V2.1 format)');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return steps.filter(step => predicate(step, step.step_id));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Check if steps format is V2.1 (array)
|
|
221
|
+
*
|
|
222
|
+
* @param {any} steps - Steps to check
|
|
223
|
+
* @returns {boolean} True if V2.1 array format
|
|
224
|
+
*/
|
|
225
|
+
static isV21Format(steps) {
|
|
226
|
+
return Array.isArray(steps);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if steps format is V2.0 (object)
|
|
231
|
+
*
|
|
232
|
+
* @param {any} steps - Steps to check
|
|
233
|
+
* @returns {boolean} True if V2.0 object format
|
|
234
|
+
*/
|
|
235
|
+
static isV20Format(steps) {
|
|
236
|
+
return steps && typeof steps === 'object' && !Array.isArray(steps);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Convert V2.0 object format to V2.1 array format
|
|
241
|
+
*
|
|
242
|
+
* @param {Object} stepsObject - V2.0 steps object
|
|
243
|
+
* @returns {Array} V2.1 steps array
|
|
244
|
+
*/
|
|
245
|
+
static convertV20ToV21(stepsObject) {
|
|
246
|
+
if (Array.isArray(stepsObject)) {
|
|
247
|
+
return stepsObject; // Already V2.1
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!stepsObject || typeof stepsObject !== 'object') {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return Object.entries(stepsObject).map(([stepId, step]) => ({
|
|
255
|
+
...step,
|
|
256
|
+
step_id: step.step_id || stepId // Ensure step_id is set
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Export both class and individual functions for convenience
|
|
262
|
+
module.exports = {
|
|
263
|
+
CookbookHelpers,
|
|
264
|
+
findStep: CookbookHelpers.findStep.bind(CookbookHelpers),
|
|
265
|
+
forEachStep: CookbookHelpers.forEachStep.bind(CookbookHelpers),
|
|
266
|
+
mapSteps: CookbookHelpers.mapSteps.bind(CookbookHelpers),
|
|
267
|
+
getStepIds: CookbookHelpers.getStepIds.bind(CookbookHelpers),
|
|
268
|
+
getFirstStep: CookbookHelpers.getFirstStep.bind(CookbookHelpers),
|
|
269
|
+
getNextStep: CookbookHelpers.getNextStep.bind(CookbookHelpers),
|
|
270
|
+
getStepCount: CookbookHelpers.getStepCount.bind(CookbookHelpers),
|
|
271
|
+
hasStep: CookbookHelpers.hasStep.bind(CookbookHelpers),
|
|
272
|
+
filterSteps: CookbookHelpers.filterSteps.bind(CookbookHelpers),
|
|
273
|
+
isV21Format: CookbookHelpers.isV21Format.bind(CookbookHelpers),
|
|
274
|
+
isV20Format: CookbookHelpers.isV20Format.bind(CookbookHelpers),
|
|
275
|
+
convertV20ToV21: CookbookHelpers.convertV20ToV21.bind(CookbookHelpers)
|
|
276
|
+
};
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Core cookbook parsing and validation library.
|
|
7
7
|
* Lightweight foundation for workflow definitions - no heavy dependencies.
|
|
8
|
+
*
|
|
9
|
+
* V2.1 FORMAT:
|
|
10
|
+
* - steps is an ARRAY (guaranteed order)
|
|
11
|
+
* - Access via step_id ONLY (use findStep, forEachStep)
|
|
12
|
+
* - Index access is FORBIDDEN
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
const {
|
|
@@ -33,6 +38,23 @@ const {
|
|
|
33
38
|
validateErrorHandlerReferences
|
|
34
39
|
} = require('./referenceValidator');
|
|
35
40
|
|
|
41
|
+
// V2.1 Helper functions for safe step access
|
|
42
|
+
const {
|
|
43
|
+
CookbookHelpers,
|
|
44
|
+
findStep,
|
|
45
|
+
forEachStep,
|
|
46
|
+
mapSteps,
|
|
47
|
+
getStepIds,
|
|
48
|
+
getFirstStep,
|
|
49
|
+
getNextStep,
|
|
50
|
+
getStepCount,
|
|
51
|
+
hasStep,
|
|
52
|
+
filterSteps,
|
|
53
|
+
isV21Format,
|
|
54
|
+
isV20Format,
|
|
55
|
+
convertV20ToV21
|
|
56
|
+
} = require('./CookbookHelpers');
|
|
57
|
+
|
|
36
58
|
module.exports = {
|
|
37
59
|
// Parser functions
|
|
38
60
|
parseCookbookFromFile,
|
|
@@ -58,7 +80,22 @@ module.exports = {
|
|
|
58
80
|
// Error classes
|
|
59
81
|
CookbookValidationError,
|
|
60
82
|
|
|
83
|
+
// V2.1 Helper class and functions
|
|
84
|
+
CookbookHelpers,
|
|
85
|
+
findStep,
|
|
86
|
+
forEachStep,
|
|
87
|
+
mapSteps,
|
|
88
|
+
getStepIds,
|
|
89
|
+
getFirstStep,
|
|
90
|
+
getNextStep,
|
|
91
|
+
getStepCount,
|
|
92
|
+
hasStep,
|
|
93
|
+
filterSteps,
|
|
94
|
+
isV21Format,
|
|
95
|
+
isV20Format,
|
|
96
|
+
convertV20ToV21,
|
|
97
|
+
|
|
61
98
|
// Utility exports
|
|
62
|
-
VERSION: '2.
|
|
99
|
+
VERSION: '2.1.0',
|
|
63
100
|
SCHEMA_VERSION: '2.1.0'
|
|
64
101
|
};
|
|
@@ -35,11 +35,11 @@ const loadSchemaFile = (filename) => {
|
|
|
35
35
|
if (fs.existsSync(schemaPath)) {
|
|
36
36
|
return JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
|
|
37
37
|
}
|
|
38
|
-
// Fallback to default schema
|
|
39
|
-
return JSON.parse(fs.readFileSync(path.join(__dirname, '../../schemas/cookbook.schema.json'), 'utf-8'));
|
|
38
|
+
// Fallback to default v2 schema
|
|
39
|
+
return JSON.parse(fs.readFileSync(path.join(__dirname, '../../schemas/cookbook.v2.schema.json'), 'utf-8'));
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
const cookbookSchema = loadSchemaFile('cookbook.schema.json');
|
|
42
|
+
const cookbookSchema = loadSchemaFile('cookbook.v2.schema.json');
|
|
43
43
|
const strictSchema = loadSchemaFile('cookbook.strict.schema.json');
|
|
44
44
|
const relaxedSchema = loadSchemaFile('cookbook.relaxed.schema.json');
|
|
45
45
|
|
|
@@ -29,18 +29,27 @@ class CookbookValidationError extends Error {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Load V2 schema - V1
|
|
32
|
+
* Load V2.1 schema - V1 and V2.0 are BANNED
|
|
33
33
|
*/
|
|
34
|
-
function loadSchema(version = '2.
|
|
34
|
+
function loadSchema(version = '2.1') {
|
|
35
35
|
// V1 is BANNED - reject immediately
|
|
36
36
|
if (version.startsWith('1.')) {
|
|
37
|
-
throw new CookbookValidationError(
|
|
37
|
+
throw new CookbookValidationError(
|
|
38
|
+
'FAIL-FAST: V1 FORMAT BANNED! Use version 2.1.x with array steps.'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// V2.0 is BANNED - reject immediately
|
|
43
|
+
if (version === '2.0') {
|
|
44
|
+
throw new CookbookValidationError(
|
|
45
|
+
'FAIL-FAST: V2.0 FORMAT BANNED! Use version 2.1.x with array steps (not object).'
|
|
46
|
+
);
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
const schemaPath = path.join(__dirname, '../../schemas/cookbook.schema.json');
|
|
49
|
+
const schemaPath = path.join(__dirname, '../../schemas/cookbook.v2.schema.json');
|
|
41
50
|
|
|
42
51
|
if (!fs.existsSync(schemaPath)) {
|
|
43
|
-
throw new CookbookValidationError('Schema file not found: cookbook.schema.json');
|
|
52
|
+
throw new CookbookValidationError('Schema file not found: cookbook.v2.schema.json');
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
return JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
|
|
@@ -113,30 +122,39 @@ function formatErrors(errors) {
|
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
/**
|
|
116
|
-
* Detect cookbook version
|
|
125
|
+
* Detect cookbook version
|
|
126
|
+
*
|
|
127
|
+
* V1.x - BANNED (array steps with 'id')
|
|
128
|
+
* V2.0 - BANNED (object steps keyed by step_id)
|
|
129
|
+
* V2.1 - REQUIRED (array steps with 'step_id')
|
|
117
130
|
*/
|
|
118
131
|
function detectVersion(cookbook) {
|
|
119
132
|
// Check version field first
|
|
120
133
|
if (cookbook.version) {
|
|
121
|
-
const major = cookbook.version.split('.')
|
|
122
|
-
if (major ===
|
|
123
|
-
if (major ===
|
|
134
|
+
const [major, minor] = cookbook.version.split('.').map(Number);
|
|
135
|
+
if (major === 1) return '1.0';
|
|
136
|
+
if (major === 2 && minor >= 1) return '2.1';
|
|
137
|
+
if (major === 2) return '2.0';
|
|
124
138
|
}
|
|
125
139
|
|
|
126
|
-
// Auto-detect based on
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
// Auto-detect based on steps format
|
|
141
|
+
if (cookbook.steps) {
|
|
142
|
+
// V2.1: steps is array with step_id
|
|
143
|
+
if (Array.isArray(cookbook.steps)) {
|
|
144
|
+
const hasStepId = cookbook.steps.length === 0 || cookbook.steps.every(s => s.step_id);
|
|
145
|
+
const hasId = cookbook.steps.some(s => s.id && !s.step_id);
|
|
146
|
+
|
|
147
|
+
if (hasId) return '1.0'; // V1 format (array with 'id')
|
|
148
|
+
if (hasStepId) return '2.1'; // V2.1 format (array with 'step_id')
|
|
149
|
+
}
|
|
150
|
+
// V2.0: steps is object
|
|
151
|
+
if (typeof cookbook.steps === 'object' && !Array.isArray(cookbook.steps)) {
|
|
152
|
+
return '2.0';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
137
155
|
|
|
138
|
-
// Default to v2.
|
|
139
|
-
return '2.
|
|
156
|
+
// Default to v2.1 for new cookbooks
|
|
157
|
+
return '2.1';
|
|
140
158
|
}
|
|
141
159
|
|
|
142
160
|
/**
|
|
@@ -179,6 +197,8 @@ function checkMigration(cookbook, detectedVersion) {
|
|
|
179
197
|
/**
|
|
180
198
|
* Main validation function
|
|
181
199
|
*
|
|
200
|
+
* V2.1 ONLY - V1 and V2.0 are BANNED
|
|
201
|
+
*
|
|
182
202
|
* @param {object} cookbook - The cookbook object to validate
|
|
183
203
|
* @param {object} options - Validation options
|
|
184
204
|
* @param {string} options.version - Force specific version validation
|
|
@@ -196,11 +216,34 @@ function validateCookbook(cookbook, options = {}) {
|
|
|
196
216
|
// Detect version if not specified
|
|
197
217
|
const cookbookVersion = version || detectVersion(cookbook);
|
|
198
218
|
|
|
199
|
-
//
|
|
219
|
+
// STRICT: Reject V1 and V2.0
|
|
220
|
+
if (cookbookVersion === '1.0') {
|
|
221
|
+
throw new CookbookValidationError(
|
|
222
|
+
'FAIL-FAST: V1 format (1.x.x) is BANNED. ' +
|
|
223
|
+
'Migrate to V2.1 format:\n' +
|
|
224
|
+
' - Change version to "2.1.0"\n' +
|
|
225
|
+
' - Keep steps as array\n' +
|
|
226
|
+
' - Rename "id" to "step_id" in each step\n' +
|
|
227
|
+
' - Access steps via findStep(stepId), not by index'
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (cookbookVersion === '2.0') {
|
|
232
|
+
throw new CookbookValidationError(
|
|
233
|
+
'FAIL-FAST: V2.0 format (object steps) is BANNED. ' +
|
|
234
|
+
'Migrate to V2.1 format:\n' +
|
|
235
|
+
' - Change version to "2.1.0"\n' +
|
|
236
|
+
' - Convert steps from object to array\n' +
|
|
237
|
+
' - Each step must have step_id property\n' +
|
|
238
|
+
' - Access steps via findStep(stepId), not by key'
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check if migration needed (for warnings)
|
|
200
243
|
checkMigration(cookbook, cookbookVersion);
|
|
201
244
|
|
|
202
|
-
// Load
|
|
203
|
-
const schema = loadSchema(
|
|
245
|
+
// Load V2.1 schema
|
|
246
|
+
const schema = loadSchema('2.1');
|
|
204
247
|
|
|
205
248
|
// Create validator
|
|
206
249
|
const validate = createValidator(schema);
|
|
@@ -222,7 +265,7 @@ function validateCookbook(cookbook, options = {}) {
|
|
|
222
265
|
}
|
|
223
266
|
}
|
|
224
267
|
|
|
225
|
-
// Additional semantic validation
|
|
268
|
+
// Additional semantic validation
|
|
226
269
|
validateSemantics(cookbook, cookbookVersion);
|
|
227
270
|
|
|
228
271
|
return true;
|
|
@@ -230,66 +273,108 @@ function validateCookbook(cookbook, options = {}) {
|
|
|
230
273
|
|
|
231
274
|
/**
|
|
232
275
|
* Semantic validation for things JSON Schema can't easily express
|
|
276
|
+
*
|
|
277
|
+
* V2.1 REQUIREMENTS:
|
|
278
|
+
* - steps MUST be an array
|
|
279
|
+
* - Each step MUST have step_id
|
|
280
|
+
* - No index-based access allowed
|
|
233
281
|
*/
|
|
234
282
|
function validateSemantics(cookbook, version) {
|
|
235
283
|
const stepIds = new Set();
|
|
236
284
|
|
|
237
|
-
// Check for duplicate step_ids (or ids in v1)
|
|
238
|
-
const idField = version === '1.0' ? 'id' : 'step_id';
|
|
239
|
-
|
|
240
285
|
function checkStep(step, path) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
stepIds.add(stepId);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Check nested steps - V2: all nested steps are objects keyed by step_id
|
|
252
|
-
if (step.steps && typeof step.steps === 'object') {
|
|
253
|
-
Object.entries(step.steps).forEach(([nestedKey, nested]) =>
|
|
254
|
-
checkStep(nested, `${path}.steps.${nestedKey}`)
|
|
286
|
+
// V2.1: step_id is REQUIRED
|
|
287
|
+
const stepId = step.step_id;
|
|
288
|
+
if (!stepId) {
|
|
289
|
+
throw new CookbookValidationError(
|
|
290
|
+
`Missing required 'step_id' at ${path}. V2.1 requires step_id for all steps.`
|
|
255
291
|
);
|
|
256
292
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
293
|
+
|
|
294
|
+
if (stepIds.has(stepId)) {
|
|
295
|
+
throw new CookbookValidationError(
|
|
296
|
+
`Duplicate step_id '${stepId}' found at ${path}`
|
|
260
297
|
);
|
|
261
298
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
299
|
+
stepIds.add(stepId);
|
|
300
|
+
|
|
301
|
+
// Check nested steps - V2.1: nested steps are also arrays
|
|
302
|
+
if (step.steps) {
|
|
303
|
+
if (Array.isArray(step.steps)) {
|
|
304
|
+
step.steps.forEach((nested, idx) =>
|
|
305
|
+
checkStep(nested, `${path}.steps[${idx}]`)
|
|
306
|
+
);
|
|
307
|
+
} else if (typeof step.steps === 'object') {
|
|
308
|
+
// Allow object format for nested steps (backwards compat during transition)
|
|
309
|
+
Object.entries(step.steps).forEach(([nestedKey, nested]) =>
|
|
310
|
+
checkStep(nested, `${path}.steps.${nestedKey}`)
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (step.body) {
|
|
316
|
+
if (Array.isArray(step.body)) {
|
|
317
|
+
step.body.forEach((nested, idx) =>
|
|
318
|
+
checkStep(nested, `${path}.body[${idx}]`)
|
|
319
|
+
);
|
|
320
|
+
} else if (typeof step.body === 'object') {
|
|
321
|
+
Object.entries(step.body).forEach(([nestedKey, nested]) =>
|
|
322
|
+
checkStep(nested, `${path}.body.${nestedKey}`)
|
|
323
|
+
);
|
|
324
|
+
}
|
|
266
325
|
}
|
|
326
|
+
|
|
327
|
+
if (step.branches) {
|
|
328
|
+
if (Array.isArray(step.branches)) {
|
|
329
|
+
step.branches.forEach((branch, idx) => {
|
|
330
|
+
if (branch.steps) {
|
|
331
|
+
if (Array.isArray(branch.steps)) {
|
|
332
|
+
branch.steps.forEach((nested, nidx) =>
|
|
333
|
+
checkStep(nested, `${path}.branches[${idx}].steps[${nidx}]`)
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
} else if (typeof step.branches === 'object') {
|
|
339
|
+
Object.entries(step.branches).forEach(([branchKey, branch]) => {
|
|
340
|
+
checkStep(branch, `${path}.branches.${branchKey}`);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
267
345
|
if (step.cases) {
|
|
268
346
|
Object.keys(step.cases).forEach(caseKey => {
|
|
269
347
|
const caseVal = step.cases[caseKey];
|
|
270
|
-
|
|
348
|
+
if (caseVal && typeof caseVal === 'object') {
|
|
349
|
+
checkStep(caseVal, `${path}.cases.${caseKey}`);
|
|
350
|
+
}
|
|
271
351
|
});
|
|
272
352
|
}
|
|
273
353
|
}
|
|
274
354
|
|
|
275
|
-
//
|
|
355
|
+
// V2.1: steps MUST be an array
|
|
276
356
|
if (cookbook.steps) {
|
|
277
|
-
if (Array.isArray(cookbook.steps)) {
|
|
278
|
-
throw new CookbookValidationError(
|
|
357
|
+
if (!Array.isArray(cookbook.steps)) {
|
|
358
|
+
throw new CookbookValidationError(
|
|
359
|
+
'FAIL-FAST: V2.0 format (object steps) is DEPRECATED. ' +
|
|
360
|
+
'Use V2.1 format with steps as an ARRAY. ' +
|
|
361
|
+
'Each step must have step_id. Access steps via findStep(stepId), not by index.'
|
|
362
|
+
);
|
|
279
363
|
}
|
|
280
|
-
|
|
281
|
-
|
|
364
|
+
|
|
365
|
+
cookbook.steps.forEach((step, idx) =>
|
|
366
|
+
checkStep(step, `steps[${idx}]`)
|
|
282
367
|
);
|
|
283
368
|
}
|
|
284
369
|
|
|
285
370
|
// Check dependencies exist
|
|
286
|
-
if (
|
|
287
|
-
|
|
371
|
+
if (cookbook.steps && Array.isArray(cookbook.steps)) {
|
|
372
|
+
cookbook.steps.forEach((step) => {
|
|
288
373
|
if (step.depends_on) {
|
|
289
374
|
step.depends_on.forEach(dep => {
|
|
290
375
|
if (!stepIds.has(dep)) {
|
|
291
376
|
throw new CookbookValidationError(
|
|
292
|
-
`Step '${
|
|
377
|
+
`Step '${step.step_id}' depends on non-existent step_id '${dep}'`
|
|
293
378
|
);
|
|
294
379
|
}
|
|
295
380
|
});
|
package/src/schema.js
CHANGED
|
@@ -22,15 +22,15 @@ function loadSchema(mode = 'default') {
|
|
|
22
22
|
schemaFile = 'cookbook.relaxed.schema.json';
|
|
23
23
|
break;
|
|
24
24
|
default:
|
|
25
|
-
schemaFile = 'cookbook.schema.json';
|
|
25
|
+
schemaFile = 'cookbook.v2.schema.json';
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const schemaPath = path.join(__dirname, '../schemas', schemaFile);
|
|
29
29
|
|
|
30
30
|
// Check if file exists
|
|
31
31
|
if (!fs.existsSync(schemaPath)) {
|
|
32
|
-
// Fall back to default schema
|
|
33
|
-
const defaultPath = path.join(__dirname, '../schemas/cookbook.schema.json');
|
|
32
|
+
// Fall back to default v2 schema
|
|
33
|
+
const defaultPath = path.join(__dirname, '../schemas/cookbook.v2.schema.json');
|
|
34
34
|
return JSON.parse(fs.readFileSync(defaultPath, 'utf-8'));
|
|
35
35
|
}
|
|
36
36
|
|
|
File without changes
|