@lakutata/core 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +29 -22
  3. package/README.zh-CN.md +182 -0
  4. package/dist/cjs/decorators/dto/Expect.d.ts +2 -1
  5. package/dist/cjs/decorators/dto/Expect.js +2 -1
  6. package/dist/cjs/dto/PaginationSearchDTO.d.ts +6 -4
  7. package/dist/cjs/dto/PaginationSearchDTO.js +5 -4
  8. package/dist/cjs/exceptions/dto/InvalidValueException.d.ts +4 -2
  9. package/dist/cjs/exceptions/dto/InvalidValueException.js +4 -2
  10. package/dist/cjs/lib/app/Application.d.ts +22 -0
  11. package/dist/cjs/lib/app/Application.js +56 -0
  12. package/dist/cjs/lib/base/BaseObject.js +3 -1
  13. package/dist/cjs/lib/base/internal/ConfigurableRecordsInjection.d.ts +7 -7
  14. package/dist/cjs/lib/base/internal/ConfigurableRecordsInjection.js +10 -14
  15. package/dist/cjs/lib/core/Container.js +3 -2
  16. package/dist/cjs/lib/dto/DTO.d.ts +3 -1
  17. package/dist/cjs/lib/dto/DTO.js +76 -5
  18. package/dist/cjs/lib/foundation/Exception.d.ts +2 -1
  19. package/dist/cjs/lib/foundation/Exception.js +27 -7
  20. package/dist/esm/decorators/dto/Expect.js +2 -1
  21. package/dist/esm/dto/PaginationSearchDTO.js +5 -4
  22. package/dist/esm/exceptions/dto/InvalidValueException.js +4 -2
  23. package/dist/esm/lib/app/Application.js +56 -0
  24. package/dist/esm/lib/base/BaseObject.js +3 -1
  25. package/dist/esm/lib/base/internal/ConfigurableRecordsInjection.js +10 -14
  26. package/dist/esm/lib/core/Container.js +3 -2
  27. package/dist/esm/lib/dto/DTO.js +76 -5
  28. package/dist/esm/lib/foundation/Exception.js +27 -7
  29. package/dist/types/decorators/dto/Expect.d.ts +2 -1
  30. package/dist/types/dto/PaginationSearchDTO.d.ts +6 -4
  31. package/dist/types/exceptions/dto/InvalidValueException.d.ts +4 -2
  32. package/dist/types/lib/app/Application.d.ts +22 -0
  33. package/dist/types/lib/base/internal/ConfigurableRecordsInjection.d.ts +7 -7
  34. package/dist/types/lib/dto/DTO.d.ts +3 -1
  35. package/dist/types/lib/foundation/Exception.d.ts +2 -1
  36. package/package.json +28 -4
@@ -10,28 +10,24 @@ exports.getConfigurableRecordsFromInstance = getConfigurableRecordsFromInstance;
10
10
  const DIMetadataKey_js_1 = require("../../../constants/DIMetadataKey.js");
11
11
  const ConstructorSymbol_js_1 = require("../../ioc/ConstructorSymbol.js");
12
12
  const As_1 = require("@lakutata/utils/As");
13
+ //The configurable records of the registrations, by resolver: each container keeps the options of its own registrations
14
+ const registrationRecords = new WeakMap();
13
15
  /**
14
- * Set configurable records to target constructor with its registration name
15
- * @param target
16
- * @param registrationName
16
+ * Set configurable records to a registration (the options it was declared with)
17
+ * @param registration
17
18
  * @param configurableRecords
18
19
  * @constructor
19
20
  */
