@kevisual/router 0.0.20 → 0.0.21
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/dist/router-browser.d.ts +2 -4
- package/dist/router-browser.js +4 -4
- package/dist/router-simple.d.ts +54 -2
- package/dist/router-simple.js +193 -2
- package/dist/router.d.ts +7 -4
- package/dist/router.js +7 -4
- package/package.json +1 -1
- package/src/app.ts +5 -0
- package/src/router-simple.ts +194 -3
- package/src/validator/rule.ts +4 -6
package/dist/router-browser.d.ts
CHANGED
|
@@ -12,8 +12,8 @@ type BaseRule = {
|
|
|
12
12
|
};
|
|
13
13
|
type RuleString = {
|
|
14
14
|
type: 'string';
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
min?: number;
|
|
16
|
+
max?: number;
|
|
17
17
|
regex?: string;
|
|
18
18
|
} & BaseRule;
|
|
19
19
|
type RuleNumber = {
|
|
@@ -27,8 +27,6 @@ type RuleBoolean = {
|
|
|
27
27
|
type RuleArray = {
|
|
28
28
|
type: 'array';
|
|
29
29
|
items: Rule;
|
|
30
|
-
minItems?: number;
|
|
31
|
-
maxItems?: number;
|
|
32
30
|
} & BaseRule;
|
|
33
31
|
type RuleObject = {
|
|
34
32
|
type: 'object';
|
package/dist/router-browser.js
CHANGED
|
@@ -4499,10 +4499,10 @@ const schemaFormRule = (rule) => {
|
|
|
4499
4499
|
switch (rule.type) {
|
|
4500
4500
|
case 'string':
|
|
4501
4501
|
let stringSchema = z.string();
|
|
4502
|
-
if (rule.
|
|
4503
|
-
stringSchema = stringSchema.min(rule.
|
|
4504
|
-
if (rule.
|
|
4505
|
-
stringSchema = stringSchema.max(rule.
|
|
4502
|
+
if (rule.min)
|
|
4503
|
+
stringSchema = stringSchema.min(rule.min, `String must be at least ${rule.min} characters long.`);
|
|
4504
|
+
if (rule.max)
|
|
4505
|
+
stringSchema = stringSchema.max(rule.max, `String must not exceed ${rule.max} characters.`);
|
|
4506
4506
|
if (rule.regex)
|
|
4507
4507
|
stringSchema = stringSchema.regex(new RegExp(rule.regex), 'Invalid format');
|
|
4508
4508
|
return stringSchema;
|
package/dist/router-simple.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import * as querystring from 'querystring';
|
|
2
2
|
import { Key } from 'path-to-regexp';
|
|
3
|
-
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
+
import { IncomingMessage, ServerResponse, Server } from 'node:http';
|
|
4
|
+
import { ListenOptions } from 'node:net';
|
|
4
5
|
|
|
5
6
|
type Req = IncomingMessage & {
|
|
6
7
|
params?: Record<string, string>;
|
|
7
8
|
};
|
|
9
|
+
type SimpleObject = {
|
|
10
|
+
[key: string]: any;
|
|
11
|
+
};
|
|
8
12
|
interface Route {
|
|
9
13
|
method: string;
|
|
10
14
|
regexp: RegExp;
|
|
@@ -28,7 +32,10 @@ declare class SimpleRouter {
|
|
|
28
32
|
use(method: string, route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>): this;
|
|
29
33
|
get(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>): this;
|
|
30
34
|
post(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>): this;
|
|
35
|
+
sse(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>): this;
|
|
31
36
|
all(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>): this;
|
|
37
|
+
getJson(v: string | number | boolean | SimpleObject): any;
|
|
38
|
+
isSse(req: Req): boolean;
|
|
32
39
|
/**
|
|
33
40
|
* 解析 req 和 res 请求
|
|
34
41
|
* @param req
|
|
@@ -36,6 +43,51 @@ declare class SimpleRouter {
|
|
|
36
43
|
* @returns
|
|
37
44
|
*/
|
|
38
45
|
parse(req: Req, res: ServerResponse): Promise<void> | "is_exclude" | "not_found";
|
|
46
|
+
/**
|
|
47
|
+
* 创建一个新的 HttpChain 实例
|
|
48
|
+
* @param req
|
|
49
|
+
* @param res
|
|
50
|
+
* @returns
|
|
51
|
+
*/
|
|
52
|
+
chain(req?: Req, res?: ServerResponse): HttpChain;
|
|
53
|
+
static Chain(opts?: HttpChainOpts): HttpChain;
|
|
54
|
+
}
|
|
55
|
+
type HttpChainOpts = {
|
|
56
|
+
req?: Req;
|
|
57
|
+
res?: ServerResponse;
|
|
58
|
+
simpleRouter?: SimpleRouter;
|
|
59
|
+
};
|
|
60
|
+
declare class HttpChain {
|
|
61
|
+
req: Req;
|
|
62
|
+
res: ServerResponse;
|
|
63
|
+
simpleRouter: SimpleRouter;
|
|
64
|
+
server: Server;
|
|
65
|
+
hasSetHeader: boolean;
|
|
66
|
+
isSseSet: boolean;
|
|
67
|
+
constructor(opts?: HttpChainOpts);
|
|
68
|
+
setReq(req: Req): this;
|
|
69
|
+
setRes(res: ServerResponse): this;
|
|
70
|
+
setRouter(router: SimpleRouter): this;
|
|
71
|
+
setServer(server: Server): this;
|
|
72
|
+
/**
|
|
73
|
+
* 兼容 express 的一点功能
|
|
74
|
+
* @param status
|
|
75
|
+
* @returns
|
|
76
|
+
*/
|
|
77
|
+
status(status: number): this;
|
|
78
|
+
writeHead(status: number): this;
|
|
79
|
+
json(data: SimpleObject): this;
|
|
80
|
+
/**
|
|
81
|
+
* 兼容 express 的一点功能
|
|
82
|
+
* @param data
|
|
83
|
+
* @returns
|
|
84
|
+
*/
|
|
85
|
+
end(data: SimpleObject | string): this;
|
|
86
|
+
listen(opts: ListenOptions, callback?: () => void): this;
|
|
87
|
+
parse(): () => void;
|
|
88
|
+
getString(value: string | SimpleObject): string;
|
|
89
|
+
sse(value: string | SimpleObject): this;
|
|
90
|
+
close(): this;
|
|
39
91
|
}
|
|
40
92
|
|
|
41
|
-
export { SimpleRouter };
|
|
93
|
+
export { HttpChain, SimpleRouter };
|
package/dist/router-simple.js
CHANGED
|
@@ -511,11 +511,36 @@ class SimpleRouter {
|
|
|
511
511
|
post(route, ...fns) {
|
|
512
512
|
return this.use('post', route, ...fns);
|
|
513
513
|
}
|
|
514
|
+
sse(route, ...fns) {
|
|
515
|
+
return this.use('sse', route, ...fns);
|
|
516
|
+
}
|
|
514
517
|
all(route, ...fns) {
|
|
515
518
|
this.use('post', route, ...fns);
|
|
516
519
|
this.use('get', route, ...fns);
|
|
520
|
+
this.use('sse', route, ...fns);
|
|
517
521
|
return this;
|
|
518
522
|
}
|
|
523
|
+
getJson(v) {
|
|
524
|
+
if (typeof v === 'object') {
|
|
525
|
+
return v;
|
|
526
|
+
}
|
|
527
|
+
try {
|
|
528
|
+
return JSON.parse(v);
|
|
529
|
+
}
|
|
530
|
+
catch (e) {
|
|
531
|
+
return {};
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
isSse(req) {
|
|
535
|
+
const { headers } = req;
|
|
536
|
+
if (headers['accept'] && headers['accept'].includes('text/event-stream')) {
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
if (headers['content-type'] && headers['content-type'].includes('text/event-stream')) {
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
519
544
|
/**
|
|
520
545
|
* 解析 req 和 res 请求
|
|
521
546
|
* @param req
|
|
@@ -524,10 +549,13 @@ class SimpleRouter {
|
|
|
524
549
|
*/
|
|
525
550
|
parse(req, res) {
|
|
526
551
|
const { pathname } = new URL(req.url, 'http://localhost');
|
|
527
|
-
|
|
552
|
+
let method = req.method.toLowerCase();
|
|
528
553
|
if (this.exclude.includes(pathname)) {
|
|
529
554
|
return 'is_exclude';
|
|
530
555
|
}
|
|
556
|
+
const isSse = this.isSse(req);
|
|
557
|
+
if (isSse)
|
|
558
|
+
method = 'sse';
|
|
531
559
|
const route = this.routes.find((route) => {
|
|
532
560
|
const matchResult = route.regexp.exec(pathname);
|
|
533
561
|
if (matchResult && route.method === method) {
|
|
@@ -545,6 +573,169 @@ class SimpleRouter {
|
|
|
545
573
|
}
|
|
546
574
|
return 'not_found';
|
|
547
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* 创建一个新的 HttpChain 实例
|
|
578
|
+
* @param req
|
|
579
|
+
* @param res
|
|
580
|
+
* @returns
|
|
581
|
+
*/
|
|
582
|
+
chain(req, res) {
|
|
583
|
+
const chain = new HttpChain({ req, res, simpleRouter: this });
|
|
584
|
+
return chain;
|
|
585
|
+
}
|
|
586
|
+
static Chain(opts) {
|
|
587
|
+
return new HttpChain(opts);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
class HttpChain {
|
|
591
|
+
req;
|
|
592
|
+
res;
|
|
593
|
+
simpleRouter;
|
|
594
|
+
server;
|
|
595
|
+
hasSetHeader = false;
|
|
596
|
+
isSseSet = false;
|
|
597
|
+
constructor(opts) {
|
|
598
|
+
this.req = opts?.req;
|
|
599
|
+
this.res = opts?.res;
|
|
600
|
+
this.simpleRouter = opts?.simpleRouter;
|
|
601
|
+
}
|
|
602
|
+
setReq(req) {
|
|
603
|
+
this.req = req;
|
|
604
|
+
return this;
|
|
605
|
+
}
|
|
606
|
+
setRes(res) {
|
|
607
|
+
this.res = res;
|
|
608
|
+
return this;
|
|
609
|
+
}
|
|
610
|
+
setRouter(router) {
|
|
611
|
+
this.simpleRouter = router;
|
|
612
|
+
return this;
|
|
613
|
+
}
|
|
614
|
+
setServer(server) {
|
|
615
|
+
this.server = server;
|
|
616
|
+
return this;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* 兼容 express 的一点功能
|
|
620
|
+
* @param status
|
|
621
|
+
* @returns
|
|
622
|
+
*/
|
|
623
|
+
status(status) {
|
|
624
|
+
if (!this.res)
|
|
625
|
+
return this;
|
|
626
|
+
if (this.hasSetHeader) {
|
|
627
|
+
return this;
|
|
628
|
+
}
|
|
629
|
+
this.hasSetHeader = true;
|
|
630
|
+
this.res.writeHead(status);
|
|
631
|
+
return this;
|
|
632
|
+
}
|
|
633
|
+
writeHead(status) {
|
|
634
|
+
if (!this.res)
|
|
635
|
+
return this;
|
|
636
|
+
if (this.hasSetHeader) {
|
|
637
|
+
return this;
|
|
638
|
+
}
|
|
639
|
+
this.hasSetHeader = true;
|
|
640
|
+
this.res.writeHead(status);
|
|
641
|
+
return this;
|
|
642
|
+
}
|
|
643
|
+
json(data) {
|
|
644
|
+
if (!this.res)
|
|
645
|
+
return this;
|
|
646
|
+
this.res.end(JSON.stringify(data));
|
|
647
|
+
return this;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* 兼容 express 的一点功能
|
|
651
|
+
* @param data
|
|
652
|
+
* @returns
|
|
653
|
+
*/
|
|
654
|
+
end(data) {
|
|
655
|
+
if (!this.res)
|
|
656
|
+
return this;
|
|
657
|
+
if (typeof data === 'object') {
|
|
658
|
+
this.res.end(JSON.stringify(data));
|
|
659
|
+
}
|
|
660
|
+
else if (typeof data === 'string') {
|
|
661
|
+
this.res.end(data);
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
this.res.end('nothing');
|
|
665
|
+
}
|
|
666
|
+
return this;
|
|
667
|
+
}
|
|
668
|
+
listen(opts, callback) {
|
|
669
|
+
this.server.listen(opts, callback);
|
|
670
|
+
return this;
|
|
671
|
+
}
|
|
672
|
+
parse() {
|
|
673
|
+
if (!this.server || !this.simpleRouter) {
|
|
674
|
+
throw new Error('Server and SimpleRouter must be set before calling parse');
|
|
675
|
+
}
|
|
676
|
+
const that = this;
|
|
677
|
+
const listener = (req, res) => {
|
|
678
|
+
try {
|
|
679
|
+
that.simpleRouter.parse(req, res);
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
console.error('Error parsing request:', error);
|
|
683
|
+
if (!res.headersSent) {
|
|
684
|
+
res.writeHead(500);
|
|
685
|
+
res.end(JSON.stringify({ code: 500, message: 'Internal Server Error' }));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
this.server.on('request', listener);
|
|
690
|
+
return () => {
|
|
691
|
+
that.server.removeListener('request', listener);
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
getString(value) {
|
|
695
|
+
if (typeof value === 'string') {
|
|
696
|
+
return value;
|
|
697
|
+
}
|
|
698
|
+
return JSON.stringify(value);
|
|
699
|
+
}
|
|
700
|
+
sse(value) {
|
|
701
|
+
const res = this.res;
|
|
702
|
+
const req = this.req;
|
|
703
|
+
if (!res || !req)
|
|
704
|
+
return;
|
|
705
|
+
const data = this.getString(value);
|
|
706
|
+
if (this.isSseSet) {
|
|
707
|
+
res.write(`data: ${data}\n\n`);
|
|
708
|
+
return this;
|
|
709
|
+
}
|
|
710
|
+
const headersMap = new Map([
|
|
711
|
+
['Content-Type', 'text/event-stream'],
|
|
712
|
+
['Cache-Control', 'no-cache'],
|
|
713
|
+
['Connection', 'keep-alive'],
|
|
714
|
+
]);
|
|
715
|
+
this.isSseSet = true;
|
|
716
|
+
let intervalId;
|
|
717
|
+
if (!this.hasSetHeader) {
|
|
718
|
+
this.hasSetHeader = true;
|
|
719
|
+
res.setHeaders(headersMap);
|
|
720
|
+
// 每隔 2 秒发送一个空行,保持连接
|
|
721
|
+
setInterval(() => {
|
|
722
|
+
res.write('\n'); // 发送一个空行,保持连接
|
|
723
|
+
}, 3000);
|
|
724
|
+
// 客户端断开连接时清理
|
|
725
|
+
req.on('close', () => {
|
|
726
|
+
clearInterval(intervalId);
|
|
727
|
+
res.end();
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
this.res.write(`data: ${data}\n\n`);
|
|
731
|
+
return this;
|
|
732
|
+
}
|
|
733
|
+
close() {
|
|
734
|
+
if (this.req?.destroy) {
|
|
735
|
+
this.req.destroy();
|
|
736
|
+
}
|
|
737
|
+
return this;
|
|
738
|
+
}
|
|
548
739
|
}
|
|
549
740
|
|
|
550
|
-
export { SimpleRouter };
|
|
741
|
+
export { HttpChain, SimpleRouter };
|
package/dist/router.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import https from 'node:https';
|
|
|
5
5
|
import http2 from 'node:http2';
|
|
6
6
|
import * as cookie from 'cookie';
|
|
7
7
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
8
|
+
import { IncomingMessage as IncomingMessage$1, ServerResponse as ServerResponse$1 } from 'http';
|
|
8
9
|
import { RouteOpts as RouteOpts$1, QueryRouterServer as QueryRouterServer$1, RouteMiddleware as RouteMiddleware$1, Run as Run$1 } from '@kevisual/router';
|
|
9
10
|
import { Query, DataOpts, Result } from '@kevisual/query/query';
|
|
10
11
|
|
|
@@ -15,8 +16,8 @@ type BaseRule = {
|
|
|
15
16
|
};
|
|
16
17
|
type RuleString = {
|
|
17
18
|
type: 'string';
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
min?: number;
|
|
20
|
+
max?: number;
|
|
20
21
|
regex?: string;
|
|
21
22
|
} & BaseRule;
|
|
22
23
|
type RuleNumber = {
|
|
@@ -30,8 +31,6 @@ type RuleBoolean = {
|
|
|
30
31
|
type RuleArray = {
|
|
31
32
|
type: 'array';
|
|
32
33
|
items: Rule;
|
|
33
|
-
minItems?: number;
|
|
34
|
-
maxItems?: number;
|
|
35
34
|
} & BaseRule;
|
|
36
35
|
type RuleObject = {
|
|
37
36
|
type: 'object';
|
|
@@ -716,6 +715,10 @@ declare class App<T = {}, U = AppReqRes> {
|
|
|
716
715
|
importRoutes(routes: any[]): void;
|
|
717
716
|
importApp(app: App): void;
|
|
718
717
|
throw(code?: number | string, message?: string, tips?: string): void;
|
|
718
|
+
static handleRequest(req: IncomingMessage$1, res: ServerResponse$1): Promise<{
|
|
719
|
+
cookies: Record<string, string>;
|
|
720
|
+
token: string;
|
|
721
|
+
}>;
|
|
719
722
|
}
|
|
720
723
|
|
|
721
724
|
export { App, Connect, CustomError, QueryConnect, QueryRouter, QueryRouterServer, QueryUtil, Route, Server, createSchema, define, handleServer, util };
|
package/dist/router.js
CHANGED
|
@@ -4521,10 +4521,10 @@ const schemaFormRule = (rule) => {
|
|
|
4521
4521
|
switch (rule.type) {
|
|
4522
4522
|
case 'string':
|
|
4523
4523
|
let stringSchema = z.string();
|
|
4524
|
-
if (rule.
|
|
4525
|
-
stringSchema = stringSchema.min(rule.
|
|
4526
|
-
if (rule.
|
|
4527
|
-
stringSchema = stringSchema.max(rule.
|
|
4524
|
+
if (rule.min)
|
|
4525
|
+
stringSchema = stringSchema.min(rule.min, `String must be at least ${rule.min} characters long.`);
|
|
4526
|
+
if (rule.max)
|
|
4527
|
+
stringSchema = stringSchema.max(rule.max, `String must not exceed ${rule.max} characters.`);
|
|
4528
4528
|
if (rule.regex)
|
|
4529
4529
|
stringSchema = stringSchema.regex(new RegExp(rule.regex), 'Invalid format');
|
|
4530
4530
|
return stringSchema;
|
|
@@ -12169,6 +12169,9 @@ class App {
|
|
|
12169
12169
|
throw(...args) {
|
|
12170
12170
|
throw new CustomError(...args);
|
|
12171
12171
|
}
|
|
12172
|
+
static handleRequest(req, res) {
|
|
12173
|
+
return handleServer(req, res);
|
|
12174
|
+
}
|
|
12172
12175
|
}
|
|
12173
12176
|
|
|
12174
12177
|
export { App, Connect, CustomError, QueryConnect, QueryRouter, QueryRouterServer, QueryUtil, Route, ZodType as Schema, Server, createSchema, define, handleServer, util };
|
package/package.json
CHANGED
package/src/app.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { QueryRouter, Route, RouteContext, RouteOpts } from './route.ts';
|
|
|
2
2
|
import { Server, ServerOpts, HandleCtx } from './server/server.ts';
|
|
3
3
|
import { WsServer } from './server/ws-server.ts';
|
|
4
4
|
import { CustomError } from './result/error.ts';
|
|
5
|
+
import { handleServer } from './server/handle-server.ts';
|
|
6
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
5
7
|
|
|
6
8
|
type RouterHandle = (msg: { path: string; [key: string]: any }) => { code: string; data?: any; message?: string; [key: string]: any };
|
|
7
9
|
type AppOptions<T = {}> = {
|
|
@@ -100,6 +102,9 @@ export class App<T = {}, U = AppReqRes> {
|
|
|
100
102
|
throw(...args: any[]) {
|
|
101
103
|
throw new CustomError(...args);
|
|
102
104
|
}
|
|
105
|
+
static handleRequest(req: IncomingMessage, res: ServerResponse) {
|
|
106
|
+
return handleServer(req, res);
|
|
107
|
+
}
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
export * from './browser.ts';
|
package/src/router-simple.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { pathToRegexp, Key } from 'path-to-regexp';
|
|
2
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { IncomingMessage, ServerResponse, Server } from 'node:http';
|
|
3
3
|
import { parseBody, parseSearch, parseSearchValue } from './server/parse-body.ts';
|
|
4
|
+
import { ListenOptions } from 'node:net';
|
|
4
5
|
|
|
5
6
|
type Req = IncomingMessage & { params?: Record<string, string> };
|
|
7
|
+
type SimpleObject = {
|
|
8
|
+
[key: string]: any;
|
|
9
|
+
};
|
|
6
10
|
interface Route {
|
|
7
11
|
method: string;
|
|
8
12
|
regexp: RegExp;
|
|
@@ -28,7 +32,6 @@ export class SimpleRouter {
|
|
|
28
32
|
use(method: string, route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>) {
|
|
29
33
|
const handlers = Array.isArray(fns) ? fns.flat() : [];
|
|
30
34
|
const pattern = pathToRegexp(route);
|
|
31
|
-
|
|
32
35
|
this.routes.push({ method: method.toLowerCase(), regexp: pattern.regexp, keys: pattern.keys, handlers });
|
|
33
36
|
return this;
|
|
34
37
|
}
|
|
@@ -38,11 +41,35 @@ export class SimpleRouter {
|
|
|
38
41
|
post(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>) {
|
|
39
42
|
return this.use('post', route, ...fns);
|
|
40
43
|
}
|
|
44
|
+
sse(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>) {
|
|
45
|
+
return this.use('sse', route, ...fns);
|
|
46
|
+
}
|
|
41
47
|
all(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>) {
|
|
42
48
|
this.use('post', route, ...fns);
|
|
43
49
|
this.use('get', route, ...fns);
|
|
50
|
+
this.use('sse', route, ...fns);
|
|
44
51
|
return this;
|
|
45
52
|
}
|
|
53
|
+
getJson(v: string | number | boolean | SimpleObject) {
|
|
54
|
+
if (typeof v === 'object') {
|
|
55
|
+
return v;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(v as string);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
isSse(req: Req) {
|
|
64
|
+
const { headers } = req;
|
|
65
|
+
if (headers['accept'] && headers['accept'].includes('text/event-stream')) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (headers['content-type'] && headers['content-type'].includes('text/event-stream')) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
46
73
|
/**
|
|
47
74
|
* 解析 req 和 res 请求
|
|
48
75
|
* @param req
|
|
@@ -51,10 +78,12 @@ export class SimpleRouter {
|
|
|
51
78
|
*/
|
|
52
79
|
parse(req: Req, res: ServerResponse) {
|
|
53
80
|
const { pathname } = new URL(req.url, 'http://localhost');
|
|
54
|
-
|
|
81
|
+
let method = req.method.toLowerCase();
|
|
55
82
|
if (this.exclude.includes(pathname)) {
|
|
56
83
|
return 'is_exclude';
|
|
57
84
|
}
|
|
85
|
+
const isSse = this.isSse(req);
|
|
86
|
+
if (isSse) method = 'sse';
|
|
58
87
|
const route = this.routes.find((route) => {
|
|
59
88
|
const matchResult = route.regexp.exec(pathname);
|
|
60
89
|
if (matchResult && route.method === method) {
|
|
@@ -74,4 +103,166 @@ export class SimpleRouter {
|
|
|
74
103
|
|
|
75
104
|
return 'not_found';
|
|
76
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* 创建一个新的 HttpChain 实例
|
|
108
|
+
* @param req
|
|
109
|
+
* @param res
|
|
110
|
+
* @returns
|
|
111
|
+
*/
|
|
112
|
+
chain(req?: Req, res?: ServerResponse) {
|
|
113
|
+
const chain = new HttpChain({ req, res, simpleRouter: this });
|
|
114
|
+
return chain;
|
|
115
|
+
}
|
|
116
|
+
static Chain(opts?: HttpChainOpts) {
|
|
117
|
+
return new HttpChain(opts);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
type HttpChainOpts = {
|
|
122
|
+
req?: Req;
|
|
123
|
+
res?: ServerResponse;
|
|
124
|
+
simpleRouter?: SimpleRouter;
|
|
125
|
+
};
|
|
126
|
+
export class HttpChain {
|
|
127
|
+
req: Req;
|
|
128
|
+
res: ServerResponse;
|
|
129
|
+
simpleRouter: SimpleRouter;
|
|
130
|
+
server: Server;
|
|
131
|
+
hasSetHeader: boolean = false;
|
|
132
|
+
isSseSet: boolean = false;
|
|
133
|
+
constructor(opts?: HttpChainOpts) {
|
|
134
|
+
this.req = opts?.req;
|
|
135
|
+
this.res = opts?.res;
|
|
136
|
+
this.simpleRouter = opts?.simpleRouter;
|
|
137
|
+
}
|
|
138
|
+
setReq(req: Req) {
|
|
139
|
+
this.req = req;
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
setRes(res: ServerResponse) {
|
|
143
|
+
this.res = res;
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
setRouter(router: SimpleRouter) {
|
|
147
|
+
this.simpleRouter = router;
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
setServer(server: Server) {
|
|
151
|
+
this.server = server;
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* 兼容 express 的一点功能
|
|
156
|
+
* @param status
|
|
157
|
+
* @returns
|
|
158
|
+
*/
|
|
159
|
+
status(status: number) {
|
|
160
|
+
if (!this.res) return this;
|
|
161
|
+
if (this.hasSetHeader) {
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
this.hasSetHeader = true;
|
|
165
|
+
this.res.writeHead(status);
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
writeHead(status: number) {
|
|
169
|
+
if (!this.res) return this;
|
|
170
|
+
if (this.hasSetHeader) {
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
this.hasSetHeader = true;
|
|
174
|
+
this.res.writeHead(status);
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
json(data: SimpleObject) {
|
|
178
|
+
if (!this.res) return this;
|
|
179
|
+
this.res.end(JSON.stringify(data));
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 兼容 express 的一点功能
|
|
184
|
+
* @param data
|
|
185
|
+
* @returns
|
|
186
|
+
*/
|
|
187
|
+
end(data: SimpleObject | string) {
|
|
188
|
+
if (!this.res) return this;
|
|
189
|
+
if (typeof data === 'object') {
|
|
190
|
+
this.res.end(JSON.stringify(data));
|
|
191
|
+
} else if (typeof data === 'string') {
|
|
192
|
+
this.res.end(data);
|
|
193
|
+
} else {
|
|
194
|
+
this.res.end('nothing');
|
|
195
|
+
}
|
|
196
|
+
return this;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
listen(opts: ListenOptions, callback?: () => void) {
|
|
200
|
+
this.server.listen(opts, callback);
|
|
201
|
+
return this;
|
|
202
|
+
}
|
|
203
|
+
parse() {
|
|
204
|
+
if (!this.server || !this.simpleRouter) {
|
|
205
|
+
throw new Error('Server and SimpleRouter must be set before calling parse');
|
|
206
|
+
}
|
|
207
|
+
const that = this;
|
|
208
|
+
const listener = (req: Req, res: ServerResponse) => {
|
|
209
|
+
try {
|
|
210
|
+
that.simpleRouter.parse(req, res);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error('Error parsing request:', error);
|
|
213
|
+
if (!res.headersSent) {
|
|
214
|
+
res.writeHead(500);
|
|
215
|
+
res.end(JSON.stringify({ code: 500, message: 'Internal Server Error' }));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
this.server.on('request', listener);
|
|
220
|
+
return () => {
|
|
221
|
+
that.server.removeListener('request', listener);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
getString(value: string | SimpleObject) {
|
|
225
|
+
if (typeof value === 'string') {
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
return JSON.stringify(value);
|
|
229
|
+
}
|
|
230
|
+
sse(value: string | SimpleObject) {
|
|
231
|
+
const res = this.res;
|
|
232
|
+
const req = this.req;
|
|
233
|
+
if (!res || !req) return;
|
|
234
|
+
const data = this.getString(value);
|
|
235
|
+
if (this.isSseSet) {
|
|
236
|
+
res.write(`data: ${data}\n\n`);
|
|
237
|
+
return this;
|
|
238
|
+
}
|
|
239
|
+
const headersMap = new Map<string, string>([
|
|
240
|
+
['Content-Type', 'text/event-stream'],
|
|
241
|
+
['Cache-Control', 'no-cache'],
|
|
242
|
+
['Connection', 'keep-alive'],
|
|
243
|
+
]);
|
|
244
|
+
this.isSseSet = true;
|
|
245
|
+
let intervalId: NodeJS.Timeout;
|
|
246
|
+
if (!this.hasSetHeader) {
|
|
247
|
+
this.hasSetHeader = true;
|
|
248
|
+
res.setHeaders(headersMap);
|
|
249
|
+
// 每隔 2 秒发送一个空行,保持连接
|
|
250
|
+
setInterval(() => {
|
|
251
|
+
res.write('\n'); // 发送一个空行,保持连接
|
|
252
|
+
}, 3000);
|
|
253
|
+
// 客户端断开连接时清理
|
|
254
|
+
req.on('close', () => {
|
|
255
|
+
clearInterval(intervalId);
|
|
256
|
+
res.end();
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
this.res.write(`data: ${data}\n\n`);
|
|
260
|
+
return this;
|
|
261
|
+
}
|
|
262
|
+
close() {
|
|
263
|
+
if (this.req?.destroy) {
|
|
264
|
+
this.req.destroy();
|
|
265
|
+
}
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
77
268
|
}
|
package/src/validator/rule.ts
CHANGED
|
@@ -8,8 +8,8 @@ type BaseRule = {
|
|
|
8
8
|
|
|
9
9
|
type RuleString = {
|
|
10
10
|
type: 'string';
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
min?: number;
|
|
12
|
+
max?: number;
|
|
13
13
|
regex?: string;
|
|
14
14
|
} & BaseRule;
|
|
15
15
|
|
|
@@ -26,8 +26,6 @@ type RuleBoolean = {
|
|
|
26
26
|
type RuleArray = {
|
|
27
27
|
type: 'array';
|
|
28
28
|
items: Rule;
|
|
29
|
-
minItems?: number;
|
|
30
|
-
maxItems?: number;
|
|
31
29
|
} & BaseRule;
|
|
32
30
|
|
|
33
31
|
type RuleObject = {
|
|
@@ -45,8 +43,8 @@ export const schemaFormRule = (rule: Rule): z.ZodType<any, any, any> => {
|
|
|
45
43
|
switch (rule.type) {
|
|
46
44
|
case 'string':
|
|
47
45
|
let stringSchema = z.string();
|
|
48
|
-
if (rule.
|
|
49
|
-
if (rule.
|
|
46
|
+
if (rule.min) stringSchema = stringSchema.min(rule.min, `String must be at least ${rule.min} characters long.`);
|
|
47
|
+
if (rule.max) stringSchema = stringSchema.max(rule.max, `String must not exceed ${rule.max} characters.`);
|
|
50
48
|
if (rule.regex) stringSchema = stringSchema.regex(new RegExp(rule.regex), 'Invalid format');
|
|
51
49
|
return stringSchema;
|
|
52
50
|
case 'number':
|