@lakutata/entrypoint 3.0.0-beta.6 → 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 (129) hide show
  1. package/README.md +39 -26
  2. package/README.zh-CN.md +182 -0
  3. package/dist/cjs/Entrypoint.d.ts +13 -7
  4. package/dist/cjs/Entrypoint.js +12 -6
  5. package/dist/cjs/adapter/commander/CommanderCLIEntrypoint.d.ts +3 -2
  6. package/dist/cjs/adapter/commander/CommanderCLIEntrypoint.js +3 -2
  7. package/dist/cjs/adapter/electron/ElectronIPCServiceEntrypoint.d.ts +2 -1
  8. package/dist/cjs/adapter/electron/ElectronIPCServiceEntrypoint.js +2 -1
  9. package/dist/cjs/adapter/express/ExpressHTTPEntrypoint.d.ts +3 -2
  10. package/dist/cjs/adapter/express/ExpressHTTPEntrypoint.js +8 -2
  11. package/dist/cjs/adapter/express/Responders.d.ts +2 -1
  12. package/dist/cjs/adapter/express/Responders.js +4 -2
  13. package/dist/cjs/adapter/express/StaticUI.d.ts +2 -1
  14. package/dist/cjs/adapter/express/StaticUI.js +2 -1
  15. package/dist/cjs/adapter/fastify/FastifyHTTPEntrypoint.d.ts +2 -1
  16. package/dist/cjs/adapter/fastify/FastifyHTTPEntrypoint.js +4 -1
  17. package/dist/cjs/adapter/fastify/Responders.d.ts +2 -1
  18. package/dist/cjs/adapter/fastify/Responders.js +4 -2
  19. package/dist/cjs/adapter/http/HTTPServiceEntrypoint.d.ts +4 -3
  20. package/dist/cjs/adapter/http/HTTPServiceEntrypoint.js +10 -22
  21. package/dist/cjs/adapter/http/HTTPServiceProxy.d.ts +190 -0
  22. package/dist/cjs/adapter/http/HTTPServiceProxy.js +371 -0
  23. package/dist/cjs/adapter/jsonrpc/JSONRPCServiceEntrypoint.d.ts +11 -7
  24. package/dist/cjs/adapter/jsonrpc/JSONRPCServiceEntrypoint.js +15 -26
  25. package/dist/cjs/adapter/schedule/ScheduleEntrypoint.d.ts +2 -1
  26. package/dist/cjs/adapter/schedule/ScheduleEntrypoint.js +12 -5
  27. package/dist/cjs/adapter/socket.io/SocketIOServiceEntrypoint.d.ts +2 -1
  28. package/dist/cjs/adapter/socket.io/SocketIOServiceEntrypoint.js +2 -1
  29. package/dist/cjs/channel/BuiltinChannels.js +4 -2
  30. package/dist/cjs/channel/EntrypointAdapter.d.ts +2 -2
  31. package/dist/cjs/decorators/HTTPAction.d.ts +5 -5
  32. package/dist/cjs/decorators/Schedule.d.ts +4 -2
  33. package/dist/cjs/decorators/Schedule.js +3 -2
  34. package/dist/cjs/decorators/ServiceAction.d.ts +12 -8
  35. package/dist/cjs/decorators/http/DELETE.d.ts +5 -5
  36. package/dist/cjs/decorators/http/GET.d.ts +7 -7
  37. package/dist/cjs/decorators/http/HEAD.d.ts +9 -9
  38. package/dist/cjs/decorators/http/OPTIONS.d.ts +8 -7
  39. package/dist/cjs/decorators/http/PATCH.d.ts +5 -5
  40. package/dist/cjs/decorators/http/POST.d.ts +5 -5
  41. package/dist/cjs/decorators/http/PUT.d.ts +7 -7
  42. package/dist/cjs/exceptions/AccessDeniedException.d.ts +7 -1
  43. package/dist/cjs/exceptions/AccessDeniedException.js +8 -3
  44. package/dist/cjs/exceptions/EntrypointStoppedException.d.ts +2 -1
  45. package/dist/cjs/exceptions/EntrypointStoppedException.js +2 -1
  46. package/dist/cjs/exceptions/InvalidActionGroupException.d.ts +4 -0
  47. package/dist/cjs/exceptions/InvalidActionGroupException.js +4 -0
  48. package/dist/cjs/exceptions/ServiceUnavailableException.d.ts +15 -0
  49. package/dist/cjs/exceptions/ServiceUnavailableException.js +18 -0
  50. package/dist/cjs/exports/Entrypoint.d.ts +3 -0
  51. package/dist/cjs/exports/Entrypoint.js +6 -1
  52. package/dist/cjs/lib/AccessControl.d.ts +2 -1
  53. package/dist/cjs/lib/AccessControl.js +2 -1
  54. package/dist/cjs/lib/AccessControlRule.d.ts +2 -1
  55. package/dist/cjs/lib/AccessControlRule.js +2 -1
  56. package/dist/cjs/lib/ExplicitStatus.d.ts +11 -0
  57. package/dist/cjs/lib/ExplicitStatus.js +22 -0
  58. package/dist/cjs/lib/Middleware.d.ts +2 -1
  59. package/dist/cjs/lib/Middleware.js +2 -1
  60. package/dist/cjs/lib/Preflight.d.ts +8 -0
  61. package/dist/cjs/lib/Preflight.js +13 -0
  62. package/dist/cjs/lib/ReadBody.d.ts +9 -0
  63. package/dist/cjs/lib/ReadBody.js +25 -0
  64. package/dist/cjs/lib/pattern/FlattenPattern.d.ts +14 -0
  65. package/dist/cjs/lib/pattern/FlattenPattern.js +59 -0
  66. package/dist/esm/Entrypoint.js +12 -6
  67. package/dist/esm/adapter/commander/CommanderCLIEntrypoint.js +3 -2
  68. package/dist/esm/adapter/electron/ElectronIPCServiceEntrypoint.js +2 -1
  69. package/dist/esm/adapter/express/ExpressHTTPEntrypoint.js +8 -2
  70. package/dist/esm/adapter/express/Responders.js +4 -2
  71. package/dist/esm/adapter/express/StaticUI.js +2 -1
  72. package/dist/esm/adapter/fastify/FastifyHTTPEntrypoint.js +4 -1
  73. package/dist/esm/adapter/fastify/Responders.js +4 -2
  74. package/dist/esm/adapter/http/HTTPServiceEntrypoint.js +9 -21
  75. package/dist/esm/adapter/http/HTTPServiceProxy.js +366 -0
  76. package/dist/esm/adapter/jsonrpc/JSONRPCServiceEntrypoint.js +14 -25
  77. package/dist/esm/adapter/schedule/ScheduleEntrypoint.js +12 -5
  78. package/dist/esm/adapter/socket.io/SocketIOServiceEntrypoint.js +2 -1
  79. package/dist/esm/channel/BuiltinChannels.js +4 -2
  80. package/dist/esm/decorators/Schedule.js +3 -2
  81. package/dist/esm/exceptions/AccessDeniedException.js +8 -3
  82. package/dist/esm/exceptions/EntrypointStoppedException.js +2 -1
  83. package/dist/esm/exceptions/InvalidActionGroupException.js +4 -0
  84. package/dist/esm/exceptions/ServiceUnavailableException.js +14 -0
  85. package/dist/esm/exports/Entrypoint.js +3 -0
  86. package/dist/esm/lib/AccessControl.js +2 -1
  87. package/dist/esm/lib/AccessControlRule.js +2 -1
  88. package/dist/esm/lib/ExplicitStatus.js +18 -0
  89. package/dist/esm/lib/Middleware.js +2 -1
  90. package/dist/esm/lib/Preflight.js +10 -0
  91. package/dist/esm/lib/ReadBody.js +22 -0
  92. package/dist/esm/lib/pattern/FlattenPattern.js +55 -0
  93. package/dist/types/Entrypoint.d.ts +13 -7
  94. package/dist/types/adapter/commander/CommanderCLIEntrypoint.d.ts +3 -2
  95. package/dist/types/adapter/electron/ElectronIPCServiceEntrypoint.d.ts +2 -1
  96. package/dist/types/adapter/express/ExpressHTTPEntrypoint.d.ts +3 -2
  97. package/dist/types/adapter/express/Responders.d.ts +2 -1
  98. package/dist/types/adapter/express/StaticUI.d.ts +2 -1
  99. package/dist/types/adapter/fastify/FastifyHTTPEntrypoint.d.ts +2 -1
  100. package/dist/types/adapter/fastify/Responders.d.ts +2 -1
  101. package/dist/types/adapter/http/HTTPServiceEntrypoint.d.ts +4 -3
  102. package/dist/types/adapter/http/HTTPServiceProxy.d.ts +190 -0
  103. package/dist/types/adapter/jsonrpc/JSONRPCServiceEntrypoint.d.ts +11 -7
  104. package/dist/types/adapter/schedule/ScheduleEntrypoint.d.ts +2 -1
  105. package/dist/types/adapter/socket.io/SocketIOServiceEntrypoint.d.ts +2 -1
  106. package/dist/types/channel/EntrypointAdapter.d.ts +2 -2
  107. package/dist/types/decorators/HTTPAction.d.ts +5 -5
  108. package/dist/types/decorators/Schedule.d.ts +4 -2
  109. package/dist/types/decorators/ServiceAction.d.ts +12 -8
  110. package/dist/types/decorators/http/DELETE.d.ts +5 -5
  111. package/dist/types/decorators/http/GET.d.ts +7 -7
  112. package/dist/types/decorators/http/HEAD.d.ts +9 -9
  113. package/dist/types/decorators/http/OPTIONS.d.ts +8 -7
  114. package/dist/types/decorators/http/PATCH.d.ts +5 -5
  115. package/dist/types/decorators/http/POST.d.ts +5 -5
  116. package/dist/types/decorators/http/PUT.d.ts +7 -7
  117. package/dist/types/exceptions/AccessDeniedException.d.ts +7 -1
  118. package/dist/types/exceptions/EntrypointStoppedException.d.ts +2 -1
  119. package/dist/types/exceptions/InvalidActionGroupException.d.ts +4 -0
  120. package/dist/types/exceptions/ServiceUnavailableException.d.ts +15 -0
  121. package/dist/types/exports/Entrypoint.d.ts +3 -0
  122. package/dist/types/lib/AccessControl.d.ts +2 -1
  123. package/dist/types/lib/AccessControlRule.d.ts +2 -1
  124. package/dist/types/lib/ExplicitStatus.d.ts +11 -0
  125. package/dist/types/lib/Middleware.d.ts +2 -1
  126. package/dist/types/lib/Preflight.d.ts +8 -0
  127. package/dist/types/lib/ReadBody.d.ts +9 -0
  128. package/dist/types/lib/pattern/FlattenPattern.d.ts +14 -0
  129. package/package.json +35 -4
