client.js

'use strict';

const defaults = require('lodash/defaults');
const axios = require('axios').default;

/**
 * Class representing an Origami Repo Data client.
 *
 * @example <caption>Create a client</caption>
 * const repoData = new RepoDataClient({
 *     apiKey: 'xxxXxXxX-XXXX-XXXX-xXXx-xxxXXXxXXXXX',
 *     apiSecret: 'xxXXXxxXXXXXXXXXxxxxxxxXXXxXxXXXXXXxxXXx'
 * });
 */
class RepoDataClient {

	/**
	 * Create an Origami Repo Data client.
	 * @param {Object} [options] - The client options.
	 * @param {String} [options.apiKey] - The API key to use when making requests.
	 * Defaults to the value of the <code>REPO_DATA_API_KEY</code> environment variable.
	 * @param {String} [options.apiSecret] - The API secret to use when making requests.
	 * Defaults to the value of the <code>REPO_DATA_API_SECRET</code> environment variable.
	 * @param {String} [options.apiUrl] - The URL of the Origami Repo Data service.
	 * Defaults to the value of the <code>REPO_DATA_API_URL</code> environment variable or the production service.
	 * @returns {RepoDataClient} A new RepoDataClient instance.
	 */
	constructor(options) {
		this.options = RepoDataClient.defaultOptions(options);
	}

	/**
	 * Get a list of all available Origami repositories as an array.
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/repositories#get-v1-repos}
	 * @param {Object} [filters] - Parameters to filter repositories by.
	 * @param {(Array.<String>|String|null)} [filters.brand] - A brand (or an array of brands) to filter repositories by.
	 * One of: <code>'master'</code>, <code>'internal'</code>, <code>'whitelabel'</code>.
	 * Any repository which doesn't include this brand will not be included in the response.
	 * If this parameter is set to <code>'none'</code> or <code>null</code> then only repositories which are not branded will be output.
	 * If this parameter is set to <code>'all'</code> then only repositories which have at least one brand will be output.
	 * @param {String} [filters.search] - free text to search repositories by.
	 * Searchable fields are name, description, keywords, and demo titles.
	 * Any repository which doesn't match the search string will not be included in the response.
	 * @param {(Array.<String>|String)} [filters.status] - An Origami component support status (or an array of statuses) to filter repositories by.
	 * One of: <code>'active'</code>, <code>'maintained'</code>, <code>'experimental'</code>, <code>'deprecated'</code>, <code>'dead'</code>.
	 * Any repository which doesn't have this status will not be included in the response.
	 * @param {(Array.<String>|String)} [filters.type] - An Origami repo type (or an array of types) to filter repositories by.
	 * One of: <code>'module'</code>, <code>'service'</code>, <code>'imageset'</code>.
	 * Any repository which doesn't have this type will not be included in the response.
	 * @param {(Array.<String>|String)} [filters.origamiVersion] - An Origami Version (or an array of Origami Versions) to filter repositories by.
	 * E.g: <code>'1'</code>, <code>'2.0'</code>.
	 * Any repository which doesn't support this version of the Origami Specification will not be included in the response.
	 * @returns {Promise<Array>} A promise which resolves with the repositories.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>List repositories</caption>
	 * const repos = await repoData.listRepos();
	 *
	 * @example <caption>List repositories with filters</caption>
	 * const repos = await repoData.listRepos({
	 *     brand: 'master',
	 *     search: 'color',
	 *     status: 'active',
	 *     type: 'module'
	 *});
	 */
	listRepos(filters = {}) {
		const {brand, search, status, type, origamiVersion} = filters;
		const query = {};

		// Sanitize and set brand
		if (brand && Array.isArray(brand)) {
			query.brand = brand.join(',');
		}
		if (brand === null) {
			query.brand = 'none';
		}
		if (brand && typeof brand === 'string') {
			query.brand = brand;
		}

		// Sanitize and set the Origami Version
		if (origamiVersion && Array.isArray(origamiVersion)) {
			query.origamiVersion = origamiVersion.join(',');
		}
		if (origamiVersion && typeof origamiVersion === 'string') {
			query.origamiVersion = origamiVersion;
		}

		// Set search
		if (search && typeof search === 'string') {
			query.q = search;
		}

		// Sanitize and set status
		if (status && Array.isArray(status)) {
			query.status = status.join(',');
		}
		if (status && typeof status === 'string') {
			query.status = status;
		}

		// Sanitize and set type
		if (type && Array.isArray(type)) {
			query.type = type.join(',');
		}
		if (type && typeof type === 'string') {
			query.type = type;
		}

		return this.get('/v1/repos', query);
	}

