typespeed 2.4.10 → 2.5.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/.github/workflows/test.yml +6 -6
- package/CHANGELOG.md +75 -0
- package/README.md +57 -15
- package/app/src/test-bind.class.ts +41 -0
- package/dist/bind.decorator.js +50 -0
- package/dist/core.decorator.js +113 -26
- package/dist/database.decorator.js +122 -16
- package/dist/decorator-utils.js +42 -0
- package/dist/route.decorator.js +175 -10
- package/dist/typespeed.d.ts +28 -24
- package/dist/typespeed.js +37 -8
- package/introduction/decorator-next/dist/main.js +116 -0
- package/introduction/decorator-next/package.json +14 -0
- package/introduction/decorator-next/src/main.ts +64 -0
- package/introduction/decorator-next/tsconfig.json +17 -0
- package/introduction/decorator-next//346/225/231/347/250/213.md +384 -0
- package/introduction/tsconfig.json +3 -0
- package/package.json +2 -2
- package/src/bind.decorator.ts +50 -0
- package/src/core.decorator.ts +111 -25
- package/src/database.decorator.ts +120 -16
- package/src/decorator-utils.ts +53 -0
- package/src/route.decorator.ts +169 -10
- package/src/typespeed.d.ts +28 -24
- package/src/typespeed.ts +38 -7
- package/test/bind.test.ts +28 -0
- package/test/decorator-utils.test.ts +42 -0
- package/test-env/docker-compose.test.yml +31 -0
- package/test-env/mysql-init/init.sql +8 -0
- package/test-env//346/234/254/345/234/260Docker/346/265/213/350/257/225/346/226/271/346/241/210.md +69 -0
package/src/core.decorator.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
2
|
import * as cron from "cron";
|
|
3
3
|
import LogFactory from "./factory/log-factory.class";
|
|
4
|
+
import { isStd, getStdArgs } from "./decorator-utils";
|
|
4
5
|
|
|
5
6
|
const resourceObjects = new Map<string, object>();
|
|
6
7
|
const beanMapper: Map<string, any> = new Map<string, any>();
|
|
7
8
|
const objectMapper: Map<string, any> = new Map<string, any>();
|
|
8
9
|
|
|
9
|
-
function component(
|
|
10
|
+
function component(...args: any[]): any {
|
|
11
|
+
if (isStd(args)) {
|
|
12
|
+
const [ctor, ctx] = getStdArgs(args);
|
|
13
|
+
ctx.addInitializer(function (this: any) {
|
|
14
|
+
objectMapper.set(this.name, new this());
|
|
15
|
+
});
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const constructorFunction = args[0];
|
|
10
19
|
objectMapper.set(constructorFunction.name, new constructorFunction());
|
|
11
20
|
}
|
|
12
21
|
|
|
@@ -14,13 +23,36 @@ function getComponent(constructorFunction) {
|
|
|
14
23
|
return objectMapper.get(constructorFunction.name);
|
|
15
24
|
}
|
|
16
25
|
|
|
17
|
-
function bean(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
function bean(...args: any[]): any {
|
|
27
|
+
if (args.length >= 2) {
|
|
28
|
+
// 直接装饰器形式:@bean(无 token,走 legacy design:returntype)
|
|
29
|
+
return beanWithToken(undefined)(...args);
|
|
30
|
+
}
|
|
31
|
+
// 工厂形式:@bean(Token)(显式返回类型 token,标准模式必需)
|
|
32
|
+
return beanWithToken(args[0]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function beanWithToken(token?: any) {
|
|
36
|
+
return function (...args: any[]): any {
|
|
37
|
+
if (isStd(args)) {
|
|
38
|
+
const [, ctx] = getStdArgs(args);
|
|
39
|
+
const key = String(ctx.name);
|
|
40
|
+
ctx.addInitializer(function (this: any) {
|
|
41
|
+
beanMapper.set(token ? token.name : key, {
|
|
42
|
+
"target": this, "propertyKey": key,
|
|
43
|
+
"factory": this[key]()
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const target = args[0];
|
|
49
|
+
const propertyKey = args[1] as string;
|
|
50
|
+
let returnType = token || Reflect.getMetadata("design:returntype", target, propertyKey);
|
|
51
|
+
beanMapper.set(returnType.name, {
|
|
52
|
+
"target": target, "propertyKey": propertyKey,
|
|
53
|
+
"factory": target[propertyKey]()
|
|
54
|
+
});
|
|
55
|
+
};
|
|
24
56
|
}
|
|
25
57
|
|
|
26
58
|
function getBean(mappingClass: Function): any {
|
|
@@ -29,31 +61,76 @@ function getBean(mappingClass: Function): any {
|
|
|
29
61
|
}
|
|
30
62
|
|
|
31
63
|
|
|
32
|
-
function autoware(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
64
|
+
function autoware(...args: any[]): any {
|
|
65
|
+
if (args.length >= 2) {
|
|
66
|
+
// 直接装饰器形式:@autoware(无 token,走 legacy design:type)
|
|
67
|
+
return autowareWithToken(undefined)(...args);
|
|
68
|
+
}
|
|
69
|
+
// 工厂形式:@autoware(Token)(显式 token,标准模式必需)
|
|
70
|
+
return autowareWithToken(args[0]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function autowareWithToken(token?: any) {
|
|
74
|
+
return function (...args: any[]): any {
|
|
75
|
+
if (isStd(args)) {
|
|
76
|
+
// 标准 field 装饰器:标准模式无 design:type,有 token 才能注入。
|
|
77
|
+
return (initialValue: any) => {
|
|
78
|
+
if (!token) return initialValue;
|
|
79
|
+
const bean = beanMapper.get(token.name);
|
|
80
|
+
if (bean !== undefined) return bean["factory"];
|
|
81
|
+
return new token();
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const target = args[0];
|
|
85
|
+
const propertyKey = args[1] as string;
|
|
86
|
+
const type = token || Reflect.getMetadata("design:type", target, propertyKey);
|
|
87
|
+
Object.defineProperty(target, propertyKey, {
|
|
88
|
+
get: () => {
|
|
89
|
+
const targetObject = beanMapper.get(type.name);
|
|
90
|
+
if (targetObject === undefined) {
|
|
91
|
+
const resourceKey = [target.constructor.name, propertyKey, type.name].toString();
|
|
92
|
+
if (!resourceObjects[resourceKey]) {
|
|
93
|
+
resourceObjects[resourceKey] = new type();
|
|
94
|
+
}
|
|
95
|
+
return resourceObjects[resourceKey];
|
|
41
96
|
}
|
|
42
|
-
return
|
|
97
|
+
return targetObject["factory"];
|
|
43
98
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 解析 @resource(...args) 的首参:若首参是 Function(类/token),作为显式 token,
|
|
105
|
+
* 其余作为构造参数;否则全部作为构造参数(legacy 行为,如 @resource("user"))。
|
|
106
|
+
*/
|
|
107
|
+
function extractTokenAndArgs(args: any[]): [any, any[]] {
|
|
108
|
+
if (args.length > 0 && typeof args[0] === "function") {
|
|
109
|
+
return [args[0], args.slice(1)];
|
|
110
|
+
}
|
|
111
|
+
return [undefined, args];
|
|
47
112
|
}
|
|
48
113
|
|
|
49
114
|
function resource(...args): any {
|
|
50
|
-
|
|
51
|
-
|
|
115
|
+
const [token, initArgs] = extractTokenAndArgs(args);
|
|
116
|
+
return (...decoratorArgs: any[]): any => {
|
|
117
|
+
if (isStd(decoratorArgs)) {
|
|
118
|
+
// 标准 field 装饰器:标准模式无 design:type,需显式 token。
|
|
119
|
+
return (initialValue: any) => {
|
|
120
|
+
if (!token) return initialValue;
|
|
121
|
+
const bean = beanMapper.get(token.name);
|
|
122
|
+
if (bean !== undefined) return bean["factory"];
|
|
123
|
+
return new token(...initArgs);
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const target = decoratorArgs[0];
|
|
127
|
+
const propertyKey = decoratorArgs[1] as string;
|
|
128
|
+
const type = token || Reflect.getMetadata("design:type", target, propertyKey);
|
|
52
129
|
Object.defineProperty(target, propertyKey, {
|
|
53
130
|
get: () => {
|
|
54
131
|
const resourceKey = [target.constructor.name, propertyKey, type.name].toString();
|
|
55
132
|
if (!resourceObjects[resourceKey]) {
|
|
56
|
-
resourceObjects[resourceKey] = new type(...
|
|
133
|
+
resourceObjects[resourceKey] = new type(...initArgs);
|
|
57
134
|
}
|
|
58
135
|
return resourceObjects[resourceKey];
|
|
59
136
|
}
|
|
@@ -90,7 +167,16 @@ function error(message?: any, ...optionalParams: any[]) {
|
|
|
90
167
|
}
|
|
91
168
|
|
|
92
169
|
function schedule(cronTime: string | Date) {
|
|
93
|
-
return (
|
|
170
|
+
return (...args: any[]): any => {
|
|
171
|
+
if (isStd(args)) {
|
|
172
|
+
const [, ctx] = getStdArgs(args);
|
|
173
|
+
ctx.addInitializer(function (this: any) {
|
|
174
|
+
new cron.CronJob(cronTime, this[String(ctx.name)]).start();
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const target = args[0];
|
|
179
|
+
const propertyKey = args[1] as string;
|
|
94
180
|
new cron.CronJob(cronTime, target[propertyKey]).start();
|
|
95
181
|
}
|
|
96
182
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ResultSetHeader } from 'mysql2';
|
|
2
2
|
import { log, getBean } from './core.decorator';
|
|
3
|
+
import { isStd, getStdArgs } from './decorator-utils';
|
|
4
|
+
import { getBindMapping } from './bind.decorator';
|
|
3
5
|
import CacheFactory from './factory/cache-factory.class';
|
|
4
6
|
import DataSourceFactory from './factory/data-source-factory.class';
|
|
5
7
|
|
|
@@ -10,9 +12,24 @@ const tableVersionMap = new Map<string, number>();
|
|
|
10
12
|
let cacheBean: CacheFactory;
|
|
11
13
|
|
|
12
14
|
function insert(sql: string) {
|
|
13
|
-
return (
|
|
14
|
-
|
|
15
|
-
const
|
|
15
|
+
return (...args: any[]): any => {
|
|
16
|
+
if (isStd(args)) {
|
|
17
|
+
const [, ctx] = getStdArgs(args);
|
|
18
|
+
const propertyKey = String(ctx.name);
|
|
19
|
+
return async function (this: any, ...callArgs: any[]) {
|
|
20
|
+
const result: ResultSetHeader = await queryForExecute(sql, callArgs, this, propertyKey);
|
|
21
|
+
if (cacheBean && result.affectedRows > 0) {
|
|
22
|
+
const [tableName, tableVersion] = getTableAndVersion("insert", sql);
|
|
23
|
+
tableVersionMap.set(tableName, tableVersion + 1);
|
|
24
|
+
}
|
|
25
|
+
return result.insertId;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const target = args[0];
|
|
29
|
+
const propertyKey = args[1] as string;
|
|
30
|
+
const descriptor = args[2] as PropertyDescriptor;
|
|
31
|
+
descriptor.value = async (...callArgs: any[]) => {
|
|
32
|
+
const result: ResultSetHeader = await queryForExecute(sql, callArgs, target, propertyKey);
|
|
16
33
|
if (cacheBean && result.affectedRows > 0) {
|
|
17
34
|
const [tableName, tableVersion] = getTableAndVersion("insert", sql);
|
|
18
35
|
tableVersionMap.set(tableName, tableVersion + 1);
|
|
@@ -23,9 +40,24 @@ function insert(sql: string) {
|
|
|
23
40
|
}
|
|
24
41
|
|
|
25
42
|
function update(sql: string) {
|
|
26
|
-
return (
|
|
27
|
-
|
|
28
|
-
const
|
|
43
|
+
return (...args: any[]): any => {
|
|
44
|
+
if (isStd(args)) {
|
|
45
|
+
const [, ctx] = getStdArgs(args);
|
|
46
|
+
const propertyKey = String(ctx.name);
|
|
47
|
+
return async function (this: any, ...callArgs: any[]) {
|
|
48
|
+
const result: ResultSetHeader = await queryForExecute(sql, callArgs, this, propertyKey);
|
|
49
|
+
if (cacheBean && result.affectedRows > 0) {
|
|
50
|
+
const [tableName, tableVersion] = getTableAndVersion("update", sql);
|
|
51
|
+
tableVersionMap.set(tableName, tableVersion + 1);
|
|
52
|
+
}
|
|
53
|
+
return result.affectedRows;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const target = args[0];
|
|
57
|
+
const propertyKey = args[1] as string;
|
|
58
|
+
const descriptor = args[2] as PropertyDescriptor;
|
|
59
|
+
descriptor.value = async (...callArgs: any[]) => {
|
|
60
|
+
const result: ResultSetHeader = await queryForExecute(sql, callArgs, target, propertyKey);
|
|
29
61
|
if (cacheBean && result.affectedRows > 0) {
|
|
30
62
|
const [tableName, tableVersion] = getTableAndVersion("update", sql);
|
|
31
63
|
tableVersionMap.set(tableName, tableVersion + 1);
|
|
@@ -36,9 +68,24 @@ function update(sql: string) {
|
|
|
36
68
|
}
|
|
37
69
|
|
|
38
70
|
function remove(sql: string) {
|
|
39
|
-
return (
|
|
40
|
-
|
|
41
|
-
const
|
|
71
|
+
return (...args: any[]): any => {
|
|
72
|
+
if (isStd(args)) {
|
|
73
|
+
const [, ctx] = getStdArgs(args);
|
|
74
|
+
const propertyKey = String(ctx.name);
|
|
75
|
+
return async function (this: any, ...callArgs: any[]) {
|
|
76
|
+
const result: ResultSetHeader = await queryForExecute(sql, callArgs, this, propertyKey);
|
|
77
|
+
if (cacheBean && result.affectedRows > 0) {
|
|
78
|
+
const [tableName, tableVersion] = getTableAndVersion("delete", sql);
|
|
79
|
+
tableVersionMap.set(tableName, tableVersion + 1);
|
|
80
|
+
}
|
|
81
|
+
return result.affectedRows;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const target = args[0];
|
|
85
|
+
const propertyKey = args[1] as string;
|
|
86
|
+
const descriptor = args[2] as PropertyDescriptor;
|
|
87
|
+
descriptor.value = async (...callArgs: any[]) => {
|
|
88
|
+
const result: ResultSetHeader = await queryForExecute(sql, callArgs, target, propertyKey);
|
|
42
89
|
if (cacheBean && result.affectedRows > 0) {
|
|
43
90
|
const [tableName, tableVersion] = getTableAndVersion("delete", sql);
|
|
44
91
|
tableVersionMap.set(tableName, tableVersion + 1);
|
|
@@ -49,9 +96,33 @@ function remove(sql: string) {
|
|
|
49
96
|
}
|
|
50
97
|
|
|
51
98
|
function select(sql: string) {
|
|
52
|
-
return (
|
|
53
|
-
|
|
54
|
-
const [
|
|
99
|
+
return (...args: any[]): any => {
|
|
100
|
+
if (isStd(args)) {
|
|
101
|
+
const [, ctx] = getStdArgs(args);
|
|
102
|
+
const propertyKey = String(ctx.name);
|
|
103
|
+
return async function (this: any, ...callArgs: any[]) {
|
|
104
|
+
const [newSql, sqlValues] = convertSQLParams(sql, this, propertyKey, callArgs);
|
|
105
|
+
const resultType = resultTypeMap.get([this.constructor.name, propertyKey].toString());
|
|
106
|
+
if (cacheBean && cacheDefindMap.has([this.constructor.name, propertyKey].toString())) {
|
|
107
|
+
const [tableName, tableVersion] = getTableAndVersion("select", newSql);
|
|
108
|
+
const cacheKey = JSON.stringify([tableName, tableVersion, newSql, sqlValues]);
|
|
109
|
+
if (cacheBean.get(cacheKey)) {
|
|
110
|
+
return cacheBean.get(cacheKey);
|
|
111
|
+
} else {
|
|
112
|
+
const rows = await actionQuery(newSql, sqlValues, resultType);
|
|
113
|
+
cacheBean.set(cacheKey, rows, cacheDefindMap.get([this.constructor.name, propertyKey].toString()));
|
|
114
|
+
return rows;
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
return await actionQuery(newSql, sqlValues, resultType);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const target = args[0];
|
|
122
|
+
const propertyKey = args[1] as string;
|
|
123
|
+
const descriptor = args[2] as PropertyDescriptor;
|
|
124
|
+
descriptor.value = async (...callArgs: any[]) => {
|
|
125
|
+
const [newSql, sqlValues] = convertSQLParams(sql, target, propertyKey, callArgs);
|
|
55
126
|
const resultType = resultTypeMap.get([target.constructor.name, propertyKey].toString());
|
|
56
127
|
if (cacheBean && cacheDefindMap.has([target.constructor.name, propertyKey].toString())) {
|
|
57
128
|
const [tableName, tableVersion] = getTableAndVersion("select", newSql);
|
|
@@ -71,7 +142,16 @@ function select(sql: string) {
|
|
|
71
142
|
}
|
|
72
143
|
|
|
73
144
|
function resultType(dataClass) {
|
|
74
|
-
return function (
|
|
145
|
+
return function (...args: any[]): any {
|
|
146
|
+
if (isStd(args)) {
|
|
147
|
+
const [, ctx] = getStdArgs(args);
|
|
148
|
+
ctx.addInitializer(function (this: any) {
|
|
149
|
+
resultTypeMap.set([this.constructor.name, String(ctx.name)].toString(), dataClass);
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const target = args[0];
|
|
154
|
+
const propertyKey = args[1] as string;
|
|
75
155
|
resultTypeMap.set([target.constructor.name, propertyKey].toString(), dataClass);
|
|
76
156
|
//never return
|
|
77
157
|
};
|
|
@@ -124,8 +204,17 @@ function convertSQLParams(decoratorSQL: string, target: any, propertyKey: string
|
|
|
124
204
|
if (typeof args[0] === 'object') {
|
|
125
205
|
argsVal = new Map(Object.getOwnPropertyNames(args[0]).map((valName) => [valName, args[0][valName]]));
|
|
126
206
|
} else {
|
|
127
|
-
|
|
128
|
-
|
|
207
|
+
// 优先 @param(legacy reflect-metadata),否则回退 @bind(方法级声明,标准模式)
|
|
208
|
+
let existingParameters: [string, number][] = Reflect.getOwnMetadata(paramMetadataKey, target, propertyKey);
|
|
209
|
+
if (!existingParameters) {
|
|
210
|
+
const bindMapping = getBindMapping(target.constructor.name, propertyKey);
|
|
211
|
+
if (bindMapping) {
|
|
212
|
+
existingParameters = Object.entries(bindMapping)
|
|
213
|
+
.filter(([, value]) => typeof value === "number")
|
|
214
|
+
.map(([name, value]) => [name, value as number]);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
argsVal = new Map((existingParameters || []).map(([argName, argIdx]) => [argName, args[argIdx]]));
|
|
129
218
|
}
|
|
130
219
|
const regExp = /#{(\w+)}/;
|
|
131
220
|
let match;
|
|
@@ -139,7 +228,22 @@ function convertSQLParams(decoratorSQL: string, target: any, propertyKey: string
|
|
|
139
228
|
}
|
|
140
229
|
|
|
141
230
|
function cache(ttl: number) {
|
|
142
|
-
return function (
|
|
231
|
+
return function (...args: any[]): any {
|
|
232
|
+
if (isStd(args)) {
|
|
233
|
+
const [, ctx] = getStdArgs(args);
|
|
234
|
+
ctx.addInitializer(function (this: any) {
|
|
235
|
+
cacheDefindMap.set([this.constructor.name, String(ctx.name)].toString(), ttl);
|
|
236
|
+
if (cacheBean == null) {
|
|
237
|
+
const cacheFactory = getBean(CacheFactory);
|
|
238
|
+
if (cacheFactory || cacheFactory["factory"]) {
|
|
239
|
+
cacheBean = cacheFactory["factory"];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const target = args[0];
|
|
246
|
+
const propertyKey = args[1] as string;
|
|
143
247
|
cacheDefindMap.set([target.constructor.name, propertyKey].toString(), ttl);
|
|
144
248
|
if (cacheBean == null) {
|
|
145
249
|
const cacheFactory = getBean(CacheFactory);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 装饰器双签名感知工具。
|
|
3
|
+
*
|
|
4
|
+
* typespeed 2.5.x 起,装饰器同时支持两套运行时签名:
|
|
5
|
+
* - legacy(experimentalDecorators: true,2.4.x 现状):
|
|
6
|
+
* 类装饰器 (ctor);方法/属性装饰器 (target, key);带 descriptor 的方法 (target, key, descriptor);参数 (target, key, index)
|
|
7
|
+
* - 标准(TC39 装饰器提案,当前 Stage 2.7,experimentalDecorators: false):
|
|
8
|
+
* 统一 (value, context),context 恒为带 kind 字段的对象
|
|
9
|
+
*
|
|
10
|
+
* 装饰器函数本质是普通函数,运行时收到什么签名由「调用方(用户代码)」的编译模式决定,
|
|
11
|
+
* 因此库可以单份源码、运行时按签名形态分流:legacy 分支逻辑与 2.4.x 逐字一致,标准分支为新增。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** 标准装饰器 context 的 kind 取值(TS/Babel 2022-03 版语义) */
|
|
15
|
+
export type StdDecoratorKind = "class" | "method" | "getter" | "setter" | "field" | "accessor";
|
|
16
|
+
|
|
17
|
+
/** 标准装饰器 context 的宽松类型描述(不依赖 esnext.decorators lib,保持 ES2017 可编译) */
|
|
18
|
+
export interface StdDecoratorContext {
|
|
19
|
+
kind: StdDecoratorKind;
|
|
20
|
+
name?: string | symbol;
|
|
21
|
+
static?: boolean;
|
|
22
|
+
private?: boolean;
|
|
23
|
+
access?: { get?: boolean; set?: boolean };
|
|
24
|
+
metadata?: Record<PropertyKey, unknown>;
|
|
25
|
+
addInitializer(fn: (this: any) => void): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 判断装饰器收到的运行时参数是否为标准装饰器签名 (value, context)。
|
|
30
|
+
*
|
|
31
|
+
* 判据(与 `2.5.x-新版装饰器使用方案.md` 一致):
|
|
32
|
+
* 参数个数 === 2 且第二个参数是「带 string 类型 kind 字段」的对象。
|
|
33
|
+
*
|
|
34
|
+
* 为什么这个判据可靠:
|
|
35
|
+
* - legacy 类装饰器只有 1 参 → 不满足 length === 2
|
|
36
|
+
* - legacy 方法/属性装饰器第二参是 string 的 key → 不是对象
|
|
37
|
+
* - legacy 带 descriptor 的方法 / 参数装饰器是 3 参 → 不满足 length === 2
|
|
38
|
+
* - 标准模式第二参恒是带 kind 的对象 → 命中
|
|
39
|
+
*/
|
|
40
|
+
export function isStd(args: unknown[]): boolean {
|
|
41
|
+
return args.length === 2
|
|
42
|
+
&& typeof args[1] === "object"
|
|
43
|
+
&& args[1] !== null
|
|
44
|
+
&& typeof (args[1] as { kind?: unknown }).kind === "string";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 从标准装饰器参数中解出 (value, context)。
|
|
49
|
+
* 仅应在 isStd(args) 为 true 时调用。
|
|
50
|
+
*/
|
|
51
|
+
export function getStdArgs(args: unknown[]): [any, StdDecoratorContext] {
|
|
52
|
+
return [args[0], args[1] as StdDecoratorContext];
|
|
53
|
+
}
|
package/src/route.decorator.ts
CHANGED
|
@@ -2,6 +2,8 @@ import * as express from "express";
|
|
|
2
2
|
import * as multiparty from "multiparty";
|
|
3
3
|
import { expressjwt } from "express-jwt";
|
|
4
4
|
import { getComponent } from "./core.decorator";
|
|
5
|
+
import { isStd, getStdArgs } from "./decorator-utils";
|
|
6
|
+
import { getBindMapping } from "./bind.decorator";
|
|
5
7
|
|
|
6
8
|
const routerMapper = {
|
|
7
9
|
"get": {},
|
|
@@ -26,7 +28,51 @@ function setRouter(app: express.Application) {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
function mapperFunction(method: string, value: string) {
|
|
29
|
-
return (
|
|
31
|
+
return (...args: any[]): any => {
|
|
32
|
+
if (isStd(args)) {
|
|
33
|
+
const [methodFn, ctx] = getStdArgs(args);
|
|
34
|
+
ctx.addInitializer(function (this: any) {
|
|
35
|
+
const className = this.constructor.name;
|
|
36
|
+
const propertyKey = String(ctx.name);
|
|
37
|
+
applyRouteBind(className, propertyKey, methodFn);
|
|
38
|
+
routerMapper[method][value] = {
|
|
39
|
+
"path": value,
|
|
40
|
+
"name": [className, propertyKey].toString(),
|
|
41
|
+
"target": this.constructor,
|
|
42
|
+
"propertyKey": propertyKey,
|
|
43
|
+
"invoker": async (req, res, next) => {
|
|
44
|
+
const routerBean = getComponent(this.constructor);
|
|
45
|
+
try {
|
|
46
|
+
let paramTotal = routerBean[propertyKey].length;
|
|
47
|
+
if (routerParamsTotal[[className, propertyKey].toString()]) {
|
|
48
|
+
paramTotal = Math.max(paramTotal, routerParamsTotal[[className, propertyKey].toString()]);
|
|
49
|
+
}
|
|
50
|
+
const callArgs = [req, res, next];
|
|
51
|
+
if (paramTotal > 0) {
|
|
52
|
+
for (let i = 0; i < paramTotal; i++) {
|
|
53
|
+
if (routerParams[[className, propertyKey, i].toString()]) {
|
|
54
|
+
callArgs[i] = routerParams[[className, propertyKey, i].toString()](req, res, next);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const testResult = await routerBean[propertyKey].apply(routerBean, callArgs);
|
|
59
|
+
if (typeof testResult === "object") {
|
|
60
|
+
res.json(testResult);
|
|
61
|
+
} else if (typeof testResult !== "undefined") {
|
|
62
|
+
res.send(testResult);
|
|
63
|
+
}
|
|
64
|
+
return testResult;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
next(err);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const target = args[0];
|
|
74
|
+
const propertyKey = args[1] as string;
|
|
75
|
+
applyRouteBind(target.constructor.name, propertyKey, target[propertyKey]);
|
|
30
76
|
routerMapper[method][value] = {
|
|
31
77
|
"path": value,
|
|
32
78
|
"name": [target.constructor.name, propertyKey].toString(),
|
|
@@ -62,7 +108,21 @@ function mapperFunction(method: string, value: string) {
|
|
|
62
108
|
}
|
|
63
109
|
}
|
|
64
110
|
|
|
65
|
-
function upload(
|
|
111
|
+
function upload(...args: any[]): any {
|
|
112
|
+
if (isStd(args)) {
|
|
113
|
+
const [, ctx] = getStdArgs(args);
|
|
114
|
+
ctx.addInitializer(function (this: any) {
|
|
115
|
+
const key = [this.constructor.name, String(ctx.name)].toString();
|
|
116
|
+
if (routerMiddleware[key]) {
|
|
117
|
+
routerMiddleware[key].push(uploadMiddleware);
|
|
118
|
+
} else {
|
|
119
|
+
routerMiddleware[key] = [uploadMiddleware];
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const target = args[0];
|
|
125
|
+
const propertyKey = args[1] as string;
|
|
66
126
|
const key = [target.constructor.name, propertyKey].toString();
|
|
67
127
|
if (routerMiddleware[key]) {
|
|
68
128
|
routerMiddleware[key].push(uploadMiddleware);
|
|
@@ -80,7 +140,21 @@ function uploadMiddleware(req, res, next) {
|
|
|
80
140
|
}
|
|
81
141
|
|
|
82
142
|
function jwt(jwtConfig) {
|
|
83
|
-
return (
|
|
143
|
+
return (...args: any[]): any => {
|
|
144
|
+
if (isStd(args)) {
|
|
145
|
+
const [, ctx] = getStdArgs(args);
|
|
146
|
+
ctx.addInitializer(function (this: any) {
|
|
147
|
+
const key = [this.constructor.name, String(ctx.name)].toString();
|
|
148
|
+
if (routerMiddleware[key]) {
|
|
149
|
+
routerMiddleware[key].push(expressjwt(jwtConfig));
|
|
150
|
+
} else {
|
|
151
|
+
routerMiddleware[key] = [expressjwt(jwtConfig)];
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const target = args[0];
|
|
157
|
+
const propertyKey = args[1] as string;
|
|
84
158
|
const key = [target.constructor.name, propertyKey].toString();
|
|
85
159
|
if (routerMiddleware[key]) {
|
|
86
160
|
routerMiddleware[key].push(expressjwt(jwtConfig));
|
|
@@ -92,15 +166,34 @@ function jwt(jwtConfig) {
|
|
|
92
166
|
|
|
93
167
|
function before(constructorFunction, methodName: string) {
|
|
94
168
|
const targetBean = getComponent(constructorFunction);
|
|
95
|
-
return function (
|
|
169
|
+
return function (...args: any[]) {
|
|
170
|
+
if (isStd(args)) {
|
|
171
|
+
const [, ctx] = getStdArgs(args);
|
|
172
|
+
const hookKey = String(ctx.name);
|
|
173
|
+
ctx.addInitializer(function (this: any) {
|
|
174
|
+
const currentMethod = this[methodName];
|
|
175
|
+
if (currentMethod && currentMethod.length > 0) {
|
|
176
|
+
routerParamsTotal[[constructorFunction.name, methodName].toString()] = currentMethod.length;
|
|
177
|
+
}
|
|
178
|
+
Object.assign(this, {
|
|
179
|
+
[methodName]: function (...innerArgs: any[]) {
|
|
180
|
+
this[hookKey](...innerArgs);
|
|
181
|
+
return currentMethod.apply(this, innerArgs);
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const target = args[0];
|
|
188
|
+
const propertyKey = args[1] as string;
|
|
96
189
|
const currentMethod = targetBean[methodName];
|
|
97
190
|
if(currentMethod.length > 0){
|
|
98
191
|
routerParamsTotal[[constructorFunction.name, methodName].toString()] = currentMethod.length;
|
|
99
192
|
}
|
|
100
193
|
Object.assign(targetBean, {
|
|
101
|
-
[methodName]: function (...
|
|
102
|
-
target[propertyKey](...
|
|
103
|
-
return currentMethod.apply(targetBean,
|
|
194
|
+
[methodName]: function (...innerArgs) {
|
|
195
|
+
target[propertyKey](...innerArgs);
|
|
196
|
+
return currentMethod.apply(targetBean, innerArgs);
|
|
104
197
|
}
|
|
105
198
|
})
|
|
106
199
|
};
|
|
@@ -108,14 +201,34 @@ function before(constructorFunction, methodName: string) {
|
|
|
108
201
|
|
|
109
202
|
function after(constructorFunction, methodName: string) {
|
|
110
203
|
const targetBean = getComponent(constructorFunction);
|
|
111
|
-
return function (
|
|
204
|
+
return function (...args: any[]) {
|
|
205
|
+
if (isStd(args)) {
|
|
206
|
+
const [, ctx] = getStdArgs(args);
|
|
207
|
+
const hookKey = String(ctx.name);
|
|
208
|
+
ctx.addInitializer(function (this: any) {
|
|
209
|
+
const currentMethod = this[methodName];
|
|
210
|
+
if (currentMethod && currentMethod.length > 0) {
|
|
211
|
+
routerParamsTotal[[constructorFunction.name, methodName].toString()] = currentMethod.length;
|
|
212
|
+
}
|
|
213
|
+
Object.assign(this, {
|
|
214
|
+
[methodName]: function (...innerArgs: any[]) {
|
|
215
|
+
const result = currentMethod.apply(this, innerArgs);
|
|
216
|
+
const afterResult = this[hookKey](result);
|
|
217
|
+
return afterResult ?? result;
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const target = args[0];
|
|
224
|
+
const propertyKey = args[1] as string;
|
|
112
225
|
const currentMethod = targetBean[methodName];
|
|
113
226
|
if(currentMethod.length > 0){
|
|
114
227
|
routerParamsTotal[[constructorFunction.name, methodName].toString()] = currentMethod.length;
|
|
115
228
|
}
|
|
116
229
|
Object.assign(targetBean, {
|
|
117
|
-
[methodName]: function (...
|
|
118
|
-
const result = currentMethod.apply(targetBean,
|
|
230
|
+
[methodName]: function (...innerArgs) {
|
|
231
|
+
const result = currentMethod.apply(targetBean, innerArgs);
|
|
119
232
|
const afterResult = target[propertyKey](result);
|
|
120
233
|
return afterResult ?? result;
|
|
121
234
|
}
|
|
@@ -155,6 +268,52 @@ function getParamInFunction(fn: Function, index: number) {
|
|
|
155
268
|
return result[index] || null;
|
|
156
269
|
}
|
|
157
270
|
|
|
271
|
+
/** 解析函数参数名列表(供 route @bind 做「参数名 → 索引」映射) */
|
|
272
|
+
function getParamNames(fn: Function): string[] {
|
|
273
|
+
const code = fn.toString().replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, '').replace(/=>.*$/mg, '').replace(/=[^,)]+/g, '');
|
|
274
|
+
const paramsStr = code.slice(code.indexOf('(') + 1, code.indexOf(')'));
|
|
275
|
+
if (!paramsStr.trim()) return [];
|
|
276
|
+
return paramsStr.split(',').map(p => {
|
|
277
|
+
const matched = p.trim().match(/([A-Za-z_$][\w$]*)/);
|
|
278
|
+
return matched ? matched[1] : p.trim();
|
|
279
|
+
}).filter(Boolean);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** 把 route @bind 的「来源字符串」转成参数解析器 */
|
|
283
|
+
function getRouteSourceResolver(source: string, paramName: string) {
|
|
284
|
+
switch (source) {
|
|
285
|
+
case "req": return (req, res, next) => req;
|
|
286
|
+
case "res": return (req, res, next) => res;
|
|
287
|
+
case "next": return (req, res, next) => next;
|
|
288
|
+
case "reqBody": return (req, res, next) => req.body;
|
|
289
|
+
case "reqParam": return (req, res, next) => req.params[paramName];
|
|
290
|
+
case "reqQuery": return (req, res, next) => req.query[paramName];
|
|
291
|
+
case "reqForm": return (req, res, next) => req.body[paramName];
|
|
292
|
+
default: return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** 读取 @bind 声明,把「参数名 → 来源」转成 routerParams 的「索引 → 解析器」 */
|
|
297
|
+
function applyRouteBind(className: string, propertyKey: string, method: Function) {
|
|
298
|
+
const bindMapping = getBindMapping(className, propertyKey);
|
|
299
|
+
if (!bindMapping) return;
|
|
300
|
+
const paramNames = getParamNames(method);
|
|
301
|
+
const nameToIndex: Record<string, number> = {};
|
|
302
|
+
paramNames.forEach((paramName, index) => {
|
|
303
|
+
if (paramName && nameToIndex[paramName] === undefined) nameToIndex[paramName] = index;
|
|
304
|
+
});
|
|
305
|
+
for (const bindName in bindMapping) {
|
|
306
|
+
const source = bindMapping[bindName];
|
|
307
|
+
if (typeof source !== "string") continue; // 跳过 database 的数值映射
|
|
308
|
+
const index = nameToIndex[bindName];
|
|
309
|
+
if (index === undefined) continue;
|
|
310
|
+
const resolver = getRouteSourceResolver(source, bindName);
|
|
311
|
+
if (resolver) {
|
|
312
|
+
routerParams[[className, propertyKey, index].toString()] = resolver;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
158
317
|
function reqQuery(target: any, propertyKey: string, parameterIndex: number) {
|
|
159
318
|
const key = [target.constructor.name, propertyKey, parameterIndex].toString();
|
|
160
319
|
const paramName = getParamInFunction(target[propertyKey], parameterIndex);
|