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.
- 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 +511 -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 +63 -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 +508 -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/WebRTC.md +1364 -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/Meeting.md +573 -0
- package/doc/en/examples/README.md +17 -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 +446 -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 +47 -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 +462 -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/WebRTC.md +1049 -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/Meeting.md +524 -0
- package/doc/zh/examples/README.md +15 -0
- package/doc/zh/examples/RestAPI.md +458 -0
- package/doc/zh/examples/ServiceCalls.md +765 -0
- package/doc/zh/examples/WebApp.md +520 -0
- package/package.json +46 -17
- package/doc/en/ActionPattern.md +0 -408
- package/doc/en/Core.md +0 -312
- package/doc/en/ORMIntegration.md +0 -341
- package/doc/zh/ActionPattern.md +0 -334
- package/doc/zh/Core.md +0 -256
- package/doc/zh/ORMIntegration.md +0 -319
package/doc/en/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.
|
package/doc/en/ORMIntegration.md
DELETED
|
@@ -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)…
|