@bytesoftio/schema
TypeScript icon, indicating that this package has built-in type declarations

3.7.0 • Public • Published

@bytesoftio/schema

Installation

yarn add @bytesoftio/schema or npm install @bytesoftio/schema

Table of contents

Description

This library provides a convenient way to describe, validate and sanitize primitive values like strings and numbers, but also objects. It can be used for complex validation scenarios as well as simple one-line assertions.

There are multiple kinds of schemas for different types of data: object, string, number, array, boolean, date and mixed.

There are two ways to run assertions / validations. For simple things like one-liners where you simply want to know if a value matches certain criteria, with a true / false as result, you can use test. For proper validation, with error messages, etc., you can use validate.

Each data type specific schema comes with many different assertion and sanitization methods. Some methods are common for all of the schemas, some are available only on a certain kind of schema.

Assertions are used for validation purposes and are used to describe the underlying value and to ensure it is valid. Methods, test, validate and sanitize exist in two versions: sync and async.

Sanitization / normalization methods are used to process the underlying value even further, for example, to ensure that a string is capitalised, or all of the object keys are camel-cased, etc.

Quick start

Here is an example of all the available schemas and how to import them.

import { string, number, array, boolean, date, object, mixed } from "@bytesoftio/schema"

Let's describe a simple user object.

  • email must be of type string and a valid email address
  • fullName must be a string between 3 and 100 characters
  • roles must be an array containing at least one role, valid roles are "admin", "publisher" and "developer", not duplicates are allowed
  • tags must be an array of string, at least 3 characters long, consisting of letter and dashes
import { array, object, string } from "@bytesoftio/schema"

const userSchema = object({
  email: string().email(),
  fullName: string().min(3).max(100),
  roles: array().min(1).someOf(["admin", "publisher", "developer"]).toUnique(),
  tags: array(string().min(3).alphaDashes())
})

The schema above contains some validation assertions as well as some sanitization / normalization logic.

Quick check if an object is valid according to the schema:

const valid = userSchema.test({ /* ... */ })

if (valid) {
  // ...
}

Regular validation:

const errors = userSchema.validate({ /* ... */ })

if ( ! errors) {
  // ...
}

Run sanitizers like array().toUnique():

const sanitizedValue = userSchema.sanitize({ /* ... */ })

All together:

const [valid, sanitizedValue] = userSchema.sanitizeAndTest({ /* ... */ })
const [errors, sanitizedValue] = userSchema.sanitizeAndValidate({ /* ... */ })

Testing

Lets take a look how to run simple assertions using the test method.

Successful assertion:

import { string } from "@bytesoftio/schema"

const schema = string().min(3).alphaNumeric()

// true
const valid = schema.test("fooBar")

if (valid) {
  // ...
}

Failed assertion:

import { string } from "@bytesoftio/schema"

const schema = string().min(3).alphaNumeric()

// false
const valid = schema.test("foo-bar")

if (valid) {
  // ...
}

Validating

Validations can be very simple, when using strings, numbers, etc. or become quite complex when using object. We'll cover objects in a later section.

Successful validation:

import { string } from "@bytesoftio/schema"

const schema = string().min(3).alphaNumeric()

// undefined
const errors = schema.validate("fooBar")

if ( ! errors) {
  // ...
}

Failed validation:

import { string } from "@bytesoftio/schema"

const schema = string().min(3).alphaNumeric()

// [ ... ]
const errors = schema.validate("foo-bar")

if ( ! errors) {
  // ...
}

This is what the validation error looks like:

[
  {
    // identifies validation and translation key
    type: 'string_alpha_numeric', 
    // translated validation message
    message: 'Must consist of letters and digits only',
    // additional arguments into the the assertion method, like string().min(1)
    args: [],
    // underlying value that was validated
    value: 'foo-bar',
    // description of logical validation links, see .or() and .and() methods
    link: undefined,
    // path of the validated property, when validating objects, 
    // using dot notation "path.to.property"
    path: undefined
  }
]

Sanitizing

Lets take a look on how schema can be used to sanitize / normalize data. For convenience, all sanitization methods start with to, like toCamelCase.

import { string } from "@bytesoftio/schema"

const schema = string().toTrimmed().toCamelCase()

// "fooBar"
const value = schema.sanitize("  foo bar  ")

Sanitize and test

Now let's mix some things up, what if you could sanitize your data before running the assertions?

Successful test:

import { string } from "@bytesoftio/schema"

const schema = string().min(4).toCamelCase()

// [true, "fooBar"]
const [valid, value] = schema.sanitizeAndTest("foo bar")

