From 00aff08a294db606649b05984a0b62e2d042fc6c Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Wed, 21 Jan 2026 19:54:07 +0100 Subject: [PATCH 1/2] feat: add tryValidateUsing to HttpRequest --- modules/http/request_validator.ts | 76 ++++++++++---- providers/vinejs_provider.ts | 4 + tests/request_validator.spec.ts | 160 +++++++++++++++++++++++++++++- 3 files changed, 219 insertions(+), 21 deletions(-) diff --git a/modules/http/request_validator.ts b/modules/http/request_validator.ts index 36f15d1a..2274d368 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 + private requestData() { + const requestBody = this.#ctx.request.all() + return { + ...requestBody, + params: this.#ctx.request.params(), + headers: this.#ctx.request.headers(), + cookies: this.#ctx.request.cookiesList(), + } + } + + private 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', + }, + ]) + }) }) From fbb8c795f6fafddc6705ce28fb8f48471817b5e2 Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Fri, 23 Jan 2026 12:02:11 +0100 Subject: [PATCH 2/2] Apply suggestions from code review --- modules/http/request_validator.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/http/request_validator.ts b/modules/http/request_validator.ts index 2274d368..91793ddb 100644 --- a/modules/http/request_validator.ts +++ b/modules/http/request_validator.ts @@ -56,7 +56,7 @@ export class RequestValidator { */ static messagesProvider?: (_: HttpContext) => MessagesProviderContact - private requestData() { + #requestData() { const requestBody = this.#ctx.request.all() return { ...requestBody, @@ -66,7 +66,7 @@ export class RequestValidator { } } - private processValidatorOptions>( + #processValidatorOptions>( options: RequestValidationOptions | undefined ): RequestValidationOptions { const validatorOptions: RequestValidationOptions = options || {} @@ -121,12 +121,12 @@ export class RequestValidator { /** * Process the validation options */ - const validatorOptions = this.processValidatorOptions(options) + const validatorOptions = this.#processValidatorOptions(options) /** * Data to validate */ - const data = validatorOptions.data || this.requestData() + const data = validatorOptions.data || this.#requestData() return validator.validate(data, validatorOptions as any) } @@ -143,12 +143,12 @@ export class RequestValidator { /** * Process the validation options */ - const validatorOptions = this.processValidatorOptions(options) + const validatorOptions = this.#processValidatorOptions(options) /** * Data to validate */ - const data = validatorOptions.data || this.requestData() + const data = validatorOptions.data || this.#requestData() return validator.tryValidate(data, validatorOptions as any) }