jtcsv 2.1.3 → 2.2.2
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/LICENSE +1 -1
- package/README.md +60 -341
- package/bin/jtcsv.js +2462 -1372
- package/csv-to-json.js +35 -26
- package/dist/jtcsv.cjs.js +807 -133
- package/dist/jtcsv.cjs.js.map +1 -1
- package/dist/jtcsv.esm.js +800 -134
- package/dist/jtcsv.esm.js.map +1 -1
- package/dist/jtcsv.umd.js +807 -133
- package/dist/jtcsv.umd.js.map +1 -1
- package/errors.js +20 -0
- package/examples/browser-vanilla.html +37 -0
- package/examples/cli-batch-processing.js +38 -0
- package/examples/error-handling.js +324 -0
- package/examples/ndjson-processing.js +434 -0
- package/examples/react-integration.jsx +637 -0
- package/examples/schema-validation.js +640 -0
- package/examples/simple-usage.js +10 -7
- package/examples/typescript-example.ts +486 -0
- package/examples/web-workers-advanced.js +28 -0
- package/index.d.ts +2 -0
- package/json-save.js +2 -1
- package/json-to-csv.js +171 -131
- package/package.json +20 -4
- package/plugins/README.md +41 -467
- package/plugins/express-middleware/README.md +32 -274
- package/plugins/hono/README.md +16 -13
- package/plugins/nestjs/README.md +13 -11
- package/plugins/nextjs-api/README.md +28 -423
- package/plugins/nextjs-api/index.js +1 -2
- package/plugins/nextjs-api/route.js +1 -2
- package/plugins/nuxt/README.md +6 -7
- package/plugins/remix/README.md +9 -9
- package/plugins/sveltekit/README.md +8 -8
- package/plugins/trpc/README.md +8 -5
- package/src/browser/browser-functions.js +33 -3
- package/src/browser/csv-to-json-browser.js +269 -11
- package/src/browser/errors-browser.js +19 -1
- package/src/browser/index.js +39 -5
- package/src/browser/streams.js +393 -0
- package/src/browser/workers/csv-parser.worker.js +20 -2
- package/src/browser/workers/worker-pool.js +507 -447
- package/src/core/plugin-system.js +4 -0
- package/src/engines/fast-path-engine.js +31 -23
- package/src/errors.js +26 -0
- package/src/formats/ndjson-parser.js +54 -5
- package/src/formats/tsv-parser.js +4 -1
- package/src/utils/schema-validator.js +594 -0
- package/src/utils/transform-loader.js +205 -0
- package/src/web-server/index.js +683 -0
- package/stream-csv-to-json.js +16 -87
- package/stream-json-to-csv.js +18 -86
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Validator Utility
|
|
3
|
+
*
|
|
4
|
+
* Utility for loading and applying JSON schema validation in CLI
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
ValidationError,
|
|
12
|
+
SecurityError,
|
|
13
|
+
ConfigurationError
|
|
14
|
+
} = require('../errors');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Loads JSON schema from file or string
|
|
18
|
+
*
|
|
19
|
+
* @param {string} schemaPathOrJson - Path to JSON file or JSON string
|
|
20
|
+
* @returns {Object} Parsed JSON schema
|
|
21
|
+
*/
|
|
22
|
+
function loadSchema(schemaPathOrJson) {
|
|
23
|
+
if (!schemaPathOrJson || typeof schemaPathOrJson !== 'string') {
|
|
24
|
+
throw new ValidationError('Schema must be a string (JSON or file path)');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let schemaString = schemaPathOrJson;
|
|
28
|
+
|
|
29
|
+
// Check if it's a file path (ends with .json or contains path separators)
|
|
30
|
+
const isFilePath = schemaPathOrJson.endsWith('.json') ||
|
|
31
|
+
schemaPathOrJson.includes('/') ||
|
|
32
|
+
schemaPathOrJson.includes('\\');
|
|
33
|
+
|
|
34
|
+
if (isFilePath) {
|
|
35
|
+
// Validate file path
|
|
36
|
+
const safePath = path.resolve(schemaPathOrJson);
|
|
37
|
+
|
|
38
|
+
// Prevent directory traversal
|
|
39
|
+
const normalizedPath = path.normalize(schemaPathOrJson);
|
|
40
|
+
if (normalizedPath.includes('..') ||
|
|
41
|
+
/\\\.\.\\|\/\.\.\//.test(schemaPathOrJson) ||
|
|
42
|
+
schemaPathOrJson.startsWith('..') ||
|
|
43
|
+
schemaPathOrJson.includes('/..')) {
|
|
44
|
+
throw new SecurityError('Directory traversal detected in schema file path');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check file exists and has .json extension
|
|
48
|
+
if (!fs.existsSync(safePath)) {
|
|
49
|
+
throw new ValidationError(`Schema file not found: ${schemaPathOrJson}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!safePath.toLowerCase().endsWith('.json')) {
|
|
53
|
+
throw new ValidationError('Schema file must have .json extension');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
schemaString = fs.readFileSync(safePath, 'utf8');
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error.code === 'EACCES') {
|
|
60
|
+
throw new SecurityError(`Permission denied reading schema file: ${schemaPathOrJson}`);
|
|
61
|
+
}
|
|
62
|
+
throw new ValidationError(`Failed to read schema file: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Parse JSON schema
|
|
67
|
+
try {
|
|
68
|
+
const schema = JSON.parse(schemaString);
|
|
69
|
+
|
|
70
|
+
// Validate basic schema structure
|
|
71
|
+
if (typeof schema !== 'object' || schema === null) {
|
|
72
|
+
throw new ValidationError('Schema must be a JSON object');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return schema;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof SyntaxError) {
|
|
78
|
+
throw new ValidationError(`Invalid JSON in schema: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
throw new ValidationError(`Failed to parse schema: ${error.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Creates a validation hook for use with csvToJson/jsonToCsv hooks system
|
|
86
|
+
*
|
|
87
|
+
* @param {string|Object} schema - Schema object or path to schema file
|
|
88
|
+
* @returns {Function} Validation hook function
|
|
89
|
+
*/
|
|
90
|
+
function createValidationHook(schema) {
|
|
91
|
+
let schemaObj;
|
|
92
|
+
|
|
93
|
+
if (typeof schema === 'string') {
|
|
94
|
+
// Load schema from file or JSON string
|
|
95
|
+
schemaObj = loadSchema(schema);
|
|
96
|
+
} else if (typeof schema === 'object' && schema !== null) {
|
|
97
|
+
// Use provided schema object
|
|
98
|
+
schemaObj = schema;
|
|
99
|
+
} else {
|
|
100
|
+
throw new ValidationError('Schema must be an object or a path to a JSON file');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Try to use @jtcsv/validator if available
|
|
104
|
+
let validator;
|
|
105
|
+
try {
|
|
106
|
+
const JtcsvValidator = require('../../packages/jtcsv-validator/src/index');
|
|
107
|
+
validator = new JtcsvValidator();
|
|
108
|
+
|
|
109
|
+
// Convert simple schema format to validator format
|
|
110
|
+
if (schemaObj.fields) {
|
|
111
|
+
// Assume it's already in validator format
|
|
112
|
+
validator.schema(schemaObj.fields);
|
|
113
|
+
} else {
|
|
114
|
+
// Convert simple field definitions
|
|
115
|
+
Object.entries(schemaObj).forEach(([field, rule]) => {
|
|
116
|
+
if (typeof rule === 'object') {
|
|
117
|
+
validator.field(field, rule);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
// Fallback to simple validation if validator is not available
|
|
123
|
+
console.warn('@jtcsv/validator not available, using simple validation');
|
|
124
|
+
validator = createSimpleValidator(schemaObj);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Return a hook function compatible with hooks.perRow
|
|
128
|
+
return function(row, index, context) {
|
|
129
|
+
try {
|
|
130
|
+
const result = validator.validate([row], {
|
|
131
|
+
stopOnFirstError: true,
|
|
132
|
+
transform: false
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (!result.valid && result.errors.length > 0) {
|
|
136
|
+
const error = result.errors[0];
|
|
137
|
+
throw new ValidationError(
|
|
138
|
+
`Row ${index + 1}: ${error.message} (field: ${error.field})`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return row;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (error instanceof ValidationError) {
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
// Log error but don't crash - return original row
|
|
148
|
+
console.error(`Validation error at row ${index}: ${error.message}`);
|
|
149
|
+
if (process.env.NODE_ENV === 'development') {
|
|
150
|
+
console.error(error.stack);
|
|
151
|
+
}
|
|
152
|
+
return row;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Creates a simple validator for fallback when @jtcsv/validator is not available
|
|
159
|
+
*
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
162
|
+
function createSimpleValidator(schema) {
|
|
163
|
+
return {
|
|
164
|
+
validate(data, options = {}) {
|
|
165
|
+
const errors = [];
|
|
166
|
+
const warnings = [];
|
|
167
|
+
|
|
168
|
+
if (!Array.isArray(data)) {
|
|
169
|
+
return {
|
|
170
|
+
valid: false,
|
|
171
|
+
errors: [{ type: 'INVALID_DATA', message: 'Data must be an array' }],
|
|
172
|
+
warnings: [],
|
|
173
|
+
summary: {
|
|
174
|
+
totalRows: 0,
|
|
175
|
+
validRows: 0,
|
|
176
|
+
errorCount: 1,
|
|
177
|
+
warningCount: 0
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (let i = 0; i < data.length; i++) {
|
|
183
|
+
const row = data[i];
|
|
184
|
+
|
|
185
|
+
for (const [field, rule] of Object.entries(schema)) {
|
|
186
|
+
const value = row[field];
|
|
187
|
+
|
|
188
|
+
// Check required
|
|
189
|
+
if (rule.required && (value === undefined || value === null || value === '')) {
|
|
190
|
+
errors.push({
|
|
191
|
+
row: i + 1,
|
|
192
|
+
type: 'REQUIRED',
|
|
193
|
+
field,
|
|
194
|
+
message: `Field "${field}" is required`,
|
|
195
|
+
value
|
|
196
|
+
});
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Skip further validation if value is empty and not required
|
|
201
|
+
if (value === undefined || value === null || value === '') {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check type
|
|
206
|
+
if (rule.type) {
|
|
207
|
+
const types = Array.isArray(rule.type) ? rule.type : [rule.type];
|
|
208
|
+
let typeValid = false;
|
|
209
|
+
|
|
210
|
+
for (const type of types) {
|
|
211
|
+
if (checkType(value, type)) {
|
|
212
|
+
typeValid = true;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!typeValid) {
|
|
218
|
+
errors.push({
|
|
219
|
+
row: i + 1,
|
|
220
|
+
type: 'TYPE',
|
|
221
|
+
field,
|
|
222
|
+
message: `Field "${field}" must be of type ${types.join(' or ')}`,
|
|
223
|
+
value,
|
|
224
|
+
expected: types
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check min/max for strings
|
|
230
|
+
if (rule.min !== undefined && typeof value === 'string' && value.length < rule.min) {
|
|
231
|
+
errors.push({
|
|
232
|
+
row: i + 1,
|
|
233
|
+
type: 'MIN_LENGTH',
|
|
234
|
+
field,
|
|
235
|
+
message: `Field "${field}" must be at least ${rule.min} characters`,
|
|
236
|
+
value,
|
|
237
|
+
min: rule.min
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (rule.max !== undefined && typeof value === 'string' && value.length > rule.max) {
|
|
242
|
+
errors.push({
|
|
243
|
+
row: i + 1,
|
|
244
|
+
type: 'MAX_LENGTH',
|
|
245
|
+
field,
|
|
246
|
+
message: `Field "${field}" must be at most ${rule.max} characters`,
|
|
247
|
+
value,
|
|
248
|
+
max: rule.max
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Check min/max for numbers
|
|
253
|
+
if (rule.min !== undefined && typeof value === 'number' && value < rule.min) {
|
|
254
|
+
errors.push({
|
|
255
|
+
row: i + 1,
|
|
256
|
+
type: 'MIN_VALUE',
|
|
257
|
+
field,
|
|
258
|
+
message: `Field "${field}" must be at least ${rule.min}`,
|
|
259
|
+
value,
|
|
260
|
+
min: rule.min
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (rule.max !== undefined && typeof value === 'number' && value > rule.max) {
|
|
265
|
+
errors.push({
|
|
266
|
+
row: i + 1,
|
|
267
|
+
type: 'MAX_VALUE',
|
|
268
|
+
field,
|
|
269
|
+
message: `Field "${field}" must be at most ${rule.max}`,
|
|
270
|
+
value,
|
|
271
|
+
max: rule.max
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check pattern
|
|
276
|
+
if (rule.pattern && typeof value === 'string') {
|
|
277
|
+
const pattern = rule.pattern instanceof RegExp ? rule.pattern : new RegExp(rule.pattern);
|
|
278
|
+
if (!pattern.test(value)) {
|
|
279
|
+
errors.push({
|
|
280
|
+
row: i + 1,
|
|
281
|
+
type: 'PATTERN',
|
|
282
|
+
field,
|
|
283
|
+
message: `Field "${field}" must match pattern`,
|
|
284
|
+
value,
|
|
285
|
+
pattern: pattern.toString()
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Check enum
|
|
291
|
+
if (rule.enum && Array.isArray(rule.enum) && !rule.enum.includes(value)) {
|
|
292
|
+
errors.push({
|
|
293
|
+
row: i + 1,
|
|
294
|
+
type: 'ENUM',
|
|
295
|
+
field,
|
|
296
|
+
message: `Field "${field}" must be one of: ${rule.enum.join(', ')}`,
|
|
297
|
+
value,
|
|
298
|
+
allowed: rule.enum
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
valid: errors.length === 0,
|
|
306
|
+
errors,
|
|
307
|
+
warnings,
|
|
308
|
+
summary: {
|
|
309
|
+
totalRows: data.length,
|
|
310
|
+
validRows: data.length - errors.length,
|
|
311
|
+
errorCount: errors.length,
|
|
312
|
+
warningCount: warnings.length
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Checks if value matches type
|
|
321
|
+
*
|
|
322
|
+
* @private
|
|
323
|
+
*/
|
|
324
|
+
function checkType(value, type) {
|
|
325
|
+
switch (type) {
|
|
326
|
+
case 'string':
|
|
327
|
+
return typeof value === 'string';
|
|
328
|
+
case 'number':
|
|
329
|
+
return typeof value === 'number' && !isNaN(value);
|
|
330
|
+
case 'boolean':
|
|
331
|
+
return typeof value === 'boolean';
|
|
332
|
+
case 'integer':
|
|
333
|
+
return Number.isInteger(value);
|
|
334
|
+
case 'float':
|
|
335
|
+
return typeof value === 'number' && !Number.isInteger(value);
|
|
336
|
+
case 'date':
|
|
337
|
+
return value instanceof Date && !isNaN(value);
|
|
338
|
+
case 'array':
|
|
339
|
+
return Array.isArray(value);
|
|
340
|
+
case 'object':
|
|
341
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
342
|
+
default:
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Applies schema validation to data array
|
|
349
|
+
*
|
|
350
|
+
* @param {Array} data - Array of data to validate
|
|
351
|
+
* @param {string|Object} schema - Schema object or path to schema file
|
|
352
|
+
* @returns {Object} Validation result
|
|
353
|
+
*/
|
|
354
|
+
function applySchemaValidation(data, schema) {
|
|
355
|
+
if (!Array.isArray(data)) {
|
|
356
|
+
throw new ValidationError('Data must be an array');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const validationHook = createValidationHook(schema);
|
|
360
|
+
const errors = [];
|
|
361
|
+
const validatedData = [];
|
|
362
|
+
|
|
363
|
+
for (let i = 0; i < data.length; i++) {
|
|
364
|
+
try {
|
|
365
|
+
const validatedRow = validationHook(data[i], i, { operation: 'validate' });
|
|
366
|
+
validatedData.push(validatedRow);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (error instanceof ValidationError) {
|
|
369
|
+
errors.push({
|
|
370
|
+
row: i + 1,
|
|
371
|
+
message: error.message,
|
|
372
|
+
data: data[i]
|
|
373
|
+
});
|
|
374
|
+
} else {
|
|
375
|
+
// Skip rows with non-validation errors
|
|
376
|
+
validatedData.push(data[i]);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
valid: errors.length === 0,
|
|
383
|
+
errors,
|
|
384
|
+
data: validatedData,
|
|
385
|
+
summary: {
|
|
386
|
+
totalRows: data.length,
|
|
387
|
+
validRows: validatedData.length,
|
|
388
|
+
errorCount: errors.length,
|
|
389
|
+
errorRate: data.length > 0 ? (errors.length / data.length) * 100 : 0
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Creates a TransformHooks instance with validation
|
|
396
|
+
*
|
|
397
|
+
* @param {string|Object} schema - Schema object or path to schema file
|
|
398
|
+
* @returns {Object} TransformHooks instance
|
|
399
|
+
*/
|
|
400
|
+
function createValidationHooks(schema) {
|
|
401
|
+
const { TransformHooks } = require('../core/transform-hooks');
|
|
402
|
+
const hooks = new TransformHooks();
|
|
403
|
+
|
|
404
|
+
const validationHook = createValidationHook(schema);
|
|
405
|
+
hooks.perRow(validationHook);
|
|
406
|
+
|
|
407
|
+
return hooks;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Creates schema validators from JSON schema
|
|
412
|
+
*
|
|
413
|
+
* @param {Object} schema - JSON schema
|
|
414
|
+
* @returns {Object} Validators object
|
|
415
|
+
*/
|
|
416
|
+
function createSchemaValidators(schema) {
|
|
417
|
+
const validators = {};
|
|
418
|
+
|
|
419
|
+
// Handle both JSON Schema format and simple format
|
|
420
|
+
const properties = schema.properties || schema;
|
|
421
|
+
const requiredFields = schema.required || [];
|
|
422
|
+
|
|
423
|
+
if (!properties || typeof properties !== 'object') {
|
|
424
|
+
return validators;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
for (const [key, definition] of Object.entries(properties)) {
|
|
428
|
+
const validator = {
|
|
429
|
+
type: definition.type,
|
|
430
|
+
required: requiredFields.includes(key)
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// Add format function for dates and other formats
|
|
434
|
+
if (definition.type === 'string' && definition.format) {
|
|
435
|
+
validator.format = (value) => {
|
|
436
|
+
// Handle date-time format
|
|
437
|
+
if (definition.format === 'date-time') {
|
|
438
|
+
if (value instanceof Date) {
|
|
439
|
+
return value.toISOString();
|
|
440
|
+
}
|
|
441
|
+
/* istanbul ignore next */
|
|
442
|
+
if (typeof value === 'string') {
|
|
443
|
+
// Try to parse as date
|
|
444
|
+
const date = new Date(value);
|
|
445
|
+
if (!isNaN(date.getTime())) {
|
|
446
|
+
return date.toISOString();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Handle email format
|
|
451
|
+
if (definition.format === 'email') {
|
|
452
|
+
if (typeof value === 'string') {
|
|
453
|
+
return value.toLowerCase().trim();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Handle uri format
|
|
457
|
+
if (definition.format === 'uri') {
|
|
458
|
+
if (typeof value === 'string') {
|
|
459
|
+
return value.trim();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return value;
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Add validation function
|
|
467
|
+
validator.validate = (value) => {
|
|
468
|
+
if (value === null || value === undefined) {
|
|
469
|
+
return !validator.required;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Type validation
|
|
473
|
+
if (definition.type === 'string' && typeof value !== 'string') {
|
|
474
|
+
// For date-time format, also accept Date objects
|
|
475
|
+
if (definition.format === 'date-time' && value instanceof Date) {
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
if (definition.type === 'number' && typeof value !== 'number') {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
if (definition.type === 'integer' && (!Number.isInteger(value) || typeof value !== 'number')) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
if (definition.type === 'boolean' && typeof value !== 'boolean') {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
if (definition.type === 'array' && !Array.isArray(value)) {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
if (definition.type === 'object' && (typeof value !== 'object' || value === null || Array.isArray(value))) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Additional constraints for strings
|
|
497
|
+
if (definition.type === 'string') {
|
|
498
|
+
if (definition.minLength !== undefined && value.length < definition.minLength) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
if (definition.maxLength !== undefined && value.length > definition.maxLength) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
if (definition.pattern && !new RegExp(definition.pattern).test(value)) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
if (definition.format === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
if (definition.format === 'uri') {
|
|
511
|
+
try {
|
|
512
|
+
new URL(value);
|
|
513
|
+
} catch {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Additional constraints for numbers
|
|
520
|
+
if (definition.type === 'number' || definition.type === 'integer') {
|
|
521
|
+
if (definition.minimum !== undefined && value < definition.minimum) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
if (definition.maximum !== undefined && value > definition.maximum) {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
if (definition.exclusiveMinimum !== undefined && value <= definition.exclusiveMinimum) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
if (definition.exclusiveMaximum !== undefined && value >= definition.exclusiveMaximum) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
if (definition.multipleOf !== undefined && value % definition.multipleOf !== 0) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Additional constraints for arrays
|
|
539
|
+
if (definition.type === 'array') {
|
|
540
|
+
if (definition.minItems !== undefined && value.length < definition.minItems) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
if (definition.maxItems !== undefined && value.length > definition.maxItems) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
if (definition.uniqueItems && new Set(value).size !== value.length) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
// Validate array items if schema is provided
|
|
550
|
+
if (definition.items) {
|
|
551
|
+
for (const item of value) {
|
|
552
|
+
const itemValidator = createSchemaValidators({ properties: { item: definition.items } });
|
|
553
|
+
if (itemValidator.item && !itemValidator.item.validate(item)) {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Additional constraints for objects
|
|
561
|
+
if (definition.type === 'object' && definition.properties) {
|
|
562
|
+
const nestedValidators = createSchemaValidators(definition);
|
|
563
|
+
for (const [nestedKey, nestedValidator] of Object.entries(nestedValidators)) {
|
|
564
|
+
if (value[nestedKey] !== undefined && !nestedValidator.validate(value[nestedKey])) {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
if (nestedValidator.required && value[nestedKey] === undefined) {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Check enum
|
|
574
|
+
if (definition.enum && !definition.enum.includes(value)) {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return true;
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
validators[key] = validator;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return validators;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
module.exports = {
|
|
588
|
+
loadSchema,
|
|
589
|
+
createValidationHook,
|
|
590
|
+
applySchemaValidation,
|
|
591
|
+
createValidationHooks,
|
|
592
|
+
checkType,
|
|
593
|
+
createSchemaValidators // Add this line
|
|
594
|
+
};
|