888 lines
30 KiB
TypeScript
888 lines
30 KiB
TypeScript
|
export {
|
||
|
Format,
|
||
|
FormatDefinition,
|
||
|
AsyncFormatDefinition,
|
||
|
KeywordDefinition,
|
||
|
KeywordErrorDefinition,
|
||
|
CodeKeywordDefinition,
|
||
|
MacroKeywordDefinition,
|
||
|
FuncKeywordDefinition,
|
||
|
Vocabulary,
|
||
|
Schema,
|
||
|
SchemaObject,
|
||
|
AnySchemaObject,
|
||
|
AsyncSchema,
|
||
|
AnySchema,
|
||
|
ValidateFunction,
|
||
|
AsyncValidateFunction,
|
||
|
AnyValidateFunction,
|
||
|
ErrorObject,
|
||
|
ErrorNoParams,
|
||
|
} from "./types"
|
||
|
|
||
|
export {SchemaCxt, SchemaObjCxt} from "./compile"
|
||
|
export interface Plugin<Opts> {
|
||
|
(ajv: Ajv, options?: Opts): Ajv
|
||
|
[prop: string]: any
|
||
|
}
|
||
|
|
||
|
export {KeywordCxt} from "./compile/validate"
|
||
|
export {DefinedError} from "./vocabularies/errors"
|
||
|
export {JSONType} from "./compile/rules"
|
||
|
export {JSONSchemaType} from "./types/json-schema"
|
||
|
export {JTDSchemaType, SomeJTDSchemaType, JTDDataType} from "./types/jtd-schema"
|
||
|
export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen"
|
||
|
|
||
|
import type {
|
||
|
Schema,
|
||
|
AnySchema,
|
||
|
AnySchemaObject,
|
||
|
SchemaObject,
|
||
|
AsyncSchema,
|
||
|
Vocabulary,
|
||
|
KeywordDefinition,
|
||
|
AddedKeywordDefinition,
|
||
|
AnyValidateFunction,
|
||
|
ValidateFunction,
|
||
|
AsyncValidateFunction,
|
||
|
ErrorObject,
|
||
|
Format,
|
||
|
AddedFormat,
|
||
|
RegExpEngine,
|
||
|
UriResolver,
|
||
|
} from "./types"
|
||
|
import type {JSONSchemaType} from "./types/json-schema"
|
||
|
import type {JTDSchemaType, SomeJTDSchemaType, JTDDataType} from "./types/jtd-schema"
|
||
|
import ValidationError from "./runtime/validation_error"
|
||
|
import MissingRefError from "./compile/ref_error"
|
||
|
import {getRules, ValidationRules, Rule, RuleGroup, JSONType} from "./compile/rules"
|
||
|
import {SchemaEnv, compileSchema, resolveSchema} from "./compile"
|
||
|
import {Code, ValueScope} from "./compile/codegen"
|
||
|
import {normalizeId, getSchemaRefs} from "./compile/resolve"
|
||
|
import {getJSONTypes} from "./compile/validate/dataType"
|
||
|
import {eachItem} from "./compile/util"
|
||
|
import * as $dataRefSchema from "./refs/data.json"
|
||
|
|
||
|
import DefaultUriResolver from "./runtime/uri"
|
||
|
|
||
|
const defaultRegExp: RegExpEngine = (str, flags) => new RegExp(str, flags)
|
||
|
defaultRegExp.code = "new RegExp"
|
||
|
|
||
|
const META_IGNORE_OPTIONS: (keyof Options)[] = ["removeAdditional", "useDefaults", "coerceTypes"]
|
||
|
const EXT_SCOPE_NAMES = new Set([
|
||
|
"validate",
|
||
|
"serialize",
|
||
|
"parse",
|
||
|
"wrapper",
|
||
|
"root",
|
||
|
"schema",
|
||
|
"keyword",
|
||
|
"pattern",
|
||
|
"formats",
|
||
|
"validate$data",
|
||
|
"func",
|
||
|
"obj",
|
||
|
"Error",
|
||
|
])
|
||
|
|
||
|
export type Options = CurrentOptions & DeprecatedOptions
|
||
|
|
||
|
export interface CurrentOptions {
|
||
|
// strict mode options (NEW)
|
||
|
strict?: boolean | "log"
|
||
|
strictSchema?: boolean | "log"
|
||
|
strictNumbers?: boolean | "log"
|
||
|
strictTypes?: boolean | "log"
|
||
|
strictTuples?: boolean | "log"
|
||
|
strictRequired?: boolean | "log"
|
||
|
allowMatchingProperties?: boolean // disables a strict mode restriction
|
||
|
allowUnionTypes?: boolean
|
||
|
validateFormats?: boolean
|
||
|
// validation and reporting options:
|
||
|
$data?: boolean
|
||
|
allErrors?: boolean
|
||
|
verbose?: boolean
|
||
|
discriminator?: boolean
|
||
|
unicodeRegExp?: boolean
|
||
|
timestamp?: "string" | "date" // JTD only
|
||
|
parseDate?: boolean // JTD only
|
||
|
allowDate?: boolean // JTD only
|
||
|
$comment?:
|
||
|
| true
|
||
|
| ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown)
|
||
|
formats?: {[Name in string]?: Format}
|
||
|
keywords?: Vocabulary
|
||
|
schemas?: AnySchema[] | {[Key in string]?: AnySchema}
|
||
|
logger?: Logger | false
|
||
|
loadSchema?: (uri: string) => Promise<AnySchemaObject>
|
||
|
// options to modify validated data:
|
||
|
removeAdditional?: boolean | "all" | "failing"
|
||
|
useDefaults?: boolean | "empty"
|
||
|
coerceTypes?: boolean | "array"
|
||
|
// advanced options:
|
||
|
next?: boolean // NEW
|
||
|
unevaluated?: boolean // NEW
|
||
|
dynamicRef?: boolean // NEW
|
||
|
schemaId?: "id" | "$id"
|
||
|
jtd?: boolean // NEW
|
||
|
meta?: SchemaObject | boolean
|
||
|
defaultMeta?: string | AnySchemaObject
|
||
|
validateSchema?: boolean | "log"
|
||
|
addUsedSchema?: boolean
|
||
|
inlineRefs?: boolean | number
|
||
|
passContext?: boolean
|
||
|
loopRequired?: number
|
||
|
loopEnum?: number // NEW
|
||
|
ownProperties?: boolean
|
||
|
multipleOfPrecision?: number
|
||
|
int32range?: boolean // JTD only
|
||
|
messages?: boolean
|
||
|
code?: CodeOptions // NEW
|
||
|
uriResolver?: UriResolver
|
||
|
}
|
||
|
|
||
|
export interface CodeOptions {
|
||
|
es5?: boolean
|
||
|
esm?: boolean
|
||
|
lines?: boolean
|
||
|
optimize?: boolean | number
|
||
|
formats?: Code // code to require (or construct) map of available formats - for standalone code
|
||
|
source?: boolean
|
||
|
process?: (code: string, schema?: SchemaEnv) => string
|
||
|
regExp?: RegExpEngine
|
||
|
}
|
||
|
|
||
|
interface InstanceCodeOptions extends CodeOptions {
|
||
|
regExp: RegExpEngine
|
||
|
optimize: number
|
||
|
}
|
||
|
|
||
|
interface DeprecatedOptions {
|
||
|
/** @deprecated */
|
||
|
ignoreKeywordsWithRef?: boolean
|
||
|
/** @deprecated */
|
||
|
jsPropertySyntax?: boolean // added instead of jsonPointers
|
||
|
/** @deprecated */
|
||
|
unicode?: boolean
|
||
|
}
|
||
|
|
||
|
interface RemovedOptions {
|
||
|
format?: boolean
|
||
|
errorDataPath?: "object" | "property"
|
||
|
nullable?: boolean // "nullable" keyword is supported by default
|
||
|
jsonPointers?: boolean
|
||
|
extendRefs?: true | "ignore" | "fail"
|
||
|
missingRefs?: true | "ignore" | "fail"
|
||
|
processCode?: (code: string, schema?: SchemaEnv) => string
|
||
|
sourceCode?: boolean
|
||
|
strictDefaults?: boolean
|
||
|
strictKeywords?: boolean
|
||
|
uniqueItems?: boolean
|
||
|
unknownFormats?: true | string[] | "ignore"
|
||
|
cache?: any
|
||
|
serialize?: (schema: AnySchema) => unknown
|
||
|
ajvErrors?: boolean
|
||
|
}
|
||
|
|
||
|
type OptionsInfo<T extends RemovedOptions | DeprecatedOptions> = {
|
||
|
[K in keyof T]-?: string | undefined
|
||
|
}
|
||
|
|
||
|
const removedOptions: OptionsInfo<RemovedOptions> = {
|
||
|
errorDataPath: "",
|
||
|
format: "`validateFormats: false` can be used instead.",
|
||
|
nullable: '"nullable" keyword is supported by default.',
|
||
|
jsonPointers: "Deprecated jsPropertySyntax can be used instead.",
|
||
|
extendRefs: "Deprecated ignoreKeywordsWithRef can be used instead.",
|
||
|
missingRefs: "Pass empty schema with $id that should be ignored to ajv.addSchema.",
|
||
|
processCode: "Use option `code: {process: (code, schemaEnv: object) => string}`",
|
||
|
sourceCode: "Use option `code: {source: true}`",
|
||
|
strictDefaults: "It is default now, see option `strict`.",
|
||
|
strictKeywords: "It is default now, see option `strict`.",
|
||
|
uniqueItems: '"uniqueItems" keyword is always validated.',
|
||
|
unknownFormats: "Disable strict mode or pass `true` to `ajv.addFormat` (or `formats` option).",
|
||
|
cache: "Map is used as cache, schema object as key.",
|
||
|
serialize: "Map is used as cache, schema object as key.",
|
||
|
ajvErrors: "It is default now.",
|
||
|
}
|
||
|
|
||
|
const deprecatedOptions: OptionsInfo<DeprecatedOptions> = {
|
||
|
ignoreKeywordsWithRef: "",
|
||
|
jsPropertySyntax: "",
|
||
|
unicode: '"minLength"/"maxLength" account for unicode characters by default.',
|
||
|
}
|
||
|
|
||
|
type RequiredInstanceOptions = {
|
||
|
[K in
|
||
|
| "strictSchema"
|
||
|
| "strictNumbers"
|
||
|
| "strictTypes"
|
||
|
| "strictTuples"
|
||
|
| "strictRequired"
|
||
|
| "inlineRefs"
|
||
|
| "loopRequired"
|
||
|
| "loopEnum"
|
||
|
| "meta"
|
||
|
| "messages"
|
||
|
| "schemaId"
|
||
|
| "addUsedSchema"
|
||
|
| "validateSchema"
|
||
|
| "validateFormats"
|
||
|
| "int32range"
|
||
|
| "unicodeRegExp"
|
||
|
| "uriResolver"]: NonNullable<Options[K]>
|
||
|
} & {code: InstanceCodeOptions}
|
||
|
|
||
|
export type InstanceOptions = Options & RequiredInstanceOptions
|
||
|
|
||
|
const MAX_EXPRESSION = 200
|
||
|
|
||
|
// eslint-disable-next-line complexity
|
||
|
function requiredOptions(o: Options): RequiredInstanceOptions {
|
||
|
const s = o.strict
|
||
|
const _optz = o.code?.optimize
|
||
|
const optimize = _optz === true || _optz === undefined ? 1 : _optz || 0
|
||
|
const regExp = o.code?.regExp ?? defaultRegExp
|
||
|
const uriResolver = o.uriResolver ?? DefaultUriResolver
|
||
|
return {
|
||
|
strictSchema: o.strictSchema ?? s ?? true,
|
||
|
strictNumbers: o.strictNumbers ?? s ?? true,
|
||
|
strictTypes: o.strictTypes ?? s ?? "log",
|
||
|
strictTuples: o.strictTuples ?? s ?? "log",
|
||
|
strictRequired: o.strictRequired ?? s ?? false,
|
||
|
code: o.code ? {...o.code, optimize, regExp} : {optimize, regExp},
|
||
|
loopRequired: o.loopRequired ?? MAX_EXPRESSION,
|
||
|
loopEnum: o.loopEnum ?? MAX_EXPRESSION,
|
||
|
meta: o.meta ?? true,
|
||
|
messages: o.messages ?? true,
|
||
|
inlineRefs: o.inlineRefs ?? true,
|
||
|
schemaId: o.schemaId ?? "$id",
|
||
|
addUsedSchema: o.addUsedSchema ?? true,
|
||
|
validateSchema: o.validateSchema ?? true,
|
||
|
validateFormats: o.validateFormats ?? true,
|
||
|
unicodeRegExp: o.unicodeRegExp ?? true,
|
||
|
int32range: o.int32range ?? true,
|
||
|
uriResolver: uriResolver,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export interface Logger {
|
||
|
log(...args: unknown[]): unknown
|
||
|
warn(...args: unknown[]): unknown
|
||
|
error(...args: unknown[]): unknown
|
||
|
}
|
||
|
|
||
|
export default class Ajv {
|
||
|
opts: InstanceOptions
|
||
|
errors?: ErrorObject[] | null // errors from the last validation
|
||
|
logger: Logger
|
||
|
// shared external scope values for compiled functions
|
||
|
readonly scope: ValueScope
|
||
|
readonly schemas: {[Key in string]?: SchemaEnv} = {}
|
||
|
readonly refs: {[Ref in string]?: SchemaEnv | string} = {}
|
||
|
readonly formats: {[Name in string]?: AddedFormat} = {}
|
||
|
readonly RULES: ValidationRules
|
||
|
readonly _compilations: Set<SchemaEnv> = new Set()
|
||
|
private readonly _loading: {[Ref in string]?: Promise<AnySchemaObject>} = {}
|
||
|
private readonly _cache: Map<AnySchema, SchemaEnv> = new Map()
|
||
|
private readonly _metaOpts: InstanceOptions
|
||
|
|
||
|
static ValidationError = ValidationError
|
||
|
static MissingRefError = MissingRefError
|
||
|
|
||
|
constructor(opts: Options = {}) {
|
||
|
opts = this.opts = {...opts, ...requiredOptions(opts)}
|
||
|
const {es5, lines} = this.opts.code
|
||
|
|
||
|
this.scope = new ValueScope({scope: {}, prefixes: EXT_SCOPE_NAMES, es5, lines})
|
||
|
this.logger = getLogger(opts.logger)
|
||
|
const formatOpt = opts.validateFormats
|
||
|
opts.validateFormats = false
|
||
|
|
||
|
this.RULES = getRules()
|
||
|
checkOptions.call(this, removedOptions, opts, "NOT SUPPORTED")
|
||
|
checkOptions.call(this, deprecatedOptions, opts, "DEPRECATED", "warn")
|
||
|
this._metaOpts = getMetaSchemaOptions.call(this)
|
||
|
|
||
|
if (opts.formats) addInitialFormats.call(this)
|
||
|
this._addVocabularies()
|
||
|
this._addDefaultMetaSchema()
|
||
|
if (opts.keywords) addInitialKeywords.call(this, opts.keywords)
|
||
|
if (typeof opts.meta == "object") this.addMetaSchema(opts.meta)
|
||
|
addInitialSchemas.call(this)
|
||
|
opts.validateFormats = formatOpt
|
||
|
}
|
||
|
|
||
|
_addVocabularies(): void {
|
||
|
this.addKeyword("$async")
|
||
|
}
|
||
|
|
||
|
_addDefaultMetaSchema(): void {
|
||
|
const {$data, meta, schemaId} = this.opts
|
||
|
let _dataRefSchema: SchemaObject = $dataRefSchema
|
||
|
if (schemaId === "id") {
|
||
|
_dataRefSchema = {...$dataRefSchema}
|
||
|
_dataRefSchema.id = _dataRefSchema.$id
|
||
|
delete _dataRefSchema.$id
|
||
|
}
|
||
|
if (meta && $data) this.addMetaSchema(_dataRefSchema, _dataRefSchema[schemaId], false)
|
||
|
}
|
||
|
|
||
|
defaultMeta(): string | AnySchemaObject | undefined {
|
||
|
const {meta, schemaId} = this.opts
|
||
|
return (this.opts.defaultMeta = typeof meta == "object" ? meta[schemaId] || meta : undefined)
|
||
|
}
|
||
|
|
||
|
// Validate data using schema
|
||
|
// AnySchema will be compiled and cached using schema itself as a key for Map
|
||
|
validate(schema: Schema | string, data: unknown): boolean
|
||
|
validate(schemaKeyRef: AnySchema | string, data: unknown): boolean | Promise<unknown>
|
||
|
validate<T>(schema: Schema | JSONSchemaType<T> | string, data: unknown): data is T
|
||
|
// Separated for type inference to work
|
||
|
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||
|
validate<T>(schema: JTDSchemaType<T>, data: unknown): data is T
|
||
|
// This overload is only intended for typescript inference, the first
|
||
|
// argument prevents manual type annotation from matching this overload
|
||
|
validate<N extends never, T extends SomeJTDSchemaType>(
|
||
|
schema: T,
|
||
|
data: unknown
|
||
|
): data is JTDDataType<T>
|
||
|
validate<T>(schema: AsyncSchema, data: unknown | T): Promise<T>
|
||
|
validate<T>(schemaKeyRef: AnySchema | string, data: unknown): data is T | Promise<T>
|
||
|
validate<T>(
|
||
|
schemaKeyRef: AnySchema | string, // key, ref or schema object
|
||
|
data: unknown | T // to be validated
|
||
|
): boolean | Promise<T> {
|
||
|
let v: AnyValidateFunction | undefined
|
||
|
if (typeof schemaKeyRef == "string") {
|
||
|
v = this.getSchema<T>(schemaKeyRef)
|
||
|
if (!v) throw new Error(`no schema with key or ref "${schemaKeyRef}"`)
|
||
|
} else {
|
||
|
v = this.compile<T>(schemaKeyRef)
|
||
|
}
|
||
|
|
||
|
const valid = v(data)
|
||
|
if (!("$async" in v)) this.errors = v.errors
|
||
|
return valid
|
||
|
}
|
||
|
|
||
|
// Create validation function for passed schema
|
||
|
// _meta: true if schema is a meta-schema. Used internally to compile meta schemas of user-defined keywords.
|
||
|
compile<T = unknown>(schema: Schema | JSONSchemaType<T>, _meta?: boolean): ValidateFunction<T>
|
||
|
// Separated for type inference to work
|
||
|
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||
|
compile<T = unknown>(schema: JTDSchemaType<T>, _meta?: boolean): ValidateFunction<T>
|
||
|
// This overload is only intended for typescript inference, the first
|
||
|
// argument prevents manual type annotation from matching this overload
|
||
|
compile<N extends never, T extends SomeJTDSchemaType>(
|
||
|
schema: T,
|
||
|
_meta?: boolean
|
||
|
): ValidateFunction<JTDDataType<T>>
|
||
|
compile<T = unknown>(schema: AsyncSchema, _meta?: boolean): AsyncValidateFunction<T>
|
||
|
compile<T = unknown>(schema: AnySchema, _meta?: boolean): AnyValidateFunction<T>
|
||
|
compile<T = unknown>(schema: AnySchema, _meta?: boolean): AnyValidateFunction<T> {
|
||
|
const sch = this._addSchema(schema, _meta)
|
||
|
return (sch.validate || this._compileSchemaEnv(sch)) as AnyValidateFunction<T>
|
||
|
}
|
||
|
|
||
|
// Creates validating function for passed schema with asynchronous loading of missing schemas.
|
||
|
// `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema.
|
||
|
// TODO allow passing schema URI
|
||
|
// meta - optional true to compile meta-schema
|
||
|
compileAsync<T = unknown>(
|
||
|
schema: SchemaObject | JSONSchemaType<T>,
|
||
|
_meta?: boolean
|
||
|
): Promise<ValidateFunction<T>>
|
||
|
// Separated for type inference to work
|
||
|
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||
|
compileAsync<T = unknown>(schema: JTDSchemaType<T>, _meta?: boolean): Promise<ValidateFunction<T>>
|
||
|
compileAsync<T = unknown>(schema: AsyncSchema, meta?: boolean): Promise<AsyncValidateFunction<T>>
|
||
|
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||
|
compileAsync<T = unknown>(
|
||
|
schema: AnySchemaObject,
|
||
|
meta?: boolean
|
||
|
): Promise<AnyValidateFunction<T>>
|
||
|
compileAsync<T = unknown>(
|
||
|
schema: AnySchemaObject,
|
||
|
meta?: boolean
|
||
|
): Promise<AnyValidateFunction<T>> {
|
||
|
if (typeof this.opts.loadSchema != "function") {
|
||
|
throw new Error("options.loadSchema should be a function")
|
||
|
}
|
||
|
const {loadSchema} = this.opts
|
||
|
return runCompileAsync.call(this, schema, meta)
|
||
|
|
||
|
async function runCompileAsync(
|
||
|
this: Ajv,
|
||
|
_schema: AnySchemaObject,
|
||
|
_meta?: boolean
|
||
|
): Promise<AnyValidateFunction> {
|
||
|
await loadMetaSchema.call(this, _schema.$schema)
|
||
|
const sch = this._addSchema(_schema, _meta)
|
||
|
return sch.validate || _compileAsync.call(this, sch)
|
||
|
}
|
||
|
|
||
|
async function loadMetaSchema(this: Ajv, $ref?: string): Promise<void> {
|
||
|
if ($ref && !this.getSchema($ref)) {
|
||
|
await runCompileAsync.call(this, {$ref}, true)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async function _compileAsync(this: Ajv, sch: SchemaEnv): Promise<AnyValidateFunction> {
|
||
|
try {
|
||
|
return this._compileSchemaEnv(sch)
|
||
|
} catch (e) {
|
||
|
if (!(e instanceof MissingRefError)) throw e
|
||
|
checkLoaded.call(this, e)
|
||
|
await loadMissingSchema.call(this, e.missingSchema)
|
||
|
return _compileAsync.call(this, sch)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function checkLoaded(this: Ajv, {missingSchema: ref, missingRef}: MissingRefError): void {
|
||
|
if (this.refs[ref]) {
|
||
|
throw new Error(`AnySchema ${ref} is loaded but ${missingRef} cannot be resolved`)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async function loadMissingSchema(this: Ajv, ref: string): Promise<void> {
|
||
|
const _schema = await _loadSchema.call(this, ref)
|
||
|
if (!this.refs[ref]) await loadMetaSchema.call(this, _schema.$schema)
|
||
|
if (!this.refs[ref]) this.addSchema(_schema, ref, meta)
|
||
|
}
|
||
|
|
||
|
async function _loadSchema(this: Ajv, ref: string): Promise<AnySchemaObject> {
|
||
|
const p = this._loading[ref]
|
||
|
if (p) return p
|
||
|
try {
|
||
|
return await (this._loading[ref] = loadSchema(ref))
|
||
|
} finally {
|
||
|
delete this._loading[ref]
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Adds schema to the instance
|
||
|
addSchema(
|
||
|
schema: AnySchema | AnySchema[], // If array is passed, `key` will be ignored
|
||
|
key?: string, // Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`.
|
||
|
_meta?: boolean, // true if schema is a meta-schema. Used internally, addMetaSchema should be used instead.
|
||
|
_validateSchema = this.opts.validateSchema // false to skip schema validation. Used internally, option validateSchema should be used instead.
|
||
|
): Ajv {
|
||
|
if (Array.isArray(schema)) {
|
||
|
for (const sch of schema) this.addSchema(sch, undefined, _meta, _validateSchema)
|
||
|
return this
|
||
|
}
|
||
|
let id: string | undefined
|
||
|
if (typeof schema === "object") {
|
||
|
const {schemaId} = this.opts
|
||
|
id = schema[schemaId]
|
||
|
if (id !== undefined && typeof id != "string") {
|
||
|
throw new Error(`schema ${schemaId} must be string`)
|
||
|
}
|
||
|
}
|
||
|
key = normalizeId(key || id)
|
||
|
this._checkUnique(key)
|
||
|
this.schemas[key] = this._addSchema(schema, _meta, key, _validateSchema, true)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
// Add schema that will be used to validate other schemas
|
||
|
// options in META_IGNORE_OPTIONS are alway set to false
|
||
|
addMetaSchema(
|
||
|
schema: AnySchemaObject,
|
||
|
key?: string, // schema key
|
||
|
_validateSchema = this.opts.validateSchema // false to skip schema validation, can be used to override validateSchema option for meta-schema
|
||
|
): Ajv {
|
||
|
this.addSchema(schema, key, true, _validateSchema)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
// Validate schema against its meta-schema
|
||
|
validateSchema(schema: AnySchema, throwOrLogError?: boolean): boolean | Promise<unknown> {
|
||
|
if (typeof schema == "boolean") return true
|
||
|
let $schema: string | AnySchemaObject | undefined
|
||
|
$schema = schema.$schema
|
||
|
if ($schema !== undefined && typeof $schema != "string") {
|
||
|
throw new Error("$schema must be a string")
|
||
|
}
|
||
|
$schema = $schema || this.opts.defaultMeta || this.defaultMeta()
|
||
|
if (!$schema) {
|
||
|
this.logger.warn("meta-schema not available")
|
||
|
this.errors = null
|
||
|
return true
|
||
|
}
|
||
|
const valid = this.validate($schema, schema)
|
||
|
if (!valid && throwOrLogError) {
|
||
|
const message = "schema is invalid: " + this.errorsText()
|
||
|
if (this.opts.validateSchema === "log") this.logger.error(message)
|
||
|
else throw new Error(message)
|
||
|
}
|
||
|
return valid
|
||
|
}
|
||
|
|
||
|
// Get compiled schema by `key` or `ref`.
|
||
|
// (`key` that was passed to `addSchema` or full schema reference - `schema.$id` or resolved id)
|
||
|
getSchema<T = unknown>(keyRef: string): AnyValidateFunction<T> | undefined {
|
||
|
let sch
|
||
|
while (typeof (sch = getSchEnv.call(this, keyRef)) == "string") keyRef = sch
|
||
|
if (sch === undefined) {
|
||
|
const {schemaId} = this.opts
|
||
|
const root = new SchemaEnv({schema: {}, schemaId})
|
||
|
sch = resolveSchema.call(this, root, keyRef)
|
||
|
if (!sch) return
|
||
|
this.refs[keyRef] = sch
|
||
|
}
|
||
|
return (sch.validate || this._compileSchemaEnv(sch)) as AnyValidateFunction<T> | undefined
|
||
|
}
|
||
|
|
||
|
// Remove cached schema(s).
|
||
|
// If no parameter is passed all schemas but meta-schemas are removed.
|
||
|
// If RegExp is passed all schemas with key/id matching pattern but meta-schemas are removed.
|
||
|
// Even if schema is referenced by other schemas it still can be removed as other schemas have local references.
|
||
|
removeSchema(schemaKeyRef?: AnySchema | string | RegExp): Ajv {
|
||
|
if (schemaKeyRef instanceof RegExp) {
|
||
|
this._removeAllSchemas(this.schemas, schemaKeyRef)
|
||
|
this._removeAllSchemas(this.refs, schemaKeyRef)
|
||
|
return this
|
||
|
}
|
||
|
switch (typeof schemaKeyRef) {
|
||
|
case "undefined":
|
||
|
this._removeAllSchemas(this.schemas)
|
||
|
this._removeAllSchemas(this.refs)
|
||
|
this._cache.clear()
|
||
|
return this
|
||
|
case "string": {
|
||
|
const sch = getSchEnv.call(this, schemaKeyRef)
|
||
|
if (typeof sch == "object") this._cache.delete(sch.schema)
|
||
|
delete this.schemas[schemaKeyRef]
|
||
|
delete this.refs[schemaKeyRef]
|
||
|
return this
|
||
|
}
|
||
|
case "object": {
|
||
|
const cacheKey = schemaKeyRef
|
||
|
this._cache.delete(cacheKey)
|
||
|
let id = schemaKeyRef[this.opts.schemaId]
|
||
|
if (id) {
|
||
|
id = normalizeId(id)
|
||
|
delete this.schemas[id]
|
||
|
delete this.refs[id]
|
||
|
}
|
||
|
return this
|
||
|
}
|
||
|
default:
|
||
|
throw new Error("ajv.removeSchema: invalid parameter")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// add "vocabulary" - a collection of keywords
|
||
|
addVocabulary(definitions: Vocabulary): Ajv {
|
||
|
for (const def of definitions) this.addKeyword(def)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
addKeyword(
|
||
|
kwdOrDef: string | KeywordDefinition,
|
||
|
def?: KeywordDefinition // deprecated
|
||
|
): Ajv {
|
||
|
let keyword: string | string[]
|
||
|
if (typeof kwdOrDef == "string") {
|
||
|
keyword = kwdOrDef
|
||
|
if (typeof def == "object") {
|
||
|
this.logger.warn("these parameters are deprecated, see docs for addKeyword")
|
||
|
def.keyword = keyword
|
||
|
}
|
||
|
} else if (typeof kwdOrDef == "object" && def === undefined) {
|
||
|
def = kwdOrDef
|
||
|
keyword = def.keyword
|
||
|
if (Array.isArray(keyword) && !keyword.length) {
|
||
|
throw new Error("addKeywords: keyword must be string or non-empty array")
|
||
|
}
|
||
|
} else {
|
||
|
throw new Error("invalid addKeywords parameters")
|
||
|
}
|
||
|
|
||
|
checkKeyword.call(this, keyword, def)
|
||
|
if (!def) {
|
||
|
eachItem(keyword, (kwd) => addRule.call(this, kwd))
|
||
|
return this
|
||
|
}
|
||
|
keywordMetaschema.call(this, def)
|
||
|
const definition: AddedKeywordDefinition = {
|
||
|
...def,
|
||
|
type: getJSONTypes(def.type),
|
||
|
schemaType: getJSONTypes(def.schemaType),
|
||
|
}
|
||
|
eachItem(
|
||
|
keyword,
|
||
|
definition.type.length === 0
|
||
|
? (k) => addRule.call(this, k, definition)
|
||
|
: (k) => definition.type.forEach((t) => addRule.call(this, k, definition, t))
|
||
|
)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
getKeyword(keyword: string): AddedKeywordDefinition | boolean {
|
||
|
const rule = this.RULES.all[keyword]
|
||
|
return typeof rule == "object" ? rule.definition : !!rule
|
||
|
}
|
||
|
|
||
|
// Remove keyword
|
||
|
removeKeyword(keyword: string): Ajv {
|
||
|
// TODO return type should be Ajv
|
||
|
const {RULES} = this
|
||
|
delete RULES.keywords[keyword]
|
||
|
delete RULES.all[keyword]
|
||
|
for (const group of RULES.rules) {
|
||
|
const i = group.rules.findIndex((rule) => rule.keyword === keyword)
|
||
|
if (i >= 0) group.rules.splice(i, 1)
|
||
|
}
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
// Add format
|
||
|
addFormat(name: string, format: Format): Ajv {
|
||
|
if (typeof format == "string") format = new RegExp(format)
|
||
|
this.formats[name] = format
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
errorsText(
|
||
|
errors: ErrorObject[] | null | undefined = this.errors, // optional array of validation errors
|
||
|
{separator = ", ", dataVar = "data"}: ErrorsTextOptions = {} // optional options with properties `separator` and `dataVar`
|
||
|
): string {
|
||
|
if (!errors || errors.length === 0) return "No errors"
|
||
|
return errors
|
||
|
.map((e) => `${dataVar}${e.instancePath} ${e.message}`)
|
||
|
.reduce((text, msg) => text + separator + msg)
|
||
|
}
|
||
|
|
||
|
$dataMetaSchema(metaSchema: AnySchemaObject, keywordsJsonPointers: string[]): AnySchemaObject {
|
||
|
const rules = this.RULES.all
|
||
|
metaSchema = JSON.parse(JSON.stringify(metaSchema))
|
||
|
for (const jsonPointer of keywordsJsonPointers) {
|
||
|
const segments = jsonPointer.split("/").slice(1) // first segment is an empty string
|
||
|
let keywords = metaSchema
|
||
|
for (const seg of segments) keywords = keywords[seg] as AnySchemaObject
|
||
|
|
||
|
for (const key in rules) {
|
||
|
const rule = rules[key]
|
||
|
if (typeof rule != "object") continue
|
||
|
const {$data} = rule.definition
|
||
|
const schema = keywords[key] as AnySchemaObject | undefined
|
||
|
if ($data && schema) keywords[key] = schemaOrData(schema)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return metaSchema
|
||
|
}
|
||
|
|
||
|
private _removeAllSchemas(schemas: {[Ref in string]?: SchemaEnv | string}, regex?: RegExp): void {
|
||
|
for (const keyRef in schemas) {
|
||
|
const sch = schemas[keyRef]
|
||
|
if (!regex || regex.test(keyRef)) {
|
||
|
if (typeof sch == "string") {
|
||
|
delete schemas[keyRef]
|
||
|
} else if (sch && !sch.meta) {
|
||
|
this._cache.delete(sch.schema)
|
||
|
delete schemas[keyRef]
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_addSchema(
|
||
|
schema: AnySchema,
|
||
|
meta?: boolean,
|
||
|
baseId?: string,
|
||
|
validateSchema = this.opts.validateSchema,
|
||
|
addSchema = this.opts.addUsedSchema
|
||
|
): SchemaEnv {
|
||
|
let id: string | undefined
|
||
|
const {schemaId} = this.opts
|
||
|
if (typeof schema == "object") {
|
||
|
id = schema[schemaId]
|
||
|
} else {
|
||
|
if (this.opts.jtd) throw new Error("schema must be object")
|
||
|
else if (typeof schema != "boolean") throw new Error("schema must be object or boolean")
|
||
|
}
|
||
|
let sch = this._cache.get(schema)
|
||
|
if (sch !== undefined) return sch
|
||
|
|
||
|
baseId = normalizeId(id || baseId)
|
||
|
const localRefs = getSchemaRefs.call(this, schema, baseId)
|
||
|
sch = new SchemaEnv({schema, schemaId, meta, baseId, localRefs})
|
||
|
this._cache.set(sch.schema, sch)
|
||
|
if (addSchema && !baseId.startsWith("#")) {
|
||
|
// TODO atm it is allowed to overwrite schemas without id (instead of not adding them)
|
||
|
if (baseId) this._checkUnique(baseId)
|
||
|
this.refs[baseId] = sch
|
||
|
}
|
||
|
if (validateSchema) this.validateSchema(schema, true)
|
||
|
return sch
|
||
|
}
|
||
|
|
||
|
private _checkUnique(id: string): void {
|
||
|
if (this.schemas[id] || this.refs[id]) {
|
||
|
throw new Error(`schema with key or id "${id}" already exists`)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private _compileSchemaEnv(sch: SchemaEnv): AnyValidateFunction {
|
||
|
if (sch.meta) this._compileMetaSchema(sch)
|
||
|
else compileSchema.call(this, sch)
|
||
|
|
||
|
/* istanbul ignore if */
|
||
|
if (!sch.validate) throw new Error("ajv implementation error")
|
||
|
return sch.validate
|
||
|
}
|
||
|
|
||
|
private _compileMetaSchema(sch: SchemaEnv): void {
|
||
|
const currentOpts = this.opts
|
||
|
this.opts = this._metaOpts
|
||
|
try {
|
||
|
compileSchema.call(this, sch)
|
||
|
} finally {
|
||
|
this.opts = currentOpts
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export interface ErrorsTextOptions {
|
||
|
separator?: string
|
||
|
dataVar?: string
|
||
|
}
|
||
|
|
||
|
function checkOptions(
|
||
|
this: Ajv,
|
||
|
checkOpts: OptionsInfo<RemovedOptions | DeprecatedOptions>,
|
||
|
options: Options & RemovedOptions,
|
||
|
msg: string,
|
||
|
log: "warn" | "error" = "error"
|
||
|
): void {
|
||
|
for (const key in checkOpts) {
|
||
|
const opt = key as keyof typeof checkOpts
|
||
|
if (opt in options) this.logger[log](`${msg}: option ${key}. ${checkOpts[opt]}`)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function getSchEnv(this: Ajv, keyRef: string): SchemaEnv | string | undefined {
|
||
|
keyRef = normalizeId(keyRef) // TODO tests fail without this line
|
||
|
return this.schemas[keyRef] || this.refs[keyRef]
|
||
|
}
|
||
|
|
||
|
function addInitialSchemas(this: Ajv): void {
|
||
|
const optsSchemas = this.opts.schemas
|
||
|
if (!optsSchemas) return
|
||
|
if (Array.isArray(optsSchemas)) this.addSchema(optsSchemas)
|
||
|
else for (const key in optsSchemas) this.addSchema(optsSchemas[key] as AnySchema, key)
|
||
|
}
|
||
|
|
||
|
function addInitialFormats(this: Ajv): void {
|
||
|
for (const name in this.opts.formats) {
|
||
|
const format = this.opts.formats[name]
|
||
|
if (format) this.addFormat(name, format)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function addInitialKeywords(
|
||
|
this: Ajv,
|
||
|
defs: Vocabulary | {[K in string]?: KeywordDefinition}
|
||
|
): void {
|
||
|
if (Array.isArray(defs)) {
|
||
|
this.addVocabulary(defs)
|
||
|
return
|
||
|
}
|
||
|
this.logger.warn("keywords option as map is deprecated, pass array")
|
||
|
for (const keyword in defs) {
|
||
|
const def = defs[keyword] as KeywordDefinition
|
||
|
if (!def.keyword) def.keyword = keyword
|
||
|
this.addKeyword(def)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function getMetaSchemaOptions(this: Ajv): InstanceOptions {
|
||
|
const metaOpts = {...this.opts}
|
||
|
for (const opt of META_IGNORE_OPTIONS) delete metaOpts[opt]
|
||
|
return metaOpts
|
||
|
}
|
||
|
|
||
|
const noLogs = {log() {}, warn() {}, error() {}}
|
||
|
|
||
|
function getLogger(logger?: Partial<Logger> | false): Logger {
|
||
|
if (logger === false) return noLogs
|
||
|
if (logger === undefined) return console
|
||
|
if (logger.log && logger.warn && logger.error) return logger as Logger
|
||
|
throw new Error("logger must implement log, warn and error methods")
|
||
|
}
|
||
|
|
||
|
const KEYWORD_NAME = /^[a-z_$][a-z0-9_$:-]*$/i
|
||
|
|
||
|
function checkKeyword(this: Ajv, keyword: string | string[], def?: KeywordDefinition): void {
|
||
|
const {RULES} = this
|
||
|
eachItem(keyword, (kwd) => {
|
||
|
if (RULES.keywords[kwd]) throw new Error(`Keyword ${kwd} is already defined`)
|
||
|
if (!KEYWORD_NAME.test(kwd)) throw new Error(`Keyword ${kwd} has invalid name`)
|
||
|
})
|
||
|
if (!def) return
|
||
|
if (def.$data && !("code" in def || "validate" in def)) {
|
||
|
throw new Error('$data keyword must have "code" or "validate" function')
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function addRule(
|
||
|
this: Ajv,
|
||
|
keyword: string,
|
||
|
definition?: AddedKeywordDefinition,
|
||
|
dataType?: JSONType
|
||
|
): void {
|
||
|
const post = definition?.post
|
||
|
if (dataType && post) throw new Error('keyword with "post" flag cannot have "type"')
|
||
|
const {RULES} = this
|
||
|
let ruleGroup = post ? RULES.post : RULES.rules.find(({type: t}) => t === dataType)
|
||
|
if (!ruleGroup) {
|
||
|
ruleGroup = {type: dataType, rules: []}
|
||
|
RULES.rules.push(ruleGroup)
|
||
|
}
|
||
|
RULES.keywords[keyword] = true
|
||
|
if (!definition) return
|
||
|
|
||
|
const rule: Rule = {
|
||
|
keyword,
|
||
|
definition: {
|
||
|
...definition,
|
||
|
type: getJSONTypes(definition.type),
|
||
|
schemaType: getJSONTypes(definition.schemaType),
|
||
|
},
|
||
|
}
|
||
|
if (definition.before) addBeforeRule.call(this, ruleGroup, rule, definition.before)
|
||
|
else ruleGroup.rules.push(rule)
|
||
|
RULES.all[keyword] = rule
|
||
|
definition.implements?.forEach((kwd) => this.addKeyword(kwd))
|
||
|
}
|
||
|
|
||
|
function addBeforeRule(this: Ajv, ruleGroup: RuleGroup, rule: Rule, before: string): void {
|
||
|
const i = ruleGroup.rules.findIndex((_rule) => _rule.keyword === before)
|
||
|
if (i >= 0) {
|
||
|
ruleGroup.rules.splice(i, 0, rule)
|
||
|
} else {
|
||
|
ruleGroup.rules.push(rule)
|
||
|
this.logger.warn(`rule ${before} is not defined`)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function keywordMetaschema(this: Ajv, def: KeywordDefinition): void {
|
||
|
let {metaSchema} = def
|
||
|
if (metaSchema === undefined) return
|
||
|
if (def.$data && this.opts.$data) metaSchema = schemaOrData(metaSchema)
|
||
|
def.validateSchema = this.compile(metaSchema, true)
|
||
|
}
|
||
|
|
||
|
const $dataRef = {
|
||
|
$ref: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#",
|
||
|
}
|
||
|
|
||
|
function schemaOrData(schema: AnySchema): AnySchemaObject {
|
||
|
return {anyOf: [schema, $dataRef]}
|
||
|
}
|