Failed test:

import { string } from "@bytesoftio/schema"

const schema = string().min(4).toTrimmed()

// [false, "foo"]
const [valid, value] = schema.sanitizeAndTest("  foo  ")

As you can see, even though the string " foo " has a length greater than 4, after it gets trimmed (all surrounding whitespace gets stripped away), it becomes"foo" and therefore its length is less than 4.

Sanitize and validate

This method works exactly the same as sanitizeAndTest, except instead of calling test behind the scenes, it calls the validate method.

Successful validation:

import { string } from "@bytesoftio/schema"

const schema = string().min(4).toCamelCase()

// [undefined, "fooBar"]
const [errors, value] = schema.sanitizeAndValidate("foo bar")

Failed validation:

import { string } from "@bytesoftio/schema"

const schema = string().min(4).toTrimmed()

// [[ ... ], "foo"]
const [errors, value] = schema.sanitizeAndValidate("  foo  ")

This what the errors would look like:

[
  {
    type: 'string_min',
    message: 'Must be at least "4" characters long',
    args: [ 4 ],
    value: 'foo',
    link: undefined,
    path: undefined
  }
]

Reusing validation schemas

Schemas can be chained using conditions. It is also possible to shape the contents of an array or object using a dedicated schema. Sounds complicated, but it isn't. Based on the reasons above you might want to split schemas into small reusable pieces.

import { array, string } from "@bytesoftio/schema"

// a valid username is alpha numeric and has a length from 3 to 10 characters
const usernameSchema = string().alphaNumeric().between(3, 10)

// array contain at least 3 valid usernames
const usernameListSchema = array().min(3).shape(usernameSchema)

// undefined
const errors = usernameListSchema.validate(["foo", "bar", "baz"])

Relations with and() / or() / also()

Schemas can logically be linked together using and and or methods. An and schema will only be executed if the higher order schema, that it is linked to, could validate successfully. An or schema will only execute if the parent schema failed, the or schema will be tried instead.

string().min(3).and(string().noneOf(["foo", "bar"]))

number().or(string().numeric())

Conditional schemas can also be wrapped into a callback that will be executed at validation time.

string().min(3).and(() => string().noneOf(["foo", "bar"]))

number().or(() => string().numeric())

and(), or() are practically interchangeable with validator() and therefore can also return an error message directly.

number().and((value) => value < 12 && "Value must be bigger than 12")

There is also a method also() that is basically an alias for validator() and is syntactic sugar for some use cases.

Add a custom validator

Adding custom validation behaviour is fairly easy to do.

import { string } from "@bytesoftio/schema"

const assertMinLength = (min: number) => {
  return (value) => {
    if (typeof value === "string" && value.length < min) {
      return "Value is too short"
    }
  }
}

const schema = string().validator(assertMinLength(10))

// [ ... ]
const errors = schema.validate("foo bar")

This is what the errors would look like:

[
  {
    type: 'custom',
    message: 'Value is too short',
    args: [],
    value: 'foo bar',
    link: undefined,
    path: undefined
  }
]

A validator can also return another schema.

import { string } from "@bytesoftio/schema"

const schema = string().validator(() => string().min(3))

Add a custom sanitizer

It is very easy to hook up a custom sanitizer into an existing schema.

import { string } from "@bytesoftio/schema"

const toUpperCase = (value) => {
  if (typeof value === "string") {
    return value.toUpperCase()
  }

  return value
}

const schema = string().sanitizer(toUpperCase)

  // "FOO BAR"
const value = schema.sanitize("foo bar")

Async methods and logic

Every validation, sanitizing and testing method has an async counterpart. Synchronous methods would be the go to ones, most of the time. This library itself does not come with any async validation or sanitizing logic. However, it is possible for you to add custom validation and sanitizing methods and you might need them to be async. If you try to run any kind of validation or sanitizing logic trough a sync method, you will get an error - you'll be asked to use an async mehtod instead.

const schema = object({ /* ... */ })
schema.test(/* ... */)
await schema.testAsync(/* ... */)
schema.validate(/* ... */)
await schema.validateAsync(/* ... */)
schema.sanitize(/* ... */)
await schema.sanitizeAsync(/* ... */)
schema.sanitizeAndTest(/* ... */)
await schema.sanitizeAndTestAsync(/* ... */)
schema.sanitizeAndValidate(/* ... */)
await schema.sanitizeAndValidateAsync(/* ... */)

Alternative syntax

You can create any schema starting with the default value, this is especially useful for forms.

import { value, string } from "@bytesoftio/schema"

