From 8e3b54af27f8424732640adca1cb4b3376e79ce0 Mon Sep 17 00:00:00 2001 From: Joris Date: Fri, 22 Jan 2021 08:57:48 +0100 Subject: [PATCH] feat: add Joi sync validation (#116) * feat: add Joi sync validation * fix(joi): sync error * test: add validateAllCriteria test * test(joi): update test description --- joi/src/__tests__/__snapshots__/joi.ts.snap | 82 +++++++++++++++++++++ joi/src/__tests__/joi.ts | 59 +++++++++++++++ joi/src/joi.ts | 20 ++++- joi/src/types.ts | 3 +- 4 files changed, 159 insertions(+), 5 deletions(-) diff --git a/joi/src/__tests__/__snapshots__/joi.ts.snap b/joi/src/__tests__/__snapshots__/joi.ts.snap index 1202d999..8862c0b2 100644 --- a/joi/src/__tests__/__snapshots__/joi.ts.snap +++ b/joi/src/__tests__/__snapshots__/joi.ts.snap @@ -32,6 +32,38 @@ Object { } `; +exports[`joiResolver should return a single error from joiResolver with \`mode: sync\` when validation fails 1`] = ` +Object { + "errors": Object { + "birthYear": Object { + "message": "\\"birthYear\\" must be a number", + "type": "number.base", + }, + "email": Object { + "message": "\\"email\\" is not allowed to be empty", + "type": "string.empty", + }, + "enabled": Object { + "message": "\\"enabled\\" is required", + "type": "any.required", + }, + "password": Object { + "message": "\\"password\\" with value \\"___\\" fails to match the required pattern: /^[a-zA-Z0-9]{3,30}$/", + "type": "string.pattern.base", + }, + "tags": Object { + "message": "\\"tags\\" is required", + "type": "any.required", + }, + "username": Object { + "message": "\\"username\\" is required", + "type": "any.required", + }, + }, + "values": Object {}, +} +`; + exports[`joiResolver should return all the errors from joiResolver when validation fails with \`validateAllFieldCriteria\` set to true 1`] = ` Object { "errors": Object { @@ -81,3 +113,53 @@ Object { "values": Object {}, } `; + +exports[`joiResolver should return all the errors from joiResolver when validation fails with \`validateAllFieldCriteria\` set to true and \`mode: sync\` 1`] = ` +Object { + "errors": Object { + "birthYear": Object { + "message": "\\"birthYear\\" must be a number", + "type": "number.base", + "types": Object { + "number.base": "\\"birthYear\\" must be a number", + }, + }, + "email": Object { + "message": "\\"email\\" is not allowed to be empty", + "type": "string.empty", + "types": Object { + "string.empty": "\\"email\\" is not allowed to be empty", + }, + }, + "enabled": Object { + "message": "\\"enabled\\" is required", + "type": "any.required", + "types": Object { + "any.required": "\\"enabled\\" is required", + }, + }, + "password": Object { + "message": "\\"password\\" with value \\"___\\" fails to match the required pattern: /^[a-zA-Z0-9]{3,30}$/", + "type": "string.pattern.base", + "types": Object { + "string.pattern.base": "\\"password\\" with value \\"___\\" fails to match the required pattern: /^[a-zA-Z0-9]{3,30}$/", + }, + }, + "tags": Object { + "message": "\\"tags\\" is required", + "type": "any.required", + "types": Object { + "any.required": "\\"tags\\" is required", + }, + }, + "username": Object { + "message": "\\"username\\" is required", + "type": "any.required", + "types": Object { + "any.required": "\\"username\\" is required", + }, + }, + }, + "values": Object {}, +} +`; diff --git a/joi/src/__tests__/joi.ts b/joi/src/__tests__/joi.ts index 38369fd8..d4ca6313 100644 --- a/joi/src/__tests__/joi.ts +++ b/joi/src/__tests__/joi.ts @@ -38,8 +38,34 @@ describe('joiResolver', () => { enabled: true, }; + const validateAsyncSpy = jest.spyOn(schema, 'validateAsync'); + const validateSpy = jest.spyOn(schema, 'validate'); + const result = await joiResolver(schema)(data); + expect(validateSpy).not.toHaveBeenCalled(); + expect(validateAsyncSpy).toHaveBeenCalledTimes(1); + expect(result).toEqual({ errors: {}, values: data }); + }); + + it('should return values from joiResolver with `mode: sync` when validation pass', async () => { + const data: Data = { + username: 'Doe', + password: 'Password123', + repeatPassword: 'Password123', + birthYear: 2000, + email: 'john@doe.com', + tags: ['tag1', 'tag2'], + enabled: true, + }; + + const validateAsyncSpy = jest.spyOn(schema, 'validateAsync'); + const validateSpy = jest.spyOn(schema, 'validate'); + + const result = await joiResolver(schema, undefined, { mode: 'sync' })(data); + + expect(validateAsyncSpy).not.toHaveBeenCalled(); + expect(validateSpy).toHaveBeenCalledTimes(1); expect(result).toEqual({ errors: {}, values: data }); }); @@ -55,6 +81,23 @@ describe('joiResolver', () => { expect(result).toMatchSnapshot(); }); + it('should return a single error from joiResolver with `mode: sync` when validation fails', async () => { + const data = { + password: '___', + email: '', + birthYear: 'birthYear', + }; + + const validateAsyncSpy = jest.spyOn(schema, 'validateAsync'); + const validateSpy = jest.spyOn(schema, 'validate'); + + const result = await joiResolver(schema, undefined, { mode: 'sync' })(data); + + expect(validateAsyncSpy).not.toHaveBeenCalled(); + expect(validateSpy).toHaveBeenCalledTimes(1); + expect(result).toMatchSnapshot(); + }); + it('should return all the errors from joiResolver when validation fails with `validateAllFieldCriteria` set to true', async () => { const data = { password: '___', @@ -66,4 +109,20 @@ describe('joiResolver', () => { expect(result).toMatchSnapshot(); }); + + it('should return all the errors from joiResolver when validation fails with `validateAllFieldCriteria` set to true and `mode: sync`', async () => { + const data = { + password: '___', + email: '', + birthYear: 'birthYear', + }; + + const result = await joiResolver(schema, undefined, { mode: 'sync' })( + data, + undefined, + true, + ); + + expect(result).toMatchSnapshot(); + }); }); diff --git a/joi/src/joi.ts b/joi/src/joi.ts index 00b5a077..dd4985ca 100644 --- a/joi/src/joi.ts +++ b/joi/src/joi.ts @@ -46,15 +46,27 @@ const parseErrorSchema = ( export const joiResolver: Resolver = ( schema, - options = { + schemaOptions = { abortEarly: false, }, + { mode } = { mode: 'async' }, ) => async (values, _, validateAllFieldCriteria = false) => { try { + let result; + if (mode === 'async') { + result = await schema.validateAsync(values, schemaOptions); + } else { + const { value, error } = schema.validate(values, schemaOptions); + + if (error) { + throw error; + } + + result = value; + } + return { - values: await schema.validateAsync(values, { - ...options, - }), + values: result, errors: {}, }; } catch (e) { diff --git a/joi/src/types.ts b/joi/src/types.ts index 6faf8880..c56a690d 100644 --- a/joi/src/types.ts +++ b/joi/src/types.ts @@ -7,7 +7,8 @@ import type { AsyncValidationOptions, Schema } from 'joi'; export type Resolver = ( schema: T, - options?: AsyncValidationOptions, + schemaOptions?: AsyncValidationOptions, + resolverOptions?: { mode: 'async' | 'sync' }, ) => ( values: UnpackNestedValue, context?: TContext,