├── .compodocrc.json ├── .eslintrc.json ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jasmine.json ├── package-lock.json ├── package.json ├── src ├── argument-serializer-rules.spec.ts ├── argument-serializer-rules.ts ├── http │ ├── index.ts │ └── verb.ts ├── index.ts ├── interchange.ts ├── metadata.ts ├── request.spec.ts ├── request.ts ├── response.spec.ts ├── response.ts ├── uapi │ ├── index.ts │ ├── request.spec.ts │ ├── request.ts │ ├── response.spec.ts │ └── response.ts ├── utils │ ├── argument.spec.ts │ ├── argument.ts │ ├── encoders.spec.ts │ ├── encoders.ts │ ├── filter.spec.ts │ ├── filter.ts │ ├── headers.spec.ts │ ├── headers.ts │ ├── index.ts │ ├── json │ │ └── serializable.ts │ ├── location-service.ts │ ├── pager.spec.ts │ ├── pager.ts │ ├── path.ts │ ├── perl.spec.ts │ ├── perl.ts │ ├── sort.spec.ts │ └── sort.ts └── whmapi │ ├── index.ts │ ├── request.spec.ts │ ├── request.ts │ ├── response.spec.ts │ └── response.ts ├── tsconfig.base.json ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── tsconfig.prod-cjs.json ├── tsconfig.prod-esm.json └── webpack.config.js /.compodocrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hideGenerator": true, 3 | "disableSourceCode": true, 4 | "disablePrivate": true 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "overrides": [ 4 | { 5 | "files": ["*.ts"], 6 | "parser": "@typescript-eslint/parser", 7 | "plugins": ["@typescript-eslint"], 8 | "parserOptions": { 9 | "ecmaVersion": 6, 10 | "sourceType": "module", 11 | "project": "./tsconfig.base.json" 12 | }, 13 | "env": { 14 | "browser": true, 15 | "jasmine": true 16 | }, 17 | "extends": [ 18 | "eslint:recommended", 19 | "plugin:@typescript-eslint/eslint-recommended", 20 | "plugin:@typescript-eslint/recommended", 21 | "prettier" 22 | ], 23 | "rules": { 24 | "quotes": [ 25 | "error", 26 | "double", 27 | { "allowTemplateLiterals": true, "avoidEscape": true } 28 | ], 29 | "spaced-comment": ["error", "always", { "exceptions": ["-*"] }], 30 | "@typescript-eslint/no-explicit-any": "off" 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | documentation/ 3 | npm-debug.log 4 | dist/ 5 | docs/ 6 | coverage 7 | .nyc_output 8 | .vscode -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | # use npx22 if available else use npx 5 | if command -v /usr/local/cpanel/3rdparty/bin/npx22 >/dev/null 2>&1; then 6 | /usr/local/cpanel/3rdparty/bin/npx22 lint-staged 7 | else 8 | npx lint-staged 9 | fi 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | update-notifier=false -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | CHANGELOG.md 3 | .vscode 4 | coverage 5 | .nyc_output -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v3.0.0 2 | 3 | Initial open source release. 4 | 5 | * Reorganized build to use the modern ./dist directory. 6 | * Removed any references to cPanel internal build resources that would be inaccessible to external developers. 7 | * Updated the license to MIT for publication. 8 | 9 | # v3.0.1 10 | 11 | Removed testing files from ./dist. 12 | 13 | # v3.0.2 14 | 15 | Reworked the build so the developer build will can still run the tests. 16 | 17 | # v3.0.3 18 | 19 | Fixed the unit tests to run again. 20 | 21 | # v3.0.4 22 | 23 | Removed internal publication references. 24 | 25 | # v3.0.5 26 | 27 | Minor adjustments for the typescript publication in the npm module. 28 | 29 | # v3.0.6 30 | 31 | Changed to es5 code generation to get nodejs typescript to work. 32 | 33 | # v3.0.7 34 | 35 | Trying a different export strategy to get deep typescript object to publish. 36 | 37 | # v3.0.8 38 | 39 | Correctly exported the utils modules. 40 | 41 | # v3.0.9 42 | 43 | Updated the library version to remove low security threat 44 | 45 | # v3.0.10 46 | 47 | Add support for cPanel and WHM API tokens. 48 | Add support to convert headers to an object using toObject() or an array using toArray(). This simplifies use with some libraries. 49 | Fixed the examples in the README.md. 50 | 51 | # v4.0.0 52 | 53 | Switch default package management strategy to npm from yarn. 54 | 55 | # v4.0.1 56 | 57 | Patch to retain yarn support as older systems that utilize this require it. 58 | 59 | # v5.0.0 60 | 61 | Update @cpanel/API to support tree-shaking lodash 62 | 63 | # v5.1.0 64 | 65 | Supports consumers using both ES Modules and CommonJS. 66 | Fixes a lodash import that was causing code bloat. 67 | Adds developer tooling (eslint, prettier, husky). 68 | 69 | # v5.1.1 70 | 71 | Update prettier configuration to use 4-space tabs. Tidy. 72 | 73 | # v5.1.2 74 | 75 | Version lock dependencies. 76 | 77 | # v5.2.1 78 | 79 | Fixed export of types for more modern Node versions. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2021 cPanel L.L.C. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @cpanel/api Libraries 2 | 3 | cPanel API JavaScript and TypeScript interface libraries. 4 | 5 | This library provides a set of classes for calling cPanel WHM API 1 and UAPI calls. The classes hide much of the complexity of these API's behind classes that abstract the underlying variances between these API systems. Users of this library can focus on what they want to accomplish rather than having to learn the various complexities of the underlying wire formats for each of the cPanel product's APIs. 6 | 7 | ## Installing @cpanel/api 8 | 9 | To install these libraries with NPM, run: 10 | 11 | ```sh 12 | npm install @cpanel/api 13 | ``` 14 | 15 | ## Using @cpanel/api 16 | 17 | ### TypeScript 18 | 19 | #### Calling a WHM API 1 function 20 | 21 | ```ts 22 | import { 23 | Argument, 24 | WhmApiResponse, 25 | WhmApiRequest, 26 | WhmApiType, 27 | WhmApiTokenHeader, 28 | } from "@cpanel/api"; 29 | 30 | const token = "...paste your API token here..."; 31 | const request = new WhmApiRequest(WhmApiType.JsonApi, { 32 | method: "api_token_create", 33 | arguments: [ 34 | new Argument("token_name", "my-Auth-Token"), 35 | // ---- API Token Permissions ---- 36 | // Login to the UI 37 | new Argument("acl", "create-user-session"), 38 | // Delete a token 39 | new Argument("acl", "manage-api-tokens"), 40 | ], 41 | headers: [new WhmApiTokenHeader(token, "root")], 42 | }).generate(); 43 | 44 | fetch("http://my-cpanel-server.com:2087", { 45 | method: "POST", 46 | headers: request.headers.toObject(), 47 | body: request.body, 48 | }) 49 | .then((response) => response.json()) 50 | .then((response) => { 51 | response.data = new WhmApiResponse(response.data); 52 | if (!response.data.status) { 53 | throw new Error(response.data.errors[0].message); 54 | } 55 | return response; 56 | }) 57 | .then((data) => console.log(data)); 58 | ``` 59 | 60 | ### JavaScript 61 | 62 | #### Calling a WHM API 1 function 63 | 64 | ```js 65 | let { 66 | Argument, 67 | WhmApiResponse, 68 | WhmApiRequest, 69 | WhmApiType, 70 | WhmApiTokenHeader 71 | } = require("@cpanel/api"); 72 | 73 | const token = "...paste your API token here..."; 74 | const request = new WhmApiRequest(WhmApiType.JsonApi, { 75 | method: "api_token_create", 76 | arguments: [ 77 | new Argument('token_name', 'my-Auth-Token'), 78 | // ---- API Token Permissions ---- 79 | // Login to the UI 80 | new Argument('acl', 'create-user-session'), 81 | // Delete a token 82 | new Argument('acl', 'manage-api-tokens'), 83 | ], 84 | headers: [ 85 | new WhmApiTokenHeader(token, 'root'), 86 | ], 87 | }).generate(); 88 | 89 | fetch('http://my-cpanel-server.com:2087', { 90 | method: 'POST', 91 | headers: request.headers.toObject()), 92 | body: request.body 93 | }) 94 | .then(response => response.json()) 95 | .then(response => { 96 | response.data = new WhmApiResponse(response.data); 97 | if(!response.data.status) { 98 | throw new Error(response.data.errors[0].message); 99 | } 100 | return response; 101 | }) 102 | .then(data => console.log(data)); 103 | ``` 104 | 105 | ## Development 106 | 107 | 1. Set up your development environment to test the local version of your library rather than the one distributed on `npm.dev.cpanel.net`: 108 | 109 | ```sh 110 | npm install --also=dev 111 | npm run build:dev 112 | ``` 113 | 114 | 2. Make the changes to the library. 115 | 3. Rebuild the library: 116 | 117 | ```sh 118 | npm run build:dev 119 | ``` 120 | 121 | ## Testing 122 | 123 | To install the development dependencies, run: 124 | 125 | ```sh 126 | npm install --also=dev 127 | npm run test 128 | ``` 129 | 130 | ## Contributing 131 | 132 | The maintainer will evaluate all bugs and feature requests, and reserves the right to reject a request for any reason. 133 | 134 | ### Bugs 135 | 136 | Please submit bugs via the github [issue tracker](https://github.com/CpanelInc/cpanel-node-api/issues). Bug reports must include the following: 137 | 138 | 1. The version of the @cpanel/api library. 139 | 2. The version of cPanel & WHM you are testing against. 140 | 3. A step by step set of instructions on how to reproduce the bug. 141 | 4. Sample code that reproduces the bug. 142 | 143 | The maintainers will evaluate all bugs. 144 | 145 | ### Improvements and Feature Requests 146 | 147 | Please submit feature requests via the github [issue tracker](https://github.com/CpanelInc/cpanel-node-api/issues). 148 | 149 | Describe the feature in detail. Try to include information on why the suggested feature would be valuable and under what scenarios. 150 | 151 | ### Pull requests 152 | 153 | We welcome pull requests against the @cpanel/api library. 154 | 155 | The maintainers will evaluate all pull requests. Pull requests are subject to review by the maintainers. We may request additional adjustments to any submitted pull requests. 156 | 157 | Any code submitted via pull requests that is incorporated into the library will become the property of cPanel L.L.C. and will be published under the cPanel L.L.C. copyright and the MIT license. 158 | 159 | ## Publishing 160 | 161 | **Note** Publishing is limited to select cPanel maintainers. 162 | 163 | When your changes are implemented and tested, and you're ready to publish, run: 164 | 165 | ### Developer publishing (publishing an alpha build for testing) 166 | 167 | 1. As part of the development changes update the `version` property in `package.json` to the format `X.X.X-alpha.X` so that the semver is updated correctly and appended with alpha build information (1.0.0 -> 1.0.1-alpha.1). 168 | 2. Request maintainers of the repository to publish alpha builds using `npm publish` 169 | 170 | ### Production publishing (This will be done by a UI3) 171 | 172 | 1. When dev changes are accepted and complete update the `version` property in `package.json` to the format `X.X.X` so that the semver is updated correctly and alpha build information has been removed. Do this on the development branch before merging pull request (1.0.1-alpha.1 -> 1.0.1). 173 | 2. Request maintainers of the repository to publish your latest changes using `npm publish` post merge. 174 | 175 | ## Authors 176 | 177 | - **Team Phoenix @ cPanel** 178 | - **Team Artemis @ cPanel** 179 | - **Team Cobra @ cPanel** 180 | - **Team Moonshot @ cPanel** 181 | 182 | ### Contributors 183 | 184 | - Thomas Green 185 | - Sruthi Sanigarapu 186 | - Aneece Yazdani 187 | - Sarah Kiniry 188 | - Dustin Scherer 189 | - Philip King 190 | - Caitlin Flattery 191 | - Aspen Hollyer 192 | 193 | ## License 194 | 195 | Copyright © 2024 cPanel, L.L.C. 196 | Licensed under the included [MIT](https://github.com/CpanelInc/cpanel-node-api/blob/main/LICENSE) license. 197 | -------------------------------------------------------------------------------- /jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "./dist", 3 | "spec_files": ["**/*.spec.js"], 4 | "stopSpecOnExpectationFailure": true, 5 | "random": false 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cpanel/api", 3 | "version": "6.0.0", 4 | "description": "cPanel API JavaScript and TypeScript interface libraries. This library provides a set of classes for calling cPanel WHM API 1 and UAPI calls. The classes hide much of the complexity of these APIs behind classes the abstract the underlying variances between these API systems. Users of this library can focus on what they want to accomplish rather the having to learn the various complexities of the underlying wire formats for each of the cPanel Products APIs", 5 | "main": "dist/cjs/index.js", 6 | "module": "dist/esm/index.js", 7 | "types": "dist/types/index.d.ts", 8 | "exports": { 9 | "import": "./dist/esm/index.js", 10 | "require": "./dist/cjs/index.js", 11 | "default": "./dist/esm/index.js", 12 | "types": "./dist/types/index.d.ts" 13 | }, 14 | "sideEffects": false, 15 | "files": [ 16 | "*.d.ts", 17 | "*.js", 18 | "*.map.js", 19 | "**/*.d.ts", 20 | "**/*.js", 21 | "**/*.map.js", 22 | "documentation/**/*" 23 | ], 24 | "lint-staged": { 25 | "*.ts": "npx eslint", 26 | "**/*": "prettier --write --ignore-unknown" 27 | }, 28 | "scripts": { 29 | "build:prod": "npm run clean && npm run build:ts && npm run webpack:prod", 30 | "build:dev": "npm run clean && npm run build:ts-dev && npm run webpack:dev", 31 | "build:ts": "tsc -p tsconfig.prod-esm.json & tsc -p tsconfig.prod-cjs.json", 32 | "build:ts-dev": "tsc -p tsconfig.esm.json & tsc -p tsconfig.cjs.json", 33 | "build:test-watch": "tsc -w -p tsconfig.cjs.json", 34 | "webpack:dev": "webpack --env dev", 35 | "webpack:prod": "webpack --env prod", 36 | "clean": "rm -rf dist", 37 | "build-docs": "compodoc -p tsconfig.esm.json -d ./docs", 38 | "serve-docs": "compodoc -p tsconfig.esm.json -d ./docs -r 4214 -s", 39 | "test": "npm run build:dev && jasmine --config=jasmine.json --reporter=jasmine-console-reporter", 40 | "test:cover": "npm run build:dev && nyc -r lcov -x '**/*.spec.ts' jasmine --config=jasmine.json && nyc report", 41 | "test:watch": "npm run build:test-watch && onchange -i -v -d 100 dist -- jasmine --config=jasmine.json --reporter=jasmine-console-reporter", 42 | "prepare": "husky install", 43 | "prepublish": "npm run build:prod", 44 | "lint": "npx eslint 'src/**/*.ts'", 45 | "lint:fix": "npx eslint 'src/**/*.ts' --fix", 46 | "tidy": "npx prettier --write .", 47 | "tidy:check": "npx prettier --check .", 48 | "tidy:file": "npx prettier --write ", 49 | "tidy:watch": "onchange \"**/*\" -- prettier --write --ignore-unknown {{changed}}" 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "url": "https://github.com/CpanelInc/cpanel-node-api" 54 | }, 55 | "author": "cPanel L.L.C.", 56 | "contributors": [ 57 | { 58 | "name": "Sruthi Sanigarapu", 59 | "email": "sruthi@cpanel.net" 60 | }, 61 | { 62 | "name": "Caitlin Flattery", 63 | "email": "c.flattery@cpanel.net" 64 | }, 65 | { 66 | "name": "Sarah Kiniry", 67 | "email": "sarah.kiniry@cpanel.net" 68 | }, 69 | { 70 | "name": "Thomas Green", 71 | "email": "tomg@cpanel.net" 72 | }, 73 | { 74 | "name": "Aneece Yazdani", 75 | "email": "aneece@cpanel.net" 76 | }, 77 | { 78 | "name": "Philip King", 79 | "email": "phil@cpanel.net" 80 | }, 81 | { 82 | "name": "Dustin Scherer", 83 | "email": "dustin.scherer@cpanel.net" 84 | }, 85 | { 86 | "name": "Aspen Hollyer", 87 | "email": "a.hollyer@cpanel.net" 88 | } 89 | ], 90 | "license": "MIT", 91 | "bugs": { 92 | "url": "https://github.com/CpanelInc/cpanel-node-api/issues" 93 | }, 94 | "homepage": "https://cpanel.com", 95 | "dependencies": { 96 | "lodash": "4.17.21" 97 | }, 98 | "devDependencies": { 99 | "@compodoc/compodoc": "1.1.25", 100 | "@types/jasmine": "5.1.4", 101 | "@types/lodash": "4.17.9", 102 | "@types/node": "17.0.23", 103 | "@typescript-eslint/eslint-plugin": "7.18.0", 104 | "eslint": "8.57.1", 105 | "eslint-config-prettier": "8.5.0", 106 | "http-server": "0.12.3", 107 | "husky": "8.0.1", 108 | "jasmine": "5.3.0", 109 | "jasmine-console-reporter": "3.1.0", 110 | "lint-staged": "13.0.3", 111 | "nyc": "17.1.0", 112 | "onchange": "7.1.0", 113 | "prettier": "3.3.3", 114 | "tslib": "2.7.0", 115 | "typescript": "5.4.5", 116 | "webpack": "5.95.0", 117 | "webpack-cli": "5.1.4" 118 | }, 119 | "keywords": [ 120 | "api", 121 | "uapi", 122 | "whmapi", 123 | "cpanel", 124 | "whm" 125 | ] 126 | } 127 | -------------------------------------------------------------------------------- /src/argument-serializer-rules.spec.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { HttpVerb } from "./http/verb"; 24 | 25 | import { argumentSerializationRules } from "./argument-serializer-rules"; 26 | 27 | describe("ArgumentSerializationRules getRule()", () => { 28 | it("should return a predefined rule when passed HttpVerb.GET", () => { 29 | const rule = argumentSerializationRules.getRule(HttpVerb.GET); 30 | expect(rule).toBeDefined(); 31 | expect(rule.verb).toBe("GET"); 32 | expect(rule.dataInBody).toBe(false); 33 | }); 34 | 35 | it("should return a predefined rule when passed HttpVerb.DELETE", () => { 36 | const rule = argumentSerializationRules.getRule(HttpVerb.DELETE); 37 | expect(rule).toBeDefined(); 38 | expect(rule.verb).toBe("DELETE"); 39 | expect(rule.dataInBody).toBe(false); 40 | }); 41 | 42 | it("should return a predefined rule when passed HttpVerb.HEAD", () => { 43 | const rule = argumentSerializationRules.getRule(HttpVerb.HEAD); 44 | expect(rule).toBeDefined(); 45 | expect(rule.verb).toBe("HEAD"); 46 | expect(rule.dataInBody).toBe(false); 47 | }); 48 | 49 | it("should return a predefined rule when passed HttpVerb.POST", () => { 50 | const rule = argumentSerializationRules.getRule(HttpVerb.POST); 51 | expect(rule).toBeDefined(); 52 | expect(rule.verb).toBe("POST"); 53 | expect(rule.dataInBody).toBe(true); 54 | }); 55 | 56 | it("should return a predefined rule when passed HttpVerb.PUT", () => { 57 | const rule = argumentSerializationRules.getRule(HttpVerb.PUT); 58 | expect(rule).toBeDefined(); 59 | expect(rule.verb).toBe("PUT"); 60 | expect(rule.dataInBody).toBe(true); 61 | }); 62 | 63 | it("should return a predefined rule when passed HttpVerb.PATCH", () => { 64 | const rule = argumentSerializationRules.getRule(HttpVerb.PATCH); 65 | expect(rule).toBeDefined(); 66 | expect(rule.verb).toBe("PATCH"); 67 | expect(rule.dataInBody).toBe(true); 68 | }); 69 | 70 | it("should return a predefined rule when passed an unrecognized verb", () => { 71 | const rule = argumentSerializationRules.getRule("CUSTOM"); 72 | expect(rule).toBeDefined(); 73 | expect(rule.verb).toBe("DEFAULT"); 74 | expect(rule.dataInBody).toBe(true); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/argument-serializer-rules.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { HttpVerb } from "./http/verb"; 24 | 25 | export type Nullable = { [P in keyof T]: T[P] | null }; 26 | 27 | /** 28 | * Abstract argument serialization rule 29 | */ 30 | export interface ArgumentSerializationRule { 31 | /** 32 | * Name of the verb or DEFAULT 33 | */ 34 | verb: string; 35 | 36 | /** 37 | * Flag to indicate if the data is in the body. If false, its in the url. 38 | */ 39 | dataInBody: boolean; 40 | } 41 | 42 | /** 43 | * Collection of argument serialize rules 44 | */ 45 | export type ArgumentSerializationRules = { 46 | [key: string]: ArgumentSerializationRule; 47 | }; 48 | 49 | /** 50 | * Default argument serialization rules for each well known HTTP verb. 51 | */ 52 | export class DefaultArgumentSerializationRules { 53 | map: ArgumentSerializationRules = {}; 54 | 55 | /** 56 | * Construct the lookup table for well know verbs. 57 | */ 58 | constructor() { 59 | // fallback rule if the verb is not defined. 60 | this.map["DEFAULT"] = { 61 | verb: "DEFAULT", 62 | dataInBody: true, 63 | }; 64 | 65 | [HttpVerb.GET, HttpVerb.DELETE, HttpVerb.HEAD].forEach( 66 | (verb: HttpVerb) => { 67 | const label = HttpVerb[verb].toString(); 68 | this.map[label] = { 69 | verb: label, 70 | dataInBody: false, 71 | }; 72 | }, 73 | ); 74 | 75 | [HttpVerb.POST, HttpVerb.PUT, HttpVerb.PATCH].forEach( 76 | (verb: HttpVerb) => { 77 | const label = HttpVerb[verb].toString(); 78 | this.map[label] = { 79 | verb: label, 80 | dataInBody: true, 81 | }; 82 | }, 83 | ); 84 | } 85 | 86 | /** 87 | * Get a rule for serialization of arguments. This tells the generators where 88 | * argument data is packaged in a request. Arguments can be located in one of 89 | * the following: 90 | * 91 | * Body, 92 | * Url 93 | * 94 | * @param verb verb to lookup. 95 | */ 96 | getRule(verb: HttpVerb | string): ArgumentSerializationRule { 97 | const name: string = 98 | typeof verb === "string" ? verb : HttpVerb[verb].toString(); 99 | let rule = this.map[name]; 100 | if (!rule) { 101 | rule = this.map["DEFAULT"]; 102 | } 103 | return rule; 104 | } 105 | } 106 | 107 | /** 108 | * Singleton with the default argument serialization rules in it. 109 | */ 110 | const argumentSerializationRules = new DefaultArgumentSerializationRules(); 111 | 112 | export { argumentSerializationRules }; 113 | -------------------------------------------------------------------------------- /src/http/index.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { HttpVerb } from "./verb"; 24 | 25 | export { HttpVerb }; 26 | -------------------------------------------------------------------------------- /src/http/verb.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | /** 24 | * Common http verbs 25 | */ 26 | export enum HttpVerb { 27 | /** 28 | * Get request 29 | */ 30 | GET, 31 | 32 | /** 33 | * Head request 34 | */ 35 | HEAD, 36 | 37 | /** 38 | * Post request 39 | */ 40 | POST, 41 | 42 | /** 43 | * Put request 44 | */ 45 | PUT, 46 | 47 | /** 48 | * Delete request 49 | */ 50 | DELETE, 51 | 52 | /** 53 | * Connect request 54 | */ 55 | CONNECT, 56 | 57 | /** 58 | * Options request 59 | */ 60 | OPTIONS, 61 | 62 | /** 63 | * Trace request 64 | */ 65 | TRACE, 66 | 67 | /** 68 | * Patch request 69 | */ 70 | PATCH, 71 | } 72 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | export * from "./argument-serializer-rules"; 24 | export * from "./interchange"; 25 | export * from "./metadata"; 26 | export * from "./request"; 27 | export * from "./response"; 28 | export * from "./http"; 29 | export * from "./utils"; 30 | export * from "./uapi"; 31 | export * from "./whmapi"; 32 | -------------------------------------------------------------------------------- /src/interchange.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { Headers } from "./utils/headers"; 24 | 25 | /** 26 | * Abstract data structure used to pass rendered API information to the remoting layer. 27 | */ 28 | export interface RequestInfo { 29 | /** 30 | * List of headers for the request 31 | */ 32 | headers: Headers; 33 | 34 | /** 35 | * URL used to make the request. 36 | */ 37 | url: string; 38 | 39 | /** 40 | * Body of the request. 41 | */ 42 | body: string; 43 | } 44 | -------------------------------------------------------------------------------- /src/metadata.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | /** 24 | * Common interface for metadata. 25 | */ 26 | export interface IMetaData { 27 | /** 28 | * Indicates if the data is paged. 29 | */ 30 | isPaged: boolean; 31 | 32 | /** 33 | * The record number of the first record of a page. 34 | */ 35 | record: number; 36 | 37 | /** 38 | * The current page. 39 | */ 40 | page: number; 41 | 42 | /** 43 | * The page size of the returned set. 44 | */ 45 | pageSize: number; 46 | 47 | /** 48 | * The total number of records available on the backend. 49 | */ 50 | totalRecords: number; 51 | 52 | /** 53 | * The total number of pages of records on the backend. 54 | */ 55 | totalPages: number; 56 | 57 | /** 58 | * Indicates if the data set is filtered. 59 | */ 60 | isFiltered: boolean; 61 | 62 | /** 63 | * Number of records available before the filter was processed. 64 | */ 65 | recordsBeforeFilter: number; 66 | 67 | /** 68 | * Indicates the response was the result of a batch API. 69 | */ 70 | batch: boolean; 71 | 72 | /** 73 | * A collection of the other less common or custom UAPI metadata properties. 74 | */ 75 | properties: { [index: string]: string }; 76 | } 77 | -------------------------------------------------------------------------------- /src/request.spec.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import * as Perl from "./utils/perl"; 24 | 25 | import { IRequest, Request } from "./request"; 26 | 27 | import { Argument } from "./utils/argument"; 28 | 29 | import { Sort, SortDirection, SortType } from "./utils/sort"; 30 | 31 | import { Filter, FilterOperator } from "./utils/filter"; 32 | 33 | import { Pager } from "./utils/pager"; 34 | 35 | import { Headers } from "./utils/headers"; 36 | 37 | import { RequestInfo } from "./interchange"; 38 | 39 | /** 40 | * This class implements the abstract method from the base class 41 | * so we can test the base class constructor and methods. 42 | */ 43 | class MockRequest extends Request { 44 | constructor(init?: IRequest) { 45 | super(init); 46 | } 47 | 48 | generate(): RequestInfo { 49 | return { 50 | headers: new Headers([ 51 | { name: "content-type", value: "text/plain" }, 52 | ]), 53 | url: "/execute/test/get_tests", 54 | body: "", 55 | }; 56 | } 57 | } 58 | 59 | describe("Request derived object that implements generate", () => { 60 | it("should be creatable", () => { 61 | const request = new MockRequest(); 62 | expect(request).toBeDefined(); 63 | }); 64 | 65 | it("should accept a configuration argument", () => { 66 | const request = new MockRequest({ 67 | namespace: "test", 68 | method: "test", 69 | config: { analytics: false }, 70 | }); 71 | expect(request).toBeDefined(); 72 | expect(request.config).toEqual({ analytics: false }); 73 | expect(request.namespace).toBe("test"); 74 | expect(request.method).toBe("test"); 75 | }); 76 | 77 | it("should accept an simple initialization object", () => { 78 | const request = new MockRequest({ 79 | namespace: "test", 80 | method: "get_tests", 81 | }); 82 | expect(request).toBeDefined(); 83 | expect(request.config).toBeDefined(); 84 | expect(request.namespace).toBe("test"); 85 | expect(request.method).toBe("get_tests"); 86 | }); 87 | 88 | it("should accept a complex initialization object", () => { 89 | const request = new MockRequest({ 90 | namespace: "test", 91 | method: "get_tests", 92 | arguments: [ 93 | { 94 | name: "set", 95 | value: "unit", 96 | }, 97 | ], 98 | sorts: [ 99 | { 100 | column: "name", 101 | direction: SortDirection.Ascending, 102 | type: SortType.Lexicographic, 103 | }, 104 | ], 105 | filters: [ 106 | { 107 | column: "enabled", 108 | operator: FilterOperator.Equal, 109 | value: Perl.fromBoolean(true), 110 | }, 111 | ], 112 | columns: ["name", "description", "steps", "set"], 113 | pager: { page: 1, pageSize: 20 }, 114 | config: { json: true }, 115 | }); 116 | expect(request).toBeDefined(); 117 | expect(request.config).toEqual({ json: true }); 118 | expect(request.namespace).toBe("test"); 119 | expect(request.method).toBe("get_tests"); 120 | expect(request.arguments).toEqual([new Argument("set", "unit")]); 121 | expect(request.sorts).toEqual([ 122 | new Sort("name", SortDirection.Ascending, SortType.Lexicographic), 123 | ]); 124 | expect(request.filters).toEqual([ 125 | new Filter("enabled", FilterOperator.Equal, "1"), 126 | ]); 127 | expect(request.columns).toEqual([ 128 | "name", 129 | "description", 130 | "steps", 131 | "set", 132 | ]); 133 | expect(request.pager as unknown).toEqual( 134 | jasmine.objectContaining({ page: 1, pageSize: 20 }), 135 | ); 136 | }); 137 | 138 | describe("when the addArgument() method is called", () => { 139 | let request: MockRequest; 140 | beforeEach(() => { 141 | request = new MockRequest({ 142 | namespace: "test", 143 | method: "get_tests", 144 | }); 145 | }); 146 | 147 | it("should add a name/value argument", () => { 148 | request.addArgument({ name: "set", value: "unit" }); 149 | expect(request.arguments).toBeDefined(); 150 | expect(request.arguments.length).toBe(1); 151 | expect(request.arguments[0]).toEqual(new Argument("set", "unit")); 152 | }); 153 | 154 | it("should add a name/value argument", () => { 155 | request.addArgument(new Argument("set", "unit")); 156 | expect(request.arguments).toBeDefined(); 157 | expect(request.arguments.length).toBe(1); 158 | expect(request.arguments[0]).toEqual(new Argument("set", "unit")); 159 | }); 160 | }); 161 | 162 | describe("when the addSort() method is called", () => { 163 | let request: MockRequest; 164 | beforeEach(() => { 165 | request = new MockRequest({ 166 | namespace: "test", 167 | method: "get_tests", 168 | }); 169 | }); 170 | 171 | it("should sorting rule using interface", () => { 172 | request.addSort({ 173 | column: "name", 174 | direction: SortDirection.Descending, 175 | type: SortType.Lexicographic, 176 | }); 177 | expect(request.sorts).toBeDefined(); 178 | expect(request.sorts.length).toBe(1); 179 | expect(request.sorts[0]).toEqual( 180 | new Sort( 181 | "name", 182 | SortDirection.Descending, 183 | SortType.Lexicographic, 184 | ), 185 | ); 186 | }); 187 | 188 | it("should sorting rule using object", () => { 189 | request.addSort( 190 | new Sort( 191 | "name", 192 | SortDirection.Descending, 193 | SortType.Lexicographic, 194 | ), 195 | ); 196 | expect(request.sorts).toBeDefined(); 197 | expect(request.sorts.length).toBe(1); 198 | expect(request.sorts[0]).toEqual( 199 | new Sort( 200 | "name", 201 | SortDirection.Descending, 202 | SortType.Lexicographic, 203 | ), 204 | ); 205 | }); 206 | }); 207 | 208 | describe("when the addFilter() method is called", () => { 209 | let request: MockRequest; 210 | beforeEach(() => { 211 | request = new MockRequest({ 212 | namespace: "test", 213 | method: "get_tests", 214 | }); 215 | }); 216 | 217 | it("should filter using interface", () => { 218 | request.addFilter({ 219 | column: "enabled", 220 | operator: FilterOperator.Equal, 221 | value: Perl.fromBoolean(true), 222 | }); 223 | expect(request.filters).toBeDefined(); 224 | expect(request.filters.length).toBe(1); 225 | expect(request.filters[0]).toEqual( 226 | new Filter( 227 | "enabled", 228 | FilterOperator.Equal, 229 | Perl.fromBoolean(true), 230 | ), 231 | ); 232 | }); 233 | 234 | it("should filter using object", () => { 235 | request.addFilter( 236 | new Filter( 237 | "enabled", 238 | FilterOperator.Equal, 239 | Perl.fromBoolean(true), 240 | ), 241 | ); 242 | expect(request.filters).toBeDefined(); 243 | expect(request.filters.length).toBe(1); 244 | expect(request.filters[0]).toEqual( 245 | new Filter( 246 | "enabled", 247 | FilterOperator.Equal, 248 | Perl.fromBoolean(true), 249 | ), 250 | ); 251 | }); 252 | }); 253 | 254 | describe("when the addColumn() method is called", () => { 255 | let request: MockRequest; 256 | beforeEach(() => { 257 | request = new MockRequest({ 258 | namespace: "test", 259 | method: "get_tests", 260 | }); 261 | }); 262 | 263 | it("should add a column", () => { 264 | request.addColumn("name"); 265 | expect(request.columns).toBeDefined(); 266 | expect(request.columns.length).toBe(1); 267 | expect(request.columns[0]).toEqual("name"); 268 | }); 269 | }); 270 | 271 | describe("when the paginate() method is called", () => { 272 | let request: MockRequest; 273 | beforeEach(() => { 274 | request = new MockRequest({ 275 | namespace: "test", 276 | method: "get_tests", 277 | }); 278 | }); 279 | 280 | it("should add pager using the interface", () => { 281 | request.paginate({ page: 10, pageSize: 25 }); 282 | expect(request.pager as unknown).toEqual( 283 | jasmine.objectContaining({ page: 10, pageSize: 25 }), 284 | ); 285 | }); 286 | 287 | it("should add pager using object", () => { 288 | request.paginate(new Pager(10, 25)); 289 | expect(request.pager as unknown).toEqual( 290 | jasmine.objectContaining({ page: 10, pageSize: 25 }), 291 | ); 292 | }); 293 | }); 294 | }); 295 | -------------------------------------------------------------------------------- /src/request.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { IArgument, Argument } from "./utils/argument"; 24 | import { IFilter, Filter } from "./utils/filter"; 25 | import { IPager, Pager } from "./utils/pager"; 26 | import { ISort, Sort } from "./utils/sort"; 27 | import { RequestInfo } from "./interchange"; 28 | import { Header, Headers, CustomHeader } from "./utils/headers"; 29 | import { IArgumentEncoder } from "./utils/encoders"; 30 | import { HttpVerb } from "./http/verb"; 31 | 32 | /** 33 | * Rule used to convert a request to the interchange format 34 | */ 35 | export interface GenerateRule { 36 | /** 37 | * Http verb 38 | */ 39 | verb: HttpVerb | string; 40 | 41 | /** 42 | * Specific Argument Encoder 43 | */ 44 | encoder: IArgumentEncoder; 45 | } 46 | 47 | /** 48 | * Other top level options for the construction of the request. 49 | */ 50 | export interface IRequestConfiguration { 51 | /** 52 | * Enable analytics for the request 53 | */ 54 | analytics?: boolean; 55 | 56 | /** 57 | * Encode the arguments as a JSON object 58 | */ 59 | json?: boolean; 60 | } 61 | 62 | /** 63 | * Interface for developers that just want to pass a object literal 64 | */ 65 | export interface IRequest { 66 | /** 67 | * Namespace where the API call lives 68 | */ 69 | namespace?: string; 70 | 71 | /** 72 | * Method name of the API call. 73 | */ 74 | method: string; 75 | 76 | /** 77 | * Optional list of arguments for the API call. You can use types 78 | * * Argument or IArgument 79 | */ 80 | arguments?: IArgument[]; 81 | 82 | /** 83 | * Optional list of sorting rules to pass to the API call. 84 | */ 85 | sorts?: ISort[]; 86 | 87 | /** 88 | * Optional list of filter rules to pass to the API call. 89 | */ 90 | filters?: IFilter[]; 91 | 92 | /** 93 | * Optional list of columns to include with the response to the API call. 94 | */ 95 | columns?: string[]; 96 | 97 | /** 98 | * Optional pager rule to pass to the API. 99 | */ 100 | pager?: IPager; 101 | 102 | /** 103 | * Optional additional configuration for the request. 104 | */ 105 | config?: IRequestConfiguration; 106 | 107 | /** 108 | * Optional additional HTTP headers for the request. 109 | */ 110 | headers?: Header[]; 111 | } 112 | 113 | /** 114 | * Extra information about the request that generates the request. 115 | */ 116 | export interface IRequestMeta { 117 | /** 118 | * Request object that generated the RequestInfo object. 119 | * @type {Request} 120 | */ 121 | request: Request; 122 | } 123 | 124 | /** 125 | * Extra information about the batch request that generates the request. 126 | */ 127 | export interface IBatchRequestMeta { 128 | /** 129 | * List of abstract request objects that make up the batch that generated the RequestInfo object. 130 | */ 131 | requests: Request[]; 132 | } 133 | 134 | /** 135 | * Abstract base class for all Request objects. Developers should 136 | * create a subclass of this that implements the generate() method. 137 | */ 138 | export abstract class Request { 139 | /** 140 | * Namespace where the API call lives 141 | * @type {string} 142 | */ 143 | public namespace = ""; 144 | 145 | /** 146 | * Method name of the API call. 147 | * @type {string} 148 | */ 149 | public method = ""; 150 | 151 | /** 152 | * Optional list of arguments for the API call. 153 | * @type {IArgument[]} 154 | */ 155 | public arguments: IArgument[] = []; 156 | 157 | /** 158 | * Optional list of sorting rules to pass to the API call. 159 | */ 160 | public sorts: Sort[] = []; 161 | 162 | /** 163 | * Optional list of filter rules to pass to the API call. 164 | */ 165 | public filters: Filter[] = []; 166 | 167 | /** 168 | * Optional list of columns to include with the response to the API call. 169 | */ 170 | public columns: string[] = []; 171 | 172 | /** 173 | * Optional pager rule to pass to the API. 174 | */ 175 | public pager: Pager = new Pager(); 176 | 177 | /** 178 | * Optional custom headers collection 179 | */ 180 | public headers: Headers = new Headers(); 181 | 182 | private _usePager = false; 183 | 184 | /** 185 | * Use the pager only if true. 186 | */ 187 | public get usePager(): boolean { 188 | return this._usePager; 189 | } 190 | 191 | /** 192 | * Default configuration object. 193 | */ 194 | private defaultConfig: IRequestConfiguration = { 195 | analytics: false, 196 | json: false, 197 | }; 198 | 199 | /** 200 | * Optional configuration information 201 | */ 202 | public config: IRequestConfiguration = this.defaultConfig; 203 | 204 | /** 205 | * Create a new request. 206 | * 207 | * @param init Optional request object used to initialize this object. 208 | */ 209 | constructor(init?: IRequest) { 210 | if (init) { 211 | this.method = init.method; 212 | if (init.namespace) { 213 | this.namespace = init.namespace; 214 | } 215 | 216 | if (init.arguments) { 217 | init.arguments.forEach((argument) => { 218 | this.addArgument(argument); 219 | }); 220 | } 221 | 222 | if (init.sorts) { 223 | init.sorts.forEach((sort) => { 224 | this.addSort(sort); 225 | }); 226 | } 227 | 228 | if (init.filters) { 229 | init.filters.forEach((filter) => { 230 | this.addFilter(filter); 231 | }); 232 | } 233 | 234 | if (init.columns) { 235 | init.columns.forEach((column) => this.addColumn(column)); 236 | } 237 | 238 | if (init.pager) { 239 | this.paginate(init.pager); 240 | } 241 | 242 | if (init.config) { 243 | this.config = init.config; 244 | } else { 245 | this.config = this.defaultConfig; 246 | } 247 | 248 | if (init.headers) { 249 | init.headers.forEach((header) => { 250 | this.addHeader(header); 251 | }); 252 | } 253 | } 254 | } 255 | 256 | /** 257 | * Add an argument to the request. 258 | * 259 | * @param argument 260 | * @return Updated Request object. 261 | */ 262 | addArgument(argument: IArgument): Request { 263 | if (argument instanceof Argument) { 264 | this.arguments.push(argument); 265 | } else { 266 | this.arguments.push(new Argument(argument.name, argument.value)); 267 | } 268 | return this; 269 | } 270 | 271 | /** 272 | * Add sorting rule to the request. 273 | * 274 | * @param sort Sort object with sorting information. 275 | * @return Updated Request object. 276 | */ 277 | addSort(sort: ISort): Request { 278 | if (sort instanceof Sort) { 279 | this.sorts.push(sort); 280 | } else { 281 | this.sorts.push(new Sort(sort.column, sort.direction, sort.type)); 282 | } 283 | return this; 284 | } 285 | 286 | /** 287 | * Add a filter to the request. 288 | * 289 | * @param filter Filter object with filter information. 290 | * @return Updated Request object. 291 | */ 292 | addFilter(filter: IFilter): Request { 293 | if (filter instanceof Filter) { 294 | this.filters.push(filter); 295 | } else { 296 | this.filters.push( 297 | new Filter(filter.column, filter.operator, filter.value), 298 | ); 299 | } 300 | return this; 301 | } 302 | 303 | /** 304 | * Add a column to include in the request. If no columns are specified, all columns are retrieved. 305 | * 306 | * @param name Name of a column 307 | * @return Updated Request object. 308 | */ 309 | addColumn(column: string): Request { 310 | this.columns.push(column); 311 | return this; 312 | } 313 | 314 | /** 315 | * Add a custom http header to the request 316 | * 317 | * @param name Name of a column 318 | * @return Updated Request object. 319 | */ 320 | addHeader(header: Header): Request { 321 | if (header instanceof CustomHeader) { 322 | this.headers.push(header); 323 | } else { 324 | this.headers.push(new CustomHeader(header)); 325 | } 326 | return this; 327 | } 328 | 329 | /** 330 | * Set the pager setting for the request. 331 | * 332 | * @param pager Pager object with pagination information. 333 | * @return Updated Request object. 334 | */ 335 | paginate(pager: IPager): Request { 336 | if (pager instanceof Pager) { 337 | this.pager = pager; 338 | } else { 339 | this.pager = new Pager(pager.page, pager.pageSize || 20); 340 | } 341 | this._usePager = true; 342 | return this; 343 | } 344 | 345 | /** 346 | * Generate the request interchange information. Note: This method is abstracted and 347 | * must be implemented in derived request generators. 348 | * 349 | * @param Rule used to create the interchange. If not provided, implementations 350 | * should select the rule to use. 351 | * @return Interchange data. 352 | */ 353 | abstract generate(rule?: GenerateRule): RequestInfo; 354 | } 355 | -------------------------------------------------------------------------------- /src/response.spec.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { Response, ResponseOptions, MessageType } from "./response"; 24 | 25 | /** 26 | * Fake to help with testing abstract class. 27 | */ 28 | class FakeResponse extends Response { 29 | /** 30 | * Construct a FakeResponse object. 31 | * @param {any} response 32 | * @param {ResponseOptions} options 33 | */ 34 | constructor(response: any, options?: ResponseOptions) { 35 | super(response, options); 36 | this.meta = { 37 | isPaged: true, 38 | isFiltered: true, 39 | record: 1, 40 | page: 1, 41 | pageSize: 10, 42 | totalRecords: 100, 43 | totalPages: 10, 44 | recordsBeforeFilter: 200, 45 | batch: false, 46 | properties: {}, 47 | }; 48 | } 49 | } 50 | 51 | /** 52 | * Extend the FakeResponse to also have messages 53 | */ 54 | class FakeResponseWithMessages extends FakeResponse { 55 | constructor(response: any, options?: ResponseOptions) { 56 | super(response, options); 57 | this.messages = [ 58 | { 59 | type: MessageType.Error, 60 | message: "Fake Error", 61 | }, 62 | { 63 | type: MessageType.Warning, 64 | message: "Fake Warning", 65 | }, 66 | { 67 | type: MessageType.Information, 68 | message: "Fake Information", 69 | }, 70 | ]; 71 | } 72 | } 73 | 74 | describe("Response", () => { 75 | describe("constructor", () => { 76 | it("should not keep a copy of the raw response by default", () => { 77 | const response = new FakeResponse({}); 78 | expect(response.raw).not.toBeDefined(); 79 | }); 80 | it("should keep a copy of the raw response when requested", () => { 81 | const resp = {}; 82 | const response = new FakeResponse(resp, { 83 | keepUnprocessedResponse: true, 84 | }); 85 | expect(response.raw).toEqual(resp); 86 | }); 87 | it("should not keep a copy of the raw response when configured", () => { 88 | const resp = {}; 89 | const response = new FakeResponse(resp, { 90 | keepUnprocessedResponse: false, 91 | }); 92 | expect(response.raw).not.toBeDefined(); 93 | }); 94 | }); 95 | describe("error, warning, messsage properties", () => { 96 | describe("without any messages", () => { 97 | let response: FakeResponse; 98 | beforeEach(() => { 99 | response = new FakeResponse({}); 100 | }); 101 | 102 | it("should return no errors", () => { 103 | expect(response.hasErrors).toBe(false); 104 | expect(response.errors).toEqual([]); 105 | }); 106 | 107 | it("should return no warnings", () => { 108 | expect(response.hasWarnings).toBe(false); 109 | expect(response.warnings).toEqual([]); 110 | }); 111 | 112 | it("should return no info messages", () => { 113 | expect(response.hasInfoMessages).toBe(false); 114 | expect(response.infoMessages).toEqual([]); 115 | }); 116 | }); 117 | describe("with messages of each type", () => { 118 | let response: FakeResponseWithMessages; 119 | beforeEach(() => { 120 | response = new FakeResponseWithMessages({}); 121 | }); 122 | 123 | it("should return an error when there is an error", () => { 124 | expect(response.hasErrors).toBe(true); 125 | expect(response.errors).toEqual([ 126 | { type: MessageType.Error, message: "Fake Error" }, 127 | ]); 128 | }); 129 | 130 | it("should return a warning when there is an warning", () => { 131 | expect(response.hasWarnings).toBe(true); 132 | expect(response.warnings).toEqual([ 133 | { type: MessageType.Warning, message: "Fake Warning" }, 134 | ]); 135 | }); 136 | 137 | it("should return a info message when there is an info message", () => { 138 | expect(response.hasInfoMessages).toBe(true); 139 | expect(response.infoMessages).toEqual([ 140 | { 141 | type: MessageType.Information, 142 | message: "Fake Information", 143 | }, 144 | ]); 145 | }); 146 | }); 147 | }); 148 | 149 | describe("meta data methods", () => { 150 | describe("isPaged", () => { 151 | let response: FakeResponse; 152 | beforeEach(() => { 153 | response = new FakeResponse({}); 154 | }); 155 | 156 | it("should return state from metadata", () => { 157 | expect(response.isPaged).toBe(true); 158 | }); 159 | }); 160 | 161 | describe("isFiltered", () => { 162 | let response: FakeResponse; 163 | beforeEach(() => { 164 | response = new FakeResponse({}); 165 | }); 166 | 167 | it("should return state from metadata", () => { 168 | expect(response.isFiltered).toBe(true); 169 | }); 170 | }); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /src/response.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { IMetaData } from "./metadata"; 24 | 25 | import isUndefined from "lodash/isUndefined"; 26 | import isNull from "lodash/isNull"; 27 | 28 | /** 29 | * Options for how to handle response parsing. 30 | */ 31 | export interface ResponseOptions { 32 | keepUnprocessedResponse: boolean; 33 | } 34 | 35 | /** 36 | * Types of message that can be in a response. 37 | */ 38 | export enum MessageType { 39 | /** 40 | * Message is an error. 41 | */ 42 | Error, 43 | 44 | /** 45 | * Message is a warning. 46 | */ 47 | Warning, 48 | 49 | /** 50 | * Message is informational. 51 | */ 52 | Information, 53 | 54 | /** 55 | * The message type is unknown. 56 | */ 57 | Unknown, 58 | } 59 | 60 | /** 61 | * Abstract structure for a message 62 | */ 63 | export interface IMessage { 64 | /** 65 | * Type of the message 66 | */ 67 | type: MessageType; 68 | 69 | /** 70 | * Actual message 71 | */ 72 | message: string; 73 | 74 | /** 75 | * Any other data related to a message 76 | */ 77 | data?: any; 78 | } 79 | 80 | /** 81 | * Abstract structure of a response shared by all responses. 82 | */ 83 | export interface IResponse { 84 | /** 85 | * The unprocessed response from the server. 86 | */ 87 | raw: any; 88 | 89 | /** 90 | * The status code returned by the API. Usually 1 for success, 0 for failure. 91 | */ 92 | status: number; 93 | 94 | /** 95 | * List of messages related to the response. 96 | */ 97 | messages: IMessage[]; 98 | 99 | /** 100 | * Additional data returned about the request. Paging, filtering, and maybe other custom properties. 101 | */ 102 | meta: IMetaData; 103 | 104 | /** 105 | * Data returned from the server for the request. This is the primary data returned. 106 | */ 107 | data: any; 108 | 109 | /** 110 | * Options about how to handle the response processing. 111 | */ 112 | options: ResponseOptions; 113 | } 114 | 115 | export const DefaultMetaData: IMetaData = { 116 | isPaged: false, 117 | isFiltered: false, 118 | record: 0, 119 | page: 0, 120 | pageSize: 0, 121 | totalRecords: 0, 122 | totalPages: 0, 123 | recordsBeforeFilter: 0, 124 | batch: false, 125 | properties: {}, 126 | }; 127 | 128 | /** 129 | * Deep cloning of a object to avoid reference overwritting. 130 | * 131 | * @param data Metadata object to be cloned. 132 | * @returns Cloned Metadata object. 133 | */ 134 | function clone(data: IMetaData): IMetaData { 135 | return JSON.parse(JSON.stringify(data)) as IMetaData; 136 | } 137 | 138 | /** 139 | * Base class for all response. Must be sub-classed by a real implementation. 140 | */ 141 | export abstract class Response implements IResponse { 142 | /** 143 | * The unprocessed response from the server. 144 | */ 145 | raw: any; 146 | 147 | /** 148 | * The status code returned by the API. Usually 1 for success, 0 for failure. 149 | */ 150 | status = 0; 151 | 152 | /** 153 | * List of messages related to the response. 154 | */ 155 | messages: IMessage[] = []; 156 | 157 | /** 158 | * Additional data returned about the request. Paging, filtering, and maybe other custom properties. 159 | */ 160 | meta: IMetaData = clone(DefaultMetaData); 161 | 162 | /** 163 | * Data returned from the server for the request. This is the primary data returned. 164 | */ 165 | data: any; 166 | 167 | /** 168 | * Options about how to handle the response processing. 169 | */ 170 | options: ResponseOptions = { 171 | keepUnprocessedResponse: false, 172 | }; 173 | 174 | /** 175 | * Build a new response object from the response. Note, this class should not be called 176 | * directly. 177 | * @param response Complete data passed from the server. Probably it's been parsed using JSON.parse(). 178 | * @param options for how to handle the processing of the response data. 179 | */ 180 | constructor(response: any, options?: ResponseOptions) { 181 | if (isUndefined(response) || isNull(response)) { 182 | throw new Error("The response was unexpectedly undefined or null"); 183 | } 184 | 185 | if (options) { 186 | this.options = options; 187 | } 188 | 189 | if (this.options.keepUnprocessedResponse) { 190 | this.raw = JSON.parse(JSON.stringify(response)); // deep clone 191 | } 192 | } 193 | 194 | /** 195 | * Checks if the API was successful. 196 | * 197 | * @return true if successful, false if failure. 198 | */ 199 | get success(): boolean { 200 | return this.status > 0; 201 | } 202 | 203 | /** 204 | * Checks if the api failed. 205 | * 206 | * @return true if the API reports failure, false otherwise. 207 | */ 208 | get failed(): boolean { 209 | return this.status === 0; 210 | } 211 | 212 | /** 213 | * Get the list of messages based on the requested type. 214 | * 215 | * @param type Type of the message to look up. 216 | * @return List of messages that match the filter. 217 | */ 218 | private _getMessages(type: MessageType): IMessage[] { 219 | return this.messages.filter((message) => message.type === type); 220 | } 221 | 222 | /** 223 | * Get the list of error messages. 224 | * 225 | * @return List of errors. 226 | */ 227 | get errors(): IMessage[] { 228 | return this._getMessages(MessageType.Error); 229 | } 230 | 231 | /** 232 | * Get the list of warning messages. 233 | * 234 | * @return List of warnings. 235 | */ 236 | get warnings(): IMessage[] { 237 | return this._getMessages(MessageType.Warning); 238 | } 239 | 240 | /** 241 | * Get the list of informational messages. 242 | * 243 | * @return List of informational messages. 244 | */ 245 | get infoMessages(): IMessage[] { 246 | return this._getMessages(MessageType.Information); 247 | } 248 | 249 | /** 250 | * Checks if there are any messages of a given type. 251 | * @param type Type of the message to check for. 252 | * @return true if there are messages of the requested type. false otherwise. 253 | */ 254 | private _hasMessages(type: MessageType): boolean { 255 | return ( 256 | this.messages.filter((message) => message.type === type).length > 0 257 | ); 258 | } 259 | 260 | /** 261 | * Checks if there are any error messages in the response. 262 | * 263 | * @return true if there are error messages, false otherwise. 264 | */ 265 | get hasErrors(): boolean { 266 | return this._hasMessages(MessageType.Error); 267 | } 268 | 269 | /** 270 | * Checks if there are any warnings in the response. 271 | * 272 | * @return true if there are warnings, false otherwise. 273 | */ 274 | get hasWarnings(): boolean { 275 | return this._hasMessages(MessageType.Warning); 276 | } 277 | 278 | /** 279 | * Checks if there are any informational messages in the response. 280 | * 281 | * @return true if there are informational messages, false otherwise. 282 | */ 283 | get hasInfoMessages(): boolean { 284 | return this._hasMessages(MessageType.Information); 285 | } 286 | 287 | /** 288 | * Check if the response was paginated by the backend. 289 | * 290 | * @return true if the backend returned a page of the total records. 291 | */ 292 | get isPaged(): boolean { 293 | return this.meta.isPaged; 294 | } 295 | 296 | /** 297 | * Check if the response was filtered by the backend. 298 | * 299 | * @return true if the backend filtered the records. 300 | */ 301 | get isFiltered(): boolean { 302 | return this.meta.isFiltered; 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /src/uapi/index.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | export * from "./request"; 24 | export * from "./response"; 25 | -------------------------------------------------------------------------------- /src/uapi/request.spec.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { UapiRequest } from "./request"; 24 | 25 | import { FilterOperator } from "../utils/filter"; 26 | 27 | import { SortDirection, SortType } from "../utils/sort"; 28 | 29 | import { 30 | Headers, 31 | CpanelApiTokenHeader, 32 | WhmApiTokenHeader, 33 | WhmApiTokenMismatchError, 34 | } from "../utils/headers"; 35 | 36 | describe("UapiRequest", () => { 37 | describe("when not fully initialized", () => { 38 | it("should not generate without a namespace", () => { 39 | const request = new UapiRequest(); 40 | expect(request).toBeDefined(); 41 | expect(() => { 42 | request.generate(); 43 | }).toThrowError(); 44 | }); 45 | }); 46 | 47 | describe("when relying on default rules", () => { 48 | it("should generate a POST with a wwwurlencoded body by default", () => { 49 | const request = new UapiRequest({ 50 | namespace: "test", 51 | method: "get_tests", 52 | }); 53 | expect(request).toBeDefined(); 54 | expect(request.generate()).toEqual({ 55 | headers: new Headers([ 56 | { 57 | name: "Content-Type", 58 | value: "application/x-www-form-urlencoded", 59 | }, 60 | ]), 61 | url: "/execute/test/get_tests", 62 | body: "", 63 | }); 64 | }); 65 | 66 | it("should generate include paging params if set", () => { 67 | const request = new UapiRequest({ 68 | namespace: "test", 69 | method: "get_tests", 70 | pager: { 71 | page: 3, 72 | pageSize: 7, 73 | }, 74 | }); 75 | expect(request).toBeDefined(); 76 | expect(request.generate()).toEqual({ 77 | headers: new Headers([ 78 | { 79 | name: "Content-Type", 80 | value: "application/x-www-form-urlencoded", 81 | }, 82 | ]), 83 | url: "/execute/test/get_tests", 84 | body: `api.paginate=1&api.paginate_start=15&api.paginate_size=7`, 85 | }); 86 | }); 87 | 88 | it("should generate filter params if set", () => { 89 | const request = new UapiRequest({ 90 | namespace: "test", 91 | method: "get_tests", 92 | filters: [ 93 | { 94 | column: "id", 95 | operator: FilterOperator.GreaterThan, 96 | value: 100, 97 | }, 98 | ], 99 | }); 100 | expect(request).toBeDefined(); 101 | expect(request.generate()).toEqual({ 102 | headers: new Headers([ 103 | { 104 | name: "Content-Type", 105 | value: "application/x-www-form-urlencoded", 106 | }, 107 | ]), 108 | url: "/execute/test/get_tests", 109 | body: `api.filter_column_0=id&api.filter_type_0=gt&api.filter_term_0=100`, 110 | }); 111 | }); 112 | 113 | it("should generate multiple filter params if set", () => { 114 | const request = new UapiRequest({ 115 | namespace: "test", 116 | method: "get_tests", 117 | filters: [ 118 | { 119 | column: "id", 120 | operator: FilterOperator.GreaterThan, 121 | value: 100, 122 | }, 123 | { 124 | column: "name", 125 | operator: FilterOperator.Contains, 126 | value: "unit test", 127 | }, 128 | ], 129 | }); 130 | expect(request).toBeDefined(); 131 | expect(request.generate()).toEqual({ 132 | headers: new Headers([ 133 | { 134 | name: "Content-Type", 135 | value: "application/x-www-form-urlencoded", 136 | }, 137 | ]), 138 | url: "/execute/test/get_tests", 139 | body: `api.filter_column_0=id&api.filter_type_0=gt&api.filter_term_0=100&api.filter_column_1=name&api.filter_type_1=contains&api.filter_term_1=unit%20test`, 140 | }); 141 | }); 142 | 143 | it("should generate sort parameters if set", () => { 144 | const request = new UapiRequest({ 145 | namespace: "test", 146 | method: "get_tests", 147 | sorts: [ 148 | { 149 | column: "title", 150 | direction: SortDirection.Descending, 151 | type: SortType.Lexicographic, 152 | }, 153 | ], 154 | }); 155 | expect(request).toBeDefined(); 156 | expect(request.generate()).toEqual({ 157 | headers: new Headers([ 158 | { 159 | name: "Content-Type", 160 | value: "application/x-www-form-urlencoded", 161 | }, 162 | ]), 163 | url: "/execute/test/get_tests", 164 | body: `api.sort=1&api.sort_column_0=title&api.sort_reverse_0=1&api.sort_method_0=lexicographic`, 165 | }); 166 | }); 167 | 168 | it("should generate the arguments", () => { 169 | const request = new UapiRequest({ 170 | namespace: "test", 171 | method: "get_tests_by_label", 172 | arguments: [ 173 | { 174 | name: "label", 175 | value: "unit", 176 | }, 177 | ], 178 | }); 179 | expect(request).toBeDefined(); 180 | expect(request.generate()).toEqual({ 181 | headers: new Headers([ 182 | { 183 | name: "Content-Type", 184 | value: "application/x-www-form-urlencoded", 185 | }, 186 | ]), 187 | url: "/execute/test/get_tests_by_label", 188 | body: "label=unit", 189 | }); 190 | }); 191 | }); 192 | 193 | describe("when JSON encoding is requested", () => { 194 | it("should generate a POST with a JSON body by default", () => { 195 | const request = new UapiRequest({ 196 | namespace: "test", 197 | method: "get_tests_by_label", 198 | arguments: [ 199 | { 200 | name: "label", 201 | value: "unit", 202 | }, 203 | ], 204 | config: { 205 | json: true, 206 | }, 207 | }); 208 | expect(request).toBeDefined(); 209 | expect(request.generate()).toEqual({ 210 | headers: new Headers([ 211 | { 212 | name: "Content-Type", 213 | value: "application/json", 214 | }, 215 | ]), 216 | url: "/execute/test/get_tests_by_label", 217 | body: '{"label":"unit"}', 218 | }); 219 | }); 220 | }); 221 | 222 | describe("when calling with cPanel API token with token and user", () => { 223 | it("should generate a correct interchange", () => { 224 | const request = new UapiRequest({ 225 | namespace: "test", 226 | method: "simple_call", 227 | headers: [new CpanelApiTokenHeader("fake", "user")], 228 | }); 229 | expect(request).toBeDefined(); 230 | expect(request.generate()).toEqual({ 231 | headers: new Headers([ 232 | { 233 | name: "Content-Type", 234 | value: "application/x-www-form-urlencoded", 235 | }, 236 | { 237 | name: "Authorization", 238 | value: "cpanel user:fake", 239 | }, 240 | ]), 241 | url: "/execute/test/simple_call", 242 | body: "", 243 | }); 244 | }); 245 | }); 246 | 247 | describe("when calling with cPanel API token with combined user/token", () => { 248 | it("should generate a correct interchange", () => { 249 | const request = new UapiRequest({ 250 | namespace: "test", 251 | method: "simple_call", 252 | headers: [new CpanelApiTokenHeader("user:fake")], 253 | }); 254 | expect(request).toBeDefined(); 255 | expect(request.generate()).toEqual({ 256 | headers: new Headers([ 257 | { 258 | name: "Content-Type", 259 | value: "application/x-www-form-urlencoded", 260 | }, 261 | { 262 | name: "Authorization", 263 | value: "cpanel user:fake", 264 | }, 265 | ]), 266 | url: "/execute/test/simple_call", 267 | body: "", 268 | }); 269 | }); 270 | }); 271 | 272 | describe("when calling with WHM API token", () => { 273 | it("should throw an error", () => { 274 | expect(() => { 275 | new UapiRequest({ 276 | namespace: "test", 277 | method: "simple_call", 278 | headers: [new WhmApiTokenHeader("fake", "user")], 279 | }); 280 | }).toThrowError(WhmApiTokenMismatchError); 281 | }); 282 | }); 283 | }); 284 | -------------------------------------------------------------------------------- /src/uapi/request.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import snakeCase from "lodash/snakeCase"; 24 | 25 | import * as Perl from "../utils/perl"; 26 | 27 | import { SortDirection, SortType } from "../utils/sort"; 28 | 29 | import { FilterOperator } from "../utils/filter"; 30 | 31 | import { IArgument } from "../utils/argument"; 32 | 33 | import { IPager } from "../utils/pager"; 34 | 35 | import { GenerateRule, Request, IRequest } from "../request"; 36 | 37 | import { HttpVerb } from "../http/verb"; 38 | 39 | import { RequestInfo } from "../interchange"; 40 | 41 | import { 42 | WhmApiTokenHeader, 43 | WhmApiTokenMismatchError, 44 | Headers, 45 | Header, 46 | } from "../utils/headers"; 47 | 48 | import { 49 | ArgumentSerializationRule, 50 | argumentSerializationRules, 51 | } from "../argument-serializer-rules"; 52 | 53 | import { 54 | IArgumentEncoder, 55 | JsonArgumentEncoder, 56 | WwwFormUrlArgumentEncoder, 57 | } from "../utils/encoders"; 58 | 59 | export class UapiRequest extends Request { 60 | /** 61 | * Add a custom HTTP header to the request 62 | * 63 | * @param name Name of a column 64 | * @return Updated Request object. 65 | */ 66 | addHeader(header: Header): Request { 67 | if (header instanceof WhmApiTokenHeader) { 68 | throw new WhmApiTokenMismatchError( 69 | "A WhmApiTokenHeader cannot be used on a CpanelApiRequest", 70 | ); 71 | } 72 | super.addHeader(header); 73 | return this; 74 | } 75 | 76 | /** 77 | * Build a fragment of the parameter list based on the list of name/value pairs. 78 | * 79 | * @param params Parameters to serialize. 80 | * @param encoder Encoder to use to serialize the each parameter. 81 | * @return Fragment with the serialized parameters 82 | */ 83 | private _build(params: IArgument[], encoder: IArgumentEncoder): string { 84 | let fragment = ""; 85 | params.forEach((arg, index, array) => { 86 | const isLast: boolean = index === array.length - 1; 87 | fragment += encoder.encode(arg.name, arg.value, isLast); 88 | }); 89 | return encoder.separatorStart + fragment + encoder.separatorEnd; 90 | } 91 | 92 | /** 93 | * Generates the arguments for the request. 94 | * 95 | * @param params List of parameters to adjust based on the sort rules in the Request. 96 | */ 97 | private _generateArguments(params: IArgument[]): void { 98 | this.arguments.forEach((argument) => params.push(argument)); 99 | } 100 | 101 | /** 102 | * Generates the sort parameters for the request. 103 | * 104 | * @param params List of parameters to adjust based on the sort rules in the Request. 105 | */ 106 | private _generateSorts(params: IArgument[]): void { 107 | this.sorts.forEach((sort, index) => { 108 | if (index === 0) { 109 | params.push({ 110 | name: "api.sort", 111 | value: Perl.fromBoolean(true), 112 | }); 113 | } 114 | params.push({ 115 | name: "api.sort_column_" + index, 116 | value: sort.column, 117 | }); 118 | params.push({ 119 | name: "api.sort_reverse_" + index, 120 | value: Perl.fromBoolean( 121 | sort.direction !== SortDirection.Ascending, 122 | ), 123 | }); 124 | params.push({ 125 | name: "api.sort_method_" + index, 126 | value: snakeCase(SortType[sort.type]), 127 | }); 128 | }); 129 | } 130 | 131 | /** 132 | * Look up the correct name for the filter operator 133 | * 134 | * @param operator Type of filter operator to use to filter the items 135 | * @returns The string counter part for the filter operator. 136 | * @throws Will throw an error if an unrecognized FilterOperator is provided. 137 | */ 138 | private _lookupFilterOperator(operator: FilterOperator): string { 139 | switch (operator) { 140 | case FilterOperator.GreaterThanUnlimited: 141 | return "gt_handle_unlimited"; 142 | case FilterOperator.GreaterThan: 143 | return "gt"; 144 | case FilterOperator.LessThanUnlimited: 145 | return "lt_handle_unlimited"; 146 | case FilterOperator.LessThan: 147 | return "lt"; 148 | case FilterOperator.NotEqual: 149 | return "ne"; 150 | case FilterOperator.Equal: 151 | return "eq"; 152 | case FilterOperator.Defined: 153 | return "defined"; 154 | case FilterOperator.Undefined: 155 | return "undefined"; 156 | case FilterOperator.Matches: 157 | return "matches"; 158 | case FilterOperator.Ends: 159 | return "ends"; 160 | case FilterOperator.Begins: 161 | return "begins"; 162 | case FilterOperator.Contains: 163 | return "contains"; 164 | default: 165 | // eslint-disable-next-line no-case-declarations -- just used for readability 166 | const key = FilterOperator[operator]; 167 | throw new Error(`Unrecognized FilterOperator ${key} for UAPI`); 168 | } 169 | } 170 | 171 | /** 172 | * Generate the filter parameters if any. 173 | * 174 | * @param params List of parameters to adjust based on the filter rules provided. 175 | */ 176 | private _generateFilters(params: IArgument[]): void { 177 | this.filters.forEach((filter, index) => { 178 | params.push({ 179 | name: "api.filter_column_" + index, 180 | value: filter.column, 181 | }); 182 | params.push({ 183 | name: "api.filter_type_" + index, 184 | value: this._lookupFilterOperator(filter.operator), 185 | }); 186 | params.push({ 187 | name: "api.filter_term_" + index, 188 | value: filter.value, 189 | }); 190 | }); 191 | } 192 | 193 | /** 194 | * In UAPI, we request the starting record, not the starting page. This translates 195 | * the page and page size into the correct starting record. 196 | */ 197 | private _traslatePageToStart(pager: IPager) { 198 | return (pager.page - 1) * pager.pageSize + 1; 199 | } 200 | 201 | /** 202 | * Generate the pager request parameters, if any. 203 | * 204 | * @param params List of parameters to adjust based on the pagination rules. 205 | */ 206 | private _generatePagination(params: IArgument[]): void { 207 | if (!this.usePager) { 208 | return; 209 | } 210 | 211 | const allPages = this.pager.all(); 212 | params.push({ 213 | name: "api.paginate", 214 | value: Perl.fromBoolean(true), 215 | }); 216 | params.push({ 217 | name: "api.paginate_start", 218 | value: allPages ? -1 : this._traslatePageToStart(this.pager), 219 | }); 220 | if (!allPages) { 221 | params.push({ 222 | name: "api.paginate_size", 223 | value: this.pager.pageSize, 224 | }); 225 | } 226 | } 227 | 228 | /** 229 | * Generate any additional parameters from the configuration data. 230 | * 231 | * @param params List of parameters to adjust based on the configuration. 232 | */ 233 | private _generateConfiguration(params: IArgument[]): void { 234 | if (this.config && this.config["analytics"]) { 235 | params.push({ 236 | name: "api.analytics", 237 | value: Perl.fromBoolean(this.config.analytics), 238 | }); 239 | } 240 | } 241 | 242 | /** 243 | * Create a new uapi request. 244 | * 245 | * @param init Optional request objects used to initialize this object. 246 | */ 247 | constructor(init?: IRequest) { 248 | super(init); 249 | } 250 | 251 | /** 252 | * Generate the interchange object that has the pre-encoded 253 | * request using UAPI formatting. 254 | * 255 | * @param rule Optional parameter to specify a specific Rule we want the Request to be generated for. 256 | * @return Request information ready to be used by a remoting layer 257 | */ 258 | generate(rule?: GenerateRule): RequestInfo { 259 | // Needed for pure JS clients, since they don't get the compiler checks 260 | if (!this.namespace) { 261 | throw new Error( 262 | "You must define a namespace for the UAPI call before you generate a request", 263 | ); 264 | } 265 | if (!this.method) { 266 | throw new Error( 267 | "You must define a method for the UAPI call before you generate a request", 268 | ); 269 | } 270 | 271 | if (!rule) { 272 | rule = { 273 | verb: HttpVerb.POST, 274 | encoder: this.config.json 275 | ? new JsonArgumentEncoder() 276 | : new WwwFormUrlArgumentEncoder(), 277 | }; 278 | } 279 | 280 | if (!rule.encoder) { 281 | rule.encoder = this.config.json 282 | ? new JsonArgumentEncoder() 283 | : new WwwFormUrlArgumentEncoder(); 284 | } 285 | 286 | const argumentRule: ArgumentSerializationRule = 287 | argumentSerializationRules.getRule(rule.verb); 288 | 289 | const info: RequestInfo = { 290 | headers: new Headers([ 291 | { 292 | name: "Content-Type", 293 | value: rule.encoder.contentType, 294 | }, 295 | ]), 296 | url: ["", "execute", this.namespace, this.method] 297 | .map(encodeURIComponent) 298 | .join("/"), 299 | body: "", 300 | }; 301 | 302 | const params: IArgument[] = []; 303 | this._generateArguments(params); 304 | this._generateSorts(params); 305 | this._generateFilters(params); 306 | this._generatePagination(params); 307 | this._generateConfiguration(params); 308 | 309 | const encoded = this._build(params, rule.encoder); 310 | 311 | if (argumentRule.dataInBody) { 312 | info["body"] = encoded; 313 | } else { 314 | if (rule.verb === HttpVerb.GET) { 315 | info["url"] += `?${encoded}`; 316 | } else { 317 | info["url"] += encoded; 318 | } 319 | } 320 | 321 | this.headers.forEach((header) => { 322 | info.headers.push({ 323 | name: header.name, 324 | value: header.value, 325 | }); 326 | }); 327 | 328 | return info; 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/uapi/response.spec.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { MessageType } from "../response"; 24 | 25 | import { UapiResponse, UapiMetaData } from "./response"; 26 | 27 | describe("UapiResponse", () => { 28 | describe("constructor", () => { 29 | it("should fail when response does not have a data property", () => { 30 | expect(() => new UapiResponse({ status: "0" })).toThrowError(); 31 | }); 32 | it("should report failure for any 0 status", () => { 33 | expect(new UapiResponse({ data: {}, status: "0" }).success).toBe( 34 | false, 35 | ); 36 | expect(new UapiResponse({ data: {}, status: 0 }).success).toBe( 37 | false, 38 | ); 39 | expect(new UapiResponse({ data: {}, status: "" }).success).toBe( 40 | false, 41 | ); 42 | }); 43 | it("should report success for any 1 status", () => { 44 | expect(new UapiResponse({ data: {}, status: "1" }).success).toBe( 45 | true, 46 | ); 47 | expect(new UapiResponse({ data: {}, status: 1 }).success).toBe( 48 | true, 49 | ); 50 | }); 51 | it("should store the data property if present in the response", () => { 52 | const data = {}; 53 | expect(new UapiResponse({ data: data, status: 1 }).data).toBe(data); 54 | }); 55 | it("should store the metadata if present in the response", () => { 56 | const data = {}; 57 | const metadata = {}; 58 | const response = new UapiResponse({ 59 | data: data, 60 | metadata: metadata, 61 | status: 1, 62 | }); 63 | expect(response.data).toBe(data); 64 | expect(response.meta).toBeDefined(); 65 | }); 66 | it("should parse any errors provided", () => { 67 | let response = new UapiResponse({ 68 | data: {}, 69 | status: 1, 70 | errors: [], 71 | }); 72 | expect(response.errors.length).toBe(0); 73 | 74 | response = new UapiResponse({ 75 | data: {}, 76 | status: 0, 77 | errors: ["yo"], 78 | }); 79 | expect(response.errors.length).toBe(1); 80 | expect(response.errors[0]).toEqual({ 81 | message: "yo", 82 | type: MessageType.Error, 83 | }); 84 | }); 85 | it("should parse any messages provided", () => { 86 | let response = new UapiResponse({ 87 | data: {}, 88 | status: 1, 89 | messages: [], 90 | }); 91 | expect(response.errors.length).toBe(0); 92 | 93 | response = new UapiResponse({ 94 | data: {}, 95 | status: 0, 96 | messages: ["yo"], 97 | }); 98 | expect(response.messages.length).toBe(1); 99 | expect(response.messages[0]).toEqual({ 100 | message: "yo", 101 | type: MessageType.Information, 102 | }); 103 | }); 104 | }); 105 | }); 106 | 107 | describe("UapiMetaData", () => { 108 | describe("constructor with no elements", () => { 109 | it("should use the default values", () => { 110 | const raw = {}; 111 | const meta = new UapiMetaData(raw); 112 | expect(meta.isPaged).toBe(false); 113 | expect(meta.record).toBe(0); 114 | expect(meta.page).toBe(0); 115 | expect(meta.pageSize).toBe(0); 116 | expect(meta.totalPages).toBe(0); 117 | expect(meta.totalRecords).toBe(0); 118 | expect(meta.isFiltered).toBe(false); 119 | expect(meta.recordsBeforeFilter).toBe(0); 120 | expect(Object.keys(meta.properties).length).toBe(0); 121 | }); 122 | }); 123 | 124 | describe("constructor with pagination", () => { 125 | it("should parse the pagination data", () => { 126 | const raw = { 127 | paginate: { 128 | start_result: 20, 129 | current_page: 3, 130 | results_per_page: 10, 131 | total_pages: 5, 132 | total_results: 49, 133 | }, 134 | }; 135 | const meta = new UapiMetaData(raw); 136 | expect(meta.isPaged).toBe(true); 137 | expect(meta.record).toBe(20); 138 | expect(meta.page).toBe(3); 139 | expect(meta.pageSize).toBe(10); 140 | expect(meta.totalPages).toBe(5); 141 | expect(meta.totalRecords).toBe(49); 142 | expect(meta.isFiltered).toBe(false); 143 | expect(meta.recordsBeforeFilter).toBe(0); 144 | expect(Object.keys(meta.properties).length).toBe(0); 145 | }); 146 | }); 147 | 148 | describe("constructor with filter", () => { 149 | it("should parse the filter data", () => { 150 | const raw = { 151 | filter: { 152 | records_before_filter: 75, 153 | }, 154 | }; 155 | const meta = new UapiMetaData(raw); 156 | expect(meta.isPaged).toBe(false); 157 | expect(meta.record).toBe(0); 158 | expect(meta.page).toBe(0); 159 | expect(meta.pageSize).toBe(0); 160 | expect(meta.totalPages).toBe(0); 161 | expect(meta.totalRecords).toBe(0); 162 | expect(meta.isFiltered).toBe(true); 163 | expect(meta.recordsBeforeFilter).toBe(75); 164 | expect(Object.keys(meta.properties).length).toBe(0); 165 | }); 166 | }); 167 | 168 | describe("constructor with custom metadata properties", () => { 169 | it("should put the custom properties in the properties collection, but not paginate or filter", () => { 170 | const raw = { 171 | filter: { 172 | records_before_filter: 75, 173 | }, 174 | paginate: { 175 | start_result: 20, 176 | current_page: 3, 177 | results_per_page: 10, 178 | total_pages: 5, 179 | total_results: 49, 180 | }, 181 | "custom.bool": true, 182 | "custom.number": 55, 183 | "custom.string": "hello", 184 | "custom.array": [1, 2, 3], 185 | "custom.object": { a: 10, b: 11 }, 186 | }; 187 | const meta = new UapiMetaData(raw); 188 | expect(Object.keys(meta.properties).length).toBe(5); 189 | expect(meta.properties["custom.bool"]).toBe(true); 190 | expect(meta.properties["custom.number"]).toBe(55); 191 | expect(meta.properties["custom.string"]).toBe("hello"); 192 | expect(meta.properties["custom.array"]).toBeDefined(); 193 | expect(meta.properties["custom.object"]).toBeDefined(); 194 | expect(meta.properties["filter"]).not.toBeDefined(); 195 | expect(meta.properties["paginate"]).not.toBeDefined(); 196 | }); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /src/uapi/response.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { IMetaData } from "../metadata"; 24 | 25 | import { MessageType, Response, ResponseOptions } from "../response"; 26 | 27 | /** 28 | * This class will extract the available metadata from the UAPI format into a standard format for JavaScript developers. 29 | */ 30 | export class UapiMetaData implements IMetaData { 31 | /** 32 | * Indicates if the data is paged. 33 | */ 34 | isPaged = false; 35 | 36 | /** 37 | * The record number of the first record of a page. 38 | */ 39 | record = 0; 40 | 41 | /** 42 | * The current page. 43 | */ 44 | page = 0; 45 | 46 | /** 47 | * The page size of the returned set. 48 | */ 49 | pageSize = 0; 50 | 51 | /** 52 | * The total number of records available on the backend. 53 | */ 54 | totalRecords = 0; 55 | 56 | /** 57 | * The total number of pages of records on the backend. 58 | */ 59 | totalPages = 0; 60 | 61 | /** 62 | * Indicates if the data set if filtered. 63 | */ 64 | isFiltered = false; 65 | 66 | /** 67 | * Number of records available before the filter was processed. 68 | */ 69 | recordsBeforeFilter = 0; 70 | 71 | /** 72 | * Indicates the response was the result of a batch API. 73 | */ 74 | batch = false; 75 | 76 | /** 77 | * A collection of the other less common or custom UAPI metadata properties. 78 | */ 79 | properties: { [index: string]: any } = {}; 80 | 81 | /** 82 | * Build a new MetaData object from the metadata response from the server. 83 | * 84 | * @param meta UAPI metadata object. 85 | */ 86 | constructor(meta: any) { 87 | // Handle pagination 88 | if (meta.paginate) { 89 | this.isPaged = true; 90 | this.record = parseInt(meta.paginate.start_result, 10) || 0; 91 | this.page = parseInt(meta.paginate.current_page, 10) || 0; 92 | this.pageSize = parseInt(meta.paginate.results_per_page, 10) || 0; 93 | this.totalPages = parseInt(meta.paginate.total_pages, 10) || 0; 94 | this.totalRecords = parseInt(meta.paginate.total_results, 10) || 0; 95 | } 96 | 97 | // Handle filtering 98 | if (meta.filter) { 99 | this.isFiltered = true; 100 | this.recordsBeforeFilter = 101 | parseInt(meta.filter.records_before_filter, 10) || 0; 102 | } 103 | 104 | // Get any other custom metadata properties off the object 105 | const builtinSet = new Set(["paginate", "filter"]); 106 | Object.keys(meta) 107 | .filter((key: string) => !builtinSet.has(key)) 108 | .forEach((key: string) => { 109 | this.properties[key] = meta[key]; 110 | }); 111 | } 112 | } 113 | 114 | /** 115 | * Parser that will convert a UAPI wire-formated object into a standard response object for JavaScript developers. 116 | */ 117 | export class UapiResponse extends Response { 118 | /** 119 | * Parse out the status from the response. 120 | * 121 | * @param response Raw response object from the backend. Already passed through JSON.parse(). 122 | * @return Number indicating success or failure. > 1 success, 0 failure. 123 | */ 124 | private _parseStatus(response: any): void { 125 | this.status = 0; // Assume it failed. 126 | if (typeof response.status === "undefined") { 127 | throw new Error( 128 | "The response should have a numeric status property indicating the API succeeded (>0) or failed (=0)", 129 | ); 130 | } 131 | this.status = parseInt(response.status, 10); 132 | } 133 | 134 | /** 135 | * Parse out the messages from the response. 136 | * 137 | * @param response The response object sent by the API method. 138 | */ 139 | private _parseMessages(response: any): void { 140 | if ("errors" in response) { 141 | const errors = response.errors; 142 | if (errors && errors.length) { 143 | errors.forEach((error: string) => { 144 | this.messages.push({ 145 | type: MessageType.Error, 146 | message: error, 147 | }); 148 | }); 149 | } 150 | } 151 | 152 | if ("messages" in response) { 153 | const messages = response.messages; 154 | if (messages) { 155 | messages.forEach((message: string) => { 156 | this.messages.push({ 157 | type: MessageType.Information, 158 | message: message, 159 | }); 160 | }); 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * Parse out the status, data and metadata from a UAPI response into the abstract Response and IMetaData structures. 167 | * 168 | * @param response Raw response from the server. It's just been JSON.parse() at this point. 169 | * @param Options on how to handle parsing of the response. 170 | */ 171 | constructor(response: any, options?: ResponseOptions) { 172 | super(response, options); 173 | 174 | this._parseStatus(response); 175 | this._parseMessages(response); 176 | 177 | if ( 178 | !response || 179 | !Object.prototype.hasOwnProperty.call(response, "data") 180 | ) { 181 | throw new Error( 182 | "Expected response to contain a data property, but it is missing", 183 | ); 184 | } 185 | 186 | // TODO: Add parsing by specific types to take care of renames and type coercion. 187 | this.data = response.data; 188 | 189 | if (response.metadata) { 190 | this.meta = new UapiMetaData(response.metadata); 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/utils/argument.spec.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { Argument, PerlBooleanArgument } from "./argument"; 24 | 25 | import * as perl from "./perl"; 26 | 27 | describe("Argument Class", () => { 28 | describe("constructor", () => { 29 | it("should throw an error when an empty string is passed in name field for name/value arguments", () => { 30 | expect(function () { 31 | new Argument("", ""); 32 | }).toThrowError(); 33 | }); 34 | 35 | it("should build a new name/value when passed only a name and value", () => { 36 | const arg = new Argument("name", "kermit"); 37 | expect(arg).toBeDefined(); 38 | expect(arg.name).toBe("name"); 39 | expect(arg.value).toBe("kermit"); 40 | }); 41 | }); 42 | }); 43 | 44 | describe("PerlBooleanArgument Class", () => { 45 | describe("constructor", () => { 46 | it("should throw an error when an empty string is passed in name field for name/value arguments", () => { 47 | expect(function () { 48 | new PerlBooleanArgument("", true); 49 | }).toThrowError(); 50 | }); 51 | 52 | it("should build a new name/value when passed a name and true value", () => { 53 | const arg = new PerlBooleanArgument("name", true); 54 | expect(arg).toBeDefined(); 55 | expect(arg.name).toBe("name"); 56 | expect(arg.value).toBe(perl.fromBoolean(true)); 57 | }); 58 | 59 | it("should build a new name/value when passed a name and false value", () => { 60 | const arg = new PerlBooleanArgument("name", false); 61 | expect(arg).toBeDefined(); 62 | expect(arg.name).toBe("name"); 63 | expect(arg.value).toBe(perl.fromBoolean(false)); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/utils/argument.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { fromBoolean } from "./perl"; 24 | 25 | /** 26 | * Abstract interface that value-based arguments must implement 27 | */ 28 | export interface IArgument { 29 | /** 30 | * Name of the argument 31 | */ 32 | name: string; 33 | 34 | /** 35 | * Value of the argument. 36 | */ 37 | value: any; 38 | } 39 | 40 | /** 41 | * An name/value pair argument 42 | */ 43 | export class Argument implements IArgument { 44 | /** 45 | * Name of the argument. 46 | */ 47 | name: string; 48 | 49 | /** 50 | * Value of the argument 51 | */ 52 | value: any; 53 | 54 | /** 55 | * Build a new Argument. 56 | * 57 | * @param name Name of the argument 58 | * @param value Value of the argument. 59 | */ 60 | constructor(name: string, value: any) { 61 | if (!name) { 62 | throw new Error( 63 | "You must provide a name when creating a name/value argument", 64 | ); 65 | } 66 | this.name = name; 67 | this.value = value; 68 | } 69 | } 70 | 71 | /** 72 | * Specialty argument class that will auto-coerce a Boolean to a perl Boolean 73 | */ 74 | export class PerlBooleanArgument extends Argument { 75 | /** 76 | * Build a new Argument 77 | * @param name Name of the argument 78 | * @param value Value of the argument. Will be serialized to use perl's Boolean rules. 79 | */ 80 | constructor(name: string, value: boolean) { 81 | super(name, fromBoolean(value)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/utils/encoders.spec.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { 24 | IArgumentEncoder, 25 | UrlArgumentEncoder, 26 | WwwFormUrlArgumentEncoder, 27 | JsonArgumentEncoder, 28 | } from "./encoders"; 29 | 30 | describe("UrlArgumentEncoder", () => { 31 | let encoder: IArgumentEncoder; 32 | beforeEach(() => { 33 | encoder = new UrlArgumentEncoder(); 34 | }); 35 | 36 | it("should be created by new()", () => { 37 | expect(encoder).toBeDefined(); 38 | expect(encoder.contentType).toBe(""); 39 | expect(encoder.separatorStart).toBe("?"); 40 | expect(encoder.separatorEnd).toBe(""); 41 | expect(encoder.recordSeparator).toBe("&"); 42 | }); 43 | 44 | it("should throw an error when passed an empty name", () => { 45 | expect(() => encoder.encode("", "", false)).toThrowError(); 46 | }); 47 | 48 | it("should encode data correctly", () => { 49 | expect(encoder.encode("name", "", false)).toBe("name=&"); 50 | expect(encoder.encode("name", "", true)).toBe("name="); 51 | expect(encoder.encode("name", "value", false)).toBe("name=value&"); 52 | expect(encoder.encode("name", "value", true)).toBe("name=value"); 53 | expect(encoder.encode("name", "&", false)).toBe("name=%26&"); 54 | expect(encoder.encode("name", "&", true)).toBe("name=%26"); 55 | }); 56 | }); 57 | 58 | describe("WwwFormUrlArgumentEncoder", () => { 59 | let encoder: IArgumentEncoder; 60 | beforeEach(() => { 61 | encoder = new WwwFormUrlArgumentEncoder(); 62 | }); 63 | 64 | it("should be created by new()", () => { 65 | expect(encoder).toBeDefined(); 66 | expect(encoder.contentType).toBe("application/x-www-form-urlencoded"); 67 | expect(encoder.separatorStart).toBe(""); 68 | expect(encoder.separatorEnd).toBe(""); 69 | expect(encoder.recordSeparator).toBe("&"); 70 | }); 71 | 72 | it("should throw an error when passed an empty name", () => { 73 | expect(() => encoder.encode("", "", false)).toThrowError(); 74 | }); 75 | 76 | it("should encode data correctly", () => { 77 | expect(encoder.encode("name", "", false)).toBe("name=&"); 78 | expect(encoder.encode("name", "", true)).toBe("name="); 79 | expect(encoder.encode("name", "value", false)).toBe("name=value&"); 80 | expect(encoder.encode("name", "value", true)).toBe("name=value"); 81 | expect(encoder.encode("name", "&", false)).toBe("name=%26&"); 82 | expect(encoder.encode("name", "&", true)).toBe("name=%26"); 83 | }); 84 | }); 85 | 86 | describe("JsonArgumentEncoder", () => { 87 | let encoder: IArgumentEncoder; 88 | beforeEach(() => { 89 | encoder = new JsonArgumentEncoder(); 90 | }); 91 | 92 | it("should be created by new()", () => { 93 | expect(encoder).toBeDefined(); 94 | expect(encoder.contentType).toBe("application/json"); 95 | expect(encoder.separatorStart).toBe("{"); 96 | expect(encoder.separatorEnd).toBe("}"); 97 | expect(encoder.recordSeparator).toBe(","); 98 | }); 99 | 100 | it("should encode data correctly", () => { 101 | expect(encoder.encode("data", "value", true)).toBe('"data":"value"'); 102 | expect(encoder.encode("data", "value", false)).toBe('"data":"value",'); 103 | expect(encoder.encode("data", "&", true)).toBe('"data":"&"'); 104 | expect(encoder.encode("data", "&", false)).toBe('"data":"&",'); 105 | expect(encoder.encode("data", 1, true)).toBe('"data":1'); 106 | expect(encoder.encode("data", 1, false)).toBe('"data":1,'); 107 | expect(encoder.encode("data", true, true)).toBe('"data":true'); 108 | expect(encoder.encode("data", true, false)).toBe('"data":true,'); 109 | expect(encoder.encode("data", [1, 2, 3], true)).toBe('"data":[1,2,3]'); 110 | expect(encoder.encode("data", [1, 2, 3], false)).toBe( 111 | '"data":[1,2,3],', 112 | ); 113 | expect(encoder.encode("data", { a: 1, b: 2 }, true)).toBe( 114 | '"data":{"a":1,"b":2}', 115 | ); 116 | expect(encoder.encode("data", { a: 1, b: 2 }, false)).toBe( 117 | '"data":{"a":1,"b":2},', 118 | ); 119 | }); 120 | 121 | it("should throw an error when passed a value that can not be serialized", () => { 122 | expect(() => encoder.encode("", "", false)).toThrowError(); 123 | expect(() => 124 | encoder.encode("", { a: 1, b: () => true }, false), 125 | ).toThrowError(); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /src/utils/encoders.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import * as json from "./json/serializable"; 24 | 25 | /** 26 | * Abstract argument encoder 27 | */ 28 | export interface IArgumentEncoder { 29 | /** 30 | * Name of the content type if any. May be an empty string. 31 | */ 32 | contentType: string; 33 | 34 | /** 35 | * Separator to inject at the start of the arguments. May be empty. 36 | */ 37 | separatorStart: string; 38 | 39 | /** 40 | * Separator to inject at the end of the arguments. May be empty. 41 | */ 42 | separatorEnd: string; 43 | 44 | /** 45 | * Record separator 46 | */ 47 | recordSeparator: string; 48 | 49 | /** 50 | * Encode a given value into the requested format. 51 | * 52 | * @param name Name of the field, may be empty string. 53 | * @param value Value to serialize 54 | * @param last True if this is the last argument being serialized. 55 | * @return Encoded version of the argument. 56 | */ 57 | encode(name: string, value: any, last: boolean): string; 58 | } 59 | 60 | /** 61 | * Encode parameters using urlencode. 62 | */ 63 | export class UrlArgumentEncoder implements IArgumentEncoder { 64 | contentType = ""; 65 | separatorStart = "?"; 66 | separatorEnd = ""; 67 | recordSeparator = "&"; 68 | 69 | /** 70 | * Encode a given value into query-string compatible format. 71 | * 72 | * @param name Name of the field, may be empty string. 73 | * @param value Value to serialize 74 | * @param last True if this is the last argument being serialized. 75 | * @return Encoded version of the argument. 76 | */ 77 | encode(name: string, value: any, last: boolean): string { 78 | if (!name) { 79 | throw new Error("Name must have a non-empty value"); 80 | } 81 | return ( 82 | `${name}=${encodeURIComponent(value.toString())}` + 83 | (!last ? this.recordSeparator : "") 84 | ); 85 | } 86 | } 87 | 88 | /** 89 | * Encode parameters using application/x-www-form-urlencoded 90 | */ 91 | export class WwwFormUrlArgumentEncoder implements IArgumentEncoder { 92 | contentType = "application/x-www-form-urlencoded"; 93 | separatorStart = ""; 94 | separatorEnd = ""; 95 | recordSeparator = "&"; 96 | 97 | /** 98 | * Encode a given value into the application/x-www-form-urlencoded. 99 | * 100 | * @param name Name of the field, may be empty string. 101 | * @param value Value to serialize 102 | * @param last True if this is the last argument being serialized. 103 | * @return Encoded version of the argument. 104 | */ 105 | encode(name: string, value: any, last: boolean): string { 106 | if (!name) { 107 | throw new Error("Name must have a non-empty value"); 108 | } 109 | 110 | return ( 111 | `${name}=${encodeURIComponent(value.toString())}` + 112 | (!last ? this.recordSeparator : "") 113 | ); 114 | } 115 | } 116 | 117 | /** 118 | * Encode the parameter into JSON 119 | */ 120 | export class JsonArgumentEncoder implements IArgumentEncoder { 121 | contentType = "application/json"; 122 | separatorStart = "{"; 123 | separatorEnd = "}"; 124 | recordSeparator = ","; 125 | 126 | /** 127 | * Encode a given value into the JSON application/json body. 128 | * 129 | * @param name Name of the field. 130 | * @param value Value to serialize 131 | * @param last True if this is the last argument being serialized. 132 | * @return {string} Encoded version of the argument. 133 | */ 134 | encode(name: string, value: any, last: boolean): string { 135 | if (!name) { 136 | throw new Error("Name must have a non-empty value"); 137 | } 138 | if (!json.isSerializable(value)) { 139 | throw new Error( 140 | "The passed in value can not be serialized to JSON", 141 | ); 142 | } 143 | return ( 144 | JSON.stringify(name) + 145 | ":" + 146 | JSON.stringify(value) + 147 | (!last ? this.recordSeparator : "") 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/utils/filter.spec.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { FilterOperator, Filter } from "./filter"; 24 | 25 | describe("Filter Class", () => { 26 | describe("constructor", () => { 27 | it("should throw an error when an empty string is passed in column", () => { 28 | expect(function () { 29 | new Filter("", FilterOperator.Contains, ""); 30 | }).toThrowError(); 31 | }); 32 | it("should build a new Filter when passed all the parameters", () => { 33 | const filter = new Filter( 34 | "name", 35 | FilterOperator.Contains, 36 | "kermit", 37 | ); 38 | expect(filter).toBeDefined(); 39 | expect(filter.column).toBe("name"); 40 | expect(filter.operator).toBe(FilterOperator.Contains); 41 | expect(filter.value).toBe("kermit"); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/utils/filter.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | /** 24 | * The filter operator defines the rule used to compare data in a column with the passed-in value. It 25 | * behaves something like: 26 | * 27 | * const value = 1; 28 | * data.map(item => item[column]) 29 | * .filter(itemValue => operator(itemValue, value)); 30 | * 31 | * where item is the data from the column 32 | */ 33 | export enum FilterOperator { 34 | /** 35 | * String contains value 36 | */ 37 | Contains, 38 | 39 | /** 40 | * String begins with value 41 | */ 42 | Begins, 43 | 44 | /** 45 | * String ends with value 46 | */ 47 | Ends, 48 | 49 | /** 50 | * String matches pattern in value 51 | */ 52 | Matches, 53 | 54 | /** 55 | * Column value equals value 56 | */ 57 | Equal, 58 | 59 | /** 60 | * Column value not equal value 61 | */ 62 | NotEqual, 63 | 64 | /** 65 | * Column value is less than value 66 | */ 67 | LessThan, 68 | 69 | /** 70 | * Column value is less than value using unlimited rules. 71 | */ 72 | LessThanUnlimited, 73 | 74 | /** 75 | * Column value is greater than value. 76 | */ 77 | GreaterThan, 78 | 79 | /** 80 | * Column value is greater than value using unlimited rules. 81 | */ 82 | GreaterThanUnlimited, 83 | 84 | /** 85 | * Column value is defined. Value is ignored in this case. 86 | */ 87 | Defined, 88 | 89 | /** 90 | * Column value is undefined. Value is ignored in this case. 91 | */ 92 | Undefined, 93 | } 94 | 95 | /** 96 | * Interface for filter data. 97 | */ 98 | export interface IFilter { 99 | /** 100 | * Column name to look at in a record. 101 | */ 102 | column: string; 103 | 104 | /** 105 | * Comparison operator to apply 106 | */ 107 | operator: FilterOperator; 108 | 109 | /** 110 | * Value to compare the column data to. The kinds of values here vary depending on the FilterOperator 111 | */ 112 | value: any; 113 | } 114 | 115 | /** 116 | * Defines a filter request for a Api call. 117 | */ 118 | export class Filter implements IFilter { 119 | /** 120 | * Column name to look at in a record. 121 | */ 122 | column: string; 123 | 124 | /** 125 | * Comparison operator to apply 126 | */ 127 | operator: FilterOperator; 128 | 129 | /** 130 | * Value to compare the column data to. The kinds of values here vary depending on the FilterOperator 131 | */ 132 | value: any; 133 | 134 | /** 135 | * Construct a new Filter object. 136 | * 137 | * @param column Column name requests. Must be non-empty and exist on the related backend collection. 138 | * @param operator Comparison operator to use when applying the filter. 139 | * @param value Value to compare the columns value too. 140 | */ 141 | constructor(column: string, operator: FilterOperator, value: any) { 142 | if (!column) { 143 | throw new Error("You must define a non-empty column name."); 144 | } 145 | 146 | this.column = column; 147 | this.operator = operator; 148 | this.value = value; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/utils/headers.spec.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { 24 | Headers, 25 | CustomHeader, 26 | CpanelApiTokenHeader, 27 | WhmApiTokenHeader, 28 | CpanelApiTokenInvalidError, 29 | WhmApiTokenInvalidError, 30 | } from "../utils/headers"; 31 | 32 | describe("Headers collection: ", () => { 33 | describe("when you call constructor without an argument", () => { 34 | it("should return an empty headers collection", () => { 35 | const headers = new Headers(); 36 | expect(headers.toArray()).toEqual([]); 37 | }); 38 | }); 39 | 40 | describe("when you call constructor an object array", () => { 41 | it("should return an empty headers collection", () => { 42 | const headers = new Headers([ 43 | { 44 | name: "dog", 45 | value: "barks", 46 | }, 47 | ]); 48 | expect(headers.toArray()).toEqual([ 49 | { 50 | name: "dog", 51 | value: "barks", 52 | }, 53 | ]); 54 | expect(headers.toObject()).toEqual({ 55 | dog: "barks", 56 | }); 57 | }); 58 | }); 59 | 60 | describe("when you call constructor an specific CustomerHeader classes array", () => { 61 | it("should return an empty headers collection", () => { 62 | const headers = new Headers([ 63 | new CustomHeader({ 64 | name: "cat", 65 | value: "meows", 66 | }), 67 | ]); 68 | const array = headers.toArray(); 69 | 70 | expect(array).toEqual([ 71 | { 72 | name: "cat", 73 | value: "meows", 74 | }, 75 | ]); 76 | expect(headers.toObject()).toEqual({ 77 | cat: "meows", 78 | }); 79 | }); 80 | }); 81 | }); 82 | 83 | describe("CpanelApiTokenHeader: ", () => { 84 | describe("when you attempt to pass an empty token", () => { 85 | it("should throw an error", () => { 86 | expect(() => new CpanelApiTokenHeader("")).toThrowError( 87 | CpanelApiTokenInvalidError, 88 | ); 89 | }); 90 | }); 91 | 92 | describe("when you attempt to pass an token and no user", () => { 93 | it("should throw an error", () => { 94 | expect(() => new CpanelApiTokenHeader("fake")).toThrowError( 95 | CpanelApiTokenInvalidError, 96 | ); 97 | }); 98 | }); 99 | 100 | describe("when you attempt to pass an token and an empty user", () => { 101 | it("should throw an error", () => { 102 | expect(() => new CpanelApiTokenHeader("fake", "")).toThrowError( 103 | CpanelApiTokenInvalidError, 104 | ); 105 | }); 106 | }); 107 | 108 | describe("when you attempt to pass an token and an empty user prefix", () => { 109 | it("should throw an error", () => { 110 | expect(() => new CpanelApiTokenHeader(":fake")).toThrowError( 111 | CpanelApiTokenInvalidError, 112 | ); 113 | }); 114 | }); 115 | 116 | describe("when you attempt to pass an empty token with a user prefix", () => { 117 | it("should throw an error", () => { 118 | expect(() => new CpanelApiTokenHeader("user:")).toThrowError( 119 | CpanelApiTokenInvalidError, 120 | ); 121 | }); 122 | }); 123 | 124 | describe("when you attempt to pass an token and user", () => { 125 | it("should successfully ", () => { 126 | const { name, value } = new CpanelApiTokenHeader("fake", "user"); 127 | expect({ name, value }).toEqual({ 128 | name: "Authorization", 129 | value: "cpanel user:fake", 130 | }); 131 | }); 132 | }); 133 | 134 | describe("when you attempt to pass an token with a user prefix", () => { 135 | it("should successfully ", () => { 136 | const { name, value } = new CpanelApiTokenHeader("user:fake"); 137 | expect({ name, value }).toEqual({ 138 | name: "Authorization", 139 | value: "cpanel user:fake", 140 | }); 141 | }); 142 | }); 143 | }); 144 | 145 | describe("WhmApiTokenHeader: ", () => { 146 | describe("when you attempt to pass an empty token", () => { 147 | it("should throw an error", () => { 148 | expect(() => new WhmApiTokenHeader("")).toThrowError( 149 | WhmApiTokenInvalidError, 150 | ); 151 | }); 152 | }); 153 | 154 | describe("when you attempt to pass an token and no user", () => { 155 | it("should throw an error", () => { 156 | expect(() => new WhmApiTokenHeader("fake")).toThrowError(); 157 | }); 158 | }); 159 | 160 | describe("when you attempt to pass an token and an empty user", () => { 161 | it("should throw an error", () => { 162 | expect(() => new WhmApiTokenHeader("fake", "")).toThrowError( 163 | WhmApiTokenInvalidError, 164 | ); 165 | }); 166 | }); 167 | 168 | describe("when you attempt to pass an token and an empty user prefix", () => { 169 | it("should throw an error", () => { 170 | expect(() => new WhmApiTokenHeader(":fake")).toThrowError( 171 | WhmApiTokenInvalidError, 172 | ); 173 | }); 174 | }); 175 | 176 | describe("when you attempt to pass an empty token with a user prefix", () => { 177 | it("should throw an error", () => { 178 | expect(() => new WhmApiTokenHeader("user:")).toThrowError( 179 | WhmApiTokenInvalidError, 180 | ); 181 | }); 182 | }); 183 | 184 | describe("when you attempt to pass an token and user", () => { 185 | it("should successfully ", () => { 186 | const { name, value } = new WhmApiTokenHeader("fake", "user"); 187 | expect({ name, value }).toEqual({ 188 | name: "Authorization", 189 | value: "whm user:fake", 190 | }); 191 | }); 192 | }); 193 | 194 | describe("when you attempt to pass an token with a user prefix", () => { 195 | it("should successfully ", () => { 196 | const { name, value } = new WhmApiTokenHeader("user:fake"); 197 | expect({ name, value }).toEqual({ 198 | name: "Authorization", 199 | value: "whm user:fake", 200 | }); 201 | }); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /src/utils/headers.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | /** 24 | * HTTP Header Abstraction 25 | */ 26 | export interface Header { 27 | /** 28 | * Name of the header 29 | */ 30 | name: string; 31 | 32 | /** 33 | * Value of the header 34 | */ 35 | value: string; 36 | } 37 | 38 | export type HeaderHash = { [index: string]: string }; 39 | 40 | /** 41 | * HTTP Headers Collection Abstraction 42 | * 43 | * The abstraction is an adapter to allow easy transformation of the headers array 44 | * into various formats for external HTTP libraries. 45 | */ 46 | export class Headers { 47 | private headers: Header[]; 48 | 49 | /** 50 | * Create the adapter. 51 | * 52 | * @param headers - List of headers. 53 | */ 54 | constructor(headers: Header[] = []) { 55 | this.headers = headers; 56 | } 57 | 58 | /** 59 | * Push a header into the collection. 60 | * 61 | * @param header - A header to add to the collection 62 | */ 63 | push(header: Header) { 64 | this.headers.push(header); 65 | } 66 | 67 | /** 68 | * Iterator for the headers collection. 69 | * 70 | * @param fn - Transform for the forEach 71 | * @param thisArg - Optional reference to `this` to apply to the transform function. 72 | */ 73 | forEach(fn: (v: Header, i: number, a: Header[]) => void, thisArg?: any) { 74 | this.headers.forEach(fn, thisArg); 75 | } 76 | 77 | /** 78 | * Retrieve the headers as an array of Headers 79 | */ 80 | toArray(): Header[] { 81 | const copy: Header[] = []; 82 | this.headers.forEach((h) => 83 | copy.push({ name: h.name, value: h.value }), 84 | ); 85 | return copy; 86 | } 87 | 88 | /** 89 | * Retrieve the headers as an object 90 | */ 91 | toObject(): HeaderHash { 92 | return this.headers.reduce((o: HeaderHash, header: Header) => { 93 | o[header.name] = header.value; 94 | return o; 95 | }, {}); 96 | } 97 | } 98 | 99 | export class CustomHeader implements Header { 100 | constructor(private _header: Header) {} 101 | 102 | public get name() { 103 | return this._header.name; 104 | } 105 | 106 | public get value() { 107 | return this._header.value; 108 | } 109 | } 110 | 111 | export class CpanelApiTokenInvalidError extends Error { 112 | public readonly name = "CpanelApiTokenInvalidError"; 113 | constructor(m: string) { 114 | super(m); 115 | 116 | // Set the prototype explicitly. This fixes unit tests. 117 | Object.setPrototypeOf(this, CpanelApiTokenInvalidError.prototype); 118 | } 119 | } 120 | 121 | export class CpanelApiTokenMismatchError extends Error { 122 | public readonly name = "CpanelApiTokenMismatchError"; 123 | constructor(m: string) { 124 | super(m); 125 | 126 | // Set the prototype explicitly. This fixes unit tests. 127 | Object.setPrototypeOf(this, CpanelApiTokenMismatchError.prototype); 128 | } 129 | } 130 | 131 | export class CpanelApiTokenHeader extends CustomHeader { 132 | constructor(token: string, user?: string) { 133 | if (!token) { 134 | throw new CpanelApiTokenInvalidError( 135 | "You must pass a valid token to the constructor.", 136 | ); 137 | } 138 | if (!user && !/^.+[:]/.test(token)) { 139 | throw new CpanelApiTokenInvalidError( 140 | "You must pass a cPanel username associated with the cPanel API token.", 141 | ); 142 | } 143 | if (!user && !/[:].+$/.test(token)) { 144 | throw new CpanelApiTokenInvalidError( 145 | "You must pass a valid cPanel API token.", 146 | ); 147 | } 148 | super({ 149 | name: "Authorization", 150 | value: `cpanel ${user ? user + ":" : ""}${token}`, 151 | }); 152 | } 153 | } 154 | 155 | export class WhmApiTokenInvalidError extends Error { 156 | public readonly name = "WhmApiTokenInvalidError"; 157 | constructor(m: string) { 158 | super(m); 159 | 160 | // Set the prototype explicitly. This fixes unit tests. 161 | Object.setPrototypeOf(this, WhmApiTokenInvalidError.prototype); 162 | } 163 | } 164 | 165 | export class WhmApiTokenMismatchError extends Error { 166 | public readonly name = "WhmApiTokenMismatchError"; 167 | constructor(m: string) { 168 | super(m); 169 | 170 | // Set the prototype explicitly. This fixes unit tests. 171 | Object.setPrototypeOf(this, WhmApiTokenMismatchError.prototype); 172 | } 173 | } 174 | 175 | export class WhmApiTokenHeader extends CustomHeader { 176 | constructor(token: string, user?: string) { 177 | if (!token) { 178 | throw new WhmApiTokenInvalidError( 179 | "You must pass a valid token to the constructor.", 180 | ); 181 | } 182 | if (!user && !/^.+:/.test(token)) { 183 | throw new WhmApiTokenInvalidError( 184 | "You must pass a WHM username associated with the WHM API token.", 185 | ); 186 | } 187 | if (!user && !/:.+$/.test(token)) { 188 | throw new WhmApiTokenInvalidError( 189 | "You must pass a valid WHM API token.", 190 | ); 191 | } 192 | super({ 193 | name: "Authorization", 194 | value: `whm ${user ? user + ":" : ""}${token}`, 195 | }); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | export * from "./argument"; 24 | export * from "./filter"; 25 | export * from "./pager"; 26 | export * from "./sort"; 27 | export * from "./encoders"; 28 | export * from "./perl"; 29 | export * from "./json/serializable"; 30 | export * from "./path"; 31 | export * from "./location-service"; 32 | export * from "./encoders"; 33 | export * from "./headers"; 34 | -------------------------------------------------------------------------------- /src/utils/json/serializable.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import isUndefined from "lodash/isUndefined"; 24 | import isNull from "lodash/isNull"; 25 | import isBoolean from "lodash/isBoolean"; 26 | import isNumber from "lodash/isNumber"; 27 | import isString from "lodash/isString"; 28 | import isArray from "lodash/isArray"; 29 | import isPlainObject from "lodash/isPlainObject"; 30 | 31 | /** 32 | * Verify if the value can be serialized to JSON 33 | * 34 | * @param value Value to check. 35 | * @source https://stackoverflow.com/questions/30579940/reliable-way-to-check-if-objects-is-serializable-in-javascript#answer-30712764 36 | */ 37 | function isSerializable(value: any) { 38 | if ( 39 | isUndefined(value) || 40 | isNull(value) || 41 | isBoolean(value) || 42 | isNumber(value) || 43 | isString(value) 44 | ) { 45 | return true; 46 | } 47 | 48 | if (!isPlainObject(value) && !isArray(value)) { 49 | return false; 50 | } 51 | 52 | for (const key in value) { 53 | if (!isSerializable(value[key])) { 54 | return false; 55 | } 56 | } 57 | 58 | return true; 59 | } 60 | 61 | export { isSerializable }; 62 | -------------------------------------------------------------------------------- /src/utils/location-service.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | /** 24 | * Provides a mockable layer between the tools below and window.location. 25 | */ 26 | export class LocationService { 27 | /** 28 | * The pathname part of the URL. 29 | */ 30 | public get pathname(): string { 31 | return window.location.pathname; 32 | } 33 | 34 | /** 35 | * The port part of the URL. 36 | */ 37 | public get port(): string { 38 | return window.location.port; 39 | } 40 | 41 | /** 42 | * The hostname part of the URL. 43 | */ 44 | public get hostname(): string { 45 | return window.location.hostname; 46 | } 47 | 48 | /** 49 | * The protocol part of the URL. 50 | */ 51 | public get protocol(): string { 52 | return window.location.protocol; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/pager.spec.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { ALL, DEFAULT_PAGE_SIZE, Pager } from "./pager"; 24 | 25 | describe("Pager Class", () => { 26 | describe("constructor", () => { 27 | it("should throw error when passed a negative page number", () => { 28 | expect(function () { 29 | new Pager(-2); 30 | }).toThrowError(); 31 | }); 32 | 33 | it("should throw error when passed a zero page number", () => { 34 | expect(function () { 35 | new Pager(0); 36 | }).toThrowError(); 37 | }); 38 | 39 | it("should not throw error when passed a positive non-zero page number", () => { 40 | expect(function () { 41 | new Pager(1); 42 | }).not.toThrowError(); 43 | expect(function () { 44 | new Pager(1000); 45 | }).not.toThrowError(); 46 | }); 47 | 48 | it("should not throw error when passed the ALL cardinal", () => { 49 | expect(function () { 50 | new Pager(1, ALL); 51 | }).not.toThrowError(); 52 | }); 53 | 54 | it("should default the page and pageSize ", () => { 55 | const pager = new Pager(); 56 | expect(pager.page).toBe(1); 57 | expect(pager.pageSize).toBe(DEFAULT_PAGE_SIZE); 58 | }); 59 | 60 | it("should set the page when passed and default the pageSize to whatever is set in the DEFAULT_PAGE_SIZE", () => { 61 | const pager = new Pager(10); 62 | expect(pager.page).toBe(10); 63 | expect(pager.pageSize).toBe(DEFAULT_PAGE_SIZE); 64 | }); 65 | 66 | it("should set the page and pageSize to the desired values", () => { 67 | const pager = new Pager(10, 10); 68 | expect(pager.page).toBe(10); 69 | expect(pager.pageSize).toBe(10); 70 | }); 71 | 72 | it("should set the page and pageSize to the desired values: all", () => { 73 | const pager = new Pager(1, ALL); 74 | expect(pager.page).toBe(1); 75 | expect(pager.pageSize).toBe(ALL); 76 | expect(pager.all()).toBe(true); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/utils/pager.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | export const DEFAULT_PAGE_SIZE = 20; 24 | 25 | /** 26 | * When passed in the pageSize, will request all available records in a single page. Note: The backend process may not honor this request. 27 | */ 28 | export const ALL = Number.POSITIVE_INFINITY; 29 | 30 | /** 31 | * Interface for a pagination request. 32 | */ 33 | export interface IPager { 34 | /** 35 | * One-based index of the pages of data. 36 | */ 37 | page: number; 38 | 39 | /** 40 | * Number of elements a page of data is composed of. This is the requested page size, if there are less than this number of records in the set, only the remaining records are returned. 41 | */ 42 | pageSize: number; 43 | } 44 | 45 | /** 46 | * Defines a pagination request for an API. 47 | */ 48 | export class Pager implements IPager { 49 | /** 50 | * One-based index of the pages of data. 51 | */ 52 | page: number; 53 | 54 | /** 55 | * Number of elements a page of data is composed of. This is the requested page size, if there are less than this number of records in the set, only the remaining records are returned. 56 | */ 57 | pageSize: number; 58 | 59 | /** 60 | * Create a new pagination object. 61 | * 62 | * @param page Page to request. From 1 .. n where n is the set.length % pageSize. Defaults to 1. 63 | * @param pageSize Number of records to request in a page of data. Defaults to DEFAULT_PAGE_SIZE. 64 | * If the string 'all' is passed, then all the records are requested. Note: The backend 65 | * system may still impose page size limits in this case. 66 | */ 67 | constructor(page = 1, pageSize: number = DEFAULT_PAGE_SIZE) { 68 | if (page <= 0) { 69 | throw new Error( 70 | "The page must be 1 or greater. This is the logical page, not a programming index.", 71 | ); 72 | } 73 | 74 | if (pageSize <= 0) { 75 | throw new Error( 76 | "The pageSize must be set to 'ALL' or a number > 0", 77 | ); 78 | } 79 | 80 | this.page = page; 81 | this.pageSize = pageSize; 82 | } 83 | 84 | /** 85 | * Check if the pagesize is set to ALL. 86 | * 87 | * @return true if requesting all records, false otherwise. 88 | */ 89 | all(): boolean { 90 | return this.pageSize === ALL; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/utils/path.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { LocationService } from "./location-service"; 24 | 25 | /** 26 | * Check if the protocol is https. 27 | * @param protocol Protocol to test 28 | * @return true if its https: in any case, false otherwise. 29 | */ 30 | export function isHttps(protocol: string): boolean { 31 | return /^https:$/i.test(protocol); 32 | } 33 | 34 | /** 35 | * Check if the protocol is http. 36 | * @param protocol Protocol to test 37 | * @return true if its http: in any case, false otherwise. 38 | */ 39 | export function isHttp(protocol: string): boolean { 40 | return /^http:$/i.test(protocol); 41 | } 42 | 43 | /** 44 | * Strip any trailing slashes from a string. 45 | * 46 | * @method stripTrailingSlash 47 | * @param path The path string to process. 48 | * @return The path string without a trailing slash. 49 | */ 50 | export function stripTrailingSlash(path: string) { 51 | return path && path.replace(/\/?$/, ""); 52 | } 53 | 54 | /** 55 | * Add a trailing slash to a string if it doesn't have one. 56 | * 57 | * @method ensureTrailingSlash 58 | * @param path The path string to process. 59 | * @return The path string with a guaranteed trailing slash. 60 | */ 61 | export function ensureTrailingSlash(path: string) { 62 | return path && path.replace(/\/?$/, "/"); 63 | } 64 | 65 | type PortNameMap = { [index: string]: string }; 66 | 67 | // This will work in any context except a proxy URL to cPanel or Webmail 68 | // that accesses a URL outside /frontend (cPanel) or /webmail (Webmail), 69 | // but URLs like that are non-production by definition. 70 | const PortToApplicationMap: PortNameMap = { 71 | "80": "other", 72 | "443": "other", 73 | "2082": "cpanel", 74 | "2083": "cpanel", 75 | "2086": "whostmgr", 76 | "2087": "whostmgr", 77 | "2095": "webmail", 78 | "2096": "webmail", 79 | "9876": "unittest", 80 | "9877": "unittest", 81 | "9878": "unittest", 82 | "9879": "unittest", 83 | frontend: "cpanel", 84 | webmail: "webmail", 85 | }; 86 | 87 | /** 88 | * Helper class used to calculate paths within cPanel applications. 89 | */ 90 | export class ApplicationPath { 91 | private unprotectedPaths = ["/resetpass", "/invitation"]; 92 | 93 | /** 94 | * Name of the application 95 | */ 96 | applicationName: string; 97 | 98 | /** 99 | * Protocol used to access the page. 100 | */ 101 | protocol: string; 102 | 103 | /** 104 | * Port used to access the product. 105 | */ 106 | port: number; 107 | 108 | /** 109 | * Path part of the URL. 110 | */ 111 | path: string; 112 | 113 | /** 114 | * Domain used to access the page. 115 | */ 116 | domain: string; 117 | 118 | /** 119 | *Session token. 120 | */ 121 | securityToken: string; 122 | 123 | /** 124 | * The path to the application. 125 | */ 126 | applicationPath: string; 127 | 128 | /** 129 | * The name of the theme in the path. 130 | */ 131 | theme: string; 132 | 133 | /** 134 | * The theme path. 135 | */ 136 | themePath: string; 137 | 138 | /** 139 | * Just the protocol, domain, and port. 140 | */ 141 | rootUrl: string; 142 | 143 | /** 144 | * Create the PathHelper. This class is used to help generate paths 145 | * within an application. It has special knowledge about how paths are 146 | * constructed in the cPanel family of applications. 147 | * 148 | * @param location Abstraction for the window.location object to aid in unit testing this module. 149 | */ 150 | constructor(location: LocationService) { 151 | this.protocol = location.protocol; 152 | 153 | let port = location.port; 154 | if (!port) { 155 | // Since some browsers won't fill this in, we have to derive it from 156 | // the protocol if it's not provided in the window.location object. 157 | if (isHttps(this.protocol)) { 158 | port = "443"; 159 | } else if (isHttp(this.protocol)) { 160 | port = "80"; 161 | } 162 | } 163 | 164 | this.domain = location.hostname; 165 | this.port = parseInt(port, 10); 166 | this.path = location.pathname; 167 | 168 | const pathMatch = 169 | // eslint-disable-next-line no-useless-escape -- regex, not a string 170 | this.path.match(/((?:\/cpsess\d+)?)(?:\/([^\/]+))?/) || []; 171 | 172 | // For proxy subdomains, we look at the first subdomain to identify the application. 173 | if (/^whm\./.test(this.domain)) { 174 | this.applicationName = PortToApplicationMap["2087"]; 175 | } else if (/^cpanel\./.test(this.domain)) { 176 | this.applicationName = PortToApplicationMap["2083"]; 177 | } else if (/^webmail\./.test(this.domain)) { 178 | this.applicationName = PortToApplicationMap["2095"]; 179 | } else { 180 | this.applicationName = 181 | PortToApplicationMap[port.toString()] || 182 | PortToApplicationMap[pathMatch[2]] || 183 | "whostmgr"; 184 | } 185 | 186 | this.securityToken = pathMatch[1] || ""; 187 | this.applicationPath = this.securityToken 188 | ? this.path.replace(this.securityToken, "") 189 | : this.path; 190 | this.theme = ""; 191 | if (!this.isUnprotected && (this.isCpanel || this.isWebmail)) { 192 | const folders = this.path.split("/"); 193 | this.theme = folders[3]; 194 | } 195 | 196 | this.themePath = ""; 197 | let themePath = this.securityToken + "/"; 198 | if (this.isUnprotected) { 199 | themePath = "/"; 200 | } else if (this.isCpanel) { 201 | themePath += "frontend/" + this.theme + "/"; 202 | } else if (this.isWebmail) { 203 | themePath += "webmail/" + this.theme + "/"; 204 | } else if (this.isOther) { 205 | // For unrecognized applications, use the path passed in PAGE.THEME_PATH 206 | themePath = "/"; 207 | } 208 | this.themePath = themePath; 209 | this.rootUrl = this.protocol + "//" + this.domain + ":" + this.port; 210 | } 211 | 212 | /** 213 | * Return whether we are running inside some other framework or application 214 | * 215 | * @return true if this is an unrecognized application or framework; false otherwise 216 | */ 217 | get isOther(): boolean { 218 | return /other/i.test(this.applicationName); 219 | } 220 | 221 | /** 222 | * Return whether we are running inside an unprotected path 223 | * 224 | * @return true if this is unprotected; false otherwise 225 | */ 226 | get isUnprotected(): boolean { 227 | return ( 228 | !this.securityToken && 229 | this.unprotectedPaths.indexOf( 230 | stripTrailingSlash(this.applicationPath), 231 | ) !== -1 232 | ); 233 | } 234 | 235 | /** 236 | * Return whether we are running inside cPanel or something else (e.g., WHM) 237 | * 238 | * @return true if this is cPanel; false otherwise 239 | */ 240 | get isCpanel(): boolean { 241 | return /cpanel/i.test(this.applicationName); 242 | } 243 | 244 | /** 245 | * Return whether we are running inside WHM or something else (e.g., WHM) 246 | * 247 | * @return true if this is WHM; false otherwise 248 | */ 249 | get isWhm(): boolean { 250 | return /whostmgr/i.test(this.applicationName); 251 | } 252 | 253 | /** 254 | * Return whether we are running inside WHM or something else (e.g., WHM) 255 | * 256 | * @return true if this is Webmail; false otherwise 257 | */ 258 | get isWebmail(): boolean { 259 | return /webmail/i.test(this.applicationName); 260 | } 261 | 262 | /** 263 | * Get the domain relative path for the relative URL path. 264 | * 265 | * @param relative Relative path to the resource. 266 | * @return Domain relative URL path including theme, if applicable, for the application to the file. 267 | */ 268 | buildPath(relative: string) { 269 | return this.themePath + relative; 270 | } 271 | 272 | /** 273 | * Get the full url path for the relative URL path. 274 | * 275 | * @param relative Relative path to the resource. 276 | * @return Full URL path including theme, if applicable, for the application to the file. 277 | */ 278 | buildFullPath(relative: string) { 279 | return ( 280 | this.protocol + 281 | "//" + 282 | this.domain + 283 | ":" + 284 | this.port + 285 | this.buildPath(relative) 286 | ); 287 | } 288 | 289 | /** 290 | * Build a path relative to the security token 291 | * 292 | * @param relative Relative path to the resource. 293 | * @return Full path to the token relative resource. 294 | */ 295 | buildTokenPath(relative: string) { 296 | return ( 297 | this.protocol + 298 | "//" + 299 | this.domain + 300 | ":" + 301 | this.port + 302 | this.securityToken + 303 | relative 304 | ); 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/utils/perl.spec.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import * as Perl from "./perl"; 24 | 25 | describe("fromBoolean()", () => { 26 | it("should return a perl falsism when passed false value", () => { 27 | expect(Perl.fromBoolean(false)).toBe("0"); 28 | }); 29 | it("should return a perl truism when passed false value", () => { 30 | expect(Perl.fromBoolean(true)).toBe("1"); 31 | }); 32 | }); 33 | 34 | describe("toBoolean()", () => { 35 | it("should return false when passed false a Perl falsism", () => { 36 | expect(Perl.toBoolean("")).toBe(false); 37 | expect(Perl.toBoolean(0)).toBe(false); 38 | expect(Perl.toBoolean("0")).toBe(false); 39 | }); 40 | it("should return true when passed a Perl truism", () => { 41 | expect(Perl.toBoolean("1")).toBe(true); 42 | expect(Perl.toBoolean(1)).toBe(true); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/utils/perl.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | /** 24 | * Convert from a JavaScript boolean to a Perl boolean. 25 | */ 26 | export function fromBoolean(value: boolean) { 27 | return value ? "1" : "0"; 28 | } 29 | 30 | const perlFalse = new Set(["", "0", 0]); 31 | 32 | /** 33 | * Convert from a Perl boolean to a JavaScript boolean 34 | */ 35 | export function toBoolean(value: any) { 36 | if (perlFalse.has(value)) { 37 | return false; 38 | } 39 | return true; 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/sort.spec.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { Sort, SortDirection, SortType } from "./sort"; 24 | 25 | describe("Sort Class", () => { 26 | describe("constructor", () => { 27 | it("should throw error when passed an empty column name ", () => { 28 | expect(function () { 29 | new Sort(""); 30 | }).toThrowError(); 31 | }); 32 | 33 | it("should default the direction and type when not passed", () => { 34 | const sort = new Sort("column"); 35 | expect(sort.column).toBe("column"); 36 | expect(sort.direction).toBe(SortDirection.Ascending); 37 | expect(sort.type).toBe(SortType.Lexicographic); 38 | }); 39 | 40 | it("should default the type when not passed", () => { 41 | const sort = new Sort("column", SortDirection.Descending); 42 | expect(sort.column).toBe("column"); 43 | expect(sort.direction).toBe(SortDirection.Descending); 44 | expect(sort.type).toBe(SortType.Lexicographic); 45 | }); 46 | 47 | it("should set the values passed when passed explicitly", () => { 48 | const sort = new Sort( 49 | "column", 50 | SortDirection.Descending, 51 | SortType.Numeric, 52 | ); 53 | expect(sort.column).toBe("column"); 54 | expect(sort.direction).toBe(SortDirection.Descending); 55 | expect(sort.type).toBe(SortType.Numeric); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/utils/sort.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | /** 24 | * Sorting direction. The SortType and SortDirection combine to define the sorting for collections returned. 25 | */ 26 | export enum SortDirection { 27 | /** 28 | * Records are sorted from low value to high value based on the SortType 29 | */ 30 | Ascending, 31 | 32 | /** 33 | * Records are sorted from high value to low value based on the SortType 34 | */ 35 | Descending, 36 | } 37 | 38 | /** 39 | * Sorting type. Defines how values are compared. 40 | */ 41 | export enum SortType { 42 | /** 43 | * Uses character-by-character comparison. 44 | */ 45 | Lexicographic, 46 | 47 | /** 48 | * Special rule for handing IPv4 comparison. This takes into account the segments. 49 | */ 50 | Ipv4, 51 | 52 | /** 53 | * Assumes the values are numeric and compares them using number rules. 54 | */ 55 | Numeric, 56 | 57 | /** 58 | * Special rule for certain data where 0 is considered unlimited. 59 | */ 60 | NumericZeroAsMax, 61 | } 62 | 63 | /** 64 | * Sort interface 65 | */ 66 | export interface ISort { 67 | /** 68 | * Column name to sort on. 69 | */ 70 | column: string; 71 | 72 | /** 73 | * Direction to apply to sort: ascending or descending 74 | */ 75 | direction: SortDirection; 76 | 77 | /** 78 | * Sort type applied. See SortType for information on available sorting rules. 79 | */ 80 | type: SortType; 81 | } 82 | 83 | /** 84 | * Defines a sort rule. These can be combined into a list to define a complex sort for a list dataset. 85 | */ 86 | export class Sort implements ISort { 87 | /** 88 | * Column name to sort on. 89 | */ 90 | column: string; 91 | 92 | /** 93 | * Direction to apply to sort: ascending or descending 94 | */ 95 | direction: SortDirection; 96 | 97 | /** 98 | * Sort type applied. See SortType for information on available sorting rules. 99 | */ 100 | type: SortType; 101 | 102 | /** 103 | * Create a new instance of a Sort 104 | * 105 | * @param column Column to sort 106 | * @param direction Optional sort direction. Defaults to Ascending 107 | * @param type Optional sort type. Defaults to Lexicographic 108 | */ 109 | constructor( 110 | column: string, 111 | direction: SortDirection = SortDirection.Ascending, 112 | type: SortType = SortType.Lexicographic, 113 | ) { 114 | if (!column) { 115 | throw new Error( 116 | "You must provide a non-empty column name for a Sort rule.", 117 | ); 118 | } 119 | this.column = column; 120 | this.direction = direction; 121 | this.type = type; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/whmapi/index.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | export * from "./request"; 24 | export * from "./response"; 25 | -------------------------------------------------------------------------------- /src/whmapi/request.spec.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { WhmApiRequest, WhmApiType } from "./request"; 24 | import { Pager } from "../utils/pager"; 25 | import { FilterOperator } from "../utils/filter"; 26 | import { SortDirection, SortType } from "../utils/sort"; 27 | import { 28 | Headers, 29 | WhmApiTokenHeader, 30 | CpanelApiTokenHeader, 31 | CpanelApiTokenMismatchError, 32 | } from "../utils/headers"; 33 | 34 | describe("WhmApiRequest: ", () => { 35 | describe("when not fully initialized", () => { 36 | it("should not create request object without a method", () => { 37 | expect(() => new WhmApiRequest(WhmApiType.JsonApi)).toThrowError(); 38 | }); 39 | }); 40 | 41 | it("Should generate a POST with a wwwurlencoded body by default", () => { 42 | const request = new WhmApiRequest(WhmApiType.XmlApi, { 43 | method: "api_method", 44 | }); 45 | expect(request).toBeDefined(); 46 | expect(request.generate()).toEqual({ 47 | headers: new Headers([ 48 | { 49 | name: "Content-Type", 50 | value: "application/x-www-form-urlencoded", 51 | }, 52 | ]), 53 | url: "/xml-api/api_method", 54 | body: "api.version=1", 55 | }); 56 | }); 57 | 58 | it("Should generate a request that always contains ‘api.version=1’ as a request parameter", () => { 59 | const request = new WhmApiRequest(WhmApiType.XmlApi, { 60 | method: "api_method", 61 | }); 62 | expect(request).toBeDefined(); 63 | const genReq = request.generate(); 64 | expect(genReq.body).toEqual("api.version=1"); 65 | }); 66 | 67 | it("Should generate a request including paging params if set", () => { 68 | const request = new WhmApiRequest(WhmApiType.JsonApi, { 69 | method: "api_method", 70 | pager: new Pager(2, 10), 71 | }); 72 | expect(request).toBeDefined(); 73 | const genReq = request.generate(); 74 | expect(genReq.body).toMatch( 75 | "api.chunk.enable=1&api.chunk.verbose=1&api.chunk.start=11&api.chunk.size=10", 76 | ); 77 | }); 78 | 79 | it("should generate a request including filter params if set", () => { 80 | const request = new WhmApiRequest(WhmApiType.JsonApi, { 81 | method: "api_method", 82 | filters: [ 83 | { 84 | column: "id", 85 | operator: FilterOperator.GreaterThan, 86 | value: 100, 87 | }, 88 | ], 89 | }); 90 | const genReq = request.generate(); 91 | expect(genReq.body).toMatch( 92 | "api.filter.enable=1&api.filter.verbose=1&api.filter.a.field=id&api.filter.a.type=gt&api.filter.a.arg0=100", 93 | ); 94 | }); 95 | 96 | it("should generate a request if multiple filter params if set", () => { 97 | const request = new WhmApiRequest(WhmApiType.XmlApi, { 98 | method: "api_method", 99 | filters: [ 100 | { 101 | column: "id", 102 | operator: FilterOperator.GreaterThan, 103 | value: 100, 104 | }, 105 | { 106 | column: "name", 107 | operator: FilterOperator.Contains, 108 | value: "unit test", 109 | }, 110 | ], 111 | }); 112 | const genReq = request.generate(); 113 | expect(genReq.body).toMatch( 114 | "api.filter.enable=1&api.filter.verbose=1&api.filter.a.field=id&api.filter.a.type=gt&api.filter.a.arg0=100&api.filter.b.field=name&api.filter.b.type=contains&api.filter.b.arg0=unit%20test", 115 | ); 116 | }); 117 | 118 | it("should generate a request with sort parameters if set", () => { 119 | const request = new WhmApiRequest(WhmApiType.XmlApi, { 120 | method: "api_method", 121 | sorts: [ 122 | { 123 | column: "title", 124 | direction: SortDirection.Descending, 125 | type: SortType.Lexicographic, 126 | }, 127 | ], 128 | }); 129 | const genReq = request.generate(); 130 | expect(genReq.body).toMatch( 131 | "api.sort.enable=1&api.sort.a.field=title&api.sort.a.reverse=1&api.sort.a.method=lexicographic", 132 | ); 133 | }); 134 | 135 | it("should generate a request with multiple sort parameters if set", () => { 136 | const request = new WhmApiRequest(WhmApiType.XmlApi, { 137 | method: "api_method", 138 | sorts: [ 139 | { 140 | column: "title", 141 | direction: SortDirection.Descending, 142 | type: SortType.Lexicographic, 143 | }, 144 | { 145 | column: "user", 146 | direction: SortDirection.Ascending, 147 | type: SortType.Numeric, 148 | }, 149 | ], 150 | }); 151 | const genReq = request.generate(); 152 | expect(genReq.body).toMatch( 153 | "api.sort.enable=1&api.sort.a.field=title&api.sort.a.reverse=1&api.sort.a.method=lexicographic", 154 | ); 155 | }); 156 | 157 | it("should generate a request with the arguments", () => { 158 | const request = new WhmApiRequest(WhmApiType.JsonApi, { 159 | method: "api_method", 160 | arguments: [ 161 | { 162 | name: "label", 163 | value: "unit", 164 | }, 165 | ], 166 | }); 167 | const genReq = request.generate(); 168 | expect(genReq.body).toMatch("label=unit"); 169 | }); 170 | 171 | it("should generate a request with the arguments", () => { 172 | const request = new WhmApiRequest(WhmApiType.JsonApi, { 173 | method: "api_method", 174 | arguments: [ 175 | { 176 | name: "label", 177 | value: "unit", 178 | }, 179 | ], 180 | }); 181 | const genReq = request.generate(); 182 | expect(genReq.body).toMatch("label=unit"); 183 | }); 184 | 185 | it("should generate a json-api request when API type is set to WhmApiType.JsonApi", () => { 186 | const request = new WhmApiRequest(WhmApiType.JsonApi, { 187 | method: "api_method", 188 | arguments: [ 189 | { 190 | name: "label", 191 | value: "unit", 192 | }, 193 | ], 194 | }); 195 | const genReq = request.generate(); 196 | expect(genReq.url).toEqual("/json-api/api_method"); 197 | }); 198 | 199 | describe("when json encoding is requested", () => { 200 | it("should generate a POST with a JSON body by default", () => { 201 | const request = new WhmApiRequest(WhmApiType.JsonApi, { 202 | method: "api_method", 203 | arguments: [ 204 | { 205 | name: "label", 206 | value: "unit", 207 | }, 208 | ], 209 | config: { 210 | json: true, 211 | }, 212 | }); 213 | expect(request).toBeDefined(); 214 | expect(request.generate()).toEqual({ 215 | headers: new Headers([ 216 | { 217 | name: "Content-Type", 218 | value: "application/json", 219 | }, 220 | ]), 221 | url: "/json-api/api_method", 222 | body: '{"api.version":1,"label":"unit"}', 223 | }); 224 | }); 225 | }); 226 | 227 | describe("when calling with WHM API token with token and user", () => { 228 | it("should generate a correct interchange", () => { 229 | const request = new WhmApiRequest(WhmApiType.JsonApi, { 230 | namespace: "test", 231 | method: "simple_call", 232 | headers: [new WhmApiTokenHeader("fake", "user")], 233 | }); 234 | expect(request).toBeDefined(); 235 | expect(request.generate()).toEqual({ 236 | headers: new Headers([ 237 | { 238 | name: "Content-Type", 239 | value: "application/x-www-form-urlencoded", 240 | }, 241 | { 242 | name: "Authorization", 243 | value: "whm user:fake", 244 | }, 245 | ]), 246 | url: "/json-api/simple_call", 247 | body: "api.version=1", 248 | }); 249 | }); 250 | }); 251 | 252 | describe("when calling with WHM API token with combined user/token", () => { 253 | it("should generate a correct interchange", () => { 254 | const request = new WhmApiRequest(WhmApiType.JsonApi, { 255 | namespace: "test", 256 | method: "simple_call", 257 | headers: [new WhmApiTokenHeader("user:fake")], 258 | }); 259 | expect(request).toBeDefined(); 260 | expect(request.generate()).toEqual({ 261 | headers: new Headers([ 262 | { 263 | name: "Content-Type", 264 | value: "application/x-www-form-urlencoded", 265 | }, 266 | { 267 | name: "Authorization", 268 | value: "whm user:fake", 269 | }, 270 | ]), 271 | url: "/json-api/simple_call", 272 | body: "api.version=1", 273 | }); 274 | }); 275 | }); 276 | 277 | describe("when calling with cPanel API token", () => { 278 | it("should throw an error", () => { 279 | expect(() => { 280 | new WhmApiRequest(WhmApiType.JsonApi, { 281 | namespace: "test", 282 | method: "simple_call", 283 | headers: [new CpanelApiTokenHeader("fake", "user")], 284 | }); 285 | }).toThrowError(CpanelApiTokenMismatchError); 286 | }); 287 | }); 288 | }); 289 | -------------------------------------------------------------------------------- /src/whmapi/request.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import snakeCase from "lodash/snakeCase"; 24 | 25 | import * as Perl from "../utils/perl"; 26 | 27 | import { SortDirection, SortType } from "../utils/sort"; 28 | 29 | import { FilterOperator } from "../utils/filter"; 30 | 31 | import { IArgument } from "../utils/argument"; 32 | 33 | import { IPager } from "../utils/pager"; 34 | 35 | import { GenerateRule, Request, IRequest } from "../request"; 36 | 37 | import { HttpVerb } from "../http/verb"; 38 | 39 | import { RequestInfo } from "../interchange"; 40 | 41 | import { 42 | CpanelApiTokenHeader, 43 | CpanelApiTokenMismatchError, 44 | Headers, 45 | Header, 46 | } from "../utils/headers"; 47 | 48 | import { 49 | ArgumentSerializationRule, 50 | argumentSerializationRules, 51 | } from "../argument-serializer-rules"; 52 | 53 | import { 54 | IArgumentEncoder, 55 | JsonArgumentEncoder, 56 | WwwFormUrlArgumentEncoder, 57 | } from "../utils/encoders"; 58 | 59 | import padStart from "lodash/padStart"; 60 | 61 | /** 62 | * Type of response format for WHM API 1. The data can be requested to be sent back 63 | * either in JSON format or XML format. 64 | */ 65 | export enum WhmApiType { 66 | /** 67 | * Json-Api request 68 | */ 69 | JsonApi = "json-api", 70 | 71 | /** 72 | * Xml-Api request 73 | */ 74 | XmlApi = "xml-api", 75 | } 76 | 77 | export class WhmApiRequest extends Request { 78 | /** 79 | * The API output format the request should be generated for. 80 | */ 81 | public apiType: WhmApiType = WhmApiType.JsonApi; 82 | 83 | /** 84 | * Add a custom HTTP header to the request 85 | * 86 | * @param name Name of a column 87 | * @return Updated Request object. 88 | */ 89 | addHeader(header: Header): Request { 90 | if (header instanceof CpanelApiTokenHeader) { 91 | throw new CpanelApiTokenMismatchError( 92 | "A CpanelApiTokenHeader cannot be used on a WhmApiRequest", 93 | ); 94 | } 95 | super.addHeader(header); 96 | return this; 97 | } 98 | 99 | /** 100 | * Build a fragment of the parameter list based on the list of name/value pairs. 101 | * 102 | * @param params Parameters to serialize. 103 | * @param encoder Encoder to use to serialize the each parameter. 104 | * @return Fragment with the serialized parameters 105 | */ 106 | private _build(params: IArgument[], encoder: IArgumentEncoder): string { 107 | let fragment = ""; 108 | params.forEach((arg, index, array) => { 109 | const isLast: boolean = index === array.length - 1; 110 | fragment += encoder.encode(arg.name, arg.value, isLast); 111 | }); 112 | return encoder.separatorStart + fragment + encoder.separatorEnd; 113 | } 114 | 115 | /** 116 | * Convert from a number into a string that WHM API 1 will sort 117 | * in the same order as the numbers; e.g.: 26=>"za", 52=>"zza", ... 118 | * @method _make_whm_api_fieldspec_from_number 119 | * @private 120 | * @param num Index of sort item 121 | * @return letter combination for the index of the sort item. 122 | */ 123 | private _make_whm_api_fieldspec_from_number(num: number): string { 124 | const left = padStart("", Math.floor(num / 26), "z"); 125 | return left + "abcdefghijklmnopqrstuvwxyz".charAt(num % 26); 126 | } 127 | 128 | /** 129 | * Generates the arguments for the request. 130 | * 131 | * @param params List of parameters to adjust based on the sort rules in the Request. 132 | */ 133 | private _generateArguments(params: IArgument[]): void { 134 | // For any WHM API call, the API version must be specified as an argument. It is required. 135 | // Adding it first before everything. 136 | const apiVersionParam: IArgument = { name: "api.version", value: 1 }; 137 | params.push(apiVersionParam); 138 | this.arguments.forEach((argument) => params.push(argument)); 139 | } 140 | 141 | /** 142 | * Generates the sort parameters for the request. 143 | * 144 | * @param params List of parameters to adjust based on the sort rules in the Request. 145 | */ 146 | private _generateSorts(params: IArgument[]): void { 147 | this.sorts.forEach((sort, index) => { 148 | if (index === 0) { 149 | params.push({ 150 | name: "api.sort.enable", 151 | value: Perl.fromBoolean(true), 152 | }); 153 | } 154 | 155 | const sortPrefix = `api.sort.${this._make_whm_api_fieldspec_from_number( 156 | index, 157 | )}`; 158 | 159 | params.push({ name: `${sortPrefix}.field`, value: sort.column }); 160 | params.push({ 161 | name: `${sortPrefix}.reverse`, 162 | value: Perl.fromBoolean( 163 | sort.direction !== SortDirection.Ascending, 164 | ), 165 | }); 166 | params.push({ 167 | name: `${sortPrefix}.method`, 168 | value: snakeCase(SortType[sort.type]), 169 | }); 170 | }); 171 | } 172 | 173 | /** 174 | * Look up the correct name for the filter operator 175 | * 176 | * @param operator Type of filter operator to use to filter the items 177 | * @returns The string counter part for the filter operator. 178 | * @throws Will throw an error if an unrecognized FilterOperator is provided. 179 | */ 180 | private _lookupFilterOperator(operator: FilterOperator): string { 181 | switch (operator) { 182 | case FilterOperator.GreaterThanUnlimited: 183 | return "gt_handle_unlimited"; 184 | case FilterOperator.GreaterThan: 185 | return "gt"; 186 | case FilterOperator.LessThanUnlimited: 187 | return "lt_handle_unlimited"; 188 | case FilterOperator.LessThan: 189 | return "lt"; 190 | case FilterOperator.Equal: 191 | return "eq"; 192 | case FilterOperator.Begins: 193 | return "begins"; 194 | case FilterOperator.Contains: 195 | return "contains"; 196 | default: 197 | // eslint-disable-next-line no-case-declarations -- improves readability 198 | const key = FilterOperator[operator]; 199 | throw new Error( 200 | `Unrecoginzed FilterOperator ${key} for WHM API 1`, 201 | ); 202 | } 203 | } 204 | 205 | /** 206 | * Generate the filter parameters, if any. 207 | * 208 | * @param params List of parameters to adjust based on the filter rules provided. 209 | */ 210 | private _generateFilters(params: IArgument[]): void { 211 | this.filters.forEach((filter, index) => { 212 | if (index === 0) { 213 | params.push({ 214 | name: "api.filter.enable", 215 | value: Perl.fromBoolean(true), 216 | }); 217 | params.push({ 218 | name: "api.filter.verbose", 219 | value: Perl.fromBoolean(true), 220 | }); 221 | } 222 | 223 | const filterPrefix = `api.filter.${this._make_whm_api_fieldspec_from_number( 224 | index, 225 | )}`; 226 | params.push({ 227 | name: `${filterPrefix}.field`, 228 | value: filter.column, 229 | }); 230 | params.push({ 231 | name: `${filterPrefix}.type`, 232 | value: this._lookupFilterOperator(filter.operator), 233 | }); 234 | params.push({ name: `${filterPrefix}.arg0`, value: filter.value }); 235 | }); 236 | } 237 | 238 | /** 239 | * In UAPI, we request the starting record, not the starting page. This translates 240 | * the page and page size into the correct starting record. 241 | * 242 | * @param pager Object containing pager settings. 243 | */ 244 | private _translatePageToStart(pager: IPager) { 245 | return (pager.page - 1) * pager.pageSize + 1; 246 | } 247 | 248 | /** 249 | * Generate the pager request parameters, if any. 250 | * 251 | * @param params List of parameters to adjust based on the pagination rules. 252 | */ 253 | private _generatePagination(params: IArgument[]): void { 254 | if (!this.usePager) { 255 | return; 256 | } 257 | 258 | const allPages = this.pager.all(); 259 | params.push({ 260 | name: "api.chunk.enable", 261 | value: Perl.fromBoolean(true), 262 | }); 263 | params.push({ 264 | name: "api.chunk.verbose", 265 | value: Perl.fromBoolean(true), 266 | }); 267 | params.push({ 268 | name: "api.chunk.start", 269 | value: allPages ? -1 : this._translatePageToStart(this.pager), 270 | }); 271 | if (!allPages) { 272 | params.push({ 273 | name: "api.chunk.size", 274 | value: this.pager.pageSize, 275 | }); 276 | } 277 | } 278 | 279 | /** 280 | * Create a new UAPI request. 281 | * 282 | * @param init Optional request object used to initialize this object. 283 | */ 284 | constructor(apiType: WhmApiType, init?: IRequest) { 285 | super(init); 286 | 287 | // Needed for or pure js clients since they don't get the compiler checks 288 | if (apiType != WhmApiType.JsonApi && apiType != WhmApiType.XmlApi) { 289 | throw new Error( 290 | "You must define the API type for the whmapi call before you generate a request.", 291 | ); 292 | } else { 293 | this.apiType = apiType; 294 | } 295 | 296 | if (!this.method) { 297 | throw new Error( 298 | "You must define a method for the WHM API call before you generate a request", 299 | ); 300 | } 301 | } 302 | 303 | /** 304 | * Generate the interchange object that has the pre-encoded 305 | * request using UAPI formatting. 306 | * 307 | * @param rule Optional parameter to specify a specific Rule we want the Request to be generated for. 308 | * @return {RequestInfo} Request information ready to be used by a remoting layer 309 | */ 310 | generate(rule?: GenerateRule): RequestInfo { 311 | if (!rule) { 312 | rule = { 313 | verb: HttpVerb.POST, 314 | encoder: this.config.json 315 | ? new JsonArgumentEncoder() 316 | : new WwwFormUrlArgumentEncoder(), 317 | }; 318 | } 319 | 320 | if (!rule.encoder) { 321 | rule.encoder = this.config.json 322 | ? new JsonArgumentEncoder() 323 | : new WwwFormUrlArgumentEncoder(); 324 | } 325 | 326 | const argumentRule: ArgumentSerializationRule = 327 | argumentSerializationRules.getRule(rule.verb); 328 | 329 | const info = { 330 | headers: new Headers([ 331 | { 332 | name: "Content-Type", 333 | value: rule.encoder.contentType, 334 | }, 335 | ]), 336 | url: ["", this.apiType, this.method] 337 | .map(encodeURIComponent) 338 | .join("/"), 339 | body: "", 340 | }; 341 | 342 | const params: IArgument[] = []; 343 | this._generateArguments(params); 344 | this._generateSorts(params); 345 | this._generateFilters(params); 346 | this._generatePagination(params); 347 | 348 | const encoded = this._build(params, rule.encoder); 349 | 350 | if (argumentRule.dataInBody) { 351 | info["body"] = encoded; 352 | } else { 353 | if (rule.verb === HttpVerb.GET) { 354 | info["url"] += `?${encoded}`; 355 | } else { 356 | info["url"] += encoded; 357 | } 358 | } 359 | 360 | this.headers.forEach((header) => { 361 | info.headers.push({ 362 | name: header.name, 363 | value: header.value, 364 | }); 365 | }); 366 | 367 | return info; 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /src/whmapi/response.spec.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { WhmApiResponse } from "./response"; 24 | import { DefaultMetaData } from "../response"; 25 | 26 | describe("WhmApiResponse", () => { 27 | describe("constructor", () => { 28 | it("should fail when response is undefined", () => { 29 | expect(() => new WhmApiResponse(undefined)).toThrowError(); 30 | }); 31 | it("should report failure when response result is 0", () => { 32 | const serverResponse = { 33 | metadata: { 34 | result: 0, 35 | }, 36 | }; 37 | expect(new WhmApiResponse(serverResponse).success).toBe(false); 38 | }); 39 | it("should report success when response result is 1", () => { 40 | const serverResponse = { 41 | data: {}, 42 | metadata: { 43 | result: 1, 44 | }, 45 | }; 46 | expect(new WhmApiResponse(serverResponse).success).toBe(true); 47 | }); 48 | it("should store the data property if present in the response", () => { 49 | const expectedData = { 50 | test: "Test", 51 | }; 52 | const serverResponse = { 53 | data: expectedData, 54 | metadata: { 55 | result: 1, 56 | }, 57 | }; 58 | expect(new WhmApiResponse(serverResponse).data).toBe(expectedData); 59 | }); 60 | it("should store extra metadata information in ‘properties’ property under whmApiResponseObj.meta", () => { 61 | const expectedData = {}; 62 | const expectedMetadata = { 63 | result: 1, 64 | extra: "property", 65 | }; 66 | const response = new WhmApiResponse({ 67 | data: expectedData, 68 | metadata: expectedMetadata, 69 | }); 70 | expect(response.meta.properties).toBeDefined(); 71 | expect(Object.keys(response.meta.properties)).toContain("extra"); 72 | }); 73 | it("should parse any errors provided", () => { 74 | const error = "Api Failure"; 75 | const serverResponse = { 76 | metadata: { 77 | result: 0, 78 | reason: error, 79 | }, 80 | }; 81 | const response = new WhmApiResponse(serverResponse); 82 | expect(response.errors.length).not.toBe(0); 83 | expect(response.errors[0].message).toEqual(error); 84 | }); 85 | it("should reduce the list data if an array is assigned to a single hash.", () => { 86 | const listData = [ 87 | "Test", 88 | "List", 89 | "Returned", 90 | "To", 91 | "Single", 92 | "Hash", 93 | ]; 94 | const serverResponse = { 95 | data: { 96 | test: listData, 97 | }, 98 | metadata: { 99 | result: 1, 100 | }, 101 | }; 102 | expect(new WhmApiResponse(serverResponse).data).toBe(listData); 103 | }); 104 | it("should assign default metadata if metadata is not defined.", () => { 105 | const expectedMetadata = DefaultMetaData; 106 | expect(new WhmApiResponse({}).meta).toEqual(expectedMetadata); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /src/whmapi/response.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | import { IMetaData } from "../metadata"; 24 | 25 | import { MessageType, Response, ResponseOptions } from "../response"; 26 | 27 | /** 28 | * This class will extract the available metadata from the WHM API format into a standard format for JavaScript developers. 29 | */ 30 | export class WhmApiMetaData implements IMetaData { 31 | /** 32 | * Indicates if the data is paged. 33 | */ 34 | isPaged = false; 35 | 36 | /** 37 | * The record number of the first record of a page. 38 | */ 39 | record = 0; 40 | 41 | /** 42 | * The current page. 43 | */ 44 | page = 0; 45 | 46 | /** 47 | * The page size of the returned set. 48 | */ 49 | pageSize = 0; 50 | 51 | /** 52 | * The total number of records available on the backend. 53 | */ 54 | totalRecords = 0; 55 | 56 | /** 57 | * The total number of pages of records on the backend. 58 | */ 59 | totalPages = 0; 60 | 61 | /** 62 | * Indicates if the data set if filtered. 63 | */ 64 | isFiltered = false; 65 | 66 | /** 67 | * Number of records available before the filter was processed. 68 | */ 69 | recordsBeforeFilter = 0; 70 | 71 | /** 72 | * Indicates the response was the result of a batch API. 73 | */ 74 | batch = false; 75 | 76 | /** 77 | * A collection of the other less-common or custom WHM API metadata properties. 78 | */ 79 | properties: { [index: string]: any } = {}; 80 | 81 | /** 82 | * Build a new MetaData object from the metadata response from the server. 83 | * 84 | * @param meta WHM API metadata object. 85 | */ 86 | constructor(meta: any) { 87 | // Handle pagination 88 | if (meta.chunk) { 89 | this.isPaged = true; 90 | this.record = parseInt(meta.chunk.start, 10) || 0; 91 | this.page = parseInt(meta.chunk.current, 10) || 0; 92 | this.pageSize = parseInt(meta.chunk.size, 10) || 0; 93 | this.totalPages = parseInt(meta.chunk.chunks, 10) || 0; 94 | this.totalRecords = parseInt(meta.chunk.records, 10) || 0; 95 | } 96 | 97 | // Handle filtering 98 | if (meta.filter) { 99 | this.isFiltered = true; 100 | this.recordsBeforeFilter = parseInt(meta.filter.filtered, 10) || 0; 101 | } 102 | 103 | // Get any other custom metadata properties off the object 104 | const builtinSet = new Set(["paginate", "filter"]); 105 | Object.keys(meta) 106 | .filter((key: string) => !builtinSet.has(key)) 107 | .forEach((key: string) => { 108 | this.properties[key] = meta[key]; 109 | }); 110 | } 111 | } 112 | 113 | /** 114 | * Parser that will convert a WHM API wire-formatted object into a standard response object for JavaScript developers. 115 | */ 116 | export class WhmApiResponse extends Response { 117 | /** 118 | * Parse out the status from the response. 119 | * 120 | * @param resMetadata Metadata returned in response object from the backend. 121 | * @return Number indicating success or failure. > 1 success, 0 failure. 122 | */ 123 | private _parseStatus(resMetadata: any): void { 124 | this.status = 0; // Assume it failed. 125 | if (typeof resMetadata.result === "undefined") { 126 | throw new Error( 127 | "The response should have a numeric status property indicating the API succeeded (>0) or failed (=0)", 128 | ); 129 | } 130 | this.status = parseInt(resMetadata.result, 10); 131 | } 132 | 133 | /** 134 | * Parse out the messages from the response. 135 | * 136 | * @param resMetadata Metadata returned in response object from the backend. 137 | */ 138 | private _parseMessages(resMetadata: any): void { 139 | if (!resMetadata.result) { 140 | const errors: any[] = [resMetadata.reason]; 141 | if (errors && errors.length) { 142 | errors.forEach((error: string) => { 143 | this.messages.push({ 144 | type: MessageType.Error, 145 | message: error, 146 | }); 147 | }); 148 | } 149 | } 150 | 151 | // TODO: If there are any other types of messages sent. They need to be handled here (like non error messages returned via API call.) 152 | } 153 | 154 | /** 155 | * WHM API 1 usually puts list data into a single-key hash. 156 | * This isn't useful for us, so we get rid of the extra hash. 157 | * 158 | * @method _reduce_list_data 159 | * @private 160 | * @param data The "data" member of the API JSON response 161 | * @return The reduced data object. 162 | */ 163 | private _reduce_list_data(data: any): any { 164 | if (typeof data === "object" && !(data instanceof Array)) { 165 | const keys = Object.keys(data); 166 | if (keys.length === 1) { 167 | const maybe_data = data[keys[0]]; 168 | if (maybe_data) { 169 | if (maybe_data instanceof Array) { 170 | data = maybe_data; 171 | } 172 | } else { 173 | data = []; 174 | } 175 | } 176 | } 177 | 178 | return data; 179 | } 180 | 181 | /** 182 | * Parse out the status, data and metadata from a WHM API response into the abstract Response and IMetaData structures. 183 | * 184 | * @param response Raw response from the server. It's just been JSON.parse() at this point. 185 | * @param Options On how to handle parsing of the response. 186 | */ 187 | constructor(response: any, options?: ResponseOptions) { 188 | super(response, options); 189 | 190 | if (response) { 191 | if (response.metadata) { 192 | this._parseStatus(response.metadata); 193 | this._parseMessages(response.metadata); 194 | this.meta = new WhmApiMetaData(response.metadata); 195 | } 196 | } else { 197 | throw new Error("Response object should be defined."); 198 | } 199 | 200 | // TODO: Add parsing by specific types to take care of renames and type coercion. 201 | this.data = this._reduce_list_data(response.data); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "tsconfig", 4 | "lib": ["esnext", "dom"], 5 | 6 | "moduleResolution": "node", 7 | "target": "ES2020", 8 | "strict": true, 9 | "rootDir": "./src", 10 | 11 | "declaration": true, 12 | "declarationDir": "./dist/types", 13 | "esModuleInterop": true, 14 | 15 | "noEmitOnError": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitAny": true, 18 | "noImplicitThis": true, 19 | "noUnusedParameters": false, 20 | "noUnusedLocals": true, 21 | "skipDefaultLibCheck": true, 22 | "skipLibCheck": true, 23 | "strictNullChecks": true, 24 | "types": ["jasmine", "node"], 25 | 26 | "sourceMap": true, 27 | "removeComments": false 28 | }, 29 | "include": ["src/*.ts", "src/**/*.ts"] 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "./dist/cjs" 6 | }, 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base", 3 | "compilerOptions": { 4 | "module": "ES2020", 5 | "outDir": "./dist/esm" 6 | }, 7 | "exclude": ["node_modules", "src/**/*.spec.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.prod-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "./dist/cjs" 6 | }, 7 | "exclude": ["node_modules", "src/**/*.spec.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.prod-esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base", 3 | "compilerOptions": { 4 | "module": "ES2020", 5 | "outDir": "./dist/esm" 6 | }, 7 | "exclude": ["node_modules", "src/**/*.spec.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright 2021 cPanel L.L.C. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | const path = require("path"); 24 | const webpack = require("webpack"); 25 | const commonConfig = { 26 | entry: "./dist/cjs/index.js", 27 | output: { 28 | path: path.resolve(__dirname, "dist", "bundles"), 29 | library: ["@cpanel", "api"], 30 | libraryTarget: "umd", 31 | }, 32 | }; 33 | function buildConfig(env) { 34 | if (env["dev"]) { 35 | let devConfigOptions = commonConfig; 36 | devConfigOptions.mode = "development"; 37 | devConfigOptions.output.filename = "cpanel-api.umd.js"; 38 | return devConfigOptions; 39 | } else if (env["prod"]) { 40 | let prodConfigOptions = commonConfig; 41 | prodConfigOptions.mode = "production"; 42 | prodConfigOptions.output.filename = "cpanel-api.umd.min.js"; 43 | return prodConfigOptions; 44 | } else { 45 | console.log( 46 | "Wrong webpack build parameter. Possible choices: 'dev' or 'prod'.", 47 | ); 48 | } 49 | } 50 | 51 | module.exports = buildConfig; 52 | --------------------------------------------------------------------------------