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/en/ActionPattern.md
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
# Lakutata Controllers and Actions
|
|
2
|
-
|
|
3
|
-
The `Entrypoint` component (`lakutata/com/entrypoint`, package `@lakutata/entrypoint`) serves the methods of the
|
|
4
|
-
controllers, the **actions**, over several transports. An action is addressed by an HTTP route, a CLI command, a
|
|
5
|
-
service pattern or a schedule, and one method can carry several of these addresses at once: its input is validated by
|
|
6
|
-
its DTO, checked by the access control and passed through the middlewares the same way whatever the transport. The
|
|
7
|
-
README of `@lakutata/entrypoint` and the typings of `lakutata/com/entrypoint` and `lakutata/decorator/ctrl` give the
|
|
8
|
-
details.
|
|
9
|
-
|
|
10
|
-
## 1. Controllers
|
|
11
|
-
|
|
12
|
-
A controller extends `Controller` (`lakutata/com/entrypoint`), a provider whose lifetime is `@Scoped` (locked): each
|
|
13
|
-
request creates its own controller in the request's scope and destroys it when the request ends; the providers it
|
|
14
|
-
injects are scoped to the request too.
|
|
15
|
-
|
|
16
|
-
- `this.context` is the context of the request: an `HTTPContext` (its `request`, `response`, `route`, `method`), a
|
|
17
|
-
`CLIContext` (its `command`), a `ServiceContext` (its `clientId`) or a `ScheduleContext`; tell them apart by their
|
|
18
|
-
class or their `type`.
|
|
19
|
-
- `this.signal` is an `AbortSignal` aborted when the request is: its client gone, its deadline exceeded, or the
|
|
20
|
-
entrypoint stopping. Long-running actions should stop then: their result is dropped.
|
|
21
|
-
- The method receives its input (the data of the request, validated by the DTO of the action, its defaults applied and
|
|
22
|
-
its unknown keys removed) as a plain object, not a DTO instance. Without a DTO, the input is accepted as it is.
|
|
23
|
-
- The value returned is the response; an exception is answered with its `statusCode` (HTTP) and its `errno` and
|
|
24
|
-
message, an invalid input with the status 400.
|
|
25
|
-
|
|
26
|
-
```typescript
|
|
27
|
-
import {Application, DTO} from 'lakutata'
|
|
28
|
-
import {buildEntrypoints, Controller, scheduleEntrypoint} from 'lakutata/com/entrypoint'
|
|
29
|
-
import {CLIAction, GET, POST, Schedule, ServiceAction} from 'lakutata/decorator/ctrl'
|
|
30
|
-
import {Expect} from 'lakutata/decorator/dto'
|
|
31
|
-
import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
|
|
32
|
-
import {commanderCLIEntrypoint} from 'lakutata/adapter/commander'
|
|
33
|
-
import {socketIOServiceEntrypoint} from 'lakutata/adapter/socket.io'
|
|
34
|
-
|
|
35
|
-
class GetOrderDTO extends DTO {
|
|
36
|
-
//The route parameters and the query are strings: strict(false) converts them
|
|
37
|
-
@Expect(DTO.Number().integer().strict(false).required())
|
|
38
|
-
public id: number
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
class CreateOrderDTO extends DTO {
|
|
42
|
-
@Expect(DTO.String().required())
|
|
43
|
-
public product: string
|
|
44
|
-
|
|
45
|
-
@Expect(DTO.Number().integer().min(1).default(1))
|
|
46
|
-
public quantity: number
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
class OrderController extends Controller {
|
|
50
|
-
//GET /orders/42, the command "order --id 42", the service message {cmd: 'order.get', id: 42}
|
|
51
|
-
@GET('/orders/:id', GetOrderDTO)
|
|
52
|
-
@CLIAction('order', GetOrderDTO)
|
|
53
|
-
@ServiceAction({cmd: 'order.get'}, GetOrderDTO)
|
|
54
|
-
public async get(input: GetOrderDTO): Promise<{ id: number }> {
|
|
55
|
-
return {id: input.id}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
//POST /orders with a JSON body
|
|
59
|
-
@POST('/orders', CreateOrderDTO)
|
|
60
|
-
public async create(input: CreateOrderDTO): Promise<{ product: string, quantity: number }> {
|
|
61
|
-
return input
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
//Every 5 minutes
|
|
65
|
-
@Schedule('5m')
|
|
66
|
-
public async report(): Promise<void> {
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
Application.run(() => ({
|
|
71
|
-
id: 'orders.app',
|
|
72
|
-
name: 'Orders',
|
|
73
|
-
components: {
|
|
74
|
-
entrypoint: buildEntrypoints({
|
|
75
|
-
controllers: [OrderController],
|
|
76
|
-
http: expressHTTPEntrypoint({port: 8080}),
|
|
77
|
-
cli: commanderCLIEntrypoint(),
|
|
78
|
-
service: socketIOServiceEntrypoint({port: 8081}),
|
|
79
|
-
schedule: scheduleEntrypoint()
|
|
80
|
-
})
|
|
81
|
-
},
|
|
82
|
-
bootstrap: ['entrypoint']
|
|
83
|
-
}))
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## 2. The action decorators (`lakutata/decorator/ctrl`)
|
|
87
|
-
|
|
88
|
-
Each decorator takes its address, then optionally the DTO class of the input, then optionally the action options
|
|
89
|
-
(`acl`, `name`, `description`, `groups`, `rule`, `middlewares`).
|
|
90
|
-
|
|
91
|
-
### HTTP: `@HTTPAction`, `@GET`, `@POST`, `@PUT`, `@PATCH`, `@DELETE`, `@HEAD`, `@OPTIONS`
|
|
92
|
-
|
|
93
|
-
```text
|
|
94
|
-
@HTTPAction(route, method | methods, DTOClass?, options?)
|
|
95
|
-
@GET(route, DTOClass?, options?)
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
- The route uses the syntax of the HTTP adapter (`/users/:id`); the method is case-insensitive (`'get'`, `['get', 'post']`).
|
|
99
|
-
- The input is the query merged with the parsed body (JSON or URL-encoded), then the route parameters over them. The
|
|
100
|
-
query values and the route parameters are strings and the schemas are strict: convert them with `.strict(false)`.
|
|
101
|
-
- The value returned is answered as JSON (204 for `undefined`). `Result.status(201, data, headers)`,
|
|
102
|
-
`Result.file(path)` and `Result.events(producer)` (Server-Sent Events), from `lakutata/com/entrypoint`, control the
|
|
103
|
-
response.
|
|
104
|
-
|
|
105
|
-
### CLI: `@CLIAction`
|
|
106
|
-
|
|
107
|
-
```text
|
|
108
|
-
@CLIAction(command, DTOClass?, options?)
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
- The command is its name, optionally followed by positional arguments (`'copy <source> [target]'`, `'<files...>'`);
|
|
112
|
-
the words before the name are groups (`'package exe <file>'`). The other properties of the DTO are options
|
|
113
|
-
(`--times 2`), the booleans flags (`--shout`); `.description()` of a schema is the help of its option, and the
|
|
114
|
-
`description` action option the help of the command.
|
|
115
|
-
- The value returned is written to stdout (a string as it is, other values as JSON); an error is written to stderr and
|
|
116
|
-
the command ends with the code 1.
|
|
117
|
-
|
|
118
|
-
### Service: `@ServiceAction`
|
|
119
|
-
|
|
120
|
-
```text
|
|
121
|
-
@ServiceAction(pattern, DTOClass?, options?)
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
- The pattern is an object (nested 2 levels at most) whose keys a message must hold with the same values; a string
|
|
125
|
-
value may be a glob (`'user.*'`). The most specific matching pattern wins. The input is the message without the keys
|
|
126
|
-
of the pattern.
|
|
127
|
-
- The caller receives `{success: true, error: null, payload}`, or `{success: false, error, payload: null}` where
|
|
128
|
-
`error` holds the properties of the error (`errno`, `errMsg`, `statusCode`, `message`…); JSON-RPC answers with its
|
|
129
|
-
own `result` and `error`. A message no pattern matches fails with `ControllerActionNotFoundException`.
|
|
130
|
-
|
|
131
|
-
### Schedules: `@Schedule`
|
|
132
|
-
|
|
133
|
-
```text
|
|
134
|
-
@Schedule(schedule, options?)
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
The method runs on a schedule when the entrypoint has a schedule adapter (`schedule: scheduleEntrypoint()`). The
|
|
138
|
-
schedule is a duration (`'30s'`, `'1h30m'`, an interval), a cron expression (`'0 */5 * * * *'`, 5 or 6 fields, or a
|
|
139
|
-
macro such as `'@daily'`), or the options `{every}` or `{cron, timezone}` with:
|
|
140
|
-
|
|
141
|
-
- `overlap` (default `false`): start a run while the previous one is in progress; otherwise the run is skipped;
|
|
142
|
-
- `immediate` (default `false`): also run once as soon as the application launched;
|
|
143
|
-
- `exclusive` (default `false`): run in one instance of the application at a time, with the locks of a mutex component
|
|
144
|
-
(`true` uses the component named `mutex`, `{mutex, key}` names it and the lock); see [Mutex](Mutex.md).
|
|
145
|
-
|
|
146
|
-
Each run has its own scope and goes through the middlewares and the access control; the method receives an empty
|
|
147
|
-
input, `this.context` is a `ScheduleContext` (its `schedule`, its planned time, its `run` number). An error thrown is
|
|
148
|
-
given to the `onError` option of `scheduleEntrypoint`, or logged, and the next runs go on.
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
import {Controller, ScheduleContext} from 'lakutata/com/entrypoint'
|
|
152
|
-
import {Schedule} from 'lakutata/decorator/ctrl'
|
|
153
|
-
|
|
154
|
-
class JobController extends Controller {
|
|
155
|
-
//Every 30 seconds, and once as soon as the application launched
|
|
156
|
-
@Schedule({every: '30s', immediate: true})
|
|
157
|
-
public async heartbeat(): Promise<void> {
|
|
158
|
-
if (this.context instanceof ScheduleContext) console.log(`run ${this.context.run} of ${this.context.schedule}`)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
//At 03:00 every day in Shanghai, in one instance at a time (a mutex component named "mutex")
|
|
162
|
-
@Schedule({cron: '0 3 * * *', timezone: 'Asia/Shanghai', exclusive: true}, {name: 'Purge the expired sessions'})
|
|
163
|
-
public async purge(): Promise<void> {
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### Prefixes: `@Prefix`
|
|
169
|
-
|
|
170
|
-
`@Prefix({route, command, pattern})` on a controller class prefixes its actions: `route` is prepended to the HTTP
|
|
171
|
-
routes, `command` to the CLI commands (joined by `:`, `user` and `get` give `user:get`), `pattern` is merged into the
|
|
172
|
-
service patterns.
|
|
173
|
-
|
|
174
|
-
```typescript
|
|
175
|
-
import {DTO} from 'lakutata'
|
|
176
|
-
import {Controller} from 'lakutata/com/entrypoint'
|
|
177
|
-
import {CLIAction, GET, Prefix, ServiceAction} from 'lakutata/decorator/ctrl'
|
|
178
|
-
import {Expect} from 'lakutata/decorator/dto'
|
|
179
|
-
|
|
180
|
-
class UserIdDTO extends DTO {
|
|
181
|
-
@Expect(DTO.String().required())
|
|
182
|
-
public id: string
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
@Prefix({route: '/users', command: 'user', pattern: {resource: 'user'}})
|
|
186
|
-
class UserController extends Controller {
|
|
187
|
-
//GET /users/:id, the command "user:get <id>" and the message {resource: 'user', act: 'get', id}
|
|
188
|
-
@GET('/:id', UserIdDTO)
|
|
189
|
-
@CLIAction('get <id>', UserIdDTO)
|
|
190
|
-
@ServiceAction({act: 'get'}, UserIdDTO)
|
|
191
|
-
public async get(input: UserIdDTO): Promise<{ id: string }> {
|
|
192
|
-
return {id: input.id}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## 3. Middlewares and access control
|
|
198
|
-
|
|
199
|
-
A middleware runs around an action, before its input validation and its access control: a function
|
|
200
|
-
`(invocation, next) => Promise<unknown>` or a `Middleware` class. They are given to `buildEntrypoints({middlewares})`
|
|
201
|
-
(every action), to an adapter's `middlewares` option, to `@UseMiddlewares(...)` on a controller class or a method, and to
|
|
202
|
-
the `middlewares` action option.
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
import {type ActionInvocation, Controller, type Next} from 'lakutata/com/entrypoint'
|
|
206
|
-
import {GET, UseMiddlewares} from 'lakutata/decorator/ctrl'
|
|
207
|
-
|
|
208
|
-
async function timing(invocation: ActionInvocation, next: Next): Promise<unknown> {
|
|
209
|
-
const started: number = Date.now()
|
|
210
|
-
try {
|
|
211
|
-
return await next()
|
|
212
|
-
} finally {
|
|
213
|
-
console.log(`${invocation.context.type} took ${Date.now() - started} ms`)
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
@UseMiddlewares(timing)
|
|
218
|
-
class HealthController extends Controller {
|
|
219
|
-
@GET('/health')
|
|
220
|
-
public async health(): Promise<string> {
|
|
221
|
-
return 'ok'
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
An action declared with `{acl: true}` is checked by the rules given to `buildEntrypoints({rules})`: `AccessControlRule`
|
|
227
|
-
subclasses, created for each request, whose `rule()` resolving `false` denies it with an `AccessDeniedException` (HTTP
|
|
228
|
-
403). The `rule` action option is the action's own rule.
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
231
|
-
import {AccessControlRule, ContextType, Controller, type HTTPContext} from 'lakutata/com/entrypoint'
|
|
232
|
-
import {DELETE} from 'lakutata/decorator/ctrl'
|
|
233
|
-
|
|
234
|
-
class AdminRule extends AccessControlRule<HTTPContext> {
|
|
235
|
-
public get supportContextTypes(): ContextType[] {
|
|
236
|
-
return [ContextType.HTTP]
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
public async rule(): Promise<boolean> {
|
|
240
|
-
if (!this.targetAction.groups.includes('admin')) return true
|
|
241
|
-
return this.context.request.headers['x-role'] === 'admin'
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
class UserController extends Controller {
|
|
246
|
-
@DELETE('/users/:id', {acl: true, groups: ['admin']})
|
|
247
|
-
public async remove(input: { id: string }): Promise<void> {
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
//buildEntrypoints({controllers: [UserController], rules: [AdminRule], httpActionGroups: {admin: 'Administration'}})
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
The groups of an action must be declared in `httpActionGroups`, `cliActionGroups` or `serviceActionGroups` of
|
|
255
|
-
`buildEntrypoints`. `shutdownTimeout` (default 30000 ms) is the time given to the running requests when the entrypoint
|
|
256
|
-
stops.
|
|
257
|
-
|
|
258
|
-
## 4. The transports
|
|
259
|
-
|
|
260
|
-
| Option of `buildEntrypoints` | Adapter | Subpath | Install |
|
|
261
|
-
|---|---|---|---|
|
|
262
|
-
| `http` | `expressHTTPEntrypoint({port})` | `lakutata/adapter/express` | `express` |
|
|
263
|
-
| `http` | `fastifyHTTPEntrypoint({port})` | `lakutata/adapter/fastify` | `fastify` |
|
|
264
|
-
| `cli` | `commanderCLIEntrypoint()` | `lakutata/adapter/commander` | `commander` (`@inquirer/prompts` for the prompts) |
|
|
265
|
-
| `service` | `socketIOServiceEntrypoint({port})` | `lakutata/adapter/socket.io` | `socket.io` |
|
|
266
|
-
| `service` | `jsonRPCServiceEntrypoint()` (HTTP, WebSocket, stdio) | `lakutata/adapter/jsonrpc` | `ws` for WebSocket |
|
|
267
|
-
| `service` | `electronIPCServiceEntrypoint()` | `lakutata/adapter/electron` | (Electron) |
|
|
268
|
-
| `service` | `httpServiceEntrypoint({port})` (`POST /`, events by SSE) | `lakutata/com/entrypoint` | |
|
|
269
|
-
| `service` | `grpcServiceEntrypoint({port})` | `@lakutata/grpc/service` | `@lakutata/grpc` |
|
|
270
|
-
| `service` | `natsServiceEntrypoint('nats')` | `@lakutata/nats` | `@lakutata/nats` |
|
|
271
|
-
| `schedule` | `scheduleEntrypoint()` | `lakutata/com/entrypoint` | |
|
|
272
|
-
|
|
273
|
-
Each option takes one adapter or an array of adapters. `buildHTTPEntrypoint`, `buildCLIEntrypoint` and
|
|
274
|
-
`buildServiceEntrypoint` (from `lakutata/com/entrypoint`) type an entrypoint function in the style of lakutata 2.x, to
|
|
275
|
-
plug another library; `EntrypointAdapter` (from `@lakutata/entrypoint/channel`) is the interface of the adapters.
|
|
276
|
-
|
|
277
|
-
### From an entrypoint function of 2.x to the HTTP adapters
|
|
278
|
-
|
|
279
|
-
An HTTP entrypoint function of 2.x (`buildHTTPEntrypoint`) keeps working: it is called in the startup of the entrypoint
|
|
280
|
-
(the objects it gets, such as an authenticator injecting the entrypoint, are available) and awaited, so the application
|
|
281
|
-
launches once its server listens. To move it to `expressHTTPEntrypoint`:
|
|
282
|
-
|
|
283
|
-
- the middlewares and the routes of the application (CORS, compression, a health check, the authenticator, a web UI) go
|
|
284
|
-
into its `before` hook, which gets the objects with `module.getObject()`; the body parsers are its `body` option
|
|
285
|
-
(`{limit: '100mb'}`); the `keepAliveTimeout`/`headersTimeout` of the server its `server` option;
|
|
286
|
-
- the input built from the request (the query, the body, a value of the session) is its `input` option;
|
|
287
|
-
- the `{errno, errMsg, data}` envelope is `jsonEnvelope()`. The applications of 2.x usually answered the errors of the
|
|
288
|
-
actions with 200 and the errors of their express middlewares (`next(error)`: an authenticator's 401) with their
|
|
289
|
-
status: `jsonEnvelope({actionErrorStatus: 200})` does the same — `errorStatus: 200` would answer the 401 with 200 too,
|
|
290
|
-
which the clients redirecting to the login page on a 401 would miss;
|
|
291
|
-
- the other fields of the envelope of the application (`timestamp`, `nonce`, a request id) are given by its `extra`
|
|
292
|
-
option, called for each response, results and errors: `jsonEnvelope({actionErrorStatus: 200, extra: () =>
|
|
293
|
-
({timestamp: Date.now()})})`. They are added after `errno`, `errMsg` and `data`, which they never replace — no
|
|
294
|
-
responder of its own is needed to keep them.
|
|
295
|
-
|
|
296
|
-
```typescript
|
|
297
|
-
import {buildEntrypoints} from 'lakutata/com/entrypoint'
|
|
298
|
-
import {expressHTTPEntrypoint, jsonEnvelope} from 'lakutata/adapter/express'
|
|
299
|
-
import type {Express, NextFunction, Request, Response} from 'express'
|
|
300
|
-
|
|
301
|
-
class Unauthorized extends Error {
|
|
302
|
-
public readonly errno: string = 'E_UNAUTHORIZED'
|
|
303
|
-
|
|
304
|
-
public readonly statusCode: number = 401
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export const entrypoint = buildEntrypoints({
|
|
308
|
-
controllers: [],
|
|
309
|
-
http: expressHTTPEntrypoint({
|
|
310
|
-
port: 3000,
|
|
311
|
-
body: {limit: '100mb'},
|
|
312
|
-
before: async (app: Express): Promise<void> => {
|
|
313
|
-
app.get('/health', (_request: Request, response: Response): unknown => response.json({status: 'ok'}))
|
|
314
|
-
//The authenticator of the requests: its errors keep their status (401)
|
|
315
|
-
app.use((request: Request, _response: Response, next: NextFunction): void => request.headers.authorization ? next() : next(new Unauthorized('Not logged in')))
|
|
316
|
-
},
|
|
317
|
-
//The errors of the actions answered with 200 in the envelope, as in 2.x, with its timestamp
|
|
318
|
-
respond: jsonEnvelope({actionErrorStatus: 200, extra: (): Record<string, unknown> => ({timestamp: Date.now()})})
|
|
319
|
-
})
|
|
320
|
-
})
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
`fastifyHTTPEntrypoint` takes the same options (`fastifyJSONEnvelope({actionErrorStatus: 200, extra})`, the authenticator
|
|
324
|
-
being a hook of fastify). A responder of its own receives the origin of each error as the fourth argument of its
|
|
325
|
-
`error` (`'action'` or `'request'`).
|
|
326
|
-
|
|
327
|
-
### The clients of the service actions and their events
|
|
328
|
-
|
|
329
|
-
The service adapters with clients give the id of the client of a request to its context, `ServiceContext.clientId`:
|
|
330
|
-
`electronIPCServiceEntrypoint` (each page of a window), `socketIOServiceEntrypoint` (each socket, its id given when it
|
|
331
|
-
connects by its `auth.id` or its `id` header), `httpServiceEntrypoint` (the `id` header of the `POST` requests, the `id`
|
|
332
|
-
header or query of the event stream `GET /events`), `jsonRPCServiceEntrypoint` over WebSocket and stdio, and
|
|
333
|
-
`grpcServiceEntrypoint` (the `id` metadata). The entrypoint pushes events to a client with `notify(clientId, event,
|
|
334
|
-
...args)` (it returns whether a client received it) or to every client with `broadcast(event, ...args)` (it returns the
|
|
335
|
-
number of clients):
|
|
336
|
-
|
|
337
|
-
```typescript
|
|
338
|
-
import {Controller, Entrypoint, ServiceContext} from 'lakutata/com/entrypoint'
|
|
339
|
-
import {ServiceAction} from 'lakutata/decorator/ctrl'
|
|
340
|
-
import {Inject} from 'lakutata/decorator/di'
|
|
341
|
-
|
|
342
|
-
class TaskController extends Controller {
|
|
343
|
-
@Inject('entrypoint')
|
|
344
|
-
protected readonly entrypoint: Entrypoint
|
|
345
|
-
|
|
346
|
-
@ServiceAction({cmd: 'start'})
|
|
347
|
-
public async start(): Promise<void> {
|
|
348
|
-
const clientId: string | undefined = (this.context as ServiceContext).clientId
|
|
349
|
-
if (clientId) setTimeout((): void => void this.entrypoint.notify(clientId, 'done', {task: 1}), 1000)
|
|
350
|
-
this.entrypoint.broadcast('started', {task: 1})
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
The renderers of Electron listen to the events with `window.ipc.on(event, listener)` (the Electron templates), the
|
|
356
|
-
sockets with `socket.on(event, listener)`, the HTTP clients with the Server-Sent Events of `GET /events`
|
|
357
|
-
(`EventSource`, curl). A page of a window is a client until it is loaded again or its window is closed.
|
|
358
|
-
|
|
359
|
-
### JSON-RPC 2.0
|
|
360
|
-
|
|
361
|
-
`jsonRPCServiceEntrypoint` serves the service actions over JSON-RPC 2.0: the method and the params (by name) of a
|
|
362
|
-
request are the pattern and the input of an action, `{"method": "order.get", "params": {"id": 1}}` being dispatched as
|
|
363
|
-
`{method: 'order.get', id: 1}` (`methodField` names the field, `toPattern(method, params)` maps them to other patterns,
|
|
364
|
-
`positionalParams` accepts the params by position). The batches and the notifications follow the specification; the
|
|
365
|
-
errors are `-32601` (no action matches), `-32602` (the DTO refuses the input) and `-32000` (the errors of the actions,
|
|
366
|
-
the whole error as their `data`). The transports are HTTP (`POST`), WebSocket (the package `ws`) and stdio (framed by
|
|
367
|
-
lines or by `Content-Length` headers); the last two push the events of the clients as notifications. Over WebSocket, the
|
|
368
|
-
connections that do not answer the pings are closed (`heartbeat`, 30 s); over stdio, the application exits once its
|
|
369
|
-
input ends (`end: 'application'`).
|
|
370
|
-
|
|
371
|
-
```text
|
|
372
|
-
service: [
|
|
373
|
-
jsonRPCServiceEntrypoint({port: 4000, path: '/rpc'}),
|
|
374
|
-
jsonRPCServiceEntrypoint({transport: 'websocket', port: 4001}),
|
|
375
|
-
jsonRPCServiceEntrypoint({transport: 'stdio', framing: 'content-length'})
|
|
376
|
-
]
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
### gRPC and NATS
|
|
380
|
-
|
|
381
|
-
`grpcServiceEntrypoint` (`@lakutata/grpc/service`) serves the service actions over gRPC: the service
|
|
382
|
-
`lakutata.service.v1.Service` (`SERVICE_PROTO`, for the clients of other languages) invokes an action with the JSON of
|
|
383
|
-
its pattern and input, and streams the events of a client. `GRPCServiceProxy`, declared with `buildGRPCServiceProxy`,
|
|
384
|
-
invokes the actions of a remote application and listens to its events. The deadline of a call aborts its action
|
|
385
|
-
(`this.signal`). See the README of `@lakutata/grpc`.
|
|
386
|
-
|
|
387
|
-
```typescript
|
|
388
|
-
import {Application} from 'lakutata'
|
|
389
|
-
import {buildGRPCServiceProxy, type GRPCServiceProxy} from '@lakutata/grpc/service'
|
|
390
|
-
|
|
391
|
-
Application
|
|
392
|
-
.run(() => ({
|
|
393
|
-
id: 'shop.app',
|
|
394
|
-
name: 'Shop',
|
|
395
|
-
components: {
|
|
396
|
-
orders: buildGRPCServiceProxy({address: 'orders.internal:50051'})
|
|
397
|
-
}
|
|
398
|
-
}))
|
|
399
|
-
.onLaunched(async (app: Application): Promise<void> => {
|
|
400
|
-
const orders: GRPCServiceProxy = await app.getObject<GRPCServiceProxy>('orders')
|
|
401
|
-
orders.on('orderChanged', (order: { id: number }): void => console.log(order))
|
|
402
|
-
console.log(await orders.invoke({cmd: 'order.get', id: 1}))
|
|
403
|
-
})
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
`natsServiceEntrypoint` (`@lakutata/nats`) serves them over NATS, on the subject of the application id, the instances
|
|
407
|
-
sharing a queue group; `ServiceProxy`, declared with `buildServiceProxy`, invokes them. See the README of
|
|
408
|
-
`@lakutata/nats`.
|