package/README.md CHANGED
@@ -1,13 +1,15 @@
1
1
  # @lakutata/entrypoint
2
2
 
3
- The entrypoint of the lakutata framework: the controllers and their actions, served by transports. One controller
4
- method can be an HTTP route, a CLI command, a service action (invoked over HTTP, Socket.IO, JSON-RPC, gRPC, NATS or
5
- Electron IPC) and a scheduled job at the same time; its input is validated by its DTO, checked by the access control and
6
- passed through the middlewares the same way whatever the transport.
3
+ English | [中文](README.zh-CN.md)
7
4
 
8
- The umbrella package `lakutata` re-exports it: `lakutata/com/entrypoint` (the component, the controllers, the
9
- contexts, the results), `lakutata/decorator/ctrl` (the action decorators) and `lakutata/adapter/*` (the transports),
10
- and presets the `entrypoint` component of the applications.
5
+ The entrypoint of the lakutata framework: controllers and their actions, served by transports. One controller method
6
+ can be an HTTP route, a CLI command, a service action (called over HTTP, Socket.IO, JSON-RPC, gRPC, NATS or Electron
7
+ IPC) and a scheduled job at the same time. Whatever the transport, a request goes through the same middlewares, its
8
+ input is validated by the same DTO, and the same access control applies.
9
+
10
+ The umbrella package `lakutata` re-exports it as `lakutata/com/entrypoint` (the component, controllers, contexts and
11
+ results), `lakutata/decorator/ctrl` (the action decorators) and `lakutata/adapter/*` (the transports), and presets the
12
+ `entrypoint` component in every application.
11
13
 
