@microfocus/alm-octane-js-rest-sdk
Version:
NodeJS wrapper for the Open Text ALM Octane API
559 lines (558 loc) • 22.2 kB
JavaScript
"use strict";
/*
* Copyright 2020-2023 Open Text.
*
* The only warranties for products and services of Open Text and
* its affiliates and licensors (“Open Text”) are as may be set forth
* in the express warranty statements accompanying such products and services.
* Nothing herein should be construed as constituting an additional warranty.
* Open Text shall not be liable for technical or editorial errors or
* omissions contained herein. The information contained herein is subject
* to change without notice.
*
* Except as specifically indicated otherwise, this document contains
* confidential information and a valid license is required for possession,
* use or copying. If this work is provided to the U.S. Government,
* consistent with FAR 12.211 and 12.212, Commercial Computer Software,
* Computer Software Documentation, and Technical Data for Commercial Items are
* licensed to the U.S. Government under vendor's standard commercial license.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/** @module octane */
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const request_1 = __importDefault(require("request"));
const query_1 = __importDefault(require("../query"));
const reference_1 = __importDefault(require("./models/reference"));
const multi_reference_1 = __importDefault(require("./models/multi-reference"));
const debug = require('debug')('octane');
const utils = __importStar(require("./utils"));
const Error = require('http-errors');
const META_ROUTES_FILE = '../../routes/meta.json';
const DEFAULT_ROUTES_FILE = '../../routes/default.json';
/**
* @class
*
* @param {Object} config - configurations to access Octane REST API
* @param {('http'|'https')} [config.protocol] - protocol of Octane REST API URL
* @param {String} config.host - server of Octane REST API URL
* @param {Number} [config.port] - port of Octane REST API URL
* @param {String} [config.pathPrefix] - path prefix of Octane REST API URL
* @param {Number} config.shared_space_id - Octane shared space id
* @param {Number} config.workspace_id - Octane workspace id
* @param {String} [config.proxy] - if set, using proxy to connect to Octane
* @param {String|Object} [config.routesConfig] - if set, using the specified path as API definition, or use the JSON object directly
*/
class OctaneVanilla {
constructor(config) {
config = config || {};
if (!config.host || !config.shared_space_id || !config.workspace_id) {
throw new Error('Octane host / shared space id / workspace id are required');
}
this.config = config;
if (config.routesConfig && typeof config.routesConfig === 'object') {
this.routes = config.routesConfig;
}
else if (config.routesConfig &&
path_1.default.isAbsolute(config.routesConfig) &&
fs_1.default.existsSync(config.routesConfig)) {
this.routes = JSON.parse(fs_1.default.readFileSync(config.routesConfig).toString());
}
else if (fs_1.default.existsSync(path_1.default.join(__dirname, DEFAULT_ROUTES_FILE))) {
this.routes = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, DEFAULT_ROUTES_FILE), 'utf8'));
}
else {
this.routes = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, META_ROUTES_FILE), 'utf8'));
}
if (this.config.octaneCredentials) {
this.octaneCredentials = this.config.octaneCredentials;
delete this.config.octaneCredentials;
}
this.setupRoutes();
const host = this.config.host || this.constants.host;
const protocol = this.config.protocol || this.constants.protocol || 'http';
const port = this.config.port || (protocol === 'https' ? 443 : 80);
const pathPrefix = this.config.pathPrefix
? '/' + this.config.pathPrefix.replace(/(^[/]+|[/]+$)/g, '')
: '';
this.baseUrl = protocol + '://' + host + ':' + port + pathPrefix;
this.baseApiUrl =
this.baseUrl +
'/api/shared_spaces/' +
this.config.shared_space_id +
'/workspaces/' +
this.config.workspace_id;
const opt = {
jar: true,
json: true,
baseUrl: this.baseApiUrl,
};
if (this.config.headers) {
opt.headers = this.config.headers;
}
if (this.config.tech_preview_API) {
// legacy tech preview client type
opt.headers = Object.assign(Object.assign({}, opt.headers), { 'ALM-OCTANE-TECH-PREVIEW': true });
}
if (this.config.proxy) {
opt.proxy = this.config.proxy;
}
debug('with octaneCredentials %j', opt);
this.requestor = request_1.default.defaults(opt);
}
/**
* Login to Octane.
* @param {OctaneCredentials|OctaneVanilla~authenticateCallback} [octaneCredentials] - Credentials needed to access octane.
* If no credentials are given, the last given credentials will be used.
* If no credentials were ever given, the callback will be called with an error.
* @param {OctaneVanilla~authenticateCallback} callback - Callback that handles error and authenticated operations
*/
authenticate(octaneCredentials, callback) {
if (callback === undefined && typeof octaneCredentials === 'function') {
callback = octaneCredentials;
octaneCredentials = undefined;
}
if (octaneCredentials) {
this.octaneCredentials = octaneCredentials;
}
else {
if (!this.octaneCredentials) {
const error = new Error('No authentication credentials given');
return callback(error);
}
}
debug('authenticate %j', octaneCredentials);
const body = {};
if (this.octaneCredentials.username && this.octaneCredentials.password) {
body.user = this.octaneCredentials.username;
body.password = this.octaneCredentials.password;
}
else if (this.octaneCredentials.client_id &&
this.octaneCredentials.client_secret) {
body.client_id = this.octaneCredentials.client_id;
body.client_secret = this.octaneCredentials.client_secret;
}
this.requestor.post({
url: this.baseUrl + '/authentication/sign_in',
body: body,
baseUrl: '', // set base url to nothing such that the url is not interpreted as an uri
}, (err, response, body) => {
if (err) {
debug('authentication failed %j', err);
return callback(err);
}
if (response.statusCode >= 400) {
err = new Error.HttpError(response.statusCode, body, response.headers);
debug('authentication failed %j', err);
return callback(err);
}
callback();
});
}
/**
* setupRoute() is invoked by the constructor, takes the contents of the
* JSON document that contains the definitions of all the available API
* routes and iterate over them.
* It first recurses through each definition block until it reaches an API
* endpoint. It knows that an endpoint is found when the 'url' and 'param'
* definitions are found as a direct member of a definition block.
* A method is attached to the Octane instance and becomes available for use.
* Inside this method, the parameter validation and typecasting is done,
* according to the definition of the parameters in the 'params' block, upon
* invocation.
*/
setupRoutes() {
const routes = this.routes;
if (typeof routes === 'object') {
const defines = routes.defines;
this.constants = defines.constants;
this.params = defines.params;
delete routes.defines;
}
this.prepareApi(routes);
}
prepareApi(struct, baseType) {
baseType = baseType || '';
const self = this;
Object.keys(struct).forEach(function (routePart) {
const block = struct[routePart];
if (!block) {
return;
}
const messageType = baseType + '/' + routePart;
if (block.url && block.params) {
const parts = messageType.split('/');
const section = utils.toCamelCase(parts[1].toLowerCase(), true);
parts.splice(0, 2);
const funcName = utils.toCamelCase(parts.join('-'), true);
if (!self[section]) {
self[section] = {};
self[utils.toCamelCase('get-' + section + '-api', true)] =
function () {
return self[section];
};
}
self[section][funcName] = function (msg, callback) {
debug('#%s.%s %j', section, funcName, msg);
if (!self.requestor) {
const err = new Error.Unauthorized('authentication is required');
self.sendError(err, msg, block, callback);
}
try {
self.parseParams(msg, block.params);
}
catch (ex) {
self.sendError(ex, msg, block, callback);
return;
}
self.handler(msg, JSON.parse(JSON.stringify(block)), callback);
};
}
else {
self.prepareApi(block, messageType);
}
});
}
parseParams(msg, paramsStruct) {
const self = this;
if (msg instanceof Array) {
msg.forEach((entry) => self.parseParams(entry, paramsStruct));
return;
}
const params = Object.keys(paramsStruct);
for (let i = 0, l = params.length; i < l; i++) {
let paramName = params[i];
let def;
if (paramName.charAt(0) === '$') {
paramName = paramName.substr(1);
if (self.params[paramName]) {
def = paramsStruct[paramName] = self.params[paramName];
delete paramsStruct[`$${paramName}`];
}
else {
throw new Error.BadRequest(`Invalid variable parameter name substitution; param '${paramName}' not found in defines block`);
}
}
else {
def = paramsStruct[paramName];
}
let value = utils.trim(msg[paramName]);
if (value === undefined || value === null || value === '') {
if (def.required) {
throw new Error.BadRequest(`Empty value for parameter '${paramName}': ${value}`);
}
else {
continue;
}
}
value = this.sanitizeParam(paramName, value, def);
msg[paramName] = value;
}
}
sanitizeParam(paramName, value, definition) {
if (!definition.type) {
return value;
}
let isInvalid = false;
switch (definition.type) {
case 'integer': {
const i = parseInt(value, 10);
if (isNaN(i)) {
isInvalid = false;
}
else {
if ((definition.min_value && i < definition.min_value) ||
(definition.max_value && i > definition.max_value)) {
isInvalid = false;
}
else {
value = i;
}
}
break;
}
case 'boolean': {
if (typeof value !== 'boolean') {
isInvalid = true;
}
break;
}
case 'datetime':
case 'date': {
const d = new Date(value);
if (isNaN(d.getTime())) {
isInvalid = true;
}
else {
value = d.toISOString();
}
break;
}
case 'string':
case 'memo': {
if (typeof value !== 'string') {
isInvalid = true;
}
else {
if (definition.max_length && value.length > definition.max_length) {
isInvalid = true;
}
}
break;
}
case 'object': {
if (typeof value !== 'object') {
isInvalid = true;
}
break;
}
case 'reference': {
let ref;
const multiple = definition.field_type_data.multiple;
if (multiple) {
ref = multi_reference_1.default.parse(value);
}
else {
ref = reference_1.default.parse(value);
}
if (ref) {
value = ref;
}
else {
isInvalid = true;
}
break;
}
case 'query': {
if (value instanceof query_1.default) {
value = '"' + value.build() + '"';
}
else {
isInvalid = true;
}
break;
}
case 'file': {
if (typeof value !== 'string') {
isInvalid = true;
}
else {
if (path_1.default.isAbsolute(value)) {
value = fs_1.default.createReadStream(value);
}
else {
isInvalid = true;
}
}
break;
}
default: {
throw new Error.BadRequest("Unknown parameter type for parameter '" +
paramName +
"': " +
definition.type);
}
}
if (isInvalid) {
throw new Error.BadRequest("Invalid value for parameter '" + paramName + "': " + typeof value ===
'object'
? JSON.stringify(value)
: value);
}
return value;
}
handler(msg, block, callback, retryIfUnauthorized = true) {
this.httpSend(msg, block, (err, response, body) => {
if (err) {
return this.sendError(err, msg, block, callback);
}
debug('status %d', response.statusCode);
if (response.statusCode === 401) {
if (retryIfUnauthorized) {
debug('Trying to reauthenticate');
return this.authenticate((err) => {
if (err) {
callback(err);
}
else {
this.handler(msg, block, callback, false);
}
});
}
else {
err = new Error(response.statusCode, body, response.headers);
return this.sendError(err, msg, block, callback);
}
}
if (response.statusCode >= 400) {
err = new Error(response.statusCode, body, response.headers);
return this.sendError(err, msg, block, callback);
}
let ret = body;
if (response.headers['content-type'] === 'application/octet-stream') {
ret = body;
}
else if (response.req.method === 'GET' ||
response.request.method === 'GET') {
if ('total_count' in ret && 'data' in ret) {
const totalCount = ret.total_count;
ret = ret.data;
ret.meta = { total_count: totalCount };
}
}
else if (response.req.method === 'POST') {
if ('data' in ret && ret.data.length === 1) {
ret = ret.data[0];
}
}
if (callback && typeof callback === 'function') {
callback(null, ret);
}
});
}
convertBlockToBody(msg, block) {
const body = {};
Object.keys(block.params).forEach(function (paramName) {
paramName = paramName.replace(/^[$]+/, '');
if (!(paramName in msg)) {
return;
}
const paramValue = msg[paramName];
const isUrlParam = block.url.indexOf(':' + paramName) !== -1;
if (isUrlParam) {
block.url = block.url.replace(':' + paramName, paramValue);
}
else {
body[paramName] = paramValue;
}
});
return body;
}
httpSend(msg, block, callback) {
const self = this;
let body = {};
const method = block.method.toUpperCase();
const accept = block.accept;
const contentType = block['content-type'];
if (msg instanceof Array) {
body.data = msg.map((entry) => self.convertBlockToBody(entry, block), self);
}
else if (method === 'POST' &&
(!contentType || contentType !== 'multipart/form-data')) {
body.data = [self.convertBlockToBody(msg, block)];
}
else {
body = self.convertBlockToBody(msg, block);
}
const url = block.url;
const options = {
uri: url,
method: method,
};
if (Object.keys(body)) {
if ('HEAD|GET|DELETE'.indexOf(method) !== -1) {
options.qs = body;
}
else if (method === 'POST' &&
contentType &&
contentType === 'multipart/form-data') {
const content = body.file;
delete body.file;
const entity = JSON.stringify(body);
options.formData = {
entity: {
value: entity,
options: {
contentType: 'application/json',
},
},
content: content,
};
}
else {
options.body = body;
}
}
if (accept && accept === 'application/octet-stream') {
options.headers = { accept: 'application/octet-stream' };
options.encoding = null;
}
debug('sendHttp %j', options);
this.requestor(options, callback);
}
sendError(err, msg, block, callback) {
debug('error %j', err);
debug('block %j', block);
debug('msg %j', msg);
if (typeof err === 'string') {
err = new Error.InternalServerError(err);
}
if (callback && typeof callback === 'function') {
callback(err);
}
}
}
exports.default = OctaneVanilla;
/**
* Octane API access credentials
* @typedef {Object} OctaneApiCredentials
* @property {String} client_id - API client id
* @property {String} client_secret - API client secret
*
* Octane User credentials
* @typedef {Object} OctaneUserCredentials
* @property {String} username - username
* @property {String} password - password
*
* Octane User credentials
* @typedef {OctaneUserCredentials|OctaneApiCredentials} OctaneCredentials
*
* @callback OctaneVanilla~authenticateCallback
* @param {Object} - error that occurred
*/