diff --git a/package.json b/package.json index 76693291..2633371e 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "@types/pluralize": "^0.0.33", "@types/pretty-hrtime": "^1.0.3", "@types/qs": "^6.9.10", + "@vinejs/vine": "^1.7.0", "better-sqlite3": "^9.1.1", "c8": "^8.0.1", "chance": "^1.1.11", diff --git a/providers/database_provider.ts b/providers/database_provider.ts index 37b2c0a6..2e67fde8 100644 --- a/providers/database_provider.ts +++ b/providers/database_provider.ts @@ -30,6 +30,30 @@ declare module '@adonisjs/core/types' { export default class DatabaseServiceProvider { constructor(protected app: ApplicationService) {} + /** + * Registers repl bindings when running the application + * in the REPL environment + */ + protected async registerReplBindings() { + if (this.app.getEnvironment() === 'repl') { + const { defineReplBindings } = await import('../src/bindings/repl.js') + defineReplBindings(this.app, await this.app.container.make('repl')) + } + } + + /** + * Registers validation rules for VineJS + */ + protected async registerVineJSRules(db: Database) { + if (this.app.usingVineJS) { + const { defineValidationRules } = await import('../src/bindings/vinejs.js') + defineValidationRules(db) + } + } + + /** + * Invoked by AdonisJS to register container bindings + */ register() { this.app.container.singleton(Database, async (resolver) => { const config = this.app.config.get('database') @@ -46,8 +70,15 @@ export default class DatabaseServiceProvider { this.app.container.alias('lucid.db', Database) } + /** + * Invoked by AdonisJS to extend the framework or pre-configure + * objects + */ async boot() { const db = await this.app.container.make('lucid.db') BaseModel.$adapter = new Adapter(db) + + await this.registerReplBindings() + await this.registerVineJSRules(db) } } diff --git a/src/bindings/vinejs.ts b/src/bindings/vinejs.ts new file mode 100644 index 00000000..2dbd0e83 --- /dev/null +++ b/src/bindings/vinejs.ts @@ -0,0 +1,78 @@ +/* + * @adonisjs/lucid + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import vine, { VineString } from '@vinejs/vine' +import type { FieldContext } from '@vinejs/vine/types' +import type { Database } from '../database/main.js' + +/** + * The callback function to check a row inside the database + */ +type DatabaseRowChecker = (db: Database, value: string, field: FieldContext) => Promise + +declare module '@vinejs/vine' { + export interface VineString { + /** + * Ensure the value is unique inside the database by self + * executing a query. + * + * - The callback must return "true", if the value is unique (does not exist). + * - The callback must return "false", if the value is not unique (already exists). + */ + unique(callback: DatabaseRowChecker): this + + /** + * Ensure the value is exists inside the database by self + * executing a query. + * + * - The callback must return "true", if the value exists. + * - The callback must return "false", if the value does not exist. + */ + exists(callback: DatabaseRowChecker): this + } +} + +/** + * Defines the "unique" and "exists" validation rules with + * VineJS. + */ +export function defineValidationRules(db: Database) { + const uniqueRule = vine.createRule( + async (value, optionsOrCallback, field) => { + if (!field.isValid) { + return + } + + const isUnqiue = await optionsOrCallback(db, value as string, field) + if (!isUnqiue) { + field.report('The {{ field }} has already been taken', 'database.unique', field) + } + } + ) + + const existsRule = vine.createRule( + async (value, optionsOrCallback, field) => { + if (!field.isValid) { + return + } + + const exists = await optionsOrCallback(db, value as string, field) + if (!exists) { + field.report('The selected {{ field }} is invalid', 'database.exists', field) + } + } + ) + + VineString.macro('unique', function (this: VineString, optionsOrCallback) { + return this.use(uniqueRule(optionsOrCallback)) + }) + VineString.macro('exists', function (this: VineString, optionsOrCallback) { + return this.use(existsRule(optionsOrCallback)) + }) +}