lakutata 3.0.0 → 3.1.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.
Files changed (117) hide show
  1. package/README.md +50 -30
  2. package/README.zh-CN.md +130 -0
  3. package/dist/cjs/exports/QueueComponent.d.ts +1 -0
  4. package/dist/cjs/exports/QueueComponent.js +20 -0
  5. package/dist/esm/exports/QueueComponent.js +1 -0
  6. package/dist/types/exports/QueueComponent.d.ts +1 -0
  7. package/doc/en/Application.md +107 -0
  8. package/doc/en/CLI.md +274 -0
  9. package/doc/en/Cache.md +379 -0
  10. package/doc/en/Command.md +110 -0
  11. package/doc/en/ContainerdIntegration.md +17 -16
  12. package/doc/en/Controllers.md +256 -0
  13. package/doc/en/DBus.md +291 -0
  14. package/doc/en/DTOValidation.md +72 -43
  15. package/doc/en/Database.md +188 -0
  16. package/doc/en/DatabaseDrivers.md +287 -0
  17. package/doc/en/Decorator.md +52 -40
  18. package/doc/en/DockerIntegration.md +7 -6
  19. package/doc/en/Entities.md +240 -0
  20. package/doc/en/Entrypoint.md +216 -0
  21. package/doc/en/Exceptions.md +174 -0
  22. package/doc/en/GRPC.md +251 -0
  23. package/doc/en/GettingStarted.md +165 -0
  24. package/doc/en/HTTP.md +507 -0
  25. package/doc/en/HelperFunctions.md +11 -11
  26. package/doc/en/I18n.md +119 -115
  27. package/doc/en/Injection.md +144 -0
  28. package/doc/en/Installers.md +280 -0
  29. package/doc/en/Logger.md +206 -0
  30. package/doc/en/MCP.md +293 -0
  31. package/doc/en/Middlewares.md +213 -0
  32. package/doc/en/Migrations.md +202 -0
  33. package/doc/en/MongoEmbedded.md +149 -0
  34. package/doc/en/Monitor.md +164 -0
  35. package/doc/en/Mutex.md +27 -25
  36. package/doc/en/NATS.md +246 -0
  37. package/doc/en/NativeAddons.md +212 -0
  38. package/doc/en/Objects.md +143 -0
  39. package/doc/en/Overview.md +62 -85
  40. package/doc/en/Packaging.md +193 -0
  41. package/doc/en/ProjectStructure.md +124 -0
  42. package/doc/en/Queue.md +269 -0
  43. package/doc/en/Schedules.md +217 -0
  44. package/doc/en/Security.md +10 -9
  45. package/doc/en/ServiceCalls.md +504 -0
  46. package/doc/en/Time.md +19 -20
  47. package/doc/en/UpgradingFrom1.md +435 -0
  48. package/doc/en/UpgradingFrom2.md +315 -0
  49. package/doc/en/Workspaces.md +189 -0
  50. package/doc/en/examples/BackgroundJobs.md +677 -0
  51. package/doc/en/examples/CommandLineTool.md +664 -0
  52. package/doc/en/examples/DesktopApp.md +809 -0
  53. package/doc/en/examples/DeviceService.md +607 -0
  54. package/doc/en/examples/MCPServer.md +653 -0
  55. package/doc/en/examples/README.md +16 -0
  56. package/doc/en/examples/RestAPI.md +490 -0
  57. package/doc/en/examples/ServiceCalls.md +829 -0
  58. package/doc/en/examples/WebApp.md +568 -0
  59. package/doc/zh/Application.md +75 -0
  60. package/doc/zh/CLI.md +250 -0
  61. package/doc/zh/Cache.md +328 -0
  62. package/doc/zh/Command.md +78 -0
  63. package/doc/zh/ContainerdIntegration.md +9 -9
  64. package/doc/zh/Controllers.md +228 -0
  65. package/doc/zh/DBus.md +257 -0
  66. package/doc/zh/DTOValidation.md +49 -19
  67. package/doc/zh/Database.md +175 -0
  68. package/doc/zh/DatabaseDrivers.md +261 -0
  69. package/doc/zh/Decorator.md +25 -18
  70. package/doc/zh/DockerIntegration.md +6 -6
  71. package/doc/zh/Entities.md +224 -0
  72. package/doc/zh/Entrypoint.md +193 -0
  73. package/doc/zh/Exceptions.md +158 -0
  74. package/doc/zh/GRPC.md +226 -0
  75. package/doc/zh/GettingStarted.md +148 -0
  76. package/doc/zh/HTTP.md +445 -0
  77. package/doc/zh/HelperFunctions.md +2 -2
  78. package/doc/zh/I18n.md +26 -26
  79. package/doc/zh/Injection.md +129 -0
  80. package/doc/zh/Installers.md +232 -0
  81. package/doc/zh/Logger.md +188 -0
  82. package/doc/zh/MCP.md +265 -0
  83. package/doc/zh/Middlewares.md +198 -0
  84. package/doc/zh/Migrations.md +178 -0
  85. package/doc/zh/MongoEmbedded.md +118 -0
  86. package/doc/zh/Monitor.md +145 -0
  87. package/doc/zh/Mutex.md +10 -10
  88. package/doc/zh/NATS.md +207 -0
  89. package/doc/zh/NativeAddons.md +180 -0
  90. package/doc/zh/Objects.md +119 -0
  91. package/doc/zh/Overview.md +46 -71
  92. package/doc/zh/Packaging.md +154 -0
  93. package/doc/zh/ProjectStructure.md +102 -0
  94. package/doc/zh/Queue.md +214 -0
  95. package/doc/zh/Schedules.md +193 -0
  96. package/doc/zh/Security.md +6 -6
  97. package/doc/zh/ServiceCalls.md +459 -0
  98. package/doc/zh/Time.md +2 -2
  99. package/doc/zh/UpgradingFrom1.md +347 -0
  100. package/doc/zh/UpgradingFrom2.md +230 -0
  101. package/doc/zh/Workspaces.md +150 -0
  102. package/doc/zh/examples/BackgroundJobs.md +618 -0
  103. package/doc/zh/examples/CommandLineTool.md +618 -0
  104. package/doc/zh/examples/DesktopApp.md +748 -0
  105. package/doc/zh/examples/DeviceService.md +546 -0
  106. package/doc/zh/examples/MCPServer.md +604 -0
  107. package/doc/zh/examples/README.md +14 -0
  108. package/doc/zh/examples/RestAPI.md +458 -0
  109. package/doc/zh/examples/ServiceCalls.md +765 -0
  110. package/doc/zh/examples/WebApp.md +520 -0
  111. package/package.json +46 -17
  112. package/doc/en/ActionPattern.md +0 -408
  113. package/doc/en/Core.md +0 -312
  114. package/doc/en/ORMIntegration.md +0 -341
  115. package/doc/zh/ActionPattern.md +0 -334
  116. package/doc/zh/Core.md +0 -256
  117. package/doc/zh/ORMIntegration.md +0 -319