value('').string()
// same as 
string().toDefault('')

// same applies for boolean, number, date, etc. ...

Translations

This library uses @bytesoftio/translator behind the scenes. Please take a look at the corresponding docs for examples of how to add / replace translations, etc.

Access translator like this:

import { schemaTranslator } from "@bytesoftio/schema"

// take a look at available translations
schemaTranslator.getTranslations()

// customize translations
schemaTranslator.setTranslations({
  en: { string_min: "Value too short" }
})

String schema

String schema has all the methods related to string validation and sanitization.

import { string } from "@bytesoftio/schema"

required

Value must be a non empty string. Active by default.

string().required()
// or
string().required(() => false)

optional

Value might be a string, opposite of required.

string().optional()

equals

String must be equal to the given value.

string().equals("foo")
// or
string().equals(() => "foo")

length

String must have an exact length

string().length(3)
// or
string().length(() => 3)

min

String must not be shorter than given value.

string().min(3)
// or
string().min(() => 3)

max

String must not be longer than given value.

string().max(3)
// or
string().max(() => 3)

between

String must have a length between min and max.

string().between(3, 6)
// or
string().between(() => 3, () => 6)

matches

String must match given RegExp.

string().matches(/^red/)
// or
string().matches(() => /^red/)

email

String must be a valid email address.

string().email()

url

String must be a valid URL.

string().url()

startsWith

String must start with a given value.

string().startsWith("foo")
// or
string().startsWith(() => "foo")

endsWith

String must end with a given value.

string().endsWith("foo")
// or
string().endsWith(() => "foo")

includes

String must include given substring.

string().includes("foo")
// or
string().includes(() => "foo")

omits

String must not include given substring.

string().omits("foo")
// or
string().omits(() => "foo")

oneOf

String must be one of the whitelisted values.

string().oneOf(["foo", "bar"])
// or
string().oneOf(() => ["foo", "bar"])

noneOf

String must not be one of the blacklisted values.

string().noneOf(["foo", "bar"])
// or
string().noneOf(() => ["foo", "bar"])

numeric

String must contain numbers only, including floats.

string().numeric()

alpha

String must contain letters only.

string().alpha()

alphaNumeric

String must contain numbers and letters only.

string().alphaNumeric()

alphaDashes

String must contain letters and dashes "-" only.

string().alphaDashes()

alphaUnderscores

String must container letters and underscores "_" only.

string().alphaUnderscores()

alphaNumericDashes

String must container letters, numbers and dashes only.

string().alphaNumericDashes()

alphaNumericUnderscores

String must contain letters, numbers and underscores only.

string().alphaNumericUnderscores()

date

String must be a valid ISO date string.

string().date()

dateBefore

String must be a valid ISO date string before the given date.

string().dateBefore(new Date())
// or
string().dateBefore(() => new Date())

dateBeforeOrSame

Similar to dateBefore, but allows dates to be equal.

string().dateBeforeOrSame(new Date())
// or
string().dateBeforeOrSame(() => new Date())

dateAfter

String must be a valid ISO date string after the given date.

string().dateAfter(new Date())
// or
string().dateAfter(() => new Date())

dateAfterOrSame

Similar to dateAfter, but allows dates to be equal.

string().dateAfterOrSame(new Date())
// or
string().dateAfterOrSame(() => new Date())

dateBetween

String must be a valid ISO date string between the two given dates.

string().dateBetween(new Date(), new Date())
// or
string().dateBetween(() => new Date(), new Date())

dateBetweenOrSame

Similar to dateBetween, but allows dates to be equal.

string().dateBetweenOrSame(new Date(), new Date())
// or
string().dateBetweenOrSame(() => new Date(), new Date())

time

String must be a valid ISO time string.

string().time()

timeBefore

String must be a valid ISO time string before the given time.

string().timeBefore("10:00")
// or
string().timeBefore(() => "10:00")

timeBeforeOrSame

Similar to timeBefore, but allows times to be equal.

string().timeBeforeOrSame("10:00")
// or
string().timeBeforeOrSame(() => "10:00")

timeAfter

String must be a valid ISO time string after the given time.

string().timeAfter("10:00")
// or
string().timeAfter(() => "10:00")

timeAfterOrSame

Similar to timeAfter, but allows times to be equal.

string().timeAfterOrSame("10:00")
// or
string().timeAfterOrSame(() => "10:00")

timeBetween

String must be a valid ISO time string between the two given times.

string().timeBetween("10:00", "15:00")
// or
string().timeBetween(() => "10:00", "15:00")

timeBetweenOrSame

