import { Context } from 'cordis'; import Schema from 'schemastery'; import { ValidationError } from './error'; import type { Handler } from './server'; import { Converter, Type, Validator } from './validator'; type MethodDecorator = (target: any, funcName: string, obj: any) => any; type ClassDecorator = any>(Class: T) => T extends new (...args: infer R) => infer S ? new (...args: R) => S : never; export interface ParamOption { name: string; source: 'all' | 'get' | 'post' | 'route'; isOptional?: boolean | 'convert'; convert?: Converter; 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]; if (type[1]) v.validate = type[1]; if (type[2]) v.isOptional = type[2]; } else if (typeof current === 'boolean') v.isOptional = current; else if (isValidate) { if (current !== null) v.validate = v.validate ? mergeValidator(v.validate, current) : current; isValidate = false; } else v.convert = current; cursor++; } return v; } function _descriptor(v: ParamOption) { return function desc(this: Handler, target: any, funcName: string, obj: any) { target.__param ||= {}; target.__param[target.constructor.name] ||= {}; if (!target.__param[target.constructor.name][funcName]) { const originalMethod = obj.value; const val = originalMethod.toString(); const firstArg = val.split(')')[0]?.split(',')[0]?.split('(')[1]?.trim() || ''; const domainIdStyle = firstArg.toLowerCase().startsWith('domainid'); target.__param[target.constructor.name][funcName] = []; obj.value = function validate(this: Handler, rawArgs: any, ...extra: any[]) { if (typeof rawArgs !== 'object' || extra.length) return originalMethod.call(this, rawArgs, ...extra); const c = []; const arglist: ParamOption[] = target.__param[target.constructor.name][funcName]; if (typeof rawArgs.domainId !== 'string' || !rawArgs.domainId) throw new ValidationError('domainId'); for (const item of arglist) { const src = item.source === 'all' ? rawArgs : item.source === 'get' ? this.request.query : item.source === 'route' ? { ...this.request.params, domainId: this.args.domainId } : this.request.body; const value = src[item.name]; if (!item.isOptional || value) { if (value === undefined || value === null || value === '') throw new ValidationError(item.name); if (item.validate && !item.validate(value)) throw new ValidationError(item.name); if (item.convert) c.push(item.convert(value)); else c.push(value); } else if (item.isOptional === 'convert') { c.push(item.convert ? item.convert(value) : value); } else c.push(undefined); } return domainIdStyle ? originalMethod.call(this, rawArgs.domainId, ...c) : originalMethod.call(this, rawArgs, ...c); }; } target.__param[target.constructor.name][funcName].unshift(v); return obj; }; } type DescriptorBuilder = ((name: string, type: Type) => MethodDecorator) & ((name: string, type: Type, validate: null, convert: Converter) => MethodDecorator) & ((name: string, type: Type, validate?: Validator, convert?: Converter) => MethodDecorator) & ((name: string, type?: Type, isOptional?: boolean, validate?: Validator, convert?: Converter) => MethodDecorator) & ((name: string, ...args: Array | boolean | Validator | Converter>) => MethodDecorator); export const get: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'get', ...args)); export const query: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'get', ...args)); export const post: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'post', ...args)); export const route: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'route', ...args)); export const param: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'all', ...args)); export const subscribe: (name: string) => MethodDecorator & ClassDecorator = (name) => (target, funcName?, obj?) => { if (funcName) { target.__subscribe ||= []; target.__subscribe.push({ name, target: obj.value }); return obj; } return (...args) => { const c = new target(...args); // eslint-disable-line new-cap c.__subscribe = [{ name, target: c.send }]; return c; }; };