Skip to content

🚧 Just another runtime validation library 🚧

License

Notifications You must be signed in to change notification settings

AlecVision/borg

Repository files navigation

Borg

🚧 There will be regular breaking changes until 1.0 🚧

Version License NPM Downloads

Borg is TypeSafety as code. Borg Schemas are "Write Once, Use Everywhere" - you can use Borg to parse, validate, assert, serialize, deserialize, (coming soon) and even generate BSON or JSON schemas (coming soon). Pair Borg with tRPC for a complete end-to-end solution for your API.

Getting Started

Installation

npm install @alecvision/borg

Creating a Schema

Borg uses a fluent API to define schemas:

import b from "@alecvision/borg";

const userSchema = b.object({
  name: b.string().minLength(1).required(),
  age: b.number().min(0).required(),
  isAdmin: b.boolean().optional()
});

Note

Look familiar? The API of the 'JavaScript' layer is heavily inspired by Zod.†
Where borg really shines is the TypeScript layer!

† The implementation is also inspired by, but differs significantly from, that of Zod.

Type Inference

You can use Borg to infer types from your schema, or to infer the type of the object after conversion to BSON:

import b from "@alecvision/borg";

const userSchema = b.object({
  id: b.id(),
  name: b.string().minLength(1).required(),
  age: b.number().min(0).required(),
  isAdmin: b.boolean().optional()
});

type User = b.Type<typeof userSchema>; // { id: string, name: string; age: number; isAdmin?: boolean; }
type UserBson = b.bsonType<typeof userSchema>; // { id: ObjectId, name: string; age: Double; isAdmin?: boolean; }

Parsing

Parsing produces a new reference, which now has additional type guarantees:

const user = {
  name: "John Doe",
  age: 30,
  isAdmin: true
};

const parsedUser = userSchema.parse(user);
type ParsedUser = typeof parsedUser; // { name: string; age: number; isAdmin: boolean; }
console.log(`User Name: ${parsedUser.name}`); // User Name: John Doe
console.log(`User Age: ${parsedUser.age}`); // User Age: 30
console.log(`User Is Admin: ${parsedUser.isAdmin}`); // User Is Admin: true
console.log(parsedUser === user); // false

Validating

You can validate an object in place using the is method:

if (userSchema.is(user)) {
  type User = typeof user; // { name: string; age: number; isAdmin: boolean; }
  console.log(`User Name: ${user.name}`); // User Name: John Doe
  console.log(`User Age: ${user.age}`); // User Age: 30
  console.log(`User Is Admin: ${user.isAdmin}`); // User Is Admin: true
}

Error Handling

When .parse() fails, it throws an instance of BorgError. When .try() fails, it returns an object of the shape { ok: false, error: BorgError }. You can use the try() method to handle errors gracefully:

const result = userSchema.try(user);
if (!result.ok) console.log("Validation failed with errors:", result.error);

Or, you can use a try-catch block and an instanceof check to handle errors:

try {
  const parsedUser = userSchema.parse(user);
} catch (error) {
  if (error instanceof BorgError) {
    console.log("Validation failed with errors:", error.errors);
  } else {
    throw error;
  }
}

API

.parse()

Parses the input and returns a new reference with additional type guarantees. Throws BorgError if validation fails.

.try()

Parses an object and returns a result object with the following shape:

{ ok: false, error: BorgError } | { ok: true, value: T, meta: TMeta }

.is()

A type guard that returns true if the object matches the schema. It asserts that the object is of the correct type.

.serialize()

Serializes an object to JSON that includes metadata for deserialization with a separate library (coming soon)

Common Chainable Methods

.copy()

Returns a copy of the schema. (This is used under the hood to create new instances of the schema when chaining methods. Because Borg schemas are immutable, you can safely chain methods without mutating the original schema, likely obviating the need for this method.)

.optional()

Returns an instance of the schema that permits undefined or missing values.

.required() (default)

Returns an instance of the schema that must be present and not undefined.

.nullable()

Returns an instance of the schema that permits null values.

.notNull() (default)

Returns an instance of the schema that does not permit null values.

.nullish()

Short for .nullable().optional()

.notNullish()

Short for .notNull().required()

.private() (currently does nothing)

Returns an instance of the schema that parses as normal, but fails serialization. If part of an object, the property is removed from the serialized object. If part of an array, the item is removed from the serialized array.

.public() (default) (currently does nothing)

Returns an instance of the schema that parses and serializes as normal.

Chainable String Schema Instance Methods

  • .minLength(), .maxLength(), .length()

Returns an instance of the schema that validates the length of the string.

b.string().length(5); // string must be exactly 5 characters long
b.string().minLength(5); // string must be at least 5 characters long
b.string().maxLength(5); // string must be at most 5 characters long
b.string().length(5, 10); // string must be between 5 and 10 characters long, inclusive
b.string().minLength(4).length(null); // string may be any length
  • .pattern() (default: null [shows as .* in type hint])

Returns an instance of the schema that validates the string against a regular expression, supplied as a string.

b.string().pattern("^[a-z]+$"); // string must contain only lowercase letters

NOTE: Special characters in the regular expression must be double-escaped. The typescript inference will display the correct string in all cases EXCEPT when the regular expression includes backslashes. When using backslashes, the type hint will show the incorrect number of slashes. To work around this, Borg will parse a regex correctly when an additional backslash is used in the backslash escape sequence. i.e. \\\\ will parse the same as \\\, however will show 4 slashes in the type hint.

b.string().pattern("^[a-z\\\\]+$"); // string must contain only lowercase letters and backslashes
//  ?^
//  BorgString<["required", "notNull", "public"], [null, null], "^[a-z\\\\]+$">

b.string().pattern("^[a-z\\]+$"); // string must contain only lowercase letters and backslashes
//  ?^
//  BorgString<["required", "notNull", "public"], [null, null], "^[a-z\\]+$"> <-- (**incorrect**)

Chainable Number Schema Instance Methods

  • .min(), .max(), .range()

Returns an instance of the schema that validates the number against a range.

b.number().range(5, 10); // number must be between 5 and 10, inclusive
b.number().min(5); // number must be at least 5
b.number().max(10); // number must be at most 10
b.number().min(5).max(10).range(5, null); // number must be at least 5

Chainable Array Schema Instance Methods

  • .minItems(), .maxItems(), .length()

Returns an instance of the schema that validates the length of the array.

b.array(b.number()).length(5); // array must be exactly 5 items long
b.array(b.number()).minItems(5); // array must be at least 5 items long
b.array(b.number()).maxItems(5); // array must be at most 5 items long
b.array(b.number()).length(5, 10); // array must be at least 5 and at most 10 items long
b.array(b.number()).minItems(4).minItems(null); // array may be any length

Chaining

Borg schemas are immutable, so you can chain methods to create new instances of the schema. The effects are applied in the order that they are called, making them reversible and composable.

const nameSchema = b.string().minLength(5).maxLength(10).notNull().private();

is exactly the same as

const nameSchema = b
  .string()
  .private()
  .public()
  .minLength(10)
  .nullable()
  .notNull()
  .optional()
  .required()
  .nullish()
  .notNullish()
  .nullish()
  .required()
  .maxLength(15)
  .private()
  .minLength(null)
  .length(5, 10);

About

🚧 Just another runtime validation library 🚧

Resources

License

Stars

Watchers

Forks

Packages

No packages published