Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions src/validation/ajv-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ function createDefaultAjvInstance(): Ajv {
*/
export class AjvJsonSchemaValidator implements jsonSchemaValidator {
private _ajv: Ajv;
private _compiledCache: Map<string, ReturnType<Ajv['compile']>>

/**
* Create an AJV validator
Expand All @@ -58,6 +59,7 @@ export class AjvJsonSchemaValidator implements jsonSchemaValidator {
*/
constructor(ajv?: Ajv) {
this._ajv = ajv ?? createDefaultAjvInstance();
this._compiledCache = new Map();
}

/**
Expand All @@ -70,11 +72,7 @@ export class AjvJsonSchemaValidator implements jsonSchemaValidator {
* @returns A validator function that validates input data
*/
getValidator<T>(schema: JsonSchemaType): JsonSchemaValidator<T> {
// Check if schema has $id and is already compiled/cached
const ajvValidator =
'$id' in schema && typeof schema.$id === 'string'
? (this._ajv.getSchema(schema.$id) ?? this._ajv.compile(schema))
: this._ajv.compile(schema);
const ajvValidator = this._getCompiled(schema);

return (input: unknown): JsonSchemaValidatorResult<T> => {
const valid = ajvValidator(input);
Expand All @@ -94,4 +92,22 @@ export class AjvJsonSchemaValidator implements jsonSchemaValidator {
}
};
}

private _getCompiled(schema: JsonSchemaType) {
if ('$id' in schema && typeof schema.$id === 'string') {
return this._ajv.getSchema(schema.$id) ?? this._ajv.compile(schema);
}
let key: string;
try {
key = JSON.stringify(schema);
} catch {
return this._ajv.compile(schema);
}
let cached = this._compiledCache.get(key);
if (cached === undefined) {
cached = this._ajv.compile(JSON.parse(key));
this._compiledCache.set(key, cached);
}
return cached;
}
}
56 changes: 56 additions & 0 deletions test/validation/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,62 @@ describe('JSON Schema Validators', () => {
});
});

describe('AJV caching', () => {
it('caches compiled validators for identical schemas without $id', () => {
const validator = new AjvJsonSchemaValidator();
const schema: JsonSchemaType = {
type: 'object',
properties: { name: { type: 'string' } },
required: ['name']
};

// Get validator multiple times for the same schema
const v1 = validator.getValidator(schema);
const v2 = validator.getValidator(schema);

// Both should produce the same results
expect(v1({ name: 'test' })).toEqual(v2({ name: 'test' }));
expect(v1({})).toEqual(v2({}));
});

it('caches validators for schemas with same content but different references', () => {
const validator = new AjvJsonSchemaValidator();
const schema1: JsonSchemaType = { type: 'string' };
const schema2: JsonSchemaType = { type: 'string' };

const v1 = validator.getValidator(schema1);
const v2 = validator.getValidator(schema2);

expect(v1('test')).toEqual(v2('test'));
expect(v1(42)).toEqual(v2(42));
});

it('returns different validators for different schemas', () => {
const validator = new AjvJsonSchemaValidator();
const schema1: JsonSchemaType = { type: 'string' };
const schema2: JsonSchemaType = { type: 'number' };

const v1 = validator.getValidator(schema1);
const v2 = validator.getValidator(schema2);

expect(v1('test').valid).toBe(true);
expect(v2('test').valid).toBe(false);
});

it('compiles each unique schema only once', () => {
const validator = new AjvJsonSchemaValidator();
const schema: JsonSchemaType = { type: 'string' };

// Call getValidator 5 times with same schema
for (let i = 0; i < 5; i++) {
validator.getValidator(schema)(i === 0 ? 'test' : 'other');
}

// The cache should have exactly 1 entry
expect((validator as any)._compiledCache.size).toBe(1);
});
});

describe('Missing dependencies', () => {
describe('AJV not installed but CfWorker is', () => {
beforeEach(() => {
Expand Down
Loading