20
- function setConfigurableRecords(target, registrationName, configurableRecords) {
21
- if (Reflect.hasOwnMetadata(registrationName, target))
22
- Reflect.deleteMetadata(registrationName, target);
23
- Reflect.defineMetadata(registrationName, configurableRecords, target);
21
+ function setConfigurableRecords(registration, configurableRecords) {
22
+ registrationRecords.set(registration, configurableRecords);
24
23
  }
25
24
  /**
26
- * Get configurable records from target constructor by its registration name
27
- * @param target
25
+ * Get configurable records from a registration
26
+ * @param registration
28
27
  * @constructor
29
28
  */
30
- function getConfigurableRecords(target, registrationName) {
31
- const configurableRecords = Reflect.getOwnMetadata(registrationName, target);
32
- if (configurableRecords)
33
- return configurableRecords;
34
- return {};
29
+ function getConfigurableRecords(registration) {
30
+ return (registration && registrationRecords.get(registration)) ?? {};
35
31
  }
36
32
  /**
37
33
  * Set id to instance
@@ -305,7 +305,8 @@ class Container {
305
305
  (0, ThrowWarning_js_1.throwWarning)(`The configuration passed for the already created object "${(0, As_1.As)(resolved).className}" is ignored. Use a scoped or transient object to get an instance with its own configuration.`);
306
306
  }
307
307
  if (isNewSubBaseObject) {
308
- const presetConfigurableRecords = (0, ConfigurableRecordsInjection_js_1.getConfigurableRecords)((0, As_1.As)(resolved.constructor), registrationName);
308
+ //The options of the registration in effect here (a child container's registration shadows its parent's)
309
+ const presetConfigurableRecords = (0, ConfigurableRecordsInjection_js_1.getConfigurableRecords)(this.#dic.getRegistration(registrationName));
309
310
  //set id into object instance
310
311
  (0, ConfigurableRecordsInjection_js_1.setIdToInstance)((0, As_1.As)(resolved), registrationName);
311
312
  //set configurable records into object instance
@@ -326,8 +327,8 @@ class Container {
326
327
  const configurableRecords = { ...options, class: void (0) };
327
328
  delete configurableRecords.id;
328
329
  delete configurableRecords.class;
329
- (0, ConfigurableRecordsInjection_js_1.setConfigurableRecords)(options.class, id, configurableRecords);
330
330
  pair[id] = this.#createResolver(options.class);
331
+ (0, ConfigurableRecordsInjection_js_1.setConfigurableRecords)(pair[id], configurableRecords);
331
332
  return pair;
332
333
  }
333
334
  /**
@@ -45,7 +45,9 @@ export declare class DTO extends DataValidator {
45
45
  * Create a DTO from its data, validated against the class schema: the defaults applied, the keys without schema
46
46
  * removed (unless the class has an `@IndexSignature`). The instance then validates each change of a property, or
47
47
  * of a value nested in it (an array, an object): an invalid change throws an `InvalidValueException` and is
48
- * undone.
48
+ * undone. A property accepting any value (`DTO.Any()`, or an index signature `DTO.Any()`) gives the objects of
49
+ * other classes it holds as they are, since no change of theirs can be invalid: their getters, their private fields
50
+ * and their identity are those of the object itself.
49
51
  * @param props The data of the DTO. Default: no data (`{}`).
50
52
  * @throws {InvalidValueException} When the data does not match the class schema.
51
53
  */
@@ -94,6 +94,55 @@ function getPropertySchema(instance, propertyKey) {
94
94
  schemas.set(propertyKey, schema);
95
95
  return schema;
96
96
  }
97
+ /**
98
+ * Whether the properties of a DTO accept any value, per DTO class
99
+ */
100
+ const anyValuePropertyCache = new WeakMap();
101
+ /**
102
+ * Whether a schema accepts any value: an any schema without rule, value list, condition or external validation, so no
103
+ * change of a value it holds can fail validation
104
+ * @param schema
105
+ */
106
+ function acceptsAnything(schema) {
107
+ if (!schema || schema.type !== 'any' || schema._rules.length || schema._valids || schema._invalids)
108
+ return false;
109
+ const terms = schema.$_terms;
110
+ return !['whens', 'externals'].some((term) => Array.isArray(terms[term]) && terms[term].length > 0);
111
+ }
112
+ /**
113
+ * Whether a property of a DTO accepts any value: declared with `DTO.Any()`, or undeclared and matched by an index
114
+ * signature `DTO.Any()` (the transport objects of a context)
115
+ * @param instance
116
+ * @param propertyKey
117
+ */
118
+ function acceptsAnyValue(instance, propertyKey) {
119
+ if (typeof propertyKey !== 'string')
120
+ return false;
121
+ const properties = (0, ObjectSchemaValidation_js_1.getCachedDTOSchema)(anyValuePropertyCache, (0, ObjectConstructor_1.objectConstructor)(instance), () => new Map());
122
+ let accepts = properties.get(propertyKey);
123
+ if (accepts !== undefined)
124
+ return accepts;
125
+ const objectSchema = (0, As_1.As)(getObjectSchema(instance));
126
+ const child = (objectSchema.$_terms.keys || []).find(({ key }) => key === propertyKey);
127
+ //An undeclared property: the first pattern matching its key (the index signature)
128
+ const pattern = child ? undefined : (objectSchema.$_terms.patterns || []).find((each) => each.regex
129
+ ? each.regex.test(propertyKey)
130
+ : !each.schema.validate(propertyKey).error);
131
+ accepts = acceptsAnything(child ? child.schema : pattern?.rule);
132
+ properties.set(propertyKey, accepts);
133
+ return accepts;
134
+ }
135
+ /**
136
+ * Whether an object is plain data: an array, an object literal, or an object without prototype (an instance of a
137
+ * subclass of Array or Object, and the objects of another realm, count as objects of other classes)
138
+ * @param value
139
+ */
140
+ function isPlainData(value) {
141
+ const constructor = value.constructor;
142
+ if (constructor === Object || constructor === Array)
143
+ return true;
144
+ return constructor === undefined && Object.getPrototypeOf(value) === null;
145
+ }
97
146
  /**
98
147
  * Validate property value (only the property when the properties are validated independently)
99
148
  * @param instance
@@ -160,8 +209,10 @@ function propertyState(target, prop) {
160
209
  * @param prop
161
210
  * @param receiver
162
211
  * @param validateFn
212
+ * @param acceptsAny Whether a property of the DTO holding the value accepts any value
213
+ * @param ownerKey The property of the DTO holding the value
163
214
  */
164
- function dynamicProxyProperty(target, prop, receiver, validateFn) {
215
+ function dynamicProxyProperty(target, prop, receiver, validateFn, acceptsAny, ownerKey) {
165
216
  const propertyValue = Reflect.get(target, prop, receiver);
166
217
  if (!propertyValue)
167
218
  return propertyValue;
@@ -183,8 +234,18 @@ function dynamicProxyProperty(target, prop, receiver, validateFn) {
183
234
  }
184
235
  };
185
236
  }
186
- return propertyValue.bind(target);
237
+ //A method is called on its object, not on the proxy (the internal slots of a Date, a Map); the function itself is
238
+ //kept, with its own properties: an express application is a function whose methods (its own bind among them)
239
+ //its requests read
240
+ return new Proxy(propertyValue, {
241
+ apply: (method, _thisArg, args) => Reflect.apply(method, target, args)
242
+ });
187
243
  }
244
+ //An object of another class held by a property accepting any value is given as it is: no change of it can be
245
+ //refused, and a proxy would break the getters reading its private fields, the internal slots of the built-ins (the
246
+ //size of a Map) and its identity
247
+ if (acceptsAny?.(ownerKey) && !isPlainData(propertyValue))
248
+ return propertyValue;
188
249
  return new Proxy(propertyValue, {
189
250
  set(target, p, value, receiver) {
190
251
  const state = propertyState(target, p);
@@ -199,7 +260,7 @@ function dynamicProxyProperty(target, prop, receiver, validateFn) {
199
260
  }
200
261
  },
201
262
  get(target, p, receiver) {
202
- return dynamicProxyProperty(target, p, receiver, validateFn);
263
+ return dynamicProxyProperty(target, p, receiver, validateFn, acceptsAny, ownerKey);
203
264
  },
204
265
  deleteProperty: (target, prop) => {
205
266
  const state = propertyState(target, prop);
@@ -257,12 +318,22 @@ let DTO = DTO_1 = class DTO extends DataValidator_js_1.DataValidator {
257
318
  * Create a DTO from its data, validated against the class schema: the defaults applied, the keys without schema
258
319
  * removed (unless the class has an `@IndexSignature`). The instance then validates each change of a property, or
259
320
  * of a value nested in it (an array, an object): an invalid change throws an `InvalidValueException` and is
260
- * undone.
321
+ * undone. A property accepting any value (`DTO.Any()`, or an index signature `DTO.Any()`) gives the objects of
322
+ * other classes it holds as they are, since no change of theirs can be invalid: their getters, their private fields
323
+ * and their identity are those of the object itself.
261
324
  * @param props The data of the DTO. Default: no data (`{}`).
262
325
  * @throws {InvalidValueException} When the data does not match the class schema.
263
326
  */
264
327
  constructor(props = {}) {
265
328
  super();
329
+ //Whether each property accepts any value, remembered by the instance once known
330
+ let accepting;
331
+ const acceptsAny = (key) => {
332
+ if (typeof key !== 'string')
333
+ return false;
334
+ accepting ??= Object.create(null);
335
+ return accepting[key] ??= acceptsAnyValue(this, key);
336
+ };
266
337
  //Create DTO proxy object
267
338
  const DTOInstanceProxy = new Proxy(this, {
268
339
  set: (target, prop, value, receiver) => {
@@ -278,7 +349,7 @@ let DTO = DTO_1 = class DTO extends DataValidator_js_1.DataValidator {
278
349
  else {
279
350
  validatePropertyValue(this, p, Reflect.get(target, p));
280
351
  }
281
- }),
352
+ }, acceptsAny, p),
282
353
  deleteProperty: (target, prop) => {
283
354
  if (!(0, IsSymbol_1.isSymbol)(prop))
284
355
  validatePropertyValue(this, prop, void (0));
@@ -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
  /**
@@ -3,7 +3,8 @@ import { setObjectPropertySchema } from '../../lib/dto/ObjectSchemaValidation.js
3
3
  * Declare the schema of a property of a DTO class. The DTO validates the property against it when it is constructed
4
4
  * (the whole object at once: defaults applied, the keys without schema removed unless the class has an
5
5
  * `@IndexSignature`), and each time the property, or a value nested in it, is changed afterwards (the change is undone
6
- * when invalid). The schema is also part of the class schema (`MyDTO.Schema()`) used by `@Accept`, `@Return` and the
6
+ * when invalid). With `DTO.Any()`, an object of another class the property holds is given as it is, not watched: no
7
+ * change of it can be invalid. The schema is also part of the class schema (`MyDTO.Schema()`) used by `@Accept`, `@Return` and the
7
8
  * actions' input validation. Subclasses inherit the property schemas, and may declare a property again to replace its
8
9
  * schema. Ignored on a symbol property or on a class which is not a DTO.
9
10
  * @param schema The schema of the property, built with the static methods of `DTO` (`DTO.String().required()`); a
@@ -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}"', {
@@ -1,28 +1,24 @@
1
1
  import { DI_TARGET_ID } from '../../../constants/DIMetadataKey.js';
2
2
  import { constructorSymbol } from '../../ioc/ConstructorSymbol.js';
3
3
  import { As } from '@lakutata/utils/As';
4
+ //The configurable records of the registrations, by resolver: each container keeps the options of its own registrations
5
+ const registrationRecords = new WeakMap();
4
6
  /**
5
- * Set configurable records to target constructor with its registration name
6
- * @param target
7
- * @param registrationName
7
+ * Set configurable records to a registration (the options it was declared with)
8
+ * @param registration
8
9
  * @param configurableRecords
9
10
  * @constructor
10
11
  */
11
- export function setConfigurableRecords(target, registrationName, configurableRecords) {
12
- if (Reflect.hasOwnMetadata(registrationName, target))
13
- Reflect.deleteMetadata(registrationName, target);
14
- Reflect.defineMetadata(registrationName, configurableRecords, target);
12
+ export function setConfigurableRecords(registration, configurableRecords) {
13
+ registrationRecords.set(registration, configurableRecords);
15
14
  }
16
15
  /**
17
- * Get configurable records from target constructor by its registration name
18
- * @param target
16
+ * Get configurable records from a registration
17
+ * @param registration
19
18
  * @constructor
20
19
  */
21
- export function getConfigurableRecords(target, registrationName) {
22
- const configurableRecords = Reflect.getOwnMetadata(registrationName, target);
23
- if (configurableRecords)
24
- return configurableRecords;
25
- return {};
20
+ export function getConfigurableRecords(registration) {
21
+ return (registration && registrationRecords.get(registration)) ?? {};
26
22
  }
27
23
  /**
28
24
  * Set id to instance
@@ -268,7 +268,8 @@ export class Container {
268
268
  throwWarning(`The configuration passed for the already created object "${As(resolved).className}" is ignored. Use a scoped or transient object to get an instance with its own configuration.`);
269
269
  }
270
270
  if (isNewSubBaseObject) {
271
- const presetConfigurableRecords = getConfigurableRecords(As(resolved.constructor), registrationName);
271
+ //The options of the registration in effect here (a child container's registration shadows its parent's)
272
+ const presetConfigurableRecords = getConfigurableRecords(this.#dic.getRegistration(registrationName));
272
273
  //set id into object instance
273
274
  setIdToInstance(As(resolved), registrationName);
274
275
  //set configurable records into object instance
@@ -289,8 +290,8 @@ export class Container {
289
290
  const configurableRecords = { ...options, class: void (0) };
290
291
  delete configurableRecords.id;
291
292
  delete configurableRecords.class;
292
- setConfigurableRecords(options.class, id, configurableRecords);
293
293
  pair[id] = this.#createResolver(options.class);
294
+ setConfigurableRecords(pair[id], configurableRecords);
294
295
  return pair;
295
296
  }
296
297
  /**
@@ -91,6 +91,55 @@ function getPropertySchema(instance, propertyKey) {
91
91
  schemas.set(propertyKey, schema);
92
92
  return schema;
93
93
  }
94
+ /**
95
+ * Whether the properties of a DTO accept any value, per DTO class
96
+ */
97
+ const anyValuePropertyCache = new WeakMap();
98
+ /**
99
+ * Whether a schema accepts any value: an any schema without rule, value list, condition or external validation, so no
100
+ * change of a value it holds can fail validation
101
+ * @param schema
102
+ */
103
+ function acceptsAnything(schema) {
104
+ if (!schema || schema.type !== 'any' || schema._rules.length || schema._valids || schema._invalids)
105
+ return false;
106
+ const terms = schema.$_terms;
107
+ return !['whens', 'externals'].some((term) => Array.isArray(terms[term]) && terms[term].length > 0);
108
+ }
109
+ /**
110
+ * Whether a property of a DTO accepts any value: declared with `DTO.Any()`, or undeclared and matched by an index
111
+ * signature `DTO.Any()` (the transport objects of a context)
112
+ * @param instance
113
+ * @param propertyKey
114
+ */
115
+ function acceptsAnyValue(instance, propertyKey) {
116
+ if (typeof propertyKey !== 'string')
117
+ return false;
118
+ const properties = getCachedDTOSchema(anyValuePropertyCache, objectConstructor(instance), () => new Map());
119
+ let accepts = properties.get(propertyKey);
120
+ if (accepts !== undefined)
121
+ return accepts;
122
+ const objectSchema = As(getObjectSchema(instance));
123
+ const child = (objectSchema.$_terms.keys || []).find(({ key }) => key === propertyKey);
124
+ //An undeclared property: the first pattern matching its key (the index signature)
125
+ const pattern = child ? undefined : (objectSchema.$_terms.patterns || []).find((each) => each.regex
126
+ ? each.regex.test(propertyKey)
127
+ : !each.schema.validate(propertyKey).error);
128
+ accepts = acceptsAnything(child ? child.schema : pattern?.rule);
129
+ properties.set(propertyKey, accepts);
130
+ return accepts;
131
+ }
132
+ /**
133
+ * Whether an object is plain data: an array, an object literal, or an object without prototype (an instance of a
134
+ * subclass of Array or Object, and the objects of another realm, count as objects of other classes)
135
+ * @param value
136
+ */
137
+ function isPlainData(value) {
138
+ const constructor = value.constructor;
139
+ if (constructor === Object || constructor === Array)
140
+ return true;
141
+ return constructor === undefined && Object.getPrototypeOf(value) === null;
142
+ }
94
143
  /**
95
144
  * Validate property value (only the property when the properties are validated independently)
96
145
  * @param instance
@@ -157,8 +206,10 @@ function propertyState(target, prop) {
157
206
  * @param prop
158
207
  * @param receiver
159
208
  * @param validateFn
209
+ * @param acceptsAny Whether a property of the DTO holding the value accepts any value
210
+ * @param ownerKey The property of the DTO holding the value
160
211
  */
161
- function dynamicProxyProperty(target, prop, receiver, validateFn) {
212
+ function dynamicProxyProperty(target, prop, receiver, validateFn, acceptsAny, ownerKey) {
162
213
  const propertyValue = Reflect.get(target, prop, receiver);
163
214
  if (!propertyValue)
164
215
  return propertyValue;
@@ -180,8 +231,18 @@ function dynamicProxyProperty(target, prop, receiver, validateFn) {
180
231
  }
181
232
  };
182
233
  }
183
- return propertyValue.bind(target);
234
+ //A method is called on its object, not on the proxy (the internal slots of a Date, a Map); the function itself is
235
+ //kept, with its own properties: an express application is a function whose methods (its own bind among them)
236
+ //its requests read
237
+ return new Proxy(propertyValue, {
238
+ apply: (method, _thisArg, args) => Reflect.apply(method, target, args)
239
+ });
184
240
  }
241
+ //An object of another class held by a property accepting any value is given as it is: no change of it can be
242
+ //refused, and a proxy would break the getters reading its private fields, the internal slots of the built-ins (the
243
+ //size of a Map) and its identity
244
+ if (acceptsAny?.(ownerKey) && !isPlainData(propertyValue))
245
+ return propertyValue;
185
246
  return new Proxy(propertyValue, {
186
247
  set(target, p, value, receiver) {
187
248
  const state = propertyState(target, p);
@@ -196,7 +257,7 @@ function dynamicProxyProperty(target, prop, receiver, validateFn) {
196
257
  }
197
258
  },
198
259
  get(target, p, receiver) {
199
- return dynamicProxyProperty(target, p, receiver, validateFn);
260
+ return dynamicProxyProperty(target, p, receiver, validateFn, acceptsAny, ownerKey);
200
261
  },
201
262
  deleteProperty: (target, prop) => {
202
263
  const state = propertyState(target, prop);
@@ -254,12 +315,22 @@ let DTO = DTO_1 = class DTO extends DataValidator {
254
315
  * Create a DTO from its data, validated against the class schema: the defaults applied, the keys without schema
255
316
  * removed (unless the class has an `@IndexSignature`). The instance then validates each change of a property, or
256
317
  * of a value nested in it (an array, an object): an invalid change throws an `InvalidValueException` and is
257
- * undone.
318
+ * undone. A property accepting any value (`DTO.Any()`, or an index signature `DTO.Any()`) gives the objects of
319
+ * other classes it holds as they are, since no change of theirs can be invalid: their getters, their private fields
320
+ * and their identity are those of the object itself.
258
321
  * @param props The data of the DTO. Default: no data (`{}`).
259
322
  * @throws {InvalidValueException} When the data does not match the class schema.
260
323
  */
261
324
  constructor(props = {}) {
262
325
  super();
326
+ //Whether each property accepts any value, remembered by the instance once known
327
+ let accepting;
328
+ const acceptsAny = (key) => {
329
+ if (typeof key !== 'string')
330
+ return false;
331
+ accepting ??= Object.create(null);
332
+ return accepting[key] ??= acceptsAnyValue(this, key);
333
+ };
263
334
  //Create DTO proxy object
264
335
  const DTOInstanceProxy = new Proxy(this, {
265
336
  set: (target, prop, value, receiver) => {
@@ -275,7 +346,7 @@ let DTO = DTO_1 = class DTO extends DataValidator {
275
346
  else {
276
347
  validatePropertyValue(this, p, Reflect.get(target, p));
277
348
  }
278
- }),
349
+ }, acceptsAny, p),
279
350
  deleteProperty: (target, prop) => {
280
351
  if (!isSymbol(prop))
281
352
  validatePropertyValue(this, prop, void (0));