@lakutata/core 3.1.0 → 3.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -169,6 +169,7 @@ await container.destroy()
169
169
  | `Before`, `After` | `lakutata/decorator/asst` | Functions run before and after a method |
170
170
  | `PaginationSearchDTO`, `PaginationResultDTO`, `paginator`, `FlexibleDTO`, `toJsonSchema`, `fromJsonSchema` | `lakutata/dtos` | Pagination, open DTOs and JSON Schema conversion |
171
171
  | `ConsoleLogger`, `ILogger` | `@lakutata/core/com/logger` | The logger interface and its console implementation |
172
+ | `IInterfaceListening`, `IInterfaceListener`, `IInterfaceListenOptions`, `IListeningAddress`, `NetworkInterfaceSelector` | `lakutata` | The contract of the listening on network interfaces, which the entrypoints use for their `interface` option and `@lakutata/netif` implements (types only) |
172
173
 
173
174
  The typings document each declaration with examples.
174
175
 
package/README.zh-CN.md CHANGED
@@ -158,6 +158,7 @@ await container.destroy()
158
158
  | `Before`、`After` | `lakutata/decorator/asst` | 在方法之前和之后执行的函数 |
159
159
  | `PaginationSearchDTO`、`PaginationResultDTO`、`paginator`、`FlexibleDTO`、`toJsonSchema`、`fromJsonSchema` | `lakutata/dtos` | 分页、接受任意字段的 DTO、JSON Schema 转换 |
160
160
  | `ConsoleLogger`、`ILogger` | `@lakutata/core/com/logger` | 日志记录器接口及其控制台实现 |
161
+ | `IInterfaceListening`、`IInterfaceListener`、`IInterfaceListenOptions`、`IListeningAddress`、`NetworkInterfaceSelector` | `lakutata` | 按网口监听的接口:入口的 `interface` 选项通过它监听,`@lakutata/netif` 实现了它(只有类型) |
161
162
 
162
163
  类型声明中的每一项都附有说明和示例。
163
164
 
@@ -5,7 +5,8 @@ import { Schema } from '../../lib/validation/types/Schema.js';
5
5
  * Declare the schema of a property of a DTO class. The DTO validates the property against it when it is constructed
6
6
  * (the whole object at once: defaults applied, the keys without schema removed unless the class has an
7
7
  * `@IndexSignature`), and each time the property, or a value nested in it, is changed afterwards (the change is undone
8
- * when invalid). The schema is also part of the class schema (`MyDTO.Schema()`) used by `@Accept`, `@Return` and the
8
+ * when invalid). With `DTO.Any()`, an object of another class the property holds is given as it is, not watched: no
9
+ * change of it can be invalid. The schema is also part of the class schema (`MyDTO.Schema()`) used by `@Accept`, `@Return` and the
9
10
  * actions' input validation. Subclasses inherit the property schemas, and may declare a property again to replace its
10
11
  * schema. Ignored on a symbol property or on a class which is not a DTO.
11
12
  * @param schema The schema of the property, built with the static methods of `DTO` (`DTO.String().required()`); a
@@ -6,7 +6,8 @@ const ObjectSchemaValidation_js_1 = require("../../lib/dto/ObjectSchemaValidatio
6
6
  * Declare the schema of a property of a DTO class. The DTO validates the property against it when it is constructed
7
7
  * (the whole object at once: defaults applied, the keys without schema removed unless the class has an
8
8
  * `@IndexSignature`), and each time the property, or a value nested in it, is changed afterwards (the change is undone
9
- * when invalid). The schema is also part of the class schema (`MyDTO.Schema()`) used by `@Accept`, `@Return` and the
9
+ * when invalid). With `DTO.Any()`, an object of another class the property holds is given as it is, not watched: no
10
+ * change of it can be invalid. The schema is also part of the class schema (`MyDTO.Schema()`) used by `@Accept`, `@Return` and the
10
11
  * actions' input validation. Subclasses inherit the property schemas, and may declare a property again to replace its
