Consolidate uses of AJV (#47662)
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import Ajv from 'ajv'
|
||||
import addErrors from 'ajv-errors'
|
||||
import semver from 'semver'
|
||||
|
||||
import featureVersionsSchema from '../lib/feature-versions-schema.js'
|
||||
import { getDeepDataByLanguage } from '#src/data-directory/lib/get-data.js'
|
||||
import { getJsonValidator } from '#src/tests/lib/validate-json-schema.js'
|
||||
import { formatAjvErrors } from '#src/tests/helpers/schemas.js'
|
||||
|
||||
/*
|
||||
@@ -18,26 +16,18 @@ import { formatAjvErrors } from '#src/tests/helpers/schemas.js'
|
||||
jest.useFakeTimers({ legacyFakeTimers: true })
|
||||
|
||||
const featureVersions = Object.entries(getDeepDataByLanguage('features', 'en'))
|
||||
|
||||
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true })
|
||||
addErrors(ajv)
|
||||
// *** TODO: We can drop this override once the frontmatter schema has been updated to work with AJV. ***
|
||||
ajv.addFormat('semver', {
|
||||
validate: (x) => semver.validRange(x),
|
||||
})
|
||||
// *** End TODO ***
|
||||
const validate = ajv.compile(featureVersionsSchema)
|
||||
const validate = getJsonValidator(featureVersionsSchema)
|
||||
|
||||
// Make sure data/features/*.yml contains valid versioning.
|
||||
describe('lint feature versions', () => {
|
||||
test.each(featureVersions)('data/features/%s matches the schema', (name, featureVersion) => {
|
||||
const valid = validate(featureVersion)
|
||||
const isValid = validate(featureVersion)
|
||||
let errors
|
||||
|
||||
if (!valid) {
|
||||
if (!isValid) {
|
||||
errors = formatAjvErrors(validate.errors)
|
||||
}
|
||||
|
||||
expect(valid, errors).toBe(true)
|
||||
expect(isValid, errors).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import express from 'express'
|
||||
import { omit, without, mapValues } from 'lodash-es'
|
||||
import Ajv from 'ajv'
|
||||
import addFormats from 'ajv-formats'
|
||||
import QuickLRU from 'quick-lru'
|
||||
|
||||
import { schemas, hydroNames } from './lib/schema.js'
|
||||
import catchMiddlewareError from '#src/observability/middleware/catch-middleware-error.js'
|
||||
import { noCacheControl } from '#src/frame/middleware/cache-control.js'
|
||||
import { getJsonValidator } from '#src/tests/lib/validate-json-schema.js'
|
||||
import { formatErrors } from './lib/middleware-errors.js'
|
||||
import { publish as _publish } from './lib/hydro.js'
|
||||
|
||||
const router = express.Router()
|
||||
const ajv = new Ajv()
|
||||
addFormats(ajv)
|
||||
const OMIT_FIELDS = ['type']
|
||||
const allowedTypes = new Set(without(Object.keys(schemas), 'validation'))
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
const validations = mapValues(schemas, (schema) => ajv.compile(schema))
|
||||
|
||||
const validators = mapValues(schemas, (schema) => getJsonValidator(schema))
|
||||
// In production, fire and not wait to respond.
|
||||
// _publish will send an error to failbot,
|
||||
// so we don't get alerts but we still track it.
|
||||
@@ -47,7 +43,7 @@ router.post(
|
||||
}
|
||||
|
||||
// Validate the data matches the corresponding data schema
|
||||
const validate = validations[type]
|
||||
const validate = validators[type]
|
||||
if (!validate(req.body)) {
|
||||
const hash = `${req.ip}:${validate.errors
|
||||
.map((error) => error.message + error.instancePath)
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import Ajv from 'ajv'
|
||||
import addFormats from 'ajv-formats'
|
||||
import { validateJson } from '#src/tests/lib/validate-json-schema.js'
|
||||
import { formatErrors } from '../lib/middleware-errors.js'
|
||||
import { schemas } from '../lib/schema.js'
|
||||
|
||||
const ajv = new Ajv()
|
||||
addFormats(ajv)
|
||||
|
||||
expect.extend({
|
||||
toMatchSchema(data, schema) {
|
||||
const isValid = ajv.validate(schema, data)
|
||||
const { isValid, errors } = validateJson(schema, data)
|
||||
return {
|
||||
pass: isValid,
|
||||
message: () => (isValid ? '' : ajv.errorsText()),
|
||||
message: () => (isValid ? '' : errors.message),
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -19,8 +15,9 @@ expect.extend({
|
||||
describe('formatErrors', () => {
|
||||
it('should produce objects that match the validation spec', () => {
|
||||
// Produce an error
|
||||
ajv.validate({ type: 'string' }, 0)
|
||||
for (const formatted of formatErrors(ajv.errors, '')) {
|
||||
const { errors } = validateJson({ type: 'string' }, 0)
|
||||
const formattedErrors = formatErrors(errors, '')
|
||||
for (const formatted of formattedErrors) {
|
||||
expect(formatted).toMatchSchema(schemas.validation)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,25 +1,12 @@
|
||||
import matter from 'gray-matter'
|
||||
import Ajv from 'ajv'
|
||||
import addErrors from 'ajv-errors'
|
||||
import addFormats from 'ajv-formats'
|
||||
import semver from 'semver'
|
||||
|
||||
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true })
|
||||
ajv.addKeyword({
|
||||
keyword: 'translatable',
|
||||
})
|
||||
ajv.addFormat('semver', {
|
||||
validate: (x) => semver.validRange(x),
|
||||
})
|
||||
addErrors(ajv)
|
||||
addFormats(ajv)
|
||||
import { validateJson } from '#src/tests/lib/validate-json-schema.js'
|
||||
|
||||
function readFrontmatter(markdown, opts = {}) {
|
||||
const schema = opts.schema || { type: 'object', properties: {} }
|
||||
const filepath = opts.filepath || null
|
||||
|
||||
let content, data
|
||||
let errors = []
|
||||
|
||||
try {
|
||||
;({ content, data } = matter(markdown))
|
||||
@@ -39,18 +26,13 @@ function readFrontmatter(markdown, opts = {}) {
|
||||
}
|
||||
|
||||
if (filepath) error.filepath = filepath
|
||||
errors.push(error)
|
||||
const errors = [error]
|
||||
console.warn(errors)
|
||||
|
||||
return { errors }
|
||||
}
|
||||
|
||||
const ajvValidate = ajv.compile(schema)
|
||||
const valid = ajvValidate(data)
|
||||
|
||||
if (!valid) {
|
||||
errors = ajvValidate.errors
|
||||
}
|
||||
const validate = validateJson(schema, data)
|
||||
|
||||
// Combine the AJV-supplied `instancePath` and `params` into a more user-friendly frontmatter path.
|
||||
// For example, given:
|
||||
@@ -69,8 +51,10 @@ function readFrontmatter(markdown, opts = {}) {
|
||||
return typeof mainProps !== 'object' ? `${prefixProps}.${mainProps}` : prefixProps
|
||||
}
|
||||
|
||||
if (!valid && filepath) {
|
||||
errors = ajvValidate.errors.map((error) => {
|
||||
const errors = []
|
||||
|
||||
if (!validate.isValid && filepath) {
|
||||
const formattedErrors = validate.errors.map((error) => {
|
||||
const userFriendly = {}
|
||||
userFriendly.property = cleanPropertyPath(error.params, error.instancePath)
|
||||
userFriendly.message = error.message
|
||||
@@ -78,6 +62,9 @@ function readFrontmatter(markdown, opts = {}) {
|
||||
userFriendly.filepath = filepath
|
||||
return userFriendly
|
||||
})
|
||||
errors.push(...formattedErrors)
|
||||
} else if (!validate.isValid) {
|
||||
errors.push(...validate.errors)
|
||||
}
|
||||
|
||||
return { content, data, errors }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Ajv from 'ajv'
|
||||
import { jest } from '@jest/globals'
|
||||
|
||||
import { getJsonValidator } from '#src/tests/lib/validate-json-schema.js'
|
||||
import schema from '#src/tests/helpers/schemas/site-tree-schema.js'
|
||||
import EnterpriseServerReleases from '#src/versions/lib/enterprise-server-releases.js'
|
||||
import { loadSiteTree } from '#src/frame/lib/page-data.js'
|
||||
@@ -8,8 +9,7 @@ import { formatAjvErrors } from '#src/tests/helpers/schemas.js'
|
||||
|
||||
const latestEnterpriseRelease = EnterpriseServerReleases.latest
|
||||
|
||||
const ajv = new Ajv({ allErrors: true })
|
||||
const siteTreeValidate = ajv.compile(schema.childPage)
|
||||
const siteTreeValidate = getJsonValidator(schema.childPage)
|
||||
|
||||
describe('siteTree', () => {
|
||||
jest.setTimeout(3 * 60 * 1000)
|
||||
@@ -58,14 +58,14 @@ describe('siteTree', () => {
|
||||
|
||||
function validate(currentPage) {
|
||||
;(currentPage.childPages || []).forEach((childPage) => {
|
||||
const valid = siteTreeValidate(childPage)
|
||||
const isValid = siteTreeValidate(childPage)
|
||||
let errors
|
||||
|
||||
if (!valid) {
|
||||
if (!isValid) {
|
||||
errors = `file ${childPage.page.fullPath}: ${formatAjvErrors(siteTreeValidate.errors)}`
|
||||
}
|
||||
|
||||
expect(valid, errors).toBe(true)
|
||||
expect(isValid, errors).toBe(true)
|
||||
|
||||
// Run recurisvely until we run out of child pages
|
||||
validate(childPage)
|
||||
|
||||
@@ -10,7 +10,7 @@ import yaml from 'js-yaml'
|
||||
import { getContents } from '#src/workflows/git-utils.js'
|
||||
import permissionSchema from './permission-list-schema.js'
|
||||
import enabledSchema from './enabled-list-schema.js'
|
||||
import { validateData } from '../../rest/scripts/utils/validate-data.js'
|
||||
import { validateJson } from '#src/tests/lib/validate-json-schema.js'
|
||||
|
||||
const ENABLED_APPS_DIR = 'src/github-apps/data'
|
||||
const CONFIG_FILE = 'src/github-apps/lib/config.json'
|
||||
@@ -287,12 +287,20 @@ function initAppData(storage, category, data) {
|
||||
async function validateAppData(data, pageType) {
|
||||
if (pageType.includes('permissions')) {
|
||||
for (const value of Object.values(data)) {
|
||||
validateData(value, permissionSchema)
|
||||
const { isValid, errors } = validateJson(permissionSchema, value)
|
||||
if (!isValid) {
|
||||
console.error(JSON.stringify(errors, null, 2))
|
||||
throw new Error('GitHub Apps permission schema validation failed')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const arrayItems of Object.values(data)) {
|
||||
for (const item of arrayItems) {
|
||||
validateData(item, enabledSchema)
|
||||
const { isValid, errors } = validateJson(enabledSchema, item)
|
||||
if (!isValid) {
|
||||
console.error(JSON.stringify(errors, null, 2))
|
||||
throw new Error('GitHub Apps enabled apps schema validation failed')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import Ajv from 'ajv'
|
||||
|
||||
import { getJsonValidator, validateJson } from '#src/tests/lib/validate-json-schema.js'
|
||||
import readJsonFile from '#src/frame/lib/read-json-file.js'
|
||||
import { schemaValidator, previewsValidator, upcomingChangesValidator } from '../lib/validator.js'
|
||||
import { formatAjvErrors } from '#src/tests/helpers/schemas.js'
|
||||
@@ -11,15 +11,8 @@ const allVersionValues = Object.values(allVersions)
|
||||
const graphqlVersions = allVersionValues.map((v) => v.openApiVersionName)
|
||||
const graphqlTypes = readJsonFile('./src/graphql/lib/types.json').map((t) => t.kind)
|
||||
|
||||
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true })
|
||||
const previewsValidate = ajv.compile(previewsValidator)
|
||||
const upcomingChangesValidate = ajv.compile(upcomingChangesValidator)
|
||||
// setup ajv validator functions for each graphql type (e.g. queries, mutations,
|
||||
// etc.)
|
||||
const schemaValidatorFunctions = {}
|
||||
graphqlTypes.forEach((type) => {
|
||||
schemaValidatorFunctions[type] = ajv.compile(schemaValidator[type])
|
||||
})
|
||||
const previewsValidate = getJsonValidator(previewsValidator)
|
||||
const upcomingChangesValidate = getJsonValidator(upcomingChangesValidator)
|
||||
|
||||
describe('graphql json files', () => {
|
||||
jest.setTimeout(3 * 60 * 1000)
|
||||
@@ -38,16 +31,16 @@ describe('graphql json files', () => {
|
||||
if (typeObjsTested.has(key)) return
|
||||
typeObjsTested.add(key)
|
||||
|
||||
const valid = schemaValidatorFunctions[type](typeObj)
|
||||
let errors
|
||||
const { isValid, errors } = validateJson(schemaValidator[type], typeObj)
|
||||
|
||||
if (!valid) {
|
||||
errors = `kind: ${typeObj.kind}, name: ${typeObj.name}: ${formatAjvErrors(
|
||||
schemaValidatorFunctions[type].errors,
|
||||
let formattedErrors = errors
|
||||
if (!isValid) {
|
||||
formattedErrors = `kind: ${typeObj.kind}, name: ${typeObj.name}: ${formatAjvErrors(
|
||||
errors,
|
||||
)}`
|
||||
}
|
||||
|
||||
expect(valid, errors).toBe(true)
|
||||
expect(isValid, formattedErrors).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -57,14 +50,14 @@ describe('graphql json files', () => {
|
||||
graphqlVersions.forEach((version) => {
|
||||
const previews = readJsonFile(`${GRAPHQL_DATA_DIR}/${version}/previews.json`)
|
||||
previews.forEach((preview) => {
|
||||
const valid = previewsValidate(preview)
|
||||
const isValid = previewsValidate(preview)
|
||||
let errors
|
||||
|
||||
if (!valid) {
|
||||
if (!isValid) {
|
||||
errors = formatAjvErrors(previewsValidate.errors)
|
||||
}
|
||||
|
||||
expect(valid, errors).toBe(true)
|
||||
expect(isValid, errors).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -75,14 +68,14 @@ describe('graphql json files', () => {
|
||||
for (const changes of Object.values(upcomingChanges)) {
|
||||
// each object value is an array of changes
|
||||
changes.forEach((changeObj) => {
|
||||
const valid = upcomingChangesValidate(changeObj)
|
||||
const isValid = upcomingChangesValidate(changeObj)
|
||||
let errors
|
||||
|
||||
if (!valid) {
|
||||
if (!isValid) {
|
||||
errors = formatAjvErrors(upcomingChangesValidate.errors)
|
||||
}
|
||||
|
||||
expect(valid, errors).toBe(true)
|
||||
expect(isValid, errors).toBe(true)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,10 +6,10 @@ import { jest } from '@jest/globals'
|
||||
import { liquid } from '#src/content-render/index.js'
|
||||
import learningTracksSchema from '../lib/learning-tracks-schema.js'
|
||||
import { formatAjvErrors } from '#src/tests/helpers/schemas.js'
|
||||
import { ajvValidate } from '#src/tests/lib/ajv-validate.js'
|
||||
import { getJsonValidator } from '#src/tests/lib/validate-json-schema.js'
|
||||
|
||||
const learningTrackRootPath = 'data/learning-tracks'
|
||||
const jsonValidator = ajvValidate(learningTracksSchema)
|
||||
const validate = getJsonValidator(learningTracksSchema)
|
||||
const yamlWalkOptions = {
|
||||
globs: ['**/*.yml'],
|
||||
directories: false,
|
||||
@@ -31,14 +31,14 @@ describe('lint learning tracks', () => {
|
||||
})
|
||||
|
||||
it('matches the schema', () => {
|
||||
const valid = jsonValidator(yamlContent)
|
||||
const isValid = validate(yamlContent)
|
||||
let errors
|
||||
|
||||
if (!valid) {
|
||||
errors = formatAjvErrors(jsonValidator.errors)
|
||||
if (!isValid) {
|
||||
errors = formatAjvErrors(validate.errors)
|
||||
}
|
||||
|
||||
expect(valid, errors).toBe(true)
|
||||
expect(isValid, errors).toBe(true)
|
||||
})
|
||||
|
||||
it('contains valid liquid', () => {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import Ajv from 'ajv'
|
||||
import { getJsonValidator } from '#src/tests/lib/validate-json-schema.js'
|
||||
import { productMap } from '#src/products/lib/all-products.js'
|
||||
import { formatAjvErrors } from '#src/tests/helpers/schemas.js'
|
||||
import schema from '#src/tests/helpers/schemas/products-schema.js'
|
||||
|
||||
const ajv = new Ajv({ allErrors: true })
|
||||
const validate = ajv.compile(schema)
|
||||
const validate = getJsonValidator(schema)
|
||||
|
||||
describe('products module', () => {
|
||||
test('is an object with product ids as keys', () => {
|
||||
@@ -14,13 +13,13 @@ describe('products module', () => {
|
||||
|
||||
test('every product is valid', () => {
|
||||
Object.values(productMap).forEach((product) => {
|
||||
const valid = validate(product)
|
||||
const isValid = validate(product)
|
||||
let errors
|
||||
|
||||
if (!valid) {
|
||||
errors = formatAjvErrors(valid.errors)
|
||||
if (!isValid) {
|
||||
errors = formatAjvErrors(validate.errors)
|
||||
}
|
||||
expect(valid, errors).toBe(true)
|
||||
expect(isValid, errors).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,10 +5,10 @@ import { jest } from '@jest/globals'
|
||||
|
||||
import releaseNotesSchema from '../lib/release-notes-schema.js'
|
||||
import { formatAjvErrors } from '#src/tests/helpers/schemas.js'
|
||||
import { ajvValidate } from '#src/tests/lib/ajv-validate.js'
|
||||
import { getJsonValidator } from '#src/tests/lib/validate-json-schema.js'
|
||||
|
||||
const ghesReleaseNoteRootPath = 'data/release-notes'
|
||||
const jsonValidator = ajvValidate(releaseNotesSchema)
|
||||
const validate = getJsonValidator(releaseNotesSchema)
|
||||
const yamlWalkOptions = {
|
||||
globs: ['**/*.yml'],
|
||||
directories: false,
|
||||
@@ -29,14 +29,14 @@ describe('lint enterprise release notes', () => {
|
||||
})
|
||||
|
||||
it('matches the schema', () => {
|
||||
const valid = jsonValidator(yamlContent)
|
||||
const isValid = validate(yamlContent)
|
||||
let errors
|
||||
|
||||
if (!valid) {
|
||||
errors = formatAjvErrors(jsonValidator.errors)
|
||||
if (!isValid) {
|
||||
errors = formatAjvErrors(validate.errors)
|
||||
}
|
||||
|
||||
expect(valid, errors).toBe(true)
|
||||
expect(isValid, errors).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ import mergeAllOf from 'json-schema-merge-allof'
|
||||
import { renderContent } from '#src/content-render/index.js'
|
||||
import getCodeSamples from './create-rest-examples.js'
|
||||
import operationSchema from './operation-schema.js'
|
||||
import { validateData } from './validate-data.js'
|
||||
import { validateJson } from '#src/tests/lib/validate-json-schema.js'
|
||||
import { getBodyParams } from './get-body-params.js'
|
||||
|
||||
export default class Operation {
|
||||
@@ -59,7 +59,11 @@ export default class Operation {
|
||||
this.renderPreviewNotes(),
|
||||
])
|
||||
|
||||
validateData(this, operationSchema)
|
||||
const { isValid, errors } = validateJson(operationSchema, this)
|
||||
if (!isValid) {
|
||||
console.error(JSON.stringify(errors, null, 2))
|
||||
throw new Error('Invalid OpenAPI operation found')
|
||||
}
|
||||
}
|
||||
|
||||
async renderDescription() {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import Ajv from 'ajv'
|
||||
const ajv = new Ajv()
|
||||
|
||||
export async function validateData(data, schema) {
|
||||
const valid = ajv.validate(schema, data)
|
||||
if (!valid) {
|
||||
console.error(JSON.stringify(ajv.errors, null, 2))
|
||||
throw new Error('Invalid OpenAPI operation found')
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import fs from 'fs'
|
||||
import yaml from 'js-yaml'
|
||||
import { jest } from '@jest/globals'
|
||||
|
||||
import { ajvValidate } from '#src/tests/lib/ajv-validate.js'
|
||||
import { getJsonValidator } from '#src/tests/lib/validate-json-schema.js'
|
||||
import { formatAjvErrors } from '#src/tests/helpers/schemas.js'
|
||||
import secretScanningSchema from '../lib/secret-scanning-schema.js'
|
||||
|
||||
@@ -10,16 +10,16 @@ jest.useFakeTimers({ legacyFakeTimers: true })
|
||||
|
||||
describe('lint secret-scanning', () => {
|
||||
const yamlContent = yaml.load(fs.readFileSync('data/secret-scanning.yml', 'utf8'))
|
||||
const jsonValidate = ajvValidate(secretScanningSchema)
|
||||
const validate = getJsonValidator(secretScanningSchema)
|
||||
|
||||
test('matches the schema', () => {
|
||||
const valid = jsonValidate(yamlContent)
|
||||
const isValid = validate(yamlContent)
|
||||
let errors
|
||||
|
||||
if (!valid) {
|
||||
errors = formatAjvErrors(jsonValidate.errors)
|
||||
if (!isValid) {
|
||||
errors = formatAjvErrors(validate.errors)
|
||||
}
|
||||
|
||||
expect(valid, errors).toBe(true)
|
||||
expect(isValid, errors).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import Ajv from 'ajv'
|
||||
import addErrors from 'ajv-errors'
|
||||
import addFormats from 'ajv-formats'
|
||||
import semver from 'semver'
|
||||
|
||||
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true })
|
||||
addFormats(ajv)
|
||||
addErrors(ajv)
|
||||
// *** TODO: We can drop this override once the frontmatter schema has been updated to work with AJV. ***
|
||||
ajv.addFormat('semver', {
|
||||
validate: (x) => semver.validRange(x),
|
||||
})
|
||||
|
||||
export function ajvValidate(schema) {
|
||||
return ajv.compile(schema)
|
||||
}
|
||||
41
src/tests/lib/validate-json-schema.js
Normal file
41
src/tests/lib/validate-json-schema.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import Ajv from 'ajv'
|
||||
import addErrors from 'ajv-errors'
|
||||
import addFormats from 'ajv-formats'
|
||||
import semver from 'semver'
|
||||
|
||||
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true })
|
||||
addFormats(ajv)
|
||||
addErrors(ajv)
|
||||
|
||||
// Custom JSON keywords
|
||||
ajv.addKeyword({
|
||||
keyword: 'translatable',
|
||||
})
|
||||
|
||||
// Custom JSON formats
|
||||
ajv.addFormat('semver', {
|
||||
validate: (x) => semver.validRange(x),
|
||||
})
|
||||
|
||||
// The ajv.validate function is supposed to cache
|
||||
// the compiled schema, but the documentation says
|
||||
// that the best permformance is achieved by calling
|
||||
// the compile function and then calling validate.
|
||||
// So when the same schema is validated multiple times,
|
||||
// this is the best function to use. If the schema
|
||||
// changes from one call to the next, then the validateJson
|
||||
// function makes more sense to use.
|
||||
export function getJsonValidator(schema) {
|
||||
return ajv.compile(schema)
|
||||
}
|
||||
|
||||
// The next call to ajv.validate will overwrite
|
||||
// the ajv.errors property, so returning it here
|
||||
// ensures that it remains accessible.
|
||||
export function validateJson(schema, data) {
|
||||
const isValid = ajv.validate(schema, data)
|
||||
return {
|
||||
isValid,
|
||||
errors: isValid ? null : structuredClone(ajv.errors),
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import Ajv from 'ajv'
|
||||
|
||||
import { getJsonValidator } from '#src/tests/lib/validate-json-schema.js'
|
||||
import { allVersions } from '#src/versions/lib/all-versions.js'
|
||||
import { latest } from '#src/versions/lib/enterprise-server-releases.js'
|
||||
import schema from '#src/tests/helpers/schemas/versions-schema.js'
|
||||
@@ -9,8 +9,7 @@ import { formatAjvErrors } from '#src/tests/helpers/schemas.js'
|
||||
|
||||
jest.useFakeTimers({ legacyFakeTimers: true })
|
||||
|
||||
const ajv = new Ajv({ allErrors: true })
|
||||
const validate = ajv.compile(schema)
|
||||
const validate = getJsonValidator(schema)
|
||||
|
||||
describe('versions module', () => {
|
||||
test('is an object with versions as keys', () => {
|
||||
@@ -20,14 +19,14 @@ describe('versions module', () => {
|
||||
|
||||
test('every version is valid', () => {
|
||||
Object.values(allVersions).forEach((versionObj) => {
|
||||
const valid = validate(versionObj)
|
||||
const isValid = validate(versionObj)
|
||||
let errors
|
||||
|
||||
if (!valid) {
|
||||
if (!isValid) {
|
||||
errors = `version '${versionObj.version}': ${formatAjvErrors(validate.errors)}`
|
||||
}
|
||||
|
||||
expect(valid, errors).toBe(true)
|
||||
expect(isValid, errors).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import Ajv from 'ajv'
|
||||
import { get, isPlainObject } from 'lodash-es'
|
||||
|
||||
import { getJsonValidator } from '#src/tests/lib/validate-json-schema.js'
|
||||
import { renderContent } from '#src/content-render/index.js'
|
||||
import webhookSchema from './webhook-schema.js'
|
||||
import { getBodyParams } from '../../rest/scripts/utils/get-body-params.js'
|
||||
@@ -15,6 +15,8 @@ const NO_CHILD_PROPERTIES = [
|
||||
'sender',
|
||||
]
|
||||
|
||||
const validate = getJsonValidator(webhookSchema)
|
||||
|
||||
export default class Webhook {
|
||||
#webhook
|
||||
constructor(webhook) {
|
||||
@@ -50,10 +52,9 @@ export default class Webhook {
|
||||
async process() {
|
||||
await Promise.all([this.renderDescription(), this.renderBodyParameterDescriptions()])
|
||||
|
||||
const ajv = new Ajv()
|
||||
const valid = ajv.validate(webhookSchema, this)
|
||||
if (!valid) {
|
||||
console.error(JSON.stringify(ajv.errors, null, 2))
|
||||
const isValid = validate(this)
|
||||
if (!isValid) {
|
||||
console.error(JSON.stringify(validate.errors, null, 2))
|
||||
throw new Error(`Invalid OpenAPI webhook found: ${this.category}`)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user