@ortoo/angular-resource
Version:
Magical CRUD resource management for angular
268 lines (219 loc) • 7.24 kB
text/typescript
// tslint:disable max-classes-per-file
import * as jiff from 'jiff';
import * as moment from 'moment';
import { SubscribableOrPromise } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { Subscriber } from 'rxjs/Subscriber';
import { Subscription } from 'rxjs/Subscription';
import { ObjectUnsubscribedError } from 'rxjs/util/ObjectUnsubscribedError';
import { clone } from 'lodash';
import { isObject } from 'lodash';
import { isString } from 'lodash';
import { map } from 'lodash';
import { Deferred as IDeferred, JSONPatch, LocalForage } from './types';
let hiddenKeyRegex = /^\$+/;
let TIMESTAMP_RE = /^(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+)|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d)|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d)$/; // tslint:disable-line max-line-length
export function convertJsonDates(jsonData: any) {
let outObj;
if (Array.isArray(jsonData)) {
outObj = [];
} else {
outObj = {};
}
for (let key of Object.keys(jsonData)) {
let val = jsonData[key];
let res = val;
if (isString(val)) {
// The value is a string - does it match any of our things we want to convert
if (TIMESTAMP_RE.test(val)) {
// We're probably a timestamp
let dt = moment(val);
if (dt.isValid()) {
res = dt.toDate();
}
}
} else if (Array.isArray(val) || isObject(val)) {
res = convertJsonDates(val);
}
outObj[key] = res;
}
return outObj;
}
export function toJSON(obj: any) {
return JSON.parse(JSON.stringify(obj));
}
export function diff(obj1: any, obj2: any) {
obj1 = toJSON(obj1);
obj2 = toJSON(obj2);
return jiff.diff(obj1, obj2);
}
export function applyPatch(res: any, patch: JSONPatch) {
// Go to JSON - don't do anything with dates
let obj = JSON.parse(JSON.stringify(res, toJsonReplacer));
obj = jiff.patch(patch, obj);
// Go back to dates etc
obj = convertJsonDates(obj);
removeResValues(res);
setResValues(res, obj);
}
export function mergeObjects(mine: any, old: any, yours: any) {
// Make copies - we might modify the objects if ids dont exist
mine = clone(mine);
old = clone(old);
// First sort out _id's. We could have a new id if one doesn't exist on mine and old
if (!mine._id && !old._id && yours._id) {
mine._id = yours._id;
old._id = yours._id;
}
// We also need to convert into and out of dates
mine = toJSON(mine);
old = toJSON(old);
yours = toJSON(yours);
let yourpatch = jiff.diff(old, yours);
let mypatch = jiff.diff(old, mine);
let mypaths = map(mypatch, 'path');
let patch = yourpatch.filter((patchval: any) => {
return mypaths.indexOf(patchval.path) === -1;
});
let patched = jiff.patch(patch, mine);
return convertJsonDates(patched);
}
export function forEachVal(res: any, cb: Function) {
for (let key in res) {
if (!hiddenKeyRegex.test(key)) {
cb(res, key);
}
}
}
export function toJsonReplacer(key: any, value: any) {
let val = value;
if (isString(key) && key.charAt(0) === '$') {
val = undefined;
}
return val;
}
export function fromJsonReviver(_: any, value: any) {
let val = value;
if (isString(value) && TIMESTAMP_RE.test(value)) {
let dt = moment(value);
if (dt.isValid()) {
val = dt.toDate();
}
}
return val;
}
export function toObject(res: any) {
// The toJsonReplacer gets rid of attributes beginning with $ and the fromJsonReviver
// converts date strings back into dates
let str = JSON.stringify(res, toJsonReplacer);
try {
return JSON.parse(str, fromJsonReviver);
} catch (err) {
// Older versions of IE8 throw 'Out of stack space' errors if we use a reviver function
// due to the bug described here: http://support.microsoft.com/kb/976662. Everyone hates
// IE
if (err.message === 'Out of stack space') {
return convertJsonDates(JSON.parse(str));
}
throw(err);
}
}
export function removeResValues(res: any) {
forEachVal(res, (_: any, key: any) => {
delete res[key];
});
}
export function setResValues(res: any, vals: any) {
for (let key of Object.keys(vals)) {
res[key] = vals[key];
}
}
export function persistentStorageKey(url: string) {
return 'or2ws:' + url;
}
export function advancedStorage(localForage: LocalForage) {
return localForage.driver() !== 'localStorageWrapper';
}
// lifted from here -> http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523
export function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
let r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); // tslint:disable-line
return v.toString(16);
});
}
export class Deferred<T> implements IDeferred<T> {
public promise: Promise<T>;
public resolve: (val?: T) => void;
public reject: (err: Error) => void;
constructor() {
/* A method to resolve the associated Promise with the value passed.
* If the promise is already settled it does nothing.
*
* @param {anything} value : This value is used to resolve the promise
* If the value is a Promise then the associated promise assumes the state
* of Promise passed as value.
*/
this.resolve = null;
/* A method to reject the assocaited Promise with the value passed.
* If the promise is already settled it does nothing.
*
* @param {anything} reason: The reason for the rejection of the Promise.
* Generally its an Error object. If however a Promise is passed, then the Promise
* itself will be the reason for rejection no matter the state of the Promise.
*/
this.reject = null;
/* A newly created Pomise object.
* Initially in pending state.
*/
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
Object.freeze(this);
}
}
export function isSubscribableOrPromise<T>(value: any): value is SubscribableOrPromise<T> {
return value && (typeof value.subscribe === 'function' || typeof value.then === 'function');
}
// Like BehaviorSubject but waits for a promise to resolve
export class ResolvedSubject<T> extends Subject<T> {
private _value: T;
private _resolved: boolean;
constructor(promise: Promise<T>) {
super();
promise.then(val => {
this._resolved = true;
this.next(val);
});
}
public get value() {
return this.getValue();
}
public getValue() {
if (this.hasError) {
throw this.thrownError;
} else if (this.closed) {
throw new ObjectUnsubscribedError();
} else {
return this._value;
}
}
public next(value: T) {
if (this._resolved) {
this._value = value;
super.next(value);
}
}
protected _subscribe(subscriber: Subscriber<T>): Subscription {
const subscription = super._subscribe(subscriber);
if (this._resolved && subscription && !subscription.closed) {
subscriber.next(this._value);
}
return subscription;
}
}
export function convertToResolvedSubject<T>(subject: Subject<T>, promise: Promise<T>) {
let resolvedSub = new ResolvedSubject<T>(promise);
subject.subscribe(resolvedSub);
return resolvedSub;
}