lakutata 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +50 -30
  2. package/README.zh-CN.md +130 -0
  3. package/dist/cjs/exports/QueueComponent.d.ts +1 -0
  4. package/dist/cjs/exports/QueueComponent.js +20 -0
  5. package/dist/esm/exports/QueueComponent.js +1 -0
  6. package/dist/types/exports/QueueComponent.d.ts +1 -0
  7. package/doc/en/Application.md +107 -0
  8. package/doc/en/CLI.md +274 -0
  9. package/doc/en/Cache.md +379 -0
  10. package/doc/en/Command.md +110 -0
  11. package/doc/en/ContainerdIntegration.md +17 -16
  12. package/doc/en/Controllers.md +256 -0
  13. package/doc/en/DBus.md +291 -0
  14. package/doc/en/DTOValidation.md +72 -43
  15. package/doc/en/Database.md +188 -0
  16. package/doc/en/DatabaseDrivers.md +287 -0
  17. package/doc/en/Decorator.md +52 -40
  18. package/doc/en/DockerIntegration.md +7 -6
  19. package/doc/en/Entities.md +240 -0
  20. package/doc/en/Entrypoint.md +216 -0
  21. package/doc/en/Exceptions.md +174 -0
  22. package/doc/en/GRPC.md +251 -0
  23. package/doc/en/GettingStarted.md +165 -0
  24. package/doc/en/HTTP.md +507 -0
  25. package/doc/en/HelperFunctions.md +11 -11
  26. package/doc/en/I18n.md +119 -115
  27. package/doc/en/Injection.md +144 -0
  28. package/doc/en/Installers.md +280 -0
  29. package/doc/en/Logger.md +206 -0
  30. package/doc/en/MCP.md +293 -0
  31. package/doc/en/Middlewares.md +213 -0
  32. package/doc/en/Migrations.md +202 -0
  33. package/doc/en/MongoEmbedded.md +149 -0
  34. package/doc/en/Monitor.md +164 -0
  35. package/doc/en/Mutex.md +27 -25
  36. package/doc/en/NATS.md +246 -0
  37. package/doc/en/NativeAddons.md +212 -0
  38. package/doc/en/Objects.md +143 -0
  39. package/doc/en/Overview.md +62 -85
  40. package/doc/en/Packaging.md +193 -0
  41. package/doc/en/ProjectStructure.md +124 -0
  42. package/doc/en/Queue.md +269 -0
  43. package/doc/en/Schedules.md +217 -0
  44. package/doc/en/Security.md +10 -9
  45. package/doc/en/ServiceCalls.md +504 -0
  46. package/doc/en/Time.md +19 -20
  47. package/doc/en/UpgradingFrom1.md +435 -0
  48. package/doc/en/UpgradingFrom2.md +315 -0
  49. package/doc/en/Workspaces.md +189 -0
  50. package/doc/en/examples/BackgroundJobs.md +677 -0
  51. package/doc/en/examples/CommandLineTool.md +664 -0
  52. package/doc/en/examples/DesktopApp.md +809 -0
  53. package/doc/en/examples/DeviceService.md +607 -0
  54. package/doc/en/examples/MCPServer.md +653 -0
  55. package/doc/en/examples/README.md +16 -0
  56. package/doc/en/examples/RestAPI.md +490 -0
  57. package/doc/en/examples/ServiceCalls.md +829 -0
  58. package/doc/en/examples/WebApp.md +568 -0
  59. package/doc/zh/Application.md +75 -0
  60. package/doc/zh/CLI.md +250 -0
  61. package/doc/zh/Cache.md +328 -0
  62. package/doc/zh/Command.md +78 -0
  63. package/doc/zh/ContainerdIntegration.md +9 -9
  64. package/doc/zh/Controllers.md +228 -0
  65. package/doc/zh/DBus.md +257 -0
  66. package/doc/zh/DTOValidation.md +49 -19
  67. package/doc/zh/Database.md +175 -0
  68. package/doc/zh/DatabaseDrivers.md +261 -0
  69. package/doc/zh/Decorator.md +25 -18
  70. package/doc/zh/DockerIntegration.md +6 -6
  71. package/doc/zh/Entities.md +224 -0
  72. package/doc/zh/Entrypoint.md +193 -0
  73. package/doc/zh/Exceptions.md +158 -0
  74. package/doc/zh/GRPC.md +226 -0
  75. package/doc/zh/GettingStarted.md +148 -0
  76. package/doc/zh/HTTP.md +445 -0
  77. package/doc/zh/HelperFunctions.md +2 -2
  78. package/doc/zh/I18n.md +26 -26
  79. package/doc/zh/Injection.md +129 -0
  80. package/doc/zh/Installers.md +232 -0
  81. package/doc/zh/Logger.md +188 -0
  82. package/doc/zh/MCP.md +265 -0
  83. package/doc/zh/Middlewares.md +198 -0
  84. package/doc/zh/Migrations.md +178 -0
  85. package/doc/zh/MongoEmbedded.md +118 -0
  86. package/doc/zh/Monitor.md +145 -0
  87. package/doc/zh/Mutex.md +10 -10
  88. package/doc/zh/NATS.md +207 -0
  89. package/doc/zh/NativeAddons.md +180 -0
  90. package/doc/zh/Objects.md +119 -0
  91. package/doc/zh/Overview.md +46 -71
  92. package/doc/zh/Packaging.md +154 -0
  93. package/doc/zh/ProjectStructure.md +102 -0
  94. package/doc/zh/Queue.md +214 -0
  95. package/doc/zh/Schedules.md +193 -0
  96. package/doc/zh/Security.md +6 -6
  97. package/doc/zh/ServiceCalls.md +459 -0
  98. package/doc/zh/Time.md +2 -2
  99. package/doc/zh/UpgradingFrom1.md +347 -0
  100. package/doc/zh/UpgradingFrom2.md +230 -0
  101. package/doc/zh/Workspaces.md +150 -0
  102. package/doc/zh/examples/BackgroundJobs.md +618 -0
  103. package/doc/zh/examples/CommandLineTool.md +618 -0
  104. package/doc/zh/examples/DesktopApp.md +748 -0
  105. package/doc/zh/examples/DeviceService.md +546 -0
  106. package/doc/zh/examples/MCPServer.md +604 -0
  107. package/doc/zh/examples/README.md +14 -0
  108. package/doc/zh/examples/RestAPI.md +458 -0
  109. package/doc/zh/examples/ServiceCalls.md +765 -0
  110. package/doc/zh/examples/WebApp.md +520 -0
  111. package/package.json +46 -17
  112. package/doc/en/ActionPattern.md +0 -408
  113. package/doc/en/Core.md +0 -312
  114. package/doc/en/ORMIntegration.md +0 -341
  115. package/doc/zh/ActionPattern.md +0 -334
  116. package/doc/zh/Core.md +0 -256
  117. package/doc/zh/ORMIntegration.md +0 -319
@@ -0,0 +1,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).