mercurius-js / validation

Adds configurable validation support to Mercurius.
MIT License
30 stars 6 forks source link

Basic implementation #1

Closed jonnydgreen closed 3 years ago

jonnydgreen commented 3 years ago

Hey! Opening up an issue here to discuss a basic initial implementation of the plugin off the back of: https://github.com/mercurius-js/mercurius/issues/509

Current state of play:

@mcollina when you discussed with @simone-sanfratello about writing a validation plugin, did you talk about any alternative approaches that could be worth looking at?

mcollina commented 3 years ago

I think we have two approaches:

  1. in-band using directives
  2. out-of-band using an external policy

Both would wrap the original resolver.

I'm leaning over the 2nd approach as consumers might not be really interested in the validation rules of a field.

Regarding the validation rules, I think we can use Ajv 8 and support both JSON Schema and JTD.

jonnydgreen commented 3 years ago

I do like both approaches with a slight preference towards the second approach as well :) Are you thinking something similar to fastify validation? For example, we could have a validation schema for each GraphQL field (for inputs and outputs) and at startup, we make sure this validation schema is compatible with the GraphQL schema?

Also, if we do the second approach, I think in theory we could still do the in-band directive approach in the future by constructing an "external policy" from the schema directives and then running the same flow as if it was out of band.

One thought I did have (but maybe a feature for future releases), is to add an option to enable auto-documentation of the GraphQL schema with the validation rules (such as maybe prefixing the related field/argument descriptions) so that it automatically appears in the GraphQL schema documentation. I have worked with consumers in the past who prefer to have some indication of validation rules prior to running their code, but like you mentioned, this feels like a nice to have for the time being?

Definitely agreed on the Ajv 8 validation rules! :)

simone-sanfratello commented 3 years ago

I like this approach with directives https://github.com/mercurius-js/validation/blob/0bbffb759e697527214b833ee2a804fe15a8dfda/test/basic.js#L8 to cover basic validations, but I'd also like to add other complicated validation like: checking of consistency between fields and checking of fields asynchronously

For example (please don't take the code too strictly, it's just for describe the idea)

const schema = `

input UserInput {
    name: String! @constraint(minLength: 3) @constraint(match: "[a-z0-9.-_]+/i")
    email: String!
    address: Address!
    birthdate: Date! -- Date is a custom scalar
    marriedSince: Date
}

type Mutation {
    register(user: UserInput): User
}
`

app.register(validation, {
  validation: {
    Mutation: {
      register: {
        email: async (source, args, context, info) => {
          const checkEmail = await context.database.query("SELECT email FROM users WHERE email = $1", args.user.email)
          if (checkEmail.length > 1) {
            // not sure where/how to return violations, should be a list
            context.errors.push("Email already exists")
            return false
          }
          return true
        },
        birthdate: async (source, args, context, info) => {
          return args.user.birthdate > Date.now()
        }
        marriedSince: async (source, args, context, info) => {
          if(args.user.marriedSince && args.user.marriedSince < args.user.birthdate) {
            context.errors.push("Married date is invalid")
            return false
          }
          return true
        }
      }
    }
  }
})
jonnydgreen commented 3 years ago

@simone-sanfratello I like the async support as well, that's a nice idea for the use cases you described! :) With this in mind, a policy could look like:

app.register(validation, {
  validation: {
    Mutation: {
      register: {
        email: JSON Schema | JTD | async (source, args, context, info) => {...}
      }
      ...
    }
    ...
  }
})

I think this is a good starting point and I don't think it closes the door if we want to support directives too. For example, we could traverse the schema at plugin registration time and build up a JSON Schema to run against args (where the directives would support fields that match up with JSON schema definitions - we can infer the type from GQL :)).

In a nutshell, if I draft a PR with policy support similar to the pseudo code as described above and as well, support directives (by constructing a policy using JSON schema), do you both agree with this approach?

mcollina commented 3 years ago

Yes, it looks pretty solid!

mcollina commented 3 years ago

Yes, it looks pretty solid!

simone-sanfratello commented 3 years ago

Absolutely!

jonnydgreen commented 3 years ago

Just to let you both know, I've not forgotten about this, just needed to sort out a few things that came up - now they're sorted, I'm aiming to get something out by the end of this week :)

jonnydgreen commented 3 years ago

Cobbled together a basic proof of concept this evening. Let me know what you think so far - I've included extra details in the PR description :)

simone-sanfratello commented 3 years ago

Great!

I agree to start with only args in functions, we could probably add context and reply if we want to do a validation like "this auth user can't access to entity id X" but for v1 imo is not necessary