lakutata 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -30
- package/README.zh-CN.md +130 -0
- package/dist/cjs/exports/QueueComponent.d.ts +1 -0
- package/dist/cjs/exports/QueueComponent.js +20 -0
- package/dist/esm/exports/QueueComponent.js +1 -0
- package/dist/types/exports/QueueComponent.d.ts +1 -0
- package/doc/en/Application.md +107 -0
- package/doc/en/CLI.md +274 -0
- package/doc/en/Cache.md +379 -0
- package/doc/en/Command.md +110 -0
- package/doc/en/ContainerdIntegration.md +17 -16
- package/doc/en/Controllers.md +256 -0
- package/doc/en/DBus.md +291 -0
- package/doc/en/DTOValidation.md +72 -43
- package/doc/en/Database.md +188 -0
- package/doc/en/DatabaseDrivers.md +287 -0
- package/doc/en/Decorator.md +52 -40
- package/doc/en/DockerIntegration.md +7 -6
- package/doc/en/Entities.md +240 -0
- package/doc/en/Entrypoint.md +216 -0
- package/doc/en/Exceptions.md +174 -0
- package/doc/en/GRPC.md +251 -0
- package/doc/en/GettingStarted.md +165 -0
- package/doc/en/HTTP.md +507 -0
- package/doc/en/HelperFunctions.md +11 -11
- package/doc/en/I18n.md +119 -115
- package/doc/en/Injection.md +144 -0
- package/doc/en/Installers.md +280 -0
- package/doc/en/Logger.md +206 -0
- package/doc/en/MCP.md +293 -0
- package/doc/en/Middlewares.md +213 -0
- package/doc/en/Migrations.md +202 -0
- package/doc/en/MongoEmbedded.md +149 -0
- package/doc/en/Monitor.md +164 -0
- package/doc/en/Mutex.md +27 -25
- package/doc/en/NATS.md +246 -0
- package/doc/en/NativeAddons.md +212 -0
- package/doc/en/Objects.md +143 -0
- package/doc/en/Overview.md +62 -85
- package/doc/en/Packaging.md +193 -0
- package/doc/en/ProjectStructure.md +124 -0
- package/doc/en/Queue.md +269 -0
- package/doc/en/Schedules.md +217 -0
- package/doc/en/Security.md +10 -9
- package/doc/en/ServiceCalls.md +504 -0
- package/doc/en/Time.md +19 -20
- package/doc/en/UpgradingFrom1.md +435 -0
- package/doc/en/UpgradingFrom2.md +315 -0
- package/doc/en/Workspaces.md +189 -0
- package/doc/en/examples/BackgroundJobs.md +677 -0
- package/doc/en/examples/CommandLineTool.md +664 -0
- package/doc/en/examples/DesktopApp.md +809 -0
- package/doc/en/examples/DeviceService.md +607 -0
- package/doc/en/examples/MCPServer.md +653 -0
- package/doc/en/examples/README.md +16 -0
- package/doc/en/examples/RestAPI.md +490 -0
- package/doc/en/examples/ServiceCalls.md +829 -0
- package/doc/en/examples/WebApp.md +568 -0
- package/doc/zh/Application.md +75 -0
- package/doc/zh/CLI.md +250 -0
- package/doc/zh/Cache.md +328 -0
- package/doc/zh/Command.md +78 -0
- package/doc/zh/ContainerdIntegration.md +9 -9
- package/doc/zh/Controllers.md +228 -0
- package/doc/zh/DBus.md +257 -0
- package/doc/zh/DTOValidation.md +49 -19
- package/doc/zh/Database.md +175 -0
- package/doc/zh/DatabaseDrivers.md +261 -0
- package/doc/zh/Decorator.md +25 -18
- package/doc/zh/DockerIntegration.md +6 -6
- package/doc/zh/Entities.md +224 -0
- package/doc/zh/Entrypoint.md +193 -0
- package/doc/zh/Exceptions.md +158 -0
- package/doc/zh/GRPC.md +226 -0
- package/doc/zh/GettingStarted.md +148 -0
- package/doc/zh/HTTP.md +445 -0
- package/doc/zh/HelperFunctions.md +2 -2
- package/doc/zh/I18n.md +26 -26
- package/doc/zh/Injection.md +129 -0
- package/doc/zh/Installers.md +232 -0
- package/doc/zh/Logger.md +188 -0
- package/doc/zh/MCP.md +265 -0
- package/doc/zh/Middlewares.md +198 -0
- package/doc/zh/Migrations.md +178 -0
- package/doc/zh/MongoEmbedded.md +118 -0
- package/doc/zh/Monitor.md +145 -0
- package/doc/zh/Mutex.md +10 -10
- package/doc/zh/NATS.md +207 -0
- package/doc/zh/NativeAddons.md +180 -0
- package/doc/zh/Objects.md +119 -0
- package/doc/zh/Overview.md +46 -71
- package/doc/zh/Packaging.md +154 -0
- package/doc/zh/ProjectStructure.md +102 -0
- package/doc/zh/Queue.md +214 -0
- package/doc/zh/Schedules.md +193 -0
- package/doc/zh/Security.md +6 -6
- package/doc/zh/ServiceCalls.md +459 -0
- package/doc/zh/Time.md +2 -2
- package/doc/zh/UpgradingFrom1.md +347 -0
- package/doc/zh/UpgradingFrom2.md +230 -0
- package/doc/zh/Workspaces.md +150 -0
- package/doc/zh/examples/BackgroundJobs.md +618 -0
- package/doc/zh/examples/CommandLineTool.md +618 -0
- package/doc/zh/examples/DesktopApp.md +748 -0
- package/doc/zh/examples/DeviceService.md +546 -0
- package/doc/zh/examples/MCPServer.md +604 -0
- package/doc/zh/examples/README.md +14 -0
- package/doc/zh/examples/RestAPI.md +458 -0
- package/doc/zh/examples/ServiceCalls.md +765 -0
- package/doc/zh/examples/WebApp.md +520 -0
- package/package.json +46 -17
- package/doc/en/ActionPattern.md +0 -408
- package/doc/en/Core.md +0 -312
- package/doc/en/ORMIntegration.md +0 -341
- package/doc/zh/ActionPattern.md +0 -334
- package/doc/zh/Core.md +0 -256
- package/doc/zh/ORMIntegration.md +0 -319
|
@@ -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
|
|
27
|
-
|
|
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
|
|
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
|
|
194
|
-
and the daemon
|
|
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).
|