diff --git a/src/schema/base/literal.ts b/src/schema/base/literal.ts index d43db59..ff955e3 100644 --- a/src/schema/base/literal.ts +++ b/src/schema/base/literal.ts @@ -21,6 +21,9 @@ import type { FieldOptions, ParserOptions, ConstructableSchema, + ComparisonOperators, + ArrayComparisonOperators, + NumericComparisonOperators, } from '../../types.js' import { requiredWhen } from './rules.js' import { helpers } from '../../vine/helpers.js' @@ -177,8 +180,68 @@ class OptionalModifier> extends BaseM * field as required, or "false" to skip the required * validation */ - requiredWhen(callback: (field: FieldContext) => boolean) { - return this.use(requiredWhen(callback)) + requiredWhen( + otherField: string, + operator: Operator, + expectedValue: Operator extends ArrayComparisonOperators + ? (string | number | boolean)[] + : Operator extends NumericComparisonOperators + ? number + : string | number | boolean + ): this + requiredWhen(callback: (field: FieldContext) => boolean): this + requiredWhen( + otherField: string | ((field: FieldContext) => boolean), + operator?: ComparisonOperators, + expectedValue?: any + ) { + /** + * The equality check if self implemented + */ + if (typeof otherField === 'function') { + return this.use(requiredWhen(otherField)) + } + + /** + * Creating the checker function based upon the + * operator used for the comparison + */ + let checker: (value: any) => boolean + switch (operator!) { + case '=': + checker = (value) => value === expectedValue + break + case '!=': + checker = (value) => value !== expectedValue + break + case 'in': + checker = (value) => expectedValue.includes(value) + break + case 'notIn': + checker = (value) => !expectedValue.includes(value) + break + case '>': + checker = (value) => value > expectedValue + break + case '<': + checker = (value) => value < expectedValue + break + case '>=': + checker = (value) => value >= expectedValue + break + case '<=': + checker = (value) => value <= expectedValue + } + + /** + * Registering rule with custom implementation + */ + return this.use( + requiredWhen((field) => { + const otherFieldValue = helpers.getNestedValue(otherField, field) + return checker(otherFieldValue) + }) + ) } /** diff --git a/src/types.ts b/src/types.ts index 1a43a31..4a8f924 100644 --- a/src/types.ts +++ b/src/types.ts @@ -270,3 +270,11 @@ export type ValidationOptions | undefined> * Infers the schema type */ export type Infer = Schema[typeof OTYPE] + +/** + * Comparison operators supported by requiredWhen + * rule + */ +export type NumericComparisonOperators = '>' | '<' | '>=' | '<=' +export type ArrayComparisonOperators = 'in' | 'notIn' +export type ComparisonOperators = ArrayComparisonOperators | NumericComparisonOperators | '=' | '!=' diff --git a/tests/integration/schema/conditional_required.spec.ts b/tests/integration/schema/conditional_required.spec.ts index 84b59ef..a331754 100644 --- a/tests/integration/schema/conditional_required.spec.ts +++ b/tests/integration/schema/conditional_required.spec.ts @@ -9,7 +9,6 @@ import { test } from '@japa/runner' import vine from '../../../index.js' -import { helpers } from '../../../src/vine/helpers.js' test.group('requiredIfExists', () => { test('fail when value is missing but other field exists', async ({ assert }) => { @@ -260,16 +259,11 @@ test.group('requiredIfMissingAny', () => { }) }) -test.group('requiredWhen | optional', () => { +test.group('requiredWhen', () => { test('fail when required field is missing', async ({ assert }) => { const schema = vine.object({ game: vine.string().optional(), - teamName: vine - .string() - .optional() - .requiredWhen((field) => { - return helpers.exists(field.data.game) && field.data.game === 'volleyball' - }), + teamName: vine.string().optional().requiredWhen('game', '=', 'volleyball'), }) const data = { @@ -285,15 +279,25 @@ test.group('requiredWhen | optional', () => { ]) }) + test('pass when required condition has not been met', async ({ assert }) => { + const schema = vine.object({ + game: vine.string().optional(), + teamName: vine.string().optional().requiredWhen('game', '=', 'volleyball'), + }) + + const data = { + game: 'handball', + } + + await assert.validationOutput(vine.validate({ schema, data }), { + game: 'handball', + }) + }) + test('pass when required field is defined', async ({ assert }) => { const schema = vine.object({ game: vine.string().optional(), - teamName: vine - .string() - .optional() - .requiredWhen((field) => { - return helpers.exists(field.data.game) && field.data.game === 'volleyball' - }), + teamName: vine.string().optional().requiredWhen('game', '=', 'volleyball'), }) const data = { @@ -306,4 +310,161 @@ test.group('requiredWhen | optional', () => { teamName: 'foo', }) }) + + test('compare using "not equal" operator', async ({ assert }) => { + const schema = vine.object({ + game: vine.string().optional(), + teamName: vine.string().optional().requiredWhen('game', '!=', 'volleyball'), + }) + + const data = { + game: 'handball', + } + + await assert.validationErrors(vine.validate({ schema, data }), [ + { + field: 'teamName', + message: 'The teamName field must be defined', + rule: 'required', + }, + ]) + }) + + test('compare using "in" operator', async ({ assert }) => { + const schema = vine.object({ + game: vine.string().optional(), + teamName: vine.string().optional().requiredWhen('game', 'in', ['volleyball']), + }) + + const data = { + game: 'volleyball', + } + + await assert.validationErrors(vine.validate({ schema, data }), [ + { + field: 'teamName', + message: 'The teamName field must be defined', + rule: 'required', + }, + ]) + }) + + test('compare using "not In" operator', async ({ assert }) => { + const schema = vine.object({ + game: vine.string().optional(), + teamName: vine.string().optional().requiredWhen('game', 'notIn', ['volleyball']), + }) + + const data = { + game: 'handball', + } + + await assert.validationErrors(vine.validate({ schema, data }), [ + { + field: 'teamName', + message: 'The teamName field must be defined', + rule: 'required', + }, + ]) + }) + + test('compare using ">" operator', async ({ assert }) => { + const schema = vine.object({ + age: vine.number(), + guardianName: vine.string().optional().requiredWhen('age', '>', 1), + }) + + const data = { + age: 2, + } + + await assert.validationErrors(vine.validate({ schema, data }), [ + { + field: 'guardianName', + message: 'The guardianName field must be defined', + rule: 'required', + }, + ]) + }) + + test('compare using "<" operator', async ({ assert }) => { + const schema = vine.object({ + age: vine.number(), + guardianName: vine.string().optional().requiredWhen('age', '<', 19), + }) + + const data = { + age: 2, + } + + await assert.validationErrors(vine.validate({ schema, data }), [ + { + field: 'guardianName', + message: 'The guardianName field must be defined', + rule: 'required', + }, + ]) + }) + + test('compare using "<=" operator', async ({ assert }) => { + const schema = vine.object({ + age: vine.number(), + guardianName: vine.string().optional().requiredWhen('age', '<=', 18), + }) + + const data = { + age: 18, + } + + await assert.validationErrors(vine.validate({ schema, data }), [ + { + field: 'guardianName', + message: 'The guardianName field must be defined', + rule: 'required', + }, + ]) + }) + + test('compare using ">=" operator', async ({ assert }) => { + const schema = vine.object({ + age: vine.number(), + guardianName: vine.string().optional().requiredWhen('age', '>=', 1), + }) + + const data = { + age: 1, + } + + await assert.validationErrors(vine.validate({ schema, data }), [ + { + field: 'guardianName', + message: 'The guardianName field must be defined', + rule: 'required', + }, + ]) + }) + + test('compare using custom callback', async ({ assert }) => { + const schema = vine.object({ + game: vine.string().optional(), + teamName: vine + .string() + .optional() + .requiredWhen((field) => { + return field.parent.game === 'volleyball' + }), + }) + + const data = { + game: 'volleyball', + } + + await assert.validationErrors(vine.validate({ schema, data }), [ + { + field: 'teamName', + message: 'The teamName field must be defined', + rule: 'required', + }, + ]) + }) }) diff --git a/tsconfig.json b/tsconfig.json index 9d417c2..ad0cc44 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,6 @@ "extends": "@adonisjs/tsconfig/tsconfig.package.json", "compilerOptions": { "rootDir": "./", - "outDir": "./build", - }, + "outDir": "./build" + } }