@@ -0,0 +1,347 @@
1
+ # 从 Lakutata 1.x 升级
2
+
3
+ Lakutata 1.x 是一组包:`@lakutata/core` 1.x、模块 `@lakutata-module/*`(`service`、`api`)、组件 `@lakutata-component/*`(`typeorm`、`cacher` 等)以及 `@lakutata/cli` 1.0。Lakutata 3 是 API 完全不同的另一个框架,所以 1.x 项目要移植到 3.x,而不是原地升级。本页说明移植的方法,以及 1.x 的各个部分在 3.x 中对应什么。
4
+
5
+ 本页只适用于依赖 1.x 版本 `@lakutata/core` 的项目。依赖 `lakutata` 包 2.x 版本的项目请看 [UpgradingFrom2](UpgradingFrom2.md),不要按本页升级。本页涉及的 1.x 包是 npm 上发布的版本(`@lakutata/core` 1.0.60、`@lakutata-module/service` 1.2.7、`@lakutata-module/api` 1.0.29、`@lakutata-component/typeorm` 1.1.19、`@lakutata-component/cacher` 1.0.15、`@lakutata/cli` 1.0.37);从其他地方获得的这些包的构建版本,可能有本页没有涉及的 API。
6
+
7
+ ## 1. 识别 1.x 项目
8
+
9
+ | 特征 | 1.x | 3.x |
10
+ |---|---|---|
11
+ | 依赖 | `@lakutata/core` `^1…`、`@lakutata-module/service`、`@lakutata-component/*` | `lakutata` `^3…` |
12
+ | `src/App.ts` | `createApp(Config(true)).then(…)` | `Application.run(config)` |
13
+ | 模块系统 | CommonJS:导入不带扩展名,使用 `__dirname` | ES 模块:相对导入以 `.js` 结尾 |
14
+ | 服务动作 | `@lakutata-module/service` 的 `@Controller({_ctrl: …})` 和 `@PATTERN({_act: …})` | `lakutata/decorator/ctrl` 的 `@ServiceAction({…})` |
15
+ | 校验 | 交给 `@Accept` 的 `Validator` schema(yup) | 带 `@Expect` 的 DTO 类 |
16
+ | Node.js | `>=14.15.1`(`@lakutata/core` 1.0.60) | `^22.13.0 \|\| >=24.11.0` |
17
+
18
+ **3.x 的 `@lakutata/core` 不是 1.x 那个库的新版本。** 它是 Lakutata 3 的核心,只是用了同一个名字,API 完全不同。在 1.x 项目中执行 `npm install @lakutata/core@latest`,或者使用 `*` 这样的版本范围,都会用它替换 1.x 的核心库,导致构建失败。不要逐个升级 1.x 的包:移植后的应用依赖 `lakutata` 3,它会带来自己的 `@lakutata/core`。
19
+
20
+ ## 2. 移植的方法
21
+
22
+ 1. **创建一个 3.x 项目**,放在 1.x 项目旁边,选用与它同类的模板:服务或网关用 `service`(带 HTTP API、健康检查和 `Dockerfile`),命令行工具用 `cli`(见 [GettingStarted](GettingStarted.md)):
23
+
24
+ ```bash
25
+ npx @lakutata/cli create --name orders --template service
26
+ ```
27
+
28
+ 2. **逐个部分移植**,每移植一部分就构建和测试一次:数据库及其实体(第 8 节)、作为 DTO 的输入(第 7 节)、组件(第 3 节)、服务动作及其调用方(第 5 节)、HTTP API(第 6 节)、配置(第 9 节)、测试(第 10 节)。
29
+ 3. **保持对客户端的约定不变**:服务动作的模式及其中的键、`errno` 错误码、HTTP 路由及其响应的格式、事件的名称。这样调用方、用户界面和设备都不需要随移植而改动。
30
+ 4. **规划各个服务的移植顺序。** 1.x 的服务模块和 3.x 的服务传输方式互不相通:3.x 服务既不能响应 1.x 服务的 `invoke()`,也不能调用 1.x 服务。请把互相调用的服务一起移植,或者保留一个 1.x 服务作为桥梁,直到它的调用方都移植完成。
31
+
32
+ 直接修改 1.x 项目是行不通的:模块系统、容器、校验和传输方式全都同时改变了。
33
+
34
+ ## 3. 应用及其对象
35
+
36
+ 见 [Application](Application.md)、[Objects](Objects.md) 和 [Injection](Injection.md)。
37
+
38
+ | 1.x | 3.x |
39
+ |---|---|
40
+ | `createApp(Config(isProd))`,返回应用的 promise | `Application.run(config).onLaunched(…)` |
41
+ | 选项 `id`、`name`、`timezone`、`components: {name: {class, …}}` | 同样的选项 |
42
+ | `modules: {name: {class, …}}` | `components`:长期存在的对象就是 `Component` |
43
+ | `plugins: [{class, scope, …}]`,通过 `app.<类名>` 访问 | `providers`(共享的客户端用 `@Singleton()`)或 `components`,按名字注入 |
44
+ | `processes`、`threads`、`workflows`、`onModulesLoaded`、`onComponentsLoaded` | 没有对应项:使用 `bootstrap` 步骤,并在组件中使用 Node.js 的 `child_process` 或 `worker_threads` |
45
+ | `initialize()` | `init()`;3.x 新增了销毁时运行的 `destroy()` |
46
+ | 组件中的 `app.on('ready', …)` | 组件的 `launched()` 钩子,或者应用的 `onLaunched` |
47
+ | `@Inject('name')`、`@Inject(Application)` | 相同,来自 `lakutata/decorator/di` |
48
+ | `app.Components.get<T>('name')`、`app.Modules.service`,以及为它们声明类型的 `declare module` | 在带类型的属性上使用 `@Inject('name')`;在对象之外使用 `await app.getObject<T>('name')`;不再需要类型扩充 |
49
+ | `@Configurable()` | `@Configurable(schema)`:启动时用 schema 校验选项(不给 schema 时接受任何值) |
50
+ | 全局的 `getApp()` | `@Inject(Application)` |
51
+ | 测试中的 `new SomeComponent()` | 不要这样做:使用 `await new Container().set(SomeComponent, options)` |
52
+ | `app.Logger.info`、`.warning`、`.error`、`.debug` 等(winston) | `@Inject('log')` 注入 `ILogger`:`info`、`warn`、`error`、`debug`、`trace`(`emerg`、`alert`、`crit` 改为 `error`,`notice` 改为 `info`) |
53
+ | `app.Security.MD5()`、`SHA1()`、`SHA256()` | `lakutata/utils` 中的 `md5()`、`sha1()`、`sha256()`,它们返回 `Buffer`(用 `.toString('hex')` 转换);带密钥时使用 `lakutata/security` 中的 `hmac()` |
54
+ | `app.Security.generateRandomString()` | `lakutata/utils` 中的 `randomString()` |
55
+ | `app.Security.encryptByPassword()`、`encryptByKey()` 及对应的 `decrypt…` | `lakutata/security` 中的同名函数(见 [Security](Security.md)) |
56
+ | `app.Security.generatePasswordHash()`、`validatePassword()` | `lakutata/provider/passwordHash` 中的 `PasswordHash` provider |
57
+ | `app.Formatter.asTimestamp()`、`asDateText()` | `lakutata` 中的 `Time`(见 [Time](Time.md)) |
58
+ | `app.JSON`、`app.MessagePack`、`app.HttpRequest`、`app.Shell` | `JSON`、`lakutata/utils` 中的 `MessagePack`、`fetch()`、Node.js 的 `child_process` |
59
+ | `delay()` | `lakutata/utils` 中的 `delay()` |
60
+ | 用 `app.emit()`、`app.on()` 作为事件总线 | 组件本身就是事件发射器:注入它,使用它的 `emit()` 和 `on()` |
61
+ | `throw this.generateException(OrderNotFoundException, 'Order', 'not found')` | `throw new OrderNotFoundException('Order {id} not found', {id})`;异常不再有 `district` |
62
+
63
+ 一个 1.x 组件:
64
+
65
+ ```text
66
+ //1.x
67
+ export class OrderComponent extends Component {
68
+ @Inject(Application) protected readonly app: Application
69
+ @Inject('db') protected readonly db: TypeOrmComponent
70
+ @Configurable() protected readonly keepDays: number = 30
71
+
72
+ protected async initialize(): Promise<void> {
73
+ this.app.on('ready', () => this.startCleanup())
74
+ }
75
+ }
76
+ ```
77
+
78
+ 在 3.x 中,同一个组件:
79
+
80
+ ```typescript
81
+ import {Component, DTO, Exception, type ILogger} from 'lakutata'
82
+ import {Configurable, Inject} from 'lakutata/decorator/di'
83
+ import type {Database} from 'lakutata/com/database'
84
+
85
+ export class OrderNotFoundException extends Exception {
86
+ public errno: string = 'E_ORDER_NOT_FOUND'
87
+
88
+ public readonly statusCode: number = 404
89
+ }
90
+
91
+ export class Orders extends Component {
92
+ @Inject('db')
93
+ protected readonly db: Database
94
+
95
+ @Inject('log')
96
+ protected readonly log: ILogger
97
+
98
+ @Configurable(DTO.Number().integer().min(1))
99
+ protected readonly keepDays: number = 30
100
+
101
+ protected timer: NodeJS.Timeout
102
+
103
+ //应用启动完成之后(1.x 中是 app.on('ready'))
104
+ protected async launched(): Promise<void> {
105
+ this.timer = setInterval((): void => this.log.info('Keeping %d days of orders', this.keepDays), 3600000)
106
+ }
107
+
108
+ protected async destroy(): Promise<void> {
109
+ clearInterval(this.timer)
110
+ }
111
+
112
+ public async get(id: string): Promise<Record<string, unknown>> {
113
+ const rows: Record<string, unknown>[] = await this.db.query('SELECT * FROM orders WHERE id = ?', [id])
114
+ if (!rows.length) throw new OrderNotFoundException('Order {id} not found', {id: id})
115
+ return rows[0]
116
+ }
117
+ }
118
+ ```
119
+
120
+ - 异常的 `errno` 保持不变,所以检查它的客户端照常工作。3.x 新增了 `statusCode`,即错误的 HTTP 状态码,并用模板生成错误消息(见 [Exceptions](Exceptions.md))。
121
+ - 同一个类仍然可以用不同的名字、不同的选项声明多次(多个缓存、两个 HTTP 服务器):每个声明都是独立的实例,按名字注入。
122
+
123
+ ## 4. 包的对应关系
124
+
125
+ | 1.x | 3.x |
126
+ |---|---|
127
+ | `@lakutata/core` 1.x | `lakutata`:`lakutata`、`lakutata/decorator/di`、`lakutata/decorator/dto`、`lakutata/utils` |
128
+ | `@lakutata-module/service` | `lakutata/com/entrypoint`(`@ServiceAction` 和服务传输方式)、`@lakutata/nats`(第 5 节) |
129
+ | `@lakutata-module/api` | `lakutata/adapter/express` 或 `lakutata/adapter/fastify`,配合 `@GET`、`@POST` 等(第 6 节) |
130
+ | `@lakutata-component/typeorm` | `lakutata/com/database`、`lakutata/orm`、`lakutata/decorator/orm`(第 8 节) |
131
+ | `@lakutata-component/cacher` | `lakutata/com/cacher`(第 8 节) |
132
+ | `@lakutata/cli` 1.0(`lakutata`、`lakutata-deploy`) | `@lakutata/cli` 3:`lakutata create`、`run`、`migration`、`package` 等(见 [Command](Command.md));没有部署命令 |
133
+ | 其他 `@lakutata-component/*` 包 | 没有对应项:在应用中自行实现它们的功能,或者改用其他库 |
134
+
135
+ 只能从这些子路径导入:3.x 不支持从包内部的文件导入(1.x 项目有时会从包的 `/distro/…`、`/src/…` 或 `/build/…` 导入)。
136
+
137
+ ## 5. 服务调用
138
+
139
+ 在 1.x 中,每个应用都有一个 `service` 模块:它把自己的控制器注册到一个注册中心应用(基于 Redis),用 `invoke({serviceId, data, timeout})` 调用其他应用的动作;它的通道(channel)在服务之间传递事件。3.x 没有注册中心应用:`entrypoint` 组件通过某种传输方式提供服务动作,由代理调用它们(见 [ServiceCalls](ServiceCalls.md)):
140
+
141
+ - **NATS**(见 [NATS](NATS.md))最接近 1.x 的模型:各个服务共用一个 NATS 服务器,按应用 id(相当于 1.x 的 `serviceId`)互相寻址,调用在一个服务的多个实例之间分摊,并且可以发布事件。它的 `ServiceProxy` 调用其他服务;NATS 服务器取代了注册中心应用。
142
+ - **HTTP 服务**配合 `HTTPServiceProxy`,或者 **gRPC**(见 [GRPC](GRPC.md)),按已知的地址调用服务。
143
+
144
+ 1.x 服务模块中的一个控制器:
145
+
146
+ ```text
147
+ //1.x
148
+ @Controller({_ctrl: 'order'})
149
+ export class OrderController extends BaseController {
150
+ @Inject('order') protected readonly order: OrderComponent
151
+
152
+ @PATTERN({_act: 'get'})
153
+ @Accept(TGetOrderOptionsSchema)
154
+ public async get(inp: TPattern & TGetOrderOptions): Promise<IOrder> {
155
+ return await this.order.get(inp.id)
156
+ }
157
+ }
158
+ ```
159
+
160
+ 在 3.x 中,同样的动作通过 NATS 提供,并用一个代理调用另一个服务:
161
+
162
+ ```typescript
163
+ import {Application, DTO} from 'lakutata'
164
+ import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
165
+ import {Prefix, ServiceAction} from 'lakutata/decorator/ctrl'
166
+ import {Expect} from 'lakutata/decorator/dto'
167
+ import {buildNatsClientOptions, buildServiceProxy, natsServiceEntrypoint} from '@lakutata/nats'
168
+
169
+ class GetOrderDTO extends DTO {
170
+ @Expect(DTO.String().required())
171
+ public id: string
172
+ }
173
+
174
+ //模式中的键可以任意取:调用方仍然发送 {_ctrl: 'order', _act: 'get', id}
175
+ @Prefix({pattern: {_ctrl: 'order'}})
176
+ class OrderController extends Controller {
177
+ @ServiceAction({_act: 'get'}, GetOrderDTO)
178
+ public async get(input: GetOrderDTO): Promise<{ id: string }> {
179
+ return {id: input.id}
180
+ }
181
+ }
182
+
183
+ Application.run(() => ({
184
+ //其他服务调用时使用的 id:即 1.x 的 serviceId
185
+ id: 'orders.app',
186
+ name: 'orders',
187
+ components: {
188
+ nats: buildNatsClientOptions({servers: process.env.NATS_URL ?? 'nats://127.0.0.1:4222'}),
189
+ entrypoint: buildEntrypoints({
190
+ controllers: [OrderController],
191
+ service: natsServiceEntrypoint('nats')
192
+ })
193
+ },
194
+ providers: {
195
+ //另一个服务:await this.users.invoke({_ctrl: 'user', _act: 'get', id}, 30000)
196
+ users: buildServiceProxy({natsComponentName: 'nats', serviceId: 'users.app'})
197
+ },
198
+ bootstrap: ['nats', 'entrypoint']
199
+ }))
200
+ ```
201
+
202
+ - **模式。** `@Prefix({pattern})` 加上 `@ServiceAction` 取代 `@Controller` 加上 `@PATTERN`,使用同样的键,所以调用方发送的消息不需要改变(用 `_action` 代替 `_act` 也一样)。模式中额外的键仍然会选中不同的动作:`{_act: 'query', batch: true}` 和 `{_act: 'query', batch: false}` 是两个动作。前缀和动作给同一个键设置不同的值时,3.x 会报错(1.x 中以控制器的值为准)。
203
+ - **输入。** 动作收到的消息中不含它的模式中的键:原来读取 `inp.batch` 或 `inp._act` 的方法,要改为从完整的消息 `this.context.input` 中读取。网关给每条消息加上的字段(用户的 id、组织的 id)仍然作为 DTO 的普通字段,或者改由中间件处理(见 [Middlewares](Middlewares.md))。
204
+ - **调用。** `this.app.Modules.service.invoke({serviceId, data, timeout})` 改为对注入的代理调用 `this.users.invoke(message, timeout)`。动作失败时抛出 `ServiceInvokeException`,它带有远端错误的 `errno` 和其他属性。`transform` 选项和由多个调用对象组成的调用链没有对应项:请用代码实现;`service.parallel(…)` 改为对多个调用使用 `Promise.all()`。1.x 的调用默认等待 60 秒,3.x 的代理默认等待 10 分钟:请给调用指定超时时间。
205
+ - **事件。** 通道的 `triggerAll(event, …args)` 改为 NATS 组件的 `nats.emitServiceEvent(event, …args)`,每个实例都通过 `proxy.on(event, listener)` 或 `nats.onServiceEvent(serviceId, event, listener)` 接收。`triggerOne` 的效果是每个服务只有一个实例收到事件:请发布到自己的主题上(`nats.publish(subject, payload)`),并以服务的 id 作为队列组订阅(`nats.subscribe(subject, handler, {queue: app.appId})`),同一个服务的各个实例共用这个队列组。
206
+ - **由多个实例分担的工作。** 必须由服务的某一个实例处理的工作(例如处理一个数据流),适合使用消息队列(见 [Queue](Queue.md)):同一个组的各个实例分摊队列中的消息,并支持重试。
207
+ - **时钟服务。** 原来向其他服务发布定时信号的应用,改为在每个服务中使用 `@Schedule`;只能在一个实例中运行的任务加上 `exclusive`(见 [Schedules](Schedules.md))。
208
+
209
+ ## 6. HTTP API
210
+
211
+ 1.x 的 `api` 模块改为入口的 HTTP 适配器 `expressHTTPEntrypoint()` 或 `fastifyHTTPEntrypoint()`(见 [HTTP](HTTP.md))。一个路由就是一个带 `@GET`、`@POST` 等装饰器的控制器方法;它的输入是一个对象:查询参数和请求体,路由参数覆盖在它们之上,由它的 DTO 校验。1.x 的参数装饰器(请求体、查询参数、某个请求头、请求和响应)由 DTO 和 `this.context` 取代,后者是带有 `request` 和 `response` 的 `HTTPContext`。
212
+
213
+ 只要响应保持原来的格式,客户端就能照常工作。自定义的响应器按 1.x 客户端期望的格式写出响应,例如 `{code, payload}`,结果的 `code` 为 0:
214
+
215
+ ```typescript
216
+ import {Application, DTO} from 'lakutata'
217
+ import {buildEntrypoints, Controller, HTTPContext} from 'lakutata/com/entrypoint'
218
+ import {POST} from 'lakutata/decorator/ctrl'
219
+ import {Expect} from 'lakutata/decorator/dto'
220
+ import {describeError, expressHTTPEntrypoint, statusOfError, type ExpressResponder} from 'lakutata/adapter/express'
221
+ import type {Request, Response} from 'express'
222
+
223
+ //按 1.x 客户端读取的格式响应:结果为 {code: 0, payload},错误的 code 不为 0
224
+ const legacyEnvelope: ExpressResponder = {
225
+ success(response: Response, data: unknown): void {
226
+ response.status(200).json({code: 0, payload: data ?? null})
227
+ },
228
+ error(response: Response, error: unknown): void {
229
+ response.status(statusOfError(error)).json({code: 1, payload: describeError(error)})
230
+ }
231
+ }
232
+
233
+ class QueryDTO extends DTO {
234
+ @Expect(DTO.String().required())
235
+ public method: string
236
+ }
237
+
238
+ class RpcController extends Controller {
239
+ //POST /rpc?method=order.get,带 JSON 请求体
240
+ @POST('/rpc', QueryDTO)
241
+ public async call(input: QueryDTO): Promise<{ method: string, token?: string }> {
242
+ //从上下文的请求中读取一个请求头
243
+ const request: Request = (this.context as HTTPContext).request as Request
244
+ return {method: input.method, token: request.header('x-token')}
245
+ }
246
+ }
247
+
248
+ Application.run(() => ({
249
+ id: 'gateway.app',
250
+ name: 'gateway',
251
+ components: {
252
+ entrypoint: buildEntrypoints({
253
+ controllers: [RpcController],
254
+ http: expressHTTPEntrypoint({port: 8080, respond: legacyEnvelope})
255
+ })
256
+ },
257
+ bootstrap: ['entrypoint']
258
+ }))
259
+ ```
260
+
261
+ - `statusOfError()` 和 `describeError()` 给出适配器本来会发送的状态码和 `{errno, errMsg}`;响应器决定响应的格式。1.x 的 `api` 模块的响应是 `{statusCode, code, error, payload, timestamp, server}`(或者经它的 `responseFormatter` 处理后的格式):请写出客户端读取的那些字段。`jsonEnvelope()` 给出的则是 3.x 的封套 `{errno, errMsg, data}`。
262
+ - `api` 模块允许任何来源的跨域请求(CORS),并对响应签名(`x-signature` 系列响应头)。3.x 的适配器不发送 CORS 响应头,除非用中间件加上(见 [HTTP](HTTP.md) 第 7 节);校验签名的客户端需要一个加上这些响应头的中间件。
263
+ - 1.x 组件自己启动的服务器(为 Socket.IO 启动的 HTTP 服务器、其他 Web 框架)可以继续留在组件中,在 `startup()` 中启动,在 `destroy()` 中关闭。`socketIOServiceEntrypoint()` 也可以共用 HTTP 适配器的服务器,通过 Socket.IO 提供服务动作(见 [ServiceCalls](ServiceCalls.md) 第 4 节)。
264
+
265
+ ## 7. 校验
266
+
267
+ 1.x 的 `Validator` 生成 yup schema;3.x 用 DTO 类校验,DTO 的 schema 采用 joi 的 API(见 [DTOValidation](DTOValidation.md))。1.x 中成对出现的 schema 和类型,合并成一个 DTO 类:
268
+
269
+ | 1.x(`Validator`,yup) | 3.x(`DTO`) |
270
+ |---|---|
271
+ | `Validator.Object({…})` 和一个 `T…Options` 类型 | 一个继承 `DTO` 的类,每个属性一个 `@Expect` |
272
+ | `Validator.String`、`.Number`、`.Boolean`、`.Date`、`.Array(of)`、`.Any` | `DTO.String()`、`DTO.Number()`、`DTO.Boolean()`、`DTO.Date()`、`DTO.Array(of)`、`DTO.Any()` |
273
+ | `Validator.Integer`、`Validator.Email` | `DTO.Number().integer()`、`DTO.String().email()` |
274
+ | `.required()` | `.required()` |
275
+ | `.notRequired()`、`.optional()` | `.optional()` |
276
+ | `.nullable()` | `.allow(null)` |
277
+ | `.oneOf([…])` | `.valid(…)` |
278
+ | `.matches(regex)` | `.pattern(regex)` |
279
+ | `.test(fn)` | `.custom(fn)` |
280
+ | `.when(…)` | `.when(…)`(使用 joi 的选项) |
281
+ | `schema.concat(other)` | 继承另一个 DTO,或者使用 `.concat()` |
282
+ | 方法上的 `@Accept(schema, {stripUnknown})` | 方法上的 `@Accept(DTO)`,或者动作的 DTO |
283
+
284
+ - **类型转换。** 1.x 的 `@Accept` 默认会转换值(yup 的 `strict: false`):在每个被校验的方法上,`'5'` 都会变成 5。3.x 的 schema 是严格的:类型不对的值会被拒绝,而不是被转换。依赖这种转换的输入(以文本形式到达的数字、布尔值或日期,无论来自查询字符串、路由参数还是其他来源),要在 schema 上声明 `.strict(false)`。
285
+ - 默认会删除未声明的键;要保留它们的 DTO 继承 `FlexibleDTO`(`lakutata/dtos`)。
286
+ - 不合法的输入会以 `InvalidValueException`(`E_INVALID_VALUE`,HTTP 状态码 400)失败。
287
+
288
+ ## 8. 数据库和缓存
289
+
290
+ **数据库。** 1.x 的 `TypeOrmComponent` 改为 `Database` 组件,用 `buildDatabaseOptions()` 声明(见 [Database](Database.md)、[Entities](Entities.md))。ORM 是 TypeORM 1.1(1.x 使用的是 0.3):
291
+
292
+ | 1.x | 3.x |
293
+ |---|---|
294
+ | `db: {class: TypeOrmComponent, options: {type: 'mysql', …, entities: [glob]}}` | `db: buildDatabaseOptions({type: 'mysql', …, entities: [Order, …]})` |
295
+ | `this.db.getRepository(Order)`、`this.db.manager`、`this.db.transaction(…)`、`this.db.query(sql)` | `Database` 的同名方法 |
296
+ | 从 `@lakutata-component/typeorm` 或 `typeorm` 导入的装饰器、操作符和类型 | `lakutata/decorator/orm`(装饰器)和 `lakutata/orm`(`In`、`Like` 等操作符,以及各种类型);不再安装 `typeorm` 包 |
297
+ | `relations: ['customer']`、`select: ['id']` | `relations: {customer: true}`、`select: {id: true}` |
298
+ | `ConnectionOptions` | `MysqlDataSourceOptions` 等(`ConnectionOptions` 系列名字是已弃用的别名) |
299
+ | `this.db.queryData(options, Entity)` 及其查询语言 | 没有对应项:把这种查询语言作为应用自己的组件保留下来,基于 TypeORM 的查询选项实现,分页使用 `paginator`(`lakutata/dtos`) |
300
+
301
+ - **生产环境中的 `synchronize: true`** 会在每次启动时修改数据表。请关闭它,改用迁移(见 [Migrations](Migrations.md)):先对生产数据库的副本生成第一个迁移(实体与数据表一致时,它应该是空的),之后由 `migrationsRun` 在启动时运行新的迁移。
302
+ - **使用 `__dirname` 的 glob** 在 ES 模块中不存在:请逐个列出实体类,迁移文件的路径用 `import.meta.dirname` 构造。
303
+
304
+ **缓存。** 1.x 的 `CacherComponent` 改为 `Cacher` 组件,用 `buildCacherOptions()` 声明(见 [Cache](Cache.md))。`get(key)`、`set(key, value, ttl)` 和 `del(key)` 保留原来的名字(1.x 只能在选项中设置的存活时间,现在可以在每次 `set` 时给出),`wrap(key, fn)` 先读缓存、未命中时再计算。键不存在时返回 `undefined`,而 1.x 返回 `null`(存储出错时也是 `null`);`null` 和 `false` 现在可以作为值缓存。键的构成方式也不同,1.x 写入的缓存条目在 3.x 中读不到:请从空缓存开始。
305
+
306
+ ## 9. 配置、运行模式和入口
307
+
308
+ - **一份配置。** 1.x 通过入口文件选择生产或开发环境(`App.ts` 调用 `Config(true)`,开发入口调用 `Config(false)`)。在 3.x 中,`config()` 只有一个函数,运行模式来自环境:依次是 `MODE`、`NODE_ENV`、`mode` 选项;`app.mode()` 给出当前的模式(见 [Application](Application.md))。
309
+ - **配置的值**来自环境变量,在 `config()` 中读取,或者作为 `@Configurable` 属性的默认值。声明不合法时,启动会以 `InvalidObjectOptionsException` 失败,并列出所有不合法的选项,所以缺少的环境变量会在启动时立即暴露,而不是等到第一次使用时。
310
+ - **选项中接收应用、用来访问其他组件的函数**,改为注入:组件自己注入它需要的对象。
311
+ - **写在配置代码中的凭据**,请移到环境变量或部署环境的密钥中。
312
+
313
+ ## 10. 测试、构建和部署
314
+
315
+ - **测试。** 1.x 中连接共享开发服务器、直接启动应用的 `npm test`,改为对编译后的测试运行 `node --test`:每个测试文件用 `Application.run(config(port))` 在自己的端口上启动应用,调用它,最后释放它;组件放在 `Container` 中单独测试;其他服务用替身代替(见[示例](examples/README.md))。
316
+ - **`package.json` 中的 `lakutata` 字段**在 1.x 中告诉命令行工具项目的类型(`type`)、目录和配置文件。在 3.x 中这个字段有别的含义(打包应用的 `resources` 和 `installer`,见 [Packaging](Packaging.md)):请删除 1.x 的这个字段。
317
+ - **Node.js、TypeScript 和模块系统。** Node.js `^22.13.0 || >=24.11.0`,与模板一样使用 TypeScript 6,ES 模块(`"type": "module"`,相对导入以 `.js` 结尾),`tsconfig.json` 继承 `lakutata/tsconfig.json`。
318
+ - **端口。** 1.x 的服务模块监听自己的端口(默认 6912),注册中心监听另一个端口(6911)。3.x 的适配器监听各自选项中的端口,而使用 NATS 的服务只主动连接 NATS 服务器:`Dockerfile` 改为暴露这些端口。
319
+ - **脚本。** 模板的脚本取代了 `shx rm -rf ./build`(改为 `lakutata clean`)、安装时编译的 `prepare`,以及开发入口;`lakutata-deploy` 没有对应项:`service` 模板带有 `Dockerfile` 和 `docker:build` 脚本,镜像由部署环境自己的工具推送。
320
+
321
+ ## 11. 用户界面
322
+
323
+ 1.x 系统中单独构建、通过 HTTP 调用网关的 Web 界面并不使用 Lakutata,不需要改动,只要网关保持它的路由和响应格式(第 6 节)、界面用到的模式,以及界面检查的 `errno` 错误码不变。服务器保持原来的路径和事件时,它们的 WebSocket 客户端也能照常工作。
324
+
325
+ ## 12. 检查移植结果
326
+
327
+ ```bash
328
+ npm ls @lakutata/core # 只有一个版本:lakutata 带来的 3.x 版本,没有残留的 1.x
329
+ npm run build # 没有从 @lakutata-module/*、@lakutata-component/* 或 1.x 的 @lakutata/core 导入的代码
330
+ grep -rnE "createApp|generateException|initialize\(|app\.(Components|Modules|Logger)|Validator\.|__dirname" src
331
+ npm test
332
+ MODE=production npm start # 查看启动日志,然后按 Ctrl+C:应用平稳退出
333
+ ```
334
+
335
+ - `grep` 列出残留的 1.x 代码。请逐行检查它找到的结果。
336
+ - 调用方和界面得到的响应与 1.x 服务的相同:相同的模式、`errno` 错误码和响应格式。
337
+ - 数据库:对生产数据库的副本运行 `lakutata migration generate`,不应生成任何变更。
338
+
339
+ ## 13. 常见错误
340
+
341
+ - 在 1.x 项目中升级 `@lakutata/core`:3.x 版本是另一个库(第 1 节)。
342
+ - 从动作的输入中读取模式中的键:它们已经从输入中删除(第 5 节)。
343
+ - 依赖 1.x 类型转换的输入(把 `'5'` 当作 5):没有 `.strict(false)` 时,3.x 的 schema 会拒绝它们(第 7 节)。
344
+ - 用 `=== null` 判断缓存未命中:未命中时返回的是 `undefined`(第 8 节)。
345
+ - `app.Logger.warning(…)`:3.x 的方法是 `warn`。
346
+ - 移植后仍在生产环境中开启 `synchronize: true`:实体一变化,启动时就会修改数据表。
347
+ - 在同一个进程中混用两个框架,或者让 1.x 服务调用 3.x 服务:它们的服务传输方式互不相通(第 2 节)。
@@ -0,0 +1,230 @@
1
+ # 从 Lakutata 2.x 升级
2
+
3
+ 本页说明如何把项目从 Lakutata 2.x(最后一个版本是 2.0.126)升级到 3.x:怎样区分两个版本、升级步骤、改名和移动了的 API、编译器不会报告的行为变化,以及如何检查升级结果。
4
+
5
+ 本页只适用于依赖 `lakutata` 包 2.x 版本的项目。Lakutata 1.x 的项目依赖的是 1.x 版本的 `@lakutata/core`,以及 `@lakutata-component/*` 或 `@lakutata-module/*` 包:这类项目请看 [UpgradingFrom1](UpgradingFrom1.md),不要按本页升级。
6
+
7
+ ## 1. 区分 2.x 和 3.x
8
+
9
+ | 特征 | 2.x | 3.x |
10
+ |---|---|---|
11
+ | `package.json` 中的 `lakutata` | `^2.…` | `^3.…` |
12
+ | `node_modules/lakutata/package.json` | 没有 `dependencies`(全部打包在内) | 依赖与它同一版本的 `@lakutata/core`、`@lakutata/entrypoint` 等包 |
13
+ | 辅助函数 | `import {Delay} from 'lakutata/helper'` | `import {delay} from 'lakutata/utils'` |
14
+ | 导出的函数 | 大驼峰:`BuildEntrypoints`、`BuildDatabaseOptions`、`MD5` | 小驼峰:`buildEntrypoints`、`buildDatabaseOptions`、`md5` |
15
+ | 传输方式 | 手写:`BuildHTTPEntrypoint((module, routeMap, handler, registerDestroy) => …)` | 适配器:`lakutata/adapter/express` 中的 `expressHTTPEntrypoint()` 等 |
16
+ | 只有 3.x 才有的子路径 | | `lakutata/utils`、`lakutata/security`、`lakutata/adapter/*`、`lakutata/com/{containerd,mutex,queue,i18n}` |
17
+ | Node.js | `>=20` | `^22.13.0 \|\| >=24.11.0` |
18
+
19
+ `npx @lakutata/cli info` 会显示命令行工具、框架和 Node.js 的版本,以及项目声明的和实际安装的 `lakutata` 版本。没有升级命令:下面的步骤需要手动完成。
20
+
21
+ ## 2. 升级步骤
22
+
23
+ - **Node.js** `^22.13.0 || >=24.11.0`(22.13 或更高的 22.x,或者 24.11 及以上;不包括 23)。同时更新 `engines`、CI 的版本矩阵,以及 Dockerfile 的基础镜像。
24
+ - **支持 `"module": "Node20"` 的 TypeScript**(模板使用 TypeScript 6):`lakutata/tsconfig.json` 现在设置了这个选项(2.x 中是 `Node16`),更旧的编译器不认识它。
25
+ - **`package.json`**:`"type": "module"`,`lakutata` 为 `^3.0.0`,所有 `@lakutata/*` 包使用同一个版本(它们互相依赖对方的确切版本),`@lakutata/cli` 放在 `devDependencies` 中。
26
+ - **全新安装**:删除 `node_modules` 和锁文件,再重新安装。原地升级可能留下多份 `@lakutata/core`,它们的类和装饰器元数据互不相通(跨副本的 `instanceof` 和注入都会失败)。`npm ls @lakutata/core` 必须只显示一个版本。
27
+ - **`tsconfig.json`** 继承 `lakutata/tsconfig.json`,并设置自己的 `rootDir`、`outDir` 和 `include`。要保留 `include`:发布的选项设置了 `allowJs`,没有它的话,项目根目录下的配置文件(`eslint.config.js`)也会被编译。保留自己选项的项目需要 `experimentalDecorators`、`emitDecoratorMetadata` 和 `useDefineForClassFields: false`。
28
+
29
+ ```json
30
+ {
31
+ "extends": "lakutata/tsconfig.json",
32
+ "compilerOptions": {
33
+ "rootDir": "src",
34
+ "outDir": "build"
35
+ },
36
+ "include": ["src"]
37
+ }
38
+ ```
39
+
40
+ - **相对导入**以 `.js` 结尾(`import {config} from './config/Config.js'`),和 2.x 的 ES 模块项目一样。
41
+ - **所用功能的库**是 `lakutata` 的可选对等依赖,需要项目自己安装(2.x 项目已经装了自己的传输方式所导入的那些);见 [GettingStarted](GettingStarted.md) 第 3 节:
42
+
43
+ | 使用了 | 安装 |
44
+ |---|---|
45
+ | `lakutata/adapter/express` | `express`(4.21+ 或 5)、`@types/express` |
46
+ | `lakutata/adapter/fastify` | `fastify` 5 |
47
+ | `lakutata/adapter/commander` | `commander`(12 到 15);提示输入需要 `@inquirer/prompts`(7 或 8) |
48
+ | `lakutata/adapter/socket.io` | `socket.io` 4 |
49
+ | 通过 WebSocket 的 `lakutata/adapter/jsonrpc` | `ws` 8 |
50
+ | 某种数据库 `type` | `pg`、`mysql2`(3.15.3+)、`better-sqlite3` 12(取代 `sqlite3`)、`mongodb` 7、`mssql` 12、`oracledb`、`sql.js` 等 |
51
+ | 某种缓存存储 | `redis`(4 或 5)或 `@redis/client`、`memjs`、`mongodb`、`mysql2`、`pg`、`better-sqlite3`(2.x 中 SQLite 存储使用的是 `sqlite3`) |
52
+ | NATS、D-Bus | `@lakutata/nats`、`@lakutata/dbus`,版本与 `lakutata` 相同 |
53
+
54
+ - **按第 3 节的表格改名**,然后构建:改名的函数都没有别名,所以编译器会报告每一个遗留的导入。之后再逐条检查第 4 节,这些变化编译器不会报告。
55
+
56
+ ## 3. 改名和移动
57
+
58
+ | 2.x | 3.x |
59
+ |---|---|
60
+ | `lakutata/helper` | `lakutata/utils`(同样的辅助函数,按下文改名,另外新增 `Time`、`cloneDeep`、`isSubclassOf`) |
61
+ | `lakutata/helper` 中的 `Paginator`、`PaginatorHandlerResult` | `lakutata/dtos` 中的 `paginator`、`PaginatorHandlerResult` |
62
+ | `lakutata` 中的 `IPatRun` | 已移除:模式匹配属于内部实现 |
63
+
64
+ 2.x 的其他子路径保持原名:`lakutata`、`lakutata/orm`、`lakutata/dtos`、`lakutata/com/{entrypoint,database,cacher,docker,monitor,logger}`、`lakutata/provider/{database,passwordHash}`、`lakutata/decorator/{di,dto,ctrl,asst,orm}` 和 `lakutata/tsconfig.json`。
65
+
66
+ 所有导出的函数都改用小驼峰命名,2.x 的名字没有别名。其中大多数只是把首字母改成小写(`ArrayToSet` 变成 `arrayToSet`):
67
+
68
+ - `lakutata/utils`:`arrayToSet`、`convertArrayLikeToIterable`、`convertArrayLikeToStream`、`delay`、`getObjectNestingDepth`、`getObjectPropertyPaths`、`hexToIEEE754`、`hexToSigned`、`hexToUnsigned`、`isAbortError`、`isEmptyObject`、`isGlobString`、`isNativeFunction`、`isPath`、`isPromise`、`isPromiseLike`、`isSymbol`、`isXML`、`mergeArray`、`mergeMap`、`mergeSet`、`noCase`、`objectConstructor`、`objectHash`、`objectParentConstructor`、`objectParentConstructors`、`objectPrototype`、`objectToMap`、`randomString`、`setToArray`、`signedToHex`、`sortArray`、`sortKeys`、`sortObject`、`templating`、`uniqueArray`、`unsignedToHex`;
69
+ - `lakutata/com/entrypoint`:`buildEntrypoints`、`buildHTTPEntrypoint`、`buildCLIEntrypoint`、`buildServiceEntrypoint`;
70
+ - `lakutata/com/cacher`:`buildCacherOptions`;`lakutata/com/database` 和 `lakutata/provider/database`:`buildDatabaseOptions`;
71
+ - `lakutata/com/docker`:`buildDockerConnectionOptions`、`buildDockerSocketConnectionOptions`、`buildDockerHttpConnectionOptions`、`buildDockerHttpsConnectionOptions`、`buildDockerSSHConnectionOptions`、`parseEnvToRecord`、`parseRepositoryTag`。
72
+
73
+ `As`、`DevNull` 和 `Glob`(以及新增的 `GlobSync`)仍是大驼峰,类(`Time`、`URLBuilder`、`UUID`、`ObjectPath`、`MessagePack`、`Statistics`、`IP`、`IPv4`、`IPv6`)和装饰器也一样。其他改名:
74
+
75
+ | 2.x | 3.x |
76
+ |---|---|
77
+ | `GraceExit` | `gracefulExit` |
78
+ | `IsExists` | `pathExists` |
79
+ | `IsHtml` | `isHTML` |
80
+ | `NonceStr` | `nonceString` |
81
+ | `MD5`、`SHA1`、`SHA256` | `md5`、`sha1`、`sha256` |
82
+ | `IEEE754ToHex` | `ieee754ToHex` |
83
+ | `Split`、`ToLower`、`ToUpper` | 已移除:`noCase(input).split(' ')` 得到小写的单词(输入中没有单词时为 `['']`) |
84
+ | 类型 `SortOptions` | `SortArrayOptions` |
85
+ | 类型 `IPv4SubNet`、`IPv6SubNet` | `IPv4Subnet`、`IPv6Subnet` |
86
+ | `IPv6Result` 的字段 `comperssed` | `compressed` |
87
+ | `AccessDenyException` | `AccessDeniedException`:`errno` 仍为 `E_ACCESS_DENY`,`err`(类名)改变了 |
88
+ | 类型 `OnDelEventData`(`lakutata/com/cacher`) | `OnDeleteEventData`;事件名仍为 `'del'` |
89
+ | `@Lifetime('APPLICATION_SINGLETON')`、`'MODULE_SINGLETON'` | `@Singleton()`:生命周期只有 `SINGLETON`、`SCOPED` 和 `TRANSIENT` |
90
+ | `Database` 的静态属性 `databaseSymbol` | 已移除 |
91
+
92
+ ORM 从 TypeORM 0.3.24 升级到 TypeORM 1.1.2,其源码已并入 `@lakutata/orm`(不再有 `typeorm` 包):
93
+
94
+ | 2.x | 3.x |
95
+ |---|---|
96
+ | `type: 'sqlite'`(驱动 `sqlite3`) | `type: 'better-sqlite3'`(驱动 `better-sqlite3`) |
97
+ | `PostgresConnectionOptions`、`MysqlConnectionOptions` 等 | `PostgresDataSourceOptions`、`MysqlDataSourceOptions` 等(旧名字是已弃用的别名) |
98
+ | `SqliteConnectionOptions` | `BetterSqlite3DataSourceOptions` |
99
+ | `AbstractRepository` | 已移除:使用 `Repository`,或者 `repository.extend({...})` |
100
+ | `relations: ['author']`、`select: ['id']` | `relations: {author: true}`、`select: {id: true}` |
101
+ | `import {In} from 'typeorm'` | `import {In} from 'lakutata/orm'`:再装一个 `typeorm` 包会成为另一份副本 |
102
+
103
+ 2.x 时期在 `lakutata` 之外单独发布的包也按同样的方式改名:
104
+
105
+ | 2.x | 3.x |
106
+ |---|---|
107
+ | `SetupNatsServiceEntrypoint`(`@lakutata/nats`) | `natsServiceEntrypoint` |
108
+ | `BuildServiceProxy`、`JSONCodec`、`MessagePackCodec` | `buildServiceProxy`、`jsonCodec`、`messagePackCodec` |
109
+ | `CreateDBusError`(`@lakutata/dbus`) | `createDBusError` |
110
+ | D-Bus 客户端的事件 `online`、`offline`、`replaced`、`NameOwnerChanged`、`NameAcquired`、`NameLost` | `serviceOnline`、`serviceOffline`、`serviceReplaced`、`nameOwnerChanged`、`nameAcquired`、`nameLost`(监听旧名字的监听器永远不会被调用) |
111
+ | D-Bus 客户端的选项 `advancedResponse`、`convertBigIntToNumber` | `decode`:`{types: 'all'}` 保留 D-Bus 类型,`{int64: 'bigint'}` 保留 bigint |
112
+
113
+ ## 4. 编译器不会报告的行为变化
114
+
115
+ ### 应用及其对象
116
+
117
+ 见 [Application](Application.md) 和 [Objects](Objects.md)。
118
+
119
+ - **运行模式**(`app.mode()`)依次取环境变量 `MODE`、`NODE_ENV`、`mode` 选项,都没有时为 `development`,并写回这两个环境变量。2.x 使用的是 `mode` 选项(默认为 `development`),并用它覆盖 `NODE_ENV`:现在设置了 `NODE_ENV=production` 的部署环境会以生产模式运行,不论选项怎么写。
120
+ - **SIGTERM 和 SIGINT** 会让应用平稳退出(运行 `onDone`,入口停止,对象被销毁,退出码为 0);2.x 把它们交给 Node.js 处理。请删除应用自己的 `process.on('SIGTERM', …)`,或者设置 `shutdownSignals: false`。`onDone` 处理函数会收到第三个参数 `{code, signal?}`。
121
+ - **进程无事可做时**(没有服务器、连接或定时器),应用会平稳退出:此时 `onDone` 处理函数和 `destroy()` 钩子也会运行。在 2.x 中,进程直接结束。
122
+ - **启动失败**时,如果没有 `onFatalException` 处理函数,进程以退出码 1 结束。2.x 只记录错误,进程继续运行,或者以退出码 0 结束。
123
+ - **启动时检查选项**(`checkOptions`,默认开启):`@Configurable` 属性的值不合法时,启动失败并抛出 `InvalidObjectOptionsException`(`E_INVALID_OBJECT_OPTIONS`),它列出所有不合法的选项,即使对象从未被创建;不是 `@Configurable` 属性的选项会产生一条警告。`''` 不满足必填的字符串:请写 `process.env.X || undefined`,不要写 `|| ''`。接受其他选项的类可以声明静态方法 `acceptsOption(name, value)`。
124
+ - **`@Configurable` 属性的初始值就是它的默认值**(`protected readonly port: number = 8080`),并且和配置的值一样会被校验。2.x 会用 schema 的默认值或 `undefined` 替换它。
125
+ - **新的生命周期钩子**:`startup()`(在 `init()` 之后、对象交给使用者之前)、`launched()`(应用启动完成之后)、`beforeDestroy()`(销毁开始时),以及静态方法 `acceptsOption()`。2.x 的类如果有同名的成员,现在会被框架调用:请给这个成员改名。
126
+ - **`Component` 的生命周期锁定为单例**:子类上的 `@Transient()` 或 `@Scoped()` 会在类定义时抛出 `LifetimeLockedException`(2.x 接受这种写法)。请把它改成 `Provider`:默认是作用域生命周期,也可以是 `@Singleton()` 或 `@Transient()`。
127
+ - **用 `class` 声明的内置组件**(`components: {log: {class: MyLogger}}`)会替换预设的组件;2.x 会悄悄保留预设的组件。不带 `class` 的声明仍然只是配置预设的 `log` 或 `entrypoint`。
128
+ - **用 glob 声明的对象**(`objects.anonymous`、`container.load()`)会注册每个文件导出的所有 `BaseObject` 子类(2.x 只注册第一个导出),导入失败的文件会让启动失败(2.x 会跳过它)。
129
+
130
+ ### 错误和值
131
+
132
+ - **状态码**:`AccessDeniedException` 的 `statusCode` 为 403,`ControllerActionNotFoundException` 为 404,`InvalidValueException` 为 400。在 2.x 中这三者都是 500,所以检查 500 的客户端和测试需要修改。
133
+ - **消息**:不带参数创建的异常,以类名中的单词为消息(`Order not found exception`;2.x 是 `Ordernotfoundexception`);单个参数会填入 `{0}`(`new OrderException('Order {0}', 7)`);`%` 占位符遵循 `util.format()`(`%s %d %i %f %j %o %O %c %%`),`%` 后面跟其他字符时保持原样。
134
+ - **DTO 和 2.x 一样是严格的**:HTTP 适配器给出的查询参数和路由参数都是字符串,所以从中读取数字或布尔值需要 `.strict(false)`。`PaginationSearchDTO` 现在会转换它的 `limit` 和 `offset`。
135
+ - **`Time`**:没有时间部分的日期(`new Time('2024-03-01')`)是默认时区的零点(2.x 是 UTC 的零点)。
136
+ - **`PasswordHash`**:超过 72 字节 UTF-8 的密码,`hash()` 会抛出 `PasswordTooLongException`,`validate()` 返回 `false`。2.x 只对前 72 字节做哈希,所以这些用户将无法再登录。`saltRounds` 的取值范围是 4 到 31。
137
+ - **`Glob`** 只接受 `cwd`、`absolute`、`dot`、`nodir`、`follow`、`ignore` 和 `nocase`,返回排好序、没有重复的 `string[]`:`withFileTypes` 和扩展模式(`+(a|b)`)都已移除。
138
+
139
+ ### 入口
140
+
141
+ 见 [HTTP](HTTP.md) 第 8 节和 [ServiceCalls](ServiceCalls.md)。
142
+
143
+ - **2.x 的传输方式函数仍然可用**,放在 `http`、`cli` 和 `service` 下(改名为 `buildHTTPEntrypoint` 等)。入口启动时调用它们,并等待它们返回的 promise:启动会等待服务器就绪,promise 被拒绝(例如端口被占用)时启动失败,而等待一个长时间运行的命令(`await program.parseAsync()`)的函数会让启动一直等到命令结束。请不要 `await` 这个调用,或者改用 `commanderCLIEntrypoint()`。
144
+ - **改用适配器**:手写的 Express 服务器的中间件和路由放进 `expressHTTPEntrypoint()` 的 `before` 钩子,请求体的大小限制放进 `body`,它构造的输入放进 `input`。响应会变化:HTTP 适配器用错误的状态码和 `{errno, errMsg}` 响应错误,而 `jsonEnvelope({actionErrorStatus: 200, extra})` 能给出 2.x 应用常常自己写的 `{errno, errMsg, data}` 封套(见下面的例子)。服务适配器(HTTP 服务、Socket.IO、JSON-RPC、Electron)以 `{success, error, payload}` 响应。
145
+ - **嵌套的服务模式按键逐个比较**:`@ServiceAction({res: {name: 'user'}, act: 'get'})` 只匹配这个嵌套的值。2.x 把任何对象都当作 `[object Object]` 比较,所以仅在嵌套值上不同的两个动作会互相覆盖。
146
+ - **`Controller.context`** 的类型是 `CLIContext | HTTPContext | ServiceContext | BaseContext`:参数类型是 2.x 那个联合类型的函数不再接受 `this.context`。请加上 `BaseContext`,或者用 `instanceof` 收窄类型。
147
+ - **Electron**:Lakutata 2.x 没有 Electron 传输方式。3.x 在主进程中用 `lakutata/adapter/electron` 的 `electronIPCServiceEntrypoint()` 提供动作:预加载脚本调用 `ipcRenderer.invoke('lakutata:service', message, clientId)`,得到 `{success, error, payload}` 响应;事件来自 `entrypoint.notify(clientId, event, ...args)` 和 `entrypoint.broadcast(event, ...args)`,客户端 id 在 `this.context.clientId` 中。Electron 模板带有预加载脚本和 `window.ipc`,它的 `invoke()` 返回 payload 或者抛出错误;读取自己的 IPC 层原始响应的代码需要修改。
148
+
149
+ ### 组件
150
+
151
+ 见 [Database](Database.md)、[DockerIntegration](DockerIntegration.md) 和 [Logger](Logger.md)。
152
+
153
+ - **数据库**:where 条件中值为 `null` 或 `undefined` 的字段仍然会被忽略(`invalidWhereValuesBehavior`),所以 `findOneBy({id: undefined})` 会返回第一行:请检查从请求中取得的值。表名或列名含有非 ASCII 字符时,TypeORM 1 生成索引和约束名称的方式不同:`namingStrategy: new NamingStrategyV03()`(来自 `lakutata/orm`)保留 TypeORM 0.3 的名称。
154
+ - **Docker**:`exec()` 的结果是 `{exitCode, stdout, stderr}`(2.x 是字符串形式的输出);`exec()` 和 `createTTY()` 中字符串形式的 `cmd` 通过 `/bin/sh -c` 运行,`createTTY()` 默认运行 `/bin/sh`(2.x 是 `bash`)。
155
+ - **日志**:只有标准输出是终端、且没有设置 `NO_COLOR` 时才输出颜色(2.x 总是输出颜色);记录请求时会隐去 `Authorization`、`Cookie`、`Set-Cookie` 和 `Proxy-Authorization` 请求头(`redactedHeaders: []` 保留它们);不是合法级别的 `level` 会让启动失败。
156
+
157
+ ```typescript
158
+ import {Application, type ApplicationExitReason, type ApplicationOptions, type ILogger} from 'lakutata'
159
+ import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
160
+ import {GET} from 'lakutata/decorator/ctrl'
161
+ import {expressHTTPEntrypoint, jsonEnvelope} from 'lakutata/adapter/express'
162
+ import {delay, md5} from 'lakutata/utils'
163
+
164
+ class HashController extends Controller {
165
+ //2.x 中是 'lakutata/helper' 的 Delay() 和 MD5()
166
+ @GET('/hash')
167
+ public async hash(): Promise<{ hash: string }> {
168
+ await delay(10)
169
+ return {hash: md5('text').toString('hex')}
170
+ }
171
+ }
172
+
173
+ Application
174
+ .run((): ApplicationOptions => ({
175
+ id: 'com.example.app',
176
+ name: 'Example',
177
+ //只有环境中既没有 MODE 也没有 NODE_ENV 时才使用
178
+ mode: 'production',
179
+ components: {
180
+ entrypoint: buildEntrypoints({
181
+ controllers: [HashController],
182
+ http: expressHTTPEntrypoint({
183
+ port: 8080,
184
+ //动作的错误以 200 响应,格式为 {errno, errMsg, data, timestamp}
185
+ respond: jsonEnvelope({
186
+ actionErrorStatus: 200,
187
+ extra: (): Record<string, unknown> => ({timestamp: Date.now()})
188
+ })
189
+ })
190
+ })
191
+ },
192
+ bootstrap: ['entrypoint']
193
+ }))
194
+ .onDone((app: Application, log: ILogger, reason: ApplicationExitReason): void => {
195
+ log.info('%s stopped with code %d', app.appName, reason.code)
196
+ })
197
+ ```
198
+
199
+ ## 5. 有用的工具
200
+
201
+ - `lakutata create --name probe --template <模板> --path <一个临时目录>` 创建一个同类的全新 3.x 项目,用来对照它的 `package.json`、`tsconfig.json` 和脚本,Electron 项目还可以对照它的预加载脚本。`--init` 在已有的目录中创建项目,保留目录中已有的文件,并列出没有复制的文件。
202
+ - `lakutata migration generate|create|run|revert|show` 运行数据库迁移(2.x 的 `MIGRATION_*` 环境变量仍然有效);`lakutata package bundle|exe|installer` 打包应用。见 [Command](Command.md)。
203
+
204
+ ## 6. 检查升级结果
205
+
206
+ ```bash
207
+ node --version # ^22.13.0 || >=24.11.0
208
+ npm ls lakutata @lakutata/core # 每个包只有一个版本
209
+ npm run build # tsc 没有错误:没有遗留的 2.x 导入
210
+ grep -rnE "process\.on\('SIG|\|\| ''|(startup|launched|beforeDestroy|acceptsOption)\(|AccessDenyException|=== 500" src
211
+ npm test
212
+ MODE=production npm start # 查看启动日志,然后按 Ctrl+C:onDone 运行,退出码为 0
213
+ ```
214
+
215
+ - `grep` 列出能编译、但行为已经变化的代码:信号处理函数、传给选项的空字符串、与新钩子同名的成员、与 2.x 的错误名或状态码的比较。请逐行检查它找到的结果。
216
+ - 启动日志显示预期的运行模式,没有 `InvalidObjectOptionsException`,也没有以 `is not a configurable property, it is ignored` 结尾的警告。
217
+ - 不合法的输入以 400 `E_INVALID_VALUE` 响应,被拒绝的访问以 403 `E_ACCESS_DENY` 响应,未知的路由以 404 响应,并且使用客户端期望的封套。
218
+ - 配置错误时,进程以退出码 1 结束。
219
+ - 对生产数据库的副本运行 `lakutata migration generate` 不应生成任何变更:如果生成了,说明命名或实体有差异(见第 4 节的 `NamingStrategyV03`),而不是有迁移需要运行。
220
+
221
+ ## 7. 常见错误
222
+
223
+ - 用查找替换给函数改名时,也会改掉包含这个名字的字符串、日志消息、对象的键和更长的名字(`RequestDelay` 中的 `Delay`):请按整词匹配,并检查改动。新的小驼峰名字也可能与局部变量或参数重名(`delay`、`paginator`、`md5`)。
224
+ - 一个进程只能加载一份框架。`lakutata` 的 ES 模块构建和 CommonJS 构建是不同的文件:ES 模块应用加载一个只有 CommonJS 版本、并且 require 了 `lakutata` 的库时,会得到两份副本,它们的类和元数据互不相同。请让这些库同时提供两种模块系统的构建,或者让整个进程只用其中一种。
225
+ - 必填选项写成 `process.env.X || ''` 会让启动失败;写成 `|| undefined` 才会使用默认值。
226
+ - 名为 `startup`、`launched`、`beforeDestroy` 或 `acceptsOption` 的成员现在会被框架调用。
227
+ - 决定运行模式的是部署环境的 `NODE_ENV`,而不是 `mode` 选项。
228
+ - 2.x 的 SQLite 文件用 `type: 'better-sqlite3'` 打开。`better-sqlite3` 是针对某个 Node.js 版本构建的原生插件:Electron 应用需要为它的 Electron 构建的版本。
229
+ - 2.x 为超过 72 字节的密码生成的哈希不再能通过校验:请为这些用户安排重置密码。
230
+ - 用 `statusCode === 500` 判断访问被拒绝、或者比较 `err === 'AccessDenyException'` 的代码不再匹配:请比较 `errno`。