	/**
	 * Get a list of all branded Origami repositories as an array.
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/repositories#get-v1-repos}
	 * @param {String} brand Brand to look for. One of: 'all', 'master', 'internal', 'whitelabel' or 'none'
	 * @returns {Promise<Array>} A promise which resolves with the repositories.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 * @deprecated Deprecated in favour of filter options for {@link RepoDataClient#listRepos}.
	 *
	 * @example <caption>List repositories based on brand</caption>
	 * const repos = await repoData.listBrandedRepos('all');
	 */
	listBrandedRepos(brand) {
		const query = {};
		if (brand) {
			query.brand = brand;
		}
		return this.get('/v1/repos', query);
	}

	/**
	 * Get a single Origami repository by ID or name.
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/repositories#get-v1-repos-(id)}
	 * @param {String} repoId - The repository UUID or name. Warning: using name over ID incurs a redirect.
	 * @returns {Promise<Object>} A promise which resolves with the repository.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Get a repository using a UUID</caption>
	 * const repo = await repoData.getRepo('c3a499f8-3d20-503c-95b0-c4705bc272b3');
	 *
	 * @example <caption>Get a repository using a name</caption>
	 * const repo = await repoData.getRepo('origami-repo-data');
	 */
	getRepo(repoId) {
		return this.get(`/v1/repos/${repoId}`);
	}

	/**
	 * Get a list of all versions for an Origami repository as an array.
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/repositories#get-v1-repos-(id)-versions}
	 * @param {String} repoId - The repository UUID or name. Warning: using name over ID incurs a redirect.
	 * @returns {Promise<Array>} A promise which resolves with the versions.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Get all repository versions using a UUID</caption>
	 * const versions = await repoData.listVersions('c3a499f8-3d20-503c-95b0-c4705bc272b3');
	 *
	 * @example <caption>Get all repository versions using a name</caption>
	 * const versions = await repoData.listVersions('origami-repo-data');
	 */
	listVersions(repoId) {
		return this.get(`/v1/repos/${repoId}/versions`);
	}

	/**
	 * Get a single version for an Origami repository by ID or name.
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/repositories#get-v1-repos-(id)-versions-(id)}
	 * @param {String} repoId - The repository UUID or name. Warning: using name over ID incurs a redirect.
	 * @param {String} versionId - The version UUID or number. Warning: using number over ID incurs a redirect.
	 * @returns {Promise<Object>} A promise which resolves with the version.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Get a repository version using UUIDs</caption>
	 * const version = await repoData.getVersion('c3a499f8-3d20-503c-95b0-c4705bc272b3', 'a530dab8-f6ff-410a-9e56-8d6f49ecff2c');
	 *
	 * @example <caption>Get a repository version using a name and number</caption>
	 * const version = await repoData.getVersion('origami-repo-data', '57.0.0');
	 */
	getVersion(repoId, versionId) {
		return this.get(`/v1/repos/${repoId}/versions/${versionId}`);
	}

	/**
	 * Get a single manifest for an Origami repository and version by type.
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/repositories#get-v1-repos-(id)-versions-(id)-manifests-(type)}
	 * @param {String} repoId - The repository UUID or name. Warning: using name over ID incurs a redirect.
	 * @param {String} versionId - The version UUID or number. Warning: using number over ID incurs a redirect.
	 * @param {String} manifestType - The type of manifest to retrieve. One of "about", "bower", "imageSet", "origami", or "package".
	 * @returns {Promise<Object>} A promise which resolves with the manifest file contents parsed as JSON.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Get a manifest using UUIDs</caption>
	 * const packageManifest = await repoData.getManifest('c3a499f8-3d20-503c-95b0-c4705bc272b3', 'a530dab8-f6ff-410a-9e56-8d6f49ecff2c', 'package');
	 *
	 * @example <caption>Get a manifest using a name and number</caption>
	 * const packageManifest = await repoData.getManifest('origami-repo-data', '57.0.0', 'package');
	 */
	getManifest(repoId, versionId, manifestType) {
		return this.get(`/v1/repos/${repoId}/versions/${versionId}/manifests/${manifestType}`);
	}

