Creating custom rules

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.

rules/unique.ts
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.

rules/unique.ts
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.

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)