Similar to dateBetween, but allows dates to be equal.

string().dateBetweenOrSame(new Date(), new Date())
// or
string().dateBetweenOrSame(() => new Date(), new Date())

dateTime

String must be a valid ISO date time string.

string().dateTime()

toDefault

Provide a fallback value in case the underlying value is not a string.

string().toDefault("default value")
// or
string().dateBefore(() => "default value")

toUpperCase

Convert string to all upper case.

string().toUpperCase()

toLowerCase

Convert string to all lower case.

string().toLowerCase()

toCapitalized

Capitalize first letter.

string().toCapitalized()

toCamelCase

Convert string to camelCase.

string().toCamelCase()

toSnakeCase

Convert string to snake_case.

string().toSnakeCase()

toKebabCase

Convert string to kebab-case.

string().toKebabCase()

toConstantCase

Convert string to CONSTANT_CASE.

string().toConstantCase()

toTrimmed

Trim surrounding white space.

string().toTrimmed()

Number schema

Number schema has all the methods related to number validation and sanitization.

import { number } from "@bytesoftio/schema"

required

Value must be a number.

number().required()
// or
number().required(() => false)

optional

Value might be a number, opposite of required.

number().optional()

equals

Number must be equal to the given value.

number().equals(3)
// or
number().equals(() => 3)

min

Number must not be smaller than the given value.

number().min(5)
// or
number().min(() => 5)

max

Number must not be bigger than the given value.

number().max(10)
// or
number().max(() => 10)

between

Number must be between the two given numbers.

number().between(5, 10)
// or
number().between(() => 5, () => 10)

between

Number must be positive - bigger than 0.

number().positive()

negative

Number must be negative - smaller than 0.

number().negative()

integer

Number must be an integer - no floats.

number().integer()

toDefault

Default value in case the underlying value is not a number.

number().toDefault(10)
// or
number().toDefault(() => 10)

toRounded

Round value using Math.round().

number().toRounded(2)
// or
number().toRounded(() => 2)

toFloored

Round value using Math.floor().

number().toFloored()

toCeiled

Round value using Math.ceil().

number().toCeiled()

toTrunced

Trunc value - drop everything after the decimal point.

number().toTrunced()

Boolean schema

Boolean schema has all the methods related to boolean validation and sanitization.

import { boolean } from "@bytesoftio/schema"

required

Value must be a boolean.

boolean().required()
// or
boolean().required(() => false)

optional

Value might be a boolean, opposite of required.

boolean().optional()

equals

Number must be equal to the given value.

boolean().equals(true)
// or
boolean().equals(() => true)

toDefault

Provide a fallback value in case the underlying value is not a boolean.

boolean().toDefault(true)
// or
boolean().toDefault(() => true)

Date schema

Date schema has all the methods related to date validation and sanitization.

import { date } from "@bytesoftio/schema"

required

Value must be a date.

date().required()
// or
date().required(() => false)

optional

Value might be a date, opposite of required.

date().optional()

equals

Date must be equal to the given value.

date().equals(new Date())
// or
date().equals(() => new Date())

after

Underlying value must be after the given date.

date().after(new Date())
// or
date().after(() => new Date())

before

Underlying value must be before the given date.

date().before(new Date())
// or
date().before(() => new Date())

between

Underlying value must be between the two dates.

date().between(new Date(), new Date())
// or
date().between(() => new Date(), () => new Date())

toDefault

Provide a fallback value in case the underlying value is not a date.

date().toDefault(new Date())
// or
date().toDefault(() => new Date())

Array schema

Array schema has all the methods related to array validation and sanitization.

import { array } from "@bytesoftio/schema"

required

Value must be a array.

array().required()
// or
array().required(() => false)

optional

Value might be a array, opposite of required.

array().optional()

equals

Array must be equal to the given value.

array().equals([1, 2])
// or
array().equals(() => [1, 2])

length

Array must have an exact length.

array().length(3)
// or
array().length(() => 3)

min

Array must not be shorter than the given length.

array().min(3)
// or
array().min(() => 3)

max

Array must not be longer than the given length.

array().max(3)
// or
array().max(() => 3)

between

Array must have a length between the two given values.

array().between(3, 5)
// or
array().between(() => 3, () => 5)

someOf

Array must only contain whitelisted values.

array().someOf([3, 4])
// or
array().someOf(() => [3, 4])

noneOf

Array must not contain any of the blacklisted values.

array().noneOf([3, 4])
// or
array().noneOf(() => [3, 4])

shape

Specify a schema for array items. Every item must be valid according to the schema.

