jet-schema
Advanced tools
Comparing version 1.2.2 to 1.2.3
{ | ||
"name": "jet-schema", | ||
"version": "1.2.2", | ||
"version": "1.2.3", | ||
"description": "Simple, typescript-first schema validation tool", | ||
@@ -11,4 +11,4 @@ "main": "dist/index.js", | ||
"test:pg": "ts-node ./test/playground.ts", | ||
"test:min:mjs": "node ./test/test-es6-min.mjs", | ||
"test:min:cjs": "node ./test/test-cjs-min.js", | ||
"test:min-mjs": "node ./test/test-es6-min.mjs", | ||
"test:min-cjs": "node ./test/test-cjs-min.js", | ||
"build": "rm -rf ./dist && tsc -p tsconfig.build.json && npm run bundle", | ||
@@ -15,0 +15,0 @@ "bundle": "rollup --bundleConfigAsCjs --config rollup.config.ts --configPlugin typescript", |
410
README.md
@@ -9,12 +9,23 @@ # Jet-Schema ✈️ | ||
- [Guide](#guide) | ||
- [Getting Started](#getting-started) | ||
- [Creating Custom Schemas](#creating-custom-schemas) | ||
- [Making schemas optional/nullable](#making-schemas-opt-null) | ||
- [Child schemas](#child-schemas) | ||
- [Installation](#installation) | ||
- [Validator functions](#init-validator-functions) | ||
- [Creating Schemas](#creating-schemas) | ||
- [Schema APIs](#schema-apis) | ||
- [.new](#new) | ||
- [.test](#test) | ||
- [.pick](#pick) | ||
- [.parse](#parse) | ||
- [Schema Options](#schema-options) | ||
- [Configuring settings](#configuring-settings) | ||
- [Global settings](#global-settings) | ||
- [Local settings](#local-settings) | ||
- [Combining Schemas](#combining-schemas) | ||
- [TypeScript Caveats](#typescript-caveats) | ||
- [Bonus Features](#bonus-features) | ||
- [Miscellaneous Notes](#misc-notes) | ||
- [Using jet-schema without TypeScript](#without-typescript) | ||
- [Combining jet-schema with parse from ts-validators](#parse-from-ts-validators) | ||
- [Tips](#tips) | ||
- [Creating wrapper functions](#creating-wrapper-functions) | ||
- [Recommended Defaults](#recommended-defaults) | ||
- [Recommended Global Settings](#recommended-global-settings) | ||
- [Author's Notes](#authors-notes) | ||
<br/> | ||
@@ -24,23 +35,18 @@ | ||
## Introduction <a name="introduction"></a> | ||
Most schema validation libraries have fancy functions for validating objects and their properties, but the problem is I already have a lot of my own custom validation functions specific for each of my applications that I also like to copy-n-paste a lot and modify as needed (i.e. functions to check primitive-types, regexes for validating strings etc). The only thing that was making me use schema-validation libraries was trying to validate an object. So I thought, why not figure out a way to integrate my all the functions I had already written with something that can validate them against object properties? Well **jet-schema** does just that :) | ||
<br/> | ||
**jet-schema** is a simple, TypeScript first schema validation tool, which enables you to use your own validator functions against each property in an object. That way you don't have to refer to documentation everytime you want to validate some new object property. It also means you get to grow a list of TypeScript validator functions that aren't tied to a specific library. | ||
> I know some libraries like `zod` have functions such as `.refine()` which allow you to integrate custom validation logic. But this isn't quite the same because you could still chain additional validation logic onto `.refine` and you still have to look through the documentation to see how to do it. I wanted a library that allowed me to just slap in validator-functions that were not tied to any specific schema-validation library. | ||
> If you're open to `jet-schema` but think writing your own validator-functions could be a hassle, you can copy-n-paste the file (https://github.com/seanpmaxwell/ts-validators/blob/master/src/validators.ts) into your application and add/remove/edit validators as needed. | ||
If you want a library that includes all kinds of special functions for validating things other than objects, **jet-schema** is probably not for you. However, the vast majority of projects I've worked on have involved implementing lots of type-checking functions specific to the needs of that project. For example, maybe you use another datetime handling library other than JavasScript's `Date` object (i.e. `DayJs`). Instead of of having to dig into the library's features to accept `dayjs` objects as valid-objects, with **jet-schema** you can just drop in `dayjs.isValid`. | ||
<br/> | ||
> If you're open to `jet-schema` but think writing your own validator-functions could be a hassle, you can copy-n-paste the file (https://github.com/seanpmaxwell/ts-validators/blob/master/src/validators.ts) into your application and add/remove/edit validators as needed. | ||
### Reasons to use Jet-Schema 😎 | ||
- Focus is on using your own validator-functions to validate object properties (this is why I wrote it). | ||
- Focus is on using your own validator-functions to validate object properties. | ||
- TypeScript first! | ||
- Quick, terse, simple, small (this library only exports 2 functions and 2 types, minified size is **4.56 KB**). | ||
- Fast, terse, small (this library only exports 2 functions and 2 types, size **2.2 KB** minified + zipped). | ||
- Less complex than nearly every other schema-validation library. | ||
- Typesafety works both ways, you can either force a schema structure using a pre-defined type OR you can infer a type from a schema. | ||
- `new`, `test`, `parse` functions provided automatically on every new schema. | ||
- Setting defaults and transforming values can be set globally on initial setup or at the schema-level. | ||
- Validator-function settings can be done globally (for every schema) or locally (for each new schema). | ||
- Works client-side or server-side. | ||
- Enums can be used for validation. | ||
- `Date` constructor can be used to automatically transform and validate any valid date value. | ||
- `Date` constructor can be used to automatically transform and validate date values. | ||
- Doesn't require a compilation step (so still works with `ts-node`, unlike `typia`). | ||
@@ -53,3 +59,3 @@ <br/> | ||
import schema from 'utils/schema'; | ||
import { isRelKey, isString, isNumber, isOptionalStr } from 'utils/validators'; | ||
import { isString, isNumber } from 'utils/validators'; | ||
@@ -59,25 +65,13 @@ interface IUser { | ||
name: string; | ||
email: string; | ||
age: number; | ||
created: Date; | ||
address?: { | ||
street: string; | ||
zip: number; | ||
country?: string; | ||
}; | ||
} | ||
// NOTE: we can also infer the type from the schema | ||
const User = schema<IUser>({ | ||
id: isNumber, | ||
name: isString, | ||
email: { vf: isEmail, default: 'x@x.com' }, | ||
age: { vf: isNumber, transform: Number }, | ||
created: Date, | ||
address: schema<IUser['address']>({ | ||
street: isString, | ||
zip: isNumber, | ||
country: isOptionalStr, | ||
}, { optional: true }), | ||
}); | ||
User.new({ id: 5, name: 'joe' }) // => { id: 5, name: 'joe' } | ||
User.test('asdf') // => false | ||
User.pick('name').test('john') // => true | ||
User.parse('something') // => Error | ||
``` | ||
@@ -89,176 +83,219 @@ <br/> | ||
### Getting Started <a name="getting-started"></a> | ||
### Installation <a name="installation"></a> | ||
> npm install -s jet-schema | ||
After installation, you need to configure the `schema` function by importing and calling `jetSchema()`. When setting up **jet-schema** for the first time, usually what I do is create two files in my `util/` folder: `schema.ts` and `validators.ts`. In `schema.ts` I'll import and call the `jet-schema` function then apply any globals and a custom clone-function. If you don't want to go through this step, you can import the `schema` function directly from `jet-schema`. | ||
<br/> | ||
`jetSchema()` accepts an optional settings object with 3 three optional properties: | ||
- `globals`: An array of **validator-objects**, which map certain global settings for specific validator-functions. These functions are passed by reference so you need to make sure you reuse the same functions (not a copy of those functions) when setting up a particular schema. You should use this option for frequently used validator-function/default/transform combinations where you don't want to set a default value or transform-function every time. Upon initialization, the validator-functions will check their defaults. If a value is not optional and you do not supply a default value, then an error will occur when the schema is initialized. If you don't set a default value for a validator-function in `jetSchema()`, you can also do so when setting up an individual schema (the next 3 snippets below contain examples).<br/> | ||
The format for a **validator-object** is: | ||
```typescript | ||
{ | ||
vf: <T>(arg: unknown) => arg is T; // a vf => "validator function" | ||
default?: T; // the value to use for the validator-function | ||
transform?: (arg: unknown) => T; // modify the value before calling the validator-function | ||
onError?: (property: string, value?: unknown, moreDetails?: string, schemaId?: string) => void; // Custom error message for the function | ||
} | ||
``` | ||
- `cloneFn`: A custom clone-function if you don't want to use the built-in function which uses `structuredClone` (I like to use `lodash.cloneDeep`). | ||
- `onError`: A global error handler. By default, if a validator-function fails then an error is thrown. You can override this behavior by passing a custom error handling function. This feature is really useful for testing when you may want to print an error string instead of throw an error. Format: `(property: string, value?: unknown, origMessage?: string, schemaId?: string) => void;`. | ||
<br/> | ||
## Validator functions <a name="init-validator-functions"></a> | ||
`util/validators.ts` and `util/schema.ts` overview: | ||
Validator-functions are functions which receive an `unknown` value and return a type-predicate if the value satisfies the given logic: | ||
```typescript | ||
// "util/validators.ts" | ||
// As mentioned in the intro, you can copy some predefined validators from here (https://github.com/seanpmaxwell/ts-validators/blob/master/src/validators.ts). These are just some examples. | ||
export const isStr = (arg: unknown): arg is string => typeof param === 'string'; | ||
export const isOptStr = (arg: unknown): arg is string => arg === undefined || typeof param === 'string'; | ||
export const isNum = (arg: unknown): arg is string => typeof param === 'number'; | ||
function isOptionalString(arg: unknown): param is string | undefined { | ||
return arg === undefined || typeof arg === 'string'; | ||
} | ||
``` | ||
> ⚠️ **IMPORTANT:** You need to use type-predicates when writing validator-functions. If a value can be `null/undefined`, your validator-function's type-predicate needs account for this (`i.e. (arg: unknown): arg is string | undefined => ... `). | ||
> I like to place all my validator-functions in a `util/validators.ts` file. As mentioned in the intro, you can copy some predefined validators from <a href="https://github.com/seanpmaxwell/ts-validators/blob/master/src/validators.ts">here</a>. | ||
```typescript | ||
// "util/schema.ts" | ||
import jetSchema from 'jet-schema'; | ||
import { isNum, isStr } from './validators'; | ||
export default jetLogger({ | ||
globals: [ | ||
{ vf: isNum, default: 0 }, | ||
{ vf: isStr, default: '' }, | ||
], | ||
cloneFn: () => ... // pass a custom clone-function here if you want | ||
onError: () => ... // pass a custom error-handler here if you want, | ||
}); | ||
``` | ||
## Creating schemas <a name="creating-schemas"></a> | ||
### Creating custom schemas <a name="creating-custom-schemas"></a> | ||
Using the `schema` function exported from `jet-schema` or the function returned from calling `jetSchema(...)` if you configured global-settings (see the <a href="#global-settings">Global Settings</a> section), call the function and pass it an object with a key for each property you are trying to validate: with the value being a validator-function or a settings-object (see the <a href="#configuring-settings">Configuring Settings</a> section for how to use settings-objects). For handling a schema's type, you can enforce a schema from a type or infer a type from a schema. | ||
Now that we have our `schema` function setup, let's make a schema. Simply import the `schema` function from `util/schema.ts` and your existing validator-functions, then pass them as the value to each property in the `schema` function or use a validator-object. The format for a validator-object is the same both locally and globally (see above). All local settings for a validator-function will overwrite the global ones. Remember that if a property is required then a default must be set for its validator-function (locally or globally) or else `new` won't know what to use as a value when passing a partial.<br/> | ||
For handling an individual schema's type, there are two ways to go about this, enforcing a schema from a type or infering a type from a schema. I'll show you an example of doing it both ways. | ||
> Personally, I like to create an interface first cause I feel like interfaces are great way to document your data-types; however, I created `inferType` because I know some people prefer to setup their schemas first and infer their types from that. | ||
**Option 1:** Create a schema using a type: | ||
```typescript | ||
// "models/User.ts" | ||
import { inferType } from 'jet-schema'; | ||
import schema from 'util/schema.ts'; | ||
import { schema } from 'jet-schema'; | ||
import { isNum, isStr, isOptionalStr } from 'util/validators.ts'; | ||
// **OPTION 1**: Create schema using type | ||
interface IUser { | ||
id: number; | ||
name: string; | ||
email: string; | ||
nickName?: string; | ||
email?: string; | ||
} | ||
const User = schema<IUser>({ | ||
id: isNum, | ||
name: isStr, | ||
email: { vf: isEmail, default: '' }, | ||
nickName: isOptionalStr, | ||
}) | ||
email: isOptionalStr, | ||
}); | ||
``` | ||
// **OPTION 2**: Create type using schema | ||
**Option 2:** Create a type using a schema: | ||
```typescript | ||
import { schema } from 'jet-schema'; | ||
import { isNum, isStr, isOptionalStr } from 'util/validators.ts'; | ||
const User = schema({ | ||
id: isNum, | ||
name: isStr, | ||
email: { vf: isEmail, default: '' }, | ||
nickName: isOptionalStr, | ||
}) | ||
email: isOptionalStr, | ||
}); | ||
const TUser = inferType<typeof User>; | ||
``` | ||
Once you have your custom schema setup, you can call the `new`, `test`, `pick`, and `parse` functions. Here is an overview of what each one does: | ||
- `new` allows you to create new instances of your type using partials. If the property is absent, `new` will use the default supplied. If no default is supplied and the property is optional, then the value will be skipped. Runtime validation will still be done on every incoming property. Also, if you pass no parameter then a new instance will be created using all the defaults. | ||
- `test` accepts any unknown value, tests that it's valid, and returns a type-predicate. | ||
- `pick` allows you to select any property and returns an object with the `test` and `default` functions. | ||
- `parse` is like a combination of `new` and `test`. It accepts an `unknown` value which is not optional, validates the properties but returns a new instance (while removing an extra ones) instead of a type-predicate. Note: only objects will pass the `parse` function, even if a schema is nullish, `null/undefined` values will not pass. | ||
## Schema APIs <a name="schema-apis"></a> | ||
Once you have your custom schema setup, you can call the `.new`, `.test`, `.pick`, and `.parse` functions. | ||
### Making schemas optional/nullable <a name="making-schemas-opt-null"></a> | ||
In addition to a schema-object, the `schema` function accepts an additional **options** object parameter. The values here are type-checked against the generic (`schema<"The Generic">(...)`) that was passed so you must use the correct values. If your generic is optional/nullable then your are required to pass the object so at runtime the correct values are used. | ||
> NOTE: the following examples assume you set `0` as the default for `isNum`, `''` for `isStr`, and nothing for `isOptionalStr`. See the <a href="#configuring-settings">Configuring Settings</a> section for how to set default values. | ||
### `.new` <a name="new"></a> | ||
Allows you to create new instances of your type using partials. If the property is absent, `.new` will use the default supplied. If no default is supplied and the property is optional, then the value will be skipped. Runtime validation will still be done on every incoming property: | ||
```typescript | ||
{ | ||
optional?: boolean; // default "false", must be true if generic is optional | ||
nullable?: boolean; // default "false", must be true if generic is nullable | ||
init?: boolean | null; // default "true", must be undefined, true, or null if generic is not optional. | ||
nullish?: true; // Use this instead of "{ optional: true, nullable: true; }" | ||
id?: string; // Will identify the schema in error messages | ||
} | ||
User.new(); // => { id: 0, name: '' } | ||
User.new({ id: 5 }); // => { id: 5, name: '' } | ||
User.new({ id: 'asdf' }); // => Error | ||
User.new({ name: 'john' }); // => { id: 0, name: 'john' } | ||
User.new({ id: 1, name: 'a', email: 'b@b' }); // => { id: 1, name: 'a', email: 'b@b' } | ||
``` | ||
The option `init` defines the behavior when a schema is a child-schema and is being initialized from the parent. If `true (default)`, then a nested child-object will be added to the property when a new instance of the parent is created. However, if a child-schema is optional or nullable, maybe you don't want a nested object and just want it to be `null` or skipped entirely. If `init` is `null` then `nullable` must be `true`, if `false` then `optional` must be `true`.<br/> | ||
### `.test` <a name="test"></a> | ||
Accepts any unknown value, tests that it's valid, and returns a type-predicate: | ||
```typescript | ||
User.test(); // => Error | ||
User.test({ id: 5, name: 'john' }); // => param is IUser | ||
User.test({ name: 'john' }); // => Error | ||
User.test({ id: 1, name: 'a', email: 'b@b' }); // => param is IUser | ||
``` | ||
In the real world it's very common to have a lot of schemas which are both optional, nullable. So you don't have to write out `{ optional: true, nullable: true }` over-and-over again, you can write `{ nullish: true }` as an shorthand alternative.<br/> | ||
### `.pick` <a name="pick"></a> | ||
Selects a property and returns an object with the `.test` and `.default` functions. If you use `.pick` on a child schema, you can also use the schema functions (`.new`, `.pick` etc), in addition to `.default`. Note that for a child-schema, `.default` could return a different value from `.new` if the default value is set to `null` or `undefined` (see the `init:` setting in the <a href="#schema-options">Schema Options</a> section). | ||
```typescript | ||
const User = schema<IUser>({ | ||
id: isNum, | ||
address: schema<IUser['address'>({ | ||
street: isStr, | ||
city: isStr, | ||
}, { init: null }), | ||
}); | ||
You can also set the optional `id` field, if you need a unique identifier for your schema for whatever reason. If you set this option then it will be added to the default error message. This can be useful if you have to debug a bunch of schemas at once (that's pretty much all I use it for). | ||
User.pick('id').default(); // => "0" | ||
User.pick('id').test(0); // => "true" | ||
User.pick('id').test('asdf'); // => "false" | ||
User.pick('address').new(); // => { street: '', city: '' } | ||
User.pick('address').default(); // => "null" | ||
User.pick('address').pick('city').test('asdf'); // => "true" | ||
``` | ||
Here's an example of the options in use: | ||
### `.parse` <a name="parse"></a> | ||
Like a combination of `.new` and `.test`. It accepts an `unknown` value which is not optional, validates the properties but returns a new instance (while removing an extra ones) instead of a type-predicate. Note: only objects will pass the `.parse` function, even if a schema is nullish, `null/undefined` values will not pass. | ||
```typescript | ||
// models/User.ts | ||
const User = schema<IUser>({ | ||
id: isNum, | ||
name: isStr, | ||
}); | ||
interface IUser { | ||
id: number; | ||
name: string; | ||
address?: { street: string, zip: number } | null; | ||
} | ||
User.parse(); // => Error | ||
User.parse({ id: 1, name: 'john' }); // => { id: 1, name: 'john' } | ||
User.parse({ id: 1, name: 'john', foo: 'bar' }); // => { id: 1, name: 'john' } | ||
User.parse({ id: '1', name: 'john' }); // => Error | ||
``` | ||
## Schema options <a name="schema-options"></a> | ||
In addition to a schema-object, the `schema` function accepts an additional **options** object parameter: | ||
```typescript | ||
const User = schema<IUser>({ | ||
id: isNumber, | ||
name: isString, | ||
address: schema<IUser['address']>({ | ||
street: isString, | ||
zip: isNumber, | ||
}, { nullish: true, init: false, id: 'User_address' }), | ||
}) | ||
User.new() // => { id: 0, name: '' } | ||
id: isNum, | ||
name: isStr, | ||
}, /* { ...options object... } */); // <-- Pass options here | ||
``` | ||
**options** explained: | ||
- `optional`: Default `false`, must be set to true if the generic is optional. | ||
- `nullable`: Default `false`, must be set to true if the generic is nullable. | ||
- `nullish`: Default `false`, convenient alternative to `{ optional: true, nullable: true; }` | ||
- `init`: Tells the parent what to do when the parent calls `.new`. There are 3 options: | ||
- `false`: Skip creating a child-object. The child-object must be `optional`. | ||
- `true`: Create a new child-object (Uses the child's `.new` function). | ||
- `null`: Set the child object's value to `null` (`nullable` must be true for the child). | ||
- `id`: A unique-identifier for the schema (useful if debugging a bunch of schemas at once). | ||
### Child (aka nested) schemas <a name="child-schemas"></a> | ||
- If an object property is a mapped-type, then it must be initialized with the `schema` function. | ||
- Just like with the parent schemas, you can also call `new`, `test`, `pick`, `parse` in addition to `default`. Note: the value returned from `default` could be different from `new` if the schema is optional/nullable and the default value is `null` or `undefined`. | ||
- There is one extra function `schema()` that you can call when using `pick` on a child-schema. This can be handy if you need to export a child-schema from one parent-schema to another: | ||
**options** example: | ||
```typescript | ||
interface IUser { | ||
id: number; | ||
address?: { street: string, city: string }; | ||
} | ||
type TUser = IUser | null | undefined; | ||
const User = schema<IUser>({ | ||
id: isNumber, | ||
address: schema<IUser['address']>({ | ||
street: isStr, | ||
city: isString | ||
}, { optional: true, init: false }), | ||
const User = schema<TUser>({ | ||
id: isNum, | ||
name: isStr, | ||
}, { | ||
optional: true, // Must be true because TUser is `| undefined` | ||
nullable: true, // Must be true because TUser is `| null` | ||
nullish: true, // Alternative to { optional: true, nullable: true } | ||
init: false, // Can be "null", "false", or "true" | ||
id: 'User', // custom string | ||
}); | ||
``` | ||
User.pick('address').default() // => undefined because we said "init: false" | ||
User.pick('address').new() // { street: '', city: '' } | ||
interface IUserAlt { | ||
id: number; | ||
address?: IUser['address']; | ||
## Configuring settings <a name="configuring-settings"></a> | ||
Validator-functions can be used alone or within a **settings-object**, which enables you to do more than just validate an object property. Settings can be configured at the **global-level** (so you don't have to configure them for every new schema) or when a schema is initialized (**local-level**). | ||
Settings object overview: | ||
```typescript | ||
{ | ||
vf: <T>(arg: unknown) => arg is T; // "vf" => "validator function", | ||
default?: T; // the default value for the validator-function | ||
transform?: (arg: unknown) => T; // modify the value before calling the validator-function | ||
onError?: (property: string, value?: unknown, moreDetails?: string, schemaId?: string) => void; // Custom error message for the function | ||
} | ||
``` | ||
const UserAlt = schema<IUserAlt>({ | ||
id: isNumber, | ||
address: User.pick('address').schema(), | ||
### Global settings <a name="global-settings"></a> | ||
You can configure global settings by importing and calling the `jetSchema` function which returns a function with your global settings saved: | ||
```typescript | ||
import jetSchema from 'jet-schema'; | ||
import { isNum, isStr } from './validators'; | ||
export default jetLogger({ | ||
globals?: [ | ||
{ vf: isNum, default: 0 }, | ||
{ vf: isStr, default: '' }, | ||
], | ||
cloneFn?: () => ... // use a custom clone-function | ||
onError?: () => ... // pass a custom error-handler, | ||
}); | ||
``` | ||
Global settings explained: | ||
- `globals`: An array of settings-objects, which map certain global settings for specific validator-functions. Use this option for frequently used validator-function settings you don't want to configure every time. | ||
- `cloneFn`: A custom clone-function, the default clone function uses `structuredClone` (I like to use `lodash.cloneDeep`). | ||
- `onError`: A global error handler, the default error-handler throws an error. | ||
- Format is: `(property: string, value?: unknown, origMessage?: string, schemaId?: string) => void;`. | ||
### Combining Schemas <a name="combining-schemas"></a> | ||
If you want to declare part of a schema that will be used elsewhere, you can import the `TJetSchema` type and use it to setup a partial schema, then merge it with your full schema later. | ||
> The `moreDetails/origMessage` parameter for `onError` will return the full original stringified error message when passed to the global `onError` handler, but returns various error strings when overriding error functions for an individual validator-function, such as in what schema function (i.e. `.new`) the error happened. | ||
### Local settings <a name="local-settings"></a> | ||
To configure settings at the local-level, use them when creating a schema. All local-settings will override all global ones; if you don't need the schema to have any global settings you can import the `schema` function directly from `jet-schema`: | ||
```typescript | ||
// Use this if you don't want use global-settings | ||
// import { schema } from 'jet-schema'; | ||
// Where we set our global settings | ||
import schema from 'util/schema.ts'; | ||
const User = schema({ | ||
id: { | ||
vf: isNum, | ||
transform: (arg: unknown) => Number(arg), | ||
default: -1, | ||
onError: (prop: string, value: unknown) => `Property "id" was not a number. Value: ${value}.` | ||
}, | ||
name: isStr, | ||
}); | ||
// Local setting overwrote -1 as default for isNum whose global setting was 0, | ||
// empty-string remains default for isStr | ||
User.new() // => { id: -1, name: '' } | ||
``` | ||
## Combining Schemas <a name="combining-schemas"></a> | ||
If you want to declare part of a schema that will be used elsewhere, you can import the `TJetSchema` type and use it to setup a partial schema, then merge it with your full schema later: | ||
```typescript | ||
import schema, { TJetSchema } from 'jet-schema'; | ||
@@ -280,7 +317,7 @@ | ||
### TypeScript Caveats <a name="typescript-caveats"></a> | ||
## TypeScript Caveats <a name="typescript-caveats"></a> | ||
Due to how structural-typing works in typescript, there are some limitations with typesafety that you need to be aware of. To put things in perspective, if type `A` has all the properties of type `B`, we can use type `A` for places where type `B` is required, even if `A` has additional properties. | ||
**Validator functions**<br/> | ||
If an object property's type can be `string | undefined`, then a validator-function whose type-predicate only returns `arg is string` will still work. However a if a type predicate returns `arg is string | undefined` we cannot use it for type `string`. This could cause runtime issues if a you pass a validator function like `isString` (when you should have passed `isOptionalString`) to a property whose value ends up being `undefined`. | ||
#### <ins>Validator functions</ins> | ||
If an object property's type can be `string | undefined`, then a validator-function whose type-predicate only returns `param is string` will still work. However a if a type predicate returns `param is string | undefined` we cannot use it for type `string`. This could cause runtime issues if a you pass a validator function like `isString` (when you should have passed `isOptionalString`) to a property whose value ends up being `undefined`: | ||
```typescript | ||
@@ -298,4 +335,4 @@ interface IUser { | ||
**Child schemas**<br/> | ||
As mentioned, if a property in a parent is mapped-object type (it has a defined set of keys), then you need to call `schema` again for the nested object. If you don't use a generic on the child-schema, typescript will still make sure all the required properties are there; however, because of structural-typing the child could have additional properties. It is highly-recommended that you pass a generic to your child-objects so additional properties don't get added. | ||
#### <ins>Child schemas</ins> | ||
As mentioned, if a property in a parent is a mapped-object type (it has a defined set of keys), then you need to call `schema` again for the nested object. If you don't use a generic on the child-schema, typescript will still make sure all the required properties are there; however, because of structural-typing the child could have additional properties. It is highly-recommended that you pass a generic to your child-objects so additional properties don't get added: | ||
```typescript | ||
@@ -318,13 +355,18 @@ interface IUser { | ||
### Bonus Features <a name="bonus-features"></a> | ||
## Bonus Features <a name="bonus-features"></a> | ||
- When passing the `Date` constructor, `jet-schema` sets the type to be a `Date` object and automatically converts all valid date values (i.e. `string/number`, maybe a `Date` object got stringified in an API call) to a `Date` object. The default value will be a `Date` object with the current datetime. | ||
- You can also use an enum as a validator. The default value will be the first value in the enum object and validation will make sure it is value of that enum. | ||
- You can also use an `enum` as a validator. The default value will be the first value in the enum object and validation will make sure it is value of that enum. | ||
## Using jet-schema without TypeScript <a name="without-typescript"></a> | ||
`jet-schema` is built in TypeScript for TypScript but can be used directly with plain JavaScript. There are two minified files you can import if you want to use plain javascript: | ||
- `dist/index.min.js`: CommonJS | ||
- `dist/index.min.mjs`: ESM (es6) | ||
<br> | ||
## Miscellaneous Notes <a name="misc-notes"></a> | ||
## Tips <a name="tips"></a> | ||
### Creating wrapper functions <a name="creating-wrapper-functions"></a> | ||
If you need to modify the value of the `test` function for a property, (like removing `nullables`) then I recommended merging your schema with a new object and adding a wrapper function around that property's test function. | ||
If you need to modify the value of the `.test` function for a property, (like removing `nullables`) then I recommended merging your schema with a new object and adding a wrapper function around that property's test function. | ||
```typescript | ||
@@ -354,6 +396,5 @@ // models/User.ts | ||
### Recommended Defaults <a name="recommended-defaults"></a> | ||
When calling the `jetSchema` function for this first time, at the very least, I highly recommend you set these default values for each of your basic primitive validator functions, unless of course your application has some other specific need. | ||
### Recommended Global Settings <a name="recommended-global-settings"></a> | ||
I highly recommend you set these default values for each of your basic primitive validator functions, unless of course your application has some other specific need. | ||
```typescript | ||
// util/schema.ts | ||
import { isNum, isStr, isBool } from 'util/validators.ts'; | ||
@@ -369,1 +410,40 @@ | ||
``` | ||
### Combining jet-schema with `parse` from ts-validators <a name="parse-from-ts-validators"></a> | ||
The before mentioned repo <a href="https://github.com/seanpmaxwell/ts-validators/blob/master">ts-validators</a> contains a function called `parse` (not to be confused with the jet-schema function `.parse`) which is handy for doing lots of little validations on objects where setting up a full stand-alone schema isn't really practical. For instance, maybe you have a backend webserver with many different APIs where every `request` object coming in needs to have one or a couple of properties validated on it before continuing the api call.<br/> | ||
**ts-validators**'s `parse` function also works by receiving a schema filled with validator-functions and returns another validator-function to check if the object satisfies the schema. Using `parse` alone is trivial for doing simple primitive checks but can be very powerful if you have an object which contains both a combination of primitives and complex models that were setup with `jet-schema`. | ||
```typescript | ||
import { Request, Response } from 'express'; | ||
import { parse, isNum } from 'util/validators.ts'; // standalone .parse function | ||
import User from 'models/User.ts' // This was setup with jet-schema | ||
const validateReqBody = parse({ | ||
userId: isNum, | ||
address: User.pick('address').test, | ||
}) | ||
/** | ||
* parse checks req.body and makes sure .userId is a number with the isNum | ||
* validator-function and that the .address is a valid User['address'] object | ||
* using the "User" schema setup by jet-schema. | ||
*/ | ||
function updateUsersAddr(req: Request, res: Response) { | ||
const { userId, address } = validateReqBody(req.body); | ||
...do stuff | ||
} | ||
``` | ||
> See this <a href="https://github.com/seanpmaxwell/express5-typescript-template/tree/master/src/routes">template</a> for a full example. | ||
<br/> | ||
## Author's Notes <a name="authors-notes"></a> | ||
Most schema validation libraries have fancy functions for validating objects and their properties, but the problem is I already have a lot of my own custom validation functions specific for each of my applications that I also like to copy-n-paste a lot and modify as needed (i.e. functions to check primitive-types, regexes for validating strings etc). The only thing that was making me use schema-validation libraries was trying to validate an object. So I thought, why not figure out a way to integrate my all the functions I had already written with something that can validate them against object properties? | ||
<br/> | ||
I know some libraries like `zod` have functions such as `.refine` which allow you to integrate custom validation logic. But this isn't quite the same because you could still chain additional validation logic onto `.refine` and you still have to look through the documentation to see how to do it. I wanted a library that allowed me to just slap in validator-functions that were not tied to any specific schema-validation library. | ||
<br/> | ||
If you want a library that includes all kinds of special functions for validating things other than objects, **jet-schema** is probably not for you. However, the vast majority of projects I've worked on have involved implementing lots of type-checking functions specific to the needs of that project. For example, maybe you use another datetime handling library other than JavasScript's `Date` object (i.e. `DayJs`). Instead of of having to dig into the library's features to accept `dayjs` objects as valid-objects, with **jet-schema** you can just drop in `dayjs.isValid`. | ||
<br/> |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
82575
16
5