lakutata 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 (121) hide show
  1. package/README.md +50 -30
  2. package/README.zh-CN.md +130 -0
  3. package/dist/cjs/exports/QueueComponent.d.ts +1 -0
  4. package/dist/cjs/exports/QueueComponent.js +20 -0
  5. package/dist/esm/exports/QueueComponent.js +1 -0
  6. package/dist/types/exports/QueueComponent.d.ts +1 -0
  7. package/doc/en/Application.md +107 -0
  8. package/doc/en/CLI.md +274 -0
  9. package/doc/en/Cache.md +379 -0
  10. package/doc/en/Command.md +110 -0
  11. package/doc/en/ContainerdIntegration.md +17 -16
  12. package/doc/en/Controllers.md +256 -0
  13. package/doc/en/DBus.md +291 -0
  14. package/doc/en/DTOValidation.md +72 -43
  15. package/doc/en/Database.md +188 -0
  16. package/doc/en/DatabaseDrivers.md +287 -0
  17. package/doc/en/Decorator.md +52 -40
  18. package/doc/en/DockerIntegration.md +7 -6
  19. package/doc/en/Entities.md +240 -0
  20. package/doc/en/Entrypoint.md +216 -0
  21. package/doc/en/Exceptions.md +174 -0
  22. package/doc/en/GRPC.md +251 -0
  23. package/doc/en/GettingStarted.md +165 -0
  24. package/doc/en/HTTP.md +511 -0
  25. package/doc/en/HelperFunctions.md +11 -11
  26. package/doc/en/I18n.md +119 -115
  27. package/doc/en/Injection.md +144 -0
  28. package/doc/en/Installers.md +280 -0
  29. package/doc/en/Logger.md +206 -0
  30. package/doc/en/MCP.md +293 -0
  31. package/doc/en/Middlewares.md +213 -0
  32. package/doc/en/Migrations.md +202 -0
  33. package/doc/en/MongoEmbedded.md +149 -0
  34. package/doc/en/Monitor.md +164 -0
  35. package/doc/en/Mutex.md +27 -25
  36. package/doc/en/NATS.md +246 -0
  37. package/doc/en/NativeAddons.md +212 -0
  38. package/doc/en/Objects.md +143 -0
  39. package/doc/en/Overview.md +63 -85
  40. package/doc/en/Packaging.md +193 -0
  41. package/doc/en/ProjectStructure.md +124 -0
  42. package/doc/en/Queue.md +269 -0
  43. package/doc/en/Schedules.md +217 -0
  44. package/doc/en/Security.md +10 -9
  45. package/doc/en/ServiceCalls.md +508 -0
  46. package/doc/en/Time.md +19 -20
  47. package/doc/en/UpgradingFrom1.md +435 -0
  48. package/doc/en/UpgradingFrom2.md +315 -0
  49. package/doc/en/WebRTC.md +1364 -0
  50. package/doc/en/Workspaces.md +189 -0
  51. package/doc/en/examples/BackgroundJobs.md +677 -0
  52. package/doc/en/examples/CommandLineTool.md +664 -0
  53. package/doc/en/examples/DesktopApp.md +809 -0
  54. package/doc/en/examples/DeviceService.md +607 -0
  55. package/doc/en/examples/MCPServer.md +653 -0
  56. package/doc/en/examples/Meeting.md +573 -0
  57. package/doc/en/examples/README.md +17 -0
  58. package/doc/en/examples/RestAPI.md +490 -0
  59. package/doc/en/examples/ServiceCalls.md +829 -0
  60. package/doc/en/examples/WebApp.md +568 -0
  61. package/doc/zh/Application.md +75 -0
  62. package/doc/zh/CLI.md +250 -0
  63. package/doc/zh/Cache.md +328 -0
  64. package/doc/zh/Command.md +78 -0
  65. package/doc/zh/ContainerdIntegration.md +9 -9
  66. package/doc/zh/Controllers.md +228 -0
  67. package/doc/zh/DBus.md +257 -0
  68. package/doc/zh/DTOValidation.md +49 -19
  69. package/doc/zh/Database.md +175 -0
  70. package/doc/zh/DatabaseDrivers.md +261 -0
  71. package/doc/zh/Decorator.md +25 -18
  72. package/doc/zh/DockerIntegration.md +6 -6
  73. package/doc/zh/Entities.md +224 -0
  74. package/doc/zh/Entrypoint.md +193 -0
  75. package/doc/zh/Exceptions.md +158 -0
  76. package/doc/zh/GRPC.md +226 -0
  77. package/doc/zh/GettingStarted.md +148 -0
  78. package/doc/zh/HTTP.md +446 -0
  79. package/doc/zh/HelperFunctions.md +2 -2
  80. package/doc/zh/I18n.md +26 -26
  81. package/doc/zh/Injection.md +129 -0
  82. package/doc/zh/Installers.md +232 -0
  83. package/doc/zh/Logger.md +188 -0
  84. package/doc/zh/MCP.md +265 -0
  85. package/doc/zh/Middlewares.md +198 -0
  86. package/doc/zh/Migrations.md +178 -0
  87. package/doc/zh/MongoEmbedded.md +118 -0
  88. package/doc/zh/Monitor.md +145 -0
  89. package/doc/zh/Mutex.md +10 -10
  90. package/doc/zh/NATS.md +207 -0
  91. package/doc/zh/NativeAddons.md +180 -0
  92. package/doc/zh/Objects.md +119 -0
  93. package/doc/zh/Overview.md +47 -71
  94. package/doc/zh/Packaging.md +154 -0
  95. package/doc/zh/ProjectStructure.md +102 -0
  96. package/doc/zh/Queue.md +214 -0
  97. package/doc/zh/Schedules.md +193 -0
  98. package/doc/zh/Security.md +6 -6
  99. package/doc/zh/ServiceCalls.md +462 -0
  100. package/doc/zh/Time.md +2 -2
  101. package/doc/zh/UpgradingFrom1.md +347 -0
  102. package/doc/zh/UpgradingFrom2.md +230 -0
  103. package/doc/zh/WebRTC.md +1049 -0
  104. package/doc/zh/Workspaces.md +150 -0
  105. package/doc/zh/examples/BackgroundJobs.md +618 -0
  106. package/doc/zh/examples/CommandLineTool.md +618 -0
  107. package/doc/zh/examples/DesktopApp.md +748 -0
  108. package/doc/zh/examples/DeviceService.md +546 -0
  109. package/doc/zh/examples/MCPServer.md +604 -0
  110. package/doc/zh/examples/Meeting.md +524 -0
  111. package/doc/zh/examples/README.md +15 -0
  112. package/doc/zh/examples/RestAPI.md +458 -0
  113. package/doc/zh/examples/ServiceCalls.md +765 -0
  114. package/doc/zh/examples/WebApp.md +520 -0
  115. package/package.json +46 -17
  116. package/doc/en/ActionPattern.md +0 -408
  117. package/doc/en/Core.md +0 -312
  118. package/doc/en/ORMIntegration.md +0 -341
  119. package/doc/zh/ActionPattern.md +0 -334
  120. package/doc/zh/Core.md +0 -256
  121. package/doc/zh/ORMIntegration.md +0 -319
