Research
Security News
Quasar RAT Disguised as an npm Package for Detecting Vulnerabilities in Ethereum Smart Contracts
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
@condenast/cross-check-schema
Advanced tools
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.
First, let's define a schema.
import { Schema, type } from "@condenast/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 @condenast/cross-check
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.
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:
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.
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 "@condenast/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
You can also say that a field contains a list of items of a particular type.
import { Schema, type } from "@condenast/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.
A field can also contain a dictionary.
import { Schema, type } from "@condenast/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"]
}]
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.
FAQs
A library for schema validations
We found that @condenast/cross-check-schema demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 354 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.
Research
Security News
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Security News
Research
A supply chain attack on Rspack's npm packages injected cryptomining malware, potentially impacting thousands of developers.
Research
Security News
Socket researchers discovered a malware campaign on npm delivering the Skuld infostealer via typosquatted packages, exposing sensitive data.