From 1bdf5e698960e5e0f9a8e766ccfb4d748ae5c7b6 Mon Sep 17 00:00:00 2001 From: Harminder virk Date: Thu, 30 Apr 2020 20:24:57 +0530 Subject: [PATCH] improvement: allow whereNot contraints for the unique and the exists rules If the value inside key-value pair is an array, then we will apply a whereIn or whereNotIn clause --- adonis-typings/validator.ts | 24 ++-- src/Bindings/Validator.ts | 103 ++++++++++++--- test/bindings/validator.spec.ts | 216 +++++++++++++++++++++++++++----- 3 files changed, 277 insertions(+), 66 deletions(-) diff --git a/adonis-typings/validator.ts b/adonis-typings/validator.ts index 3a158cdd..8fced278 100644 --- a/adonis-typings/validator.ts +++ b/adonis-typings/validator.ts @@ -10,19 +10,17 @@ declare module '@ioc:Adonis/Core/Validator' { import { Rule } from '@ioc:Adonis/Core/Validator' - export interface Rules { - exists (options: { - table: string, - column: string, - connection?: string, - constraints?: { [key: string]: any } | { [key: string]: any }[], - }): Rule + export type DbRowCheckOptions = { + table: string, + column: string, + connection?: string, + constraints?: { [key: string]: any }, + where?: { [key: string]: any }, + whereNot?: { [key: string]: any }, + } - unique (options: { - table: string, - column: string, - connection?: string, - constraints?: { [key: string]: any } | { [key: string]: any }[], - }): Rule + export interface Rules { + exists (options: DbRowCheckOptions): Rule + unique (options: DbRowCheckOptions): Rule } } diff --git a/src/Bindings/Validator.ts b/src/Bindings/Validator.ts index 854b894f..bcae0d95 100644 --- a/src/Bindings/Validator.ts +++ b/src/Bindings/Validator.ts @@ -10,7 +10,28 @@ import { Exception } from '@poppinss/utils' import { DatabaseContract } from '@ioc:Adonis/Lucid/Database' import { DatabaseQueryBuilderContract } from '@ioc:Adonis/Lucid/DatabaseQueryBuilder' -import { validator as validatorStatic, ValidationRuntimeOptions } from '@ioc:Adonis/Core/Validator' +import { + DbRowCheckOptions, + ValidationRuntimeOptions, + validator as validatorStatic, +} from '@ioc:Adonis/Core/Validator' + +/** + * Shape of constraint after normalization + */ +type NormalizedConstraint = { + key: string, + operator: 'in' | 'eq', + value: string | string[], +} + +/** + * Normalized validation options + */ +type NormalizedOptions = Omit & { + where: NormalizedConstraint[], + whereNot: NormalizedConstraint[], +} /** * Checks for database rows for `exists` and `unique` rule. @@ -20,22 +41,64 @@ class DbRowCheck { } /** - * Applies user defined constraints on the query builder + * Applies user defined where constraints on the query builder + */ + private applyWhere (query: DatabaseQueryBuilderContract, constraints: NormalizedConstraint[]) { + if (!constraints.length) { + return + } + + constraints.forEach(({ key, operator, value }) => { + if (operator === 'in') { + query.whereIn(key, value as string[]) + } else { + query.where(key, value) + } + }) + } + + /** + * Applies user defined where not constraints on the query builder + */ + private applyWhereNot (query: DatabaseQueryBuilderContract, constraints: NormalizedConstraint[]) { + if (!constraints.length) { + return + } + + constraints.forEach(({ key, operator, value }) => { + if (operator === 'in') { + query.whereNotIn(key, value as string[]) + } else { + query.whereNot(key, value) + } + }) + } + + /** + * Normalizes constraints */ - private applyConstraints (query: DatabaseQueryBuilderContract, constraints: any[]) { - if (constraints.length > 1) { - query.where((builder) => { - constraints.forEach((constraint) => builder.orWhere(constraint)) - }) - } else { - constraints.forEach((constraint) => query.where(constraint)) + private normalizeConstraints (constraints: DbRowCheckOptions['where']) { + const normalized: NormalizedConstraint[] = [] + if (!constraints) { + return normalized } + + /** + * Normalize object into an array of objects + */ + return Object.keys(constraints).reduce((result, key) => { + const value = constraints[key] + const operator = Array.isArray(value) ? 'in' : 'eq' + result.push({ key, value, operator }) + + return result + }, normalized) } /** * Compile validation options */ - public compile (options) { + public compile (options: DbRowCheckOptions) { /** * Ensure options are defined with table and column name */ @@ -44,20 +107,21 @@ class DbRowCheck { } /** - * Normalize where constraints + * Emit warning */ - let constraints: { [key: string]: any }[] = [] - if (options.constraints && Array.isArray(options.constraints)) { - constraints = options.constraints - } else if (options.constraints && typeof (options.constraints) === 'object' && options.constraints !== null) { - constraints = [options.constraints] + if (options.constraints) { + process.emitWarning( + 'DeprecationWarning', + '"options.constraints" have been depreciated. Use "options.where" instead.', + ) } return { table: options.table, column: options.column, connection: options.connection, - constraints: constraints, + where: this.normalizeConstraints(options.where || options.constraints), + whereNot: this.normalizeConstraints(options.whereNot), } } @@ -66,11 +130,12 @@ class DbRowCheck { */ public async validate ( value: any, - { table, column, constraints, connection }: any, + { table, column, where, whereNot, connection }: NormalizedOptions, { pointer, errorReporter, arrayExpressionPointer }: ValidationRuntimeOptions, ) { const query = this.database.connection(connection).query().from(table).where(column, value) - this.applyConstraints(query, constraints) + this.applyWhere(query, where) + this.applyWhereNot(query, whereNot) const row = await query.first() if (this.ruleName === 'exists') { diff --git a/test/bindings/validator.spec.ts b/test/bindings/validator.spec.ts index c678ef74..d2c185dd 100644 --- a/test/bindings/validator.spec.ts +++ b/test/bindings/validator.spec.ts @@ -92,7 +92,7 @@ test.group('Validator | exists', (group) => { }) }) - test('check row with custom where contraints', async (assert) => { + test('add where contraints', async (assert) => { assert.plan(3) const [userId] = await db @@ -120,7 +120,7 @@ test.group('Validator | exists', (group) => { id: schema.number([validator.rules.exists({ table: 'users', column: 'id', - constraints: { + where: { username: 'nikk', }, })]), @@ -134,7 +134,7 @@ test.group('Validator | exists', (group) => { } }) - test('check row with custom or where contraints', async (assert) => { + test('add wherein contraints', async (assert) => { assert.plan(3) const [userId] = await db @@ -148,11 +148,7 @@ test.group('Validator | exists', (group) => { .getReadClient() .from('users') .where('id', userId) - .where((builder) => { - builder - .orWhere({ username: 'nikk' }) - .orWhere({ username: 'virk', email: 'foo@bar.com' }) - }) + .whereIn('username', ['nikk', 'romain']) .limit(1) .toSQL() @@ -166,15 +162,93 @@ test.group('Validator | exists', (group) => { id: schema.number([validator.rules.exists({ table: 'users', column: 'id', - constraints: [ - { - username: 'nikk', - }, - { - username: 'virk', - email: 'foo@bar.com', - }, - ], + where: { + username: ['nikk', 'romain'], + }, + })]), + })), + data: { id: userId }, + }) + } catch (error) { + assert.deepEqual(error.messages, { + id: ['exists validation failure'], + }) + } + }) + + test('add where not constraints', async (assert) => { + assert.plan(3) + + const [userId] = await db + .table('users') + .returning('id') + .insert({ email: 'virk@adonisjs.com', username: 'virk' }) + + db.connection().getReadClient().on('query', ({ sql, bindings }) => { + const { sql: knexSql, bindings: knexBindings } = db + .connection() + .getReadClient() + .from('users') + .where('id', userId) + .whereNot('username', 'virk') + .limit(1) + .toSQL() + + assert.equal(sql, knexSql) + assert.deepEqual(bindings, knexBindings) + }) + + try { + await validator.validate({ + schema: validator.compile(schema.create({ + id: schema.number([validator.rules.exists({ + table: 'users', + column: 'id', + whereNot: { + username: 'virk', + }, + })]), + })), + data: { id: userId }, + }) + } catch (error) { + assert.deepEqual(error.messages, { + id: ['exists validation failure'], + }) + } + }) + + test('add where not in constraints', async (assert) => { + assert.plan(3) + + const [userId] = await db + .table('users') + .returning('id') + .insert({ email: 'virk@adonisjs.com', username: 'virk' }) + + db.connection().getReadClient().on('query', ({ sql, bindings }) => { + const { sql: knexSql, bindings: knexBindings } = db + .connection() + .getReadClient() + .from('users') + .where('id', userId) + .whereNotIn('username', ['virk', 'nikk']) + .limit(1) + .toSQL() + + assert.equal(sql, knexSql) + assert.deepEqual(bindings, knexBindings) + }) + + try { + await validator.validate({ + schema: validator.compile(schema.create({ + id: schema.number([validator.rules.exists({ + table: 'users', + column: 'id', + whereNot: { + username: ['virk', 'nikk'], + }, })]), })), data: { id: userId }, @@ -241,7 +315,7 @@ test.group('Validator | unique', (group) => { }) }) - test('check row with custom where contraints', async (assert) => { + test('add where contraints', async (assert) => { assert.plan(3) const [userId] = await db @@ -269,7 +343,7 @@ test.group('Validator | unique', (group) => { id: schema.number([validator.rules.unique({ table: 'users', column: 'id', - constraints: { + where: { username: 'virk', }, })]), @@ -283,7 +357,7 @@ test.group('Validator | unique', (group) => { } }) - test('check row with custom or where contraints', async (assert) => { + test('add where in contraints', async (assert) => { assert.plan(3) const [userId] = await db @@ -297,11 +371,7 @@ test.group('Validator | unique', (group) => { .getReadClient() .from('users') .where('id', userId) - .where((builder) => { - builder - .orWhere({ username: 'nikk' }) - .orWhere({ username: 'virk', email: 'virk@adonisjs.com' }) - }) + .whereIn('username', ['virk', 'nikk']) .limit(1) .toSQL() @@ -315,15 +385,93 @@ test.group('Validator | unique', (group) => { id: schema.number([validator.rules.unique({ table: 'users', column: 'id', - constraints: [ - { - username: 'nikk', - }, - { - username: 'virk', - email: 'virk@adonisjs.com', - }, - ], + where: { + username: ['virk', 'nikk'], + }, + })]), + })), + data: { id: userId }, + }) + } catch (error) { + assert.deepEqual(error.messages, { + id: ['unique validation failure'], + }) + } + }) + + test('add whereNot contraints', async (assert) => { + assert.plan(3) + + const [userId] = await db + .table('users') + .returning('id') + .insert({ email: 'virk@adonisjs.com', username: 'virk' }) + + db.connection().getReadClient().on('query', ({ sql, bindings }) => { + const { sql: knexSql, bindings: knexBindings } = db + .connection() + .getReadClient() + .from('users') + .where('id', userId) + .whereNot('username', 'nikk') + .limit(1) + .toSQL() + + assert.equal(sql, knexSql) + assert.deepEqual(bindings, knexBindings) + }) + + try { + await validator.validate({ + schema: validator.compile(schema.create({ + id: schema.number([validator.rules.unique({ + table: 'users', + column: 'id', + whereNot: { + username: 'nikk', + }, + })]), + })), + data: { id: userId }, + }) + } catch (error) { + assert.deepEqual(error.messages, { + id: ['unique validation failure'], + }) + } + }) + + test('add whereNot in contraints', async (assert) => { + assert.plan(3) + + const [userId] = await db + .table('users') + .returning('id') + .insert({ email: 'virk@adonisjs.com', username: 'virk', country_id: 4 }) + + db.connection().getReadClient().on('query', ({ sql, bindings }) => { + const { sql: knexSql, bindings: knexBindings } = db + .connection() + .getReadClient() + .from('users') + .where('id', userId) + .whereNotIn('country_id', [1, 2]) + .limit(1) + .toSQL() + + assert.equal(sql, knexSql) + assert.deepEqual(bindings, knexBindings) + }) + + try { + await validator.validate({ + schema: validator.compile(schema.create({ + id: schema.number([validator.rules.unique({ + table: 'users', + column: 'id', + whereNot: { + country_id: [1, 2], + }, })]), })), data: { id: userId },