In a vineJs validator we have validation function vine.date() however, it doesn't accept any DateTime values (with timezone) as far as I can tell. It will always reject as 'not a valid date'.
Is there some way of getting this to work?
You can use vine.date({ formats: { utc: true } }).
Then you can pass in the payload :
2024-05-17T00:00:00.000Z2024-05-17T00:00:00.000+00:002024-05-17T02:00:00.000+02:00and you will always get the same date 2024-05-17T00:00:00.000Z
As Tom Gobich said here : https://adocasts.com/lessons/validating-form-data-with-vinejs
You can use the transform method like this.
import { DateTime } from 'luxon'
const validator = vine.compile(
vine.object({
startDate: vine.date().transform((date) => DateTime.fromJSDate(date))
})
)
I've revised and made an entire custom schema to handle all the chainable methods as well. Currently only working with ISO date strings but you can modify as needed.
You can now use vine.dateTime().afterField('field') etc.
Make a datetime.ts file for the rules.
import { FieldContext } from '@vinejs/vine/types'
import vine from '@vinejs/vine'
import { DateTime, DateTimeOptions } from 'luxon'
import { messages } from '@vinejs/vine/defaults'
export const DEFAULT_DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZ'
type DateEqualsOptions = {
compare?: 'day' | 'month' | 'year'
format?: string
opts?: DateTimeOptions
}
/**
* Schema for checking whether input value is DateTime value
*/
export const dateTimeRule = vine.createRule((value: unknown, options, field: FieldContext) => {
if (typeof value !== 'string') {
field.report(messages.date, 'date', field)
return
}
if (!DateTime.fromISO(value).isValid) {
field.report('The {{ field }} is not a valid ISO date time value',
'datetime', field)
return
}
field.meta.$value = DateTime.fromISO(value)
field.mutate(DateTime.fromISO(value), field)
})
/**
* The equals rule compares the input date to be same as the expected
value.
*
* By default, the comparisons of day, month and years are performed
*/
export const equalsRule = vine.createRule<
{
expectedValue: string | ((field: FieldContext) => string)
} & DateEqualsOptions
>((value: unknown, options, field: FieldContext) => {
if (!field.meta.$value) {
return
}
const compare = options.compare || 'day'
const format = options.format || DEFAULT_DATE_FORMAT
const opts = options.opts || {}
const dateTime = field.meta.$value as DateTime
const expectedValue =
typeof options.expectedValue === 'function'
? options.expectedValue(field)
: options.expectedValue
const expectedDateTime = DateTime.fromFormat(expectedValue, format,
opts)
if (!DateTime.isDateTime(expectedDateTime)) {
throw new Error(`Invalid datetime value "${expectedValue}" value
provided to the equals rule`)
}
/**
* Ensure both the dates are the same
*/
if (!dateTime.hasSame(expectedDateTime, compare)) {
field.report(messages['date.equals'], 'date.equals', field, {
expectedValue,
compare,
})
}
})
/**
* The after rule compares the input value to be after
* the expected value.
*
* By default, the comparions of day, month and years are performed.
*/
export const afterRule = vine.createRule<
{
expectedValue:
| 'today'
| 'tomorrow'
| (string & { _?: never })
| ((field: FieldContext) => string)
} & DateEqualsOptions
>((_, options, field) => {
if (!field.meta.$value) {
return
}
const compare = options.compare || 'day'
const format = options.format || DEFAULT_DATE_FORMAT
const dateTime = field.meta.$value as DateTime
const opts = options.opts || {}
const expectedValue =
typeof options.expectedValue === 'function'
? options.expectedValue(field)
: options.expectedValue
console.log('expectedValue', expectedValue)
const expectedDateTime =
expectedValue === 'today'
? DateTime.now()
: expectedValue === 'tomorrow'
? DateTime.now().plus({ days: 1 })
: DateTime.fromISO(expectedValue)
if (!DateTime.isDateTime(expectedDateTime)) {
throw new Error(`Invalid datetime value "${expectedValue}" value provided to the after rule`)
}
/**
* Ensure the input is after the expected value
*/
if (!dateTime.hasSame(expectedDateTime, compare) && !(dateTime > expectedDateTime)) {
console.log('dateTime', dateTime)
field.report(messages['date.after'], 'date.after', field, {
expectedValue,
compare,
})
}
})
/**
* The after or equal rule compares the input value to be
* after or equal to the expected value.
*
* By default, the comparions of day, month and years are performed.
*/
export const afterOrEqualRule = vine.createRule<
{
expectedValue:
| 'today'
| 'tomorrow'
| (string & { _?: never })
| ((field: FieldContext) => string)
} & DateEqualsOptions
>((_, options, field) => {
if (!field.meta.$value) {
return
}
const compare = options.compare || 'day'
const format = options.format || DEFAULT_DATE_FORMAT
const dateTime = field.meta.$value as DateTime
const opts = options.opts || {}
const expectedValue =
typeof options.expectedValue === 'function'
? options.expectedValue(field)
: options.expectedValue
const expectedDateTime =
expectedValue === 'today'
? DateTime.now()
: expectedValue === 'tomorrow'
? DateTime.now().plus({ days: 1 })
: DateTime.fromFormat(expectedValue, format, opts)
if (!DateTime.isDateTime(expectedDateTime)) {
throw new Error(
`Invalid datetime value "${expectedValue}" value provided to the afterOrEqual rule`
)
}
/**
* Ensure both the dates are the same or the input
* is after the expected value.
*/
if (!dateTime.hasSame(expectedDateTime, compare) || !(dateTime > expectedDateTime)) {
field.report(messages['date.afterOrEqual'], 'date.afterOrEqual', field, {
expectedValue,
compare,
})
}
})
/**
* The before rule compares the input value to be before
* the expected value.
*
* By default, the comparions of day, month and years are performed.
*/
export const beforeRule = vine.createRule<
{
expectedValue:
| 'today'
| 'yesterday'
| (string & { _?: never })
| ((field: FieldContext) => string)
} & DateEqualsOptions
>((_, options, field) => {
if (!field.meta.$value) {
return
}
const compare = options.compare || 'day'
const format = options.format || DEFAULT_DATE_FORMAT
const dateTime = field.meta.$value as DateTime
const opts = options.opts || {}
const expectedValue =
typeof options.expectedValue === 'function'
? options.expectedValue(field)
: options.expectedValue
const expectedDateTime =
expectedValue === 'today'
? DateTime.now()
: expectedValue === 'tomorrow'
? DateTime.now().minus({ days: 1 })
: DateTime.fromFormat(expectedValue, format, opts)
if (!DateTime.isDateTime(expectedDateTime)) {
throw new Error(`Invalid datetime value "${expectedValue}" value provided to the before rule`)
}
/**
* Ensure the input is before the expected value
*/
if (!dateTime.hasSame(expectedDateTime, compare) && !(dateTime < expectedDateTime)) {
field.report(messages['date.before'], 'date.before', field, {
expectedValue,
compare,
})
}
})
/**
* The before or equal rule compares the input value to be
* before or equal to the expected value.
*
* By default, the comparions of day, month and years are performed.
*/
export const beforeOrEqualRule = vine.createRule<
{
expectedValue:
| 'today'
| 'yesterday'
| (string & { _?: never })
| ((field: FieldContext) => string)
} & DateEqualsOptions
>((_, options, field) => {
if (!field.meta.$value) {
return
}
const compare = options.compare || 'day'
const format = options.format || DEFAULT_DATE_FORMAT
const dateTime = field.meta.$value as DateTime
const opts = options.opts || {}
const expectedValue =
typeof options.expectedValue === 'function'
? options.expectedValue(field)
: options.expectedValue
const expectedDateTime =
expectedValue === 'today'
? DateTime.now()
: expectedValue === 'tomorrow'
? DateTime.now().minus({ days: 1 })
: DateTime.fromFormat(expectedValue, format, opts)
if (!DateTime.isDateTime(expectedDateTime)) {
throw new Error(
`Invalid datetime value "${expectedValue}" value provided to the beforeOrEqual rule`
)
}
/**
* Ensure the input is same or before the expected value
*/
if (!dateTime.hasSame(expectedDateTime, compare) || !(dateTime < expectedDateTime)) {
field.report(messages['date.beforeOrEqual'], 'date.beforeOrEqual', field, {
expectedValue,
compare,
})
}
})
/**
* The sameAs rule expects the input value to be same
* as the value of the other field.
*
* By default, the comparions of day, month and years are performed
*/
export const sameAsRule = vine.createRule<
{
otherField: string
} & DateEqualsOptions
>((_, options, field) => {
if (!field.meta.$value) {
return
}
const compare = options.compare || 'day'
const dateTime = field.meta.$value as DateTime
const format = options.format || field.meta.$formats
const expectedValue = vine.helpers.getNestedValue(options.otherField, field)
const opts = options.opts || {}
const expectedDateTime = DateTime.fromFormat(expectedValue, format, opts)
/**
* Skip validation when the other field is not a valid
* datetime. We will let the `date` rule on that
* other field handle the invalid date.
*/
if (!DateTime.isDateTime(expectedDateTime)) {
return
}
/**
* Ensure both the dates are the same
*/
if (!dateTime.hasSame(expectedDateTime, compare)) {
field.report(messages['date.sameAs'], 'date.sameAs', field, {
otherField: options.otherField,
expectedValue,
compare,
})
}
})
/**
* The notSameAs rule expects the input value to be different
* from the other field's value
*
* By default, the comparions of day, month and years are performed
*/
export const notSameAsRule = vine.createRule<
{
otherField: string
} & DateEqualsOptions
>((_, options, field) => {
if (!field.meta.$value) {
return
}
const compare = options.compare || 'day'
const dateTime = field.meta.$value as DateTime
const format = options.format || field.meta.$formats
const opts = options.opts || {}
const expectedValue = vine.helpers.getNestedValue(options.otherField, field)
const expectedDateTime = DateTime.fromFormat(expectedValue, format, opts)
/**
* Skip validation when the other field is not a valid
* datetime. We will let the `date` rule on that
* other field to handle the invalid date.
*/
if (!DateTime.isDateTime(expectedDateTime)) {
return
}
/**
* Ensure both the dates are different
*/
if (dateTime.hasSame(expectedDateTime, compare)) {
field.report(messages['date.notSameAs'], 'date.notSameAs', field, {
otherField: options.otherField,
expectedValue,
compare,
})
}
})
/**
* The afterField rule expects the input value to be after
* the other field's value.
*
* By default, the comparions of day, month and years are performed
*/
export const afterFieldRule = vine.createRule<
{
otherField: string
} & DateEqualsOptions
>((_, options, field) => {
if (!field.meta.$value) {
return
}
const compare = options.compare || 'day'
const dateTime = field.meta.$value as DateTime
const expectedValue = vine.helpers.getNestedValue(options.otherField, field)
const expectedDateTime = DateTime.fromISO(expectedValue)
/**
* Skip validation when the other field is not a valid
* datetime. We will let the `date` rule on that
* other field to handle the invalid date.
*/
if (!DateTime.isDateTime(expectedDateTime)) {
return
}
/**
* Ensure the input date is after the other field's value
*/
if (!(dateTime > expectedDateTime) || dateTime.hasSame(expectedDateTime, compare)) {
field.report(messages['date.afterField'], 'date.afterField', field, {
otherField: options.otherField,
expectedValue,
compare,
})
}
})
/**
* The afterOrSameAs rule expects the input value to be after
* or same as the other field's value.
*
* By default, the comparions of day, month and years are performed
*/
export const afterOrSameAsRule = vine.createRule<
{
otherField: string
} & DateEqualsOptions
>((_, options, field) => {
if (!field.meta.$value) {
return
}
const compare = options.compare || 'day'
const dateTime = field.meta.$value as DateTime
const format = options.format || field.meta.$formats
const expectedValue = vine.helpers.getNestedValue(options.otherField, field)
const opts = options.opts || {}
const expectedDateTime = DateTime.fromFormat(expectedValue, format, opts)
/**
* Skip validation when the other field is not a valid
* datetime. We will let the `date` rule on that
* other field to handle the invalid date.
*/
if (!DateTime.isDateTime(expectedDateTime)) {
return
}
/**
* Ensure the input date is same as or after the other field's value
*/
if (!dateTime.hasSame(expectedDateTime, compare) || !(dateTime > expectedDateTime)) {
field.report(messages['date.afterOrSameAs'], 'date.afterOrSameAs', field, {
otherField: options.otherField,
expectedValue,
compare,
})
}
})
/**
* The beforeField rule expects the input value to be before
* the other field's value.
*
* By default, the comparions of day, month and years are performed
*/
export const beforeFieldRule = vine.createRule<
{
otherField: string
} & DateEqualsOptions
>((_, options, field) => {
if (!field.meta.$value) {
return
}
const compare = options.compare || 'day'
const dateTime = field.meta.$value as DateTime
const format = options.format || field.meta.$formats
const opts = options.opts || {}
const expectedValue = vine.helpers.getNestedValue(options.otherField, field)
const expectedDateTime = DateTime.fromFormat(expectedValue, format, opts)
/**
* Skip validation when the other field is not a valid
* datetime. We will let the `date` rule on that
* other field to handle the invalid date.
*/
if (!DateTime.isDateTime(expectedDateTime)) {
return
}
/**
* Ensure the input date is before the other field's value
*/
if (dateTime.hasSame(expectedDateTime, compare) || !(dateTime < expectedDateTime)) {
field.report(messages['date.beforeField'], 'date.beforeField', field, {
otherField: options.otherField,
expectedValue,
compare,
})
}
})
/**
* The beforeOrSameAs rule expects the input value to be before
* or same as the other field's value.
*
* By default, the comparions of day, month and years are performed
*/
export const beforeOrSameAsRule = vine.createRule<
{
otherField: string
} & DateEqualsOptions
>((_, options, field) => {
if (!field.meta.$value) {
return
}
const compare = options.compare || 'day'
const dateTime = field.meta.$value as DateTime
const format = options.format || field.meta.$formats
const opts = options.opts || {}
const expectedValue = vine.helpers.getNestedValue(options.otherField, field)
const expectedDateTime = DateTime.fromFormat(expectedValue, format, opts)
/**
* Skip validation when the other field is not a valid
* datetime. We will let the `date` rule on that
* other field to handle the invalid date.
*/
if (!DateTime.isDateTime(expectedDateTime)) {
return
}
/**
* Ensure the input date is before or same as the other field's value
*/
if (!dateTime.hasSame(expectedDateTime, compare) || !(dateTime < expectedDateTime)) {
field.report(messages['date.beforeOrSameAs'], 'date.beforeOrSameAs', field, {
otherField: options.otherField,
expectedValue,
compare,
})
}
})
/**
* The weekend rule ensures the date falls on a weekend
*/
export const weekendRule = vine.createRule((_, __, field) => {
if (!field.meta.$value) {
return
}
const dateTime = field.meta.$value as DateTime
const day = dateTime.weekday
if (day !== 6 && day !== 7) {
field.report(messages['date.weekend'], 'date.weekend', field)
}
})
/**
* The weekday rule ensures the date falls on a weekday
*/
export const weekdayRule = vine.createRule((_, __, field) => {
if (!field.meta.$value) {
return
}
const dateTime = field.meta.$value as DateTime
const day = dateTime.weekday
if (day === 6 || day === 7) {
field.report(messages['date.weekday'], 'date.weekday', field)
}
})
Make a VineDateTime schema class:
/*
* vinejs
*
* (c) VineJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import dayjs from 'dayjs'
import { BaseLiteralType } from '@vinejs/vine'
import {
dateTimeRule,
afterRule,
beforeRule,
sameAsRule,
equalsRule,
weekendRule,
weekdayRule,
notSameAsRule,
afterFieldRule,
beforeFieldRule,
afterOrEqualRule,
afterOrSameAsRule,
beforeOrEqualRule,
beforeOrSameAsRule,
} from '../datetime.js'
import type { Validation, FieldOptions, FieldContext } from
'@vinejs/vine/types'
import { DateTime, type DateTimeOptions } from 'luxon'
type DateEqualsOptions = {
compare?: 'day' | 'month' | 'year'
format?: string
opts?: DateTimeOptions
}
/**
* VineDateTime represents a Luxon DateTime object created by parsing a
* string or number value as a dateTime.
*/
export class VineDateTime extends BaseLiteralType<string | DateTime,
DateTime> {
constructor(options?: FieldOptions, validations?: Validation<any>[]) {
super(options, validations || [dateTimeRule()])
}
/**
* Available VineDate rules
*/
static rules = {
equals: equalsRule,
after: afterRule,
afterOrEqual: afterOrEqualRule,
before: beforeRule,
beforeOrEqual: beforeOrEqualRule,
sameAs: sameAsRule,
notSameAs: notSameAsRule,
afterField: afterFieldRule,
afterOrSameAs: afterOrSameAsRule,
beforeField: beforeFieldRule,
beforeOrSameAs: beforeOrSameAsRule,
weekend: weekendRule,
weekday: weekdayRule,
}
/**
* The equals rule compares the input value to be same
* as the expected value.
*
* By default, the comparions of day, month and years are performed.
*/
equals(
expectedValue: string | ((field: FieldContext) => string),
options?: DateEqualsOptions
): this {
return this.use(equalsRule({ expectedValue, ...options }))
}
/**
* The after rule compares the input value to be after
* the expected value.
*
* By default, the comparions of day, month and years are performed.
*/
after(
expectedValue:
| 'today'
| 'tomorrow'
| (string & { _?: never })
| ((field: FieldContext) => string),
options?: DateEqualsOptions
): this {
return this.use(afterRule({ expectedValue, ...options }))
}
/**
* The after or equal rule compares the input value to be
* after or equal to the expected value.
*
* By default, the comparions of day, month and years are performed.
*/
afterOrEqual(
expectedValue:
| 'today'
| 'tomorrow'
| (string & { _?: never })
| ((field: FieldContext) => string),
options?: DateEqualsOptions
): this {
return this.use(afterOrEqualRule({ expectedValue, ...options }))
}
/**
* The before rule compares the input value to be before
* the expected value.
*
* By default, the comparions of day, month and years are performed.
*/
before(
expectedValue:
| 'today'
| 'yesterday'
| (string & { _?: never })
| ((field: FieldContext) => string),
options?: DateEqualsOptions
): this {
return this.use(beforeRule({ expectedValue, ...options }))
}
/**
* The before rule compares the input value to be before
* the expected value.
*
* By default, the comparions of day, month and years are performed.
*/
beforeOrEqual(
expectedValue:
| 'today'
| 'yesterday'
| (string & { _?: never })
| ((field: FieldContext) => string),
options?: DateEqualsOptions
): this {
return this.use(beforeOrEqualRule({ expectedValue, ...options }))
}
/**
* The sameAs rule expects the input value to be same
* as the value of the other field.
*
* By default, the comparions of day, month and years are performed
*/
sameAs(otherField: string, options?: DateEqualsOptions): this {
return this.use(sameAsRule({ otherField, ...options }))
}
/**
* The notSameAs rule expects the input value to be different
* from the other field's value
*
* By default, the comparions of day, month and years are performed
*/
notSameAs(otherField: string, options?: DateEqualsOptions): this {
return this.use(notSameAsRule({ otherField, ...options }))
}
/**
* The afterField rule expects the input value to be after
* the other field's value.
*
* By default, the comparions of day, month and years are performed
*/
afterField(otherField: string, options?: DateEqualsOptions): this {
return this.use(afterFieldRule({ otherField, ...options }))
}
/**
* The afterOrSameAs rule expects the input value to be after
* or equal to the other field's value.
*
* By default, the comparions of day, month and years are performed
*/
afterOrSameAs(otherField: string, options?: DateEqualsOptions): this {
return this.use(afterOrSameAsRule({ otherField, ...options }))
}
/**
* The beforeField rule expects the input value to be before
* the other field's value.
*
* By default, the comparions of day, month and years are performed
*/
beforeField(otherField: string, options?: DateEqualsOptions): this {
return this.use(beforeFieldRule({ otherField, ...options }))
}
/**
* The beforeOrSameAs rule expects the input value to be before
* or same as the other field's value.
*
* By default, the comparions of day, month and years are performed
*/
beforeOrSameAs(otherField: string, options?: DateEqualsOptions): this {
return this.use(beforeOrSameAsRule({ otherField, ...options }))
}
/**
* The weekend rule ensures the date falls on a weekend
*/
weekend(): this {
return this.use(weekendRule())
}
/**
* The weekday rule ensures the date falls on a weekday
*/
weekday(): this {
return this.use(weekdayRule())
}
/**
* Clones the VineDate schema type. The applied options
* and validations are copied to the new instance
*/
clone(): this {
return new VineDateTime(this.cloneOptions(), this.cloneValidations())
as this
}
}
Make a service provider called vine_provider
import type { ApplicationService } from '@adonisjs/core/types'
import vine, { Vine } from '@vinejs/vine'
import { VineDateTime } from '../app/rules/schemas/vine_datetime.js'
export default class VineProvider {
constructor(protected app: ApplicationService) {}
/**
* Register bindings to the container
*/
register() {}
/**
* The container bindings have booted
*/
async boot() {}
/**
* The application has been booted
*/
async start() {
Vine.macro('dateTime', function () {
return new VineDateTime()
})
}
/**
* The process has been started
*/
async ready() {}
/**
* Preparing to shutdown the app
*/
async shutdown() {}
}
Register to start with app in a vine.ts file in start folder
import vine, { Vine } from '@vinejs/vine'
import { VineDateTime } from '../app/rules/schemas/vine_datetime.js'
Vine.macro('dateTime', function () {
return new VineDateTime()
})
/**
* Informing TypeScript about the newly added method
*/
declare module '@vinejs/vine' {
interface Vine {
dateTime(): VineDateTime
}
}