UNPKG

servicestack-client

Version:

ServiceStack's TypeScript library providing convenience utilities in developing web apps. Integrates with ServiceStack's Server features including ServiceClient, Server Events, Error Handling and Validation

1,333 lines (1,131 loc) 44.6 kB
import 'fetch-everywhere'; export interface IReturnVoid { createResponse(); } export interface IReturn<T> { createResponse(): T; } export class ResponseStatus { errorCode: string; message: string; stackTrace: string; errors: ResponseError[]; meta: { [index: string]: string; }; } export class ResponseError { errorCode: string; fieldName: string; message: string; meta: { [index: string]: string; }; } export class ErrorResponse { type: ErrorResponseType; responseStatus: ResponseStatus; } export type ErrorResponseType = null | "RefreshTokenException"; export interface IResolver { tryResolve(Function): any; } export class NewInstanceResolver implements IResolver { tryResolve(ctor:ObjectConstructor): any { return new ctor(); } } export class SingletonInstanceResolver implements IResolver { tryResolve(ctor:ObjectConstructor): any { return (ctor as any).instance || ((ctor as any).instance = new ctor()); } } export interface ServerEventMessage { type: "ServerEventConnect" | "ServerEventHeartbeat" | "ServerEventJoin" | "ServerEventLeave" | "ServerEventUpdate" | "ServerEventMessage"; eventId: number; channel: string; data: string; selector: string; json: string; op: string; target: string; cssSelector: string; body: any; meta: { [index:string]: string; }; } export interface ServerEventCommand extends ServerEventMessage { userId: string; displayName: string; channels: string; profileUrl: string; } export interface ServerEventConnect extends ServerEventCommand { id: string; unRegisterUrl: string; heartbeatUrl: string; updateSubscriberUrl: string; heartbeatIntervalMs: number; idleTimeoutMs: number; } export interface ServerEventHeartbeat extends ServerEventCommand { } export interface ServerEventJoin extends ServerEventCommand { } export interface ServerEventLeave extends ServerEventCommand { } export interface ServerEventUpdate extends ServerEventCommand { } const TypeMap = { onConnect: "ServerEventConnect", onHeartbeat: "ServerEventHeartbeat", onJoin: "ServerEventJoin", onLeave: "ServerEventLeave", onUpdate: "ServerEventUpdate" }; export interface IReconnectServerEventsOptions { url?: string; onerror?: (...args: any[]) => void; onmessage?: (...args: any[]) => void; error?: Error; } /** * EventSource */ export enum ReadyState { CONNECTING = 0, OPEN = 1, CLOSED = 2 } export interface IEventSourceStatic extends EventTarget { new (url: string, eventSourceInitDict?: IEventSourceInit): IEventSourceStatic; url: string; withCredentials: boolean; CONNECTING: ReadyState; // constant, always 0 OPEN: ReadyState; // constant, always 1 CLOSED: ReadyState; // constant, always 2 readyState: ReadyState; onopen: Function; onmessage: (event: IOnMessageEvent) => void; onerror: Function; close: () => void; } export interface IEventSourceInit { withCredentials?: boolean; } export interface IOnMessageEvent { data: string; } declare var EventSource: IEventSourceStatic; export interface IEventSourceOptions { channels?: string; handlers?: any; receivers?: any; onException?: Function; onReconnect?: Function; onTick?: Function; resolver?: IResolver; validate?: (request:ServerEventMessage) => boolean; heartbeatUrl?: string; unRegisterUrl?: string; updateSubscriberUrl?: string; heartbeatIntervalMs?: number; heartbeat?: number; resolveStreamUrl?: (url:string) => string; } export class ServerEventsClient { static UnknownChannel = "*"; eventStreamUri: string; updateSubscriberUrl: string; connectionInfo: ServerEventConnect; serviceClient: JsonServiceClient; stopped: boolean; resolver: IResolver; listeners: { [index:string]: ((e:ServerEventMessage) => void)[] }; EventSource: IEventSourceStatic; withCredentials: boolean; constructor( baseUrl: string, public channels: string[], public options: IEventSourceOptions = {}, public eventSource: IEventSourceStatic = null) { if (this.channels.length === 0) throw "at least 1 channel is required"; this.resolver = this.options.resolver || new NewInstanceResolver(); this.eventStreamUri = combinePaths(baseUrl, "event-stream") + "?"; this.updateChannels(channels); this.serviceClient = new JsonServiceClient(baseUrl); this.listeners = {}; this.withCredentials = true; if (!this.options.handlers) this.options.handlers = {}; } onMessage = (e: IOnMessageEvent) => { if (this.stopped) return; var opt = this.options; if (typeof document == "undefined") { var document:any = { //node querySelectorAll: sel => [] }; } var parts = splitOnFirst(e.data, " "); var channel = null; var selector = parts[0]; var selParts = splitOnFirst(selector, "@"); if (selParts.length > 1) { channel = selParts[0]; selector = selParts[1]; } const json = parts[1]; var body = null; try { body = json ? JSON.parse(json) : null; } catch(ignore){} parts = splitOnFirst(selector, "."); if (parts.length <= 1) throw "invalid selector format: " + selector; var op = parts[0], target = parts[1].replace(new RegExp("%20", "g"), " "); const tokens = splitOnFirst(target, "$"); const [cmd, cssSelector] = tokens; const els = cssSelector && document.querySelectorAll(cssSelector); const el = els && els[0]; const eventId = parseInt((e as any).lastEventId); const data = e.data; const type = TypeMap[cmd] || "ServerEventMessage"; const request:ServerEventMessage = { eventId, data, type, channel, selector, json, body, op, target:tokens[0], cssSelector, meta:{} }; const mergedBody = typeof body == "object" ? Object.assign({}, request, body) : request; if (opt.validate && opt.validate(request) === false) return; var headers = new Headers(); headers.set("Content-Type", "text/plain"); if (op === "cmd") { if (cmd === "onConnect") { this.connectionInfo = mergedBody; if (typeof body.heartbeatIntervalMs == "string") this.connectionInfo.heartbeatIntervalMs = parseInt(body.heartbeatIntervalMs); if (typeof body.idleTimeoutMs == "string") this.connectionInfo.idleTimeoutMs = parseInt(body.idleTimeoutMs); Object.assign(opt, body); var fn = opt.handlers["onConnect"]; if (fn){ fn.call(el || document.body, this.connectionInfo, request); if (this.stopped) return; } if (opt.heartbeatUrl) { if (opt.heartbeat) { clearInterval(opt.heartbeat); } opt.heartbeat = setInterval(() => { if (this.eventSource.readyState === EventSource.CLOSED) { clearInterval(opt.heartbeat); const stopFn = opt.handlers["onStop"]; if (stopFn != null) stopFn.apply(this.eventSource); this.reconnectServerEvents({ error: new Error("EventSource is CLOSED") }); return; } fetch(new Request(opt.heartbeatUrl, { method: "POST", mode: "cors", headers: headers })) .then(res => { if (!res.ok) throw new Error(`${res.status} - ${res.statusText}`); }) .catch(error => this.reconnectServerEvents({ error })); }, (this.connectionInfo && this.connectionInfo.heartbeatIntervalMs) || opt.heartbeatIntervalMs || 10000); } if (opt.unRegisterUrl) { if (typeof window != "undefined") { window.onunload = () => this.stop(); } } this.updateSubscriberUrl = opt.updateSubscriberUrl; this.updateChannels((opt.channels || "").split(",")); } else { var isCmdMsg = cmd == "onJoin" || cmd == "onLeave" || cmd == "onUpdate"; var fn = opt.handlers[cmd]; if (fn) { if (isCmdMsg) { fn.call(el || document.body, mergedBody); } else { fn.call(el || document.body, body, request); } } else { if (!isCmdMsg) { //global receiver var r = opt.receivers && opt.receivers["cmd"]; this.invokeReceiver(r, cmd, el, request, "cmd"); } } if (isCmdMsg) { fn = opt.handlers["onCommand"]; if (fn) { fn.call(el || document.body, mergedBody); } } } } else if (op === "trigger") { this.raiseEvent(target, request); } else if (op === "css") { css(els || document.querySelectorAll("body"), cmd, body); } //Named Receiver var r = opt.receivers && opt.receivers[op]; this.invokeReceiver(r, cmd, el, request, op); if (!TypeMap[cmd]) { var fn = opt.handlers["onMessage"]; if (fn) { fn.call(el || document.body, mergedBody); } } if (opt.onTick) opt.onTick(); } onError = (error?:any):void => { if (this.stopped) return; if (!error) error = event; var fn = this.options.onException; if (fn != null) fn.call(this.eventSource, error); if (this.options.onTick) this.options.onTick(); } getEventSourceOptions() { return { withCredentials: this.withCredentials }; } reconnectServerEvents(opt:IReconnectServerEventsOptions = {}) { if (this.stopped) return; if (opt.error) this.onError(opt.error); const hold = this.eventSource; var url = opt.url || this.eventStreamUri || hold.url; if (this.options.resolveStreamUrl != null) { url = this.options.resolveStreamUrl(url); } const es = this.EventSource ? new this.EventSource(url, this.getEventSourceOptions()) : new EventSource(url, this.getEventSourceOptions()); es.addEventListener('error', e => opt.onerror || hold.onerror || this.onError); es.addEventListener('message', opt.onmessage || hold.onmessage || this.onMessage); var fn = this.options.onReconnect; if (fn != null) fn.call(es, opt.error); if (hold.removeEventListener) { hold.removeEventListener('error', this.onError); hold.removeEventListener('message', this.onMessage as any); } hold.close(); return this.eventSource = es; } start() { this.stopped = false; if (this.eventSource == null || this.eventSource.readyState === EventSource.CLOSED) { var url = this.eventStreamUri; if (this.options.resolveStreamUrl != null) { url = this.options.resolveStreamUrl(url); } this.eventSource = this.EventSource ? new this.EventSource(url, this.getEventSourceOptions()) : new EventSource(url, this.getEventSourceOptions()); this.eventSource.addEventListener('error', this.onError); this.eventSource.addEventListener('message', e => this.onMessage(e as any)); } return this; } stop() : Promise<void> { this.stopped = true; if (this.eventSource) { this.eventSource.close(); } var opt = this.options; if (opt && opt.heartbeat) { clearInterval(opt.heartbeat); } var hold = this.connectionInfo; if (hold == null || hold.unRegisterUrl == null) return new Promise<void>((resolve, reject) => resolve()); this.connectionInfo = null; return fetch(new Request(hold.unRegisterUrl, { method: "POST", mode: "cors" })) .then(res => { if (!res.ok) throw new Error(`${res.status} - ${res.statusText}`); }) .catch(this.onError); } invokeReceiver(r:any, cmd:string, el:Element, request:ServerEventMessage, name:string) { if (r) { if (typeof r == "function") { r = this.resolver.tryResolve(r); } cmd = cmd.replace("-",""); r.client = this; r.request = request; if (typeof (r[cmd]) == "function") { r[cmd].call(el || r, request.body, request); } else if (cmd in r) { r[cmd] = request.body; } else { var cmdLower = cmd.toLowerCase(); for (var k in r) { if (k.toLowerCase() == cmdLower) { if (typeof r[k] == "function") { r[k].call(el || r, request.body, request); } else { r[k] = request.body; } return; } } var noSuchMethod = r["noSuchMethod"]; if (typeof noSuchMethod == "function") { noSuchMethod.call(el || r, request.target, request); } } } } hasConnected() { return this.connectionInfo != null; } registerHandler(name:string, fn:Function) { if (!this.options.handlers) this.options.handlers = {}; this.options.handlers[name] = fn; return this; } setResolver(resolver:IResolver) { this.options.resolver = resolver; return this; } registerReceiver(receiver:any){ return this.registerNamedReceiver("cmd", receiver); } registerNamedReceiver(name:string, receiver:any) { if (!this.options.receivers) this.options.receivers = {}; this.options.receivers[name] = receiver; return this; } unregisterReceiver(name:string = "cmd") { if (this.options.receivers) { delete this.options.receivers[name]; } return this; } updateChannels(channels:string[]) { this.channels = channels; const url = this.eventSource != null ? this.eventSource.url : this.eventStreamUri; this.eventStreamUri = url.substring(0, Math.min(url.indexOf("?"), url.length)) + "?channels=" + channels.join(",") + "&t=" + new Date().getTime(); } update(subscribe:string|string[], unsubscribe:string|string[]) { var sub = typeof subscribe == "string" ? subscribe.split(',') : subscribe; var unsub = typeof unsubscribe == "string" ? unsubscribe.split(',') : unsubscribe; var channels = []; for (var i in this.channels) { var c = this.channels[i]; if (unsub == null || unsub.indexOf(c) === -1) { channels.push(c); } } if (sub) { for (var i in sub) { var c = sub[i]; if (channels.indexOf(c) === -1) { channels.push(c); } } } this.updateChannels(channels); } addListener(eventName:string, handler:((e:ServerEventMessage) => void)) { var handlers = this.listeners[eventName] || (this.listeners[eventName] = []); handlers.push(handler); return this; } removeListener(eventName:string, handler:((e:ServerEventMessage) => void)) { var handlers = this.listeners[eventName]; if (handlers) { var pos = handlers.indexOf(handler); if (pos >= 0) { handlers.splice(pos, 1); } } return this; } raiseEvent(eventName:string, msg:ServerEventMessage) { var handlers = this.listeners[eventName]; if (handlers) { handlers.forEach(x => { try { x(msg); } catch (e) { this.onError(e); } }); } } getConnectionInfo(){ if (this.connectionInfo == null) throw "Not Connected"; return this.connectionInfo; } getSubscriptionId() { return this.getConnectionInfo().id; } updateSubscriber(request:UpdateEventSubscriber): Promise<void> { if (request.id == null) request.id = this.getSubscriptionId(); return this.serviceClient.post(request) .then(x => { this.update(request.subscribeChannels, request.unsubscribeChannels); }).catch(this.onError); } subscribeToChannels(...channels:string[]): Promise<void> { let request = new UpdateEventSubscriber(); request.id = this.getSubscriptionId(); request.subscribeChannels = channels; return this.serviceClient.post(request) .then(x => { this.update(channels, null); }).catch(this.onError); } unsubscribeFromChannels(...channels:string[]): Promise<void> { let request = new UpdateEventSubscriber(); request.id = this.getSubscriptionId(); request.unsubscribeChannels = channels; return this.serviceClient.post(request) .then(x => { this.update(null, channels); }).catch(this.onError); } getChannelSubscribers(): Promise<ServerEventUser[]> { let request = new GetEventSubscribers(); request.channels = this.channels; return this.serviceClient.get(request) .then(r => r.map(x => this.toServerEventUser(x))) .catch(e => { this.onError(e); return []; }); } toServerEventUser(map: { [id: string] : string; }): ServerEventUser { var channels = map["channels"]; var to = new ServerEventUser(); to.userId = map["userId"]; to.displayName = map["displayName"]; to.profileUrl = map["profileUrl"]; to.channels = channels ? channels.split(',') : null; for (var k in map) { if (k == "userId" || k == "displayName" || k == "profileUrl" || k == "channels") continue; if (to.meta == null) to.meta = {}; to.meta[k] = map[k]; } return to; } } export interface IReceiver { noSuchMethod(selector: string, message:any); } export class ServerEventReceiver implements IReceiver { public client: ServerEventsClient; public request: ServerEventMessage; noSuchMethod(selector: string, message:any) {} } export class UpdateEventSubscriber implements IReturn<UpdateEventSubscriberResponse> { id: string; subscribeChannels: string[]; unsubscribeChannels: string[]; createResponse() { return new UpdateEventSubscriberResponse(); } getTypeName() { return "UpdateEventSubscriber"; } } export class UpdateEventSubscriberResponse { responseStatus: ResponseStatus; } export class GetEventSubscribers implements IReturn<any[]> { channels: string[]; createResponse() { return []; } getTypeName() { return "GetEventSubscribers"; } } export class ServerEventUser { userId: string; displayName: string; profileUrl: string; channels: string[]; meta: { [index:string]: string; }; } export class HttpMethods { static Get = "GET"; static Post = "POST"; static Put = "PUT"; static Delete = "DELETE"; static Patch = "PATCH"; static Head = "HEAD"; static Options = "OPTIONS"; static hasRequestBody = (method: string) => !(method === "GET" || method === "DELETE" || method === "HEAD" || method === "OPTIONS"); } export interface IRequestFilterOptions { url:string } export interface Cookie { name: string; value: string; path: string; domain?: string; expires?: Date; httpOnly?: boolean; secure?: boolean; sameSite?: string; } class GetAccessToken implements IReturn<GetAccessTokenResponse> { refreshToken: string; createResponse() { return new GetAccessTokenResponse(); } getTypeName() { return "GetAccessToken"; } } export class GetAccessTokenResponse { accessToken: string; responseStatus: ResponseStatus; } export interface ISendRequest { method:string; request:any|null; body?:any|null; args?:any; url?:string; returns?: { createResponse: () => any }; } export class JsonServiceClient { baseUrl: string; replyBaseUrl: string; oneWayBaseUrl: string; mode: RequestMode; credentials: RequestCredentials; headers: Headers; userName: string; password: string; bearerToken: string; refreshToken: string; refreshTokenUri: string; requestFilter: (req:Request, options?:IRequestFilterOptions) => void; responseFilter: (res:Response) => void; exceptionFilter: (res:Response, error:any) => void; onAuthenticationRequired: () => Promise<any>; manageCookies: boolean; cookies:{ [index:string]: Cookie }; static toBase64: (rawString:string) => string; constructor(baseUrl: string) { if (baseUrl == null) throw "baseUrl is required"; this.baseUrl = baseUrl; this.replyBaseUrl = combinePaths(baseUrl, "json", "reply") + "/"; this.oneWayBaseUrl = combinePaths(baseUrl, "json", "oneway") + "/"; this.mode = "cors"; this.credentials = 'include'; this.headers = new Headers(); this.headers.set("Content-Type", "application/json"); this.manageCookies = typeof document == "undefined"; //because node-fetch doesn't this.cookies = {}; } setCredentials(userName:string, password:string): void { this.userName = userName; this.password = password; } // @deprecated use bearerToken property setBearerToken(token:string): void { this.bearerToken = token; } get<T>(request: IReturn<T>|string, args?:any): Promise<T> { return typeof request != "string" ? this.send<T>(HttpMethods.Get, request, args) : this.send<T>(HttpMethods.Get, null, args, this.toAbsoluteUrl(request)); } delete<T>(request: IReturn<T>|string, args?:any): Promise<T> { return typeof request != "string" ? this.send<T>(HttpMethods.Delete, request, args) : this.send<T>(HttpMethods.Delete, null, args, this.toAbsoluteUrl(request)); } post<T>(request: IReturn<T>, args?:any): Promise<T> { return this.send<T>(HttpMethods.Post, request, args); } postToUrl<T>(url:string, request:IReturn<T>, args?:any): Promise<T> { return this.send<T>(HttpMethods.Post, request, args, this.toAbsoluteUrl(url)); } postBody<T>(request:IReturn<T>, body:string|any, args?:any) { return this.sendBody<T>(HttpMethods.Post, request, body, args); } put<T>(request: IReturn<T>, args?:any): Promise<T> { return this.send<T>(HttpMethods.Put, request, args); } putToUrl<T>(url:string, request:IReturn<T>, args?:any): Promise<T> { return this.send<T>(HttpMethods.Put, request, args, this.toAbsoluteUrl(url)); } putBody<T>(request:IReturn<T>, body:string|any, args?:any) { return this.sendBody<T>(HttpMethods.Post, request, body, args); } patch<T>(request: IReturn<T>, args?:any): Promise<T> { return this.send<T>(HttpMethods.Patch, request, args); } patchToUrl<T>(url:string, request:IReturn<T>, args?:any): Promise<T> { return this.send<T>(HttpMethods.Patch, request, args, this.toAbsoluteUrl(url)); } patchBody<T>(request:IReturn<T>, body:string|any, args?:any) { return this.sendBody<T>(HttpMethods.Post, request, body, args); } createUrlFromDto<T>(method:string, request: IReturn<T>) : string { let url = combinePaths(this.replyBaseUrl, nameOf(request)); const hasRequestBody = HttpMethods.hasRequestBody(method); if (!hasRequestBody) url = appendQueryString(url, request); return url; } toAbsoluteUrl(relativeOrAbsoluteUrl:string) : string { return relativeOrAbsoluteUrl.startsWith("http://") || relativeOrAbsoluteUrl.startsWith("https://") ? relativeOrAbsoluteUrl : combinePaths(this.baseUrl, relativeOrAbsoluteUrl); } private createRequest({ method, request, url, args, body } : ISendRequest) : [Request,IRequestFilterOptions] { if (!url) url = this.createUrlFromDto(method, request); if (args) url = appendQueryString(url, args); if (this.bearerToken != null) { this.headers.set("Authorization", "Bearer " + this.bearerToken); } else if (this.userName != null) { this.headers.set('Authorization', 'Basic '+ JsonServiceClient.toBase64(`${this.userName}:${this.password}`)); } if (this.manageCookies) { var cookies = Object.keys(this.cookies) .map(x => { var c = this.cookies[x]; return c.expires && c.expires < new Date() ? null : `${c.name}=${encodeURIComponent(c.value)}` }) .filter(x => !!x); if (cookies.length > 0) this.headers.set("Cookie", cookies.join("; ")); else this.headers.delete("Cookie"); } // Set `compress` false due to common error // https://github.com/bitinn/node-fetch/issues/93#issuecomment-200791658 var reqOptions = { method: method, mode: this.mode, credentials: this.credentials, headers: this.headers, compress: false }; const req = new Request(url, reqOptions); if (HttpMethods.hasRequestBody(method)) (req as any).body = body || JSON.stringify(request); var opt:IRequestFilterOptions = { url }; if (this.requestFilter != null) this.requestFilter(req, opt); return [req, opt]; } private createResponse<T>(res:Response, request:any|null) { if (!res.ok) throw res; if (this.manageCookies) { var setCookies = []; res.headers.forEach((v,k) => { if ("set-cookie" == k.toLowerCase()) setCookies.push(v); }); setCookies.forEach(x => { var cookie = parseCookie(x); if (cookie) this.cookies[cookie.name] = cookie; }); } if (this.responseFilter != null) this.responseFilter(res); var x = request && typeof request != "string" && typeof request.createResponse == 'function' ? request.createResponse() : null; if (typeof x === 'string') return res.text().then(o => o as Object as T); var contentType = res.headers.get("content-type"); var isJson = contentType && contentType.indexOf("application/json") !== -1; if (isJson) { return res.json().then(o => o as Object as T); } if (typeof Uint8Array != "undefined" && x instanceof Uint8Array) { if (typeof res.arrayBuffer != 'function') throw new Error("This fetch polyfill does not implement 'arrayBuffer'"); return res.arrayBuffer().then(o => new Uint8Array(o) as Object as T); } else if (typeof Blob == "function" && x instanceof Blob) { if (typeof res.blob != 'function') throw new Error("This fetch polyfill does not implement 'blob'"); return res.blob().then(o => o as Object as T); } let contentLength = res.headers.get("content-length"); if (contentLength === "0" || (contentLength == null && !isJson)) { return x; } return res.json().then(o => o as Object as T); //fallback } private handleError(holdRes:Response, res, type:ErrorResponseType=null) { if (res instanceof Error) throw this.raiseError(holdRes, res); // res.json can only be called once. if (res.bodyUsed) throw this.raiseError(res, createErrorResponse(res.status, res.statusText, type)); let isErrorResponse = typeof res.json == "undefined" && res.responseStatus; if (isErrorResponse) { return new Promise((resolve,reject) => reject(this.raiseError(null, res)) ); } return res.json().then(o => { var errorDto = sanitize(o); if (!errorDto.responseStatus) throw createErrorResponse(res.status, res.statusText, type); if (type != null) errorDto.type = type; throw errorDto; }).catch(error => { // No responseStatus body, set from `res` Body object if (error instanceof Error || (typeof window != "undefined" && error instanceof (window as any).DOMException /*MS Edge*/)) { throw this.raiseError(res, createErrorResponse(res.status, res.statusText, type)); } throw this.raiseError(res, error); }); } send<T>(method:string, request:any|null, args?:any, url?:string): Promise<T> { return this.sendRequest<T>({ method, request, args, url }); } private sendBody<T>(method:string, request:IReturn<T>, body:string|any, args?:any) { let url = combinePaths(this.replyBaseUrl, nameOf(request)); return this.sendRequest<T>({ method, request: body, body: typeof body == "string" ? body : JSON.stringify(body), url: appendQueryString(url, request), args, returns: request }); } sendRequest<T>(info:ISendRequest): Promise<T> { const [req, opt] = this.createRequest(info); const returns = info.returns || info.request; let holdRes:Response = null; const resendRequest = () => { const [req, opt] = this.createRequest(info); return fetch(opt.url || req.url, req) .then(res => this.createResponse(res, returns)) .catch(res => this.handleError(holdRes, res)); } return fetch(opt.url || req.url, req) .then(res => { holdRes = res; const response = this.createResponse(res, returns); return response; }) .catch(res => { if (res.status === 401) { if (this.refreshToken) { const jwtReq = new GetAccessToken(); jwtReq.refreshToken = this.refreshToken; let url = this.refreshTokenUri || this.createUrlFromDto(HttpMethods.Post, jwtReq); let [jwtRequest, jwtOpt] = this.createRequest({ method:HttpMethods.Post, request:jwtReq, args:null, url }); return fetch(url, jwtRequest) .then(r => this.createResponse(r, jwtReq).then(jwtResponse => { this.bearerToken = jwtResponse.accessToken; return resendRequest(); })) .catch(res => { return this.handleError(holdRes, res, "RefreshTokenException") }); } if (this.onAuthenticationRequired) { return this.onAuthenticationRequired().then(resendRequest); } } return this.handleError(holdRes, res); }); } raiseError(res:Response, error:any) : any { if (this.exceptionFilter != null) { this.exceptionFilter(res, error); } return error; } } const createErrorResponse = (errorCode: string|number, message: string, type:ErrorResponseType=null) => { const error = new ErrorResponse(); if (type != null) error.type = type; error.responseStatus = new ResponseStatus(); error.responseStatus.errorCode = errorCode && errorCode.toString(); error.responseStatus.message = message; return error; }; export const toCamelCase = (key: string) => { return !key ? key : key.charAt(0).toLowerCase() + key.substring(1); }; export const sanitize = (status: any): any => { if (status.responseStatus) return status; if (status.errors) return status; var to: any = {}; for (let k in status) { if (status.hasOwnProperty(k)) { if (status[k] instanceof Object) to[toCamelCase(k)] = sanitize(status[k]); else to[toCamelCase(k)] = status[k]; } } to.errors = []; if (status.Errors != null) { for (var i=0, len = status.Errors.length; i<len; i++) { var o = status.Errors[i]; var err = {}; for (var k in o) err[toCamelCase(k)] = o[k]; to.errors.push(err); } } return to; } export const nameOf = (o: any) => { if (!o) return "null"; if (typeof o.getTypeName == "function") return o.getTypeName(); var ctor = o && o.constructor; if (ctor == null) throw `${o} doesn't have constructor`; if (ctor.name) return ctor.name; var str = ctor.toString(); return str.substring(9, str.indexOf("(")); //"function ".length == 9 }; /* utils */ function log<T>(o:T, prefix:string="LOG") { console.log(prefix, o); return o; } export const css = (selector: string | NodeListOf<Element>, name: string, value: string) => { const els = typeof selector == "string" ? document.querySelectorAll(selector as string) : selector as NodeListOf<Element>; for (let i = 0; i < els.length; i++) { const el = els[i] as any; if (el != null && el.style != null) { el.style[name] = value; } } } export const splitOnFirst = (s: string, c: string): string[] => { if (!s) return [s]; var pos = s.indexOf(c); return pos >= 0 ? [s.substring(0, pos), s.substring(pos + 1)] : [s]; }; export const splitOnLast = (s: string, c: string): string[] => { if (!s) return [s]; var pos = s.lastIndexOf(c); return pos >= 0 ? [s.substring(0, pos), s.substring(pos + 1)] : [s]; }; const splitCase = (t: string) => typeof t != 'string' ? t : t.replace(/([A-Z]|[0-9]+)/g, ' $1').replace(/_/g, ' ').trim(); export const humanize = s => (!s || s.indexOf(' ') >= 0 ? s : splitCase(s)); export const queryString = (url: string): any => { if (!url || url.indexOf('?') === -1) return {}; var pairs = splitOnFirst(url, '?')[1].split('&'); var map = {}; for (var i = 0; i < pairs.length; ++i) { var p = pairs[i].split('='); map[p[0]] = p.length > 1 ? decodeURIComponent(p[1].replace(/\+/g, ' ')) : null; } return map; }; export const combinePaths = (...paths: string[]): string => { var parts = [], i, l; for (i = 0, l = paths.length; i < l; i++) { var arg = paths[i]; parts = arg.indexOf("://") === -1 ? parts.concat(arg.split("/")) : parts.concat(arg.lastIndexOf("/") === arg.length - 1 ? arg.substring(0, arg.length - 1) : arg); } var combinedPaths = []; for (i = 0, l = parts.length; i < l; i++) { var part = parts[i]; if (!part || part === ".") continue; if (part === "..") combinedPaths.pop(); else combinedPaths.push(part); } if (parts[0] === "") combinedPaths.unshift(""); return combinedPaths.join("/") || (combinedPaths.length ? "/" : "."); }; export const createPath = (route: string, args: any) => { var argKeys = {}; for (let k in args) { argKeys[k.toLowerCase()] = k; } var parts = route.split("/"); var url = ""; for (let i = 0; i < parts.length; i++) { var p = parts[i]; if (p == null) p = ""; if (p[0] === "{" && p[p.length - 1] === "}") { const key = argKeys[p.substring(1, p.length - 1).toLowerCase()]; if (key) { p = args[key]; delete args[key]; } } if (url.length > 0) url += "/"; url += p; } return url; }; export const createUrl = (route: string, args: any) => { var url = createPath(route, args); return appendQueryString(url, args); }; export const appendQueryString = (url: string, args: any): string => { for (let k in args) { if (args.hasOwnProperty(k)) { url += url.indexOf("?") >= 0 ? "&" : "?"; url += k + "=" + qsValue(args[k]); } } return url; }; const qsValue = (arg: any) => { if (arg == null) return ""; if (typeof Uint8Array != "undefined" && arg instanceof Uint8Array) return bytesToBase64(arg as Uint8Array); return encodeURIComponent(arg) || ""; } //from: https://github.com/madmurphy/stringview.js/blob/master/stringview.js export const bytesToBase64 = (aBytes: Uint8Array): string => { var eqLen = (3 - (aBytes.length % 3)) % 3, sB64Enc = ""; for (var nMod3, nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) { nMod3 = nIdx % 3; nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24); if (nMod3 === 2 || aBytes.length - nIdx === 1) { sB64Enc += String.fromCharCode(uint6ToB64(nUint24 >>> 18 & 63), uint6ToB64(nUint24 >>> 12 & 63), uint6ToB64(nUint24 >>> 6 & 63), uint6ToB64(nUint24 & 63)); nUint24 = 0; } } return eqLen === 0 ? sB64Enc : sB64Enc.substring(0, sB64Enc.length - eqLen) + (eqLen === 1 ? "=" : "=="); } const uint6ToB64 = (nUint6: number) : number => nUint6 < 26 ? nUint6 + 65 : nUint6 < 52 ? nUint6 + 71 : nUint6 < 62 ? nUint6 - 4 : nUint6 === 62 ? 43 : nUint6 === 63 ? 47 : 65; //JsonServiceClient.toBase64 requires IE10+ or node interface NodeBuffer extends Uint8Array { toString(encoding?: string, start?: number, end?: number): string; } interface Buffer extends NodeBuffer { } declare var Buffer: { new (str: string, encoding?: string): Buffer; } var _btoa = typeof btoa == 'function' ? btoa : (str) => new Buffer(str).toString('base64'); //from: http://stackoverflow.com/a/30106551/85785 JsonServiceClient.toBase64 = (str:string) => _btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match:any, p1:string) => String.fromCharCode(new Number('0x' + p1).valueOf()) )); export const stripQuotes = (s:string) => s && s[0] == '"' && s[s.length] == '"' ? s.slice(1,-1) : s; export const tryDecode = (s:string) => { try { return decodeURIComponent(s); } catch(e) { return s; } }; export const parseCookie = (setCookie:string): Cookie => { if (!setCookie) return null; var to:Cookie = null; var pairs = setCookie.split(/; */); for (var i=0; i<pairs.length; i++) { var pair = pairs[i]; var parts = splitOnFirst(pair, '='); var name = parts[0].trim(); var value = parts.length > 1 ? tryDecode(stripQuotes(parts[1].trim())) : null; if (i == 0) { to = { name, value, path: "/" }; } else { var lower = name.toLowerCase(); if (lower == "httponly") { to.httpOnly = true; } else if (lower == "secure") { to.secure = true; } else if (lower == "expires") { to.expires = new Date(value); // MS Edge returns Invalid Date when using '-' in "12-Mar-2037" if (to.expires.toString() === "Invalid Date") { to.expires = new Date(value.replace(/-/g, " ")); } } else { to[name] = value; } } } return to; } export const normalizeKey = (key: string) => key.toLowerCase().replace(/_/g, ''); const isArray = (o: any) => Object.prototype.toString.call(o) === '[object Array]'; export const normalize = (dto: any, deep?: boolean) => { if (isArray(dto)) { if (!deep) return dto; const to = []; for (let i = 0; i < dto.length; i++) { to[i] = normalize(dto[i], deep); } return to; } if (typeof dto != "object") return dto; var o = {}; for (let k in dto) { o[normalizeKey(k)] = deep ? normalize(dto[k], deep) : dto[k]; } return o; } export const getField = (o: any, name: string) => o == null || name == null ? null : o[name] || o[Object.keys(o).filter(k => normalizeKey(k) === normalizeKey(name))[0] || '']; export const parseResponseStatus = (json:string, defaultMsg=null) => { try { var err = JSON.parse(json); return sanitize(err.ResponseStatus || err.responseStatus); } catch (e) { return { message: defaultMsg || e.message || e, __error: { error: e, json: json } }; } }; export const toDate = (s: string) => new Date(parseFloat(/Date\(([^)]+)\)/.exec(s)[1])); export const toDateFmt = (s: string) => dateFmt(toDate(s)); export const padInt = (n: number) => n < 10 ? '0' + n : n; export const dateFmt = (d: Date = new Date()) => d.getFullYear() + '/' + padInt(d.getMonth() + 1) + '/' + padInt(d.getDate()); export const dateFmtHM = (d: Date = new Date()) => d.getFullYear() + '/' + padInt(d.getMonth() + 1) + '/' + padInt(d.getDate()) + ' ' + padInt(d.getHours()) + ":" + padInt(d.getMinutes()); export const timeFmt12 = (d: Date = new Date()) => padInt((d.getHours() + 24) % 12 || 12) + ":" + padInt(d.getMinutes()) + ":" + padInt(d.getSeconds()) + " " + (d.getHours() > 12 ? "PM" : "AM");