@lakutata/core 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/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-present Lakutata
4
+
5
+ All rights reserved
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md CHANGED
@@ -1,13 +1,16 @@
1
1
  # @lakutata/core
2
2
 
3
+ English | [中文](README.zh-CN.md)
4
+
3
5
  The core of the lakutata framework: the IoC container and dependency injection, the object model (`BaseObject`,
4
- `Component`, `Provider`, `Module`, `Application`) with its asynchronous construction and destruction, and the schema
5
- validation (`DTO`, built on joi) used by the decorators `@Configurable`, `@Expect`, `@Accept` and `@Return`.
6
+ `Component`, `Provider`, `Application`) with its asynchronous construction and destruction, and schema validation
7
+ (`DTO`, a validation engine with the API of joi), which the `@Configurable`, `@Expect`, `@Accept` and `@Return`
8
+ decorators use.
6
9
 
7
10
  Most applications install the umbrella package `lakutata`, which re-exports this package (`lakutata`,
8
- `lakutata/decorator/di`, `lakutata/decorator/dto`, `lakutata/decorator/asst`, `lakutata/dtos`) and presets a pino `log`
9
- component and the `entrypoint` component. `@lakutata/core` alone is enough for a library or an application without
10
- them (it logs to the console then).
11
+ `lakutata/decorator/di`, `lakutata/decorator/dto`, `lakutata/decorator/asst`, `lakutata/dtos`) and presets the `log`
12
+ and `entrypoint` components. `@lakutata/core` alone is enough for a library, or for an application that needs neither
13
+ (it then logs to the console).
11
14
 
12
15
  ## Installation
13
16
 
@@ -16,7 +19,8 @@ npm install lakutata # the framework
16
19
  npm install @lakutata/core # or the core only
17
20
  ```
18
21
 
19
- Node.js 22 or later, ESM (`"type": "module"`). The TypeScript options the decorators need:
22
+ The package needs Node.js `^22.13.0 || >=24.11.0`. Projects use ES modules (`"type": "module"`); the package also ships
23
+ CommonJS builds. The decorators need these TypeScript options:
20
24
 
21
25
  ```json
22
26
  {
@@ -33,8 +37,8 @@ Node.js 22 or later, ESM (`"type": "module"`). The TypeScript options the decora
33
37
 
34
38
  ### An application
35
39
 
36
- An application is a tree of objects created by its container. `Application.run()` validates its options, registers its
37
- components and providers, creates the objects listed in `bootstrap`, then calls `onLaunched`.
40
+ An application is a tree of objects that its container creates. `Application.run()` validates the application's
41
+ options, registers its components and providers, creates the objects listed in `bootstrap`, then calls `onLaunched`.
38
42
 
39
43
  ```typescript
40
44
  import {Application, Component, DTO, type ILogger} from 'lakutata'
@@ -42,7 +46,7 @@ import {Configurable, Inject} from 'lakutata/decorator/di'
42
46
 
43
47
  //A component: a singleton of the application, created when it is first needed (or by bootstrap)
44
48
  class Mailer extends Component {
45
- //Its option, validated when it is created: given by the application options below
49
+ //An option, validated when the component is created; its value comes from the application options below
46
50
  @Configurable(DTO.String().required())
47
51
  protected readonly host: string
48
52
 
@@ -90,14 +94,14 @@ Application
90
94
 
91
95
  ### Dependency injection
92
96
 
93
- `@Inject()` injects the object registered under the property name, `@Inject('name')` under another name,
94
- `@Inject(SomeClass)` an instance of a class (registered automatically when the class is marked `@Autoload()`). The
95
- injected objects are set before `init()` runs.
97
+ `@Inject()` injects the object registered under the property's name, `@Inject('name')` the one registered under
98
+ another name, and `@Inject(SomeClass)` an instance of a class (registered automatically when the class is marked
99
+ `@Autoload()`). Injected objects are set before `init()` runs.
96
100
 
97
101
  ### Validation
98
102
 
99
- A `DTO` subclass declares its fields with `@Expect(schema)`. The schemas are built by the static methods of `DTO`
100
- (`DTO.String()`, `DTO.Number()`, `DTO.Object()`, `DTO.Array()`, `DTO.Alternatives()`…, the joi API):
103
+ A `DTO` subclass declares its fields with `@Expect(schema)`. Schemas are built with the static methods of `DTO`
104
+ (`DTO.String()`, `DTO.Number()`, `DTO.Object()`, `DTO.Array()`, `DTO.Alternatives()`…), which follow the joi API:
101
105
 
102
106
  ```typescript
103
107
  import {DTO} from 'lakutata'
@@ -118,8 +122,8 @@ class Users {
118
122
  }
119
123
  }
120
124
 
121
- //Validated directly: the value with its defaults ({email: 'a@example.com', credit: 0}), InvalidValueException thrown
122
- //when it is invalid
125
+ //Validated directly: resolves to the value with its defaults ({email: 'a@example.com', credit: 0}), or rejects with
126
+ //InvalidValueException when the value is invalid
123
127
  const input = await CreateUserDTO.validateAsync({email: 'a@example.com'})
