lakutata 3.0.0 → 3.1.1

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 (121) 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 +511 -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 +63 -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 +508 -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/WebRTC.md +1364 -0
  50. package/doc/en/Workspaces.md +189 -0
  51. package/doc/en/examples/BackgroundJobs.md +677 -0
  52. package/doc/en/examples/CommandLineTool.md +664 -0
  53. package/doc/en/examples/DesktopApp.md +809 -0
  54. package/doc/en/examples/DeviceService.md +607 -0
  55. package/doc/en/examples/MCPServer.md +653 -0
  56. package/doc/en/examples/Meeting.md +573 -0
  57. package/doc/en/examples/README.md +17 -0
  58. package/doc/en/examples/RestAPI.md +490 -0
  59. package/doc/en/examples/ServiceCalls.md +829 -0
  60. package/doc/en/examples/WebApp.md +568 -0
  61. package/doc/zh/Application.md +75 -0
  62. package/doc/zh/CLI.md +250 -0
  63. package/doc/zh/Cache.md +328 -0
  64. package/doc/zh/Command.md +78 -0
  65. package/doc/zh/ContainerdIntegration.md +9 -9
  66. package/doc/zh/Controllers.md +228 -0
  67. package/doc/zh/DBus.md +257 -0
  68. package/doc/zh/DTOValidation.md +49 -19
  69. package/doc/zh/Database.md +175 -0
  70. package/doc/zh/DatabaseDrivers.md +261 -0
  71. package/doc/zh/Decorator.md +25 -18
  72. package/doc/zh/DockerIntegration.md +6 -6
  73. package/doc/zh/Entities.md +224 -0
  74. package/doc/zh/Entrypoint.md +193 -0
  75. package/doc/zh/Exceptions.md +158 -0
  76. package/doc/zh/GRPC.md +226 -0
  77. package/doc/zh/GettingStarted.md +148 -0
  78. package/doc/zh/HTTP.md +446 -0
  79. package/doc/zh/HelperFunctions.md +2 -2
  80. package/doc/zh/I18n.md +26 -26
  81. package/doc/zh/Injection.md +129 -0
  82. package/doc/zh/Installers.md +232 -0
  83. package/doc/zh/Logger.md +188 -0
  84. package/doc/zh/MCP.md +265 -0
  85. package/doc/zh/Middlewares.md +198 -0
  86. package/doc/zh/Migrations.md +178 -0
  87. package/doc/zh/MongoEmbedded.md +118 -0
  88. package/doc/zh/Monitor.md +145 -0
  89. package/doc/zh/Mutex.md +10 -10
  90. package/doc/zh/NATS.md +207 -0
  91. package/doc/zh/NativeAddons.md +180 -0
  92. package/doc/zh/Objects.md +119 -0
  93. package/doc/zh/Overview.md +47 -71
  94. package/doc/zh/Packaging.md +154 -0
  95. package/doc/zh/ProjectStructure.md +102 -0
  96. package/doc/zh/Queue.md +214 -0
  97. package/doc/zh/Schedules.md +193 -0
  98. package/doc/zh/Security.md +6 -6
  99. package/doc/zh/ServiceCalls.md +462 -0
  100. package/doc/zh/Time.md +2 -2
  101. package/doc/zh/UpgradingFrom1.md +347 -0
  102. package/doc/zh/UpgradingFrom2.md +230 -0
  103. package/doc/zh/WebRTC.md +1049 -0
  104. package/doc/zh/Workspaces.md +150 -0
  105. package/doc/zh/examples/BackgroundJobs.md +618 -0
  106. package/doc/zh/examples/CommandLineTool.md +618 -0
  107. package/doc/zh/examples/DesktopApp.md +748 -0
  108. package/doc/zh/examples/DeviceService.md +546 -0
  109. package/doc/zh/examples/MCPServer.md +604 -0
  110. package/doc/zh/examples/Meeting.md +524 -0
  111. package/doc/zh/examples/README.md +15 -0
  112. package/doc/zh/examples/RestAPI.md +458 -0
  113. package/doc/zh/examples/ServiceCalls.md +765 -0
  114. package/doc/zh/examples/WebApp.md +520 -0
  115. package/package.json +46 -17
  116. package/doc/en/ActionPattern.md +0 -408
  117. package/doc/en/Core.md +0 -312
  118. package/doc/en/ORMIntegration.md +0 -341
  119. package/doc/zh/ActionPattern.md +0 -334
  120. package/doc/zh/Core.md +0 -256
  121. package/doc/zh/ORMIntegration.md +0 -319