	/**
	 * Get a single markdown document for an Origami repository and version by type.
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/repositories#get-v1-repos-(id)-versions-(id)-markdown-(type)}
	 * @param {String} repoId - The repository UUID or name. Warning: using name over ID incurs a redirect.
	 * @param {String} versionId - The version UUID or number. Warning: using number over ID incurs a redirect.
	 * @param {String} markdownType - The type of markdown document to retrieve. One of "designguidelines" or "readme".
	 * @returns {Promise<String>} A promise which resolves with the markdown document as a string.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Get a markdown document using UUIDs</caption>
	 * const readme = await repoData.getMarkdown('c3a499f8-3d20-503c-95b0-c4705bc272b3', 'a530dab8-f6ff-410a-9e56-8d6f49ecff2c', 'readme');
	 *
	 * @example <caption>Get a markdown document using a name and number</caption>
	 * const readme = await repoData.getMarkdown('origami-repo-data', '57.0.0', 'readme');
	 */
	getMarkdown(repoId, versionId, markdownType) {
		return this.get(`/v1/repos/${repoId}/versions/${versionId}/markdown/${markdownType}`);
	}

	/**
	 * Get the README text for an Origami repository and version. This is a shortcut method which uses {@link RepoDataClient.getMarkdown}.
	 * @param {String} repoId - The repository UUID or name. Warning: using name over ID incurs a redirect.
	 * @param {String} versionId - The version UUID or number. Warning: using number over ID incurs a redirect.
	 * @returns {Promise<String>} A promise which resolves with the README as a string.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Get the README using UUIDs</caption>
	 * const readme = await repoData.getReadme('c3a499f8-3d20-503c-95b0-c4705bc272b3', 'a530dab8-f6ff-410a-9e56-8d6f49ecff2c');
	 *
	 * @example <caption>Get the README using a name and number</caption>
	 * const readme = await repoData.getReadme('origami-repo-data', '57.0.0');
	 */
	getReadme(repoId, versionId) {
		return this.getMarkdown(repoId, versionId, 'readme');
	}

	/**
	 * Get a list of all demos for an Origami repository and version as an array.
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/repositories#get-v1-repos-(id)-versions-(id)-demos}
	 * @param {String} repoId - The repository UUID or name. Warning: using name over ID incurs a redirect.
	 * @param {String} versionId - The version UUID or number. Warning: using number over ID incurs a redirect.
	 * @param {String} brand [null] - The brand to filter demos by. If included, only demos with the specified brand (or no brands at all) will be returned.
	 * @returns {Promise<String>} A promise which resolves with the demos.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Get all demos</caption>
	 * const demos = await repoData.listDemos('c3a499f8-3d20-503c-95b0-c4705bc272b3', 'a530dab8-f6ff-410a-9e56-8d6f49ecff2c');
	 *
	 * @example <caption>Get all demos with a brand filter</caption>
	 * const demos = await repoData.listDemos('c3a499f8-3d20-503c-95b0-c4705bc272b3', 'a530dab8-f6ff-410a-9e56-8d6f49ecff2c', 'internal');
	 */
	listDemos(repoId, versionId, brand = null) {
		const query = {};
		if (brand) {
			query.brand = brand;
		}
		return this.get(`/v1/repos/${repoId}/versions/${versionId}/demos`, query);
	}

	/**
	 * Get a list of all image set images for an Origami repository and version as an array.
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/repositories#get-v1-repos-(id)-versions-(id)-images}
	 * @param {String} repoId - The repository UUID or name. Warning: using name over ID incurs a redirect.
	 * @param {String} versionId - The version UUID or number. Warning: using number over ID incurs a redirect.
	 * @param {Object} [imageOptions] - Options which change the format of the returned images.
	 * @param {String} [imageOptions.sourceParam] - The Image Service source parameter to add to the returned image URLs.
	 * Defaults to "origami-repo-data-client-node".
	 * @returns {Promise<String>} A promise which resolves with the images.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Get all images in an image set</caption>
	 * const images = await repoData.listImages('c3a499f8-3d20-503c-95b0-c4705bc272b3', 'a530dab8-f6ff-410a-9e56-8d6f49ecff2c');
	 */
	listImages(repoId, versionId, imageOptions) {
		imageOptions = defaults({}, imageOptions, {
			sourceParam: 'origami-repo-data-client-node'
		});
		return this.get(`/v1/repos/${repoId}/versions/${versionId}/images`, {
			sourceParam: imageOptions.sourceParam
		});
	}