12
14
  ## Installation
13
15
 
@@ -15,7 +17,7 @@ and presets the `entrypoint` component of the applications.
15
17
  npm install lakutata
16
18
  ```
17
19
 
18
- The transports use optional peer dependencies, installed when used:
20
+ The transports rely on optional peer dependencies, which you install only when you use them:
19
21
 
20
22
  | Transport | Subpath | Install |
21
23
  |---|---|---|
@@ -71,7 +73,7 @@ class OrderController extends Controller {
71
73
  return input
72
74
  }
73
75
 
74
- //Every 5 minutes (with {every: '5m', exclusive: true} and a mutex component: in one instance at a time)
76
+ //Every 5 minutes (with {every: '5m', exclusive: true} and a mutex component, in one instance at a time)
75
77
  @Schedule('5m')
76
78
  public async report(): Promise<void> {
77
79
  }
@@ -93,15 +95,18 @@ Application.run(() => ({
93
95
  }))
94
96
  ```
95
97
 
96
- ### The request of an action
98
+ ### Inside an action
97
99
 
98
- - `this.context` is the context of the request: `HTTPContext` (its `request`, `response`, `route`, `method`),
99
- `CLIContext` (its `command`), `ServiceContext` (its `clientId`) or `ScheduleContext`.
100
- - `this.signal` is aborted when the request is: its client gone, its deadline exceeded, the entrypoint stopping.
101
- - A controller is created for each request (`@Scoped`): its injected objects of the request are shared by the request.
102
- - The value returned is the response. `Result.status(201, data, headers)`, `Result.file(path)` and `Result.events()`
100
+ - `this.context` is the request's context: `HTTPContext` (with `request`, `response`, `route`, `method`),
101
+ `CLIContext` (with `command`), `ServiceContext` (with `clientId`) or `ScheduleContext`.
102
+ - `this.signal` is aborted when the request is aborted: the client goes away, the deadline passes or the entrypoint
103
+ stops.
104
+ - A controller is created for each request (`@Scoped`), and the request-scoped objects it injects are shared within
105
+ that request.
106
+ - The returned value is the response. `Result.status(201, data, headers)`, `Result.file(path)` and `Result.events()`
103
107
  (Server-Sent Events) control the HTTP response.
104
- - An exception is answered with its `statusCode` (HTTP) and its `errno`, `message` (JSON); an invalid input with 400.
108
+ - Over HTTP (Express, Fastify), an exception is answered with its `statusCode` and the JSON `{errno, errMsg}`, and
109
+ invalid input with 400.
105
110
 
106
111
  ### Middlewares and access control
107
112
 
