├── index.js ├── samples ├── .cfignore ├── .eslintignore ├── manifest.yml ├── .eslintrc.json ├── package.json ├── sample_1_packages.js ├── README.md ├── sample_5_express_application.js ├── sample_4_thing_data.js ├── sample_2_property_set_types.js ├── sample_6_data_ingestion_iot_services.js ├── sample_3_thing_setup.js ├── helper │ └── IoTServicesHelper.js └── sample_7_multi_tenant_model_migration.js ├── test ├── integration │ ├── .integration-mocharc.json │ ├── step_4_update.test.js │ ├── step_0_1_basics.test.js │ ├── step_0_2_cleanup.test.js │ ├── step_1_create.test.js │ ├── helper │ │ ├── requestHelper.js │ │ └── DataHelper.js │ ├── step_3_read.test.js │ ├── step_6_delete.test.js │ ├── step_2_readAll.test.js │ └── step_5_data.test.js └── unit │ ├── certs │ ├── public.pem │ └── private.pem │ ├── index.test.js │ ├── services │ ├── PackageService.test.js │ ├── AuthorizationService.test.js │ ├── TimeSeriesColdStoreService.test.js │ ├── PropertySetTypeService.test.js │ ├── TimeSeriesStoreService.test.js │ ├── TimeSeriesAggregateStoreService.test.js │ ├── ThingService.test.js │ └── ThingTypeService.test.js │ ├── auth │ ├── Token.test.js │ └── Authenticator.test.js │ ├── utils │ ├── Navigator.test.js │ └── ConfigurationProvider.test.js │ ├── AssertionUtil.js │ ├── mocha.env.js │ └── LeonardoIoT.test.js ├── .mocharc.json ├── .editorconfig ├── .github ├── workflows │ ├── reuse.yml │ ├── pr-samples.yml │ ├── library.yml │ └── pr-library.yml └── dependabot.yml ├── .nycrc.json ├── sonar-project.properties ├── .eslintignore ├── .gitignore ├── SECURITY.md ├── .eslintrc.json ├── CHANGELOG.md ├── lib ├── auth │ ├── Token.js │ └── Authenticator.js ├── utils │ ├── Navigator.js │ └── ConfigurationProvider.js ├── services │ ├── PackageService.js │ ├── TimeSeriesStoreService.js │ ├── TimeSeriesColdStoreService.js │ ├── TimeSeriesAggregateStoreService.js │ ├── AuthorizationService.js │ ├── ThingTypeService.js │ ├── PropertySetTypeService.js │ └── ThingService.js └── LeonardoIoT.js ├── package.json ├── .reuse └── dep5 ├── MIGRATION-GUIDE.md ├── CONTRIBUTING.md └── LICENSE.md /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/LeonardoIoT'); 2 | -------------------------------------------------------------------------------- /samples/.cfignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | !node_modules/@sap 3 | -------------------------------------------------------------------------------- /samples/.eslintignore: -------------------------------------------------------------------------------- 1 | .env 2 | .npm 3 | default-env*.json 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /test/integration/.integration-mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "diff": true, 3 | "extension": [ 4 | "js" 5 | ], 6 | "package": "../package.json", 7 | "reporter": "spec", 8 | "slow": 1000, 9 | "timeout": 60000 10 | } 11 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "diff": true, 3 | "extension": [ 4 | "js" 5 | ], 6 | "package": "./package.json", 7 | "reporter": "spec", 8 | "recursive": true, 9 | "slow": 75, 10 | "timeout": 2000, 11 | "require": [ 12 | "./test/unit/mocha.env.js" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /samples/manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: sap-iot-sdk-js-sample 4 | command: node --inspect sample_5_express_application.js 5 | buildpacks: 6 | - https://github.com/cloudfoundry/nodejs-buildpack#v1.6.34 7 | instances: 1 8 | memory: 128MB 9 | services: 10 | - 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | max_line_length = 100 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_size = 8 17 | indent_style = tab 18 | -------------------------------------------------------------------------------- /test/integration/step_4_update.test.js: -------------------------------------------------------------------------------- 1 | const LeonardoIoT = require('../../lib/LeonardoIoT'); 2 | 3 | describe('4) UPDATE', function () { 4 | // eslint-disable-next-line mocha/no-hooks-for-single-case 5 | before(function () { 6 | // eslint-disable-next-line no-new 7 | new LeonardoIoT(); 8 | }); 9 | 10 | it('none', function () { }); 11 | }); 12 | -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | name: REUSE Compliance 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: [main] 8 | 9 | jobs: 10 | reuse: 11 | runs-on: ubuntu-latest 12 | name: Check REUSE Compliance 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Run REUSE Check 16 | uses: fsfe/reuse-action@v1 17 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "check-coverage": true, 4 | "reporter": [ 5 | "lcov", 6 | "text-summary", 7 | "json-summary" 8 | ], 9 | "exclude": [ 10 | "samples/**", 11 | "**/test/**", 12 | "coverage/**" 13 | ], 14 | "report-dir": "coverage", 15 | "branches": 90, 16 | "lines": 90, 17 | "functions": 90, 18 | "statements": 90 19 | } 20 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.organization=sap-1 2 | sonar.projectKey=sap-iot-sdk-nodejs 3 | sonar.projectName=SAP IoT SDK Node.js 4 | sonar.exclusions=node_modules/**,coverage/**,lint/** 5 | sonar.javascript.lcov.reportPaths=coverage/lcov.info 6 | sonar.eslint.reportPaths=lint/results.json 7 | sonar.coverage.exclusions=coverage/**,sample/** 8 | sonar.sources=index.js,lib 9 | sonar.tests=test 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .env 2 | .npm 3 | default-env*.json 4 | package-lock.json 5 | 6 | # Exclude samples, as they will be linted with an own file 7 | /samples 8 | 9 | # Coverage directory used by tools like istanbul 10 | coverage 11 | 12 | # nyc test coverage 13 | .nyc_output 14 | 15 | # Sonar Scan 16 | .scannerwork 17 | 18 | # Lint Result output from ESLint 19 | lint 20 | 21 | # eslint ignores hidden files by default 22 | !.* 23 | **/node_modules 24 | 25 | # Visual Studio Code 26 | .vscode 27 | 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 3 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | open-pull-requests-limit: 3 14 | 15 | - package-ecosystem: "npm" 16 | directory: "/samples" 17 | schedule: 18 | interval: "monthly" 19 | open-pull-requests-limit: 3 20 | -------------------------------------------------------------------------------- /test/unit/certs/public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyp0tzyIJXpPYJxbkOzaX 3 | Xahj90c6wJqeLkTFiPZu8yF0jQTPtIIEFwSRetJqImI+iJ9EaF0ZsemzZiptMlrV 4 | TBZec2JM4pJv/OAvJeT8I7EKu57IXDeGos+Pxjj04SBqCnCvIvtlwdsCTzotRUv2 5 | fEL7NJXzxtpxQ8AQRkEQ4+FAIe/yGX8cP/dIoXwbdM6NvkDU3QHcjHMPdZ6/s+sI 6 | +E2orFv4qYTj8NYqymXeJeWe31xSL/fJaX3Wo0NaoZuyh0MJOvA0D7bWqKw/ZBF7 7 | A05PiqozeAzhYeSvQSsNQ2dc9tmDadLTF8Q9BwURgejGOpvAxdJ3wEVWTohC3Sv6 8 | nQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment for local development 2 | .env 3 | default-env*.json 4 | 5 | # Packages 6 | .tgz 7 | 8 | # Dependency directories 9 | node_modules 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | 16 | # IntelliJ 17 | .idea 18 | *.iml 19 | 20 | # Optional npm cache directory 21 | .npm 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Sonar Scan 30 | .scannerwork 31 | 32 | # Lint Result output from ESLint 33 | lint 34 | 35 | # Visual Studio Code 36 | .vscode 37 | -------------------------------------------------------------------------------- /samples/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "plugins": [ 8 | "no-loops", 9 | "node", 10 | "json-format" 11 | ], 12 | "extends": [ 13 | "airbnb-base", 14 | "eslint:recommended", 15 | "plugin:node/recommended" 16 | ], 17 | "rules": { 18 | "no-underscore-dangle": "off", 19 | "no-console": "off", 20 | "comma-dangle": [ 21 | "error", 22 | "always-multiline" 23 | ], 24 | "max-len": [ 25 | "error", 26 | { 27 | "code": 120 28 | } 29 | ], 30 | "quotes": [ 31 | "error", 32 | "single" 33 | ], 34 | "no-loops/no-loops": "error" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/unit/index.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const LeonardoIoT = require('../..'); 3 | 4 | describe('SDK Default Export', function () { 5 | it('should successfully create instance of sdk', function () { 6 | const testClient = new LeonardoIoT({ 7 | uaa: { 8 | clientid: 'testId', 9 | clientsecret: 'testSecret', 10 | url: 'https://test.authentication.eu10.hana.ondemand.com', 11 | }, 12 | endpoints: { 13 | 'appiot-mds': 'https://appiot-mds.cfapps.eu10.hana.ondemand.com', 14 | 'config-thing-sap': 'https://config-thing-sap.cfapps.eu10.hana.ondemand.com', 15 | }, 16 | }); 17 | assert.notStrictEqual(testClient, undefined, 'Invalid constructor for LeonardoIoT client'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | We take security issues in our projects seriously. We appreciate your efforts to responsibly disclose your findings. 3 | Please do not report security issues directly on GitHub but using one of the channels listed below. This allows us to provide a fix before an issue can be exploited. 4 | 5 | - **Researchers/Non-SAP Customers:** Please consult SAPs [disclosure guidelines](https://wiki.scn.sap.com/wiki/display/PSR/Disclosure+Guidelines+for+SAP+Security+Advisories) and send the related information in a PGP encrypted e-mail to secure@sap.com. Find the public PGP key [here](https://www.sap.com/dmc/policies/pgp/keyblock.txt). 6 | - **SAP Customers:** If the security issue is not covered by a published security note, please report it by creating a customer message at https://launchpad.support.sap.com. 7 | 8 | Please also refer to the general [SAP security information page](https://www.sap.com/about/trust-center/security/incident-management.html). 9 | -------------------------------------------------------------------------------- /samples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sap-iot-sdk-samples", 3 | "version": "1.0.0", 4 | "description": "Samples for usage of SAP Leonardo IoT SDK", 5 | "license": "Apache-2.0", 6 | "author": "SAP SE", 7 | "scripts": { 8 | "checkDependencies": "depcheck .", 9 | "lint": "eslint --ext .js,.json .", 10 | "lint:fix": "eslint --fix --ext .js,.json .", 11 | "start": "node --inspect server.js", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "dependencies": { 15 | "async-mqtt": "2.6.1", 16 | "debug": "^4.3.1", 17 | "express": "^4.17.1", 18 | "request": "^2.88.0", 19 | "request-promise-native": "^1.0.8", 20 | "sap-iot-sdk": "https://github.com:SAP/sap-iot-sdk-nodejs#v0.2.0" 21 | }, 22 | "devDependencies": { 23 | "depcheck": "^1.4.3", 24 | "eslint": "^8.33.0", 25 | "eslint-config-airbnb-base": "^15.0.0", 26 | "eslint-plugin-import": "^2.27.5", 27 | "eslint-plugin-json-format": "^2.0.1", 28 | "eslint-plugin-no-loops": "^0.3.0", 29 | "eslint-plugin-node": "^11.1.0" 30 | }, 31 | "engines": { 32 | "node": "^10" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/integration/step_0_1_basics.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const LeonardoIoT = require('../../lib/LeonardoIoT'); 3 | 4 | describe('Basics', function () { 5 | let client; 6 | 7 | before(function () { 8 | client = new LeonardoIoT(); 9 | }); 10 | 11 | it('should get a token', async function () { 12 | const token = await client.authenticator.getToken(); 13 | assert(token.getScopes().length > 0); 14 | }); 15 | 16 | it('should get a token with specific scopes', async function () { 17 | const token = await client.authenticator.getToken(); 18 | assert(token.getScopes().length > 2); 19 | 20 | const requiredScopes = token.getScopes().slice(0, 2); 21 | const newToken = await client.authenticator.getToken(requiredScopes); 22 | assert.strictEqual(newToken.getScopes().length, 2); 23 | requiredScopes.forEach((scope) => { 24 | assert(newToken.getScopes().includes(scope)); 25 | }); 26 | 27 | const readThingsScopes = token.getScopes().filter((scope) => scope.match('thing!t[1-9]*.r')); 28 | const response = await client.getThings(null, { resolveWithFullResponse: true, scopes: readThingsScopes }); 29 | assert.strictEqual(response.statusCode, 200); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/integration/step_0_2_cleanup.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const LeonardoIoT = require('../../lib/LeonardoIoT'); 3 | const DataHelper = require('./helper/DataHelper'); 4 | const requestHelper = require('./helper/requestHelper'); 5 | 6 | describe('Cleanup and Prepare', function () { 7 | let client; 8 | 9 | // eslint-disable-next-line mocha/no-hooks-for-single-case 10 | before(async function () { 11 | client = new LeonardoIoT(); 12 | await DataHelper.init(client); 13 | }); 14 | 15 | // eslint-disable-next-line consistent-return 16 | it('should do cleanup', async function () { 17 | this.timeout(180000); 18 | const package = await client.getPackage(DataHelper.package().Name).catch(() => { 19 | assert.ok(true, 'Package not found'); 20 | }); 21 | if (package) { 22 | await requestHelper.deletePackageCascading(client, DataHelper.package().Name); 23 | } 24 | const objectGroupResponse = await client.getObjectGroups({ 25 | $filter: `name eq ${DataHelper.objectGroup().name}`, 26 | }); 27 | const deleteObjectGroupPromises = objectGroupResponse.value.map((objectGroup) => client.deleteObjectGroup(objectGroup.objectGroupID, objectGroup.etag)); 28 | return Promise.all(deleteObjectGroupPromises); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/integration/step_1_create.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const LeonardoIoT = require('../../lib/LeonardoIoT'); 3 | const DataHelper = require('./helper/DataHelper'); 4 | 5 | describe('Create Entities', function () { 6 | let client; 7 | 8 | before(function () { 9 | client = new LeonardoIoT(); 10 | }); 11 | 12 | it('should create a package', async function () { 13 | const response = await client.createPackage(DataHelper.package()); 14 | assert.strictEqual(response.d.Status, 'Active'); 15 | }); 16 | 17 | it('should create a property set type', async function () { 18 | const response = await client.createPropertySetType(DataHelper.package().Name, DataHelper.propertySetType()); 19 | assert.strictEqual(response.d.Name, DataHelper.propertySetType().Name); 20 | }); 21 | 22 | it('should create a thing type', async function () { 23 | const response = await client.createThingType(DataHelper.package().Name, DataHelper.thingType()); 24 | assert.strictEqual(response.d.Name, DataHelper.thingType().Name); 25 | }); 26 | 27 | it('should create a object group', function () { 28 | return client.createObjectGroup(DataHelper.objectGroup()); 29 | }); 30 | 31 | it('should create a thing', function () { 32 | return client.createThing(DataHelper.thing()); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "mocha": true, 6 | "es6": true 7 | }, 8 | "plugins": [ 9 | "mocha", 10 | "node", 11 | "no-loops", 12 | "json-format", 13 | "jsdoc" 14 | ], 15 | "extends": [ 16 | "airbnb-base", 17 | "eslint:recommended", 18 | "plugin:mocha/recommended", 19 | "plugin:jsdoc/recommended", 20 | "plugin:node/recommended" 21 | ], 22 | "rules": { 23 | "jsdoc/require-hyphen-before-param-description": "warn", 24 | "jsdoc/require-throws": "warn", 25 | "jsdoc/check-indentation": "warn", 26 | "jsdoc/require-jsdoc": "off", 27 | "no-underscore-dangle": "off", 28 | "comma-dangle": [ 29 | "error", 30 | "always-multiline" 31 | ], 32 | "max-len": [ 33 | "error", 34 | { 35 | "code": 120, 36 | "ignorePattern": ".*\\*\\s.*" 37 | } 38 | ], 39 | "quotes": [ 40 | "error", 41 | "single" 42 | ], 43 | "no-loops/no-loops": "error" 44 | }, 45 | "overrides": [ 46 | { 47 | "files": [ 48 | "*.test.js", 49 | "*.spec.js" 50 | ], 51 | "rules": { 52 | "max-len": [ 53 | "error", 54 | { 55 | "code": 160 56 | } 57 | ], 58 | "prefer-arrow-callback": "off", 59 | "mocha/prefer-arrow-callback": "error", 60 | "func-names": "off" 61 | } 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for SAP IoT SDK for Node.js 2 | 3 | ## 1.0.0 (01/2023) 4 | * Bump dependency versions 5 | * Fix eslint errors 6 | * Introduce dependabot 7 | * Remove events functionality (service deprecated and removed) 8 | * Remove support for Node.Js version 10 and 12 9 | * Performance enhancements for the workflows 10 | 11 | ## 0.2.1 (03/2021) 12 | * Enhance internal usage of query parameters 13 | * Fix Sonar code smells 14 | 15 | ## 0.2.0 (03/2021) 16 | * Enhance eslint setup 17 | * Introduce automatic GH Action for linting 18 | * Introduce automatic dependency check 19 | * Fix setup of samples 20 | * Fix eslint findings 21 | * Fix deprecated assert APIs 22 | * Bump unit test coverage 23 | * Enhance integeation tests 24 | 25 | ## 0.1.6 (03/2021) 26 | * Introduce scope handling on request level 27 | 28 | ## 0.1.5 (01/2021) 29 | * Add timeseries store recalculate aggregates API 30 | * Add timeseries store snapshot v2 API 31 | 32 | ## 0.1.4 (12/2019) 33 | * Add event instance APIs 34 | 35 | ## 0.1.3 (10/2019) 36 | * Enable manual client configuration via Leonardo IoT constructor arguments 37 | * Add SDK version to User-Agent header field of requests 38 | 39 | ## 0.1.2 (08/2019) 40 | * Rename of module 41 | * Fix broken samples 42 | 43 | ## 0.1.1 (08/2019) 44 | * Add missing JS documentation 45 | 46 | ## 0.1.0-beta (08/2019) 47 | * Initial commit 48 | * Add migration guide 49 | -------------------------------------------------------------------------------- /.github/workflows/pr-samples.yml: -------------------------------------------------------------------------------- 1 | name: PR Samples 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | paths: 7 | - "samples/**" 8 | - ".github/workflows/pr-samples.yml" 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | name: Lint samples 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Setup Node 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: lts/* 20 | check-latest: true 21 | cache: 'npm' 22 | - name: Install dependencies 23 | run: npm install 24 | working-directory: ./samples 25 | - name: Lint samples 26 | run: npm run lint 27 | working-directory: ./samples 28 | 29 | dependency-check: 30 | runs-on: ubuntu-latest 31 | name: Check samples dependencies 32 | steps: 33 | - uses: actions/checkout@v3 34 | - name: Setup Node 35 | uses: actions/setup-node@v3 36 | with: 37 | node-version: lts/* 38 | check-latest: true 39 | cache: 'npm' 40 | - name: Install dependencies 41 | run: npm install 42 | working-directory: ./samples 43 | - name: Run dependency check 44 | run: npm run checkDependencies 45 | working-directory: ./samples 46 | 47 | check-markdown-links: 48 | runs-on: ubuntu-latest 49 | name: Check for dead links in markdown documents 50 | steps: 51 | - uses: actions/checkout@v3 52 | - name: Run markdown link check 53 | uses: gaurav-nelson/github-action-markdown-link-check@v1 54 | with: 55 | folder-path: "samples" 56 | -------------------------------------------------------------------------------- /test/unit/certs/private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAyp0tzyIJXpPYJxbkOzaXXahj90c6wJqeLkTFiPZu8yF0jQTP 3 | tIIEFwSRetJqImI+iJ9EaF0ZsemzZiptMlrVTBZec2JM4pJv/OAvJeT8I7EKu57I 4 | XDeGos+Pxjj04SBqCnCvIvtlwdsCTzotRUv2fEL7NJXzxtpxQ8AQRkEQ4+FAIe/y 5 | GX8cP/dIoXwbdM6NvkDU3QHcjHMPdZ6/s+sI+E2orFv4qYTj8NYqymXeJeWe31xS 6 | L/fJaX3Wo0NaoZuyh0MJOvA0D7bWqKw/ZBF7A05PiqozeAzhYeSvQSsNQ2dc9tmD 7 | adLTF8Q9BwURgejGOpvAxdJ3wEVWTohC3Sv6nQIDAQABAoIBAGQgptm85Upy35f5 8 | rRJCGS10oOo7riIuhsswuznDJvNJ7jIeVZLLyb+iR06eG0sgp+yWYJT+pUsRxdFQ 9 | WCRRVSVDzKtSwdIaMfOSyln8vknZHQe5ISTJX+SnlFKOJR34Cc9c/n/YIuJZG9wR 10 | UPv03TttUn0bOZxkqSKlEKXXU8R40+oddC9aIrgBjqrxkj5MCQwAEeZZ/iJpFbIX 11 | oIUtCo56BAaOLaq3uFL82e+JJCVZ71b6MmnjVH5pmU+5ZIRXygBzWxc7S6vJp2OU 12 | ZddsL1jnL6369pbH4GcN+6VcsPPQFca/s2a6rEogdrsYlDNZU5wygYOKeybC5RGG 13 | BW0bo1UCgYEA8K+KZ/aYve6wHt7AzgpRujVu8KfzaAQL9aHR+KtoaQFMotp1/q4Q 14 | 8kpocfT9aBYCfNpoWwjmb+4njVkYJaPWTzvBKKi98g9lAph5Y9uFSFoPfcIpiApf 15 | LeKeNNrcBLx5siVpBp5zPCpGmwW4zqujLzrD4tkvwcPZIjGlDgTP6RcCgYEA14F/ 16 | wZJLTJpD3JYDuJnLE8PvnuXpFlU0RE3hg4f67JQDOV4xKVSYgmfjNrVOwjGDDqmC 17 | tUC+SXo2wf7PfYIm0Jpj9nIiC/E+TZd37ES4QD+DB6aCMtdBBaPqxgi/38afLgRK 18 | 5p1+4gl1X7TWDGBPalPTndnSuYRzALD93k6DomsCgYBU62qnAb+ki9nCGo/mYoex 19 | bnblHCzqTzs1AFJILZoaKmYys2ecYygBhLVTN8BNAC8ChED7lalQZrO30G6PoT3V 20 | GN0vDlJuXHYkM03pKKPfAj+i/GWQ5S/tzZ5KQcoNNb6uVzq2JiO8X6Inwh4RdUeP 21 | O2mv2TdbY1FyGhgFWTdOIwKBgB/qk+stKW284xQGO6LZVBQnTpOv5SdjkwwSpLWA 22 | LA9hlGCorIs8diHKFQKDM5jbEFfZjvwdDJGBQh7VCBHEds8qLmfPW299WQbQyff5 23 | 7XfGcDdv15SEAKM/NYQCw7f2iJieFiG1bZc4Y2O7OoA2u0w6Afs8yVKkZQvTKqYD 24 | 5sblAoGAd/fh3JfIP9vVYe0lFFzVJ7MOJ+XQ0jWJgGyRyxbeygG2wBo0d73IWFzk 25 | EH5wLUQNW3RCEDFn/L4880OomMxGpq+uUNaSHZ18P3u6CpNt40NY5Pc8HGM28J1G 26 | QS62TMZhZLhdteWM8z3vvR8wtJr9RMAZBInZDGDfvGZdCUhLpkQ= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /lib/auth/Token.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('SAPIoT:Token'); 2 | const jwt = require('jwt-simple'); 3 | 4 | /** 5 | * Class representing a JWT token 6 | * 7 | * @class Authenticator 8 | * @author Lukas Brinkmann, Jan Reichert, Soeren Buehler 9 | */ 10 | class Token { 11 | /** 12 | * Create a Token 13 | * 14 | * @param {string} accessToken - The JWT token. 15 | * @param {number} expiresIn - The number of seconds in which the token expires. 16 | */ 17 | constructor(accessToken, expiresIn) { 18 | debug('Creating a new Token'); 19 | this.accessToken = accessToken; 20 | this.decodedToken = jwt.decode(accessToken, '', true); 21 | const currentTime = new Date().getTime() / 1000; // current time since 1.1.1970 in seconds 22 | this.expiresAt = currentTime + expiresIn; 23 | } 24 | 25 | /** 26 | * Get the JWT token. 27 | * 28 | * @returns {string} The JWT token. 29 | */ 30 | getAccessToken() { 31 | debug('Getting AccessToken from token'); 32 | return this.accessToken; 33 | } 34 | 35 | /** 36 | * Get array of token scopes 37 | * 38 | * @returns {string[]} List of token scopes 39 | */ 40 | getScopes() { 41 | debug('Getting Scopes from token'); 42 | let tokenScopes = []; 43 | if (this.decodedToken.scope) { 44 | tokenScopes = this.decodedToken.scope; 45 | } 46 | return tokenScopes; 47 | } 48 | 49 | /** 50 | * Indicates if the token is expired. 51 | * 52 | * @returns {boolean} True, if the stored token is expired 53 | */ 54 | isExpired() { 55 | debug('Checking the stored token regarding expiration'); 56 | const currentTime = new Date().getTime() / 1000; // current time since 1.1.1970 in seconds 57 | return this.expiresAt < currentTime; 58 | } 59 | } 60 | 61 | module.exports = Token; 62 | -------------------------------------------------------------------------------- /test/integration/helper/requestHelper.js: -------------------------------------------------------------------------------- 1 | function determineTenantPrefix(client) { 2 | return client.request({ 3 | url: `${client.navigator.businessPartner()}/Tenants`, 4 | }).then((response) => response.value[0].package); 5 | } 6 | 7 | async function deletePackageCascading(client, packageName) { 8 | const thingTypes = await client.getThingTypesByPackage(packageName).then((result) => result.d.results); 9 | const thingTypeDeletePromises = thingTypes.map(async (thingType) => { 10 | const thingsResponse = await client.getThingsByThingType(thingType.Name).then((result) => result.value); 11 | const thingDeletePromises = thingsResponse.map(async (thing) => client.deleteThing(thing._id)); 12 | await Promise.all(thingDeletePromises); 13 | const thingTypeResponse = await client.getThingType(thingType.Name, {}, { resolveWithFullResponse: true }); 14 | return client.deleteThingType(thingType.Name, thingTypeResponse.headers.etag); 15 | }); 16 | await Promise.all(thingTypeDeletePromises); 17 | 18 | const psts = await client.getPropertySetTypesByPackage(packageName).then((result) => result.d.results); 19 | psts.sort((a, b) => { 20 | if (a.DataCategory === 'ReferencePropertyData') { 21 | return -1; 22 | } 23 | return b.DataCategory === 'ReferencePropertyData' ? 1 : 0; 24 | }); 25 | const pstDeletePromises = psts.map(async (pst) => { 26 | const pstResponse = await client.getPropertySetType(pst.Name, {}, { resolveWithFullResponse: true }); 27 | return client.deletePropertySetType(pst.Name, pstResponse.headers.etag); 28 | }); 29 | await Promise.all(pstDeletePromises); 30 | const packageResponse = await client.getPackage(packageName, { resolveWithFullResponse: true }); 31 | return client.deletePackage(packageName, packageResponse.headers.etag); 32 | } 33 | 34 | module.exports = { 35 | determineTenantPrefix, 36 | deletePackageCascading, 37 | }; 38 | -------------------------------------------------------------------------------- /samples/sample_1_packages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This example shows different package service calls 3 | */ 4 | 5 | const LeonardoIoT = require('sap-iot-sdk'); 6 | 7 | const client = new LeonardoIoT(); 8 | let tenantPackagePrefix; 9 | 10 | /* 11 | * Identify current operating tenant and add prefix to package name 12 | */ 13 | async function determineTenant() { 14 | const tenantInfo = await client.request({ 15 | url: `${client.navigator.businessPartner()}/Tenants`, 16 | }); 17 | 18 | tenantPackagePrefix = tenantInfo.value[0].package; 19 | } 20 | 21 | async function runPackageOperations() { 22 | console.log('Start creation of sample packages'); 23 | 24 | // Create package 25 | await client.createPackage({ 26 | Name: `${tenantPackagePrefix}.sdk.sample.package1`, 27 | Scope: 'private', 28 | }); 29 | 30 | // Copy existing package 31 | await client.createPackage({ 32 | Name: `${tenantPackagePrefix}.sdk.sample.package2`, 33 | Scope: 'tenant', 34 | }); 35 | 36 | // Read existing sample packages 37 | const response = await client.getPackages({ 38 | $filter: `startswith(Name,'${tenantPackagePrefix}.sdk.sample.package')`, 39 | }); 40 | const packages = response.d.results; 41 | console.log(`Number of existing sample packages: ${packages.length}`); 42 | 43 | packages.forEach(async (packageObject) => { 44 | // Get etag for each package 45 | const packageResponse = await client.getPackage(packageObject.Name, { resolveWithFullResponse: true }); 46 | // Delete created packages 47 | await client.deletePackage(packageObject.Name, packageResponse.headers.etag); 48 | console.log(`Package successfully deleted: ${packageObject.Name}`); 49 | }); 50 | } 51 | 52 | // Entry point of script 53 | (async () => { 54 | try { 55 | await determineTenant(); 56 | await runPackageOperations(); 57 | } catch (err) { 58 | console.log(err); 59 | } 60 | })(); 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sap-iot-sdk", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "SDK which simplifies the interaction between an client / server application and SAP IoT.", 6 | "keywords": [ 7 | "SAP", 8 | "IoT", 9 | "SDK" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/SAP/sap-iot-sdk-nodejs" 14 | }, 15 | "license": "Apache-2.0", 16 | "author": "SAP SE", 17 | "main": "index.js", 18 | "files": [ 19 | "NOTICE.md", 20 | "LICENSE", 21 | "README.md", 22 | "index.js", 23 | "lib/" 24 | ], 25 | "scripts": { 26 | "checkDependencies": "depcheck .", 27 | "lint": "eslint --max-warnings=0 --ext .js,.json .", 28 | "lint:ci": "eslint -f json -o lint/results.json --max-warnings=0 --ext .js,.json .", 29 | "lint:fix": "eslint --max-warnings=0 --fix --ext .js,.json .", 30 | "prepareRelease": "npm prune --production", 31 | "test": "nyc mocha test/unit", 32 | "test:integration": "cross-env DEBUG=LeonardoIoT nyc --check-coverage=false mocha test/integration --sort --bail --config ./test/integration/.integration-mocharc.json" 33 | }, 34 | "dependencies": { 35 | "@sap/xsenv": "^3.1.1", 36 | "@sap/xssec": "^3.2.10", 37 | "debug": "^4.3.2", 38 | "jwt-simple": "^0.5.6", 39 | "request": "^2.88.2", 40 | "request-promise-native": "^1.0.9" 41 | }, 42 | "devDependencies": { 43 | "cross-env": "^7.0.3", 44 | "depcheck": "^1.4.3", 45 | "eslint": "^8.33.0", 46 | "eslint-config-airbnb-base": "^15.0.0", 47 | "eslint-plugin-import": "^2.27.5", 48 | "eslint-plugin-jsdoc": "^39.7.5", 49 | "eslint-plugin-json-format": "^2.0.1", 50 | "eslint-plugin-mocha": "^10.1.0", 51 | "eslint-plugin-no-loops": "^0.3.0", 52 | "eslint-plugin-node": "^11.1.0", 53 | "mocha": "^10.2.0", 54 | "nock": "^13.2.1", 55 | "nyc": "^15.1.0", 56 | "proxyquire": "^2.1.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: sap-iot-sdk 3 | Upstream-Contact: SAP IoT Open Source SDK Team 4 | Source: https://github.com/SAP/sap-iot-sdk-nodejs 5 | Disclaimer: The code in this project may include calls to APIs (“API Calls”) of 6 | SAP or third-party products or services developed outside of this project 7 | (“External Products”). 8 | “APIs” means application programming interfaces, as well as their respective 9 | specifications and implementing code that allows software to communicate with 10 | other software. 11 | API Calls to External Products are not licensed under the open source license 12 | that governs this project. The use of such API Calls and related External 13 | Products are subject to applicable additional agreements with the relevant 14 | provider of the External Products. In no event shall the open source license 15 | that governs this project grant any rights in or to any External Products,or 16 | alter, expand or supersede any terms of the applicable additional agreements. 17 | If you have a valid license agreement with SAP for the use of a particular SAP 18 | External Product, then you may make use of any API Calls included in this 19 | project’s code for that SAP External Product, subject to the terms of such 20 | license agreement. If you do not have a valid license agreement for the use of 21 | a particular SAP External Product, then you may only make use of any API Calls 22 | in this project for that SAP External Product for your internal, non-productive 23 | and non-commercial test and evaluation of such API Calls. Nothing herein grants 24 | you any rights to use or access any SAP External Product, or provide any third 25 | parties the right to use of access any SAP External Product, through API Calls. 26 | 27 | Files: * 28 | Copyright: 2019-2021 SAP SE or an SAP affiliate company and Project "SAP IoT SDK" contributors 29 | License: Apache-2.0 30 | -------------------------------------------------------------------------------- /test/integration/step_3_read.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const LeonardoIoT = require('../../lib/LeonardoIoT'); 3 | const DataHelper = require('./helper/DataHelper'); 4 | 5 | describe('Read Single Entities', function () { 6 | let client; 7 | 8 | before(function () { 9 | client = new LeonardoIoT(); 10 | }); 11 | 12 | it('should read a package', async function () { 13 | const response = await client.getPackage(DataHelper.package().Name); 14 | assert.strictEqual(response.d.Name, DataHelper.package().Name); 15 | }); 16 | 17 | it('should read a property set type', async function () { 18 | const response = await client.getPropertySetType(DataHelper.propertySetType().Name); 19 | assert.strictEqual(response.d.Name, DataHelper.propertySetType().Name); 20 | }); 21 | 22 | it('should read a thing type', async function () { 23 | const response = await client.getThingType(DataHelper.thingType().Name); 24 | assert.strictEqual(response.d.Name, DataHelper.thingType().Name); 25 | }); 26 | 27 | it('should read a object group', async function () { 28 | const response = await client.getObjectGroup(DataHelper.data.objectGroup.objectGroupID); 29 | assert.strictEqual(response.objectGroupID, DataHelper.data.objectGroup.objectGroupID); 30 | }); 31 | 32 | it('should read a root object group', async function () { 33 | const response = await client.getRootObjectGroup(); 34 | assert.strictEqual(response.objectGroupID, DataHelper.rootObjectGroup.objectGroupID); 35 | }); 36 | 37 | it('should read a thing', async function () { 38 | const response = await client.getThing(DataHelper.data.thing._id); 39 | assert.strictEqual(response._id, DataHelper.data.thing._id); 40 | }); 41 | 42 | it('should read a thing by alternate id', async function () { 43 | const response = await client.getThingByAlternateId(DataHelper.data.thing._alternateId); 44 | assert.strictEqual(response._alternateId, DataHelper.data.thing._alternateId); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/integration/step_6_delete.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const LeonardoIoT = require('../../lib/LeonardoIoT'); 3 | const DataHelper = require('./helper/DataHelper'); 4 | 5 | describe('Delete Entities', function () { 6 | let client; 7 | 8 | before(function () { 9 | client = new LeonardoIoT(); 10 | }); 11 | 12 | it('should delete all things', async function () { 13 | const things = await client.getThingsByThingType(DataHelper.thingType().Name); 14 | assert(things.value.length > 0, 'No thing found for deletion'); 15 | const deleteThingPromises = things.value.map((thing) => client.deleteThing(thing._id)); 16 | return Promise.all(deleteThingPromises); 17 | }); 18 | 19 | it('should delete all object groups', async function () { 20 | const objectGroups = await client.getObjectGroups({ 21 | $filter: `name eq ${DataHelper.objectGroup().name}`, 22 | }); 23 | const deleteObjectGroupPromises = objectGroups.value.map((objectGroup) => client.deleteObjectGroup(objectGroup.objectGroupID, objectGroup.etag)); 24 | return Promise.all(deleteObjectGroupPromises); 25 | }); 26 | 27 | it('should delete a thing types', async function () { 28 | const thingTypeResponse = await client.getThingType(DataHelper.thingType().Name, {}, { resolveWithFullResponse: true }); 29 | return client.deleteThingType(DataHelper.thingType().Name, thingTypeResponse.headers.etag); 30 | }); 31 | 32 | it('should delete a property set types', async function () { 33 | const propertySetTypeResponse = await client.getPropertySetType(DataHelper.propertySetType().Name, {}, { resolveWithFullResponse: true }); 34 | return client.deletePropertySetType(DataHelper.propertySetType().Name, propertySetTypeResponse.headers.etag); 35 | }); 36 | 37 | it('should delete a package', async function () { 38 | const packageResponse = await client.getPackage(DataHelper.package().Name, { resolveWithFullResponse: true }); 39 | return client.deletePackage(DataHelper.package().Name, packageResponse.headers.etag); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/unit/services/PackageService.test.js: -------------------------------------------------------------------------------- 1 | const AssertionUtil = require('../AssertionUtil'); 2 | const LeonardoIoT = require('../../../lib/LeonardoIoT'); 3 | 4 | const configPackageUrl = 'https://config-package-sap.cfapps.eu10.hana.ondemand.com'; 5 | 6 | describe('Package Service', function () { 7 | let client; 8 | 9 | beforeEach(function () { 10 | client = new LeonardoIoT(); 11 | }); 12 | 13 | describe('Package', function () { 14 | it('should create new package', function () { 15 | const packagePayload = { Name: 'MyPackage' }; 16 | client.request = (requestConfig) => { 17 | AssertionUtil.assertRequestConfig(requestConfig, { 18 | url: `${configPackageUrl}/Package/v1/Packages`, 19 | method: 'POST', 20 | body: packagePayload, 21 | }); 22 | }; 23 | 24 | return client.createPackage(packagePayload); 25 | }); 26 | 27 | it('should read single package', function () { 28 | const packageName = 'MyPackage'; 29 | client.request = (requestConfig) => { 30 | AssertionUtil.assertRequestConfig(requestConfig, { 31 | url: `${configPackageUrl}/Package/v1/Packages('${packageName}')`, 32 | }); 33 | }; 34 | 35 | return client.getPackage(packageName); 36 | }); 37 | 38 | it('should read multiple packages', function () { 39 | client.request = (requestConfig) => { 40 | AssertionUtil.assertRequestConfig(requestConfig, { 41 | url: `${configPackageUrl}/Package/v1/Packages`, 42 | }); 43 | }; 44 | 45 | return client.getPackages(); 46 | }); 47 | 48 | it('should delete package', function () { 49 | const packageName = 'MyPackage'; 50 | const etag = '8f9da184-5af1-4237-8ede-a7fee8ddc57e'; 51 | client.request = (requestConfig) => { 52 | AssertionUtil.assertRequestConfig(requestConfig, { 53 | url: `${configPackageUrl}/Package/v1/Packages('${packageName}')`, 54 | method: 'DELETE', 55 | headers: { 'If-Match': etag }, 56 | }); 57 | }; 58 | 59 | return client.deletePackage(packageName, etag); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/unit/auth/Token.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const jwt = require('jwt-simple'); 3 | const Token = require('../../../lib/auth/Token'); 4 | 5 | const tokenSecret = 'test'; 6 | const sampleToken = { name: 'SAP IoT Token', scope: ['thing.r', 'thing.c'] }; 7 | 8 | describe('Token', function () { 9 | describe('getAccessToken', function () { 10 | it('should return the stored token', function () { 11 | const jwtToken = jwt.encode(sampleToken, tokenSecret); 12 | const token = new Token(jwtToken, 60); 13 | assert.strictEqual(jwtToken, token.getAccessToken()); 14 | }); 15 | }); 16 | 17 | describe('getScopes', function () { 18 | it('should return empty array', function () { 19 | const nonScopeToken = JSON.parse(JSON.stringify(sampleToken)); 20 | delete nonScopeToken.scope; 21 | 22 | const jwtToken = jwt.encode(nonScopeToken, tokenSecret); 23 | const token = new Token(jwtToken, 60); 24 | const scopes = token.getScopes(); 25 | assert(Array.isArray(scopes)); 26 | assert.strictEqual(scopes.length, 0); 27 | }); 28 | 29 | it('should return token scopes', function () { 30 | const scopes = ['action.r', 'action.c', 'action.d']; 31 | const scopeToken = JSON.parse(JSON.stringify(sampleToken)); 32 | scopeToken.scope = scopes; 33 | 34 | const jwtToken = jwt.encode(scopeToken, tokenSecret); 35 | const token = new Token(jwtToken, 60); 36 | assert.strictEqual(scopes.join(' '), token.getScopes().join(' ')); 37 | }); 38 | }); 39 | 40 | describe('isExpired', function () { 41 | it('should return false if token is not expired', function () { 42 | const expiresIn = 1000; 43 | const jwtToken = jwt.encode(sampleToken, tokenSecret); 44 | const token = new Token(jwtToken, expiresIn); 45 | assert.strictEqual(token.isExpired(), false); 46 | }); 47 | it('should return true if token is expired', function () { 48 | const expiresIn = -1000; 49 | const jwtToken = jwt.encode(sampleToken, tokenSecret); 50 | const token = new Token(jwtToken, expiresIn); 51 | assert.strictEqual(token.isExpired(), true); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/integration/helper/DataHelper.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const requestHelper = require('./requestHelper'); 3 | 4 | class DataHelper { 5 | static async init(client) { 6 | DataHelper.client = client; 7 | DataHelper.tenantPrefix = await requestHelper.determineTenantPrefix(client); 8 | DataHelper.rootObjectGroup = await client.getRootObjectGroup(); 9 | DataHelper.data = {}; 10 | } 11 | 12 | static _getVersioningSuffix(delimiter = '.') { 13 | const nodeVersion = process.versions.node.replace(/[\W_]+/g, '').substring(0, 6); 14 | const osVersion = os.release().replace(/[\W_]+/g, '').substring(0, 6); 15 | return `${os.platform()}${delimiter}v${osVersion}${delimiter}v${nodeVersion}`; 16 | } 17 | 18 | static _getPackageName() { 19 | return `${DataHelper.tenantPrefix}.sdk.${DataHelper._getVersioningSuffix('.')}`; 20 | } 21 | 22 | static package() { 23 | return { 24 | Name: DataHelper._getPackageName(), 25 | Scope: 'private', 26 | }; 27 | } 28 | 29 | static propertySetType() { 30 | return { 31 | Name: `${DataHelper.package().Name}:TestPropertySetTypeSDK`, 32 | DataCategory: 'TimeSeriesData', 33 | Properties: [{ Name: 'Temperature', Type: 'Numeric' }], 34 | }; 35 | } 36 | 37 | static thingType() { 38 | return { 39 | Name: `${DataHelper.package().Name}:TestThingTypeSDK`, 40 | PropertySets: [ 41 | { Name: 'TestPropertySet', PropertySetType: `${DataHelper.package().Name}:TestPropertySetTypeSDK` }, 42 | ], 43 | }; 44 | } 45 | 46 | static objectGroup() { 47 | const objectGroupName = `TestObjectGroupSDK_${DataHelper._getVersioningSuffix('_')}`; 48 | return { 49 | name: objectGroupName, 50 | objectGroupParentID: DataHelper.rootObjectGroup.objectGroupID, 51 | }; 52 | } 53 | 54 | static thing() { 55 | return { 56 | _name: 'TestThingSDK', 57 | _alternateId: `ThingSDK_${DataHelper._getVersioningSuffix('_')}`, 58 | _description: { en: 'TestThingSDK' }, 59 | _thingType: [DataHelper.thingType().Name], 60 | _objectGroup: DataHelper.rootObjectGroup.objectGroupID, 61 | }; 62 | } 63 | } 64 | 65 | module.exports = DataHelper; 66 | -------------------------------------------------------------------------------- /test/unit/utils/Navigator.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const Navigator = require('../../../lib/utils/Navigator'); 3 | const ConfigurationProvider = require('../../../lib/utils/ConfigurationProvider'); 4 | 5 | describe('Navigator', function () { 6 | describe('Destinations', function () { 7 | let navigator; 8 | 9 | beforeEach(function () { 10 | navigator = new Navigator(ConfigurationProvider.getDestinations()); 11 | }); 12 | 13 | it('should call constructor successfully', function () { 14 | navigator = new Navigator({ 15 | 'appiot-mds': 'https://appiot-mds.cfapps.eu10.hana.ondemand.com', 16 | }); 17 | assert(navigator.appiotMds()); 18 | }); 19 | 20 | it('should throw error for invalid constructor call', function () { 21 | try { 22 | navigator = new Navigator(); 23 | assert.fail('Expected Error was not thrown'); 24 | } catch (err) { 25 | assert.strictEqual( 26 | err.message, 27 | // eslint-disable-next-line max-len 28 | 'Incomplete navigator configuration. Ensure a SAP IoT service instance binding or configure authentication options including endpoints via default-env.json file as described in the readme section of used SAP IoT SDK', 29 | 'Unexpected error message', 30 | ); 31 | } 32 | }); 33 | 34 | it('all functional destinations should exist in sample env', function () { 35 | assert(navigator.authorization()); 36 | assert(navigator.businessPartner()); 37 | assert(navigator.configPackage()); 38 | assert(navigator.configThing()); 39 | assert(navigator.appiotMds()); 40 | assert(navigator.tmDataMapping()); 41 | assert(navigator.appiotColdstore()); 42 | }); 43 | 44 | it('should get destination for known destination', function () { 45 | assert.strictEqual(navigator.getDestination('appiot-mds'), 'https://appiot-mds.cfapps.eu10.hana.ondemand.com', 'Unexpected destination'); 46 | }); 47 | 48 | it('should throw error for unknwon destination', function () { 49 | assert.throws(() => navigator.getDestination('unknown'), Error, 'Expected Error was not thrown'); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /samples/README.md: -------------------------------------------------------------------------------- 1 | # SAP Leonardo IoT SDK Samples 2 | 3 | ## Remark 4 | This coding is a sample showcase how to use SAP Leonardo IoT JavaScript SDK functionality, it is no official product of SAP SE. This code is free of public documentation and support and will not be maintained. Only SAP SE is allowed to share and publish this source code, receivers are allowed to align their coding to this sample. 5 | 6 | ## Samples 7 | 8 | - [1) Package operations like create, read and delete](./sample_1_packages.js) 9 | - [2) Property Set Type operations like create, read and delete](./sample_2_property_set_types.js) 10 | - [3) Thing onboarding including configuration setup](./sample_3_thing_setup.js) 11 | - [4) Read and write thing data into time series store](./sample_4_thing_data.js) 12 | - [5) Express server application reading things and packages](./sample_5_express_application.js) 13 | - [6) Data ingestion for IoT Services device with MQTT or REST gateway](./sample_6_data_ingestion_iot_services.js) 14 | - [7) Migrate models from one tenant to another](./sample_7_multi_tenant_model_migration.js) 15 | 16 | ## How to run a sample 17 | 18 | #### Setup authorization 19 | First please make sure that the authorization credentials are maintained in your local environment by using a `default-env.json` file within the sample folder. For more details about setting up a local definition for authorization please check the linked [***SDK's readme section***](../README.md#authorization-concept) Option 2. 20 | 21 | #### Install dependencies 22 | Next all dependencies including the SAP Leonardo IoT SDK itself has to be installed via NPM. 23 | ``` 24 | $ npm install 25 | ``` 26 | 27 | #### Choose, prepare and run sample 28 | Some samples require manual user input (i.e. thing identifier) before they can be executed. 29 | Please choose a sample file you want to execute, open it and have a look to the upper description as well as all fields flagged with *//TODO* annotation. 30 | No worry, in case you miss any input the script will tell you on execution. 31 | 32 | Now you are ready to run your script by entering the following command 33 | ``` 34 | $ node 35 | 36 | # Example for sample 1 37 | $ node sample_1_packages.js 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /test/unit/AssertionUtil.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const querystring = require('querystring'); 3 | 4 | class AssertionUtil { 5 | static assertRequestConfig(requestConfig, expectedConfig) { 6 | const expected = expectedConfig; 7 | if (!expected.headers) expected.headers = {}; 8 | 9 | assert.strictEqual(querystring.unescape(requestConfig.url), expected.url, 'Unexpected Request URL'); 10 | // eslint-disable-next-line no-unused-expressions 11 | expected.method 12 | ? assert.strictEqual(requestConfig.method, expected.method, 'Unexpected HTTP method') 13 | : assert(!requestConfig.method, 'Unexpected HTTP method'); 14 | // eslint-disable-next-line no-unused-expressions 15 | expected.headers 16 | ? assert.deepStrictEqual(requestConfig.headers, expected.headers, 'Unexpected headers') 17 | : assert(!requestConfig.headers, 'Unexpected headers'); 18 | // eslint-disable-next-line no-unused-expressions 19 | expected.qs 20 | ? assert.deepStrictEqual(requestConfig.qs, expected.qs, 'Unexpected queryParameters') 21 | : assert(!requestConfig.qs, 'Unexpected queryParameters'); 22 | // eslint-disable-next-line no-unused-expressions 23 | expected.body 24 | ? assert.deepStrictEqual(requestConfig.body, expected.body, 'Unexpected body') 25 | : assert(!requestConfig.body, 'Unexpected body'); 26 | // eslint-disable-next-line no-unused-expressions 27 | expected.resolveWithFullResponse !== undefined 28 | ? assert.strictEqual( 29 | requestConfig.resolveWithFullResponse, 30 | expected.resolveWithFullResponse, 31 | 'Unexpected response resolve option', 32 | ) 33 | : assert(!requestConfig.resolveWithFullResponse, 'Unexpected response resolve option'); 34 | // eslint-disable-next-line no-unused-expressions 35 | expected.jwt 36 | ? assert.strictEqual(requestConfig.jwt, expected.jwt, 'Unexpected jwt token') 37 | : assert(!requestConfig.jwt, 'Unexpected jwt token'); 38 | // eslint-disable-next-line no-unused-expressions 39 | expected.agentOptions 40 | ? assert.deepStrictEqual(requestConfig.agentOptions, expected.agentOptions, 'Unexpected agent options') 41 | : assert(!requestConfig.agentOptions, 'Unexpected agent options'); 42 | 43 | if (expected.json !== undefined) { 44 | assert.strictEqual(requestConfig.json, expected.json, 'Unexpected JSON parameter'); 45 | } 46 | } 47 | } 48 | 49 | module.exports = AssertionUtil; 50 | -------------------------------------------------------------------------------- /samples/sample_5_express_application.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This sample application shows how the SAP Leonardo IoT SDK can be used within an Node.js express application 3 | */ 4 | 5 | const express = require('express'); 6 | const LeonardoIoT = require('sap-iot-sdk'); 7 | 8 | const client = new LeonardoIoT(); 9 | const app = express(); 10 | 11 | app.get('/', async (req, res) => { 12 | res.send('SAP Leonardo IoT express sample application'); 13 | }); 14 | 15 | /** 16 | * Returning all packages of current tenant in a readable numbered list by requesting '/packages'. 17 | * Authorization is handled by the SDK itself in the following manner: 18 | * - Credentials for JWT token creation are fetched by Service Broker service binding 19 | * - In case no service broker service binding was found, credentials are fetched from .env file 20 | * - In case no .env file was found, credentials are fetched from environment variables 21 | * - In case no credentials could be determined the SDK will throw an error and the request will fail 22 | */ 23 | app.get('/packages', async (req, res) => { 24 | try { 25 | const packages = await client.getPackages(); 26 | const stringifiedPackages = packages.d.results 27 | .map((pckg, index) => `#${index + 1}
Name: ${pckg.Name}`).join('

'); 28 | const response = `

