Security News
JSR Working Group Kicks Off with Ambitious Roadmap and Plans for Open Governance
At its inaugural meeting, the JSR Working Group outlined plans for an open governance model and a roadmap to enhance JavaScript package management.
Runtime Verification Library for browsers and Node.js.
This library is isomorphic: It runs equally well in modern browsers and on the server with Node.js.
The latest versions of major browsers, and maintained Node.js releases, are supported.
Give it a test drive with RunKit!
npm install rtvjs
The package's /dist
directory contains two types of builds:
./cjs/rtv[.dev].js
: CJS loader, full (.dev) and minified. This is the main package export../umd/rtv[.dev].js
: UMD loader, full (.dev) and minified.The CJS build can be used like this:
const rtv = require('rtvjs');
Note that the main package export points to ./dist/cjs/index.js
which will automatically select the right build to include based on the value of process.env.NODE_ENV
: If it's "production"
, it will include the minified version; otherwise, it will include full version.
Use the Webpack Define Plugin or the Rollup Replace Plugin, for example, to configure this in your build.
The UMD build can be used like this:
// as a CommonJS module (e.g. Node.js)
const rtv = require('rtvjs');
// as an AMD module (e.g. RequireJS)
define(['rtvjs'], function(rtv) {
});
// as a global, when loaded via a <script> tag in HTML
window.rtv;
To provide an easy, intuitive way to perform validations at runtime on values whenever they cross the boundaries of an API or a function call.
Tools like TypeScript and Flow are useful for static analysis (i.e. as code is being written and then transpiled to regular JavaScript), but they come at a price and they don't work at runtime.
For example, they can't signal when there are integration issues between frontend and backend systems that are being co-developed. In one conversation, an API may be designed to return an object with certain properties. Later on, an on-the-fly decision to alter the implementation (yes, it happens in spite of the best intentions and processes), or simply a bug in the implementation, may result in an object that is missing an expected property, or has a property with an unexpected value.
Let's consider a case where a "state" property, which is really an enumeration of string values, ends-up set to an unexpected state. What should a client do with an unexpected state when there's no implementation to back it up? Ignoring it could be an option, but perhaps not the best course of action. Even worse, the unexpected state somehow could trickle deep down into code before it finally causes an exception, making it really difficult to find the true source of the problem.
RTV.js can help signal the unexpected state by failing early, right at the API boundary:
async function getTodoList() {
const response = await fetch('/api/todos');
const json = await response.json();
// verify (require) that the response be a list of TODO items: this function
// will throw if `json` doesn't meet the specified typeset (requirement)
rtv.verify(json, [[{ // list of objects (could be empty)
// non-empty string
title: rtv.STRING,
// 'YYYY-MM-DD', or null
due: [rtv.EXPECTED, rtv.STRING, {exp: '\\d{4}-\\d{2}-\\d{2}'}],
// string (could be empty), null, or not even defined
note: [rtv.OPTIONAL, rtv.STRING]
}]]);
return json;
}
There may also be a need to ensure that a critical function call is being given the parameters it expects. Rather than write a series of if (!state) { throw new Error('state is required'); }
(which don't tell us much about what "state" is expected to be, other than it's required), it would be more helpful to have an easy way to express that "state" should be a non-empty string with a value in a given list (i.e. a value found in an enumeration).
RTV.js can help signal the unexpected state immediately when execution enters the function:
function applyState(state) {
rtv.verify(state, [rtv.STRING, {oneOf: ['on', 'off']}]);
if (state === 'on') {
// turn the lights on
} else {
// turn the lights off
}
}
applyState('on'); // ok
applyState('dimmed'); // ERROR
While tools like TypeScript and Flow have their merits, they come at a price. Typings or not, integration issues will remain. RTV.js allows you to check for types at runtime, when it really matters, and has a simple API so it's easy to learn.
The following statement verifies that the variable "state" is a non-empty string whose value is found in a list of permitted values:
rtv.verify(state, [rtv.STRING, {oneOf: ['on', 'off']}]);
The [rtv.STRING, {oneOf: ['on', 'off']}]
portion of the example above is called a typeset. It expresses the expectation for the value of the "state" variable.
Typesets must be:
[]
for lists and complex typesets, and inline objects {}
for shapes (i.e. interfaces).JSON.stringify()
so they can be easily transferred between systems.
@context
property of a JavaScript Object for JSON-LD, an object's expected shape could be transferred along with the object itself.ctor
property of shape object arguments.Tutorials and example uses of the RTV.js library.
RTV.js provides two functions for verifying values against typesets. A typeset is simply a set of one or more types that form an expectation about the value:
rtv.verify(value, typeset); // will throw an error if verification fails
rtv.check(value, typeset); // returns the error instead of throwing it
Typesets can be strings, objects (shapes), functions (custom validators), or Arrays (multiple possibilities).
At their simplest, typesets are strings that represent type names like STRING
, INT
, DATE
, etc. See the full list of types here.
rtv.verify('Hello world!', rtv.STRING); // ok
rtv.verify('', rtv.STRING); // ERROR: a required string cannot be empty
The first verification succeeds because the value is a non-empty string. The second one fails because the typeset uses the default qualifier, which is REQUIRED
. A required string cannot be empty (nor can it be null
or undefined
).
In some implementations, an empty string is considered a bad value because it's a falsy value in JavaScript, just like null
, undefined
, false
, 0
, and NaN
.
There are 3 other qualifiers, EXPECTED
, OPTIONAL
, and TRUTHY
. A typeset may only have one qualifier, and it must be specified before any types.
The only way to specify an alternate qualifier is to use an Array to describe the typeset: [<qualifier>, types...]
If we wanted to accept an empty string (or null
) as the value, we could use the EXPECTED
qualifier:
rtv.verify('Hello world!', [rtv.EXPECTED, rtv.STRING]); // ok
rtv.verify('', [rtv.EXPECTED, rtv.STRING]); // ok
rtv.verify(null, [rtv.EXPECTED, rtv.STRING]); // ok
If we had a variable which we expect to be an object whenever it's value is truthy, we could use the TRUTHY
qualifier, which would permit any falsy value, but require the value to be of a specified type otherwise:
let objectOrFalsy = false;
rtv.verify(objectOrFalsy, [rtv.TRUTHY, rtv.PLAIN_OBJECT]); // ok
objectOrFalsy = {hello: 'world!'};
rtv.verify(objectOrFalsy, [rtv.TRUTHY, rtv.PLAIN_OBJECT]); // ok
rtv.verify(objectOrFalsy, [rtv.TRUTHY, rtv.ARRAY]); // ERROR: value is not an array
// similar to how the following code would either print "world!" or not get executed
// depending on the truthiness of `objectOrFalsy`
if (objectOrFalsy) {
console.log(objectOrFalsy.hello);
}
Some types accept arguments. Arguments are simple objects that map argument names to values, and immediately follow a type in a typeset. Once again, an Array must be used to describe the typeset. Type arguments are optional, unless otherwise stated; some types don't accept arguments.
The STRING
type accepts arguments, one of which is min
. It lets us specify the minimum length of the string. By default, when the qualifier is REQUIRED
, min
defaults to 1, but we can override that:
rtv.verify('Hello world!', [rtv.STRING, {min: 0}]); // ok
rtv.verify('', [rtv.STRING, {min: 0}]); // ok
rtv.verify(null, [rtv.STRING, {min: 0}]); // ERROR
This verifies the value cannot be null
or undefined
because of the (implied) REQUIRED
qualifier. However, it could be empty because the min
argument allows a zero-length string as the value.
So far, we've seen simple typesets: Either just a string as the type name, or the type name and some arguments, and an optional qualifier that precedes it. There may be cases where a value could be one of multiple types. To verify against additional types, an Array is used to state all the possibilities: [<qualifier>, <type1>, <type1-args>, <type2>, <type2-args>, ...]
. This is called an "Array typeset", which we've already seen in the two previous sections.
Since a value can only be of a single type at any given time, Array typesets are evaluated using a short-circuit OR conjunction, which means the verification will pass as long as at least one type verifies the value (and verification will stop evaluating any other types against the value once a match is made).
For example, we could verify that a value is either a boolean, or a string that looks like a boolean:
const typeset = [rtv.BOOLEAN, rtv.STRING, {
exp: '^(?:true|false)$',
expFlags: 'i'
}];
rtv.verify(true, typeset); // ok
rtv.verify('true', typeset); // ok
rtv.verify('True', typeset); // ok
rtv.verify('TRUE', typeset); // ok
rtv.verify(false, typeset); // ok
rtv.verify('false', typeset); // ok
Since the check for the
BOOLEAN
type is faster than evaluating a regular expression against a string, we list theBOOLEAN
type first in the typeset.
It's worth pointing out here that:
The same type can appear multiple times in the same typeset.
This is very useful when a value could be of one or another set of values.
For example, we could verify that a value is a finite number in two different ranges:
const typeset = [
rtv.FINITE, {min: 0, max: 9},
rtv.FINITE, {min: 100, max: 199}
];
rtv.verify(1, typeset); // ok
rtv.verify(50, typeset); // ERROR
rtv.verify(150, typeset); // ok
rtv.verify(-1, typeset); // ERROR
rtv.verify(200, typeset); // ERROR
This is also useful for composition where you need to combine multiple smaller typesets into a larger one:
const lowerRangeTs = [rtv.FINITE, {min: 0, max: 9}];
const upperRangeTs = [rtv.FINITE, {min: 100, max: 199}];
const typeset = [...lowerRangeTs, ...upperRangeTs];
rtv.verify(1, typeset); // ok
rtv.verify(50, typeset); // ERROR
typeset
here will yield the same results as in the previous example.
Most of the time, especially when integrating with an API, you'll want to verify what you receive against an expected shape. A shape describes the interface an object is expected to have. As the term implies, an interface describes the properties, and types thereof, expected on an object while ignoring any other properties that the object may have (since the code using this object shouldn't care about them anyway).
Plain JavaScript objects are used to describe shapes, where expected property names are own-enumerable properties mapped to typesets. For example, we could describe a simple TODO item like this:
{
title: rtv.STRING, // non-empty string
created: rtv.DATE, // Date instance
priority: rtv.INT // some whole number
}
Since typesets are fully nestable/composable, we can get a bit more sophisticated by using Array typesets so we can provide arguments and different qualifiers:
{
title: rtv.STRING, // required (non-empty) title
created: [rtv.OPTIONAL, rtv.DATE], // either a TODO or just a note
priority: [rtv.INT, {oneOf: [0, 1, 2]}] // 0=none, 1=low, 2=high
}
Since shapes also represent objects, they have an implied (default) type of OBJECT. When fully-qualified (which means not using any implied typeset elements like the qualifier and type), the shape would move into the special $
argument of the OBJECT
type:
[rtv.REQUIRED, rtv.OBJECT, {$: {
title: rtv.STRING,
created: [rtv.OPTIONAL, rtv.DATE],
priority: [rtv.INT, {oneOf: [0, 1, 2]}]
}
}]
When the default object type is sufficient, it's really easy to nest shapes. Let's say our TODO item also had a note, which is an object with "text" and "updated" properties:
const {STRING, DATE, INT} = rtv;
const {EXPECTED, OPTIONAL} = rtv;
const item = {
title: 'Make Christmas Oatmeal',
due: new Date('12/25/2018'),
priority: 1,
note: {
text: 'Make 4 cups to have enough to share!',
updated: new Date('09/21/2018')
}
};
rtv.verify(item, {
title: STRING,
created: [OPTIONAL, DATE],
priority: [INT, {oneOf: [0, 1, 2]}],
note: { // <- nested shape
text: STRING, // <- required contents
updated: DATE // <- required Date
}
}); // ok
The typeset above would require a TODO item to have a "note" with a non-empty string value for "text", and a Date
instance for "updated". We could make the entire note optional, however, by expecting it to be either null
if a note wasn't provided, or the shape if one was:
const {STRING, DATE, INT} = rtv;
const {EXPECTED, OPTIONAL} = rtv;
const item = {
title: 'Make Christmas Oatmeal',
due: new Date('12/25/2018'),
priority: 1,
note: null
};
rtv.verify(item, {
title: STRING,
created: [OPTIONAL, DATE],
priority: [INT, {oneOf: [0, 1, 2]}],
note: [EXPECTED, { // <- null, or note object
text: STRING,
updated: DATE
}]
}); // ok
When the default object type is implied, this is called the shorthand syntax. For shapes, it may be used when the typeset is the shape itself, or in an Array typeset that is not fully-qualified, when a qualifier immediately precedes the shape (as we've done above for the "note" property).
Many times, an API response or a function's arguments will contain a list of values or objects. At their most basic, lists are simple JavaScript Arrays that contain values of some type. The simplest way to verify a list is homogenous is to use the shorthand syntax for the ARRAY type:
[[rtv.STRING]]
This would verify that an Array contains non-empty string values, but the Array could be empty, given the default arguments.
Note the nested Array.
What the example above defines is an Array typeset that has a single implied ARRAY
type with an element typeset of STRING
that will be applied to all elements found in the Array.
When the full notation is used, the element typeset moves into the ts
argument:
[rtv.ARRAY, {ts: [rtv.STRING]}] // same as before, but in full notation
Either form is acceptable, and either form can show-up anywhere in a typeset. Therefore, we could verify a value is either a boolean, an Array of non-empty strings, or an Array of integers like this:
[rtv.BOOLEAN, [rtv.STRING], [rtv.INT]]
A more practical example could be requiring a TODO item to have a non-empty list of notes associated with it, if "notes" isn't null
, meaning there are no notes (i.e. either "notes" is null
because there are no notes, or "notes" is an Array of note objects containing at least one note):
const {STRING, DATE, INT} = rtv;
const {EXPECTED, OPTIONAL} = rtv;
const item = {
title: 'Make Christmas Oatmeal',
due: new Date('12/25/2018'),
priority: 1,
note: null
};
const shape = {
title: STRING,
created: [OPTIONAL, DATE],
priority: [INT, {oneOf: [0, 1, 2]}],
note: [EXPECTED, ARRAY, { // <- null, or non-empty Array of notes
ts: {
text: STRING,
updated: DATE
},
min: 1 // <- require a non-empty Array when not null
}]
};
rtv.verify(item, shape); // ok
item.notes = [];
rtv.verify(item, shape); // ERROR: `notes` cannot be empty
item.notes.push({
text: 'Make 4 cups to have enough to share!',
updated: new Date('09/21/2018')
});
rtv.verify(item, shape); // ok
Finally, there may be occasions where a type, or even its arguments, aren't sufficient to verify the value. In that case, the typeset can be customized with a custom validator function.
The function on its own is considered a valid typeset, and gets an implied type of ANY, which validates anything, even
undefined
andnull
, regardless of the qualifier.
Let's say we wanted to verify that a value is a multiple of two. None of the numeric type arguments will verify that on their own, so we would need a custom validator:
function validator(value) {
const n = parseInt(value);
return (!IsNaN(n) && n % 2 === 0);
}
rtv.verify(2, validator); // ok
rtv.verify(3, validator); // ERROR
A custom validator can fail the verification either by returning a falsy value (other than undefined
), or throwing an Error
. When a falsy value is returned, a default Error
will be generated. Throwing an error with a helpful message is the recommended way to fail verification because of a custom validator:
function(value) {
const n = parseInt(value);
if (IsNaN(n) || n % 2 != 0) {
throw new Error('Not a number, or not a multiple of two.');
}
}
rtv.verify(2, validator); // ok
rtv.verify(3, validator); // ERROR (rootCause: 'Not a number...')
The error thrown by the custom validator (or the one generated by the library) will be included in the rootCause property of the failed verification results.
Custom validators are intended to be used as compliments to existing types rather than complete replacements. For example, rather than worry about parsing the value as an integer and checking to see if it's not a number, we could let RTV.js first verify the value is an integer by using an Array typeset:
const typeset = [rtv.INT, (v) => v % 2 === 0];
rtv.verify(2, typeset); // ok
rtv.verify(3, typeset); // ERROR (rootCause: 'Verification failed...')
An Array typeset may have at most one custom validator, and it must be the last element. Each sub-typeset may have its own validator. When one or more types are in the typeset, the validator is immediately invoked if one of the types matches (i.e. verifies) the value (any remaining types are ignored):
const typeset = [rtv.INT, rtv.STRING, (v) => v % 2 === 0];
// in both cases, STRING verification is skipped because INT matches first
rtv.verify(2, typeset); // ok
rtv.verify(3, typeset); // ERROR (rootCause: 'Verification failed...')
Finally, we could enhance our TODO item verification with a custom validator that verifies the created
Date is not in the past:
const {STRING, DATE, INT} = rtv;
const {EXPECTED, OPTIONAL} = rtv;
const item = {
title: 'Make Christmas Oatmeal',
due: new Date(Date.now() + 24 * 60 * 60 * 1000), // tomorrow
priority: 1,
note: null
};
const shape = {
title: STRING,
created: [
OPTIONAL,
DATE,
(v) => !v || v.getTime() >= Date.now()) // <- validator
],
priority: [INT, {oneOf: [0, 1, 2]}],
note: [EXPECTED, ARRAY, {
ts: {
text: STRING,
updated: DATE
},
min: 1
}]
};
rtv.verify(item, shape); // ok
item.due = new Date(Date.now() - 12 * 60 * 1000); // 12 hours ago
rtv.verify(item, shape); // ok
Notice how the validator must handle
null
andundefined
values because of the OPTIONAL qualifier, and is careful to return a truthy result so that the property remains optional.
RTV.js provides a configuration interface which allows checks (rtv.check(value, typeset)
) and verifications (rtv.verify(value, typeset)
) to be globally enabled or disabled:
rtv.config.enabled = false; // default: true
rtv.verify('foo', rtv.INT); // no-op, always returns RtvSuccess
rtv.check('foo', rtv.INT); // no-op, always returns RtvSuccess
But why even make the function call?
if (rtv.config.enabled) {
rtv.verify('foo', rtv.INT);
}
Even better, since rtv.enabled
is a getter that returns the value of rtv.config.enabled
, we can make this terse:
rtv.enabled && rtv.verify('foo', rtv.INT);
Finally, a JavaScript bundler that supports tree shaking (e.g. Webpack or Rollup) can be configured to completely exclude the entire code for a build. This could be handy if you're concerned about script download size over runtime checks, say, in a production build. See the Rollup example for more information.
Let's say we're building a simple TODO app. We might use the following object as representative of a "todo" item in a list:
const item = {
title: 'Make Christmas Oatmeal',
due: new Date('12/25/2018'),
priority: 1,
notes: [
{
text: 'Ingredients: Cranberries, apples, cinnamon, walnuts, raisins, maple syrup.',
updated: new Date('09/20/2018')
},
{
text: 'Make 4 cups to have enough to share!',
updated: new Date('09/21/2018')
}
]
};
We can describe this object using two shapes:
const {STRING, DATE, INT} = rtv;
const priorities = [1, 2, 3, 4]; // simple enumeration of priority levels
const shapes = {
get todo() { // 'todo' shape
return {
title: STRING,
due: DATE,
priority: [INT, {oneOf: priorities}], // use 'priorities' enum
notes: [[this.note]] // compose 'note' shape into this 'todo' shape
};
},
get note() { // 'note' shape
return {
text: STRING,
updated: DATE
};
}
};
Now we can verify that "todo" is a valid TODO item:
rtv.verify(item, shapes.todo);
The above verification will pass because "todo" meets the requirements of the shape.
Now let's change the second note in "todo" such that its "updated" property is a boolean, true
(a simple indication that the note was changed at some point -- a change that seems to make sense, but would break code that expects a Date
object to use for formatting in the UI, for example):
todo.notes[1].updated = true;
Lexically, there's no reason for this assignment to fail, but the boolean value violates what is stated in the spec for a TODO item.
If we were to run the same verification again, an exception would be thrown. The exception would be an RtvError with the following properties:
rtv.verify(item, shapes.todo);
// RtvError exception thrown from the above statement:
{
message: 'Verification failed: path="/notes/1/updated", mismatch=["!","DATE"], typeset={"title":"STRING","due":"DATE","priority":["INT",{"oneOf":[1,2,3,4]}],"notes":[[{"text":"STRING","updated":"DATE"}]]}',
path: ['notes', '1', 'updated'], // path to the property that failed verification
mismatch: ['!', 'DATE'], // fully-qualified typeset that caused the failure
typeset: {...}, // reference to "shapes.todo"
value: {...}, // reference to "todo"
...
}
The cause
property is providing us with the fully-qualified version of the nested typeset that caused the failure. The original typeset simply specified DATE
as the nested typeset for the note.updated
property.
In reality, all typesets have a qualifier, and the default qualifier is '!'
which means the value is required. Required values can neither be undefined
nor null
. Depending on the type, other restrictions may be imposed, such as the STRING type, which must also not be empty (by default).
For brevity, typesets don't always have to be fully-qualified since the default qualifier is implied when not specified. Note that a typeset must have exactly one qualifier, implied or not, but each nested typeset may have its own qualifier.
For example, some TODO items may not have due dates. However, our shape currently requires them. To handle this requirement, we could alter the nested typeset of the todo.due
property to be ['*', DATE]
This would state that the due
property is expected rather than required, which means its value could be null
(but still not undefined
). There is a third qualifier, '?'
, which would indicate the value is optional, in which case it could also be undefined
(which, in JavaScript terms, means the property could also not even exist anywhere up the prototype chain of the todo
object).
The RtvError
object can also be obtained without catching an exception thrown by using the rtv.check()
method:
rtv.check(item, shapes.todo); // returns the RtvError object
If the check was successful, an RtvSuccess would be returned instead. Since both RtvError
and RtvSuccess
objects have a common valid: boolean
property, it's easy to check for success and failure:
if (rtv.check(item, shapes.todo).valid) {
// check passed, "todo" is valid!
} else {
// check failed, ignore the item
}
Finally, we can check simple values too:
rtv.verify('1', rtv.INT); // ERROR: not an integer number
rtv.verify('', [rtv.EXPECTED, rtv.STRING]); // ok: expected strings can be null/empty
This is an advanced use of the RTV.js library. I recommend you read through the Getting Started guide or the Verifications example first.
Let's suppose we have the following shape that describes a simple note:
const {STRING, DATE} = rtv; // some types
const {EXPECTED} = rtv; // some qualifiers
const tags = ['car', 'money', 'reminder', 'grocery'];
const noteShape = {
// required, non-empty string
text: STRING,
// required Array (could be empty) of non-empty tags names from the user's
// list of "tags"
tags: [[STRING, {oneOf: tags}]],
// required Date when the note was created
created: DATE,
// expected date of update (either null, or Date)
updated: [EXPECTED, DATE]
};
Based on this shape, we can dynamically define a JavaScript class with getters and setters that ensure they are being set correctly:
const classGenerator = function(shape) {
const ctor = function(initialValues) {
// by definition, a shape descriptor is made-up of its own-enumerable
// properties, so we enumerate them
const props = Object.keys(shape);
const typesets = {}; // prop -> fully-qualified Array typeset
const values = {}; // prop -> value
let initializing = true; // true while we apply "initialValues"
props.forEach((prop) => {
typesets[prop] = rtv.fullyQualify(shape[prop]);
Object.defineProperty(this, prop, {
enumerable: true,
configurable: true, // could be false to lock this down further
get() {
return values[prop];
},
set(newValue) {
const typeset = typesets[prop].concat(); // shallow clone
if (initializing) {
// allow each property to be initially null, or as the typeset specifies
// so we don't end-up with junk data
// NOTE: in a fully-qualified typeset, the qualifier is always the
// first element
typeset[0] = EXPECTED;
}
// we assume there are no interdependencies between nested typesets
// this verification will throw an RtvError if the "newValue"
// violates the property's typeset
rtv.verify(newValue, typeset);
values[prop] = newValue;
}
});
if (initialValues && initialValues.hasOwnProperty(prop)) {
// go through the setter for verification
this[prop] = initialValues[prop];
} else {
// initialize to null
values[prop] = null;
}
});
initializing = false;
};
return ctor;
};
Now we can generate a Note class and create an instance:
const Note = classGenerator(noteShape);
const note = new Note({text: 'Hello world!'});
note.text; // "Hello world!", since it was initialized
note.created; // null, since it wasn't initialized
note.text = ''; // ERROR: "text" must be a non-empty string
Let's revisit the Note shape from the Dynamic Classes example, but we'll add one more property, tagCount
:
const {STRING, DATE, SAFE_INT} = rtv; // some types
const {EXPECTED} = rtv; // some qualifiers
const tags = ['car', 'money', 'reminder', 'grocery'];
const noteShape = {
// required, non-empty string
text: STRING,
// required Array (could be empty) of non-empty tags names from the user's
// list of "tags"
tags: [[STRING, {oneOf: tags}]],
//
tagCount: SAFE_INT, // <- NEW
//
// required Date when the note was created
created: DATE,
// expected date of update (either null, or Date)
updated: [EXPECTED, DATE]
};
The tagCount
property should always be an integer equal to the length of the tags
array. The most basic validation we could do is the above: Mark it as a SAFE_INT
. The problem is, it's not a complete validation because the following Node would pass, however it would still be invalid:
{
text: 'Buy potatoes',
tags: ['reminder', 'grocery'],
tagCount: 1, // <- does not match length of `tags` array
created: new Date(Date.now()),
updated: null
}
To address this issue, we can use the context
parameter provided to any custom validator since it provides a reference to the originalValue
being validated:
const noteShape = {
...,
// tagCount: SAFE_INT, // <- BEFORE
tagCount: [
SAFE_INT,
(value, match, typeset, context) => // <- AFTER
value === context.originalValue.tags.length
],
...
};
The first parameter, value
, is the value of the tagCount
property being validated by the typeset in which the custom validator is located. The fourth parameter, context
, provides some additional information such as the original value, that being the Node object itself (the note
object given to rtv.verify(note, typeset)
).
With this change, we now have a reactive validation, since it reacts (or adjusts) according to some of the data its given.
RTV.js is not your only choice for runtime verification of values. Here are some alternatives you should consider. Compare them to what this library offers and choose the best one to fit your needs!
Joi offers object schema validation. In the hapi ecosystem, this is commonly paired with Hoek. Note that Joi
is only supported in Node.js environments.
prop-types is useful if you're building a React app, but you can't get it to fail on purpose (it's React's support for it that causes errors in the console at runtime in a development build).
yup is useful if you want to validate an object schema (and can be a viable alternative to Joi
which is only supported on Node.js since yup
is also supported in the browser).
While it has similarities to RTV.js, the picture is very different in practice: Yup has a concept of coercions and transformations such that, for instance, yup.string().required().validateSync(1)
would not fail. Even if strict
mode is enabled (for which there's no global setting; that would be on a per-statement basis), there are cases where it would still pass! You wouldn't be the only one concerned with this (and it's never been resolved, which is fine since yup
isn't strictly focused on exact types for validations).
RTV.js, on the other hand, is specially designed to validate values against exact types, no implicit type coercions or transformations. It's either a string or it's not. That's what's needed for type validation. Otherwise, (IMO) it's useless, in terms of validation, because you can't be certain of what you have (e.g. if "1"
and 1
were the same, would value.substr()
always work...?).
Contributing, including local development guide.
See the list of proposed enhancements. Up-vote the ones you like to help contributors prioritize them!
Feel free to log an enhancement if you have an idea! You may also file a PR, although it might be best to discuss your idea with the community first by creating an enhancement issue.
2.3.1
Release date: 2020-06-09
engines
back to oldest supported Node LTS (>=10.21.0
) and the version of NPM it ships with (>=6.14.4
).FAQs
Runtime Verification Library for browsers and Node.js.
The npm package rtvjs receives a total of 103 weekly downloads. As such, rtvjs popularity was classified as not popular.
We found that rtvjs demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
At its inaugural meeting, the JSR Working Group outlined plans for an open governance model and a roadmap to enhance JavaScript package management.
Security News
Research
An advanced npm supply chain attack is leveraging Ethereum smart contracts for decentralized, persistent malware control, evading traditional defenses.
Security News
Research
Attackers are impersonating Sindre Sorhus on npm with a fake 'chalk-node' package containing a malicious backdoor to compromise developers' projects.