@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.
- package/LICENSE +23 -0
- package/README.md +29 -22
- package/README.zh-CN.md +182 -0
- package/dist/cjs/decorators/dto/Expect.d.ts +2 -1
- package/dist/cjs/decorators/dto/Expect.js +2 -1
- package/dist/cjs/dto/PaginationSearchDTO.d.ts +6 -4
- package/dist/cjs/dto/PaginationSearchDTO.js +5 -4
- package/dist/cjs/exceptions/dto/InvalidValueException.d.ts +4 -2
- package/dist/cjs/exceptions/dto/InvalidValueException.js +4 -2
- package/dist/cjs/lib/app/Application.d.ts +22 -0
- package/dist/cjs/lib/app/Application.js +56 -0
- package/dist/cjs/lib/base/BaseObject.js +3 -1
- package/dist/cjs/lib/base/internal/ConfigurableRecordsInjection.d.ts +7 -7
- package/dist/cjs/lib/base/internal/ConfigurableRecordsInjection.js +10 -14
- package/dist/cjs/lib/core/Container.js +3 -2
- package/dist/cjs/lib/dto/DTO.d.ts +3 -1
- package/dist/cjs/lib/dto/DTO.js +76 -5
- package/dist/cjs/lib/foundation/Exception.d.ts +2 -1
- package/dist/cjs/lib/foundation/Exception.js +27 -7
- package/dist/esm/decorators/dto/Expect.js +2 -1
- package/dist/esm/dto/PaginationSearchDTO.js +5 -4
- package/dist/esm/exceptions/dto/InvalidValueException.js +4 -2
- package/dist/esm/lib/app/Application.js +56 -0
- package/dist/esm/lib/base/BaseObject.js +3 -1
- package/dist/esm/lib/base/internal/ConfigurableRecordsInjection.js +10 -14
- package/dist/esm/lib/core/Container.js +3 -2
- package/dist/esm/lib/dto/DTO.js +76 -5
- package/dist/esm/lib/foundation/Exception.js +27 -7
- package/dist/types/decorators/dto/Expect.d.ts +2 -1
- package/dist/types/dto/PaginationSearchDTO.d.ts +6 -4
- package/dist/types/exceptions/dto/InvalidValueException.d.ts +4 -2
- package/dist/types/lib/app/Application.d.ts +22 -0
- package/dist/types/lib/base/internal/ConfigurableRecordsInjection.d.ts +7 -7
- package/dist/types/lib/dto/DTO.d.ts +3 -1
- package/dist/types/lib/foundation/Exception.d.ts +2 -1
- 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
|
|
15
|
-
* @param
|
|
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(
|
|
21
|
-
|
|
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
|
|
27
|
-
* @param
|
|
25
|
+
* Get configurable records from a registration
|
|
26
|
+
* @param registration
|
|
28
27
|
* @constructor
|
|
29
28
|
*/
|
|
30
|
-
function getConfigurableRecords(
|
|
31
|
-
|
|
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
|
-
|
|
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
|
*/
|
package/dist/cjs/lib/dto/DTO.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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).
|
|
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
|
|
14
|
-
* and pass the validated input to
|
|
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
|
|
7
|
-
*
|
|
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
|
-
|
|
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
|
|
6
|
-
* @param
|
|
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(
|
|
12
|
-
|
|
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
|
|
18
|
-
* @param
|
|
16
|
+
* Get configurable records from a registration
|
|
17
|
+
* @param registration
|
|
19
18
|
* @constructor
|
|
20
19
|
*/
|
|
21
|
-
export function getConfigurableRecords(
|
|
22
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/dist/esm/lib/dto/DTO.js
CHANGED
|
@@ -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
|
-
|
|
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));
|