Creating custom rules
VineJS allows you to create custom rules and use them either as standalone functions or add them as methods to existing schema classes.
A custom rule is a function that performs either validation or normalizes the value of a field. It receives the following parameters.
- The value of the field under validation. The value is
unknown
; therefore, you must check for its type before using it. - The options accepted by the rule. If the rule accepts no options, the 2nd parameter will be
undefined
. - Finally, it receives the field context as the 3rd parameter.
For demonstration, we will create a rule named unique
, that queries the database to ensure the value is unique inside a table.
import { FieldContext } from '@vinejs/vine/types'
/**
* Options accepted by the unique rule
*/
type Options = {
table: string
column: string
}
/**
* Implementation
*/
async function unique(
value: unknown,
options: Options,
field: FieldContext
) {
/**
* We do not want to deal with non-string
* values. The "string" rule will handle the
* the validation.
*/
if (typeof value !== 'string') {
return
}
const row = await db
.select(options.column)
.from(options.table)
.where(options.column, value)
.first()
if (row) {
field.report(
'The {{ field }} field is not unique',
'unique',
field
)
}
}
With our unique
rule implementation completed, we can convert this function to a VineJS rule using the vine.createRule
method.
The createRule
method accepts the validation function and returns a new function you can use with any schema type.
import { FieldContext } from '@vinejs/vine/types'
/**
* Options accepted by the unique rule
*/
type Options = {
table: string
column: string
}
/**
* Implementation
*/
async function unique(
value: unknown,
options: Options,
field: FieldContext
) {
/**
* We do not want to deal with non-string
* values. The "string" rule will handle the
* the validation.
*/
if (typeof value !== 'string') {
return
}
const row = await db
.from(options.table)
.select(options.column)
.where(options.column, value)
.first()
if (row) {
field.report(
'The {{ field }} field is not unique',
'unique',
field
)
}
}
/**
* Converting a function to a VineJS rule
*/
export const uniqueRule = vine.createRule(unique)
Let's use this rule inside a schema.
import vine from '@vinejs/vine'
import { uniqueRule } from './rules/unique.js'
const schema = vine.object({
email: vine
.string()
.use(
uniqueRule({ table: 'users', column: 'email' })
)
})
Extending schema classes
As a next step, you may extend the VineString
class and a unique
method to it. The method on the class will offer a chainable API for calling the unique
rule, similar to how we apply email
or url
rules.
import { VineString } from '@vinejs/vine'
import { uniqueRule, Options } from './rules/unique.js'
VineString.macro('unique', function (this: VineString, options: Options) {
return this.use(uniqueRule(options))
})
Since the unique
method is added to the VineString
class at runtime, we must inform TypeScript about it using declaration merging.
import { VineString } from '@vinejs/vine'
import { uniqueRule, Options } from './rules/unique.js'
declare module '@vinejs/vine' {
interface VineString {
unique(options: Options): this
}
}
VineString.macro('unique', function (this: VineString, options: Options) {
return this.use(uniqueRule(options))
})
That's all. Now, we can use the unique
method as follows.
import vine from '@vinejs/vine'
const schema = vine.object({
email: vine
.string()
.unique({
table: 'users',
column: 'email'
})
})
Like VineString
, you may add macros to the following classes.
- VineBoolean
- VineNumber
- VineAccepted
- VineEnum
- VineObject
- VineArray
- VineRecord
- VineTuple
- VineDate
- VineAny
- VineLiteral
Guarding against invalid values
VineJS stops the validations pipeline after a rule reports an error. For example, if a field fails the string
validation rule, VineJS will not run the unique
validation rule.
However, this behavior can be changed by turning off the bail mode. With bail mode disabled, the unique
rule will be executed, even when the value is not a string.
However, you can find if the field has failed a validation using the field.isValid
property and do not perform the SQL query.
async function unique(
value: unknown,
options: Options,
field: FieldContext
) {
if (typeof value !== 'string') {
return
}
if (!field.isValid) {
return
}
const row = await db
.select(options.column)
.from(options.table)
.first()
if (row) {
field.report(
'The value of {{ field }} field is not unique',
'unique',
field
)
}
}
Creating an implicit rule
As per the default behavior of VineJS, a rule is not executed if the field's value is null
or undefined
. However, if you want the rule to be executed regardless of the value, you must mark it as an implicit
rule.
export const uniqueRule = vine.createRule(unique, {
implicit: true
})
Marking rule as async
Since VineJS is built with performance in mind, we do not await
rules when they are synchronous.
If a rule uses the async
keyword, we consider it async and make sure to await
it. However, there are many ways to create async functions in JavaScript, which are tricky to detect.
Therefore, if you are creating an async function without the async
keyword, make sure to notify the createRule
method explicitly.
export const uniqueRule = vine.createRule(unique, {
implicit: true,
async: true,
})
Testing rules
You may use the validator
test factory to write unit tests for your custom validation rules.
The validator.executeAsync
method accepts the validation rule and the value to validate. The output is an instance of ValidationResult you can use to write assertions.
import { validator } from '@vinejs/vine/factories'
import { uniqueRule } from '../src/rules/unique.js'
const value = 'foo@bar.com'
const unique = uniqueRule({ table: 'users', column: 'email' })
const validated = await validator.executeAsync(unique, value)
/**
* Assert the validation succeeded.
*/
validated.assertSucceeded()
You may use the assertErrorsCount
and assertError
methods to ensure the validation fails with a given error message.
const value = 'foo@example.com'
const unique = uniqueRule({ table: 'users', column: 'email' })
const validated = await validator
.withContext({
fieldName: 'email'
})
.executeAsync(unique, value)
validated.assertErrorsCount(1)
validated.assertError('The email field is not unique')
Available assertions
Following is the list of available assertion methods
/**
* Assert the validation succeeded
*/
validated.assertSucceeded()
/**
* Assert the value output after validation. You may
* use this assertion if the rule mutates the field's
* value.
*/
validated.assertOutput(expectedOutput)
/**
* Assert the validation failed
*/
validated.assertFailed()
/**
* Assert expected errors count
*/
validated.assertErrorsCount(1)
/**
* Assert an error with the expected text is reported
* during validation
*/
validated.assertError('The email field is not unique')
Executing a chain of validations
Suppose your validation rule relies on other validation rules. In that case, you may pass an array of validations to the executeAsync
method, and all the validations will be executed in the defined sequence.
import { VineString } from '@vinejs/vine'
import { uniqueRule } from '../src/rules/unique.js'
const email = VineString.rules.email()
const unique = uniqueRule({ table: 'users', column: 'email' })
const validated = await validator.executeAsync([
email,
unique,
], value)
Defining custom field context
You may pass a custom field context using the withContext
method. The context object will be merged with the default context created by the test factory.
await validator
.withContext({
fieldName: 'email',
wildCardPath: 'profile.email'
})
.executeAsync(unique, value)
Disabling bail mode
You may disable the bail mode using the bail
method.
await validator
.bail(false)
.executeAsync(unique, value)