@schmock/faker 2.3.0 → 2.3.1
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 +21 -0
- package/README.md +45 -0
- package/dist/constants.d.ts +82 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/field-name-matcher.d.ts +19 -0
- package/dist/field-name-matcher.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/jsf-config.d.ts +19 -4
- package/dist/jsf-config.d.ts.map +1 -1
- package/dist/output-limits.d.ts +3 -0
- package/dist/output-limits.d.ts.map +1 -0
- package/dist/overrides.d.ts +2 -1
- package/dist/overrides.d.ts.map +1 -1
- package/dist/schema-children.d.ts +14 -0
- package/dist/schema-children.d.ts.map +1 -0
- package/dist/schema-enhancement.d.ts +1 -0
- package/dist/schema-enhancement.d.ts.map +1 -1
- package/dist/validation.d.ts +24 -5
- package/dist/validation.d.ts.map +1 -1
- package/package.json +17 -7
- package/dist/constants.js +0 -8
- package/dist/field-mappings.js +0 -1114
- package/dist/field-name-matcher.js +0 -148
- package/dist/jsf-config.js +0 -117
- package/dist/overrides.js +0 -137
- package/dist/schema-enhancement.js +0 -104
- package/dist/test-utils.d.ts +0 -57
- package/dist/test-utils.d.ts.map +0 -1
- package/dist/test-utils.js +0 -284
- package/dist/validation.js +0 -300
- package/src/advanced-features.test.ts +0 -912
- package/src/audit-faker-args.test.ts +0 -97
- package/src/constants.d.ts.map +0 -1
- package/src/constants.ts +0 -9
- package/src/data-quality.test.ts +0 -415
- package/src/error-handling.test.ts +0 -504
- package/src/field-mappings.test.ts +0 -351
- package/src/field-mappings.ts +0 -1148
- package/src/field-name-matcher.test.ts +0 -292
- package/src/field-name-matcher.ts +0 -187
- package/src/index.d.ts.map +0 -1
- package/src/index.test.ts +0 -1320
- package/src/index.ts +0 -185
- package/src/integration.test.ts +0 -634
- package/src/jsf-config.d.ts.map +0 -1
- package/src/jsf-config.test.ts +0 -65
- package/src/jsf-config.ts +0 -156
- package/src/overrides.d.ts.map +0 -1
- package/src/overrides.property.test.ts +0 -172
- package/src/overrides.test.ts +0 -38
- package/src/overrides.ts +0 -179
- package/src/performance.test.ts +0 -441
- package/src/plugin-integration.test.ts +0 -575
- package/src/post-process.test.ts +0 -177
- package/src/real-world.test.ts +0 -636
- package/src/schema-enhancement.d.ts.map +0 -1
- package/src/schema-enhancement.test.ts +0 -222
- package/src/schema-enhancement.ts +0 -144
- package/src/steps/deterministic-seeds.steps.ts +0 -112
- package/src/steps/faker-plugin.steps.ts +0 -301
- package/src/test-utils.ts +0 -385
- package/src/validation.d.ts.map +0 -1
- package/src/validation.ts +0 -417
package/dist/test-utils.js
DELETED
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
import { expect } from "vitest";
|
|
2
|
-
import { generateFromSchema } from "./index";
|
|
3
|
-
function isRecord(value) {
|
|
4
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
-
}
|
|
6
|
-
// Schema Factory Functions
|
|
7
|
-
export const schemas = {
|
|
8
|
-
simple: {
|
|
9
|
-
string: () => ({
|
|
10
|
-
type: "string",
|
|
11
|
-
}),
|
|
12
|
-
number: () => ({
|
|
13
|
-
type: "number",
|
|
14
|
-
}),
|
|
15
|
-
object: (properties = {}) => ({
|
|
16
|
-
type: "object",
|
|
17
|
-
properties,
|
|
18
|
-
}),
|
|
19
|
-
array: (items, constraints) => ({
|
|
20
|
-
type: "array",
|
|
21
|
-
items,
|
|
22
|
-
...constraints,
|
|
23
|
-
}),
|
|
24
|
-
},
|
|
25
|
-
withFaker: (type, fakerMethod) => ({
|
|
26
|
-
type,
|
|
27
|
-
faker: fakerMethod,
|
|
28
|
-
}),
|
|
29
|
-
nested: {
|
|
30
|
-
deep: (depth, leafSchema = schemas.simple.string()) => {
|
|
31
|
-
if (depth <= 0)
|
|
32
|
-
return leafSchema;
|
|
33
|
-
return {
|
|
34
|
-
type: "object",
|
|
35
|
-
properties: {
|
|
36
|
-
nested: schemas.nested.deep(depth - 1, leafSchema),
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
},
|
|
40
|
-
wide: (width, propertySchema = schemas.simple.string()) => ({
|
|
41
|
-
type: "object",
|
|
42
|
-
properties: Object.fromEntries(Array.from({ length: width }, (_, i) => [`prop${i}`, propertySchema])),
|
|
43
|
-
}),
|
|
44
|
-
},
|
|
45
|
-
complex: {
|
|
46
|
-
user: () => ({
|
|
47
|
-
type: "object",
|
|
48
|
-
properties: {
|
|
49
|
-
id: { type: "string", format: "uuid" },
|
|
50
|
-
email: { type: "string" },
|
|
51
|
-
firstName: { type: "string" },
|
|
52
|
-
lastName: { type: "string" },
|
|
53
|
-
createdAt: { type: "string" },
|
|
54
|
-
},
|
|
55
|
-
required: ["id", "email"],
|
|
56
|
-
}),
|
|
57
|
-
apiResponse: () => ({
|
|
58
|
-
type: "object",
|
|
59
|
-
properties: {
|
|
60
|
-
success: { type: "boolean" },
|
|
61
|
-
data: {
|
|
62
|
-
type: "array",
|
|
63
|
-
items: schemas.complex.user(),
|
|
64
|
-
},
|
|
65
|
-
meta: {
|
|
66
|
-
type: "object",
|
|
67
|
-
properties: {
|
|
68
|
-
page: { type: "number" },
|
|
69
|
-
total: { type: "number" },
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
}),
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
// Validation Helpers
|
|
77
|
-
export const validators = {
|
|
78
|
-
// Check if a field was mapped to a faker method by comparing with unmapped behavior
|
|
79
|
-
isFieldMapped: async (fieldName, fieldType = "string") => {
|
|
80
|
-
const mappedSchema = {
|
|
81
|
-
type: "object",
|
|
82
|
-
properties: {
|
|
83
|
-
[fieldName]: { type: fieldType },
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
const unmappedSchema = {
|
|
87
|
-
type: "object",
|
|
88
|
-
properties: {
|
|
89
|
-
unmappedRandomField12345: { type: fieldType },
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
// Generate multiple samples to check for patterns
|
|
93
|
-
const mappedSamples = [];
|
|
94
|
-
for (let i = 0; i < 10; i++) {
|
|
95
|
-
const result = await generateFromSchema({
|
|
96
|
-
schema: mappedSchema,
|
|
97
|
-
});
|
|
98
|
-
if (!isRecord(result)) {
|
|
99
|
-
throw new Error("Expected mapped schema generation to return an object");
|
|
100
|
-
}
|
|
101
|
-
mappedSamples.push(result[fieldName]);
|
|
102
|
-
}
|
|
103
|
-
const unmappedSamples = [];
|
|
104
|
-
for (let i = 0; i < 10; i++) {
|
|
105
|
-
const result = await generateFromSchema({
|
|
106
|
-
schema: unmappedSchema,
|
|
107
|
-
});
|
|
108
|
-
if (!isRecord(result)) {
|
|
109
|
-
throw new Error("Expected unmapped schema generation to return an object");
|
|
110
|
-
}
|
|
111
|
-
unmappedSamples.push(result.unmappedRandomField12345);
|
|
112
|
-
}
|
|
113
|
-
// If field is mapped to a specific faker method, it should have different characteristics
|
|
114
|
-
// than the generic unmapped field
|
|
115
|
-
return (analyzeDataCharacteristics(mappedSamples) !==
|
|
116
|
-
analyzeDataCharacteristics(unmappedSamples));
|
|
117
|
-
},
|
|
118
|
-
// Analyze uniqueness of generated data
|
|
119
|
-
uniquenessRatio: (samples) => {
|
|
120
|
-
const unique = new Set(samples);
|
|
121
|
-
return unique.size / samples.length;
|
|
122
|
-
},
|
|
123
|
-
// Check if all samples match a basic pattern without being too specific
|
|
124
|
-
allMatch: (samples, validator) => {
|
|
125
|
-
return samples.every(validator);
|
|
126
|
-
},
|
|
127
|
-
// Check if data appears to be from a specific faker category
|
|
128
|
-
appearsToBeFromCategory: (samples, category) => {
|
|
129
|
-
switch (category) {
|
|
130
|
-
case "email":
|
|
131
|
-
return validators.allMatch(samples, (s) => typeof s === "string" && s.includes("@") && s.includes("."));
|
|
132
|
-
case "name":
|
|
133
|
-
return validators.allMatch(samples, (s) => typeof s === "string" &&
|
|
134
|
-
s.length > 1 &&
|
|
135
|
-
s.length < 50 &&
|
|
136
|
-
/^[A-Z]/.test(s));
|
|
137
|
-
case "phone":
|
|
138
|
-
return validators.allMatch(samples, (s) => typeof s === "string" && /\d/.test(s) && s.length > 10);
|
|
139
|
-
case "address":
|
|
140
|
-
return validators.allMatch(samples, (s) => typeof s === "string" &&
|
|
141
|
-
s.length > 10 &&
|
|
142
|
-
/\d/.test(s) &&
|
|
143
|
-
/[A-Z]/.test(s));
|
|
144
|
-
case "uuid":
|
|
145
|
-
return validators.allMatch(samples, (s) => typeof s === "string" &&
|
|
146
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s));
|
|
147
|
-
case "date":
|
|
148
|
-
return validators.allMatch(samples, (s) => typeof s === "string" && !Number.isNaN(Date.parse(s)));
|
|
149
|
-
default:
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
};
|
|
154
|
-
// Performance Testing Utilities
|
|
155
|
-
export const performance = {
|
|
156
|
-
measure: async (fn) => {
|
|
157
|
-
const start = Date.now();
|
|
158
|
-
const result = await fn();
|
|
159
|
-
const duration = Date.now() - start;
|
|
160
|
-
return { result, duration };
|
|
161
|
-
},
|
|
162
|
-
measureMemory: (fn) => {
|
|
163
|
-
if (globalThis.gc) {
|
|
164
|
-
globalThis.gc();
|
|
165
|
-
}
|
|
166
|
-
const before = process.memoryUsage().heapUsed;
|
|
167
|
-
fn();
|
|
168
|
-
const after = process.memoryUsage().heapUsed;
|
|
169
|
-
return after - before;
|
|
170
|
-
},
|
|
171
|
-
benchmark: async (_name, fn, iterations = 100) => {
|
|
172
|
-
const times = [];
|
|
173
|
-
for (let i = 0; i < iterations; i++) {
|
|
174
|
-
const start = Date.now();
|
|
175
|
-
await fn();
|
|
176
|
-
const duration = Date.now() - start;
|
|
177
|
-
times.push(duration);
|
|
178
|
-
}
|
|
179
|
-
return {
|
|
180
|
-
mean: times.reduce((a, b) => a + b, 0) / times.length,
|
|
181
|
-
min: Math.min(...times),
|
|
182
|
-
max: Math.max(...times),
|
|
183
|
-
};
|
|
184
|
-
},
|
|
185
|
-
};
|
|
186
|
-
// Test Data Generators
|
|
187
|
-
export const generate = {
|
|
188
|
-
samples: async (schema, count = 10, options) => {
|
|
189
|
-
const results = [];
|
|
190
|
-
for (let i = 0; i < count; i++) {
|
|
191
|
-
results.push(await generateFromSchema({ schema, ...options }));
|
|
192
|
-
}
|
|
193
|
-
return results;
|
|
194
|
-
},
|
|
195
|
-
withSeed: async (schema, seed) => {
|
|
196
|
-
return await generateFromSchema({ schema, seed });
|
|
197
|
-
},
|
|
198
|
-
};
|
|
199
|
-
// Statistical Analysis
|
|
200
|
-
export const stats = {
|
|
201
|
-
distribution: (samples) => {
|
|
202
|
-
const dist = new Map();
|
|
203
|
-
for (const sample of samples) {
|
|
204
|
-
const key = JSON.stringify(sample) ?? "undefined";
|
|
205
|
-
dist.set(key, (dist.get(key) || 0) + 1);
|
|
206
|
-
}
|
|
207
|
-
return dist;
|
|
208
|
-
},
|
|
209
|
-
entropy: (samples) => {
|
|
210
|
-
const dist = stats.distribution(samples);
|
|
211
|
-
const total = samples.length;
|
|
212
|
-
let entropy = 0;
|
|
213
|
-
for (const count of dist.values()) {
|
|
214
|
-
const p = count / total;
|
|
215
|
-
if (p > 0) {
|
|
216
|
-
entropy -= p * Math.log2(p);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return entropy;
|
|
220
|
-
},
|
|
221
|
-
};
|
|
222
|
-
// Schema Validation Test Helpers
|
|
223
|
-
export const schemaTests = {
|
|
224
|
-
expectValid: async (schema) => {
|
|
225
|
-
await expect(generateFromSchema({ schema })).resolves.not.toThrow();
|
|
226
|
-
},
|
|
227
|
-
expectInvalid: async (schema, errorMessage) => {
|
|
228
|
-
if (errorMessage) {
|
|
229
|
-
await expect(generateFromSchema({ schema })).rejects.toThrow(errorMessage);
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
await expect(generateFromSchema({ schema })).rejects.toThrow();
|
|
233
|
-
}
|
|
234
|
-
},
|
|
235
|
-
expectSchemaError: async (schema, path, issue) => {
|
|
236
|
-
try {
|
|
237
|
-
await generateFromSchema({ schema });
|
|
238
|
-
throw new Error("Expected schema validation to fail");
|
|
239
|
-
}
|
|
240
|
-
catch (error) {
|
|
241
|
-
expect(error).toBeInstanceOf(Error);
|
|
242
|
-
if (!(error instanceof Error)) {
|
|
243
|
-
throw new Error("Expected schema validation to throw an Error");
|
|
244
|
-
}
|
|
245
|
-
expect(error.name).toBe("SchemaValidationError");
|
|
246
|
-
// The schemaPath is in the context
|
|
247
|
-
if ("context" in error &&
|
|
248
|
-
isRecord(error.context) &&
|
|
249
|
-
error.context.schemaPath) {
|
|
250
|
-
expect(error.context.schemaPath).toBe(path);
|
|
251
|
-
}
|
|
252
|
-
if (issue) {
|
|
253
|
-
expect(error.message).toContain(issue);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
};
|
|
258
|
-
// Helper to analyze data characteristics without hardcoding patterns
|
|
259
|
-
function analyzeDataCharacteristics(samples) {
|
|
260
|
-
if (samples.length === 0)
|
|
261
|
-
return "empty";
|
|
262
|
-
const first = samples[0];
|
|
263
|
-
const type = typeof first;
|
|
264
|
-
if (!samples.every((sample) => typeof sample === "string"))
|
|
265
|
-
return type;
|
|
266
|
-
// Analyze string characteristics
|
|
267
|
-
const characteristics = [type];
|
|
268
|
-
// Check common patterns without being too specific
|
|
269
|
-
if (samples.every((s) => s.includes("@")))
|
|
270
|
-
characteristics.push("has-at");
|
|
271
|
-
if (samples.every((s) => /^\d+$/.test(s)))
|
|
272
|
-
characteristics.push("numeric");
|
|
273
|
-
if (samples.every((s) => /^[0-9a-f-]+$/i.test(s)))
|
|
274
|
-
characteristics.push("hex-like");
|
|
275
|
-
if (samples.every((s) => s.length > 50))
|
|
276
|
-
characteristics.push("long");
|
|
277
|
-
if (samples.every((s) => s.length < 10))
|
|
278
|
-
characteristics.push("short");
|
|
279
|
-
if (validators.uniquenessRatio(samples) > 0.8)
|
|
280
|
-
characteristics.push("high-entropy");
|
|
281
|
-
if (validators.uniquenessRatio(samples) < 0.2)
|
|
282
|
-
characteristics.push("low-entropy");
|
|
283
|
-
return characteristics.join("-");
|
|
284
|
-
}
|
package/dist/validation.js
DELETED
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
import { ResourceLimitError, SchemaValidationError } from "@schmock/core";
|
|
2
|
-
import { DEEP_NESTING_THRESHOLD, DEFAULT_ARRAY_COUNT, LARGE_ARRAY_THRESHOLD, MAX_ARRAY_SIZE, MAX_NESTING_DEPTH, } from "./constants.js";
|
|
3
|
-
import { createFakerInstance } from "./jsf-config.js";
|
|
4
|
-
let validationFaker;
|
|
5
|
-
export function isJSONSchema7(value) {
|
|
6
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Validate JSON Schema structure and enforce resource limits
|
|
10
|
-
* Checks for malformed schemas, circular references, excessive nesting,
|
|
11
|
-
* and dangerous patterns that could cause memory issues
|
|
12
|
-
* @param schema - JSON Schema to validate
|
|
13
|
-
* @param path - Current path in schema tree (for error messages)
|
|
14
|
-
* @throws {SchemaValidationError} When schema structure is invalid
|
|
15
|
-
* @throws {ResourceLimitError} When schema exceeds safety limits
|
|
16
|
-
*/
|
|
17
|
-
export function validateSchema(schema, path = "$") {
|
|
18
|
-
if (!schema || typeof schema !== "object") {
|
|
19
|
-
throw new SchemaValidationError(path, "Schema must be a valid JSON Schema object");
|
|
20
|
-
}
|
|
21
|
-
if (Object.keys(schema).length === 0) {
|
|
22
|
-
throw new SchemaValidationError(path, "Schema cannot be empty");
|
|
23
|
-
}
|
|
24
|
-
// Check for invalid schema types
|
|
25
|
-
const validTypes = [
|
|
26
|
-
"object",
|
|
27
|
-
"array",
|
|
28
|
-
"string",
|
|
29
|
-
"number",
|
|
30
|
-
"integer",
|
|
31
|
-
"boolean",
|
|
32
|
-
"null",
|
|
33
|
-
];
|
|
34
|
-
if (schema.type &&
|
|
35
|
-
typeof schema.type === "string" &&
|
|
36
|
-
!validTypes.includes(schema.type)) {
|
|
37
|
-
throw new SchemaValidationError(path, `Invalid schema type: "${schema.type}"`, "Supported types are: object, array, string, number, integer, boolean, null");
|
|
38
|
-
}
|
|
39
|
-
// Check for malformed properties (must be object, not string)
|
|
40
|
-
if (schema.type === "object" && schema.properties) {
|
|
41
|
-
if (typeof schema.properties !== "object" ||
|
|
42
|
-
Array.isArray(schema.properties)) {
|
|
43
|
-
throw new SchemaValidationError(`${path}.properties`, "Properties must be an object mapping property names to schemas", 'Use { "propertyName": { "type": "string" } } format');
|
|
44
|
-
}
|
|
45
|
-
// Validate each property recursively
|
|
46
|
-
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
47
|
-
if (typeof propSchema === "object" && propSchema !== null) {
|
|
48
|
-
// Check for invalid faker methods in property schemas
|
|
49
|
-
const fakerProp = "faker" in propSchema ? String(propSchema.faker) : undefined;
|
|
50
|
-
if (fakerProp) {
|
|
51
|
-
try {
|
|
52
|
-
validateFakerMethod(fakerProp);
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
// Re-throw with proper path context
|
|
56
|
-
if (error instanceof SchemaValidationError) {
|
|
57
|
-
const ctx = error.context;
|
|
58
|
-
let issue = "Invalid faker method";
|
|
59
|
-
let suggestion;
|
|
60
|
-
if (ctx && typeof ctx === "object") {
|
|
61
|
-
if ("issue" in ctx && typeof ctx.issue === "string")
|
|
62
|
-
issue = ctx.issue;
|
|
63
|
-
if ("suggestion" in ctx && typeof ctx.suggestion === "string")
|
|
64
|
-
suggestion = ctx.suggestion;
|
|
65
|
-
}
|
|
66
|
-
throw new SchemaValidationError(`${path}.properties.${propName}.faker`, issue, suggestion);
|
|
67
|
-
}
|
|
68
|
-
if (error instanceof Error)
|
|
69
|
-
throw error;
|
|
70
|
-
throw new Error(String(error));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
validateSchema(propSchema, `${path}.properties.${propName}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
// Check for invalid array items
|
|
78
|
-
if (schema.type === "array") {
|
|
79
|
-
// Array must have items defined and non-null
|
|
80
|
-
if (schema.items === null || schema.items === undefined) {
|
|
81
|
-
throw new SchemaValidationError(`${path}.items`, "Array schema must have valid items definition", "Define items as a schema object or array of schemas");
|
|
82
|
-
}
|
|
83
|
-
if (Array.isArray(schema.items)) {
|
|
84
|
-
if (schema.items.length === 0) {
|
|
85
|
-
throw new SchemaValidationError(`${path}.items`, "Array items cannot be empty array", "Provide at least one item schema");
|
|
86
|
-
}
|
|
87
|
-
schema.items.forEach((item, index) => {
|
|
88
|
-
if (typeof item === "object" && item !== null) {
|
|
89
|
-
validateSchema(item, `${path}.items[${index}]`);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
else if (typeof schema.items === "object" && schema.items !== null) {
|
|
94
|
-
validateSchema(schema.items, `${path}.items`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
// Full-tree integrity checks are O(n) each, so only run them once at the
|
|
98
|
-
// top of the recursion. Running them at every node turns validation into
|
|
99
|
-
// O(n²) on deep schemas without finding anything the top-level pass misses.
|
|
100
|
-
// The self-ref/$ref="#" check is part of this group so that an inner $ref
|
|
101
|
-
// is reported as "circular references" (caught by hasCircularReference at
|
|
102
|
-
// the root), matching the legacy error message.
|
|
103
|
-
if (path === "$") {
|
|
104
|
-
if (hasCircularReference(schema)) {
|
|
105
|
-
throw new SchemaValidationError(path, "Schema contains circular references which are not supported");
|
|
106
|
-
}
|
|
107
|
-
if (schema.$ref === "#") {
|
|
108
|
-
throw new SchemaValidationError(path, "Self-referencing schemas are not supported");
|
|
109
|
-
}
|
|
110
|
-
const depth = calculateNestingDepth(schema);
|
|
111
|
-
if (depth > MAX_NESTING_DEPTH) {
|
|
112
|
-
throw new ResourceLimitError("schema_nesting_depth", MAX_NESTING_DEPTH, depth);
|
|
113
|
-
}
|
|
114
|
-
if (depth >= DEEP_NESTING_THRESHOLD) {
|
|
115
|
-
checkForDeepNestingWithArrays(schema, path);
|
|
116
|
-
}
|
|
117
|
-
checkArraySizeLimits(schema, path);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Detect circular references in JSON Schema using path-based traversal
|
|
122
|
-
* Uses backtracking to distinguish between cycles and legitimate schema reuse
|
|
123
|
-
* @param schema - Schema to check for cycles
|
|
124
|
-
* @param currentPath - Set of schemas currently in traversal path
|
|
125
|
-
* @returns true if circular reference detected, false otherwise
|
|
126
|
-
* @example
|
|
127
|
-
* // Detects: schema A -> B -> A (cycle)
|
|
128
|
-
* // Allows: schema A -> B, A -> C (reuse of A)
|
|
129
|
-
*/
|
|
130
|
-
function hasCircularReference(schema, currentPath = new Set()) {
|
|
131
|
-
// Check if this schema is currently being traversed (cycle detected)
|
|
132
|
-
if (currentPath.has(schema)) {
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
if (schema.$ref === "#") {
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
// Add to current path for this traversal branch
|
|
139
|
-
currentPath.add(schema);
|
|
140
|
-
if (schema.type === "object" && schema.properties) {
|
|
141
|
-
for (const prop of Object.values(schema.properties)) {
|
|
142
|
-
if (isJSONSchema7(prop)) {
|
|
143
|
-
if (hasCircularReference(prop, currentPath)) {
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (schema.type === "array" && schema.items) {
|
|
150
|
-
const items = Array.isArray(schema.items) ? schema.items : [schema.items];
|
|
151
|
-
for (const item of items) {
|
|
152
|
-
if (isJSONSchema7(item)) {
|
|
153
|
-
if (hasCircularReference(item, currentPath)) {
|
|
154
|
-
return true;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
// Remove from current path after checking all children (backtrack)
|
|
160
|
-
currentPath.delete(schema);
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Calculate maximum nesting depth of a JSON Schema
|
|
165
|
-
* Recursively traverses object properties and array items
|
|
166
|
-
* @param schema - Schema to measure
|
|
167
|
-
* @param depth - Current depth (internal recursion parameter)
|
|
168
|
-
* @returns Maximum nesting depth found
|
|
169
|
-
*/
|
|
170
|
-
function calculateNestingDepth(schema, depth = 0) {
|
|
171
|
-
if (depth > MAX_NESTING_DEPTH) {
|
|
172
|
-
return depth;
|
|
173
|
-
}
|
|
174
|
-
let maxDepth = depth;
|
|
175
|
-
if (schema.type === "object" && schema.properties) {
|
|
176
|
-
for (const prop of Object.values(schema.properties)) {
|
|
177
|
-
if (isJSONSchema7(prop)) {
|
|
178
|
-
maxDepth = Math.max(maxDepth, calculateNestingDepth(prop, depth + 1));
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (schema.type === "array" && schema.items) {
|
|
183
|
-
const items = Array.isArray(schema.items) ? schema.items : [schema.items];
|
|
184
|
-
for (const item of items) {
|
|
185
|
-
if (isJSONSchema7(item)) {
|
|
186
|
-
maxDepth = Math.max(maxDepth, calculateNestingDepth(item, depth + 1));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return maxDepth;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Check for dangerous patterns of deep nesting combined with large arrays
|
|
194
|
-
* Prevents memory issues from schemas like: depth 3+ with 100+ item arrays
|
|
195
|
-
* @param schema - Schema to check
|
|
196
|
-
* @param _path - Path in schema (unused but kept for signature consistency)
|
|
197
|
-
* @throws {ResourceLimitError} When dangerous nesting pattern detected
|
|
198
|
-
*/
|
|
199
|
-
function checkForDeepNestingWithArrays(schema, _path) {
|
|
200
|
-
// Look for arrays in deeply nested structures that could cause memory issues
|
|
201
|
-
function findArraysInDeepNesting(node, currentDepth) {
|
|
202
|
-
const schemaType = node.type;
|
|
203
|
-
const isArray = Array.isArray(schemaType)
|
|
204
|
-
? schemaType.includes("array")
|
|
205
|
-
: schemaType === "array";
|
|
206
|
-
if (isArray) {
|
|
207
|
-
const maxItems = node.maxItems || DEFAULT_ARRAY_COUNT;
|
|
208
|
-
// Be more aggressive about deep nesting detection
|
|
209
|
-
if (currentDepth >= DEEP_NESTING_THRESHOLD &&
|
|
210
|
-
maxItems >= LARGE_ARRAY_THRESHOLD) {
|
|
211
|
-
throw new ResourceLimitError("deep_nesting_memory_risk", DEEP_NESTING_THRESHOLD * LARGE_ARRAY_THRESHOLD, currentDepth * maxItems);
|
|
212
|
-
}
|
|
213
|
-
// Check items if they exist
|
|
214
|
-
if (node.items) {
|
|
215
|
-
const items = Array.isArray(node.items) ? node.items : [node.items];
|
|
216
|
-
for (const item of items) {
|
|
217
|
-
if (isJSONSchema7(item)) {
|
|
218
|
-
findArraysInDeepNesting(item, currentDepth + 1);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
if (schemaType === "object" && node.properties) {
|
|
225
|
-
for (const prop of Object.values(node.properties)) {
|
|
226
|
-
if (isJSONSchema7(prop)) {
|
|
227
|
-
findArraysInDeepNesting(prop, currentDepth + 1);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
findArraysInDeepNesting(schema, 0);
|
|
233
|
-
}
|
|
234
|
-
function checkArraySizeLimits(schema, path) {
|
|
235
|
-
// Recursively check all array constraints in the schema
|
|
236
|
-
if (schema.type === "array") {
|
|
237
|
-
// Check for dangerously large maxItems
|
|
238
|
-
if (schema.maxItems && schema.maxItems > MAX_ARRAY_SIZE) {
|
|
239
|
-
throw new ResourceLimitError("array_max_items", MAX_ARRAY_SIZE, schema.maxItems);
|
|
240
|
-
}
|
|
241
|
-
// Check for combination of deep nesting and large arrays
|
|
242
|
-
const depth = calculateNestingDepth(schema);
|
|
243
|
-
const estimatedSize = schema.maxItems || schema.minItems || DEFAULT_ARRAY_COUNT;
|
|
244
|
-
// If we have deep nesting and large arrays, it could cause memory issues
|
|
245
|
-
if (depth > DEEP_NESTING_THRESHOLD &&
|
|
246
|
-
estimatedSize > LARGE_ARRAY_THRESHOLD) {
|
|
247
|
-
throw new ResourceLimitError("memory_estimation", DEEP_NESTING_THRESHOLD * LARGE_ARRAY_THRESHOLD, depth * estimatedSize);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
// Recursively check nested schemas
|
|
251
|
-
if (schema.type === "object" && schema.properties) {
|
|
252
|
-
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
253
|
-
if (isJSONSchema7(propSchema)) {
|
|
254
|
-
checkArraySizeLimits(propSchema, `${path}.properties.${propName}`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
if (schema.type === "array" && schema.items) {
|
|
259
|
-
if (Array.isArray(schema.items)) {
|
|
260
|
-
schema.items.forEach((item, index) => {
|
|
261
|
-
if (isJSONSchema7(item)) {
|
|
262
|
-
checkArraySizeLimits(item, `${path}.items[${index}]`);
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
else if (isJSONSchema7(schema.items)) {
|
|
267
|
-
checkArraySizeLimits(schema.items, `${path}.items`);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Validate that faker method string references a valid Faker.js API
|
|
273
|
-
* Checks format (namespace.method) and validates against known namespaces
|
|
274
|
-
* @param fakerMethod - Faker method string (e.g., "person.fullName")
|
|
275
|
-
* @throws {SchemaValidationError} When faker method format or namespace is invalid
|
|
276
|
-
*/
|
|
277
|
-
export function validateFakerMethod(fakerMethod) {
|
|
278
|
-
// Check if faker method follows valid format (namespace.method)
|
|
279
|
-
const parts = fakerMethod.split(".");
|
|
280
|
-
if (parts.length < 2) {
|
|
281
|
-
throw new SchemaValidationError("$.faker", `Invalid faker method format: "${fakerMethod}"`, "Use format like 'person.firstName' or 'internet.email'");
|
|
282
|
-
}
|
|
283
|
-
// Validate by resolving the method path on a cached faker instance
|
|
284
|
-
if (!validationFaker) {
|
|
285
|
-
validationFaker = createFakerInstance();
|
|
286
|
-
}
|
|
287
|
-
const faker = validationFaker;
|
|
288
|
-
let current = faker;
|
|
289
|
-
for (const part of parts) {
|
|
290
|
-
if (current && typeof current === "object" && part in current) {
|
|
291
|
-
current = Reflect.get(current, part);
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
throw new SchemaValidationError("$.faker", `Invalid faker method: "${fakerMethod}"`, "Check faker.js documentation for valid methods");
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
if (typeof current !== "function") {
|
|
298
|
-
throw new SchemaValidationError("$.faker", `Invalid faker method: "${fakerMethod}" is not a function`, "Check faker.js documentation for valid methods");
|
|
299
|
-
}
|
|
300
|
-
}
|