From 0db039ad6c4b110dbef77c703e62d293a56aaeab Mon Sep 17 00:00:00 2001 From: jlenon7 Date: Mon, 30 Dec 2024 12:15:17 -0300 Subject: [PATCH 1/2] feat(validator): add custom exists rule --- src/index.ts | 9 +- src/providers/ValidatorProvider.ts | 85 +----- src/types/ExtendReturnType.ts | 35 +++ src/types/index.ts | 3 + src/types/validators/ExistsOptions.ts | 46 +++ src/types/validators/UniqueOptions.ts | 46 +++ src/validator/BaseValidator.ts | 3 +- src/validator/CustomValidations.ts | 115 ++++++++ src/validator/ValidatorImpl.ts | 101 ++++--- templates/validator-console.edge | 8 +- templates/validator-http.edge | 8 +- tests/unit/providers/ValidatorProviderTest.ts | 24 ++ tests/unit/validator/CustomValidationsTest.ts | 267 ++++++++++++++++++ tests/unit/validator/ValidatorImplTest.ts | 59 +++- 14 files changed, 663 insertions(+), 146 deletions(-) create mode 100644 src/types/ExtendReturnType.ts create mode 100644 src/types/validators/ExistsOptions.ts create mode 100644 src/types/validators/UniqueOptions.ts create mode 100644 src/validator/CustomValidations.ts create mode 100644 tests/unit/validator/CustomValidationsTest.ts diff --git a/src/index.ts b/src/index.ts index dd97666..faf5617 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,8 @@ import vine, { VineLiteral, VineBoolean, VineAccepted, - SimpleErrorReporter + SimpleErrorReporter, + SimpleMessagesProvider } from '@vinejs/vine' export * from '#src/types' @@ -32,7 +33,10 @@ export * from '#src/validator/ValidatorImpl' export * from '#src/providers/ValidatorProvider' export * from '#src/exceptions/ValidationException' +const v = vine + export { + v, vine, Vine, VineAny, @@ -47,5 +51,6 @@ export { VineLiteral, VineBoolean, VineAccepted, - SimpleErrorReporter + SimpleErrorReporter, + SimpleMessagesProvider } diff --git a/src/providers/ValidatorProvider.ts b/src/providers/ValidatorProvider.ts index 5b7d3df..5f1cdbc 100644 --- a/src/providers/ValidatorProvider.ts +++ b/src/providers/ValidatorProvider.ts @@ -9,53 +9,18 @@ import { sep } from 'node:path' import { Config } from '@athenna/config' -import { Validate, SimpleErrorReporter } from '#src' -import { Is, Exec, Module, Path } from '@athenna/common' +import { SimpleErrorReporter } from '#src' +import { Exec, Module, Path } from '@athenna/common' import { Annotation, ServiceProvider } from '@athenna/ioc' import { ValidatorImpl } from '#src/validator/ValidatorImpl' +import type { UniqueOptions, ExistsOptions } from '#src/types' +import { CustomValidations } from '#src/validator/CustomValidations' import { ValidationException } from '#src/exceptions/ValidationException' -type UniqueOptions = { - /** - * The table where the database will lookup for the data. - */ - table: string - - /** - * The column name in database. If not defined, the name - * of the field in the schema will be used. - * - * @default 'fieldNameInYourSchema' - */ - column?: string - - /** - * Use the max field to stablish a max limit for your validation. - * In some cases in your database you might have a max of 10 tuples - * with the same data. Use this option to validate that the number - * of fields registered in database cannot be bigger than the number - * defined on this option. - * - * @example - * ```ts - * const schema = this.validator.object({ - * name: this.validator.string().unique({ table: 'users', max: 10 }) - * }) - * - * const data = { name: 'lenon' } - * - * // Will throw if there are 10 users with name `lenon` - * // created in database - * await this.validator.validate({ schema: this.schema, data }) - * ``` - * @default undefined - */ - max?: number -} - declare module '@vinejs/vine' { interface VineString { unique(options: UniqueOptions): this + exists(options: ExistsOptions): this } } @@ -81,44 +46,8 @@ export class ValidatorProvider extends ServiceProvider { return } - const DB = ioc.safeUse('Athenna/Core/Database') - - Validate.extend().string('unique', async (value, options, field) => { - /** - * We do not want to deal with non-string - * values. The "string" rule will handle the - * the validation. - */ - if (!Is.String(value)) { - return - } - - if (!options.column) { - options.column = field.name as string - } - - if (options.max) { - const rows = await DB.table(options.table) - .select(options.column) - .where(options.column, value) - .findMany() - - if (rows.length > options.max) { - field.report('The {{ field }} field is not unique', 'unique', field) - } - - return - } - - const existsRow = await DB.table(options.table) - .select(options.column) - .where(options.column, value) - .exists() - - if (existsRow) { - field.report('The {{ field }} field is not unique', 'unique', field) - } - }) + CustomValidations.registerUnique() + CustomValidations.registerExists() } public async registerValidators() { diff --git a/src/types/ExtendReturnType.ts b/src/types/ExtendReturnType.ts new file mode 100644 index 0000000..b87a811 --- /dev/null +++ b/src/types/ExtendReturnType.ts @@ -0,0 +1,35 @@ +import type { ExtendHandlerType } from '#src/types' +import type { ValidatorImpl } from '#src/validator/ValidatorImpl' + +export type ExtendReturnType = { + /** + * Extend error messages of all your validations. This method + * doesn't save past extends, which means that if you call + * it twice, only the second one will be respected. + * + * ```ts + * Validate.extend().messages({ + * // Applicable for all fields + * 'required': 'The {{ field }} field is required', + * 'string': 'The value of {{ field }} field must be a string', + * 'email': 'The value is not a valid email address', + * + * // Error message only for the username field + * 'username.required': 'Please choose a username for your account' + * }) + * ``` + */ + messages: (messages: Record) => ValidatorImpl + accepted: (name: string, handler: ExtendHandlerType) => ValidatorImpl + date: (name: string, handler: ExtendHandlerType) => ValidatorImpl + record: (name: string, handler: ExtendHandlerType) => ValidatorImpl + tuple: (name: string, handler: ExtendHandlerType) => ValidatorImpl + literal: (name: string, handler: ExtendHandlerType) => ValidatorImpl + array: (name: string, handler: ExtendHandlerType) => ValidatorImpl + any: (name: string, handler: ExtendHandlerType) => ValidatorImpl + string: (name: string, handler: ExtendHandlerType) => ValidatorImpl + number: (name: string, handler: ExtendHandlerType) => ValidatorImpl + enum: (name: string, handler: ExtendHandlerType) => ValidatorImpl + boolean: (name: string, handler: ExtendHandlerType) => ValidatorImpl + object: (name: string, handler: ExtendHandlerType) => ValidatorImpl +} diff --git a/src/types/index.ts b/src/types/index.ts index c41b604..2cdd6a8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -14,4 +14,7 @@ import type { } from '@vinejs/vine/types' export * from '#src/types/ExtendHandler' +export * from '#src/types/ExtendReturnType' +export * from '#src/types/validators/ExistsOptions' +export * from '#src/types/validators/UniqueOptions' export type { FieldContext, SchemaTypes, ErrorReporterContract } diff --git a/src/types/validators/ExistsOptions.ts b/src/types/validators/ExistsOptions.ts new file mode 100644 index 0000000..dc0de96 --- /dev/null +++ b/src/types/validators/ExistsOptions.ts @@ -0,0 +1,46 @@ +/** + * @athenna/validator + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export type ExistsOptions = { + /** + * The table where the database will lookup for the data. + */ + table: string + + /** + * The column name in database. If not defined, the name + * of the field in the schema will be used. + * + * @default 'fieldNameInYourSchema' + */ + column?: string + + /** + * Use the min field to stablish a min limit for your validation. + * In some cases in your database you might have a min of 10 tuples + * with the same data. Use this option to validate that the number + * of fields registered in database needs to be the same or bigger + * than the number defined on this option. + * + * @example + * ```ts + * const schema = this.validator.object({ + * name: this.validator.string().exists({ table: 'users', min: 10 }) + * }) + * + * const data = { name: 'lenon' } + * + * // Will throw if there aren't 10 users with name `lenon` + * // created in database + * await this.validator.validate({ schema: this.schema, data }) + * ``` + * @default undefined + */ + min?: number +} diff --git a/src/types/validators/UniqueOptions.ts b/src/types/validators/UniqueOptions.ts new file mode 100644 index 0000000..d3fab69 --- /dev/null +++ b/src/types/validators/UniqueOptions.ts @@ -0,0 +1,46 @@ +/** + * @athenna/validator + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export type UniqueOptions = { + /** + * The table where the database will lookup for the data. + */ + table: string + + /** + * The column name in database. If not defined, the name + * of the field in the schema will be used. + * + * @default 'fieldNameInYourSchema' + */ + column?: string + + /** + * Use the max field to stablish a max limit for your validation. + * In some cases in your database you might have a max of 10 tuples + * with the same data. Use this option to validate that the number + * of fields registered in database cannot be bigger than the number + * defined on this option. + * + * @example + * ```ts + * const schema = this.validator.object({ + * name: this.validator.string().unique({ table: 'users', max: 10 }) + * }) + * + * const data = { name: 'lenon' } + * + * // Will throw if there are 10 users with name `lenon` + * // created in database + * await this.validator.validate({ schema: this.schema, data }) + * ``` + * @default undefined + */ + max?: number +} diff --git a/src/validator/BaseValidator.ts b/src/validator/BaseValidator.ts index 9e900b9..6e54fce 100644 --- a/src/validator/BaseValidator.ts +++ b/src/validator/BaseValidator.ts @@ -7,6 +7,7 @@ * file that was distributed with this source code. */ +import { v } from '#src' import type { SchemaTypes } from '#src/types' import { Validate } from '#src/facades/Validate' @@ -17,6 +18,6 @@ export abstract class BaseValidator { public abstract handle(data: any): Promise public async validate(data: any) { - return this.validator.validate({ schema: this.schema, data }) + return v.validate({ schema: this.schema, data }) } } diff --git a/src/validator/CustomValidations.ts b/src/validator/CustomValidations.ts new file mode 100644 index 0000000..baae369 --- /dev/null +++ b/src/validator/CustomValidations.ts @@ -0,0 +1,115 @@ +/** + * @athenna/validator + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Is } from '@athenna/common' +import { Validate } from '#src/facades/Validate' + +export class CustomValidations { + /** + * Register custom `string().unique()` validation. + */ + public static registerUnique() { + const DB = ioc.safeUse('Athenna/Core/Database') + + Validate.extend().string('unique', async (value, options, field) => { + /** + * Don't validate non string values, let `string` + * validation rule throw the error. + */ + if (!Is.String(value)) { + return + } + + /** + * Use custom column name, otherwise use the same + * in the field name. + */ + if (!options.column) { + options.column = field.name as string + } + + /** + * Define a max number of values that could be + * repeated in database to pass the validation. + */ + if (options.max) { + const rows = await DB.table(options.table) + .select(options.column) + .where(options.column, value) + .count() + + if (rows > options.max) { + field.report('The {{ field }} field is not unique', 'unique', field) + } + + return + } + + const existsRow = await DB.table(options.table) + .select(options.column) + .where(options.column, value) + .exists() + + if (existsRow) { + field.report('The {{ field }} field is not unique', 'unique', field) + } + }) + } + + /** + * Register custom `string().unique()` validation. + */ + public static registerExists() { + const DB = ioc.safeUse('Athenna/Core/Database') + + Validate.extend().string('exists', async (value, options, field) => { + /** + * Don't validate non string values, let `string` + * validation rule throw the error. + */ + if (!Is.String(value)) { + return + } + + /** + * Use custom column name, otherwise use the same + * in the field name. + */ + if (!options.column) { + options.column = field.name as string + } + + /** + * Define a minimum number of values that needs to + * exist in database to pass the validation. + */ + if (options.min) { + const rows = await DB.table(options.table) + .select(options.column) + .where(options.column, value) + .count() + + if (rows < options.min) { + field.report('The {{ field }} field does not exist', 'exists', field) + } + + return + } + + const existsRow = await DB.table(options.table) + .select(options.column) + .where(options.column, value) + .exists() + + if (!existsRow) { + field.report('The {{ field }} field does not exist', 'exists', field) + } + }) + } +} diff --git a/src/validator/ValidatorImpl.ts b/src/validator/ValidatorImpl.ts index 374f45a..16f9d7a 100644 --- a/src/validator/ValidatorImpl.ts +++ b/src/validator/ValidatorImpl.ts @@ -10,7 +10,7 @@ /* eslint-disable no-use-before-define */ import { - vine, + v, Vine, VineAccepted, VineAny, @@ -26,22 +26,13 @@ import { VineTuple } from '#src' -import type { ExtendHandlerType } from '#src/types' - -type ExtendReturnType = { - accepted: (name: string, handler: ExtendHandlerType) => ValidatorImpl - date: (name: string, handler: ExtendHandlerType) => ValidatorImpl - record: (name: string, handler: ExtendHandlerType) => ValidatorImpl - tuple: (name: string, handler: ExtendHandlerType) => ValidatorImpl - literal: (name: string, handler: ExtendHandlerType) => ValidatorImpl - array: (name: string, handler: ExtendHandlerType) => ValidatorImpl - any: (name: string, handler: ExtendHandlerType) => ValidatorImpl - string: (name: string, handler: ExtendHandlerType) => ValidatorImpl - number: (name: string, handler: ExtendHandlerType) => ValidatorImpl - enum: (name: string, handler: ExtendHandlerType) => ValidatorImpl - boolean: (name: string, handler: ExtendHandlerType) => ValidatorImpl - object: (name: string, handler: ExtendHandlerType) => ValidatorImpl -} +import type { + SchemaTypes, + ExtendReturnType, + ExtendHandlerType +} from '#src/types' +import { SimpleMessagesProvider } from '@vinejs/vine' +import type { Infer, ValidationOptions } from '@vinejs/vine/types' export class ValidatorImpl { /** @@ -49,34 +40,49 @@ export class ValidatorImpl { * build your validation schemas: * * ```ts - * const schema = Validate.schema.object({ - * name: Validate.schema.string(), - * email: Validate.schema.string().email(), - * password: Validate.schema.string().minLength(8).maxLength(32).confirmed() + * const schema = v.object({ + * name: v.string(), + * email: v.string().email(), + * password: v.string().minLength(8).maxLength(32).confirmed() * }) * ``` */ public get schema(): Vine { - return vine + return v + } + + /** + * Validate data by passing a schema and data. Also + * accepts other fields such as message provider + * and error reporter. + * + * ```ts + * const data = { name: 'Lenon' } + * const schema = v.object({ name: v.string() }) + * + * await v.validate({ schema, data }) + * ``` + */ + public async validate( + options: { schema: SchemaTypes; data: any } & ValidationOptions< + Record | undefined + > + ): Promise> { + return v.validate(options) } /** * Extend vine validation schema by adding new - * validation rules: + * validation rules or add custom messages: * * ```ts - * Validate.extend().string('unique', (value, options, field) => { - * if (!options.column) { - * options.column = field.name as string + * Validate.extend().string('lenon', (value, options, field) => { + * if (!Is.String(value)) { + * return * } * - * const existsRow = await Database.table(options.table) - * .select(options.column) - * .where(options.column, value) - * .exists() - * - * if (existsRow) { - * field.report('The {{ field }} field is not unique', 'unique', field) + * if (value !== 'lenon') { + * field.report('The {{ field }} field value is not lenon', 'lenon', field) * } * }) * ``` @@ -87,9 +93,14 @@ export class ValidatorImpl { } return { + messages: (messages: Record) => { + v.messagesProvider = new SimpleMessagesProvider(messages) + + return this + }, accepted: (name: string, handler: ExtendHandlerType) => { macro(VineAccepted, name, function (this: VineAccepted, opt: any) { - const rule = vine.createRule(handler) + const rule = v.createRule(handler) return this.use(rule(opt)) }) @@ -97,7 +108,7 @@ export class ValidatorImpl { }, date: (name: string, handler: ExtendHandlerType) => { macro(VineDate, name, function (this: VineDate, opt: any) { - const rule = vine.createRule(handler) + const rule = v.createRule(handler) return this.use(rule(opt)) }) @@ -105,7 +116,7 @@ export class ValidatorImpl { }, record: (name: string, handler: ExtendHandlerType) => { macro(VineRecord, name, function (this: VineRecord, opt: any) { - const rule = vine.createRule(handler) + const rule = v.createRule(handler) return this.use(rule(opt)) }) @@ -113,7 +124,7 @@ export class ValidatorImpl { }, tuple: (name: string, handler: ExtendHandlerType) => { macro(VineTuple, name, function (this: any, opt: any) { - const rule = vine.createRule(handler) + const rule = v.createRule(handler) return this.use(rule(opt)) }) @@ -121,7 +132,7 @@ export class ValidatorImpl { }, literal: (name: string, handler: ExtendHandlerType) => { macro(VineLiteral, name, function (this: VineLiteral, opt: any) { - const rule = vine.createRule(handler) + const rule = v.createRule(handler) return this.use(rule(opt)) }) @@ -129,7 +140,7 @@ export class ValidatorImpl { }, array: (name: string, handler: ExtendHandlerType) => { macro(VineArray, name, function (this: VineArray, opt: any) { - const rule = vine.createRule(handler) + const rule = v.createRule(handler) return this.use(rule(opt)) }) @@ -137,7 +148,7 @@ export class ValidatorImpl { }, any: (name: string, handler: ExtendHandlerType) => { macro(VineAny, name, function (this: VineAny, opt: any) { - const rule = vine.createRule(handler) + const rule = v.createRule(handler) return this.use(rule(opt)) }) @@ -145,7 +156,7 @@ export class ValidatorImpl { }, string: (name: string, handler: ExtendHandlerType) => { macro(VineString, name, function (this: VineString, opt: any) { - const rule = vine.createRule(handler) + const rule = v.createRule(handler) return this.use(rule(opt)) }) @@ -153,7 +164,7 @@ export class ValidatorImpl { }, number: (name: string, handler: ExtendHandlerType) => { macro(VineNumber, name, function (this: VineNumber, opt: any) { - const rule = vine.createRule(handler) + const rule = v.createRule(handler) return this.use(rule(opt)) }) @@ -161,7 +172,7 @@ export class ValidatorImpl { }, enum: (name: string, handler: ExtendHandlerType) => { macro(VineEnum, name, function (this: VineEnum, opt: any) { - const rule = vine.createRule(handler) + const rule = v.createRule(handler) return this.use(rule(opt)) }) @@ -169,7 +180,7 @@ export class ValidatorImpl { }, boolean: (name: string, handler: ExtendHandlerType) => { macro(VineBoolean, name, function (this: VineBoolean, opt: any) { - const rule = vine.createRule(handler) + const rule = v.createRule(handler) return this.use(rule(opt)) }) @@ -177,7 +188,7 @@ export class ValidatorImpl { }, object: (name: string, handler: ExtendHandlerType) => { macro(VineObject, name, function (this: any, opt: any) { - const rule = vine.createRule(handler) + const rule = v.createRule(handler) return this.use(rule(opt)) }) diff --git a/templates/validator-console.edge b/templates/validator-console.edge index 26d7cae..7f33434 100644 --- a/templates/validator-console.edge +++ b/templates/validator-console.edge @@ -1,12 +1,12 @@ -import { Validator, BaseValidator } from '@athenna/validator' +import { v, Validator, BaseValidator } from '@athenna/validator' @Validator() export class {{ namePascal }} extends BaseValidator { - public schema = this.validator.object({ - name: this.validator.string() + public schema = v.object({ + name: v.string() }) public async handle(data: any) { - await this.validate(data) + await v.validate(data) } } diff --git a/templates/validator-http.edge b/templates/validator-http.edge index 8c19dcd..e20b725 100644 --- a/templates/validator-http.edge +++ b/templates/validator-http.edge @@ -1,15 +1,15 @@ import type { Context } from '@athenna/http' -import { Validator, BaseValidator } from '@athenna/validator' +import { v, Validator, BaseValidator } from '@athenna/validator' @Validator() export class {{ namePascal }} extends BaseValidator { - public schema = this.validator.object({ - name: this.validator.string() + public schema = v.object({ + name: v.string() }) public async handle({ request }: Context) { const data = request.body - await this.validate(data) + await v.validate(data) } } diff --git a/tests/unit/providers/ValidatorProviderTest.ts b/tests/unit/providers/ValidatorProviderTest.ts index c82b104..552d047 100644 --- a/tests/unit/providers/ValidatorProviderTest.ts +++ b/tests/unit/providers/ValidatorProviderTest.ts @@ -61,4 +61,28 @@ export class ValidatorProviderTest { assert.isTrue(ioc.has('App/Validators/Names/hello')) } + + @Test() + public async shouldRegisterCustomUniqueValidation({ assert }: Context) { + ioc.singleton('Athenna/Core/Database', {}) + + const provider = new ValidatorProvider() + + await provider.register() + await provider.boot() + + assert.isDefined(Validate.schema.string().unique) + } + + @Test() + public async shouldRegisterCustomExistsValidation({ assert }: Context) { + ioc.singleton('Athenna/Core/Database', {}) + + const provider = new ValidatorProvider() + + await provider.register() + await provider.boot() + + assert.isDefined(Validate.schema.string().exists) + } } diff --git a/tests/unit/validator/CustomValidationsTest.ts b/tests/unit/validator/CustomValidationsTest.ts new file mode 100644 index 0000000..52de94b --- /dev/null +++ b/tests/unit/validator/CustomValidationsTest.ts @@ -0,0 +1,267 @@ +/** + * @athenna/validator + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Path } from '@athenna/common' +import { v, ValidatorProvider } from '#src' +import { Test, Mock, AfterEach, type Context, BeforeEach } from '@athenna/test' + +export class CustomValidationsTest { + @BeforeEach() + public async beforeEach() { + await Config.loadAll(Path.fixtures('config')) + } + + @AfterEach() + public async afterEach() { + Mock.restoreAll() + ioc.reconstruct() + Config.clear() + } + + @Test() + public async shouldThrowUniqueValidationIfDataExistsInDatabase({ assert }: Context) { + ioc.singleton('Athenna/Core/Database', { + table: () => ({ + select: () => ({ + where: () => ({ + exists: () => true + }) + }) + }) + }) + + const provider = new ValidatorProvider() + + await provider.register() + await provider.boot() + + const schema = v.object({ + name: v.string().unique({ table: 'users' }) + }) + + await assert.rejects(() => v.validate({ schema, data: { name: 'lenon' } })) + } + + @Test() + public async shouldPassUniqueValidationIfDataDoesNotExistsInDatabase({ assert }: Context) { + ioc.singleton('Athenna/Core/Database', { + table: () => ({ + select: () => ({ + where: () => ({ + exists: () => false + }) + }) + }) + }) + + const provider = new ValidatorProvider() + + await provider.register() + await provider.boot() + + const schema = v.object({ + name: v.string().unique({ table: 'users' }) + }) + + await assert.doesNotReject(() => v.validate({ schema, data: { name: 'lenon' } })) + } + + @Test() + public async shouldThrowUniqueValidationIfDataCountExceedMaxLimitInDatabase({ assert }: Context) { + ioc.singleton('Athenna/Core/Database', { + table: () => ({ + select: () => ({ + where: () => ({ + count: () => 11 + }) + }) + }) + }) + + const provider = new ValidatorProvider() + + await provider.register() + await provider.boot() + + const schema = v.object({ + name: v.string().unique({ table: 'users', max: 10 }) + }) + + await assert.rejects(() => v.validate({ schema, data: { name: 'lenon' } })) + } + + @Test() + public async shouldPassUniqueValidationIfDataCountDoesNotExceedMaxLimitInDatabase({ assert }: Context) { + ioc.singleton('Athenna/Core/Database', { + table: () => ({ + select: () => ({ + where: () => ({ + count: () => 10 + }) + }) + }) + }) + + const provider = new ValidatorProvider() + + await provider.register() + await provider.boot() + + const schema = v.object({ + name: v.string().unique({ table: 'users', max: 10 }) + }) + + await assert.doesNotReject(() => v.validate({ schema, data: { name: 'lenon' } })) + } + + @Test() + public async shouldBeAbleToUseCustomColumnNamesInUniqueValidation({ assert }: Context) { + ioc.singleton('Athenna/Core/Database', { + table: () => ({ + select: () => ({ + where: () => ({ + exists: () => false + }) + }) + }) + }) + + const provider = new ValidatorProvider() + + await provider.register() + await provider.boot() + + const schema = v.object({ + name: v.string().unique({ table: 'users', column: 'email' }) + }) + + await assert.doesNotReject(() => v.validate({ schema, data: { name: 'lenon' } })) + } + + @Test() + public async shouldThrowExistsValidationIfDataDoesNotExistsInDatabase({ assert }: Context) { + ioc.singleton('Athenna/Core/Database', { + table: () => ({ + select: () => ({ + where: () => ({ + exists: () => false + }) + }) + }) + }) + + const provider = new ValidatorProvider() + + await provider.register() + await provider.boot() + + const schema = v.object({ + name: v.string().exists({ table: 'users' }) + }) + + await assert.rejects(() => v.validate({ schema, data: { name: 'lenon' } })) + } + + @Test() + public async shouldPassExistsValidationIfDataExistsInDatabase({ assert }: Context) { + ioc.singleton('Athenna/Core/Database', { + table: () => ({ + select: () => ({ + where: () => ({ + exists: () => true + }) + }) + }) + }) + + const provider = new ValidatorProvider() + + await provider.register() + await provider.boot() + + const schema = v.object({ + name: v.string().exists({ table: 'users' }) + }) + + await assert.doesNotReject(() => v.validate({ schema, data: { name: 'lenon' } })) + } + + @Test() + public async shouldThrowExistsValidationIfDataCountIsLowerThanMinLimitInDatabase({ assert }: Context) { + ioc.singleton('Athenna/Core/Database', { + table: () => ({ + select: () => ({ + where: () => ({ + count: () => 4 + }) + }) + }) + }) + + const provider = new ValidatorProvider() + + await provider.register() + await provider.boot() + + const schema = v.object({ + name: v.string().exists({ table: 'users', min: 5 }) + }) + + await assert.rejects(() => v.validate({ schema, data: { name: 'lenon' } })) + } + + @Test() + public async shouldPassExistsValidationIfDataCountIsLowerThanMinLimitInDatabase({ assert }: Context) { + ioc.singleton('Athenna/Core/Database', { + table: () => ({ + select: () => ({ + where: () => ({ + count: () => 5 + }) + }) + }) + }) + + const provider = new ValidatorProvider() + + await provider.register() + await provider.boot() + + const schema = v.object({ + name: v.string().exists({ table: 'users', min: 5 }) + }) + + await v.validate({ schema, data: { name: 'lenon' } }) + await assert.doesNotReject(() => v.validate({ schema, data: { name: 'lenon' } })) + } + + @Test() + public async shouldBeAbleToUseCustomColumnNamesInExistsValidation({ assert }: Context) { + ioc.singleton('Athenna/Core/Database', { + table: () => ({ + select: () => ({ + where: () => ({ + exists: () => true + }) + }) + }) + }) + + const provider = new ValidatorProvider() + + await provider.register() + await provider.boot() + + const schema = v.object({ + name: v.string().exists({ table: 'users', column: 'email' }) + }) + + await assert.doesNotReject(() => v.validate({ schema, data: { name: 'lenon' } })) + } +} diff --git a/tests/unit/validator/ValidatorImplTest.ts b/tests/unit/validator/ValidatorImplTest.ts index 412ea84..dd52ccd 100644 --- a/tests/unit/validator/ValidatorImplTest.ts +++ b/tests/unit/validator/ValidatorImplTest.ts @@ -73,6 +73,41 @@ export class ValidatorImplTest { assert.isDefined(schema.getProperties().name) } + @Test() + public async shouldBeAbleToValidateDataUsingValidateMethod({ assert }: Context) { + const validator = new ValidatorImpl() + + const schema = validator.schema.object({ + name: validator.schema.string() + }) + + const data = await validator.validate({ schema, data: { name: 'lenon' } }) + + assert.deepEqual(data, { name: 'lenon' }) + } + + @Test() + public async shouldBeAbleToExtendValidationMessages({ assert }: Context) { + assert.plan(1) + const validator = new ValidatorImpl() + + validator.extend().messages({ + required: 'The {{ field }} field is REQUIRED!' + }) + + const schema = validator.schema.object({ + test: validator.schema.string() + }) + + try { + await validator.validate({ schema, data: { test: undefined } }) + } catch (err) { + assert.deepEqual(err.details || err.messages, [ + { message: 'The test field is REQUIRED!', rule: 'required', field: 'test' } + ]) + } + } + @Test() public async shouldBeAbleToExtendTheAcceptedValidationSchema({ assert }: Context) { assert.plan(3) @@ -89,7 +124,7 @@ export class ValidatorImplTest { test: validator.schema.accepted().test({ opt1: 'opt1' }) }) - await validator.schema.validate({ schema, data: { test: 'on' } }) + await validator.validate({ schema, data: { test: 'on' } }) } @Test() @@ -108,7 +143,7 @@ export class ValidatorImplTest { test: validator.schema.date({ formats: ['YYYY/MM/DD'] }).test({ opt1: 'opt1' }) }) - await validator.schema.validate({ schema, data: { test: '2024/12/09' } }) + await validator.validate({ schema, data: { test: '2024/12/09' } }) } @Test() @@ -127,7 +162,7 @@ export class ValidatorImplTest { test: validator.schema.record(validator.schema.string()).test({ opt1: 'opt1' }) }) - await validator.schema.validate({ schema, data: { test: { hello: 'world' } } }) + await validator.validate({ schema, data: { test: { hello: 'world' } } }) } @Test() @@ -146,7 +181,7 @@ export class ValidatorImplTest { test: validator.schema.tuple([validator.schema.number(), validator.schema.string()]).test({ opt1: 'opt1' }) }) - await validator.schema.validate({ schema, data: { test: [1, '2'] } }) + await validator.validate({ schema, data: { test: [1, '2'] } }) } @Test() @@ -165,7 +200,7 @@ export class ValidatorImplTest { test: validator.schema.literal(true).test({ opt1: 'opt1' }) }) - await validator.schema.validate({ schema, data: { test: true } }) + await validator.validate({ schema, data: { test: true } }) } @Test() @@ -184,7 +219,7 @@ export class ValidatorImplTest { test: validator.schema.array(validator.schema.number()).test({ opt1: 'opt1' }) }) - await validator.schema.validate({ schema, data: { test: [1, 2, 3] } }) + await validator.validate({ schema, data: { test: [1, 2, 3] } }) } @Test() @@ -203,7 +238,7 @@ export class ValidatorImplTest { test: validator.schema.any().test({ opt1: 'opt1' }) }) - await validator.schema.validate({ schema, data: { test: [1, 2, 3] } }) + await validator.validate({ schema, data: { test: [1, 2, 3] } }) } @Test() @@ -222,7 +257,7 @@ export class ValidatorImplTest { test: validator.schema.string().test({ opt1: 'opt1' }) }) - await validator.schema.validate({ schema, data: { test: '1' } }) + await validator.validate({ schema, data: { test: '1' } }) } @Test() @@ -241,7 +276,7 @@ export class ValidatorImplTest { test: validator.schema.number().test({ opt1: 'opt1' }) }) - await validator.schema.validate({ schema, data: { test: 1 } }) + await validator.validate({ schema, data: { test: 1 } }) } @Test() @@ -260,7 +295,7 @@ export class ValidatorImplTest { test: validator.schema.enum(['admin', 'customer']).test({ opt1: 'opt1' }) }) - await validator.schema.validate({ schema, data: { test: 'admin' } }) + await validator.validate({ schema, data: { test: 'admin' } }) } @Test() @@ -279,7 +314,7 @@ export class ValidatorImplTest { test: validator.schema.boolean().test({ opt1: 'opt1' }) }) - await validator.schema.validate({ schema, data: { test: true } }) + await validator.validate({ schema, data: { test: true } }) } @Test() @@ -298,6 +333,6 @@ export class ValidatorImplTest { test: validator.schema.object({ hello: validator.schema.string() }).test({ opt1: 'opt1' }) }) - await validator.schema.validate({ schema, data: { test: { hello: 'world' } } }) + await validator.validate({ schema, data: { test: { hello: 'world' } } }) } } From 8f489a406294be925831deb88735f86069da51fe Mon Sep 17 00:00:00 2001 From: jlenon7 Date: Mon, 30 Dec 2024 12:15:41 -0300 Subject: [PATCH 2/2] chore(npm): upgrade minor --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d936ccb..4e2c23e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/validator", - "version": "5.2.0", + "version": "5.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athenna/validator", - "version": "5.2.0", + "version": "5.3.0", "license": "MIT", "dependencies": { "@vinejs/vine": "^2.1.0" diff --git a/package.json b/package.json index 023b3da..8bc6c91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/validator", - "version": "5.2.0", + "version": "5.3.0", "description": "The Athenna validation solution. Built on top of VineJS.", "license": "MIT", "author": "João Lenon ",