packages/schema/src/json-schema.ts
import { _ } from '@feathersjs/commons'
import { JSONSchema } from 'json-schema-to-ts'
import { JSONSchemaDefinition, Ajv, Validator } from './schema'
export type DataSchemaMap = {
create: JSONSchemaDefinition
update?: JSONSchemaDefinition
patch?: JSONSchemaDefinition
}
export type DataValidatorMap = {
create: Validator
update: Validator
patch: Validator
}
/**
* Returns a compiled validation function for a schema and AJV validator instance.
*
* @param schema The JSON schema definition
* @param validator The AJV validation instance
* @returns A compiled validation function
*/
export const getValidator = <T = any, R = T>(schema: JSONSchemaDefinition, validator: Ajv): Validator<T, R> =>
validator.compile({
$async: true,
...(schema as any)
}) as any as Validator<T, R>
/**
* Returns compiled validation functions to validate data for the `create`, `update` and `patch`
* service methods. If not passed explicitly, the `update` validator will be the same as the `create`
* and `patch` will be the `create` validator with no required fields.
*
* @param def Either general JSON schema definition or a mapping of `create`, `update` and `patch`
* to their respecitve JSON schema
* @param validator The Ajv instance to use as the validator
* @returns A map of validator functions
*/
export const getDataValidator = (
def: JSONSchemaDefinition | DataSchemaMap,
validator: Ajv
): DataValidatorMap => {
const schema = ((def as any).create ? def : { create: def }) as DataSchemaMap
return {
create: getValidator(schema.create, validator),
update: getValidator(
schema.update || {
...(schema.create as any),
$id: `${schema.create.$id}Update`
},
validator
),
patch: getValidator(
schema.patch || {
...(schema.create as any),
$id: `${schema.create.$id}Patch`,
required: []
},
validator
)
}
}
export type PropertyQuery<D extends JSONSchema, X> = {
anyOf: [
D,
{
type: 'object'
additionalProperties: false
properties: {
$gt: D
$gte: D
$lt: D
$lte: D
$ne: D
$in: {
type: 'array'
items: D
}
$nin: {
type: 'array'
items: D
}
} & X
}
]
}
/**
* Create a Feathers query syntax compatible JSON schema definition for a property definition.
*
* @param def The property definition (e.g. `{ type: 'string' }`)
* @param extensions Additional properties to add to the query property schema
* @returns A JSON schema definition for the Feathers query syntax for this property.
*/
export const queryProperty = <T extends JSONSchema, X extends { [key: string]: JSONSchema }>(
def: T,
extensions: X = {} as X
) => {
const definition = _.omit(def, 'default')
return {
anyOf: [
definition,
{
type: 'object',
additionalProperties: false,
properties: {
$gt: definition,
$gte: definition,
$lt: definition,
$lte: definition,
$ne: definition,
$in:
definition.type === 'array'
? definition
: {
type: 'array',
items: definition
},
$nin:
definition.type === 'array'
? definition
: {
type: 'array',
items: definition
},
...extensions
}
}
]
} as const
}
/**
* Creates Feathers a query syntax compatible JSON schema for multiple properties.
*
* @param definitions A map of property definitions
* @param extensions Additional properties to add to the query property schema
* @returns The JSON schema definition for the Feathers query syntax for multiple properties
*/
export const queryProperties = <
T extends { [key: string]: JSONSchema },
X extends { [K in keyof T]?: { [key: string]: JSONSchema } }
>(
definitions: T,
extensions: X = {} as X
) =>
Object.keys(definitions).reduce(
(res, key) => {
const result = res as any
const definition = definitions[key]
result[key] = queryProperty(definition as JSONSchemaDefinition, extensions[key as keyof T])
return result
},
{} as { [K in keyof T]: PropertyQuery<T[K], X[K]> }
)
/**
* Creates a JSON schema for the complete Feathers query syntax including `$limit`, $skip`
* and `$sort` and `$select` for the allowed properties.
*
* @param definition The property definitions to create the query syntax schema for
* @param extensions Additional properties to add to the query property schema
* @returns A JSON schema for the complete query syntax
*/
export const querySyntax = <
T extends { [key: string]: JSONSchema },
X extends { [K in keyof T]?: { [key: string]: JSONSchema } }
>(
definition: T,
extensions: X = {} as X
) => {
const keys = Object.keys(definition)
const props = queryProperties(definition, extensions)
const $or = {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
properties: props
}
} as const
const $and = {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
properties: {
...props,
$or
}
}
} as const
return {
$limit: {
type: 'number',
minimum: 0
},
$skip: {
type: 'number',
minimum: 0
},
$sort: {
type: 'object',
properties: keys.reduce(
(res, key) => {
const result = res as any
result[key] = {
type: 'number',
enum: [1, -1]
}
return result
},
{} as { [K in keyof T]: { readonly type: 'number'; readonly enum: [1, -1] } }
)
},
$select: {
type: 'array',
maxItems: keys.length,
items: {
type: 'string',
...(keys.length > 0 ? { enum: keys as any as (keyof T)[] } : {})
}
},
$or,
$and,
...props
} as const
}
export const ObjectIdSchema = () =>
({
anyOf: [
{ type: 'string', objectid: true },
{ type: 'object', properties: {}, additionalProperties: true }
]
}) as const