@stopsopa/validator

0.0.116 • Public • Published

example workflow npm version NpmLicense

Table of Contents

(TOC generated using markdown-toc)

Motivation

I haven't found good enough implementation of JSR-303 Bean Validation for javascript, so here we go:

Main goals during implementation of this library was:

  • simple and robust architecture
  • asynchronous behaviour (due to asynchronous nature of javascript)
  • extendability (custom asynchronous validator)
  • validation of any data structure and easyness in use (guaranteed by following JSR-303)
  • well tested (different node versions and browsers - done with "jest" and "karma") for polymorphic use on server and in the browser

Feel free to contribute.



Loosely inspired by:

Live example:

https://codesandbox.io/s/ymwky9603j

Simple example:

import validator, {
  Required,
  Optional,
  Collection,
  All,
  Blank,
  Callback,
  Choice,
  Count,
  Email,
  IsFalse,
  IsNull,
  IsTrue,
  Length,
  NotBlank,
  NotNull,
  Regex,
  Type,
  ValidatorLogicError,
} from "@stopsopa/validator";

(async () => {
  const errors = await validator(
    {
      name: "",
      surname: "doe",
      email: "",
      terms: false,
      comments: [
        {
          comment: "What an ugly library",
        },
        {
          comment: "empty",
        },
      ],
    },
    new Collection({
      name: new Required([new NotBlank(), new Length({ min: 3, max: 255 })]),
      surname: new Required([new NotBlank(), new Length({ min: 10, max: 255 })]),
      email: new Required(new Email()),
      terms: new Optional(new IsTrue()),
      comments: new All(
        new Collection({
          comment: new Required(new Length({ min: 10 })),
        })
      ),
    })
  );

  if (errors.count()) {
    // ... handle errors

    console.log(JSON.stringify(errors.getFlat(), null, 4));
    // {
    //     "name": "This value should not be blank.",
    //     "surname": "This value is too short. It should have 10 characters or more.",
    //     "email": "This value is not a valid email address.",
    //     "terms": "This value should be true.",
    //     "comments.1.comment": "This value is too short. It should have 10 characters or more."
    // }

    console.log(JSON.stringify(errors.getTree(), null, 4));
    // {
    //     "name": "This value should not be blank.",
    //     "surname": "This value is too short. It should have 10 characters or more.",
    //     "email": "This value is not a valid email address.",
    //     "terms": "This value should be true.",
    //     "comments": {
    //         "1": {
    //             "comment": "This value is too short. It should have 10 characters or more."
    //         }
    //     }
    // }
  }
})();

Some basic facts about functioning of the validator

  • validator() don't care if some validation errors will occur or not, it will just count them and return two methods to extract them in different formats (as it is visible in above example)
  • validator() always return a promise. Rejected promise returned when special ValidatorLogicError() is thrown in Callback type validator only. Only this kind of error is different because it's not "validation error" but actual error in the process of validation - that's a different thing. Usually it's not something user can "fix" in his form or in his UI -> this is rather system error that should be logged and addressed by developers.
  • normally all validators are executed in single Promise.allSettled() but there is a way to group sets of validators into separate Promise.allSettled() (using integer "async" extra flag) and execute those groups one by one. This is where another "extra" flag called "stop" of individual validators comes handy because turning it ON on particular validator will result in not executing next Promise.allSettled() in case when error was detected by that single validator -> so returning resolved or rejected promise from individual validators together with stearing it through flag "stop" serves rather as an flow control mechanizm.
  • read Conclusions section of this readme

Example

Entity manager

const abstract = require("@stopsopa/knex-abstract");

const extend = abstract.extend;

const prototype = abstract.prototype;

const log = require("inspc");

const a = prototype.a;

const {
  Collection,
  All,
  Required,
  Optional,
  NotBlank,
  Length,
  Email,
  Type,
  IsTrue,
  Callback,
  Regex,
} = require("@stopsopa/validator");

