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.
- package/README.md +50 -30
- package/README.zh-CN.md +130 -0
- package/dist/cjs/exports/QueueComponent.d.ts +1 -0
- package/dist/cjs/exports/QueueComponent.js +20 -0
- package/dist/esm/exports/QueueComponent.js +1 -0
- package/dist/types/exports/QueueComponent.d.ts +1 -0
- package/doc/en/Application.md +107 -0
- package/doc/en/CLI.md +274 -0
- package/doc/en/Cache.md +379 -0
- package/doc/en/Command.md +110 -0
- package/doc/en/ContainerdIntegration.md +17 -16
- package/doc/en/Controllers.md +256 -0
- package/doc/en/DBus.md +291 -0
- package/doc/en/DTOValidation.md +72 -43
- package/doc/en/Database.md +188 -0
- package/doc/en/DatabaseDrivers.md +287 -0
- package/doc/en/Decorator.md +52 -40
- package/doc/en/DockerIntegration.md +7 -6
- package/doc/en/Entities.md +240 -0
- package/doc/en/Entrypoint.md +216 -0
- package/doc/en/Exceptions.md +174 -0
- package/doc/en/GRPC.md +251 -0
- package/doc/en/GettingStarted.md +165 -0
- package/doc/en/HTTP.md +507 -0
- package/doc/en/HelperFunctions.md +11 -11
- package/doc/en/I18n.md +119 -115
- package/doc/en/Injection.md +144 -0
- package/doc/en/Installers.md +280 -0
- package/doc/en/Logger.md +206 -0
- package/doc/en/MCP.md +293 -0
- package/doc/en/Middlewares.md +213 -0
- package/doc/en/Migrations.md +202 -0
- package/doc/en/MongoEmbedded.md +149 -0
- package/doc/en/Monitor.md +164 -0
- package/doc/en/Mutex.md +27 -25
- package/doc/en/NATS.md +246 -0
- package/doc/en/NativeAddons.md +212 -0
- package/doc/en/Objects.md +143 -0
- package/doc/en/Overview.md +62 -85
- package/doc/en/Packaging.md +193 -0
- package/doc/en/ProjectStructure.md +124 -0
- package/doc/en/Queue.md +269 -0
- package/doc/en/Schedules.md +217 -0
- package/doc/en/Security.md +10 -9
- package/doc/en/ServiceCalls.md +504 -0
- package/doc/en/Time.md +19 -20
- package/doc/en/UpgradingFrom1.md +435 -0
- package/doc/en/UpgradingFrom2.md +315 -0
- package/doc/en/Workspaces.md +189 -0
- package/doc/en/examples/BackgroundJobs.md +677 -0
- package/doc/en/examples/CommandLineTool.md +664 -0
- package/doc/en/examples/DesktopApp.md +809 -0
- package/doc/en/examples/DeviceService.md +607 -0
- package/doc/en/examples/MCPServer.md +653 -0
- package/doc/en/examples/README.md +16 -0
- package/doc/en/examples/RestAPI.md +490 -0
- package/doc/en/examples/ServiceCalls.md +829 -0
- package/doc/en/examples/WebApp.md +568 -0
- package/doc/zh/Application.md +75 -0
- package/doc/zh/CLI.md +250 -0
- package/doc/zh/Cache.md +328 -0
- package/doc/zh/Command.md +78 -0
- package/doc/zh/ContainerdIntegration.md +9 -9
- package/doc/zh/Controllers.md +228 -0
- package/doc/zh/DBus.md +257 -0
- package/doc/zh/DTOValidation.md +49 -19
- package/doc/zh/Database.md +175 -0
- package/doc/zh/DatabaseDrivers.md +261 -0
- package/doc/zh/Decorator.md +25 -18
- package/doc/zh/DockerIntegration.md +6 -6
- package/doc/zh/Entities.md +224 -0
- package/doc/zh/Entrypoint.md +193 -0
- package/doc/zh/Exceptions.md +158 -0
- package/doc/zh/GRPC.md +226 -0
- package/doc/zh/GettingStarted.md +148 -0
- package/doc/zh/HTTP.md +445 -0
- package/doc/zh/HelperFunctions.md +2 -2
- package/doc/zh/I18n.md +26 -26
- package/doc/zh/Injection.md +129 -0
- package/doc/zh/Installers.md +232 -0
- package/doc/zh/Logger.md +188 -0
- package/doc/zh/MCP.md +265 -0
- package/doc/zh/Middlewares.md +198 -0
- package/doc/zh/Migrations.md +178 -0
- package/doc/zh/MongoEmbedded.md +118 -0
- package/doc/zh/Monitor.md +145 -0
- package/doc/zh/Mutex.md +10 -10
- package/doc/zh/NATS.md +207 -0
- package/doc/zh/NativeAddons.md +180 -0
- package/doc/zh/Objects.md +119 -0
- package/doc/zh/Overview.md +46 -71
- package/doc/zh/Packaging.md +154 -0
- package/doc/zh/ProjectStructure.md +102 -0
- package/doc/zh/Queue.md +214 -0
- package/doc/zh/Schedules.md +193 -0
- package/doc/zh/Security.md +6 -6
- package/doc/zh/ServiceCalls.md +459 -0
- package/doc/zh/Time.md +2 -2
- package/doc/zh/UpgradingFrom1.md +347 -0
- package/doc/zh/UpgradingFrom2.md +230 -0
- package/doc/zh/Workspaces.md +150 -0
- package/doc/zh/examples/BackgroundJobs.md +618 -0
- package/doc/zh/examples/CommandLineTool.md +618 -0
- package/doc/zh/examples/DesktopApp.md +748 -0
- package/doc/zh/examples/DeviceService.md +546 -0
- package/doc/zh/examples/MCPServer.md +604 -0
- package/doc/zh/examples/README.md +14 -0
- package/doc/zh/examples/RestAPI.md +458 -0
- package/doc/zh/examples/ServiceCalls.md +765 -0
- package/doc/zh/examples/WebApp.md +520 -0
- package/package.json +46 -17
- package/doc/en/ActionPattern.md +0 -408
- package/doc/en/Core.md +0 -312
- package/doc/en/ORMIntegration.md +0 -341
- package/doc/zh/ActionPattern.md +0 -334
- package/doc/zh/Core.md +0 -256
- package/doc/zh/ORMIntegration.md +0 -319
package/doc/zh/Decorator.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Lakutata 装饰器
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
依赖、选项、动作和校验都用装饰器声明(TypeScript 的 `experimentalDecorators` 和 `emitDecoratorMetadata`,两者都已在 `lakutata/tsconfig.json` 中开启)。装饰器由以下子路径导出:
|
|
4
4
|
|
|
5
5
|
| 子路径 | 装饰器 |
|
|
6
6
|
|---|---|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
| `lakutata/decorator/asst` | `@Before`、`@After` |
|
|
11
11
|
| `lakutata/decorator/orm` | TypeORM 的装饰器(`@Entity`、`@Column` 等) |
|
|
12
12
|
| `lakutata/com/mutex` | `@Synchronized` |
|
|
13
|
+
| `lakutata/com/queue` | `@QueueAction` |
|
|
13
14
|
|
|
14
15
|
## 1. 依赖注入(`lakutata/decorator/di`)
|
|
15
16
|
|
|
@@ -51,11 +52,11 @@ class Signup extends Component {
|
|
|
51
52
|
}
|
|
52
53
|
```
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
依赖无法解析时,对象创建失败,抛出 `DependencyInjectionException`。
|
|
55
56
|
|
|
56
57
|
### 1.2 `@Configurable`
|
|
57
58
|
|
|
58
|
-
`@Configurable(schema?, fn?)` 把属性声明为对象的选项,选项值来自对象的声明(`{class: Server, port: 8080}`)或 `getObject()`
|
|
59
|
+
`@Configurable(schema?, fn?)` 把属性声明为对象的选项,选项值来自对象的声明(`{class: Server, port: 8080}`)或 `getObject()` 的第二个参数。值先经过校验规则(默认 `DTO.Any()`)校验和转换;如果提供了 `fn`,再交给 `fn` 处理,调用时 `this` 是这个实例。
|
|
59
60
|
|
|
60
61
|
```typescript
|
|
61
62
|
import {Application, Component, DTO} from 'lakutata'
|
|
@@ -90,12 +91,12 @@ Application.run(() => ({
|
|
|
90
91
|
```
|
|
91
92
|
|
|
92
93
|
- **取值**:优先使用配置里给的值,其次是属性的初始值(`protected readonly host: string = '127.0.0.1'`),最后是校验规则的默认值(`DTO.Number().default(80)`)。初始值会和配置值一样经过校验和转换;子类可以用自己的初始值给出自己的默认值。
|
|
93
|
-
- **`maxListeners`**:所有组件都有这个可配置属性(默认 11
|
|
94
|
-
-
|
|
94
|
+
- **`maxListeners`**:所有组件都有这个可配置属性(默认 11),同一个事件的监听器超过这个数量时,会发出警告,提示可能有内存泄漏(0 表示不警告;监听器的数量本身从不受限制)。组件可以用 `protected readonly maxListeners: number = 0` 设置自己的默认值,也可以在声明里设置:`{class: MyComponent, maxListeners: 0}`。
|
|
95
|
+
- **启动检查**:应用启动时,会在创建任何对象之前检查它声明的所有对象(组件、提供者和对象)的选项,所以即使某个对象从未被使用,配置错误也会在启动时暴露:
|
|
95
96
|
- 不符合 `@Configurable` 属性校验规则的值会导致启动失败,抛出 `InvalidObjectOptionsException`,并列出所有不合法的选项;
|
|
96
|
-
- 不是该类 `@Configurable`
|
|
97
|
+
- 不是该类 `@Configurable` 属性的选项(例如拼错的选项名)会被忽略,并发出警告,警告中列出该类的可配置属性。
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
不通过可配置属性、而是通过 `getConfigurableRecords()` 读取选项的类,可以覆盖静态方法 `acceptsOption(name, value)` 来接受这些选项,这样就不会产生警告。由外部(异步)规则校验的值在对象创建时校验。在应用的选项中设置 `checkOptions: false` 可以关闭这项检查。
|
|
99
100
|
|
|
100
101
|
### 1.3 `@Autoload`
|
|
101
102
|
|
|
@@ -105,17 +106,17 @@ Application.run(() => ({
|
|
|
105
106
|
|
|
106
107
|
| 装饰器 | 实例数量 | 默认用于 |
|
|
107
108
|
|---|---|---|
|
|
108
|
-
| `@Singleton(lock?)` | 每个注册该类的容器一个 | `Component
|
|
109
|
+
| `@Singleton(lock?)` | 每个注册该类的容器一个 | `Component`(锁定) |
|
|
109
110
|
| `@Scoped(lock?)` | 每个作用域一个(入口的一次请求) | `Provider`、`Controller`(锁定) |
|
|
110
111
|
| `@Transient(lock?)` | 每次解析一个 | `BaseObject` |
|
|
111
112
|
|
|
112
|
-
`@Lifetime('SINGLETON' | 'SCOPED' | 'TRANSIENT', lock?)` 是它们的完整写法。`lock` 为 `true`
|
|
113
|
+
`@Lifetime('SINGLETON' | 'SCOPED' | 'TRANSIENT', lock?)` 是它们的完整写法。`lock` 为 `true` 时,子类不能声明其他生命周期(定义这样的子类会抛出 `LifetimeLockedException`)。`Provider` 可以是单例,与 2.x 相同。
|
|
113
114
|
|
|
114
115
|
```typescript
|
|
115
116
|
import {Provider} from 'lakutata'
|
|
116
117
|
import {Transient} from 'lakutata/decorator/di'
|
|
117
118
|
|
|
118
|
-
|
|
119
|
+
//每个注入它的对象都得到自己的构建器实例,各自保存状态
|
|
119
120
|
@Transient()
|
|
120
121
|
export class QueryBuilder extends Provider {
|
|
121
122
|
protected readonly clauses: string[] = []
|
|
@@ -130,8 +131,8 @@ export class QueryBuilder extends Provider {
|
|
|
130
131
|
### 1.5 子类的属性
|
|
131
132
|
|
|
132
133
|
- **继承**:子类继承父类的 `@Inject` 和 `@Configurable` 属性。子类重新声明某个属性时,会取代父类对它的声明,不论使用的是哪种装饰器,以最近一层的声明为准(父类注入的属性,在子类里用 `@Configurable` 重新声明后,只按配置处理,不再注入;反之亦然)。
|
|
133
|
-
- **属性的类型**:TypeScript 允许子类在重新声明属性时把类型改窄(例如把 `Store` 改成 `MemoryStore`),但不允许换成其他类型:把 `number` 换成 `string` 会报 `TS2416`(`Property 'port' in type 'BrokenServer' is not assignable to the same property in base type 'PlainServer'
|
|
134
|
-
- 父类在类上声明泛型参数,按属性命名并以 `Type` 结尾(属性 `store` 对应 `StoreType`)。加上约束(`extends Store
|
|
134
|
+
- **属性的类型**:TypeScript 允许子类在重新声明属性时把类型改窄(例如把 `Store` 改成 `MemoryStore`),但不允许换成其他类型:把 `number` 换成 `string` 会报 `TS2416`(`Property 'port' in type 'BrokenServer' is not assignable to the same property in base type 'PlainServer'`),而且没有任何编译选项可以关闭这个检查。如果子类可能需要给某个属性换一种类型,父类就用泛型参数来声明这个属性:
|
|
135
|
+
- 父类在类上声明泛型参数,按属性命名并以 `Type` 结尾(属性 `store` 对应 `StoreType`)。加上约束(`extends Store`):父类可以按约束的类型使用这个属性,子类也只能换成兼容的类型;再加上默认类型(`= Store`):不需要替换类型的子类,继承时不用填写类型参数;
|
|
135
136
|
- 子类在 `extends` 处填入具体类型。只有需要更换装饰器时,才在子类里重新声明这个属性。
|
|
136
137
|
|
|
137
138
|
```typescript
|
|
@@ -180,9 +181,11 @@ export class NamedPortServer extends Server<string> {
|
|
|
180
181
|
|
|
181
182
|
在 `MemoryRepository` 里,IDE 会把 `this.store` 当作 `MemoryStore` 来补全;在 `Repository` 里则当作 `Store`。如果父类没有声明泛型参数,子类只能在属性上用 `// @ts-expect-error` 压制这个错误,而父类的方法仍会按原来的类型使用这个属性。
|
|
182
183
|
|
|
184
|
+
像 `NamedPortServer` 这样不带初始值地重新声明属性,在 `useDefineForClassFields: false` 下可以编译,`lakutata/tsconfig.json` 已经这样设置。设为 `true`(TypeScript 对 `ES2022` 及更新的编译目标的默认值)时,编译器会报 `TS2612`;这时在属性前加上 `declare`(`@Configurable(...) declare protected readonly port: string`),装饰器照样生效。
|
|
185
|
+
|
|
183
186
|
## 2. 控制器动作(`lakutata/decorator/ctrl`)
|
|
184
187
|
|
|
185
|
-
这些装饰器用于 `Controller` 子类的方法(`@Prefix` 和 `@UseMiddlewares` 也可用于类);详见 [
|
|
188
|
+
这些装饰器用于 `Controller` 子类的方法(`@Prefix` 和 `@UseMiddlewares` 也可用于类);详见 [Controllers](Controllers.md)。
|
|
186
189
|
|
|
187
190
|
| 装饰器 | 地址 |
|
|
188
191
|
|---|---|
|
|
@@ -224,7 +227,7 @@ CLI 命令名称前面的词是命令分组:`@CLIAction('package exe <name>',
|
|
|
224
227
|
| 装饰器 | 效果 |
|
|
225
228
|
|---|---|
|
|
226
229
|
| `@Expect(schema)` | DTO 属性的校验规则(`DTO.String().required()`) |
|
|
227
|
-
| `@IndexSignature(schema)` | DTO
|
|
230
|
+
| `@IndexSignature(schema)` | DTO 类保留没有规则的字段,并按这条规则校验它们 |
|
|
228
231
|
| `@Accept(...schemas)` | 每次调用方法时按位置校验参数 |
|
|
229
232
|
| `@Return(schema)` | 每次调用方法时校验返回值 |
|
|
230
233
|
|
|
@@ -232,7 +235,7 @@ CLI 命令名称前面的词是命令分组:`@CLIAction('package exe <name>',
|
|
|
232
235
|
|
|
233
236
|
## 4. 辅助装饰器(`lakutata/decorator/asst`)
|
|
234
237
|
|
|
235
|
-
`@Before(fn)`
|
|
238
|
+
`@Before(fn)` 在方法每次调用之前执行一个函数,传入方法的参数:函数返回的数组会替换传给方法的参数。`@After(fn)` 在每次调用之后执行一个函数,传入方法的结果:函数返回的非 `undefined` 值会替换结果。这些函数调用时 `this` 为该实例;只有方法是异步的,函数才可以是异步的。
|
|
236
239
|
|
|
237
240
|
```typescript
|
|
238
241
|
import {Component} from 'lakutata'
|
|
@@ -261,9 +264,13 @@ class Prices extends Component {
|
|
|
261
264
|
|
|
262
265
|
## 5. 锁(`lakutata/com/mutex`)
|
|
263
266
|
|
|
264
|
-
`@Synchronized`
|
|
267
|
+
`@Synchronized` 让控制器动作在 `Mutex` 组件的锁的保护下执行,锁在输入校验和访问控制通过之后获取。见 [Mutex](Mutex.md)。
|
|
268
|
+
|
|
269
|
+
## 6. 消息队列(`lakutata/com/queue`)
|
|
270
|
+
|
|
271
|
+
`@QueueAction(queue, dto?, options?)` 让控制器动作成为一个队列的消费者:`queueEntrypoint` 适配器把队列的消息投递给它。动作正常返回时确认消息,失败后重试,重试次数用完后把消息送进死信队列。见 [Queue](Queue.md)。
|
|
265
272
|
|
|
266
|
-
##
|
|
273
|
+
## 7. 数据库实体(`lakutata/decorator/orm`)
|
|
267
274
|
|
|
268
275
|
`lakutata/decorator/orm` 重新导出 TypeORM 的装饰器:
|
|
269
276
|
|
|
@@ -295,4 +302,4 @@ export class User {
|
|
|
295
302
|
}
|
|
296
303
|
```
|
|
297
304
|
|
|
298
|
-
见 [
|
|
305
|
+
见 [Entities](Entities.md)。
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Docker 组件**(`lakutata/com/docker`,`@lakutata/docker` 包)通过 Docker Engine API 操作 Docker 守护进程,不需要 Docker CLI:拉取、构建(使用 BuildKit)、导入、导出、打标签和推送镜像,创建、运行和管理容器(exec、交互式 TTY、日志、统计、commit、导出文件),以及管理网络。它可以连接本地套接字,也可以通过 HTTP、HTTPS 或 SSH 连接远程守护进程。全部选项见 `@lakutata/docker` 的 README 和 `lakutata/com/docker` 的类型声明。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
这个组件没有预置:需要在应用的 `components` 中声明。只运行 containerd、没有 Docker 的机器(嵌入式 Linux 设备)见 [ContainerdIntegration](ContainerdIntegration.md)。
|
|
6
6
|
|
|
7
7
|
## 1. 环境要求与连接
|
|
8
8
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
| TCP,HTTPS | `host`、`port`,以及 PEM 内容形式的 `ca`、`cert`、`key`(`buildDockerHttpsConnectionOptions`) |
|
|
17
17
|
| SSH | `host`、`port`、`username`、`sshAuthAgent`(`buildDockerSSHConnectionOptions`) |
|
|
18
18
|
|
|
19
|
-
组件创建时会 ping
|
|
19
|
+
组件创建时会 ping 守护进程,并协商 API 版本:守护进程连不上时,组件创建失败,抛出 `DockerConnectionException`;组件列在 `bootstrap` 里时,应用因此启动失败。`timeout`(毫秒)是请求最多可以空闲多久。
|
|
20
20
|
|
|
21
21
|
```typescript
|
|
22
22
|
import {readFileSync} from 'node:fs'
|
|
@@ -175,11 +175,11 @@ async function containers(docker: Docker): Promise<void> {
|
|
|
175
175
|
```
|
|
176
176
|
|
|
177
177
|
- `DockerContainer` 是一个快照:它自己的操作会刷新它;其他地方做的修改需要重新获取才能看到(`docker.getContainer(id)`,也接受名称或短 ID)。
|
|
178
|
-
-
|
|
179
|
-
- `exec()`
|
|
178
|
+
- 启动正在运行的容器,或者停止已停止的容器,都不会做任何事,与 Docker CLI 相同。
|
|
179
|
+
- `exec()` 运行时没有 TTY,也没有 stdin。数组形式的命令原样执行;字符串是命令行,由 `/bin/sh -c` 执行。
|
|
180
180
|
- `kill()` 默认发送 `SIGKILL`,也可以指定信号。
|
|
181
181
|
- `commit()` 用容器生成镜像,`exportDirectory()` 把容器中的目录写成 tar 归档。
|
|
182
|
-
-
|
|
182
|
+
- 不提供的功能:附加到主进程、向容器中复制文件、管理卷(`prune()` 的清理除外),以及守护进程的事件。
|
|
183
183
|
|
|
184
184
|
## 5. 网络
|
|
185
185
|
|
|
@@ -202,4 +202,4 @@ async function networks(docker: Docker): Promise<void> {
|
|
|
202
202
|
|
|
203
203
|
## 6. 错误
|
|
204
204
|
|
|
205
|
-
组件的异常由 `lakutata/com/docker` 导出:`DockerConnectionException
|
|
205
|
+
组件的异常由 `lakutata/com/docker` 导出:`DockerConnectionException`(组件创建时连不上守护进程)、`DockerContainerNotFoundException`、`DockerImageNotFoundException`、`DockerNetworkNotFoundException`、`DockerImagePullException`、`DockerImageBuildException`、`DockerImageImportException`、`DockerImagePushException`、`DockerImageRepoTagNotFoundException`,以及 `DockerRequestException`(守护进程拒绝请求;其 `statusCode` 为 HTTP 状态码,名称已被占用时为 409)。选项不合法时抛出 `InvalidMethodAcceptException`。组件被销毁时,正在进行的拉取和构建会被中止。
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Lakutata 实体、查询与事务
|
|
2
|
+
|
|
3
|
+
实体、仓储、查询和事务都来自 [TypeORM](https://typeorm.io) 1:装饰器来自 `lakutata/decorator/orm`,其余 API 来自 `lakutata/orm`。本指南介绍常见的用法,每个选项的说明见 TypeORM 的文档。
|
|
4
|
+
|
|
5
|
+
## 1. 实体
|
|
6
|
+
|
|
7
|
+
实体是映射到一张表的类:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import {
|
|
11
|
+
BeforeInsert, Column, CreateDateColumn, DeleteDateColumn, Entity, Index, JoinColumn, ManyToOne, OneToMany,
|
|
12
|
+
PrimaryGeneratedColumn, UpdateDateColumn, VersionColumn
|
|
13
|
+
} from 'lakutata/decorator/orm'
|
|
14
|
+
|
|
15
|
+
@Entity('orders')
|
|
16
|
+
export class Order {
|
|
17
|
+
@PrimaryGeneratedColumn('uuid')
|
|
18
|
+
public id: string
|
|
19
|
+
|
|
20
|
+
@Index()
|
|
21
|
+
@Column()
|
|
22
|
+
public customer: string
|
|
23
|
+
|
|
24
|
+
//随订单一起保存(级联)
|
|
25
|
+
@OneToMany(() => OrderLine, (line: OrderLine) => line.order, {cascade: true})
|
|
26
|
+
public lines: OrderLine[]
|
|
27
|
+
|
|
28
|
+
@CreateDateColumn({type: 'timestamptz'})
|
|
29
|
+
public createdAt: Date
|
|
30
|
+
|
|
31
|
+
@UpdateDateColumn({type: 'timestamptz'})
|
|
32
|
+
public updatedAt: Date
|
|
33
|
+
|
|
34
|
+
//由 softDelete() 设置:除非设置了 withDeleted,查询会跳过这个订单
|
|
35
|
+
@DeleteDateColumn({type: 'timestamptz'})
|
|
36
|
+
public deletedAt: Date | null
|
|
37
|
+
|
|
38
|
+
//每次保存加一
|
|
39
|
+
@VersionColumn()
|
|
40
|
+
public version: number
|
|
41
|
+
|
|
42
|
+
@BeforeInsert()
|
|
43
|
+
protected normalize(): void {
|
|
44
|
+
this.customer = this.customer.trim()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Entity('order_lines')
|
|
49
|
+
export class OrderLine {
|
|
50
|
+
@PrimaryGeneratedColumn()
|
|
51
|
+
public id: number
|
|
52
|
+
|
|
53
|
+
@Column()
|
|
54
|
+
public product: string
|
|
55
|
+
|
|
56
|
+
@Column('integer')
|
|
57
|
+
public quantity: number
|
|
58
|
+
|
|
59
|
+
@ManyToOne(() => Order, (order: Order) => order.lines, {onDelete: 'CASCADE'})
|
|
60
|
+
@JoinColumn({name: 'order_id'})
|
|
61
|
+
public order: Order
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
| 装饰器 | 用途 |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `@Entity(name)`、`@ViewEntity`、`@ChildEntity`、`@TableInheritance` | 实体、视图、表的继承 |
|
|
68
|
+
| `@PrimaryGeneratedColumn(strategy)`、`@PrimaryColumn` | 主键:自动生成(`'increment'`、`'uuid'`、`'identity'`),或者手动给出 |
|
|
69
|
+
| `@Column(type, options)` | 列:类型、`length`、`nullable`、`default`、`unique`、`transformer`…… |
|
|
70
|
+
| `@CreateDateColumn`、`@UpdateDateColumn`、`@DeleteDateColumn`、`@VersionColumn` | 由 TypeORM 自动设置的列 |
|
|
71
|
+
| `@OneToOne`、`@OneToMany`、`@ManyToOne`、`@ManyToMany`、`@JoinColumn`、`@JoinTable` | 关系 |
|
|
72
|
+
| `@Index`、`@Unique`、`@Check` | 索引和约束 |
|
|
73
|
+
| `@BeforeInsert`、`@AfterLoad`、`@BeforeUpdate`…… | 在操作前后执行的方法 |
|
|
74
|
+
| `@ObjectIdColumn` | MongoDB 实体的 `_id` |
|
|
75
|
+
|
|
76
|
+
实体要列在每个使用它的数据库的 `entities` 选项中。实体也可以继承 `lakutata/orm` 中的 `BaseEntity`,从而自己保存和查询(`await Order.findOneBy({id})`、`await order.save()`):这些操作使用最后一个连接了该实体的数据库。
|
|
77
|
+
|
|
78
|
+
## 2. 查询
|
|
79
|
+
|
|
80
|
+
仓储(`db.getRepository(Order)`)用来读写一张表中的实体:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import {Component} from 'lakutata'
|
|
84
|
+
import {Inject} from 'lakutata/decorator/di'
|
|
85
|
+
import {Database} from 'lakutata/com/database'
|
|
86
|
+
import {Column, CreateDateColumn, Entity, PrimaryGeneratedColumn} from 'lakutata/decorator/orm'
|
|
87
|
+
import {Between, ILike, type Repository} from 'lakutata/orm'
|
|
88
|
+
|
|
89
|
+
@Entity('orders')
|
|
90
|
+
class Order {
|
|
91
|
+
@PrimaryGeneratedColumn('uuid')
|
|
92
|
+
public id: string
|
|
93
|
+
|
|
94
|
+
@Column()
|
|
95
|
+
public customer: string
|
|
96
|
+
|
|
97
|
+
@CreateDateColumn({type: 'timestamptz'})
|
|
98
|
+
public createdAt: Date
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class Orders extends Component {
|
|
102
|
+
@Inject('db')
|
|
103
|
+
protected readonly db: Database
|
|
104
|
+
|
|
105
|
+
protected get orders(): Repository<Order> {
|
|
106
|
+
return this.db.getRepository(Order)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//某段时间内订单的一页,以及订单总数
|
|
110
|
+
public async search(customer: string, from: Date, to: Date, page: number): Promise<[Order[], number]> {
|
|
111
|
+
return await this.orders.findAndCount({
|
|
112
|
+
where: {customer: ILike(`%${customer}%`), createdAt: Between(from, to)},
|
|
113
|
+
order: {createdAt: 'DESC'},
|
|
114
|
+
skip: (page - 1) * 20,
|
|
115
|
+
take: 20
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
//查询构造器
|
|
120
|
+
public async byCustomer(customer: string): Promise<Order[]> {
|
|
121
|
+
return await this.db.createQueryBuilder(Order, 'order')
|
|
122
|
+
.where('order.customer LIKE :customer', {customer: `%${customer}%`})
|
|
123
|
+
.orderBy('order.createdAt', 'DESC')
|
|
124
|
+
.getMany()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
//SQL:占位符在 PostgreSQL 上是 $1、$2……,在 MySQL 和 SQLite 上是 ?
|
|
128
|
+
public async count(customer: string): Promise<number> {
|
|
129
|
+
const rows: { count: string }[] = await this.db.query('SELECT COUNT(*) AS count FROM orders WHERE customer = $1', [customer])
|
|
130
|
+
return Number(rows[0].count)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- **查询:** `find`、`findBy`、`findOne`、`findOneBy`、`findAndCount`、`count`、`exists`;`findOneByOrFail` 什么都没找到时会抛出 `EntityNotFoundError`。
|
|
136
|
+
- **查询选项**使用对象:`where`、`relations: {lines: true}`、`select: {id: true, customer: true}`、`order: {createdAt: 'DESC'}`、`skip` 和 `take`、`withDeleted`。`where` 中给出条件列表表示 `OR`。
|
|
137
|
+
- **查询操作符**来自 `lakutata/orm`:`In`、`Not`、`IsNull`、`Like`、`ILike`、`Between`、`MoreThan`、`MoreThanOrEqual`、`LessThan`、`LessThanOrEqual`、`Any`、`ArrayContains`、`Raw`……
|
|
138
|
+
- **写入:** `save`(插入或更新,并按关系的级联设置一起保存)、`insert`、`update`、`delete`、`softDelete` 和 `restore`(需要 `@DeleteDateColumn`)、`increment`、`upsert`。
|
|
139
|
+
- **查询构造器**(`createQueryBuilder`)用来写查询选项表达不了的查询:带条件的连接、分组、子查询。它的值(`:customer`)作为参数传递,从不拼接进 SQL 文本。
|
|
140
|
+
- **SQL**(`query(sql, parameters)`)原样返回驱动给出的行:PostgreSQL 的 `COUNT(*)` 是字符串。
|
|
141
|
+
|
|
142
|
+
## 3. 事务
|
|
143
|
+
|
|
144
|
+
`transaction(fn)` 在事务中执行一个函数:函数成功时提交,失败时回滚,并重新抛出它的错误。事务中的每个操作都要通过传给函数的实体管理器执行;通过 `db.manager` 或 `db.getRepository()` 执行的操作不在事务中。
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import {Component} from 'lakutata'
|
|
148
|
+
import {Inject} from 'lakutata/decorator/di'
|
|
149
|
+
import {Database} from 'lakutata/com/database'
|
|
150
|
+
import {Column, Entity, PrimaryGeneratedColumn} from 'lakutata/decorator/orm'
|
|
151
|
+
import {type EntityManager} from 'lakutata/orm'
|
|
152
|
+
|
|
153
|
+
@Entity('accounts')
|
|
154
|
+
class Account {
|
|
155
|
+
@PrimaryGeneratedColumn()
|
|
156
|
+
public id: number
|
|
157
|
+
|
|
158
|
+
@Column('integer')
|
|
159
|
+
public balance: number
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
class Transfers extends Component {
|
|
163
|
+
@Inject('db')
|
|
164
|
+
protected readonly db: Database
|
|
165
|
+
|
|
166
|
+
//两个更新要么都完成,要么都不完成
|
|
167
|
+
public async transfer(from: number, to: number, amount: number): Promise<void> {
|
|
168
|
+
await this.db.transaction('SERIALIZABLE', async (manager: EntityManager): Promise<void> => {
|
|
169
|
+
await manager.decrement(Account, {id: from}, 'balance', amount)
|
|
170
|
+
await manager.increment(Account, {id: to}, 'balance', amount)
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- 隔离级别可以省略:`'READ UNCOMMITTED'`、`'READ COMMITTED'`、`'REPEATABLE READ'`、`'SERIALIZABLE'`。
|
|
177
|
+
- `createQueryRunner()` 提供一个独占的连接,用来手动控制事务(`startTransaction()`、`commitTransaction()`、`rollbackTransaction()`,最后 `release()`)。
|
|
178
|
+
- MongoDB 和嵌入式 MongoDB 没有事务:在它们上面,`transaction()` 会在没有事务的情况下执行函数。
|
|
179
|
+
|
|
180
|
+
## 4. 需要注意的值
|
|
181
|
+
|
|
182
|
+
- **大数。** PostgreSQL 和 MySQL 以字符串返回 `bigint` 和 `decimal` 列,以保持精度。SQLite 以数字返回它们,超过 2^53 时会丢失精度。如果数字就够用,可以用列的 `transformer` 转换:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import {Column, Entity, PrimaryGeneratedColumn} from 'lakutata/decorator/orm'
|
|
186
|
+
import type {ValueTransformer} from 'lakutata/orm'
|
|
187
|
+
|
|
188
|
+
const decimal: ValueTransformer = {
|
|
189
|
+
to: (value: number): number => value,
|
|
190
|
+
from: (value: string | null): number | null => value === null ? null : Number(value)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@Entity('invoices')
|
|
194
|
+
export class Invoice {
|
|
195
|
+
@PrimaryGeneratedColumn()
|
|
196
|
+
public id: number
|
|
197
|
+
|
|
198
|
+
@Column({type: 'decimal', precision: 10, scale: 2, transformer: decimal})
|
|
199
|
+
public total: number
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
- **条件中的 `null` 和 `undefined`** 默认会被忽略:`findOneBy({id: undefined})` 会查到第一行。见 [Database](Database.md) 第 2 节。
|
|
204
|
+
- **日期。** 优先使用带时区的列(PostgreSQL 的 `timestamptz`)。在 MySQL 上,连接的 `timezone` 选项决定日期如何写入和读取。
|
|
205
|
+
|
|
206
|
+
## 5. 错误
|
|
207
|
+
|
|
208
|
+
错误都来自 TypeORM,由 `lakutata/orm` 导出:
|
|
209
|
+
|
|
210
|
+
| 错误 | 什么时候 |
|
|
211
|
+
|---|---|
|
|
212
|
+
| `QueryFailedError` | 数据库拒绝了查询。它的 `driverError` 是驱动的错误:在 PostgreSQL 上,代码 `23505` 表示重复键,`constraint` 是约束的名字 |
|
|
213
|
+
| `EntityNotFoundError` | `findOneByOrFail()` 和 `findOneOrFail()` 什么都没找到 |
|
|
214
|
+
| `EntityMetadataNotFoundError` | 实体不在数据库的 `entities` 中 |
|
|
215
|
+
| `OptimisticLockVersionMismatchError` | 加锁时 `@VersionColumn` 与期望的版本不一致 |
|
|
216
|
+
| `CannotExecuteNotConnectedError` | 在没有连接或已经断开的数据库上查询 |
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import {QueryFailedError} from 'lakutata/orm'
|
|
220
|
+
|
|
221
|
+
export function isDuplicate(error: unknown): boolean {
|
|
222
|
+
return error instanceof QueryFailedError && (error.driverError as { code?: string }).code === '23505'
|
|
223
|
+
}
|
|
224
|
+
```
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Lakutata Entrypoint
|
|
2
|
+
|
|
3
|
+
`entrypoint` 组件把控制器和各种传输方式连接起来。它持有控制器和各传输方式的适配器(HTTP、命令行、服务调用、定时任务、消息队列……):每个适配器接收自己传输方式的请求,入口则执行对应的动作,连同它的中间件、校验和访问控制。
|
|
4
|
+
|
|
5
|
+
## 1. 声明入口
|
|
6
|
+
|
|
7
|
+
`lakutata` 包已经声明了一个名为 `entrypoint` 的组件。应用用 `buildEntrypoints` 在同一个名字下给它提供选项,并把它列入 `bootstrap`:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import {Application} from 'lakutata'
|
|
11
|
+
import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
|
|
12
|
+
import {GET} from 'lakutata/decorator/ctrl'
|
|
13
|
+
import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
|
|
14
|
+
|
|
15
|
+
class HealthController extends Controller {
|
|
16
|
+
@GET('/health')
|
|
17
|
+
public async health(): Promise<string> {
|
|
18
|
+
return 'ok'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Application.run(() => ({
|
|
23
|
+
id: 'api.app',
|
|
24
|
+
name: 'API',
|
|
25
|
+
components: {
|
|
26
|
+
entrypoint: buildEntrypoints({
|
|
27
|
+
controllers: [HealthController],
|
|
28
|
+
http: expressHTTPEntrypoint({port: 8080})
|
|
29
|
+
})
|
|
30
|
+
},
|
|
31
|
+
bootstrap: ['entrypoint']
|
|
32
|
+
}))
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
组件在第一次被需要时才会创建。没有其他对象需要入口,所以如果没有 `bootstrap: ['entrypoint']`,它永远不会被创建:服务不会监听,命令不会执行,定时任务也不会开始。
|
|
36
|
+
|
|
37
|
+
## 2. 选项
|
|
38
|
+
|
|
39
|
+
| 选项 | 默认值 | 含义 |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `controllers` | `[]` | 控制器类 |
|
|
42
|
+
| `http`、`cli`、`service`、`schedule`、`queue`、`mcp`、`dbus` | | 各传输方式的适配器(见下表):一个适配器,或者适配器数组 |
|
|
43
|
+
| `middlewares` | `[]` | 所有动作的中间件(见 [Middlewares](Middlewares.md)) |
|
|
44
|
+
| `rules` | `[]` | 声明了 `acl: true` 的动作使用的访问规则(见 [Middlewares](Middlewares.md)) |
|
|
45
|
+
| `shutdownTimeout` | `30000` | 应用停止时,留给正在处理的请求的毫秒数(第 4 节) |
|
|
46
|
+
| `httpActionGroups`、`cliActionGroups`、`serviceActionGroups` | `{}` | 动作可以使用的分组及其名称(第 5 节) |
|
|
47
|
+
|
|
48
|
+
适配器:
|
|
49
|
+
|
|
50
|
+
| 选项 | 适配器 | 导入自 | 需要安装 | 指南 |
|
|
51
|
+
|---|---|---|---|---|
|
|
52
|
+
| `http` | `expressHTTPEntrypoint()` | `lakutata/adapter/express` | `express` | [HTTP](HTTP.md) |
|
|
53
|
+
| `http` | `fastifyHTTPEntrypoint()` | `lakutata/adapter/fastify` | `fastify` | [HTTP](HTTP.md) |
|
|
54
|
+
| `cli` | `commanderCLIEntrypoint()` | `lakutata/adapter/commander` | `commander` | [CLI](CLI.md) |
|
|
55
|
+
| `service` | `httpServiceEntrypoint()` | `lakutata/com/entrypoint` | | [ServiceCalls](ServiceCalls.md) |
|
|
56
|
+
| `service` | `socketIOServiceEntrypoint()` | `lakutata/adapter/socket.io` | `socket.io` | [ServiceCalls](ServiceCalls.md) |
|
|
57
|
+
| `service` | `jsonRPCServiceEntrypoint()` | `lakutata/adapter/jsonrpc` | `ws`(WebSocket 时) | [ServiceCalls](ServiceCalls.md) |
|
|
58
|
+
| `service` | `electronIPCServiceEntrypoint()` | `lakutata/adapter/electron` | (Electron) | [ServiceCalls](ServiceCalls.md) |
|
|
59
|
+
| `service` | `grpcServiceEntrypoint()` | `@lakutata/grpc/service` | `@lakutata/grpc` | [GRPC](GRPC.md) |
|
|
60
|
+
| `service` | `natsServiceEntrypoint()` | `@lakutata/nats` | `@lakutata/nats` | [NATS](NATS.md) |
|
|
61
|
+
| `schedule` | `scheduleEntrypoint()` | `lakutata/com/entrypoint` | | [Schedules](Schedules.md) |
|
|
62
|
+
| `queue` | `queueEntrypoint()` | `lakutata/com/queue` | 队列服务器的客户端 | [Queue](Queue.md) |
|
|
63
|
+
| `mcp` | `httpMCPEntrypoint()`、`stdioMCPEntrypoint()` | `@lakutata/mcp` | `@lakutata/mcp` | [MCP](MCP.md) |
|
|
64
|
+
| `dbus` | `dbusEntrypoint()` | `@lakutata/dbus` | `@lakutata/dbus` | [DBus](DBus.md) |
|
|
65
|
+
|
|
66
|
+
## 3. 多个适配器
|
|
67
|
+
|
|
68
|
+
每个选项都可以是数组,用多种方式提供同一种传输。比如服务动作同时通过 Socket.IO 提供给浏览器、通过 JSON-RPC 提供给脚本,或者 HTTP 动作同时监听两个端口:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import {buildEntrypoints} from 'lakutata/com/entrypoint'
|
|
72
|
+
import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
|
|
73
|
+
import {jsonRPCServiceEntrypoint} from 'lakutata/adapter/jsonrpc'
|
|
74
|
+
import {socketIOServiceEntrypoint} from 'lakutata/adapter/socket.io'
|
|
75
|
+
|
|
76
|
+
export const entrypoint = buildEntrypoints({
|
|
77
|
+
controllers: [],
|
|
78
|
+
http: [
|
|
79
|
+
expressHTTPEntrypoint({name: 'public', port: 8080}),
|
|
80
|
+
expressHTTPEntrypoint({name: 'internal', host: '127.0.0.1', port: 9090})
|
|
81
|
+
],
|
|
82
|
+
service: [
|
|
83
|
+
socketIOServiceEntrypoint({port: 3000}),
|
|
84
|
+
jsonRPCServiceEntrypoint({port: 4000, path: '/rpc'})
|
|
85
|
+
]
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
一种传输方式的每个适配器都提供该传输方式的全部动作。`name` 选项用来区分这些适配器。
|
|
90
|
+
|
|
91
|
+
## 4. 启动与停止
|
|
92
|
+
|
|
93
|
+
**应用启动时**,入口先检查控制器。下面任何一种错误都会让启动失败,并抛出对应的异常:
|
|
94
|
+
|
|
95
|
+
- 同一传输方式中有两个同名的动作(`DuplicateActionNameException`);
|
|
96
|
+
- 动作使用的分组没有在对应的 `*ActionGroups` 选项中声明(`InvalidActionGroupException`);
|
|
97
|
+
- 前缀与某个动作的模式冲突(`InvalidPrefixException`);
|
|
98
|
+
- 中间件既不是函数,也不是 `Middleware` 类(`InvalidMiddlewareException`);
|
|
99
|
+
- 适配器放在了另一种传输方式的键下,或者某个选项既不是入口的选项,也不是传输方式(`InvalidEntrypointAdapterException`)。
|
|
100
|
+
|
|
101
|
+
然后按选项的顺序依次启动各个适配器。某个适配器启动失败(比如端口已被占用)时,应用启动失败。命令行和定时任务在整个应用启动完成之后才开始。
|
|
102
|
+
|
|
103
|
+
**应用停止时**(收到 `SIGTERM` 或 `SIGINT`,或者调用了 `app.exit()`),适配器按相反的顺序停止:
|
|
104
|
+
|
|
105
|
+
1. 不再接受新请求。视传输方式而定,仍然到达的请求会以 `EntrypointStoppedException` 被拒绝(HTTP 上是状态码 503);
|
|
106
|
+
2. 正在处理的请求有 `shutdownTimeout`(默认 30 秒)可以完成;
|
|
107
|
+
3. 仍在执行的请求被中止:它们的 `this.signal` 被触发,并以 `EntrypointStoppedException` 回复;
|
|
108
|
+
4. 适配器关闭各自的服务器和连接。
|
|
109
|
+
|
|
110
|
+
## 5. 分组与动作列表
|
|
111
|
+
|
|
112
|
+
动作的 `groups` 选项把它放进分组。访问规则可以使用分组(见 [Middlewares](Middlewares.md)),入口也可以按分组列出动作,用来生成 API 目录或权限列表。HTTP、命令行和服务动作的分组必须连同名称一起声明:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import {Application} from 'lakutata'
|
|
116
|
+
import {buildEntrypoints, Controller, type Entrypoint} from 'lakutata/com/entrypoint'
|
|
117
|
+
import {DELETE, GET} from 'lakutata/decorator/ctrl'
|
|
118
|
+
import {Inject} from 'lakutata/decorator/di'
|
|
119
|
+
|
|
120
|
+
class UserController extends Controller {
|
|
121
|
+
@Inject('entrypoint')
|
|
122
|
+
protected readonly entrypoint: Entrypoint
|
|
123
|
+
|
|
124
|
+
@GET('/users', {description: 'List the users'})
|
|
125
|
+
public async list(): Promise<string[]> {
|
|
126
|
+
return []
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@DELETE('/users/:id', {description: 'Delete a user', groups: ['admin']})
|
|
130
|
+
public async remove(): Promise<void> {
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
//每个分组的动作:[{"group":"Administration","actions":["Delete a user"]}]
|
|
134
|
+
@GET('/permissions')
|
|
135
|
+
public async permissions(): Promise<{ group: string, actions: string[] }[]> {
|
|
136
|
+
return this.entrypoint.getHttpActions().groups.map((group) => ({
|
|
137
|
+
group: group.name,
|
|
138
|
+
actions: group.actions.map((action): string => action.description)
|
|
139
|
+
}))
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
Application.run(() => ({
|
|
144
|
+
id: 'users.app',
|
|
145
|
+
name: 'Users',
|
|
146
|
+
components: {
|
|
147
|
+
entrypoint: buildEntrypoints({
|
|
148
|
+
controllers: [UserController],
|
|
149
|
+
httpActionGroups: {admin: 'Administration'}
|
|
150
|
+
})
|
|
151
|
+
},
|
|
152
|
+
bootstrap: ['entrypoint']
|
|
153
|
+
}))
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`getHttpActions()`、`getCliActions()` 和 `getServiceActions()` 返回 `{groups, actions}`:`groups` 是已声明的分组(`id`、`name` 及其 `actions`),`actions` 是该传输方式的全部动作。其他传输方式的动作用 `getActions(channel)` 列出(比如 `getActions('schedule')`)。每个动作由以下属性描述:
|
|
157
|
+
|
|
158
|
+
| 属性 | 含义 |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `id` | 由控制器、方法和地址计算出的固定 id |
|
|
161
|
+
| `name`、`description`、`groups`、`acl` | 它的选项 |
|
|
162
|
+
| `controller`、`method` | 控制器类名和方法名 |
|
|
163
|
+
| `route` / `command` / `pattern` | 它的地址,包含前缀 |
|
|
164
|
+
| `jsonSchema` | 它的 DTO 的 JSON schema |
|
|
165
|
+
|
|
166
|
+
## 6. 向客户端推送事件
|
|
167
|
+
|
|
168
|
+
保持客户端连接的服务传输方式(Socket.IO、Electron、HTTP 服务的事件流、WebSocket 或 stdio 上的 JSON-RPC、gRPC)可以接收应用推送的事件。`entrypoint.notify(clientId, event, ...args)` 把事件发给一个客户端,即某个动作从 `this.context.clientId` 读到的那个客户端,返回客户端是否收到;`entrypoint.broadcast(event, ...args)` 发给所有已连接的客户端,返回客户端的数量。见 [GRPC](GRPC.md) 和 [ServiceCalls](ServiceCalls.md)。
|
|
169
|
+
|
|
170
|
+
## 7. 定制入口
|
|
171
|
+
|
|
172
|
+
`Entrypoint` 的子类可以替换组件的类:用 `class` 声明它,其他选项照常写在旁边。
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import {AccessControl, Entrypoint} from 'lakutata/com/entrypoint'
|
|
176
|
+
|
|
177
|
+
//记录访问控制的每一次判断
|
|
178
|
+
class AuditedAccessControl extends AccessControl {
|
|
179
|
+
protected async match(): Promise<boolean> {
|
|
180
|
+
const allowed: boolean = await super.match()
|
|
181
|
+
console.log(`${this.target.name}: ${allowed ? 'allowed' : 'denied'}`)
|
|
182
|
+
return allowed
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export class AuditedEntrypoint extends Entrypoint {
|
|
187
|
+
protected readonly accessControl: typeof AccessControl = AuditedAccessControl
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
//components: {entrypoint: {class: AuditedEntrypoint, controllers: [...], http: ...}}, bootstrap: ['entrypoint']
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
其他传输方式的适配器需要实现 `@lakutata/entrypoint/channel` 中的 `EntrypointAdapter` 接口。lakutata 2.x 的入口函数(`buildHTTPEntrypoint`、`buildCLIEntrypoint`、`buildServiceEntrypoint`)仍然可以用在 `http`、`cli` 和 `service` 选项中。
|