@@ -109,8 +114,9 @@ Application.run(() => ({
109
114
  import {Application} from 'lakutata'
110
115
  import {buildEntrypoints, Controller, type ActionInvocation, type Next} from 'lakutata/com/entrypoint'
111
116
  import {GET, UseMiddlewares} from 'lakutata/decorator/ctrl'
117
+ import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
112
118
 
113
- //Runs around the action: before its input validation and its access control
119
+ //Runs around the action, before input validation and access control
114
120
  async function timing(invocation: ActionInvocation, next: Next): Promise<unknown> {
115
121
  const started: number = Date.now()
116
122
  try {
@@ -120,6 +126,7 @@ async function timing(invocation: ActionInvocation, next: Next): Promise<unknown
120
126
  }
121
127
  }
122
128
 
129
+ //For the actions of this controller (buildEntrypoints({middlewares: [timing]}) would run it for every action)
123
130
  @UseMiddlewares(timing)
124
131
  class HealthController extends Controller {
125
132
  @GET('/health')
@@ -132,13 +139,14 @@ Application.run(() => ({
132
139
  id: 'health.app',
133
140
  name: 'Health',
134
141
  components: {
135
- entrypoint: buildEntrypoints({controllers: [HealthController], middlewares: [timing]})
136
- }
142
+ entrypoint: buildEntrypoints({controllers: [HealthController], http: expressHTTPEntrypoint({port: 3000})})
143
+ },
144
+ bootstrap: ['entrypoint']
137
145
  }))
138
146
  ```
139
147
 
140
- The access control: an action declared with `{acl: true}` is checked by the rules given to `buildEntrypoints({rules})`
141
- (`AccessControlRule` subclasses, created for each request); a rule resolving `false` denies it with
148
+ Access control: an action declared with `{acl: true}` is checked by the rules given to `buildEntrypoints({rules})`
149
+ (`AccessControlRule` subclasses, created for each request). A rule that resolves to `false` denies the request with
142
150
  `AccessDeniedException` (HTTP 403).
143
151
 
144
152
  ## API
@@ -152,11 +160,12 @@ The access control: an action declared with `{acl: true}` is checked by the rule
152
160
  | `AccessControlRule`, `AccessControl` | `lakutata/com/entrypoint` | The access control |
153
161
  | `Middleware`, `ActionInvocation`, `Next` | `lakutata/com/entrypoint` | The middlewares |
154
162
  | `httpServiceEntrypoint`, `scheduleEntrypoint` | `lakutata/com/entrypoint` | The service over HTTP, the schedules |
163
+ | `HTTPServiceProxy`, `buildHTTPServiceProxy` | `lakutata/com/entrypoint` | The client of a service over HTTP: its actions and its events |
155
164
  | `Cron`, `parseDuration` | `lakutata/com/entrypoint` | Cron expressions and durations |
156
165
  | `HTTPAction`, `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS` | `lakutata/decorator/ctrl` | HTTP routes |
157
166
  | `CLIAction`, `ServiceAction`, `Schedule` | `lakutata/decorator/ctrl` | Commands, service actions, scheduled jobs |
158
167
  | `Prefix`, `UseMiddlewares` | `lakutata/decorator/ctrl` | A prefix for the routes, commands and patterns of a controller; its middlewares |
159
- | `expressHTTPEntrypoint`, `fastifyHTTPEntrypoint` | `lakutata/adapter/express`, `lakutata/adapter/fastify` | HTTP transports (with static UIs) |
168
+ | `expressHTTPEntrypoint`, `fastifyHTTPEntrypoint` | `lakutata/adapter/express`, `lakutata/adapter/fastify` | HTTP transports (`staticUI` with Express) |
160
169
  | `commanderCLIEntrypoint` | `lakutata/adapter/commander` | CLI transport |
161
170
  | `socketIOServiceEntrypoint`, `jsonRPCServiceEntrypoint`, `electronIPCServiceEntrypoint` | `lakutata/adapter/*` | Service transports |
162
171
  | `EntrypointAdapter`, `ChannelRuntime` | `@lakutata/entrypoint/channel` | Writing a transport or a channel |
@@ -172,12 +181,16 @@ The typings document each declaration with examples.
172
181
  | `ControllerActionNotFoundException`, `RouteNotFoundException` | No action matches the request (HTTP 404) |
173
182
  | `EntrypointStoppedException` | The entrypoint is stopping (HTTP 503) |
174
183
  | `DuplicateActionNameException` | Two actions of a channel have the same name |
175
- | `InvalidPrefixException`, `InvalidMiddlewareException`, `InvalidChannelException`, `InvalidEntrypointAdapterException` | The declarations of the controllers or the options are invalid |
184
+ | `InvalidActionGroupException`, `InvalidPrefixException`, `InvalidMiddlewareException`, `InvalidChannelException`, `InvalidEntrypointAdapterException` | The declarations of the controllers or the options are invalid |
176
185
  | `ServiceInvokeException` | A remote service action failed (its error's properties copied) |
186
+ | `ServiceUnavailableException` | A service called by an `HTTPServiceProxy` gave no response (HTTP 503) |
177
187
 
178
188
  ## See also
179
189
 
180
- - The guide: `doc/en/ActionPattern.md` of the package `lakutata` (Chinese in `doc/zh`).
181
- - `@lakutata/core`: the DTOs, the dependency injection and the application.
182
- - `@lakutata/mutex`: `@Synchronized` actions and the `exclusive` schedules.
190
+ - The guides on the website: [Controllers and Actions](https://lakutata.com/guide/Controllers) and
191
+ [Entrypoint](https://lakutata.com/guide/Entrypoint).
192
+ - The guides in the `lakutata` package (`doc/en`, Chinese in `doc/zh`): `Controllers.md`, `Entrypoint.md`,
193
+ `Middlewares.md`, `HTTP.md` (the HTTP adapters), `ServiceCalls.md` (the service transports), `CLI.md`, `Schedules.md`.
194
+ - `@lakutata/core`: DTOs, dependency injection and the application.
195
+ - `@lakutata/mutex`: `@Synchronized` actions and `exclusive` schedules.
183
196
  - `@lakutata/grpc`, `@lakutata/nats`: the service transports over gRPC and NATS.
@@ -0,0 +1,182 @@
1
+ # @lakutata/entrypoint
2
+
3
+ [English](README.md) | 中文
4
+
5
+ lakutata 框架的入口:控制器及其动作,通过各种传输方式对外提供。一个控制器方法可以同时是 HTTP 路由、命令行命令、服务动作(通过 HTTP、Socket.IO、JSON-RPC、gRPC、NATS 或 Electron IPC 调用)和定时任务。无论通过哪种传输方式,请求都经过相同的中间件,输入由相同的 DTO 校验,适用的访问控制也相同。
6
+
7
+ `lakutata` 包把它重新导出为 `lakutata/com/entrypoint`(组件、控制器、上下文和结果)、`lakutata/decorator/ctrl`(动作装饰器)和 `lakutata/adapter/*`(各种传输方式),并在每个应用中预置 `entrypoint` 组件。
8
+
9
+ ## 安装
10
+
11
+ ```shell
12
+ npm install lakutata
13
+ ```
14
+
15
+ 各传输方式所需的包都是可选的 peer 依赖,用到时才需要安装:
16
+
17
+ | 传输方式 | 子路径 | 需要安装 |
18
+ |---|---|---|
19
+ | 基于 Express 4/5 的 HTTP | `lakutata/adapter/express` | `express` |
20
+ | 基于 Fastify 5 的 HTTP | `lakutata/adapter/fastify` | `fastify` |
21
+ | 基于 Commander 的命令行 | `lakutata/adapter/commander` | `commander`(提示输入还需要 `@inquirer/prompts`) |
22
+ | 基于 Socket.IO 的服务调用 | `lakutata/adapter/socket.io` | `socket.io` |
23
+ | 基于 JSON-RPC 2.0 的服务调用(HTTP、WebSocket、stdio) | `lakutata/adapter/jsonrpc` | 使用 WebSocket 时需要 `ws` |
24
+ | 基于 Electron IPC 的服务调用 | `lakutata/adapter/electron` | (Electron) |
25
+ | 基于 HTTP 的服务调用(`POST /`,事件通过 SSE 推送) | `lakutata/com/entrypoint` | 无 |
26
+ | 定时任务(`@Schedule`) | `lakutata/com/entrypoint` | 无 |
27
+ | 基于 gRPC / NATS 的服务调用 | `@lakutata/grpc/service` / `@lakutata/nats` | 它们各自的包 |
28
+
29
+ ## 用法
30
+
31
+ ### 控制器与传输方式
32
+
33
+ ```typescript
34
+ import {Application, DTO} from 'lakutata'
35
+ import {buildEntrypoints, Controller, scheduleEntrypoint} from 'lakutata/com/entrypoint'
36
+ import {CLIAction, GET, POST, Schedule, ServiceAction} from 'lakutata/decorator/ctrl'
37
+ import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
38
+ import {commanderCLIEntrypoint} from 'lakutata/adapter/commander'
39
+ import {socketIOServiceEntrypoint} from 'lakutata/adapter/socket.io'
40
+ import {Expect} from 'lakutata/decorator/dto'
41
+
42
+ class GetOrderDTO extends DTO {
43
+ //路由参数和查询参数都是字符串:strict(false) 会转换它们
44
+ @Expect(DTO.Number().integer().strict(false).required())
45
+ public id: number
46
+ }
47
+
48
+ class CreateOrderDTO extends DTO {
49
+ @Expect(DTO.String().required())
50
+ public product: string
51
+
52
+ @Expect(DTO.Number().integer().min(1).default(1))
53
+ public quantity: number
54
+ }
55
+
56
+ class OrderController extends Controller {
57
+ //GET /orders/42、命令 "order --id 42"、服务请求 {cmd: 'order.get', id: 42}
58
+ @GET('/orders/:id', GetOrderDTO)
59
+ @CLIAction('order', GetOrderDTO)
60
+ @ServiceAction({cmd: 'order.get'}, GetOrderDTO)
61
+ public async get(input: GetOrderDTO): Promise<{id: number}> {
62
+ return {id: input.id}
63
+ }
64
+
65
+ //带 JSON 请求体的 POST /orders;返回值就是响应(JSON)
66
+ @POST('/orders', CreateOrderDTO)
67
+ public async create(input: CreateOrderDTO): Promise<{product: string, quantity: number}> {
68
+ return input
69
+ }
70
+
71
+ //每 5 分钟执行一次(使用 {every: '5m', exclusive: true} 并配置锁组件时,同一时间只在一个实例中执行)
72
+ @Schedule('5m')
73
+ public async report(): Promise<void> {
74
+ }
75
+ }
76
+
77
+ Application.run(() => ({
78
+ id: 'orders.app',
79
+ name: 'Orders',
80
+ components: {
81
+ entrypoint: buildEntrypoints({
82
+ controllers: [OrderController],
83
+ http: expressHTTPEntrypoint({port: 8080}),
84
+ cli: commanderCLIEntrypoint(),
85
+ service: socketIOServiceEntrypoint({port: 8081}),
86
+ schedule: scheduleEntrypoint()
87
+ })
88
+ },
89
+ bootstrap: ['entrypoint']
90
+ }))
91
+ ```
92
+
93
+ ### 在动作中
94
+
95
+ - `this.context` 是请求的上下文:`HTTPContext`(带有 `request`、`response`、`route`、`method`)、`CLIContext`(带有 `command`)、`ServiceContext`(带有 `clientId`)或 `ScheduleContext`。
96
+ - 请求被中止时,`this.signal` 也会被中止:客户端离开、截止时间已过,或者入口停止。
97
+ - 每个请求都会创建一个控制器(`@Scoped`),它注入的请求作用域对象在这个请求内共享。
98
+ - 返回值就是响应。`Result.status(201, data, headers)`、`Result.file(path)` 和 `Result.events()`(Server-Sent Events)用来控制 HTTP 响应。
99
+ - 通过 HTTP(Express、Fastify)调用时,抛出的异常会以它的 `statusCode` 和 JSON `{errno, errMsg}` 返回,输入不合法时返回 400。
100
+
101
+ ### 中间件与访问控制
102
+
103
+ ```typescript
104
+ import {Application} from 'lakutata'
105
+ import {buildEntrypoints, Controller, type ActionInvocation, type Next} from 'lakutata/com/entrypoint'
106
+ import {GET, UseMiddlewares} from 'lakutata/decorator/ctrl'
107
+ import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
108
+
109
+ //在动作前后运行,早于输入校验和访问控制
110
+ async function timing(invocation: ActionInvocation, next: Next): Promise<unknown> {
111
+ const started: number = Date.now()
112
+ try {
113
+ return await next()
114
+ } finally {
115
+ console.log(`${invocation.context.type} took ${Date.now() - started} ms`)
116
+ }
117
+ }
118
+
119
+ //作用于这个控制器的动作(写在 buildEntrypoints({middlewares: [timing]}) 中则作用于所有动作)
120
+ @UseMiddlewares(timing)
121
+ class HealthController extends Controller {
122
+ @GET('/health')
123
+ public async health(): Promise<string> {
124
+ return 'ok'
125
+ }
126
+ }
127
+
128
+ Application.run(() => ({
129
+ id: 'health.app',
130
+ name: 'Health',
131
+ components: {
132
+ entrypoint: buildEntrypoints({controllers: [HealthController], http: expressHTTPEntrypoint({port: 3000})})
133
+ },
134
+ bootstrap: ['entrypoint']
135
+ }))
136
+ ```
137
+
138
+ 访问控制:以 `{acl: true}` 声明的动作,由传给 `buildEntrypoints({rules})` 的规则检查(`AccessControlRule` 的子类,每个请求创建一次)。某条规则的结果为 `false` 时,请求被拒绝,抛出 `AccessDeniedException`(HTTP 403)。
139
+
140
+ ## API
141
+
142
+ | 导出 | 子路径 | 说明 |
143
+ |---|---|---|
144
+ | `Entrypoint`、`buildEntrypoints` | `lakutata/com/entrypoint` | 组件及其选项:控制器、传输方式、中间件、规则、分组 |
145
+ | `Controller` | `lakutata/com/entrypoint` | 控制器的基类(`context`、`signal`) |
146
+ | `HTTPContext`、`CLIContext`、`ServiceContext`、`ScheduleContext`、`BaseContext`、`ContextType` | `lakutata/com/entrypoint` | 请求的上下文 |
147
+ | `Result`、`StatusResult`、`FileResult`、`EventsResult` | `lakutata/com/entrypoint` | 控制 HTTP 响应的结果 |
148
+ | `AccessControlRule`、`AccessControl` | `lakutata/com/entrypoint` | 访问控制 |
149
+ | `Middleware`、`ActionInvocation`、`Next` | `lakutata/com/entrypoint` | 中间件 |
150
+ | `httpServiceEntrypoint`、`scheduleEntrypoint` | `lakutata/com/entrypoint` | 基于 HTTP 的服务调用;定时任务 |
151
+ | `HTTPServiceProxy`、`buildHTTPServiceProxy` | `lakutata/com/entrypoint` | 基于 HTTP 的服务的客户端:调用它的动作,接收它推送的事件 |
152
+ | `Cron`、`parseDuration` | `lakutata/com/entrypoint` | cron 表达式和时长 |
153
+ | `HTTPAction`、`GET`、`POST`、`PUT`、`PATCH`、`DELETE`、`HEAD`、`OPTIONS` | `lakutata/decorator/ctrl` | HTTP 路由 |
154
+ | `CLIAction`、`ServiceAction`、`Schedule` | `lakutata/decorator/ctrl` | 命令、服务动作、定时任务 |
155
+ | `Prefix`、`UseMiddlewares` | `lakutata/decorator/ctrl` | 为控制器的路由、命令和模式加上前缀;控制器的中间件 |
156
+ | `expressHTTPEntrypoint`、`fastifyHTTPEntrypoint` | `lakutata/adapter/express`、`lakutata/adapter/fastify` | HTTP 传输方式(Express 支持 `staticUI`) |
157
+ | `commanderCLIEntrypoint` | `lakutata/adapter/commander` | 命令行传输方式 |
158
+ | `socketIOServiceEntrypoint`、`jsonRPCServiceEntrypoint`、`electronIPCServiceEntrypoint` | `lakutata/adapter/*` | 服务调用的传输方式 |
159
+ | `EntrypointAdapter`、`ChannelRuntime` | `@lakutata/entrypoint/channel` | 编写适配器或新的传输方式 |
160
+
161
+ 类型声明中的每一项都附有说明和示例。
162
+
163
+ ## 错误
164
+
165
+ | 异常 | 什么时候抛出 |
166
+ |---|---|
167
+ | `InvalidValueException`(来自 `lakutata`) | 输入不符合动作的 DTO(HTTP 400) |
168
+ | `AccessDeniedException` | 规则拒绝了请求(HTTP 403) |
169
+ | `ControllerActionNotFoundException`、`RouteNotFoundException` | 没有与请求匹配的动作(HTTP 404) |
170
+ | `EntrypointStoppedException` | 入口正在停止(HTTP 503) |
171
+ | `DuplicateActionNameException` | 同一传输方式中有两个同名的动作 |
172
+ | `InvalidActionGroupException`、`InvalidPrefixException`、`InvalidMiddlewareException`、`InvalidChannelException`、`InvalidEntrypointAdapterException` | 控制器的声明或选项不合法 |
173
+ | `ServiceInvokeException` | 远程服务动作失败(带有原错误的各个属性) |
174
+ | `ServiceUnavailableException` | `HTTPServiceProxy` 调用的服务没有给出响应(HTTP 503) |
175
+
176
+ ## 另见
177
+
178
+ - 网站上的指南:[控制器与动作](https://lakutata.com/zh/guide/Controllers)、[Entrypoint](https://lakutata.com/zh/guide/Entrypoint)。
179
+ - `lakutata` 包中的指南(`doc/zh`):`Controllers.md`、`Entrypoint.md`、`Middlewares.md`、`HTTP.md`(HTTP 适配器)、`ServiceCalls.md`(服务调用的传输方式)、`CLI.md`、`Schedules.md`。
180
+ - `@lakutata/core`:DTO、依赖注入和应用。
181
+ - `@lakutata/mutex`:`@Synchronized` 动作和 `exclusive` 定时任务。
182
+ - `@lakutata/grpc`、`@lakutata/nats`:基于 gRPC 和 NATS 的服务调用传输方式。
@@ -60,7 +60,7 @@ export type ServiceEntrypoint = (module: Module, handler: ServiceEntrypointHandl
60
60
  export type CLIMap = Map<string, JSONSchema>;
61
61
  /**
62
62
  * The HTTP actions given to an {@link HTTPEntrypoint}: each route (with its controller prefix) mapped to the set of
63
- * its HTTP methods (`GET`, `POST`…).
63
+ * its HTTP methods, as declared (`get`, `post`… from `@GET`, `@POST`…; `@HTTPAction` keeps the case it is given).
64
64
  */
65
65
  export type HTTPRouteMap<HTTPMethods = string> = Map<string, Set<HTTPMethods>>;
66
66
  /**
@@ -303,7 +303,8 @@ export interface EntrypointActions<T extends BaseActionInfo> {
303
303
  * if (cliMap.has(command)) console.log(await handler(new CLIContext({command: command, data: {}})))
304
304
  * })
305
305
  * })
306
- * }
306
+ * },
307
+ * bootstrap: ['entrypoint']
307
308
  * }))
308
309
  * ```
309
310
  */
@@ -335,8 +336,9 @@ export declare const buildCLIEntrypoint: (entrypoint: CLIEntrypoint) => CLIEntry
335
336
  * controllers: [HealthController],
336
337
  * http: buildHTTPEntrypoint((module, routeMap, handler, registerDestroy): void => {
337
338
  * const server: Server = createServer(async (request, response): Promise<void> => {
339
+ * //The route map holds the routes and the methods as declared: '/health', 'get'
338
340
  * const route: string = (request.url ?? '/').split('?')[0]
339
- * const method: string = request.method ?? 'GET'
341
+ * const method: string = (request.method ?? 'GET').toLowerCase()
340
342
  * if (!routeMap.get(route)?.has(method)) {
341
343
  * response.writeHead(404).end()
342
344
  * return
@@ -353,7 +355,8 @@ export declare const buildCLIEntrypoint: (entrypoint: CLIEntrypoint) => CLIEntry
353
355
  * registerDestroy((): Promise<void> => new Promise((resolve): void => void server.close((): void => resolve())))
354
356
  * })
355
357
  * })
356
- * }
358
+ * },
359
+ * bootstrap: ['entrypoint']
357
360
  * }))
