mnubo-sdk
Version:
mnubo SmartObjects JavaScript client
237 lines (192 loc) • 6.68 kB
text/typescript
import { base64Encode } from './utils/underscore';
import { http } from './http/http';
import { RequestOptions } from './http/request';
import * as promiseRetry from 'promise-retry';
import { Owners } from './ingestion/owners';
import { Objects } from './ingestion/objects';
import { Events } from './ingestion/events';
import { Search } from './restitution/search';
import { Bigdata } from './bigdata/bigdata';
import { Model } from './model/model';
let version: string;
try {
version = require('../package.json').version;
} catch (e) {
version = 'unknown';
}
export enum OAuth2Scopes {
ALL,
LIMITED,
READ,
WRITE,
}
export interface ClientCompression {
requests: boolean;
responses: boolean;
}
export interface ExponentialBackoff {
numberOfAttempts?: number;
initialDelayInMillis?: number;
onRetry?: (attempt: number) => any;
}
export interface ClientOptions {
id?: string;
secret?: string;
env?: string;
token?: string;
httpOptions?: RequestOptions;
compression?: boolean | ClientCompression;
exponentialBackoff?: ExponentialBackoff;
}
function isClientCompression(object: boolean | ClientCompression): object is ClientCompression {
return (<ClientCompression>object).requests !== undefined || (<ClientCompression>object).responses !== undefined;
}
class AccessToken {
private requestedAt: Date;
constructor(public value: string, public type: string, public expiresIn: number, public jti: string) {
this.requestedAt = new Date();
}
isValid() {
const now = new Date();
return now.getTime() < this.requestedAt.getTime() + this.expiresIn * 1000;
}
}
export class Client {
private defaultNumberOfAttempts: number = 5;
private defaultInitialDelay: number = 500;
public owners: Owners;
public objects: Objects;
public events: Events;
public search: Search;
public bigdata: Bigdata;
public model: Model;
private token: AccessToken;
constructor(public options: ClientOptions) {
if (options.env === undefined) {
options.env = 'sandbox';
}
if (!options.httpOptions) {
options.httpOptions = {
protocol: 'https',
hostname: this.hostname(),
port: 443,
};
}
if (!options.compression) {
options.compression = false;
}
this.owners = new Owners(this);
this.objects = new Objects(this);
this.events = new Events(this);
this.search = new Search(this);
this.bigdata = new Bigdata(this);
this.model = new Model(this);
}
hostname(): string {
let part = 'sandbox';
if (this.options.env === 'production') {
part = 'api';
}
return `rest.${part}.mnubo.com`;
}
getAccessToken(scope?: OAuth2Scopes): Promise<any> {
scope = scope || OAuth2Scopes.ALL;
const id = this.options.id;
const secret = this.options.secret;
const payload = `grant_type=client_credentials&scope=${OAuth2Scopes[scope].toUpperCase()}`;
const options: RequestOptions = {
path: '/oauth/token',
headers: new Map<string, string>(),
};
options.headers.set('Authorization', `Basic ${base64Encode(id + ':' + secret)}`);
options.headers.set('Content-Type', 'application/x-www-form-urlencoded');
options.headers.set('X-MNUBO-SDK', `JavaScript/${version}`);
Object.assign(options, this.options.httpOptions);
return http.post(options, payload).then((data: any) => {
this.token = new AccessToken(data.access_token, data.token_type, data.expires_in, data.jti);
});
}
/**
* Is the access token still valid?
* @return {boolean} false if there is no access token or if it has expired.
*/
isAccessTokenValid(): boolean {
return this.token && this.token.isValid();
}
authenticate(): Promise<any> {
if (this.isAccessTokenValid()) {
return Promise.resolve(this.token);
} else {
return this.getAccessToken().then(() => {
return this.token;
});
}
}
buildHttpOptions(path: string, contentType?: string): RequestOptions {
const options: RequestOptions = {
path: path,
headers: new Map<string, string>(),
};
const compressionAlgorithm = 'gzip';
const compression = this.options.compression;
contentType = contentType || 'application/json';
const token = this.options.token ? this.options.token : this.token.value;
options.headers.set('Authorization', `Bearer ${token}`);
options.headers.set('Content-Type', `${contentType}`);
if (isClientCompression(compression)) {
if (compression.requests === true) {
options.headers.set('Content-Encoding', compressionAlgorithm);
}
if (compression.responses === true) {
options.headers.set('Accept-Encoding', compressionAlgorithm);
}
} else if (compression === true) {
options.headers.set('Accept-Encoding', compressionAlgorithm);
options.headers.set('Content-Encoding', compressionAlgorithm);
}
Object.assign(options, this.options.httpOptions);
return options;
}
possiblyRetry(f: () => Promise<any>): Promise<any> {
const exponentialBackoff = this.options.exponentialBackoff;
if (!exponentialBackoff) {
return f().catch((err) => Promise.reject(err.payload));
} else {
const retries = exponentialBackoff.numberOfAttempts
? exponentialBackoff.numberOfAttempts
: this.defaultNumberOfAttempts;
const minTimeout = exponentialBackoff.initialDelayInMillis
? exponentialBackoff.initialDelayInMillis
: this.defaultInitialDelay;
const opts: any = {
retries,
minTimeout,
randomize: true,
};
return promiseRetry((retry: any, attempt: number) => {
return f().catch((err) => {
if (err.statusCode === 503) {
if (exponentialBackoff.onRetry && attempt <= exponentialBackoff.numberOfAttempts) {
exponentialBackoff.onRetry(attempt);
}
return retry();
} else {
return Promise.reject(err.payload);
}
});
}, opts);
}
}
get(path: string, contentType?: string) {
return this.possiblyRetry(() => http.get(this.buildHttpOptions(path, contentType)));
}
post(path: string, payload: any, contentType?: string) {
return this.possiblyRetry(() => http.post(this.buildHttpOptions(path, contentType), payload));
}
put(path: string, payload: any, contentType?: string) {
return this.possiblyRetry(() => http.put(this.buildHttpOptions(path, contentType), payload));
}
delete(path: string, contentType?: string) {
return this.possiblyRetry(() => http.delete(this.buildHttpOptions(path, contentType)));
}
}