From 82225b7cf9a0f8950cdeeedda5197d5c4d9267d9 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Wed, 15 Jan 2014 22:38:09 -0700 Subject: [PATCH] Closes #7. Implemented all lifecycle hooks, including tests. Cleaned up use of promises in async methods. --- dist/angular-data.js | 155 ++++++++++-------- dist/angular-data.min.js | 4 +- karma.start.js | 73 ++++++++- src/datastore/async_methods/destroy/index.js | 37 +++-- src/datastore/async_methods/find/index.js | 53 +++--- src/datastore/async_methods/findAll/index.js | 11 +- src/datastore/async_methods/refresh/index.js | 5 +- src/datastore/async_methods/save/index.js | 2 +- src/datastore/index.js | 19 ++- .../sync_methods/defineResource/index.js | 19 ++- src/datastore/sync_methods/get/index.js | 4 +- src/utils/index.js | 1 + .../async_methods/create/index.test.js | 41 +++++ .../async_methods/destroy/index.test.js | 45 +++++ .../async_methods/find/index.test.js | 82 +++++++++ .../async_methods/save/index.test.js | 69 ++++++++ .../datastore/sync_methods/get/index.test.js | 47 ++++++ 17 files changed, 525 insertions(+), 142 deletions(-) create mode 100644 test/unit/datastore/async_methods/create/index.test.js create mode 100644 test/unit/datastore/async_methods/destroy/index.test.js create mode 100644 test/unit/datastore/async_methods/find/index.test.js create mode 100644 test/unit/datastore/async_methods/save/index.test.js create mode 100644 test/unit/datastore/sync_methods/get/index.test.js diff --git a/dist/angular-data.js b/dist/angular-data.js index 5bd6e01..99dc5b2 100644 --- a/dist/angular-data.js +++ b/dist/angular-data.js @@ -1,12 +1,3 @@ -/** - * @author Jason Dobry - * @file angular-data.js - * @version 0.5.0 - Homepage - * @copyright (c) 2014 Jason Dobry - * @license MIT - * - * @overview Data store for Angular.js. - */ require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;oc?d+c:c;d>e;){if(a[e]===b)return e;e++}return-1}b.exports=c},{}],4:[function(a,b){function c(a){return null!=a&&""!==a}function d(a,b){return b=b||"",e(a,c).join(b)}var e=a("./filter");b.exports=d},{"./filter":2}],5:[function(a,b){function c(a,b,c){return d.call(a,b,c)}var d=Array.prototype.slice;b.exports=c},{}],6:[function(a,b){function c(a,b){if(null==a)return[];if(a.length<2)return a;null==b&&(b=d);var f,g,h;return f=~~(a.length/2),g=c(a.slice(0,f),b),h=c(a.slice(f,a.length),b),e(g,h,b)}function d(a,b){return b>a?-1:a>b?1:0}function e(a,b,c){for(var d=[];a.length&&b.length;)c(a[0],b[0])<=0?d.push(a.shift()):d.push(b.shift());return a.length&&d.push.apply(d,a),b.length&&d.push.apply(d,b),d}b.exports=c},{}],7:[function(a,b){function c(a,b){var c={};if(null==a)return c;var e,f=-1,g=a.length;if(d(b))for(;++f>>0}function d(a){return+a}function e(a){return a===Object(a)}function f(a,b){return a===b?0!==a||1/a===1/b:K(a)&&K(b)?!0:a!==a&&b!==b}function g(a){return"string"!=typeof a?!1:(a=a.replace(/\s/g,""),""==a?!0:"."==a[0]?!1:S.test(a))}function h(a){var b=T[a];if(b)return b;if(g(a)){var b=new i(a);return T[a]=b,b}}function i(a){return""==a.trim()?this:c(a)?(this.push(String(a)),this):(a.split(/\./).filter(function(a){return a}).forEach(function(a){this.push(a)},this),H&&this.length&&(this.getValueFrom=this.compiledGetValueFromFn()),void 0)}function j(a){for(var b=0;U>b&&a.check();)a.report(),b++}function k(a){for(var b in a)return!1;return!0}function l(a){return k(a.added)&&k(a.removed)&&k(a.changed)}function m(a,b){var c={},d={},e={};for(var f in b){var g=a[f];(void 0===g||g!==b[f])&&(f in a?g!==b[f]&&(e[f]=g):d[f]=void 0)}for(var f in a)f in b||(c[f]=a[f]);return Array.isArray(a)&&a.length!==b.length&&(e.length=a.length),{added:c,removed:d,changed:e}}function n(a,b){var c=b||(Array.isArray(a)?[]:{});for(var d in a)c[d]=a[d];return Array.isArray(a)&&(c.length=a.length),c}function o(a,b,c,d){if(this.closed=!1,this.object=a,this.callback=b,this.target=c,this.token=d,this.reporting=!0,G){var e=this;this.boundInternalCallback=function(a){e.internalCallback(a)}}p(this),this.connect(),this.sync(!0)}function p(a){W&&(V.push(a),o._allObserversCount++)}function q(a,b,c,d){o.call(this,a,b,c,d)}function r(a,b,c,d){if(!Array.isArray(a))throw Error("Provided object is not an Array");o.call(this,a,b,c,d)}function s(a){this.arr=[],this.callback=a,this.isObserved=!0}function t(a,b,c,d,f){this.value=void 0;var g=h(b);return g?g.length?e(a)?(this.path=g,o.call(this,a,c,d,f),void 0):(this.closed=!0,this.value=void 0,void 0):(this.closed=!0,this.value=a,void 0):(this.closed=!0,this.value=void 0,void 0)}function u(a,b){if("function"==typeof Object.observe){var c=Object.getNotifier(a);return function(d,e){var f={object:a,type:d,name:b};2===arguments.length&&(f.oldValue=e),c.notify(f)}}}function v(a,b,c){for(var d={},e={},f=0;fj;j++)i[j]=new Array(h),i[j][0]=j;for(var k=0;h>k;k++)i[0][k]=k;for(var j=1;g>j;j++)for(var k=1;h>k;k++)if(d[e+j-1]===a[b+k-1])i[j][k]=i[j-1][k-1];else{var l=i[j-1][k]+1,m=i[j][k-1]+1;i[j][k]=m>l?l:m}return i}function x(a){for(var b=a.length-1,c=a[0].length-1,d=a[b][c],e=[];b>0||c>0;)if(0!=b)if(0!=c){var f,g=a[b-1][c-1],h=a[b-1][c],i=a[b][c-1];f=i>h?g>h?h:g:g>i?i:g,f==g?(g==d?e.push(ab):(e.push(bb),d=g),b--,c--):f==h?(e.push(db),b--,d=h):(e.push(cb),c--,d=i)}else e.push(db),b--;else e.push(cb),c--;return e.reverse(),e}function y(a,b,c){for(var d=0;c>d;d++)if(a[d]!==b[d])return d;return c}function z(a,b,c){for(var d=a.length,e=b.length,f=0;c>f&&a[--d]===b[--e];)f++;return f}function A(a,b,c){return{index:a,removed:b,addedCount:c}}function B(a,b,c,d,e,f){var g=0,h=0,i=Math.min(c-b,f-e);if(0==b&&0==e&&(g=y(a,d,i)),c==a.length&&f==d.length&&(h=z(a,d,i-g)),b+=g,e+=g,c-=h,f-=h,c-b==0&&f-e==0)return[];if(b==c){for(var j=A(b,[],0);f>e;)j.removed.push(d[e++]);return[j]}if(e==f)return[A(b,[],c-b)];for(var k=x(w(a,b,c,d,e,f)),j=void 0,l=[],m=b,n=e,o=0;ob||a>d?-1:b==c||d==a?0:c>a?d>b?b-c:d-c:b>d?d-a:b-a}function D(a,b,c,d){for(var e=A(b,c,d),f=!1,g=0,h=0;h=0){a.splice(h,1),h--,g-=i.addedCount-i.removed.length,e.addedCount+=i.addedCount-j;var k=e.removed.length+i.removed.length-j;if(e.addedCount||k){var c=i.removed;if(e.indexi.index+i.addedCount){var m=e.removed.slice(i.index+i.addedCount-e.index);Array.prototype.push.apply(c,m)}e.removed=c,i.indexh)continue;D(e,h,[g.oldValue],1);break;default:console.error("Unexpected record type: "+JSON.stringify(g))}}return e}function F(a,b){var c=[];return E(a,b).forEach(function(b){return 1==b.addedCount&&1==b.removed.length?(b.removed[0]!==a[b.index]&&c.push(b),void 0):(c=c.concat(B(a,b.index,b.index+b.addedCount,b.removed,0,b.removed.length)),void 0)}),c}var G=b(),H=!1;try{var I=new Function("","return true;");H=I()}catch(J){}var K=a.Number.isNaN||function(b){return"number"==typeof b&&a.isNaN(b)},L="__proto__"in{}?function(a){return a}:function(a){var b=a.__proto__;if(!b)return a;var c=Object.create(b);return Object.getOwnPropertyNames(a).forEach(function(b){Object.defineProperty(c,b,Object.getOwnPropertyDescriptor(a,b))}),c},M="[$_a-zA-Z]",N="[$_a-zA-Z0-9]",O=M+"+"+N+"*",P="(?:[0-9]|[1-9]+[0-9]+)",Q="(?:"+O+"|"+P+")",R="(?:"+Q+")(?:\\."+Q+")*",S=new RegExp("^"+R+"$"),T={};i.prototype=L({__proto__:[],toString:function(){return this.join(".")},getValueFrom:function(a){for(var b=0;ba&&b.anyChanged);o._allObserversCount=V.length,X=!1}}},W&&(a.Platform.clearObservers=function(){V=[]}),q.prototype=L({__proto__:o.prototype,connect:function(){G&&Object.observe(this.object,this.boundInternalCallback)},sync:function(){G||(this.oldObject=n(this.object))},check:function(a){var b,c;if(G){if(!a)return!1;c={},b=v(this.object,a,c)}else c=this.oldObject,b=m(this.object,this.oldObject);return l(b)?!1:(this.reportArgs=[b.added||{},b.removed||{},b.changed||{}],this.reportArgs.push(function(a){return c[a]}),!0)},disconnect:function(){G?this.object&&Object.unobserve(this.object,this.boundInternalCallback):this.oldObject=void 0}}),r.prototype=L({__proto__:q.prototype,connect:function(){G&&Array.observe(this.object,this.boundInternalCallback)},sync:function(){G||(this.oldObject=this.object.slice())},check:function(a){var b;if(G){if(!a)return!1;b=F(this.object,a)}else b=B(this.object,0,this.object.length,this.oldObject,0,this.oldObject.length);return b&&b.length?(this.reportArgs=[b],!0):!1}}),r.applySplices=function(a,b,c){c.forEach(function(c){for(var d=[c.index,c.removed.length],e=c.index;e=0&&this.arr[b+1]===this.isObserved||(0>b&&(b=this.arr.length,this.arr[b]=a,Object.observe(a,this.callback)),this.arr[b+1]=this.isObserved,this.observe(Object.getPrototypeOf(a)))}},cleanup:function(){for(var a=0,b=0,c=this.isObserved;ba&&(this.arr[a]=d,this.arr[a+1]=c),a+=2):Object.unobserve(d,this.callback),b+=2}this.arr.length=a}},t.prototype=L({__proto__:o.prototype,connect:function(){G&&(this.observedSet=new s(this.boundInternalCallback))},disconnect:function(){this.value=void 0,G&&(this.observedSet.reset(),this.observedSet.cleanup(),this.observedSet=void 0)},check:function(){return this.value=G?this.path.getValueFromObserved(this.object,this.observedSet):this.path.getValueFrom(this.object),f(this.value,this.oldValue)?!1:(this.reportArgs=[this.value,this.oldValue],!0)},sync:function(a){a&&(this.value=G?this.path.getValueFromObserved(this.object,this.observedSet):this.path.getValueFrom(this.object)),this.oldValue=this.value}}),t.getValueAtPath=function(a,b){var c=h(b);if(c)return c.getValueFrom(a)},t.setValueAtPath=function(a,b,c){var d=h(b);d&&d.setValueFrom(a,c)};var _={"new":!0,updated:!0,deleted:!0};t.defineProperty=function(a,b,c){var d=c.object,e=h(c.path),f=u(a,b),g=new t(d,c.path,function(a,b){f&&f("updated",b)});return Object.defineProperty(a,b,{get:function(){return e.getValueFrom(d)},set:function(a){e.setValueFrom(d,a)},configurable:!0}),{close:function(){var c=e.getValueFrom(d);f&&g.deliver(),g.close(),Object.defineProperty(a,b,{value:c,writable:!0,configurable:!0})}}};var ab=0,bb=1,cb=2,db=3;a.Observer=o,a.Observer.hasObjectObserve=G,a.ArrayObserver=r,a.ArrayObserver.calculateSplices=function(a,b){return B(a,0,a.length,b,0,b.length)},a.ObjectObserver=q,a.PathObserver=t,a.Path=i}((c.Number={isNaN:window.isNaN})?c:c)},{}],27:[function(a,b){function c(a,b){var c=f.$q.defer(),h=c.promise;if(f.store[a])if(d.isObject(b))try{var i=f.store[a],j=this;h=h.then(function(b){return f.$q.promisify(i.beforeValidate)(a,b)}).then(function(b){return f.$q.promisify(i.validate)(a,b)}).then(function(b){return f.$q.promisify(i.afterValidate)(a,b)}).then(function(b){return f.$q.promisify(i.beforeCreate)(a,b)}).then(function(a){return j.POST(d.makePath(i.baseUrl,i.endpoint),a,null)}).then(function(b){return f.$q.promisify(i.afterCreate)(a,b)}).then(function(a){return j.inject(i.name,a)}),c.resolve(b)}catch(k){c.reject(new e.UnhandledError(k))}else c.reject(new e.IllegalArgumentError(g+"attrs: Must be an object!",{attrs:{actual:typeof b,expected:"object"}}));else c.reject(new e.RuntimeError(g+a+" is not a registered resource!"));return h}var d=a("utils"),e=a("errors"),f=a("services"),g="DS.create(resourceName, attrs): ";b.exports=c},{errors:"hIh4e1",services:"cX8q+p",utils:"uE/lJt"}],28:[function(a,b){function c(a,b){var c=service.$q.defer();f.store[a]?d.isString(b)||d.isNumber(b)||c.reject(new e.IllegalArgumentError(g+"id: Must be a string or a number!",{id:{actual:typeof b,expected:"string|number"}})):c.reject(new e.RuntimeError(g+a+" is not a registered resource!"));try{var h=f.store[a],i=this,j=d.makePath(h.baseUrl||f.config.baseUrl,h.endpoint||h.name,b);i.DEL(j,null).then(function(){try{i.eject(a,b),c.resolve(b)}catch(d){c.reject(d)}},c.reject)}catch(k){c.reject(new e.UnhandledError(k))}return c.promise}var d=a("utils"),e=a("errors"),f=a("services"),g="DS.destroy(resourceName, id): ";b.exports=c},{errors:"hIh4e1",services:"cX8q+p",utils:"uE/lJt"}],29:[function(a,b){function c(a,b,c){var i=f.$q.defer();if(c=c||{},f.store[a])if(d.isString(b)||d.isNumber(b))if(d.isObject(c)){var j=this;try{var k=f.store[a];if(b in k.index&&!c.bypassCache)i.resolve(j.get(a,b));else{var l=d.makePath(k.baseUrl||f.config.baseUrl,k.endpoint||k.name,b),m=null;c.bypassCache&&(m={headers:{"Last-Modified":new Date(k.modified[b])}}),g(l,m).then(function(c){try{j.inject(a,c),i.resolve(j.get(a,b))}catch(d){i.reject(d)}},i.reject)}}catch(n){n instanceof e.UnhandledError?i.reject(n):i.reject(new e.UnhandledError(n))}}else i.reject(new e.IllegalArgumentError(h+"options: Must be an object!",{options:{actual:typeof c,expected:"object"}}));else i.reject(new e.IllegalArgumentError(h+"id: Must be a string or a number!",{id:{actual:typeof b,expected:"string|number"}}));else i.reject(new e.RuntimeError(h+a+" is not a registered resource!"));return i.promise}var d=a("utils"),e=a("errors"),f=a("services"),g=a("../../http").GET,h="DS.find(resourceName, id[, options]): ";b.exports=c},{"../../http":34,errors:"hIh4e1",services:"cX8q+p",utils:"uE/lJt"}],30:[function(a,b){function c(a,b,c){var d=h.store[b];a=a||[],delete d.pendingQueries[c],d.completedQueries[c]=(new Date).getTime();for(var e=0;e"in b?c=c&&a[e]>b[">"]:">="in b?c=c&&a[e]>=b[">="]:"<"in b?c=c&&a[e]f?-1:f>e?1:0:f>e?-1:e>f?1:0})}}return d.isNumber(b.query.limit)&&d.isNumber(b.query.skip)?j=d.slice(j,b.query.skip,b.query.skip+b.query.limit):d.isNumber(b.query.limit)?j=d.slice(j,0,b.query.limit):d.isNumber(b.query.skip)&&(j=d.slice(j,b.query.skip)),j}catch(l){throw l instanceof e.IllegalArgumentError?l:new e.UnhandledError(l)}}var d=a("utils"),e=a("errors"),f=a("services"),g="DS.filter(resourceName, params[, options]): ";b.exports=c},{errors:"hIh4e1",services:"cX8q+p",utils:"uE/lJt"}],43:[function(a,b){function c(a,b,c){if(c=c||{},!f.store[a])throw new e.RuntimeError(g+a+" is not a registered resource!");if(!d.isString(b)&&!d.isNumber(b))throw new e.IllegalArgumentError(g+"id: Must be a string or a number!",{id:{actual:typeof b,expected:"string|number"}});if(!d.isObject(c))throw new e.IllegalArgumentError(g+"options: Must be an object!",{options:{actual:typeof c,expected:"object"}});try{return b in f.store[a].index||!c.loadFromServer||this.find(a,b),f.store[a].index[b]}catch(h){throw new e.UnhandledError(h)}}var d=a("utils"),e=a("errors"),f=a("services"),g="DS.get(resourceName, id[, options]): ";b.exports=c},{errors:"hIh4e1",services:"cX8q+p",utils:"uE/lJt"}],44:[function(a,b){function c(a){return e.isEmpty(a.added)&&e.isEmpty(a.removed)&&e.isEmpty(a.changed)}function d(a,b){if(!g.store[a])throw new f.RuntimeError(h+a+" is not a registered resource!");if(!e.isString(b)&&!e.isNumber(b))throw new f.IllegalArgumentError(h+"id: Must be a string or a number!",{id:{actual:typeof b,expected:"string|number"}});try{return c(g.store[a].changes[b])}catch(d){throw new f.UnhandledError(d)}}var e=a("utils"),f=a("errors"),g=a("services"),h="DS.hasChanges(resourceName, id): ";b.exports=d},{errors:"hIh4e1",services:"cX8q+p",utils:"uE/lJt"}],45:[function(a,b){b.exports={defineResource:a("./defineResource"),eject:a("./eject"),filter:a("./filter"),get:a("./get"),inject:a("./inject"),lastModified:a("./lastModified"),lastSaved:a("./lastSaved"),digest:a("./digest"),changes:a("./changes"),previous:a("./previous"),hasChanges:a("./hasChanges")}},{"./changes":38,"./defineResource":39,"./digest":40,"./eject":41,"./filter":42,"./get":43,"./hasChanges":44,"./inject":46,"./lastModified":47,"./lastSaved":48,"./previous":49}],46:[function(a,b){function c(a,b){var d=this;if(e.isArray(b))for(var i=0;i=b?a+1:b},deepFreeze:function c(a){if("function"==typeof Object.freeze){var b,d;Object.freeze(a);for(d in a)b=a[d],a.hasOwnProperty(d)&&"object"==typeof b&&!Object.isFrozen(b)&&c(b)}},diffObjectFromOldObject:function(a,b){var c={},d={},e={};for(var f in b){var g=a[f];(void 0===g||g!==b[f])&&(f in a?g!==b[f]&&(e[f]=g):d[f]=void 0)}for(var h in a)h in b||(c[h]=a[h]);return{added:c,removed:d,changed:e}}}},{"mout/array/contains":1,"mout/array/filter":2,"mout/array/slice":5,"mout/array/sort":6,"mout/array/toLookup":7,"mout/lang/isEmpty":12,"mout/object/deepMixIn":19,"mout/object/forOwn":21,"mout/string/makePath":23,"mout/string/upperCase":24}],utils:[function(a,b){b.exports=a("uE/lJt")},{}]},{},[52]); \ No newline at end of file +require=function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};b[g][0].call(j.exports,function(a){var c=b[g][1][a];return e(c?c:a)},j,j.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;gc?d+c:c;d>e;){if(a[e]===b)return e;e++}return-1}b.exports=c},{}],4:[function(a,b){function c(a){return null!=a&&""!==a}function d(a,b){return b=b||"",e(a,c).join(b)}var e=a("./filter");b.exports=d},{"./filter":2}],5:[function(a,b){function c(a,b,c){return d.call(a,b,c)}var d=Array.prototype.slice;b.exports=c},{}],6:[function(a,b){function c(a,b){if(null==a)return[];if(a.length<2)return a;null==b&&(b=d);var f,g,h;return f=~~(a.length/2),g=c(a.slice(0,f),b),h=c(a.slice(f,a.length),b),e(g,h,b)}function d(a,b){return b>a?-1:a>b?1:0}function e(a,b,c){for(var d=[];a.length&&b.length;)c(a[0],b[0])<=0?d.push(a.shift()):d.push(b.shift());return a.length&&d.push.apply(d,a),b.length&&d.push.apply(d,b),d}b.exports=c},{}],7:[function(a,b){function c(a,b){var c={};if(null==a)return c;var e,f=-1,g=a.length;if(d(b))for(;++f>>0}function d(a){return+a}function e(a){return a===Object(a)}function f(a,b){return a===b?0!==a||1/a===1/b:K(a)&&K(b)?!0:a!==a&&b!==b}function g(a){return"string"!=typeof a?!1:(a=a.replace(/\s/g,""),""==a?!0:"."==a[0]?!1:S.test(a))}function h(a){var b=T[a];if(b)return b;if(g(a)){var b=new i(a);return T[a]=b,b}}function i(a){return""==a.trim()?this:c(a)?(this.push(String(a)),this):(a.split(/\./).filter(function(a){return a}).forEach(function(a){this.push(a)},this),H&&this.length&&(this.getValueFrom=this.compiledGetValueFromFn()),void 0)}function j(a){for(var b=0;U>b&&a.check();)a.report(),b++}function k(a){for(var b in a)return!1;return!0}function l(a){return k(a.added)&&k(a.removed)&&k(a.changed)}function m(a,b){var c={},d={},e={};for(var f in b){var g=a[f];(void 0===g||g!==b[f])&&(f in a?g!==b[f]&&(e[f]=g):d[f]=void 0)}for(var f in a)f in b||(c[f]=a[f]);return Array.isArray(a)&&a.length!==b.length&&(e.length=a.length),{added:c,removed:d,changed:e}}function n(a,b){var c=b||(Array.isArray(a)?[]:{});for(var d in a)c[d]=a[d];return Array.isArray(a)&&(c.length=a.length),c}function o(a,b,c,d){if(this.closed=!1,this.object=a,this.callback=b,this.target=c,this.token=d,this.reporting=!0,G){var e=this;this.boundInternalCallback=function(a){e.internalCallback(a)}}p(this),this.connect(),this.sync(!0)}function p(a){W&&(V.push(a),o._allObserversCount++)}function q(a,b,c,d){o.call(this,a,b,c,d)}function r(a,b,c,d){if(!Array.isArray(a))throw Error("Provided object is not an Array");o.call(this,a,b,c,d)}function s(a){this.arr=[],this.callback=a,this.isObserved=!0}function t(a,b,c,d,f){this.value=void 0;var g=h(b);return g?g.length?e(a)?(this.path=g,o.call(this,a,c,d,f),void 0):(this.closed=!0,this.value=void 0,void 0):(this.closed=!0,this.value=a,void 0):(this.closed=!0,this.value=void 0,void 0)}function u(a,b){if("function"==typeof Object.observe){var c=Object.getNotifier(a);return function(d,e){var f={object:a,type:d,name:b};2===arguments.length&&(f.oldValue=e),c.notify(f)}}}function v(a,b,c){for(var d={},e={},f=0;fj;j++)i[j]=new Array(h),i[j][0]=j;for(var k=0;h>k;k++)i[0][k]=k;for(var j=1;g>j;j++)for(var k=1;h>k;k++)if(d[e+j-1]===a[b+k-1])i[j][k]=i[j-1][k-1];else{var l=i[j-1][k]+1,m=i[j][k-1]+1;i[j][k]=m>l?l:m}return i}function x(a){for(var b=a.length-1,c=a[0].length-1,d=a[b][c],e=[];b>0||c>0;)if(0!=b)if(0!=c){var f,g=a[b-1][c-1],h=a[b-1][c],i=a[b][c-1];f=i>h?g>h?h:g:g>i?i:g,f==g?(g==d?e.push(ab):(e.push(bb),d=g),b--,c--):f==h?(e.push(db),b--,d=h):(e.push(cb),c--,d=i)}else e.push(db),b--;else e.push(cb),c--;return e.reverse(),e}function y(a,b,c){for(var d=0;c>d;d++)if(a[d]!==b[d])return d;return c}function z(a,b,c){for(var d=a.length,e=b.length,f=0;c>f&&a[--d]===b[--e];)f++;return f}function A(a,b,c){return{index:a,removed:b,addedCount:c}}function B(a,b,c,d,e,f){var g=0,h=0,i=Math.min(c-b,f-e);if(0==b&&0==e&&(g=y(a,d,i)),c==a.length&&f==d.length&&(h=z(a,d,i-g)),b+=g,e+=g,c-=h,f-=h,c-b==0&&f-e==0)return[];if(b==c){for(var j=A(b,[],0);f>e;)j.removed.push(d[e++]);return[j]}if(e==f)return[A(b,[],c-b)];for(var k=x(w(a,b,c,d,e,f)),j=void 0,l=[],m=b,n=e,o=0;ob||a>d?-1:b==c||d==a?0:c>a?d>b?b-c:d-c:b>d?d-a:b-a}function D(a,b,c,d){for(var e=A(b,c,d),f=!1,g=0,h=0;h=0){a.splice(h,1),h--,g-=i.addedCount-i.removed.length,e.addedCount+=i.addedCount-j;var k=e.removed.length+i.removed.length-j;if(e.addedCount||k){var c=i.removed;if(e.indexi.index+i.addedCount){var m=e.removed.slice(i.index+i.addedCount-e.index);Array.prototype.push.apply(c,m)}e.removed=c,i.indexh)continue;D(e,h,[g.oldValue],1);break;default:console.error("Unexpected record type: "+JSON.stringify(g))}}return e}function F(a,b){var c=[];return E(a,b).forEach(function(b){return 1==b.addedCount&&1==b.removed.length?(b.removed[0]!==a[b.index]&&c.push(b),void 0):(c=c.concat(B(a,b.index,b.index+b.addedCount,b.removed,0,b.removed.length)),void 0)}),c}var G=b(),H=!1;try{var I=new Function("","return true;");H=I()}catch(J){}var K=a.Number.isNaN||function(b){return"number"==typeof b&&a.isNaN(b)},L="__proto__"in{}?function(a){return a}:function(a){var b=a.__proto__;if(!b)return a;var c=Object.create(b);return Object.getOwnPropertyNames(a).forEach(function(b){Object.defineProperty(c,b,Object.getOwnPropertyDescriptor(a,b))}),c},M="[$_a-zA-Z]",N="[$_a-zA-Z0-9]",O=M+"+"+N+"*",P="(?:[0-9]|[1-9]+[0-9]+)",Q="(?:"+O+"|"+P+")",R="(?:"+Q+")(?:\\."+Q+")*",S=new RegExp("^"+R+"$"),T={};i.prototype=L({__proto__:[],toString:function(){return this.join(".")},getValueFrom:function(a){for(var b=0;ba&&b.anyChanged);o._allObserversCount=V.length,X=!1}}},W&&(a.Platform.clearObservers=function(){V=[]}),q.prototype=L({__proto__:o.prototype,connect:function(){G&&Object.observe(this.object,this.boundInternalCallback)},sync:function(){G||(this.oldObject=n(this.object))},check:function(a){var b,c;if(G){if(!a)return!1;c={},b=v(this.object,a,c)}else c=this.oldObject,b=m(this.object,this.oldObject);return l(b)?!1:(this.reportArgs=[b.added||{},b.removed||{},b.changed||{}],this.reportArgs.push(function(a){return c[a]}),!0)},disconnect:function(){G?this.object&&Object.unobserve(this.object,this.boundInternalCallback):this.oldObject=void 0}}),r.prototype=L({__proto__:q.prototype,connect:function(){G&&Array.observe(this.object,this.boundInternalCallback)},sync:function(){G||(this.oldObject=this.object.slice())},check:function(a){var b;if(G){if(!a)return!1;b=F(this.object,a)}else b=B(this.object,0,this.object.length,this.oldObject,0,this.oldObject.length);return b&&b.length?(this.reportArgs=[b],!0):!1}}),r.applySplices=function(a,b,c){c.forEach(function(c){for(var d=[c.index,c.removed.length],e=c.index;e=0&&this.arr[b+1]===this.isObserved||(0>b&&(b=this.arr.length,this.arr[b]=a,Object.observe(a,this.callback)),this.arr[b+1]=this.isObserved,this.observe(Object.getPrototypeOf(a)))}},cleanup:function(){for(var a=0,b=0,c=this.isObserved;ba&&(this.arr[a]=d,this.arr[a+1]=c),a+=2):Object.unobserve(d,this.callback),b+=2}this.arr.length=a}},t.prototype=L({__proto__:o.prototype,connect:function(){G&&(this.observedSet=new s(this.boundInternalCallback))},disconnect:function(){this.value=void 0,G&&(this.observedSet.reset(),this.observedSet.cleanup(),this.observedSet=void 0)},check:function(){return this.value=G?this.path.getValueFromObserved(this.object,this.observedSet):this.path.getValueFrom(this.object),f(this.value,this.oldValue)?!1:(this.reportArgs=[this.value,this.oldValue],!0)},sync:function(a){a&&(this.value=G?this.path.getValueFromObserved(this.object,this.observedSet):this.path.getValueFrom(this.object)),this.oldValue=this.value}}),t.getValueAtPath=function(a,b){var c=h(b);if(c)return c.getValueFrom(a)},t.setValueAtPath=function(a,b,c){var d=h(b);d&&d.setValueFrom(a,c)};var _={"new":!0,updated:!0,deleted:!0};t.defineProperty=function(a,b,c){var d=c.object,e=h(c.path),f=u(a,b),g=new t(d,c.path,function(a,b){f&&f("updated",b)});return Object.defineProperty(a,b,{get:function(){return e.getValueFrom(d)},set:function(a){e.setValueFrom(d,a)},configurable:!0}),{close:function(){var c=e.getValueFrom(d);f&&g.deliver(),g.close(),Object.defineProperty(a,b,{value:c,writable:!0,configurable:!0})}}};var ab=0,bb=1,cb=2,db=3;a.Observer=o,a.Observer.hasObjectObserve=G,a.ArrayObserver=r,a.ArrayObserver.calculateSplices=function(a,b){return B(a,0,a.length,b,0,b.length)},a.ObjectObserver=q,a.PathObserver=t,a.Path=i}((c.Number={isNaN:window.isNaN})?c:c)},{}],27:[function(a,b){function c(a,b){var c=f.$q.defer(),h=c.promise;if(f.store[a])if(d.isObject(b))try{var i=f.store[a],j=this;h=h.then(function(b){return f.$q.promisify(i.beforeValidate)(a,b)}).then(function(b){return f.$q.promisify(i.validate)(a,b)}).then(function(b){return f.$q.promisify(i.afterValidate)(a,b)}).then(function(b){return f.$q.promisify(i.beforeCreate)(a,b)}).then(function(a){return j.POST(d.makePath(i.baseUrl,i.endpoint),a,null)}).then(function(b){return f.$q.promisify(i.afterCreate)(a,b)}).then(function(a){return j.inject(i.name,a)}),c.resolve(b)}catch(k){c.reject(new e.UnhandledError(k))}else c.reject(new e.IllegalArgumentError(g+"attrs: Must be an object!",{attrs:{actual:typeof b,expected:"object"}}));else c.reject(new e.RuntimeError(g+a+" is not a registered resource!"));return h}var d=a("utils"),e=a("errors"),f=a("services"),g="DS.create(resourceName, attrs): ";b.exports=c},{errors:"hIh4e1",services:"cX8q+p",utils:"uE/lJt"}],28:[function(a,b){function c(a,b){var c=f.$q.defer(),h=c.promise;if(f.store[a])if(d.isString(b)||d.isNumber(b)){var i=f.store[a],j=this;h=h.then(function(b){return f.$q.promisify(i.beforeDestroy)(a,b)}).then(function(){return j.DEL(d.makePath(i.baseUrl,i.endpoint,b),null)}).then(function(){return f.$q.promisify(i.afterDestroy)(a,i.index[b])}).then(function(){return j.eject(a,b),b}),c.resolve(i.index[b])}else c.reject(new e.IllegalArgumentError(g+"id: Must be a string or a number!",{id:{actual:typeof b,expected:"string|number"}}));else c.reject(new e.RuntimeError(g+a+" is not a registered resource!"));return h}var d=a("utils"),e=a("errors"),f=a("services"),g="DS.destroy(resourceName, id): ";b.exports=c},{errors:"hIh4e1",services:"cX8q+p",utils:"uE/lJt"}],29:[function(a,b){function c(a,b,c){var i=f.$q.defer(),j=i.promise;if(c=c||{},f.store[a])if(d.isString(b)||d.isNumber(b))if(d.isObject(c))try{var k=f.store[a],l=this;if(c.bypassCache&&delete k.completedQueries[b],!(b in k.completedQueries))return b in k.pendingQueries||(j=k.pendingQueries[b]=g(d.makePath(k.baseUrl,k.endpoint,b),null).then(function(c){return delete k.pendingQueries[b],k.completedQueries[b]=(new Date).getTime(),l.inject(a,c)})),k.pendingQueries[b];i.resolve(l.get(a,b))}catch(m){i.reject(m)}else i.reject(new e.IllegalArgumentError(h+"options: Must be an object!",{options:{actual:typeof c,expected:"object"}}));else i.reject(new e.IllegalArgumentError(h+"id: Must be a string or a number!",{id:{actual:typeof b,expected:"string|number"}}));else i.reject(new e.RuntimeError(h+a+" is not a registered resource!"));return j}var d=a("utils"),e=a("errors"),f=a("services"),g=a("../../http").GET,h="DS.find(resourceName, id[, options]): ";b.exports=c},{"../../http":34,errors:"hIh4e1",services:"cX8q+p",utils:"uE/lJt"}],30:[function(a,b){function c(a,b,c){var d=h.store[b];a=a||[],delete d.pendingQueries[c],d.completedQueries[c]=(new Date).getTime();for(var e=0;e"in b?c=c&&a[e]>b[">"]:">="in b?c=c&&a[e]>=b[">="]:"<"in b?c=c&&a[e]f?-1:f>e?1:0:f>e?-1:e>f?1:0})}}return d.isNumber(b.query.limit)&&d.isNumber(b.query.skip)?j=d.slice(j,b.query.skip,b.query.skip+b.query.limit):d.isNumber(b.query.limit)?j=d.slice(j,0,b.query.limit):d.isNumber(b.query.skip)&&(j=d.slice(j,b.query.skip)),j}catch(l){throw l instanceof e.IllegalArgumentError?l:new e.UnhandledError(l)}}var d=a("utils"),e=a("errors"),f=a("services"),g="DS.filter(resourceName, params[, options]): ";b.exports=c},{errors:"hIh4e1",services:"cX8q+p",utils:"uE/lJt"}],43:[function(a,b){function c(a,b,c){if(c=c||{},!f.store[a])throw new e.RuntimeError(g+a+" is not a registered resource!");if(!d.isString(b)&&!d.isNumber(b))throw new e.IllegalArgumentError(g+"id: Must be a string or a number!",{id:{actual:typeof b,expected:"string|number"}});if(!d.isObject(c))throw new e.IllegalArgumentError(g+"options: Must be an object!",{options:{actual:typeof c,expected:"object"}});try{return b in f.store[a].index||!c.loadFromServer||this.find(a,b).then(null,function(a){throw a}),f.store[a].index[b]}catch(h){throw new e.UnhandledError(h)}}var d=a("utils"),e=a("errors"),f=a("services"),g="DS.get(resourceName, id[, options]): ";b.exports=c},{errors:"hIh4e1",services:"cX8q+p",utils:"uE/lJt"}],44:[function(a,b){function c(a){return e.isEmpty(a.added)&&e.isEmpty(a.removed)&&e.isEmpty(a.changed)}function d(a,b){if(!g.store[a])throw new f.RuntimeError(h+a+" is not a registered resource!");if(!e.isString(b)&&!e.isNumber(b))throw new f.IllegalArgumentError(h+"id: Must be a string or a number!",{id:{actual:typeof b,expected:"string|number"}});try{return c(g.store[a].changes[b])}catch(d){throw new f.UnhandledError(d)}}var e=a("utils"),f=a("errors"),g=a("services"),h="DS.hasChanges(resourceName, id): ";b.exports=d},{errors:"hIh4e1",services:"cX8q+p",utils:"uE/lJt"}],45:[function(a,b){b.exports={defineResource:a("./defineResource"),eject:a("./eject"),filter:a("./filter"),get:a("./get"),inject:a("./inject"),lastModified:a("./lastModified"),lastSaved:a("./lastSaved"),digest:a("./digest"),changes:a("./changes"),previous:a("./previous"),hasChanges:a("./hasChanges")}},{"./changes":38,"./defineResource":39,"./digest":40,"./eject":41,"./filter":42,"./get":43,"./hasChanges":44,"./inject":46,"./lastModified":47,"./lastSaved":48,"./previous":49}],46:[function(a,b){function c(a,b){var d=this;if(e.isArray(b))for(var i=0;i=b?a+1:b},deepFreeze:function c(a){if("function"==typeof Object.freeze){var b,d;Object.freeze(a);for(d in a)b=a[d],a.hasOwnProperty(d)&&"object"==typeof b&&!Object.isFrozen(b)&&c(b)}},diffObjectFromOldObject:function(a,b){var c={},d={},e={};for(var f in b){var g=a[f];(void 0===g||g!==b[f])&&(f in a?g!==b[f]&&(e[f]=g):d[f]=void 0)}for(var h in a)h in b||(c[h]=a[h]);return{added:c,removed:d,changed:e}}}},{"mout/array/contains":1,"mout/array/filter":2,"mout/array/slice":5,"mout/array/sort":6,"mout/array/toLookup":7,"mout/lang/isEmpty":12,"mout/object/deepMixIn":19,"mout/object/forOwn":21,"mout/string/makePath":23,"mout/string/upperCase":24}],utils:[function(a,b){b.exports=a("uE/lJt")},{}]},{},[52]); \ No newline at end of file diff --git a/karma.start.js b/karma.start.js index 1709ffd..d46e42d 100644 --- a/karma.start.js +++ b/karma.start.js @@ -1,5 +1,7 @@ // Setup global test variables -var $rootScope, $q, $log, DS, app, $httpBackend, p1, p2, p3, p4; +var $rootScope, $q, $log, DSProvider, DS, app, $httpBackend, p1, p2, p3, p4; + +var lifecycle = {}; // Helper globals var fail = function (msg) { @@ -27,20 +29,66 @@ angular.module('app', ['ng', 'jmdobry.angular-data']); // Setup before each test beforeEach(function (done) { - module('app'); + lifecycle.beforeValidate = function (resourceName, attrs, cb) { + lifecycle.beforeValidate.callCount += 1; + cb(null, attrs); + }; + lifecycle.validate = function (resourceName, attrs, cb) { + lifecycle.validate.callCount += 1; + cb(null, attrs); + }; + lifecycle.afterValidate = function (resourceName, attrs, cb) { + lifecycle.afterValidate.callCount += 1; + cb(null, attrs); + }; + lifecycle.beforeCreate = function (resourceName, attrs, cb) { + lifecycle.beforeCreate.callCount += 1; + cb(null, attrs); + }; + lifecycle.afterCreate = function (resourceName, attrs, cb) { + lifecycle.afterCreate.callCount += 1; + cb(null, attrs); + }; + lifecycle.beforeUpdate = function (resourceName, attrs, cb) { + lifecycle.beforeUpdate.callCount += 1; + cb(null, attrs); + }; + lifecycle.afterUpdate = function (resourceName, attrs, cb) { + lifecycle.afterUpdate.callCount += 1; + cb(null, attrs); + }; + lifecycle.beforeDestroy = function (resourceName, attrs, cb) { + lifecycle.beforeDestroy.callCount += 1; + cb(null, attrs); + }; + lifecycle.afterDestroy = function (resourceName, attrs, cb) { + lifecycle.afterDestroy.callCount += 1; + cb(null, attrs); + }; + module('app', function (_DSProvider_) { + DSProvider = _DSProvider_; + DSProvider.config({ + baseUrl: 'http://test.angular-cache.com', + beforeValidate: lifecycle.beforeValidate, + validate: lifecycle.validate, + afterValidate: lifecycle.afterValidate, + beforeCreate: lifecycle.beforeCreate, + afterCreate: lifecycle.afterCreate, + beforeUpdate: lifecycle.beforeUpdate, + afterUpdate: lifecycle.afterUpdate, + beforeDestroy: lifecycle.beforeDestroy, + afterDestroy: lifecycle.afterDestroy + }); + }); inject(function (_$rootScope_, _$q_, _$httpBackend_, _DS_) { // Setup global mocks $q = _$q_; $rootScope = _$rootScope_; DS = _DS_; $httpBackend = _$httpBackend_; - app = { - baseUrl: 'http://test.angular-cache.com' - }; DS.defineResource({ name: 'post', - endpoint: '/posts', - baseUrl: app.baseUrl + endpoint: '/posts' }); $log = { warn: function () { @@ -61,6 +109,16 @@ beforeEach(function (done) { sinon.spy($log, 'info'); sinon.spy($log, 'error'); sinon.spy($log, 'debug'); + + lifecycle.beforeValidate.callCount = 0; + lifecycle.validate.callCount = 0; + lifecycle.afterValidate.callCount = 0; + lifecycle.beforeCreate.callCount = 0; + lifecycle.afterCreate.callCount = 0; + lifecycle.beforeUpdate.callCount = 0; + lifecycle.afterUpdate.callCount = 0; + lifecycle.beforeDestroy.callCount = 0; + lifecycle.afterDestroy.callCount = 0; }); p1 = { author: 'John', age: 30, id: 5 }; @@ -79,6 +137,7 @@ afterEach(function () { $log.info.restore(); $log.error.restore(); $log.debug.restore(); + $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); diff --git a/src/datastore/async_methods/destroy/index.js b/src/datastore/async_methods/destroy/index.js index 0b46b8e..4dfe842 100644 --- a/src/datastore/async_methods/destroy/index.js +++ b/src/datastore/async_methods/destroy/index.js @@ -45,31 +45,36 @@ var utils = require('utils'), * - `{UnhandledError}` */ function destroy(resourceName, id) { - var deferred = service.$q.defer(); + var deferred = services.$q.defer(), + promise = deferred.promise; + if (!services.store[resourceName]) { deferred.reject(new errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!')); } else if (!utils.isString(id) && !utils.isNumber(id)) { deferred.reject(new errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!', { id: { actual: typeof id, expected: 'string|number' } })); - } - - try { + } else { var resource = services.store[resourceName], - _this = this, - url = utils.makePath(resource.baseUrl || services.config.baseUrl, resource.endpoint || resource.name, id); + _this = this; - _this.DEL(url, null).then(function () { - try { + promise = promise + .then(function (attrs) { + return services.$q.promisify(resource.beforeDestroy)(resourceName, attrs); + }) + .then(function () { + return _this.DEL(utils.makePath(resource.baseUrl, resource.endpoint, id), null); + }) + .then(function () { + return services.$q.promisify(resource.afterDestroy)(resourceName, resource.index[id]); + }) + .then(function () { _this.eject(resourceName, id); - deferred.resolve(id); - } catch (err) { - deferred.reject(err); - } - }, deferred.reject); - } catch (err) { - deferred.reject(new errors.UnhandledError(err)); + return id; + }); + + deferred.resolve(resource.index[id]); } - return deferred.promise; + return promise; } module.exports = destroy; diff --git a/src/datastore/async_methods/find/index.js b/src/datastore/async_methods/find/index.js index a5a1fe5..12ac463 100644 --- a/src/datastore/async_methods/find/index.js +++ b/src/datastore/async_methods/find/index.js @@ -1,7 +1,6 @@ var utils = require('utils'), errors = require('errors'), services = require('services'), - GET = require('../../http').GET, errorPrefix = 'DS.find(resourceName, id[, options]): '; /** @@ -49,7 +48,9 @@ var utils = require('utils'), * - `{UnhandledError}` */ function find(resourceName, id, options) { - var deferred = services.$q.defer(); + var deferred = services.$q.defer(), + promise = deferred.promise; + options = options || {}; if (!services.store[resourceName]) { @@ -59,43 +60,35 @@ function find(resourceName, id, options) { } else if (!utils.isObject(options)) { deferred.reject(new errors.IllegalArgumentError(errorPrefix + 'options: Must be an object!', { options: { actual: typeof options, expected: 'object' } })); } else { - var _this = this; - try { - var resource = services.store[resourceName]; + var resource = services.store[resourceName], + _this = this; - if (id in resource.index && !options.bypassCache) { - deferred.resolve(_this.get(resourceName, id)); - } else { - var url = utils.makePath(resource.baseUrl || services.config.baseUrl, resource.endpoint || resource.name, id), - config = null; + if (options.bypassCache) { + delete resource.completedQueries[id]; + } - if (options.bypassCache) { - config = { - headers: { - 'Last-Modified': new Date(resource.modified[id]) - } - }; + if (!(id in resource.completedQueries)) { + if (!(id in resource.pendingQueries)) { + promise = resource.pendingQueries[id] = _this.GET(utils.makePath(resource.baseUrl, resource.endpoint, id), null) + .then(function (data) { + // Query is no longer pending + delete resource.pendingQueries[id]; + resource.completedQueries[id] = new Date().getTime(); + return _this.inject(resourceName, data); + }); } - GET(url, config).then(function (data) { - try { - _this.inject(resourceName, data); - deferred.resolve(_this.get(resourceName, id)); - } catch (err) { - deferred.reject(err); - } - }, deferred.reject); - } - } catch (err) { - if (!(err instanceof errors.UnhandledError)) { - deferred.reject(new errors.UnhandledError(err)); + + return resource.pendingQueries[id]; } else { - deferred.reject(err); + deferred.resolve(_this.get(resourceName, id)); } + } catch (err) { + deferred.reject(err); } } - return deferred.promise; + return promise; } module.exports = find; diff --git a/src/datastore/async_methods/findAll/index.js b/src/datastore/async_methods/findAll/index.js index b91a99f..e24da21 100644 --- a/src/datastore/async_methods/findAll/index.js +++ b/src/datastore/async_methods/findAll/index.js @@ -1,7 +1,6 @@ var utils = require('utils'), errors = require('errors'), services = require('services'), - GET = require('../../http').GET, errorPrefix = 'DS.findAll(resourceName, params[, options]): '; function processResults(data, resourceName, queryHash) { @@ -28,9 +27,8 @@ function processResults(data, resourceName, queryHash) { function _findAll(resourceName, params, options) { var resource = services.store[resourceName], - _this = this; - - var queryHash = utils.toJson(params); + _this = this, + queryHash = utils.toJson(params); if (options.bypassCache) { delete resource.completedQueries[queryHash]; @@ -39,10 +37,10 @@ function _findAll(resourceName, params, options) { if (!(queryHash in resource.completedQueries)) { // This particular query has never been completed - if (!resource.pendingQueries[queryHash]) { + if (!(queryHash in resource.pendingQueries)) { // This particular query has never even been made - resource.pendingQueries[queryHash] = GET(utils.makePath(resource.baseUrl, resource.endpoint), { params: params }) + resource.pendingQueries[queryHash] = _this.GET(utils.makePath(resource.baseUrl, resource.endpoint), { params: params }) .then(function (data) { try { return processResults.apply(_this, [data, resourceName, queryHash]); @@ -51,6 +49,7 @@ function _findAll(resourceName, params, options) { } }); } + return resource.pendingQueries[queryHash]; } else { return this.filter(resourceName, params, options); diff --git a/src/datastore/async_methods/refresh/index.js b/src/datastore/async_methods/refresh/index.js index e11633b..51b6c57 100644 --- a/src/datastore/async_methods/refresh/index.js +++ b/src/datastore/async_methods/refresh/index.js @@ -1,7 +1,6 @@ var utils = require('utils'), errors = require('errors'), services = require('services'), - PUT = require('../../http').PUT, errorPrefix = 'DS.refresh(resourceName, id): '; /** @@ -61,13 +60,13 @@ function refresh(resourceName, id, options) { if (!services.store[resourceName]) { throw new errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!'); } else if (!utils.isString(id) && !utils.isNumber(id)) { - throw new errors.IllegalArgumentError('DS.refresh(resourceName, id): id: Must be a string or a number!', { id: { actual: typeof id, expected: 'string|number' } }); + throw new errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!', { id: { actual: typeof id, expected: 'string|number' } }); } else if (!utils.isObject(options)) { throw new errors.IllegalArgumentError(errorPrefix + 'options: Must be an object!', { options: { actual: typeof options, expected: 'object' } }); } if (id in services.store[resourceName].index) { - return this.find(resourceName, id, true); + return this.find(resourceName, id, { bypassCache: true }); } else { return false; } diff --git a/src/datastore/async_methods/save/index.js b/src/datastore/async_methods/save/index.js index 8aec482..e104a08 100644 --- a/src/datastore/async_methods/save/index.js +++ b/src/datastore/async_methods/save/index.js @@ -57,7 +57,7 @@ function save(resourceName, id, options) { } else if (!utils.isString(id) && !utils.isNumber(id)) { deferred.reject(new errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!', { id: { actual: typeof id, expected: 'string|number' } })); } else if (!utils.isObject(options)) { - deferred.reject(new errors.IllegalArgumentError(errorPrefix + 'id: Must be an object!', { options: { actual: typeof options, expected: 'object' } })); + deferred.reject(new errors.IllegalArgumentError(errorPrefix + 'options: Must be an object!', { options: { actual: typeof options, expected: 'object' } })); } else if (!(id in services.store[resourceName].index)) { deferred.reject(new errors.RuntimeError(errorPrefix + 'id: "' + id + '" not found!')); } else { diff --git a/src/datastore/index.js b/src/datastore/index.js index 729578c..b8651db 100644 --- a/src/datastore/index.js +++ b/src/datastore/index.js @@ -19,7 +19,11 @@ var utils = require('utils'), * ```js * DSProvider.config({ * baseUrl: 'http://myapp.com/api', - * idAttribute: '_id' + * idAttribute: '_id', + * validate: function (resourceName, attrs, cb) { + * console.log('looks good to me'); + * cb(null, attrs); + * } * }); * ``` * @@ -27,7 +31,18 @@ var utils = require('utils'), * * - `{IllegalArgumentError}` * - * @param {object} options Configuration for the data store. + * @param {object} options Global configuration for the data store. Properties: + * - `{string=}` - `baseUrl` - The default base url to be used by the data store. Can be overridden via `DS.defineResource`. + * - `{string=}` - `idAttribute` - The default property that specifies the primary key of an object. Default: `"id"`. + * - `{function=}` - `beforeValidate` - Global lifecycle hook. Signature: `beforeValidate(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `validate` - Global lifecycle hook. Signature: `validate(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `afterValidate` - Global lifecycle hook. Signature: `afterValidate(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `beforeCreate` - Global lifecycle hook. Signature: `beforeCreate(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `afterCreate` - Global lifecycle hook. Signature: `afterCreate(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `beforeUpdate` - Global lifecycle hook. Signature: `beforeUpdate(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `afterUpdate` - Global lifecycle hook. Signature: `afterUpdate(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `beforeDestroy` - Global lifecycle hook. Signature: `beforeDestroy(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `afterDestroy` - Global lifecycle hook. Signature: `afterDestroy(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. */ function config(options) { options = options || {}; diff --git a/src/datastore/sync_methods/defineResource/index.js b/src/datastore/sync_methods/defineResource/index.js index a0cf7f8..0c8aef9 100644 --- a/src/datastore/sync_methods/defineResource/index.js +++ b/src/datastore/sync_methods/defineResource/index.js @@ -28,6 +28,8 @@ function Resource(options) { this.collectionModified = 0; } +Resource.prototype = services.config; + /** * @doc method * @id DS.sync_methods:defineResource @@ -48,9 +50,9 @@ function Resource(options) { * idAttribute: '_id', * endpoint: '/documents * baseUrl: 'http://myapp.com/api', - * validate: function (attrs, options, cb) { + * beforeDestroy: function (resourceName attrs, cb) { * console.log('looks good to me'); - * cb(null); + * cb(null, attrs); * } * }); * ``` @@ -66,8 +68,16 @@ function Resource(options) { * - `{string}` - `name` - The name by which this resource will be identified. * - `{string="id"}` - `idAttribute` - The attribute that specifies the primary key for this resource. * - `{string=}` - `endpoint` - The attribute that specifies the primary key for this resource. Default is the value of `name`. - * - `{string="/"}` - `baseUrl` - The url relative to which all AJAX requests will be made. - * - `{function=}` - `validate` - The validation function to be executed before create operations. + * - `{string=}` - `baseUrl` - The url relative to which all AJAX requests will be made. + * - `{function=}` - `beforeValidate` - Lifecycle hook. Overrides global. Signature: `beforeValidate(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `validate` - Lifecycle hook. Overrides global. Signature: `validate(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `afterValidate` - Lifecycle hook. Overrides global. Signature: `afterValidate(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `beforeCreate` - Lifecycle hook. Overrides global. Signature: `beforeCreate(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `afterCreate` - Lifecycle hook. Overrides global. Signature: `afterCreate(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `beforeUpdate` - Lifecycle hook. Overrides global. Signature: `beforeUpdate(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `afterUpdate` - Lifecycle hook. Overrides global. Signature: `afterUpdate(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `beforeDestroy` - Lifecycle hook. Overrides global. Signature: `beforeDestroy(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. + * - `{function=}` - `afterDestroy` - Lifecycle hook. Overrides global. Signature: `afterDestroy(resourceName, attrs, cb)`. Callback signature: `cb(err, attrs)`. */ function defineResource(definition) { if (utils.isString(definition)) { @@ -86,7 +96,6 @@ function defineResource(definition) { } try { - Resource.prototype = services.config; services.store[definition.name] = new Resource(definition); } catch (err) { delete services.store[definition.name]; diff --git a/src/datastore/sync_methods/get/index.js b/src/datastore/sync_methods/get/index.js index 77e9ea3..b313f89 100644 --- a/src/datastore/sync_methods/get/index.js +++ b/src/datastore/sync_methods/get/index.js @@ -48,7 +48,9 @@ function get(resourceName, id, options) { try { // cache miss, request resource from server if (!(id in services.store[resourceName].index) && options.loadFromServer) { - this.find(resourceName, id); + this.find(resourceName, id).then(null, function (err) { + throw err; + }); } // return resource from cache diff --git a/src/utils/index.js b/src/utils/index.js index 4f24371..8491f3e 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -3,6 +3,7 @@ module.exports = { isArray: angular.isArray, isObject: angular.isObject, isNumber: angular.isNumber, + isFunction: angular.isFunction, isEmpty: require('mout/lang/isEmpty'), toJson: angular.toJson, makePath: require('mout/string/makePath'), diff --git a/test/unit/datastore/async_methods/create/index.test.js b/test/unit/datastore/async_methods/create/index.test.js new file mode 100644 index 0000000..6cf8355 --- /dev/null +++ b/test/unit/datastore/async_methods/create/index.test.js @@ -0,0 +1,41 @@ +describe('DS.create(resourceName, attrs)', function () { + var errorPrefix = 'DS.create(resourceName, attrs): '; + + it('should throw an error when method pre-conditions are not met', function (done) { + DS.create('does not exist', 5).then(function () { + fail('should have rejected'); + }, function (err) { + assert.isTrue(err instanceof DS.errors.RuntimeError); + assert.equal(err.message, errorPrefix + 'does not exist is not a registered resource!'); + }); + + angular.forEach(TYPES_EXCEPT_OBJECT, function (key) { + DS.create('post', key).then(function () { + fail('should have rejected'); + }, function (err) { + assert.isTrue(err instanceof DS.errors.IllegalArgumentError); + assert.equal(err.message, errorPrefix + 'attrs: Must be an object!'); + }); + }); + + done(); + }); + it('should create an item and save it to the server', function (done) { + $httpBackend.expectPOST('http://test.angular-cache.com/posts').respond(200, p1); + + DS.create('post', { author: 'John', age: 30 }).then(function (post) { + assert.deepEqual(post, p1, 'post 5 should have been created'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + assert.equal(lifecycle.beforeCreate.callCount, 1, 'beforeCreate should have been called'); + assert.equal(lifecycle.afterCreate.callCount, 1, 'afterCreate should have been called'); + assert.deepEqual(DS.get('post', 5), p1); + + done(); + }); +}); diff --git a/test/unit/datastore/async_methods/destroy/index.test.js b/test/unit/datastore/async_methods/destroy/index.test.js new file mode 100644 index 0000000..ade9bf3 --- /dev/null +++ b/test/unit/datastore/async_methods/destroy/index.test.js @@ -0,0 +1,45 @@ +describe('DS.destroy(resourceName, id)', function () { + var errorPrefix = 'DS.destroy(resourceName, id): '; + + it('should throw an error when method pre-conditions are not met', function (done) { + DS.destroy('does not exist', 5).then(function () { + fail('should have rejected'); + }, function (err) { + assert.isTrue(err instanceof DS.errors.RuntimeError); + assert.equal(err.message, errorPrefix + 'does not exist is not a registered resource!'); + }); + + angular.forEach(TYPES_EXCEPT_STRING_OR_NUMBER, function (key) { + DS.destroy('post', key).then(function () { + fail('should have rejected'); + }, function (err) { + assert.isTrue(err instanceof DS.errors.IllegalArgumentError); + assert.equal(err.message, errorPrefix + 'id: Must be a string or a number!'); + }); + }); + + done(); + }); + it('should delete an item from the data store', function (done) { + $httpBackend.expectDELETE('http://test.angular-cache.com/posts/5').respond(200, 5); + + DS.inject('post', p1); + + DS.destroy('post', 5).then(function (id) { + assert.equal(id, 5, 'post 5 should have been deleted'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + assert.equal(lifecycle.beforeDestroy.callCount, 1, 'beforeDestroy should have been called'); + assert.equal(lifecycle.afterDestroy.callCount, 1, 'afterDestroy should have been called'); + assert.isUndefined(DS.get('post', 5)); + assert.equal(DS.lastModified('post', 5), 0); + assert.equal(DS.lastSaved('post', 5), 0); + + done(); + }); +}); diff --git a/test/unit/datastore/async_methods/find/index.test.js b/test/unit/datastore/async_methods/find/index.test.js new file mode 100644 index 0000000..030d7bf --- /dev/null +++ b/test/unit/datastore/async_methods/find/index.test.js @@ -0,0 +1,82 @@ +describe('DS.find(resourceName, id[, options]): ', function () { + var errorPrefix = 'DS.find(resourceName, id[, options]): '; + + it('should throw an error when method pre-conditions are not met', function (done) { + DS.find('does not exist', 5).then(function () { + fail('should have rejected'); + }, function (err) { + assert.isTrue(err instanceof DS.errors.RuntimeError); + assert.equal(err.message, errorPrefix + 'does not exist is not a registered resource!'); + }); + + angular.forEach(TYPES_EXCEPT_STRING_OR_NUMBER, function (key) { + DS.find('post', key).then(function () { + fail('should have rejected'); + }, function (err) { + assert.isTrue(err instanceof DS.errors.IllegalArgumentError); + assert.equal(err.message, errorPrefix + 'id: Must be a string or a number!'); + }); + }); + + angular.forEach(TYPES_EXCEPT_OBJECT, function (key) { + if (key) { + DS.find('post', 5, key).then(function () { + fail('should have rejected'); + }, function (err) { + assert.isTrue(err instanceof DS.errors.IllegalArgumentError); + assert.equal(err.message, errorPrefix + 'options: Must be an object!'); + }); + } + }); + + done(); + }); + it('should get an item from the server', function (done) { + $httpBackend.expectGET('http://test.angular-cache.com/posts/5').respond(200, p1); + + DS.find('post', 5).then(function (post) { + assert.deepEqual(post, p1); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + assert.isUndefined(DS.get('post', 5), 'The post should not be in the store yet'); + + // Should have no effect because there is already a pending query + DS.find('post', 5).then(function (post) { + assert.deepEqual(post, p1); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + $httpBackend.flush(); + + assert.deepEqual(DS.get('post', 5), p1, 'The post is now in the store'); + assert.isNumber(DS.lastModified('post', 5)); + assert.isNumber(DS.lastSaved('post', 5)); + + // Should not make a request because the request was already completed + DS.find('post', 5).then(function (post) { + assert.deepEqual(post, p1); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + $httpBackend.expectGET('http://test.angular-cache.com/posts/5').respond(200, p1); + + // Should make a request because loadFromServer is set to true + DS.find('post', 5, { bypassCache: true }).then(function (post) { + assert.deepEqual(post, p1); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + $httpBackend.flush(); + + done(); + }); +}); diff --git a/test/unit/datastore/async_methods/save/index.test.js b/test/unit/datastore/async_methods/save/index.test.js new file mode 100644 index 0000000..2f269a1 --- /dev/null +++ b/test/unit/datastore/async_methods/save/index.test.js @@ -0,0 +1,69 @@ +describe('DS.save(resourceName, id[, options])', function () { + var errorPrefix = 'DS.save(resourceName, id[, options]): '; + + it('should throw an error when method pre-conditions are not met', function (done) { + DS.save('does not exist', 5).then(function () { + fail('should have rejected'); + }, function (err) { + assert.isTrue(err instanceof DS.errors.RuntimeError); + assert.equal(err.message, errorPrefix + 'does not exist is not a registered resource!'); + }); + + angular.forEach(TYPES_EXCEPT_STRING_OR_NUMBER, function (key) { + DS.save('post', key).then(function () { + fail('should have rejected'); + }, function (err) { + assert.isTrue(err instanceof DS.errors.IllegalArgumentError); + assert.equal(err.message, errorPrefix + 'id: Must be a string or a number!'); + }); + }); + + angular.forEach(TYPES_EXCEPT_OBJECT, function (key) { + if (key) { + DS.save('post', 5, key).then(function () { + fail('should have rejected'); + }, function (err) { + assert.isTrue(err instanceof DS.errors.IllegalArgumentError); + assert.equal(err.message, errorPrefix + 'options: Must be an object!'); + }); + } + }); + + done(); + }); + it('should save an item to the server', function (done) { + $httpBackend.expectPUT('http://test.angular-cache.com/posts/5').respond(200, p1); + + DS.inject('post', p1); + + var initialModified = DS.lastModified('post', 5), + initialSaved = DS.lastSaved('post', 5); + + p1.author = 'Jake'; + + DS.save('post', 5).then(function (post) { + assert.deepEqual(post, p1, 'post 5 should have been saved'); + assert.equal(post.author, 'Jake'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + assert.equal(lifecycle.beforeUpdate.callCount, 1, 'beforeUpdate should have been called'); + assert.equal(lifecycle.afterUpdate.callCount, 1, 'afterUpdate should have been called'); + assert.deepEqual(DS.get('post', 5), p1); + assert.notEqual(DS.lastModified('post', 5), initialModified); + assert.notEqual(DS.lastSaved('post', 5), initialSaved); + + DS.save('post', 6).then(function () { + fail('should not have succeeded'); + }, function (err) { + assert.isTrue(err instanceof DS.errors.RuntimeError); + assert.equal(err.message, errorPrefix + 'id: "6" not found!'); + }); + + done(); + }); +}); diff --git a/test/unit/datastore/sync_methods/get/index.test.js b/test/unit/datastore/sync_methods/get/index.test.js new file mode 100644 index 0000000..f3eb412 --- /dev/null +++ b/test/unit/datastore/sync_methods/get/index.test.js @@ -0,0 +1,47 @@ +describe('DS.get(resourceName, id[, options])', function () { + var errorPrefix = 'DS.get(resourceName, id[, options]): '; + + it('should throw an error when method pre-conditions are not met', function (done) { + assert.throw(function () { + DS.get('does not exist', {}); + }, DS.errors.RuntimeError, errorPrefix + 'does not exist is not a registered resource!'); + + angular.forEach(TYPES_EXCEPT_STRING_OR_NUMBER, function (key) { + assert.throw(function () { + DS.get('post', key); + }, DS.errors.IllegalArgumentError, errorPrefix + 'id: Must be a string or a number!'); + }); + + angular.forEach(TYPES_EXCEPT_OBJECT, function (key) { + if (key) { + assert.throw(function () { + DS.get('post', 5, key); + }, DS.errors.IllegalArgumentError, errorPrefix + 'options: Must be an object!'); + } + }); + + done(); + }); + it('should return undefined if the query has never been made before', function (done) { + + assert.isUndefined(DS.get('post', 5), 'should be undefined'); + + done(); + }); + it('should return undefined and send the query to the server if the query has never been made before and loadFromServer is set to true', function (done) { + $httpBackend.expectGET('http://test.angular-cache.com/posts/5').respond(200, p1); + + assert.isUndefined(DS.get('post', 5, { loadFromServer: true }), 'should be undefined'); + + // There should only be one GET request, so this should have no effect. + assert.isUndefined(DS.get('post', 5, { loadFromServer: true }), 'should be undefined'); + + $httpBackend.flush(); + + assert.deepEqual(DS.get('post', 5), p1, 'p1 should now be in the store'); + assert.isNumber(DS.lastModified('post', 5)); + assert.isNumber(DS.lastSaved('post', 5)); + + done(); + }); +});