arb / celebrate

A joi validation middleware for Express.
MIT License
1.34k stars 65 forks source link

question: how to throw custom error after route validation or perform async validation? #131

Closed j-d-carmichael closed 5 years ago

j-d-carmichael commented 5 years ago

My joi schema looks like this:

  v1RegisterEmailPost: {
    body: {
      email: Joi.string()
        .min(5)
        .required(),
      password: Joi.string()
        .min(5)
        .required(),
      passwordConfirm: Joi.string()
        .min(5)
        .required(),
      firstName: Joi.string()
        .min(1)
        .required(),
      lastName: Joi.string()
        .min(1)
        .required()
    }
  },

The issue I am having with celebrate is:

Celebrate works as expected with the express routes, but once in the domain layer I need to throw a custom/manually built celebrate error. Better yet, to be able to perform asynchronous validation would be even better and bypass the requirement to throw a custom celebrate error.

The above example is for a registration route, and I need to check if the given email is already registered which would be perfect for an async validator:

For example:

Route:

  router.post(
    '/email',
    celebrate(registerValidators.v1RegisterEmailPost),
    async (req: any, res: express.Response) => {

Joi Validation object:

import { Joi } from 'celebrate';
import RegistrationDomain from '@/domain/RegistrationDomain';

export default {
  v1RegisterEmailPost: {
    body: {
      email: Joi.string()
        .min(5)
        .required()
        .async(RegistrationDomain.isUniqueEmail),
arb commented 5 years ago

I'm afraid I don't understand what your question is... what are you trying to accomplish?

j-d-carmichael commented 5 years ago

The following is simplified to explain:

I either want to throw a celebrate error easily from within the app:

router.post(
    'register/email',
    celebrate(registerValidators.v1RegisterEmailPost),
    async (req: any, res) => { 
       const existingUser = await UserRegistory.findOne({email:req.body.email})
       if(existingUser){
          // throw error in same format as 422 from celebrate
       }
    }
);

OR: Add asynchronous validators, ie, make celebrate wait for a database check before continuing. Essentially the same as laravel eg: https://laravel.com/docs/5.7/validation#rule-unique

arb commented 5 years ago

I see, well there is going to be a format method exposed in #129. It hasn't landed on npm yet, but that's on the horizon.

As far as async support... that comes with Joi 16 (which there is an issue https://github.com/arb/celebrate/issues/116) and is on my radar.

I could publish a version of celebrate with format but without the version of Joi bump as well if that would help you here? I don't want to give you a timeline of when I'll bump the Joi version because I haven't had much time lately.

j-d-carmichael commented 5 years ago

That would be helpful yes thanks!

The async validators with the latest joi would be amazing though, do you have a work in progress on this, any help required?

arb commented 5 years ago

I haven't even started yet on the Joi update TBH. I know version 16 is like a complete soup-to-nuts rewrite. I'm not sure how big of an impact it's really going to be though.

If your feeling ambitious, I'd take a PR for it!

j-d-carmichael commented 5 years ago

@arb I'll give it a crack, I would really benifit from async validators. Would be great if you could merge in your format feature you referenced above first though.

arb commented 5 years ago

@johndcarmichael so that PR has landed and is part of the master branch, if you want, you could point your package.json to it and give it a spin before I publish it to npm. Just to make sure the API feels right and accomplishes what you are trying to do.

j-d-carmichael commented 5 years ago

Yup, works a charm:

import { format, Joi } from 'celebrate';

class LoginDomain {
  /**
   * Operation ID: v1LoginEmailPost
   * Summary: Login
   * Description: Log a user into the system
   */
  public async v1LoginEmailPost (
    body: LoginEmailPostBody
  ): Promise<Login> {
    // force error
    const result = Joi.validate(123, Joi.string().required(), { abortEarly: false });
    throw  format(result, 'params');
  }
}

export default new LoginDomain();

Throws a 422 without changing anything else:

422 | Error: Unprocessable Entity

{
  "joi": {
    "isJoi": true,
    "name": "ValidationError",
    "details": [
      {
        "message": "\"value\" must be a string",
        "path": [],
        "type": "string.base",
        "context": {
          "value": 123,
          "label": "value"
        }
      }
    ],
    "_object": 123
  },
  "meta": {
    "source": "params"
  }
}
Kamahl19 commented 5 years ago

@arb format looks great! Would you consider publishing it? Thanks!

arb commented 5 years ago

I'm working on getting a release candidate done by the end of the week. I don't feel comfortable including format in a non-breaking version bump because I can't be 100% sure I didn't change some other implementation detail when I added it.

If you need it now, point your package.json to the master branch at least until #136 lands.

arb commented 5 years ago

Actually, I changed my mind 😅 I'll publish version 10.1.0 which includes format and I bumped a few DEV dependencies.

arb commented 5 years ago

Version 10.1.0 now exposes format. I will likely have to change it for version 11 because of how Joi changed how it returns errors, but that'll be part of the breaking changes.