Skip to content

Instantly share code, notes, and snippets.

@lyschoening
Last active February 7, 2016 14:43
Show Gist options
  • Save lyschoening/ec27d28a0b38a7b16ac2 to your computer and use it in GitHub Desktop.
Save lyschoening/ec27d28a0b38a7b16ac2 to your computer and use it in GitHub Desktop.
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