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.
@@ -5,68 +5,74 @@ import "reflect-metadata";
5
5
 
6
6
  /**设置路由中间件 */
7
7
  declare function setRouter(app: express.Application): void;
8
- /**上传文件装饰器,装饰页面具备解析上传文件的能力 */
9
- declare function upload(target: any, propertyKey: string): void;
8
+ /**上传文件装饰器,装饰页面具备解析上传文件的能力(legacy 方法装饰器;2.5.x 起兼容标准 (value, context) 签名) */
9
+ declare function upload(...args: any[]): any;
10
10
  /**
11
11
  * 页面支持 JWT 鉴权能力
12
12
  * @param jwtConfig jwt 配置
13
13
  */
14
- declare function jwt(jwtConfig: any): (target: any, propertyKey: string) => void;
14
+ declare function jwt(jwtConfig: any): (...args: any[]) => any;
15
15
  /**
16
16
  * GET 请求装饰器
17
17
  * @param value 请求路径
18
18
  */
19
- declare const getMapping: (value: string) => (target: any, propertyKey: string) => void;
19
+ declare const getMapping: (value: string) => (...args: any[]) => any;
20
20
  /**
21
21
  * POST 请求装饰器
22
22
  * @param value 请求路径
23
23
  */
24
- declare const postMapping: (value: string) => (target: any, propertyKey: string) => void;
24
+ declare const postMapping: (value: string) => (...args: any[]) => any;
25
25
  /**
26
26
  * 请求装饰器,不区分请求类型
27
27
  * @param value 请求路径
28
28
  */
29
- declare const requestMapping: (value: string) => (target: any, propertyKey: string) => void;
29
+ declare const requestMapping: (value: string) => (...args: any[]) => any;
30
30
  /**
31
31
  * INSERT 装饰器
32
32
  * 将方法作为 INSERT SQL 使用
33
33
  * @param sql INSERT SQL 语句
34
34
  */
35
- declare function insert(sql: string): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
35
+ declare function insert(sql: string): (...args: any[]) => any;
36
36
  /**
37
37
  * UPDATE 装饰器
38
38
  * 将方法作为 UPDATE SQL 使用
39
39
  * @param sql UPDATE SQL 语句
40
40
  */
41
- declare function update(sql: string): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
41
+ declare function update(sql: string): (...args: any[]) => any;
42
42
  /**
43
43
  * DELETE 装饰器
44
44
  * 将方法作为 DELETE SQL 使用
45
45
  * @param sql DELETE SQL 语句
46
46
  */
47
- declare function remove(sql: string): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
47
+ declare function remove(sql: string): (...args: any[]) => any;
48
48
  /**
49
49
  * SELECT 装饰器
50
50
  * 将方法作为 SELECT SQL 使用
51
51
  * @param sql SELECT SQL 语句
52
52
  */
53
- declare function select(sql: string): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
53
+ declare function select(sql: string): (...args: any[]) => any;
54
54
  /**
55
55
  * SELECT 结果类型装饰器,和 @select 配合使用
56
56
  * @param dataClass 结果类型
57
57
  */
58
- declare function resultType(dataClass: any): (target: any, propertyKey: string) => void;
58
+ declare function resultType(dataClass: any): (...args: any[]) => any;
59
59
  /**
60
60
  * SQL 参数装饰器,标注 SQL 语句的绑定参数值,和 @select @insert @update @remove 配合使用
61
61
  * @param name 参数在 SQL 语句内的标记值
62
62
  */
63
63
  declare function param(name: string): (target: any, propertyKey: string | symbol, parameterIndex: number) => void;