const ext = {
  initial: async function () {
    return {
      updated: this.now(),
      created: this.now(),
      port: 80,
    };
  },
  toDb: (row) => {
    return row;
  },
  update: function (...args) {
    let [debug, trx, entity, id] = a(args);

    delete entity.created;

    entity.updated = this.now();

    return prototype.prototype.update.call(this, debug, trx, entity, id);
  },
  insert: async function (...args) {
    let [debug, trx, entity] = a(args);

    entity.created = this.now();

    delete entity.updated;

    const id = await prototype.prototype.insert.call(this, debug, trx, entity);

    return id;
  },
  prepareToValidate: function (data = {}, mode) {
    delete data.created;

    delete data.updated;

    return data;
  },
  getValidators: function (mode = null, id, entity) {
    const validators = {
      id: new Optional(),
      cluster: new Required([
        new NotBlank(),
        new Length({ max: 50 }),
        new Callback(
          (value, context, path, extra) =>
            new Promise(async (resolve, reject) => {
              const { cluster, node, id } = context.rootData;

              const condition = node === null ? "is" : "=";

              let c;

              log(mode);

              if (mode === "create") {
                c = await this.queryColumn(
                  true,
                  `select count(*) c from :table: where cluster = :cluster and node ${condition} :node`,
                  {
                    cluster,
                    node,
                  }
                );
              } else {
                c = await this.queryColumn(
                  true,
                  `select count(*) c from :table: where cluster = :cluster and node ${condition} :node and id != :id`,
                  {
                    cluster,
                    node,
                    id,
                  }
                );
              }

              log.dump(c);

              const code = "CALLBACK-NOTUNIQUE";

              if (c > 0) {
                context
                  .buildViolation("Not unique")
                  .atPath(path)
                  .setParameter("{{ callback }}", "not equal")
                  .setCode(code)
                  .setInvalidValue(`cluster: '${cluster}' and node: '${node}'`)
                  .addViolation();

                if (extra && extra.stop) {
                  return reject("reject " + code);
                }
              }

              resolve("resolve " + code);
            })
        ),
      ]),
      domain: new Required([new NotBlank(), new Length({ max: 50 })]),
      port: new Required([new NotBlank(), new Length({ max: 8 }), new Regex(/^\d+$/)]),
    };

    if (typeof entity.node !== "undefined") {
      if (entity.node === null) {
        validators.node = new Optional();
      } else {
        validators.node = new Required([new NotBlank(), new Length({ max: 50 })]);
      }
    }

    return new Collection(validators);
  },
};

module.exports = (knex) => extend(knex, prototype, Object.assign({}, require("./abstract"), ext), "clusters", "id");

Controller

const knex          = require('@stopsopa/knex-abstract');

const log           = require('inspc');

const validator     = require('@stopsopa/validator');
    ...
    app.all('/register', async (req, res) => {

        let entity              = req.body;

        let id                  = entity.id;

        const mode              = id ? 'edit' : 'create';

        const man               = knex().model.clusters;

        const validators        = man.getValidators(mode, id);

        if (mode === 'create') {

            entity = {
                ...man.initial(),
                ...entity,
            };
        }

        const entityPrepared    = man.prepareToValidate(entity, mode);

        const errors            = await validator(entityPrepared, validators);

        if ( ! errors.count() ) {

            try {

                if (mode === 'edit') {

                    await man.update(entityPrepared, id);
                }
                else {

                    id = await man.insert(entityPrepared);
                }

                entity = await man.find(id);

                if ( ! entity ) {

                    return res.jsonError("Database state conflict: updated/created entity doesn't exist");
                }
            }
            catch (e) {

                log.dump(e);

                return res.jsonError(`Can't register: ` + JSON.stringify(req.body));
            }
        }

        return res.jsonNoCache({
            entity: entity,
            errors: errors.getTree(),
        });

    });
    ...

For further examples please follow test cases

Validators references

Blank

Source code Blank.js

new Blank({
  message: "This value should be blank.",
});

Callback

Source code Callback.js

See test example Callback.test.js

new Callback((value, context, path, extra) => {...}); // function required

Choice

Source code Choice.js

new Choice({
  choices: ["..."], // required

  multiple: false,
  min: 0, // only if multiple=true
  max: 0, // only if multiple=true

  message: "The value you selected is not a valid choice.",
  multipleMessage: "One or more of the given values is invalid.",
  minMessage: "You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.",
  maxMessage: "You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.",
});

// or shorter syntax if ony choices are given:

new Choice(["..."]); // just choices

