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

@cross-check/schema

v0.11.5

Published

A library for schema validations

Downloads

23

Readme

@cross-check/schema

Crosscheck Schemas allow you to define a schema for your data and use the schema to validate the data.

Uniquely, it allows you to differentiate between "draft" data and published data without creating two separate schema definitions.

For example, if a field in your schema has the "URL" type, you can allow that field to hold any string while the record is being drafted.

This reduces friction when saving initial drafts of content types. It also makes it easy to implement "auto-save", which is optimized for saving in-progress data.

Basic Usage

First, let's define a schema.

import { Schema, type } from "@cross-check/schema";

const schema = new Schema({
  // SingleLine is a string that contains no newlines.
  // Required means that the field must not be missing
  title: type.SingleLine().required(),
  subtitle: type.SingleLine(),

  // Text is a string that can contain newlines
  body: type.Text().required(),

  // SingleWord is a string with no whitespace at all
  tags: type.List(type.SingleWord()),

  geo: Dictionary({
    lat: type.Float().required(),
    long: type.Float().required()
  })
});

Now, let's try to validate some content:

> schema.validate({});
[{
  message: { key: "type", args: "present" },
  path: ["title"]
}, {
  message: { key: "type", args: "present" },
  path: ["body"]
}]

The first thing to notice here is that Crosscheck returns a list of errors, rather than raising an exception. This allows you to use these errors in an interactive UI, and provide richer error information over web services.

Additionally, Crosscheck errors are returned as data: a kind of error and the arguments to the validation. This allows you to present the errors in using application-appropriate language, as well as properly internationalize error messages.

Under the hood, Crosscheck Schema uses the advanced @cross-check/core validation library to validate objects, a validation library extracted from the real-world requirements of the Conde Nast CMS. Its compositional, asynchronous core makes it a perfect fit for validating schemas with embedded lists and dictionaries. To learn more about the philosophy and mechanics of Crosscheck Validations, check out its README.

Drafts

The schema we wrote is pretty strict. It absolutely requires a title and body. But when we're drafting an article, we don't want to be bothered with this kind of busywork just to save in-progress content. And worse, how could we implement auto-save for our form if our authors need to fix a bunch of validation errors before they can even get off the ground.

To solve this problem, every schema creates a looser "draft" schema at the same time.

> schema.draft.validate({});
[]

Because we're validating the draft version of the schema, a completely empty document is totally fine.

But not any kind of document will validate in the draft schema.

> schema.draft.validate({ title: 12, geo: { lat: "100", long: "50" } });
[{
  message: { key: "type", args: "string" },
  path: ["title"]
}, {
  message: { key: "type", args: "number" },
  path: ["geo", "lat"]
}, {
  message: { key: "type", args: "number" },
  path: ["geo", "long"]
}]

Even though we are generally loose with the kind of document we're willing to accept as a draft, we're still expected to pass the right basic data types if we send anything at all.

The philosophy of drafts comes from two observations:

  • We want to allow clients to send in-progress data that the user hasn't finished filling out, but the user is not responsible for picking the data type. For example, if a field is supposed to be a number, the client should user an appropriate number field, and pass a number back to the server.
  • Servers need to store data in data stores (like relational databases) that apply some structure to the data. As a result, even when clients send drafts to the server, we want to be able to impose some constraints on the form of the data.

To give a concrete example, consider a Url type that requires that its data is a valid URL. That type allows any string at all to be provided when used in draft mode. This satisfies the "auto-save" heuristic: the end user can type any text into the text box provided by the CMS, and we want to be able to save a draft even during this period.

Required and Optional Fields

As the above example illustrated, you can mark any field as required. If a field is not marked as required, it is optional.

import { Schema, type } from "@cross-check/schema";

const Person = new Schema({
  first: type.SingleLine().required(),
  middle: type.SingleLine(),
  last: type.SingleLine().required()
});

This Person schema requires a first and last name, but makes the middle name optional.

> Person.validate({})
[{
  message: { key: "first", args: "present" },
  path: ["title"]
}, {
  message: { key: "last", args: "present" },
  path: ["body"]
}]

> Person.draft.validate({})
[]

> Person.validate({ first: "Christina", last: "Kung" })
[]

> Person.validate({ first: "Christina", middle: "multi\nline", last: "Kung" })
[{
  message: { key: "type", args: "string:single-line" },
  path: ["middle"]
}]

> Person.draft.validate({ first: "Christina", middle: "multi\nline", last: "Kung" })
[] // the draft version of a single-line string is any string

> Person.draft.validate({ first: "Christina", middle: 12, last: "Kung" })
[{
  message: { key: "type", args: "string" },
  path: ["middle"]
}] // but you still can't pass a number

Lists

You can also say that a field contains a list of items of a particular type.

import { Schema, type } from "@cross-check/schema";

const Article = new Schema({
  headline: type.SingleLine(),
  body: type.Text(),
  tags: type.List(type.SingleWord())
});

This Article schema has an optional headline and body, and an optional list of single words.

> Article.validate({ tags: "sometag" })
[{
  message: { key: "type", args: "array" },
  path: ["tags"]
}]

> Article.validate({ tags: [12, 15] })
[{
  message: { key: "type", args: "string" },
  path: ["tags", "0"]
}, {
  message: { key: "type", args: "string" },
  path: ["tags", "1"]
}]

> Article.validate({ tags: ["whoops too many words", "totes-fine"] })
[{
  message: { key: "type", args: "string:single-word" },
  path: ["tags", "0"]
}]

> Article.draft.validate({ tags: [12, 15] })
[{
  message: { key: "type", args: "string" },
  path: ["tags", "0"]
}, {
  message: { key: "type", args: "string" },
  path: ["tags", "1"]
}] // Even in draft mode, a number is not a string

> Article.validate({ tags: ["too many words", "totes-fine"] })
[] // but in draft mode, weird strings are ok

A list can contain other lists, dictionaries, or any other type.

Dictionaries

A field can also contain a dictionary.

import { Schema, type } from "@cross-check/schema";

const Location = new Schema({
  geo: type.Dictionary({
    lat: type.Float().required(),
    long: type.Float().required()
  })
})

This location schema has a single geo field that contains a dictionary with two fields: a lat, which is a number, and a long, which is also a number. We have marked the lat and long as required, which means that if the dictionary is present, it must contain both a lat and long.

Since the dictionary itself is optional, you can leave off the dictionary itself.

> Location.validate({});
[] // geo is optional

> Location.validate({ geo: { lat: 12 } })
[{
  message: { key: "type", args: "present" },
  path: ["geo", "long"]
}]

Custom Types

Formatters

Sponsors

cross-check was originally extracted from Condé Nast's CMS, and the work to extract it and release it as open source was funded by Condé Nast.