feathersjs/feathers

View on GitHub
packages/schema/src/schema.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
import Ajv, { AsyncValidateFunction, ValidateFunction } from 'ajv'
import { FromSchema, JSONSchema } from 'json-schema-to-ts'
import { BadRequest } from '@feathersjs/errors'

export const DEFAULT_AJV = new Ajv({
  coerceTypes: true,
  addUsedSchema: false
})

export { Ajv }

/**
 * A validation function that takes data and returns the (possibly coerced)
 * data or throws a validation error.
 */
export type Validator<T = any, R = T> = (data: T) => Promise<R>

export type JSONSchemaDefinition = JSONSchema & {
  $id: string
  $async?: true
  properties?: { [key: string]: JSONSchema }
  required?: readonly string[]
}

export interface Schema<T> {
  validate<X = T>(...args: Parameters<ValidateFunction<X>>): Promise<X>
}

export class SchemaWrapper<S extends JSONSchemaDefinition> implements Schema<FromSchema<S>> {
  ajv: Ajv
  validator: AsyncValidateFunction
  readonly _type!: FromSchema<S>

  constructor(
    public definition: S,
    ajv: Ajv = DEFAULT_AJV
  ) {
    this.ajv = ajv
    this.validator = this.ajv.compile({
      $async: true,
      ...(this.definition as any)
    }) as AsyncValidateFunction
  }

  get properties() {
    return this.definition.properties as S['properties']
  }

  get required() {
    return this.definition.required as S['required']
  }

  async validate<T = FromSchema<S>>(...args: Parameters<ValidateFunction<T>>) {
    try {
      const validated = (await this.validator(...args)) as T

      return validated
    } catch (error: any) {
      throw new BadRequest(error.message, error.errors)
    }
  }

  toJSON() {
    return this.definition
  }
}

export function schema<S extends JSONSchemaDefinition>(definition: S, ajv: Ajv = DEFAULT_AJV) {
  return new SchemaWrapper(definition, ajv)
}