@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,493 lines • 100 kB
JavaScript
export class ResponseStatus {
constructor(init) { Object.assign(this, init); }
errorCode;
message;
stackTrace;
errors;
meta;
}
export class ResponseError {
constructor(init) { Object.assign(this, init); }
errorCode;
fieldName;
message;
meta;
}
export class ErrorResponse {
constructor(init) { Object.assign(this, init); }
type;
responseStatus;
}
export class EmptyResponse {
constructor(init) { Object.assign(this, init); }
responseStatus;
}
export class NavItem {
label;
href;
exact;
id;
className;
iconClass;
show;
hide;
children;
meta;
constructor(init) { Object.assign(this, init); }
}
export class GetNavItems {
constructor(init) { Object.assign(this, init); }
createResponse() { return new GetNavItemsResponse(); }
getTypeName() { return 'GetNavItems'; }
getMethod() { return 'GET'; }
}
export class GetNavItemsResponse {
baseUrl;
results;
navItemsMap;
meta;
responseStatus;
constructor(init) { Object.assign(this, init); }
}
export class MetadataTypesConfig {
baseUrl;
defaultNamespaces;
defaultImports;
includeTypes;
excludeTypes;
treatTypesAsStrings;
globalNamespace;
ignoreTypes;
exportTypes;
exportAttributes;
ignoreTypesInNamespaces;
constructor(init) { Object.assign(this, init); }
}
export class MetadataRoute {
path;
verbs;
notes;
summary;
constructor(init) { Object.assign(this, init); }
}
export class MetadataOperationType {
request;
response;
actions;
returnsVoid;
returnType;
routes;
dataModel;
viewModel;
requiresAuth;
requiredRoles;
requiresAnyRole;
requiredPermissions;
requiresAnyPermission;
tags;
constructor(init) { Object.assign(this, init); }
}
export class MetadataTypes {
config;
namespaces;
types;
operations;
constructor(init) { Object.assign(this, init); }
}
export class MetadataTypeName {
name;
namespace;
genericArgs;
constructor(init) { Object.assign(this, init); }
}
export class MetadataDataContract {
name;
namespace;
constructor(init) { Object.assign(this, init); }
}
export class MetadataDataMember {
name;
order;
isRequired;
emitDefaultValue;
constructor(init) { Object.assign(this, init); }
}
export class MetadataAttribute {
name;
constructorArgs;
args;
constructor(init) { Object.assign(this, init); }
}
export class MetadataPropertyType {
name;
type;
isValueType;
isSystemType;
isEnum;
isPrimaryKey;
typeNamespace;
genericArgs;
value;
description;
dataMember;
readOnly;
paramType;
displayType;
isRequired;
allowableValues;
allowableMin;
allowableMax;
attributes;
uploadTo;
input;
format;
ref;
constructor(init) { Object.assign(this, init); }
}
export class MetadataType {
name;
namespace;
genericArgs;
inherits;
implements;
displayType;
description;
notes;
icon;
isNested;
isEnum;
isEnumInt;
isInterface;
isAbstract;
dataContract;
properties;
attributes;
innerTypes;
enumNames;
enumValues;
enumMemberValues;
enumDescriptions;
meta;
constructor(init) { Object.assign(this, init); }
}
export class ImageInfo {
svg;
uri;
alt;
cls;
}
export class InputInfo {
id;
name;
type;
value;
placeholder;
help;
label;
title;
size;
pattern;
readOnly;
required;
disabled;
autocomplete;
autofocus;
min;
max;
step;
minLength;
maxLength;
accept;
capture;
multiple;
allowableValues;
allowableEntries;
options;
ignore;
css;
meta;
}
export class FormatInfo {
method;
options;
locale;
}
export class RefInfo {
model;
selfId;
refId;
refLabel;
}
export class KeyValuePair {
key;
value;
}
export class FieldCss {
field;
input;
label;
}
export class NewInstanceResolver {
tryResolve(ctor) {
return new ctor();
}
}
export class SingletonInstanceResolver {
tryResolve(ctor) {
return ctor.instance
|| (ctor.instance = new ctor());
}
}
function eventMessageType(evt) {
switch (evt) {
case 'onConnect':
return 'ServerEventConnect';
case 'onHeartbeat':
return 'ServerEventHeartbeat';
case 'onJoin':
return 'ServerEventJoin';
case 'onLeave':
return 'ServerEventLeave';
case 'onUpdate':
return 'ServerEventUpdate';
}
return null;
}
/**
* EventSource
*/
export var ReadyState;
(function (ReadyState) {
ReadyState[ReadyState["CONNECTING"] = 0] = "CONNECTING";
ReadyState[ReadyState["OPEN"] = 1] = "OPEN";
ReadyState[ReadyState["CLOSED"] = 2] = "CLOSED";
})(ReadyState || (ReadyState = {}));
export class ServerEventsClient {
channels;
options;
eventSource;
static UnknownChannel = "*";
eventStreamUri;
updateSubscriberUrl;
connectionInfo;
serviceClient;
stopped;
resolver;
listeners;
EventSource;
withCredentials;
constructor(baseUrl, channels, options = {}, eventSource = null) {
this.channels = channels;
this.options = options;
this.eventSource = eventSource;
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) => {
if (typeof document == "undefined") { //node
//latest node-fetch + eventsource doesn't split SSE messages properly
let requireSplitPos = e.data ? e.data.indexOf('\n') : -1;
if (requireSplitPos >= 0) {
let data = e.data;
let lastEventId = e.lastEventId;
let e1 = Object.assign({}, { lastEventId, data: data.substring(0, requireSplitPos) }), e2 = Object.assign({}, { lastEventId, data: data.substring(requireSplitPos + 1) });
this._onMessage(e1);
this._onMessage(e2);
return;
}
}
this._onMessage(e);
};
_onMessage = (e) => {
if (this.stopped)
return;
let opt = this.options;
if (typeof document == "undefined") {
var document = {
querySelectorAll: sel => []
};
}
let parts = splitOnFirst(e.data, " ");
let channel = null;
let selector = parts[0];
let selParts = splitOnFirst(selector, "@");
if (selParts.length > 1) {
channel = selParts[0];
selector = selParts[1];
}
const json = parts[1];
let body = null;
try {
body = json ? JSON.parse(json) : null;
}
catch (ignore) { }
parts = splitOnFirst(selector, ".");
if (parts.length <= 1)
throw "invalid selector format: " + selector;
let op = parts[0], target = parts[1].replace(new RegExp("%20", "g"), " ");
const tokens = splitOnFirst(target, "$");
const [cmd, cssSelector] = tokens;
const els = cssSelector && $$(cssSelector);
const el = els && els[0];
const eventId = parseInt(e.lastEventId);
const data = e.data;
const type = eventMessageType(cmd) || "ServerEventMessage";
const request = { 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;
let 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);
let 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(async () => {
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;
}
const reqHeartbeat = new Request(opt.heartbeatUrl, {
method: "POST", mode: "cors", headers: headers, credentials: this.serviceClient.credentials
});
try {
let res = await fetch(reqHeartbeat);
if (!res.ok) {
const error = new Error(`${res.status} - ${res.statusText}`);
this.reconnectServerEvents({ error });
}
else {
await res.text();
}
}
catch (error) {
this.reconnectServerEvents({ error });
}
}, (this.connectionInfo && this.connectionInfo.heartbeatIntervalMs) || opt.heartbeatIntervalMs || 10000);
}
if (opt.unRegisterUrl) {
if (typeof window != "undefined") {
window.onunload = () => {
if (navigator.sendBeacon) { // Chrome https://developers.google.com/web/updates/2019/12/chrome-80-deps-rems
this.stopped = true;
if (this.eventSource)
this.eventSource.close();
navigator.sendBeacon(opt.unRegisterUrl);
}
else {
this.stop();
}
};
}
}
this.updateSubscriberUrl = opt.updateSubscriberUrl;
this.updateChannels((opt.channels || "").split(","));
}
else {
let isCmdMsg = cmd == "onJoin" || cmd == "onLeave" || cmd == "onUpdate";
let 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
let 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 || $$("body"), cmd, body);
}
//Named Receiver
let r = opt.receivers && opt.receivers[op];
this.invokeReceiver(r, cmd, el, request, op);
if (!eventMessageType(cmd)) {
let fn = opt.handlers["onMessage"];
if (fn) {
fn.call(el || document.body, mergedBody);
}
}
if (opt.onTick)
opt.onTick();
};
onError = (error) => {
if (this.stopped)
return;
if (!error)
error = event;
let 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 = {}) {
if (this.stopped)
return;
if (opt.error)
this.onError(opt.error);
const hold = this.eventSource;
let 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)(e));
es.addEventListener('message', opt.onmessage || hold.onmessage || this.onMessage);
let 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);
}
hold.close();
return this.eventSource = es;
}
start() {
this.stopped = false;
if (this.eventSource == null || this.eventSource.readyState === EventSource.CLOSED) {
let 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));
}
return this;
}
stop() {
this.stopped = true;
if (this.eventSource) {
this.eventSource.close();
}
let opt = this.options;
if (opt && opt.heartbeat) {
clearInterval(opt.heartbeat);
}
let hold = this.connectionInfo;
if (hold == null || hold.unRegisterUrl == null)
return new Promise((resolve, reject) => resolve());
this.connectionInfo = null;
return fetch(new Request(hold.unRegisterUrl, { method: "POST", mode: "cors", credentials: this.serviceClient.credentials }))
.then(res => { if (!res.ok)
throw new Error(`${res.status} - ${res.statusText}`); })
.catch(this.onError);
}
invokeReceiver(r, cmd, el, request, name) {
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 {
let metaProp = Object.getOwnPropertyDescriptor(r, cmd);
if (metaProp != null) {
if (metaProp.set) {
metaProp.set(request.body);
}
else if (metaProp.writable) {
r[cmd] = request.body;
}
return;
}
let cmdLower = cmd.toLowerCase();
getAllMembers(r).forEach(k => {
if (k.toLowerCase() == cmdLower) {
if (typeof r[k] == "function") {
r[k].call(el || r, request.body, request);
}
else {
r[k] = request.body;
}
return;
}
});
let noSuchMethod = r["noSuchMethod"];
if (typeof noSuchMethod == "function") {
noSuchMethod.call(el || r, request.target, request);
}
}
}
}
hasConnected() {
return this.connectionInfo != null;
}
registerHandler(name, fn) {
if (!this.options.handlers)
this.options.handlers = {};
this.options.handlers[name] = fn;
return this;
}
setResolver(resolver) {
this.options.resolver = resolver;
return this;
}
registerReceiver(receiver) {
return this.registerNamedReceiver("cmd", receiver);
}
registerNamedReceiver(name, receiver) {
if (!this.options.receivers)
this.options.receivers = {};
this.options.receivers[name] = receiver;
return this;
}
unregisterReceiver(name = "cmd") {
if (this.options.receivers) {
delete this.options.receivers[name];
}
return this;
}
updateChannels(channels) {
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, unsubscribe) {
let sub = typeof subscribe == "string" ? subscribe.split(',') : subscribe;
let unsub = typeof unsubscribe == "string" ? unsubscribe.split(',') : unsubscribe;
let channels = [];
for (let i in this.channels) {
let c = this.channels[i];
if (unsub == null || unsub.indexOf(c) === -1) {
channels.push(c);
}
}
if (sub) {
for (let i in sub) {
let c = sub[i];
if (channels.indexOf(c) === -1) {
channels.push(c);
}
}
}
this.updateChannels(channels);
}
addListener(eventName, handler) {
let handlers = this.listeners[eventName] || (this.listeners[eventName] = []);
handlers.push(handler);
return this;
}
removeListener(eventName, handler) {
let handlers = this.listeners[eventName];
if (handlers) {
let pos = handlers.indexOf(handler);
if (pos >= 0) {
handlers.splice(pos, 1);
}
}
return this;
}
raiseEvent(eventName, msg) {
let 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) {
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) {
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) {
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() {
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) {
let channels = map["channels"];
let to = new ServerEventUser();
to.userId = map["userId"];
to.displayName = map["displayName"];
to.profileUrl = map["profileUrl"];
to.channels = channels ? channels.split(',') : null;
for (let 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 function getAllMembers(o) {
let props = [];
do {
const l = Object.getOwnPropertyNames(o)
.concat(Object.getOwnPropertySymbols(o).map(s => s.toString()))
.sort()
.filter((p, i, arr) => p !== 'constructor' && //not the constructor
(i == 0 || p !== arr[i - 1]) && //not overriding in this prototype
props.indexOf(p) === -1 //not overridden in a child
);
props = props.concat(l);
} while ((o = Object.getPrototypeOf(o)) && //walk-up the prototype chain
Object.getPrototypeOf(o) //not the the Object prototype methods (hasOwnProperty, etc...)
);
return props;
}
export class ServerEventReceiver {
client;
request;
noSuchMethod(selector, message) { }
}
export class UpdateEventSubscriber {
id;
subscribeChannels;
unsubscribeChannels;
createResponse() { return new UpdateEventSubscriberResponse(); }
getTypeName() { return "UpdateEventSubscriber"; }
}
export class UpdateEventSubscriberResponse {
responseStatus;
}
export class GetEventSubscribers {
channels;
createResponse() { return []; }
getTypeName() { return "GetEventSubscribers"; }
}
export class ServerEventUser {
userId;
displayName;
profileUrl;
channels;
meta;
}
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) => !(method === "GET" || method === "DELETE" || method === "HEAD" || method === "OPTIONS");
}
class GetAccessToken {
constructor(init) { Object.assign(this, init); }
refreshToken;
useTokenCookie;
createResponse() { return new GetAccessTokenResponse(); }
getTypeName() { return 'GetAccessToken'; }
getMethod() { return 'POST'; }
}
export class GetAccessTokenResponse {
accessToken;
responseStatus;
}
export class JsonServiceClient {
baseUrl;
replyBaseUrl;
oneWayBaseUrl;
mode;
credentials;
headers;
userName;
password;
bearerToken;
refreshToken;
refreshTokenUri;
useTokenCookie;
enableAutoRefreshToken;
requestFilter;
static globalRequestFilter;
responseFilter;
static globalResponseFilter;
exceptionFilter;
urlFilter;
onAuthenticationRequired;
manageCookies;
cookies;
parseJson;
static toBase64;
constructor(baseUrl = "/") {
this.baseUrl = baseUrl;
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 = {};
this.enableAutoRefreshToken = true;
this.basePath = 'api';
}
setCredentials(userName, password) {
this.userName = userName;
this.password = password;
}
useBasePath(path) {
this.basePath = path;
return this;
}
set basePath(path) {
if (!path) {
this.replyBaseUrl = combinePaths(this.baseUrl, "json", "reply") + "/";
this.oneWayBaseUrl = combinePaths(this.baseUrl, "json", "oneway") + "/";
}
else {
this.replyBaseUrl = combinePaths(this.baseUrl, path) + "/";
this.oneWayBaseUrl = combinePaths(this.baseUrl, path) + "/";
}
}
apply(f) {
f(this);
return this;
}
get(request, args) {
return typeof request != "string"
? this.fetch(HttpMethods.Get, request, args)
: this.fetch(HttpMethods.Get, null, args, this.toAbsoluteUrl(request));
}
delete(request, args) {
return typeof request != "string"
? this.fetch(HttpMethods.Delete, request, args)
: this.fetch(HttpMethods.Delete, null, args, this.toAbsoluteUrl(request));
}
post(request, args) {
return this.fetch(HttpMethods.Post, request, args);
}
postToUrl(url, request, args) {
return this.fetch(HttpMethods.Post, request, args, this.toAbsoluteUrl(url));
}
postBody(request, body, args) {
return this.fetchBody(HttpMethods.Post, request, body, args);
}
put(request, args) {
return this.fetch(HttpMethods.Put, request, args);
}
putToUrl(url, request, args) {
return this.fetch(HttpMethods.Put, request, args, this.toAbsoluteUrl(url));
}
putBody(request, body, args) {
return this.fetchBody(HttpMethods.Put, request, body, args);
}
patch(request, args) {
return this.fetch(HttpMethods.Patch, request, args);
}
patchToUrl(url, request, args) {
return this.fetch(HttpMethods.Patch, request, args, this.toAbsoluteUrl(url));
}
patchBody(request, body, args) {
return this.fetchBody(HttpMethods.Patch, request, body, args);
}
publish(request, args) {
return this.sendOneWay(request, args);
}
sendOneWay(request, args) {
const url = combinePaths(this.oneWayBaseUrl, nameOf(request));
return this.fetch(HttpMethods.Post, request, null, url);
}
sendAll(requests) {
if (requests.length == 0)
return Promise.resolve([]);
const url = combinePaths(this.replyBaseUrl, nameOf(requests[0]) + "[]");
return this.fetch(HttpMethods.Post, requests, null, url);
}
sendAllOneWay(requests) {
if (requests.length == 0)
return Promise.resolve(void 0);
const url = combinePaths(this.oneWayBaseUrl, nameOf(requests[0]) + "[]");
return this.fetch(HttpMethods.Post, requests, null, url)
.then(r => void 0);
}
createUrlFromDto(method, request) {
let url = combinePaths(this.replyBaseUrl, nameOf(request));
const hasRequestBody = HttpMethods.hasRequestBody(method);
if (!hasRequestBody)
url = appendQueryString(url, request);
return url;
}
toAbsoluteUrl(relativeOrAbsoluteUrl) {
return relativeOrAbsoluteUrl.startsWith("http://") ||
relativeOrAbsoluteUrl.startsWith("https://")
? relativeOrAbsoluteUrl
: combinePaths(this.baseUrl, relativeOrAbsoluteUrl);
}
deleteCookie(name) {
if (this.manageCookies) {
delete this.cookies[name];
}
else {
if (document) {
document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/';
}
}
}
createRequest({ method, request, url, args, body }) {
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) {
let cookies = Object.keys(this.cookies)
.map(x => {
let 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");
}
let headers = new Headers(this.headers);
let hasRequestBody = HttpMethods.hasRequestBody(method);
let reqInit = {
url,
method: method,
mode: this.mode,
credentials: this.credentials,
headers,
compress: false, // https://github.com/bitinn/node-fetch/issues/93#issuecomment-200791658
};
if (hasRequestBody) {
reqInit.body = body || JSON.stringify(request);
if (isFormData(body)) {
reqInit.body = sanitizeFormData(body);
headers.delete('Content-Type'); //set by FormData
}
}
if (this.requestFilter != null)
this.requestFilter(reqInit);
if (JsonServiceClient.globalRequestFilter != null)
JsonServiceClient.globalRequestFilter(reqInit);
return reqInit;
}
json(res) {
if (this.parseJson)
return this.parseJson(res);
return res.text().then(txt => {
return txt.length > 0 ? JSON.parse(txt) : null;
});
}
applyResponseFilters(res) {
if (this.responseFilter != null)
this.responseFilter(res);
if (JsonServiceClient.globalResponseFilter != null)
JsonServiceClient.globalResponseFilter(res);
}
createResponse(res, request) {
if (!res.ok) {
this.applyResponseFilters(res);
throw res;
}
if (this.manageCookies) {
let setCookies = [];
res.headers.forEach((v, k) => {
switch (k.toLowerCase()) {
case "set-cookie":
let cookies = v.split(',');
cookies.forEach(c => setCookies.push(c));
break;
}
});
setCookies.forEach(x => {
let cookie = parseCookie(x);
if (cookie)
this.cookies[cookie.name] = cookie;
});
}
res.headers.forEach((v, k) => {
switch (k.toLowerCase()) {
case "x-cookies":
if (v.split(',').indexOf('ss-reftok') >= 0)
this.useTokenCookie = true;
break;
}
});
this.applyResponseFilters(res);
let x = request && typeof request != "string" && typeof request.createResponse == 'function'
? request.createResponse()
: null;
if (typeof x === 'string')
return res.text().then(o => o);
let contentType = res.headers.get("content-type");
let isJson = contentType && contentType.indexOf("application/json") !== -1;
if (isJson) {
return this.json(res).then(o => o);
}
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));
}
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);
}
let contentLength = res.headers.get("content-length");
if (contentLength === "0" || (contentLength == null && !isJson)) {
return res.text().then(_ => x);
}
return this.json(res).then(o => o); //fallback
}
handleError(holdRes, res, type = 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 this.json(res).then(o => {
let 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" && window.DOMException && error instanceof window.DOMException /*MS Edge*/)) {
throw this.raiseError(res, createErrorResponse(res.status, res.statusText, type));
}
throw this.raiseError(res, error);
});
}
fetch(method, request, args, url) {
return this.sendRequest({ method, request, args, url });
}
fetchBody(method, request, body, args) {
let url = combinePaths(this.replyBaseUrl, nameOf(request));
return this.sendRequest({
method,
request: body,
body: typeof body == "string"
? body
: isFormData(body)
? body
: JSON.stringify(body),
url: appendQueryString(url, request),
args,
returns: request
});
}
sendRequest(info) {
const req = this.createRequest(info);
const returns = info.returns || info.request;
let holdRes = null;
const resendRequest = () => {
const req = this.createRequest(info);
if (this.urlFilter)
this.urlFilter(req.url);
return fetch(req.url, req)
.then(res => this.createResponse(res, returns))
.catch(res => this.handleError(holdRes, res));
};
if (this.urlFilter)
this.urlFilter(req.url);
return fetch(req.url, req)
.then(res => {
holdRes = res;
const response = this.createResponse(res, returns);
return response;
})
.catch(res => {
if (res.status === 401) {
if (this.enableAutoRefreshToken && (this.refreshToken || this.useTokenCookie || this.cookies['ss-reftok'] != null)) {
const jwtReq = new GetAccessToken({ refreshToken: this.refreshToken, useTokenCookie: !!this.useTokenCookie });
let url = this.refreshTokenUri || this.createUrlFromDto(HttpMethods.Post, jwtReq);
if (this.useTokenCookie) {
this.bearerToken = null;
this.headers.delete("Authorization");
}
let jwtRequest = 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 || null;
return resendRequest();
}))
.catch(res => {
if (this.onAuthenticationRequired) {
return this.onAuthenticationRequired()
.then(resendRequest)
.catch(resHandler => {
return this.handleError(holdRes, resHandler, "RefreshTokenException");
});
}
else {
return this.handleError(holdRes, res, "RefreshTokenException");
}
});
}
else {
if (this.onAuthenticationRequired) {
return this.onAuthenticationRequired().then(resendRequest);
}
}
}
return this.handleError(holdRes, res);
});
}
raiseError(res, error) {
if (this.exceptionFilter != null) {
this.exceptionFilter(res, error);
}
return error;
}
// Generic send that uses APIs preferred HTTP Method (requires v5.13+ DTOs)
send(request, args, url) {
return this.sendRequest({ method: getMethod(request), request, args, url });
}
// Generic send IReturnVoid that uses APIs preferred HTTP Method (requires v5.13+ DTOs)
sendVoid(request, args, url) {
return this.sendRequest({ method: getMethod(request), request, args, url });
}
async api(request, args, method) {
try {
const result = await this.fetch(getMethod(request, method), request, args);
return new ApiResult({ response: result });
}
catch (e) {
return new ApiResult({ error: getResponseStatus(e) });
}
}
async apiVoid(request, args, method) {
try {
const result = await this.fetch(getMethod(request, method), request, args);
return new ApiResult({ response: result ?? new EmptyResponse() });
}
catch (e) {
return new ApiResult({ error: getResponseStatus(e) });
}
}
async apiForm(request, body, args, method) {
try {
const result = await this.fetchBody(getMethod(request, method), request, body, args);
return new ApiResult({ response: result });
}
catch (e) {
return new ApiResult({ error: getResponseStatus(e) });
}
}
async apiFormVoid(request, body, args, method) {
try {
const result = await this.fetchBody(getMethod(request, method), request, body, args);
return new ApiResult({ response: result ?? new EmptyResponse() });
}
catch (e) {
return new ApiResult({ error: getResponseStatus(e) });
}
}
}
export class JsonApiClient {
static create(baseUrl = "/", f) {
let client = new JsonServiceClient(baseUrl).apply(c => {
c.basePath = "/api";
c.headers = new Headers(); //avoid pre-flight CORS requests
c.enableAutoRefreshToken = false; // Use JWT Cookies by default
if (f) {
f(c);
}
});
return client;
}
}
export function getMethod(request, method) {
return method ?? (typeof request.getMethod == "function"
? request.getMethod()
: HttpMethods.Post);
}
export function getResponseStatus(e) {
return e.responseStatus ?? e.ResponseStatus ??
(e.errorCode
? e
: (e.message ? createErrorStatus(e.message, e.errorCode) : null));
}
export class ApiResult {
response;
error;
constructor(init) { Object.assign(this, init); }
get completed() { return this.response != null || this.error != null; }
get failed() { return this.error?.errorCode != null || this.error?.message != null; }
get succeeded() { return !this.failed && this.response != null; }
get errorMessage() { return this.error?.message; }
get errorCode() { return this.error?.errorCode; }
get errors() { return this.error?.errors ?? []; }
get errorSummary() { return this.error != null && this.errors.length == 0 ? this.errorMessage : null; }
fieldError(fieldName) {
let matchField = fieldName.toLowerCase();
return this.errors?.find(x => x.fieldName.toLowerCase() == matchField);
}
fieldErrorMessage(fieldName) { return this.fieldError(fieldName)?.message; }
hasFieldError(fieldName) { return this.fieldError(fieldName) != null; }
showSummary(exceptFields = []) {
if (!this.failed)
return false;
return exceptFields.every(x => !this.hasFieldError(x));
}
summaryMessage(exceptFields = []) {
if (this.showSummary(exceptFields)) {
// Return first field error that's not visible
let fieldSet = exceptFields.map(x => x.toLowerCase());
let fieldError = fieldSet.find(x => fieldSet.indexOf(x.toLowerCase()) == -1);
return fieldError ?? this.errorMessage;
}
}
addFieldError(fieldName, message, errorCode = 'Exception') {
if (!this.error)
this.error = new ResponseStatus();
const fieldError = this.fieldError(fieldName);
if (fieldError != null) {
fieldError.errorCode = errorCode;
fieldError.message = message;
}
else {
this.error.errors.push(new ResponseError({ fieldName, errorCode, message }));
}
}
}
export function createErrorStatus(message, errorCode = 'Exception') {
return new ResponseStatus({ errorCode, message });
}
export function createFieldError(fieldName, message, errorCode = 'Exception') {
return new ResponseStatus({ errors: [new ResponseError({ fieldName, errorCode, message })] });
}
export function isFormData(body) { return body instanceof FormData; }
function createErrorResponse(errorCode, message, type = null) {
const error = apply(new ErrorResponse(), e => {
if (type != null)
e.type = type;
e.responseStatus = apply(new ResponseStatus(), status => {
status.errorCode = errorCode && errorCode.toString();
status.message = message;
});
});
return error;
}
export function createError(errorCode, message, fieldName) {
return new ErrorResponse({
responseStatus: new ResponseStatus({
errorCode,
message,
errors: fieldName ? [new ResponseError({ errorCode, message, fieldName })] : undefined
})
});
}
export function toPascalCase(s) {
if (!s)
return '';
const isAllCaps = s.match(/^[A-Z0-9_]+$/);
if (isAllCaps) {
const words = s.split('_');
return words.map(x => x[0].toUpperCase() + x.substring(1).toLowerCase()).join('');
}
if (s.includes('_')) {
return s.split('_').filter(x => x[0]).map(x => x[0].toUpperCase() + x.substring(1)).join('');
}
return s.charAt(0).toUpperCase() + s.substring(1);
}
export function toCamelCase(s) {
s = toPascalCase(s);
if (!s)
return '';
return s.charAt(0).toLowerCase() + s.substring(1);
}
export function toKebabCase(s) {
if (!s || s.length <= 1)
return s.toLowerCase();
return s
.replace(/([A-Z0-9])/g, '-$1')
.toLowerCase()
.replace(/^-/, '')
.replace(/-+/g, '-');
}
export function map(o, f) { return o == null ? null : f(o); }
export function camelCaseAny(o) {
if (!o || !(o instanceof Object) || Array.isArray(o))
return o;
let to = {};
for (let k in o) {
if (o.hasOwnProperty(k)) {
const key = toCamelCase(k);
const val = o[k];
if (Array.isArray(val))
to[key] = val.map(x => camelCaseAny(x));
else if (val instanceof Object)
to[key] = camelCaseAny(val);
else
to[key] = val;
}
}
return to;
}
export function sanitize(status) {
if (!sanitize)
return sanitize;
if (status.responseStatus)
return status;
if (status.errors)
return status;
let to = camelCaseAny(status);
return to;
}
export function nameOf(o) {
if (!o)
return "null";
if (typeof o.getTypeName == "function")
return o.getTypeName();
let ctor = o && o.constructor;
if (ctor == null)
throw `${o} doesn't have constructor`;
if (ctor.name)
return ctor.name;
let str = ctor.toString();
return str.substring(9, str.indexOf("(")); //"function ".length == 9
}
/* utils */
function log(o, prefix = "LOG") {
console.log(prefix, o);
return o;
}
export function css(selector, name, value) {
const els = typeof selector == "string"
? document.querySelectorAll(selector)
: selector;
for (let i = 0; i < els.length; i++) {
const el = els[i];
if (el != null && el.style != null) {
el.style[name] = value;
}
}
}
export function splitOnFirst(s, c) {
if (!s)
return [s];
let pos = s.indexOf(c);
return pos >= 0 ? [s.substring(0, pos), s.substring(pos + 1)] : [s];
}
export function splitOnLast(s, c) {
if (!s)
return [s];
let pos = s.lastIndexOf(c);
return pos >= 0
? [s.substring(0, pos), s.substring(pos + 1)]
: [s];
}
export function leftPart(s, needle) {
if (s == null)
return null;
let pos = s.indexOf(needle);
return pos == -1
? s
: s.substring(0, pos);
}
export function rightPart(s, needle) {
if (s == null)
return null;
let pos = s.indexOf(needle);
return pos == -1
? s
: s.substring(pos + needle.length);
}
export function lastLeftPart(s, needle) {
if (s == null)
return null;
let pos = s.lastIndexOf(needle);
return pos == -1
? s
: s.substring(0, pos);
}
export function lastRightPart(s, needle) {
if (s == null)
return null;
let pos = s.lastIndexOf(needle);
return pos == -1
? s
: s.substring(pos + needle.length);
}
export function chop(str, len = 1) {
len = Math.abs(len);
return str ? len < str.length ? str.substring(0, str.length - len) : '' : str;
}
export function onlyProps(obj, keys) {
let to = {};
keys.forEach(key => to[key] = obj[key]);
return to;
}
function splitCase(t) {
return typeof t != 'string' ? t : t.replace(/([A-Z]|[0-9]+)/g, ' $1').replace(/_/g, ' ').trim();
}
export function humanize(s) { return (!s || s.indexOf(' ') >= 0 ? s : splitCase(toPascalCase(s))); }
export const ucFirst = (s) => s.charAt(0).toUpperCase() + s.substring(1);
export const isUpper = (c) => c >= 'A' && c <= 'Z';
export const isLower = (c) => c >= 'a' && c <= 'z';
export const isDigit = (c) => c >= '0' && c <= '9';
const upperOrDigit = (c) => isUpper(c) || isDigit(c);
export function splitTitleCase(s) {
let to = [];
if (typeof s != 'string')
return to;
let lastSplit = 0;
for (let i = 0; i < s.length; i++) {
let c = s[i];
let prev = i > 0 ? s[i - 1] : null;
let next = i + 1 < s.length ? s[i + 1] : null;
if (upperOrDigit(c) && (!upperOrDigit(prev) || !upperOrDigit(next))) {
to.push(s.substring(lastSplit, i));
lastSplit = i;
}
}
to.push(s.substring(lastSplit, s.length));
return to.filter(x => !!x);
}
export function humanify(s) {
return !s || indexOfAny(s, [' ', ',', '.', ':', '-']) >= 0
? s
: ucFirst(splitTitleCase(s).join(' '));
}
export function queryString(url) {
if (!url || url.indexOf('?') === -1)
return {};
let pairs = rightPart(url, '?').split('&');
let map = {};
for (let i = 0; i < pairs.length; ++i) {
let p = pairs[i].split('=');
map[p[0]] = p.length > 1
? decodeURIComponent(p[1].replace(/\+/g, ' '))
: null;
}
return map;
}
export function combinePaths(...paths) {
let parts = [], i, l;
for (i = 0, l = paths.length; i < l; i++) {
let 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);
}
let combinedPaths = [];
for (i = 0, l = parts.length; i < l; i++) {
let 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 function createPath(route, args) {
let argKeys =