array().shape(string().min(3))
// or
array().shape(() => string().min(3))

toDefault

Provide a default value in case the underlying value is not an array.

array().toDefault([1, 2])
// or
array().toDefault(() => [1, 2])

toFiltered

Filter out invalid array items manually.

const isString = (value) => typeof value === "string"

array().toFiltered(isString)

toMapped

Map every array item manually.

const toUpperCase = (value) => typeof value === "string" ? value.toUpperCase() : value

array().toMapped(toUpperCase)

toCompact

Filter out all falsey values like null, undefined, """ and 0.

array().toCompact()

toUnique

Filter out all duplicate values.

array().toUnique()

Object schema

Object schema has all the methods related to object validation and sanitization.

import { object } from "@bytesoftio/schema"

required

Value must be a object.

object().required()
// or
object().required(() => false)

optional

Value might be a object, opposite of required.

object().optional()

equals

Underlying value must be equal to the given value.

object().equals({foo: "bar"})
// or
object().equals(() => ({foo: "bar"}))

shape

Shape an object and set up schemas for all of its properties.

object().shape({ firstName: string().min(3).max(20) })

allowUnknownKeys

Allow object to contain keys that have not been configured through .shape().

object()
  .shape({ firstName: string().min(3).max(20) })
  .allowUnknownKeys() 

disallowUnknownKeys

Forbid object to contain keys that have not been configured through .shape(), active by default.

object()
  .shape({ firstName: string().min(3).max(20) })
  .disallowUnknownKeys() 

shapeUnknownKeys

Shape unknown object keys to make sure they adhere to a certain format / are valid.

object()
  .shape({ firstName: string().min(3).max(20) })
  .shapeUnknownKeys(string().min(3).toCamelCase()) 

shapeUnknownValues

Shape unknown object values to make sure they adhere to a format / are valid.

object()
  .shape({ firstName: string().min(3).max(20) })
  .shapeUnknownValues(string().min(3).max(20)) 

toDefault

Provide a fallback value in case the underlying value is not an object.

object().toDefault({title: "Foo"})
// or
object().toDefault(() => ({title: "Foo"})) 

toCamelCaseKeys

Transform all object keys to camelCase.

object().toCamelCaseKeys()

toCamelCaseKeysDeep

Transform all object keys deeply to camelCase.

object().toCamelCaseKeysDeep()

toSnakeCaseKeys

Transform all object keys to snake_case.

object().toSnakeCaseKeys()

toSnakeCaseKeysDeep

Transform all object keys deeply to snake_case.

object().toSnakeCaseKeysDeep()

toKebabCaseKeys

Transform all object keys to kebab-case.

object().toKebabCaseKeys()

toKebabCaseKeysDeep

Transform all object keys deeply to kebab-case.

object().toKebabCaseKeysDeep()

toConstantCaseKeys

Transform all object keys to CONSTANT_CASE.

object().toConstantCaseKeys()

toConstantCaseKeysDeep

Transform all object keys deeply to CONSTANT_CASE.

object().toConstantCaseKeysDeep()

toMappedValues

Transform all object values.

object().toMappedValues((value, key) => value)

toMappedValuesDeep

Transform all object values deeply.

object().toMappedValuesDeep((value, key) => value)

toMappedKeys

Transform all object keys.

object().toMappedKeys((value, key) => key)

toMappedKeysDeep

Transform all object keys deeply.

object().toMappedKeysDeep((value, key) => key)

Mixed schema

Mixed schema is used when validating / sanitizing data that can have different / unknown types.

import { mixed } from "@bytesoftio/schema"

required

Value must not be null nor undefined.

mixed().required()
// or
mixed().required(() => false)

optional

Value might als be a null or undefined, opposite of required.

mixed().optional()

equals

Underlying value must be equal to the given value.

mixed().equals("yolo")
// or
mixed().equals(() => "yolo")

oneOf

Underlying value must be one of the whitelisted values.

mixed().oneOf(["foo", "bar"])
// or
mixed().oneOf(() => ["foo", "bar"])

noneOf

Underlying value must not be one of the blacklisted values.

mixed().noneOf(["foo", "bar"])
// or
mixed().noneOf(() => ["foo", "bar"])

toDefault

Provide a fallback value in case the underlying value is a null or undefined.

mixed().toDefault(true)
// or
mixed().toDefault(() => true)

Readme

Keywords

none

Package Sidebar

Install

npm i @bytesoftio/schema

Weekly Downloads

1

Version

3.7.0

License

MIT

Unpacked Size

602 kB

Total Files

169

Last publish

Collaborators

  • maximkott