@edgeguideab/expect
v9.0.0
Published
Check for user input in a consistent way and generate error messages for missings
Downloads
221
Readme
Zero dependencies
Can be used in both browser and server environments
TypeScript support, infers types for the input according to the validation schema
Installation
npm install @edgeguideab/expect
Note: The library is transpiled to ES5 and tested with Node LTS, older JavaScript environments may require shims/polyfills.
function expect(
schema: object, // Object for the validation schema
input: unknown // Input to validate according to the schema
): {
isValid: boolean; // Indicates whether the input passed the validation
getParsed(): object; // Returns parsed input for the properties that passed the validation
errors(): object; // Returns errors for the properties that failed the validation
};
// ES module import
import expect from "@edgeguideab/expect";
// CommonJS
const expect = require("@edgeguideab/expect");
function addUser(req, res) {
const validation = expect(
{ username: "string", age: "number", hasAcceptedTerms: "boolean" },
req.body
);
if (!validation.isValid) {
console.log(validation.errors());
return res.status(400).send();
}
const { username, age, hasAcceptedTerms } = validation.getParsed();
// Types for username, age and hasAcceptedTerms are valid and inferred
}
Validation schema
The first argument for expect
is an object that specifies how to validate the input.
Types are specified in the schema either with a string or with an object that has the type
property. Using a string will apply the default validation, while an object can be used to customize the validation with various options.
import expect from "@edgeguideab/expect";
expect(
{
foo: "number",
bar: {
type: "number",
condition: (bar) => bar > 300,
},
},
{
foo: 123,
bar: 321,
}
).isValid; // true
Types
| Type | Default validation | Custom options |
| ------------- | --------------------------------------------------------------- | ---------------------------------------------- |
| any | value != null && value !== ""
| N/A |
| number | typeof x === "number" && !Number.isNaN(x)
| N/A |
| boolean | typeof x === "boolean"
| N/A |
| string | typeof x === "string"
| sanitize, allowed, blockUnsafe, strictEntities |
| array | Array.isArray(x)
| items, convert |
| object | typeof x === "object" && x !== null && !Array.isArray(x)
| keys, strictKeyCheck |
| date | Valid Date instance, or a valid string for the Date constructor | N/A |
Note that null, undefined and empty string are not allowed by default. These will be referred to as "null values" and their validation can be customized by using the allowNull
or requiredIf
option.
Options
The validation can be configured using options when the default behavior does not sufffice.
The errorCode
option can be used to customize the messages returned by errors()
for each property that fails the validation. Some options have a corresponding errorCode option, these can be combined but beware of their priorities.
The allowNull
option is available for all types and is disabled by default.
The allowNull
accepts a boolean or a function that takes the input value as its argument and returns a boolean. If the function throws an error, it will be ignored and treated as false. A function may be used to filter which null values are allowed, see the example below.
import expect from "@edgeguideab/expect";
expect(
{
foo: { type: "string", allowNull: (foo) => foo !== "" },
bar: { type: "number", allowNull: true },
},
{ bar: "" }
).isValid; // true
expect(
{
foo: { type: "string", allowNull: true },
bar: { type: "number", allowNull: (bar) => bar !== "" },
},
{ bar: "" }
).isValid; // false
The requiredIf
option is available for all types. When set, it allows a property to be a null value if another property is also a null value. Note that allowNull
has a higher priority than requiredIf
.
import expect from "@edgeguideab/expect";
expect(
{
foo: { type: "string", allowNull: true },
bar: { type: "string", requiredIf: "foo" },
},
{}
).isValid; // true
expect(
{
foo: { type: "string", allowNull: true },
bar: { type: "string", requiredIf: "foo" },
},
{ foo: "test" }
).isValid; // false
expect(
{
foo: { type: "string", allowNull: true },
bar: { type: "string", allowNull: true, requiredIf: "foo" },
},
{ foo: "test" }
).isValid; // true (requiredIf is redundant when allowNull is true)
When using requiredIf
on nested objects or arrays, the option takes an array with the path to the target parameter.
import expect from "@edgeguideab/expect";
expect(
{
foo: {
type: "object",
keys: { buzz: { type: "string", allowNull: true } },
},
bar: { type: "string", requiredIf: ["foo", "buzz"] },
},
{
foo: { buzz: null },
bar: null,
}
).isValid; // true
The parse
option is available to all types. This option allows the user to mutate input values before the values are validated and returned by getParsed()
.
The parse
option may use a function with the original input value as its parameter, the return value will then be used for type checking instead of the original input value. Any errors thrown will be ignored and the type checker will proceed using the original input value.
import expect from "@edgeguideab/expect";
expect(
{ test: { type: "number", parse: (test) => Number(test) } },
{ test: "123" }
).getParsed(); // { test: 123 }
Some types support setting the parse
option to true which will use the following default type conversions:
number
-Number()
, only for non-empty stringsboolean
-!!JSON.parse()
- Strings "undefined" and "NaN" are also parsed to false
- Fallback on coercing the initial value if JSON.parse() fails.
string
-JSON.stringify()
array
-JSON.parse()
object
-JSON.parse()
date
-new Date()
Note that parse
has a particular interaction with the allowNull
and requiredIf
options.
- If null values are not allowed,
parse
will not be applied for a null value - If null values are allowed,
parse
will be applied. The parsed value must either be a null value or matching the type parse
will not be applied for the target parameter whenrequiredIf
checks the value of the target path
import expect from "@edgeguideab/expect";
const invalid = expect(
{ test: { type: "string", allowNull: false, parse: true } },
{ test: null }
);
invalid.isValid; // false
invalid.getParsed(); // {}
const valid = expect(
{ test: { type: "string", allowNull: true, parse: true } },
{ test: null }
);
valid.isValid; // true
valid.getParsed(); // { test: 'null' }
const alsoValid = expect(
{ test: { type: "string", allowNull: true, parse: () => null } },
{ test: "test" }
);
alsoValid.isValid; // true
alsoValid.getParsed(); // { test: null }
const anotherOne = expect(
{
test: { type: "string", requiredIf: "existing" },
existing: { type: "string", allowNull: true, parse: () => "test" },
},
{ test: null, existing: null }
);
anotherOne.isValid; // true
anotherOne.getParsed(); // { test: null, existing: 'test' }
equalTo
is another option available to all types. It ensures that the input value matches another value specified by a key.
import expect from "@edgeguideab/expect";
expect(
{
foo: { type: "boolean", equalTo: "bar" },
bar: "boolean",
},
{ foo: true, bar: true }
).isValid; // true
expect(
{
foo: { type: "boolean", parse: true, equalTo: "bar" },
bar: "boolean",
},
{ foo: "true", bar: true }
).isValid; // true
expect(
{
foo: { type: "boolean", equalTo: "bar" },
bar: "boolean",
},
{ foo: true, bar: false }
).isValid; // false
expect(
{
foo: { type: "boolean", allowNull: true, equalTo: "bar" },
bar: { type: "boolean", allowNull: true },
},
{ foo: null, bar: null }
).isValid; // true
Note that when using the keys/items options when nestling objects/arrays, you need to provide an array with the path to the other parameter.
import expect from "@edgeguideab/expect";
expect(
{
foo: { type: "object", keys: { buzz: "string" } },
bar: { type: "string", equalTo: ["foo", "buzz"] },
},
{
foo: { buzz: "abc" },
bar: "abc",
}
).isValid; // true
The condition
option is available for all types. Passing a function as a condition
option will test that the function evaluates to a truthy value with the input value as its parameter.
import expect from "@edgeguideab/expect";
expect(
{
foo: {
type: "array",
condition: (test) => test.length,
},
},
{ foo: [] }
).isValid; // false
Note that the condition
option has a lower priority than allowNull
, requiredIf
and parse
.
import expect from "@edgeguideab/expect";
expect(
{
foo: {
type: "array",
condition: (test) => test !== null,
allowNull: true,
},
},
{ foo: null }
).isValid; // true
expect(
{
foo: {
type: "boolean",
parse: (foo) => !!foo,
condition: (foo) => typeof foo !== "string",
},
},
{ foo: "bar" }
).isValid; // true
If the keys
option is provided, each property of the input object can be evaluated.
import expect from "@edgeguideab/expect";
expect(
{
foo: "object",
bar: {
type: "object",
keys: { fizz: "number", buzz: "string" },
},
},
{
foo: { bizz: 1 },
bar: { fizz: 1, buzz: 1 },
}
).errors(); // { bar: { buzz: 'Expected parameter bar.buzz to be of type string but it was 1' } }
Object validation may be nested with several keys-options.
import expect from "@edgeguideab/expect";
expect(
{
bar: {
type: "object",
keys: {
fizz: "number",
buzz: { type: "object", keys: { bizz: "number" } },
},
},
},
{ bar: { fizz: 1, buzz: { bizz: "hello" } } }
).errors(); // { bar: { buzz: { bizz: 'Expected parameter bar.buzz.bizz to be of type number but it was "hello"' } } }
import expect from "@edgeguideab/expect";
expect(
{
bar: {
type: "object",
strictKeyCheck: true,
keys: {
fizz: "number",
buzz: { type: "object", keys: { bizz: "number" } },
},
},
},
{
bar: {
fizz: 1,
buzz: { bizz: 2 },
kizz: 3,
},
}
).errors(); // { bar: 'Object contained unchecked keys "kizz"' }
items
is available for the array
type to validate each item within the array. Arrays and objects may be nested by combining the items
and keys
options.
import expect from "@edgeguideab/expect";
expect(
{
beef: {
type: "array",
items: {
type: "object",
keys: { foo: "number", bar: "string" },
},
},
},
{
beef: [
{ foo: 1, bar: "1" },
{ foo: 2, bar: "2" },
{ foo: 3, bar: "3" },
{ foo: 4, bar: "4" },
],
}
).isValid; // true
A function may be used as an items
option. The function will be passed the input array as its parameter and must return a validation schema.
import expect from "@edgeguideab/expect";
const schema = {
beef: {
type: "array",
items: (item) => ({
type: "object",
keys: {
foo: item.bar ? "number" : "string",
bar: "boolean",
},
}),
},
};
expect(schema, {
beef: [
{ foo: 1, bar: true },
{ foo: 2, bar: true },
],
}).isValid; // true
expect(schema, {
beef: [
{ foo: "1", bar: false },
{ foo: "2", bar: false },
],
}).isValid; // true
expect(schema, {
beef: [
{ foo: "1", bar: true },
{ foo: "2", bar: true },
],
}).isValid; // false
A function can also be used for recursive validation schemas.
import expect from "@edgeguideab/expect";
const schema = {
type: "object",
keys: {
value: "string",
branches: {
type: "array",
allowNull: true,
items: () => schema,
},
},
};
expect(
{ root: schema },
{
root: {
value: "foo",
branches: [
{ value: "bar" },
{ value: "bizz", branches: [{ value: "buzz" }] },
],
},
}
).isValid; // true
convert
is only available for the array type. Similar to parse
, this option will try to parse the given value into the desired type. Typically useful for parsing arrays from the request query in Express.js.
blockUnsafe
is only available for the string type. If true, the validation will fail if the value contains unsafe characters that can be used for XSS injections. In non-strict mode, these characters are
& < > " '
, and with the strictEntities
option enabled they are & < > " ' ! @ $ ( ) = + { } [ ]
.
import expect from "@edgeguideab/expect";
expect(
{ test: { type: "string", blockUnsafe: false } },
{ test: "<div>Some html</div>" }
).isValid; // true
expect(
{ test: { type: "string", blockUnsafe: true } },
{ test: "<div>Some html</div>" }
).isValid; // false
strictEntities
is only available for the string type and only works in combination with blockUnsafe
and/or sanitize
.
If strictEntities
is true, the validation will fail if the value contains & < > " ' ! @ $ ( ) = + { } [ ]
, instead of the default restricted characters & < > " '
.
import expect from "@edgeguideab/expect";
expect(
{ test: { type: "string", blockUnsafe: true } },
{ test: "This is not so unsafe in non-strict mode!" }
).isValid; // true
expect(
{ test: { type: "string", blockUnsafe: true, strictEntities: true } },
{ test: "But it is not safe in strict mode!" }
).isValid; // false
sanitize
is only available for the string type and can be used to replace dangerous characters with html entities. In non-strict mode, these characters are
& < > " '
, and with the strictEntities
option enabled they are & < > " ' ! @ $ ( ) = + { } [ ]
.
The original values will be kept as-is, and the sanitized value will can be retrieved using the getParsed method.
import expect from "@edgdeguideab/expect";
expect(
{ test: { type: 'string', sanitize: true } },
{ test: '<div>Some html</div>' } }
).getParsed(); // { test: '<div>Some html</div>' }
import expect from "@edgeguideab/expect";
expect(
{ test: { type: "string", sanitize: true } },
{ test: "This will be kept as-is in non-strict mode!" }
).getParsed(); // { test: 'This will be kept as-is in non-strict mode!' }
expect(
{ test: { type: "string", sanitize: true, strictEntities: true } },
{ test: "But sanitized in strict mode!" }
).getParsed(); // { test: 'But sanitized in strict mode!' }
allowed
is only available for the string type and only works in combination with blockUnsafe
and/or sanitize
.
To explicitly allow some characters, allowed
can be passed an array of
characters that will not be sanitized or blocked.
import expect from "@edgeguideab/expect";
expect(
{
test: {
type: "string",
sanitize: true,
strictEntities: true,
allowed: ["(", ")"],
},
},
{ test: "keep (some) of this as it is [test]" }
).getParsed(); // { test: 'keep (some) of this as it is [test]'}
The errorCode
option is available for all types and configures the message returned by errors()
if the validation fails.
errorCode
has the lowest priority of all the errorCode options (errorCode
is used as a fallback).
import expect from "@edgeguideab/expect";
expect(
{
bar: { type: "string" },
},
{ bar: {} }
).errors(); // { bar: 'Expected parameter bar to be of type string but it was {}' }
expect(
{
bar: { type: "string", errorCode: "Invalid format" },
},
{ bar: {} }
).errors(); // { bar: 'Invalid format' }
Custom error message if the error was caused by the allowNull
(or requiredIf
) option.
Errors caused by allowNull
have the highest priority.
Custom error message if the error was caused by the blockUnsafe
option.
Errors caused by blockUnsafe
have the second highest priority.
Custom error message if the error was caused by the equalTo
option.
Errors caused by equalTo
have the third highest priority.
Custom error message if the error was caused by the condition
option.
Errors caused by condition
have the fourth highest priority.