diff --git a/src/Schema.ts b/src/Schema.ts index 44154c4..d343bc3 100644 --- a/src/Schema.ts +++ b/src/Schema.ts @@ -10,14 +10,27 @@ import ValidationError, { FieldErrors } from './errors/ValidationError' import SchemaField, { FieldProperties } from './SchemaField' import FieldResolutionError from './errors/FieldResolutionError' -// todo allow type inference (autocomplete schema fields from fields definition) -interface FieldsDefinition { - [key: string]: FieldProperties; +type Fields = Record + +type SchemaFields = { [K in keyof F]: SchemaField } + +/** + * Makes fields partial. (required: false) + */ +export type PartialFields = { + [K in keyof F]: Omit & { required: false } +} + +/** + * Makes fields mandatory. (required: true) + */ +export type RequiredFields = { + [K in keyof F]: Omit & { required: true } } interface ValidateOptions { clean?: boolean; - context?: Record, + context?: Record; ignoreMissing?: boolean; ignoreUnknown?: boolean; parse?: boolean; @@ -25,20 +38,19 @@ interface ValidateOptions { removeUnknown?: boolean; } -class Schema { - public fields: { [key: string]: SchemaField } - - constructor (fields: FieldsDefinition) { - this.fields = {} +class Schema { + public fields: SchemaFields = {} as any + constructor (fields: F) { // Set fields. - Object.keys(fields).forEach((name: string): void => { - this.fields[name] = new SchemaField(name, fields[name]) + Object.keys(fields).forEach((name: keyof F): void => { + this.fields[name] = new SchemaField(String(name), fields[name]) }) } /** * Returns a clean copy of the object. + * todo move to util functions * @param object * @param options */ @@ -66,21 +78,26 @@ class Schema { /** * Returns a clone of the schema. */ - clone (): Schema { - return this.pick(Object.keys(this.fields)) + clone (): Schema { + const fields: Fields = {} + + Object.keys(this.fields).forEach((name) => { + fields[name] = this.fields[name].getProperties() + }) + return new Schema(deepExtend({}, fields)) } /** * Returns a new schema based on current schema. - * @param fields + * @param newFields */ - extend (fields: FieldsDefinition): Schema { - const fieldsDefinition: FieldsDefinition = {} + extend (newFields: NF): Schema { + const fields: Fields = {} - Object.keys(this.fields).forEach((name: string): void => { - fieldsDefinition[name] = this.fields[name].getProperties() + Object.keys(this.fields).forEach((name) => { + fields[name] = this.fields[name].getProperties() }) - return new Schema(deepExtend({}, fieldsDefinition, fields)) + return new Schema(deepExtend({}, fields, newFields)) } /** @@ -120,14 +137,14 @@ class Schema { * Returns a field. * @param name */ - getField (name: string): SchemaField { - return this.resolveField(name) + getField (name: N): SchemaFields[N] { + return this.fields[name] } /** * Returns all fields. */ - getFields (): { [key: string]: SchemaField } { + getFields (): SchemaFields { return this.fields } @@ -149,11 +166,11 @@ class Schema { * Returns a sub schema without some fields. * @param fieldNames */ - omit (fieldNames: string[]): Schema { - const fields: FieldsDefinition = {} + omit (fieldNames: K[]): Schema> { + const fields: Fields = {} - Object.keys(this.fields).forEach((name: string): void => { - if (fieldNames.indexOf(name) === -1) { + Object.keys(this.fields).forEach((name) => { + if (!fieldNames.includes(name as K)) { fields[name] = this.fields[name].getProperties() } }) @@ -178,39 +195,26 @@ class Schema { /** * Returns a copy of the schema where all fields are not required. */ - partial () { - const fields: FieldsDefinition = {} + partial (): Schema> { + const fields: Fields = {} - Object.keys(this.fields).forEach((name: string): void => { + Object.keys(this.fields).forEach((name) => { fields[name] = deepExtend({}, this.fields[name].getProperties()) fields[name].required = false }) return new Schema(deepExtend({}, fields)) } - /** - * Returns a copy of the schema where all fields are required. - */ - required () { - const fields: FieldsDefinition = {} - - Object.keys(this.fields).forEach((name: string): void => { - fields[name] = deepExtend({}, this.fields[name].getProperties()) - fields[name].required = true - }) - return new Schema(deepExtend({}, fields)) - } - /** * Returns a sub schema from selected fields. * @param fieldNames */ - pick (fieldNames: string[]): Schema { - const fields: FieldsDefinition = {} + pick (fieldNames: K[]): Schema> { + const fields: Fields = {} - fieldNames.forEach((name: string): void => { + fieldNames.forEach((name) => { if (typeof this.fields[name] !== 'undefined') { - fields[name] = this.fields[name].getProperties() + fields[String(name)] = this.fields[name].getProperties() } }) return new Schema(deepExtend({}, fields)) @@ -220,14 +224,14 @@ class Schema { * Returns a copy of the object without unknown fields. * @param object */ - removeUnknownFields (object: Record): T { + removeUnknownFields (object: Record): Schema { if (object == null) { return object } const clone = deepExtend({}, object) Object.keys(clone).forEach((name: string): void => { - const field: SchemaField = this.fields[name] + const field = this.fields[name] if (typeof field === 'undefined') { delete clone[name] @@ -235,8 +239,8 @@ class Schema { clone[name] = (field.getType() as Schema).removeUnknownFields(clone[name]) } else if (field.getItems()?.type instanceof Schema) { if (clone[name] instanceof Array) { - clone[name] = clone[name].map((item: any) => ( - (field.getItems().type as Schema).removeUnknownFields(item) + clone[name] = clone[name].map((item) => ( + (field.getItems()?.type as Schema).removeUnknownFields(item) )) } } @@ -244,15 +248,28 @@ class Schema { return clone } + /** + * Returns a copy of the schema where all fields are required. + */ + required (): Schema> { + const fields = {} as Fields + + Object.keys(this.fields).forEach((name: string): void => { + fields[name] = deepExtend({}, this.fields[name].getProperties()) + fields[name].required = true + }) + return new Schema(deepExtend({}, fields)) + } + /** * Builds an object from a string (ex: [colors][0][code]). * @param path (ex: address[country][code]) * @param syntaxChecked - * @throws {SyntaxError|TypeError} */ - resolveField (path: string, syntaxChecked = false): SchemaField { + resolveField> (path: keyof F | string, syntaxChecked = false): T { + const p = path.toString() // Removes array indexes from path because we want to resolve field and not data. - const realPath = path.replace(/\[\d+]/g, '') + const realPath = p.replace(/\[\d+]/g, '') const bracketIndex = realPath.indexOf('[') const bracketEnd = realPath.indexOf(']') @@ -262,15 +279,15 @@ class Schema { if (!syntaxChecked) { // Check for extra space. if (realPath.indexOf(' ') !== -1) { - throw new SyntaxError(`path "${path}" is not valid`) + throw new SyntaxError(`path "${p}" is not valid`) } // Check if key is not defined (ex: []). if (realPath.indexOf('[]') !== -1) { - throw new SyntaxError(`missing array index or object attribute in "${path}"`) + throw new SyntaxError(`missing array index or object attribute in "${p}"`) } // Check for missing object attribute. if (dotIndex + 1 === realPath.length) { - throw new SyntaxError(`missing object attribute in "${path}"`) + throw new SyntaxError(`missing object attribute in "${p}"`) } const closingBrackets = realPath.split(']').length @@ -278,15 +295,15 @@ class Schema { // Check for missing opening bracket. if (openingBrackets < closingBrackets) { - throw new SyntaxError(`missing opening bracket "[" in "${path}"`) + throw new SyntaxError(`missing opening bracket "[" in "${p}"`) } // Check for missing closing bracket. if (closingBrackets < openingBrackets) { - throw new SyntaxError(`missing closing bracket "]" in "${path}"`) + throw new SyntaxError(`missing closing bracket "]" in "${p}"`) } } - let name = realPath + let name: keyof F = realPath let subPath // Resolve dot "." path. @@ -313,26 +330,29 @@ class Schema { } if (typeof this.fields[name] === 'undefined') { - throw new FieldResolutionError(path) + throw new FieldResolutionError(p) } - let field: SchemaField = this.fields[name] + const field = this.fields[name] + // Get nested field if (typeof subPath === 'string' && subPath.length > 0) { const type = field.getType() const props = field.getProperties() if (type instanceof Schema) { - field = type.resolveField(subPath, true) - } else if (typeof props.items !== 'undefined' && + return type.resolveField(subPath, true) + } + if (typeof props.items !== 'undefined' && typeof props.items.type !== 'undefined' && props.items.type instanceof Schema) { - field = props.items.type.resolveField(subPath, true) - } else { - throw new FieldResolutionError(path) + return props.items.type.resolveField(subPath, true) } + } else if (name in this.fields) { + // @ts-ignore fixme TS error + return field } - return field + throw new FieldResolutionError(p) } /** @@ -420,15 +440,6 @@ class Schema { } return clone } - - /** - * Returns a sub schema without some fields. - * @deprecated use `omit()` instead - * @param fieldNames - */ - without (fieldNames: string[]): Schema { - return this.omit(fieldNames) - } } export default Schema diff --git a/src/SchemaField.ts b/src/SchemaField.ts index 500e472..99f4c8b 100644 --- a/src/SchemaField.ts +++ b/src/SchemaField.ts @@ -24,7 +24,6 @@ import { checkType, checkTypeArray, checkUniqueItems, - Computable, FieldFormat, FieldItems, FieldMinMax, @@ -37,56 +36,48 @@ import ValidationError, { FieldErrors } from './errors/ValidationError' import Schema from './Schema' import { computeValue, joinPath } from './utils' -interface ValidateOptions { +type ValidateOptions = { clean?: boolean; context?: Record; path?: string; rootOnly?: boolean; } -export interface FieldProperties { - allowed?: Computable; - +export type FieldProperties = { + allowed?: any[]; check? (value: any, context?: Record): boolean; - clean? (value: any): any; - - denied?: Computable; - format?: Computable; - items?: FieldItems; - label?: Computable; - length?: Computable; - max?: Computable; - maxItems?: Computable; - maxLength?: Computable; - maxWords?: Computable; - min?: Computable; - minItems?: Computable; - minLength?: Computable; - minWords?: Computable; - multipleOf?: Computable; - + denied?: any[]; + format?: FieldFormat; + items?: FieldItems; + label?: string; + length?: number; + max?: FieldMinMax; + maxItems?: number; + maxLength?: number; + maxWords?: number; + min?: FieldMinMax; + minItems?: number; + minLength?: number; + minWords?: number; + multipleOf?: number; parse? (value: any): any; - - pattern?: Computable; - + pattern?: FieldPattern; prepare? (value: any, context?: Record): any; - - required?: Computable; - type?: Computable>; - uniqueItems?: Computable + required?: boolean; + type?: FieldType; + uniqueItems?: boolean; } -class SchemaField { +class SchemaField

