generated from avior/template-web-astro
23
src/Schema/Items/DzeioLiteral.ts
Normal file
23
src/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/Schema/Items/SchemaArray.ts
Normal file
93
src/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/Schema/Items/SchemaBoolean.ts
Normal file
8
src/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'
|
||||
}
|
||||
}
|
62
src/Schema/Items/SchemaDate.ts
Normal file
62
src/Schema/Items/SchemaDate.ts
Normal file
@ -0,0 +1,62 @@
|
||||
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 parseFrenchDate(): this {
|
||||
this.parseActions.push((input) => {
|
||||
if (typeof input !== 'string') {
|
||||
return input
|
||||
}
|
||||
const splitted = input.split('/').map((it) => Number.parseInt(it, 10))
|
||||
if (splitted.length !== 3) {
|
||||
return input
|
||||
}
|
||||
return new Date(Date.UTC(splitted[2]!, splitted[1], splitted[0]))
|
||||
})
|
||||
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/Schema/Items/SchemaFile.ts
Normal file
21
src/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/Schema/Items/SchemaNullable.ts
Normal file
65
src/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/Schema/Items/SchemaNumber.ts
Normal file
89
src/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/Schema/Items/SchemaRecord.ts
Normal file
88
src/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/Schema/Items/SchemaString.ts
Normal file
76
src/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'
|
||||
}
|
||||
}
|
25
src/Schema/README.md
Normal file
25
src/Schema/README.md
Normal file
@ -0,0 +1,25 @@
|
||||
a Full featured and lightweight Schema validation/parsing library
|
||||
|
||||
it is meant to be used for input validation
|
||||
|
||||
example :
|
||||
|
||||
```ts
|
||||
import Schema, { s, type SchemaInfer } from 'libs/Schema'
|
||||
|
||||
const schema = new Schema({
|
||||
test: s.record(s.string(), s.object({
|
||||
a: s.number().parseString().min(3, 'a is too small')
|
||||
}))
|
||||
})
|
||||
|
||||
const t = {
|
||||
test: {
|
||||
b: {a: '34'}
|
||||
}
|
||||
}
|
||||
|
||||
// validate that `t` is coherant with the schema above
|
||||
const { object, error } = schema.validate(t)
|
||||
console.log(object, error)
|
||||
```
|
169
src/Schema/SchemaItem.ts
Normal file
169
src/Schema/SchemaItem.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import type { ValidationResult } from '.'
|
||||
import Schema from '.'
|
||||
import { isNull } from './utils'
|
||||
|
||||
export interface Messages {
|
||||
globalInvalid: string
|
||||
}
|
||||
|
||||
/**
|
||||
* An element of a schema
|
||||
*/
|
||||
export default abstract class SchemaItem<T> {
|
||||
/**
|
||||
* get additionnal attributes used to make the Schema work with outside libs
|
||||
*/
|
||||
public attributes: Array<string> = []
|
||||
|
||||
/**
|
||||
* the list of validations
|
||||
*/
|
||||
protected validations: Array<{
|
||||
fn: (input: T) => boolean
|
||||
message?: string | undefined
|
||||
}> = []
|
||||
|
||||
protected parseActions: Array<(input: unknown) => T | unknown> = []
|
||||
protected transforms: Array<(input: T) => T> = []
|
||||
|
||||
/**
|
||||
* set the list of attributes for the item of the schema
|
||||
* @param attributes the attributes
|
||||
*/
|
||||
public attr(...attributes: Array<string>) {
|
||||
this.attributes = attributes
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* set the default value of the schema element
|
||||
* @param value the default value
|
||||
* @param strict if strict, it will use it for null/undefined, else it will check for falthy values
|
||||
*/
|
||||
public defaultValue(value: T, strict = true) {
|
||||
this.parseActions.push((input) => {
|
||||
if (strict && isNull(input)) {
|
||||
return value
|
||||
}
|
||||
if (!value) {
|
||||
return input
|
||||
}
|
||||
return input
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param values the possible values the field can be
|
||||
* @param message the message returned if it does not respect the value
|
||||
*/
|
||||
public in(values: Array<T>, message?: string) {
|
||||
this.validations.push({
|
||||
fn: (input) => values.includes(input),
|
||||
message
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse the input from another format
|
||||
*
|
||||
* @param input the input to transform, it is done before validation, so the value can be anything
|
||||
* @returns the transformed value
|
||||
*/
|
||||
public parse(input: unknown): T | unknown {
|
||||
for (const transform of this.parseActions) {
|
||||
const tmp = transform(input)
|
||||
if (this.isOfType(tmp)) {
|
||||
return tmp
|
||||
}
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
|
||||
/**
|
||||
* transform a valid value
|
||||
*
|
||||
* @param input the input to transform, it MUST be validated beforehand
|
||||
* @returns the transformed value
|
||||
*/
|
||||
public transform(input: T): T {
|
||||
let res = input
|
||||
|
||||
for (const action of this.transforms) {
|
||||
res = action(res)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* validate that the input is valid or not
|
||||
* @param input the input to validate
|
||||
* @param fast if true the validation stops at the first error
|
||||
* @returns a string if it's not valid, else null
|
||||
*/
|
||||
public validate(input: T, fast = false): ValidationResult<T> {
|
||||
for (const validation of this.validations) {
|
||||
if (!validation.fn(input as T)) {
|
||||
return {
|
||||
error: [{
|
||||
message: validation.message ?? Schema.messages.globalInvalid
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
object: input as T
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* validate that the input value is of the type of the schema item
|
||||
*
|
||||
* it makes others functions easier to works with
|
||||
* @param input the input to validate
|
||||
*/
|
||||
public abstract isOfType(input: unknown): input is T
|
||||
|
||||
// public abstract toJSON(): JSONSchemaItem
|
||||
}
|
||||
|
||||
type Parseable = string | number | boolean
|
||||
|
||||
export interface ValidatorJSON {
|
||||
/**
|
||||
* the function name (ex: `min`, `max`)
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* the function parameters
|
||||
*/
|
||||
params?: Array<Parseable>
|
||||
}
|
||||
|
||||
export interface JSONSchemaItem {
|
||||
/**
|
||||
* Schema item
|
||||
*
|
||||
* ex: `string`, `number`, `boolean`, ...
|
||||
*/
|
||||
type: string
|
||||
/**
|
||||
* constructor params
|
||||
*/
|
||||
params?: Array<Parseable>
|
||||
/**
|
||||
* list of attributes
|
||||
*/
|
||||
attributes?: Array<string>
|
||||
actions?: Array<ValidatorJSON>
|
||||
}
|
||||
|
||||
export type JSONSchema = {
|
||||
[a: string]: JSONSchemaItem
|
||||
}
|
216
src/Schema/index.ts
Normal file
216
src/Schema/index.ts
Normal file
@ -0,0 +1,216 @@
|
||||
import { isObject, objectLoop } from '@dzeio/object-util'
|
||||
import DzeioLiteral from './Items/DzeioLiteral'
|
||||
import SchemaArray from './Items/SchemaArray'
|
||||
import SchemaBoolean from './Items/SchemaBoolean'
|
||||
import SchemaDate from './Items/SchemaDate'
|
||||
import SchemaFile from './Items/SchemaFile'
|
||||
import SchemaNullable from './Items/SchemaNullable'
|
||||
import SchemaNumber from './Items/SchemaNumber'
|
||||
import SchemaRecord from './Items/SchemaRecord'
|
||||
import SchemaString from './Items/SchemaString'
|
||||
import SchemaItem from './SchemaItem'
|
||||
|
||||
export interface ValidationError {
|
||||
message: string
|
||||
field?: string
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
export type ValidationResult<T> = {
|
||||
object: T
|
||||
error?: undefined
|
||||
} | {
|
||||
object?: undefined
|
||||
error: Array<ValidationError>
|
||||
}
|
||||
|
||||
export type Model = Record<string, SchemaItem<any>>
|
||||
|
||||
export type SchemaInfer<S extends Schema> = ModelInfer<S['model']>
|
||||
|
||||
export type ModelInfer<M extends Model> = {
|
||||
[key in keyof M]: ReturnType<M[key]['transform']>
|
||||
}
|
||||
|
||||
/**
|
||||
* A schema to validate input or external datas
|
||||
*/
|
||||
export default class Schema<M extends Model = Model> extends SchemaItem<ModelInfer<Model>> {
|
||||
|
||||
public static messages = {
|
||||
typeInvalid: 'Type of field is not valid',
|
||||
notAnObject: 'the data submitted is not valid',
|
||||
globalInvalid: 'the field is invalid'
|
||||
}
|
||||
|
||||
public constructor(public readonly model: M) {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link SchemaString}
|
||||
*/
|
||||
public static string(
|
||||
...inputs: ConstructorParameters<typeof SchemaString>
|
||||
): SchemaString {
|
||||
return new SchemaString(...inputs)
|
||||
}
|
||||
|
||||
public static file(
|
||||
...inputs: ConstructorParameters<typeof SchemaFile>
|
||||
): SchemaFile {
|
||||
return new SchemaFile(...inputs)
|
||||
}
|
||||
|
||||
public static number(
|
||||
...inputs: ConstructorParameters<typeof SchemaNumber>
|
||||
): SchemaNumber {
|
||||
return new SchemaNumber(...inputs)
|
||||
}
|
||||
|
||||
public static date(
|
||||
...inputs: ConstructorParameters<typeof SchemaDate>
|
||||
): SchemaDate {
|
||||
return new SchemaDate(...inputs)
|
||||
}
|
||||
|
||||
public static literal<T>(
|
||||
...inputs: ConstructorParameters<typeof DzeioLiteral<T>>
|
||||
): DzeioLiteral<T> {
|
||||
return new DzeioLiteral<T>(...inputs)
|
||||
}
|
||||
|
||||
public static object<T extends Model>(
|
||||
...inputs: ConstructorParameters<typeof Schema<T>>
|
||||
): Schema<T> {
|
||||
return new Schema(...inputs)
|
||||
}
|
||||
|
||||
public static record<A extends string | number, B>(
|
||||
...inputs: ConstructorParameters<typeof SchemaRecord<A, B>>
|
||||
): SchemaRecord<A, B> {
|
||||
return new SchemaRecord<A, B>(...inputs)
|
||||
}
|
||||
|
||||
public static array<A>(
|
||||
...inputs: ConstructorParameters<typeof SchemaArray<A>>
|
||||
): SchemaArray<A> {
|
||||
return new SchemaArray<A>(...inputs)
|
||||
}
|
||||
|
||||
public static nullable<A>(
|
||||
...inputs: ConstructorParameters<typeof SchemaNullable<A>>
|
||||
): SchemaNullable<A> {
|
||||
return new SchemaNullable<A>(...inputs)
|
||||
}
|
||||
|
||||
public static boolean(
|
||||
...inputs: ConstructorParameters<typeof SchemaBoolean>
|
||||
): SchemaBoolean {
|
||||
return new SchemaBoolean(...inputs)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param query the URL params to validate
|
||||
* @returns
|
||||
*/
|
||||
public validateQuery(query: URLSearchParams, fast = false): ReturnType<Schema<M>['validate']> {
|
||||
const record: Record<string, unknown> = {}
|
||||
for (const [key, value] of query) {
|
||||
record[key] = value
|
||||
}
|
||||
|
||||
return this.validate(record, fast)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param form the form to validate
|
||||
*/
|
||||
public validateForm(form: HTMLFormElement, fast = false): ReturnType<Schema<M>['validate']> {
|
||||
const data = new FormData(form)
|
||||
return this.validateFormData(data, fast)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data the FormData to validate
|
||||
* @returns
|
||||
*/
|
||||
public validateFormData(data: FormData, fast = false): ReturnType<Schema<M>['validate']> {
|
||||
const record: Record<string, unknown> = {}
|
||||
for (const [key, value] of data) {
|
||||
const isArray = this.model[key]?.isOfType([]) ?? false
|
||||
record[key] = isArray ? data.getAll(key) : value
|
||||
}
|
||||
|
||||
return this.validate(record, fast)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param input the data to validate
|
||||
* @param options additionnal validation options
|
||||
* @returns blablabla
|
||||
*/
|
||||
public override validate(input: unknown, fast = false): ValidationResult<SchemaInfer<this>> {
|
||||
if (!isObject(input)) {
|
||||
return {
|
||||
error: [{
|
||||
message: Schema.messages.notAnObject
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
const errors: Array<ValidationError> = []
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
const res: ModelInfer<M> = {} as any
|
||||
objectLoop(this.model, (v, k) => {
|
||||
// parse value from other formats
|
||||
const value = v.parse(input[k])
|
||||
|
||||
// validate that the value is of type
|
||||
if (!v.isOfType(value)) {
|
||||
errors.push({
|
||||
message: Schema.messages.typeInvalid,
|
||||
field: k,
|
||||
value: value
|
||||
})
|
||||
return !fast
|
||||
}
|
||||
|
||||
// run validations
|
||||
const invalid = v.validate(value)
|
||||
if (invalid.error) {
|
||||
errors.push(...invalid.error.map((it) => ({
|
||||
message: it.message,
|
||||
field: it.field ? `${k}.${it.field}` : k
|
||||
})))
|
||||
return !fast
|
||||
}
|
||||
|
||||
// transform and assign final value
|
||||
// @ts-expect-error normal behavior
|
||||
res[k] = v.transform(value)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if (errors.length > 0) {
|
||||
return {
|
||||
error: errors
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
object: res
|
||||
}
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is ModelInfer<Model> {
|
||||
return isObject(input)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* alias of {@link Schema}
|
||||
*/
|
||||
export const s = Schema
|
3
src/Schema/utils.ts
Normal file
3
src/Schema/utils.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function isNull(value: unknown): value is undefined | null {
|
||||
return typeof value === 'undefined' || value === null
|
||||
}
|
Reference in New Issue
Block a user