diff --git a/README.md b/README.md index 6200191f7..c30da7439 100644 --- a/README.md +++ b/README.md @@ -56,13 +56,9 @@ For further information please refer to the official [W3C Web of Things](https:/ ## Installation -The framework can be used in two ways: as a library or as a CLI tool. In this section we will explain how to install the framework in both ways. +The framework is composed by different packages that users can use as they please. The core package is @node-wot/core and it is the only mandatory package to install. The other packages are bindings that allow the framework to communicate with different protocols. -### As a library - -The framework is composed by different packages that users can use as they please. The core package is `@node-wot/core` and it is the only mandatory package to install. The other packages are bindings that allow the framework to communicate with different protocols. - -#### Node.js +### Prerequisite: Node.js with build tools > [!WARNING] > We no longer actively support Node.js version 18 and lower. @@ -81,27 +77,52 @@ Platforms specific prerequisites: - Mac OS: Meet the [node-gyp](https://github.com/nodejs/node-gyp#installation) requirements: - `xcode-select --install` -If you want to use node-wot as a library in your Node.js application, you can use npm to install the node-wot packages that you need. To do so, `cd` inside your application folder, and run: +### As a library + +If you want to use node-wot as a library in your Node.js application, you can use npm to install the node-wot packages that you need. +Todo so, `cd` inside your application folder and install at least the mandatory core package: ``` -npm i @node-wot/core @node-wot/binding-http --save +npm i @node-wot/core ``` -#### Browser +Usually, your application needs at least one protocol binding to commmunicate, e.g.,: -To use node-wot as a browser-side JavaScript Library, the browser needs to support ECMAScript 2015. +``` +npm i @node-wot/binding-http +``` -Using a browser with only ES5 support (e.g., IE 11) might be possible if you add polyfills. If you want to use node-wot as a library in your browser application, you can install the `@node-wot/browser-bundle` as following: +In case the application shall consume Thing Descriptions that are stored locally, you would also need the `file` binding: ``` -npm i @node-wot/browser-bundle --save +npm i @node-wot/binding-file ``` -you can find more installation options in the specific [package README](./packages/browser-bundle/README.md). +You see other available bindings in the [packages folder](./packages), which you can install via `npm i @node-wot/`. + +### In the browser + +To use node-wot as JavaScript library insde the Web browser, it needs to support ECMAScript 2015+. Using a browser with only ES5 support (e.g., IE 11) might be possible if you add polyfills. + +If you want to use node-wot as a library in your browser application,`cd` inside your application folder and install the browser bundle: + +``` +npm i @node-wot/browser-bundle +``` + +You can find more (non-)installation options in the specific [package README](./packages/browser-bundle/README.md). ### As a CLI tool -You can alternatively use node-wot via its command line interface (CLI). Please visit the [CLI tool's Readme](<[url](https://github.com/eclipse-thingweb/node-wot/tree/master/packages/cli)>) to find out more. +You can alternatively use node-wot via its command line interface (CLI). Please visit the [CLI tool README](https://github.com/eclipse-thingweb/node-wot/tree/master/packages/cli) to find out more. + +#### As global tool + +To make the `wot-servient` command available on your machine, install the CLI tool in the global scope: + +``` +npm i @node-wot/cli -g +``` #### As a docker image diff --git a/eslint.config.mjs b/eslint.config.mjs index 6ffc45ab9..e14545ebb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -84,7 +84,7 @@ export default defineConfig([ "n/no-unsupported-features/node-builtins": "off", // https://github.com/eclipse-thingweb/node-wot/issues/1430 "n/no-extraneous-import": "off", // https://github.com/eclipse-thingweb/node-wot/issues/1430 "n/no-deprecated-api": "off", // https://github.com/eclipse-thingweb/node-wot/issues/1430 - "n/no-unpublished-import": "off", // https://github.com/eclipse-thingweb/node-wot/issues/1430 + "n/no-unpublished-import": "error", "n/no-process-exit": "off", // https://github.com/eclipse-thingweb/node-wot/issues/1430 "n/hashbang": "warn", @@ -122,9 +122,9 @@ export default defineConfig([ "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-expressions": "off", "@typescript-eslint/no-require-imports": "off", // https://github.com/eclipse-thingweb/node-wot/issues/1430 - "@typescript-eslint/prefer-nullish-coalescing": "off", // https://github.com/eclipse-thingweb/node-wot/issues/1430 + "@typescript-eslint/prefer-nullish-coalescing": "error", "@typescript-eslint/no-empty-object-type": "off", // https://github.com/eclipse-thingweb/node-wot/issues/1430 - "@typescript-eslint/no-floating-promises": "off", // https://github.com/eclipse-thingweb/node-wot/issues/1430 + "@typescript-eslint/no-floating-promises": "error", // **************** Enforce usage of `const` over `let` wherever possible, to prevent accidental reassignments "prefer-const": "error", @@ -135,7 +135,7 @@ export default defineConfig([ "no-use-before-define": "error", - "no-unused-private-class-members": "off", // https://github.com/eclipse-thingweb/node-wot/issues/1430 + "no-unused-private-class-members": "error", "no-prototype-builtins": "off", "no-case-declarations": "off", diff --git a/package-lock.json b/package-lock.json index a1f7df527..292c5f939 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6488,7 +6488,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -6554,7 +6556,9 @@ } }, "node_modules/json-schema-ref-parser/node_modules/js-yaml": { - "version": "3.14.1", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -6848,7 +6852,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -7721,6 +7727,12 @@ "node-opcua-variant": "2.142.0" } }, + "node_modules/node-opcua-address-space/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/node-opcua-aggregates": { "version": "2.143.0", "license": "MIT", @@ -8427,6 +8439,12 @@ "thenify-ex": "4.4.0" } }, + "node_modules/node-opcua-server/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/node-opcua-service-browse": { "version": "2.143.0", "license": "MIT", @@ -11159,7 +11177,9 @@ } }, "node_modules/tslint/node_modules/js-yaml": { - "version": "3.14.1", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -11658,11 +11678,13 @@ } }, "node_modules/vm2": { - "version": "3.9.18", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.10.0.tgz", + "integrity": "sha512-3ggF4Bs0cw4M7Rxn19/Cv3nJi04xrgHwt4uLto+zkcZocaKwP/nKP9wPx6ggN2X0DSXxOOIc63BV1jvES19wXQ==", "license": "MIT", "dependencies": { - "acorn": "^8.7.0", - "acorn-walk": "^8.2.0" + "acorn": "^8.14.1", + "acorn-walk": "^8.3.4" }, "bin": { "vm2": "bin/vm2" @@ -11872,6 +11894,8 @@ }, "node_modules/wot-thing-description-types": { "version": "1.1.0-12-March-2025", + "resolved": "https://registry.npmjs.org/wot-thing-description-types/-/wot-thing-description-types-1.1.0-12-March-2025.tgz", + "integrity": "sha512-BRRW4uBATljCPqCbpzNhgY2HFXz8Llh7mAJmjkVimxizOPq8bkmlm0Op/Yv8l7pNQ1GaoBtYwn2BQQjhcQCadA==", "license": "W3C-20150513" }, "node_modules/wot-thing-model-types": { @@ -11880,6 +11904,8 @@ }, "node_modules/wot-typescript-definitions": { "version": "0.8.0-SNAPSHOT.31", + "resolved": "https://registry.npmjs.org/wot-typescript-definitions/-/wot-typescript-definitions-0.8.0-SNAPSHOT.31.tgz", + "integrity": "sha512-BBUWfJnNsSMb60FlUfQN+TWFP2W71LM4g7in83Y+vO6BjGvsyO0KY4jTbazjhLdxxTjbyftrExNkKNwtBIbxfA==", "license": "W3C-20150513", "dependencies": { "wot-thing-description-types": "1.1.0-12-March-2025" @@ -12449,8 +12475,8 @@ "ajv": "^8.11.0", "commander": "^9.1.0", "dotenv": "^16.4.7", - "lodash": "^4.17.21", - "vm2": "3.9.18" + "lodash": "^4.17.23", + "vm2": "3.10.0" }, "bin": { "wot-servient": "bin/index.js" @@ -12479,14 +12505,14 @@ "uritemplate": "0.3.4", "url-toolkit": "2.1.6", "uuid": "^7.0.3", - "web-streams-polyfill": "^4.0.0" + "web-streams-polyfill": "^4.0.0", + "wot-thing-description-types": "^1.1.0-12-March-2025" }, "devDependencies": { "@types/content-type": "^1.1.8", "@types/debug": "^4.1.7", "@types/uritemplate": "^0.3.4", - "@types/uuid": "^8.3.1", - "wot-thing-description-types": "^1.1.0-12-March-2025" + "@types/uuid": "^8.3.1" } }, "packages/core/node_modules/ajv-formats": { diff --git a/packages/binding-coap/src/coap-server.ts b/packages/binding-coap/src/coap-server.ts index bcebb179d..d31acfa72 100644 --- a/packages/binding-coap/src/coap-server.ts +++ b/packages/binding-coap/src/coap-server.ts @@ -201,9 +201,7 @@ export default class CoapServer implements ProtocolServer { return; } - if (thing.forms == null) { - thing.forms = []; - } + thing.forms ??= []; const form = this.createAffordanceForm(base, this.PROPERTY_DIR, offeredMediaType, opValues, thing.uriVariables); diff --git a/packages/binding-http/src/http-client-impl.ts b/packages/binding-http/src/http-client-impl.ts index 3344c4afa..584badd64 100644 --- a/packages/binding-http/src/http-client-impl.ts +++ b/packages/binding-http/src/http-client-impl.ts @@ -356,7 +356,7 @@ export default class HttpClient implements ProtocolClient { const url = HttpClient.fixLocalhostName(form.href); - requestInit.method = form["htv:methodName"] ? form["htv:methodName"] : defaultMethod; + requestInit.method = form["htv:methodName"] ?? defaultMethod; requestInit.headers = requestInit.headers ?? []; requestInit.headers = requestInit.headers as string[][]; diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index 83159b2f9..4f4f7dd62 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -64,6 +64,7 @@ export default class HttpServer implements ProtocolServer { private readonly address?: string; private readonly baseUri?: string; private readonly urlRewrite?: Record; + private readonly devFriendlyUri: boolean; private readonly supportedSecuritySchemes: string[] = ["nosec"]; private readonly validOAuthClients: RegExp = /.*/g; private readonly server: http.Server | https.Server; @@ -83,6 +84,7 @@ export default class HttpServer implements ProtocolServer { this.baseUri = config.baseUri; this.urlRewrite = config.urlRewrite; this.middleware = config.middleware; + this.devFriendlyUri = config.devFriendlyUri ?? true; const router = Router({ ignoreTrailingSlash: true, @@ -267,21 +269,32 @@ export default class HttpServer implements ProtocolServer { } public async expose(thing: ExposedThing, tdTemplate: WoT.ExposedThingInit = {}): Promise { - let urlPath = slugify(thing.title, { lower: true }); - - // avoid URL clashes - if (this.things.has(urlPath)) { - let uniqueUrlPath; - let nameClashCnt = 2; - do { - uniqueUrlPath = urlPath + "_" + nameClashCnt++; - } while (this.things.has(uniqueUrlPath)); - urlPath = uniqueUrlPath; - } - if (this.getPort() !== -1) { - debug(`HttpServer on port ${this.getPort()} exposes '${thing.title}' as unique '/${urlPath}'`); - this.things.set(urlPath, thing); + const paths: string[] = []; + // If not id is given we create the path using the title even if devFriendlyUri is false. + // in Thing Description 1.1 id is optional + if (this.devFriendlyUri || thing.id == null) { + let urlPath = slugify(thing.title, { lower: true }); + + // avoid URL clashes + if (this.things.has(urlPath)) { + let uniqueUrlPath; + let nameClashCnt = 2; + do { + uniqueUrlPath = urlPath + "_" + nameClashCnt++; + } while (this.things.has(uniqueUrlPath)); + urlPath = uniqueUrlPath; + } + this.things.set(urlPath, thing); + paths.push(urlPath); + debug("HttpServer on port %d exposes %s as unique '/%s'", this.getPort(), thing.name, urlPath); + } + + if (thing.id != null) { + this.things.set(thing.id, thing); + paths.push(thing.id); + debug("HttpServer on port %d exposes %s as unique '/%s'", this.getPort(), thing.name, thing.id); + } if (this.scheme === "http" && Object.keys(thing.securityDefinitions).length !== 0) { warn(`HTTP Server will attempt to use your security schemes even if you are not using HTTPS.`); @@ -290,16 +303,20 @@ export default class HttpServer implements ProtocolServer { this.fillSecurityScheme(thing); if (this.baseUri !== undefined) { - const base: string = this.baseUri.concat("/", encodeURIComponent(urlPath)); - info("HttpServer TD hrefs using baseUri " + this.baseUri); - this.addEndpoint(thing, tdTemplate, base); + for (const path of paths) { + info("HttpServer TD hrefs using baseUri %s and path %s", this.baseUri, path); + const base: string = this.baseUri.concat("/", encodeURIComponent(path)); + this.addEndpoint(thing, tdTemplate, base); + } } else { // fill in binding data for (const address of Helpers.getAddresses()) { - const base: string = - this.scheme + "://" + address + ":" + this.getPort() + "/" + encodeURIComponent(urlPath); - - this.addEndpoint(thing, tdTemplate, base); + for (const path of paths) { + const base: string = + this.scheme + "://" + address + ":" + this.getPort() + "/" + encodeURIComponent(path); + info("HttpServer TD hrefs using address %s and path %s", address, path); + this.addEndpoint(thing, tdTemplate, base); + } } } } @@ -311,6 +328,7 @@ export default class HttpServer implements ProtocolServer { for (const [name, thing] of this.things.entries()) { if (thing.id === thingId) { this.things.delete(name); + this.things.delete(thingId); info(`HttpServer successfully destroyed '${thing.title}'`); return true; @@ -369,9 +387,7 @@ export default class HttpServer implements ProtocolServer { "writemultipleproperties", ]; } - if (thing.forms == null) { - thing.forms = []; - } + thing.forms ??= []; thing.forms.push(form); this.addUrlRewriteEndpoints(form, thing.forms); } diff --git a/packages/binding-http/src/http.ts b/packages/binding-http/src/http.ts index 0dae9fdd5..5d4b3e839 100644 --- a/packages/binding-http/src/http.ts +++ b/packages/binding-http/src/http.ts @@ -45,6 +45,7 @@ export interface HttpConfig { serverKey?: string; serverCert?: string; security?: SecurityScheme[]; + devFriendlyUri?: boolean; middleware?: MiddlewareRequestHandler; } diff --git a/packages/binding-http/test/http-server-test.ts b/packages/binding-http/test/http-server-test.ts index b03fafe1a..509fc507f 100644 --- a/packages/binding-http/test/http-server-test.ts +++ b/packages/binding-http/test/http-server-test.ts @@ -1020,4 +1020,53 @@ class HttpServerTest { return httpServer.stop(); } + + @test async "should expose Thing with id and title and be reachable from both"() { + const httpServer = new HttpServer({ port: 0 }); + + await httpServer.start(new Servient()); + + const testThing = new ExposedThing(new Servient(), { + title: "TestThing", + id: "urn:dev:wot:test-thing-1234", + properties: { + test: { + type: "string", + forms: [], + }, + }, + actions: { + test: { + output: { type: "string" }, + forms: [], + }, + }, + }); + + await httpServer.expose(testThing); + + const uriByTitle = `http://localhost:${httpServer.getPort()}/testthing`; + const uriById = `http://localhost:${httpServer.getPort()}/urn:dev:wot:test-thing-1234`; + + let resp; + resp = await (await fetch(uriByTitle)).json(); + expect(resp.title).to.be.eq("TestThing"); + expect(resp.properties.test.forms.some((form: { href: string }) => form.href.includes("testthing"))).to.be.true; + expect(resp.actions.test.forms.some((form: { href: string }) => form.href.includes("testthing"))).to.be.true; + + resp = await (await fetch(uriById)).json(); + expect(resp.id).to.be.eq("urn:dev:wot:test-thing-1234"); + expect( + resp.properties.test.forms.some((form: { href: string }) => + form.href.includes(encodeURIComponent("urn:dev:wot:test-thing-1234")) + ) + ).to.be.true; + expect( + resp.actions.test.forms.some((form: { href: string }) => + form.href.includes(encodeURIComponent("urn:dev:wot:test-thing-1234")) + ) + ).to.be.true; + + return httpServer.stop(); + } } diff --git a/packages/binding-modbus/README.md b/packages/binding-modbus/README.md index b19208fba..1593c961f 100644 --- a/packages/binding-modbus/README.md +++ b/packages/binding-modbus/README.md @@ -8,7 +8,20 @@ W3C WoT Binding Template for Modbus can be found [here](https://w3c.github.io/wo Current Maintainer(s): [@relu91](https://github.com/relu91) [@fillobotto](https://github.com/fillobotto) -## Client Example +## Protocol specifier + +The protocol prefix handled by this binding is `modbus+tcp://`. + +## Getting Started + +In the following examples it is shown how to use the Modbus binding of node-wot. + +### Prerequisites + +- `npm install @node-wot/core` +- `npm install @node-wot/binding-modbus` + +### Client Example You can use a code like the following to use the binding. This specific code is interacting with one of the Eclipse Thingweb Test Things at . @@ -42,10 +55,6 @@ main(); ## Binding Information -### Protocol specifier - -The protocol prefix handled by this binding is `modbus+tcp://`. - ### New Form Fields for the Modbus Binding **Note**: for further details please refer to the [documentation](https://github.com/eclipse-thingweb/node-wot/blob/master/packages/binding-modbus/src/modbus.ts). diff --git a/packages/binding-modbus/src/modbus-client.ts b/packages/binding-modbus/src/modbus-client.ts index ad06f03a8..017fbf49f 100644 --- a/packages/binding-modbus/src/modbus-client.ts +++ b/packages/binding-modbus/src/modbus-client.ts @@ -39,11 +39,9 @@ class ModbusSubscription { error?: (error: Error) => void, complete?: () => void ) { - if (!complete) { - complete = () => { - // do nothing. - }; - } + complete ??= () => { + // do nothing. + }; this.interval = global.setInterval(async () => { try { const result = await client.readResource(form); diff --git a/packages/binding-opcua/src/opcua-protocol-client.ts b/packages/binding-opcua/src/opcua-protocol-client.ts index 08bb99278..60afb8745 100644 --- a/packages/binding-opcua/src/opcua-protocol-client.ts +++ b/packages/binding-opcua/src/opcua-protocol-client.ts @@ -257,9 +257,7 @@ export class OPCUAProtocolClient implements ProtocolClient { private async _getNamespaceArray(form: OPCUAForm): Promise { return this._withConnection(form, async (c: OPCUAConnection) => { - if (!c.namespaceArray) { - c.namespaceArray = await readNamespaceArray(c.session); - } + c.namespaceArray ??= await readNamespaceArray(c.session); return c.namespaceArray; }); } diff --git a/packages/cli/package.json b/packages/cli/package.json index 53d9047e3..2fe74df3f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -29,8 +29,8 @@ "ajv": "^8.11.0", "commander": "^9.1.0", "dotenv": "^16.4.7", - "lodash": "^4.17.21", - "vm2": "3.9.18" + "lodash": "^4.17.23", + "vm2": "3.10.0" }, "scripts": { "build": "tsc -b", diff --git a/packages/core/package.json b/packages/core/package.json index 6acad672a..6e8fa3dd3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -18,8 +18,7 @@ "@types/content-type": "^1.1.8", "@types/debug": "^4.1.7", "@types/uritemplate": "^0.3.4", - "@types/uuid": "^8.3.1", - "wot-thing-description-types": "^1.1.0-12-March-2025" + "@types/uuid": "^8.3.1" }, "dependencies": { "@petamoriken/float16": "^3.1.1", @@ -34,7 +33,8 @@ "uritemplate": "0.3.4", "url-toolkit": "2.1.6", "uuid": "^7.0.3", - "web-streams-polyfill": "^4.0.0" + "web-streams-polyfill": "^4.0.0", + "wot-thing-description-types": "^1.1.0-12-March-2025" }, "scripts": { "build": "tsc -b", diff --git a/packages/core/src/consumed-thing.ts b/packages/core/src/consumed-thing.ts index c340b9ad8..d622e9029 100644 --- a/packages/core/src/consumed-thing.ts +++ b/packages/core/src/consumed-thing.ts @@ -65,38 +65,20 @@ export interface ClientAndForm { } class ConsumedThingProperty extends ThingProperty implements ThingProperty, BaseSchema { - #name: string; - #thing: ConsumedThing; - - constructor(name: string, thing: ConsumedThing) { + constructor() { super(); - - this.#name = name; - this.#thing = thing; } } class ConsumedThingAction extends ThingAction implements ThingAction { - #name: string; - #thing: ConsumedThing; - - constructor(name: string, thing: ConsumedThing) { + constructor() { super(); - - this.#name = name; - this.#thing = thing; } } class ConsumedThingEvent extends ThingEvent { - #name: string; - #thing: ConsumedThing; - - constructor(name: string, thing: ConsumedThing) { + constructor() { super(); - - this.#name = name; - this.#thing = thing; } } @@ -306,7 +288,7 @@ class InternalEventSubscription extends InternalSubscription { const formWithoutURIvariables = handleUriVariables(this.thing, te, form, options); debug(`ConsumedThing '${this.thing.title}' unsubscribing to ${form.href}`); - this.client.unlinkResource(formWithoutURIvariables); + await this.client.unlinkResource(formWithoutURIvariables); this.active = false; } @@ -398,15 +380,15 @@ export default class ConsumedThing extends Thing implements IConsumedThing { extendInteractions(): void { for (const [propertyName, property] of Object.entries(this.properties)) { - const newProp = Helpers.extend(property, new ConsumedThingProperty(propertyName, this)); + const newProp = Helpers.extend(property, new ConsumedThingProperty()); this.properties[propertyName] = newProp; } for (const [actionName, action] of Object.entries(this.actions)) { - const newAction = Helpers.extend(action, new ConsumedThingAction(actionName, this)); + const newAction = Helpers.extend(action, new ConsumedThingAction()); this.actions[actionName] = newAction; } for (const [eventName, event] of Object.entries(this.events)) { - const newEvent = Helpers.extend(event, new ConsumedThingEvent(eventName, this)); + const newEvent = Helpers.extend(event, new ConsumedThingEvent()); this.events[eventName] = newEvent; } } diff --git a/packages/core/src/exposed-thing.ts b/packages/core/src/exposed-thing.ts index 51401068d..92e79ae94 100644 --- a/packages/core/src/exposed-thing.ts +++ b/packages/core/src/exposed-thing.ts @@ -595,7 +595,9 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { } const unsubscribe = this.#eventHandlers.get(name)?.unsubscribe; if (unsubscribe) { - unsubscribe(options); + unsubscribe(options).catch((error) => { + throw error; + }); } debug(`ExposedThing '${this.title}' unsubscribes from event '${name}'`); } else { @@ -663,7 +665,9 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { const unobserveHandler = this.#propertyHandlers.get(name)?.unobserveHandler; if (unobserveHandler) { - unobserveHandler(options); + unobserveHandler(options).catch((error) => { + throw error; + }); } } else { throw new Error(`ExposedThing '${this.title}', no property found for '${name}'`); diff --git a/packages/core/src/protocol-helpers.ts b/packages/core/src/protocol-helpers.ts index 1b568e122..97a90a9a1 100644 --- a/packages/core/src/protocol-helpers.ts +++ b/packages/core/src/protocol-helpers.ts @@ -215,17 +215,27 @@ export default class ProtocolHelpers { const reader = stream.getReader(); const result = new ManagedReadable({ read: (size) => { - reader.read().then((data) => { - result.push(data.value); - if (data.done) { - // signal end - result.push(null); - } - }); + reader + .read() + .then((data) => { + result.push(data.value); + if (data.done) { + // signal end + result.push(null); + } + }) + .catch((error) => { + throw error; + }); }, destroy: (error, callback) => { reader.releaseLock(); - stream.cancel(error).then(() => callback(error)); + stream + .cancel(error) + .then(() => callback(error)) + .catch((error) => { + throw error; + }); }, }); result.wotStream = stream as ReadableStream; diff --git a/packages/core/test/client-test.ts b/packages/core/test/client-test.ts index b640b8f5e..824b470c0 100644 --- a/packages/core/test/client-test.ts +++ b/packages/core/test/client-test.ts @@ -358,9 +358,14 @@ class WoTClientTest { this.clientFactory = new TrapClientFactory(); this.servient.addClientFactory(this.clientFactory); this.servient.addClientFactory(new TDClientFactory()); - this.servient.start().then((myWoT) => { - this.WoT = myWoT; - }); + this.servient + .start() + .then((myWoT) => { + this.WoT = myWoT; + }) + .catch((error) => { + throw error; + }); debug("started test suite"); } @@ -570,11 +575,17 @@ class WoTClientTest { expect(thing).to.have.property("title").that.equals("aThing"); expect(thing).to.have.property("events").that.has.property("anEvent"); return new Promise((resolve) => { - thing.subscribeEvent("anEvent", async (x) => { - const value = await x.value(); - expect(value).to.equal("triggered"); - resolve(true); - }); + thing + .subscribeEvent("anEvent", async (x) => { + const value = await x.value(); + expect(value).to.equal("triggered"); + resolve(true); + }) + .catch((error) => { + throw error; + }); + }).catch((error) => { + throw error; }); } @@ -626,17 +637,25 @@ class WoTClientTest { expect(thing).to.have.property("title").that.equals("aThing"); expect(thing).to.have.property("events").that.has.property("anEvent"); - const subscription = await thing.subscribeEvent("anEvent", () => { - /** */ - }); + const subscription = await thing + .subscribeEvent("anEvent", () => { + /** */ + }) + .catch((error) => { + throw error; + }); await subscription.stop(); return new Promise((resolve) => { - thing.subscribeEvent("anEvent", async (x) => { - const value = await x.value(); - expect(value).to.equal("triggered"); - resolve(true); - }); + thing + .subscribeEvent("anEvent", async (x) => { + const value = await x.value(); + expect(value).to.equal("triggered"); + resolve(true); + }) + .catch((error) => { + throw error; + }); }); } @@ -649,11 +668,15 @@ class WoTClientTest { expect(thing).to.have.property("title").that.equals("aThing"); expect(thing).to.have.property("properties").that.has.property("aPropertyToObserve"); return new Promise((resolve) => { - thing.observeProperty("aPropertyToObserve", async (data) => { - const value = await data.value(); - expect(value).to.equal(12); - resolve(true); - }); + thing + .observeProperty("aPropertyToObserve", async (data) => { + const value = await data.value(); + expect(value).to.equal(12); + resolve(true); + }) + .catch((error) => { + throw error; + }); }); } diff --git a/packages/core/test/server-test.ts b/packages/core/test/server-test.ts index 8216aa370..619f54a26 100644 --- a/packages/core/test/server-test.ts +++ b/packages/core/test/server-test.ts @@ -68,9 +68,14 @@ class WoTServerTest { this.servient = new Servient(); this.server = new TestProtocolServer(); this.servient.addServer(this.server); - this.servient.start().then((WoTruntime) => { - this.WoT = WoTruntime; - }); + this.servient + .start() + .then((WoTruntime) => { + this.WoT = WoTruntime; + }) + .catch((error) => { + throw error; + }); debug("started test suite"); } @@ -889,7 +894,9 @@ class WoTServerTest { thing.setPropertyReadHandler("test", callback); - (thing as ExposedThing).handleObserveProperty("test", protocolListener, { formIndex: 0 }); + (thing as ExposedThing).handleObserveProperty("test", protocolListener, { formIndex: 0 }).catch((error) => { + throw error; + }); await (thing).emitPropertyChange("test"); diff --git a/packages/examples/package.json b/packages/examples/package.json index d63ff530a..ee4979e46 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -17,7 +17,18 @@ "build": "tsc -b", "lint": "eslint .", "lint:fix": "eslint . --fix", - "format": "prettier --write \"src/**/*.ts\" \"**/*.json\"" + "format": "prettier --write \"src/**/*.ts\" \"**/*.json\"", + "bindings:coap:server": "node dist/bindings/coap/example-server.js", + "bindings:coap:client": "node dist/bindings/coap/example-client.js", + "bindings:http:server": "node dist/bindings/http/example-server.js", + "bindings:http:server-secure": "node dist/bindings/http/example-server-secure.js", + "bindings:http:client": "node dist/bindings/http/example-client.js", + "bindings:opcua:1": "node dist/bindings/opcua/opcua-demo1.js", + "bindings:opcua:2": "node dist/bindings/opcua/opcua-demo2.js", + "bindings:opcua:coffee-machine": "node dist/bindings/opcua/opcua-coffee-machine-demo.js", + "quickstart:smart-clock": "node dist/quickstart/smart-clock.js", + "quickstart:simple-coffee-machine": "node dist/quickstart/simple-coffee-machine.js", + "quickstart:presence-sensor": "node dist/quickstart/presence-sensor.js" }, "bugs": { "url": "https://github.com/eclipse-thingweb/node-wot/issues" diff --git a/packages/examples/src/bindings/README.md b/packages/examples/src/bindings/README.md new file mode 100644 index 000000000..f877d5ce8 --- /dev/null +++ b/packages/examples/src/bindings/README.md @@ -0,0 +1,15 @@ +## Binding Examples + +This folder contains examples for different binding protocols. + +It demonstrates how to create Things that take their properties, actions, and events from different protocol bindings. + +For each use case a Thing Description is provided that describes the Thing in a protocol-agnostic way. +Then a Servient is created that uses the respective binding protocol to expose the Thing. +A console client is also provided to interact with the Thing. + +Examples are located in + +- `bindings\coap` +- `bindings\http` +- [`bindings\opcua`](./opcua/README.md) diff --git a/packages/examples/src/bindings/opcua/README.md b/packages/examples/src/bindings/opcua/README.md new file mode 100644 index 000000000..ec7d4cb63 --- /dev/null +++ b/packages/examples/src/bindings/opcua/README.md @@ -0,0 +1,26 @@ +## OPCUA + +For inializing an OPCUA client Servient, we need to import the `OPCUAClientFactory` from the `@node-wot/binding-opcua` package. + +```typescript +const servient = new Servient(); +servient.addClientFactory(new OPCUAClientFactory()); +const wot = await servient.start(); +const thing = await wot.consume(thingDescription); +``` + +Then we can interact with the Thing as usual: + +```typescript +// now interact with the things +await thing.invokeAction(...); +await thing.readProperty(...); +await thing.subscribeEvent(...); + +``` + +Finally, we can shutdown the servient: + +```typescript +await servient.shutdown(); +``` diff --git a/packages/examples/src/bindings/opcua/demo-opcua-thing-description.ts b/packages/examples/src/bindings/opcua/demo-opcua-thing-description.ts index bec245db5..ed6721bb5 100644 --- a/packages/examples/src/bindings/opcua/demo-opcua-thing-description.ts +++ b/packages/examples/src/bindings/opcua/demo-opcua-thing-description.ts @@ -25,6 +25,7 @@ export const thingDescription: WoT.ThingDescription = { security: "nosec_sc", title: "servient", description: "node-wot CLI Servient", + base: endpointUrl, properties: { pumpSpeed: { description: "the pump speed", @@ -34,7 +35,7 @@ export const thingDescription: WoT.ThingDescription = { type: "number", forms: [ { - href: endpointUrl + "?id=ns=1;s=PumpSpeed", + href: "?id=ns=1;s=PumpSpeed", op: ["readproperty", "observeproperty"], }, ], @@ -47,7 +48,7 @@ export const thingDescription: WoT.ThingDescription = { type: "number", forms: [ { - href: endpointUrl + "?id=ns=1;s=Temperature", + href: "?id=ns=1;s=Temperature", op: ["readproperty", "observeproperty"], }, ], diff --git a/packages/examples/src/bindings/opcua/opcua-coffee-machine-demo.ts b/packages/examples/src/bindings/opcua/opcua-coffee-machine-demo.ts index 57788c5d0..0c990a799 100644 --- a/packages/examples/src/bindings/opcua/opcua-coffee-machine-demo.ts +++ b/packages/examples/src/bindings/opcua/opcua-coffee-machine-demo.ts @@ -14,7 +14,7 @@ ********************************************************************************/ /* eslint no-console: "off" */ - +import util from "util"; import { Servient } from "@node-wot/core"; import { OPCUAClientFactory } from "@node-wot/binding-opcua"; import { thingDescription } from "./opcua-coffee-machine-thing-description"; @@ -25,37 +25,156 @@ const pause = async (ms: number) => new Promise((resolve) => setTimeout(resolve, servient.addClientFactory(new OPCUAClientFactory()); const wot = await servient.start(); + const thing = await wot.consume(thingDescription); + let lastTemperature = NaN; + let lastWaterTankLevel = NaN; + let lastCoffeeBeanLevel = NaN; + let lastCurrentState = NaN; + let lastGrindingDuration = NaN; + let lastGrinderStatus = NaN; + let lastHeaterStatus = NaN; + let lastPumpStatus = NaN; + let lastValveStatus = NaN; + + const recordedActions: string[] = []; + const recordAction = (actionName: string) => { + recordedActions.push(`${new Date().toISOString()} - ${actionName}`); + }; + process.stdout.write("\x1Bc"); // clear console + process.stdout.write("\x1B[?25l"); // hide cursor + const currentStateEnum = ["Off", "Standby", "Error", "Cleaning", "Serving Coffee", "Under Maintenance"]; + const grinderStates = ["Off", "On", "Jammed", "Malfunctioning"]; + const heaterStates = ["Off", "Heating", "Ready", "Malfunctioning"]; + const pumpStates = ["Off", "On", "Malfunctioning"]; + const valveStates = ["Open", "Opening", "Close", "Closing", "Malfunctioning"]; + + const waitingMachineCoffeeStandByState = async () => { + await pause(1000); + let state = lastCurrentState; + while (state !== 1) { + // Standby + await pause(1000); + state = lastCurrentState; + } + }; + const writeLine = (...args: unknown[]) => { + process.stdout.write(util.format(...args) + " \n"); + }; + const displayOnlineStatus = () => { + process.stdout.write("\x1B[1;1H"); // move cursor to top left + writeLine(`======== Coffee Machine Status ======== ${new Date().toISOString()}`); + writeLine( + ` 🔄 Current State : ${ + isNaN(lastCurrentState) ? "n/a" : (currentStateEnum[lastCurrentState] ?? lastCurrentState) + }` + ); + writeLine( + ` 🔥 Heater Status : ${ + isNaN(lastHeaterStatus) ? "n/a" : (heaterStates[lastHeaterStatus] ?? lastHeaterStatus) + }` + ); + writeLine( + ` 🌡️ Boiler Temperature : ${isNaN(lastTemperature) ? "n/a" : lastTemperature.toFixed(2) + " °C"}` + ); + writeLine( + ` 🚰 Pump Status : ${ + isNaN(lastPumpStatus) ? "n/a" : (pumpStates[lastPumpStatus] ?? lastPumpStatus) + }` + ); + writeLine( + ` 🚪 Valve Status : ${ + isNaN(lastValveStatus) ? "n/a" : (valveStates[lastValveStatus] ?? lastValveStatus) + }` + ); + writeLine( + ` 💧 Water Tank Level : ${isNaN(lastWaterTankLevel) ? "n/a" : lastWaterTankLevel.toFixed(2) + " ml"}` + ); + writeLine( + ` ⚙️ Grinder Status : ${ + isNaN(lastGrinderStatus) ? "n/a" : (grinderStates[lastGrinderStatus] ?? lastGrinderStatus) + }` + ); + writeLine( + ` ⏱️ Grinding Duration : ${ + isNaN(lastGrindingDuration) ? "n/a" : lastGrindingDuration.toFixed(2) + " s" + }` + ); + writeLine( + ` ☕ Coffee Bean Level : ${isNaN(lastCoffeeBeanLevel) ? "n/a" : lastCoffeeBeanLevel.toFixed(2) + " g"}` + ); + writeLine("========================================"); + writeLine("---- Recorded Actions (last 5) ----"); + recordedActions + .slice(-5) + .forEach((action) => writeLine(action + " ")); + writeLine("-----------------------------------"); + }; try { - thing - .observeProperty("waterTankLevel", async (data) => { - const waterTankLevel = await data.value(); - console.log("------------------------------"); - console.log("tankLevel : ", waterTankLevel, "ml"); - console.log("------------------------------"); - }) - .catch((err) => { - console.error("Error observing waterTankLevel property:", err); - }); - thing - .observeProperty("coffeeBeanLevel", async (data) => { - const coffeBeanLevel = await data.value(); - console.log("------------------------------"); - console.log("bean level : ", coffeBeanLevel, "g"); - console.log("------------------------------"); - }) - .catch((err) => { - console.error("Error observing coffeeBeanLevel property:", err); - }); + await thing.observeProperty("waterTankLevel", async (data) => { + lastWaterTankLevel = (await data.value()) as number; + displayOnlineStatus(); + }); + await thing.observeProperty("coffeeBeanLevel", async (data) => { + lastCoffeeBeanLevel = (await data.value()) as number; + displayOnlineStatus(); + }); + await thing.observeProperty("temperature", async (data) => { + lastTemperature = (await data.value()) as number; + displayOnlineStatus(); + }); + await thing.observeProperty("currentState", async (data) => { + lastCurrentState = (await data.value()) as number; + displayOnlineStatus(); + }); + await thing.observeProperty("grinderStatus", async (data) => { + lastGrinderStatus = (await data.value()) as number; + displayOnlineStatus(); + }); + await thing.observeProperty("grindingDuration", async (data) => { + lastGrindingDuration = (await data.value()) as number; + displayOnlineStatus(); + }); + await thing.observeProperty("heaterStatus", async (data) => { + lastHeaterStatus = (await data.value()) as number; + displayOnlineStatus(); + }); + await thing.observeProperty("pumpStatus", async (data) => { + lastPumpStatus = (await data.value()) as number; + displayOnlineStatus(); + }); + await thing.observeProperty("valveStatus", async (data) => { + lastValveStatus = (await data.value()) as number; + displayOnlineStatus(); + }); + + // give some time to gather initial values + await pause(2000); + await waitingMachineCoffeeStandByState(); + recordAction("Machine is ready !"); + + await pause(10000); + + recordAction("Invoking brewCoffee(Mocha) action..."); + await thing.invokeAction("brewCoffee", { RecipeName: "Mocha" }); + await waitingMachineCoffeeStandByState(); + recordAction("Coffee is ready !"); + + await pause(10000); + + recordAction("Invoking brewCoffee(Americano) action..."); + await thing.invokeAction("brewCoffee", { RecipeName: "Americano" }); + await waitingMachineCoffeeStandByState(); + recordAction("Coffee is ready !"); - await thing.invokeAction("brewCoffee", { CoffeeType: 1 }); - await pause(5000); - await thing.invokeAction("brewCoffee", { CoffeeType: 0 }); - await pause(5000); + await pause(10000); + recordAction("Invoking fillTank action..."); await thing.invokeAction("fillTank"); - await pause(5000); + await waitingMachineCoffeeStandByState(); + recordAction("Tank is refilled !"); + recordAction("Done !"); } finally { await servient.shutdown(); } diff --git a/packages/examples/src/bindings/opcua/opcua-coffee-machine-thing-description.ts b/packages/examples/src/bindings/opcua/opcua-coffee-machine-thing-description.ts index 099a92566..b21132c69 100644 --- a/packages/examples/src/bindings/opcua/opcua-coffee-machine-thing-description.ts +++ b/packages/examples/src/bindings/opcua/opcua-coffee-machine-thing-description.ts @@ -14,9 +14,21 @@ ********************************************************************************/ const endpointUrl = "opc.tcp://opcuademo.sterfive.com:26543"; +const coffeeMachine = "1:CoffeeMachineA"; + export const thingDescription: WoT.ThingDescription = { - "@context": "https://www.w3.org/2019/wot/td/v1", + "@context": [ + "https://www.w3.org/2019/wot/td/v1", + { + uav: "http://opcfoundation.org/UA/WoT-Binding/", + "1": "http://example.namespace.com/demo/pump", + "2": "http://opcfoundation.org/UA/DI/", + "7": "http://opcfoundation.org/UA/CommercialKitchenEquipment/", + "17": "http://sterfive.com/UA/CoffeeMachine/", + }, + ], "@type": ["Thing"], + base: endpointUrl, securityDefinitions: { nosec_sc: { scheme: "nosec", @@ -27,12 +39,11 @@ export const thingDescription: WoT.ThingDescription = { description: "node-wot CLI Servient", properties: { deviceHealth: { - // type: "number", observable: true, readOnly: true, forms: [ { - href: endpointUrl, + href: "/", op: ["readproperty", "observeproperty"], "opcua:nodeId": { root: "i=84", @@ -42,34 +53,139 @@ export const thingDescription: WoT.ThingDescription = { ], }, waterTankLevel: { - // type: "number", observable: true, readOnly: true, forms: [ { - href: endpointUrl, + href: "/", op: ["readproperty", "observeproperty"], "opcua:nodeId": { root: "i=84", - path: "/Objects/2:DeviceSet/1:CoffeeMachine/2:ParameterSet/9:WaterTankLevel", + path: `/Objects/2:DeviceSet/${coffeeMachine}/7:Parameters/17:WaterTankLevel`, }, }, ], + type: "number", }, coffeeBeanLevel: { - // type: "number", observable: true, readOnly: true, forms: [ { - href: endpointUrl, + href: "/", + op: ["readproperty", "observeproperty"], + "opcua:nodeId": { + root: "i=84", + path: `/Objects/2:DeviceSet/${coffeeMachine}/7:Parameters/17:CoffeeBeanLevel`, + }, + }, + ], + type: "number", + }, + temperature: { + observable: true, + readOnly: true, + forms: [ + { + href: "/", + op: ["readproperty", "observeproperty"], + "opcua:nodeId": { + root: "i=84", + path: `/Objects/2:DeviceSet/${coffeeMachine}/7:Parameters/7:BoilerTempWater`, + }, + }, + ], + type: "number", + }, + currentState: { + observable: true, + readOnly: true, + forms: [ + { + href: "/", + op: ["readproperty", "observeproperty"], + "opcua:nodeId": { + root: "i=84", + path: `/Objects/2:DeviceSet/${coffeeMachine}/7:Parameters/7:CurrentState`, + }, + }, + ], + type: "number", + }, + grinderStatus: { + observable: true, + readOnly: true, + forms: [ + { + href: "/", + op: ["readproperty", "observeproperty"], + "opcua:nodeId": { + root: "i=84", + path: `/Objects/2:DeviceSet/${coffeeMachine}/7:Parameters/17:GrinderStatus`, + }, + }, + ], + type: "number", + }, + heaterStatus: { + observable: true, + readOnly: true, + forms: [ + { + href: "/", op: ["readproperty", "observeproperty"], "opcua:nodeId": { root: "i=84", - path: "/Objects/2:DeviceSet/1:CoffeeMachine/2:ParameterSet/9:CoffeeBeanLevel", + path: `/Objects/2:DeviceSet/${coffeeMachine}/7:Parameters/17:HeaterStatus`, }, }, ], + type: "number", + }, + pumpStatus: { + observable: true, + readOnly: true, + forms: [ + { + href: "/", + op: ["readproperty", "observeproperty"], + "opcua:nodeId": { + root: "i=84", + path: `/Objects/2:DeviceSet/${coffeeMachine}/7:Parameters/17:PumpStatus`, + }, + }, + ], + type: "number", + }, + valveStatus: { + observable: true, + readOnly: true, + forms: [ + { + href: "/", + op: ["readproperty", "observeproperty"], + "opcua:nodeId": { + root: "i=84", + path: `/Objects/2:DeviceSet/${coffeeMachine}/7:Parameters/17:ValveStatus`, + }, + }, + ], + type: "number", + }, + grindingDuration: { + observable: true, + readOnly: true, + forms: [ + { + href: "/", + op: ["readproperty", "observeproperty"], + "opcua:nodeId": { + root: "i=84", + path: `/Objects/2:DeviceSet/${coffeeMachine}/7:Parameters/17:GrindingDuration`, + }, + }, + ], + type: "number", }, }, actions: { @@ -77,33 +193,36 @@ export const thingDescription: WoT.ThingDescription = { forms: [ { type: "object", - href: endpointUrl, + href: "/", op: ["invokeaction"], - "opcua:nodeId": { root: "i=84", path: "/Objects/2:DeviceSet/1:CoffeeMachine" }, - "opcua:method": { root: "i=84", path: "/Objects/2:DeviceSet/1:CoffeeMachine/2:MethodSet/9:Start" }, + "opcua:nodeId": { root: "i=84", path: `/Objects/2:DeviceSet/${coffeeMachine}` }, + "opcua:method": { + root: "i=84", + path: `/Objects/2:DeviceSet/${coffeeMachine}/2:MethodSet/17:MakeCoffee`, + }, }, ], input: { type: "object", properties: { - CoffeeType: { - title: "1 for Americano, 2 for Expressp", - type: "number", + RecipeName: { + title: "Americano or Espresso or Mocha (see available Recipes in OPCUA server)", + type: "string", }, }, - required: ["CoffeeType"], + required: ["RecipeName"], }, }, fillTank: { forms: [ { type: "object", - href: endpointUrl, + href: "/", op: ["invokeaction"], - "opcua:nodeId": { root: "i=84", path: "/Objects/2:DeviceSet/1:CoffeeMachine" }, + "opcua:nodeId": { root: "i=84", path: `/Objects/2:DeviceSet/${coffeeMachine}` }, "opcua:method": { root: "i=84", - path: "/Objects/2:DeviceSet/1:CoffeeMachine/2:MethodSet/9:FillTank", + path: `/Objects/2:DeviceSet/${coffeeMachine}/2:MethodSet/17:FillTank`, }, }, ], diff --git a/packages/examples/src/security/oauth/package.json b/packages/examples/src/security/oauth/package.json index ce6c0c28f..654548ec6 100644 --- a/packages/examples/src/security/oauth/package.json +++ b/packages/examples/src/security/oauth/package.json @@ -18,7 +18,9 @@ "cors": "^2.8.5", "ts-node": "10.9.1", "typescript": "4.7.4", - "typescript-standard": "^0.3.36", + "typescript-standard": "^0.3.36" + }, + "dependencies": { "wot-typescript-definitions": "0.8.0-SNAPSHOT.31" } }