├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── 32px.png ├── ACKNOWLEDGEMENTS.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── examples └── using-jquery-for-ajax.js ├── package.json ├── src ├── Oid.js ├── Oid.specs.js ├── connectors │ ├── axiosConnector.js │ └── jqueryConnector.js ├── createMeta.js ├── createMeta.specs.js ├── getV1Urls.js ├── getV1Urls.specs.js ├── index.js ├── index.specs.js ├── transformDataToAsset.js └── transformDataToAsset.specs.js └── wallaby.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-2" 5 | ], 6 | "env": { 7 | "test": { 8 | "plugins": [ 9 | "rewire" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | 8 | [{package.json,.babelrc,.eslintrc}] 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | /examples 3 | /dist -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "ecmaFeatures": { 9 | "arrowFunctions": true, 10 | "blockBindings": true, 11 | "classes": true, 12 | "defaultParams": true, 13 | "destructuring": true, 14 | "forOf": true, 15 | "generators": true, 16 | "modules": true, 17 | "objectLiteralComputedProperties": true, 18 | "objectLiteralDuplicateProperties": false, 19 | "objectLiteralShorthandMethods": true, 20 | "objectLiteralShorthandProperties": true, 21 | "spread": true, 22 | "superInFunctions": true, 23 | "templateStrings": true, 24 | "jsx": true 25 | }, 26 | "rules": { 27 | "strict": [ 28 | 2, 29 | "never" 30 | ], 31 | "arrow-parens": [ 32 | 2, 33 | "always" 34 | ], 35 | "no-var": 2, 36 | "prefer-const": 2, 37 | "no-shadow": 2, 38 | "no-shadow-restricted-names": 2, 39 | "no-undef": 2, 40 | "no-unused-vars": [ 41 | 2, 42 | { 43 | "vars": "local", 44 | "args": "after-used" 45 | } 46 | ], 47 | "no-use-before-define": 0, 48 | "comma-dangle": 0, 49 | "no-cond-assign": [ 50 | 2, 51 | "always" 52 | ], 53 | "no-console": 1, 54 | "no-debugger": 1, 55 | "no-alert": 1, 56 | "no-constant-condition": 1, 57 | "no-dupe-keys": 2, 58 | "no-duplicate-case": 2, 59 | "no-empty": 2, 60 | "no-ex-assign": 2, 61 | "no-extra-boolean-cast": 0, 62 | "no-extra-semi": 2, 63 | "no-func-assign": 2, 64 | "no-inner-declarations": 2, 65 | "no-invalid-regexp": 2, 66 | "no-irregular-whitespace": 2, 67 | "no-obj-calls": 2, 68 | "no-sparse-arrays": 2, 69 | "no-unreachable": 2, 70 | "use-isnan": 2, 71 | "block-scoped-var": 2, 72 | "consistent-return": 2, 73 | "curly": [ 74 | 2, 75 | "multi-line" 76 | ], 77 | "default-case": 2, 78 | "dot-notation": [ 79 | 2, 80 | { 81 | "allowKeywords": true 82 | } 83 | ], 84 | "eqeqeq": 2, 85 | "guard-for-in": 2, 86 | "no-caller": 2, 87 | "no-else-return": 2, 88 | "no-eq-null": 2, 89 | "no-eval": 2, 90 | "no-extend-native": 2, 91 | "no-extra-bind": 2, 92 | "no-fallthrough": 2, 93 | "no-floating-decimal": 2, 94 | "no-implied-eval": 2, 95 | "no-lone-blocks": 2, 96 | "no-loop-func": 2, 97 | "no-multi-str": 2, 98 | "no-native-reassign": 2, 99 | "no-new": 2, 100 | "no-new-func": 2, 101 | "no-new-wrappers": 2, 102 | "no-octal": 2, 103 | "no-octal-escape": 2, 104 | "no-param-reassign": 2, 105 | "no-proto": 2, 106 | "no-redeclare": 2, 107 | "no-return-assign": 2, 108 | "no-script-url": 2, 109 | "no-self-compare": 2, 110 | "no-sequences": 2, 111 | "no-throw-literal": 2, 112 | "no-with": 2, 113 | "radix": 2, 114 | "vars-on-top": 2, 115 | "wrap-iife": [ 116 | 2, 117 | "any" 118 | ], 119 | "yoda": 2, 120 | "indent": 2, 121 | "brace-style": [ 122 | 2, 123 | "1tbs", 124 | { 125 | "allowSingleLine": true 126 | } 127 | ], 128 | "quotes": [ 129 | 2, 130 | "single", 131 | "avoid-escape" 132 | ], 133 | "camelcase": [ 134 | 2, 135 | { 136 | "properties": "never" 137 | } 138 | ], 139 | "comma-spacing": [ 140 | 2, 141 | { 142 | "before": false, 143 | "after": true 144 | } 145 | ], 146 | "comma-style": [ 147 | 2, 148 | "last" 149 | ], 150 | "eol-last": 2, 151 | "func-names": 0, 152 | "key-spacing": [ 153 | 2, 154 | { 155 | "beforeColon": false, 156 | "afterColon": true 157 | } 158 | ], 159 | "new-cap": [ 160 | 2, 161 | { 162 | "newIsCap": true 163 | } 164 | ], 165 | "no-multiple-empty-lines": [ 166 | 2, 167 | { 168 | "max": 2 169 | } 170 | ], 171 | "no-mixed-spaces-and-tabs": [ 172 | 2 173 | ], 174 | "no-nested-ternary": 2, 175 | "no-new-object": 2, 176 | "no-spaced-func": 2, 177 | "no-trailing-spaces": 2, 178 | "no-extra-parens": [ 179 | 2, 180 | "functions" 181 | ], 182 | "no-underscore-dangle": 0, 183 | "one-var": [ 184 | 2, 185 | "never" 186 | ], 187 | "padded-blocks": [ 188 | 2, 189 | "never" 190 | ], 191 | "semi": [ 192 | 2, 193 | "always" 194 | ], 195 | "semi-spacing": [ 196 | 2, 197 | { 198 | "before": false, 199 | "after": true 200 | } 201 | ], 202 | "space-after-keywords": 2, 203 | "space-before-blocks": 2, 204 | "space-before-function-paren": [ 205 | 2, 206 | "never" 207 | ], 208 | "space-infix-ops": 2, 209 | "space-return-throw-case": 2, 210 | "spaced-comment": [ 211 | 0, 212 | "always", 213 | { 214 | "exceptions": [ 215 | "-", 216 | "+" 217 | ], 218 | "markers": [ 219 | "=", 220 | "!" 221 | ] 222 | } 223 | ] 224 | } 225 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea 3 | 4 | # dependencies 5 | node_modules 6 | 7 | *.log 8 | 9 | # Built Output 10 | /dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea 3 | 4 | # dependencies 5 | node_modules 6 | 7 | *.log 8 | /src 9 | **/*.specs.js 10 | /examples 11 | -------------------------------------------------------------------------------- /32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versionone/VersionOne.SDK.JavaScript/22c4c47cb5b659deb08258db7176e1c9a33b8cda/32px.png -------------------------------------------------------------------------------- /ACKNOWLEDGEMENTS.md: -------------------------------------------------------------------------------- 1 | ## Acknowledgements 2 | Base64 encoding and decoding support is provided by base64.js, which is open source software, Copyright (c) 2007, David Lindquist . 3 | 4 | The original software is available from: 5 | http://www.stringify.com/static/js/base64.js 6 | 7 | This software is available under the MIT License: 8 | http://opensource.org/licenses/MIT 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to VersionOne SDK.JavaScript Library 2 | 3 | 1. [Getting Involved](#getting-involved) 4 | 2. [Reporting Bugs](#reporting-bugs) 5 | 3. [Improving Documentation](#improving-documentation) 6 | 4. [Contributing Code](#contributing-code) 7 | 8 | ## Getting Involved 9 | 10 | We need your help to make the VersionOne SDK.JavaScript Library a useful tool for developing integrations and complementary applications. While third-party patches are absolutely essential, they are not the only way to get involved. You can help the project by discovering and [reporting bugs](#reporting-bugs), [improving documentation](#improving-documentation), and helping others on the [versionone-dev group](http://groups.google.com/group/versionone-dev/) and [GitHub issues](https://github.com/versionone/VersionOne.SDK.JavaScript/issues). 11 | 12 | ## Reporting Bugs 13 | 14 | Before reporting a bug on the project's [issues page](https://github.com/versionone/VersionOne.SDK.JavaScript/issues), first make sure that your issue is caused by the Library, not your application code (e.g. passing incorrect arguments to methods, etc.). Second, search the already reported issues for similar cases, and if it has been reported already, just add any additional details in the comments. 15 | 16 | After you made sure that you have found a new bug, here are some tips for creating a helpful report that will make fixing it much easier and quicker: 17 | 18 | * Write a **descriptive, specific title**. Bad: *Problem with filtering*. Good: *Scope.Workitems always returns an empty list*. 19 | * Whenever possible, include **Function** info in the description. 20 | * Create a **simple test case** that demonstrates the bug. 21 | 22 | ## Improving Documentation 23 | 24 | All documentation and examples are located in the `gh-pages` branch. The easiest way to make little improvements such as fixing typos without even leaving the browser is by editing one of the files with the online GitHub editor: browse the [gh-pages branch](https://github.com/VersionOne/VersionOne.SDK.JavaScript/tree/gh-pages), choose a certain file for editing, click the Edit button, make changes and follow instructions from there. Once it gets merged, the changes will immediately appear on the website. 25 | 26 | If you need to make edits in a local repository to see how it looks in the process, do the following: 27 | 28 | 1. [Install Ruby](http://www.ruby-lang.org/en/) if do not have it yet. 29 | 2. Run `gem install jekyll`. 30 | 3. Run `jekyll --auto` inside the `VersionOne.SDK.JavaScript` folder. 31 | 4. Open the website from the `_site` folder. 32 | 33 | Now any file changes will be reflected on the generated pages automatically. After commiting the changes, just send a pull request. 34 | 35 | If you need to update documentation according to a new feature that only appeared in the master version (not stable one), you need to make changes to `gh-pages-master` branch instead of `gh-pages`. It will get merged into the latter when released as stable. 36 | 37 | ## Contributing Code 38 | 39 | ### Making Changes to Source 40 | 41 | If you are not yet familiar with the way GitHub works (forking, pull requests, etc.), be sure to read [the article about forking](https://help.github.com/articles/fork-a-repo) on the GitHub Help website — it will get you started quickly. 42 | 43 | You should always write each batch of changes (feature, bugfix, etc.) in **its own topic branch**. Please do not commit to the `master` branch, or your unrelated changes will go into the same pull request. 44 | 45 | You should also follow the code style and whitespace conventions of the original codebase. 46 | 47 | ### Considerations for Accepting Patches 48 | 49 | Before sending a pull request with a new feature, first check if it has been discussed before already (either on [GitHub issues](https://github.com/versionone/VersionOne.SDK.JavaScript/issues). If your feature or API improvement did get merged into master, please consider submitting another pull request with the corresponding [documentation update](#improving-documentation). 50 | 51 | ### Open Source Licenses and Attribution 52 | Regardless of whether attribution is required by included code or a dependency, we want to acknowledge the work that VersionOne.SDK.JavaScript depends on and make it easy for people to evaluate the legal implications of using this library. Therefore, all dependencies should be attributed in the ACKNOWLEDGEMENTS.md. This should include the persons or organizations who contributed the libraries, a link to the source code, and a link to the underlying license. 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # VersionOne SDK.JavaScript License 2 | Copyright (c) 2012 VersionOne, Inc. 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | * Neither the name of VersionOne, Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission of 17 | VersionOne, Inc. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 20 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 21 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 22 | MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 24 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 26 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 29 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 30 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 31 | SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gitter](https://badges.gitter.im/versionone/VersionOne.SDK.JavaScript.svg)](https://gitter.im/versionone/VersionOne.SDK.JavaScript?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 2 | 3 | # VersionOne JavaScript SDK 4 | 5 | The VersionOne JavaScript SDK is an open-source and community supported JavaScript client for the VersionOne API. The SDK simplifies the creation of server-side JavaScript integrations (i.e. node/express server) with the VersionOne platform. 6 | 7 | As an "open-sourced and community supported" product, the VersionOne JavaScript SDK is not formally supported by VersionOne. That said, there are a number of options for getting your questions addressed: 8 | 9 | * [StackOverflow](http://stackoverflow.com/questions/tagged/versionone): For asking questions of the VersionOne Development Community. 10 | * [GitHub Issues](https://github.com/versionone/VersionOne.SDK.JavaScript/issues): For submitting issues that others may try to address. 11 | * [![Gitter](https://badges.gitter.im/versionone/VersionOne.SDK.JavaScript.svg)](https://gitter.im/versionone/VersionOne.SDK.JavaScript?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge): For participating in the development of the SDK and chatting with other developers. 12 | 13 | In general, StackOverflow is your best option for getting support for the VersionOne JavaScript SDK. 14 | 15 | The source code for the VersionOne JavaScript SDK is free and open-source, and we encourage you to improve it by [submitting pull requests](https://help.github.com/articles/using-pull-requests)! 16 | 17 | # Getting Started 18 | 19 | Please note: 20 | * 2.x.x SDK is only supported with a VersionOne instance 17.1 or later. 21 | * 1.x.x SDK is only supported with a VersionOne instance 15.3-17.0 22 | * 1.x.x SDK currently does not support querying for Meta definitions; if this is something needed, please use any 0.x.x 23 | version. 24 | 25 | **See the repo's Wiki for API usage and additional information.** 26 | 27 | ## Installation via NPM 28 | 29 | `npm install v1sdk` 30 | 31 | ### jQuery example 32 | ```javascript 33 | import $ from 'jquery'; 34 | import sdk, {jqueryConnector} from 'v1sdk'; 35 | 36 | const jqueryConnectedSdk = jqueryConnector($)(sdk); 37 | const v1 = jqueryConnectedSdk('www14.v1host.com', 'v1sdktesting', 443, true) 38 | .withCreds('admin', 'admin'); // usage with username/password 39 | // .withAccessToken('your token'); // usage with access tokens 40 | // .withImplicitAuth(); // let the browser do its thing 41 | 42 | v1.create('Story', {estimate: 5, status: 'Not Started'}) 43 | .then((story) => v1.update(story.oidToken, {estimate: 7})) 44 | .then(v1.query({ 45 | from: 'Story', 46 | select: ['Estimate', 'Status'], 47 | where: { 48 | Status: 'Not Started' 49 | } 50 | })) 51 | .then(console.log) 52 | .catch(console.log); 53 | ``` 54 | 55 | ## More Examples 56 | Additional examples are available in the [examples](/examples) folder. 57 | 58 | ## Client Side Integrations 59 | As stated above, the VersionOne JavaScript SDK is intended for server-side integration. By default client-side integration is not possible because browsers only allow scripts to interact with web pages/applications at the same origin. This restriction, known as the [same-origin policy (SOP)](https://en.wikipedia.org/wiki/Same-origin_policy), is intended to prevent malicious scripts from accessing sensitive data. 60 | 61 | The SOP can be overridden using a mechanism known as [Cross-origin resource sharing (CORS)](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing). Enabling CORS opens a hole in the SOP and permits scripts to view data from another origin. This defeats the security measures intended to prevent a malicious attack. 62 | 63 | Enabling CORS is possible - *but not recommended* - for both hosted and on-premise installations of VersionOne. 64 | 65 | If you are interested in enabling CORS in your on-premise instance you need to include an entry for ```CorsAllowedOrigins``` in your user.confg file. The value attribute should contain the list of valid domains. Only domains in this list will be allowed to make cross-origin requets. Separate domains names with a comma. 66 | 67 | Here is an example user.config file with CORS enabled for a single domain 68 | ```xml 69 | 70 | 71 | 72 | 73 | ``` 74 | 75 | Here is an example user.config file with CORS enabled for two domains 76 | ```xml 77 | 78 | 79 | 80 | 81 | ``` 82 | 83 | If you are interested in enabling CORS for a hosted instance of VersionOne, please contact your system administrator and ask them to email VersionOne support requesting this change. This email needs to include the list of domains you would like permitted. Because this change has security implications, we cannot accept requests from anyone. 84 | 85 | ## Other Resources 86 | 87 | * [ACKNOWLEDGEMENTS.md](https://github.com/versionone/VersionOne.SDK.JavaScript/blob/master/ACKNOWLEDGEMENTS.md) - Acknowledgments of included software and associated licenses 88 | * [LICENSE.md](https://github.com/versionone/VersionOne.SDK.NET.APIClient/blob/master/LICENSE.md) - License for source code and redistribution 89 | * [CONTRIBUTING.md](https://github.com/versionone/VersionOne.SDK.JavaScript/blob/master/CONTRIBUTING.md) - Guidelines and information on contributing to this project 90 | 91 | ### Getting Help 92 | Need to bootstrap on VersionOne SDK.JavaScript quickly? VersionOne services brings a wealth of development experience to training and mentoring: 93 | 94 | http://www.versionone.com/training/product_training_services/ 95 | 96 | Not into the chat thing? Get help from the community of VersionOne developers: 97 | 98 | http://groups.google.com/group/versionone-dev/ 99 | -------------------------------------------------------------------------------- /examples/using-jquery-for-ajax.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import sdk, {jqueryConnector} from 'v1sdk'; 3 | 4 | const jqueryConnectedSdk = jqueryConnector($)(sdk); 5 | const v1 = jqueryConnectedSdk('www14.v1host.com', 'v1sdktesting', 443, true) 6 | .withCreds('admin', 'admin'); // usage with username/password 7 | // .withAccessToken('your token'); // usage with access tokens 8 | 9 | v1.create('Story', {estimate: 5, status: 'Not Started'}) 10 | .then((story) => v1.update(story.oidToken, {estimate: 7})) 11 | .then(v1.query({ 12 | from: 'Story', 13 | select: ['Estimate', 'Status'], 14 | where: { 15 | Status: 'Not Started' 16 | } 17 | })) 18 | .then(console.log) 19 | .catch(console.log); 20 | 21 | // Retrieve a description of all the Attributes, Operations for a given AssetType 22 | v1.queryDefinition('Story') 23 | .then(console.log) 24 | .catch(console.log); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v1sdk", 3 | "version": "2.3.0", 4 | "description": "VersionOne API Client for JavaScript", 5 | "license": "MIT", 6 | "keywords": [ 7 | "VersionOne", 8 | "V1", 9 | "SDK" 10 | ], 11 | "homepage": "https://github.com/versionone/VersionOne.SDK.JavaScript", 12 | "main": "./dist/index.js", 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:versionone/VersionOne.SDK.JavaScript.git" 16 | }, 17 | "scripts": { 18 | "clean": "rm -rf dist", 19 | "prebuild": "npm run lint && npm run clean", 20 | "build": "babel src -d dist --ignore *.specs.js", 21 | "lint": "eslint ./src", 22 | "test": "./node_modules/.bin/better-npm-run test", 23 | "prepublish": "npm test && npm run build" 24 | }, 25 | "betterScripts": { 26 | "test": { 27 | "command": "mocha src/**/*.specs.js --compilers js:babel-core/register --recursive", 28 | "env": { 29 | "NODE_ENV": "test" 30 | } 31 | }, 32 | "build:prod": { 33 | "command": "webpack --config $npm_package_webpack --progress --colors", 34 | "env": { 35 | "NODE_ENV": "production" 36 | } 37 | } 38 | }, 39 | "dependencies": { 40 | "btoa": "1.1.2", 41 | "invariant": "2.2.0" 42 | }, 43 | "devDependencies": { 44 | "babel": "6.3.13", 45 | "babel-cli": "6.3.17", 46 | "babel-core": "6.3.21", 47 | "babel-eslint": "^6.1.0", 48 | "babel-plugin-rewire": "1.0.0-beta-3", 49 | "babel-polyfill": "^6.9.0", 50 | "babel-preset-es2015": "6.3.13", 51 | "babel-preset-stage-2": "6.3.13", 52 | "babel-runtime": "6.3.19", 53 | "better-npm-run": "0.0.8", 54 | "chai": "3.4.1", 55 | "chai-as-promised": "5.2.0", 56 | "escope": "^3.3.0", 57 | "eslint": "1.10.3", 58 | "mocha": "2.3.4", 59 | "sinon": "2.0.0-pre", 60 | "wallaby-webpack": "0.0.22", 61 | "webpack": "^1.13.1" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Oid.js: -------------------------------------------------------------------------------- 1 | export const InvalidOidToken = class { 2 | constructor(message) { 3 | this.name = this.constructor.name; 4 | this.message = message; 5 | if (typeof Error.captureStackTrace === 'function') { 6 | Error.captureStackTrace(this, this.constructor); 7 | } else { 8 | this.stack = (new Error(message)).stack; 9 | } 10 | } 11 | }; 12 | InvalidOidToken.prototype = Object.create(Error.prototype); 13 | 14 | export default class { 15 | constructor(oidToken) { 16 | const oidParts = oidToken.split(':'); 17 | if (/[1-9][0-9]*/.exec(oidParts[1]) === null) { 18 | throw new InvalidOidToken(oidToken); 19 | } 20 | this.type = oidParts[0]; 21 | this.idNumber = parseInt(oidParts[1], 10); 22 | } 23 | 24 | get assetType() { 25 | return this.type; 26 | } 27 | 28 | get number() { 29 | return this.idNumber; 30 | } 31 | 32 | toString() { 33 | return `${this.assetType}:${this.number}`; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Oid.specs.js: -------------------------------------------------------------------------------- 1 | import Sut, {InvalidOidToken} from './Oid'; 2 | 3 | describe('src/Oid', function() { 4 | beforeEach(() => { 5 | this.actual = undefined; 6 | }); 7 | describe('given a valid Oid Token', () => { 8 | beforeEach(() => { 9 | this.oidToken = 'Member:20'; 10 | }); 11 | describe('when creating an Oid from the Oid Token', () => { 12 | beforeEach(() => { 13 | this.actual = new Sut(this.oidToken); 14 | }); 15 | it('it should return an Oid with an asset type', () => { 16 | this.actual.assetType.should.equal('Member'); 17 | }); 18 | it('it should return a read-only asset type property', () => { 19 | (() => this.actual.assetType = 'Story').should.throw(); 20 | }); 21 | it('it should return an Oid with a ID number', () => { 22 | this.actual.number.should.equal(20); 23 | }); 24 | it('it should return a read-only ID number property', () => { 25 | (() => this.actual.number = 25).should.throw(); 26 | }); 27 | }); 28 | 29 | describe('when toString is called', () => { 30 | beforeEach(() => { 31 | this.actual = new Sut(this.oidToken).toString(); 32 | }); 33 | it('it should return an Oid Token for the Oid', () => { 34 | this.actual.should.equal('Member:20'); 35 | }); 36 | }); 37 | }); 38 | 39 | describe('given an invalid Oid Token', () => { 40 | beforeEach(() => { 41 | this.oidToken = 'Member:Username'; 42 | }); 43 | describe('when creating an Oid from the Oid token', () => { 44 | beforeEach(() => { 45 | this.fn = () => this.actual = new Sut(this.oidToken); 46 | }); 47 | it('it should throw an InvalidOidToken error', () => { 48 | this.fn.should.throw(InvalidOidToken); 49 | }); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/connectors/axiosConnector.js: -------------------------------------------------------------------------------- 1 | export default (axios) => (sdk) => sdk( 2 | (url, data, headers) =>axios({ 3 | method: 'POST', 4 | url, 5 | data, 6 | headers 7 | }), 8 | (url, data, headers) => axios({ 9 | method: 'GET', 10 | url, 11 | params: data, 12 | headers 13 | }) 14 | ); 15 | 16 | -------------------------------------------------------------------------------- /src/connectors/jqueryConnector.js: -------------------------------------------------------------------------------- 1 | export default ($) => (sdk) => sdk( 2 | (url, data, headers) => new Promise((resolve, reject) => $.ajax(url, { 3 | method: 'POST', 4 | data: JSON.stringify(data), 5 | dataType: 'json', 6 | headers, 7 | success: resolve, 8 | error: reject 9 | })), 10 | (url, data, headers) => new Promise((resolve, reject) => $.ajax(url, { 11 | method: 'GET', 12 | data: JSON.stringify(data), 13 | headers, 14 | success: resolve, 15 | error: reject 16 | })) 17 | ); 18 | -------------------------------------------------------------------------------- /src/createMeta.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import transformDataToAsset from './transformDataToAsset'; 3 | import getV1Urls from './getV1Urls'; 4 | import Oid from './Oid'; 5 | 6 | export default ({hostname, instance, protocol, port, token, postFn, getFn, isBasic}) => { 7 | const urls = getV1Urls(hostname, instance, protocol, port); 8 | const headers = createHeaderObj(token, isBasic); 9 | 10 | return { 11 | create(assetType, assetData) { 12 | invariant(assetType, `Error: there was no \`assetType\` provided to create`); 13 | invariant(assetData, `Error: there was no \`assetAttributeData\` provided to create`); 14 | invariant(Object.keys(assetData).length, `Error: there was no \`assetAttributeData\` provided to create`); 15 | const postData = transformDataToAsset(assetData); 16 | const url = `${urls.rest}/${assetType}`; 17 | return postFn(url, postData, headers); 18 | }, 19 | 20 | update(oidToken, assetData, changeComment) { 21 | invariant(oidToken, `Error: there was no \`oidToken\` provided to update`); 22 | invariant(assetData, `Error: there was no \`assetAttributeData\` provided to update`); 23 | invariant(Object.keys(assetData).length, `Error: there was no \`assetAttributeData\` provided to update`); 24 | const oid = new Oid(oidToken); 25 | const postData = transformDataToAsset(assetData); 26 | const comment = changeComment ? `?comment=${encodeURIComponent(changeComment)}` : ''; 27 | const url = `${urls.rest}/${oid.assetType}/${oid.number}${comment}`; 28 | return postFn(url, postData, headers); 29 | }, 30 | 31 | query(queryObj) { 32 | invariant(queryObj.from, `Error: there was no \`from\` property on provided query: ${queryObj}`); 33 | invariant(queryObj.select, `Error: there was no \`select\` property on provided query: ${queryObj}`); 34 | invariant(Array.isArray(queryObj.select), `Error: \`select\` property must be an Array on provided query: ${queryObj}`); 35 | invariant(queryObj.select.length, `Error: \`select\` property must contain values on provided query: ${queryObj}`); 36 | const url = urls.query; 37 | return postFn(url, queryObj, headers); 38 | }, 39 | 40 | executeOperation(oidToken, operationName) { 41 | invariant(oidToken, `Error: there was no \`oidToken\` provided to execute operation`); 42 | invariant(operationName, `Error: there was no \`operationName\` provided to execute operation`); 43 | const oid = new Oid(oidToken); 44 | const url = `${urls.rest}/${oid.assetType}/${oid.number}?op=${operationName}`; 45 | return postFn(url, null, headers); 46 | }, 47 | 48 | queryDefinition(assetType) { 49 | const queryAssetType = assetType ? assetType : ''; 50 | const url = `${urls.meta}/${queryAssetType}`; 51 | return getFn(url, null, headers); 52 | }, 53 | 54 | getActivityStream(oidToken) { 55 | invariant(oidToken, `Error: there was no \`oidToken\` provided to execute operation`); 56 | const url = `${urls.activityStream}/${oidToken}`; 57 | return getFn(url, null, headers); 58 | } 59 | }; 60 | }; 61 | 62 | const createHeaderObj = (token, isBasic) => { 63 | const headers = { 64 | Accept: 'application/json', 65 | 'Content-Type': 'application/json', 66 | }; 67 | 68 | 69 | const hasToken = Boolean(token); 70 | const hasType = typeof isBasic !== 'undefined'; 71 | 72 | if (hasToken || hasType) { 73 | invariant(hasToken, `Error: there was no \`token\` provided to the SDK`); 74 | invariant(hasType, `Error: there was no \`isBasic\` provided to the SDK`); 75 | 76 | headers['Authorization'] = `${isBasic ? 'Basic' : 'Bearer'} ${token}`; // eslint-disable-line dot-notation 77 | } 78 | 79 | return headers; 80 | }; 81 | -------------------------------------------------------------------------------- /src/createMeta.specs.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import createMeta, {__RewireAPI__ as RewireApi} from './createMeta'; 4 | import sinon from 'sinon'; 5 | chai.use(chaiAsPromised); 6 | chai.should(); 7 | 8 | describe('src/meta', function() { 9 | beforeEach(() => { 10 | this.actual = undefined; 11 | }); 12 | describe('given a required meta creation information', () => { 13 | beforeEach(() => { 14 | this.postFn = sinon.stub(); 15 | this.getFn = sinon.stub(); 16 | }); 17 | describe('when creating a meta object', () => { 18 | beforeEach(() => { 19 | this.actual = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, token: 'token', postFn: this.postFn, getFn: this.getFn, isBasic: false }); 20 | }); 21 | it('it should return an object with a create asset function', () => { 22 | this.actual.create.should.be.a('function'); 23 | }); 24 | it('it should return an object with an update asset function', () => { 25 | this.actual.update.should.be.a('function'); 26 | }); 27 | it('it should return an object with a query function', () => { 28 | this.actual.query.should.be.a('function'); 29 | }); 30 | it('it should return an object with an execution operation function', () => { 31 | this.actual.executeOperation.should.be.a('function'); 32 | }); 33 | it('it should return an object with a query definition function', () => { 34 | this.actual.queryDefinition.should.be.a('function'); 35 | }); 36 | it('it should return an object with a activity stream function', () => { 37 | this.actual.getActivityStream.should.be.a('function'); 38 | }); 39 | }); 40 | }); 41 | }); 42 | 43 | describe('src/meta.create', function() { 44 | beforeEach(() => { 45 | this.actual = undefined; 46 | }); 47 | describe('given no asset type', () => { 48 | beforeEach(() => { 49 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80 }, () => { 50 | }, () => { 51 | }, false); 52 | }); 53 | describe('when creating an asset', () => { 54 | it('it should throw an invariant error', () => { 55 | (() => this.meta.create()).should.throw(); 56 | }); 57 | }); 58 | }); 59 | describe('given no asset data', () => { 60 | describe('when creating an asset', () => { 61 | it('it should throw an invariant error', () => { 62 | (() => this.meta.create('Actual')).should.throw(); 63 | }); 64 | }); 65 | }); 66 | describe('given empty asset data', () => { 67 | describe('when creating an asset', () => { 68 | it('it should throw an invariant error', () => { 69 | (() => this.meta.create('Actual', {})).should.throw(); 70 | }); 71 | }); 72 | }); 73 | describe('given token based authentication', () => { 74 | describe('given an asset type and asset data', () => { 75 | describe('when creating an asset', () => { 76 | beforeEach(() => { 77 | this.assetData = {key: 'value'}; 78 | this.headers = { 79 | Accept: 'application/json', 80 | 'Content-Type': 'application/json', 81 | Authorization: 'Bearer token' 82 | }; 83 | const getV1Urls = sinon.mock() 84 | .withArgs('h', 'i', 'http', 80) 85 | .returns({ 86 | rest: 'rest URL' 87 | }); 88 | const transformDataToAsset = sinon.mock() 89 | .withArgs({Value: 5.5}) 90 | .returns(this.assetData); 91 | RewireApi.__Rewire__('getV1Urls', getV1Urls); 92 | RewireApi.__Rewire__('transformDataToAsset', transformDataToAsset); 93 | this.postFn = sinon.stub(); 94 | 95 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, token: 'token', postFn: this.postFn, isBasic: false }); 96 | this.actual = this.meta.create('Actual', {Value: 5.5}); 97 | }); 98 | afterEach(() => { 99 | RewireApi.__ResetDependency__('getV1Urls'); 100 | RewireApi.__ResetDependency__('transformDataToAsset'); 101 | }); 102 | it('it should post the asset creation to the REST URL endpoint with token based authentication headers', () => { 103 | this.postFn.calledWith('rest URL/Actual', this.assetData, this.headers).should.be.true; 104 | }); 105 | }); 106 | }); 107 | 108 | describe('given basic authentication', () => { 109 | describe('given an asset type and asset data', () => { 110 | describe('when creating an asset', () => { 111 | beforeEach(() => { 112 | this.assetData = {key: 'value'}; 113 | this.headers = { 114 | Accept: 'application/json', 115 | 'Content-Type': 'application/json', 116 | Authorization: 'Basic token' 117 | }; 118 | const getV1Urls = sinon.mock() 119 | .withArgs('h', 'i', 'http', 80) 120 | .returns({ 121 | rest: 'rest URL' 122 | }); 123 | const transformDataToAsset = sinon.mock() 124 | .withArgs({Value: 5.5}) 125 | .returns(this.assetData); 126 | RewireApi.__Rewire__('getV1Urls', getV1Urls); 127 | RewireApi.__Rewire__('transformDataToAsset', transformDataToAsset); 128 | this.postFn = sinon.stub(); 129 | 130 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, token: 'token', postFn: this.postFn, isBasic: true }); 131 | this.actual = this.meta.create('Actual', {Value: 5.5}); 132 | }); 133 | afterEach(() => { 134 | RewireApi.__ResetDependency__('getV1Urls'); 135 | RewireApi.__ResetDependency__('transformDataToAsset'); 136 | }); 137 | it('it should post the asset creation to the REST URL endpoint with basic authentication headers', () => { 138 | this.postFn.calledWith('rest URL/Actual', this.assetData, this.headers).should.be.true; 139 | }); 140 | }); 141 | }); 142 | }); 143 | 144 | describe('given implicit authentication', () => { 145 | describe('given an asset type and asset data', () => { 146 | describe('when creating an asset', () => { 147 | beforeEach(() => { 148 | this.assetData = {key: 'value'}; 149 | this.headers = { 150 | Accept: 'application/json', 151 | 'Content-Type': 'application/json', 152 | }; 153 | const getV1Urls = sinon.mock() 154 | .withArgs('h', 'i', 'http', 80) 155 | .returns({ 156 | rest: 'rest URL' 157 | }); 158 | const transformDataToAsset = sinon.mock() 159 | .withArgs({Value: 5.5}) 160 | .returns(this.assetData); 161 | RewireApi.__Rewire__('getV1Urls', getV1Urls); 162 | RewireApi.__Rewire__('transformDataToAsset', transformDataToAsset); 163 | this.postFn = sinon.stub(); 164 | 165 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, postFn: this.postFn }); 166 | this.actual = this.meta.create('Actual', {Value: 5.5}); 167 | }); 168 | afterEach(() => { 169 | RewireApi.__ResetDependency__('getV1Urls'); 170 | RewireApi.__ResetDependency__('transformDataToAsset'); 171 | }); 172 | it('it should post the asset creation to the REST URL endpoint with basic authentication headers', () => { 173 | this.postFn.calledWith('rest URL/Actual', this.assetData, this.headers).should.be.true; 174 | }); 175 | }); 176 | }); 177 | }); 178 | }); 179 | }); 180 | 181 | describe('src/meta.update', function() { 182 | beforeEach(() => { 183 | this.actual = undefined; 184 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80 }, () => { 185 | }, () => { 186 | }); 187 | }); 188 | describe('given no oid token', () => { 189 | describe('when updating an asset', () => { 190 | it('it should throw an invariant error', () => { 191 | (() => this.meta.update()).should.throw(); 192 | }); 193 | }); 194 | }); 195 | describe('given no asset data', () => { 196 | describe('when creating an asset', () => { 197 | it('it should throw an invariant error', () => { 198 | (() => this.meta.update('Actual:1001')).should.throw(); 199 | }); 200 | }); 201 | }); 202 | describe('given empty asset data', () => { 203 | describe('when creating an asset', () => { 204 | it('it should throw an invariant error', () => { 205 | (() => this.meta.update('Actual:10001', {})).should.throw(); 206 | }); 207 | }); 208 | }); 209 | describe('given token based authentication', () => { 210 | describe('given an asset type and asset data', () => { 211 | describe('when updating an asset', () => { 212 | beforeEach(() => { 213 | this.assetData = {key: 'value'}; 214 | this.headers = { 215 | Accept: 'application/json', 216 | 'Content-Type': 'application/json', 217 | Authorization: 'Bearer token' 218 | }; 219 | const getV1Urls = sinon.mock() 220 | .withArgs('h', 'i', 'http', 80) 221 | .returns({ 222 | rest: 'rest URL' 223 | }); 224 | const transformDataToAsset = sinon.mock() 225 | .withArgs({Value: 5.5}) 226 | .returns(this.assetData); 227 | RewireApi.__Rewire__('getV1Urls', getV1Urls); 228 | RewireApi.__Rewire__('transformDataToAsset', transformDataToAsset); 229 | this.postFn = sinon.stub(); 230 | 231 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, token: 'token', postFn: this.postFn, getFn: this.getFn, isBasic: false }); 232 | this.actual = this.meta.update('Actual:10011', {Value: 5.5}); 233 | }); 234 | afterEach(() => { 235 | RewireApi.__ResetDependency__('getV1Urls'); 236 | RewireApi.__ResetDependency__('transformDataToAsset'); 237 | }); 238 | it('it should post the asset update to the REST URL endpoint with token based authentication header', () => { 239 | this.postFn.calledWith('rest URL/Actual/10011', this.assetData, this.headers).should.be.true; 240 | }); 241 | }); 242 | }); 243 | }); 244 | describe('given basic authentication', () => { 245 | describe('given an asset type and asset data', () => { 246 | describe('when updating an asset', () => { 247 | beforeEach(() => { 248 | this.assetData = {key: 'value'}; 249 | this.headers = { 250 | Accept: 'application/json', 251 | 'Content-Type': 'application/json', 252 | Authorization: 'Basic token' 253 | }; 254 | const getV1Urls = sinon.mock() 255 | .withArgs('h', 'i', 'http', 80) 256 | .returns({ 257 | rest: 'rest URL' 258 | }); 259 | const transformDataToAsset = sinon.mock() 260 | .withArgs({Value: 5.5}) 261 | .returns(this.assetData); 262 | RewireApi.__Rewire__('getV1Urls', getV1Urls); 263 | RewireApi.__Rewire__('transformDataToAsset', transformDataToAsset); 264 | this.postFn = sinon.stub(); 265 | 266 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, token: 'token', postFn: this.postFn, getFn: this.getFn, isBasic: true }); 267 | this.actual = this.meta.update('Actual:10011', {Value: 5.5}); 268 | }); 269 | afterEach(() => { 270 | RewireApi.__ResetDependency__('getV1Urls'); 271 | RewireApi.__ResetDependency__('transformDataToAsset'); 272 | }); 273 | it('it should post the asset update to the REST URL endpoint with token based authentication header', () => { 274 | this.postFn.calledWith('rest URL/Actual/10011', this.assetData, this.headers).should.be.true; 275 | }); 276 | }); 277 | }); 278 | }); 279 | describe('given an asset type, asset data, and change comment', () => { 280 | describe('when updating an asset', () => { 281 | beforeEach(() => { 282 | this.assetData = {key: 'value'}; 283 | this.headers = { 284 | Accept: 'application/json', 285 | 'Content-Type': 'application/json', 286 | Authorization: 'Basic token' 287 | }; 288 | const getV1Urls = sinon.mock() 289 | .withArgs('h', 'i', 'http', 80) 290 | .returns({ 291 | rest: 'rest URL' 292 | }); 293 | const transformDataToAsset = sinon.mock() 294 | .withArgs({Value: 5.5}) 295 | .returns(this.assetData); 296 | RewireApi.__Rewire__('getV1Urls', getV1Urls); 297 | RewireApi.__Rewire__('transformDataToAsset', transformDataToAsset); 298 | this.postFn = sinon.stub(); 299 | 300 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, token: 'token', postFn: this.postFn, getFn: this.getFn, isBasic: true }); 301 | this.actual = this.meta.update('Actual:10011', {Value: 5.5}, 'change comment'); 302 | }); 303 | afterEach(() => { 304 | RewireApi.__ResetDependency__('getV1Urls'); 305 | RewireApi.__ResetDependency__('transformDataToAsset'); 306 | }); 307 | it('it should post the asset update to the REST URL endpoint with token based authentication header', () => { 308 | this.postFn.calledWith(`rest URL/Actual/10011?comment=${encodeURIComponent('change comment')}`, this.assetData, this.headers).should.be.true; 309 | }); 310 | }); 311 | }); 312 | }); 313 | 314 | describe('src/meta.query', function() { 315 | beforeEach(() => { 316 | this.actual = undefined; 317 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80 }, () => { 318 | }, () => { 319 | }); 320 | }); 321 | describe('given no query object', () => { 322 | describe('when querying meta', () => { 323 | it('it should throw an invariant error', () => { 324 | (() => this.meta.query()).should.throw(); 325 | }); 326 | }); 327 | }); 328 | describe('given empty query object', () => { 329 | describe('when querying meta', () => { 330 | it('it should throw an invariant error', () => { 331 | (() => this.meta.query({})).should.throw(); 332 | }); 333 | }); 334 | }); 335 | describe('given a query object without a from', () => { 336 | describe('when querying meta', () => { 337 | it('it should throw an invariant error', () => { 338 | (() => this.meta.query({select: []})).should.throw(); 339 | }); 340 | }); 341 | }); 342 | describe('given a query object without a select', () => { 343 | describe('when querying meta', () => { 344 | it('it should throw an invariant error', () => { 345 | (() => this.meta.query({from: 'Story'})).should.throw(); 346 | }); 347 | }); 348 | }); 349 | describe('given a query object with an empty select', () => { 350 | describe('when querying meta', () => { 351 | it('it should throw an invariant error', () => { 352 | (() => this.meta.query({from: 'Story', select: []})).should.throw(); 353 | }); 354 | }); 355 | }); 356 | describe('given token based authentication', () => { 357 | describe('given a valid query object', () => { 358 | describe('when querying meta', () => { 359 | beforeEach(() => { 360 | this.query = {from: 'Story', select: ['Estimate']}; 361 | this.headers = { 362 | Accept: 'application/json', 363 | 'Content-Type': 'application/json', 364 | Authorization: 'Bearer token' 365 | }; 366 | const getV1Urls = sinon.mock() 367 | .withArgs('h', 'i', 'http', 80) 368 | .returns({ 369 | query: 'query Url' 370 | }); 371 | RewireApi.__Rewire__('getV1Urls', getV1Urls); 372 | this.postFn = sinon.stub(); 373 | 374 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, token: 'token', postFn: this.postFn, getFn: this.getFn, isBasic: false }); 375 | this.actual = this.meta.query(this.query); 376 | }); 377 | afterEach(() => { 378 | RewireApi.__ResetDependency__('getV1Urls'); 379 | }); 380 | it('it should post the asset update to the query URL endpoint with token based authentication header', () => { 381 | this.postFn.calledWith('query Url', this.query, this.headers).should.be.true; 382 | }); 383 | }); 384 | }); 385 | }); 386 | describe('given basic authentication', () => { 387 | describe('given a valid query object', () => { 388 | describe('when querying meta', () => { 389 | beforeEach(() => { 390 | this.query = {from: 'Story', select: ['Estimate']}; 391 | this.headers = { 392 | Accept: 'application/json', 393 | 'Content-Type': 'application/json', 394 | Authorization: 'Basic token' 395 | }; 396 | const getV1Urls = sinon.mock() 397 | .withArgs('h', 'i', 'http', 80) 398 | .returns({ 399 | query: 'query Url' 400 | }); 401 | RewireApi.__Rewire__('getV1Urls', getV1Urls); 402 | this.postFn = sinon.stub(); 403 | 404 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, token: 'token', postFn: this.postFn, getFn: this.getFn, isBasic: true }); 405 | this.actual = this.meta.query(this.query); 406 | }); 407 | afterEach(() => { 408 | RewireApi.__ResetDependency__('getV1Urls'); 409 | }); 410 | it('it should post the asset update to the query URL endpoint with basic authentication header', () => { 411 | this.postFn.calledWith('query Url', this.query, this.headers).should.be.true; 412 | }); 413 | }); 414 | }); 415 | }); 416 | }); 417 | 418 | describe('src/meta.executeOperation', function() { 419 | beforeEach(() => { 420 | this.actual = undefined; 421 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80 }, () => { 422 | }, () => { 423 | }); 424 | }); 425 | describe('given no oid token', () => { 426 | describe('when executing an operation against an asset', () => { 427 | it('it should throw an invariant error', () => { 428 | (() => this.meta.executeOperation()).should.throw(); 429 | }); 430 | }); 431 | }); 432 | describe('given no operation name', () => { 433 | describe('when executing an operation against an asset', () => { 434 | it('it should throw an invariant error', () => { 435 | (() => this.meta.executeOperation('Actual:10001')).should.throw(); 436 | }); 437 | }); 438 | }); 439 | describe('given token based authentication', () => { 440 | describe('given an oid token and an operation name', () => { 441 | describe('when executing an operation against an asset', () => { 442 | beforeEach(() => { 443 | this.operationName = 'operation name'; 444 | this.headers = { 445 | Accept: 'application/json', 446 | 'Content-Type': 'application/json', 447 | Authorization: 'Bearer token' 448 | }; 449 | const getV1Urls = sinon.mock() 450 | .withArgs('h', 'i', 'http', 80) 451 | .returns({ 452 | rest: 'rest URL' 453 | }); 454 | RewireApi.__Rewire__('getV1Urls', getV1Urls); 455 | this.postFn = sinon.stub(); 456 | 457 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, token: 'token', postFn: this.postFn, getFn: this.getFn, isBasic: false }); 458 | this.actual = this.meta.executeOperation('Actual:10011', this.operationName); 459 | }); 460 | afterEach(() => { 461 | RewireApi.__ResetDependency__('getV1Urls'); 462 | }); 463 | it('it should post the the operation to the REST URL endpoint with token based authentication header', () => { 464 | this.postFn.calledWith(`rest URL/Actual/10011?op=${this.operationName}`, null, this.headers).should.be.true; 465 | }); 466 | }); 467 | }); 468 | }); 469 | describe('given basic authentication', () => { 470 | describe('given an oid token and an operation name', () => { 471 | describe('when executing an operation against an asset', () => { 472 | beforeEach(() => { 473 | this.operationName = 'operation name'; 474 | this.headers = { 475 | Accept: 'application/json', 476 | 'Content-Type': 'application/json', 477 | Authorization: 'Basic token' 478 | }; 479 | const getV1Urls = sinon.mock() 480 | .withArgs('h', 'i', 'http', 80) 481 | .returns({ 482 | rest: 'rest URL' 483 | }); 484 | RewireApi.__Rewire__('getV1Urls', getV1Urls); 485 | this.postFn = sinon.stub(); 486 | 487 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, token: 'token', postFn: this.postFn, getFn: this.getFn, isBasic: true }); 488 | this.actual = this.meta.executeOperation('Actual:10011', this.operationName); 489 | }); 490 | afterEach(() => { 491 | RewireApi.__ResetDependency__('getV1Urls'); 492 | }); 493 | it('it should post the the operation to the REST URL endpoint with token basic authentication header', () => { 494 | this.postFn.calledWith(`rest URL/Actual/10011?op=${this.operationName}`, null, this.headers).should.be.true; 495 | }); 496 | }); 497 | }); 498 | }); 499 | }); 500 | 501 | describe('src/meta.queryDefinition', function() { 502 | beforeEach(() => { 503 | this.actual = undefined; 504 | }); 505 | describe('given no asset type', () => { 506 | describe('when querying for meta data', () => { 507 | beforeEach(() => { 508 | this.headers = { 509 | Accept: 'application/json', 510 | 'Content-Type': 'application/json', 511 | Authorization: 'Bearer token' 512 | }; 513 | const getV1Urls = sinon.mock() 514 | .withArgs('h', 'i', 'http', 80) 515 | .returns({ 516 | meta: 'meta URL' 517 | }); 518 | RewireApi.__Rewire__('getV1Urls', getV1Urls); 519 | this.getFn = sinon.stub(); 520 | 521 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, token: 'token', getFn: this.getFn, isBasic: false }); 522 | this.actual = this.meta.queryDefinition(); 523 | }); 524 | afterEach(() => { 525 | RewireApi.__ResetDependency__('getV1Urls'); 526 | }); 527 | it('it should query for all meta data', () => { 528 | this.getFn.calledWith(`meta URL/`, null, this.headers).should.be.true; 529 | }); 530 | }); 531 | }); 532 | describe('given an asset type', () => { 533 | describe('when querying for meta data for the asset type', () => { 534 | beforeEach(() => { 535 | this.headers = { 536 | Accept: 'application/json', 537 | 'Content-Type': 'application/json', 538 | Authorization: 'Bearer token' 539 | }; 540 | const getV1Urls = sinon.mock() 541 | .withArgs('h', 'i', 'http', 80) 542 | .returns({ 543 | meta: 'meta URL' 544 | }); 545 | RewireApi.__Rewire__('getV1Urls', getV1Urls); 546 | this.getFn = sinon.stub(); 547 | 548 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, token: 'token', getFn: this.getFn, isBasic: false }); 549 | this.actual = this.meta.queryDefinition('Actual'); 550 | }); 551 | afterEach(() => { 552 | RewireApi.__ResetDependency__('getV1Urls'); 553 | }); 554 | it('it should post the the operation to the REST URL endpoint', () => { 555 | this.getFn.calledWith(`meta URL/Actual`, null, this.headers).should.be.true; 556 | }); 557 | }); 558 | }); 559 | describe('given an oidToken', () => { 560 | describe('when retreiving an activity stream for the asset', () => { 561 | beforeEach(() => { 562 | this.headers = { 563 | Accept: 'application/json', 564 | 'Content-Type': 'application/json', 565 | Authorization: 'Bearer token' 566 | }; 567 | const getV1Urls = sinon.mock() 568 | .withArgs('h', 'i', 'http', 80) 569 | .returns({ 570 | activityStream: 'activityStream URL' 571 | }); 572 | RewireApi.__Rewire__('getV1Urls', getV1Urls); 573 | this.getFn = sinon.stub(); 574 | 575 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, token: 'token', getFn: this.getFn, isBasic: false }); 576 | this.actual = this.meta.getActivityStream('Story:1234'); 577 | }); 578 | afterEach(() => { 579 | RewireApi.__ResetDependency__('getV1Urls'); 580 | }); 581 | it('it should query the ActivityStream endpoint', () => { 582 | this.getFn.calledWith(`activityStream URL/Story:1234`, null, this.headers).should.be.true; 583 | }); 584 | }); 585 | }); 586 | describe('given token based authentication', () => { 587 | describe('when querying for meta data', () => { 588 | beforeEach(() => { 589 | this.headers = { 590 | Accept: 'application/json', 591 | 'Content-Type': 'application/json', 592 | Authorization: 'Bearer token' 593 | }; 594 | const getV1Urls = sinon.mock() 595 | .withArgs('h', 'i', 'http', 80) 596 | .returns({ 597 | meta: 'meta URL' 598 | }); 599 | RewireApi.__Rewire__('getV1Urls', getV1Urls); 600 | this.getFn = sinon.stub(); 601 | 602 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, token: 'token', getFn: this.getFn, isBasic: false }); 603 | this.actual = this.meta.queryDefinition(); 604 | }); 605 | afterEach(() => { 606 | RewireApi.__ResetDependency__('getV1Urls'); 607 | }); 608 | it('it should query for all meta data', () => { 609 | this.getFn.calledWith(`meta URL/`, null, this.headers).should.be.true; 610 | }); 611 | }); 612 | describe('given basic authentication', () => { 613 | describe('when querying for meta data', () => { 614 | describe('when querying for meta data', () => { 615 | beforeEach(() => { 616 | this.headers = { 617 | Accept: 'application/json', 618 | 'Content-Type': 'application/json', 619 | Authorization: 'Basic token' 620 | }; 621 | const getV1Urls = sinon.mock() 622 | .withArgs('h', 'i', 'http', 80) 623 | .returns({ 624 | meta: 'meta URL' 625 | }); 626 | RewireApi.__Rewire__('getV1Urls', getV1Urls); 627 | this.getFn = sinon.stub(); 628 | 629 | this.meta = createMeta({ hostname: 'h', instance: 'i', protocol: 'http', port: 80, token: 'token', getFn: this.getFn, isBasic: true }); 630 | this.actual = this.meta.queryDefinition(); 631 | }); 632 | afterEach(() => { 633 | RewireApi.__ResetDependency__('getV1Urls'); 634 | }); 635 | it('it should query for all meta data', () => { 636 | this.getFn.calledWith(`meta URL/`, null, this.headers).should.be.true; 637 | }); 638 | }); 639 | }); 640 | }); 641 | }); 642 | }); 643 | -------------------------------------------------------------------------------- /src/getV1Urls.js: -------------------------------------------------------------------------------- 1 | export default (hostname, instance, protocol, port) => { 2 | const rootUrl = getUrlToV1Server(hostname, instance, protocol, port); 3 | return Object.freeze({ 4 | rest: `${rootUrl}/rest-1.v1/Data`, 5 | query: `${rootUrl}/query.v1`, 6 | meta: `${rootUrl}/meta.v1`, 7 | activityStream: `${rootUrl}/api/ActivityStream` 8 | }); 9 | }; 10 | 11 | const getUrlToV1Server = (hostname, instance, protocol, port) => `${protocol}://${hostname}:${port}/${instance}`; 12 | -------------------------------------------------------------------------------- /src/getV1Urls.specs.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import getV1Urls from './getV1Urls'; 4 | chai.use(chaiAsPromised); 5 | chai.should(); 6 | 7 | describe('src/getV1Urls', function() { 8 | beforeEach(() => { 9 | this.actual = undefined; 10 | }); 11 | describe('given a V1 hostname, instance name, protocol and port', () => { 12 | describe('when getting the URLs for the VersionOne instance', () => { 13 | beforeEach(() => { 14 | this.actual = getV1Urls('some URL', 'some Instance', 'https', 8081); 15 | }); 16 | 17 | it('it should return the Rest API URL', () => { 18 | this.actual.rest.should.equal('https://some URL:8081/some Instance/rest-1.v1/Data'); 19 | }); 20 | it('it should return the Query API URL', () => { 21 | this.actual.query.should.equal('https://some URL:8081/some Instance/query.v1'); 22 | }); 23 | it('it should return the Meta API URL', () => { 24 | this.actual.meta.should.equal('https://some URL:8081/some Instance/meta.v1'); 25 | }); 26 | it('it should return the Activity Stream API URL', () => { 27 | this.actual.activityStream.should.equal('https://some URL:8081/some Instance/api/ActivityStream'); 28 | }); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import btoa from 'btoa'; 2 | import createMeta from './createMeta'; 3 | 4 | export {default as Oid} from './Oid'; 5 | export {default as jqueryConnector} from './connectors/jqueryConnector'; 6 | export {default as axiosConnector} from './connectors/axiosConnector'; 7 | export default (postFn, getFn) => (hostname, instance, port = 80, isHttps = false) => { 8 | const protocol = isHttps ? 'https' : 'http'; 9 | return { 10 | withImplicitAuth: () => createMeta({ 11 | hostname, 12 | instance, 13 | protocol, 14 | port, 15 | postFn, 16 | getFn, 17 | }), 18 | withAccessToken: (token) => createMeta({ 19 | hostname, 20 | instance, 21 | protocol, 22 | port, 23 | token, 24 | postFn, 25 | getFn, 26 | isBasic: false, 27 | }), 28 | withCreds: (username, password) => createMeta({ 29 | hostname, 30 | instance, 31 | protocol, 32 | port, 33 | token: btoa(`${username}:${password}`), 34 | postFn, 35 | getFn, 36 | isBasic: true, 37 | }) 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/index.specs.js: -------------------------------------------------------------------------------- 1 | import sdk, {Oid, __RewireAPI__ as RewireApi} from './index'; 2 | import oid from './Oid'; 3 | import sinon from 'sinon'; 4 | 5 | describe('when loading the module', function() { 6 | it('it should export the sdk as the default', () => { 7 | sdk.should.be.a('function'); 8 | }); 9 | it('it should export Oid', () => { 10 | Oid.should.equal(oid); 11 | }); 12 | 13 | beforeEach(() => { 14 | this.actual = undefined; 15 | }); 16 | 17 | describe('given a post and get AJAX function', () => { 18 | beforeEach(() => { 19 | this.postFn = sinon.stub(); 20 | this.getFn = sinon.stub(); 21 | this.sdk = sdk(this.postFn, this.getFn); 22 | }); 23 | it('it should return a function to set V1 host information', () => { 24 | this.sdk.should.be.a('function'); 25 | }); 26 | 27 | describe('given a hostname, instance name, port, and https protocol', () => { 28 | beforeEach(() => { 29 | this.setSecurity = this.sdk('hostname', 'instance', 80, true); 30 | }); 31 | it('it should return a meta object with function to authenticate via username and password', () => { 32 | this.setSecurity.withCreds.should.be.a('function'); 33 | }); 34 | it('it should return a meta object with function to authenticate via an access token', () => { 35 | this.setSecurity.withAccessToken.should.be.a('function'); 36 | }); 37 | it('it should return a meta object with function to authenticate implicitly', () => { 38 | this.setSecurity.withImplicitAuth.should.be.a('function'); 39 | }); 40 | 41 | describe('given an access token', () => { 42 | beforeEach(() => { 43 | this.accessToken = 'access token'; 44 | }); 45 | describe('when creating the SDK', () => { 46 | beforeEach(() => { 47 | this.metaStub = sinon.mock() 48 | .withExactArgs({ hostname: 'hostname', instance: 'instance', protocol: 'https', port: 80, token: this.accessToken, postFn: this.postFn, getFn: this.getFn, isBasic: false }) 49 | .returns('meta'); 50 | RewireApi.__Rewire__('createMeta', this.metaStub); 51 | this.meta = this.setSecurity.withAccessToken(this.accessToken); 52 | }); 53 | it('it should return a Meta object with the access token, hostname, instance, port, protocol, post and get functions', () => { 54 | this.meta.should.equal('meta'); 55 | }); 56 | }); 57 | }); 58 | 59 | describe('given a username and password', () => { 60 | beforeEach(() => { 61 | this.username = 'username'; 62 | this.password = 'password'; 63 | }); 64 | describe('when creating the SDK', () => { 65 | beforeEach(() => { 66 | this.token = 'username token'; 67 | this.metaStub = sinon.mock() 68 | .withExactArgs({ hostname: 'hostname', instance: 'instance', protocol: 'https', port: 80, token: this.token, postFn: this.postFn, getFn: this.getFn, isBasic: true }) 69 | .returns('meta1'); 70 | this.btoa = sinon.mock() 71 | .withArgs(`${this.username}:${this.password}`) 72 | .returns(this.token); 73 | RewireApi.__Rewire__('createMeta', this.metaStub); 74 | RewireApi.__Rewire__('btoa', this.btoa); 75 | this.meta = this.setSecurity.withCreds(this.username, this.password); 76 | }); 77 | it('it should return a Meta object with the username/password encoded token, hostname, instance, port, protocol, post and get functions', () => { 78 | this.meta.should.equal('meta1'); 79 | }); 80 | }); 81 | }); 82 | 83 | describe('given no explicit credentials', () => { 84 | describe('when creating the SDK', () => { 85 | beforeEach(() => { 86 | this.metaStub = sinon.mock() 87 | .withExactArgs({ hostname: 'hostname', instance: 'instance', protocol: 'https', port: 80, postFn: this.postFn, getFn: this.getFn }) 88 | .returns('meta1'); 89 | RewireApi.__Rewire__('createMeta', this.metaStub); 90 | this.meta = this.setSecurity.withImplicitAuth(); 91 | }); 92 | it('it should return a Meta object with hostname, instance, port, protocol, post and get functions', () => { 93 | this.meta.should.equal('meta1'); 94 | }); 95 | }); 96 | }); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/transformDataToAsset.js: -------------------------------------------------------------------------------- 1 | export default (assetData) => ({ 2 | Attributes: reduceAssetData(assetData) 3 | }); 4 | 5 | const reduceAssetData = (obj) => Object.keys(obj) 6 | .reduce((output, key) => { 7 | const attributeData = obj[key]; 8 | if (Array.isArray(attributeData)) { 9 | return { 10 | ...output, 11 | [key]: { 12 | name: key, 13 | value: attributeData.map(reduceRelationalAttributes) 14 | } 15 | }; 16 | } 17 | if (isFunction(attributeData)) { 18 | return { 19 | ...output, 20 | [key]: { 21 | value: obj[key](), 22 | act: 'set' 23 | } 24 | }; 25 | } 26 | return { 27 | ...output, 28 | [key]: { 29 | value: obj[key], 30 | act: 'set' 31 | } 32 | }; 33 | }, {}); 34 | 35 | const reduceRelationalAttributes = (obj) => { 36 | if (typeof obj === 'string') { 37 | return { 38 | idref: obj, 39 | act: 'add' 40 | }; 41 | } 42 | return Object.keys(obj).reduce((output)=> { 43 | output.idref = obj.idref; 44 | output.act = obj.act ? obj.act : 'add'; 45 | return output; 46 | }, {}); 47 | }; 48 | 49 | const isFunction = (obj) => obj && obj.constructor && obj.call && obj.apply; 50 | -------------------------------------------------------------------------------- /src/transformDataToAsset.specs.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import transformDataToAsset from './transformDataToAsset'; 4 | chai.use(chaiAsPromised); 5 | chai.should(); 6 | 7 | describe('src/transformDataToAsset', function() { 8 | beforeEach(() => { 9 | this.actual = undefined; 10 | }); 11 | describe('given an object of asset data', () => { 12 | beforeEach(() => { 13 | this.assetData = { 14 | Value: 20 15 | }; 16 | }); 17 | describe('when transforming the asset data into something acceptable to the V1 Server instance', () => { 18 | beforeEach(() => { 19 | this.actual = transformDataToAsset(this.assetData); 20 | }); 21 | it('it should return an object with Attributes property', () => { 22 | chai.should().exist(this.actual.Attributes); 23 | this.actual.Attributes.Value.value.should.equal(20); 24 | this.actual.Attributes.Value.act.should.equal('set'); 25 | }); 26 | }); 27 | }); 28 | 29 | describe('given an object of asset data with single-relational values', () => { 30 | beforeEach(() => { 31 | this.assetData = { 32 | Value: 20, 33 | Member: 'Member:20' 34 | }; 35 | }); 36 | 37 | describe('when transforming the asset data to something acceptable to the V1 Server instance', () => { 38 | beforeEach(() => { 39 | this.actual = transformDataToAsset(this.assetData); 40 | }); 41 | it('it should transform keys with non-array values to single-relation properties on the output object', () => { 42 | this.actual.Attributes.Member.value.should.equal('Member:20'); 43 | this.actual.Attributes.Value.value.should.equal(20); 44 | }); 45 | it('it should set the action to set for each output attribute', () => { 46 | this.actual.Attributes.Member.act.should.be.equal('set'); 47 | this.actual.Attributes.Value.act.should.be.equal('set'); 48 | }); 49 | }); 50 | }); 51 | 52 | describe('given an object of asset data with mutli-relational values that do not contain an idref', () => { 53 | beforeEach(() => { 54 | this.assetData = { 55 | Actuals: ['Actual:10001', 'Actual:10002'] 56 | }; 57 | }); 58 | 59 | describe('when transforming the asset data to something acceptable to the V1 Server instance', () => { 60 | beforeEach(() => { 61 | this.actual = transformDataToAsset(this.assetData); 62 | }); 63 | it('it should transform keys with array values to a multi-relation asset attribute property on the output object', () => { 64 | this.actual.Attributes.Actuals.name.should.equal('Actuals'); 65 | this.actual.Attributes.Actuals.value[0].idref.should.equal('Actual:10001'); 66 | this.actual.Attributes.Actuals.value[1].idref.should.equal('Actual:10002'); 67 | }); 68 | it('it should set the action for each value to add', () => { 69 | this.actual.Attributes.Actuals.value[0].act.should.equal('add'); 70 | this.actual.Attributes.Actuals.value[1].act.should.equal('add'); 71 | }); 72 | }); 73 | }); 74 | 75 | describe('given an object of asset data with mutli-relational values that contain an idref', () => { 76 | beforeEach(() => { 77 | this.assetData = { 78 | Actuals: [{idref: 'Actual:10001'}, {idref: 'Actual:10002'}] 79 | }; 80 | }); 81 | 82 | describe('when transforming the asset data to something acceptable to the V1 Server instance', () => { 83 | beforeEach(() => { 84 | this.actual = transformDataToAsset(this.assetData); 85 | }); 86 | it('it should transform keys with array values to a multi-relation asset attribute property on the output object', () => { 87 | this.actual.Attributes.Actuals.name.should.equal('Actuals'); 88 | this.actual.Attributes.Actuals.value[0].idref.should.equal('Actual:10001'); 89 | this.actual.Attributes.Actuals.value[1].idref.should.equal('Actual:10002'); 90 | }); 91 | it('it should set the action for each value to add', () => { 92 | this.actual.Attributes.Actuals.value[0].act.should.equal('add'); 93 | this.actual.Attributes.Actuals.value[1].act.should.equal('add'); 94 | }); 95 | }); 96 | 97 | describe('given the multi-relational values contain an act action', () => { 98 | beforeEach(() => { 99 | this.assetData = { 100 | Actuals: [{idref: 'Actual:10001', act: 'add'}, {idref: 'Actual:10002', act: 'remove'}] 101 | }; 102 | }); 103 | describe('when transforming the asset data to something acceptable to the V1 Server instance', () => { 104 | beforeEach(() => { 105 | this.actual = transformDataToAsset(this.assetData); 106 | }); 107 | it('it should set the action for each value to the provided value', () => { 108 | this.actual.Attributes.Actuals.value[0].act.should.equal('add'); 109 | this.actual.Attributes.Actuals.value[1].act.should.equal('remove'); 110 | }); 111 | }); 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | process.env.BABEL_ENV = 'test'; 2 | process.env.NODE_ENV = 'test'; 3 | var wallabyWebpack = require('wallaby-webpack'); 4 | var webpackPostprocessor = wallabyWebpack({}); 5 | 6 | module.exports = function wallabyConfig(wallaby) { 7 | return { 8 | files: [ 9 | {pattern: 'node_modules/babel-polyfill/dist/polyfill.js', instrument: false}, 10 | {pattern: 'node_modules/chai/chai.js', instrument: false}, 11 | {pattern: 'node_modules/chai-as-promised/lib/chai-as-promised', instrument: false}, 12 | {pattern: 'src/**/*.js', load: false}, 13 | {pattern: '!src/**/*.specs.js'} 14 | ], 15 | tests: [ 16 | {pattern: 'src/**/*.specs.js', load: false} 17 | ], 18 | compilers: { 19 | 'src/**/*.js': wallaby.compilers.babel() 20 | }, 21 | testFramework: 'mocha', 22 | postprocessor: webpackPostprocessor, 23 | setup: function setup() { 24 | chai.should(); 25 | window.__moduleBundler.loadTests(); 26 | } 27 | }; 28 | }; 29 | --------------------------------------------------------------------------------