Skip to content

Instantly share code, notes, and snippets.

@dgonzalez
Last active October 14, 2019 14:49
Show Gist options
  • Save dgonzalez/d1ce21f6f280e69a7969dbb03c2909d0 to your computer and use it in GitHub Desktop.
Save dgonzalez/d1ce21f6f280e69a7969dbb03c2909d0 to your computer and use it in GitHub Desktop.
joi-validators

joi-decorators

What do we want?

Something similar to (roughly, not even syntactically ok):

------
class User {
  @Min(10)
  @Max(10)
  @NotNull
  private _name: string
}
------ or
class User {
  private _name: string

  @Min(10)
  @Max(10)
  @NotNull
  set name (): string {

  }
}

validator = new JoiValidator(...) // <- this is what we want.

user:User = new User(...)

errors:Error = validator.validate(user)

for (let error:Error of errors) {
  console.log(error)
}

@remojansen
Copy link

remojansen commented Dec 4, 2017

From JOI:

const Joi = require('joi');
 
const schema = Joi.object().keys({
    username: Joi.string().alphanum().min(3).max(30).required(),
    password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/),
    access_token: [Joi.string(), Joi.number()],
    birthyear: Joi.number().integer().min(1900).max(2013),
    email: Joi.string().email()
}).with('username', 'birthyear').without('password', 'access_token');
 
// Return result.
const result = Joi.validate({ username: 'abc', birthyear: 1994 }, schema);
// result.error === null -> valid
 
// You can also pass a callback which will be called synchronously with the validation result.
Joi.validate({ username: 'abc', birthyear: 1994 }, schema, function (err, value) { });  // err === null -> valid

What we want is:

import { Min, Max, NotNull, validate } from "joi-decorators";

class User {
  @Min(10)
  @Max(10)
  @NotNull
  private _name: string
}

const user:User = new User(...);

const result = validate(user); // the same result returned by Joi.validate

And this is how we can implement it:

import * as Joi from "joi";

export function validate<T>(instance: T) {
    const metadata = Reflect.getMetadata("validation", instance.prototype.constructor);
    const schema = _createSchemaFromMetadata(metadata);
    return Joi.validate(instance, schema);
}

function _createSchemaFromMetadata(metadata) {
    // "la chicha" goes here
}

@remojansen
Copy link

remojansen commented Dec 4, 2017

We can actually make this much more simple (and powerful):

class User {
    @validate(Joi.string().alphanum().min(3).max(30).required())
    username: string;
    @validate(Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/))
    password: string,
    @validate([Joi.string(), Joi.number()])
    access_token: [string, number],
    @validate(Joi.number().integer().min(1900).max(2013))
    birthyear: number,
    @validate(Joi.string().email())
    email: string;
}

This generates metatada:

[
    { key: "username", val: Joi.string().alphanum().min(3).max(30).required() },
    { key: "password", val: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/) },
    { key: "access_token", val: [Joi.string(), Joi.number()] },
    { key: "birthyear", val: Joi.number().integer().min(1900).max(2013) },
    { key: "email", val: Joi.string().email() }
]

The real implementation will use a ES6 Map not an array...

The function _createSchemaFromMetadata becomes then very simple 👍

The only missing bit is .with('username', 'birthyear').without('password', 'access_token'); but we I think .required() gets you the same.

@dgonzalez
Copy link
Author

Oh, that is a good idea... I've been too long in Java. On that way we support out of the box all the joi constraints and we dont need to make changes if there are new.

@remojansen
Copy link

Amazing project and can be done in one evening 🤣 TypeScript rules 💯

@dgonzalez
Copy link
Author

I can see a problem with it... we are then tied to Joi which it is fine because it is a de-facto standard but we need to compromise on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment