Last active
February 7, 2016 14:43
-
-
Save lyschoening/ec27d28a0b38a7b16ac2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import angular from 'angular'; | |
function copy(source, target) { | |
Object.keys(source).forEach((key) => { | |
target[key] = source[key]; | |
}); | |
return target; | |
} | |
function fromCamelCase(string, separator = '_') { | |
return string.replace(/([a-z][A-Z])/g, (g) => `${g[0]}${separator}${g[1].toLowerCase()}`); | |
} | |
function toCamelCase(string) { | |
return string.replace(/_([a-z0-9])/g, (g) => g[1].toUpperCase()); | |
} | |
function omap(object, callback, thisArg) { | |
var O = {}; | |
Object.keys(object).forEach((key) => { | |
var [k, v] = callback.call(thisArg, key, object[key]); | |
O[k] = v; | |
}); | |
return O; | |
} | |
function ApiProvider() { | |
var provider = this; | |
provider.prefix = ''; | |
provider.$get = ['$cacheFactory', function ($cacheFactory) { | |
return { | |
prefix: provider.prefix, | |
defaultPerPage: 20, | |
resources: {}, | |
cache: $cacheFactory('resource-items'), | |
parseUri: function parseUri(uri) { | |
uri = decodeURIComponent(uri); | |
if (uri.indexOf(this.prefix) === 0) { | |
uri = uri.substring(this.prefix.length); | |
} | |
for (var resourceUri in this.resources) { | |
if (uri.indexOf(`${resourceUri}/`) === 0) { | |
var remainder = uri.substring(resourceUri.length + 1); | |
return { | |
constructor: this.resources[resourceUri], | |
params: remainder.split('/') | |
} | |
} | |
} | |
throw new Error(`Unknown Resource URI: ${uri}`); | |
} | |
} | |
}]; | |
return provider; | |
} | |
function ResourceTypeFactory($q, Api, Route) { | |
class ResourceType { | |
equals(other) { | |
if (other != null && other.$uri != null) { | |
return this.$uri === other.$uri; | |
} | |
return this === other; | |
} | |
save() { | |
if (this.$hasBeenSaved()) { | |
return this.$route.post(this.toJSON(), null, this); | |
} else { | |
return this.constructor.route.post(this.toJSON(), null, this); | |
} | |
} | |
update(changes) { | |
Object.assign(this, changes); | |
if (this.$hasBeenSaved()) { | |
return this.$route.patch(changes, null, this) | |
} else { | |
return this.save(); | |
} | |
} | |
['delete']() { | |
if (this.$hasBeenSaved()) { | |
return this.$route | |
.delete() | |
.then(() => { | |
Api.cache.remove(this.$uri); | |
return this; | |
}); | |
} | |
return false; | |
} | |
get $id() { | |
return parseInt(Api.parseUri(this.$uri).params[0]); | |
} | |
get $route() { | |
return new Route(this.$uri); | |
} | |
$hasBeenSaved() { | |
return !!this.$uri && this.$saved; | |
} | |
$ensureLoaded() { | |
if(this.$promise) { | |
return $q.when(this); | |
} else { | |
this.$promise = this.$route.get(); | |
return this.$promise; | |
} | |
} | |
toJSON() { | |
// omit read-only fields | |
// resolve promises | |
var instance = {}; | |
Object.keys(this) | |
.filter((k) => this.constructor.meta.readOnly.indexOf(k) === -1 && k != '$uri') | |
.forEach((k) => {instance[fromCamelCase(k)] = this[k]}); | |
return instance; | |
} | |
} | |
return ResourceType; | |
} | |
function ResourceFactory($q, Api, Route, LazyPromise, ResourceType) { | |
var toUri = (route, id) => `${route}/${id}`; | |
function cacheInstance(ctype, data) { | |
var instance, uri = data.$uri; | |
if (!(instance = Api.cache.get(uri))) { | |
instance = new ctype(data); | |
Api.cache.put(uri, instance); | |
} else { | |
Object.assign(instance, data); | |
} | |
return instance; | |
} | |
return function (resourceUri, {promises=[], routes={}, instanceRoutes={}, readOnly=[]} = {}) { | |
class Resource extends ResourceType { | |
constructor(data) { | |
super(); | |
var raw = {}; | |
constructor.meta.promises.forEach((key) => { | |
raw[key] = data[key]; | |
Object.defineProperty(this, key, { | |
enumerable: false, | |
get: () => new LazyPromise(raw[key]), | |
set: (value) => { | |
raw[key] = value; | |
} | |
}); | |
}); | |
Object.assign(this, data || {}); | |
// TODO promises | |
} | |
} | |
var constructor = Resource; | |
var route = constructor.route = new Route(resourceUri); | |
constructor.meta = {resourceUri, readOnly, promises, routes, instanceRoutes}; | |
constructor.empty = (id) => { | |
return cacheInstance(constructor, {$uri: toUri(resourceUri, id)}); | |
}; | |
constructor.get = (id) => { | |
let instance, uri = toUri(resourceUri, id); | |
if (instance = Api.cache.get(uri)) { | |
return $q.when(instance); | |
} | |
instance = cacheInstance(constructor, {$uri: uri}); | |
return instance.$ensureLoaded(); | |
}; | |
constructor.query = (queryParams, options = {}) => { | |
return route.query(queryParams, options); | |
}; | |
Object.keys(routes).forEach((key) => { | |
constructor[key] = new Route(`${resourceUri}${routes[key]}`); | |
}); | |
Object.keys(instanceRoutes).forEach((key) => { | |
Object.defineProperty(Resource.prototype, key, { | |
enumerable: false, | |
get: function() { | |
return new Route(`${this.$uri}${instanceRoutes[key]}`) | |
} | |
}); | |
}); | |
Api.resources[resourceUri] = Resource; | |
return Resource; | |
} | |
} | |
function fromPotionJSONFactory($q, Api) { | |
function fromJSON(instance, defaultObj=null) { | |
var value; | |
if(typeof instance == 'object' && instance !== null) { | |
if(instance instanceof Array) { | |
return $q.all(instance.map((v) => fromJSON(v))); | |
} else if(typeof instance.$uri == 'string') { | |
var uri = instance.$uri.substring(Api.prefix.length); | |
if(defaultObj) { | |
var constructor = defaultObj.constructor; | |
} else { | |
var {constructor} = Api.parseUri(instance.$uri); | |
} | |
var data = omap(instance, (k, v) => { | |
var value; | |
if (k == '$uri') { | |
return [k, v]; | |
} else if(constructor.meta.promises.indexOf(k) !== -1) { | |
value = () => fromJSON(v); | |
} else { | |
value = fromJSON(v); | |
} | |
return [toCamelCase(k), value]; | |
}); | |
return $q.all(data).then((data) => { | |
if(defaultObj) { | |
value = defaultObj; | |
Object.assign(value, data); | |
} else if (!(value = Api.cache.get(uri))) { | |
value = new constructor(data); | |
} else { | |
Object.assign(value, data); | |
} | |
value.$uri = uri; | |
value.$saved = true; | |
Api.cache.put(uri, value); | |
return value; | |
}); | |
} else if(typeof instance.$date != 'undefined' && Object.keys(instance).length == 1) { | |
return $q.when(new Date(instance.$date)); | |
} else if(typeof instance.$ref == 'string' && Object.keys(instance).length == 1) { | |
var uri = instance.$ref.substring(Api.prefix.length); | |
var value = Api.cache.get(uri); | |
if(typeof value == 'undefined') { | |
var {constructor, params} = Api.parseUri(uri); | |
return constructor.get(params[0]); | |
} | |
return value; | |
} else { | |
var object = {}; | |
for(var key of Object.keys(instance)) { | |
object[toCamelCase(key)] = fromJSON(instance[key]); | |
} | |
return $q.all(object); | |
} | |
} else { | |
return $q.when(instance); | |
} | |
} | |
return fromJSON; | |
} | |
function RouteFactory($q, $http, Api, LazyPromise, Pagination, fromPotionJSON) { | |
function toJSON(instance) { | |
if(typeof instance == 'object' && instance !== null) { | |
if(typeof instance.$uri == 'string') { | |
// TODO if not $hasBeenSaved(), save() first. | |
//if(!v.$hasBeenSaved()) { | |
// await v.save(); | |
//} | |
return {"$ref": `${Api.prefix}${instance.$uri}`}; | |
} else if(instance instanceof Date) { | |
return {$date: instance.getTime()} | |
} else if(instance instanceof LazyPromise) { | |
return instance.$raw; | |
} else if(instance instanceof Array) { | |
return instance.map(toJSON); | |
} else { | |
return omap(instance, (k, v) => [fromCamelCase(k), toJSON(v)]); | |
} | |
} else { | |
return instance; | |
} | |
} | |
function request(route, httpConfig, paginationObj=null, defaultObj=null) { | |
return $http(httpConfig) | |
.then((response) => { | |
var promise = fromPotionJSON(response.data, defaultObj); | |
// XXX paginate only when explicitly requested. | |
//if(!paginationObj && response.headers('link')) { | |
// paginationObj = new Pagination(null, null, route); | |
//} | |
if(paginationObj) { | |
return promise.then((data) => { | |
paginationObj._applyResponse(response, httpConfig, data); | |
return paginationObj; | |
}); | |
} else { | |
return promise; | |
} | |
}); | |
} | |
// transform | |
class Route { | |
constructor(uri, {prefix=Api.prefix} = {}) { | |
this.uri = uri; | |
this.prefix = prefix; | |
} | |
callWithHttpConfig(httpConfig, paginationObj = null) { | |
return request(this, httpConfig, paginationObj) | |
} | |
get(queryParams = {}, {paginate = false, cache = false, paginationObj = null} = {}) { | |
if(paginate && !paginationObj) { | |
var {page=1, perPage=Api.defaultPerPage} = queryParams; | |
paginationObj = new Pagination(page, perPage, this); | |
} | |
return request(this, { | |
url: `${this.prefix}${this.uri}`, | |
params: omap(queryParams, (k, v) => [fromCamelCase(k), toJSON(v)]) || {}, | |
method: 'GET', | |
cache: cache | |
}, paginationObj); | |
} | |
query(queryParams = {}, options = {}) { | |
return this.get(queryParams, options); | |
} | |
post(data, params = null, defaultObj = null) { | |
return request(this, { | |
url: `${this.prefix}${this.uri}`, | |
data: toJSON(data || {}), | |
params: params || {}, | |
method: 'POST', | |
cache: false | |
}, null, defaultObj) | |
} | |
patch(data, params = null, defaultObj = null) { | |
return request(this, { | |
url: `${this.prefix}${this.uri}`, | |
data: toJSON(data || {}), | |
params: params || {}, | |
method: 'PATCH', | |
cache: false | |
}, null, defaultObj) | |
} | |
['delete'](data, params = null) { | |
return request(this, { | |
url: `${this.prefix}${this.uri}`, | |
data: toJSON(data || {}), | |
params: params || {}, | |
method: 'DELETE', | |
cache: false | |
}) | |
} | |
} | |
return Route; | |
} | |
function PaginationFactory() { | |
function parseLinkHeader(linkHeader) { | |
var key, link, links, param, queryString, re, rel, url, val; | |
links = {}; | |
re = /<([^>]+)>; rel="([a-z0-9]+),?"/g; | |
if (linkHeader == null) { | |
return null; | |
} | |
while (link = re.exec(linkHeader)) { | |
[url, rel] = link.slice(1); | |
links[rel] = {rel: rel, url: url}; | |
if (url.indexOf('?') !== -1) { | |
queryString = url.substring(url.indexOf('?') + 1); | |
for(param of queryString.split('&')) { | |
[key, val] = param.split(/\=/); | |
links[rel][toCamelCase(key)] = val; | |
} | |
} | |
} | |
return links; | |
} | |
class Pagination extends Array { | |
constructor(page, perPage, route) { | |
super(); // for API compatibility in ES2015 | |
this._page = page; | |
this._pages = null; | |
this._perPage = perPage; | |
this._route = route; | |
} | |
// TODO move this into Route function: | |
_applyResponse(response, httpConfig, items) { | |
var links = parseLinkHeader(response.headers('link')); | |
if(links) { | |
this._page = parseInt(links.self.page) || this._page; | |
this._perPage = parseInt(links.self.perPage) || this._perPage; | |
this._pages = links.last ? parseInt(links.last.page) : this._page; | |
} else { | |
this._page = undefined; | |
this._perPage = undefined; | |
this._pages = undefined; | |
} | |
this._total = parseInt(response.headers('X-Total-Count')); | |
this._httpConfig = httpConfig; | |
this.length = 0; | |
this.push(...items); | |
return this; | |
} | |
map(callback, thisArg) { | |
var P = new Pagination(); | |
P._page = this._page; | |
P._pages = this._pages; | |
P._perPage = this._perPage; | |
P._httpConfig = this._httpConfig; | |
P._route = this._route; | |
P.length = this.length; | |
var k = 0; | |
while(k < this.length) { | |
P[k] = callback.call(thisArg, this[k], k, this); | |
k++; | |
} | |
return P; | |
} | |
get page() { | |
return this._page; | |
} | |
set page(page) { | |
this.changePageTo(page); | |
} | |
changePageTo(page, perPage = null) { | |
var httpConfig = this._httpConfig; | |
if(perPage) { | |
this._perPage = perPage; | |
} | |
httpConfig.params.perPage = perPage; | |
httpConfig.params.page = page; | |
return this._route.callWithHttpConfig(httpConfig, this); | |
} | |
get pages() { | |
return this._pages; | |
} | |
get perPage() { | |
return this._perPage; | |
} | |
get total() { | |
return this._total; | |
} | |
toArray() { | |
return Array.from(this); | |
} | |
} | |
return Pagination; | |
} | |
function LazyPromiseFactory($q) { | |
class LazyPromise { | |
constructor(raw) { | |
this.$raw = raw; | |
} | |
get $promise() { | |
if(typeof this.$raw == 'function') { | |
return $q.when(this.$raw()); | |
} | |
return $q.when(this.$raw); | |
} | |
then(success, error, notify) { | |
return this.$promise.then(success, error, notify); | |
} | |
['catch'](callback) { | |
return this.$promise.catch(callback); | |
} | |
['finally'](callback) { | |
return this.$promise.finally(callback); | |
} | |
} | |
return LazyPromise; | |
} | |
function DynamicItemsFactory() { | |
class DynamicItems { | |
/** | |
* Infinite scroll for mdVirtualRepeat | |
* | |
* @param store | |
* @param sort | |
*/ | |
constructor(resource, {sort = undefined, where = undefined} = {}) { | |
this._resource = resource; | |
this._sort = sort; | |
this._where = where; | |
this._pages = {}; | |
this._pending = []; | |
this.PAGE_SIZE = 25; | |
this._length = 1; | |
} | |
async _fetchPage(pageNumber) { | |
if (this._pending.includes(pageNumber)) { | |
return | |
} | |
try { | |
this._pending.push(pageNumber); | |
await this._resource.route.get({ | |
page: pageNumber, | |
perPage: this.PAGE_SIZE, | |
sort: this._sort, | |
where: this._where | |
}, { | |
cache: false, | |
paginationObj: { | |
_applyResponse: (response, httpConfig, data) => { | |
this._length = parseInt(response.headers('X-Total-Count')); | |
this._pages[pageNumber] = data; | |
} | |
} | |
}) | |
} finally { | |
this._pending.splice(this._pending.indexOf(pageNumber), 1); | |
} | |
} | |
getItemAtIndex(index) { | |
let pageNumber = Math.ceil((index + 1) / this.PAGE_SIZE); | |
let page = this._pages[pageNumber]; | |
if (page) { | |
return page[index % this.PAGE_SIZE]; | |
} else { | |
this._fetchPage(pageNumber) | |
} | |
} | |
getLength() { | |
return this._length; | |
} | |
} | |
return DynamicItems; | |
} | |
export default angular.module('resource', []) | |
.provider('Api', ApiProvider) | |
.factory('ResourceType', ['$q', 'Api', 'Route', ResourceTypeFactory]) | |
.factory('Resource', ['$q', 'Api', 'Route', 'LazyPromise', 'ResourceType', ResourceFactory]) | |
.factory('fromPotionJSON', ['$q', 'Api', fromPotionJSONFactory]) | |
.factory('Route', ['$q', '$http', 'Api', 'LazyPromise', 'Pagination', 'fromPotionJSON', RouteFactory]) | |
.factory('Pagination', PaginationFactory) | |
.factory('LazyPromise', ['$q', LazyPromiseFactory]) | |
.factory('DynamicItems', DynamicItemsFactory); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment