chore: initial commit
This commit is contained in:
commit
4ab90d8476
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules/
|
165
Schema.ts
Normal file
165
Schema.ts
Normal file
@ -0,0 +1,165 @@
|
||||
import SchemaArray from "./items/array"
|
||||
import SchemaBoolean from "./items/boolean"
|
||||
import SchemaEnum, { EnumLike } from "./items/enum"
|
||||
import SchemaLiteral from "./items/literal"
|
||||
import SchemaNullable from "./items/nullable"
|
||||
import SchemaNumber from "./items/number"
|
||||
import SchemaObject from "./items/object"
|
||||
import SchemaString from "./items/string"
|
||||
import { SchemaUnion } from "./items/union"
|
||||
import SchemaItem from "./SchemaItem"
|
||||
import { SchemaJSON } from "./types"
|
||||
|
||||
export function parceable() {
|
||||
|
||||
return (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<any>) => {
|
||||
if (!(target instanceof SchemaItem)) {
|
||||
throw new Error('the decorator is only usable on Schema')
|
||||
}
|
||||
|
||||
|
||||
const original = target[propertyKey]
|
||||
|
||||
const t = function () { }
|
||||
|
||||
descriptor.value = function (this: SchemaItem, ...args: Array<any>) {
|
||||
this.savedCalls.push({ name: propertyKey as string, args: args })
|
||||
const res = original.call(this, ...args)
|
||||
return res
|
||||
}
|
||||
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
interface SchemaItemStatic {
|
||||
new(...args: Array<any>): SchemaItem
|
||||
}
|
||||
|
||||
type ExtractGeneric<T> = T extends SchemaItem<infer U> ? U : never
|
||||
|
||||
export default class Schema {
|
||||
private static registeredModules: Array<SchemaItemStatic> = [
|
||||
SchemaArray,
|
||||
SchemaBoolean,
|
||||
SchemaEnum,
|
||||
SchemaLiteral,
|
||||
SchemaNullable,
|
||||
SchemaObject,
|
||||
SchemaString,
|
||||
SchemaUnion
|
||||
]
|
||||
|
||||
public static register(module: SchemaItemStatic) {
|
||||
this.registeredModules.push(module)
|
||||
}
|
||||
|
||||
public static getModule(name: string) {
|
||||
return this.registeredModules.find((it) => it.name === name)
|
||||
}
|
||||
|
||||
public static array<Type extends SchemaItem>(
|
||||
...inputs: ConstructorParameters<typeof SchemaArray<Type>>
|
||||
): SchemaArray<Type> {
|
||||
return new SchemaArray<Type>(...inputs)
|
||||
}
|
||||
|
||||
public static boolean(
|
||||
...inputs: ConstructorParameters<typeof SchemaBoolean>
|
||||
): SchemaBoolean {
|
||||
return new SchemaBoolean(...inputs)
|
||||
}
|
||||
|
||||
public static enum<Type extends EnumLike>(
|
||||
...inputs: ConstructorParameters<typeof SchemaEnum<Type>>
|
||||
): SchemaEnum<Type> {
|
||||
return new SchemaEnum<Type>(...inputs)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param input the literal value (note: append `as const` else the typing won't work correctly)
|
||||
* @returns
|
||||
*/
|
||||
public static literal<Type>(
|
||||
input: Type
|
||||
): SchemaLiteral<Type> {
|
||||
return new SchemaLiteral<Type>(input)
|
||||
}
|
||||
|
||||
public static nullable<Type extends SchemaItem>(
|
||||
...inputs: ConstructorParameters<typeof SchemaNullable<Type>>
|
||||
): SchemaNullable<Type> {
|
||||
return new SchemaNullable<Type>(...inputs)
|
||||
}
|
||||
|
||||
public static number(
|
||||
...inputs: ConstructorParameters<typeof SchemaNumber>
|
||||
): SchemaNumber {
|
||||
return new SchemaNumber(...inputs)
|
||||
}
|
||||
|
||||
public static object<Type extends Record<string, SchemaItem<any>>>(
|
||||
...inputs: ConstructorParameters<typeof SchemaObject<Type>>
|
||||
): SchemaObject<Type> {
|
||||
return new SchemaObject<Type>(...inputs)
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link SchemaString}
|
||||
*/
|
||||
public static string(
|
||||
...inputs: ConstructorParameters<typeof SchemaString>
|
||||
): SchemaString {
|
||||
return new SchemaString(...inputs)
|
||||
}
|
||||
|
||||
public static union<Type extends Array<SchemaItem<any>>>(
|
||||
...inputs: ConstructorParameters<typeof SchemaUnion<Type>>
|
||||
): SchemaUnion<Type> {
|
||||
return new SchemaUnion<Type>(...inputs)
|
||||
}
|
||||
|
||||
public static fromJSON(json: SchemaJSON): SchemaItem {
|
||||
|
||||
// get the module
|
||||
const fn = this.getModule(json.i)
|
||||
|
||||
// handle module not detected
|
||||
if (!fn) {
|
||||
throw new Error(`Schema cannot parse ${json.i}`)
|
||||
}
|
||||
|
||||
// init the module
|
||||
const item = new fn(...(json.c?.map((it) => this.isSchemaJSON(it) ? Schema.fromJSON(it) : it) ?? []))
|
||||
|
||||
// handle validations
|
||||
for (const validation of (json.f ?? [])) {
|
||||
// validation not found in item :(
|
||||
if (!(validation.n in item)) {
|
||||
throw new Error('validation not available in Schema Item')
|
||||
}
|
||||
|
||||
// init the validation
|
||||
(item[validation.n] as (...params: Array<unknown>) => void)(...validation.a.map((it) => Schema.isSchemaJSON(it) ? Schema.fromJSON(it) : it))
|
||||
}
|
||||
|
||||
// add the attributes
|
||||
item.attrs(...json.a ?? [])
|
||||
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
public static isSchemaJSON(data: unknown): data is SchemaJSON {
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!('i' in data)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
164
SchemaItem.ts
Normal file
164
SchemaItem.ts
Normal file
@ -0,0 +1,164 @@
|
||||
import Schema from "./Schema"
|
||||
import { SchemaJSON, ValidationError, ValidationResult } from "./types"
|
||||
|
||||
export default abstract class SchemaItem<Type = any> {
|
||||
|
||||
private invalidError = 'the field is invalid'
|
||||
|
||||
/**
|
||||
* list of attributes for custom works
|
||||
*/
|
||||
public readonly attributes: Array<string> = []
|
||||
|
||||
public attrs(...attributes: Array<string>) {
|
||||
this.attributes.concat(attributes)
|
||||
return this
|
||||
}
|
||||
|
||||
public setInvalidError(err: string): this {
|
||||
this.invalidError = err
|
||||
return this
|
||||
}
|
||||
|
||||
public clone(): this {
|
||||
return Schema.fromJSON(this.toJSON()) as this
|
||||
}
|
||||
|
||||
/**
|
||||
* schemas implementing unwrap can return their child component (mostly the value)
|
||||
*
|
||||
* ex: s.record(s.number(), s.string()) returns s.string()
|
||||
*
|
||||
* es2: s.nullable(s.string()) returns s.string()
|
||||
*/
|
||||
public unwrap?(): SchemaItem
|
||||
|
||||
/**
|
||||
* Pre process the variable for various reasons
|
||||
*
|
||||
* It will execute every pre-process sequentially in order of being added
|
||||
*
|
||||
* note: The type of the final Pre-Process MUST be valid
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||
public preProcess: Array<(input: unknown) => Type | unknown> = []
|
||||
|
||||
/**
|
||||
* post process the variable after it being validated
|
||||
*
|
||||
* it will execute each post-processes sequentially
|
||||
*
|
||||
* note: the type of the final post-process MUST be valid
|
||||
*/
|
||||
public postProcess: Array<(input: Type) => Type> = []
|
||||
|
||||
/**
|
||||
* keep public ?
|
||||
*/
|
||||
public validations: Array<{
|
||||
fn: (input: Type) => boolean
|
||||
error?: string | undefined
|
||||
}> = []
|
||||
|
||||
public savedCalls: Array<{ name: string, args: Array<string> | IArguments | undefined }> = []
|
||||
|
||||
private readonly items?: Array<unknown>
|
||||
|
||||
public constructor(items?: Array<unknown> | IArguments) {
|
||||
if (items && items.length > 0) {
|
||||
this.items = Array.isArray(items) ? items : Array.from(items)
|
||||
}
|
||||
}
|
||||
|
||||
public parse(input: unknown, options?: { fast?: boolean }): ValidationResult<Type> {
|
||||
// pre process the variable
|
||||
for (const preProcess of this.preProcess) {
|
||||
input = preProcess(input)
|
||||
}
|
||||
|
||||
// validate that the pre process handled correctly the variable
|
||||
if (!this.isOfType(input)) {
|
||||
return {
|
||||
valid: false,
|
||||
errors: [{ message: 'invalid type' }]
|
||||
}
|
||||
}
|
||||
|
||||
// run validation checks
|
||||
const errors: Array<ValidationError> = []
|
||||
for (const validation of this.validations) {
|
||||
if (!validation.fn(input)) {
|
||||
|
||||
// add the error to the list
|
||||
errors.push({
|
||||
message: validation.error ?? this.invalidError
|
||||
})
|
||||
|
||||
// if the system should be fast, stop checking for other errors
|
||||
if (options?.fast) {
|
||||
return {
|
||||
valid: false,
|
||||
object: input as (Type extends object ? Partial<Type> : Type) | undefined,
|
||||
errors: errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return with errors
|
||||
if (errors.length > 0) {
|
||||
return {
|
||||
valid: false,
|
||||
object: input as (Type extends object ? Partial<Type> : Type) | undefined,
|
||||
errors: errors
|
||||
}
|
||||
}
|
||||
|
||||
// post-process the variable
|
||||
for (const postProcess of this.postProcess) {
|
||||
input = postProcess(input as Type)
|
||||
}
|
||||
|
||||
// validate that the pre process handled correctly the variable
|
||||
if (!this.isOfType(input)) {
|
||||
throw new Error('Post process error occured :(')
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
object: input
|
||||
}
|
||||
}
|
||||
|
||||
public toJSON(): SchemaJSON {
|
||||
|
||||
return {
|
||||
i: this.constructor.name,
|
||||
a: this.attributes.length > 0 ? this.attributes : undefined,
|
||||
c: this.items?.map((it) => it instanceof SchemaItem ? it.toJSON() : it),
|
||||
f: this.savedCalls
|
||||
.map((it) => (it.args ? { n: it.name, a: Array.from(it.args) } : { n: it.name }))
|
||||
}
|
||||
}
|
||||
|
||||
protected addValidation(fn: ((input: Type) => boolean) | { fn: (input: Type) => boolean, error?: string }, error?: string) {
|
||||
|
||||
this.validations.push(typeof fn === 'function' ? { fn, error } : fn)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
protected addPreProcess(fn: (input: unknown) => Type | unknown) {
|
||||
this.preProcess.push(fn)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
protected addPostProcess(fn: (input: Type) => Type) {
|
||||
this.postProcess.push(fn)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public abstract isOfType(input: unknown): input is Type
|
||||
}
|
308
eslint.config.mjs
Normal file
308
eslint.config.mjs
Normal file
@ -0,0 +1,308 @@
|
||||
import js from '@eslint/js'
|
||||
import stylistic from '@stylistic/eslint-plugin'
|
||||
import typescriptEslint from '@typescript-eslint/eslint-plugin'
|
||||
import globals from 'globals'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['tsconfig.json']
|
||||
},
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.browser,
|
||||
...globals.nodeBuiltin
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
'@stylistic': stylistic
|
||||
},
|
||||
ignores: [
|
||||
'node_modules/',
|
||||
'out/',
|
||||
'*.js',
|
||||
'__tests__/',
|
||||
'src/route.ts',
|
||||
'dist/',
|
||||
'.astro/',
|
||||
'.diaz/'
|
||||
],
|
||||
|
||||
rules: {
|
||||
'@stylistic/arrow-parens': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/eol-last': 'error',
|
||||
'@stylistic/indent': [
|
||||
'error',
|
||||
'tab',
|
||||
{
|
||||
SwitchCase: 1
|
||||
}
|
||||
],
|
||||
'@stylistic/linebreak-style': [
|
||||
'error',
|
||||
'unix'
|
||||
],
|
||||
'@stylistic/max-len': [
|
||||
'warn',
|
||||
{
|
||||
code: 256
|
||||
}
|
||||
],
|
||||
'@stylistic/member-delimiter-style': [
|
||||
'error',
|
||||
{
|
||||
multiline: {
|
||||
delimiter: 'none',
|
||||
requireLast: true
|
||||
},
|
||||
singleline: {
|
||||
delimiter: 'comma',
|
||||
requireLast: false
|
||||
}
|
||||
}
|
||||
],
|
||||
'@stylistic/new-parens': 'error',
|
||||
'@stylistic/no-extra-parens': 'off',
|
||||
'@stylistic/no-extra-semi': 'error',
|
||||
'@stylistic/no-multiple-empty-lines': 'error',
|
||||
'@stylistic/no-trailing-spaces': 'error',
|
||||
'@stylistic/quote-props': [
|
||||
'error',
|
||||
'consistent-as-needed'
|
||||
],
|
||||
'@stylistic/quotes': [
|
||||
'error',
|
||||
'single',
|
||||
{
|
||||
avoidEscape: true
|
||||
}
|
||||
],
|
||||
'@stylistic/semi': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'@stylistic/space-before-function-paren': [
|
||||
'error',
|
||||
{
|
||||
anonymous: 'never',
|
||||
asyncArrow: 'always',
|
||||
named: 'never'
|
||||
}
|
||||
],
|
||||
'@stylistic/spaced-comment': [
|
||||
'error',
|
||||
'always',
|
||||
{
|
||||
block: {
|
||||
exceptions: [
|
||||
'*'
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
'@stylistic/type-annotation-spacing': 'error',
|
||||
|
||||
|
||||
'@typescript-eslint/adjacent-overload-signatures': 'error',
|
||||
'@typescript-eslint/array-type': [
|
||||
'error',
|
||||
{
|
||||
default: 'generic'
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/consistent-type-assertions': 'error',
|
||||
'@typescript-eslint/consistent-type-definitions': 'error',
|
||||
'@typescript-eslint/explicit-member-accessibility': [
|
||||
'error',
|
||||
{
|
||||
accessibility: 'explicit'
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/member-ordering': 'error',
|
||||
'@typescript-eslint/no-empty-function': 'error',
|
||||
'@typescript-eslint/no-empty-interface': 'error',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-extra-parens': 'off',
|
||||
'@typescript-eslint/no-extraneous-class': [
|
||||
'warn',
|
||||
{
|
||||
allowStaticOnly: true
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/no-misused-new': 'error',
|
||||
'@typescript-eslint/no-namespace': 'error',
|
||||
'@typescript-eslint/no-non-null-assertion': [
|
||||
'warn'
|
||||
],
|
||||
'@typescript-eslint/no-parameter-properties': 'off',
|
||||
'@typescript-eslint/no-shadow': 'error',
|
||||
'@typescript-eslint/no-unused-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowTernary: true
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
args: 'all',
|
||||
argsIgnorePattern: '^_',
|
||||
caughtErrors: 'all',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
destructuredArrayIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/prefer-for-of': 'error',
|
||||
'@typescript-eslint/prefer-function-type': 'error',
|
||||
'@typescript-eslint/prefer-namespace-keyword': 'error',
|
||||
'@typescript-eslint/restrict-template-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowNumber: true
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/triple-slash-reference': 'error',
|
||||
'@typescript-eslint/unified-signatures': 'error',
|
||||
|
||||
|
||||
'arrow-body-style': 'error',
|
||||
'complexity': [
|
||||
'warn',
|
||||
10
|
||||
],
|
||||
'constructor-super': 'error',
|
||||
'curly': 'error',
|
||||
'dot-notation': 'error',
|
||||
'eqeqeq': [
|
||||
'error',
|
||||
'smart'
|
||||
],
|
||||
'for-direction': 'error',
|
||||
'getter-return': 'error',
|
||||
'guard-for-in': 'error',
|
||||
'id-blacklist': [
|
||||
'error',
|
||||
'any',
|
||||
'Number',
|
||||
'number',
|
||||
'String',
|
||||
'string',
|
||||
'Boolean',
|
||||
'boolean',
|
||||
'Undefined'
|
||||
],
|
||||
'id-length': [
|
||||
'warn',
|
||||
{
|
||||
exceptions: [
|
||||
'_'
|
||||
]
|
||||
}
|
||||
],
|
||||
'id-match': 'error',
|
||||
'max-classes-per-file': [
|
||||
'error',
|
||||
1
|
||||
],
|
||||
'max-depth': [
|
||||
'warn',
|
||||
2
|
||||
],
|
||||
'no-async-promise-executor': 'error',
|
||||
'no-await-in-loop': 'warn',
|
||||
'no-bitwise': 'error',
|
||||
'no-caller': 'error',
|
||||
'no-compare-neg-zero': 'error',
|
||||
'no-cond-assign': 'error',
|
||||
'no-console': 'off',
|
||||
'no-constant-condition': 'error',
|
||||
'no-control-regex': 'warn',
|
||||
'no-debugger': 'error',
|
||||
'no-delete-var': 'error',
|
||||
'no-dupe-args': 'error',
|
||||
'no-dupe-else-if': 'error',
|
||||
'no-dupe-keys': 'error',
|
||||
'no-duplicate-case': 'error',
|
||||
'no-empty': [
|
||||
'error',
|
||||
{
|
||||
allowEmptyCatch: true
|
||||
}
|
||||
],
|
||||
'no-empty-character-class': 'error',
|
||||
'no-eval': 'error',
|
||||
'no-ex-assign': 'error',
|
||||
'no-extra-boolean-cast': 'error',
|
||||
'no-fallthrough': 'off',
|
||||
'no-func-assign': 'error',
|
||||
'no-import-assign': 'error',
|
||||
'no-inner-declarations': 'error',
|
||||
'no-invalid-regexp': 'error',
|
||||
'no-irregular-whitespace': 'error',
|
||||
'no-label-var': 'error',
|
||||
'no-loss-of-precision': 'error',
|
||||
'no-misleading-character-class': 'error',
|
||||
'no-new-wrappers': 'error',
|
||||
'no-obj-calls': 'error',
|
||||
'no-promise-executor-return': 'error',
|
||||
'no-prototype-builtins': 'error',
|
||||
'no-regex-spaces': 'error',
|
||||
'no-setter-return': 'error',
|
||||
'no-shadow': [
|
||||
'error',
|
||||
{
|
||||
builtinGlobals: false,
|
||||
hoist: 'all'
|
||||
}
|
||||
],
|
||||
'no-shadow-restricted-names': 'error',
|
||||
'no-sparse-arrays': 'error',
|
||||
'no-template-curly-in-string': 'warn',
|
||||
'no-throw-literal': 'error',
|
||||
'no-undef': 'error',
|
||||
'no-undef-init': 'error',
|
||||
'no-underscore-dangle': 'off',
|
||||
'no-unexpected-multiline': 'error',
|
||||
'no-unreachable': 'warn',
|
||||
'no-unreachable-loop': 'warn',
|
||||
'no-unsafe-finally': 'error',
|
||||
'no-unsafe-negation': 'error',
|
||||
'no-unsafe-optional-chaining': 'error',
|
||||
'no-unused-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowTernary: true
|
||||
}
|
||||
],
|
||||
'no-unused-labels': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'no-var': 'error',
|
||||
'object-shorthand': [
|
||||
'warn',
|
||||
'methods'
|
||||
],
|
||||
'one-var': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'prefer-arrow-callback': 'warn',
|
||||
'prefer-const': 'error',
|
||||
'prefer-rest-params': 'warn',
|
||||
'radix': 'error',
|
||||
'require-atomic-updates': 'warn',
|
||||
'use-isnan': 'error',
|
||||
'valid-typeof': 'warn'
|
||||
}
|
||||
}
|
||||
]
|
10
index.ts
Normal file
10
index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import s from "./Schema"
|
||||
|
||||
s.string()
|
||||
|
||||
/*
|
||||
buffer
|
||||
date
|
||||
file
|
||||
record
|
||||
*/
|
76
items/array.ts
Normal file
76
items/array.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { parceable } from "../Schema"
|
||||
import SchemaItem from "../SchemaItem"
|
||||
import { SchemaInfer, ValidationError, ValidationResult } from "../types"
|
||||
|
||||
export default class SchemaArray<Type extends SchemaItem> extends SchemaItem<Array<SchemaInfer<Type>>> {
|
||||
public constructor(public readonly values: Type) { super(arguments) }
|
||||
|
||||
public unwrap() {
|
||||
return this.values
|
||||
}
|
||||
|
||||
/**
|
||||
* transform the array so it only contains one of each elements
|
||||
*/
|
||||
@parceable()
|
||||
public unique(): this {
|
||||
this.postProcess.push((input) => input.filter((it, idx) => input.indexOf(it) === idx))
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public parse(input: unknown, options?: { fast?: boolean }): ValidationResult<Array<SchemaInfer<Type>>> {
|
||||
|
||||
// check errors from itself
|
||||
let { valid, object, errors = [] } = super.parse(input, options)
|
||||
|
||||
// skip checking childs if self is not valid (maybe still try to check childs whan fast is false ?)
|
||||
if (!valid) {
|
||||
return {
|
||||
valid,
|
||||
object,
|
||||
errors
|
||||
} as ValidationResult<SchemaInfer<Type>>
|
||||
}
|
||||
|
||||
const clone: Array<SchemaInfer<Type>> = []
|
||||
const errs: Array<ValidationError> = []
|
||||
for (let idx = 0; idx < (object as Array<Type>).length; idx++) {
|
||||
const item = (object as Array<Type>)[idx]
|
||||
const res = this.values.parse(item)
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
const errors = res.errors.map((it) => ({
|
||||
...it,
|
||||
field: it.field ? `${idx}.${it.field}` : idx.toString()
|
||||
}))
|
||||
if (options?.fast) {
|
||||
return {
|
||||
valid: false,
|
||||
object: clone as any,
|
||||
errors: errors
|
||||
}
|
||||
}
|
||||
errs.push(...errors)
|
||||
} else {
|
||||
clone.push(res.object)
|
||||
}
|
||||
}
|
||||
|
||||
if (errs.length > 0) {
|
||||
return {
|
||||
valid: false,
|
||||
object: clone as any,
|
||||
errors: errs
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
object: clone as any
|
||||
}
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is Array<SchemaInfer<Type>> {
|
||||
return Array.isArray(input)
|
||||
}
|
||||
}
|
30
items/boolean.ts
Normal file
30
items/boolean.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { parceable } from "../Schema"
|
||||
import SchemaItem from "../SchemaItem"
|
||||
|
||||
export default class SchemaBoolean extends SchemaItem<boolean> {
|
||||
|
||||
/**
|
||||
* @param [trueValue='true'] the truhty value (default to `'true'`)
|
||||
* @param [falseValue='false'] the falthy value (default to `'false'`)
|
||||
*/
|
||||
@parceable()
|
||||
public parseString(trueValue = 'true', falseValue = 'false'): this {
|
||||
this.addPreProcess((it) => {
|
||||
if (typeof it !== 'string') {
|
||||
return it
|
||||
}
|
||||
if (it === trueValue) {
|
||||
return true
|
||||
}
|
||||
if (it === falseValue) {
|
||||
return false
|
||||
}
|
||||
return it
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is boolean {
|
||||
return typeof input === 'boolean'
|
||||
}
|
||||
}
|
31
items/enum.ts
Normal file
31
items/enum.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import SchemaItem from '../SchemaItem'
|
||||
|
||||
export type EnumLike = {
|
||||
[k: string]: string | number
|
||||
[n: number]: string
|
||||
}
|
||||
|
||||
export default class SchemaEnum<E extends EnumLike> extends SchemaItem<E[keyof E]> {
|
||||
|
||||
private type: 'string' | 'number' = 'string'
|
||||
|
||||
public constructor(private templateEnum: E) {
|
||||
super()
|
||||
|
||||
// get the type of Enum used (Number enums have reverse maping while string enums don't)
|
||||
const firstValue = Object.values(templateEnum)[0]
|
||||
if (typeof firstValue === 'number' || typeof templateEnum[firstValue] === 'number') {
|
||||
this.type = 'number'
|
||||
}
|
||||
|
||||
// test above === numebr
|
||||
this.validations.push({
|
||||
fn: (input) => Object.values(this.templateEnum).includes(input),
|
||||
message: `Input is not part of ${templateEnum.constructor.name}`
|
||||
})
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is E[keyof E] {
|
||||
return typeof input === this.type
|
||||
}
|
||||
}
|
13
items/literal.ts
Normal file
13
items/literal.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import SchemaItem from "../SchemaItem"
|
||||
|
||||
export default class SchemaLiteral<Type> extends SchemaItem<Type> {
|
||||
public constructor(private readonly value: Type) {
|
||||
super(arguments)
|
||||
|
||||
this.validations.push({ fn: (value) => value === this.value })
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is Type {
|
||||
return typeof input === typeof this.value
|
||||
}
|
||||
}
|
29
items/nullable.ts
Normal file
29
items/nullable.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import SchemaItem from "../SchemaItem"
|
||||
import { SchemaInfer, ValidationResult } from "../types"
|
||||
|
||||
export default class SchemaNullable<Type extends SchemaItem> extends SchemaItem<SchemaInfer<Type> | undefined> {
|
||||
public constructor(public readonly child: Type) { super(arguments) }
|
||||
|
||||
public unwrap() {
|
||||
return this.child
|
||||
}
|
||||
|
||||
public parse(input: unknown, options?: { fast?: boolean }): ValidationResult<SchemaInfer<Type>> {
|
||||
if (this.isNull(input)) {
|
||||
return {
|
||||
valid: true,
|
||||
object: undefined
|
||||
}
|
||||
}
|
||||
|
||||
return this.child.parse(input)
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is SchemaInfer<Type> | undefined {
|
||||
return Array.isArray(input)
|
||||
}
|
||||
|
||||
private isNull(value: unknown): value is undefined | null {
|
||||
return typeof value === 'undefined' || value === null
|
||||
}
|
||||
}
|
95
items/number.ts
Normal file
95
items/number.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { parceable } from '../Schema'
|
||||
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
|
||||
*/
|
||||
@parceable()
|
||||
public lte(value: number, message?: string): this {
|
||||
this.addValidation({
|
||||
fn(input) {
|
||||
return input <= value
|
||||
},
|
||||
error: 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
|
||||
*/
|
||||
@parceable()
|
||||
public gte(value: number, message?: string): this {
|
||||
this.addValidation({
|
||||
fn(input) {
|
||||
return input >= value
|
||||
},
|
||||
error: 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
|
||||
*/
|
||||
@parceable()
|
||||
public lt(value: number, message?: string): this {
|
||||
this.addValidation({
|
||||
fn(input) {
|
||||
return input < value
|
||||
},
|
||||
error: 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
|
||||
*/
|
||||
@parceable()
|
||||
public gt(value: number, message?: string): this {
|
||||
this.addValidation({
|
||||
fn(input) {
|
||||
return input > value
|
||||
},
|
||||
error: message
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse strings before validating
|
||||
*/
|
||||
@parceable()
|
||||
public parseString(): this {
|
||||
this.addPreProcess((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)
|
||||
}
|
||||
}
|
59
items/object.ts
Normal file
59
items/object.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { isObject, objectLoop, objectClone } from '@dzeio/object-util'
|
||||
import SchemaItem from "../SchemaItem"
|
||||
import { SchemaInfer, ValidationResult } from "../types"
|
||||
|
||||
type ModelInfer<M extends Record<string, SchemaItem<any>>> = {
|
||||
[key in keyof M]: SchemaInfer<M[key]>
|
||||
}
|
||||
|
||||
export default class SchemaObject<T extends Record<string, SchemaItem<any>>> extends SchemaItem<ModelInfer<T>> {
|
||||
public id: string = 'object'
|
||||
|
||||
public constructor(public readonly model: T) {
|
||||
super(arguments)
|
||||
}
|
||||
|
||||
public override parse(input: unknown, options?: { fast?: boolean }): ValidationResult<ModelInfer<T>> {
|
||||
// check errors from itself
|
||||
let { valid, object, errors = [] } = super.parse(objectClone(input), options)
|
||||
|
||||
// skip checking childs if self is not valid (maybe still try to check childs whan fast is false ?)
|
||||
if (!valid) {
|
||||
return {
|
||||
valid,
|
||||
object,
|
||||
errors
|
||||
} as ValidationResult<ModelInfer<T>>
|
||||
}
|
||||
|
||||
// loop through the childs
|
||||
objectLoop(this.model, (childSchema, key) => {
|
||||
const childValue = object![key]
|
||||
|
||||
// parse the child
|
||||
const child = childSchema.parse(childValue)
|
||||
|
||||
// add errors
|
||||
if ((child.errors?.length ?? 0) > 0) {
|
||||
errors.push(...child.errors!)
|
||||
}
|
||||
|
||||
// @ts-expect-error while it's a generic we know by proof above that it's valid !
|
||||
object![key] = child.object
|
||||
|
||||
// skip rest of items if current one is invalid
|
||||
return child.valid || !options?.fast
|
||||
})
|
||||
|
||||
// answer !
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors: errors,
|
||||
object: object
|
||||
} as ValidationResult<ModelInfer<T>>
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is ModelInfer<T> {
|
||||
return isObject(input)
|
||||
}
|
||||
}
|
96
items/string.ts
Normal file
96
items/string.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { parceable } from "../Schema"
|
||||
import SchemaItem from "../SchemaItem"
|
||||
|
||||
export default class SchemaString extends SchemaItem<string> {
|
||||
public id = 'string'
|
||||
|
||||
public minLength(value: number, message?: string) {
|
||||
return this.min(value, message)
|
||||
}
|
||||
|
||||
public maxLength(value: number, message?: string) {
|
||||
return this.max(value, message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@parceable()
|
||||
public min(value: number, message?: string): this {
|
||||
this.addValidation({
|
||||
fn(input) {
|
||||
return input.length >= value
|
||||
},
|
||||
error: message
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* transform the final text to the defined casing
|
||||
* @param casing the final casing
|
||||
*/
|
||||
@parceable()
|
||||
public toCasing(casing: 'lower' | 'upper'): this {
|
||||
const fn: keyof string = casing === 'lower' ? 'toLowerCase' : 'toUpperCase'
|
||||
this.addPostProcess((it) => it[fn]())
|
||||
|
||||
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
|
||||
*/
|
||||
@parceable()
|
||||
public max(value: number, message?: string): this {
|
||||
this.addValidation({
|
||||
fn(input) {
|
||||
return input.length <= value
|
||||
},
|
||||
error: message
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* the value must not be empty (`''`)
|
||||
* @param message
|
||||
* @returns
|
||||
*/
|
||||
@parceable()
|
||||
public notEmpty(message?: string): this {
|
||||
this.addValidation({
|
||||
fn(input) {
|
||||
return input !== ''
|
||||
},
|
||||
error: message
|
||||
})
|
||||
return 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
|
||||
*/
|
||||
@parceable()
|
||||
public regex(regex: RegExp, message?: string): this {
|
||||
this.addValidation({
|
||||
fn(input) {
|
||||
return regex.test(input)
|
||||
},
|
||||
error: message
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
public override isOfType(input: unknown): input is string {
|
||||
return typeof input === 'string'
|
||||
}
|
||||
}
|
49
items/union.ts
Normal file
49
items/union.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import SchemaItem from '../SchemaItem'
|
||||
import { SchemaInfer, ValidationResult } from '../types'
|
||||
|
||||
type ItemType<T extends Array<SchemaItem<any>>> = SchemaInfer<T[number]>
|
||||
export class SchemaUnion<T extends Array<SchemaItem<any>>> extends SchemaItem<ItemType<T>> {
|
||||
|
||||
private schemas: T
|
||||
|
||||
public constructor(...schemas: T) {
|
||||
super()
|
||||
this.schemas = schemas
|
||||
}
|
||||
|
||||
public parse(input: unknown, options?: { fast?: boolean }): ValidationResult<SchemaInfer<T[number]>> {
|
||||
|
||||
// check errors from itself
|
||||
let { valid, object, errors = [] } = super.parse(input, options)
|
||||
|
||||
// skip checking childs if self is not valid (maybe still try to check childs whan fast is false ?)
|
||||
if (!valid) {
|
||||
return {
|
||||
valid,
|
||||
object,
|
||||
errors
|
||||
} as ValidationResult<SchemaInfer<T[number]>>
|
||||
}
|
||||
|
||||
// loop through the childs
|
||||
for (const schema of this.schemas) {
|
||||
const validation = schema.parse(input, options)
|
||||
if (validation.valid) {
|
||||
return validation
|
||||
} else {
|
||||
errors.push(...validation.errors)
|
||||
}
|
||||
}
|
||||
|
||||
// answer !
|
||||
return {
|
||||
valid: false,
|
||||
errors: errors,
|
||||
object: object
|
||||
} as ValidationResult<SchemaInfer<T[number]>>
|
||||
}
|
||||
|
||||
public override isOfType(input: unknown): input is ItemType<T> {
|
||||
return this.schemas.some((it) => it.isOfType(input))
|
||||
}
|
||||
}
|
13
package.json
Normal file
13
package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@dzeio/object-util": "^1.8.3",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@stylistic/eslint-plugin": "^2.13.0",
|
||||
"@types/bun": "^1.1.18",
|
||||
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
||||
"globals": "^15.14.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.21.0",
|
||||
"vitest": "^3.0.4"
|
||||
}
|
||||
}
|
183
tests/Schema.test.ts
Normal file
183
tests/Schema.test.ts
Normal file
@ -0,0 +1,183 @@
|
||||
import { expect, test } from 'bun:test'
|
||||
import s from '../Schema'
|
||||
|
||||
test('number enum', () => {
|
||||
enum Test {
|
||||
A,
|
||||
B
|
||||
}
|
||||
// test by enum call
|
||||
expect(s.enum(Test).parse(Test.A).valid).toBe(true)
|
||||
// test by value call
|
||||
expect(s.enum(Test).parse(0).valid).toBe(true)
|
||||
// test by key call
|
||||
expect(s.enum(Test).parse('A').valid).toBe(false)
|
||||
})
|
||||
|
||||
test('string enum', () => {
|
||||
enum Test {
|
||||
A = 'B',
|
||||
B = 'A',
|
||||
C = 'D'
|
||||
}
|
||||
// test by enumn call
|
||||
expect(s.enum(Test).parse(Test.A).valid).toBe(true)
|
||||
// test by raw call
|
||||
expect(s.enum(Test).parse('A').valid).toBe(true)
|
||||
// test by key call
|
||||
expect(s.enum(Test).parse('C').valid).toBe(false)
|
||||
})
|
||||
|
||||
test('literal', () => {
|
||||
const schema = s.literal('a')
|
||||
expect(schema.parse('a').valid).toBe(true)
|
||||
expect(schema.parse('b').valid).toBe(false)
|
||||
})
|
||||
|
||||
test('nullable', () => {
|
||||
const schema = s.nullable(s.string())
|
||||
expect(schema.parse('a').valid).toBe(true)
|
||||
expect(schema.parse(undefined).valid).toBe(true)
|
||||
})
|
||||
|
||||
test('number', () => {
|
||||
const schema = s.number()
|
||||
expect(schema.parse(1).valid).toBe(true)
|
||||
expect(schema.parse('1').valid).toBe(false)
|
||||
|
||||
let c = s.number().gt(2)
|
||||
expect(c.parse(3).valid).toBe(true)
|
||||
expect(c.parse(2).valid).toBe(false)
|
||||
c = s.number().lt(2)
|
||||
expect(c.parse(1).valid).toBe(true)
|
||||
expect(c.parse(2).valid).toBe(false)
|
||||
c = s.number().max(2)
|
||||
expect(c.parse(1).valid).toBe(true)
|
||||
expect(c.parse(2).valid).toBe(true)
|
||||
expect(c.parse(3).valid).toBe(false)
|
||||
c = s.number().min(2)
|
||||
expect(c.parse(1).valid).toBe(false)
|
||||
expect(c.parse(2).valid).toBe(true)
|
||||
expect(c.parse(3).valid).toBe(true)
|
||||
c = s.number().parseString()
|
||||
expect(c.parse(1).valid).toBe(true)
|
||||
expect(c.parse('2').valid).toBe(true)
|
||||
expect(c.parse(true).valid).toBe(false)
|
||||
})
|
||||
|
||||
test('object', () => {
|
||||
const schema = s.object({
|
||||
a: s.string(),
|
||||
b: s.number()
|
||||
})
|
||||
expect(schema.parse({ a: 'a', b: 1 }).valid).toBe(true)
|
||||
expect(schema.parse({ a: 'a', b: '1' }).valid).toBe(false)
|
||||
})
|
||||
|
||||
test('string', () => {
|
||||
const schema = s.string()
|
||||
expect(schema.parse('1').valid).toBe(true)
|
||||
expect(schema.parse(1).valid).toBe(false)
|
||||
|
||||
// min length
|
||||
let c = s.string()
|
||||
.minLength(8)
|
||||
expect(c.parse('12345678').valid).toBe(true)
|
||||
expect(c.parse('1234567').valid).toBe(false)
|
||||
|
||||
// max length
|
||||
c = s.string()
|
||||
.maxLength(8)
|
||||
expect(c.parse('12345678').valid).toBe(true)
|
||||
expect(c.parse('123456789').valid).toBe(false)
|
||||
|
||||
// to lower case
|
||||
c = s.string()
|
||||
.toCasing('lower')
|
||||
expect(c.parse('HELLO WORLD').valid).toBe(true)
|
||||
expect(c.parse('HELLO WORLD').object).toBe('hello world')
|
||||
|
||||
// to upper case
|
||||
c = s.string()
|
||||
.toCasing('upper')
|
||||
expect(c.parse('hello world').valid).toBe(true)
|
||||
expect(c.parse('hello world').object).toBe('HELLO WORLD')
|
||||
|
||||
// to not empty
|
||||
c = s.string()
|
||||
.notEmpty()
|
||||
expect(c.parse('hello world').valid).toBe(true)
|
||||
expect(c.parse('').valid).toBe(false)
|
||||
|
||||
// regex validation
|
||||
c = s.string()
|
||||
.regex(/llo/g)
|
||||
expect(c.parse('hello world').valid).toBe(true)
|
||||
expect(c.parse('hell world').valid).toBe(false)
|
||||
|
||||
})
|
||||
|
||||
test('union', () => {
|
||||
const schema = s.union(s.string(), s.number())
|
||||
expect(schema.parse('1').valid).toBe(true)
|
||||
expect(schema.parse(1).valid).toBe(true)
|
||||
expect(schema.parse(true).valid).toBe(false)
|
||||
})
|
||||
|
||||
test('union advanced', () => {
|
||||
const schema = s.union(s.object({
|
||||
type: s.literal('pokemon' as const),
|
||||
value: s.string()
|
||||
}), s.object({
|
||||
type: s.literal('pouet' as const),
|
||||
value: s.number()
|
||||
}))
|
||||
|
||||
expect(schema.parse({
|
||||
type: 'pokemon',
|
||||
value: 'a'
|
||||
}).valid).toBe(true)
|
||||
|
||||
expect(schema.parse({
|
||||
type: 'pouet',
|
||||
value: 'a'
|
||||
}).valid).toBe(false)
|
||||
|
||||
expect(schema.parse({
|
||||
type: 'pouet',
|
||||
value: 123
|
||||
}).valid).toBe(true)
|
||||
})
|
||||
|
||||
test('array', () => {
|
||||
const schema = s.array(s.string())
|
||||
expect(schema.parse(['1']).valid).toBe(true)
|
||||
expect(schema.parse([]).valid).toBe(true)
|
||||
|
||||
expect(schema.parse([1]).valid).toBe(false)
|
||||
expect(schema.parse(['1', 1]).valid).toBe(false)
|
||||
|
||||
const a = s.array(s.string())
|
||||
.unique()
|
||||
|
||||
expect(a.parse(['1', '2', '1']).valid).toBe(true)
|
||||
expect(a.parse(['1', '2', '1']).object).toEqual(['1', '2'])
|
||||
|
||||
})
|
||||
|
||||
test('boolean', () => {
|
||||
const schema = s.boolean()
|
||||
expect(schema.parse(true).valid).toBe(true)
|
||||
expect(schema.parse(false).valid).toBe(true)
|
||||
|
||||
const p = s.boolean().parseString('oui', 'non')
|
||||
expect(p.parse('oui').valid).toBe(true)
|
||||
expect(p.parse('pokemon').valid).toBe(false)
|
||||
expect(p.parse(1).valid).toBe(false)
|
||||
expect(p.parse('oui').object).toBe(true)
|
||||
expect(p.parse('non').valid).toBe(true)
|
||||
expect(p.parse('non').object).toBe(false)
|
||||
|
||||
expect(schema.parse('true').valid).toBe(false)
|
||||
expect(schema.parse('false').valid).toBe(false)
|
||||
})
|
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"exclude": [
|
||||
"cypress"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
}
|
||||
}
|
29
types.d.ts
vendored
Normal file
29
types.d.ts
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
import SchemaItem from "./SchemaItem"
|
||||
|
||||
export type SchemaInfer<Type extends SchemaItem> = Type extends SchemaItem<infer X> ? X : never
|
||||
|
||||
export interface ValidationError {
|
||||
message: string
|
||||
field?: string
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
export type ValidationResult<T> = {
|
||||
object: T
|
||||
valid: true
|
||||
errors?: undefined
|
||||
} | {
|
||||
valid: false
|
||||
object?: (T extends object ? Partial<T> : T) | undefined
|
||||
errors: Array<ValidationError>
|
||||
}
|
||||
|
||||
export interface SchemaJSON<T extends SchemaItem = any> {
|
||||
i: string
|
||||
a?: Array<string> | undefined
|
||||
c?: Array<unknown> | undefined
|
||||
f?: Array<{
|
||||
n: string
|
||||
a?: Array<unknown> | undefined
|
||||
}> | undefined
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user