From c893f109372dd8cea1dde350b422c92696057483 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 21 Nov 2023 14:53:48 +0530 Subject: [PATCH] feat: add first set of date validation rules --- index.ts | 1 + package.json | 1 + src/defaults.ts | 15 + src/schema/builder.ts | 10 +- src/schema/date/main.ts | 230 ++++ src/schema/date/rules.ts | 549 ++++++++++ src/schema/string/rules.ts | 17 +- src/types.ts | 29 +- src/vine/helpers.ts | 36 +- tests/integration/schema/date.spec.ts | 103 ++ tests/unit/rules/date.spec.ts | 1450 +++++++++++++++++++++++++ tests/unit/schema/date.spec.ts | 842 ++++++++++++++ 12 files changed, 3250 insertions(+), 33 deletions(-) create mode 100644 src/schema/date/main.ts create mode 100644 src/schema/date/rules.ts create mode 100644 tests/integration/schema/date.spec.ts create mode 100644 tests/unit/rules/date.spec.ts create mode 100644 tests/unit/schema/date.spec.ts diff --git a/index.ts b/index.ts index e20459c..b6cd5a4 100644 --- a/index.ts +++ b/index.ts @@ -20,6 +20,7 @@ export { VineArray } from './src/schema/array/main.js' export { VineValidator } from './src/vine/validator.js' export { VineString } from './src/schema/string/main.js' export { VineNumber } from './src/schema/number/main.js' +export { VineDate } from './src/schema/date/main.js' export { VineRecord } from './src/schema/record/main.js' export { VineObject } from './src/schema/object/main.js' export { VineLiteral } from './src/schema/literal/main.js' diff --git a/package.json b/package.json index ff4f3a6..36c1dbc 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@types/validator": "^13.11.6", "@vinejs/compiler": "^2.3.0", "camelcase": "^8.0.0", + "dayjs": "^1.11.10", "dlv": "^1.1.3", "normalize-url": "^8.0.0", "validator": "^13.11.0" diff --git a/src/defaults.ts b/src/defaults.ts index da85bfa..33ec90d 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -77,6 +77,21 @@ export const messages = { 'union': 'Invalid value provided for {{ field }} field', 'unionGroup': 'Invalid value provided for {{ field }} field', 'unionOfTypes': 'Invalid value provided for {{ field }} field', + + 'date': 'The {{ field }} field must be a datetime value', + 'date.equals': 'The {{ field }} field must be a date equal to {{ expectedValue }}', + 'date.after': 'The {{ field }} field must be a date after {{ expectedValue }}', + 'date.before': 'The {{ field }} field must be a date before {{ expectedValue }}', + 'date.afterOrEqual': 'The {{ field }} field must be a date after or equal to {{ expectedValue }}', + 'date.beforeOrEqual': + 'The {{ field }} field must be a date before or equal to {{ expectedValue }}', + + 'date.sameAs': 'The {{ field }} field and {{ otherField }} field must be the same', + 'date.notSameAs': 'The {{ field }} field and {{ otherField }} field must be different', + 'date.afterField': 'The {{ field }} field must be a date after {{ otherField }}', + 'date.afterOrSameAs': 'The {{ field }} field must be a date after or same as {{ otherField }}', + 'date.beforeField': 'The {{ field }} field must be a date before {{ otherField }}', + 'date.beforeOrSameAs': 'The {{ field }} field must be a date before or same as {{ otherField }}', } /** diff --git a/src/schema/builder.ts b/src/schema/builder.ts index 0b63d73..4395866 100644 --- a/src/schema/builder.ts +++ b/src/schema/builder.ts @@ -26,7 +26,8 @@ import { group } from './object/group_builder.js' import { VineNativeEnum } from './enum/native_enum.js' import { VineUnionOfTypes } from './union_of_types/main.js' import { OTYPE, COTYPE, IS_OF_TYPE, UNIQUE_NAME } from '../symbols.js' -import type { EnumLike, FieldContext, SchemaTypes } from '../types.js' +import type { DateFieldOptions, EnumLike, FieldContext, SchemaTypes } from '../types.js' +import { VineDate } from './date/main.js' /** * Schema builder exposes methods to construct a Vine schema. You may @@ -71,6 +72,13 @@ export class SchemaBuilder extends Macroable { return new VineNumber(options) } + /** + * Define a datetime value + */ + date(options?: DateFieldOptions) { + return new VineDate(options) + } + /** * Define a schema type in which the input value * matches the pre-defined value diff --git a/src/schema/date/main.ts b/src/schema/date/main.ts new file mode 100644 index 0000000..2ab83ab --- /dev/null +++ b/src/schema/date/main.ts @@ -0,0 +1,230 @@ +/* + * 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 '../base/literal.js' +import { IS_OF_TYPE, UNIQUE_NAME } from '../../symbols.js' +import { + dateRule, + afterRule, + beforeRule, + sameAsRule, + equalsRule, + notSameAsRule, + afterFieldRule, + beforeFieldRule, + afterOrEqualRule, + afterOrSameAsRule, + beforeOrEqualRule, + beforeOrSameAsRule, + DEFAULT_DATE_FORMATS, +} from './rules.js' +import type { + Validation, + FieldOptions, + FieldContext, + DateFieldOptions, + DateEqualsOptions, +} from '../../types.js' + +/** + * VineDate represents a Date object created by parsing a + * string or number value as a date. + */ +export class VineDate extends BaseLiteralType { + /** + * 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, + }; + + /** + * The property must be implemented for "unionOfTypes" + */ + [UNIQUE_NAME] = 'vine.date'; + + /** + * Checks if the value is of date type. The method must be + * implemented for "unionOfTypes" + */ + [IS_OF_TYPE] = (value: unknown) => { + if (typeof value !== 'string') { + return false + } + + return dayjs(value, this.options.formats || DEFAULT_DATE_FORMATS, true).isValid() + } + + protected declare options: FieldOptions & DateFieldOptions + + constructor(options?: Partial & DateFieldOptions, validations?: Validation[]) { + super(options, validations || [dateRule(options || {})]) + } + + /** + * 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 })) + } + + /** + * Clones the VineDate schema type. The applied options + * and validations are copied to the new instance + */ + clone(): this { + return new VineDate(this.cloneOptions(), this.cloneValidations()) as this + } +} diff --git a/src/schema/date/rules.ts b/src/schema/date/rules.ts new file mode 100644 index 0000000..2a78e01 --- /dev/null +++ b/src/schema/date/rules.ts @@ -0,0 +1,549 @@ +/* + * @vinejs/vine + * + * (c) VineJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import dayjs, { type Dayjs } from 'dayjs' +import isSameOrAfter from 'dayjs/plugin/isSameOrAfter.js' +import isSameOrBefore from 'dayjs/plugin/isSameOrBefore.js' +import customParseFormat from 'dayjs/plugin/customParseFormat.js' + +import { messages } from '../../defaults.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'] + +/** + * Registering plugins + */ +dayjs.extend(customParseFormat) +dayjs.extend(isSameOrAfter) +dayjs.extend(isSameOrBefore) + +/** + * Validates the value to be a string or number formatted + * as per the expected date-time format. + */ +export const dateRule = createRule>((value, options, field) => { + if (typeof value !== 'string') { + field.report(messages.date, 'date', field) + return + } + + const formats = options.formats || DEFAULT_DATE_FORMATS + const dateTime = dayjs(value, formats, true) + + /** + * Ensure post parsing the datetime instance is valid + */ + if (!dateTime.isValid()) { + field.report(messages.date, 'date', field) + return + } + + field.meta.$value = dateTime + field.meta.$formats = formats + field.mutate(dateTime.toDate(), field) +}) + +/** + * 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 + */ +export const equalsRule = createRule< + { + expectedValue: string | ((field: FieldContext) => string) + } & DateEqualsOptions +>((_, options, field) => { + if (!field.meta.$value) { + return + } + + const compare = options.compare || 'day' + const format = options.format || DEFAULT_DATE_FORMATS + const dateTime = field.meta.$value as Dayjs + const expectedValue = + typeof options.expectedValue === 'function' + ? options.expectedValue(field) + : options.expectedValue + + const expectedDateTime = dayjs(expectedValue, format, true) + if (!expectedDateTime.isValid()) { + throw new Error(`Invalid datetime value "${expectedValue}" value provided to the equals rule`) + } + + /** + * Ensure both the dates are the same + */ + if (!dateTime.isSame(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 = 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_FORMATS + const dateTime = field.meta.$value as Dayjs + + const expectedValue = + typeof options.expectedValue === 'function' + ? options.expectedValue(field) + : options.expectedValue + + const expectedDateTime = + expectedValue === 'today' + ? dayjs() + : expectedValue === 'tomorrow' + ? dayjs().add(1, 'day') + : dayjs(expectedValue, format, true) + + if (!expectedDateTime.isValid()) { + throw new Error(`Invalid datetime value "${expectedValue}" value provided to the after rule`) + } + + /** + * Ensure the input is after the expected value + */ + if (!dateTime.isAfter(expectedDateTime, compare)) { + 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 = 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_FORMATS + const dateTime = field.meta.$value as Dayjs + + const expectedValue = + typeof options.expectedValue === 'function' + ? options.expectedValue(field) + : options.expectedValue + + const expectedDateTime = + expectedValue === 'today' + ? dayjs() + : expectedValue === 'tomorrow' + ? dayjs().add(1, 'day') + : dayjs(expectedValue, format, true) + + if (!expectedDateTime.isValid()) { + 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 than the expected value. + */ + if (!dateTime.isSameOrAfter(expectedDateTime, compare)) { + 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 = 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_FORMATS + const dateTime = field.meta.$value as Dayjs + + const expectedValue = + typeof options.expectedValue === 'function' + ? options.expectedValue(field) + : options.expectedValue + + const expectedDateTime = + expectedValue === 'today' + ? dayjs() + : expectedValue === 'yesterday' + ? dayjs().subtract(1, 'day') + : dayjs(expectedValue, format, true) + + if (!expectedDateTime.isValid()) { + throw new Error(`Invalid datetime value "${expectedValue}" value provided to the before rule`) + } + + /** + * Ensure the input is before the expected value + */ + if (!dateTime.isBefore(expectedDateTime, compare)) { + 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 = 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_FORMATS + const dateTime = field.meta.$value as Dayjs + + const expectedValue = + typeof options.expectedValue === 'function' + ? options.expectedValue(field) + : options.expectedValue + + const expectedDateTime = + expectedValue === 'today' + ? dayjs() + : expectedValue === 'yesterday' + ? dayjs().subtract(1, 'day') + : dayjs(expectedValue, format, true) + + if (!expectedDateTime.isValid()) { + 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.isSameOrBefore(expectedDateTime, compare)) { + 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 = createRule< + { + otherField: string + } & DateEqualsOptions +>((_, options, field) => { + if (!field.meta.$value) { + return + } + + const compare = options.compare || 'day' + const dateTime = field.meta.$value as Dayjs + const format = options.format || field.meta.$formats + const expectedValue = helpers.getNestedValue(options.otherField, field) + const expectedDateTime = dayjs(expectedValue, format, true) + + /** + * 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 (!expectedDateTime.isValid()) { + return + } + + /** + * Ensure both the dates are the same + */ + if (!dateTime.isSame(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 = createRule< + { + otherField: string + } & DateEqualsOptions +>((_, options, field) => { + if (!field.meta.$value) { + return + } + + const compare = options.compare || 'day' + const dateTime = field.meta.$value as Dayjs + const format = options.format || field.meta.$formats + const expectedValue = helpers.getNestedValue(options.otherField, field) + const expectedDateTime = dayjs(expectedValue, format, true) + + /** + * 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 (!expectedDateTime.isValid()) { + return + } + + /** + * Ensure both the dates are different + */ + if (dateTime.isSame(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 = createRule< + { + otherField: string + } & DateEqualsOptions +>((_, options, field) => { + if (!field.meta.$value) { + return + } + + const compare = options.compare || 'day' + const dateTime = field.meta.$value as Dayjs + const format = options.format || field.meta.$formats + const expectedValue = helpers.getNestedValue(options.otherField, field) + const expectedDateTime = dayjs(expectedValue, format, true) + + /** + * 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 (!expectedDateTime.isValid()) { + return + } + + /** + * Ensure the input date is after the other field's value + */ + if (!dateTime.isAfter(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 = createRule< + { + otherField: string + } & DateEqualsOptions +>((_, options, field) => { + if (!field.meta.$value) { + return + } + + const compare = options.compare || 'day' + const dateTime = field.meta.$value as Dayjs + const format = options.format || field.meta.$formats + const expectedValue = helpers.getNestedValue(options.otherField, field) + const expectedDateTime = dayjs(expectedValue, format, true) + + /** + * 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 (!expectedDateTime.isValid()) { + return + } + + /** + * Ensure the input date is same as or after the other field's value + */ + if (!dateTime.isSameOrAfter(expectedDateTime, compare)) { + 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 = createRule< + { + otherField: string + } & DateEqualsOptions +>((_, options, field) => { + if (!field.meta.$value) { + return + } + + const compare = options.compare || 'day' + const dateTime = field.meta.$value as Dayjs + const format = options.format || field.meta.$formats + const expectedValue = helpers.getNestedValue(options.otherField, field) + const expectedDateTime = dayjs(expectedValue, format, true) + + /** + * 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 (!expectedDateTime.isValid()) { + return + } + + /** + * Ensure the input date is before the other field's value + */ + if (!dateTime.isBefore(expectedDateTime, compare)) { + 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 = createRule< + { + otherField: string + } & DateEqualsOptions +>((_, options, field) => { + if (!field.meta.$value) { + return + } + + const compare = options.compare || 'day' + const dateTime = field.meta.$value as Dayjs + const format = options.format || field.meta.$formats + const expectedValue = helpers.getNestedValue(options.otherField, field) + const expectedDateTime = dayjs(expectedValue, format, true) + + /** + * 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 (!expectedDateTime.isValid()) { + return + } + + /** + * Ensure the input date is before or same as the other field's value + */ + if (!dateTime.isSameOrBefore(expectedDateTime, compare)) { + field.report(messages['date.beforeOrSameAs'], 'date.beforeOrSameAs', field, { + otherField: options.otherField, + expectedValue, + compare, + }) + } +}) diff --git a/src/schema/string/rules.ts b/src/schema/string/rules.ts index 4480c07..707b11b 100644 --- a/src/schema/string/rules.ts +++ b/src/schema/string/rules.ts @@ -7,7 +7,6 @@ * file that was distributed with this source code. */ -import delve from 'dlv' import camelcase from 'camelcase' import normalizeUrl from 'normalize-url' import escape from 'validator/lib/escape.js' @@ -30,18 +29,6 @@ import type { NormalizeEmailOptions, } from '../../types.js' -/** - * Returns the nested value from the field root - * object or the sibling value from the field - * parent object - */ -function getNestedValue(key: string, field: FieldContext) { - if (key.indexOf('.') > -1) { - return delve(field.data, key) - } - return field.parent[key] -} - /** * Validates the value to be a string */ @@ -293,7 +280,7 @@ export const sameAsRule = createRule<{ otherField: string }>((value, options, fi return } - const input = getNestedValue(options.otherField, field) + const input = helpers.getNestedValue(options.otherField, field) /** * Performing validation and reporting error @@ -315,7 +302,7 @@ export const notSameAsRule = createRule<{ otherField: string }>((value, options, return } - const input = getNestedValue(options.otherField, field) + const input = helpers.getNestedValue(options.otherField, field) /** * Performing validation and reporting error diff --git a/src/types.ts b/src/types.ts index baebdae..1a43a31 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,13 @@ * file that was distributed with this source code. */ +import type dayjs from 'dayjs' +import type { Options as UrlOptions } from 'normalize-url' +import type { IsURLOptions } from 'validator/lib/isURL.js' +import type { IsEmailOptions } from 'validator/lib/isEmail.js' +import type { PostalCodeLocale } from 'validator/lib/isPostalCode.js' +import type { NormalizeEmailOptions } from 'validator/lib/normalizeEmail.js' +import type { IsMobilePhoneOptions, MobilePhoneLocale } from 'validator/lib/isMobilePhone.js' import type { ParseFn, RefsStore, @@ -16,12 +23,6 @@ import type { MessagesProviderContact, ErrorReporterContract as BaseReporter, } from '@vinejs/compiler/types' -import type { Options as UrlOptions } from 'normalize-url' -import type { IsURLOptions } from 'validator/lib/isURL.js' -import type { IsEmailOptions } from 'validator/lib/isEmail.js' -import type { NormalizeEmailOptions } from 'validator/lib/normalizeEmail.js' -import type { IsMobilePhoneOptions, MobilePhoneLocale } from 'validator/lib/isMobilePhone.js' -import type { PostalCodeLocale } from 'validator/lib/isPostalCode.js' import type { helpers } from './vine/helpers.js' import type { ValidationError } from './errors/validation_error.js' @@ -201,6 +202,22 @@ export type FieldOptions = { parse?: Parser } +/** + * A set of options accepted by the date field + */ +export type DateFieldOptions = { + formats?: dayjs.OptionType +} + +/** + * A set of options accepted by the equals rule + * on the date field + */ +export type DateEqualsOptions = { + compare?: dayjs.OpUnitType + format?: dayjs.OptionType +} + /** * Options accepted when compiling schema types. */ diff --git a/src/vine/helpers.ts b/src/vine/helpers.ts index 862041a..9b2a1b4 100644 --- a/src/vine/helpers.ts +++ b/src/vine/helpers.ts @@ -7,29 +7,31 @@ * file that was distributed with this source code. */ -import isEmail from 'validator/lib/isEmail.js' -import isURL from 'validator/lib/isURL.js' -import isAlpha from 'validator/lib/isAlpha.js' -import isAlphanumeric from 'validator/lib/isAlphanumeric.js' +import delve from 'dlv' import isIP from 'validator/lib/isIP.js' +import isJWT from 'validator/lib/isJWT.js' +import isURL from 'validator/lib/isURL.js' +import isSlug from 'validator/lib/isSlug.js' +import isIBAN from 'validator/lib/isIBAN.js' import isUUID from 'validator/lib/isUUID.js' import isAscii from 'validator/lib/isAscii.js' -import isCreditCard from 'validator/lib/isCreditCard.js' -import isIBAN from 'validator/lib/isIBAN.js' -import isJWT from 'validator/lib/isJWT.js' +import isEmail from 'validator/lib/isEmail.js' +import isAlpha from 'validator/lib/isAlpha.js' import isLatLong from 'validator/lib/isLatLong.js' -import isPassportNumber from 'validator/lib/isPassportNumber.js' -import isSlug from 'validator/lib/isSlug.js' import isDecimal from 'validator/lib/isDecimal.js' import isHexColor from 'validator/lib/isHexColor.js' -import isMobilePhone, { type MobilePhoneLocale } from 'validator/lib/isMobilePhone.js' +import { resolve4, resolve6 } from 'node:dns/promises' +import isCreditCard from 'validator/lib/isCreditCard.js' +import isAlphanumeric from 'validator/lib/isAlphanumeric.js' +import isPassportNumber from 'validator/lib/isPassportNumber.js' import isPostalCode, { type PostalCodeLocale } from 'validator/lib/isPostalCode.js' +import isMobilePhone, { type MobilePhoneLocale } from 'validator/lib/isMobilePhone.js' // @ts-ignore type missing from @types/validator import { locales as mobilePhoneLocales } from 'validator/lib/isMobilePhone.js' // @ts-ignore type missing from @types/validator import { locales as postalCodeLocales } from 'validator/lib/isPostalCode.js' -import { resolve4, resolve6 } from 'node:dns/promises' +import type { FieldContext } from '../types.js' const BOOLEAN_POSITIVES = ['1', 1, 'true', true, 'on'] const BOOLEAN_NEGATIVES = ['0', 0, 'false', false] @@ -333,4 +335,16 @@ export const helpers = { return true }, + + /** + * Returns the nested value from the field root + * object or the sibling value from the field + * parent object + */ + getNestedValue(key: string, field: FieldContext) { + if (key.indexOf('.') > -1) { + return delve(field.data, key) + } + return field.parent[key] + }, } diff --git a/tests/integration/schema/date.spec.ts b/tests/integration/schema/date.spec.ts new file mode 100644 index 0000000..76b1c61 --- /dev/null +++ b/tests/integration/schema/date.spec.ts @@ -0,0 +1,103 @@ +/* + * @vinejs/vine + * + * (c) VineJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import vine from '../../../index.js' + +test.group('VineDate', () => { + test('fail when value is not a string formatted as date', async ({ assert }) => { + const schema = vine.object({ + created_at: vine.date(), + }) + + const data = { created_at: 'foo' } + await assert.validationErrors(vine.validate({ schema, data }), [ + { + field: 'created_at', + message: 'The created_at field must be a datetime value', + rule: 'date', + }, + ]) + }) + + test('pass when value is a valid date', async ({ assert }) => { + const schema = vine.object({ + created_at: vine.date(), + }) + + const data = { created_at: '2024-10-01' } + const result = await vine.validate({ schema, data }) + assert.instanceOf(result.created_at, Date) + assert.equal(result.created_at.getDate(), 1) + assert.equal(result.created_at.getMonth() + 1, 10) + assert.equal(result.created_at.getFullYear(), 2024) + assert.equal(result.created_at.getMinutes(), 0) + assert.equal(result.created_at.getHours(), 0) + }) + + test('throw fatal error when invalid value is provided to the comparison rules', async ({ + assert, + }) => { + const data = { created_at: '2024-10-01' } + await assert.rejects( + () => + vine.validate({ + schema: vine.object({ + created_at: vine.date().equals('foo'), + }), + data, + }), + 'Invalid datetime value "foo" value provided to the equals rule' + ) + + await assert.rejects( + () => + vine.validate({ + schema: vine.object({ + created_at: vine.date().after('foo'), + }), + data, + }), + 'Invalid datetime value "foo" value provided to the after rule' + ) + + await assert.rejects( + () => + vine.validate({ + schema: vine.object({ + created_at: vine.date().before('foo'), + }), + data, + }), + 'Invalid datetime value "foo" value provided to the before rule' + ) + + await assert.rejects( + () => + vine.validate({ + schema: vine.object({ + created_at: vine.date().beforeOrEqual('foo'), + }), + data, + }), + 'Invalid datetime value "foo" value provided to the beforeOrEqual rule' + ) + + await assert.rejects( + () => + vine.validate({ + schema: vine.object({ + created_at: vine.date().afterOrEqual('foo'), + }), + data, + }), + 'Invalid datetime value "foo" value provided to the afterOrEqual rule' + ) + }) +}) diff --git a/tests/unit/rules/date.spec.ts b/tests/unit/rules/date.spec.ts new file mode 100644 index 0000000..3a9730d --- /dev/null +++ b/tests/unit/rules/date.spec.ts @@ -0,0 +1,1450 @@ +/* + * @vinejs/vine + * + * (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 { test } from '@japa/runner' +import { validator } from '../../../factories/main.js' +import { + dateRule, + afterRule, + beforeRule, + sameAsRule, + equalsRule, + notSameAsRule, + afterFieldRule, + beforeFieldRule, + afterOrEqualRule, + beforeOrEqualRule, + afterOrSameAsRule, + beforeOrSameAsRule, +} from '../../../src/schema/date/rules.js' + +test.group('Date | date', () => { + test('report when value is not a number or a string', () => { + const date = dateRule({}) + const validated = validator.execute(date, {}) + + validated.assertError('The dummy field must be a datetime value') + }) + + test('report when value is an invalid datetime string', () => { + const date = dateRule({}) + const validated = validator.execute(date, '2020-32-32') + + validated.assertError('The dummy field must be a datetime value') + }) + + test('report when value is an invalid datetime string as per the expected format', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const validated = validator.execute(date, '2024/01/24') + + validated.assertError('The dummy field must be a datetime value') + }) + + test('report when value is a number but expected format is a string', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const validated = validator.execute(date, dayjs().unix()) + + validated.assertError('The dummy field must be a datetime value') + }) + + test('pass validation when value is a valid datetime string', ({ assert }) => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const validated = validator.execute(date, '2024-01-22') + + validated.assertSucceeded() + const output = validated.getOutput() + assert.instanceOf(output, Date) + }) + + test('pass validation when value matches any of the mentioned formats', ({ assert }) => { + const date = dateRule({ formats: ['YYYY-MM-DD', 'YYYY/DD/MM'] }) + const validated = validator.execute(date, '2024-01-22') + validated.assertSucceeded() + const output = validated.getOutput() + assert.instanceOf(output, Date) + assert.equal(dayjs(output).format('YYYY-MM-DD'), '2024-01-22') + + const validated1 = validator.execute(date, '2024/22/01') + validated1.assertSucceeded() + const output1 = validated.getOutput() + assert.instanceOf(output1, Date) + assert.equal(dayjs(output1).format('YYYY-MM-DD'), '2024-01-22') + }) + + test('validate time', ({ assert }) => { + const date = dateRule({ formats: ['HH:mm:ss'] }) + const validated = validator.execute(date, '10:12:32') + + validated.assertSucceeded() + const output = validated.getOutput() + assert.instanceOf(output, Date) + }) +}) + +test.group('Date | equals', () => { + test('skip validation when value is not a valid date', () => { + const date = dateRule({}) + const equals = equalsRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, equals], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when value is not a valid date with bail mode disabled', () => { + const date = dateRule({}) + const equals = equalsRule({ expectedValue: '2024-01-22' }) + const validated = validator.bail(false).execute([date, equals], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('report error when year is not the same', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const equals = equalsRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, equals], '2023-01-22') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date equal to 2024-01-22') + }) + + test('report error when month is not the same', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const equals = equalsRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, equals], '2024-12-22') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date equal to 2024-01-22') + }) + + test('report error when day is not the same', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const equals = equalsRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, equals], '2024-01-01') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date equal to 2024-01-22') + }) + + test('with month comparison pass when day is not the same', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const equals = equalsRule({ expectedValue: '2024-01-22', compare: 'month' }) + const validated = validator.execute([date, equals], '2024-01-01') + + validated.assertSucceeded() + }) + + test('use custom format for expected value', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const equals = equalsRule({ expectedValue: '2024/24/01', format: 'YYYY/DD/MM' }) + const validated = validator.execute([date, equals], '2024-01-24') + + validated.assertSucceeded() + }) + + test('compute comparison value from a callback', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const equals = equalsRule({ + expectedValue: () => '2024/24/01', + compare: 'year', + format: 'YYYY/DD/MM', + }) + const validated = validator.execute([date, equals], '2024-10-10') + + validated.assertSucceeded() + }) +}) + +test.group('Date | after', () => { + test('skip validation when value is not a valid date', () => { + const date = dateRule({}) + const after = afterRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, after], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when value is not a valid date with bail mode disabled', () => { + const date = dateRule({}) + const after = afterRule({ expectedValue: '2024-01-22' }) + const validated = validator.bail(false).execute([date, after], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('report error when year is smaller', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const after = afterRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, after], '2022-01-23') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date after 2024-01-22') + }) + + test('report error when month is smaller', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const after = afterRule({ expectedValue: '2024-02-22' }) + const validated = validator.execute([date, after], '2024-01-23') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date after 2024-02-22') + }) + + test('report error when day is smaller or same', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const after = afterRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, after], '2024-01-22') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date after 2024-01-22') + }) + + test('pass when minutes are in the future', () => { + const date = dateRule({}) + const after = afterRule({ expectedValue: '2024-01-22', compare: 'seconds' }) + const validated = validator.execute([date, after], '2024-01-22 12:00:00') + + validated.assertSucceeded() + }) + + test('pass when day is in the future', () => { + const date = dateRule({}) + const after = afterRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, after], '2024-01-23') + + validated.assertSucceeded() + }) + + test('report error when day is in future but comparing for months', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const after = afterRule({ expectedValue: '2024-01-22', compare: 'month' }) + const validated = validator.execute([date, after], '2024-01-23') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date after 2024-01-22') + }) + + test('use custom format for expected value', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const after = afterRule({ expectedValue: '2024/24/01', format: 'YYYY/DD/MM' }) + const validated = validator.execute([date, after], '2024-01-25') + + validated.assertSucceeded() + }) + + test('compute comparison value from a callback', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const after = afterRule({ + expectedValue: () => '2024/24/01', + compare: 'year', + format: 'YYYY/DD/MM', + }) + const validated = validator.execute([date, after], '2025-01-01') + + validated.assertSucceeded() + }) + + test('use "today" keyword', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const after = afterRule({ expectedValue: 'today' }) + const validated = validator.execute([date, after], dayjs().add(1, 'day').format('YYYY-MM-DD')) + validated.assertSucceeded() + + const validated1 = validator.execute([date, after], dayjs().format('YYYY-MM-DD')) + validated1.assertErrorsCount(1) + validated1.assertError('The dummy field must be a date after today') + }) + + test('use "tomorrow" keyword', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const after = afterRule({ expectedValue: 'tomorrow' }) + const validated = validator.execute([date, after], dayjs().add(2, 'day').format('YYYY-MM-DD')) + validated.assertSucceeded() + + const validated1 = validator.execute([date, after], dayjs().add(1, 'day').format('YYYY-MM-DD')) + validated1.assertErrorsCount(1) + validated1.assertError('The dummy field must be a date after tomorrow') + }) +}) + +test.group('Date | afterOrEqual', () => { + test('skip validation when value is not a valid date', () => { + const date = dateRule({}) + const afterOrEqual = afterOrEqualRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, afterOrEqual], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when value is not a valid date with bail mode disabled', () => { + const date = dateRule({}) + const afterOrEqual = afterOrEqualRule({ expectedValue: '2024-01-22' }) + const validated = validator.bail(false).execute([date, afterOrEqual], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('report error when year is smaller', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterOrEqual = afterOrEqualRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, afterOrEqual], '2022-01-23') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date after or equal to 2024-01-22') + }) + + test('report error when month is smaller', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterOrEqual = afterOrEqualRule({ expectedValue: '2024-02-22' }) + const validated = validator.execute([date, afterOrEqual], '2024-01-23') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date after or equal to 2024-02-22') + }) + + test('report error when day is smaller', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterOrEqual = afterOrEqualRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, afterOrEqual], '2024-01-21') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date after or equal to 2024-01-22') + }) + + test('pass when date is the same', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterOrEqual = afterOrEqualRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, afterOrEqual], '2024-01-22') + + validated.assertSucceeded() + }) + + test('pass when minutes are in the future', () => { + const date = dateRule({}) + const afterOrEqual = afterOrEqualRule({ expectedValue: '2024-01-22', compare: 'seconds' }) + const validated = validator.execute([date, afterOrEqual], '2024-01-22 12:00:00') + + validated.assertSucceeded() + }) + + test('pass when day is in the future', () => { + const date = dateRule({}) + const afterOrEqual = afterOrEqualRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, afterOrEqual], '2024-01-23') + + validated.assertSucceeded() + }) + + test('report error when day is in future but comparing for months', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterOrEqual = afterOrEqualRule({ expectedValue: '2024-02-22', compare: 'month' }) + const validated = validator.execute([date, afterOrEqual], '2024-01-23') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date after or equal to 2024-02-22') + }) + + test('use custom format for expected value', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterOrEqual = afterOrEqualRule({ expectedValue: '2024/24/01', format: 'YYYY/DD/MM' }) + const validated = validator.execute([date, afterOrEqual], '2024-01-25') + + validated.assertSucceeded() + }) + + test('compute comparison value from a callback', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterOrEqual = afterOrEqualRule({ + expectedValue: () => '2024/24/01', + compare: 'year', + format: 'YYYY/DD/MM', + }) + const validated = validator.execute([date, afterOrEqual], '2025-01-01') + + validated.assertSucceeded() + }) + + test('use "today" keyword', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterOrEqual = afterOrEqualRule({ expectedValue: 'today' }) + const validated = validator.execute([date, afterOrEqual], dayjs().format('YYYY-MM-DD')) + validated.assertSucceeded() + + const validated1 = validator.execute( + [date, afterOrEqual], + dayjs().subtract(1, 'day').format('YYYY-MM-DD') + ) + validated1.assertErrorsCount(1) + validated1.assertError('The dummy field must be a date after or equal to today') + }) + + test('use "tomorrow" keyword', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterOrEqual = afterOrEqualRule({ expectedValue: 'tomorrow' }) + const validated = validator.execute( + [date, afterOrEqual], + dayjs().add(1, 'day').format('YYYY-MM-DD') + ) + validated.assertSucceeded() + + const validated1 = validator.execute([date, afterOrEqual], dayjs().format('YYYY-MM-DD')) + validated1.assertErrorsCount(1) + validated1.assertError('The dummy field must be a date after or equal to tomorrow') + }) +}) + +test.group('Date | before', () => { + test('skip validation when value is not a valid date', () => { + const date = dateRule({}) + const before = beforeRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, before], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when value is not a valid date with bail mode disabled', () => { + const date = dateRule({}) + const before = beforeRule({ expectedValue: '2024-01-22' }) + const validated = validator.bail(false).execute([date, before], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('report error when year is greater', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const before = beforeRule({ expectedValue: '2024-02-22' }) + const validated = validator.execute([date, before], '2025-01-21') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date before 2024-02-22') + }) + + test('report error when month is greater', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const before = beforeRule({ expectedValue: '2024-02-22' }) + const validated = validator.execute([date, before], '2024-03-21') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date before 2024-02-22') + }) + + test('report error when day is greater or same', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const before = beforeRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, before], '2024-01-22') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date before 2024-01-22') + }) + + test('pass when minutes are in the past', () => { + const date = dateRule({}) + const before = beforeRule({ expectedValue: '2024-01-22 12:10:00', compare: 'seconds' }) + const validated = validator.execute([date, before], '2024-01-22 12:00:00') + + validated.assertSucceeded() + }) + + test('pass when day is in the past', () => { + const date = dateRule({}) + const before = beforeRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, before], '2024-01-21') + + validated.assertSucceeded() + }) + + test('report error when day is in past but comparing for months', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const before = beforeRule({ expectedValue: '2024-01-22', compare: 'month' }) + const validated = validator.execute([date, before], '2024-01-21') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date before 2024-01-22') + }) + + test('use custom format for expected value', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const before = beforeRule({ expectedValue: '2024/24/01', format: 'YYYY/DD/MM' }) + const validated = validator.execute([date, before], '2024-01-23') + + validated.assertSucceeded() + }) + + test('compute comparison value from a callback', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const before = beforeRule({ + expectedValue: () => '2024/24/01', + compare: 'year', + format: 'YYYY/DD/MM', + }) + const validated = validator.execute([date, before], '2023-01-01') + + validated.assertSucceeded() + }) + + test('use "today" keyword', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const before = beforeRule({ expectedValue: 'today' }) + const validated = validator.execute( + [date, before], + dayjs().subtract(1, 'day').format('YYYY-MM-DD') + ) + validated.assertSucceeded() + + const validated1 = validator.execute([date, before], dayjs().format('YYYY-MM-DD')) + validated1.assertErrorsCount(1) + validated1.assertError('The dummy field must be a date before today') + }) + + test('use "yesterday" keyword', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const before = beforeRule({ expectedValue: 'yesterday' }) + const validated = validator.execute( + [date, before], + dayjs().subtract(2, 'day').format('YYYY-MM-DD') + ) + validated.assertSucceeded() + + const validated1 = validator.execute( + [date, before], + dayjs().subtract(1, 'day').format('YYYY-MM-DD') + ) + validated1.assertErrorsCount(1) + validated1.assertError('The dummy field must be a date before yesterday') + }) +}) + +test.group('Date | beforeOrEqual', () => { + test('skip validation when value is not a valid date', () => { + const date = dateRule({}) + const beforeOrEqual = beforeOrEqualRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, beforeOrEqual], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when value is not a valid date with bail mode disabled', () => { + const date = dateRule({}) + const beforeOrEqual = beforeOrEqualRule({ expectedValue: '2024-01-22' }) + const validated = validator.bail(false).execute([date, beforeOrEqual], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('report error when year is greater', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeOrEqual = beforeOrEqualRule({ expectedValue: '2024-02-22' }) + const validated = validator.execute([date, beforeOrEqual], '2025-01-21') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date before or equal to 2024-02-22') + }) + + test('report error when month is greater', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeOrEqual = beforeOrEqualRule({ expectedValue: '2024-02-22' }) + const validated = validator.execute([date, beforeOrEqual], '2024-03-21') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date before or equal to 2024-02-22') + }) + + test('report error when day is greater', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeOrEqual = beforeOrEqualRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, beforeOrEqual], '2024-01-23') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date before or equal to 2024-01-22') + }) + + test('pass when date is the same', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeOrEqual = beforeOrEqualRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, beforeOrEqual], '2024-01-22') + + validated.assertSucceeded() + }) + + test('pass when minutes are in the past', () => { + const date = dateRule({}) + const beforeOrEqual = beforeOrEqualRule({ + expectedValue: '2024-01-22 12:10:00', + compare: 'seconds', + }) + const validated = validator.execute([date, beforeOrEqual], '2024-01-22 12:00:00') + + validated.assertSucceeded() + }) + + test('pass when day is in the past', () => { + const date = dateRule({}) + const beforeOrEqual = beforeOrEqualRule({ expectedValue: '2024-01-22' }) + const validated = validator.execute([date, beforeOrEqual], '2024-01-21') + + validated.assertSucceeded() + }) + + test('report error when day is in past but comparing for months', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeOrEqual = beforeOrEqualRule({ expectedValue: '2024-01-22', compare: 'month' }) + const validated = validator.execute([date, beforeOrEqual], '2024-02-21') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date before or equal to 2024-01-22') + }) + + test('use custom format for expected value', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeOrEqual = beforeOrEqualRule({ expectedValue: '2024/24/01', format: 'YYYY/DD/MM' }) + const validated = validator.execute([date, beforeOrEqual], '2024-01-23') + + validated.assertSucceeded() + }) + + test('compute comparison value from a callback', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeOrEqual = beforeOrEqualRule({ + expectedValue: () => '2024/24/01', + compare: 'year', + format: 'YYYY/DD/MM', + }) + const validated = validator.execute([date, beforeOrEqual], '2024-01-01') + + validated.assertSucceeded() + }) + + test('use "today" keyword', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeOrEqual = beforeOrEqualRule({ expectedValue: 'today' }) + const validated = validator.execute([date, beforeOrEqual], dayjs().format('YYYY-MM-DD')) + validated.assertSucceeded() + + const validated1 = validator.execute( + [date, beforeOrEqual], + dayjs().add(1, 'day').format('YYYY-MM-DD') + ) + validated1.assertErrorsCount(1) + validated1.assertError('The dummy field must be a date before or equal to today') + }) + + test('use "yesterday" keyword', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeOrEqual = beforeOrEqualRule({ expectedValue: 'yesterday' }) + const validated = validator.execute( + [date, beforeOrEqual], + dayjs().subtract(1, 'day').format('YYYY-MM-DD') + ) + validated.assertSucceeded() + + const validated1 = validator.execute([date, beforeOrEqual], dayjs().format('YYYY-MM-DD')) + validated1.assertErrorsCount(1) + validated1.assertError('The dummy field must be a date before or equal to yesterday') + }) +}) + +test.group('Date | sameAs', () => { + test('skip validation when value is not a valid date', () => { + const date = dateRule({}) + const sameAs = sameAsRule({ otherField: 'checkin_date' }) + const validated = validator.execute([date, sameAs], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when value is not a valid date with bail mode disabled', () => { + const date = dateRule({}) + const sameAs = sameAsRule({ otherField: 'checkin_date' }) + const validated = validator.bail(false).execute([date, sameAs], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when other field is not a valid date', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const sameAs = sameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: 'foo', + }, + }) + .execute([date, sameAs], '2023-01-22') + + validated.assertSucceeded() + }) + + test('report error when year is not the same', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const sameAs = sameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2022-01-22', + }, + }) + .execute([date, sameAs], '2023-01-22') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field and checkin_date field must be the same') + }) + + test('report error when month is not the same', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const sameAs = sameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-02-22', + }, + }) + .execute([date, sameAs], '2023-01-22') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field and checkin_date field must be the same') + }) + + test('report error when day is not the same', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const sameAs = sameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-01-21', + }, + }) + .execute([date, sameAs], '2023-01-22') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field and checkin_date field must be the same') + }) + + test('with month comparison pass when day is not the same', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const sameAs = sameAsRule({ otherField: 'checkin_date', compare: 'month' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-01-21', + }, + }) + .execute([date, sameAs], '2023-01-22') + + validated.assertSucceeded() + }) + + test('infer format from the input format', () => { + const date = dateRule({ formats: ['YYYY/MM/DD'] }) + const sameAs = sameAsRule({ otherField: 'checkin_date', compare: 'month' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023/01/21', + }, + }) + .execute([date, sameAs], '2023/01/22') + + validated.assertSucceeded() + }) + + test('define custom format for the other field', () => { + const date = dateRule({ formats: ['YYYY/MM/DD'] }) + const sameAs = sameAsRule({ + otherField: 'checkin_date', + compare: 'month', + format: ['YYYY-MM-DD'], + }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-01-21', + }, + }) + .execute([date, sameAs], '2023/01/22') + + validated.assertSucceeded() + }) +}) + +test.group('Date | notSameAs', () => { + test('skip validation when value is not a valid date', () => { + const date = dateRule({}) + const notSameAs = notSameAsRule({ otherField: 'checkin_date' }) + const validated = validator.execute([date, notSameAs], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when value is not a valid date with bail mode disabled', () => { + const date = dateRule({}) + const notSameAs = notSameAsRule({ otherField: 'checkin_date' }) + const validated = validator.bail(false).execute([date, notSameAs], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when other field is not a valid date', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const notSameAs = notSameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: 'foo', + }, + }) + .execute([date, notSameAs], '2023-01-22') + + validated.assertSucceeded() + }) + + test('report error when dates are the same', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const notSameAs = notSameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2022-01-22', + }, + }) + .execute([date, notSameAs], '2022-01-22') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field and checkin_date field must be different') + }) + + test('report error when comparing month and date is different', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const notSameAs = notSameAsRule({ otherField: 'checkin_date', compare: 'month' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-02-22', + }, + }) + .execute([date, notSameAs], '2023-02-21') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field and checkin_date field must be different') + }) + + test('report error when comparing year and month is different', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const notSameAs = notSameAsRule({ otherField: 'checkin_date', compare: 'year' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-02-21', + }, + }) + .execute([date, notSameAs], '2023-01-22') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field and checkin_date field must be different') + }) + + test('infer format from the input format', () => { + const date = dateRule({ formats: ['YYYY/MM/DD'] }) + const notSameAs = notSameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023/01/21', + }, + }) + .execute([date, notSameAs], '2023/01/22') + + validated.assertSucceeded() + }) + + test('define custom format for the other field', () => { + const date = dateRule({ formats: ['YYYY/MM/DD'] }) + const notSameAs = notSameAsRule({ + otherField: 'checkin_date', + format: ['YYYY-MM-DD'], + }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-01-21', + }, + }) + .execute([date, notSameAs], '2023/01/22') + + validated.assertSucceeded() + }) +}) + +test.group('Date | afterField', () => { + test('skip validation when value is not a valid date', () => { + const date = dateRule({}) + const afterField = afterFieldRule({ otherField: 'checkin_date' }) + const validated = validator.execute([date, afterField], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when value is not a valid date with bail mode disabled', () => { + const date = dateRule({}) + const afterField = afterFieldRule({ otherField: 'checkin_date' }) + const validated = validator.bail(false).execute([date, afterField], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when other field is not a valid date', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterField = afterFieldRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: 'foo', + }, + }) + .execute([date, afterField], '2023-01-22') + + validated.assertSucceeded() + }) + + test("report error when date is not after the other field's value", () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterField = afterFieldRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2022-01-22', + }, + }) + .execute([date, afterField], '2022-01-21') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date after checkin_date') + }) + + test("report error when month is not after the other field's value", () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterField = afterFieldRule({ otherField: 'checkin_date', compare: 'month' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2022-01-22', + }, + }) + .execute([date, afterField], '2022-01-23') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date after checkin_date') + }) + + test("report error when year is not after the other field's value", () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterField = afterFieldRule({ otherField: 'checkin_date', compare: 'year' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2022-01-22', + }, + }) + .execute([date, afterField], '2022-04-23') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date after checkin_date') + }) + + test('pass when date is in the future', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterField = afterFieldRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-01-21', + }, + }) + .execute([date, afterField], '2023-01-22') + + validated.assertSucceeded() + }) + + test('infer format from the input format', () => { + const date = dateRule({ formats: ['YYYY/MM/DD'] }) + const afterField = afterFieldRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023/01/21', + }, + }) + .execute([date, afterField], '2023/01/22') + + validated.assertSucceeded() + }) + + test('define custom format for the other field', () => { + const date = dateRule({ formats: ['YYYY/MM/DD'] }) + const afterField = afterFieldRule({ otherField: 'checkin_date', format: 'YYYY-MM-DD' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-01-21', + }, + }) + .execute([date, afterField], '2023/01/22') + + validated.assertSucceeded() + }) +}) + +test.group('Date | afterOrSameAs', () => { + test('skip validation when value is not a valid date', () => { + const date = dateRule({}) + const afterOrSameAs = afterOrSameAsRule({ otherField: 'checkin_date' }) + const validated = validator.execute([date, afterOrSameAs], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when value is not a valid date with bail mode disabled', () => { + const date = dateRule({}) + const afterOrSameAs = afterOrSameAsRule({ otherField: 'checkin_date' }) + const validated = validator.bail(false).execute([date, afterOrSameAs], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when other field is not a valid date', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterOrSameAs = afterOrSameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: 'foo', + }, + }) + .execute([date, afterOrSameAs], '2023-01-22') + + validated.assertSucceeded() + }) + + test("report error when date is not after or same as the other field's value", () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterOrSameAs = afterOrSameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2022-01-22', + }, + }) + .execute([date, afterOrSameAs], '2022-01-21') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date after or same as checkin_date') + }) + + test("report error when month is not after or same as the other field's value", () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterOrSameAs = afterOrSameAsRule({ otherField: 'checkin_date', compare: 'month' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2022-02-22', + }, + }) + .execute([date, afterOrSameAs], '2022-01-23') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date after or same as checkin_date') + }) + + test("report error when year is not after or samea as the other field's value", () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterOrSameAs = afterOrSameAsRule({ otherField: 'checkin_date', compare: 'year' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2022-01-22', + }, + }) + .execute([date, afterOrSameAs], '2021-04-23') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date after or same as checkin_date') + }) + + test('pass when date is same as the other fields date', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterOrSameAs = afterOrSameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-01-21', + }, + }) + .execute([date, afterOrSameAs], '2023-01-21') + + validated.assertSucceeded() + }) + + test('pass when date is after the other fields date', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const afterOrSameAs = afterOrSameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-01-21', + }, + }) + .execute([date, afterOrSameAs], '2023-01-22') + + validated.assertSucceeded() + }) + + test('infer format from the input format', () => { + const date = dateRule({ formats: ['YYYY/MM/DD'] }) + const afterOrSameAs = afterOrSameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023/01/21', + }, + }) + .execute([date, afterOrSameAs], '2023/01/22') + + validated.assertSucceeded() + }) + + test('define custom format for the other field', () => { + const date = dateRule({ formats: ['YYYY/MM/DD'] }) + const afterOrSameAs = afterOrSameAsRule({ otherField: 'checkin_date', format: 'YYYY-MM-DD' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-01-21', + }, + }) + .execute([date, afterOrSameAs], '2023/01/22') + + validated.assertSucceeded() + }) +}) + +test.group('Date | beforeField', () => { + test('skip validation when value is not a valid date', () => { + const date = dateRule({}) + const beforeField = beforeFieldRule({ otherField: 'checkin_date' }) + const validated = validator.execute([date, beforeField], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when value is not a valid date with bail mode disabled', () => { + const date = dateRule({}) + const beforeField = beforeFieldRule({ otherField: 'checkin_date' }) + const validated = validator.bail(false).execute([date, beforeField], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when other field is not a valid date', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeField = beforeFieldRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: 'foo', + }, + }) + .execute([date, beforeField], '2023-01-22') + + validated.assertSucceeded() + }) + + test("report error when date is not before the other field's value", () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeField = beforeFieldRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2022-01-22', + }, + }) + .execute([date, beforeField], '2022-01-22') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date before checkin_date') + }) + + test("report error when month is not before the other field's value", () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeField = beforeFieldRule({ otherField: 'checkin_date', compare: 'month' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2022-01-22', + }, + }) + .execute([date, beforeField], '2022-01-21') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date before checkin_date') + }) + + test("report error when year is not after the before field's value", () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeField = beforeFieldRule({ otherField: 'checkin_date', compare: 'year' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2022-01-22', + }, + }) + .execute([date, beforeField], '2022-01-21') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date before checkin_date') + }) + + test('pass when date is in the past', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeField = beforeFieldRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-01-21', + }, + }) + .execute([date, beforeField], '2023-01-20') + + validated.assertSucceeded() + }) + + test('infer format from the input format', () => { + const date = dateRule({ formats: ['YYYY/MM/DD'] }) + const beforeField = beforeFieldRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023/01/21', + }, + }) + .execute([date, beforeField], '2023/01/20') + + validated.assertSucceeded() + }) + + test('define custom format for the other field', () => { + const date = dateRule({ formats: ['YYYY/MM/DD'] }) + const beforeField = beforeFieldRule({ otherField: 'checkin_date', format: 'YYYY-MM-DD' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-01-21', + }, + }) + .execute([date, beforeField], '2023/01/20') + + validated.assertSucceeded() + }) +}) + +test.group('Date | beforeOrSameAs', () => { + test('skip validation when value is not a valid date', () => { + const date = dateRule({}) + const beforeOrSameAs = beforeOrSameAsRule({ otherField: 'checkin_date' }) + const validated = validator.execute([date, beforeOrSameAs], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when value is not a valid date with bail mode disabled', () => { + const date = dateRule({}) + const beforeOrSameAs = beforeOrSameAsRule({ otherField: 'checkin_date' }) + const validated = validator.bail(false).execute([date, beforeOrSameAs], 'foo') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a datetime value') + }) + + test('skip validation when other field is not a valid date', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeOrSameAs = beforeOrSameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: 'foo', + }, + }) + .execute([date, beforeOrSameAs], '2023-01-22') + + validated.assertSucceeded() + }) + + test("report error when date is not before or same as the other field's value", () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeOrSameAs = beforeOrSameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2022-01-22', + }, + }) + .execute([date, beforeOrSameAs], '2022-01-23') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date before or same as checkin_date') + }) + + test("report error when month is not before or same as the other field's value", () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeOrSameAs = beforeOrSameAsRule({ otherField: 'checkin_date', compare: 'month' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2022-01-22', + }, + }) + .execute([date, beforeOrSameAs], '2022-02-21') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date before or same as checkin_date') + }) + + test("report error when year is not before or same the other field's value", () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeOrSameAs = beforeOrSameAsRule({ otherField: 'checkin_date', compare: 'year' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2022-01-22', + }, + }) + .execute([date, beforeOrSameAs], '2023-01-21') + + validated.assertErrorsCount(1) + validated.assertError('The dummy field must be a date before or same as checkin_date') + }) + + test('pass when date is in the past', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeOrSameAs = beforeOrSameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-01-21', + }, + }) + .execute([date, beforeOrSameAs], '2023-01-20') + + validated.assertSucceeded() + }) + + test('pass when date is same as the other fields value', () => { + const date = dateRule({ formats: ['YYYY-MM-DD'] }) + const beforeOrSameAs = beforeOrSameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-01-21', + }, + }) + .execute([date, beforeOrSameAs], '2023-01-21') + + validated.assertSucceeded() + }) + + test('infer format from the input format', () => { + const date = dateRule({ formats: ['YYYY/MM/DD'] }) + const beforeOrSameAs = beforeOrSameAsRule({ otherField: 'checkin_date' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023/01/21', + }, + }) + .execute([date, beforeOrSameAs], '2023/01/20') + + validated.assertSucceeded() + }) + + test('define custom format for the other field', () => { + const date = dateRule({ formats: ['YYYY/MM/DD'] }) + const beforeOrSameAs = beforeOrSameAsRule({ otherField: 'checkin_date', format: 'YYYY-MM-DD' }) + const validated = validator + .withContext({ + data: {}, + parent: { + checkin_date: '2023-01-21', + }, + }) + .execute([date, beforeOrSameAs], '2023/01/20') + + validated.assertSucceeded() + }) +}) diff --git a/tests/unit/schema/date.spec.ts b/tests/unit/schema/date.spec.ts new file mode 100644 index 0000000..f80559d --- /dev/null +++ b/tests/unit/schema/date.spec.ts @@ -0,0 +1,842 @@ +/* + * @vinejs/vine + * + * (c) VineJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { refsBuilder } from '@vinejs/compiler' + +import { Vine } from '../../../src/vine/main.js' +import { IS_OF_TYPE, PARSE } from '../../../src/symbols.js' +import { + afterRule, + beforeRule, + sameAsRule, + equalsRule, + notSameAsRule, + afterFieldRule, + beforeFieldRule, + afterOrEqualRule, + beforeOrEqualRule, + afterOrSameAsRule, + beforeOrSameAsRule, +} from '../../../src/schema/date/rules.js' + +const vine = new Vine() + +test.group('VineDate', () => { + test('create date schema', ({ assert }) => { + const schema = vine.date() + assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: true, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + }) + + test('apply nullable modifier', ({ assert }) => { + const schema = vine.date().nullable() + + assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: true, + isOptional: false, + bail: true, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + }) + + test('apply optional modifier', ({ assert }) => { + const schema = vine.date().optional() + + assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: true, + bail: true, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + }) + + test('disable bail mode', ({ assert }) => { + const schema = vine.date().bail(false) + + assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + }) + + test('apply rules', ({ assert }) => { + const schema = vine.date().bail(false).after('today') + + assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + ], + }) + }) + + test('apply transformer', ({ assert }) => { + const schema = vine.date().transform(() => {}) + assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: true, + parseFnId: undefined, + transformFnId: 'ref://2', + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + }) + + test('apply parser', ({ assert }) => { + const schema = vine.date().parse(() => {}) + assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: true, + parseFnId: 'ref://1', + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + ], + }) + }) + + test('check if value is a date using IS_OF_TYPE method', ({ assert }) => { + const schema = vine.date() + + assert.isTrue(schema[IS_OF_TYPE]('2024-01-22')) + assert.isTrue(schema[IS_OF_TYPE]('2024-01-22 23:00:00')) + assert.isFalse(schema[IS_OF_TYPE]('foo')) + assert.isFalse(schema[IS_OF_TYPE](undefined)) + assert.isFalse(schema[IS_OF_TYPE](null)) + assert.isFalse(schema[IS_OF_TYPE]({})) + }) +}) + +test.group('VineDate | clone', () => { + test('clone date schema', ({ assert }) => { + const schema = vine.date() + const schema1 = schema.clone() + + assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: true, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + assert.deepEqual(schema1[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: true, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + }) + + test('clone and apply nullable modifier', ({ assert }) => { + const schema = vine.date() + const schema1 = schema.clone().nullable() + + assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: true, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + assert.deepEqual(schema1[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: true, + isOptional: false, + bail: true, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + }) + + test('clone and apply optional modifier', ({ assert }) => { + const schema = vine.date() + const schema1 = schema.clone().optional() + + assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: true, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + assert.deepEqual(schema1[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: true, + bail: true, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + }) + + test('clone and disable bail mode', ({ assert }) => { + const schema = vine.date() + const schema1 = schema.clone().bail(false) + + assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: true, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + assert.deepEqual(schema1[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + }) + + test('clone and apply rules', ({ assert }) => { + const schema = vine.date().bail(false).after('today') + const schema1 = schema.clone().afterOrEqual('today') + + assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + ], + }) + assert.deepEqual(schema1[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://3', + }, + ], + }) + }) + + test('clone and apply transformer', ({ assert }) => { + const schema = vine.date() + const schema1 = schema.clone().transform(() => {}) + + assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: true, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + + assert.deepEqual(schema1[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: true, + parseFnId: undefined, + transformFnId: 'ref://2', + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + }) + + test('clone and apply parser', ({ assert }) => { + const schema = vine.date() + const schema1 = schema.clone().parse(() => {}) + + assert.deepEqual(schema[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: true, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + ], + }) + assert.deepEqual(schema1[PARSE]('*', refsBuilder(), { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + allowNull: false, + isOptional: false, + bail: true, + parseFnId: 'ref://1', + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + ], + }) + }) +}) + +test.group('VineDate | applying rules', () => { + test('apply equals rule', ({ assert }) => { + const refs = refsBuilder() + const schema = vine.date().equals('2024-10-20') + + assert.deepEqual(schema[PARSE]('*', refs, { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + bail: true, + allowNull: false, + isOptional: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + ], + }) + + const equals = equalsRule({ expectedValue: '2024-10-20' }) + assert.deepEqual(refs.toJSON()['ref://2'], { + validator: equals.rule.validator, + options: equals.options, + }) + }) + + test('apply after rule', ({ assert }) => { + const refs = refsBuilder() + const schema = vine.date().after('today') + + assert.deepEqual(schema[PARSE]('*', refs, { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + bail: true, + allowNull: false, + isOptional: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + ], + }) + + const after = afterRule({ expectedValue: 'today' }) + assert.deepEqual(refs.toJSON()['ref://2'], { + validator: after.rule.validator, + options: after.options, + }) + }) + + test('apply before rule', ({ assert }) => { + const refs = refsBuilder() + const schema = vine.date().before('today') + + assert.deepEqual(schema[PARSE]('*', refs, { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + bail: true, + allowNull: false, + isOptional: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + ], + }) + + const before = beforeRule({ expectedValue: 'today' }) + assert.deepEqual(refs.toJSON()['ref://2'], { + validator: before.rule.validator, + options: before.options, + }) + }) + + test('apply afterOrEqual rule', ({ assert }) => { + const refs = refsBuilder() + const schema = vine.date().afterOrEqual('2024-01-10') + + assert.deepEqual(schema[PARSE]('*', refs, { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + bail: true, + allowNull: false, + isOptional: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + ], + }) + + const afterOrEqual = afterOrEqualRule({ expectedValue: '2024-01-10' }) + assert.deepEqual(refs.toJSON()['ref://2'], { + validator: afterOrEqual.rule.validator, + options: afterOrEqual.options, + }) + }) + + test('apply beforeOrEqual rule', ({ assert }) => { + const refs = refsBuilder() + const schema = vine.date().beforeOrEqual('2024-01-10') + + assert.deepEqual(schema[PARSE]('*', refs, { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + bail: true, + allowNull: false, + isOptional: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + ], + }) + + const beforeOrEqual = beforeOrEqualRule({ expectedValue: '2024-01-10' }) + assert.deepEqual(refs.toJSON()['ref://2'], { + validator: beforeOrEqual.rule.validator, + options: beforeOrEqual.options, + }) + }) + + test('apply sameAs rule', ({ assert }) => { + const refs = refsBuilder() + const schema = vine.date().sameAs('checkin_date') + + assert.deepEqual(schema[PARSE]('*', refs, { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + bail: true, + allowNull: false, + isOptional: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + ], + }) + + const sameAs = sameAsRule({ otherField: 'checkin_date' }) + assert.deepEqual(refs.toJSON()['ref://2'], { + validator: sameAs.rule.validator, + options: sameAs.options, + }) + }) + + test('apply notSameAs rule', ({ assert }) => { + const refs = refsBuilder() + const schema = vine.date().notSameAs('checkin_date') + + assert.deepEqual(schema[PARSE]('*', refs, { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + bail: true, + allowNull: false, + isOptional: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + ], + }) + + const notSameAs = notSameAsRule({ otherField: 'checkin_date' }) + assert.deepEqual(refs.toJSON()['ref://2'], { + validator: notSameAs.rule.validator, + options: notSameAs.options, + }) + }) + + test('apply afterField rule', ({ assert }) => { + const refs = refsBuilder() + const schema = vine.date().afterField('checkin_date') + + assert.deepEqual(schema[PARSE]('*', refs, { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + bail: true, + allowNull: false, + isOptional: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + ], + }) + + const afterField = afterFieldRule({ otherField: 'checkin_date' }) + assert.deepEqual(refs.toJSON()['ref://2'], { + validator: afterField.rule.validator, + options: afterField.options, + }) + }) + + test('apply afterOrSameAs rule', ({ assert }) => { + const refs = refsBuilder() + const schema = vine.date().afterOrSameAs('checkin_date') + + assert.deepEqual(schema[PARSE]('*', refs, { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + bail: true, + allowNull: false, + isOptional: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + ], + }) + + const afterOrSameAs = afterOrSameAsRule({ otherField: 'checkin_date' }) + assert.deepEqual(refs.toJSON()['ref://2'], { + validator: afterOrSameAs.rule.validator, + options: afterOrSameAs.options, + }) + }) + + test('apply beforeField rule', ({ assert }) => { + const refs = refsBuilder() + const schema = vine.date().beforeField('checkin_date') + + assert.deepEqual(schema[PARSE]('*', refs, { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + bail: true, + allowNull: false, + isOptional: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + ], + }) + + const beforeField = beforeFieldRule({ otherField: 'checkin_date' }) + assert.deepEqual(refs.toJSON()['ref://2'], { + validator: beforeField.rule.validator, + options: beforeField.options, + }) + }) + + test('apply beforeOrSameAs rule', ({ assert }) => { + const refs = refsBuilder() + const schema = vine.date().beforeOrSameAs('checkin_date') + + assert.deepEqual(schema[PARSE]('*', refs, { toCamelCase: false }), { + type: 'literal', + fieldName: '*', + propertyName: '*', + bail: true, + allowNull: false, + isOptional: false, + parseFnId: undefined, + validations: [ + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://1', + }, + { + implicit: false, + isAsync: false, + ruleFnId: 'ref://2', + }, + ], + }) + + const beforeOrSameAs = beforeOrSameAsRule({ otherField: 'checkin_date' }) + assert.deepEqual(refs.toJSON()['ref://2'], { + validator: beforeOrSameAs.rule.validator, + options: beforeOrSameAs.options, + }) + }) +})