diff --git a/modules/http/request_validator.ts b/modules/http/request_validator.ts index 36f15d1a..91793ddb 100644 --- a/modules/http/request_validator.ts +++ b/modules/http/request_validator.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import type { VineValidator } from '@vinejs/vine' +import type { ValidationError, VineValidator } from '@vinejs/vine' import type { Infer, SchemaTypes, @@ -56,6 +56,39 @@ export class RequestValidator { */ static messagesProvider?: (_: HttpContext) => MessagesProviderContact + #requestData() { + const requestBody = this.#ctx.request.all() + return { + ...requestBody, + params: this.#ctx.request.params(), + headers: this.#ctx.request.headers(), + cookies: this.#ctx.request.cookiesList(), + } + } + + #processValidatorOptions>( + options: RequestValidationOptions | undefined + ): RequestValidationOptions { + const validatorOptions: RequestValidationOptions = options || {} + + /** + * Assign request specific error reporter + */ + if (RequestValidator.errorReporter && !validatorOptions.errorReporter) { + const errorReporter = RequestValidator.errorReporter(this.#ctx) + validatorOptions.errorReporter = () => errorReporter + } + + /** + * Assign request specific messages provider + */ + if (RequestValidator.messagesProvider && !validatorOptions.messagesProvider) { + validatorOptions.messagesProvider = RequestValidator.messagesProvider(this.#ctx) + } + + return validatorOptions + } + /** * Validate the current HTTP request data using a VineJS validator. * This method automatically includes request body, files, URL parameters, @@ -85,35 +118,38 @@ export class RequestValidator { ? [options?: RequestValidationOptions | undefined] : [options: RequestValidationOptions] ): Promise> { - const validatorOptions: RequestValidationOptions = options || {} - /** - * Assign request specific error reporter + * Process the validation options */ - if (RequestValidator.errorReporter && !validatorOptions.errorReporter) { - const errorReporter = RequestValidator.errorReporter(this.#ctx) - validatorOptions.errorReporter = () => errorReporter - } + const validatorOptions = this.#processValidatorOptions(options) /** - * Assign request specific messages provider + * Data to validate */ - if (RequestValidator.messagesProvider && !validatorOptions.messagesProvider) { - validatorOptions.messagesProvider = RequestValidator.messagesProvider(this.#ctx) - } + const data = validatorOptions.data || this.#requestData() - const requestBody = this.#ctx.request.all() + return validator.validate(data, validatorOptions as any) + } + + async tryValidateUsing< + Schema extends SchemaTypes, + MetaData extends undefined | Record, + >( + validator: VineValidator, + ...[options]: [undefined] extends MetaData + ? [options?: RequestValidationOptions | undefined] + : [options: RequestValidationOptions] + ): Promise<[ValidationError, null] | [null, Infer]> { + /** + * Process the validation options + */ + const validatorOptions = this.#processValidatorOptions(options) /** * Data to validate */ - const data = validatorOptions.data || { - ...requestBody, - params: this.#ctx.request.params(), - headers: this.#ctx.request.headers(), - cookies: this.#ctx.request.cookiesList(), - } + const data = validatorOptions.data || this.#requestData() - return validator.validate(data, validatorOptions as any) + return validator.tryValidate(data, validatorOptions as any) } } diff --git a/providers/vinejs_provider.ts b/providers/vinejs_provider.ts index e37fca8b..ad0485df 100644 --- a/providers/vinejs_provider.ts +++ b/providers/vinejs_provider.ts @@ -80,5 +80,9 @@ export default class VineJSServiceProvider { HttpRequest.macro('validateUsing', function (this: HttpRequest, ...args) { return new RequestValidator(this.ctx!).validateUsing(...args) }) + + HttpRequest.macro('tryValidateUsing', function (this: HttpRequest, ...args) { + return new RequestValidator(this.ctx!).tryValidateUsing(...args) + }) } } diff --git a/tests/request_validator.spec.ts b/tests/request_validator.spec.ts index 9f13ffaa..0cef90cf 100644 --- a/tests/request_validator.spec.ts +++ b/tests/request_validator.spec.ts @@ -9,7 +9,7 @@ import { test } from '@japa/runner' import { type FieldContext } from '@vinejs/vine/types' -import vine, { SimpleErrorReporter, SimpleMessagesProvider } from '@vinejs/vine' +import vine, { SimpleErrorReporter, SimpleMessagesProvider, ValidationError } from '@vinejs/vine' import { RequestValidator } from '../modules/http/main.ts' import { IgnitorFactory } from '../factories/core/ignitor.ts' @@ -319,4 +319,162 @@ test.group('Request validator', () => { ]) } }) + + test('try validate using') + .with([ + { expectValid: true, body: { username: 'virk' } }, + { expectValid: false, body: { foo: true } }, + ]) + .run(async ({ assert }, { expectValid, body }) => { + assert.plan(2) + + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [ + () => import('../providers/app_provider.js'), + () => import('../providers/vinejs_provider.js'), + ], + }, + }) + .create(BASE_URL) + + const testUtils = new TestUtilsFactory().create(ignitor) + await testUtils.app.init() + await testUtils.app.boot() + await testUtils.boot() + + const ctx = await testUtils.createHttpContext() + const validator = vine.compile( + vine.object({ + username: vine.string(), + }) + ) + + ctx.request.setInitialBody(body) + + const [error, data] = await ctx.request.tryValidateUsing(validator) + + if (expectValid) { + assert.isNull(error) + assert.deepEqual(data, body) + } else { + assert.instanceOf(error, ValidationError) + assert.isNull(data) + } + }) + + test('try validate using with custom messages provider', async ({ assert, cleanup }) => { + assert.plan(3) + + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [ + () => import('../providers/app_provider.js'), + () => import('../providers/vinejs_provider.js'), + ], + }, + }) + .create(BASE_URL) + + const testUtils = new TestUtilsFactory().create(ignitor) + await testUtils.app.init() + await testUtils.app.boot() + await testUtils.boot() + + const ctx = await testUtils.createHttpContext() + const validator = vine.compile( + vine.object({ + username: vine.string(), + }) + ) + + ctx.request.setInitialBody({ error: true }) + + RequestValidator.messagesProvider = () => + new SimpleMessagesProvider( + { + required: 'The value is missing', + }, + {} + ) + cleanup(() => { + RequestValidator.messagesProvider = undefined + }) + + const [error, data] = await ctx.request.tryValidateUsing(validator) + + assert.isNull(data) + assert.instanceOf(error, ValidationError) + assert.deepEqual(error!.messages, [ + { + field: 'username', + message: 'The value is missing', + rule: 'required', + }, + ]) + }) + + test('try validate using with custom error reporter', async ({ assert, cleanup }) => { + assert.plan(3) + + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [ + () => import('../providers/app_provider.js'), + () => import('../providers/vinejs_provider.js'), + ], + }, + }) + .create(BASE_URL) + + const testUtils = new TestUtilsFactory().create(ignitor) + await testUtils.app.init() + await testUtils.app.boot() + await testUtils.boot() + + const ctx = await testUtils.createHttpContext() + const validator = vine.compile( + vine.object({ + username: vine.string(), + }) + ) + + ctx.request.setInitialBody({ error: true }) + + class MyErrorReporter extends SimpleErrorReporter { + report( + message: string, + rule: string, + field: FieldContext, + meta?: Record | undefined + ): void { + return super.report(message, `validations.${rule}`, field, meta) + } + } + + RequestValidator.errorReporter = () => { + return new MyErrorReporter() + } + cleanup(() => { + RequestValidator.errorReporter = undefined + }) + + const [error, data] = await ctx.request.tryValidateUsing(validator) + + assert.isNull(data) + assert.instanceOf(error, ValidationError) + assert.deepEqual(error!.messages, [ + { + field: 'username', + message: 'The username field must be defined', + rule: 'validations.required', + }, + ]) + }) })