From e38bc9b0b2b350a2c11387a5c24e8cee7db6a7b6 Mon Sep 17 00:00:00 2001 From: Avior Date: Wed, 9 Oct 2024 17:47:31 +0200 Subject: [PATCH] fix: update Signed-off-by: Avior --- package-lock.json | 9 + package.json | 1 + src/Schema/Items/DzeioLiteral.ts | 23 + src/Schema/Items/SchemaArray.ts | 93 +++ src/Schema/Items/SchemaBoolean.ts | 8 + src/Schema/Items/SchemaDate.ts | 62 ++ src/Schema/Items/SchemaFile.ts | 21 + src/Schema/Items/SchemaNullable.ts | 65 ++ src/Schema/Items/SchemaNumber.ts | 89 +++ src/Schema/Items/SchemaRecord.ts | 88 +++ src/Schema/Items/SchemaString.ts | 76 ++ src/Schema/README.md | 25 + src/Schema/SchemaItem.ts | 169 +++++ src/Schema/index.ts | 216 ++++++ src/Schema/utils.ts | 3 + src/components/global/Input.astro | 5 +- .../global/Select/SelectController.ts | 58 ++ src/components/global/Select/index.astro | 182 ++++- src/env.d.ts | 34 +- src/layouts/Base.astro | 25 +- src/libs/DOMElement.ts | 241 +++++++ src/libs/Hyperion/README.md | 133 ---- src/libs/Hyperion/index.ts | 662 ------------------ src/libs/Schema/Items/DzeioLiteral.ts | 23 + src/libs/Schema/Items/SchemaArray.ts | 93 +++ src/libs/Schema/Items/SchemaBoolean.ts | 8 + src/libs/Schema/Items/SchemaDate.ts | 62 ++ src/libs/Schema/Items/SchemaFile.ts | 21 + src/libs/Schema/Items/SchemaNullable.ts | 65 ++ src/libs/Schema/Items/SchemaNumber.ts | 89 +++ src/libs/Schema/Items/SchemaRecord.ts | 88 +++ src/libs/Schema/Items/SchemaString.ts | 76 ++ src/libs/Schema/README.md | 25 + src/libs/Schema/SchemaItem.ts | 169 +++++ src/libs/Schema/index.ts | 216 ++++++ src/libs/Schema/utils.ts | 3 + src/middleware/database.ts | 35 + src/middleware/index.ts | 3 +- src/models/Adapters/AdapterUtils.ts | 47 +- src/models/Adapters/CSVAdapter.ts | 55 ++ src/models/Adapters/CassandraAdapter.ts | 54 +- src/models/{ => Adapters}/DaoAdapter.ts | 23 +- src/models/Adapters/FSAdapter.ts | 132 ++-- src/models/Adapters/LDAPAdapter.ts | 120 ++-- src/models/Adapters/MultiAdapter.ts | 29 +- src/models/Adapters/PostgresAdapter.ts | 187 +++-- src/models/Clients/CassandraClient.ts | 130 ++++ src/models/Clients/PostgresClient.ts | 141 ++-- src/models/Clients/index.ts | 141 ++++ src/models/Dao.ts | 51 +- src/models/DaoFactory.ts | 55 +- src/models/History.ts | 18 + src/models/Issue/index.ts | 28 +- src/models/Migrations/0.ts | 22 - src/models/Migrations/Migration.d.ts | 10 + src/models/Migrations/Migration0.ts | 22 + ...7:48.ts => Migration2024-05-15T9-57-48.ts} | 18 +- src/models/Migrations/index.ts | 98 --- src/models/Project/index.ts | 16 +- src/models/Query.ts | 8 +- src/models/Schema.ts | 588 ---------------- src/models/State/index.ts | 14 +- src/models/config.ts | 49 ++ .../api/v1/projects/[id]/issues/[issueId].ts | 18 +- .../projects/[id]/issues/[issueId]/details.ts | 19 + .../api/v1/projects/[id]/issues/index.ts | 36 +- src/pages/index.astro | 23 +- src/pages/projects/[id].astro | 131 ++-- src/pages/test.astro | 6 - 69 files changed, 3482 insertions(+), 2071 deletions(-) create mode 100644 src/Schema/Items/DzeioLiteral.ts create mode 100644 src/Schema/Items/SchemaArray.ts create mode 100644 src/Schema/Items/SchemaBoolean.ts create mode 100644 src/Schema/Items/SchemaDate.ts create mode 100644 src/Schema/Items/SchemaFile.ts create mode 100644 src/Schema/Items/SchemaNullable.ts create mode 100644 src/Schema/Items/SchemaNumber.ts create mode 100644 src/Schema/Items/SchemaRecord.ts create mode 100644 src/Schema/Items/SchemaString.ts create mode 100644 src/Schema/README.md create mode 100644 src/Schema/SchemaItem.ts create mode 100644 src/Schema/index.ts create mode 100644 src/Schema/utils.ts create mode 100644 src/components/global/Select/SelectController.ts create mode 100644 src/libs/DOMElement.ts delete mode 100644 src/libs/Hyperion/README.md delete mode 100644 src/libs/Hyperion/index.ts create mode 100644 src/libs/Schema/Items/DzeioLiteral.ts create mode 100644 src/libs/Schema/Items/SchemaArray.ts create mode 100644 src/libs/Schema/Items/SchemaBoolean.ts create mode 100644 src/libs/Schema/Items/SchemaDate.ts create mode 100644 src/libs/Schema/Items/SchemaFile.ts create mode 100644 src/libs/Schema/Items/SchemaNullable.ts create mode 100644 src/libs/Schema/Items/SchemaNumber.ts create mode 100644 src/libs/Schema/Items/SchemaRecord.ts create mode 100644 src/libs/Schema/Items/SchemaString.ts create mode 100644 src/libs/Schema/README.md create mode 100644 src/libs/Schema/SchemaItem.ts create mode 100644 src/libs/Schema/index.ts create mode 100644 src/libs/Schema/utils.ts create mode 100644 src/middleware/database.ts create mode 100644 src/models/Adapters/CSVAdapter.ts rename src/models/{ => Adapters}/DaoAdapter.ts (63%) create mode 100644 src/models/Clients/CassandraClient.ts create mode 100644 src/models/Clients/index.ts create mode 100644 src/models/History.ts delete mode 100644 src/models/Migrations/0.ts create mode 100644 src/models/Migrations/Migration.d.ts create mode 100644 src/models/Migrations/Migration0.ts rename src/models/Migrations/{2024-05-15T9:57:48.ts => Migration2024-05-15T9-57-48.ts} (56%) delete mode 100644 src/models/Migrations/index.ts delete mode 100644 src/models/Schema.ts create mode 100644 src/models/config.ts create mode 100644 src/pages/api/v1/projects/[id]/issues/[issueId]/details.ts diff --git a/package-lock.json b/package-lock.json index 2f09846..12a124b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@dzeio/object-util": "^1", "@dzeio/url-manager": "^1", "astro": "^4", + "hyperions": "^1.0.0-beta.8", "lucide-astro": "^0", "pg": "^8.11.5", "sharp": "^0", @@ -3739,6 +3740,14 @@ "node": ">=16.17.0" } }, + "node_modules/hyperions": { + "version": "1.0.0-beta.10", + "resolved": "https://registry.npmjs.org/hyperions/-/hyperions-1.0.0-beta.10.tgz", + "integrity": "sha512-Jyx9WPQOsUfMhhzQEibsR1sCLlf2QajZjDDkhwcsIcs2eQ2P3OOoMJtk0xZR6547e1sTn+fjN666ABGN8SLMYQ==", + "dependencies": { + "@dzeio/object-util": "^1.8.3" + } + }, "node_modules/import-meta-resolve": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", diff --git a/package.json b/package.json index 6d98523..e6696e0 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@dzeio/object-util": "^1", "@dzeio/url-manager": "^1", "astro": "^4", + "hyperions": "^1.0.0-beta.8", "lucide-astro": "^0", "pg": "^8.11.5", "sharp": "^0", diff --git a/src/Schema/Items/DzeioLiteral.ts b/src/Schema/Items/DzeioLiteral.ts new file mode 100644 index 0000000..59be327 --- /dev/null +++ b/src/Schema/Items/DzeioLiteral.ts @@ -0,0 +1,23 @@ +import SchemaItem, { type JSONSchemaItem } from '../SchemaItem' + +export default class DzeioLiteral extends SchemaItem { + 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] + } + } +} diff --git a/src/Schema/Items/SchemaArray.ts b/src/Schema/Items/SchemaArray.ts new file mode 100644 index 0000000..a8eebdd --- /dev/null +++ b/src/Schema/Items/SchemaArray.ts @@ -0,0 +1,93 @@ +import type { ValidationError, ValidationResult } from '..' +import SchemaItem from '../SchemaItem' + +export default class SchemaArray extends SchemaItem> { + + public constructor( + private readonly values: SchemaItem + ) { + 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 { + const tmp = super.validate(input, fast) + if (tmp.error) { + return tmp + } + const clone: Array = [] + const errs: Array = [] + 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 { + return Array.isArray(input) + } + + // public override toJSON(): JSONSchemaItem { + // return { + // type: 'array', + // childs: this.values + // } + // } +} diff --git a/src/Schema/Items/SchemaBoolean.ts b/src/Schema/Items/SchemaBoolean.ts new file mode 100644 index 0000000..574df90 --- /dev/null +++ b/src/Schema/Items/SchemaBoolean.ts @@ -0,0 +1,8 @@ +import SchemaItem from '../SchemaItem' + +export default class SchemaBoolean extends SchemaItem { + + public override isOfType(input: unknown): input is boolean { + return typeof input === 'boolean' + } +} diff --git a/src/Schema/Items/SchemaDate.ts b/src/Schema/Items/SchemaDate.ts new file mode 100644 index 0000000..7923194 --- /dev/null +++ b/src/Schema/Items/SchemaDate.ts @@ -0,0 +1,62 @@ +import SchemaItem from '../SchemaItem' + +export default class SchemaDate extends SchemaItem { + + 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()) + } +} diff --git a/src/Schema/Items/SchemaFile.ts b/src/Schema/Items/SchemaFile.ts new file mode 100644 index 0000000..9d5e57b --- /dev/null +++ b/src/Schema/Items/SchemaFile.ts @@ -0,0 +1,21 @@ +import SchemaItem from '../SchemaItem' + +export default class SchemaFile extends SchemaItem { + 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 + } +} diff --git a/src/Schema/Items/SchemaNullable.ts b/src/Schema/Items/SchemaNullable.ts new file mode 100644 index 0000000..9b51377 --- /dev/null +++ b/src/Schema/Items/SchemaNullable.ts @@ -0,0 +1,65 @@ +import type { ValidationResult } from '..' +import SchemaItem from '../SchemaItem' +import { isNull } from '../utils' + +export default class SchemaNullable extends SchemaItem { + + public constructor(private readonly item: SchemaItem) { + 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 { + 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) + } +} diff --git a/src/Schema/Items/SchemaNumber.ts b/src/Schema/Items/SchemaNumber.ts new file mode 100644 index 0000000..738e40f --- /dev/null +++ b/src/Schema/Items/SchemaNumber.ts @@ -0,0 +1,89 @@ +import SchemaItem from '../SchemaItem' + +export default class SchemaNumber extends SchemaItem { + + public min(...params: Parameters): this { + return this.gte(...params) + } + + public max(...params: Parameters): 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) + } +} diff --git a/src/Schema/Items/SchemaRecord.ts b/src/Schema/Items/SchemaRecord.ts new file mode 100644 index 0000000..571c0e6 --- /dev/null +++ b/src/Schema/Items/SchemaRecord.ts @@ -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 extends SchemaItem> { + + public constructor( + private readonly key: SchemaItem, + private readonly values: SchemaItem + ) { + super() + } + + public override parse(input: unknown): unknown { + input = super.parse(input) + + if (!this.isOfType(input)) { + return input + } + + const finalObj: Record = {} as Record + 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): Record { + return objectRemap(super.transform(input), (value, key) => { + return { + key: this.key.transform(key), + value: this.values.transform(value) + } + }) + } + + public override validate(input: Record, fast = false): ValidationResult> { + const tmp = super.validate(input) + if (tmp.error) { + return tmp + } + + const errs: Array = [] + const finalObj: Record = {} as Record + + 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 { + return isObject(input) && Object.prototype.toString.call(input) === '[object Object]' + } +} diff --git a/src/Schema/Items/SchemaString.ts b/src/Schema/Items/SchemaString.ts new file mode 100644 index 0000000..de09bf8 --- /dev/null +++ b/src/Schema/Items/SchemaString.ts @@ -0,0 +1,76 @@ +import SchemaItem from '../SchemaItem' +import SchemaNullable from './SchemaNullable' + +export default class SchemaString extends SchemaItem { + /** + * 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' + } +} diff --git a/src/Schema/README.md b/src/Schema/README.md new file mode 100644 index 0000000..8415edd --- /dev/null +++ b/src/Schema/README.md @@ -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) +``` diff --git a/src/Schema/SchemaItem.ts b/src/Schema/SchemaItem.ts new file mode 100644 index 0000000..1aee20e --- /dev/null +++ b/src/Schema/SchemaItem.ts @@ -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 { + /** + * get additionnal attributes used to make the Schema work with outside libs + */ + public attributes: Array = [] + + /** + * 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) { + 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, 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 { + 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 +} + +export interface JSONSchemaItem { + /** + * Schema item + * + * ex: `string`, `number`, `boolean`, ... + */ + type: string + /** + * constructor params + */ + params?: Array + /** + * list of attributes + */ + attributes?: Array + actions?: Array +} + +export type JSONSchema = { + [a: string]: JSONSchemaItem +} diff --git a/src/Schema/index.ts b/src/Schema/index.ts new file mode 100644 index 0000000..427530f --- /dev/null +++ b/src/Schema/index.ts @@ -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 = { + object: T + error?: undefined +} | { + object?: undefined + error: Array +} + +export type Model = Record> + +export type SchemaInfer = ModelInfer + +export type ModelInfer = { + [key in keyof M]: ReturnType +} + +/** + * A schema to validate input or external datas + */ +export default class Schema extends SchemaItem> { + + 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 + ): SchemaString { + return new SchemaString(...inputs) + } + + public static file( + ...inputs: ConstructorParameters + ): SchemaFile { + return new SchemaFile(...inputs) + } + + public static number( + ...inputs: ConstructorParameters + ): SchemaNumber { + return new SchemaNumber(...inputs) + } + + public static date( + ...inputs: ConstructorParameters + ): SchemaDate { + return new SchemaDate(...inputs) + } + + public static literal( + ...inputs: ConstructorParameters> + ): DzeioLiteral { + return new DzeioLiteral(...inputs) + } + + public static object( + ...inputs: ConstructorParameters> + ): Schema { + return new Schema(...inputs) + } + + public static record( + ...inputs: ConstructorParameters> + ): SchemaRecord { + return new SchemaRecord(...inputs) + } + + public static array( + ...inputs: ConstructorParameters> + ): SchemaArray { + return new SchemaArray(...inputs) + } + + public static nullable( + ...inputs: ConstructorParameters> + ): SchemaNullable { + return new SchemaNullable(...inputs) + } + + public static boolean( + ...inputs: ConstructorParameters + ): SchemaBoolean { + return new SchemaBoolean(...inputs) + } + + /** + * @param query the URL params to validate + * @returns + */ + public validateQuery(query: URLSearchParams, fast = false): ReturnType['validate']> { + const record: Record = {} + 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['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['validate']> { + const record: Record = {} + 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> { + if (!isObject(input)) { + return { + error: [{ + message: Schema.messages.notAnObject + }] + } + } + + const errors: Array = [] + // biome-ignore lint/suspicious/noExplicitAny: + const res: ModelInfer = {} 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 { + return isObject(input) + } +} + +/** + * alias of {@link Schema} + */ +export const s = Schema diff --git a/src/Schema/utils.ts b/src/Schema/utils.ts new file mode 100644 index 0000000..86f6ec6 --- /dev/null +++ b/src/Schema/utils.ts @@ -0,0 +1,3 @@ +export function isNull(value: unknown): value is undefined | null { + return typeof value === 'undefined' || value === null +} diff --git a/src/components/global/Input.astro b/src/components/global/Input.astro index 10f941a..1b47d17 100644 --- a/src/components/global/Input.astro +++ b/src/components/global/Input.astro @@ -6,6 +6,7 @@ interface Props extends Omit { block?: boolean suffix?: string prefix?: string + parentClass?: string } const baseProps = objectOmit(Astro.props, 'label', 'block', 'suffix', 'prefix') @@ -16,7 +17,7 @@ if (baseProps.type === 'textarea') { --- -