124
128
  ```
125
129
 
@@ -138,7 +142,7 @@ class Mailer extends Component {
138
142
  }
139
143
  }
140
144
 
141
- //Created with its options, its init() awaited
145
+ //Created with its options; set() resolves once its init() has completed
142
146
  const container: Container = new Container()
143
147
  const mailer: Mailer = await container.set(Mailer, {host: 'localhost'})
144
148
  await mailer.send('someone@example.com', 'Hello')
@@ -151,7 +155,7 @@ await container.destroy()
151
155
  | Export | Subpath | What it is |
152
156
  |---|---|---|
153
157
  | `Application` | `lakutata` | The application: `run()`, `env()`, `alias()`, `onLaunched()`, `onDone()`, `onFatalException()`, `getObject()`, `exit()` |
154
- | `Module` | `lakutata` | A group of components, providers and objects with its own container |
158
+ | `Module` | `lakutata` | The base class of `Application`: the container and the declared objects live here |
155
159
  | `Component` | `lakutata` | A singleton service with events (`emit`, `on`…) |
156
160
  | `Provider` | `lakutata` | A scoped object (one per scope) |
157
161
  | `BaseObject` | `lakutata` | The root class: asynchronous construction (`init`), destruction (`destroy`), `getObject` |
@@ -164,7 +168,7 @@ await container.destroy()
164
168
  | `Expect`, `Accept`, `Return`, `IndexSignature` | `lakutata/decorator/dto` | DTO fields, method argument and return validation |
165
169
  | `Before`, `After` | `lakutata/decorator/asst` | Functions run before and after a method |
166
170
  | `PaginationSearchDTO`, `PaginationResultDTO`, `paginator`, `FlexibleDTO`, `toJsonSchema`, `fromJsonSchema` | `lakutata/dtos` | Pagination, open DTOs and JSON Schema conversion |
167
- | `ConsoleLogger`, `ILogger` | `lakutata/com/logger` | The logger interface and its console implementation |
171
+ | `ConsoleLogger`, `ILogger` | `@lakutata/core/com/logger` | The logger interface and its console implementation |
168
172
 
169
173
  The typings document each declaration with examples.
170
174
 
@@ -183,7 +187,10 @@ The typings document each declaration with examples.
183
187
 
184
188
  ## See also
185
189
 
186
- - The guides: `doc/en/Core.md`, `doc/en/DTOValidation.md`, `doc/en/Decorator.md` of the package `lakutata` (Chinese
187
- in `doc/zh`).
190
+ - The guides on the website: [Objects and Lifetimes](https://lakutata.com/guide/Objects),
191
+ [Dependency Injection](https://lakutata.com/guide/Injection) and
192
+ [DTO and Validation](https://lakutata.com/guide/DTOValidation).
193
+ - The guides `doc/en/Application.md`, `doc/en/Objects.md`, `doc/en/Injection.md`, `doc/en/DTOValidation.md` and
194
+ `doc/en/Decorator.md` in the `lakutata` package (Chinese in `doc/zh`).
188
195
  - `@lakutata/entrypoint`: the controllers and their HTTP, CLI and service transports.
189
- - `@lakutata/logger`: the pino logger preset by `lakutata`.
196
+ - `@lakutata/logger`: the logger that `lakutata` presets as `log` (pino-compatible JSON lines).
@@ -0,0 +1,182 @@
1
+ # @lakutata/core
2
+
3
+ [English](README.md) | 中文
4
+
5
+ lakutata 框架的核心:IoC 容器和依赖注入,对象模型(`BaseObject`、`Component`、`Provider`、`Application`)及其异步的创建与销毁,以及 `@Configurable`、`@Expect`、`@Accept` 和 `@Return` 装饰器所用的数据校验(`DTO`,一个 API 和 joi 一样的校验引擎)。
6
+
7
+ 大多数应用安装的是 `lakutata` 包:它重新导出这个包(`lakutata`、`lakutata/decorator/di`、`lakutata/decorator/dto`、`lakutata/decorator/asst`、`lakutata/dtos`),并预置 `log` 和 `entrypoint` 组件。对于库,或者这两个组件都不需要的应用,只用 `@lakutata/core` 就够了(此时日志输出到控制台)。
8
+
9
+ ## 安装
10
+
11
+ ```shell
12
+ npm install lakutata # 框架
13
+ npm install @lakutata/core # 或者只安装核心
14
+ ```
15
+
16
+ 这个包需要 Node.js `^22.13.0 || >=24.11.0`。项目使用 ES 模块(`"type": "module"`);包里也带有 CommonJS 构建。装饰器需要以下 TypeScript 选项:
17
+
18
+ ```json
19
+ {
20
+ "compilerOptions": {
21
+ "module": "Node16",
22
+ "moduleResolution": "Node16",
23
+ "experimentalDecorators": true,
24
+ "emitDecoratorMetadata": true
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## 用法
30
+
31
+ ### 一个应用
32
+
33
+ 应用是一棵由它的容器创建的对象树。`Application.run()` 校验应用的选项,注册它的组件和提供者,创建 `bootstrap` 中列出的对象,然后调用 `onLaunched`。
34
+
35
+ ```typescript
36
+ import {Application, Component, DTO, type ILogger} from 'lakutata'
37
+ import {Configurable, Inject} from 'lakutata/decorator/di'
38
+
39
+ //组件:应用中的单例,在第一次被需要时创建(或者由 bootstrap 创建)
40
+ class Mailer extends Component {
41
+ //一个选项,在组件创建时校验;它的值来自下面的应用选项
42
+ @Configurable(DTO.String().required())
43
+ protected readonly host: string
44
+
45
+ @Inject('log')
46
+ protected readonly log: ILogger
47
+
48
+ //注入的对象和选项都设置好之后调用
49
+ protected async init(): Promise<void> {
50
+ this.log.info('Mailer connected to %s', this.host)
51
+ }
52
+
53
+ public async send(to: string, text: string): Promise<void> {
54
+ this.log.info('Mail to %s: %s', to, text)
55
+ }
56
+
57
+ //应用停止时调用
58
+ protected async destroy(): Promise<void> {
59
+ }
60
+ }
61
+
62
+ Application
63
+ .run(() => ({
64
+ id: 'com.example.app',
65
+ name: 'Example',
66
+ components: {
67
+ mailer: {class: Mailer, host: 'smtp.example.com'}
68
+ },
69
+ bootstrap: ['mailer']
70
+ }))
71
+ .onLaunched(async (app: Application, log: ILogger): Promise<void> => {
72
+ const mailer: Mailer = await app.getObject('mailer')
73
+ await mailer.send('someone@example.com', 'Hello')
74
+ log.info('%s started', app.appName)
75
+ })
76
+ .onFatalException((error: Error, log: ILogger): void => log.error(error.message))
77
+ ```
78
+
79
+ ### 生命周期
80
+
81
+ | 装饰器 | 实例数量 | 默认用于 |
82
+ |---|---|---|
83
+ | `@Singleton()` | 每个容器一个 | `Component` |
84
+ | `@Scoped()` | 每个作用域一个(`container.createScope()`,例如一次请求) | `Provider`(包括控制器) |
85
+ | `@Transient()` | 每次解析一个 | `BaseObject` |
86
+
87
+ ### 依赖注入
88
+
89
+ `@Inject()` 注入以属性名注册的对象,`@Inject('name')` 注入以另一个名称注册的对象,`@Inject(SomeClass)` 注入一个类的实例(类标记了 `@Autoload()` 时会自动注册)。注入的对象在 `init()` 执行之前就已设置好。
90
+
91
+ ### 校验
92
+
93
+ `DTO` 的子类用 `@Expect(schema)` 声明它的字段。校验规则由 `DTO` 的静态方法构建(`DTO.String()`、`DTO.Number()`、`DTO.Object()`、`DTO.Array()`、`DTO.Alternatives()`……),API 与 joi 相同:
94
+
95
+ ```typescript
96
+ import {DTO} from 'lakutata'
97
+ import {Accept, Expect} from 'lakutata/decorator/dto'
98
+
99
+ class CreateUserDTO extends DTO {
100
+ @Expect(DTO.String().email().required())
101
+ public email: string
102
+
103
+ @Expect(DTO.Number().integer().min(0).default(0))
104
+ public credit: number
105
+ }
106
+
107
+ class Users {
108
+ //方法执行之前校验参数(并填充默认值)
109
+ @Accept(CreateUserDTO.required())
110
+ public async create(input: CreateUserDTO): Promise<void> {
111
+ }
112
+ }
113
+
114
+ //直接校验:得到补上默认值后的对象({email: 'a@example.com', credit: 0});值不合法时
115
+ //以 InvalidValueException 拒绝
116
+ const input = await CreateUserDTO.validateAsync({email: 'a@example.com'})
117
+ ```
118
+
119
+ ### 测试与库:不使用应用的容器
120
+
121
+ ```typescript
122
+ import {Component, Container, DTO} from 'lakutata'
123
+ import {Configurable} from 'lakutata/decorator/di'
124
+
125
+ class Mailer extends Component {
126
+ @Configurable(DTO.String().required())
127
+ protected readonly host: string
128
+
129
+ public async send(to: string, text: string): Promise<string> {
130
+ return `${this.host} -> ${to}: ${text}`
131
+ }
132
+ }
133
+
134
+ //按选项创建;set() 会等到它的 init() 完成
135
+ const container: Container = new Container()
136
+ const mailer: Mailer = await container.set(Mailer, {host: 'localhost'})
137
+ await mailer.send('someone@example.com', 'Hello')
138
+ //销毁它创建的对象
139
+ await container.destroy()
140
+ ```
141
+
142
+ ## API
143
+
144
+ | 导出 | 子路径 | 说明 |
145
+ |---|---|---|
146
+ | `Application` | `lakutata` | 应用:`run()`、`env()`、`alias()`、`onLaunched()`、`onDone()`、`onFatalException()`、`getObject()`、`exit()` |
147
+ | `Module` | `lakutata` | `Application` 的基类:容器和声明的对象都在这里 |
148
+ | `Component` | `lakutata` | 带事件(`emit`、`on`……)的单例服务 |
149
+ | `Provider` | `lakutata` | 作用域对象(每个作用域一个) |
150
+ | `BaseObject` | `lakutata` | 根类:异步构造(`init`)、销毁(`destroy`)、`getObject` |
151
+ | `Container` | `lakutata` | IoC 容器:`set`、`get`、`getShared`、`register`、`createScope`、`destroy` |
152
+ | `DTO` | `lakutata` | 带校验的数据类,以及构建校验规则的方法 |
153
+ | `Exception` | `lakutata` | 框架中各种异常的基类(`errno`、`statusCode`) |
154
+ | `isPackaged`、`resourcesPath` | `lakutata` | 应用是否以打包形式运行,以及它的资源目录 |
155
+ | `registerValidationMessages`、`runInValidationLanguage`…… | `lakutata` | 校验消息的语言 |
156
+ | `Inject`、`Configurable`、`Autoload`、`Singleton`、`Scoped`、`Transient`、`Lifetime` | `lakutata/decorator/di` | 依赖注入与生命周期 |
157
+ | `Expect`、`Accept`、`Return`、`IndexSignature` | `lakutata/decorator/dto` | DTO 字段,以及方法参数和返回值的校验 |
158
+ | `Before`、`After` | `lakutata/decorator/asst` | 在方法之前和之后执行的函数 |
159
+ | `PaginationSearchDTO`、`PaginationResultDTO`、`paginator`、`FlexibleDTO`、`toJsonSchema`、`fromJsonSchema` | `lakutata/dtos` | 分页、接受任意字段的 DTO、JSON Schema 转换 |
160
+ | `ConsoleLogger`、`ILogger` | `@lakutata/core/com/logger` | 日志记录器接口及其控制台实现 |
161
+
162
+ 类型声明中的每一项都附有说明和示例。
163
+
164
+ ## 错误
165
+
166
+ | 异常 | 什么时候抛出 |
167
+ |---|---|
168
+ | `InvalidValueException` | 值不符合它的校验规则(`DTO.validate`、`@Configurable`、`@Expect`) |
169
+ | `InvalidMethodAcceptException`、`InvalidMethodReturnException` | 方法的参数(`@Accept`)或返回值(`@Return`)不合法 |
170
+ | `DependencyInjectionException` | 要注入的对象无法解析 |
171
+ | `InvalidObjectOptionsException`、`InvalidObjectTypeException` | 所声明对象的选项或类不合法 |
172
+ | `LifetimeLockedException` | 类的生命周期锁定之后又被修改 |
173
+ | `ObjectDestroyedException` | 使用了已销毁的对象 |
174
+ | `AliasExistsException`、`AliasNotFoundException`、`InvalidAliasNameException` | 路径别名重复声明、不存在或不合法 |
175
+ | `MethodNotFoundException` | 按名称调用的方法不存在 |
176
+
177
+ ## 另见
178
+
179
+ - 网站上的指南:[对象与生命周期](https://lakutata.com/zh/guide/Objects)、[依赖注入](https://lakutata.com/zh/guide/Injection)、[DTO 与数据校验](https://lakutata.com/zh/guide/DTOValidation)。
180
+ - `lakutata` 包中的指南 `doc/zh/Application.md`、`doc/zh/Objects.md`、`doc/zh/Injection.md`、`doc/zh/DTOValidation.md` 和 `doc/zh/Decorator.md`。
181
+ - `@lakutata/entrypoint`:控制器,以及它们的 HTTP、命令行和服务调用传输方式。
182
+ - `@lakutata/logger`:`lakutata` 预置为 `log` 的日志记录器(与 pino 兼容的 JSON 行)。
@@ -1,7 +1,8 @@
1
1
  import { DTO } from '../lib/dto/DTO.js';
2
2
  /**
3
- * The input of a paginated search: how many items to return and how many to skip. Extend it with the search criteria,
4
- * and pass the validated input to `paginator()`.
3
+ * The input of a paginated search: how many items to return and how many to skip, numbers or their text (the query
4
+ * string of a request, `?limit=20&offset=40`). Extend it with the search criteria, and pass the validated input to
5
+ * `paginator()`.
5
6
  * @example
6
7
  * ```typescript
7
8
  * import {DTO} from 'lakutata'
@@ -21,12 +22,13 @@ import { DTO } from '../lib/dto/DTO.js';
21
22
  */
22
23
  export declare class PaginationSearchDTO extends DTO {
23
24
  /**
24
- * The maximum number of items to return: an integer, 0 or more.
25
+ * The maximum number of items to return: an integer, 0 or more (its text too, as a query string gives it).
25
26
  * @default 10
26
27
  */
27
28
  limit?: number;
28
29
  /**
29
- * The number of items to skip before the first one returned: an integer, 0 or more.
30
+ * The number of items to skip before the first one returned: an integer, 0 or more (its text too, as a query string
31
+ * gives it).
30
32
  * @default 0
31
33
  */
32
34
  offset?: number;
@@ -13,8 +13,9 @@ exports.PaginationSearchDTO = void 0;
13
13
  const DTO_js_1 = require("../lib/dto/DTO.js");
14
14
  const Expect_js_1 = require("../decorators/dto/Expect.js");
15
15
  /**
16
- * The input of a paginated search: how many items to return and how many to skip. Extend it with the search criteria,
17
- * and pass the validated input to `paginator()`.
16
+ * The input of a paginated search: how many items to return and how many to skip, numbers or their text (the query
17
+ * string of a request, `?limit=20&offset=40`). Extend it with the search criteria, and pass the validated input to
18
+ * `paginator()`.
18
19
  * @example
19
20
  * ```typescript
20
21
  * import {DTO} from 'lakutata'
@@ -36,10 +37,10 @@ class PaginationSearchDTO extends DTO_js_1.DTO {
36
37
  }
37
38
  exports.PaginationSearchDTO = PaginationSearchDTO;
38
39
  __decorate([
39
- (0, Expect_js_1.Expect)(DTO_js_1.DTO.Number().integer().positive().allow(0).optional().default(10)),
40
+ (0, Expect_js_1.Expect)(DTO_js_1.DTO.Number().integer().positive().allow(0).strict(false).optional().default(10)),
40
41
  __metadata("design:type", Number)
41
42
  ], PaginationSearchDTO.prototype, "limit", void 0);
42
43
  __decorate([
43
- (0, Expect_js_1.Expect)(DTO_js_1.DTO.Number().integer().positive().allow(0).optional().default(0)),
44
+ (0, Expect_js_1.Expect)(DTO_js_1.DTO.Number().integer().positive().allow(0).strict(false).optional().default(0)),
44
45
  __metadata("design:type", Number)
45
46
  ], PaginationSearchDTO.prototype, "offset", void 0);
@@ -3,9 +3,11 @@ import { Exception } from '../../lib/foundation/Exception.js';
3
3
  * Thrown when a value does not match its schema: by `DTO.validate()` and `DTO.validateAsync()`, by the DTO constructor
4
4
  * and the assignments to a DTO property, and by the creation of an object whose `@Configurable` value is invalid. Its
5
5
  * message describes the first error; its causes lead to the `SchemaValidationException` holding the details (the path
6
- * and the rule of each error). Its `errno` is `E_INVALID_VALUE`; the HTTP entrypoints answer an invalid action input with the
7
- * status 400, JSON-RPC with the error "invalid params".
6
+ * and the rule of each error). Its `errno` is `E_INVALID_VALUE` and its `statusCode` 400, a value refused being an
7
+ * error of the caller: the HTTP entrypoints answer an invalid action input with the status 400, the service transports
8
+ * send the error with it, JSON-RPC answers with the error "invalid params".
8
9
  */
9
10
  export declare class InvalidValueException extends Exception {
10
11
  errno: string | number;
12
+ readonly statusCode: number;
11
13
  }
@@ -6,13 +6,15 @@ const Exception_js_1 = require("../../lib/foundation/Exception.js");
6
6
  * Thrown when a value does not match its schema: by `DTO.validate()` and `DTO.validateAsync()`, by the DTO constructor
7
7
  * and the assignments to a DTO property, and by the creation of an object whose `@Configurable` value is invalid. Its
8
8
  * message describes the first error; its causes lead to the `SchemaValidationException` holding the details (the path
9
- * and the rule of each error). Its `errno` is `E_INVALID_VALUE`; the HTTP entrypoints answer an invalid action input with the
10
- * status 400, JSON-RPC with the error "invalid params".
9
+ * and the rule of each error). Its `errno` is `E_INVALID_VALUE` and its `statusCode` 400, a value refused being an
10
+ * error of the caller: the HTTP entrypoints answer an invalid action input with the status 400, the service transports
11
+ * send the error with it, JSON-RPC answers with the error "invalid params".
11
12
  */
12
13
  class InvalidValueException extends Exception_js_1.Exception {
13
14
  constructor() {
14
15
  super(...arguments);
15
16
  this.errno = 'E_INVALID_VALUE';
17
+ this.statusCode = 400;
16
18
  }
17
19
  }
18
20
  exports.InvalidValueException = InvalidValueException;
@@ -1,4 +1,5 @@
1
1
  import { Module } from '../core/Module.js';
2
+ import { __destroy } from '../base/BaseObject.js';
2
3
  import { ApplicationConfigLoader } from './ApplicationConfigLoader.js';
3
4
  import { ApplicationOptions } from '../../options/ApplicationOptions.js';
4
5
  import { Alias } from '../core/Alias.js';
@@ -270,6 +271,20 @@ export declare class Application extends Module {
270
271
  * @protected
271
272
  */
272
273
  protected static listenShutdownSignals(signals: NodeJS.Signals[]): void;
274
+ /**
275
+ * End the launched application gracefully once nothing keeps the process busy (no server, connection or timer
276
+ * left): its exit, with the exit code the process has (`process.exitCode`, 0 by default), as with `exit()`. A
277
+ * process ending so does not wait for an exit, so the done handlers and the teardown would not run otherwise.
278
+ * @param app The launched application.
279
+ * @protected
280
+ */
281
+ protected static listenNaturalEnd(app: Application): void;
282
+ /**
283
+ * Stop ending the application with the process: it exits, or it is destroyed (disposed by a test, which leaves the
284
+ * end of the process to its runner)
285
+ * @protected
286
+ */
287
+ protected static stopListeningNaturalEnd(): void;
273
288
  /**
274
289
  * Launch the application: environment variables, aliases, options (or the database migration command given by the
275
290
  * `MIGRATION_*` environment variables), then the application created in a new root container.
@@ -328,6 +343,13 @@ export declare class Application extends Module {
328
343
  * variable.
329
344
  */
330
345
  get timezone(): string;
346
+ /**
347
+ * The framework's destroyer, see {@link Module}: a destroyed application no longer ends with the process.
348
+ * @param hooks The steps the framework's subclasses run before `destroy()`.
349
+ * @returns Resolves once the application and its objects are destroyed.
350
+ * @protected
351
+ */
352
+ protected [__destroy](...hooks: (() => Promise<void>)[]): Promise<void>;
331
353
  /**
332
354
  * The time the process has been running, in seconds (rounded down).
333
355
  */
@@ -14,6 +14,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.Application = exports.ApplicationState = void 0;
16
16
  const Module_js_1 = require("../core/Module.js");
17
+ const BaseObject_js_1 = require("../base/BaseObject.js");
17
18
  const Container_js_1 = require("../core/Container.js");
18
19
  const ApplicationConfigLoader_js_1 = require("./ApplicationConfigLoader.js");
19
20
  const ApplicationOptions_js_1 = require("../../options/ApplicationOptions.js");
@@ -136,6 +137,16 @@ class Application extends Module_js_1.Module {
136
137
  * @private
137
138
  */
138
139
  static #consoleLogger;
140
+ /**
141
+ * Whether the application is exiting: a step of its bootstrap that exits it (a command of the migrations) ends its
142
+ * launch
143
+ */
144
+ static #exiting = false;
145
+ /**
146
+ * The listener of the end of the process once nothing keeps it busy (beforeExit): the launched application exits
147
+ * gracefully then, as with exit()
148
+ */
149
+ static #naturalEnd;
139
150
  /**
140
151
  * Preset the options of the applications, before they run: the packages assembling the framework preset their
141
152
  * components (the `lakutata` package presets its Logger as `log` and its Entrypoint as `entrypoint`). An object
@@ -374,6 +385,36 @@ class Application extends Module_js_1.Module {
374
385
  });
375
386
  }
376
387
  }
388
+ /**
389
+ * End the launched application gracefully once nothing keeps the process busy (no server, connection or timer
390
+ * left): its exit, with the exit code the process has (`process.exitCode`, 0 by default), as with `exit()`. A
391
+ * process ending so does not wait for an exit, so the done handlers and the teardown would not run otherwise.
392
+ * @param app The launched application.
393
+ * @protected
394
+ */
395
+ static listenNaturalEnd(app) {
396
+ Application.stopListeningNaturalEnd();
397
+ const listener = () => {
398
+ Application.stopListeningNaturalEnd();
399
+ //Exiting already (a signal, exit()): its teardown runs
400
+ if (Application.#exiting)
401
+ return;
402
+ const code = Number(process.exitCode ?? 0);
403
+ Application.eventEmitter.emit('exit', app, Number.isInteger(code) ? code : 1);
404
+ };
405
+ Application.#naturalEnd = listener;
406
+ process.once('beforeExit', listener);
407
+ }
408
+ /**
409
+ * Stop ending the application with the process: it exits, or it is destroyed (disposed by a test, which leaves the
410
+ * end of the process to its runner)
411
+ * @protected
412
+ */
413
+ static stopListeningNaturalEnd() {
414
+ if (Application.#naturalEnd)
415
+ process.removeListener('beforeExit', Application.#naturalEnd);
416
+ Application.#naturalEnd = undefined;
417
+ }
377
418
  /**
378
419
  * Launch the application: environment variables, aliases, options (or the database migration command given by the
379
420
  * `MIGRATION_*` environment variables), then the application created in a new root container.
@@ -382,6 +423,7 @@ class Application extends Module_js_1.Module {
382
423
  */
383
424
  static async launchApplication() {
384
425
  this.eventEmitter.once('exit', async (app, exitCode, signal) => {
426
+ this.#exiting = true;
385
427
  try {
386
428
  //The done handler, told why the application exits
387
429
  const reason = signal ? { code: exitCode ? exitCode : 0, signal: signal } : { code: exitCode ? exitCode : 0 };
@@ -497,6 +539,9 @@ class Application extends Module_js_1.Module {
497
539
  if (applicationOptions.shutdownSignals)
498
540
  this.listenShutdownSignals(applicationOptions.shutdownSignals);
499
541
  applicationOptions.bootstrap?.push(async (target) => {
542
+ //Exited by a step of the bootstrap: its container is being destroyed, it does not launch
543
+ if (Application.#exiting)
544
+ return;
500
545
  const launchedCallbackRemover = function () {
501
546
  this.options.bootstrap?.pop();
502
547
  (0, As_1.As)(Reflect.getOwnMetadata('#bootstrap', this))?.pop();
@@ -508,6 +553,7 @@ class Application extends Module_js_1.Module {
508
553
  (0, ApplicationLaunch_js_1.markApplicationLaunched)(target);
509
554
  //The application launched is the one the signals end (its launched handlers may receive them)
510
555
  Reflect.set(this, 'appInstance', target);
556
+ Application.listenNaturalEnd((0, As_1.As)(target));
511
557
  this.eventEmitter.emit(ApplicationState.Launched, (0, As_1.As)(target));
512
558
  });
513
559
  //Resolved once the application is initialized (its objects loaded, its bootstrap run), rejected with
@@ -582,6 +628,16 @@ class Application extends Module_js_1.Module {
582
628
  get timezone() {
583
629
  return (0, BasicInfo_js_1.getBasicInfo)().timezone;
584
630
  }
631
+ /**
632
+ * The framework's destroyer, see {@link Module}: a destroyed application no longer ends with the process.
633
+ * @param hooks The steps the framework's subclasses run before `destroy()`.
634
+ * @returns Resolves once the application and its objects are destroyed.
635
+ * @protected
636
+ */
637
+ async [BaseObject_js_1.__destroy](...hooks) {
638
+ Application.stopListeningNaturalEnd();
639
+ return await super[BaseObject_js_1.__destroy](...hooks);
640
+ }
585
641
  /**
586
642
  * The time the process has been running, in seconds (rounded down).
587
643
  */
@@ -559,7 +559,9 @@ let BaseObject = BaseObject_1 = class BaseObject extends AsyncConstructor_js_1.A
559
559
  */
560
560
  getMethod(name, throwExceptionIfNotFound = false) {
561
561
  if (this.hasMethod(name)) {
562
- return (...args) => this[name](...args);
562
+ //Named: else V8 names it after the code below in the stack traces (MethodNotFoundException.methodName)
563
+ const callMethod = (...args) => this[name](...args);
564
+ return callMethod;
563
565
  }
564
566
  else if (throwExceptionIfNotFound) {
565
567
  throw new MethodNotFoundException_js_1.MethodNotFoundException('Method "{methodName}" not found in "{className}"', {
@@ -183,7 +183,12 @@ function dynamicProxyProperty(target, prop, receiver, validateFn) {
183
183
  }
184
184
  };
185
185
  }
186
- return propertyValue.bind(target);
186
+ //A method is called on its object, not on the proxy (the internal slots of a Date, a Map); the function itself is
187
+ //kept, with its own properties: an express application is a function whose methods (its own bind among them)
188
+ //its requests read
189
+ return new Proxy(propertyValue, {
190
+ apply: (method, _thisArg, args) => Reflect.apply(method, target, args)
191
+ });
187
192
  }
188
193
  return new Proxy(propertyValue, {
189
194
  set(target, p, value, receiver) {
@@ -30,7 +30,8 @@ export declare function exceptionParams(exception: Error): Record<string, unknow
30
30
  * leaves its placeholder in the message; building the message never throws.
31
31
  * - `new XxxException('Order not found')`: the message as given.
32
32
  * - `new XxxException(error)`: the message of another error.
33
- * - `new XxxException()`: a message made from the class name.
33
+ * - `new XxxException()`: a message made from the words of the class name, the acronyms kept (`Order not found
34
+ * exception` for `OrderNotFoundException`, `HTTP request exception` for `HTTPRequestException`).
34
35
  *
35
36
  * The template parameters are kept for the translation of the message, see {@link exceptionParams}.
36
37
  * @example
@@ -6,7 +6,6 @@ const node_util_1 = require("node:util");
6
6
  const BasicInfo_js_1 = require("./BasicInfo.js");
7
7
  const Templating_1 = require("@lakutata/utils/Templating");
8
8
  const As_1 = require("@lakutata/utils/As");
9
- const CamelCase_1 = require("@lakutata/utils/CamelCase");
10
9
  const DevNull_1 = require("@lakutata/utils/DevNull");
11
10
  /**
12
11
  * The placeholders of util.format() consuming an argument
@@ -87,7 +86,8 @@ function exceptionParams(exception) {
87
86
  * leaves its placeholder in the message; building the message never throws.
88
87
  * - `new XxxException('Order not found')`: the message as given.
89
88
  * - `new XxxException(error)`: the message of another error.
90
- * - `new XxxException()`: a message made from the class name.
89
+ * - `new XxxException()`: a message made from the words of the class name, the acronyms kept (`Order not found
90
+ * exception` for `OrderNotFoundException`, `HTTP request exception` for `HTTPRequestException`).
91
91
  *
92
92
  * The template parameters are kept for the translation of the message, see {@link exceptionParams}.
93
93
  * @example
@@ -124,9 +124,10 @@ class Exception extends Error {
124
124
  this.statusCode = 500;
125
125
  if (b.length) {
126
126
  const template = a;
127
- const templatingArgs = b.length > 1 ? b : b[0];
127
+ //The parameters given one by one ({0}, {1}…), even a single one, or the object or array given alone
128
+ const templatingArgs = b.length > 1 || typeof b[0] !== 'object' || b[0] === null ? b : b[0];
128
129
  //Kept for the translations of the message, not an enumerable property (the errors sent are not changed)
129
- Object.defineProperty(this, PARAMS, { value: b.length > 1 || typeof b[0] !== 'object' || b[0] === null ? b : b[0], enumerable: false });
130
+ Object.defineProperty(this, PARAMS, { value: templatingArgs, enumerable: false });
130
131
  try {
131
132
  this.message = (0, Templating_1.templating)(template, templatingArgs, { ignoreMissing: true });
132
133
  }
@@ -147,11 +148,30 @@ class Exception extends Error {
147
148
  }
148
149
  }
149
150
  else {
150
- //Empty message
151
- const message = (0, CamelCase_1.camelCase)(this.name).toLowerCase();
151
+ //Empty message: the words of the class name, in lower case but the acronyms
152
+ const message = this.name
153
+ .replace(/([a-z\d])([A-Z])/g, '$1 $2')
154
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
155
+ .split(/[\s_-]+/)
156
+ .filter((word) => !!word)
157
+ .map((word) => /^[A-Z\d]{2,}$/.test(word) ? word : word.toLowerCase())
158
+ .join(' ');
152
159
  this.message = `${message.charAt(0).toUpperCase()}${message.slice(1)}`;
153
160
  }
154
- this.errMsg = this.message;
161
+ //message as an accessor writing errMsg too (enumerable as the message assigned before): a subclass setting its
162
+ //message in a field initializer (run after this constructor) keeps errMsg the same, errMsg staying a data
163
+ //property (serialized, logged as it is)
164
+ let message = this.message;
165
+ Object.defineProperty(this, 'message', {
166
+ configurable: true,
167
+ enumerable: true,
168
+ get: () => message,
169
+ set: (value) => {
170
+ message = value;
171
+ (0, As_1.As)(this).errMsg = value;
172
+ }
173
+ });
174
+ this.errMsg = message;
155
175
  this.err = this.name;
156
176
  }
157
177
  /**
@@ -10,8 +10,9 @@ var __metadata = (this && this.__metadata) || function (k, v) {
10
10
  import { DTO } from '../lib/dto/DTO.js';
11
11
  import { Expect } from '../decorators/dto/Expect.js';
12
12
  /**
13
- * The input of a paginated search: how many items to return and how many to skip. Extend it with the search criteria,
14
- * and pass the validated input to `paginator()`.
13
+ * The input of a paginated search: how many items to return and how many to skip, numbers or their text (the query
14
+ * string of a request, `?limit=20&offset=40`). Extend it with the search criteria, and pass the validated input to
15
+ * `paginator()`.
15
16
  * @example
16
17
  * ```typescript
17
18
  * import {DTO} from 'lakutata'
@@ -32,10 +33,10 @@ import { Expect } from '../decorators/dto/Expect.js';
32
33
  export class PaginationSearchDTO extends DTO {
33
34
  }
34
35
  __decorate([
35
- Expect(DTO.Number().integer().positive().allow(0).optional().default(10)),
36
+ Expect(DTO.Number().integer().positive().allow(0).strict(false).optional().default(10)),
36
37
  __metadata("design:type", Number)
37
38
  ], PaginationSearchDTO.prototype, "limit", void 0);
38
39
  __decorate([
39
- Expect(DTO.Number().integer().positive().allow(0).optional().default(0)),
40
+ Expect(DTO.Number().integer().positive().allow(0).strict(false).optional().default(0)),
40
41
  __metadata("design:type", Number)
41
42
  ], PaginationSearchDTO.prototype, "offset", void 0);
@@ -3,12 +3,14 @@ import { Exception } from '../../lib/foundation/Exception.js';
3
3
  * Thrown when a value does not match its schema: by `DTO.validate()` and `DTO.validateAsync()`, by the DTO constructor
4
4
  * and the assignments to a DTO property, and by the creation of an object whose `@Configurable` value is invalid. Its
5
5
  * message describes the first error; its causes lead to the `SchemaValidationException` holding the details (the path
6
- * and the rule of each error). Its `errno` is `E_INVALID_VALUE`; the HTTP entrypoints answer an invalid action input with the
7
- * status 400, JSON-RPC with the error "invalid params".
6
+ * and the rule of each error). Its `errno` is `E_INVALID_VALUE` and its `statusCode` 400, a value refused being an
7
+ * error of the caller: the HTTP entrypoints answer an invalid action input with the status 400, the service transports
8
+ * send the error with it, JSON-RPC answers with the error "invalid params".
8
9
  */
9
10
  export class InvalidValueException extends Exception {
10
11
  constructor() {
11
12
  super(...arguments);
12
13
  this.errno = 'E_INVALID_VALUE';
14
+ this.statusCode = 400;
13
15
  }
14
16
  }
@@ -8,6 +8,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  };
10
10
  import { Module } from '../core/Module.js';
11
+ import { __destroy } from '../base/BaseObject.js';
11
12
  import { Container } from '../core/Container.js';
12
13
  import { ApplicationConfigLoader } from './ApplicationConfigLoader.js';
13
14
  import { ApplicationOptions } from '../../options/ApplicationOptions.js';
@@ -130,6 +131,16 @@ export class Application extends Module {
130
131
  * @private
131
132
  */
132
133
  static #consoleLogger;
134
+ /**
135
+ * Whether the application is exiting: a step of its bootstrap that exits it (a command of the migrations) ends its
136
+ * launch
137
+ */
138
+ static #exiting = false;
139
+ /**
140
+ * The listener of the end of the process once nothing keeps it busy (beforeExit): the launched application exits
141
+ * gracefully then, as with exit()
142
+ */
143
+ static #naturalEnd;
133
144
  /**
134
145
  * Preset the options of the applications, before they run: the packages assembling the framework preset their
135
146
  * components (the `lakutata` package presets its Logger as `log` and its Entrypoint as `entrypoint`). An object
@@ -368,6 +379,36 @@ export class Application extends Module {
368
379
  });
369
380
  }
370
381
  }
382
+ /**
383
+ * End the launched application gracefully once nothing keeps the process busy (no server, connection or timer
384
+ * left): its exit, with the exit code the process has (`process.exitCode`, 0 by default), as with `exit()`. A
385
+ * process ending so does not wait for an exit, so the done handlers and the teardown would not run otherwise.
386
+ * @param app The launched application.
387
+ * @protected
388
+ */
389
+ static listenNaturalEnd(app) {
390
+ Application.stopListeningNaturalEnd();
391
+ const listener = () => {
392
+ Application.stopListeningNaturalEnd();
393
+ //Exiting already (a signal, exit()): its teardown runs
394
+ if (Application.#exiting)
395
+ return;
396
+ const code = Number(process.exitCode ?? 0);
397
+ Application.eventEmitter.emit('exit', app, Number.isInteger(code) ? code : 1);
398
+ };
399
+ Application.#naturalEnd = listener;
400
+ process.once('beforeExit', listener);
401
+ }
402
+ /**
403
+ * Stop ending the application with the process: it exits, or it is destroyed (disposed by a test, which leaves the
404
+ * end of the process to its runner)
405
+ * @protected
406
+ */
407
+ static stopListeningNaturalEnd() {
408
+ if (Application.#naturalEnd)
409
+ process.removeListener('beforeExit', Application.#naturalEnd);
410
+ Application.#naturalEnd = undefined;
411
+ }
371
412
  /**
372
413
  * Launch the application: environment variables, aliases, options (or the database migration command given by the
373
414
  * `MIGRATION_*` environment variables), then the application created in a new root container.
@@ -376,6 +417,7 @@ export class Application extends Module {
376
417
  */
377
418
  static async launchApplication() {
378
419
  this.eventEmitter.once('exit', async (app, exitCode, signal) => {
420
+ this.#exiting = true;
379
421
  try {
380
422
  //The done handler, told why the application exits
381
423
  const reason = signal ? { code: exitCode ? exitCode : 0, signal: signal } : { code: exitCode ? exitCode : 0 };
@@ -491,6 +533,9 @@ export class Application extends Module {
491
533
  if (applicationOptions.shutdownSignals)
492
534
  this.listenShutdownSignals(applicationOptions.shutdownSignals);
493
535
  applicationOptions.bootstrap?.push(async (target) => {
536
+ //Exited by a step of the bootstrap: its container is being destroyed, it does not launch
537
+ if (Application.#exiting)
538
+ return;
494
539
  const launchedCallbackRemover = function () {
495
540
  this.options.bootstrap?.pop();
496
541
  As(Reflect.getOwnMetadata('#bootstrap', this))?.pop();
@@ -502,6 +547,7 @@ export class Application extends Module {
502
547
  markApplicationLaunched(target);
503
548
  //The application launched is the one the signals end (its launched handlers may receive them)
504
549
  Reflect.set(this, 'appInstance', target);
550
+ Application.listenNaturalEnd(As(target));
505
551
  this.eventEmitter.emit(ApplicationState.Launched, As(target));
506
552
  });
507
553
  //Resolved once the application is initialized (its objects loaded, its bootstrap run), rejected with
@@ -576,6 +622,16 @@ export class Application extends Module {
576
622
  get timezone() {
577
623
  return getBasicInfo().timezone;
578
624
  }
625
+ /**
626
+ * The framework's destroyer, see {@link Module}: a destroyed application no longer ends with the process.
627
+ * @param hooks The steps the framework's subclasses run before `destroy()`.
628
+ * @returns Resolves once the application and its objects are destroyed.
629
+ * @protected
630
+ */
631
+ async [__destroy](...hooks) {
632
+ Application.stopListeningNaturalEnd();
633
+ return await super[__destroy](...hooks);
634
+ }
579
635
  /**
580
636
  * The time the process has been running, in seconds (rounded down).
581
637
  */
@@ -556,7 +556,9 @@ let BaseObject = BaseObject_1 = class BaseObject extends AsyncConstructor {
556
556
  */
557
557
  getMethod(name, throwExceptionIfNotFound = false) {
558
558
  if (this.hasMethod(name)) {
559
- return (...args) => this[name](...args);
559
+ //Named: else V8 names it after the code below in the stack traces (MethodNotFoundException.methodName)
560
+ const callMethod = (...args) => this[name](...args);
561
+ return callMethod;
560
562
  }
561
563
  else if (throwExceptionIfNotFound) {
562
564
  throw new MethodNotFoundException('Method "{methodName}" not found in "{className}"', {
@@ -180,7 +180,12 @@ function dynamicProxyProperty(target, prop, receiver, validateFn) {
180
180
  }
181
181
  };
182
182
  }
183
- return propertyValue.bind(target);
183
+ //A method is called on its object, not on the proxy (the internal slots of a Date, a Map); the function itself is
184
+ //kept, with its own properties: an express application is a function whose methods (its own bind among them)
185
+ //its requests read
186
+ return new Proxy(propertyValue, {
187
+ apply: (method, _thisArg, args) => Reflect.apply(method, target, args)
188
+ });
184
189
  }
185
190
  return new Proxy(propertyValue, {
186
191
  set(target, p, value, receiver) {
@@ -2,7 +2,6 @@ import { format as utilFormat } from 'node:util';
2
2
  import { getAppId, getAppName } from './BasicInfo.js';
3
3
  import { templating } from '@lakutata/utils/Templating';
4
4
  import { As } from '@lakutata/utils/As';
5
- import { camelCase } from '@lakutata/utils/CamelCase';
6
5
  import { DevNull } from '@lakutata/utils/DevNull';
7
6
  /**
8
7
  * The placeholders of util.format() consuming an argument
@@ -83,7 +82,8 @@ export function exceptionParams(exception) {
83
82
  * leaves its placeholder in the message; building the message never throws.
84
83
  * - `new XxxException('Order not found')`: the message as given.
85
84
  * - `new XxxException(error)`: the message of another error.
86
- * - `new XxxException()`: a message made from the class name.
85
+ * - `new XxxException()`: a message made from the words of the class name, the acronyms kept (`Order not found
86
+ * exception` for `OrderNotFoundException`, `HTTP request exception` for `HTTPRequestException`).
87
87
  *
88
88
  * The template parameters are kept for the translation of the message, see {@link exceptionParams}.
89
89
  * @example
@@ -120,9 +120,10 @@ export class Exception extends Error {
120
120
  this.statusCode = 500;
121
121
  if (b.length) {
122
122
  const template = a;
123
- const templatingArgs = b.length > 1 ? b : b[0];
123
+ //The parameters given one by one ({0}, {1}…), even a single one, or the object or array given alone
124
+ const templatingArgs = b.length > 1 || typeof b[0] !== 'object' || b[0] === null ? b : b[0];
124
125
  //Kept for the translations of the message, not an enumerable property (the errors sent are not changed)
125
- Object.defineProperty(this, PARAMS, { value: b.length > 1 || typeof b[0] !== 'object' || b[0] === null ? b : b[0], enumerable: false });
126
+ Object.defineProperty(this, PARAMS, { value: templatingArgs, enumerable: false });
126
127
  try {
127
128
  this.message = templating(template, templatingArgs, { ignoreMissing: true });
128
129
  }
@@ -143,11 +144,30 @@ export class Exception extends Error {
143
144
  }
144
145
  }
145
146
  else {
146
- //Empty message
147
- const message = camelCase(this.name).toLowerCase();
147
+ //Empty message: the words of the class name, in lower case but the acronyms
148
+ const message = this.name
149
+ .replace(/([a-z\d])([A-Z])/g, '$1 $2')
150
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
151
+ .split(/[\s_-]+/)
152
+ .filter((word) => !!word)
153
+ .map((word) => /^[A-Z\d]{2,}$/.test(word) ? word : word.toLowerCase())
154
+ .join(' ');
148
155
  this.message = `${message.charAt(0).toUpperCase()}${message.slice(1)}`;
149
156
  }
150
- this.errMsg = this.message;
157
+ //message as an accessor writing errMsg too (enumerable as the message assigned before): a subclass setting its
158
+ //message in a field initializer (run after this constructor) keeps errMsg the same, errMsg staying a data
159
+ //property (serialized, logged as it is)
160
+ let message = this.message;
161
+ Object.defineProperty(this, 'message', {
162
+ configurable: true,
163
+ enumerable: true,
164
+ get: () => message,
165
+ set: (value) => {
166
+ message = value;
167
+ As(this).errMsg = value;
168
+ }
169
+ });
170
+ this.errMsg = message;
151
171
  this.err = this.name;
152
172
  }
153
173
  /**
@@ -1,7 +1,8 @@
1
1
  import { DTO } from '../lib/dto/DTO.js';
2
2
  /**
3
- * The input of a paginated search: how many items to return and how many to skip. Extend it with the search criteria,
4
- * and pass the validated input to `paginator()`.
3
+ * The input of a paginated search: how many items to return and how many to skip, numbers or their text (the query
4
+ * string of a request, `?limit=20&offset=40`). Extend it with the search criteria, and pass the validated input to
5
+ * `paginator()`.
5
6
  * @example
6
7
  * ```typescript
7
8
  * import {DTO} from 'lakutata'
@@ -21,12 +22,13 @@ import { DTO } from '../lib/dto/DTO.js';
21
22
  */
22
23
  export declare class PaginationSearchDTO extends DTO {
23
24
  /**
24
- * The maximum number of items to return: an integer, 0 or more.
25
+ * The maximum number of items to return: an integer, 0 or more (its text too, as a query string gives it).
25
26
  * @default 10
26
27
  */
27
28
  limit?: number;
28
29
  /**
29
- * The number of items to skip before the first one returned: an integer, 0 or more.
30
+ * The number of items to skip before the first one returned: an integer, 0 or more (its text too, as a query string
31
+ * gives it).
30
32
  * @default 0
31
33
  */
32
34
  offset?: number;
@@ -3,9 +3,11 @@ import { Exception } from '../../lib/foundation/Exception.js';
3
3
  * Thrown when a value does not match its schema: by `DTO.validate()` and `DTO.validateAsync()`, by the DTO constructor
4
4
  * and the assignments to a DTO property, and by the creation of an object whose `@Configurable` value is invalid. Its
5
5
  * message describes the first error; its causes lead to the `SchemaValidationException` holding the details (the path
6
- * and the rule of each error). Its `errno` is `E_INVALID_VALUE`; the HTTP entrypoints answer an invalid action input with the
7
- * status 400, JSON-RPC with the error "invalid params".
6
+ * and the rule of each error). Its `errno` is `E_INVALID_VALUE` and its `statusCode` 400, a value refused being an
7
+ * error of the caller: the HTTP entrypoints answer an invalid action input with the status 400, the service transports
8
+ * send the error with it, JSON-RPC answers with the error "invalid params".
8
9
  */
9
10
  export declare class InvalidValueException extends Exception {
10
11
  errno: string | number;
12
+ readonly statusCode: number;
11
13
  }
@@ -1,4 +1,5 @@
1
1
  import { Module } from '../core/Module.js';
2
+ import { __destroy } from '../base/BaseObject.js';
2
3
  import { ApplicationConfigLoader } from './ApplicationConfigLoader.js';
3
4
  import { ApplicationOptions } from '../../options/ApplicationOptions.js';
4
5
  import { Alias } from '../core/Alias.js';
@@ -270,6 +271,20 @@ export declare class Application extends Module {
270
271
  * @protected
271
272
  */
272
273
  protected static listenShutdownSignals(signals: NodeJS.Signals[]): void;
274
+ /**
275
+ * End the launched application gracefully once nothing keeps the process busy (no server, connection or timer
276
+ * left): its exit, with the exit code the process has (`process.exitCode`, 0 by default), as with `exit()`. A
277
+ * process ending so does not wait for an exit, so the done handlers and the teardown would not run otherwise.
278
+ * @param app The launched application.
279
+ * @protected
280
+ */
281
+ protected static listenNaturalEnd(app: Application): void;
282
+ /**
283
+ * Stop ending the application with the process: it exits, or it is destroyed (disposed by a test, which leaves the
284
+ * end of the process to its runner)
285
+ * @protected
286
+ */
287
+ protected static stopListeningNaturalEnd(): void;
273
288
  /**
274
289
  * Launch the application: environment variables, aliases, options (or the database migration command given by the
275
290
  * `MIGRATION_*` environment variables), then the application created in a new root container.
@@ -328,6 +343,13 @@ export declare class Application extends Module {
328
343
  * variable.
329
344
  */
330
345
  get timezone(): string;
346
+ /**
347
+ * The framework's destroyer, see {@link Module}: a destroyed application no longer ends with the process.
348
+ * @param hooks The steps the framework's subclasses run before `destroy()`.
349
+ * @returns Resolves once the application and its objects are destroyed.
350
+ * @protected
351
+ */
352
+ protected [__destroy](...hooks: (() => Promise<void>)[]): Promise<void>;
331
353
  /**
332
354
  * The time the process has been running, in seconds (rounded down).
333
355
  */
@@ -30,7 +30,8 @@ export declare function exceptionParams(exception: Error): Record<string, unknow
30
30
  * leaves its placeholder in the message; building the message never throws.
31
31
  * - `new XxxException('Order not found')`: the message as given.
32
32
  * - `new XxxException(error)`: the message of another error.
33
- * - `new XxxException()`: a message made from the class name.
33
+ * - `new XxxException()`: a message made from the words of the class name, the acronyms kept (`Order not found
34
+ * exception` for `OrderNotFoundException`, `HTTP request exception` for `HTTPRequestException`).
34
35
  *
35
36
  * The template parameters are kept for the translation of the message, see {@link exceptionParams}.
36
37
  * @example
package/package.json CHANGED
@@ -1,7 +1,31 @@
1
1
  {
2
2
  "name": "@lakutata/core",
3
- "version": "3.0.0",
4
- "description": "Core of the lakutata IoC framework (IoC/DI, BaseObject, DTO/validation, decorators, console logger, monitor).",
3
+ "version": "3.1.0",
4
+ "description": "The core of the lakutata framework: the IoC container and dependency injection, the object model with asynchronous construction, and DTO schema validation with the API of joi.",
5
+ "keywords": [
6
+ "lakutata",
7
+ "ioc",
8
+ "dependency-injection",
9
+ "di",
10
+ "container",
11
+ "decorators",
12
+ "dto",
13
+ "validation",
14
+ "joi",
15
+ "typescript"
16
+ ],
17
+ "homepage": "https://lakutata.com/packages/core",
18
+ "bugs": {
19
+ "url": "https://github.com/lakutata/lakutata/issues",
20
+ "email": "admin@lakutata.com"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/lakutata/lakutata.git",
25
+ "directory": "packages/core"
26
+ },
27
+ "author": "alex.ma",
28
+ "license": "MIT",
5
29
  "type": "module",
6
30
  "main": "./dist/cjs/exports/Core.js",
7
31
  "types": "./dist/cjs/exports/Core.d.ts",
@@ -83,11 +107,11 @@
83
107
  "test:engine-types": "node scripts/check-engine-types.mjs"
84
108
  },
85
109
  "dependencies": {
86
- "@lakutata/utils": "3.0.0",
110
+ "@lakutata/utils": "3.1.0",
87
111
  "reflect-metadata": "0.2.2"
88
112
  },
89
113
  "engines": {
90
- "node": ">=22"
114
+ "node": "^22.13.0 || >=24.11.0"
91
115
  },
92
116
  "files": [
93
117
  "dist",