23
src/libs/Schema/Items/DzeioLiteral.ts
Normal file
23
src/libs/Schema/Items/DzeioLiteral.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import SchemaItem, { type JSONSchemaItem } from '../SchemaItem'
|
||||
|
||||
export default class DzeioLiteral<T> extends SchemaItem<T> {
|
||||
public constructor(private readonly value: T) {
|
||||
super()
|
||||
this.validations.push({
|
||||
fn(input) {
|
||||
return input === value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is T {
|
||||
return typeof input === typeof this.value
|
||||
}
|
||||
|
||||
public override toJSON(): JSONSchemaItem {
|
||||
return {
|
||||
type: 'literal',
|
||||
params: [this.value as string]
|
||||
}
|
||||
}
|
||||
}
|
93
src/libs/Schema/Items/SchemaArray.ts
Normal file
93
src/libs/Schema/Items/SchemaArray.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import type { ValidationError, ValidationResult } from '..'
|
||||
import SchemaItem from '../SchemaItem'
|
||||
|
||||
export default class SchemaArray<A> extends SchemaItem<Array<A>> {
|
||||
|
||||
public constructor(
|
||||
private readonly values: SchemaItem<A>
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public override parse(input: unknown): A[] | unknown {
|
||||
// let master handle the first pass is desired
|
||||
input = super.parse(input)
|
||||
|
||||
if (!Array.isArray(input)) {
|
||||
return input
|
||||
}
|
||||
|
||||
const clone = []
|
||||
for (const item of input) {
|
||||
clone.push(this.values.parse(item))
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
public override validate(input: A[], fast = false): ValidationResult<A[]> {
|
||||
const tmp = super.validate(input, fast)
|
||||
if (tmp.error) {
|
||||
return tmp
|
||||
}
|
||||
const clone: Array<A> = []
|
||||
const errs: Array<ValidationError> = []
|
||||
for (let idx = 0; idx < tmp.object.length; idx++) {
|
||||
const item = tmp.object[idx];
|
||||
const res = this.values.validate(item as A)
|
||||
if (res.error) {
|
||||
const errors = res.error.map((it) => ({
|
||||
message: it.message,
|
||||
field: it.field ? `${idx}.${it.field}` : idx.toString()
|
||||
}))
|
||||
if (fast) {
|
||||
return {
|
||||
error: errors
|
||||
}
|
||||
}
|
||||
errs.push(...errors)
|
||||
} else {
|
||||
clone.push(res.object as A)
|
||||
}
|
||||
}
|
||||
|
||||
if (errs.length > 0) {
|
||||
return {
|
||||
error: errs
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
object: clone
|
||||
}
|
||||
}
|
||||
|
||||
public override transform(input: A[]): A[] {
|
||||
const clone = []
|
||||
for (const item of super.transform(input)) {
|
||||
clone.push(this.values.transform(item))
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
/**
|
||||
* transform the array so it only contains one of each elements
|
||||
*/
|
||||
public unique(): this {
|
||||
this.transforms.push((input) => input.filter((it, idx) => input.indexOf(it) === idx))
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is Array<A> {
|
||||
return Array.isArray(input)
|
||||
}
|
||||
|
||||
// public override toJSON(): JSONSchemaItem {
|
||||
// return {
|
||||
// type: 'array',
|
||||
// childs: this.values
|
||||
// }
|
||||
// }
|
||||
}
|
8
src/libs/Schema/Items/SchemaBoolean.ts
Normal file
8
src/libs/Schema/Items/SchemaBoolean.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import SchemaItem from '../SchemaItem'
|
||||
|
||||
export default class SchemaBoolean extends SchemaItem<boolean> {
|
||||
|
||||
public override isOfType(input: unknown): input is boolean {
|
||||
return typeof input === 'boolean'
|
||||
}
|
||||
}
|
48
src/libs/Schema/Items/SchemaDate.ts
Normal file
48
src/libs/Schema/Items/SchemaDate.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import SchemaItem from '../SchemaItem'
|
||||
|
||||
export default class SchemaDate extends SchemaItem<Date> {
|
||||
|
||||
public parseString(): this {
|
||||
this.parseActions.push((input) => typeof input === 'string' ? new Date(input) : input)
|
||||
return this
|
||||
}
|
||||
|
||||
public min(value: Date, message?: string): this {
|
||||
this.validations.push({
|
||||
fn(input) {
|
||||
return input >= value
|
||||
},
|
||||
message: message
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public parseFromExcelString(): this {
|
||||
this.parseActions.push((input) => {
|
||||
if (typeof input !== 'string') {
|
||||
return input
|
||||
}
|
||||
const days = parseFloat(input)
|
||||
const millis = days * 24 * 60 * 60 * 1000
|
||||
const date = new Date('1900-01-01')
|
||||
date.setTime(date.getTime() + millis)
|
||||
return date
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
public max(value: Date, message?: string): this {
|
||||
this.validations.push({
|
||||
fn(input) {
|
||||
return input <= value
|
||||
},
|
||||
message: message
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is Date {
|
||||
return input instanceof Date && !isNaN(input.getTime())
|
||||
}
|
||||
}
|
21
src/libs/Schema/Items/SchemaFile.ts
Normal file
21
src/libs/Schema/Items/SchemaFile.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import SchemaItem from '../SchemaItem'
|
||||
|
||||
export default class SchemaFile extends SchemaItem<File> {
|
||||
constructor () {
|
||||
super()
|
||||
this.parseActions.push((input) => this.isOfType(input) && input.size > 0 ? input : undefined)
|
||||
}
|
||||
|
||||
public extension(ext: string, message?: string): this {
|
||||
this.validations.push({
|
||||
fn: (input) => input.name.endsWith(ext),
|
||||
message
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is File {
|
||||
return input instanceof File
|
||||
}
|
||||
}
|
65
src/libs/Schema/Items/SchemaNullable.ts
Normal file
65
src/libs/Schema/Items/SchemaNullable.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import type { ValidationResult } from '..'
|
||||
import SchemaItem from '../SchemaItem'
|
||||
import { isNull } from '../utils'
|
||||
|
||||
export default class SchemaNullable<A> extends SchemaItem<A | undefined | null> {
|
||||
|
||||
public constructor(private readonly item: SchemaItem<A>) {
|
||||
super()
|
||||
}
|
||||
|
||||
public emptyAsNull(): this {
|
||||
this.parseActions.push((input) => {
|
||||
if (typeof input === 'string' && input === '') {
|
||||
return null
|
||||
}
|
||||
return input
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public falthyAsNull(): this {
|
||||
this.parseActions.push((input) => {
|
||||
if (!input) {
|
||||
return null
|
||||
}
|
||||
return input
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public override transform(input: A | null | undefined): A | null | undefined {
|
||||
const transformed = super.transform(input)
|
||||
|
||||
if (isNull(transformed) || isNull(input)) {
|
||||
return transformed
|
||||
}
|
||||
|
||||
return this.item.transform(input)
|
||||
}
|
||||
|
||||
public override validate(input: A | null | undefined): ValidationResult<A | null | undefined> {
|
||||
if (isNull(input)) {
|
||||
return {
|
||||
object: input
|
||||
}
|
||||
}
|
||||
return this.item.validate(input)
|
||||
}
|
||||
|
||||
public override parse(input: unknown): (A | null | undefined) | unknown {
|
||||
const parsed = super.parse(input)
|
||||
|
||||
if (isNull(parsed) || isNull(input)) {
|
||||
return parsed
|
||||
}
|
||||
|
||||
return this.item.parse(input)
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is A | undefined | null {
|
||||
return isNull(input) || this.item.isOfType(input)
|
||||
}
|
||||
}
|
89
src/libs/Schema/Items/SchemaNumber.ts
Normal file
89
src/libs/Schema/Items/SchemaNumber.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import SchemaItem from '../SchemaItem'
|
||||
|
||||
export default class SchemaNumber extends SchemaItem<number> {
|
||||
|
||||
public min(...params: Parameters<SchemaNumber['gte']>): this {
|
||||
return this.gte(...params)
|
||||
}
|
||||
|
||||
public max(...params: Parameters<SchemaNumber['lte']>): this {
|
||||
return this.lte(...params)
|
||||
}
|
||||
|
||||
/**
|
||||
* validate that the number is less or equal than {@link value}
|
||||
* @param value the maxumum value (inclusive)
|
||||
* @param message the message sent if not valid
|
||||
*/
|
||||
public lte(value: number, message?: string): this {
|
||||
this.validations.push({
|
||||
fn(input) {
|
||||
return input <= value
|
||||
},
|
||||
message: message
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* validate that the number is more or equal than {@link value}
|
||||
* @param value the minimum value (inclusive)
|
||||
* @param message the message sent if not valid
|
||||
*/
|
||||
public gte(value: number, message?: string): this {
|
||||
this.validations.push({
|
||||
fn(input) {
|
||||
return input >= value
|
||||
},
|
||||
message: message
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* validate that the number is less than {@link value}
|
||||
* @param value the maxumum value (exclusive)
|
||||
* @param message the message sent if not valid
|
||||
*/
|
||||
public lt(value: number, message?: string): this {
|
||||
this.validations.push({
|
||||
fn(input) {
|
||||
return input < value
|
||||
},
|
||||
message: message
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* validate that the number is more than {@link value}
|
||||
* @param value the minimum value (exclusive)
|
||||
* @param message the message sent if not valid
|
||||
*/
|
||||
public gt(value: number, message?: string): this {
|
||||
this.validations.push({
|
||||
fn(input) {
|
||||
return input > value
|
||||
},
|
||||
message: message
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse strings before validating
|
||||
*/
|
||||
public parseString(): this {
|
||||
this.parseActions.push((input) =>
|
||||
typeof input === 'string' ? Number.parseFloat(input) : input
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is number {
|
||||
return typeof input === 'number' && !Number.isNaN(input)
|
||||
}
|
||||
}
|
88
src/libs/Schema/Items/SchemaRecord.ts
Normal file
88
src/libs/Schema/Items/SchemaRecord.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { isObject, objectLoop, objectRemap } from '@dzeio/object-util'
|
||||
import type { ValidationError, ValidationResult } from '..'
|
||||
import SchemaItem from '../SchemaItem'
|
||||
|
||||
export default class SchemaRecord<A extends string | number | symbol, B> extends SchemaItem<Record<A, B>> {
|
||||
|
||||
public constructor(
|
||||
private readonly key: SchemaItem<A>,
|
||||
private readonly values: SchemaItem<B>
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public override parse(input: unknown): unknown {
|
||||
input = super.parse(input)
|
||||
|
||||
if (!this.isOfType(input)) {
|
||||
return input
|
||||
}
|
||||
|
||||
const finalObj: Record<A, B> = {} as Record<A, B>
|
||||
const error = objectLoop(input, (value, key) => {
|
||||
const res1 = this.key.parse(key)
|
||||
const res2 = this.values.parse(value)
|
||||
if (typeof res1 !== 'string' && typeof res1 !== 'number') {
|
||||
return false
|
||||
}
|
||||
// @ts-expect-error normal behavior
|
||||
finalObj[res1] = res2
|
||||
return true
|
||||
})
|
||||
if (error) {
|
||||
return input
|
||||
}
|
||||
return finalObj
|
||||
}
|
||||
|
||||
public override transform(input: Record<A, B>): Record<A, B> {
|
||||
return objectRemap(super.transform(input), (value, key) => {
|
||||
return {
|
||||
key: this.key.transform(key),
|
||||
value: this.values.transform(value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public override validate(input: Record<A, B>, fast = false): ValidationResult<Record<A, B>> {
|
||||
const tmp = super.validate(input)
|
||||
if (tmp.error) {
|
||||
return tmp
|
||||
}
|
||||
|
||||
const errs: Array<ValidationError> = []
|
||||
const finalObj: Record<A, B> = {} as Record<A, B>
|
||||
|
||||
objectLoop(tmp.object, (value, key) => {
|
||||
const res1 = this.key.validate(key)
|
||||
const res2 = this.values.validate(value)
|
||||
const localErrs = (res1.error ?? []).concat(...(res2.error ?? []))
|
||||
if (localErrs.length > 0) {
|
||||
errs.push(...localErrs.map((it) => ({
|
||||
message: it.message,
|
||||
field: it.field ? `${key as string}.${it.field}` : key.toString()
|
||||
})))
|
||||
return !fast
|
||||
} else {
|
||||
// @ts-expect-error the check in the if assure the typing below
|
||||
finalObj[res1.object] = res2.object
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if (errs.length > 0) {
|
||||
return {
|
||||
error: errs
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
object: finalObj
|
||||
}
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is Record<A, B> {
|
||||
return isObject(input) && Object.prototype.toString.call(input) === '[object Object]'
|
||||
}
|
||||
}
|
76
src/libs/Schema/Items/SchemaString.ts
Normal file
76
src/libs/Schema/Items/SchemaString.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import SchemaItem from '../SchemaItem'
|
||||
import SchemaNullable from './SchemaNullable'
|
||||
|
||||
export default class SchemaString extends SchemaItem<string> {
|
||||
/**
|
||||
* force the input text to be a minimum of `value` size
|
||||
* @param value the minimum length of the text
|
||||
* @param message the message to display on an error
|
||||
*/
|
||||
public min(value: number, message?: string): SchemaString {
|
||||
this.validations.push({
|
||||
fn(input) {
|
||||
return input.length >= value
|
||||
},
|
||||
message: message
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* force the input text to be a maximum of `value` size
|
||||
* @param value the maximum length of the text
|
||||
* @param message the message to display on an error
|
||||
*/
|
||||
public max(value: number, message?: string): SchemaString {
|
||||
this.validations.push({
|
||||
fn(input) {
|
||||
return input.length <= value
|
||||
},
|
||||
message: message
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* the value must not be empty (`''`)
|
||||
* @param message
|
||||
* @returns
|
||||
*/
|
||||
public notEmpty(message?: string): this {
|
||||
this.validations.push({
|
||||
fn(input) {
|
||||
return input !== ''
|
||||
},
|
||||
message: message
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* note: this nullable MUST be used last as it change the type of the returned function
|
||||
*/
|
||||
public nullable() {
|
||||
return new SchemaNullable(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* force the input text to respect a Regexp
|
||||
* @param regex the regex to validate against
|
||||
* @param message the message to display on an error
|
||||
*/
|
||||
public regex(regex: RegExp, message?: string): SchemaString {
|
||||
this.validations.push({
|
||||
fn(input) {
|
||||
return regex.test(input)
|
||||
},
|
||||
message
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is string {
|
||||
return typeof input === 'string'
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user