package/doc/en/Core.md DELETED
@@ -1,312 +0,0 @@
1
- # Lakutata Core
2
-
3
- This guide covers the core of the framework (the package `@lakutata/core`, re-exported by `lakutata`,
4
- `lakutata/decorator/di`, `lakutata/decorator/dto` and `lakutata/decorator/asst`): the objects and their container, their
5
- lifetimes and scopes, their asynchronous construction, dependency injection, modules and the application lifecycle. The
6
- README of `@lakutata/core` and the typings of each export give the details.
7
-
8
- ## 1. Objects and the container
9
-
10
- Every object of an application is created by an IoC container, never with `new`. The class hierarchy:
11
-
12
- | Class | Role | Default lifetime |
13
- |---|---|---|
14
- | `BaseObject` | The root class: asynchronous construction, `init()`/`destroy()`, `getObject()` | `@Transient` |
15
- | `Provider` | A functional unit living in a scope, such as a request (a repository, a hasher) | `@Scoped` |
16
- | `Component` | A long-lived service shared by the application (a logger, a database, an entrypoint); an event emitter | `@Singleton` (locked) |
17
- | `Module` | A component owning its own components, providers and objects | `@Singleton` (locked) |
18
- | `Application` | The root module, created by `Application.run()` | |
19
- | `Controller` (`lakutata/com/entrypoint`) | A provider serving the requests of an entrypoint | `@Scoped` (locked) |
20
-
21
- ### Lifetimes
22
-
23
- | Decorator (`lakutata/decorator/di`) | One instance… |
24
- |---|---|
25
- | `@Singleton()` | per container registering the class, shared by its users and its scopes |
26
- | `@Scoped()` | per scope (`container.createScope()`; each request of an entrypoint is a scope, the root container is one too) |
27
- | `@Transient()` | per resolution, never cached |
28
-
29
- Subclasses inherit the lifetime. `Component` and `Module` lock theirs: a subclass declaring another one throws a
30
- `LifetimeLockedException`. A `Provider` is `@Scoped` by default and may be declared `@Transient` or `@Singleton` (as in
31
- 2.x: one instance shared by the scopes, such as the `ServiceProxy` of `@lakutata/nats`); a long-lived object owning
32
- resources (connections, subscriptions, servers) is rather a `Component`. A singleton injecting a scoped object keeps the
33
- instance of the scope which created it.
34
- `@Lifetime('SINGLETON' | 'SCOPED' | 'TRANSIENT', lock)` is the long form of the three decorators.
35
-
36
- ### Scopes
37
-
38
- A scope is a child container holding its own instances of the scoped objects, the singletons and registrations of its
39
- parent being shared. The entrypoint creates one per request: the controller of the request and the providers it injects
40
- are created in it and destroyed when the request ends. A component creates its own with its protected `createScope()`.
41
-
42
- ## 2. Asynchronous construction
43
-
44
- The container creates an object asynchronously, and the promise of `getObject()` (or `@Inject`) settles once the
45
- object is ready:
46
-
47
- 1. its `@Inject` properties are resolved;
48
- 2. its `@Configurable` properties get their configured values, validated (and converted) by their schemas; a property
49
- not configured keeps its initializer, else the default of its schema;
50
- 3. its `init()` runs (an error thrown here fails the creation);
51
- 4. its `startup()` runs, once it can be got by the other objects: the requesters wait for it (and fail with its error),
52
- except the objects got from within it, directly or through the objects they create, which get the object at once —
53
- so an object it gets may inject it (an entrypoint starts its adapters there, their hooks getting an authenticator
54
- which injects the entrypoint). It is named `startup` so that the classes with a `start()` method of their own (the
55
- containers of `@lakutata/docker`) are not started by the framework;
56
- 5. once the application has launched (and the object started), its `launched()` runs (not awaited; its errors are
57
- logged).
58
-
59
- At its teardown (`dispose()`, or the teardown of the object, scope or application that created it), once its
60
- `startup()` ended, its `beforeDestroy()` runs, then the objects it created are destroyed, then its `destroy()` runs. Each object owns a
61
- container: the objects it creates with `this.getObject()` are destroyed with it.
62
-
63
- ```typescript
64
- import {Application, Component, DTO, type ILogger} from 'lakutata'
65
- import {Configurable, Inject} from 'lakutata/decorator/di'
66
-
67
- class Mailer extends Component {
68
- //Its option, given by its declaration in the application options
69
- @Configurable(DTO.String().required())
70
- protected readonly host: string
71
-
72
- //25 unless configured: the initializer is the default, validated like a configured value
73
- @Configurable(DTO.Number().integer().min(1).max(65535))
74
- protected readonly port: number = 25
75
-
76
- //The logger preset by the lakutata package as the "log" component
77
- @Inject('log')
78
- protected readonly log: ILogger
79
-
80
- protected async init(): Promise<void> {
81
- this.log.info('Mailer connected to %s:%d', this.host, this.port)
82
- }
83
-
84
- public async send(to: string, text: string): Promise<void> {
85
- this.log.info('Mail to %s: %s', to, text)
86
- }
87
-
88
- protected async destroy(): Promise<void> {
89
- this.log.info('Mailer disconnected')
90
- }
91
- }
92
-
93
- Application
94
- .run(() => ({
95
- id: 'com.example.app',
96
- name: 'Example',
97
- components: {
98
- mailer: {class: Mailer, host: 'smtp.example.com'}
99
- },
100
- bootstrap: ['mailer']
101
- }))
102
- .onLaunched(async (app: Application): Promise<void> => {
103
- const mailer: Mailer = await app.getObject<Mailer>('mailer')
104
- await mailer.send('someone@example.com', 'Hello')
105
- })
106
- ```
107
-
108
- ## 3. Dependency injection
109
-
110
- `@Inject` (from `lakutata/decorator/di`) sets a property when the object is created, before `init()`:
111
-
112
- - `@Inject()`: the object registered under the property name;
113
- - `@Inject('name')` or `@Inject(symbol)`: the object registered under that name;
114
- - `@Inject(SomeClass)`: the object registered for the class (`@Inject(Application)` gives the application);
115
- - `@Inject('name', transform)`: the value returned by `transform(object)` (it may be async).
116
-
117
- A class marked `@Autoload()` is registered on its first request instead of failing: a singleton in the root container,
118
- a scoped class in the nearest scope, a transient class in the requesting container. A registration that cannot be
119
- resolved fails the creation with a `DependencyInjectionException`.
120
-
121
- `@Configurable(schema, fn?)` makes a property an option of the object: its value comes from the object's declaration
122
- (`{class: Mailer, host: 'smtp.example.com'}`) or from the second argument of `getObject()`, validated by the schema, then
123
- passed to `fn` when given. A module checks the options of the objects it declares when it initializes: an invalid value
124
- fails it with an `InvalidObjectOptionsException`, an option that is not a configurable property is warned of.
125
-
126
- ```typescript
127
- import {Application, Component, Provider} from 'lakutata'
128
- import {Autoload, Inject, Scoped} from 'lakutata/decorator/di'
129
-
130
- //Not declared in the application: registered the first time it is injected
131
- @Autoload()
132
- @Scoped()
133
- class RequestClock extends Provider {
134
- public readonly startedAt: number = Date.now()
135
- }
136
-
137
- class Reports extends Component {
138
- @Inject(RequestClock)
139
- protected readonly clock: RequestClock
140
-
141
- @Inject(Application)
142
- protected readonly app: Application
143
-
144
- public elapsed(): number {
145
- return Date.now() - this.clock.startedAt
146
- }
147
- }
148
- ```
149
-
150
- ## 4. Modules
151
-
152
- A `Module` declares its own components, providers and objects with the same options as the application, in the
153
- initializer of its `options` property. Its objects resolve their dependencies from the module first, then from the
154
- application; the application's objects do not see the module's. Declare a module in the `objects.named` of the
155
- application (or of a parent module), its options under `options` (merged over its initializer), and list it in
156
- `bootstrap` to start it at launch.
157
-
158
- When a module is created, its options are checked, its objects registered, then its `bootstrap` runs in order. An
159
- object outside the module getting it (`getObject()`, `@Inject`) receives it once these steps are complete, or receives
160
- the error of the step that failed; the module's own objects, created by these steps, receive it at once.
161
-
162
- ```typescript
163
- import {Application, Component, Module, type ModuleOptions} from 'lakutata'
164
-
165
- class Billing extends Component {
166
- public charge(amount: number): void {
167
- console.log(`Charged ${amount}`)
168
- }
169
- }
170
-
171
- class PaymentModule extends Module {
172
- protected options: ModuleOptions = {
173
- components: {
174
- billing: {class: Billing}
175
- },
176
- bootstrap: ['billing']
177
- }
178
- }
179
-
180
- Application.run(() => ({
181
- id: 'shop.app',
182
- name: 'Shop',
183
- objects: {
184
- named: {
185
- payment: {class: PaymentModule}
186
- }
187
- },
188
- bootstrap: ['payment']
189
- }))
190
- ```
191
-
192
- `module.reload()` destroys the module with its objects and initializes it again.
193
-
194
- ## 5. The application
195
-
196
- ### Options
197
-
198
- `Application.run(options)` (or `run(() => options)`, the getter called at launch, possibly async) takes:
199
-
200
- | Option | Meaning |
201
- |---|---|
202
- | `id`, `name` | Required. The ID (also the `process.title`) and the name of the application (`app.appId`, `app.appName`) |
203
- | `timezone` | An IANA time zone, or `'auto'` (default) for the system's; written to `TZ` |
204
- | `mode` | `'development'` or `'production'`, when the environment gives none (see below) |
205
- | `shutdownSignals` | The signals ending the application gracefully (default `['SIGTERM', 'SIGINT']`), or `false` |
206
- | `components`, `providers` | The components and providers by name: `{class, ...options}` |
207
- | `objects` | Other objects (modules, plain objects): `{named: {...}, anonymous: [...]}` |
208
- | `bootstrap` | The steps run in order at launch: a name, a symbol or a class (the object is created), or an async function called with the application |
209
- | `checkOptions` | Check the options of the declared objects at launch (default `true`) |
210
-
211
- A declaration without `class` completes the options of the object of the same name preset by the framework: the
212
- `lakutata` package presets `log` (the `Logger`) and `entrypoint` (the `Entrypoint`), so `log: {level: 'debug'}` sets the
213
- level of the preset logger. `Application.preset(options)` presets objects for every application of the process.
214
-
215
- ### The fluent API
216
-
217
- ```typescript
218
- import {Application, type ApplicationExitReason, type ILogger} from 'lakutata'
219
-
220
- Application
221
- //Environment variables, set at launch before the options getter runs
222
- .env({TZ_NAME: 'Asia/Shanghai'})
223
- //Path aliases: '@data' in a path given to path.resolve() or app.alias.resolve(); created when missing
224
- .alias({'@data': './data'}, true)
225
- .run(() => ({
226
- id: 'com.example.app',
227
- name: 'Example',
228
- components: {}
229
- }))
230
- //Called once the bootstrap completed (not awaited)
231
- .onLaunched((app: Application, log: ILogger): void => log.info('%s launched in %s mode', app.appName, app.mode()))
232
- //Called, awaited, when the application exits, before its objects are destroyed
233
- .onDone(async (app: Application, log: ILogger, reason: ApplicationExitReason): Promise<void> => {
234
- log.info('%s done (exit code %d%s)', app.appName, reason.code, reason.signal ? `, ${reason.signal}` : '')
235
- })
236
- //An uncaught exception of the process, the application keeps running
237
- .onUncaughtException((error: Error, log: ILogger): void => log.warn(error.message))
238
- //The launch failed: the returned number is the exit code (1 when none)
239
- .onFatalException((error: Error, log: ILogger): number => {
240
- log.error('Launch failed: %s', error.message)
241
- return 100
242
- })
243
- ```
244
-
245
- `app.exit(code)` ends the application gracefully with the exit code; `app.exit(true)` ends the process at once with the
246
- code 2, without the done handlers nor the teardown.
247
-
248
- ### Lifecycle
249
-
250
- 1. The environment variables are set and the aliases registered (`@runtime` is the working directory).
251
- 2. The options are read and validated; the ID, the time zone and the mode are applied.
252
- 3. The application is created in a new root container: its objects are registered and its `bootstrap` runs in order.
253
- 4. The `onLaunched` handlers and the `launched()` hooks of the objects run.
254
-
255
- - **Launch failure**: when the application cannot launch (invalid options, an object failing to initialize, a bootstrap
256
- step failing), the `onFatalException` handler gives the exit code (1 when it returns none). Without handler, the error
257
- is logged and the process exits with the code 1, as Node.js does on an uncaught exception, so the tools running it
258
- (Docker restart policies, Kubernetes, systemd, CI) know it failed; the objects already created (a listening server, a
259
- connection) do not keep it running.
260
- - **Shutdown signals**: on `SIGTERM` (`docker stop`, the rolling updates of Kubernetes, systemd) and `SIGINT` (Ctrl+C),
261
- the application ends gracefully: the `onDone` handlers run first, the application still working (their third
262
- argument tells why: `{code: 0, signal: 'SIGTERM'}`, or `{code}` for `app.exit(code)`), then the entrypoints stop
263
- accepting requests and let the running ones complete, the objects are destroyed, and the process exits with the code
264
- 0. A second signal (a shutdown hanging), or a signal received while the application launches, ends the process at
265
- once with the code 128 + the number of the signal (143 for `SIGTERM`, 130 for `SIGINT`). The option
266
- `shutdownSignals` gives other signals (`['SIGTERM', 'SIGINT', 'SIGHUP']`), or `false` to leave them to Node.js.
267
- - **The mode**: `app.mode()` gives `'development'` or `'production'`. The environment overrides the code, so the same
268
- build runs in each mode: the `MODE` environment variable, then `NODE_ENV`, then the `mode` option, `'development'`
269
- otherwise. A `MODE` that is not a mode is warned of and ignored; a `NODE_ENV` that is not a mode (`test`) is skipped.
270
- The mode is written back to `MODE` and `NODE_ENV`, so the libraries read the same mode. The variables come from the
271
- system, from `Application.env({MODE: 'production'})`, or from the `.env` file Bun reads (`bun App.js`; the
272
- executables built by Bun do not read it).
273
- - **The packed application**: `isPackaged()` (from `lakutata`) tells whether the application runs bundled
274
- (`lakutata package bundle`) or as an executable (`lakutata package exe`). The files it reads at run time (the built
275
- pages of a web application, templates, data) are declared by `lakutata.resources` in its `package.json`, as the
276
- `extraResources` of electron-builder: `[{"from": "../ui/dist", "to": "ui"}]`. Packed, `resourcesPath()` gives their
277
- directory; not packed, it gives `undefined` and the application finds them where they are built:
278
- `const ui = resourcesPath() ? path.join(resourcesPath()!, 'ui') : uiBuildDirectory`.
279
-
280
- ## 6. Entrypoints
281
-
282
- The `entrypoint` component (`lakutata/com/entrypoint`) serves the controllers over HTTP, CLI, service transports and
283
- schedules. Give its options with `buildEntrypoints({controllers, http, cli, service, schedule})` and list it in
284
- `bootstrap`; see [ActionPattern](ActionPattern.md).
285
-
286
- ## 7. A container without an application
287
-
288
- Tests, scripts and libraries can use the objects in a bare container:
289
-
290
- ```typescript
291
- import {Component, Container, DTO} from 'lakutata'
292
- import {Configurable} from 'lakutata/decorator/di'
293
-
294
- class Mailer extends Component {
295
- @Configurable(DTO.String().required())
296
- protected readonly host: string
297
-
298
- public async send(to: string, text: string): Promise<string> {
299
- return `${this.host} -> ${to}: ${text}`
300
- }
301
- }
302
-
303
- const container: Container = new Container()
304
- //Created with its options, its init() awaited
305
- const mailer: Mailer = await container.set(Mailer, {host: 'localhost'})
306
- await mailer.send('someone@example.com', 'Hello')
307
- //Destroys the objects it created
308
- await container.destroy()
309
- ```
310
-
311
- `container.register()`/`registerNamed()` register a class without creating it, `get()` resolves an object,
312
- `build()` creates a new one whatever its lifetime, `createScope()` creates a scope.
@@ -1,341 +0,0 @@
1
- # Lakutata ORM Integration
2
-
3
- The ORM of the framework (the package `@lakutata/orm`) is built on [TypeORM](https://typeorm.io) 0.3. The package
4
- `lakutata` re-exports it under four subpaths:
5
-
6
- | Subpath | What it provides |
7
- |---|---|
8
- | `lakutata/com/database` | The `Database` component (a connection shared by the application) and `buildDatabaseOptions` |
9
- | `lakutata/provider/database` | The `Database` provider (a connection of its own per resolution) and `buildDatabaseOptions` |
10
- | `lakutata/decorator/orm` | The TypeORM entity decorators (`@Entity`, `@Column`, the relations…) |
11
- | `lakutata/orm` | The TypeORM API: `Repository`, `EntityManager`, `QueryRunner`, the find operators, `BaseEntity`, the migrations, the errors |
12
-
13
- The README of `@lakutata/orm` gives the details; the TypeORM documentation describes the entities, the find options,
14
- the query builder and the migrations.
15
-
16
- ## 1. Installation
17
-
18
- TypeORM is a dependency of the package; the database driver is not. Install the driver of the `type` of the connection:
19
-
20
- | Database | `type` | Install |
21
- |---|---|---|
22
- | PostgreSQL, CockroachDB | `postgres`, `cockroachdb` | `pg` |
23
- | MySQL, MariaDB | `mysql`, `mariadb` | `mysql2` |
24
- | SQLite | `sqlite` / `better-sqlite3` | `sqlite3` / `better-sqlite3` |
25
- | SQLite in memory (WebAssembly) | `sqljs` | `sql.js` |
26
- | Microsoft SQL Server | `mssql` | `mssql` |
27
- | Oracle | `oracle` | `oracledb` |
28
- | MongoDB | `mongodb` | `mongodb` (5 or 6) |
29
-
30
- ```shell
31
- npm install lakutata pg
32
- ```
33
-
34
- ## 2. The Database component
35
-
36
- `buildDatabaseOptions` declares a `Database` component with the TypeORM connection options (`DataSourceOptions`, passed
37
- to TypeORM as they are). The component connects when it is created (at launch when it is listed in `bootstrap`; a
38
- connection failure stops the launch) and disconnects when the application stops. Other objects inject it by its name.
39
-
40
- ```typescript
41
- import {Application, Component} from 'lakutata'
42
- import {Inject} from 'lakutata/decorator/di'
43
- import {buildDatabaseOptions, Database} from 'lakutata/com/database'
44
- import {Column, CreateDateColumn, Entity, PrimaryGeneratedColumn} from 'lakutata/decorator/orm'
45
-
46
- @Entity('users')
47
- class User {
48
- @PrimaryGeneratedColumn()
49
- public id: number
50
-
51
- @Column({type: 'varchar', length: 100})
52
- public name: string
53
-
54
- @Column({unique: true})
55
- public email: string
56
-
57
- @CreateDateColumn({type: 'timestamptz'})
58
- public createdAt: Date
59
- }
60
-
61
- class Users extends Component {
62
- //The component registered as "db"
63
- @Inject('db')
64
- protected readonly db: Database
65
-
66
- public async register(name: string, email: string): Promise<User> {
67
- return await this.db.getRepository(User).save({name: name, email: email})
68
- }
69
-
70
- public async find(id: number): Promise<User | null> {
71
- return await this.db.getRepository(User).findOneBy({id: id})
72
- }
73
-
74
- public async rename(id: number, name: string): Promise<void> {
75
- await this.db.getRepository(User).update({id: id}, {name: name})
76
- }
77
-
78
- public async remove(id: number): Promise<boolean> {
79
- const result = await this.db.getRepository(User).delete({id: id})
80
- return !!result.affected
81
- }
82
- }
83
-
84
- Application
85
- .run(() => ({
86
- id: 'com.example.users',
87
- name: 'Users',
88
- components: {
89
- db: buildDatabaseOptions({
90
- type: 'postgres',
91
- host: process.env.DB_HOST ?? 'localhost',
92
- port: 5432,
93
- username: 'postgres',
94
- password: process.env.DB_PASSWORD,
95
- database: 'users',
96
- entities: [User],
97
- //Development only: the schema follows the entities; the migrations change it in production
98
- synchronize: true
99
- }),
100
- users: {class: Users}
101
- },
102
- bootstrap: ['db']
103
- }))
104
- .onLaunched(async (app: Application): Promise<void> => {
105
- const users: Users = await app.getObject<Users>('users')
106
- await users.register('Ada', 'ada@example.com')
107
- })
108
- ```
109
-
110
- The common connection options:
111
-
112
- | Option | What it sets |
113
- |---|---|
114
- | `type` | The driver: `'postgres'`, `'mysql'`, `'sqlite'`… |
115
- | `url` or `host`, `port`, `username`, `password`, `database` | The server and the database |
116
- | `entities` | The entity classes, `EntitySchema`s, or glob paths of their compiled files (`'dist/entities/*.js'`) |
117
- | `synchronize` | Update the schema from the entities on each connection (default `false`): development only |
118
- | `migrations`, `migrationsRun`, `migrationsTransactionMode` | The migrations, whether they run on each connection (default `false`), their transactions (default `'all'`) |
119
- | `logging`, `logger` | What TypeORM logs and where |
120
- | `poolSize`, `extra` | The connection pool size; the options passed to the driver |
121
- | `subscribers`, `namingStrategy`, `cache` | The entity subscribers, the naming of the tables and columns, the query result cache |
122
-
123
- The component gives the TypeORM API of a data source: `getRepository()`, `getTreeRepository()`,
124
- `getMongoRepository()`, `manager`, `transaction()`, `query()`, `createQueryBuilder()`, `createQueryRunner()`,
125
- `synchronize()`, `runMigrations()`, `undoLastMigration()`, `showMigrations()`.
126
-
127
- ## 3. Entities
128
-
129
- The decorators of `lakutata/decorator/orm` are the ones of TypeORM (see [Decorator](Decorator.md) for the list):
130
-
131
- ```typescript
132
- import {
133
- BeforeInsert, Column, CreateDateColumn, DeleteDateColumn, Entity, Index, JoinColumn, ManyToOne, OneToMany,
134
- PrimaryGeneratedColumn, UpdateDateColumn, VersionColumn
135
- } from 'lakutata/decorator/orm'
136
-
137
- @Entity('orders')
138
- export class Order {
139
- @PrimaryGeneratedColumn('uuid')
140
- public id: string
141
-
142
- @Index()
143
- @Column()
144
- public customer: string
145
-
146
- //Saved with the order
147
- @OneToMany(() => OrderLine, (line: OrderLine) => line.order, {cascade: true})
148
- public lines: OrderLine[]
149
-
150
- @CreateDateColumn({type: 'timestamptz'})
151
- public createdAt: Date
152
-
153
- @UpdateDateColumn({type: 'timestamptz'})
154
- public updatedAt: Date
155
-
156
- //Set by softDelete(): the finds skip the order unless withDeleted is set
157
- @DeleteDateColumn({type: 'timestamptz'})
158
- public deletedAt: Date | null
159
-
160
- //Incremented by each save
161
- @VersionColumn()
162
- public version: number
163
-
164
- @BeforeInsert()
165
- protected normalize(): void {
166
- this.customer = this.customer.trim()
167
- }
168
- }
169
-
170
- @Entity('order_lines')
171
- export class OrderLine {
172
- @PrimaryGeneratedColumn()
173
- public id: number
174
-
175
- @Column()
176
- public product: string
177
-
178
- @Column('integer')
179
- public quantity: number
180
-
181
- @ManyToOne(() => Order, (order: Order) => order.lines, {onDelete: 'CASCADE'})
182
- @JoinColumn({name: 'order_id'})
183
- public order: Order
184
- }
185
- ```
186
-
187
- An entity may also extend `BaseEntity` (from `lakutata/orm`) for the active record operations (`User.findOneBy()`,
188
- `user.save()`): they use the last connection that registered the entity.
189
-
190
- ## 4. Queries
191
-
192
- `lakutata/orm` exports the find operators (`In`, `ILike`, `Between`, `MoreThan`, `IsNull`, `Not`, `Raw`…), the
193
- repositories and the query builders.
194
-
195
- ```typescript
196
- import {Component} from 'lakutata'
197
- import {Inject} from 'lakutata/decorator/di'
198
- import {Database} from 'lakutata/com/database'
199
- import {Between, ILike, type Repository} from 'lakutata/orm'
200
-
201
- class Orders extends Component {
202
- @Inject('db')
203
- protected readonly db: Database
204
-
205
- protected get orders(): Repository<Order> {
206
- return this.db.getRepository(Order)
207
- }
208
-
209
- //A page of the orders of a period, and their count
210
- public async search(customer: string, from: Date, to: Date, page: number): Promise<[Order[], number]> {
211
- return await this.orders.findAndCount({
212
- where: {customer: ILike(`%${customer}%`), createdAt: Between(from, to)},
213
- order: {createdAt: 'DESC'},
214
- skip: (page - 1) * 20,
215
- take: 20
216
- })
217
- }
218
-
219
- //A query builder
220
- public async findByCustomer(customer: string): Promise<Order[]> {
221
- return await this.db.createQueryBuilder(Order, 'order')
222
- .where('order.customer LIKE :customer', {customer: `%${customer}%`})
223
- .orderBy('order.createdAt', 'DESC')
224
- .getMany()
225
- }
226
-
227
- //Raw SQL: the PostgreSQL placeholders are $1, $2…
228
- public async count(customer: string): Promise<number> {
229
- const rows: { count: string }[] = await this.db.query('SELECT COUNT(*) AS count FROM orders WHERE customer = $1', [customer])
230
- return Number(rows[0].count)
231
- }
232
- }
233
- ```
234
-
235
- ## 5. Transactions
236
-
237
- `transaction([isolationLevel], fn)` runs a function in a transaction: committed when it resolves, rolled back when it
238
- rejects (its error rethrown). The operations of the transaction go through the entity manager given to the function;
239
- the ones made through `db.manager` or `db.getRepository()` run outside of it. `createQueryRunner()` controls a
240
- transaction by hand.
241
-
242
- ```typescript
243
- import {Component} from 'lakutata'
244
- import {Inject} from 'lakutata/decorator/di'
245
- import {Database} from 'lakutata/com/database'
246
- import {type EntityManager} from 'lakutata/orm'
247
-
248
- class Signup extends Component {
249
- @Inject('db')
250
- protected readonly db: Database
251
-
252
- //Both saves, or none
253
- public async signup(name: string, email: string, product: string): Promise<User> {
254
- return await this.db.transaction('SERIALIZABLE', async (manager: EntityManager): Promise<User> => {
255
- const user: User = await manager.save(User, {name: name, email: email})
256
- await manager.save(Order, {customer: email, lines: [{product: product, quantity: 1}]})
257
- return user
258
- })
259
- }
260
- }
261
- ```
262
-
263
- ## 6. The Database provider
264
-
265
- The provider of `lakutata/provider/database` has the API of the component, but it is transient: each resolution
266
- (`@Inject`, `getObject()`) creates a new instance, which opens its own connection pool and closes it when it is
267
- destroyed with the object or the container that created it. It suits a short use (a job, a script, a tenant
268
- database); the requests of an application share the component. Its `entities` option is added to the entities of its
269
- connection options.
270
-
271
- ```typescript
272
- import {Application} from 'lakutata'
273
- import {buildDatabaseOptions, Database} from 'lakutata/provider/database'
274
-
275
- Application
276
- .run(() => ({
277
- id: 'com.example.reports',
278
- name: 'Reports',
279
- providers: {
280
- reports: {
281
- ...buildDatabaseOptions({type: 'postgres', url: process.env.REPORTS_URL}),
282
- entities: [Report]
283
- }
284
- }
285
- }))
286
- .onLaunched(async (app: Application): Promise<void> => {
287
- //A connection opened here, closed with the application
288
- const database: Database = await app.getObject<Database>('reports')
289
- console.log(await database.getRepository(Report).count())
290
- })
291
- ```
292
-
293
- ## 7. Several databases
294
-
295
- Each database is a component of its own name, injected by that name:
296
-
297
- ```typescript
298
- Application.run(() => ({
299
- id: 'com.example.shop',
300
- name: 'Shop',
301
- components: {
302
- db: buildDatabaseOptions({type: 'postgres', url: process.env.DATABASE_URL, entities: [User, Order, OrderLine]}),
303
- analytics: buildDatabaseOptions({type: 'mysql', url: process.env.ANALYTICS_URL, entities: [Visit]})
304
- },
305
- bootstrap: ['db', 'analytics']
306
- }))
307
- ```
308
-
309
- ## 8. Migrations
310
-
311
- A migration is a class implementing `MigrationInterface` (from `lakutata/orm`): its `up()` changes the schema, its
312
- `down()` reverts the change. The `migrations` connection option lists them. They run when the database connects with
313
- `migrationsRun: true`, by `runMigrations()` and `undoLastMigration()` of the component or provider, or by the commands
314
- of the application.
315
-
316
- The commands are environment variables given to the application itself: started with one of them, the application
317
- reads its options, then, instead of its `bootstrap` and its `onLaunched` handlers, does the command on each `Database`
318
- component and provider of its options, and exits (code 0, or 1 when the command fails).
319
-
320
- | Variable | Command |
321
- |---|---|
322
- | `MIGRATION_GENERATE=<directory>` | Write the SQL of the differences between the entities and the schema as a migration in the directory |
323
- | `MIGRATION_RUN=1` | Run the pending migrations |
324
- | `MIGRATION_REVERT=1` | Revert the last migration run (`MIGRATION_DATABASE` required with several databases) |
325
- | `MIGRATION_SHOW=1` | Print the migrations run and pending |
326
- | `MIGRATION_DATABASE=<name>` | The database of the command (all by default) |
327
- | `MIGRATION_TRANSACTION=all\|each\|none` | The transactions of `MIGRATION_RUN` and `MIGRATION_REVERT` |
328
- | `MIGRATION_FAKE=1` | Record the migrations as run (or reverted) without running them |
329
-
330
- ```shell
331
- MIGRATION_GENERATE=src/migrations node dist/App.js
332
- MIGRATION_RUN=1 node dist/App.js
333
- ```
334
-
335
- ## 9. Notes
336
-
337
- - Set `synchronize` to `false` in production and change the schema with migrations.
338
- - `pg` reads the `numeric` and `bigint` columns back as strings: convert them with a column `transformer`.
339
- - The errors are the ones of TypeORM, exported by `lakutata/orm`: `QueryFailedError` (its `driverError` holds the
340
- driver error), `EntityNotFoundError` (`findOneByOrFail()`), `EntityMetadataNotFoundError` (an entity not in the
341
- `entities` of the connection)…