@ortoo/angular-resource
Version:
Magical CRUD resource management for angular
312 lines (256 loc) • 9.53 kB
text/typescript
// tslint:disable only-arrow-functions
import { EventEmitter } from './emitter';
import { JSONPatch, ModelArray, ServerModel, ServerResource as IServerResource, SocketFactory } from './types';
import { uniq } from 'lodash';
import { isFunction } from 'lodash';
import { isString } from 'lodash';
import { isUndefined } from 'lodash';
import * as utils from './utils';
export function createServerResourceFactory(socket: SocketFactory) {
function ServerResourceFactory(url: string, rootKey: string, rootKeyPlural: string): IServerResource {
let sock = socket(url, rootKey, rootKeyPlural);
// Object holding the resources (keys are the object ids)
let _resources = {};
let _internalRes = {};
function toObject() {
return utils.toObject(this);
}
// Returns the object or a list of objects (depending on ids passed in)
// The array or object will have the $promise attribute to use.
function get(ids: (string[] | string), force?: boolean, transform?: Function) {
if (isFunction(force)) {
transform = force;
force = false;
}
if (!transform) {
transform = (val: any) => { return val; };
}
let singleId = false;
let results: ModelArray<ServerModel> = [];
function transformResults() {
if (singleId) {
return results[0];
} else {
return results;
}
}
// If we've been passed
if (isString(ids)) {
singleId = true;
ids = [ids];
}
// Make sure we have no repeated items in the array
ids = uniq(ids);
// Go and load in the results, compiling a list of resources we need to go and fetch
let unknownIds: string[] = [];
ids.forEach(function(id) {
let res: ServerModel = _resources[id];
if (res) {
results.push(transform(res, id));
// If we are forcing then we want to go and refetch assuming we've already resolved
// If we haven't resolved then we are going to refresh the data anyway so don't do
// anything here. If we aren't resolved and we aren't fetching then we want to
// go and refresh the data because we could have been created from somewhere that
// isn't the server
if ((force && res.$resolved) || !(res.$resolved || res.$fetching)) {
unknownIds.push(id);
res.$fetching = true;
// If we've already resolved then create a new promise object on the resource
if (res.$resolved) {
res.$deferred = new utils.Deferred<ServerModel>();
res.$promise = this.$deferred.promise;
res.$resolved = false;
}
}
} else {
// Haven't seen this id yet. Create a new resource and store it off
res = _resources[id] = <ServerModel> (new (<any> ServerResource)());
res.$fetching = true;
results.push(transform(res, id));
unknownIds.push(id);
}
});
// Do we have any ids to fetch. If so go and get them
if (unknownIds.length > 0) {
let req = sock.get(unknownIds);
req.then(function(response: any) {
// We might not get a response (say if the app is offline). In this case
// we just resolve everything as is.
if (isUndefined(response)) {
unknownIds.forEach(function(id) {
let res = _resources[id];
res.$fetching = false;
if (!res.$resolved) {
res.$resolved = true;
res.$deferred.resolve(res);
}
});
} else {
response.forEach(function(resdata: any) {
if (resdata._id) {
let id = resdata._id;
let res = _resources[id];
res.$fetching = false;
updateVal(res, resdata);
// We've got the data for the first time - resolve the deferred
if (!res.$resolved) {
res.$deferred.resolve(res);
res.$resolved = true;
}
}
});
// If any of our unknown ids hasn't been resolved then we assume its deleted...
unknownIds.forEach(function(id) {
let res = _resources[id];
if (!res) {
return;
}
res.$fetching = false;
if (!res.$resolved) {
res.$deleted = true;
res.$emitter.emit('update', null, res.$toObject());
}
});
}
}, function(reason: any) {
// Handle an error...
// Clean up any of our unknown ids - we don't know about them
unknownIds.forEach(function(id) {
let res = _resources[id];
res.$fetching = false;
res.$deferred.reject(reason);
delete _resources[id];
});
});
}
if (!singleId) {
// Wait for all the promises to be resolved before resolving
let promises: Array<Promise<any>> = [];
results.forEach(function(res) {
promises.push(res.$promise);
});
let tmpProm = Promise.all(promises);
results.$promise = tmpProm.then(function() {
// Return the results array as we resolve
return results;
});
}
return transformResults();
}
// Perform a save
function save(this: ServerModel, patch: JSONPatch) {
utils.applyPatch(this, patch);
// We update if we have an _id - otherwise we create.
if (this._id) {
return updateResource(this, patch);
} else {
// This is an initial create
return createResource(this);
}
}
function updateResource(res: any, patch: JSONPatch) {
let req = sock.patch(res._id, patch);
return req.then(function(response: any) {
updateVal(res, response);
// Resolve the deferred
_resources[res._id] = res;
if (!res.$resolved) {
res.$deferred.resolve(res);
res.$resolved = true;
}
return res;
}).catch(function(reason: any) {
// Something went wrong - go and fetch the object again so we are up to date
if (res._id) {
res.$refresh();
}
throw reason;
});
}
function createResource(res: any) {
let data = res.$toObject();
let req = sock.create(data);
return req.then(function(response) {
// There could be a race condition here where we could end up creating the id map
// earlier (maybe...?)
updateVal(res, response);
_resources[res._id] = res;
if (!res.$resolved) {
res.$deferred.resolve(res);
res.$resolved = true;
}
});
}
function remove() {
let res = this;
res.$deleted = true;
let req = sock.remove(res._id);
return req.then(function(response) {
if (response) {
delete _resources[res._id];
} else {
res.$deleted = false;
}
return response;
});
}
// Updates the value from the server
function updateVal(res: any, val: any) {
// Do we have a null value (indicating the resource has been deleted?)
if (val) {
// Convert any JSON dates into dates
val = utils.convertJsonDates(val);
let oldData = res.$toObject();
utils.removeResValues(res);
utils.setResValues(res, val);
// Make sure we have store this resource off
_resources[res._id] = res;
res.$emitter.emit('update', res.$toObject(), oldData);
} else {
// Deleted - clean me up scotty
delete _resources[res._id];
res.$emitter.emit('update', null, res.$toObject());
}
}
function _updateVal(this: ServerModel, val: any) {
return updateVal(this, val);
}
function refresh(this: ServerModel) {
if (this._id) {
get(this._id, true);
}
}
// The main constructor for theServerResource class
function ServerResourceConstructor(val?: any, id?: string): void {
let res = this;
this.$emitter = new EventEmitter();
this.$deferred = new utils.Deferred();
this.$promise = this.$deferred.promise; // An initial promise for our initial
// fetch or create of data
this.$resolved = false; // Have we had an initial resolution of the promise
this.$deleted = false; // Has the resource been deleted
this.$fetching = false; // Are we currently fetching data for this resource
this.$id = id ? id : utils.uuid();
_internalRes[this.$id] = this;
if (val) {
for (let key of Object.keys(val)) {
res[key] = val[key];
}
}
// If we have an id then add us to the store
if (this._id) {
_resources[this._id] = this;
}
}
const ServerResource: IServerResource = ServerResourceConstructor;
ServerResource.get = get;
ServerResource.prototype.$save = save;
ServerResource.prototype.$remove = remove;
ServerResource.prototype.$delete = remove;
ServerResource.prototype.$refresh = refresh;
ServerResource.prototype.$toObject = toObject;
ServerResource.prototype.$updateVal = _updateVal;
return ServerResource;
}
return ServerResourceFactory;
}