358
361
  * ```
359
362
  */
@@ -394,7 +397,8 @@ export declare const buildHTTPEntrypoint: (entrypoint: HTTPEntrypoint) => HTTPEn
394
397
  * registerDestroy((): void => void bus.off('request', listener))
395
398
  * })
396
399
  * })
397
- * }
400
+ * },
401
+ * bootstrap: ['entrypoint']
398
402
  * }))
399
403
  * ```
400
404
  */
@@ -444,7 +448,8 @@ export declare const buildServiceEntrypoint: (entrypoint: ServiceEntrypoint) =>
444
448
  * http: expressHTTPEntrypoint({port: 3000}),
445
449
  * schedule: scheduleEntrypoint()
446
450
  * })
447
- * }
451
+ * },
452
+ * bootstrap: ['entrypoint']
448
453
  * }))
449
454
  * ```
450
455
  */
@@ -531,7 +536,8 @@ type StartedAdapter = {
531
536
  * http: expressHTTPEntrypoint({port: 3000}),
532
537
  * service: socketIOServiceEntrypoint({port: 3001})
533
538
  * })
534
- * }
539
+ * },
540
+ * bootstrap: ['entrypoint']
535
541
  * }))
536
542
  * .onLaunched(async (app: Application): Promise<void> => {
537
543
  * const entrypoint: Entrypoint = await app.getObject<Entrypoint>('entrypoint')
@@ -67,7 +67,8 @@ Object.defineProperty(exports, "ScheduleContext", { enumerable: true, get: funct
67
67
  * if (cliMap.has(command)) console.log(await handler(new CLIContext({command: command, data: {}})))
68
68
  * })
69
69
  * })
70
- * }
70
+ * },
71
+ * bootstrap: ['entrypoint']
71
72
  * }))
72
73
  * ```