package/doc/en/DBus.md ADDED
@@ -0,0 +1,291 @@
1
+ # Lakutata D-Bus
2
+
3
+ D-Bus is the message bus of the Linux desktops and system services: the programs of a machine call each other's
4
+ methods, read each other's properties and listen to each other's signals through it. The package `@lakutata/dbus`
5
+ serves controller methods as the methods of a D-Bus service, exposes objects, properties and signals, and calls the
6
+ other services as a client. It is not part of `lakutata`:
7
+
8
+ ```bash
9
+ npm install lakutata @lakutata/dbus
10
+ ```
11
+
12
+ It needs a D-Bus daemon: the session bus or the system bus of a Linux machine, or a private bus started with
13
+ `dbus-daemon` (for the tests, and on macOS with the `dbus` package of Homebrew).
14
+
15
+ ## 1. A service
16
+
17
+ ```typescript
18
+ import {Application, DTO} from 'lakutata'
19
+ import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
20
+ import {Expect, Return} from 'lakutata/decorator/dto'
21
+ import {dbusEntrypoint, DBusInterface, DBusMethod} from '@lakutata/dbus'
22
+
23
+ class ConfigureDTO extends DTO {
24
+ @Expect(DTO.String().required())
25
+ public sn: string
26
+
27
+ @Expect(DTO.Number().integer().min(0).optional())
28
+ public retries?: number
29
+ }
30
+
31
+ @DBusInterface('com.example.Manager', {path: '/com/example/Manager'})
32
+ class ManagerController extends Controller {
33
+ //No argument, returns a string (s)
34
+ @DBusMethod('Ping', {returns: 's'})
35
+ public async ping(): Promise<string> {
36
+ return 'pong'
37
+ }
38
+
39
+ //One argument a{sv}, returns a dictionary a{sv} (the type of @Return)
40
+ @DBusMethod('Configure', ConfigureDTO)
41
+ @Return(ConfigureDTO)
42
+ public async configure(input: ConfigureDTO): Promise<ConfigureDTO> {
43
+ return input
44
+ }
45
+ }
46
+
47
+ Application.run(() => ({
48
+ id: 'devices.app',
49
+ name: 'Devices',
50
+ components: {
51
+ entrypoint: buildEntrypoints({
52
+ controllers: [ManagerController],
53
+ dbus: dbusEntrypoint({bus: 'session', service: 'com.example.DeviceService'})
54
+ })
55
+ },
56
+ bootstrap: ['entrypoint']
57
+ }))
58
+ ```
59
+
60
+ ```text
61
+ $ gdbus call --session --dest com.example.DeviceService --object-path /com/example/Manager --method com.example.Manager.Ping
62
+ ('pong',)
63
+ $ gdbus call --session --dest com.example.DeviceService --object-path /com/example/Manager --method com.example.Manager.Configure "{'sn': <'A1'>, 'retries': <3>}"
64
+ ({'sn': <'A1'>, 'retries': <3>},)
65
+ ```
66
+
67
+ - `@DBusInterface(name, {path})` makes a controller an interface, served at an object path. Several controllers with
68
+ the same path are the interfaces of the same object.
69
+ - `@DBusMethod(name, DTO?, options?)` makes a method of the controller a method of the interface. Its options are
70
+ those of every action, and `returns`, the type of its result (section 3).
71
+ - Every object also implements `org.freedesktop.DBus.Properties`, `Introspectable` and `Peer`; the root object `/`
72
+ implements `ObjectManager`.
73
+ - On a machine with systemd, `busctl --user call com.example.DeviceService /com/example/Manager com.example.Manager
74
+ Ping` does the same as the first `gdbus` command.
75
+
76
+ ## 2. The adapter
77
+
78
+ | Option | Default | Meaning |
79
+ |---|---|---|
80
+ | `service` | required | The well-known name of the service: elements of letters, digits and `_`, separated by dots |
81
+ | `bus` | `'session'` | `'session'` (the address in `DBUS_SESSION_BUS_ADDRESS`), `'system'`, an address (`'unix:path=…'`, `'tcp:host=…,port=…'`), or the path of the socket of a private bus |
82
+ | `replaceExisting` | `false` | Take the name from its owner, when the owner allows it |
83
+ | `allowReplacement` | `false` | Let another service take the name |
84
+ | `decode` | `{int64: 'number'}` | How the arguments are decoded (section 3) |
85
+ | `name`, `middlewares` | | The name of the adapter, and the middlewares of its calls, after those of the entrypoint |
86
+
87
+ - The adapter connects and takes the name when the application starts. A bus that cannot be reached (for `'session'`,
88
+ also a `DBUS_SESSION_BUS_ADDRESS` not set), or a name owned by another connection, makes the start fail.
89
+ - D-Bus allows `-` in the names of the services, but this adapter does not: the name of the service prefixes the names
90
+ of the errors, which may not hold it.
91
+ - On the system bus, a policy file in `/etc/dbus-1/system.d/` must allow the process to own the name.
92
+ - With `allowReplacement`, a service that takes the name with `replaceExisting` receives the calls from then on. The
93
+ replaced application keeps running without the name, and does not get it back when the other one stops.
94
+ - When the application stops, the adapter releases the name (new calls fail with
95
+ `org.freedesktop.DBus.Error.ServiceUnknown`), lets the calls in progress end, then disconnects.
96
+
97
+ ## 3. Arguments and results
98
+
99
+ **The input.** A method with a DTO takes one argument, a dictionary of variants (`a{sv}`) holding the properties of the
100
+ DTO; a method without a DTO takes no argument. The input goes through the middlewares, the validation and the access
101
+ control, as on the other transports. A variant accepts any type, so a dictionary of strings (`a{ss}`) is accepted too.
102
+
103
+ | D-Bus type | JavaScript value |
104
+ |---|---|
105
+ | `s`, `o` | A string |
106
+ | `y`, `n`, `i`, `u`, `d` | A number |
107
+ | `x`, `t` (64 bits) | A number when it is a safe integer, else a bigint |
108
+ | `b` | A boolean |
109
+ | `as`, `ai`… | An array |
110
+ | `a{…}` | An object |
111
+ | `(…)` | An array |
112
+
113
+ The 64-bit integers are decoded as numbers so that `DTO.Number()` accepts them; `decode: {int64: 'bigint'}` keeps them
114
+ as bigints, for `DTO.BigInt()`.
115
+
116
+ **The result** has the type of the `returns` option: a signature such as `'s'`, or a DTO, sent as `a{sv}`. Without
117
+ `returns`, it has the type of the schema of `@Return`:
118
+
119
+ | `@Return` schema | D-Bus type |
120
+ |---|---|
121
+ | An object, a DTO | `a{sv}` |
122
+ | A string | `s` |
123
+ | A boolean | `b` |
124
+ | An integer (`.integer()`) | `x` |
125
+ | Another number | `d` |
126
+ | A bigint | `x` |
127
+ | An array | `av` |
128
+ | Anything else | `v` |
129
+
130
+ - Without `returns` and `@Return`, the method has no result.
131
+ - The dates are sent as ISO strings, and the `null` and `undefined` values are left out of the objects and arrays: a
132
+ variant has no null. A method that returns `a{sv}` and returns nothing sends `{}`.
133
+ - A method whose result is a single type (`'s'`, `'i'`…) must return a value: `undefined` fails the call.
134
+ - The `Map`s, the `Buffer`s and the typed values of `dbus-sdk` (`new DBusUInt32(7)`) are sent as they are.
135
+
136
+ ## 4. Objects, properties and signals
137
+
138
+ The path of an interface is either a path (`/com/example/Manager`), whose object is exported when the adapter starts,
139
+ or a template (`/com/example/Devices/{sn}`), whose objects the application adds and removes with `DBusObjectManager`.
140
+ The variables of the path are merged into the input of the methods, over the values sent by the client.
141
+
142
+ ```typescript
143
+ import {Component, DTO} from 'lakutata'
144
+ import {Controller} from 'lakutata/com/entrypoint'
145
+ import {Expect, Return} from 'lakutata/decorator/dto'
146
+ import {Inject} from 'lakutata/decorator/di'
147
+ import {
148
+ DBusInterface,
149
+ DBusMethod,
150
+ DBusObjectManager,
151
+ defineDBusProperty,
152
+ defineDBusSignal,
153
+ type DBusPropertyWrite
154
+ } from '@lakutata/dbus'
155
+
156
+ class DeviceSnDTO extends DTO {
157
+ @Expect(DTO.String().required())
158
+ public sn: string
159
+ }
160
+
161
+ class StatusDTO extends DTO {
162
+ @Expect(DTO.String().required())
163
+ public sn: string
164
+
165
+ @Expect(DTO.Boolean().required())
166
+ public online: boolean
167
+ }
168
+
169
+ export const DeviceChanged = defineDBusSignal<StatusDTO>('Changed', StatusDTO)
170
+ export const DeviceState = defineDBusProperty<string>('State', 's', {initial: 'idle'})
171
+ export const DeviceLevel = defineDBusProperty<number>('Level', 'i', {writable: true, initial: 1, schema: DTO.Number().integer().min(0).max(10)})
172
+
173
+ @DBusInterface('com.example.Device', {path: '/com/example/Devices/{sn}', signals: [DeviceChanged], properties: [DeviceState, DeviceLevel]})
174
+ export class DeviceController extends Controller {
175
+ //The sn of the input is the one of the object path
176
+ @DBusMethod('Status', DeviceSnDTO)
177
+ @Return(StatusDTO)
178
+ public async status(input: DeviceSnDTO): Promise<StatusDTO> {
179
+ return {sn: input.sn, online: true} as StatusDTO
180
+ }
181
+ }
182
+
183
+ //Declared in the components of the application
184
+ export class Devices extends Component {
185
+ @Inject(DBusObjectManager)
186
+ protected readonly manager: DBusObjectManager
187
+
188
+ protected async init(): Promise<void> {
189
+ this.manager.on('propertyWritten', (write: DBusPropertyWrite): void => {
190
+ if (write.property === DeviceLevel) console.log(`${write.path}: level ${write.value}`)
191
+ })
192
+ }
193
+
194
+ public plug(sn: string): void {
195
+ const path: string = `/com/example/Devices/${sn}`
196
+ this.manager.addObject(path)
197
+ this.manager.setPropertyValue(path, DeviceState, 'running')
198
+ this.manager.emitSignal(path, DeviceChanged, {sn: sn, online: true} as StatusDTO)
199
+ }
200
+
201
+ public unplug(sn: string): void {
202
+ this.manager.removeObject(`/com/example/Devices/${sn}`)
203
+ }
204
+ }
205
+ ```
206
+
207
+ - `DBusObjectManager` is a component of every application: inject it (`@Inject(DBusObjectManager)`) without declaring
208
+ it. The objects added before the adapter starts are exported when it starts. The clients learn of the objects added
209
+ and removed through the signals `InterfacesAdded` and `InterfacesRemoved` of the root object.
210
+ - A property keeps one value per object, its `initial` value until it is set. `setPropertyValue()` validates the value
211
+ with the `schema` of the property, and tells the clients of the change (`PropertiesChanged`).
212
+ - A client may write a `writable` property: the value is validated (the error is given to the client), kept, and
213
+ `propertyWritten` is emitted. A write that must reach a device, or that may fail after its validation, is better a
214
+ method, whose errors reach the caller.
215
+ - `emitSignal()` validates the data with the DTO of the signal, and returns whether an object at that path sends it.
216
+ - The signatures of the properties and of the signals are checked when they are defined.
217
+
218
+ ## 5. Errors
219
+
220
+ An error thrown by a method, its validation or its access control included, reaches the caller as a D-Bus error, with
221
+ its message:
222
+
223
+ | Error thrown | D-Bus error name |
224
+ |---|---|
225
+ | An exception with an `errno` (`E_OFFLINE`) | `com.example.DeviceService.E_OFFLINE` |
226
+ | An `errno` that holds dots (`org.example.Device.Offline`) | `org.example.Device.Offline` |
227
+ | An `errno` that does not start with a letter (`500`, `_HIDDEN`) | `com.example.DeviceService.E500`, `….E_HIDDEN` |
228
+ | An error without `errno` (`Error`, `TypeError`) | `com.example.DeviceService.Error`, `….TypeError` |
229
+
230
+ The other cases are answered before any action runs: `org.freedesktop.DBus.Error.UnknownObject` for an object that does
231
+ not exist, `UnknownMethod` for a method that does not exist, `InvalidArgs` for arguments of the wrong type; the bus
232
+ answers `ServiceUnknown` when no application owns the name.
233
+
234
+ ## 6. The context and the access control
235
+
236
+ In a D-Bus method, `this.context` is a `DBusContext`: its `type` is `'DBUS'` (`DBUS_CONTEXT_TYPE`), `path` is the path
237
+ of the object called, `interface` and `member` name the method, and `data` holds the dictionary received, before the
238
+ validation.
239
+
240
+ The access rules of the entrypoint apply to the methods declared with `acl: true` when their `supportContextTypes()`
241
+ include `DBUS_CONTEXT_TYPE` (see [Middlewares](Middlewares.md)). The context does not tell who calls: a rule cannot
242
+ check the caller, which is the job of the bus policy on the system bus.
243
+
244
+ ## 7. The client
245
+
246
+ `DBusClientComponent` connects to a bus when it is created, to call the services, read their properties and listen to
247
+ their signals:
248
+
249
+ ```typescript
250
+ import {Application} from 'lakutata'
251
+ import {DBusClientComponent, type DBusRemoteInterface} from '@lakutata/dbus'
252
+
253
+ Application
254
+ .run(() => ({
255
+ id: 'client.app',
256
+ name: 'Client',
257
+ components: {
258
+ dbus: {class: DBusClientComponent, busAddress: process.env.DBUS_SESSION_BUS_ADDRESS!, callTimeout: 5000}
259
+ }
260
+ }))
261
+ .onLaunched(async (app: Application): Promise<void> => {
262
+ const dbus: DBusClientComponent = await app.getObject('dbus')
263
+ //A call with its signature: the output arguments, as an array
264
+ const [status] = await dbus.invoke({
265
+ service: 'com.example.DeviceService',
266
+ objectPath: '/com/example/Devices/A1',
267
+ interface: 'com.example.Device',
268
+ method: 'Status',
269
+ signature: 'a{sv}',
270
+ args: [{sn: 'A1'}]
271
+ })
272
+ console.log(status)
273
+ //An interface built from the introspection of its object
274
+ const device: DBusRemoteInterface = await dbus.interface('com.example.DeviceService', '/com/example/Devices/A1', 'com.example.Device')
275
+ console.log(await device.property.State.get())
276
+ device.signal.on('Changed', (data: unknown): void => console.log(data))
277
+ })
278
+ ```
279
+
280
+ | Option | Default | Meaning |
281
+ |---|---|---|
282
+ | `busAddress` | required | The address of the bus (`'unix:path=/var/run/dbus/system_bus_socket'`), not `'session'` nor a path |
283
+ | `timeout` | 20 seconds | How long the connection may take, in milliseconds |
284
+ | `callTimeout` | none | How long a call may wait for its reply, in milliseconds; a call can give its own `timeout` |
285
+ | `decode` | `{int64: 'number'}` | How the replies and the signals are decoded |
286
+
287
+ - `invoke()` resolves with the array of the output arguments; the methods of an interface (`device.method.Status()`)
288
+ resolve with the value itself.
289
+ - A call that fails rejects with an `Error` whose `name` is the D-Bus error name
290
+ (`com.example.DeviceService.E_OFFLINE`); it has no `errno`.
291
+ - The component disconnects when the application stops.
@@ -1,10 +1,9 @@
1
1
  # Lakutata DTO and Validation
