├── .gitignore ├── .DS_Store ├── README.md ├── rollup.config.js ├── package.json ├── dist └── index.min.js ├── src ├── index.js └── index.test.js └── release.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | settings.js 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/command/js/master/.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Command.js 2 | 3 | Official JavaScript library for the Command API. 4 | 5 | [Read the Documentation](http://portal.oncommand.io/docs/command-js/0.43.0/introduction) 6 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import { terser } from "rollup-plugin-terser"; 4 | import pkg from "./package.json"; 5 | 6 | export default [ 7 | { 8 | input: "src/index.js", 9 | output: { 10 | file: pkg.main, 11 | name: "command", 12 | format: "umd" 13 | }, 14 | plugins: [resolve(), commonjs(), terser()] 15 | } 16 | ]; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oncommandio/js", 3 | "version": "0.43.0", 4 | "description": "Official JavaScript library for the Command API.", 5 | "main": "dist/index.min.js", 6 | "scripts": { 7 | "release": "node ./release.js", 8 | "build": "rollup --c", 9 | "dev": "rollup -c -w", 10 | "test": "jest", 11 | "test-watch": "jest --watch" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/command/js" 16 | }, 17 | "keywords": [ 18 | "npm", 19 | "package", 20 | "template" 21 | ], 22 | "author": "oncommandio", 23 | "license": "MIT", 24 | "homepage": "https://portal.oncommand.io/docs/command-js/0.43.0/introduction", 25 | "dependencies": { 26 | "axios": "^0.19.0" 27 | }, 28 | "devDependencies": { 29 | "@rollup/plugin-commonjs": "^11.0.2", 30 | "@rollup/plugin-node-resolve": "^7.1.1", 31 | "aws-sdk": "^2.590.0", 32 | "jest": "^25.0.0", 33 | "rollup": "^1.32.0", 34 | "rollup-plugin-terser": "^5.2.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /dist/index.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).command=e()}(this,(function(){"use strict";const t=require("axios");return class{constructor(t,e={}){t||this._throwFormattedError("A valid API key is required."),this.apiKey=t,this.version="0.43.0",this.customerId=e.customerId||null,this.debug=e.debug||!1,this.customers={login:this._loginCustomer.bind(this),logout:this._logoutCustomer.bind(this),create:this._createCustomer.bind(this),update:this._updateCustomer.bind(this),delete:this._deleteCustomer.bind(this)}}_logDebugMessage(t){console.log("[[[ Command.js DEBUG ]]]"),console.log(t)}_throwFormattedError(t){throw new Error(`[Command] ${t} See https://portal.oncommand.io/docs/command-js/${this.version}/introduction.`)}_request(e,s,r={}){return this.debug&&this._logDebugMessage({method:e,url:`https://api.oncommand.io/v1${s}`,headers:{"x-api-key":this.apiKey},data:r}),t({method:e,url:`https://api.oncommand.io/v1${s}`,headers:{"x-api-key":this.apiKey},data:r}).then(t=>t&&t.data&&t.data.data).catch(t=>{if(t&&t.response){const{status:e}=t.response,s=t.response&&t.response.data&&t.response.data&&t.response.data.data&&t.response.data.data.error;console.warn(`[${e}] ${s}`),t.response.data&&console.warn(t.response.data),this.debug&&t.response.data&&t.response.data.data&&(this._logDebugMessage(t.response.data.data.error),this._logDebugMessage(t.response.data.data.validationErrors))}})}track(t,e){if(!t)throw new Error("Must pass a key to track.");const s={key:t};return this.customerId&&(s.customerId=this.customerId),e&&(s.properties=e),this._request("post","/behavior",s)}_loginCustomer(t){if(!t)throw new Error("Must pass a customerId.");return this.customerId=t,this._request("put","/customers/login",{customerId:t})}_logoutCustomer(t){if(!t&&!this.customerId)throw new Error("Must have a customerId to logout.");return this._request("put","/customers/logout",{customerId:t||this.customerId},()=>{this.customerId=null})}_createCustomer(t){if(!t)throw new Error("Must pass a customer.");return this._request("post","/customers",{...t})}_updateCustomer(t,e){if(!t)throw new Error("Must pass a customerId.");if(!e)throw new Error("Must pass an update for the customer.");return this._request("put",`/customers/${t}`,{...e})}_deleteCustomer(t){if(!t)throw new Error("Must pass a customerId.");return this._request("delete",`/customers/${t}`)}}})); 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | class CommandAPI { 4 | constructor(apiKey, options = {}) { 5 | if (!apiKey) this._throwFormattedError("A valid API key is required."); 6 | this.apiKey = apiKey; 7 | this.version = ""; 8 | this.customerId = options.customerId || null; 9 | this.debug = options.debug || false; 10 | 11 | this.customers = { 12 | login: this._loginCustomer.bind(this), 13 | logout: this._logoutCustomer.bind(this), 14 | create: this._createCustomer.bind(this), 15 | update: this._updateCustomer.bind(this), 16 | delete: this._deleteCustomer.bind(this) 17 | }; 18 | } 19 | 20 | _logDebugMessage(message) { 21 | console.log("[[[ Command.js DEBUG ]]]"); 22 | console.log(message); 23 | } 24 | 25 | _throwFormattedError(error) { 26 | throw new Error( 27 | `[Command] ${error} See https://portal.oncommand.io/docs/command-js/${this.version}/introduction.` 28 | ); 29 | } 30 | 31 | _request(method, path, data = {}) { 32 | if (this.debug) { 33 | this._logDebugMessage({ 34 | method, 35 | url: `http://localhost:4000/api/v1${path}`, 36 | headers: { 37 | "x-api-key": this.apiKey 38 | }, 39 | data 40 | }); 41 | } 42 | 43 | // NOTE: http://localhost:4000/api is dynamically swapped to https://api.oncommand.io in /release.js when releasing a new version. Leave as-is for local dev. 44 | return axios({ 45 | method, 46 | url: `http://localhost:4000/api/v1${path}`, 47 | headers: { 48 | "x-api-key": this.apiKey 49 | }, 50 | data 51 | }) 52 | .then(response => { 53 | return response && response.data && response.data.data; 54 | }) 55 | .catch(error => { 56 | if (error && error.response) { 57 | const { status } = error.response; 58 | const errorMessage = 59 | error.response && 60 | error.response.data && 61 | error.response.data && 62 | error.response.data.data && 63 | error.response.data.data.error; 64 | 65 | console.warn(`[${status}] ${errorMessage}`); 66 | 67 | if (error.response.data) { 68 | console.warn(error.response.data); 69 | } 70 | 71 | if (this.debug && error.response.data && error.response.data.data) { 72 | this._logDebugMessage(error.response.data.data.error); 73 | this._logDebugMessage(error.response.data.data.validationErrors); 74 | } 75 | } 76 | }); 77 | } 78 | 79 | track(key, properties) { 80 | if (!key) throw new Error("Must pass a key to track."); 81 | 82 | const body = { key }; 83 | 84 | if (this.customerId) body.customerId = this.customerId; 85 | if (properties) body.properties = properties; 86 | 87 | return this._request("post", "/behavior", body); 88 | } 89 | 90 | _loginCustomer(customerId) { 91 | if (!customerId) throw new Error("Must pass a customerId."); 92 | 93 | this.customerId = customerId; 94 | 95 | return this._request("put", `/customers/login`, { 96 | customerId 97 | }); 98 | } 99 | 100 | _logoutCustomer(customerId) { 101 | if (!customerId && !this.customerId) 102 | throw new Error("Must have a customerId to logout."); 103 | 104 | return this._request( 105 | "put", 106 | `/customers/logout`, 107 | { 108 | customerId: customerId || this.customerId 109 | }, 110 | () => { 111 | this.customerId = null; 112 | } 113 | ); 114 | } 115 | 116 | _createCustomer(customer) { 117 | if (!customer) throw new Error("Must pass a customer."); 118 | 119 | return this._request("post", "/customers", { 120 | ...customer 121 | }); 122 | } 123 | 124 | _updateCustomer(customerId, update) { 125 | if (!customerId) throw new Error("Must pass a customerId."); 126 | if (!update) throw new Error("Must pass an update for the customer."); 127 | 128 | return this._request("put", `/customers/${customerId}`, { 129 | ...update 130 | }); 131 | } 132 | 133 | _deleteCustomer(customerId) { 134 | if (!customerId) throw new Error("Must pass a customerId."); 135 | return this._request("delete", `/customers/${customerId}`); 136 | } 137 | } 138 | 139 | export default CommandAPI; 140 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | const Command = require("../dist/index.min"); 2 | const axios = require("axios"); 3 | 4 | jest.mock("axios"); 5 | 6 | describe("index.js", () => { 7 | beforeEach(() => { 8 | axios.mockReset(); 9 | axios.mockImplementation(() => Promise.resolve()); 10 | 11 | // NOTE: Mock global Date constructor so all dates match in test. 12 | global.Date = jest.fn(() => ({ 13 | toISOString: jest.fn(() => "2019-12-15T00:00:00.000Z") 14 | })); 15 | }); 16 | 17 | test("it creates an instance with the api key", () => { 18 | const command = new Command("apiKey123"); 19 | expect(command.apiKey).toBe("apiKey123"); 20 | }); 21 | 22 | test("it throws an error if api key is not passed", () => { 23 | expect(() => { 24 | const command = new Command(); 25 | }).toThrow("A valid API key is required."); 26 | }); 27 | 28 | test("it can track an event", () => { 29 | const command = new Command("apiKey123"); 30 | command.track("an event"); 31 | expect(axios).toHaveBeenCalledWith({ 32 | method: "post", 33 | url: `https://api.oncommand.io/v1/behavior`, 34 | headers: { 35 | "x-api-key": "apiKey123" 36 | }, 37 | data: { 38 | key: "an event" 39 | } 40 | }); 41 | }); 42 | 43 | test("it throws an error if no key is passed to track", () => { 44 | expect(() => { 45 | const command = new Command("apiKey123"); 46 | command.track(); 47 | }).toThrow("Must pass a key to track."); 48 | }); 49 | 50 | test("it can track a customer login", () => { 51 | const command = new Command("apiKey123"); 52 | command.customers.login("1234"); 53 | expect(axios).toHaveBeenCalledWith({ 54 | method: "put", 55 | url: `https://api.oncommand.io/v1/customers/login`, 56 | headers: { 57 | "x-api-key": "apiKey123" 58 | }, 59 | data: { 60 | customerId: "1234" 61 | } 62 | }); 63 | }); 64 | 65 | test("it throws an error if no customerId is passed to customers.login", () => { 66 | expect(() => { 67 | const command = new Command("apiKey123"); 68 | command.customers.login(); 69 | }).toThrow("Must pass a customerId."); 70 | }); 71 | 72 | test("it can track a customer logout", () => { 73 | const command = new Command("apiKey123"); 74 | command.customers.login("1234"); // NOTE: Assigns the current customer internally. 75 | command.customers.logout(); 76 | expect(axios).toHaveBeenCalledWith({ 77 | method: "put", 78 | url: `https://api.oncommand.io/v1/customers/logout`, 79 | headers: { 80 | "x-api-key": "apiKey123" 81 | }, 82 | data: { 83 | customerId: "1234" 84 | } 85 | }); 86 | }); 87 | 88 | test("it throws an error if no customerId is set on the command instance on customers.logout", () => { 89 | expect(() => { 90 | const command = new Command("apiKey123"); 91 | command.customers.logout(); 92 | }).toThrow("Must have a customerId to logout."); 93 | }); 94 | 95 | test("it can create a customer", () => { 96 | const command = new Command("apiKey123"); 97 | command.customers.create({ emailAddress: "test@test.com" }); 98 | expect(axios).toHaveBeenCalledWith({ 99 | method: "post", 100 | url: `https://api.oncommand.io/v1/customers`, 101 | headers: { 102 | "x-api-key": "apiKey123" 103 | }, 104 | data: { 105 | emailAddress: "test@test.com" 106 | } 107 | }); 108 | }); 109 | 110 | test("it throws an error if no customer is passed to customers.create", () => { 111 | expect(() => { 112 | const command = new Command("apiKey123"); 113 | command.customers.create(); 114 | }).toThrow("Must pass a customer."); 115 | }); 116 | 117 | test("it can update a customer", () => { 118 | const command = new Command("apiKey123"); 119 | command.customers.update("customerId123", { 120 | emailAddress: "test1@test.com" 121 | }); 122 | expect(axios).toHaveBeenCalledWith({ 123 | method: "put", 124 | url: `https://api.oncommand.io/v1/customers/customerId123`, 125 | headers: { 126 | "x-api-key": "apiKey123" 127 | }, 128 | data: { 129 | emailAddress: "test1@test.com" 130 | } 131 | }); 132 | }); 133 | 134 | test("it throws an error if no customerId is passed to customers.update", () => { 135 | expect(() => { 136 | const command = new Command("apiKey123"); 137 | command.customers.update(); 138 | }).toThrow("Must pass a customerId."); 139 | }); 140 | 141 | test("it throws an error if no update is passed to customers.update", () => { 142 | expect(() => { 143 | const command = new Command("apiKey123"); 144 | command.customers.update("customerId123"); 145 | }).toThrow("Must pass an update for the customer."); 146 | }); 147 | 148 | test("it can delete a customer", () => { 149 | const command = new Command("apiKey123"); 150 | command.customers.delete("customerId123"); 151 | expect(axios).toHaveBeenCalledWith({ 152 | method: "delete", 153 | url: `https://api.oncommand.io/v1/customers/customerId123`, 154 | headers: { 155 | "x-api-key": "apiKey123" 156 | }, 157 | data: {} 158 | }); 159 | }); 160 | 161 | test("it throws an error if no customerId is passed to customers.delete", () => { 162 | expect(() => { 163 | const command = new Command("apiKey123"); 164 | command.customers.delete(); 165 | }).toThrow("Must pass a customerId."); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /release.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable consistent-return */ 2 | 3 | const { exec } = require("child_process"); 4 | const fs = require("fs"); 5 | 6 | const promiseExec = (command, messageIfError) => { 7 | try { 8 | return new Promise((resolve, reject) => { 9 | exec(command, (error, stdout, stderr) => { 10 | if (error) { 11 | reject(messageIfError || error); 12 | return; 13 | } 14 | 15 | resolve({ stdout, stderr }); 16 | }); 17 | }); 18 | } catch (exception) { 19 | throw new Error(`[release.promiseExec] ${exception.message}`); 20 | } 21 | }; 22 | 23 | const releaseToNPM = async version => { 24 | try { 25 | await promiseExec( 26 | `npm version ${version} --allow-same-version && npm publish --access public` 27 | ); // NOTE: --access public is due to the scoped package (@oncommandio/js). 28 | } catch (exception) { 29 | throw new Error(`[release.releaseToNPM] ${exception.message}`); 30 | } 31 | }; 32 | 33 | const tagReleaseOnGit = async version => { 34 | try { 35 | await promiseExec( 36 | `git tag -a ${version} -m "release ${version}" && git push origin ${version}` 37 | ); 38 | } catch (exception) { 39 | throw new Error(`[release.tagReleaseOnGit] ${exception.message}`); 40 | } 41 | }; 42 | 43 | const pushReleaseToGit = async () => { 44 | try { 45 | await promiseExec("git push origin master"); 46 | } catch (exception) { 47 | throw new Error(`[release.pushReleaseToGit] ${exception.message}`); 48 | } 49 | }; 50 | 51 | const commitReleaseToRepo = async version => { 52 | try { 53 | await promiseExec(`git add . && git commit -m "release v${version}"`); 54 | } catch (exception) { 55 | throw new Error(`[release.commitReleaseToRepo] ${exception.message}`); 56 | } 57 | }; 58 | 59 | const updateREADME = version => { 60 | try { 61 | fs.writeFileSync( 62 | "./README.md", 63 | `## Command.js\n\nOfficial JavaScript library for the Command API.\n\n[Read the Documentation](https://portal.oncommand.io/docs/command-js/${version}/introduction)` 64 | ); 65 | } catch (exception) { 66 | throw new Error(`[release.updateREADME] ${exception.message}`); 67 | } 68 | }; 69 | 70 | const setHomepageURL = version => { 71 | try { 72 | const packageJsonContents = fs.readFileSync("./package.json", "utf-8"); 73 | const packageJson = JSON.parse(packageJsonContents); 74 | packageJson.homepage = `https://portal.oncommand.io/docs/command-js/${version}/introduction`; 75 | const stringifiedPackageJson = JSON.stringify(packageJson, null, 2); 76 | fs.writeFileSync("./package.json", stringifiedPackageJson); 77 | } catch (exception) { 78 | throw new Error(`[release.setHomepageURL] ${exception.message}`); 79 | } 80 | }; 81 | 82 | const setReleaseVersionInSource = version => { 83 | try { 84 | const sourceContents = fs.readFileSync("./dist/index.min.js", "utf-8"); 85 | const sourceContentsWithVersion = sourceContents.replace( 86 | new RegExp('this.version=""'), 87 | `this.version="${version}"` 88 | ); 89 | fs.writeFileSync("./dist/index.min.js", sourceContentsWithVersion); 90 | } catch (exception) { 91 | throw new Error(`[release.setReleaseVersionInSource] ${exception.message}`); 92 | } 93 | }; 94 | 95 | const setProductionAPIURL = () => { 96 | try { 97 | const sourceContents = fs.readFileSync("./dist/index.min.js", "utf-8"); 98 | const developmentAPIRegex = new RegExp("http://localhost:4000/api", "ig"); 99 | const scriptContentsSanitized = sourceContents.replace( 100 | developmentAPIRegex, 101 | "https://api.oncommand.io" 102 | ); 103 | 104 | fs.writeFileSync("./dist/index.min.js", scriptContentsSanitized); 105 | } catch (exception) { 106 | throw new Error(`[release.setProductionAPIURL] ${exception.message}`); 107 | } 108 | }; 109 | 110 | const runBuild = async () => { 111 | try { 112 | await promiseExec("npm run build"); 113 | } catch (exception) { 114 | throw new Error(`[release.runBuild] ${exception.message}`); 115 | } 116 | }; 117 | 118 | const runTests = async () => { 119 | try { 120 | await promiseExec( 121 | "npm run test", 122 | "❌ Tests failed! Run npm test and correct errors before releasing. \n" 123 | ); 124 | } catch (exception) { 125 | throw new Error(`[release.runTests] ${exception}`); 126 | } 127 | }; 128 | 129 | const getMajorVerison = version => { 130 | try { 131 | const versionParts = version.split("."); 132 | return `v${versionParts[0]}`; 133 | } catch (exception) { 134 | throw new Error(`[release.getMajorVerison] ${exception.message}`); 135 | } 136 | }; 137 | 138 | const release = async version => { 139 | try { 140 | await runTests(); 141 | console.log("✅ Tests passed!"); 142 | 143 | await runBuild(); 144 | console.log("✅ Build complete!"); 145 | 146 | setProductionAPIURL(); 147 | console.log("✅ Production URLs updated!"); 148 | 149 | setReleaseVersionInSource(version); 150 | console.log("✅ Release version updated!"); 151 | 152 | setHomepageURL(version); 153 | console.log("✅ Homepage updated in package.json!"); 154 | 155 | updateREADME(version); 156 | console.log("✅ README.md updated!"); 157 | 158 | await commitReleaseToRepo(version); 159 | console.log("✅ Release committed to repo!"); 160 | 161 | await pushReleaseToGit(); 162 | console.log("✅ Release pushed to repo!"); 163 | 164 | await tagReleaseOnGit(version); 165 | console.log("✅ Version tag pushed to remote repo!"); 166 | 167 | await releaseToNPM(version); 168 | console.log("✅ Released to NPM!"); 169 | } catch (exception) { 170 | console.warn(`[release] ${exception.message}`); 171 | } 172 | }; 173 | 174 | release(process.argv[2]); 175 | --------------------------------------------------------------------------------