Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow custom Ajv instance (closes #115) #116

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

pstadler
Copy link

@pstadler pstadler commented Aug 12, 2022

Previously, Validator would only take an object containing Ajv options. With this PR, we've added the possibility to pass a custom Ajv instance instead, for example one of the 2019-09 draft version[1]:

const Ajv2019 = require('ajv/dist/2019')
const ajv = new Ajv2019()

const validator = new Validator(ajv)

[1] https://ajv.js.org/json-schema.html#draft-2019-09

@simonplend
Copy link
Owner

@pstadler Thanks for opening this PR — it looks great! I'm going to check out the branch and give it a manual test before merging.

Please can you add a description to this PR that references your original issue and explains the purpose of this feature?

@pstadler
Copy link
Author

done 👍🏻

@simonplend simonplend changed the title allow custom Ajv instance (closes #115) Allow custom Ajv instance (closes #115) Aug 18, 2022
@pstadler
Copy link
Author

pstadler commented Oct 3, 2022

Ping 👋🏻

@simonplend
Copy link
Owner

@pstadler I've been quite overloaded recently, so haven't been able to give this the testing time it needs.

Thanks for being so patient, I'll do my best to pick it up soon!

@pstadler
Copy link
Author

pstadler commented Oct 4, 2022

no problem. sometimes people forget. take your time. 👍🏻

@simonplend
Copy link
Owner

simonplend commented Oct 26, 2022

Manual testing

  • Test against a JavaScript application
    • With default Ajv instance
    • With custom Ajv instance
  • Test against a TypeScript application
    • With default Ajv instance
    • With custom Ajv instance

@simonplend
Copy link
Owner

simonplend commented Oct 26, 2022

@pstadler I've run into an issue while manually testing this PR, but I'm not quite sure what's going wrong:

I've created a minimal JavaScript application (code) and then installed ajv as an application dependency so I can create an Ajv instance:

const Ajv2019 = require("ajv/dist/2019");

const ajv = new Ajv2019();

When I run the application I receive the following error from Ajv:

Error: reference "https://json-schema.org/draft/2019-09/schema" resolves to more than one schema

(Full stack trace)

Do you have any idea what's happening?

@simonplend
Copy link
Owner

From what I can tell, this instanceof check isn't working: https://github.com/simonplend/express-json-validator-middleware/pull/116/files#diff-bfe9874d239014961b1ae4e89875a6155667db834a410aaaa2ebe3cf89820556R11

I've confirmed that with a minimal reproduction too:

const Ajv2019 = require("ajv/dist/2019");

const AjvCore = require("ajv/dist/core").default;

const ajv = new Ajv2019();

console.log("Ajv2019 instanceof AjvCore:", Ajv2019 instanceof AjvCore);
// > Ajv2019 instanceof AjvCore: false

@pstadler
Copy link
Author

pstadler commented Oct 26, 2022

Just tried your sample app. It works for me:

$ curl http://localhost:3000/address -X POST -s | jq
{
  "body": [
    {
      "instancePath": "",
      "schemaPath": "#/required",
      "keyword": "required",
      "params": {
        "missingProperty": "number"
      },
      "message": "must have required property 'number'"
    }
  ]
}

$ curl http://localhost:3000/address -X POST -s \
  -d '{"number":1,"street":"Infinite Loop","type":"Avenue"}' -H "content-type:application/json" | jq
{}

@pstadler
Copy link
Author

pstadler commented Oct 26, 2022

Your check is wrong, you need to replace:

console.log("Ajv2019 instanceof AjvCore:", Ajv2019 instanceof AjvCore);

with

console.log("Ajv2019 instanceof AjvCore:", ajv instanceof AjvCore);

@pstadler
Copy link
Author

Okay, I think the instaceof check could indeed fail with different ajv package versions and needs to be changed to check if the provided argument is any kind of class, not the bundled AjvCore.

@simonplend
Copy link
Owner

simonplend commented Oct 27, 2022

Your check is wrong, you need to replace:

Good catch on the bug in my minimal reproduction. I made the replacement you suggested and it was true as expected.

Okay, I think the instaceof check could indeed fail with different ajv package versions and needs to be changed to check if the provided argument is any kind of class, not the bundled AjvCore.

How about making this check instead?

if (typeof ajvOptions?.compile === "function") {

@pstadler
Copy link
Author

This seems to be the right approach. Change amended. Please note that eslint ecmaVersion: 2020 is required for optional chaining.

@@ -23,7 +23,7 @@
"scripts": {
"test": "npm run test:unit && npm run test:types",
"test:unit": "tap",
"test:types": "tsc --noEmit --strict src/index.d.ts",
"test:types": "tsc --noEmit --strict --target ES2018 --moduleResolution node --resolveJsonModule src/index.d.ts",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the --target value here be updated to match the ecmaVersion in the ESLint config (2020)?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not clear why --moduleResolution node and --resolveJsonModule are needed here. Could you please explain?

Comment on lines -4 to +5
import Ajv, { ErrorObject, Options as AjvOptions } from "ajv";
import { ErrorObject, Options as AjvOptions } from "ajv";
import AjvCore from "ajv/lib/core";
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been testing this PR with a TypeScript application and I've run into a type error, which I think is caused by this using the AjvCore type instead of the Ajv type. Official Ajv plugins use the Ajv type, for example here in ajv-formats.

With this TypeScript code:

import addFormats from "ajv-formats";
import { Validator, ValidationError } from "express-json-validator-middleware";

const validator = new Validator({
    coerceTypes: true,
});

addFormats(validator.ajv, ["date-time"])
    .addKeyword("kind")
    .addKeyword("modifier");

I receive this type error when running tsc:

src/lib/middleware/validation/index.ts:9:12 - error TS2345: Argument of type 'import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/express-json-validator-middleware/node_modules/ajv/lib/core").default' is not assignable to parameter of type 'import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/ajv-formats/node_modules/ajv/dist/core").default'.
  Types of property 'opts' are incompatible.
    Type 'import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/express-json-validator-middleware/node_modules/ajv/lib/core").InstanceOptions' is not assignable to type 'import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/ajv-formats/node_modules/ajv/dist/core").InstanceOptions'.
      Type 'InstanceOptions' is not assignable to type 'CurrentOptions'.
        Types of property 'keywords' are incompatible.
          Type 'import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/express-json-validator-middleware/node_modules/ajv/lib/types/index").Vocabulary | undefined' is not assignable to type 'import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/ajv-formats/node_modules/ajv/dist/types/index").Vocabulary | undefined'.
            Type 'Vocabulary' is not assignable to type '(string | KeywordDefinition)[]'.
              Type 'string | import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/express-json-validator-middleware/node_modules/ajv/lib/types/index").KeywordDefinition' is not assignable to type 'string | import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/ajv-formats/node_modules/ajv/dist/types/index").KeywordDefinition'.
                Type 'CodeKeywordDefinition' is not assignable to type 'string | KeywordDefinition'.
                  Type 'import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/express-json-validator-middleware/node_modules/ajv/lib/types/index").CodeKeywordDefinition' is not assignable to type 'import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/ajv-formats/node_modules/ajv/dist/types/index").CodeKeywordDefinition'.
                    Types of property 'code' are incompatible.
                      Type '(cxt: import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/express-json-validator-middleware/node_modules/ajv/lib/compile/validate/index").KeywordCxt, ruleType?: string | undefined) => void' is not assignable to type '(cxt: import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/ajv-formats/node_modules/ajv/dist/compile/validate/index").KeywordCxt, ruleType?: string | undefined) => void'.
                        Types of parameters 'cxt' and 'cxt' are incompatible.
                          Type 'import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/ajv-formats/node_modules/ajv/dist/compile/validate/index").KeywordCxt' is not assignable to type 'import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/express-json-validator-middleware/node_modules/ajv/lib/compile/validate/index").KeywordCxt'.
                            The types of 'gen._scope' are incompatible between these types.
                              Type 'import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/ajv-formats/node_modules/ajv/dist/compile/codegen/scope").Scope' is not assignable to type 'import("/home/simonplend/dev/github/simonteaching/api-project/node_modules/express-json-validator-middleware/node_modules/ajv/lib/compile/codegen/scope").Scope'.
                                Property '_names' is protected but type 'Scope' is not a class derived from 'Scope'.

9 addFormats(validator.ajv, ["date-time"])
             ~~~~~~~~~~~~~

Can this be changed back to importing the Ajv type, and then that type be used in the constructor instead of AjvCore?

Copy link
Owner

@simonplend simonplend left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great overall! There are two things to mention:

  1. I've tested these changes with a JavaScript application and it works as expected, but I'm getting a type error when testing it with a TypeScript application (see my comment on the types).

  2. Please add an explanation on how to set a custom Ajv instance to the beginning of the Ajv instance section in the README. A short description and the code example from the PR description should suffice.

@simonplend
Copy link
Owner

Hey @pstadler, I think this PR is almost ready to merge - let me know if there's anything I can do to help you wrap it up.

@simonplend
Copy link
Owner

@pstadler Any chance you'd be up for wrapping up this PR? No problem if not, I can just close it if you don't have the time or inclination.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants