diff --git a/providers/database_provider.ts b/providers/database_provider.ts index cb06a8f4..2c8f555e 100644 --- a/providers/database_provider.ts +++ b/providers/database_provider.ts @@ -16,6 +16,7 @@ import { QueryClient } from '../src/query_client/index.js' import { BaseModel } from '../src/orm/base_model/index.js' import { DatabaseTestUtils } from '../src/test_utils/database.js' import type { DatabaseConfig, DbQueryEventNode } from '../src/types/database.js' +import { DatabaseQueryBuilderContract } from '../src/types/querybuilder.js' /** * Extending AdonisJS types @@ -49,6 +50,20 @@ declare module '@vinejs/vine' { */ unique(callback: (db: Database, value: string, field: FieldContext) => Promise): this + /** + * Ensure the value is unique inside the database by table and column name. + * Optionally, you can define a filter to narrow down the query. + */ + unique(options: { + table: string + column?: string + filter?: ( + db: DatabaseQueryBuilderContract, + value: unknown, + field: FieldContext + ) => Promise + }): this + /** * Ensure the value is exists inside the database by self * executing a query. diff --git a/src/bindings/vinejs.ts b/src/bindings/vinejs.ts index a048350e..87f84c50 100644 --- a/src/bindings/vinejs.ts +++ b/src/bindings/vinejs.ts @@ -9,24 +9,54 @@ import vine, { VineNumber, VineString } from '@vinejs/vine' import type { Database } from '../database/main.js' +import type { FieldContext } from '@vinejs/vine/types' +import { DatabaseQueryBuilderContract } from '../types/querybuilder.js' /** * Defines the "unique" and "exists" validation rules with * VineJS. */ export function defineValidationRules(db: Database) { - const uniqueRule = vine.createRule[0]>( - async (value, checker, field) => { - if (!field.isValid) { - return + const uniqueRule = vine.createRule< + | ((db: Database, value: string, field: FieldContext) => Promise) + | { + table: string + column?: string + filter?: ( + db: DatabaseQueryBuilderContract, + value: unknown, + field: FieldContext + ) => Promise } + >(async (value, checkerOrOptions, field) => { + if (!field.isValid) { + return + } - const isUnique = await checker(db, value as string, field) + if (typeof checkerOrOptions === 'function') { + const isUnique = await checkerOrOptions(db, value as string, field) if (!isUnique) { field.report('The {{ field }} has already been taken', 'database.unique', field) } + return } - ) + + if (typeof value !== 'string') { + return + } + + if (typeof field.name !== 'string') { + return + } + + const { table, column = field.name, filter } = checkerOrOptions + const baseQuery = db.from(table).select(column).where(column, value) + await filter?.(baseQuery, value, field) + const row = await baseQuery.first() + if (row) { + field.report('The {{ field }} has already been taken', 'database.unique', field) + } + }) const existsRule = vine.createRule[0]>( async (value, checker, field) => { @@ -41,14 +71,14 @@ export function defineValidationRules(db: Database) { } ) - VineString.macro('unique', function (this: VineString, checker) { - return this.use(uniqueRule(checker)) + VineString.macro('unique', function (this: VineString, checkerOrOptions) { + return this.use(uniqueRule(checkerOrOptions)) }) VineString.macro('exists', function (this: VineString, checker) { return this.use(existsRule(checker)) }) - VineNumber.macro('unique', function (this: VineNumber, checker) { - return this.use(uniqueRule(checker)) + VineNumber.macro('unique', function (this: VineNumber, checkerOrOptions) { + return this.use(uniqueRule(checkerOrOptions)) }) VineNumber.macro('exists', function (this: VineNumber, checker) { return this.use(existsRule(checker))