npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@stopsopa/validator

v0.0.116

Published

![example workflow](https://github.com/stopsopa/validator/actions/workflows/playwright.yml/badge.svg) [![npm version](https://badge.fury.io/js/%40stopsopa%2Fvalidator.svg)](https://badge.fury.io/js/%40stopsopa%2Fvalidator) [![NpmLicense](https://img.shiel

Downloads

169

Readme

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:

  • https://symfony.com/doc/current/components/validator.html
  • https://beanvalidation.org/1.0/spec/

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