ngx-resource-core
Version:
Core of resource library - Forked and modified
710 lines (534 loc) • 19.7 kB
text/typescript
import {
IResourceAction,
IResourceActionInner,
IResourceResponse,
ResourceQueryMappingMethod,
ResourceRequestBodyType,
ResourceRequestMethod
} from './Declarations';
import { ResourceGlobalConfig } from './ResourceGlobalConfig';
import { ResourceHelper } from './ResourceHelper';
import { ResourceHandler } from './ResourceHandler';
export class Resource {
private $url: string = null;
private $pathPrefix: string = null;
private $path: string = null;
private $headers: any = null;
private $body: any = null;
private $params: any = null;
private $query: any = null;
constructor(protected requestHandler: ResourceHandler) {
(this.constructor as any).instance = this;
}
/**
* Used to get url
*
* @param {IResourceAction} actionOptions
* @return {string | Promise<string>}
*/
$getUrl(actionOptions: IResourceAction = {}): string | Promise<string> {
return this.$url || actionOptions.url || ResourceGlobalConfig.url || '';
}
$setUrl(url: string) {
this.$url = url;
}
/**
* Used to get path prefix
*
* @param {IResourceAction} actionOptions
* @return {string | Promise<string>}
*/
$getPathPrefix(actionOptions: IResourceAction = {}): string | Promise<string> {
return this.$pathPrefix || actionOptions.pathPrefix || ResourceGlobalConfig.pathPrefix || '';
}
$setPathPrefix(path: string) {
this.$pathPrefix = path;
}
/**
* Used to get path
*
* @param {IResourceAction} actionOptions
* @return {string | Promise<string>}
*/
$getPath(actionOptions: IResourceAction = {}): string | Promise<string> {
return this.$path || actionOptions.path || ResourceGlobalConfig.path || '';
}
$setPath(path: string) {
this.$path = path;
}
/**
* Get headers.
*
* @param {IResourceAction} actionOptions
* @return {any | Promise<any>}
*/
$getHeaders(actionOptions: IResourceAction = {}): any | Promise<any> {
return this.$headers || actionOptions.headers || ResourceGlobalConfig.headers || {};
}
$setHeaders(headers: any) {
this.$headers = headers;
}
/**
* Get body
*
* @param {IResourceAction} actionOptions
* @return {any | Promise<any>}
*/
$getBody(actionOptions: IResourceAction = {}): any | Promise<any> {
return this.$body || actionOptions.body || ResourceGlobalConfig.body || {};
}
$setBody(body: any) {
this.$body = body;
}
/**
* Get path params
*
* @param {IResourceAction} actionOptions
* @return {any | Promise<any>}
*/
$getParams(actionOptions: IResourceAction = {}): any | Promise<any> {
return this.$params || actionOptions.params || ResourceGlobalConfig.params || {};
}
$setParams(params: any) {
this.$params = params;
}
/**
* Get query params
*
* @param {IResourceAction} actionOptions
* @return {any | Promise<any>}
*/
$getQuery(actionOptions: IResourceAction = {}): any | Promise<any> {
return this.$query || actionOptions.query || ResourceGlobalConfig.query || {};
}
$setQuery(query: any) {
this.$query = query;
}
/**
* Used to filter received data.
* Is applied on each element of array or object
*
* @param data
* @param {IResourceActionInner} options
* @return {boolean}
*/
$filter(data: any, options: IResourceActionInner = {}): boolean {
return true;
}
/**
* Used to map received data
* Is applied on each element of array or object
*
* @param data
* @param {IResourceActionInner} options
* @return {any}
*/
$map(data: any, options: IResourceActionInner = {}): any {
return data;
}
/**
* Used to create result object
* Is applied on each element of array or object
*
* @param data
* @param {IResourceActionInner} options
* @return {any}
*/
$resultFactory(data: any, options: IResourceActionInner = {}): any {
return data || {};
}
$restAction(options: IResourceActionInner) {
this.$_setResourceActionInnerDefaults(options);
this.$_setResourceActionOptionDefaults(options);
const actionOptions = options.actionOptions;
if (actionOptions.mutateBody || options.isModel) {
options.returnData = options.actionAttributes.body;
}
if (!actionOptions.asPromise) {
options.returnData = actionOptions.expectJsonArray ? [] : actionOptions.resultFactory.call(this, null, options);
}
if (this.$_canSetInternalData(options)) {
Object.defineProperty(options.returnData, '$resolved', {
enumerable: false,
configurable: true,
writable: true,
value: false
});
Object.defineProperty(options.returnData, '$abort', {
enumerable: false,
configurable: true,
writable: true,
value: () => {
// does nothing for now
}
});
}
options.mainPromise = this.$_setResolvedOptions(options)
.then((o: IResourceActionInner) => this.$_createRequestOptions(o))
.then((o: IResourceActionInner) => {
const handlerResp = this.requestHandler.handle(o.requestOptions);
if (o.returnData && this.$_canSetInternalData(options)) {
o.returnData.$abort = handlerResp.abort;
}
return handlerResp.promise;
})
.then((resp: IResourceResponse) => this.$handleSuccessResponse(options, resp))
.catch((resp: IResourceResponse) => this.$handleErrorResponse(options, resp));
if (this.$_canSetInternalData(options)) {
Object.defineProperty(options.returnData, '$promise', {
enumerable: false,
configurable: true,
writable: true,
value: options.mainPromise
});
}
return actionOptions.asPromise ? options.mainPromise : options.returnData;
}
protected $handleSuccessResponse(options: IResourceActionInner, resp: IResourceResponse): any {
let body = resp.body;
const actionOptions = options.actionOptions;
if (Array.isArray(body)) {
body = body
.filter((item: any) => actionOptions.filter.call(this, item, options))
.map((item: any) => {
item = actionOptions.map.call(this, item, options);
return actionOptions.resultFactory.call(this, item, options);
});
if (options.returnData) {
Array.prototype.push.apply(options.returnData, body);
body = options.returnData;
}
} else {
if (actionOptions.filter.call(this, body, options)) {
body = actionOptions.map.call(this, body, options);
let newBody = options.returnData;
if (newBody) {
if (typeof newBody.$setData === 'function') {
newBody.$setData(body);
} else {
Object.assign(newBody, body);
}
} else {
newBody = actionOptions.resultFactory.call(this, body, options);
}
body = newBody;
// If it's model
if (body.$resource) {
body.$resolved = true;
body.$promise = options.mainPromise;
body.$abort = () => true;
}
} else {
body = null;
}
}
if (this.$_canSetInternalData(options)) {
options.returnData.$resolved = true;
}
if (options.actionOptions.asResourceResponse) {
resp.body = body;
body = resp;
}
if (options.actionAttributes.onSuccess) {
options.actionAttributes.onSuccess(body);
}
return body;
}
protected $handleErrorResponse(options: IResourceActionInner, resp: IResourceResponse): any {
if (options.returnData && this.$_canSetInternalData(options)) {
options.returnData.$resolved = true;
}
if (options.actionAttributes.onError) {
options.actionAttributes.onError(resp);
}
throw resp;
}
protected $setRequestOptionsUrl(options: IResourceActionInner): void {
const ro = options.requestOptions;
if (!ro.url) {
ro.url =
options.resolvedOptions.url +
options.resolvedOptions.pathPrefix +
options.resolvedOptions.path;
}
options.usedInPath = {};
const params = ResourceHelper.defaults(options.actionAttributes.params, options.resolvedOptions.params);
const pathParams = ro.url.match(/{([^}]*)}/g) || [];
for (let i = 0; i < pathParams.length; i++) {
const pathParam = pathParams[i];
let pathKey = pathParam.substr(1, pathParam.length - 2);
const isMandatory = pathKey[0] === '!';
if (isMandatory) {
pathKey = pathKey.substr(1);
}
const onlyPathParam = pathKey[0] === ':';
if (onlyPathParam) {
pathKey = pathKey.substr(1);
}
if (options.actionAttributes.query && options.actionAttributes.query === options.actionAttributes.params) {
options.usedInPath[pathKey] = true;
}
const value = params[pathKey];
if (onlyPathParam) {
delete params[pathKey];
}
if (ResourceHelper.isNullOrUndefined(value)) {
if (isMandatory) {
const consoleMsg = `Mandatory ${pathParam} path parameter is missing`;
console.warn(consoleMsg);
// shell.mainObservable = Observable.create((observer: any) => {
// observer.error(new Error(consoleMsg));
// });
//
//
// this.$_releaseMainDeferredSubscriber(shell);
throw new Error(consoleMsg);
}
ro.url = ro.url.substr(0, ro.url.indexOf(pathParam));
break;
}
// Replacing in the url
ro.url = ro.url.replace(pathParam, value);
}
// Removing double slashed from final url
ro.url = ro.url.replace(/\/\/+/g, '/');
if (ro.url.startsWith('http')) {
ro.url = ro.url.replace(':/', '://');
}
// Remove trailing slash
if (options.actionOptions.removeTrailingSlash) {
while (ro.url[ro.url.length - 1] === '/') {
ro.url = ro.url.substr(0, ro.url.length - 1);
}
}
}
protected $setRequestOptionsBody(options: IResourceActionInner): void {
let body = options.actionAttributes.body;
if (!body) {
return;
}
const realBodyType = ResourceHelper.getRealTypeOf(body);
let bodyOk: boolean = realBodyType === options.actionOptions.requestBodyType;
if (!bodyOk) {
if (realBodyType === ResourceRequestBodyType.JSON) {
if (options.actionOptions.requestBodyType === ResourceRequestBodyType.FORM_DATA) {
const newBody = new FormData();
Object.keys(body).forEach((key: string) => {
const value = body[key];
if (body.hasOwnProperty(key) && typeof value !== 'function') {
const isArrayOfFiles = value instanceof Array && value.reduce((acc, elem) => acc && elem instanceof File, true);
if (isArrayOfFiles) {
value.forEach((f: File, index: number) => {
newBody.append(`${key}[${index}]`, f, (f as File).name);
});
} else if (value instanceof File) {
newBody.append(key, value, (value as File).name);
} else if (!options.actionOptions.rootNode) {
newBody.append(key, value);
}
}
});
if (options.actionOptions.rootNode) {
newBody.append(options.actionOptions.rootNode, JSON.stringify(body));
}
body = newBody;
bodyOk = true;
}
}
}
if (!bodyOk) {
throw new Error('Can not convert body');
}
if (!(body instanceof FormData)) {
// Add root node if needed
if (options.actionOptions.rootNode) {
const newBody: any = {};
newBody[options.actionOptions.rootNode] = body;
body = newBody;
}
if ((options.actionOptions.requestBodyType === ResourceRequestBodyType.NONE ||
(options.actionOptions.requestBodyType === ResourceRequestBodyType.JSON &&
typeof body === 'object' && Object.keys(body).length === 0)
) && !options.actionOptions.keepEmptyBody) {
return;
}
}
options.requestOptions.body = body;
}
protected $setRequestOptionsQuery(options: IResourceActionInner): void {
let oq = options.actionAttributes.query || {};
if (options.resolvedOptions.query) {
oq = {...options.resolvedOptions.query, ...oq};
}
if (oq) {
options.requestOptions.query = {};
Object.keys(oq).forEach((key: string) => {
if (oq.hasOwnProperty(key) && !options.usedInPath[key]) {
this.$appendQueryParams(options.requestOptions.query, key, oq[key], options.queryMappingMethod);
}
});
}
if (options.actionOptions.addTimestamp) {
options.requestOptions.query = options.requestOptions.query || {};
this.$appendQueryParams(
options.requestOptions.query,
options.actionOptions.addTimestamp as string,
Date.now().toString(10),
options.queryMappingMethod);
}
}
protected $appendQueryParams(query: { [prop: string]: string | any[] },
key: string,
value: any,
queryMappingMethod: ResourceQueryMappingMethod): void {
if (value instanceof Date) {
query[key] = value.toISOString();
return;
}
if (typeof value === 'object') {
switch (queryMappingMethod) {
case ResourceQueryMappingMethod.Plain:
if (Array.isArray(value)) {
query[key] = value.join(',');
// for (const arrValue of value) {
// query[key] = arrValue;
// }
} else {
if (value && typeof value === 'object') {
/// Convert dates to ISO format string
if (value instanceof Date) {
value = value.toISOString();
} else {
value = JSON.stringify(value);
}
}
query[key] = value;
}
return;
case ResourceQueryMappingMethod.Bracket:
/// Convert object and arrays to query params
for (const k in value) {
if (value.hasOwnProperty(k)) {
this.$appendQueryParams(query, `${key}[${k}]`, value[k], queryMappingMethod);
}
}
return;
case ResourceQueryMappingMethod.JQueryParamsBracket:
/// Convert object and arrays to query params according to $.params
for (const k in value) {
if (value.hasOwnProperty(k)) {
let path = `${key}[${k}]`;
if (Array.isArray(value) && typeof value[<any>k] !== 'object') {
path = `${key}[]`;
}
this.$appendQueryParams(query, path, value[k], queryMappingMethod);
}
}
return;
}
}
query[key] = value;
}
protected $_setResourceActionInnerDefaults(options: IResourceActionInner) {
const actionOptions = options.actionOptions;
// Setting default request method
if (!actionOptions.method) {
actionOptions.method = ResourceRequestMethod.Get;
}
const actionAttributes = options.actionAttributes;
if (actionAttributes.body) {
// Setting default request content type
if (!actionOptions.requestBodyType) {
actionOptions.requestBodyType = ResourceHelper.getRealTypeOf(actionAttributes.body);
}
// Setting params and query if needed
if (actionOptions.requestBodyType === ResourceRequestBodyType.JSON &&
typeof actionAttributes.body === 'object' && !Array.isArray(actionAttributes.body)) {
if (!actionAttributes.params) {
actionAttributes.params = actionAttributes.body;
}
options.isModel = !!actionAttributes.body.$resource;
}
}
actionAttributes.params = actionAttributes.params || {};
if (!actionAttributes.query && actionOptions.method === ResourceRequestMethod.Get) {
actionAttributes.query = actionAttributes.params;
}
options.queryMappingMethod = actionOptions.queryMappingMethod || ResourceGlobalConfig.queryMappingMethod;
}
protected $_setResourceActionOptionDefaults(options: IResourceActionInner) {
const actionOptions = options.actionOptions;
if (ResourceHelper.isNullOrUndefined(actionOptions.filter)) {
actionOptions.filter = this.$filter;
}
if (ResourceHelper.isNullOrUndefined(actionOptions.map)) {
actionOptions.map = this.$map;
}
if (ResourceHelper.isNullOrUndefined(actionOptions.resultFactory)) {
actionOptions.resultFactory = this.$resultFactory;
}
if (ResourceHelper.isNullOrUndefined(actionOptions.removeTrailingSlash)) {
actionOptions.removeTrailingSlash = ResourceGlobalConfig.removeTrailingSlash;
}
if (ResourceHelper.isNullOrUndefined(actionOptions.withCredentials)) {
actionOptions.withCredentials = ResourceGlobalConfig.withCredentials;
}
if (ResourceHelper.isNullOrUndefined(actionOptions.asPromise)) {
actionOptions.asPromise = ResourceGlobalConfig.asPromise;
}
if (ResourceHelper.isNullOrUndefined(actionOptions.asResourceResponse)) {
actionOptions.asResourceResponse = ResourceGlobalConfig.asResourceResponse;
}
if (ResourceHelper.isNullOrUndefined(actionOptions.responseBodyType)) {
actionOptions.responseBodyType = ResourceGlobalConfig.responseBodyType;
}
if (ResourceHelper.isNullOrUndefined(actionOptions.lean)) {
actionOptions.lean = ResourceGlobalConfig.lean;
if (actionOptions.mutateBody && !actionOptions.asPromise && ResourceHelper.isNullOrUndefined(actionOptions.lean)) {
actionOptions.lean = true;
}
}
if (ResourceHelper.isNullOrUndefined(actionOptions.addTimestamp)) {
actionOptions.addTimestamp = ResourceGlobalConfig.addTimestamp;
if (actionOptions.addTimestamp && typeof actionOptions.addTimestamp !== 'string') {
actionOptions.addTimestamp = 'ts';
}
}
}
protected $_setResolvedOptions(options: IResourceActionInner): Promise<IResourceActionInner> {
return Promise.all([
this.$getUrl(options.actionOptions),
this.$getPathPrefix(options.actionOptions),
this.$getPath(options.actionOptions),
this.$getHeaders(options.actionOptions),
this.$getBody(options.actionOptions),
this.$getParams(options.actionOptions),
this.$getQuery(options.actionOptions)
])
.then((resolvedMain: any[]) => {
options.resolvedOptions = {};
const r = options.resolvedOptions;
[r.url, r.pathPrefix, r.path, r.headers, r.body, r.params, r.query] = resolvedMain;
return options;
});
}
protected $_createRequestOptions(options: IResourceActionInner): IResourceActionInner | Promise<IResourceActionInner> {
options.requestOptions = {};
// Step 1 set main
options.requestOptions.method = options.actionOptions.method;
options.requestOptions.headers = options.resolvedOptions.headers;
options.requestOptions.withCredentials = options.actionOptions.withCredentials;
options.requestOptions.responseBodyType = options.actionOptions.responseBodyType;
options.requestOptions.requestBodyType = options.actionOptions.requestBodyType;
// Step 2 create url
this.$setRequestOptionsUrl(options);
// Step 3 create body
this.$setRequestOptionsBody(options);
// Step 4 set query params
this.$setRequestOptionsQuery(options);
return options;
}
protected $_canSetInternalData(options: IResourceActionInner): boolean {
return options.returnData && (!options.actionOptions.lean || options.isModel);
}
}