11
12
  * schema. Ignored on a symbol property or on a class which is not a DTO.
12
13
  * @param schema The schema of the property, built with the static methods of `DTO` (`DTO.String().required()`); a
@@ -90,4 +90,5 @@ export * from '../types/ModuleOptions.js';
90
90
  */
91
91
  export * from '../interfaces/IBaseObjectConstructor.js';
92
92
  export * from '../interfaces/ILogger.js';
93
+ export * from '../interfaces/IInterfaceListening.js';
93
94
  export * from '../interfaces/IConstructor.js';
@@ -121,6 +121,7 @@ __exportStar(require("../types/ModuleOptions.js"), exports);
121
121
  */
122
122
  __exportStar(require("../interfaces/IBaseObjectConstructor.js"), exports);
123
123
  __exportStar(require("../interfaces/ILogger.js"), exports);
124
+ __exportStar(require("../interfaces/IInterfaceListening.js"), exports);
124
125
  __exportStar(require("../interfaces/IConstructor.js"), exports);
125
126
 
126
127
  //The default of the entry: the entry itself (scripts/package-build/cjs-package.mjs)
@@ -0,0 +1,159 @@
1
+ import type { Server } from 'node:net';
2
+ /**
3
+ * A network interface, as an implementation of {@link IInterfaceListening} takes it: its name (`'eth1'`; on Windows the
4
+ * name of the network connection), its hardware address (`{mac: '52:54:00:12:34:56'}`) or its index (`{index: 3}`).
5
+ */
6
+ export type NetworkInterfaceSelector = string | {
7
+ /**
8
+ * The hardware address of the interface.
9
+ */
10
+ mac: string;
11
+ } | {
12
+ /**
13
+ * The index of the interface.
14
+ */
15
+ index: number;
16
+ };
17
+ /**
18
+ * An address a listener of a network interface can be reached at.
19
+ */
20
+ export interface IListeningAddress {
21
+ /**
22
+ * The address (an IPv6 address without its scope).
23
+ */
24
+ address: string;
25
+ /**
26
+ * `'IPv4'` or `'IPv6'`.
27
+ */
28
+ family: 'IPv4' | 'IPv6';
29
+ /**
30
+ * The port.
31
+ */
32
+ port: number;
33
+ /**
34
+ * IPv6 only: the scope of the address (the index of the interface for a link-local one), 0 for a global one.
35
+ */
36
+ scopeid?: number;
37
+ }
38
+ /**
39
+ * The options of {@link IInterfaceListening.listenOnAll}.
40
+ */
41
+ export interface IInterfaceListenOptions {
42
+ /**
43
+ * The port; 0 lets the system choose one (for one interface only).
44
+ */
45
+ port: number;
46
+ /**
47
+ * The families listened on.
48
+ * @default 'both'
49
+ */
50
+ family?: 'IPv4' | 'IPv6' | 'both';
51
+ /**
52
+ * Refuse to listen where the connections reaching the addresses of the interface through another interface cannot
53
+ * be refused, and stop listening while they cannot. An implementation that never refuses them throws.
54
+ * @default false
55
+ */
56
+ strict?: boolean;
57
+ }
58
+ /**
59
+ * The listener of a network interface for a server: it accepts the connections of the interface and gives them to the
60
+ * server, whatever the addresses of the interface, again when they change and when the interface is unplugged and
61
+ * plugged back. The server itself does not listen. The `InterfaceListener` of `@lakutata/netif` implements it.
62
+ */
63
+ export interface IInterfaceListener {
64
+ /**
65
+ * The interface listened on: its name (`undefined` while it does not exist), and whether it is the loopback one.
66
+ */
67
+ readonly networkInterface: {
68
+ readonly name: string | undefined;
69
+ readonly loopback: boolean;
70
+ };
71
+ /**
72
+ * The port, `undefined` until a port 0 is given one by the system.
73
+ */
74
+ readonly port: number | undefined;
75
+ /**
76
+ * The addresses it can be reached at now: none while the interface does not exist or has no address.
77
+ */
78
+ readonly addresses: IListeningAddress[];
79
+ /**
80
+ * Whether the interface is listened on now.
81
+ */
82
+ readonly listening: boolean;
83
+ /**
84
+ * Whether the system refuses the connections that reach the addresses of the interface through another interface.
85
+ */
86
+ readonly enforced: boolean;
87
+ /**
88
+ * Why `enforced` is `false`, when it is.
89
+ */
90
+ readonly reason: string | undefined;
91
+ /**
92
+ * Whether it was closed.
93
+ */
94
+ readonly closed: boolean;
95
+ /**
96
+ * Stop listening; the connections established are left to the server.
97
+ */
98
+ close(): Promise<void>;
99
+ /**
100
+ * The interface is listened on, again after a change.
101
+ * @param event `'listening'`.
102
+ * @param listener Called without argument.
103
+ */
104
+ on(event: 'listening', listener: () => void): this;
105
+ /**
106
+ * A failure of listening again, retried by the listener; with `strict`, the guarantee lost.
107
+ * @param event `'error'`.
108
+ * @param listener Called with the error.
109
+ */
110
+ on(event: 'error', listener: (error: Error) => void): this;
111
+ /**
112
+ * `enforced` changed.
113
+ * @param event `'enforced'`.
114
+ * @param listener Called with its new value.
115
+ */
116
+ on(event: 'enforced', listener: (enforced: boolean) => void): this;
117
+ /**
118
+ * The listener was closed.
119
+ * @param event `'close'`.
120
+ * @param listener Called without argument.
121
+ */
122
+ on(event: 'close', listener: () => void): this;
123
+ /**
124
+ * Remove a listener of an event.
125
+ * @param event The event.
126
+ * @param listener The listener given to `on()`.
127
+ */
128
+ off(event: 'listening' | 'error' | 'enforced' | 'close', listener: (...args: any[]) => void): this;
129
+ }
130
+ /**
131
+ * Listening on network interfaces instead of addresses: what the entrypoints use for their `interface` option. The
132
+ * package `@lakutata/netif` implements it, and is the one used by default; another implementation is given in the
133
+ * `interfaceListening` option of the entrypoints.
134
+ * @example
135
+ * ```typescript
136
+ * import type {IInterfaceListener, IInterfaceListening} from 'lakutata'
137
+ * import {listenOnAll} from '@lakutata/netif'
138
+ *
139
+ * //An implementation that writes each listener made before giving it to the entrypoint
140
+ * const logged: IInterfaceListening = {
141
+ * listenOnAll: async (selectors, server, options): Promise<IInterfaceListener[]> => {
142
+ * const listeners: IInterfaceListener[] = await listenOnAll(selectors, server, options)
143
+ * for (const listener of listeners) console.log(listener.networkInterface.name, listener.addresses)
144
+ * return listeners
145
+ * }
146
+ * }
147
+ * ```
148
+ */
149
+ export interface IInterfaceListening {
150
+ /**
151
+ * Listen on interfaces for a server, one listener by interface; when one cannot be listened on, the others are
152
+ * closed and the error is thrown. An interface that does not exist yet is listened on once it appears.
153
+ * @param selectors The interfaces; several need a port other than 0.
154
+ * @param server The server the connections are given to.
155
+ * @param options The port, the families, and whether the other interfaces must be refused.
156
+ * @returns The listeners, in the order of the interfaces.
157
+ */
158
+ listenOnAll(selectors: NetworkInterfaceSelector | NetworkInterfaceSelector[], server: Server, options: IInterfaceListenOptions): Promise<IInterfaceListener[]>;
159
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,18 +1,18 @@
1
1
  import { BaseObject } from '../BaseObject.js';
2
+ import { type Resolver } from '../../ioc/Resolvers.js';
2
3
  /**
3
- * Set configurable records to target constructor with its registration name
4
- * @param target
5
- * @param registrationName
4
+ * Set configurable records to a registration (the options it was declared with)
5
+ * @param registration
6
6
  * @param configurableRecords
7
7
  * @constructor
8
8
  */
9
- export declare function setConfigurableRecords<ClassConstructor extends typeof BaseObject>(target: ClassConstructor, registrationName: string | symbol, configurableRecords: Record<string, any>): void;
9
+ export declare function setConfigurableRecords(registration: Resolver<any>, configurableRecords: Record<string, any>): void;
10
10
  /**
11
- * Get configurable records from target constructor by its registration name
12
- * @param target
11
+ * Get configurable records from a registration
12
+ * @param registration
13
13
  * @constructor
14
14
  */
15
- export declare function getConfigurableRecords<ClassConstructor extends typeof BaseObject>(target: ClassConstructor, registrationName: string | symbol): Record<string, any>;
15
+ export declare function getConfigurableRecords(registration: Resolver<any> | null): Record<string, any>;
16
16
  /**
17
17
  * Set id to instance
18
18
  * @param target
@@ -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;
@@ -190,6 +241,11 @@ function dynamicProxyProperty(target, prop, receiver, validateFn) {
190
241
  apply: (method, _thisArg, args) => Reflect.apply(method, target, args)
191
242
  });
192
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;
193
249
  return new Proxy(propertyValue, {
194
250
  set(target, p, value, receiver) {
195
251
  const state = propertyState(target, p);
@@ -204,7 +260,7 @@ function dynamicProxyProperty(target, prop, receiver, validateFn) {
204
260
  }
205
261
  },
206
262
  get(target, p, receiver) {
207
- return dynamicProxyProperty(target, p, receiver, validateFn);
263
+ return dynamicProxyProperty(target, p, receiver, validateFn, acceptsAny, ownerKey);
208
264
  },
209
265
  deleteProperty: (target, prop) => {
210
266
  const state = propertyState(target, prop);
@@ -262,12 +318,22 @@ let DTO = DTO_1 = class DTO extends DataValidator_js_1.DataValidator {
262
318
  * Create a DTO from its data, validated against the class schema: the defaults applied, the keys without schema
263
319
  * removed (unless the class has an `@IndexSignature`). The instance then validates each change of a property, or
264
320
  * of a value nested in it (an array, an object): an invalid change throws an `InvalidValueException` and is
265
- * 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.
266
324
  * @param props The data of the DTO. Default: no data (`{}`).
267
325
  * @throws {InvalidValueException} When the data does not match the class schema.
268
326
  */
269
327
  constructor(props = {}) {
270
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
+ };
271
337
  //Create DTO proxy object
272
338
  const DTOInstanceProxy = new Proxy(this, {
273
339
  set: (target, prop, value, receiver) => {
@@ -283,7 +349,7 @@ let DTO = DTO_1 = class DTO extends DataValidator_js_1.DataValidator {
283
349
  else {
284
350
  validatePropertyValue(this, p, Reflect.get(target, p));
285
351
  }
286
- }),
352
+ }, acceptsAny, p),
287
353
  deleteProperty: (target, prop) => {
288
354
  if (!(0, IsSymbol_1.isSymbol)(prop))
289
355
  validatePropertyValue(this, prop, void (0));
@@ -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
@@ -61,4 +61,5 @@ export * from '../types/ModuleOptions.js';
61
61
  */
62
62
  export * from '../interfaces/IBaseObjectConstructor.js';
63
63
  export * from '../interfaces/ILogger.js';
64
+ export * from '../interfaces/IInterfaceListening.js';
64
65
  export * from '../interfaces/IConstructor.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -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;
@@ -187,6 +238,11 @@ function dynamicProxyProperty(target, prop, receiver, validateFn) {
187
238
  apply: (method, _thisArg, args) => Reflect.apply(method, target, args)
188
239
  });
189
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;
190
246
  return new Proxy(propertyValue, {
191
247
  set(target, p, value, receiver) {
192
248
  const state = propertyState(target, p);
@@ -201,7 +257,7 @@ function dynamicProxyProperty(target, prop, receiver, validateFn) {
201
257
  }
202
258
  },
203
259
  get(target, p, receiver) {
204
- return dynamicProxyProperty(target, p, receiver, validateFn);
260
+ return dynamicProxyProperty(target, p, receiver, validateFn, acceptsAny, ownerKey);
205
261
  },
206
262
  deleteProperty: (target, prop) => {
207
263
  const state = propertyState(target, prop);
@@ -259,12 +315,22 @@ let DTO = DTO_1 = class DTO extends DataValidator {
259
315
  * Create a DTO from its data, validated against the class schema: the defaults applied, the keys without schema
260
316
  * removed (unless the class has an `@IndexSignature`). The instance then validates each change of a property, or
261
317
  * of a value nested in it (an array, an object): an invalid change throws an `InvalidValueException` and is
262
- * 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.
263
321
  * @param props The data of the DTO. Default: no data (`{}`).
264
322
  * @throws {InvalidValueException} When the data does not match the class schema.
265
323
  */
266
324
  constructor(props = {}) {
267
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
+ };
268
334
  //Create DTO proxy object
269
335
  const DTOInstanceProxy = new Proxy(this, {
270
336
  set: (target, prop, value, receiver) => {
@@ -280,7 +346,7 @@ let DTO = DTO_1 = class DTO extends DataValidator {
280
346
  else {
281
347
  validatePropertyValue(this, p, Reflect.get(target, p));
282
348
  }
283
- }),
349
+ }, acceptsAny, p),
284
350
  deleteProperty: (target, prop) => {
285
351
  if (!isSymbol(prop))
286
352
  validatePropertyValue(this, prop, void (0));
@@ -5,7 +5,8 @@ import { Schema } from '../../lib/validation/types/Schema.js';
5
5
  * Declare the schema of a property of a DTO class. The DTO validates the property against it when it is constructed
6
6
  * (the whole object at once: defaults applied, the keys without schema removed unless the class has an
7
7
  * `@IndexSignature`), and each time the property, or a value nested in it, is changed afterwards (the change is undone
8
- * when invalid). The schema is also part of the class schema (`MyDTO.Schema()`) used by `@Accept`, `@Return` and the
8
+ * when invalid). With `DTO.Any()`, an object of another class the property holds is given as it is, not watched: no
9
+ * change of it can be invalid. The schema is also part of the class schema (`MyDTO.Schema()`) used by `@Accept`, `@Return` and the
9
10
  * actions' input validation. Subclasses inherit the property schemas, and may declare a property again to replace its
10
11
  * schema. Ignored on a symbol property or on a class which is not a DTO.
11
12
  * @param schema The schema of the property, built with the static methods of `DTO` (`DTO.String().required()`); a
@@ -90,4 +90,5 @@ export * from '../types/ModuleOptions.js';
90
90
  */
91
91
  export * from '../interfaces/IBaseObjectConstructor.js';
92
92
  export * from '../interfaces/ILogger.js';
93
+ export * from '../interfaces/IInterfaceListening.js';
93
94
  export * from '../interfaces/IConstructor.js';
@@ -0,0 +1,159 @@
1
+ import type { Server } from 'node:net';
2
+ /**
3
+ * A network interface, as an implementation of {@link IInterfaceListening} takes it: its name (`'eth1'`; on Windows the
4
+ * name of the network connection), its hardware address (`{mac: '52:54:00:12:34:56'}`) or its index (`{index: 3}`).
5
+ */
6
+ export type NetworkInterfaceSelector = string | {
7
+ /**
8
+ * The hardware address of the interface.
9
+ */
10
+ mac: string;
11
+ } | {
12
+ /**
13
+ * The index of the interface.
14
+ */
15
+ index: number;
16
+ };
17
+ /**
18
+ * An address a listener of a network interface can be reached at.
19
+ */
20
+ export interface IListeningAddress {
21
+ /**
22
+ * The address (an IPv6 address without its scope).
23
+ */
24
+ address: string;
25
+ /**
26
+ * `'IPv4'` or `'IPv6'`.
27
+ */
28
+ family: 'IPv4' | 'IPv6';
29
+ /**
30
+ * The port.
31
+ */
32
+ port: number;
33
+ /**
34
+ * IPv6 only: the scope of the address (the index of the interface for a link-local one), 0 for a global one.
35
+ */
36
+ scopeid?: number;
37
+ }
38
+ /**
39
+ * The options of {@link IInterfaceListening.listenOnAll}.
40
+ */
41
+ export interface IInterfaceListenOptions {
42
+ /**
43
+ * The port; 0 lets the system choose one (for one interface only).
44
+ */
45
+ port: number;
46
+ /**
47
+ * The families listened on.
48
+ * @default 'both'
49
+ */
50
+ family?: 'IPv4' | 'IPv6' | 'both';
51
+ /**
52
+ * Refuse to listen where the connections reaching the addresses of the interface through another interface cannot
53
+ * be refused, and stop listening while they cannot. An implementation that never refuses them throws.
54
+ * @default false
55
+ */
56
+ strict?: boolean;
57
+ }
58
+ /**
59
+ * The listener of a network interface for a server: it accepts the connections of the interface and gives them to the
60
+ * server, whatever the addresses of the interface, again when they change and when the interface is unplugged and
61
+ * plugged back. The server itself does not listen. The `InterfaceListener` of `@lakutata/netif` implements it.
62
+ */
63
+ export interface IInterfaceListener {
64
+ /**
65
+ * The interface listened on: its name (`undefined` while it does not exist), and whether it is the loopback one.
66
+ */
67
+ readonly networkInterface: {
68
+ readonly name: string | undefined;
69
+ readonly loopback: boolean;
70
+ };
71
+ /**
72
+ * The port, `undefined` until a port 0 is given one by the system.
73
+ */
74
+ readonly port: number | undefined;
75
+ /**
76
+ * The addresses it can be reached at now: none while the interface does not exist or has no address.
77
+ */
78
+ readonly addresses: IListeningAddress[];
79
+ /**
80
+ * Whether the interface is listened on now.
81
+ */
82
+ readonly listening: boolean;
83
+ /**
84
+ * Whether the system refuses the connections that reach the addresses of the interface through another interface.
85
+ */
86
+ readonly enforced: boolean;
87
+ /**
88
+ * Why `enforced` is `false`, when it is.
89
+ */
90
+ readonly reason: string | undefined;
91
+ /**
92
+ * Whether it was closed.
93
+ */
94
+ readonly closed: boolean;
95
+ /**
96
+ * Stop listening; the connections established are left to the server.
97
+ */
98
+ close(): Promise<void>;
99
+ /**
100
+ * The interface is listened on, again after a change.
101
+ * @param event `'listening'`.
102
+ * @param listener Called without argument.
103
+ */
104
+ on(event: 'listening', listener: () => void): this;
105
+ /**
106
+ * A failure of listening again, retried by the listener; with `strict`, the guarantee lost.
107
+ * @param event `'error'`.
108
+ * @param listener Called with the error.
109
+ */
110
+ on(event: 'error', listener: (error: Error) => void): this;
111
+ /**
112
+ * `enforced` changed.
113
+ * @param event `'enforced'`.
114
+ * @param listener Called with its new value.
115
+ */
116
+ on(event: 'enforced', listener: (enforced: boolean) => void): this;
117
+ /**
118
+ * The listener was closed.
119
+ * @param event `'close'`.
120
+ * @param listener Called without argument.
121
+ */
122
+ on(event: 'close', listener: () => void): this;
123
+ /**
124
+ * Remove a listener of an event.
125
+ * @param event The event.
126
+ * @param listener The listener given to `on()`.
127
+ */
128
+ off(event: 'listening' | 'error' | 'enforced' | 'close', listener: (...args: any[]) => void): this;
129
+ }
130
+ /**
131
+ * Listening on network interfaces instead of addresses: what the entrypoints use for their `interface` option. The
132
+ * package `@lakutata/netif` implements it, and is the one used by default; another implementation is given in the
133
+ * `interfaceListening` option of the entrypoints.
134
+ * @example
135
+ * ```typescript
136
+ * import type {IInterfaceListener, IInterfaceListening} from 'lakutata'
137
+ * import {listenOnAll} from '@lakutata/netif'
138
+ *
139
+ * //An implementation that writes each listener made before giving it to the entrypoint
140
+ * const logged: IInterfaceListening = {
141
+ * listenOnAll: async (selectors, server, options): Promise<IInterfaceListener[]> => {
142
+ * const listeners: IInterfaceListener[] = await listenOnAll(selectors, server, options)
143
+ * for (const listener of listeners) console.log(listener.networkInterface.name, listener.addresses)
144
+ * return listeners
145
+ * }
146
+ * }
147
+ * ```
148
+ */
149
+ export interface IInterfaceListening {
150
+ /**
151
+ * Listen on interfaces for a server, one listener by interface; when one cannot be listened on, the others are
152
+ * closed and the error is thrown. An interface that does not exist yet is listened on once it appears.
153
+ * @param selectors The interfaces; several need a port other than 0.
154
+ * @param server The server the connections are given to.
155
+ * @param options The port, the families, and whether the other interfaces must be refused.
156
+ * @returns The listeners, in the order of the interfaces.
157
+ */
158
+ listenOnAll(selectors: NetworkInterfaceSelector | NetworkInterfaceSelector[], server: Server, options: IInterfaceListenOptions): Promise<IInterfaceListener[]>;
159
+ }
@@ -1,18 +1,18 @@
1
1
  import { BaseObject } from '../BaseObject.js';
2
+ import { type Resolver } from '../../ioc/Resolvers.js';
2
3
  /**
3
- * Set configurable records to target constructor with its registration name
4
- * @param target
5
- * @param registrationName
4
+ * Set configurable records to a registration (the options it was declared with)
5
+ * @param registration
6
6
  * @param configurableRecords
7
7
  * @constructor
8
8
  */
9
- export declare function setConfigurableRecords<ClassConstructor extends typeof BaseObject>(target: ClassConstructor, registrationName: string | symbol, configurableRecords: Record<string, any>): void;
9
+ export declare function setConfigurableRecords(registration: Resolver<any>, configurableRecords: Record<string, any>): void;
10
10
  /**
11
- * Get configurable records from target constructor by its registration name
12
- * @param target
11
+ * Get configurable records from a registration
12
+ * @param registration
13
13
  * @constructor
14
14
  */
15
- export declare function getConfigurableRecords<ClassConstructor extends typeof BaseObject>(target: ClassConstructor, registrationName: string | symbol): Record<string, any>;
15
+ export declare function getConfigurableRecords(registration: Resolver<any> | null): Record<string, any>;
16
16
  /**
17
17
  * Set id to instance
18
18
  * @param target
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lakutata/core",
3
- "version": "3.1.0",
3
+ "version": "3.1.4",
4
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
5
  "keywords": [
6
6
  "lakutata",
@@ -107,7 +107,7 @@
107
107
  "test:engine-types": "node scripts/check-engine-types.mjs"
108
108
  },
109
109
  "dependencies": {
110
- "@lakutata/utils": "3.1.0",
110
+ "@lakutata/utils": "3.1.1",
111
111
  "reflect-metadata": "0.2.2"
112
112
  },
113
113
  "engines": {