diff --git a/package-lock.json b/package-lock.json index 8e9f37c4..400446ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.1", "license": "Mozilla Public License Version 2.0", "dependencies": { + "@chirpstack/chirpstack-api": "4.6.0", + "@grpc/grpc-js": "^1.9.12", "@nestjs/axios": "^3.0.0", "@nestjs/common": "^9.1.2", "@nestjs/config": "^2.2.0", @@ -21,7 +23,7 @@ "@nestjs/swagger": "^6.1.2", "@nestjs/typeorm": "^9.0.1", "@types/bcryptjs": "^2.4.2", - "@types/geojson": "^7946.0.7", + "@types/geojson": "^7946.0.13", "@types/kafkajs": "^1.9.0", "@types/passport-saml": "^1.1.3", "@types/pem": "^1.9.6", @@ -72,7 +74,7 @@ "@types/cron": "^1.7.2", "@types/crypto-js": "^4.1.1", "@types/express": "^4.17.9", - "@types/geojson": "^7946.0.7", + "@types/geojson": "^7946.0.13", "@types/kafkajs": "^1.9.0", "@types/lodash": "^4.14.165", "@types/node": "^14.14.14", @@ -327,21 +329,21 @@ } }, "node_modules/@babel/core": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz", - "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.6.tgz", + "integrity": "sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.5", - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.5", - "@babel/parser": "^7.23.5", + "@babel/helpers": "^7.23.6", + "@babel/parser": "^7.23.6", "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.5", - "@babel/types": "^7.23.5", + "@babel/traverse": "^7.23.6", + "@babel/types": "^7.23.6", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -366,12 +368,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", - "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.23.5", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -381,14 +383,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -414,6 +416,12 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, "node_modules/@babel/helper-environment-visitor": { "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", @@ -540,14 +548,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", - "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.6.tgz", + "integrity": "sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA==", "dev": true, "dependencies": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.5", - "@babel/types": "^7.23.5" + "@babel/traverse": "^7.23.6", + "@babel/types": "^7.23.6" }, "engines": { "node": ">=6.9.0" @@ -639,9 +647,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", - "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -828,9 +836,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", - "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", + "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -853,20 +861,20 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", - "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.6.tgz", + "integrity": "sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.5", + "@babel/generator": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.5", - "@babel/types": "^7.23.5", - "debug": "^4.1.0", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -883,9 +891,9 @@ } }, "node_modules/@babel/types": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", - "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -902,6 +910,17 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@chirpstack/chirpstack-api": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@chirpstack/chirpstack-api/-/chirpstack-api-4.6.0.tgz", + "integrity": "sha512-bGXKbWisX3tMWa9GxazVP87za2EzkoJuUIwgJGmwbHtSTz/bZqCLYhOG+77yt881qLFxUmzAsnVNTQFLE1whHQ==", + "dependencies": { + "@grpc/grpc-js": "^1.9.0", + "@mapbox/node-pre-gyp": "^1.0.11", + "@types/google-protobuf": "^3.15.6", + "google-protobuf": "^3.21.2" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -959,9 +978,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -982,14 +1001,66 @@ } }, "node_modules/@eslint/js": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", - "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.12.tgz", + "integrity": "sha512-Um5MBuge32TS3lAKX02PGCnFM4xPT996yLgZNb5H03pn6NyJ4Iwn5YcPq6Jj9yxGRk7WOgaZFtVRH5iTdYBeUg==", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", + "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -1475,6 +1546,25 @@ "node": ">=8" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, "node_modules/@nestjs/axios": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.1.tgz", @@ -1564,6 +1654,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@nestjs/cli/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@nestjs/cli/node_modules/rimraf": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.4.1.tgz", @@ -2178,6 +2277,11 @@ "integrity": "sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ==", "dev": true }, + "node_modules/@types/google-protobuf": { + "version": "3.15.12", + "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.12.tgz", + "integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -2446,16 +2550,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", - "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.14.0.tgz", + "integrity": "sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.13.1", - "@typescript-eslint/type-utils": "6.13.1", - "@typescript-eslint/utils": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1", + "@typescript-eslint/scope-manager": "6.14.0", + "@typescript-eslint/type-utils": "6.14.0", + "@typescript-eslint/utils": "6.14.0", + "@typescript-eslint/visitor-keys": "6.14.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -2481,15 +2585,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", - "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.14.0.tgz", + "integrity": "sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.13.1", - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/typescript-estree": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1", + "@typescript-eslint/scope-manager": "6.14.0", + "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/typescript-estree": "6.14.0", + "@typescript-eslint/visitor-keys": "6.14.0", "debug": "^4.3.4" }, "engines": { @@ -2509,13 +2613,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", - "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.14.0.tgz", + "integrity": "sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1" + "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/visitor-keys": "6.14.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2526,13 +2630,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", - "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.14.0.tgz", + "integrity": "sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.13.1", - "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/typescript-estree": "6.14.0", + "@typescript-eslint/utils": "6.14.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -2553,9 +2657,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", - "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.14.0.tgz", + "integrity": "sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2566,13 +2670,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", - "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.14.0.tgz", + "integrity": "sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1", + "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/visitor-keys": "6.14.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2593,17 +2697,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", - "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.14.0.tgz", + "integrity": "sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.13.1", - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/scope-manager": "6.14.0", + "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/typescript-estree": "6.14.0", "semver": "^7.5.4" }, "engines": { @@ -2618,12 +2722,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", - "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.14.0.tgz", + "integrity": "sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/types": "6.14.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -2806,6 +2910,11 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2849,14 +2958,25 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", - "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", "devOptional": true, "engines": { "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3009,6 +3129,23 @@ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -3286,9 +3423,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "dev": true, "funding": [ { @@ -3305,9 +3442,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -3445,9 +3582,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001565", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz", - "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==", + "version": "1.0.30001568", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001568.tgz", + "integrity": "sha512-vSUkH84HontZJ88MiNrOau1EBrCqEQYgkC5gIySiDlpsm8sGVrhU7Kx4V6h0tnqaHzIHZv08HlJIwPbL4XL9+A==", "dev": true, "funding": [ { @@ -3530,9 +3667,12 @@ } }, "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } }, "node_modules/chrome-trace-event": { "version": "1.0.3", @@ -3736,6 +3876,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3842,6 +3990,11 @@ "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==" }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -4127,6 +4280,11 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4257,9 +4415,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.600", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.600.tgz", - "integrity": "sha512-KD6CWjf1BnQG+NsXuyiTDDT1eV13sKuYsOUioXkQweYTQIbgHkXPry9K7M+7cKtYHnSUPitVaLrXYB1jTkkYrw==", + "version": "1.4.610", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.610.tgz", + "integrity": "sha512-mqi2oL1mfeHYtOdCxbPQYV/PL7YrQlxbvFEZ0Ee8GbDdShimqt2/S6z2RWqysuvlwdOrQdqvE0KZrBTipAeJzg==", "dev": true }, "node_modules/emittery": { @@ -4357,15 +4515,15 @@ } }, "node_modules/eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", - "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.54.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -4998,6 +5156,28 @@ "node": ">=12" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs-monkey": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", @@ -5031,6 +5211,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5126,9 +5325,9 @@ "dev": true }, "node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -5160,6 +5359,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-protobuf": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", + "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -5224,6 +5428,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", @@ -5282,6 +5491,18 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -5621,6 +5842,21 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", @@ -6458,6 +6694,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -6527,9 +6768,9 @@ } }, "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" }, "node_modules/lru-cache": { "version": "7.18.3", @@ -6572,20 +6813,27 @@ } }, "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dependencies": { - "semver": "^7.5.3" + "semver": "^6.0.0" }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -6742,10 +6990,32 @@ } }, "node_modules/minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", - "dev": true, + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { "node": ">=8" } @@ -6776,9 +7046,9 @@ } }, "node_modules/mqtt": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.7.tgz", - "integrity": "sha512-ew3qwG/TJRorTz47eW46vZ5oBw5MEYbQZVaEji44j5lAUSQSqIEoul7Kua/BatBW0H0kKQcC9kwUHa1qzaWHSw==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.8.tgz", + "integrity": "sha512-2xT75uYa0kiPEF/PE0VPdavmEkoBzMT/UL9moid0rAvlCtV48qBwxD62m7Ld/4j8tSkIO1E/iqRl/S72SEOhOw==", "dependencies": { "commist": "^1.0.0", "concat-stream": "^2.0.0", @@ -6828,11 +7098,6 @@ "node": ">=10" } }, - "node_modules/mqtt/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -6951,9 +7216,9 @@ "integrity": "sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A==" }, "node_modules/node-abi": { - "version": "3.51.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz", - "integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==", + "version": "3.52.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.52.0.tgz", + "integrity": "sha512-JJ98b02z16ILv7859irtXn4oUaFWADtvkzy2c0IAatNVX2Mc9Yoh8z6hZInn3QwvMEYhHuQloYi+TTQy67SIdQ==", "dependencies": { "semver": "^7.3.5" }, @@ -7015,6 +7280,20 @@ "node": ">=6.0.0" } }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7036,6 +7315,17 @@ "node": ">=8" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/number-allocator": { "version": "1.0.14", "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", @@ -7396,15 +7686,6 @@ "node": "14 || >=16.14" } }, - "node_modules/path-scurry/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/path-to-regexp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", @@ -7782,6 +8063,11 @@ "pbts": "bin/pbts" } }, + "node_modules/protobufjs/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7963,9 +8249,9 @@ } }, "node_modules/reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==" }, "node_modules/regenerator-runtime": { "version": "0.14.0", @@ -8190,11 +8476,6 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -8246,6 +8527,11 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "node_modules/set-function-length": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", @@ -8331,8 +8617,7 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/simple-concat": { "version": "1.0.1", @@ -8676,6 +8961,22 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -8687,6 +8988,11 @@ "tar-stream": "^2.1.4" } }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -8702,10 +9008,21 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/terser": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", - "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", + "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -8951,9 +9268,9 @@ } }, "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -9574,6 +9891,14 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/windows-release": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-4.0.0.tgz", @@ -9768,10 +10093,9 @@ } }, "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "1.10.2", @@ -10006,23 +10330,23 @@ "dev": true }, "@babel/core": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz", - "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.6.tgz", + "integrity": "sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw==", "dev": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.5", - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.5", - "@babel/parser": "^7.23.5", + "@babel/helpers": "^7.23.6", + "@babel/parser": "^7.23.6", "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.5", + "@babel/traverse": "^7.23.6", + "@babel/types": "^7.23.6", "convert-source-map": "^2.0.0", - "debug": "^4.3.4", + "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" @@ -10037,26 +10361,26 @@ } }, "@babel/generator": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", - "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "requires": { - "@babel/types": "^7.23.5", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" } }, "@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "requires": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -10075,6 +10399,12 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } }, @@ -10168,14 +10498,14 @@ "dev": true }, "@babel/helpers": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", - "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.6.tgz", + "integrity": "sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA==", "dev": true, "requires": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.5" + "@babel/traverse": "^7.23.6", + "@babel/types": "^7.23.6" } }, "@babel/highlight": { @@ -10248,9 +10578,9 @@ } }, "@babel/parser": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", - "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -10380,9 +10710,9 @@ } }, "@babel/runtime": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", - "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", + "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", "requires": { "regenerator-runtime": "^0.14.0" } @@ -10399,20 +10729,20 @@ } }, "@babel/traverse": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", - "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.6.tgz", + "integrity": "sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==", "dev": true, "requires": { "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.5", + "@babel/generator": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.5", - "@babel/types": "^7.23.5", - "debug": "^4.3.4", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "dependencies": { @@ -10425,9 +10755,9 @@ } }, "@babel/types": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", - "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "requires": { "@babel/helper-string-parser": "^7.23.4", @@ -10441,6 +10771,17 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@chirpstack/chirpstack-api": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@chirpstack/chirpstack-api/-/chirpstack-api-4.6.0.tgz", + "integrity": "sha512-bGXKbWisX3tMWa9GxazVP87za2EzkoJuUIwgJGmwbHtSTz/bZqCLYhOG+77yt881qLFxUmzAsnVNTQFLE1whHQ==", + "requires": { + "@grpc/grpc-js": "^1.9.0", + "@mapbox/node-pre-gyp": "^1.0.11", + "@types/google-protobuf": "^3.15.6", + "google-protobuf": "^3.21.2" + } + }, "@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -10485,13 +10826,13 @@ "dev": true }, "@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "requires": { "ajv": "^6.12.4", - "debug": "^4.3.4", + "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", @@ -10502,11 +10843,52 @@ } }, "@eslint/js": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", - "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", "dev": true }, + "@grpc/grpc-js": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.12.tgz", + "integrity": "sha512-Um5MBuge32TS3lAKX02PGCnFM4xPT996yLgZNb5H03pn6NyJ4Iwn5YcPq6Jj9yxGRk7WOgaZFtVRH5iTdYBeUg==", + "requires": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + } + }, + "@grpc/proto-loader": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", + "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "requires": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + }, + "dependencies": { + "protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + } + } + } + }, "@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -10514,7 +10896,7 @@ "dev": true, "requires": { "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.3.4", + "debug": "^4.1.1", "minimatch": "^3.0.5" } }, @@ -10887,6 +11269,22 @@ "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==" }, + "@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "requires": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + } + }, "@nestjs/axios": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.1.tgz", @@ -10953,6 +11351,12 @@ "brace-expansion": "^2.0.1" } }, + "minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true + }, "rimraf": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.4.1.tgz", @@ -11412,6 +11816,11 @@ "integrity": "sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ==", "dev": true }, + "@types/google-protobuf": { + "version": "3.15.12", + "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.12.tgz", + "integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==" + }, "@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -11679,16 +12088,16 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", - "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.14.0.tgz", + "integrity": "sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.13.1", - "@typescript-eslint/type-utils": "6.13.1", - "@typescript-eslint/utils": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1", + "@typescript-eslint/scope-manager": "6.14.0", + "@typescript-eslint/type-utils": "6.14.0", + "@typescript-eslint/utils": "6.14.0", + "@typescript-eslint/visitor-keys": "6.14.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -11698,54 +12107,54 @@ } }, "@typescript-eslint/parser": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", - "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.14.0.tgz", + "integrity": "sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "6.13.1", - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/typescript-estree": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1", + "@typescript-eslint/scope-manager": "6.14.0", + "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/typescript-estree": "6.14.0", + "@typescript-eslint/visitor-keys": "6.14.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", - "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.14.0.tgz", + "integrity": "sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg==", "dev": true, "requires": { - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1" + "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/visitor-keys": "6.14.0" } }, "@typescript-eslint/type-utils": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", - "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.14.0.tgz", + "integrity": "sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "6.13.1", - "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/typescript-estree": "6.14.0", + "@typescript-eslint/utils": "6.14.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", - "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.14.0.tgz", + "integrity": "sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", - "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.14.0.tgz", + "integrity": "sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==", "dev": true, "requires": { - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1", + "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/visitor-keys": "6.14.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -11754,27 +12163,27 @@ } }, "@typescript-eslint/utils": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", - "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.14.0.tgz", + "integrity": "sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.13.1", - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/scope-manager": "6.14.0", + "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/typescript-estree": "6.14.0", "semver": "^7.5.4" } }, "@typescript-eslint/visitor-keys": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", - "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.14.0.tgz", + "integrity": "sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw==", "dev": true, "requires": { - "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/types": "6.14.0", "eslint-visitor-keys": "^3.4.1" } }, @@ -11947,6 +12356,11 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -11977,11 +12391,19 @@ "requires": {} }, "acorn-walk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", - "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", "devOptional": true }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -12090,6 +12512,20 @@ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" }, + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -12312,14 +12748,14 @@ } }, "browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" } }, @@ -12416,9 +12852,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001565", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz", - "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==", + "version": "1.0.30001568", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001568.tgz", + "integrity": "sha512-vSUkH84HontZJ88MiNrOau1EBrCqEQYgkC5gIySiDlpsm8sGVrhU7Kx4V6h0tnqaHzIHZv08HlJIwPbL4XL9+A==", "dev": true }, "chalk": { @@ -12464,9 +12900,9 @@ } }, "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" }, "chrome-trace-event": { "version": "1.0.3", @@ -12543,7 +12979,7 @@ "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", - "y18n": "6.0.0-alpha.0", + "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, @@ -12617,6 +13053,11 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -12703,6 +13144,11 @@ "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==" }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -12903,6 +13349,11 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -12999,9 +13450,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.4.600", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.600.tgz", - "integrity": "sha512-KD6CWjf1BnQG+NsXuyiTDDT1eV13sKuYsOUioXkQweYTQIbgHkXPry9K7M+7cKtYHnSUPitVaLrXYB1jTkkYrw==", + "version": "1.4.610", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.610.tgz", + "integrity": "sha512-mqi2oL1mfeHYtOdCxbPQYV/PL7YrQlxbvFEZ0Ee8GbDdShimqt2/S6z2RWqysuvlwdOrQdqvE0KZrBTipAeJzg==", "dev": true }, "emittery": { @@ -13075,15 +13526,15 @@ "dev": true }, "eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", - "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.54.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -13550,6 +14001,24 @@ "universalify": "^2.0.0" } }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, "fs-monkey": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", @@ -13573,6 +14042,22 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, + "gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -13641,9 +14126,9 @@ "dev": true }, "globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -13663,6 +14148,11 @@ "slash": "^3.0.0" } }, + "google-protobuf": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", + "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" + }, "gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -13706,6 +14196,11 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", @@ -13752,6 +14247,15 @@ "toidentifier": "1.0.1" } }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "^4.3.4" + } + }, "human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -13987,6 +14491,17 @@ "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" + }, + "dependencies": { + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + } } }, "istanbul-lib-source-maps": { @@ -13995,7 +14510,7 @@ "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, "requires": { - "debug": "^4.3.4", + "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" }, @@ -14640,6 +15155,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -14703,9 +15223,9 @@ } }, "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" }, "lru-cache": { "version": "7.18.3", @@ -14733,12 +15253,18 @@ } }, "make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "requires": { - "semver": "^7.5.3" + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + } } }, "make-error": { @@ -14855,10 +15381,28 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, "minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", - "dev": true + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } }, "mkdirp": { "version": "0.5.6", @@ -14880,9 +15424,9 @@ "dev": true }, "mqtt": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.7.tgz", - "integrity": "sha512-ew3qwG/TJRorTz47eW46vZ5oBw5MEYbQZVaEji44j5lAUSQSqIEoul7Kua/BatBW0H0kKQcC9kwUHa1qzaWHSw==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.8.tgz", + "integrity": "sha512-2xT75uYa0kiPEF/PE0VPdavmEkoBzMT/UL9moid0rAvlCtV48qBwxD62m7Ld/4j8tSkIO1E/iqRl/S72SEOhOw==", "requires": { "commist": "^1.0.0", "concat-stream": "^2.0.0", @@ -14910,11 +15454,6 @@ "requires": { "yallist": "^4.0.0" } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, @@ -15038,9 +15577,9 @@ } }, "node-abi": { - "version": "3.51.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz", - "integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==", + "version": "3.52.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.52.0.tgz", + "integrity": "sha512-JJ98b02z16ILv7859irtXn4oUaFWADtvkzy2c0IAatNVX2Mc9Yoh8z6hZInn3QwvMEYhHuQloYi+TTQy67SIdQ==", "requires": { "semver": "^7.3.5" } @@ -15085,6 +15624,14 @@ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.7.tgz", "integrity": "sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==" }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "requires": { + "abbrev": "1" + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -15100,6 +15647,17 @@ "path-key": "^3.0.0" } }, + "npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "requires": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "number-allocator": { "version": "1.0.14", "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", @@ -15363,12 +15921,6 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", "dev": true - }, - "minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true } } }, @@ -15646,6 +16198,13 @@ "@types/long": "^4.0.1", "@types/node": ">=13.7.0", "long": "^4.0.0" + }, + "dependencies": { + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + } } }, "proxy-addr": { @@ -15774,9 +16333,9 @@ } }, "reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==" }, "regenerator-runtime": { "version": "0.14.0", @@ -15932,11 +16491,6 @@ "requires": { "yallist": "^4.0.0" } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, @@ -15987,6 +16541,11 @@ "send": "0.18.0" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "set-function-length": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", @@ -16051,8 +16610,7 @@ "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "simple-concat": { "version": "1.0.1", @@ -16287,6 +16845,26 @@ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true }, + "tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + } + } + }, "tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -16296,6 +16874,13 @@ "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" + }, + "dependencies": { + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + } } }, "tar-stream": { @@ -16311,9 +16896,9 @@ } }, "terser": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", - "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", + "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", "dev": true, "requires": { "@jridgewell/source-map": "^0.3.3", @@ -16474,9 +17059,9 @@ } }, "ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "requires": { "@cspotcode/source-map-support": "^0.8.0", @@ -16845,6 +17430,14 @@ "isexe": "^2.0.0" } }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "windows-release": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-4.0.0.tgz", @@ -16975,10 +17568,9 @@ "integrity": "sha512-J9CO+Qo98a30YwPMgXt1IetZS4823Y+KzBEWHPQYaO2sWcwtvVascTF0eNdUgU0Me3Efl36PnCygmVPdzqQmJg==" }, "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "1.10.2", diff --git a/package.json b/package.json index 1cbcb482..600d295c 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ }, "dependencies": { "@nestjs/axios": "^3.0.0", + "@chirpstack/chirpstack-api": "4.6.0", + "@grpc/grpc-js": "^1.9.12", "@nestjs/common": "^9.1.2", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.1.2", diff --git a/resources/chirpstack-state.proto b/resources/chirpstack-state.proto index 1befa749..bdd7a0fb 100644 --- a/resources/chirpstack-state.proto +++ b/resources/chirpstack-state.proto @@ -4,13 +4,17 @@ package gw; // ConnState contains the connection state of a gateway. message ConnState { - // Gateway ID. - bytes gateway_id = 1 [json_name = "gatewayID"]; + // Gateway ID. + // Deprecated: use gateway_id. + bytes gateway_id_legacy = 1; - enum State { - OFFLINE = 0; - ONLINE = 1; - } + // Gateway ID. + string gateway_id = 3; - State state = 2; + enum State { + OFFLINE = 0; + ONLINE = 1; + } + + State state = 2; } diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 1303c157..3ad205ee 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -15,27 +15,21 @@ export default (): any => { expiresIn: process.env.JWT_EXPIRESIN || "9h", }, backend: { - baseurl: - process.env.BACKEND_BASEURL || "https://test-os2iot-backend.os2iot.dk", - deviceStatsIntervalInDays: - parseInt(process.env.DEVICE_STATS_INTERVAL_IN_DAYS, 10) || 29, + baseurl: process.env.BACKEND_BASEURL || "https://localhost:3000", + deviceStatsIntervalInDays: parseInt(process.env.DEVICE_STATS_INTERVAL_IN_DAYS, 10) || 29, }, kombit: { entryPoint: process.env.KOMBIT_ENTRYPOINT || - "https://adgangsstyring.eksterntest-stoettesystemerne.dk/runtime/saml2/issue.idp", + "https://adgangsstyring.eksterntest-stoettesystemerne.dk/runtime/saml2/issue.idp", certificatePublicKey: process.env.KOMBIT_CERTIFICATEPUBLICKEY || "INSERT_KOMBIT_CERT", // Public certificate from Kombit Test server certificatePrivateKey: process.env.KOMBIT_CERTIFICATEPRIVATEKEY || null, - roleUri: - process.env.KOMBIT_ROLE_NAME || - "http://os2iot.dk/roles/usersystemrole/adgang/", + roleUri: process.env.KOMBIT_ROLE_NAME || "http://os2iot.dk/roles/usersystemrole/adgang/", }, chirpstack: { - jwtsecret: process.env.CHIRPSTACK_JWTSECRET || "verysecret", + apikey: process.env.CHIRPSTACK_API_KEY || "apikey", }, - logLevels: process.env.LOG_LEVEL - ? GetLogLevels(process.env.LOG_LEVEL) - : GetLogLevels("debug"), + logLevels: process.env.LOG_LEVEL ? GetLogLevels(process.env.LOG_LEVEL) : GetLogLevels("debug"), email: { host: process.env.EMAIL_HOST || "smtp.ethereal.email", port: process.env.EMAIL_PORT || 587, @@ -45,9 +39,7 @@ export default (): any => { * Can be formatted to show a user-friendly name before the e-mail. * E.g. "OS2iot " */ - from: process.env.EMAIL_FROM - ? formatEmail(process.env.EMAIL_FROM) - : "OS2iot tremayne38@ethereal.email", + from: process.env.EMAIL_FROM ? formatEmail(process.env.EMAIL_FROM) : "OS2iot tremayne38@ethereal.email", }, frontend: { baseurl: process.env.FRONTEND_BASEURL || "http://localhost:8081", diff --git a/src/controllers/admin-controller/chirpstack/device-profile.controller.ts b/src/controllers/admin-controller/chirpstack/device-profile.controller.ts index e406db01..777c9701 100644 --- a/src/controllers/admin-controller/chirpstack/device-profile.controller.ts +++ b/src/controllers/admin-controller/chirpstack/device-profile.controller.ts @@ -26,7 +26,6 @@ import { import { Read, ApplicationAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; -import { CreateChirpstackProfileResponseDto } from "@dto/chirpstack/create-chirpstack-profile-response.dto"; import { CreateDeviceProfileDto } from "@dto/chirpstack/create-device-profile.dto"; import { ListAllDeviceProfilesResponseDto } from "@dto/chirpstack/list-all-device-profiles-response.dto"; import { UpdateDeviceProfileDto } from "@dto/chirpstack/update-device-profile.dto"; @@ -38,6 +37,8 @@ import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@he import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; import { ComposeAuthGuard } from "@auth/compose-auth.guard"; +import { ListAllAdrAlgorithmsResponseDto } from "@dto/chirpstack/list-all-adr-algorithms-response.dto"; +import { IdResponse } from "@interfaces/chirpstack-id-response.interface"; @ApiTags("Chirpstack") @Controller("chirpstack/device-profiles") @@ -55,27 +56,25 @@ export class DeviceProfileController { @ApiOperation({ summary: "Create a new DeviceProfile" }) @ApiBadRequestResponse() @ApplicationAdmin() - async create( - @Req() req: AuthenticatedRequest, - @Body() createDto: CreateDeviceProfileDto - ): Promise { - checkIfUserHasAccessToOrganization(req, createDto.internalOrganizationId, OrganizationAccessScope.ApplicationWrite); + async create(@Req() req: AuthenticatedRequest, @Body() createDto: CreateDeviceProfileDto): Promise { + checkIfUserHasAccessToOrganization( + req, + createDto.internalOrganizationId, + OrganizationAccessScope.ApplicationWrite + ); try { - const result = await this.deviceProfileService.createDeviceProfile( - createDto, - req.user.userId - ); + const result = await this.deviceProfileService.createDeviceProfile(createDto, req.user.userId); AuditLog.success( ActionType.CREATE, "ChirpstackDeviceProfile", req.user.userId, - result.data.id, + result.id, createDto.deviceProfile.name ); - return result.data; + return result; } catch (err) { AuditLog.fail( ActionType.CREATE, @@ -130,6 +129,14 @@ export class DeviceProfileController { } } + @Get("adr-algorithms") + @ApiProduces("application/json") + @ApiOperation({ summary: "Find all ADR algorithms for the default network server" }) + @Read() + async getAllAdrAlgorithms(): Promise { + return await this.deviceProfileService.getAdrAlgorithmsForChirpstack(); + } + @Get(":id") @ApiProduces("application/json") @ApiOperation({ summary: "Find one DeviceProfile by id" }) @@ -160,14 +167,9 @@ export class DeviceProfileController { let result = undefined; try { this.logger.debug(`Limit: '${limit}' Offset:'${offset}'`); - result = await this.deviceProfileService.findAllDeviceProfiles( - limit || 50, - offset || 0 - ); + result = await this.deviceProfileService.findAllDeviceProfiles(limit || 50, offset || 0); } catch (err) { - this.logger.error( - `Error occured during Find all: '${JSON.stringify(err?.response?.data)}'` - ); + this.logger.error(`Error occured during Find all: '${JSON.stringify(err?.response?.data)}'`); } return result; } @@ -176,30 +178,13 @@ export class DeviceProfileController { @ApiOperation({ summary: "Delete one DeviceProfile by id" }) @ApiNotFoundResponse() @ApplicationAdmin() - async deleteOne( - @Req() req: AuthenticatedRequest, - @Param("id") id: string - ): Promise { + async deleteOne(@Req() req: AuthenticatedRequest, @Param("id") id: string): Promise { try { - const result = await this.deviceProfileService.deleteDeviceProfile(id, req); - - if (!result) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } - AuditLog.success( - ActionType.DELETE, - "ChirpstackDeviceProfile", - req.user.userId, - id - ); + await this.deviceProfileService.deleteDeviceProfile(id, req); + AuditLog.success(ActionType.DELETE, "ChirpstackDeviceProfile", req.user.userId, id); return new DeleteResponseDto(1); } catch (err) { - AuditLog.fail( - ActionType.DELETE, - "ChirpstackDeviceProfile", - req.user.userId, - id - ); + AuditLog.fail(ActionType.DELETE, "ChirpstackDeviceProfile", req.user.userId, id); if (err?.message == this.CHIRPSTACK_IN_USE_ERROR) { throw new BadRequestException(ErrorCodes.IsUsed); } diff --git a/src/controllers/admin-controller/chirpstack/network-server.controller.ts b/src/controllers/admin-controller/chirpstack/network-server.controller.ts deleted file mode 100644 index 4016f3a8..00000000 --- a/src/controllers/admin-controller/chirpstack/network-server.controller.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ComposeAuthGuard } from '@auth/compose-auth.guard'; -import { - Controller, - Get, - Logger, - UseGuards, -} from "@nestjs/common"; -import { - ApiBearerAuth, - ApiOperation, - ApiProduces, - ApiTags, -} from "@nestjs/swagger"; - -import { Read, ApplicationAdmin } from "@auth/roles.decorator"; -import { RolesGuard } from "@auth/roles.guard"; -import { ChirpstackSetupNetworkServerService } from "@services/chirpstack/network-server.service"; -import { ListAllAdrAlgorithmsResponseDto } from "@dto/chirpstack/list-all-adr-algorithms-response.dto"; - -@ApiTags("Chirpstack") -@Controller("chirpstack/network-server") -@UseGuards(ComposeAuthGuard, RolesGuard) -@ApiBearerAuth() -@ApplicationAdmin() -export class NetworkServerController { - constructor(private networkServerService: ChirpstackSetupNetworkServerService) {} - private readonly logger = new Logger(NetworkServerController.name); - - @Get("adr-algorithms") - @ApiProduces("application/json") - @ApiOperation({ summary: "Find all ADR algorithms for the default network server" }) - @Read() - async getAllAdrAlgorithms(): Promise { - return await this.networkServerService.getAdrAlgorithmsForDefaultNetworkServer(); - } -} diff --git a/src/controllers/admin-controller/chirpstack/service-profile.controller.ts b/src/controllers/admin-controller/chirpstack/service-profile.controller.ts deleted file mode 100644 index 2f1b1412..00000000 --- a/src/controllers/admin-controller/chirpstack/service-profile.controller.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { ComposeAuthGuard } from '@auth/compose-auth.guard'; -import { - BadRequestException, - Body, - Controller, - Delete, - Get, - HttpCode, - InternalServerErrorException, - Logger, - NotFoundException, - Param, - Post, - Put, - Query, - Req, - UseGuards, -} from "@nestjs/common"; -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiNotFoundResponse, - ApiOperation, - ApiProduces, - ApiTags, -} from "@nestjs/swagger"; - -import { Read, ApplicationAdmin } from "@auth/roles.decorator"; -import { RolesGuard } from "@auth/roles.guard"; -import { CreateChirpstackProfileResponseDto } from "@dto/chirpstack/create-chirpstack-profile-response.dto"; -import { CreateServiceProfileDto } from "@dto/chirpstack/create-service-profile.dto"; -import { ListAllServiceProfilesResponseDto } from "@dto/chirpstack/list-all-service-profiles-response.dto"; -import { UpdateServiceProfileDto } from "@dto/chirpstack/update-service-profile.dto"; -import { DeleteResponseDto } from "@dto/delete-application-response.dto"; -import { ErrorCodes } from "@enum/error-codes.enum"; -import { ServiceProfileService } from "@services/chirpstack/service-profile.service"; -import { AuditLog } from "@services/audit-log.service"; -import { ActionType } from "@entities/audit-log-entry"; -import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; - -@ApiTags("Chirpstack") -@Controller("chirpstack/service-profiles") -@UseGuards(ComposeAuthGuard, RolesGuard) -@ApiBearerAuth() -@ApplicationAdmin() -export class ServiceProfileController { - constructor(private serviceProfileService: ServiceProfileService) {} - private readonly logger = new Logger(ServiceProfileController.name); - - @Post() - @ApiProduces("application/json") - @ApiOperation({ summary: "Create a new ServiceProfile" }) - @ApiBadRequestResponse() - @ApplicationAdmin() - async create( - @Req() req: AuthenticatedRequest, - @Body() createDto: CreateServiceProfileDto - ): Promise { - const res = await this.serviceProfileService.createServiceProfile(createDto); - AuditLog.success( - ActionType.CREATE, - "ChirpstackServiceProfile", - req.user.userId, - res.data.id, - createDto.serviceProfile.name - ); - return res.data; - } - - @Put(":id") - @ApiProduces("application/json") - @ApiOperation({ summary: "Update an existing ServiceProfile" }) - @ApiBadRequestResponse() - @HttpCode(204) - @ApplicationAdmin() - async update( - @Req() req: AuthenticatedRequest, - @Param("id") id: string, - @Body() updateDto: UpdateServiceProfileDto - ): Promise { - const result = await this.serviceProfileService.updateServiceProfile( - updateDto, - id - ); - - if (result.status != 200) { - AuditLog.fail( - ActionType.UPDATE, - "ChirpstackServiceProfile", - req.user.userId, - updateDto.serviceProfile.id, - updateDto.serviceProfile.name - ); - throw new InternalServerErrorException(result.data); - } - AuditLog.success( - ActionType.UPDATE, - "ChirpstackServiceProfile", - req.user.userId, - updateDto.serviceProfile.id, - updateDto.serviceProfile.name - ); - return; - } - - @Get(":id") - @ApiProduces("application/json") - @ApiOperation({ summary: "Find one ServiceProfile by id" }) - @ApiNotFoundResponse() - @Read() - async findOne(@Param("id") id: string): Promise { - return await this.serviceProfileService.findOneServiceProfileById(id); - } - - @Get() - @ApiProduces("application/json") - @ApiOperation({ summary: "Find all ServiceProfile" }) - @Read() - async getAll( - @Query("limit") limit: number, - @Query("offset") offset: number - ): Promise { - this.logger.debug(`Limit: '${limit}' Offset:'${offset}'`); - const res = await this.serviceProfileService.findAllServiceProfiles( - limit || 50, - offset || 0 - ); - - return res; - } - - @Delete(":id") - @ApiOperation({ summary: "Delete one ServiceProfile by id" }) - @ApiNotFoundResponse() - @ApplicationAdmin() - async deleteOne( - @Req() req: AuthenticatedRequest, - @Param("id") id: string - ): Promise { - try { - const result = await this.serviceProfileService.deleteServiceProfile(id); - if (!result) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } - AuditLog.success( - ActionType.DELETE, - "ChirpstackServiceProfile", - req.user.userId, - id - ); - - return new DeleteResponseDto(1); - } catch (err) { - this.logger.error( - `Error occured during delete: '${JSON.stringify(err?.response?.data)}'` - ); - if ( - err?.message == "this object is used by other objects, remove them first" - ) { - throw new BadRequestException(ErrorCodes.IsUsed); - } - AuditLog.fail( - ActionType.DELETE, - "ChirpstackServiceProfile", - req.user.userId, - id - ); - throw err; - } - } -} diff --git a/src/controllers/admin-controller/iot-device.controller.ts b/src/controllers/admin-controller/iot-device.controller.ts index 7069a32f..a81fe8d8 100644 --- a/src/controllers/admin-controller/iot-device.controller.ts +++ b/src/controllers/admin-controller/iot-device.controller.ts @@ -35,15 +35,11 @@ import { LoRaWANDeviceWithChirpstackDataDto } from "@dto/lorawan-device-with-chi import { UpdateIoTDeviceDto } from "@dto/update-iot-device.dto"; import { IoTDevice } from "@entities/iot-device.entity"; import { ErrorCodes } from "@enum/error-codes.enum"; -import { - ApplicationAccessScope, - checkIfUserHasAccessToApplication, -} from "@helpers/security-helper"; +import { ApplicationAccessScope, checkIfUserHasAccessToApplication } from "@helpers/security-helper"; import { IoTDeviceService } from "@services/device-management/iot-device.service"; import { SigFoxDeviceWithBackendDataDto } from "@dto/sigfox-device-with-backend-data.dto"; import { CreateIoTDeviceDownlinkDto } from "@dto/create-iot-device-downlink.dto"; import { IoTDeviceDownlinkService } from "@services/device-management/iot-device-downlink.service"; -import { CreateChirpstackDeviceQueueItemResponse } from "@dto/chirpstack/create-chirpstack-device-queue-item.dto"; import { ChirpstackDeviceService } from "@services/chirpstack/chirpstack-device.service"; import { DeviceDownlinkQueueResponseDto } from "@dto/chirpstack/chirpstack-device-downlink-queue-response.dto"; import { IoTDeviceType } from "@enum/device-type.enum"; @@ -62,6 +58,7 @@ import { DeviceStatsResponseDto } from "@dto/chirpstack/device/device-stats.resp import { GenericHTTPDevice } from "@entities/generic-http-device.entity"; import { MQTTInternalBrokerDeviceDTO } from "@dto/mqtt-internal-broker-device.dto"; import { MQTTExternalBrokerDeviceDTO } from "@dto/mqtt-external-broker-device.dto"; +import { IdResponse } from "@interfaces/chirpstack-id-response.interface"; @ApiTags("IoT Device") @Controller("iot-device") @@ -94,10 +91,7 @@ export class IoTDeviceController { > { let result = undefined; try { - result = await this.iotDeviceService.findOneWithApplicationAndMetadata( - id, - true - ); + result = await this.iotDeviceService.findOneWithApplicationAndMetadata(id, true); } catch (err) { this.logger.error(`Error occured during findOne: '${JSON.stringify(err)}'`); } @@ -106,11 +100,7 @@ export class IoTDeviceController { throw new NotFoundException(ErrorCodes.IdDoesNotExists); } - checkIfUserHasAccessToApplication( - req, - result.application.id, - ApplicationAccessScope.Read - ); + checkIfUserHasAccessToApplication(req, result.application.id, ApplicationAccessScope.Read); return result; } @@ -123,10 +113,7 @@ export class IoTDeviceController { ): Promise { let device = undefined; try { - device = await this.iotDeviceService.findOneWithApplicationAndMetadata( - id, - true - ); + device = await this.iotDeviceService.findOneWithApplicationAndMetadata(id, true); } catch (err) { this.logger.error(`Error occured during findOne: '${JSON.stringify(err)}'`); } @@ -134,16 +121,10 @@ export class IoTDeviceController { if (!device) { throw new NotFoundException(ErrorCodes.IdDoesNotExists); } - checkIfUserHasAccessToApplication( - req, - device.application.id, - ApplicationAccessScope.Read - ); - if (device.type == IoTDeviceType.LoRaWAN) { - return this.chirpstackDeviceService.getDownlinkQueue( - (device as LoRaWANDevice).deviceEUI - ); - } else if (device.type == IoTDeviceType.SigFox) { + checkIfUserHasAccessToApplication(req, device.application.id, ApplicationAccessScope.Read); + if (device.type === IoTDeviceType.LoRaWAN) { + return this.chirpstackDeviceService.getDownlinkQueue((device as LoRaWANDevice).deviceEUI); + } else if (device.type === IoTDeviceType.SigFox) { return this.iotDeviceService.getDownlinkForSigfox(device as SigFoxDevice); } else { throw new BadRequestException(ErrorCodes.OnlyAllowedForLoRaWANAndSigfox); @@ -159,11 +140,7 @@ export class IoTDeviceController { @Param("id", new ParseIntPipe()) id: number ): Promise { const device = await this.iotDeviceService.findOne(id); - checkIfUserHasAccessToApplication( - req, - device.application.id, - ApplicationAccessScope.Read - ); + checkIfUserHasAccessToApplication(req, device.application.id, ApplicationAccessScope.Read); return this.iotDeviceService.findStats(device); } @@ -172,32 +149,15 @@ export class IoTDeviceController { @Header("Cache-Control", "none") @ApiOperation({ summary: "Create a new IoTDevice" }) @ApiBadRequestResponse() - async create( - @Req() req: AuthenticatedRequest, - @Body() createDto: CreateIoTDeviceDto - ): Promise { + async create(@Req() req: AuthenticatedRequest, @Body() createDto: CreateIoTDeviceDto): Promise { try { - checkIfUserHasAccessToApplication( - req, - createDto.applicationId, - ApplicationAccessScope.Write - ); + checkIfUserHasAccessToApplication(req, createDto.applicationId, ApplicationAccessScope.Write); const device = await this.iotDeviceService.create(createDto, req.user.userId); - AuditLog.success( - ActionType.CREATE, - IoTDevice.name, - req.user.userId, - device.id, - device.name - ); + AuditLog.success(ActionType.CREATE, IoTDevice.name, req.user.userId, device.id, device.name); return device; } catch (err) { AuditLog.fail(ActionType.CREATE, IoTDevice.name, req.user.userId); - this.logger.error( - `Failed to create IoTDevice from dto: ${JSON.stringify( - createDto - )}. Error: ${err}` - ); + this.logger.error(`Failed to create IoTDevice from dto: ${JSON.stringify(createDto)}. Error: ${err}`); throw err; } } @@ -210,19 +170,13 @@ export class IoTDeviceController { @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number, @Body() dto: CreateIoTDeviceDownlinkDto - ): Promise { + ): Promise { try { - const device = await this.iotDeviceService.findOneWithApplicationAndMetadata( - id - ); + const device = await this.iotDeviceService.findOneWithApplicationAndMetadata(id); if (!device) { throw new NotFoundException(); } - checkIfUserHasAccessToApplication( - req, - device?.application?.id, - ApplicationAccessScope.Write - ); + checkIfUserHasAccessToApplication(req, device?.application?.id, ApplicationAccessScope.Write); const result = await this.downlinkService.createDownlink(dto, device); AuditLog.success(ActionType.CREATE, "Downlink", req.user.userId); return result; @@ -242,41 +196,20 @@ export class IoTDeviceController { @Body() updateDto: UpdateIoTDeviceDto ): Promise { // Old application - const oldIotDevice = await this.iotDeviceService.findOneWithApplicationAndMetadata( - id, - false - ); + const oldIotDevice = await this.iotDeviceService.findOneWithApplicationAndMetadata(id, false); try { - checkIfUserHasAccessToApplication( - req, - oldIotDevice.application.id, - ApplicationAccessScope.Write - ); + checkIfUserHasAccessToApplication(req, oldIotDevice.application.id, ApplicationAccessScope.Write); if (updateDto.applicationId !== oldIotDevice.application.id) { // New application - checkIfUserHasAccessToApplication( - req, - updateDto.applicationId, - ApplicationAccessScope.Write - ); + checkIfUserHasAccessToApplication(req, updateDto.applicationId, ApplicationAccessScope.Write); } } catch (err) { AuditLog.fail(ActionType.UPDATE, IoTDevice.name, req.user.userId, id); throw err; } - const iotDevice = await this.iotDeviceService.update( - id, - updateDto, - req.user.userId - ); - AuditLog.success( - ActionType.UPDATE, - IoTDevice.name, - req.user.userId, - iotDevice.id, - iotDevice.name - ); + const iotDevice = await this.iotDeviceService.update(id, updateDto, req.user.userId); + AuditLog.success(ActionType.UPDATE, IoTDevice.name, req.user.userId, iotDevice.id, iotDevice.name); return iotDevice; } @@ -290,22 +223,13 @@ export class IoTDeviceController { ): Promise { try { createDto.data.forEach(createDto => - checkIfUserHasAccessToApplication( - req, - createDto.applicationId, - ApplicationAccessScope.Write - ) + checkIfUserHasAccessToApplication(req, createDto.applicationId, ApplicationAccessScope.Write) ); - const devices = await this.iotDeviceService.createMany( - createDto.data, - req.user.userId - ); + const devices = await this.iotDeviceService.createMany(createDto.data, req.user.userId); // Iterate through the devices once, splitting it into a tuple with the data we want to log - const { deviceIds, deviceNames } = buildIoTDeviceCreateUpdateAuditData( - devices - ); + const { deviceIds, deviceNames } = buildIoTDeviceCreateUpdateAuditData(devices); if (!deviceIds.length) { AuditLog.fail(ActionType.CREATE, IoTDevice.name, req.user.userId); @@ -321,11 +245,7 @@ export class IoTDeviceController { return devices; } catch (err) { AuditLog.fail(ActionType.CREATE, IoTDevice.name, req.user.userId); - this.logger.error( - `Failed to create IoTDevice from dto: ${JSON.stringify( - createDto - )}. Error: ${err}` - ); + this.logger.error(`Failed to create IoTDevice from dto: ${JSON.stringify(createDto)}. Error: ${err}`); throw err; } } @@ -346,12 +266,7 @@ export class IoTDeviceController { try { validDevices.data = updateDto.data.reduce( - ensureIoTDeviceUpdatePayload( - validDevices, - oldIotDevices, - devicesNotFound, - req - ), + ensureIoTDeviceUpdatePayload(validDevices, oldIotDevices, devicesNotFound, req), [] ); } catch (err) { @@ -388,15 +303,8 @@ export class IoTDeviceController { @Param("id", new ParseIntPipe()) id: number ): Promise { try { - const oldIotDevice = await this.iotDeviceService.findOneWithApplicationAndMetadata( - id, - false - ); - checkIfUserHasAccessToApplication( - req, - oldIotDevice?.application?.id, - ApplicationAccessScope.Write - ); + const oldIotDevice = await this.iotDeviceService.findOneWithApplicationAndMetadata(id, false); + checkIfUserHasAccessToApplication(req, oldIotDevice?.application?.id, ApplicationAccessScope.Write); const result = await this.iotDeviceService.delete(oldIotDevice); AuditLog.success(ActionType.DELETE, IoTDevice.name, req.user.userId, id); return new DeleteResponseDto(result.affected); @@ -415,21 +323,13 @@ export class IoTDeviceController { ): Promise> { try { const oldIotDevice = await this.iotDeviceService.findOne(id); - checkIfUserHasAccessToApplication( - req, - oldIotDevice?.application?.id, - ApplicationAccessScope.Write - ); + checkIfUserHasAccessToApplication(req, oldIotDevice?.application?.id, ApplicationAccessScope.Write); if (oldIotDevice.type !== IoTDeviceType.GenericHttp) { - throw new BadRequestException( - "The requested device is not a generic HTTP device" - ); + throw new BadRequestException("The requested device is not a generic HTTP device"); } - const result = await this.iotDeviceService.resetHttpDeviceApiKey( - oldIotDevice as GenericHTTPDevice - ); + const result = await this.iotDeviceService.resetHttpDeviceApiKey(oldIotDevice as GenericHTTPDevice); AuditLog.success(ActionType.UPDATE, IoTDevice.name, req.user.userId, id); return { apiKey: result.apiKey, @@ -450,14 +350,8 @@ export class IoTDeviceController { @Param("applicationId", new ParseIntPipe()) applicationId: number ): Promise { try { - checkIfUserHasAccessToApplication( - req, - applicationId, - ApplicationAccessScope.Read - ); - const csvFile = await this.iotDeviceService.getDevicesMetadataCsv( - applicationId - ); + checkIfUserHasAccessToApplication(req, applicationId, ApplicationAccessScope.Read); + const csvFile = await this.iotDeviceService.getDevicesMetadataCsv(applicationId); return new StreamableFile(csvFile); } catch (err) { this.logger.error(err); diff --git a/src/controllers/admin-controller/multicast.controller.ts b/src/controllers/admin-controller/multicast.controller.ts index 59c94b45..dd46fe7c 100644 --- a/src/controllers/admin-controller/multicast.controller.ts +++ b/src/controllers/admin-controller/multicast.controller.ts @@ -15,7 +15,6 @@ import { ParseIntPipe, Logger, } from "@nestjs/common"; -import { MulticastService } from "../../services/device-management/multicast.service"; import { CreateMulticastDto } from "../../entities/dto/create-multicast.dto"; import { UpdateMulticastDto } from "../../entities/dto/update-multicast.dto"; import { @@ -28,13 +27,10 @@ import { } from "@nestjs/swagger"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { Multicast } from "@entities/multicast.entity"; -import { - checkIfUserHasAccessToApplication, - ApplicationAccessScope, -} from "@helpers/security-helper"; +import { checkIfUserHasAccessToApplication, ApplicationAccessScope } from "@helpers/security-helper"; import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; -import { ComposeAuthGuard } from '@auth/compose-auth.guard'; +import { ComposeAuthGuard } from "@auth/compose-auth.guard"; import { RolesGuard } from "@auth/roles.guard"; import { Read, ApplicationAdmin } from "@auth/roles.decorator"; import { ListAllMulticastsDto } from "@dto/list-all-multicasts.dto"; @@ -44,6 +40,7 @@ import { DeleteResponseDto } from "@dto/delete-application-response.dto"; import { MulticastDownlinkQueueResponseDto } from "@dto/chirpstack/chirpstack-multicast-downlink-queue-response.dto"; import { CreateMulticastDownlinkDto } from "@dto/create-multicast-downlink.dto"; import { CreateChirpstackMulticastQueueItemResponse } from "@dto/chirpstack/create-chirpstack-multicast-queue-item.dto"; +import { MulticastService } from "@services/chirpstack/multicast.service"; @ApiTags("Multicast") @UseGuards(ComposeAuthGuard, RolesGuard) @@ -59,27 +56,11 @@ export class MulticastController { @Post() @ApiOperation({ summary: "Create a new multicast" }) @ApiBadRequestResponse() - async create( - @Req() req: AuthenticatedRequest, - @Body() createMulticastDto: CreateMulticastDto - ): Promise { + async create(@Req() req: AuthenticatedRequest, @Body() createMulticastDto: CreateMulticastDto): Promise { try { - checkIfUserHasAccessToApplication( - req, - createMulticastDto.applicationID, - ApplicationAccessScope.Write - ); - const multicast = await this.multicastService.create( - createMulticastDto, - req.user.userId - ); - AuditLog.success( - ActionType.CREATE, - Multicast.name, - req.user.userId, - multicast.id, - multicast.groupName - ); + checkIfUserHasAccessToApplication(req, createMulticastDto.applicationID, ApplicationAccessScope.Write); + const multicast = await this.multicastService.create(createMulticastDto, req.user.userId); + AuditLog.success(ActionType.CREATE, Multicast.name, req.user.userId, multicast.id, multicast.groupName); return multicast; } catch (err) { AuditLog.fail(ActionType.CREATE, Multicast.name, req.user.userId); @@ -106,26 +87,16 @@ export class MulticastController { throw new UnauthorizedException(); } - return await this.multicastService.findAndCountAllWithPagination( - query, - allowed - ); + return await this.multicastService.findAndCountAllWithPagination(query, allowed); } } @Get(":id") @ApiOperation({ summary: "Find Multicast by id" }) - async findOne( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { + async findOne(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise { try { const multicast = await this.multicastService.findOne(id); - checkIfUserHasAccessToApplication( - req, - multicast.application.id, - ApplicationAccessScope.Read - ); + checkIfUserHasAccessToApplication(req, multicast.application.id, ApplicationAccessScope.Read); return multicast; } catch (err) { throw new NotFoundException(ErrorCodes.IdDoesNotExists); @@ -148,15 +119,9 @@ export class MulticastController { if (!multicast) { throw new NotFoundException(ErrorCodes.IdDoesNotExists); } - checkIfUserHasAccessToApplication( - req, - multicast.application.id, - ApplicationAccessScope.Read - ); + checkIfUserHasAccessToApplication(req, multicast.application.id, ApplicationAccessScope.Read); - return this.multicastService.getDownlinkQueue( - multicast.lorawanMulticastDefinition.chirpstackGroupId - ); + return this.multicastService.getDownlinkQueue(multicast.lorawanMulticastDefinition.chirpstackGroupId); } @Post(":id/downlink-multicast") @@ -173,11 +138,7 @@ export class MulticastController { if (!multicast) { throw new NotFoundException(); } - checkIfUserHasAccessToApplication( - req, - multicast.application.id, - ApplicationAccessScope.Write - ); + checkIfUserHasAccessToApplication(req, multicast.application.id, ApplicationAccessScope.Write); const result = await this.multicastService.createDownlink(dto, multicast); AuditLog.success(ActionType.CREATE, "Downlink", req.user.userId); return result; @@ -198,17 +159,9 @@ export class MulticastController { ): Promise { const oldMulticast = await this.multicastService.findOne(id); try { - checkIfUserHasAccessToApplication( - req, - oldMulticast.application.id, - ApplicationAccessScope.Write - ); + checkIfUserHasAccessToApplication(req, oldMulticast.application.id, ApplicationAccessScope.Write); if (oldMulticast.application.id !== updateDto.applicationID) { - checkIfUserHasAccessToApplication( - req, - updateDto.applicationID, - ApplicationAccessScope.Write - ); + checkIfUserHasAccessToApplication(req, updateDto.applicationID, ApplicationAccessScope.Write); } } catch (err) { AuditLog.fail( @@ -221,11 +174,7 @@ export class MulticastController { throw err; } - const multicast = await this.multicastService.update( - oldMulticast, - updateDto, - req.user.userId - ); + const multicast = await this.multicastService.update(oldMulticast, updateDto, req.user.userId); AuditLog.success( ActionType.UPDATE, Multicast.name, @@ -246,12 +195,8 @@ export class MulticastController { ): Promise { try { const multicast = await this.multicastService.findOne(id); - checkIfUserHasAccessToApplication( - req, - multicast.application.id, - ApplicationAccessScope.Write - ); - const result = await this.multicastService.multicastDelete(id, multicast); + checkIfUserHasAccessToApplication(req, multicast.application.id, ApplicationAccessScope.Write); + const result = await this.multicastService.deleteMulticast(id, multicast); if (result.affected === 0) { throw new NotFoundException(ErrorCodes.IdDoesNotExists); diff --git a/src/entities/application.entity.ts b/src/entities/application.entity.ts index f02a8726..3a3862e7 100644 --- a/src/entities/application.entity.ts +++ b/src/entities/application.entity.ts @@ -113,4 +113,7 @@ export class Application extends DbBaseEntity { cascade: true, }) deviceTypes?: ApplicationDeviceType[]; + + @Column({ nullable: true }) + chirpstackId?: string; } diff --git a/src/entities/base.entity.ts b/src/entities/base.entity.ts index 2e896822..da07bc9d 100644 --- a/src/entities/base.entity.ts +++ b/src/entities/base.entity.ts @@ -1,10 +1,4 @@ -import { - CreateDateColumn, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from "typeorm"; +import { CreateDateColumn, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; /** * This class contains all the values which are stored by default on all entities. diff --git a/src/entities/dto/chirpstack/chirpstack-application-response.dto.ts b/src/entities/dto/chirpstack/chirpstack-application-response.dto.ts index c058724e..e80434c5 100644 --- a/src/entities/dto/chirpstack/chirpstack-application-response.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-application-response.dto.ts @@ -2,7 +2,5 @@ export class ChirpstackApplicationResponseDto { id: string; name: string; description: string; - organizationID: string; - serviceProfileID: string; - serviceProfileName: string; + tenantId?: string; } diff --git a/src/entities/dto/chirpstack/chirpstack-application.dto.ts b/src/entities/dto/chirpstack/chirpstack-application.dto.ts index f5517355..06365798 100644 --- a/src/entities/dto/chirpstack/chirpstack-application.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-application.dto.ts @@ -1,6 +1,4 @@ export class ChirpstackApplicationDto { name: string; description: string; - organizationID: string; - serviceProfileID: string; } diff --git a/src/entities/dto/chirpstack/chirpstack-device-contents.dto.ts b/src/entities/dto/chirpstack/chirpstack-device-contents.dto.ts index 47564260..8ef466b8 100644 --- a/src/entities/dto/chirpstack/chirpstack-device-contents.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-device-contents.dto.ts @@ -1,5 +1,5 @@ import { ApiHideProperty, ApiProperty } from "@nestjs/swagger"; -import { IsHexadecimal, IsOptional, IsString, IsUUID, Length, Matches } from "class-validator"; +import { IsHexadecimal, IsOptional, IsString, IsUUID, Length } from "class-validator"; export class ChirpstackDeviceContentsDto { @ApiHideProperty() @@ -14,17 +14,13 @@ export class ChirpstackDeviceContentsDto { @ApiProperty({ required: true }) @IsString() @IsHexadecimal() - @Length(16,16) + @Length(16, 16) devEUI: string; @ApiProperty({ required: true }) @IsUUID() deviceProfileID: string; - @ApiProperty({ required: true }) - @IsUUID() - serviceProfileID: string; - @ApiProperty({ required: false, default: false }) @IsOptional() isDisabled?: boolean; @@ -34,16 +30,14 @@ export class ChirpstackDeviceContentsDto { skipFCntCheck?: boolean; @ApiProperty({ required: false, default: {} }) - variables?: JSON; + variables?: Array<[string, string]>; @ApiProperty({ required: false, default: {} }) - tags?: JSON; - - @ApiProperty({ required: false, default: {} }) - OTAAapplicationKey: string; + tags?: Array<[string, string]>; @ApiHideProperty() deviceStatusBattery?: number; + @ApiHideProperty() deviceStatusMargin?: number; } diff --git a/src/entities/dto/chirpstack/chirpstack-device-keys-response.dto.ts b/src/entities/dto/chirpstack/chirpstack-device-keys-response.dto.ts index c041b78c..7b9a7ada 100644 --- a/src/entities/dto/chirpstack/chirpstack-device-keys-response.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-device-keys-response.dto.ts @@ -6,5 +6,4 @@ export class ChirpstackDeviceKeysContentDto { devEUI: string; nwkKey: string; appKey: string; - genAppKey: string; } diff --git a/src/entities/dto/chirpstack/chirpstack-device-metrics.dto.ts b/src/entities/dto/chirpstack/chirpstack-device-metrics.dto.ts new file mode 100644 index 00000000..294642df --- /dev/null +++ b/src/entities/dto/chirpstack/chirpstack-device-metrics.dto.ts @@ -0,0 +1,13 @@ +export class DeviceMetricsDto { + [timestamp: string]: { + rssi: number; + snr: number; + rxPacketsPerDr: Record; + }; +} + +export enum MetricProperties { + rssi = "rssi", + snr = "snr", + dr = "rxPacketsPerDr", +} diff --git a/src/entities/dto/chirpstack/chirpstack-many-device-response.ts b/src/entities/dto/chirpstack/chirpstack-many-device-response.ts index 1d245102..cc7633c7 100644 --- a/src/entities/dto/chirpstack/chirpstack-many-device-response.ts +++ b/src/entities/dto/chirpstack/chirpstack-many-device-response.ts @@ -1,19 +1,16 @@ -export interface ChirpstackManyDeviceResponseContents { +export interface ChirpstackDeviceResponseContents { devEUI: string; name: string; - applicationID: string; description: string; deviceProfileID: string; deviceProfileName: string; deviceStatusBattery: number; deviceStatusMargin: number; deviceStatusExternalPowerSource: boolean; - deviceStatusBatteryLevelUnavailable: boolean; - deviceStatusBatteryLevel: number; lastSeenAt?: Date; } export interface ChirpstackManyDeviceResponseDto { totalCount: string; - result: ChirpstackManyDeviceResponseContents[]; + result: ChirpstackDeviceResponseContents[]; } diff --git a/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts b/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts index 9136b803..fd7b7eae 100644 --- a/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts @@ -1,15 +1,13 @@ export class ChirpstackMQTTMessageDto { adr: boolean; - applicationID: string; - applicationName: string; data: string; - devEUI: string; - deviceName: string; fCnt: number; fPort: number; + deviceInfo: ChirpstackMQTTMessageDeviceInfo; txInfo: ChirpstackMQTTMessageTxInfoDto; dr: number; frequency: number; + confirmed: boolean } export class ChirpstackMQTTMessageTxInfoDto { @@ -21,3 +19,10 @@ export class ChirpstackMQTTConnectionStateMessageDto { gatewayId: string; isOnline: boolean; } + +export class ChirpstackMQTTMessageDeviceInfo { + applicationID: string; + applicationName: string; + devEui: string; + deviceName: string; +} diff --git a/src/entities/dto/chirpstack/chirpstack-multicast-contents.dto.ts b/src/entities/dto/chirpstack/chirpstack-multicast-contents.dto.ts index d8297465..9228164e 100644 --- a/src/entities/dto/chirpstack/chirpstack-multicast-contents.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-multicast-contents.dto.ts @@ -1,27 +1,34 @@ import { multicastGroup } from "@enum/multicast-type.enum"; import { ApiProperty } from "@nestjs/swagger"; -import { Matches } from "class-validator"; export class ChirpstackMulticastContentsDto { @ApiProperty({ required: true }) applicationID: string; + @ApiProperty({ required: true }) dr: number; + @ApiProperty({ required: true }) fCnt: number; + @ApiProperty({ required: true }) frequency: number; + @ApiProperty({ required: true }) groupType: multicastGroup; + @ApiProperty({ required: true }) mcAddr: string; + @ApiProperty({ required: true }) mcAppSKey: string; + @ApiProperty({ required: true }) mcNwkSKey: string; + @ApiProperty({ required: true }) name: string; - @ApiProperty({ required: false }) - pingSlotPeriod: number; - + + @ApiProperty({ required: true }) + id: string; } diff --git a/src/entities/dto/chirpstack/chirpstack-multicast-downlink-queue-response.dto.ts b/src/entities/dto/chirpstack/chirpstack-multicast-downlink-queue-response.dto.ts index 089f7189..13b8f9f6 100644 --- a/src/entities/dto/chirpstack/chirpstack-multicast-downlink-queue-response.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-multicast-downlink-queue-response.dto.ts @@ -1,13 +1,10 @@ export interface MulticastQueueItem { - devEUI?: string; - confirmed?: boolean; + multicastGroupId?: string; fCnt?: number; fPort?: number; data: string; - jsonObject?: string; } export interface MulticastDownlinkQueueResponseDto { deviceQueueItems: MulticastQueueItem[]; - totalCount: number; } diff --git a/src/entities/dto/chirpstack/common-location.dto.ts b/src/entities/dto/chirpstack/common-location.dto.ts index 806a7319..250d5948 100644 --- a/src/entities/dto/chirpstack/common-location.dto.ts +++ b/src/entities/dto/chirpstack/common-location.dto.ts @@ -1,4 +1,5 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { LocationSourceMap } from "@chirpstack/chirpstack-api/common/common_pb"; +import { ApiHideProperty, ApiProperty } from "@nestjs/swagger"; import { IsOptional, IsString, Max, Min } from "class-validator"; export class CommonLocationDto { @@ -16,17 +17,8 @@ export class CommonLocationDto { @IsOptional() altitude?: number; - @ApiProperty({ required: false }) - @IsString() - @IsOptional() - source?: - | "UNKNOWN" - | "GPS" - | "CONFIG" - | "GEO_RESOLVER_TDOA" - | "GEO_RESOLVER_RSSI" - | "GEO_RESOLVER_GNSS" - | "GEO_RESOLVER_WIFI"; + @ApiHideProperty() + source?: LocationSourceMap[keyof LocationSourceMap] @ApiProperty({ required: false }) @IsOptional() diff --git a/src/entities/dto/chirpstack/create-device-profile.dto.ts b/src/entities/dto/chirpstack/create-device-profile.dto.ts index 7b631ed7..f4efcc37 100644 --- a/src/entities/dto/chirpstack/create-device-profile.dto.ts +++ b/src/entities/dto/chirpstack/create-device-profile.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiHideProperty, ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { ValidateNested } from "class-validator"; @@ -12,4 +12,10 @@ export class CreateDeviceProfileDto { @ApiProperty({ required: true }) internalOrganizationId: number; + + @ApiProperty({ required: false }) + createdAt: Date; + + @ApiProperty({ required: false }) + updatedAt: Date; } diff --git a/src/entities/dto/chirpstack/create-network-server.dto.ts b/src/entities/dto/chirpstack/create-network-server.dto.ts deleted file mode 100644 index 178c7771..00000000 --- a/src/entities/dto/chirpstack/create-network-server.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { ValidateNested } from "class-validator"; - -import { NetworkServerDto } from "./network-server.dto"; - -export class CreateNetworkServerDto { - @ApiProperty({ required: true }) - @ValidateNested({ each: true }) - @Type(() => NetworkServerDto) - networkServer: NetworkServerDto; -} diff --git a/src/entities/dto/chirpstack/create-service-profile.dto.ts b/src/entities/dto/chirpstack/create-service-profile.dto.ts deleted file mode 100644 index c6882706..00000000 --- a/src/entities/dto/chirpstack/create-service-profile.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { ValidateNested } from "class-validator"; - -import { ServiceProfileDto } from "./service-profile.dto"; - -export class CreateServiceProfileDto { - @ApiProperty({ required: true }) - @ValidateNested({ each: true }) - @Type(() => ServiceProfileDto) - serviceProfile: ServiceProfileDto; -} diff --git a/src/entities/dto/chirpstack/device-profile.dto.ts b/src/entities/dto/chirpstack/device-profile.dto.ts index 51df9b1c..f849dcc5 100644 --- a/src/entities/dto/chirpstack/device-profile.dto.ts +++ b/src/entities/dto/chirpstack/device-profile.dto.ts @@ -1,14 +1,6 @@ +import { MacVersionMap, RegParamsRevisionMap } from "@chirpstack/chirpstack-api/common/common_pb"; import { ApiHideProperty, ApiProperty } from "@nestjs/swagger"; -import { - IsArray, - IsInt, - IsNumber, - IsOptional, - IsString, - Length, - Min, - ValidateIf, -} from "class-validator"; +import { IsInt, IsNotEmpty, IsOptional, IsString, Length, Min, ValidateIf } from "class-validator"; export class DeviceProfileDto { @ApiProperty({ required: true }) @@ -17,15 +9,12 @@ export class DeviceProfileDto { name: string; @ApiProperty({ required: true }) - macVersion: "1.0.0" | "1.0.1" | "1.0.2" | "1.0.3" | "1.1.0"; + @IsNotEmpty() + macVersion: MacVersionMap[keyof MacVersionMap]; @ApiProperty({ required: true }) - @IsInt() - @Min(0) - maxEIRP: number; - - @ApiProperty({ required: true }) - regParamsRevision: "A" | "B"; + @IsNotEmpty() + regParamsRevision: RegParamsRevisionMap[keyof RegParamsRevisionMap]; @ApiProperty({ required: false }) @IsString() @@ -42,37 +31,9 @@ export class DeviceProfileDto { @IsInt() classCTimeout?: number; - @ApiProperty({ required: true }) - @IsInt() - @Min(0) - geolocBufferTTL: number; - - @ApiProperty({ required: true }) - @IsInt() - @Min(0) - geolocMinBufferSize: number; - - @ApiProperty({ required: false }) - maxDutyCycle?: number; - @ApiProperty({ required: false }) id?: string; - @ApiHideProperty() - networkServerID?: string; - - @ApiHideProperty() - organizationID?: string; - - @ApiProperty({ required: false }) - payloadCodec?: string; - - @ApiProperty({ required: false }) - payloadDecoderScript?: string; - - @ApiProperty({ required: false }) - payloadEncoderScript?: string; - @ApiProperty({ required: false }) @ValidateIf((o: DeviceProfileDto) => o.supportsClassB) @Min(0) @@ -116,15 +77,6 @@ export class DeviceProfileDto { @IsInt() rxFreq2?: number; - @ApiProperty({ required: false }) - @ValidateIf((o: DeviceProfileDto) => o.supportsJoin == false) - @IsArray() - @IsNumber({ maxDecimalPlaces: 0 }, { each: true }) - factoryPresetFreqs: number[]; - - @ApiProperty({ required: false }) - supports32BitFCnt?: boolean; - @ApiProperty({ required: false }) supportsClassB?: boolean; @@ -134,8 +86,14 @@ export class DeviceProfileDto { @ApiProperty({ required: false }) supportsJoin?: boolean; + @ApiProperty({ required: false }) + devStatusReqFreq?: number; + @ApiHideProperty() - tags?: { [id: string]: string | number }; + tags?: { [id: string]: string }; + + @ApiHideProperty() + tagsMap?: Array<[string, string]>; @ApiHideProperty() internalOrganizationId?: number; @@ -145,4 +103,7 @@ export class DeviceProfileDto { @ApiHideProperty() createdBy?: number; + + @ApiHideProperty() + organizationID?: string; } diff --git a/src/entities/dto/chirpstack/device/lorawan-stats.response.dto.ts b/src/entities/dto/chirpstack/device/lorawan-stats.response.dto.ts index e4038de6..874daa01 100644 --- a/src/entities/dto/chirpstack/device/lorawan-stats.response.dto.ts +++ b/src/entities/dto/chirpstack/device/lorawan-stats.response.dto.ts @@ -3,11 +3,8 @@ export class LoRaWANStatsResponseDto { } export class LoRaWANStatsElementDto { - errors: Record; gwRssi: number; gwSnr: number; - rxPackets: number; rxPacketsPerDr: Record; - rxPacketsPerFrequency: Record; timestamp: string; } diff --git a/src/entities/dto/chirpstack/gateway-contents.dto.ts b/src/entities/dto/chirpstack/gateway-contents.dto.ts index f1876084..57c435ef 100644 --- a/src/entities/dto/chirpstack/gateway-contents.dto.ts +++ b/src/entities/dto/chirpstack/gateway-contents.dto.ts @@ -48,20 +48,14 @@ export class GatewayContentsDto { name: string; @ApiHideProperty() - networkServerID: string; - - @ApiHideProperty() - organizationID: string; + tenantId: string; @ApiProperty({ required: false }) @IsJSON() tagsString?: string; @ApiHideProperty() - tags?: { [id: string]: string | number }; - - @ApiHideProperty() - gatewayProfileID?: string; + tags?: { [id: string]: string }; @ApiHideProperty() id: string; diff --git a/src/entities/dto/chirpstack/gateway-response.dto.ts b/src/entities/dto/chirpstack/gateway-response.dto.ts index b91d3f58..71f4a27d 100644 --- a/src/entities/dto/chirpstack/gateway-response.dto.ts +++ b/src/entities/dto/chirpstack/gateway-response.dto.ts @@ -1,4 +1,23 @@ -import { CommonLocationDto } from "@dto/chirpstack/common-location.dto"; +import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb"; +import { CommonLocationDto } from "./common-location.dto"; + +export class ChirpstackGatewayResponseDto { + id?: number; + gatewayId: string; + name: string; + description?: string; + rxPacketsReceived?: number; + txPacketsEmitted?: number; + internalOrganizationId?: number; + internalOrganizationName?: string; + location: CommonLocationDto; + createdAt?: Timestamp.AsObject; + updatedAt?: Timestamp.AsObject; + lastSeenAt?: Timestamp.AsObject; + updatedBy?: number; + createdBy?: number; + tags?: { [id: string]: string }; +} export class GatewayResponseDto { id: number; diff --git a/src/entities/dto/chirpstack/gateway-stats.response.dto.ts b/src/entities/dto/chirpstack/gateway-stats.response.dto.ts index 885b361a..5de3cced 100644 --- a/src/entities/dto/chirpstack/gateway-stats.response.dto.ts +++ b/src/entities/dto/chirpstack/gateway-stats.response.dto.ts @@ -1,3 +1,5 @@ +import { Metric } from "@chirpstack/chirpstack-api/common/common_pb"; + export class GatewayStatsResponseDto { result: GatewayStatsElementDto[]; } @@ -5,7 +7,5 @@ export class GatewayStatsResponseDto { export class GatewayStatsElementDto { timestamp: string; rxPacketsReceived: number; - rxPacketsReceivedOK: number; - txPacketsReceived: number; txPacketsEmitted: number; } diff --git a/src/entities/dto/chirpstack/header.dto.ts b/src/entities/dto/chirpstack/header.dto.ts deleted file mode 100644 index bb36cbfd..00000000 --- a/src/entities/dto/chirpstack/header.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -export class HeaderDto { - @ApiProperty({ required: true }) - url: string; - - @ApiProperty({ required: true }) - timeout: number; - - @ApiProperty({ required: true }) - authorizationType: string; - - @ApiProperty({ required: true }) - authorizationHeader: string; -} diff --git a/src/entities/dto/chirpstack/list-all-applications-response.dto.ts b/src/entities/dto/chirpstack/list-all-applications-response.dto.ts index 1efabe5b..fa1fad16 100644 --- a/src/entities/dto/chirpstack/list-all-applications-response.dto.ts +++ b/src/entities/dto/chirpstack/list-all-applications-response.dto.ts @@ -1,6 +1,6 @@ import { ChirpstackApplicationResponseDto } from "./chirpstack-application-response.dto"; export class ListAllChirpstackApplicationsResponseDto { - result: ChirpstackApplicationResponseDto[]; + resultList: ChirpstackApplicationResponseDto[]; totalCount: number; } diff --git a/src/entities/dto/chirpstack/list-all-device-profiles-response.dto.ts b/src/entities/dto/chirpstack/list-all-device-profiles-response.dto.ts index a998569f..9e906979 100644 --- a/src/entities/dto/chirpstack/list-all-device-profiles-response.dto.ts +++ b/src/entities/dto/chirpstack/list-all-device-profiles-response.dto.ts @@ -1,6 +1,16 @@ import { DeviceProfileDto } from "./device-profile.dto"; export class ListAllDeviceProfilesResponseDto { - result: DeviceProfileDto[]; + result: DeviceProfileListDto[]; totalCount: string; } + +export class DeviceProfileListDto { + id: string; + name: string; + createdAt: Date; + createdBy?: number; + internalOrganizationId?: number; + updatedAt: Date; + updatedBy?: number; +} diff --git a/src/entities/dto/chirpstack/list-all-gateways.dto.ts b/src/entities/dto/chirpstack/list-all-gateways.dto.ts index 4fa6aaf4..3b0e4ab9 100644 --- a/src/entities/dto/chirpstack/list-all-gateways.dto.ts +++ b/src/entities/dto/chirpstack/list-all-gateways.dto.ts @@ -1,6 +1,11 @@ -import { GatewayResponseDto } from "@dto/chirpstack/gateway-response.dto"; +import { ChirpstackGatewayResponseDto, GatewayResponseDto } from "./gateway-response.dto"; export class ListAllGatewaysResponseDto { totalCount: number; - result: GatewayResponseDto[]; + resultList: GatewayResponseDto[]; +} + +export class ListAllChirpstackGatewaysResponseDto { + totalCount: number; + resultList: ChirpstackGatewayResponseDto[]; } diff --git a/src/entities/dto/chirpstack/list-all-network-server-response.dto.ts b/src/entities/dto/chirpstack/list-all-network-server-response.dto.ts deleted file mode 100644 index 053f9e98..00000000 --- a/src/entities/dto/chirpstack/list-all-network-server-response.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { NetworkServerDto } from "./network-server.dto"; - -export class ListAllNetworkServerResponseDto { - result: NetworkServerDto[]; - totalCount: number; -} diff --git a/src/entities/dto/chirpstack/list-all-service-profiles-response.dto.ts b/src/entities/dto/chirpstack/list-all-service-profiles-response.dto.ts deleted file mode 100644 index ea28f7ac..00000000 --- a/src/entities/dto/chirpstack/list-all-service-profiles-response.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ServiceProfileDto } from "./service-profile.dto"; - -export class ListAllServiceProfilesResponseDto { - result: ServiceProfileDto[]; - totalCount: string; -} diff --git a/src/entities/dto/chirpstack/network-server.dto.ts b/src/entities/dto/chirpstack/network-server.dto.ts deleted file mode 100644 index 2d2211a8..00000000 --- a/src/entities/dto/chirpstack/network-server.dto.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsOptional, IsString, MaxLength, MinLength } from "class-validator"; - -export class NetworkServerDto { - @ApiProperty({ required: true }) - @IsString() - @MaxLength(1024) - name: string; - - @ApiProperty({ required: true }) - @IsString() - @MaxLength(1024) - server: string; - - @ApiProperty({ required: false }) - @IsOptional() - id?: number; - - @ApiProperty({ required: false }) - @IsString() - @MinLength(1) - @MaxLength(50) - caCert?: string; - - @ApiProperty({ required: false }) - @IsOptional() - gatewayDiscoveryDR?: number; - - @ApiProperty({ required: false }) - @IsOptional() - gatewayDiscoveryEnabled?: boolean; - - @ApiProperty({ required: false }) - @IsOptional() - gatewayDiscoveryInterval?: number; - - @ApiProperty({ required: false }) - @IsOptional() - gatewayDiscoveryTXFrequency?: number; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - @MaxLength(1024) - routingProfileCACert?: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - @MaxLength(1024) - routingProfileTLSCert?: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - @MaxLength(1024) - routingProfileTLSKey?: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - @MaxLength(1024) - tlsCert?: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - @MaxLength(1024) - tlsKey?: string; -} diff --git a/src/entities/dto/chirpstack/service-profile.dto.ts b/src/entities/dto/chirpstack/service-profile.dto.ts deleted file mode 100644 index d0e3caf5..00000000 --- a/src/entities/dto/chirpstack/service-profile.dto.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { ApiHideProperty, ApiProperty } from "@nestjs/swagger"; -import { IsIn, IsInt, IsString, Length, Max, MaxLength, Min } from "class-validator"; - -export class ServiceProfileDto { - @ApiProperty({ required: false }) - id?: string; - - @ApiProperty({ required: true }) - @Length(1, 1024) - name: string; - - @ApiHideProperty() - networkServerID?: string; - - @ApiHideProperty() - addGWMetaData = true; - - @ApiProperty({ required: false }) - channelMask?: string; - - @ApiProperty({ required: false }) - devStatusReqFreq?: number; - - @ApiProperty({ required: false }) - dlBucketSize?: number; - - @ApiProperty({ required: false }) - dlRate?: number; - - @ApiProperty({ required: false }) - dlRatePolicy?: string; - - @ApiProperty({ required: false }) - @IsInt() - @Min(0, { message: "Max data rate må ikke være negativ" }) - @Max(7, { message: "Max data rate må ikke være større end 7" }) - drMax?: number; - - @ApiProperty({ required: true }) - @IsInt() - @Min(0, { message: "Min data rate må ikke være negativ" }) - @Max(7, { message: "Min data rate må ikke være større end 7" }) - drMin?: number; - - @ApiProperty({ required: false }) - hrAllowed?: boolean; - - @ApiProperty({ required: false }) - minGWDiversity?: number; - - @ApiHideProperty() - nwkGeoLoc = true; - - @ApiHideProperty() - organizationID?: string; - - @ApiProperty({ required: false }) - prAllowed?: boolean; - - @ApiProperty({ required: false }) - raAllowed?: boolean; - - @ApiProperty({ required: false }) - reportDevStatusBattery?: boolean; - - @ApiProperty({ required: false }) - reportDevStatusMargin?: boolean; - - @ApiProperty({ required: true }) - targetPER?: number; - - @ApiProperty({ required: false }) - ulBucketSize?: number; - - @ApiProperty({ required: false }) - ulRate?: number; - - @ApiProperty({ required: false }) - ulRatePolicy?: string; -} diff --git a/src/entities/dto/chirpstack/single-gateway-response.dto.ts b/src/entities/dto/chirpstack/single-gateway-response.dto.ts index d2cbae02..2580402c 100644 --- a/src/entities/dto/chirpstack/single-gateway-response.dto.ts +++ b/src/entities/dto/chirpstack/single-gateway-response.dto.ts @@ -3,5 +3,5 @@ import { GatewayResponseDto } from "@dto/chirpstack/gateway-response.dto"; export class SingleGatewayResponseDto { gateway: GatewayResponseDto; - stats: GatewayStatsElementDto[]; + stats?: GatewayStatsElementDto[]; } diff --git a/src/entities/dto/chirpstack/update-gateway.dto.ts b/src/entities/dto/chirpstack/update-gateway.dto.ts index d8e4f1f0..98c907cb 100644 --- a/src/entities/dto/chirpstack/update-gateway.dto.ts +++ b/src/entities/dto/chirpstack/update-gateway.dto.ts @@ -6,6 +6,12 @@ import { Type } from "class-transformer"; export class UpdateGatewayContentsDto extends OmitType(GatewayContentsDto, ["gatewayId"]) { @ApiHideProperty() gatewayId: string; + + @ApiHideProperty() + createdBy?: number; + + @ApiHideProperty() + updatedBy?: number; } export class UpdateGatewayDto { diff --git a/src/entities/dto/chirpstack/update-network-server.dto.ts b/src/entities/dto/chirpstack/update-network-server.dto.ts deleted file mode 100644 index 66d75153..00000000 --- a/src/entities/dto/chirpstack/update-network-server.dto.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { CreateNetworkServerDto } from "./create-network-server.dto"; - -export class UpdateNetworkServerDto extends CreateNetworkServerDto {} diff --git a/src/entities/dto/chirpstack/update-service-profile.dto.ts b/src/entities/dto/chirpstack/update-service-profile.dto.ts deleted file mode 100644 index 4020bb8c..00000000 --- a/src/entities/dto/chirpstack/update-service-profile.dto.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { CreateServiceProfileDto } from "./create-service-profile.dto"; - -export class UpdateServiceProfileDto extends CreateServiceProfileDto {} diff --git a/src/entities/dto/create-application.dto.ts b/src/entities/dto/create-application.dto.ts index ed85f630..632674be 100644 --- a/src/entities/dto/create-application.dto.ts +++ b/src/entities/dto/create-application.dto.ts @@ -6,16 +6,7 @@ import { IsSwaggerOptional } from "@helpers/optional-validator"; import { IsPhoneNumberString } from "@helpers/phone-number.validator"; import { nameof } from "@helpers/type-helper"; import { ApiProperty } from "@nestjs/swagger"; -import { - ArrayUnique, - IsArray, - IsBoolean, - IsEnum, - IsOptional, - IsString, - MaxLength, - MinLength, -} from "class-validator"; +import { ArrayUnique, IsArray, IsBoolean, IsEnum, IsOptional, IsString, MaxLength, MinLength } from "class-validator"; export class CreateApplicationDto { @ApiProperty({ required: true }) @@ -99,4 +90,10 @@ export class CreateApplicationDto { }, }) permissionIds?: number[]; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + @MaxLength(1024) + chirpstackId?: string; } diff --git a/src/entities/dto/create-lorawan-settings.dto.ts b/src/entities/dto/create-lorawan-settings.dto.ts index 383c10b1..86de0f66 100644 --- a/src/entities/dto/create-lorawan-settings.dto.ts +++ b/src/entities/dto/create-lorawan-settings.dto.ts @@ -16,7 +16,6 @@ import { ChirpstackDeviceContentsDto } from "./chirpstack/chirpstack-device-cont export class CreateLoRaWANSettingsDto extends PickType(ChirpstackDeviceContentsDto, [ "devEUI", "deviceProfileID", - "serviceProfileID", "skipFCntCheck", "isDisabled", "deviceStatusBattery", diff --git a/src/entities/dto/update-multicast.dto.ts b/src/entities/dto/update-multicast.dto.ts index 6728ac8d..2359b823 100644 --- a/src/entities/dto/update-multicast.dto.ts +++ b/src/entities/dto/update-multicast.dto.ts @@ -1,4 +1,8 @@ import { PartialType } from "@nestjs/mapped-types"; import { CreateMulticastDto } from "./create-multicast.dto"; +import { ApiProperty } from "@nestjs/swagger"; -export class UpdateMulticastDto extends PartialType(CreateMulticastDto) {} +export class UpdateMulticastDto extends PartialType(CreateMulticastDto) { + @ApiProperty({ required: false }) + id: string; +} diff --git a/src/entities/enum/error-codes.enum.ts b/src/entities/enum/error-codes.enum.ts index a64001ce..89ddd791 100644 --- a/src/entities/enum/error-codes.enum.ts +++ b/src/entities/enum/error-codes.enum.ts @@ -38,8 +38,6 @@ export enum ErrorCodes { DeleteNotAllowedHasLoRaWANDevices = "MESSAGE.DELETE-NOT-ALLOWED-HAS-LORAWAN-DEVICE", KOMBITLoginFailed = "MESSAGE.KOMBIT-LOGIN-FAILED", ApiKeyAuthFailed = "MESSAGE.API-KEY-AUTH-FAILED", - DifferentServiceprofile = "MESSAGE.DIFFERENT-SERVICE-PROFILE", - NewDevicesWrongServiceProfile = "MESSAGE.WRONG-SERVICE-PROFILE", TooMuchData = "MESSAGE.TOO-MUCH-DATA", ApplicationDoesNotExist = "MESSAGE.APPLICATION-DOES-NOT-EXIST", FailedToCreateOrUpdateIotDevice = "MESSAGE.FAILED-TO-CREATE-OR-UPDATE-IOT-DEVICE", @@ -50,4 +48,6 @@ export enum ErrorCodes { SendMailError = "MESSAGE.SEND-MAIL-ERROR", UserDoesNotExistInArray = "MESSAGE.USER-DOES-NOT-EXIST", UserAlreadyInPermission = "MESSAGE.USER-ALREADY-IN-PERMISSION", + CouldntGetApplications = "MESSAGE.COULD-NOT-GET-CS-APPLICATIONS", + DifferentServiceProfile = "MESSAGE.DIFFERENT-CREATION-SERVICE-PROFILE", } diff --git a/src/entities/interfaces/chirpstack-id-response.interface.ts b/src/entities/interfaces/chirpstack-id-response.interface.ts new file mode 100644 index 00000000..7f23bafb --- /dev/null +++ b/src/entities/interfaces/chirpstack-id-response.interface.ts @@ -0,0 +1,3 @@ +export interface IdResponse { + id: string; +} diff --git a/src/entities/interfaces/chirpstack-network-server-send-status.interface.ts b/src/entities/interfaces/chirpstack-network-server-send-status.interface.ts deleted file mode 100644 index 6b8170ce..00000000 --- a/src/entities/interfaces/chirpstack-network-server-send-status.interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ChirpstackNetworkServerSendStatus { - totalCount?: number; - result?: []; -} diff --git a/src/entities/lorawan-device.entity.ts b/src/entities/lorawan-device.entity.ts index 48c9f0fe..00d7e3b7 100644 --- a/src/entities/lorawan-device.entity.ts +++ b/src/entities/lorawan-device.entity.ts @@ -18,7 +18,7 @@ export class LoRaWANDevice extends IoTDevice { deviceProfileName: string; @Column({ nullable: true }) - chirpstackApplicationId: number; + chirpstackApplicationId: string; @BeforeInsert() private beforeInsert() { diff --git a/src/helpers/date.helper.ts b/src/helpers/date.helper.ts index 60224da5..1258e287 100644 --- a/src/helpers/date.helper.ts +++ b/src/helpers/date.helper.ts @@ -1,3 +1,5 @@ +import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb"; + export const subtractHours = (date: Date, hours = 1): Date => { const newDate = new Date(); newDate.setTime(date.getTime() - 1000 * (60 * 60 * hours)); @@ -14,4 +16,17 @@ export const subtractYears = (date: Date, years = 1): Date => { const newDate = new Date(); newDate.setDate(date.getDate() - years * 365); return newDate; -} +}; + +export const dateToTimestamp = (date: Date): Timestamp => { + const timestamp = new Timestamp(); + timestamp.fromDate(date); + return timestamp; +}; + +export const timestampToDate = (timestamp: Timestamp.AsObject): Date => { + const seconds = timestamp.seconds; + const nanoseconds = timestamp.nanos / 1e6; // Convert nanoseconds to milliseconds + const milliseconds = seconds * 1000 + nanoseconds; + return new Date(milliseconds); +}; diff --git a/src/helpers/message-payload.helper.ts b/src/helpers/message-payload.helper.ts index d443dede..4c6465a0 100644 --- a/src/helpers/message-payload.helper.ts +++ b/src/helpers/message-payload.helper.ts @@ -8,7 +8,7 @@ export interface LoRaWANSignalData { name: string; rssi: number; time: string; - loRaSNR: number; + snr: number; location: { altitude: number; latitude: number; @@ -47,8 +47,8 @@ export const isValidLoRaWANSignalData = (rxInfo: unknown): rxInfo is LoRaWANSign typeof unknownInfo[0] === "object" && hasProps(unknownInfo[0], nameof("rssi")) && typeof unknownInfo[0].rssi === "number" && - hasProps(unknownInfo[0], nameof("loRaSNR")) && - typeof unknownInfo[0].loRaSNR === "number" + hasProps(unknownInfo[0], nameof("snr")) && + typeof unknownInfo[0].snr === "number" ); } diff --git a/src/migration/1701075072037-changed-chirpstackapplicationid-to-string.ts b/src/migration/1701075072037-changed-chirpstackapplicationid-to-string.ts new file mode 100644 index 00000000..6d31c5f7 --- /dev/null +++ b/src/migration/1701075072037-changed-chirpstackapplicationid-to-string.ts @@ -0,0 +1,52 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class changedChirpstackapplicationidToString1701075072037 implements MigrationInterface { + name = 'changedChirpstackapplicationidToString1701075072037' + + GuidZeros = "00000000-0000-0000-0000-" + public async up(queryRunner: QueryRunner): Promise { + //SELECT LORAWAN IDS + const iotDevices = await queryRunner.query(`SELECT * FROM "iot_device" WHERE TYPE = 'LORAWAN'`) + + //CONVERT TO GUID + iotDevices.forEach((device: any) => { + device.chirpstackApplicationId = this.GuidZeros + this.appendZeros(+device.chirpstackApplicationId) + }); + + await queryRunner.query(`ALTER TABLE "iot_device" DROP COLUMN "chirpstackApplicationId"`); + await queryRunner.query(`ALTER TABLE "iot_device" ADD "chirpstackApplicationId" character varying`); + + //UPDATE IN NEW TABLE + iotDevices.forEach(async (device: any) => { + await queryRunner.query(`UPDATE "iot_device" SET "chirpstackApplicationId" = $1 WHERE "id" = $2`, [device.chirpstackApplicationId, device.id]); + }); + + } + + public async down(queryRunner: QueryRunner): Promise { + //SELECT LORAWAN IDS + const iotDevices = await queryRunner.query(`SELECT * FROM "iot_device" WHERE TYPE = 'LORAWAN'`) + + await queryRunner.query(`ALTER TABLE "iot_device" DROP COLUMN "chirpstackApplicationId"`); + await queryRunner.query(`ALTER TABLE "iot_device" ADD "chirpstackApplicationId" integer`); + + + //CONVERT TO INT + iotDevices.forEach((device: any) => { + device.chirpstackApplicationId = parseInt(device.chirpstackApplicationId.slice(-12)) + }); + + //UPDATE IN NEW TABLE + iotDevices.forEach(async (device: any) => { + await queryRunner.query(`UPDATE "iot_device" SET "chirpstackApplicationId" = $1 WHERE "id" = $2`, [device.chirpstackApplicationId, device.id]); + }); + } + private appendZeros(id: number) { + let numString = id.toString(); + while (numString.length < 12) { + numString = '0' + numString; + } + return numString; + } + +} diff --git a/src/migration/1701260283869-added-chirpstackId-to-application.ts b/src/migration/1701260283869-added-chirpstackId-to-application.ts new file mode 100644 index 00000000..76a91b2a --- /dev/null +++ b/src/migration/1701260283869-added-chirpstackId-to-application.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class addedChirpstackIdToApplication1701260283869 implements MigrationInterface { + name = 'addedChirpstackIdToApplication1701260283869' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "application" ADD "chirpstackId" character varying`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "application" DROP COLUMN "chirpstackId"`); + } + +} diff --git a/src/modules/device-integrations/chirpstack-administration.module.ts b/src/modules/device-integrations/chirpstack-administration.module.ts index 15dee41e..e006a566 100644 --- a/src/modules/device-integrations/chirpstack-administration.module.ts +++ b/src/modules/device-integrations/chirpstack-administration.module.ts @@ -1,36 +1,27 @@ import { ChirpstackGatewayController } from "@admin-controller/chirpstack/chirpstack-gateway.controller"; import { DeviceProfileController } from "@admin-controller/chirpstack/device-profile.controller"; -import { ServiceProfileController } from "@admin-controller/chirpstack/service-profile.controller"; import configuration from "@config/configuration"; +import { SharedModule } from "@modules/shared.module"; import { HttpModule } from "@nestjs/axios"; import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; +import { ApplicationChirpstackService } from "@services/chirpstack/chirpstack-application.service"; import { ChirpstackDeviceService } from "@services/chirpstack/chirpstack-device.service"; import { ChirpstackGatewayService } from "@services/chirpstack/chirpstack-gateway.service"; import { DeviceProfileService } from "@services/chirpstack/device-profile.service"; import { GenericChirpstackConfigurationService } from "@services/chirpstack/generic-chirpstack-configuration.service"; -import { ChirpstackSetupNetworkServerService } from "@services/chirpstack/network-server.service"; -import { ServiceProfileService } from "@services/chirpstack/service-profile.service"; -import { NetworkServerController } from "@admin-controller/chirpstack/network-server.controller"; -import { SharedModule } from "@modules/shared.module"; import { OrganizationModule } from "@modules/user-management/organization.module"; @Module({ - controllers: [ - ChirpstackGatewayController, - ServiceProfileController, - DeviceProfileController, - NetworkServerController, - ], + controllers: [ChirpstackGatewayController, DeviceProfileController], imports: [SharedModule, HttpModule, OrganizationModule, ConfigModule.forRoot({ load: [configuration] })], providers: [ GenericChirpstackConfigurationService, - ChirpstackSetupNetworkServerService, ChirpstackGatewayService, - ServiceProfileService, DeviceProfileService, ChirpstackDeviceService, + ApplicationChirpstackService, ], - exports: [ChirpstackDeviceService, ChirpstackGatewayService, DeviceProfileService], + exports: [ChirpstackDeviceService, ChirpstackGatewayService, DeviceProfileService, ApplicationChirpstackService], }) export class ChirpstackAdministrationModule {} diff --git a/src/modules/device-integrations/lorawan-gateway.module.ts b/src/modules/device-integrations/lorawan-gateway.module.ts index f97fa332..fc919c86 100644 --- a/src/modules/device-integrations/lorawan-gateway.module.ts +++ b/src/modules/device-integrations/lorawan-gateway.module.ts @@ -3,7 +3,6 @@ import { SharedModule } from "@modules/shared.module"; import { Module } from "@nestjs/common"; import { ChirpstackGatewayService } from "@services/chirpstack/chirpstack-gateway.service"; import { GatewayStatusHistoryService } from "@services/chirpstack/gateway-status-history.service"; -import { ChirpstackSetupNetworkServerService } from "@services/chirpstack/network-server.service"; import { GatewayBootstrapperService } from "@services/chirpstack/gateway-boostrapper.service"; import { HttpModule } from "@nestjs/axios"; import { OrganizationModule } from "@modules/user-management/organization.module"; @@ -11,12 +10,7 @@ import { OrganizationModule } from "@modules/user-management/organization.module @Module({ controllers: [LoRaWANGatewayController], imports: [SharedModule, HttpModule, OrganizationModule], - providers: [ - ChirpstackGatewayService, - ChirpstackSetupNetworkServerService, - GatewayStatusHistoryService, - GatewayBootstrapperService, - ], + providers: [ChirpstackGatewayService, GatewayStatusHistoryService, GatewayBootstrapperService], exports: [GatewayStatusHistoryService], }) export class LoRaWANGatewayModule {} diff --git a/src/modules/device-management/multicast.module.ts b/src/modules/device-management/multicast.module.ts index ccbe3bb7..db497135 100644 --- a/src/modules/device-management/multicast.module.ts +++ b/src/modules/device-management/multicast.module.ts @@ -3,7 +3,7 @@ import { SharedModule } from "@modules/shared.module"; import { HttpModule } from "@nestjs/axios"; import { forwardRef, Module } from "@nestjs/common"; import { MulticastController } from "../../controllers/admin-controller/multicast.controller"; -import { MulticastService } from "../../services/device-management/multicast.service"; +import { MulticastService } from "../../services/chirpstack/multicast.service"; import { ApplicationModule } from "./application.module"; import { IoTDeviceModule } from "./iot-device.module"; diff --git a/src/services/chirpstack/chirpstack-application.service.ts b/src/services/chirpstack/chirpstack-application.service.ts new file mode 100644 index 00000000..7cba49b8 --- /dev/null +++ b/src/services/chirpstack/chirpstack-application.service.ts @@ -0,0 +1,112 @@ +import { Injectable } from "@nestjs/common"; +import { GenericChirpstackConfigurationService } from "./generic-chirpstack-configuration.service"; +import { + Application as ChirpstackApplication, + CreateApplicationRequest, + DeleteApplicationRequest, + ListApplicationsRequest, + UpdateApplicationRequest, +} from "@chirpstack/chirpstack-api/api/application_pb"; +import { IdResponse } from "@interfaces/chirpstack-id-response.interface"; +import { Application } from "@entities/application.entity"; +import { ListAllChirpstackApplicationsResponseDto } from "@dto/chirpstack/list-all-applications-response.dto"; +import { LoRaWANDevice } from "@entities/lorawan-device.entity"; +import { CreateChirpstackApplicationDto } from "@dto/chirpstack/create-chirpstack-application.dto"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; + +@Injectable() +export class ApplicationChirpstackService extends GenericChirpstackConfigurationService { + @InjectRepository(Application) + private applicationRepository: Repository; + constructor() { + super(); + } + applicationNamePrefix = "os2iot-"; + DEFAULT_DESCRIPTION = "Created by OS2IoT"; + + public async findOrCreateDefaultApplication( + applications: ListAllChirpstackApplicationsResponseDto = null, + iotDevice: LoRaWANDevice + ): Promise { + const organizationID = await this.getDefaultOrganizationId(); + const req = new ListApplicationsRequest(); + req.setTenantId(organizationID); + // Fetch applications + applications = + applications ?? + (await this.getAllWithPagination( + `applications?limit=100&organizationID=${organizationID}`, + this.applicationServiceClient, + req, + 100, + undefined + )); + + // if application exist use it + let applicationId = applications.resultList.find( + element => + element.id === iotDevice.chirpstackApplicationId || element.id === iotDevice.application.chirpstackId + )?.id; + + // otherwise create new application + if (!applicationId) { + applicationId = await this.createNewApplication(iotDevice.application.name, iotDevice.application.id); + } + + return applicationId; + } + + public async createNewApplication(name: string, id: number) { + const applicationId = await this.createChirpstackApplication({ + application: { + name: `${this.applicationNamePrefix}${name}`, + description: this.DEFAULT_DESCRIPTION, + }, + }); + const existingApplication = await this.applicationRepository.findOneOrFail({ + where: { id: id }, + }); + existingApplication.chirpstackId = applicationId; + await this.applicationRepository.save(existingApplication); + return applicationId; + } + + public async createChirpstackApplication(dto: CreateChirpstackApplicationDto): Promise { + const req = new CreateApplicationRequest(); + const application = new ChirpstackApplication(); + application.setDescription( + dto.application.description ? dto.application.description : this.DEFAULT_DESCRIPTION + ); + application.setName(this.applicationNamePrefix + dto.application.name); + application.setTenantId(await this.getDefaultOrganizationId()); + + req.setApplication(application); + const applicationIdObject: IdResponse = await this.post("applications", this.applicationServiceClient, req); + return applicationIdObject.id; + } + + public async deleteApplication(id: string): Promise { + const req = new DeleteApplicationRequest(); + req.setId(id); + try { + return await this.delete("applications", this.applicationServiceClient, req); + } catch (e) { + throw e; + } + } + public async updateApplication(dto: Application): Promise { + const req = new UpdateApplicationRequest(); + const application = new ChirpstackApplication(); + application.setId(dto.chirpstackId); + application.setDescription(dto.description ? dto.description : this.DEFAULT_DESCRIPTION); + application.setName(this.applicationNamePrefix + dto.name); + application.setTenantId(await this.getDefaultOrganizationId()); + req.setApplication(application); + try { + return await this.put("applications", this.applicationServiceClient, req); + } catch (e) { + throw e; + } + } +} diff --git a/src/services/chirpstack/chirpstack-device.service.ts b/src/services/chirpstack/chirpstack-device.service.ts index 03f6a435..89b8f940 100644 --- a/src/services/chirpstack/chirpstack-device.service.ts +++ b/src/services/chirpstack/chirpstack-device.service.ts @@ -1,109 +1,88 @@ -import { BadRequestException, Injectable, Logger } from "@nestjs/common"; -import { AxiosResponse } from "axios"; - -import { - ChirpstackDeviceActivationContentsDto, - ChirpstackDeviceActivationDto, -} from "@dto/chirpstack/chirpstack-device-activation-response.dto"; +import { BadRequestException, Injectable, InternalServerErrorException, Logger } from "@nestjs/common"; +import { ChirpstackDeviceActivationContentsDto } from "@dto/chirpstack/chirpstack-device-activation-response.dto"; import { ChirpstackDeviceContentsDto } from "@dto/chirpstack/chirpstack-device-contents.dto"; -import { - ChirpstackDeviceKeysContentDto, - ChirpstackDeviceKeysResponseDto, -} from "@dto/chirpstack/chirpstack-device-keys-response.dto"; -import { ChirpstackSingleApplicationResponseDto } from "@dto/chirpstack/chirpstack-single-application-response.dto"; -import { ChirpstackSingleDeviceResponseDto } from "@dto/chirpstack/chirpstack-single-device-response.dto"; -import { CreateChirpstackApplicationDto } from "@dto/chirpstack/create-chirpstack-application.dto"; +import { ChirpstackDeviceKeysContentDto } from "@dto/chirpstack/chirpstack-device-keys-response.dto"; import { CreateChirpstackDeviceDto } from "@dto/chirpstack/create-chirpstack-device.dto"; -import { ListAllChirpstackApplicationsResponseDto } from "@dto/chirpstack/list-all-applications-response.dto"; -import { ListAllDevicesResponseDto } from "@dto/chirpstack/list-all-devices-response.dto"; import { CreateLoRaWANSettingsDto } from "@dto/create-lorawan-settings.dto"; import { GenericChirpstackConfigurationService } from "@services/chirpstack/generic-chirpstack-configuration.service"; +import { CreateChirpstackDeviceQueueItemDto } from "@dto/chirpstack/create-chirpstack-device-queue-item.dto"; import { - CreateChirpstackDeviceQueueItemDto, - CreateChirpstackDeviceQueueItemResponse, -} from "@dto/chirpstack/create-chirpstack-device-queue-item.dto"; -import { DeviceDownlinkQueueResponseDto } from "@dto/chirpstack/chirpstack-device-downlink-queue-response.dto"; + DeviceDownlinkQueueResponseDto, + DeviceQueueItem, +} from "@dto/chirpstack/chirpstack-device-downlink-queue-response.dto"; import { ErrorCodes } from "@enum/error-codes.enum"; -import { ChirpstackManyDeviceResponseDto } from "@dto/chirpstack/chirpstack-many-device-response"; +import { + ChirpstackDeviceResponseContents, + ChirpstackManyDeviceResponseDto, +} from "@dto/chirpstack/chirpstack-many-device-response"; import { IoTDevice } from "@entities/iot-device.entity"; import { LoRaWANDeviceWithChirpstackDataDto } from "@dto/lorawan-device-with-chirpstack-data.dto"; import { ActivationType } from "@enum/lorawan-activation-type.enum"; import { ChirpstackDeviceId } from "@dto/chirpstack/chirpstack-device-id.dto"; -import { ChirpstackApplicationResponseDto } from "@dto/chirpstack/chirpstack-application-response.dto"; -import { groupBy } from "lodash"; -import { LoRaWANStatsResponseDto } from "@dto/chirpstack/device/lorawan-stats.response.dto"; +import { LoRaWANStatsElementDto, LoRaWANStatsResponseDto } from "@dto/chirpstack/device/lorawan-stats.response.dto"; import { ConfigService } from "@nestjs/config"; -import { HttpService } from "@nestjs/axios"; import { DeviceProfileService } from "@services/chirpstack/device-profile.service"; +import { ServiceError } from "@grpc/grpc-js"; +import { + ActivateDeviceRequest, + CreateDeviceKeysRequest, + CreateDeviceRequest, + DeleteDeviceRequest, + Device, + DeviceActivation, + DeviceKeys, + DeviceQueueItem as DeviceQueueItemChirpstack, + EnqueueDeviceQueueItemRequest, + FlushDeviceQueueRequest, + GetDeviceActivationRequest, + GetDeviceActivationResponse, + GetDeviceKeysRequest, + GetDeviceKeysResponse, + GetDeviceLinkMetricsRequest, + GetDeviceLinkMetricsResponse, + GetDeviceQueueItemsRequest, + GetDeviceQueueItemsResponse, + GetDeviceRequest, + GetDeviceResponse, + ListDevicesRequest, + ListDevicesResponse, + UpdateDeviceKeysRequest, + UpdateDeviceRequest, +} from "@chirpstack/chirpstack-api/api/device_pb"; +import { IdResponse } from "@interfaces/chirpstack-id-response.interface"; +import { dateToTimestamp, timestampToDate } from "@helpers/date.helper"; +import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb"; +import { Aggregation } from "@chirpstack/chirpstack-api/common/common_pb"; +import { DeviceMetricsDto, MetricProperties } from "@dto/chirpstack/chirpstack-device-metrics.dto"; +import { LoRaWANDevice } from "@entities/lorawan-device.entity"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { Application as DbApplication } from "@entities/application.entity"; +import { IoTDeviceType } from "@enum/device-type.enum"; @Injectable() export class ChirpstackDeviceService extends GenericChirpstackConfigurationService { - constructor( - internalHttpService: HttpService, - private configService: ConfigService, - private deviceProfileService: DeviceProfileService - ) { - super(internalHttpService); + @InjectRepository(DbApplication) + private applicationRepository: Repository; + + constructor(private configService: ConfigService, private deviceProfileService: DeviceProfileService) { + super(); this.deviceStatsIntervalInDays = configService.get("backend.deviceStatsIntervalInDays"); } private readonly logger = new Logger(ChirpstackDeviceService.name); - defaultApplicationName = "os2iot"; - DEVICE_NAME_PREFIX = "OS2IOT-"; DEFAULT_DESCRIPTION = "Created by OS2IoT"; private readonly deviceStatsIntervalInDays: number; - async findOrCreateDefaultApplication( - dto: CreateChirpstackDeviceDto, - applications: ListAllChirpstackApplicationsResponseDto = null - ): Promise { - const organizationID = await this.getDefaultOrganizationId(); - // Fetch applications - applications = - applications ?? - (await this.getAllWithPagination( - `applications?limit=100&organizationID=${organizationID}` - )); - // if default exist use it - let applicationId = applications.result.find( - element => - element.serviceProfileID.toLowerCase() === dto.device.serviceProfileID.toLowerCase() && - element.name.startsWith(this.defaultApplicationName) - )?.id; - // otherwise create default - if (!applicationId) { - applicationId = await this.createDefaultApplication(applicationId, dto, organizationID); - } - - return +applicationId; - } - - private async createDefaultApplication( - applicationId: string, - dto: CreateChirpstackDeviceDto, - organizationID: string - ) { - applicationId = await this.createApplication({ - application: { - name: `${this.defaultApplicationName}-${dto.device.serviceProfileID.toLowerCase()}`.substring(0, 50), - description: this.DEFAULT_DESCRIPTION, - organizationID: organizationID, - serviceProfileID: dto.device.serviceProfileID, - }, - }); - return applicationId; - } - - makeCreateChirpstackDeviceDto(dto: CreateLoRaWANSettingsDto, name: string): CreateChirpstackDeviceDto { + public makeCreateChirpstackDeviceDto(dto: CreateLoRaWANSettingsDto, name: string): CreateChirpstackDeviceDto { const csDto = new ChirpstackDeviceContentsDto(); csDto.name = `${this.DEVICE_NAME_PREFIX}${name}`.toLowerCase(); csDto.description = this.DEFAULT_DESCRIPTION; csDto.devEUI = dto.devEUI; csDto.deviceProfileID = dto.deviceProfileID; - csDto.serviceProfileID = dto.serviceProfileID; csDto.isDisabled = dto.isDisabled; csDto.skipFCntCheck = dto.skipFCntCheck; @@ -111,14 +90,19 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi return { device: csDto }; } - async overwriteDownlink(dto: CreateChirpstackDeviceQueueItemDto): Promise { + public async overwriteDownlink(dto: CreateChirpstackDeviceQueueItemDto): Promise { await this.deleteDownlinkQueue(dto.deviceQueueItem.devEUI); try { - const res = await this.post( - `devices/${dto.deviceQueueItem.devEUI}/queue`, - dto - ); - return res.data; + const req = new EnqueueDeviceQueueItemRequest(); + const queueItem = new DeviceQueueItemChirpstack(); + queueItem.setConfirmed(dto.deviceQueueItem.confirmed); + queueItem.setData(dto.deviceQueueItem.data); + queueItem.setDevEui(dto.deviceQueueItem.devEUI); + queueItem.setFPort(dto.deviceQueueItem.fPort); + req.setQueueItem(queueItem); + + const res = await this.postDownlink(req); + return res; } catch (err) { const fcntError = "enqueue downlink payload error: get next downlink fcnt for deveui error"; if (err?.response?.data?.error?.startsWith(fcntError)) { @@ -129,50 +113,110 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi } } - async deleteDevice(deviceEUI: string): Promise { + public async deleteDevice(deviceEUI: string): Promise { try { - return await this.delete(`devices/`, deviceEUI); + const req = new DeleteDeviceRequest(); + req.setDevEui(deviceEUI); + await this.delete(`devices`, this.deviceServiceClient, req); } catch (err) { throw err; } } - async getDownlinkQueue(deviceEUI: string): Promise { - const res = await this.get(`devices/${deviceEUI}/queue`); - return res; + public async getDownlinkQueue(deviceEUI: string): Promise { + const req = new GetDeviceQueueItemsRequest(); + req.setDevEui(deviceEUI); + const res = await this.getQueue(req); + + const queueItems: DeviceQueueItem[] = res.getResultList().map(queueItem => { + return { + confirmed: queueItem.getConfirmed(), + devEUI: queueItem.getDevEui(), + fCnt: queueItem.getFCntDown(), + fPort: queueItem.getFPort(), + data: queueItem.getData_asB64(), + }; + }); + + const responseDto: DeviceDownlinkQueueResponseDto = { + totalCount: res.getTotalCount(), + deviceQueueItems: queueItems, + }; + return responseDto; } - async deleteDownlinkQueue(deviceEUI: string): Promise { - await this.delete(`devices/${deviceEUI}/queue`); + private async deleteDownlinkQueue(deviceEUI: string): Promise { + const req = new FlushDeviceQueueRequest(); + req.setDevEui(deviceEUI); + await this.deleteQueue(req); } - async activateDeviceWithABP( + public async activateDeviceWithABP( devEUI: string, devAddr: string, fCntUp: number, nFCntDown: number, networkSessionKey: string, - applicationSessionKey: string, - isUpdate: boolean - ): Promise { - const { res, dto } = await this.createOrUpdateABPActivation( + applicationSessionKey: string + ): Promise { + const res = await this.createOrUpdateABPActivation( devAddr, networkSessionKey, applicationSessionKey, fCntUp, nFCntDown, - devEUI, - isUpdate + devEUI ); - if (res.status != 200) { - this.logger.warn(`Could not ABP activate Chirpstack Device using body: ${JSON.stringify(dto)}`); - return false; + if (!res) { + this.logger.warn(`Could not ABP activate Chirpstack Device using DEVEUI: ${devEUI}}`); } - return res.status == 200; } - async getAllDevicesStatus(): Promise { - return await this.get(`devices?limit=10000&offset=0`); + public async getAllDevicesStatus(application: DbApplication): Promise { + const req = new ListDevicesRequest(); + if (!application.chirpstackId) { + const loraDev = application.iotDevices.find(d => d.type === IoTDeviceType.LoRaWAN); + const cast = loraDev as LoRaWANDevice; + + const deviceRequest = new GetDeviceRequest(); + deviceRequest.setDevEui(cast.deviceEUI); + + const getChirpstackDevice = await this.get( + "device", + this.deviceServiceClient, + deviceRequest + ); + + application.chirpstackId = getChirpstackDevice.getDevice().getApplicationId(); + await this.applicationRepository.save(application, {}); + } + + req.setApplicationId(application.chirpstackId); + const devices = await this.getAllWithPagination( + `devices`, + this.deviceServiceClient, + req, + 10000, + 0 + ); + + const responseDevice: ChirpstackDeviceResponseContents[] = devices.resultList.map(e => { + return { + devEUI: e.devEui, + name: e.name, + description: e.description, + lastSeenAt: e.lastSeenAt ? timestampToDate(e.lastSeenAt) : undefined, + deviceStatusBattery: e.deviceStatus?.batteryLevel, + deviceStatusMargin: e.deviceStatus?.margin, + deviceStatusExternalPowerSource: e.deviceStatus?.externalPowerSource, + deviceProfileID: e.deviceProfileId, + deviceProfileName: e.deviceProfileName, + }; + }); + return { + totalCount: devices.totalCount.toString(), + result: responseDevice, + }; } private async createOrUpdateABPActivation( @@ -181,98 +225,176 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi applicationSessionKey: string, fCntUp: number, nFCntDown: number, - devEUI: string, - isUpdate: boolean + devEUI: string ) { - const dto = { - deviceActivation: { - devAddr: devAddr, - nwkSEncKey: networkSessionKey, - appSKey: applicationSessionKey, - fCntUp: fCntUp, - nFCntDown: nFCntDown, - devEUI: devEUI, - fNwkSIntKey: networkSessionKey, - sNwkSIntKey: networkSessionKey, - }, - }; - let res; - if (isUpdate) { - res = await this.put(`devices`, dto, `${devEUI}/activate`); - } else { - res = await this.post(`devices/${devEUI}/activate`, dto); + const req = new ActivateDeviceRequest(); + const deviceActivation = this.mapActivationToChirpstack( + devAddr, + networkSessionKey, + applicationSessionKey, + fCntUp, + nFCntDown, + devEUI + ); + req.setDeviceActivation(deviceActivation); + try { + await this.postActivation(req); + } catch (e) { + return false; } - return { res, dto }; - } - async activateDeviceWithOTAA(deviceEUI: string, nwkKey: string, isUpdate: boolean): Promise { - // http://localhost:8080/api/devices/0011223344557188/keys - // {"deviceKeys":{"nwkKey":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","devEUI":"0011223344557188"}} + return true; + } + private mapActivationToChirpstack( + devAddr: string, + networkSessionKey: string, + applicationSessionKey: string, + fCntUp: number, + nFCntDown: number, + devEUI: string + ) { + const deviceActivation = new DeviceActivation(); + deviceActivation.setDevAddr(devAddr); + deviceActivation.setNwkSEncKey(networkSessionKey); + deviceActivation.setAppSKey(applicationSessionKey); + deviceActivation.setFCntUp(fCntUp); + deviceActivation.setNFCntDown(nFCntDown); + deviceActivation.setDevEui(devEUI); + deviceActivation.setFNwkSIntKey(networkSessionKey); + deviceActivation.setSNwkSIntKey(networkSessionKey); + return deviceActivation; + } - const dto = { - deviceKeys: { - nwkKey: nwkKey, - devEUI: deviceEUI, - }, - }; - let res; - if (isUpdate) { - res = await this.put(`devices`, dto, `${deviceEUI}/keys`); - } else { - res = await this.post(`devices/${deviceEUI}/keys`, dto); - } - if (res.status != 200) { - this.logger.warn(`Could not activate Chirpstack Device using body: ${JSON.stringify(dto)}`); + public async activateDeviceWithOTAA(deviceEUI: string, nwkKey: string, isUpdate: boolean): Promise { + try { + if (isUpdate) { + const req = new UpdateDeviceKeysRequest(); + const deviceKeys = this.mapDeviceKeysToChirpstack(deviceEUI, nwkKey); + req.setDeviceKeys(deviceKeys); + await this.putKeys(req); + } else { + const req = new CreateDeviceKeysRequest(); + const deviceKeys = this.mapDeviceKeysToChirpstack(deviceEUI, nwkKey); + req.setDeviceKeys(deviceKeys); + await this.postKeys(req); + } + } catch (e) { return false; } - return res.status == 200; + + return true; + } + + private mapDeviceKeysToChirpstack(deviceEUI: string, nwkKey: string) { + const deviceKeys = new DeviceKeys(); + deviceKeys.setDevEui(deviceEUI); + deviceKeys.setNwkKey(nwkKey); + return deviceKeys; } - async createOrUpdateDevice( + public async createOrUpdateDevice( dto: CreateChirpstackDeviceDto, lorawanDevices: ChirpstackDeviceId[] = null ): Promise { - let res: AxiosResponse; - if (await this.isDeviceAlreadyCreated(dto.device.devEUI, lorawanDevices)) { - res = await this.put(`devices`, dto, dto.device.devEUI); - } else { - res = await this.post(`devices`, dto); - } - - if (res.status !== 200) { - this.logger.warn(`Could not create Chirpstack Device using body: ${JSON.stringify(dto)}`); - + try { + if (await this.isDeviceAlreadyCreated(dto.device.devEUI, lorawanDevices)) { + const req = new UpdateDeviceRequest(); + const device = this.mapDeviceToChirpstack(dto); + req.setDevice(device); + await this.put(`devices`, this.deviceServiceClient, req); + } else { + const req = new CreateDeviceRequest(); + const device = this.mapDeviceToChirpstack(dto); + req.setDevice(device); + await this.post(`devices`, this.deviceServiceClient, req); + } + } catch (e) { + this.logger.error(`Update or Post device got error: ${e}`); return false; } - return true; } - async getChirpstackApplication(id: string): Promise { - return await this.get(`applications/${id}`); + private mapDeviceToChirpstack(dto: CreateChirpstackDeviceDto): Device { + const device = new Device(); + device.setApplicationId(dto.device.applicationID); + device.setDescription(dto.device.description); + device.setDevEui(dto.device.devEUI); + device.setDeviceProfileId(dto.device.deviceProfileID); + device.setIsDisabled(dto.device.isDisabled); + device.setName(dto.device.name); + device.setSkipFcntCheck(dto.device.skipFCntCheck); + return device; } - async getChirpstackDevice(id: string): Promise { - const res = await this.get(`devices/${id}`); - res.device.deviceStatusBattery = res.deviceStatusBattery; - res.device.deviceStatusMargin = res.deviceStatusMargin; - return res.device; + public async getChirpstackDevice(id: string): Promise { + try { + const req = new GetDeviceRequest(); + req.setDevEui(id); + + const res = await this.get(`devices/${id}`, this.deviceServiceClient, req); + const device = res.getDevice(); + + const deviceDto: ChirpstackDeviceContentsDto = { + deviceStatusBattery: res.getDeviceStatus()?.getBatteryLevel(), + deviceStatusMargin: res.getDeviceStatus()?.getMargin(), + devEUI: device.getDevEui(), + deviceProfileID: device.getDeviceProfileId(), + applicationID: device.getApplicationId(), + description: device.getDescription(), + isDisabled: device.getIsDisabled(), + name: device.getName(), + skipFCntCheck: device.getSkipFcntCheck(), + tags: device.getTagsMap().toObject(), + variables: device.getVariablesMap().toObject(), + }; + + return deviceDto; + } catch (err) { + throw new BadRequestException(ErrorCodes.CouldntGetApplications); + } } - async getKeys(deviceId: string): Promise { + private async getDeviceKeys(deviceId: string): Promise { try { - const res = await this.get(`devices/${deviceId}/keys`); - return res.deviceKeys; + const req = new GetDeviceKeysRequest(); + req.setDevEui(deviceId); + + const res = (await this.getKeys(req)).getDeviceKeys(); + + const keysDto: ChirpstackDeviceKeysContentDto = { + appKey: res.getAppKey(), + devEUI: res.getDevEui(), + nwkKey: res.getNwkKey(), + }; + + return keysDto; } catch (err) { // Chirpstack returns 404 if keys are not saved .. + // It seems like that the current logic is using this catch to see if the device is an ABP or OTAA device. return new ChirpstackDeviceKeysContentDto(); } } - async getActivation(deviceId: string): Promise { + private async getDeviceActivation(deviceId: string): Promise { try { - const res = await this.get(`devices/${deviceId}/activation`); - return res.deviceActivation; + const req = new GetDeviceActivationRequest(); + req.setDevEui(deviceId); + + const res = (await this.getActivation(req)).getDeviceActivation(); + + const activationDto: ChirpstackDeviceActivationContentsDto = { + aFCntDown: res.getAFCntDown(), + devEUI: res.getDevEui(), + appSKey: res.getAppSKey(), + devAddr: res.getDevAddr(), + fCntUp: res.getFCntUp(), + fNwkSIntKey: res.getFNwkSIntKey(), + nFCntDown: res.getAFCntDown(), + nwkSEncKey: res.getNwkSEncKey(), + sNwkSIntKey: res.getSNwkSIntKey(), + }; + return activationDto; } catch (err) { return new ChirpstackDeviceActivationContentsDto(); } @@ -284,17 +406,13 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi * @param applications * @returns The mutated device */ - async enrichLoRaWANDevice( - iotDevice: IoTDevice, - applications: ChirpstackApplicationResponseDto[] = [] - ): Promise { + public async enrichLoRaWANDevice(iotDevice: IoTDevice): Promise { const loraDevice = iotDevice as LoRaWANDeviceWithChirpstackDataDto; loraDevice.lorawanSettings = new CreateLoRaWANSettingsDto(); await this.mapActivationAndKeys(loraDevice); const csData = await this.getChirpstackDevice(loraDevice.deviceEUI); loraDevice.lorawanSettings.devEUI = csData.devEUI; loraDevice.lorawanSettings.deviceProfileID = csData.deviceProfileID; - loraDevice.lorawanSettings.serviceProfileID = csData.serviceProfileID; loraDevice.lorawanSettings.skipFCntCheck = csData.skipFCntCheck; loraDevice.lorawanSettings.isDisabled = csData.isDisabled; loraDevice.lorawanSettings.deviceStatusBattery = csData.deviceStatusBattery; @@ -303,28 +421,18 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi const deviceProfile = await this.deviceProfileService.findOneDeviceProfileById(csData.deviceProfileID); loraDevice.deviceProfileName = deviceProfile.deviceProfile.name; - const appMatch = applications.find(app => app.id === csData.applicationID); - loraDevice.lorawanSettings.serviceProfileID = appMatch - ? appMatch.serviceProfileID - : loraDevice.lorawanSettings.serviceProfileID; - - if (!loraDevice.lorawanSettings.serviceProfileID) { - const csAppliation = await this.getChirpstackApplication(csData.applicationID); - loraDevice.lorawanSettings.serviceProfileID = csAppliation.application.serviceProfileID; - } - return loraDevice; } private async mapActivationAndKeys(loraDevice: LoRaWANDeviceWithChirpstackDataDto) { - const keys = await this.getKeys(loraDevice.deviceEUI); + const keys = await this.getDeviceKeys(loraDevice.deviceEUI); if (keys.nwkKey) { // OTAA loraDevice.lorawanSettings.activationType = ActivationType.OTAA; loraDevice.lorawanSettings.OTAAapplicationKey = keys.nwkKey; loraDevice.OTAAapplicationKey = keys.nwkKey; } else { - const activation = await this.getActivation(loraDevice.deviceEUI); + const activation = await this.getDeviceActivation(loraDevice.deviceEUI); if (activation.devAddr != null) { // ABP loraDevice.lorawanSettings.activationType = ActivationType.ABP; @@ -339,48 +447,202 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi } } - async isDeviceAlreadyCreated(deviceEUI: string, chirpstackIds: ChirpstackDeviceId[] = null): Promise { - const devices = !chirpstackIds ? await this.getAllChirpstackDevices() : chirpstackIds; - const alreadyExists = devices.some(x => x.devEUI.toLowerCase() === deviceEUI.toLowerCase()); + public async isDeviceAlreadyCreated( + deviceEUI: string, + chirpstackIds: ChirpstackDeviceId[] = null + ): Promise { + const alreadyExists = chirpstackIds.some(x => x.devEUI.toLowerCase() === deviceEUI.toLowerCase()); return alreadyExists; } - getStats(deviceEUI: string): Promise { + public async getStats(deviceEUI: string): Promise { const now = new Date(); - const to_time = now.toISOString(); - const from_time = new Date(new Date().setDate(now.getDate() - this.deviceStatsIntervalInDays)).toISOString(); + const to_time = dateToTimestamp(now); + const from_time = new Date(new Date().setDate(now.getDate() - this.deviceStatsIntervalInDays)); + const from_time_timestamp: Timestamp = dateToTimestamp(from_time); + + const req = new GetDeviceLinkMetricsRequest(); + req.setDevEui(deviceEUI); + req.setStart(from_time_timestamp); + req.setEnd(to_time); + req.setAggregation(Aggregation.DAY); + const metaData = this.makeMetadataHeader(); + + const getDeviceMetricsPromise = new Promise((resolve, reject) => { + this.deviceServiceClient.getLinkMetrics(req, metaData, (err, resp) => { + if (err) { + reject(err); + } else { + resolve(resp); + } + }); + }); + try { + const metrics = await getDeviceMetricsPromise; + return this.mapMetrics(metrics); + } catch (err) { + throw new BadRequestException(err); + } + } + private mapMetrics(metrics: GetDeviceLinkMetricsResponse): LoRaWANStatsResponseDto { + const statsElementDto: LoRaWANStatsElementDto[] = []; + const deviceMetrics: DeviceMetricsDto = {}; + + const rssiTimestamp = metrics.getGwRssi().getTimestampsList(); + const rssis = metrics + .getGwRssi() + .getDatasetsList() + .find(e => e.getLabel() === "rssi") + .getDataList(); + + this.processPackets(rssiTimestamp, rssis, MetricProperties.rssi, deviceMetrics); + + const snrTimestamp = metrics.getGwSnr().getTimestampsList(); + const snr = metrics + .getGwSnr() + .getDatasetsList() + .find(e => e.getLabel() === "snr") + .getDataList(); + + this.processPackets(snrTimestamp, snr, MetricProperties.snr, deviceMetrics); + + const drTimestamp = metrics.getRxPacketsPerDr().getTimestampsList(); + const drDatasets = metrics.getRxPacketsPerDr().getDatasetsList(); + + drDatasets.forEach(drDataset => { + const drLabel = drDataset.getLabel(); + const drData = drDataset.getDataList(); + this.processPackets(drTimestamp, drData, MetricProperties.dr, deviceMetrics, drLabel); + }); + + Object.keys(deviceMetrics).forEach(timestamp => { + const packetCount = deviceMetrics[timestamp]; + const dto: LoRaWANStatsElementDto = { + timestamp, + gwRssi: packetCount.rssi, + gwSnr: packetCount.snr, + rxPacketsPerDr: packetCount.rxPacketsPerDr, + }; + statsElementDto.push(dto); + }); + return { result: statsElementDto }; + } + private processPackets = ( + timestamps: Array, + packets: number[], + key: string, + packetCounts: DeviceMetricsDto, + drLabel?: string + ) => { + timestamps.forEach((timestamp, index) => { + const isoTimestamp = timestamp.toDate().toISOString(); + packetCounts[isoTimestamp] = packetCounts[isoTimestamp] || { + rssi: 0, + snr: 0, + rxPacketsPerDr: {}, + }; + + if (drLabel) { + packetCounts[isoTimestamp].rxPacketsPerDr[drLabel as any] = packets[index]; + } else { + (packetCounts[isoTimestamp] as any)[key] = packets[index]; + } + }); + }; + + private async getKeys(request: GetDeviceKeysRequest): Promise { + return await this.makeRequest( + request, + this.deviceServiceClient.getKeys, + "GET KEYS success", + "GET KEYS failed and got error: " + ); + } - return this.get( - `devices/${deviceEUI}/stats?interval=DAY&startTimestamp=${from_time}&endTimestamp=${to_time}` + private async postKeys(request: CreateDeviceKeysRequest): Promise { + await this.makeRequest( + request, + this.deviceServiceClient.createKeys, + "POST KEYS success", + "POST KEYS failed and got error: " ); } - /** - * Fetch LoRaWAN applications by the device application id. This **assumes** that - * the device chirpstack application id always reflects what's on Chirpstack. - * @param devices - * @returns - */ - public async getLoRaWANApplications( - devices: LoRaWANDeviceWithChirpstackDataDto[] - ): Promise { - const loraDevicesByAppId = groupBy(devices, device => device.chirpstackApplicationId); + private async putKeys(request: UpdateDeviceKeysRequest): Promise { + await this.makeRequest( + request, + this.deviceServiceClient.updateKeys, + "UPDATE KEYS success", + "UPDATE KEYS failed and got error: " + ); + } - const res: ChirpstackSingleApplicationResponseDto[] = []; + private async getQueue(request: GetDeviceQueueItemsRequest): Promise { + return await this.makeRequest( + request, + this.deviceServiceClient.getQueue, + "GET QUEUE success", + "GET QUEUE failed and got error: " + ); + } - // Avoid async .forEach and .map when querying the API. They execute whatever's inside in "parallel" which can result in timeouts. - for (const appId of Object.keys(loraDevicesByAppId)) { - res.push(await this.getChirpstackApplication(appId)); - } + private async deleteQueue(request: FlushDeviceQueueRequest): Promise { + await this.makeRequest( + request, + this.deviceServiceClient.flushQueue, + "DELETE QUEUE success", + "DELETE QUEUE failed and got error: " + ); + } - return res; + private async postDownlink(request: EnqueueDeviceQueueItemRequest): Promise { + return await this.makeRequest( + request, + this.deviceServiceClient.enqueue, + "POST DOWNLINK success", + "POST DOWNLINK failed and got error :" + ); } - private async getAllChirpstackDevices(limit = 1000): Promise { - return (await this.get(`devices?limit=${limit}`)).result; + private async getActivation(request: GetDeviceActivationRequest): Promise { + return await this.makeRequest( + request, + this.deviceServiceClient.getActivation, + "GET ACTIVATION success", + "GET ACTIVATION failed and got error: " + ); } - private async createApplication(dto: CreateChirpstackApplicationDto): Promise { - return (await this.post("applications", dto)).data.id; + private async postActivation(request?: ActivateDeviceRequest): Promise { + await this.makeRequest( + request, + this.deviceServiceClient.activate, + "post ACTIVATION success", + "GET activation failed and got error: " + ); + } + private async makeRequest( + request: any, + method: (request: any, metaData: any, callback: (err: ServiceError, resp: any) => void) => void, + successMessage: string, + errorMessage: string + ): Promise { + const metaData = this.makeMetadataHeader(); + const promise = new Promise((resolve, reject) => { + method.call(this.deviceServiceClient, request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.logger.debug(successMessage); + resolve(resp); + } + }); + }); + try { + return await promise; + } catch (err) { + this.logger.error(errorMessage, +err); + throw new InternalServerErrorException(); + } } } diff --git a/src/services/chirpstack/chirpstack-gateway.service.ts b/src/services/chirpstack/chirpstack-gateway.service.ts index 4c0d650c..042fd6b7 100644 --- a/src/services/chirpstack/chirpstack-gateway.service.ts +++ b/src/services/chirpstack/chirpstack-gateway.service.ts @@ -5,44 +5,54 @@ import { Logger, NotFoundException, } from "@nestjs/common"; -import { AxiosResponse } from "axios"; import { ChirpstackErrorResponseDto } from "@dto/chirpstack/chirpstack-error-response.dto"; import { ChirpstackResponseStatus } from "@dto/chirpstack/chirpstack-response.dto"; import { CreateGatewayDto } from "@dto/chirpstack/create-gateway.dto"; -import { GatewayStatsResponseDto } from "@dto/chirpstack/gateway-stats.response.dto"; -import { ListAllGatewaysResponseDto } from "@dto/chirpstack/list-all-gateways.dto"; +import { GatewayStatsElementDto } from "@dto/chirpstack/gateway-stats.response.dto"; +import { + ListAllChirpstackGatewaysResponseDto, + ListAllGatewaysResponseDto, +} from "@dto/chirpstack/list-all-gateways.dto"; import { SingleGatewayResponseDto } from "@dto/chirpstack/single-gateway-response.dto"; import { UpdateGatewayContentsDto, UpdateGatewayDto } from "@dto/chirpstack/update-gateway.dto"; import { ErrorCodes } from "@enum/error-codes.enum"; import { GenericChirpstackConfigurationService } from "@services/chirpstack/generic-chirpstack-configuration.service"; -import { ChirpstackSetupNetworkServerService } from "@services/chirpstack/network-server.service"; import { GatewayContentsDto } from "@dto/chirpstack/gateway-contents.dto"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; -import { HttpService } from "@nestjs/axios"; import { InjectRepository } from "@nestjs/typeorm"; -import { Gateway } from "@entities/gateway.entity"; +import { Gateway as DbGateway } from "@entities/gateway.entity"; import { Repository } from "typeorm"; import { OrganizationService } from "@services/user-management/organization.service"; -import { GatewayResponseDto } from "@dto/chirpstack/gateway-response.dto"; import { CommonLocationDto } from "@dto/chirpstack/common-location.dto"; - +import { + CreateGatewayRequest, + DeleteGatewayRequest, + Gateway as ChirpstackGateway, + GetGatewayMetricsRequest, + GetGatewayMetricsResponse, + GetGatewayResponse, + ListGatewaysRequest, + UpdateGatewayRequest, + ListGatewaysResponse, +} from "@chirpstack/chirpstack-api/api/gateway_pb"; +import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb"; +import { Aggregation, Location } from "@chirpstack/chirpstack-api/common/common_pb"; +import { dateToTimestamp, timestampToDate } from "@helpers/date.helper"; +import { ChirpstackGatewayResponseDto, GatewayResponseDto } from "@dto/chirpstack/gateway-response.dto"; @Injectable() export class ChirpstackGatewayService extends GenericChirpstackConfigurationService { constructor( - internalHttpService: HttpService, - private chirpstackSetupNetworkServerService: ChirpstackSetupNetworkServerService, - @InjectRepository(Gateway) - private gatewayRepository: Repository, + @InjectRepository(DbGateway) + private gatewayRepository: Repository, private organizationService: OrganizationService ) { - super(internalHttpService); + super(); } GATEWAY_STATS_INTERVAL_IN_DAYS = 29; - private readonly logger = new Logger(ChirpstackGatewayService.name, { timestamp: true }); - private readonly ORG_ID_KEY = "internalOrganizationId"; - private readonly UPDATED_BY_KEY = "os2iot-updated-by"; - private readonly CREATED_BY_KEY = "os2iot-created-by"; + private readonly logger = new Logger(ChirpstackGatewayService.name, { + timestamp: true, + }); async createNewGateway(dto: CreateGatewayDto, userId: number): Promise { dto.gateway = await this.updateDtoContents(dto.gateway); @@ -58,25 +68,64 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ gateway.organization = await this.organizationService.findById(dto.organizationId); - const result = await this.post("gateways", dto); - await this.gatewayRepository.save(gateway); - return this.handlePossibleError(result, dto); + const req = new CreateGatewayRequest(); + const chirpstackLocation = this.mapToChirpstackLocation(dto); + + const gatewayChirpstack = await this.mapToChirpstackGateway(dto, chirpstackLocation); + Object.entries(dto.gateway.tags).forEach(([key, value]) => { + gatewayChirpstack.getTagsMap().set(key, value); + }); + + req.setGateway(gatewayChirpstack); + try { + await this.post("gateways", this.gatewayClient, req); + await this.gatewayRepository.save(gateway); + return { success: true }; + } catch (e) { + this.logger.error(`Error from Chirpstack: '${JSON.stringify(dto)}', got response: ${JSON.stringify(e)}`); + throw new BadRequestException({ + success: false, + error: e, + }); + } + } + + async mapToChirpstackGateway(dto: CreateGatewayDto | UpdateGatewayDto, location: Location, gatewayId?: string) { + const gateway = new ChirpstackGateway(); + gateway.setGatewayId(gatewayId ? gatewayId : dto.gateway.gatewayId); + gateway.setDescription(dto.gateway.description); + gateway.setName(dto.gateway.name); + gateway.setLocation(location); + gateway.setStatsInterval(30); + gateway.setTenantId(dto.gateway.tenantId ? dto.gateway.tenantId : await this.getDefaultOrganizationId()); + + return gateway; + } + mapToChirpstackLocation(dto: CreateGatewayDto | UpdateGatewayDto) { + const location = new Location(); + location.setAccuracy(dto.gateway.location.accuracy); + location.setAltitude(dto.gateway.location.altitude); + location.setLatitude(dto.gateway.location.latitude); + location.setLongitude(dto.gateway.location.longitude); + location.setSource(dto.gateway.location.source); + + return location; } - addUserToTags(dto: CreateGatewayDto, userId: number): { [id: string]: string | number } { + addUserToTags(dto: CreateGatewayDto, userId: number): { [id: string]: string } { const tags = dto.gateway.tags; tags[this.CREATED_BY_KEY] = `${userId}`; tags[this.UPDATED_BY_KEY] = `${userId}`; return tags; } - updateUpdatedByTag(dto: UpdateGatewayDto, userId: number): { [id: string]: string | number } { + updateUpdatedByTag(dto: UpdateGatewayDto, userId: number): { [id: string]: string } { const tags = dto.gateway.tags; tags[this.UPDATED_BY_KEY] = `${userId}`; return tags; } - addOrganizationToTags(dto: CreateGatewayDto): { [id: string]: string | number } { + addOrganizationToTags(dto: CreateGatewayDto): { [id: string]: string } { const tags = dto.gateway.tags; tags[this.ORG_ID_KEY] = `${dto.organizationId}`; return tags; @@ -94,7 +143,7 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ const gateways = await query.getMany(); return { - result: gateways.map(this.mapGatewayToResponseDto), + resultList: gateways.map(this.mapGatewayToResponseDto), totalCount: gateways.length, }; } @@ -115,7 +164,7 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ const now = new Date(); const statsFrom = new Date(new Date().setDate(now.getDate() - this.GATEWAY_STATS_INTERVAL_IN_DAYS)); - result.stats = (await this.getGatewayStats(gatewayId, statsFrom, now)).result; + result.stats = await this.getGatewayStats(gatewayId, statsFrom, now); result.gateway = this.mapGatewayToResponseDto(gateway); return result; @@ -128,13 +177,81 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ } } - async getGatewayStats(gatewayId: string, from: Date, to: Date): Promise { - const to_time = to.toISOString(); - const from_time = from.toISOString(); + async getGatewayStats(gatewayId: string, from: Date, to: Date): Promise { + const to_time = dateToTimestamp(to); + const from_time_timestamp: Timestamp = dateToTimestamp(from); + + const request = new GetGatewayMetricsRequest(); + request.setGatewayId(gatewayId); + request.setStart(from_time_timestamp); + request.setEnd(to_time); + request.setAggregation(Aggregation.DAY); + + const metaData = this.makeMetadataHeader(); + + const getGatewayMetricsPromise = new Promise((resolve, reject) => { + this.gatewayClient.getMetrics(request, metaData, (err, resp) => { + if (err) { + reject(err); + } else { + resolve(resp); + } + }); + }); + try { + const metrics = await getGatewayMetricsPromise; + return this.mapPackets(metrics); + } catch (err) { + throw new BadRequestException(err); + } + } - return await this.get( - `gateways/${gatewayId}/stats?interval=DAY&startTimestamp=${from_time}&endTimestamp=${to_time}` - ); + //TODO: This could be moved to a helper function in the future, since it has a lot of similarities with metrics from chirpstack devices. + private mapPackets(metrics: GetGatewayMetricsResponse) { + const gatewayResponseDto: GatewayStatsElementDto[] = []; + const packetCounts: { [timestamp: string]: { rx: number; tx: number } } = {}; + + const rxTimestamps = metrics.getRxPackets().getTimestampsList(); + const rxPackets = metrics + .getRxPackets() + .getDatasetsList() + .find(e => e.getLabel() === "rx_count") + .getDataList(); + + this.processPackets(rxTimestamps, rxPackets, "rx", packetCounts); + + const txTimestamps = metrics.getTxPackets().getTimestampsList(); + const txPackets = metrics + .getTxPackets() + .getDatasetsList() + .find(e => e.getLabel() === "tx_count") + .getDataList(); + + this.processPackets(txTimestamps, txPackets, "tx", packetCounts); + + Object.keys(packetCounts).forEach(timestamp => { + const packetCount = packetCounts[timestamp]; + const dto: GatewayStatsElementDto = { + timestamp, + rxPacketsReceived: packetCount.rx, + txPacketsEmitted: packetCount.tx, + }; + gatewayResponseDto.push(dto); + }); + return gatewayResponseDto; + } + + private processPackets( + timestamps: Array, + packets: number[], + key: string, + packetCounts: { [timestamp: string]: { rx: number; tx: number } } + ) { + timestamps.forEach((timestamp, index) => { + const isoTimestamp = timestamp.toDate().toISOString(); + packetCounts[isoTimestamp] = packetCounts[isoTimestamp] || { rx: 0, tx: 0 }; + (packetCounts[isoTimestamp] as any)[key] = packets[index]; + }); } async modifyGateway( @@ -150,9 +267,27 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ gateway.gatewayId = gatewayId; gateway.updatedBy = req.user.userId; - const result = await this.put("gateways", dto, gatewayId); - await this.gatewayRepository.update({ gatewayId }, gateway); - return this.handlePossibleError(result, dto); + const request = new UpdateGatewayRequest(); + const location = this.mapToChirpstackLocation(dto); + + const gatewayCs = await this.mapToChirpstackGateway(dto, location, gatewayId); + + Object.entries(dto.gateway.tags).forEach(([key, value]) => { + gatewayCs.getTagsMap().set(key, value); + }); + + request.setGateway(gatewayCs); + try { + await this.put("gateways", this.gatewayClient, request); + await this.gatewayRepository.update({ gatewayId }, gateway); + return { success: true }; + } catch (e) { + this.logger.error(`Error from Chirpstack: '${JSON.stringify(dto)}', got response: ${JSON.stringify(e)}`); + throw new BadRequestException({ + success: false, + error: e, + }); + } } public async updateGatewayStats( @@ -168,7 +303,7 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ gatewayId: string, dto: UpdateGatewayDto, req: AuthenticatedRequest - ): Promise<{ [id: string]: string | number }> { + ): Promise<{ [id: string]: string }> { const existing = await this.getOne(gatewayId); const tags = dto.gateway.tags; tags[this.ORG_ID_KEY] = `${existing.gateway.organizationId}`; @@ -180,9 +315,11 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ } async deleteGateway(gatewayId: string): Promise { + const req = new DeleteGatewayRequest(); + req.setGatewayId(gatewayId); try { + await this.delete("gateways", this.gatewayClient, req); await this.gatewayRepository.delete({ gatewayId }); - await this.delete("gateways", gatewayId); return { success: true, }; @@ -195,42 +332,11 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ } } - private handlePossibleError( - result: AxiosResponse, - dto: CreateGatewayDto | UpdateGatewayDto - ): ChirpstackResponseStatus { - if (result.status != 200) { - this.logger.error( - `Error from Chirpstack: '${JSON.stringify(dto)}', got response: ${JSON.stringify(result.data)}` - ); - throw new BadRequestException({ - success: false, - error: result.data, - }); - } - - return { success: true }; - } - private async updateDtoContents( contentsDto: GatewayContentsDto | UpdateGatewayContentsDto ): Promise { - // Chirpstack requires 'gatewayProfileID' to be set (with value or null) - if (!contentsDto?.gatewayProfileID) { - contentsDto.gatewayProfileID = null; - } - - // Add network server - if (!contentsDto?.networkServerID) { - contentsDto.networkServerID = await this.chirpstackSetupNetworkServerService.getDefaultNetworkServerId(); - } - - if (!contentsDto?.organizationID) { - contentsDto.organizationID = await this.chirpstackSetupNetworkServerService.getDefaultOrganizationId(); - } - if (contentsDto?.tagsString) { - contentsDto.tags = JSON.parse(contentsDto.tagsString); // TODO: Updaze for new format when chirpstack 4 + contentsDto.tags = JSON.parse(contentsDto.tagsString); } contentsDto.id = contentsDto.gatewayId; @@ -239,7 +345,7 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ } public mapContentsDtoToGateway(dto: GatewayContentsDto) { - const gateway = new Gateway(); + const gateway = new DbGateway(); gateway.name = dto.name; gateway.gatewayId = dto.gatewayId; gateway.description = dto.description; @@ -248,27 +354,104 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ type: "Point", coordinates: [dto.location.longitude, dto.location.latitude], }; - gateway.tags = JSON.stringify(dto.tags); + const tempTags = { ...dto.tags }; + tempTags[this.ORG_ID_KEY] = undefined; + tempTags[this.CREATED_BY_KEY] = undefined; + tempTags[this.UPDATED_BY_KEY] = undefined; + gateway.tags = JSON.stringify(tempTags); return gateway; } - private mapGatewayToResponseDto(gateway: Gateway): GatewayResponseDto { + public mapChirpstackGatewayToDatabaseGateway(chirpstackGateway: ChirpstackGateway, gwResponse: GetGatewayResponse) { + const gateway = new DbGateway(); + gateway.name = chirpstackGateway.getName(); + gateway.gatewayId = chirpstackGateway.getGatewayId(); + gateway.description = chirpstackGateway.getDescription(); + gateway.altitude = chirpstackGateway.getLocation().getAltitude(); + gateway.location = { + type: "Point", + coordinates: [ + chirpstackGateway.getLocation().getLongitude(), + chirpstackGateway.getLocation().getLatitude(), + ], + }; + const jsonRepresentation: Record = chirpstackGateway + .getTagsMap() + .toArray() + .reduce((obj: Record, [key, value]) => { + obj[key] = value; + return obj; + }, {}); + jsonRepresentation["internalOrganizationId"] = undefined; + jsonRepresentation["os2iot-updated-by"] = undefined; + jsonRepresentation["os2iot-created-by"] = undefined; + gateway.tags = JSON.stringify(jsonRepresentation); + gateway.lastSeenAt = gwResponse.getLastSeenAt() + ? timestampToDate(gwResponse.getLastSeenAt().toObject()) + : undefined; + gateway.createdAt = gwResponse.getCreatedAt() + ? timestampToDate(gwResponse.getCreatedAt().toObject()) + : undefined; + gateway.updatedAt = gwResponse.getUpdatedAt() + ? timestampToDate(gwResponse.getUpdatedAt().toObject()) + : undefined; + gateway.rxPacketsReceived = 0; + gateway.txPacketsEmitted = 0; + gateway.createdBy = + chirpstackGateway.getTagsMap().get("os2iot-created-by") !== undefined + ? Number(chirpstackGateway.getTagsMap().get("os2iot-created-by")) + : undefined; + gateway.updatedBy = + chirpstackGateway.getTagsMap().get("os2iot-updated-by") !== undefined + ? Number(chirpstackGateway.getTagsMap().get("os2iot-updated-by")) + : undefined; + + return gateway; + } + private mapGatewayToResponseDto(gateway: DbGateway): GatewayResponseDto { const responseDto = gateway as unknown as GatewayResponseDto; responseDto.organizationId = gateway.organization.id; responseDto.organizationName = gateway.organization.name; - responseDto.tags = JSON.parse(gateway.tags); - responseDto.tags["internalOrganizationId"] = undefined; - responseDto.tags["os2iot-updated-by"] = undefined; - responseDto.tags["os2iot-created-by"] = undefined; const commonLocation = new CommonLocationDto(); commonLocation.latitude = gateway.location.coordinates[1]; commonLocation.longitude = gateway.location.coordinates[0]; commonLocation.altitude = gateway.altitude; - + responseDto.tags = JSON.parse(gateway.tags); responseDto.location = commonLocation; return responseDto; } + async getAllGatewaysFromChirpstack(): Promise { + const limit = 1000; + const listReq = new ListGatewaysRequest(); + // Get all chirpstack gateways + const chirpStackGateways = await this.getAllWithPagination( + "gateways", + this.gatewayClient, + listReq, + limit, + 0 + ); + + const responseItem: ChirpstackGatewayResponseDto[] = []; + chirpStackGateways.resultList.map(e => { + const resultItem: ChirpstackGatewayResponseDto = { + gatewayId: e.gatewayId, + name: e.name, + location: e.location, + description: e.description, + createdAt: e.createdAt ?? undefined, + updatedAt: e.updatedAt ?? undefined, + lastSeenAt: e.lastSeenAt ?? undefined, + }; + responseItem.push(resultItem); + }); + const responseList: ListAllChirpstackGatewaysResponseDto = { + totalCount: chirpStackGateways.totalCount, + resultList: responseItem, + }; + return responseList; + } } diff --git a/src/services/chirpstack/device-profile.service.ts b/src/services/chirpstack/device-profile.service.ts index fd2565a5..566efed3 100644 --- a/src/services/chirpstack/device-profile.service.ts +++ b/src/services/chirpstack/device-profile.service.ts @@ -1,48 +1,73 @@ -import { BadRequestException, Injectable } from "@nestjs/common"; -import { AxiosResponse } from "axios"; - +import { + BadRequestException, + ConflictException, + Injectable, + InternalServerErrorException, + NotFoundException, +} from "@nestjs/common"; import { CreateDeviceProfileDto } from "@dto/chirpstack/create-device-profile.dto"; -import { ListAllDeviceProfilesResponseDto } from "@dto/chirpstack/list-all-device-profiles-response.dto"; - +import { + DeviceProfileListDto, + ListAllDeviceProfilesResponseDto, +} from "@dto/chirpstack/list-all-device-profiles-response.dto"; import { GenericChirpstackConfigurationService } from "./generic-chirpstack-configuration.service"; import { UpdateDeviceProfileDto } from "@dto/chirpstack/update-device-profile.dto"; import { DeviceProfileDto } from "@dto/chirpstack/device-profile.dto"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { ErrorCodes } from "@enum/error-codes.enum"; import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; +import { ServiceError } from "@grpc/grpc-js"; +import { + CreateDeviceProfileRequest, + DeleteDeviceProfileRequest, + DeviceProfile, + GetDeviceProfileRequest, + GetDeviceProfileResponse, + ListDeviceProfileAdrAlgorithmsResponse, + ListDeviceProfilesRequest, + ListDeviceProfilesResponse, + UpdateDeviceProfileRequest, +} from "@chirpstack/chirpstack-api/api/device_profile_pb"; +import { timestampToDate } from "@helpers/date.helper"; +import { ListAllAdrAlgorithmsResponseDto } from "@dto/chirpstack/list-all-adr-algorithms-response.dto"; +import { Empty } from "google-protobuf/google/protobuf/empty_pb"; +import { AdrAlgorithmDto } from "@dto/chirpstack/adr-algorithm.dto"; +import { IdResponse } from "@interfaces/chirpstack-id-response.interface"; +import { DeviceListItem, ListDevicesRequest, ListDevicesResponse } from "@chirpstack/chirpstack-api/api/device_pb"; +import { ListApplicationsRequest, ListApplicationsResponse } from "@chirpstack/chirpstack-api/api/application_pb"; +import * as google_protobuf_empty_pb from "google-protobuf/google/protobuf/empty_pb"; +import * as BluebirdPromise from "bluebird"; @Injectable() export class DeviceProfileService extends GenericChirpstackConfigurationService { - private readonly ORG_ID_KEY = "internalOrganizationId"; - private readonly UPDATED_BY_KEY = "os2iot-updated-by"; - private readonly CREATED_BY_KEY = "os2iot-created-by"; - - public async createDeviceProfile( - dto: CreateDeviceProfileDto, - userId: number - ): Promise { + public async createDeviceProfile(dto: CreateDeviceProfileDto, userId: number): Promise { if (await this.isNameInUse(dto.deviceProfile.name)) { throw new BadRequestException(ErrorCodes.NameInvalidOrAlreadyInUse); } dto.deviceProfile = await this.updateDto(dto.deviceProfile); dto.deviceProfile.tags = this.addOrganizationToTags(dto); dto.deviceProfile.tags = this.addUserIdToTags(dto, userId); - const result = await this.post("device-profiles", dto); + + const req = new CreateDeviceProfileRequest(); + + const deviceProfile = this.mapToChirpstackDto(dto, true); + + Object.entries(dto.deviceProfile.tags).forEach(([key, value]) => { + deviceProfile.getTagsMap().set(key, value); + }); + + req.setDeviceProfile(deviceProfile); + const result: IdResponse = await this.post("device-profiles", this.deviceProfileClient, req); return result; } - private addOrganizationToTags( - dto: CreateDeviceProfileDto - ): { [id: string]: string | number } { + private addOrganizationToTags(dto: CreateDeviceProfileDto): { [id: string]: string } { const tags = dto.deviceProfile?.tags != null ? dto.deviceProfile.tags : {}; tags[this.ORG_ID_KEY] = `${dto.internalOrganizationId}`; return tags; } - private addUserIdToTags( - dto: CreateDeviceProfileDto, - userId: number - ): { [id: string]: string | number } { + private addUserIdToTags(dto: CreateDeviceProfileDto, userId: number): { [id: string]: string } { const tags = dto.deviceProfile?.tags != null ? dto.deviceProfile.tags : {}; tags[this.CREATED_BY_KEY] = `${userId}`; tags[this.UPDATED_BY_KEY] = `${userId}`; @@ -53,13 +78,28 @@ export class DeviceProfileService extends GenericChirpstackConfigurationService data: UpdateDeviceProfileDto, id: string, req: AuthenticatedRequest - ): Promise { + ): Promise { if (await this.isNameInUse(data.deviceProfile.name, id)) { throw new BadRequestException(ErrorCodes.NameInvalidOrAlreadyInUse); } - data.deviceProfile.tags = await this.updateTags(id, req); + const deviceProfile = this.mapToChirpstackDto(data); + const request = new UpdateDeviceProfileRequest(); + data.deviceProfile = await this.updateDto(data.deviceProfile); - return await this.put("device-profiles", data, id); + + //Have to set these everytime, otherwise they will be erased. + deviceProfile.getTagsMap().set(this.ORG_ID_KEY, data.deviceProfile.internalOrganizationId.toString()); + deviceProfile.getTagsMap().set(this.UPDATED_BY_KEY, data.deviceProfile.updatedBy.toString()); + deviceProfile.getTagsMap().set(this.CREATED_BY_KEY, data.deviceProfile.createdBy.toString()); + + checkIfUserHasAccessToOrganization( + req, + data.deviceProfile.internalOrganizationId, + OrganizationAccessScope.ApplicationWrite + ); + + request.setDeviceProfile(deviceProfile); + return await this.put("device-profiles", this.deviceProfileClient, request); } private async isNameInUse(name: string, id?: string): Promise { @@ -69,86 +109,239 @@ export class DeviceProfileService extends GenericChirpstackConfigurationService .some(x => x.name.toLocaleLowerCase() == name.toLocaleLowerCase()); } - private async updateTags( - deviceProfileId: string, - req: AuthenticatedRequest - ): Promise<{ [id: string]: string | number }> { - const result: CreateDeviceProfileDto = await this.getOneById( + public async deleteDeviceProfile(id: string, req: AuthenticatedRequest): Promise { + const getReq = new GetDeviceProfileRequest(); + const result = await this.getOneById( "device-profiles", - deviceProfileId + id, + this.deviceProfileClient, + getReq ); - const tags = result.deviceProfile.tags; - tags[this.UPDATED_BY_KEY] = `${req.user.userId}`; - if (tags[this.ORG_ID_KEY] != null) { - checkIfUserHasAccessToOrganization(req, +tags[this.ORG_ID_KEY], OrganizationAccessScope.ApplicationWrite); - } - return tags; - } + const deviceProfileId = result.getDeviceProfile().getId(); + const listReq = new ListDevicesRequest(); + const listAppReq = new ListApplicationsRequest(); + listAppReq.setTenantId(await this.getDefaultOrganizationId()); - public async deleteDeviceProfile( - id: string, - req: AuthenticatedRequest - ): Promise { - const result: CreateDeviceProfileDto = await this.getOneById( - "device-profiles", - id + const applications = await this.getAllWithPagination( + "devices", + this.applicationServiceClient, + listAppReq, + 1000, + 0 ); - if (result.deviceProfile.tags[this.ORG_ID_KEY] != null) { + + let devices: DeviceListItem.AsObject[] = []; + for (let index = 0; index < applications.resultList.length; index++) { + listReq.setApplicationId(applications.resultList[index].id); + const devicesForApp = await this.getAllWithPagination( + "devices", + this.deviceServiceClient, + listReq, + 10000, + 0 + ); + devices = devices.concat(devicesForApp.resultList); + } + + const match = devices.find(e => e.deviceProfileId === deviceProfileId); + if (match) { + throw new ConflictException(ErrorCodes.DeleteNotAllowedHasLoRaWANDevices); + } + + if (result.getDeviceProfile().getTagsMap().get(this.ORG_ID_KEY) != null) { checkIfUserHasAccessToOrganization( req, - +result.deviceProfile.tags[this.ORG_ID_KEY], + +result.getDeviceProfile().getTagsMap().get(this.ORG_ID_KEY), OrganizationAccessScope.ApplicationWrite ); } - return await this.delete("device-profiles", id); + const deleteReq = new DeleteDeviceProfileRequest(); + deleteReq.setId(result.getDeviceProfile().getId()); + return await this.delete("device-profiles", this.deviceProfileClient, deleteReq); } - public async findAllDeviceProfiles( - limit?: number, - offset?: number - ): Promise { - const result = await this.getAllWithPagination( + public async findAllDeviceProfiles(limit?: number, offset?: number): Promise { + const req = new ListDeviceProfilesRequest(); + req.setTenantId(await this.getDefaultOrganizationId()); + + const result = await this.getAllWithPagination( "device-profiles", + this.deviceProfileClient, + req, limit, offset ); - await Promise.all( - result.result.map(async x => { - const dp = await this.findOneDeviceProfileById(x.id); - x.internalOrganizationId = +dp.deviceProfile.internalOrganizationId; - x.createdBy = +dp.deviceProfile.createdBy; - x.updatedBy = +dp.deviceProfile.updatedBy; - x.adrAlgorithmID = x.adrAlgorithmID ? x.adrAlgorithmID : "default"; - }) + const deviceResultListDto: DeviceProfileListDto[] = result.resultList.map(e => { + return { + name: e.name, + createdAt: timestampToDate(e.createdAt), + updatedAt: timestampToDate(e.updatedAt), + id: e.id, + }; + }); + const deviceProfileList: ListAllDeviceProfilesResponseDto = { + totalCount: result.totalCount.toString(), + result: deviceResultListDto, + }; + + await BluebirdPromise.all( + BluebirdPromise.map( + deviceProfileList.result, + async x => { + const dp = await this.findOneDeviceProfileById(x.id); + x.internalOrganizationId = +dp.deviceProfile.internalOrganizationId; + x.createdBy = +dp.deviceProfile.createdBy; + x.updatedBy = +dp.deviceProfile.updatedBy; + }, + { concurrency: 20 } + ) ); - return result; + return deviceProfileList; } public async findOneDeviceProfileById(id: string): Promise { - const result: CreateDeviceProfileDto = await this.getOneById( - "device-profiles", - id - ); - result.deviceProfile.internalOrganizationId = +result.deviceProfile.tags[ - this.ORG_ID_KEY - ]; - result.deviceProfile.createdBy = +result.deviceProfile.tags[this.CREATED_BY_KEY]; - result.deviceProfile.updatedBy = +result.deviceProfile.tags[this.UPDATED_BY_KEY]; - result.deviceProfile.adrAlgorithmID = result.deviceProfile.adrAlgorithmID ? result.deviceProfile.adrAlgorithmID : "default"; - - result.deviceProfile.tags[this.ORG_ID_KEY] = undefined; - result.deviceProfile.tags[this.CREATED_BY_KEY] = undefined; - result.deviceProfile.tags[this.UPDATED_BY_KEY] = undefined; + const req = new GetDeviceProfileRequest(); + req.setId(id); + try { + const result = await this.getOneById( + "device-profiles", + id, + this.deviceProfileClient, + req + ); + const deviceProfileObject = this.mapSingleDeviceProfileResponse(result); - return result; + return deviceProfileObject; + } catch (err) { + throw new InternalServerErrorException("Could not get device profile"); + } } public async updateDto(dto: DeviceProfileDto): Promise { - dto.networkServerID = await this.getDefaultNetworkServerId(); dto.organizationID = await this.getDefaultOrganizationId(); return dto; } + + private mapSingleDeviceProfileResponse(result: GetDeviceProfileResponse): CreateDeviceProfileDto { + const responseObject = result.getDeviceProfile().toObject(); + const deviceProfileMapped = this.mapDeviceInfoContent(responseObject); + const deviceProfileResponseObject: CreateDeviceProfileDto = { + deviceProfile: deviceProfileMapped, + createdAt: result.getCreatedAt().toDate(), + updatedAt: result.getUpdatedAt().toDate(), + internalOrganizationId: +result.getDeviceProfile().getTagsMap().get(this.ORG_ID_KEY), + }; + + deviceProfileResponseObject.deviceProfile.internalOrganizationId = +result + .getDeviceProfile() + .getTagsMap() + .get(this.ORG_ID_KEY); + deviceProfileResponseObject.deviceProfile.createdBy = +result + .getDeviceProfile() + .getTagsMap() + .get(this.CREATED_BY_KEY); + deviceProfileResponseObject.deviceProfile.updatedBy = +result + .getDeviceProfile() + .getTagsMap() + .get(this.UPDATED_BY_KEY); + + deviceProfileResponseObject.deviceProfile.tagsMap = deviceProfileResponseObject.deviceProfile.tagsMap.filter( + ([key]) => { + return key !== this.ORG_ID_KEY && key !== this.CREATED_BY_KEY && key !== this.UPDATED_BY_KEY; + } + ); + return deviceProfileResponseObject; + } + + private mapDeviceInfoContent(devProfile: DeviceProfile.AsObject) { + const deviceProfileMapped: DeviceProfileDto = { + name: devProfile.name, + id: devProfile.id, + adrAlgorithmID: devProfile.adrAlgorithmId, + macVersion: devProfile.macVersion, + regParamsRevision: devProfile.regParamsRevision, + classBTimeout: devProfile.classBTimeout, + classCTimeout: devProfile.classCTimeout, + pingSlotDR: devProfile.classBPingSlotDr, + pingSlotFreq: devProfile.classBPingSlotFreq, + pingSlotPeriod: devProfile.classBPingSlotNbK, + rfRegion: "EU868", + rxDROffset1: devProfile.abpRx1DrOffset, + rxDataRate2: devProfile.abpRx2Dr, + rxDelay1: devProfile.abpRx1Delay, + rxFreq2: devProfile.abpRx2Freq, + supportsClassB: devProfile.supportsClassB, + supportsClassC: devProfile.supportsClassC, + supportsJoin: devProfile.supportsOtaa, + tagsMap: devProfile.tagsMap, + devStatusReqFreq: devProfile.deviceStatusReqInterval, + }; + return deviceProfileMapped; + } + + mapToChirpstackDto(data: CreateDeviceProfileDto | UpdateDeviceProfileDto, isCreate?: boolean) { + const deviceProfile = new DeviceProfile(); + deviceProfile.setName(data.deviceProfile.name); + deviceProfile.setMacVersion(data.deviceProfile.macVersion); + deviceProfile.setRegParamsRevision(data.deviceProfile.regParamsRevision); + deviceProfile.setAdrAlgorithmId(data.deviceProfile.adrAlgorithmID); + deviceProfile.setClassBTimeout(data.deviceProfile.classBTimeout); + deviceProfile.setClassCTimeout(data.deviceProfile.classCTimeout); + deviceProfile.setId(data.deviceProfile.id); + deviceProfile.setClassBPingSlotDr(data.deviceProfile.pingSlotDR); + deviceProfile.setClassBPingSlotFreq(data.deviceProfile.pingSlotFreq); + deviceProfile.setClassBPingSlotNbK(data.deviceProfile.pingSlotPeriod); + //region 0 = EU868 + deviceProfile.setRegion(0); + deviceProfile.setAbpRx1DrOffset(data.deviceProfile.rxDROffset1); + deviceProfile.setAbpRx2Dr(data.deviceProfile.rxDataRate2); + deviceProfile.setAbpRx1Delay(data.deviceProfile.rxDelay1); + deviceProfile.setAbpRx2Freq(data.deviceProfile.rxFreq2); + deviceProfile.setSupportsClassB(data.deviceProfile.supportsClassB); + deviceProfile.setSupportsClassC(data.deviceProfile.supportsClassC); + deviceProfile.setSupportsOtaa(data.deviceProfile.supportsJoin); + deviceProfile.setDeviceStatusReqInterval( + data.deviceProfile.devStatusReqFreq === undefined ? 1 : data.deviceProfile.devStatusReqFreq + ); + + isCreate ? deviceProfile.setTenantId(data.deviceProfile.organizationID) : {}; + + return deviceProfile; + } + + public async getAdrAlgorithmsForChirpstack(): Promise { + const result = await this.getAdrAlgorithms(); + + const adrAlgoritmList: AdrAlgorithmDto[] = result.getResultList().map(e => { + return { + id: e.getId(), + name: e.getName(), + }; + }); + + return { + adrAlgorithms: adrAlgoritmList, + }; + } + + async getAdrAlgorithms(): Promise { + const metaData = this.makeMetadataHeader(); + const getPromise = new Promise((resolve, reject) => { + this.deviceProfileClient.listAdrAlgorithms(new Empty(), metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + resolve(resp); + } + }); + }); + try { + return await getPromise; + } catch (err) { + throw new NotFoundException(); + } + } } diff --git a/src/services/chirpstack/gateway-boostrapper.service.ts b/src/services/chirpstack/gateway-boostrapper.service.ts index 368d1223..80fd9705 100644 --- a/src/services/chirpstack/gateway-boostrapper.service.ts +++ b/src/services/chirpstack/gateway-boostrapper.service.ts @@ -1,6 +1,6 @@ import { ListAllGatewaysResponseDto } from "@dto/chirpstack/list-all-gateways.dto"; import { GatewayStatusHistory } from "@entities/gateway-status-history.entity"; -import { Inject, OnApplicationBootstrap } from "@nestjs/common"; +import { Inject, InternalServerErrorException, Logger, OnApplicationBootstrap } from "@nestjs/common"; import { ChirpstackGatewayService } from "./chirpstack-gateway.service"; import { GatewayStatusHistoryService } from "./gateway-status-history.service"; @@ -15,12 +15,18 @@ export class GatewayBootstrapperService implements OnApplicationBootstrap { @Inject(ChirpstackGatewayService) private chirpstackGatewayService: ChirpstackGatewayService ) {} + private readonly logger = new Logger(GatewayBootstrapperService.name); async onApplicationBootstrap(): Promise { - const chirpstackGatewaysPromise = this.chirpstackGatewayService.getAll(); - const latestStatusHistories = await this.statusHistoryService.findLatestPerGateway(); - const gateways = await chirpstackGatewaysPromise; - await this.seedGatewayStatus(gateways, latestStatusHistories); + try { + const chirpstackGatewaysPromise = this.chirpstackGatewayService.getAll(); + const latestStatusHistories = await this.statusHistoryService.findLatestPerGateway(); + const gateways = await chirpstackGatewaysPromise; + await this.seedGatewayStatus(gateways, latestStatusHistories); + } catch (e) { + this.logger.error("Error in applicationBootstrap"); + throw new InternalServerErrorException(e); + } } /** @@ -34,10 +40,10 @@ export class GatewayBootstrapperService implements OnApplicationBootstrap { errorTime.setSeconds(errorTime.getSeconds() - 150); // Don't overwrite ones which already have a status history - const newHistoriesForMissingGateways = gateways.result.reduce((res: GatewayStatusHistory[], gateway) => { - if (!statusHistories.some(history => history.mac === gateway.gatewayId)) { - // Best fit is to imitate the status logic from Chirpstack. - const lastSeenDate = new Date(gateway.lastSeenAt); + const newHistoriesForMissingGateways = gateways.resultList.reduce((res: GatewayStatusHistory[], gateway) => { + if (!statusHistories.some(history => history.mac === gateway.gatewayId) && gateway.lastSeenAt) { + const lastSeenDate = gateway.lastSeenAt; + const wasOnline = errorTime.getTime() < lastSeenDate.getTime(); res.push({ diff --git a/src/services/chirpstack/gateway-status-history.service.ts b/src/services/chirpstack/gateway-status-history.service.ts index cbb7394d..5638b016 100644 --- a/src/services/chirpstack/gateway-status-history.service.ts +++ b/src/services/chirpstack/gateway-status-history.service.ts @@ -25,7 +25,7 @@ export class GatewayStatusHistoryService { // Very expensive operation. Since no gateway data is stored on the backend database, we need // to get them from Chirpstack. There's no filter by tags support so we must fetch all gateways. const gateways = await this.chirpstackGatewayService.getAll(query.organizationId); - const gatewayIds = gateways.result.map(gateway => gateway.gatewayId); + const gatewayIds = gateways.resultList.map(gateway => gateway.gatewayId); const fromDate = gatewayStatusIntervalToDate(query.timeInterval); if (!gatewayIds.length) { @@ -38,7 +38,6 @@ export class GatewayStatusHistoryService { timestamp: MoreThanOrEqual(fromDate), }, }); - // To know the status of each gateway up till the first status since the start date, // we must fetch the previous status const latestStatusHistoryPerGatewayBeforePeriod = await this.fetchLatestStatusBeforeDate(gatewayIds, fromDate); @@ -49,7 +48,7 @@ export class GatewayStatusHistoryService { latestStatusHistoryPerGatewayBeforePeriod ); - const data: GatewayStatus[] = this.mapStatusHistoryToGateways(gateways.result, statusHistories); + const data: GatewayStatus[] = this.mapStatusHistoryToGateways(gateways.resultList, statusHistories); return { data, diff --git a/src/services/chirpstack/generic-chirpstack-configuration.service.ts b/src/services/chirpstack/generic-chirpstack-configuration.service.ts index 487835a0..24ec28a0 100644 --- a/src/services/chirpstack/generic-chirpstack-configuration.service.ts +++ b/src/services/chirpstack/generic-chirpstack-configuration.service.ts @@ -5,277 +5,222 @@ import { Logger, NotFoundException, } from "@nestjs/common"; -import { AxiosRequestConfig, AxiosResponse } from "axios"; - -import { HeaderDto } from "@dto/chirpstack/header.dto"; -import { ListAllNetworkServerResponseDto } from "@dto/chirpstack/list-all-network-server-response.dto"; -import { ListAllOrganizationsResponseDto } from "@dto/chirpstack/list-all-organizations-response.dto"; -import { AuthorizationType } from "@enum/authorization-type.enum"; -import { ErrorCodes } from "@enum/error-codes.enum"; - -import { JwtToken } from "./jwt-token"; import { ListAllChirpstackApplicationsResponseDto } from "@dto/chirpstack/list-all-applications-response.dto"; -import { HttpService } from "@nestjs/axios"; +import { Metadata, ServiceError, credentials } from "@grpc/grpc-js"; +import configuration from "@config/configuration"; +import { TenantServiceClient } from "@chirpstack/chirpstack-api/api/tenant_grpc_pb"; +import { ListTenantsRequest, ListTenantsResponse } from "@chirpstack/chirpstack-api/api/tenant_pb"; +import { ApplicationServiceClient } from "@chirpstack/chirpstack-api/api/application_grpc_pb"; +import { ListApplicationsRequest, ListApplicationsResponse } from "@chirpstack/chirpstack-api/api/application_pb"; +import { ChirpstackApplicationResponseDto } from "@dto/chirpstack/chirpstack-application-response.dto"; +import { IdResponse } from "@interfaces/chirpstack-id-response.interface"; +import { DeviceServiceClient } from "@chirpstack/chirpstack-api/api/device_grpc_pb"; +import { GatewayServiceClient } from "@chirpstack/chirpstack-api/api/gateway_grpc_pb"; +import { DeviceProfileServiceClient } from "@chirpstack/chirpstack-api/api/device_profile_grpc_pb"; +import { MulticastGroupServiceClient } from "@chirpstack/chirpstack-api/api/multicast_group_grpc_pb"; @Injectable() export class GenericChirpstackConfigurationService { - baseUrl = `http://${ - process.env.CHIRPSTACK_APPLICATION_SERVER_HOSTNAME || "localhost" - }:${process.env.CHIRPSTACK_APPLICATION_SERVER_PORT || "8080"}`; - - networkServer = `${ - process.env.CHIRPSTACK_NETWORK_SERVER || "chirpstack-network-server" - }:${process.env.CHIRPSTACK_NETWORK_SERVER_PORT || "8000"}`; - constructor(private httpService: HttpService) {} + baseUrlGRPC = `${process.env.CHIRPSTACK_HOSTNAME || "localhost"}:${process.env.CHIRPSTACK_PORT || "8080"}`; private readonly innerLogger = new Logger(GenericChirpstackConfigurationService.name); - - setupHeader(endPoint: string, limit?: number, offset?: number): HeaderDto { - const timeoutMs = 30 * 1000; - let url = this.baseUrl + "/api/" + endPoint; - - // If limits are supplied, add these as query params - if (limit != null && offset != null) { - url += `${ - endPoint.indexOf("?") >= 0 ? "&" : "?" - }limit=${limit}&offset=${offset}`; - } - - const headerDto: HeaderDto = { - url, - timeout: timeoutMs, - authorizationType: AuthorizationType.HEADER_BASED_AUTHORIZATION, - authorizationHeader: "Bearer " + JwtToken.setupToken(), - }; - - return headerDto; + protected applicationServiceClient = new ApplicationServiceClient(this.baseUrlGRPC, credentials.createInsecure()); + protected deviceServiceClient = new DeviceServiceClient(this.baseUrlGRPC, credentials.createInsecure()); + protected gatewayClient = new GatewayServiceClient(this.baseUrlGRPC, credentials.createInsecure()); + protected deviceProfileClient = new DeviceProfileServiceClient(this.baseUrlGRPC, credentials.createInsecure()); + protected multicastServiceClient = new MulticastGroupServiceClient(this.baseUrlGRPC, credentials.createInsecure()); + protected readonly ORG_ID_KEY = "internalOrganizationId"; + protected readonly UPDATED_BY_KEY = "os2iot-updated-by"; + protected readonly CREATED_BY_KEY = "os2iot-created-by"; + + makeMetadataHeader(): Metadata { + const metadata = new Metadata(); + metadata.set("authorization", "Bearer " + configuration()["chirpstack"]["apikey"]); + return metadata; } - makeAxiosConfiguration(config: { - timeout: number; - authorizationHeader: string; - }): AxiosRequestConfig { - const axiosConfig: AxiosRequestConfig = { - timeout: config.timeout, - headers: { "Content-Type": "application/json" }, - }; - - axiosConfig.headers["Authorization"] = config.authorizationHeader; - - return axiosConfig; - } - - async post(endpoint: string, data: T): Promise { - const header = this.setupHeader(endpoint); - const axiosConfig = this.makeAxiosConfiguration(header); - + async post(logName: string, client: any, request: any): Promise { + const metaData = this.makeMetadataHeader(); + const createPromise = new Promise((resolve, reject) => { + client.create(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.innerLogger.debug(`post:${logName} success`); + resolve(resp.toObject()); + } + }); + }); try { - const result = await this.httpService - .post(header.url, data, axiosConfig) - .toPromise(); - - this.innerLogger.debug( - `post: ${JSON.stringify( - data - )} to ${endpoint} resulting in ${result.status.toString()} and message: ${ - result.statusText - }` - ); - - return result; + return await createPromise; } catch (err) { - this.innerLogger.error( - `post got error: ${JSON.stringify(err?.response?.data)}` - ); - - this.throwBadRequestIf400(err); - - throw err; + this.innerLogger.error(`POST ${logName} got error: ${err}`); + throw new BadRequestException(); } } - private throwBadRequestIf400(err: any) { - if (err?.response?.status == 400) { - throw new BadRequestException({ - success: false, - chirpstackError: err?.response?.data, + async put(logName: string, client: any, request: any): Promise { + const metaData = this.makeMetadataHeader(); + const updatePromise = new Promise((resolve, reject) => { + client.update(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.innerLogger.debug(`update :${logName} success`); + resolve(resp); + } }); - } - } - - async put(endpoint: string, data: T, id: string): Promise { - const header = this.setupHeader(endpoint); - const axiosConfig = this.makeAxiosConfiguration(header); - const url = header.url + "/" + id; + }); try { - const result = await this.httpService.put(url, data, axiosConfig).toPromise(); - - this.innerLogger.debug( - `put: ${JSON.stringify( - data - )} to ${endpoint} resulting in ${result.status.toString()} and message: ${ - result.statusText - }` - ); - - return result; + await updatePromise; + return; } catch (err) { - this.throwBadRequestIf400(err); - this.innerLogger.error(`Put got error: `); - throw new NotFoundException(ErrorCodes.IdDoesNotExists); + this.innerLogger.error(`UPDATE ${logName} got error: ${err}`); + throw new BadRequestException(); } } - async getOneById(endpoint: string, id: string): Promise { - const header = this.setupHeader(endpoint); - const axiosConfig = this.makeAxiosConfiguration(header); + async getOneById(logName: string, id: string, client: any, request: any): Promise { + const metaData = this.makeMetadataHeader(); + request.setId(id); + const getPromise = new Promise((resolve, reject) => { + client.get(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.innerLogger.debug(`get from:${logName} success`); + resolve(resp); + } + }); + }); try { - const url = header.url + "/" + id; - const result = await this.httpService.get(url, axiosConfig).toPromise(); - - this.innerLogger.debug( - `get by ID from:${endpoint} resulting in ${result.status.toString()} and message: ${ - result.statusText - }` - ); - - return result.data; + return await getPromise; } catch (err) { - this.innerLogger.error( - `GET: ${err?.config?.url} Status: ${ - err?.response?.status - }. Error response: '${JSON.stringify(err?.response?.data)}'` - ); - throw new NotFoundException(ErrorCodes.IdDoesNotExists); + this.innerLogger.error(`GET ${logName} got error: ${err}`); + throw new NotFoundException(); } } - async delete(endpoint: string, id?: string): Promise { - const header = this.setupHeader(endpoint); - const axiosConfig = this.makeAxiosConfiguration(header); - const url = header.url + (id != undefined ? "/" + id : ""); - try { - const result = await this.httpService.delete(url, axiosConfig).toPromise(); - - this.innerLogger.debug( - `DELETE ${url} - Status: ${result.status.toString()} and message: ${ - result.statusText - }` - ); - return result; - } catch (err) { - this.innerLogger.error( - `DELETE ${url} - Got error: ${JSON.stringify(err?.response?.data)}` - ); - throw new InternalServerErrorException(err?.response?.data); + async delete(logName: string, client: any, request: any): Promise { + //MAYBE return boolean of result (succes vs failure) + if (client) { + const metaData = this.makeMetadataHeader(); + const deletePromise = new Promise((resolve, reject) => { + client.delete(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.innerLogger.debug(`delete :${logName} success`); + resolve(resp); + } + }); + }); + try { + await deletePromise; + return; + } catch (err) { + this.innerLogger.error(`DELETE ${logName} got error: ${err}`); + throw new BadRequestException(); + } } } - async get(endpoint: string): Promise { - const header = this.setupHeader(endpoint); - const axiosConfig = this.makeAxiosConfiguration(header); - + async get(logName: string, client: any, request: any): Promise { + const metaData = this.makeMetadataHeader(); + const getPromise = new Promise((resolve, reject) => { + client.get(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.innerLogger.debug(`get from:${logName} success`); + resolve(resp); + } + }); + }); try { - const result = await this.httpService - .get(header.url, axiosConfig) - .toPromise(); - - return result.data; + return await getPromise; } catch (err) { - this.innerLogger.error( - `GET '${header.url}' failed with error (${ - err?.response?.status - }): '${JSON.stringify(err?.response?.data)}'` - ); - if (err?.response?.status == 404) { - throw new NotFoundException(err?.response?.data); - } - - throw new InternalServerErrorException(err?.response?.data); + this.innerLogger.error(`GET ${logName} got error: ${err}`); + throw new NotFoundException(); } } - async getAllApplicationsWithPagination( - organizationID: string - ): Promise { - return this.getAllWithPagination( - `applications?limit=100&organizationID=${organizationID}` + async getAllApplicationsWithPagination(tenantID: string): Promise { + const req = new ListApplicationsRequest(); + req.setTenantId(await this.getDefaultOrganizationId()); + + const result = await this.getAllWithPagination( + `applications?limit=100&organizationID=${tenantID}`, + this.applicationServiceClient, + req, + 100, + undefined ); + const chirpstackApplicationResponseDto: ChirpstackApplicationResponseDto[] = []; + result.resultList.map(e => { + const resultItem: ChirpstackApplicationResponseDto = { + name: e.name, + description: e.description, + id: e.id, + tenantId: tenantID, + }; + chirpstackApplicationResponseDto.push(resultItem); + }); + return { + totalCount: result.totalCount, + resultList: chirpstackApplicationResponseDto, + }; } async getAllWithPagination( - endpoint: string, + logName: string, + client: any, + request: any, limit?: number, offset?: number ): Promise { - const header = this.setupHeader(endpoint, limit, offset); - const axiosConfig = this.makeAxiosConfiguration(header); - + const metaData = this.makeMetadataHeader(); + request.setLimit(limit); + request.setOffset(offset); + + const getListPromise = new Promise((resolve, reject) => { + client.list(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + const result = resp.toObject(); + resolve(result); + this.innerLogger.debug(`get all from:${logName} success`); + } + }); + }); try { - const result = await this.httpService - .get(header.url, axiosConfig) - .toPromise(); - this.innerLogger.debug( - `get all from:${endpoint} resulting in ${result.status.toString()} and message: ${ - result.statusText - }` - ); - return result.data; + return await getListPromise; } catch (err) { - this.innerLogger.error(`GET ${header.url} got error: ${err}`); + this.innerLogger.error(`GET ${logName} got error: ${err}`); throw new NotFoundException(); } } - public async getNetworkServers( - limit?: number, - offset?: number - ): Promise { - const res = await this.getAllWithPagination( - "network-servers", - limit, - offset - ); - return res; - } + public async getTenants(limit?: number, offset?: number): Promise { + const tenantClient = new TenantServiceClient(this.baseUrlGRPC, credentials.createInsecure()); + const req = new ListTenantsRequest(); - public async getOrganizations( - limit?: number, - offset?: number - ): Promise { - const res = await this.getAllWithPagination( + const res = await this.getAllWithPagination( "organizations", + tenantClient, + req, limit, offset ); return res; } - public async getDefaultNetworkServerId(): Promise { - let id = null; - await this.getNetworkServers(1000, 0).then(response => { - response.result.forEach(element => { - if (element.name.toLowerCase() === "os2iot") { - id = element.id.toString(); - } - }); - }); - if (id) { - return id; - } - throw new InternalServerErrorException( - "Could not find any NetworkServer in Chirpstack named: 'OS2iot'" - ); - } - public async getDefaultOrganizationId(): Promise { let id = null; - await this.getOrganizations(1000, 0).then(response => { - response.result.forEach(element => { - if ( - element.name.toLowerCase() == "os2iot" || - element.name.toLowerCase() == "chirpstack" - ) { + await this.getTenants(1000, 0).then(response => { + response.resultList.forEach(element => { + if (element.name.toLowerCase() == "os2iot" || element.name.toLowerCase() == "chirpstack") { id = element.id; } }); diff --git a/src/services/chirpstack/jwt-token.ts b/src/services/chirpstack/jwt-token.ts deleted file mode 100644 index e58284b6..00000000 --- a/src/services/chirpstack/jwt-token.ts +++ /dev/null @@ -1,28 +0,0 @@ -import configuration from "@config/configuration"; -import { Injectable } from "@nestjs/common"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import * as nJwt from "njwt"; - -@Injectable() -export class JwtToken { - static setupToken(): string { - const claims = { - iss: "as", // issuer of the claim - aud: "as", // audience for which the claim is intended - iat: Math.floor(new Date().valueOf() / 1000 - 10), // unix time from which the token is valid - nbf: Math.floor(new Date().valueOf() / 1000 - 10), // unix time from which the token is valid - exp: Math.floor(new Date().valueOf() / 1000) + 60 * 60 * 24 * 14, // unix time when the token expires - sub: "user", // subject of the claim (an user) - username: "admin", // username the client claims to be - }; - - const jwt = nJwt.create( - claims, - configuration()["chirpstack"]["jwtsecret"], - "HS256" - ); - const token = jwt.compact(); - return token; - } -} diff --git a/src/services/device-management/multicast.service.ts b/src/services/chirpstack/multicast.service.ts similarity index 53% rename from src/services/device-management/multicast.service.ts rename to src/services/chirpstack/multicast.service.ts index e6db0393..4bd39cfc 100644 --- a/src/services/device-management/multicast.service.ts +++ b/src/services/chirpstack/multicast.service.ts @@ -3,27 +3,21 @@ import { ListAllMulticastsResponseDto } from "@dto/list-all-multicasts-response. import { ListAllMulticastsDto } from "@dto/list-all-multicasts.dto"; import { Multicast } from "@entities/multicast.entity"; import { ErrorCodes } from "@enum/error-codes.enum"; -import { - BadRequestException, - forwardRef, - Inject, - Injectable, - Logger, -} from "@nestjs/common"; +import { BadRequestException, forwardRef, Inject, Injectable, Logger, NotFoundException } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { GenericChirpstackConfigurationService } from "@services/chirpstack/generic-chirpstack-configuration.service"; import { DeleteResult, Repository, SelectQueryBuilder } from "typeorm"; import { CreateMulticastDto } from "../../entities/dto/create-multicast.dto"; import { UpdateMulticastDto } from "../../entities/dto/update-multicast.dto"; -import { ApplicationService } from "./application.service"; -import { AxiosResponse } from "axios"; +import { ApplicationService } from "../device-management/application.service"; import { ChirpstackMulticastContentsDto } from "@dto/chirpstack/chirpstack-multicast-contents.dto"; import { LorawanMulticastDefinition } from "@entities/lorawan-multicast.entity"; import { IoTDeviceType } from "@enum/device-type.enum"; -import { AddDeviceToMulticastDto } from "@dto/chirpstack-add-device-multicast.dto"; import { LoRaWANDevice } from "@entities/lorawan-device.entity"; -import { IoTDevice } from "@entities/iot-device.entity"; -import { MulticastDownlinkQueueResponseDto } from "@dto/chirpstack/chirpstack-multicast-downlink-queue-response.dto"; +import { + MulticastDownlinkQueueResponseDto, + MulticastQueueItem, +} from "@dto/chirpstack/chirpstack-multicast-downlink-queue-response.dto"; import { CreateMulticastDownlinkDto } from "@dto/create-multicast-downlink.dto"; import { CreateChirpstackMulticastQueueItemDto, @@ -31,20 +25,36 @@ import { } from "@dto/chirpstack/create-chirpstack-multicast-queue-item.dto"; import { ChirpstackDeviceService } from "@services/chirpstack/chirpstack-device.service"; import { ChirpstackDeviceContentsDto } from "@dto/chirpstack/chirpstack-device-contents.dto"; -import { HttpService } from "@nestjs/axios"; +import { + AddDeviceToMulticastGroupRequest, + CreateMulticastGroupRequest, + DeleteMulticastGroupRequest, + EnqueueMulticastGroupQueueItemRequest, + FlushMulticastGroupQueueRequest, + GetMulticastGroupRequest, + GetMulticastGroupResponse, + ListMulticastGroupQueueRequest, + ListMulticastGroupQueueResponse, + MulticastGroup, + MulticastGroupQueueItem, + MulticastGroupType, + UpdateMulticastGroupRequest, +} from "@chirpstack/chirpstack-api/api/multicast_group_pb"; +import { MulticastGroupServiceClient } from "@chirpstack/chirpstack-api/api/multicast_group_grpc_pb"; +import { ServiceError } from "@grpc/grpc-js"; +import { IdResponse } from "@interfaces/chirpstack-id-response.interface"; +import { multicastGroup } from "@enum/multicast-type.enum"; @Injectable() export class MulticastService extends GenericChirpstackConfigurationService { constructor( - internalHttpService: HttpService, - @InjectRepository(Multicast) private multicastRepository: Repository, @Inject(forwardRef(() => ApplicationService)) // because of circular reference private applicationService: ApplicationService, private chirpStackDeviceService: ChirpstackDeviceService ) { - super(internalHttpService); + super(); } private readonly logger = new Logger(MulticastService.name); multicastGroupUrl = "multicast-groups"; @@ -62,10 +72,7 @@ export class MulticastService extends GenericChirpstackConfigurationService { let queryBuilder = this.multicastRepository .createQueryBuilder("multicast") .innerJoinAndSelect("multicast.application", "application") - .innerJoinAndSelect( - "multicast.lorawanMulticastDefinition", - "lorawan-multicast" - ) + .innerJoinAndSelect("multicast.lorawanMulticastDefinition", "lorawan-multicast") .skip(query?.offset ? +query.offset : 0) .take(query?.limit ? +query.limit : 100) .orderBy(orderByColumn, direction); @@ -82,10 +89,7 @@ export class MulticastService extends GenericChirpstackConfigurationService { } private getSortingForMulticasts(query: ListAllMulticastsDto) { let orderBy = `multicast.id`; - if ( - (query?.orderOn != null && query.orderOn == "id") || - query.orderOn == "groupName" - ) { + if ((query?.orderOn != null && query.orderOn == "id") || query.orderOn == "groupName") { orderBy = `multicast.${query.orderOn}`; } return orderBy; @@ -100,12 +104,9 @@ export class MulticastService extends GenericChirpstackConfigurationService { appId: query.applicationId, }); } else if (applicationIds) { - queryBuilder = queryBuilder.where( - '"application"."id" IN (:...allowedApplications)', - { - allowedApplications: applicationIds, - } - ); + queryBuilder = queryBuilder.where('"application"."id" IN (:...allowedApplications)', { + allowedApplications: applicationIds, + }); } return queryBuilder; } @@ -120,38 +121,24 @@ export class MulticastService extends GenericChirpstackConfigurationService { }); } - async create( - createMulticastDto: CreateMulticastDto, - userId: number - ): Promise { - //since the multicast is gonna be created in both the DB with relations and in chirpstack, two different objects is gonna be used. + async create(createMulticastDto: CreateMulticastDto, userId: number): Promise { const dbMulticast = new Multicast(); dbMulticast.lorawanMulticastDefinition = new LorawanMulticastDefinition(); - const chirpStackMulticast = new CreateMulticastChirpStackDto(); - chirpStackMulticast.multicastGroup = new ChirpstackMulticastContentsDto(); - const mappedDbMulticast = await this.mapMulticastDtoToDbMulticast( - createMulticastDto, - dbMulticast - ); + const mappedDbMulticast = await this.mapMulticastDtoToDbMulticast(createMulticastDto, dbMulticast); mappedDbMulticast.createdBy = userId; mappedDbMulticast.updatedBy = userId; mappedDbMulticast.lorawanMulticastDefinition.createdBy = userId; mappedDbMulticast.lorawanMulticastDefinition.updatedBy = userId; if (!!createMulticastDto.iotDevices) { - const lorawanDevices = this.checkForLorawan(createMulticastDto); + const lorawanDevices = this.filterForLoRaWAN(createMulticastDto); if (lorawanDevices.length > 0) { if (await this.checkForDifferentAppID(lorawanDevices)) { // If they all have same serviceID / appID then proceed. - await this.createMulticastInChirpstack( - createMulticastDto, - chirpStackMulticast, - lorawanDevices, - mappedDbMulticast - ); + await this.createMulticastInChirpstack(createMulticastDto, lorawanDevices, mappedDbMulticast); } else { - throw new BadRequestException(ErrorCodes.DifferentServiceprofile); + throw new BadRequestException(ErrorCodes.InvalidPost); } } } @@ -160,25 +147,23 @@ export class MulticastService extends GenericChirpstackConfigurationService { async createMulticastInChirpstack( createMulticastDto: CreateMulticastDto | UpdateMulticastDto, - chirpStackMulticast: CreateMulticastChirpStackDto, lorawanDevices: LoRaWANDevice[], mappedDbMulticast: Multicast ): Promise { const mappedChirpStackMulticast = await this.mapMulticastDtoToChirpStackMulticast( createMulticastDto, - chirpStackMulticast, lorawanDevices[0] // used for setting appID ); - - const result = await this.post(this.multicastGroupUrl, mappedChirpStackMulticast); // This creates the multicast in chirpstack. Chirpstack returns an id as a string + const req = new CreateMulticastGroupRequest(); + req.setMulticastGroup(mappedChirpStackMulticast); + const result: IdResponse = await this.post(this.multicastGroupUrl, this.multicastServiceClient, req); // This creates the multicast in chirpstack. Chirpstack returns an id as a string await this.addDevices(createMulticastDto, result); // iotDevices are added to multicast in a seperate endpoint. this.handlePossibleError(result, createMulticastDto); - if (result.status === 200) { - mappedDbMulticast.lorawanMulticastDefinition.chirpstackGroupId = - result.data.id; + if (result.id) { + mappedDbMulticast.lorawanMulticastDefinition.chirpstackGroupId = result.id; } } @@ -188,27 +173,15 @@ export class MulticastService extends GenericChirpstackConfigurationService { userId: number ): Promise { const oldMulticast: Multicast = { ...existingMulticast }; - const mappedMulticast = await this.mapMulticastDtoToDbMulticast( - updateMulticastDto, - existingMulticast - ); - const lorawanDevices = this.checkForLorawan(updateMulticastDto); - const oldLorawanDevices = this.checkForLorawan(oldMulticast); + const mappedMulticast = await this.mapMulticastDtoToDbMulticast(updateMulticastDto, existingMulticast); + const lorawanDevices = this.filterForLoRaWAN(updateMulticastDto); + const oldLorawanDevices = this.filterForLoRaWAN(oldMulticast); if (lorawanDevices.length > 0 || oldLorawanDevices.length > 0) { // check if new lorawan devices is included. If so, either create or update in chirpstack. Otherwise, just update db if (!existingMulticast.lorawanMulticastDefinition.chirpstackGroupId) { - await this.createIfNotInChirpstack( - lorawanDevices, - updateMulticastDto, - mappedMulticast - ); + await this.createIfNotInChirpstack(lorawanDevices, updateMulticastDto, mappedMulticast); } else { - await this.updateLogic( - existingMulticast, - lorawanDevices, - updateMulticastDto, - oldMulticast - ); + await this.updateLogic(existingMulticast, lorawanDevices, updateMulticastDto); } } mappedMulticast.updatedBy = userId; @@ -218,40 +191,24 @@ export class MulticastService extends GenericChirpstackConfigurationService { async updateMulticastToChirpstack( updateMulticastDto: UpdateMulticastDto, existingChirpStackMulticast: CreateMulticastChirpStackDto, - lorawanDevices: LoRaWANDevice[], - existingMulticast: Multicast + lorawanDevices: LoRaWANDevice[] ): Promise { const mappedChirpStackMulticast = await this.mapMulticastDtoToChirpStackMulticast( updateMulticastDto, - existingChirpStackMulticast, lorawanDevices[0] ); - - const result = await this.put( - this.multicastGroupUrl, - mappedChirpStackMulticast, - existingMulticast.lorawanMulticastDefinition.chirpstackGroupId - ); - this.handlePossibleError(result, updateMulticastDto); - - const added: IoTDevice[] = []; - const removed: IoTDevice[] = []; - this.compareDevices(existingMulticast, updateMulticastDto, added, removed); - await this.updateDevices( - // add's and removes devices from chirpstack - removed, - added, - existingMulticast.lorawanMulticastDefinition.chirpstackGroupId - ); + mappedChirpStackMulticast.setId(existingChirpStackMulticast.multicastGroup.id); + const req = new UpdateMulticastGroupRequest(); + req.setMulticastGroup(mappedChirpStackMulticast); + try { + await this.put(this.multicastGroupUrl, this.multicastServiceClient, req); + } catch (e) { + throw new BadRequestException(e); + } } - checkForLorawan( - multicastDto: CreateMulticastDto | Multicast | UpdateMulticastDto - ): LoRaWANDevice[] { - const lorawanDevices = multicastDto.iotDevices.filter( - x => x.type === IoTDeviceType.LoRaWAN - ) as LoRaWANDevice[]; - return lorawanDevices; + filterForLoRaWAN(multicastDto: CreateMulticastDto | Multicast | UpdateMulticastDto): LoRaWANDevice[] { + return multicastDto.iotDevices.filter(x => x.type === IoTDeviceType.LoRaWAN) as LoRaWANDevice[]; } async validateNewDevicesAppID( @@ -261,17 +218,12 @@ export class MulticastService extends GenericChirpstackConfigurationService { const devices: ChirpstackDeviceContentsDto[] = []; for (let index = 0; index < lorawanDevices.length; index++) { - const lora = await this.chirpStackDeviceService.getChirpstackDevice( - lorawanDevices[index].deviceEUI - ); + const lora = await this.chirpStackDeviceService.getChirpstackDevice(lorawanDevices[index].deviceEUI); devices.push(lora); } for (let i = 0; i < devices.length; i++) { - if ( - devices[i].applicationID !== - chirpStackMulticast.multicastGroup.applicationID - ) { + if (devices[i].applicationID !== chirpStackMulticast.multicastGroup.applicationID) { // if one of the application id is different than the first one, then we know that there is different // service profiles. Therefore, return false. return false; @@ -284,9 +236,7 @@ export class MulticastService extends GenericChirpstackConfigurationService { const devices: ChirpstackDeviceContentsDto[] = []; for (let index = 0; index < lorawanDevices.length; index++) { - const lora = await this.chirpStackDeviceService.getChirpstackDevice( - lorawanDevices[index].deviceEUI - ); + const lora = await this.chirpStackDeviceService.getChirpstackDevice(lorawanDevices[index].deviceEUI); devices.push(lora); } if (devices.length > 0) { @@ -304,32 +254,46 @@ export class MulticastService extends GenericChirpstackConfigurationService { return true; // If the appId is equal for each element, then it's the same service profile } - async getChirpstackMulticast( - multicastId: string - ): Promise { - const res = await this.get( - `multicast-groups/${multicastId}` + async getChirpstackMulticast(multicastId: string): Promise { + const req = new GetMulticastGroupRequest(); + req.setId(multicastId); + const res = await this.get( + `multicast-groups/${multicastId}`, + this.multicastServiceClient, + req ); + const multicast = res.getMulticastGroup(); + const multicastDtoContent: ChirpstackMulticastContentsDto = { + applicationID: multicast.getApplicationId(), + dr: multicast.getDr(), + fCnt: multicast.getFCnt(), + frequency: multicast.getFrequency(), + mcAddr: multicast.getMcAddr(), + mcAppSKey: multicast.getMcAppSKey(), + mcNwkSKey: multicast.getMcNwkSKey(), + name: multicast.getName(), + groupType: multicastGroup.ClassC, + id: multicast.getId(), + }; + + const returnDto: CreateMulticastChirpStackDto = { multicastGroup: multicastDtoContent }; - return res; + return returnDto; } - async multicastDelete( - id: number, - existingMulticast: Multicast - ): Promise { - const loraDevices = this.checkForLorawan(existingMulticast); + async deleteMulticast(id: number, existingMulticast: Multicast): Promise { + const loraDevices = this.filterForLoRaWAN(existingMulticast); if (loraDevices.length > 0) { - await this.deleteMulticastChirpstack( - existingMulticast.lorawanMulticastDefinition.chirpstackGroupId - ); + await this.deleteMulticastChirpstack(existingMulticast.lorawanMulticastDefinition.chirpstackGroupId); } return this.multicastRepository.delete(id); } - async deleteMulticastChirpstack(id: string): Promise { + async deleteMulticastChirpstack(id: string): Promise { try { - return await this.delete(this.multicastGroupUrl, id); + const req = new DeleteMulticastGroupRequest(); + req.setId(id); + return await this.delete(this.multicastGroupUrl, this.multicastServiceClient, req); } catch (err) { throw err; } @@ -341,8 +305,7 @@ export class MulticastService extends GenericChirpstackConfigurationService { ): Promise { multicast.groupName = multicastDto.name; multicast.lorawanMulticastDefinition.address = multicastDto.mcAddr; - multicast.lorawanMulticastDefinition.applicationSessionKey = - multicastDto.mcAppSKey; + multicast.lorawanMulticastDefinition.applicationSessionKey = multicastDto.mcAppSKey; multicast.lorawanMulticastDefinition.networkSessionKey = multicastDto.mcNwkSKey; multicast.lorawanMulticastDefinition.dataRate = multicastDto.dr; multicast.lorawanMulticastDefinition.frameCounter = multicastDto.fCnt; @@ -356,9 +319,7 @@ export class MulticastService extends GenericChirpstackConfigurationService { multicastDto.applicationID ); } catch (err) { - this.logger.error( - `Could not find application with id: ${multicastDto.applicationID}` - ); + this.logger.error(`Could not find application with id: ${multicastDto.applicationID}`); throw new BadRequestException(ErrorCodes.IdDoesNotExists); } @@ -371,149 +332,105 @@ export class MulticastService extends GenericChirpstackConfigurationService { private async mapMulticastDtoToChirpStackMulticast( multicastDto: CreateMulticastDto | UpdateMulticastDto, - multicast: CreateMulticastChirpStackDto, device: LoRaWANDevice - ): Promise { - multicast.multicastGroup.name = multicastDto.name; - multicast.multicastGroup.mcAddr = multicastDto.mcAddr; - multicast.multicastGroup.mcAppSKey = multicastDto.mcAppSKey; - multicast.multicastGroup.mcNwkSKey = multicastDto.mcNwkSKey; - multicast.multicastGroup.dr = multicastDto.dr; - multicast.multicastGroup.fCnt = multicastDto.fCnt; - multicast.multicastGroup.frequency = multicastDto.frequency; - multicast.multicastGroup.groupType = multicastDto.groupType; + ): Promise { + const multicast = new MulticastGroup(); + + multicast.setName(multicastDto.name); + multicast.setMcAddr(multicastDto.mcAddr); + multicast.setMcAppSKey(multicastDto.mcAppSKey); + multicast.setMcNwkSKey(multicastDto.mcNwkSKey); + multicast.setDr(multicastDto.dr); + multicast.setFCnt(multicastDto.fCnt); + multicast.setFrequency(multicastDto.frequency); + multicast.setGroupType(MulticastGroupType.CLASS_C); if (!!device) { // if devices is included, at this point we know that devices is validated. Therefore we can use appID - multicast.multicastGroup.applicationID = device.chirpstackApplicationId.toString(); + multicast.setApplicationId(device.chirpstackApplicationId.toString()); } else { // used for update when all devices are removed - multicast.multicastGroup.applicationID = - multicast.multicastGroup.applicationID; + multicast.setApplicationId(multicast.getApplicationId()); } return multicast; } private handlePossibleError( - result: AxiosResponse, - dto: - | CreateMulticastDto - | UpdateMulticastDto - | CreateChirpstackMulticastQueueItemDto + result: IdResponse, + dto: CreateMulticastDto | UpdateMulticastDto | CreateChirpstackMulticastQueueItemDto ): void { - if (result.status !== 200) { - this.logger.error( - `Error from Chirpstack: '${JSON.stringify( - dto - )}', got response: ${JSON.stringify(result.data)}` - ); + if (!result.id) { + this.logger.error(`Error from Chirpstack: '${JSON.stringify(dto)}', failed`); throw new BadRequestException({ success: false, - error: result.data, + error: result.id, }); } } - private async updateDevices( - removed: IoTDevice[], - added: IoTDevice[], - chirpstackMulticastID: string - ) { - removed.forEach(async device => { - // if the removed devices is a lorawan, then delete from chirpstack - if (device.type === IoTDeviceType.LoRaWAN) { - let lorawanDevice: LoRaWANDevice = new LoRaWANDevice(); - lorawanDevice = device as LoRaWANDevice; - return await this.delete( - this.multicastGroupUrl + - "/" + - chirpstackMulticastID + - "/" + - "devices", - lorawanDevice.deviceEUI - ); - } - }); - added.forEach(async device => { - if (device.type === IoTDeviceType.LoRaWAN) { - let lorawanDevice: LoRaWANDevice = new LoRaWANDevice(); - lorawanDevice = device as LoRaWANDevice; - const addDevice = new AddDeviceToMulticastDto(); - addDevice.devEUI = lorawanDevice.deviceEUI; - addDevice.multicastGroupID = chirpstackMulticastID; - - await this.post( - this.multicastGroupUrl + - "/" + - chirpstackMulticastID + - "/" + - "devices", - addDevice - ); - } - }); - } - private async addDevices( multicastDto: CreateMulticastDto | UpdateMulticastDto, - chirpstackMulticastID: AxiosResponse // the id returned from chirpstack when the multicast is created in chirpstack. + chirpstackMulticastID: IdResponse // the id returned from chirpstack when the multicast is created in chirpstack. ) { multicastDto.iotDevices.forEach(async device => { if (device.type === IoTDeviceType.LoRaWAN) { let lorawanDevice: LoRaWANDevice = new LoRaWANDevice(); lorawanDevice = device as LoRaWANDevice; // cast to LoRaWANDevice since it has DeviceEUI - const addDevice = new AddDeviceToMulticastDto(); - addDevice.devEUI = lorawanDevice.deviceEUI; - addDevice.multicastGroupID = chirpstackMulticastID.data.id; - - await this.post( - // post call to chirpstack - this.multicastGroupUrl + - "/" + - chirpstackMulticastID.data.id + - "/" + - "devices", - addDevice + const req = new AddDeviceToMulticastGroupRequest(); + req.setDevEui(lorawanDevice.deviceEUI); + req.setMulticastGroupId(chirpstackMulticastID.id); + + await this.addDeviceToMulticast( + this.multicastGroupUrl + "/" + chirpstackMulticastID.id + "/" + "devices", + this.multicastServiceClient, + req ); } }); } - private compareDevices( - oldMulticast: Multicast, - newMulticast: UpdateMulticastDto, - added: IoTDevice[], - removed: IoTDevice[] - ) { - oldMulticast.iotDevices.forEach(dbDevice => { - // if a device in the old multicast is not in the new one, then delete - if ( - newMulticast.iotDevices.findIndex(device => device.id === dbDevice.id) === - -1 - ) { - removed.push(dbDevice); - } + async addDeviceToMulticast( + logName: string, + client: MulticastGroupServiceClient, + request: AddDeviceToMulticastGroupRequest + ): Promise { + const metaData = this.makeMetadataHeader(); + const createPromise = new Promise((resolve, reject) => { + client.addDevice(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.logger.debug(`post:${logName} success`); + resolve(resp.toObject()); + } + }); }); + try { + return await createPromise; + } catch (err) { + this.logger.error(`POST ${logName} got error: ${err}`); + throw new BadRequestException(); + } + } - newMulticast.iotDevices.forEach(frontendDevice => { - // if a device in the new multicast is not in the old one, then add - if ( - oldMulticast.iotDevices.findIndex( - device => device.id === frontendDevice.id - ) === -1 - ) { - added.push(frontendDevice); - } + async getDownlinkQueue(multicastID: string): Promise { + const req = new ListMulticastGroupQueueRequest(); + req.setMulticastGroupId(multicastID); + const res = await this.getQueue(this.multicastServiceClient, req); + + const queueItems: MulticastQueueItem[] = res.getItemsList().map(queueItem => { + return { + multicastGroupId: queueItem.getMulticastGroupId(), + fCnt: queueItem.getFCnt(), + fPort: queueItem.getFPort(), + data: queueItem.getData_asB64(), + }; }); - } - async getDownlinkQueue( - multicastID: string - ): Promise { - const res = await this.get( - `multicast-groups/${multicastID}/queue` - ); - return res; + const responseDto: MulticastDownlinkQueueResponseDto = { + deviceQueueItems: queueItems, + }; + return responseDto; } public async createDownlink( @@ -539,25 +456,28 @@ export class MulticastService extends GenericChirpstackConfigurationService { ): Promise { await this.deleteDownlinkQueue(dto.multicastQueueItem.multicastGroupID); try { - const res = await this.post( - `multicast-groups/${dto.multicastQueueItem.multicastGroupID}/queue`, - dto - ); - return res.data; + const req = new EnqueueMulticastGroupQueueItemRequest(); + const queueItem = new MulticastGroupQueueItem(); + queueItem.setData(dto.multicastQueueItem.data); + queueItem.setMulticastGroupId(dto.multicastQueueItem.multicastGroupID); + queueItem.setFPort(dto.multicastQueueItem.fPort); + req.setQueueItem(queueItem); + + const res = await this.postDownlink(this.multicastServiceClient, req); + return res; } catch (err) { - const fcntError = - "enqueue downlink payload error: get next downlink fcnt for deveui error"; + const fcntError = "enqueue downlink payload error: get next downlink fcnt for deveui error"; if (err?.response?.data?.error?.startsWith(fcntError)) { - throw new BadRequestException( - ErrorCodes.DeviceIsNotActivatedInChirpstack - ); + throw new BadRequestException(ErrorCodes.DeviceIsNotActivatedInChirpstack); } throw err; } } async deleteDownlinkQueue(multicastID: string): Promise { - await this.delete(`multicast-groups/${multicastID}/queue`); + const req = new FlushMulticastGroupQueueRequest(); + req.setMulticastGroupId(multicastID); + await this.flushQueue(this.multicastServiceClient, req); } private hexBytesToBase64(hexBytes: string): string { @@ -569,26 +489,17 @@ export class MulticastService extends GenericChirpstackConfigurationService { updateMulticastDto: UpdateMulticastDto, mappedMulticast: Multicast ): Promise { - const chirpStackMulticast = new CreateMulticastChirpStackDto(); - chirpStackMulticast.multicastGroup = new ChirpstackMulticastContentsDto(); - if (await this.checkForDifferentAppID(lorawanDevices)) { - await this.createMulticastInChirpstack( - updateMulticastDto, - chirpStackMulticast, - lorawanDevices, - mappedMulticast - ); + await this.createMulticastInChirpstack(updateMulticastDto, lorawanDevices, mappedMulticast); } else { - throw new BadRequestException(ErrorCodes.DifferentServiceprofile); + throw new BadRequestException(ErrorCodes.InvalidPost); } } private async updateLogic( existingMulticast: Multicast, lorawanDevices: LoRaWANDevice[], - updateMulticastDto: UpdateMulticastDto, - oldMulticast: Multicast + updateMulticastDto: UpdateMulticastDto ): Promise { const existingChirpStackMulticast = await this.getChirpstackMulticast( existingMulticast.lorawanMulticastDefinition.chirpstackGroupId @@ -601,17 +512,76 @@ export class MulticastService extends GenericChirpstackConfigurationService { lorawanDevices ) ) { - await this.updateMulticastToChirpstack( - updateMulticastDto, - existingChirpStackMulticast, - lorawanDevices, - oldMulticast - ); + await this.updateMulticastToChirpstack(updateMulticastDto, existingChirpStackMulticast, lorawanDevices); } else { - throw new BadRequestException(ErrorCodes.NewDevicesWrongServiceProfile); + throw new BadRequestException(ErrorCodes.InvalidPost); } } else { - throw new BadRequestException(ErrorCodes.DifferentServiceprofile); + throw new BadRequestException(ErrorCodes.DifferentServiceProfile); + } + } + + async getQueue( + client: MulticastGroupServiceClient, + request: ListMulticastGroupQueueRequest + ): Promise { + const metaData = this.makeMetadataHeader(); + const getPromise = new Promise((resolve, reject) => { + client.listQueue(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.logger.debug(`get from Queue success`); + resolve(resp); + } + }); + }); + try { + return await getPromise; + } catch (err) { + throw new NotFoundException(); + } + } + + async flushQueue(client: MulticastGroupServiceClient, request: FlushMulticastGroupQueueRequest): Promise { + const metaData = this.makeMetadataHeader(); + const getPromise = new Promise((resolve, reject) => { + client.flushQueue(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.logger.debug(`Delete queue success`); + resolve(resp); + } + }); + }); + try { + return await getPromise; + } catch (err) { + this.logger.error(`DELETE queue got error: ${err}`); + throw new BadRequestException(); + } + } + async postDownlink( + client: MulticastGroupServiceClient, + request: EnqueueMulticastGroupQueueItemRequest + ): Promise { + const metaData = this.makeMetadataHeader(); + const createPromise = new Promise((resolve, reject) => { + client.enqueue(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.logger.debug(`post downlink success`); + resolve(resp.toObject()); + } + }); + }); + try { + return await createPromise; + } catch (err) { + this.logger.error(`POST downlink got error: ${err}`); + throw new BadRequestException(); } } } diff --git a/src/services/chirpstack/network-server.service.ts b/src/services/chirpstack/network-server.service.ts deleted file mode 100644 index 02cce160..00000000 --- a/src/services/chirpstack/network-server.service.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Injectable, InternalServerErrorException, OnModuleInit } from "@nestjs/common"; -import { AxiosResponse } from "axios"; - -import { CreateNetworkServerDto } from "@dto/chirpstack/create-network-server.dto"; -import { ListAllNetworkServerResponseDto } from "@dto/chirpstack/list-all-network-server-response.dto"; -import { NetworkServerDto } from "@dto/chirpstack/network-server.dto"; - -import { GenericChirpstackConfigurationService } from "./generic-chirpstack-configuration.service"; -import { ListAllAdrAlgorithmsResponseDto } from "@dto/chirpstack/list-all-adr-algorithms-response.dto"; - -@Injectable() -export class ChirpstackSetupNetworkServerService - extends GenericChirpstackConfigurationService - implements OnModuleInit { - networkServerName = "OS2iot"; - - async onModuleInit(): Promise { - await this.bootstrapChirpstackNetworkServerConfiguration(); - } - - public async bootstrapChirpstackNetworkServerConfiguration(): Promise { - const networkServers = await this.getNetworkServers(100, 0); - const alreadyCreated = networkServers.result.some(networkServer => { - return ( - networkServer.name.toLocaleLowerCase() == - this.networkServerName.toLocaleLowerCase() - ); - }); - - if (!alreadyCreated) { - try { - await this.postNetworkServer(this.setupNetworkServerData()); - } catch (error) { - throw new InternalServerErrorException(error?.result?.data); - } - } - } - - public async postNetworkServer(data: CreateNetworkServerDto): Promise { - return await this.post("network-servers", data); - } - - public async putNetworkServer( - data: CreateNetworkServerDto, - id: number - ): Promise { - return await this.put("network-servers", data, id.toString()); - } - public async deleteNetworkServer(id: number): Promise { - return await this.delete("network-servers", id.toString()); - } - - public async getNetworkServerCount(): Promise { - const result: ListAllNetworkServerResponseDto = await this.getNetworkServers( - 1000, - 0 - ); - return result.totalCount; - } - - public setupNetworkServerData(): CreateNetworkServerDto { - const networkServerDto: NetworkServerDto = { - name: this.networkServerName, - server: this.networkServer, - }; - const createNetworkServerDto: CreateNetworkServerDto = { - networkServer: networkServerDto, - }; - - return createNetworkServerDto; - } - - public async getAdrAlgorithmsForDefaultNetworkServer(): Promise { - const networkServerId = await this.getDefaultNetworkServerId(); - return await this.get(`network-servers/${networkServerId}/adr-algorithms`); - } -} diff --git a/src/services/chirpstack/service-profile.service.ts b/src/services/chirpstack/service-profile.service.ts deleted file mode 100644 index cfdf9a50..00000000 --- a/src/services/chirpstack/service-profile.service.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { ConflictException, Injectable } from "@nestjs/common"; -import { AxiosResponse } from "axios"; - -import { CreateServiceProfileDto } from "@dto/chirpstack/create-service-profile.dto"; -import { ListAllServiceProfilesResponseDto } from "@dto/chirpstack/list-all-service-profiles-response.dto"; -import { UpdateServiceProfileDto } from "@dto/chirpstack/update-service-profile.dto"; - -import { GenericChirpstackConfigurationService } from "./generic-chirpstack-configuration.service"; -import { ChirpstackApplicationResponseDto } from "@dto/chirpstack/chirpstack-application-response.dto"; -import { ListAllChirpstackApplicationsResponseDto } from "@dto/chirpstack/list-all-applications-response.dto"; -import { ErrorCodes } from "@enum/error-codes.enum"; - -@Injectable() -export class ServiceProfileService extends GenericChirpstackConfigurationService { - public async createServiceProfile( - dto: CreateServiceProfileDto - ): Promise { - dto = await this.updateDto(dto); - const result = await this.post("service-profiles", dto); - return result; - } - - public async updateServiceProfile( - data: CreateServiceProfileDto, - id: string - ): Promise { - data = await this.updateDto(data); - return await this.put("service-profiles", data, id); - } - - public async deleteServiceProfile(id: string): Promise { - // If any devices have been made using the service profile then an application was made in chirpstack. - // We need to remove the application if it exists before deleting the service profile. - const applications = await this.get( - `applications?search=${id}&limit=100&offset=0` - ); - const applicationToDelete = applications.result.find( - x => x.name.indexOf(id) >= 0 - ); - if (applicationToDelete) { - // Check if there is any devices on the application - const deviceOnApplication = await this.get< - ListAllChirpstackApplicationsResponseDto - >(`devices?limit=10&applicationID=${applicationToDelete.id}`); - if (deviceOnApplication.totalCount > 0) { - throw new ConflictException(ErrorCodes.DeleteNotAllowedHasLoRaWANDevices); - } - - await this.delete("applications", applicationToDelete.id); - } - - return await this.delete("service-profiles", id); - } - - public async findAllServiceProfiles( - limit?: number, - offset?: number - ): Promise { - const res = await this.getAllWithPagination( - "service-profiles", - limit, - offset - ); - - return res; - } - - public async findOneServiceProfileById(id: string): Promise { - const result: CreateServiceProfileDto = await this.getOneById( - "service-profiles", - id - ); - return result; - } - - private async updateDto( - dto: CreateServiceProfileDto | UpdateServiceProfileDto - ): Promise> { - // Chirpstack requires 'gatewayProfileID' to be set (with value or null) - if (!dto?.serviceProfile?.id) { - dto.serviceProfile.id = null; - } - - dto.serviceProfile.networkServerID = await this.getDefaultNetworkServerId(); - dto.serviceProfile.organizationID = await this.getDefaultOrganizationId(); - - return dto; - } -} diff --git a/src/services/csv-generator.service.ts b/src/services/csv-generator.service.ts index a8e90aa7..50cd4d1f 100644 --- a/src/services/csv-generator.service.ts +++ b/src/services/csv-generator.service.ts @@ -1,9 +1,6 @@ import { Injectable } from "@nestjs/common"; import { IoTDevice } from "@entities/iot-device.entity"; -import { IoTDeviceType } from "@enum/device-type.enum"; -import { AuthenticationType } from "@enum/authentication-type.enum"; import { EncryptionHelperService } from "@services/encryption-helper.service"; -import { ActivationType } from "@enum/lorawan-activation-type.enum"; @Injectable() export class CsvGeneratorService { @@ -65,7 +62,6 @@ export class CsvGeneratorService { `${this.base64Encode(deviceCertificate) ?? ""},` + `${this.base64Encode(deviceCertificateKey) ?? ""},` + `${lorawanSettings?.devEUI ?? ""},` + - `${lorawanSettings?.serviceProfileID ?? ""},` + `${lorawanSettings?.deviceProfileID ?? ""},` + `${lorawanSettings?.skipFCntCheck ?? ""},` + `${lorawanSettings?.activationType ?? ""},` + @@ -102,7 +98,6 @@ const csvFields = [ "deviceCertificate", "deviceCertificateKey", "devEUI", - "serviceProfileID", "deviceProfileID", "skipFCntCheck", "activationType", diff --git a/src/services/data-management/chirpstack-mqtt-listener.service.ts b/src/services/data-management/chirpstack-mqtt-listener.service.ts index b04147c0..15fb57c1 100644 --- a/src/services/data-management/chirpstack-mqtt-listener.service.ts +++ b/src/services/data-management/chirpstack-mqtt-listener.service.ts @@ -16,10 +16,7 @@ import * as Protobuf from "protobufjs"; @Injectable() export class ChirpstackMQTTListenerService implements OnApplicationBootstrap { - constructor( - private receiveDataService: ReceiveDataService, - private iotDeviceService: IoTDeviceService - ) { + constructor(private receiveDataService: ReceiveDataService, private iotDeviceService: IoTDeviceService) { const connStateFullTemplate = Protobuf.loadSync(ChirpstackStateTemplatePath); this.connStateType = connStateFullTemplate.lookupType("ConnState"); } @@ -27,17 +24,14 @@ export class ChirpstackMQTTListenerService implements OnApplicationBootstrap { private readonly logger = new Logger(ChirpstackMQTTListenerService.name); private readonly connStateType: Protobuf.Type; - MQTT_URL = `mqtt://${process.env.CS_MQTT_HOSTNAME || "localhost"}:${ - process.env.CS_MQTT_PORT || "1883" - }`; + MQTT_URL = `mqtt://${process.env.CS_MQTT_HOSTNAME || "localhost"}:${process.env.CS_MQTT_PORT || "1883"}`; client: Client; private readonly CHIRPSTACK_MQTT_DEVICE_DATA_PREFIX = "application/"; private readonly CHIRPSTACK_MQTT_DEVICE_DATA_TOPIC = this.CHIRPSTACK_MQTT_DEVICE_DATA_PREFIX + "+/device/+/event/up"; private readonly CHIRPSTACK_MQTT_GATEWAY_PREFIX = "gateway/"; - private readonly CHIRPSTACK_MQTT_GATEWAY_TOPIC = - this.CHIRPSTACK_MQTT_GATEWAY_PREFIX + "+/state/conn"; + private readonly CHIRPSTACK_MQTT_GATEWAY_TOPIC = this.CHIRPSTACK_MQTT_GATEWAY_PREFIX + "+/state/conn"; public async onApplicationBootstrap(): Promise { this.logger.debug("Pre-init"); @@ -51,9 +45,7 @@ export class ChirpstackMQTTListenerService implements OnApplicationBootstrap { this.client.subscribe(this.CHIRPSTACK_MQTT_GATEWAY_TOPIC); this.client.on("message", async (topic, message) => { - this.logger.debug( - `Received MQTT - Topic: '${topic}' - message: '${message}'` - ); + this.logger.debug(`Received MQTT - Topic: '${topic}' - message: '${message}'`); if (topic.startsWith(this.CHIRPSTACK_MQTT_DEVICE_DATA_PREFIX)) { await this.receiveMqttMessage(message.toString()); @@ -62,9 +54,7 @@ export class ChirpstackMQTTListenerService implements OnApplicationBootstrap { const decoded = this.connStateType.decode(message); await this.receiveMqttGatewayStatusMessage(decoded.toJSON()); } catch (error) { - this.logger.error( - `Gateway status data could not be processed. Error: ${error}` - ); + this.logger.error(`Gateway status data could not be processed. Error: ${error}`); } } else { this.logger.warn("Unrecognized MQTT topic " + topic); @@ -76,13 +66,11 @@ export class ChirpstackMQTTListenerService implements OnApplicationBootstrap { async receiveMqttMessage(message: string): Promise { const dto: ChirpstackMQTTMessageDto = JSON.parse(message); - const iotDevice = await this.iotDeviceService.findLoRaWANDeviceByDeviceEUI( - dto.devEUI - ); + const iotDevice = await this.iotDeviceService.findLoRaWANDeviceByDeviceEUI(dto.deviceInfo.devEui); if (!iotDevice) { this.logger.warn( - `Chirpstack sent MQTT message for devEUI ${dto.devEUI}, but that's not registered in OS2IoT` + `Chirpstack sent MQTT message for devEUI ${dto.deviceInfo.devEui}, but that's not registered in OS2IoT` ); return; } @@ -94,27 +82,19 @@ export class ChirpstackMQTTListenerService implements OnApplicationBootstrap { ); } - async receiveMqttGatewayStatusMessage( - message: Record - ): Promise { + async receiveMqttGatewayStatusMessage(message: Record): Promise { if ( message && - hasProps( - message, - nameof("gatewayId") - ) && + hasProps(message, nameof("gatewayId")) && typeof message.gatewayId === "string" ) { const dto: ChirpstackMQTTConnectionStateMessageDto = { - gatewayId: Buffer.from(message.gatewayId, "base64").toString("hex"), + gatewayId: message.gatewayId, isOnline: message.state === "ONLINE", }; const jsonDto = JSON.stringify(dto); - await this.receiveDataService.sendRawGatewayStateToKafka( - dto.gatewayId, - jsonDto - ); + await this.receiveDataService.sendRawGatewayStateToKafka(dto.gatewayId, jsonDto); } else { this.logger.error( `Gateway status message is not properly formatted. Gateway id, if any, is ${message?.id}` diff --git a/src/services/data-management/device-integration-persistence.service.ts b/src/services/data-management/device-integration-persistence.service.ts index 9740e292..fba9216c 100644 --- a/src/services/data-management/device-integration-persistence.service.ts +++ b/src/services/data-management/device-integration-persistence.service.ts @@ -174,7 +174,7 @@ export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { if (isValidLoRaWANPayload(payload)) { // There's signal info for each nearby gateway. Retrieve the strongest signal strength const rssi = Math.max(...payload.rxInfo.map(info => info.rssi)); - const snr = Math.max(...payload.rxInfo.map(info => info.loRaSNR)); + const snr = Math.max(...payload.rxInfo.map(info => info.snr)); message.rssi = Number.isInteger(rssi) ? rssi : message.rssi; message.snr = Number.isInteger(snr) ? snr : message.snr; } else { diff --git a/src/services/data-management/gateway-persistence.service.ts b/src/services/data-management/gateway-persistence.service.ts index bd9ca523..d862db5e 100644 --- a/src/services/data-management/gateway-persistence.service.ts +++ b/src/services/data-management/gateway-persistence.service.ts @@ -36,28 +36,20 @@ export class GatewayPersistenceService extends AbstractKafkaConsumer { async rawRequestListener(payload: KafkaPayload): Promise { this.logger.debug(`RAW_GATEWAY_STATE: '${JSON.stringify(payload)}'`); const dto = payload.body as RawGatewayStateDto; - const messageState = (dto.rawPayload as unknown) as ChirpstackMQTTConnectionStateMessageDto; + const messageState = dto.rawPayload as unknown as ChirpstackMQTTConnectionStateMessageDto; const statusHistory = this.mapDtoToEntity(dto, messageState); await this.gatewayStatusHistoryRepository.save(statusHistory); - // Clean up old statuses - await this.deleteStatusHistoriesSinceLastHour( - statusHistory.timestamp, - dto.gatewayId - ); + // Clean up old statuses + await this.deleteStatusHistoriesSinceLastHour(statusHistory.timestamp, dto.gatewayId); await this.deleteOldStatusHistories(dto.gatewayId); } - private mapDtoToEntity( - dto: RawGatewayStateDto, - messageState: ChirpstackMQTTConnectionStateMessageDto - ) { + private mapDtoToEntity(dto: RawGatewayStateDto, messageState: ChirpstackMQTTConnectionStateMessageDto) { const statusHistory = new GatewayStatusHistory(); statusHistory.mac = dto.gatewayId; - statusHistory.timestamp = dto.unixTimestamp - ? new Date(dto.unixTimestamp) - : new Date(); + statusHistory.timestamp = dto.unixTimestamp ? new Date(dto.unixTimestamp) : new Date(); statusHistory.wasOnline = !!messageState?.isOnline; return statusHistory; } @@ -68,10 +60,7 @@ export class GatewayPersistenceService extends AbstractKafkaConsumer { * @param latestMessageTime * @param gatewayId */ - private async deleteStatusHistoriesSinceLastHour( - latestMessageTime: Date, - gatewayId: string - ): Promise { + private async deleteStatusHistoriesSinceLastHour(latestMessageTime: Date, gatewayId: string): Promise { const lastHour = subtractHours(latestMessageTime); // Find the oldest items since the last hour const oldestToDelete = await this.gatewayStatusHistoryRepository.find({ @@ -89,9 +78,7 @@ export class GatewayPersistenceService extends AbstractKafkaConsumer { return; } - const result = await this.gatewayStatusHistoryRepository.delete( - oldestToDelete.map(old => old.id) - ); + const result = await this.gatewayStatusHistoryRepository.delete(oldestToDelete.map(old => old.id)); this.logger.debug(`Deleted: ${result.affected} rows from gateway_status_history`); } @@ -115,9 +102,7 @@ export class GatewayPersistenceService extends AbstractKafkaConsumer { return; } - const result = await this.gatewayStatusHistoryRepository.delete( - oldestToDelete.map(old => old.id) - ); + const result = await this.gatewayStatusHistoryRepository.delete(oldestToDelete.map(old => old.id)); this.logger.debug(`Deleted: ${result.affected} rows from gateway_status_history`); } diff --git a/src/services/data-management/search.service.ts b/src/services/data-management/search.service.ts index 1a8a9ea5..cfd2704b 100644 --- a/src/services/data-management/search.service.ts +++ b/src/services/data-management/search.service.ts @@ -1,9 +1,12 @@ -import { ListAllGatewaysResponseDto } from "@dto/chirpstack/list-all-gateways.dto"; +import { GatewayServiceClient } from "@chirpstack/chirpstack-api/api/gateway_grpc_pb"; +import { ListGatewaysRequest, ListGatewaysResponse } from "@chirpstack/chirpstack-api/api/gateway_pb"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { ListAllSearchResultsResponseDto } from "@dto/list-all-search-results-response.dto"; import { SearchResultDto, SearchResultType } from "@dto/search-result.dto"; import { Application } from "@entities/application.entity"; import { IoTDevice } from "@entities/iot-device.entity"; +import { credentials } from "@grpc/grpc-js"; +import { timestampToDate } from "@helpers/date.helper"; import { Injectable, Logger } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { ChirpstackGatewayService } from "@services/chirpstack/chirpstack-gateway.service"; @@ -85,19 +88,26 @@ export class SearchService { } private async findGateways(trimmedQuery: string): Promise { + const gatewayClient = new GatewayServiceClient(this.gatewayService.baseUrlGRPC, credentials.createInsecure()); const escapedQuery = encodeURI(trimmedQuery); - const gateways = await this.gatewayService.getAllWithPagination( - `gateways?search=${escapedQuery}`, + const req = new ListGatewaysRequest(); + + req.setSearch(escapedQuery); + + const gateways = await this.gatewayService.getAllWithPagination( + `gateways`, + gatewayClient, + req, 1000, 0 ); const mapped = await Promise.all( - gateways.result.map(async x => { - const createdAt = new Date(x.createdAt); - const updatedAt = new Date(x.updatedAt); + gateways.resultList.map(async x => { + const createdAt = timestampToDate(x.createdAt); + const updatedAt = timestampToDate(x.updatedAt); - const resultDto = new SearchResultDto(x.name, x.id, createdAt, updatedAt, x.gatewayId); + const resultDto = new SearchResultDto(x.name, x.gatewayId, createdAt, updatedAt, x.gatewayId); const detailedInfo = await this.gatewayService.getOne(x.gatewayId); resultDto.organizationId = detailedInfo.gateway.organizationId; @@ -197,7 +207,6 @@ export class SearchService { return this.iotDeviceRepository.createQueryBuilder("device"); } - // eslint-disable-next-line max-lines-per-function private async applySecuityAndSelect( req: AuthenticatedRequest, qb: SelectQueryBuilder, diff --git a/src/services/device-management/application.service.ts b/src/services/device-management/application.service.ts index 27d808d5..acf6b45c 100644 --- a/src/services/device-management/application.service.ts +++ b/src/services/device-management/application.service.ts @@ -1,5 +1,4 @@ import { CreateApplicationDto } from "@dto/create-application.dto"; -import { CreateLoRaWANSettingsDto } from "@dto/create-lorawan-settings.dto"; import { ListAllApplicationsResponseDto } from "@dto/list-all-applications-response.dto"; import { ListAllApplicationsDto } from "@dto/list-all-applications.dto"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; @@ -16,15 +15,17 @@ import { ApplicationDeviceTypes, ApplicationDeviceTypeUnion, IoTDeviceType } fro import { ErrorCodes } from "@enum/error-codes.enum"; import { findValuesInRecord } from "@helpers/record.helper"; import { nameof } from "@helpers/type-helper"; -import { ConflictException, forwardRef, Inject, Injectable } from "@nestjs/common"; +import { BadRequestException, ConflictException, forwardRef, Inject, Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { ChirpstackDeviceService } from "@services/chirpstack/chirpstack-device.service"; import { OrganizationService } from "@services/user-management/organization.service"; import { PermissionService } from "@services/user-management/permission.service"; import { DeleteResult, In, Repository } from "typeorm"; -import { MulticastService } from "./multicast.service"; import { DataTargetService } from "@services/data-targets/data-target.service"; import { DataTargetType } from "@enum/data-target-type.enum"; +import { MulticastService } from "@services/chirpstack/multicast.service"; +import { ApplicationChirpstackService } from "@services/chirpstack/chirpstack-application.service"; +import { CreateLoRaWANSettingsDto } from "@dto/create-lorawan-settings.dto"; @Injectable() export class ApplicationService { @@ -40,7 +41,8 @@ export class ApplicationService { @Inject(forwardRef(() => PermissionService)) private permissionService: PermissionService, @Inject(forwardRef(() => DataTargetService)) - private dataTargetService: DataTargetService + private dataTargetService: DataTargetService, + private chirpstackApplicationService: ApplicationChirpstackService ) {} async findAndCountInList( @@ -202,11 +204,11 @@ export class ApplicationService { } private async matchWithChirpstackStatusData(app: Application) { - const allFromChirpstack = await this.chirpstackDeviceService.getAllDevicesStatus(); + const chirpstackDevices = await this.chirpstackDeviceService.getAllDevicesStatus(app); app.iotDevices.forEach(x => { if (x.type === IoTDeviceType.LoRaWAN) { const loraDevice = x as LoRaWANDeviceWithChirpstackDataDto; - const matchingDevice = allFromChirpstack.result.find(cs => cs.devEUI === loraDevice.deviceEUI); + const matchingDevice = chirpstackDevices.result.find(cs => cs.devEUI === loraDevice.deviceEUI); if (matchingDevice) { loraDevice.lorawanSettings = new CreateLoRaWANSettingsDto(); loraDevice.lorawanSettings.deviceStatusBattery = matchingDevice.deviceStatusBattery; @@ -233,11 +235,18 @@ export class ApplicationService { mappedApplication.createdBy = userId; mappedApplication.updatedBy = userId; - const app = await this.applicationRepository.save(mappedApplication); + try { + mappedApplication.chirpstackId = await this.chirpstackApplicationService.createChirpstackApplication( + { application: { description: createApplicationDto.description, name: createApplicationDto.name } } + ); + const app = await this.applicationRepository.save(mappedApplication); - await this.permissionService.autoAddPermissionsToApplication(app); + await this.permissionService.autoAddPermissionsToApplication(app); - return app; + return app; + } catch (e) { + throw new BadRequestException(ErrorCodes.InvalidPost); + } } async update(id: number, updateApplicationDto: UpdateApplicationDto, userId: number): Promise { @@ -257,6 +266,8 @@ export class ApplicationService { userId ); + await this.chirpstackApplicationService.updateApplication(mappedApplication); + mappedApplication.updatedBy = userId; return this.applicationRepository.save(mappedApplication, {}); } @@ -297,6 +308,10 @@ export class ApplicationService { dbMulticast.lorawanMulticastDefinition.chirpstackGroupId ); } + if (application.chirpstackId) { + await this.chirpstackApplicationService.deleteApplication(application.chirpstackId); + } + return this.applicationRepository.delete(id); } @@ -405,11 +420,9 @@ export class ApplicationService { const loraDevices = data.filter( device => device.type === IoTDeviceType.LoRaWAN ) as LoRaWANDeviceWithChirpstackDataDto[]; - const applications = await this.chirpstackDeviceService.getLoRaWANApplications(loraDevices); - const loraApplications = applications.map(app => app.application); for (const device of loraDevices) { - await this.chirpstackDeviceService.enrichLoRaWANDevice(device, loraApplications); + await this.chirpstackDeviceService.enrichLoRaWANDevice(device); } return { diff --git a/src/services/device-management/iot-device-downlink.service.ts b/src/services/device-management/iot-device-downlink.service.ts index ba626ac0..355062ab 100644 --- a/src/services/device-management/iot-device-downlink.service.ts +++ b/src/services/device-management/iot-device-downlink.service.ts @@ -4,12 +4,7 @@ import { LoRaWANDevice } from "@entities/lorawan-device.entity"; import { SigFoxDevice } from "@entities/sigfox-device.entity"; import { IoTDeviceType } from "@enum/device-type.enum"; import { ErrorCodes } from "@enum/error-codes.enum"; -import { - BadRequestException, - Injectable, - InternalServerErrorException, - Logger, -} from "@nestjs/common"; +import { BadRequestException, Injectable, InternalServerErrorException, Logger } from "@nestjs/common"; import { SigFoxApiDeviceTypeService } from "@services/sigfox/sigfox-api-device-type.service"; import { SigFoxGroupService } from "@services/sigfox/sigfox-group.service"; import { IoTDeviceService } from "@services/device-management/iot-device.service"; @@ -18,6 +13,7 @@ import { CreateChirpstackDeviceQueueItemResponse, } from "@dto/chirpstack/create-chirpstack-device-queue-item.dto"; import { ChirpstackDeviceService } from "@services/chirpstack/chirpstack-device.service"; +import { IdResponse } from "@interfaces/chirpstack-id-response.interface"; @Injectable() export class IoTDeviceDownlinkService { @@ -30,14 +26,11 @@ export class IoTDeviceDownlinkService { private readonly logger = new Logger(IoTDeviceDownlinkService.name); private readonly SIGFOX_DOWNLINK_LENGTH_EXACT = 16; - async createDownlink( - dto: CreateIoTDeviceDownlinkDto, - device: IoTDevice - ): Promise { - if (device.type == IoTDeviceType.LoRaWAN) { + async createDownlink(dto: CreateIoTDeviceDownlinkDto, device: IoTDevice): Promise { + if (device.type === IoTDeviceType.LoRaWAN) { const cast = device; return await this.createLoraDownlink(dto, cast); - } else if (device.type == IoTDeviceType.SigFox) { + } else if (device.type === IoTDeviceType.SigFox) { const cast = device; return await this.createSigfoxDownlink(dto, cast); } else { @@ -45,14 +38,9 @@ export class IoTDeviceDownlinkService { } } - private async createSigfoxDownlink( - dto: CreateIoTDeviceDownlinkDto, - cast: SigFoxDevice - ): Promise { + private async createSigfoxDownlink(dto: CreateIoTDeviceDownlinkDto, cast: SigFoxDevice): Promise { this.validateSigfoxPayload(dto); - this.logger.debug( - `Creating downlink for device(${cast.id}) sigfoxId(${cast.deviceId})` - ); + this.logger.debug(`Creating downlink for device(${cast.id}) sigfoxId(${cast.deviceId})`); cast.downlinkPayload = dto.data; await this.iotDeviceService.save(cast); await this.updateSigFoxDeviceTypeDownlink(cast); @@ -60,10 +48,7 @@ export class IoTDeviceDownlinkService { private async updateSigFoxDeviceTypeDownlink(cast: SigFoxDevice) { const sigfoxGroup = await this.sigfoxGroupService.findOneByGroupId(cast.groupId); - await this.sigfoxApiDeviceTypeService.addOrUpdateCallback( - sigfoxGroup, - cast.deviceTypeId - ); + await this.sigfoxApiDeviceTypeService.addOrUpdateCallback(sigfoxGroup, cast.deviceTypeId); } private validateSigfoxPayload(dto: CreateIoTDeviceDownlinkDto) { @@ -72,10 +57,7 @@ export class IoTDeviceDownlinkService { } } - private async createLoraDownlink( - dto: CreateIoTDeviceDownlinkDto, - cast: LoRaWANDevice - ): Promise { + private async createLoraDownlink(dto: CreateIoTDeviceDownlinkDto, cast: LoRaWANDevice): Promise { const csDto: CreateChirpstackDeviceQueueItemDto = { deviceQueueItem: { fPort: dto.port, @@ -92,23 +74,16 @@ export class IoTDeviceDownlinkService { } } - private handleErrorsFromChirpstack( - csDto: CreateChirpstackDeviceQueueItemDto, - err: any - ) { + private handleErrorsFromChirpstack(csDto: CreateChirpstackDeviceQueueItemDto, err: any) { this.logger.error( `Error while trying to create downlink i chirpstack. DTO: '${JSON.stringify( csDto )}'. Error: '${JSON.stringify(err?.data)}'` ); if (err.status == 400) { - throw new BadRequestException( - "Error 400 from Chirpstack" + JSON.stringify(err?.data) - ); + throw new BadRequestException("Error 400 from Chirpstack" + JSON.stringify(err?.data)); } - throw new InternalServerErrorException( - "Could not send to chirpstack, try again later." - ); + throw new InternalServerErrorException("Could not send to chirpstack, try again later."); } private hexBytesToBase64(hexBytes: string): string { diff --git a/src/services/device-management/iot-device.service.ts b/src/services/device-management/iot-device.service.ts index c498152d..ab39ad96 100644 --- a/src/services/device-management/iot-device.service.ts +++ b/src/services/device-management/iot-device.service.ts @@ -70,6 +70,7 @@ import { CsvGeneratorService } from "@services/csv-generator.service"; import * as fs from "fs"; import { caCertPath } from "@resources/resource-paths"; import { DeviceProfileService } from "@services/chirpstack/device-profile.service"; +import { ApplicationChirpstackService } from "@services/chirpstack/chirpstack-application.service"; type IoTDeviceOrSpecialized = | IoTDevice @@ -95,6 +96,7 @@ export class IoTDeviceService { private entityManager: EntityManager, private applicationService: ApplicationService, private chirpstackDeviceService: ChirpstackDeviceService, + private applicationChirpstackService: ApplicationChirpstackService, private sigfoxApiDeviceService: SigFoxApiDeviceService, private sigfoxApiDeviceTypeService: SigFoxApiDeviceTypeService, private sigfoxGroupService: SigFoxGroupService, @@ -326,7 +328,6 @@ export class IoTDeviceService { await this.loRaWANDeviceRepository.save(devices); } - async findMQTTDevice(id: number): Promise { return await this.mqttInternalBrokerDeviceRepository.findOne({ where: { id }, @@ -479,7 +480,7 @@ export class IoTDeviceService { deleteResult = await transactionManager.delete(IoTDevice, device.id); // Now we can safely perform any actions against Chirpstack - if (device.type == IoTDeviceType.LoRaWAN) { + if (device.type === IoTDeviceType.LoRaWAN) { const lorawanDevice = device as LoRaWANDevice; this.logger.debug( `Deleting LoRaWANDevice ${lorawanDevice.id} / ${lorawanDevice.deviceEUI} in Chirpstack ...` @@ -748,9 +749,7 @@ export class IoTDeviceService { } } - private async getLorawanDeviceEuis( - iotDevicesDtoMap: CreateIoTDeviceMapDto[] - ): Promise { + private async getLorawanDeviceEuis(iotDevicesDtoMap: CreateIoTDeviceMapDto[]): Promise { const iotLorawanDevices = iotDevicesDtoMap.reduce((res: string[], { iotDevice, iotDeviceDto }) => { if (iotDevice.constructor.name === LoRaWANDevice.name && iotDeviceDto.lorawanSettings) { res.push(iotDeviceDto.lorawanSettings.devEUI); @@ -926,36 +925,40 @@ export class IoTDeviceService { loraApplications: ListAllChirpstackApplicationsResponseDto = null ): Promise { lorawanDevice.deviceEUI = dto.lorawanSettings.devEUI; - if ( !isUpdate && (await this.chirpstackDeviceService.isDeviceAlreadyCreated(dto.lorawanSettings.devEUI, lorawanDeviceEuis)) ) { throw new BadRequestException(ErrorCodes.IdInvalidOrAlreadyInUse); } - try { const chirpstackDeviceDto = this.chirpstackDeviceService.makeCreateChirpstackDeviceDto( dto.lorawanSettings, dto.name ); - const applicationId = await this.chirpstackDeviceService.findOrCreateDefaultApplication( - chirpstackDeviceDto, - loraApplications + const applicationId = await this.applicationChirpstackService.findOrCreateDefaultApplication( + loraApplications, + lorawanDevice ); lorawanDevice.chirpstackApplicationId = applicationId; - chirpstackDeviceDto.device.applicationID = applicationId.toString(); - + chirpstackDeviceDto.device.applicationID = applicationId; // Create or update the LoRa device against Chirpstack API - await this.chirpstackDeviceService.createOrUpdateDevice(chirpstackDeviceDto, lorawanDeviceEuis); - lorawanDeviceEuis.push(chirpstackDeviceDto.device); - await this.doActivation(dto, isUpdate); - lorawanDevice.OTAAapplicationKey = dto.lorawanSettings.OTAAapplicationKey; - const deviceProfile = await this.deviceProfileService.findOneDeviceProfileById( - dto.lorawanSettings.deviceProfileID + const success = await this.chirpstackDeviceService.createOrUpdateDevice( + chirpstackDeviceDto, + lorawanDeviceEuis ); - lorawanDevice.deviceProfileName = deviceProfile.deviceProfile.name; + if (success) { + lorawanDeviceEuis.push(chirpstackDeviceDto.device); + await this.doActivation(dto, isUpdate); + lorawanDevice.OTAAapplicationKey = dto.lorawanSettings.OTAAapplicationKey; + const deviceProfile = await this.deviceProfileService.findOneDeviceProfileById( + dto.lorawanSettings.deviceProfileID + ); + lorawanDevice.deviceProfileName = deviceProfile.deviceProfile.name; + } else { + throw new BadRequestException(ErrorCodes.InvalidPost); + } } catch (err) { this.logger.error(err); @@ -963,7 +966,6 @@ export class IoTDeviceService { if (err?.response?.data?.error == "object already exists") { throw new BadRequestException(ErrorCodes.NameInvalidOrAlreadyInUse); } - throw err; } return lorawanDevice; @@ -973,8 +975,8 @@ export class IoTDeviceService { if (dto.lorawanSettings.activationType == ActivationType.OTAA) { // OTAA Activate if key is provided await this.doActivationByOTAA(dto, isUpdate); - } else if (dto.lorawanSettings.activationType == ActivationType.ABP) { - await this.doActivationByABP(dto, isUpdate); + } else if (dto.lorawanSettings.activationType === ActivationType.ABP) { + await this.doActivationByABP(dto); } } @@ -990,7 +992,7 @@ export class IoTDeviceService { } } - private async doActivationByABP(dto: CreateIoTDeviceDto, isUpdate: boolean) { + private async doActivationByABP(dto: CreateIoTDeviceDto) { if ( dto.lorawanSettings.devAddr && dto.lorawanSettings.fCntUp != null && @@ -1004,8 +1006,7 @@ export class IoTDeviceService { dto.lorawanSettings.fCntUp, dto.lorawanSettings.nFCntDown, dto.lorawanSettings.networkSessionKey, - dto.lorawanSettings.applicationSessionKey, - isUpdate + dto.lorawanSettings.applicationSessionKey ); } else { throw new BadRequestException(ErrorCodes.MissingABPInfo); @@ -1052,7 +1053,7 @@ export class IoTDeviceService { private async mapMQTTExternalBrokerDevice( iotDeviceDto: CreateIoTDeviceDto, cast: MQTTExternalBrokerDevice, - isUpdate: boolean = false + isUpdate = false ): Promise { const settings = iotDeviceDto.mqttExternalBrokerSettings; validateMQTTExternalBroker(settings); diff --git a/src/services/device-management/lorawan-device-database-enrich-job.ts b/src/services/device-management/lorawan-device-database-enrich-job.ts index 54d3dda9..f6db7a63 100644 --- a/src/services/device-management/lorawan-device-database-enrich-job.ts +++ b/src/services/device-management/lorawan-device-database-enrich-job.ts @@ -4,11 +4,14 @@ import { ChirpstackDeviceService } from "@services/chirpstack/chirpstack-device. import { IoTDeviceService } from "@services/device-management/iot-device.service"; import { ChirpstackGatewayService } from "@services/chirpstack/chirpstack-gateway.service"; import * as BluebirdPromise from "bluebird"; -import { ListAllGatewaysResponseDto } from "@dto/chirpstack/list-all-gateways.dto"; import { OrganizationService } from "@services/user-management/organization.service"; import { InjectRepository } from "@nestjs/typeorm"; import { Gateway } from "@entities/gateway.entity"; import { Repository } from "typeorm"; +import { timestampToDate } from "@helpers/date.helper"; +import { GetGatewayRequest, GetGatewayResponse } from "@chirpstack/chirpstack-api/api/gateway_pb"; +import { GatewayServiceClient } from "@chirpstack/chirpstack-api/api/gateway_grpc_pb"; +import { credentials } from "@grpc/grpc-js"; @Injectable() export class LorawanDeviceDatabaseEnrichJob { @@ -20,6 +23,8 @@ export class LorawanDeviceDatabaseEnrichJob { @InjectRepository(Gateway) private gatewayRepository: Repository ) {} + baseUrlGRPC = `${process.env.CHIRPSTACK_HOSTNAME || "localhost"}:${process.env.CHIRPSTACK_PORT || "8080"}`; + private gatewayClient = new GatewayServiceClient(this.baseUrlGRPC, credentials.createInsecure()); private readonly logger = new Logger(LorawanDeviceDatabaseEnrichJob.name, { timestamp: true }); @@ -27,16 +32,12 @@ export class LorawanDeviceDatabaseEnrichJob { async fetchStatusForGateway() { // Select all gateways from our database and chirpstack (Cheaper than individual calls) const gateways = await this.gatewayService.getAll(); - const chirpStackGateways = await this.gatewayService.getAllWithPagination( - "gateways", - 1000, - 0 - ); + const chirpstackGateways = await this.gatewayService.getAllGatewaysFromChirpstack(); // Setup batched fetching of status (Only for today) await BluebirdPromise.all( BluebirdPromise.map( - gateways.result, + gateways.resultList, async gateway => { try { const fromTime = new Date(); @@ -50,16 +51,16 @@ export class LorawanDeviceDatabaseEnrichJob { new Date() ); // Save that to our database - const stats = statsToday.result[0]; - const chirpstackGateway = chirpStackGateways.result.find( - g => g.id.toString() === gateway.gatewayId + const stats = statsToday[0]; + const chirpstackGateway = chirpstackGateways.resultList.find( + g => g.gatewayId === gateway.gatewayId ); await this.gatewayService.updateGatewayStats( gateway.gatewayId, stats.rxPacketsReceived, stats.txPacketsEmitted, - chirpstackGateway.lastSeenAt + chirpstackGateway.lastSeenAt ? timestampToDate(chirpstackGateway.lastSeenAt) : undefined ); } catch (err) { this.logger.error(`Gateway status fetch failed with: ${JSON.stringify(err)}`, err); @@ -99,36 +100,31 @@ export class LorawanDeviceDatabaseEnrichJob { // This is run once on startup and will create any gateways that exist in chirpstack but not our database @Timeout(10000) async importChirpstackGateways() { - // Get all chirpstack gateways - const chirpStackGateways = await this.gatewayService.getAllWithPagination( - "gateways", - 1000, - 0 - ); - + const chirpstackGateways = await this.gatewayService.getAllGatewaysFromChirpstack(); const dbGateways = await this.gatewayService.getAll(); - // Filter for gateways not existing in our database - const unknownGateways = chirpStackGateways.result.filter( - g => dbGateways.result.findIndex(dbGateway => dbGateway.gatewayId === g.id.toString()) === -1 + const unknownGateways = chirpstackGateways.resultList.filter( + g => dbGateways.resultList.findIndex(dbGateway => dbGateway.gatewayId === g.gatewayId) === -1 ); - await BluebirdPromise.all( BluebirdPromise.map( unknownGateways, async x => { try { - const gw = (await this.gatewayService.get(`gateways/${x.id}`)) as any; - const organizationId = gw.gateway.tags["internalOrganizationId"]; + const req = new GetGatewayRequest(); + req.setGatewayId(x.gatewayId); - const gateway = this.gatewayService.mapContentsDtoToGateway(gw.gateway); - gateway.id = 0; - gateway.gatewayId = gw.gateway.id; - gateway.lastSeenAt = gw.lastSeenAt; - gateway.createdAt = new Date(Date.parse(gw.createdAt)); - gateway.rxPacketsReceived = 0; - gateway.txPacketsEmitted = 0; - gateway.createdBy = gw.gateway.tags["os2iot-created-by"]; + const gwResponse = await this.gatewayService.get( + `gateways/${x.gatewayId}`, + this.gatewayClient, + req + ); + const chirpstackGateway = gwResponse.getGateway(); + const organizationId = +chirpstackGateway.getTagsMap().get("internalOrganizationId"); + const gateway = this.gatewayService.mapChirpstackGatewayToDatabaseGateway( + chirpstackGateway, + gwResponse + ); gateway.organization = await this.organizationService.findById(organizationId); await this.gatewayRepository.save(gateway); } catch (err) {