Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 56 additions & 20 deletions modules/http/request_validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -56,6 +56,39 @@ export class RequestValidator {
*/
static messagesProvider?: (_: HttpContext) => MessagesProviderContact

private requestData() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use the JavaScript private properties vs the TypeScript modifier

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<MetaData extends undefined | Record<string, any>>(
options: RequestValidationOptions<MetaData> | undefined
): RequestValidationOptions<any> {
const validatorOptions: RequestValidationOptions<any> = 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,
Expand Down Expand Up @@ -85,35 +118,38 @@ export class RequestValidator {
? [options?: RequestValidationOptions<MetaData> | undefined]
: [options: RequestValidationOptions<MetaData>]
): Promise<Infer<Schema>> {
const validatorOptions: RequestValidationOptions<any> = 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<string, any>,
>(
validator: VineValidator<Schema, MetaData>,
...[options]: [undefined] extends MetaData
? [options?: RequestValidationOptions<MetaData> | undefined]
: [options: RequestValidationOptions<MetaData>]
): Promise<[ValidationError, null] | [null, Infer<Schema>]> {
/**
* 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)
}
}
4 changes: 4 additions & 0 deletions providers/vinejs_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
160 changes: 159 additions & 1 deletion tests/request_validator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<string, any> | 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',
},
])
})
})