feat: Filemagedon
Some checks failed
Build, check & Test / run (push) Failing after 1m45s
Lint / run (push) Failing after 48s
Build Docker Image / build_docker (push) Failing after 3m18s

Signed-off-by: Avior <git@avior.me>
This commit is contained in:
2024-09-11 14:38:58 +02:00
parent 3e91597dca
commit bc97d9106b
45 changed files with 4548 additions and 64 deletions

View 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]
}
}
}

View 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
// }
// }
}

View 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'
}
}

View 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())
}
}

View 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
}
}

View 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)
}
}

View 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)
}
}

View 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]'
}
}

View 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'
}
}