	/**
	 * Get a list of all dependencies for an Origami repository and version as an array.
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/repositories#get-v1-repos-(id)-versions-(id)-dependencies}
	 * @param {String} repoId - The repository UUID or name. Warning: using name over ID incurs a redirect.
	 * @param {String} versionId - The version UUID or number. Warning: using number over ID incurs a redirect.
	 * @returns {Promise<String>} A promise which resolves with the dependencies.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Get all dependencies</caption>
	 * const dependencies = await repoData.listDependencies('c3a499f8-3d20-503c-95b0-c4705bc272b3', 'a530dab8-f6ff-410a-9e56-8d6f49ecff2c');
	 */
	listDependencies(repoId, versionId) {
		return this.get(`/v1/repos/${repoId}/versions/${versionId}/dependencies`);
	}

	/**
	 * Get a list of bundle information for an Origami repository and version as an array.
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/repositories#get-v1-repos-(id)-versions-(id)-bundles-(language)}
	 * @param {String} repoId - The repository UUID or name. Warning: using name over ID incurs a redirect.
	 * @param {String} versionId - The version UUID or number. Warning: using number over ID incurs a redirect.
	 * @param {String} language - The bundle language. One of "js" or "css".
	 * @param {String} brand [null] - A brand (or an array of brands) to filter bundles by.
	 * One of: <code>'master'</code>, <code>'internal'</code>, <code>'whitelabel'</code>.
	 * Any non-branded bundles will not be included in the response.
	 * If this parameter is set to <code>'none'</code> or <code>null</code> then only bundles which are not branded will be output.
	 * If this parameter is set to <code>'all'</code> then only branded bundles will be output.
	 * @returns {Promise<String>} A promise which resolves with the bundles.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Get all CSS bundle information.</caption>
	 * 	const bundles = await repoData.listBundles(
	 * 		'c3a499f8-3d20-503c-95b0-c4705bc272b3',
	 * 		'a530dab8-f6ff-410a-9e56-8d6f49ecff2c',
	 * 		'css'
	 * 	);
	 */
	listBundles(repoId, versionId, language, brand = null) {
		const query = {};
		if (brand) {
			query.brand = brand;
		}
		return this.get(`/v1/repos/${repoId}/versions/${versionId}/bundles/${language}`, query);
	}

	/**
	 * Create a new API key which can be used to access the service (requires admin permissions).
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/keys#post-v1-keys}
	 * @param {Object} data - Information about the key being created.
	 * @param {String} data.description - A short human-readable description of the API key.
	 * @param {Boolean} [data.read=true] - Whether the API key grants read permissions.
	 * @param {Boolean} [data.write=false] - Whether the API key grants write permissions.
	 * @param {Boolean} [data.admin=false] - Whether the API key grants admin permissions.
	 * @returns {Promise<Object>} A promise which resolves with the new credentials.
	 * These will need to be stored somewhere, as the secret will never be displayed again.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Create an API key</caption>
	 * const credentials = await repoData.createKey({
	 *     description: 'A write key for manually adding ingestions',
	 *     read: true,
	 *     write: true,
	 *     admin: false
	 * });
	 */
	createKey(data) {
		return this.post('/v1/keys', data);
	}

	/**
	 * Get a list of all available API keys for the service as an array (requires admin permissions).
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/keys#get-v1-keys}
	 * @returns {Promise<Array>} A promise which resolves with the API keys.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>List API keys</caption>
	 * const repos = await repoData.listKeys();
	 */
	listKeys() {
		return this.get('/v1/keys');
	}

	/**
	 * Get a single API key for the service by ID.
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/keys#get-v1-keys-(id)}
	 * @param {String} keyId - The key UUID.
	 * @returns {Promise<Object>} A promise which resolves with the API key.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Get an API key</caption>
	 * const key = await repoData.getKey('xxxXxXxX-XXXX-XXXX-xXXx-xxxXXXxXXXXX');
	 */
	getKey(keyId) {
		return this.get(`/v1/keys/${keyId}`);
	}

	/**
	 * Delete a single API key from the service by ID.
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/keys#delete-v1-keys-(id)}
	 * @param {String} keyId - The key UUID.
	 * @returns {Promise<Object>} A promise which resolves when the key is deleted.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Delete an API key</caption>
	 * await repoData.deleteKey('xxxXxXxX-XXXX-XXXX-xXXx-xxxXXXxXXXXX');
	 */
	deleteKey(keyId) {
		return this.delete(`/v1/keys/${keyId}`);
	}

	/**
	 * Create a new ingestion and add it to the queue (requires write permissions).
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/queue#post-v1-queue}
	 * @param {Object} data - Information about the ingestion being created.
	 * @param {String} data.url - The GitHub repository URL to ingest.
	 * @param {String} data.tag - The GitHub repository tag to ingest.
	 * @returns {Promise<Object>} A promise which resolves with the new ingestion.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Create an ingestion</caption>
	 * const credentials = await repoData.createIngestion({
	 *     url: 'https://github.com/Financial-Times/origami-repo-data',
	 *     tag: '57.0.0'
	 * });
	 */
	createIngestion(data) {
		return this.post('/v1/queue', data);
	}

