@financial-times/cmdb.js
Advanced tools
Comparing version 3.3.0 to 3.4.0
@@ -0,0 +0,0 @@ { |
{ | ||
"name": "@financial-times/cmdb.js", | ||
"description": "A javascript library for interacting with the CMDB v3", | ||
"version": "3.3.0", | ||
"version": "3.4.0", | ||
"main": "dist/cmdb.js", | ||
@@ -6,0 +6,0 @@ "browser": "dist/cmdb.mjs", |
@@ -0,0 +0,0 @@ # cmdb.js |
389
src/cmdb.js
import querystring from 'querystring'; | ||
import fetch from 'isomorphic-unfetch'; | ||
import parseLinkHeader from './parseLinkHeader'; | ||
import parseLinkHeader from './lib/parseLinkHeader'; | ||
import required from './lib/required'; | ||
import noOpLogger from './lib/noOpLogger'; | ||
/** | ||
* Throws an error if the required key is not set in parameters | ||
* @private | ||
* @function | ||
* @example | ||
* myFunction({ | ||
* requiredParameter = required('requiredParameter) | ||
* }) | ||
* @param {string} key - The key to include in the error message | ||
* @returns {undefined} | ||
* @throws {Error} - A generic error including the given key which is missing | ||
*/ | ||
const required = key => { | ||
throw new Error(`The config parameter '${key}' is required`); | ||
}; | ||
const DEFAULT_TIMEOUT = 12000; | ||
/** | ||
* Create a noop logger with the console API | ||
* @private | ||
* @function | ||
* @returns {Object} A noop logger with the console API | ||
*/ | ||
const createNoopLogger = () => | ||
Object.keys(console).reduce( | ||
(result, key) => Object.assign({}, result, { [key]() {} }), | ||
Object.create(null) | ||
); | ||
/** | ||
* Object representing the CMDB API | ||
* @class Cmdb | ||
* @param {Object} config - An object of key/value pairs holding configuration | ||
* @param {string} [config.api=https://Cmdb.ft.com/v2/] - The CMDB API endpoint to send requests to (defaults to production, change for other environments) | ||
* @param {string} [config.api=https://cmdb.in.ft.com/v3/] - The CMDB API endpoint to send requests to (defaults to production, change for other environments) | ||
* @param {string} config.apikey - The apikey to send to CMDB API | ||
@@ -54,3 +30,3 @@ * @param {Object} [config.logger] - A logger objet with the Winston API | ||
this.verbose = verbose; | ||
this._logger = this.verbose ? logger || console : createNoopLogger(); | ||
this._logger = this.verbose ? logger || console : noOpLogger(); | ||
} | ||
@@ -101,6 +77,61 @@ | ||
statusCode: response.status, | ||
statusText: response.statusText, | ||
headers: response.headers, | ||
body: response.parsedBody, | ||
}); | ||
/** | ||
* Helper function for safely parsing the cmdb response body | ||
* @method | ||
* @private | ||
* @param {Object} [requestOptions] - The options to use in logger calls | ||
* @param {string} [requestOptions.path] - The path of the fetch call | ||
* @param {string} [requestOptions.method] - The method of the fetch call | ||
* @param {Response} response - The fetch Response object | ||
* @returns {Promise<Object>} - The parsed JSON body | ||
*/ | ||
Cmdb.prototype._parseResponseBody = function(requestOptions = {}, response) { | ||
const { path, method } = requestOptions; | ||
if (!response.ok) { | ||
this._logger.log({ | ||
event: 'CMDB_ERROR', | ||
path, | ||
method, | ||
statusCode: response.status, | ||
statusText: response.statusText, | ||
}); | ||
} | ||
const contentType = response.headers.get('content-type') || ''; | ||
return response | ||
.json() | ||
.catch(error => { | ||
this._logger.log( | ||
{ | ||
event: 'CMDB_CONTENT_TYPE_MISMATCH', | ||
path, | ||
method, | ||
error, | ||
expectedContentType: contentType, | ||
statusCode: response.status, | ||
statusText: response.statusText, | ||
}, | ||
`Expected ${contentType} but body was not parsable` | ||
); | ||
throw createResponseError( | ||
`Received response with invalid body from CMDB`, | ||
response | ||
); | ||
}) | ||
.then(responseBody => { | ||
if (!response.ok) { | ||
throw createResponseError( | ||
`Received ${response.status} response from CMDB`, | ||
Object.assign(response, { parsedBody: responseBody }) | ||
); | ||
} | ||
return responseBody; | ||
}); | ||
}; | ||
/** | ||
* Helper function for making requests to CMDB API | ||
@@ -114,4 +145,6 @@ * @method | ||
* @param {Object} [body] - An object to send to the API | ||
* @param {Object} options - the request options | ||
* @param {number} [timeout=12000] - the optional timeout period in milliseconds | ||
* @returns {Promise<Object>} The data received from CMDB (JSON-decoded) | ||
* @param {boolean} [parseBody=true] - whether to parse the body as JSON, or return the whole response | ||
* @returns {Promise<Response|Object>} The data received from CMDB. JSON decoded if parseBody is true | ||
*/ | ||
@@ -122,11 +155,6 @@ Cmdb.prototype._fetch = function _fetch( | ||
query, | ||
method, | ||
method = 'GET', | ||
body, | ||
timeout = 12000 | ||
{ timeout = DEFAULT_TIMEOUT, parseBody = true } | ||
) { | ||
// HACK: CMDB decodes paths before they hit its router, so do an extra encode on the whole path here | ||
// Check for existence of CMDBV3 variable to avoid encoding | ||
// if (!process.env.CMDBV3) { | ||
// path = encodeURIComponent(path); | ||
// } | ||
if (query && Object.keys(query).length > 0) { | ||
@@ -139,14 +167,18 @@ path = `${path}?${querystring.stringify(query)}`; | ||
this._getFetchCredentials(locals, { method, body, timeout }) | ||
).then(response => { | ||
if (response.status >= 400) { | ||
throw createResponseError( | ||
`Received ${response.status} response from CMDB`, | ||
response | ||
); | ||
} | ||
return response.json(); | ||
}); | ||
).then( | ||
response => | ||
parseBody | ||
? this._parseResponseBody({ path, method }, response) | ||
: response | ||
); | ||
}; | ||
/** | ||
@typedef documentCount | ||
@type {Object} | ||
@property {number} pages The number of pages for the request | ||
@property {number} items The number of items for the request | ||
*/ | ||
/** | ||
* Helper function for requested count of pages and itemsfrom CMDB API | ||
@@ -159,3 +191,3 @@ * @method | ||
* @param {number} [timeout=12000] - the optional timeout period in milliseconds | ||
* @returns {Promise<Object>} The count of pages and items from CMDB (JSON-decoded) | ||
* @returns {Promise<documentCount>} The count of pages and items from CMDB (JSON-decoded) | ||
*/ | ||
@@ -166,3 +198,3 @@ Cmdb.prototype._fetchCount = function _fetchCount( | ||
query, | ||
timeout = 12000 | ||
timeout = DEFAULT_TIMEOUT | ||
) { | ||
@@ -173,38 +205,36 @@ if (query && Object.keys(query).length > 0) { | ||
return fetch( | ||
`${this.api}${path}`, | ||
this._getFetchCredentials(locals, { timeout }) | ||
).then(response => { | ||
// CMDB returns entirely different output when there are zero contacts | ||
// Just return an empty array in this case. | ||
if (response.status === 404) { | ||
return {}; | ||
} | ||
if (response.status !== 200) { | ||
throw createResponseError( | ||
`Received ${response.status} response from CMDB`, | ||
response | ||
); | ||
} | ||
return this._fetch(locals, path, query, undefined, undefined, { | ||
timeout, | ||
parseBody: false, | ||
}) | ||
.then(response => { | ||
return this._parseResponseBody({ path, query }, response).then( | ||
body => { | ||
// default page and items count based on a single page containing array of items | ||
return response.json().then(body => { | ||
// default page and items count based on a single page containing array of items | ||
let pages = 1; | ||
let items = body.length; | ||
let pages = 1; | ||
let items = body.length; | ||
// aim to get "Count: Pages: nnn, Items: nnn" | ||
const countstext = response.headers.get('Count'); | ||
if (countstext) { | ||
// we now have "Pages: nnn, Items: nnn" | ||
const counts = countstext.split(','); | ||
if (counts.length === 2) { | ||
// we now have "Pages: nnn" and "Items: nnn" | ||
pages = parseInt(counts[0].split(':')[1].trim(), 10); | ||
items = parseInt(counts[1].split(':')[1].trim(), 10); | ||
// aim to get "Count: Pages: nnn, Items: nnn" | ||
const countText = response.headers.get('Count'); | ||
if (countText) { | ||
// we now have "Pages: nnn, Items: nnn" | ||
const counts = countText.split(','); | ||
if (counts.length === 2) { | ||
// we now have "Pages: nnn" and "Items: nnn" | ||
[pages, items] = counts.map(count => | ||
parseInt(count.split(':')[1].trim(), 10) | ||
); | ||
} | ||
} | ||
return { pages, items }; | ||
} | ||
); | ||
}) | ||
.catch(error => { | ||
if (error.statusCode === 404) { | ||
return []; | ||
} | ||
return { pages, items }; | ||
throw error; | ||
}); | ||
}); | ||
}; | ||
@@ -217,3 +247,3 @@ | ||
* @param {Object} [locals] - The res.locals value from a request in express | ||
* @param {string} url - The url of the request to make | ||
* @param {string} path - The path of the request to make | ||
* @param {Object} query - The query parameters to use as a javascript object | ||
@@ -225,34 +255,32 @@ * @param {number} [timeout=12000] - the optional timeout period in milliseconds | ||
locals, | ||
url, | ||
path, | ||
query, | ||
timeout = 12000 | ||
timeout = DEFAULT_TIMEOUT | ||
) { | ||
if (query && Object.keys(query).length > 0) { | ||
url = `${url}?${querystring.stringify(query)}`; | ||
} | ||
return fetch(url, this._getFetchCredentials(locals, { timeout })) | ||
.then(response => { | ||
return this._fetch(locals, path, query, 'GET', undefined, { | ||
timeout, | ||
parseBody: false, | ||
}) | ||
.then(response => | ||
this._parseResponseBody({ path, method: 'GET' }, response).then( | ||
body => { | ||
const links = parseLinkHeader(response.headers.get('link')); | ||
if (links.next) { | ||
return this._fetchAll( | ||
locals, | ||
links.next, | ||
query, | ||
timeout | ||
).then(nextBody => [...body, ...nextBody]); | ||
} | ||
return body; | ||
} | ||
) | ||
) | ||
.catch(error => { | ||
// CMDB returns entirely different output when there are zero contacts | ||
// Just return an empty array in this case. | ||
if (response.status === 404) { | ||
if (error.statusCode === 404) { | ||
return []; | ||
} | ||
if (response.status !== 200) { | ||
throw createResponseError( | ||
`Received ${response.status} response from CMDB`, | ||
response | ||
); | ||
} | ||
const links = parseLinkHeader(response.headers.get('link')); | ||
if (links.next) { | ||
return response.json().then(data => { | ||
return this._fetchAll(locals, links.next).then(nextdata => [ | ||
...data, | ||
...nextdata, | ||
]); | ||
}); | ||
} | ||
return response.json(); | ||
}) | ||
.catch(error => { | ||
this._logger.error(error); | ||
@@ -276,6 +304,8 @@ throw error; | ||
key = required('key'), | ||
timeout = 12000 | ||
timeout = DEFAULT_TIMEOUT | ||
) { | ||
const path = `items/${encodeURIComponent(type)}/${encodeURIComponent(key)}`; | ||
return this._fetch(locals, path, undefined, undefined, undefined, timeout); | ||
return this._fetch(locals, path, undefined, undefined, undefined, { | ||
timeout, | ||
}); | ||
}; | ||
@@ -300,3 +330,3 @@ | ||
relatedFields, | ||
timeout = 12000 | ||
timeout = DEFAULT_TIMEOUT | ||
) { | ||
@@ -311,3 +341,3 @@ const path = `items/${encodeURIComponent(type)}/${encodeURIComponent(key)}`; | ||
} | ||
return this._fetch(locals, path, query, undefined, undefined, timeout); | ||
return this._fetch(locals, path, query, undefined, undefined, { timeout }); | ||
}; | ||
@@ -330,6 +360,6 @@ | ||
body = required('body'), | ||
timeout = 12000 | ||
timeout = DEFAULT_TIMEOUT | ||
) { | ||
const path = `items/${encodeURIComponent(type)}/${encodeURIComponent(key)}`; | ||
return this._fetch(locals, path, undefined, 'PUT', body, timeout); | ||
return this._fetch(locals, path, undefined, 'PUT', body, { timeout }); | ||
}; | ||
@@ -350,6 +380,8 @@ | ||
key = required('key'), | ||
timeout = 12000 | ||
timeout = DEFAULT_TIMEOUT | ||
) { | ||
const path = `items/${encodeURIComponent(type)}/${encodeURIComponent(key)}`; | ||
return this._fetch(locals, path, undefined, 'DELETE', undefined, timeout); | ||
return this._fetch(locals, path, undefined, 'DELETE', undefined, { | ||
timeout, | ||
}); | ||
}; | ||
@@ -372,6 +404,6 @@ | ||
limit, | ||
timeout = 12000 | ||
timeout = DEFAULT_TIMEOUT | ||
) { | ||
const encodedTypePath = type ? `/${encodeURIComponent(type)}` : ''; | ||
const url = `${this.api}items${encodedTypePath}`; | ||
const path = `items${encodedTypePath}`; | ||
let query = {}; | ||
@@ -384,3 +416,3 @@ if (criteria) { | ||
} | ||
return this._fetchAll(locals, url, query, timeout); | ||
return this._fetchAll(locals, path, query, timeout); | ||
}; | ||
@@ -407,5 +439,5 @@ | ||
limit, | ||
timeout = 12000 | ||
timeout = DEFAULT_TIMEOUT | ||
) { | ||
const url = `${this.api}items/${encodeURIComponent(type)}`; | ||
const path = `items/${encodeURIComponent(type)}`; | ||
let query = {}; | ||
@@ -424,3 +456,3 @@ if (fields) { | ||
} | ||
return this._fetchAll(locals, url, query, timeout); | ||
return this._fetchAll(locals, path, query, timeout); | ||
}; | ||
@@ -441,3 +473,3 @@ | ||
criteria, | ||
timeout = 12000 | ||
timeout = DEFAULT_TIMEOUT | ||
) { | ||
@@ -476,3 +508,3 @@ const path = `items/${encodeURIComponent(type)}`; | ||
limit, | ||
timeout = 12000 | ||
timeout = DEFAULT_TIMEOUT | ||
) { | ||
@@ -525,3 +557,3 @@ let query = { | ||
limit, | ||
timeout = 12000 | ||
timeout = DEFAULT_TIMEOUT | ||
) { | ||
@@ -557,15 +589,33 @@ const path = `items/${encodeURIComponent(type)}`; | ||
const getRelationshipPath = ({ | ||
const arrayToPath = array => | ||
array | ||
.filter(item => !!item) | ||
.map(param => encodeURIComponent(param)) | ||
.join('/'); | ||
/** | ||
* @name Cmdb#getRelationships | ||
* @method | ||
* @memberof Cmdb | ||
* @description Returns an array of relationships from an item | ||
* @param {Object} [locals] - The res.locals value from a request in express | ||
* @param {string} subjectType - The source item type for the relationship | ||
* @param {string} subjectID - The source item dataItemID for the relationship | ||
* @param {string} [relType] - The relationship type for the relationship. Optional | ||
* @param {number} [timeout=12000] - the optional timeout period in milliseconds | ||
* @returns {Promise<Object>} The updated data about the item held in the CMDB | ||
*/ | ||
Cmdb.prototype.getRelationships = function( | ||
locals, | ||
subjectType = required('subjectType'), | ||
subjectID = required('subjectID'), | ||
relType = required('relType'), | ||
objectType = required('objectType'), | ||
objectID = required('objectID'), | ||
} = {}) => | ||
[ | ||
relType, | ||
timeout = DEFAULT_TIMEOUT | ||
) { | ||
const path = arrayToPath([ | ||
'relationships', | ||
...[subjectType, subjectID, relType, objectType, objectID].map(param => | ||
encodeURIComponent(param) | ||
), | ||
].join('/'); | ||
...[subjectType, subjectID, relType], | ||
]); | ||
return this._fetch(locals, path, undefined, 'GET', {}, { timeout }); | ||
}; | ||
@@ -587,2 +637,18 @@ /** | ||
Cmdb.prototype.getRelationship = function( | ||
locals, | ||
subjectType = required('subjectType'), | ||
subjectID = required('subjectID'), | ||
relType = required('relType'), | ||
objectType = required('objectType'), | ||
objectID = required('objectID'), | ||
timeout = DEFAULT_TIMEOUT | ||
) { | ||
const path = arrayToPath([ | ||
'relationships', | ||
...[subjectType, subjectID, relType, objectType, objectID], | ||
]); | ||
return this._fetch(locals, path, undefined, 'GET', {}, { timeout }); | ||
}; | ||
/** | ||
@@ -602,3 +668,17 @@ * @name Cmdb#putRelationship | ||
*/ | ||
Cmdb.prototype.putRelationship = function( | ||
locals, | ||
subjectType = required('subjectType'), | ||
subjectID = required('subjectID'), | ||
relType = required('relType'), | ||
objectType = required('objectType'), | ||
objectID = required('objectID'), | ||
timeout = DEFAULT_TIMEOUT | ||
) { | ||
const path = arrayToPath([ | ||
'relationships', | ||
...[subjectType, subjectID, relType, objectType, objectID], | ||
]); | ||
return this._fetch(locals, path, undefined, 'POST', {}, { timeout }); | ||
}; | ||
/** | ||
@@ -618,27 +698,18 @@ * @name Cmdb#deleteRelationship | ||
*/ | ||
[ | ||
{ key: 'putRelationship', method: 'POST' }, | ||
{ key: 'getRelationship', method: 'GET' }, | ||
{ key: 'deleteRelationship', method: 'DELETE' }, | ||
].forEach(({ key, method }) => { | ||
Cmdb.prototype[key] = function( | ||
locals, | ||
subjectType = required('subjectType'), | ||
subjectID = required('subjectID'), | ||
relType = required('relType'), | ||
objectType = required('objectType'), | ||
objectID = required('objectID'), | ||
timeout = 12000 | ||
) { | ||
const path = getRelationshipPath({ | ||
subjectType, | ||
subjectID, | ||
relType, | ||
objectType, | ||
objectID, | ||
}); | ||
return this._fetch(locals, path, undefined, method, {}, timeout); | ||
}; | ||
}); | ||
Cmdb.prototype.deleteRelationship = function( | ||
locals, | ||
subjectType = required('subjectType'), | ||
subjectID = required('subjectID'), | ||
relType = required('relType'), | ||
objectType = required('objectType'), | ||
objectID = required('objectID'), | ||
timeout = DEFAULT_TIMEOUT | ||
) { | ||
const path = arrayToPath([ | ||
'relationships', | ||
...[subjectType, subjectID, relType, objectType, objectID], | ||
]); | ||
return this._fetch(locals, path, undefined, 'DELETE', {}, { timeout }); | ||
}; | ||
export default Cmdb; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
862002
13
8528
8