mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core: split validator
This commit is contained in:
1 parent
149c3671f7
commit
05b27fc1eb
5 files changed
+185
-180
No files matched your search
@@ -1,5 +1,5 @@
|
|||||||
import { Types } from '../service/decorators';
|
|
||||||
import { md5 } from '../utils';
|
import { md5 } from '../utils';
|
||||||
|
import { Types } from './validator';
|
||||||
|
|
||||||
type AvatarProvider = (src: string, size: number) => string;
|
type AvatarProvider = (src: string, size: number) => string;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
import emojiRegex from 'emoji-regex';
|
||||||
|
import { isSafeInteger } from 'lodash';
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
import { ObjectID } from 'mongodb';
|
||||||
|
import saslprep from 'saslprep';
|
||||||
|
|
||||||
|
type InputType = string | number | Record<string, any> | any[];
|
||||||
|
export type Converter<T> = (value: any) => T;
|
||||||
|
export type Validator<Loose extends boolean = true> = (value: Loose extends true ? any : InputType) => boolean;
|
||||||
|
export type Type<T> = readonly [Converter<T>, Validator<false>?, boolean?];
|
||||||
|
|
||||||
|
export interface Types {
|
||||||
|
// String outputs
|
||||||
|
Content: Type<string>;
|
||||||
|
Key: Type<string>;
|
||||||
|
/** @deprecated */
|
||||||
|
Name: Type<string>;
|
||||||
|
Username: Type<string>;
|
||||||
|
Password: Type<string>;
|
||||||
|
UidOrName: Type<string>;
|
||||||
|
Email: Type<string>;
|
||||||
|
Filename: Type<string>;
|
||||||
|
DomainId: Type<string>;
|
||||||
|
ProblemId: Type<string | number>;
|
||||||
|
Role: Type<string>;
|
||||||
|
Title: Type<string>;
|
||||||
|
Emoji: Type<string>;
|
||||||
|
String: Type<string>;
|
||||||
|
|
||||||
|
// Number outputs
|
||||||
|
Int: Type<number>;
|
||||||
|
UnsignedInt: Type<number>;
|
||||||
|
PositiveInt: Type<number>;
|
||||||
|
Float: Type<number>;
|
||||||
|
|
||||||
|
// Other
|
||||||
|
ObjectID: Type<ObjectID>;
|
||||||
|
Boolean: Type<boolean>;
|
||||||
|
Date: Type<string>;
|
||||||
|
Time: Type<string>;
|
||||||
|
Range: <T extends string | number>(range: Array<T> | Record<string, any>) => Type<T>;
|
||||||
|
/** @deprecated */
|
||||||
|
Array: Type<any[]>;
|
||||||
|
NumericArray: Type<number[]>;
|
||||||
|
CommaSeperatedArray: Type<string[]>;
|
||||||
|
Set: Type<Set<any>>;
|
||||||
|
Any: Type<any>;
|
||||||
|
ArrayOf: <T extends Type<any>>(type: T) => (T extends Type<infer R> ? Type<R[]> : never);
|
||||||
|
AnyOf: <T extends Type<any>>(...type: T[]) => (T extends Type<infer R> ? Type<R> : never);
|
||||||
|
}
|
||||||
|
|
||||||
|
const saslprepString = <T = string>(regex?: RegExp, cb?: (i: string) => boolean, convert?: (i: string) => T) => [
|
||||||
|
convert || ((v) => saslprep(v.toString().trim())),
|
||||||
|
(v) => {
|
||||||
|
try {
|
||||||
|
const res = saslprep(v.toString().trim());
|
||||||
|
if (regex && !regex.test(res)) return false;
|
||||||
|
if (cb && !cb(res)) return false;
|
||||||
|
return !!res.length;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
] as [(v) => string, (v) => boolean];
|
||||||
|
|
||||||
|
export const Types: Types = {
|
||||||
|
Content: [(v) => v.toString().trim(), (v) => v && v.toString().trim().length < 65536],
|
||||||
|
Key: saslprepString(/^[a-zA-Z0-9-_]+$/),
|
||||||
|
/** @deprecated */
|
||||||
|
Name: saslprepString(/^.{1,255}$/),
|
||||||
|
Filename: saslprepString(/^[^\\/?#~!|*]{1,255}$/, (i) => !['con', '.', '..'].includes(i)),
|
||||||
|
UidOrName: saslprepString(/^(.{3,31}|[\u4e00-\u9fa5]{2}|-?[0-9]+)$/),
|
||||||
|
Username: saslprepString(/^(.{3,31}|[\u4e00-\u9fa5]{2})$/),
|
||||||
|
Password: saslprepString(/^.{6,255}$/),
|
||||||
|
ProblemId: saslprepString(/^[a-zA-Z0-9]+$/i, () => true, (s) => (Number.isSafeInteger(+s) ? +s : s)),
|
||||||
|
Email: saslprepString(/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i),
|
||||||
|
DomainId: saslprepString(/^[a-zA-Z][a-zA-Z0-9_]{3,31}$/),
|
||||||
|
Role: saslprepString(/^[_0-9A-Za-z\u4e00-\u9fa5]{1,31}$/i),
|
||||||
|
Title: saslprepString(/^.{1,64}$/),
|
||||||
|
String: [(v) => v.toString(), null],
|
||||||
|
|
||||||
|
Int: [(v) => +v, (v) => /^[+-]?[0-9]+$/.test(v.toString().trim()) && isSafeInteger(+v)],
|
||||||
|
UnsignedInt: [(v) => +v, (v) => /^(-0|\+?[0-9]+)$/.test(v.toString().trim()) && isSafeInteger(+v)],
|
||||||
|
PositiveInt: [(v) => +v, (v) => /^\+?[1-9][0-9]*$/.test(v.toString().trim()) && isSafeInteger(+v)],
|
||||||
|
Float: [(v) => +v, (v) => Number.isFinite(+v)],
|
||||||
|
|
||||||
|
ObjectID: [(v) => new ObjectID(v), ObjectID.isValid],
|
||||||
|
Boolean: [(v) => v && !['false', 'off'].includes(v), null, true],
|
||||||
|
Date: [
|
||||||
|
(v) => {
|
||||||
|
const d = v.split('-');
|
||||||
|
assert(d.length === 3);
|
||||||
|
return `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`;
|
||||||
|
},
|
||||||
|
(v) => {
|
||||||
|
const d = v.toString().split('-');
|
||||||
|
if (d.length !== 3) return false;
|
||||||
|
const st = `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`;
|
||||||
|
return moment(st).isValid();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Time: [
|
||||||
|
(v) => {
|
||||||
|
const t = v.split(':');
|
||||||
|
assert(t.length === 2);
|
||||||
|
return `${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`;
|
||||||
|
},
|
||||||
|
(v) => {
|
||||||
|
const t = v.toString().split(':');
|
||||||
|
if (t.length !== 2) return false;
|
||||||
|
return moment(`2020-01-01 ${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`).isValid();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Range: (range) => [
|
||||||
|
(v) => {
|
||||||
|
if (range instanceof Array) {
|
||||||
|
for (const item of range) {
|
||||||
|
if (typeof item === 'number' && item === +v) return +v;
|
||||||
|
if (item === v) return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
},
|
||||||
|
(v) => {
|
||||||
|
if (range instanceof Array) {
|
||||||
|
for (const item of range) {
|
||||||
|
if (typeof item === 'string' && item === v) return true;
|
||||||
|
if (typeof item === 'number' && item === +v) return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const key in range) {
|
||||||
|
if (key === v) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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(),
|
||||||
|
],
|
||||||
|
Set: [(v) => {
|
||||||
|
if (v instanceof Array) return new Set(v);
|
||||||
|
return v ? new Set([v]) : new Set();
|
||||||
|
}, null],
|
||||||
|
Emoji: [
|
||||||
|
(v: string) => v.matchAll(emojiRegex()).next().value[0],
|
||||||
|
(v) => emojiRegex().test(v.toString()),
|
||||||
|
],
|
||||||
|
Any: [(v) => v, null],
|
||||||
|
ArrayOf: (type) => [
|
||||||
|
(v) => {
|
||||||
|
const arr = v instanceof Array ? v : [v];
|
||||||
|
return arr.map((i) => type[0](i));
|
||||||
|
},
|
||||||
|
(v) => {
|
||||||
|
if (!type[1]) return true;
|
||||||
|
const arr = v instanceof Array ? v : [v];
|
||||||
|
return arr.every((i) => type[1](i));
|
||||||
|
},
|
||||||
|
] as any,
|
||||||
|
AnyOf: (...types) => [
|
||||||
|
(v) => types.find((type) => type[1](v))[0](v),
|
||||||
|
(v) => types.some((type) => type[1](v)),
|
||||||
|
] as any,
|
||||||
|
};
|
||||||
@@ -41,6 +41,7 @@ export { buildContent } from './lib/content';
|
|||||||
export { default as avatar } from './lib/avatar';
|
export { default as avatar } from './lib/avatar';
|
||||||
export { default as rank } from './lib/rank';
|
export { default as rank } from './lib/rank';
|
||||||
export { default as paginate } from './lib/paginate';
|
export { default as paginate } from './lib/paginate';
|
||||||
|
export * from './lib/validator';
|
||||||
export * from './service/decorators';
|
export * from './service/decorators';
|
||||||
export {
|
export {
|
||||||
Handler, ConnectionHandler, captureAllRoutes,
|
Handler, ConnectionHandler, captureAllRoutes,
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
import assert from 'assert';
|
|
||||||
import emojiRegex from 'emoji-regex';
|
|
||||||
import { isSafeInteger } from 'lodash';
|
|
||||||
import moment from 'moment-timezone';
|
|
||||||
import { ObjectID } from 'mongodb';
|
|
||||||
import saslprep from 'saslprep';
|
|
||||||
import { Time } from '@hydrooj/utils';
|
import { Time } from '@hydrooj/utils';
|
||||||
import { ValidationError } from '../error';
|
import { ValidationError } from '../error';
|
||||||
|
import { Converter, Type, Validator } from '../lib/validator';
|
||||||
import type { Handler } from './server';
|
import type { Handler } from './server';
|
||||||
|
|
||||||
type MethodDecorator = (target: any, name: string, obj: any) => any;
|
type MethodDecorator = (target: any, name: string, obj: any) => any;
|
||||||
type InputType = string | number | Record<string, any> | any[];
|
|
||||||
type Converter<T> = (value: any) => T;
|
|
||||||
type Validator<Loose extends boolean = true> = (value: Loose extends true ? any : InputType) => boolean;
|
|
||||||
export interface ParamOption<T> {
|
export interface ParamOption<T> {
|
||||||
name: string,
|
name: string,
|
||||||
source: 'all' | 'get' | 'post' | 'route',
|
source: 'all' | 'get' | 'post' | 'route',
|
||||||
@@ -20,176 +12,6 @@ export interface ParamOption<T> {
|
|||||||
validate?: Validator,
|
validate?: Validator,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Type<T> = readonly [Converter<T>, Validator<false>?, boolean?];
|
|
||||||
|
|
||||||
export interface Types {
|
|
||||||
// String outputs
|
|
||||||
Content: Type<string>;
|
|
||||||
Key: Type<string>;
|
|
||||||
/** @deprecated */
|
|
||||||
Name: Type<string>;
|
|
||||||
Username: Type<string>;
|
|
||||||
Password: Type<string>;
|
|
||||||
UidOrName: Type<string>;
|
|
||||||
Email: Type<string>;
|
|
||||||
Filename: Type<string>;
|
|
||||||
DomainId: Type<string>;
|
|
||||||
ProblemId: Type<string | number>;
|
|
||||||
Role: Type<string>;
|
|
||||||
Title: Type<string>;
|
|
||||||
Emoji: Type<string>;
|
|
||||||
String: Type<string>;
|
|
||||||
|
|
||||||
// Number outputs
|
|
||||||
Int: Type<number>;
|
|
||||||
UnsignedInt: Type<number>;
|
|
||||||
PositiveInt: Type<number>;
|
|
||||||
Float: Type<number>;
|
|
||||||
|
|
||||||
// Other
|
|
||||||
ObjectID: Type<ObjectID>;
|
|
||||||
Boolean: Type<boolean>;
|
|
||||||
Date: Type<string>;
|
|
||||||
Time: Type<string>;
|
|
||||||
Range: <T extends string | number>(range: Array<T> | Record<string, any>) => Type<T>;
|
|
||||||
/** @deprecated */
|
|
||||||
Array: Type<any[]>;
|
|
||||||
NumericArray: Type<number[]>;
|
|
||||||
CommaSeperatedArray: Type<string[]>;
|
|
||||||
Set: Type<Set<any>>;
|
|
||||||
Any: Type<any>;
|
|
||||||
ArrayOf: <T extends Type<any>>(type: T) => (T extends Type<infer R> ? Type<R[]> : never);
|
|
||||||
AnyOf: <T extends Type<any>>(...type: T[]) => (T extends Type<infer R> ? Type<R> : never);
|
|
||||||
}
|
|
||||||
|
|
||||||
const saslprepString = <T = string>(regex?: RegExp, cb?: (i: string) => boolean, convert?: (i: string) => T) => [
|
|
||||||
convert || ((v) => saslprep(v.toString().trim())),
|
|
||||||
(v) => {
|
|
||||||
try {
|
|
||||||
const res = saslprep(v.toString().trim());
|
|
||||||
if (regex && !regex.test(res)) return false;
|
|
||||||
if (cb && !cb(res)) return false;
|
|
||||||
return !!res.length;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
] as [(v) => string, (v) => boolean];
|
|
||||||
|
|
||||||
export const Types: Types = {
|
|
||||||
Content: [(v) => v.toString().trim(), (v) => v && v.toString().trim().length < 65536],
|
|
||||||
Key: saslprepString(/^[a-zA-Z0-9-_]+$/),
|
|
||||||
/** @deprecated */
|
|
||||||
Name: saslprepString(/^.{1,255}$/),
|
|
||||||
Filename: saslprepString(/^[^\\/?#~!|*]{1,255}$/, (i) => !['con', '.', '..'].includes(i)),
|
|
||||||
UidOrName: saslprepString(/^(.{3,31}|[\u4e00-\u9fa5]{2}|-?[0-9]+)$/),
|
|
||||||
Username: saslprepString(/^(.{3,31}|[\u4e00-\u9fa5]{2})$/),
|
|
||||||
Password: saslprepString(/^.{6,255}$/),
|
|
||||||
ProblemId: saslprepString(/^[a-zA-Z0-9]+$/i, () => true, (s) => (Number.isSafeInteger(+s) ? +s : s)),
|
|
||||||
Email: saslprepString(/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i),
|
|
||||||
DomainId: saslprepString(/^[a-zA-Z][a-zA-Z0-9_]{3,31}$/),
|
|
||||||
Role: saslprepString(/^[_0-9A-Za-z\u4e00-\u9fa5]{1,31}$/i),
|
|
||||||
Title: saslprepString(/^.{1,64}$/),
|
|
||||||
String: [(v) => v.toString(), null],
|
|
||||||
|
|
||||||
Int: [(v) => +v, (v) => /^[+-]?[0-9]+$/.test(v.toString().trim()) && isSafeInteger(+v)],
|
|
||||||
UnsignedInt: [(v) => +v, (v) => /^(-0|\+?[0-9]+)$/.test(v.toString().trim()) && isSafeInteger(+v)],
|
|
||||||
PositiveInt: [(v) => +v, (v) => /^\+?[1-9][0-9]*$/.test(v.toString().trim()) && isSafeInteger(+v)],
|
|
||||||
Float: [(v) => +v, (v) => Number.isFinite(+v)],
|
|
||||||
|
|
||||||
ObjectID: [(v) => new ObjectID(v), ObjectID.isValid],
|
|
||||||
Boolean: [(v) => v && !['false', 'off'].includes(v), null, true],
|
|
||||||
Date: [
|
|
||||||
(v) => {
|
|
||||||
const d = v.split('-');
|
|
||||||
assert(d.length === 3);
|
|
||||||
return `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`;
|
|
||||||
},
|
|
||||||
(v) => {
|
|
||||||
const d = v.toString().split('-');
|
|
||||||
if (d.length !== 3) return false;
|
|
||||||
const st = `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`;
|
|
||||||
return moment(st).isValid();
|
|
||||||
},
|
|
||||||
],
|
|
||||||
Time: [
|
|
||||||
(v) => {
|
|
||||||
const t = v.split(':');
|
|
||||||
assert(t.length === 2);
|
|
||||||
return `${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`;
|
|
||||||
},
|
|
||||||
(v) => {
|
|
||||||
const t = v.toString().split(':');
|
|
||||||
if (t.length !== 2) return false;
|
|
||||||
return moment(`2020-01-01 ${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`).isValid();
|
|
||||||
},
|
|
||||||
],
|
|
||||||
Range: (range) => [
|
|
||||||
(v) => {
|
|
||||||
if (range instanceof Array) {
|
|
||||||
for (const item of range) {
|
|
||||||
if (typeof item === 'number' && item === +v) return +v;
|
|
||||||
if (item === v) return v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
},
|
|
||||||
(v) => {
|
|
||||||
if (range instanceof Array) {
|
|
||||||
for (const item of range) {
|
|
||||||
if (typeof item === 'string' && item === v) return true;
|
|
||||||
if (typeof item === 'number' && item === +v) return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const key in range) {
|
|
||||||
if (key === v) return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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(),
|
|
||||||
],
|
|
||||||
Set: [(v) => {
|
|
||||||
if (v instanceof Array) return new Set(v);
|
|
||||||
return v ? new Set([v]) : new Set();
|
|
||||||
}, null],
|
|
||||||
Emoji: [
|
|
||||||
(v: string) => v.matchAll(emojiRegex()).next().value[0],
|
|
||||||
(v) => emojiRegex().test(v.toString()),
|
|
||||||
],
|
|
||||||
Any: [(v) => v, null],
|
|
||||||
ArrayOf: (type) => [
|
|
||||||
(v) => {
|
|
||||||
const arr = v instanceof Array ? v : [v];
|
|
||||||
return arr.map((i) => type[0](i));
|
|
||||||
},
|
|
||||||
(v) => {
|
|
||||||
if (!type[1]) return true;
|
|
||||||
const arr = v instanceof Array ? v : [v];
|
|
||||||
return arr.every((i) => type[1](i));
|
|
||||||
},
|
|
||||||
] as any,
|
|
||||||
AnyOf: (...types) => [
|
|
||||||
(v) => types.find((type) => type[1](v))[0](v),
|
|
||||||
(v) => types.some((type) => type[1](v)),
|
|
||||||
] as any,
|
|
||||||
};
|
|
||||||
|
|
||||||
function _buildParam(name: string, source: 'get' | 'post' | 'all' | 'route', ...args: Array<Type<any> | boolean | Validator | Converter<any>>) {
|
function _buildParam(name: string, source: 'get' | 'post' | 'all' | 'route', ...args: Array<Type<any> | boolean | Validator | Converter<any>>) {
|
||||||
let cursor = 0;
|
let cursor = 0;
|
||||||
const v: ParamOption<any> = { name, source };
|
const v: ParamOption<any> = { name, source };
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
UserFacingError,
|
UserFacingError,
|
||||||
} from '../error';
|
} from '../error';
|
||||||
import { DomainDoc } from '../interface';
|
import { DomainDoc } from '../interface';
|
||||||
|
import { Types } from '../lib/validator';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { PERM, PRIV } from '../model/builtin';
|
import { PERM, PRIV } from '../model/builtin';
|
||||||
import * as opcount from '../model/opcount';
|
import * as opcount from '../model/opcount';
|
||||||
@@ -35,6 +36,7 @@ import { Router } from './router';
|
|||||||
import { encodeRFC5987ValueChars } from './storage';
|
import { encodeRFC5987ValueChars } from './storage';
|
||||||
|
|
||||||
export * from './decorators';
|
export * from './decorators';
|
||||||
|
export * from '../lib/validator';
|
||||||
|
|
||||||
export interface HydroRequest {
|
export interface HydroRequest {
|
||||||
method: string;
|
method: string;
|
||||||
@@ -556,6 +558,7 @@ ${ctx.response.status} ${endTime - startTime}ms ${ctx.response.length}`);
|
|||||||
|
|
||||||
global.Hydro.service.server = {
|
global.Hydro.service.server = {
|
||||||
...decorators,
|
...decorators,
|
||||||
|
Types,
|
||||||
app,
|
app,
|
||||||
httpServer,
|
httpServer,
|
||||||
wsServer,
|
wsServer,
|
||||||
|
|||||||
Reference in new issue
Block a user