@gridscale/api
Version:
gridscale API Wrapper
605 lines (521 loc) • 17 kB
text/typescript
import { assignIn, isArray, isFunction, isObject, isUndefined, uniqueId, assign, forEach, isPlainObject } from 'lodash';
require('es6-promise').polyfill();
require('isomorphic-fetch');
export interface Links<T> {
/**
* References to the current resultset
*/
self?(): Promise<ApiResult<T>>;
/**
* References to the first portion of results
*/
first?(): Promise<ApiResult<T>>;
/**
* References to the next portion of results
*/
next?(): Promise<ApiResult<T>>;
/**
* References to the last portion of results
*/
last?(): Promise<ApiResult<T>>;
}
export interface Meta {
/**
* the amount of datasets returned in this page of the response
*/
count: number;
/**
* The total amount of datasets available without pagination
*/
total: number;
/**
* The active limit of datasets per page
*/
limit: number;
/**
* The current offset of total available datasets
*/
offset: number;
/**
* The current page
*/
page: number;
}
// tslint:disable-next-line: no-any
export type GenericApiResult = any;
export type VoidApiResult = void;
export interface ApiResult<T> {
/**
* Inidicates if the request itself was successfully sent to the API (no indication if the API operation was successful!)
*/
success: boolean;
/**
* The result of the API operation
*/
result: T;
/**
* The raw HTTP Response
*/
response?: Response;
/**
* The Request options
*/
requestInit?: RequestInit;
/**
* Links to other resultsets of the pagination
*/
links?: Links<T>;
/**
* Pagination meta data
*/
meta?: Meta;
/**
* If this is a function, it can be used to watch the process of asynchronous requests. The returned Promise is resolved when the request finished
*/
watch?: () => Promise<ApiResult<RequestPollResult>> | null;
/**
* A unique request id (generated by this client)
*/
id?: string | null;
/**
* indicates if the 'request' has failed or 'json' parsing failed
*/
failureType?: string | null;
}
export interface ApiSettings {
/**
* The endpoint URL
*/
endpoint?: string;
/**
* A map of specific URL overrides that should go to a different endpoint (e.g. a sandbox to test new API)
* format "path:endpoint", path can be regex (string start and end with '/')
* @example
* { '/myNewObject\/(.*)/': 'https://myNewApi.getsandbox.com/myNewObject' }
*/
endpointOverrides?: { [key: string]: string; }; // override endpoint for specific paths, format "path:endpoint", path can be regex (string start and end with '/')
/**
* The API token
*/
token?: string;
/**
* The User UUID
*/
userId?: string;
/**
* Default pagination limit
*/
limit?: number;
/**
* Default Watchdelay in ms
*/
watchdelay?: number;
/**
* Api Client identifier (used for X-Api-Client header)
*/
apiClient?: string;
/**
* A custom fetch method
*/
fetch?: Function;
}
export interface RequestOptions {
/**
* Page to get the resultset for
*/
page?: number;
/**
* Maximum number of datasets to return per page
*/
limit?: number;
/**
* Array of fields to return in the response (to reduce the size of the resultset)
*/
fields?: string[];
/**
* Filters the results by a field
* @example
* "name=foo"
*/
filter?: string[];
/**
* Field to sort the result after
*/
sort?: string | string[];
}
export interface RequestPollResult {
/**
* Current status of the watched request
*/
message: string;
/**
* Short status
*/
status: string;
/**
* Time when the request was created
*/
create_time: string;
}
export interface BaseRelationObject {
object_name?: string;
object_uuid?: string;
}
/**
* interface with basic properties each object (server, storage ...) should have
*/
export interface BaseObject {
object_uuid?: string;
status?: string;
name?: string;
labels?: string[];
location_uuid?: string;
relations?: { [key: string]: BaseRelationObject[] };
}
export interface LogData {
result: ApiResult<unknown>;
response: Response;
/**
* Unique request id (generated by client)
*/
id: string;
requestInit: RequestInit;
}
export class GSError extends Error {
result: GenericApiResult;
success = false;
response: Response;
constructor(message, result) {
super();
this.name = 'GridscaleError';
// try to assemble message with more details from result
if ( result.response
&& result.response.request
&& result.response.request.method
&& typeof (result.response.status) !== 'undefined' && result.response.request.url) {
this.message = 'Error : ' + result.response.request.method
+ ' | ' + result.response.status
+ ' | ' + result.response.request.url.split('?')[0];
} else {
this.message = message || 'Default Message';
}
this.result = result;
this.response = result.response || undefined;
}
}
export class APIClass {
// Local Settings
private settings: ApiSettings = {
endpoint: 'https://api.gridscale.io',
endpointOverrides: {},
token: '',
userId: '',
limit: 25,
watchdelay: 1000,
apiClient: 'gsclient-js'
};
/**
* Store api client in current session
* @param _client String
*/
public storeClient(_client: string) {
this.settings.apiClient = _client;
}
/**
* Store Token for Current Session
* @param _token Secret Token
*/
public storeToken(_token: string, _userId: string) {
// Store Token
this.settings.token = _token;
this.settings.userId = _userId;
}
/**
* Update local Request Options
*
* @param _option
*/
public setOptions = (_option: ApiSettings) => {
// Assign new Values
assignIn(this.settings, _option);
}
/**
* Start the API Request
*
* @param _path
* @param _options
* @param _callback
* @returns {Promise}
*/
public request(_path: string = '', _options: RequestInit, _callback: (response: Response, result: ApiResult<GenericApiResult>) => void = (response, result) => { }): Promise<ApiResult<GenericApiResult>> {
const options: RequestInit = !isObject(_options) ? {} : assignIn( {}, _options );
// check if we should use another endpoint for this path (mocking)
var endpoint = this.settings.endpoint;
if (this.settings.endpointOverrides && typeof(this.settings.endpointOverrides) === 'object') {
forEach(this.settings.endpointOverrides, (_overrideEndpoint, _overridePath) => {
if (_overridePath.match(/^\/(.*)\/$/) && _path.split('?')[0].match(new RegExp(RegExp.$1))) {
endpoint = _overrideEndpoint;
} else if (_path.split('?')[0] === _overridePath) {
endpoint = _overrideEndpoint;
} else {
return true;
}
return false;
});
}
// Build Options
const url: string = _path.search('https://') === 0 ? _path : endpoint + _path; // on Links there is already
options.headers = options.headers ? options.headers : {};
options.headers['X-Auth-UserId'] = this.settings.userId;
options.headers['X-Auth-Token'] = this.settings.token;
options.headers['X-Api-Client'] = this.settings.apiClient;
// return results as object or text
const getResult = (_response: Response, _rejectOnJsonFailure = true): Promise<GenericApiResult> => {
return new Promise((_resolve, _reject) => {
if (_response.status !== 204 && _response.headers.has('Content-Type') && _response.headers.get('Content-Type').indexOf('application/json') === 0) {
_response.json()
.then(json => {
_resolve(json);
})
.catch(() => {
if (_rejectOnJsonFailure) {
_reject();
} else {
// try text
_response.text().then(text => _resolve(text))
.catch(e => _resolve(null));
}
}
);
} else {
_response.text().then(text => _resolve(text))
.catch(e => _resolve(null));
}
});
};
// Setup DEF
const def: Promise<ApiResult<GenericApiResult>> = new Promise( ( _resolve, _reject ) => {
// Fire Request
const onSuccess = (_response: Response, _request: Request, _requestInit: RequestInit) => {
getResult(_response.clone()).then((_result) => {
const result: ApiResult<GenericApiResult> = {
success: true,
result: _result,
response: _response.clone(),
id: null,
requestInit: _requestInit
};
// Check for Links and generate them as Functions
if (_result && _result._links) {
const links = {};
forEach(_result._links, (link, linkname) => {
links[linkname] = this.link(_result._links[linkname]);
});
result.links = links;
}
if (_result && _result._meta) {
result.meta = _result._meta;
}
/**
* On POST, PATCH and DELETE Request we will inject a watch Function into the Response so you can easiely start watching the current Job
*/
if (options['method'] === 'POST' || options['method'] === 'PATCH' || options['method'] === 'DELETE') {
if (result.response.headers.has('x-request-id')) {
result.watch = () => this.watchRequest(result.response.headers.get('x-request-id'));
}
}
_resolve(result);
setTimeout(() => _callback(_response.clone(), result));
})
.catch(() => {
// tslint:disable-next-line: no-use-before-declare
onFail(_response, _request, _requestInit, 'json');
});
};
const onFail = (_response: Response, _request: Request, _requestInit: RequestInit, _failType = 'request') => {
getResult(_response.clone(), false).then((_result) => {
const result: ApiResult<GenericApiResult> = {
success: false,
result: _result,
response: assign(_response.clone(), { request: _request }),
links: {},
watch: null,
id: uniqueId('apierror_' + (new Date()).getTime() + '_'),
requestInit: _requestInit,
failureType: _failType
};
this.log({
result: result,
response: _response.clone(),
id: result.id,
requestInit: result.requestInit
});
_reject( new GSError('Request Error', result) );
setTimeout(() => _callback(_response.clone(), result));
});
};
const request = new Request(url, options);
const promise = (this.settings.fetch || fetch)(request);
promise
.then((_response) => {
if (_response.ok) {
// The promise does not reject on HTTP errors
onSuccess(_response, request, options);
} else {
onFail(_response, request, options);
}
})
.catch((_response) => {
_reject(new GSError('Network failure', _response));
});
// Return promise
return promise;
} );
// Catch all Errors and
// Return DEF
return def;
}
/**
* Build Option URL to expand URL
* @param _options
* @returns {string}
*/
private buildRequestURL(_options) {
// Push Valued
const url = [];
// Add Options to URL
forEach(_options, (val, key) => {
if ( isArray(_options[key]) ) {
if (_options[key].length > 0) {
url.push(key + '=' + _options[key].join(',') );
}
} else {
url.push(key + '=' + _options[key] );
}
});
return url.length > 0 ? ('?' + url.join('&')) : '';
}
/**
* Start Get Call
* @param _path
* @param _callback
*/
public get(_path: string, _options?: RequestOptions | Function, _callback?: (response: Response, result: ApiResult<GenericApiResult>) => void): Promise<ApiResult<GenericApiResult>> {
if ( isObject( _options ) ) {
_path += this.buildRequestURL( _options );
}
// If No Options but Callback is given
if ( isUndefined( _callback ) && isFunction( _options ) ) {
_callback = _options;
}
return this.request(_path, {method: 'GET'}, _callback );
}
/**
* Start Delete Call
* @param _path
* @param _callback
*/
public remove(_path: string, _callback?: (response: Response, result: ApiResult<GenericApiResult>) => void): Promise<ApiResult<GenericApiResult>> {
return this.request(_path, {method: 'DELETE'}, _callback );
}
/**
* Send Post Request
*
* @param _path Endpoint
* @param _attributes Attributes for Post Body
* @param _callback Optional Callback
* @returns {Promise}
*/
public post(_path: string, _attributes: Object, _callback?: (response: Response, result: ApiResult<GenericApiResult>) => void): Promise<ApiResult<GenericApiResult>> {
return this.request(_path, { method : 'POST', body : JSON.stringify(_attributes), headers: {'Content-Type': 'application/json' } }, _callback );
}
/**
* Send PAtCH Request
*
* @param _path Endpoint
* @param _attributes Attributes for Post Body
* @param _callback Optional Callback
* @returns {Promise}
*/
public patch(_path: string, _attributes: Object, _callback?: (response: Response, result: ApiResult<GenericApiResult>) => void): Promise<ApiResult<GenericApiResult>> {
return this.request(_path, { method : 'PATCH', body : JSON.stringify(_attributes), headers: {'Content-Type': 'application/json' } }, _callback );
}
/**
* Generate URL for Linked Request. No Options are required because its in the URL already
*
* @param _link
* @param _callback
* @returns {Function}
*/
private link( _link ): (_callback: (response: Response, result: ApiResult<GenericApiResult>) => void) => Promise<ApiResult<GenericApiResult>> {
/**
* generate Function that has an Optional Callback
*/
return (_callback?: (response: Response, result: ApiResult<GenericApiResult>) => void): Promise<ApiResult<GenericApiResult>> => {
return this.request(_link.href, {method: 'GET'}, _callback );
};
}
/**
* Start Pooling on Request Endpoint
*
*
* @param _requestid
* @param _callback
* @returns {Promise}
*/
public requestpooling(_requestid: string, _callback?: (response: Response, result: ApiResult<GenericApiResult>) => void): Promise<ApiResult<{ [uuid: string]: RequestPollResult }>> {
return this.request('/requests/' + _requestid, {method: 'GET'}, _callback );
}
/**
* Recursive creating of Request Proises
*
*
* @param _requestid
* @param _resolve
* @param _reject
*/
public buildAndStartRequestCallback( _requestid: string , _resolve: Function, _reject: Function): void {
/**
* Start new Request
*/
this.requestpooling(_requestid).then((_result) => {
// Check Request Status to Decide if we start again
if (_result.result[ _requestid ].status === 'pending') {
setTimeout(() => {
this.buildAndStartRequestCallback(_requestid , _resolve, _reject);
}, this.settings.watchdelay );
} else if ( _result.response.status === 200 ) {
// Job done
_resolve(_result);
} else {
// IF
_reject(_result);
}
}, (_result) => _reject(_result) );
}
/**
* Watch a Single Request until it is ready or failed
*
* @param _requestid
* @param _callback
*/
public watchRequest(_requestid: string): Promise<ApiResult<RequestPollResult>> {
return new Promise( ( _resolve, _reject ) => {
this.buildAndStartRequestCallback(_requestid , _resolve, _reject);
});
}
private callbacks = [];
/**
* Adds a new logger for error logging
* @param _callback
*/
public addLogger = (_callback: (logData: LogData) => void) => {
this.callbacks.push(_callback);
}
private log = (_logData: LogData) => {
for (var i = 0; i < this.callbacks.length; i++) {
this.callbacks[i](_logData);
}
}
}
export const api = new APIClass();