diff --git a/.gitignore b/.gitignore index c33d35450a65..15a62ca450a4 100644 --- a/.gitignore +++ b/.gitignore @@ -90,6 +90,7 @@ TestResults/* .vscode/* ./**/.vscode/* + # Node # **/node_modules/ **/cjs/ diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index de5736193ca1..9102c815c2c3 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -38,6 +38,7 @@ dependencies: '@rush-temp/eventhubs-checkpointstore-blob': file:projects/eventhubs-checkpointstore-blob.tgz '@rush-temp/identity': file:projects/identity.tgz '@rush-temp/iot-device-update': file:projects/iot-device-update.tgz + '@rush-temp/iot-modelsrepository': file:projects/iot-modelsrepository.tgz '@rush-temp/keyvault-admin': file:projects/keyvault-admin.tgz '@rush-temp/keyvault-certificates': file:projects/keyvault-certificates.tgz '@rush-temp/keyvault-common': file:projects/keyvault-common.tgz @@ -129,7 +130,7 @@ packages: '@azure/ms-rest-nodeauth': 0.9.3_debug@3.2.7 '@types/async-lock': 1.1.2 '@types/is-buffer': 2.0.0 - async-lock: 1.3.0 + async-lock: 1.2.8 buffer: 5.7.1 debug: 3.2.7 events: 3.3.0 @@ -164,7 +165,7 @@ packages: '@azure/core-auth': 1.3.0 '@azure/logger': 1.0.2 '@types/async-lock': 1.1.2 - async-lock: 1.3.0 + async-lock: 1.2.8 buffer: 5.7.1 events: 3.3.0 jssha: 3.2.0 @@ -293,7 +294,7 @@ packages: dependencies: '@azure/amqp-common': 1.0.0-preview.9 '@azure/ms-rest-nodeauth': 0.9.3_debug@3.2.7 - async-lock: 1.3.0 + async-lock: 1.2.8 debug: 3.2.7 is-buffer: 2.0.5 jssha: 2.4.2 @@ -1151,7 +1152,7 @@ packages: /@types/body-parser/1.19.0: dependencies: '@types/connect': 3.4.34 - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== @@ -1177,7 +1178,7 @@ packages: integrity: sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg== /@types/connect/3.4.34: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ== @@ -1210,7 +1211,7 @@ packages: integrity: sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg== /@types/express-serve-static-core/4.17.19: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 '@types/qs': 6.9.6 '@types/range-parser': 1.2.3 dev: false @@ -1234,20 +1235,20 @@ packages: integrity: sha512-IyNhGHu71jH1jCXTHmafuoAAdsbBON3kDh7u/UUhLmjYgN5TYB54e1R8ckTCiIevl2UuZaCsi9XRxineY5yUjw== /@types/fs-extra/8.1.1: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w== /@types/glob/7.1.3: dependencies: '@types/minimatch': 3.0.4 - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== /@types/is-buffer/2.0.0: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-0f7N/e3BAz32qDYvgB4d2cqv1DqUwvGxHkXsrucICn8la1Vb6Yl6Eg8mPScGwUiqHJeE7diXlzaK+QMA9m4Gxw== @@ -1261,7 +1262,7 @@ packages: integrity: sha1-7ihweulOEdK4J7y+UnC86n8+ce4= /@types/jsonwebtoken/8.5.1: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-rNAPdomlIUX0i0cg2+I+Q1wOUr531zHBQ+cV/28PJ39bSPKjahatZZ2LMuhiguETkCgLVzfruw/ZvNMNkKoSzw== @@ -1271,7 +1272,7 @@ packages: integrity: sha512-FLXKbwbB+4fsJECYOpIiYX2GSqSHYnkO/UnrFqlZn6crpyyOtk4LRab+G1HC7dTbT1NB7spkHecZRQGXoCWiJQ== /@types/jws/3.2.3: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-g54CHxwvaHvyJyeuZqe7VQujV9SfCXwEkboJp355INPL+kjlS3Aq153EHptaeO/Cch/NPJ1i2sHz0sDDizn7LQ== @@ -1285,7 +1286,7 @@ packages: integrity: sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== /@types/md5/2.3.0: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-556YJ7ejzxIqSSxzyGGpctuZOarNZJt/zlEkhmmDc1f/slOEANHuwu2ZX7YaZ40rMiWoxt8GvAhoDpW1cmSy6A== @@ -1311,13 +1312,13 @@ packages: integrity: sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w== /@types/mock-fs/4.10.0: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-FQ5alSzmHMmliqcL36JqIA4Yyn9jyJKvRSGV3mvPh108VFatX7naJDzSG4fnFQNZFq9dIx0Dzoe6ddflMB2Xkg== /@types/mock-require/2.0.0: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-nOgjoE5bBiDeiA+z41i95makyHUSMWQMOPocP+J67Pqx/68HAXaeWN1NFtrAYYV6LrISIZZ8vKHm/a50k0f6Sg== @@ -1327,7 +1328,7 @@ packages: integrity: sha512-DPxmjiDwubsNmguG5X4fEJ+XCyzWM3GXWsqQlvUcjJKa91IOoJUy51meDr0GkzK64qqNcq85ymLlyjoct9tInw== /@types/node-fetch/2.5.10: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 form-data: 3.0.1 dev: false resolution: @@ -1366,7 +1367,7 @@ packages: integrity: sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== /@types/resolve/1.17.1: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== @@ -1377,7 +1378,7 @@ packages: /@types/serve-static/1.13.9: dependencies: '@types/mime': 1.3.2 - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA== @@ -1393,7 +1394,7 @@ packages: integrity: sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg== /@types/stoppable/1.1.0: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-BRR23Q9CJduH7AM6mk4JRttd8XyFkb4qIPZu4mdLF+VoP+wcjIxIWIKiBbN78NBbEuynrAyMPtzOHnIp2B/JPQ== @@ -1403,7 +1404,7 @@ packages: integrity: sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A== /@types/tunnel/0.0.1: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A== @@ -1417,19 +1418,19 @@ packages: integrity: sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== /@types/ws/7.4.4: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-d/7W23JAXPodQNbOZNXvl2K+bqAQrCMwlh/nuQsPSQk6Fq0opHoPrUw43aHsvSbIiQPr8Of2hkFbnz1XBFVyZQ== /@types/xml2js/0.4.8: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false resolution: integrity: sha512-EyvT83ezOdec7BhDaEcsklWy7RSIdi6CNe95tmOAK0yx/Lm30C9K75snT3fYayK59ApC2oyW+rcHErdG05FHJA== /@types/yauzl/2.9.1: dependencies: - '@types/node': 8.10.66 + '@types/node': 10.17.60 dev: false optional: true resolution: @@ -1943,7 +1944,7 @@ packages: integrity: sha512-Xt2wLtiblGSag/zJe6vJVkgjlj+jo2C5exuaSGHuMtvkuD/MB74xyGDdgflXcOiTVzS1rXx02I02jgKHTW5BDA== /azure-iothub/1.13.1: dependencies: - '@azure/ms-rest-js': 2.3.0 + '@azure/ms-rest-js': 2.5.0 async: 2.6.3 azure-iot-amqp-base: 2.4.7 azure-iot-common: 1.12.7 @@ -1966,7 +1967,7 @@ packages: md5.js: 1.3.4 readable-stream: 2.0.6 request: 2.88.2 - underscore: 1.13.1 + underscore: 1.8.3 uuid: 3.4.0 validator: 9.4.1 xml2js: 0.2.8 @@ -1975,7 +1976,7 @@ packages: engines: node: '>= 0.8.26' resolution: - integrity: sha512-zlfRPl4js92JC6+79C2EUmNGYjSknRl8pOiHQF78zy+pbOFOHtlBF6BU/OxPeHQX3gaa6NdEZnVydFxhhndkEw== + integrity: sha512-IGLs5Xj6kO8Ii90KerQrrwuJKexLgSwYC4oLWmc11mzKe7Jt2E5IVg+ZQ8K53YWZACtVTMBNO3iGuA+4ipjJxQ== /babel-runtime/6.26.0: dependencies: core-js: 2.6.12 @@ -2582,7 +2583,7 @@ packages: integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== /debug/3.2.6: dependencies: - ms: 2.1.1 + ms: 2.1.3 deprecated: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797) dev: false resolution: @@ -2862,7 +2863,7 @@ packages: cors: 2.8.5 debug: 4.3.1 engine.io-parser: 4.0.2 - ws: 7.4.5 + ws: 7.4.4 dev: false engines: node: '>=10.0.0' @@ -6057,7 +6058,7 @@ packages: rimraf: 3.0.2 tar-fs: 2.1.1 unbzip2-stream: 1.4.3 - ws: 7.4.5 + ws: 7.4.4 dev: false engines: node: '>=10.18.1' @@ -6504,7 +6505,7 @@ packages: /rollup/1.32.1: dependencies: '@types/estree': 0.0.47 - '@types/node': 8.10.66 + '@types/node': 10.17.60 acorn: 7.4.1 dev: false hasBin: true @@ -6795,7 +6796,7 @@ packages: dependencies: '@types/cookie': 0.4.0 '@types/cors': 2.8.10 - '@types/node': 10.17.13 + '@types/node': 10.17.60 accepts: 1.3.7 base64id: 2.0.0 debug: 4.3.1 @@ -7464,6 +7465,10 @@ packages: dev: false resolution: integrity: sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== + /underscore/1.8.3: + dev: false + resolution: + integrity: sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI= /universal-user-agent/6.0.0: dev: false resolution: @@ -7716,7 +7721,7 @@ packages: utf-8-validate: optional: true resolution: - integrity: sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g== + integrity: sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== /xhr-mock/2.5.1: dependencies: global: 4.4.0 @@ -8564,7 +8569,7 @@ packages: dev: false name: '@rush-temp/confidential-ledger' resolution: - integrity: sha512-377JfE4l6ZZHgPwOzPns5aQGfcsKqoHZcSXstcyG7B6iaQF9ldkSysCxADvWg6hBa3+A7rU0ACX4zawFrJsEWQ== + integrity: sha512-/mC7zzANKf0SEh/GZYNSpyXtyk0GPdfVd80/Shbrb2su7IrqTKPjcYqbfSspN7+pSp4+BYtRwBH3gwXhTGSQnw== tarball: file:projects/confidential-ledger.tgz version: 0.0.0 file:projects/container-registry.tgz: @@ -8609,7 +8614,7 @@ packages: dev: false name: '@rush-temp/container-registry' resolution: - integrity: sha512-/kcEjsKonclJO6dZqUz95tUkXgO6rkNgS2WqBDNiq8Zmd1icNHd0czcgDj4Yg9FsrmtrNUIv3vx+PVYPYM+NUA== + integrity: sha512-p4mX6xzBEEIe41EWy81HV6pIu2ydk+5nJj+d6GAaBR7H1IGPdSl2uLjaYlWtJ9TxoYU/ASVIGHR6bNJIF02eLw== tarball: file:projects/container-registry.tgz version: 0.0.0 file:projects/core-amqp.tgz: @@ -8631,7 +8636,7 @@ packages: '@types/sinon': 9.0.11 '@types/ws': 7.4.4 assert: 1.5.0 - async-lock: 1.3.0 + async-lock: 1.2.8 buffer: 5.7.1 chai: 4.3.4 chai-as-promised: 7.1.1_chai@4.3.4 @@ -8665,7 +8670,7 @@ packages: typescript: 4.2.4 url: 0.11.0 util: 0.12.3 - ws: 7.4.5 + ws: 7.4.4 dev: false name: '@rush-temp/core-amqp' resolution: @@ -8802,7 +8807,7 @@ packages: dev: false name: '@rush-temp/core-client' resolution: - integrity: sha512-21I9CaM3FoglMQizn8osGqwXz4Tw6yk1Y8ST7G75QA99FXpZAYkV5BpkvX1HZcyvFYqgJDXPpgUujV1KgZURJg== + integrity: sha512-wbjmwbFMeFO+pBiAdWh8oVFwrb/HgRVQ9eOceMhlRWFJ6ud9wcz637OsuwZ18AOI/C9r19ZwaLP0YF7r8BpAXQ== tarball: file:projects/core-client.tgz version: 0.0.0 file:projects/core-crypto.tgz: @@ -9472,7 +9477,7 @@ packages: typedoc: 0.15.2 typescript: 4.2.4 uuid: 8.3.2 - ws: 7.4.5 + ws: 7.4.4 dev: false name: '@rush-temp/event-hubs' resolution: @@ -9498,8 +9503,8 @@ packages: '@types/node': 8.10.66 '@types/uuid': 8.3.0 '@types/ws': 7.4.4 - async-lock: 1.3.0 - azure-storage: 2.10.4 + async-lock: 1.2.8 + azure-storage: 2.10.3 chai: 4.3.4 chai-as-promised: 7.1.1_chai@4.3.4 chai-string: 1.5.0_chai@4.3.4 @@ -9522,7 +9527,7 @@ packages: typedoc: 0.15.2 typescript: 4.2.4 uuid: 8.3.2 - ws: 7.4.5 + ws: 7.4.4 dev: false name: '@rush-temp/event-processor-host' resolution: @@ -9584,7 +9589,7 @@ packages: dev: false name: '@rush-temp/eventgrid' resolution: - integrity: sha512-TrRicEN8OEh7DFa/GvPh/oxfL821p9Yb/WQM79S5WjPuJwXCyWYemMDTqn17up550TkO98CxvBQBpzVNNFXdNA== + integrity: sha512-uPwpxSWuymminStHgz0LNgqfYlykhomLYp0UyhHB85Kg2tCibv0WGFjgSkZGZ96dOJvW8p/Gp9HsH3LVsep+RA== tarball: file:projects/eventgrid.tgz version: 0.0.0 file:projects/eventhubs-checkpointstore-blob.tgz: @@ -9726,6 +9731,58 @@ packages: integrity: sha512-DuneImF4EvaxNJN8S5CocoKuUHCNqt/tuTYb+IHa6t1Sr+PaFyL2m1mQN1xD2WlDpgK5nf3arEXcdGPR0xVEeQ== tarball: file:projects/iot-device-update.tgz version: 0.0.0 + file:projects/iot-modelsrepository.tgz: + dependencies: + '@azure/core-rest-pipeline': 1.0.3 + '@azure/core-tracing': 1.0.0-preview.11 + '@microsoft/api-extractor': 7.7.11 + '@rollup/plugin-commonjs': 11.0.2_rollup@1.32.1 + '@rollup/plugin-json': 4.1.0_rollup@1.32.1 + '@rollup/plugin-multi-entry': 3.0.1_rollup@1.32.1 + '@rollup/plugin-node-resolve': 8.4.0_rollup@1.32.1 + '@rollup/plugin-replace': 2.4.2_rollup@1.32.1 + '@types/chai': 4.2.18 + '@types/mocha': 7.0.2 + '@types/node': 8.10.66 + '@types/sinon': 9.0.11 + chai: 4.3.4 + cross-env: 7.0.3 + eslint: 7.26.0 + events: 3.3.0 + inherits: 2.0.4 + karma: 6.3.2 + karma-chrome-launcher: 3.1.0 + karma-coverage: 2.0.3 + karma-edge-launcher: 0.4.2_karma@6.3.2 + karma-env-preprocessor: 0.1.1 + karma-firefox-launcher: 1.3.0 + karma-ie-launcher: 1.0.0_karma@6.3.2 + karma-json-preprocessor: 0.3.3_karma@6.3.2 + karma-json-to-file-reporter: 1.0.1 + karma-junit-reporter: 2.0.1_karma@6.3.2 + karma-mocha: 2.0.1 + karma-mocha-reporter: 2.2.5_karma@6.3.2 + mocha: 7.2.0 + mocha-junit-reporter: 1.23.3_mocha@7.2.0 + nyc: 14.1.1 + prettier: 1.19.1 + rimraf: 3.0.2 + rollup: 1.32.1 + rollup-plugin-sourcemaps: 0.4.2_rollup@1.32.1 + rollup-plugin-terser: 5.3.1_rollup@1.32.1 + rollup-plugin-visualizer: 4.2.2_rollup@1.32.1 + sinon: 9.2.4 + ts-node: 9.1.1_typescript@4.2.4 + tslib: 2.2.0 + typedoc: 0.15.2 + typescript: 4.2.4 + util: 0.12.3 + dev: false + name: '@rush-temp/iot-modelsrepository' + resolution: + integrity: sha512-zfDbETZZ9e9rmZgLSHNBmToZ+6TwgKL+6LehqplCIT55R03tcce9p1L0vvkvtdZCKlQaTWp8RmXak9xzOd7QFg== + tarball: file:projects/iot-modelsrepository.tgz + version: 0.0.0 file:projects/keyvault-admin.tgz: dependencies: '@azure/core-tracing': 1.0.0-preview.11 @@ -10163,7 +10220,7 @@ packages: dev: false name: '@rush-temp/perf-core-rest-pipeline' resolution: - integrity: sha512-zhk0kCQ0xrV5TUeFa+4yKFMQCIAZyy5P0nSlfEDhL7xMsJSFTptYrw+bf1XCpujcSBV2T++I8qYip5qtMZdE3A== + integrity: sha512-KGbqhqsPgPr22oqNu65R+L8VwqyHcc2busi/fV3YZcxr8/RZnTNQDFaFycuwjgauipLSIYzBT0cRDLTgHQlNFw== tarball: file:projects/perf-core-rest-pipeline.tgz version: 0.0.0 file:projects/perf-eventgrid.tgz: @@ -10698,7 +10755,7 @@ packages: tslib: 2.2.0 typedoc: 0.15.2 typescript: 4.2.4 - ws: 7.4.5 + ws: 7.4.4 dev: false name: '@rush-temp/service-bus' resolution: @@ -11332,16 +11389,16 @@ packages: '@azure/core-tracing': 1.0.0-preview.11 '@microsoft/api-extractor': 7.7.11 '@opentelemetry/api': 1.0.0-rc.0 - '@types/chai': 4.2.16 - '@types/chai-as-promised': 7.1.3 + '@types/chai': 4.2.18 + '@types/chai-as-promised': 7.1.4 '@types/mocha': 7.0.2 '@types/node': 8.10.66 azure-iothub: 1.13.1 chai: 4.3.4 chai-as-promised: 7.1.1_chai@4.3.4 cross-env: 7.0.3 - dotenv: 8.2.0 - eslint: 7.23.0 + dotenv: 8.6.0 + eslint: 7.26.0 events: 3.3.0 inherits: 2.0.4 karma: 6.3.2 @@ -11491,6 +11548,7 @@ packages: integrity: sha512-7DeCXxoad4VAGDhNwiYkzn7cUqIkaxlapdj5YnYiE2yVb059BjmVJ5+7Yy3SK5a9fT/rhXiidEuz7GpFF/J67w== tarball: file:projects/web-pubsub.tgz version: 0.0.0 +registry: '' specifiers: '@rush-temp/abort-controller': file:./projects/abort-controller.tgz '@rush-temp/ai-anomaly-detector': file:./projects/ai-anomaly-detector.tgz @@ -11531,6 +11589,7 @@ specifiers: '@rush-temp/eventhubs-checkpointstore-blob': file:./projects/eventhubs-checkpointstore-blob.tgz '@rush-temp/identity': file:./projects/identity.tgz '@rush-temp/iot-device-update': file:./projects/iot-device-update.tgz + '@rush-temp/iot-modelsrepository': file:./projects/iot-modelsrepository.tgz '@rush-temp/keyvault-admin': file:./projects/keyvault-admin.tgz '@rush-temp/keyvault-certificates': file:./projects/keyvault-certificates.tgz '@rush-temp/keyvault-common': file:./projects/keyvault-common.tgz diff --git a/rush.json b/rush.json index cc81ad2b7934..cb62011e4eba 100644 --- a/rush.json +++ b/rush.json @@ -666,6 +666,10 @@ "projectFolder": "sdk/videoanalyzer/video-analyzer-edge", "versionPolicyName": "client" }, + { + "packageName": "@azure/iot-modelsrepository", + "projectFolder": "sdk/iot/modelsrepository" + }, { "packageName": "@azure-tests/perf-ai-form-recognizer", "projectFolder": "sdk/formrecognizer/perf-tests/ai-form-recognizer", diff --git a/sdk/iot/modelsrepository/.npmrc b/sdk/iot/modelsrepository/.npmrc new file mode 100644 index 000000000000..9cf9495031ec --- /dev/null +++ b/sdk/iot/modelsrepository/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/sdk/iot/modelsrepository/.nycrc b/sdk/iot/modelsrepository/.nycrc new file mode 100644 index 000000000000..320eddfeffb9 --- /dev/null +++ b/sdk/iot/modelsrepository/.nycrc @@ -0,0 +1,19 @@ +{ + "include": [ + "dist-esm/src/**/*.js" + ], + "exclude": [ + "**/*.d.ts", + "dist-esm/src/generated/*" + ], + "reporter": [ + "text-summary", + "html", + "cobertura" + ], + "exclude-after-remap": false, + "sourceMap": true, + "produce-source-map": true, + "instrument": true, + "all": true + } diff --git a/sdk/iot/modelsrepository/CHANGELOG.md b/sdk/iot/modelsrepository/CHANGELOG.md new file mode 100644 index 000000000000..be614a1e3cd0 --- /dev/null +++ b/sdk/iot/modelsrepository/CHANGELOG.md @@ -0,0 +1,7 @@ +# Release History + +## 1.0.0-beta.1 (Unreleased) + +With [#14863](https://github.com/Azure/azure-sdk-for-js/pull/14863), this is the first release of the @azure/iot-modelsrepository package. + +This package contains the `ModelsRepositoryClient` to talk to the Azure Models Repository service, with initial support for getting models. Additionally, helper functions for working with DTMIs are provided. diff --git a/sdk/iot/modelsrepository/LICENSE b/sdk/iot/modelsrepository/LICENSE new file mode 100644 index 000000000000..4c529f375cc4 --- /dev/null +++ b/sdk/iot/modelsrepository/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/sdk/iot/modelsrepository/README.md b/sdk/iot/modelsrepository/README.md new file mode 100644 index 000000000000..1275dda09a8f --- /dev/null +++ b/sdk/iot/modelsrepository/README.md @@ -0,0 +1,172 @@ +# Azure IoT Models Repository client library for JavaScript + +This package contains an isomorphic Client Library for Azure IoT Models Repository in JavaScript. Use the Azure IoT Models Repository library for JavaScript to pull DTDL files from remote endpoints. + +[Source code](https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/iot/modelsrepository) | +[Package (npm)](https://www.npmjs.com/package/@azure/iot-modelsrepository/) | +Samples + +------------------------------------- + +# Getting started + +## Key concepts + +The Azure IoT Models Repository library for JavaScript provides functionality for working with the [Azure IoT PlugAndPlay Models Repository](https://devicemodels.azure.com/). It does not provide full CRUD operations, simply the ability to get models from the Models Repository or any other URL endpoints. It does not require any authentication. + +### Currently supported environments + +- Node.js version 8.x.x or higher +- Browser JavaScript + +### How to Install + +The preferred way to install the Azure IoT Models Repository client library for JavaScript is to use the npm package manager. Type the following into a terminal window: + +``` +npm install @azure/iot-modelsrepository +``` + +# Examples + + +## Initializing the Models Repository Client + +```ts +// When no URI is provided for instantiation, the Azure IoT Models Repository global endpoint +// https://devicemodels.azure.com/ is used and the model dependency resolution +// configuration is set to TryFromExpanded. +const client = new ModelsRepositoryClient(); +console.log(`Initialized client point to global endpoint: ${client.repositoryLocation}`); +``` +```ts +// The client will also work with a local filesystem URI. This example shows initalization +// with a local URI and disabling model dependency resolution. +const client = new ModelsRepositoryClient({repositoryLocation: 'file:///path/to/repository/', dependencyResolution: 'disabled'}); +console.log(`Initialized client pointing to local path: ${client.repositoryLocation}`); +``` + +## Publish Models + +Publishing models to the models repository requires [exercising](https://docs.microsoft.com/azure/iot-pnp/concepts-model-repository#publish-a-model) common GitHub workflows. + +## Get Models + +After publishing, your model(s) will be available for consumption from the global repository endpoint. The following snippet shows how to retrieve the corresponding JSON-LD content. + +```ts +// Global endpoint client +const client = new ModelsRepositoryClient(); + +// The output of getModels() will include at least the definition for the target dtmi. +// If the model dependency resolution configuration is not disabled, then models in which the +// target dtmi depends on will also be included in the returned object (mapping dtmis to model objects). +const dtmi = "dtmi:com:example:TemperatureController;1"; +const models = await client.getModels(dtmi, {dependencyResolution: 'tryFromExpanded'}); + +// In this case the above dtmi has 2 model dependencies. +// dtmi:com:example:Thermostat;1 and dtmi:azure:DeviceManagement:DeviceInformation;1 +console.log(`${dtmi} resolved in ${models.keys().length} interfaces.`); +``` + +GitHub pull-request workflows are a core aspect of the IoT Models Repository service. To submit models, the user is expected to fork and clone the global [models repository project](https://github.com/Azure/iot-plugandplay-models) then iterate against the local copy. Changes would then be pushed to the fork (ideally in a new branch) and a PR created against the global repository. + +To support this workflow and similar use cases, the client supports initialization with a local file-system URI. You can use this for example, to test and ensure newly added models to the locally cloned models repository are in their proper locations. + +```ts +// Local sample repository client +const client = new ModelsRepositoryClient(`file:///path/to/repository/`); + +// The output of getModels() will include at least the definition for the target dtmi. +// If the model dependency resolution configuration is not disabled, then models in which the +// target dtmi depends on will also be included in the returned IDictionary. +const dtmi = "dtmi:com:example:TemperatureController;1"; +const models = await client.getModels(dtmi); + +// In this case the above dtmi has 2 model dependencies. +// dtmi:com:example:Thermostat;1 and dtmi:azure:DeviceManagement:DeviceInformation;1 +console.log(`${dtmi} resolved in ${models.keys().length} interfaces.`); +``` + +You are also able to get definitions for multiple root models at a time by leveraging the `GetModels` overload. + +```ts +// Global endpoint client +const client = new ModelsRepositoryClient(); + +const dtmis = ["dtmi:com:example:TemperatureController;1", "dtmi:com:example:azuresphere:sampledevice;1"]; +const models = await client.getModels(dtmis); + +// In this case the dtmi "dtmi:com:example:TemperatureController;1" has 2 model dependencies +// and the dtmi "dtmi:com:example:azuresphere:sampledevice;1" has no additional dependencies. +// The returned IDictionary will include 4 models. +console.log(`${dtmis.toString()} resolved in ${models.keys().length} interfaces.`); +``` + +## Digital Twins Model Parser Integration + +*When the Digital Twins Model Parser is completed, we will update you with information on how to integrate this client.* + +## DtmiConventions utility functions + +The IoT Models Repository applies a set of conventions for organizing digital twin models. This package exposes two auxiliary functions related to `DtmiConventions`, `getModelUri` and `isValidDtmi`. These same functions are used throughout the client. + +```ts +// This snippet shows how to validate a given DTMI string is well-formed. + +// Returns true +isValidDtmi("dtmi:com:example:Thermostat;1"); + +// Returns false +isValidDtmi("dtmi:com:example:Thermostat"); +``` + +```ts +// This snippet shows obtaining a fully qualified path to a model file. + +// Local repository example +const localRepositoryUri: string = "file:///path/to/repository/"; +const fullyQualifiedModelPath: string = + getModelUri("dtmi:com:example:Thermostat;1", localRepositoryUri); + +// Prints '/path/to/repository/dtmi/com/example/thermostat-1.json' +console.log(fullyQualifiedModelPath); + +// Remote repository example +const remoteRepositoryUri: string = "https://contoso.com/models/"; +const fullyQualifiedModelPath: string = + GetModelUri("dtmi:com:example:Thermostat;1", remoteRepositoryUri); + +// Prints 'https://contoso.com/models/dtmi/com/example/thermostat-1.json' +console.log(fullyQualifiedModelPath); +``` + +----------------------------------------- + +# Troubleshooting + +- If you run into an error, first make sure the model you are access exists at the location you are attempting to get it from. + +# Next steps + +- Review the [DTDL Spec](https://docs.microsoft.com/azure/iot-pnp/concepts-model-parser). +- Understand the [Device Models Repository](https://devicemodels.azure.com/). +- Code a IoT Plug and Play 'Device' using the [Azure IoT SDK for Node](https://github.com/Azure/azure-iot-sdk-node/tree/master/device/samples/pnp/readme.md). + +# Related projects + +- [Microsoft Azure SDK for JavaScript](https://github.com/Azure/azure-sdk-for-js) + +# Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/sdk/iot/modelsrepository/api-extractor.json b/sdk/iot/modelsrepository/api-extractor.json new file mode 100644 index 000000000000..1aeaf380fcff --- /dev/null +++ b/sdk/iot/modelsrepository/api-extractor.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "types/src/index.d.ts", + "docModel": { + "enabled": true + }, + "apiReport": { + "enabled": true, + "reportFolder": "./review" + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "", + "publicTrimmedFilePath": "./types/iot-models-repository.d.ts" + }, + "messages": { + "tsdocMessageReporting": { + "default": { + "logLevel": "none" + } + }, + "extractorMessageReporting": { + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-unresolved-link": { + "logLevel": "none" + } + } + } +} diff --git a/sdk/iot/modelsrepository/karma.conf.js b/sdk/iot/modelsrepository/karma.conf.js new file mode 100644 index 000000000000..11ba2e7e033c --- /dev/null +++ b/sdk/iot/modelsrepository/karma.conf.js @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// https://github.com/karma-runner/karma-chrome-launcher +process.env.CHROME_BIN = require("puppeteer").executablePath(); +require("dotenv").config(); +const { + jsonRecordingFilterFunction, + isPlaybackMode, + isSoftRecordMode, + isRecordMode +} = require("@azure/test-utils-recorder"); + +module.exports = function(config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: "./", + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ["mocha"], + + plugins: [ + "karma-mocha", + "karma-mocha-reporter", + "karma-chrome-launcher", + "karma-edge-launcher", + "karma-firefox-launcher", + "karma-ie-launcher", + "karma-env-preprocessor", + "karma-coverage", + "karma-junit-reporter", + "karma-json-to-file-reporter", + "karma-json-preprocessor" + ], + + // list of files / patterns to load in the browser + files: [ + "dist-test/index.browser.js", + { pattern: "dist-test/index.browser.js.map", type: "html", included: false, served: true } + ].concat(isPlaybackMode() || isSoftRecordMode() ? ["recordings/browsers/**/*.json"] : []), + + // list of files / patterns to exclude + exclude: [], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + "**/*.js": ["env"], + "recordings/browsers/**/*.json": ["json"], + // IMPORTANT: COMMENT following line if you want to debug in your browsers!! + // Preprocess source file to calculate code coverage, however this will make source file unreadable + "test-browser/index.js": ["coverage"] + }, + + envPreprocessor: [], + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ["mocha", "coverage", "junit", "json-to-file"], + + coverageReporter: { + // specify a common output directory + dir: "coverage-browser/", + reporters: [{ type: "json", subdir: ".", file: "coverage.json" }] + }, + + junitReporter: { + outputDir: "", // results will be saved as $outputDir/$browserName.xml + outputFile: "test-results.browser.xml", // if included, results will be saved as $outputDir/$browserName/$outputFile + suite: "", // suite will become the package name attribute in xml testsuite element + useBrowserName: false, // add browser name to report and classes names + nameFormatter: undefined, // function (browser, result) to customize the name attribute in xml testcase element + classNameFormatter: undefined, // function (browser, result) to customize the classname attribute in xml testcase element + properties: {} // key value pair of properties to add to the section of the report + }, + + jsonToFileReporter: { + filter: jsonRecordingFilterFunction, + outputPath: "." + }, + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // --no-sandbox allows our tests to run in Linux without having to change the system. + // --disable-web-security allows us to authenticate from the browser without having to write tests using interactive auth, which would be far more complex. + browsers: ["ChromeHeadlessNoSandbox"], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: "ChromeHeadless", + flags: ["--no-sandbox", "--disable-web-security"] + } + }, + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: 1, + + browserNoActivityTimeout: 600000, + browserDisconnectTimeout: 10000, + browserDisconnectTolerance: 3, + browserConsoleLogOptions: { + terminal: !isRecordMode() + }, + + client: { + mocha: { + // change Karma's debug.html to the mocha web reporter + reporter: "html", + timeout: "600000" + } + } + }); +}; diff --git a/sdk/iot/modelsrepository/package.json b/sdk/iot/modelsrepository/package.json new file mode 100644 index 000000000000..b1d3d0e6d10d --- /dev/null +++ b/sdk/iot/modelsrepository/package.json @@ -0,0 +1,137 @@ +{ + "name": "@azure/iot-modelsrepository", + "version": "1.0.0-beta.1", + "description": "Device Model Repository Library with typescript type definitions for node.js and browser.", + "sdk-type": "client", + "main": "dist/index.js", + "module": "dist-esm/src/index.js", + "browser": { + "./dist-esm/src/print.js": "./dist-esm/src/print.browser.js", + "./dist-esm/src/utils/url.js": "./dist-esm/src/utils/url.browser.js", + "./dist-esm/src/utils/path.js": "./dist-esm/src/utils/path.browser.js", + "./dist-esm/src/fetcherFilesystem.js": "./dist-esm/src/utils/fetcherFilesystem.browser.js" + }, + "types": "types/iot-models-repository.d.ts", + "scripts": { + "audit": "node ../../../common/scripts/rush-audit.js && rimraf node_modules package-lock.json && npm i --package-lock-only 2>&1 && npm audit", + "build:browser": "tsc -p . && cross-env ONLY_BROWSER=true rollup -c 2>&1", + "build:node": "tsc -p . && cross-env ONLY_NODE=true rollup -c 2>&1", + "build:samples": "echo Obsolete.", + "build:test": "tsc -p . && rollup -c 2>&1", + "build": "tsc -p . && rollup -c 2>&1 && api-extractor run --local", + "check-format": "prettier --list-different --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"", + "clean": "rimraf dist dist-* test-dist temp types *.tgz *.log", + "execute:samples": "dev-tool samples run samples-dev", + "extract-api": "tsc -p . && api-extractor run --local", + "format": "prettier --write --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"", + "integration-test:browser": "karma start --single-run", + "integration-test:node": "nyc mocha -r esm --require source-map-support/register --reporter ../../../common/tools/mocha-multi-reporter.js --timeout 5000000 --full-trace \"dist-esm/test/{,!(browser)/**/}/*.spec.js\"", + "integration-test": "npm run integration-test:node && npm run integration-test:browser", + "lint:fix": "eslint package.json api-extractor.json src test --ext .ts --fix --fix-type [problem,suggestion]", + "lint": "eslint package.json api-extractor.json src test --ext .ts", + "pack": "npm pack 2>&1", + "prebuild": "npm run clean", + "test:browser": "npm run build:test && npm run unit-test:browser && npm run integration-test:browser", + "test:node": "npm run build:test && npm run unit-test:node && npm run integration-test:node", + "test": "npm run build:test && npm run unit-test && npm run integration-test", + "unit-test:browser": "karma start --single-run", + "unit-test:node": "mocha --require esm --reporter ../../../common/tools/mocha-multi-reporter.js --timeout 1200000 --full-trace \"dist-esm/test/{,!(browser)/**/}/*.spec.js\"", + "unit-test": "npm run unit-test:node && npm run unit-test:browser", + "temp-unit-test": "mocha -r ts-node/register --timeout 1200000 test/node/*.spec.ts" + }, + "files": [ + "dist/", + "dist-esm/src/", + "types/iot-models-repository.d.ts", + "README.md", + "LICENSE" + ], + "repository": "github:Azure/azure-sdk-for-js", + "engines": { + "node": ">=8.0.0" + }, + "keywords": [ + "azure", + "cloud", + "typescript" + ], + "author": "Microsoft Corporation", + "license": "MIT", + "bugs": { + "url": "https://github.com/Azure/azure-sdk-for-js/issues" + }, + "homepage": "https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/iot/modelsrepository/", + "sideEffects": false, + "prettier": "@azure/eslint-plugin-azure-sdk/prettier.json", + "dependencies": { + "@azure/core-client": "^1.0.0", + "@azure/core-util": "^1.0.0-beta.1", + "@azure/core-rest-pipeline": "^1.0.3", + "@azure/core-tracing": "1.0.0-preview.11", + "@azure/logger": "^1.0.0", + "events": "^3.0.0", + "tslib": "^2.0.0" + }, + "devDependencies": { + "@azure/dev-tool": "^1.0.0", + "@azure/eslint-plugin-azure-sdk": "^3.0.0", + "@azure/test-utils-recorder": "^1.0.0", + "@microsoft/api-extractor": "7.7.11", + "@rollup/plugin-commonjs": "11.0.2", + "@rollup/plugin-json": "^4.0.0", + "@rollup/plugin-multi-entry": "^3.0.0", + "@rollup/plugin-node-resolve": "^8.0.0", + "@rollup/plugin-replace": "^2.2.0", + "@types/chai": "^4.1.6", + "@types/mocha": "^7.0.2", + "@types/node": "^8.0.0", + "@types/sinon": "^9.0.4", + "chai": "^4.2.0", + "cross-env": "^7.0.2", + "eslint": "^7.15.0", + "inherits": "^2.0.3", + "karma": "^6.2.0", + "karma-chrome-launcher": "^3.0.0", + "karma-coverage": "^2.0.0", + "karma-edge-launcher": "^0.4.2", + "karma-env-preprocessor": "^0.1.1", + "karma-firefox-launcher": "^1.1.0", + "karma-ie-launcher": "^1.0.0", + "karma-json-preprocessor": "^0.3.3", + "karma-json-to-file-reporter": "^1.0.1", + "karma-junit-reporter": "^2.0.1", + "karma-mocha": "^2.0.1", + "karma-mocha-reporter": "^2.2.5", + "mocha": "^7.1.1", + "mocha-junit-reporter": "^1.18.0", + "nyc": "^14.0.0", + "prettier": "^1.16.4", + "rimraf": "^3.0.0", + "rollup": "^1.16.3", + "rollup-plugin-sourcemaps": "^0.4.2", + "rollup-plugin-terser": "^5.1.1", + "rollup-plugin-visualizer": "^4.0.4", + "sinon": "^9.0.2", + "ts-node": "^9.0.0", + "typedoc": "0.15.2", + "typescript": "~4.2.0", + "util": "^0.12.1" + }, + "standard": { + "env": [ + "mocha" + ] + }, + "//smokeTestConfiguration": { + "skipFolder": true + }, + "//sampleConfiguration": { + "productName": "Azure IoT Models Repository", + "productSlugs": [ + "azure", + "azure-iot-modelsrepository" + ], + "requiredResources": {}, + "apiRefLink": "https://docs.microsoft.com/javascript/api/" + } +} diff --git a/sdk/iot/modelsrepository/review/iot-modelsrepository.api.md b/sdk/iot/modelsrepository/review/iot-modelsrepository.api.md new file mode 100644 index 000000000000..1052c854ce8e --- /dev/null +++ b/sdk/iot/modelsrepository/review/iot-modelsrepository.api.md @@ -0,0 +1,51 @@ +## API Report File for "@azure/iot-modelsrepository" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { CommonClientOptions } from '@azure/core-client'; +import { OperationOptions } from '@azure/core-client'; + +// @public +export type dependencyResolutionType = "disabled" | "enabled" | "tryFromExpanded"; + +// @public +export interface GetModelsOptions extends OperationOptions { + dependencyResolution?: dependencyResolutionType; +} + +// @public +export function getModelUri(dtmi: string, repositoryUri: string, expanded?: boolean): string; + +// @public +export function isValidDtmi(dtmi: string): boolean; + +// @public +export class ModelError extends Error { + constructor(message: string); +} + +// @public +export class ModelsRepositoryClient { + constructor(options?: ModelsRepositoryClientOptions); + get apiVersion(): string; + get dependencyResolution(): dependencyResolutionType; + getModels(dtmis: string, options?: GetModelsOptions): Promise<{ + [dtmi: string]: unknown; + }>; + getModels(dtmis: string[], options?: GetModelsOptions): Promise<{ + [dtmi: string]: unknown; + }>; + get repositoryLocation(): string; + } + +// @public +export interface ModelsRepositoryClientOptions extends CommonClientOptions { + apiVersion?: string; + dependencyResolution?: dependencyResolutionType; + repositoryLocation?: string; +} + + +``` diff --git a/sdk/iot/modelsrepository/rollup.config.js b/sdk/iot/modelsrepository/rollup.config.js new file mode 100644 index 000000000000..5d7deee44c14 --- /dev/null +++ b/sdk/iot/modelsrepository/rollup.config.js @@ -0,0 +1,3 @@ +import { makeConfig } from "@azure/dev-tool/shared-config/rollup"; + +export default makeConfig(require("./package.json")); diff --git a/sdk/iot/modelsrepository/sample.env b/sdk/iot/modelsrepository/sample.env new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sdk/iot/modelsrepository/samples-dev/dtmiConventionsSample.ts b/sdk/iot/modelsrepository/samples-dev/dtmiConventionsSample.ts new file mode 100644 index 000000000000..a28b1d1ccf9d --- /dev/null +++ b/sdk/iot/modelsrepository/samples-dev/dtmiConventionsSample.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +/** + * @summary Demonstrates the use of a getModelUri and isValidDtmi, helper functions for interacting with DTMIs. + */ + +import { getModelUri, isValidDtmi } from "@azure/iot-modelsrepository"; + +function main() { + const dtmi1 = "dtmi:com:example:Thermostat;1"; + const dtmi2 = "dtmi:com:example:Thermostat"; + // returns true + const result1 = isValidDtmi(dtmi1); + console.log(`${dtmi1} is valid? ${result1}`); + + // returns false + const result2 = isValidDtmi(dtmi2); + console.log(`${dtmi2} is valid? ${result2}`); + // local repository fully qualified path to a model file + const fullyQualifiedLocalPath = getModelUri( + "dtmi:com:example:Thermostat;1", + "file:///path/to/repository/" + ); + console.log(fullyQualifiedLocalPath); + + const fullyQualifiedRemotePath = getModelUri( + "dtmi:com:example:Thermostat;1", + "https://contoso.com/models" + ); + console.log(fullyQualifiedRemotePath); +} + +main(); diff --git a/sdk/iot/modelsrepository/samples-dev/modelResolutionSample.ts b/sdk/iot/modelsrepository/samples-dev/modelResolutionSample.ts new file mode 100644 index 000000000000..bccb5c13452d --- /dev/null +++ b/sdk/iot/modelsrepository/samples-dev/modelResolutionSample.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +/** + * @summary Demonstrates the use of ModelsRepositoryClient to get models from an endpoint. + */ + +import { ModelsRepositoryClient } from "@azure/iot-modelsrepository"; + +const repositoryEndpoint = "https://devicemodels.azure.com"; +const dtmi = "dtmi:azure:DeviceManagement:DeviceInformation;1"; + +console.log(repositoryEndpoint, dtmi); + +async function main() { + // When no URI is provided for instantiation, the Azure IoT Models Repository global endpoint + // https://devicemodels.azure.com/ is used and the model dependency resolution + // configuration is set to TryFromExpanded. + const client = new ModelsRepositoryClient({ repositoryLocation: repositoryEndpoint }); + const result = await client.getModels(dtmi, { dependencyResolution: "tryFromExpanded" }); + Object.keys(result).forEach((fetchedDtmi) => { + const currentDtdl = result[fetchedDtmi] as any; + console.log("------------------------------------------------"); + console.log(`DTMI is: ${fetchedDtmi}`); + console.log(`DTDL Display Name is: ${currentDtdl.displayName}`); + console.log(`DTDL Description is: ${currentDtdl.description}`); + console.log("------------------------------------------------"); + console.log(JSON.stringify(result[fetchedDtmi])); + console.log("------------------------------------------------"); + }); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/iot/modelsrepository/samples/v1/javascript/README.md b/sdk/iot/modelsrepository/samples/v1/javascript/README.md new file mode 100644 index 000000000000..1ae1ada683ce --- /dev/null +++ b/sdk/iot/modelsrepository/samples/v1/javascript/README.md @@ -0,0 +1,62 @@ +--- +page_type: sample +languages: + - javascript +products: + - azure + - azure-iot-modelsrepository +urlFragment: iot-modelsrepository-javascript +--- + +# Azure IoT Models Repository client library samples for JavaScript + +These sample programs show how to use the JavaScript client libraries for Azure IoT Models Repository in some common scenarios. + +| **File Name** | **Description** | +| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| [dtmiConventionsSample.js][dtmiconventionssample] | Demonstrates the use of a getModelUri and isValidDtmi, helper functions for interacting with DTMIs. | +| [modelResolutionSample.js][modelresolutionsample] | Demonstrates the use of ModelsRepositoryClient to get models from an endpoint. | + +## Prerequisites + +The sample programs are compatible with Node.js >=12.0.0. + +You need [an Azure subscription][freesub] to run these sample programs. + +Samples retrieve credentials to access the service endpoint from environment variables. Alternatively, edit the source code to include the appropriate credentials. See each individual sample for details on which environment variables/credentials it requires to function. + +Adapting the samples to run in the browser may require some additional consideration. For details, please see the [package README][package]. + +## Setup + +To run the samples using the published version of the package: + +1. Install the dependencies using `npm`: + +```bash +npm install +``` + +2. Edit the file `sample.env`, adding the correct credentials to access the Azure service and run the samples. Then rename the file from `sample.env` to just `.env`. The sample programs will read this file automatically. + +3. Run whichever samples you like (note that some samples may require additional setup, see the table above): + +```bash +node dtmiConventionsSample.js +``` + +Alternatively, run a single sample with the correct environment variables set (setting up the `.env` file is not required if you do this), for example (cross-platform): + +```bash +npx cross-env node dtmiConventionsSample.js +``` + +## Next Steps + +Take a look at our [API Documentation][apiref] for more information about the APIs that are available in the clients. + +[dtmiconventionssample]: https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/iot/modelsrepository/samples/v1/javascript/dtmiConventionsSample.js +[modelresolutionsample]: https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/iot/modelsrepository/samples/v1/javascript/modelResolutionSample.js +[apiref]: https://docs.microsoft.com/javascript/api/ +[freesub]: https://azure.microsoft.com/free/ +[package]: https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/iot/modelsrepository/README.md diff --git a/sdk/iot/modelsrepository/samples/v1/javascript/dtmiConventionsSample.js b/sdk/iot/modelsrepository/samples/v1/javascript/dtmiConventionsSample.js new file mode 100644 index 000000000000..55a3042a9411 --- /dev/null +++ b/sdk/iot/modelsrepository/samples/v1/javascript/dtmiConventionsSample.js @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +/** + * @summary Demonstrates the use of a getModelUri and isValidDtmi, helper functions for interacting with DTMIs. + */ + +const { getModelUri, isValidDtmi } = require("@azure/iot-modelsrepository"); + +function main() { + const dtmi1 = "dtmi:com:example:Thermostat;1"; + const dtmi2 = "dtmi:com:example:Thermostat"; + // returns true + const result1 = isValidDtmi(dtmi1); + console.log(`${dtmi1} is valid? ${result1}`); + + // returns false + const result2 = isValidDtmi(dtmi2); + console.log(`${dtmi2} is valid? ${result2}`); + // local repository fully qualified path to a model file + const fullyQualifiedLocalPath = getModelUri( + "dtmi:com:example:Thermostat;1", + "file:///path/to/repository/" + ); + console.log(fullyQualifiedLocalPath); + + const fullyQualifiedRemotePath = getModelUri( + "dtmi:com:example:Thermostat;1", + "https://contoso.com/models" + ); + console.log(fullyQualifiedRemotePath); +} + +main(); diff --git a/sdk/iot/modelsrepository/samples/v1/javascript/modelResolutionSample.js b/sdk/iot/modelsrepository/samples/v1/javascript/modelResolutionSample.js new file mode 100644 index 000000000000..7077dff696f0 --- /dev/null +++ b/sdk/iot/modelsrepository/samples/v1/javascript/modelResolutionSample.js @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +/** + * @summary Demonstrates the use of ModelsRepositoryClient to get models from an endpoint. + */ + +const { ModelsRepositoryClient } = require("@azure/iot-modelsrepository"); + +const repositoryEndpoint = "https://devicemodels.azure.com"; +const dtmi = "dtmi:azure:DeviceManagement:DeviceInformation;1"; + +console.log(repositoryEndpoint, dtmi); + +async function main() { + // When no URI is provided for instantiation, the Azure IoT Models Repository global endpoint + // https://devicemodels.azure.com/ is used and the model dependency resolution + // configuration is set to TryFromExpanded. + const client = new ModelsRepositoryClient({ repositoryLocation: repositoryEndpoint }); + const result = await client.getModels(dtmi, { dependencyResolution: "tryFromExpanded" }); + Object.keys(result).forEach((fetchedDtmi) => { + const currentDtdl = result[fetchedDtmi]; + console.log("------------------------------------------------"); + console.log(`DTMI is: ${fetchedDtmi}`); + console.log(`DTDL Display Name is: ${currentDtdl.displayName}`); + console.log(`DTDL Description is: ${currentDtdl.description}`); + console.log("------------------------------------------------"); + console.log(JSON.stringify(result[fetchedDtmi])); + console.log("------------------------------------------------"); + }); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/iot/modelsrepository/samples/v1/javascript/package.json b/sdk/iot/modelsrepository/samples/v1/javascript/package.json new file mode 100644 index 000000000000..a7c75abd7f30 --- /dev/null +++ b/sdk/iot/modelsrepository/samples/v1/javascript/package.json @@ -0,0 +1,29 @@ +{ + "name": "azure-iot-modelsrepository-samples-js", + "private": true, + "version": "1.0.0", + "description": "Azure IoT Models Repository client library samples for JavaScript", + "engine": { + "node": ">=12.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Azure/azure-sdk-for-js.git", + "directory": "sdk/iot/modelsrepository" + }, + "keywords": [ + "azure", + "cloud", + "typescript" + ], + "author": "Microsoft Corporation", + "license": "MIT", + "bugs": { + "url": "https://github.com/Azure/azure-sdk-for-js/issues" + }, + "homepage": "https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/iot/modelsrepository", + "dependencies": { + "@azure/iot-modelsrepository": "next", + "dotenv": "latest" + } +} diff --git a/sdk/iot/modelsrepository/samples/v1/javascript/sample.env b/sdk/iot/modelsrepository/samples/v1/javascript/sample.env new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sdk/iot/modelsrepository/samples/v1/typescript/README.md b/sdk/iot/modelsrepository/samples/v1/typescript/README.md new file mode 100644 index 000000000000..fa00798356d4 --- /dev/null +++ b/sdk/iot/modelsrepository/samples/v1/typescript/README.md @@ -0,0 +1,75 @@ +--- +page_type: sample +languages: + - typescript +products: + - azure + - azure-iot-modelsrepository +urlFragment: iot-modelsrepository-typescript +--- + +# Azure IoT Models Repository client library samples for TypeScript + +These sample programs show how to use the TypeScript client libraries for Azure IoT Models Repository in some common scenarios. + +| **File Name** | **Description** | +| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| [dtmiConventionsSample.ts][dtmiconventionssample] | Demonstrates the use of a getModelUri and isValidDtmi, helper functions for interacting with DTMIs. | +| [modelResolutionSample.ts][modelresolutionsample] | Demonstrates the use of ModelsRepositoryClient to get models from an endpoint. | + +## Prerequisites + +The sample programs are compatible with Node.js >=12.0.0. + +Before running the samples in Node, they must be compiled to JavaScript using the TypeScript compiler. For more information on TypeScript, see the [TypeScript documentation][typescript]. Install the TypeScript compiler using: + +```bash +npm install -g typescript +``` + +You need [an Azure subscription][freesub] to run these sample programs. + +Samples retrieve credentials to access the service endpoint from environment variables. Alternatively, edit the source code to include the appropriate credentials. See each individual sample for details on which environment variables/credentials it requires to function. + +Adapting the samples to run in the browser may require some additional consideration. For details, please see the [package README][package]. + +## Setup + +To run the samples using the published version of the package: + +1. Install the dependencies using `npm`: + +```bash +npm install +``` + +2. Compile the samples: + +```bash +npm run build +``` + +3. Edit the file `sample.env`, adding the correct credentials to access the Azure service and run the samples. Then rename the file from `sample.env` to just `.env`. The sample programs will read this file automatically. + +4. Run whichever samples you like (note that some samples may require additional setup, see the table above): + +```bash +node dist/dtmiConventionsSample.js +``` + +Alternatively, run a single sample with the correct environment variables set (setting up the `.env` file is not required if you do this), for example (cross-platform): + +```bash +npx cross-env node dist/dtmiConventionsSample.js +``` + +## Next Steps + +Take a look at our [API Documentation][apiref] for more information about the APIs that are available in the clients. + +[dtmiconventionssample]: https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/iot/modelsrepository/samples/v1/typescript/src/dtmiConventionsSample.ts +[modelresolutionsample]: https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/iot/modelsrepository/samples/v1/typescript/src/modelResolutionSample.ts +[apiref]: https://docs.microsoft.com/javascript/api/ +[freesub]: https://azure.microsoft.com/free/ +[package]: https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/iot/modelsrepository/README.md +[typescript]: https://www.typescriptlang.org/docs/home.html diff --git a/sdk/iot/modelsrepository/samples/v1/typescript/package.json b/sdk/iot/modelsrepository/samples/v1/typescript/package.json new file mode 100644 index 000000000000..4ff9960d1be1 --- /dev/null +++ b/sdk/iot/modelsrepository/samples/v1/typescript/package.json @@ -0,0 +1,37 @@ +{ + "name": "azure-iot-modelsrepository-samples-ts", + "private": true, + "version": "1.0.0", + "description": "Azure IoT Models Repository client library samples for TypeScript", + "engine": { + "node": ">=12.0.0" + }, + "scripts": { + "build": "tsc", + "prebuild": "rimraf dist/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Azure/azure-sdk-for-js.git", + "directory": "sdk/iot/modelsrepository" + }, + "keywords": [ + "azure", + "cloud", + "typescript" + ], + "author": "Microsoft Corporation", + "license": "MIT", + "bugs": { + "url": "https://github.com/Azure/azure-sdk-for-js/issues" + }, + "homepage": "https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/iot/modelsrepository", + "dependencies": { + "@azure/iot-modelsrepository": "next", + "dotenv": "latest" + }, + "devDependencies": { + "typescript": "~4.2.0", + "rimraf": "latest" + } +} diff --git a/sdk/iot/modelsrepository/samples/v1/typescript/sample.env b/sdk/iot/modelsrepository/samples/v1/typescript/sample.env new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sdk/iot/modelsrepository/samples/v1/typescript/src/dtmiConventionsSample.ts b/sdk/iot/modelsrepository/samples/v1/typescript/src/dtmiConventionsSample.ts new file mode 100644 index 000000000000..eb8f758f70c5 --- /dev/null +++ b/sdk/iot/modelsrepository/samples/v1/typescript/src/dtmiConventionsSample.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +/** + * @summary Demonstrates the use of a getModelUri and isValidDtmi, helper functions for interacting with DTMIs. + */ + +import {getModelUri, isValidDtmi} from "@azure/iot-modelsrepository"; + +function main() { + + const dtmi1 = "dtmi:com:example:Thermostat;1"; + const dtmi2 = "dtmi:com:example:Thermostat" + // returns true + const result1 = isValidDtmi(dtmi1); + console.log(`${dtmi1} is valid? ${result1}`); + + // returns false + const result2 = isValidDtmi(dtmi2); + console.log(`${dtmi2} is valid? ${result2}`); + // local repository fully qualified path to a model file + const fullyQualifiedLocalPath = getModelUri("dtmi:com:example:Thermostat;1", "file:///path/to/repository/"); + console.log(fullyQualifiedLocalPath); + + const fullyQualifiedRemotePath = getModelUri("dtmi:com:example:Thermostat;1", "https://contoso.com/models"); + console.log(fullyQualifiedRemotePath); +} + +main(); \ No newline at end of file diff --git a/sdk/iot/modelsrepository/samples/v1/typescript/src/modelResolutionSample.ts b/sdk/iot/modelsrepository/samples/v1/typescript/src/modelResolutionSample.ts new file mode 100644 index 000000000000..56f271d997e8 --- /dev/null +++ b/sdk/iot/modelsrepository/samples/v1/typescript/src/modelResolutionSample.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +/** + * @summary Demonstrates the use of ModelsRepositoryClient to get models from an endpoint. + */ + +import {ModelsRepositoryClient} from "@azure/iot-modelsrepository"; + +const repositoryEndpoint = "https://devicemodels.azure.com"; +const dtmi = "dtmi:azure:DeviceManagement:DeviceInformation;1"; + +console.log(repositoryEndpoint, dtmi); + +async function main() { + // When no URI is provided for instantiation, the Azure IoT Models Repository global endpoint + // https://devicemodels.azure.com/ is used and the model dependency resolution + // configuration is set to TryFromExpanded. + const client = new ModelsRepositoryClient({repositoryLocation: repositoryEndpoint}); + const result = await client.getModels(dtmi, {dependencyResolution: 'tryFromExpanded'}); + Object.keys(result).forEach((fetchedDtmi) => { + const currentDtdl = result[fetchedDtmi] as any; + console.log("------------------------------------------------"); + console.log(`DTMI is: ${fetchedDtmi}`); + console.log(`DTDL Display Name is: ${currentDtdl.displayName}`); + console.log(`DTDL Description is: ${currentDtdl.description}`); + console.log("------------------------------------------------"); + console.log(JSON.stringify(result[fetchedDtmi])); + console.log("------------------------------------------------"); + }); +} + +main().catch((err) => { + console.error("The sample encountered an error:", err); +}); diff --git a/sdk/iot/modelsrepository/samples/v1/typescript/tsconfig.json b/sdk/iot/modelsrepository/samples/v1/typescript/tsconfig.json new file mode 100644 index 000000000000..416c2dd82e00 --- /dev/null +++ b/sdk/iot/modelsrepository/samples/v1/typescript/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "commonjs", + "moduleResolution": "node", + "resolveJsonModule": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "alwaysStrict": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**.ts" + ] +} diff --git a/sdk/iot/modelsrepository/src/constants.ts b/sdk/iot/modelsrepository/src/constants.ts new file mode 100644 index 000000000000..f70837abf8b7 --- /dev/null +++ b/sdk/iot/modelsrepository/src/constants.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +import {isNode} from "@azure/core-util"; + +const currentPlatform = isNode ? "node" : "browser"; + +export const SDK_VERSION = "1.0.0-beta.1"; +export const DEFAULT_USER_AGENT = `azsdk-node-modelsrepository/${SDK_VERSION} (${currentPlatform})`; +export const DEFAULT_REPOSITORY_LOCATION = "https://devicemodels.azure.com"; +export const DEFAULT_API_VERSION = "2021-02-11"; + +export const DEPENDENCY_MODE_DISABLED = "disabled"; +export const DEPENDENCY_MODE_ENABLED = "enabled"; +export const DEPENDENCY_MODE_TRY_FROM_EXPANDED = "tryFromExpanded"; diff --git a/sdk/iot/modelsrepository/src/dependencyResolutionType.ts b/sdk/iot/modelsrepository/src/dependencyResolutionType.ts new file mode 100644 index 000000000000..6280b4a55ce8 --- /dev/null +++ b/sdk/iot/modelsrepository/src/dependencyResolutionType.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +/** + * @name dependencyResolutionType + * + * @description + * either dependency resolution is disabled, and only the client will get only the model linked to the dtmi, + * it is enabled, and the client will resolve all dependency models linked to the dtmi within the endpoint, + * or it is set to tryFromExpanded, where the client will attempt to get the expanded JSON format from the endpoint, + * and in the event of failure will fallback on the standard enabled dependency resolution. + * + */ +export type dependencyResolutionType = "disabled" | "enabled" | "tryFromExpanded"; diff --git a/sdk/iot/modelsrepository/src/dom.d.ts b/sdk/iot/modelsrepository/src/dom.d.ts new file mode 100644 index 000000000000..e95d8823722e --- /dev/null +++ b/sdk/iot/modelsrepository/src/dom.d.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// This file avoids adding dom to the tsconfig. +// We still need to reference the dom for ts compiliation, since +// in the browser url shim it references the URL dom. +/// diff --git a/sdk/iot/modelsrepository/src/dtmiConventions.ts b/sdk/iot/modelsrepository/src/dtmiConventions.ts new file mode 100644 index 000000000000..9d83fd98cc61 --- /dev/null +++ b/sdk/iot/modelsrepository/src/dtmiConventions.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +/** + * isValidDtmi + * @description given a dtmi it will validate it matches the convention. + * This is based on the DTMI spec: + * https://github.com/Azure/opendigitaltwins-dtdl/blob/master/DTDL/v2/dtdlv2.md#digital-twin-model-identifier + * + * @param dtmi + * @returns {boolean} + */ +export function isValidDtmi(dtmi: string): boolean { + if (typeof dtmi !== "string") return false; + const re = /^dtmi:[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?(?::[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?)*;[1-9][0-9]{0,8}$/; + return re.test(dtmi); // true if dtmi matches regular expression, false otherwise +} + +/** + * getModelUri + * @description given the dtmi and repository uri, will get a fully qualified model uri. + * + * @param dtmi + * @param repositoryUri + * @param expanded + * @returns {string} + */ +export function getModelUri( + dtmi: string, + repositoryUri: string, + expanded: boolean = false +): string { + if (!repositoryUri.endsWith("/")) { + repositoryUri = repositoryUri.concat("/"); + } + const modelUri = repositoryUri + convertDtmiToPath(dtmi, expanded); + return modelUri; +} + +/** + * convertDtmiToPath + * @description converts a dtmi into the model path format. + * + * @param dtmi + * @param expanded + * @internal + */ +export function convertDtmiToPath(dtmi: string, expanded: boolean): string { + // presently this dtmi to path function does not return the path with a + // file format at the end, i.e. does not append .json or .expanded.json. + // that happens in the dtmiToQualifiedPath function + + if (isValidDtmi(dtmi)) { + let thePath = `${dtmi + .toLowerCase() + .replace(/:/gm, "/") + .replace(/;/gm, "-")}.json`; + if (expanded) { + thePath = thePath.replace(".json", ".expanded.json"); + } + return thePath; + } else { + throw new Error("DTMI provided is invalid. Ensure it follows DTMI conventions."); + } +} diff --git a/sdk/iot/modelsrepository/src/dtmiResolver.ts b/sdk/iot/modelsrepository/src/dtmiResolver.ts new file mode 100644 index 000000000000..ba3920c3d137 --- /dev/null +++ b/sdk/iot/modelsrepository/src/dtmiResolver.ts @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +import { OperationOptions } from "@azure/core-client"; +import { DTDL } from "./psuedoDtdl"; +import { convertDtmiToPath } from "./dtmiConventions"; +import { ModelError } from "./exceptions"; +import { Fetcher } from "./fetcherAbstract"; +import { logger } from "./logger"; + +/** + * DtmiResolver handles reformatting the DTMIs to paths and passing options + * down to the configured fetcher. It is almost like a middle man between the + * user-facing API and the PsuedoParser (which identifies if there are sub-dependencies + * to resolve), and the configured fetcher, which will go out to the endpoint, + * either in the filesystem or through a URI, and actually get the model we want. + * + * @internal + */ +export class DtmiResolver { + private _fetcher: Fetcher; + constructor(fetcher: Fetcher) { + this._fetcher = fetcher; + } + + async resolve( + dtmis: string[], + expandedModel: boolean, + options?: OperationOptions + ): Promise<{ [dtmi: string]: DTDL }> { + const modelMap: { [dtmi: string]: DTDL } = {}; + const dtdlPromises = dtmis.map(async (dtmi) => { + const dtdlPath = convertDtmiToPath(dtmi, expandedModel); + logger.info(`Model ${dtmi} located in repository at ${dtdlPath}`); + const dtdl = await this._fetcher.fetch(dtdlPath, options); + if (expandedModel) { + if (Array.isArray(dtdl)) { + const modelIds: string[] = (dtdl as DTDL[]).map((model: DTDL) => model["@id"]); + if (!modelIds.includes(dtmi)) { + throw new ModelError( + `DTMI mismatch on expanded DTDL - Request: ${dtmi}, Response: ${modelIds}` + ); + } + for (const model of dtdl) { + modelMap[model["@id"]] = model; + } + } else { + throw new ModelError("Expanded format should always return an array of models."); + } + } else { + const model = dtdl as DTDL; + if (model["@id"] != dtmi) { + throw new ModelError(`DTMI mismatch - Request: ${dtmi}, Response ${model["@id"]}`); + } + + modelMap[`${dtmi}`] = model; + } + }); + + await Promise.all(dtdlPromises); + return modelMap; + } +} diff --git a/sdk/iot/modelsrepository/src/exceptions.ts b/sdk/iot/modelsrepository/src/exceptions.ts new file mode 100644 index 000000000000..5e9b3b880aca --- /dev/null +++ b/sdk/iot/modelsrepository/src/exceptions.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +/** + * A ModelError will be thrown in the even the Model in the repo is malformed in some standard way. + */ +export class ModelError extends Error { + constructor(message: string) { + super(message); + + this.name = "ModelError"; + } +} diff --git a/sdk/iot/modelsrepository/src/fetcherAbstract.ts b/sdk/iot/modelsrepository/src/fetcherAbstract.ts new file mode 100644 index 000000000000..b0bccf40e210 --- /dev/null +++ b/sdk/iot/modelsrepository/src/fetcherAbstract.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +import { OperationOptions } from "@azure/core-client"; +import { DTDL } from "./psuedoDtdl"; + +/** + * Base Interface for Fetchers, which fetch models from endpoints. + * + * @internal + */ +export interface Fetcher { + fetch(path: string, options?: OperationOptions): Promise; +} diff --git a/sdk/iot/modelsrepository/src/fetcherFilesystem.ts b/sdk/iot/modelsrepository/src/fetcherFilesystem.ts new file mode 100644 index 000000000000..e72e5e55f12f --- /dev/null +++ b/sdk/iot/modelsrepository/src/fetcherFilesystem.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +import fs from "fs"; +import * as path from "path"; +import { RestError, RestErrorOptions } from "@azure/core-rest-pipeline"; +import { Fetcher } from "./fetcherAbstract"; +import { logger } from "./logger"; +import { DTDL } from "./psuedoDtdl"; + +function readFilePromise(path: string): Promise { + return new Promise((res, rej) => { + fs.readFile(path, "utf8", (err, data) => { + err ? rej(err) : res(data); + return 0; + }); + }); +} + +/** + * The Filesystem Fetcher implements the generic Fetcher interface + * so that models are fetched from a filesystem endpoint. + * + * @internal + */ +export class FilesystemFetcher implements Fetcher { + private _baseFilePath: string; + + constructor(baseFilePath: string) { + this._baseFilePath = baseFilePath; + } + + async fetch(filePath: string) { + logger.info(`Fetching ${filePath} from local filesystem`); + const absolutePath = path.join(this._baseFilePath, filePath); + if (absolutePath.indexOf(this._baseFilePath) !== 0) { + throw new Error("Attempted to escape base file path"); + } + + try { + logger.info(`File open on ${absolutePath}`); + const dtdlFile = await readFilePromise(absolutePath); + const parsedDtdl: DTDL | DTDL[] = JSON.parse(dtdlFile); + return parsedDtdl; + } catch (e) { + const options: RestErrorOptions = { + code: "ResourceNotFound", + statusCode: e?.status + }; + throw new RestError("Failed to fetch from Filesystem", options); + } + } +} diff --git a/sdk/iot/modelsrepository/src/fetcherHTTP.ts b/sdk/iot/modelsrepository/src/fetcherHTTP.ts new file mode 100644 index 000000000000..19f324d589c1 --- /dev/null +++ b/sdk/iot/modelsrepository/src/fetcherHTTP.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +import { OperationOptions, ServiceClient } from "@azure/core-client"; +import { + createHttpHeaders, + createPipelineRequest, + HttpHeaders, + HttpMethods, + PipelineRequest, + PipelineResponse, + RestError +} from "@azure/core-rest-pipeline"; +import { logger } from "./logger"; +import { Fetcher } from "./fetcherAbstract"; + +/** + * The HTTP Fetcher implements the Fetcher interface to + * retrieve models through HTTP calls. + * + * @internal + */ +export class HttpFetcher implements Fetcher { + private _client: ServiceClient; + private _baseURL: string; + + constructor(baseURL: string, client: ServiceClient) { + this._client = client; + this._baseURL = baseURL; + } + + async fetch(path: string, options: OperationOptions) { + logger.info(`Fetching ${path} from remote endpoint`); + const myURL = this._baseURL + "/" + path; + const requestMethod: HttpMethods = "GET"; + const requestHeader: HttpHeaders = createHttpHeaders(options.requestOptions?.customHeaders); + const requestOptions = { + url: myURL, + method: requestMethod, + headers: requestHeader, + timeout: options.requestOptions?.timeout, + abortSignal: options.abortSignal, + tracingOptions: options.tracingOptions, + allowInsecureConnection: true + }; + const request: PipelineRequest = createPipelineRequest(requestOptions); + const res: PipelineResponse = await this._client.sendRequest(request); + + if (res.status >= 200 && res.status < 400) { + const dtdlAsString = res.bodyAsText || ""; + const parsedDtdl = JSON.parse(dtdlAsString); + return parsedDtdl; + } else { + throw new RestError("Error on HTTP Request in remote model fetcher", { + code: "ResourceNotFound", + statusCode: res.status, + response: res, + request: request + }); + } + } +} diff --git a/sdk/iot/modelsrepository/src/index.ts b/sdk/iot/modelsrepository/src/index.ts new file mode 100644 index 000000000000..f4f8258b1334 --- /dev/null +++ b/sdk/iot/modelsrepository/src/index.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +/** + * This is the ModelsRepositoryClient Library for JavaScript. + * + * @remarks + * This ModelsRepositoryClient is built around getting DTDL Models from a user-specified + * location. The two main variables are the repositoryLocation, which is a path or URI to either a remote + * or local repository where the models are located, and the dtmis, which can be one or more dtmis that + * will be mapped to specific models contained in the repository location that the user wishes to get. + * + * @example + * Inline code: + * ```typescript + * import lib + * import {ModelsRepositoryClient} from "../../../src"; + * + * const repositoryEndpoint = "devicemodels.azure.com"; + * const dtmi = process.argv[2] || "dtmi:azure:DeviceManagement:DeviceInformation;1"; + * + * console.log(repositoryEndpoint, dtmi); + * + * async function main() { + * const client = new ModelsRepositoryClient({repositoryLocation: repositoryEndpoint}); + * const result = await client.getModels(dtmi, {dependencyResolution: 'tryFromExpanded'}); + * console.log(result); + * } + * + * main().catch((err) => { + * console.error("The sample encountered an error:", err); + * }); + * + * ``` + * + * @packageDocumentation + */ + +export { ModelsRepositoryClient } from "./modelsRepositoryClient"; +export { GetModelsOptions } from "./interfaces/getModelsOptions"; +export { ModelsRepositoryClientOptions } from "./interfaces/modelsRepositoryClientOptions"; +export { dependencyResolutionType } from "./dependencyResolutionType"; +export { ModelError } from "./exceptions"; +export { getModelUri, isValidDtmi } from "./dtmiConventions"; diff --git a/sdk/iot/modelsrepository/src/interfaces/getModelsOptions.ts b/sdk/iot/modelsrepository/src/interfaces/getModelsOptions.ts new file mode 100644 index 000000000000..949ffed8fd48 --- /dev/null +++ b/sdk/iot/modelsrepository/src/interfaces/getModelsOptions.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +import { OperationOptions } from "@azure/core-client"; +import { dependencyResolutionType } from "../dependencyResolutionType"; + +/** + * Options for getting models. + */ +export interface GetModelsOptions extends OperationOptions { + /** + * This is the format of dependency resolution (no dependency resolution, standard dependency + * resolution, resolution using the expanded json format) that will be used when retrieving + * models. This overwrites the default dependencyResolution settings of the client. + */ + dependencyResolution?: dependencyResolutionType; +} diff --git a/sdk/iot/modelsrepository/src/interfaces/modelsRepositoryClientOptions.ts b/sdk/iot/modelsrepository/src/interfaces/modelsRepositoryClientOptions.ts new file mode 100644 index 000000000000..2a0204945679 --- /dev/null +++ b/sdk/iot/modelsrepository/src/interfaces/modelsRepositoryClientOptions.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +import { CommonClientOptions } from "@azure/core-client"; +import { dependencyResolutionType } from "../dependencyResolutionType"; + +/** + * Options for creating a Pipeline to use with ModelsRepositoryClient. + * It serves to configure the client itself, for instance by specifying + * the repository location to use on any getModels call. + */ +export interface ModelsRepositoryClientOptions extends CommonClientOptions { + /** + * This is the base location (URI or path) that requests will be made against for this client. + */ + repositoryLocation?: string; + /** + * Though currently not relevant, can be used in future iterations to specify the API Version + * of the service. + */ + apiVersion?: string; + + /** + * This is the format of dependency resolution (no dependency resolution, standard dependency + * resolution, resolution using the expanded json format) that will be used when retrieving + * models. + */ + dependencyResolution?: dependencyResolutionType; +} diff --git a/sdk/iot/modelsrepository/src/logger.ts b/sdk/iot/modelsrepository/src/logger.ts new file mode 100644 index 000000000000..b04bd5f34807 --- /dev/null +++ b/sdk/iot/modelsrepository/src/logger.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { createClientLogger } from "@azure/logger"; + +/** + * The @azure/logger configuration for this package. + */ +export const logger = createClientLogger("iot-modelsrepository"); diff --git a/sdk/iot/modelsrepository/src/modelsRepositoryClient.ts b/sdk/iot/modelsrepository/src/modelsRepositoryClient.ts new file mode 100644 index 000000000000..29a86ee11abf --- /dev/null +++ b/sdk/iot/modelsrepository/src/modelsRepositoryClient.ts @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +import { + DEFAULT_API_VERSION, + DEFAULT_REPOSITORY_LOCATION, + DEFAULT_USER_AGENT, + DEPENDENCY_MODE_DISABLED, + DEPENDENCY_MODE_ENABLED, + DEPENDENCY_MODE_TRY_FROM_EXPANDED +} from "./constants"; +import { createClientPipeline, InternalClientPipelineOptions } from "@azure/core-client"; +import { Fetcher } from "./fetcherAbstract"; +import { URL } from "./utils/url"; +import { isLocalPath, normalize } from "./utils/path"; +import { FilesystemFetcher } from "./fetcherFilesystem"; +import { dependencyResolutionType } from "./dependencyResolutionType"; +import { DtmiResolver } from "./dtmiResolver"; +import { PseudoParser } from "./psuedoParser"; +import { ModelsRepositoryClientOptions } from "./interfaces/modelsRepositoryClientOptions"; +import { logger } from "./logger"; +import { IoTModelsRepositoryServiceClient } from "./modelsRepositoryServiceClient"; +import { HttpFetcher } from "./fetcherHTTP"; +import { GetModelsOptions } from "./interfaces/getModelsOptions"; +import { DTDL } from "./psuedoDtdl"; + +/** + * Initializes a new instance of the IoT Models Repository Client. + */ +export class ModelsRepositoryClient { + private _repositoryLocation: string; + private _dependencyResolution: dependencyResolutionType; + private _apiVersion: string; + private _fetcher: Fetcher; + private _resolver: DtmiResolver; + private _pseudoParser: PseudoParser; + + /** + * The ModelsRepositoryClient constructor + * @param options - The models repository client options that govern the behavior of the client. + */ + constructor(options: ModelsRepositoryClientOptions = {}) { + this._repositoryLocation = options.repositoryLocation || DEFAULT_REPOSITORY_LOCATION; + logger.info(`Client configured for repository location ${this._repositoryLocation}`); + this._dependencyResolution = + options.dependencyResolution || + this._checkDefaultDependencyResolution(!!options.repositoryLocation); + logger.info(`Client configured for dependency mode: ${this._dependencyResolution}`); + this._fetcher = this._createFetcher(this._repositoryLocation, options); + this._resolver = new DtmiResolver(this._fetcher); + this._pseudoParser = new PseudoParser(this._resolver); + + // Store api version here (for now). Currently doesn't do anything + this._apiVersion = options.apiVersion || DEFAULT_API_VERSION; + } + + /** + * improves the readability of the constructor. + * based on a boolean returns the proper dependency resolution setting string. + */ + private _checkDefaultDependencyResolution(customRepository: boolean): dependencyResolutionType { + if (customRepository) { + return "enabled"; + } else { + return "tryFromExpanded"; + } + } + + /** + * Though currently not relevant, can specify API Version for communicating with + * the service. + */ + get apiVersion() { + return this._apiVersion; + } + + /** + * Configured repository location for this instance. Will be used as the endpoint to get the models from. + */ + get repositoryLocation() { + return this._repositoryLocation; + } + + /** + * Configured type of dependency resolution for this instance. Dictates how the client deals with model dependencies. + */ + get dependencyResolution() { + return this._dependencyResolution; + } + + /** + * Because of the local / remote optionality of this client, the service client + * must be dynamically generated based on the repository location. If the provided + * repository location is a remote location, then this private method will be used + * to create the IoT Models Repository Service Client. + */ + private _createClient(options: ModelsRepositoryClientOptions): IoTModelsRepositoryServiceClient { + const { ...pipelineOptions } = options; + + if (!pipelineOptions.userAgentOptions) { + pipelineOptions.userAgentOptions = {}; + } + if (pipelineOptions.userAgentOptions.userAgentPrefix) { + pipelineOptions.userAgentOptions.userAgentPrefix = `${pipelineOptions.userAgentOptions.userAgentPrefix} ${DEFAULT_USER_AGENT}`; + } else { + pipelineOptions.userAgentOptions.userAgentPrefix = DEFAULT_USER_AGENT; + } + + const internalPipelineOptions: InternalClientPipelineOptions = { + ...pipelineOptions, + ...{ + loggingOptions: { + logger: logger.info + } + } + }; + + const pipeline = createClientPipeline(internalPipelineOptions); + const client = new IoTModelsRepositoryServiceClient(this._repositoryLocation, { pipeline }); + return client; + } + + /** + * The fetcher is an abstraction necessary since this client can communicate with remote or local + * Model Repositories based on the provided location. It will analyze the provided location based + * on that create either an HTTP Fetcher, which uses the IoT Models Repository Service Client, + * or a Filesystem Fetcher. + */ + private _createFetcher(location: string, options: ModelsRepositoryClientOptions): Fetcher { + let locationURL; + let fetcher; + if (isLocalPath(location)) { + // POSIX Filesystem Path or Windows Filesystem Path + logger.info(`Repository location identified as filesystem path - using FilesystemFetcher`); + fetcher = new FilesystemFetcher(normalize(location)); + } else { + locationURL = new URL(location); + const prot = locationURL.protocol; + if (prot.includes("http") || prot.includes("https")) { + logger.info(`Repository location identified as HTTP/HTTPS endpoint - using HttpFetcher`); + const client = this._createClient(options); + fetcher = new HttpFetcher(location, client); + } else if (prot.includes("file")) { + // filesystem URI + logger.info("Repository Location identified as filesystem URI - using FilesystemFetcher"); + fetcher = new FilesystemFetcher(location); + } else if (prot === "" && location.search(/\.[a-zA-Z]{2,63}$/)) { + // Web URL with protocol unspecified - default to HTTPS + logger.info( + "Repository Location identified as remote endpoint without protocol specified - using HttpFetcher" + ); + const fLocation = "https://" + location; + const client = this._createClient(options); + fetcher = new HttpFetcher(fLocation, client); + } else { + throw new EvalError(`Unable to identify location: ${location}`); + } + } + + return fetcher; + } + + /** + * Retrieve one or more models based upon on or more provided dtmis. + * @param {string} dtmis - one dtmi represented as a string + * @param {GetModelsOptions} options - options to govern behavior of model getter. + * @returns {Promise<{ [dtmi: string]: unknown}>} + */ + async getModels(dtmis: string, options?: GetModelsOptions): Promise<{ [dtmi: string]: unknown }>; + /** + * Retrieve one or more models based upon on or more provided dtmis. + * @param {string[]} dtmis - dtmi strings in an array. + * @param {GetModelsOptions} options - options to govern behavior of model getter. + * @returns {Promise<{ [dtmi: string]: unknown}>} + */ + async getModels( + dtmis: string[], + options?: GetModelsOptions + ): Promise<{ [dtmi: string]: unknown }>; + async getModels( + dtmis: string | string[], + options?: GetModelsOptions + ): Promise<{ [dtmi: string]: unknown }> { + let modelMap: { [dtmi: string]: unknown }; + if (!Array.isArray(dtmis)) { + dtmis = [dtmis]; + } + + const dependencyResolution = options?.dependencyResolution || this._dependencyResolution; + + if (dependencyResolution === DEPENDENCY_MODE_DISABLED) { + logger.info("Getting models w/ dependency resolution mode: disabled"); + logger.info(`Retreiving model(s): ${dtmis}...`); + modelMap = await this._resolver.resolve(dtmis, false, options); + } else if (dependencyResolution === DEPENDENCY_MODE_ENABLED) { + logger.info(`Getting models w/ dependency resolution mode: enabled`); + logger.info(`Retreiving model(s): ${dtmis}...`); + const baseModelMap = await this._resolver.resolve(dtmis, false, options); + const baseModelList = Object.keys(baseModelMap).map((key) => baseModelMap[key]); + logger.info(`Retreiving model dependencies for ${dtmis}...`); + modelMap = await this._pseudoParser.expand(baseModelList, false); + } else if (dependencyResolution === DEPENDENCY_MODE_TRY_FROM_EXPANDED) { + logger.info(`Getting models w/ dependency resolution mode: tryFromExpanded`); + try { + logger.info(`Retreiving expanded model(s): ${dtmis}...`); + modelMap = await this._resolver.resolve(dtmis, true, options); + } catch (e) { + if (e.name === "RestError" && e.code === "ResouceNotFound") { + let baseModelMap: { [dtmi: string]: unknown }; + logger.info("Could not retrieve model(s) from expanded model DTDL - "); + baseModelMap = await this._resolver.resolve(dtmis, false, options); + const baseModelList = Object.keys(baseModelMap).map((key) => baseModelMap[key]); + logger.info(`Retreiving model dependencies for ${dtmis}...`); + modelMap = await this._pseudoParser.expand(baseModelList as DTDL[], true); + } else { + throw e; + } + } + } else { + throw EvalError(`Invalid dependency resolution mode: ${dependencyResolution}`); + } + + return modelMap; + } +} diff --git a/sdk/iot/modelsrepository/src/modelsRepositoryServiceClient.ts b/sdk/iot/modelsrepository/src/modelsRepositoryServiceClient.ts new file mode 100644 index 000000000000..7b374e673f92 --- /dev/null +++ b/sdk/iot/modelsrepository/src/modelsRepositoryServiceClient.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +import { ServiceClientOptions, ServiceClient } from "@azure/core-client"; +import { DEFAULT_API_VERSION } from "./constants"; + +interface IoTModelsRepositoryServiceClientOptions extends ServiceClientOptions { + // API Version to be used during HTTP Calls. + version?: string; + // Endpoint that will be base of URLs for HTTP calls. + endpoint?: string; +} + +/** + * @internal + */ +export class IoTModelsRepositoryServiceClient extends ServiceClient { + url: string; + version: string; + + /** + * Initializes a new instance of the IoTModelsRepositoryServiceClient class. + * @param url The URL of the service account or table that is the target of the desired operation. + * @param options The parameter options + */ + constructor(url: string, options: IoTModelsRepositoryServiceClientOptions = {}) { + const defaults: IoTModelsRepositoryServiceClientOptions = { + baseUri: `${url}`, + requestContentType: "application/json; charset=utf-8" + }; + + const optionsWithDefaults = { + ...defaults, + ...options + }; + + super(optionsWithDefaults); + + this.url = url; + this.version = options.version || DEFAULT_API_VERSION; + } +} diff --git a/sdk/iot/modelsrepository/src/psuedoDtdl.ts b/sdk/iot/modelsrepository/src/psuedoDtdl.ts new file mode 100644 index 000000000000..7c3426f6a81b --- /dev/null +++ b/sdk/iot/modelsrepository/src/psuedoDtdl.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +interface Contents { + "@type": string; + name: string; + schema: string; +} + +/** + * @internal + */ +export interface DTDL { + "@context": any[]; + "@id": string; + extends: string | Array; + contents: Contents[]; +} diff --git a/sdk/iot/modelsrepository/src/psuedoParser.ts b/sdk/iot/modelsrepository/src/psuedoParser.ts new file mode 100644 index 000000000000..8cd32c691906 --- /dev/null +++ b/sdk/iot/modelsrepository/src/psuedoParser.ts @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +import { DTDL } from "./psuedoDtdl"; +import { logger } from "./logger"; +import { DtmiResolver } from "./dtmiResolver"; +import { RestError } from "@azure/core-rest-pipeline"; + +/** + * The PsuedoParser is an interesting implementation. Essentially, this + * codebase works in tandem with a Digital Twins Parser, which simultaneously + * defines the DTDL structure and validates models match the correct DTDL format. + * In lieu of using that Parser as a dependency (for a complex network of reasons), + * we implement this class, which kind of parses. Because it uses the resovler too, + * we can, during psuedo-parsing, identify any times we should resolve a dependency, + * and then resolve the dependencies until the dependency tree is fully resolved. + * + * @internal + */ +export class PseudoParser { + private _resolver: DtmiResolver; + + constructor(resolver: DtmiResolver) { + this._resolver = resolver; + } + + async expand(models: DTDL[], tryFromExpanded: boolean) { + let expandedMap: { [dtmi: string]: DTDL } = {}; + for (let i = 0; i < models.length; i++) { + const model: DTDL = models[i]; + if (model["@id"] !== undefined) { + expandedMap[model["@id"]] = model; + } else { + throw Error(`model ${model} does not contain @id member`); + } + await this._expand(model, expandedMap, tryFromExpanded); + } + return expandedMap; + } + + private async _expand(model: DTDL, modelMap: { [dtmi: string]: DTDL }, tryFromExpanded: boolean): Promise { + logger.info(`Expanding model: ${model["@id"]}`); + let dependencies = this._getModelDependencies(model); + let dependenciesToResolve = dependencies.filter((dependency: string) => { + return !(dependency in modelMap); + }); + if (dependenciesToResolve.length !== 0) { + logger.info(`Outstanding dependencies found: ${dependenciesToResolve}`); + let resolvedDependenciesMap: { [s: string]: unknown }; + try { + resolvedDependenciesMap = await this._resolver.resolve( + dependenciesToResolve, + tryFromExpanded + ); + } catch (e) { + if (e instanceof RestError) { + resolvedDependenciesMap = await this._resolver.resolve(dependenciesToResolve, false); + } else { + throw e; + } + } + Object.keys(resolvedDependenciesMap).forEach((key) => { + modelMap[key] = resolvedDependenciesMap[key] as DTDL; + }); + const promiseList: Promise[] = []; + Object.values(resolvedDependenciesMap).forEach((dependencyModel) => { + promiseList.push(this._expand(dependencyModel as DTDL, modelMap, tryFromExpanded)); + }); + await Promise.all(promiseList); + } + } + + private _getModelDependencies(model: DTDL) { + let dependencies = []; + + if (model.contents !== undefined) { + const contents = model.contents; + contents.forEach((element) => { + if ( + element["@type"] && + typeof element["@type"] === "string" && + element["@type"] === "Component" + ) { + if (element.schema && typeof element.schema === "string") { + dependencies.push(element.schema); + } + } + }); + } + + if (model.extends !== undefined) { + if (typeof model.extends === "string") { + dependencies.push(model.extends); + } else if (Array.isArray(model.extends)) { + model.extends.forEach((element) => { + if (typeof element === "string") { + dependencies.push(element); + } else if (typeof element === "object") { + dependencies.push(this._getModelDependencies(element)); + } + }); + } + } + + dependencies = Array.from(new Set(dependencies)); + return dependencies; + } +} diff --git a/sdk/iot/modelsrepository/src/utils/fetcherFilesystem.browser.ts b/sdk/iot/modelsrepository/src/utils/fetcherFilesystem.browser.ts new file mode 100644 index 000000000000..a3976e6451e4 --- /dev/null +++ b/sdk/iot/modelsrepository/src/utils/fetcherFilesystem.browser.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +export class FilesystemFetcher { + constructor() { + throw new Error("FilesystemFetcher is not supported in browser"); + } +} diff --git a/sdk/iot/modelsrepository/src/utils/path.browser.ts b/sdk/iot/modelsrepository/src/utils/path.browser.ts new file mode 100644 index 000000000000..af78e8b444e4 --- /dev/null +++ b/sdk/iot/modelsrepository/src/utils/path.browser.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +export function isLocalPath() { + return false; +} + +export function normalize(text: string) { + return text; +} diff --git a/sdk/iot/modelsrepository/src/utils/path.ts b/sdk/iot/modelsrepository/src/utils/path.ts new file mode 100644 index 000000000000..7e1fc058461b --- /dev/null +++ b/sdk/iot/modelsrepository/src/utils/path.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +import { isAbsolute, normalize } from "path"; + +function isLocalPath(p: string): boolean { + return isAbsolute(p); +} + +export { normalize, isLocalPath }; diff --git a/sdk/iot/modelsrepository/src/utils/url.browser.ts b/sdk/iot/modelsrepository/src/utils/url.browser.ts new file mode 100644 index 000000000000..2b9e17d383bd --- /dev/null +++ b/sdk/iot/modelsrepository/src/utils/url.browser.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +const url = URL; + +export { url as URL }; diff --git a/sdk/iot/modelsrepository/src/utils/url.ts b/sdk/iot/modelsrepository/src/utils/url.ts new file mode 100644 index 000000000000..71fcc22b49c0 --- /dev/null +++ b/sdk/iot/modelsrepository/src/utils/url.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export { URL } from "url"; diff --git a/sdk/iot/modelsrepository/test/browser/browserTest.spec.ts b/sdk/iot/modelsrepository/test/browser/browserTest.spec.ts new file mode 100644 index 000000000000..d23c445ee185 --- /dev/null +++ b/sdk/iot/modelsrepository/test/browser/browserTest.spec.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { expect } from "chai"; +import { ModelsRepositoryClient } from "../../src"; + +describe("resolver - browser", () => { + describe("single resolution (no pseudo-parsing)", () => { + it.only("integration works in browser", function(done) { + const dtmi: string = "dtmi:azure:DeviceManagement:DeviceInformation;1"; + const endpoint = "https://devicemodels.azure.com"; + const client = new ModelsRepositoryClient({ repositoryLocation: endpoint }); + const result = client.getModels(dtmi, { dependencyResolution: "tryFromExpanded" }); + result + .then((actualOutput: any) => { + expect(actualOutput["dtmi:azure:DeviceManagement:DeviceInformation;1"]).to.exist; + expect(actualOutput["dtmi:azure:DeviceManagement:DeviceInformation;1"]["@id"]).to.equal( + "dtmi:azure:DeviceManagement:DeviceInformation;1" + ); + done(); + }) + .catch((err: any) => done(err)); + }); + }); +}); diff --git a/sdk/iot/modelsrepository/test/node/integration/index.spec.ts b/sdk/iot/modelsrepository/test/node/integration/index.spec.ts new file mode 100644 index 000000000000..b28f5e2b6dcb --- /dev/null +++ b/sdk/iot/modelsrepository/test/node/integration/index.spec.ts @@ -0,0 +1,177 @@ +/* eslint-disable no-undef */ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { ModelsRepositoryClient, ModelsRepositoryClientOptions } from "../../../src"; + +import { assert, expect } from "chai"; +import * as sinon from "sinon"; + +import { dependencyResolutionType } from "../../../src/dependencyResolutionType"; +import { ServiceClient } from "@azure/core-client"; +import { PipelineRequest } from "@azure/core-rest-pipeline"; + +interface remoteResolutionScenario { + name: string; + clientOptions: { + dependencyResolution: dependencyResolutionType; + repositoryLocation: string; + }; + getModelsOptions: any; + dtmis: { + dtmi: string; + expectedUri: string; + mockedResponse: unknown; + expectedOutputJson: unknown; + }[]; +} + +const remoteResolutionScenarios: remoteResolutionScenario[] = [ + { + name: "dependencyResolution: disabled, single DTMI, no dependencies", + clientOptions: { + dependencyResolution: "disabled", + repositoryLocation: "https://www.devicemodels.contoso.com" + }, + getModelsOptions: {}, + dtmis: [ + { + dtmi: "dtmi:contoso:FakeDeviceManagement:DeviceInformation;1", + expectedUri: + "https://www.devicemodels.contoso.com/dtmi/contoso/fakedevicemanagement/deviceinformation-1.json", + mockedResponse: { + "@id": "dtmi:contoso:FakeDeviceManagement:DeviceInformation;1", + fakeDtdl: "fakeBodyAsText" + }, + expectedOutputJson: { + "@id": "dtmi:contoso:FakeDeviceManagement:DeviceInformation;1", + fakeDtdl: "fakeBodyAsText" + } + } + ] + }, + { + name: "dependencyResolution: enabled, single DTMI, no dependencies", + clientOptions: { + dependencyResolution: "enabled", + repositoryLocation: "https://www.devicemodels.contoso.com" + }, + getModelsOptions: {}, + dtmis: [ + { + dtmi: "dtmi:contoso:FakeDeviceManagement:DeviceInformation;1", + expectedUri: + "https://www.devicemodels.contoso.com/dtmi/contoso/fakedevicemanagement/deviceinformation-1.json", + mockedResponse: { + "@id": "dtmi:contoso:FakeDeviceManagement:DeviceInformation;1", + fakeDtdl: "fakeBodyAsText" + }, + expectedOutputJson: { + "@id": "dtmi:contoso:FakeDeviceManagement:DeviceInformation;1", + fakeDtdl: "fakeBodyAsText" + } + } + ] + }, + { + name: "dependencyResolution: tryFromExpanded, single DTMI, no dependencies", + clientOptions: { + dependencyResolution: "tryFromExpanded", + repositoryLocation: "https://www.devicemodels.contoso.com" + }, + getModelsOptions: {}, + dtmis: [ + { + dtmi: "dtmi:contoso:FakeDeviceManagement:DeviceInformation;1", + expectedUri: + "https://www.devicemodels.contoso.com/dtmi/contoso/fakedevicemanagement/deviceinformation-1.expanded.json", + mockedResponse: [ + { + "@id": "dtmi:contoso:FakeDeviceManagement:DeviceInformation;1", + fakeDtdl: "fakeBodyAsText" + } + ], + expectedOutputJson: { + "@id": "dtmi:contoso:FakeDeviceManagement:DeviceInformation;1", + fakeDtdl: "fakeBodyAsText" + } + } + ] + }, + { + name: "dependencyResolution: disabled, multiple DTMI, no dependencies", + clientOptions: { + dependencyResolution: "tryFromExpanded", + repositoryLocation: "https://www.devicemodels.contoso.com" + }, + getModelsOptions: {}, + dtmis: [ + { + dtmi: "dtmi:contoso:FakeDeviceManagement:DeviceInformation;1", + expectedUri: + "https://www.devicemodels.contoso.com/dtmi/contoso/fakedevicemanagement/deviceinformation-1.expanded.json", + mockedResponse: [ + { + "@id": "dtmi:contoso:FakeDeviceManagement:DeviceInformation;1", + fakeDtdl: "fakeBodyAsText" + } + ], + expectedOutputJson: { + "@id": "dtmi:contoso:FakeDeviceManagement:DeviceInformation;1", + fakeDtdl: "fakeBodyAsText" + } + }, + { + dtmi: "dtmi:com:FooFooFoo;4", + expectedUri: "https://www.devicemodels.contoso.com/dtmi/com/foofoofoo-4.expanded.json", + mockedResponse: [{ "@id": "dtmi:com:FooFooFoo;4", fakeDtdl: "fakeBodyAsText" }], + expectedOutputJson: { "@id": "dtmi:com:FooFooFoo;4", fakeDtdl: "fakeBodyAsText" } + } + ] + } +]; + +describe("resolver - node", function() { + afterEach(function() { + sinon.restore(); + }); + + describe("remote URL resolution", function() { + remoteResolutionScenarios.forEach((scenario: remoteResolutionScenario) => { + it(scenario.name, function(done) { + console.log(scenario.name); + let myStub = sinon.stub(ServiceClient.prototype, "sendRequest"); + for (let i = 0; i < scenario.dtmis.length; i++) { + myStub.onCall(i).callsFake((request: PipelineRequest) => { + expect(request.url, "URL not formatted for request correctly.").to.deep.equal( + scenario.dtmis[i].expectedUri + ); + const pipelineResponse: any = { + request: request, + bodyAsText: JSON.stringify(scenario.dtmis[i].mockedResponse), + status: 200, + headers: undefined + }; + return Promise.resolve(pipelineResponse); + }); + } + + const myOptions: ModelsRepositoryClientOptions = scenario.clientOptions; + const dtmiClient = new ModelsRepositoryClient(myOptions); + const listOfDtmis = scenario.dtmis.map((x) => x.dtmi); + const result = dtmiClient.getModels(listOfDtmis, scenario.getModelsOptions); + const expectedOutput: any = {}; + scenario.dtmis.forEach((element) => { + expectedOutput[element.dtmi] = element.expectedOutputJson; + }); + assert(result instanceof Promise, "resolve method did not return a promise"); + result + .then((actualOutput: any) => { + expect(actualOutput).to.deep.equal(expectedOutput); + done(); + }) + .catch((err: any) => done(err)); + }); + }); + }); +}); diff --git a/sdk/iot/modelsrepository/test/node/unit/constants.spec.ts b/sdk/iot/modelsrepository/test/node/unit/constants.spec.ts new file mode 100644 index 000000000000..0c450eaaf7c3 --- /dev/null +++ b/sdk/iot/modelsrepository/test/node/unit/constants.spec.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as cnst from "../../../src/constants"; +import { readFileSync } from "fs"; +import { expect } from "chai"; +describe("constants", function() { + it("uses same version as package.json", function() { + const pkgjson = readFileSync("./package.json", "utf-8"); + const pkgjsonVersion = JSON.parse(pkgjson).version; + expect(cnst.SDK_VERSION).to.equal(pkgjsonVersion); + }); +}); diff --git a/sdk/iot/modelsrepository/test/node/unit/dtmiConventions.spec.ts b/sdk/iot/modelsrepository/test/node/unit/dtmiConventions.spec.ts new file mode 100644 index 000000000000..34af79b412fa --- /dev/null +++ b/sdk/iot/modelsrepository/test/node/unit/dtmiConventions.spec.ts @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as lib from "../../../src/dtmiConventions"; + +import { assert, expect } from "chai"; + +interface TestCase { + dtmi: string; + valid: boolean; + expectedPath?: string; + expectedURL?: string; +} + +const testCases: TestCase[] = [ + { + dtmi: "dtmi:azure:DeviceManagement:DeviceInformation;1", + valid: true, + expectedPath: "dtmi/azure/devicemanagement/deviceinformation-1.json", + expectedURL: "https://contoso.com/dtmi/azure/devicemanagement/deviceinformation-1.json" + }, + { + dtmi: "dtmiazure:DeviceManagement:DeviceInformation;1", + valid: false + }, + { + dtmi: "dtmi:foobar:DeviceInformation;1", + valid: true, + expectedPath: "dtmi/foobar/deviceinformation-1.json", + expectedURL: "https://contoso.com/dtmi/foobar/deviceinformation-1.json" + } +]; + +const fakeBasePath = "https://contoso.com"; + +describe("dtmiConventions", function() { + testCases.forEach((testCase) => { + describe("isValidDtmi", function() { + if (testCase.valid) { + it(`valid dtmi - ${testCase.dtmi}`, function() { + const result = lib.isValidDtmi(testCase.dtmi); + assert(result, `${testCase.dtmi} was incorrectly labelled invalid.`); + }); + } else { + it(`invalid dtmi - ${testCase.dtmi}`, function() { + const result = lib.isValidDtmi(testCase.dtmi); + expect(result, `${testCase.dtmi} was incorrectly labelled as valid.`).to.be.false; + }); + } + }); + }); + + testCases.forEach((testCase) => { + describe("convertDtmiToPath", function() { + if (testCase.valid) { + it(`converts dtmi to path - ${testCase.dtmi}`, function() { + const result = lib.convertDtmiToPath(testCase.dtmi, false); + expect(result).to.deep.equal(testCase.expectedPath); + }); + it(`converts dtmi to expanded path - ${testCase.dtmi}`, function() { + const result = lib.convertDtmiToPath(testCase.dtmi, true); + const expected = testCase.expectedPath?.replace(".json", ".expanded.json"); + expect(result).to.deep.equal(expected); + }); + } else { + it(`throw error on invalid dtmi - ${testCase.dtmi}`, function() { + expect(() => { + lib.convertDtmiToPath(testCase.dtmi, false); + }).to.throw("DTMI provided is invalid. Ensure it follows DTMI conventions."); + }); + } + }); + }); + + testCases.forEach((testCase) => { + describe("getModelUri", function() { + if (testCase.valid) { + it(`generates model uri - ${testCase.dtmi}`, function() { + const result = lib.getModelUri(testCase.dtmi, fakeBasePath, false); + expect(result).to.equal(testCase.expectedURL); + }); + + it(`generates expanded model uri - ${testCase.dtmi}`, function() { + const result = lib.getModelUri(testCase.dtmi, fakeBasePath, true); + const expected = testCase.expectedURL?.replace(".json", ".expanded.json"); + expect(result).to.equal(expected); + }); + } else { + it("should fail if the dtmi is not formatted correctly", function() { + expect(() => { + lib.getModelUri(testCase.dtmi, fakeBasePath, false); + }).to.throw("DTMI provided is invalid. Ensure it follows DTMI conventions."); + }); + } + }); + }); +}); diff --git a/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/azure/DeviceManagement/deviceinformation-1.json b/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/azure/DeviceManagement/deviceinformation-1.json new file mode 100644 index 000000000000..37e4a39138e8 --- /dev/null +++ b/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/azure/DeviceManagement/deviceinformation-1.json @@ -0,0 +1,64 @@ +{ + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:azure:DeviceManagement:DeviceInformation;1", + "@type": "Interface", + "displayName": "Device Information", + "contents": [ + { + "@type": "Property", + "name": "manufacturer", + "displayName": "Manufacturer", + "schema": "string", + "description": "Company name of the device manufacturer. This could be the same as the name of the original equipment manufacturer (OEM). Ex. Contoso." + }, + { + "@type": "Property", + "name": "model", + "displayName": "Device model", + "schema": "string", + "description": "Device model name or ID. Ex. Surface Book 2." + }, + { + "@type": "Property", + "name": "swVersion", + "displayName": "Software version", + "schema": "string", + "description": "Version of the software on your device. This could be the version of your firmware. Ex. 1.3.45" + }, + { + "@type": "Property", + "name": "osName", + "displayName": "Operating system name", + "schema": "string", + "description": "Name of the operating system on the device. Ex. Windows 10 IoT Core." + }, + { + "@type": "Property", + "name": "processorArchitecture", + "displayName": "Processor architecture", + "schema": "string", + "description": "Architecture of the processor on the device. Ex. x64 or ARM." + }, + { + "@type": "Property", + "name": "processorManufacturer", + "displayName": "Processor manufacturer", + "schema": "string", + "description": "Name of the manufacturer of the processor on the device. Ex. Intel." + }, + { + "@type": "Property", + "name": "totalStorage", + "displayName": "Total storage", + "schema": "double", + "description": "Total available storage on the device in kilobytes. Ex. 2048000 kilobytes." + }, + { + "@type": "Property", + "name": "totalMemory", + "displayName": "Total memory", + "schema": "double", + "description": "Total available memory on the device in kilobytes. Ex. 256000 kilobytes." + } + ] +} diff --git a/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/deviceinformation-1.json b/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/deviceinformation-1.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/temperaturecontroller-1.expanded.json b/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/temperaturecontroller-1.expanded.json new file mode 100644 index 000000000000..cde19984abb6 --- /dev/null +++ b/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/temperaturecontroller-1.expanded.json @@ -0,0 +1,203 @@ +[ + { + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:contoso:FakeDeviceManagement:TemperatureController;1", + "@type": "Interface", + "displayName": "Temperature Controller", + "description": "Device with two thermostats and remote reboot.", + "contents": [ + { + "@type": ["Telemetry", "DataSize"], + "name": "workingSet", + "displayName": "Working Set", + "description": "Current working set of the device memory in KiB.", + "schema": "double", + "unit": "kibibyte" + }, + { + "@type": "Property", + "name": "serialNumber", + "displayName": "Serial Number", + "description": "Serial number of the device.", + "schema": "string" + }, + { + "@type": "Command", + "name": "reboot", + "displayName": "Reboot", + "description": "Reboots the device after waiting the number of seconds specified.", + "request": { + "name": "delay", + "displayName": "Delay", + "description": "Number of seconds to wait before rebooting the device.", + "schema": "integer" + } + }, + { + "@type": "Component", + "schema": "dtmi:contoso:FakeDeviceManagement:Thermostat;1", + "name": "thermostat1", + "displayName": "Thermostat One", + "description": "Thermostat One of Two." + }, + { + "@type": "Component", + "schema": "dtmi:contoso:FakeDeviceManagement:Thermostat;1", + "name": "thermostat2", + "displayName": "Thermostat Two", + "description": "Thermostat Two of Two." + }, + { + "@type": "Component", + "schema": "dtmi:azure:DeviceManagement:DeviceInformation;1", + "name": "deviceInformation", + "displayName": "Device Information interface", + "description": "Optional interface with basic device hardware information." + } + ] + }, + { + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:contoso:FakeDeviceManagement:Thermostat;1", + "@type": "Interface", + "displayName": "Thermostat", + "description": "Reports current temperature and provides desired temperature control.", + "contents": [ + { + "@type": ["Telemetry", "Temperature"], + "name": "temperature", + "displayName": "Temperature", + "description": "Temperature in degrees Celsius.", + "schema": "double", + "unit": "degreeCelsius" + }, + { + "@type": ["Property", "Temperature"], + "name": "targetTemperature", + "schema": "double", + "displayName": "Target Temperature", + "description": "Allows to remotely specify the desired target temperature.", + "unit": "degreeCelsius", + "writable": true + }, + { + "@type": ["Property", "Temperature"], + "name": "maxTempSinceLastReboot", + "schema": "double", + "unit": "degreeCelsius", + "displayName": "Max temperature since last reboot.", + "description": "Returns the max temperature since last device reboot." + }, + { + "@type": "Command", + "name": "getMaxMinReport", + "displayName": "Get Max-Min report.", + "description": "This command returns the max, min and average temperature from the specified time to the current time.", + "request": { + "name": "since", + "displayName": "Since", + "description": "Period to return the max-min report.", + "schema": "dateTime" + }, + "response": { + "name": "tempReport", + "displayName": "Temperature Report", + "schema": { + "@type": "Object", + "fields": [ + { + "name": "maxTemp", + "displayName": "Max temperature", + "schema": "double" + }, + { + "name": "minTemp", + "displayName": "Min temperature", + "schema": "double" + }, + { + "name": "avgTemp", + "displayName": "Average Temperature", + "schema": "double" + }, + { + "name": "startTime", + "displayName": "Start Time", + "schema": "dateTime" + }, + { + "name": "endTime", + "displayName": "End Time", + "schema": "dateTime" + } + ] + } + } + } + ] + }, + { + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:azure:DeviceManagement:DeviceInformation;1", + "@type": "Interface", + "displayName": "Device Information", + "contents": [ + { + "@type": "Property", + "name": "manufacturer", + "displayName": "Manufacturer", + "schema": "string", + "description": "Company name of the device manufacturer. This could be the same as the name of the original equipment manufacturer (OEM). Ex. Contoso." + }, + { + "@type": "Property", + "name": "model", + "displayName": "Device model", + "schema": "string", + "description": "Device model name or ID. Ex. Surface Book 2." + }, + { + "@type": "Property", + "name": "swVersion", + "displayName": "Software version", + "schema": "string", + "description": "Version of the software on your device. This could be the version of your firmware. Ex. 1.3.45" + }, + { + "@type": "Property", + "name": "osName", + "displayName": "Operating system name", + "schema": "string", + "description": "Name of the operating system on the device. Ex. Windows 10 IoT Core." + }, + { + "@type": "Property", + "name": "processorArchitecture", + "displayName": "Processor architecture", + "schema": "string", + "description": "Architecture of the processor on the device. Ex. x64 or ARM." + }, + { + "@type": "Property", + "name": "processorManufacturer", + "displayName": "Processor manufacturer", + "schema": "string", + "description": "Name of the manufacturer of the processor on the device. Ex. Intel." + }, + { + "@type": "Property", + "name": "totalStorage", + "displayName": "Total storage", + "schema": "double", + "description": "Total available storage on the device in kilobytes. Ex. 2048000 kilobytes." + }, + { + "@type": "Property", + "name": "totalMemory", + "displayName": "Total memory", + "schema": "double", + "description": "Total available memory on the device in kilobytes. Ex. 256000 kilobytes." + } + ] + } +] diff --git a/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/temperaturecontroller-1.json b/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/temperaturecontroller-1.json new file mode 100644 index 000000000000..1527a6dbe1ae --- /dev/null +++ b/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/temperaturecontroller-1.json @@ -0,0 +1,57 @@ +{ + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:contoso:FakeDeviceManagement:TemperatureController;1", + "@type": "Interface", + "displayName": "Temperature Controller", + "description": "Device with two thermostats and remote reboot.", + "contents": [ + { + "@type": ["Telemetry", "DataSize"], + "name": "workingSet", + "displayName": "Working Set", + "description": "Current working set of the device memory in KiB.", + "schema": "double", + "unit": "kibibyte" + }, + { + "@type": "Property", + "name": "serialNumber", + "displayName": "Serial Number", + "description": "Serial number of the device.", + "schema": "string" + }, + { + "@type": "Command", + "name": "reboot", + "displayName": "Reboot", + "description": "Reboots the device after waiting the number of seconds specified.", + "request": { + "name": "delay", + "displayName": "Delay", + "description": "Number of seconds to wait before rebooting the device.", + "schema": "integer" + } + }, + { + "@type": "Component", + "schema": "dtmi:contoso:FakeDeviceManagement:Thermostat;1", + "name": "thermostat1", + "displayName": "Thermostat One", + "description": "Thermostat One of Two." + }, + { + "@type": "Component", + "schema": "dtmi:contoso:FakeDeviceManagement:Thermostat;1", + "name": "thermostat2", + "displayName": "Thermostat Two", + "description": "Thermostat Two of Two." + }, + { + "@type": "Component", + "schema": "dtmi:azure:DeviceManagement:DeviceInformation;1", + "name": "deviceInformation", + "displayName": "Device Information interface", + "description": "Optional interface with basic device hardware information." + } + ] +} diff --git a/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/temperaturecontroller-2.json b/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/temperaturecontroller-2.json new file mode 100644 index 000000000000..32cbc7cfd588 --- /dev/null +++ b/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/temperaturecontroller-2.json @@ -0,0 +1,57 @@ +{ + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:contoso:FakeDeviceManagement:TemperatureController;2", + "@type": "Interface", + "displayName": "Temperature Controller", + "description": "Device with two thermostats and remote reboot.", + "contents": [ + { + "@type": ["Telemetry", "DataSize"], + "name": "workingSet", + "displayName": "Working Set", + "description": "Current working set of the device memory in KiB.", + "schema": "double", + "unit": "kibibyte" + }, + { + "@type": "Property", + "name": "serialNumber", + "displayName": "Serial Number", + "description": "Serial number of the device.", + "schema": "string" + }, + { + "@type": "Command", + "name": "reboot", + "displayName": "Reboot", + "description": "Reboots the device after waiting the number of seconds specified.", + "request": { + "name": "delay", + "displayName": "Delay", + "description": "Number of seconds to wait before rebooting the device.", + "schema": "integer" + } + }, + { + "@type": "Component", + "schema": "dtmi:contoso:FakeDeviceManagement:Thermostat;1", + "name": "thermostat1", + "displayName": "Thermostat One", + "description": "Thermostat One of Two." + }, + { + "@type": "Component", + "schema": "dtmi:contoso:FakeDeviceManagement:Thermostat;1", + "name": "thermostat2", + "displayName": "Thermostat Two", + "description": "Thermostat Two of Two." + }, + { + "@type": "Component", + "schema": "dtmi:azure:DeviceManagement:DeviceInformation;1", + "name": "deviceInformation", + "displayName": "Device Information interface", + "description": "Optional interface with basic device hardware information." + } + ] +} diff --git a/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/thermostat-1.expanded.json b/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/thermostat-1.expanded.json new file mode 100644 index 000000000000..bae336740ac9 --- /dev/null +++ b/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/thermostat-1.expanded.json @@ -0,0 +1,82 @@ +[ + { + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:contoso:FakeDeviceManagement:Thermostat;1", + "@type": "Interface", + "displayName": "Thermostat", + "description": "Reports current temperature and provides desired temperature control.", + "contents": [ + { + "@type": ["Telemetry", "Temperature"], + "name": "temperature", + "displayName": "Temperature", + "description": "Temperature in degrees Celsius.", + "schema": "double", + "unit": "degreeCelsius" + }, + { + "@type": ["Property", "Temperature"], + "name": "targetTemperature", + "schema": "double", + "displayName": "Target Temperature", + "description": "Allows to remotely specify the desired target temperature.", + "unit": "degreeCelsius", + "writable": true + }, + { + "@type": ["Property", "Temperature"], + "name": "maxTempSinceLastReboot", + "schema": "double", + "unit": "degreeCelsius", + "displayName": "Max temperature since last reboot.", + "description": "Returns the max temperature since last device reboot." + }, + { + "@type": "Command", + "name": "getMaxMinReport", + "displayName": "Get Max-Min report.", + "description": "This command returns the max, min and average temperature from the specified time to the current time.", + "request": { + "name": "since", + "displayName": "Since", + "description": "Period to return the max-min report.", + "schema": "dateTime" + }, + "response": { + "name": "tempReport", + "displayName": "Temperature Report", + "schema": { + "@type": "Object", + "fields": [ + { + "name": "maxTemp", + "displayName": "Max temperature", + "schema": "double" + }, + { + "name": "minTemp", + "displayName": "Min temperature", + "schema": "double" + }, + { + "name": "avgTemp", + "displayName": "Average Temperature", + "schema": "double" + }, + { + "name": "startTime", + "displayName": "Start Time", + "schema": "dateTime" + }, + { + "name": "endTime", + "displayName": "End Time", + "schema": "dateTime" + } + ] + } + } + } + ] + } +] diff --git a/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/thermostat-1.json b/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/thermostat-1.json new file mode 100644 index 000000000000..7e902ab5b36f --- /dev/null +++ b/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/thermostat-1.json @@ -0,0 +1,80 @@ +{ + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:contoso:FakeDeviceManagement:Thermostat;1", + "@type": "Interface", + "displayName": "Thermostat", + "description": "Reports current temperature and provides desired temperature control.", + "contents": [ + { + "@type": ["Telemetry", "Temperature"], + "name": "temperature", + "displayName": "Temperature", + "description": "Temperature in degrees Celsius.", + "schema": "double", + "unit": "degreeCelsius" + }, + { + "@type": ["Property", "Temperature"], + "name": "targetTemperature", + "schema": "double", + "displayName": "Target Temperature", + "description": "Allows to remotely specify the desired target temperature.", + "unit": "degreeCelsius", + "writable": true + }, + { + "@type": ["Property", "Temperature"], + "name": "maxTempSinceLastReboot", + "schema": "double", + "unit": "degreeCelsius", + "displayName": "Max temperature since last reboot.", + "description": "Returns the max temperature since last device reboot." + }, + { + "@type": "Command", + "name": "getMaxMinReport", + "displayName": "Get Max-Min report.", + "description": "This command returns the max, min and average temperature from the specified time to the current time.", + "request": { + "name": "since", + "displayName": "Since", + "description": "Period to return the max-min report.", + "schema": "dateTime" + }, + "response": { + "name": "tempReport", + "displayName": "Temperature Report", + "schema": { + "@type": "Object", + "fields": [ + { + "name": "maxTemp", + "displayName": "Max temperature", + "schema": "double" + }, + { + "name": "minTemp", + "displayName": "Min temperature", + "schema": "double" + }, + { + "name": "avgTemp", + "displayName": "Average Temperature", + "schema": "double" + }, + { + "name": "startTime", + "displayName": "Start Time", + "schema": "dateTime" + }, + { + "name": "endTime", + "displayName": "End Time", + "schema": "dateTime" + } + ] + } + } + } + ] +} diff --git a/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/thermostat-2.json b/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/thermostat-2.json new file mode 100644 index 000000000000..3c9027e825cc --- /dev/null +++ b/sdk/iot/modelsrepository/test/node/unit/testRepository/dtmi/contoso/FakeDeviceManagement/thermostat-2.json @@ -0,0 +1,80 @@ +{ + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:com:example:Thermostat;2", + "@type": "Interface", + "displayName": "Thermostat", + "description": "Reports current temperature and provides desired temperature control.", + "contents": [ + { + "@type": ["Telemetry", "Temperature"], + "name": "temperature", + "displayName": "Temperature", + "description": "Temperature in degrees Celsius.", + "schema": "double", + "unit": "degreeCelsius" + }, + { + "@type": ["Property", "Temperature"], + "name": "targetTemperature", + "schema": "double", + "displayName": "Target Temperature", + "description": "Allows to remotely specify the desired target temperature.", + "unit": "degreeCelsius", + "writable": true + }, + { + "@type": ["Property", "Temperature"], + "name": "maxTempSinceLastReboot", + "schema": "double", + "unit": "degreeCelsius", + "displayName": "Max temperature since last reboot.", + "description": "Returns the max temperature since last device reboot." + }, + { + "@type": "Command", + "name": "getMaxMinReport", + "displayName": "Get Max-Min report.", + "description": "This command returns the max, min and average temperature from the specified time to the current time.", + "request": { + "name": "since", + "displayName": "Since", + "description": "Period to return the max-min report.", + "schema": "dateTime" + }, + "response": { + "name": "tempReport", + "displayName": "Temperature Report", + "schema": { + "@type": "Object", + "fields": [ + { + "name": "maxTemp", + "displayName": "Max temperature", + "schema": "double" + }, + { + "name": "minTemp", + "displayName": "Min temperature", + "schema": "double" + }, + { + "name": "avgTemp", + "displayName": "Average Temperature", + "schema": "double" + }, + { + "name": "startTime", + "displayName": "Start Time", + "schema": "dateTime" + }, + { + "name": "endTime", + "displayName": "End Time", + "schema": "dateTime" + } + ] + } + } + } + ] +} diff --git a/sdk/iot/modelsrepository/tsconfig.json b/sdk/iot/modelsrepository/tsconfig.json new file mode 100644 index 000000000000..1483294dd54b --- /dev/null +++ b/sdk/iot/modelsrepository/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.package", + "compilerOptions": { + "outDir": "./dist-esm", + "declarationDir": "./types", + "paths": { + "@azure/iot-modelsrepository": ["./src/index"] + } + }, + "include": ["src/**/*.ts", "test/**/*.ts", "samples-dev/**/*.ts"] +}