Skip to content

Commit 97c7030

Browse files
authored
Consolidate uses of AJV (#47662)
1 parent d505488 commit 97c7030

17 files changed

Lines changed: 139 additions & 150 deletions

File tree

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { jest } from '@jest/globals'
2-
import Ajv from 'ajv'
3-
import addErrors from 'ajv-errors'
4-
import semver from 'semver'
52

63
import featureVersionsSchema from '../lib/feature-versions-schema.js'
74
import { getDeepDataByLanguage } from '#src/data-directory/lib/get-data.js'
5+
import { getJsonValidator } from '#src/tests/lib/validate-json-schema.js'
86
import { formatAjvErrors } from '#src/tests/helpers/schemas.js'
97

108
/*
@@ -18,26 +16,18 @@ import { formatAjvErrors } from '#src/tests/helpers/schemas.js'
1816
jest.useFakeTimers({ legacyFakeTimers: true })
1917

2018
const featureVersions = Object.entries(getDeepDataByLanguage('features', 'en'))
21-
22-
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true })
23-
addErrors(ajv)
24-
// *** TODO: We can drop this override once the frontmatter schema has been updated to work with AJV. ***
25-
ajv.addFormat('semver', {
26-
validate: (x) => semver.validRange(x),
27-
})
28-
// *** End TODO ***
29-
const validate = ajv.compile(featureVersionsSchema)
19+
const validate = getJsonValidator(featureVersionsSchema)
3020

3121
// Make sure data/features/*.yml contains valid versioning.
3222
describe('lint feature versions', () => {
3323
test.each(featureVersions)('data/features/%s matches the schema', (name, featureVersion) => {
34-
const valid = validate(featureVersion)
24+
const isValid = validate(featureVersion)
3525
let errors
3626

37-
if (!valid) {
27+
if (!isValid) {
3828
errors = formatAjvErrors(validate.errors)
3929
}
4030

41-
expect(valid, errors).toBe(true)
31+
expect(isValid, errors).toBe(true)
4232
})
4333
})

src/events/middleware.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
import express from 'express'
22
import { omit, without, mapValues } from 'lodash-es'
3-
import Ajv from 'ajv'
4-
import addFormats from 'ajv-formats'
53
import QuickLRU from 'quick-lru'
64

75
import { schemas, hydroNames } from './lib/schema.js'
86
import catchMiddlewareError from '#src/observability/middleware/catch-middleware-error.js'
97
import { noCacheControl } from '#src/frame/middleware/cache-control.js'
8+
import { getJsonValidator } from '#src/tests/lib/validate-json-schema.js'
109
import { formatErrors } from './lib/middleware-errors.js'
1110
import { publish as _publish } from './lib/hydro.js'
1211

1312
const router = express.Router()
14-
const ajv = new Ajv()
15-
addFormats(ajv)
1613
const OMIT_FIELDS = ['type']
1714
const allowedTypes = new Set(without(Object.keys(schemas), 'validation'))
1815
const isProd = process.env.NODE_ENV === 'production'
19-
const validations = mapValues(schemas, (schema) => ajv.compile(schema))
20-
16+
const validators = mapValues(schemas, (schema) => getJsonValidator(schema))
2117
// In production, fire and not wait to respond.
2218
// _publish will send an error to failbot,
2319
// so we don't get alerts but we still track it.
@@ -47,7 +43,7 @@ router.post(
4743
}
4844

4945
// Validate the data matches the corresponding data schema
50-
const validate = validations[type]
46+
const validate = validators[type]
5147
if (!validate(req.body)) {
5248
const hash = `${req.ip}:${validate.errors
5349
.map((error) => error.message + error.instancePath)

src/events/tests/middleware-errors.js

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
1-
import Ajv from 'ajv'
2-
import addFormats from 'ajv-formats'
1+
import { validateJson } from '#src/tests/lib/validate-json-schema.js'
32
import { formatErrors } from '../lib/middleware-errors.js'
43
import { schemas } from '../lib/schema.js'
54

6-
const ajv = new Ajv()
7-
addFormats(ajv)
8-
95
expect.extend({
106
toMatchSchema(data, schema) {
11-
const isValid = ajv.validate(schema, data)
7+
const { isValid, errors } = validateJson(schema, data)
128
return {
139
pass: isValid,
14-
message: () => (isValid ? '' : ajv.errorsText()),
10+
message: () => (isValid ? '' : errors.message),
1511
}
1612
},
1713
})
1814

1915
describe('formatErrors', () => {
2016
it('should produce objects that match the validation spec', () => {
2117
// Produce an error
22-
ajv.validate({ type: 'string' }, 0)
23-
for (const formatted of formatErrors(ajv.errors, '')) {
18+
const { errors } = validateJson({ type: 'string' }, 0)
19+
const formattedErrors = formatErrors(errors, '')
20+
for (const formatted of formattedErrors) {
2421
expect(formatted).toMatchSchema(schemas.validation)
2522
}
2623
})

src/frame/lib/read-frontmatter.js

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,12 @@
11
import matter from 'gray-matter'
2-
import Ajv from 'ajv'
3-
import addErrors from 'ajv-errors'
4-
import addFormats from 'ajv-formats'
5-
import semver from 'semver'
6-
7-
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true })
8-
ajv.addKeyword({
9-
keyword: 'translatable',
10-
})
11-
ajv.addFormat('semver', {
12-
validate: (x) => semver.validRange(x),
13-
})
14-
addErrors(ajv)
15-
addFormats(ajv)
2+
3+
import { validateJson } from '#src/tests/lib/validate-json-schema.js'
164

175
function readFrontmatter(markdown, opts = {}) {
186
const schema = opts.schema || { type: 'object', properties: {} }
197
const filepath = opts.filepath || null
208

219
let content, data
22-
let errors = []
2310

2411
try {
2512
;({ content, data } = matter(markdown))
@@ -39,18 +26,13 @@ function readFrontmatter(markdown, opts = {}) {
3926
}
4027

4128
if (filepath) error.filepath = filepath
42-
errors.push(error)
29+
const errors = [error]
4330
console.warn(errors)
4431

4532
return { errors }
4633
}
4734

48-
const ajvValidate = ajv.compile(schema)
49-
const valid = ajvValidate(data)
50-
51-
if (!valid) {
52-
errors = ajvValidate.errors
53-
}
35+
const validate = validateJson(schema, data)
5436

5537
// Combine the AJV-supplied `instancePath` and `params` into a more user-friendly frontmatter path.
5638
// For example, given:
@@ -69,15 +51,20 @@ function readFrontmatter(markdown, opts = {}) {
6951
return typeof mainProps !== 'object' ? `${prefixProps}.${mainProps}` : prefixProps
7052
}
7153

72-
if (!valid && filepath) {
73-
errors = ajvValidate.errors.map((error) => {
54+
const errors = []
55+
56+
if (!validate.isValid && filepath) {
57+
const formattedErrors = validate.errors.map((error) => {
7458
const userFriendly = {}
7559
userFriendly.property = cleanPropertyPath(error.params, error.instancePath)
7660
userFriendly.message = error.message
7761
userFriendly.reason = error.keyword
7862
userFriendly.filepath = filepath
7963
return userFriendly
8064
})
65+
errors.push(...formattedErrors)
66+
} else if (!validate.isValid) {
67+
errors.push(...validate.errors)
8168
}
8269

8370
return { content, data, errors }

src/frame/tests/site-tree.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import Ajv from 'ajv'
21
import { jest } from '@jest/globals'
2+
3+
import { getJsonValidator } from '#src/tests/lib/validate-json-schema.js'
34
import schema from '#src/tests/helpers/schemas/site-tree-schema.js'
45
import EnterpriseServerReleases from '#src/versions/lib/enterprise-server-releases.js'
56
import { loadSiteTree } from '#src/frame/lib/page-data.js'
@@ -8,8 +9,7 @@ import { formatAjvErrors } from '#src/tests/helpers/schemas.js'
89

910
const latestEnterpriseRelease = EnterpriseServerReleases.latest
1011

11-
const ajv = new Ajv({ allErrors: true })
12-
const siteTreeValidate = ajv.compile(schema.childPage)
12+
const siteTreeValidate = getJsonValidator(schema.childPage)
1313

1414
describe('siteTree', () => {
1515
jest.setTimeout(3 * 60 * 1000)
@@ -58,14 +58,14 @@ describe('siteTree', () => {
5858

5959
function validate(currentPage) {
6060
;(currentPage.childPages || []).forEach((childPage) => {
61-
const valid = siteTreeValidate(childPage)
61+
const isValid = siteTreeValidate(childPage)
6262
let errors
6363

64-
if (!valid) {
64+
if (!isValid) {
6565
errors = `file ${childPage.page.fullPath}: ${formatAjvErrors(siteTreeValidate.errors)}`
6666
}
6767

68-
expect(valid, errors).toBe(true)
68+
expect(isValid, errors).toBe(true)
6969

7070
// Run recurisvely until we run out of child pages
7171
validate(childPage)

src/github-apps/scripts/sync.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import yaml from 'js-yaml'
1010
import { getContents } from '#src/workflows/git-utils.js'
1111
import permissionSchema from './permission-list-schema.js'
1212
import enabledSchema from './enabled-list-schema.js'
13-
import { validateData } from '../../rest/scripts/utils/validate-data.js'
13+
import { validateJson } from '#src/tests/lib/validate-json-schema.js'
1414

1515
const ENABLED_APPS_DIR = 'src/github-apps/data'
1616
const CONFIG_FILE = 'src/github-apps/lib/config.json'
@@ -287,12 +287,20 @@ function initAppData(storage, category, data) {
287287
async function validateAppData(data, pageType) {
288288
if (pageType.includes('permissions')) {
289289
for (const value of Object.values(data)) {
290-
validateData(value, permissionSchema)
290+
const { isValid, errors } = validateJson(permissionSchema, value)
291+
if (!isValid) {
292+
console.error(JSON.stringify(errors, null, 2))
293+
throw new Error('GitHub Apps permission schema validation failed')
294+
}
291295
}
292296
} else {
293297
for (const arrayItems of Object.values(data)) {
294298
for (const item of arrayItems) {
295-
validateData(item, enabledSchema)
299+
const { isValid, errors } = validateJson(enabledSchema, item)
300+
if (!isValid) {
301+
console.error(JSON.stringify(errors, null, 2))
302+
throw new Error('GitHub Apps enabled apps schema validation failed')
303+
}
296304
}
297305
}
298306
}

src/graphql/tests/validate-schema.js

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { jest } from '@jest/globals'
2-
import Ajv from 'ajv'
32

3+
import { getJsonValidator, validateJson } from '#src/tests/lib/validate-json-schema.js'
44
import readJsonFile from '#src/frame/lib/read-json-file.js'
55
import { schemaValidator, previewsValidator, upcomingChangesValidator } from '../lib/validator.js'
66
import { formatAjvErrors } from '#src/tests/helpers/schemas.js'
@@ -11,15 +11,8 @@ const allVersionValues = Object.values(allVersions)
1111
const graphqlVersions = allVersionValues.map((v) => v.openApiVersionName)
1212
const graphqlTypes = readJsonFile('./src/graphql/lib/types.json').map((t) => t.kind)
1313

14-
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true })
15-
const previewsValidate = ajv.compile(previewsValidator)
16-
const upcomingChangesValidate = ajv.compile(upcomingChangesValidator)
17-
// setup ajv validator functions for each graphql type (e.g. queries, mutations,
18-
// etc.)
19-
const schemaValidatorFunctions = {}
20-
graphqlTypes.forEach((type) => {
21-
schemaValidatorFunctions[type] = ajv.compile(schemaValidator[type])
22-
})
14+
const previewsValidate = getJsonValidator(previewsValidator)
15+
const upcomingChangesValidate = getJsonValidator(upcomingChangesValidator)
2316

2417
describe('graphql json files', () => {
2518
jest.setTimeout(3 * 60 * 1000)
@@ -38,16 +31,16 @@ describe('graphql json files', () => {
3831
if (typeObjsTested.has(key)) return
3932
typeObjsTested.add(key)
4033

41-
const valid = schemaValidatorFunctions[type](typeObj)
42-
let errors
34+
const { isValid, errors } = validateJson(schemaValidator[type], typeObj)
4335

44-
if (!valid) {
45-
errors = `kind: ${typeObj.kind}, name: ${typeObj.name}: ${formatAjvErrors(
46-
schemaValidatorFunctions[type].errors,
36+
let formattedErrors = errors
37+
if (!isValid) {
38+
formattedErrors = `kind: ${typeObj.kind}, name: ${typeObj.name}: ${formatAjvErrors(
39+
errors,
4740
)}`
4841
}
4942

50-
expect(valid, errors).toBe(true)
43+
expect(isValid, formattedErrors).toBe(true)
5144
})
5245
})
5346
})
@@ -57,14 +50,14 @@ describe('graphql json files', () => {
5750
graphqlVersions.forEach((version) => {
5851
const previews = readJsonFile(`${GRAPHQL_DATA_DIR}/${version}/previews.json`)
5952
previews.forEach((preview) => {
60-
const valid = previewsValidate(preview)
53+
const isValid = previewsValidate(preview)
6154
let errors
6255

63-
if (!valid) {
56+
if (!isValid) {
6457
errors = formatAjvErrors(previewsValidate.errors)
6558
}
6659

67-
expect(valid, errors).toBe(true)
60+
expect(isValid, errors).toBe(true)
6861
})
6962
})
7063
})
@@ -75,14 +68,14 @@ describe('graphql json files', () => {
7568
for (const changes of Object.values(upcomingChanges)) {
7669
// each object value is an array of changes
7770
changes.forEach((changeObj) => {
78-
const valid = upcomingChangesValidate(changeObj)
71+
const isValid = upcomingChangesValidate(changeObj)
7972
let errors
8073

81-
if (!valid) {
74+
if (!isValid) {
8275
errors = formatAjvErrors(upcomingChangesValidate.errors)
8376
}
8477

85-
expect(valid, errors).toBe(true)
78+
expect(isValid, errors).toBe(true)
8679
})
8780
}
8881
})

src/learning-track/tests/validate-schema.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { jest } from '@jest/globals'
66
import { liquid } from '#src/content-render/index.js'
77
import learningTracksSchema from '../lib/learning-tracks-schema.js'
88
import { formatAjvErrors } from '#src/tests/helpers/schemas.js'
9-
import { ajvValidate } from '#src/tests/lib/ajv-validate.js'
9+
import { getJsonValidator } from '#src/tests/lib/validate-json-schema.js'
1010

1111
const learningTrackRootPath = 'data/learning-tracks'
12-
const jsonValidator = ajvValidate(learningTracksSchema)
12+
const validate = getJsonValidator(learningTracksSchema)
1313
const yamlWalkOptions = {
1414
globs: ['**/*.yml'],
1515
directories: false,
@@ -31,14 +31,14 @@ describe('lint learning tracks', () => {
3131
})
3232

3333
it('matches the schema', () => {
34-
const valid = jsonValidator(yamlContent)
34+
const isValid = validate(yamlContent)
3535
let errors
3636

37-
if (!valid) {
38-
errors = formatAjvErrors(jsonValidator.errors)
37+
if (!isValid) {
38+
errors = formatAjvErrors(validate.errors)
3939
}
4040

41-
expect(valid, errors).toBe(true)
41+
expect(isValid, errors).toBe(true)
4242
})
4343

4444
it('contains valid liquid', () => {

src/products/tests/products.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import Ajv from 'ajv'
1+
import { getJsonValidator } from '#src/tests/lib/validate-json-schema.js'
22
import { productMap } from '#src/products/lib/all-products.js'
33
import { formatAjvErrors } from '#src/tests/helpers/schemas.js'
44
import schema from '#src/tests/helpers/schemas/products-schema.js'
55

6-
const ajv = new Ajv({ allErrors: true })
7-
const validate = ajv.compile(schema)
6+
const validate = getJsonValidator(schema)
87

98
describe('products module', () => {
109
test('is an object with product ids as keys', () => {
@@ -14,13 +13,13 @@ describe('products module', () => {
1413

1514
test('every product is valid', () => {
1615
Object.values(productMap).forEach((product) => {
17-
const valid = validate(product)
16+
const isValid = validate(product)
1817
let errors
1918

20-
if (!valid) {
21-
errors = formatAjvErrors(valid.errors)
19+
if (!isValid) {
20+
errors = formatAjvErrors(validate.errors)
2221
}
23-
expect(valid, errors).toBe(true)
22+
expect(isValid, errors).toBe(true)
2423
})
2524
})
2625
})

0 commit comments

Comments
 (0)