diff --git a/package.json b/package.json index cbf8802..ef45189 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@typescript-eslint/eslint-plugin": "^2.1.0", "@typescript-eslint/parser": "^2.1.0", "@typescript-eslint/typescript-estree": "^2.1.0", + "ajv8": "npm:ajv@^8.6.3", "dockest": "^2.1.0", "eslint": "^6.3.0", "eslint-config-prettier": "^6.1.0", diff --git a/src/@types.ts b/src/@types.ts index 7d122ae..944418f 100644 --- a/src/@types.ts +++ b/src/@types.ts @@ -1,4 +1,5 @@ import { Resolver, ForSchemaOptions } from 'avsc' +import { ValidateFunction } from './JsonSchema' import Ajv from 'ajv' export enum SchemaType { @@ -14,7 +15,11 @@ export interface SchemaHelper { } export type AvroOptions = Partial -export type JsonOptions = ConstructorParameters[0] & { ajvInstance?: Ajv } +export type JsonOptions = ConstructorParameters[0] & { + ajvInstance?: { + compile: (schema: any) => ValidateFunction + } +} export type ProtoOptions = { messageName: string } export interface LegacyOptions { diff --git a/src/JsonSchema.ts b/src/JsonSchema.ts index 5186cd8..8085e0c 100644 --- a/src/JsonSchema.ts +++ b/src/JsonSchema.ts @@ -1,7 +1,26 @@ import { Schema, JsonOptions, ConfluentSchema } from './@types' -import Ajv, { DefinedError, ValidateFunction } from 'ajv' +import Ajv from 'ajv' import { ConfluentSchemaRegistryValidationError } from './errors' +interface BaseAjvValidationError { + data?: unknown + schema?: unknown +} + +interface OldAjvValidationError extends BaseAjvValidationError { + dataPath: string + instancePath?: string +} +interface NewAjvValidationError extends BaseAjvValidationError { + instancePath: string +} + +type AjvValidationError = OldAjvValidationError | NewAjvValidationError + +export interface ValidateFunction { + (this: any, data: any): boolean + errors?: null | AjvValidationError[] +} export default class JsonSchema implements Schema { private validate: ValidateFunction @@ -39,12 +58,17 @@ export default class JsonSchema implements Schema { ): boolean { if (!this.validate(payload)) { if (opts?.errorHook) { - for (const err of this.validate.errors as DefinedError[]) { - opts.errorHook([err.dataPath], err.data, err.schema) + for (const err of this.validate.errors as AjvValidationError[]) { + const path = this.isOldAjvValidationError(err) ? err.dataPath : err.instancePath + opts.errorHook([path], err.data, err.schema) } } return false } return true } + + private isOldAjvValidationError(error: AjvValidationError): error is OldAjvValidationError { + return (error as OldAjvValidationError).dataPath != null + } } diff --git a/src/SchemaRegistry.newApi.spec.ts b/src/SchemaRegistry.newApi.spec.ts index 4534e5f..75f1d9a 100644 --- a/src/SchemaRegistry.newApi.spec.ts +++ b/src/SchemaRegistry.newApi.spec.ts @@ -15,6 +15,9 @@ import encodedAnotherPersonV2Json from '../fixtures/json/encodedAnotherPersonV2' import encodedAnotherPersonV2Proto from '../fixtures/proto/encodedAnotherPersonV2' import encodedNestedV2Proto from '../fixtures/proto/encodedNestedV2' import wrongMagicByte from '../fixtures/wrongMagicByte' +import Ajv2020 from 'ajv8/dist/2020' +import Ajv from 'ajv' +import { ConfluentSchemaRegistryValidationError } from './errors' const REGISTRY_HOST = 'http://localhost:8982' const schemaRegistryAPIClientArgs = { host: REGISTRY_HOST } @@ -561,4 +564,38 @@ describe('SchemaRegistry - new Api', () => { }) }) }) + + describe('JSON Schema tests', () => { + describe('passing an Ajv instance in the constructor', () => { + test.each([ + ['Ajv 7', new Ajv()], + ['Ajv2020', new Ajv2020()], + ])( + 'Errors are thrown with their path in %s when the validation fails', + async (_, ajvInstance) => { + expect.assertions(3) + const registry = new SchemaRegistry(schemaRegistryArgs, { + [SchemaType.JSON]: { ajvInstance }, + }) + const subject: ConfluentSubject = { + name: [SchemaType.JSON, 'com.org.domain.fixtures', 'AnotherPerson'].join('.'), + } + const schema: ConfluentSchema = { + type: SchemaType.JSON, + schema: schemaStringsByType[SchemaType.JSON].v1, + } + + const { id: schemaId } = await registry.register(schema, { subject: subject.name }) + + try { + await schemaRegistry.encode(schemaId, { fullName: true }) + } catch (error) { + expect(error).toBeInstanceOf(ConfluentSchemaRegistryValidationError) + expect(error.message).toEqual('invalid payload') + expect(error.paths).toEqual([['/fullName']]) + } + }, + ) + }) + }) }) diff --git a/yarn.lock b/yarn.lock index 1934768..821b9bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -709,6 +709,16 @@ acorn@^7.1.0, acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== +"ajv8@npm:ajv@^8.6.3": + version "8.6.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764" + integrity sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: version "6.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" @@ -1308,13 +1318,18 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -dockest@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/dockest/-/dockest-1.0.4.tgz#0da0fb44aeba1d9923aa72a6f43fe6aea1174296" - integrity sha512-fHAU1z9CkMDHeoJo0oxg0sK1BYc3tANmGKAm7tPY/OpVV3DUVCSDwnpqtq7H3/I7tKvII5xF44hE1iSC+qNvnA== +dockest@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/dockest/-/dockest-2.1.0.tgz#efbeaca7bb6078b9bb0a431a050cdefc5b50f07e" + integrity sha512-cEudMXrP9Sl1obedYDEyXoKSePGsmbT8750W2D1/W19szr+EbSlEpaRp3tIl5q8oeZ6UxFnfZjXzD+4nDrVhZw== dependencies: - execa "^2.0.4" + chalk "^3.0.0" + execa "^4.0.0" + fp-ts "^2.8.3" + io-ts "^2.2.10" + is-docker "^2.0.0" js-yaml "^3.13.1" + rxjs "^6.5.4" doctrine@^3.0.0: version "3.0.0" @@ -1568,6 +1583,21 @@ execa@^3.2.0: signal-exit "^3.0.2" strip-final-newline "^2.0.0" +execa@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -1750,6 +1780,11 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +fp-ts@^2.8.3: + version "2.11.5" + resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.11.5.tgz#97cceb26655b1452d7088d6fb0864f84cceffbe4" + integrity sha512-OqlwJq1BdpB83BZXTqI+dNcA6uYk6qk4u9Cgnt64Y+XS7dwdbp/mobx8S2KXf2AXH+scNmA/UVK3SEFHR3vHZA== + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -2008,6 +2043,11 @@ inquirer@^7.0.0: strip-ansi "^6.0.0" through "^2.3.6" +io-ts@^2.2.10: + version "2.2.16" + resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.2.16.tgz#597dffa03db1913fc318c9c6df6931cb4ed808b2" + integrity sha512-y5TTSa6VP6le0hhmIyN0dqEXkrZeJLeC5KApJq6VLci3UEKF80lZ+KuoUs02RhBxNWlrqSNxzfI7otLX1Euv8Q== + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -2071,6 +2111,11 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -3406,6 +3451,13 @@ rxjs@^6.5.3: dependencies: tslib "^1.9.0" +rxjs@^6.5.4: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"