73
74
  */
@@ -100,8 +101,9 @@ exports.buildCLIEntrypoint = buildCLIEntrypoint;
100
101
  * controllers: [HealthController],
101
102
  * http: buildHTTPEntrypoint((module, routeMap, handler, registerDestroy): void => {
102
103
  * const server: Server = createServer(async (request, response): Promise<void> => {
104
+ * //The route map holds the routes and the methods as declared: '/health', 'get'
103
105
  * const route: string = (request.url ?? '/').split('?')[0]
104
- * const method: string = request.method ?? 'GET'
106
+ * const method: string = (request.method ?? 'GET').toLowerCase()
105
107
  * if (!routeMap.get(route)?.has(method)) {
106
108
  * response.writeHead(404).end()
107
109
  * return
@@ -118,7 +120,8 @@ exports.buildCLIEntrypoint = buildCLIEntrypoint;
118
120
  * registerDestroy((): Promise<void> => new Promise((resolve): void => void server.close((): void => resolve())))
119
121
  * })
120
122
  * })
121
- * }
123
+ * },
124
+ * bootstrap: ['entrypoint']
122
125
  * }))
123
126
  * ```
124
127
  */
@@ -160,7 +163,8 @@ exports.buildHTTPEntrypoint = buildHTTPEntrypoint;
160
163
  * registerDestroy((): void => void bus.off('request', listener))
161
164
  * })
162
165
  * })
163
- * }
166
+ * },
167
+ * bootstrap: ['entrypoint']
164
168
  * }))
165
169
  * ```