	/**
	 * Get a list of all current ingestions in the queue as an array.
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/queue#get-v1-queue}
	 * @returns {Promise<Array>} A promise which resolves with the ingestion queue.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>List the ingestion queue</caption>
	 * const ingestionQueue = await repoData.listIngestions();
	 */
	listIngestions() {
		return this.get('/v1/queue');
	}

	/**
	 * Get a single ingestion in the queue by ID.
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/queue#get-v1-queue-(id)}
	 * @param {String} ingestionId - The ingestion UUID.
	 * @returns {Promise<Object>} A promise which resolves with the ingestion.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Get an ingestion</caption>
	 * const ingestion = await repoData.getIngestion('799798e6-967d-492e-8fee-f7f35ec39d44');
	 */
	getIngestion(ingestionId) {
		return this.get(`/v1/queue/${ingestionId}`);
	}

	/**
	 * Delete a single ingestion from the queue by ID, preventing that repo/tag combination from being ingested (requires admin permissions).
	 * @see {@link https://origami-repo-data.ft.com/v1/docs/api/queue#delete-v1-queue-(id)}
	 * @param {String} ingestionID - The ingestion UUID.
	 * @returns {Promise<Object>} A promise which resolves when the ingestion is deleted.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 *
	 * @example <caption>Delete an ingestion</caption>
	 * await repoData.deleteIngestion('799798e6-967d-492e-8fee-f7f35ec39d44');
	 */
	deleteIngestion(ingestionID) {
		return this.delete(`/v1/queue/${ingestionID}`);
	}

	/**
	 * Perform a GET request.
	 * @private
	 * @param {String} endpoint - The service endpoint to request. The service base URL will be prepended.
	 * @param {Object} [query] - Parameters to append to the URL, which will be serialized as a querystring.
	 * @returns {Promise} A promise which resolves with the response body.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 */
	get(endpoint, query) {
		return this.request('GET', endpoint, query);
	}

	/**
	 * Perform a POST request.
	 * @private
	 * @param {String} endpoint - The service endpoint to request. The service base URL will be prepended.
	 * @param {Object} data - The POST data to send, which will be serialized as JSON.
	 * @returns {Promise} A promise which resolves with the response body.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 */
	post(endpoint, data) {
		return this.request('POST', endpoint, undefined, data);
	}

	/**
	 * Perform a DELETE request.
	 * @private
	 * @param {String} endpoint - The service endpoint to request. The service base URL will be prepended.
	 * @returns {Promise} A promise which resolves with the response body.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 */
	delete(endpoint) {
		return this.request('DELETE', endpoint);
	}

	/**
	 * Perform an HTTP request.
	 * @private
	 * @param {String} method - The HTTP method to perform the request with.
	 * @param {String} endpoint - The service endpoint to request. The service base URL will be prepended.
	 * @param {Object} [query] - Parameters to append to the URL, which will be serialized as a querystring.
	 * @param {Object} [postData] - The data to send (if method is POST), which will be serialized as JSON.
	 * @returns {Promise} A promise which resolves with the response body.
	 * @throws {Error} Will throw if a network error occurs, or if the API responds with a 40x/50x status.
	 */
	async request(method, endpoint, query, postData) {
		let response;

		try {
			response = await axios({
				method: method,
				url: `${this.options.apiUrl}${endpoint}`,
				params: query,
				headers: {
					'X-Api-Key': this.options.apiKey,
					'X-Api-Secret': this.options.apiSecret
				},
				data: postData
			});
		} catch (error) {
			if (error.response && error.response.status) {
				const newError = new Error(`${error.response.status}: ${error.message}`);
				newError.status = error.response.status;
				throw newError;
			} else {
				throw new Error(error.message);
			}
		}

		return response.data || undefined;
	}

	/**
	 * Default the client options.
	 * @private
	 * @param {Object} [options] - The user options to be defaulted.
	 * @returns {Object} The defaulted options.
	 */
	static defaultOptions(options) {
		return defaults({}, options, {
			apiUrl: process.env.REPO_DATA_API_URL,
			apiKey: process.env.REPO_DATA_API_KEY,
			apiSecret: process.env.REPO_DATA_API_SECRET,
		}, {
			apiUrl: 'https://origami-repo-data.ft.com'
		});
	}

};

module.exports = RepoDataClient;