@halooj/framework 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/api.ts ADDED
@@ -0,0 +1,322 @@
1
+ import { Context, Service } from 'cordis';
2
+ import Schema from 'schemastery';
3
+ import { param } from './decorators';
4
+ import { BadRequestError, MethodNotAllowedError, NotFoundError } from './error';
5
+ import { } from './interface';
6
+ import { ConnectionHandler, Handler } from './server';
7
+ import { Types } from './validator';
8
+
9
+ const BINARY = Symbol.for('hydro.api.response.binary');
10
+ const REDIRECT = Symbol.for('hydro.api.response.redirect');
11
+
12
+ type MaybePromise<T> = T | Promise<T>;
13
+ export type ApiType = 'Query' | 'Mutation' | 'Subscription';
14
+ export interface ApiCall<Type extends ApiType, Arg, Res, Progress = void> {
15
+ readonly type: Type;
16
+ readonly input: Schema<Arg>;
17
+ readonly func: (Type extends 'Subscription'
18
+ ? (context: any, args: Arg, emit: (payload: Res) => void) => (() => MaybePromise<void>)
19
+ : (context: any, args: Arg) => MaybePromise<Res | AsyncGenerator<Progress, Res, never>>);
20
+ readonly hooks: ApiCall<'Query', Arg, void>[];
21
+ }
22
+
23
+ export const _get = <Type extends ApiType>(type: Type) => <Arg, Res, Progress = void>(
24
+ schema: Schema<Arg>,
25
+ func: ApiCall<Type, Arg, Res, Progress>['func'],
26
+ hooks: ApiCall<'Query', Arg, void, void>[] = [],
27
+ ): ApiCall<Type, Arg, Res, Progress> => ({ input: schema, func, hooks, type } as const);
28
+
29
+ export const Query = _get('Query');
30
+ export const Mutation = _get('Mutation');
31
+ export const Subscription = _get('Subscription');
32
+
33
+ export class BinaryResponse {
34
+ [BINARY] = true;
35
+ constructor(public readonly data: Buffer, public filename: string) { }
36
+ static check(value: any): value is BinaryResponse {
37
+ return value && typeof value === 'object' && BINARY in value && value[BINARY] === true;
38
+ }
39
+ }
40
+
41
+ export class RedirectResponse {
42
+ [REDIRECT] = true;
43
+ constructor(public readonly url: string) { }
44
+ static check(value: any): value is RedirectResponse {
45
+ return value && typeof value === 'object' && REDIRECT in value && value[REDIRECT] === true;
46
+ }
47
+ }
48
+
49
+ /** @deprecated TODO */
50
+ export const NOP = Query(Schema.any(), () => { });
51
+
52
+ export const APIS = {
53
+ 'query.batch': Query(Schema.array(Schema.object({ op: Schema.string(), args: Schema.any() })), () => ({})),
54
+ 'mutation.batch': Mutation(Schema.array(Schema.object({ op: Schema.string(), args: Schema.any() })), () => ({})),
55
+ } as const;
56
+ export interface Apis {
57
+ builtin: {
58
+ 'query.batch': ApiCall<'Query', { op: string, args: any }[], { [key: string]: any }>;
59
+ 'mutation.batch': ApiCall<'Mutation', { op: string, args: any }[], { [key: string]: any }>;
60
+ };
61
+ test: typeof TestApis;
62
+ }
63
+ export type FlattenedApis = Apis[keyof Apis];
64
+
65
+ // Thanks to @ForkKILLET for the projection function
66
+ type ProjectionSchemaId = 1;
67
+ type MKeyOf<T> = T extends any ? keyof T : never;
68
+ type MId<T> = { [K in MKeyOf<T>]: T[K] } & {};
69
+ type ProjectionSchema<T> = T extends Array<infer U>
70
+ ? ProjectionSchema<U>
71
+ : { [K in keyof T]?: ProjectionSchemaId | ProjectionSchema<T[K]> } | Record<keyof any, ProjectionSchemaId | object>;
72
+ type AsKeys<T> = T extends Array<infer U extends string> ? Record<U, 1> : T;
73
+ type Projection<T, S> = S extends ProjectionSchemaId
74
+ ? T : T extends Array<infer U> ? Array<MId<Projection<U, S>>> : {
75
+ [K in keyof T & keyof S]: K extends keyof AsKeys<S> ? Projection<T[K], AsKeys<S>[K]> : never
76
+ };
77
+
78
+ export const projection = <T, S extends ProjectionSchema<T>>(input: T, schema: S, serializeCtx?: any): Projection<T, S> => {
79
+ if (typeof input !== 'object' || input === null) throw new Error('Input must be an object.');
80
+ type R = Projection<T, S>;
81
+ if ('serialize' in input && typeof (input as any).serialize === 'function') {
82
+ input = (input as any).serialize(serializeCtx);
83
+ }
84
+ if (Array.isArray(schema)) schema = Object.fromEntries(schema.map((s) => [s, 1])) as S;
85
+ if (Array.isArray(input)) {
86
+ return input.map((item) => projection(item, schema, serializeCtx)) as R;
87
+ }
88
+ const result = {} as R;
89
+ for (const key of Reflect.ownKeys(input as any)) {
90
+ const schemaIt = schema[key];
91
+ if (!schemaIt) continue;
92
+ if (schemaIt === 1 || !input[key]) result[key] = input[key];
93
+ else result[key] = projection(input[key], schemaIt, serializeCtx);
94
+ }
95
+ return result;
96
+ };
97
+
98
+ export interface ApiExecutionContext {
99
+ }
100
+
101
+ function handleArguments(args: any) {
102
+ try {
103
+ if (typeof args.args === 'string') {
104
+ args.args = JSON.parse(args.args);
105
+ }
106
+ if (typeof args.projection === 'string') {
107
+ args.projection = '{['.includes(args.projection[0])
108
+ ? JSON.parse(args.projection)
109
+ : args.projection.split(',').map((i) => i.trim()).filter((i) => i);
110
+ }
111
+ } catch (e) {
112
+ throw new BadRequestError('Invalid arguments');
113
+ }
114
+ }
115
+
116
+ export class ApiService extends Service {
117
+ constructor(ctx: Context) {
118
+ super(ctx, 'api');
119
+ }
120
+
121
+ provide(calls: Partial<FlattenedApis>, atNameSpace = '') {
122
+ this.ctx.effect(() => {
123
+ for (const key in calls) {
124
+ const target = `${atNameSpace ? `${atNameSpace}.` : ''}${key}`;
125
+ if (APIS[target]) console.warn(`API ${target} already exists, will be overridden`);
126
+ APIS[target] = calls[key];
127
+ }
128
+ return () => {
129
+ for (const key in calls) {
130
+ delete APIS[`${atNameSpace ? `${atNameSpace}.` : ''}${key}`];
131
+ }
132
+ };
133
+ });
134
+ }
135
+
136
+ serialize() {
137
+ const result = {};
138
+ for (const key in APIS) {
139
+ result[key] = {
140
+ type: APIS[key].type,
141
+ input: APIS[key].input.toJSON(),
142
+ };
143
+ }
144
+ return result;
145
+ }
146
+
147
+ async execute(
148
+ context: ApiExecutionContext, callOrName: ApiCall<ApiType, any, any> | string,
149
+ rawArgs: any, emitHook?: any, project?: any, sendPayload?: (payload: any) => void,
150
+ ) {
151
+ const call = typeof callOrName === 'string' ? APIS[callOrName] : callOrName;
152
+ if (!call) throw new NotFoundError(callOrName);
153
+ const { input, func, hooks } = call;
154
+ // eslint-disable-next-line no-await-in-loop
155
+ for (const hook of hooks) await this.execute(context, hook, rawArgs);
156
+
157
+ let args: any;
158
+ try {
159
+ args = input ? input(rawArgs as any) : rawArgs;
160
+ } catch (e) {
161
+ throw new BadRequestError(e.message);
162
+ }
163
+ if (typeof callOrName === 'string') {
164
+ await emitHook?.('api/before', args);
165
+ await emitHook?.(`api/before/${callOrName}`, args);
166
+ }
167
+ let result = await func(context, args as any, sendPayload);
168
+ if (result && typeof result === 'object' && 'next' in result) {
169
+ const it = result as AsyncGenerator<any, any, never>;
170
+ while (true) {
171
+ const value = await it.next(); // eslint-disable-line no-await-in-loop
172
+ if (value.done) {
173
+ result = value;
174
+ break;
175
+ } else {
176
+ sendPayload?.(value.value);
177
+ }
178
+ }
179
+ }
180
+ return (project && typeof result === 'object' && result !== null) ? projection(result, project, context) : result;
181
+ }
182
+ }
183
+
184
+ declare module 'cordis' {
185
+ interface Context {
186
+ api: ApiService;
187
+ }
188
+ }
189
+
190
+ export class ApiHandler extends Handler {
191
+ @param('op', Types.String)
192
+ async all({ }, op: string) {
193
+ if (!['get', 'post'].includes(this.request.method.toLowerCase())) {
194
+ throw new MethodNotAllowedError(this.request.method);
195
+ }
196
+ if (!APIS[op]) throw new BadRequestError(`Invalid API operation: ${op}`);
197
+ if (APIS[op].type === 'Subscription') {
198
+ throw new BadRequestError('Subscription operation cannot be called in HTTP handler');
199
+ }
200
+ if (APIS[op].type === 'Mutation' && this.request.method.toLowerCase() === 'get') {
201
+ throw new BadRequestError('Mutation operation cannot be called with GET method');
202
+ }
203
+ handleArguments(this.args);
204
+ // @ts-ignore
205
+ await this.ctx.parallel('handler/api/before', this);
206
+ // @ts-ignore
207
+ await this.ctx.parallel(`handler/api/before/${op}`, this);
208
+ const result = await this.ctx.api.execute(
209
+ this, op, { domainId: this.args.domainId, ...this.args, ...(this.args.args || {}) },
210
+ (m, args) => (this.ctx.parallel as any)(m, args), this.args.projection,
211
+ );
212
+ if (BinaryResponse.check(result)) {
213
+ this.binary(result.data, result.filename);
214
+ } else if (RedirectResponse.check(result)) {
215
+ this.response.redirect = result.url;
216
+ } else {
217
+ this.response.body = result;
218
+ }
219
+ }
220
+ }
221
+
222
+ export class ApiConnectionHandler extends ConnectionHandler {
223
+ dispose: () => Promise<void> | void;
224
+ isRpc: boolean;
225
+
226
+ @param('op', Types.String)
227
+ async prepare({ }, op: string) {
228
+ if (op === 'rpc') {
229
+ this.isRpc = true;
230
+ return;
231
+ }
232
+ if (!APIS[op]) throw new BadRequestError(`Invalid API operation: ${op}`);
233
+ if (APIS[op].type !== 'Subscription') {
234
+ throw new BadRequestError('Only subscription operations are supported');
235
+ }
236
+ handleArguments(this.args);
237
+ // @ts-ignore
238
+ await this.ctx.parallel('handler/api/before', this);
239
+ // @ts-ignore
240
+ await this.ctx.parallel(`handler/api/before/${op}`, this);
241
+ this.dispose = await this.ctx.api.execute(
242
+ this, op, { domainId: this.args.domainId, ...this.args, ...(this.args.args || {}) },
243
+ (m, args) => (this.ctx.parallel as any)(m, args), this.args.projection, (p) => this.send(p),
244
+ );
245
+ }
246
+
247
+ async message(message) {
248
+ if (!this.isRpc) throw new BadRequestError('Only RPC operations are supported');
249
+ if (typeof message === 'string') {
250
+ try {
251
+ message = JSON.parse(message);
252
+ } catch (e) {
253
+ throw new BadRequestError('Invalid message');
254
+ }
255
+ }
256
+ if (!APIS[message.op]) throw new BadRequestError(`Invalid API operation: ${message.op}`);
257
+ if (APIS[message.op].type !== 'Subscription') {
258
+ throw new BadRequestError('Only subscription operations are supported');
259
+ }
260
+ handleArguments(message);
261
+ const result = await this.ctx.api.execute(
262
+ this, message.op, message.args, (m, args) => (this.ctx.parallel as any)(m, args), message.projection,
263
+ );
264
+ this.send(result);
265
+ }
266
+
267
+ async cleanup() {
268
+ await this.dispose?.();
269
+ }
270
+ }
271
+
272
+ export async function applyApiHandler(ctx: Context, name: string, path: string) {
273
+ ctx.plugin(ApiService);
274
+ await ctx.inject(['server', 'api'], ({ Route, Connection }) => {
275
+ Route(name, path, ApiHandler);
276
+ Connection(`${name}_conn`, `${path}/conn`, ApiConnectionHandler);
277
+ });
278
+ }
279
+
280
+ const TestApis = {
281
+ 'test.query': Query(Schema.object({
282
+ name: Schema.string(),
283
+ }), (c, { name }) => ({
284
+ ok: true,
285
+ name,
286
+ })),
287
+ 'test.mutation': Mutation(Schema.object({
288
+ name: Schema.string().required(),
289
+ }), (c, { name }) => ({
290
+ ok: true,
291
+ name,
292
+ })),
293
+ 'test.mutation_progress': Mutation(Schema.object({
294
+ count: Schema.number().step(1).min(1).default(10),
295
+ }), async function* (c, { count }) {
296
+ for (let i = 1; i <= count; i++) {
297
+ yield { progress: i };
298
+ }
299
+ return {
300
+ ok: true,
301
+ count,
302
+ };
303
+ }),
304
+ 'test.subscription': Subscription(Schema.object({
305
+ initial: Schema.number().step(1).min(0).default(0),
306
+ }), (c, { initial }, send) => {
307
+ let count = initial;
308
+ const interval = setInterval(() => {
309
+ count++;
310
+ send({ count });
311
+ }, 1000);
312
+ return () => {
313
+ clearInterval(interval);
314
+ };
315
+ }),
316
+ } as const;
317
+
318
+ export function applyTestApis(ctx: Context) {
319
+ ctx.inject(['api'], ({ api }) => {
320
+ api.provide(TestApis);
321
+ });
322
+ }
package/base.ts ADDED
@@ -0,0 +1,144 @@
1
+ import { PassThrough } from 'stream';
2
+ import type { Next } from 'koa';
3
+ import {
4
+ HydroRequest, HydroResponse, KoaContext, serializer,
5
+ } from '@halooj/framework';
6
+ import { errorMessage } from '@halooj/utils/lib/utils';
7
+ import { SystemError, UserFacingError } from './error';
8
+
9
+ const pick = <T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> => {
10
+ const result: Partial<Pick<T, K>> = {};
11
+ for (const key of keys) result[key] = obj[key];
12
+ return result as Pick<T, K>;
13
+ };
14
+
15
+ export default (logger, xff, xhost) => async (ctx: KoaContext, next: Next) => {
16
+ // Base Layer
17
+ const request: HydroRequest = {
18
+ method: ctx.request.method.toLowerCase(),
19
+ host: ctx.request.headers[xhost?.toLowerCase() || ''] as string || ctx.request.host,
20
+ ip: (ctx.request.headers[xff?.toLowerCase() || ''] as string || ctx.request.ip).split(',')[0].trim(),
21
+ ...pick(ctx, ['cookies', 'query', 'path', 'originalPath', 'querystring']),
22
+ ...pick(ctx.request, ['headers', 'body', 'hostname']),
23
+ files: ctx.request.files as any,
24
+ referer: ctx.request.headers.referer || '',
25
+ json: (ctx.request.headers.accept || '').includes('application/json'),
26
+ websocket: ctx.request.headers.upgrade === 'websocket',
27
+ get params() {
28
+ return ctx.params;
29
+ },
30
+ };
31
+ const response: HydroResponse = {
32
+ body: {},
33
+ type: '',
34
+ status: null,
35
+ template: null,
36
+ redirect: null,
37
+ attachment: (name, streamOrBuffer) => {
38
+ if (name) ctx.attachment(name);
39
+ if (streamOrBuffer instanceof Buffer || streamOrBuffer instanceof PassThrough) {
40
+ response.body = null;
41
+ ctx.body = streamOrBuffer;
42
+ } else {
43
+ response.body = null;
44
+ ctx.body = streamOrBuffer.pipe(new PassThrough());
45
+ }
46
+ },
47
+ addHeader: (name: string, value: string) => ctx.set(name, value),
48
+ disposition: null,
49
+ };
50
+ const args = {
51
+ ...ctx.params, ...ctx.query, ...ctx.request.body, __start: Date.now(),
52
+ };
53
+ ctx.HydroContext = { request, response, args } as any;
54
+ try {
55
+ await next();
56
+ if (request.websocket) return;
57
+ const handler = ctx.handler;
58
+ if (!handler) {
59
+ logger.error('No handler found on request', request);
60
+ ctx.response.status = 500;
61
+ return;
62
+ }
63
+ const { UiContext, user } = ctx.HydroContext;
64
+ if (response.redirect) {
65
+ response.body ||= {};
66
+ response.body.url = response.redirect;
67
+ }
68
+ if (!response.type) {
69
+ if (response.pjax && args.pjax) {
70
+ const pjax = typeof response.pjax === 'string' ? [[response.pjax, {}]] : response.pjax;
71
+ response.body = {
72
+ fragments: (await Promise.all(
73
+ pjax.map(async ([template, extra]) => handler.renderHTML(template, { ...response.body, ...extra })),
74
+ )).map((i) => ({ html: i })),
75
+ };
76
+ response.type = 'application/json';
77
+ } else if (
78
+ request.json || response.redirect
79
+ || request.query.noTemplate || !response.template // no template, send raw data
80
+ ) {
81
+ // Send raw data
82
+ try {
83
+ if (request.headers['x-hydro-inject']) {
84
+ const inject = request.headers['x-hydro-inject'].toString().toLowerCase().split(',').map((i) => i.trim());
85
+ if (inject.includes('pagename')) {
86
+ ctx.set('x-hydro-page', ctx._matchedRouteName || '');
87
+ ctx.set('x-hydro-template', response.template || '');
88
+ }
89
+ if (response.body !== null && typeof response.body === 'object') {
90
+ if (inject.includes('uicontext')) response.body.UiContext = UiContext;
91
+ if (inject.includes('usercontext')) response.body.UserContext = user;
92
+ if (inject.includes('routemap')) response.body.routeMap = handler.ctx.server.routeMap;
93
+ }
94
+ }
95
+ response.body = JSON.stringify(response.body, serializer(false, handler));
96
+ } catch (e) {
97
+ response.body = new SystemError('Serialize failure', e.message);
98
+ }
99
+ response.type = 'application/json';
100
+ } else if (response.template) {
101
+ response.body = await handler.renderHTML(response.template, response.body || {});
102
+ response.type = 'text/html';
103
+ }
104
+ }
105
+ if (response.disposition) ctx.set('Content-Disposition', response.disposition);
106
+ if (response.etag) {
107
+ ctx.set('ETag', response.etag);
108
+ ctx.set('Cache-Control', 'public');
109
+ }
110
+ } catch (err) {
111
+ const error = errorMessage(err);
112
+ response.status = error instanceof UserFacingError ? error.code : 500;
113
+ if (request.json) response.body = { error };
114
+ else {
115
+ try {
116
+ response.body = await ctx.handler.renderHTML(
117
+ error instanceof UserFacingError ? 'error.html' : 'bsod.html',
118
+ { UserFacingError, error },
119
+ );
120
+ response.type = 'text/html';
121
+ } catch (e) {
122
+ logger.error(e);
123
+ // this.response.body.error = {};
124
+ }
125
+ }
126
+ } finally {
127
+ if (!request.websocket) {
128
+ if (response.etag && request.headers['if-none-match'] === response.etag) {
129
+ ctx.response.status = 304;
130
+ } else if (response.redirect && !request.json) {
131
+ ctx.response.type = 'application/octet-stream';
132
+ ctx.response.status = 302;
133
+ ctx.redirect(response.redirect);
134
+ } else if (response.body) {
135
+ ctx.body = response.body instanceof Blob ? Buffer.from(await response.body.arrayBuffer()) : response.body;
136
+ ctx.response.status = response.status || 200;
137
+ ctx.response.type = response.type
138
+ || (request.json
139
+ ? 'application/json'
140
+ : ctx.response.type);
141
+ }
142
+ }
143
+ }
144
+ };
package/decorators.ts ADDED
@@ -0,0 +1,123 @@
1
+ import Schema from 'schemastery';
2
+ import { ValidationError } from './error';
3
+ import type { Handler } from './server';
4
+ import { Converter, Type, Validator } from './validator';
5
+
6
+ type MethodDecorator = (target: any, funcName: string, obj: any) => any;
7
+ type ClassDecorator = <T extends new (...args: any[]) => any>(Class: T) => T extends new (...args: infer R) => infer S
8
+ ? new (...args: R) => S : never;
9
+ export interface ParamOption<T> {
10
+ name: string;
11
+ source: 'all' | 'get' | 'post' | 'route';
12
+ isOptional?: boolean | 'convert';
13
+ convert?: Converter<T>;
14
+ validate?: Validator;
15
+ }
16
+
17
+ const kSchema = Symbol.for('schemastery');
18
+ const mergeValidator = (L: Validator, R: Validator) => (v: any) => L(v) && R(v);
19
+
20
+ function isSchema(v: any): v is Schema {
21
+ return v && typeof v === 'function' && kSchema in v;
22
+ }
23
+
24
+ function _buildParam(name: string, source: 'get' | 'post' | 'all' | 'route', ...args: Array<Type<any> | boolean | Validator | Converter<any>>) {
25
+ let cursor = 0;
26
+ const v: ParamOption<any> = { name, source };
27
+ let isValidate = true;
28
+ while (cursor < args.length) {
29
+ const current = args[cursor];
30
+ if (isSchema(current)) {
31
+ v.validate = (val) => {
32
+ try {
33
+ current(val);
34
+ return true;
35
+ } catch (e) {
36
+ return false;
37
+ }
38
+ };
39
+ v.convert = (val) => current(val);
40
+ cursor++;
41
+ continue;
42
+ }
43
+ if (current instanceof Array) {
44
+ const type = current;
45
+ if (type[0]) v.convert = type[0];
46
+ if (type[1]) v.validate = type[1];
47
+ if (type[2]) v.isOptional = type[2];
48
+ } else if (typeof current === 'boolean') v.isOptional = current;
49
+ else if (isValidate) {
50
+ if (current !== null) v.validate = v.validate ? mergeValidator(v.validate, current) : current;
51
+ isValidate = false;
52
+ } else v.convert = current;
53
+ cursor++;
54
+ }
55
+ return v;
56
+ }
57
+
58
+ function _descriptor(v: ParamOption<any>) {
59
+ return function desc(this: Handler, target: any, funcName: string, obj: any) {
60
+ target.__param ||= {};
61
+ target.__param[target.constructor.name] ||= {};
62
+ if (!target.__param[target.constructor.name][funcName]) {
63
+ const originalMethod = obj.value;
64
+ const val = originalMethod.toString();
65
+ const firstArg = val.split(')')[0]?.split(',')[0]?.split('(')[1]?.trim() || '';
66
+ const domainIdStyle = firstArg.toLowerCase().startsWith('domainid');
67
+ target.__param[target.constructor.name][funcName] = [];
68
+ obj.value = function validate(this: Handler, rawArgs: any, ...extra: any[]) {
69
+ if (typeof rawArgs !== 'object' || extra.length) return originalMethod.call(this, rawArgs, ...extra);
70
+ const c = [];
71
+ const arglist: ParamOption<any>[] = target.__param[target.constructor.name][funcName];
72
+ if (typeof rawArgs.domainId !== 'string' || !rawArgs.domainId) throw new ValidationError('domainId');
73
+ for (const item of arglist) {
74
+ const src = item.source === 'all'
75
+ ? rawArgs
76
+ : item.source === 'get'
77
+ ? this.request.query
78
+ : item.source === 'route'
79
+ ? { ...this.request.params, domainId: this.args.domainId }
80
+ : this.request.body;
81
+ const value = src[item.name];
82
+ if (!item.isOptional || value) {
83
+ if (value === undefined || value === null || value === '') throw new ValidationError(item.name);
84
+ if (item.validate && !item.validate(value)) throw new ValidationError(item.name);
85
+ if (item.convert) c.push(item.convert(value));
86
+ else c.push(value);
87
+ } else if (item.isOptional === 'convert') {
88
+ c.push(item.convert ? item.convert(value) : value);
89
+ } else c.push(undefined);
90
+ }
91
+ return domainIdStyle ? originalMethod.call(this, rawArgs.domainId, ...c) : originalMethod.call(this, rawArgs, ...c);
92
+ };
93
+ }
94
+ target.__param[target.constructor.name][funcName].unshift(v);
95
+ return obj;
96
+ };
97
+ }
98
+
99
+ type DescriptorBuilder =
100
+ ((name: string, type: Type<any>) => MethodDecorator)
101
+ & ((name: string, type: Type<any>, validate: null, convert: Converter<any>) => MethodDecorator)
102
+ & ((name: string, type: Type<any>, validate?: Validator, convert?: Converter<any>) => MethodDecorator)
103
+ & ((name: string, type?: Type<any>, isOptional?: boolean, validate?: Validator, convert?: Converter<any>) => MethodDecorator)
104
+ & ((name: string, ...args: Array<Type<any> | boolean | Validator | Converter<any>>) => MethodDecorator);
105
+
106
+ export const get: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'get', ...args));
107
+ export const query: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'get', ...args));
108
+ export const post: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'post', ...args));
109
+ export const route: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'route', ...args));
110
+ export const param: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'all', ...args));
111
+
112
+ export const subscribe: (name: string) => MethodDecorator & ClassDecorator = (name) => (target, funcName?, obj?) => {
113
+ if (funcName) {
114
+ target.__subscribe ||= [];
115
+ target.__subscribe.push({ name, target: obj.value });
116
+ return obj;
117
+ }
118
+ return (...args) => {
119
+ const c = new target(...args); // eslint-disable-line new-cap
120
+ c.__subscribe = [{ name, target: c.send }];
121
+ return c;
122
+ };
123
+ };
package/error.ts ADDED
@@ -0,0 +1,68 @@
1
+ interface IHydroError {
2
+ new(...args: any[]): HydroError;
3
+ }
4
+
5
+ export class HydroError extends Error {
6
+ params: any[];
7
+ code: number;
8
+
9
+ constructor(...params: any[]) {
10
+ super();
11
+ this.params = params;
12
+ }
13
+
14
+ msg() {
15
+ return 'HydroError';
16
+ }
17
+
18
+ get message() {
19
+ return this.msg();
20
+ }
21
+ }
22
+
23
+ const Err = (name: string, Class: IHydroError, ...info: Array<(() => string) | string | number>) => {
24
+ let msg: () => string;
25
+ let code: number;
26
+ for (const item of info) {
27
+ if (typeof item === 'number') {
28
+ code = item;
29
+ } else if (typeof item === 'string') {
30
+ msg = function () { return item; };
31
+ } else if (typeof item === 'function') {
32
+ msg = item;
33
+ }
34
+ }
35
+ // eslint-disable-next-line ts/no-shadow
36
+ return class HydroError extends Class {
37
+ name = name;
38
+ constructor(...args: any[]) {
39
+ super(...args);
40
+ if (msg) this.msg = msg;
41
+ if (code) this.code = code;
42
+ }
43
+ };
44
+ };
45
+
46
+ export const UserFacingError = Err('UserFacingError', HydroError, 'UserFacingError', 400);
47
+ export const SystemError = Err('SystemError', HydroError, 'SystemError', 500);
48
+
49
+ export const BadRequestError = Err('BadRequestError', UserFacingError, 'BadRequestError', 400);
50
+ export const ForbiddenError = Err('ForbiddenError', UserFacingError, 'ForbiddenError', 403);
51
+ export const NotFoundError = Err('NotFoundError', UserFacingError, 'NotFoundError', 404);
52
+ export const MethodNotAllowedError = Err('MethodNotAllowedError', UserFacingError, 'MethodNotAllowedError', 405);
53
+
54
+ export const ValidationError = Err('ValidationError', ForbiddenError, function (this: HydroError) {
55
+ if (this.params.length === 3) {
56
+ return this.params[1]
57
+ ? 'Field {0} or {1} validation failed. ({2})'
58
+ : 'Field {0} validation failed. ({2})';
59
+ }
60
+ return this.params[1]
61
+ ? 'Field {0} or {1} validation failed.'
62
+ : 'Field {0} validation failed.';
63
+ });
64
+ export const CsrfTokenError = Err('CsrfTokenError', ForbiddenError, 'CsrfTokenError');
65
+ export const InvalidOperationError = Err('InvalidOperationError', MethodNotAllowedError);
66
+ export const FileTooLargeError = Err('FileTooLargeError', ValidationError, 'The uploaded file is too long.');
67
+
68
+ export const CreateError = Err;
package/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './api';
2
+ export * from './decorators';
3
+ export * from './error';
4
+ export * from './interface';
5
+ export * from './router';
6
+ export { default as serializer } from './serializer';
7
+ export * from './server';
8
+ export * from './validator';