3

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?

3 Answers 3

2

You can use vine.date({ formats: { utc: true } }).

Then you can pass in the payload :

  • 2024-05-17T00:00:00.000Z
  • 2024-05-17T00:00:00.000+00:00
  • 2024-05-17T02:00:00.000+02:00

and you will always get the same date 2024-05-17T00:00:00.000Z

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks, but I don't want the same date, I need to keep the timezone info.
For the date validity, this is more than enough. Keeping timezone info is something else since timezone IS relative. I've dealt with the same issue and had to explicitly pass the timezone with a request param.
1

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))
  })
)

2 Comments

Thanks, but that only transforms the output. I need to handle different date inputs, e.g. of format "2024-11-18T00:00:00+10:00" That's where the formats: option falls down as it can't handle different formats. Even if I pass in a YYYY-MM etc. format to match my inputs it will fail with "name": "Exception", "status": 500, "code": "E_INVALID_DATE_COLUMN_VALUE"
Ahh, actually my issue was that I needed to add a null check as well. .transform((date) => (date ? DateTime.fromJSDate(date) : null))
0

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
  }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.