166
170
  */
@@ -211,7 +215,8 @@ exports.buildServiceEntrypoint = buildServiceEntrypoint;
211
215
  * http: expressHTTPEntrypoint({port: 3000}),
212
216
  * schedule: scheduleEntrypoint()
213
217
  * })
214
- * }
218
+ * },
219
+ * bootstrap: ['entrypoint']
215
220
  * }))
216
221
  * ```
217
222
  */
@@ -282,7 +287,8 @@ const LEGACY_CHANNELS = new Set(['cli', 'http', 'service']);
282
287
  * http: expressHTTPEntrypoint({port: 3000}),
283
288
  * service: socketIOServiceEntrypoint({port: 3001})
284
289
  * })
285
- * }
290
+ * },
291
+ * bootstrap: ['entrypoint']
286
292
  * }))
287
293
  * .onLaunched(async (app: Application): Promise<void> => {
288
294
  * const entrypoint: Entrypoint = await app.getObject<Entrypoint>('entrypoint')
@@ -173,7 +173,7 @@ export type CommanderCLIEntrypointOptions = {
173
173
  * @CLIAction('greet <name>', GreetDTO)
174
174
  * public async greet(input: GreetDTO): Promise<string> {
175
175
  * const text: string = `Hello, ${input.name}`
176
- * return (input.loud ? text.toUpperCase() : text).repeat(input.times ?? 1)
176
+ * return Array(input.times ?? 1).fill(input.loud ? text.toUpperCase() : text).join('\n')
177
177
  * }
178
178
  * }
179
179
  *
@@ -185,7 +185,8 @@ export type CommanderCLIEntrypointOptions = {
185
185
  * controllers: [GreetController],
186
186
  * cli: commanderCLIEntrypoint({bin: 'tool', description: 'A greeting tool', version: '1.0.0'})
187
187
  * })
188
- * }
188
+ * },
189
+ * bootstrap: ['entrypoint']
189
190
  * })
190
191
  * ```
191
192
  */
@@ -196,7 +196,7 @@ function errorMessage(error) {
196
196
  * @CLIAction('greet <name>', GreetDTO)
197
197
  * public async greet(input: GreetDTO): Promise<string> {
198
198
  * const text: string = `Hello, ${input.name}`
199
- * return (input.loud ? text.toUpperCase() : text).repeat(input.times ?? 1)
199
+ * return Array(input.times ?? 1).fill(input.loud ? text.toUpperCase() : text).join('\n')
200
200
  * }
201
201
  * }
202
202
  *
@@ -208,7 +208,8 @@ function errorMessage(error) {
208
208
  * controllers: [GreetController],
209
209
  * cli: commanderCLIEntrypoint({bin: 'tool', description: 'A greeting tool', version: '1.0.0'})
210
210
  * })
211
- * }
211
+ * },
212
+ * bootstrap: ['entrypoint']
212
213
  * })
