feathers/docs/api/schema/schema.md
2022-10-17 12:49:03 -07:00

10 KiB

outline
deep

Schemas

schema is a small wrapper around three existing projects:

This package essentially allows for a single place to define your types and validation rules in plain JavaScript or TypeScript which can then be used by many other parts of a Feathers application.

Schemas are also used by resolvers to validate and convert data before or after dynamically resolving properties.

You can find a lot of type-specific JSON Schema examples in the json-schema-to-ts docs.

Creating Schemas

Definitions

If you are not familiar with JSON schema have a look at the official getting started guide. Here is an example for a possible user schema:

import { HookContext } from './definitions'
import { schema, type Infer } from '@feathersjs/schema'

export const userSchema = schema({
  $id: 'User',
  type: 'object',
  additionalProperties: false,
  required: ['email', 'password'],
  properties: {
    id: { type: 'number' },
    email: { type: 'string' },
    password: { type: 'string' }
  }
} as const)

export type User = Infer<typeof userSchema>

To get the correct TypeScript types the definition always needs to be declared as const. See examples.

AJV

AJV is the JSON Schema validator that runs under the hood of Feathers Schema. We chose it because it's fully compliant with the JSON Schema spec and it's the fastest JSON Schema validator because it has its own compiler. It pre-compiles code for each validator, instead of dynamically creating validators from schemas during runtime. The Feathers Schema package takes care of the compiling part for you, so you generally don't have to do it yourself.

The only time you would need to manually compile validators would be if you're deploying to an environment where dynamic code generation APIs are not available, such as Cloudflare Workers. Any environment that doesn't support the eval function will require pre-compiling each schema.

AJV uses JavaScript's global eval function under the hood to pre-compile its schema. It's normally frowned upon to use eval due to security risks. In the case of Feathers Schema, the risk is eliminated as long as you are not dynamically compiling schema. The generator sets up the Feathers application to compile during startup, which virtually eliminates the risk of running arbitrary or malicious code.

The primary utility in AJV is the AJV class. You need an AJV class instance in order to register and run validators. Feathers Schema handles this for you under the hood by providing a default ajv singleton instance. Sometimes it's necessary to customize AJV to work in a specific way for your app. You'll learn how to provide a custom AJV instance to Feathers Schema in the next section.

Customize AJV

Learn what AJV options are customizable in the AJV class API docs.

Add Formatters to AJV

Save yourself some time making a custom formatter. Learn how to apply AJV's extended formatters in the ajv-formats docs.

Example of Customized AJV

See an example of custom keywords and formatters in the MongoDB adapter documentation.

schema(definition, ajv)

schema(definition, ajv) allows to initialize a schema with a custom AJV instance:

import ajvErrors from 'ajv-errors';
import Ajv form 'ajv';
import { schema } from '@feathersjs/schema';

const ajv = new Ajv({
  coerceTypes: true
});

ajvErrors(ajv);

export const userSchema = schema({
  $id: 'User',
  type: 'object',
  additionalProperties: false,
  required: ['email', 'password'],
  properties: {
    id: { type: 'number' },
    email: { type: 'string' },
    password: { type: 'string' }
  }
}, ajv);

Generating Correct Types

For correct TypeScript types, the definition always needs to be declared as const. This first example will not produce correct types because the definition is not immediately followed by as const:

// Will not produce correct types.
const definition = { type: 'object' } // `as const` is missing, here.
const userSchema = schema(definition)

This next example does declare as const after the definition, so the types will be generated correctly:

// Produces correct types.
const definition = { type: 'object' } as const
const userSchema = schema(definition)

Extending Schemas

To create a new schema that extends an existing one, combine the schema properties from schema.properties (and schema.required, if used) with the new properties:

import { HookContext } from './definitions'
import { schema, type Infer } from '@feathersjs/schema'

export const userSchema = schema({
  $id: 'User',
  type: 'object',
  additionalProperties: false,
  required: ['email', 'password'],
  properties: {
    email: { type: 'string' },
    password: { type: 'string' }
  }
} as const)

export type User = Infer<typeof userSchema>

export const userResultSchema = schema({
  $id: 'UserResult',
  type: 'object',
  additionalProperties: false,
  required: [...userSchema.required, 'id'],
  properties: {
    ...userSchema.properties,
    id: { type: 'number' }
  }
})

export type User = Infer<typeof userResultSchema>
import { schema } from '@feathersjs/schema'

export const userSchema = schema({
  $id: 'User',
  type: 'object',
  additionalProperties: false,
  required: ['email', 'password'],
  properties: {
    id: { type: 'number' },
    email: { type: 'string' },
    password: { type: 'string' }
  }
})

// The user result has all properties from the user but also an
// additional `id` added by the database
export const userResultSchema = schema({
  $id: 'UserResult',
  type: 'object',
  additionalProperties: false,
  required: [...userSchema.required, 'id'],
  properties: {
    ...userSchema.properties,
    id: { type: 'number' }
  }
})

Associations with $ref

Associated schemas can be initialized via the $ref keyword referencing the $id set during schema definition.

In TypeScript, the referenced type needs to be added explicitly.

import { HookContext } from './definitions'
import { schema, type Infer } from '@feathersjs/schema'

export const userSchema = schema({
  $id: 'User',
  type: 'object',
  additionalProperties: false,
  required: ['email', 'password'],
  properties: {
    id: { type: 'number' },
    email: { type: 'string' },
    password: { type: 'string' }
  }
})

export type User = Infer<typeof userSchema>

export const messageSchema = schema({
  $id: 'Message',
  type: 'object',
  additionalProperties: false,
  required: ['text'],
  properties: {
    text: { type: 'string' },
    user: { $ref: 'User' }
  }
})

export type Message = Infer<typeof messageSchema> & {
  user: User
}

Query Helpers

Schema ships with a few helpers to automatically create schemas that comply with the Feathers query syntax (like $gt, $ne etc.):

queryProperty

queryProperty helper takes a definition for a single property and returns a schema that allows the default query operators. This helper supports the operators listed, below. Learn what each one means in the common query operator documentation.

  • $gt
  • $gte
  • $lt
  • $lte
  • $ne
  • $in
  • $nin

The name property in the example, below, shows how queryProperty wraps a single property's definition.

export const userQuerySchema = schema({
  $id: 'UserQuery',
  type: 'object',
  additionalProperties: false,
  properties: {
    name: queryProperty({ type: 'string' })
  }
} as const)

With the queryProperty utility in place, the schema will allow querying on name using any of the above-listed operators. With it in place, the query in the following example will not throw an error:

const query = { name: { $in: ['Marco', 'Polo'] } }

app.service('users').find({ query })

You can learn how it works, here.

queryProperties

queryProperties(schema.properties) takes the all properties of a schema and converts them into query schema properties (using queryProperty)

querySyntax

querySyntax(schema.properties) initializes all properties the additional query syntax properties $limit, $skip, $select and $sort. $select and $sort will be typed so they only allow existing schema properties.

import { querySyntax, type Infer } from '@feathersjs/schema';

export const userQuerySchema = schema({
  $id: 'UserQuery',
  type: 'object',
  additionalProperties: false,
  properties: {
    ...querySyntax(userSchema.properties)
  }
} as const);

export type UserQuery = Infer<typeof userQuerySchema>

const userQuery: UserQuery = {
  $limit: 10,
  $select: [ 'email', 'id' ],
  $sort: {
    email: 1
  }
}

Validation hooks

Schemas will be used for validation when they are passed to a Resolver. See the Feathers resolver on how to use the schema with resolvers.