2
2
 
3
- A **DTO** (data transfer object) is a class whose properties are declared with validation schemas. The framework
4
- validates with them the options of the objects (`@Configurable`), the arguments and results of methods (`@Accept`,
5
- `@Return`) and the inputs of the controller actions. `DTO` is exported by `lakutata`, the decorators by
6
- `lakutata/decorator/dto`; the validation engine is the framework's own, with the API of joi (extended with bigints and
7
- the numeric types below).
3
+ A **DTO** (data transfer object) is a class whose properties are declared with validation schemas. The framework uses
4
+ DTOs to validate the options of objects (`@Configurable`), the arguments and results of methods (`@Accept`, `@Return`)
5
+ and the input of controller actions. `DTO` is exported by `lakutata` and the decorators by `lakutata/decorator/dto`; the
6
+ validation engine is the framework's own, with the API of joi (extended with bigints and the numeric types below).
8
7
 
9
8
  ## 1. Defining a DTO
10
9
 
@@ -45,16 +44,16 @@ class CustomerDTO extends DTO {
45
44
  The schemas are built by the static methods of `DTO`: `DTO.String()`, `DTO.Number()`, `DTO.BigInt()`, `DTO.Boolean()`,
46
45
  `DTO.Date()`, `DTO.Binary()`, `DTO.Symbol()`, `DTO.Object()`, `DTO.Array()`, `DTO.Alternatives()`, `DTO.Any()`,
47
46
  `DTO.Function()`, `DTO.AsyncFunction()`, `DTO.Class()`, `DTO.InstanceOf()`, `DTO.Cron()`, `DTO.Glob()`, `DTO.Ref()`,
48
- `DTO.Link()`… Their rules are the ones of joi (`.required()`, `.optional()`, `.default()`, `.valid()`, `.min()`,
47
+ `DTO.Link()`… Their rules are those of joi (`.required()`, `.optional()`, `.default()`, `.valid()`, `.min()`,
49
48
  `.max()`, `.pattern()`, `.email()`, `.uri()`…).
50
49
 
51
50
  - **Strict by default**: a value of the wrong type is rejected, not converted (the string `'5'` is not the number 5).
52
- Call `.strict(false)` to convert, as for the query strings and route parameters of HTTP, which are always strings.
51
+ Call `.strict(false)` to convert, for example for HTTP query strings and route parameters, which are always strings.
53
52
  - **Numeric types**: `DTO.Number()` provides the integer types `int8`, `uint8`, `int16`, `uint16`, `int32`, `uint32`,
54
53
  `int64` and `uint64` (the 64-bit types limited to the safe integers of JavaScript), and the floating point types
55
54
  `float` (32-bit range) and `double`.
56
- - **Validation options**: by default the validation stops at the first error (`abortEarly`), and the unknown keys are
57
- removed (`stripUnknown`). `MyDTO.options({...})` gives other options for a DTO used as a schema.
55
+ - **Validation options**: by default, validation stops at the first error (`abortEarly`) and unknown keys are removed
56
+ (`stripUnknown`). `MyDTO.options({...})` returns the schema of a DTO with other options.
58
57
  - **A DTO class as a schema**: `MyDTO.Schema()` is its object schema, `MyDTO.required()` and `MyDTO.optional()` the same
59
58
  schema marked required or optional. A DTO class is accepted wherever a schema is expected (`@Accept`, `@Return`, the
60
59
  action decorators).
@@ -62,18 +61,30 @@ The schemas are built by the static methods of `DTO`: `DTO.String()`, `DTO.Numbe
62
61
  ## 2. Validating
63
62
 
64
63
  ```typescript
65
- //Validated when constructed: the defaults applied, the unknown key "note" removed;
66
- //an InvalidValueException is thrown when the data is invalid
67
- const customer: CustomerDTO = new CustomerDTO({name: 'Ada', email: 'ada@example.com', address: {city: 'London'}, note: 'vip'})
64
+ import {DTO} from 'lakutata'
65
+ import {Expect} from 'lakutata/decorator/dto'
66
+
67
+ class CustomerDTO extends DTO {
68
+ @Expect(DTO.String().min(2).required())
69
+ public name: string
70
+
71
+ @Expect(DTO.Number().integer().min(0).default(0))
72
+ public credit: number
73
+ }
74
+
75
+ //Validated when constructed: defaults are applied and the unknown key "note" is removed;
76
+ //invalid data throws an InvalidValueException
77
+ const customer: CustomerDTO = new CustomerDTO({name: 'Ada', note: 'vip'})
68
78
 
69
79
  //A DTO instance validates each change of its properties: an invalid change throws and is undone
70
80
  customer.credit = 10
71
81
 
72
- //Validated without an instance: the validated value, or an InvalidValueException
73
- const data: CustomerDTO = CustomerDTO.validate({name: 'Ada', email: 'ada@example.com', address: {city: 'London'}})
74
- const later: CustomerDTO = await CustomerDTO.validateAsync({name: 'Ada', email: 'ada@example.com', address: {city: 'London'}})
82
+ //Validated without creating an instance: returns the validated data (a plain object) or throws an InvalidValueException
83
+ const received: any = JSON.parse('{"name": "Ada"}')
84
+ const data: CustomerDTO = CustomerDTO.validate(received)
85
+ const later: CustomerDTO = await CustomerDTO.validateAsync(received)
75
86
 
76
- //Checked without exception
87
+ //Checked without throwing
77
88
  const valid: boolean = CustomerDTO.isValid({name: ''})
78
89
 
79
90
  //Any value against any schema
@@ -88,13 +99,13 @@ own by default) and the validation options.
88
99
  | Decorator | Applies to | Effect |
89
100
  |---|---|---|
90
101
  | `@Expect(schema)` | A property of a DTO | Declares the schema of the property |
91
- | `@IndexSignature(schema)` | A DTO class | Keeps the keys without `@Expect` schema, validated against the schema, instead of removing them |
102
+ | `@IndexSignature(schema)` | A DTO class | Keeps the keys that have no `@Expect` schema, validating them against this schema, instead of removing them |
92
103
  | `@Accept(...schemas)` | A method | Validates the arguments of each call, by position; throws `InvalidMethodAcceptException` |
93
104
  | `@Return(schema)` | A method | Validates the result of each call (awaited for an async method); throws `InvalidMethodReturnException` |
94
105
 
95
- `@Accept` and `@Return` give the method (or its caller) the validated values: defaults applied, unknown keys removed.
96
- A DTO class validates an argument or a result against its schema, and the value given is a plain object, not an
97
- instance of the class. A missing argument is accepted unless its schema is required (`MyDTO.required()`).
106
+ `@Accept` and `@Return` pass the validated values on to the method (or to its caller), with defaults applied and unknown
107
+ keys removed. When a DTO class validates an argument or a result, the value passed on is a plain object, not an instance
108
+ of the class. A missing argument is accepted unless its schema is required (`MyDTO.required()`).
98
109
 
99
110
  ```typescript
100
111
  import {Component, DTO} from 'lakutata'
@@ -142,15 +153,15 @@ class Bank extends Component {
142
153
 
143
154
  ## 4. DTOs in the framework
144
155
 
145
- - **Object options**: `@Configurable(schema)` (from `lakutata/decorator/di`) validates an option of a component,
146
- provider or module; see [Core](Core.md).
156
+ - **Object options**: `@Configurable(schema)` (from `lakutata/decorator/di`) validates an option of a component or a
157
+ provider; see [Injection](Injection.md).
147
158
  - **Controller actions**: the DTO given to `@GET`, `@POST`, `@CLIAction`, `@ServiceAction`… validates the input of the
148
- action; an invalid input is answered with the status 400. See [ActionPattern](ActionPattern.md).
149
- - **Pagination** (`lakutata/dtos`): `PaginationSearchDTO` and `PaginationResultDTO` are the input and the result of the
150
- paginated actions, `paginator` computes the pages; `FlexibleDTO` is a DTO accepting any key.
151
- - **Validation messages**: the messages of the errors are translated; `registerValidationMessages`,
152
- `runInValidationLanguage` and `currentValidationLanguage` (from `lakutata`) manage their languages, and the language
153
- middleware of the I18n component sets the language of each request (see [I18n](I18n.md)).
159
+ action; invalid input gets a 400 response. See [Controllers](Controllers.md).
160
+ - **Pagination** (`lakutata/dtos`): `PaginationSearchDTO` and `PaginationResultDTO` are the input and the result of
161
+ paginated actions, and `paginator` computes the pages; `FlexibleDTO` is a DTO that accepts any key.
162
+ - **Validation messages**: error messages can be translated; `registerValidationMessages`, `runInValidationLanguage` and
163
+ `currentValidationLanguage` (from `lakutata`) manage their languages, and the language middleware of the I18n
164
+ component sets the language of each request (see [I18n](I18n.md)).
154
165
 
155
166
  ## 5. JSON Schema and OpenAPI
156
167
 
@@ -162,44 +173,62 @@ documents.
162
173
 
163
174
  ```typescript
164
175
  import {DTO} from 'lakutata'
176
+ import {Expect} from 'lakutata/decorator/dto'
165
177
  import {toJsonSchema} from 'lakutata/dtos'
166
178
 
179
+ class UserDTO extends DTO {
180
+ @Expect(DTO.Number().integer().required())
181
+ public id: number
182
+
183
+ @Expect(DTO.String().required())
184
+ public name: string
185
+ }
186
+
167
187
  UserDTO.toJsonSchema() //JSON Schema 2020-12 (default)
168
188
  UserDTO.toJsonSchema('draft-07') //'draft-2020-12' | 'draft-2019-09' | 'draft-07' | 'draft-04'
169
189
  UserDTO.toOpenAPIJsonSchema() //OpenAPI 3.1 (default), or '3.0'
170
190
  toJsonSchema(DTO.String().email(), {target: 'openapi-3.0'}) //any schema, or a DTO class
171
191
  ```
172
192
 
173
- - The JSON Schema describes the values the schema accepts without conversion, so ajv (with ajv-formats) and other
174
- validators accept exactly these values; what JSON Schema cannot express (custom rules, references to other values,
175
- conversions) is dropped, so the JSON Schema may accept more values, never fewer.
176
- - The unknown keys follow the validation options: with the defaults of lakutata (`allowUnknown`, `stripUnknown`) the
177
- objects accept them; pass `preferences` to convert for other options.
178
- - The numeric types convert to integer bounds (`int8` to `uint64`) and float ranges; OpenAPI also names the formats
193
+ - The JSON Schema describes the values the schema accepts without conversion, and ajv (with ajv-formats) and other
194
+ validators accept those values. What JSON Schema cannot express (custom rules, references to other values,
195
+ conversions) is left out, so the JSON Schema may also accept values the schema rejects, never the reverse.
196
+ - Unknown keys follow the validation options: with the defaults of lakutata (`allowUnknown`, `stripUnknown`), objects
197
+ accept them; pass `preferences` to convert a schema for other options.
198
+ - The numeric types become integer bounds (`int8` to `uint64`) and float ranges; in OpenAPI they also get the formats
179
199
  `int32`, `int64`, `float` and `double`.
180
200
 
181
201
  ### 5.2 Import
182
202
 
183
203
  ```typescript
184
- import {DTO} from 'lakutata'
204
+ import {readFileSync} from 'node:fs'
205
+ import {DTO, type JSONSchema} from 'lakutata'
185
206
  import {fromJsonSchema} from 'lakutata/dtos'
186
207
 
187
- const schema = fromJsonSchema({type: 'object', properties: {id: {type: 'integer'}}, required: ['id']})
208
+ const userJsonSchema: JSONSchema = {
209
+ type: 'object',
210
+ properties: {id: {type: 'integer'}, name: {type: 'string'}},
211
+ required: ['id']
212
+ }
213
+
214
+ //A schema
215
+ const schema = fromJsonSchema(userJsonSchema)
188
216
  DTO.validate({id: 1}, schema)
189
217
 
190
- //A DTO class, a subclass of the class it is created from
218
+ //A DTO class, a subclass of the class on which fromJsonSchema() is called
191
219
  const UserDTO = DTO.fromJsonSchema(userJsonSchema)
192
220
  const user = new UserDTO({id: 1, name: 'Alice'})
193
221
 
194
222
  //The DTO classes of the object schemas of an OpenAPI document (components.schemas), by name
223
+ const openApiDocument: Record<string, unknown> = JSON.parse(readFileSync('openapi.json', 'utf8'))
195
224
  const {Pet, Person} = DTO.fromOpenAPI(openApiDocument)
196
225
  ```
197
226
 
198
- - The imported schemas validate like JSON Schema: the values are not converted (`convert: true` converts them), the
199
- unknown keys follow `additionalProperties`, the formats validate like ajv-formats, and the defaults are applied.
200
- - The references resolve by JSON pointer, `$anchor` and `$id` (pass other documents with `schemas`, an OpenAPI document
201
- with `document`); recursive references are supported.
227
+ - Imported schemas validate as JSON Schema does: values are not converted (unless `convert: true` is given), unknown
228
+ keys follow `additionalProperties`, formats are checked as ajv-formats checks them, and defaults are applied.
229
+ - References are resolved by JSON pointer, `$anchor` and `$id` (pass other documents in `schemas`, and an OpenAPI
230
+ document in `document`); recursive references are supported.
202
231
  - The version is read from `$schema`, or given with `dialect` (default: 2020-12).
203
- - JSON Schema is lossy for the schemas of lakutata (custom rules, conversions): to store a schema and restore it
204
- exactly, keep its `describe()` output (a JSON value unless it holds functions) and rebuild it with
232
+ - Converting a lakutata schema to JSON Schema loses information (custom rules, conversions): to store a schema and
233
+ restore it exactly, keep its `describe()` output (a JSON value unless it contains functions) and rebuild it with
205
234
  `DTO.Build(description)`.