/** * @ngdoc provider * @name apiProvider * @module superdesk.core.api * @description This provider allows registering API configuration. */ function APIProvider() { var apis = {}; /** * @ngdoc method * @name apiProvider#api_ * @public * @param {string} name * @param {Object} config * @description * > **WARNING** This method is actually called `api` but can not named that * because of a bug in dgeni. So replace `api_` with `api` when calling. * * Register an api. */ this.api = function(name, config) { apis[name] = config; return this; }; /** * @ngdoc service * @name api * @module superdesk.core.api * @requires $injector * @requires $q * @requires $http * @requires urls * @requires lodash * @requires HttpEndpointFactory * @description Raw API operations. */ this.$get = apiServiceFactory; apiServiceFactory.$inject = ['$injector', '$q', '$http', 'urls', 'lodash', 'HttpEndpointFactory']; function apiServiceFactory($injector, $q, $http, urls, _, HttpEndpointFactory) { const CACHE_TTL = 100; var endpoints = { http: HttpEndpointFactory, }; function isOK(response) { function isErrData(data) { return data && data._status && data._status === 'ERR'; } return response.status >= 200 && response.status < 300 && !isErrData(response.data); } var cache = {}; /** * Call $http once url is resolved * * Detect duplicate requests and serve these from cache. */ function http(config) { return $q.when(config.url) .then((url) => { config.url = url; if (config.method !== 'GET') { return $http(config); } let now = Date.now(); let key = config.url + angular.toJson(config.params || {}); let last = cache[key] || null; if (last && now - last.now < CACHE_TTL) { console.warn('duplicate request', config.url, 'after', now - last.now, 'ms', config.params, ); return last.promise; } let promise = $http(config); cache[key] = { now: now, promise: promise, }; return promise; }) .then((response) => isOK(response) ? response.data : $q.reject(response)); } /** * Remove keys prefixed with '_' */ function clean(data, keepId?) { var blacklist = { _type: 1, _status: 1, _updated: 1, _created: 1, _etag: 1, _links: 1, _id: keepId ? 0 : 1, }, cleanData = {}; angular.forEach(data, (val, key) => { if (!blacklist[key]) { cleanData[key] = val; } }); return cleanData; } /** * Get headers for given item */ function getHeaders(item) { var headers = {}; if (item && item._etag) { headers['If-Match'] = item._etag; } return headers; } /** * API Resource instance */ function Resource(resource, parent) { /** * @ngdoc property * @name api#resource * @type {string} * @public * @description API resource */ this.resource = resource; /** * @ngdoc property * @name api#parent * @public * @type {string} * @description Resource parent. */ this.parent = parent; } /** * @ngdoc method * @name api#url * @public * @description * Get resource url. */ Resource.prototype.url = function(_id) { function resolve(urlTemplate, data) { return urlTemplate.replace(/<.*>/, data._id); } return urls.resource(this.resource) .then(angular.bind(this, function(url) { let addr = url; if (this.parent) { var newUrl = resolve(url, this.parent); if (newUrl !== addr) { return newUrl; } } if (_id) { addr = url + '/' + _id; } return addr; })); }; /** * @ngdoc method * @name api#save * @public * @description * Save an item */ Resource.prototype.save = function(item, diff, httpParams, options?: {skipPostProcessing: boolean}) { if (diff && diff._etag) { item._etag = diff._etag; } return http({ method: item._links ? 'PATCH' : 'POST', url: item._links ? urls.item(item._links.self.href) : this.url(), data: diff ? clean(diff, !item._links) : clean(item, !item._links), params: httpParams, headers: getHeaders(item), }).then((data) => { if (options?.skipPostProcessing === true) { return data; } delete data._type; angular.extend(item, diff || {}); angular.extend(item, data); return item; }); }; /** * @ngdoc method * @name api#replace * @public * @description * Replace an item */ Resource.prototype.replace = function(item) { return http({ method: 'PUT', url: this.url(item._id), data: clean(item), }); }; /** * @ngdoc method * @name api#query * @public * @param {Object} params * @param {bool} cache * @description * Query resource */ Resource.prototype.query = function(params, _cache) { return http({ method: 'GET', url: this.url(), params: params, cache: _cache, }); }; /** * @ngdoc method * @name api#getAll * @public * @param {Object} params * @description * Retrieve all items of a query */ Resource.prototype.getAll = function(params) { function _getAll(page = 1, items = []) { return this.query(Object.assign({max_results: 199, page: page}, params)) .then((result) => { let pg = page; let merged = items.concat(result._items); if (result._links.next) { pg++; // p = p.then(_getAll.call(this, pg, merged)); return _getAll.call(this, pg, merged); } else { // deferred.resolve(merged); return merged; } }); } return _getAll.call(this); }; /** * @ngdoc method * @name api#getById * @public * * @param {String} _id * @param {Object} params * @param {bool} cache * * @description * Get an item by _id */ Resource.prototype.getById = function(_id, params, _cache) { return http({ method: 'GET', url: this.url(_id), params: params, cache: _cache, }); }; /** * @ngdoc method * @name api#remove * @public * * @param {Object} item * @param {Object} params * * @description Remove an item */ Resource.prototype.remove = function(item, params) { return http({ method: 'DELETE', url: urls.item(item._links.self.href), params: params, headers: getHeaders(item), }); }; // api service var api: any = function apiService(resource, parent) { return new Resource(resource, parent); }; /** * @alias api(resource).getById(id) */ api.find = function apiFind(resource, id, params, _cache) { return api(resource).getById(id, params, _cache); }; /** * @alias api(resource).save(dest, diff) */ api.save = function apiSave(resource, dest, diff, parent, params, options?: {skipPostProcessing: boolean}) { return api(resource, parent).save(dest, diff, params, options); }; /** * Remove a given item. */ api.remove = function apiRemove(item, params, resource) { var url = resource ? getResourceUrl(resource, item, item._id) : urls.item(item._links.self.href); return http({ method: 'DELETE', url: url, params: params, headers: getHeaders(item), }); }; /** * Update item via given resource * * @param {string} resource * @param {Object} item * @param {Object} updates * @param {Object} params */ api.update = function apiUpdate(resource, item, updates, params) { return http({ method: 'PATCH', url: getResourceUrl(resource, item, item._id), data: updates, params: params, headers: getHeaders(item), }); }; /** * Query qiven resource * * @param {string} resource * @param {Object} query * @param {Object} parent * @param {boolean} cache */ api.query = function apiQuery(resource, query, parent, _cache) { return api(resource, parent).query(query, _cache); }; function getResourceUrl(resource, item, id) { return api(resource, item).url(id); } /** * @ngdoc method * @name api#get * @public * * @param {string} url * * @description Get on a given url */ api.get = function apiGet(url, params) { return http({ method: 'GET', url: urls.item(url), params: params, }); }; api.getAll = function apiGetAll(resource, params) { return api(resource).getAll(params); }; angular.forEach(apis, (config, apiName) => { var service = config.service || _.noop; service.prototype = new endpoints[config.type](apiName, config.backend); api[apiName] = $injector.instantiate(service, {resource: service.prototype}); }); return api; } } angular.module('superdesk.core.api.service', []) .provider('api', APIProvider);