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
@@ -23,8 +23,9 @@ remote host.
23
23
  | TCP, HTTPS | `host`, `port`, `ca`, `cert`, `key` as PEM contents (`buildDockerHttpsConnectionOptions`) |
24
24
  | SSH | `host`, `port`, `username`, `sshAuthAgent` (`buildDockerSSHConnectionOptions`) |
25
25
 
26
- When it is created, the component pings the daemon and negotiates the API version: an unreachable daemon fails the
27
- launch with `DockerConnectionException`. `timeout` (milliseconds) is the time a request may stay idle.
26
+ When it is created, the component pings the daemon and negotiates the API version: an unreachable daemon makes its
27
+ creation fail with `DockerConnectionException`; when the component is listed in `bootstrap`, the application then fails
28
+ to start. `timeout` (milliseconds) is how long a request may stay idle.
28
29
 
29
30
  ```typescript
30
31
  import {readFileSync} from 'node:fs'
@@ -185,13 +186,13 @@ async function containers(docker: Docker): Promise<void> {
185
186
 
186
187
  - A `DockerContainer` is a snapshot: its operations refresh it; the changes made by others are seen by getting it
187
188
  again (`docker.getContainer(id)`, which also takes a name or a short ID).
188
- - Starting a running container or stopping a stopped one does nothing, as the Docker CLI.
189
- - `exec()` runs without TTY nor stdin. An array command runs as it is; a string is a command line, run by
189
+ - Starting a running container, or stopping a stopped one, does nothing, as with the Docker CLI.
190
+ - `exec()` runs without a TTY or stdin. An array command runs as it is; a string is a command line, run by
190
191
  `/bin/sh -c`.
191
192
  - `kill()` sends `SIGKILL` by default, or the signal given.
192
193
  - `commit()` makes an image of a container, `exportDirectory()` writes a directory of a container as a tar archive.
193
- - Not provided: attaching to the main process, copying files into a container, the volumes (other than by `prune()`)
194
- and the daemon's events.
194
+ - Not provided: attaching to the main process, copying files into a container, managing the volumes (other than pruning
195
+ them) and the events of the daemon.
195
196
 
196
197
  ## 5. Networks
197
198
 
@@ -0,0 +1,240 @@
1
+ # Lakutata Entities, Queries and Transactions
2
+
3
+ The entities, the repositories, the queries and the transactions are the ones of [TypeORM](https://typeorm.io) 1: the
4
+ decorators come from `lakutata/decorator/orm`, the rest of the API from `lakutata/orm`. This guide shows the common
5
+ uses; the documentation of TypeORM describes every option.
6
+
7
+ ## 1. Entities
8
+
9
+ An entity is a class mapped to a table:
10
+
11
+ ```typescript
12
+ import {
13
+ BeforeInsert, Column, CreateDateColumn, DeleteDateColumn, Entity, Index, JoinColumn, ManyToOne, OneToMany,
14
+ PrimaryGeneratedColumn, UpdateDateColumn, VersionColumn
15
+ } from 'lakutata/decorator/orm'
16
+
17
+ @Entity('orders')
18
+ export class Order {
19
+ @PrimaryGeneratedColumn('uuid')
20
+ public id: string
21
+
22
+ @Index()
23
+ @Column()
24
+ public customer: string
25
+
26
+ //Saved with the order (cascade)
27
+ @OneToMany(() => OrderLine, (line: OrderLine) => line.order, {cascade: true})
28
+ public lines: OrderLine[]
29
+
30
+ @CreateDateColumn({type: 'timestamptz'})
31
+ public createdAt: Date
32
+
33
+ @UpdateDateColumn({type: 'timestamptz'})
34
+ public updatedAt: Date
35
+
36
+ //Set by softDelete(): the finds skip the order unless withDeleted is set
37
+ @DeleteDateColumn({type: 'timestamptz'})
38
+ public deletedAt: Date | null
39
+
40
+ //Incremented by each save
41
+ @VersionColumn()
42
+ public version: number
43
+
44
+ @BeforeInsert()
45
+ protected normalize(): void {
46
+ this.customer = this.customer.trim()
47
+ }
48
+ }
49
+
50
+ @Entity('order_lines')
51
+ export class OrderLine {
52
+ @PrimaryGeneratedColumn()
53
+ public id: number
54
+
55
+ @Column()
56
+ public product: string
57
+
58
+ @Column('integer')
59
+ public quantity: number
60
+
61
+ @ManyToOne(() => Order, (order: Order) => order.lines, {onDelete: 'CASCADE'})
62
+ @JoinColumn({name: 'order_id'})
63
+ public order: Order
64
+ }
65
+ ```
66
+
67
+ | Decorators | Use |
68
+ |---|---|
69
+ | `@Entity(name)`, `@ViewEntity`, `@ChildEntity`, `@TableInheritance` | An entity, a view, the inheritance of tables |
70
+ | `@PrimaryGeneratedColumn(strategy)`, `@PrimaryColumn` | The primary key: generated (`'increment'`, `'uuid'`, `'identity'`), or given |
71
+ | `@Column(type, options)` | A column: its type, `length`, `nullable`, `default`, `unique`, `transformer`… |
72
+ | `@CreateDateColumn`, `@UpdateDateColumn`, `@DeleteDateColumn`, `@VersionColumn` | Columns set by TypeORM |
73
+ | `@OneToOne`, `@OneToMany`, `@ManyToOne`, `@ManyToMany`, `@JoinColumn`, `@JoinTable` | The relations |
74
+ | `@Index`, `@Unique`, `@Check` | Indices and constraints |
75
+ | `@BeforeInsert`, `@AfterLoad`, `@BeforeUpdate`… | Methods run around the operations |
76
+ | `@ObjectIdColumn` | The `_id` of the entities of MongoDB |
77
+
78
+ An entity is listed in the `entities` option of each database that uses it. An entity may also extend `BaseEntity` (from
79
+ `lakutata/orm`) to save and find itself (`await Order.findOneBy({id})`, `await order.save()`): these use the last
80
+ database connected with the entity.
81
+
82
+ ## 2. Queries
83
+
84
+ A repository (`db.getRepository(Order)`) reads and writes the entities of a table:
85
+
86
+ ```typescript
87
+ import {Component} from 'lakutata'
88
+ import {Inject} from 'lakutata/decorator/di'
89
+ import {Database} from 'lakutata/com/database'
90
+ import {Column, CreateDateColumn, Entity, PrimaryGeneratedColumn} from 'lakutata/decorator/orm'
91
+ import {Between, ILike, type Repository} from 'lakutata/orm'
92
+
93
+ @Entity('orders')
94
+ class Order {
95
+ @PrimaryGeneratedColumn('uuid')
96
+ public id: string
97
+
98
+ @Column()
99
+ public customer: string
100
+
101
+ @CreateDateColumn({type: 'timestamptz'})
102
+ public createdAt: Date
103
+ }
104
+
105
+ class Orders extends Component {
106
+ @Inject('db')
107
+ protected readonly db: Database
108
+
109
+ protected get orders(): Repository<Order> {
110
+ return this.db.getRepository(Order)
111
+ }
112
+
113
+ //A page of the orders of a period, and the count of all of them
114
+ public async search(customer: string, from: Date, to: Date, page: number): Promise<[Order[], number]> {
115
+ return await this.orders.findAndCount({
116
+ where: {customer: ILike(`%${customer}%`), createdAt: Between(from, to)},
117
+ order: {createdAt: 'DESC'},
118
+ skip: (page - 1) * 20,
119
+ take: 20
120
+ })
121
+ }
122
+
123
+ //A query builder
124
+ public async byCustomer(customer: string): Promise<Order[]> {
125
+ return await this.db.createQueryBuilder(Order, 'order')
126
+ .where('order.customer LIKE :customer', {customer: `%${customer}%`})
127
+ .orderBy('order.createdAt', 'DESC')
128
+ .getMany()
129
+ }
130
+
131
+ //SQL: the placeholders are $1, $2… on PostgreSQL, ? on MySQL and SQLite
132
+ public async count(customer: string): Promise<number> {
133
+ const rows: { count: string }[] = await this.db.query('SELECT COUNT(*) AS count FROM orders WHERE customer = $1', [customer])
134
+ return Number(rows[0].count)
135
+ }
136
+ }
137
+ ```
138
+
139
+ - **Finding:** `find`, `findBy`, `findOne`, `findOneBy`, `findAndCount`, `count`, `exists`; `findOneByOrFail` throws
140
+ an `EntityNotFoundError` when nothing is found.
141
+ - **The find options** take objects: `where`, `relations: {lines: true}`, `select: {id: true, customer: true}`,
142
+ `order: {createdAt: 'DESC'}`, `skip` and `take`, `withDeleted`. A list of conditions in `where` is an `OR`.
143
+ - **The find operators** of `lakutata/orm`: `In`, `Not`, `IsNull`, `Like`, `ILike`, `Between`, `MoreThan`,
144
+ `MoreThanOrEqual`, `LessThan`, `LessThanOrEqual`, `Any`, `ArrayContains`, `Raw`…
145
+ - **Writing:** `save` (inserts or updates, with the cascades of the relations), `insert`, `update`, `delete`,
146
+ `softDelete` and `restore` (with a `@DeleteDateColumn`), `increment`, `upsert`.
147
+ - **A query builder** (`createQueryBuilder`) writes the queries the find options cannot: joins with conditions, groups,
148
+ subqueries. Its values (`:customer`) are passed as parameters, never put into the SQL text.
149
+ - **SQL** (`query(sql, parameters)`) returns the rows of the driver as they are: a `COUNT(*)` of PostgreSQL is a string.
150
+
151
+ ## 3. Transactions
152
+
153
+ `transaction(fn)` runs a function in a transaction: committed when the function resolves, rolled back when it
154
+ rejects, its error thrown again. Every operation of the transaction goes through the entity manager given to the
155
+ function; the ones made through `db.manager` or `db.getRepository()` run outside of it.
156
+
157
+ ```typescript
158
+ import {Component} from 'lakutata'
159
+ import {Inject} from 'lakutata/decorator/di'
160
+ import {Database} from 'lakutata/com/database'
161
+ import {Column, Entity, PrimaryGeneratedColumn} from 'lakutata/decorator/orm'
162
+ import {type EntityManager} from 'lakutata/orm'
163
+
164
+ @Entity('accounts')
165
+ class Account {
166
+ @PrimaryGeneratedColumn()
167
+ public id: number
168
+
169
+ @Column('integer')
170
+ public balance: number
171
+ }
172
+
173
+ class Transfers extends Component {
174
+ @Inject('db')
175
+ protected readonly db: Database
176
+
177
+ //Both updates, or none
178
+ public async transfer(from: number, to: number, amount: number): Promise<void> {
179
+ await this.db.transaction('SERIALIZABLE', async (manager: EntityManager): Promise<void> => {
180
+ await manager.decrement(Account, {id: from}, 'balance', amount)
181
+ await manager.increment(Account, {id: to}, 'balance', amount)
182
+ })
183
+ }
184
+ }
185
+ ```
186
+
187
+ - The isolation level is optional: `'READ UNCOMMITTED'`, `'READ COMMITTED'`, `'REPEATABLE READ'`, `'SERIALIZABLE'`.
188
+ - `createQueryRunner()` gives a connection of its own, to control a transaction by hand (`startTransaction()`,
189
+ `commitTransaction()`, `rollbackTransaction()`, then `release()`).
190
+ - MongoDB and the embedded MongoDB have no transactions: there, `transaction()` runs the function without one.
191
+
192
+ ## 4. Values to watch
193
+
194
+ - **Big numbers.** PostgreSQL and MySQL return the `bigint` and `decimal` columns as strings, to keep their precision.
195
+ SQLite returns them as numbers, which lose precision beyond 2^53. A column `transformer` converts them where a
196
+ number is enough:
197
+
198
+ ```typescript
199
+ import {Column, Entity, PrimaryGeneratedColumn} from 'lakutata/decorator/orm'
200
+ import type {ValueTransformer} from 'lakutata/orm'
201
+
202
+ const decimal: ValueTransformer = {
203
+ to: (value: number): number => value,
204
+ from: (value: string | null): number | null => value === null ? null : Number(value)
205
+ }
206
+
207
+ @Entity('invoices')
208
+ export class Invoice {
209
+ @PrimaryGeneratedColumn()
210
+ public id: number
211
+
212
+ @Column({type: 'decimal', precision: 10, scale: 2, transformer: decimal})
213
+ public total: number
214
+ }
215
+ ```
216
+
217
+ - **`null` and `undefined` in the conditions** are left out of the condition by default: `findOneBy({id: undefined})`
218
+ finds the first row. See [Database](Database.md), section 2.
219
+ - **Dates.** Prefer the columns with a time zone (`timestamptz` on PostgreSQL). On MySQL, the `timezone` option of the
220
+ connection tells how the dates are written and read.
221
+
222
+ ## 5. Errors
223
+
224
+ The errors are the ones of TypeORM, exported by `lakutata/orm`:
225
+
226
+ | Error | When |
227
+ |---|---|
228
+ | `QueryFailedError` | The database refused a query. Its `driverError` is the error of the driver: on PostgreSQL, the code `23505` is a duplicate key, its `constraint` the name of the constraint |
229
+ | `EntityNotFoundError` | `findOneByOrFail()` and `findOneOrFail()` found nothing |
230
+ | `EntityMetadataNotFoundError` | An entity that is not in the `entities` of the database |
231
+ | `OptimisticLockVersionMismatchError` | A `@VersionColumn` did not match the version expected by a lock |
232
+ | `CannotExecuteNotConnectedError` | A query on a database that is not connected, or no longer is |
233
+
234
+ ```typescript
235
+ import {QueryFailedError} from 'lakutata/orm'
236
+
237
+ export function isDuplicate(error: unknown): boolean {
238
+ return error instanceof QueryFailedError && (error.driverError as { code?: string }).code === '23505'
239
+ }
240
+ ```
@@ -0,0 +1,216 @@
1
+ # Lakutata Entrypoint
2
+
3
+ The `entrypoint` component connects the controllers to the transports. It holds the controllers and the adapters of
4
+ the transports (HTTP, command line, service calls, schedules, queues…): each adapter receives the requests of its
5
+ transport, and the entrypoint runs the matching action, with its middlewares, its validation and its access control.
6
+
7
+ ## 1. Declaring the entrypoint
8
+
9
+ The `lakutata` package already declares a component named `entrypoint`. An application gives it its options with
10
+ `buildEntrypoints`, under the same name, and lists it in `bootstrap`:
11
+
12
+ ```typescript
13
+ import {Application} from 'lakutata'
14
+ import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
15
+ import {GET} from 'lakutata/decorator/ctrl'
16
+ import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
17
+
18
+ class HealthController extends Controller {
19
+ @GET('/health')
20
+ public async health(): Promise<string> {
21
+ return 'ok'
22
+ }
23
+ }
24
+
25
+ Application.run(() => ({
26
+ id: 'api.app',
27
+ name: 'API',
28
+ components: {
29
+ entrypoint: buildEntrypoints({
30
+ controllers: [HealthController],
31
+ http: expressHTTPEntrypoint({port: 8080})
32
+ })
33
+ },
34
+ bootstrap: ['entrypoint']
35
+ }))
36
+ ```
37
+
38
+ A component is created the first time something needs it. No other object needs the entrypoint, so without
39
+ `bootstrap: ['entrypoint']` it is never created: no server listens, no command runs, no job starts.
40
+
41
+ ## 2. Options
42
+
43
+ | Option | Default | Meaning |
44
+ |---|---|---|
45
+ | `controllers` | `[]` | The controller classes |
46
+ | `http`, `cli`, `service`, `schedule`, `queue`, `mcp`, `dbus` | | The adapters of the transports (below): one adapter, or an array of them |
47
+ | `middlewares` | `[]` | The middlewares of every action (see [Middlewares](Middlewares.md)) |
48
+ | `rules` | `[]` | The access rules of the actions declared with `acl: true` (see [Middlewares](Middlewares.md)) |
49
+ | `shutdownTimeout` | `30000` | The milliseconds given to the requests in progress when the application stops (section 4) |
50
+ | `httpActionGroups`, `cliActionGroups`, `serviceActionGroups` | `{}` | The groups the actions may use, with their names (section 5) |
51
+
52
+ The adapters:
53
+
54
+ | Option | Adapter | Imported from | To install | Guide |
55
+ |---|---|---|---|---|
56
+ | `http` | `expressHTTPEntrypoint()` | `lakutata/adapter/express` | `express` | [HTTP](HTTP.md) |
57
+ | `http` | `fastifyHTTPEntrypoint()` | `lakutata/adapter/fastify` | `fastify` | [HTTP](HTTP.md) |
58
+ | `cli` | `commanderCLIEntrypoint()` | `lakutata/adapter/commander` | `commander` | [CLI](CLI.md) |
59
+ | `service` | `httpServiceEntrypoint()` | `lakutata/com/entrypoint` | | [ServiceCalls](ServiceCalls.md) |
60
+ | `service` | `socketIOServiceEntrypoint()` | `lakutata/adapter/socket.io` | `socket.io` | [ServiceCalls](ServiceCalls.md) |
61
+ | `service` | `jsonRPCServiceEntrypoint()` | `lakutata/adapter/jsonrpc` | `ws`, for WebSocket | [ServiceCalls](ServiceCalls.md) |
62
+ | `service` | `electronIPCServiceEntrypoint()` | `lakutata/adapter/electron` | (Electron) | [ServiceCalls](ServiceCalls.md) |
63
+ | `service` | `grpcServiceEntrypoint()` | `@lakutata/grpc/service` | `@lakutata/grpc` | [GRPC](GRPC.md) |
64
+ | `service` | `natsServiceEntrypoint()` | `@lakutata/nats` | `@lakutata/nats` | [NATS](NATS.md) |
65
+ | `schedule` | `scheduleEntrypoint()` | `lakutata/com/entrypoint` | | [Schedules](Schedules.md) |
66
+ | `queue` | `queueEntrypoint()` | `lakutata/com/queue` | The client of the queue server | [Queue](Queue.md) |
67
+ | `mcp` | `httpMCPEntrypoint()`, `stdioMCPEntrypoint()` | `@lakutata/mcp` | `@lakutata/mcp` | [MCP](MCP.md) |
68
+ | `dbus` | `dbusEntrypoint()` | `@lakutata/dbus` | `@lakutata/dbus` | [DBus](DBus.md) |
69
+
70
+ ## 3. Several adapters
71
+
72
+ Each option accepts an array, to serve one transport in several ways. For example, the service actions over Socket.IO
73
+ for the browsers and over JSON-RPC for the scripts, or the HTTP actions on two ports:
74
+
75
+ ```typescript
76
+ import {buildEntrypoints} from 'lakutata/com/entrypoint'
77
+ import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
78
+ import {jsonRPCServiceEntrypoint} from 'lakutata/adapter/jsonrpc'
79
+ import {socketIOServiceEntrypoint} from 'lakutata/adapter/socket.io'
80
+
81
+ export const entrypoint = buildEntrypoints({
82
+ controllers: [],
83
+ http: [
84
+ expressHTTPEntrypoint({name: 'public', port: 8080}),
85
+ expressHTTPEntrypoint({name: 'internal', host: '127.0.0.1', port: 9090})
86
+ ],
87
+ service: [
88
+ socketIOServiceEntrypoint({port: 3000}),
89
+ jsonRPCServiceEntrypoint({port: 4000, path: '/rpc'})
90
+ ]
91
+ })
92
+ ```
93
+
94
+ Every adapter of a transport serves all the actions of that transport. The `name` option tells the adapters apart.
95
+
96
+ ## 4. Starting and stopping
97
+
98
+ **When the application launches**, the entrypoint first checks the controllers. Each of these mistakes stops the
99
+ launch with its exception:
100
+
101
+ - two actions of the same transport with the same name (`DuplicateActionNameException`);
102
+ - an action in a group that its `*ActionGroups` option does not declare (`InvalidActionGroupException`);
103
+ - a prefix that conflicts with the pattern of an action (`InvalidPrefixException`);
104
+ - a middleware that is neither a function nor a `Middleware` class (`InvalidMiddlewareException`);
105
+ - an adapter given under the key of another transport, or an option that is neither an option of the entrypoint nor a
106
+ transport (`InvalidEntrypointAdapterException`).
107
+
108
+ Then the adapters start one after another, in the order of the options. One that fails, such as a server whose port is
109
+ already in use, stops the launch. The command line and the scheduled jobs begin once the whole application has
110
+ launched.
111
+
112
+ **When the application stops** (on `SIGTERM` or `SIGINT`, or after `app.exit()`), the adapters stop in the reverse
113
+ order:
114
+
115
+ 1. they stop accepting new requests. Depending on the transport, a request that still arrives is refused with an
116
+ `EntrypointStoppedException` (status 503 over HTTP);
117
+ 2. the requests in progress are given `shutdownTimeout` (30 seconds by default) to finish;
118
+ 3. the requests still running are aborted: their `this.signal` fires, and they are answered with an
119
+ `EntrypointStoppedException`;
120
+ 4. the adapters close their servers and connections.
121
+
122
+ ## 5. Groups and the list of the actions
123
+
124
+ The `groups` option of an action puts it in groups. The access rules can use them (see [Middlewares](Middlewares.md)),
125
+ and the entrypoint can list the actions by group, to build an index of an API or a list of permissions. The groups of
126
+ the HTTP, command line and service actions must be declared, with their names:
127
+
128
+ ```typescript
129
+ import {Application} from 'lakutata'
130
+ import {buildEntrypoints, Controller, type Entrypoint} from 'lakutata/com/entrypoint'
131
+ import {DELETE, GET} from 'lakutata/decorator/ctrl'
132
+ import {Inject} from 'lakutata/decorator/di'
133
+
134
+ class UserController extends Controller {
135
+ @Inject('entrypoint')
136
+ protected readonly entrypoint: Entrypoint
137
+
138
+ @GET('/users', {description: 'List the users'})
139
+ public async list(): Promise<string[]> {
140
+ return []
141
+ }
142
+
143
+ @DELETE('/users/:id', {description: 'Delete a user', groups: ['admin']})
144
+ public async remove(): Promise<void> {
145
+ }
146
+
147
+ //The actions of each group: [{"group":"Administration","actions":["Delete a user"]}]
148
+ @GET('/permissions')
149
+ public async permissions(): Promise<{ group: string, actions: string[] }[]> {
150
+ return this.entrypoint.getHttpActions().groups.map((group) => ({
151
+ group: group.name,
152
+ actions: group.actions.map((action): string => action.description)
153
+ }))
154
+ }
155
+ }
156
+
157
+ Application.run(() => ({
158
+ id: 'users.app',
159
+ name: 'Users',
160
+ components: {
161
+ entrypoint: buildEntrypoints({
162
+ controllers: [UserController],
163
+ httpActionGroups: {admin: 'Administration'}
164
+ })
165
+ },
166
+ bootstrap: ['entrypoint']
167
+ }))
168
+ ```
169
+
170
+ `getHttpActions()`, `getCliActions()` and `getServiceActions()` return `{groups, actions}`: `groups` holds the declared
171
+ groups (`id`, `name` and their `actions`), `actions` every action of the transport. `getActions(channel)` lists the
172
+ actions of any other transport (`getActions('schedule')`). Each action is described by:
173
+
174
+ | Property | Meaning |
175
+ |---|---|
176
+ | `id` | A stable id, computed from its controller, its method and its address |
177
+ | `name`, `description`, `groups`, `acl` | Its options |
178
+ | `controller`, `method` | The name of its controller class, and of its method |
179
+ | `route` / `command` / `pattern` | Its address, prefix included |
180
+ | `jsonSchema` | The JSON schema of its DTO |
181
+
182
+ ## 6. Pushing events to the clients
183
+
184
+ The service transports that keep their clients connected (Socket.IO, Electron, the event stream of the HTTP service,
185
+ JSON-RPC over WebSocket or stdio, gRPC) can receive events from the application.
186
+ `entrypoint.notify(clientId, event, ...args)` sends an event to the client whose id an action found in
187
+ `this.context.clientId`, and returns whether that client received it. `entrypoint.broadcast(event, ...args)` sends it to
188
+ every connected client, and returns their number. See [GRPC](GRPC.md) and [ServiceCalls](ServiceCalls.md).
189
+
190
+ ## 7. Customizing the entrypoint
191
+
192
+ A subclass of `Entrypoint` can replace the class of the component: declare it with `class`, and give the same options
193
+ next to it.
194
+
195
+ ```typescript
196
+ import {AccessControl, Entrypoint} from 'lakutata/com/entrypoint'
197
+
198
+ //Logs every decision of the access control
199
+ class AuditedAccessControl extends AccessControl {
200
+ protected async match(): Promise<boolean> {
201
+ const allowed: boolean = await super.match()
202
+ console.log(`${this.target.name}: ${allowed ? 'allowed' : 'denied'}`)
203
+ return allowed
204
+ }
205
+ }
206
+
207
+ export class AuditedEntrypoint extends Entrypoint {
208
+ protected readonly accessControl: typeof AccessControl = AuditedAccessControl
209
+ }
210
+
211
+ //components: {entrypoint: {class: AuditedEntrypoint, controllers: [...], http: ...}}, bootstrap: ['entrypoint']
212
+ ```
213
+
214
+ An adapter for another transport implements the `EntrypointAdapter` interface of `@lakutata/entrypoint/channel`. The
215
+ entrypoint functions of lakutata 2.x (`buildHTTPEntrypoint`, `buildCLIEntrypoint`, `buildServiceEntrypoint`) are still
216
+ accepted by the `http`, `cli` and `service` options.
@@ -0,0 +1,174 @@
1
+ # Lakutata Exceptions
2
+
3
+ `Exception`, from `lakutata`, is the base class of the errors of the framework, and of the application's own. An
4
+ exception has a code (`errno`) that the clients and the logs identify the error by, a message built from a template, and
5
+ the HTTP status of the error. The transports send these to the clients when an action fails.
6
+
7
+ ## 1. Declaring an exception
8
+
9
+ Extend `Exception`, set `errno` and, for the errors that reach the clients, `statusCode`:
10
+
11
+ ```typescript
12
+ import {Exception} from 'lakutata'
13
+
14
+ export class OrderNotFoundException extends Exception {
15
+ public errno: string = 'E_ORDER_NOT_FOUND'
16
+
17
+ public readonly statusCode: number = 404
18
+ }
19
+
20
+ export class PaymentFailedException extends Exception {
21
+ public errno: string = 'E_PAYMENT_FAILED'
22
+
23
+ public readonly statusCode: number = 502
24
+
25
+ //A property of its own: sent and logged with the error
26
+ public readonly gateway: string = 'stripe'
27
+ }
28
+
29
+ export function findOrder(id: number): never {
30
+ throw new OrderNotFoundException('Order {id} not found', {id: id})
31
+ }
32
+ ```
33
+
34
+ An exception carries:
35
+
36
+ | Property | What it is |
37
+ |---|---|
38
+ | `errno` | The code of the error, a string (`'E_ORDER_NOT_FOUND'`) or a number |
39
+ | `message`, `errMsg` | The message; `errMsg` is a copy of it, sent to the clients |
40
+ | `err` | The name of the class (`'OrderNotFoundException'`) |
41
+ | `statusCode` | The HTTP status of the error; 500 by default |
42
+ | `appId`, `appName` | The id and the name of the application, `'Unknown'` when the exception is created before the application has loaded its options |
43
+ | `stack` | The stack trace |
44
+
45
+ The properties of the subclass, such as `gateway` above, are sent and logged with the others.
46
+
47
+ ## 2. The message
48
+
49
+ The arguments of the constructor build the message:
50
+
51
+ | Created with | Message |
52
+ |---|---|
53
+ | `new OrderNotFoundException('Order {id} not found', {id: 7})` | `Order 7 not found` |
54
+ | `new OrderNotFoundException('Order {0} of {1} not found', 7, 'ada')` | `Order 7 of ada not found` |
55
+ | `new OrderNotFoundException('Order {0} of {1} not found', [7, 'ada'])` | `Order 7 of ada not found` |
56
+ | `new OrderNotFoundException('Order {0} not found', 7)` | `Order 7 not found` |
57
+ | `new OrderNotFoundException('Order %d not found (%s)', 7, 'archived')` | `Order 7 not found (archived)` |
58
+ | `new OrderNotFoundException('Order {id} of {owner} not found', {id: 7})` | `Order 7 of {owner} not found` |
59
+ | `new OrderNotFoundException('Order not found')` | `Order not found` |
60
+ | `new OrderNotFoundException(new Error('timeout'))` | `timeout` |
61
+ | `new OrderNotFoundException()` | `Order not found exception` |
62
+
63
+ - A template takes its values from an object (`{id}`), or from the parameters that follow it, by position (`{0}`,
64
+ `{1}`), given one by one or as an array. The placeholders of `util.format()` (`%s`, `%d`, `%j`…) take the same
65
+ parameters.
66
+ - A value that is missing leaves its placeholder in the message. Building the message never throws.
67
+ - Without arguments, the message is made of the words of the class name; the acronyms are kept
68
+ (`HTTPRequestException` gives `HTTP request exception`).
69
+
70
+ ## 3. A default message and the cause
71
+
72
+ To give an exception a default message, take it in a constructor and pass it to `super()`:
73
+
74
+ ```typescript
75
+ import {Exception} from 'lakutata'
76
+
77
+ export class QuotaExceededException extends Exception {
78
+ public errno: string = 'E_QUOTA_EXCEEDED'
79
+
80
+ public readonly statusCode: number = 429
81
+
82
+ constructor(message: string = 'The quota of the month is used up') {
83
+ super(message)
84
+ }
85
+ }
86
+ ```
87
+
88
+ Avoid setting `message` in a field: the field would replace the message given when the exception is thrown, and with
89
+ the compiler option `useDefineForClassFields` set to `true`, `errMsg` would keep the default message. The
90
+ `tsconfig.json` published by `lakutata` sets that option to `false`.
91
+
92
+ `new XxxException(error)` keeps only the message of the error. To keep the error itself, set it as the `cause`:
93
+
94
+ ```typescript
95
+ import {Exception} from 'lakutata'
96
+
97
+ class PaymentFailedException extends Exception {
98
+ public errno: string = 'E_PAYMENT_FAILED'
99
+
100
+ public readonly statusCode: number = 502
101
+ }
102
+
103
+ export async function pay(orderId: number, charge: () => Promise<void>): Promise<void> {
104
+ try {
105
+ await charge()
106
+ } catch (error) {
107
+ const failure: PaymentFailedException = new PaymentFailedException('Payment of order {id} failed', {id: orderId})
108
+ failure.cause = error
109
+ throw failure
110
+ }
111
+ }
112
+ ```
113
+
114
+ The logger writes the causes after the error (see [Logger](Logger.md), section 2), and the HTTP adapters read the status
115
+ along the causes: the first client error (4xx) found wins (see [HTTP](HTTP.md), section 6).
116
+
117
+ ## 4. What the clients receive
118
+
119
+ When an action fails, its transport reports the exception:
120
+
121
+ | Transport | The client receives |
122
+ |---|---|
123
+ | HTTP | The status `statusCode` and `{errno, errMsg}` (see [HTTP](HTTP.md), section 6) |
124
+ | Socket.IO, Electron, the HTTP service, NATS, gRPC | The whole error: all its own properties (see [ServiceCalls](ServiceCalls.md), section 3) |
125
+ | JSON-RPC | An error with the code `-32602` for an invalid input, `-32601` when no action matches, `-32001` while the application stops, `-32000` otherwise, and the whole error as its `data` |
126
+ | The command line | `error: <message>` on the standard error, and the exit code 1 (see [CLI](CLI.md)) |
127
+ | Schedules | No client: the exception is given to `onError`, or logged (see [Schedules](Schedules.md)) |
128
+
129
+ - Choose `statusCode` by what the client can do about the error: 400 to 499 when the request is wrong (404 for a
130
+ missing resource, 409 for a conflict), 500 and above when the application failed.
131
+ - An input refused by its DTO is an `InvalidValueException`, whose status is 400: an error of the client.
132
+
133
+ ## 5. Translating and recognizing an exception
134
+
135
+ With the `I18nMiddleware`, the message of an exception is translated into the language of the request, from the key
136
+ `errors.<errno>` and the parameters of its template (see
137
+ [I18n](I18n.md#exception-messages)). Out of a request, `exceptionParams(exception)`, from `lakutata`, gives
138
+ those parameters.
139
+
140
+ To recognize an exception, use `instanceof` within the application, and `errno` across processes: the clients receive
141
+ the properties of an error, not its class. The proxies of the HTTP service, NATS and gRPC throw a
142
+ `ServiceInvokeException` that carries the `errno` of the remote error.
143
+
144
+ ```typescript
145
+ import {Exception} from 'lakutata'
146
+
147
+ class OrderNotFoundException extends Exception {
148
+ public errno: string = 'E_ORDER_NOT_FOUND'
149
+ }
150
+
151
+ export function isMissingOrder(error: unknown): boolean {
152
+ return error instanceof OrderNotFoundException || (error as { errno?: unknown })?.errno === 'E_ORDER_NOT_FOUND'
153
+ }
154
+ ```
155
+
156
+ ## 6. The exceptions of the framework
157
+
158
+ The framework throws exceptions of its own. The ones met most often:
159
+
160
+ | Exception | `errno` | When |
161
+ |---|---|---|
162
+ | `InvalidValueException` | `E_INVALID_VALUE` | A DTO refuses a value: the input of an action, a call to `validate()` (400) |
163
+ | `InvalidObjectOptionsException` | `E_INVALID_OBJECT_OPTIONS` | The options of an object are invalid, when the application starts |
164
+ | `DependencyInjectionException` | `E_DEPENDENCY_INJECTION` | An object cannot be created: a dependency is missing |
165
+ | `AccessDeniedException` | `E_ACCESS_DENY` | The access control refuses a request (403) |
166
+ | `RouteNotFoundException` | `E_ROUTE_NOT_FOUND` | No HTTP action has this route and method (404) |
167
+ | `ControllerActionNotFoundException` | `E_CONTROLLER_ACTION_NOT_FOUND` | No action matches a service message or a command (404) |
168
+ | `EntrypointStoppedException` | `E_ENTRYPOINT_STOPPED` | The application is stopping (503) |
169
+ | `ServiceInvokeException` | `E_SERVICE_INVOKE` | An action called through a proxy (HTTP service, NATS, gRPC) failed; it carries the properties of the remote error |
170
+ | `ServiceUnavailableException` | `E_SERVICE_UNAVAILABLE` | The service called by an `HTTPServiceProxy` gave no response (503) |
171
+
172
+ The README of each package lists its own exceptions. An exception that nothing catches, and an exception that stops the
173
+ launch of the application, go to its handlers (`onUncaughtException`, `onFatalException`, see
174
+ [Application](Application.md), section 2).