Package List

${stringifiedPackages}`; 29 | res.send(response); 30 | } catch (err) { 31 | res.status(500).send(`Something went wrong: ${err}`); 32 | } 33 | }); 34 | 35 | /** 36 | * Returning the first 10 things of current tenant ordered by ID in a readable numbered list by requesting '/things'. 37 | * Authorization will be forwarded from incoming request if available. 38 | * If there was no authorization provided, it will be handled like in the /packages call above. 39 | */ 40 | app.get('/things', async (req, res) => { 41 | try { 42 | const things = await client.getThings( 43 | { $select: '_id,_name', $orderby: '_id', $top: 10 }, 44 | { jwt: req.headers.authorization }, 45 | ); 46 | const stringifiedThings = things.value 47 | .map((thing, index) => `#${index + 1}
ID: ${thing._id}
Name: ${thing._name}`).join('

'); 48 | const response = `

Thing List

${stringifiedThings}`; 49 | res.send(response); 50 | } catch (err) { 51 | res.status(500).send(`Something went wrong: ${err}`); 52 | } 53 | }); 54 | 55 | app.listen(8080, () => { 56 | console.log('App started successfully, listening on port 8080'); 57 | }); 58 | -------------------------------------------------------------------------------- /test/unit/services/AuthorizationService.test.js: -------------------------------------------------------------------------------- 1 | const AssertionUtil = require('../AssertionUtil'); 2 | const LeonardoIoT = require('../../../lib/LeonardoIoT'); 3 | 4 | const authorizationUrl = 'https://authorization.cfapps.eu10.hana.ondemand.com'; 5 | 6 | describe('Authorization Service', function () { 7 | let client; 8 | 9 | beforeEach(function () { 10 | client = new LeonardoIoT(); 11 | }); 12 | 13 | describe('ObjectGroup', function () { 14 | it('should create a new object group', function () { 15 | const objectGroupPayload = { Name: 'MyObjectGroup' }; 16 | client.request = (requestConfig) => { 17 | AssertionUtil.assertRequestConfig(requestConfig, { 18 | url: `${authorizationUrl}/ObjectGroups`, 19 | method: 'POST', 20 | body: objectGroupPayload, 21 | }); 22 | }; 23 | 24 | return client.createObjectGroup(objectGroupPayload); 25 | }); 26 | 27 | it('should read a single object group', function () { 28 | const objectGroupId = 'MyObjectGroup'; 29 | client.request = (requestConfig) => { 30 | AssertionUtil.assertRequestConfig(requestConfig, { 31 | url: `${authorizationUrl}/ObjectGroups('${objectGroupId}')`, 32 | }); 33 | }; 34 | 35 | return client.getObjectGroup(objectGroupId); 36 | }); 37 | 38 | it('should read multiple object groups', function () { 39 | client.request = (requestConfig) => { 40 | AssertionUtil.assertRequestConfig(requestConfig, { 41 | url: `${authorizationUrl}/ObjectGroups`, 42 | }); 43 | }; 44 | 45 | return client.getObjectGroups(); 46 | }); 47 | 48 | it('should read root object group', function () { 49 | client.request = (requestConfig) => { 50 | AssertionUtil.assertRequestConfig(requestConfig, { 51 | url: `${authorizationUrl}/ObjectGroups/TenantRoot`, 52 | }); 53 | }; 54 | 55 | return client.getRootObjectGroup(); 56 | }); 57 | 58 | it('should delete object group', function () { 59 | const objectGroupId = 'MyObjectGroup'; 60 | const etag = '8f9da184-5af1-4237-8ede-a7fee8ddc57e'; 61 | client.request = (requestConfig) => { 62 | AssertionUtil.assertRequestConfig(requestConfig, { 63 | url: `${authorizationUrl}/ObjectGroups('${objectGroupId}')`, 64 | method: 'DELETE', 65 | headers: { 'If-Match': etag }, 66 | }); 67 | }; 68 | 69 | return client.deleteObjectGroup(objectGroupId, etag); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/unit/services/TimeSeriesColdStoreService.test.js: -------------------------------------------------------------------------------- 1 | const AssertionUtil = require('../AssertionUtil'); 2 | const LeonardoIoT = require('../../../lib/LeonardoIoT'); 3 | 4 | const appiotColdstoreUrl = 'https://appiot-coldstore.cfapps.eu10.hana.ondemand.com'; 5 | 6 | describe('Time Series Cold Store Service', function () { 7 | let client; 8 | 9 | beforeEach(function () { 10 | client = new LeonardoIoT(); 11 | }); 12 | 13 | describe('Time Series Data', function () { 14 | it('should create coldstore timeseries data entry', function () { 15 | const thingId = 'MyThing'; 16 | const thingTypeName = 'MyThingType'; 17 | const propertySetId = 'MyPropertySet'; 18 | const timeSeriesPayload = { value: [{ Temperature: '25', _time: '2019-01-15T10:00:00Z' }] }; 19 | 20 | client.request = (requestConfig) => { 21 | AssertionUtil.assertRequestConfig(requestConfig, { 22 | url: `${appiotColdstoreUrl}/Things('${thingId}')/${thingTypeName}/${propertySetId}`, 23 | method: 'PUT', 24 | body: timeSeriesPayload, 25 | }); 26 | }; 27 | 28 | return client.createColdStoreTimeSeriesData(thingId, thingTypeName, propertySetId, timeSeriesPayload); 29 | }); 30 | 31 | it('should read coldstore timeseries data', function () { 32 | const thingId = 'MyThing'; 33 | const thingTypeName = 'MyThingType'; 34 | const propertySetId = 'MyPropertySet'; 35 | const fromTime = '2019-01-15T08:00:00Z'; 36 | const toTime = '2019-01-15T20:00:00Z'; 37 | client.request = (requestConfig) => { 38 | AssertionUtil.assertRequestConfig(requestConfig, { 39 | url: `${appiotColdstoreUrl}/Things('${thingId}')/${thingTypeName}/${propertySetId}`, 40 | qs: { timerange: `${fromTime}-${toTime}` }, 41 | }); 42 | }; 43 | 44 | return client.getColdStoreTimeSeriesData(thingId, thingTypeName, propertySetId, fromTime, toTime); 45 | }); 46 | 47 | it('should delete coldstore timeseries data', function () { 48 | const thingId = 'MyThing'; 49 | const thingTypeName = 'MyThingType'; 50 | const propertySetId = 'MyPropertySet'; 51 | const fromTime = '2019-01-15T08:00:00Z'; 52 | const toTime = '2019-01-15T20:00:00Z'; 53 | client.request = (requestConfig) => { 54 | AssertionUtil.assertRequestConfig(requestConfig, { 55 | url: `${appiotColdstoreUrl}/Things('${thingId}')/${thingTypeName}/${propertySetId}`, 56 | method: 'DELETE', 57 | qs: { timerange: `${fromTime}-${toTime}` }, 58 | }); 59 | }; 60 | 61 | return client.deleteColdStoreTimeSeriesData(thingId, thingTypeName, propertySetId, fromTime, toTime); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /samples/sample_4_thing_data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This example shows how to read the latest measurements of a single thing 3 | * and also how to write them directly into the time series store without using the ingestion pipeline. 4 | * 5 | * TASKS BEFORE START: 6 | * - Enter value for variable "thingId" 7 | */ 8 | 9 | const assert = require('assert'); 10 | const LeonardoIoT = require('sap-iot-sdk'); 11 | 12 | const client = new LeonardoIoT(); 13 | 14 | const thingId = ''; // TODO enter thingId used for ingestion 15 | assert(thingId, 'Enter value for variable "thingId" before running this sample'); 16 | 17 | function generateTimeSeriesDataPayload(propertySet) { 18 | // Create time series payload skeleton with current timestamp 19 | const timeSeriesDataPayload = { 20 | value: [{ 21 | _time: new Date().toISOString(), 22 | }], 23 | }; 24 | 25 | // Generate test data for properties of property set 26 | propertySet.Properties.results.forEach((property) => { 27 | let value; 28 | if (property.Type === 'NumericFlexible') { 29 | value = Math.floor(Math.random() * 100) + 0.5; 30 | } else if (property.Type === 'Numeric') { 31 | value = Math.floor(Math.random() * 100); 32 | } else if (property.Type === 'Boolean') { 33 | value = Math.random() >= 0.5; 34 | } else if (property.Type === 'String') { 35 | value = new Date().toISOString().substring(0, 6); 36 | } 37 | 38 | if (value) timeSeriesDataPayload.value[0][property.Name] = value; 39 | }); 40 | 41 | return timeSeriesDataPayload; 42 | } 43 | 44 | async function runTimeSeriesDataOperations() { 45 | const thing = await client.getThing(thingId); 46 | const thingType = await client.getThingType(thing._thingType[0], { 47 | $expand: 'PropertySets/Properties', 48 | }); 49 | 50 | let thingSnapshotData = await client.getThingSnapshot(thingId); 51 | console.log(`(1) Latest thing snapshot data\n${JSON.stringify(thingSnapshotData)}`); 52 | 53 | // Fetch first time series property set of thing type 54 | const propertySet = thingType.d.PropertySets.results.filter((pSet) => pSet.DataCategory === 'TimeSeriesData')[0]; 55 | 56 | const timeSeriesDataPayload = generateTimeSeriesDataPayload(propertySet); 57 | console.log(`\n (2) Sending new time series data:\n${JSON.stringify(timeSeriesDataPayload)}`); 58 | await client.createTimeSeriesData(thingId, thing._thingType, propertySet.Name, timeSeriesDataPayload); 59 | thingSnapshotData = await client.getThingSnapshot(thingId); 60 | console.log(`\n(3) New thing snapshot data:\n${JSON.stringify(thingSnapshotData)}`); 61 | } 62 | 63 | // Entry point of script 64 | (async () => { 65 | try { 66 | await runTimeSeriesDataOperations(); 67 | } catch (err) { 68 | console.error(err); 69 | } 70 | })(); 71 | -------------------------------------------------------------------------------- /test/integration/step_2_readAll.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const LeonardoIoT = require('../../lib/LeonardoIoT'); 3 | const DataHelper = require('./helper/DataHelper'); 4 | 5 | describe('Read All Entities', function () { 6 | let client; 7 | 8 | before(function () { 9 | client = new LeonardoIoT(); 10 | }); 11 | 12 | it('should read all packages', async function () { 13 | const response = await client.getPackages(); 14 | assert(response.d.results.length > 0); 15 | }); 16 | 17 | it('should read all property set types', async function () { 18 | const response = await client.getPropertySetTypes(); 19 | assert(response.d.results.length > 0); 20 | }); 21 | 22 | it('should read all property set types by package', async function () { 23 | const response = await client.getPropertySetTypesByPackage(DataHelper.package().Name); 24 | assert(response.d.results.length > 0); 25 | assert(response.d.results[0].PackageName, DataHelper.package().Name); 26 | }); 27 | 28 | it('should read all thing types', async function () { 29 | const response = await client.getThingTypes(); 30 | assert(response.d.results.length > 0); 31 | }); 32 | 33 | it('should read all thing types by package', async function () { 34 | const response = await client.getThingTypesByPackage(DataHelper.package().Name); 35 | assert(response.d.results.length > 0); 36 | assert.strictEqual(response.d.results[0].PackageName, DataHelper.package().Name); 37 | }); 38 | 39 | it('should read all object groups', async function () { 40 | const response = await client.getObjectGroups(); 41 | assert(response.value.length > 0); 42 | }); 43 | 44 | it('should read all object groups filtered by name', async function () { 45 | const objectGroups = await client.getObjectGroups({ $filter: `name eq ${DataHelper.objectGroup().name}` }); 46 | [DataHelper.data.objectGroup] = objectGroups.value; 47 | assert.strictEqual(objectGroups.value[0].name, DataHelper.data.objectGroup.name); 48 | }); 49 | 50 | it('should read all things', async function () { 51 | const things = await client.getThings(); 52 | assert(things.value.length > 0); 53 | }); 54 | 55 | it('should read all things filtered by name', async function () { 56 | const things = await client.getThingsByThingType(DataHelper.thingType().Name, { $filter: '_name ne \'xyz\'' }); 57 | assert(things.value.length > 0); 58 | assert.strictEqual(things.value[0]._name, DataHelper.thing()._name); 59 | }); 60 | 61 | it('should read all things by thingtype', async function () { 62 | const things = await client.getThingsByThingType(DataHelper.thingType().Name); 63 | [DataHelper.data.thing] = things.value; 64 | assert(things.value.length > 0); 65 | assert.strictEqual(things.value[0]._name, DataHelper.thing()._name); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/unit/services/PropertySetTypeService.test.js: -------------------------------------------------------------------------------- 1 | const AssertionUtil = require('../AssertionUtil'); 2 | const LeonardoIoT = require('../../../lib/LeonardoIoT'); 3 | 4 | const configThingUrl = 'https://config-thing-sap.cfapps.eu10.hana.ondemand.com'; 5 | 6 | describe('Property Set Type Service', function () { 7 | let client; 8 | 9 | beforeEach(function () { 10 | client = new LeonardoIoT(); 11 | }); 12 | 13 | describe('PropertySetType', function () { 14 | it('should create PropertySetType', function () { 15 | const packageName = 'MyPackage'; 16 | const propertySetTypePayload = { Name: 'MyPropertySetType' }; 17 | client.request = (requestConfig) => { 18 | AssertionUtil.assertRequestConfig(requestConfig, { 19 | url: `${configThingUrl}/ThingConfiguration/v1/Packages('${packageName}')/PropertySetTypes`, 20 | method: 'POST', 21 | body: propertySetTypePayload, 22 | }); 23 | }; 24 | 25 | return client.createPropertySetType(packageName, propertySetTypePayload); 26 | }); 27 | 28 | it('should read single propertySetType', function () { 29 | const propertySetTypeName = 'MyPropertySetType'; 30 | client.request = (requestConfig) => { 31 | AssertionUtil.assertRequestConfig(requestConfig, { 32 | url: `${configThingUrl}/ThingConfiguration/v1/PropertySetTypes('${propertySetTypeName}')`, 33 | }); 34 | }; 35 | 36 | return client.getPropertySetType(propertySetTypeName); 37 | }); 38 | 39 | it('should read multiple propertySetTypes', function () { 40 | client.request = (requestConfig) => { 41 | AssertionUtil.assertRequestConfig(requestConfig, { 42 | url: `${configThingUrl}/ThingConfiguration/v1/PropertySetTypes`, 43 | }); 44 | }; 45 | 46 | return client.getPropertySetTypes(); 47 | }); 48 | 49 | it('should read multiple propertySetTypes by package name', function () { 50 | const packageName = 'MyPackage'; 51 | client.request = (requestConfig) => { 52 | AssertionUtil.assertRequestConfig(requestConfig, { 53 | url: `${configThingUrl}/ThingConfiguration/v1/Packages('${packageName}')/PropertySetTypes`, 54 | }); 55 | }; 56 | 57 | return client.getPropertySetTypesByPackage(packageName); 58 | }); 59 | 60 | it('should delete propertySetType by name', function () { 61 | const propertySetTypeName = 'MyPropertySetType'; 62 | const etag = '8f9da184-5af1-4237-8ede-a7fee8ddc57e'; 63 | client.request = (requestConfig) => { 64 | AssertionUtil.assertRequestConfig(requestConfig, { 65 | url: `${configThingUrl}/ThingConfiguration/v1/PropertySetTypes('${propertySetTypeName}')`, 66 | method: 'DELETE', 67 | headers: { 'If-Match': etag }, 68 | }); 69 | }; 70 | 71 | return client.deletePropertySetType(propertySetTypeName, etag); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /samples/sample_2_property_set_types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This example shows different property set type service calls 3 | */ 4 | 5 | const LeonardoIoT = require('sap-iot-sdk'); 6 | 7 | const client = new LeonardoIoT(); 8 | let packageName; 9 | 10 | /* 11 | * Identify current operating tenant and add prefix to package name 12 | */ 13 | async function determineTenant() { 14 | const tenantInfo = await client.request({ 15 | url: `${client.navigator.businessPartner()}/Tenants`, 16 | }); 17 | 18 | packageName = `${tenantInfo.value[0].package}.sdk.sample.package`; 19 | } 20 | 21 | /* 22 | * Create package for new property set types 23 | */ 24 | async function setup() { 25 | console.log('SETUP: Create sample package'); 26 | await client.createPackage({ 27 | Name: packageName, 28 | Scope: 'private', 29 | }); 30 | } 31 | 32 | async function runPropertySetTypeOperations() { 33 | console.log('Start creation of sample property set types'); 34 | 35 | // Create property set type for master data 36 | await client.createPropertySetType(packageName, { 37 | Name: `${packageName}:General`, 38 | DataCategory: 'MasterData', 39 | Properties: [{ Name: 'Vendor', Type: 'String' }], 40 | }); 41 | 42 | // Create property set type for time series data 43 | await client.createPropertySetType(packageName, { 44 | Name: `${packageName}:Measurements`, 45 | DataCategory: 'TimeSeriesData', 46 | Properties: [{ Name: 'Temperature', Type: 'Numeric' }, { Name: 'Pressure', Type: 'NumericFlexible' }], 47 | }); 48 | 49 | // Read existing sample property set types 50 | const response = await client.getPropertySetTypesByPackage(packageName); 51 | const propertySetTypes = response.d.results; 52 | 53 | // Read property set type details 54 | console.log(`Number of existing property set types in sample package: ${propertySetTypes.length}`); 55 | 56 | propertySetTypes.forEach(async (propertySetType) => { 57 | // Get etag for each package 58 | const propertySetTypeResponse = await client.getPropertySetType( 59 | propertySetType.Name, 60 | null, 61 | { resolveWithFullResponse: true }, 62 | ); 63 | // Delete created packages 64 | await client.deletePropertySetType(propertySetType.Name, propertySetTypeResponse.headers.etag); 65 | console.log(`Property Set Type successfully deleted: ${propertySetType.Name}`); 66 | }); 67 | } 68 | 69 | /* 70 | * Delete newly created package 71 | */ 72 | async function cleanup() { 73 | console.log('CLEANUP: Delete sample package'); 74 | const packageResponse = await client.getPackage(packageName, { resolveWithFullResponse: true }); 75 | await client.deletePackage(packageName, packageResponse.headers.etag); 76 | } 77 | 78 | // Entry point of script 79 | (async () => { 80 | try { 81 | await determineTenant(); 82 | await setup(); 83 | await runPropertySetTypeOperations(); 84 | await cleanup(); 85 | } catch (err) { 86 | console.error(err); 87 | } 88 | })(); 89 | -------------------------------------------------------------------------------- /MIGRATION-GUIDE.md: -------------------------------------------------------------------------------- 1 | ## Migration Guide 2 | This migration guide supports you in adapting your source code from the deprecated [IoT Application Services SDK](https://github.com/SAP/iot-application-services-sdk-nodejs) to the newly released [SAP Leonardo IoT SDK](https://github.com/SAP/sap-iot-sdk-nodejs) step by step. 3 | Before starting the migration we highly recommend to read the [documentation](./README.md) and have a look at the samples of the new SDK to ensure all concepts and features are known. 4 | 5 | #### 1) Update Dependency 6 | Remove existing dependency from package.json via command: 7 | ``` 8 | $ npm uninstall SAP/iot-application-services-sdk-nodejs 9 | ``` 10 | 11 | Next install the latest version of the new Leonardo IoT SDK: 12 | ``` 13 | $ npm install SAP/sap-iot-sdk-nodejs --save 14 | ``` 15 | 16 | #### 2) Adapt tenant credential configuration 17 | For Leonardo IoT SDK all access credentials are maintained in the application runtime environment, there is no `.env` file used anymore. 18 | For Cloud Foundry deployments, all credentials are fetched from application service bindings, in case of local operation the environment is configured in the file `default-env.json`. 19 | 20 | 2.1) Remove existing .env files from project root directory 21 | 22 | 2.2) Follow [this step](https://github.com/SAP/sap-iot-sdk-nodejs#2-setup-authorization-for-local-usage) and store the file in the projects root directory 23 | 24 | 2.3) Copy Leonardo IoT service key of your tenant into template and rename the file to `default-env.json`. 25 | 26 | 27 | #### 3) Replace requirements and client instantiation 28 | Replace all require statements and also align variable names: 29 | ```js 30 | // OLD CODING 31 | const AE = require('iot-application-services-sdk-nodejs'); 32 | const client = new AE(); 33 | 34 | // NEW CODING 35 | const LeonardoIoT = require('sap-iot-sdk'); 36 | const client = new LeonardoIoT(); 37 | ``` 38 | 39 | #### 4) Adapt service calls 40 | All functionality from the [feature overview table](./README.md#feature-overview) can be called by a designated function of the instantiated Leonardo IoT client: 41 | 42 | ```js 43 | const LeonardoIoT = require('sap-iot-sdk'); 44 | const client = new LeonardoIoT(); 45 | 46 | // Read things 47 | const things = await client.getThings(); 48 | ``` 49 | 50 | In case your used functionality is not covered by a designated function, you can send the request by using a general request facade of the client: 51 | ```js 52 | const LeonardoIoT = require('sap-iot-sdk'); 53 | const client = new LeonardoIoT(); 54 | 55 | // Read assignments 56 | let url = client.navigator.getDestination('tm-data-mapping') + '/v1/assignments'; 57 | const assignments = await client.request({url}); 58 | ``` 59 | 60 | #### 5) Test your project 61 | That's is! Last step is to run your applications unit and integration tests as well as some manual tests. If this guide was missing any step please feel free to let us know by creating an [Github Issue](https://github.com/SAP/sap-iot-sdk-nodejs/issues) with an attached migration label. 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /lib/utils/Navigator.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('SAPIoT:Navigator'); 2 | 3 | /** 4 | * This class fetches the current landscape information from {ConfigurationProvider} 5 | * and simplifies the navigation to different service destination URIs. 6 | * 7 | * @class Navigator 8 | * @author Soeren Buehler 9 | */ 10 | class Navigator { 11 | /** 12 | * Create class instance 13 | * 14 | * @param {object} destinations - Destination collection for micro service endpoints 15 | */ 16 | constructor(destinations) { 17 | debug('Creating a new navigator'); 18 | 19 | if (!destinations || Object.keys(destinations).length < 1) { 20 | // eslint-disable-next-line max-len 21 | throw Error('Incomplete navigator configuration. Ensure a SAP IoT service instance binding or configure authentication options including endpoints via default-env.json file as described in the readme section of used SAP IoT SDK'); 22 | } 23 | 24 | this.destinations = destinations; 25 | } 26 | 27 | /** 28 | * Returning landscape specific destination URI for authorization service 29 | * 30 | * @returns {string} Service URI 31 | */ 32 | authorization() { 33 | return this.destinations.authorization; 34 | } 35 | 36 | /** 37 | * Returning landscape specific destination URI for authorization service 38 | * 39 | * @returns {string} Service URI 40 | */ 41 | businessPartner() { 42 | return this.destinations['business-partner']; 43 | } 44 | 45 | /** 46 | * Returning landscape specific destination URI for config-package-sap service 47 | * 48 | * @returns {string} Service URI 49 | */ 50 | configPackage() { 51 | return this.destinations['config-package-sap']; 52 | } 53 | 54 | /** 55 | * Returning landscape specific destination URI for config-thing-sap service 56 | * 57 | * @returns {string} Service URI 58 | */ 59 | configThing() { 60 | return this.destinations['config-thing-sap']; 61 | } 62 | 63 | /** 64 | * Returning landscape specific destination URI for appiot-mds service 65 | * 66 | * @returns {string} Service URI 67 | */ 68 | appiotMds() { 69 | return this.destinations['appiot-mds']; 70 | } 71 | 72 | /** 73 | * Returning landscape specific destination URI for tm-data-mapping service 74 | * 75 | * @returns {string} Service URI 76 | */ 77 | tmDataMapping() { 78 | return this.destinations['tm-data-mapping']; 79 | } 80 | 81 | /** 82 | * Returning landscape specific destination URI for appiot-coldstore service 83 | * 84 | * @returns {string} Service URI 85 | */ 86 | appiotColdstore() { 87 | return this.destinations['appiot-coldstore']; 88 | } 89 | 90 | /** 91 | * Returning landscape specific destination URI for provided service name 92 | * 93 | * @param {string} serviceName - Service identifier 94 | * @throws {Error} Will throw if the destination is unknown for the given servicename 95 | * @returns {string} Service URI 96 | */ 97 | getDestination(serviceName) { 98 | const destination = this.destinations[serviceName]; 99 | if (!destination) throw new Error(`Unknown destination for service name: ${serviceName}`); 100 | return destination; 101 | } 102 | } 103 | 104 | module.exports = Navigator; 105 | -------------------------------------------------------------------------------- /test/unit/services/TimeSeriesStoreService.test.js: -------------------------------------------------------------------------------- 1 | const AssertionUtil = require('../AssertionUtil'); 2 | const LeonardoIoT = require('../../../lib/LeonardoIoT'); 3 | 4 | const appiotMdsUrl = 'https://appiot-mds.cfapps.eu10.hana.ondemand.com'; 5 | 6 | describe('Time Series Store', function () { 7 | let client; 8 | let queryParameters; 9 | let queryKey; 10 | let queryValue; 11 | 12 | before(function () { 13 | queryParameters = {}; 14 | queryKey = '$expand'; 15 | queryValue = 'Descriptions'; 16 | queryParameters[queryKey] = queryValue; 17 | }); 18 | 19 | beforeEach(function () { 20 | client = new LeonardoIoT(); 21 | }); 22 | 23 | describe('Time Series Data', function () { 24 | it('should create timeseries data', function () { 25 | const thingId = 'MyThing'; 26 | const thingTypeName = 'MyThingType'; 27 | const propertySetId = 'MyPropertySet'; 28 | const timeSeriesPayload = { value: [{ Temperature: '25', _time: new Date().toISOString() }] }; 29 | 30 | client.request = (requestConfig) => { 31 | AssertionUtil.assertRequestConfig(requestConfig, { 32 | url: `${appiotMdsUrl}/Things('${thingId}')/${thingTypeName}/${propertySetId}`, 33 | method: 'PUT', 34 | body: timeSeriesPayload, 35 | }); 36 | }; 37 | 38 | return client.createTimeSeriesData(thingId, thingTypeName, propertySetId, timeSeriesPayload); 39 | }); 40 | 41 | it('should read timeseries data', function () { 42 | const thingId = 'MyThing'; 43 | const thingTypeName = 'MyThingType'; 44 | const propertySetId = 'MyPropertySet'; 45 | client.request = (requestConfig) => { 46 | AssertionUtil.assertRequestConfig(requestConfig, { 47 | url: `${appiotMdsUrl}/Things('${thingId}')/${thingTypeName}/${propertySetId}`, 48 | }); 49 | }; 50 | 51 | return client.getTimeSeriesData(thingId, thingTypeName, propertySetId); 52 | }); 53 | 54 | it('should read timeseries data with query parameters', function () { 55 | const thingId = 'MyThing'; 56 | const thingTypeName = 'MyThingType'; 57 | const propertySetId = 'MyPropertySet'; 58 | const queryParatemeters = {}; 59 | queryParatemeters[queryKey] = queryValue; 60 | client.request = (requestConfig) => { 61 | AssertionUtil.assertRequestConfig(requestConfig, { 62 | url: `${appiotMdsUrl}/Things('${thingId}')/${thingTypeName}/${propertySetId}`, 63 | qs: queryParatemeters, 64 | }); 65 | }; 66 | 67 | return client.getTimeSeriesData(thingId, thingTypeName, propertySetId, queryParameters); 68 | }); 69 | 70 | it('should delete timeseries data', function () { 71 | const thingId = 'MyThing'; 72 | const thingTypeName = 'MyThingType'; 73 | const propertySetId = 'MyPropertySet'; 74 | const fromTime = '2019-06-15T08:00:00Z'; 75 | const toTime = '2019-06-15T20:00:00Z'; 76 | client.request = (requestConfig) => { 77 | AssertionUtil.assertRequestConfig(requestConfig, { 78 | url: `${appiotMdsUrl}/Things('${thingId}')/${thingTypeName}/${propertySetId}`, 79 | qs: { timerange: `${fromTime}-${toTime}` }, 80 | method: 'DELETE', 81 | }); 82 | }; 83 | 84 | return client.deleteTimeSeriesData(thingId, thingTypeName, propertySetId, fromTime, toTime); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/integration/step_5_data.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const LeonardoIoT = require('../../lib/LeonardoIoT'); 3 | const DataHelper = require('./helper/DataHelper'); 4 | 5 | describe('5) DATA', function () { 6 | let client; 7 | let thingTypeName; 8 | let propertySetName; 9 | let thingId; 10 | 11 | let currentTime; 12 | let oneYearAgoTime; 13 | 14 | before(function () { 15 | client = new LeonardoIoT(); 16 | currentTime = new Date().toISOString(); 17 | oneYearAgoTime = new Date(new Date().setFullYear(new Date().getFullYear() - 1)).toISOString(); 18 | }); 19 | 20 | describe('Timeseries Store & Timeseries Aggregate Store', function () { 21 | before(function () { 22 | thingTypeName = DataHelper.thingType().Name; 23 | propertySetName = DataHelper.thingType().PropertySets[0].Name; 24 | thingId = DataHelper.data.thing._id; 25 | }); 26 | 27 | it('should create a timeseries data entity', function () { 28 | return client.createTimeSeriesData(thingId, thingTypeName, propertySetName, { 29 | value: [{ 30 | _time: currentTime, 31 | Temperature: 25, 32 | }], 33 | }); 34 | }); 35 | 36 | it('should read timeseries data', async function () { 37 | const response = await client.getTimeSeriesData(thingId, thingTypeName, propertySetName); 38 | assert(response.value.length > 0); 39 | }); 40 | 41 | it('should read snapshot', async function () { 42 | const response = await client.getThingSnapshot(thingId); 43 | assert.strictEqual(response._id, thingId); 44 | }); 45 | 46 | it('should read snapshot within time range', async function () { 47 | const fromTime = currentTime; 48 | const toTime = new Date().toISOString(); 49 | const response = await client.getThingSnapshotWithinTimeRange(thingId, fromTime, toTime); 50 | assert.strictEqual(response._id, thingId); 51 | }); 52 | 53 | it('should recalculate aggregates', function () { 54 | const fromTime = currentTime; 55 | const toTime = new Date().toISOString(); 56 | return client.recalculateAggregates(thingId, thingTypeName, propertySetName, fromTime, toTime); 57 | }); 58 | 59 | it('should delete timeseries data', function () { 60 | return client.deleteTimeSeriesData(thingId, thingTypeName, propertySetName, currentTime, currentTime); 61 | }); 62 | }); 63 | 64 | describe('Timeseries Cold Store', function () { 65 | before(function () { 66 | thingTypeName = DataHelper.thingType().Name; 67 | propertySetName = DataHelper.thingType().PropertySets[0].Name; 68 | thingId = DataHelper.data.thing._id; 69 | }); 70 | 71 | it('should create a timeseries coldstore data entity', function () { 72 | return client.createColdStoreTimeSeriesData(thingId, thingTypeName, propertySetName, { 73 | value: [{ 74 | _time: oneYearAgoTime, 75 | Temperature: 28, 76 | }], 77 | }); 78 | }); 79 | 80 | it('should read timeseries coldstore data', function () { 81 | return client.getColdStoreTimeSeriesData(thingId, thingTypeName, propertySetName, oneYearAgoTime, oneYearAgoTime); 82 | }); 83 | 84 | it('should delete timeseries coldstore data', function () { 85 | return client.deleteColdStoreTimeSeriesData(thingId, thingTypeName, propertySetName, oneYearAgoTime, oneYearAgoTime); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## 📝 Reporting Issues 2 | 3 | ### Requirements for a Bug Report 4 | 5 | 1. **Only SAP IoT SDK issues** 6 | * Please do not report: 7 | * Issues caused by dependencies or plugins. 8 | * Issues caused by the use of non-public/internal methods. Only the public methods listed in the API documentation may be used. 9 | 2. **No duplicate**: You have searched the [issue tracker](https://github.com/SAP/sap-iot-sdk-nodejs/issues) to make sure the bug has not yet been reported. 10 | 3. **Good summary**: The summary should be specific to the issue. 11 | 4. **Current bug**: The bug can be reproduced in the most current version. 12 | 5. **Reproducible bug**: There are step-by-step instructions provided on how to reproduce the issue. 13 | 6. **Well-documented**: 14 | * Precisely state the expected and the actual behavior. 15 | * Give information about the environment in which the issue occurs (OS/Platform, Node.js version, etc.). 16 | * Generally, give as much additional information as possible. 17 | 8. **Only one bug per report**: Open additional tickets for additional issues. 18 | 9. **Please report bugs in English.** 19 | 20 | ### Reporting Security Issues 21 | If you find a security issue, act responsibly and do not report it in the public issue tracker, but directly to us. Please refer [here for more information](./SECURITY.md). 22 | 23 | 24 | ## 💻 Contributing Code 25 | ### General Remarks 26 | You are welcome to contribute code to the SAP IoT SDK in order to fix bugs or to implement new features. 27 | There are two important things to know: 28 | 29 | 1. You must be aware of the Apache License (which describes contributions) and **agree to the Developer Certificate of Origin***. This is common practice in major Open Source projects. To make this process as simple as possible, we are using *[CLA assistant](https://cla-assistant.io/)* for individual contributions. CLA assistant is an open source tool that integrates with GitHub very well and enables a one-click experience for accepting the DCO. For company contributers, special rules apply. See the respective section below for details. 30 | 2. **Not all proposed contributions can be accepted**. Some features may just fit a third-party add-on better. The code must match the overall direction of the SAP IoT SDK and improve it. For most bug fixes this is a given, but a major feature implementation first needs to be discussed with one of the committers. Possibly, one who touched the related code or module recently. The more effort you invest, the better you should clarify in advance whether the contribution will match the project's direction. The best way would be to just open an issue to discuss the feature you plan to implement (make it clear that you intend to contribute). We will then forward the proposal to the respective code owner. This avoids disappointment. 31 | 32 | ### Developer Certificate of Origin (DCO) 33 | 34 | Due to legal reasons, contributors will be asked to accept a DCO before they submit the first pull request to this projects, this happens in an automated fashion during the submission process. SAP uses [the standard DCO text of the Linux Foundation](https://developercertificate.org/). 35 | 36 | This happens in an automated fashion during the submission process: The CLA assistant tool will add a comment to the pull request. Click it to check the DCO, then accept it on the following screen. CLA assistant will save this decision for upcoming contributions. 37 | -------------------------------------------------------------------------------- /test/unit/services/TimeSeriesAggregateStoreService.test.js: -------------------------------------------------------------------------------- 1 | const AssertionUtil = require('../AssertionUtil'); 2 | const LeonardoIoT = require('../../../lib/LeonardoIoT'); 3 | 4 | const appiotMdsUrl = 'https://appiot-mds.cfapps.eu10.hana.ondemand.com'; 5 | 6 | describe('Time Series Store', function () { 7 | let client; 8 | let currentTime; 9 | 10 | beforeEach(function () { 11 | client = new LeonardoIoT(); 12 | currentTime = new Date().toISOString(); 13 | }); 14 | 15 | describe('Time Series Data', function () { 16 | it('should read thing snapshot', function () { 17 | const thingId = 'MyThing'; 18 | const dataCategory = 'TimeSeries'; 19 | 20 | client.request = (requestConfig) => { 21 | AssertionUtil.assertRequestConfig(requestConfig, { 22 | url: `${appiotMdsUrl}/Snapshot(thingId='${thingId}',fromTime='',dataCategory='${dataCategory}')`, 23 | }); 24 | }; 25 | 26 | return client.getThingSnapshot(thingId, dataCategory); 27 | }); 28 | 29 | it('should read thing snapshot with default data category', function () { 30 | const thingId = 'MyThing'; 31 | 32 | client.request = (requestConfig) => { 33 | AssertionUtil.assertRequestConfig(requestConfig, { 34 | url: `${appiotMdsUrl}/Snapshot(thingId='${thingId}',fromTime='',dataCategory='')`, 35 | }); 36 | }; 37 | 38 | return client.getThingSnapshot(thingId); 39 | }); 40 | 41 | it('should read thing snapshot within time range', function () { 42 | const thingId = 'MyThing'; 43 | const fromTime = currentTime; 44 | const toTime = new Date().toISOString(); 45 | const dataCategory = 'TimeSeries'; 46 | 47 | client.request = (requestConfig) => { 48 | AssertionUtil.assertRequestConfig(requestConfig, { 49 | url: `${appiotMdsUrl}/v2/Snapshot(thingId='${thingId}',fromTime='${fromTime}',toTime='${toTime}',dataCategory='${dataCategory}')`, 50 | }); 51 | }; 52 | 53 | return client.getThingSnapshotWithinTimeRange(thingId, fromTime, toTime, dataCategory); 54 | }); 55 | 56 | it('should read thing snapshot within time range with default data category', function () { 57 | const thingId = 'MyThing'; 58 | const fromTime = currentTime; 59 | const toTime = new Date().toISOString(); 60 | 61 | client.request = (requestConfig) => { 62 | AssertionUtil.assertRequestConfig(requestConfig, { 63 | url: `${appiotMdsUrl}/v2/Snapshot(thingId='${thingId}',fromTime='${fromTime}',toTime='${toTime}',dataCategory='')`, 64 | }); 65 | }; 66 | 67 | return client.getThingSnapshotWithinTimeRange(thingId, fromTime, toTime); 68 | }); 69 | 70 | it('should recalculate aggregates', function () { 71 | const thingId = 'MyThing'; 72 | const thingTypeName = 'MyThingType'; 73 | const propertySetName = 'MyPropertySet'; 74 | const fromTime = currentTime; 75 | const toTime = new Date().toISOString(); 76 | 77 | client.request = (requestConfig) => { 78 | AssertionUtil.assertRequestConfig(requestConfig, { 79 | method: 'POST', 80 | url: `${appiotMdsUrl}/Things('${thingId}')/${thingTypeName}/${propertySetName}/RecalculateAggregate`, 81 | qs: { timerange: `${fromTime}-${toTime}` }, 82 | }); 83 | }; 84 | 85 | return client.recalculateAggregates(thingId, thingTypeName, propertySetName, fromTime, toTime); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /samples/sample_6_data_ingestion_iot_services.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This example shows how to interact as sending IoT Service device using a MQTT or REST gateway 3 | * 4 | * TASKS BEFORE START: 5 | * - Enter IoT Services credentials to access device certificate 6 | * - Enter value for variable "deviceAlternateId" 7 | * - Enter value for variable "sensorAlternateId" 8 | * - Enter value for variable "capabilityAlternateId" 9 | * - Adapt measurement payload content to your current capability definition 10 | */ 11 | 12 | const assert = require('assert'); 13 | const LeonardoIoT = require('sap-iot-sdk'); 14 | const IoTServiceHelper = require('./helper/IoTServicesHelper'); 15 | 16 | const client = new LeonardoIoT(); 17 | 18 | const iotServicesCredentials = { 19 | host: '', // TODO enter IoT Services Host (without https:// protocol) 20 | tenant: '', // TODO enter IoT Services Tenant 21 | user: '', // TODO enter IoT Services User 22 | password: '', // TODO enter IoT Services Password 23 | }; 24 | const helper = new IoTServiceHelper(iotServicesCredentials); 25 | 26 | const deviceAlternateId = ''; // TODO enter alternateId of device 27 | const sensorAlternateId = ''; // TODO enter alternateId of sensor 28 | const capabilityAlternateId = ''; // TODO enter alternateId of capability 29 | assert( 30 | deviceAlternateId 31 | && sensorAlternateId 32 | && capabilityAlternateId, 33 | // eslint-disable-next-line max-len 34 | 'Enter values for variables "deviceAlternateId", "sensorAlternateId" and "capabilityAlternateId" before running this sample', 35 | ); 36 | 37 | async function _sleep(ms) { 38 | // eslint-disable-next-line no-promise-executor-return 39 | return new Promise((resolve) => setTimeout(resolve, ms)); 40 | } 41 | 42 | function generateMeasurementPayload() { 43 | const payload = {}; 44 | // TODO adapt measurement payload to your IoT Services Capability model 45 | payload.temperature = Math.floor(Math.random() * 50); 46 | payload.humidity = Math.floor(Math.random() * 50); 47 | return payload; 48 | } 49 | 50 | async function sendData() { 51 | const device = await helper.getDeviceByAlternateId(deviceAlternateId); 52 | const gateway = await helper.getGateway(device.gatewayId); 53 | 54 | // Sending sample test data for measurements with current timestamp 55 | const now = new Date(); 56 | const measurementPayload = generateMeasurementPayload(); 57 | 58 | if (gateway.protocolId === 'mqtt') { 59 | await helper.ingestDataMqtt(deviceAlternateId, sensorAlternateId, capabilityAlternateId, measurementPayload, now); 60 | } else if (gateway.protocolId === 'rest') { 61 | await helper.ingestDataRest(deviceAlternateId, sensorAlternateId, capabilityAlternateId, measurementPayload, now); 62 | } else { 63 | throw new Error(`Unsupported protocol for this sample: ${gateway.protocolId}`); 64 | } 65 | 66 | // eslint-disable-next-line max-len 67 | console.log(`Successfully sent data with timestamp ${now.toISOString()} for device ${deviceAlternateId}: ${JSON.stringify(measurementPayload)}`); 68 | 69 | const sensor = await helper.getSensorByAlternateId(sensorAlternateId); 70 | const assignments = await client.request({ 71 | url: `${client.navigator.tmDataMapping()}/v1/assignments?sensorId=${sensor.id}`, 72 | }); 73 | 74 | if (assignments && assignments.length === 1) { 75 | await _sleep(20000); 76 | const snapshot = await client.getThingSnapshot(assignments[0].thingId); 77 | console.log(JSON.stringify(snapshot)); 78 | } else { 79 | console.log(`No thing assignment found for sensor ${sensorAlternateId}`); 80 | } 81 | } 82 | 83 | // Entry point of script 84 | (async () => { 85 | try { 86 | await sendData(); 87 | } catch (err) { 88 | console.log(err); 89 | } 90 | })(); 91 | -------------------------------------------------------------------------------- /test/unit/services/ThingService.test.js: -------------------------------------------------------------------------------- 1 | const AssertionUtil = require('../AssertionUtil'); 2 | const LeonardoIoT = require('../../../lib/LeonardoIoT'); 3 | 4 | const appiotMdsUrl = 'https://appiot-mds.cfapps.eu10.hana.ondemand.com'; 5 | 6 | describe('Thing Service', function () { 7 | let client; 8 | 9 | beforeEach(function () { 10 | client = new LeonardoIoT(); 11 | }); 12 | 13 | describe('Thing', function () { 14 | it('should create thing', function () { 15 | const thingPayload = { Name: 'MyThing' }; 16 | client.request = (requestConfig) => { 17 | AssertionUtil.assertRequestConfig(requestConfig, { 18 | url: `${appiotMdsUrl}/Things`, 19 | method: 'POST', 20 | body: thingPayload, 21 | }); 22 | }; 23 | 24 | return client.createThing(thingPayload); 25 | }); 26 | 27 | it('should read single thing', function () { 28 | const thingId = 'MyThing'; 29 | client.request = (requestConfig) => { 30 | AssertionUtil.assertRequestConfig(requestConfig, { 31 | url: `${appiotMdsUrl}/Things('${thingId}')`, 32 | }); 33 | }; 34 | 35 | return client.getThing(thingId); 36 | }); 37 | 38 | it('should read single thing by alternate identifier', function () { 39 | const thingAlternateId = 'MyAlternateThing'; 40 | client.request = (requestConfig) => { 41 | AssertionUtil.assertRequestConfig(requestConfig, { 42 | url: `${appiotMdsUrl}/ThingsByAlternateId('${thingAlternateId}')`, 43 | }); 44 | }; 45 | 46 | return client.getThingByAlternateId(thingAlternateId); 47 | }); 48 | 49 | it('should read multiple things', function () { 50 | client.request = (requestConfig) => { 51 | AssertionUtil.assertRequestConfig(requestConfig, { 52 | url: `${appiotMdsUrl}/Things`, 53 | }); 54 | }; 55 | 56 | return client.getThings(); 57 | }); 58 | 59 | it('should read multiple things with different query parameters', function () { 60 | client.request = (requestConfig) => { 61 | AssertionUtil.assertRequestConfig(requestConfig, { 62 | url: `${appiotMdsUrl}/Things`, 63 | qs: { 64 | $select: '_id,_name', $orderby: '_id', $top: 10, $skip: 5, 65 | }, 66 | }); 67 | }; 68 | 69 | return client.getThings({ 70 | $select: '_id,_name', $orderby: '_id', $top: 10, $skip: 5, 71 | }); 72 | }); 73 | 74 | it('should read multiple things by thing type', function () { 75 | const thingTypeName = 'MyThingType'; 76 | client.request = (requestConfig) => { 77 | AssertionUtil.assertRequestConfig(requestConfig, { 78 | url: `${appiotMdsUrl}/Things`, 79 | qs: { $filter: `_thingType eq '${thingTypeName}'` }, 80 | }); 81 | }; 82 | 83 | return client.getThingsByThingType(thingTypeName); 84 | }); 85 | 86 | it('should read multiple things by thing type with complex filter', function () { 87 | const thingTypeName = 'MyThingType'; 88 | client.request = (requestConfig) => { 89 | AssertionUtil.assertRequestConfig(requestConfig, { 90 | url: `${appiotMdsUrl}/Things`, 91 | qs: { $filter: `_name eq 'test' and _thingType eq '${thingTypeName}'` }, 92 | }); 93 | }; 94 | 95 | return client.getThingsByThingType(thingTypeName, { $filter: '_name eq \'test\'' }); 96 | }); 97 | 98 | it('should delete thing by id', function () { 99 | const thingId = 'MyThing'; 100 | client.request = (requestConfig) => { 101 | AssertionUtil.assertRequestConfig(requestConfig, { 102 | url: `${appiotMdsUrl}/Things('${thingId}')`, 103 | method: 'DELETE', 104 | }); 105 | }; 106 | 107 | return client.deleteThing(thingId); 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /lib/services/PackageService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration parameters for the request that will be sent 3 | * 4 | * @typedef {object} RequestConfig 5 | * @property {object} [headers] - Custom header fields which enrich the request 6 | * @property {boolean} [resolveWithFullResponse=false] - Return full or only body part of the response 7 | * @property {string} [jwt] - Jwt token used in authorization header field 8 | * @property {string[]} [scopes] - List of scopes requested within token for this request 9 | */ 10 | 11 | /** 12 | * Expose functions for most used package APIs 13 | * 14 | * @class PackageService 15 | * @author Soeren Buehler 16 | */ 17 | class PackageService { 18 | /** 19 | * Create a package 20 | * 21 | * @param {object} payload - Request body payload 22 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 23 | * @returns {Promise.<*>} API response 24 | * @see SAP Help API documentation 25 | */ 26 | static createPackage(payload, { 27 | headers = {}, resolveWithFullResponse = false, jwt, scopes, 28 | } = {}) { 29 | return this.request({ 30 | url: `${this.navigator.configPackage()}/Package/v1/Packages`, 31 | method: 'POST', 32 | headers, 33 | body: payload, 34 | resolveWithFullResponse, 35 | jwt, 36 | scopes, 37 | }); 38 | } 39 | 40 | /** 41 | * Read a package 42 | * 43 | * @param {string} packageName - Package identifier 44 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 45 | * @returns {Promise.<*>} API response 46 | * @see SAP Help API documentation 47 | */ 48 | static getPackage(packageName, { 49 | headers = {}, resolveWithFullResponse = false, jwt, scopes, 50 | } = {}) { 51 | return this.request({ 52 | url: `${this.navigator.configPackage()}/Package/v1/Packages('${packageName}')`, 53 | headers, 54 | resolveWithFullResponse, 55 | jwt, 56 | scopes, 57 | }); 58 | } 59 | 60 | /** 61 | * Read all packages filtered by query parameters 62 | * 63 | * @param {object} [queryParameters] - Map of query parameters 64 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 65 | * @returns {Promise.<*>} API response 66 | * @see SAP Help API documentation 67 | */ 68 | static getPackages(queryParameters, { 69 | headers = {}, resolveWithFullResponse = false, jwt, scopes, 70 | } = {}) { 71 | return this.request({ 72 | url: `${this.navigator.configPackage()}/Package/v1/Packages`, 73 | qs: queryParameters, 74 | headers, 75 | resolveWithFullResponse, 76 | jwt, 77 | scopes, 78 | }); 79 | } 80 | 81 | /** 82 | * Delete a package 83 | * 84 | * @param {string} packageName - Package identifier 85 | * @param {string} etag - Latest entity tag 86 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 87 | * @returns {Promise.<*>} API response 88 | * @see SAP Help API documentation 89 | */ 90 | static deletePackage(packageName, etag, { 91 | headers = {}, resolveWithFullResponse, jwt, scopes, 92 | } = {}) { 93 | const enhancedHeaders = headers; 94 | enhancedHeaders['If-Match'] = etag; 95 | return this.request({ 96 | url: `${this.navigator.configPackage()}/Package/v1/Packages('${packageName}')`, 97 | method: 'DELETE', 98 | headers: enhancedHeaders, 99 | resolveWithFullResponse, 100 | jwt, 101 | scopes, 102 | }); 103 | } 104 | } 105 | 106 | module.exports = PackageService; 107 | -------------------------------------------------------------------------------- /lib/services/TimeSeriesStoreService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration parameters for the request that will be sent 3 | * 4 | * @typedef {object} RequestConfig 5 | * @property {object} [headers] - Custom header fields which enrich the request 6 | * @property {boolean} [resolveWithFullResponse=false] - Return full or only body part of the response 7 | * @property {string} [jwt] - Jwt token used in authorization header field 8 | * @property {string[]} [scopes] - List of scopes requested within token for this request 9 | */ 10 | 11 | /** 12 | * Expose functions for most used time series store APIs 13 | * 14 | * @class TimeSeriesStoreService 15 | * @author Soeren Buehler 16 | */ 17 | class TimeSeriesStoreService { 18 | /** 19 | * Create time series data of a thing within retention period 20 | * 21 | * @param {string} thingId - Thing identifier 22 | * @param {string} thingTypeName - Thing type name 23 | * @param {string} propertySetId - Property set identifier 24 | * @param {object} payload - Time series data payload 25 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 26 | * @returns {Promise.<*>} API response 27 | * @see SAP Help API documentation 28 | */ 29 | static createTimeSeriesData(thingId, thingTypeName, propertySetId, payload, { 30 | headers = {}, resolveWithFullResponse, jwt, scopes, 31 | } = {}) { 32 | const url = `${this.navigator.appiotMds()}/Things('${thingId}')/${thingTypeName}/${propertySetId}`; 33 | return this.request({ 34 | url, 35 | method: 'PUT', 36 | headers, 37 | body: payload, 38 | resolveWithFullResponse, 39 | jwt, 40 | scopes, 41 | }); 42 | } 43 | 44 | /** 45 | * Read time series data of a thing within retention period 46 | * 47 | * @param {string} thingId - Thing identifier 48 | * @param {string} thingTypeName - Thing type name 49 | * @param {string} propertySetId - Property set identifier 50 | * @param {object} [queryParameters] - Map of query parameters 51 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 52 | * @returns {Promise.<*>} Result of service request 53 | * @see SAP Help API documentation 54 | */ 55 | static getTimeSeriesData(thingId, thingTypeName, propertySetId, queryParameters, { 56 | headers = {}, resolveWithFullResponse, jwt, scopes, 57 | } = {}) { 58 | return this.request({ 59 | url: `${this.navigator.appiotMds()}/Things('${thingId}')/${thingTypeName}/${propertySetId}`, 60 | qs: queryParameters, 61 | headers, 62 | resolveWithFullResponse, 63 | jwt, 64 | scopes, 65 | }); 66 | } 67 | 68 | /** 69 | * Delete time series data of a thing within retention period 70 | * 71 | * @param {string} thingId - Thing identifier 72 | * @param {string} thingTypeName - Thing type name 73 | * @param {string} propertySetId - Property set identifier 74 | * @param {Date} fromTime - From timestamp in ISO8601 format used for data selection 75 | * @param {Date} toTime - To timestamp in ISO8601 format used for data selection 76 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 77 | * @returns {Promise.<*>} Result of service request 78 | * @see SAP Help API documentation 79 | */ 80 | static deleteTimeSeriesData(thingId, thingTypeName, propertySetId, fromTime, toTime, { 81 | headers = {}, resolveWithFullResponse, jwt, scopes, 82 | } = {}) { 83 | const queryParameters = { 84 | timerange: `${fromTime}-${toTime}`, 85 | }; 86 | return this.request({ 87 | url: `${this.navigator.appiotMds()}/Things('${thingId}')/${thingTypeName}/${propertySetId}`, 88 | qs: queryParameters, 89 | method: 'DELETE', 90 | headers, 91 | resolveWithFullResponse, 92 | jwt, 93 | scopes, 94 | }); 95 | } 96 | } 97 | 98 | module.exports = TimeSeriesStoreService; 99 | -------------------------------------------------------------------------------- /samples/sample_3_thing_setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This example shows the creation of a full configuration (package, property set type and thing type). 3 | * Next this configuration is used to instantiate a single thing, 4 | * which now can be used for further operations like data ingestion of time series read. 5 | * 6 | * After successful operation of the mentioned steps, 7 | * the cleanup will remove all created definitions so the script can be executed multiple times without manual cleanup. 8 | * Feel free to skip the cleanup function to keep created test data. 9 | */ 10 | 11 | const LeonardoIoT = require('sap-iot-sdk'); 12 | 13 | const client = new LeonardoIoT(); 14 | 15 | let packagePayload; 16 | let propertySetTypePayload; 17 | let thingTypePayload; 18 | 19 | async function preparePayloads() { 20 | const tenantInfo = await client.request({ 21 | url: `${client.navigator.businessPartner()}/Tenants`, 22 | }); 23 | 24 | const packageName = `${tenantInfo.value[0].package}.sdk.sample`; 25 | packagePayload = { 26 | Name: packageName, 27 | Scope: 'private', 28 | }; 29 | propertySetTypePayload = { 30 | Name: `${packageName}:RotorBladeMeasurements`, 31 | DataCategory: 'TimeSeriesData', 32 | Properties: [{ Name: 'RotationSpeed', Type: 'NumericFlexible' }, { Name: 'Angle', Type: 'Numeric' }], 33 | }; 34 | thingTypePayload = { 35 | Name: `${packageName}:WindTurbine`, 36 | PropertySets: [ 37 | { Name: 'RotorBlade1', PropertySetType: `${packageName}:RotorBladeMeasurements` }, 38 | { Name: 'RotorBlade2', PropertySetType: `${packageName}:RotorBladeMeasurements` }, 39 | { Name: 'RotorBlade3', PropertySetType: `${packageName}:RotorBladeMeasurements` }, 40 | ], 41 | }; 42 | } 43 | 44 | /** 45 | * Creation of configuration entities package, property set type and thing type 46 | */ 47 | async function createConfiguration() { 48 | await client.createPackage(packagePayload); 49 | console.log(`Package created: ${packagePayload.Name}`); 50 | await client.createPropertySetType(packagePayload.Name, propertySetTypePayload); 51 | console.log(`Property set type created: ${propertySetTypePayload.Name}`); 52 | await client.createThingType(packagePayload.Name, thingTypePayload); 53 | console.log(`Thing type created: ${thingTypePayload.Name}`); 54 | } 55 | 56 | /** 57 | * Creation of thing using created configuration of previous step 58 | */ 59 | async function createThing() { 60 | const rootObjectGroup = await client.getRootObjectGroup(); 61 | const thingPayload = { 62 | _name: 'SampleThing', 63 | _description: { 64 | en: 'Sample thing created with SAP Leonardo IoT SDK', 65 | }, 66 | _thingType: [thingTypePayload.Name], 67 | _objectGroup: rootObjectGroup.objectGroupID, 68 | }; 69 | 70 | const createThingResponse = await client.createThing(thingPayload, { resolveWithFullResponse: true }); 71 | const thingId = createThingResponse.headers.location.split('\'')[1]; 72 | console.log(`Thing created: ${thingId}`); 73 | } 74 | 75 | /** 76 | * Cleanup of configuration and all related thing instances 77 | */ 78 | async function cleanup() { 79 | const packageResponse = await client.getPackage(packagePayload.Name, { resolveWithFullResponse: true }); 80 | const propertySetTypeResponse = await client.getPropertySetType( 81 | propertySetTypePayload.Name, 82 | null, 83 | { resolveWithFullResponse: true }, 84 | ); 85 | const thingTypeResponse = await client.getThingType(thingTypePayload.Name, null, { resolveWithFullResponse: true }); 86 | 87 | const things = await client.getThingsByThingType(thingTypePayload.Name); 88 | things.value.forEach(async (thing) => { 89 | await client.deleteThing(thing._id); 90 | console.log(`Thing deleted: ${thing._id}`); 91 | }); 92 | 93 | await client.deleteThingType(thingTypePayload.Name, thingTypeResponse.headers.etag); 94 | console.log(`Thing type deleted: ${thingTypePayload.Name}`); 95 | await client.deletePropertySetType(propertySetTypePayload.Name, propertySetTypeResponse.headers.etag); 96 | console.log(`Property set type deleted: ${propertySetTypePayload.Name}`); 97 | await client.deletePackage(packagePayload.Name, packageResponse.headers.etag); 98 | console.log(`Package deleted: ${packagePayload.Name}`); 99 | } 100 | 101 | // Entry point of script 102 | (async () => { 103 | try { 104 | await preparePayloads(); 105 | await createConfiguration(); 106 | await createThing(); 107 | await cleanup(); 108 | } catch (err) { 109 | console.log(err); 110 | } 111 | })(); 112 | -------------------------------------------------------------------------------- /.github/workflows/library.yml: -------------------------------------------------------------------------------- 1 | name: Library 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | paths-ignore: 8 | - "samples/**" 9 | - ".github/workflows/pr-samples.yml" 10 | - ".github/workflows/pr-library.yml" 11 | 12 | jobs: 13 | unit-tests: 14 | strategy: 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | - macos-latest 19 | - windows-latest 20 | node-version: 21 | - 14 22 | - 16 23 | - 18 24 | - latest 25 | runs-on: ${{ matrix.os }} 26 | name: Unit tests Node ${{ matrix.node-version }} on ${{ matrix.os }} 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Setup Node ${{ matrix.node-version }} 30 | uses: actions/setup-node@v3 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | check-latest: true 34 | cache: 'npm' 35 | - name: Install dependencies 36 | run: npm install 37 | - name: Run unit tests 38 | run: npm test 39 | 40 | lint: 41 | runs-on: ubuntu-latest 42 | name: Lint SDK 43 | steps: 44 | - uses: actions/checkout@v3 45 | - name: Setup Node 46 | uses: actions/setup-node@v3 47 | with: 48 | node-version: lts/* 49 | check-latest: true 50 | cache: 'npm' 51 | - name: Install dependencies 52 | run: npm install 53 | - name: Lint SDK 54 | run: npm run lint 55 | 56 | sonarcloud: 57 | needs: [unit-tests, lint] 58 | runs-on: ubuntu-latest 59 | name: Sonar Cloud Scan 60 | steps: 61 | - uses: actions/checkout@v3 62 | with: 63 | fetch-depth: 0 64 | - name: Setup Node 65 | uses: actions/setup-node@v3 66 | with: 67 | node-version: lts/* 68 | check-latest: true 69 | cache: 'npm' 70 | - name: Install dependencies 71 | run: npm install 72 | - name: Lint SDK 73 | run: npm run lint:ci 74 | - name: Upload ESLint results 75 | uses: actions/upload-artifact@v3 76 | with: 77 | name: lint-report 78 | path: lint 79 | - name: Run unit tests 80 | run: npm test 81 | - name: Upload code coverage results 82 | uses: actions/upload-artifact@v3 83 | with: 84 | name: code-coverage-report 85 | path: coverage 86 | - name: Resolve file paths for Sonar 87 | shell: bash 88 | run: | 89 | sed -i 's+/home/runner/work/sap-iot-sdk-nodejs/sap-iot-sdk-nodejs+/github/workspace+g' coverage/lcov.info 90 | sed -i 's+/home/runner/work/sap-iot-sdk-nodejs/sap-iot-sdk-nodejs+/github/workspace+g' lint/results.json 91 | - name: Extract application version 92 | id: app-version 93 | shell: bash 94 | run: | 95 | printf %"s\n" "Reading package.json from ./package.json" 96 | PACKAGE_VERSION=$(cat ./package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[[:space:]]') 97 | printf %"s\n" "Version is ${PACKAGE_VERSION}" 98 | echo ::set-output name=version::$PACKAGE_VERSION 99 | - name: SonarCloud Scan 100 | uses: SonarSource/sonarcloud-github-action@v1.9 101 | env: 102 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 103 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 104 | with: 105 | args: | 106 | -Dsonar.projectVersion=${{ steps.app-version.outputs.version}} 107 | 108 | integration-tests: 109 | needs: sonarcloud 110 | strategy: 111 | matrix: 112 | os: 113 | - ubuntu-latest 114 | - macos-latest 115 | - windows-latest 116 | node-version: 117 | - 14 118 | - 16 119 | - 18 120 | - latest 121 | runs-on: ${{ matrix.os }} 122 | name: Integration tests Node ${{ matrix.node-version }} on ${{ matrix.os }} 123 | steps: 124 | - uses: actions/checkout@v3 125 | - name: Setup Node ${{ matrix.node-version }} 126 | uses: actions/setup-node@v3 127 | with: 128 | node-version: ${{ matrix.node-version }} 129 | check-latest: true 130 | cache: 'npm' 131 | - name: Install dependencies 132 | run: npm install 133 | - name: Run integration tests 134 | run: npm run test:integration 135 | env: 136 | VCAP_SERVICES: ${{ secrets.TENANT_CREDENTIALS_EU10_PLAYGROUND }} 137 | -------------------------------------------------------------------------------- /test/unit/services/ThingTypeService.test.js: -------------------------------------------------------------------------------- 1 | const AssertionUtil = require('../AssertionUtil'); 2 | const LeonardoIoT = require('../../../lib/LeonardoIoT'); 3 | 4 | const configThingUrl = 'https://config-thing-sap.cfapps.eu10.hana.ondemand.com'; 5 | 6 | describe('Thing Type Service', function () { 7 | let client; 8 | let queryParameters; 9 | let queryKey; 10 | let queryValue; 11 | 12 | before(function () { 13 | queryParameters = {}; 14 | queryKey = '$expand'; 15 | queryValue = 'Descriptions'; 16 | queryParameters[queryKey] = queryValue; 17 | }); 18 | 19 | beforeEach(function () { 20 | client = new LeonardoIoT(); 21 | }); 22 | 23 | describe('ThingType', function () { 24 | it('should create thingType', function () { 25 | const packageName = 'MyPackage'; 26 | const thingTypePayload = { Name: 'MyThingType' }; 27 | client.request = (requestConfig) => { 28 | AssertionUtil.assertRequestConfig(requestConfig, { 29 | url: `${configThingUrl}/ThingConfiguration/v2/Packages('${packageName}')/ThingTypes`, 30 | method: 'POST', 31 | body: thingTypePayload, 32 | }); 33 | }; 34 | return client.createThingType(packageName, thingTypePayload); 35 | }); 36 | 37 | it('should read single thingType', function () { 38 | const thingTypeName = 'MyThingType'; 39 | client.request = (requestConfig) => { 40 | AssertionUtil.assertRequestConfig(requestConfig, { 41 | url: `${configThingUrl}/ThingConfiguration/v1/ThingTypes('${thingTypeName}')`, 42 | }); 43 | }; 44 | return client.getThingType(thingTypeName); 45 | }); 46 | 47 | it('should read single thingType with query parameters', function () { 48 | const thingTypeName = 'MyThingType'; 49 | const queryParatemeters = {}; 50 | queryParatemeters[queryKey] = queryValue; 51 | client.request = (requestConfig) => { 52 | AssertionUtil.assertRequestConfig(requestConfig, { 53 | url: `${configThingUrl}/ThingConfiguration/v1/ThingTypes('${thingTypeName}')`, 54 | qs: queryParameters, 55 | }); 56 | }; 57 | return client.getThingType(thingTypeName, queryParameters); 58 | }); 59 | 60 | it('should read multiple thingTypes', function () { 61 | client.request = (requestConfig) => { 62 | AssertionUtil.assertRequestConfig(requestConfig, { 63 | url: `${configThingUrl}/ThingConfiguration/v1/ThingTypes`, 64 | }); 65 | }; 66 | return client.getThingTypes(); 67 | }); 68 | 69 | it('should read multiple thingTypes with query parameters', function () { 70 | const queryParatemeters = {}; 71 | queryParatemeters[queryKey] = queryValue; 72 | client.request = (requestConfig) => { 73 | AssertionUtil.assertRequestConfig(requestConfig, { 74 | url: `${configThingUrl}/ThingConfiguration/v1/ThingTypes`, 75 | qs: queryParameters, 76 | }); 77 | }; 78 | return client.getThingTypes(queryParameters); 79 | }); 80 | 81 | it('should read multiple thingType by package', function () { 82 | const packageName = 'MyPackage'; 83 | client.request = (requestConfig) => { 84 | AssertionUtil.assertRequestConfig(requestConfig, { 85 | url: `${configThingUrl}/ThingConfiguration/v1/Packages('${packageName}')/ThingTypes`, 86 | }); 87 | }; 88 | return client.getThingTypesByPackage(packageName); 89 | }); 90 | 91 | it('should read multiple thingType by package with query parameters', function () { 92 | const packageName = 'MyPackage'; 93 | const queryParatemeters = {}; 94 | queryParatemeters[queryKey] = queryValue; 95 | client.request = (requestConfig) => { 96 | AssertionUtil.assertRequestConfig(requestConfig, { 97 | url: `${configThingUrl}/ThingConfiguration/v1/Packages('${packageName}')/ThingTypes`, 98 | qs: queryParatemeters, 99 | }); 100 | }; 101 | return client.getThingTypesByPackage(packageName, queryParameters); 102 | }); 103 | 104 | it('should delete thingType', function () { 105 | const thingTypeName = 'MyThingType'; 106 | const etag = '8f9da184-5af1-4237-8ede-a7fee8ddc57e'; 107 | client.request = (requestConfig) => { 108 | AssertionUtil.assertRequestConfig(requestConfig, { 109 | url: `${configThingUrl}/ThingConfiguration/v1/ThingTypes('${thingTypeName}')`, 110 | method: 'DELETE', 111 | headers: { 'If-Match': etag }, 112 | }); 113 | }; 114 | return client.deleteThingType(thingTypeName, etag); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /lib/services/TimeSeriesColdStoreService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration parameters for the request that will be sent 3 | * 4 | * @typedef {object} RequestConfig 5 | * @property {object} [headers] - Custom header fields which enrich the request 6 | * @property {boolean} [resolveWithFullResponse=false] - Return full or only body part of the response 7 | * @property {string} [jwt] - Jwt token used in authorization header field 8 | * @property {string[]} [scopes] - List of scopes requested within token for this request 9 | */ 10 | 11 | /** 12 | * Expose functions for most used time series cold store APIs 13 | * 14 | * @class TimeSeriesColdStoreService 15 | * @author Soeren Buehler 16 | */ 17 | class TimeSeriesColdStoreService { 18 | /** 19 | * Create time series data of a thing beyond retention period 20 | * 21 | * @param {string} thingId - Thing identifier 22 | * @param {string} thingTypeName - Thing type name 23 | * @param {string} propertySetId - Property set identifier 24 | * @param {object} payload - Time series data payload 25 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 26 | * @returns {Promise.<*>} API response 27 | * @see SAP Help API documentation 28 | */ 29 | static createColdStoreTimeSeriesData(thingId, thingTypeName, propertySetId, payload, { 30 | headers = {}, resolveWithFullResponse, jwt, scopes, 31 | } = {}) { 32 | const url = `${this.navigator.appiotColdstore()}/Things('${thingId}')/${thingTypeName}/${propertySetId}`; 33 | return this.request({ 34 | url, 35 | method: 'PUT', 36 | headers, 37 | body: payload, 38 | resolveWithFullResponse, 39 | jwt, 40 | scopes, 41 | }); 42 | } 43 | 44 | /** 45 | * Read time series data of a thing beyond retention period 46 | * 47 | * @param {string} thingId - Thing identifier 48 | * @param {string} thingTypeName - Thing type name 49 | * @param {string} propertySetId - Property set identifier 50 | * @param {Date} fromTime - From timestamp in ISO8601 format used for data selection 51 | * @param {Date} toTime - To timestamp in ISO8601 format used for data selection 52 | * @param {object} [queryParameters] - Map of query parameters 53 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 54 | * @returns {Promise.<*>} Result of service request 55 | * @see SAP Help API documentation 56 | */ 57 | static getColdStoreTimeSeriesData(thingId, thingTypeName, propertySetId, fromTime, toTime, queryParameters, { 58 | headers = {}, resolveWithFullResponse, jwt, scopes, 59 | } = {}) { 60 | const enhancedQueryParameters = queryParameters || {}; 61 | enhancedQueryParameters.timerange = `${fromTime}-${toTime}`; 62 | return this.request({ 63 | url: `${this.navigator.appiotColdstore()}/Things('${thingId}')/${thingTypeName}/${propertySetId}`, 64 | qs: enhancedQueryParameters, 65 | headers, 66 | resolveWithFullResponse, 67 | jwt, 68 | scopes, 69 | }); 70 | } 71 | 72 | /** 73 | * Delete time series data of a thing beyond retention period 74 | * 75 | * @param {string} thingId - Thing identifier 76 | * @param {string} thingTypeName - Thing type name 77 | * @param {string} propertySetId - Property set identifier 78 | * @param {Date} fromTime - From timestamp in ISO8601 format used for data selection 79 | * @param {Date} toTime - To timestamp in ISO8601 format used for data selection 80 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 81 | * @returns {Promise.<*>} Result of service request 82 | * @see SAP Help API documentation 83 | */ 84 | static deleteColdStoreTimeSeriesData(thingId, thingTypeName, propertySetId, fromTime, toTime, { 85 | headers = {}, resolveWithFullResponse, jwt, scopes, 86 | } = {}) { 87 | const queryParameters = { 88 | timerange: `${fromTime}-${toTime}`, 89 | }; 90 | return this.request({ 91 | url: `${this.navigator.appiotColdstore()}/Things('${thingId}')/${thingTypeName}/${propertySetId}`, 92 | qs: queryParameters, 93 | method: 'DELETE', 94 | headers, 95 | resolveWithFullResponse, 96 | jwt, 97 | scopes, 98 | }); 99 | } 100 | } 101 | 102 | module.exports = TimeSeriesColdStoreService; 103 | -------------------------------------------------------------------------------- /lib/services/TimeSeriesAggregateStoreService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration parameters for the request that will be sent 3 | * 4 | * @typedef {object} RequestConfig 5 | * @property {object} [headers] - Custom header fields which enrich the request 6 | * @property {boolean} [resolveWithFullResponse=false] - Return full or only body part of the response 7 | * @property {string} [jwt] - Jwt token used in authorization header field 8 | * @property {string[]} [scopes] - List of scopes requested within token for this request 9 | */ 10 | 11 | /** 12 | * Expose functions for most used time series aggregate store APIs 13 | * 14 | * @class TimeSeriesAggregateStoreService 15 | * @author Soeren Buehler 16 | */ 17 | class TimeSeriesAggregateStoreService { 18 | /** 19 | * Read snapshot data of a thing 20 | * 21 | * @param {string} thingId - Thing identifier / Unique thing identifier assigned by the creator of a thing (alternateId) 22 | * @param {string} [dataCategory=''] - Filter for data category 23 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 24 | * @returns {Promise.<*>} API response 25 | * @see SAP Help API documentation 26 | */ 27 | static getThingSnapshot(thingId, dataCategory = '', { 28 | headers = {}, resolveWithFullResponse, jwt, scopes, 29 | } = {}) { 30 | return this.request({ 31 | url: `${this.navigator.appiotMds()}/Snapshot(thingId='${thingId}',fromTime='',dataCategory='${dataCategory}')`, 32 | headers, 33 | resolveWithFullResponse, 34 | jwt, 35 | scopes, 36 | }); 37 | } 38 | 39 | /** 40 | * Read snapshot data within a time range for a thing 41 | * 42 | * @param {string} thingId - Thing identifier / Unique thing identifier assigned by the creator of a thing (alternateId) 43 | * @param {string} fromTime - Time from which the snapshot details are retrieved for a non-null value of a property. The value for fromTime must correspond to UTC timestamp 44 | * @param {string} toTime - Time up to which the snapshot details are retrieved for a non-null value of a property. The value for toTime must correspond to UTC timestamp 45 | * @param {string} [dataCategory=''] - Filter for data category 46 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 47 | * @returns {Promise.<*>} API response 48 | * @see SAP Help API documentation 49 | */ 50 | static getThingSnapshotWithinTimeRange(thingId, fromTime, toTime, dataCategory = '', { 51 | headers = {}, resolveWithFullResponse, jwt, scopes, 52 | } = {}) { 53 | // eslint-disable-next-line max-len 54 | const path = `/v2/Snapshot(thingId='${thingId}',fromTime='${fromTime}',toTime='${toTime}',dataCategory='${dataCategory}')`; 55 | return this.request({ 56 | url: `${this.navigator.appiotMds()}${path}`, 57 | headers, 58 | resolveWithFullResponse, 59 | jwt, 60 | scopes, 61 | }); 62 | } 63 | 64 | /** 65 | * Recalculate Aggregates for Time Series Data 66 | * 67 | * @param {string} thingId - Thing identifier / Unique thing identifier assigned by the creator of a thing (alternateId) 68 | * @param {string} thingTypeName - Thing type name 69 | * @param {string} propertySetId - Property set identifier 70 | * @param {string} fromTime - Time from which the snapshot details are retrieved for a non-null value of a property. The value for fromTime must correspond to UTC timestamp 71 | * @param {string} toTime - Time up to which the snapshot details are retrieved for a non-null value of a property. The value for toTime must correspond to UTC timestamp 72 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 73 | * @returns {Promise.<*>} API response 74 | * @see SAP Help API documentation 75 | */ 76 | static async recalculateAggregates(thingId, thingTypeName, propertySetId, fromTime, toTime, { 77 | headers = {}, resolveWithFullResponse, jwt, scopes, 78 | } = {}) { 79 | const queryParameters = { 80 | timerange: `${fromTime}-${toTime}`, 81 | }; 82 | return this.request({ 83 | method: 'POST', 84 | url: `${this.navigator.appiotMds()}/Things('${thingId}')/${thingTypeName}/${propertySetId}/RecalculateAggregate`, 85 | qs: queryParameters, 86 | headers, 87 | resolveWithFullResponse, 88 | jwt, 89 | scopes, 90 | }); 91 | } 92 | } 93 | 94 | module.exports = TimeSeriesAggregateStoreService; 95 | -------------------------------------------------------------------------------- /lib/utils/ConfigurationProvider.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('SAPIoT:ConfigurationProvider'); 2 | const xsenv = require('@sap/xsenv'); 3 | 4 | /** 5 | * This class identifies and provides different configurations i.e. for authentication from local or server environment 6 | * 7 | * @class ConfigurationProvider 8 | * @author Soeren Buehler 9 | */ 10 | class ConfigurationProvider { 11 | /** 12 | * Fetching credentials for authentication in the following order 13 | * 1) Fetch credentials from user provided service 14 | * 2) Fetch credentials from service binding of SAP IoT service 15 | * 3) Fetch credentials from local default-env.json file 16 | * 4) Fetch credentials from environment variables 17 | * This method is not throwing any error in case no credentials have been found as this library can also be used in a mode in which all access tokens are handled manually 18 | * 19 | * @param {string} [serviceName] - Name of service which is providing tenant information 20 | * @returns {{uaaUrl, clientId, clientSecret}|undefined} Xsuaa configuration from SAP IoT instance if it can be found. Otherwise returns undefined. 21 | */ 22 | static getCredentials(serviceName) { 23 | debug('Fetching authentication options'); 24 | const sapIoTService = this._getSAPIoTService(serviceName); 25 | if (sapIoTService && sapIoTService.credentials) { 26 | return sapIoTService.credentials.uaa; 27 | } 28 | return undefined; 29 | } 30 | 31 | /** 32 | * Creating a repository of service destination URLs considering the current landscape. Landscape information is fetched in the following order 33 | * 1) Fetch landscape information from service binding of SAP IoT service 34 | * 2) Fetch landscape information from local default-env.json file 35 | * 3) Fetch landscape information from environment variables 36 | * 37 | * In case no landscape information can be determined the default landscape is EU10. 38 | * This method returns a key value map where key equals the service name (i.e. appiot-mds) and value equals the full destination URI (i.e. https://appiot-mds.cfapps.eu10.hana.ondemand.com) 39 | * 40 | * @param {string} [serviceName] - Name of service which is providing tenant information 41 | * @returns {*} List of endpoints from SAP IoT 42 | */ 43 | static getDestinations(serviceName) { 44 | debug('Fetching destinations options from service binding'); 45 | const sapIoTService = this._getSAPIoTService(serviceName); 46 | if (sapIoTService && sapIoTService.credentials) { 47 | return sapIoTService.credentials.endpoints; 48 | } 49 | return undefined; 50 | } 51 | 52 | /** 53 | * Return service object of bound SAP IoT service instance if available 54 | * 55 | * @param {string} [serviceName] - Name of service which is providing tenant information 56 | * @returns {*} SAP IoT servie object from environment 57 | * @private 58 | */ 59 | static _getSAPIoTService(serviceName) { 60 | if (serviceName) { 61 | return this._getService({ name: serviceName }); 62 | } 63 | return this._getService({ tag: 'leonardoiot' }); 64 | } 65 | 66 | /** 67 | * Return service object of bound XSUAA service instance if available 68 | * 69 | * @returns {*} Xsuaa service object from environment 70 | */ 71 | static getXsuaaService() { 72 | return this._getService({ tag: 'xsuaa' }); 73 | } 74 | 75 | /** 76 | * Return service object which fits query parameters 77 | * 78 | * @param {object} [configuration] - Service configuration query parameters for filtering 79 | * @param {string} [configuration.name] - Filters service by name, selection parameter with highest priority 80 | * @param {string} [configuration.tag] - Filters service by tag, selection parameter with lower priority than name 81 | * @throws {Error} Throws Error if runtime environment configuration cant be loaded 82 | * @returns {object} Matching service object from environment 83 | * @private 84 | */ 85 | static _getService({ name, tag } = {}) { 86 | if (!process.env.VCAP_SERVICES) { 87 | xsenv.loadEnv(); 88 | if (!process.env.VCAP_SERVICES) { 89 | throw new Error('Runtime environment configuration (default-env.json file) missing'); 90 | } 91 | } 92 | 93 | const services = xsenv.readCFServices(); 94 | let result; 95 | if (name) { 96 | result = Object.values(services).find((service) => service && service.name && service.name === name); 97 | } 98 | if (tag && !result) { 99 | result = Object.values(services).find((service) => service && service.tags && service.tags.includes(tag)); 100 | } 101 | return result; 102 | } 103 | } 104 | 105 | module.exports = ConfigurationProvider; 106 | -------------------------------------------------------------------------------- /lib/services/AuthorizationService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration parameters for the request that will be sent 3 | * 4 | * @typedef {object} RequestConfig 5 | * @property {object} [headers] - Custom header fields which enrich the request 6 | * @property {boolean} [resolveWithFullResponse=false] - Return full or only body part of the response 7 | * @property {string} [jwt] - Jwt token used in authorization header field 8 | * @property {string[]} [scopes] - List of scopes requested within token for this request 9 | */ 10 | 11 | /** 12 | * Expose functions for most used authorization APIs 13 | * 14 | * @class AuthorizationService 15 | * @author Soeren Buehler 16 | */ 17 | class AuthorizationService { 18 | /** 19 | * Create an object group 20 | * 21 | * @param {object} payload - Request body payload 22 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 23 | * @returns {Promise.<*>} API response 24 | * @see SAP Help API documentation 25 | */ 26 | static createObjectGroup(payload, { 27 | headers = {}, resolveWithFullResponse, jwt, scopes, 28 | } = {}) { 29 | return this.request({ 30 | url: `${this.navigator.authorization()}/ObjectGroups`, 31 | method: 'POST', 32 | headers, 33 | body: payload, 34 | resolveWithFullResponse, 35 | jwt, 36 | scopes, 37 | }); 38 | } 39 | 40 | /** 41 | * Read an object group 42 | * 43 | * @param {string} objectGroupId - Object group identifier 44 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 45 | * @returns {Promise.<*>} API response 46 | * @see SAP Help API documentation 47 | */ 48 | static getObjectGroup(objectGroupId, { 49 | headers = {}, resolveWithFullResponse, jwt, scopes, 50 | } = {}) { 51 | return this.request({ 52 | url: `${this.navigator.authorization()}/ObjectGroups('${objectGroupId}')`, 53 | headers, 54 | resolveWithFullResponse, 55 | jwt, 56 | scopes, 57 | }); 58 | } 59 | 60 | /** 61 | * Read all object groups filtered by query parameters 62 | * 63 | * @param {object} [queryParameters] - Map of query parameters 64 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 65 | * @returns {Promise.<*>} API response 66 | * @see SAP Help API documentation 67 | */ 68 | static getObjectGroups(queryParameters, { 69 | headers = {}, resolveWithFullResponse, jwt, scopes, 70 | } = {}) { 71 | return this.request({ 72 | url: `${this.navigator.authorization()}/ObjectGroups`, 73 | qs: queryParameters, 74 | headers, 75 | resolveWithFullResponse, 76 | jwt, 77 | scopes, 78 | }); 79 | } 80 | 81 | /** 82 | * Read the root object group 83 | * 84 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 85 | * @returns {Promise.<*>} API response 86 | * @see SAP Help API documentation 87 | */ 88 | static getRootObjectGroup({ 89 | headers = {}, resolveWithFullResponse, jwt, scopes, 90 | } = {}) { 91 | return this.request({ 92 | url: `${this.navigator.authorization()}/ObjectGroups/TenantRoot`, 93 | headers, 94 | resolveWithFullResponse, 95 | jwt, 96 | scopes, 97 | }); 98 | } 99 | 100 | /** 101 | * Delete an object group 102 | * 103 | * @param {string} objectGroupId - Object group identifier 104 | * @param {string} etag - Latest entity tag 105 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 106 | * @returns {Promise.<*>} API response 107 | * @see SAP Help API documentation 108 | */ 109 | static deleteObjectGroup(objectGroupId, etag, { 110 | headers = {}, resolveWithFullResponse, jwt, scopes, 111 | } = {}) { 112 | const enhancedHeaders = headers; 113 | enhancedHeaders['If-Match'] = etag; 114 | return this.request({ 115 | url: `${this.navigator.authorization()}/ObjectGroups('${objectGroupId}')`, 116 | method: 'DELETE', 117 | headers: enhancedHeaders, 118 | resolveWithFullResponse, 119 | jwt, 120 | scopes, 121 | }); 122 | } 123 | } 124 | 125 | module.exports = AuthorizationService; 126 | -------------------------------------------------------------------------------- /samples/helper/IoTServicesHelper.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('LeonardoIoT:Sample:IoTServicesHelper'); 2 | const assert = require('assert'); 3 | const rp = require('request-promise-native'); 4 | const mqtt = require('async-mqtt'); 5 | 6 | class IoTServicesHelper { 7 | constructor(iotServicesCredentials) { 8 | assert( 9 | iotServicesCredentials.host 10 | && iotServicesCredentials.tenant 11 | && iotServicesCredentials.user 12 | && iotServicesCredentials.password, 13 | 'Enter iot services credentials before running this sample', 14 | ); 15 | 16 | this.host = iotServicesCredentials.host; 17 | this.tenant = iotServicesCredentials.tenant; 18 | this.user = iotServicesCredentials.user; 19 | this.password = iotServicesCredentials.password; 20 | } 21 | 22 | async ingestDataRest(deviceAlternateId, sensorAlternateId, capabilityAlternateId, measuresPayload, messageTimestamp) { 23 | const payload = { 24 | sensorAlternateId, 25 | capabilityAlternateId, 26 | measures: [measuresPayload], 27 | }; 28 | 29 | if (messageTimestamp) { 30 | payload.timestamp = messageTimestamp; 31 | } 32 | 33 | const device = await this.getDeviceByAlternateId(deviceAlternateId); 34 | const certificate = await this._getCertificate(device.id); 35 | 36 | return rp({ 37 | url: `https://${this.host}/iot/gateway/rest/measures/${deviceAlternateId}`, 38 | method: 'POST', 39 | body: payload, 40 | agentOptions: { 41 | key: certificate.key, 42 | cert: certificate.cert, 43 | passphrase: certificate.secret, 44 | }, 45 | }); 46 | } 47 | 48 | async ingestDataMqtt(deviceAlternateId, sensorAlternateId, capabilityAlternateId, measuresPayload, messageTimestamp) { 49 | const payload = { 50 | sensorAlternateId, 51 | capabilityAlternateId, 52 | measures: [measuresPayload], 53 | }; 54 | 55 | if (messageTimestamp) { 56 | payload.timestamp = messageTimestamp; 57 | } 58 | 59 | debug(`Sending message to IoTS: ${JSON.stringify(payload)}`); 60 | const mqttClient = await this._createMqttClient(deviceAlternateId); 61 | if (mqttClient._client.connected) { 62 | await mqttClient.publish(`measures/${deviceAlternateId}`, JSON.stringify(payload)); 63 | } else { 64 | throw new Error('MQTT client not connected'); 65 | } 66 | } 67 | 68 | async getGateway(gatewayId) { 69 | return this._instanceRequest({ relativeUrl: `/gateways/${gatewayId}` }); 70 | } 71 | 72 | async getDeviceByAlternateId(alternateId) { 73 | const result = await this._instanceRequest({ relativeUrl: `/devices?filter=alternateId eq '${alternateId}'` }); 74 | return result[0]; 75 | } 76 | 77 | async getSensorByAlternateId(alternateId) { 78 | const result = await this._instanceRequest({ relativeUrl: `/sensors?filter=alternateId eq '${alternateId}'` }); 79 | return result[0]; 80 | } 81 | 82 | async _getCertificate(deviceId) { 83 | const result = await this._instanceRequest({ 84 | relativeUrl: `/devices/${deviceId}/authentications/clientCertificate/pem`, 85 | }); 86 | const key = result.pem.substring(0, result.pem.indexOf('-----BEGIN CERTIFICATE-----')); 87 | const cert = result.pem.substring(key.length, result.pem.length); 88 | const { secret } = result; 89 | return { key, cert, secret }; 90 | } 91 | 92 | async _createMqttClient(deviceAlternateId) { 93 | const device = await this.getDeviceByAlternateId(deviceAlternateId); 94 | const certificate = await this._getCertificate(device.id); 95 | return new Promise(((resolve, reject) => { 96 | const url = `mqtts://${this.host}:8883`; 97 | 98 | const mqttClient = mqtt.connect(url, { 99 | key: certificate.key, 100 | cert: certificate.cert, 101 | rejectUnauthorized: true, 102 | passphrase: certificate.secret, 103 | clientId: deviceAlternateId, 104 | }); 105 | 106 | mqttClient.on('connect', async () => { 107 | debug('MQTT connection established'); 108 | mqttClient.subscribe(`commands/${deviceAlternateId}`); 109 | mqttClient.subscribe(`measures/${deviceAlternateId}`); 110 | resolve(mqttClient); 111 | }); 112 | 113 | mqttClient.on('error', (err) => { 114 | debug(`MQTT client creation failed: ${err}`); 115 | reject(err); 116 | }); 117 | })); 118 | } 119 | 120 | async _instanceRequest({ relativeUrl, method = 'GET', headers = {} } = {}) { 121 | const url = `https://${this.host}/iot/core/api/v1/tenant/${this.tenant}${relativeUrl}`; 122 | const requestHeaders = { 123 | ...headers, 124 | Authorization: `Basic ${Buffer.from(`${this.user}:${this.password}`).toString('base64')}`, 125 | }; 126 | return rp({ 127 | url, method, requestHeaders, json: true, 128 | }); 129 | } 130 | } 131 | 132 | module.exports = IoTServicesHelper; 133 | -------------------------------------------------------------------------------- /test/unit/utils/ConfigurationProvider.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const proxyquire = require('proxyquire'); 3 | const ConfigurationProvider = require('../../../lib/utils/ConfigurationProvider'); 4 | 5 | describe('ConfigurationProvider', function () { 6 | let tmpVcapServices; 7 | 8 | describe('Authentication', function () { 9 | beforeEach(function () { 10 | tmpVcapServices = JSON.parse(JSON.stringify(process.env.VCAP_SERVICES)); 11 | }); 12 | 13 | afterEach(function () { 14 | process.env.VCAP_SERVICES = JSON.parse(JSON.stringify(tmpVcapServices)); 15 | }); 16 | 17 | it('should get credentials from service broker service', function () { 18 | // eslint-disable-next-line max-len 19 | process.env.VCAP_SERVICES = '{"iotae":[{"name":"internal","credentials":{"uaa":{"url":"ServiceBrokerUaaUrl","clientid":"ServiceBrokerClientId","clientsecret":"ServiceBrokerClientSecret"}},"tags":["leonardoiot"]}]}'; 20 | 21 | const authentication = ConfigurationProvider.getCredentials(); 22 | assert.strictEqual(authentication.url, 'ServiceBrokerUaaUrl', 'Unexpected UAA url'); 23 | assert.strictEqual(authentication.clientid, 'ServiceBrokerClientId', 'Unexpected Client ID'); 24 | assert.strictEqual(authentication.clientsecret, 'ServiceBrokerClientSecret', 'Unexpected Client secret'); 25 | }); 26 | 27 | it('should get credentials without any settings', function () { 28 | process.env.VCAP_SERVICES = '{}'; 29 | const authentication = ConfigurationProvider.getCredentials(); 30 | assert(authentication === undefined, 'Unexpected return value'); 31 | }); 32 | }); 33 | 34 | describe('Destination', function () { 35 | beforeEach(function () { 36 | tmpVcapServices = JSON.parse(JSON.stringify(process.env.VCAP_SERVICES)); 37 | process.env.VCAP_SERVICES = '{}'; 38 | }); 39 | 40 | afterEach(function () { 41 | process.env.VCAP_SERVICES = JSON.parse(JSON.stringify(tmpVcapServices)); 42 | }); 43 | 44 | it('should get destinations from environment', function () { 45 | // eslint-disable-next-line max-len 46 | process.env.VCAP_SERVICES = '{"iotae":[{"credentials":{"endpoints":{"appiot-mds":"https://appiot-mds-backup.cfapps.de01.hana.ondemand.com"}},"tags":["leonardoiot"]}]}'; 47 | 48 | const destinations = ConfigurationProvider.getDestinations(); 49 | assert.strictEqual(destinations['appiot-mds'], 'https://appiot-mds-backup.cfapps.de01.hana.ondemand.com', 'Unexpected destination'); 50 | }); 51 | 52 | it('should get destinations without any settings', function () { 53 | process.env.VCAP_SERVICES = '{}'; 54 | const authentication = ConfigurationProvider.getDestinations(); 55 | assert(authentication === undefined, 'Unexpected return value'); 56 | }); 57 | }); 58 | 59 | describe('Get service filtered', function () { 60 | beforeEach(function () { 61 | tmpVcapServices = JSON.parse(JSON.stringify(process.env.VCAP_SERVICES)); 62 | }); 63 | 64 | afterEach(function () { 65 | process.env.VCAP_SERVICES = JSON.parse(JSON.stringify(tmpVcapServices)); 66 | }); 67 | 68 | it('should get existing leonardo iot service by tag', function () { 69 | const service = ConfigurationProvider._getService({ tag: 'leonardoiot' }); 70 | assert.strictEqual(service.tags[0], 'leonardoiot', 'Unexpected service'); 71 | }); 72 | 73 | it('should get existing xsuaa service by tag', function () { 74 | const service = ConfigurationProvider._getService({ tag: 'xsuaa' }); 75 | assert.strictEqual(service.tags[0], 'xsuaa', 'Unexpected service'); 76 | }); 77 | 78 | it('should not get existing service by tag', function () { 79 | const service = ConfigurationProvider._getService({ tag: 'notExisting' }); 80 | assert.strictEqual(service, undefined, 'Unexpected service'); 81 | }); 82 | 83 | it('should get existing service by name', function () { 84 | const service = ConfigurationProvider._getService({ name: 'iot_internal' }); 85 | assert.strictEqual(service.name, 'iot_internal', 'Unexpected service'); 86 | }); 87 | 88 | it('should get existing user-provided service by name', function () { 89 | const service = ConfigurationProvider._getService({ name: 'sap-iot-account-test' }); 90 | assert.strictEqual(service.name, 'sap-iot-account-test', 'Unexpected service'); 91 | }); 92 | 93 | it('should not get existing service by name', function () { 94 | const service = ConfigurationProvider._getService({ name: 'notExisting' }); 95 | assert.strictEqual(service, undefined, 'Unexpected service'); 96 | }); 97 | 98 | it('should throw error for missing environment configuration', function () { 99 | const xsenvStub = { loadEnv: () => { } }; 100 | const ProxyquireConfigurationProvider = proxyquire('../../../lib/utils/ConfigurationProvider', { '@sap/xsenv': xsenvStub }); 101 | 102 | delete process.env.VCAP_SERVICES; 103 | assert.throws(() => ProxyquireConfigurationProvider._getService(), Error, 'Expected Error was not thrown'); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/unit/mocha.env.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len, no-console */ 2 | console.log('Setting mocha test environment variables'); 3 | console.log('SET VCAP_SERVICES'); 4 | process.env.VCAP_SERVICES = JSON.stringify({ 5 | iotae: [{ 6 | name: 'iot_internal', 7 | plan: 'standard', 8 | tags: ['leonardoiot'], 9 | credentials: { 10 | endpoints: { 11 | 'tm-data-mapping': 'https://tm-data-mapping.cfapps.eu10.hana.ondemand.com', 12 | authorization: 'https://authorization.cfapps.eu10.hana.ondemand.com', 13 | 'appiot-mds': 'https://appiot-mds.cfapps.eu10.hana.ondemand.com', 14 | 'appiot-coldstore': 'https://appiot-coldstore.cfapps.eu10.hana.ondemand.com', 15 | 'config-thing-sap': 'https://config-thing-sap.cfapps.eu10.hana.ondemand.com', 16 | 'config-package-sap': 'https://config-package-sap.cfapps.eu10.hana.ondemand.com', 17 | 'analytics-thing-sap': 'https://analytics-thing-sap.cfapps.eu10.hana.ondemand.com', 18 | 'rules-designtime': 'https://sap-iot-noah-live-rules-designtime.cfapps.eu10.hana.ondemand.com', 19 | 'business-partner': 'https://business-partner.cfapps.eu10.hana.ondemand.com', 20 | }, 21 | uaa: { 22 | uaadomain: 'authentication.eu10.hana.ondemand.com', 23 | tenantmode: 'dedicated', 24 | sburl: 'https://internal-xsuaa.authentication.eu10.hana.ondemand.com', 25 | clientid: 'MyClientId', 26 | apiurl: 'https://api.authentication.eu10.hana.ondemand.com', 27 | xsappname: 'saptest!b16977|iotae_service!b5', 28 | identityzone: 'saptest', 29 | identityzoneid: '92da712a-4ce5-40d9-9d8f-b6a6d47a58aa', 30 | verificationkey: '-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyp0tzyIJXpPYJxbkOzaXXahj90c6wJqeLkTFiPZu8yF0jQTPtIIEFwSRetJqImI+iJ9EaF0ZsemzZiptMlrVTBZec2JM4pJv/OAvJeT8I7EKu57IXDeGos+Pxjj04SBqCnCvIvtlwdsCTzotRUv2fEL7NJXzxtpxQ8AQRkEQ4+FAIe/yGX8cP/dIoXwbdM6NvkDU3QHcjHMPdZ6/s+sI+E2orFv4qYTj8NYqymXeJeWe31xSL/fJaX3Wo0NaoZuyh0MJOvA0D7bWqKw/ZBF7A05PiqozeAzhYeSvQSsNQ2dc9tmDadLTF8Q9BwURgejGOpvAxdJ3wEVWTohC3Sv6nQIDAQAB-----END PUBLIC KEY-----', 31 | clientsecret: 'MyClientSecret', 32 | tenantid: '92da712a-4ce5-40d9-9d8f-b6a6d47a58aa', 33 | url: 'https://saptest.authentication.eu10.hana.ondemand.com', 34 | }, 35 | }, 36 | }], 37 | 'user-provided': [ 38 | { 39 | name: 'sap-iot-account-test', 40 | credentials: { 41 | endpoints: { 42 | 'appiot-mds': 'https://appiot-mds-backup.cfapps.de01.hana.ondemand.com', 43 | }, 44 | uaa: { 45 | url: 'https://testAccountUrl', 46 | clientid: 'testAccountId', 47 | clientsecret: 'testAccountSecret', 48 | verificationkey: '-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyp0tzyIJXpPYJxbkOzaXXahj90c6wJqeLkTFiPZu8yF0jQTPtIIEFwSRetJqImI+iJ9EaF0ZsemzZiptMlrVTBZec2JM4pJv/OAvJeT8I7EKu57IXDeGos+Pxjj04SBqCnCvIvtlwdsCTzotRUv2fEL7NJXzxtpxQ8AQRkEQ4+FAIe/yGX8cP/dIoXwbdM6NvkDU3QHcjHMPdZ6/s+sI+E2orFv4qYTj8NYqymXeJeWe31xSL/fJaX3Wo0NaoZuyh0MJOvA0D7bWqKw/ZBF7A05PiqozeAzhYeSvQSsNQ2dc9tmDadLTF8Q9BwURgejGOpvAxdJ3wEVWTohC3Sv6nQIDAQAB-----END PUBLIC KEY-----', 49 | }, 50 | }, 51 | }, 52 | { 53 | name: 'sap-iot-account-dev', 54 | credentials: { 55 | endpoints: { 56 | 'appiot-mds': 'https://appiot-mds-backup.cfapps.de01.hana.ondemand.com', 57 | }, 58 | uaa: { 59 | url: 'https://devAccountUrl', 60 | clientid: 'devAccountId', 61 | clientsecret: 'devAccountSecret', 62 | verificationkey: '-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyp0tzyIJXpPYJxbkOzaXXahj90c6wJqeLkTFiPZu8yF0jQTPtIIEFwSRetJqImI+iJ9EaF0ZsemzZiptMlrVTBZec2JM4pJv/OAvJeT8I7EKu57IXDeGos+Pxjj04SBqCnCvIvtlwdsCTzotRUv2fEL7NJXzxtpxQ8AQRkEQ4+FAIe/yGX8cP/dIoXwbdM6NvkDU3QHcjHMPdZ6/s+sI+E2orFv4qYTj8NYqymXeJeWe31xSL/fJaX3Wo0NaoZuyh0MJOvA0D7bWqKw/ZBF7A05PiqozeAzhYeSvQSsNQ2dc9tmDadLTF8Q9BwURgejGOpvAxdJ3wEVWTohC3Sv6nQIDAQAB-----END PUBLIC KEY-----', 63 | }, 64 | }, 65 | }, 66 | ], 67 | xsuaa: [ 68 | { 69 | credentials: { 70 | apiurl: 'https://api.authentication.eu10.hana.ondemand.com', 71 | clientid: 'xsuaaClientId', 72 | clientsecret: 'xsuaaClientSecret', 73 | identityzone: 'sap-test', 74 | identityzoneid: 'ade586c6-f5b1-4ddc-aecb-ead3c2e6e725', 75 | verificationkey: '-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyp0tzyIJXpPYJxbkOzaXXahj90c6wJqeLkTFiPZu8yF0jQTPtIIEFwSRetJqImI+iJ9EaF0ZsemzZiptMlrVTBZec2JM4pJv/OAvJeT8I7EKu57IXDeGos+Pxjj04SBqCnCvIvtlwdsCTzotRUv2fEL7NJXzxtpxQ8AQRkEQ4+FAIe/yGX8cP/dIoXwbdM6NvkDU3QHcjHMPdZ6/s+sI+E2orFv4qYTj8NYqymXeJeWe31xSL/fJaX3Wo0NaoZuyh0MJOvA0D7bWqKw/ZBF7A05PiqozeAzhYeSvQSsNQ2dc9tmDadLTF8Q9BwURgejGOpvAxdJ3wEVWTohC3Sv6nQIDAQAB-----END PUBLIC KEY-----', 76 | sburl: 'https://internal-xsuaa.authentication.eu10.hana.ondemand.com', 77 | tenantid: 'ade586c6-f5b1-4ddc-aecb-ead3c2e6e725', 78 | tenantmode: 'dedicated', 79 | uaadomain: 'authentication.eu10.hana.ondemand.com', 80 | url: 'https://sap-test.authentication.eu10.hana.ondemand.com', 81 | xsappname: 'sap-test!t4969', 82 | }, 83 | name: 'test-uaa', 84 | tags: [ 85 | 'xsuaa', 86 | ], 87 | }, 88 | ], 89 | }); 90 | -------------------------------------------------------------------------------- /.github/workflows/pr-library.yml: -------------------------------------------------------------------------------- 1 | name: PR Library 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | paths-ignore: 7 | - "**.md" 8 | - "samples/**" 9 | - ".github/workflows/pr-samples.yml" 10 | 11 | jobs: 12 | unit-tests: 13 | strategy: 14 | matrix: 15 | os: 16 | - ubuntu-latest 17 | - macos-latest 18 | - windows-latest 19 | node-version: 20 | - 14 21 | - 16 22 | - 18 23 | - latest 24 | runs-on: ${{ matrix.os }} 25 | name: Unit tests Node ${{ matrix.node-version }} on ${{ matrix.os }} 26 | steps: 27 | - uses: actions/checkout@v3 28 | - name: Setup Node ${{ matrix.node-version }} 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | check-latest: true 33 | cache: 'npm' 34 | - name: Install dependencies 35 | run: npm install 36 | - name: Run unit tests 37 | run: npm test 38 | 39 | lint: 40 | runs-on: ubuntu-latest 41 | name: Lint SDK 42 | steps: 43 | - uses: actions/checkout@v3 44 | - name: Setup Node 45 | uses: actions/setup-node@v3 46 | with: 47 | node-version: lts/* 48 | check-latest: true 49 | cache: 'npm' 50 | - name: Install dependencies 51 | run: npm install 52 | - name: Lint SDK 53 | run: npm run lint 54 | 55 | sonarcloud: 56 | needs: [unit-tests, lint] 57 | runs-on: ubuntu-latest 58 | name: Sonar Cloud Scan 59 | steps: 60 | - uses: actions/checkout@v3 61 | with: 62 | fetch-depth: 0 63 | - name: Setup Node 64 | uses: actions/setup-node@v3 65 | with: 66 | node-version: lts/* 67 | check-latest: true 68 | cache: 'npm' 69 | - name: Install dependencies 70 | run: npm install 71 | - name: Lint SDK 72 | run: npm run lint:ci 73 | - name: Upload ESLint results 74 | uses: actions/upload-artifact@v3 75 | with: 76 | name: lint-report 77 | path: lint 78 | - name: Run unit tests 79 | run: npm test 80 | - name: Upload code coverage results 81 | uses: actions/upload-artifact@v3 82 | with: 83 | name: code-coverage-report 84 | path: coverage 85 | - name: Resolve file paths for Sonar 86 | shell: bash 87 | run: | 88 | sed -i 's+/home/runner/work/sap-iot-sdk-nodejs/sap-iot-sdk-nodejs+/github/workspace+g' coverage/lcov.info 89 | sed -i 's+/home/runner/work/sap-iot-sdk-nodejs/sap-iot-sdk-nodejs+/github/workspace+g' lint/results.json 90 | - name: Extract application version 91 | id: app-version 92 | shell: bash 93 | run: | 94 | printf %"s\n" "Reading package.json from ./package.json" 95 | PACKAGE_VERSION=$(cat ./package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[[:space:]]') 96 | printf %"s\n" "Version is ${PACKAGE_VERSION}" 97 | echo ::set-output name=version::$PACKAGE_VERSION 98 | - name: SonarCloud Scan 99 | uses: SonarSource/sonarcloud-github-action@v1.9 100 | env: 101 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 102 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 103 | with: 104 | args: | 105 | -Dsonar.projectVersion=${{ steps.app-version.outputs.version}} 106 | 107 | integration-tests: 108 | needs: sonarcloud 109 | strategy: 110 | matrix: 111 | os: 112 | - ubuntu-latest 113 | - macos-latest 114 | - windows-latest 115 | node-version: 116 | - 14 117 | - 16 118 | - 18 119 | - latest 120 | runs-on: ${{ matrix.os }} 121 | name: Integration tests Node ${{ matrix.node-version }} on ${{ matrix.os }} 122 | steps: 123 | - uses: actions/checkout@v3 124 | - name: Setup Node ${{ matrix.node-version }} 125 | uses: actions/setup-node@v3 126 | with: 127 | node-version: ${{ matrix.node-version }} 128 | check-latest: true 129 | cache: 'npm' 130 | - name: Install dependencies 131 | run: npm install 132 | - name: Run integration tests 133 | run: npm run test:integration 134 | env: 135 | VCAP_SERVICES: ${{ secrets.TENANT_CREDENTIALS_EU10_PLAYGROUND }} 136 | 137 | dependency-check: 138 | runs-on: ubuntu-latest 139 | name: Check SDK dependencies 140 | steps: 141 | - uses: actions/checkout@v3 142 | - name: Setup Node 143 | uses: actions/setup-node@v3 144 | with: 145 | node-version: lts/* 146 | check-latest: true 147 | cache: 'npm' 148 | - name: Install dependencies 149 | run: npm install 150 | - name: Run dependency check 151 | run: npm run checkDependencies 152 | 153 | check-markdown-links: 154 | runs-on: ubuntu-latest 155 | name: Check for dead links in markdown documents 156 | steps: 157 | - uses: actions/checkout@v3 158 | - name: Run markdown link check 159 | uses: gaurav-nelson/github-action-markdown-link-check@v1 160 | with: 161 | max-depth: 1 162 | -------------------------------------------------------------------------------- /lib/services/ThingTypeService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration parameters for the request that will be sent 3 | * 4 | * @typedef {object} RequestConfig 5 | * @property {object} [headers] - Custom header fields which enrich the request 6 | * @property {boolean} [resolveWithFullResponse=false] - Return full or only body part of the response 7 | * @property {string} [jwt] - Jwt token used in authorization header field 8 | * @property {string[]} [scopes] - List of scopes requested within token for this request 9 | */ 10 | 11 | /** 12 | * Expose functions for most used thing type APIs 13 | * 14 | * @class ThingTypeService 15 | * @author Soeren Buehler 16 | */ 17 | class ThingTypeService { 18 | /** 19 | * Create a thing type 20 | * 21 | * @param {string} packageName - Package identifier / name 22 | * @param {object} payload - Request body payload 23 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 24 | * @returns {Promise.<*>} API response 25 | * @see SAP Help API documentation 26 | */ 27 | static createThingType(packageName, payload, { 28 | headers = {}, resolveWithFullResponse = false, jwt, scopes, 29 | } = {}) { 30 | return this.request({ 31 | url: `${this.navigator.configThing()}/ThingConfiguration/v2/Packages('${packageName}')/ThingTypes`, 32 | method: 'POST', 33 | headers, 34 | body: payload, 35 | resolveWithFullResponse, 36 | jwt, 37 | scopes, 38 | }); 39 | } 40 | 41 | /** 42 | * Read a thing type 43 | * 44 | * @param {string} thingTypeName - Thing type identifier 45 | * @param {object} [queryParameters] - Map of query parameters 46 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 47 | * @returns {Promise.<*>} API response 48 | * @see SAP Help API documentation 49 | */ 50 | static getThingType(thingTypeName, queryParameters, { 51 | headers = {}, resolveWithFullResponse = false, jwt, scopes, 52 | } = {}) { 53 | return this.request({ 54 | url: `${this.navigator.configThing()}/ThingConfiguration/v1/ThingTypes('${thingTypeName}')`, 55 | qs: queryParameters, 56 | headers, 57 | resolveWithFullResponse, 58 | jwt, 59 | scopes, 60 | }); 61 | } 62 | 63 | /** 64 | * Read all thing types filtered by query parameters 65 | * 66 | * @param {object} [queryParameters] - Map of query parameters 67 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 68 | * @returns {Promise.<*>} API response 69 | * @see SAP Help API documentation 70 | */ 71 | static getThingTypes(queryParameters, { 72 | headers = {}, resolveWithFullResponse, jwt, scopes, 73 | } = {}) { 74 | return this.request({ 75 | url: `${this.navigator.configThing()}/ThingConfiguration/v1/ThingTypes`, 76 | qs: queryParameters, 77 | headers, 78 | resolveWithFullResponse, 79 | jwt, 80 | scopes, 81 | }); 82 | } 83 | 84 | /** 85 | * Read all thing types for a package 86 | * 87 | * @param {string} packageName - Package identifier 88 | * @param {object} [queryParameters] - Map of query parameters 89 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 90 | * @returns {Promise.<*>} API response 91 | * @see SAP Help API documentation 92 | */ 93 | static getThingTypesByPackage(packageName, queryParameters, { 94 | headers = {}, resolveWithFullResponse, jwt, scopes, 95 | } = {}) { 96 | return this.request({ 97 | url: `${this.navigator.configThing()}/ThingConfiguration/v1/Packages('${packageName}')/ThingTypes`, 98 | qs: queryParameters, 99 | headers, 100 | resolveWithFullResponse, 101 | jwt, 102 | scopes, 103 | }); 104 | } 105 | 106 | /** 107 | * Delete a thing type 108 | * 109 | * @param {string} thingTypeName - Thing type identifier 110 | * @param {string} etag - Latest entity tag 111 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 112 | * @returns {Promise.<*>} API response 113 | * @see SAP Help API documentation 114 | */ 115 | static deleteThingType(thingTypeName, etag, { 116 | headers = {}, resolveWithFullResponse, jwt, scopes, 117 | } = {}) { 118 | const enhancedHeaders = headers; 119 | enhancedHeaders['If-Match'] = etag; 120 | return this.request({ 121 | url: `${this.navigator.configThing()}/ThingConfiguration/v1/ThingTypes('${thingTypeName}')`, 122 | method: 'DELETE', 123 | headers: enhancedHeaders, 124 | resolveWithFullResponse, 125 | jwt, 126 | scopes, 127 | }); 128 | } 129 | } 130 | 131 | module.exports = ThingTypeService; 132 | -------------------------------------------------------------------------------- /lib/services/PropertySetTypeService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration parameters for the request that will be sent 3 | * 4 | * @typedef {object} RequestConfig 5 | * @property {object} [headers] - Custom header fields which enrich the request 6 | * @property {boolean} [resolveWithFullResponse=false] - Return full or only body part of the response 7 | * @property {string} [jwt] - Jwt token used in authorization header field 8 | * @property {string[]} [scopes] - List of scopes requested within token for this request 9 | */ 10 | 11 | /** 12 | * Expose functions for most used property set type APIs 13 | * 14 | * @class PropertySetTypeService 15 | * @author Soeren Buehler 16 | */ 17 | class PropertySetTypeService { 18 | /** 19 | * Create a property set type 20 | * 21 | * @param {string} packageName - Package identifier 22 | * @param {object} payload - Request body payload 23 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 24 | * @returns {Promise.<*>} API response 25 | * @see SAP Help API documentation 26 | */ 27 | static createPropertySetType(packageName, payload, { 28 | headers = {}, resolveWithFullResponse = false, jwt, scopes, 29 | } = {}) { 30 | return this.request({ 31 | url: `${this.navigator.configThing()}/ThingConfiguration/v1/Packages('${packageName}')/PropertySetTypes`, 32 | method: 'POST', 33 | headers, 34 | body: payload, 35 | resolveWithFullResponse, 36 | jwt, 37 | scopes, 38 | }); 39 | } 40 | 41 | /** 42 | * Read a property set type 43 | * 44 | * @param {string} propertySetTypeName - Property set type identifier 45 | * @param {object} [queryParameters] - Map of query parameters 46 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 47 | * @returns {Promise.<*>} API response 48 | * @see SAP Help API documentation 49 | */ 50 | static getPropertySetType(propertySetTypeName, queryParameters, { 51 | headers = {}, resolveWithFullResponse = false, jwt, scopes, 52 | } = {}) { 53 | return this.request({ 54 | url: `${this.navigator.configThing()}/ThingConfiguration/v1/PropertySetTypes('${propertySetTypeName}')`, 55 | qs: queryParameters, 56 | headers, 57 | resolveWithFullResponse, 58 | jwt, 59 | scopes, 60 | }); 61 | } 62 | 63 | /** 64 | * Read all property set types filtered by query parameters 65 | * 66 | * @param {object} [queryParameters] - Map of query parameters 67 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 68 | * @returns {Promise.<*>} API response 69 | * @see SAP Help API documentation 70 | */ 71 | static getPropertySetTypes(queryParameters, { 72 | headers = {}, resolveWithFullResponse = false, jwt, scopes, 73 | } = {}) { 74 | return this.request({ 75 | url: `${this.navigator.configThing()}/ThingConfiguration/v1/PropertySetTypes`, 76 | qs: queryParameters, 77 | headers, 78 | resolveWithFullResponse, 79 | jwt, 80 | scopes, 81 | }); 82 | } 83 | 84 | /** 85 | * Read all property set types for a package 86 | * 87 | * @param {string} packageName - Package identifier 88 | * @param {object} [queryParameters] - Map of query parameters 89 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 90 | * @returns {Promise.<*>} Resulting Propertysettypes 91 | * @see SAP Help API documentation 92 | */ 93 | static getPropertySetTypesByPackage(packageName, queryParameters, { 94 | headers = {}, resolveWithFullResponse = false, jwt, scopes, 95 | } = {}) { 96 | return this.request({ 97 | url: `${this.navigator.configThing()}/ThingConfiguration/v1/Packages('${packageName}')/PropertySetTypes`, 98 | qs: queryParameters, 99 | headers, 100 | resolveWithFullResponse, 101 | jwt, 102 | scopes, 103 | }); 104 | } 105 | 106 | /** 107 | * 108 | * @param {string} propertySetTypeName - Property set type identifier 109 | * @param {string} etag - Latest entity tag 110 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 111 | * @returns {Promise.<*>} API response 112 | * @see SAP Help API documentation 113 | */ 114 | static deletePropertySetType(propertySetTypeName, etag, { 115 | headers = {}, resolveWithFullResponse, jwt, scopes, 116 | } = {}) { 117 | const enhancedHeaders = headers; 118 | enhancedHeaders['If-Match'] = etag; 119 | return this.request({ 120 | url: `${this.navigator.configThing()}/ThingConfiguration/v1/PropertySetTypes('${propertySetTypeName}')`, 121 | method: 'DELETE', 122 | headers: enhancedHeaders, 123 | resolveWithFullResponse, 124 | jwt, 125 | scopes, 126 | }); 127 | } 128 | } 129 | 130 | module.exports = PropertySetTypeService; 131 | -------------------------------------------------------------------------------- /lib/auth/Authenticator.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('SAPIoT:Authenticator'); 2 | const rp = require('request-promise-native'); 3 | const xssec = require('@sap/xssec'); 4 | const Token = require('./Token'); 5 | 6 | /** 7 | * Authenticating the client at the API of SAP IoT 8 | * 9 | * @class Authenticator 10 | * @author Lukas Brinkmann, Jan Reichert, Soeren Buehler 11 | */ 12 | class Authenticator { 13 | /** 14 | * Create a new Authenticator. 15 | * 16 | * @param {object} credentials - SAP IoT service UAA information 17 | * @param {object} xsuaaService - XSUAA service object 18 | */ 19 | constructor(credentials, xsuaaService) { 20 | debug('Creating a new authenticator'); 21 | if (credentials && credentials.url && credentials.clientid && credentials.clientsecret) { 22 | this.clientId = credentials.clientid; 23 | this.clientSecret = credentials.clientsecret; 24 | this.authUrl = `${credentials.url}/oauth/token`; 25 | } else { 26 | // eslint-disable-next-line max-len 27 | throw Error('Incomplete authentication configuration. Ensure a SAP IoT service instance binding or configure authentication options via default-env.json file as described in the readme section of used SAP IoT SDK'); 28 | } 29 | 30 | this._credentials = credentials; 31 | this._xsuaaService = xsuaaService; 32 | } 33 | 34 | /** 35 | * Retrieves a JWT Token to authenticate at the API of SAP IoT. 36 | * If the client has authenticated before and the JWT token is not expired, no 37 | * new JWT Token is requested. 38 | * 39 | * @param {string[]} scopes - Scopes to request for the token 40 | * @returns {string} The JWT Token. 41 | */ 42 | async getToken(scopes = []) { 43 | debug('Authenticating'); 44 | if (this._checkNewTokenRequired(scopes)) { 45 | this.token = await this.getNewToken(scopes); 46 | } 47 | 48 | return this.token; 49 | } 50 | 51 | _checkNewTokenRequired(scopes = []) { 52 | // no token given 53 | if (!this.token) { 54 | return true; 55 | } 56 | 57 | // token expired 58 | if (this.token.isExpired()) { 59 | return true; 60 | } 61 | 62 | if (scopes.length > 0) { 63 | const tokenScopes = this.token.getScopes(); 64 | 65 | // token scopes not matching 66 | if (tokenScopes.length !== scopes.length) { 67 | return true; 68 | } 69 | 70 | const requiredScopeMissing = scopes.some((scope) => !tokenScopes.includes(scope)); 71 | if (requiredScopeMissing) return true; 72 | } 73 | return false; 74 | } 75 | 76 | /** 77 | * Retrieves a new JWT token to authenticate at the API of SAP IoT 78 | * 79 | * @param {string[]} [scopes] - List of scopes which are mandatory for the token 80 | * @throws {Error} Will throw if request fails 81 | * @returns {Token} The JWT Token 82 | */ 83 | async getNewToken(scopes) { 84 | debug('Getting a new token.'); 85 | const credentialsBase64 = Buffer 86 | .from(`${this.clientId}:${this.clientSecret}`) 87 | .toString('base64'); 88 | 89 | const form = { 90 | grant_type: 'client_credentials', 91 | response_type: 'token', 92 | }; 93 | 94 | if (scopes && scopes.length > 0) { 95 | form.scope = scopes.join(' '); 96 | } 97 | 98 | let responseBody; 99 | try { 100 | responseBody = await rp({ 101 | url: this.authUrl, 102 | method: 'POST', 103 | form, 104 | headers: { 105 | 'Content-Type': 'application/x-www-form-urlencoded', 106 | Authorization: `Basic ${credentialsBase64}`, 107 | }, 108 | json: true, 109 | }); 110 | } catch (error) { 111 | debug(error.message); 112 | throw error; 113 | } 114 | 115 | debug('Authentification was successful'); 116 | return new Token(responseBody.access_token, responseBody.expires_in); 117 | } 118 | 119 | /** 120 | * Exchange token exposed by own XSUAA instance with service token for SAP IoT. 121 | * 122 | * @param {string} accessToken - JWT token exposed by UAA instance of SDK user 123 | * @returns {Promise.<*>} Result of the token exchange operation 124 | */ 125 | exchangeToken(accessToken) { 126 | const that = this; 127 | // eslint-disable-next-line consistent-return 128 | return new Promise((resolve, reject) => { 129 | if (!that._xsuaaService || !that._xsuaaService.credentials) { 130 | reject(new Error('XSUAA (Source of token) service binding missing')); 131 | return; 132 | } 133 | if (!that._credentials) { 134 | reject(new Error('SAP IoT service binding missing')); 135 | return; 136 | } 137 | 138 | // eslint-disable-next-line consistent-return 139 | xssec.createSecurityContext(accessToken, that._xsuaaService.credentials, (error, securityContext) => { 140 | if (error) { 141 | debug(`Token exchange error: ${error}`); 142 | return reject(error); 143 | } 144 | 145 | debug('Security context created successfully'); 146 | let grantType = xssec.constants.TYPE_USER_TOKEN; 147 | if (securityContext.getGrantType() === 'client_credentials') { 148 | grantType = xssec.constants.TYPE_CLIENT_CREDENTIALS_TOKEN; 149 | } 150 | 151 | securityContext.requestToken(that._credentials, grantType, {}, (err, newToken) => { 152 | if (err) { 153 | debug(`Token exchange error: ${err}`); 154 | return reject(err); 155 | } 156 | 157 | debug('Token successfully exchanged'); 158 | return resolve(newToken); 159 | }); 160 | }); 161 | }); 162 | } 163 | } 164 | 165 | module.exports = Authenticator; 166 | -------------------------------------------------------------------------------- /lib/services/ThingService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration parameters for the request that will be sent 3 | * 4 | * @typedef {object} RequestConfig 5 | * @property {object} [headers] - Custom header fields which enrich the request 6 | * @property {boolean} [resolveWithFullResponse=false] - Return full or only body part of the response 7 | * @property {string} [jwt] - Jwt token used in authorization header field 8 | * @property {string[]} [scopes] - List of scopes requested within token for this request 9 | */ 10 | 11 | /** 12 | * Expose functions for most used thing APIs 13 | * 14 | * @class ThingService 15 | * @author Soeren Buehler 16 | */ 17 | class ThingService { 18 | /** 19 | * Create an thing instance based on a thing type 20 | * 21 | * @param {object} payload - Request body payload 22 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 23 | * @returns {Promise.<*>} API response 24 | * @see SAP Help API documentation 25 | */ 26 | static createThing(payload, { 27 | headers = {}, resolveWithFullResponse, jwt, scopes, 28 | } = {}) { 29 | return this.request({ 30 | url: `${this.navigator.appiotMds()}/Things`, 31 | method: 'POST', 32 | headers, 33 | body: payload, 34 | resolveWithFullResponse, 35 | jwt, 36 | scopes, 37 | }); 38 | } 39 | 40 | /** 41 | * Read a thing 42 | * 43 | * @param {string} thingId - Thing identifier 44 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 45 | * @returns {Promise.<*>} API response 46 | * @see SAP Help API documentation 47 | */ 48 | static getThing(thingId, { 49 | headers = {}, resolveWithFullResponse, jwt, scopes, 50 | } = {}) { 51 | return this.request({ 52 | url: `${this.navigator.appiotMds()}/Things('${thingId}')`, 53 | headers, 54 | resolveWithFullResponse, 55 | jwt, 56 | scopes, 57 | }); 58 | } 59 | 60 | /** 61 | * Read a thing by alternate identifier 62 | * 63 | * @param {string} alternateId - Unique thing identifier assigned by the creator of a thing 64 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 65 | * @returns {Promise.<*>} API response 66 | * @see SAP Help API documentation 67 | */ 68 | static getThingByAlternateId(alternateId, { 69 | headers = {}, resolveWithFullResponse, jwt, scopes, 70 | } = {}) { 71 | return this.request({ 72 | url: `${this.navigator.appiotMds()}/ThingsByAlternateId('${alternateId}')`, 73 | headers, 74 | resolveWithFullResponse, 75 | jwt, 76 | scopes, 77 | }); 78 | } 79 | 80 | /** 81 | * Read all things filtered by query parameters 82 | * 83 | * @param {object} [queryParameters] - Map of query parameters 84 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 85 | * @returns {Promise.<*>} API response 86 | * @see SAP Help API documentation 87 | */ 88 | static getThings(queryParameters, { 89 | headers = {}, resolveWithFullResponse, jwt, scopes, 90 | } = {}) { 91 | return this.request({ 92 | url: `${this.navigator.appiotMds()}/Things`, 93 | qs: queryParameters, 94 | headers, 95 | resolveWithFullResponse, 96 | jwt, 97 | scopes, 98 | }); 99 | } 100 | 101 | /** 102 | * Read all things of thing type filtered by query parameters 103 | * 104 | * @param {string} thingTypeName - Thing type identifier / name 105 | * @param {object} [queryParameters] - Map of query parameters 106 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 107 | * @returns {Promise.<*>} API response 108 | * @see SAP Help API documentation 109 | */ 110 | static getThingsByThingType(thingTypeName, queryParameters, { 111 | headers = {}, resolveWithFullResponse, jwt, scopes, 112 | } = {}) { 113 | const enhancedQeryParameters = queryParameters || {}; 114 | if (enhancedQeryParameters.$filter) { 115 | enhancedQeryParameters.$filter += ` and _thingType eq '${thingTypeName}'`; 116 | } else { 117 | enhancedQeryParameters.$filter = `_thingType eq '${thingTypeName}'`; 118 | } 119 | 120 | return this.getThings(enhancedQeryParameters, { 121 | headers, resolveWithFullResponse, jwt, scopes, 122 | }); 123 | } 124 | 125 | /** 126 | * Delete a thing 127 | * 128 | * @param {string} thingId - Thing identifier 129 | * @param {RequestConfig} [requestConfig] - Configuration of request metadata parameters 130 | * @returns {Promise.<*>} API response 131 | * @see SAP Help API documentation 132 | */ 133 | static deleteThing(thingId, { 134 | headers = {}, resolveWithFullResponse, jwt, scopes, 135 | } = {}) { 136 | return this.request({ 137 | url: `${this.navigator.appiotMds()}/Things('${thingId}')`, 138 | method: 'DELETE', 139 | headers, 140 | resolveWithFullResponse, 141 | jwt, 142 | scopes, 143 | }); 144 | } 145 | } 146 | 147 | module.exports = ThingService; 148 | -------------------------------------------------------------------------------- /samples/sample_7_multi_tenant_model_migration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This sample shows how multiple Leonardo IoT clients for different 3 | * tenants / subaccounts can be created and used for data migration. 4 | * In this scenario we have a source (quality assurance) 5 | * and a target (development) tenant for model migration. 6 | * All packages and property set types are replicated 7 | * from the source (quality assurance) tenant to the target (development) tenant. 8 | * 9 | * TASKS BEFORE START: 10 | * - Provide user provided services in 'default-env.json' file containing service keys for two different tenants. 11 | * In this sample they are called 'sap-iot-account-dev' 12 | * and 'sap-iot-account-qa', but feel free to adapt these names to your scenario. 13 | */ 14 | 15 | const LeonardoIoT = require('sap-iot-sdk'); 16 | // Client for DEV tenant fetching configuration from user provided service with name 'sap-iot-account-dev' 17 | const clientDev = new LeonardoIoT('sap-iot-account-dev'); 18 | // Client for QA tenant fetching configuration from user provided service with name 'sap-iot-account-qa' 19 | const clientQa = new LeonardoIoT('sap-iot-account-qa'); 20 | 21 | let sourceTenantPrefix; 22 | let targetTenantPrefix; 23 | 24 | async function determineTenantPrefixes(sourceClient, targetClient) { 25 | sourceTenantPrefix = (await sourceClient.request({ 26 | url: `${clientQa.navigator.businessPartner()}/Tenants`, 27 | })).value[0].package; 28 | 29 | targetTenantPrefix = (await targetClient.request({ 30 | url: `${clientDev.navigator.businessPartner()}/Tenants`, 31 | })).value[0].package; 32 | } 33 | 34 | async function copyPropertySetTypes(sourceClient, targetClient, sourcePackageName, targetPackageName) { 35 | const sourcePropertySetTypesResponse = await sourceClient.getPropertySetTypesByPackage(sourcePackageName); 36 | const sourcePropertySetTypes = sourcePropertySetTypesResponse.d.results; 37 | 38 | // Order property set types that reference property set types get created last 39 | sourcePropertySetTypes.sort((a, b) => { 40 | if (a.DataCategory === 'ReferencePropertyData') { 41 | return 1; 42 | } 43 | return b.DataCategory === 'ReferencePropertyData' ? -1 : 0; 44 | }); 45 | sourcePropertySetTypes.forEach(async (sourcePropertySetType) => { 46 | const targetPropertySetTypeName = sourcePropertySetType.Name.replace(sourceTenantPrefix, targetTenantPrefix); 47 | let existingPropertySetType; 48 | try { 49 | existingPropertySetType = await targetClient.getPropertySetType(targetPropertySetTypeName); 50 | } catch (err) { 51 | console.error(err); 52 | } 53 | 54 | if (existingPropertySetType) return; 55 | 56 | // Create payload for new property set type 57 | const payload = { 58 | Name: targetPropertySetTypeName, 59 | DataCategory: sourcePropertySetType.DataCategory, 60 | Properties: [], 61 | Annotations: [], 62 | }; 63 | 64 | // Set reference property set type 65 | if (sourcePropertySetType.ReferredPropertySetType) { 66 | payload.ReferredPropertySetType = sourcePropertySetType.ReferredPropertySetType.replace( 67 | sourceTenantPrefix, 68 | targetTenantPrefix, 69 | ); 70 | } 71 | 72 | // Add properties 73 | const expandedProperties = await sourceClient.getPropertySetType( 74 | sourcePropertySetType.Name, 75 | { $expand: 'Properties' }, 76 | ); 77 | 78 | // eslint-disable-next-line no-restricted-syntax, no-loops/no-loops 79 | for (const sourceProperty of expandedProperties.d.Properties.results) { 80 | sourceProperty.PropertySetType = sourceProperty.PropertySetType.replace(sourceTenantPrefix, targetTenantPrefix); 81 | 82 | if (sourceProperty.ReferenceProperty) { 83 | payload.Properties.push({ 84 | Name: sourceProperty.Name, 85 | AttributeType: sourceProperty.AttributeType, 86 | ReferenceProperty: sourceProperty.ReferenceProperty, 87 | }); 88 | } else { 89 | payload.Properties.push({ 90 | Name: sourceProperty.Name, 91 | Type: sourceProperty.Type, 92 | Description: sourceProperty.Description, 93 | PropertyLength: sourceProperty.PropertyLength, 94 | UnitOfMeasure: sourceProperty.UnitOfMeasure, 95 | QualityCode: sourceProperty.QualityCode, 96 | }); 97 | } 98 | } 99 | 100 | // Add descriptions 101 | const expandedDescriptions = await sourceClient.getPropertySetType( 102 | sourcePropertySetType.Name, 103 | { $expand: 'Descriptions' }, 104 | ); 105 | payload.Descriptions = expandedDescriptions.d.Descriptions.results; 106 | 107 | // Add annotations 108 | const expandedAnnotations = await sourceClient.getPropertySetType( 109 | sourcePropertySetType.Name, 110 | { $expand: 'Annotations' }, 111 | ); 112 | expandedAnnotations.d.Annotations.results.forEach((sourceAnnotation) => { 113 | payload.Annotations.push({ 114 | Name: sourceAnnotation.Name, 115 | PackageName: sourceAnnotation.PackageName, 116 | }); 117 | }); 118 | 119 | await targetClient.createPropertySetType(targetPackageName, payload); 120 | }); 121 | } 122 | 123 | async function copyPackages(sourceClient, targetClient) { 124 | const sourcePackages = await sourceClient.getPackages(); 125 | sourcePackages.d.results.forEach(async (sourcePackage) => { 126 | const targetPackageName = sourcePackage.Name.replace(sourceTenantPrefix, targetTenantPrefix); 127 | 128 | let existingPackage; 129 | try { 130 | existingPackage = await targetClient.getPackage(targetPackageName); 131 | } catch (err) { 132 | console.error(err); 133 | } 134 | 135 | if (!existingPackage) { 136 | await targetClient.createPackage({ 137 | Name: targetPackageName, 138 | Description: sourcePackage.Description, 139 | Scope: sourcePackage.Scope, 140 | Status: sourcePackage.Status, 141 | }); 142 | } 143 | 144 | await copyPropertySetTypes(sourceClient, targetClient, sourcePackage.Name, targetPackageName); 145 | }); 146 | } 147 | 148 | // Entry point of script 149 | (async () => { 150 | try { 151 | await determineTenantPrefixes(clientQa, clientDev); 152 | await copyPackages(clientQa, clientDev); 153 | } catch (err) { 154 | console.log(err); 155 | } 156 | })(); 157 | -------------------------------------------------------------------------------- /test/unit/LeonardoIoT.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const jwt = require('jwt-simple'); 3 | const proxyquire = require('proxyquire'); 4 | const Token = require('../../lib/auth/Token'); 5 | 6 | let rpStub = () => Promise.reject(); 7 | const LeonardoIoT = proxyquire('../../lib/LeonardoIoT', { 'request-promise-native': (requestConfig) => rpStub(requestConfig) }); 8 | const AssertionUtil = require('./AssertionUtil'); 9 | const packageJson = require('../../package.json'); 10 | 11 | const tokenSecret = 'test'; 12 | const generatedAccessToken = { 13 | zid: 'ade586c6-f5b1-4ddc-aecb-ead3c2e6e725', scope: ['uaa.user'], name: 'SAP IoT Test 1', iat: 1516239022, 14 | }; 15 | const forwardedAccessToken = { 16 | zid: 'ade586c6-f5b1-4ddc-aecb-ead3c2e6e725', scope: ['uaa.user'], name: 'SAP IoT Test 2', iat: 1516239022, 17 | }; 18 | 19 | describe('LeonardoIoT', function () { 20 | let client; 21 | let tmpVcapServices; 22 | 23 | beforeEach(function () { 24 | client = new LeonardoIoT(); 25 | tmpVcapServices = JSON.parse(JSON.stringify(process.env.VCAP_SERVICES)); 26 | }); 27 | 28 | afterEach(function () { 29 | process.env.VCAP_SERVICES = JSON.parse(JSON.stringify(tmpVcapServices)); 30 | }); 31 | 32 | describe('constructor', function () { 33 | it('should create a new sdk instance without arguments', function () { 34 | const clientDefault = new LeonardoIoT(); 35 | assert.notStrictEqual(clientDefault, undefined, 'Invalid constructor for LeonardoIoT client'); 36 | }); 37 | 38 | it('should create a new sdk instance with configuration arguments', function () { 39 | const testClient = new LeonardoIoT({ 40 | uaa: { 41 | clientid: 'testId', 42 | clientsecret: 'testSecret', 43 | url: 'https://test.authentication.eu10.hana.ondemand.com', 44 | }, 45 | endpoints: { 46 | 'appiot-mds': 'https://appiot-mds.cfapps.eu10.hana.ondemand.com', 47 | 'config-thing-sap': 'https://config-thing-sap.cfapps.eu10.hana.ondemand.com', 48 | }, 49 | }); 50 | assert.notStrictEqual(testClient, undefined, 'Invalid constructor for LeonardoIoT client'); 51 | }); 52 | 53 | it('should create a new sdk instance with configuration arguments including xsuaa config', function () { 54 | const testClient = new LeonardoIoT({ 55 | uaa: { 56 | clientid: 'testId', 57 | clientsecret: 'testSecret', 58 | url: 'https://test.authentication.eu10.hana.ondemand.com', 59 | }, 60 | endpoints: { 61 | 'appiot-mds': 'https://appiot-mds.cfapps.eu10.hana.ondemand.com', 62 | 'config-thing-sap': 'https://config-thing-sap.cfapps.eu10.hana.ondemand.com', 63 | }, 64 | }); 65 | assert.notStrictEqual(testClient, undefined, 'Invalid constructor for LeonardoIoT client'); 66 | }); 67 | 68 | it('should create multiple sdk instances for multi tenant mode', function () { 69 | const clientTest = new LeonardoIoT('sap-iot-account-test'); 70 | const clientDev = new LeonardoIoT('sap-iot-account-dev'); 71 | 72 | assert.notStrictEqual(clientTest.authenticator.authUrl, clientDev.authenticator.authUrl, 'Mismatching client ID of clients'); 73 | assert.notStrictEqual(clientTest.authenticator.clientId, clientDev.authenticator.clientId, 'Mismatching client ID of clients'); 74 | assert.notStrictEqual(clientTest.authenticator.clientSecret, clientDev.authenticator.clientSecret, 'Mismatching client ID of clients'); 75 | }); 76 | }); 77 | 78 | describe('Request', function () { 79 | it('should have default parameters', function () { 80 | const token = jwt.encode(forwardedAccessToken, tokenSecret); 81 | const testHeaders = LeonardoIoT._addUserAgent({}); 82 | testHeaders.Authorization = `Bearer ${token}`; 83 | rpStub = (requestConfig) => { 84 | AssertionUtil.assertRequestConfig(requestConfig, { 85 | url: 'https://appiot-mds.cfapps.eu10.hana.ondemand.com/Things', 86 | method: 'GET', 87 | headers: testHeaders, 88 | body: {}, 89 | qs: {}, 90 | agentOptions: {}, 91 | resolveWithFullResponse: false, 92 | }); 93 | }; 94 | client.authenticator.exchangeToken = async function () { 95 | return token; 96 | }; 97 | 98 | return client.request({ 99 | url: 'https://appiot-mds.cfapps.eu10.hana.ondemand.com/Things', 100 | jwt: token, 101 | }); 102 | }); 103 | 104 | it('should have correct version in user agent header field', function () { 105 | const headers = LeonardoIoT._addUserAgent({}); 106 | assert.strictEqual(headers['User-Agent'], `${packageJson.name}-nodejs / ${packageJson.version}`, 'Unexpected User-Agent header field value'); 107 | }); 108 | 109 | it('should throw error for missing URL parameter', async function () { 110 | try { 111 | await LeonardoIoT._request({ url: null }); 112 | assert.fail('Expected Error was not thrown'); 113 | } catch (err) { 114 | assert.strictEqual(err.message, 'URL argument is empty for "request" call in SAP IoT', 'Unexpected error message'); 115 | } 116 | }); 117 | }); 118 | 119 | describe('JWT token', function () { 120 | it('should get forwarded correctly', function () { 121 | const token = jwt.encode(forwardedAccessToken, tokenSecret); 122 | rpStub = (requestConfig) => { 123 | const expectedJwt = `Bearer ${token}`; 124 | assert.strictEqual(requestConfig.headers.Authorization, expectedJwt, 'Unexpected JWT token forwarding'); 125 | }; 126 | client.authenticator.exchangeToken = async function () { 127 | return token; 128 | }; 129 | 130 | return client.request({ 131 | url: 'https://appiot-mds.cfapps.eu10.hana.ondemand.com/Things', 132 | jwt: token, 133 | }); 134 | }); 135 | 136 | it('should get sliced and forwarded correctly', function () { 137 | const token = jwt.encode(forwardedAccessToken, tokenSecret); 138 | rpStub = (requestConfig) => { 139 | const expectedJwt = `Bearer ${token}`; 140 | assert.strictEqual(requestConfig.headers.Authorization, expectedJwt, 'Unexpected JWT token forwarding'); 141 | }; 142 | client.authenticator.exchangeToken = async function () { 143 | return token; 144 | }; 145 | 146 | return client.request({ 147 | url: 'https://appiot-mds.cfapps.eu10.hana.ondemand.com/Things', 148 | jwt: `bearer ${token}`, 149 | }); 150 | }); 151 | 152 | it('should get fetched from authentication URL for request', function () { 153 | const token = jwt.encode(generatedAccessToken, tokenSecret); 154 | rpStub = (requestConfig) => { 155 | const expectedJwt = `Bearer ${token}`; 156 | assert.strictEqual(requestConfig.headers.Authorization, expectedJwt, 'Unexpected JWT token forwarding'); 157 | }; 158 | client.authenticator.getToken = async function () { 159 | return new Token(token, 900); 160 | }; 161 | 162 | return client.request({ 163 | url: 'https://appiot-mds.cfapps.eu10.hana.ondemand.com/Things', 164 | }); 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /lib/LeonardoIoT.js: -------------------------------------------------------------------------------- 1 | const rp = require('request-promise-native'); 2 | const debug = require('debug')('LeonardoIoT'); 3 | const packageJson = require('../package.json'); 4 | const Authenticator = require('./auth/Authenticator'); 5 | const PackageService = require('./services/PackageService'); 6 | const PropertySetTypeService = require('./services/PropertySetTypeService'); 7 | const ThingTypeService = require('./services/ThingTypeService'); 8 | const AuthorizationService = require('./services/AuthorizationService'); 9 | const ThingService = require('./services/ThingService'); 10 | const TimeSeriesStoreService = require('./services/TimeSeriesStoreService'); 11 | const TimeSeriesColdStoreService = require('./services/TimeSeriesColdStoreService'); 12 | const TimeSeriesAggregateStoreService = require('./services/TimeSeriesAggregateStoreService'); 13 | const Navigator = require('./utils/Navigator'); 14 | const ConfigurationProvider = require('./utils/ConfigurationProvider'); 15 | 16 | /** 17 | * Class acting as a SAP Leonardo IoT client 18 | * 19 | * @class LeonardoIoT 20 | * @property {ThingService} thing - Service for thing related API calls 21 | * @property {AuthorizationService} authorization - Service for authorization related API calls 22 | * @property {TimeSeriesStoreService} timeSeriesStore - Service for time series store related API calls 23 | */ 24 | class LeonardoIoT { 25 | /** 26 | * Creates a SAP Leonardo IoT client 27 | * 28 | * @param {string|object} [configuration] - Either name of service which contains your SAP Leonardo IoT service key or authentication and endpoint configuration 29 | * @param {object} [configuration.uaa] - Tenant access credentials containing client ID, client secret and authentication URL 30 | * @param {object} [configuration.endpoints] - Service API endpoint map 31 | * @param {object} [configuration.xsuaa] - Configuration of custom XSUAA instance used for token exchange 32 | * @returns {LeonardoIoT} Instance of the SAP IoT SDK 33 | */ 34 | constructor(configuration) { 35 | if (configuration && configuration.uaa && configuration.endpoints) { 36 | this.authenticator = new Authenticator(configuration.uaa, configuration.xsuaa); 37 | this.navigator = new Navigator(configuration.endpoints); 38 | } else { 39 | this.authenticator = new Authenticator( 40 | ConfigurationProvider.getCredentials(configuration), 41 | ConfigurationProvider.getXsuaaService(), 42 | ); 43 | this.navigator = new Navigator(ConfigurationProvider.getDestinations(configuration)); 44 | } 45 | } 46 | 47 | /** 48 | * Sends a http request to SAP Leonardo IoT 49 | * 50 | * @param {object} requestConfig - The request configuration 51 | * @param {string} requestConfig.url - The url / resource path of the request 52 | * @param {string} [requestConfig.method='GET'] - The http method of the request 53 | * @param {object} [requestConfig.headers] - The headers of the request 54 | * @param {object} [requestConfig.body] - The JSON body of the request. 55 | * @param {boolean} [requestConfig.resolveWithFullResponse=false] - If set to `true`, the full response is returned (not just the response body). 56 | * @param {string} [requestConfig.jwt] - Access token used in authorization header field 57 | * @param {string} [requestConfig.scopes] - Scopes requested within token for this request 58 | * @param {object} [requestConfig.agentOptions] - Configuration for the request library. Will be passed directly to the library. 59 | * @param {object } requestConfig.qs - Map of request params 60 | * @returns {object} The response. 61 | */ 62 | async request({ 63 | url, method = 'GET', headers = {}, body = {}, qs = {}, 64 | agentOptions = {}, resolveWithFullResponse = false, jwt, scopes, 65 | } = {}) { 66 | const token = await this._manageJWTAuthorization(jwt, scopes); 67 | const enhancedHeaders = LeonardoIoT._addUserAgent(headers); 68 | enhancedHeaders.Authorization = `Bearer ${token}`; 69 | return LeonardoIoT._request({ 70 | url, 71 | method, 72 | qs, 73 | headers: enhancedHeaders, 74 | body, 75 | agentOptions, 76 | resolveWithFullResponse, 77 | json: true, 78 | }); 79 | } 80 | 81 | /** 82 | * Evaluate access token if existing. Else a new one will be fetched. 83 | * 84 | * @param {string} accessToken - Forwarded access token 85 | * @param {string[]} scopes - List of scopes that should be fetched for the token 86 | * @returns {Promise.<*>} Accesstoken 87 | * @private 88 | */ 89 | async _manageJWTAuthorization(accessToken = null, scopes = []) { 90 | if (accessToken) { 91 | debug('Using given access token for authorization'); 92 | const token = accessToken.startsWith('Bearer') 93 | || accessToken.startsWith('bearer') ? accessToken.slice(6, accessToken.length).trim() : accessToken; 94 | return this.authenticator.exchangeToken(token); 95 | } 96 | debug('Fetching access token from authenticator'); 97 | const newToken = await this.authenticator.getToken(scopes); 98 | return newToken.getAccessToken(); 99 | } 100 | 101 | static _addUserAgent(headers) { 102 | const enhancedHeaders = headers; 103 | enhancedHeaders['User-Agent'] = `${packageJson.name}-nodejs / ${packageJson.version}`; 104 | return enhancedHeaders; 105 | } 106 | 107 | /** 108 | * Forward request to backend 109 | * 110 | * @param {object} requestConfig - The request configuration 111 | * @throws {Error} Will throw if an error if the url argument is empty 112 | * @returns {Promise.<*>} Request result promise 113 | * @private 114 | */ 115 | static async _request(requestConfig) { 116 | if (!requestConfig.url) { 117 | throw new Error('URL argument is empty for "request" call in SAP IoT'); 118 | } 119 | debug(`Sending a ${requestConfig.method} request to ${requestConfig.url}`); 120 | return rp(requestConfig); 121 | } 122 | } 123 | 124 | // Package 125 | LeonardoIoT.prototype.createPackage = PackageService.createPackage; 126 | LeonardoIoT.prototype.getPackage = PackageService.getPackage; 127 | LeonardoIoT.prototype.getPackages = PackageService.getPackages; 128 | LeonardoIoT.prototype.deletePackage = PackageService.deletePackage; 129 | 130 | // Property Set Type 131 | LeonardoIoT.prototype.createPropertySetType = PropertySetTypeService.createPropertySetType; 132 | LeonardoIoT.prototype.getPropertySetType = PropertySetTypeService.getPropertySetType; 133 | LeonardoIoT.prototype.getPropertySetTypes = PropertySetTypeService.getPropertySetTypes; 134 | LeonardoIoT.prototype.getPropertySetTypesByPackage = PropertySetTypeService.getPropertySetTypesByPackage; 135 | LeonardoIoT.prototype.deletePropertySetType = PropertySetTypeService.deletePropertySetType; 136 | 137 | // Thing Type 138 | LeonardoIoT.prototype.createThingType = ThingTypeService.createThingType; 139 | LeonardoIoT.prototype.getThingType = ThingTypeService.getThingType; 140 | LeonardoIoT.prototype.getThingTypes = ThingTypeService.getThingTypes; 141 | LeonardoIoT.prototype.getThingTypesByPackage = ThingTypeService.getThingTypesByPackage; 142 | LeonardoIoT.prototype.deleteThingType = ThingTypeService.deleteThingType; 143 | 144 | // Object Group 145 | LeonardoIoT.prototype.createObjectGroup = AuthorizationService.createObjectGroup; 146 | LeonardoIoT.prototype.getObjectGroup = AuthorizationService.getObjectGroup; 147 | LeonardoIoT.prototype.getObjectGroups = AuthorizationService.getObjectGroups; 148 | LeonardoIoT.prototype.getRootObjectGroup = AuthorizationService.getRootObjectGroup; 149 | LeonardoIoT.prototype.deleteObjectGroup = AuthorizationService.deleteObjectGroup; 150 | 151 | // Thing 152 | LeonardoIoT.prototype.createThing = ThingService.createThing; 153 | LeonardoIoT.prototype.getThing = ThingService.getThing; 154 | LeonardoIoT.prototype.getThingByAlternateId = ThingService.getThingByAlternateId; 155 | LeonardoIoT.prototype.getThings = ThingService.getThings; 156 | LeonardoIoT.prototype.getThingsByThingType = ThingService.getThingsByThingType; 157 | LeonardoIoT.prototype.deleteThing = ThingService.deleteThing; 158 | 159 | // Time Series Store 160 | LeonardoIoT.prototype.createTimeSeriesData = TimeSeriesStoreService.createTimeSeriesData; 161 | LeonardoIoT.prototype.getTimeSeriesData = TimeSeriesStoreService.getTimeSeriesData; 162 | LeonardoIoT.prototype.deleteTimeSeriesData = TimeSeriesStoreService.deleteTimeSeriesData; 163 | 164 | // Time Series Cold Store 165 | LeonardoIoT.prototype.createColdStoreTimeSeriesData = TimeSeriesColdStoreService.createColdStoreTimeSeriesData; 166 | LeonardoIoT.prototype.getColdStoreTimeSeriesData = TimeSeriesColdStoreService.getColdStoreTimeSeriesData; 167 | LeonardoIoT.prototype.deleteColdStoreTimeSeriesData = TimeSeriesColdStoreService.deleteColdStoreTimeSeriesData; 168 | 169 | // Time Series Aggregate Store 170 | LeonardoIoT.prototype.getThingSnapshot = TimeSeriesAggregateStoreService.getThingSnapshot; 171 | LeonardoIoT.prototype.getThingSnapshotWithinTimeRange = TimeSeriesAggregateStoreService.getThingSnapshotWithinTimeRange; 172 | LeonardoIoT.prototype.recalculateAggregates = TimeSeriesAggregateStoreService.recalculateAggregates; 173 | 174 | module.exports = LeonardoIoT; 175 | -------------------------------------------------------------------------------- /test/unit/auth/Authenticator.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const jwt = require('jwt-simple'); 3 | const nock = require('nock'); 4 | const proxyquire = require('proxyquire'); 5 | const Authenticator = require('../../../lib/auth/Authenticator'); 6 | const Token = require('../../../lib/auth/Token'); 7 | 8 | let authenticator; 9 | let xssecStub; 10 | 11 | const tokenSecret = 'test'; 12 | const sampleToken = { name: 'SAP IoT Token', scope: ['rule.r', 'rule.c', 'rule.d'] }; 13 | const exchangedToken = { name: 'Exchange Token', scope: ['rule.r', 'rule.c', 'rule.d'] }; 14 | 15 | describe('Authenticator', function () { 16 | beforeEach(function () { 17 | xssecStub = {}; 18 | const ProxyquireAuthenticator = proxyquire('../../../lib/auth/Authenticator', { '@sap/xssec': xssecStub }); 19 | authenticator = new ProxyquireAuthenticator({ 20 | url: 'https://test.authentication.eu10.hana.ondemand.com', 21 | clientid: 'clientId', 22 | clientsecret: 'clientSecret', 23 | }, {}); 24 | 25 | authenticator._xsuaaService = { credentials: {} }; 26 | authenticator._credentials = {}; 27 | }); 28 | 29 | describe('constructor', function () { 30 | it('should throw error if no credentials are provided', function () { 31 | assert.throws(() => new Authenticator(), Error, 'Expected Error was not thrown'); 32 | }); 33 | }); 34 | 35 | describe('getToken', function () { 36 | it('should return a token', async function () { 37 | nock('https://test.authentication.eu10.hana.ondemand.com') 38 | .post('/oauth/token') 39 | .reply(200, { 40 | access_token: jwt.encode(sampleToken, tokenSecret), 41 | expires_in: 1000, 42 | }); 43 | const token = await authenticator.getToken(); 44 | const expectedToken = jwt.encode(sampleToken, tokenSecret); 45 | assert.strictEqual(token.getAccessToken(), expectedToken); 46 | }); 47 | 48 | it('should return a token with specific scopes', async function () { 49 | const scopes = ['thing.r', 'thing.c']; 50 | const scopeToken = JSON.parse(JSON.stringify(sampleToken)); 51 | scopeToken.scope = scopes; 52 | 53 | nock('https://test.authentication.eu10.hana.ondemand.com') 54 | .post('/oauth/token') 55 | .reply(200, { 56 | access_token: jwt.encode(scopeToken, tokenSecret), 57 | expires_in: 1000, 58 | }); 59 | const token = await authenticator.getToken(scopes); 60 | const expectedToken = jwt.encode(scopeToken, tokenSecret); 61 | assert.strictEqual(token.getAccessToken(), expectedToken); 62 | }); 63 | 64 | it('should only return a new token if the stored token is expired', async function () { 65 | nock('https://test.authentication.eu10.hana.ondemand.com') 66 | .post('/oauth/token') 67 | .reply(200, { 68 | access_token: jwt.encode(sampleToken, tokenSecret), 69 | expires_in: -1000, 70 | }); 71 | const token = await authenticator.getToken(); 72 | const expectedToken = jwt.encode(sampleToken, tokenSecret); 73 | assert.strictEqual(token.getAccessToken(), expectedToken); 74 | }); 75 | }); 76 | 77 | describe('checkNewTokenRequired', function () { 78 | beforeEach(function () { 79 | authenticator = new Authenticator({ 80 | url: 'https://test.authentication.eu10.hana.ondemand.com', 81 | clientid: 'clientId', 82 | clientsecret: 'clientSecret', 83 | }, {}); 84 | }); 85 | 86 | it('should return true if no existing token can be found', async function () { 87 | assert(authenticator._checkNewTokenRequired()); 88 | }); 89 | 90 | it('should return true if existing token has expired', async function () { 91 | authenticator.token = new Token(jwt.encode(sampleToken, tokenSecret), -1); 92 | assert(authenticator._checkNewTokenRequired()); 93 | }); 94 | 95 | it('should return true if number of required scopes does not match', async function () { 96 | authenticator.token = new Token(jwt.encode(sampleToken, tokenSecret), 900); 97 | const requiredScopes = sampleToken.scope; 98 | requiredScopes.push('thing.r'); 99 | assert(authenticator._checkNewTokenRequired(requiredScopes)); 100 | }); 101 | 102 | it('should return true if required scopes are not matching', async function () { 103 | authenticator.token = new Token(jwt.encode(sampleToken, tokenSecret), 900); 104 | const requiredScopes = ['thing.r', 'thing.c', 'thing.d']; 105 | assert(authenticator._checkNewTokenRequired(requiredScopes)); 106 | }); 107 | 108 | it('should return true if there is an existing valid token without scope definition', async function () { 109 | authenticator.token = new Token(jwt.encode(sampleToken, tokenSecret), 900); 110 | assert(!authenticator._checkNewTokenRequired()); 111 | }); 112 | 113 | it('should return false if valid token with matching scopes exists', async function () { 114 | authenticator.token = new Token(jwt.encode(sampleToken, tokenSecret), 900); 115 | assert(!authenticator._checkNewTokenRequired(sampleToken.scope)); 116 | }); 117 | }); 118 | 119 | describe('getNewToken', function () { 120 | it('should return a new token', async function () { 121 | nock('https://test.authentication.eu10.hana.ondemand.com') 122 | .post('/oauth/token') 123 | // eslint-disable-next-line func-names 124 | .reply(function (uri, requestBody) { 125 | assert.strictEqual(this.req.headers['content-type'], 'application/x-www-form-urlencoded'); 126 | assert.strictEqual(this.req.headers.authorization, 'Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0'); 127 | assert.strictEqual(requestBody, 'grant_type=client_credentials&response_type=token'); 128 | return [200, { 129 | access_token: jwt.encode(sampleToken, tokenSecret), 130 | expires_in: -1000, 131 | }]; 132 | }); 133 | const token = await authenticator.getNewToken(); 134 | const expectedToken = jwt.encode(sampleToken, tokenSecret); 135 | assert.strictEqual(token.getAccessToken(), expectedToken); 136 | }); 137 | 138 | it('should return a new token with specific scopes', async function () { 139 | const scopes = ['thing.r', 'thing.c']; 140 | const scopeToken = JSON.parse(JSON.stringify(sampleToken)); 141 | scopeToken.scope = scopes; 142 | 143 | nock('https://test.authentication.eu10.hana.ondemand.com') 144 | .post('/oauth/token') 145 | // eslint-disable-next-line func-names 146 | .reply(function (uri, requestBody) { 147 | assert.strictEqual(this.req.headers['content-type'], 'application/x-www-form-urlencoded'); 148 | assert.strictEqual(this.req.headers.authorization, 'Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0'); 149 | assert.strictEqual(requestBody, 'grant_type=client_credentials&response_type=token&scope=thing.r%20thing.c'); 150 | return [200, { 151 | access_token: jwt.encode(scopeToken, tokenSecret), 152 | expires_in: -1000, 153 | }]; 154 | }); 155 | const token = await authenticator.getNewToken(scopes); 156 | const expectedToken = jwt.encode(scopeToken, tokenSecret); 157 | assert.strictEqual(token.getAccessToken(), expectedToken); 158 | }); 159 | 160 | it('should return an error', async function () { 161 | nock('https://test.authentication.eu10.hana.ondemand.com') 162 | .post('/oauth/token') 163 | .replyWithError('UAA Error'); 164 | 165 | try { 166 | await authenticator.getNewToken(); 167 | assert.fail('Should not have been resolved'); 168 | } catch (error) { 169 | assert.strictEqual(error.message, 'Error: UAA Error'); 170 | } 171 | }); 172 | }); 173 | 174 | describe('exchangeToken', function () { 175 | it('should throw error for missing xsuaa configuration', async function () { 176 | authenticator._credentials = {}; 177 | delete authenticator._xsuaaService; 178 | 179 | try { 180 | await authenticator.exchangeToken(); 181 | assert.fail('Should not have been resolved'); 182 | } catch (err) { 183 | assert.strictEqual(err.message, 'XSUAA (Source of token) service binding missing', 'Should have rejected with error'); 184 | } 185 | }); 186 | 187 | it('should throw error for missing SAP IoT credentials configuration', async function () { 188 | delete authenticator._credentials; 189 | authenticator._xsuaaService = { credentials: {} }; 190 | 191 | try { 192 | await authenticator.exchangeToken(); 193 | assert.fail('Should not have been resolved'); 194 | } catch (err) { 195 | assert.strictEqual(err.message, 'SAP IoT service binding missing', 'Should have rejected with error'); 196 | } 197 | }); 198 | 199 | it('should throw error if security context creation fails', async function () { 200 | xssecStub.createSecurityContext = (accessToken, credentials, callback) => { 201 | callback(new Error('SecurityContext error'), null); 202 | }; 203 | 204 | try { 205 | await authenticator.exchangeToken(); 206 | assert.fail('Should not have been resolved'); 207 | } catch (err) { 208 | assert.strictEqual(err.message, 'SecurityContext error', 'Should have rejected with error'); 209 | } 210 | }); 211 | 212 | it('should throw error when exchange token request fails', async function () { 213 | xssecStub.createSecurityContext = (accessToken, credentials, callback) => { 214 | callback(null, { 215 | getGrantType: () => 'client_credentials', 216 | requestToken: (serviceCredentials, type, additionalAttributes, cb) => { 217 | cb(new Error('RequestToken error'), null); 218 | }, 219 | }); 220 | }; 221 | 222 | try { 223 | await authenticator.exchangeToken(); 224 | assert.fail('Should not have been resolved'); 225 | } catch (err) { 226 | assert.strictEqual(err.message, 'RequestToken error', 'Should have rejected with error'); 227 | } 228 | }); 229 | 230 | it('should exchange token successfully', async function () { 231 | xssecStub.createSecurityContext = (accessToken, credentials, callback) => { 232 | callback(null, { 233 | getGrantType: () => 'client_credentials', 234 | requestToken: (serviceCredentials, type, additionalAttributes, cb) => { 235 | cb(null, exchangedToken); 236 | }, 237 | }); 238 | }; 239 | const token = await authenticator.exchangeToken(); 240 | assert.strictEqual(token, exchangedToken); 241 | }); 242 | }); 243 | }); 244 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2019-2021 SAP SE or an SAP affiliate company and Project "SAP IoT SDK" contributors 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | --------------------------------------------------------------------------------