Collection

Source code Collection.js

new Collection({
  fields: {
    // required type: non empty object
    a: new Require(),
    b: new Optional(),
  },
  allowExtraFields: false,
  allowMissingFields: false,
  extraFieldsMessage: "This field was not expected.",
  missingFieldsMessage: "This field is missing.",
});

// or shorter syntax if only fields are given:

new Collection({
  // required type: non empty object
  a: new Require(),
  b: new Optional(),
});

Count

Source code Count.js

new Count({
  // min; // min or max required (or both) - if min given then have to be > 0
  // max, // min or max required (or both) - if max given then have to be > 0

  minMessage:
    "This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.",
  maxMessage:
    "This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.",
  exactMessage:
    "This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.",
});

// or shorter syntax if ony min and max given and min = max:

new Count(5);

Email

Source code Email.js

new Email({
  message: "This value is not a valid email address.",
});

IsFalse

Source code IsFalse.js

new IsFalse({
  message: "This value should be false.",
});

IsTrue

Source code IsTrue.js

new IsTrue({
  message: "This value should be true.",
});

IsNull

Source code IsNull.js

new IsNull({
  message: "This value should be null.",
});

Length

Source code Length.js

new Length({
  // min; // min or max required (or both)
  // max, // min or max required (or both)

  maxMessage:
    "This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.",
  minMessage:
    "This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.",
  exactMessage:
    "This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.",
});

NotBlank

Source code NotBlank.js

new NotBlank({
  message: "This value should not be blank.",
});

NotNull

Source code NotNull.js

new NotNull({
  message: "This value should not be blank.",
});

Regex

Source code Regex.js

new Regex({
  pattern: /abc/gi, // required, type regex
  message: "This value is not valid.",
  match: true, // true     - if value match regex then validation passed
  // false    - if value NOT match regex then validation passed
});

Type

Source code Type.js

// available values for field 'type' are:
// 'undefined', 'object', 'boolean', 'bool', 'number', 'str', 'string',
// 'symbol', 'function', 'integer', 'int', 'array'
new Type({
  type: "...", // required
  message: `This value should be of type '{{ type }}'.`,
});

// or shorter syntax if ony type is given:

new Type("str");

Addidional tools

require('@stopsopa/validator/set')
require('@stopsopa/validator/get')
require('@stopsopa/validator/delay')
require('@stopsopa/validator/each')
require('@stopsopa/validator/size')

Other similar libraries:

next generation

  • or validator
  • condition validator
  • respecting order of validators - executing in the same order as declared

Conclusions:

Always use types for primitives and collections:

example cases:

  • Length validator fires only if given data type is string (use Type('str') to avoid issues)
  • Collection validator validates only if given data is object (use Type('object') to avoid issues)
(async function () {
    const errors = await validator(6, new Collection({
    // collection fires only if given data is object
    // here it is integer
        a: new Type('str'),
        b: new Length({
             min: 1,
             max: 2,
          })
        ])
    }));

    const raw = errors.getRaw();

    expect(raw).toEqual([]);
    //

    done();
})();

fixed:

(async function () {
  const errors = await validator(
    undefined, // will generate error: "This value should be of type 'object'."
    // {a: '', b: 7}, // will generate error: "This value should be of type 'str'." on field "b"
    new Required([
      new Type("object"), // this solves the problem on that level
      new Collection({
        a: new Type("str"),
        b: new Required([
          new Type("str"), // this solves the problem on that level
          new Length({
            min: 1,
            max: 2,
          }),
        ]),
      }),
    ])
  );

  const raw = errors.getRaw();

  expect(raw).toEqual([[undefined, "This value should be of type 'object'.", "INVALID_TYPE_ERROR", undefined]]);

  done();
})();

with above the question might come "why Collection itself don't validate if field is an object?". The thing is that Collection can be used to check object but also array element by element, so it is better to deal with checking the type and validation of that structure separately.

Don't relay on new Optional on the root level, more about: option_require_case.test.js

Readme

Keywords

Package Sidebar

Install

npm i @stopsopa/validator

Weekly Downloads

5

Version

0.0.116

License

MIT

Unpacked Size

152 kB

Total Files

44

Last publish

Collaborators

  • stopsopa-owner