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.
- package/README.md +50 -30
- package/README.zh-CN.md +130 -0
- package/dist/cjs/exports/QueueComponent.d.ts +1 -0
- package/dist/cjs/exports/QueueComponent.js +20 -0
- package/dist/esm/exports/QueueComponent.js +1 -0
- package/dist/types/exports/QueueComponent.d.ts +1 -0
- package/doc/en/Application.md +107 -0
- package/doc/en/CLI.md +274 -0
- package/doc/en/Cache.md +379 -0
- package/doc/en/Command.md +110 -0
- package/doc/en/ContainerdIntegration.md +17 -16
- package/doc/en/Controllers.md +256 -0
- package/doc/en/DBus.md +291 -0
- package/doc/en/DTOValidation.md +72 -43
- package/doc/en/Database.md +188 -0
- package/doc/en/DatabaseDrivers.md +287 -0
- package/doc/en/Decorator.md +52 -40
- package/doc/en/DockerIntegration.md +7 -6
- package/doc/en/Entities.md +240 -0
- package/doc/en/Entrypoint.md +216 -0
- package/doc/en/Exceptions.md +174 -0
- package/doc/en/GRPC.md +251 -0
- package/doc/en/GettingStarted.md +165 -0
- package/doc/en/HTTP.md +511 -0
- package/doc/en/HelperFunctions.md +11 -11
- package/doc/en/I18n.md +119 -115
- package/doc/en/Injection.md +144 -0
- package/doc/en/Installers.md +280 -0
- package/doc/en/Logger.md +206 -0
- package/doc/en/MCP.md +293 -0
- package/doc/en/Middlewares.md +213 -0
- package/doc/en/Migrations.md +202 -0
- package/doc/en/MongoEmbedded.md +149 -0
- package/doc/en/Monitor.md +164 -0
- package/doc/en/Mutex.md +27 -25
- package/doc/en/NATS.md +246 -0
- package/doc/en/NativeAddons.md +212 -0
- package/doc/en/Objects.md +143 -0
- package/doc/en/Overview.md +63 -85
- package/doc/en/Packaging.md +193 -0
- package/doc/en/ProjectStructure.md +124 -0
- package/doc/en/Queue.md +269 -0
- package/doc/en/Schedules.md +217 -0
- package/doc/en/Security.md +10 -9
- package/doc/en/ServiceCalls.md +508 -0
- package/doc/en/Time.md +19 -20
- package/doc/en/UpgradingFrom1.md +435 -0
- package/doc/en/UpgradingFrom2.md +315 -0
- package/doc/en/WebRTC.md +1364 -0
- package/doc/en/Workspaces.md +189 -0
- package/doc/en/examples/BackgroundJobs.md +677 -0
- package/doc/en/examples/CommandLineTool.md +664 -0
- package/doc/en/examples/DesktopApp.md +809 -0
- package/doc/en/examples/DeviceService.md +607 -0
- package/doc/en/examples/MCPServer.md +653 -0
- package/doc/en/examples/Meeting.md +573 -0
- package/doc/en/examples/README.md +17 -0
- package/doc/en/examples/RestAPI.md +490 -0
- package/doc/en/examples/ServiceCalls.md +829 -0
- package/doc/en/examples/WebApp.md +568 -0
- package/doc/zh/Application.md +75 -0
- package/doc/zh/CLI.md +250 -0
- package/doc/zh/Cache.md +328 -0
- package/doc/zh/Command.md +78 -0
- package/doc/zh/ContainerdIntegration.md +9 -9
- package/doc/zh/Controllers.md +228 -0
- package/doc/zh/DBus.md +257 -0
- package/doc/zh/DTOValidation.md +49 -19
- package/doc/zh/Database.md +175 -0
- package/doc/zh/DatabaseDrivers.md +261 -0
- package/doc/zh/Decorator.md +25 -18
- package/doc/zh/DockerIntegration.md +6 -6
- package/doc/zh/Entities.md +224 -0
- package/doc/zh/Entrypoint.md +193 -0
- package/doc/zh/Exceptions.md +158 -0
- package/doc/zh/GRPC.md +226 -0
- package/doc/zh/GettingStarted.md +148 -0
- package/doc/zh/HTTP.md +446 -0
- package/doc/zh/HelperFunctions.md +2 -2
- package/doc/zh/I18n.md +26 -26
- package/doc/zh/Injection.md +129 -0
- package/doc/zh/Installers.md +232 -0
- package/doc/zh/Logger.md +188 -0
- package/doc/zh/MCP.md +265 -0
- package/doc/zh/Middlewares.md +198 -0
- package/doc/zh/Migrations.md +178 -0
- package/doc/zh/MongoEmbedded.md +118 -0
- package/doc/zh/Monitor.md +145 -0
- package/doc/zh/Mutex.md +10 -10
- package/doc/zh/NATS.md +207 -0
- package/doc/zh/NativeAddons.md +180 -0
- package/doc/zh/Objects.md +119 -0
- package/doc/zh/Overview.md +47 -71
- package/doc/zh/Packaging.md +154 -0
- package/doc/zh/ProjectStructure.md +102 -0
- package/doc/zh/Queue.md +214 -0
- package/doc/zh/Schedules.md +193 -0
- package/doc/zh/Security.md +6 -6
- package/doc/zh/ServiceCalls.md +462 -0
- package/doc/zh/Time.md +2 -2
- package/doc/zh/UpgradingFrom1.md +347 -0
- package/doc/zh/UpgradingFrom2.md +230 -0
- package/doc/zh/WebRTC.md +1049 -0
- package/doc/zh/Workspaces.md +150 -0
- package/doc/zh/examples/BackgroundJobs.md +618 -0
- package/doc/zh/examples/CommandLineTool.md +618 -0
- package/doc/zh/examples/DesktopApp.md +748 -0
- package/doc/zh/examples/DeviceService.md +546 -0
- package/doc/zh/examples/MCPServer.md +604 -0
- package/doc/zh/examples/Meeting.md +524 -0
- package/doc/zh/examples/README.md +15 -0
- package/doc/zh/examples/RestAPI.md +458 -0
- package/doc/zh/examples/ServiceCalls.md +765 -0
- package/doc/zh/examples/WebApp.md +520 -0
- package/package.json +46 -17
- package/doc/en/ActionPattern.md +0 -408
- package/doc/en/Core.md +0 -312
- package/doc/en/ORMIntegration.md +0 -341
- package/doc/zh/ActionPattern.md +0 -334
- package/doc/zh/Core.md +0 -256
- 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.
|
package/doc/en/DTOValidation.md
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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,
|
|
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
|
|
57
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
73
|
-
const
|
|
74
|
-
const
|
|
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
|
|
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
|
|
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`
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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;
|
|
149
|
-
- **Pagination** (`lakutata/dtos`): `PaginationSearchDTO` and `PaginationResultDTO` are the input and the result of
|
|
150
|
-
paginated actions, `paginator` computes the pages; `FlexibleDTO` is a DTO
|
|
151
|
-
- **Validation messages**:
|
|
152
|
-
`
|
|
153
|
-
|
|
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,
|
|
174
|
-
validators accept
|
|
175
|
-
conversions) is
|
|
176
|
-
-
|
|
177
|
-
|
|
178
|
-
- The numeric types
|
|
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 {
|
|
204
|
+
import {readFileSync} from 'node:fs'
|
|
205
|
+
import {DTO, type JSONSchema} from 'lakutata'
|
|
185
206
|
import {fromJsonSchema} from 'lakutata/dtos'
|
|
186
207
|
|
|
187
|
-
const
|
|
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
|
|
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
|
-
-
|
|
199
|
-
|
|
200
|
-
-
|
|
201
|
-
|
|
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
|
-
-
|
|
204
|
-
exactly, keep its `describe()` output (a JSON value unless it
|
|
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)`.
|