{ public name: string + public properties: P - public properties: FieldProperties - - constructor (name: string, properties: FieldProperties) { + constructor (name: string, properties: P) { // Default properties - const props: FieldProperties = { - label: name, - ...properties + const props: P = { + ...properties, + label: properties.label ?? name } checkFieldProperties(name, props) @@ -100,7 +91,7 @@ class SchemaField { * @param value * @param options */ - clean (value?: any | any[] | Schema, options = {}): string | undefined { + clean (value?: any, options = {}): string | undefined { if (value == null) { return value } @@ -136,136 +127,121 @@ class SchemaField { /** * Returns field's allowed values. - * @param context */ - getAllowed (context?: Record): any[] { - return computeValue(this.properties.allowed, context) + getAllowed (): P['allowed'] { + return this.properties.allowed } /** * Returns field's denied values. - * @param context */ - getDenied (context?: Record): any[] { - return computeValue(this.properties.denied, context) + getDenied (): P['denied'] { + return this.properties.denied } /** * Returns field's format. - * @param context */ - getFormat (context?: Record): FieldFormat { - return computeValue(this.properties.format, context) + getFormat (): P['format'] { + return this.properties.format } /** * Returns field's items. - * @param context */ - getItems (context?: Record): FieldItems { - return computeValue>(this.properties.items, context) + getItems (): P['items'] { + return this.properties.items } /** * Returns field's label. - * @param context */ - getLabel (context?: Record): string { - return computeValue(this.properties.label, context) + getLabel (): P['label'] { + return this.properties.label } /** * Returns field's length. - * @param context */ - getLength (context?: Record): number { - return computeValue(this.properties.length, context) + getLength (): P['length'] { + return this.properties.length } /** * Returns field's maximal value. - * @param context */ - getMax (context?: Record): FieldMinMax { - return computeValue(this.properties.max, context) + getMax (): P['max'] { + return this.properties.max } /** * Returns field's maximal length. - * @param context */ - getMaxLength (context?: Record): number { - return computeValue(this.properties.maxLength, context) + getMaxLength (): P['maxLength'] { + return this.properties.maxLength } /** * Returns field's maximal words. - * @param context */ - getMaxWords (context?: Record): number { - return computeValue(this.properties.maxWords, context) + getMaxWords (): P['maxWords'] { + return this.properties.maxWords } /** * Returns field's minimal value. - * @param context */ - getMin (context?: Record): FieldMinMax { - return computeValue(this.properties.min, context) + getMin (): P['min'] { + return this.properties.min } /** * Returns field's minimal length. - * @param context */ - getMinLength (context?: Record): number { - return computeValue(this.properties.minLength, context) + getMinLength (): P['minLength'] { + return this.properties.minLength } /** * Returns field's minimal words. - * @param context */ - getMinWords (context?: Record): number { - return computeValue(this.properties.minWords, context) + getMinWords (): P['minWords'] { + return this.properties.minWords } /** * Returns field name. */ getName (): string { - return computeValue(this.properties.label) || this.name + return this.name } /** * Returns field's pattern (regular expression). - * @param context */ - getPattern (context?: Record): FieldPattern { - return computeValue(this.properties.pattern, context) + getPattern (): P['pattern'] { + return this.properties.pattern } /** * Returns a copy of the field's properties. */ - getProperties (): FieldProperties { + getProperties (): P { return deepExtend({}, this.properties) } /** * Returns field's type. - * @param context */ - getType (context?: Record): FieldType { - return computeValue>(this.properties.type, context) + getType (): P['type'] { + return this.properties.type } /** * Checks if field is required - * @param context */ - isRequired (context?: Record): boolean { - return computeValue(this.properties.required, context) || false + isRequired (): P['required'] { + return this.properties.required ?? false } /** @@ -286,23 +262,15 @@ class SchemaField { * Parses a value. * @param value */ - parse (value: any): E { + parse (value: any): E | null { if (value == null) { - return value + return null } let val - const props: FieldProperties = this.properties + const props = this.properties - if (typeof value === 'object') { - // Parses all values in the array. - if (value instanceof Array) { - val = value.map((key: number) => this.parse(value[key])) - } else if (props.type instanceof Schema && typeof props.type?.parse === 'function') { - // todo test this line - val = props.type.parse(value) - } - } else if (typeof value === 'string') { + if (typeof value === 'string') { if (typeof props.parse === 'function') { val = props.parse.call(this, value) } else { @@ -318,6 +286,12 @@ class SchemaField { break } } + } else if (value instanceof Array) { + // Parses all values in the array. + val = value.map((key: number) => this.parse(value[key])) + } else if (props.type instanceof Schema && typeof props.type?.parse === 'function') { + // todo add test for this line + val = props.type.parse(value) } return val } @@ -360,9 +334,9 @@ class SchemaField { context, path } = opts - const props: FieldProperties = this.properties - const label: string = computeValue(props.label, context) - const isRequired: boolean = computeValue(props.required, context) || false + const props = this.properties + const label = props.label ?? this.name + const isRequired: boolean = props.required ?? false const isArray: boolean = props.type === 'array' || props.type instanceof Array // Prepare value @@ -403,9 +377,13 @@ class SchemaField { }) } } else if (typeof props.type === 'function') { + // todo remove in v5 // Check if value is an instance of the function. if (!(newVal instanceof props.type)) { - throw new FieldTypeError(label, props.type.name, path) + throw new FieldTypeError(label, + // @ts-ignore + props.type.name, + path) } } else if (props.type instanceof Array) { // Check different types (ex: ['string', 'number']) @@ -425,18 +403,20 @@ class SchemaField { // Validate all values of the array. for (let i = 0; i < newVal.length; i += 1) { - const field: SchemaField = new SchemaField(`[${i}]`, props.items) - try { - field.validate(newVal[i], opts) - } catch (error) { - if (error instanceof FieldError) { - errors[error.path] = error - } else if (error instanceof ValidationError) { - Object.entries(error.errors).forEach(([fieldPath, fieldError]) => { - errors[fieldPath] = fieldError - }) - } else { - throw error + const itemPath = `${path}[${i}]` + if (props.items.type != null) { + try { + checkType(props.items.type, newVal[i], label, itemPath) + } catch (error) { + if (error instanceof FieldError) { + errors[error.path] = error + } else if (error instanceof ValidationError) { + Object.entries(error.errors).forEach(([fieldPath, fieldError]) => { + errors[fieldPath] = fieldError + }) + } else { + throw error + } } } } @@ -449,77 +429,62 @@ class SchemaField { if (props.uniqueItems != null && newVal instanceof Array && computeValue(props.uniqueItems, context)) { checkUniqueItems(newVal, label, path) } - // Check allowed values if (props.allowed != null) { - checkAllowed(computeValue(props.allowed, context), newVal, label, path) + checkAllowed(props.allowed, newVal, label, path) } - // Check denied values if (props.denied != null) { - checkDenied(computeValue(props.denied, context), newVal, label, path) + checkDenied(props.denied, newVal, label, path) } - // Check string format if (props.format != null) { - checkFormat(computeValue(props.format, context), newVal, label, path) + checkFormat(props.format, newVal, label, path) } - // Check length if value has the length attribute if (props.length != null) { - checkLength(computeValue(props.length, context), newVal, label, path) + checkLength(props.length, newVal, label, path) } - // Check max items if (props.maxItems != null && newVal != null) { - checkMaxItems(computeValue(props.maxItems, context), newVal, label, path) + checkMaxItems(props.maxItems, newVal, label, path) } - // Check min items if (props.minItems != null && newVal != null) { - checkMinItems(computeValue(props.minItems, context), newVal, label, path) + checkMinItems(props.minItems, newVal, label, path) } - // Check maximal length if (props.maxLength != null) { - checkMaxLength(computeValue(props.maxLength, context), newVal, label, path) + checkMaxLength(props.maxLength, newVal, label, path) } - // Check minimal length if (props.minLength != null) { - checkMinLength(computeValue(props.minLength, context), newVal, label, path) + checkMinLength(props.minLength, newVal, label, path) } - // Check maximal words if (props.maxWords != null) { - checkMaxWords(computeValue(props.maxWords, context), newVal, label, path) + checkMaxWords(props.maxWords, newVal, label, path) } - // Check minimal words if (props.minWords != null) { - checkMinWords(computeValue(props.minWords, context), newVal, label, path) + checkMinWords(props.minWords, newVal, label, path) } - // Check maximal value if (props.max != null) { - checkMax(computeValue(props.max, context), newVal, label, path) + checkMax(props.max, newVal, label, path) } - // Check minimal value if (props.min != null) { - checkMin(computeValue(props.min, context), newVal, label, path) + checkMin(props.min, newVal, label, path) } - // Check if value is a multiple of a number. if (props.multipleOf != null) { - checkMultipleOf(computeValue(props.multipleOf, context), newVal, label, path) + checkMultipleOf(props.multipleOf, newVal, label, path) } - // Test regular expression if (props.pattern != null) { - checkPattern(computeValue(props.pattern, context), newVal, label, path) + checkPattern(props.pattern, newVal, label, path) } - // Execute custom checks if (props.check != null && !props.check.call(this, newVal, context)) { throw new FieldError(label, path) diff --git a/src/checks.ts b/src/checks.ts index ad0928c..a1b4376 100644 --- a/src/checks.ts +++ b/src/checks.ts @@ -37,31 +37,33 @@ export type FieldFormat = 'date' | 'datetime' | 'date-time' + // todo add 'duration' | 'email' | 'hostname' | 'ipv4' | 'ipv6' | 'time' - | 'uri'; + | 'uri' +// todo add 'uuid' export type FieldMinMax = number | Date export type FieldPattern = string | RegExp -export type FieldType = +export type FieldType = 'array' | 'boolean' - | 'function' + | 'function' // todo remove in v5 | 'integer' | 'number' | 'object' | 'string' | Schema + // todo remove in v5 | Array<'array' | 'boolean' | 'function' | 'integer' | 'number' | 'object' | 'string' | Schema> - | T; -export type FieldItems = { - type?: FieldType +export type FieldItems = { + type?: FieldType } export type Computable = T | ((context: Record) => T) @@ -69,11 +71,12 @@ export type Computable = T | ((context: Record) => T) /** * Schema field properties */ -const FIELD_PROPERTIES: string[] = [ +const FIELD_PROPERTIES: (keyof FieldProperties)[] = [ 'allowed', 'check', 'clean', 'denied', + // todo add 'enum', 'format', 'items', 'label', @@ -87,7 +90,6 @@ const FIELD_PROPERTIES: string[] = [ 'minLength', 'minWords', 'multipleOf', - 'name', 'parse', 'pattern', 'prepare', @@ -139,10 +141,11 @@ export function checkDenied (denied: any[], value: any, label: string, path: str * @param name * @param props */ -export function checkFieldProperties (name: string, props: FieldProperties): void { +export function checkFieldProperties (name: string, props: FieldProperties): void { // Check unknown properties. - Object.keys(props).forEach((prop) => { - if (!FIELD_PROPERTIES.includes(prop)) { + const keys = Object.keys(props) + keys.forEach((prop) => { + if (!FIELD_PROPERTIES.includes(prop as keyof FieldProperties)) { // eslint-disable-next-line no-console console.warn(`Unknown schema field property "${name}.${prop}"`) } @@ -494,8 +497,8 @@ export function checkRequired (required: boolean, value: any, label: string, pat * @param label * @param path */ -export function checkType ( - type: FieldType, +export function checkType ( + type: FieldType, value: any[] | boolean | number | object | string | ((...args: any[]) => void), label: string, path: string @@ -551,8 +554,8 @@ export function checkType ( * @param label * @param path */ -export function checkTypeArray ( - types: Array>, +export function checkTypeArray ( + types: Array, values: Array void)>, label: string, path: string diff --git a/test/Schema.test.ts b/test/Schema.test.ts index 4b5f570..f3bee76 100644 --- a/test/Schema.test.ts +++ b/test/Schema.test.ts @@ -27,7 +27,7 @@ describe('Schema', () => { type: 'boolean' }, date: { - type: Date, + type: 'function', parse (value) { const [year, month, day] = value.split('-') return new Date(year, parseInt(month, 10) - 1, day) @@ -62,15 +62,23 @@ describe('Schema', () => { }) describe('extend(schema)', () => { - const ExtendedSchema = BaseSchema.extend({ extended: {} }) + const NewSchema = new Schema({ + a: { type: 'string' } + }) + const ExtendedSchema = NewSchema.extend({ + b: { type: 'string' } + }) it('should create an extended version of the schema', () => { - expect(typeof ExtendedSchema.getField('array')).not.toBeUndefined() - expect(typeof ExtendedSchema.getField('extended')).not.toBeUndefined() + expect(ExtendedSchema.getField('a')).toBeDefined() + expect(ExtendedSchema.getField('b')).toBeDefined() }) it('should not modify parent schema', () => { - expect(() => BaseSchema.getField('extended')).toThrow(FieldResolutionError) + expect(BaseSchema.getField( + // @ts-expect-error key not defined + 'b' + )).toBeUndefined() }) }) @@ -115,7 +123,10 @@ describe('Schema', () => { describe('with invalid field name', () => { it('should throw an error', () => { - expect(() => BaseSchema.getField('unknown').getType()).toThrow() + expect(() => BaseSchema.getField( + // @ts-expect-error key not defined + 'unknown' + ).getType()).toThrow() }) }) }) @@ -152,7 +163,9 @@ describe('Schema', () => { string: 'test', unknown: true } - const result = { string: object.string } + const result = { + string: object.string + } expect(BaseSchema.removeUnknownFields(object)).toMatchObject(result) }) @@ -178,8 +191,10 @@ describe('Schema', () => { unknown: true }] } - const result = { embeddedArray: [{ string: 'test' }] } - expect(result).toMatchObject(BaseSchema.removeUnknownFields(object)) + const result = { + embeddedArray: [{ string: 'test' }] + } + expect(BaseSchema.removeUnknownFields(object)).toMatchObject(result) }) }) @@ -451,18 +466,25 @@ describe('Schema', () => { }) describe('omit(fieldNames)', () => { - const NewSchema = BaseSchema.omit(['string']) + const NewSchema = new Schema({ + a: { type: 'string' }, + b: { type: 'boolean' }, + c: { type: 'number' } + }).omit(['a', 'b']) it('should return a schema without excluded fields', () => { - expect(() => { - NewSchema.getField('string') - }).toThrow(FieldResolutionError) + expect(NewSchema.getField( + // @ts-expect-error key not defined + 'a' + )).toBeUndefined() + expect(NewSchema.getField( + // @ts-expect-error key not defined + 'b' + )).toBeUndefined() }) it('should return a schema with non excluded fields', () => { - expect(() => { - NewSchema.getField('number') - }).not.toThrow() + expect(NewSchema.getField('c')).toBeDefined() }) it('should not modify parent schema', () => { diff --git a/test/SchemaField.test.ts b/test/SchemaField.test.ts index ceb4ce6..1edf511 100644 --- a/test/SchemaField.test.ts +++ b/test/SchemaField.test.ts @@ -1,6 +1,6 @@ /* * The MIT License (MIT) - * Copyright (c) 2023 Karl STEIN + * Copyright (c) 2024 Karl STEIN */ import { describe, expect, it } from '@jest/globals' @@ -345,7 +345,7 @@ describe('SchemaField', () => { }) it('should return value using parse function', () => { - expect(field.parse('2018-04-05').getTime()) + expect(field.parse('2018-04-05')?.getTime()) .toBe(new Date(2018, 3, 5).getTime()) }) }) @@ -429,30 +429,6 @@ describe('SchemaField', () => { }) }) }) - - describe('allowed: Function', () => { - const field = new SchemaField('field', { - allowed: () => (['off', 'on']) - }) - - describe('with allowed values', () => { - it('should not throw FieldAllowedError', () => { - expect(() => { - field.validate(['on']) - }) - .not.toThrow() - }) - }) - - describe('without allowed values', () => { - it('should throw FieldAllowedError', () => { - expect(() => { - field.validate(['yes']) - }) - .toThrow(FieldAllowedError) - }) - }) - }) }) describe('with check', () => { @@ -503,30 +479,6 @@ describe('SchemaField', () => { }) }) }) - - describe('denied: Function', () => { - const field = new SchemaField('field', { - denied: () => (['off', 'on']) - }) - - describe('with denied values', () => { - it('should throw FieldDeniedError', () => { - expect(() => { - field.validate(['on']) - }) - .toThrow(FieldDeniedError) - }) - }) - - describe('without denied values', () => { - it('should not throw FieldDeniedError', () => { - expect(() => { - field.validate(['yes']) - }) - .not.toThrow() - }) - }) - }) }) describe('with items', () => { @@ -1398,36 +1350,6 @@ describe('SchemaField', () => { }) }) }) - - describe('minWords: Function', () => { - const field = new SchemaField('field', { - minWords: () => 5 - }) - - describe('with length lower than minWords', () => { - it('should throw FieldMinWordsError', () => { - expect(() => { - field.validate('') - }).toThrow(FieldMinWordsError) - }) - }) - - describe('with length equal to minWords', () => { - it('should not throw FieldMinWordsError', () => { - expect(() => { - field.validate('0 0 0 0 0') - }).not.toThrow() - }) - }) - - describe('with length higher than minWords', () => { - it('should throw FieldMinWordsError', () => { - expect(() => { - field.validate('0 0 0 0 0 0') - }).not.toThrow() - }) - }) - }) }) describe('with multipleOf', () => { @@ -1546,40 +1468,6 @@ describe('SchemaField', () => { }) }) }) - - describe('pattern: Function', () => { - const options = { context: { basic: true } } - const patternField = new SchemaField('field', { - pattern: (context) => (context.basic ? /^[a-z0-9]+$/ : /^[a-z0-9_-]+$/) - }) - - describe('with incorrect value', () => { - it('should throw FieldPatternError', () => { - expect(() => { - patternField.validate('abc_123', options) - }) - .toThrow(FieldPatternError) - }) - }) - - describe('with correct value', () => { - it('should not throw FieldPatternError', () => { - expect(() => { - patternField.validate('abc123', options) - }) - .not.toThrow() - }) - }) - - describe('with undefined', () => { - it('should not throw FieldPatternError', () => { - expect(() => { - patternField.validate(undefined, options) - }) - .not.toThrow() - }) - }) - }) }) describe('with required', () => { @@ -1684,31 +1572,6 @@ describe('SchemaField', () => { }) }) }) - - describe('required: Function', () => { - const options = { context: { checked: true } } - const requiredField = new SchemaField('field', { - required: (context) => context.checked === true - }) - - describe('with undefined', () => { - it('should throw FieldRequiredError', () => { - expect(() => { - requiredField.validate(undefined, options) - }) - .toThrow(FieldRequiredError) - }) - }) - - describe('with null', () => { - it('should throw FieldRequiredError', () => { - expect(() => { - requiredField.validate(null, options) - }) - .toThrow(FieldRequiredError) - }) - }) - }) }) describe('with type', () => {