├── .dockerignore ├── src ├── evrythng.polyfill.js ├── symbols.js ├── util │ ├── buildUrl.js │ ├── parseLinkHeader.js │ ├── getCurrentPosition.js │ ├── mixin.js │ ├── buildParams.js │ └── callback.js ├── entity │ ├── User.js │ ├── Place.js │ ├── Status.js │ ├── Me.js │ ├── Redirector.js │ ├── ADIOrderEvent.js │ ├── Container.js │ ├── PurchaseOrder.js │ ├── AccessToken.js │ ├── AccessPolicy.js │ ├── OperatorAccess.js │ ├── ReactorSchedule.js │ ├── Batch.js │ ├── Role.js │ ├── Project.js │ ├── ADIOrder.js │ ├── Task.js │ ├── Access.js │ ├── SecretKey.js │ ├── ShipmentNotice.js │ ├── Product.js │ ├── Permission.js │ ├── Rule.js │ ├── Thng.js │ ├── Account.js │ ├── ReactorScript.js │ ├── Domain.js │ ├── ShortDomain.js │ ├── File.js │ ├── CommissionState.js │ ├── Application.js │ ├── Collection.js │ ├── ReactorLog.js │ ├── Entity.js │ ├── ActionType.js │ ├── Property.js │ └── Redirection.js ├── evrythng.js ├── setup.js ├── alias.js ├── scope │ ├── Device.js │ ├── AccessToken.js │ ├── Scope.js │ ├── User.js │ ├── Operator.js │ └── Application.js ├── settings.js └── entities.js ├── .prettierignore ├── .gitignore ├── .npmignore ├── examples ├── node │ ├── index.js │ ├── package.json │ └── package-lock.json ├── browser-npm-globals │ ├── package.json │ ├── package-lock.json │ └── index.html └── browser-npm-browserify │ ├── index.js │ ├── index.html │ └── package.json ├── test ├── e2e │ ├── config.js │ └── dataGenerator.js ├── helpers │ ├── apiUrl.js │ ├── paths.js │ ├── responses.js │ ├── dummy.js │ ├── data.js │ └── apiMock.js ├── README.md ├── integration │ ├── entity │ │ ├── rules.spec.js │ │ ├── domains.spec.js │ │ ├── shortDomains.spec.js │ │ ├── commissionState.spec.js │ │ ├── secretKey.spec.js │ │ ├── adiOrderEvents.spec.js │ │ ├── me.spec.js │ │ ├── accountRedirector.spec.js │ │ ├── accounts.spec.js │ │ ├── accesses.spec.js │ │ ├── locations.spec.js │ │ ├── applicationRedirector.spec.js │ │ ├── tasks.spec.js │ │ ├── projects.spec.js │ │ ├── roles.spec.js │ │ ├── thngs.spec.js │ │ ├── places.spec.js │ │ ├── products.spec.js │ │ ├── collections.spec.js │ │ ├── files.spec.js │ │ ├── applications.spec.js │ │ ├── adiOrders.spec.js │ │ ├── actionTypes.spec.js │ │ ├── properties.spec.js │ │ ├── shipmentNotice.spec.js │ │ ├── accessTokens.spec.js │ │ ├── actions.spec.js │ │ ├── accessPolicies.spec.js │ │ ├── redirection.spec.js │ │ ├── purchaseOrders.spec.js │ │ ├── operatorAccesses.spec.js │ │ └── permissions.spec.js │ ├── scope │ │ ├── device.spec.js │ │ └── operator.spec.js │ └── misc │ │ ├── stream.spec.js │ │ ├── streamPages.spec.js │ │ ├── pages.spec.js │ │ ├── use.spec.js │ │ ├── find.spec.js │ │ ├── alias.spec.js │ │ ├── rescope.spec.js │ │ ├── upsert.spec.js │ │ └── paramSetters.spec.js ├── unit │ └── outDated │ │ ├── settings.spec.js │ │ ├── evrythng.spec.js │ │ ├── util │ │ ├── buildUrl.spec.js │ │ ├── mixin.spec.js │ │ ├── buildParams.spec.js │ │ └── getCurrentPosition.spec.js │ │ ├── setup.spec.js │ │ ├── entity │ │ ├── File.spec.js │ │ ├── User.spec.js │ │ ├── Place.spec.js │ │ ├── Status.spec.js │ │ ├── ReactorSchedule.spec.js │ │ ├── Task.spec.js │ │ ├── Batch.spec.js │ │ ├── Role.spec.js │ │ ├── Project.spec.js │ │ ├── Permission.spec.js │ │ ├── Thng.spec.js │ │ ├── ReactorScript.spec.js │ │ ├── Product.spec.js │ │ ├── Collection.spec.js │ │ ├── ReactorLog.spec.js │ │ └── Application.spec.js │ │ └── scope │ │ ├── Scope.spec.js │ │ ├── Application.spec.js │ │ └── Operator.spec.js └── require-main.js ├── Dockerfile ├── playground.html ├── .circleci └── config.yml ├── jenkins └── deploy.sh ├── webpack.config.js └── package.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /src/evrythng.polyfill.js: -------------------------------------------------------------------------------- 1 | import 'cross-fetch/polyfill' 2 | export * from './evrythng' 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | terraform/* 2 | reports/* 3 | test-results/* 4 | mochawesome-report/* 5 | .nyc_output 6 | .vscode 7 | dist -------------------------------------------------------------------------------- /src/symbols.js: -------------------------------------------------------------------------------- 1 | export default { 2 | init: Symbol('init'), 3 | path: Symbol('path'), 4 | resource: Symbol('resource') 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Update .npmignore whenever you update this file! 2 | 3 | npm-debug.log 4 | node_modules 5 | bower_components 6 | dist 7 | report 8 | .idea 9 | *.DS_Store 10 | .vscode 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Update .gitignore whenever you update this file! 2 | 3 | npm-debug.log 4 | node_modules 5 | bower_components 6 | examples 7 | report 8 | .idea 9 | *.html 10 | .vscode 11 | -------------------------------------------------------------------------------- /examples/node/index.js: -------------------------------------------------------------------------------- 1 | const evrythng = require('evrythng') 2 | 3 | evrythng 4 | .api({ 5 | url: '/time' 6 | }) 7 | .then((res) => console.log(`Current time: ${res.timestamp}`)) 8 | .catch(console.log) 9 | -------------------------------------------------------------------------------- /test/e2e/config.js: -------------------------------------------------------------------------------- 1 | const { OPERATOR_API_KEY, OPERATOR_EMAIL, ACCOUNT_ID } = process.env 2 | const config = { 3 | OPERATOR_API_KEY, 4 | OPERATOR_EMAIL, 5 | ACCOUNT_ID 6 | } 7 | 8 | module.exports = config 9 | -------------------------------------------------------------------------------- /examples/browser-npm-globals/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evrythng.js-browser-npm-globals", 3 | "version": "1.0.0", 4 | "license": "Apache-2.0", 5 | "dependencies": { 6 | "evrythng": "file:../.." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evrythng.js-node", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "Apache-2.0", 6 | "dependencies": { 7 | "evrythng": "file:../.." 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/browser-npm-browserify/index.js: -------------------------------------------------------------------------------- 1 | const evrythng = require('evrythng') 2 | 3 | evrythng 4 | .api({ 5 | url: '/time' 6 | }) 7 | .then((res) => { 8 | const output = document.getElementById('output') 9 | output.innerHTML = res.timestamp 10 | }) 11 | .catch(alert) 12 | -------------------------------------------------------------------------------- /examples/browser-npm-browserify/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | evrythng.js NPM Browserify Example 6 | 7 | 8 |

Current time:

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/helpers/apiUrl.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import settings from '../../src/settings' 3 | 4 | /** 5 | * Merge base apiUrl with given endpoint. 6 | * 7 | * @param {string} endpoint - Endpoint path 8 | * @return {string} - Full API url 9 | */ 10 | export default function apiUrl (endpoint = '') { 11 | return `${settings.apiUrl}${endpoint}` 12 | } 13 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Presetup 2 | 3 | 1. `npm i` 4 | 2. Run `npm run build` or `npm run build-dev` in the main `evrythng.js` repository. 5 | 6 | # Unit tests 7 | 8 | Run `npm run test:unit` 9 | 10 | # Integration tests 11 | 12 | Run `npm run test` 13 | 14 | # e2e 15 | 16 | 1. Ensure you export all environment variables from `e2e/config.js` file 17 | 2. `npm run test:e2e` 18 | -------------------------------------------------------------------------------- /examples/node/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evrythng.js-node", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "evrythng": { 8 | "version": "file:../..", 9 | "requires": { 10 | "@babel/runtime": "^7.4.3", 11 | "isomorphic-fetch": "^2.2.1", 12 | "node-fetch": "^2.3.0" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | 3 | WORKDIR /srv 4 | COPY . /srv 5 | 6 | # Install dependencues 7 | RUN apk add --no-cache python3 py3-pip 8 | RUN pip3 install awscli --upgrade --user 9 | 10 | # Build 11 | RUN npm ci 12 | RUN npm run build 13 | 14 | # Deploy 15 | CMD ["sh", "-c", "~/.local/bin/aws s3 cp /srv/dist/evrythng.browser.js s3://$BUCKET/js/evrythng/$VERSION/evrythng-$VERSION.js --acl public-read"] 16 | -------------------------------------------------------------------------------- /examples/browser-npm-globals/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evrythng.js-browser-npm-globals", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "evrythng": { 8 | "version": "file:../..", 9 | "requires": { 10 | "@babel/runtime": "^7.4.3", 11 | "isomorphic-fetch": "^2.2.1", 12 | "node-fetch": "^2.3.0" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/browser-npm-browserify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evrythng.js-browser-npm-browserify", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "license": "Apache-2.0", 6 | "dependencies": { 7 | "browserify": "^16.2.3", 8 | "evrythng": "file:../.." 9 | }, 10 | "scripts": { 11 | "postinstall": "npm run build", 12 | "build": "mkdir -p dist && ./node_modules/.bin/browserify index.js -o dist/index.js" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/integration/entity/rules.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Rules', () => { 6 | let scope 7 | 8 | before(() => { 9 | scope = getScope(scopeType) 10 | }) 11 | 12 | it('should add a rule resource', async () => { 13 | expect(scope.rule).to.be.a('function') 14 | }) 15 | 16 | it('should run a given rule') 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | evrythng.js playground 6 | 7 | 8 | 9 |

evrythng.js Playground

10 | 11 |

Open the console and use the 'evrythng' global variable to get started!

12 | 13 |

Simple example:

14 | evrythng.api({ url: '/time' }).then(console.log) 15 | 16 | -------------------------------------------------------------------------------- /src/util/buildUrl.js: -------------------------------------------------------------------------------- 1 | import buildParams from './buildParams' 2 | 3 | /** 4 | * Concatenate url with parameters from request options. 5 | * 6 | * @export 7 | * @param {Object} options request options including url and params 8 | * @returns {string} 9 | */ 10 | export default function buildUrl (options) { 11 | let url = `${options.apiUrl}${options.url}` 12 | 13 | if (options.params) { 14 | url += `?${buildParams(options.params)}` 15 | } 16 | 17 | return url 18 | } 19 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | build: 4 | docker: 5 | - image: node:10.16-alpine 6 | steps: 7 | - checkout 8 | - run: 9 | name: 'Install dependencies' 10 | command: 'npm ci' 11 | - run: 12 | name: 'Build SDK' 13 | command: 'npm run build' 14 | - run: 15 | name: 'Audit dependencies' 16 | command: 'npm audit' 17 | - run: 18 | name: 'Run tests' 19 | command: 'npm test' 20 | -------------------------------------------------------------------------------- /src/entity/User.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | 4 | const path = '/users' 5 | 6 | /** 7 | * Represents a User entity object. 8 | * 9 | * @extends Entity 10 | */ 11 | export default class User extends Entity { 12 | /** 13 | * Return simple resource factory for AppUsers. 14 | * 15 | * @static 16 | * @return {{user: Function}} 17 | */ 18 | static resourceFactory () { 19 | return { 20 | user: Resource.factoryFor(User, path) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/entity/Place.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | 4 | const path = '/places' 5 | 6 | /** 7 | * Represents a Place entity object. 8 | * 9 | * @extends Entity 10 | */ 11 | export default class Place extends Entity { 12 | /** 13 | * Return simple resource factory for Places. 14 | * 15 | * @static 16 | * @return {{place: Function}} 17 | */ 18 | static resourceFactory () { 19 | return { 20 | place: Resource.factoryFor(Place, path) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/unit/outDated/settings.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import settings from '../../src/settings' 3 | 4 | const defaultSettings = { 5 | apiUrl: 'https://api.evrythng.com', 6 | apiKey: '', 7 | fullResponse: false, 8 | geolocation: true, 9 | timeout: 0, 10 | headers: { 11 | 'content-type': 'application/json' 12 | }, 13 | interceptors: [] 14 | } 15 | 16 | describe('settings', () => { 17 | it('should start with the default settings', () => { 18 | expect(settings).toEqual(defaultSettings) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /src/entity/Status.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | import isString from 'lodash-es/isString' 4 | 5 | const path = '/status' 6 | 7 | export default class Status extends Entity { 8 | static resourceFactory () { 9 | return { 10 | status () { 11 | if (isString(arguments[0])) { 12 | throw new TypeError('There is no single resource for Status') 13 | } 14 | 15 | return Resource.factoryFor(Status, path).call(this) 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/entity/Me.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | 4 | const path = '/me' 5 | 6 | /** 7 | * Represents a Me entity. 8 | * 9 | * @extends Entity 10 | */ 11 | export default class Me extends Entity { 12 | /** 13 | * Return simple resource factory for Me. 14 | * 15 | * @static 16 | * @return {{me: Function}} 17 | */ 18 | static resourceFactory () { 19 | return { 20 | me () { 21 | return Resource.factoryFor(Me, path).call(this) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/entity/Redirector.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | 4 | const path = '/redirector' 5 | 6 | /** 7 | * Represents a Redirector entity object. 8 | * 9 | * @extends Entity 10 | */ 11 | export default class Redirector extends Entity { 12 | /** 13 | * Return simple resource factory for a Redirector. 14 | * 15 | * @static 16 | * @return {{redirector: Function}} 17 | */ 18 | static resourceFactory () { 19 | return { 20 | redirector: Resource.factoryFor(Redirector, path) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/entity/ADIOrderEvent.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | 4 | const path = '/events' 5 | 6 | /** 7 | * Represents a ADIOrderEvent entity object. 8 | * 9 | * @extends Entity 10 | */ 11 | export default class ADIOrderEvent extends Entity { 12 | /** 13 | * Return simple resource factory for ADI Order events. 14 | * 15 | * @static 16 | * @return {{event: Function}} 17 | */ 18 | static resourceFactory () { 19 | return { 20 | event: Resource.factoryFor(ADIOrderEvent, path) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/entity/Container.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | 4 | const path = '/containers' 5 | 6 | /** 7 | * Represents a Container entity object. 8 | * 9 | * @extends Entity 10 | */ 11 | export default class Container extends Entity { 12 | /** 13 | * Return simple resource factory for shipment notice containers. 14 | * 15 | * @static 16 | * @return {{container: Function}} 17 | */ 18 | static resourceFactory () { 19 | return { 20 | container: Resource.factoryFor(Container, path) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/entity/PurchaseOrder.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | 4 | const path = '/purchaseOrders' 5 | 6 | /** 7 | * Represents a PurchaseOrder entity object. 8 | * 9 | * @extends Entity 10 | */ 11 | export default class PurchaseOrder extends Entity { 12 | /** 13 | * Return simple resource factory for ADI Orders. 14 | * 15 | * @static 16 | * @return {{purchaseOrder: Function}} 17 | */ 18 | static resourceFactory () { 19 | return { 20 | purchaseOrder: Resource.factoryFor(PurchaseOrder, path) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/entity/AccessToken.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | 4 | const path = '/accessTokens' 5 | 6 | /** 7 | * Represents an Access Token entity. 8 | * 9 | * @extends Entity 10 | */ 11 | export default class AccessToken extends Entity { 12 | /** 13 | * Return simple resource factory for Access Tokens. 14 | * 15 | * @static 16 | * @return {{accessToken: Function}} 17 | */ 18 | static resourceFactory () { 19 | return { 20 | accessToken (id) { 21 | return Resource.factoryFor(AccessToken, path).call(this, id) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/integration/scope/device.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (url) => { 5 | describe('Device', () => { 6 | let device, api 7 | 8 | before(() => { 9 | device = getScope('device') 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should represent a Thng', async () => { 14 | api.get('/thngs/deviceThngId').reply(200, { id: 'deviceThngId', apiKey: 'apiKey' }) 15 | const res = await device.init() 16 | 17 | expect(res).to.be.an('object') 18 | expect(res.apiKey).to.be.a('string') 19 | }) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/entity/AccessPolicy.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | 4 | const path = '/accessPolicies' 5 | 6 | /** 7 | * Represents an Access Policy entity. 8 | * 9 | * @extends Entity 10 | */ 11 | export default class AccessPolicy extends Entity { 12 | /** 13 | * Return simple resource factory for Access Policies. 14 | * 15 | * @static 16 | * @return {{accessPolicy: Function}} 17 | */ 18 | static resourceFactory () { 19 | return { 20 | accessPolicy (id) { 21 | return Resource.factoryFor(AccessPolicy, path).call(this, id) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/entity/OperatorAccess.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | 4 | const path = '/operatorAccess' 5 | 6 | /** 7 | * Represents an Operator Access entity. 8 | * 9 | * @extends Entity 10 | */ 11 | export default class OperatorAccess extends Entity { 12 | /** 13 | * Return simple resource factory for Operator Accesses. 14 | * 15 | * @static 16 | * @return {{operatorAccess: Function}} 17 | */ 18 | static resourceFactory () { 19 | return { 20 | operatorAccess (id) { 21 | return Resource.factoryFor(OperatorAccess, path).call(this, id) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/util/parseLinkHeader.js: -------------------------------------------------------------------------------- 1 | // Parse Link headers for API pagination. 2 | // https://gist.github.com/niallo/3109252 3 | export default function parseLinkHeader (header) { 4 | const links = {} 5 | 6 | if (header && header.length) { 7 | // Split parts by comma 8 | const parts = header.split(',') 9 | // Parse each part into a named link 10 | for (let i = 0; i < parts.length; i++) { 11 | const section = parts[i].split(';') 12 | const url = section[0].replace(/<(.*)>/, '$1').trim() 13 | const name = section[1].replace(/rel="(.*)"/, '$1').trim() 14 | links[name] = url 15 | } 16 | } 17 | 18 | return links 19 | } 20 | -------------------------------------------------------------------------------- /test/unit/outDated/evrythng.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import * as evrythng from '../../src/evrythng' 3 | 4 | // Unit tested separately. Just to confirm the evrythng module is exposing the 5 | // right stuff. 6 | import settings from '../../src/settings' 7 | import setup from '../../src/setup' 8 | 9 | describe('evrythng', () => { 10 | describe('settings', () => { 11 | it('should expose settings', () => { 12 | expect(evrythng.settings).toBe(settings) 13 | }) 14 | }) 15 | 16 | describe('setup', () => { 17 | it('should expose settings setup', () => { 18 | expect(evrythng.setup).toBe(setup) 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /examples/browser-npm-globals/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | evrythng.js NPM Globals Example 6 | 7 | 8 |

Current time:

9 | 10 | 20 | 21 | -------------------------------------------------------------------------------- /src/entity/ReactorSchedule.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | import Scope from '../scope/Scope' 4 | import symbols from '../symbols' 5 | 6 | const path = '/reactor/schedules' 7 | 8 | /** 9 | * Represents a ReactorSchedule entity object. 10 | * 11 | * @extends Entity 12 | */ 13 | export default class ReactorSchedule extends Entity { 14 | static resourceFactory () { 15 | return { 16 | reactorSchedule (id) { 17 | const appPath = this instanceof Scope ? this[symbols.path] : '' 18 | 19 | return Resource.factoryFor(ReactorSchedule, appPath + path).call(this, id) 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/integration/entity/domains.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Domains', () => { 6 | let scope, api 7 | 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should read all domains', async () => { 14 | api.get('/accounts/accountId/domains').reply(200, [{ domain: 'wrxfq.tn.gg' }]) 15 | const res = await scope.sharedAccount('accountId').domain().read() 16 | 17 | expect(res).to.be.an('array') 18 | expect(res).to.have.length.gte(1) 19 | }) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /test/integration/entity/shortDomains.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Short Domains', () => { 6 | let scope, api 7 | 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should read all short domains', async () => { 14 | api.get('/accounts/accountId/shortDomains').reply(200, ['tn.gg']) 15 | const res = await scope.sharedAccount('accountId').shortDomain().read() 16 | 17 | expect(res).to.be.an('array') 18 | expect(res).to.have.length.gte(1) 19 | }) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/entity/Batch.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Task from './Task' 3 | import Resource from '../resource/Resource' 4 | import { mixinResources } from '../util/mixin' 5 | 6 | const path = '/batches' 7 | const BatchResources = mixinResources([Task]) 8 | 9 | /** 10 | * Represents a Batch entity object. 11 | * 12 | * @extends Entity 13 | */ 14 | export default class Batch extends BatchResources(Entity) { 15 | /** 16 | * Return simple resource factory for Batches. 17 | * 18 | * @static 19 | * @return {{batch: Function}} 20 | */ 21 | static resourceFactory () { 22 | return { 23 | batch: Resource.factoryFor(Batch, path, BatchResources) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/entity/Role.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Permission from './Permission' 3 | import Resource from '../resource/Resource' 4 | import { mixinResources } from '../util/mixin' 5 | 6 | const path = '/roles' 7 | const RoleResources = mixinResources([Permission]) 8 | 9 | /** 10 | * Represents a Role entity object. 11 | * 12 | * @extends Entity 13 | */ 14 | export default class Role extends RoleResources(Entity) { 15 | /** 16 | * Return simple resource factory for Roles. 17 | * 18 | * @static 19 | * @return {{role: Function}} 20 | */ 21 | static resourceFactory () { 22 | return { 23 | role: Resource.factoryFor(Role, path, RoleResources) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/unit/outDated/util/buildUrl.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import buildUrl from '../../../src/util/buildUrl' 3 | import paths from '../../helpers/paths' 4 | 5 | const apiUrl = paths.testBase 6 | const url = paths.dummy 7 | 8 | describe('buildUrl', () => { 9 | it('should join base apiUrl and path given in options', () => { 10 | expect(buildUrl({ apiUrl, url })).toEqual(`${apiUrl}${url}`) 11 | }) 12 | 13 | it('should convert params object to query string', () => { 14 | const params = { 15 | foo: 'bar', 16 | baz: 1 17 | } 18 | const paramsStr = 'foo=bar&baz=1' 19 | expect(buildUrl({ apiUrl, url, params })).toEqual(`${apiUrl}${url}?${paramsStr}`) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/entity/Project.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Application from './Application' 3 | import Resource from '../resource/Resource' 4 | import { mixinResources } from '../util/mixin' 5 | 6 | const path = '/projects' 7 | const ProjectResources = mixinResources([Application]) 8 | 9 | /** 10 | * Represents a Project entity object. 11 | * 12 | * @extends Entity 13 | */ 14 | export default class Project extends ProjectResources(Entity) { 15 | /** 16 | * Return simple resource factory for Projects. 17 | * 18 | * @static 19 | * @return {{project: Function}} 20 | */ 21 | static resourceFactory () { 22 | return { 23 | project: Resource.factoryFor(Project, path, ProjectResources) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/entity/ADIOrder.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import ADIOrderEvent from './ADIOrderEvent' 3 | import Resource from '../resource/Resource' 4 | import { mixinResources } from '../util/mixin' 5 | 6 | const path = '/adis/orders' 7 | const ADIOrderResources = mixinResources([ADIOrderEvent]) 8 | 9 | /** 10 | * Represents a ADIOrder entity object. 11 | * 12 | * @extends Entity 13 | */ 14 | export default class ADIOrder extends ADIOrderResources(Entity) { 15 | /** 16 | * Return simple resource factory for ADI Orders. 17 | * 18 | * @static 19 | * @return {{adiOrder: Function}} 20 | */ 21 | static resourceFactory () { 22 | return { 23 | adiOrder: Resource.factoryFor(ADIOrder, path, ADIOrderResources) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /jenkins/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Use a Docker container to build and deploy a new SDK build to the S3 bucket 4 | # used for distribution (publish to npm happens separately). 5 | # 6 | # Required environment variables: 7 | #  BUCKET - The S3 bucket to push the built SDK file. 8 | #  VERSION - The new SDK version being published. 9 | #  AWS_ACCESS_KEY_ID - AWS Access Key ID with permission to put to the bucket. 10 | #  AWS_SECRET_ACCESS_KEY - AWS Secret Key ID corresponding to the AWS_ACCESS_KEY_ID. 11 | 12 | docker build -t evrythng-js-deploy . 13 | 14 | docker run \ 15 | -e "BUCKET=$BUCKET" \ 16 | -e "VERSION=$VERSION" \ 17 | -e "AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID" \ 18 | -e "AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY" \ 19 | evrythng-js-deploy 20 | -------------------------------------------------------------------------------- /test/unit/outDated/setup.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import settings from '../../src/settings' 3 | import setup from '../../src/setup' 4 | 5 | // Copy initial settings 6 | const initialSettings = Object.assign({}, settings) 7 | 8 | describe('setup', () => { 9 | afterEach(() => setup(initialSettings)) 10 | 11 | it('should merge settings with custom settings', () => { 12 | const customSettings = { 13 | apiUrl: 'https://test-api.evrythng.net', 14 | fullResponse: true 15 | } 16 | setup(customSettings) 17 | 18 | expect(settings.apiUrl).toEqual(customSettings.apiUrl) 19 | expect(settings.fullResponse).toEqual(customSettings.fullResponse) 20 | expect(settings.geolocation).toEqual(initialSettings.geolocation) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/integration/entity/commissionState.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | let scope, api 6 | 7 | describe('Commission State', () => { 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it("should read a Thng's commissioning state", async () => { 14 | api 15 | .get('/thngs/gs1%3A21%3A23984736/commissionState') 16 | .reply(200, { state: 'not_commissioned' }) 17 | const res = await scope.thng('gs1:21:23984736').commissionState().read() 18 | 19 | expect(res).to.be.an('object') 20 | expect(res.state).to.equal('not_commissioned') 21 | }) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /test/require-main.js: -------------------------------------------------------------------------------- 1 | const paths = { 2 | evrythng: 'evrythng.polyfill', 3 | 'isomorphic-fetch': '../node_modules/whatwg-fetch/fetch' 4 | } 5 | 6 | if (typeof module !== 'undefined') { 7 | module.exports = paths 8 | } else { 9 | const tests = [] 10 | for (const file in window.__karma__.files) { 11 | if (window.__karma__.files.hasOwnProperty(file)) { 12 | if (/spec\.js$/.test(file)) { 13 | tests.push(file) 14 | } 15 | } 16 | } 17 | 18 | require.config({ 19 | // Karma serves files under /base, which is the basePath from your config file 20 | baseUrl: '/base/dist', 21 | 22 | deps: tests, 23 | 24 | paths, 25 | 26 | // we have to kickoff jasmine, as it is asynchronous 27 | callback: window.__karma__.start 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/entity/Task.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | import Scope from '../scope/Scope' 4 | 5 | const path = '/tasks' 6 | 7 | /** 8 | * Represents a Task entity. 9 | * 10 | * @extends Entity 11 | */ 12 | export default class Task extends Entity { 13 | /** 14 | * Return simple resource factory for Tasks. 15 | * 16 | * @static 17 | * @return {{task: Function}} 18 | */ 19 | static resourceFactory () { 20 | return { 21 | task (id) { 22 | // Only allowed on Entities and Resources. 23 | if (this instanceof Scope) { 24 | throw new Error('Task is not a top-level resource.') 25 | } 26 | 27 | return Resource.factoryFor(Task, path).call(this, id) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/integration/entity/secretKey.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Secret Key', () => { 6 | let scope, api 7 | 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it("should read an application's secret key", async () => { 14 | api 15 | .get('/projects/projectId/applications/applicationId/secretKey') 16 | .reply(200, { secretApiKey: 'secretApiKey' }) 17 | const res = await scope.project('projectId').application('applicationId').secretKey().read() 18 | 19 | expect(res).to.be.an('object') 20 | expect(res.secretApiKey).to.be.a('string') 21 | }) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/entity/Access.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | import Scope from '../scope/Scope' 4 | 5 | const path = '/accesses' 6 | 7 | /** 8 | * Represents an Access entity. 9 | * 10 | * @extends Entity 11 | */ 12 | export default class Access extends Entity { 13 | /** 14 | * Return simple resource factory for Accesses. 15 | * 16 | * @static 17 | * @return {{access: Function}} 18 | */ 19 | static resourceFactory () { 20 | return { 21 | access (id) { 22 | // Only allowed on Entities and Resources. 23 | if (this instanceof Scope) { 24 | throw new Error('Access is not a top-level resource.') 25 | } 26 | 27 | return Resource.factoryFor(Access, path).call(this, id) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/entity/SecretKey.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | import isString from 'lodash-es/isString' 4 | 5 | const path = '/secretKey' 6 | 7 | /** 8 | * Represents a SecretKey entity on a Trusted Application. 9 | * 10 | * @extends Entity 11 | */ 12 | export default class SecretKey extends Entity { 13 | /** 14 | * Return simple resource factory for SecretKey. 15 | * 16 | * @static 17 | * @return {{secretKey: Function}} 18 | */ 19 | static resourceFactory () { 20 | return { 21 | secretKey () { 22 | if (isString(arguments[0])) { 23 | throw new TypeError('There is no single resource for SecretKey') 24 | } 25 | 26 | return Resource.factoryFor(SecretKey, path).call(this) 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/entity/ShipmentNotice.js: -------------------------------------------------------------------------------- 1 | import { mixinResources } from '../util/mixin' 2 | import Container from './Container' 3 | import Entity from './Entity' 4 | import Resource from '../resource/Resource' 5 | 6 | const path = '/shipmentNotices' 7 | 8 | const ShipmentNoticeResources = mixinResources([Container]) 9 | 10 | /** 11 | * Represents a ShipmentNotice entity object. 12 | * 13 | * @extends Entity 14 | */ 15 | export default class ShipmentNotice extends ShipmentNoticeResources(Entity) { 16 | /** 17 | * Return simple resource factory for shipment notices. 18 | * 19 | * @static 20 | * @return {{shipmentNotice: Function}} 21 | */ 22 | static resourceFactory () { 23 | return { 24 | shipmentNotice: Resource.factoryFor(ShipmentNotice, path, ShipmentNoticeResources) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/entity/Product.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Property from './Property' 3 | import Action from './Action' 4 | import Redirection from './Redirection' 5 | import Resource from '../resource/Resource' 6 | import { mixinResources } from '../util/mixin' 7 | 8 | const path = '/products' 9 | const ProductResources = mixinResources([Property, Action, Redirection]) 10 | 11 | /** 12 | * Represents a Product entity object. 13 | * 14 | * @extends Entity 15 | */ 16 | export default class Product extends ProductResources(Entity) { 17 | /** 18 | * Return simple resource factory for Products. 19 | * 20 | * @static 21 | * @return {{product: Function}} 22 | */ 23 | static resourceFactory () { 24 | return { 25 | product: Resource.factoryFor(Product, path, ProductResources, 'product') 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/unit/outDated/util/mixin.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import mixin from '../../../src/util/mixin' 3 | 4 | const behavior = { 5 | foo () {} 6 | } 7 | class Dummy { 8 | test () {} 9 | } 10 | 11 | let Mixin 12 | 13 | describe('mixin', () => { 14 | beforeEach(() => { 15 | // real use-case scenario 16 | Mixin = mixin(behavior)(class extends Dummy {}) 17 | }) 18 | 19 | it('should add behavior to target', () => { 20 | expect(Mixin.prototype.foo).toBeDefined() 21 | expect(Mixin.prototype.test).toBeDefined() 22 | }) 23 | 24 | it('should not modify base class', () => { 25 | expect(Dummy.prototype.foo).not.toBeDefined() 26 | expect(Dummy.prototype.test).toBeDefined() 27 | }) 28 | 29 | it('should inherit from class', () => { 30 | expect(Mixin.constructor).toBe(Dummy.constructor) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/entity/Permission.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | import Scope from '../scope/Scope' 4 | 5 | const path = '/permissions' 6 | 7 | /** 8 | * Represents a Permission entity. 9 | * 10 | * @extends Entity 11 | */ 12 | export default class Permission extends Entity { 13 | /** 14 | * Return simple resource factory for Permissions. 15 | * 16 | * @static 17 | * @return {{permission: Function}} 18 | */ 19 | static resourceFactory () { 20 | return { 21 | permission (name) { 22 | // Only allowed on Entities and Resources. 23 | if (this instanceof Scope) { 24 | throw new Error('Permission is not a top-level resource.') 25 | } 26 | 27 | return Resource.factoryFor(Permission, path).call(this, name) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/util/getCurrentPosition.js: -------------------------------------------------------------------------------- 1 | // Maximum acceptable age of cached location from the browser 2 | const maximumAge = 5 * 60 * 1000 3 | 4 | /** 5 | * Get browser's current position from Geolocation API. 6 | * 7 | * @return {Promise} - Resolves with current position or rejects with failure 8 | * explanation. 9 | */ 10 | export default function getCurrentPosition () { 11 | return new Promise((resolve, reject) => { 12 | if (typeof window === 'undefined' || !window.navigator.geolocation) { 13 | throw new Error('Geolocation API not available.') 14 | } 15 | 16 | const geolocationOptions = { 17 | maximumAge, 18 | timeout: 10000, 19 | enableHighAccuracy: true 20 | } 21 | 22 | window.navigator.geolocation.getCurrentPosition( 23 | resolve, 24 | (err) => reject(err), 25 | geolocationOptions 26 | ) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /src/entity/Rule.js: -------------------------------------------------------------------------------- 1 | import isString from 'lodash-es/isString' 2 | import Entity from './Entity' 3 | import Resource from '../resource/Resource' 4 | 5 | const path = '/rules' 6 | 7 | /** 8 | * Represents a Rule entity object. 9 | * 10 | * @extends Entity 11 | */ 12 | export default class Rule extends Entity { 13 | /** 14 | * Return simple resource factory for Rules. 15 | * 16 | * @static 17 | * @return {{rule: Function}} 18 | */ 19 | static resourceFactory () { 20 | return { 21 | rule (ruleName) { 22 | if (!isString(ruleName)) { 23 | throw new TypeError('Rule name must be a string') 24 | } 25 | 26 | return Object.assign(Resource.factoryFor(Rule, path).call(this, ruleName), { 27 | run (...args) { 28 | return Resource.prototype.create.call(this, ...args) 29 | } 30 | }) 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/entity/Thng.js: -------------------------------------------------------------------------------- 1 | import CommissionState from './CommissionState' 2 | import Entity from './Entity' 3 | import Property from './Property' 4 | import Action from './Action' 5 | import Location from './Location' 6 | import Redirection from './Redirection' 7 | import Resource from '../resource/Resource' 8 | import { mixinResources } from '../util/mixin' 9 | 10 | const path = '/thngs' 11 | const ThngResources = mixinResources([Property, Action, Location, Redirection, CommissionState]) 12 | 13 | /** 14 | * Represents a Thng entity object. 15 | * 16 | * @extends Entity 17 | */ 18 | export default class Thng extends ThngResources(Entity) { 19 | /** 20 | * Return simple resource factory for Thngs. 21 | * 22 | * @static 23 | * @return {{thng: Function}} 24 | */ 25 | static resourceFactory () { 26 | return { 27 | thng: Resource.factoryFor(Thng, path, ThngResources, 'thng') 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/File.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import File from '../../../src/entity/File' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyScope } from '../../helpers/dummy' 7 | import { fileTemplate } from '../../helpers/data' 8 | 9 | let fileResource 10 | 11 | describe('File', () => { 12 | mockApi() 13 | 14 | describe('resourceFactory', () => { 15 | beforeEach(() => { 16 | const scope = Object.assign(dummyScope(), File.resourceFactory()) 17 | fileResource = scope.file(fileTemplate.id) 18 | }) 19 | 20 | it('should create new Role resource', () => { 21 | expect(fileResource instanceof Resource).toBe(true) 22 | expect(fileResource.type).toBe(File) 23 | expect(fileResource.path).toEqual(`${paths.files}/${fileTemplate.id}`) 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/User.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import User from '../../../src/entity/User' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyScope } from '../../helpers/dummy' 7 | import { userTemplate } from '../../helpers/data' 8 | 9 | let userResource 10 | 11 | describe('User', () => { 12 | mockApi() 13 | 14 | describe('resourceFactory', () => { 15 | beforeEach(() => { 16 | const scope = Object.assign(dummyScope(), User.resourceFactory()) 17 | userResource = scope.user(userTemplate.id) 18 | }) 19 | 20 | it('should create new Product resource', () => { 21 | expect(userResource instanceof Resource).toBe(true) 22 | expect(userResource.type).toBe(User) 23 | expect(userResource.path).toEqual(`${paths.users}/${userTemplate.id}`) 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/Place.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import Place from '../../../src/entity/Place' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyScope } from '../../helpers/dummy' 7 | import { placeTemplate } from '../../helpers/data' 8 | 9 | let placeResource 10 | 11 | describe('Place', () => { 12 | mockApi() 13 | 14 | describe('resourceFactory', () => { 15 | beforeEach(() => { 16 | const scope = Object.assign(dummyScope(), Place.resourceFactory()) 17 | placeResource = scope.place(placeTemplate.id) 18 | }) 19 | 20 | it('should create new Product resource', () => { 21 | expect(placeResource instanceof Resource).toBe(true) 22 | expect(placeResource.type).toBe(Place) 23 | expect(placeResource.path).toEqual(`${paths.places}/${placeTemplate.id}`) 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/entity/Account.js: -------------------------------------------------------------------------------- 1 | import Access from './Access' 2 | import Domain from './Domain' 3 | import OperatorAccess from './OperatorAccess' 4 | import Entity from './Entity' 5 | import ShortDomain from './ShortDomain' 6 | import Resource from '../resource/Resource' 7 | import { mixinResources } from '../util/mixin' 8 | 9 | const path = '/accounts' 10 | const AccountResources = mixinResources([ 11 | Domain, // R 12 | ShortDomain, // R 13 | Access, // RU 14 | OperatorAccess // CRUD 15 | ]) 16 | 17 | /** 18 | * Represents an Account entity object. 19 | * 20 | * @extends Entity 21 | */ 22 | export default class Account extends AccountResources(Entity) { 23 | /** 24 | * Return simple resource factory for Accounts. 25 | * 26 | * @static 27 | * @return {{sharedAccount: Function}} 28 | */ 29 | static resourceFactory () { 30 | return { 31 | sharedAccount: Resource.factoryFor(Account, path, AccountResources) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/entity/ReactorScript.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Status from './Status' 3 | import Resource from '../resource/Resource' 4 | import { mixinResources } from '../util/mixin' 5 | import isString from 'lodash-es/isString' 6 | 7 | const path = '/reactor/script' 8 | const ReactorScriptResources = mixinResources([Status]) 9 | 10 | /** 11 | * Represents a ReactorScript entity object. 12 | * 13 | * @extends Entity 14 | */ 15 | export default class ReactorScript extends ReactorScriptResources(Entity) { 16 | static resourceFactory () { 17 | return { 18 | reactorScript () { 19 | // Reactor scripts don't have single resource endpoint (e.g.: /scripts/:id) 20 | if (isString(arguments[0])) { 21 | throw new TypeError('There is no single resource for Reactor Scripts') 22 | } 23 | 24 | return Resource.factoryFor(ReactorScript, path, ReactorScriptResources).call(this) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/integration/entity/adiOrderEvents.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('ADI Orders', () => { 6 | let scope, api 7 | 8 | before(() => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should create an ADI Order event', async () => { 14 | const payload = { 15 | metadata: { 16 | type: 'encodings', 17 | tags: ['example'] 18 | }, 19 | ids: ['serial1', 'serial2'], 20 | customFields: { internalId: 'X7JF' }, 21 | tags: ['X7JF'] 22 | } 23 | 24 | api 25 | .post('/adis/orders/adiOrderId/events', payload) 26 | .reply(201, { id: 'UrCPgMhMMmPEY6awwEf6gKfb' }) 27 | const res = await scope.adiOrder('adiOrderId').event().create(payload) 28 | 29 | expect(res).to.be.an('object') 30 | expect(res.id).to.be.a('string') 31 | }) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/Status.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import Status from '../../../src/entity/Status' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyScope } from '../../helpers/dummy' 7 | 8 | let statusResource 9 | let scope 10 | 11 | describe('Status', () => { 12 | mockApi() 13 | 14 | describe('resourceFactory', () => { 15 | beforeEach(() => { 16 | scope = Object.assign(dummyScope(), Status.resourceFactory()) 17 | }) 18 | 19 | it('should not allow single resource access', () => { 20 | const singleResource = () => scope.status('id') 21 | expect(singleResource).toThrow() 22 | }) 23 | 24 | it('should create new Status resource', () => { 25 | statusResource = scope.status() 26 | expect(statusResource instanceof Resource).toBe(true) 27 | expect(statusResource.type).toBe(Status) 28 | expect(statusResource.path).toEqual(`${paths.status}`) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/entity/Domain.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | import Scope from '../scope/Scope' 4 | import isString from 'lodash-es/isString' 5 | 6 | const path = '/domains' 7 | 8 | /** 9 | * Represents a Domain entity. 10 | * 11 | * @extends Entity 12 | */ 13 | export default class Domain extends Entity { 14 | /** 15 | * Return simple resource factory for Domains. 16 | * 17 | * @static 18 | * @return {{domain: Function}} 19 | */ 20 | static resourceFactory () { 21 | return { 22 | domain () { 23 | // Domains don't have single resource endpoint (e.g.: /domains/:id) 24 | if (isString(arguments[0])) { 25 | throw new TypeError('There is no single resource for Domains') 26 | } 27 | 28 | // Only allowed on Entities and Resources. 29 | if (this instanceof Scope) { 30 | throw new Error('Domain is not a top-level resource.') 31 | } 32 | 33 | return Resource.factoryFor(Domain, path).call(this) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/evrythng.js: -------------------------------------------------------------------------------- 1 | // Globals 2 | export { default as settings } from './settings' 3 | export { default as setup } from './setup' 4 | export { default as api } from './api' 5 | export { default as alias } from './alias' 6 | export { default as use } from './use' 7 | 8 | // Scopes 9 | export { default as AccessToken } from './scope/AccessToken' 10 | export { default as Operator } from './scope/Operator' 11 | export { default as ActionApp } from './scope/ActionApp' 12 | export { default as Application } from './scope/Application' 13 | export { default as TrustedApplication } from './scope/TrustedApplication' 14 | export { default as User } from './scope/User' 15 | export { default as Device } from './scope/Device' 16 | 17 | // Namespaces for: Entities and Symbols 18 | export { default as Entity } from './entities' 19 | export { default as Symbol } from './symbols' 20 | 21 | // Extensible internal modules 22 | export { default as _Resource } from './resource/Resource' 23 | export { default as _Entity } from './entity/Entity' 24 | export { default as _Scope } from './scope/Scope' 25 | -------------------------------------------------------------------------------- /test/integration/entity/me.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, settings) => { 5 | describe('Me', () => { 6 | let scope, api 7 | 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | api = mockApi(settings.apiUrl) 11 | }) 12 | 13 | if (settings.apiVersion == 2) { 14 | it('should read access', async () => { 15 | api.get('/me').reply(200, { id: 'operatorId' }) 16 | const res = await scope.me().read() 17 | 18 | expect(res).to.be.an('object') 19 | expect(res.id).to.be.a('string') 20 | }) 21 | } 22 | if (settings.apiVersion == 1) { 23 | it('should NOT read access', async () => { 24 | let caughtError = false 25 | try { 26 | api.get('/me').reply(403, {}) 27 | await scope.me().read() 28 | } catch (err) { 29 | caughtError = true 30 | expect(err).to.exist 31 | } 32 | expect(caughtError).to.be.equal(true) 33 | }) 34 | } 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/entity/ShortDomain.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | import Scope from '../scope/Scope' 4 | import isString from 'lodash-es/isString' 5 | 6 | const path = '/shortDomains' 7 | 8 | /** 9 | * Represents a ShortDomain entity. 10 | * 11 | * @extends Entity 12 | */ 13 | export default class ShortDomain extends Entity { 14 | /** 15 | * Return simple resource factory for ShortDomains. 16 | * 17 | * @static 18 | * @return {{shortDomain: Function}} 19 | */ 20 | static resourceFactory () { 21 | return { 22 | shortDomain () { 23 | // ShortDomains don't have single resource endpoint (e.g.: /shortDomains/:id) 24 | if (isString(arguments[0])) { 25 | throw new TypeError('There is no single resource for ShortDomains') 26 | } 27 | 28 | // Only allowed on Entities and Resources. 29 | if (this instanceof Scope) { 30 | throw new Error('ShortDomain is not a top-level resource.') 31 | } 32 | 33 | return Resource.factoryFor(ShortDomain, path).call(this) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/helpers/paths.js: -------------------------------------------------------------------------------- 1 | import { operatorTemplate, projectTemplate, applicationTemplate, userAccessTemplate } from './data' 2 | 3 | export default { 4 | testBase: 'https://api-test.evrythng.net', 5 | error: '/error', 6 | access: '/access', 7 | actions: '/actions', 8 | actionTypes: '/actions', 9 | applications: '/applications', 10 | application: `/projects/${projectTemplate.id}/applications/${applicationTemplate.id}`, 11 | batches: '/batches', 12 | collections: '/collections', 13 | dummy: '/path', 14 | files: '/files', 15 | locations: '/locations', 16 | operators: '/operators', 17 | operator: `/operators/${operatorTemplate.id}`, 18 | permissions: '/permissions', 19 | places: '/places', 20 | products: '/products', 21 | projects: '/projects', 22 | properties: '/properties', 23 | reactorScript: '/reactor/script', 24 | reactorSchedules: '/reactor/schedules', 25 | reactorLogs: '/reactor/logs', 26 | roles: '/roles', 27 | status: '/status', 28 | tasks: '/tasks', 29 | thngs: '/thngs', 30 | users: '/users', 31 | usersAccess: '/auth/evrythng/users', 32 | usersAccessValidate: `${userAccessTemplate.evrythngUser}/validate` 33 | } 34 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/ReactorSchedule.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import ReactorSchedule from '../../../src/entity/ReactorSchedule' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyResource } from '../../helpers/dummy' 7 | import { reactorScheduleTemplate } from '../../helpers/data' 8 | 9 | let reactorScheduleResource 10 | 11 | describe('ReactorSchedule', () => { 12 | mockApi() 13 | 14 | describe('resourceFactory', () => { 15 | beforeEach(() => { 16 | const resource = Object.assign(dummyResource(), ReactorSchedule.resourceFactory()) 17 | reactorScheduleResource = resource.reactorSchedule(reactorScheduleTemplate.id) 18 | }) 19 | 20 | it('should create new ReactorSchedule resource', () => { 21 | expect(reactorScheduleResource instanceof Resource).toBe(true) 22 | expect(reactorScheduleResource.type).toBe(ReactorSchedule) 23 | expect(reactorScheduleResource.path).toEqual( 24 | `${paths.dummy}${paths.reactorSchedules}/${reactorScheduleTemplate.id}` 25 | ) 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/entity/File.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | 4 | const path = '/files' 5 | 6 | /** 7 | * Represents a File entity object. 8 | * 9 | * @extends Entity 10 | */ 11 | export default class File extends Entity { 12 | /** 13 | * Return simple resource factory for Files. 14 | * 15 | * @static 16 | * @return {{file: Function}} 17 | */ 18 | static resourceFactory () { 19 | return { 20 | file (id) { 21 | return Object.assign(Resource.factoryFor(File, path).call(this, id), { 22 | upload 23 | }) 24 | } 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * Upload file data, previously obtained from disk or the user. 31 | * 32 | * Note: Only text or encoded text is currently supported here. 33 | * 34 | * @param {*} data - Data as buffer or string. 35 | */ 36 | async function upload (data) { 37 | const resource = await this.read() 38 | const opts = { 39 | method: 'put', 40 | headers: { 41 | 'Content-Type': resource.type, 42 | 'x-amz-acl': resource.privateAccess ? 'private' : 'public-read' 43 | }, 44 | body: data 45 | } 46 | 47 | return fetch(resource.uploadUrl, opts) 48 | } 49 | -------------------------------------------------------------------------------- /src/entity/CommissionState.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | import Scope from '../scope/Scope' 4 | import symbols from '../symbols' 5 | import isString from 'lodash-es/isString' 6 | 7 | const path = '/commissionState' 8 | 9 | /** 10 | * Represents a Thng's commissioning state. 11 | * 12 | * @extends Entity 13 | */ 14 | export default class CommissionState extends Entity { 15 | /** 16 | * Return resource factory for CommissionState. 17 | * 18 | * Only read() is permitted by the API. 19 | * 20 | * @static 21 | * @return {{commissionState: Function}} 22 | */ 23 | static resourceFactory () { 24 | return { 25 | commissionState () { 26 | // commissionState don't have single resource endpoint (e.g.: /locations/:id) 27 | if (isString(arguments[0])) { 28 | throw new TypeError('There is no single resource for CommissionState') 29 | } 30 | 31 | // Concatenate on the end of the Thng resource location 32 | const thngPath = this instanceof Scope ? this[symbols.path] : '' 33 | 34 | return Resource.factoryFor(CommissionState, thngPath + path).call(this) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/integration/entity/accountRedirector.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Account Redirector', () => { 6 | let scope, api 7 | 8 | before(() => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should read the account Redirector', async () => { 14 | api.get('/redirector').reply(200, { rules: [] }) 15 | const res = await scope.redirector().read() 16 | 17 | expect(res).to.be.an('object') 18 | expect(res.rules).to.be.an('array') 19 | }) 20 | 21 | it('should update the account redirector', async () => { 22 | const payload = { 23 | rules: [{ match: 'thng.name=test' }] 24 | } 25 | api.put('/redirector', payload).reply(200, payload) 26 | const res = await scope.redirector().update(payload) 27 | 28 | expect(res.rules).to.deep.equal(payload.rules) 29 | }) 30 | 31 | it('should delete the account redirector', async () => { 32 | api.delete('/redirector').reply(204) 33 | const res = await scope.redirector().delete() 34 | 35 | expect(res).to.not.exist 36 | }) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /test/integration/scope/operator.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (url) => { 5 | describe('Operator', () => { 6 | let operator, api 7 | 8 | before(() => { 9 | operator = getScope('operator') 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should read Operator resource', async () => { 14 | expect(operator.id).to.equal('operatorId') 15 | expect(operator.email).to.equal('test.user@evrythng.com') 16 | expect(operator.firstName).to.equal('Test') 17 | expect(operator.lastName).to.equal('User') 18 | }) 19 | 20 | it('should allow self-same Operator update', async () => { 21 | api.put('/operators/operatorId').reply(200, { 22 | customFields: { foo: 'bar' } 23 | }) 24 | const res = await operator.update({ 25 | customFields: { 26 | foo: 'bar' 27 | } 28 | }) 29 | 30 | // Response should be accurate of new state 31 | expect(res).to.be.an('object') 32 | expect(res.customFields.foo).to.equal('bar') 33 | 34 | // Operator should have updated state 35 | expect(operator.customFields.foo).to.equal('bar') 36 | }) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | 3 | const path = resolve(__dirname, 'dist') 4 | const entry = './src/evrythng.polyfill.js' 5 | const library = 'evrythng' 6 | 7 | const babelrc = { 8 | presets: ['@babel/preset-env'], 9 | plugins: [['@babel/transform-runtime', { regenerator: true }]] 10 | } 11 | 12 | const browserConfig = { 13 | entry, 14 | output: { 15 | path, 16 | library, 17 | filename: 'evrythng.browser.js', 18 | libraryTarget: 'var' 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.js$/, 24 | exclude: /node_modules/, 25 | use: { 26 | loader: 'babel-loader', 27 | options: babelrc 28 | } 29 | } 30 | ] 31 | } 32 | } 33 | 34 | const nodeConfig = { 35 | entry, 36 | target: 'node', 37 | output: { 38 | path, 39 | library, 40 | filename: 'evrythng.node.js', 41 | libraryTarget: 'umd', 42 | umdNamedDefine: true, 43 | globalObject: "typeof self !== 'undefined' ? self : this" 44 | }, 45 | module: { 46 | rules: [ 47 | { 48 | test: /\.js$/, 49 | exclude: /node_modules/ 50 | } 51 | ] 52 | } 53 | } 54 | 55 | module.exports = [browserConfig, nodeConfig] 56 | -------------------------------------------------------------------------------- /test/helpers/responses.js: -------------------------------------------------------------------------------- 1 | import * as data from './data' 2 | 3 | export default { 4 | ok: { 5 | status: 200, 6 | body: {} 7 | }, 8 | 9 | noContent: { 10 | status: 204, 11 | body: null 12 | }, 13 | 14 | error: { 15 | generic: { 16 | status: 400, 17 | body: { 18 | status: 400, 19 | errors: ['Generic error'] 20 | } 21 | } 22 | }, 23 | 24 | access: { 25 | operator: { 26 | body: { 27 | actor: { 28 | type: 'operator', 29 | id: data.operatorTemplate.id 30 | }, 31 | apiKey: data.apiKey 32 | } 33 | }, 34 | application: { 35 | body: { 36 | project: data.projectTemplate.id, 37 | actor: { 38 | type: 'app', 39 | id: data.applicationTemplate.id 40 | } 41 | } 42 | } 43 | }, 44 | 45 | operator: { 46 | one: { 47 | body: data.operatorTemplate 48 | } 49 | }, 50 | 51 | application: { 52 | one: { 53 | body: data.applicationTemplate 54 | } 55 | }, 56 | 57 | entity: { 58 | one: { 59 | body: data.entityTemplate 60 | }, 61 | multiple: { 62 | body: [data.entityTemplate, data.entityTemplate] 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/Task.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import Task from '../../../src/entity/Task' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyScope, dummyResource } from '../../helpers/dummy' 7 | 8 | let taskResource 9 | let resource 10 | 11 | describe('Task', () => { 12 | mockApi() 13 | 14 | describe('resourceFactory', () => { 15 | beforeEach(() => { 16 | resource = Object.assign(dummyResource(), Task.resourceFactory()) 17 | }) 18 | 19 | it('should not be allowed as top level resource (on Scopes)', () => { 20 | const scope = Object.assign(dummyScope(), Task.resourceFactory()) 21 | const topLevelResource = () => scope.task() 22 | expect(topLevelResource).toThrow() 23 | }) 24 | 25 | it('should create new Task resource', () => { 26 | taskResource = resource.task() 27 | expect(taskResource instanceof Resource).toBe(true) 28 | expect(taskResource.type).toBe(Task) 29 | }) 30 | 31 | it('should add permissions path', () => { 32 | taskResource = resource.task() 33 | expect(taskResource.path).toEqual(`${paths.dummy}${paths.tasks}`) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/setup.js: -------------------------------------------------------------------------------- 1 | import settings from './settings' 2 | 3 | /** 4 | * Override global settings. Ignore unknown settings. 5 | * 6 | * @param {Object} customSettings - Custom settings 7 | * @returns {Object} new 8 | */ 9 | 10 | /** Allowed API versions */ 11 | const VERSIONS = [1, 2] 12 | /** Allowed API regions */ 13 | const REGIONS = ['us', 'eu'] 14 | /** Map of API URLs by version and region */ 15 | const API_MAP = { 16 | 2: { 17 | us: 'https://api.evrythng.io/v2', 18 | eu: 'https://api.eu.evrythng.io/v2' 19 | }, 20 | 1: { 21 | us: 'https://api.evrythng.com', 22 | eu: 'https://api-eu.evrythng.com' 23 | } 24 | } 25 | 26 | export default function setup (newSettings = {}) { 27 | const { apiUrl, apiVersion = 2, region = 'us' } = newSettings 28 | 29 | if (!VERSIONS.includes(apiVersion)) { 30 | throw new Error(`Invalid apiVersion: ${apiVersion}. Choose from ${VERSIONS.join(', ')}`) 31 | } 32 | if (!REGIONS.includes(region)) { 33 | throw new Error(`Invalid region: ${region}. Choose from ${REGIONS.join(', ')}`) 34 | } 35 | 36 | // Set the API URL and region 37 | newSettings.apiUrl = apiUrl || API_MAP[apiVersion][region] 38 | newSettings.apiVersion = apiVersion 39 | newSettings.region = region 40 | 41 | return Object.assign(settings, newSettings) 42 | } 43 | -------------------------------------------------------------------------------- /test/integration/misc/stream.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('stream', () => { 6 | let scope, api 7 | 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should stream Thngs once at a time', (done) => { 14 | const linkUrl = encodeURIComponent(url) 15 | api.get('/thngs').reply(200, [{ name: 'Thng 1' }, { name: 'Thng 2' }], { 16 | link: `<${linkUrl}%2Fthngs%3FperPage%3D30%26sortOrder%3DDESCENDING%26nextPageToken%3DU7hXyw5DVQ8QT7fYsbyEpdAp>; rel="next"` 17 | }) 18 | api 19 | .get('/thngs?perPage=30&sortOrder=DESCENDING&nextPageToken=U7hXyw5DVQ8QT7fYsbyEpdAp') 20 | .reply(200, [{ name: 'Thng 3' }, { name: 'Thng 4' }], { 21 | link: `<${linkUrl}%2Fthngs%3FperPage%3D2%26sortOrder%3DDESCENDING%26nextPageToken%3DUprntQaysgRph8aRwFTAKPtn>; rel="next"` 22 | }) 23 | const cb = (item, index) => { 24 | expect(item.name).to.be.a('string') 25 | expect(index).to.be.a('number') 26 | 27 | if (index === 2) { 28 | done() 29 | return true 30 | } 31 | } 32 | 33 | scope.thng().stream(cb) 34 | }) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/entity/Application.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import ReactorScript from './ReactorScript' 3 | import ReactorSchedule from './ReactorSchedule' 4 | import ReactorLog from './ReactorLog' 5 | import Redirector from './Redirector' 6 | import Resource from '../resource/Resource' 7 | import SecretKey from './SecretKey' 8 | import Scope from '../scope/Scope' 9 | import { mixinResources } from '../util/mixin' 10 | 11 | const path = '/applications' 12 | const ApplicationResources = mixinResources([ 13 | ReactorScript, 14 | ReactorSchedule, 15 | ReactorLog, 16 | Redirector, 17 | SecretKey 18 | ]) 19 | 20 | /** 21 | * Represents an Application entity. 22 | * 23 | * @extends Entity 24 | */ 25 | export default class Application extends ApplicationResources(Entity) { 26 | /** 27 | * Return simple resource factory for Applications. 28 | * 29 | * @static 30 | * @return {{application: Function}} 31 | */ 32 | static resourceFactory () { 33 | return { 34 | application (id) { 35 | // Only allowed on Entities and Resources. 36 | if (this instanceof Scope) { 37 | throw new Error('Application is not a top-level resource.') 38 | } 39 | 40 | return Resource.factoryFor(Application, path, ApplicationResources).call(this, id) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/integration/entity/accounts.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Accounts', () => { 6 | let scope, api 7 | 8 | before(() => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should read all shared accounts', async () => { 14 | api.get('/accounts').reply(200, [{ id: 'accountId' }]) 15 | const res = await scope.sharedAccount().read() 16 | 17 | expect(res).to.be.an('array') 18 | expect(res).to.have.length.gte(1) 19 | }) 20 | 21 | it('should read a shared account', async () => { 22 | api.get('/accounts/accountId').reply(200, { id: 'accountId' }) 23 | const res = await scope.sharedAccount('accountId').read() 24 | 25 | expect(res).to.be.an('object') 26 | expect(res.id).to.be.a('string') 27 | }) 28 | 29 | it('should update a shared account', async () => { 30 | const payload = { customFields: { env: 'test' } } 31 | api.put('/accounts/accountId', payload).reply(200, payload) 32 | const res = await scope.sharedAccount('accountId').update(payload) 33 | 34 | expect(res).to.be.an('object') 35 | expect(res.customFields).to.deep.equal(payload.customFields) 36 | }) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/Batch.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import Batch from '../../../src/entity/Batch' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyScope, dummyResource } from '../../helpers/dummy' 7 | import { batchTemplate } from '../../helpers/data' 8 | 9 | let batchResource 10 | let batch 11 | 12 | describe('Batch', () => { 13 | mockApi() 14 | 15 | describe('resourceFactory', () => { 16 | beforeEach(() => { 17 | const scope = Object.assign(dummyScope(), Batch.resourceFactory()) 18 | batchResource = scope.batch(batchTemplate.id) 19 | }) 20 | 21 | it('should create new Batch resource', () => { 22 | expect(batchResource instanceof Resource).toBe(true) 23 | expect(batchResource.type).toBe(Batch) 24 | expect(batchResource.path).toEqual(`${paths.batches}/${batchTemplate.id}`) 25 | }) 26 | 27 | it('should have nested task resource', () => { 28 | expect(batchResource.task).toBeDefined() 29 | }) 30 | }) 31 | 32 | describe('access', () => { 33 | beforeEach(() => { 34 | batch = new Batch(dummyResource()) 35 | }) 36 | 37 | it('should have task resource', () => { 38 | expect(batch.task).toBeDefined() 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/Role.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import Role from '../../../src/entity/Role' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyScope, dummyResource } from '../../helpers/dummy' 7 | import { roleTemplate } from '../../helpers/data' 8 | 9 | let roleResource 10 | let role 11 | 12 | describe('Role', () => { 13 | mockApi() 14 | 15 | describe('resourceFactory', () => { 16 | beforeEach(() => { 17 | const scope = Object.assign(dummyScope(), Role.resourceFactory()) 18 | roleResource = scope.role(roleTemplate.id) 19 | }) 20 | 21 | it('should create new Role resource', () => { 22 | expect(roleResource instanceof Resource).toBe(true) 23 | expect(roleResource.type).toBe(Role) 24 | expect(roleResource.path).toEqual(`${paths.roles}/${roleTemplate.id}`) 25 | }) 26 | 27 | it('should have nested permission resource', () => { 28 | expect(roleResource.permission).toBeDefined() 29 | }) 30 | }) 31 | 32 | describe('access', () => { 33 | beforeEach(() => { 34 | role = new Role(dummyResource()) 35 | }) 36 | 37 | it('should have permission resource', () => { 38 | expect(role.permission).toBeDefined() 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/integration/entity/accesses.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Accesses', () => { 6 | let scope, api 7 | before(() => { 8 | scope = getScope(scopeType) 9 | api = mockApi(url) 10 | }) 11 | 12 | it('should read all account accesses', async () => { 13 | api.get('/accounts/accountId/accesses').reply(200, [{ id: 'accessId' }]) 14 | const res = await scope.sharedAccount('accountId').access().read() 15 | 16 | expect(res).to.have.length.gte(1) 17 | }) 18 | 19 | it('should read a single account access', async () => { 20 | api.get('/accounts/accountId/accesses/accessId').reply(200, { id: 'accessId' }) 21 | const res = await scope.sharedAccount('accountId').access('accessId').read() 22 | 23 | expect(res).to.be.an('object') 24 | expect(res.id).to.be.a('string') 25 | }) 26 | 27 | it('should update a single account access', async () => { 28 | const payload = { role: 'admin' } 29 | api.put('/accounts/accountId/accesses/accessId', payload).reply(200, { id: 'accessId' }) 30 | const res = await scope.sharedAccount('accountId').access('accessId').update(payload) 31 | 32 | expect(res).to.be.an('object') 33 | expect(res.id).to.equal('accessId') 34 | }) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /test/integration/misc/streamPages.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('streamPages', () => { 6 | let scope, api 7 | 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should stream pages of Thngs', (done) => { 14 | const linkUrl = encodeURIComponent(url) 15 | api.get('/thngs').reply(200, [{ name: 'Thng 1' }, { name: 'Thng 2' }], { 16 | link: `<${linkUrl}%2Fthngs%3FperPage%3D30%26sortOrder%3DDESCENDING%26nextPageToken%3DU7hXyw5DVQ8QT7fYsbyEpdAp>; rel="next"` 17 | }) 18 | api 19 | .get('/thngs?perPage=30&sortOrder=DESCENDING&nextPageToken=U7hXyw5DVQ8QT7fYsbyEpdAp') 20 | .reply(200, [{ name: 'Thng 3' }, { name: 'Thng 4' }], { 21 | link: `<${linkUrl}%2Fthngs%3FperPage%3D2%26sortOrder%3DDESCENDING%26nextPageToken%3DUprntQaysgRph8aRwFTAKPtn>; rel="next"` 22 | }) 23 | const eachPageCb = (page, totalSoFar) => { 24 | expect(page.length).to.equal(2) 25 | 26 | const [item] = page 27 | expect(item.name).to.be.a('string') 28 | 29 | if (totalSoFar === 2) { 30 | done() 31 | return true 32 | } 33 | } 34 | 35 | scope.thng().streamPages(eachPageCb) 36 | }) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /test/helpers/dummy.js: -------------------------------------------------------------------------------- 1 | import Scope from '../../src/scope/Scope' 2 | import Resource from '../../src/resource/Resource' 3 | import Entity from '../../src/entity/Entity' 4 | import paths from './paths' 5 | import { apiKey, entityTemplate } from './data' 6 | 7 | // Any call to dummy creators below should be inside test blocks (either 8 | // `beforeAll`, `beforeEach` or `it`). 9 | 10 | /** 11 | * Return new simplistic Scope as base for scope mixins. 12 | * 13 | * @param {string} [key] - Optional API Key 14 | * @return {Scope} 15 | */ 16 | export function dummyScope (key = apiKey) { 17 | return new Scope(key) 18 | } 19 | 20 | /** 21 | * Return new simplistic Resource as base for resource mixins. 22 | * 23 | * @param {Entity} [EntityType] - Entity class 24 | * @param {string} [path] - Path for resource 25 | * @return {Resource} 26 | */ 27 | export function dummyResource (EntityType = Entity, path = paths.dummy) { 28 | const scope = dummyScope() 29 | return new Resource(scope, path, EntityType) 30 | } 31 | 32 | /** 33 | * Return new simplistic Entity as base for entity mixins. 34 | * 35 | * @param {Entity} [EntityType] - Entity class 36 | * @param {Object} [body] - Predefined entity body 37 | * @return {Entity} 38 | */ 39 | export function dummyEntity (EntityType = Entity, body = entityTemplate) { 40 | const resource = dummyResource() 41 | return new EntityType(resource, body) 42 | } 43 | -------------------------------------------------------------------------------- /test/integration/entity/locations.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | let scope, api 6 | 7 | describe('Locations', () => { 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it("should update a Thng's location", async () => { 14 | const payload = [ 15 | { 16 | position: { type: 'Point', coordinates: [-17.3, 36] } 17 | } 18 | ] 19 | api.put('/thngs/thngId/location', payload).reply(200, payload) 20 | const res = await scope.thng('thngId').locations().update(payload) 21 | 22 | expect(res).to.be.an('array') 23 | expect(res).to.have.length.gte(1) 24 | }) 25 | 26 | it("should read a Thng's location", async () => { 27 | api.get('/thngs/thngId/location').reply(200, [ 28 | { 29 | position: { type: 'Point', coordinates: [-17.3, 36] } 30 | } 31 | ]) 32 | const res = await scope.thng('thngId').locations().read() 33 | 34 | expect(res).to.be.an('array') 35 | expect(res).to.have.length.gte(1) 36 | }) 37 | 38 | it("should delete a Thng's location", async () => { 39 | api.delete('/thngs/thngId/location').reply(200) 40 | await scope.thng('thngId').locations().delete() 41 | }) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/Project.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import Project from '../../../src/entity/Project' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyScope, dummyResource } from '../../helpers/dummy' 7 | import { projectTemplate } from '../../helpers/data' 8 | 9 | let projectResource 10 | let project 11 | 12 | describe('Project', () => { 13 | mockApi() 14 | 15 | describe('resourceFactory', () => { 16 | beforeEach(() => { 17 | const scope = Object.assign(dummyScope(), Project.resourceFactory()) 18 | projectResource = scope.project(projectTemplate.id) 19 | }) 20 | 21 | it('should create new Product resource', () => { 22 | expect(projectResource instanceof Resource).toBe(true) 23 | expect(projectResource.type).toBe(Project) 24 | expect(projectResource.path).toEqual(`${paths.projects}/${projectTemplate.id}`) 25 | }) 26 | 27 | it('should have nested application resource', () => { 28 | expect(projectResource.application).toBeDefined() 29 | }) 30 | }) 31 | 32 | describe('access', () => { 33 | beforeEach(() => { 34 | project = new Project(dummyResource()) 35 | }) 36 | 37 | it('should have application resource', () => { 38 | expect(project.application).toBeDefined() 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/unit/outDated/util/buildParams.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import buildParams from '../../../src/util/buildParams' 3 | 4 | describe('buildParams', () => { 5 | it('should handle empty params', () => { 6 | expect(buildParams()).toEqual('') 7 | expect(buildParams({})).toEqual('') 8 | }) 9 | 10 | it('should do nothing on string values', () => { 11 | const qs = 'foo=bar' 12 | expect(buildParams(qs)).toEqual(qs) 13 | }) 14 | 15 | it('should join params', () => { 16 | const params = { 17 | foo: 'bar', 18 | baz: 1 19 | } 20 | const paramsStr = 'foo=bar&baz=1' 21 | expect(buildParams(params)).toEqual(paramsStr) 22 | }) 23 | 24 | it('should encode query string', () => { 25 | const params = { 26 | 'a+b': 'a=b' 27 | } 28 | const paramsStr = 'a%2Bb=a%3Db' 29 | expect(buildParams(params)).toEqual(paramsStr) 30 | }) 31 | 32 | it('should handle nested params', () => { 33 | const params = { 34 | a: { 35 | b: 'c', 36 | d: 'e=1' 37 | }, 38 | f: 1 39 | } 40 | const paramsStr = 'a=b%3Dc%26d%3De%253D1&f=1' 41 | expect(buildParams(params)).toEqual(paramsStr) 42 | }) 43 | 44 | it('should escape special characters', () => { 45 | const params = { 46 | a: 'va|ue' 47 | } 48 | const paramsStr = 'a=va%7Cue' 49 | expect(buildParams(params)).toEqual(paramsStr) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /test/integration/misc/pages.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('pages', () => { 6 | let scope, api 7 | 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should read thngs through an async iterator', async () => { 14 | const params = { perPage: 2 } 15 | const linkUrl = encodeURIComponent(url) 16 | const it = scope.thng().pages({ params }) 17 | api.get('/thngs?perPage=2').reply(200, [{ name: 'Thng 1' }, { name: 'Thng 2' }], { 18 | link: `<${linkUrl}%2Fthngs%3FperPage%3D2%26sortOrder%3DDESCENDING%26nextPageToken%3DU7hXyw5DVQ8QT7fYsbyEpdAp>; rel="next"` 19 | }) 20 | let page = await it.next() 21 | 22 | expect(page.value.length).to.equal(2) 23 | expect(page.done).to.equal(false) 24 | 25 | api 26 | .get('/thngs?perPage=2&sortOrder=DESCENDING&nextPageToken=U7hXyw5DVQ8QT7fYsbyEpdAp') 27 | .reply(200, [{ name: 'Thng 3' }, { name: 'Thng 4' }], { 28 | link: `<${linkUrl}; rel="next"` 29 | }) 30 | page = await it.next() 31 | expect(page.value.length).to.equal(2) 32 | expect(page.done).to.equal(false) 33 | }) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /test/integration/misc/use.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | const evrythng = require('../../../') 4 | 5 | const TestPlugin = { 6 | install: (api) => { 7 | // Extend the Operator scope API 8 | api.scopes.Operator.prototype.getFoo = () => 'foo' 9 | 10 | // Add functionality to a Thng 11 | api.entities.Thng.prototype.getSummary = function () { 12 | return `${this.name} (${this.id})` 13 | } 14 | } 15 | } 16 | 17 | module.exports = (scopeType, url) => { 18 | describe('use', () => { 19 | let scope, api, thng 20 | 21 | before(async () => { 22 | scope = getScope(scopeType) 23 | api = mockApi(url) 24 | 25 | const payload = { name: 'test' } 26 | api.post('/thngs', payload).reply(201, payload) 27 | thng = await scope.thng().create(payload) 28 | }) 29 | 30 | it('should not throw when installing a plugin', async () => { 31 | evrythng.use(TestPlugin) 32 | }) 33 | 34 | it('should extend a scope with a new method', async () => { 35 | expect(evrythng.Operator.prototype.getFoo).to.be.a('function') 36 | expect(scope.getFoo).to.be.a('function') 37 | expect(scope.getFoo()).to.equal('foo') 38 | }) 39 | 40 | it('should expect an entity with a new method', async () => { 41 | const expected = `${thng.name} (${thng.id})` 42 | expect(thng.getSummary).to.be.a('function') 43 | expect(thng.getSummary()).to.equal(expected) 44 | }) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /src/util/mixin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return a subclass factory function that mixins the resource factory 3 | * functions from all the entities provided. 4 | * This provides a way to extend a Scope class and add a mixin that contains 5 | * all the resource methods available for that scope. 6 | * 7 | * E.g. 8 | * ``` 9 | * class Operator extends OperatorAccess(Scope) {} 10 | * ``` 11 | * 12 | * See: http://raganwald.com/2015/12/28/mixins-subclass-factories-and-method-advice.html#fn:simplified 13 | * 14 | * @param {Array} entities - List of entities to add resources to 15 | * @return {function(Scope)} 16 | */ 17 | export function mixinResources (entities) { 18 | const resourceFactories = entities.map((e) => e.resourceFactory()) 19 | const accessResources = Object.assign({}, ...resourceFactories) 20 | return (Superclass) => mixin(accessResources)(class extends Superclass {}) 21 | } 22 | 23 | /** 24 | * Simplified mixin definition. Enough for our use case. 25 | * See: http://raganwald.com/2015/06/17/functional-mixins.html 26 | * 27 | * @param {Object} behaviour - Shared behaviour object literal 28 | * @param {Boolean} proto - Indicates if mixin should be applied to prototype 29 | * @return {function(target)} 30 | */ 31 | export default function mixin (behaviour, proto = true) { 32 | return (target) => { 33 | for (const property of Reflect.ownKeys(behaviour)) { 34 | Object.defineProperty(proto ? target.prototype : target, property, { 35 | value: behaviour[property] 36 | }) 37 | } 38 | return target 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/Permission.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import Permission from '../../../src/entity/Permission' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyScope, dummyResource } from '../../helpers/dummy' 7 | 8 | let permissionResource 9 | let resource 10 | 11 | describe('Permission', () => { 12 | mockApi() 13 | 14 | describe('resourceFactory', () => { 15 | beforeEach(() => { 16 | resource = Object.assign(dummyResource(), Permission.resourceFactory()) 17 | }) 18 | 19 | it('should not allow single resource access', () => { 20 | const singleResource = () => resource.permission('id') 21 | expect(singleResource).toThrow() 22 | }) 23 | 24 | it('should not be allowed as top level resource (on Scopes)', () => { 25 | const scope = Object.assign(dummyScope(), Permission.resourceFactory()) 26 | const topLevelResource = () => scope.permission() 27 | expect(topLevelResource).toThrow() 28 | }) 29 | 30 | it('should create new Permission resource', () => { 31 | permissionResource = resource.permission() 32 | expect(permissionResource instanceof Resource).toBe(true) 33 | expect(permissionResource.type).toBe(Permission) 34 | }) 35 | 36 | it('should add permissions path', () => { 37 | permissionResource = resource.permission() 38 | expect(permissionResource.path).toEqual(`${paths.dummy}${paths.permissions}`) 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/Thng.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import Thng from '../../../src/entity/Thng' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyScope, dummyResource } from '../../helpers/dummy' 7 | import { thngTemplate } from '../../helpers/data' 8 | 9 | let thngResource 10 | let thng 11 | 12 | describe('Thng', () => { 13 | mockApi() 14 | 15 | describe('resourceFactory', () => { 16 | beforeEach(() => { 17 | const scope = Object.assign(dummyScope(), Thng.resourceFactory()) 18 | thngResource = scope.thng(thngTemplate.id) 19 | }) 20 | 21 | it('should create new Thng resource', () => { 22 | expect(thngResource instanceof Resource).toBe(true) 23 | expect(thngResource.type).toBe(Thng) 24 | expect(thngResource.path).toEqual(`${paths.thngs}/${thngTemplate.id}`) 25 | }) 26 | 27 | it('should have nested property resource', () => { 28 | expect(thngResource.property).toBeDefined() 29 | }) 30 | 31 | it('should have nested action resource', () => { 32 | expect(thngResource.action).toBeDefined() 33 | }) 34 | }) 35 | 36 | describe('access', () => { 37 | beforeEach(() => { 38 | thng = new Thng(dummyResource()) 39 | }) 40 | 41 | it('should have property resource', () => { 42 | expect(thng.property).toBeDefined() 43 | }) 44 | 45 | it('should have action resource', () => { 46 | expect(thng.action).toBeDefined() 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/ReactorScript.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import ReactorScript from '../../../src/entity/ReactorScript' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyScope, dummyResource } from '../../helpers/dummy' 7 | 8 | let reactorScriptResource 9 | let reactorScript 10 | let scope 11 | 12 | describe('ReactorScript', () => { 13 | mockApi() 14 | 15 | describe('resourceFactory', () => { 16 | beforeEach(() => { 17 | scope = Object.assign(dummyScope(), ReactorScript.resourceFactory()) 18 | }) 19 | 20 | it('should not allow single resource access', () => { 21 | const singleResource = () => scope.reactorScript('id') 22 | expect(singleResource).toThrow() 23 | }) 24 | 25 | it('should create new ReactorScript resource', () => { 26 | reactorScriptResource = scope.reactorScript() 27 | expect(reactorScriptResource instanceof Resource).toBe(true) 28 | expect(reactorScriptResource.type).toBe(ReactorScript) 29 | expect(reactorScriptResource.path).toEqual(`${paths.reactorScript}`) 30 | }) 31 | 32 | it('should have nested status resource', () => { 33 | expect(reactorScriptResource.status).toBeDefined() 34 | }) 35 | }) 36 | 37 | describe('access', () => { 38 | beforeEach(() => { 39 | reactorScript = new ReactorScript(dummyResource()) 40 | }) 41 | 42 | it('should have status resource', () => { 43 | expect(reactorScript.status).toBeDefined() 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /test/integration/misc/find.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | const chaiAsPromised = require('chai-as-promised') 4 | 5 | const { expect } = chai 6 | chai.use(chaiAsPromised) 7 | 8 | const payload = { name: 'Test Thng', identifiers: { serial: '78fd6hsd' } } 9 | 10 | module.exports = (scopeType, url) => { 11 | describe('find', () => { 12 | let operator, api 13 | 14 | before(async () => { 15 | operator = getScope(scopeType) 16 | api = mockApi(url) 17 | }) 18 | 19 | it('should find Thngs by identifiers', async () => { 20 | const payload1 = { name: 'Test Thng', identifiers: { serial: '78fd6hsd' } } 21 | api.get('/thngs?filter=identifiers.serial=78fd6hsd').reply(200, [payload1]) 22 | 23 | const res = await operator.thng().find(payload1.identifiers) 24 | 25 | expect(res).to.be.an('array') 26 | expect(res).to.have.length.gte(1) 27 | }) 28 | 29 | it('should refuse to find if given more than one key-value', async () => { 30 | payload.identifiers.foo = 'bar' 31 | 32 | api.get('/thngs?filter=identifiers.serial%3D78fd6hsd').reply(200, payload) 33 | const attempt = operator.thng().find(payload.identifiers) 34 | return expect(attempt).to.eventually.be.rejected 35 | }) 36 | 37 | it('should find Thngs by name', async () => { 38 | api.get('/thngs?filter=name%3DTest%20Thng').reply(200, [payload]) 39 | const res = await operator.thng().find(payload.name) 40 | 41 | expect(res).to.be.an('array') 42 | expect(res).to.have.length.gte(1) 43 | }) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/alias.js: -------------------------------------------------------------------------------- 1 | import Operator from './scope/Operator' 2 | import Application from './scope/Application' 3 | import TrustedApplication from './scope/TrustedApplication' 4 | import User from './scope/User' 5 | 6 | const SCOPES = { 7 | Operator, 8 | Application, 9 | TrustedApplication, 10 | User 11 | } 12 | 13 | /** 14 | * Define a new resource type as a simple alias of an existing resource type, 15 | * and can be used to create use-case or client-specific libraries. 16 | * 17 | * The new resource is made available on the same scopes that had access before. 18 | * The existing resource is not removed or hidden from scopes. The alias in in 19 | * name only. 20 | * 21 | * For example, if a Product is to be used as a SKU by Application Users: 22 | * evrythng.alias({ product: 'sku' }, 'User') 23 | * This enables the use of a 'sku' resource in the exact same way as a product: 24 | * user.sku().read() 25 | * 26 | * @param {object} map - Object mapping the old type to a new type. 27 | * @param {string} target - Name of the scope to apply the new resource to. 28 | */ 29 | export default function alias (map, target) { 30 | const scopes = Object.keys(SCOPES) 31 | if (!scopes.includes(target)) { 32 | throw new Error(`Invalid target. Choose from ${scopes.join(', ')}`) 33 | } 34 | 35 | const original = SCOPES[target] 36 | for (const existing in map) { 37 | if (!original.prototype[existing]) { 38 | throw new Error(`${existing} does not exist for ${target}`) 39 | } 40 | 41 | Object.assign(original.prototype, { 42 | [map[existing]]: original.prototype[existing] 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/unit/outDated/util/getCurrentPosition.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import getCurrentPosition from '../../../src/util/getCurrentPosition' 3 | import { positionTemplate } from '../../helpers/data' 4 | 5 | if (typeof window !== 'undefined') { 6 | describe('getCurrentPosition', () => { 7 | describe('success', () => { 8 | beforeEach(() => { 9 | spyOn(window.navigator.geolocation, 'getCurrentPosition').and.callFake((success) => 10 | success(positionTemplate) 11 | ) 12 | }) 13 | 14 | it('should use defaults', () => { 15 | getCurrentPosition() 16 | expect(window.navigator.geolocation.getCurrentPosition.calls.mostRecent().args[2]).toEqual( 17 | jasmine.objectContaining({ 18 | maximumAge: 0, 19 | timeout: 10000, 20 | enableHighAccuracy: true 21 | }) 22 | ) 23 | }) 24 | 25 | it('should resolve with Geolocation', (done) => { 26 | getCurrentPosition().then((resp) => { 27 | expect(resp).toEqual(positionTemplate) 28 | done() 29 | }) 30 | }) 31 | }) 32 | 33 | describe('error', () => { 34 | beforeEach(() => { 35 | spyOn(window.navigator.geolocation, 'getCurrentPosition').and.callFake((success, error) => 36 | error(new Error('Location error')) 37 | ) 38 | }) 39 | 40 | it('should reject with text message', (done) => { 41 | getCurrentPosition().catch((resp) => { 42 | expect(resp).toEqual(jasmine.any(String)) 43 | done() 44 | }) 45 | }) 46 | }) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /src/scope/Device.js: -------------------------------------------------------------------------------- 1 | import Scope from './Scope' 2 | import Property from '../entity/Property' 3 | import Action from '../entity/Action' 4 | import Location from '../entity/Location' 5 | import symbols from '../symbols' 6 | import { mixinResources } from '../util/mixin' 7 | 8 | /** 9 | * Mixin with all the top-level Application resources. 10 | * 11 | * @mixin 12 | */ 13 | const DeviceAccess = mixinResources([ 14 | Property, // CRUD 15 | Action, // CRUD 16 | Location // CRUD 17 | ]) 18 | 19 | /** 20 | * Device is the Scope that represents an active/smart Thng. It can only 21 | * essentially update itself and its nested resources (e.g. Property, Location, 22 | * Action). 23 | * 24 | * @extends Scope 25 | */ 26 | export default class Device extends DeviceAccess(Scope) { 27 | /** 28 | * Creates an instance of Device. 29 | * 30 | * @param {string} apiKey - API Key of scope 31 | * @param {Object} [data={}] - Optional device data 32 | */ 33 | constructor (apiKey, data = {}) { 34 | super(apiKey, data) 35 | 36 | this.initPromise = super 37 | .readAccess() 38 | .then((access) => { 39 | this.id = access.actor.id 40 | this[symbols.path] = this._getPath() 41 | }) 42 | .then(() => this.read()) 43 | } 44 | 45 | /** 46 | * Read the Thng's data asynchronously. 47 | * 48 | * @returns {Promise} 49 | */ 50 | init () { 51 | return this.initPromise 52 | } 53 | 54 | // PRIVATE 55 | 56 | /** 57 | * Return device thng endpoint. 58 | * 59 | * @return {string} 60 | */ 61 | _getPath () { 62 | return `/thngs/${this.id}` 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/Product.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import Product from '../../../src/entity/Product' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyScope, dummyResource } from '../../helpers/dummy' 7 | import { productTemplate } from '../../helpers/data' 8 | 9 | let productResource 10 | let product 11 | 12 | describe('Product', () => { 13 | mockApi() 14 | 15 | describe('resourceFactory', () => { 16 | beforeEach(() => { 17 | const scope = Object.assign(dummyScope(), Product.resourceFactory()) 18 | productResource = scope.product(productTemplate.id) 19 | }) 20 | 21 | it('should create new Product resource', () => { 22 | expect(productResource instanceof Resource).toBe(true) 23 | expect(productResource.type).toBe(Product) 24 | expect(productResource.path).toEqual(`${paths.products}/${productTemplate.id}`) 25 | }) 26 | 27 | it('should have nested property resource', () => { 28 | expect(productResource.property).toBeDefined() 29 | }) 30 | 31 | it('should have nested action resource', () => { 32 | expect(productResource.action).toBeDefined() 33 | }) 34 | }) 35 | 36 | describe('access', () => { 37 | beforeEach(() => { 38 | product = new Product(dummyResource()) 39 | }) 40 | 41 | it('should have property resource', () => { 42 | expect(product.property).toBeDefined() 43 | }) 44 | 45 | it('should have action resource', () => { 46 | expect(product.action).toBeDefined() 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /src/entity/Collection.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Thng from './Thng' 3 | import Action from './Action' 4 | import Resource from '../resource/Resource' 5 | import mixin, { mixinResources } from '../util/mixin' 6 | 7 | const path = '/collections' 8 | const CollectionResources = mixinResources([ 9 | Thng, 10 | Action 11 | // Collection // Read explanation below. 12 | ]) 13 | 14 | /** 15 | * Represents a Collection entity object. Collection has nested Collections 16 | * sub-resources. The workaround for the circular dependency is to only add 17 | * the Collection resource mixin after the class definition. This is different 18 | * than baking it in the parent Class Expression mixin 19 | * (i.e. CollectionResources) as the method is attached to the Collection 20 | * prototype, rather than the extended Entity class. Though, given the JS 21 | * prototype chain, there is no difference for the end user. 22 | * 23 | * @extends Entity 24 | */ 25 | export default class Collection extends CollectionResources(Entity) { 26 | /** 27 | * Return simple resource factory for Collections. 28 | * 29 | * @static 30 | * @return {{collection: Function}} 31 | */ 32 | static resourceFactory () { 33 | return { 34 | collection (id) { 35 | // Explicitly add Collection resource mixin to nested resource. 36 | return Object.assign( 37 | Resource.factoryFor(Collection, path, CollectionResources).call(this, id), 38 | Collection.resourceFactory() 39 | ) 40 | } 41 | } 42 | } 43 | } 44 | 45 | // Explicitly add Collection resource mixin to Collection. 46 | mixin(Collection.resourceFactory())(Collection) 47 | -------------------------------------------------------------------------------- /test/integration/entity/applicationRedirector.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Application Redirector', () => { 6 | let scope, api 7 | 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should read the application Redirector', async () => { 14 | api 15 | .get('/projects/projectId/applications/applicationId/redirector') 16 | .reply(200, { rules: [] }) 17 | const res = await scope.project('projectId').application('applicationId').redirector().read() 18 | 19 | expect(res).to.be.an('object') 20 | expect(res.rules).to.be.an('array') 21 | }) 22 | 23 | it('should update the application redirector', async () => { 24 | const payload = { 25 | rules: [{ match: 'thng.name=test' }] 26 | } 27 | api.put('/projects/projectId/applications/applicationId/redirector').reply(200, payload) 28 | const res = await scope 29 | .project('projectId') 30 | .application('applicationId') 31 | .redirector() 32 | .update(payload) 33 | 34 | expect(res.rules).to.deep.equal(payload.rules) 35 | }) 36 | 37 | it('should delete the application redirector', async () => { 38 | api.delete('/projects/projectId/applications/applicationId/redirector').reply(204) 39 | const res = await scope 40 | .project('projectId') 41 | .application('applicationId') 42 | .redirector() 43 | .delete() 44 | 45 | expect(res).to.not.exist 46 | }) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /src/util/buildParams.js: -------------------------------------------------------------------------------- 1 | import isPlainObject from 'lodash-es/isPlainObject' 2 | 3 | const SPECIALS = ['&', '|', '!', '>', '<', '=', '~', '(', ')', ','] 4 | const SPECIALS_REGEXP = new RegExp(`[${SPECIALS.join('\\')}]`, 'g') 5 | const SPECIALS_ESCAPE = '\\$&' 6 | 7 | /** 8 | * Build url safe parameter string if an object provided. 9 | * 10 | * @export 11 | * @param {(Object | string)} [params] key-value object or final query string 12 | * @param {boolean} [useEncoding] whether to skip encoding 13 | * @returns {string} 14 | */ 15 | export default function buildParams (params = {}, useEncoding = true) { 16 | return isPlainObject(params) 17 | ? Object.entries(params).map(buildParam(useEncoding)).join('&') 18 | : params 19 | } 20 | 21 | /** 22 | * Returns reducer function that adds the encoded key-value params to 23 | * accumulator. 24 | * 25 | * @param {boolean} useEncoding 26 | * @returns {Function} 27 | */ 28 | function buildParam (useEncoding) { 29 | const encode = uriEncoder(useEncoding) 30 | return ([key, value]) => { 31 | return `${encode(key)}=${encode(buildParams(value))}` 32 | } 33 | } 34 | 35 | /** 36 | * Returns function that encodes values using encodeURIComponent. 37 | * 38 | * @param {boolean} useEncoding 39 | * @returns {Function} 40 | */ 41 | function uriEncoder (useEncoding) { 42 | return (value) => (useEncoding ? encodeURIComponent(value) : escapeSpecials(value)) 43 | } 44 | 45 | /** 46 | * Escape special characters in value with the backslash (\) character. 47 | * 48 | * @param {string} value 49 | * @returns {string} 50 | */ 51 | function escapeSpecials (value) { 52 | return value.replace(SPECIALS_REGEXP, SPECIALS_ESCAPE) 53 | } 54 | -------------------------------------------------------------------------------- /src/util/callback.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Apply error-first callback if available. 3 | * 4 | * @param {function} callback - Error-first callback 5 | * @returns {function} - Response handler function 6 | */ 7 | export function success (callback) { 8 | return (response) => { 9 | if (callback) callback(null, response) 10 | return response 11 | } 12 | } 13 | 14 | /** 15 | * Apply error-first callback with error if available. 16 | * 17 | * @param {function} callback - Error-first callback 18 | * @returns {function} - Response handler function 19 | */ 20 | export function failure (callback) { 21 | return (err) => { 22 | if (callback) callback(err) 23 | 24 | if (!err) { 25 | throw new Error('No error message available, err was:', err) 26 | } 27 | 28 | // Native Error? 29 | if (err.name && err.name.includes('Error')) { 30 | throw err 31 | } 32 | 33 | // If a Fetch API response, get the body text 34 | if (typeof err.ok !== 'undefined' && !err.ok) { 35 | // Must use 'then' instead of an async function here, or a client-side 36 | // 'catch' will pass a rejected Promise instead of an error. 37 | return err.text().then((body) => Promise.reject(body)) 38 | } 39 | 40 | // If it's text instead of an already-parsed JSON object 41 | if (typeof err === 'string') { 42 | try { 43 | err = JSON.parse(err) 44 | } catch (e) { 45 | // The error text is not JSON 46 | } 47 | } 48 | 49 | // Throw a native Error to play nicer with error handling/retry libraries 50 | // Bonus: no need to check both e.message and e.errors 51 | throw new Error(err.message || JSON.stringify(err)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/integration/entity/tasks.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Tasks', () => { 6 | let scope, api 7 | 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should create a task', async () => { 14 | const payload = { 15 | type: 'POPULATING', 16 | inputParameters: { 17 | type: 'FIXED_AMOUNT', 18 | generateThngs: true, 19 | generateRedirections: true, 20 | defaultRedirectUrl: 'https://google.com', 21 | thngTemplate: { name: 'E2E Test Thng' }, 22 | shortDomain: 'tn.gg', 23 | quantity: 1, 24 | shortIdTemplate: { type: 'THNG_ID' } 25 | } 26 | } 27 | api.post('/batches/batchId/tasks', payload).reply(202) 28 | const res = await scope.batch('batchId').task().create(payload, { fullResponse: true }) 29 | 30 | expect(typeof res).to.equal('object') 31 | expect(res.status).to.equal(202) 32 | }) 33 | 34 | it('should read all tasks', async () => { 35 | api.get('/batches/batchId/tasks').reply(200, [{ id: 'taskId' }]) 36 | const res = await scope.batch('batchId').task().read() 37 | 38 | expect(res).to.be.an('array') 39 | expect(res).to.have.length.gte(1) 40 | }) 41 | 42 | it('should read a task', async () => { 43 | api.get('/batches/batchId/tasks/taskId').reply(200, { id: 'taskId' }) 44 | const res = await scope.batch('batchId').task('taskId').read() 45 | 46 | expect(res).to.be.an('object') 47 | expect(res.id).to.equal('taskId') 48 | }) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /test/integration/entity/projects.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Projects', () => { 6 | let scope, api 7 | 8 | before(() => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should create a project', async () => { 14 | const payload = { name: 'Test Project' } 15 | api.post('/projects', payload).reply(201, payload) 16 | 17 | const res = await scope.project().create(payload) 18 | 19 | expect(res).to.be.an('object') 20 | expect(res.name).to.equal('Test Project') 21 | }) 22 | 23 | it('should read all projects', async () => { 24 | api.get('/projects').reply(200, [{ id: 'projectId' }]) 25 | const res = await scope.project().read() 26 | 27 | expect(res).to.be.an('array') 28 | expect(res).to.have.length.gte(1) 29 | }) 30 | 31 | it('should read a project', async () => { 32 | api.get('/projects/projectId').reply(200, { id: 'projectId' }) 33 | const res = await scope.project('projectId').read() 34 | 35 | expect(res).to.be.an('object') 36 | expect(res.id).to.be.a('string') 37 | }) 38 | 39 | it('should update a project', async () => { 40 | const payload = { tags: ['updated'] } 41 | api.put('/projects/projectId', payload).reply(200, payload) 42 | const res = await scope.project('projectId').update(payload) 43 | 44 | expect(res).to.be.an('object') 45 | expect(res.tags).to.deep.equal(['updated']) 46 | }) 47 | 48 | it('should delete a project', async () => { 49 | api.delete('/projects/projectId').reply(200) 50 | await scope.project('projectId').delete() 51 | }) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /test/integration/entity/roles.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Roles', () => { 6 | let scope, api 7 | 8 | before(() => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should read all roles', async () => { 14 | api.get('/roles').reply(200, [{ id: 'roleId' }]) 15 | const res = await scope.role().read() 16 | 17 | expect(res).to.be.an('array') 18 | expect(res).to.have.length.gte(1) 19 | }) 20 | 21 | if (scopeType === 'operator') { 22 | it('should create a role', async () => { 23 | const payload = { name: 'Test Role' } 24 | api.post('/roles', payload).reply(201, payload) 25 | const res = await scope.role().create(payload) 26 | 27 | expect(res).to.be.an('object') 28 | expect(res.name).to.equal('Test Role') 29 | }) 30 | 31 | it('should read a role', async () => { 32 | api.get('/roles/roleId').reply(200, { id: 'roleId' }) 33 | const res = await scope.role('roleId').read() 34 | 35 | expect(res).to.be.an('object') 36 | expect(res.id).to.equal('roleId') 37 | }) 38 | 39 | it('should update a role', async () => { 40 | const payload = { description: 'updated' } 41 | api.put('/roles/roleId', payload).reply(200, payload) 42 | const res = await scope.role('roleId').update(payload) 43 | 44 | expect(res).to.be.an('object') 45 | expect(res.description).to.equal(payload.description) 46 | }) 47 | 48 | it('should delete a role', async () => { 49 | api.delete('/roles/roleId').reply(200) 50 | await scope.role('roleId').delete() 51 | }) 52 | } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An interceptor implements a request and/or response handlers that are called 3 | * before and after each request, respectively. 4 | * 5 | * @typedef {Object} Interceptor 6 | * @property {Function} request - Function to run before the request is sent 7 | * @property {Function} response - Function to run after the response is received 8 | */ 9 | 10 | /** 11 | * Settings can be applied globally or for individual requests. 12 | * Available options are provided below: 13 | * 14 | * @typedef {Object} Settings 15 | * @property {string} apiUrl - API url of request 16 | * @property {string} url - Url relative to `apiUrl` 17 | * @property {string} method - HTTP Method of request 18 | * @property {string} apiKey - API Key to use with request 19 | * @property {boolean} fullResponse - Flags if request should remain unwrapped 20 | * @property {boolean} geolocation - Flags if action creation should use the Web 21 | * Geolocation API 22 | * @property {number} timeout - Timeout for request 23 | * @property {Object} headers - Headers to send with request 24 | * @property {Interceptor[]} interceptors - List of request/response interceptors 25 | * @property {string} defaultShortDomain - Default short domain to use for redirections 26 | */ 27 | 28 | /** 29 | * Default settings. Never change. 30 | * 31 | * @type {Settings} 32 | */ 33 | const defaultSettings = { 34 | apiUrl: 'https://api.evrythng.io/v2', 35 | apiKey: '', 36 | fullResponse: false, 37 | geolocation: true, 38 | timeout: 0, 39 | headers: { 40 | 'content-type': 'application/json' 41 | }, 42 | interceptors: [], 43 | defaultShortDomain: 'tn.gg', 44 | apiVersion: 2, 45 | region: 'us' 46 | } 47 | 48 | // Initialize settings with defaults. 49 | const settings = Object.assign({}, defaultSettings) 50 | 51 | export default settings 52 | -------------------------------------------------------------------------------- /test/integration/entity/thngs.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Thngs', () => { 6 | let scope, api 7 | 8 | before(() => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should create a Thng', async () => { 14 | const payload = { name: 'Test Thng' } 15 | api.post('/thngs', payload).reply(201, payload) 16 | const res = await scope.thng().create(payload) 17 | 18 | expect(res).to.be.an('object') 19 | expect(res.name).to.equal(payload.name) 20 | }) 21 | 22 | it('should read a Thng', async () => { 23 | api.get('/thngs/thngId').reply(200, { id: 'thngId' }) 24 | const res = await scope.thng('thngId').read() 25 | 26 | expect(res).to.be.an('object') 27 | expect(res.id).to.equal('thngId') 28 | }) 29 | 30 | it('should read all Thngs', async () => { 31 | api.get('/thngs').reply(200, [{ id: 'thngId' }]) 32 | const res = await scope.thng().read() 33 | 34 | expect(res).to.be.an('array') 35 | expect(res).to.have.length.gte(1) 36 | }) 37 | 38 | it('should update a Thng', async () => { 39 | const payload = { tags: ['updated'] } 40 | api.put('/thngs/thngId', payload).reply(200, payload) 41 | const res = await scope.thng('thngId').update(payload) 42 | 43 | expect(res).to.be.an('object') 44 | expect(res.tags).to.deep.equal(['updated']) 45 | }) 46 | 47 | if (['operator', 'trustedApp'].includes(scopeType)) { 48 | it('should delete a Thng', async () => { 49 | api.delete('/thngs/thngId').reply(200) 50 | const res = await scope.thng('thngId').delete() 51 | expect(res).to.not.exist 52 | }) 53 | } 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /test/integration/entity/places.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Places', () => { 6 | let scope, api 7 | 8 | before(() => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should read all places', async () => { 14 | api.get('/places').reply(200, [{ id: 'placeId' }]) 15 | const res = await scope.place().read() 16 | 17 | expect(res).to.be.an('array') 18 | expect(res).to.have.length.gte(0) 19 | }) 20 | 21 | if (['operator', 'trustedApp'].includes(scopeType)) { 22 | it('should create a place', async () => { 23 | const payload = { name: 'Test Place' } 24 | api.post('/places', payload).reply(201, payload) 25 | const res = await scope.place().create(payload) 26 | 27 | expect(res).to.be.an('object') 28 | expect(res.name).to.equal(payload.name) 29 | }) 30 | 31 | it('should read a place', async () => { 32 | api.get('/places/placeId').reply(200, { id: 'placeId' }) 33 | const res = await scope.place('placeId').read() 34 | 35 | expect(res).to.be.an('object') 36 | expect(res.id).to.equal('placeId') 37 | }) 38 | 39 | it('should update a place', async () => { 40 | const payload = { tags: ['updated'] } 41 | api.put('/places/placeId', payload).reply(200, payload) 42 | const res = await scope.place('placeId').update(payload) 43 | 44 | expect(res).to.be.an('object') 45 | expect(res.tags).to.deep.equal(['updated']) 46 | }) 47 | 48 | it('should delete a place', async () => { 49 | api.delete('/places/placeId').reply(200) 50 | await scope.place('placeId').delete() 51 | }) 52 | } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /test/integration/entity/products.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Products', () => { 6 | let scope, api 7 | 8 | before(() => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should create a product', async () => { 14 | const payload = { name: 'Test Product' } 15 | api.post('/products', payload).reply(201, payload) 16 | const res = await scope.product().create(payload) 17 | 18 | expect(res).to.be.an('object') 19 | expect(res.name).to.equal(payload.name) 20 | }) 21 | 22 | it('should read a product', async () => { 23 | api.get('/products/productId').reply(200, { id: 'productId' }) 24 | const res = await scope.product('productId').read() 25 | 26 | expect(res).to.be.an('object') 27 | expect(res.id).to.equal('productId') 28 | }) 29 | 30 | it('should read all products', async () => { 31 | api.get('/products').reply(200, [{ id: 'productId' }]) 32 | const res = await scope.product().read() 33 | 34 | expect(res).to.be.an('array') 35 | expect(res).to.have.length.gte(1) 36 | }) 37 | 38 | it('should update a product', async () => { 39 | const payload = { tags: ['updated'] } 40 | api.put('/products/productId', payload).reply(200, payload) 41 | const res = await scope.product('productId').update(payload) 42 | 43 | expect(res).to.be.an('object') 44 | expect(res.tags).to.deep.equal(['updated']) 45 | }) 46 | 47 | if (['operator', 'trustedApp'].includes(scopeType)) { 48 | it('should delete a product', async () => { 49 | api.delete('/products/productId').reply(200) 50 | await scope.product('productId').delete() 51 | }) 52 | } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /src/entity/ReactorLog.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | import isString from 'lodash-es/isString' 4 | 5 | const path = '/reactor/logs' 6 | 7 | /** 8 | * Represents a ReactorLog entity object. 9 | * 10 | * @extends Entity 11 | */ 12 | export default class ReactorLog extends Entity { 13 | static resourceFactory () { 14 | return { 15 | /** 16 | * ReactorLog Entity resource. If using with Operator, app and project 17 | * details are required. 18 | * 19 | * @param {string} projectId - Project ID, if using with Operator scope. 20 | * @param {string} applicationId - Application ID, if using with Operator scope. 21 | * @returns {Object} 22 | */ 23 | reactorLog (projectId, applicationId) { 24 | if (isString(projectId) && !isString(applicationId)) { 25 | throw new Error('When using with Operator, projectId and applicationId are required') 26 | } 27 | 28 | const useProjectId = projectId || this.project 29 | const useApplicationId = applicationId || this.app 30 | 31 | // Find the path to this application 32 | const appPath = `/projects/${useProjectId}/applications/${useApplicationId}` 33 | 34 | return Object.assign(Resource.factoryFor(ReactorLog, appPath + path).call(this), { 35 | create (...args) { 36 | return createLogs.call(this, ...args) 37 | } 38 | }) 39 | } 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * Use bulk endpoint when creating array of logs. 46 | * 47 | * @param {Object} data - Logs payload. 48 | * @param {Object} options - Optional request options. 49 | * @return {Promise} 50 | */ 51 | function createLogs (data, options = {}) { 52 | const logs = Array.isArray(data) ? data : [data] 53 | 54 | // Change URL for bulk creation 55 | options.url = `${this.path}/bulk` 56 | 57 | return Resource.prototype.create.call(this, logs, options) 58 | } 59 | -------------------------------------------------------------------------------- /test/integration/entity/collections.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Collections', () => { 6 | let scope, api 7 | 8 | before(() => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should create a collection', async () => { 14 | const payload = { name: 'Test Collection' } 15 | api.post('/collections', payload).reply(200, payload) 16 | const res = await scope.collection().create(payload) 17 | 18 | expect(res).to.be.an('object') 19 | expect(res.name).to.equal('Test Collection') 20 | }) 21 | 22 | it('should read a collection', async () => { 23 | api.get('/collections/collectionId').reply(200, { id: 'collectionId' }) 24 | const res = await scope.collection('collectionId').read() 25 | 26 | expect(res).to.be.an('object') 27 | expect(res.id).to.equal('collectionId') 28 | }) 29 | 30 | it('should read all collections', async () => { 31 | api.get('/collections').reply(200, [{ id: 'collectionId' }]) 32 | const res = await scope.collection().read() 33 | 34 | expect(res).to.be.an('array') 35 | expect(res).to.have.length.gte(1) 36 | }) 37 | 38 | it('should update a collection', async () => { 39 | const payload = { tags: ['updated'] } 40 | api.put('/collections/collectionId').reply(200, { tags: ['updated'] }) 41 | const res = await scope.collection('collectionId').update(payload) 42 | 43 | expect(res).to.be.an('object') 44 | expect(res.tags).to.deep.equal(['updated']) 45 | }) 46 | 47 | if (['operator', 'trustedApp'].includes(scopeType)) { 48 | it('should delete a collection', async () => { 49 | api.delete('/collections/collectionId').reply(200) 50 | const res = await scope.collection('collectionId').delete() 51 | 52 | expect(res).to.not.exist 53 | }) 54 | } 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /test/unit/outDated/scope/Scope.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Scope from '../../../src/scope/Scope' 3 | import symbols from '../../../src/symbols' 4 | import fetchMock from 'fetch-mock' 5 | import mockApi from '../../helpers/apiMock' 6 | import apiUrl from '../../helpers/apiUrl' 7 | import paths from '../../helpers/paths' 8 | import { apiKey, operatorTemplate } from '../../helpers/data' 9 | 10 | let scope 11 | 12 | describe('Scope', () => { 13 | mockApi() 14 | 15 | describe('constructor', () => { 16 | describe('invalid', () => { 17 | it('should need an API key', () => { 18 | const badCall = () => new Scope() 19 | expect(badCall).toThrow() 20 | }) 21 | 22 | it('should need a String API Key', () => { 23 | const badCall = () => new Scope({ apiKey }) 24 | expect(badCall).toThrow() 25 | }) 26 | }) 27 | 28 | describe('valid', () => { 29 | beforeEach(() => { 30 | scope = new Scope(apiKey, operatorTemplate) 31 | }) 32 | 33 | it('should have API Key', () => { 34 | expect(scope.apiKey).toEqual(apiKey) 35 | }) 36 | 37 | it('should extend document with any pre-provided data', () => { 38 | expect(scope.id).toEqual(operatorTemplate.id) 39 | expect(scope.email).toEqual(operatorTemplate.email) 40 | }) 41 | 42 | it('should expose init promise', () => { 43 | expect(scope[symbols.init]).toBeDefined() 44 | expect(scope[symbols.init] instanceof Promise).toBe(true) 45 | }) 46 | 47 | it('should fetch scope access using scope apiKey', (done) => { 48 | scope[symbols.init].then(() => { 49 | expect(fetchMock.lastUrl()).toEqual(apiUrl(paths.access)) 50 | expect(fetchMock.lastOptions()).toEqual( 51 | jasmine.objectContaining({ 52 | headers: jasmine.objectContaining({ authorization: apiKey }) 53 | }) 54 | ) 55 | done() 56 | }) 57 | }) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /test/integration/entity/files.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const nock = require('nock') 3 | const { getScope, mockApi } = require('../util') 4 | 5 | module.exports = (scopeType, apiUrl) => { 6 | describe('Files', () => { 7 | let scope, api 8 | 9 | before(() => { 10 | scope = getScope(scopeType) 11 | api = mockApi(apiUrl) 12 | }) 13 | it('should create a file', async () => { 14 | const payload = { name: 'TestFile.txt' } 15 | api.post('/files', payload).reply(201, payload) 16 | const res = await scope.file().create(payload) 17 | 18 | expect(res).to.be.an('object') 19 | expect(res.name).to.equal(payload.name) 20 | }) 21 | 22 | it('should read all files', async () => { 23 | api.get('/files').reply(200, [{ id: 'fileId' }]) 24 | const res = await scope.file().read() 25 | 26 | expect(res).to.be.an('array') 27 | expect(res).to.have.length.gte(1) 28 | }) 29 | 30 | it('should read a file', async () => { 31 | api.get('/files/fileId').reply(200, { id: 'fileId' }) 32 | const res = await scope.file('fileId').read() 33 | 34 | expect(res).to.be.an('object') 35 | expect(res.id).to.equal('fileId') 36 | }) 37 | 38 | it.skip('should upload file content - text', async () => { 39 | const fileData = 'This is example text file content' 40 | 41 | api.get('/files/fileId').reply(200, { id: 'fileId', uploadUrl: 'https://s3.amazonaws.com' }) 42 | await scope.file('fileId').upload(fileData) 43 | 44 | api.get('/files/fileId').reply(200, { contentUrl: 'https://s3.amazonaws.com/upload' }) 45 | const resource = await scope.file('fileId').read() 46 | nock('https://s3.amazonaws.com').get('/upload').reply(200, fileData) 47 | const readData = await fetch(resource.contentUrl).then((res) => res.text()) 48 | 49 | expect(readData).to.equal(fileData) 50 | }) 51 | 52 | it('should delete a file', async () => { 53 | api.delete('/files/fileId').reply(200) 54 | await scope.file('fileId').delete() 55 | }) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /test/integration/misc/alias.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | const evrythng = require('../../../') 4 | 5 | module.exports = (scopeType, url) => { 6 | describe('alias', () => { 7 | let scope, api 8 | 9 | before(() => { 10 | scope = getScope(scopeType) 11 | api = mockApi(url) 12 | }) 13 | 14 | it("should alias 'product' to 'sku' for Operator scope", async () => { 15 | evrythng.alias({ product: 'sku' }, 'Operator') 16 | 17 | expect(scope.sku).to.be.a('function') 18 | }) 19 | 20 | it("should use the alias to read all 'sku's", async () => { 21 | api.get('/products').reply(200, [{ id: 'productId' }]) 22 | const skus = await scope.sku().read() 23 | 24 | expect(skus).to.be.an('array') 25 | expect(skus).to.have.length.gte(0) 26 | }) 27 | 28 | it("should use the alias to create a 'sku'", async () => { 29 | const payload = { name: 'Example SKU' } 30 | api.post('/products', payload).reply(200, { id: 'productId' }) 31 | const res = await scope.sku().create(payload) 32 | 33 | expect(res).to.be.an('object') 34 | expect(res.id).to.be.a('string') 35 | }) 36 | 37 | it("should use the alias to read a 'sku'", async () => { 38 | api.get('/products/productId').reply(200, { id: 'productId' }) 39 | const res = await scope.sku('productId').read() 40 | 41 | expect(res).to.be.an('object') 42 | expect(res.id).to.equal('productId') 43 | }) 44 | 45 | it("should use the alias to update a 'sku'", async () => { 46 | const payload = { tags: ['updated'] } 47 | api.put('/products/productId', payload).reply(200, { id: 'productId' }) 48 | const res = await scope.sku('productId').update(payload) 49 | 50 | expect(res).to.be.an('object') 51 | expect(res.id).to.equal('productId') 52 | }) 53 | 54 | it("should use the alias to delete a 'sku'", async () => { 55 | api.delete('/products/productId').reply(200) 56 | await scope.sku('productId').delete() 57 | }) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /test/integration/entity/applications.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Applications', () => { 6 | let scope, api 7 | 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should create an application', async () => { 14 | const payload = { name: 'Application Name', socialNetworks: {} } 15 | api.post('/projects/projectId/applications').reply(201, payload) 16 | const res = await scope.project('projectId').application().create(payload) 17 | 18 | expect(res).to.be.an('object') 19 | expect(res.name).to.be.a('string') 20 | }) 21 | 22 | it('should read all applications', async () => { 23 | api.get('/projects/projectId/applications').reply(200, [{ id: 'applicationId' }]) 24 | const res = await scope.project('projectId').application().read() 25 | 26 | expect(res).to.be.an('array') 27 | expect(res).to.have.length.gte(1) 28 | }) 29 | 30 | it('should read an application', async () => { 31 | api.get('/projects/projectId/applications/applicationId').reply(200, { id: 'applicationId' }) 32 | const res = await scope.project('projectId').application('applicationId').read() 33 | 34 | expect(res).to.be.an('object') 35 | expect(res.id).to.equal('applicationId') 36 | }) 37 | 38 | it('should update an application', async () => { 39 | const payload = { tags: ['updated'] } 40 | api.put('/projects/projectId/applications/applicationId').reply(200, payload) 41 | const res = await scope.project('projectId').application('applicationId').update(payload) 42 | 43 | expect(res).to.be.an('object') 44 | expect(res.tags).to.deep.equal(payload.tags) 45 | }) 46 | 47 | it('should delete an application', async () => { 48 | api.delete('/projects/projectId/applications/applicationId').reply(200) 49 | await scope.project('projectId').application('applicationId').delete() 50 | }) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /test/integration/entity/adiOrders.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('ADI Orders', () => { 6 | let scope, api 7 | 8 | before(() => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should create an ADI Order', async () => { 14 | const payload = { 15 | ids: ['serial1', 'serial2'], 16 | purchaseOrder: '234567890', 17 | metadata: { 18 | identifierKey: 'gs1:21', 19 | customFields: { factory: '0400321' }, 20 | tags: ['factory:0400321'], 21 | shortDomain: 'tn.gg', 22 | defaultRedirectUrl: 'https://evrythng.com?id={shortId}' 23 | }, 24 | identifiers: { internalId: 'X7JF' }, 25 | tags: ['X7JF'] 26 | } 27 | 28 | api.post('/adis/orders', payload).reply(201, { id: 'UEp4rDGsnpCAF6xABbys5Amc' }) 29 | const res = await scope.adiOrder().create(payload) 30 | 31 | expect(res).to.be.an('object') 32 | expect(res.id).to.have.length(24) 33 | }) 34 | 35 | it('should read an ADI Order by ID', async () => { 36 | api.get('/adis/orders/adiOrderId').reply(200, { id: 'adiOrderId' }) 37 | const res = await scope.adiOrder('adiOrderId').read() 38 | 39 | expect(res).to.be.an('object') 40 | expect(res.id).to.be.a('string') 41 | }) 42 | 43 | it('should create an ADI Order event', async () => { 44 | const payload = { 45 | metadata: { 46 | type: 'encodings', 47 | tags: ['example'] 48 | }, 49 | ids: ['serial1', 'serial2'], 50 | customFields: { internalId: 'X7JF' }, 51 | tags: ['X7JF'] 52 | } 53 | 54 | api 55 | .post('/adis/orders/adiOrderId/events', payload) 56 | .reply(201, { id: 'UrCPgMhMMmPEY6awwEf6gKfb' }) 57 | const res = await scope.adiOrder('adiOrderId').event().create(payload) 58 | 59 | expect(res).to.be.an('object') 60 | expect(res.id).to.be.a('string') 61 | }) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /test/integration/entity/actionTypes.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Action Types', () => { 6 | let scope, api 7 | 8 | before(() => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should read all action types', async () => { 14 | api.get('/actions').reply(200, [{ name: '_CustomType' }]) 15 | const res = await scope.actionType().read() 16 | 17 | expect(res).to.be.an('array') 18 | expect(res).to.have.length.gte(1) 19 | }) 20 | 21 | if (['operator', 'trustedApp'].includes(scopeType)) { 22 | it('should create an action type', async () => { 23 | const payload = { name: '_CustomType' } 24 | api.post('/actions', payload).reply(201, { name: '_CustomType' }) 25 | const res = await scope.actionType().create(payload) 26 | 27 | expect(res).to.be.an('object') 28 | expect(res.name).to.equal(payload.name) 29 | }) 30 | 31 | it('should read an action type', async () => { 32 | api.get('/actions?filter=name%3D_CustomType').reply(200, [{ name: '_CustomType' }]) 33 | const res = await scope.actionType('_CustomType').read() 34 | 35 | expect(res).to.be.an('object') 36 | expect(res.name).to.equal('_CustomType') 37 | }) 38 | } 39 | 40 | if (scopeType === 'operator') { 41 | it('should update an action type', async () => { 42 | const payload = { tags: ['updated'] } 43 | api.put('/actions/_CustomType', payload).reply(200, payload) 44 | const res = await scope.actionType('_CustomType').update(payload) 45 | 46 | expect(res).to.be.an('object') 47 | expect(res.tags).to.deep.equal(payload.tags) 48 | }) 49 | } 50 | 51 | if (['operator', 'trustedApp'].includes(scopeType)) { 52 | it('should delete an actionType', async () => { 53 | api.delete('/actions/_CustomType').reply(200) 54 | await scope.actionType('_CustomType').delete() 55 | }) 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /src/scope/AccessToken.js: -------------------------------------------------------------------------------- 1 | import Scope from './Scope' 2 | import Account from '../entity/Account' 3 | import ADIOrder from '../entity/ADIOrder' 4 | import Product from '../entity/Product' 5 | import Thng from '../entity/Thng' 6 | import Collection from '../entity/Collection' 7 | import Action from '../entity/Action' 8 | import ActionType from '../entity/ActionType' 9 | import Project from '../entity/Project' 10 | import PurchaseOrder from '../entity/PurchaseOrder' 11 | import Redirector from '../entity/Redirector' 12 | import ShipmentNotice from '../entity/ShipmentNotice' 13 | import Place from '../entity/Place' 14 | import File from '../entity/File' 15 | import AccessPolicy from '../entity/AccessPolicy' 16 | import AccessTokenEntity from '../entity/AccessToken' 17 | import Me from '../entity/Me' 18 | import { mixinResources } from '../util/mixin' 19 | 20 | /** 21 | * Mixin with all AccessToken resources. 22 | * 23 | * @mixin 24 | */ 25 | const AccessTokenResources = mixinResources([ 26 | AccessPolicy, 27 | AccessTokenEntity, 28 | Account, 29 | Action, 30 | ActionType, 31 | ADIOrder, 32 | Collection, 33 | File, 34 | Me, 35 | Place, 36 | Product, 37 | Project, 38 | PurchaseOrder, 39 | Redirector, 40 | ShipmentNotice, 41 | Thng 42 | ]) 43 | 44 | /** 45 | * AccessToken is a Scope type for the v2 API with the potential to manage any account resources, depending on the API key's 46 | * permissions. It represents an API access for a specific purpose, instead of a type of Actor, such as an Operator. 47 | * 48 | * @extends Scope 49 | * @mixes AccessTokenResources 50 | */ 51 | export default class AccessToken extends AccessTokenResources(Scope) { 52 | /** 53 | * Creates an instance of AccessToken. 54 | * 55 | * @param {string} apiKey - API Key of scope 56 | * @param {Object} [data={}] - Optional data 57 | */ 58 | constructor (apiKey, data = {}) { 59 | super(apiKey, data) 60 | 61 | this.initPromise = super.readAccess() 62 | } 63 | 64 | /** 65 | * Read the access token's data asynchronously. 66 | * 67 | * @returns {Promise} 68 | */ 69 | init () { 70 | return this.initPromise 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/integration/entity/properties.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, targetType, url) => { 5 | let scope, api 6 | 7 | describe(`Properties (${targetType})`, () => { 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it(`should create a ${targetType} property`, async () => { 14 | const payload = { key: 'temp_c', value: 42 } 15 | api.post(`/${targetType}s/targetId/properties`).reply(200, [payload]) 16 | const res = await scope[targetType]('targetId').property().create(payload) 17 | 18 | expect(res).to.be.an('array') 19 | expect(res).to.have.length(1) 20 | }) 21 | 22 | it(`should read all ${targetType} properties`, async () => { 23 | api.get(`/${targetType}s/targetId/properties`).reply(200, [{ key: 'temp_c', value: 42 }]) 24 | const res = await scope[targetType]('targetId').property().read() 25 | 26 | expect(res).to.be.an('array') 27 | expect(res).to.have.length(1) 28 | }) 29 | 30 | it(`should read a single ${targetType} property`, async () => { 31 | api.get(`/${targetType}s/targetId/properties/temp_c`).reply(200, [{ value: 42 }]) 32 | const res = await scope[targetType]('targetId').property('temp_c').read() 33 | 34 | expect(res).to.be.an('array') 35 | expect(res).to.have.length(1) 36 | }) 37 | 38 | it(`should update a single ${targetType} property`, async () => { 39 | api 40 | .put(`/${targetType}s/targetId/properties/temp_c`, [{ value: 43 }]) 41 | .reply(200, [{ value: 43 }]) 42 | const res = await scope[targetType]('targetId').property('temp_c').update(43) 43 | 44 | expect(res).to.be.an('array') 45 | expect(res[0].value).to.equal(43) 46 | }) 47 | 48 | if (scopeType !== 'anonUser') { 49 | it(`should delete a single ${targetType} property`, async () => { 50 | api.delete(`/${targetType}s/targetId/properties/temp_c`).reply(200) 51 | await scope[targetType]('targetId').property('temp_c').delete() 52 | }) 53 | } 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /test/integration/entity/shipmentNotice.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | const noticePayload = { 5 | asnId: '2343689643', 6 | version: '1', 7 | issueDate: '2019-06-19T16:39:57-08:00', 8 | transportation: 'Expedited Freight', 9 | parties: [ 10 | { 11 | id: 'gs1:414:01251', 12 | type: 'ship-from' 13 | }, 14 | { 15 | name: 'The Landmark, Shop No. G14', 16 | type: 'ship-to', 17 | address: { 18 | street: '113-114, Central', 19 | city: 'Hong Kong' 20 | } 21 | } 22 | ], 23 | tags: ['ongoing'] 24 | } 25 | 26 | module.exports = (scopeType, url) => { 27 | describe('Shipment Notices', () => { 28 | let scope, api 29 | 30 | before(() => { 31 | scope = getScope(scopeType) 32 | api = mockApi(url) 33 | }) 34 | 35 | it('should create a shipment notice', async () => { 36 | api.post('/shipmentNotices', noticePayload).reply(201, noticePayload) 37 | 38 | const res = await scope.shipmentNotice().create(noticePayload) 39 | 40 | expect(res).to.be.an('object') 41 | expect(res.asnId).to.equal(noticePayload.asnId) 42 | }) 43 | 44 | it('should read a shipment notice', async () => { 45 | api.get('/shipmentNotices/shipmentNoticeId').reply(200, noticePayload) 46 | 47 | const res = await scope.shipmentNotice('shipmentNoticeId').read() 48 | 49 | expect(res).to.be.an('object') 50 | expect(res.asnId).to.equal(noticePayload.asnId) 51 | }) 52 | 53 | it('should update a shipment notice', async () => { 54 | api.put('/shipmentNotices/shipmentNoticeId').reply(200, noticePayload) 55 | 56 | const res = await scope.shipmentNotice('shipmentNoticeId').update(noticePayload) 57 | 58 | expect(res).to.be.an('object') 59 | expect(res.tags).to.deep.equal(noticePayload.tags) 60 | }) 61 | 62 | it('should delete a shipment notice', async () => { 63 | api.delete('/shipmentNotices/shipmentNoticeId').reply(204) 64 | 65 | const res = await scope.shipmentNotice('shipmentNoticeId').delete() 66 | 67 | expect(res).to.not.exist 68 | }) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /src/entity/Entity.js: -------------------------------------------------------------------------------- 1 | import Resource from '../resource/Resource' 2 | import symbols from '../symbols' 3 | 4 | /** 5 | * Entity is the base class for all types of entities in the EVRYTHNG API. 6 | * An Entity knows how to update and delete itself given that a resource is 7 | * provided. 8 | */ 9 | export default class Entity { 10 | /** 11 | * Creates an new entity of given Resource. Optionally can be initialized 12 | * with pre-defined data. 13 | * 14 | * @param {Resource} resource - Resource owner of this entity. 15 | * @param {Object} [body] Optional entity data 16 | */ 17 | constructor (resource, body = {}) { 18 | if (!(resource && resource instanceof Resource)) { 19 | throw new Error('Resource must be a Resource.') 20 | } 21 | 22 | // Define non-enumerable unique resource property so it's not copied over 23 | // in shallow copies of this (e.g. using Object.assign). 24 | this[symbols.resource] = resource 25 | 26 | // Extend Entity with data. 27 | Object.assign(this, body) 28 | } 29 | 30 | /** 31 | * Returns all enumerable properties. 32 | * 33 | * @returns {Object} 34 | */ 35 | json () { 36 | return Object.entries(this).reduce((ret, [k, v]) => Object.assign(ret, { [k]: v }), {}) 37 | } 38 | 39 | /** 40 | * Update itself by calling the update method of the owning resource and 41 | * passing the JSON representation of itself or the given body object. 42 | * 43 | * @param {Object} body - optional body, use self as default 44 | * @param {Function} callback - error-first callback 45 | * @returns {Promise.} 46 | */ 47 | update (body = this.json(), callback) { 48 | return this[symbols.resource].update(body, callback).then((updated) => { 49 | // Update self and keep chaining with API response. 50 | Object.assign(this, updated) 51 | return updated 52 | }) 53 | } 54 | 55 | /** 56 | * Delete itself by calling the delete method of the owning resource. 57 | * 58 | * @param {Function} callback - error-first callback 59 | * @returns {Promise.} 60 | */ 61 | ['delete'] (callback) { 62 | return this[symbols.resource].delete(callback) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/entities.js: -------------------------------------------------------------------------------- 1 | import Product from './entity/Product' 2 | import Thng from './entity/Thng' 3 | import Collection from './entity/Collection' 4 | import Property from './entity/Property' 5 | import Action from './entity/Action' 6 | import ActionType from './entity/ActionType' 7 | import Application from './entity/Application' 8 | import User from './entity/User' 9 | import Batch from './entity/Batch' 10 | import Location from './entity/Location' 11 | import Permission from './entity/Permission' 12 | import Project from './entity/Project' 13 | import Role from './entity/Role' 14 | import Task from './entity/Task' 15 | import OperatorAccess from './entity/OperatorAccess' 16 | import AccessPolicy from './entity/AccessPolicy' 17 | import AccessTokens from './entity/AccessToken' 18 | import Me from './entity/Me' 19 | import Access from './entity/Access' 20 | import ADIOrder from './entity/ADIOrder' 21 | import ADIOrderEvent from './entity/ADIOrderEvent' 22 | import Account from './entity/Account' 23 | import CommissionState from './entity/CommissionState' 24 | import Domain from './entity/Domain' 25 | import Place from './entity/Place' 26 | import PurchaseOrder from './entity/PurchaseOrder' 27 | import ReactorLog from './entity/ReactorLog' 28 | import ReactorSchedule from './entity/ReactorSchedule' 29 | import ReactorScript from './entity/ReactorScript' 30 | import Redirection from './entity/Redirection' 31 | import Redirector from './entity/Redirector' 32 | import ShipmentNotice from './entity/ShipmentNotice' 33 | import ShortDomain from './entity/ShortDomain' 34 | import File from './entity/File' 35 | 36 | export default { 37 | Access, 38 | AccessPolicy, 39 | AccessTokens, 40 | Account, 41 | Action, 42 | ActionType, 43 | ADIOrder, 44 | ADIOrderEvent, 45 | Application, 46 | Batch, 47 | Collection, 48 | CommissionState, 49 | Domain, 50 | File, 51 | Location, 52 | Me, 53 | OperatorAccess, 54 | Permission, 55 | Place, 56 | Product, 57 | Project, 58 | Property, 59 | PurchaseOrder, 60 | Role, 61 | ReactorLog, 62 | ReactorSchedule, 63 | ReactorScript, 64 | Redirection, 65 | Redirector, 66 | ShipmentNotice, 67 | ShortDomain, 68 | Task, 69 | Thng, 70 | User 71 | } 72 | -------------------------------------------------------------------------------- /test/integration/entity/accessTokens.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | const payload = { 5 | name: 'accessTokens', 6 | description: 'create accessTokens', 7 | policies: ['UPb7E6shapktcaaabfahfpds'], 8 | conditions: [], 9 | tags: ['operatorAccess'], 10 | identifiers: {}, 11 | customFields: {} 12 | } 13 | 14 | module.exports = (scopeType, settings) => { 15 | describe('AccessTokens', () => { 16 | let scope, api 17 | 18 | before(async () => { 19 | scope = getScope(scopeType) 20 | api = mockApi(settings.apiUrl) 21 | }) 22 | 23 | if (settings.apiVersion == 2) { 24 | it('should create access token', async () => { 25 | api.post('/accessTokens', payload).reply(201, { id: 'accessTokenId' }) 26 | const res = await scope.accessToken().create(payload) 27 | 28 | expect(res).to.be.an('object') 29 | expect(res.id).to.be.a('string') 30 | }) 31 | 32 | it('should read access token by id', async () => { 33 | api.get('/accessTokens/accessTokenId').reply(200, { id: 'accessTokenId' }) 34 | const res = await scope.accessToken('accessTokenId').read() 35 | 36 | expect(res).to.be.an('object') 37 | expect(res.id).to.be.a('string') 38 | }) 39 | 40 | it('should read all accessTokens', async () => { 41 | api.get('/accessTokens').reply(200, [payload]) 42 | const res = await scope.accessToken().read() 43 | 44 | expect(res).to.be.an('array') 45 | expect(res).to.have.length.gte(1) 46 | }) 47 | 48 | it('should update accessToken', async () => { 49 | api.put('/accessTokens/accessTokenId', payload).reply(200, payload) 50 | 51 | const res = await scope.accessToken('accessTokenId').update(payload) 52 | 53 | expect(res).to.be.an('object') 54 | expect(res.name).to.deep.equal(payload.name) 55 | }) 56 | 57 | it('should delete access policy', async () => { 58 | api.delete('/accessTokens/accessTokenId').reply(204) 59 | const res = await scope.accessToken('accessTokenId').delete() 60 | 61 | expect(res).to.not.exist 62 | }) 63 | } 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /test/integration/entity/actions.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | let scope, api 6 | 7 | describe('Actions', () => { 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should create an action', async () => { 14 | const payload = { type: 'scans', thng: 'thngId', tags: ['foo'] } 15 | api.post('/actions/scans', payload).reply(201, { id: 'actionId' }) 16 | const res = await scope.action('scans').create(payload) 17 | 18 | expect(res).to.be.an('object') 19 | expect(res.id).to.be.a('string') 20 | }) 21 | 22 | it('should read all actions of a type', async () => { 23 | api.get('/actions/scans').reply(200, [{ id: 'actionId' }]) 24 | const res = await scope.action('scans').read() 25 | 26 | expect(res).to.be.an('array') 27 | expect(res).to.have.length.gte(0) 28 | }) 29 | 30 | it('should create an aliased action', async () => { 31 | const payload = { type: 'scans' } 32 | api.post('/thngs/thngId/actions/scans', payload).reply(201, { id: 'actionId' }) 33 | const res = await scope.thng('thngId').action('scans').create(payload) 34 | 35 | expect(res).to.be.an('object') 36 | expect(res.id).to.be.a('string') 37 | }) 38 | 39 | it('should read all aliased actions', async () => { 40 | api.get('/thngs/thngId/actions/scans').reply(200, [{ id: 'actionId' }]) 41 | const res = await scope.thng('thngId').action('scans').read() 42 | 43 | expect(res).to.be.an('array') 44 | expect(res).to.have.length.gte(1) 45 | }) 46 | 47 | if (scopeType === 'operator') { 48 | it('should read a single action', async () => { 49 | api.get('/actions/scans/actionId').reply(200, { id: 'actionId' }) 50 | const res = await scope.action('scans', 'actionId').read() 51 | 52 | expect(res).to.be.an('object') 53 | expect(res.id).to.be.a('string') 54 | }) 55 | 56 | it('should delete an action', async () => { 57 | api.delete('/actions/scans/actionId').reply(200) 58 | await scope.action('scans', 'actionId').delete() 59 | }) 60 | } 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /test/integration/entity/accessPolicies.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | const payload = { 5 | name: 'custom policy', 6 | description: 'Create custom policy', 7 | policies: ['UPb7E6shapktcaaabfahfpds'], 8 | conditions: [], 9 | operator: 'UtnGTkaPDphkYDC9F2KHBtPp', 10 | tags: ['operatorAccess'], 11 | identifiers: {}, 12 | customFields: {} 13 | } 14 | 15 | module.exports = (scopeType, settings) => { 16 | describe('AccessPolicies', () => { 17 | let scope, api 18 | 19 | before(async () => { 20 | scope = getScope(scopeType) 21 | api = mockApi(settings.apiUrl) 22 | }) 23 | 24 | if (settings.apiVersion == 2) { 25 | it('should create access policy', async () => { 26 | api.post('/accessPolicies', payload).reply(201, { id: 'accessPolicyId' }) 27 | const res = await scope.accessPolicy().create(payload) 28 | 29 | expect(res).to.be.an('object') 30 | expect(res.id).to.be.a('string') 31 | }) 32 | 33 | it('should read access policy by id', async () => { 34 | api.get('/accessPolicies/accessPolicyId').reply(200, { id: 'accessPolicyId' }) 35 | const res = await scope.accessPolicy('accessPolicyId').read() 36 | 37 | expect(res).to.be.an('object') 38 | expect(res.id).to.be.a('string') 39 | }) 40 | 41 | it('should read all access policies', async () => { 42 | api.get('/accessPolicies').reply(200, [payload]) 43 | const res = await scope.accessPolicy().read() 44 | 45 | expect(res).to.be.an('array') 46 | expect(res).to.have.length.gte(1) 47 | }) 48 | 49 | it('should update access policy', async () => { 50 | api.put('/accessPolicies/accessPolicyId', payload).reply(200, payload) 51 | 52 | const res = await scope.accessPolicy('accessPolicyId').update(payload) 53 | 54 | expect(res).to.be.an('object') 55 | expect(res.name).to.deep.equal(payload.name) 56 | }) 57 | 58 | it('should delete access policy', async () => { 59 | api.delete('/accessPolicies/accessPolicyId').reply(204) 60 | const res = await scope.accessPolicy('accessPolicyId').delete() 61 | 62 | expect(res).to.not.exist 63 | }) 64 | } 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /src/scope/Scope.js: -------------------------------------------------------------------------------- 1 | import isString from 'lodash-es/isString' 2 | import api from '../api' 3 | import symbols from '../symbols' 4 | 5 | /** 6 | * Scope defines the context in which API calls are made. A scope is defined 7 | * by its API Key. That key is sent in each request's Authorization header that 8 | * uses this scope. 9 | */ 10 | export default class Scope { 11 | /** 12 | * Creates an instance of Scope. 13 | * 14 | * @param {string} apiKey API Key of scope 15 | * @param {Object} [data={}] Optional scope data 16 | */ 17 | constructor (apiKey, data = {}) { 18 | if (!isString(apiKey)) { 19 | throw new Error('Scope constructor should be called with an API Key') 20 | } 21 | 22 | this.apiKey = apiKey 23 | 24 | // Extend scope with any given details. 25 | Object.assign(this, data) 26 | } 27 | 28 | /** 29 | * Read the scope's access data asynchronously. 30 | * 31 | * @returns {Promise} 32 | */ 33 | readAccess () { 34 | return api({ 35 | url: '/access', 36 | apiKey: this.apiKey 37 | }) 38 | } 39 | 40 | /** 41 | * Read itself and extend scope document. 42 | * 43 | * @param {Settings} [options={}] - Optional API request options 44 | * @returns {Promise} - Updated operator scope 45 | */ 46 | read (options = {}) { 47 | const opts = Object.assign(options, { 48 | method: 'get', 49 | url: this[symbols.path], 50 | apiKey: this.apiKey 51 | }) 52 | 53 | return this._request(opts) 54 | } 55 | 56 | /** 57 | * Update self and extend scope document. 58 | * 59 | * @param {Object} data - Operator data 60 | * @param {Settings} [options={}] - Optional API request options 61 | * @returns {Promise} - Updated operator scope 62 | */ 63 | update (data, options = {}) { 64 | const opts = Object.assign(options, { 65 | method: 'put', 66 | url: this[symbols.path], 67 | apiKey: this.apiKey, 68 | data 69 | }) 70 | 71 | return this._request(opts) 72 | } 73 | 74 | // Private 75 | 76 | /** 77 | * 78 | * @param {Settings} options - Request options 79 | * @returns {Promise} - Updated operator scope 80 | * @private 81 | */ 82 | _request (options) { 83 | return api(options).then((data) => Object.assign(this, data)) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/Collection.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import Collection from '../../../src/entity/Collection' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyScope, dummyResource } from '../../helpers/dummy' 7 | import { collectionTemplate } from '../../helpers/data' 8 | 9 | let collectionResource 10 | let collection 11 | const collectionPath = `${paths.collections}/${collectionTemplate.id}` 12 | 13 | describe('Collection', () => { 14 | mockApi() 15 | 16 | describe('resourceFactory', () => { 17 | beforeEach(() => { 18 | const scope = Object.assign(dummyScope(), Collection.resourceFactory()) 19 | collectionResource = scope.collection(collectionTemplate.id) 20 | }) 21 | 22 | it('should create new Collection resource', () => { 23 | expect(collectionResource instanceof Resource).toBe(true) 24 | expect(collectionResource.type).toBe(Collection) 25 | expect(collectionResource.path).toEqual(collectionPath) 26 | }) 27 | 28 | it('should have nested thng resource', () => { 29 | expect(collectionResource.thng).toBeDefined() 30 | expect(collectionResource.thng().path).toEqual(`${collectionPath}${paths.thngs}`) 31 | }) 32 | 33 | it('should have nested collection resource', () => { 34 | expect(collectionResource.collection).toBeDefined() 35 | expect(collectionResource.collection().path).toEqual(`${collectionPath}${paths.collections}`) 36 | }) 37 | 38 | it('should have nested action resource', () => { 39 | expect(collectionResource.action).toBeDefined() 40 | expect(collectionResource.action('all').path).toEqual( 41 | `${collectionPath}${paths.actions}/all` 42 | ) 43 | }) 44 | }) 45 | 46 | describe('access', () => { 47 | beforeEach(() => { 48 | collection = new Collection(dummyResource()) 49 | }) 50 | 51 | it('should have thng resource', () => { 52 | expect(collection.thng).toBeDefined() 53 | }) 54 | 55 | it('should have collection resource', () => { 56 | expect(collection.collection).toBeDefined() 57 | }) 58 | 59 | it('should have action resource', () => { 60 | expect(collection.action).toBeDefined() 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /test/e2e/dataGenerator.js: -------------------------------------------------------------------------------- 1 | const Chance = require('chance') 2 | 3 | const chance = new Chance() 4 | 5 | const characterPool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' 6 | const numberPool = '1234567890' 7 | const wordPool = `${characterPool}${numberPool}` 8 | const permissionPool = [ 9 | ['actions:create', 'places:list,read,update'], 10 | ['products:list,read', 'purchaseOrders:list,read', 'thngs:read'] 11 | ] 12 | const uiPermissionPool = [ 13 | ['activation', 'counterfeit'], 14 | ['authenticate', 'inventoryVisibility', 'inventoryTrace'] 15 | ] 16 | const customFieldsPool = [ 17 | { 18 | brand: 12 19 | }, 20 | { 21 | size: 'EU 45' 22 | } 23 | ] 24 | const identifiersPool = [ 25 | { 26 | 'ean:': 'sadivnojs89324' 27 | }, 28 | { 29 | 'random:': 'random' 30 | } 31 | ] 32 | const tagsPool = [ 33 | ['roles', 'and', 'permissions'], 34 | ['end', 'to'] 35 | ] 36 | 37 | const policyData = () => { 38 | const uiPermissions = chance.pickone(uiPermissionPool) 39 | return { 40 | name: chance.string({ length: 30, pool: wordPool }), 41 | permissions: chance.pickone(permissionPool), 42 | uiPermissions, 43 | homepage: chance.pickone(uiPermissions), 44 | description: chance.string({ length: 100, pool: wordPool }), 45 | customFields: chance.pickone(customFieldsPool), 46 | identifiers: chance.pickone(identifiersPool), 47 | tags: chance.pickone(tagsPool) 48 | } 49 | } 50 | 51 | const operatorAccessData = (operatorEmail, evtAccountAdminPolicy) => ({ 52 | email: operatorEmail, 53 | policies: [evtAccountAdminPolicy], 54 | conditions: [], 55 | customFields: {}, 56 | identifiers: {}, 57 | tags: [] 58 | }) 59 | 60 | const accessTokenData = (evtManagedPolicy) => ({ 61 | conditions: [], 62 | name: chance.word({ length: 6 }), 63 | description: chance.sentence({ words: 10 }), 64 | policies: [evtManagedPolicy], 65 | customFields: {}, 66 | identifiers: chance.pickone(identifiersPool), 67 | tags: chance.pickone(tagsPool) 68 | }) 69 | 70 | const thngData = () => ({ 71 | name: chance.word({ length: 3 }), 72 | description: chance.sentence({ words: 10 }), 73 | identifiers: {}, 74 | customFields: {}, 75 | properties: { 76 | status: chance.word({ length: 5 }) 77 | } 78 | }) 79 | 80 | module.exports = { 81 | policyData, 82 | operatorAccessData, 83 | accessTokenData, 84 | thngData 85 | } 86 | -------------------------------------------------------------------------------- /test/integration/misc/rescope.spec.js: -------------------------------------------------------------------------------- 1 | const { getScope, mockApi } = require('../util') 2 | 3 | module.exports = (scopeType, url) => { 4 | describe('rescope', () => { 5 | let scope, api 6 | 7 | before(async () => { 8 | scope = getScope(scopeType) 9 | api = mockApi(url) 10 | }) 11 | 12 | it('should remove all scopes', async () => { 13 | api.get('/thngs/thngId?withScopes=true').reply(200, { 14 | scopes: { 15 | projects: ['projectId'], 16 | users: ['all'] 17 | } 18 | }) 19 | api 20 | .put('/thngs/thngId', { 21 | scopes: { 22 | projects: ['projectId'], 23 | users: [] 24 | } 25 | }) 26 | .reply(200, {}) 27 | await scope.thng('thngId').rescope(['projectId'], []) 28 | }) 29 | 30 | it('should set one project scope', async () => { 31 | api.get('/thngs/thngId?withScopes=true').reply(200, { 32 | scopes: { 33 | projects: ['projectId'], 34 | users: ['all'] 35 | } 36 | }) 37 | api 38 | .put('/thngs/thngId', { 39 | scopes: { 40 | projects: ['projectId'], 41 | users: ['all'] 42 | } 43 | }) 44 | .reply(200, {}) 45 | await scope.thng('thngId').rescope(['projectId']) 46 | }) 47 | 48 | it('should set all users scope', async () => { 49 | api.get('/thngs/thngId?withScopes=true').reply(200, { 50 | scopes: { 51 | projects: ['projectId'], 52 | users: [] 53 | } 54 | }) 55 | api 56 | .put('/thngs/thngId', { 57 | scopes: { 58 | projects: ['projectId'], 59 | users: ['all'] 60 | } 61 | }) 62 | .reply(200, {}) 63 | await scope.thng('thngId').rescope(['projectId'], ['all']) 64 | }) 65 | 66 | it('should remove only the project scopes', async () => { 67 | api.get('/thngs/thngId?withScopes=true').reply(200, { 68 | scopes: { 69 | projects: ['projectId'], 70 | users: ['userId'] 71 | } 72 | }) 73 | api 74 | .put('/thngs/thngId', { 75 | scopes: { 76 | projects: [], 77 | users: ['userId'] 78 | } 79 | }) 80 | .reply(200, {}) 81 | await scope.thng('thngId').rescope([]) 82 | }) 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /test/integration/entity/redirection.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, targetType) => { 5 | let scope 6 | 7 | describe(`Redirection (${targetType})`, () => { 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | }) 11 | 12 | it(`should create a ${targetType} redirection`, async () => { 13 | const payload = { defaultRedirectUrl: 'https://www.google.com' } 14 | mockApi('https://tn.gg').post('/redirections', payload).reply(201, payload) 15 | const res = await scope[targetType]('targetId').redirection().create(payload) 16 | 17 | expect(res).to.be.an('object') 18 | expect(res.defaultRedirectUrl).to.be.a('string') 19 | }) 20 | 21 | it(`should read a ${targetType} redirection`, async () => { 22 | mockApi('https://tn.gg') 23 | .get('/redirections?evrythngId=targetId') 24 | .reply(200, [{ hits: 0 }]) 25 | const res = await scope[targetType]('targetId').redirection().read() 26 | 27 | expect(res.hits).to.equal(0) 28 | }) 29 | 30 | it(`should read a ${targetType} redirection with explicit shortDomain`, async () => { 31 | mockApi('https://abc.tn.gg') 32 | .get('/redirections?evrythngId=targetId') 33 | .reply(200, [{ hits: 0 }]) 34 | const res = await scope[targetType]('targetId').redirection('abc.tn.gg').read() 35 | 36 | expect(res.hits).to.equal(0) 37 | }) 38 | 39 | it(`should update a ${targetType} redirection`, async () => { 40 | const payload = { defaultRedirectUrl: 'https://google.com/updated?item={shortId}' } 41 | mockApi('https://tn.gg') 42 | .get('/redirections?evrythngId=targetId') 43 | .reply(200, [{ hits: 0, shortId: 'shortId' }]) 44 | mockApi('https://tn.gg').put('/redirections/shortId', payload).reply(200, payload) 45 | const res = await scope[targetType]('targetId').redirection().update(payload) 46 | 47 | expect(res).to.be.an('object') 48 | }) 49 | 50 | it(`should delete a ${targetType} redirection`, async () => { 51 | mockApi('https://tn.gg') 52 | .get('/redirections?evrythngId=targetId') 53 | .reply(200, [{ hits: 0, shortId: 'shortId' }]) 54 | mockApi('https://tn.gg').delete('/redirections/shortId').reply(200) 55 | await scope[targetType]('targetId').redirection().delete() 56 | }) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/ReactorLog.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import ReactorLog from '../../../src/entity/ReactorLog' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyResource } from '../../helpers/dummy' 7 | import { reactorLogTemplate } from '../../helpers/data' 8 | 9 | let reactorLogResource 10 | let resource 11 | 12 | describe('ReactorLog', () => { 13 | mockApi() 14 | 15 | describe('resourceFactory', () => { 16 | beforeEach(() => { 17 | resource = Object.assign(dummyResource(), ReactorLog.resourceFactory()) 18 | }) 19 | 20 | it('should not allow single resource access', () => { 21 | const singleResource = () => resource.reactorLog('id') 22 | expect(singleResource).toThrow() 23 | }) 24 | 25 | it('should create new ReactorLog resource', () => { 26 | reactorLogResource = resource.reactorLog() 27 | expect(reactorLogResource instanceof Resource).toBe(true) 28 | expect(reactorLogResource.type).toBe(ReactorLog) 29 | expect(reactorLogResource.path).toEqual(`${paths.dummy}${paths.reactorLogs}`) 30 | }) 31 | 32 | describe('with normalization', () => { 33 | beforeEach(() => { 34 | spyOn(Resource.prototype, 'create').and.returnValue(Promise.resolve()) 35 | resource = Object.assign(dummyResource(), ReactorLog.resourceFactory()) 36 | }) 37 | 38 | describe('create', () => { 39 | it('should do nothing for single log', (done) => { 40 | reactorLogResource = resource.reactorLog() 41 | reactorLogResource.create(reactorLogTemplate).then(() => { 42 | expect(Resource.prototype.create).toHaveBeenCalledWith(reactorLogTemplate) 43 | done() 44 | }) 45 | }) 46 | 47 | it('should use bulk endpoint for multiple logs', (done) => { 48 | const data = [reactorLogTemplate, reactorLogTemplate] 49 | resource 50 | .reactorLog() 51 | .create(data) 52 | .then(() => { 53 | expect(Resource.prototype.create).toHaveBeenCalledWith( 54 | data, 55 | jasmine.objectContaining({ 56 | url: `${paths.dummy}${paths.reactorLogs}/bulk` 57 | }) 58 | ) 59 | done() 60 | }) 61 | }) 62 | }) 63 | }) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /test/integration/entity/purchaseOrders.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | const payload = { 5 | id: `${Date.now()}`, 6 | status: 'open', 7 | type: 'stand-alone', 8 | description: 'A purchase order for 100 items', 9 | issueDate: '2019-09-13', 10 | parties: [ 11 | { id: 'gs1:414:943234', type: 'supplier' }, 12 | { id: 'gs1:414:01251', type: 'ship-from' }, 13 | { id: 'gs1:414:NA0193', type: 'ship-to' } 14 | ], 15 | lines: [ 16 | { 17 | id: '00010', 18 | quantity: 100, 19 | product: 'gs1:01:00000123456789', 20 | exportDate: '2019-02-17', 21 | deliveryDate: '2019-02-20' 22 | } 23 | ] 24 | } 25 | 26 | module.exports = (scopeType, url) => { 27 | describe('Purchase Orders', () => { 28 | let scope, api 29 | 30 | before(() => { 31 | scope = getScope(scopeType) 32 | api = mockApi(url) 33 | }) 34 | 35 | it('should read all purchase orders', async () => { 36 | api.get('/purchaseOrders').reply(200, [payload]) 37 | 38 | const res = await scope.purchaseOrder().read() 39 | 40 | expect(res).to.be.an('array') 41 | expect(res).to.have.length.gte(1) 42 | }) 43 | 44 | if (scopeType === 'operator') { 45 | it('should create a purchase order', async () => { 46 | api.post('/purchaseOrders', payload).reply(201, payload) 47 | 48 | const res = await scope.purchaseOrder().create(payload) 49 | 50 | expect(res).to.be.an('object') 51 | }) 52 | 53 | it('should read a purchase order', async () => { 54 | api.get('/purchaseOrders/purchaseOrderId').reply(200, payload) 55 | const res = await scope.purchaseOrder('purchaseOrderId').read() 56 | 57 | expect(res).to.be.an('object') 58 | expect(res.id).to.equal(payload.id) 59 | }) 60 | 61 | it('should update a purchase order', async () => { 62 | api.put('/purchaseOrders/purchaseOrderId', payload).reply(200, payload) 63 | 64 | const res = await scope.purchaseOrder('purchaseOrderId').update(payload) 65 | 66 | expect(res).to.be.an('object') 67 | expect(res.tags).to.deep.equal(payload.tags) 68 | }) 69 | 70 | it('should delete a purchaseOrder', async () => { 71 | api.delete('/purchaseOrders/purchaseOrderId').reply(204) 72 | 73 | const res = await scope.purchaseOrder('purchaseOrderId').delete() 74 | expect(res).to.not.exist 75 | }) 76 | } 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /test/helpers/data.js: -------------------------------------------------------------------------------- 1 | export const apiKey = 'apiKey' 2 | export const operatorApiKey = 'operatorApiKey' 3 | export const appApiKey = 'appApiKey' 4 | 5 | export const optionsTemplate = { 6 | fullResponse: true, 7 | headers: { 8 | Accept: '*/*' 9 | } 10 | } 11 | 12 | export const entityTemplate = { 13 | foo: 'bar' 14 | } 15 | 16 | export const productTemplate = { 17 | id: 'productId', 18 | name: 'Test Product' 19 | } 20 | 21 | export const thngTemplate = { 22 | id: 'thndId', 23 | name: 'Test Thng' 24 | } 25 | 26 | export const propertyTemplate = { 27 | key: 'test', 28 | value: 1 29 | } 30 | 31 | export const actionTemplate = { 32 | id: 'actionId', 33 | type: 'test', 34 | product: productTemplate.id 35 | } 36 | 37 | export const operatorTemplate = { 38 | id: 'operatorId', 39 | firstName: 'Test', 40 | lastName: 'Operator', 41 | email: 'test@evrythng.com' 42 | } 43 | 44 | export const positionTemplate = { 45 | coords: { 46 | latitude: 1, 47 | longitude: 2 48 | } 49 | } 50 | 51 | export const locationTemplate = { 52 | position: { 53 | type: 'Point', 54 | coordinates: [1.1, 2.2] 55 | }, 56 | timestamp: 1 57 | } 58 | 59 | export const collectionTemplate = { 60 | id: 'collectionId', 61 | name: 'Test Collection' 62 | } 63 | 64 | export const actionTypeTemplate = { 65 | name: '_foobar', 66 | customFields: { 67 | displayname: 'Foo Bar' 68 | } 69 | } 70 | 71 | export const projectTemplate = { 72 | id: 'projectId', 73 | name: 'Test Project' 74 | } 75 | 76 | export const applicationTemplate = { 77 | id: 'applicationId', 78 | name: 'Test Application' 79 | } 80 | 81 | export const roleTemplate = { 82 | id: 'roleId', 83 | name: 'Test Role' 84 | } 85 | 86 | export const userTemplate = { 87 | id: 'userId', 88 | firstName: 'Test', 89 | lastName: 'User', 90 | email: 'test@evrythng.com' 91 | } 92 | 93 | export const batchTemplate = { 94 | id: 'batchId', 95 | name: 'Test Batch' 96 | } 97 | 98 | export const placeTemplate = { 99 | id: 'placeId', 100 | name: 'Test Place' 101 | } 102 | 103 | export const reactorScheduleTemplate = { 104 | id: 'reactorScheduleId', 105 | enabled: true 106 | } 107 | 108 | export const reactorLogTemplate = { 109 | message: 'Log example message', 110 | logLevel: 'info', 111 | createdAt: 1 112 | } 113 | 114 | export const fileTemplate = { 115 | id: 'fileId', 116 | contentUrl: 'http://example.com/test.jpg', 117 | name: 'File example' 118 | } 119 | 120 | export const userAccessTemplate = { 121 | evrythngUser: 'userId', 122 | activationCode: 'activactionCode' 123 | } 124 | -------------------------------------------------------------------------------- /test/integration/entity/operatorAccesses.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | const payload = { 5 | email: 'operatorAccess@gmail.com', 6 | description: 'Create operator access', 7 | policies: ['UPb7E6shapktcaaabfahfpds'], 8 | conditions: [], 9 | operator: 'UtnGTkaPDphkYDC9F2KHBtPp', 10 | tags: ['operatorAccess'], 11 | identifiers: {}, 12 | customFields: {} 13 | } 14 | 15 | module.exports = (scopeType, settings) => { 16 | describe('OperatorAccesses', () => { 17 | let scope, api 18 | 19 | before(async () => { 20 | scope = getScope(scopeType) 21 | api = mockApi(settings.apiUrl) 22 | }) 23 | 24 | if (settings.apiVersion == 2) { 25 | it('should create operator access', async () => { 26 | api 27 | .post('/accounts/accountId/operatorAccess', payload) 28 | .reply(201, { id: 'operatorAccessId' }) 29 | const res = await scope.sharedAccount('accountId').operatorAccess().create(payload) 30 | 31 | expect(res).to.be.an('object') 32 | expect(res.id).to.be.a('string') 33 | }) 34 | 35 | it('should read operator access by id', async () => { 36 | api 37 | .get('/accounts/accountId/operatorAccess/operatorAccessId') 38 | .reply(200, { id: 'operatorAccessId' }) 39 | const res = await scope 40 | .sharedAccount('accountId') 41 | .operatorAccess('operatorAccessId') 42 | .read() 43 | 44 | expect(res).to.be.an('object') 45 | expect(res.id).to.be.a('string') 46 | }) 47 | 48 | it('should read all operator accesses', async () => { 49 | api.get('/accounts/accountId/operatorAccess').reply(200, [payload]) 50 | const res = await scope.sharedAccount('accountId').operatorAccess().read() 51 | 52 | expect(res).to.be.an('array') 53 | expect(res).to.have.length.gte(1) 54 | }) 55 | 56 | it('should update operator access', async () => { 57 | api.put('/accounts/accountId/operatorAccess/operatorAccessId', payload).reply(200, payload) 58 | 59 | const res = await scope 60 | .sharedAccount('accountId') 61 | .operatorAccess('operatorAccessId') 62 | .update(payload) 63 | 64 | expect(res).to.be.an('object') 65 | expect(res.name).to.deep.equal(payload.name) 66 | }) 67 | 68 | it('should delete operator access', async () => { 69 | api.delete('/accounts/accountId/operatorAccess/operatorAccessId').reply(204) 70 | const res = await scope 71 | .sharedAccount('accountId') 72 | .operatorAccess('operatorAccessId') 73 | .delete() 74 | 75 | expect(res).to.not.exist 76 | }) 77 | } 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /test/unit/outDated/entity/Application.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Resource from '../../../src/resource/Resource' 3 | import Application from '../../../src/entity/Application' 4 | import mockApi from '../../helpers/apiMock' 5 | import paths from '../../helpers/paths' 6 | import { dummyScope, dummyResource, dummyEntity } from '../../helpers/dummy' 7 | import { applicationTemplate } from '../../helpers/data' 8 | 9 | let applicationResource 10 | let application 11 | 12 | describe('Application', () => { 13 | mockApi() 14 | 15 | describe('resourceFactory', () => { 16 | it('should only be allowed as a nested resource', () => { 17 | const scope = Object.assign(dummyScope(), Application.resourceFactory()) 18 | const wrongBase = () => scope.application() 19 | expect(wrongBase).toThrow() 20 | 21 | const resource = Object.assign(dummyResource(), Application.resourceFactory()) 22 | const resourceProperty = () => resource.application() 23 | expect(resourceProperty).not.toThrow() 24 | 25 | const entity = Object.assign(dummyEntity(), Application.resourceFactory()) 26 | const entityProperty = () => entity.application() 27 | expect(entityProperty).not.toThrow() 28 | }) 29 | 30 | describe('valid', () => { 31 | beforeEach(() => { 32 | const resource = Object.assign(dummyResource(), Application.resourceFactory()) 33 | applicationResource = resource.application(applicationTemplate.id) 34 | }) 35 | 36 | it('should create new Product resource', () => { 37 | expect(applicationResource instanceof Resource).toBe(true) 38 | expect(applicationResource.type).toBe(Application) 39 | expect(applicationResource.path).toEqual( 40 | `${paths.dummy}${paths.applications}/${applicationTemplate.id}` 41 | ) 42 | }) 43 | 44 | it('should have nested reactorScript resource', () => { 45 | expect(applicationResource.reactorScript).toBeDefined() 46 | }) 47 | 48 | it('should have nested reactorSchedule resource', () => { 49 | expect(applicationResource.reactorSchedule).toBeDefined() 50 | }) 51 | 52 | it('should have nested reactorLog resource', () => { 53 | expect(applicationResource.reactorLog).toBeDefined() 54 | }) 55 | }) 56 | }) 57 | 58 | describe('access', () => { 59 | beforeEach(() => { 60 | application = new Application(dummyResource()) 61 | }) 62 | 63 | it('should have reactorScript resource', () => { 64 | expect(application.reactorScript).toBeDefined() 65 | }) 66 | 67 | it('should have reactorSchedule resource', () => { 68 | expect(application.reactorSchedule).toBeDefined() 69 | }) 70 | 71 | it('should have reactorLog resource', () => { 72 | expect(application.reactorLog).toBeDefined() 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /src/entity/ActionType.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | import isFunction from 'lodash-es/isFunction' 4 | import isUndefined from 'lodash-es/isUndefined' 5 | 6 | const path = '/actions' 7 | 8 | /** 9 | * Represents an ActionType entity. Action types endpoint it weird as it 10 | * overlaps with the Actions (/actions), so there is a normalization necessary 11 | * on the read method. 12 | * 13 | * @extends Entity 14 | */ 15 | export default class ActionType extends Entity { 16 | /** 17 | * Return overridden resource factory for ActionsTypes. Read method needs to 18 | * use a filter as there is no single action type resource endpoint. 19 | * 20 | * @static 21 | * @return {{actionType: Function}} 22 | */ 23 | static resourceFactory () { 24 | return { 25 | actionType (id) { 26 | return Object.assign(Resource.factoryFor(ActionType, path).call(this, id), { 27 | read (...args) { 28 | return readActionType.call(this, id, ...args) 29 | } 30 | }) 31 | } 32 | } 33 | } 34 | } 35 | 36 | /** 37 | * Normalize arguments and response on single read request. 38 | * 39 | * @param {String} id - Action type ID 40 | * @param {*} args - Arguments passed to .read method 41 | * @return {Promise} 42 | */ 43 | function readActionType (id, ...args) { 44 | if (!id) { 45 | return Resource.prototype.read.call(this, ...args) 46 | } else { 47 | const normalizedArgs = normalizeArguments(id)(...args) 48 | return new Promise((resolve, reject) => { 49 | // If reading an action type, only use the root of the path 50 | this.path = this.path.split('/').slice(0, 2).join('/') 51 | 52 | Resource.prototype.read.call(this, ...normalizedArgs).then((actionTypes) => { 53 | if (!actionTypes.length) { 54 | // Fake 404 55 | reject({ 56 | status: 404, 57 | errors: ['The action type was not found.'] 58 | }) 59 | } 60 | resolve(actionTypes[0]) 61 | }) 62 | }) 63 | } 64 | } 65 | 66 | /** 67 | * Curry normalizeArguments with action type id. Converts single resource path 68 | * (e.g. /actions/_custom) into plural with filter 69 | * (e.g. /actions?filter=name=_custom). 70 | * 71 | * @param {String} id - ID of action type 72 | * @return {Function} Normalize arguments transformer. 73 | */ 74 | function normalizeArguments (id) { 75 | return (...args) => { 76 | let options 77 | const firstArg = args[0] 78 | 79 | if (isUndefined(firstArg) || isFunction(firstArg)) { 80 | options = {} 81 | args.unshift(options) 82 | } else { 83 | options = firstArg 84 | } 85 | 86 | options.url = path 87 | options.params = Object.assign({ filter: { name: decodeURIComponent(id) } }, options.params) 88 | 89 | return args 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/entity/Property.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | import Scope from '../scope/Scope' 4 | import symbols from '../symbols' 5 | import isString from 'lodash-es/isString' 6 | import isPlainObject from 'lodash-es/isPlainObject' 7 | 8 | const path = '/properties' 9 | 10 | /** 11 | * Represents a Property entity. Properties are always nested and required 12 | * to be constructed on Resource/Entity objects (not top level Scopes). 13 | * 14 | * @extends Entity 15 | */ 16 | export default class Property extends Entity { 17 | /** 18 | * Return overridden resource factory for Properties. Properties are 19 | * sub-resources of Thngs and Products and are not allowed on top level 20 | * Scope classes. This factory also override the default Resource's create 21 | * and update methods to accept and normalize different types of arguments. 22 | * 23 | * @static 24 | * @return {{property: Function}} 25 | */ 26 | static resourceFactory () { 27 | return { 28 | property (id) { 29 | const thngPath = this instanceof Scope ? this[symbols.path] : '' 30 | 31 | // Creates and returns Resource of type Property. 32 | // Override property resource create/update to allow custom value 33 | // params. See `normalizeArguments()`. 34 | return Object.assign(Resource.factoryFor(Property, thngPath + path).call(this, id), { 35 | create (...args) { 36 | return Resource.prototype.create.call(this, ...normalizeArguments(...args)) 37 | }, 38 | update (...args) { 39 | return Resource.prototype.update.call(this, ...normalizeArguments(...args)) 40 | } 41 | }) 42 | } 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * The API only allows array format for property updates. The SDK relaxed that 49 | * and allows developers to pass in simple strings, numbers, booleans and objects. 50 | * It also allows to pass multiple key-value updates in a single object. 51 | * 52 | * @param {*} data - Property data. 53 | * @param {*} rest - Rest of parameters. 54 | * @return {Array} - Same input format, with first data param updated. 55 | * @example 56 | * 57 | * product.property().update({ 58 | * on: true, 59 | * temp: 26, 60 | * custom: ['1', '2'] 61 | * }) 62 | */ 63 | function normalizeArguments (data, ...rest) { 64 | if (isString(data) || typeof data === 'number' || typeof data === 'boolean') { 65 | // Convert simple property values to API format. 66 | data = [{ value: data }] 67 | } else if (isPlainObject(data)) { 68 | if (data.hasOwnProperty('value')) { 69 | // Update single property using object notation. 70 | data = [data] 71 | } else { 72 | // Update multiple properties creating an object for each key-value pair. 73 | data = Object.entries(data).map((val) => ({ 74 | key: val[0], 75 | value: val[1] 76 | })) 77 | } 78 | } 79 | 80 | return [data, ...rest] 81 | } 82 | -------------------------------------------------------------------------------- /src/scope/User.js: -------------------------------------------------------------------------------- 1 | import Scope from './Scope' 2 | import Product from '../entity/Product' 3 | import Thng from '../entity/Thng' 4 | import Collection from '../entity/Collection' 5 | import Action from '../entity/Action' 6 | import ActionType from '../entity/ActionType' 7 | import Role from '../entity/Role' 8 | import Rule from '../entity/Rule' 9 | import Place from '../entity/Place' 10 | import PurchaseOrder from '../entity/PurchaseOrder' 11 | import File from '../entity/File' 12 | import { mixinResources } from '../util/mixin' 13 | import api from '../api' 14 | import symbols from '../symbols' 15 | 16 | /** 17 | * Mixin with all the top-level User resources. 18 | * 19 | * @mixin 20 | */ 21 | const AppUser = mixinResources([ 22 | Product, // CRU 23 | Thng, // CRU 24 | Collection, // CRU 25 | Action, // CR 26 | ActionType, // R 27 | PurchaseOrder, // R 28 | Role, // R 29 | Rule, 30 | Place, // R 31 | File // C 32 | ]) 33 | 34 | /** 35 | * User is the Scope that represents an application user. It is usually 36 | * retrieved by authenticating a user in an app, but can also be instantiated 37 | * explicitly if API Key and details are known (e.g. stored in localStorage). 38 | * 39 | * @extends Scope 40 | * @mixes AppUser 41 | */ 42 | export default class User extends AppUser(Scope) { 43 | /** 44 | * Creates an instance of User. 45 | * 46 | * @param {string} apiKey - API Key of scope 47 | * @param {Object} [data={}] - Optional user data 48 | */ 49 | constructor (apiKey, data = {}) { 50 | super(apiKey, data) 51 | 52 | this.initPromise = super 53 | .readAccess() 54 | .then((access) => { 55 | this.id = access.actor.id 56 | this[symbols.path] = this._getPath() 57 | }) 58 | .then(() => this.read()) 59 | } 60 | 61 | /** 62 | * Read the user's data asynchronously. 63 | * 64 | * @returns {Promise} 65 | */ 66 | init () { 67 | return this.initPromise 68 | } 69 | 70 | /** 71 | * Log current user out of EVRYTHNG platform. The API key is no longer valid. 72 | * 73 | * @param {Function} callback - Error first callback 74 | * @returns {Promise.} 75 | */ 76 | async logout (callback) { 77 | try { 78 | const res = await this._invalidateUser() 79 | if (callback) callback(res) 80 | return res 81 | } catch (err) { 82 | if (callback) callback(err) 83 | throw err 84 | } 85 | } 86 | 87 | // PRIVATE 88 | 89 | /** 90 | * Return user endpoint. 91 | * 92 | * @return {string} 93 | */ 94 | _getPath () { 95 | return `/users/${this.id}` 96 | } 97 | 98 | /** 99 | * Request to invalidate API Key. 100 | * 101 | * @returns {Promise} 102 | * @private 103 | */ 104 | _invalidateUser () { 105 | return api({ 106 | url: '/auth/all/logout', 107 | method: 'post', 108 | apiKey: this.apiKey 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /test/integration/entity/permissions.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | const describeOperatorPermissionTests = (url) => { 5 | describe('Permissions (Operator Roles)', () => { 6 | let operator, api 7 | 8 | before(async () => { 9 | operator = getScope('operator') 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should read all role permissions', async () => { 14 | api.get('/roles/roleId/permissions').reply(200, [{ name: 'global_read' }]) 15 | const res = await operator.role('roleId').permission().read() 16 | 17 | expect(res).to.be.an('array') 18 | expect(res).to.have.length.gte(1) 19 | }) 20 | 21 | it('should read a single role permission', async () => { 22 | api.get('/roles/roleId/permissions/global_read').reply(200, { name: 'global_read' }) 23 | const res = await operator.role('roleId').permission('global_read').read() 24 | 25 | expect(res).to.be.an('object') 26 | expect(res.name).to.equal('global_read') 27 | }) 28 | 29 | it('should update a single role permission', async () => { 30 | const payload = { name: 'global_read', enabled: false } 31 | api.put('/roles/roleId/permissions/global_read', payload).reply(200, payload) 32 | const res = await operator.role('roleId').permission('global_read').update(payload) 33 | 34 | expect(res).to.be.an('object') 35 | expect(res.enabled).to.equal(false) 36 | }) 37 | }) 38 | } 39 | 40 | const describeAppUserPermissionTests = (url) => { 41 | describe('Permissions (App User Roles)', () => { 42 | let operator, api 43 | 44 | before(async () => { 45 | operator = getScope('operator') 46 | api = mockApi(url) 47 | }) 48 | 49 | it('should read all role permissions', async () => { 50 | api.get('/roles/roleId/permissions').reply(200, [{ access: 'cru', path: '/thngs' }]) 51 | const res = await operator.role('roleId').permission().read() 52 | 53 | expect(res).to.be.an('array') 54 | expect(res).to.have.length.gte(1) 55 | }) 56 | 57 | it('should update role permissions', async () => { 58 | const payload = [{ path: '/thngs', access: 'cr' }] 59 | api.put('/roles/roleId/permissions', payload).reply(200, payload) 60 | const res = await operator.role('roleId').permission().update(payload) 61 | 62 | expect(res).to.be.an('array') 63 | expect(res).to.have.length.gte(1) 64 | }) 65 | 66 | it('should minimise role permissions', async () => { 67 | const payload = [] 68 | api.put('/roles/roleId/permissions', payload).reply(200, payload) 69 | const res = await operator.role('roleId').permission().update(payload) 70 | 71 | expect(res).to.be.an('array') 72 | }) 73 | }) 74 | } 75 | 76 | module.exports = (type, url) => { 77 | if (type === 'operator') { 78 | describeOperatorPermissionTests(url) 79 | } 80 | 81 | if (type === 'userInApp') { 82 | describeAppUserPermissionTests(url) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/helpers/apiMock.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import fetchMock from 'fetch-mock' 3 | import apiUrl from './apiUrl' 4 | import responses from './responses' 5 | import paths from './paths' 6 | import { apiKey, operatorApiKey, appApiKey } from './data' 7 | 8 | /** 9 | * Mock API for each separate test block. It is important that tests run 10 | * independently from each other by guaranteeing there's only one fetchMock 11 | * instance at a time. Otherwise, one instance tracks and matches the requests 12 | * from another (as there's only one global fetch). Thus, this method should 13 | * be called within the first describe block for the module. 14 | * 15 | * @example 16 | * 17 | * describe('Operator', () => { 18 | * mockApi() 19 | * 20 | * ... 21 | * } 22 | */ 23 | export default function mockApi () { 24 | beforeAll(prepare) 25 | afterAll(tearDown) 26 | } 27 | 28 | /** 29 | * Delayed promise. 30 | * 31 | * @param {Number} time - Delay time in milliseconds 32 | */ 33 | const delay = (time) => new Promise((resolve) => setTimeout(resolve, time)) 34 | 35 | /** 36 | * Init API mock as a whole. 37 | */ 38 | function prepare () { 39 | // Root - generic requests handles 40 | fetchMock.mock(apiUrl(), responses.ok) 41 | fetchMock.mock(/\?.*/, responses.ok) 42 | fetchMock.mock(`${paths.testBase}${paths.dummy}`, responses.ok) 43 | 44 | // Unavailable/error endpoint 45 | fetchMock.get(apiUrl(paths.error), responses.error.generic) 46 | 47 | // Access 48 | fetchMock.get(apiUrl(paths.access), (url, opts) => validAccess(opts)) 49 | 50 | // Dummy resource path for entities 51 | fetchMock.get(apiUrl(paths.dummy), responses.entity.multiple) 52 | fetchMock.post(apiUrl(paths.dummy), responses.entity.one) 53 | fetchMock.put(apiUrl(paths.dummy), responses.entity.one) 54 | fetchMock.delete(apiUrl(paths.dummy), responses.noContent) 55 | 56 | // Operator 57 | fetchMock.get(apiUrl(paths.operator), responses.operator.one) 58 | fetchMock.post(apiUrl(paths.operators), responses.operator.one) 59 | fetchMock.put(apiUrl(paths.operator), responses.operator.one) 60 | 61 | // Application 62 | fetchMock.get(apiUrl(paths.application), responses.application.one) 63 | fetchMock.put(apiUrl(paths.application), responses.application.one) 64 | 65 | // Validate user 66 | fetchMock.post(apiUrl(`${paths.dummy}/${paths.usersAccessValidate}`), responses.ok) 67 | fetchMock.post(apiUrl(`${paths.usersAccess}/${paths.usersAccessValidate}`), responses.ok) 68 | } 69 | 70 | /** 71 | * Wait a few milliseconds to ensure there are no pending Promises 72 | * chains that will call fetch. 73 | * 74 | * @param {Function} done - Jasmine done callback 75 | */ 76 | function tearDown (done) { 77 | delay(100).then(fetchMock.restore).then(done) 78 | } 79 | 80 | /** 81 | * Verifies if access is valid by matching to the globally defined apiKey. 82 | * 83 | * @param {Object} opts - Request options 84 | * @return {Object} - Response 85 | */ 86 | function validAccess (opts) { 87 | if (opts.headers.authorization === apiKey || opts.headers.authorization === operatorApiKey) { 88 | return responses.access.operator 89 | } else if (opts.headers.authorization === appApiKey) { 90 | return responses.access.application 91 | } else { 92 | return responses.error.generic 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/integration/misc/upsert.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | const chaiAsPromised = require('chai-as-promised') 4 | 5 | const { expect } = chai 6 | chai.use(chaiAsPromised) 7 | 8 | const payload = { name: 'Test Thng', identifiers: { serial: '8230947' } } 9 | 10 | module.exports = (scopeType, url) => { 11 | describe('upsert', () => { 12 | let scope, api 13 | 14 | before(async () => { 15 | scope = getScope(scopeType) 16 | api = mockApi(url) 17 | }) 18 | 19 | it('should create a Thng by identifiers', async () => { 20 | api.get('/thngs?filter=identifiers.serial%3D8230947').reply(200, []) 21 | api.post('/thngs', payload).reply(201, payload) 22 | const res = await scope.thng().upsert(payload, payload.identifiers) 23 | 24 | expect(res).to.be.an('object') 25 | expect(res.name).to.equal(payload.name) 26 | expect(res.identifiers).to.deep.equal(payload.identifiers) 27 | }) 28 | 29 | it('should update the same Thng by identifiers', async () => { 30 | payload.name = 'Updated Thng' 31 | api 32 | .get('/thngs?filter=identifiers.serial%3D8230947') 33 | .reply(200, [{ id: 'thngId', name: 'Updated Thng' }]) 34 | api.put('/thngs/thngId', payload).reply(200, payload) 35 | const res = await scope.thng().upsert(payload, payload.identifiers) 36 | 37 | expect(res).to.be.an('object') 38 | expect(res.name).to.equal(payload.name) 39 | }) 40 | 41 | it('should refuse to update if more than one Thng is found', async () => { 42 | api 43 | .get('/thngs?filter=identifiers.serial%3D8230947') 44 | .reply(200, [{ id: 'thngId' }, { id: 'thngId2' }]) 45 | const attempt = scope.thng().upsert(payload, payload.identifiers) 46 | return expect(attempt).to.eventually.be.rejected 47 | }) 48 | 49 | it('should allow overriding with allowPlural', async () => { 50 | payload.name = 'Twice Updated Thng' 51 | api 52 | .get('/thngs?filter=identifiers.serial%3D8230947') 53 | .reply(200, [{ id: 'thngId' }, { id: 'thngId2' }]) 54 | api.put('/thngs/thngId', payload).reply(200, { id: 'thngId' }) 55 | const res = await scope.thng().upsert(payload, payload.identifiers, true) 56 | 57 | expect(res).to.be.an('object') 58 | expect(res.id).to.equal('thngId') 59 | }) 60 | 61 | it('should create a Thng by name', async () => { 62 | payload.name = 'New Thng Name' 63 | api.get('/thngs?filter=name%3DNew%20Thng%20Name').reply(200, []) 64 | api.post('/thngs', payload).reply(200, { id: 'thngId', name: 'New Thng Name' }) 65 | const res = await scope.thng().upsert(payload, payload.name) 66 | 67 | expect(res).to.be.an('object') 68 | expect(res.name).to.equal(payload.name) 69 | }) 70 | 71 | it('should update a Thng by name', async () => { 72 | payload.tags = ['test', 'tags'] 73 | api.get('/thngs?filter=name%3DNew%20Thng%20Name').reply(200, [{ id: 'thngId' }]) 74 | api.put('/thngs/thngId', payload).reply(200, payload) 75 | const res = await scope.thng().upsert(payload, payload.name) 76 | 77 | expect(res).to.be.an('object') 78 | expect(res.name).to.equal(payload.name) 79 | expect(res.tags).to.deep.equal(payload.tags) 80 | }) 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /src/scope/Operator.js: -------------------------------------------------------------------------------- 1 | import Scope from './Scope' 2 | import Account from '../entity/Account' 3 | import ADIOrder from '../entity/ADIOrder' 4 | import Product from '../entity/Product' 5 | import Thng from '../entity/Thng' 6 | import Collection from '../entity/Collection' 7 | import Action from '../entity/Action' 8 | import ActionType from '../entity/ActionType' 9 | import Project from '../entity/Project' 10 | import PurchaseOrder from '../entity/PurchaseOrder' 11 | import ReactorLog from '../entity/ReactorLog' 12 | import Redirector from '../entity/Redirector' 13 | import Role from '../entity/Role' 14 | import Rule from '../entity/Rule' 15 | import ShipmentNotice from '../entity/ShipmentNotice' 16 | import User from '../entity/User' 17 | import Batch from '../entity/Batch' 18 | import Place from '../entity/Place' 19 | import File from '../entity/File' 20 | import AccessPolicy from '../entity/AccessPolicy' 21 | import AccessToken from '../entity/AccessToken' 22 | import Me from '../entity/Me' 23 | import { mixinResources } from '../util/mixin' 24 | import symbols from '../symbols' 25 | 26 | /** 27 | * Mixin with all the top-level Operator resources. 28 | * 29 | * @mixin 30 | */ 31 | const OperatorAccess = mixinResources([ 32 | AccessPolicy, // CRUDL 33 | AccessToken, // CL 34 | Account, // LRU 35 | Action, // CRLD 36 | ActionType, // CRULD 37 | ADIOrder, // CRL 38 | Batch, // CRUD 39 | Collection, // CRUDL 40 | File, // CRUD 41 | Me, // R 42 | Place, // CRUDL 43 | Product, // CRUDL 44 | Project, // CRUDL 45 | PurchaseOrder, // CRUDL 46 | ReactorLog, // C 47 | Redirector, // RUD 48 | Role, // CRUD 49 | Rule, 50 | ShipmentNotice, // CRUDL 51 | Thng, // CRUDL 52 | User // R 53 | ]) 54 | 55 | /** 56 | * Operator is the Scope with highest permissions that can manage the account 57 | * resources. Should be used with caution in server-side code. 58 | * 59 | * @extends Scope 60 | * @mixes OperatorAccess 61 | */ 62 | export default class Operator extends OperatorAccess(Scope) { 63 | /** 64 | * Creates an instance of Operator. 65 | * 66 | * @param {string} apiKey - API Key of scope 67 | * @param {Object} [data={}] - Optional operator data 68 | */ 69 | constructor (apiKey, data = {}) { 70 | super(apiKey, data) 71 | 72 | this.initPromise = super 73 | .readAccess() 74 | .then((access) => { 75 | this.id = access.actor.id 76 | this[symbols.path] = this._getPath() 77 | }) 78 | .then(() => this.read()) 79 | } 80 | 81 | /** 82 | * Read the operator's data asynchronously. 83 | * 84 | * @returns {Promise} 85 | */ 86 | init () { 87 | return this.initPromise 88 | } 89 | 90 | /** 91 | * Update the self-same Operator's data. 92 | * 93 | * @param {object} data - Update payload, such as { customFields } 94 | */ 95 | update (data) { 96 | const opts = { 97 | method: 'put', 98 | url: `/operators/${this.id}`, 99 | apiKey: this.apiKey, 100 | data 101 | } 102 | 103 | return this._request(opts) 104 | } 105 | 106 | // PRIVATE 107 | 108 | /** 109 | * Return operator endpoint. 110 | * 111 | * @return {string} 112 | */ 113 | _getPath () { 114 | return `/operators/${this.id}` 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /test/unit/outDated/scope/Application.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Application from '../../../src/scope/Application' 3 | import symbols from '../../../src/symbols' 4 | import fetchMock from 'fetch-mock' 5 | import mockApi from '../../helpers/apiMock' 6 | import apiUrl from '../../helpers/apiUrl' 7 | import paths from '../../helpers/paths' 8 | import { appApiKey, applicationTemplate } from '../../helpers/data' 9 | 10 | let application 11 | 12 | describe('Application', () => { 13 | mockApi() 14 | 15 | describe('constructor', () => { 16 | describe('existent', () => { 17 | beforeEach(() => { 18 | spyOn(Application.prototype, 'read') 19 | application = new Application(appApiKey) 20 | }) 21 | 22 | it('should read itself', (done) => { 23 | application[symbols.init].then(() => { 24 | expect(Application.prototype.read).toHaveBeenCalled() 25 | done() 26 | }) 27 | }) 28 | }) 29 | 30 | describe('non-existent', () => { 31 | beforeEach(() => { 32 | application = new Application('invalidKey') 33 | }) 34 | 35 | it('should throw error', (done) => { 36 | application[symbols.init].catch(() => expect(true).toBe(true)).then(done) 37 | }) 38 | }) 39 | }) 40 | 41 | describe('read', () => { 42 | beforeEach((done) => { 43 | application = new Application(appApiKey) 44 | application[symbols.init].then(done) 45 | }) 46 | 47 | it('should send get request to project and application ID', (done) => { 48 | application.read().then(() => { 49 | expect(fetchMock.lastUrl()).toEqual(apiUrl(paths.application)) 50 | expect(fetchMock.lastOptions().method).toEqual('get') 51 | done() 52 | }) 53 | }) 54 | 55 | it('should fill operator scope with details', (done) => { 56 | application.read().then((response) => { 57 | expect(application).toBe(response) 58 | expect(application).toEqual(jasmine.objectContaining(applicationTemplate)) 59 | done() 60 | }) 61 | }) 62 | }) 63 | 64 | describe('update', () => { 65 | beforeEach((done) => { 66 | application = new Application(appApiKey) 67 | application[symbols.init].then(done) 68 | }) 69 | 70 | it('should send get request to operator ID', (done) => { 71 | application.update(applicationTemplate).then(() => { 72 | expect(fetchMock.lastUrl()).toEqual(apiUrl(paths.application)) 73 | expect(fetchMock.lastOptions().method).toEqual('put') 74 | done() 75 | }) 76 | }) 77 | 78 | it('should fill operator scope with details', (done) => { 79 | application.update(applicationTemplate).then((response) => { 80 | expect(application).toBe(response) 81 | expect(application).toEqual(jasmine.objectContaining(applicationTemplate)) 82 | done() 83 | }) 84 | }) 85 | }) 86 | 87 | describe('access', () => { 88 | const operatorResources = ['product', 'action', 'place', 'appUser'] 89 | 90 | beforeEach((done) => { 91 | application = new Application(appApiKey) 92 | application[symbols.init].then(done) 93 | }) 94 | 95 | operatorResources.forEach((resource) => { 96 | it(`should have ${resource} resource`, () => { 97 | expect(application[resource]).toBeDefined() 98 | }) 99 | }) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /test/integration/misc/paramSetters.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { getScope, mockApi } = require('../util') 3 | 4 | module.exports = (scopeType, url) => { 5 | describe('Param Setters', () => { 6 | let scope, api 7 | 8 | before(async () => { 9 | scope = getScope(scopeType) 10 | api = mockApi(url) 11 | }) 12 | 13 | it('should set withScopes via setWithScopes', async () => { 14 | api.get('/thngs?withScopes=true').reply(200, [ 15 | { 16 | name: 'Thng 1', 17 | scopes: { 18 | project: [], 19 | users: ['all'] 20 | } 21 | } 22 | ]) 23 | const res = await scope.thng().setWithScopes().read() 24 | 25 | expect(res).to.be.an('array') 26 | expect(res).to.have.length.gte(1) 27 | expect(res[0].scopes).to.be.an('object') 28 | }) 29 | 30 | it('should set context via setContext', async () => { 31 | api.get('/actions/all?context=true').reply(200, [ 32 | { 33 | type: 'scans', 34 | context: { countryCode: 'GB' } 35 | } 36 | ]) 37 | const res = await scope.action('all').setContext().read() 38 | 39 | expect(res).to.be.an('array') 40 | expect(res).to.have.length.gte(1) 41 | expect(res[0].context).to.be.an('object') 42 | }) 43 | 44 | it('should set perPage via setPerPage', async () => { 45 | api.get('/thngs?perPage=1').reply(200, [{ name: 'Thng 1' }]) 46 | const res = await scope.thng().setPerPage(1).read() 47 | 48 | expect(res).to.be.an('array') 49 | expect(res).to.have.length(1) 50 | }) 51 | 52 | it('should set project via setProject', async () => { 53 | api.get('/thngs?project=projectId').reply(200, [{ name: 'Thng 1' }]) 54 | const res = await scope.thng().setProject('projectId').read() 55 | 56 | expect(res).to.be.an('array') 57 | expect(res).to.have.length.gte(1) 58 | }) 59 | 60 | it('should set filter via setFilter', async () => { 61 | api.get('/thngs?filter=name%3DTest').reply(200, [{ name: 'Test' }]) 62 | const res = await scope.thng().setFilter('name=Test').read() 63 | 64 | expect(res).to.be.an('array') 65 | expect(res).to.have.length.gte(1) 66 | }) 67 | 68 | it('should set ids via setIds', async () => { 69 | const payload = [{ id: 'thngId1' }, { id: 'thngId2' }] 70 | api.get('/thngs?ids=thngId1%2CthngId2').reply(200, payload) 71 | const res = await scope 72 | .thng() 73 | .setIds(payload.map((p) => p.id)) 74 | .read() 75 | 76 | expect(res.length).to.equal(payload.length) 77 | }) 78 | 79 | it('should allow chaining of multiple param setters', async () => { 80 | api.get('/thngs?project=projectId&filter=name%3DTest&perPage=1&withScopes=true').reply(200, [ 81 | { 82 | name: 'Test', 83 | scopes: { 84 | project: [], 85 | users: ['all'] 86 | } 87 | } 88 | ]) 89 | const res = await scope 90 | .thng() 91 | .setProject('projectId') 92 | .setFilter('name=Test') 93 | .setPerPage(1) 94 | .setWithScopes() 95 | .read() 96 | 97 | expect(res).to.be.an('array') 98 | expect(res).to.have.length.gte(1) 99 | expect(res[0].scopes).to.be.an('object') 100 | }) 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /src/scope/Application.js: -------------------------------------------------------------------------------- 1 | import Scope from './Scope' 2 | import User from './User' 3 | import Product from '../entity/Product' 4 | import Action from '../entity/Action' 5 | import Place from '../entity/Place' 6 | import AppUser from '../entity/AppUser' 7 | import { mixinResources } from '../util/mixin' 8 | import api from '../api' 9 | import symbols from '../symbols' 10 | import isPlainObject from 'lodash-es/isPlainObject' 11 | 12 | /** 13 | * E-mail and password used to create a user into the platform. 14 | * 15 | * @typedef {Object} AccessCredentials 16 | * @param {string} email - E-mail used on registration 17 | * @param {string} password - Password defined on registration 18 | */ 19 | 20 | /** 21 | * Mixin with all the top-level Application resources. 22 | * 23 | * @mixin 24 | */ 25 | const ApplicationAccess = mixinResources([ 26 | Product, // R 27 | Action, // C scans 28 | AppUser, // C 29 | Place // R 30 | ]) 31 | 32 | /** 33 | * Application is the Scope with the least permissions. It is meant to be used 34 | * to create and authenticate application users. 35 | * 36 | * @extends Scope 37 | * @mixes ApplicationAccess 38 | */ 39 | export default class Application extends ApplicationAccess(Scope) { 40 | /** 41 | * Creates an instance of Application. 42 | * 43 | * @param {string} apiKey - API Key of scope 44 | * @param {Object} [data={}] - Optional application data 45 | */ 46 | constructor (apiKey, data = {}) { 47 | super(apiKey, data) 48 | 49 | this.initPromise = super 50 | .readAccess() 51 | .then((access) => { 52 | this.id = access.actor.id 53 | this.project = access.project 54 | this[symbols.path] = this._getPath() 55 | }) 56 | .then(() => this.read()) 57 | } 58 | 59 | /** 60 | * Read the application's data asynchronously 61 | * 62 | * @returns {Promise} 63 | */ 64 | init () { 65 | return this.initPromise 66 | } 67 | 68 | /** 69 | * Login user using EVRYTHNG credentials and create User scope on success. 70 | * 71 | * @param {AccessCredentials} credentials - User login credentials 72 | * @param {Function} callback - Error first callback 73 | * @returns {Promise.} - Authorized User scope 74 | */ 75 | async login (credentials, callback) { 76 | try { 77 | const user = await this._authenticateUser(credentials) 78 | const userScope = new User(user.access.apiKey, user) 79 | await userScope.init() 80 | 81 | if (callback) callback(null, userScope) 82 | return userScope 83 | } catch (err) { 84 | if (callback) callback(err) 85 | throw err 86 | } 87 | } 88 | 89 | // PRIVATE 90 | 91 | /** 92 | * Return application endpoint, nested within projects. 93 | * 94 | * @returns {string} 95 | * @private 96 | */ 97 | _getPath () { 98 | return '/applications/me' 99 | } 100 | 101 | /** 102 | * Validate user credentials. 103 | * 104 | * @param {AccessCredentials} credentials - User login credentials 105 | * @returns {Promise.} - User details with access 106 | * @private 107 | */ 108 | _authenticateUser (credentials) { 109 | if (!credentials || !isPlainObject(credentials)) { 110 | throw new TypeError('Credentials are missing.') 111 | } 112 | 113 | return api({ 114 | url: '/users/login', 115 | method: 'post', 116 | data: credentials, 117 | apiKey: this.apiKey 118 | }) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test/unit/outDated/scope/Operator.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | import Operator from '../../../src/scope/Operator' 3 | import symbols from '../../../src/symbols' 4 | import fetchMock from 'fetch-mock' 5 | import mockApi from '../../helpers/apiMock' 6 | import apiUrl from '../../helpers/apiUrl' 7 | import paths from '../../helpers/paths' 8 | import { operatorApiKey, operatorTemplate } from '../../helpers/data' 9 | 10 | let operator 11 | 12 | describe('Operator', () => { 13 | mockApi() 14 | 15 | describe('constructor', () => { 16 | describe('existent', () => { 17 | beforeEach(() => { 18 | spyOn(Operator.prototype, 'read') 19 | operator = new Operator(operatorApiKey) 20 | }) 21 | 22 | it('should read itself', (done) => { 23 | operator[symbols.init].then(() => { 24 | expect(Operator.prototype.read).toHaveBeenCalled() 25 | done() 26 | }) 27 | }) 28 | }) 29 | 30 | describe('non-existent', () => { 31 | beforeEach(() => { 32 | operator = new Operator('invalidKey') 33 | }) 34 | 35 | it('should throw error', (done) => { 36 | operator[symbols.init].catch(() => expect(true).toBe(true)).then(done) 37 | }) 38 | }) 39 | }) 40 | 41 | describe('read', () => { 42 | beforeEach((done) => { 43 | operator = new Operator(operatorApiKey) 44 | operator[symbols.init].then(done) 45 | }) 46 | 47 | it('should send get request to operator ID', (done) => { 48 | operator.read().then(() => { 49 | expect(fetchMock.lastUrl()).toEqual(apiUrl(paths.operator)) 50 | expect(fetchMock.lastOptions().method).toEqual('get') 51 | done() 52 | }) 53 | }) 54 | 55 | it('should fill operator scope with details', (done) => { 56 | operator.read().then((response) => { 57 | expect(operator).toBe(response) 58 | expect(operator).toEqual(jasmine.objectContaining(operatorTemplate)) 59 | done() 60 | }) 61 | }) 62 | }) 63 | 64 | describe('update', () => { 65 | beforeEach((done) => { 66 | operator = new Operator(operatorApiKey) 67 | operator[symbols.init].then(done) 68 | }) 69 | 70 | it('should send get request to operator ID', (done) => { 71 | operator.update(operatorTemplate).then(() => { 72 | expect(fetchMock.lastUrl()).toEqual(apiUrl(paths.operator)) 73 | expect(fetchMock.lastOptions().method).toEqual('put') 74 | done() 75 | }) 76 | }) 77 | 78 | it('should fill operator scope with details', (done) => { 79 | operator.update(operatorTemplate).then((response) => { 80 | expect(operator).toBe(response) 81 | expect(operator).toEqual(jasmine.objectContaining(operatorTemplate)) 82 | done() 83 | }) 84 | }) 85 | }) 86 | 87 | describe('access', () => { 88 | const operatorResources = [ 89 | 'product', 90 | 'thng', 91 | 'collection', 92 | 'action', 93 | 'actionType', 94 | 'project', 95 | 'user', 96 | 'batch', 97 | 'place', 98 | 'file' 99 | ] 100 | 101 | beforeEach((done) => { 102 | operator = new Operator(operatorApiKey) 103 | operator[symbols.init].then(done) 104 | }) 105 | 106 | operatorResources.forEach((resource) => { 107 | it(`should have ${resource} resource`, () => { 108 | expect(operator[resource]).toBeDefined() 109 | }) 110 | }) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /src/entity/Redirection.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity' 2 | import Resource from '../resource/Resource' 3 | import Scope from '../scope/Scope' 4 | import settings from '../settings' 5 | 6 | /** 7 | * Represents a Redirection entity. 8 | * 9 | * @extends Entity 10 | */ 11 | export default class Redirection extends Entity { 12 | /** 13 | * Return simple resource factory for Redirections. 14 | * 15 | * @static 16 | * @return {{redirection: Function}} 17 | */ 18 | static resourceFactory () { 19 | return { 20 | redirection (shortDomain = settings.defaultShortDomain) { 21 | // Only allowed on Entities and Resources. 22 | if (this instanceof Scope) { 23 | throw new Error('Redirection is not a top-level resource.') 24 | } 25 | 26 | const _this = this 27 | 28 | /** 29 | * Helper for repetitive shortDomain focussed requests. 30 | * 31 | * @param {object} changes - Additional options. 32 | * @returns {Promise} API response. 33 | */ 34 | const shortDomainRequest = async (changes) => 35 | Resource.prototype._request.call( 36 | _this, 37 | Object.assign( 38 | { 39 | apiUrl: `https://${shortDomain}`, 40 | url: '/redirections', 41 | headers: { 42 | Accept: 'application/json' 43 | } 44 | }, 45 | changes 46 | ) 47 | ) 48 | 49 | // Special case: use the shortDomain API instead of the /redirector API. 50 | return { 51 | /** 52 | * Create the redirection. 53 | * 54 | * @param {object} payload - Redirection payload. 55 | */ 56 | async create (payload) { 57 | payload.evrythngId = _this.id 58 | payload.type = _this.typeName 59 | 60 | return shortDomainRequest({ 61 | method: 'post', 62 | body: JSON.stringify(payload) 63 | }) 64 | }, 65 | 66 | /** 67 | * Read the redirection. 68 | */ 69 | async read () { 70 | const [first] = await shortDomainRequest({ 71 | params: { evrythngId: _this.id } 72 | }) 73 | return first 74 | }, 75 | 76 | /** 77 | * Update the redirection. If it doesn't exist, it is created. 78 | * 79 | * @param {object} payload - Redirection update payload. 80 | */ 81 | async update (payload) { 82 | const existing = await this.read() 83 | if (!existing) { 84 | return this.create(payload) 85 | } 86 | 87 | // Else update it 88 | return shortDomainRequest({ 89 | url: `/redirections/${existing.shortId}`, 90 | method: 'put', 91 | body: JSON.stringify(payload) 92 | }) 93 | }, 94 | 95 | /** 96 | * Delete the redirection, if it exists. 97 | */ 98 | async delete () { 99 | const existing = await this.read() 100 | if (!existing) { 101 | return Promise.resolve() 102 | } 103 | 104 | // Else delete it 105 | return shortDomainRequest({ 106 | url: `/redirections/${existing.shortId}`, 107 | method: 'delete' 108 | }) 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evrythng", 3 | "version": "6.0.5", 4 | "description": "Official Javascript SDK for the EVRYTHNG API.", 5 | "main": "./dist/evrythng.node.js", 6 | "scripts": { 7 | "lint": "eslint --fix --max-warnings 0 --ext .js .", 8 | "format": "./node_modules/.bin/prettier --write '**/*.{js,json,md,yaml,yml}'", 9 | "build": "webpack --config webpack.config.js --mode production", 10 | "build-dev": "webpack --config webpack.config.js --mode development", 11 | "show-notice": "echo '\n New to evrythng.js v6? Make sure to read the migration guide:\n https://developers.evrythng.com/docs/evrythngjs-v600\n'", 12 | "postinstall": "npm run --silent show-notice", 13 | "test": "mocha test/integration/**.spec.js", 14 | "test:unit": "mocha --require esm test/unit/**.spec.js", 15 | "test:e2e": "mocha test/e2e/**.spec.js", 16 | "prepublishOnly": "npm run build && npm test" 17 | }, 18 | "husky": { 19 | "hooks": { 20 | "pre-commit": "lint-staged -p false" 21 | } 22 | }, 23 | "lint-staged": { 24 | "*.{js,json,md,yaml,yml}": "prettier --write", 25 | "*.js": "eslint --fix" 26 | }, 27 | "prettier": { 28 | "printWidth": 100, 29 | "singleQuote": true, 30 | "trailingComma": "all" 31 | }, 32 | "eslintConfig": { 33 | "env": { 34 | "browser": true, 35 | "commonjs": true, 36 | "mocha": true 37 | }, 38 | "extends": [ 39 | "standard" 40 | ], 41 | "parserOptions": { 42 | "ecmaVersion": 12 43 | }, 44 | "rules": { 45 | "eqeqeq": [ 46 | 0, 47 | "smart" 48 | ], 49 | "no-unused-expressions": "off", 50 | "no-prototype-builtins": "off", 51 | "prefer-promise-reject-errors": "off", 52 | "operator-linebreak": "off" 53 | } 54 | }, 55 | "eslintIgnore": [ 56 | "node_modules", 57 | "reports", 58 | "mochawesome-report", 59 | ".nyc_output", 60 | "dist" 61 | ], 62 | "repository": { 63 | "type": "git", 64 | "url": "git+https://github.com/evrythng/evrythng.js.git" 65 | }, 66 | "keywords": [ 67 | "evrythng", 68 | "iot", 69 | "wot", 70 | "internet of things", 71 | "web of things" 72 | ], 73 | "author": "EVRYTHNG Ltd. ", 74 | "license": "Apache-2.0", 75 | "bugs": { 76 | "url": "https://github.com/evrythng/evrythng.js/issues" 77 | }, 78 | "homepage": "https://github.com/evrythng/evrythng.js#readme", 79 | "dependencies": { 80 | "@babel/runtime": "^7.4.3", 81 | "cross-fetch": "^3.0.6", 82 | "node-fetch": "^2.6.1" 83 | }, 84 | "devDependencies": { 85 | "@babel/core": "^7.4.3", 86 | "@babel/plugin-transform-runtime": "^7.4.3", 87 | "@babel/preset-env": "^7.11.5", 88 | "acorn": "^6.4.1", 89 | "ajv": "^6.10.0", 90 | "babel-loader": "^8.0.5", 91 | "chai": "^4.2.0", 92 | "chai-as-promised": "^7.1.1", 93 | "chance": "^1.1.7", 94 | "eslint": "^7.14.0", 95 | "eslint-config-standard": "^16.0.2", 96 | "eslint-plugin-import": "^2.22.1", 97 | "eslint-plugin-node": "^11.1.0", 98 | "eslint-plugin-promise": "^4.2.1", 99 | "esm": "^3.2.25", 100 | "fetch-mock": "^9.10.7", 101 | "husky": "^4.3.0", 102 | "lint-staged": "^10.5.2", 103 | "lodash-es": "^4.17.15", 104 | "mocha": "^9.1.3", 105 | "nock": "^11.3.3", 106 | "prettier": "^2.2.1", 107 | "webpack": "^5.65.0", 108 | "webpack-cli": "^4.9.1" 109 | }, 110 | "standard": { 111 | "globals": [ 112 | "fetch", 113 | "Response", 114 | "FormData" 115 | ], 116 | "env": [ 117 | "mocha" 118 | ] 119 | } 120 | } 121 | --------------------------------------------------------------------------------