Skip to content

Commit

Permalink
Merge branch 'adonisjs:21.x' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
zaosoula authored Dec 14, 2024
2 parents a0417b9 + ca67d8d commit 918f473
Show file tree
Hide file tree
Showing 16 changed files with 926 additions and 72 deletions.
2 changes: 1 addition & 1 deletion commands/db_seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default class DbSeed extends BaseCommand {
startApp: true,
}

private declare seeder: SeedsRunner
declare private seeder: SeedsRunner

/**
* Track if one or more seeders have failed
Expand Down
30 changes: 17 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@adonisjs/lucid",
"description": "SQL ORM built on top of Active Record pattern",
"version": "21.4.0",
"version": "21.5.1",
"engines": {
"node": ">=18.16.0"
},
Expand Down Expand Up @@ -60,7 +60,7 @@
},
"devDependencies": {
"@adonisjs/assembler": "^7.8.2",
"@adonisjs/core": "^6.14.1",
"@adonisjs/core": "^6.16.0",
"@adonisjs/eslint-config": "^2.0.0-beta.7",
"@adonisjs/prettier-config": "^1.4.0",
"@adonisjs/tsconfig": "^1.4.0",
Expand All @@ -69,36 +69,36 @@
"@japa/runner": "^3.1.4",
"@libsql/sqlite3": "^0.3.1",
"@release-it/conventional-changelog": "^9.0.3",
"@swc/core": "^1.9.2",
"@swc/core": "^1.10.0",
"@types/chance": "^1.1.6",
"@types/luxon": "^3.4.2",
"@types/node": "^22.9.0",
"@types/node": "^22.10.1",
"@types/pretty-hrtime": "^1.0.3",
"@types/qs": "^6.9.17",
"@vinejs/vine": "^2.1.0",
"better-sqlite3": "^11.5.0",
"@vinejs/vine": "^3.0.0",
"better-sqlite3": "^11.7.0",
"c8": "^10.1.2",
"chance": "^1.1.12",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"del-cli": "^6.0.0",
"dotenv": "^16.4.5",
"eslint": "^9.14.0",
"dotenv": "^16.4.7",
"eslint": "^9.16.0",
"fs-extra": "^11.2.0",
"luxon": "^3.5.0",
"mysql2": "^3.11.4",
"mysql2": "^3.11.5",
"pg": "^8.13.1",
"prettier": "^3.3.3",
"prettier": "^3.4.2",
"reflect-metadata": "^0.2.2",
"release-it": "^17.10.0",
"sqlite3": "^5.1.7",
"tedious": "^18.6.1",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
"typescript": "^5.7.2"
},
"dependencies": {
"@adonisjs/presets": "^2.6.3",
"@faker-js/faker": "^9.2.0",
"@faker-js/faker": "^9.3.0",
"@poppinss/hooks": "^7.2.4",
"@poppinss/macroable": "^1.0.3",
"@poppinss/utils": "^6.8.3",
Expand All @@ -108,19 +108,23 @@
"knex": "^3.1.0",
"knex-dynamic-connection": "^3.2.0",
"pretty-hrtime": "^1.0.3",
"qs": "^6.13.0",
"qs": "^6.13.1",
"slash": "^5.1.0",
"tarn": "^3.0.2"
},
"peerDependencies": {
"@adonisjs/assembler": "^7.7.0",
"@adonisjs/core": "^6.10.1",
"@vinejs/vine": "^2.0.0 || ^3.0.0",
"luxon": "^3.4.4"
},
"peerDependenciesMeta": {
"@adonisjs/assembler": {
"optional": true
},
"@vinejs/vine": {
"optional": true
},
"luxon": {
"optional": true
}
Expand Down
29 changes: 20 additions & 9 deletions providers/database_provider.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <[email protected]>
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import type { FieldContext } from '@vinejs/vine/types'
import type { ApplicationService } from '@adonisjs/core/types'

import { Database } from '../src/database/main.js'
Expand All @@ -16,6 +15,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 { VineDbSearchCallback, VineDbSearchOptions } from '../src/types/vine.js'

/**
* Extending AdonisJS types
Expand All @@ -39,29 +39,40 @@ declare module '@adonisjs/core/test_utils' {
* Extending VineJS schema types
*/
declare module '@vinejs/vine' {
interface VineLucidBindings {
interface VineLucidBindings<ValueType> {
/**
* 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: VineDbSearchOptions<ValueType>): this

/**
* 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: (db: Database, value: string, field: FieldContext) => Promise<boolean>): this
unique(callback: VineDbSearchCallback<ValueType>): this

/**
* Ensure the value is exists inside the database by self
* Ensure the value exists inside the database by table and column name.
* Optionally, you can define a filter to narrow down the query.
*/
exists(options: VineDbSearchOptions<ValueType>): this

/**
* Ensure the value exists inside the database by self
* executing a query.
*
* - The callback must return "false", if the value exists.
* - The callback must return "true", if the value does not exist.
*/
exists(callback: (db: Database, value: string, field: FieldContext) => Promise<boolean>): this
exists(callback: VineDbSearchCallback<ValueType>): this
}

interface VineNumber extends VineLucidBindings {}

interface VineString extends VineLucidBindings {}
interface VineNumber extends VineLucidBindings<number> {}
interface VineString extends VineLucidBindings<string> {}
}

/**
Expand Down
148 changes: 119 additions & 29 deletions src/bindings/vinejs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,140 @@
* file that was distributed with this source code.
*/

import vine, { VineNumber, VineString } from '@vinejs/vine'
import type { Database } from '../database/main.js'
import vine, { VineNumber, VineString } from '@vinejs/vine'
import { VineDbSearchCallback, VineDbSearchOptions } from '../types/vine.js'

/**
* Default validation messages used by the unique and the
* exists rules
*/
export const messages = {
'database.unique': 'The {{ field }} has already been taken',
'database.exists': 'The selected {{ field }} is invalid',
}

/**
* Defines the "unique" and "exists" validation rules with
* VineJS.
*/
export function defineValidationRules(db: Database) {
const uniqueRule = vine.createRule<Parameters<VineString['unique'] | VineNumber['unique']>[0]>(
async (value, checker, field) => {
if (!field.isValid) {
return
}
function uniqueRule<ValueType>() {
return vine.createRule<VineDbSearchCallback<ValueType> | VineDbSearchOptions<ValueType>>(
async (value, callbackOrOptions, field) => {
if (!field.isValid) {
return
}

const isUnique = await checker(db, value as string, field)
if (!isUnique) {
field.report('The {{ field }} has already been taken', 'database.unique', field)
}
}
)
/**
* Rely on the callback to execute the query and return value
* a boolean.
*
* True means value is unique
* False means value is not unique
*/
if (typeof callbackOrOptions === 'function') {
const isUnique = await callbackOrOptions(db, value as ValueType, field)
if (!isUnique) {
field.report(messages['database.unique'], 'database.unique', field)
}
return
}

const { table, column, filter, connection, caseInsensitive } = callbackOrOptions
const query = db.connection(connection).from(table).select(column)

const existsRule = vine.createRule<Parameters<VineString['exists'] | VineNumber['exists']>[0]>(
async (value, checker, field) => {
if (!field.isValid) {
return
/**
* Apply where clause respecting the caseInsensitive flag.
*/
if (caseInsensitive) {
query.whereRaw(`lower(${column}) = ?`, [db.raw(`lower(?)`, [value])])
} else {
query.where(column, value as string | number)
}

/**
* Apply user filter
*/
await filter?.(query, value as ValueType, field)

/**
* Fetch the first row from the database
*/
const row = await query.first()
if (row) {
field.report(messages['database.unique'], 'database.unique', field)
}
}
)
}

function existsRule<ValueType>() {
return vine.createRule<VineDbSearchCallback<ValueType> | VineDbSearchOptions<ValueType>>(
async (value, callbackOrOptions, field) => {
if (!field.isValid) {
return
}

/**
* Rely on the callback to execute the query and return value
* a boolean.
*
* True means value exists
* False means value does not exist
*/
if (typeof callbackOrOptions === 'function') {
const exists = await callbackOrOptions(db, value as ValueType, field)
if (!exists) {
field.report(messages['database.exists'], 'database.exists', field)
}
return
}

const { table, column, filter, connection, caseInsensitive } = callbackOrOptions
const query = db.connection(connection).from(table).select(column)

const exists = await checker(db, value as string, field)
if (!exists) {
field.report('The selected {{ field }} is invalid', 'database.exists', field)
/**
* Apply where clause respecting the caseInsensitive flag.
*/
if (caseInsensitive) {
query.whereRaw(`lower(${column}) = ?`, [db.raw(`lower(?)`, [value])])
} else {
query.where(column, value as string | number)
}

/**
* Apply user filter
*/
await filter?.(query, value as ValueType, field)

/**
* Fetch the first row from the database
*/
const row = await query.first()
if (!row) {
field.report(messages['database.exists'], 'database.exists', field)
}
}
}
)
)
}

VineString.macro('unique', function (this: VineString, checker) {
return this.use(uniqueRule(checker))
const uniqueRuleForString = uniqueRule<string>()
const uniqueRuleForNumber = uniqueRule<number>()
const existsRuleForString = existsRule<string>()
const existsRuleForNumber = existsRule<number>()

VineString.macro('unique', function (this: VineString, callbackOrOptions) {
return this.use(uniqueRuleForString(callbackOrOptions))
})
VineString.macro('exists', function (this: VineString, checker) {
return this.use(existsRule(checker))
VineString.macro('exists', function (this: VineString, callbackOrOptions) {
return this.use(existsRuleForString(callbackOrOptions))
})
VineNumber.macro('unique', function (this: VineNumber, checker) {
return this.use(uniqueRule(checker))

VineNumber.macro('unique', function (this: VineNumber, callbackOrOptions) {
return this.use(uniqueRuleForNumber(callbackOrOptions))
})
VineNumber.macro('exists', function (this: VineNumber, checker) {
return this.use(existsRule(checker))
VineNumber.macro('exists', function (this: VineNumber, callbackOrOptions) {
return this.use(existsRuleForNumber(callbackOrOptions))
})
}
2 changes: 1 addition & 1 deletion src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const E_UNMANAGED_DB_CONNECTION = createError<[string]>(
)

export const E_MISSING_MODEL_ATTRIBUTE = createError<[string, string, string]>(
'"%s" expects "%s" to exist on "%s" model, but is missing',
'Relation "%s" expects "%s" to exist on "%s" model, but is missing. Did you forget to define the column?',
'E_MISSING_MODEL_ATTRIBUTE',
500
)
Expand Down
Loading

0 comments on commit 918f473

Please sign in to comment.