Skip to content

Commit

Permalink
feat(validator): add unique() validation
Browse files Browse the repository at this point in the history
  • Loading branch information
jlenon7 committed May 8, 2024
1 parent 268d827 commit 87286aa
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 31 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@
"@athenna/mail/providers/MailProvider",
"@athenna/mail/providers/SmtpServerProvider",
"@athenna/view/providers/ViewProvider",
"#src/providers/queueworker.provider"
"#src/providers/queueworker.provider",
"#src/providers/validator.provider"
],
"controllers": [
"#src/controllers/user.controller",
Expand Down
6 changes: 3 additions & 3 deletions src/exceptions/validation.exception.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Exception } from '@athenna/common'

export class ValidationException extends Exception {
public constructor(error: any) {
const status = error.status
public constructor(errors: any[]) {
const status = 422
const message = 'Validation failure'
const code = 'E_VALIDATION_ERROR'
const details = error.messages
const details = errors

super({ code, status, message, details })
}
Expand Down
66 changes: 66 additions & 0 deletions src/providers/validator.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Is } from '@athenna/common'
import { Database } from '@athenna/database'
import { ServiceProvider } from '@athenna/ioc'
import type { FieldContext } from '@vinejs/vine/types'
import vine, { SimpleErrorReporter, VineString } from '@vinejs/vine'
import { ValidationException } from '#src/exceptions/validation.exception'

type UniqueOptions = {
table: string
column?: string
}

declare module '@vinejs/vine' {
interface VineString {
unique(options: UniqueOptions): this
}
}

export class ErrorReporter extends SimpleErrorReporter {
createError(): any {
return new ValidationException(this.errors)
}
}

export default class ValidatorProvider extends ServiceProvider {
public async boot() {
vine.errorReporter = () => new ErrorReporter()

const uniqueRule = vine.createRule(this.unique)

VineString.macro(
'unique',
function (this: VineString, options: UniqueOptions) {
return this.use(uniqueRule(options))
}
)
}

public async unique(
value: unknown,
options: UniqueOptions,
field: FieldContext
) {
/**
* 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
}

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)
}
}
}
20 changes: 5 additions & 15 deletions src/validators/base.validator.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
import type { Context } from '@athenna/http'
import vine, { type Vine } from '@vinejs/vine'
import type { SchemaTypes } from '@vinejs/vine/types'
import { ValidationException } from '#src/exceptions/validation.exception'

export abstract class BaseValidator {
public abstract definition: SchemaTypes
public abstract handleHttp(ctx: Context): Promise<void>
protected validator: Vine = vine

protected schema: Vine = vine
public abstract schema: SchemaTypes
public abstract handle(data: any): Promise<void>

public handle(ctx: Context): Promise<void> {
return this.handleHttp(ctx)
}

protected async validate(data: any) {
try {
await vine.validate({ schema: this.definition, data })
} catch (err) {
throw new ValidationException(err)
}
public validate(data: any) {
return this.validator.validate({ schema: this.schema, data })
}
}
8 changes: 4 additions & 4 deletions src/validators/login.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { BaseValidator } from '#src/validators/base.validator'

@Middleware({ name: 'login:validator' })
export class LoginValidator extends BaseValidator {
public definition = this.schema.object({
email: this.schema.string().email(),
password: this.schema.string()
public schema = this.validator.object({
email: this.validator.string().email(),
password: this.validator.string()
})

public async handleHttp({ request }: Context): Promise<void> {
public async handle({ request }: Context) {
const data = request.only(['email', 'password'])

await this.validate(data)
Expand Down
10 changes: 5 additions & 5 deletions src/validators/register.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { BaseValidator } from '#src/validators/base.validator'

@Middleware({ name: 'register:validator' })
export class RegisterValidator extends BaseValidator {
public definition = this.schema.object({
name: this.schema.string(),
email: this.schema.string().email(),
password: this.schema.string().minLength(8).maxLength(32).confirmed()
public schema = this.validator.object({
name: this.validator.string(),
email: this.validator.string().email().unique({ table: 'users' }),
password: this.validator.string().minLength(8).maxLength(32).confirmed()
})

public async handleHttp({ request }: Context): Promise<void> {
public async handle({ request }: Context) {
const data = request.only([
'name',
'email',
Expand Down
9 changes: 8 additions & 1 deletion storage/queues.json
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
{"default":[],"deadletter":[],"user:email":[],"user:password":[{"user":{"name":"João Lenon","email":"Dereck_Conn@hotmail.com"},"password":"$2b$10$wpmwbiGnqA.W0toBLwGQsu9li3/qldRGx6vwGReQYx1PmQd6bkmMC"}],"user:email:password":[],"user:confirm":[]}
{
"default": [],
"deadletter": [],
"user:email": [],
"user:password": [],
"user:email:password": [],
"user:confirm": []
}
31 changes: 29 additions & 2 deletions tests/e2e/auth.controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default class AuthControllerTest extends BaseHttpTest {

@Test()
public async shouldThrowUnauthorizedExceptionIfAuthenticatedDontHaveRolesKey({ request }: Context) {
const token = await jwt.sign({ user: { id: -1 } }, Config.get('auth.jwt.secret'), {
const token = jwt.sign({ user: { id: -1 } }, Config.get('auth.jwt.secret'), {
expiresIn: Config.get('auth.jwt.expiresIn')
})

Expand All @@ -55,7 +55,7 @@ export default class AuthControllerTest extends BaseHttpTest {

@Test()
public async shouldThrowUnauthorizedExceptionIfAuthenticatedUserCannotBeFound({ request }: Context) {
const token = await jwt.sign({ user: { id: -1, roles: [] } }, Config.get('auth.jwt.secret'), {
const token = jwt.sign({ user: { id: -1, roles: [] } }, Config.get('auth.jwt.secret'), {
expiresIn: Config.get('auth.jwt.expiresIn')
})

Expand Down Expand Up @@ -238,6 +238,33 @@ export default class AuthControllerTest extends BaseHttpTest {
})
}

@Test()
public async shouldThrowValidationErrorWhenTryingToCreateAnUserWithAnEmailThatAlreadyExists({ request }: Context) {
const response = await request.post('/api/v1/register', {
body: {
name: 'Test',
email: 'customer@athenna.io',
password: '12345678'
}
})

response.assertStatusCode(422)
response.assertBodyContains({
data: {
code: 'E_VALIDATION_ERROR',
message: 'Validation failure',
name: 'ValidationException',
details: [
{
field: 'email',
message: 'The email field is not unique',
rule: 'unique'
}
]
}
})
}

@Test()
public async shouldBeAbleToConfirmUserAccount({ assert, request }: Context) {
const user = await User.factory().create({ token: Uuid.generate(), emailVerifiedAt: null })
Expand Down

0 comments on commit 87286aa

Please sign in to comment.