Skip to content

Commit

Permalink
fix: unix timetamp validation with x format
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Jan 29, 2024
1 parent 9dd9d85 commit bcebea5
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 4 deletions.
34 changes: 30 additions & 4 deletions src/schema/date/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import isSameOrBefore from 'dayjs/plugin/isSameOrBefore.js'
import customParseFormat from 'dayjs/plugin/customParseFormat.js'

import { messages } from '../../defaults.js'
import { helpers } from '../../vine/helpers.js'
import { createRule } from '../../vine/create_rule.js'
import type { DateEqualsOptions, DateFieldOptions, FieldContext } from '../../types.js'
import { helpers } from '../../vine/helpers.js'

export const DEFAULT_DATE_FORMATS = ['YYYY-MM-DD', 'YYYY-MM-DD HH:mm:ss']

Expand All @@ -31,13 +31,39 @@ dayjs.extend(isSameOrBefore)
* as per the expected date-time format.
*/
export const dateRule = createRule<Partial<DateFieldOptions>>((value, options, field) => {
if (typeof value !== 'string') {
if (typeof value !== 'string' && typeof value !== 'number') {
field.report(messages.date, 'date', field)
return
}

const formats = options.formats || DEFAULT_DATE_FORMATS
const dateTime = dayjs(value, formats, true)
let isTimestampAllowed = false
let formats: DateEqualsOptions['format'] = options.formats || DEFAULT_DATE_FORMATS

/**
* DayJS mutates the formats property under the hood. There
* we have to create a shallow clone before passing formats.
*
* https://github.com/iamkun/dayjs/issues/2136
*/
if (Array.isArray(formats)) {
formats = [...formats]
isTimestampAllowed = formats.includes('x')
} else if (typeof formats !== 'string') {
formats = { ...formats }
isTimestampAllowed = formats.format === 'x'
}

const valueAsNumber = isTimestampAllowed ? helpers.asNumber(value) : value

/**
* The timestamp validation does not work with formats array
* when using "customFormatsPlugin". Therefore we have
* to create dayjs instance without formats option
*/
const dateTime =
isTimestampAllowed && !Number.isNaN(valueAsNumber)
? dayjs(valueAsNumber)
: dayjs(value, formats, true)

/**
* Ensure post parsing the datetime instance is valid
Expand Down
68 changes: 68 additions & 0 deletions tests/integration/schema/date.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import { test } from '@japa/runner'
import vine from '../../../index.js'
import dayjs from 'dayjs'

test.group('VineDate', () => {
test('fail when value is not a string formatted as date', async ({ assert }) => {
Expand Down Expand Up @@ -41,6 +42,73 @@ test.group('VineDate', () => {
assert.equal(result.created_at.getHours(), 0)
})

test('pass when value is a valid timestamp', async ({ assert }) => {
const schema = vine.object({
created_at: vine.date({ formats: ['x'] }),
})

const data = { created_at: new Date().getTime() }
const result = await vine.validate({ schema, data })
assert.instanceOf(result.created_at, Date)
assert.equal(result.created_at.getDate(), new Date().getDate())
assert.equal(result.created_at.getMonth(), new Date().getMonth())
assert.equal(result.created_at.getFullYear(), new Date().getFullYear())
})

test('pass when value is a string representation of a timestamp', async ({ assert }) => {
const schema = vine.object({
created_at: vine.date({ formats: ['x'] }),
})

const data = { created_at: new Date().getTime().toString() }
const result = await vine.validate({ schema, data })
assert.instanceOf(result.created_at, Date)
assert.equal(result.created_at.getDate(), new Date().getDate())
assert.equal(result.created_at.getMonth(), new Date().getMonth())
assert.equal(result.created_at.getFullYear(), new Date().getFullYear())
})

test('do not allow timestamp when "x" format is not allowed', async ({ assert }) => {
const schema = vine.object({
created_at: vine.date(),
})

const data = { created_at: new Date().getTime().toString() }
await assert.validationErrors(vine.validate({ schema, data }), [
{
field: 'created_at',
message: 'The created_at field must be a datetime value',
rule: 'date',
},
])
})

test('allow other formats when timestamps are allowed', async ({ assert }) => {
const schema = vine.object({
created_at: vine.date({ formats: ['x', 'YYYY-MM-DD'] }),
})

const data = { created_at: dayjs().format('YYYY-MM-DD') }
const result = await vine.validate({ schema, data })
assert.instanceOf(result.created_at, Date)
assert.equal(result.created_at.getDate(), new Date().getDate())
assert.equal(result.created_at.getMonth(), new Date().getMonth())
assert.equal(result.created_at.getFullYear(), new Date().getFullYear())
})

test('pass format options as an object', async ({ assert }) => {
const schema = vine.object({
created_at: vine.date({ formats: { utc: true } }),
})

const data = { created_at: new Date().toUTCString() }
const result = await vine.validate({ schema, data })
assert.instanceOf(result.created_at, Date)
assert.equal(result.created_at.getDate(), new Date().getDate())
assert.equal(result.created_at.getMonth(), new Date().getMonth())
assert.equal(result.created_at.getFullYear(), new Date().getFullYear())
})

test('throw fatal error when invalid value is provided to the comparison rules', async ({
assert,
}) => {
Expand Down

0 comments on commit bcebea5

Please sign in to comment.