213
214
  * ```
214
215
  */
@@ -187,7 +187,8 @@ export declare function ipcEventOfContext<Event extends IPCInvokeEvent = IPCInvo
187
187
  * controllers: [EchoController],
188
188
  * service: electronIPCServiceEntrypoint()
189
189
  * })
190
- * }
190
+ * },
191
+ * bootstrap: ['entrypoint']
191
192
  * })
192
193
  *
193
194
  * //In the preload script of a window
@@ -127,7 +127,8 @@ async function loadIPCMain() {
127
127
  * controllers: [EchoController],
128
128
  * service: electronIPCServiceEntrypoint()
129
129
  * })
130
- * }
130
+ * },
131
+ * bootstrap: ['entrypoint']
131
132
  * })
132
133
  *
133
134
  * //In the preload script of a window
@@ -20,7 +20,7 @@ export type ExpressHookContext = {
20
20
  */
21
21
  readonly server: Server;
22
22
  /**
23
- * Each action route mapped to its HTTP methods, in lower case as declared (`'/users/:id'` → `{'get', 'delete'}`).
23
+ * Each action route mapped to its HTTP methods, as declared (`'/users/:id'` → `{'get', 'delete'}`).
24
24
  */
25
25
  readonly routes: Map<string, Set<string>>;
26
26
  /**
@@ -166,7 +166,8 @@ export type ExpressHTTPEntrypointOptions = {
166
166
  * fallback: staticUI('./ui/dist')
167
167
  * })
168
168
  * })
169
- * }
169
+ * },
170
+ * bootstrap: ['entrypoint']
170
171
  * })
171
172
  * ```
172
173
  */
@@ -12,6 +12,7 @@ const BuiltinChannels_js_1 = require("../../channel/BuiltinChannels.js");
12
12
  const Result_js_1 = require("../../lib/Result.js");
13
13
  const RouteNotFoundException_js_1 = require("../../exceptions/RouteNotFoundException.js");
14
14
  const Connections_js_1 = require("../../lib/Connections.js");
15
+ const ExplicitStatus_js_1 = require("../../lib/ExplicitStatus.js");
15
16
  const Responders_js_1 = require("./Responders.js");
16
17
  /**
17
18
  * The first segments of the routes
@@ -100,7 +101,8 @@ function eventFrame(data, event, id) {
100
101
  * fallback: staticUI('./ui/dist')
101
102
  * })
102
103
  * })
103
- * }
104
+ * },
105
+ * bootstrap: ['entrypoint']
104
106
  * })
105
107
  * ```
106
108
  */
@@ -199,6 +201,7 @@ function expressHTTPEntrypoint(options = {}) {
199
201
  return await sendFile(result, request, response);
200
202
  if (result instanceof Result_js_1.StatusResult) {
201
203
  response.status(result.status).set(result.headers);
204
+ (0, ExplicitStatus_js_1.markExplicitStatus)(response);
202
205
  return respond.success(response, result.data, request);
203
206
  }
204
207
  respond.success(response, result, request);
@@ -257,7 +260,10 @@ function expressHTTPEntrypoint(options = {}) {
257
260
  await options.before?.(app, { module: runtime.module, server: server, routes: routes, actionPrefixes: actionPrefixes, isActionPath: isActionPath });
258
261
  if (options.body !== false)
259
262
  app.use(express_1.default.json({ limit: options.body?.limit ?? '1mb' }), express_1.default.urlencoded({ extended: true, limit: options.body?.limit ?? '1mb' }));
260
- for (const action of runtime.actions) {
263
+ //The HEAD actions first: express answers a HEAD request with the first GET route of its path when no HEAD route
264
+ //comes before it
265
+ const isHead = (action) => String(action.address.method).toLowerCase() === 'head';
266
+ for (const action of [...runtime.actions.filter(isHead), ...runtime.actions.filter((action) => !isHead(action))]) {
261
267
  const method = String(action.address.method).toLowerCase();
262
268
  const register = app[method];
263
269
  if (typeof register !== 'function')
@@ -100,7 +100,8 @@ export type JSONEnvelopeOptions = {
100
100
  * })
101
101
  * })
102
102
  * })
103
- * }
103
+ * },
104
+ * bootstrap: ['entrypoint']
104
105
  * })
105
106
  * ```
106
107
  */
@@ -6,6 +6,7 @@ const HTTPErrors_js_1 = require("../../lib/HTTPErrors.js");
6
6
  Object.defineProperty(exports, "describeError", { enumerable: true, get: function () { return HTTPErrors_js_1.describeError; } });
7
7
  Object.defineProperty(exports, "statusOfError", { enumerable: true, get: function () { return HTTPErrors_js_1.statusOfError; } });
8
8
  const Envelope_js_1 = require("../../lib/Envelope.js");
9
+ const ExplicitStatus_js_1 = require("../../lib/ExplicitStatus.js");
9
10
  /**
10
11
  * The default responder of {@link expressHTTPEntrypoint}: a result is sent as JSON, no result as an empty 204 (or the
11
12
  * status set by a `StatusResult`); an error is sent as `{errno, errMsg}` (see `describeError`) with its HTTP status
@@ -14,7 +15,7 @@ const Envelope_js_1 = require("../../lib/Envelope.js");
14
15
  exports.JSONResponder = {
15
16
  success: (response, data) => {
16
17
  if (data === undefined) {
17
- response.status(response.statusCode === 200 ? 204 : response.statusCode).end();
18
+ response.status(response.statusCode === 200 && !(0, ExplicitStatus_js_1.hasExplicitStatus)(response) ? 204 : response.statusCode).end();
18
19
  return;
19
20
  }
20
21
  response.json(data);
@@ -63,7 +64,8 @@ exports.JSONResponder = {
63
64
  * })
64
65
  * })
65
66
  * })
66
- * }
67
+ * },
68
+ * bootstrap: ['entrypoint']
67
69
  * })
68
70
  * ```
69
71
  */
@@ -76,7 +76,8 @@ export declare function assetsMaxAge(value: string | number | undefined): number
76
76
  * fallback: staticUI('./ui/dist', {assetsMaxAge: 86400})
77
77
  * })
78
78
  * })
79
- * }
79
+ * },
80
+ * bootstrap: ['entrypoint']
80
81
  * })
81
82
  * ```
82
83
  */
@@ -69,7 +69,8 @@ function assetsMaxAge(value) {
69
69
  * fallback: staticUI('./ui/dist', {assetsMaxAge: 86400})
70
70
  * })
71
71
  * })
72
- * }
72
+ * },
73
+ * bootstrap: ['entrypoint']
73
74
  * })
74
75
  * ```
75
76
  */
@@ -150,7 +150,8 @@ export type FastifyHTTPEntrypointOptions = {
150
150
  * }
151
151
  * })
152
152
  * })
153
- * }
153
+ * },
154
+ * bootstrap: ['entrypoint']
154
155
  * })
155
156
  * ```
156
157
  */