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
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# Upgrading from Lakutata 1.x
|
|
2
|
+
|
|
3
|
+
Lakutata 1.x was a family of packages: `@lakutata/core` 1.x, the modules `@lakutata-module/*` (`service`, `api`), the
|
|
4
|
+
components `@lakutata-component/*` (`typeorm`, `cacher`…) and `@lakutata/cli` 1.0. Lakutata 3 is a different framework
|
|
5
|
+
with a different API, so a 1.x project is ported to it rather than upgraded in place. This page gives the way to port
|
|
6
|
+
it and what each part of 1.x becomes.
|
|
7
|
+
|
|
8
|
+
It is for the projects that depend on `@lakutata/core` at version 1. A project that depends on the package `lakutata` at
|
|
9
|
+
version 2 follows [UpgradingFrom2](UpgradingFrom2.md), not this page. The 1.x packages covered are those published on
|
|
10
|
+
npm (`@lakutata/core` 1.0.60, `@lakutata-module/service` 1.2.7, `@lakutata-module/api` 1.0.29,
|
|
11
|
+
`@lakutata-component/typeorm` 1.1.19, `@lakutata-component/cacher` 1.0.15, `@lakutata/cli` 1.0.37); builds of these
|
|
12
|
+
packages obtained elsewhere may have APIs that this page does not cover.
|
|
13
|
+
|
|
14
|
+
## 1. Telling 1.x apart
|
|
15
|
+
|
|
16
|
+
| Sign | 1.x | 3.x |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| Dependencies | `@lakutata/core` `^1…`, `@lakutata-module/service`, `@lakutata-component/*` | `lakutata` `^3…` |
|
|
19
|
+
| `src/App.ts` | `createApp(Config(true)).then(…)` | `Application.run(config)` |
|
|
20
|
+
| Modules | CommonJS: imports without extensions, `__dirname` | ES modules: relative imports ending in `.js` |
|
|
21
|
+
| Service actions | `@Controller({_ctrl: …})` and `@PATTERN({_act: …})` from `@lakutata-module/service` | `@ServiceAction({…})` from `lakutata/decorator/ctrl` |
|
|
22
|
+
| Validation | `Validator` schemas (yup) given to `@Accept` | DTO classes with `@Expect` |
|
|
23
|
+
| Node.js | `>=14.15.1` (`@lakutata/core` 1.0.60) | `^22.13.0 \|\| >=24.11.0` |
|
|
24
|
+
|
|
25
|
+
**`@lakutata/core` 3 is not a new version of the 1.x library.** It is the core of Lakutata 3, a different API under the
|
|
26
|
+
same name. `npm install @lakutata/core@latest`, or a range such as `*`, in a 1.x project puts it in place of the 1.x
|
|
27
|
+
core and breaks the build. Never upgrade the 1.x packages one by one: the ported application depends on `lakutata` 3,
|
|
28
|
+
which brings its own `@lakutata/core`.
|
|
29
|
+
|
|
30
|
+
## 2. The way to port
|
|
31
|
+
|
|
32
|
+
1. **Create a 3.x project** next to the 1.x one, from the template of its kind: `service` for a service or a gateway
|
|
33
|
+
(an HTTP API, a health check, a `Dockerfile`), `cli` for a command line tool
|
|
34
|
+
([GettingStarted](GettingStarted.md)):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx @lakutata/cli create --name orders --template service
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
2. **Port it area by area**, building and testing after each: the database and its entities (section 8), the inputs as
|
|
41
|
+
DTOs (section 7), the components (section 3), the service actions and their callers (section 5), the HTTP API
|
|
42
|
+
(section 6), the configuration (section 9), the tests (section 10).
|
|
43
|
+
3. **Keep the contracts of the clients**: the patterns of the service actions and their keys, the `errno` codes, the
|
|
44
|
+
HTTP routes and the shape of their responses, the names of the events. Then the callers, the user interfaces and the
|
|
45
|
+
devices do not change with the port.
|
|
46
|
+
4. **Plan the order of the services.** The service module of 1.x and the service transports of 3.x do not talk to each
|
|
47
|
+
other: a 3.x service cannot answer the `invoke()` of a 1.x service, nor call one. Move the services that call each
|
|
48
|
+
other together, or keep a 1.x service as a bridge until its callers are ported.
|
|
49
|
+
|
|
50
|
+
Editing the 1.x project in place does not work: the module system, the container, the validation and the transports
|
|
51
|
+
all change at once.
|
|
52
|
+
|
|
53
|
+
## 3. The application and its objects
|
|
54
|
+
|
|
55
|
+
See [Application](Application.md), [Objects](Objects.md) and [Injection](Injection.md).
|
|
56
|
+
|
|
57
|
+
| 1.x | 3.x |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `createApp(Config(isProd))`, a promise of the application | `Application.run(config).onLaunched(…)` |
|
|
60
|
+
| The options `id`, `name`, `timezone`, `components: {name: {class, …}}` | The same options |
|
|
61
|
+
| `modules: {name: {class, …}}` | `components`: a long-lived object is a `Component` |
|
|
62
|
+
| `plugins: [{class, scope, …}]`, reached as `app.<ClassName>` | `providers` (`@Singleton()` for a shared client) or `components`, injected by name |
|
|
63
|
+
| `processes`, `threads`, `workflows`, `onModulesLoaded`, `onComponentsLoaded` | None: `bootstrap` steps, and `child_process` or `worker_threads` of Node.js in a component |
|
|
64
|
+
| `initialize()` | `init()`; 3.x adds `destroy()`, run at the teardown |
|
|
65
|
+
| `app.on('ready', …)` in a component | The `launched()` hook of the component, or `onLaunched` of the application |
|
|
66
|
+
| `@Inject('name')`, `@Inject(Application)` | The same, from `lakutata/decorator/di` |
|
|
67
|
+
| `app.Components.get<T>('name')`, `app.Modules.service`, and the `declare module` that types them | `@Inject('name')` on a typed property; `await app.getObject<T>('name')` outside the objects; no augmentation |
|
|
68
|
+
| `@Configurable()` | `@Configurable(schema)`: the schema validates the option at launch (without one, any value is accepted) |
|
|
69
|
+
| The global `getApp()` | `@Inject(Application)` |
|
|
70
|
+
| `new SomeComponent()` in a test | Never: `await new Container().set(SomeComponent, options)` |
|
|
71
|
+
| `app.Logger.info`, `.warning`, `.error`, `.debug`… (winston) | `@Inject('log')` an `ILogger`: `info`, `warn`, `error`, `debug`, `trace` (`emerg`, `alert`, `crit` become `error`, `notice` becomes `info`) |
|
|
72
|
+
| `app.Security.MD5()`, `SHA1()`, `SHA256()` | `md5()`, `sha1()`, `sha256()` from `lakutata/utils`, which return a `Buffer` (`.toString('hex')`); with a key, `hmac()` from `lakutata/security` |
|
|
73
|
+
| `app.Security.generateRandomString()` | `randomString()` from `lakutata/utils` |
|
|
74
|
+
| `app.Security.encryptByPassword()`, `encryptByKey()` and their `decrypt…` | The same functions, from `lakutata/security` ([Security](Security.md)) |
|
|
75
|
+
| `app.Security.generatePasswordHash()`, `validatePassword()` | The `PasswordHash` provider, from `lakutata/provider/passwordHash` |
|
|
76
|
+
| `app.Formatter.asTimestamp()`, `asDateText()` | `Time`, from `lakutata` ([Time](Time.md)) |
|
|
77
|
+
| `app.JSON`, `app.MessagePack`, `app.HttpRequest`, `app.Shell` | `JSON`, `MessagePack` from `lakutata/utils`, `fetch()`, `child_process` of Node.js |
|
|
78
|
+
| `delay()` | `delay()` from `lakutata/utils` |
|
|
79
|
+
| `app.emit()`, `app.on()` as an event bus | A component is an event emitter: inject it and use its `emit()` and `on()` |
|
|
80
|
+
| `throw this.generateException(OrderNotFoundException, 'Order', 'not found')` | `throw new OrderNotFoundException('Order {id} not found', {id})`; the exceptions have no `district` |
|
|
81
|
+
|
|
82
|
+
A 1.x component:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
//1.x
|
|
86
|
+
export class OrderComponent extends Component {
|
|
87
|
+
@Inject(Application) protected readonly app: Application
|
|
88
|
+
@Inject('db') protected readonly db: TypeOrmComponent
|
|
89
|
+
@Configurable() protected readonly keepDays: number = 30
|
|
90
|
+
|
|
91
|
+
protected async initialize(): Promise<void> {
|
|
92
|
+
this.app.on('ready', () => this.startCleanup())
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The same component in 3.x:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import {Component, DTO, Exception, type ILogger} from 'lakutata'
|
|
101
|
+
import {Configurable, Inject} from 'lakutata/decorator/di'
|
|
102
|
+
import type {Database} from 'lakutata/com/database'
|
|
103
|
+
|
|
104
|
+
export class OrderNotFoundException extends Exception {
|
|
105
|
+
public errno: string = 'E_ORDER_NOT_FOUND'
|
|
106
|
+
|
|
107
|
+
public readonly statusCode: number = 404
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class Orders extends Component {
|
|
111
|
+
@Inject('db')
|
|
112
|
+
protected readonly db: Database
|
|
113
|
+
|
|
114
|
+
@Inject('log')
|
|
115
|
+
protected readonly log: ILogger
|
|
116
|
+
|
|
117
|
+
@Configurable(DTO.Number().integer().min(1))
|
|
118
|
+
protected readonly keepDays: number = 30
|
|
119
|
+
|
|
120
|
+
protected timer: NodeJS.Timeout
|
|
121
|
+
|
|
122
|
+
//Once the application has launched (1.x: app.on('ready'))
|
|
123
|
+
protected async launched(): Promise<void> {
|
|
124
|
+
this.timer = setInterval((): void => this.log.info('Keeping %d days of orders', this.keepDays), 3600000)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
protected async destroy(): Promise<void> {
|
|
128
|
+
clearInterval(this.timer)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public async get(id: string): Promise<Record<string, unknown>> {
|
|
132
|
+
const rows: Record<string, unknown>[] = await this.db.query('SELECT * FROM orders WHERE id = ?', [id])
|
|
133
|
+
if (!rows.length) throw new OrderNotFoundException('Order {id} not found', {id: id})
|
|
134
|
+
return rows[0]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- The `errno` of the exceptions stays, so the clients that test it keep working. 3.x adds `statusCode`, the HTTP status
|
|
140
|
+
of the error, and builds the message from a template ([Exceptions](Exceptions.md)).
|
|
141
|
+
- One class may still be declared several times under different names with different options (several caches, two
|
|
142
|
+
HTTP servers): each declaration is its own instance, injected by its name.
|
|
143
|
+
|
|
144
|
+
## 4. The packages
|
|
145
|
+
|
|
146
|
+
| 1.x | 3.x |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `@lakutata/core` 1.x | `lakutata`: `lakutata`, `lakutata/decorator/di`, `lakutata/decorator/dto`, `lakutata/utils` |
|
|
149
|
+
| `@lakutata-module/service` | `lakutata/com/entrypoint` (`@ServiceAction` and the service transports), `@lakutata/nats` (section 5) |
|
|
150
|
+
| `@lakutata-module/api` | `lakutata/adapter/express` or `lakutata/adapter/fastify`, with `@GET`, `@POST`… (section 6) |
|
|
151
|
+
| `@lakutata-component/typeorm` | `lakutata/com/database`, `lakutata/orm`, `lakutata/decorator/orm` (section 8) |
|
|
152
|
+
| `@lakutata-component/cacher` | `lakutata/com/cacher` (section 8) |
|
|
153
|
+
| `@lakutata/cli` 1.0 (`lakutata`, `lakutata-deploy`) | `@lakutata/cli` 3: `lakutata create`, `run`, `migration`, `package`… ([Command](Command.md)); no deploy command |
|
|
154
|
+
| The other `@lakutata-component/*` packages | No counterpart: keep what they did in the application, or use another library |
|
|
155
|
+
|
|
156
|
+
Import only from these subpaths: 3.x does not support imports from the files inside a package (1.x projects sometimes
|
|
157
|
+
imported from `/distro/…`, `/src/…` or `/build/…` of a package).
|
|
158
|
+
|
|
159
|
+
## 5. Service calls
|
|
160
|
+
|
|
161
|
+
In 1.x, each application had a `service` module: it registered its controllers with a registry application (backed by
|
|
162
|
+
Redis), and called the actions of the others with `invoke({serviceId, data, timeout})`; its channels carried events
|
|
163
|
+
between the services. In 3.x there is no registry application: the `entrypoint` component serves the service actions
|
|
164
|
+
over a transport, and a proxy calls them ([ServiceCalls](ServiceCalls.md)):
|
|
165
|
+
|
|
166
|
+
- **NATS** ([NATS](NATS.md)) is the closest to the 1.x model: the services share one NATS server, address each other
|
|
167
|
+
by their application id (as the `serviceId` of 1.x), balance the calls between the instances of a service, and
|
|
168
|
+
publish events. Its `ServiceProxy` calls another service; the NATS server replaces the registry application.
|
|
169
|
+
- **The HTTP service** with `HTTPServiceProxy`, or **gRPC** ([GRPC](GRPC.md)), call a service at a known address.
|
|
170
|
+
|
|
171
|
+
A 1.x controller of the service module:
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
//1.x
|
|
175
|
+
@Controller({_ctrl: 'order'})
|
|
176
|
+
export class OrderController extends BaseController {
|
|
177
|
+
@Inject('order') protected readonly order: OrderComponent
|
|
178
|
+
|
|
179
|
+
@PATTERN({_act: 'get'})
|
|
180
|
+
@Accept(TGetOrderOptionsSchema)
|
|
181
|
+
public async get(inp: TPattern & TGetOrderOptions): Promise<IOrder> {
|
|
182
|
+
return await this.order.get(inp.id)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The same actions in 3.x, served over NATS, and a proxy that calls another service:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import {Application, DTO} from 'lakutata'
|
|
191
|
+
import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
|
|
192
|
+
import {Prefix, ServiceAction} from 'lakutata/decorator/ctrl'
|
|
193
|
+
import {Expect} from 'lakutata/decorator/dto'
|
|
194
|
+
import {buildNatsClientOptions, buildServiceProxy, natsServiceEntrypoint} from '@lakutata/nats'
|
|
195
|
+
|
|
196
|
+
class GetOrderDTO extends DTO {
|
|
197
|
+
@Expect(DTO.String().required())
|
|
198
|
+
public id: string
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
//The keys of the patterns are free: the callers keep sending {_ctrl: 'order', _act: 'get', id}
|
|
202
|
+
@Prefix({pattern: {_ctrl: 'order'}})
|
|
203
|
+
class OrderController extends Controller {
|
|
204
|
+
@ServiceAction({_act: 'get'}, GetOrderDTO)
|
|
205
|
+
public async get(input: GetOrderDTO): Promise<{ id: string }> {
|
|
206
|
+
return {id: input.id}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
Application.run(() => ({
|
|
211
|
+
//The id the other services call: the serviceId of 1.x
|
|
212
|
+
id: 'orders.app',
|
|
213
|
+
name: 'orders',
|
|
214
|
+
components: {
|
|
215
|
+
nats: buildNatsClientOptions({servers: process.env.NATS_URL ?? 'nats://127.0.0.1:4222'}),
|
|
216
|
+
entrypoint: buildEntrypoints({
|
|
217
|
+
controllers: [OrderController],
|
|
218
|
+
service: natsServiceEntrypoint('nats')
|
|
219
|
+
})
|
|
220
|
+
},
|
|
221
|
+
providers: {
|
|
222
|
+
//Another service: await this.users.invoke({_ctrl: 'user', _act: 'get', id}, 30000)
|
|
223
|
+
users: buildServiceProxy({natsComponentName: 'nats', serviceId: 'users.app'})
|
|
224
|
+
},
|
|
225
|
+
bootstrap: ['nats', 'entrypoint']
|
|
226
|
+
}))
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
- **The patterns.** `@Prefix({pattern})` plus `@ServiceAction` replace `@Controller` plus `@PATTERN`, with the same
|
|
230
|
+
keys, so the messages of the callers do not change (`_action` instead of `_act` works the same way). Extra keys of a
|
|
231
|
+
pattern still select another action: `{_act: 'query', batch: true}` and `{_act: 'query', batch: false}` are two
|
|
232
|
+
actions. A key that the prefix and an action give different values is an error in 3.x (in 1.x, the controller's
|
|
233
|
+
value won).
|
|
234
|
+
- **The input.** The action receives the message without the keys of its pattern: a method that read `inp.batch` or
|
|
235
|
+
`inp._act` reads them from `this.context.input`, the whole message. Fields that a gateway added to every message (the
|
|
236
|
+
id of the user, of the organization) stay ordinary fields of the DTOs, or move to a middleware
|
|
237
|
+
([Middlewares](Middlewares.md)).
|
|
238
|
+
- **Calls.** `this.app.Modules.service.invoke({serviceId, data, timeout})` becomes `this.users.invoke(message, timeout)`
|
|
239
|
+
on an injected proxy. A failed action throws a `ServiceInvokeException` that carries the `errno` and the other
|
|
240
|
+
properties of the remote error. The `transform` option and the chains of several invoke objects have no counterpart:
|
|
241
|
+
write them as code; `service.parallel(…)` becomes `Promise.all()` of the calls. A 1.x call waited 60 seconds by
|
|
242
|
+
default, a 3.x proxy 10 minutes: give the calls their timeout.
|
|
243
|
+
- **Events.** A channel's `triggerAll(event, …args)` becomes `nats.emitServiceEvent(event, …args)` of the NATS
|
|
244
|
+
component, received by `proxy.on(event, listener)` or `nats.onServiceEvent(serviceId, event, listener)` in every
|
|
245
|
+
instance. For `triggerOne`, where one instance of each service receives the event, publish on a subject of your own
|
|
246
|
+
(`nats.publish(subject, payload)`) and subscribe with the id of the service as the queue group
|
|
247
|
+
(`nats.subscribe(subject, handler, {queue: app.appId})`): the instances of a service share the group.
|
|
248
|
+
- **Work shared by the instances.** A job that one instance of a service must take, such as the processing of a
|
|
249
|
+
stream of data, fits a message queue ([Queue](Queue.md)): the instances of a group share its messages, with retries.
|
|
250
|
+
- **A clock service.** An application that published timer ticks to the others is replaced by `@Schedule` in each
|
|
251
|
+
service, with `exclusive` for the jobs that must run in one instance only ([Schedules](Schedules.md)).
|
|
252
|
+
|
|
253
|
+
## 6. The HTTP API
|
|
254
|
+
|
|
255
|
+
The `api` module of 1.x becomes the HTTP adapter of the entrypoint, `expressHTTPEntrypoint()` or
|
|
256
|
+
`fastifyHTTPEntrypoint()` ([HTTP](HTTP.md)). A route is a controller method with `@GET`, `@POST`…; its input is one
|
|
257
|
+
object, the query and the body with the route parameters over them, validated by its DTO. The parameter decorators of
|
|
258
|
+
1.x (the body, the query, a header, the request and the response) are replaced by the DTO and by `this.context`, an
|
|
259
|
+
`HTTPContext` with `request` and `response`.
|
|
260
|
+
|
|
261
|
+
The clients keep working when the responses keep their shape. A responder of your own writes them as the 1.x
|
|
262
|
+
clients expect, for example `{code, payload}` with `code` 0 for a result:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import {Application, DTO} from 'lakutata'
|
|
266
|
+
import {buildEntrypoints, Controller, HTTPContext} from 'lakutata/com/entrypoint'
|
|
267
|
+
import {POST} from 'lakutata/decorator/ctrl'
|
|
268
|
+
import {Expect} from 'lakutata/decorator/dto'
|
|
269
|
+
import {describeError, expressHTTPEntrypoint, statusOfError, type ExpressResponder} from 'lakutata/adapter/express'
|
|
270
|
+
import type {Request, Response} from 'express'
|
|
271
|
+
|
|
272
|
+
//The responses as the clients of 1.x read them: {code: 0, payload} for a result, a code that is not 0 for an error
|
|
273
|
+
const legacyEnvelope: ExpressResponder = {
|
|
274
|
+
success(response: Response, data: unknown): void {
|
|
275
|
+
response.status(200).json({code: 0, payload: data ?? null})
|
|
276
|
+
},
|
|
277
|
+
error(response: Response, error: unknown): void {
|
|
278
|
+
response.status(statusOfError(error)).json({code: 1, payload: describeError(error)})
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
class QueryDTO extends DTO {
|
|
283
|
+
@Expect(DTO.String().required())
|
|
284
|
+
public method: string
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
class RpcController extends Controller {
|
|
288
|
+
//POST /rpc?method=order.get with a JSON body
|
|
289
|
+
@POST('/rpc', QueryDTO)
|
|
290
|
+
public async call(input: QueryDTO): Promise<{ method: string, token?: string }> {
|
|
291
|
+
//A header, read from the request of the context
|
|
292
|
+
const request: Request = (this.context as HTTPContext).request as Request
|
|
293
|
+
return {method: input.method, token: request.header('x-token')}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
Application.run(() => ({
|
|
298
|
+
id: 'gateway.app',
|
|
299
|
+
name: 'gateway',
|
|
300
|
+
components: {
|
|
301
|
+
entrypoint: buildEntrypoints({
|
|
302
|
+
controllers: [RpcController],
|
|
303
|
+
http: expressHTTPEntrypoint({port: 8080, respond: legacyEnvelope})
|
|
304
|
+
})
|
|
305
|
+
},
|
|
306
|
+
bootstrap: ['entrypoint']
|
|
307
|
+
}))
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
- `statusOfError()` and `describeError()` give the status and the `{errno, errMsg}` that the adapter would send; the
|
|
311
|
+
responder chooses the shape. The `api` module of 1.x answered `{statusCode, code, error, payload, timestamp, server}`
|
|
312
|
+
(or what its `responseFormatter` made of it): write the fields that the clients read. `jsonEnvelope()` gives the
|
|
313
|
+
envelope of 3.x, `{errno, errMsg, data}`, instead.
|
|
314
|
+
- The `api` module allowed any origin (CORS) and signed its responses (the `x-signature` headers). The 3.x adapters
|
|
315
|
+
send no CORS headers unless a middleware adds them ([HTTP](HTTP.md), section 7); a client that checks the signatures
|
|
316
|
+
needs a middleware that adds them.
|
|
317
|
+
- A server that a 1.x component started itself (an HTTP server for Socket.IO, another web framework) can stay in a
|
|
318
|
+
component, started in `startup()` and closed in `destroy()`. A `socketIOServiceEntrypoint()` can also share the
|
|
319
|
+
server of the HTTP adapter and serve the service actions over Socket.IO ([ServiceCalls](ServiceCalls.md), section 4).
|
|
320
|
+
|
|
321
|
+
## 7. Validation
|
|
322
|
+
|
|
323
|
+
The `Validator` of 1.x builds yup schemas; 3.x validates with DTO classes, whose schemas have the API of joi
|
|
324
|
+
([DTOValidation](DTOValidation.md)). A 1.x pair of a schema and a type becomes one DTO class:
|
|
325
|
+
|
|
326
|
+
| 1.x (`Validator`, yup) | 3.x (`DTO`) |
|
|
327
|
+
|---|---|
|
|
328
|
+
| `Validator.Object({…})` and a type `T…Options` | A class extending `DTO`, one `@Expect` per property |
|
|
329
|
+
| `Validator.String`, `.Number`, `.Boolean`, `.Date`, `.Array(of)`, `.Any` | `DTO.String()`, `DTO.Number()`, `DTO.Boolean()`, `DTO.Date()`, `DTO.Array(of)`, `DTO.Any()` |
|
|
330
|
+
| `Validator.Integer`, `Validator.Email` | `DTO.Number().integer()`, `DTO.String().email()` |
|
|
331
|
+
| `.required()` | `.required()` |
|
|
332
|
+
| `.notRequired()`, `.optional()` | `.optional()` |
|
|
333
|
+
| `.nullable()` | `.allow(null)` |
|
|
334
|
+
| `.oneOf([…])` | `.valid(…)` |
|
|
335
|
+
| `.matches(regex)` | `.pattern(regex)` |
|
|
336
|
+
| `.test(fn)` | `.custom(fn)` |
|
|
337
|
+
| `.when(…)` | `.when(…)` (the options of joi) |
|
|
338
|
+
| `schema.concat(other)` | A DTO that extends another, or `.concat()` |
|
|
339
|
+
| `@Accept(schema, {stripUnknown})` on a method | `@Accept(DTO)` on a method, or the DTO of an action |
|
|
340
|
+
|
|
341
|
+
- **Conversion.** The `@Accept` of 1.x converted the values by default (yup's `strict: false`): `'5'` became 5 on every
|
|
342
|
+
validated method. The 3.x schemas are strict: a value of the wrong type is refused, not converted. An input that
|
|
343
|
+
relied on the conversion (numbers, booleans or dates arriving as text, from a query string, a route parameter or any
|
|
344
|
+
other source) declares it with `.strict(false)` on its schema.
|
|
345
|
+
- The unknown keys are removed by default; a DTO that keeps them extends `FlexibleDTO` (`lakutata/dtos`).
|
|
346
|
+
- An invalid input fails with an `InvalidValueException` (`E_INVALID_VALUE`, HTTP status 400).
|
|
347
|
+
|
|
348
|
+
## 8. Databases and caches
|
|
349
|
+
|
|
350
|
+
**The database.** The `TypeOrmComponent` of 1.x becomes the `Database` component, declared with
|
|
351
|
+
`buildDatabaseOptions()` ([Database](Database.md), [Entities](Entities.md)). The ORM is TypeORM 1.1 (1.x used 0.3):
|
|
352
|
+
|
|
353
|
+
| 1.x | 3.x |
|
|
354
|
+
|---|---|
|
|
355
|
+
| `db: {class: TypeOrmComponent, options: {type: 'mysql', …, entities: [glob]}}` | `db: buildDatabaseOptions({type: 'mysql', …, entities: [Order, …]})` |
|
|
356
|
+
| `this.db.getRepository(Order)`, `this.db.manager`, `this.db.transaction(…)`, `this.db.query(sql)` | The same methods of `Database` |
|
|
357
|
+
| The decorators, operators and types imported from `@lakutata-component/typeorm` or `typeorm` | `lakutata/decorator/orm` (decorators) and `lakutata/orm` (operators such as `In`, `Like`, and the types); no `typeorm` package |
|
|
358
|
+
| `relations: ['customer']`, `select: ['id']` | `relations: {customer: true}`, `select: {id: true}` |
|
|
359
|
+
| `ConnectionOptions` | `MysqlDataSourceOptions`… (the `ConnectionOptions` names are deprecated aliases) |
|
|
360
|
+
| `this.db.queryData(options, Entity)` and its query language | No counterpart: keep the query language as a component of the application, on top of the find options of TypeORM, and `paginator` (`lakutata/dtos`) for the pages |
|
|
361
|
+
|
|
362
|
+
- **`synchronize: true` in production** changed the tables at every start. Turn it off and use migrations
|
|
363
|
+
([Migrations](Migrations.md)): generate a first migration against a copy of the production database (it should be
|
|
364
|
+
empty when the entities match the tables), then let `migrationsRun` apply the next ones at startup.
|
|
365
|
+
- **Globs with `__dirname`** do not exist in ES modules: list the entity classes, and build the migration paths with
|
|
366
|
+
`import.meta.dirname`.
|
|
367
|
+
|
|
368
|
+
**The caches.** The `CacherComponent` of 1.x becomes the `Cacher` component, declared with `buildCacherOptions()`
|
|
369
|
+
([Cache](Cache.md)). `get(key)`, `set(key, value, ttl)` and `del(key)` keep their names (the time to live, given only
|
|
370
|
+
by the options in 1.x, may be given to each `set`), and `wrap(key, fn)` reads through the cache. A missing key gives
|
|
371
|
+
`undefined`, where 1.x gave `null` (on a failing store too), and `null` and `false` can be cached as values. The keys
|
|
372
|
+
are built differently, so the entries that 1.x wrote are not found: start with an empty cache.
|
|
373
|
+
|
|
374
|
+
## 9. Configuration, mode and entries
|
|
375
|
+
|
|
376
|
+
- **One configuration.** 1.x chose production or development by the entry file (`App.ts` called `Config(true)`, the
|
|
377
|
+
development entry `Config(false)`). In 3.x, `config()` is one function, and the mode comes from the environment:
|
|
378
|
+
`MODE`, else `NODE_ENV`, else the `mode` option; `app.mode()` gives it ([Application](Application.md)).
|
|
379
|
+
- **The values** come from the environment variables, read in `config()` or given as defaults to the `@Configurable`
|
|
380
|
+
properties. A declaration that is invalid stops the launch with an `InvalidObjectOptionsException` listing every
|
|
381
|
+
invalid option, so a missing variable fails at once rather than at the first use.
|
|
382
|
+
- **Functions in the options** that received the application to reach other components become injections: the
|
|
383
|
+
component injects what it needs.
|
|
384
|
+
- **Credentials** in the code of the configuration move to environment variables or secrets of the deployment.
|
|
385
|
+
|
|
386
|
+
## 10. Tests, build and deployment
|
|
387
|
+
|
|
388
|
+
- **Tests.** A 1.x `npm test` that booted the application against shared development servers becomes `node --test` on
|
|
389
|
+
the compiled tests: each test file starts the application with `Application.run(config(port))` on a port of its
|
|
390
|
+
own, calls it, and disposes of it; a component is tested alone in a `Container`; another service is replaced by a
|
|
391
|
+
stand-in ([examples](examples/README.md)).
|
|
392
|
+
- **The `lakutata` field of `package.json`** told the 1.x tool the kind of the project (`type`), its directories and
|
|
393
|
+
its configuration file. In 3.x the field has another meaning (the `resources` and the `installer` of a packed
|
|
394
|
+
application, [Packaging](Packaging.md)): remove the 1.x one.
|
|
395
|
+
- **Node.js, TypeScript and modules.** Node.js `^22.13.0 || >=24.11.0`, TypeScript 6 as in the templates, ES modules
|
|
396
|
+
(`"type": "module"`, relative imports ending in `.js`), `tsconfig.json` extending `lakutata/tsconfig.json`.
|
|
397
|
+
- **Ports.** The service module listened on a port of its own (6912 by default) and the registry on another (6911).
|
|
398
|
+
The 3.x adapters listen on the ports of their options, and a service on NATS only connects out to the NATS server:
|
|
399
|
+
the `Dockerfile` exposes those ports instead.
|
|
400
|
+
- **Scripts.** The template's scripts replace `shx rm -rf ./build` (`lakutata clean`), `prepare` compiling on install,
|
|
401
|
+
and the development entry; `lakutata-deploy` has no counterpart: the `service` template has a `Dockerfile` and a
|
|
402
|
+
`docker:build` script, and the image is pushed by the deployment's own tools.
|
|
403
|
+
|
|
404
|
+
## 11. User interfaces
|
|
405
|
+
|
|
406
|
+
The web interfaces of a 1.x system, built apart and calling a gateway over HTTP, do not use Lakutata and do not change,
|
|
407
|
+
as long as the gateway keeps its routes and the shape of its responses (section 6), the patterns that they name, and
|
|
408
|
+
the `errno` codes that they test. Their WebSocket clients keep working when the server keeps its path and its events.
|
|
409
|
+
|
|
410
|
+
## 12. Checking the port
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
npm ls @lakutata/core # one version: the 3.x one that lakutata brings, no 1.x left
|
|
414
|
+
npm run build # no import from @lakutata-module/*, @lakutata-component/* or @lakutata/core 1.x
|
|
415
|
+
grep -rnE "createApp|generateException|initialize\(|app\.(Components|Modules|Logger)|Validator\.|__dirname" src
|
|
416
|
+
npm test
|
|
417
|
+
MODE=production npm start # the launch log, then Ctrl+C: the application stops gracefully
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
- The `grep` lists the 1.x code left. Check each line it finds.
|
|
421
|
+
- The callers and the interfaces get the same answers as from the 1.x service: the same patterns, `errno` codes and
|
|
422
|
+
shapes of response.
|
|
423
|
+
- The database: `lakutata migration generate` against a copy of production generates no change.
|
|
424
|
+
|
|
425
|
+
## 13. Common mistakes
|
|
426
|
+
|
|
427
|
+
- Upgrading `@lakutata/core` in the 1.x project: version 3 is another library (section 1).
|
|
428
|
+
- Reading the keys of the pattern from the input of an action: they are removed from it (section 5).
|
|
429
|
+
- Inputs that relied on the conversion of 1.x (`'5'` for 5): the 3.x schemas refuse them without `.strict(false)`
|
|
430
|
+
(section 7).
|
|
431
|
+
- Testing `=== null` for a missing cache entry: a miss is `undefined` (section 8).
|
|
432
|
+
- `app.Logger.warning(…)`: the method is `warn`.
|
|
433
|
+
- Leaving `synchronize: true` on for production after the port: changes of entities then alter the tables at startup.
|
|
434
|
+
- Mixing the two frameworks in one process, or a 1.x service calling a 3.x one: their service transports do not
|
|
435
|
+
understand each other (section 2).
|