64
+ /**
65
+ * 方法级参数绑定装饰器(标准装饰器删除了参数装饰器后的替代,legacy 模式也可用)。
66
+ * database 场景映射 SQL 占位符 → 参数索引(值为 number);route 场景映射参数名 → 请求来源(值为 string)。
67
+ * @param mapping 绑定声明,如 { name: 0, id: 1 } 或 { id: "reqParam", body: "reqBody" }
68
+ */
69
+ declare function bind(mapping: Record<string, string | number>): (...args: any[]) => any;
64
70
  /**
65
71
  * SELECT 缓存装饰器,自动缓存 SELECT 查询结果,和 @select 配合使用
66
72
  * 当执行 @insert @update @remove 时,会自动清除缓存。
67
73
  * @param ttl 缓存时间,单位秒
68
74
  */
69
- declare function cache(ttl: number): (target: any, propertyKey: string) => void;
75
+ declare function cache(ttl: number): (...args: any[]) => any;
70
76
  /**模型数据操作类 */
71
77
  declare class Model {
72
78
  /**分页数据 */
@@ -139,23 +145,21 @@ declare class Model {
139
145
  *
140
146
  * 被装饰的类将作为应用程序入口,框架将启动该类的 main 方法
141
147
  */
142
- declare function app<T extends {
143
- new(...args: any[]): {};
144
- }>(constructor: T): void;
148
+ declare function app(...args: any[]): any;
145
149
  /**获取配置文件中的配置项 */
146
150
  declare function config(node: string): any;
147
- /**组件装饰器,被装饰的类可以通过 @autoware 取得实例 */
148
- declare function component(constructorFunction: any): void;
151
+ /**组件装饰器,被装饰的类可以通过 @autoware 取得实例(legacy 类装饰器;2.5.x 起兼容标准 (value, context) 签名) */
152
+ declare function component(...args: any[]): any;
149
153
  /**获取组件实例函数,返回结果同 @autoware */
150
154
  declare function getComponent(constructorFunction: any): any;
151
- /**提供对象装饰器,框架将使用 @bean 装饰的方法来获取对象实例 */
152
- declare function bean(target: any, propertyKey: string): void;
155
+ /**提供对象装饰器,框架将使用 @bean 装饰的方法来获取对象实例(legacy 方法装饰器;2.5.x 起兼容标准签名) */
156
+ declare function bean(...args: any[]): any;
153
157
  /**获取对象实例函数,返回结果由 @bean 装饰的方法提供 */
154
158
  declare function getBean(mappingClass: Function): any;
155
159
  /**配置装饰器,装饰类成员变量值,获取配置文件中的配置项 */
156
160
  declare function value(configPath: string): any;
157
161
  /**自动装配装饰器,无参数版本,被装饰的类成员变量将自动注入实例 */
158
- declare function autoware(target: any, propertyKey: string): void;
162
+ declare function autoware(...args: any[]): any;
159
163
  /**自动装配装饰器,带参数版本,可输入参数作为实例初始化参数,被装饰的类成员变量将自动注入实例 */
160
164
  declare function resource(...args: any[]): any;
161
165
  /**日志函数,输出打印日志 */
@@ -165,11 +169,11 @@ declare function logx(message: any): void;
165
169
  /**错误日志函数,输出打印错误日志 */
166
170
  declare function error(message?: any, ...optionalParams: any[]): void;
167
171
  /**路由页面前置执行装饰器,参数指向路由页面方法,被装饰的方法将在路由页面之前执行 */
168
- declare function before(constructorFunction: any, methodName: string): (target: any, propertyKey: string) => void;
172
+ declare function before(constructorFunction: any, methodName: string): (...args: any[]) => any;
169
173
  /**路由页面后置执行装饰器,参数指向路由页面方法,被装饰的方法将在路由页面之后执行 */
170
- declare function after(constructorFunction: any, methodName: string): (target: any, propertyKey: string) => void;
174
+ declare function after(constructorFunction: any, methodName: string): (...args: any[]) => any;
171
175
  /**定时程序装饰器,参数支持 crontab 格式字符串,可根据参数定时执行被装饰的方法 */
172
- declare function schedule(cronTime: string | Date): (target: any, propertyKey: string) => void;
176
+ declare function schedule(cronTime: string | Date): (...args: any[]) => any;
173
177
  /**RabbitMQ 监听装饰器,参数是监听的队列名称,当接受到消息时将执行被装饰方法 */
174
178
  declare function rabbitListener(queue: string): (target: any, propertyKey: string) => void;
175
179
  /**Redis 监听装饰器,参数是监听的队列名称,当接受到消息时将执行被装饰方法 */
@@ -396,4 +400,4 @@ declare class SocketIo {
396
400
  /**Socket IO 服务实现类 */
397
401
  declare const io: IoServer;
398
402
 
399
- export { ExpressServer, LogDefault, NodeCache, RabbitMQ, rabbitListener, redisSubscriber, ReadWriteDb, Redis, CacheFactory, DataSourceFactory, LogFactory, ServerFactory, AuthenticationFactory, next, reqBody, reqQuery, reqForm, reqParam, req, req as request, res, res as response, component, bean, resource, log, logx, app, before, after, value, error, config, autoware, getBean, getComponent, schedule, getMapping, postMapping, requestMapping, setRouter, upload, jwt, insert, update, remove, select, param, resultType, cache, Model, SocketIo, io };
403
+ export { ExpressServer, LogDefault, NodeCache, RabbitMQ, rabbitListener, redisSubscriber, ReadWriteDb, Redis, CacheFactory, DataSourceFactory, LogFactory, ServerFactory, AuthenticationFactory, next, reqBody, reqQuery, reqForm, reqParam, req, req as request, res, res as response, component, bean, resource, log, logx, app, before, after, value, error, config, autoware, getBean, getComponent, schedule, getMapping, postMapping, requestMapping, setRouter, upload, jwt, insert, update, remove, select, param, bind, resultType, cache, Model, SocketIo, io };
package/src/typespeed.ts CHANGED
@@ -2,6 +2,7 @@ import "reflect-metadata";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import * as walkSync from "walk-sync";
5
+ import { isStd, getStdArgs } from "./decorator-utils";
5
6
 
6
7
  let globalConfig = {};
7
8
  const corePath = __dirname;
@@ -18,7 +19,18 @@ if (fs.existsSync(configFile)) {
18
19
  globalConfig["MAIN_PATH"] = mainPath;
19
20
  globalConfig["CORE_PATH"] = corePath;
20
21
 
21
- function app<T extends { new(...args: any[]): {} }>(constructor: T) {
22
+ function app(...args: any[]): any {
23
+ if (isStd(args)) {
24
+ const [, ctx] = getStdArgs(args);
25
+ ctx.addInitializer(function (this: any) {
26
+ startApp(this.constructor);
27
+ });
28
+ return;
29
+ }
30
+ startApp(args[0]);
31
+ }
32
+
33
+ function startApp(constructor: any) {
22
34
  const coreFiles = walkSync(corePath, { globs: ['**/*.ts'], ignore: ['**/*.d.ts', 'scaffold/**'] });
23
35
  const mainFiles = walkSync(mainPath, { globs: ['**/*.ts'] });
24
36
 
@@ -47,7 +59,23 @@ function config(node: string) {
47
59
  }
48
60
 
49
61
  function value(configPath: string): any {
50
- return function (target: any, propertyKey: string) {
62
+ return function (...args: any[]): any {
63
+ if (isStd(args)) {
64
+ // 标准 field 装饰器:返回 initializer 注入配置值(标准模式下无 design:type)。
65
+ return (initialValue: any) => {
66
+ if (globalConfig === undefined) {
67
+ return initialValue;
68
+ }
69
+ let pathNodes = configPath.split(".");
70
+ let nodeValue = globalConfig;
71
+ for (let i = 0; i < pathNodes.length; i++) {
72
+ nodeValue = nodeValue[pathNodes[i]];
73
+ }
74
+ return nodeValue === undefined ? initialValue : nodeValue;
75
+ };
76
+ }
77
+ const target = args[0];
78
+ const propertyKey = args[1] as string;
51
79
  if (globalConfig === undefined) {
52
80
  Object.defineProperty(target, propertyKey, {
53
81
  get: () => {
@@ -70,19 +98,21 @@ function value(configPath: string): any {
70
98
  }
71
99
 
72
100
  function getRootPath(lines: string[]) {
73
- const macths = ["at Function.Module._load", "at Module.require", "at require", "at Object.<anonymous>"];
101
+ // 兼容新旧 Node 栈帧(Node<22: "Function.Module._load" / Node22+: "Function._load")、
102
+ // mocha(ts-node) 下 "Context.<anonymous>" 与中间可能插入的 "wrapModuleLoad" 帧:
103
+ // 按顺序出现匹配(不要求严格相邻),提高在各种加载器下的容错。
104
+ const macths = [/at Function(\.Module)?\._load/, "at Module.require", /at require\b/, /at (Object|Context)\.<anonymous>/];
74
105
  let matchIndex = 0;
75
106
  for (let line of lines) {
76
- if (line.includes(macths[matchIndex])) {
77
- if(matchIndex === macths.length - 1) {
107
+ const matcher = macths[matchIndex];
108
+ if (matcher instanceof RegExp ? matcher.test(line) : line.includes(matcher)) {
109
+ if (matchIndex === macths.length - 1) {
78
110
  let arr = line.split("(")[1].split(":")
79
111
  arr.pop()
80
112
  arr.pop()
81
113
  return arr.join(':')
82
114
  }
83
115
  matchIndex++;
84
- }else{
85
- matchIndex = 0;
86
116
  }
87
117
  }
88
118
  return undefined;
@@ -92,6 +122,7 @@ export { app, value, config };
92
122
  export * from "./core.decorator";
93
123
  export * from "./route.decorator";
94
124
  export * from "./database.decorator";
125
+ export * from "./bind.decorator";
95
126
 
96
127
  export { default as LogFactory} from "./factory/log-factory.class";
97
128
  export { default as CacheFactory} from "./factory/cache-factory.class";
@@ -0,0 +1,28 @@
1
+ const chaiObj = require('chai');
2
+ chaiObj.use(require("chai-http"));
3
+ const expect = chaiObj.expect;
4
+
5
+ describe("Test @bind and token", () => {
6
+ const testAddr = `http://${process.env.LOCAL_HOST || "localhost"}:8081`;
7
+ it("/bind/param (route @bind)", (done) => {
8
+ chaiObj.request(testAddr).get("/bind/param/100?name=testname").end((err, res) => {
9
+ expect(JSON.parse(res.text)).to.deep.equal({ id: "100", name: "testname" });
10
+ done();
11
+ });
12
+ });
13
+ it("/bind/token (@bean(Token) + @autoware(Token) + @resource(Token, args))", (done) => {
14
+ chaiObj.request(testAddr).get("/bind/token").end((err, res) => {
15
+ expect(res.text).to.equal("test-bean|model-ok");
16
+ done();
17
+ });
18
+ });
19
+ it("/bind/db (database @bind scalar)", (done) => {
20
+ const id = Math.floor(Math.random() * 900000) + 100000;
21
+ chaiObj.request(testAddr).get(`/bind/db?id=${id}`).end((err, res) => {
22
+ expect(res.text).to.match(/^bind insert: \d+/);
23
+ done();
24
+ });
25
+ });
26
+ });
27
+
28
+ export {};
@@ -0,0 +1,42 @@
1
+ import { expect } from "chai";
2
+ import { isStd, getStdArgs } from "../src/decorator-utils";
3
+
4
+ describe("decorator-utils · isStd 双签名判据", () => {
5
+ it("legacy 类装饰器(1 参)→ false", () => {
6
+ expect(isStd([class Foo {}])).to.equal(false);
7
+ });
8
+
9
+ it("legacy 方法/属性装饰器(2 参,第 2 参是 string key)→ false", () => {
10
+ expect(isStd([{}, "methodName"])).to.equal(false);
11
+ });
12
+
13
+ it("legacy 带 descriptor 方法 / 参数装饰器(3 参)→ false", () => {
14
+ expect(isStd([{}, "methodName", {}])).to.equal(false);
15
+ expect(isStd([{}, "methodName", 0])).to.equal(false);
16
+ });
17
+
18
+ it("标准类/方法/field 装饰器(2 参,第 2 参带 string kind)→ true", () => {
19
+ expect(isStd([function () {}, { kind: "class" }])).to.equal(true);
20
+ expect(isStd([function () {}, { kind: "method" }])).to.equal(true);
21
+ expect(isStd([undefined, { kind: "field" }])).to.equal(true);
22
+ expect(isStd([{}, { kind: "accessor" }])).to.equal(true);
23
+ });
24
+
25
+ it("第 2 参是对象但无 kind → false", () => {
26
+ expect(isStd([{}, {}])).to.equal(false);
27
+ });
28
+
29
+ it("第 2 参是 null / 非对象 → false", () => {
30
+ expect(isStd([{}, null])).to.equal(false);
31
+ expect(isStd([{}, 42])).to.equal(false);
32
+ });
33
+
34
+ it("getStdArgs 解出 (value, context)", () => {
35
+ const ctx = { kind: "method", name: "foo" };
36
+ const [value, context] = getStdArgs([function bar() {}, ctx]);
37
+ expect(typeof value).to.equal("function");
38
+ expect(context).to.equal(ctx);
39
+ });
40
+ });
41
+
42
+ export {};
@@ -0,0 +1,31 @@
1
+ services:
2
+ mysql:
3
+ image: mysql:8.0
4
+ container_name: typespeed-test-mysql
5
+ environment:
6
+ MYSQL_ROOT_PASSWORD: root
7
+ MYSQL_DATABASE: test
8
+ ports:
9
+ - "3306:3306"
10
+ volumes:
11
+ - ./mysql-init:/docker-entrypoint-initdb.d
12
+ healthcheck:
13
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-proot"]
14
+ interval: 3s
15
+ timeout: 3s
16
+ retries: 20
17
+
18
+ redis:
19
+ image: redis:7
20
+ container_name: typespeed-test-redis
21
+ ports:
22
+ - "6379:6379"
23
+
24
+ rabbitmq:
25
+ image: rabbitmq:3.8
26
+ container_name: typespeed-test-rabbitmq
27
+ environment:
28
+ RABBITMQ_DEFAULT_USER: guest
29
+ RABBITMQ_DEFAULT_PASS: guest
30
+ ports:
31
+ - "5672:5672"
@@ -0,0 +1,8 @@
1
+ -- 与 CI 的 Setup MySQL 步骤一致:建 test 库 + user 表
2
+ CREATE DATABASE IF NOT EXISTS test DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
3
+ USE test;
4
+ CREATE TABLE IF NOT EXISTS `user` (
5
+ `id` int(11) NOT NULL AUTO_INCREMENT,
6
+ `name` varchar(100) DEFAULT NULL,
7
+ PRIMARY KEY (`id`)
8
+ ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
@@ -0,0 +1,69 @@
1
+ # 本地 Docker 测试方案
2
+
3
+ typespeed 测试依赖三个外部服务(MySQL / Redis / RabbitMQ),本机没有常驻服务时用 Docker 起一套与 CI 一致的环境来跑测试。
4
+
5
+ ## 一、前置条件
6
+
7
+ - 本机已安装 **OrbStack**(或 Docker Desktop),Docker CLI 可用:`docker --version`
8
+ - 若 OrbStack 未运行,先启动它(菜单栏点开即可,或 `open -a OrbStack`)
9
+ - 端口占用:默认映射 `3306 / 6379 / 5672`,起容器前确认空闲(`nc -z localhost 3306` 等)
10
+
11
+ ## 二、一键启动测试依赖
12
+
13
+ ```bash
14
+ cd /Users/zzz/book/typespeed
15
+ docker compose -f test-env/docker-compose.test.yml up -d
16
+ ```
17
+
18
+ 启动三个容器:
19
+
20
+ | 服务 | 容器名 | 端口 | 账号 |
21
+ |------|--------|------|------|
22
+ | MySQL 8.0 | typespeed-test-mysql | 127.0.0.1:3306 | root / root,库 `test` |
23
+ | Redis 7 | typespeed-test-redis | 127.0.0.1:6379 | 无 |
24
+ | RabbitMQ 3.8 | typespeed-test-rabbitmq | 127.0.0.1:5672 | guest / guest |
25
+
26
+ MySQL 首次启动会自动执行 `test-env/mysql-init/init.sql`(建 `test` 库 + `user` 表,与 CI 的 Setup MySQL 步骤一致)。
27
+
28
+ 检查就绪状态:
29
+
30
+ ```bash
31
+ docker compose -f test-env/docker-compose.test.yml ps # 三容器 Up 且 mysql 为 healthy
32
+ docker exec typespeed-test-mysql mysql -uroot -proot -e "USE test; SHOW TABLES;"
33
+ docker exec typespeed-test-redis redis-cli ping # PONG
34
+ docker exec typespeed-test-rabbitmq rabbitmq-diagnostics -q ping # Ping succeeded
35
+ ```
36
+
37
+ ## 三、跑测试
38
+
39
+ ```bash
40
+ npm test
41
+ ```
42
+
43
+ 预期:`51 passing`。
44
+
45
+ > 说明:`package.json` 的 test 脚本已加 `NODE_OPTIONS=--no-experimental-strip-types`,这是 Node 22 下 mocha 加载 CJS 风格 .ts 测试的必需项(否则报 `require is not defined`)。
46
+
47
+ ## 四、收尾
48
+
49
+ ```bash
50
+ docker compose -f test-env/docker-compose.test.yml down # 停容器(保留数据卷需加 -v)
51
+ ```
52
+
53
+ ## 五、本次排障记录(2026-09-06)
54
+
55
+ 本地 `npm test` 曾长期 0 passing / 47 failing,所有请求返回 404 错误页(`src/default/pages/404.html`,即 MIT License 模板页)。曾误判为"缺本地服务"(环境差异),用 Docker 起齐服务后依然全挂,最终定位为**框架根因**:
56
+
57
+ 1. `src/typespeed.ts` 的 `getRootPath()` 靠匹配调用栈推断应用根目录(`mainPath`),原匹配写死旧版 Node 栈帧格式 `at Function.Module._load` 和 `at Object.<anonymous>`:
58
+ - Node 22 的帧是 `at Function._load`(少了 `Module.`);
59
+ - mocha 下 hooks 的帧是 `at Context.<anonymous>`(不是 `Object.`),且 `Function._load` 与 `Module.require` 之间会插入 `wrapModuleLoad` 帧。
60
+ - 匹配全部落空 → `mainPath` 回退成 mocha 的 bin 目录 → config 不加载、app/src 下组件不加载、路由不注册 → 一切请求 404。
61
+ 2. 修复:`getRootPath` 改为**按顺序出现匹配**(不要求帧严格相邻),帧格式用正则兼容新旧 Node 与 mocha。改动见 `src/typespeed.ts`(未提交)。
62
+ 3. 修复后本地 `npm test` 全绿 **51 passing (3s)**。
63
+ 4. 连带影响:CI 若从 node 16 升到 node 22(见 0.4 改动),旧的栈匹配同样会失效——本修复对 CI 也是必需的。
64
+
65
+ ## 六、附:docker-compose.test.yml 内容要点
66
+
67
+ - 服务定义与 CI(`.github/workflows/test.yml`)的 services 保持一致:mysql root/root + `test` 库、redis、rabbitmq guest/guest。
68
+ - MySQL 加了 healthcheck,避免测试启动时库还没就绪。
69
+ - 初始化 SQL 挂在 `test-env/mysql-init/init.sql`,与 CI 建表语句一致。