UNPKG

ngx-restangular-jc

Version:
821 lines (707 loc) 25.4 kB
import { includes, isUndefined, isNull, isArray, isObject, isBoolean, defaults, each, extend, find, has, initial, last, clone, reduce, keys, isEmpty, forEach, } from 'lodash'; export function RestangularConfigurer(object, configuration) { object.configuration = configuration; /** * Those are HTTP safe methods for which there is no need to pass any data with the request. */ const safeMethods = ['get', 'head', 'options', 'trace', 'getlist']; configuration.isSafe = function (operation) { return includes(safeMethods, operation.toLowerCase()); }; const absolutePattern = /^https?:\/\//i; configuration.isAbsoluteUrl = function (string) { return isUndefined(configuration.absoluteUrl) || isNull(configuration.absoluteUrl) ? string && absolutePattern.test(string) : configuration.absoluteUrl; }; configuration.absoluteUrl = isUndefined(configuration.absoluteUrl) ? true : configuration.absoluteUrl; object.setSelfLinkAbsoluteUrl = function (value) { configuration.absoluteUrl = value; }; /** * This is the BaseURL to be used with Restangular */ configuration.baseUrl = isUndefined(configuration.baseUrl) ? '' : configuration.baseUrl; object.setBaseUrl = function (newBaseUrl) { configuration.baseUrl = /\/$/.test(newBaseUrl) ? newBaseUrl.substring(0, newBaseUrl.length - 1) : newBaseUrl; return this; }; /** * Sets the extra fields to keep from the parents */ configuration.extraFields = configuration.extraFields || []; object.setExtraFields = function (newExtraFields) { configuration.extraFields = newExtraFields; return this; }; /** * Some default $http parameter to be used in EVERY call **/ configuration.defaultHttpFields = configuration.defaultHttpFields || {}; object.setDefaultHttpFields = function (values) { configuration.defaultHttpFields = values; return this; }; /** * Always return plain data, no restangularized object **/ configuration.plainByDefault = configuration.plainByDefault || false; object.setPlainByDefault = function (value) { configuration.plainByDefault = value === true ? true : false; return this; }; configuration.withHttpValues = function (httpLocalConfig, obj) { return defaults(obj, httpLocalConfig, configuration.defaultHttpFields); }; configuration.encodeIds = isUndefined(configuration.encodeIds) ? true : configuration.encodeIds; object.setEncodeIds = function (encode) { configuration.encodeIds = encode; }; configuration.defaultRequestParams = configuration.defaultRequestParams || { get: {}, post: {}, put: {}, remove: {}, common: {} }; object.setDefaultRequestParams = function (param1, param2) { let methods = []; const params = param2 || param1; if (!isUndefined(param2)) { if (isArray(param1)) { methods = param1; } else { methods.push(param1); } } else { methods.push('common'); } each(methods, function (method) { configuration.defaultRequestParams[method] = params; }); return this; }; object.requestParams = configuration.defaultRequestParams; configuration.defaultHeaders = configuration.defaultHeaders || {}; object.setDefaultHeaders = function (headers) { configuration.defaultHeaders = headers; object.defaultHeaders = configuration.defaultHeaders; return this; }; object.defaultHeaders = configuration.defaultHeaders; /** * Method overriders response Method **/ configuration.defaultResponseMethod = configuration.defaultResponseMethod || 'promise'; object.setDefaultResponseMethod = function (method) { configuration.defaultResponseMethod = method; object.defaultResponseMethod = configuration.defaultResponseMethod; return this; }; object.defaultResponseMethod = configuration.defaultResponseMethod; /** * Method overriders will set which methods are sent via POST with an X-HTTP-Method-Override **/ configuration.methodOverriders = configuration.methodOverriders || []; object.setMethodOverriders = function (values) { const overriders = extend([], values); if (configuration.isOverridenMethod('delete', overriders)) { overriders.push('remove'); } configuration.methodOverriders = overriders; return this; }; configuration.jsonp = isUndefined(configuration.jsonp) ? false : configuration.jsonp; object.setJsonp = function (active) { configuration.jsonp = active; }; configuration.isOverridenMethod = function (method, values) { const search = values || configuration.methodOverriders; return !isUndefined(find(search, function (one: string) { return one.toLowerCase() === method.toLowerCase(); })); }; /** * Sets the URL creator type. For now, only Path is created. In the future we'll have queryParams **/ configuration.urlCreator = configuration.urlCreator || 'path'; object.setUrlCreator = function (name) { if (!has(configuration.urlCreatorFactory, name)) { throw new Error('URL Path selected isn\'t valid'); } configuration.urlCreator = name; return this; }; /** * You can set the restangular fields here. The 3 required fields for Restangular are: * * id: Id of the element * route: name of the route of this element * parentResource: the reference to the parent resource * * All of this fields except for id, are handled (and created) by Restangular. By default, * the field values will be id, route and parentResource respectively */ configuration.restangularFields = configuration.restangularFields || { id: 'id', route: 'route', parentResource: 'parentResource', restangularCollection: 'restangularCollection', cannonicalId: '__cannonicalId', etag: 'restangularEtag', selfLink: 'href', get: 'get', getList: 'getList', put: 'put', post: 'post', remove: 'remove', head: 'head', trace: 'trace', options: 'options', patch: 'patch', getRestangularUrl: 'getRestangularUrl', getRequestedUrl: 'getRequestedUrl', putElement: 'putElement', addRestangularMethod: 'addRestangularMethod', getParentList: 'getParentList', clone: 'clone', ids: 'ids', httpConfig: '_$httpConfig', reqParams: 'reqParams', one: 'one', all: 'all', several: 'several', oneUrl: 'oneUrl', allUrl: 'allUrl', customPUT: 'customPUT', customPATCH: 'customPATCH', customPOST: 'customPOST', customDELETE: 'customDELETE', customGET: 'customGET', customGETLIST: 'customGETLIST', customOperation: 'customOperation', doPUT: 'doPUT', doPATCH: 'doPATCH', doPOST: 'doPOST', doDELETE: 'doDELETE', doGET: 'doGET', doGETLIST: 'doGETLIST', fromServer: 'fromServer', withConfig: 'withConfig', withHttpConfig: 'withHttpConfig', singleOne: 'singleOne', plain: 'plain', save: 'save', restangularized: 'restangularized' }; object.setRestangularFields = function (resFields) { configuration.restangularFields = extend({}, configuration.restangularFields, resFields); return this; }; configuration.isRestangularized = function (obj) { return !!obj[configuration.restangularFields.restangularized]; }; configuration.setFieldToElem = function (field, elem, value) { const properties = field.split('.'); let idValue = elem; each(initial(properties), function (prop: any) { idValue[prop] = {}; idValue = idValue[prop]; }); const index: any = last(properties); idValue[index] = value; return this; }; configuration.getFieldFromElem = function (field, elem) { const properties = field.split('.'); let idValue: any = elem; each(properties, function (prop) { if (idValue) { idValue = idValue[prop]; } }); return clone(idValue); }; configuration.setIdToElem = function (elem, id /*, route */) { configuration.setFieldToElem(configuration.restangularFields.id, elem, id); return this; }; configuration.getIdFromElem = function (elem) { return configuration.getFieldFromElem(configuration.restangularFields.id, elem); }; configuration.isValidId = function (elemId) { return '' !== elemId && !isUndefined(elemId) && !isNull(elemId); }; configuration.setUrlToElem = function (elem, url /*, route */) { configuration.setFieldToElem(configuration.restangularFields.selfLink, elem, url); return this; }; configuration.getUrlFromElem = function (elem) { return configuration.getFieldFromElem(configuration.restangularFields.selfLink, elem); }; configuration.useCannonicalId = isUndefined(configuration.useCannonicalId) ? false : configuration.useCannonicalId; object.setUseCannonicalId = function (value) { configuration.useCannonicalId = value; return this; }; configuration.getCannonicalIdFromElem = function (elem) { const cannonicalId = elem[configuration.restangularFields.cannonicalId]; const actualId = configuration.isValidId(cannonicalId) ? cannonicalId : configuration.getIdFromElem(elem); return actualId; }; /** * Sets the Response parser. This is used in case your response isn't directly the data. * For example if you have a response like {meta: {'meta'}, data: {name: 'Gonto'}} * you can extract this data which is the one that needs wrapping * * The ResponseExtractor is a function that receives the response and the method executed. */ configuration.responseInterceptors = configuration.responseInterceptors ? [...configuration.responseInterceptors] : []; configuration.defaultResponseInterceptor = function (data /*, operation, what, url, response, subject */) { return data || {}; }; configuration.responseExtractor = function (data, operation, what, url, response, subject) { const interceptors = clone(configuration.responseInterceptors); interceptors.push(configuration.defaultResponseInterceptor); let theData = data; each(interceptors, function (interceptor: any) { theData = interceptor(theData, operation, what, url, response, subject); }); return theData; }; object.addResponseInterceptor = function (extractor) { configuration.responseInterceptors.push(extractor); return this; }; configuration.errorInterceptors = configuration.errorInterceptors ? [...configuration.errorInterceptors] : []; object.addErrorInterceptor = function (interceptor) { configuration.errorInterceptors = [interceptor, ...configuration.errorInterceptors]; return this; }; object.setResponseInterceptor = object.addResponseInterceptor; object.setResponseExtractor = object.addResponseInterceptor; object.setErrorInterceptor = object.addErrorInterceptor; /** * Response interceptor is called just before resolving promises. */ /** * Request interceptor is called before sending an object to the server. */ configuration.requestInterceptors = configuration.requestInterceptors ? [...configuration.requestInterceptors] : []; configuration.defaultInterceptor = function (element, operation, path, url, headers, params, httpConfig) { return { element: element, headers: headers, params: params, httpConfig: httpConfig }; }; configuration.fullRequestInterceptor = function (element, operation, path, url, headers, params, httpConfig) { const interceptors = clone(configuration.requestInterceptors); const defaultRequest = configuration.defaultInterceptor(element, operation, path, url, headers, params, httpConfig); return reduce(interceptors, function (request: any, interceptor: any) { const returnInterceptor: any = interceptor( request.element, operation, path, url, request.headers, request.params, request.httpConfig ); return extend(request, returnInterceptor); }, defaultRequest); }; object.addRequestInterceptor = function (interceptor) { configuration.requestInterceptors.push(function (elem, operation, path, url, headers, params, httpConfig) { return { headers: headers, params: params, element: interceptor(elem, operation, path, url), httpConfig: httpConfig }; }); return this; }; object.setRequestInterceptor = object.addRequestInterceptor; object.addFullRequestInterceptor = function (interceptor) { configuration.requestInterceptors.push(interceptor); return this; }; object.setFullRequestInterceptor = object.addFullRequestInterceptor; configuration.onBeforeElemRestangularized = configuration.onBeforeElemRestangularized || function (elem) { return elem; }; object.setOnBeforeElemRestangularized = function (post) { configuration.onBeforeElemRestangularized = post; return this; }; object.setRestangularizePromiseInterceptor = function (interceptor) { configuration.restangularizePromiseInterceptor = interceptor; return this; }; /** * This method is called after an element has been "Restangularized". * * It receives the element, a boolean indicating if it's an element or a collection * and the name of the model * */ configuration.onElemRestangularized = configuration.onElemRestangularized || function (elem) { return elem; }; object.setOnElemRestangularized = function (post) { configuration.onElemRestangularized = post; return this; }; configuration.shouldSaveParent = configuration.shouldSaveParent || function () { return true; }; object.setParentless = function (values) { if (isArray(values)) { configuration.shouldSaveParent = function (route) { return !includes(values, route); }; } else if (isBoolean(values)) { configuration.shouldSaveParent = function () { return !values; }; } return this; }; /** * This lets you set a suffix to every request. * * For example, if your api requires that for JSon requests you do /users/123.json, you can set that * in here. * * * By default, the suffix is null */ configuration.suffix = isUndefined(configuration.suffix) ? null : configuration.suffix; object.setRequestSuffix = function (newSuffix) { configuration.suffix = newSuffix; return this; }; /** * Add element transformers for certain routes. */ configuration.transformers = configuration.transformers || {}; object.addElementTransformer = function (type, secondArg, thirdArg) { let isCollection = null; let transformer = null; if (arguments.length === 2) { transformer = secondArg; } else { transformer = thirdArg; isCollection = secondArg; } let typeTransformers = configuration.transformers[type]; if (!typeTransformers) { typeTransformers = configuration.transformers[type] = []; } typeTransformers.push(function (coll, elem) { if (isNull(isCollection) || (coll === isCollection)) { return transformer(elem); } return elem; }); return object; }; object.extendCollection = function (route, fn) { return object.addElementTransformer(route, true, fn); }; object.extendModel = function (route, fn) { return object.addElementTransformer(route, false, fn); }; configuration.transformElem = function (elem, isCollection, route, Restangular, force) { if (!force && !configuration.transformLocalElements && !elem[configuration.restangularFields.fromServer]) { return elem; } const typeTransformers = configuration.transformers[route]; let changedElem = elem; if (typeTransformers) { each(typeTransformers, function (transformer: (isCollection: boolean, changedElem: any) => any) { changedElem = transformer(isCollection, changedElem); }); } return configuration.onElemRestangularized(changedElem, isCollection, route, Restangular); }; configuration.transformLocalElements = isUndefined(configuration.transformLocalElements) ? false : configuration.transformLocalElements; object.setTransformOnlyServerElements = function (active) { configuration.transformLocalElements = !active; }; configuration.fullResponse = isUndefined(configuration.fullResponse) ? false : configuration.fullResponse; object.setFullResponse = function (full) { configuration.fullResponse = full; return this; }; // Internal values and functions configuration.urlCreatorFactory = {}; /** * Base URL Creator. Base prototype for everything related to it **/ const BaseCreator = function () { }; BaseCreator.prototype.setConfig = function (config) { this.config = config; return this; }; BaseCreator.prototype.parentsArray = function (current) { const parents = []; while (current) { parents.push(current); current = current[this.config.restangularFields.parentResource]; } return parents.reverse(); }; function RestangularResource(config, $http, url, configurer) { const resource = {}; each(keys(configurer), function (key) { const value = configurer[key]; // Add default parameters value.params = extend({}, value.params, config.defaultRequestParams[value.method.toLowerCase()]); // We don't want the ? if no params are there if (isEmpty(value.params)) { delete value.params; } if (config.isSafe(value.method)) { resource[key] = function () { const resultConfig = extend(value, { url: url }); return $http.createRequest(resultConfig); }; } else { resource[key] = function (data) { const resultConfig = extend(value, { url: url, data: data }); return $http.createRequest(resultConfig); }; } }); return resource; } BaseCreator.prototype.resource = function (current, $http, localHttpConfig, callHeaders, callParams, what, etag, operation) { const params = defaults(callParams || {}, this.config.defaultRequestParams.common); const headers = defaults(callHeaders || {}, this.config.defaultHeaders); if (etag) { if (!configuration.isSafe(operation)) { headers['If-Match'] = etag; } else { headers['If-None-Match'] = etag; } } let url = this.base(current); if (what) { let add = ''; if (!/\/$/.test(url)) { add += '/'; } add += what; url += add; } if (this.config.suffix && url.indexOf(this.config.suffix, url.length - this.config.suffix.length) === -1 && !this.config.getUrlFromElem(current)) { url += this.config.suffix; } current[this.config.restangularFields.httpConfig] = undefined; return RestangularResource(this.config, $http, url, { getList: this.config.withHttpValues(localHttpConfig, { method: 'GET', params: params, headers: headers }), get: this.config.withHttpValues(localHttpConfig, { method: 'GET', params: params, headers: headers }), jsonp: this.config.withHttpValues(localHttpConfig, { method: 'jsonp', params: params, headers: headers }), put: this.config.withHttpValues(localHttpConfig, { method: 'PUT', params: params, headers: headers }), post: this.config.withHttpValues(localHttpConfig, { method: 'POST', params: params, headers: headers }), remove: this.config.withHttpValues(localHttpConfig, { method: 'DELETE', params: params, headers: headers }), head: this.config.withHttpValues(localHttpConfig, { method: 'HEAD', params: params, headers: headers }), trace: this.config.withHttpValues(localHttpConfig, { method: 'TRACE', params: params, headers: headers }), options: this.config.withHttpValues(localHttpConfig, { method: 'OPTIONS', params: params, headers: headers }), patch: this.config.withHttpValues(localHttpConfig, { method: 'PATCH', params: params, headers: headers }) }); }; /** * This is the Path URL creator. It uses Path to show Hierarchy in the Rest API. * This means that if you have an Account that then has a set of Buildings, a URL to a building * would be /accounts/123/buildings/456 **/ const Path = function () { }; Path.prototype = new BaseCreator(); Path.prototype.normalizeUrl = function (url) { const parts = /((?:http[s]?:)?\/\/)?(.*)?/.exec(url); parts[2] = parts[2].replace(/[\\\/]+/g, '/'); return (typeof parts[1] !== 'undefined') ? parts[1] + parts[2] : parts[2]; }; Path.prototype.base = function (current) { const __this = this; return reduce(this.parentsArray(current), function (acum: any, elem: any) { let elemUrl; const elemSelfLink = __this.config.getUrlFromElem(elem); if (elemSelfLink) { if (__this.config.isAbsoluteUrl(elemSelfLink)) { return elemSelfLink; } else { elemUrl = elemSelfLink; } } else { elemUrl = elem[__this.config.restangularFields.route]; if (elem[__this.config.restangularFields.restangularCollection]) { const ids = elem[__this.config.restangularFields.ids]; if (ids) { elemUrl += '/' + ids.join(','); } } else { let elemId: any; if (__this.config.useCannonicalId) { elemId = __this.config.getCannonicalIdFromElem(elem); } else { elemId = __this.config.getIdFromElem(elem); } if (configuration.isValidId(elemId) && !elem.singleOne) { elemUrl += '/' + (__this.config.encodeIds ? encodeURIComponent(elemId) : elemId); } } } acum = acum.replace(/\/$/, '') + '/' + elemUrl; return __this.normalizeUrl(acum); }, this.config.baseUrl); }; Path.prototype.fetchUrl = function (current, what) { let baseUrl = this.base(current); if (what) { baseUrl += '/' + what; } return baseUrl; }; Path.prototype.fetchRequestedUrl = function (current, what) { const url = this.fetchUrl(current, what); const params = current[configuration.restangularFields.reqParams]; // From here on and until the end of fetchRequestedUrl, // the code has been kindly borrowed from angular.js // The reason for such code bloating is coherence: // If the user were to use this for cache management, the // serialization of parameters would need to be identical // to the one done by angular for cache keys to match. function sortedKeys(obj) { const resultKeys = []; for (const key in obj) { if (obj.hasOwnProperty(key)) { resultKeys.push(key); } } return resultKeys.sort(); } function forEachSorted(obj, iterator?, context?) { const sortedKeysArray = sortedKeys(obj); for (let i = 0; i < sortedKeysArray.length; i++) { iterator.call(context, obj[sortedKeysArray[i]], sortedKeysArray[i]); } return sortedKeysArray; } function encodeUriQuery(val, pctEncodeSpaces?) { return encodeURIComponent(val) .replace(/%40/gi, '@') .replace(/%3A/gi, ':') .replace(/%24/g, '$') .replace(/%2C/gi, ',') .replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); } if (!params) { return url + (this.config.suffix || ''); } const parts = []; forEachSorted(params, function (value, key) { if (value === null || value === undefined) { return; } if (!isArray(value)) { value = [value]; } forEach(value, function (v) { if (isObject(v)) { v = JSON.stringify(v); } parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(v)); }); }); return url + (this.config.suffix || '') + ((url.indexOf('?') === -1) ? '?' : '&') + parts.join('&'); }; configuration.urlCreatorFactory.path = Path; }