diff --git a/framework/framework/decorators.ts b/framework/framework/decorators.ts index c23519794..1e9ef6f65 100644 --- a/framework/framework/decorators.ts +++ b/framework/framework/decorators.ts @@ -1,4 +1,5 @@ import { Context } from 'cordis'; +import Schema from 'schemastery'; import { ValidationError } from './error'; import type { Handler } from './server'; import { Converter, Type, Validator } from './validator'; @@ -14,14 +15,32 @@ export interface ParamOption { validate?: Validator, } +const kSchema = Symbol.for('schemastery'); const mergeValidator = (L: Validator, R: Validator) => (v: any) => L(v) && R(v); +function isSchema(v: any): v is Schema { + return v && typeof v === 'function' && kSchema in v; +} + function _buildParam(name: string, source: 'get' | 'post' | 'all' | 'route', ...args: Array | boolean | Validator | Converter>) { let cursor = 0; const v: ParamOption = { name, source }; let isValidate = true; while (cursor < args.length) { const current = args[cursor]; + if (isSchema(current)) { + v.validate = (val) => { + try { + current(val); + return true; + } catch (e) { + return false; + } + }; + v.convert = (val) => current(val); + cursor++; + continue; + } if (current instanceof Array) { const type = current; if (type[0]) v.convert = type[0]; diff --git a/framework/framework/tests/validator.spec.ts b/framework/framework/tests/validator.spec.ts new file mode 100644 index 000000000..4fc830bd8 --- /dev/null +++ b/framework/framework/tests/validator.spec.ts @@ -0,0 +1,20 @@ +import { expect } from 'chai'; +import { describe, it } from 'node:test'; +import { Types } from '../validator'; + +const k = Symbol.for('schemastery'); + +describe('validator', () => { + it('NumericArray', () => { + expect(Types.NumericArray('1,2,3')).to.deep.equal([1, 2, 3]); + expect(Types.NumericArray([1, 2, 3])).to.deep.equal([1, 2, 3]); + expect(() => Types.NumericArray('123a')).to.throw(); + }); + + it('CommaSeperatedArray', () => { + expect(Types.CommaSeperatedArray('1,2,3')).to.deep.equal(['1', '2', '3']); + expect(Types.CommaSeperatedArray([1, 2, 3])).to.deep.equal(['1', '2', '3']); + expect(Types.CommaSeperatedArray('123a')).to.deep.equal(['123a']); + expect(k in Types.CommaSeperatedArray).to.eq(true); + }); +}); diff --git a/framework/framework/validator.ts b/framework/framework/validator.ts index 261866183..794e0a509 100644 --- a/framework/framework/validator.ts +++ b/framework/framework/validator.ts @@ -4,11 +4,29 @@ import { isSafeInteger } from 'lodash'; import moment from 'moment-timezone'; import sanitize from 'sanitize-filename'; import saslprep from 'saslprep'; +import Schema from 'schemastery'; type InputType = string | number | Record | any[]; export type Converter = (value: any) => T; export type Validator = (value: Loose extends true ? any : InputType) => boolean; -export type Type = readonly [Converter, Validator?, (boolean | 'convert')?]; +export type Type = Schema | readonly [Converter, Validator?, (boolean | 'convert')?]; + +const MaybeArray = (inner: Schema) => Schema.union([Schema.array(inner), inner]); +type CheckFunction = (v: IsNumber extends true ? number : string) => boolean; +const ArrayBase = (check: CheckFunction, number: IsNumber, doSplit: boolean = number) => Schema.transform( + MaybeArray(Schema.union([Number, String])), + (v) => { + const input = (doSplit && typeof v === 'string') ? v.split(',') : v; + const res = number + ? (typeof input === 'string' ? [+input] : typeof input === 'number' ? [input] : input.map(Number)) + : typeof input === 'string' ? [input] : typeof input === 'number' ? [input.toString()] : input.map(String); + const locate = number + ? res.find((i) => !Number.isFinite(+i) || !check(+i as any)) + : res.find((i) => !check(i as any)); + if (locate !== undefined) throw new Error(`Invalid input: ${locate}`); + return res; + }, +) as Schema; export interface Types { // String outputs @@ -41,8 +59,6 @@ export interface Types { Date: Type; Time: Type; Range: (range: Array | Record) => Type; - /** @deprecated */ - Array: Type; NumericArray: Type; CommaSeperatedArray: Type; Set: Type>; @@ -74,7 +90,7 @@ const saslprepString = (regex?: RegExp, cb?: (i: string) => boolean, }, ] as [(v) => string, (v) => boolean]; -export const Types: Types = { +export const Types = { Content: [(v) => v.toString().trim(), (v) => v?.toString()?.trim() && v.toString().trim().length < 65536], Key: saslprepString(/^[a-zA-Z0-9-_]{1,255}$/), /** @deprecated */ @@ -147,22 +163,8 @@ export const Types: Types = { return false; }, ], - /** @deprecated suggested to use Types.ArrayOf instead. */ - Array: [(v) => { - if (v instanceof Array) return v; - return v ? [v] : []; - }, null], - NumericArray: [(v) => { - if (v instanceof Array) return v.map(Number); - return v.split(',').map(Number); - }, (v) => { - if (v instanceof Array) return v.map(Number).every(Number.isSafeInteger); - return v.toString().split(',').map(Number).every(Number.isSafeInteger); - }], - CommaSeperatedArray: [ - (v) => v.toString().replace(/,/g, ',').split(',').map((e) => e.trim()).filter((i) => i), - (v) => !!v.toString(), - ], + NumericArray: ArrayBase(Number.isFinite, true), + CommaSeperatedArray: ArrayBase(() => true, false, true), Set: [(v) => { if (v instanceof Array) return new Set(v); return v ? new Set([v]) : new Set(); @@ -187,11 +189,11 @@ export const Types: Types = { (v) => types.find((type) => type[1](v))[0](v), (v) => types.some((type) => type[1](v)), ] as any, -}; +} satisfies Types; try { const { ObjectId } = require('mongodb'); - Types.ObjectId = [(v) => new ObjectId(v), ObjectId.isValid]; + Types.ObjectId = [((v) => new ObjectId(v)) as any, ObjectId.isValid]; } catch (e) { }