From 07805198430a46bbdf2776b29fbdeb0442c75db0 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Wed, 23 May 2018 13:43:31 +0000 Subject: [PATCH 01/46] Bryan's initial commit. --- couscous.js | 14942 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 14942 insertions(+) create mode 100644 couscous.js diff --git a/couscous.js b/couscous.js new file mode 100644 index 0000000..80da113 --- /dev/null +++ b/couscous.js @@ -0,0 +1,14942 @@ +/*! URI.js v1.12.0 http://medialize.github.com/URI.js/ */ +/* build contains: IPv6.js, punycode.js, SecondLevelDomains.js, URI.js, URI.fragmentQuery.js */ +(function(e,k){"object"===typeof exports?module.exports=k():"function"===typeof define&&define.amd?define(k):e.IPv6=k(e)})(this,function(e){var k=e&&e.IPv6;return{best:function(e){e=e.toLowerCase().split(":");var k=e.length,d=8;""===e[0]&&""===e[1]&&""===e[2]?(e.shift(),e.shift()):""===e[0]&&""===e[1]?e.shift():""===e[k-1]&&""===e[k-2]&&e.pop();k=e.length;-1!==e[k-1].indexOf(".")&&(d=7);var g;for(g=0;gq;q++)if("0"===k[0]&&1q&&(k=r,q=l)):"0"==e[g]&&(z=!0,r=g,l=1);l>q&&(k=r,q=l);1=h&&c>>10&1023|55296),a=56320|a&1023);return b+=x(a)}).join("")}function q(a, +b){return a+22+75*(26>a)-((0!=b)<<5)}function l(a,b,c){var d=0;a=c?A(a/H):a>>1;for(a+=A(a/b);a>n*y>>1;d+=s)a=A(a/n);return A(d+(n+1)*a/(a+I))}function r(b){var c=[],d=b.length,h,p=0,e=F,f=G,n,x,q,t,m;n=b.lastIndexOf(a);0>n&&(n=0);for(x=0;x=d&&k("invalid-input");t=b.charCodeAt(n++);t=10>t-48?t-22:26>t-65?t-65:26>t-97?t-97:s;(t>=s||t>A((w-p)/h))&&k("overflow");p+=t*h;m=q<=f?v:q>=f+y?y: +q-f;if(tA(w/t)&&k("overflow");h*=t}h=c.length+1;f=l(p-x,h,0==x);A(p/h)>w-e&&k("overflow");e+=A(p/h);p%=h;c.splice(p++,0,e)}return g(c)}function z(b){var c,h,p,e,f,n,g,m,r,t=[],B,u,z;b=d(b);B=b.length;c=F;h=0;f=G;for(n=0;nr&&t.push(x(r));for((p=e=t.length)&&t.push(a);p=c&&rA((w-h)/u)&&k("overflow");h+=(g-c)*u;c=g;for(n=0;nw&&k("overflow"),r==c){m=h;for(g=s;;g+=s){r=g<=f?v:g>=f+y?y:g-f; +if(m= 0x80 (not a basic code point)", +"invalid-input":"Invalid input"},n=s-v,A=Math.floor,x=String.fromCharCode,B;f={version:"1.2.3",ucs2:{decode:d,encode:g},decode:r,encode:z,toASCII:function(a){return m(a,function(a){return c.test(a)?"xn--"+z(a):a})},toUnicode:function(a){return m(a,function(a){return b.test(a)?r(a.slice(4).toLowerCase()):a})}};if("function"==typeof define&&"object"==typeof define.amd&&define.amd)define(function(){return f});else if(D&&!D.nodeType)if(E)E.exports=f;else for(B in f)f.hasOwnProperty(B)&&(D[B]=f[B]);else e.punycode= +f})(this); +(function(e,k){"object"===typeof exports?module.exports=k():"function"===typeof define&&define.amd?define(k):e.SecondLevelDomains=k(e)})(this,function(e){var k=e&&e.SecondLevelDomains,u=Object.prototype.hasOwnProperty,m={list:{ac:"com|gov|mil|net|org",ae:"ac|co|gov|mil|name|net|org|pro|sch",af:"com|edu|gov|net|org",al:"com|edu|gov|mil|net|org",ao:"co|ed|gv|it|og|pb",ar:"com|edu|gob|gov|int|mil|net|org|tur",at:"ac|co|gv|or",au:"asn|com|csiro|edu|gov|id|net|org",ba:"co|com|edu|gov|mil|net|org|rs|unbi|unmo|unsa|untz|unze",bb:"biz|co|com|edu|gov|info|net|org|store|tv", +bh:"biz|cc|com|edu|gov|info|net|org",bn:"com|edu|gov|net|org",bo:"com|edu|gob|gov|int|mil|net|org|tv",br:"adm|adv|agr|am|arq|art|ato|b|bio|blog|bmd|cim|cng|cnt|com|coop|ecn|edu|eng|esp|etc|eti|far|flog|fm|fnd|fot|fst|g12|ggf|gov|imb|ind|inf|jor|jus|lel|mat|med|mil|mus|net|nom|not|ntr|odo|org|ppg|pro|psc|psi|qsl|rec|slg|srv|tmp|trd|tur|tv|vet|vlog|wiki|zlg",bs:"com|edu|gov|net|org",bz:"du|et|om|ov|rg",ca:"ab|bc|mb|nb|nf|nl|ns|nt|nu|on|pe|qc|sk|yk",ck:"biz|co|edu|gen|gov|info|net|org",cn:"ac|ah|bj|com|cq|edu|fj|gd|gov|gs|gx|gz|ha|hb|he|hi|hl|hn|jl|js|jx|ln|mil|net|nm|nx|org|qh|sc|sd|sh|sn|sx|tj|tw|xj|xz|yn|zj", +co:"com|edu|gov|mil|net|nom|org",cr:"ac|c|co|ed|fi|go|or|sa",cy:"ac|biz|com|ekloges|gov|ltd|name|net|org|parliament|press|pro|tm","do":"art|com|edu|gob|gov|mil|net|org|sld|web",dz:"art|asso|com|edu|gov|net|org|pol",ec:"com|edu|fin|gov|info|med|mil|net|org|pro",eg:"com|edu|eun|gov|mil|name|net|org|sci",er:"com|edu|gov|ind|mil|net|org|rochest|w",es:"com|edu|gob|nom|org",et:"biz|com|edu|gov|info|name|net|org",fj:"ac|biz|com|info|mil|name|net|org|pro",fk:"ac|co|gov|net|nom|org",fr:"asso|com|f|gouv|nom|prd|presse|tm", +gg:"co|net|org",gh:"com|edu|gov|mil|org",gn:"ac|com|gov|net|org",gr:"com|edu|gov|mil|net|org",gt:"com|edu|gob|ind|mil|net|org",gu:"com|edu|gov|net|org",hk:"com|edu|gov|idv|net|org",id:"ac|co|go|mil|net|or|sch|web",il:"ac|co|gov|idf|k12|muni|net|org","in":"ac|co|edu|ernet|firm|gen|gov|i|ind|mil|net|nic|org|res",iq:"com|edu|gov|i|mil|net|org",ir:"ac|co|dnssec|gov|i|id|net|org|sch",it:"edu|gov",je:"co|net|org",jo:"com|edu|gov|mil|name|net|org|sch",jp:"ac|ad|co|ed|go|gr|lg|ne|or",ke:"ac|co|go|info|me|mobi|ne|or|sc", +kh:"com|edu|gov|mil|net|org|per",ki:"biz|com|de|edu|gov|info|mob|net|org|tel",km:"asso|com|coop|edu|gouv|k|medecin|mil|nom|notaires|pharmaciens|presse|tm|veterinaire",kn:"edu|gov|net|org",kr:"ac|busan|chungbuk|chungnam|co|daegu|daejeon|es|gangwon|go|gwangju|gyeongbuk|gyeonggi|gyeongnam|hs|incheon|jeju|jeonbuk|jeonnam|k|kg|mil|ms|ne|or|pe|re|sc|seoul|ulsan",kw:"com|edu|gov|net|org",ky:"com|edu|gov|net|org",kz:"com|edu|gov|mil|net|org",lb:"com|edu|gov|net|org",lk:"assn|com|edu|gov|grp|hotel|int|ltd|net|ngo|org|sch|soc|web", +lr:"com|edu|gov|net|org",lv:"asn|com|conf|edu|gov|id|mil|net|org",ly:"com|edu|gov|id|med|net|org|plc|sch",ma:"ac|co|gov|m|net|org|press",mc:"asso|tm",me:"ac|co|edu|gov|its|net|org|priv",mg:"com|edu|gov|mil|nom|org|prd|tm",mk:"com|edu|gov|inf|name|net|org|pro",ml:"com|edu|gov|net|org|presse",mn:"edu|gov|org",mo:"com|edu|gov|net|org",mt:"com|edu|gov|net|org",mv:"aero|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro",mw:"ac|co|com|coop|edu|gov|int|museum|net|org",mx:"com|edu|gob|net|org",my:"com|edu|gov|mil|name|net|org|sch", +nf:"arts|com|firm|info|net|other|per|rec|store|web",ng:"biz|com|edu|gov|mil|mobi|name|net|org|sch",ni:"ac|co|com|edu|gob|mil|net|nom|org",np:"com|edu|gov|mil|net|org",nr:"biz|com|edu|gov|info|net|org",om:"ac|biz|co|com|edu|gov|med|mil|museum|net|org|pro|sch",pe:"com|edu|gob|mil|net|nom|org|sld",ph:"com|edu|gov|i|mil|net|ngo|org",pk:"biz|com|edu|fam|gob|gok|gon|gop|gos|gov|net|org|web",pl:"art|bialystok|biz|com|edu|gda|gdansk|gorzow|gov|info|katowice|krakow|lodz|lublin|mil|net|ngo|olsztyn|org|poznan|pwr|radom|slupsk|szczecin|torun|warszawa|waw|wroc|wroclaw|zgora", +pr:"ac|biz|com|edu|est|gov|info|isla|name|net|org|pro|prof",ps:"com|edu|gov|net|org|plo|sec",pw:"belau|co|ed|go|ne|or",ro:"arts|com|firm|info|nom|nt|org|rec|store|tm|www",rs:"ac|co|edu|gov|in|org",sb:"com|edu|gov|net|org",sc:"com|edu|gov|net|org",sh:"co|com|edu|gov|net|nom|org",sl:"com|edu|gov|net|org",st:"co|com|consulado|edu|embaixada|gov|mil|net|org|principe|saotome|store",sv:"com|edu|gob|org|red",sz:"ac|co|org",tr:"av|bbs|bel|biz|com|dr|edu|gen|gov|info|k12|name|net|org|pol|tel|tsk|tv|web",tt:"aero|biz|cat|co|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel", +tw:"club|com|ebiz|edu|game|gov|idv|mil|net|org",mu:"ac|co|com|gov|net|or|org",mz:"ac|co|edu|gov|org",na:"co|com",nz:"ac|co|cri|geek|gen|govt|health|iwi|maori|mil|net|org|parliament|school",pa:"abo|ac|com|edu|gob|ing|med|net|nom|org|sld",pt:"com|edu|gov|int|net|nome|org|publ",py:"com|edu|gov|mil|net|org",qa:"com|edu|gov|mil|net|org",re:"asso|com|nom",ru:"ac|adygeya|altai|amur|arkhangelsk|astrakhan|bashkiria|belgorod|bir|bryansk|buryatia|cbg|chel|chelyabinsk|chita|chukotka|chuvashia|com|dagestan|e-burg|edu|gov|grozny|int|irkutsk|ivanovo|izhevsk|jar|joshkar-ola|kalmykia|kaluga|kamchatka|karelia|kazan|kchr|kemerovo|khabarovsk|khakassia|khv|kirov|koenig|komi|kostroma|kranoyarsk|kuban|kurgan|kursk|lipetsk|magadan|mari|mari-el|marine|mil|mordovia|mosreg|msk|murmansk|nalchik|net|nnov|nov|novosibirsk|nsk|omsk|orenburg|org|oryol|penza|perm|pp|pskov|ptz|rnd|ryazan|sakhalin|samara|saratov|simbirsk|smolensk|spb|stavropol|stv|surgut|tambov|tatarstan|tom|tomsk|tsaritsyn|tsk|tula|tuva|tver|tyumen|udm|udmurtia|ulan-ude|vladikavkaz|vladimir|vladivostok|volgograd|vologda|voronezh|vrn|vyatka|yakutia|yamal|yekaterinburg|yuzhno-sakhalinsk", +rw:"ac|co|com|edu|gouv|gov|int|mil|net",sa:"com|edu|gov|med|net|org|pub|sch",sd:"com|edu|gov|info|med|net|org|tv",se:"a|ac|b|bd|c|d|e|f|g|h|i|k|l|m|n|o|org|p|parti|pp|press|r|s|t|tm|u|w|x|y|z",sg:"com|edu|gov|idn|net|org|per",sn:"art|com|edu|gouv|org|perso|univ",sy:"com|edu|gov|mil|net|news|org",th:"ac|co|go|in|mi|net|or",tj:"ac|biz|co|com|edu|go|gov|info|int|mil|name|net|nic|org|test|web",tn:"agrinet|com|defense|edunet|ens|fin|gov|ind|info|intl|mincom|nat|net|org|perso|rnrt|rns|rnu|tourism",tz:"ac|co|go|ne|or", +ua:"biz|cherkassy|chernigov|chernovtsy|ck|cn|co|com|crimea|cv|dn|dnepropetrovsk|donetsk|dp|edu|gov|if|in|ivano-frankivsk|kh|kharkov|kherson|khmelnitskiy|kiev|kirovograd|km|kr|ks|kv|lg|lugansk|lutsk|lviv|me|mk|net|nikolaev|od|odessa|org|pl|poltava|pp|rovno|rv|sebastopol|sumy|te|ternopil|uzhgorod|vinnica|vn|zaporizhzhe|zhitomir|zp|zt",ug:"ac|co|go|ne|or|org|sc",uk:"ac|bl|british-library|co|cym|gov|govt|icnet|jet|lea|ltd|me|mil|mod|national-library-scotland|nel|net|nhs|nic|nls|org|orgn|parliament|plc|police|sch|scot|soc", +us:"dni|fed|isa|kids|nsn",uy:"com|edu|gub|mil|net|org",ve:"co|com|edu|gob|info|mil|net|org|web",vi:"co|com|k12|net|org",vn:"ac|biz|com|edu|gov|health|info|int|name|net|org|pro",ye:"co|com|gov|ltd|me|net|org|plc",yu:"ac|co|edu|gov|org",za:"ac|agric|alt|bourse|city|co|cybernet|db|edu|gov|grondar|iaccess|imt|inca|landesign|law|mil|net|ngo|nis|nom|olivetti|org|pix|school|tm|web",zm:"ac|co|com|edu|gov|net|org|sch"},has_expression:null,is_expression:null,has:function(d){return!!d.match(m.has_expression)}, +is:function(d){return!!d.match(m.is_expression)},get:function(d){return(d=d.match(m.has_expression))&&d[1]||null},noConflict:function(){e.SecondLevelDomains===this&&(e.SecondLevelDomains=k);return this},init:function(){var d="",e;for(e in m.list)u.call(m.list,e)&&(d+="|("+("("+m.list[e]+")."+e)+")");m.has_expression=RegExp("\\.("+d.substr(1)+")$","i");m.is_expression=RegExp("^("+d.substr(1)+")$","i")}};m.init();return m}); +(function(e,k){"object"===typeof exports?module.exports=k(require("./punycode"),require("./IPv6"),require("./SecondLevelDomains")):"function"===typeof define&&define.amd?define(["./punycode","./IPv6","./SecondLevelDomains"],k):e.URI=k(e.punycode,e.IPv6,e.SecondLevelDomains,e)})(this,function(e,k,u,m){function d(a,b){if(!(this instanceof d))return new d(a,b);void 0===a&&(a="undefined"!==typeof location?location.href+"":"");this.href(a);return void 0!==b?this.absoluteTo(b):this}function g(a){return a.replace(/([.*+?^=!:${}()|[\]\/\\])/g, +"\\$1")}function q(a){return void 0===a?"Undefined":String(Object.prototype.toString.call(a)).slice(8,-1)}function l(a){return"Array"===q(a)}function r(a,b){var c,d;if(l(b)){c=0;for(d=b.length;c]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?\u00ab\u00bb\u201c\u201d\u2018\u2019]))/ig;d.findUri={start:/\b(?:([a-z][a-z0-9.+-]*:\/\/)|www\.)/gi,end:/[\s\r\n]|$/,trim:/[`!()\[\]{};:'".,<>?\u00ab\u00bb\u201c\u201d\u201e\u2018\u2019]+$/};d.defaultPorts={http:"80",https:"443",ftp:"21",gopher:"70",ws:"80",wss:"443"};d.invalid_hostname_characters= +/[^a-zA-Z0-9\.-]/;d.domAttributes={a:"href",blockquote:"cite",link:"href",base:"href",script:"src",form:"action",img:"src",area:"href",iframe:"src",embed:"src",source:"src",track:"src",input:"src"};d.getDomAttribute=function(a){if(a&&a.nodeName){var b=a.nodeName.toLowerCase();return"input"===b&&"image"!==a.type?void 0:d.domAttributes[b]}};d.encode=E;d.decode=decodeURIComponent;d.iso8859=function(){d.encode=escape;d.decode=unescape};d.unicode=function(){d.encode=E;d.decode=decodeURIComponent};d.characters= +{pathname:{encode:{expression:/%(24|26|2B|2C|3B|3D|3A|40)/ig,map:{"%24":"$","%26":"&","%2B":"+","%2C":",","%3B":";","%3D":"=","%3A":":","%40":"@"}},decode:{expression:/[\/\?#]/g,map:{"/":"%2F","?":"%3F","#":"%23"}}},reserved:{encode:{expression:/%(21|23|24|26|27|28|29|2A|2B|2C|2F|3A|3B|3D|3F|40|5B|5D)/ig,map:{"%3A":":","%2F":"/","%3F":"?","%23":"#","%5B":"[","%5D":"]","%40":"@","%21":"!","%24":"$","%26":"&","%27":"'","%28":"(","%29":")","%2A":"*","%2B":"+","%2C":",","%3B":";","%3D":"="}}}};d.encodeQuery= +function(a,b){var c=d.encode(a+"");return b?c.replace(/%20/g,"+"):c};d.decodeQuery=function(a,b){a+="";try{return d.decode(b?a.replace(/\+/g,"%20"):a)}catch(c){return a}};d.recodePath=function(a){a=(a+"").split("/");for(var b=0,c=a.length;bd)return a.charAt(0)===b.charAt(0)&&"/"===a.charAt(0)?"/":"";if("/"!==a.charAt(d)||"/"!==b.charAt(d))d=a.substring(0,d).lastIndexOf("/");return a.substring(0,d+1)};d.withinString=function(a,b,c){c||(c={});var h=c.start||d.findUri.start,e=c.end||d.findUri.end,f=c.trim||d.findUri.trim,k=/[a-z0-9-]=["']?$/i;for(h.lastIndex=0;;){var g=h.exec(a);if(!g)break;g=g.index;if(c.ignoreHtml){var l=a.slice(Math.max(g-3,0), +g);if(l&&k.test(l))continue}var l=g+a.slice(g).search(e),q=a.slice(g,l).replace(f,"");c.ignore&&c.ignore.test(q)||(l=g+q.length,q=b(q,g,l,a),a=a.slice(0,g)+q+a.slice(l),h.lastIndex=g+q.length)}h.lastIndex=0;return a};d.ensureValidHostname=function(a){if(a.match(d.invalid_hostname_characters)){if(!e)throw new TypeError("Hostname '"+a+"' contains characters other than [A-Z0-9.-] and Punycode.js is not available");if(e.toASCII(a).match(d.invalid_hostname_characters))throw new TypeError("Hostname '"+ +a+"' contains characters other than [A-Z0-9.-]");}};d.noConflict=function(a){if(a)return a={URI:this.noConflict()},URITemplate&&"function"==typeof URITemplate.noConflict&&(a.URITemplate=URITemplate.noConflict()),k&&"function"==typeof k.noConflict&&(a.IPv6=k.noConflict()),SecondLevelDomains&&"function"==typeof SecondLevelDomains.noConflict&&(a.SecondLevelDomains=SecondLevelDomains.noConflict()),a;m.URI===this&&(m.URI=C);return this};f.build=function(a){if(!0===a)this._deferred_build=!0;else if(void 0=== +a||this._deferred_build)this._string=d.build(this._parts),this._deferred_build=!1;return this};f.clone=function(){return new d(this)};f.valueOf=f.toString=function(){return this.build(!1)._string};s={protocol:"protocol",username:"username",password:"password",hostname:"hostname",port:"port"};y=function(a){return function(b,c){if(void 0===b)return this._parts[a]||"";this._parts[a]=b||null;this.build(!c);return this}};for(v in s)f[v]=y(s[v]);s={query:"?",fragment:"#"};y=function(a,b){return function(c, +d){if(void 0===c)return this._parts[a]||"";null!==c&&(c+="",c.charAt(0)===b&&(c=c.substring(1)));this._parts[a]=c;this.build(!d);return this}};for(v in s)f[v]=y(v,s[v]);s={search:["?","query"],hash:["#","fragment"]};y=function(a,b){return function(c,d){var e=this[a](c,d);return"string"===typeof e&&e.length?b+e:e}};for(v in s)f[v]=y(s[v][1],s[v][0]);f.pathname=function(a,b){if(void 0===a||!0===a){var c=this._parts.path||(this._parts.hostname?"/":"");return a?d.decodePath(c):c}this._parts.path=a?d.recodePath(a): +"/";this.build(!b);return this};f.path=f.pathname;f.href=function(a,b){var c;if(void 0===a)return this.toString();this._string="";this._parts=d._parts();var h=a instanceof d,e="object"===typeof a&&(a.hostname||a.path||a.pathname);a.nodeName&&(e=d.getDomAttribute(a),a=a[e]||"",e=!1);!h&&e&&void 0!==a.pathname&&(a=a.toString());if("string"===typeof a)this._parts=d.parse(a,this._parts);else if(h||e)for(c in h=h?a._parts:a,h)w.call(this._parts,c)&&(this._parts[c]=h[c]);else throw new TypeError("invalid input"); +this.build(!b);return this};f.is=function(a){var b=!1,c=!1,h=!1,e=!1,f=!1,g=!1,k=!1,l=!this._parts.urn;this._parts.hostname&&(l=!1,c=d.ip4_expression.test(this._parts.hostname),h=d.ip6_expression.test(this._parts.hostname),b=c||h,f=(e=!b)&&u&&u.has(this._parts.hostname),g=e&&d.idn_expression.test(this._parts.hostname),k=e&&d.punycode_expression.test(this._parts.hostname));switch(a.toLowerCase()){case "relative":return l;case "absolute":return!l;case "domain":case "name":return e;case "sld":return f; +case "ip":return b;case "ip4":case "ipv4":case "inet4":return c;case "ip6":case "ipv6":case "inet6":return h;case "idn":return g;case "url":return!this._parts.urn;case "urn":return!!this._parts.urn;case "punycode":return k}return null};var I=f.protocol,H=f.port,G=f.hostname;f.protocol=function(a,b){if(void 0!==a&&a&&(a=a.replace(/:(\/\/)?$/,""),!a.match(d.protocol_expression)))throw new TypeError("Protocol '"+a+"' contains characters other than [A-Z0-9.+-] or doesn't start with [A-Z]");return I.call(this, +a,b)};f.scheme=f.protocol;f.port=function(a,b){if(this._parts.urn)return void 0===a?"":this;if(void 0!==a&&(0===a&&(a=null),a&&(a+="",":"===a.charAt(0)&&(a=a.substring(1)),a.match(/[^0-9]/))))throw new TypeError("Port '"+a+"' contains characters other than [0-9]");return H.call(this,a,b)};f.hostname=function(a,b){if(this._parts.urn)return void 0===a?"":this;if(void 0!==a){var c={};d.parseHost(a,c);a=c.hostname}return G.call(this,a,b)};f.host=function(a,b){if(this._parts.urn)return void 0===a?"":this; +if(void 0===a)return this._parts.hostname?d.buildHost(this._parts):"";d.parseHost(a,this._parts);this.build(!b);return this};f.authority=function(a,b){if(this._parts.urn)return void 0===a?"":this;if(void 0===a)return this._parts.hostname?d.buildAuthority(this._parts):"";d.parseAuthority(a,this._parts);this.build(!b);return this};f.userinfo=function(a,b){if(this._parts.urn)return void 0===a?"":this;if(void 0===a){if(!this._parts.username)return"";var c=d.buildUserinfo(this._parts);return c.substring(0, +c.length-1)}"@"!==a[a.length-1]&&(a+="@");d.parseUserinfo(a,this._parts);this.build(!b);return this};f.resource=function(a,b){var c;if(void 0===a)return this.path()+this.search()+this.hash();c=d.parse(a);this._parts.path=c.path;this._parts.query=c.query;this._parts.fragment=c.fragment;this.build(!b);return this};f.subdomain=function(a,b){if(this._parts.urn)return void 0===a?"":this;if(void 0===a){if(!this._parts.hostname||this.is("IP"))return"";var c=this._parts.hostname.length-this.domain().length- +1;return this._parts.hostname.substring(0,c)||""}c=this._parts.hostname.length-this.domain().length;c=this._parts.hostname.substring(0,c);c=RegExp("^"+g(c));a&&"."!==a.charAt(a.length-1)&&(a+=".");a&&d.ensureValidHostname(a);this._parts.hostname=this._parts.hostname.replace(c,a);this.build(!b);return this};f.domain=function(a,b){if(this._parts.urn)return void 0===a?"":this;"boolean"===typeof a&&(b=a,a=void 0);if(void 0===a){if(!this._parts.hostname||this.is("IP"))return"";var c=this._parts.hostname.match(/\./g); +if(c&&2>c.length)return this._parts.hostname;c=this._parts.hostname.length-this.tld(b).length-1;c=this._parts.hostname.lastIndexOf(".",c-1)+1;return this._parts.hostname.substring(c)||""}if(!a)throw new TypeError("cannot set domain empty");d.ensureValidHostname(a);!this._parts.hostname||this.is("IP")?this._parts.hostname=a:(c=RegExp(g(this.domain())+"$"),this._parts.hostname=this._parts.hostname.replace(c,a));this.build(!b);return this};f.tld=function(a,b){if(this._parts.urn)return void 0===a?"": +this;"boolean"===typeof a&&(b=a,a=void 0);if(void 0===a){if(!this._parts.hostname||this.is("IP"))return"";var c=this._parts.hostname.lastIndexOf("."),c=this._parts.hostname.substring(c+1);return!0!==b&&u&&u.list[c.toLowerCase()]?u.get(this._parts.hostname)||c:c}if(a)if(a.match(/[^a-zA-Z0-9-]/))if(u&&u.is(a))c=RegExp(g(this.tld())+"$"),this._parts.hostname=this._parts.hostname.replace(c,a);else throw new TypeError("TLD '"+a+"' contains characters other than [A-Z0-9]");else{if(!this._parts.hostname|| +this.is("IP"))throw new ReferenceError("cannot set TLD on non-domain host");c=RegExp(g(this.tld())+"$");this._parts.hostname=this._parts.hostname.replace(c,a)}else throw new TypeError("cannot set TLD empty");this.build(!b);return this};f.directory=function(a,b){if(this._parts.urn)return void 0===a?"":this;if(void 0===a||!0===a){if(!this._parts.path&&!this._parts.hostname)return"";if("/"===this._parts.path)return"/";var c=this._parts.path.length-this.filename().length-1,c=this._parts.path.substring(0, +c)||(this._parts.hostname?"/":"");return a?d.decodePath(c):c}c=this._parts.path.length-this.filename().length;c=this._parts.path.substring(0,c);c=RegExp("^"+g(c));this.is("relative")||(a||(a="/"),"/"!==a.charAt(0)&&(a="/"+a));a&&"/"!==a.charAt(a.length-1)&&(a+="/");a=d.recodePath(a);this._parts.path=this._parts.path.replace(c,a);this.build(!b);return this};f.filename=function(a,b){if(this._parts.urn)return void 0===a?"":this;if(void 0===a||!0===a){if(!this._parts.path||"/"===this._parts.path)return""; +var c=this._parts.path.lastIndexOf("/"),c=this._parts.path.substring(c+1);return a?d.decodePathSegment(c):c}c=!1;"/"===a.charAt(0)&&(a=a.substring(1));a.match(/\.?\//)&&(c=!0);var h=RegExp(g(this.filename())+"$");a=d.recodePath(a);this._parts.path=this._parts.path.replace(h,a);c?this.normalizePath(b):this.build(!b);return this};f.suffix=function(a,b){if(this._parts.urn)return void 0===a?"":this;if(void 0===a||!0===a){if(!this._parts.path||"/"===this._parts.path)return"";var c=this.filename(),h=c.lastIndexOf("."); +if(-1===h)return"";c=c.substring(h+1);c=/^[a-z0-9%]+$/i.test(c)?c:"";return a?d.decodePathSegment(c):c}"."===a.charAt(0)&&(a=a.substring(1));if(c=this.suffix())h=a?RegExp(g(c)+"$"):RegExp(g("."+c)+"$");else{if(!a)return this;this._parts.path+="."+d.recodePath(a)}h&&(a=d.recodePath(a),this._parts.path=this._parts.path.replace(h,a));this.build(!b);return this};f.segment=function(a,b,c){var d=this._parts.urn?":":"/",e=this.path(),f="/"===e.substring(0,1),e=e.split(d);void 0!==a&&"number"!==typeof a&& +(c=b,b=a,a=void 0);if(void 0!==a&&"number"!==typeof a)throw Error("Bad segment '"+a+"', must be 0-based integer");f&&e.shift();0>a&&(a=Math.max(e.length+a,0));if(void 0===b)return void 0===a?e:e[a];if(null===a||void 0===e[a])if(l(b)){e=[];a=0;for(var g=b.length;a= 'a' && chr <= 'z') || ((chr >= 'A' && chr <= 'Z')); + } + + function isDigit (chr) { + return chr >= '0' && chr <= '9'; + } + + function isHexDigit (chr) { + return isDigit(chr) || (chr >= 'a' && chr <= 'f') || (chr >= 'A' && chr <= 'F'); + } + + return { + isAlpha: isAlpha, + isDigit: isDigit, + isHexDigit: isHexDigit + }; +}()); + +var pctEncoder = (function () { + var utf8 = { + encode: function (chr) { + // see http://ecmanaut.blogspot.de/2006/07/encoding-decoding-utf8-in-javascript.html + return unescape(encodeURIComponent(chr)); + }, + numBytes: function (firstCharCode) { + if (firstCharCode <= 0x7F) { + return 1; + } + else if (0xC2 <= firstCharCode && firstCharCode <= 0xDF) { + return 2; + } + else if (0xE0 <= firstCharCode && firstCharCode <= 0xEF) { + return 3; + } + else if (0xF0 <= firstCharCode && firstCharCode <= 0xF4) { + return 4; + } + // no valid first octet + return 0; + }, + isValidFollowingCharCode: function (charCode) { + return 0x80 <= charCode && charCode <= 0xBF; + } + }; + + /** + * encodes a character, if needed or not. + * @param chr + * @return pct-encoded character + */ + function encodeCharacter (chr) { + var + result = '', + octets = utf8.encode(chr), + octet, + index; + for (index = 0; index < octets.length; index += 1) { + octet = octets.charCodeAt(index); + result += '%' + (octet < 0x10 ? '0' : '') + octet.toString(16).toUpperCase(); + } + return result; + } + + /** + * Returns, whether the given text at start is in the form 'percent hex-digit hex-digit', like '%3F' + * @param text + * @param start + * @return {boolean|*|*} + */ + function isPercentDigitDigit (text, start) { + return text.charAt(start) === '%' && charHelper.isHexDigit(text.charAt(start + 1)) && charHelper.isHexDigit(text.charAt(start + 2)); + } + + /** + * Parses a hex number from start with length 2. + * @param text a string + * @param start the start index of the 2-digit hex number + * @return {Number} + */ + function parseHex2 (text, start) { + return parseInt(text.substr(start, 2), 16); + } + + /** + * Returns whether or not the given char sequence is a correctly pct-encoded sequence. + * @param chr + * @return {boolean} + */ + function isPctEncoded (chr) { + if (!isPercentDigitDigit(chr, 0)) { + return false; + } + var firstCharCode = parseHex2(chr, 1); + var numBytes = utf8.numBytes(firstCharCode); + if (numBytes === 0) { + return false; + } + for (var byteNumber = 1; byteNumber < numBytes; byteNumber += 1) { + if (!isPercentDigitDigit(chr, 3*byteNumber) || !utf8.isValidFollowingCharCode(parseHex2(chr, 3*byteNumber + 1))) { + return false; + } + } + return true; + } + + /** + * Reads as much as needed from the text, e.g. '%20' or '%C3%B6'. It does not decode! + * @param text + * @param startIndex + * @return the character or pct-string of the text at startIndex + */ + function pctCharAt(text, startIndex) { + var chr = text.charAt(startIndex); + if (!isPercentDigitDigit(text, startIndex)) { + return chr; + } + var utf8CharCode = parseHex2(text, startIndex + 1); + var numBytes = utf8.numBytes(utf8CharCode); + if (numBytes === 0) { + return chr; + } + for (var byteNumber = 1; byteNumber < numBytes; byteNumber += 1) { + if (!isPercentDigitDigit(text, startIndex + 3 * byteNumber) || !utf8.isValidFollowingCharCode(parseHex2(text, startIndex + 3 * byteNumber + 1))) { + return chr; + } + } + return text.substr(startIndex, 3 * numBytes); + } + + return { + encodeCharacter: encodeCharacter, + isPctEncoded: isPctEncoded, + pctCharAt: pctCharAt + }; +}()); + +var rfcCharHelper = (function () { + + /** + * Returns if an character is an varchar character according 2.3 of rfc 6570 + * @param chr + * @return (Boolean) + */ + function isVarchar (chr) { + return charHelper.isAlpha(chr) || charHelper.isDigit(chr) || chr === '_' || pctEncoder.isPctEncoded(chr); + } + + /** + * Returns if chr is an unreserved character according 1.5 of rfc 6570 + * @param chr + * @return {Boolean} + */ + function isUnreserved (chr) { + return charHelper.isAlpha(chr) || charHelper.isDigit(chr) || chr === '-' || chr === '.' || chr === '_' || chr === '~'; + } + + /** + * Returns if chr is an reserved character according 1.5 of rfc 6570 + * or the percent character mentioned in 3.2.1. + * @param chr + * @return {Boolean} + */ + function isReserved (chr) { + return chr === ':' || chr === '/' || chr === '?' || chr === '#' || chr === '[' || chr === ']' || chr === '@' || chr === '!' || chr === '$' || chr === '&' || chr === '(' || + chr === ')' || chr === '*' || chr === '+' || chr === ',' || chr === ';' || chr === '=' || chr === "'"; + } + + return { + isVarchar: isVarchar, + isUnreserved: isUnreserved, + isReserved: isReserved + }; + +}()); + +/** + * encoding of rfc 6570 + */ +var encodingHelper = (function () { + + function encode (text, passReserved) { + var + result = '', + index, + chr = ''; + if (typeof text === "number" || typeof text === "boolean") { + text = text.toString(); + } + for (index = 0; index < text.length; index += chr.length) { + chr = text.charAt(index); + result += rfcCharHelper.isUnreserved(chr) || (passReserved && rfcCharHelper.isReserved(chr)) ? chr : pctEncoder.encodeCharacter(chr); + } + return result; + } + + function encodePassReserved (text) { + return encode(text, true); + } + + function encodeLiteralCharacter (literal, index) { + var chr = pctEncoder.pctCharAt(literal, index); + if (chr.length > 1) { + return chr; + } + else { + return rfcCharHelper.isReserved(chr) || rfcCharHelper.isUnreserved(chr) ? chr : pctEncoder.encodeCharacter(chr); + } + } + + function encodeLiteral (literal) { + var + result = '', + index, + chr = ''; + for (index = 0; index < literal.length; index += chr.length) { + chr = pctEncoder.pctCharAt(literal, index); + if (chr.length > 1) { + result += chr; + } + else { + result += rfcCharHelper.isReserved(chr) || rfcCharHelper.isUnreserved(chr) ? chr : pctEncoder.encodeCharacter(chr); + } + } + return result; + } + + return { + encode: encode, + encodePassReserved: encodePassReserved, + encodeLiteral: encodeLiteral, + encodeLiteralCharacter: encodeLiteralCharacter + }; + +}()); + + +// the operators defined by rfc 6570 +var operators = (function () { + + var + bySymbol = {}; + + function create (symbol) { + bySymbol[symbol] = { + symbol: symbol, + separator: (symbol === '?') ? '&' : (symbol === '' || symbol === '+' || symbol === '#') ? ',' : symbol, + named: symbol === ';' || symbol === '&' || symbol === '?', + ifEmpty: (symbol === '&' || symbol === '?') ? '=' : '', + first: (symbol === '+' ) ? '' : symbol, + encode: (symbol === '+' || symbol === '#') ? encodingHelper.encodePassReserved : encodingHelper.encode, + toString: function () { + return this.symbol; + } + }; + } + + create(''); + create('+'); + create('#'); + create('.'); + create('/'); + create(';'); + create('?'); + create('&'); + return { + valueOf: function (chr) { + if (bySymbol[chr]) { + return bySymbol[chr]; + } + if ("=,!@|".indexOf(chr) >= 0) { + return null; + } + return bySymbol['']; + } + }; +}()); + + +/** + * Detects, whether a given element is defined in the sense of rfc 6570 + * Section 2.3 of the RFC makes clear defintions: + * * undefined and null are not defined. + * * the empty string is defined + * * an array ("list") is defined, if it is not empty (even if all elements are not defined) + * * an object ("map") is defined, if it contains at least one property with defined value + * @param object + * @return {Boolean} + */ +function isDefined (object) { + var + propertyName; + if (object === null || object === undefined) { + return false; + } + if (objectHelper.isArray(object)) { + // Section 2.3: A variable defined as a list value is considered undefined if the list contains zero members + return object.length > 0; + } + if (typeof object === "string" || typeof object === "number" || typeof object === "boolean") { + // falsy values like empty strings, false or 0 are "defined" + return true; + } + // else Object + for (propertyName in object) { + if (object.hasOwnProperty(propertyName) && isDefined(object[propertyName])) { + return true; + } + } + return false; +} + +var LiteralExpression = (function () { + function LiteralExpression (literal) { + this.literal = encodingHelper.encodeLiteral(literal); + } + + LiteralExpression.prototype.expand = function () { + return this.literal; + }; + + LiteralExpression.prototype.toString = LiteralExpression.prototype.expand; + + return LiteralExpression; +}()); + +var parse = (function () { + + function parseExpression (expressionText) { + var + operator, + varspecs = [], + varspec = null, + varnameStart = null, + maxLengthStart = null, + index, + chr = ''; + + function closeVarname () { + var varname = expressionText.substring(varnameStart, index); + if (varname.length === 0) { + throw new UriTemplateError({expressionText: expressionText, message: "a varname must be specified", position: index}); + } + varspec = {varname: varname, exploded: false, maxLength: null}; + varnameStart = null; + } + + function closeMaxLength () { + if (maxLengthStart === index) { + throw new UriTemplateError({expressionText: expressionText, message: "after a ':' you have to specify the length", position: index}); + } + varspec.maxLength = parseInt(expressionText.substring(maxLengthStart, index), 10); + maxLengthStart = null; + } + + operator = (function (operatorText) { + var op = operators.valueOf(operatorText); + if (op === null) { + throw new UriTemplateError({expressionText: expressionText, message: "illegal use of reserved operator", position: index, operator: operatorText}); + } + return op; + }(expressionText.charAt(0))); + index = operator.symbol.length; + + varnameStart = index; + + for (; index < expressionText.length; index += chr.length) { + chr = pctEncoder.pctCharAt(expressionText, index); + + if (varnameStart !== null) { + // the spec says: varname = varchar *( ["."] varchar ) + // so a dot is allowed except for the first char + if (chr === '.') { + if (varnameStart === index) { + throw new UriTemplateError({expressionText: expressionText, message: "a varname MUST NOT start with a dot", position: index}); + } + continue; + } + if (rfcCharHelper.isVarchar(chr)) { + continue; + } + closeVarname(); + } + if (maxLengthStart !== null) { + if (index === maxLengthStart && chr === '0') { + throw new UriTemplateError({expressionText: expressionText, message: "A :prefix must not start with digit 0", position: index}); + } + if (charHelper.isDigit(chr)) { + if (index - maxLengthStart >= 4) { + throw new UriTemplateError({expressionText: expressionText, message: "A :prefix must have max 4 digits", position: index}); + } + continue; + } + closeMaxLength(); + } + if (chr === ':') { + if (varspec.maxLength !== null) { + throw new UriTemplateError({expressionText: expressionText, message: "only one :maxLength is allowed per varspec", position: index}); + } + if (varspec.exploded) { + throw new UriTemplateError({expressionText: expressionText, message: "an exploeded varspec MUST NOT be varspeced", position: index}); + } + maxLengthStart = index + 1; + continue; + } + if (chr === '*') { + if (varspec === null) { + throw new UriTemplateError({expressionText: expressionText, message: "exploded without varspec", position: index}); + } + if (varspec.exploded) { + throw new UriTemplateError({expressionText: expressionText, message: "exploded twice", position: index}); + } + if (varspec.maxLength) { + throw new UriTemplateError({expressionText: expressionText, message: "an explode (*) MUST NOT follow to a prefix", position: index}); + } + varspec.exploded = true; + continue; + } + // the only legal character now is the comma + if (chr === ',') { + varspecs.push(varspec); + varspec = null; + varnameStart = index + 1; + continue; + } + throw new UriTemplateError({expressionText: expressionText, message: "illegal character", character: chr, position: index}); + } // for chr + if (varnameStart !== null) { + closeVarname(); + } + if (maxLengthStart !== null) { + closeMaxLength(); + } + varspecs.push(varspec); + return new VariableExpression(expressionText, operator, varspecs); + } + + function escape_regexp_string(string) { + // http://simonwillison.net/2006/Jan/20/escape/ + return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&"); + } + + function parse (uriTemplateText) { + // assert filled string + var + index, + chr, + expressions = [], + expression, + braceOpenIndex = null, + regexp_string = '', + can_match = true, + literalStart = 0; + for (index = 0; index < uriTemplateText.length; index += 1) { + chr = uriTemplateText.charAt(index); + if (literalStart !== null) { + if (chr === '}') { + throw new UriTemplateError({templateText: uriTemplateText, message: "unopened brace closed", position: index}); + } + if (chr === '{') { + if (literalStart < index) { + expression = new LiteralExpression(uriTemplateText.substring(literalStart, index)); + expressions.push(expression); + regexp_string += escape_regexp_string( + expression.literal); + } + literalStart = null; + braceOpenIndex = index; + } + continue; + } + + if (braceOpenIndex !== null) { + // here just { is forbidden + if (chr === '{') { + throw new UriTemplateError({templateText: uriTemplateText, message: "brace already opened", position: index}); + } + if (chr === '}') { + if (braceOpenIndex + 1 === index) { + throw new UriTemplateError({templateText: uriTemplateText, message: "empty braces", position: braceOpenIndex}); + } + try { + expression = parseExpression(uriTemplateText.substring(braceOpenIndex + 1, index)); + } + catch (error) { + if (error.prototype === UriTemplateError.prototype) { + throw new UriTemplateError({templateText: uriTemplateText, message: error.options.message, position: braceOpenIndex + error.options.position, details: error.options}); + } + throw error; + } + expressions.push(expression); + if (expression.operator.symbol.length === 0) { + regexp_string += "([^/]+)"; + } else { + can_match = false; + } + braceOpenIndex = null; + literalStart = index + 1; + } + continue; + } + throw new Error('reached unreachable code'); + } + if (braceOpenIndex !== null) { + throw new UriTemplateError({templateText: uriTemplateText, message: "unclosed brace", position: braceOpenIndex}); + } + if (literalStart < uriTemplateText.length) { + expression = new LiteralExpression(uriTemplateText.substring(literalStart)); + expressions.push(expression); + regexp_string += escape_regexp_string(expression.literal); + } + if (can_match === false) { + regexp_string = undefined; + } + return new UriTemplate(uriTemplateText, expressions, regexp_string); + } + + return parse; +}()); + +var VariableExpression = (function () { + // helper function if JSON is not available + function prettyPrint (value) { + return (JSON && JSON.stringify) ? JSON.stringify(value) : value; + } + + function isEmpty (value) { + if (!isDefined(value)) { + return true; + } + if (objectHelper.isString(value)) { + return value === ''; + } + if (objectHelper.isNumber(value) || objectHelper.isBoolean(value)) { + return false; + } + if (objectHelper.isArray(value)) { + return value.length === 0; + } + for (var propertyName in value) { + if (value.hasOwnProperty(propertyName)) { + return false; + } + } + return true; + } + + function propertyArray (object) { + var + result = [], + propertyName; + for (propertyName in object) { + if (object.hasOwnProperty(propertyName)) { + result.push({name: propertyName, value: object[propertyName]}); + } + } + return result; + } + + function VariableExpression (templateText, operator, varspecs) { + this.templateText = templateText; + this.operator = operator; + this.varspecs = varspecs; + } + + VariableExpression.prototype.toString = function () { + return this.templateText; + }; + + function expandSimpleValue(varspec, operator, value) { + var result = ''; + value = value.toString(); + if (operator.named) { + result += encodingHelper.encodeLiteral(varspec.varname); + if (value === '') { + result += operator.ifEmpty; + return result; + } + result += '='; + } + if (varspec.maxLength !== null) { + value = value.substr(0, varspec.maxLength); + } + result += operator.encode(value); + return result; + } + + function valueDefined (nameValue) { + return isDefined(nameValue.value); + } + + function expandNotExploded(varspec, operator, value) { + var + arr = [], + result = ''; + if (operator.named) { + result += encodingHelper.encodeLiteral(varspec.varname); + if (isEmpty(value)) { + result += operator.ifEmpty; + return result; + } + result += '='; + } + if (objectHelper.isArray(value)) { + arr = value; + arr = objectHelper.filter(arr, isDefined); + arr = objectHelper.map(arr, operator.encode); + result += objectHelper.join(arr, ','); + } + else { + arr = propertyArray(value); + arr = objectHelper.filter(arr, valueDefined); + arr = objectHelper.map(arr, function (nameValue) { + return operator.encode(nameValue.name) + ',' + operator.encode(nameValue.value); + }); + result += objectHelper.join(arr, ','); + } + return result; + } + + function expandExplodedNamed (varspec, operator, value) { + var + isArray = objectHelper.isArray(value), + arr = []; + if (isArray) { + arr = value; + arr = objectHelper.filter(arr, isDefined); + arr = objectHelper.map(arr, function (listElement) { + var tmp = encodingHelper.encodeLiteral(varspec.varname); + if (isEmpty(listElement)) { + tmp += operator.ifEmpty; + } + else { + tmp += '=' + operator.encode(listElement); + } + return tmp; + }); + } + else { + arr = propertyArray(value); + arr = objectHelper.filter(arr, valueDefined); + arr = objectHelper.map(arr, function (nameValue) { + var tmp = encodingHelper.encodeLiteral(nameValue.name); + if (isEmpty(nameValue.value)) { + tmp += operator.ifEmpty; + } + else { + tmp += '=' + operator.encode(nameValue.value); + } + return tmp; + }); + } + return objectHelper.join(arr, operator.separator); + } + + function expandExplodedUnnamed (operator, value) { + var + arr = [], + result = ''; + if (objectHelper.isArray(value)) { + arr = value; + arr = objectHelper.filter(arr, isDefined); + arr = objectHelper.map(arr, operator.encode); + result += objectHelper.join(arr, operator.separator); + } + else { + arr = propertyArray(value); + arr = objectHelper.filter(arr, function (nameValue) { + return isDefined(nameValue.value); + }); + arr = objectHelper.map(arr, function (nameValue) { + return operator.encode(nameValue.name) + '=' + operator.encode(nameValue.value); + }); + result += objectHelper.join(arr, operator.separator); + } + return result; + } + + + VariableExpression.prototype.expand = function (variables) { + var + expanded = [], + index, + varspec, + value, + valueIsArr, + oneExploded = false, + operator = this.operator; + + // expand each varspec and join with operator's separator + for (index = 0; index < this.varspecs.length; index += 1) { + varspec = this.varspecs[index]; + value = variables[varspec.varname]; + // if (!isDefined(value)) { + // if (variables.hasOwnProperty(varspec.name)) { + if (value === null || value === undefined) { + continue; + } + if (varspec.exploded) { + oneExploded = true; + } + valueIsArr = objectHelper.isArray(value); + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + expanded.push(expandSimpleValue(varspec, operator, value)); + } + else if (varspec.maxLength && isDefined(value)) { + // 2.4.1 of the spec says: "Prefix modifiers are not applicable to variables that have composite values." + throw new Error('Prefix modifiers are not applicable to variables that have composite values. You tried to expand ' + this + " with " + prettyPrint(value)); + } + else if (!varspec.exploded) { + if (operator.named || !isEmpty(value)) { + expanded.push(expandNotExploded(varspec, operator, value)); + } + } + else if (isDefined(value)) { + if (operator.named) { + expanded.push(expandExplodedNamed(varspec, operator, value)); + } + else { + expanded.push(expandExplodedUnnamed(operator, value)); + } + } + } + + if (expanded.length === 0) { + return ""; + } + else { + return operator.first + objectHelper.join(expanded, operator.separator); + } + }; + + return VariableExpression; +}()); + +var UriTemplate = (function () { + function UriTemplate (templateText, expressions, regexp_string) { + this.templateText = templateText; + this.expressions = expressions; + + if (regexp_string !== undefined) { + this.regexp = new RegExp("^" + regexp_string + "$"); + } + + objectHelper.deepFreeze(this); + } + + UriTemplate.prototype.toString = function () { + return this.templateText; + }; + + UriTemplate.prototype.expand = function (variables) { + // this.expressions.map(function (expression) {return expression.expand(variables);}).join(''); + var + index, + result = ''; + for (index = 0; index < this.expressions.length; index += 1) { + result += this.expressions[index].expand(variables); + } + return result; + }; + + UriTemplate.prototype.extract = function (text) { + var expression_index, + extracted_index = 1, + expression, + varspec, + matched = true, + variables = {}, + result; + + if ((this.regexp !== undefined) && (this.regexp.test(text))) { + result = this.regexp.exec(text); + for (expression_index = 0; expression_index < this.expressions.length; expression_index += 1) { + expression = this.expressions[expression_index]; + if (expression.literal === undefined) { + if ((expression.operator !== undefined) && (expression.operator.symbol.length === 0) && (expression.varspecs.length === 1)) { + varspec = expression.varspecs[0]; + if ((varspec.exploded === false) && (varspec.maxLength === null)) { + if (result[extracted_index].indexOf(',') === -1) { + variables[varspec.varname] = decodeURIComponent(result[extracted_index]); + extracted_index += 1; + } else { + matched = false; + } + } else { + matched = false; + } + } else { + matched = false; + } + } + } + if (matched) { + return variables; + } + } + return false; + }; + + UriTemplate.parse = parse; + UriTemplate.UriTemplateError = UriTemplateError; + return UriTemplate; +}()); + + exportCallback(UriTemplate); + +}(function (UriTemplate) { + "use strict"; + // export UriTemplate, when module is present, or pass it to window or global + if (typeof module !== "undefined") { + module.exports = UriTemplate; + } + else if (typeof define === "function") { + define([],function() { + return UriTemplate; + }); + } + else if (typeof window !== "undefined") { + window.UriTemplate = UriTemplate; + } + else { + global.UriTemplate = UriTemplate; + } + } +)); +;// Copyright (c) 2013 Pieroxy +// This work is free. You can redistribute it and/or modify it +// under the terms of the WTFPL, Version 2 +// For more information see LICENSE.txt or http://www.wtfpl.net/ +// +// For more information, the home page: +// http://pieroxy.net/blog/pages/lz-string/testing.html +// +// LZ-based compression algorithm, version 1.4.4 +var LZString = (function() { + +// private property +var f = String.fromCharCode; +var keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; +var keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$"; +var baseReverseDic = {}; + +function getBaseValue(alphabet, character) { + if (!baseReverseDic[alphabet]) { + baseReverseDic[alphabet] = {}; + for (var i=0 ; i>> 8; + buf[i*2+1] = current_value % 256; + } + return buf; + }, + + //decompress from uint8array (UCS-2 big endian format) + decompressFromUint8Array:function (compressed) { + if (compressed===null || compressed===undefined){ + return LZString.decompress(compressed); + } else { + var buf=new Array(compressed.length/2); // 2 bytes per character + for (var i=0, TotalLen=buf.length; i> 1; + } + } else { + value = 1; + for (i=0 ; i> 1; + } + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + delete context_dictionaryToCreate[context_w]; + } else { + value = context_dictionary[context_w]; + for (i=0 ; i> 1; + } + + + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + // Add wc to the dictionary. + context_dictionary[context_wc] = context_dictSize++; + context_w = String(context_c); + } + } + + // Output the code for w. + if (context_w !== "") { + if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) { + if (context_w.charCodeAt(0)<256) { + for (i=0 ; i> 1; + } + } else { + value = 1; + for (i=0 ; i> 1; + } + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + delete context_dictionaryToCreate[context_w]; + } else { + value = context_dictionary[context_w]; + for (i=0 ; i> 1; + } + + + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + } + + // Mark the end of the stream + value = 2; + for (i=0 ; i> 1; + } + + // Flush the last char + while (true) { + context_data_val = (context_data_val << 1); + if (context_data_position == bitsPerChar-1) { + context_data.push(getCharFromInt(context_data_val)); + break; + } + else context_data_position++; + } + return context_data.join(''); + }, + + decompress: function (compressed) { + if (compressed == null) return ""; + if (compressed == "") return null; + return LZString._decompress(compressed.length, 32768, function(index) { return compressed.charCodeAt(index); }); + }, + + _decompress: function (length, resetValue, getNextValue) { + var dictionary = [], + next, + enlargeIn = 4, + dictSize = 4, + numBits = 3, + entry = "", + result = [], + i, + w, + bits, resb, maxpower, power, + c, + data = {val:getNextValue(0), position:resetValue, index:1}; + + for (i = 0; i < 3; i += 1) { + dictionary[i] = i; + } + + bits = 0; + maxpower = Math.pow(2,2); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + switch (next = bits) { + case 0: + bits = 0; + maxpower = Math.pow(2,8); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + c = f(bits); + break; + case 1: + bits = 0; + maxpower = Math.pow(2,16); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + c = f(bits); + break; + case 2: + return ""; + } + dictionary[3] = c; + w = c; + result.push(c); + while (true) { + if (data.index > length) { + return ""; + } + + bits = 0; + maxpower = Math.pow(2,numBits); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + switch (c = bits) { + case 0: + bits = 0; + maxpower = Math.pow(2,8); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + dictionary[dictSize++] = f(bits); + c = dictSize-1; + enlargeIn--; + break; + case 1: + bits = 0; + maxpower = Math.pow(2,16); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + dictionary[dictSize++] = f(bits); + c = dictSize-1; + enlargeIn--; + break; + case 2: + return result.join(''); + } + + if (enlargeIn == 0) { + enlargeIn = Math.pow(2, numBits); + numBits++; + } + + if (dictionary[c]) { + entry = dictionary[c]; + } else { + if (c === dictSize) { + entry = w + w.charAt(0); + } else { + return null; + } + } + result.push(entry); + + // Add w+entry[0] to the dictionary. + dictionary[dictSize++] = w + entry.charAt(0); + enlargeIn--; + + w = entry; + + if (enlargeIn == 0) { + enlargeIn = Math.pow(2, numBits); + numBits++; + } + + } + } +}; + return LZString; +})(); + +if (typeof define === 'function' && define.amd) { + define(function () { return LZString; }); +} else if( typeof module !== 'undefined' && module != null ) { + module.exports = LZString +} +;//! moment.js + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + global.moment = factory() +}(this, (function () { 'use strict'; + +var hookCallback; + +function hooks () { + return hookCallback.apply(null, arguments); +} + +// This is done to register the method called with moment() +// without creating circular dependencies. +function setHookCallback (callback) { + hookCallback = callback; +} + +function isArray(input) { + return input instanceof Array || Object.prototype.toString.call(input) === '[object Array]'; +} + +function isObject(input) { + // IE8 will treat undefined and null as object if it wasn't for + // input != null + return input != null && Object.prototype.toString.call(input) === '[object Object]'; +} + +function isObjectEmpty(obj) { + if (Object.getOwnPropertyNames) { + return (Object.getOwnPropertyNames(obj).length === 0); + } else { + var k; + for (k in obj) { + if (obj.hasOwnProperty(k)) { + return false; + } + } + return true; + } +} + +function isUndefined(input) { + return input === void 0; +} + +function isNumber(input) { + return typeof input === 'number' || Object.prototype.toString.call(input) === '[object Number]'; +} + +function isDate(input) { + return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]'; +} + +function map(arr, fn) { + var res = [], i; + for (i = 0; i < arr.length; ++i) { + res.push(fn(arr[i], i)); + } + return res; +} + +function hasOwnProp(a, b) { + return Object.prototype.hasOwnProperty.call(a, b); +} + +function extend(a, b) { + for (var i in b) { + if (hasOwnProp(b, i)) { + a[i] = b[i]; + } + } + + if (hasOwnProp(b, 'toString')) { + a.toString = b.toString; + } + + if (hasOwnProp(b, 'valueOf')) { + a.valueOf = b.valueOf; + } + + return a; +} + +function createUTC (input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, true).utc(); +} + +function defaultParsingFlags() { + // We need to deep clone this object. + return { + empty : false, + unusedTokens : [], + unusedInput : [], + overflow : -2, + charsLeftOver : 0, + nullInput : false, + invalidMonth : null, + invalidFormat : false, + userInvalidated : false, + iso : false, + parsedDateParts : [], + meridiem : null, + rfc2822 : false, + weekdayMismatch : false + }; +} + +function getParsingFlags(m) { + if (m._pf == null) { + m._pf = defaultParsingFlags(); + } + return m._pf; +} + +var some; +if (Array.prototype.some) { + some = Array.prototype.some; +} else { + some = function (fun) { + var t = Object(this); + var len = t.length >>> 0; + + for (var i = 0; i < len; i++) { + if (i in t && fun.call(this, t[i], i, t)) { + return true; + } + } + + return false; + }; +} + +function isValid(m) { + if (m._isValid == null) { + var flags = getParsingFlags(m); + var parsedParts = some.call(flags.parsedDateParts, function (i) { + return i != null; + }); + var isNowValid = !isNaN(m._d.getTime()) && + flags.overflow < 0 && + !flags.empty && + !flags.invalidMonth && + !flags.invalidWeekday && + !flags.weekdayMismatch && + !flags.nullInput && + !flags.invalidFormat && + !flags.userInvalidated && + (!flags.meridiem || (flags.meridiem && parsedParts)); + + if (m._strict) { + isNowValid = isNowValid && + flags.charsLeftOver === 0 && + flags.unusedTokens.length === 0 && + flags.bigHour === undefined; + } + + if (Object.isFrozen == null || !Object.isFrozen(m)) { + m._isValid = isNowValid; + } + else { + return isNowValid; + } + } + return m._isValid; +} + +function createInvalid (flags) { + var m = createUTC(NaN); + if (flags != null) { + extend(getParsingFlags(m), flags); + } + else { + getParsingFlags(m).userInvalidated = true; + } + + return m; +} + +// Plugins that add properties should also add the key here (null value), +// so we can properly clone ourselves. +var momentProperties = hooks.momentProperties = []; + +function copyConfig(to, from) { + var i, prop, val; + + if (!isUndefined(from._isAMomentObject)) { + to._isAMomentObject = from._isAMomentObject; + } + if (!isUndefined(from._i)) { + to._i = from._i; + } + if (!isUndefined(from._f)) { + to._f = from._f; + } + if (!isUndefined(from._l)) { + to._l = from._l; + } + if (!isUndefined(from._strict)) { + to._strict = from._strict; + } + if (!isUndefined(from._tzm)) { + to._tzm = from._tzm; + } + if (!isUndefined(from._isUTC)) { + to._isUTC = from._isUTC; + } + if (!isUndefined(from._offset)) { + to._offset = from._offset; + } + if (!isUndefined(from._pf)) { + to._pf = getParsingFlags(from); + } + if (!isUndefined(from._locale)) { + to._locale = from._locale; + } + + if (momentProperties.length > 0) { + for (i = 0; i < momentProperties.length; i++) { + prop = momentProperties[i]; + val = from[prop]; + if (!isUndefined(val)) { + to[prop] = val; + } + } + } + + return to; +} + +var updateInProgress = false; + +// Moment prototype object +function Moment(config) { + copyConfig(this, config); + this._d = new Date(config._d != null ? config._d.getTime() : NaN); + if (!this.isValid()) { + this._d = new Date(NaN); + } + // Prevent infinite loop in case updateOffset creates new moment + // objects. + if (updateInProgress === false) { + updateInProgress = true; + hooks.updateOffset(this); + updateInProgress = false; + } +} + +function isMoment (obj) { + return obj instanceof Moment || (obj != null && obj._isAMomentObject != null); +} + +function absFloor (number) { + if (number < 0) { + // -0 -> 0 + return Math.ceil(number) || 0; + } else { + return Math.floor(number); + } +} + +function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; + + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + value = absFloor(coercedNumber); + } + + return value; +} + +// compare two arrays, return the number of differences +function compareArrays(array1, array2, dontConvert) { + var len = Math.min(array1.length, array2.length), + lengthDiff = Math.abs(array1.length - array2.length), + diffs = 0, + i; + for (i = 0; i < len; i++) { + if ((dontConvert && array1[i] !== array2[i]) || + (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) { + diffs++; + } + } + return diffs + lengthDiff; +} + +function warn(msg) { + if (hooks.suppressDeprecationWarnings === false && + (typeof console !== 'undefined') && console.warn) { + console.warn('Deprecation warning: ' + msg); + } +} + +function deprecate(msg, fn) { + var firstTime = true; + + return extend(function () { + if (hooks.deprecationHandler != null) { + hooks.deprecationHandler(null, msg); + } + if (firstTime) { + var args = []; + var arg; + for (var i = 0; i < arguments.length; i++) { + arg = ''; + if (typeof arguments[i] === 'object') { + arg += '\n[' + i + '] '; + for (var key in arguments[0]) { + arg += key + ': ' + arguments[0][key] + ', '; + } + arg = arg.slice(0, -2); // Remove trailing comma and space + } else { + arg = arguments[i]; + } + args.push(arg); + } + warn(msg + '\nArguments: ' + Array.prototype.slice.call(args).join('') + '\n' + (new Error()).stack); + firstTime = false; + } + return fn.apply(this, arguments); + }, fn); +} + +var deprecations = {}; + +function deprecateSimple(name, msg) { + if (hooks.deprecationHandler != null) { + hooks.deprecationHandler(name, msg); + } + if (!deprecations[name]) { + warn(msg); + deprecations[name] = true; + } +} + +hooks.suppressDeprecationWarnings = false; +hooks.deprecationHandler = null; + +function isFunction(input) { + return input instanceof Function || Object.prototype.toString.call(input) === '[object Function]'; +} + +function set (config) { + var prop, i; + for (i in config) { + prop = config[i]; + if (isFunction(prop)) { + this[i] = prop; + } else { + this['_' + i] = prop; + } + } + this._config = config; + // Lenient ordinal parsing accepts just a number in addition to + // number + (possibly) stuff coming from _dayOfMonthOrdinalParse. + // TODO: Remove "ordinalParse" fallback in next major release. + this._dayOfMonthOrdinalParseLenient = new RegExp( + (this._dayOfMonthOrdinalParse.source || this._ordinalParse.source) + + '|' + (/\d{1,2}/).source); +} + +function mergeConfigs(parentConfig, childConfig) { + var res = extend({}, parentConfig), prop; + for (prop in childConfig) { + if (hasOwnProp(childConfig, prop)) { + if (isObject(parentConfig[prop]) && isObject(childConfig[prop])) { + res[prop] = {}; + extend(res[prop], parentConfig[prop]); + extend(res[prop], childConfig[prop]); + } else if (childConfig[prop] != null) { + res[prop] = childConfig[prop]; + } else { + delete res[prop]; + } + } + } + for (prop in parentConfig) { + if (hasOwnProp(parentConfig, prop) && + !hasOwnProp(childConfig, prop) && + isObject(parentConfig[prop])) { + // make sure changes to properties don't modify parent config + res[prop] = extend({}, res[prop]); + } + } + return res; +} + +function Locale(config) { + if (config != null) { + this.set(config); + } +} + +var keys; + +if (Object.keys) { + keys = Object.keys; +} else { + keys = function (obj) { + var i, res = []; + for (i in obj) { + if (hasOwnProp(obj, i)) { + res.push(i); + } + } + return res; + }; +} + +var defaultCalendar = { + sameDay : '[Today at] LT', + nextDay : '[Tomorrow at] LT', + nextWeek : 'dddd [at] LT', + lastDay : '[Yesterday at] LT', + lastWeek : '[Last] dddd [at] LT', + sameElse : 'L' +}; + +function calendar (key, mom, now) { + var output = this._calendar[key] || this._calendar['sameElse']; + return isFunction(output) ? output.call(mom, now) : output; +} + +var defaultLongDateFormat = { + LTS : 'h:mm:ss A', + LT : 'h:mm A', + L : 'MM/DD/YYYY', + LL : 'MMMM D, YYYY', + LLL : 'MMMM D, YYYY h:mm A', + LLLL : 'dddd, MMMM D, YYYY h:mm A' +}; + +function longDateFormat (key) { + var format = this._longDateFormat[key], + formatUpper = this._longDateFormat[key.toUpperCase()]; + + if (format || !formatUpper) { + return format; + } + + this._longDateFormat[key] = formatUpper.replace(/MMMM|MM|DD|dddd/g, function (val) { + return val.slice(1); + }); + + return this._longDateFormat[key]; +} + +var defaultInvalidDate = 'Invalid date'; + +function invalidDate () { + return this._invalidDate; +} + +var defaultOrdinal = '%d'; +var defaultDayOfMonthOrdinalParse = /\d{1,2}/; + +function ordinal (number) { + return this._ordinal.replace('%d', number); +} + +var defaultRelativeTime = { + future : 'in %s', + past : '%s ago', + s : 'a few seconds', + ss : '%d seconds', + m : 'a minute', + mm : '%d minutes', + h : 'an hour', + hh : '%d hours', + d : 'a day', + dd : '%d days', + M : 'a month', + MM : '%d months', + y : 'a year', + yy : '%d years' +}; + +function relativeTime (number, withoutSuffix, string, isFuture) { + var output = this._relativeTime[string]; + return (isFunction(output)) ? + output(number, withoutSuffix, string, isFuture) : + output.replace(/%d/i, number); +} + +function pastFuture (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return isFunction(format) ? format(output) : format.replace(/%s/i, output); +} + +var aliases = {}; + +function addUnitAlias (unit, shorthand) { + var lowerCase = unit.toLowerCase(); + aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; +} + +function normalizeUnits(units) { + return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined; +} + +function normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop; + + for (prop in inputObject) { + if (hasOwnProp(inputObject, prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } + } + } + + return normalizedInput; +} + +var priorities = {}; + +function addUnitPriority(unit, priority) { + priorities[unit] = priority; +} + +function getPrioritizedUnits(unitsObj) { + var units = []; + for (var u in unitsObj) { + units.push({unit: u, priority: priorities[u]}); + } + units.sort(function (a, b) { + return a.priority - b.priority; + }); + return units; +} + +function zeroFill(number, targetLength, forceSign) { + var absNumber = '' + Math.abs(number), + zerosToFill = targetLength - absNumber.length, + sign = number >= 0; + return (sign ? (forceSign ? '+' : '') : '-') + + Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + absNumber; +} + +var formattingTokens = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g; + +var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g; + +var formatFunctions = {}; + +var formatTokenFunctions = {}; + +// token: 'M' +// padded: ['MM', 2] +// ordinal: 'Mo' +// callback: function () { this.month() + 1 } +function addFormatToken (token, padded, ordinal, callback) { + var func = callback; + if (typeof callback === 'string') { + func = function () { + return this[callback](); + }; + } + if (token) { + formatTokenFunctions[token] = func; + } + if (padded) { + formatTokenFunctions[padded[0]] = function () { + return zeroFill(func.apply(this, arguments), padded[1], padded[2]); + }; + } + if (ordinal) { + formatTokenFunctions[ordinal] = function () { + return this.localeData().ordinal(func.apply(this, arguments), token); + }; + } +} + +function removeFormattingTokens(input) { + if (input.match(/\[[\s\S]/)) { + return input.replace(/^\[|\]$/g, ''); + } + return input.replace(/\\/g, ''); +} + +function makeFormatFunction(format) { + var array = format.match(formattingTokens), i, length; + + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; + } else { + array[i] = removeFormattingTokens(array[i]); + } + } + + return function (mom) { + var output = '', i; + for (i = 0; i < length; i++) { + output += isFunction(array[i]) ? array[i].call(mom, format) : array[i]; + } + return output; + }; +} + +// format date using native date object +function formatMoment(m, format) { + if (!m.isValid()) { + return m.localeData().invalidDate(); + } + + format = expandFormat(format, m.localeData()); + formatFunctions[format] = formatFunctions[format] || makeFormatFunction(format); + + return formatFunctions[format](m); +} + +function expandFormat(format, locale) { + var i = 5; + + function replaceLongDateFormatTokens(input) { + return locale.longDateFormat(input) || input; + } + + localFormattingTokens.lastIndex = 0; + while (i >= 0 && localFormattingTokens.test(format)) { + format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); + localFormattingTokens.lastIndex = 0; + i -= 1; + } + + return format; +} + +var match1 = /\d/; // 0 - 9 +var match2 = /\d\d/; // 00 - 99 +var match3 = /\d{3}/; // 000 - 999 +var match4 = /\d{4}/; // 0000 - 9999 +var match6 = /[+-]?\d{6}/; // -999999 - 999999 +var match1to2 = /\d\d?/; // 0 - 99 +var match3to4 = /\d\d\d\d?/; // 999 - 9999 +var match5to6 = /\d\d\d\d\d\d?/; // 99999 - 999999 +var match1to3 = /\d{1,3}/; // 0 - 999 +var match1to4 = /\d{1,4}/; // 0 - 9999 +var match1to6 = /[+-]?\d{1,6}/; // -999999 - 999999 + +var matchUnsigned = /\d+/; // 0 - inf +var matchSigned = /[+-]?\d+/; // -inf - inf + +var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z +var matchShortOffset = /Z|[+-]\d\d(?::?\d\d)?/gi; // +00 -00 +00:00 -00:00 +0000 -0000 or Z + +var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123 + +// any word (or two) characters or numbers including two/three word month in arabic. +// includes scottish gaelic two word and hyphenated months +var matchWord = /[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i; + +var regexes = {}; + +function addRegexToken (token, regex, strictRegex) { + regexes[token] = isFunction(regex) ? regex : function (isStrict, localeData) { + return (isStrict && strictRegex) ? strictRegex : regex; + }; +} + +function getParseRegexForToken (token, config) { + if (!hasOwnProp(regexes, token)) { + return new RegExp(unescapeFormat(token)); + } + + return regexes[token](config._strict, config._locale); +} + +// Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript +function unescapeFormat(s) { + return regexEscape(s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { + return p1 || p2 || p3 || p4; + })); +} + +function regexEscape(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); +} + +var tokens = {}; + +function addParseToken (token, callback) { + var i, func = callback; + if (typeof token === 'string') { + token = [token]; + } + if (isNumber(callback)) { + func = function (input, array) { + array[callback] = toInt(input); + }; + } + for (i = 0; i < token.length; i++) { + tokens[token[i]] = func; + } +} + +function addWeekParseToken (token, callback) { + addParseToken(token, function (input, array, config, token) { + config._w = config._w || {}; + callback(input, config._w, config, token); + }); +} + +function addTimeToArrayFromToken(token, input, config) { + if (input != null && hasOwnProp(tokens, token)) { + tokens[token](input, config._a, config, token); + } +} + +var YEAR = 0; +var MONTH = 1; +var DATE = 2; +var HOUR = 3; +var MINUTE = 4; +var SECOND = 5; +var MILLISECOND = 6; +var WEEK = 7; +var WEEKDAY = 8; + +// FORMATTING + +addFormatToken('Y', 0, 0, function () { + var y = this.year(); + return y <= 9999 ? '' + y : '+' + y; +}); + +addFormatToken(0, ['YY', 2], 0, function () { + return this.year() % 100; +}); + +addFormatToken(0, ['YYYY', 4], 0, 'year'); +addFormatToken(0, ['YYYYY', 5], 0, 'year'); +addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); + +// ALIASES + +addUnitAlias('year', 'y'); + +// PRIORITIES + +addUnitPriority('year', 1); + +// PARSING + +addRegexToken('Y', matchSigned); +addRegexToken('YY', match1to2, match2); +addRegexToken('YYYY', match1to4, match4); +addRegexToken('YYYYY', match1to6, match6); +addRegexToken('YYYYYY', match1to6, match6); + +addParseToken(['YYYYY', 'YYYYYY'], YEAR); +addParseToken('YYYY', function (input, array) { + array[YEAR] = input.length === 2 ? hooks.parseTwoDigitYear(input) : toInt(input); +}); +addParseToken('YY', function (input, array) { + array[YEAR] = hooks.parseTwoDigitYear(input); +}); +addParseToken('Y', function (input, array) { + array[YEAR] = parseInt(input, 10); +}); + +// HELPERS + +function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; +} + +function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; +} + +// HOOKS + +hooks.parseTwoDigitYear = function (input) { + return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); +}; + +// MOMENTS + +var getSetYear = makeGetSet('FullYear', true); + +function getIsLeapYear () { + return isLeapYear(this.year()); +} + +function makeGetSet (unit, keepTime) { + return function (value) { + if (value != null) { + set$1(this, unit, value); + hooks.updateOffset(this, keepTime); + return this; + } else { + return get(this, unit); + } + }; +} + +function get (mom, unit) { + return mom.isValid() ? + mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]() : NaN; +} + +function set$1 (mom, unit, value) { + if (mom.isValid() && !isNaN(value)) { + if (unit === 'FullYear' && isLeapYear(mom.year()) && mom.month() === 1 && mom.date() === 29) { + mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value, mom.month(), daysInMonth(value, mom.month())); + } + else { + mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); + } + } +} + +// MOMENTS + +function stringGet (units) { + units = normalizeUnits(units); + if (isFunction(this[units])) { + return this[units](); + } + return this; +} + + +function stringSet (units, value) { + if (typeof units === 'object') { + units = normalizeObjectUnits(units); + var prioritized = getPrioritizedUnits(units); + for (var i = 0; i < prioritized.length; i++) { + this[prioritized[i].unit](units[prioritized[i].unit]); + } + } else { + units = normalizeUnits(units); + if (isFunction(this[units])) { + return this[units](value); + } + } + return this; +} + +function mod(n, x) { + return ((n % x) + x) % x; +} + +var indexOf; + +if (Array.prototype.indexOf) { + indexOf = Array.prototype.indexOf; +} else { + indexOf = function (o) { + // I know + var i; + for (i = 0; i < this.length; ++i) { + if (this[i] === o) { + return i; + } + } + return -1; + }; +} + +function daysInMonth(year, month) { + if (isNaN(year) || isNaN(month)) { + return NaN; + } + var modMonth = mod(month, 12); + year += (month - modMonth) / 12; + return modMonth === 1 ? (isLeapYear(year) ? 29 : 28) : (31 - modMonth % 7 % 2); +} + +// FORMATTING + +addFormatToken('M', ['MM', 2], 'Mo', function () { + return this.month() + 1; +}); + +addFormatToken('MMM', 0, 0, function (format) { + return this.localeData().monthsShort(this, format); +}); + +addFormatToken('MMMM', 0, 0, function (format) { + return this.localeData().months(this, format); +}); + +// ALIASES + +addUnitAlias('month', 'M'); + +// PRIORITY + +addUnitPriority('month', 8); + +// PARSING + +addRegexToken('M', match1to2); +addRegexToken('MM', match1to2, match2); +addRegexToken('MMM', function (isStrict, locale) { + return locale.monthsShortRegex(isStrict); +}); +addRegexToken('MMMM', function (isStrict, locale) { + return locale.monthsRegex(isStrict); +}); + +addParseToken(['M', 'MM'], function (input, array) { + array[MONTH] = toInt(input) - 1; +}); + +addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { + var month = config._locale.monthsParse(input, token, config._strict); + // if we didn't find a month name, mark the date as invalid. + if (month != null) { + array[MONTH] = month; + } else { + getParsingFlags(config).invalidMonth = input; + } +}); + +// LOCALES + +var MONTHS_IN_FORMAT = /D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/; +var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); +function localeMonths (m, format) { + if (!m) { + return isArray(this._months) ? this._months : + this._months['standalone']; + } + return isArray(this._months) ? this._months[m.month()] : + this._months[(this._months.isFormat || MONTHS_IN_FORMAT).test(format) ? 'format' : 'standalone'][m.month()]; +} + +var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); +function localeMonthsShort (m, format) { + if (!m) { + return isArray(this._monthsShort) ? this._monthsShort : + this._monthsShort['standalone']; + } + return isArray(this._monthsShort) ? this._monthsShort[m.month()] : + this._monthsShort[MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'][m.month()]; +} + +function handleStrictParse(monthName, format, strict) { + var i, ii, mom, llc = monthName.toLocaleLowerCase(); + if (!this._monthsParse) { + // this is not used + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; + for (i = 0; i < 12; ++i) { + mom = createUTC([2000, i]); + this._shortMonthsParse[i] = this.monthsShort(mom, '').toLocaleLowerCase(); + this._longMonthsParse[i] = this.months(mom, '').toLocaleLowerCase(); + } + } + + if (strict) { + if (format === 'MMM') { + ii = indexOf.call(this._shortMonthsParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._longMonthsParse, llc); + return ii !== -1 ? ii : null; + } + } else { + if (format === 'MMM') { + ii = indexOf.call(this._shortMonthsParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._longMonthsParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._longMonthsParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._shortMonthsParse, llc); + return ii !== -1 ? ii : null; + } + } +} + +function localeMonthsParse (monthName, format, strict) { + var i, mom, regex; + + if (this._monthsParseExact) { + return handleStrictParse.call(this, monthName, format, strict); + } + + if (!this._monthsParse) { + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; + } + + // TODO: add sorting + // Sorting makes sure if one month (or abbr) is a prefix of another + // see sorting in computeMonthsParse + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + mom = createUTC([2000, i]); + if (strict && !this._longMonthsParse[i]) { + this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i'); + this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i'); + } + if (!strict && !this._monthsParse[i]) { + regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); + this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) { + return i; + } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) { + return i; + } else if (!strict && this._monthsParse[i].test(monthName)) { + return i; + } + } +} + +// MOMENTS + +function setMonth (mom, value) { + var dayOfMonth; + + if (!mom.isValid()) { + // No op + return mom; + } + + if (typeof value === 'string') { + if (/^\d+$/.test(value)) { + value = toInt(value); + } else { + value = mom.localeData().monthsParse(value); + // TODO: Another silent failure? + if (!isNumber(value)) { + return mom; + } + } + } + + dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); + mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); + return mom; +} + +function getSetMonth (value) { + if (value != null) { + setMonth(this, value); + hooks.updateOffset(this, true); + return this; + } else { + return get(this, 'Month'); + } +} + +function getDaysInMonth () { + return daysInMonth(this.year(), this.month()); +} + +var defaultMonthsShortRegex = matchWord; +function monthsShortRegex (isStrict) { + if (this._monthsParseExact) { + if (!hasOwnProp(this, '_monthsRegex')) { + computeMonthsParse.call(this); + } + if (isStrict) { + return this._monthsShortStrictRegex; + } else { + return this._monthsShortRegex; + } + } else { + if (!hasOwnProp(this, '_monthsShortRegex')) { + this._monthsShortRegex = defaultMonthsShortRegex; + } + return this._monthsShortStrictRegex && isStrict ? + this._monthsShortStrictRegex : this._monthsShortRegex; + } +} + +var defaultMonthsRegex = matchWord; +function monthsRegex (isStrict) { + if (this._monthsParseExact) { + if (!hasOwnProp(this, '_monthsRegex')) { + computeMonthsParse.call(this); + } + if (isStrict) { + return this._monthsStrictRegex; + } else { + return this._monthsRegex; + } + } else { + if (!hasOwnProp(this, '_monthsRegex')) { + this._monthsRegex = defaultMonthsRegex; + } + return this._monthsStrictRegex && isStrict ? + this._monthsStrictRegex : this._monthsRegex; + } +} + +function computeMonthsParse () { + function cmpLenRev(a, b) { + return b.length - a.length; + } + + var shortPieces = [], longPieces = [], mixedPieces = [], + i, mom; + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + mom = createUTC([2000, i]); + shortPieces.push(this.monthsShort(mom, '')); + longPieces.push(this.months(mom, '')); + mixedPieces.push(this.months(mom, '')); + mixedPieces.push(this.monthsShort(mom, '')); + } + // Sorting makes sure if one month (or abbr) is a prefix of another it + // will match the longer piece. + shortPieces.sort(cmpLenRev); + longPieces.sort(cmpLenRev); + mixedPieces.sort(cmpLenRev); + for (i = 0; i < 12; i++) { + shortPieces[i] = regexEscape(shortPieces[i]); + longPieces[i] = regexEscape(longPieces[i]); + } + for (i = 0; i < 24; i++) { + mixedPieces[i] = regexEscape(mixedPieces[i]); + } + + this._monthsRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); + this._monthsShortRegex = this._monthsRegex; + this._monthsStrictRegex = new RegExp('^(' + longPieces.join('|') + ')', 'i'); + this._monthsShortStrictRegex = new RegExp('^(' + shortPieces.join('|') + ')', 'i'); +} + +function createDate (y, m, d, h, M, s, ms) { + // can't just apply() to create a date: + // https://stackoverflow.com/q/181348 + var date = new Date(y, m, d, h, M, s, ms); + + // the date constructor remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0 && isFinite(date.getFullYear())) { + date.setFullYear(y); + } + return date; +} + +function createUTCDate (y) { + var date = new Date(Date.UTC.apply(null, arguments)); + + // the Date.UTC function remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0 && isFinite(date.getUTCFullYear())) { + date.setUTCFullYear(y); + } + return date; +} + +// start-of-first-week - start-of-year +function firstWeekOffset(year, dow, doy) { + var // first-week day -- which january is always in the first week (4 for iso, 1 for other) + fwd = 7 + dow - doy, + // first-week day local weekday -- which local weekday is fwd + fwdlw = (7 + createUTCDate(year, 0, fwd).getUTCDay() - dow) % 7; + + return -fwdlw + fwd - 1; +} + +// https://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday +function dayOfYearFromWeeks(year, week, weekday, dow, doy) { + var localWeekday = (7 + weekday - dow) % 7, + weekOffset = firstWeekOffset(year, dow, doy), + dayOfYear = 1 + 7 * (week - 1) + localWeekday + weekOffset, + resYear, resDayOfYear; + + if (dayOfYear <= 0) { + resYear = year - 1; + resDayOfYear = daysInYear(resYear) + dayOfYear; + } else if (dayOfYear > daysInYear(year)) { + resYear = year + 1; + resDayOfYear = dayOfYear - daysInYear(year); + } else { + resYear = year; + resDayOfYear = dayOfYear; + } + + return { + year: resYear, + dayOfYear: resDayOfYear + }; +} + +function weekOfYear(mom, dow, doy) { + var weekOffset = firstWeekOffset(mom.year(), dow, doy), + week = Math.floor((mom.dayOfYear() - weekOffset - 1) / 7) + 1, + resWeek, resYear; + + if (week < 1) { + resYear = mom.year() - 1; + resWeek = week + weeksInYear(resYear, dow, doy); + } else if (week > weeksInYear(mom.year(), dow, doy)) { + resWeek = week - weeksInYear(mom.year(), dow, doy); + resYear = mom.year() + 1; + } else { + resYear = mom.year(); + resWeek = week; + } + + return { + week: resWeek, + year: resYear + }; +} + +function weeksInYear(year, dow, doy) { + var weekOffset = firstWeekOffset(year, dow, doy), + weekOffsetNext = firstWeekOffset(year + 1, dow, doy); + return (daysInYear(year) - weekOffset + weekOffsetNext) / 7; +} + +// FORMATTING + +addFormatToken('w', ['ww', 2], 'wo', 'week'); +addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); + +// ALIASES + +addUnitAlias('week', 'w'); +addUnitAlias('isoWeek', 'W'); + +// PRIORITIES + +addUnitPriority('week', 5); +addUnitPriority('isoWeek', 5); + +// PARSING + +addRegexToken('w', match1to2); +addRegexToken('ww', match1to2, match2); +addRegexToken('W', match1to2); +addRegexToken('WW', match1to2, match2); + +addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) { + week[token.substr(0, 1)] = toInt(input); +}); + +// HELPERS + +// LOCALES + +function localeWeek (mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; +} + +var defaultLocaleWeek = { + dow : 0, // Sunday is the first day of the week. + doy : 6 // The week that contains Jan 1st is the first week of the year. +}; + +function localeFirstDayOfWeek () { + return this._week.dow; +} + +function localeFirstDayOfYear () { + return this._week.doy; +} + +// MOMENTS + +function getSetWeek (input) { + var week = this.localeData().week(this); + return input == null ? week : this.add((input - week) * 7, 'd'); +} + +function getSetISOWeek (input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add((input - week) * 7, 'd'); +} + +// FORMATTING + +addFormatToken('d', 0, 'do', 'day'); + +addFormatToken('dd', 0, 0, function (format) { + return this.localeData().weekdaysMin(this, format); +}); + +addFormatToken('ddd', 0, 0, function (format) { + return this.localeData().weekdaysShort(this, format); +}); + +addFormatToken('dddd', 0, 0, function (format) { + return this.localeData().weekdays(this, format); +}); + +addFormatToken('e', 0, 0, 'weekday'); +addFormatToken('E', 0, 0, 'isoWeekday'); + +// ALIASES + +addUnitAlias('day', 'd'); +addUnitAlias('weekday', 'e'); +addUnitAlias('isoWeekday', 'E'); + +// PRIORITY +addUnitPriority('day', 11); +addUnitPriority('weekday', 11); +addUnitPriority('isoWeekday', 11); + +// PARSING + +addRegexToken('d', match1to2); +addRegexToken('e', match1to2); +addRegexToken('E', match1to2); +addRegexToken('dd', function (isStrict, locale) { + return locale.weekdaysMinRegex(isStrict); +}); +addRegexToken('ddd', function (isStrict, locale) { + return locale.weekdaysShortRegex(isStrict); +}); +addRegexToken('dddd', function (isStrict, locale) { + return locale.weekdaysRegex(isStrict); +}); + +addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config, token) { + var weekday = config._locale.weekdaysParse(input, token, config._strict); + // if we didn't get a weekday name, mark the date as invalid + if (weekday != null) { + week.d = weekday; + } else { + getParsingFlags(config).invalidWeekday = input; + } +}); + +addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { + week[token] = toInt(input); +}); + +// HELPERS + +function parseWeekday(input, locale) { + if (typeof input !== 'string') { + return input; + } + + if (!isNaN(input)) { + return parseInt(input, 10); + } + + input = locale.weekdaysParse(input); + if (typeof input === 'number') { + return input; + } + + return null; +} + +function parseIsoWeekday(input, locale) { + if (typeof input === 'string') { + return locale.weekdaysParse(input) % 7 || 7; + } + return isNaN(input) ? null : input; +} + +// LOCALES + +var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); +function localeWeekdays (m, format) { + if (!m) { + return isArray(this._weekdays) ? this._weekdays : + this._weekdays['standalone']; + } + return isArray(this._weekdays) ? this._weekdays[m.day()] : + this._weekdays[this._weekdays.isFormat.test(format) ? 'format' : 'standalone'][m.day()]; +} + +var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); +function localeWeekdaysShort (m) { + return (m) ? this._weekdaysShort[m.day()] : this._weekdaysShort; +} + +var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); +function localeWeekdaysMin (m) { + return (m) ? this._weekdaysMin[m.day()] : this._weekdaysMin; +} + +function handleStrictParse$1(weekdayName, format, strict) { + var i, ii, mom, llc = weekdayName.toLocaleLowerCase(); + if (!this._weekdaysParse) { + this._weekdaysParse = []; + this._shortWeekdaysParse = []; + this._minWeekdaysParse = []; + + for (i = 0; i < 7; ++i) { + mom = createUTC([2000, 1]).day(i); + this._minWeekdaysParse[i] = this.weekdaysMin(mom, '').toLocaleLowerCase(); + this._shortWeekdaysParse[i] = this.weekdaysShort(mom, '').toLocaleLowerCase(); + this._weekdaysParse[i] = this.weekdays(mom, '').toLocaleLowerCase(); + } + } + + if (strict) { + if (format === 'dddd') { + ii = indexOf.call(this._weekdaysParse, llc); + return ii !== -1 ? ii : null; + } else if (format === 'ddd') { + ii = indexOf.call(this._shortWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._minWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } + } else { + if (format === 'dddd') { + ii = indexOf.call(this._weekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._shortWeekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._minWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } else if (format === 'ddd') { + ii = indexOf.call(this._shortWeekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._weekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._minWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._minWeekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._weekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._shortWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } + } +} + +function localeWeekdaysParse (weekdayName, format, strict) { + var i, mom, regex; + + if (this._weekdaysParseExact) { + return handleStrictParse$1.call(this, weekdayName, format, strict); + } + + if (!this._weekdaysParse) { + this._weekdaysParse = []; + this._minWeekdaysParse = []; + this._shortWeekdaysParse = []; + this._fullWeekdaysParse = []; + } + + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + + mom = createUTC([2000, 1]).day(i); + if (strict && !this._fullWeekdaysParse[i]) { + this._fullWeekdaysParse[i] = new RegExp('^' + this.weekdays(mom, '').replace('.', '\.?') + '$', 'i'); + this._shortWeekdaysParse[i] = new RegExp('^' + this.weekdaysShort(mom, '').replace('.', '\.?') + '$', 'i'); + this._minWeekdaysParse[i] = new RegExp('^' + this.weekdaysMin(mom, '').replace('.', '\.?') + '$', 'i'); + } + if (!this._weekdaysParse[i]) { + regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); + this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (strict && format === 'dddd' && this._fullWeekdaysParse[i].test(weekdayName)) { + return i; + } else if (strict && format === 'ddd' && this._shortWeekdaysParse[i].test(weekdayName)) { + return i; + } else if (strict && format === 'dd' && this._minWeekdaysParse[i].test(weekdayName)) { + return i; + } else if (!strict && this._weekdaysParse[i].test(weekdayName)) { + return i; + } + } +} + +// MOMENTS + +function getSetDayOfWeek (input) { + if (!this.isValid()) { + return input != null ? this : NaN; + } + var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); + if (input != null) { + input = parseWeekday(input, this.localeData()); + return this.add(input - day, 'd'); + } else { + return day; + } +} + +function getSetLocaleDayOfWeek (input) { + if (!this.isValid()) { + return input != null ? this : NaN; + } + var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; + return input == null ? weekday : this.add(input - weekday, 'd'); +} + +function getSetISODayOfWeek (input) { + if (!this.isValid()) { + return input != null ? this : NaN; + } + + // behaves the same as moment#day except + // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) + // as a setter, sunday should belong to the previous week. + + if (input != null) { + var weekday = parseIsoWeekday(input, this.localeData()); + return this.day(this.day() % 7 ? weekday : weekday - 7); + } else { + return this.day() || 7; + } +} + +var defaultWeekdaysRegex = matchWord; +function weekdaysRegex (isStrict) { + if (this._weekdaysParseExact) { + if (!hasOwnProp(this, '_weekdaysRegex')) { + computeWeekdaysParse.call(this); + } + if (isStrict) { + return this._weekdaysStrictRegex; + } else { + return this._weekdaysRegex; + } + } else { + if (!hasOwnProp(this, '_weekdaysRegex')) { + this._weekdaysRegex = defaultWeekdaysRegex; + } + return this._weekdaysStrictRegex && isStrict ? + this._weekdaysStrictRegex : this._weekdaysRegex; + } +} + +var defaultWeekdaysShortRegex = matchWord; +function weekdaysShortRegex (isStrict) { + if (this._weekdaysParseExact) { + if (!hasOwnProp(this, '_weekdaysRegex')) { + computeWeekdaysParse.call(this); + } + if (isStrict) { + return this._weekdaysShortStrictRegex; + } else { + return this._weekdaysShortRegex; + } + } else { + if (!hasOwnProp(this, '_weekdaysShortRegex')) { + this._weekdaysShortRegex = defaultWeekdaysShortRegex; + } + return this._weekdaysShortStrictRegex && isStrict ? + this._weekdaysShortStrictRegex : this._weekdaysShortRegex; + } +} + +var defaultWeekdaysMinRegex = matchWord; +function weekdaysMinRegex (isStrict) { + if (this._weekdaysParseExact) { + if (!hasOwnProp(this, '_weekdaysRegex')) { + computeWeekdaysParse.call(this); + } + if (isStrict) { + return this._weekdaysMinStrictRegex; + } else { + return this._weekdaysMinRegex; + } + } else { + if (!hasOwnProp(this, '_weekdaysMinRegex')) { + this._weekdaysMinRegex = defaultWeekdaysMinRegex; + } + return this._weekdaysMinStrictRegex && isStrict ? + this._weekdaysMinStrictRegex : this._weekdaysMinRegex; + } +} + + +function computeWeekdaysParse () { + function cmpLenRev(a, b) { + return b.length - a.length; + } + + var minPieces = [], shortPieces = [], longPieces = [], mixedPieces = [], + i, mom, minp, shortp, longp; + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + mom = createUTC([2000, 1]).day(i); + minp = this.weekdaysMin(mom, ''); + shortp = this.weekdaysShort(mom, ''); + longp = this.weekdays(mom, ''); + minPieces.push(minp); + shortPieces.push(shortp); + longPieces.push(longp); + mixedPieces.push(minp); + mixedPieces.push(shortp); + mixedPieces.push(longp); + } + // Sorting makes sure if one weekday (or abbr) is a prefix of another it + // will match the longer piece. + minPieces.sort(cmpLenRev); + shortPieces.sort(cmpLenRev); + longPieces.sort(cmpLenRev); + mixedPieces.sort(cmpLenRev); + for (i = 0; i < 7; i++) { + shortPieces[i] = regexEscape(shortPieces[i]); + longPieces[i] = regexEscape(longPieces[i]); + mixedPieces[i] = regexEscape(mixedPieces[i]); + } + + this._weekdaysRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); + this._weekdaysShortRegex = this._weekdaysRegex; + this._weekdaysMinRegex = this._weekdaysRegex; + + this._weekdaysStrictRegex = new RegExp('^(' + longPieces.join('|') + ')', 'i'); + this._weekdaysShortStrictRegex = new RegExp('^(' + shortPieces.join('|') + ')', 'i'); + this._weekdaysMinStrictRegex = new RegExp('^(' + minPieces.join('|') + ')', 'i'); +} + +// FORMATTING + +function hFormat() { + return this.hours() % 12 || 12; +} + +function kFormat() { + return this.hours() || 24; +} + +addFormatToken('H', ['HH', 2], 0, 'hour'); +addFormatToken('h', ['hh', 2], 0, hFormat); +addFormatToken('k', ['kk', 2], 0, kFormat); + +addFormatToken('hmm', 0, 0, function () { + return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2); +}); + +addFormatToken('hmmss', 0, 0, function () { + return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2) + + zeroFill(this.seconds(), 2); +}); + +addFormatToken('Hmm', 0, 0, function () { + return '' + this.hours() + zeroFill(this.minutes(), 2); +}); + +addFormatToken('Hmmss', 0, 0, function () { + return '' + this.hours() + zeroFill(this.minutes(), 2) + + zeroFill(this.seconds(), 2); +}); + +function meridiem (token, lowercase) { + addFormatToken(token, 0, 0, function () { + return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); + }); +} + +meridiem('a', true); +meridiem('A', false); + +// ALIASES + +addUnitAlias('hour', 'h'); + +// PRIORITY +addUnitPriority('hour', 13); + +// PARSING + +function matchMeridiem (isStrict, locale) { + return locale._meridiemParse; +} + +addRegexToken('a', matchMeridiem); +addRegexToken('A', matchMeridiem); +addRegexToken('H', match1to2); +addRegexToken('h', match1to2); +addRegexToken('k', match1to2); +addRegexToken('HH', match1to2, match2); +addRegexToken('hh', match1to2, match2); +addRegexToken('kk', match1to2, match2); + +addRegexToken('hmm', match3to4); +addRegexToken('hmmss', match5to6); +addRegexToken('Hmm', match3to4); +addRegexToken('Hmmss', match5to6); + +addParseToken(['H', 'HH'], HOUR); +addParseToken(['k', 'kk'], function (input, array, config) { + var kInput = toInt(input); + array[HOUR] = kInput === 24 ? 0 : kInput; +}); +addParseToken(['a', 'A'], function (input, array, config) { + config._isPm = config._locale.isPM(input); + config._meridiem = input; +}); +addParseToken(['h', 'hh'], function (input, array, config) { + array[HOUR] = toInt(input); + getParsingFlags(config).bigHour = true; +}); +addParseToken('hmm', function (input, array, config) { + var pos = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos)); + array[MINUTE] = toInt(input.substr(pos)); + getParsingFlags(config).bigHour = true; +}); +addParseToken('hmmss', function (input, array, config) { + var pos1 = input.length - 4; + var pos2 = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos1)); + array[MINUTE] = toInt(input.substr(pos1, 2)); + array[SECOND] = toInt(input.substr(pos2)); + getParsingFlags(config).bigHour = true; +}); +addParseToken('Hmm', function (input, array, config) { + var pos = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos)); + array[MINUTE] = toInt(input.substr(pos)); +}); +addParseToken('Hmmss', function (input, array, config) { + var pos1 = input.length - 4; + var pos2 = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos1)); + array[MINUTE] = toInt(input.substr(pos1, 2)); + array[SECOND] = toInt(input.substr(pos2)); +}); + +// LOCALES + +function localeIsPM (input) { + // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays + // Using charAt should be more compatible. + return ((input + '').toLowerCase().charAt(0) === 'p'); +} + +var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; +function localeMeridiem (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } +} + + +// MOMENTS + +// Setting the hour should keep the time, because the user explicitly +// specified which hour he wants. So trying to maintain the same hour (in +// a new timezone) makes sense. Adding/subtracting hours does not follow +// this rule. +var getSetHour = makeGetSet('Hours', true); + +var baseConfig = { + calendar: defaultCalendar, + longDateFormat: defaultLongDateFormat, + invalidDate: defaultInvalidDate, + ordinal: defaultOrdinal, + dayOfMonthOrdinalParse: defaultDayOfMonthOrdinalParse, + relativeTime: defaultRelativeTime, + + months: defaultLocaleMonths, + monthsShort: defaultLocaleMonthsShort, + + week: defaultLocaleWeek, + + weekdays: defaultLocaleWeekdays, + weekdaysMin: defaultLocaleWeekdaysMin, + weekdaysShort: defaultLocaleWeekdaysShort, + + meridiemParse: defaultLocaleMeridiemParse +}; + +// internal storage for locale config files +var locales = {}; +var localeFamilies = {}; +var globalLocale; + +function normalizeLocale(key) { + return key ? key.toLowerCase().replace('_', '-') : key; +} + +// pick the locale from the array +// try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each +// substring from most specific to least, but move to the next array item if it's a more specific variant than the current root +function chooseLocale(names) { + var i = 0, j, next, locale, split; + + while (i < names.length) { + split = normalizeLocale(names[i]).split('-'); + j = split.length; + next = normalizeLocale(names[i + 1]); + next = next ? next.split('-') : null; + while (j > 0) { + locale = loadLocale(split.slice(0, j).join('-')); + if (locale) { + return locale; + } + if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { + //the next array item is better than a shallower substring of this one + break; + } + j--; + } + i++; + } + return globalLocale; +} + +function loadLocale(name) { + var oldLocale = null; + // TODO: Find a better way to register and load all the locales in Node + if (!locales[name] && (typeof module !== 'undefined') && + module && module.exports) { + try { + oldLocale = globalLocale._abbr; + var aliasedRequire = require; + aliasedRequire('./locale/' + name); + getSetGlobalLocale(oldLocale); + } catch (e) {} + } + return locales[name]; +} + +// This function will load locale and then set the global locale. If +// no arguments are passed in, it will simply return the current global +// locale key. +function getSetGlobalLocale (key, values) { + var data; + if (key) { + if (isUndefined(values)) { + data = getLocale(key); + } + else { + data = defineLocale(key, values); + } + + if (data) { + // moment.duration._locale = moment._locale = data; + globalLocale = data; + } + else { + if ((typeof console !== 'undefined') && console.warn) { + //warn user if arguments are passed but the locale could not be set + console.warn('Locale ' + key + ' not found. Did you forget to load it?'); + } + } + } + + return globalLocale._abbr; +} + +function defineLocale (name, config) { + if (config !== null) { + var locale, parentConfig = baseConfig; + config.abbr = name; + if (locales[name] != null) { + deprecateSimple('defineLocaleOverride', + 'use moment.updateLocale(localeName, config) to change ' + + 'an existing locale. moment.defineLocale(localeName, ' + + 'config) should only be used for creating a new locale ' + + 'See http://momentjs.com/guides/#/warnings/define-locale/ for more info.'); + parentConfig = locales[name]._config; + } else if (config.parentLocale != null) { + if (locales[config.parentLocale] != null) { + parentConfig = locales[config.parentLocale]._config; + } else { + locale = loadLocale(config.parentLocale); + if (locale != null) { + parentConfig = locale._config; + } else { + if (!localeFamilies[config.parentLocale]) { + localeFamilies[config.parentLocale] = []; + } + localeFamilies[config.parentLocale].push({ + name: name, + config: config + }); + return null; + } + } + } + locales[name] = new Locale(mergeConfigs(parentConfig, config)); + + if (localeFamilies[name]) { + localeFamilies[name].forEach(function (x) { + defineLocale(x.name, x.config); + }); + } + + // backwards compat for now: also set the locale + // make sure we set the locale AFTER all child locales have been + // created, so we won't end up with the child locale set. + getSetGlobalLocale(name); + + + return locales[name]; + } else { + // useful for testing + delete locales[name]; + return null; + } +} + +function updateLocale(name, config) { + if (config != null) { + var locale, tmpLocale, parentConfig = baseConfig; + // MERGE + tmpLocale = loadLocale(name); + if (tmpLocale != null) { + parentConfig = tmpLocale._config; + } + config = mergeConfigs(parentConfig, config); + locale = new Locale(config); + locale.parentLocale = locales[name]; + locales[name] = locale; + + // backwards compat for now: also set the locale + getSetGlobalLocale(name); + } else { + // pass null for config to unupdate, useful for tests + if (locales[name] != null) { + if (locales[name].parentLocale != null) { + locales[name] = locales[name].parentLocale; + } else if (locales[name] != null) { + delete locales[name]; + } + } + } + return locales[name]; +} + +// returns locale data +function getLocale (key) { + var locale; + + if (key && key._locale && key._locale._abbr) { + key = key._locale._abbr; + } + + if (!key) { + return globalLocale; + } + + if (!isArray(key)) { + //short-circuit everything else + locale = loadLocale(key); + if (locale) { + return locale; + } + key = [key]; + } + + return chooseLocale(key); +} + +function listLocales() { + return keys(locales); +} + +function checkOverflow (m) { + var overflow; + var a = m._a; + + if (a && getParsingFlags(m).overflow === -2) { + overflow = + a[MONTH] < 0 || a[MONTH] > 11 ? MONTH : + a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ? DATE : + a[HOUR] < 0 || a[HOUR] > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR : + a[MINUTE] < 0 || a[MINUTE] > 59 ? MINUTE : + a[SECOND] < 0 || a[SECOND] > 59 ? SECOND : + a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND : + -1; + + if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { + overflow = DATE; + } + if (getParsingFlags(m)._overflowWeeks && overflow === -1) { + overflow = WEEK; + } + if (getParsingFlags(m)._overflowWeekday && overflow === -1) { + overflow = WEEKDAY; + } + + getParsingFlags(m).overflow = overflow; + } + + return m; +} + +// Pick the first defined of two or three arguments. +function defaults(a, b, c) { + if (a != null) { + return a; + } + if (b != null) { + return b; + } + return c; +} + +function currentDateArray(config) { + // hooks is actually the exported moment object + var nowValue = new Date(hooks.now()); + if (config._useUTC) { + return [nowValue.getUTCFullYear(), nowValue.getUTCMonth(), nowValue.getUTCDate()]; + } + return [nowValue.getFullYear(), nowValue.getMonth(), nowValue.getDate()]; +} + +// convert an array to a date. +// the array should mirror the parameters below +// note: all values past the year are optional and will default to the lowest possible value. +// [year, month, day , hour, minute, second, millisecond] +function configFromArray (config) { + var i, date, input = [], currentDate, expectedWeekday, yearToUse; + + if (config._d) { + return; + } + + currentDate = currentDateArray(config); + + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + dayOfYearFromWeekInfo(config); + } + + //if the day of the year is set, figure out what it is + if (config._dayOfYear != null) { + yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); + + if (config._dayOfYear > daysInYear(yearToUse) || config._dayOfYear === 0) { + getParsingFlags(config)._overflowDayOfYear = true; + } + + date = createUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); + } + + // Default to current date. + // * if no year, month, day of month are given, default to today + // * if day of month is given, default month and year + // * if month is given, default only year + // * if year is given, don't default anything + for (i = 0; i < 3 && config._a[i] == null; ++i) { + config._a[i] = input[i] = currentDate[i]; + } + + // Zero out whatever was not defaulted, including time + for (; i < 7; i++) { + config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; + } + + // Check for 24:00:00.000 + if (config._a[HOUR] === 24 && + config._a[MINUTE] === 0 && + config._a[SECOND] === 0 && + config._a[MILLISECOND] === 0) { + config._nextDay = true; + config._a[HOUR] = 0; + } + + config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input); + expectedWeekday = config._useUTC ? config._d.getUTCDay() : config._d.getDay(); + + // Apply timezone offset from input. The actual utcOffset can be changed + // with parseZone. + if (config._tzm != null) { + config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); + } + + if (config._nextDay) { + config._a[HOUR] = 24; + } + + // check for mismatching day of week + if (config._w && typeof config._w.d !== 'undefined' && config._w.d !== expectedWeekday) { + getParsingFlags(config).weekdayMismatch = true; + } +} + +function dayOfYearFromWeekInfo(config) { + var w, weekYear, week, weekday, dow, doy, temp, weekdayOverflow; + + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + dow = 1; + doy = 4; + + // TODO: We need to take the current isoWeekYear, but that depends on + // how we interpret now (local, utc, fixed offset). So create + // a now version of current config (take local/utc/offset flags, and + // create now). + weekYear = defaults(w.GG, config._a[YEAR], weekOfYear(createLocal(), 1, 4).year); + week = defaults(w.W, 1); + weekday = defaults(w.E, 1); + if (weekday < 1 || weekday > 7) { + weekdayOverflow = true; + } + } else { + dow = config._locale._week.dow; + doy = config._locale._week.doy; + + var curWeek = weekOfYear(createLocal(), dow, doy); + + weekYear = defaults(w.gg, config._a[YEAR], curWeek.year); + + // Default to current week. + week = defaults(w.w, curWeek.week); + + if (w.d != null) { + // weekday -- low day numbers are considered next week + weekday = w.d; + if (weekday < 0 || weekday > 6) { + weekdayOverflow = true; + } + } else if (w.e != null) { + // local weekday -- counting starts from begining of week + weekday = w.e + dow; + if (w.e < 0 || w.e > 6) { + weekdayOverflow = true; + } + } else { + // default to begining of week + weekday = dow; + } + } + if (week < 1 || week > weeksInYear(weekYear, dow, doy)) { + getParsingFlags(config)._overflowWeeks = true; + } else if (weekdayOverflow != null) { + getParsingFlags(config)._overflowWeekday = true; + } else { + temp = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy); + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; + } +} + +// iso 8601 regex +// 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00) +var extendedIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/; +var basicIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/; + +var tzRegex = /Z|[+-]\d\d(?::?\d\d)?/; + +var isoDates = [ + ['YYYYYY-MM-DD', /[+-]\d{6}-\d\d-\d\d/], + ['YYYY-MM-DD', /\d{4}-\d\d-\d\d/], + ['GGGG-[W]WW-E', /\d{4}-W\d\d-\d/], + ['GGGG-[W]WW', /\d{4}-W\d\d/, false], + ['YYYY-DDD', /\d{4}-\d{3}/], + ['YYYY-MM', /\d{4}-\d\d/, false], + ['YYYYYYMMDD', /[+-]\d{10}/], + ['YYYYMMDD', /\d{8}/], + // YYYYMM is NOT allowed by the standard + ['GGGG[W]WWE', /\d{4}W\d{3}/], + ['GGGG[W]WW', /\d{4}W\d{2}/, false], + ['YYYYDDD', /\d{7}/] +]; + +// iso time formats and regexes +var isoTimes = [ + ['HH:mm:ss.SSSS', /\d\d:\d\d:\d\d\.\d+/], + ['HH:mm:ss,SSSS', /\d\d:\d\d:\d\d,\d+/], + ['HH:mm:ss', /\d\d:\d\d:\d\d/], + ['HH:mm', /\d\d:\d\d/], + ['HHmmss.SSSS', /\d\d\d\d\d\d\.\d+/], + ['HHmmss,SSSS', /\d\d\d\d\d\d,\d+/], + ['HHmmss', /\d\d\d\d\d\d/], + ['HHmm', /\d\d\d\d/], + ['HH', /\d\d/] +]; + +var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i; + +// date from iso format +function configFromISO(config) { + var i, l, + string = config._i, + match = extendedIsoRegex.exec(string) || basicIsoRegex.exec(string), + allowTime, dateFormat, timeFormat, tzFormat; + + if (match) { + getParsingFlags(config).iso = true; + + for (i = 0, l = isoDates.length; i < l; i++) { + if (isoDates[i][1].exec(match[1])) { + dateFormat = isoDates[i][0]; + allowTime = isoDates[i][2] !== false; + break; + } + } + if (dateFormat == null) { + config._isValid = false; + return; + } + if (match[3]) { + for (i = 0, l = isoTimes.length; i < l; i++) { + if (isoTimes[i][1].exec(match[3])) { + // match[2] should be 'T' or space + timeFormat = (match[2] || ' ') + isoTimes[i][0]; + break; + } + } + if (timeFormat == null) { + config._isValid = false; + return; + } + } + if (!allowTime && timeFormat != null) { + config._isValid = false; + return; + } + if (match[4]) { + if (tzRegex.exec(match[4])) { + tzFormat = 'Z'; + } else { + config._isValid = false; + return; + } + } + config._f = dateFormat + (timeFormat || '') + (tzFormat || ''); + configFromStringAndFormat(config); + } else { + config._isValid = false; + } +} + +// RFC 2822 regex: For details see https://tools.ietf.org/html/rfc2822#section-3.3 +var rfc2822 = /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/; + +function extractFromRFC2822Strings(yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr) { + var result = [ + untruncateYear(yearStr), + defaultLocaleMonthsShort.indexOf(monthStr), + parseInt(dayStr, 10), + parseInt(hourStr, 10), + parseInt(minuteStr, 10) + ]; + + if (secondStr) { + result.push(parseInt(secondStr, 10)); + } + + return result; +} + +function untruncateYear(yearStr) { + var year = parseInt(yearStr, 10); + if (year <= 49) { + return 2000 + year; + } else if (year <= 999) { + return 1900 + year; + } + return year; +} + +function preprocessRFC2822(s) { + // Remove comments and folding whitespace and replace multiple-spaces with a single space + return s.replace(/\([^)]*\)|[\n\t]/g, ' ').replace(/(\s\s+)/g, ' ').trim(); +} + +function checkWeekday(weekdayStr, parsedInput, config) { + if (weekdayStr) { + // TODO: Replace the vanilla JS Date object with an indepentent day-of-week check. + var weekdayProvided = defaultLocaleWeekdaysShort.indexOf(weekdayStr), + weekdayActual = new Date(parsedInput[0], parsedInput[1], parsedInput[2]).getDay(); + if (weekdayProvided !== weekdayActual) { + getParsingFlags(config).weekdayMismatch = true; + config._isValid = false; + return false; + } + } + return true; +} + +var obsOffsets = { + UT: 0, + GMT: 0, + EDT: -4 * 60, + EST: -5 * 60, + CDT: -5 * 60, + CST: -6 * 60, + MDT: -6 * 60, + MST: -7 * 60, + PDT: -7 * 60, + PST: -8 * 60 +}; + +function calculateOffset(obsOffset, militaryOffset, numOffset) { + if (obsOffset) { + return obsOffsets[obsOffset]; + } else if (militaryOffset) { + // the only allowed military tz is Z + return 0; + } else { + var hm = parseInt(numOffset, 10); + var m = hm % 100, h = (hm - m) / 100; + return h * 60 + m; + } +} + +// date and time from ref 2822 format +function configFromRFC2822(config) { + var match = rfc2822.exec(preprocessRFC2822(config._i)); + if (match) { + var parsedArray = extractFromRFC2822Strings(match[4], match[3], match[2], match[5], match[6], match[7]); + if (!checkWeekday(match[1], parsedArray, config)) { + return; + } + + config._a = parsedArray; + config._tzm = calculateOffset(match[8], match[9], match[10]); + + config._d = createUTCDate.apply(null, config._a); + config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); + + getParsingFlags(config).rfc2822 = true; + } else { + config._isValid = false; + } +} + +// date from iso format or fallback +function configFromString(config) { + var matched = aspNetJsonRegex.exec(config._i); + + if (matched !== null) { + config._d = new Date(+matched[1]); + return; + } + + configFromISO(config); + if (config._isValid === false) { + delete config._isValid; + } else { + return; + } + + configFromRFC2822(config); + if (config._isValid === false) { + delete config._isValid; + } else { + return; + } + + // Final attempt, use Input Fallback + hooks.createFromInputFallback(config); +} + +hooks.createFromInputFallback = deprecate( + 'value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), ' + + 'which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are ' + + 'discouraged and will be removed in an upcoming major release. Please refer to ' + + 'http://momentjs.com/guides/#/warnings/js-date/ for more info.', + function (config) { + config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); + } +); + +// constant that refers to the ISO standard +hooks.ISO_8601 = function () {}; + +// constant that refers to the RFC 2822 form +hooks.RFC_2822 = function () {}; + +// date from string and format string +function configFromStringAndFormat(config) { + // TODO: Move this to another part of the creation flow to prevent circular deps + if (config._f === hooks.ISO_8601) { + configFromISO(config); + return; + } + if (config._f === hooks.RFC_2822) { + configFromRFC2822(config); + return; + } + config._a = []; + getParsingFlags(config).empty = true; + + // This array is used to make a Date, either with `new Date` or `Date.UTC` + var string = '' + config._i, + i, parsedInput, tokens, token, skipped, + stringLength = string.length, + totalParsedInputLength = 0; + + tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; + + for (i = 0; i < tokens.length; i++) { + token = tokens[i]; + parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; + // console.log('token', token, 'parsedInput', parsedInput, + // 'regex', getParseRegexForToken(token, config)); + if (parsedInput) { + skipped = string.substr(0, string.indexOf(parsedInput)); + if (skipped.length > 0) { + getParsingFlags(config).unusedInput.push(skipped); + } + string = string.slice(string.indexOf(parsedInput) + parsedInput.length); + totalParsedInputLength += parsedInput.length; + } + // don't parse if it's not a known token + if (formatTokenFunctions[token]) { + if (parsedInput) { + getParsingFlags(config).empty = false; + } + else { + getParsingFlags(config).unusedTokens.push(token); + } + addTimeToArrayFromToken(token, parsedInput, config); + } + else if (config._strict && !parsedInput) { + getParsingFlags(config).unusedTokens.push(token); + } + } + + // add remaining unparsed input length to the string + getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength; + if (string.length > 0) { + getParsingFlags(config).unusedInput.push(string); + } + + // clear _12h flag if hour is <= 12 + if (config._a[HOUR] <= 12 && + getParsingFlags(config).bigHour === true && + config._a[HOUR] > 0) { + getParsingFlags(config).bigHour = undefined; + } + + getParsingFlags(config).parsedDateParts = config._a.slice(0); + getParsingFlags(config).meridiem = config._meridiem; + // handle meridiem + config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem); + + configFromArray(config); + checkOverflow(config); +} + + +function meridiemFixWrap (locale, hour, meridiem) { + var isPm; + + if (meridiem == null) { + // nothing to do + return hour; + } + if (locale.meridiemHour != null) { + return locale.meridiemHour(hour, meridiem); + } else if (locale.isPM != null) { + // Fallback + isPm = locale.isPM(meridiem); + if (isPm && hour < 12) { + hour += 12; + } + if (!isPm && hour === 12) { + hour = 0; + } + return hour; + } else { + // this is not supposed to happen + return hour; + } +} + +// date from string and array of format strings +function configFromStringAndArray(config) { + var tempConfig, + bestMoment, + + scoreToBeat, + i, + currentScore; + + if (config._f.length === 0) { + getParsingFlags(config).invalidFormat = true; + config._d = new Date(NaN); + return; + } + + for (i = 0; i < config._f.length; i++) { + currentScore = 0; + tempConfig = copyConfig({}, config); + if (config._useUTC != null) { + tempConfig._useUTC = config._useUTC; + } + tempConfig._f = config._f[i]; + configFromStringAndFormat(tempConfig); + + if (!isValid(tempConfig)) { + continue; + } + + // if there is any input that was not parsed add a penalty for that format + currentScore += getParsingFlags(tempConfig).charsLeftOver; + + //or tokens + currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; + + getParsingFlags(tempConfig).score = currentScore; + + if (scoreToBeat == null || currentScore < scoreToBeat) { + scoreToBeat = currentScore; + bestMoment = tempConfig; + } + } + + extend(config, bestMoment || tempConfig); +} + +function configFromObject(config) { + if (config._d) { + return; + } + + var i = normalizeObjectUnits(config._i); + config._a = map([i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond], function (obj) { + return obj && parseInt(obj, 10); + }); + + configFromArray(config); +} + +function createFromConfig (config) { + var res = new Moment(checkOverflow(prepareConfig(config))); + if (res._nextDay) { + // Adding is smart enough around DST + res.add(1, 'd'); + res._nextDay = undefined; + } + + return res; +} + +function prepareConfig (config) { + var input = config._i, + format = config._f; + + config._locale = config._locale || getLocale(config._l); + + if (input === null || (format === undefined && input === '')) { + return createInvalid({nullInput: true}); + } + + if (typeof input === 'string') { + config._i = input = config._locale.preparse(input); + } + + if (isMoment(input)) { + return new Moment(checkOverflow(input)); + } else if (isDate(input)) { + config._d = input; + } else if (isArray(format)) { + configFromStringAndArray(config); + } else if (format) { + configFromStringAndFormat(config); + } else { + configFromInput(config); + } + + if (!isValid(config)) { + config._d = null; + } + + return config; +} + +function configFromInput(config) { + var input = config._i; + if (isUndefined(input)) { + config._d = new Date(hooks.now()); + } else if (isDate(input)) { + config._d = new Date(input.valueOf()); + } else if (typeof input === 'string') { + configFromString(config); + } else if (isArray(input)) { + config._a = map(input.slice(0), function (obj) { + return parseInt(obj, 10); + }); + configFromArray(config); + } else if (isObject(input)) { + configFromObject(config); + } else if (isNumber(input)) { + // from milliseconds + config._d = new Date(input); + } else { + hooks.createFromInputFallback(config); + } +} + +function createLocalOrUTC (input, format, locale, strict, isUTC) { + var c = {}; + + if (locale === true || locale === false) { + strict = locale; + locale = undefined; + } + + if ((isObject(input) && isObjectEmpty(input)) || + (isArray(input) && input.length === 0)) { + input = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c._isAMomentObject = true; + c._useUTC = c._isUTC = isUTC; + c._l = locale; + c._i = input; + c._f = format; + c._strict = strict; + + return createFromConfig(c); +} + +function createLocal (input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, false); +} + +var prototypeMin = deprecate( + 'moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/', + function () { + var other = createLocal.apply(null, arguments); + if (this.isValid() && other.isValid()) { + return other < this ? this : other; + } else { + return createInvalid(); + } + } +); + +var prototypeMax = deprecate( + 'moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/', + function () { + var other = createLocal.apply(null, arguments); + if (this.isValid() && other.isValid()) { + return other > this ? this : other; + } else { + return createInvalid(); + } + } +); + +// Pick a moment m from moments so that m[fn](other) is true for all +// other. This relies on the function fn to be transitive. +// +// moments should either be an array of moment objects or an array, whose +// first element is an array of moment objects. +function pickBy(fn, moments) { + var res, i; + if (moments.length === 1 && isArray(moments[0])) { + moments = moments[0]; + } + if (!moments.length) { + return createLocal(); + } + res = moments[0]; + for (i = 1; i < moments.length; ++i) { + if (!moments[i].isValid() || moments[i][fn](res)) { + res = moments[i]; + } + } + return res; +} + +// TODO: Use [].sort instead? +function min () { + var args = [].slice.call(arguments, 0); + + return pickBy('isBefore', args); +} + +function max () { + var args = [].slice.call(arguments, 0); + + return pickBy('isAfter', args); +} + +var now = function () { + return Date.now ? Date.now() : +(new Date()); +}; + +var ordering = ['year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond']; + +function isDurationValid(m) { + for (var key in m) { + if (!(indexOf.call(ordering, key) !== -1 && (m[key] == null || !isNaN(m[key])))) { + return false; + } + } + + var unitHasDecimal = false; + for (var i = 0; i < ordering.length; ++i) { + if (m[ordering[i]]) { + if (unitHasDecimal) { + return false; // only allow non-integers for smallest unit + } + if (parseFloat(m[ordering[i]]) !== toInt(m[ordering[i]])) { + unitHasDecimal = true; + } + } + } + + return true; +} + +function isValid$1() { + return this._isValid; +} + +function createInvalid$1() { + return createDuration(NaN); +} + +function Duration (duration) { + var normalizedInput = normalizeObjectUnits(duration), + years = normalizedInput.year || 0, + quarters = normalizedInput.quarter || 0, + months = normalizedInput.month || 0, + weeks = normalizedInput.week || 0, + days = normalizedInput.day || 0, + hours = normalizedInput.hour || 0, + minutes = normalizedInput.minute || 0, + seconds = normalizedInput.second || 0, + milliseconds = normalizedInput.millisecond || 0; + + this._isValid = isDurationValid(normalizedInput); + + // representation for dateAddRemove + this._milliseconds = +milliseconds + + seconds * 1e3 + // 1000 + minutes * 6e4 + // 1000 * 60 + hours * 1000 * 60 * 60; //using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978 + // Because of dateAddRemove treats 24 hours as different from a + // day when working around DST, we need to store them separately + this._days = +days + + weeks * 7; + // It is impossible to translate months into days without knowing + // which months you are are talking about, so we have to store + // it separately. + this._months = +months + + quarters * 3 + + years * 12; + + this._data = {}; + + this._locale = getLocale(); + + this._bubble(); +} + +function isDuration (obj) { + return obj instanceof Duration; +} + +function absRound (number) { + if (number < 0) { + return Math.round(-1 * number) * -1; + } else { + return Math.round(number); + } +} + +// FORMATTING + +function offset (token, separator) { + addFormatToken(token, 0, 0, function () { + var offset = this.utcOffset(); + var sign = '+'; + if (offset < 0) { + offset = -offset; + sign = '-'; + } + return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2); + }); +} + +offset('Z', ':'); +offset('ZZ', ''); + +// PARSING + +addRegexToken('Z', matchShortOffset); +addRegexToken('ZZ', matchShortOffset); +addParseToken(['Z', 'ZZ'], function (input, array, config) { + config._useUTC = true; + config._tzm = offsetFromString(matchShortOffset, input); +}); + +// HELPERS + +// timezone chunker +// '+10:00' > ['10', '00'] +// '-1530' > ['-15', '30'] +var chunkOffset = /([\+\-]|\d\d)/gi; + +function offsetFromString(matcher, string) { + var matches = (string || '').match(matcher); + + if (matches === null) { + return null; + } + + var chunk = matches[matches.length - 1] || []; + var parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; + var minutes = +(parts[1] * 60) + toInt(parts[2]); + + return minutes === 0 ? + 0 : + parts[0] === '+' ? minutes : -minutes; +} + +// Return a moment from input, that is local/utc/zone equivalent to model. +function cloneWithOffset(input, model) { + var res, diff; + if (model._isUTC) { + res = model.clone(); + diff = (isMoment(input) || isDate(input) ? input.valueOf() : createLocal(input).valueOf()) - res.valueOf(); + // Use low-level api, because this fn is low-level api. + res._d.setTime(res._d.valueOf() + diff); + hooks.updateOffset(res, false); + return res; + } else { + return createLocal(input).local(); + } +} + +function getDateOffset (m) { + // On Firefox.24 Date#getTimezoneOffset returns a floating point. + // https://github.com/moment/moment/pull/1871 + return -Math.round(m._d.getTimezoneOffset() / 15) * 15; +} + +// HOOKS + +// This function will be called whenever a moment is mutated. +// It is intended to keep the offset in sync with the timezone. +hooks.updateOffset = function () {}; + +// MOMENTS + +// keepLocalTime = true means only change the timezone, without +// affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> +// 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset +// +0200, so we adjust the time as needed, to be valid. +// +// Keeping the time actually adds/subtracts (one hour) +// from the actual represented time. That is why we call updateOffset +// a second time. In case it wants us to change the offset again +// _changeInProgress == true case, then we have to adjust, because +// there is no such time in the given timezone. +function getSetOffset (input, keepLocalTime, keepMinutes) { + var offset = this._offset || 0, + localAdjust; + if (!this.isValid()) { + return input != null ? this : NaN; + } + if (input != null) { + if (typeof input === 'string') { + input = offsetFromString(matchShortOffset, input); + if (input === null) { + return this; + } + } else if (Math.abs(input) < 16 && !keepMinutes) { + input = input * 60; + } + if (!this._isUTC && keepLocalTime) { + localAdjust = getDateOffset(this); + } + this._offset = input; + this._isUTC = true; + if (localAdjust != null) { + this.add(localAdjust, 'm'); + } + if (offset !== input) { + if (!keepLocalTime || this._changeInProgress) { + addSubtract(this, createDuration(input - offset, 'm'), 1, false); + } else if (!this._changeInProgress) { + this._changeInProgress = true; + hooks.updateOffset(this, true); + this._changeInProgress = null; + } + } + return this; + } else { + return this._isUTC ? offset : getDateOffset(this); + } +} + +function getSetZone (input, keepLocalTime) { + if (input != null) { + if (typeof input !== 'string') { + input = -input; + } + + this.utcOffset(input, keepLocalTime); + + return this; + } else { + return -this.utcOffset(); + } +} + +function setOffsetToUTC (keepLocalTime) { + return this.utcOffset(0, keepLocalTime); +} + +function setOffsetToLocal (keepLocalTime) { + if (this._isUTC) { + this.utcOffset(0, keepLocalTime); + this._isUTC = false; + + if (keepLocalTime) { + this.subtract(getDateOffset(this), 'm'); + } + } + return this; +} + +function setOffsetToParsedOffset () { + if (this._tzm != null) { + this.utcOffset(this._tzm, false, true); + } else if (typeof this._i === 'string') { + var tZone = offsetFromString(matchOffset, this._i); + if (tZone != null) { + this.utcOffset(tZone); + } + else { + this.utcOffset(0, true); + } + } + return this; +} + +function hasAlignedHourOffset (input) { + if (!this.isValid()) { + return false; + } + input = input ? createLocal(input).utcOffset() : 0; + + return (this.utcOffset() - input) % 60 === 0; +} + +function isDaylightSavingTime () { + return ( + this.utcOffset() > this.clone().month(0).utcOffset() || + this.utcOffset() > this.clone().month(5).utcOffset() + ); +} + +function isDaylightSavingTimeShifted () { + if (!isUndefined(this._isDSTShifted)) { + return this._isDSTShifted; + } + + var c = {}; + + copyConfig(c, this); + c = prepareConfig(c); + + if (c._a) { + var other = c._isUTC ? createUTC(c._a) : createLocal(c._a); + this._isDSTShifted = this.isValid() && + compareArrays(c._a, other.toArray()) > 0; + } else { + this._isDSTShifted = false; + } + + return this._isDSTShifted; +} + +function isLocal () { + return this.isValid() ? !this._isUTC : false; +} + +function isUtcOffset () { + return this.isValid() ? this._isUTC : false; +} + +function isUtc () { + return this.isValid() ? this._isUTC && this._offset === 0 : false; +} + +// ASP.NET json date format regex +var aspNetRegex = /^(\-|\+)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/; + +// from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html +// somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere +// and further modified to allow for strings containing both week and day +var isoRegex = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/; + +function createDuration (input, key) { + var duration = input, + // matching against regexp is expensive, do it on demand + match = null, + sign, + ret, + diffRes; + + if (isDuration(input)) { + duration = { + ms : input._milliseconds, + d : input._days, + M : input._months + }; + } else if (isNumber(input)) { + duration = {}; + if (key) { + duration[key] = input; + } else { + duration.milliseconds = input; + } + } else if (!!(match = aspNetRegex.exec(input))) { + sign = (match[1] === '-') ? -1 : 1; + duration = { + y : 0, + d : toInt(match[DATE]) * sign, + h : toInt(match[HOUR]) * sign, + m : toInt(match[MINUTE]) * sign, + s : toInt(match[SECOND]) * sign, + ms : toInt(absRound(match[MILLISECOND] * 1000)) * sign // the millisecond decimal point is included in the match + }; + } else if (!!(match = isoRegex.exec(input))) { + sign = (match[1] === '-') ? -1 : (match[1] === '+') ? 1 : 1; + duration = { + y : parseIso(match[2], sign), + M : parseIso(match[3], sign), + w : parseIso(match[4], sign), + d : parseIso(match[5], sign), + h : parseIso(match[6], sign), + m : parseIso(match[7], sign), + s : parseIso(match[8], sign) + }; + } else if (duration == null) {// checks for null or undefined + duration = {}; + } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) { + diffRes = momentsDifference(createLocal(duration.from), createLocal(duration.to)); + + duration = {}; + duration.ms = diffRes.milliseconds; + duration.M = diffRes.months; + } + + ret = new Duration(duration); + + if (isDuration(input) && hasOwnProp(input, '_locale')) { + ret._locale = input._locale; + } + + return ret; +} + +createDuration.fn = Duration.prototype; +createDuration.invalid = createInvalid$1; + +function parseIso (inp, sign) { + // We'd normally use ~~inp for this, but unfortunately it also + // converts floats to ints. + // inp may be undefined, so careful calling replace on it. + var res = inp && parseFloat(inp.replace(',', '.')); + // apply sign while we're at it + return (isNaN(res) ? 0 : res) * sign; +} + +function positiveMomentsDifference(base, other) { + var res = {milliseconds: 0, months: 0}; + + res.months = other.month() - base.month() + + (other.year() - base.year()) * 12; + if (base.clone().add(res.months, 'M').isAfter(other)) { + --res.months; + } + + res.milliseconds = +other - +(base.clone().add(res.months, 'M')); + + return res; +} + +function momentsDifference(base, other) { + var res; + if (!(base.isValid() && other.isValid())) { + return {milliseconds: 0, months: 0}; + } + + other = cloneWithOffset(other, base); + if (base.isBefore(other)) { + res = positiveMomentsDifference(base, other); + } else { + res = positiveMomentsDifference(other, base); + res.milliseconds = -res.milliseconds; + res.months = -res.months; + } + + return res; +} + +// TODO: remove 'name' arg after deprecation is removed +function createAdder(direction, name) { + return function (val, period) { + var dur, tmp; + //invert the arguments, but complain about it + if (period !== null && !isNaN(+period)) { + deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period). ' + + 'See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info.'); + tmp = val; val = period; period = tmp; + } + + val = typeof val === 'string' ? +val : val; + dur = createDuration(val, period); + addSubtract(this, dur, direction); + return this; + }; +} + +function addSubtract (mom, duration, isAdding, updateOffset) { + var milliseconds = duration._milliseconds, + days = absRound(duration._days), + months = absRound(duration._months); + + if (!mom.isValid()) { + // No op + return; + } + + updateOffset = updateOffset == null ? true : updateOffset; + + if (months) { + setMonth(mom, get(mom, 'Month') + months * isAdding); + } + if (days) { + set$1(mom, 'Date', get(mom, 'Date') + days * isAdding); + } + if (milliseconds) { + mom._d.setTime(mom._d.valueOf() + milliseconds * isAdding); + } + if (updateOffset) { + hooks.updateOffset(mom, days || months); + } +} + +var add = createAdder(1, 'add'); +var subtract = createAdder(-1, 'subtract'); + +function getCalendarFormat(myMoment, now) { + var diff = myMoment.diff(now, 'days', true); + return diff < -6 ? 'sameElse' : + diff < -1 ? 'lastWeek' : + diff < 0 ? 'lastDay' : + diff < 1 ? 'sameDay' : + diff < 2 ? 'nextDay' : + diff < 7 ? 'nextWeek' : 'sameElse'; +} + +function calendar$1 (time, formats) { + // We want to compare the start of today, vs this. + // Getting start-of-today depends on whether we're local/utc/offset or not. + var now = time || createLocal(), + sod = cloneWithOffset(now, this).startOf('day'), + format = hooks.calendarFormat(this, sod) || 'sameElse'; + + var output = formats && (isFunction(formats[format]) ? formats[format].call(this, now) : formats[format]); + + return this.format(output || this.localeData().calendar(format, this, createLocal(now))); +} + +function clone () { + return new Moment(this); +} + +function isAfter (input, units) { + var localInput = isMoment(input) ? input : createLocal(input); + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(!isUndefined(units) ? units : 'millisecond'); + if (units === 'millisecond') { + return this.valueOf() > localInput.valueOf(); + } else { + return localInput.valueOf() < this.clone().startOf(units).valueOf(); + } +} + +function isBefore (input, units) { + var localInput = isMoment(input) ? input : createLocal(input); + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(!isUndefined(units) ? units : 'millisecond'); + if (units === 'millisecond') { + return this.valueOf() < localInput.valueOf(); + } else { + return this.clone().endOf(units).valueOf() < localInput.valueOf(); + } +} + +function isBetween (from, to, units, inclusivity) { + inclusivity = inclusivity || '()'; + return (inclusivity[0] === '(' ? this.isAfter(from, units) : !this.isBefore(from, units)) && + (inclusivity[1] === ')' ? this.isBefore(to, units) : !this.isAfter(to, units)); +} + +function isSame (input, units) { + var localInput = isMoment(input) ? input : createLocal(input), + inputMs; + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(units || 'millisecond'); + if (units === 'millisecond') { + return this.valueOf() === localInput.valueOf(); + } else { + inputMs = localInput.valueOf(); + return this.clone().startOf(units).valueOf() <= inputMs && inputMs <= this.clone().endOf(units).valueOf(); + } +} + +function isSameOrAfter (input, units) { + return this.isSame(input, units) || this.isAfter(input,units); +} + +function isSameOrBefore (input, units) { + return this.isSame(input, units) || this.isBefore(input,units); +} + +function diff (input, units, asFloat) { + var that, + zoneDelta, + output; + + if (!this.isValid()) { + return NaN; + } + + that = cloneWithOffset(input, this); + + if (!that.isValid()) { + return NaN; + } + + zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4; + + units = normalizeUnits(units); + + switch (units) { + case 'year': output = monthDiff(this, that) / 12; break; + case 'month': output = monthDiff(this, that); break; + case 'quarter': output = monthDiff(this, that) / 3; break; + case 'second': output = (this - that) / 1e3; break; // 1000 + case 'minute': output = (this - that) / 6e4; break; // 1000 * 60 + case 'hour': output = (this - that) / 36e5; break; // 1000 * 60 * 60 + case 'day': output = (this - that - zoneDelta) / 864e5; break; // 1000 * 60 * 60 * 24, negate dst + case 'week': output = (this - that - zoneDelta) / 6048e5; break; // 1000 * 60 * 60 * 24 * 7, negate dst + default: output = this - that; + } + + return asFloat ? output : absFloor(output); +} + +function monthDiff (a, b) { + // difference in months + var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), + // b is in (anchor - 1 month, anchor + 1 month) + anchor = a.clone().add(wholeMonthDiff, 'months'), + anchor2, adjust; + + if (b - anchor < 0) { + anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor - anchor2); + } else { + anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor2 - anchor); + } + + //check for negative zero, return zero if negative zero + return -(wholeMonthDiff + adjust) || 0; +} + +hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; +hooks.defaultFormatUtc = 'YYYY-MM-DDTHH:mm:ss[Z]'; + +function toString () { + return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); +} + +function toISOString(keepOffset) { + if (!this.isValid()) { + return null; + } + var utc = keepOffset !== true; + var m = utc ? this.clone().utc() : this; + if (m.year() < 0 || m.year() > 9999) { + return formatMoment(m, utc ? 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]' : 'YYYYYY-MM-DD[T]HH:mm:ss.SSSZ'); + } + if (isFunction(Date.prototype.toISOString)) { + // native implementation is ~50x faster, use it when we can + if (utc) { + return this.toDate().toISOString(); + } else { + return new Date(this.valueOf() + this.utcOffset() * 60 * 1000).toISOString().replace('Z', formatMoment(m, 'Z')); + } + } + return formatMoment(m, utc ? 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]' : 'YYYY-MM-DD[T]HH:mm:ss.SSSZ'); +} + +/** + * Return a human readable representation of a moment that can + * also be evaluated to get a new moment which is the same + * + * @link https://nodejs.org/dist/latest/docs/api/util.html#util_custom_inspect_function_on_objects + */ +function inspect () { + if (!this.isValid()) { + return 'moment.invalid(/* ' + this._i + ' */)'; + } + var func = 'moment'; + var zone = ''; + if (!this.isLocal()) { + func = this.utcOffset() === 0 ? 'moment.utc' : 'moment.parseZone'; + zone = 'Z'; + } + var prefix = '[' + func + '("]'; + var year = (0 <= this.year() && this.year() <= 9999) ? 'YYYY' : 'YYYYYY'; + var datetime = '-MM-DD[T]HH:mm:ss.SSS'; + var suffix = zone + '[")]'; + + return this.format(prefix + year + datetime + suffix); +} + +function format (inputString) { + if (!inputString) { + inputString = this.isUtc() ? hooks.defaultFormatUtc : hooks.defaultFormat; + } + var output = formatMoment(this, inputString); + return this.localeData().postformat(output); +} + +function from (time, withoutSuffix) { + if (this.isValid() && + ((isMoment(time) && time.isValid()) || + createLocal(time).isValid())) { + return createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); + } else { + return this.localeData().invalidDate(); + } +} + +function fromNow (withoutSuffix) { + return this.from(createLocal(), withoutSuffix); +} + +function to (time, withoutSuffix) { + if (this.isValid() && + ((isMoment(time) && time.isValid()) || + createLocal(time).isValid())) { + return createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix); + } else { + return this.localeData().invalidDate(); + } +} + +function toNow (withoutSuffix) { + return this.to(createLocal(), withoutSuffix); +} + +// If passed a locale key, it will set the locale for this +// instance. Otherwise, it will return the locale configuration +// variables for this instance. +function locale (key) { + var newLocaleData; + + if (key === undefined) { + return this._locale._abbr; + } else { + newLocaleData = getLocale(key); + if (newLocaleData != null) { + this._locale = newLocaleData; + } + return this; + } +} + +var lang = deprecate( + 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', + function (key) { + if (key === undefined) { + return this.localeData(); + } else { + return this.locale(key); + } + } +); + +function localeData () { + return this._locale; +} + +function startOf (units) { + units = normalizeUnits(units); + // the following switch intentionally omits break keywords + // to utilize falling through the cases. + switch (units) { + case 'year': + this.month(0); + /* falls through */ + case 'quarter': + case 'month': + this.date(1); + /* falls through */ + case 'week': + case 'isoWeek': + case 'day': + case 'date': + this.hours(0); + /* falls through */ + case 'hour': + this.minutes(0); + /* falls through */ + case 'minute': + this.seconds(0); + /* falls through */ + case 'second': + this.milliseconds(0); + } + + // weeks are a special case + if (units === 'week') { + this.weekday(0); + } + if (units === 'isoWeek') { + this.isoWeekday(1); + } + + // quarters are also special + if (units === 'quarter') { + this.month(Math.floor(this.month() / 3) * 3); + } + + return this; +} + +function endOf (units) { + units = normalizeUnits(units); + if (units === undefined || units === 'millisecond') { + return this; + } + + // 'date' is an alias for 'day', so it should be considered as such. + if (units === 'date') { + units = 'day'; + } + + return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); +} + +function valueOf () { + return this._d.valueOf() - ((this._offset || 0) * 60000); +} + +function unix () { + return Math.floor(this.valueOf() / 1000); +} + +function toDate () { + return new Date(this.valueOf()); +} + +function toArray () { + var m = this; + return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; +} + +function toObject () { + var m = this; + return { + years: m.year(), + months: m.month(), + date: m.date(), + hours: m.hours(), + minutes: m.minutes(), + seconds: m.seconds(), + milliseconds: m.milliseconds() + }; +} + +function toJSON () { + // new Date(NaN).toJSON() === null + return this.isValid() ? this.toISOString() : null; +} + +function isValid$2 () { + return isValid(this); +} + +function parsingFlags () { + return extend({}, getParsingFlags(this)); +} + +function invalidAt () { + return getParsingFlags(this).overflow; +} + +function creationData() { + return { + input: this._i, + format: this._f, + locale: this._locale, + isUTC: this._isUTC, + strict: this._strict + }; +} + +// FORMATTING + +addFormatToken(0, ['gg', 2], 0, function () { + return this.weekYear() % 100; +}); + +addFormatToken(0, ['GG', 2], 0, function () { + return this.isoWeekYear() % 100; +}); + +function addWeekYearFormatToken (token, getter) { + addFormatToken(0, [token, token.length], 0, getter); +} + +addWeekYearFormatToken('gggg', 'weekYear'); +addWeekYearFormatToken('ggggg', 'weekYear'); +addWeekYearFormatToken('GGGG', 'isoWeekYear'); +addWeekYearFormatToken('GGGGG', 'isoWeekYear'); + +// ALIASES + +addUnitAlias('weekYear', 'gg'); +addUnitAlias('isoWeekYear', 'GG'); + +// PRIORITY + +addUnitPriority('weekYear', 1); +addUnitPriority('isoWeekYear', 1); + + +// PARSING + +addRegexToken('G', matchSigned); +addRegexToken('g', matchSigned); +addRegexToken('GG', match1to2, match2); +addRegexToken('gg', match1to2, match2); +addRegexToken('GGGG', match1to4, match4); +addRegexToken('gggg', match1to4, match4); +addRegexToken('GGGGG', match1to6, match6); +addRegexToken('ggggg', match1to6, match6); + +addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) { + week[token.substr(0, 2)] = toInt(input); +}); + +addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { + week[token] = hooks.parseTwoDigitYear(input); +}); + +// MOMENTS + +function getSetWeekYear (input) { + return getSetWeekYearHelper.call(this, + input, + this.week(), + this.weekday(), + this.localeData()._week.dow, + this.localeData()._week.doy); +} + +function getSetISOWeekYear (input) { + return getSetWeekYearHelper.call(this, + input, this.isoWeek(), this.isoWeekday(), 1, 4); +} + +function getISOWeeksInYear () { + return weeksInYear(this.year(), 1, 4); +} + +function getWeeksInYear () { + var weekInfo = this.localeData()._week; + return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); +} + +function getSetWeekYearHelper(input, week, weekday, dow, doy) { + var weeksTarget; + if (input == null) { + return weekOfYear(this, dow, doy).year; + } else { + weeksTarget = weeksInYear(input, dow, doy); + if (week > weeksTarget) { + week = weeksTarget; + } + return setWeekAll.call(this, input, week, weekday, dow, doy); + } +} + +function setWeekAll(weekYear, week, weekday, dow, doy) { + var dayOfYearData = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy), + date = createUTCDate(dayOfYearData.year, 0, dayOfYearData.dayOfYear); + + this.year(date.getUTCFullYear()); + this.month(date.getUTCMonth()); + this.date(date.getUTCDate()); + return this; +} + +// FORMATTING + +addFormatToken('Q', 0, 'Qo', 'quarter'); + +// ALIASES + +addUnitAlias('quarter', 'Q'); + +// PRIORITY + +addUnitPriority('quarter', 7); + +// PARSING + +addRegexToken('Q', match1); +addParseToken('Q', function (input, array) { + array[MONTH] = (toInt(input) - 1) * 3; +}); + +// MOMENTS + +function getSetQuarter (input) { + return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); +} + +// FORMATTING + +addFormatToken('D', ['DD', 2], 'Do', 'date'); + +// ALIASES + +addUnitAlias('date', 'D'); + +// PRIOROITY +addUnitPriority('date', 9); + +// PARSING + +addRegexToken('D', match1to2); +addRegexToken('DD', match1to2, match2); +addRegexToken('Do', function (isStrict, locale) { + // TODO: Remove "ordinalParse" fallback in next major release. + return isStrict ? + (locale._dayOfMonthOrdinalParse || locale._ordinalParse) : + locale._dayOfMonthOrdinalParseLenient; +}); + +addParseToken(['D', 'DD'], DATE); +addParseToken('Do', function (input, array) { + array[DATE] = toInt(input.match(match1to2)[0]); +}); + +// MOMENTS + +var getSetDayOfMonth = makeGetSet('Date', true); + +// FORMATTING + +addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); + +// ALIASES + +addUnitAlias('dayOfYear', 'DDD'); + +// PRIORITY +addUnitPriority('dayOfYear', 4); + +// PARSING + +addRegexToken('DDD', match1to3); +addRegexToken('DDDD', match3); +addParseToken(['DDD', 'DDDD'], function (input, array, config) { + config._dayOfYear = toInt(input); +}); + +// HELPERS + +// MOMENTS + +function getSetDayOfYear (input) { + var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1; + return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); +} + +// FORMATTING + +addFormatToken('m', ['mm', 2], 0, 'minute'); + +// ALIASES + +addUnitAlias('minute', 'm'); + +// PRIORITY + +addUnitPriority('minute', 14); + +// PARSING + +addRegexToken('m', match1to2); +addRegexToken('mm', match1to2, match2); +addParseToken(['m', 'mm'], MINUTE); + +// MOMENTS + +var getSetMinute = makeGetSet('Minutes', false); + +// FORMATTING + +addFormatToken('s', ['ss', 2], 0, 'second'); + +// ALIASES + +addUnitAlias('second', 's'); + +// PRIORITY + +addUnitPriority('second', 15); + +// PARSING + +addRegexToken('s', match1to2); +addRegexToken('ss', match1to2, match2); +addParseToken(['s', 'ss'], SECOND); + +// MOMENTS + +var getSetSecond = makeGetSet('Seconds', false); + +// FORMATTING + +addFormatToken('S', 0, 0, function () { + return ~~(this.millisecond() / 100); +}); + +addFormatToken(0, ['SS', 2], 0, function () { + return ~~(this.millisecond() / 10); +}); + +addFormatToken(0, ['SSS', 3], 0, 'millisecond'); +addFormatToken(0, ['SSSS', 4], 0, function () { + return this.millisecond() * 10; +}); +addFormatToken(0, ['SSSSS', 5], 0, function () { + return this.millisecond() * 100; +}); +addFormatToken(0, ['SSSSSS', 6], 0, function () { + return this.millisecond() * 1000; +}); +addFormatToken(0, ['SSSSSSS', 7], 0, function () { + return this.millisecond() * 10000; +}); +addFormatToken(0, ['SSSSSSSS', 8], 0, function () { + return this.millisecond() * 100000; +}); +addFormatToken(0, ['SSSSSSSSS', 9], 0, function () { + return this.millisecond() * 1000000; +}); + + +// ALIASES + +addUnitAlias('millisecond', 'ms'); + +// PRIORITY + +addUnitPriority('millisecond', 16); + +// PARSING + +addRegexToken('S', match1to3, match1); +addRegexToken('SS', match1to3, match2); +addRegexToken('SSS', match1to3, match3); + +var token; +for (token = 'SSSS'; token.length <= 9; token += 'S') { + addRegexToken(token, matchUnsigned); +} + +function parseMs(input, array) { + array[MILLISECOND] = toInt(('0.' + input) * 1000); +} + +for (token = 'S'; token.length <= 9; token += 'S') { + addParseToken(token, parseMs); +} +// MOMENTS + +var getSetMillisecond = makeGetSet('Milliseconds', false); + +// FORMATTING + +addFormatToken('z', 0, 0, 'zoneAbbr'); +addFormatToken('zz', 0, 0, 'zoneName'); + +// MOMENTS + +function getZoneAbbr () { + return this._isUTC ? 'UTC' : ''; +} + +function getZoneName () { + return this._isUTC ? 'Coordinated Universal Time' : ''; +} + +var proto = Moment.prototype; + +proto.add = add; +proto.calendar = calendar$1; +proto.clone = clone; +proto.diff = diff; +proto.endOf = endOf; +proto.format = format; +proto.from = from; +proto.fromNow = fromNow; +proto.to = to; +proto.toNow = toNow; +proto.get = stringGet; +proto.invalidAt = invalidAt; +proto.isAfter = isAfter; +proto.isBefore = isBefore; +proto.isBetween = isBetween; +proto.isSame = isSame; +proto.isSameOrAfter = isSameOrAfter; +proto.isSameOrBefore = isSameOrBefore; +proto.isValid = isValid$2; +proto.lang = lang; +proto.locale = locale; +proto.localeData = localeData; +proto.max = prototypeMax; +proto.min = prototypeMin; +proto.parsingFlags = parsingFlags; +proto.set = stringSet; +proto.startOf = startOf; +proto.subtract = subtract; +proto.toArray = toArray; +proto.toObject = toObject; +proto.toDate = toDate; +proto.toISOString = toISOString; +proto.inspect = inspect; +proto.toJSON = toJSON; +proto.toString = toString; +proto.unix = unix; +proto.valueOf = valueOf; +proto.creationData = creationData; +proto.year = getSetYear; +proto.isLeapYear = getIsLeapYear; +proto.weekYear = getSetWeekYear; +proto.isoWeekYear = getSetISOWeekYear; +proto.quarter = proto.quarters = getSetQuarter; +proto.month = getSetMonth; +proto.daysInMonth = getDaysInMonth; +proto.week = proto.weeks = getSetWeek; +proto.isoWeek = proto.isoWeeks = getSetISOWeek; +proto.weeksInYear = getWeeksInYear; +proto.isoWeeksInYear = getISOWeeksInYear; +proto.date = getSetDayOfMonth; +proto.day = proto.days = getSetDayOfWeek; +proto.weekday = getSetLocaleDayOfWeek; +proto.isoWeekday = getSetISODayOfWeek; +proto.dayOfYear = getSetDayOfYear; +proto.hour = proto.hours = getSetHour; +proto.minute = proto.minutes = getSetMinute; +proto.second = proto.seconds = getSetSecond; +proto.millisecond = proto.milliseconds = getSetMillisecond; +proto.utcOffset = getSetOffset; +proto.utc = setOffsetToUTC; +proto.local = setOffsetToLocal; +proto.parseZone = setOffsetToParsedOffset; +proto.hasAlignedHourOffset = hasAlignedHourOffset; +proto.isDST = isDaylightSavingTime; +proto.isLocal = isLocal; +proto.isUtcOffset = isUtcOffset; +proto.isUtc = isUtc; +proto.isUTC = isUtc; +proto.zoneAbbr = getZoneAbbr; +proto.zoneName = getZoneName; +proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth); +proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth); +proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear); +proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/', getSetZone); +proto.isDSTShifted = deprecate('isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information', isDaylightSavingTimeShifted); + +function createUnix (input) { + return createLocal(input * 1000); +} + +function createInZone () { + return createLocal.apply(null, arguments).parseZone(); +} + +function preParsePostFormat (string) { + return string; +} + +var proto$1 = Locale.prototype; + +proto$1.calendar = calendar; +proto$1.longDateFormat = longDateFormat; +proto$1.invalidDate = invalidDate; +proto$1.ordinal = ordinal; +proto$1.preparse = preParsePostFormat; +proto$1.postformat = preParsePostFormat; +proto$1.relativeTime = relativeTime; +proto$1.pastFuture = pastFuture; +proto$1.set = set; + +proto$1.months = localeMonths; +proto$1.monthsShort = localeMonthsShort; +proto$1.monthsParse = localeMonthsParse; +proto$1.monthsRegex = monthsRegex; +proto$1.monthsShortRegex = monthsShortRegex; +proto$1.week = localeWeek; +proto$1.firstDayOfYear = localeFirstDayOfYear; +proto$1.firstDayOfWeek = localeFirstDayOfWeek; + +proto$1.weekdays = localeWeekdays; +proto$1.weekdaysMin = localeWeekdaysMin; +proto$1.weekdaysShort = localeWeekdaysShort; +proto$1.weekdaysParse = localeWeekdaysParse; + +proto$1.weekdaysRegex = weekdaysRegex; +proto$1.weekdaysShortRegex = weekdaysShortRegex; +proto$1.weekdaysMinRegex = weekdaysMinRegex; + +proto$1.isPM = localeIsPM; +proto$1.meridiem = localeMeridiem; + +function get$1 (format, index, field, setter) { + var locale = getLocale(); + var utc = createUTC().set(setter, index); + return locale[field](utc, format); +} + +function listMonthsImpl (format, index, field) { + if (isNumber(format)) { + index = format; + format = undefined; + } + + format = format || ''; + + if (index != null) { + return get$1(format, index, field, 'month'); + } + + var i; + var out = []; + for (i = 0; i < 12; i++) { + out[i] = get$1(format, i, field, 'month'); + } + return out; +} + +// () +// (5) +// (fmt, 5) +// (fmt) +// (true) +// (true, 5) +// (true, fmt, 5) +// (true, fmt) +function listWeekdaysImpl (localeSorted, format, index, field) { + if (typeof localeSorted === 'boolean') { + if (isNumber(format)) { + index = format; + format = undefined; + } + + format = format || ''; + } else { + format = localeSorted; + index = format; + localeSorted = false; + + if (isNumber(format)) { + index = format; + format = undefined; + } + + format = format || ''; + } + + var locale = getLocale(), + shift = localeSorted ? locale._week.dow : 0; + + if (index != null) { + return get$1(format, (index + shift) % 7, field, 'day'); + } + + var i; + var out = []; + for (i = 0; i < 7; i++) { + out[i] = get$1(format, (i + shift) % 7, field, 'day'); + } + return out; +} + +function listMonths (format, index) { + return listMonthsImpl(format, index, 'months'); +} + +function listMonthsShort (format, index) { + return listMonthsImpl(format, index, 'monthsShort'); +} + +function listWeekdays (localeSorted, format, index) { + return listWeekdaysImpl(localeSorted, format, index, 'weekdays'); +} + +function listWeekdaysShort (localeSorted, format, index) { + return listWeekdaysImpl(localeSorted, format, index, 'weekdaysShort'); +} + +function listWeekdaysMin (localeSorted, format, index) { + return listWeekdaysImpl(localeSorted, format, index, 'weekdaysMin'); +} + +getSetGlobalLocale('en', { + dayOfMonthOrdinalParse: /\d{1,2}(th|st|nd|rd)/, + ordinal : function (number) { + var b = number % 10, + output = (toInt(number % 100 / 10) === 1) ? 'th' : + (b === 1) ? 'st' : + (b === 2) ? 'nd' : + (b === 3) ? 'rd' : 'th'; + return number + output; + } +}); + +// Side effect imports + +hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', getSetGlobalLocale); +hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', getLocale); + +var mathAbs = Math.abs; + +function abs () { + var data = this._data; + + this._milliseconds = mathAbs(this._milliseconds); + this._days = mathAbs(this._days); + this._months = mathAbs(this._months); + + data.milliseconds = mathAbs(data.milliseconds); + data.seconds = mathAbs(data.seconds); + data.minutes = mathAbs(data.minutes); + data.hours = mathAbs(data.hours); + data.months = mathAbs(data.months); + data.years = mathAbs(data.years); + + return this; +} + +function addSubtract$1 (duration, input, value, direction) { + var other = createDuration(input, value); + + duration._milliseconds += direction * other._milliseconds; + duration._days += direction * other._days; + duration._months += direction * other._months; + + return duration._bubble(); +} + +// supports only 2.0-style add(1, 's') or add(duration) +function add$1 (input, value) { + return addSubtract$1(this, input, value, 1); +} + +// supports only 2.0-style subtract(1, 's') or subtract(duration) +function subtract$1 (input, value) { + return addSubtract$1(this, input, value, -1); +} + +function absCeil (number) { + if (number < 0) { + return Math.floor(number); + } else { + return Math.ceil(number); + } +} + +function bubble () { + var milliseconds = this._milliseconds; + var days = this._days; + var months = this._months; + var data = this._data; + var seconds, minutes, hours, years, monthsFromDays; + + // if we have a mix of positive and negative values, bubble down first + // check: https://github.com/moment/moment/issues/2166 + if (!((milliseconds >= 0 && days >= 0 && months >= 0) || + (milliseconds <= 0 && days <= 0 && months <= 0))) { + milliseconds += absCeil(monthsToDays(months) + days) * 864e5; + days = 0; + months = 0; + } + + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; + + seconds = absFloor(milliseconds / 1000); + data.seconds = seconds % 60; + + minutes = absFloor(seconds / 60); + data.minutes = minutes % 60; + + hours = absFloor(minutes / 60); + data.hours = hours % 24; + + days += absFloor(hours / 24); + + // convert days to months + monthsFromDays = absFloor(daysToMonths(days)); + months += monthsFromDays; + days -= absCeil(monthsToDays(monthsFromDays)); + + // 12 months -> 1 year + years = absFloor(months / 12); + months %= 12; + + data.days = days; + data.months = months; + data.years = years; + + return this; +} + +function daysToMonths (days) { + // 400 years have 146097 days (taking into account leap year rules) + // 400 years have 12 months === 4800 + return days * 4800 / 146097; +} + +function monthsToDays (months) { + // the reverse of daysToMonths + return months * 146097 / 4800; +} + +function as (units) { + if (!this.isValid()) { + return NaN; + } + var days; + var months; + var milliseconds = this._milliseconds; + + units = normalizeUnits(units); + + if (units === 'month' || units === 'year') { + days = this._days + milliseconds / 864e5; + months = this._months + daysToMonths(days); + return units === 'month' ? months : months / 12; + } else { + // handle milliseconds separately because of floating point math errors (issue #1867) + days = this._days + Math.round(monthsToDays(this._months)); + switch (units) { + case 'week' : return days / 7 + milliseconds / 6048e5; + case 'day' : return days + milliseconds / 864e5; + case 'hour' : return days * 24 + milliseconds / 36e5; + case 'minute' : return days * 1440 + milliseconds / 6e4; + case 'second' : return days * 86400 + milliseconds / 1000; + // Math.floor prevents floating point math errors here + case 'millisecond': return Math.floor(days * 864e5) + milliseconds; + default: throw new Error('Unknown unit ' + units); + } + } +} + +// TODO: Use this.as('ms')? +function valueOf$1 () { + if (!this.isValid()) { + return NaN; + } + return ( + this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6 + ); +} + +function makeAs (alias) { + return function () { + return this.as(alias); + }; +} + +var asMilliseconds = makeAs('ms'); +var asSeconds = makeAs('s'); +var asMinutes = makeAs('m'); +var asHours = makeAs('h'); +var asDays = makeAs('d'); +var asWeeks = makeAs('w'); +var asMonths = makeAs('M'); +var asYears = makeAs('y'); + +function clone$1 () { + return createDuration(this); +} + +function get$2 (units) { + units = normalizeUnits(units); + return this.isValid() ? this[units + 's']() : NaN; +} + +function makeGetter(name) { + return function () { + return this.isValid() ? this._data[name] : NaN; + }; +} + +var milliseconds = makeGetter('milliseconds'); +var seconds = makeGetter('seconds'); +var minutes = makeGetter('minutes'); +var hours = makeGetter('hours'); +var days = makeGetter('days'); +var months = makeGetter('months'); +var years = makeGetter('years'); + +function weeks () { + return absFloor(this.days() / 7); +} + +var round = Math.round; +var thresholds = { + ss: 44, // a few seconds to seconds + s : 45, // seconds to minute + m : 45, // minutes to hour + h : 22, // hours to day + d : 26, // days to month + M : 11 // months to year +}; + +// helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize +function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { + return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); +} + +function relativeTime$1 (posNegDuration, withoutSuffix, locale) { + var duration = createDuration(posNegDuration).abs(); + var seconds = round(duration.as('s')); + var minutes = round(duration.as('m')); + var hours = round(duration.as('h')); + var days = round(duration.as('d')); + var months = round(duration.as('M')); + var years = round(duration.as('y')); + + var a = seconds <= thresholds.ss && ['s', seconds] || + seconds < thresholds.s && ['ss', seconds] || + minutes <= 1 && ['m'] || + minutes < thresholds.m && ['mm', minutes] || + hours <= 1 && ['h'] || + hours < thresholds.h && ['hh', hours] || + days <= 1 && ['d'] || + days < thresholds.d && ['dd', days] || + months <= 1 && ['M'] || + months < thresholds.M && ['MM', months] || + years <= 1 && ['y'] || ['yy', years]; + + a[2] = withoutSuffix; + a[3] = +posNegDuration > 0; + a[4] = locale; + return substituteTimeAgo.apply(null, a); +} + +// This function allows you to set the rounding function for relative time strings +function getSetRelativeTimeRounding (roundingFunction) { + if (roundingFunction === undefined) { + return round; + } + if (typeof(roundingFunction) === 'function') { + round = roundingFunction; + return true; + } + return false; +} + +// This function allows you to set a threshold for relative time strings +function getSetRelativeTimeThreshold (threshold, limit) { + if (thresholds[threshold] === undefined) { + return false; + } + if (limit === undefined) { + return thresholds[threshold]; + } + thresholds[threshold] = limit; + if (threshold === 's') { + thresholds.ss = limit - 1; + } + return true; +} + +function humanize (withSuffix) { + if (!this.isValid()) { + return this.localeData().invalidDate(); + } + + var locale = this.localeData(); + var output = relativeTime$1(this, !withSuffix, locale); + + if (withSuffix) { + output = locale.pastFuture(+this, output); + } + + return locale.postformat(output); +} + +var abs$1 = Math.abs; + +function sign(x) { + return ((x > 0) - (x < 0)) || +x; +} + +function toISOString$1() { + // for ISO strings we do not use the normal bubbling rules: + // * milliseconds bubble up until they become hours + // * days do not bubble at all + // * months bubble up until they become years + // This is because there is no context-free conversion between hours and days + // (think of clock changes) + // and also not between days and months (28-31 days per month) + if (!this.isValid()) { + return this.localeData().invalidDate(); + } + + var seconds = abs$1(this._milliseconds) / 1000; + var days = abs$1(this._days); + var months = abs$1(this._months); + var minutes, hours, years; + + // 3600 seconds -> 60 minutes -> 1 hour + minutes = absFloor(seconds / 60); + hours = absFloor(minutes / 60); + seconds %= 60; + minutes %= 60; + + // 12 months -> 1 year + years = absFloor(months / 12); + months %= 12; + + + // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js + var Y = years; + var M = months; + var D = days; + var h = hours; + var m = minutes; + var s = seconds ? seconds.toFixed(3).replace(/\.?0+$/, '') : ''; + var total = this.asSeconds(); + + if (!total) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } + + var totalSign = total < 0 ? '-' : ''; + var ymSign = sign(this._months) !== sign(total) ? '-' : ''; + var daysSign = sign(this._days) !== sign(total) ? '-' : ''; + var hmsSign = sign(this._milliseconds) !== sign(total) ? '-' : ''; + + return totalSign + 'P' + + (Y ? ymSign + Y + 'Y' : '') + + (M ? ymSign + M + 'M' : '') + + (D ? daysSign + D + 'D' : '') + + ((h || m || s) ? 'T' : '') + + (h ? hmsSign + h + 'H' : '') + + (m ? hmsSign + m + 'M' : '') + + (s ? hmsSign + s + 'S' : ''); +} + +var proto$2 = Duration.prototype; + +proto$2.isValid = isValid$1; +proto$2.abs = abs; +proto$2.add = add$1; +proto$2.subtract = subtract$1; +proto$2.as = as; +proto$2.asMilliseconds = asMilliseconds; +proto$2.asSeconds = asSeconds; +proto$2.asMinutes = asMinutes; +proto$2.asHours = asHours; +proto$2.asDays = asDays; +proto$2.asWeeks = asWeeks; +proto$2.asMonths = asMonths; +proto$2.asYears = asYears; +proto$2.valueOf = valueOf$1; +proto$2._bubble = bubble; +proto$2.clone = clone$1; +proto$2.get = get$2; +proto$2.milliseconds = milliseconds; +proto$2.seconds = seconds; +proto$2.minutes = minutes; +proto$2.hours = hours; +proto$2.days = days; +proto$2.weeks = weeks; +proto$2.months = months; +proto$2.years = years; +proto$2.humanize = humanize; +proto$2.toISOString = toISOString$1; +proto$2.toString = toISOString$1; +proto$2.toJSON = toISOString$1; +proto$2.locale = locale; +proto$2.localeData = localeData; + +proto$2.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', toISOString$1); +proto$2.lang = lang; + +// Side effect imports + +// FORMATTING + +addFormatToken('X', 0, 0, 'unix'); +addFormatToken('x', 0, 0, 'valueOf'); + +// PARSING + +addRegexToken('x', matchSigned); +addRegexToken('X', matchTimestamp); +addParseToken('X', function (input, array, config) { + config._d = new Date(parseFloat(input, 10) * 1000); +}); +addParseToken('x', function (input, array, config) { + config._d = new Date(toInt(input)); +}); + +// Side effect imports + + +hooks.version = '2.21.0'; + +setHookCallback(createLocal); + +hooks.fn = proto; +hooks.min = min; +hooks.max = max; +hooks.now = now; +hooks.utc = createUTC; +hooks.unix = createUnix; +hooks.months = listMonths; +hooks.isDate = isDate; +hooks.locale = getSetGlobalLocale; +hooks.invalid = createInvalid; +hooks.duration = createDuration; +hooks.isMoment = isMoment; +hooks.weekdays = listWeekdays; +hooks.parseZone = createInZone; +hooks.localeData = getLocale; +hooks.isDuration = isDuration; +hooks.monthsShort = listMonthsShort; +hooks.weekdaysMin = listWeekdaysMin; +hooks.defineLocale = defineLocale; +hooks.updateLocale = updateLocale; +hooks.locales = listLocales; +hooks.weekdaysShort = listWeekdaysShort; +hooks.normalizeUnits = normalizeUnits; +hooks.relativeTimeRounding = getSetRelativeTimeRounding; +hooks.relativeTimeThreshold = getSetRelativeTimeThreshold; +hooks.calendarFormat = getCalendarFormat; +hooks.prototype = proto; + +// currently HTML5 input type only supports 24-hour formats +hooks.HTML5_FMT = { + DATETIME_LOCAL: 'YYYY-MM-DDTHH:mm', // + DATETIME_LOCAL_SECONDS: 'YYYY-MM-DDTHH:mm:ss', // + DATETIME_LOCAL_MS: 'YYYY-MM-DDTHH:mm:ss.SSS', // + DATE: 'YYYY-MM-DD', // + TIME: 'HH:mm', // + TIME_SECONDS: 'HH:mm:ss', // + TIME_MS: 'HH:mm:ss.SSS', // + WEEK: 'YYYY-[W]WW', // + MONTH: 'YYYY-MM' // +}; + +return hooks; + +}))); +;/** + * Parse a text request to a json query object tree + * + * @param {String} string The string to parse + * @return {Object} The json query tree + */ +function parseStringToObject(string) { + +var arrayExtend = function () { + var j, i, newlist = [], list_list = arguments; + for (j = 0; j < list_list.length; j += 1) { + for (i = 0; i < list_list[j].length; i += 1) { + newlist.push(list_list[j][i]); + } + } + return newlist; + +}, mkSimpleQuery = function (key, value, operator) { + var object = {"type": "simple", "key": key, "value": value}; + if (operator !== undefined) { + object.operator = operator; + } + return object; + +}, mkNotQuery = function (query) { + if (query.operator === "NOT") { + return query.query_list[0]; + } + return {"type": "complex", "operator": "NOT", "query_list": [query]}; + +}, mkComplexQuery = function (operator, query_list) { + var i, query_list2 = []; + for (i = 0; i < query_list.length; i += 1) { + if (query_list[i].operator === operator) { + query_list2 = arrayExtend(query_list2, query_list[i].query_list); + } else { + query_list2.push(query_list[i]); + } + } + return {type:"complex",operator:operator,query_list:query_list2}; + +}, simpleQuerySetKey = function (query, key) { + var i; + if (query.type === "complex") { + for (i = 0; i < query.query_list.length; ++i) { + simpleQuerySetKey (query.query_list[i],key); + } + return true; + } + if (query.type === "simple" && !query.key) { + query.key = key; + return true; + } + return false; +}, + error_offsets = [], + error_lookaheads = [], + error_count = 0, + result; +;/* parser generated by jison 0.4.16 */ +/* + Returns a Parser object of the following structure: + + Parser: { + yy: {} + } + + Parser.prototype: { + yy: {}, + trace: function(), + symbols_: {associative list: name ==> number}, + terminals_: {associative list: number ==> name}, + productions_: [...], + performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), + table: [...], + defaultActions: {...}, + parseError: function(str, hash), + parse: function(input), + + lexer: { + EOF: 1, + parseError: function(str, hash), + setInput: function(input), + input: function(), + unput: function(str), + more: function(), + less: function(n), + pastInput: function(), + upcomingInput: function(), + showPosition: function(), + test_match: function(regex_match_array, rule_index), + next: function(), + lex: function(), + begin: function(condition), + popState: function(), + _currentRules: function(), + topState: function(), + pushState: function(condition), + + options: { + ranges: boolean (optional: true ==> token location info will include a .range[] member) + flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) + backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) + }, + + performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), + rules: [...], + conditions: {associative list: name ==> set}, + } + } + + + token location info (@$, _$, etc.): { + first_line: n, + last_line: n, + first_column: n, + last_column: n, + range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) + } + + + the parseError function receives a 'hash' object with these members for lexer and parser errors: { + text: (matched text) + token: (the produced terminal token, if any) + line: (yylineno) + } + while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { + loc: (yylloc) + expected: (string describing the set of expected tokens) + recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) + } +*/ +var parser = (function(){ +var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,5],$V1=[1,7],$V2=[1,8],$V3=[1,10],$V4=[1,12],$V5=[1,6,7,15],$V6=[1,6,7,9,12,14,15,16,19,21],$V7=[1,6,7,9,11,12,14,15,16,19,21],$V8=[2,17]; +var parser = {trace: function trace() { }, +yy: {}, +symbols_: {"error":2,"begin":3,"search_text":4,"end":5,"EOF":6,"NEWLINE":7,"and_expression":8,"OR":9,"boolean_expression":10,"AND":11,"NOT":12,"expression":13,"LEFT_PARENTHESE":14,"RIGHT_PARENTHESE":15,"WORD":16,"DEFINITION":17,"value":18,"OPERATOR":19,"string":20,"QUOTE":21,"QUOTED_STRING":22,"$accept":0,"$end":1}, +terminals_: {2:"error",6:"EOF",7:"NEWLINE",9:"OR",11:"AND",12:"NOT",14:"LEFT_PARENTHESE",15:"RIGHT_PARENTHESE",16:"WORD",17:"DEFINITION",19:"OPERATOR",21:"QUOTE",22:"QUOTED_STRING"}, +productions_: [0,[3,2],[5,0],[5,1],[5,1],[4,1],[4,2],[4,3],[8,1],[8,3],[10,2],[10,1],[13,3],[13,3],[13,1],[18,2],[18,1],[20,1],[20,3]], +performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { +/* this == yyval */ + +var $0 = $$.length - 1; +switch (yystate) { +case 1: + return $$[$0-1]; +break; +case 5: case 8: case 11: case 14: case 16: + this.$ = $$[$0]; +break; +case 6: + this.$ = mkComplexQuery('AND', [$$[$0-1], $$[$0]]); +break; +case 7: + this.$ = mkComplexQuery('OR', [$$[$0-2], $$[$0]]); +break; +case 9: + this.$ = mkComplexQuery('AND', [$$[$0-2], $$[$0]]); +break; +case 10: + this.$ = mkNotQuery($$[$0]); +break; +case 12: + this.$ = $$[$0-1]; +break; +case 13: + simpleQuerySetKey($$[$0], $$[$0-2]); this.$ = $$[$0]; +break; +case 15: + $$[$0].operator = $$[$0-1] ; this.$ = $$[$0]; +break; +case 17: + this.$ = mkSimpleQuery('', $$[$0]); +break; +case 18: + this.$ = mkSimpleQuery('', $$[$0-1]); +break; +} +}, +table: [{3:1,4:2,8:3,10:4,12:$V0,13:6,14:$V1,16:$V2,18:9,19:$V3,20:11,21:$V4},{1:[3]},{1:[2,2],5:13,6:[1,14],7:[1,15]},o($V5,[2,5],{8:3,10:4,13:6,18:9,20:11,4:16,9:[1,17],12:$V0,14:$V1,16:$V2,19:$V3,21:$V4}),o($V6,[2,8],{11:[1,18]}),{13:19,14:$V1,16:$V2,18:9,19:$V3,20:11,21:$V4},o($V7,[2,11]),{4:20,8:3,10:4,12:$V0,13:6,14:$V1,16:$V2,18:9,19:$V3,20:11,21:$V4},o($V7,$V8,{17:[1,21]}),o($V7,[2,14]),{16:[1,23],20:22,21:$V4},o($V7,[2,16]),{22:[1,24]},{1:[2,1]},{1:[2,3]},{1:[2,4]},o($V5,[2,6]),{4:25,8:3,10:4,12:$V0,13:6,14:$V1,16:$V2,18:9,19:$V3,20:11,21:$V4},{8:26,10:4,12:$V0,13:6,14:$V1,16:$V2,18:9,19:$V3,20:11,21:$V4},o($V7,[2,10]),{15:[1,27]},{13:28,14:$V1,16:$V2,18:9,19:$V3,20:11,21:$V4},o($V7,[2,15]),o($V7,$V8),{21:[1,29]},o($V5,[2,7]),o($V6,[2,9]),o($V7,[2,12]),o($V7,[2,13]),o($V7,[2,18])], +defaultActions: {13:[2,1],14:[2,3],15:[2,4]}, +parseError: function parseError(str, hash) { + if (hash.recoverable) { + this.trace(str); + } else { + function _parseError (msg, hash) { + this.message = msg; + this.hash = hash; + } + _parseError.prototype = new Error(); + + throw new _parseError(str, hash); + } +}, +parse: function parse(input) { + var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; + var args = lstack.slice.call(arguments, 1); + var lexer = Object.create(this.lexer); + var sharedState = { yy: {} }; + for (var k in this.yy) { + if (Object.prototype.hasOwnProperty.call(this.yy, k)) { + sharedState.yy[k] = this.yy[k]; + } + } + lexer.setInput(input, sharedState.yy); + sharedState.yy.lexer = lexer; + sharedState.yy.parser = this; + if (typeof lexer.yylloc == 'undefined') { + lexer.yylloc = {}; + } + var yyloc = lexer.yylloc; + lstack.push(yyloc); + var ranges = lexer.options && lexer.options.ranges; + if (typeof sharedState.yy.parseError === 'function') { + this.parseError = sharedState.yy.parseError; + } else { + this.parseError = Object.getPrototypeOf(this).parseError; + } + function popStack(n) { + stack.length = stack.length - 2 * n; + vstack.length = vstack.length - n; + lstack.length = lstack.length - n; + } + _token_stack: + var lex = function () { + var token; + token = lexer.lex() || EOF; + if (typeof token !== 'number') { + token = self.symbols_[token] || token; + } + return token; + }; + var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; + while (true) { + state = stack[stack.length - 1]; + if (this.defaultActions[state]) { + action = this.defaultActions[state]; + } else { + if (symbol === null || typeof symbol == 'undefined') { + symbol = lex(); + } + action = table[state] && table[state][symbol]; + } + if (typeof action === 'undefined' || !action.length || !action[0]) { + var errStr = ''; + expected = []; + for (p in table[state]) { + if (this.terminals_[p] && p > TERROR) { + expected.push('\'' + this.terminals_[p] + '\''); + } + } + if (lexer.showPosition) { + errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; + } else { + errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); + } + this.parseError(errStr, { + text: lexer.match, + token: this.terminals_[symbol] || symbol, + line: lexer.yylineno, + loc: yyloc, + expected: expected + }); + } + if (action[0] instanceof Array && action.length > 1) { + throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); + } + switch (action[0]) { + case 1: + stack.push(symbol); + vstack.push(lexer.yytext); + lstack.push(lexer.yylloc); + stack.push(action[1]); + symbol = null; + if (!preErrorSymbol) { + yyleng = lexer.yyleng; + yytext = lexer.yytext; + yylineno = lexer.yylineno; + yyloc = lexer.yylloc; + if (recovering > 0) { + recovering--; + } + } else { + symbol = preErrorSymbol; + preErrorSymbol = null; + } + break; + case 2: + len = this.productions_[action[1]][1]; + yyval.$ = vstack[vstack.length - len]; + yyval._$ = { + first_line: lstack[lstack.length - (len || 1)].first_line, + last_line: lstack[lstack.length - 1].last_line, + first_column: lstack[lstack.length - (len || 1)].first_column, + last_column: lstack[lstack.length - 1].last_column + }; + if (ranges) { + yyval._$.range = [ + lstack[lstack.length - (len || 1)].range[0], + lstack[lstack.length - 1].range[1] + ]; + } + r = this.performAction.apply(yyval, [ + yytext, + yyleng, + yylineno, + sharedState.yy, + action[1], + vstack, + lstack + ].concat(args)); + if (typeof r !== 'undefined') { + return r; + } + if (len) { + stack = stack.slice(0, -1 * len * 2); + vstack = vstack.slice(0, -1 * len); + lstack = lstack.slice(0, -1 * len); + } + stack.push(this.productions_[action[1]][0]); + vstack.push(yyval.$); + lstack.push(yyval._$); + newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; + stack.push(newState); + break; + case 3: + return true; + } + } + return true; +}}; +/* generated by jison-lex 0.3.4 */ +var lexer = (function(){ +var lexer = ({ + +EOF:1, + +parseError:function parseError(str, hash) { + if (this.yy.parser) { + this.yy.parser.parseError(str, hash); + } else { + throw new Error(str); + } + }, + +// resets the lexer, sets new input +setInput:function (input, yy) { + this.yy = yy || this.yy || {}; + this._input = input; + this._more = this._backtrack = this.done = false; + this.yylineno = this.yyleng = 0; + this.yytext = this.matched = this.match = ''; + this.conditionStack = ['INITIAL']; + this.yylloc = { + first_line: 1, + first_column: 0, + last_line: 1, + last_column: 0 + }; + if (this.options.ranges) { + this.yylloc.range = [0,0]; + } + this.offset = 0; + return this; + }, + +// consumes and returns one char from the input +input:function () { + var ch = this._input[0]; + this.yytext += ch; + this.yyleng++; + this.offset++; + this.match += ch; + this.matched += ch; + var lines = ch.match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno++; + this.yylloc.last_line++; + } else { + this.yylloc.last_column++; + } + if (this.options.ranges) { + this.yylloc.range[1]++; + } + + this._input = this._input.slice(1); + return ch; + }, + +// unshifts one char (or a string) into the input +unput:function (ch) { + var len = ch.length; + var lines = ch.split(/(?:\r\n?|\n)/g); + + this._input = ch + this._input; + this.yytext = this.yytext.substr(0, this.yytext.length - len); + //this.yyleng -= len; + this.offset -= len; + var oldLines = this.match.split(/(?:\r\n?|\n)/g); + this.match = this.match.substr(0, this.match.length - 1); + this.matched = this.matched.substr(0, this.matched.length - 1); + + if (lines.length - 1) { + this.yylineno -= lines.length - 1; + } + var r = this.yylloc.range; + + this.yylloc = { + first_line: this.yylloc.first_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.first_column, + last_column: lines ? + (lines.length === oldLines.length ? this.yylloc.first_column : 0) + + oldLines[oldLines.length - lines.length].length - lines[0].length : + this.yylloc.first_column - len + }; + + if (this.options.ranges) { + this.yylloc.range = [r[0], r[0] + this.yyleng - len]; + } + this.yyleng = this.yytext.length; + return this; + }, + +// When called from action, caches matched text and appends it on next action +more:function () { + this._more = true; + return this; + }, + +// When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. +reject:function () { + if (this.options.backtrack_lexer) { + this._backtrack = true; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + + } + return this; + }, + +// retain first n characters of the match +less:function (n) { + this.unput(this.match.slice(n)); + }, + +// displays already matched input, i.e. for error messages +pastInput:function () { + var past = this.matched.substr(0, this.matched.length - this.match.length); + return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); + }, + +// displays upcoming input, i.e. for error messages +upcomingInput:function () { + var next = this.match; + if (next.length < 20) { + next += this._input.substr(0, 20-next.length); + } + return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); + }, + +// displays the character position where the lexing error occurred, i.e. for error messages +showPosition:function () { + var pre = this.pastInput(); + var c = new Array(pre.length + 1).join("-"); + return pre + this.upcomingInput() + "\n" + c + "^"; + }, + +// test the lexed token: return FALSE when not a match, otherwise return token +test_match:function (match, indexed_rule) { + var token, + lines, + backup; + + if (this.options.backtrack_lexer) { + // save context + backup = { + yylineno: this.yylineno, + yylloc: { + first_line: this.yylloc.first_line, + last_line: this.last_line, + first_column: this.yylloc.first_column, + last_column: this.yylloc.last_column + }, + yytext: this.yytext, + match: this.match, + matches: this.matches, + matched: this.matched, + yyleng: this.yyleng, + offset: this.offset, + _more: this._more, + _input: this._input, + yy: this.yy, + conditionStack: this.conditionStack.slice(0), + done: this.done + }; + if (this.options.ranges) { + backup.yylloc.range = this.yylloc.range.slice(0); + } + } + + lines = match[0].match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno += lines.length; + } + this.yylloc = { + first_line: this.yylloc.last_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.last_column, + last_column: lines ? + lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : + this.yylloc.last_column + match[0].length + }; + this.yytext += match[0]; + this.match += match[0]; + this.matches = match; + this.yyleng = this.yytext.length; + if (this.options.ranges) { + this.yylloc.range = [this.offset, this.offset += this.yyleng]; + } + this._more = false; + this._backtrack = false; + this._input = this._input.slice(match[0].length); + this.matched += match[0]; + token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); + if (this.done && this._input) { + this.done = false; + } + if (token) { + return token; + } else if (this._backtrack) { + // recover context + for (var k in backup) { + this[k] = backup[k]; + } + return false; // rule action called reject() implying the next rule should be tested instead. + } + return false; + }, + +// return next match in input +next:function () { + if (this.done) { + return this.EOF; + } + if (!this._input) { + this.done = true; + } + + var token, + match, + tempMatch, + index; + if (!this._more) { + this.yytext = ''; + this.match = ''; + } + var rules = this._currentRules(); + for (var i = 0; i < rules.length; i++) { + tempMatch = this._input.match(this.rules[rules[i]]); + if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { + match = tempMatch; + index = i; + if (this.options.backtrack_lexer) { + token = this.test_match(tempMatch, rules[i]); + if (token !== false) { + return token; + } else if (this._backtrack) { + match = false; + continue; // rule action called reject() implying a rule MISmatch. + } else { + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + } else if (!this.options.flex) { + break; + } + } + } + if (match) { + token = this.test_match(match, rules[index]); + if (token !== false) { + return token; + } + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + if (this._input === "") { + return this.EOF; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + } + }, + +// return next match that has a token +lex:function lex() { + var r = this.next(); + if (r) { + return r; + } else { + return this.lex(); + } + }, + +// activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) +begin:function begin(condition) { + this.conditionStack.push(condition); + }, + +// pop the previously active lexer condition state off the condition stack +popState:function popState() { + var n = this.conditionStack.length - 1; + if (n > 0) { + return this.conditionStack.pop(); + } else { + return this.conditionStack[0]; + } + }, + +// produce the lexer rule set which is active for the currently active lexer condition state +_currentRules:function _currentRules() { + if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { + return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; + } else { + return this.conditions["INITIAL"].rules; + } + }, + +// return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available +topState:function topState(n) { + n = this.conditionStack.length - 1 - Math.abs(n || 0); + if (n >= 0) { + return this.conditionStack[n]; + } else { + return "INITIAL"; + } + }, + +// alias for begin(condition) +pushState:function pushState(condition) { + this.begin(condition); + }, + +// return the number of states currently on the stack +stateStackSize:function stateStackSize() { + return this.conditionStack.length; + }, +options: {}, +performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { +var YYSTATE=YY_START; +switch($avoiding_name_collisions) { +case 0:this.begin("letsquote"); return "QUOTE"; +break; +case 1:this.popState(); this.begin("endquote"); return "QUOTED_STRING"; +break; +case 2:this.popState(); return "QUOTE"; +break; +case 3:/* skip whitespace */ +break; +case 4:return "LEFT_PARENTHESE"; +break; +case 5:return "RIGHT_PARENTHESE"; +break; +case 6:return "AND"; +break; +case 7:return "OR"; +break; +case 8:return "NOT"; +break; +case 9:return "DEFINITION"; +break; +case 10:return 19; +break; +case 11:return 16; +break; +case 12:return 6; +break; +} +}, +rules: [/^(?:")/,/^(?:(\\"|[^"])*)/,/^(?:")/,/^(?:[^\S]+)/,/^(?:\()/,/^(?:\))/,/^(?:AND\b)/,/^(?:OR\b)/,/^(?:NOT\b)/,/^(?::)/,/^(?:(!?=|<=?|>=?))/,/^(?:[^\s\n"():>=?)$/i; + + /** + * Convert metadata values to array of strings. ex: + * + * "a" -> ["a"], + * {"content": "a"} -> ["a"] + * + * @param {Any} value The metadata value + * @return {Array} The value in string array format + */ + function metadataValueToStringArray(value) { + var i, new_value = []; + if (value === undefined) { + return undefined; + } + if (!Array.isArray(value)) { + value = [value]; + } + for (i = 0; i < value.length; i += 1) { + if (typeof value[i] === 'object') { + new_value[i] = value[i].content; + } else { + new_value[i] = value[i]; + } + } + return new_value; + } + + /** + * A sort function to sort items by key + * + * @param {Array} sort_list List of couples [key, direction] + * @return {Function} The sort function + */ + function generateSortFunction(key_schema, sort_list) { + return function sortByMultipleIndex(a, b) { + var result, + cast_to, + key = sort_list[0][0], + way = sort_list[0][1], + i, + l, + a_string_array, + b_string_array, + f_a, + f_b, + tmp; + + if (way === 'descending') { + result = 1; + } else if (way === 'ascending') { + result = -1; + } else { + throw new TypeError("Query.sortFunction(): " + + "Argument 2 must be 'ascending' or 'descending'"); + } + + if (key_schema !== undefined && + key_schema.key_set !== undefined && + key_schema.key_set[key] !== undefined && + key_schema.key_set[key].cast_to !== undefined) { + if (typeof key_schema.key_set[key].cast_to === "string") { + cast_to = key_schema.cast_lookup[key_schema.key_set[key].cast_to]; + } else { + cast_to = key_schema.key_set[key].cast_to; + } + f_a = cast_to(a[key]); + f_b = cast_to(b[key]); + if (typeof f_b.cmp === 'function') { + tmp = result * f_b.cmp(f_a); + if (tmp !== 0) { + return tmp; + } + if (sort_list.length > 1) { + return generateSortFunction(key_schema, sort_list.slice(1))(a, b); + } + return tmp; + } + if (f_a > f_b) { + return -result; + } + if (f_a < f_b) { + return result; + } + if (sort_list.length > 1) { + return generateSortFunction(key_schema, sort_list.slice(1))(a, b); + } + return 0; + } + + // this comparison is 5 times faster than json comparison + a_string_array = metadataValueToStringArray(a[key]) || []; + b_string_array = metadataValueToStringArray(b[key]) || []; + l = Math.max(a_string_array.length, b_string_array.length); + for (i = 0; i < l; i += 1) { + if (a_string_array[i] === undefined) { + return result; + } + if (b_string_array[i] === undefined) { + return -result; + } + if (a_string_array[i] > b_string_array[i]) { + return -result; + } + if (a_string_array[i] < b_string_array[i]) { + return result; + } + } + if (sort_list.length > 1) { + return generateSortFunction(key_schema, sort_list.slice(1))(a, b); + } + return 0; + + }; + } + + + /** + * Sort a list of items, according to keys and directions. + * + * @param {Array} sort_on_option List of couples [key, direction] + * @param {Array} list The item list to sort + * @return {Array} The filtered list + */ + function sortOn(sort_on_option, list, key_schema) { + if (!Array.isArray(sort_on_option)) { + throw new TypeError("jioquery.sortOn(): " + + "Argument 1 is not of type 'array'"); + } + list.sort(generateSortFunction( + key_schema, + sort_on_option + )); + return list; + } + + /** + * Limit a list of items, according to index and length. + * + * @param {Array} limit_option A couple [from, length] + * @param {Array} list The item list to limit + * @return {Array} The filtered list + */ + function limit(limit_option, list) { + if (!Array.isArray(limit_option)) { + throw new TypeError("jioquery.limit(): " + + "Argument 1 is not of type 'array'"); + } + if (!Array.isArray(list)) { + throw new TypeError("jioquery.limit(): " + + "Argument 2 is not of type 'array'"); + } + list.splice(0, limit_option[0]); + if (limit_option[1]) { + list.splice(limit_option[1]); + } + return list; + } + + /** + * Filter a list of items, modifying them to select only wanted keys. + * + * @param {Array} select_option Key list to keep + * @param {Array} list The item list to filter + * @return {Array} The filtered list + */ + function select(select_option, list) { + var i, j, new_item; + if (!Array.isArray(select_option)) { + throw new TypeError("jioquery.select(): " + + "Argument 1 is not of type Array"); + } + if (!Array.isArray(list)) { + throw new TypeError("jioquery.select(): " + + "Argument 2 is not of type Array"); + } + for (i = 0; i < list.length; i += 1) { + new_item = {}; + for (j = 0; j < select_option.length; j += 1) { + if (list[i].hasOwnProperty([select_option[j]])) { + new_item[select_option[j]] = list[i][select_option[j]]; + } + } + for (j in new_item) { + if (new_item.hasOwnProperty(j)) { + list[i] = new_item; + break; + } + } + } + return list; + } + + function checkKeySchema(key_schema) { + var prop; + + if (key_schema !== undefined) { + if (typeof key_schema !== 'object') { + throw new TypeError("Query().create(): " + + "key_schema is not of type 'object'"); + } + // key_set is mandatory + if (key_schema.key_set === undefined) { + throw new TypeError("Query().create(): " + + "key_schema has no 'key_set' property"); + } + for (prop in key_schema) { + if (key_schema.hasOwnProperty(prop)) { + switch (prop) { + case 'key_set': + case 'cast_lookup': + case 'match_lookup': + break; + default: + throw new TypeError("Query().create(): " + + "key_schema has unknown property '" + prop + "'"); + } + } + } + } + } + + /** + * The query to use to filter a list of objects. + * This is an abstract class. + * + * @class Query + * @constructor + */ + function Query(key_schema) { + + checkKeySchema(key_schema); + this._key_schema = key_schema || {}; + + /** + * Called before parsing the query. Must be overridden! + * + * @method onParseStart + * @param {Object} object The object shared in the parse process + * @param {Object} option Some option gave in parse() + */ + // this.onParseStart = emptyFunction; + + /** + * Called when parsing a simple query. Must be overridden! + * + * @method onParseSimpleQuery + * @param {Object} object The object shared in the parse process + * @param {Object} option Some option gave in parse() + */ + // this.onParseSimpleQuery = emptyFunction; + + /** + * Called when parsing a complex query. Must be overridden! + * + * @method onParseComplexQuery + * @param {Object} object The object shared in the parse process + * @param {Object} option Some option gave in parse() + */ + // this.onParseComplexQuery = emptyFunction; + + /** + * Called after parsing the query. Must be overridden! + * + * @method onParseEnd + * @param {Object} object The object shared in the parse process + * @param {Object} option Some option gave in parse() + */ + // this.onParseEnd = emptyFunction; + + return; + } + + /** + * Filter the item list with matching item only + * + * @method exec + * @param {Array} item_list The list of object + * @param {Object} [option] Some operation option + * @param {Array} [option.select_list] A object keys to retrieve + * @param {Array} [option.sort_on] Couples of object keys and "ascending" + * or "descending" + * @param {Array} [option.limit] Couple of integer, first is an index and + * second is the length. + */ + Query.prototype.exec = function (item_list, option) { + if (!Array.isArray(item_list)) { + throw new TypeError("Query().exec(): Argument 1 is not of type 'array'"); + } + if (option === undefined) { + option = {}; + } + if (typeof option !== 'object') { + throw new TypeError("Query().exec(): " + + "Optional argument 2 is not of type 'object'"); + } + var context = this, + i; + for (i = item_list.length - 1; i >= 0; i -= 1) { + if (!context.match(item_list[i])) { + item_list.splice(i, 1); + } + } + + if (option.sort_on) { + sortOn(option.sort_on, item_list, this._key_schema); + } + + if (option.limit) { + limit(option.limit, item_list); + } + + select(option.select_list || [], item_list); + + return new RSVP.Queue() + .push(function () { + return item_list; + }); + }; + + /** + * Test if an item matches this query + * + * @method match + * @param {Object} item The object to test + * @return {Boolean} true if match, false otherwise + */ + Query.prototype.match = function () { + return true; + }; + + /** + * Browse the Query in deep calling parser method in each step. + * + * `onParseStart` is called first, on end `onParseEnd` is called. + * It starts from the simple queries at the bottom of the tree calling the + * parser method `onParseSimpleQuery`, and go up calling the + * `onParseComplexQuery` method. + * + * @method parse + * @param {Object} option Any options you want (except 'parsed') + * @return {Any} The parse result + */ + Query.prototype.parse = function (option) { + var that = this, + object; + /** + * The recursive parser. + * + * @param {Object} object The object shared in the parse process + * @param {Object} options Some options usable in the parseMethods + * @return {Any} The parser result + */ + function recParse(object, option) { + var query = object.parsed, + queue = new RSVP.Queue(), + i; + + function enqueue(j) { + queue + .push(function () { + object.parsed = query.query_list[j]; + return recParse(object, option); + }) + .push(function () { + query.query_list[j] = object.parsed; + }); + } + + if (query.type === "complex") { + + + for (i = 0; i < query.query_list.length; i += 1) { + enqueue(i); + } + + return queue + .push(function () { + object.parsed = query; + return that.onParseComplexQuery(object, option); + }); + + } + if (query.type === "simple") { + return that.onParseSimpleQuery(object, option); + } + } + object = { + parsed: JSON.parse(JSON.stringify(that.serialized())) + }; + return new RSVP.Queue() + .push(function () { + return that.onParseStart(object, option); + }) + .push(function () { + return recParse(object, option); + }) + .push(function () { + return that.onParseEnd(object, option); + }) + .push(function () { + return object.parsed; + }); + + }; + + /** + * Convert this query to a parsable string. + * + * @method toString + * @return {String} The string version of this query + */ + Query.prototype.toString = function () { + return ""; + }; + + /** + * Convert this query to an jsonable object in order to be remake thanks to + * QueryFactory class. + * + * @method serialized + * @return {Object} The jsonable object + */ + Query.prototype.serialized = function () { + return undefined; + }; + + /** + * Provides static methods to create Query object + * + * @class QueryFactory + */ + function QueryFactory() { + return; + } + + /** + * Escapes regexp special chars from a string. + * + * @param {String} string The string to escape + * @return {String} The escaped string + */ + function stringEscapeRegexpCharacters(string) { + return string.replace(regexp_escape, "\\$&"); + } + + /** + * Inherits the prototype methods from one constructor into another. The + * prototype of `constructor` will be set to a new object created from + * `superConstructor`. + * + * @param {Function} constructor The constructor which inherits the super one + * @param {Function} superConstructor The super constructor + */ + function inherits(constructor, superConstructor) { + constructor.super_ = superConstructor; + constructor.prototype = Object.create(superConstructor.prototype, { + "constructor": { + "configurable": true, + "enumerable": false, + "writable": true, + "value": constructor + } + }); + } + + /** + * Convert a search text to a regexp. + * + * @param {String} string The string to convert + * @param {Boolean} [use_wildcard_character=true] Use wildcard "%" and "_" + * @return {RegExp} The search text regexp + */ + function searchTextToRegExp(string, use_wildcard_characters) { + if (typeof string !== 'string') { + throw new TypeError("jioquery.searchTextToRegExp(): " + + "Argument 1 is not of type 'string'"); + } + if (use_wildcard_characters === false) { + return new RegExp("^" + stringEscapeRegexpCharacters(string) + "$"); + } + return new RegExp("^" + stringEscapeRegexpCharacters(string) + .replace(regexp_percent, '[\\s\\S]*') + .replace(regexp_underscore, '.') + "$", "i"); + } + + /** + * The ComplexQuery inherits from Query, and compares one or several metadata + * values. + * + * @class ComplexQuery + * @extends Query + * @param {Object} [spec={}] The specifications + * @param {String} [spec.operator="AND"] The compare method to use + * @param {String} spec.key The metadata key + * @param {String} spec.value The value of the metadata to compare + */ + function ComplexQuery(spec, key_schema) { + Query.call(this, key_schema); + + /** + * Logical operator to use to compare object values + * + * @attribute operator + * @type String + * @default "AND" + * @optional + */ + this.operator = spec.operator; + + /** + * The sub Query list which are used to query an item. + * + * @attribute query_list + * @type Array + * @default [] + * @optional + */ + this.query_list = spec.query_list || []; + this.query_list = this.query_list.map( + // decorate the map to avoid sending the index as key_schema argument + function (o) { return QueryFactory.create(o, key_schema); } + ); + + } + inherits(ComplexQuery, Query); + + ComplexQuery.prototype.operator = "AND"; + ComplexQuery.prototype.type = "complex"; + + /** + * #crossLink "Query/match:method" + */ + ComplexQuery.prototype.match = function (item) { + var operator = this.operator; + if (!(regexp_operator.test(operator))) { + operator = "AND"; + } + return this[operator.toUpperCase()](item); + }; + + /** + * #crossLink "Query/toString:method" + */ + ComplexQuery.prototype.toString = function () { + var str_list = [], this_operator = this.operator; + if (this.operator === "NOT") { + str_list.push("NOT ("); + str_list.push(this.query_list[0].toString()); + str_list.push(")"); + return str_list.join(" "); + } + this.query_list.forEach(function (query) { + str_list.push("("); + str_list.push(query.toString()); + str_list.push(")"); + str_list.push(this_operator); + }); + str_list.length -= 1; + return str_list.join(" "); + }; + + /** + * #crossLink "Query/serialized:method" + */ + ComplexQuery.prototype.serialized = function () { + var s = { + "type": "complex", + "operator": this.operator, + "query_list": [] + }; + this.query_list.forEach(function (query) { + s.query_list.push( + typeof query.toJSON === "function" ? query.toJSON() : query + ); + }); + return s; + }; + ComplexQuery.prototype.toJSON = ComplexQuery.prototype.serialized; + + /** + * Comparison operator, test if all sub queries match the + * item value + * + * @method AND + * @param {Object} item The item to match + * @return {Boolean} true if all match, false otherwise + */ + ComplexQuery.prototype.AND = function (item) { + var result = true, + i = 0; + + while (result && (i !== this.query_list.length)) { + result = this.query_list[i].match(item); + i += 1; + } + return result; + + }; + + /** + * Comparison operator, test if one of the sub queries matches the + * item value + * + * @method OR + * @param {Object} item The item to match + * @return {Boolean} true if one match, false otherwise + */ + ComplexQuery.prototype.OR = function (item) { + var result = false, + i = 0; + + while ((!result) && (i !== this.query_list.length)) { + result = this.query_list[i].match(item); + i += 1; + } + + return result; + }; + + /** + * Comparison operator, test if the sub query does not match the + * item value + * + * @method NOT + * @param {Object} item The item to match + * @return {Boolean} true if one match, false otherwise + */ + ComplexQuery.prototype.NOT = function (item) { + return !this.query_list[0].match(item); + }; + + /** + * Creates Query object from a search text string or a serialized version + * of a Query. + * + * @method create + * @static + * @param {Object,String} object The search text or the serialized version + * of a Query + * @return {Query} A Query object + */ + QueryFactory.create = function (object, key_schema) { + if (object === "") { + return new Query(key_schema); + } + if (typeof object === "string") { + object = parseStringToObject(object); + } + if (typeof (object || {}).type === "string" && + query_class_dict[object.type]) { + return new query_class_dict[object.type](object, key_schema); + } + throw new TypeError("QueryFactory.create(): " + + "Argument 1 is not a search text or a parsable object"); + }; + + function objectToSearchText(query) { + var str_list = []; + if (query.type === "complex") { + str_list.push("("); + (query.query_list || []).forEach(function (sub_query) { + str_list.push(objectToSearchText(sub_query)); + str_list.push(query.operator); + }); + str_list.length -= 1; + str_list.push(")"); + return str_list.join(" "); + } + if (query.type === "simple") { + return (query.key ? query.key + ": " : "") + + (query.operator || "") + ' "' + query.value + '"'; + } + throw new TypeError("This object is not a query"); + } + + /** + * The SimpleQuery inherits from Query, and compares one metadata value + * + * @class SimpleQuery + * @extends Query + * @param {Object} [spec={}] The specifications + * @param {String} [spec.operator="="] The compare method to use + * @param {String} spec.key The metadata key + * @param {String} spec.value The value of the metadata to compare + */ + function SimpleQuery(spec, key_schema) { + Query.call(this, key_schema); + + /** + * Operator to use to compare object values + * + * @attribute operator + * @type String + * @optional + */ + this.operator = spec.operator; + + /** + * Key of the object which refers to the value to compare + * + * @attribute key + * @type String + */ + this.key = spec.key; + + /** + * Value is used to do the comparison with the object value + * + * @attribute value + * @type String + */ + this.value = spec.value; + + } + inherits(SimpleQuery, Query); + + SimpleQuery.prototype.type = "simple"; + + function checkKey(key) { + var prop; + + if (key.read_from === undefined) { + throw new TypeError("Custom key is missing the read_from property"); + } + + for (prop in key) { + if (key.hasOwnProperty(prop)) { + switch (prop) { + case 'read_from': + case 'cast_to': + case 'equal_match': + break; + default: + throw new TypeError("Custom key has unknown property '" + + prop + "'"); + } + } + } + } + + /** + * #crossLink "Query/match:method" + */ + SimpleQuery.prototype.match = function (item) { + var object_value = null, + equal_match = null, + cast_to = null, + matchMethod = null, + operator = this.operator, + value = null, + key = this.key, + k; + + if (!(regexp_comparaison.test(operator))) { + // `operator` is not correct, we have to change it to "like" or "=" + if (regexp_percent.test(this.value)) { + // `value` contains a non escaped `%` + operator = "like"; + } else { + // `value` does not contain non escaped `%` + operator = "="; + } + } + + matchMethod = this[operator]; + + if (this._key_schema.key_set && this._key_schema.key_set[key] !== undefined) { + key = this._key_schema.key_set[key]; + } + + // match with all the fields if key is empty + if (key === '') { + matchMethod = this.like; + value = '%' + this.value + '%'; + for (k in item) { + if (item.hasOwnProperty(k)) { + if (k !== '__id' && item[k]) { + if (matchMethod(item[k], value) === true) { + return true; + } + } + } + } + return false; + } + + if (typeof key === 'object') { + checkKey(key); + object_value = item[key.read_from]; + + equal_match = key.equal_match; + + // equal_match can be a string + if (typeof equal_match === 'string') { + // XXX raise error if equal_match not in match_lookup + equal_match = this._key_schema.match_lookup[equal_match]; + } + + // equal_match overrides the default '=' operator + if (equal_match !== undefined) { + matchMethod = (operator === "=" || operator === "like" ? + equal_match : matchMethod); + } + + value = this.value; + cast_to = key.cast_to; + if (cast_to) { + // cast_to can be a string + if (typeof cast_to === 'string') { + // XXX raise error if cast_to not in cast_lookup + cast_to = this._key_schema.cast_lookup[cast_to]; + } + + try { + value = cast_to(value); + } catch (e) { + value = undefined; + } + + try { + object_value = cast_to(object_value); + } catch (e) { + object_value = undefined; + } + } + } else { + object_value = item[key]; + value = this.value; + } + if (object_value === undefined || value === undefined) { + return false; + } + return matchMethod(object_value, value); + }; + + /** + * #crossLink "Query/toString:method" + */ + SimpleQuery.prototype.toString = function () { + return (this.key ? this.key + ":" : "") + + (this.operator ? " " + this.operator : "") + ' "' + this.value + '"'; + }; + + /** + * #crossLink "Query/serialized:method" + */ + SimpleQuery.prototype.serialized = function () { + var object = { + "type": "simple", + "key": this.key, + "value": this.value + }; + if (this.operator !== undefined) { + object.operator = this.operator; + } + return object; + }; + SimpleQuery.prototype.toJSON = SimpleQuery.prototype.serialized; + + /** + * Comparison operator, test if this query value matches the item value + * + * @method = + * @param {String} object_value The value to compare + * @param {String} comparison_value The comparison value + * @return {Boolean} true if match, false otherwise + */ + SimpleQuery.prototype["="] = function (object_value, comparison_value) { + var value, i; + if (!Array.isArray(object_value)) { + object_value = [object_value]; + } + for (i = 0; i < object_value.length; i += 1) { + value = object_value[i]; + if (typeof value === 'object' && value.hasOwnProperty('content')) { + value = value.content; + } + if (typeof value.cmp === "function") { + return (value.cmp(comparison_value) === 0); + } + if (comparison_value.toString() === value.toString()) { + return true; + } + } + return false; + }; + + /** + * Comparison operator, test if this query value matches the item value + * + * @method like + * @param {String} object_value The value to compare + * @param {String} comparison_value The comparison value + * @return {Boolean} true if match, false otherwise + */ + SimpleQuery.prototype.like = function (object_value, comparison_value) { + var value, i; + if (!Array.isArray(object_value)) { + object_value = [object_value]; + } + for (i = 0; i < object_value.length; i += 1) { + value = object_value[i]; + if (typeof value === 'object' && value.hasOwnProperty('content')) { + value = value.content; + } + if (typeof value.cmp === "function") { + return (value.cmp(comparison_value) === 0); + } + if ( + searchTextToRegExp(comparison_value.toString()).test(value.toString()) + ) { + return true; + } + } + return false; + }; + + /** + * Comparison operator, test if this query value does not match the item value + * + * @method != + * @param {String} object_value The value to compare + * @param {String} comparison_value The comparison value + * @return {Boolean} true if not match, false otherwise + */ + SimpleQuery.prototype["!="] = function (object_value, comparison_value) { + var value, i; + if (!Array.isArray(object_value)) { + object_value = [object_value]; + } + for (i = 0; i < object_value.length; i += 1) { + value = object_value[i]; + if (typeof value === 'object' && value.hasOwnProperty('content')) { + value = value.content; + } + if (typeof value.cmp === "function") { + return (value.cmp(comparison_value) !== 0); + } + if (comparison_value.toString() === value.toString()) { + return false; + } + } + return true; + }; + + /** + * Comparison operator, test if this query value is lower than the item value + * + * @method < + * @param {Number, String} object_value The value to compare + * @param {Number, String} comparison_value The comparison value + * @return {Boolean} true if lower, false otherwise + */ + SimpleQuery.prototype["<"] = function (object_value, comparison_value) { + var value; + if (!Array.isArray(object_value)) { + object_value = [object_value]; + } + value = object_value[0]; + if (typeof value === 'object' && value.hasOwnProperty('content')) { + value = value.content; + } + if (typeof value.cmp === "function") { + return (value.cmp(comparison_value) < 0); + } + return (value < comparison_value); + }; + + /** + * Comparison operator, test if this query value is equal or lower than the + * item value + * + * @method <= + * @param {Number, String} object_value The value to compare + * @param {Number, String} comparison_value The comparison value + * @return {Boolean} true if equal or lower, false otherwise + */ + SimpleQuery.prototype["<="] = function (object_value, comparison_value) { + var value; + if (!Array.isArray(object_value)) { + object_value = [object_value]; + } + value = object_value[0]; + if (typeof value === 'object' && value.hasOwnProperty('content')) { + value = value.content; + } + if (typeof value.cmp === "function") { + return (value.cmp(comparison_value) <= 0); + } + return (value <= comparison_value); + }; + + /** + * Comparison operator, test if this query value is greater than the item + * value + * + * @method > + * @param {Number, String} object_value The value to compare + * @param {Number, String} comparison_value The comparison value + * @return {Boolean} true if greater, false otherwise + */ + SimpleQuery.prototype[">"] = function (object_value, comparison_value) { + var value; + if (!Array.isArray(object_value)) { + object_value = [object_value]; + } + value = object_value[0]; + if (typeof value === 'object' && value.hasOwnProperty('content')) { + value = value.content; + } + if (typeof value.cmp === "function") { + return (value.cmp(comparison_value) > 0); + } + return (value > comparison_value); + }; + + /** + * Comparison operator, test if this query value is equal or greater than the + * item value + * + * @method >= + * @param {Number, String} object_value The value to compare + * @param {Number, String} comparison_value The comparison value + * @return {Boolean} true if equal or greater, false otherwise + */ + SimpleQuery.prototype[">="] = function (object_value, comparison_value) { + var value; + if (!Array.isArray(object_value)) { + object_value = [object_value]; + } + value = object_value[0]; + if (typeof value === 'object' && value.hasOwnProperty('content')) { + value = value.content; + } + if (typeof value.cmp === "function") { + return (value.cmp(comparison_value) >= 0); + } + return (value >= comparison_value); + }; + + query_class_dict.simple = SimpleQuery; + query_class_dict.complex = ComplexQuery; + + Query.parseStringToObject = parseStringToObject; + Query.objectToSearchText = objectToSearchText; + + window.Query = Query; + window.SimpleQuery = SimpleQuery; + window.ComplexQuery = ComplexQuery; + window.QueryFactory = QueryFactory; + +}(RSVP, window, parseStringToObject)); +;/*global window, moment */ +/*jslint nomen: true, maxlen: 200*/ +(function (window, moment) { + "use strict"; + +// /** +// * Add a secured (write permission denied) property to an object. +// * +// * @param {Object} object The object to fill +// * @param {String} key The object key where to store the property +// * @param {Any} value The value to store +// */ +// function _export(key, value) { +// Object.defineProperty(to_export, key, { +// "configurable": false, +// "enumerable": true, +// "writable": false, +// "value": value +// }); +// } + + var YEAR = 'year', + MONTH = 'month', + DAY = 'day', + HOUR = 'hour', + MIN = 'minute', + SEC = 'second', + MSEC = 'millisecond', + precision_grade = { + 'year': 0, + 'month': 1, + 'day': 2, + 'hour': 3, + 'minute': 4, + 'second': 5, + 'millisecond': 6 + }, + lesserPrecision = function (p1, p2) { + return (precision_grade[p1] < precision_grade[p2]) ? p1 : p2; + }, + JIODate; + + + JIODate = function (str) { + // in case of forgotten 'new' + if (!(this instanceof JIODate)) { + return new JIODate(str); + } + + if (str instanceof JIODate) { + this.mom = str.mom.clone(); + this._precision = str._precision; + return; + } + + if (str === undefined) { + this.mom = moment(); + this.setPrecision(MSEC); + return; + } + + this.mom = null; + this._str = str; + + // http://www.w3.org/TR/NOTE-datetime + // http://dotat.at/tmp/ISO_8601-2004_E.pdf + + // XXX these regexps fail to detect many invalid dates. + + if (str.match(/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+\-][0-2]\d:[0-5]\d|Z)/) + || str.match(/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d/)) { + // ISO, milliseconds + this.mom = moment(str); + this.setPrecision(MSEC); + } else if (str.match(/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+\-][0-2]\d:[0-5]\d|Z)/) + || str.match(/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/)) { + // ISO, seconds + this.mom = moment(str); + this.setPrecision(SEC); + } else if (str.match(/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+\-][0-2]\d:[0-5]\d|Z)/) + || str.match(/\d\d\d\d-\d\d-\d\d \d\d:\d\d/)) { + // ISO, minutes + this.mom = moment(str); + this.setPrecision(MIN); + } else if (str.match(/\d\d\d\d-\d\d-\d\d \d\d/)) { + this.mom = moment(str); + this.setPrecision(HOUR); + } else if (str.match(/\d\d\d\d-\d\d-\d\d/)) { + this.mom = moment(str); + this.setPrecision(DAY); + } else if (str.match(/\d\d\d\d-\d\d/)) { + this.mom = moment(str); + this.setPrecision(MONTH); + } else if (str.match(/\d\d\d\d/)) { + // Creating a moment with only the year will show this deprecation + // warning: + // + // Deprecation warning: moment construction falls back to js Date. This is + // discouraged and will be removed in upcoming major release. Please refer + // to https://github.com/moment/moment/issues/1407 for more info. + // + // TL;DR: parsing year-only strings with momentjs falls back to native + // Date and it won't correctly represent the year in local time if UTF + // offset is negative. + // + // The solution is to use the format parameter, so momentjs won't fall + // back to the native Date and we will have the correct year in local + // time. + // + this.mom = moment(str, 'YYYY'); + this.setPrecision(YEAR); + } + + if (!this.mom) { + throw new Error("Cannot parse: " + str); + } + + }; + + + JIODate.prototype.setPrecision = function (prec) { + this._precision = prec; + }; + + + JIODate.prototype.getPrecision = function () { + return this._precision; + }; + + + JIODate.prototype.cmp = function (other) { + var m1 = this.mom, + m2 = other.mom, + p = lesserPrecision(this._precision, other._precision); + return m1.isBefore(m2, p) ? -1 : (m1.isSame(m2, p) ? 0 : +1); + }; + + + JIODate.prototype.toPrecisionString = function (precision) { + var fmt; + + precision = precision || this._precision; + + fmt = { + 'millisecond': 'YYYY-MM-DD HH:mm:ss.SSS', + 'second': 'YYYY-MM-DD HH:mm:ss', + 'minute': 'YYYY-MM-DD HH:mm', + 'hour': 'YYYY-MM-DD HH', + 'day': 'YYYY-MM-DD', + 'month': 'YYYY-MM', + 'year': 'YYYY' + }[precision]; + + if (!fmt) { + throw new TypeError("Unsupported precision value '" + precision + "'"); + } + + return this.mom.format(fmt); + }; + + + JIODate.prototype.toString = function () { + return this._str; + }; + + +// _export('JIODate', JIODate); +// +// _export('YEAR', YEAR); +// _export('MONTH', MONTH); +// _export('DAY', DAY); +// _export('HOUR', HOUR); +// _export('MIN', MIN); +// _export('SEC', SEC); +// _export('MSEC', MSEC); + + window.jiodate = { + JIODate: JIODate, + YEAR: YEAR, + MONTH: MONTH, + DAY: DAY, + HOUR: HOUR, + MIN: MIN, + SEC: SEC, + MSEC: MSEC + }; +}(window, moment)); +;/*global window, RSVP, Blob, XMLHttpRequest, QueryFactory, Query, atob, + FileReader, ArrayBuffer, Uint8Array, navigator */ +(function (window, RSVP, Blob, QueryFactory, Query, atob, + FileReader, ArrayBuffer, Uint8Array, navigator) { + "use strict"; + + if (window.openDatabase === undefined) { + window.openDatabase = function () { + throw new Error('WebSQL is not supported by ' + navigator.userAgent); + }; + } + + /* Safari does not define DOMError */ + if (window.DOMError === undefined) { + window.DOMError = {}; + } + + var util = {}, + jIO; + + function jIOError(message, status_code) { + if ((message !== undefined) && (typeof message !== "string")) { + throw new TypeError('You must pass a string.'); + } + this.message = message || "Default Message"; + this.status_code = status_code || 500; + } + jIOError.prototype = new Error(); + jIOError.prototype.constructor = jIOError; + util.jIOError = jIOError; + + /** + * Send request with XHR and return a promise. xhr.onload: The promise is + * resolved when the status code is lower than 400 with the xhr object as + * first parameter. xhr.onerror: reject with xhr object as first + * parameter. xhr.onprogress: notifies the xhr object. + * + * @param {Object} param The parameters + * @param {String} [param.type="GET"] The request method + * @param {String} [param.dataType=""] The data type to retrieve + * @param {String} param.url The url + * @param {Any} [param.data] The data to send + * @param {Number} param.timeout The request timeout value + * @param {Function} [param.beforeSend] A function called just before the + * send request. The first parameter of this function is the XHR object. + * @return {Promise} The promise + */ + function ajax(param) { + var xhr = new XMLHttpRequest(); + return new RSVP.Promise(function (resolve, reject, notify) { + var k; + xhr.open(param.type || "GET", param.url, true); + xhr.responseType = param.dataType || ""; + if (typeof param.headers === 'object' && param.headers !== null) { + for (k in param.headers) { + if (param.headers.hasOwnProperty(k)) { + xhr.setRequestHeader(k, param.headers[k]); + } + } + } + xhr.addEventListener("load", function (e) { + if (e.target.status >= 400) { + return reject(e); + } + resolve(e); + }); + xhr.addEventListener("error", reject); + xhr.addEventListener("progress", notify); + if (typeof param.xhrFields === 'object' && param.xhrFields !== null) { + for (k in param.xhrFields) { + if (param.xhrFields.hasOwnProperty(k)) { + xhr[k] = param.xhrFields[k]; + } + } + } + if (param.timeout !== undefined && param.timeout !== 0) { + xhr.timeout = param.timeout; + xhr.ontimeout = function () { + return reject(new jIO.util.jIOError("Gateway Timeout", 504)); + }; + } + if (typeof param.beforeSend === 'function') { + param.beforeSend(xhr); + } + xhr.send(param.data); + }, function () { + xhr.abort(); + }); + } + util.ajax = ajax; + + function readBlobAsText(blob, encoding) { + var fr = new FileReader(); + return new RSVP.Promise(function (resolve, reject, notify) { + fr.addEventListener("load", resolve); + fr.addEventListener("error", reject); + fr.addEventListener("progress", notify); + fr.readAsText(blob, encoding); + }, function () { + fr.abort(); + }); + } + util.readBlobAsText = readBlobAsText; + + function readBlobAsArrayBuffer(blob) { + var fr = new FileReader(); + return new RSVP.Promise(function (resolve, reject, notify) { + fr.addEventListener("load", resolve); + fr.addEventListener("error", reject); + fr.addEventListener("progress", notify); + fr.readAsArrayBuffer(blob); + }, function () { + fr.abort(); + }); + } + util.readBlobAsArrayBuffer = readBlobAsArrayBuffer; + + function readBlobAsDataURL(blob) { + var fr = new FileReader(); + return new RSVP.Promise(function (resolve, reject, notify) { + fr.addEventListener("load", resolve); + fr.addEventListener("error", reject); + fr.addEventListener("progress", notify); + fr.readAsDataURL(blob); + }, function () { + fr.abort(); + }); + } + util.readBlobAsDataURL = readBlobAsDataURL; + + function stringify(obj) { + // Implement a stable JSON.stringify + // Object's keys are alphabetically ordered + var key, + key_list, + i, + value, + result_list; + if (obj === undefined) { + return undefined; + } + if (obj === null) { + return 'null'; + } + if (obj.constructor === Object) { + key_list = Object.keys(obj).sort(); + result_list = []; + for (i = 0; i < key_list.length; i += 1) { + key = key_list[i]; + value = stringify(obj[key]); + if (value !== undefined) { + result_list.push(stringify(key) + ':' + value); + } + } + return '{' + result_list.join(',') + '}'; + } + if (obj.constructor === Array) { + result_list = []; + for (i = 0; i < obj.length; i += 1) { + result_list.push(stringify(obj[i])); + } + return '[' + result_list.join(',') + ']'; + } + return JSON.stringify(obj); + } + util.stringify = stringify; + + + // https://gist.github.com/davoclavo/4424731 + function dataURItoBlob(dataURI) { + if (dataURI === 'data:') { + return new Blob(); + } + // convert base64 to raw binary data held in a string + var byteString = atob(dataURI.split(',')[1]), + // separate out the mime component + mimeString = dataURI.split(',')[0].split(':')[1], + // write the bytes of the string to an ArrayBuffer + arrayBuffer = new ArrayBuffer(byteString.length), + _ia = new Uint8Array(arrayBuffer), + i; + mimeString = mimeString.slice(0, mimeString.length - ";base64".length); + for (i = 0; i < byteString.length; i += 1) { + _ia[i] = byteString.charCodeAt(i); + } + return new Blob([arrayBuffer], {type: mimeString}); + } + + util.dataURItoBlob = dataURItoBlob; + + // tools + function checkId(argument_list, storage, method_name) { + if (typeof argument_list[0] !== 'string' || argument_list[0] === '') { + throw new jIO.util.jIOError( + "Document id must be a non empty string on '" + storage.__type + + "." + method_name + "'.", + 400 + ); + } + } + + function checkAttachmentId(argument_list, storage, method_name) { + if (typeof argument_list[1] !== 'string' || argument_list[1] === '') { + throw new jIO.util.jIOError( + "Attachment id must be a non empty string on '" + storage.__type + + "." + method_name + "'.", + 400 + ); + } + } + + function declareMethod(klass, name, precondition_function, post_function) { + klass.prototype[name] = function () { + var argument_list = arguments, + context = this, + precondition_result; + + return new RSVP.Queue() + .push(function () { + if (precondition_function !== undefined) { + return precondition_function.apply( + context.__storage, + [argument_list, context, name] + ); + } + }) + .push(function (result) { + var storage_method = context.__storage[name]; + precondition_result = result; + if (storage_method === undefined) { + throw new jIO.util.jIOError( + "Capacity '" + name + "' is not implemented on '" + + context.__type + "'", + 501 + ); + } + return storage_method.apply( + context.__storage, + argument_list + ); + }) + .push(function (result) { + if (post_function !== undefined) { + return post_function.call( + context, + argument_list, + result, + precondition_result + ); + } + return result; + }); + }; + // Allow chain + return this; + } + + + + + ///////////////////////////////////////////////////////////////// + // jIO Storage Proxy + ///////////////////////////////////////////////////////////////// + function JioProxyStorage(type, storage) { + if (!(this instanceof JioProxyStorage)) { + return new JioProxyStorage(); + } + this.__type = type; + this.__storage = storage; + } + + declareMethod(JioProxyStorage, "put", checkId, function (argument_list) { + return argument_list[0]; + }); + declareMethod(JioProxyStorage, "get", checkId); + declareMethod(JioProxyStorage, "bulk"); + declareMethod(JioProxyStorage, "remove", checkId, function (argument_list) { + return argument_list[0]; + }); + + JioProxyStorage.prototype.post = function () { + var context = this, + argument_list = arguments; + return new RSVP.Queue() + .push(function () { + var storage_method = context.__storage.post; + if (storage_method === undefined) { + throw new jIO.util.jIOError( + "Capacity 'post' is not implemented on '" + context.__type + "'", + 501 + ); + } + return context.__storage.post.apply(context.__storage, argument_list); + }); + }; + + declareMethod(JioProxyStorage, 'putAttachment', function (argument_list, + storage, + method_name) { + checkId(argument_list, storage, method_name); + checkAttachmentId(argument_list, storage, method_name); + + var options = argument_list[3] || {}; + + if (typeof argument_list[2] === 'string') { + argument_list[2] = new Blob([argument_list[2]], { + "type": options._content_type || options._mimetype || + "text/plain;charset=utf-8" + }); + } else if (!(argument_list[2] instanceof Blob)) { + throw new jIO.util.jIOError( + 'Attachment content is not a blob', + 400 + ); + } + }); + + declareMethod(JioProxyStorage, 'removeAttachment', function (argument_list, + storage, + method_name) { + checkId(argument_list, storage, method_name); + checkAttachmentId(argument_list, storage, method_name); + }); + + declareMethod(JioProxyStorage, 'getAttachment', function (argument_list, + storage, + method_name) { + var result = "blob"; +// if (param.storage_spec.type !== "indexeddb" && +// param.storage_spec.type !== "dav" && +// (param.kwargs._start !== undefined +// || param.kwargs._end !== undefined)) { +// restCommandRejecter(param, [ +// 'bad_request', +// 'unsupport', +// '_start, _end not support' +// ]); +// return false; +// } + checkId(argument_list, storage, method_name); + checkAttachmentId(argument_list, storage, method_name); + // Drop optional parameters, which are only used in postfunction + if (argument_list[2] !== undefined) { + result = argument_list[2].format || result; + delete argument_list[2].format; + } + return result; + }, function (argument_list, blob, convert) { + var result; + if (!(blob instanceof Blob)) { + throw new jIO.util.jIOError( + "'getAttachment' (" + argument_list[0] + " , " + + argument_list[1] + ") on '" + this.__type + + "' does not return a Blob.", + 501 + ); + } + if (convert === "blob") { + result = blob; + } else if (convert === "data_url") { + result = new RSVP.Queue() + .push(function () { + return jIO.util.readBlobAsDataURL(blob); + }) + .push(function (evt) { + return evt.target.result; + }); + } else if (convert === "array_buffer") { + result = new RSVP.Queue() + .push(function () { + return jIO.util.readBlobAsArrayBuffer(blob); + }) + .push(function (evt) { + return evt.target.result; + }); + } else if (convert === "text") { + result = new RSVP.Queue() + .push(function () { + return jIO.util.readBlobAsText(blob); + }) + .push(function (evt) { + return evt.target.result; + }); + } else if (convert === "json") { + result = new RSVP.Queue() + .push(function () { + return jIO.util.readBlobAsText(blob); + }) + .push(function (evt) { + return JSON.parse(evt.target.result); + }); + } else { + throw new jIO.util.jIOError( + this.__type + ".getAttachment format: '" + convert + + "' is not supported", + 400 + ); + } + return result; + }); + + JioProxyStorage.prototype.buildQuery = function () { + var storage_method = this.__storage.buildQuery, + context = this, + argument_list = arguments; + if (storage_method === undefined) { + throw new jIO.util.jIOError( + "Capacity 'buildQuery' is not implemented on '" + this.__type + "'", + 501 + ); + } + return new RSVP.Queue() + .push(function () { + return storage_method.apply( + context.__storage, + argument_list + ); + }); + }; + + JioProxyStorage.prototype.hasCapacity = function (name) { + var storage_method = this.__storage.hasCapacity, + capacity_method = this.__storage[name]; + if (capacity_method !== undefined) { + return true; + } + if ((storage_method === undefined) || + !storage_method.apply(this.__storage, arguments)) { + throw new jIO.util.jIOError( + "Capacity '" + name + "' is not implemented on '" + this.__type + "'", + 501 + ); + } + return true; + }; + + JioProxyStorage.prototype.allDocs = function (options) { + var context = this; + if (options === undefined) { + options = {}; + } + return new RSVP.Queue() + .push(function () { + if (context.hasCapacity("list") && + ((options.query === undefined) || context.hasCapacity("query")) && + ((options.sort_on === undefined) || context.hasCapacity("sort")) && + ((options.select_list === undefined) || + context.hasCapacity("select")) && + ((options.include_docs === undefined) || + context.hasCapacity("include")) && + ((options.limit === undefined) || context.hasCapacity("limit"))) { + return context.buildQuery(options); + } + }) + .push(function (result) { + return { + data: { + rows: result, + total_rows: result.length + } + }; + }); + }; + + declareMethod(JioProxyStorage, "allAttachments", checkId); + declareMethod(JioProxyStorage, "repair"); + + JioProxyStorage.prototype.repair = function () { + var context = this, + argument_list = arguments; + return new RSVP.Queue() + .push(function () { + var storage_method = context.__storage.repair; + if (storage_method !== undefined) { + return context.__storage.repair.apply(context.__storage, + argument_list); + } + }); + }; + + ///////////////////////////////////////////////////////////////// + // Storage builder + ///////////////////////////////////////////////////////////////// + function JioBuilder() { + if (!(this instanceof JioBuilder)) { + return new JioBuilder(); + } + this.__storage_types = {}; + } + + JioBuilder.prototype.createJIO = function (storage_spec, util) { + + if (typeof storage_spec.type !== 'string') { + throw new TypeError("Invalid storage description"); + } + if (!this.__storage_types[storage_spec.type]) { + throw new TypeError("Unknown storage '" + storage_spec.type + "'"); + } + + return new JioProxyStorage( + storage_spec.type, + new this.__storage_types[storage_spec.type](storage_spec, util) + ); + + }; + + JioBuilder.prototype.addStorage = function (type, Constructor) { + if (typeof type !== 'string') { + throw new TypeError( + "jIO.addStorage(): Argument 1 is not of type 'string'" + ); + } + if (typeof Constructor !== 'function') { + throw new TypeError("jIO.addStorage(): " + + "Argument 2 is not of type 'function'"); + } + if (this.__storage_types[type] !== undefined) { + throw new TypeError("jIO.addStorage(): Storage type already exists"); + } + this.__storage_types[type] = Constructor; + }; + + JioBuilder.prototype.util = util; + JioBuilder.prototype.QueryFactory = QueryFactory; + JioBuilder.prototype.Query = Query; + + ///////////////////////////////////////////////////////////////// + // global + ///////////////////////////////////////////////////////////////// + jIO = new JioBuilder(); + window.jIO = jIO; + +}(window, RSVP, Blob, QueryFactory, Query, atob, + FileReader, ArrayBuffer, Uint8Array, navigator)); +;/* + * Rusha, a JavaScript implementation of the Secure Hash Algorithm, SHA-1, + * as defined in FIPS PUB 180-1, tuned for high performance with large inputs. + * (http://github.com/srijs/rusha) + * + * Inspired by Paul Johnstons implementation (http://pajhome.org.uk/crypt/md5). + * + * Copyright (c) 2013 Sam Rijs (http://awesam.de). + * Released under the terms of the MIT license as follows: + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +(function () { + // If we'e running in Node.JS, export a module. + if (typeof module !== 'undefined') { + module.exports = Rusha; + } else if (typeof window !== 'undefined') { + window.Rusha = Rusha; + } + // If we're running in a webworker, accept + // messages containing a jobid and a buffer + // or blob object, and return the hash result. + if (typeof FileReaderSync !== 'undefined') { + var reader = new FileReaderSync(), hasher = new Rusha(4 * 1024 * 1024); + self.onmessage = function onMessage(event) { + var hash, data = event.data.data; + try { + hash = hasher.digest(data); + self.postMessage({ + id: event.data.id, + hash: hash + }); + } catch (e) { + self.postMessage({ + id: event.data.id, + error: e.name + }); + } + }; + } + var util = { + getDataType: function (data) { + if (typeof data === 'string') { + return 'string'; + } + if (data instanceof Array) { + return 'array'; + } + if (typeof global !== 'undefined' && global.Buffer && global.Buffer.isBuffer(data)) { + return 'buffer'; + } + if (data instanceof ArrayBuffer) { + return 'arraybuffer'; + } + if (data.buffer instanceof ArrayBuffer) { + return 'view'; + } + if (data instanceof Blob) { + return 'blob'; + } + throw new Error('Unsupported data type.'); + } + }; + // The Rusha object is a wrapper around the low-level RushaCore. + // It provides means of converting different inputs to the + // format accepted by RushaCore as well as other utility methods. + function Rusha(chunkSize) { + 'use strict'; + // Private object structure. + var self$2 = { fill: 0 }; + // Calculate the length of buffer that the sha1 routine uses + // including the padding. + var padlen = function (len) { + for (len += 9; len % 64 > 0; len += 1); + return len; + }; + var padZeroes = function (bin, len) { + for (var i = len >> 2; i < bin.length; i++) + bin[i] = 0; + }; + var padData = function (bin, chunkLen, msgLen) { + bin[chunkLen >> 2] |= 128 << 24 - (chunkLen % 4 << 3); + bin[((chunkLen >> 2) + 2 & ~15) + 14] = msgLen >> 29; + bin[((chunkLen >> 2) + 2 & ~15) + 15] = msgLen << 3; + }; + // Convert a binary string and write it to the heap. + // A binary string is expected to only contain char codes < 256. + var convStr = function (H8, H32, start, len, off) { + var str = this, i, om = off % 4, lm = len % 4, j = len - lm; + if (j > 0) { + switch (om) { + case 0: + H8[off + 3 | 0] = str.charCodeAt(start); + case 1: + H8[off + 2 | 0] = str.charCodeAt(start + 1); + case 2: + H8[off + 1 | 0] = str.charCodeAt(start + 2); + case 3: + H8[off | 0] = str.charCodeAt(start + 3); + } + } + for (i = om; i < j; i = i + 4 | 0) { + H32[off + i >> 2] = str.charCodeAt(start + i) << 24 | str.charCodeAt(start + i + 1) << 16 | str.charCodeAt(start + i + 2) << 8 | str.charCodeAt(start + i + 3); + } + switch (lm) { + case 3: + H8[off + j + 1 | 0] = str.charCodeAt(start + j + 2); + case 2: + H8[off + j + 2 | 0] = str.charCodeAt(start + j + 1); + case 1: + H8[off + j + 3 | 0] = str.charCodeAt(start + j); + } + }; + // Convert a buffer or array and write it to the heap. + // The buffer or array is expected to only contain elements < 256. + var convBuf = function (H8, H32, start, len, off) { + var buf = this, i, om = off % 4, lm = len % 4, j = len - lm; + if (j > 0) { + switch (om) { + case 0: + H8[off + 3 | 0] = buf[start]; + case 1: + H8[off + 2 | 0] = buf[start + 1]; + case 2: + H8[off + 1 | 0] = buf[start + 2]; + case 3: + H8[off | 0] = buf[start + 3]; + } + } + for (i = 4 - om; i < j; i = i += 4 | 0) { + H32[off + i >> 2] = buf[start + i] << 24 | buf[start + i + 1] << 16 | buf[start + i + 2] << 8 | buf[start + i + 3]; + } + switch (lm) { + case 3: + H8[off + j + 1 | 0] = buf[start + j + 2]; + case 2: + H8[off + j + 2 | 0] = buf[start + j + 1]; + case 1: + H8[off + j + 3 | 0] = buf[start + j]; + } + }; + var convBlob = function (H8, H32, start, len, off) { + var blob = this, i, om = off % 4, lm = len % 4, j = len - lm; + var buf = new Uint8Array(reader.readAsArrayBuffer(blob.slice(start, start + len))); + if (j > 0) { + switch (om) { + case 0: + H8[off + 3 | 0] = buf[0]; + case 1: + H8[off + 2 | 0] = buf[1]; + case 2: + H8[off + 1 | 0] = buf[2]; + case 3: + H8[off | 0] = buf[3]; + } + } + for (i = 4 - om; i < j; i = i += 4 | 0) { + H32[off + i >> 2] = buf[i] << 24 | buf[i + 1] << 16 | buf[i + 2] << 8 | buf[i + 3]; + } + switch (lm) { + case 3: + H8[off + j + 1 | 0] = buf[j + 2]; + case 2: + H8[off + j + 2 | 0] = buf[j + 1]; + case 1: + H8[off + j + 3 | 0] = buf[j]; + } + }; + var convFn = function (data) { + switch (util.getDataType(data)) { + case 'string': + return convStr.bind(data); + case 'array': + return convBuf.bind(data); + case 'buffer': + return convBuf.bind(data); + case 'arraybuffer': + return convBuf.bind(new Uint8Array(data)); + case 'view': + return convBuf.bind(new Uint8Array(data.buffer, data.byteOffset, data.byteLength)); + case 'blob': + return convBlob.bind(data); + } + }; + var slice = function (data, offset) { + switch (util.getDataType(data)) { + case 'string': + return data.slice(offset); + case 'array': + return data.slice(offset); + case 'buffer': + return data.slice(offset); + case 'arraybuffer': + return data.slice(offset); + case 'view': + return data.buffer.slice(offset); + } + }; + // Convert an ArrayBuffer into its hexadecimal string representation. + var hex = function (arrayBuffer) { + var i, x, hex_tab = '0123456789abcdef', res = [], binarray = new Uint8Array(arrayBuffer); + for (i = 0; i < binarray.length; i++) { + x = binarray[i]; + res[i] = hex_tab.charAt(x >> 4 & 15) + hex_tab.charAt(x >> 0 & 15); + } + return res.join(''); + }; + var ceilHeapSize = function (v) { + // The asm.js spec says: + // The heap object's byteLength must be either + // 2^n for n in [12, 24) or 2^24 * n for n ≥ 1. + // Also, byteLengths smaller than 2^16 are deprecated. + var p; + // If v is smaller than 2^16, the smallest possible solution + // is 2^16. + if (v <= 65536) + return 65536; + // If v < 2^24, we round up to 2^n, + // otherwise we round up to 2^24 * n. + if (v < 16777216) { + for (p = 1; p < v; p = p << 1); + } else { + for (p = 16777216; p < v; p += 16777216); + } + return p; + }; + // Initialize the internal data structures to a new capacity. + var init = function (size) { + if (size % 64 > 0) { + throw new Error('Chunk size must be a multiple of 128 bit'); + } + self$2.maxChunkLen = size; + self$2.padMaxChunkLen = padlen(size); + // The size of the heap is the sum of: + // 1. The padded input message size + // 2. The extended space the algorithm needs (320 byte) + // 3. The 160 bit state the algoritm uses + self$2.heap = new ArrayBuffer(ceilHeapSize(self$2.padMaxChunkLen + 320 + 20)); + self$2.h32 = new Int32Array(self$2.heap); + self$2.h8 = new Int8Array(self$2.heap); + self$2.core = RushaCore({ + Int32Array: Int32Array, + DataView: DataView + }, {}, self$2.heap); + self$2.buffer = null; + }; + // Iinitializethe datastructures according + // to a chunk siyze. + init(chunkSize || 64 * 1024); + var initState = function (heap, padMsgLen) { + var io = new Int32Array(heap, padMsgLen + 320, 5); + io[0] = 1732584193; + io[1] = -271733879; + io[2] = -1732584194; + io[3] = 271733878; + io[4] = -1009589776; + }; + var padChunk = function (chunkLen, msgLen) { + var padChunkLen = padlen(chunkLen); + var view = new Int32Array(self$2.heap, 0, padChunkLen >> 2); + padZeroes(view, chunkLen); + padData(view, chunkLen, msgLen); + return padChunkLen; + }; + // Write data to the heap. + var write = function (data, chunkOffset, chunkLen) { + convFn(data)(self$2.h8, self$2.h32, chunkOffset, chunkLen, 0); + }; + // Initialize and call the RushaCore, + // assuming an input buffer of length len * 4. + var coreCall = function (data, chunkOffset, chunkLen, msgLen, finalize) { + var padChunkLen = chunkLen; + if (finalize) { + padChunkLen = padChunk(chunkLen, msgLen); + } + write(data, chunkOffset, chunkLen); + self$2.core.hash(padChunkLen, self$2.padMaxChunkLen); + }; + var getRawDigest = function (heap, padMaxChunkLen) { + var io = new Int32Array(heap, padMaxChunkLen + 320, 5); + var out = new Int32Array(5); + var arr = new DataView(out.buffer); + arr.setInt32(0, io[0], false); + arr.setInt32(4, io[1], false); + arr.setInt32(8, io[2], false); + arr.setInt32(12, io[3], false); + arr.setInt32(16, io[4], false); + return out; + }; + // Calculate the hash digest as an array of 5 32bit integers. + var rawDigest = this.rawDigest = function (str) { + var msgLen = str.byteLength || str.length || str.size || 0; + initState(self$2.heap, self$2.padMaxChunkLen); + var chunkOffset = 0, chunkLen = self$2.maxChunkLen, last; + for (chunkOffset = 0; msgLen > chunkOffset + chunkLen; chunkOffset += chunkLen) { + coreCall(str, chunkOffset, chunkLen, msgLen, false); + } + coreCall(str, chunkOffset, msgLen - chunkOffset, msgLen, true); + return getRawDigest(self$2.heap, self$2.padMaxChunkLen); + }; + // The digest and digestFrom* interface returns the hash digest + // as a hex string. + this.digest = this.digestFromString = this.digestFromBuffer = this.digestFromArrayBuffer = function (str) { + return hex(rawDigest(str).buffer); + }; + } + ; + // The low-level RushCore module provides the heart of Rusha, + // a high-speed sha1 implementation working on an Int32Array heap. + // At first glance, the implementation seems complicated, however + // with the SHA1 spec at hand, it is obvious this almost a textbook + // implementation that has a few functions hand-inlined and a few loops + // hand-unrolled. + function RushaCore(stdlib, foreign, heap) { + 'use asm'; + var H = new stdlib.Int32Array(heap); + function hash(k, x) { + // k in bytes + k = k | 0; + x = x | 0; + var i = 0, j = 0, y0 = 0, z0 = 0, y1 = 0, z1 = 0, y2 = 0, z2 = 0, y3 = 0, z3 = 0, y4 = 0, z4 = 0, t0 = 0, t1 = 0; + y0 = H[x + 320 >> 2] | 0; + y1 = H[x + 324 >> 2] | 0; + y2 = H[x + 328 >> 2] | 0; + y3 = H[x + 332 >> 2] | 0; + y4 = H[x + 336 >> 2] | 0; + for (i = 0; (i | 0) < (k | 0); i = i + 64 | 0) { + z0 = y0; + z1 = y1; + z2 = y2; + z3 = y3; + z4 = y4; + for (j = 0; (j | 0) < 64; j = j + 4 | 0) { + t1 = H[i + j >> 2] | 0; + t0 = ((y0 << 5 | y0 >>> 27) + (y1 & y2 | ~y1 & y3) | 0) + ((t1 + y4 | 0) + 1518500249 | 0) | 0; + y4 = y3; + y3 = y2; + y2 = y1 << 30 | y1 >>> 2; + y1 = y0; + y0 = t0; + ; + H[k + j >> 2] = t1; + } + for (j = k + 64 | 0; (j | 0) < (k + 80 | 0); j = j + 4 | 0) { + t1 = (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) << 1 | (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) >>> 31; + t0 = ((y0 << 5 | y0 >>> 27) + (y1 & y2 | ~y1 & y3) | 0) + ((t1 + y4 | 0) + 1518500249 | 0) | 0; + y4 = y3; + y3 = y2; + y2 = y1 << 30 | y1 >>> 2; + y1 = y0; + y0 = t0; + ; + H[j >> 2] = t1; + } + for (j = k + 80 | 0; (j | 0) < (k + 160 | 0); j = j + 4 | 0) { + t1 = (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) << 1 | (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) >>> 31; + t0 = ((y0 << 5 | y0 >>> 27) + (y1 ^ y2 ^ y3) | 0) + ((t1 + y4 | 0) + 1859775393 | 0) | 0; + y4 = y3; + y3 = y2; + y2 = y1 << 30 | y1 >>> 2; + y1 = y0; + y0 = t0; + ; + H[j >> 2] = t1; + } + for (j = k + 160 | 0; (j | 0) < (k + 240 | 0); j = j + 4 | 0) { + t1 = (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) << 1 | (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) >>> 31; + t0 = ((y0 << 5 | y0 >>> 27) + (y1 & y2 | y1 & y3 | y2 & y3) | 0) + ((t1 + y4 | 0) - 1894007588 | 0) | 0; + y4 = y3; + y3 = y2; + y2 = y1 << 30 | y1 >>> 2; + y1 = y0; + y0 = t0; + ; + H[j >> 2] = t1; + } + for (j = k + 240 | 0; (j | 0) < (k + 320 | 0); j = j + 4 | 0) { + t1 = (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) << 1 | (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) >>> 31; + t0 = ((y0 << 5 | y0 >>> 27) + (y1 ^ y2 ^ y3) | 0) + ((t1 + y4 | 0) - 899497514 | 0) | 0; + y4 = y3; + y3 = y2; + y2 = y1 << 30 | y1 >>> 2; + y1 = y0; + y0 = t0; + ; + H[j >> 2] = t1; + } + y0 = y0 + z0 | 0; + y1 = y1 + z1 | 0; + y2 = y2 + z2 | 0; + y3 = y3 + z3 | 0; + y4 = y4 + z4 | 0; + } + H[x + 320 >> 2] = y0; + H[x + 324 >> 2] = y1; + H[x + 328 >> 2] = y2; + H[x + 332 >> 2] = y3; + H[x + 336 >> 2] = y4; + } + return { hash: hash }; + } +}());;/* + * JIO extension for resource replication. + * Copyright (C) 2013, 2015 Nexedi SA + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/*jslint nomen: true*/ +/*global jIO, RSVP, Rusha*/ + +(function (jIO, RSVP, Rusha, stringify) { + "use strict"; + + var rusha = new Rusha(), + CONFLICT_THROW = 0, + CONFLICT_KEEP_LOCAL = 1, + CONFLICT_KEEP_REMOTE = 2, + CONFLICT_CONTINUE = 3; + + function SkipError(message) { + if ((message !== undefined) && (typeof message !== "string")) { + throw new TypeError('You must pass a string.'); + } + this.message = message || "Skip some asynchronous code"; + } + SkipError.prototype = new Error(); + SkipError.prototype.constructor = SkipError; + + /**************************************************** + Use a local jIO to read/write/search documents + Synchronize in background those document with a remote jIO. + Synchronization status is stored for each document as an local attachment. + ****************************************************/ + + function generateHash(content) { + // XXX Improve performance by moving calculation to WebWorker + return rusha.digestFromString(content); + } + + function generateHashFromArrayBuffer(content) { + // XXX Improve performance by moving calculation to WebWorker + return rusha.digestFromArrayBuffer(content); + } + + function ReplicateStorage(spec) { + this._query_options = spec.query || {}; + if (spec.signature_hash_key !== undefined) { + this._query_options.select_list = [spec.signature_hash_key]; + } + this._signature_hash_key = spec.signature_hash_key; + + this._local_sub_storage = jIO.createJIO(spec.local_sub_storage); + this._remote_sub_storage = jIO.createJIO(spec.remote_sub_storage); + + if (spec.hasOwnProperty('signature_sub_storage')) { + this._signature_sub_storage = jIO.createJIO(spec.signature_sub_storage); + this._custom_signature_sub_storage = true; + } else { + this._signature_hash = "_replicate_" + generateHash( + stringify(spec.local_sub_storage) + + stringify(spec.remote_sub_storage) + + stringify(this._query_options) + ); + this._signature_sub_storage = jIO.createJIO({ + type: "query", + sub_storage: { + type: "document", + document_id: this._signature_hash, + sub_storage: spec.local_sub_storage + } + }); + this._custom_signature_sub_storage = false; + } + + this._use_remote_post = spec.use_remote_post || false; + // Number of request we allow browser execution for attachments + this._parallel_operation_attachment_amount = + spec.parallel_operation_attachment_amount || 1; + // Number of request we allow browser execution for documents + this._parallel_operation_amount = + spec.parallel_operation_amount || 1; + + this._conflict_handling = spec.conflict_handling || 0; + // 0: no resolution (ie, throw an Error) + // 1: keep the local state + // (overwrites the remote document with local content) + // (delete remote document if local is deleted) + // 2: keep the remote state + // (overwrites the local document with remote content) + // (delete local document if remote is deleted) + // 3: keep both copies (leave documents untouched, no signature update) + if ((this._conflict_handling !== CONFLICT_THROW) && + (this._conflict_handling !== CONFLICT_KEEP_LOCAL) && + (this._conflict_handling !== CONFLICT_KEEP_REMOTE) && + (this._conflict_handling !== CONFLICT_CONTINUE)) { + throw new jIO.util.jIOError("Unsupported conflict handling: " + + this._conflict_handling, 400); + } + + this._check_local_modification = spec.check_local_modification; + if (this._check_local_modification === undefined) { + this._check_local_modification = true; + } + this._check_local_creation = spec.check_local_creation; + if (this._check_local_creation === undefined) { + this._check_local_creation = true; + } + this._check_local_deletion = spec.check_local_deletion; + if (this._check_local_deletion === undefined) { + this._check_local_deletion = true; + } + this._check_remote_modification = spec.check_remote_modification; + if (this._check_remote_modification === undefined) { + this._check_remote_modification = true; + } + this._check_remote_creation = spec.check_remote_creation; + if (this._check_remote_creation === undefined) { + this._check_remote_creation = true; + } + this._check_remote_deletion = spec.check_remote_deletion; + if (this._check_remote_deletion === undefined) { + this._check_remote_deletion = true; + } + this._check_local_attachment_modification = + spec.check_local_attachment_modification; + if (this._check_local_attachment_modification === undefined) { + this._check_local_attachment_modification = false; + } + this._check_local_attachment_creation = + spec.check_local_attachment_creation; + if (this._check_local_attachment_creation === undefined) { + this._check_local_attachment_creation = false; + } + this._check_local_attachment_deletion = + spec.check_local_attachment_deletion; + if (this._check_local_attachment_deletion === undefined) { + this._check_local_attachment_deletion = false; + } + this._check_remote_attachment_modification = + spec.check_remote_attachment_modification; + if (this._check_remote_attachment_modification === undefined) { + this._check_remote_attachment_modification = false; + } + this._check_remote_attachment_creation = + spec.check_remote_attachment_creation; + if (this._check_remote_attachment_creation === undefined) { + this._check_remote_attachment_creation = false; + } + this._check_remote_attachment_deletion = + spec.check_remote_attachment_deletion; + if (this._check_remote_attachment_deletion === undefined) { + this._check_remote_attachment_deletion = false; + } + } + + ReplicateStorage.prototype.remove = function (id) { + if (id === this._signature_hash) { + throw new jIO.util.jIOError(this._signature_hash + " is frozen", + 403); + } + return this._local_sub_storage.remove.apply(this._local_sub_storage, + arguments); + }; + ReplicateStorage.prototype.post = function () { + return this._local_sub_storage.post.apply(this._local_sub_storage, + arguments); + }; + ReplicateStorage.prototype.put = function (id) { + if (id === this._signature_hash) { + throw new jIO.util.jIOError(this._signature_hash + " is frozen", + 403); + } + return this._local_sub_storage.put.apply(this._local_sub_storage, + arguments); + }; + ReplicateStorage.prototype.get = function () { + return this._local_sub_storage.get.apply(this._local_sub_storage, + arguments); + }; + ReplicateStorage.prototype.getAttachment = function () { + return this._local_sub_storage.getAttachment.apply(this._local_sub_storage, + arguments); + }; + ReplicateStorage.prototype.allAttachments = function () { + return this._local_sub_storage.allAttachments.apply(this._local_sub_storage, + arguments); + }; + ReplicateStorage.prototype.putAttachment = function (id) { + if (id === this._signature_hash) { + throw new jIO.util.jIOError(this._signature_hash + " is frozen", + 403); + } + return this._local_sub_storage.putAttachment.apply(this._local_sub_storage, + arguments); + }; + ReplicateStorage.prototype.removeAttachment = function (id) { + if (id === this._signature_hash) { + throw new jIO.util.jIOError(this._signature_hash + " is frozen", + 403); + } + return this._local_sub_storage.removeAttachment.apply( + this._local_sub_storage, + arguments + ); + }; + ReplicateStorage.prototype.hasCapacity = function () { + return this._local_sub_storage.hasCapacity.apply(this._local_sub_storage, + arguments); + }; + ReplicateStorage.prototype.buildQuery = function () { + // XXX Remove signature document? + return this._local_sub_storage.buildQuery.apply(this._local_sub_storage, + arguments); + }; + + function dispatchQueue(context, function_used, argument_list, + number_queue) { + var result_promise_list = [], + i; + + function pushAndExecute(queue) { + queue + .push(function () { + if (argument_list.length > 0) { + var argument_array = argument_list.shift(), + sub_queue = new RSVP.Queue(); + argument_array[0] = sub_queue; + function_used.apply(context, argument_array); + pushAndExecute(queue); + return sub_queue; + } + }); + } + for (i = 0; i < number_queue; i += 1) { + result_promise_list.push(new RSVP.Queue()); + pushAndExecute(result_promise_list[i]); + } + if (number_queue > 1) { + return RSVP.all(result_promise_list); + } + return result_promise_list[0]; + } + + function callAllDocsOnStorage(context, storage, cache, cache_key) { + return new RSVP.Queue() + .push(function () { + if (!cache.hasOwnProperty(cache_key)) { + return storage.allDocs(context._query_options) + .push(function (result) { + var i, + cache_entry = {}; + for (i = 0; i < result.data.total_rows; i += 1) { + cache_entry[result.data.rows[i].id] = result.data.rows[i].value; + } + cache[cache_key] = cache_entry; + }); + } + }) + .push(function () { + return cache[cache_key]; + }); + } + + function propagateAttachmentDeletion(context, skip_attachment_dict, + destination, + id, name) { + return destination.removeAttachment(id, name) + .push(function () { + return context._signature_sub_storage.removeAttachment(id, name); + }) + .push(function () { + skip_attachment_dict[name] = null; + }); + } + + function propagateAttachmentModification(context, skip_attachment_dict, + destination, + blob, hash, id, name) { + return destination.putAttachment(id, name, blob) + .push(function () { + return context._signature_sub_storage.putAttachment(id, name, + JSON.stringify({ + hash: hash + })); + }) + .push(function () { + skip_attachment_dict[name] = null; + }); + } + + function checkAndPropagateAttachment(context, + skip_attachment_dict, + status_hash, local_hash, blob, + source, destination, id, name, + conflict_force, conflict_revert, + conflict_ignore) { + var remote_blob; + return destination.getAttachment(id, name) + .push(function (result) { + remote_blob = result; + return jIO.util.readBlobAsArrayBuffer(remote_blob); + }) + .push(function (evt) { + return generateHashFromArrayBuffer( + evt.target.result + ); + }, function (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + remote_blob = null; + return null; + } + throw error; + }) + .push(function (remote_hash) { + if (local_hash === remote_hash) { + // Same modifications on both side + if (local_hash === null) { + // Deleted on both side, drop signature + return context._signature_sub_storage.removeAttachment(id, name) + .push(function () { + skip_attachment_dict[name] = null; + }); + } + + return context._signature_sub_storage.putAttachment(id, name, + JSON.stringify({ + hash: local_hash + })) + .push(function () { + skip_attachment_dict[name] = null; + }); + } + + if ((remote_hash === status_hash) || (conflict_force === true)) { + // Modified only locally. No conflict or force + if (local_hash === null) { + // Deleted locally + return propagateAttachmentDeletion(context, skip_attachment_dict, + destination, + id, name); + } + return propagateAttachmentModification(context, + skip_attachment_dict, + destination, blob, + local_hash, id, name); + } + + // Conflict cases + if (conflict_ignore === true) { + return; + } + + if ((conflict_revert === true) || (local_hash === null)) { + // Automatically resolve conflict or force revert + if (remote_hash === null) { + // Deleted remotely + return propagateAttachmentDeletion(context, skip_attachment_dict, + source, id, name); + } + return propagateAttachmentModification( + context, + skip_attachment_dict, + source, + remote_blob, + remote_hash, + id, + name + ); + } + + // Minimize conflict if it can be resolved + if (remote_hash === null) { + // Copy remote modification remotely + return propagateAttachmentModification(context, + skip_attachment_dict, + destination, blob, + local_hash, id, name); + } + throw new jIO.util.jIOError("Conflict on '" + id + + "' with attachment '" + + name + "'", + 409); + }); + } + + function checkAttachmentSignatureDifference(queue, context, + skip_attachment_dict, + source, + destination, id, name, + conflict_force, + conflict_revert, + conflict_ignore, + is_creation, is_modification) { + var blob, + status_hash; + queue + .push(function () { + // Optimisation to save a get call to signature storage + if (is_creation === true) { + return RSVP.all([ + source.getAttachment(id, name), + {hash: null} + ]); + } + if (is_modification === true) { + return RSVP.all([ + source.getAttachment(id, name), + context._signature_sub_storage.getAttachment( + id, + name, + {format: 'json'} + ) + ]); + } + throw new jIO.util.jIOError("Unexpected call of" + + " checkAttachmentSignatureDifference", + 409); + }) + .push(function (result_list) { + blob = result_list[0]; + status_hash = result_list[1].hash; + return jIO.util.readBlobAsArrayBuffer(blob); + }) + .push(function (evt) { + var array_buffer = evt.target.result, + local_hash = generateHashFromArrayBuffer(array_buffer); + + if (local_hash !== status_hash) { + return checkAndPropagateAttachment(context, + skip_attachment_dict, + status_hash, local_hash, blob, + source, destination, id, name, + conflict_force, conflict_revert, + conflict_ignore); + } + }); + } + + function checkAttachmentLocalDeletion(queue, context, + skip_attachment_dict, + destination, id, name, source, + conflict_force, conflict_revert, + conflict_ignore) { + var status_hash; + queue + .push(function () { + return context._signature_sub_storage.getAttachment(id, name, + {format: 'json'}); + }) + .push(function (result) { + status_hash = result.hash; + return checkAndPropagateAttachment(context, + skip_attachment_dict, + status_hash, null, null, + source, destination, id, name, + conflict_force, conflict_revert, + conflict_ignore); + }); + } + + function pushDocumentAttachment(context, + skip_attachment_dict, id, source, + destination, signature_allAttachments, + options) { + var local_dict = {}, + signature_dict = {}; + return source.allAttachments(id) + .push(undefined, function (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + return {}; + } + throw error; + }) + .push(function (source_allAttachments) { + var is_modification, + is_creation, + key, + argument_list = []; + for (key in source_allAttachments) { + if (source_allAttachments.hasOwnProperty(key)) { + if (!skip_attachment_dict.hasOwnProperty(key)) { + local_dict[key] = null; + } + } + } + for (key in signature_allAttachments) { + if (signature_allAttachments.hasOwnProperty(key)) { + if (!skip_attachment_dict.hasOwnProperty(key)) { + signature_dict[key] = null; + } + } + } + + for (key in local_dict) { + if (local_dict.hasOwnProperty(key)) { + is_modification = signature_dict.hasOwnProperty(key) + && options.check_modification; + is_creation = !signature_dict.hasOwnProperty(key) + && options.check_creation; + if (is_modification === true || is_creation === true) { + argument_list.push([undefined, + context, + skip_attachment_dict, + source, + destination, id, key, + options.conflict_force, + options.conflict_revert, + options.conflict_ignore, + is_creation, + is_modification]); + } + } + } + return dispatchQueue( + context, + checkAttachmentSignatureDifference, + argument_list, + context._parallel_operation_attachment_amount + ); + }) + .push(function () { + var key, argument_list = []; + if (options.check_deletion === true) { + for (key in signature_dict) { + if (signature_dict.hasOwnProperty(key)) { + if (!local_dict.hasOwnProperty(key)) { + argument_list.push([undefined, + context, + skip_attachment_dict, + destination, id, key, + source, + options.conflict_force, + options.conflict_revert, + options.conflict_ignore]); + } + } + } + return dispatchQueue( + context, + checkAttachmentLocalDeletion, + argument_list, + context._parallel_operation_attachment_amount + ); + } + }); + } + + function propagateFastAttachmentDeletion(queue, id, name, storage) { + return queue + .push(function () { + return storage.removeAttachment(id, name); + }); + } + + function propagateFastAttachmentModification(queue, id, key, source, + destination, signature, hash) { + return queue + .push(function () { + return signature.getAttachment(id, key, {format: 'json'}) + .push(undefined, function (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + return {hash: null}; + } + throw error; + }) + .push(function (result) { + if (result.hash !== hash) { + return source.getAttachment(id, key) + .push(function (blob) { + return destination.putAttachment(id, key, blob); + }) + .push(function () { + return signature.putAttachment(id, key, JSON.stringify({ + hash: hash + })); + }); + } + }); + + }); + } + + function repairFastDocumentAttachment(context, id, + signature_hash, + signature_attachment_hash, + signature_from_local) { + if (signature_hash === signature_attachment_hash) { + // No replication to do + return; + } + return new RSVP.Queue() + .push(function () { + return RSVP.all([ + context._signature_sub_storage.allAttachments(id), + context._local_sub_storage.allAttachments(id), + context._remote_sub_storage.allAttachments(id) + ]); + }) + .push(function (result_list) { + var key, + source_attachment_dict, + destination_attachment_dict, + source, + destination, + push_argument_list = [], + delete_argument_list = [], + signature_attachment_dict = result_list[0], + local_attachment_dict = result_list[1], + remote_attachment_list = result_list[2], + check_local_modification = + context._check_local_attachment_modification, + check_local_creation = context._check_local_attachment_creation, + check_local_deletion = context._check_local_attachment_deletion, + check_remote_modification = + context._check_remote_attachment_modification, + check_remote_creation = context._check_remote_attachment_creation, + check_remote_deletion = context._check_remote_attachment_deletion; + + if (signature_from_local) { + source_attachment_dict = local_attachment_dict; + destination_attachment_dict = remote_attachment_list; + source = context._local_sub_storage; + destination = context._remote_sub_storage; + } else { + source_attachment_dict = remote_attachment_list; + destination_attachment_dict = local_attachment_dict; + source = context._remote_sub_storage; + destination = context._local_sub_storage; + check_local_modification = check_remote_modification; + check_local_creation = check_remote_creation; + check_local_deletion = check_remote_deletion; + check_remote_creation = check_local_creation; + check_remote_deletion = check_local_deletion; + } + + // Push all source attachments + for (key in source_attachment_dict) { + if (source_attachment_dict.hasOwnProperty(key)) { + + if ((check_local_creation && + !signature_attachment_dict.hasOwnProperty(key)) || + (check_local_modification && + signature_attachment_dict.hasOwnProperty(key))) { + push_argument_list.push([ + undefined, + id, + key, + source, + destination, + context._signature_sub_storage, + signature_hash + ]); + } + } + } + + // Delete remaining signature + remote attachments + for (key in signature_attachment_dict) { + if (signature_attachment_dict.hasOwnProperty(key)) { + if (check_local_deletion && + !source_attachment_dict.hasOwnProperty(key)) { + delete_argument_list.push([ + undefined, + id, + key, + context._signature_sub_storage + ]); + } + } + } + for (key in destination_attachment_dict) { + if (destination_attachment_dict.hasOwnProperty(key)) { + if (!source_attachment_dict.hasOwnProperty(key)) { + if ((check_local_deletion && + signature_attachment_dict.hasOwnProperty(key)) || + (check_remote_creation && + !signature_attachment_dict.hasOwnProperty(key))) { + delete_argument_list.push([ + undefined, + id, + key, + destination + ]); + } + } + } + } + + return RSVP.all([ + dispatchQueue( + context, + propagateFastAttachmentModification, + push_argument_list, + context._parallel_operation_attachment_amount + ), + dispatchQueue( + context, + propagateFastAttachmentDeletion, + delete_argument_list, + context._parallel_operation_attachment_amount + ) + ]); + }) + .push(function () { + // Mark that all attachments have been synchronized + return context._signature_sub_storage.put(id, { + hash: signature_hash, + attachment_hash: signature_hash, + from_local: signature_from_local + }); + }); + } + + function repairDocumentAttachment(context, id, signature_hash_key, + signature_hash, + signature_attachment_hash, + signature_from_local) { + if (signature_hash_key !== undefined) { + return repairFastDocumentAttachment(context, id, + signature_hash, + signature_attachment_hash, + signature_from_local); + } + + var skip_attachment_dict = {}; + return new RSVP.Queue() + .push(function () { + if (context._check_local_attachment_modification || + context._check_local_attachment_creation || + context._check_local_attachment_deletion || + context._check_remote_attachment_modification || + context._check_remote_attachment_creation || + context._check_remote_attachment_deletion) { + return context._signature_sub_storage.allAttachments(id); + } + return {}; + }) + .push(undefined, function (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + return {}; + } + throw error; + }) + .push(function (signature_allAttachments) { + if (context._check_local_attachment_modification || + context._check_local_attachment_creation || + context._check_local_attachment_deletion) { + return pushDocumentAttachment( + context, + skip_attachment_dict, + id, + context._local_sub_storage, + context._remote_sub_storage, + signature_allAttachments, + { + conflict_force: (context._conflict_handling === + CONFLICT_KEEP_LOCAL), + conflict_revert: (context._conflict_handling === + CONFLICT_KEEP_REMOTE), + conflict_ignore: (context._conflict_handling === + CONFLICT_CONTINUE), + check_modification: + context._check_local_attachment_modification, + check_creation: context._check_local_attachment_creation, + check_deletion: context._check_local_attachment_deletion + } + ) + .push(function () { + return signature_allAttachments; + }); + } + return signature_allAttachments; + }) + .push(function (signature_allAttachments) { + if (context._check_remote_attachment_modification || + context._check_remote_attachment_creation || + context._check_remote_attachment_deletion) { + return pushDocumentAttachment( + context, + skip_attachment_dict, + id, + context._remote_sub_storage, + context._local_sub_storage, + signature_allAttachments, + { + use_revert_post: context._use_remote_post, + conflict_force: (context._conflict_handling === + CONFLICT_KEEP_REMOTE), + conflict_revert: (context._conflict_handling === + CONFLICT_KEEP_LOCAL), + conflict_ignore: (context._conflict_handling === + CONFLICT_CONTINUE), + check_modification: + context._check_remote_attachment_modification, + check_creation: context._check_remote_attachment_creation, + check_deletion: context._check_remote_attachment_deletion + } + ); + } + }); + } + + function propagateModification(context, source, destination, doc, hash, id, + skip_document_dict, + skip_deleted_document_dict, + options) { + var result = new RSVP.Queue(), + post_id, + to_skip = true, + from_local; + if (options === undefined) { + options = {}; + } + from_local = options.from_local; + + if (doc === null) { + result + .push(function () { + return source.get(id); + }) + .push(function (source_doc) { + doc = source_doc; + }, function (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + throw new SkipError(id); + } + throw error; + }); + } + if (options.use_post) { + result + .push(function () { + return destination.post(doc); + }) + .push(function (new_id) { + to_skip = false; + post_id = new_id; + return source.put(post_id, doc); + }) + .push(function () { + // Copy all attachments + // This is not related to attachment replication + // It's just about not losing user data + return source.allAttachments(id); + }) + .push(function (attachment_dict) { + var key, + copy_queue = new RSVP.Queue(); + + function copyAttachment(name) { + copy_queue + .push(function () { + return source.getAttachment(id, name); + }) + .push(function (blob) { + return source.putAttachment(post_id, name, blob); + }); + } + + for (key in attachment_dict) { + if (attachment_dict.hasOwnProperty(key)) { + copyAttachment(key); + } + } + return copy_queue; + }) + .push(function () { + return source.remove(id); + }) + .push(function () { + return context._signature_sub_storage.remove(id); + }) + .push(function () { + to_skip = true; + return context._signature_sub_storage.put(post_id, { + hash: hash, + from_local: from_local + }); + }) + .push(function () { + skip_document_dict[post_id] = null; + }); + } else { + result + .push(function () { + // Drop signature if the destination document was empty + // but a signature exists + if (options.create_new_document === true) { + delete skip_deleted_document_dict[id]; + return context._signature_sub_storage.remove(id); + } + }) + .push(function () { + return destination.put(id, doc); + }) + .push(function () { + return context._signature_sub_storage.put(id, { + hash: hash, + from_local: from_local + }); + }); + } + return result + .push(function () { + if (to_skip) { + skip_document_dict[id] = null; + } + }) + .push(undefined, function (error) { + if (error instanceof SkipError) { + return; + } + throw error; + }); + } + + function propagateDeletion(context, destination, id, skip_document_dict, + skip_deleted_document_dict) { + // Do not delete a document if it has an attachment + // ie, replication should prevent losing user data + // Synchronize attachments before, to ensure + // all of them will be deleted too + var result; + if (context._signature_hash_key !== undefined) { + result = destination.remove(id) + .push(function () { + return context._signature_sub_storage.remove(id); + }); + } else { + result = repairDocumentAttachment(context, id) + .push(function () { + return destination.allAttachments(id); + }) + .push(function (attachment_dict) { + if (JSON.stringify(attachment_dict) === "{}") { + return destination.remove(id) + .push(function () { + return context._signature_sub_storage.remove(id); + }); + } + }, function (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + return; + } + throw error; + }); + } + return result + .push(function () { + skip_document_dict[id] = null; + // No need to sync attachment twice on this document + skip_deleted_document_dict[id] = null; + }); + } + + function checkAndPropagate(context, skip_document_dict, + skip_deleted_document_dict, + cache, destination_key, + status_hash, local_hash, doc, + source, destination, id, + conflict_force, conflict_revert, + conflict_ignore, + options) { + var from_local = options.from_local; + return new RSVP.Queue() + .push(function () { + if (options.signature_hash_key !== undefined) { + return callAllDocsOnStorage(context, destination, + cache, destination_key) + .push(function (result) { + if (result.hasOwnProperty(id)) { + return [null, result[id][options.signature_hash_key]]; + } + return [null, null]; + }); + } + return destination.get(id) + .push(function (remote_doc) { + return [remote_doc, generateHash(stringify(remote_doc))]; + }, function (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + return [null, null]; + } + throw error; + }); + }) + + .push(function (remote_list) { + var remote_doc = remote_list[0], + remote_hash = remote_list[1]; + if (local_hash === remote_hash) { + // Same modifications on both side + if (local_hash === null) { + // Deleted on both side, drop signature + return context._signature_sub_storage.remove(id) + .push(function () { + skip_document_dict[id] = null; + }); + } + + return context._signature_sub_storage.put(id, { + hash: local_hash, + from_local: from_local + }) + .push(function () { + skip_document_dict[id] = null; + }); + } + + if ((remote_hash === status_hash) || (conflict_force === true)) { + // Modified only locally. No conflict or force + if (local_hash === null) { + // Deleted locally + return propagateDeletion(context, destination, id, + skip_document_dict, + skip_deleted_document_dict); + } + return propagateModification(context, source, destination, doc, + local_hash, id, skip_document_dict, + skip_deleted_document_dict, + {use_post: ((options.use_post) && + (remote_hash === null)), + from_local: from_local, + create_new_document: + ((remote_hash === null) && + (status_hash !== null)) + }); + } + + // Conflict cases + if (conflict_ignore === true) { + return; + } + + if ((conflict_revert === true) || (local_hash === null)) { + // Automatically resolve conflict or force revert + if (remote_hash === null) { + // Deleted remotely + return propagateDeletion(context, source, id, skip_document_dict, + skip_deleted_document_dict); + } + return propagateModification( + context, + destination, + source, + remote_doc, + remote_hash, + id, + skip_document_dict, + skip_deleted_document_dict, + {use_post: ((options.use_revert_post) && + (local_hash === null)), + from_local: !from_local, + create_new_document: ((local_hash === null) && + (status_hash !== null))} + ); + } + + // Minimize conflict if it can be resolved + if (remote_hash === null) { + // Copy remote modification remotely + return propagateModification(context, source, destination, doc, + local_hash, id, skip_document_dict, + skip_deleted_document_dict, + {use_post: options.use_post, + from_local: from_local, + create_new_document: + (status_hash !== null)}); + } + doc = doc || local_hash; + remote_doc = remote_doc || remote_hash; + throw new jIO.util.jIOError("Conflict on '" + id + "': " + + stringify(doc) + " !== " + + stringify(remote_doc), + 409); + }); + } + + function checkLocalDeletion(queue, context, skip_document_dict, + skip_deleted_document_dict, + cache, destination_key, + destination, id, source, + conflict_force, conflict_revert, + conflict_ignore, options) { + var status_hash; + queue + .push(function () { + return context._signature_sub_storage.get(id); + }) + .push(function (result) { + status_hash = result.hash; + return checkAndPropagate(context, skip_document_dict, + skip_deleted_document_dict, + cache, destination_key, + status_hash, null, null, + source, destination, id, + conflict_force, conflict_revert, + conflict_ignore, + options); + }); + } + + function checkSignatureDifference(queue, context, skip_document_dict, + skip_deleted_document_dict, + cache, destination_key, + source, destination, id, + conflict_force, conflict_revert, + conflict_ignore, + local_hash, status_hash, + options) { + queue + .push(function () { + if (local_hash === null) { + // Hash was not provided by the allDocs query + return source.get(id); + } + return null; + }) + .push(function (doc) { + if (local_hash === null) { + // Hash was not provided by the allDocs query + local_hash = generateHash(stringify(doc)); + } + + if (local_hash !== status_hash) { + return checkAndPropagate(context, skip_document_dict, + skip_deleted_document_dict, + cache, destination_key, + status_hash, local_hash, doc, + source, destination, id, + conflict_force, conflict_revert, + conflict_ignore, + options); + } + }); + } + + function pushStorage(context, skip_document_dict, + skip_deleted_document_dict, + cache, source_key, destination_key, + source, destination, signature_allDocs, options) { + var argument_list = [], + argument_list_deletion = []; + if (!options.hasOwnProperty("use_post")) { + options.use_post = false; + } + if (!options.hasOwnProperty("use_revert_post")) { + options.use_revert_post = false; + } + return callAllDocsOnStorage(context, source, cache, source_key) + .push(function (source_allDocs) { + var i, + local_dict = {}, + signature_dict = {}, + is_modification, + is_creation, + status_hash, + local_hash, + key, + queue = new RSVP.Queue(); + for (key in source_allDocs) { + if (source_allDocs.hasOwnProperty(key)) { + if (!skip_document_dict.hasOwnProperty(key)) { + local_dict[key] = source_allDocs[key]; + } + } + } + /* + for (i = 0; i < source_allDocs.data.total_rows; i += 1) { + if (!skip_document_dict.hasOwnProperty( + source_allDocs.data.rows[i].id + )) { + local_dict[source_allDocs.data.rows[i].id] = + source_allDocs.data.rows[i].value; + } + } + */ + for (i = 0; i < signature_allDocs.data.total_rows; i += 1) { + if (!skip_document_dict.hasOwnProperty( + signature_allDocs.data.rows[i].id + )) { + signature_dict[signature_allDocs.data.rows[i].id] = + signature_allDocs.data.rows[i].value.hash; + } + } + for (key in local_dict) { + if (local_dict.hasOwnProperty(key)) { + is_modification = signature_dict.hasOwnProperty(key) + && options.check_modification; + is_creation = !signature_dict.hasOwnProperty(key) + && options.check_creation; + + if (is_creation === true) { + status_hash = null; + } else if (is_modification === true) { + status_hash = signature_dict[key]; + } + + local_hash = null; + if (options.signature_hash_key !== undefined) { + local_hash = local_dict[key][options.signature_hash_key]; + if (is_modification === true) { + // Bypass fetching all documents and calculating the sha + // Compare the select list values returned by allDocs calls + is_modification = false; + if (local_hash !== status_hash) { + is_modification = true; + } + } + } + + if (is_modification === true || is_creation === true) { + argument_list.push([undefined, context, skip_document_dict, + skip_deleted_document_dict, + cache, destination_key, + source, destination, + key, + options.conflict_force, + options.conflict_revert, + options.conflict_ignore, + local_hash, status_hash, + options]); + } + } + } + queue + .push(function () { + return dispatchQueue( + context, + checkSignatureDifference, + argument_list, + options.operation_amount + ); + }); + for (key in signature_dict) { + if (signature_dict.hasOwnProperty(key)) { + if (!local_dict.hasOwnProperty(key)) { + if (options.check_deletion === true) { + argument_list_deletion.push([undefined, + context, + skip_document_dict, + skip_deleted_document_dict, + cache, destination_key, + destination, key, + source, + options.conflict_force, + options.conflict_revert, + options.conflict_ignore, + options]); + } else { + skip_deleted_document_dict[key] = null; + } + } + } + } + if (argument_list_deletion.length !== 0) { + queue.push(function () { + return dispatchQueue( + context, + checkLocalDeletion, + argument_list_deletion, + options.operation_amount + ); + }); + } + return queue; + }); + } + + function repairDocument(queue, context, id, signature_hash_key, + signature_hash, signature_attachment_hash, + signature_from_local) { + queue.push(function () { + return repairDocumentAttachment(context, id, signature_hash_key, + signature_hash, + signature_attachment_hash, + signature_from_local); + }); + } + + ReplicateStorage.prototype.repair = function () { + var context = this, + argument_list = arguments, + skip_document_dict = {}, + skip_deleted_document_dict = {}, + cache = {}; + + return new RSVP.Queue() + .push(function () { + // Ensure that the document storage is usable + if (context._custom_signature_sub_storage === false) { + // Do not sync the signature document + skip_document_dict[context._signature_hash] = null; + + return context._signature_sub_storage.__storage._sub_storage + .__storage._sub_storage.get( + context._signature_hash + ); + } + }) + .push(undefined, function (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + return context._signature_sub_storage.__storage._sub_storage + .__storage._sub_storage.put( + context._signature_hash, + {} + ); + } + throw error; + }) + + .push(function () { + return RSVP.all([ +// Don't repair local_sub_storage twice +// context._signature_sub_storage.repair.apply( +// context._signature_sub_storage, +// argument_list +// ), + context._local_sub_storage.repair.apply( + context._local_sub_storage, + argument_list + ), + context._remote_sub_storage.repair.apply( + context._remote_sub_storage, + argument_list + ) + ]); + }) + + .push(function () { + if (context._check_local_modification || + context._check_local_creation || + context._check_local_deletion || + context._check_remote_modification || + context._check_remote_creation || + context._check_remote_deletion) { + return context._signature_sub_storage.allDocs({ + select_list: ['hash'] + }); + } + }) + + .push(function (signature_allDocs) { + if (context._check_local_modification || + context._check_local_creation || + context._check_local_deletion) { + return pushStorage(context, skip_document_dict, + skip_deleted_document_dict, + cache, 'local', 'remote', + context._local_sub_storage, + context._remote_sub_storage, + signature_allDocs, + { + use_post: context._use_remote_post, + conflict_force: (context._conflict_handling === + CONFLICT_KEEP_LOCAL), + conflict_revert: (context._conflict_handling === + CONFLICT_KEEP_REMOTE), + conflict_ignore: (context._conflict_handling === + CONFLICT_CONTINUE), + check_modification: context._check_local_modification, + check_creation: context._check_local_creation, + check_deletion: context._check_local_deletion, + operation_amount: context._parallel_operation_amount, + signature_hash_key: context._signature_hash_key, + from_local: true + }) + .push(function () { + return signature_allDocs; + }); + } + return signature_allDocs; + }) + .push(function (signature_allDocs) { + if (context._check_remote_modification || + context._check_remote_creation || + context._check_remote_deletion) { + return pushStorage(context, skip_document_dict, + skip_deleted_document_dict, + cache, 'remote', 'local', + context._remote_sub_storage, + context._local_sub_storage, + signature_allDocs, { + use_revert_post: context._use_remote_post, + conflict_force: (context._conflict_handling === + CONFLICT_KEEP_REMOTE), + conflict_revert: (context._conflict_handling === + CONFLICT_KEEP_LOCAL), + conflict_ignore: (context._conflict_handling === + CONFLICT_CONTINUE), + check_modification: context._check_remote_modification, + check_creation: context._check_remote_creation, + check_deletion: context._check_remote_deletion, + operation_amount: context._parallel_operation_amount, + signature_hash_key: context._signature_hash_key, + from_local: false + }); + } + }) + .push(function () { + if (context._check_local_attachment_modification || + context._check_local_attachment_creation || + context._check_local_attachment_deletion || + context._check_remote_attachment_modification || + context._check_remote_attachment_creation || + context._check_remote_attachment_deletion) { + // Attachments are synchronized if and only if their parent document + // has been also marked as synchronized. + return context._signature_sub_storage.allDocs({ + select_list: ['hash', 'attachment_hash', 'from_local'] + }) + .push(function (result) { + var i, + local_argument_list = [], + row, + len = result.data.total_rows; + + for (i = 0; i < len; i += 1) { + row = result.data.rows[i]; + // Do not synchronize attachment if one version of the document + // is deleted but not pushed to the other storage + if (!skip_deleted_document_dict.hasOwnProperty(row.id)) { + local_argument_list.push( + [undefined, context, row.id, context._signature_hash_key, + row.value.hash, row.value.attachment_hash, + row.value.from_local] + ); + } + } + return dispatchQueue( + context, + repairDocument, + local_argument_list, + context._parallel_operation_amount + ); + }); + } + }); + }; + + jIO.addStorage('replicate', ReplicateStorage); + +}(jIO, RSVP, Rusha, jIO.util.stringify)); +;/* + * Copyright 2015, Nexedi SA + * Released under the LGPL license. + * http://www.gnu.org/licenses/lgpl.html + */ + +/*jslint nomen: true*/ +/*global Rusha*/ + +/** + * JIO Sha Storage. Type = 'sha'. + */ + +(function (Rusha) { + "use strict"; + + var rusha = new Rusha(); + + function ShaStorage(spec) { + this._sub_storage = jIO.createJIO(spec.sub_storage); + } + + ShaStorage.prototype.post = function (param) { + return this._sub_storage.put( + rusha.digestFromString(JSON.stringify(param)), + param + ); + }; + + ShaStorage.prototype.get = function () { + return this._sub_storage.get.apply(this._sub_storage, arguments); + }; + ShaStorage.prototype.remove = function () { + return this._sub_storage.remove.apply(this._sub_storage, arguments); + }; + ShaStorage.prototype.hasCapacity = function () { + return this._sub_storage.hasCapacity.apply(this._sub_storage, arguments); + }; + ShaStorage.prototype.buildQuery = function () { + return this._sub_storage.buildQuery.apply(this._sub_storage, arguments); + }; + ShaStorage.prototype.getAttachment = function () { + return this._sub_storage.getAttachment.apply(this._sub_storage, arguments); + }; + ShaStorage.prototype.putAttachment = function () { + return this._sub_storage.putAttachment.apply(this._sub_storage, arguments); + }; + ShaStorage.prototype.removeAttachment = function () { + return this._sub_storage.removeAttachment.apply(this._sub_storage, + arguments); + }; + ShaStorage.prototype.allAttachments = function () { + return this._sub_storage.allAttachments.apply(this._sub_storage, arguments); + }; + ShaStorage.prototype.repair = function () { + return this._sub_storage.repair.apply(this._sub_storage, arguments); + }; + + jIO.addStorage('sha', ShaStorage); + +}(Rusha)); +;/*jslint nomen: true*/ +(function (jIO) { + "use strict"; + + /** + * The jIO UUIDStorage extension + * + * @class UUIDStorage + * @constructor + */ + function UUIDStorage(spec) { + this._sub_storage = jIO.createJIO(spec.sub_storage); + } + + UUIDStorage.prototype.get = function () { + return this._sub_storage.get.apply(this._sub_storage, arguments); + }; + UUIDStorage.prototype.allAttachments = function () { + return this._sub_storage.allAttachments.apply(this._sub_storage, arguments); + }; + UUIDStorage.prototype.post = function (param) { + + function S4() { + return ('0000' + Math.floor( + Math.random() * 0x10000 /* 65536 */ + ).toString(16)).slice(-4); + } + + var id = S4() + S4() + "-" + + S4() + "-" + + S4() + "-" + + S4() + "-" + + S4() + S4() + S4(); + + return this.put(id, param); + }; + UUIDStorage.prototype.put = function () { + return this._sub_storage.put.apply(this._sub_storage, arguments); + }; + UUIDStorage.prototype.remove = function () { + return this._sub_storage.remove.apply(this._sub_storage, arguments); + }; + UUIDStorage.prototype.getAttachment = function () { + return this._sub_storage.getAttachment.apply(this._sub_storage, arguments); + }; + UUIDStorage.prototype.putAttachment = function () { + return this._sub_storage.putAttachment.apply(this._sub_storage, arguments); + }; + UUIDStorage.prototype.removeAttachment = function () { + return this._sub_storage.removeAttachment.apply(this._sub_storage, + arguments); + }; + UUIDStorage.prototype.repair = function () { + return this._sub_storage.repair.apply(this._sub_storage, arguments); + }; + UUIDStorage.prototype.hasCapacity = function (name) { + return this._sub_storage.hasCapacity(name); + }; + UUIDStorage.prototype.buildQuery = function () { + return this._sub_storage.buildQuery.apply(this._sub_storage, + arguments); + }; + + jIO.addStorage('uuid', UUIDStorage); + +}(jIO)); +;/* + * Copyright 2013, Nexedi SA + * Released under the LGPL license. + * http://www.gnu.org/licenses/lgpl.html + */ + +/*jslint nomen: true*/ +/*global jIO, RSVP*/ + +/** + * JIO Memory Storage. Type = 'memory'. + * Memory browser "database" storage. + * + * Storage Description: + * + * { + * "type": "memory" + * } + * + * @class MemoryStorage + */ + +(function (jIO, JSON, RSVP) { + "use strict"; + + /** + * The JIO MemoryStorage extension + * + * @class MemoryStorage + * @constructor + */ + function MemoryStorage() { + this._database = {}; + } + + MemoryStorage.prototype.put = function (id, metadata) { + if (!this._database.hasOwnProperty(id)) { + this._database[id] = { + attachments: {} + }; + } + this._database[id].doc = JSON.stringify(metadata); + return id; + }; + + MemoryStorage.prototype.get = function (id) { + try { + return JSON.parse(this._database[id].doc); + } catch (error) { + if (error instanceof TypeError) { + throw new jIO.util.jIOError( + "Cannot find document: " + id, + 404 + ); + } + throw error; + } + }; + + MemoryStorage.prototype.allAttachments = function (id) { + var key, + attachments = {}; + try { + for (key in this._database[id].attachments) { + if (this._database[id].attachments.hasOwnProperty(key)) { + attachments[key] = {}; + } + } + } catch (error) { + if (error instanceof TypeError) { + throw new jIO.util.jIOError( + "Cannot find document: " + id, + 404 + ); + } + throw error; + } + return attachments; + }; + + MemoryStorage.prototype.remove = function (id) { + delete this._database[id]; + return id; + }; + + MemoryStorage.prototype.getAttachment = function (id, name) { + try { + var result = this._database[id].attachments[name]; + if (result === undefined) { + throw new jIO.util.jIOError( + "Cannot find attachment: " + id + " , " + name, + 404 + ); + } + return jIO.util.dataURItoBlob(result); + } catch (error) { + if (error instanceof TypeError) { + throw new jIO.util.jIOError( + "Cannot find attachment: " + id + " , " + name, + 404 + ); + } + throw error; + } + }; + + MemoryStorage.prototype.putAttachment = function (id, name, blob) { + var attachment_dict; + try { + attachment_dict = this._database[id].attachments; + } catch (error) { + if (error instanceof TypeError) { + throw new jIO.util.jIOError("Cannot find document: " + id, 404); + } + throw error; + } + return new RSVP.Queue() + .push(function () { + return jIO.util.readBlobAsDataURL(blob); + }) + .push(function (evt) { + attachment_dict[name] = evt.target.result; + }); + }; + + MemoryStorage.prototype.removeAttachment = function (id, name) { + try { + delete this._database[id].attachments[name]; + } catch (error) { + if (error instanceof TypeError) { + throw new jIO.util.jIOError( + "Cannot find document: " + id, + 404 + ); + } + throw error; + } + }; + + + MemoryStorage.prototype.hasCapacity = function (name) { + return ((name === "list") || (name === "include")); + }; + + MemoryStorage.prototype.buildQuery = function (options) { + var rows = [], + i; + for (i in this._database) { + if (this._database.hasOwnProperty(i)) { + if (options.include_docs === true) { + rows.push({ + id: i, + value: {}, + doc: JSON.parse(this._database[i].doc) + }); + } else { + rows.push({ + id: i, + value: {} + }); + } + + } + } + return rows; + }; + + jIO.addStorage('memory', MemoryStorage); + +}(jIO, JSON, RSVP)); +;/*jslint nomen: true*/ +/*global RSVP, Blob, LZString, DOMException*/ +(function (RSVP, Blob, LZString, DOMException) { + "use strict"; + + /** + * The jIO ZipStorage extension + * + * @class ZipStorage + * @constructor + */ + + var MIME_TYPE = "application/x-jio-utf16_lz_string"; + + function ZipStorage(spec) { + this._sub_storage = jIO.createJIO(spec.sub_storage); + } + + ZipStorage.prototype.get = function () { + return this._sub_storage.get.apply(this._sub_storage, + arguments); + }; + + ZipStorage.prototype.post = function () { + return this._sub_storage.post.apply(this._sub_storage, + arguments); + }; + + ZipStorage.prototype.put = function () { + return this._sub_storage.put.apply(this._sub_storage, + arguments); + }; + + ZipStorage.prototype.remove = function () { + return this._sub_storage.remove.apply(this._sub_storage, + arguments); + }; + + ZipStorage.prototype.hasCapacity = function () { + return this._sub_storage.hasCapacity.apply(this._sub_storage, + arguments); + }; + + ZipStorage.prototype.buildQuery = function () { + return this._sub_storage.buildQuery.apply(this._sub_storage, + arguments); + }; + + ZipStorage.prototype.getAttachment = function (id, name) { + var that = this; + return that._sub_storage.getAttachment(id, name) + .push(function (blob) { + if (blob.type !== MIME_TYPE) { + return blob; + } + return new RSVP.Queue() + .push(function () { + return jIO.util.readBlobAsText(blob, 'utf16'); + }) + .push(function (evt) { + var result = + LZString.decompressFromUTF16(evt.target.result); + if (result === '') { + return blob; + } + try { + return jIO.util.dataURItoBlob( + result + ); + } catch (error) { + if (error instanceof DOMException) { + return blob; + } + throw error; + } + }); + }); + }; + + function myEndsWith(str, query) { + return (str.indexOf(query) === str.length - query.length); + } + + ZipStorage.prototype.putAttachment = function (id, name, blob) { + var that = this; + if ((blob.type.indexOf("text/") === 0) || myEndsWith(blob.type, "xml") || + myEndsWith(blob.type, "json")) { + return new RSVP.Queue() + .push(function () { + return jIO.util.readBlobAsDataURL(blob); + }) + .push(function (data) { + var result = LZString.compressToUTF16(data.target.result); + blob = new Blob([result], + {type: MIME_TYPE}); + return that._sub_storage.putAttachment(id, name, blob); + }); + } + return this._sub_storage.putAttachment.apply(this._sub_storage, + arguments); + }; + + ZipStorage.prototype.removeAttachment = function () { + return this._sub_storage.removeAttachment.apply(this._sub_storage, + arguments); + }; + + ZipStorage.prototype.allAttachments = function () { + return this._sub_storage.allAttachments.apply(this._sub_storage, + arguments); + }; + + jIO.addStorage('zip', ZipStorage); +}(RSVP, Blob, LZString, DOMException)); +;/*jslint nomen: true*/ +/*global jIO, DOMParser, Node */ +(function (jIO, DOMParser, Node) { + "use strict"; + + ///////////////////////////////////////////////////////////// + // OPML Parser + ///////////////////////////////////////////////////////////// + function OPMLParser(txt) { + this._dom_parser = new DOMParser().parseFromString(txt, 'text/xml'); + } + + OPMLParser.prototype.parseHead = function () { + // fetch all children instead + var channel_element = this._dom_parser.querySelector("opml > head"), + tag_element, + i, + result = {}; + + for (i = channel_element.childNodes.length - 1; i >= 0; i -= 1) { + tag_element = channel_element.childNodes[i]; + if (tag_element.nodeType === Node.ELEMENT_NODE) { + result[tag_element.tagName] = tag_element.textContent; + } + } + return result; + }; + + OPMLParser.prototype.parseOutline = function (result_list, outline_element, + prefix, include, id) { + var attribute, + i, + child, + result = {}; + + if ((id === prefix) || (id === undefined)) { + result_list.push({ + id: prefix, + value: {} + }); + if (include) { + for (i = outline_element.attributes.length - 1; i >= 0; i -= 1) { + attribute = outline_element.attributes[i]; + if (attribute.value) { + result[attribute.name] = attribute.value; + } + } + result_list[result_list.length - 1].doc = result; + } + } + + for (i = outline_element.childNodes.length - 1; i >= 0; i -= 1) { + child = outline_element.childNodes[i]; + if (child.tagName === 'outline') { + this.parseOutline(result_list, child, prefix + '/' + i, include, id); + } + } + }; + + OPMLParser.prototype.getDocumentList = function (include, id) { + var result_list, + item_list = this._dom_parser.querySelectorAll("body > outline"), + i; + + if ((id === '/0') || (id === undefined)) { + result_list = [{ + id: '/0', + value: {} + }]; + if (include) { + result_list[0].doc = this.parseHead(); + } + } else { + result_list = []; + } + + for (i = 0; i < item_list.length; i += 1) { + this.parseOutline(result_list, item_list[i], '/1/' + i, include, id); + } + return result_list; + }; + ///////////////////////////////////////////////////////////// + // ATOM Parser + ///////////////////////////////////////////////////////////// + function ATOMParser(txt) { + this._dom_parser = new DOMParser().parseFromString(txt, 'text/xml'); + } + ATOMParser.prototype.parseElement = function (element) { + var tag_element, + i, + j, + tag_name, + attribute, + result = {}; + + for (i = element.childNodes.length - 1; i >= 0; i -= 1) { + tag_element = element.childNodes[i]; + if ((tag_element.nodeType === Node.ELEMENT_NODE) && + (tag_element.tagName !== 'entry')) { + tag_name = tag_element.tagName; + // may have several links, with different rel value + // default is alternate + if (tag_name === 'link') { + tag_name += '_' + (tag_element.getAttribute('rel') || 'alternate'); + } else { + result[tag_name] = tag_element.textContent; + } + for (j = tag_element.attributes.length - 1; j >= 0; j -= 1) { + attribute = tag_element.attributes[j]; + if (attribute.value) { + result[tag_name + '_' + attribute.name] = + attribute.value; + } + } + + } + } + return result; + }; + ATOMParser.prototype.getDocumentList = function (include, id) { + var result_list, + item_list = this._dom_parser.querySelectorAll("feed > entry"), + i; + + if ((id === '/0') || (id === undefined)) { + result_list = [{ + id: '/0', + value: {} + }]; + if (include) { + result_list[0].doc = this.parseElement( + this._dom_parser.querySelector("feed") + ); + } + } else { + result_list = []; + } + + for (i = 0; i < item_list.length; i += 1) { + if ((id === '/0/' + i) || (id === undefined)) { + result_list.push({ + id: '/0/' + i, + value: {} + }); + if (include) { + result_list[result_list.length - 1].doc = + this.parseElement(item_list[i]); + } + } + } + return result_list; + }; + + ///////////////////////////////////////////////////////////// + // RSS Parser + ///////////////////////////////////////////////////////////// + function RSSParser(txt) { + this._dom_parser = new DOMParser().parseFromString(txt, 'text/xml'); + } + + RSSParser.prototype.parseElement = function (element) { + var tag_element, + i, + j, + attribute, + result = {}; + + for (i = element.childNodes.length - 1; i >= 0; i -= 1) { + tag_element = element.childNodes[i]; + if ((tag_element.nodeType === Node.ELEMENT_NODE) && + (tag_element.tagName !== 'item')) { + result[tag_element.tagName] = tag_element.textContent; + + for (j = tag_element.attributes.length - 1; j >= 0; j -= 1) { + attribute = tag_element.attributes[j]; + if (attribute.value) { + result[tag_element.tagName + '_' + attribute.name] = + attribute.value; + } + } + } + } + return result; + }; + + RSSParser.prototype.getDocumentList = function (include, id) { + var result_list, + item_list = this._dom_parser.querySelectorAll("rss > channel > item"), + i; + + if ((id === '/0') || (id === undefined)) { + result_list = [{ + id: '/0', + value: {} + }]; + if (include) { + result_list[0].doc = this.parseElement( + this._dom_parser.querySelector("rss > channel") + ); + } + } else { + result_list = []; + } + + for (i = 0; i < item_list.length; i += 1) { + if ((id === '/0/' + i) || (id === undefined)) { + result_list.push({ + id: '/0/' + i, + value: {} + }); + if (include) { + result_list[result_list.length - 1].doc = + this.parseElement(item_list[i]); + } + } + } + return result_list; + }; + + ///////////////////////////////////////////////////////////// + // Helpers + ///////////////////////////////////////////////////////////// + var parser_dict = { + 'rss': RSSParser, + 'opml': OPMLParser, + 'atom': ATOMParser + }; + + function getParser(storage) { + return storage._sub_storage.getAttachment(storage._document_id, + storage._attachment_id, + {format: 'text'}) + .push(function (txt) { + return new parser_dict[storage._parser_name](txt); + }); + } + + ///////////////////////////////////////////////////////////// + // Storage + ///////////////////////////////////////////////////////////// + function ParserStorage(spec) { + this._attachment_id = spec.attachment_id; + this._document_id = spec.document_id; + this._parser_name = spec.parser; + this._sub_storage = jIO.createJIO(spec.sub_storage); + } + + ParserStorage.prototype.hasCapacity = function (capacity) { + return (capacity === "list") || (capacity === 'include'); + }; + + ParserStorage.prototype.buildQuery = function (options) { + if (options === undefined) { + options = {}; + } + return getParser(this) + .push(function (parser) { + return parser.getDocumentList((options.include_docs || false)); + }); + }; + + ParserStorage.prototype.get = function (id) { + return getParser(this) + .push(function (parser) { + var result_list = parser.getDocumentList(true, id); + if (result_list.length) { + return result_list[0].doc; + } + throw new jIO.util.jIOError( + "Cannot find parsed document: " + id, + 404 + ); + }); + }; + + jIO.addStorage('parser', ParserStorage); + +}(jIO, DOMParser, Node)); +;/*global RSVP, Blob*/ +/*jslint nomen: true*/ +(function (jIO, RSVP, Blob) { + "use strict"; + + function HttpStorage(spec) { + if (spec.hasOwnProperty('catch_error')) { + this._catch_error = spec.catch_error; + } else { + this._catch_error = false; + } + // If timeout not set, use 0 for no timeout value + this._timeout = spec.timeout || 0; + } + + HttpStorage.prototype.get = function (id) { + var context = this; + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + type: 'HEAD', + url: id, + timeout: context._timeout + }); + }) + .push(undefined, function (error) { + if (context._catch_error) { + return error; + } + if ((error.target !== undefined) && + (error.target.status === 404)) { + throw new jIO.util.jIOError("Cannot find url " + id, 404); + } + throw error; + }) + .push(function (response) { + + var key_list = ["Content-Disposition", "Content-Type", "Date", + "Last-Modified", "Vary", "Cache-Control", "Etag", + "Accept-Ranges", "Content-Range"], + i, + key, + value, + result = {}; + result.Status = response.target.status; + for (i = 0; i < key_list.length; i += 1) { + key = key_list[i]; + value = response.target.getResponseHeader(key); + if (value !== null) { + result[key] = value; + } + } + return result; + }); + }; + + HttpStorage.prototype.allAttachments = function () { + return {enclosure: {}}; + }; + + HttpStorage.prototype.getAttachment = function (id, name) { + var context = this; + if (name !== 'enclosure') { + throw new jIO.util.jIOError("Forbidden attachment: " + + id + " , " + name, + 400); + } + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + type: 'GET', + url: id, + dataType: "blob", + timeout: context._timeout + }); + }) + .push(undefined, function (error) { + if (context._catch_error) { + return error; + } + if ((error.target !== undefined) && + (error.target.status === 404)) { + throw new jIO.util.jIOError("Cannot find url " + id, 404); + } + throw error; + }) + .push(function (response) { + return new Blob( + [response.target.response || response.target.responseText], + {"type": response.target.getResponseHeader('Content-Type') || + "application/octet-stream"} + ); + }); + }; + + jIO.addStorage('http', HttpStorage); + +}(jIO, RSVP, Blob));;/* + * Copyright 2013, Nexedi SA + * Released under the LGPL license. + * http://www.gnu.org/licenses/lgpl.html + */ +/** + * JIO Dropbox Storage. Type = "dropbox". + * Dropbox "database" storage. + */ +/*global Blob, jIO, RSVP*/ +/*jslint nomen: true*/ + +(function (jIO, RSVP, Blob, JSON) { + "use strict"; + var GET_URL = "https://content.dropboxapi.com/2/files/download", + UPLOAD_URL = "https://content.dropboxapi.com/2/files/upload", + REMOVE_URL = "https://api.dropboxapi.com/2/files/delete_v2", + CREATE_DIR_URL = "https://api.dropboxapi.com/2/files/create_folder_v2", + METADATA_URL = "https://api.dropboxapi.com/2/files/get_metadata", + LIST_FOLDER_URL = "https://api.dropboxapi.com/2/files/list_folder", + LIST_MORE_URL = "https://api.dropboxapi.com/2/files/list_folder/continue"; + + function restrictDocumentId(id) { + if (id.indexOf("/") !== 0) { + throw new jIO.util.jIOError("id " + id + " is forbidden (no begin /)", + 400); + } + if (id.lastIndexOf("/") !== (id.length - 1)) { + throw new jIO.util.jIOError("id " + id + " is forbidden (no end /)", + 400); + } + return id.slice(0, -1); + } + + function restrictAttachmentId(id) { + if (id.indexOf("/") !== -1) { + throw new jIO.util.jIOError("attachment " + id + " is forbidden", + 400); + } + } + + function recursiveAllAttachments(result, token, id, cursor) { + var data, + url; + if (cursor === undefined) { + data = { + "path": id, + "recursive": false, + "include_media_info": false, + "include_deleted": false, + "include_has_explicit_shared_members": false, + "include_mounted_folders": true + }; + url = LIST_FOLDER_URL; + } else { + data = {"cursor": cursor}; + url = LIST_MORE_URL; + } + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + type: "POST", + url: url, + headers: { + "Authorization": "Bearer " + token, + "Content-Type": "application/json" + }, + data: JSON.stringify(data) + }); + }) + .push(function (evt) { + var obj = JSON.parse(evt.target.response || evt.target.responseText), + i; + for (i = 0; i < obj.entries.length; i += 1) { + if (obj.entries[i][".tag"] === "file") { + result[obj.entries[i].name] = {}; + } + } + if (obj.has_more) { + return recursiveAllAttachments(result, token, id, obj.cursor); + } + return result; + }, function (error) { + if (error.target !== undefined && error.target.status === 409) { + var err_content = JSON.parse(error.target.response || + error.target.responseText); + if ((err_content.error['.tag'] === 'path') && + (err_content.error.path['.tag'] === 'not_folder')) { + throw new jIO.util.jIOError("Not a directory: " + id + "/", + 404); + } + if ((err_content.error['.tag'] === 'path') && + (err_content.error.path['.tag'] === 'not_found')) { + throw new jIO.util.jIOError("Cannot find document: " + id + "/", + 404); + } + } + throw error; + }); + } + + /** + * The JIO Dropbox Storage extension + * + * @class DropboxStorage + * @constructor + */ + function DropboxStorage(spec) { + if (typeof spec.access_token !== 'string' || !spec.access_token) { + throw new TypeError("Access Token' must be a string " + + "which contains more than one character."); + } + this._access_token = spec.access_token; + } + + DropboxStorage.prototype.put = function (id, param) { + var that = this; + id = restrictDocumentId(id); + if (Object.getOwnPropertyNames(param).length > 0) { + // Reject if param has some properties + throw new jIO.util.jIOError("Can not store properties: " + + Object.getOwnPropertyNames(param), 400); + } + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + type: "POST", + url: CREATE_DIR_URL, + headers: { + "Authorization": "Bearer " + that._access_token, + "Content-Type": "application/json" + }, + data: JSON.stringify({"path": id, "autorename": false}) + }); + }) + .push(undefined, function (err) { + if ((err.target !== undefined) && + (err.target.status === 409)) { + var err_content = JSON.parse(err.target.response || + err.target.responseText); + if ((err_content.error['.tag'] === 'path') && + (err_content.error.path['.tag'] === 'conflict')) { + // Directory already exists, no need to fail + return; + } + } + throw err; + }); + }; + + DropboxStorage.prototype.remove = function (id) { + id = restrictDocumentId(id); + return jIO.util.ajax({ + type: "POST", + url: REMOVE_URL, + headers: { + "Authorization": "Bearer " + this._access_token, + "Content-Type": "application/json" + }, + data: JSON.stringify({"path": id}) + }); + }; + + DropboxStorage.prototype.get = function (id) { + var that = this; + + if (id === "/") { + return {}; + } + id = restrictDocumentId(id); + + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + type: "POST", + url: METADATA_URL, + headers: { + "Authorization": "Bearer " + that._access_token, + "Content-Type": "application/json" + }, + data: JSON.stringify({"path": id}) + }); + }) + .push(function (evt) { + var obj = JSON.parse(evt.target.response || + evt.target.responseText); + if (obj[".tag"] === "folder") { + return {}; + } + throw new jIO.util.jIOError("Not a directory: " + id + "/", 404); + }, function (error) { + if (error.target !== undefined && error.target.status === 409) { + var err_content = JSON.parse(error.target.response || + error.target.responseText); + if ((err_content.error['.tag'] === 'path') && + (err_content.error.path['.tag'] === 'not_found')) { + throw new jIO.util.jIOError("Cannot find document: " + id + "/", + 404); + } + } + throw error; + }); + }; + + DropboxStorage.prototype.allAttachments = function (id) { + id = restrictDocumentId(id); + return recursiveAllAttachments({}, this._access_token, id); + }; + + //currently, putAttachment will fail with files larger than 150MB, + //due to the Dropbox API. the API provides the "chunked_upload" method + //to pass this limit, but upload process becomes more complex to implement. + // + //putAttachment will also create a folder if you try to put an attachment + //to an inexisting foler. + + DropboxStorage.prototype.putAttachment = function (id, name, blob) { + id = restrictDocumentId(id); + restrictAttachmentId(name); + + return jIO.util.ajax({ + type: "POST", + url: UPLOAD_URL, + headers: { + "Authorization": "Bearer " + this._access_token, + "Content-Type": "application/octet-stream", + "Dropbox-API-Arg": JSON.stringify({ + "path": id + "/" + name, + "mode": "overwrite", + "autorename": false, + "mute": false + }) + }, + data: blob + }); + }; + + DropboxStorage.prototype.getAttachment = function (id, name) { + var context = this; + + id = restrictDocumentId(id); + restrictAttachmentId(name); + + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + url: GET_URL, + type: "POST", + dataType: "blob", + headers: { + "Authorization": "Bearer " + context._access_token, + "Dropbox-API-Arg": JSON.stringify({"path": id + "/" + name}) + } + }); + }) + .push(function (evt) { + if (evt.target.response instanceof Blob) { + return evt.target.response; + } + return new Blob( + [evt.target.responseText], + {"type": evt.target.getResponseHeader('Content-Type') || + "application/octet-stream"} + ); + }, function (error) { + if (error.target !== undefined && error.target.status === 409) { + if (!(error.target.response instanceof Blob)) { + var err_content = JSON.parse(error.target.responseText); + if ((err_content.error['.tag'] === 'path') && + (err_content.error.path['.tag'] === 'not_found')) { + throw new jIO.util.jIOError("Cannot find attachment: " + + id + "/, " + name, 404); + } + throw error; + } + return new RSVP.Queue() + .push(function () { + return jIO.util.readBlobAsText(error.target.response); + }) + .push(function (evt) { + var err_content = JSON.parse(evt.target.result); + if ((err_content.error['.tag'] === 'path') && + (err_content.error.path['.tag'] === 'not_found')) { + throw new jIO.util.jIOError("Cannot find attachment: " + + id + "/, " + name, 404); + } + throw error; + }); + } + throw error; + }); + }; + + //removeAttachment removes also directories.(due to Dropbox API) + + DropboxStorage.prototype.removeAttachment = function (id, name) { + var that = this; + id = restrictDocumentId(id); + restrictAttachmentId(name); + + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + type: "POST", + url: REMOVE_URL, + headers: { + "Authorization": "Bearer " + that._access_token, + "Content-Type": "application/json" + }, + data: JSON.stringify({"path": id + "/" + name}) + }); + }).push(undefined, function (error) { + if (error.target !== undefined && error.target.status === 409) { + var err_content = JSON.parse(error.target.response || + error.target.responseText); + if ((err_content.error['.tag'] === 'path_lookup') && + (err_content.error.path_lookup['.tag'] === 'not_found')) { + throw new jIO.util.jIOError("Cannot find attachment: " + + id + "/, " + name, 404); + } + } + throw error; + }); + }; + + jIO.addStorage('dropbox', DropboxStorage); + +}(jIO, RSVP, Blob, JSON)); +;/* + * Copyright 2013, Nexedi SA + * Released under the LGPL license. + * http://www.gnu.org/licenses/lgpl.html + */ + +/*jslint nomen: true*/ +/*global jIO, RSVP, DOMParser, Blob */ + +// JIO Dav Storage Description : +// { +// type: "dav", +// url: {string}, +// basic_login: {string} // Basic authentication +// } + +// NOTE: to get the authentication type -> +// curl --verbose -X OPTION http://domain/ +// In the headers: "WWW-Authenticate: Basic realm="DAV-upload" + +(function (jIO, RSVP, DOMParser, Blob) { + "use strict"; + + function ajax(storage, options) { + if (options === undefined) { + options = {}; + } + if (storage._authorization !== undefined) { + if (options.headers === undefined) { + options.headers = {}; + } + options.headers.Authorization = storage._authorization; + } + + if (storage._with_credentials !== undefined) { + if (options.xhrFields === undefined) { + options.xhrFields = {}; + } + options.xhrFields.withCredentials = storage._with_credentials; + } +// if (start !== undefined) { +// if (end !== undefined) { +// headers.Range = "bytes=" + start + "-" + end; +// } else { +// headers.Range = "bytes=" + start + "-"; +// } +// } + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax(options); + }); + } + + function restrictDocumentId(id) { + if (id.indexOf("/") !== 0) { + throw new jIO.util.jIOError("id " + id + " is forbidden (no begin /)", + 400); + } + if (id.lastIndexOf("/") !== (id.length - 1)) { + throw new jIO.util.jIOError("id " + id + " is forbidden (no end /)", + 400); + } + return id; + } + + function restrictAttachmentId(id) { + if (id.indexOf("/") !== -1) { + throw new jIO.util.jIOError("attachment " + id + " is forbidden", + 400); + } + } + + /** + * The JIO WebDAV Storage extension + * + * @class DavStorage + * @constructor + */ + function DavStorage(spec) { + if (typeof spec.url !== 'string') { + throw new TypeError("DavStorage 'url' is not of type string"); + } + this._url = spec.url; + // XXX digest login + if (typeof spec.basic_login === 'string') { + this._authorization = "Basic " + spec.basic_login; + } + this._with_credentials = spec.with_credentials; + } + + DavStorage.prototype.put = function (id, param) { + var that = this; + id = restrictDocumentId(id); + if (Object.getOwnPropertyNames(param).length > 0) { + // Reject if param has some properties + throw new jIO.util.jIOError("Can not store properties: " + + Object.getOwnPropertyNames(param), 400); + } + return new RSVP.Queue() + .push(function () { + return ajax(that, { + type: "MKCOL", + url: that._url + id + }); + }) + .push(undefined, function (err) { + if ((err.target !== undefined) && + (err.target.status === 405)) { + return; + } + throw err; + }); + }; + + DavStorage.prototype.remove = function (id) { + id = restrictDocumentId(id); + return ajax(this, { + type: "DELETE", + url: this._url + id + }); + }; + + DavStorage.prototype.get = function (id) { + var context = this; + id = restrictDocumentId(id); + + return new RSVP.Queue() + .push(function () { + return ajax(context, { + type: "PROPFIND", + url: context._url + id, + dataType: "text", + headers: { + // Increasing this value is a performance killer + Depth: "1" + } + }); + }) + .push(function () { + return {}; + }, function (error) { + if ((error.target !== undefined) && + (error.target.status === 404)) { + throw new jIO.util.jIOError("Cannot find document", 404); + } + throw error; + }); + }; + + DavStorage.prototype.allAttachments = function (id) { + + var context = this; + id = restrictDocumentId(id); + + return new RSVP.Queue() + .push(function () { + return ajax(context, { + type: "PROPFIND", + url: context._url + id, + dataType: "text", + headers: { + // Increasing this value is a performance killer + Depth: "1" + } + }); + }) + + + .push(function (response) { + // Extract all meta informations and return them to JSON + + var i, + attachment = {}, + id, + attachment_list = new DOMParser().parseFromString( + response.target.responseText, + "text/xml" + ).querySelectorAll( + "D\\:response, response" + ); + + // exclude parent folder and browse + for (i = 1; i < attachment_list.length; i += 1) { + // XXX Only get files for now + id = attachment_list[i].querySelector("D\\:href, href"). + textContent.split('/').slice(-1)[0]; + // XXX Ugly + if ((id !== undefined) && (id !== "")) { + attachment[id] = {}; + } + } + return attachment; + + }, function (error) { + if ((error.target !== undefined) && + (error.target.status === 404)) { + throw new jIO.util.jIOError("Cannot find document", 404); + } + throw error; + }); + + }; + + + DavStorage.prototype.putAttachment = function (id, name, blob) { + var that = this; + id = restrictDocumentId(id); + restrictAttachmentId(name); + + return new RSVP.Queue() + .push(function () { + return ajax(that, { + type: "PUT", + url: that._url + id + name, + data: blob + }); + }) + .push(undefined, function (error) { + if (error.target.status === 403 || error.target.status === 424) { + throw new jIO.util.jIOError("Cannot access subdocument", 404); + } + throw error; + }); + }; + + DavStorage.prototype.getAttachment = function (id, name) { + var context = this; + id = restrictDocumentId(id); + restrictAttachmentId(name); + + return new RSVP.Queue() + .push(function () { + return ajax(context, { + type: "GET", + url: context._url + id + name, + dataType: "blob" + }); + }) + .push(function (response) { + return new Blob( + [response.target.response || response.target.responseText], + {"type": response.target.getResponseHeader('Content-Type') || + "application/octet-stream"} + ); + }, function (error) { + if ((error.target !== undefined) && + (error.target.status === 404)) { + throw new jIO.util.jIOError("Cannot find attachment: " + + id + " , " + name, + 404); + } + throw error; + }); + + }; + + DavStorage.prototype.removeAttachment = function (id, name) { + var context = this; + id = restrictDocumentId(id); + restrictAttachmentId(name); + + return new RSVP.Queue() + .push(function () { + return ajax(context, { + type: "DELETE", + url: context._url + id + name + }); + }) + .push(undefined, function (error) { + if ((error.target !== undefined) && + (error.target.status === 404)) { + throw new jIO.util.jIOError("Cannot find attachment: " + + id + " , " + name, + 404); + } + throw error; + }); + }; + + // JIO COMMANDS // + + // wedDav methods rfc4918 (short summary) + // COPY Reproduces single resources (files) and collections (directory + // trees). Will overwrite files (if specified by request) but will + // respond 209 (Conflict) if it would overwrite a tree + // DELETE deletes files and directory trees + // GET just the vanilla HTTP/1.1 behaviour + // HEAD ditto + // LOCK locks a resources + // MKCOL creates a directory + // MOVE Moves (rename or copy) a file or a directory tree. Will + // 'overwrite' files (if specified by the request) but will respond + // 209 (Conflict) if it would overwrite a tree. + // OPTIONS If WebDAV is enabled and available for the path this reports the + // WebDAV extension methods + // PROPFIND Retrieves the requested file characteristics, DAV lock status + // and 'dead' properties for individual files, a directory and its + // child files, or a directory tree + // PROPPATCHset and remove 'dead' meta-data properties + // PUT Update or create resource or collections + // UNLOCK unlocks a resource + + // Notes: all Ajax requests should be CORS (cross-domain) + // adding custom headers triggers preflight OPTIONS request! + // http://remysharp.com/2011/04/21/getting-cors-working/ + + jIO.addStorage('dav', DavStorage); + +}(jIO, RSVP, DOMParser, Blob)); +;/* + * Copyright 2015, Nexedi SA + * Released under the LGPL license. + * http://www.gnu.org/licenses/lgpl.html + */ +/** + * JIO Google Drive Storage. Type = "gdrive". + * Google Drive "database" storage. + */ +/*global jIO, Blob, RSVP, UriTemplate, JSON*/ +/*jslint nomen: true*/ + +(function (jIO, Blob, RSVP, UriTemplate, JSON) { + "use strict"; + + var UPLOAD_URL = "https://www.googleapis.com{/upload}/drive/v2/files{/id}" + + "{?uploadType,access_token}", + upload_template = UriTemplate.parse(UPLOAD_URL), + REMOVE_URL = "https://www.googleapis.com/drive/v2/" + + "files{/id,trash}{?access_token}", + remove_template = UriTemplate.parse(REMOVE_URL), + LIST_URL = "https://www.googleapis.com/drive/v2/files" + + "?prettyPrint=false{&pageToken}&q=trashed=false" + + "&fields=nextPageToken,items(id){&access_token}", + list_template = UriTemplate.parse(LIST_URL), + GET_URL = "https://www.googleapis.com/drive/v2/files{/id}{?alt}", + get_template = UriTemplate.parse(GET_URL); + + function handleError(error, id) { + if (error.target.status === 404) { + throw new jIO.util.jIOError( + "Cannot find document: " + id, + 404 + ); + } + throw error; + } + + function listPage(result, token) { + var i, + obj; + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + "type": "GET", + "url": list_template.expand({ + pageToken : (result.nextPageToken || ""), + access_token: token + }) + }); + }) + .push(function (data) { + obj = JSON.parse(data.target.response || data.target.responseText); + for (i = 0; i < obj.items.length; i += 1) { + obj.items[i].value = {}; + result.push(obj.items[i]); + } + result.nextPageToken = obj.nextPageToken; + return result; + }, handleError); + } + + function checkName(name) { + if (name !== "enclosure") { + throw new jIO.util.jIOError("Only support 'enclosure' attachment", 400); + } + } + + /** + * The JIO Google Drive Storage extension + * + * @class GdriveStorage + * @constructor + */ + function GdriveStorage(spec) { + if (spec === undefined || spec.access_token === undefined || + typeof spec.access_token !== 'string') { + throw new TypeError("Access Token must be a string " + + "which contains more than one character."); + } + if (spec.trashing !== undefined && + (spec.trashing !== true && spec.trashing !== false)) { + throw new TypeError("trashing parameter" + + " must be a boolean (true or false)"); + } + this._trashing = spec.trashing || true; + this._access_token = spec.access_token; + return; + } + + function recursiveAllDocs(result, accessToken) { + return new RSVP.Queue() + .push(function () { + return listPage(result, accessToken); + }) + .push(function () { + if (result.nextPageToken) { + return recursiveAllDocs(result, accessToken); + } + return result; + }); + } + + GdriveStorage.prototype.hasCapacity = function (name) { + return (name === "list"); + }; + + GdriveStorage.prototype.buildQuery = function () { + return recursiveAllDocs([], this._access_token); + }; + + function sendMetaData(id, param, token) { + var boundary = "-------314159265358979323846"; + + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + "type": id ? "PUT" : "POST", + "url": upload_template.expand({ + access_token: token, + id: id || [], + upload: id ? [] : "upload", + uploadType: "multipart" + }), + headers: { + "Content-Type" : 'multipart/related; boundary="' + boundary + '"' + }, + data: '--' + boundary + '\n' + + 'Content-Type: application/json; charset=UTF-8\n\n' + + JSON.stringify(param) + '\n\n--' + boundary + "--" + }); + }) + .push(function (result) { + var obj = JSON.parse(result.target.responseText); + + return obj.id; + }, + function (error) {handleError(error, id); }); + } + + GdriveStorage.prototype.put = function (id, param) { + return sendMetaData(id, param, this._access_token); + }; + + GdriveStorage.prototype.post = function (param) { + return sendMetaData(undefined, param, this._access_token); + }; + + function sendData(id, blob, token) { + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + "type": "PUT", + "url": upload_template.expand({ + access_token: token, + upload: "upload", + id: id, + uploadType: "media" + }), + data: blob + }); + }) + .push(function (data) { + data = JSON.parse(data.target.responseText); + if (data.mimeType === "application/vnd.google-apps.folder") { + throw new jIO.util.jIOError("cannot put attachments to folder", 400); + } + return data; + }, function (error) {handleError(error, id); }); + } + + GdriveStorage.prototype.putAttachment = function (id, name, blob) { + checkName(name); + return sendData(id, blob, this._access_token); + }; + + GdriveStorage.prototype.remove = function (id) { + var that = this; + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + type: that._trashing ? "POST" : "DELETE", + url: remove_template.expand({ + id : id, + access_token : that._access_token, + trash : that._trashing ? "trash" : [] + }) + }); + }) + .push(undefined, function (error) {handleError(error, id); }); + }; + + function getData(id, attach, token) { + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + type: "GET", + dataType: attach ? "blob" : "json", + url: get_template.expand({ + id: id, + alt: attach ? "media" : [], + access_token: token + }), + headers: { + "Authorization" : "Bearer " + token + } + }); + }) + .push(function (evt) { + return evt.target.response || + (attach ? new Blob([evt.target.responseText], + {"type" : + evt.target.responseHeaders["Content-Type"]}) : + JSON.parse(evt.target.responseText)); + }, function (error) {handleError(error, id); }); + } + + GdriveStorage.prototype.get = function (id) { + return getData(id, false, this._access_token); + }; + + GdriveStorage.prototype.getAttachment = function (id, name) { + checkName(name); + return getData(id, true, this._access_token); + }; + + GdriveStorage.prototype.allAttachments = function (id) { + var token = this._access_token; + + return new RSVP.Queue() + .push(function () { + return getData(id, false, token); + }) + .push(function (data) { + if (data.mimeType === "application/vnd.google-apps.folder") { + return {}; + } + return {"enclosure": {}}; + }); + }; + + jIO.addStorage('gdrive', GdriveStorage); + +}(jIO, Blob, RSVP, UriTemplate, JSON)); +;/*jslint nomen: true */ +/*global RSVP*/ + +/** + * JIO Union Storage. Type = 'union'. + * This provide a unified access other multiple storage. + * New document are created in the first sub storage. + * Document are searched in each sub storage until it is found. + * + * + * Storage Description: + * + * { + * "type": "union", + * "storage_list": [ + * sub_storage_description_1, + * sub_storage_description_2, + * + * sub_storage_description_X, + * ] + * } + * + * @class UnionStorage + */ + +(function (jIO, RSVP) { + "use strict"; + + /** + * The JIO UnionStorage extension + * + * @class UnionStorage + * @constructor + */ + function UnionStorage(spec) { + if (!Array.isArray(spec.storage_list)) { + throw new jIO.util.jIOError("storage_list is not an Array", 400); + } + var i; + this._storage_list = []; + for (i = 0; i < spec.storage_list.length; i += 1) { + this._storage_list.push(jIO.createJIO(spec.storage_list[i])); + } + } + + UnionStorage.prototype._getWithStorageIndex = function () { + var i, + index = 0, + context = this, + arg = arguments, + result = this._storage_list[0].get.apply(this._storage_list[0], arg); + + function handle404(j) { + result + .push(undefined, function (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + return context._storage_list[j].get.apply(context._storage_list[j], + arg) + .push(function (doc) { + index = j; + return doc; + }); + } + throw error; + }); + } + + for (i = 1; i < this._storage_list.length; i += 1) { + handle404(i); + } + return result + .push(function (doc) { + return [index, doc]; + }); + }; + + /* + * Get a document + * Try on each substorage on after the other + */ + UnionStorage.prototype.get = function () { + return this._getWithStorageIndex.apply(this, arguments) + .push(function (result) { + return result[1]; + }); + }; + + /* + * Get attachments list + * Try on each substorage on after the other + */ + UnionStorage.prototype.allAttachments = function () { + var argument_list = arguments, + context = this; + return this._getWithStorageIndex.apply(this, arguments) + .push(function (result) { + var sub_storage = context._storage_list[result[0]]; + return sub_storage.allAttachments.apply(sub_storage, argument_list); + }); + }; + + /* + * Post a document + * Simply store on the first substorage + */ + UnionStorage.prototype.post = function () { + return this._storage_list[0].post.apply(this._storage_list[0], arguments); + }; + + /* + * Put a document + * Search the document location, and modify it in its storage. + */ + UnionStorage.prototype.put = function () { + var arg = arguments, + context = this; + return this._getWithStorageIndex(arg[0]) + .push(undefined, function (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + // Document does not exist, create in first substorage + return [0]; + } + throw error; + }) + .push(function (result) { + // Storage found, modify in it directly + var sub_storage = context._storage_list[result[0]]; + return sub_storage.put.apply(sub_storage, arg); + }); + }; + + /* + * Remove a document + * Search the document location, and remove it from its storage. + */ + UnionStorage.prototype.remove = function () { + var arg = arguments, + context = this; + return this._getWithStorageIndex(arg[0]) + .push(function (result) { + // Storage found, remove from it directly + var sub_storage = context._storage_list[result[0]]; + return sub_storage.remove.apply(sub_storage, arg); + }); + }; + + UnionStorage.prototype.buildQuery = function () { + var promise_list = [], + i, + id_dict = {}, + len = this._storage_list.length, + sub_storage; + for (i = 0; i < len; i += 1) { + sub_storage = this._storage_list[i]; + promise_list.push(sub_storage.buildQuery.apply(sub_storage, arguments)); + } + return new RSVP.Queue() + .push(function () { + return RSVP.all(promise_list); + }) + .push(function (result_list) { + var result = [], + sub_result, + sub_result_len, + j; + len = result_list.length; + for (i = 0; i < len; i += 1) { + sub_result = result_list[i]; + sub_result_len = sub_result.length; + for (j = 0; j < sub_result_len; j += 1) { + if (!id_dict.hasOwnProperty(sub_result[j].id)) { + id_dict[sub_result[j].id] = null; + result.push(sub_result[j]); + } + } + } + return result; + }); + }; + + UnionStorage.prototype.hasCapacity = function (name) { + var i, + len, + result, + sub_storage; + if ((name === "list") || + (name === "query") || + (name === "select")) { + result = true; + len = this._storage_list.length; + for (i = 0; i < len; i += 1) { + sub_storage = this._storage_list[i]; + result = result && sub_storage.hasCapacity(name); + } + return result; + } + return false; + }; + + UnionStorage.prototype.repair = function () { + var i, + promise_list = []; + for (i = 0; i < this._storage_list.length; i += 1) { + promise_list.push(this._storage_list[i].repair.apply( + this._storage_list[i], + arguments + )); + } + return RSVP.all(promise_list); + }; + + UnionStorage.prototype.getAttachment = function () { + var argument_list = arguments, + context = this; + return this._getWithStorageIndex.apply(this, arguments) + .push(function (result) { + var sub_storage = context._storage_list[result[0]]; + return sub_storage.getAttachment.apply(sub_storage, argument_list); + }); + }; + + UnionStorage.prototype.putAttachment = function () { + var argument_list = arguments, + context = this; + return this._getWithStorageIndex.apply(this, arguments) + .push(function (result) { + var sub_storage = context._storage_list[result[0]]; + return sub_storage.putAttachment.apply(sub_storage, argument_list); + }); + }; + + UnionStorage.prototype.removeAttachment = function () { + var argument_list = arguments, + context = this; + return this._getWithStorageIndex.apply(this, arguments) + .push(function (result) { + var sub_storage = context._storage_list[result[0]]; + return sub_storage.removeAttachment.apply(sub_storage, argument_list); + }); + }; + + jIO.addStorage('union', UnionStorage); + +}(jIO, RSVP)); +;/* + * Copyright 2013, Nexedi SA + * Released under the LGPL license. + * http://www.gnu.org/licenses/lgpl.html + */ +// JIO ERP5 Storage Description : +// { +// type: "erp5" +// url: {string} +// } + +/*jslint nomen: true, unparam: true */ +/*global jIO, UriTemplate, FormData, RSVP, URI, Blob, + SimpleQuery, ComplexQuery*/ + +(function (jIO, UriTemplate, FormData, RSVP, URI, Blob, + SimpleQuery, ComplexQuery) { + "use strict"; + + function getSiteDocument(storage) { + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + "type": "GET", + "url": storage._url, + "xhrFields": { + withCredentials: true + } + }); + }) + .push(function (event) { + return JSON.parse(event.target.responseText); + }); + } + + function getDocumentAndHateoas(storage, id, options) { + if (options === undefined) { + options = {}; + } + return getSiteDocument(storage) + .push(function (site_hal) { + // XXX need to get modified metadata + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + "type": "GET", + "url": UriTemplate.parse(site_hal._links.traverse.href) + .expand({ + relative_url: id, + view: options._view + }), + "xhrFields": { + withCredentials: true + } + }); + }) + .push(undefined, function (error) { + if ((error.target !== undefined) && + (error.target.status === 404)) { + throw new jIO.util.jIOError("Cannot find document: " + id, 404); + } + throw error; + }); + }); + } + + var allowed_field_dict = { + "StringField": null, + "EmailField": null, + "IntegerField": null, + "FloatField": null, + "TextAreaField": null + }; + + function extractPropertyFromFormJSON(json) { + return new RSVP.Queue() + .push(function () { + var form = json._embedded._view, + converted_json = { + portal_type: json._links.type.name + }, + form_data_json = {}, + field, + key, + prefix_length, + result; + + if (json._links.hasOwnProperty('parent')) { + converted_json.parent_relative_url = + new URI(json._links.parent.href).segment(2); + } + + form_data_json.form_id = { + "key": [form.form_id.key], + "default": form.form_id["default"] + }; + // XXX How to store datetime + for (key in form) { + if (form.hasOwnProperty(key)) { + field = form[key]; + prefix_length = 0; + if (key.indexOf('my_') === 0 && field.editable) { + prefix_length = 3; + } + if (key.indexOf('your_') === 0) { + prefix_length = 5; + } + if ((prefix_length !== 0) && + (allowed_field_dict.hasOwnProperty(field.type))) { + form_data_json[key.substring(prefix_length)] = { + "default": field["default"], + "key": field.key + }; + converted_json[key.substring(prefix_length)] = field["default"]; + } + } + } + + result = { + data: converted_json, + form_data: form_data_json + }; + if (form.hasOwnProperty('_actions') && + form._actions.hasOwnProperty('put')) { + result.action_href = form._actions.put.href; + } + return result; + }); + } + + function extractPropertyFromForm(context, id) { + return context.getAttachment(id, "view") + .push(function (blob) { + return jIO.util.readBlobAsText(blob); + }) + .push(function (evt) { + return JSON.parse(evt.target.result); + }) + .push(function (json) { + return extractPropertyFromFormJSON(json); + }); + } + + // XXX docstring + function ERP5Storage(spec) { + if (typeof spec.url !== "string" || !spec.url) { + throw new TypeError("ERP5 'url' must be a string " + + "which contains more than one character."); + } + this._url = spec.url; + this._default_view_reference = spec.default_view_reference; + } + + function convertJSONToGet(json) { + var key, + result = json.data; + // Remove all ERP5 hateoas links / convert them into jIO ID + for (key in result) { + if (result.hasOwnProperty(key)) { + if (!result[key]) { + delete result[key]; + } + } + } + return result; + } + + ERP5Storage.prototype.get = function (id) { + return extractPropertyFromForm(this, id) + .push(function (result) { + return convertJSONToGet(result); + }); + }; + + ERP5Storage.prototype.post = function (data) { + var context = this, + new_id; + + return getSiteDocument(this) + .push(function (site_hal) { + var form_data = new FormData(); + form_data.append("portal_type", data.portal_type); + form_data.append("parent_relative_url", data.parent_relative_url); + return jIO.util.ajax({ + type: "POST", + url: site_hal._actions.add.href, + data: form_data, + xhrFields: { + withCredentials: true + } + }); + }) + .push(function (evt) { + var location = evt.target.getResponseHeader("X-Location"), + uri = new URI(location); + new_id = uri.segment(2); + return context.put(new_id, data); + }) + .push(function () { + return new_id; + }); + }; + + ERP5Storage.prototype.put = function (id, data) { + var context = this; + + return extractPropertyFromForm(context, id) + .push(function (result) { + var key, + json = result.form_data, + form_data = {}; + form_data[json.form_id.key] = json.form_id["default"]; + + // XXX How to store datetime:!!!!! + for (key in data) { + if (data.hasOwnProperty(key)) { + if (key === "form_id") { + throw new jIO.util.jIOError( + "ERP5: forbidden property: " + key, + 400 + ); + } + if ((key !== "portal_type") && (key !== "parent_relative_url")) { + if (!json.hasOwnProperty(key)) { + throw new jIO.util.jIOError( + "ERP5: can not store property: " + key, + 400 + ); + } + form_data[json[key].key] = data[key]; + } + } + } + if (!result.hasOwnProperty('action_href')) { + throw new jIO.util.jIOError( + "ERP5: can not modify document: " + id, + 403 + ); + } + return context.putAttachment( + id, + result.action_href, + new Blob([JSON.stringify(form_data)], {type: "application/json"}) + ); + }); + }; + + ERP5Storage.prototype.allAttachments = function (id) { + var context = this; + return getDocumentAndHateoas(this, id) + .push(function () { + if (context._default_view_reference === undefined) { + return { + links: {} + }; + } + return { + view: {}, + links: {} + }; + }); + }; + + ERP5Storage.prototype.getAttachment = function (id, action, options) { + if (options === undefined) { + options = {}; + } + if (action === "view") { + if (this._default_view_reference === undefined) { + throw new jIO.util.jIOError( + "Cannot find attachment view for: " + id, + 404 + ); + } + return getDocumentAndHateoas(this, id, + {"_view": this._default_view_reference}) + .push(function (response) { + var result = JSON.parse(response.target.responseText); + // Remove all ERP5 hateoas links / convert them into jIO ID + + // XXX Change default action to an jio urn with attachment name inside + // if Base_edit, do put URN + // if others, do post URN (ie, unique new attachment name) + // XXX Except this attachment name should be generated when + return new Blob( + [JSON.stringify(result)], + {"type": 'application/hal+json'} + ); + }); + } + if (action === "links") { + return getDocumentAndHateoas(this, id) + .push(function (response) { + return new Blob( + [JSON.stringify(JSON.parse(response.target.responseText))], + {"type": 'application/hal+json'} + ); + }); + } + if (action.indexOf(this._url) === 0) { + return new RSVP.Queue() + .push(function () { + var start, + end, + range, + request_options = { + "type": "GET", + "dataType": "blob", + "url": action, + "xhrFields": { + withCredentials: true + } + }; + if (options.start !== undefined || options.end !== undefined) { + start = options.start || 0; + end = options.end; + if (end !== undefined && end < 0) { + throw new jIO.util.jIOError("end must be positive", + 400); + } + if (start < 0) { + range = "bytes=" + start; + } else if (end === undefined) { + range = "bytes=" + start + "-"; + } else { + if (start > end) { + throw new jIO.util.jIOError("start is greater than end", + 400); + } + range = "bytes=" + start + "-" + end; + } + request_options.headers = {Range: range}; + } + return jIO.util.ajax(request_options); + }) + .push(function (evt) { + if (evt.target.response === undefined) { + return new Blob( + [evt.target.responseText], + {"type": evt.target.getResponseHeader("Content-Type")} + ); + } + return evt.target.response; + }); + } + throw new jIO.util.jIOError("ERP5: not support get attachment: " + action, + 400); + }; + + ERP5Storage.prototype.putAttachment = function (id, name, blob) { + // Assert we use a callable on a document from the ERP5 site + if (name.indexOf(this._url) !== 0) { + throw new jIO.util.jIOError("Can not store outside ERP5: " + + name, 400); + } + + return new RSVP.Queue() + .push(function () { + return jIO.util.readBlobAsText(blob); + }) + .push(function (evt) { + var form_data = JSON.parse(evt.target.result), + data = new FormData(), + array, + i, + key, + value; + for (key in form_data) { + if (form_data.hasOwnProperty(key)) { + if (Array.isArray(form_data[key])) { + array = form_data[key]; + } else { + array = [form_data[key]]; + } + for (i = 0; i < array.length; i += 1) { + value = array[i]; + if (typeof value === "object") { + data.append(key, jIO.util.dataURItoBlob(value.url), + value.file_name); + } else { + data.append(key, value); + } + } + } + } + return jIO.util.ajax({ + "type": "POST", + "url": name, + "data": data, + "dataType": "blob", + "xhrFields": { + withCredentials: true + } + }); + }); + }; + + ERP5Storage.prototype.hasCapacity = function (name) { + return ((name === "list") || (name === "query") || + (name === "select") || (name === "limit") || + (name === "sort")); + }; + + function isSingleLocalRoles(parsed_query) { + if ((parsed_query instanceof SimpleQuery) && + (parsed_query.operator === undefined) && + (parsed_query.key === 'local_roles')) { + // local_roles:"Assignee" + return parsed_query.value; + } + } + + function isSingleDomain(parsed_query) { + if ((parsed_query instanceof SimpleQuery) && + (parsed_query.operator === undefined) && + (parsed_query.key !== undefined) && + (parsed_query.key.indexOf('selection_domain_') === 0)) { + // domain_region:"europe/france" + var result = {}; + result[parsed_query.key.slice('selection_domain_'.length)] = + parsed_query.value; + return result; + } + } + + function isMultipleLocalRoles(parsed_query) { + var i, + sub_query, + is_multiple = true, + local_role_list = []; + if ((parsed_query instanceof ComplexQuery) && + (parsed_query.operator === 'OR')) { + + for (i = 0; i < parsed_query.query_list.length; i += 1) { + sub_query = parsed_query.query_list[i]; + if ((sub_query instanceof SimpleQuery) && + (sub_query.key !== undefined) && + (sub_query.key === 'local_roles')) { + local_role_list.push(sub_query.value); + } else { + is_multiple = false; + } + } + if (is_multiple) { + // local_roles:"Assignee" OR local_roles:"Assignor" + return local_role_list; + } + } + } + + ERP5Storage.prototype.buildQuery = function (options) { +// if (typeof options.query !== "string") { +// options.query = (options.query ? +// jIO.Query.objectToSearchText(options.query) : +// undefined); +// } + return getSiteDocument(this) + .push(function (site_hal) { + var query = options.query, + i, + key, + parsed_query, + sub_query, + result_list, + local_roles, + local_role_found = false, + selection_domain, + sort_list = []; + if (options.query) { + parsed_query = jIO.QueryFactory.create(options.query); + result_list = isSingleLocalRoles(parsed_query); + if (result_list) { + query = undefined; + local_roles = result_list; + } else { + result_list = isSingleDomain(parsed_query); + if (result_list) { + query = undefined; + selection_domain = result_list; + } else { + + result_list = isMultipleLocalRoles(parsed_query); + if (result_list) { + query = undefined; + local_roles = result_list; + } else if ((parsed_query instanceof ComplexQuery) && + (parsed_query.operator === 'AND')) { + + // portal_type:"Person" AND local_roles:"Assignee" + // AND selection_domain_region:"europe/france" + for (i = 0; i < parsed_query.query_list.length; i += 1) { + sub_query = parsed_query.query_list[i]; + + if (!local_role_found) { + result_list = isSingleLocalRoles(sub_query); + if (result_list) { + local_roles = result_list; + parsed_query.query_list.splice(i, 1); + query = jIO.Query.objectToSearchText(parsed_query); + local_role_found = true; + } else { + result_list = isMultipleLocalRoles(sub_query); + if (result_list) { + local_roles = result_list; + parsed_query.query_list.splice(i, 1); + query = jIO.Query.objectToSearchText(parsed_query); + local_role_found = true; + } + } + } + + result_list = isSingleDomain(sub_query); + if (result_list) { + parsed_query.query_list.splice(i, 1); + query = jIO.Query.objectToSearchText(parsed_query); + if (selection_domain) { + for (key in result_list) { + if (result_list.hasOwnProperty(key)) { + selection_domain[key] = result_list[key]; + } + } + } else { + selection_domain = result_list; + } + i -= 1; + } + + } + } + } + } + } + + if (options.sort_on) { + for (i = 0; i < options.sort_on.length; i += 1) { + sort_list.push(JSON.stringify(options.sort_on[i])); + } + } + + if (selection_domain) { + selection_domain = JSON.stringify(selection_domain); + } + + return jIO.util.ajax({ + "type": "GET", + "url": UriTemplate.parse(site_hal._links.raw_search.href) + .expand({ + query: query, + // XXX Force erp5 to return embedded document + select_list: options.select_list || ["title", "reference"], + limit: options.limit, + sort_on: sort_list, + local_roles: local_roles, + selection_domain: selection_domain + }), + "xhrFields": { + withCredentials: true + } + }); + }) + .push(function (response) { + return JSON.parse(response.target.responseText); + }) + .push(function (catalog_json) { + var data = catalog_json._embedded.contents, + count = data.length, + i, + uri, + item, + result = []; + for (i = 0; i < count; i += 1) { + item = data[i]; + uri = new URI(item._links.self.href); + delete item._links; + result.push({ + id: uri.segment(2), + value: item + }); + } + return result; + }); + }; + + jIO.addStorage("erp5", ERP5Storage); + +}(jIO, UriTemplate, FormData, RSVP, URI, Blob, + SimpleQuery, ComplexQuery)); +;/*jslint nomen: true*/ +/*global RSVP, jiodate*/ +(function (jIO, RSVP, jiodate) { + "use strict"; + + function dateType(str) { + return jiodate.JIODate(new Date(str).toISOString()); + } + + function initKeySchema(storage, spec) { + var property; + for (property in spec.schema) { + if (spec.schema.hasOwnProperty(property)) { + if (spec.schema[property].type === "string" && + spec.schema[property].format === "date-time") { + storage._key_schema.key_set[property] = { + read_from: property, + cast_to: "dateType" + }; + if (storage._key_schema.cast_lookup.dateType === undefined) { + storage._key_schema.cast_lookup.dateType = dateType; + } + } else { + throw new jIO.util.jIOError( + "Wrong schema for property: " + property, + 400 + ); + } + } + } + } + + /** + * The jIO QueryStorage extension + * + * @class QueryStorage + * @constructor + */ + function QueryStorage(spec) { + this._sub_storage = jIO.createJIO(spec.sub_storage); + this._key_schema = {key_set: {}, cast_lookup: {}}; + initKeySchema(this, spec); + } + + QueryStorage.prototype.get = function () { + return this._sub_storage.get.apply(this._sub_storage, arguments); + }; + QueryStorage.prototype.allAttachments = function () { + return this._sub_storage.allAttachments.apply(this._sub_storage, arguments); + }; + QueryStorage.prototype.post = function () { + return this._sub_storage.post.apply(this._sub_storage, arguments); + }; + QueryStorage.prototype.put = function () { + return this._sub_storage.put.apply(this._sub_storage, arguments); + }; + QueryStorage.prototype.remove = function () { + return this._sub_storage.remove.apply(this._sub_storage, arguments); + }; + QueryStorage.prototype.getAttachment = function () { + return this._sub_storage.getAttachment.apply(this._sub_storage, arguments); + }; + QueryStorage.prototype.putAttachment = function () { + return this._sub_storage.putAttachment.apply(this._sub_storage, arguments); + }; + QueryStorage.prototype.removeAttachment = function () { + return this._sub_storage.removeAttachment.apply(this._sub_storage, + arguments); + }; + QueryStorage.prototype.repair = function () { + return this._sub_storage.repair.apply(this._sub_storage, arguments); + }; + + QueryStorage.prototype.hasCapacity = function (name) { + var this_storage_capacity_list = ["limit", + "sort", + "select", + "query"]; + + if (this_storage_capacity_list.indexOf(name) !== -1) { + return true; + } + if (name === "list") { + return this._sub_storage.hasCapacity(name); + } + return false; + }; + QueryStorage.prototype.buildQuery = function (options) { + var substorage = this._sub_storage, + context = this, + sub_options = {}, + is_manual_query_needed = false, + is_manual_include_needed = false; + + if (substorage.hasCapacity("list")) { + + // Can substorage handle the queries if needed? + try { + if (((options.query === undefined) || + (substorage.hasCapacity("query"))) && + ((options.sort_on === undefined) || + (substorage.hasCapacity("sort"))) && + ((options.select_list === undefined) || + (substorage.hasCapacity("select"))) && + ((options.limit === undefined) || + (substorage.hasCapacity("limit")))) { + sub_options.query = options.query; + sub_options.sort_on = options.sort_on; + sub_options.select_list = options.select_list; + sub_options.limit = options.limit; + } + } catch (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 501)) { + is_manual_query_needed = true; + } else { + throw error; + } + } + + // Can substorage include the docs if needed? + try { + if ((is_manual_query_needed || + (options.include_docs === true)) && + (substorage.hasCapacity("include"))) { + sub_options.include_docs = true; + } + } catch (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 501)) { + is_manual_include_needed = true; + } else { + throw error; + } + } + + return substorage.buildQuery(sub_options) + + // Include docs if needed + .push(function (result) { + var include_query_list = [result], + len, + i; + + function safeGet(j) { + var id = result[j].id; + return substorage.get(id) + .push(function (doc) { + // XXX Can delete user data! + doc._id = id; + return doc; + }, function (error) { + // Document may have been dropped after listing + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + return; + } + throw error; + }); + } + + if (is_manual_include_needed) { + len = result.length; + for (i = 0; i < len; i += 1) { + include_query_list.push(safeGet(i)); + } + result = RSVP.all(include_query_list); + } + return result; + }) + .push(function (result) { + var original_result, + len, + i; + if (is_manual_include_needed) { + original_result = result[0]; + len = original_result.length; + for (i = 0; i < len; i += 1) { + original_result[i].doc = result[i + 1]; + } + result = original_result; + } + return result; + + }) + + // Manual query if needed + .push(function (result) { + var data_rows = [], + len, + i; + if (is_manual_query_needed) { + len = result.length; + for (i = 0; i < len; i += 1) { + result[i].doc.__id = result[i].id; + data_rows.push(result[i].doc); + } + if (options.select_list) { + options.select_list.push("__id"); + } + result = jIO.QueryFactory.create(options.query || "", + context._key_schema). + exec(data_rows, options); + } + return result; + }) + + // reconstruct filtered rows, preserving the order from docs + .push(function (result) { + var new_result = [], + element, + len, + i; + if (is_manual_query_needed) { + len = result.length; + for (i = 0; i < len; i += 1) { + element = { + id: result[i].__id, + value: options.select_list ? result[i] : {}, + doc: {} + }; + if (options.select_list) { + // Does not work if user manually request __id + delete element.value.__id; + } + if (options.include_docs) { + // XXX To implement + throw new Error("QueryStorage does not support include docs"); + } + new_result.push(element); + } + result = new_result; + } + return result; + }); + + } + }; + + jIO.addStorage('query', QueryStorage); + +}(jIO, RSVP, jiodate)); +;/*jslint nomen: true*/ +/*global RSVP, Blob*/ +(function (jIO, RSVP, Blob) { + "use strict"; + + /** + * The jIO FileSystemBridgeStorage extension + * + * @class FileSystemBridgeStorage + * @constructor + */ + function FileSystemBridgeStorage(spec) { + this._sub_storage = jIO.createJIO(spec.sub_storage); + } + var DOCUMENT_EXTENSION = ".json", + DOCUMENT_KEY = "/.jio_documents/", + ROOT = "/"; + + function endsWith(str, suffix) { + return str.indexOf(suffix, str.length - suffix.length) !== -1; + } + + FileSystemBridgeStorage.prototype.get = function (id) { + var context = this; + return new RSVP.Queue() + + // First, try to get explicit reference to the document + + .push(function () { + // First get the document itself if it exists + return context._sub_storage.getAttachment( + DOCUMENT_KEY, + id + DOCUMENT_EXTENSION, + {format: "json"} + ); + }) + .push(undefined, function (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + + // Second, try to get default attachment + return context._sub_storage.allAttachments(ROOT) + .push(function (attachment_dict) { + if (attachment_dict.hasOwnProperty(id)) { + return {}; + } + throw new jIO.util.jIOError("Cannot find document " + id, + 404); + }); + } + throw error; + }); + }; + + FileSystemBridgeStorage.prototype.allAttachments = function (id) { + var context = this; + return context._sub_storage.allAttachments(ROOT) + .push(function (attachment_dict) { + if (attachment_dict.hasOwnProperty(id)) { + return { + enclosure: {} + }; + } + // Second get the document itself if it exists + return context._sub_storage.getAttachment( + DOCUMENT_KEY, + id + DOCUMENT_EXTENSION + ) + .push(function () { + return {}; + }, function (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + throw new jIO.util.jIOError("Cannot find document " + id, + 404); + } + throw error; + }); + }); + + }; + + FileSystemBridgeStorage.prototype.put = function (doc_id, param) { + var context = this; + // XXX Handle conflict! + + return context._sub_storage.putAttachment( + DOCUMENT_KEY, + doc_id + DOCUMENT_EXTENSION, + new Blob([JSON.stringify(param)], {type: "application/json"}) + ) + .push(undefined, function (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + return context._sub_storage.put(DOCUMENT_KEY, {}) + .push(function () { + return context._sub_storage.putAttachment( + DOCUMENT_KEY, + doc_id + DOCUMENT_EXTENSION, + new Blob([JSON.stringify(param)], + {type: "application/json"}) + ); + }); + } + throw error; + }) + .push(function () { + return doc_id; + }); + + }; + + FileSystemBridgeStorage.prototype.remove = function (doc_id) { + var context = this, + got_error = false; + return new RSVP.Queue() + + // First, try to remove enclosure + .push(function () { + return context._sub_storage.removeAttachment( + ROOT, + doc_id + ); + }) + + .push(undefined, function (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + got_error = true; + return; + } + throw error; + }) + + // Second, try to remove explicit doc + .push(function () { + return context._sub_storage.removeAttachment( + DOCUMENT_KEY, + doc_id + DOCUMENT_EXTENSION + ); + }) + + .push(undefined, function (error) { + if ((!got_error) && (error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + return doc_id; + } + throw error; + }); + + }; + + FileSystemBridgeStorage.prototype.hasCapacity = function (capacity) { + return (capacity === "list"); + }; + + FileSystemBridgeStorage.prototype.buildQuery = function () { + var result_dict = {}, + context = this; + return new RSVP.Queue() + + // First, get list of explicit documents + + .push(function () { + return context._sub_storage.allAttachments(DOCUMENT_KEY); + }) + .push(function (result) { + var key; + for (key in result) { + if (result.hasOwnProperty(key)) { + if (endsWith(key, DOCUMENT_EXTENSION)) { + result_dict[key.substring( + 0, + key.length - DOCUMENT_EXTENSION.length + )] = null; + } + } + } + }, function (error) { + if ((error instanceof jIO.util.jIOError) && + (error.status_code === 404)) { + return; + } + throw error; + }) + + // Second, get list of enclosure + + .push(function () { + return context._sub_storage.allAttachments(ROOT); + }) + .push(function (result) { + var key; + for (key in result) { + if (result.hasOwnProperty(key)) { + result_dict[key] = null; + } + } + }) + + // Finally, build the result + + .push(function () { + var result = [], + key; + for (key in result_dict) { + if (result_dict.hasOwnProperty(key)) { + result.push({ + id: key, + value: {} + }); + } + } + return result; + }); + + }; + + FileSystemBridgeStorage.prototype.getAttachment = function (id, name) { + if (name !== "enclosure") { + throw new jIO.util.jIOError("Only support 'enclosure' attachment", + 400); + } + + return this._sub_storage.getAttachment(ROOT, id); + }; + + FileSystemBridgeStorage.prototype.putAttachment = function (id, name, blob) { + if (name !== "enclosure") { + throw new jIO.util.jIOError("Only support 'enclosure' attachment", + 400); + } + + return this._sub_storage.putAttachment( + ROOT, + id, + blob + ); + }; + + FileSystemBridgeStorage.prototype.removeAttachment = function (id, name) { + if (name !== "enclosure") { + throw new jIO.util.jIOError("Only support 'enclosure' attachment", + 400); + } + + return this._sub_storage.removeAttachment(ROOT, id); + }; + + FileSystemBridgeStorage.prototype.repair = function () { + return this._sub_storage.repair.apply(this._sub_storage, arguments); + }; + + jIO.addStorage('drivetojiomapping', FileSystemBridgeStorage); + +}(jIO, RSVP, Blob)); +;/*jslint nomen: true*/ +/*global Blob, RSVP, unescape, escape*/ +(function (jIO, Blob, RSVP, unescape, escape) { + "use strict"; + /** + * The jIO DocumentStorage extension + * + * @class DocumentStorage + * @constructor + */ + function DocumentStorage(spec) { + this._sub_storage = jIO.createJIO(spec.sub_storage); + this._document_id = spec.document_id; + this._repair_attachment = spec.repair_attachment || false; + } + + var DOCUMENT_EXTENSION = ".json", + DOCUMENT_REGEXP = new RegExp("^jio_document/([\\w=]+)" + + DOCUMENT_EXTENSION + "$"), + ATTACHMENT_REGEXP = new RegExp("^jio_attachment/([\\w=]+)/([\\w=]+)$"), + btoa = function (str) { + return window.btoa(unescape(encodeURIComponent(str))); + }, + atob = function (str) { + return decodeURIComponent(escape(window.atob(str))); + }; + + function getSubAttachmentIdFromParam(id, name) { + if (name === undefined) { + return 'jio_document/' + btoa(id) + DOCUMENT_EXTENSION; + } + return 'jio_attachment/' + btoa(id) + "/" + btoa(name); + } + + DocumentStorage.prototype.get = function (id) { + return this._sub_storage.getAttachment( + this._document_id, + getSubAttachmentIdFromParam(id), + {format: "json"} + ); + }; + + DocumentStorage.prototype.allAttachments = function (id) { + return this._sub_storage.allAttachments(this._document_id) + .push(function (result) { + var attachments = {}, + exec, + key; + for (key in result) { + if (result.hasOwnProperty(key)) { + if (ATTACHMENT_REGEXP.test(key)) { + exec = ATTACHMENT_REGEXP.exec(key); + try { + if (atob(exec[1]) === id) { + attachments[atob(exec[2])] = {}; + } + } catch (error) { + // Check if unable to decode base64 data + if (!error instanceof ReferenceError) { + throw error; + } + } + } + } + } + return attachments; + }); + }; + + DocumentStorage.prototype.put = function (doc_id, param) { + return this._sub_storage.putAttachment( + this._document_id, + getSubAttachmentIdFromParam(doc_id), + new Blob([JSON.stringify(param)], {type: "application/json"}) + ) + .push(function () { + return doc_id; + }); + + }; + + DocumentStorage.prototype.remove = function (id) { + var context = this; + return this.allAttachments(id) + .push(function (result) { + var key, + promise_list = []; + for (key in result) { + if (result.hasOwnProperty(key)) { + promise_list.push(context.removeAttachment(id, key)); + } + } + return RSVP.all(promise_list); + }) + .push(function () { + return context._sub_storage.removeAttachment( + context._document_id, + getSubAttachmentIdFromParam(id) + ); + }) + .push(function () { + return id; + }); + }; + + DocumentStorage.prototype.repair = function () { + var context = this; + return this._sub_storage.repair.apply(this._sub_storage, arguments) + .push(function (result) { + if (context._repair_attachment) { + return context._sub_storage.allAttachments(context._document_id) + .push(function (result_dict) { + var promise_list = [], + id_dict = {}, + attachment_dict = {}, + id, + attachment, + exec, + key; + for (key in result_dict) { + if (result_dict.hasOwnProperty(key)) { + id = undefined; + attachment = undefined; + if (DOCUMENT_REGEXP.test(key)) { + try { + id = atob(DOCUMENT_REGEXP.exec(key)[1]); + } catch (error) { + // Check if unable to decode base64 data + if (!error instanceof ReferenceError) { + throw error; + } + } + if (id !== undefined) { + id_dict[id] = null; + } + } else if (ATTACHMENT_REGEXP.test(key)) { + exec = ATTACHMENT_REGEXP.exec(key); + try { + id = atob(exec[1]); + attachment = atob(exec[2]); + } catch (error) { + // Check if unable to decode base64 data + if (!error instanceof ReferenceError) { + throw error; + } + } + if (attachment !== undefined) { + if (!id_dict.hasOwnProperty(id)) { + if (!attachment_dict.hasOwnProperty(id)) { + attachment_dict[id] = {}; + } + attachment_dict[id][attachment] = null; + } + } + } + } + } + for (id in attachment_dict) { + if (attachment_dict.hasOwnProperty(id)) { + if (!id_dict.hasOwnProperty(id)) { + for (attachment in attachment_dict[id]) { + if (attachment_dict[id].hasOwnProperty(attachment)) { + promise_list.push(context.removeAttachment( + id, + attachment + )); + } + } + } + } + } + return RSVP.all(promise_list); + }); + } + return result; + }); + }; + + DocumentStorage.prototype.hasCapacity = function (capacity) { + return (capacity === "list"); + }; + + DocumentStorage.prototype.buildQuery = function () { + return this._sub_storage.allAttachments(this._document_id) + .push(function (attachment_dict) { + var result = [], + key; + for (key in attachment_dict) { + if (attachment_dict.hasOwnProperty(key)) { + if (DOCUMENT_REGEXP.test(key)) { + try { + result.push({ + id: atob(DOCUMENT_REGEXP.exec(key)[1]), + value: {} + }); + } catch (error) { + // Check if unable to decode base64 data + if (!error instanceof ReferenceError) { + throw error; + } + } + } + } + } + return result; + }); + }; + + DocumentStorage.prototype.getAttachment = function (id, name) { + return this._sub_storage.getAttachment( + this._document_id, + getSubAttachmentIdFromParam(id, name) + ); + }; + + DocumentStorage.prototype.putAttachment = function (id, name, blob) { + return this._sub_storage.putAttachment( + this._document_id, + getSubAttachmentIdFromParam(id, name), + blob + ); + }; + + DocumentStorage.prototype.removeAttachment = function (id, name) { + return this._sub_storage.removeAttachment( + this._document_id, + getSubAttachmentIdFromParam(id, name) + ); + }; + + jIO.addStorage('document', DocumentStorage); + +}(jIO, Blob, RSVP, unescape, escape)); +;/* + * Copyright 2013, Nexedi SA + * Released under the LGPL license. + * http://www.gnu.org/licenses/lgpl.html + */ + +/*jslint nomen: true*/ +/*global jIO, sessionStorage, localStorage, RSVP */ + +/** + * JIO Local Storage. Type = 'local'. + * Local browser "database" storage. + * + * Storage Description: + * + * { + * "type": "local", + * "sessiononly": false + * } + * + * @class LocalStorage + */ + +(function (jIO, sessionStorage, localStorage, RSVP) { + "use strict"; + + function LocalStorage(spec) { + if (spec.sessiononly === true) { + this._storage = sessionStorage; + } else { + this._storage = localStorage; + } + } + + function restrictDocumentId(id) { + if (id !== "/") { + throw new jIO.util.jIOError("id " + id + " is forbidden (!== /)", + 400); + } + } + + LocalStorage.prototype.get = function (id) { + restrictDocumentId(id); + return {}; + }; + + LocalStorage.prototype.allAttachments = function (id) { + restrictDocumentId(id); + + var attachments = {}, + key; + + for (key in this._storage) { + if (this._storage.hasOwnProperty(key)) { + attachments[key] = {}; + } + } + return attachments; + }; + + LocalStorage.prototype.getAttachment = function (id, name) { + restrictDocumentId(id); + + var textstring = this._storage.getItem(name); + + if (textstring === null) { + throw new jIO.util.jIOError( + "Cannot find attachment " + name, + 404 + ); + } + return jIO.util.dataURItoBlob(textstring); + }; + + LocalStorage.prototype.putAttachment = function (id, name, blob) { + var context = this; + restrictDocumentId(id); + + // the document already exists + // download data + return new RSVP.Queue() + .push(function () { + return jIO.util.readBlobAsDataURL(blob); + }) + .push(function (e) { + context._storage.setItem(name, e.target.result); + }); + }; + + LocalStorage.prototype.removeAttachment = function (id, name) { + restrictDocumentId(id); + return this._storage.removeItem(name); + }; + + + LocalStorage.prototype.hasCapacity = function (name) { + return (name === "list"); + }; + + LocalStorage.prototype.buildQuery = function () { + return [{ + id: "/", + value: {} + }]; + }; + + jIO.addStorage('local', LocalStorage); + +}(jIO, sessionStorage, localStorage, RSVP)); +;/* + * Copyright 2014, Nexedi SA + * Released under the LGPL license. + * http://www.gnu.org/licenses/lgpl.html + */ + +/** + * JIO Indexed Database Storage. + * + * A local browser "database" storage greatly more powerful than localStorage. + * + * Description: + * + * { + * "type": "indexeddb", + * "database": + * } + * + * The database name will be prefixed by "jio:", so if the database property is + * "hello", then you can manually reach this database with + * `indexedDB.open("jio:hello");`. (Or + * `indexedDB.deleteDatabase("jio:hello");`.) + * + * For more informations: + * + * - http://www.w3.org/TR/IndexedDB/ + * - https://developer.mozilla.org/en-US/docs/IndexedDB/Using_IndexedDB + */ + +/*jslint nomen: true */ +/*global indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest, + DOMError, Event*/ + +(function (indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest, + DOMError) { + "use strict"; + + // Read only as changing it can lead to data corruption + var UNITE = 2000000; + + function IndexedDBStorage(description) { + if (typeof description.database !== "string" || + description.database === "") { + throw new TypeError("IndexedDBStorage 'database' description property " + + "must be a non-empty string"); + } + this._database_name = "jio:" + description.database; + } + + IndexedDBStorage.prototype.hasCapacity = function (name) { + return ((name === "list") || (name === "include")); + }; + + function buildKeyPath(key_list) { + return key_list.join("_"); + } + + function handleUpgradeNeeded(evt) { + var db = evt.target.result, + store; + + store = db.createObjectStore("metadata", { + keyPath: "_id", + autoIncrement: false + }); + // It is not possible to use openKeyCursor on keypath directly + // https://www.w3.org/Bugs/Public/show_bug.cgi?id=19955 + store.createIndex("_id", "_id", {unique: true}); + + store = db.createObjectStore("attachment", { + keyPath: "_key_path", + autoIncrement: false + }); + store.createIndex("_id", "_id", {unique: false}); + + store = db.createObjectStore("blob", { + keyPath: "_key_path", + autoIncrement: false + }); + store.createIndex("_id_attachment", + ["_id", "_attachment"], {unique: false}); + store.createIndex("_id", "_id", {unique: false}); + } + + function openIndexedDB(jio_storage) { + var db_name = jio_storage._database_name; + function resolver(resolve, reject) { + // Open DB // + var request = indexedDB.open(db_name); + request.onerror = function (error) { + if (request.result) { + request.result.close(); + } + if ((error !== undefined) && + (error.target instanceof IDBOpenDBRequest) && + (error.target.error instanceof DOMError)) { + reject("Connection to: " + db_name + " failed: " + + error.target.error.message); + } else { + reject(error); + } + }; + + request.onabort = function () { + request.result.close(); + reject("Aborting connection to: " + db_name); + }; + + request.ontimeout = function () { + request.result.close(); + reject("Connection to: " + db_name + " timeout"); + }; + + request.onblocked = function () { + request.result.close(); + reject("Connection to: " + db_name + " was blocked"); + }; + + // Create DB if necessary // + request.onupgradeneeded = handleUpgradeNeeded; + + request.onversionchange = function () { + request.result.close(); + reject(db_name + " was upgraded"); + }; + + request.onsuccess = function () { + resolve(request.result); + }; + } + // XXX Canceller??? + return new RSVP.Queue() + .push(function () { + return new RSVP.Promise(resolver); + }); + } + + function openTransaction(db, stores, flag, autoclosedb) { + var tx = db.transaction(stores, flag); + if (autoclosedb !== false) { + tx.oncomplete = function () { + db.close(); + }; + } + tx.onabort = function () { + db.close(); + }; + return tx; + } + + function handleCursor(request, callback, resolve, reject) { + request.onerror = function (error) { + if (request.transaction) { + request.transaction.abort(); + } + reject(error); + }; + + request.onsuccess = function (evt) { + var cursor = evt.target.result; + if (cursor) { + // XXX Wait for result + try { + callback(cursor); + } catch (error) { + reject(error); + } + + // continue to next iteration + cursor["continue"](); + } else { + resolve(); + } + }; + } + + IndexedDBStorage.prototype.buildQuery = function (options) { + var result_list = []; + + function pushIncludedMetadata(cursor) { + result_list.push({ + "id": cursor.key, + "value": {}, + "doc": cursor.value.doc + }); + } + + function pushMetadata(cursor) { + result_list.push({ + "id": cursor.key, + "value": {} + }); + } + return openIndexedDB(this) + .push(function (db) { + return new RSVP.Promise(function (resolve, reject) { + var tx = openTransaction(db, ["metadata"], "readonly"); + if (options.include_docs === true) { + handleCursor(tx.objectStore("metadata").index("_id").openCursor(), + pushIncludedMetadata, resolve, reject); + } else { + handleCursor(tx.objectStore("metadata").index("_id") + .openKeyCursor(), pushMetadata, resolve, reject); + } + }); + }) + .push(function () { + return result_list; + }); + }; + + function handleGet(store, id, resolve, reject) { + var request = store.get(id); + request.onerror = reject; + request.onsuccess = function () { + if (request.result) { + resolve(request.result); + } else { + reject(new jIO.util.jIOError( + "IndexedDB: cannot find object '" + id + "' in the '" + + store.name + "' store", + 404 + )); + } + }; + } + + IndexedDBStorage.prototype.get = function (id) { + return openIndexedDB(this) + .push(function (db) { + return new RSVP.Promise(function (resolve, reject) { + var transaction = openTransaction(db, ["metadata"], "readonly"); + handleGet( + transaction.objectStore("metadata"), + id, + resolve, + reject + ); + }); + }) + .push(function (result) { + return result.doc; + }); + }; + + IndexedDBStorage.prototype.allAttachments = function (id) { + var attachment_dict = {}; + + function addEntry(cursor) { + attachment_dict[cursor.value._attachment] = {}; + } + + return openIndexedDB(this) + .push(function (db) { + return new RSVP.Promise(function (resolve, reject) { + var transaction = openTransaction(db, ["metadata", "attachment"], + "readonly"); + function getAttachments() { + handleCursor( + transaction.objectStore("attachment").index("_id") + .openCursor(IDBKeyRange.only(id)), + addEntry, + resolve, + reject + ); + } + handleGet( + transaction.objectStore("metadata"), + id, + getAttachments, + reject + ); + }); + }) + .push(function () { + return attachment_dict; + }); + }; + + function handleRequest(request, resolve, reject) { + request.onerror = reject; + request.onsuccess = function () { + resolve(request.result); + }; + } + + IndexedDBStorage.prototype.put = function (id, metadata) { + return openIndexedDB(this) + .push(function (db) { + return new RSVP.Promise(function (resolve, reject) { + var transaction = openTransaction(db, ["metadata"], "readwrite"); + handleRequest( + transaction.objectStore("metadata").put({ + "_id": id, + "doc": metadata + }), + resolve, + reject + ); + }); + }); + }; + + function deleteEntry(cursor) { + cursor["delete"](); + } + + IndexedDBStorage.prototype.remove = function (id) { + var resolved_amount = 0; + return openIndexedDB(this) + .push(function (db) { + return new RSVP.Promise(function (resolve, reject) { + function resolver() { + if (resolved_amount < 2) { + resolved_amount += 1; + } else { + resolve(); + } + } + var transaction = openTransaction(db, ["metadata", "attachment", + "blob"], "readwrite"); + handleRequest( + transaction.objectStore("metadata")["delete"](id), + resolver, + reject + ); + // XXX Why not possible to delete with KeyCursor? + handleCursor(transaction.objectStore("attachment").index("_id") + .openCursor(IDBKeyRange.only(id)), + deleteEntry, + resolver, + reject + ); + handleCursor(transaction.objectStore("blob").index("_id") + .openCursor(IDBKeyRange.only(id)), + deleteEntry, + resolver, + reject + ); + }); + }); + }; + + IndexedDBStorage.prototype.getAttachment = function (id, name, options) { + var transaction, + type, + start, + end; + if (options === undefined) { + options = {}; + } + return openIndexedDB(this) + .push(function (db) { + return new RSVP.Promise(function (resolve, reject) { + transaction = openTransaction( + db, + ["attachment", "blob"], + "readonly" + ); + function getBlob(attachment) { + var total_length = attachment.info.length, + result_list = [], + store = transaction.objectStore("blob"), + start_index, + end_index; + type = attachment.info.content_type; + start = options.start || 0; + end = options.end || total_length; + if (end > total_length) { + end = total_length; + } + if (start < 0 || end < 0) { + throw new jIO.util.jIOError( + "_start and _end must be positive", + 400 + ); + } + if (start > end) { + throw new jIO.util.jIOError("_start is greater than _end", + 400); + } + start_index = Math.floor(start / UNITE); + end_index = Math.floor(end / UNITE) - 1; + if (end % UNITE === 0) { + end_index -= 1; + } + function resolver(result) { + if (result.blob !== undefined) { + result_list.push(result); + } + resolve(result_list); + } + function getPart(i) { + return function (result) { + if (result) { + result_list.push(result); + } + i += 1; + handleGet(store, + buildKeyPath([id, name, i]), + (i <= end_index) ? getPart(i) : resolver, + reject + ); + }; + } + getPart(start_index - 1)(); + } + // XXX Should raise if key is not good + handleGet(transaction.objectStore("attachment"), + buildKeyPath([id, name]), + getBlob, + reject + ); + }); + }) + .push(function (result_list) { + var array_buffer_list = [], + blob, + i, + index, + len = result_list.length; + for (i = 0; i < len; i += 1) { + array_buffer_list.push(result_list[i].blob); + } + if ((options.start === undefined) && (options.end === undefined)) { + return new Blob(array_buffer_list, {type: type}); + } + index = Math.floor(start / UNITE) * UNITE; + blob = new Blob(array_buffer_list, {type: "application/octet-stream"}); + return blob.slice(start - index, end - index, + "application/octet-stream"); + }); + }; + + function removeAttachment(transaction, id, name, resolve, reject) { + // XXX How to get the right attachment + function deleteContent() { + handleCursor( + transaction.objectStore("blob").index("_id_attachment") + .openCursor(IDBKeyRange.only([id, name])), + deleteEntry, + resolve, + reject + ); + } + handleRequest( + transaction.objectStore("attachment")["delete"]( + buildKeyPath([id, name]) + ), + deleteContent, + reject + ); + } + + IndexedDBStorage.prototype.putAttachment = function (id, name, blob) { + var blob_part = [], + transaction, + db; + + return openIndexedDB(this) + .push(function (database) { + db = database; + + // Split the blob first + return jIO.util.readBlobAsArrayBuffer(blob); + }) + .push(function (event) { + var array_buffer = event.target.result, + total_size = blob.size, + handled_size = 0; + + while (handled_size < total_size) { + blob_part.push(array_buffer.slice(handled_size, + handled_size + UNITE)); + handled_size += UNITE; + } + + // Remove previous attachment + transaction = openTransaction(db, ["attachment", "blob"], "readwrite"); + return new RSVP.Promise(function (resolve, reject) { + function write() { + var len = blob_part.length - 1, + attachment_store = transaction.objectStore("attachment"), + blob_store = transaction.objectStore("blob"); + function putBlobPart(i) { + return function () { + i += 1; + handleRequest( + blob_store.put({ + "_key_path": buildKeyPath([id, name, i]), + "_id" : id, + "_attachment" : name, + "_part" : i, + "blob": blob_part[i] + }), + (i < len) ? putBlobPart(i) : resolve, + reject + ); + }; + } + handleRequest( + attachment_store.put({ + "_key_path": buildKeyPath([id, name]), + "_id": id, + "_attachment": name, + "info": { + "content_type": blob.type, + "length": blob.size + } + }), + putBlobPart(-1), + reject + ); + } + removeAttachment(transaction, id, name, write, reject); + }); + }); + }; + + IndexedDBStorage.prototype.removeAttachment = function (id, name) { + return openIndexedDB(this) + .push(function (db) { + var transaction = openTransaction(db, ["attachment", "blob"], + "readwrite"); + return new RSVP.Promise(function (resolve, reject) { + removeAttachment(transaction, id, name, resolve, reject); + }); + }); + }; + + jIO.addStorage("indexeddb", IndexedDBStorage); +}(indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest, DOMError)); +;/* + * Copyright 2015, Nexedi SA + * Released under the LGPL license. + * http://www.gnu.org/licenses/lgpl.html + */ + +/*jslint nomen: true*/ +/*global jIO, RSVP, DOMException, Blob, crypto, Uint8Array, ArrayBuffer*/ + +(function (jIO, RSVP, DOMException, Blob, crypto, Uint8Array, ArrayBuffer) { + "use strict"; + + /* + The cryptography system used by this storage is AES-GCM. + Here is an example of how to generate a key to the json format: + + return new RSVP.Queue() + .push(function () { + return crypto.subtle.generateKey({name: "AES-GCM", length: 256}, + true, ["encrypt", "decrypt"]); + }) + .push(function (key) { + return crypto.subtle.exportKey("jwk", key); + }) + .push(function (json_key) { + var jio = jIO.createJIO({ + type: "crypt", + key: json_key, + sub_storage: {storage_definition} + }); + }); + + Find more informations about this cryptography system on + https://github.com/diafygi/webcrypto-examples#aes-gcm + */ + + /** + * The JIO Cryptography Storage extension + * + * @class CryptStorage + * @constructor + */ + + var MIME_TYPE = "application/x-jio-aes-gcm-encryption"; + + function CryptStorage(spec) { + this._key = spec.key; + this._jsonKey = true; + this._sub_storage = jIO.createJIO(spec.sub_storage); + } + + function convertKey(that) { + return new RSVP.Queue() + .push(function () { + return crypto.subtle.importKey("jwk", that._key, + "AES-GCM", false, + ["encrypt", "decrypt"]); + }) + .push(function (res) { + that._key = res; + that._jsonKey = false; + return; + }); + } + + CryptStorage.prototype.get = function () { + return this._sub_storage.get.apply(this._sub_storage, + arguments); + }; + + CryptStorage.prototype.post = function () { + return this._sub_storage.post.apply(this._sub_storage, + arguments); + }; + + CryptStorage.prototype.put = function () { + return this._sub_storage.put.apply(this._sub_storage, + arguments); + }; + + CryptStorage.prototype.remove = function () { + return this._sub_storage.remove.apply(this._sub_storage, + arguments); + }; + + CryptStorage.prototype.hasCapacity = function () { + return this._sub_storage.hasCapacity.apply(this._sub_storage, + arguments); + }; + + CryptStorage.prototype.buildQuery = function () { + return this._sub_storage.buildQuery.apply(this._sub_storage, + arguments); + }; + + + CryptStorage.prototype.putAttachment = function (id, name, blob) { + var initializaton_vector = crypto.getRandomValues(new Uint8Array(12)), + that = this; + + return new RSVP.Queue() + .push(function () { + if (that._jsonKey === true) { + return convertKey(that); + } + return; + }) + .push(function () { + return jIO.util.readBlobAsDataURL(blob); + }) + .push(function (dataURL) { + //string->arraybuffer + var strLen = dataURL.target.result.length, + buf = new ArrayBuffer(strLen), + bufView = new Uint8Array(buf), + i; + + dataURL = dataURL.target.result; + for (i = 0; i < strLen; i += 1) { + bufView[i] = dataURL.charCodeAt(i); + } + return crypto.subtle.encrypt({ + name : "AES-GCM", + iv : initializaton_vector + }, + that._key, buf); + }) + .push(function (coded) { + var blob = new Blob([initializaton_vector, coded], {type: MIME_TYPE}); + return that._sub_storage.putAttachment(id, name, blob); + }); + }; + + CryptStorage.prototype.getAttachment = function (id, name) { + var that = this; + + return that._sub_storage.getAttachment(id, name) + .push(function (blob) { + if (blob.type !== MIME_TYPE) { + return blob; + } + return new RSVP.Queue() + .push(function () { + if (that._jsonKey === true) { + return convertKey(that); + } + return; + }) + .push(function () { + return jIO.util.readBlobAsArrayBuffer(blob); + }) + .push(function (coded) { + var initializaton_vector; + + coded = coded.target.result; + initializaton_vector = new Uint8Array(coded.slice(0, 12)); + return new RSVP.Queue() + .push(function () { + return crypto.subtle.decrypt({ + name : "AES-GCM", + iv : initializaton_vector + }, + that._key, coded.slice(12)); + }) + .push(function (arr) { + //arraybuffer->string + arr = String.fromCharCode.apply(null, new Uint8Array(arr)); + return jIO.util.dataURItoBlob(arr); + }) + .push(undefined, function (error) { + if (error instanceof DOMException) { + return blob; + } + throw error; + }); + }); + }); + }; + + CryptStorage.prototype.removeAttachment = function () { + return this._sub_storage.removeAttachment.apply(this._sub_storage, + arguments); + }; + + CryptStorage.prototype.allAttachments = function () { + return this._sub_storage.allAttachments.apply(this._sub_storage, + arguments); + }; + + jIO.addStorage('crypt', CryptStorage); + +}(jIO, RSVP, DOMException, Blob, crypto, Uint8Array, ArrayBuffer)); +;/* + * Copyright 2013, Nexedi SA + * Released under the LGPL license. + * http://www.gnu.org/licenses/lgpl.html + */ +/** + * JIO Websql Storage. Type = "websql". + * websql "database" storage. + */ +/*global Blob, jIO, RSVP, openDatabase*/ +/*jslint nomen: true*/ + +(function (jIO, RSVP, Blob, openDatabase) { + + "use strict"; + + /** + * The JIO Websql Storage extension + * + * @class WebSQLStorage + * @constructor + */ + + function queueSql(db, query_list, argument_list) { + return new RSVP.Promise(function (resolve, reject) { + /*jslint unparam: true*/ + db.transaction(function (tx) { + var len = query_list.length, + result_list = [], + i; + + function resolveTransaction(tx, result) { + result_list.push(result); + if (result_list.length === len) { + resolve(result_list); + } + } + function rejectTransaction(tx, error) { + reject(error); + return true; + } + for (i = 0; i < len; i += 1) { + tx.executeSql(query_list[i], argument_list[i], resolveTransaction, + rejectTransaction); + } + }, function (tx, error) { + reject(error); + }); + /*jslint unparam: false*/ + }); + } + + function initDatabase(db) { + var query_list = [ + "CREATE TABLE IF NOT EXISTS document" + + "(id VARCHAR PRIMARY KEY NOT NULL, data TEXT)", + "CREATE TABLE IF NOT EXISTS attachment" + + "(id VARCHAR, attachment VARCHAR, part INT, blob TEXT)", + "CREATE TRIGGER IF NOT EXISTS removeAttachment " + + "BEFORE DELETE ON document FOR EACH ROW " + + "BEGIN DELETE from attachment WHERE id = OLD.id;END;", + "CREATE INDEX IF NOT EXISTS index_document ON document (id);", + "CREATE INDEX IF NOT EXISTS index_attachment " + + "ON attachment (id, attachment);" + ]; + return new RSVP.Queue() + .push(function () { + return queueSql(db, query_list, []); + }); + } + + function WebSQLStorage(spec) { + if (typeof spec.database !== 'string' || !spec.database) { + throw new TypeError("database must be a string " + + "which contains more than one character."); + } + this._database = openDatabase("jio:" + spec.database, + '1.0', '', 2 * 1024 * 1024); + if (spec.blob_length && + (typeof spec.blob_length !== "number" || + spec.blob_length < 20)) { + throw new TypeError("blob_len parameter must be a number >= 20"); + } + this._blob_length = spec.blob_length || 2000000; + this._init_db_promise = initDatabase(this._database); + } + + WebSQLStorage.prototype.put = function (id, param) { + var db = this._database, + that = this, + data_string = JSON.stringify(param); + + return new RSVP.Queue() + .push(function () { + return that._init_db_promise; + }) + .push(function () { + return queueSql(db, ["INSERT OR REPLACE INTO " + + "document(id, data) VALUES(?,?)"], + [[id, data_string]]); + }) + .push(function () { + return id; + }); + }; + + WebSQLStorage.prototype.remove = function (id) { + var db = this._database, + that = this; + + return new RSVP.Queue() + .push(function () { + return that._init_db_promise; + }) + .push(function () { + return queueSql(db, ["DELETE FROM document WHERE id = ?"], [[id]]); + }) + .push(function (result_list) { + if (result_list[0].rowsAffected === 0) { + throw new jIO.util.jIOError("Cannot find document", 404); + } + return id; + }); + + }; + + WebSQLStorage.prototype.get = function (id) { + var db = this._database, + that = this; + + return new RSVP.Queue() + .push(function () { + return that._init_db_promise; + }) + .push(function () { + return queueSql(db, ["SELECT data FROM document WHERE id = ?"], + [[id]]); + }) + .push(function (result_list) { + if (result_list[0].rows.length === 0) { + throw new jIO.util.jIOError("Cannot find document", 404); + } + return JSON.parse(result_list[0].rows[0].data); + }); + }; + + WebSQLStorage.prototype.allAttachments = function (id) { + var db = this._database, + that = this; + + return new RSVP.Queue() + .push(function () { + return that._init_db_promise; + }) + .push(function () { + return queueSql(db, [ + "SELECT id FROM document WHERE id = ?", + "SELECT DISTINCT attachment FROM attachment WHERE id = ?" + ], [[id], [id]]); + }) + .push(function (result_list) { + if (result_list[0].rows.length === 0) { + throw new jIO.util.jIOError("Cannot find document", 404); + } + + var len = result_list[1].rows.length, + obj = {}, + i; + + for (i = 0; i < len; i += 1) { + obj[result_list[1].rows[i].attachment] = {}; + } + return obj; + }); + }; + + function sendBlobPart(blob, argument_list, index, queue) { + queue.push(function () { + return jIO.util.readBlobAsDataURL(blob); + }) + .push(function (strBlob) { + argument_list[index + 2].push(strBlob.target.result); + return; + }); + } + + WebSQLStorage.prototype.putAttachment = function (id, name, blob) { + var db = this._database, + that = this, + part_size = this._blob_length; + + return new RSVP.Queue() + .push(function () { + return that._init_db_promise; + }) + .push(function () { + return queueSql(db, ["SELECT id FROM document WHERE id = ?"], [[id]]); + }) + .push(function (result) { + var query_list = [], + argument_list = [], + blob_size = blob.size, + queue = new RSVP.Queue(), + i, + index; + + if (result[0].rows.length === 0) { + throw new jIO.util.jIOError("Cannot access subdocument", 404); + } + query_list.push("DELETE FROM attachment WHERE id = ? " + + "AND attachment = ?"); + argument_list.push([id, name]); + query_list.push("INSERT INTO attachment(id, attachment, part, blob)" + + "VALUES(?, ?, ?, ?)"); + argument_list.push([id, name, -1, + blob.type || "application/octet-stream"]); + + for (i = 0, index = 0; i < blob_size; i += part_size, index += 1) { + query_list.push("INSERT INTO attachment(id, attachment, part, blob)" + + "VALUES(?, ?, ?, ?)"); + argument_list.push([id, name, index]); + sendBlobPart(blob.slice(i, i + part_size), argument_list, index, + queue); + } + queue.push(function () { + return queueSql(db, query_list, argument_list); + }); + return queue; + }); + }; + + WebSQLStorage.prototype.getAttachment = function (id, name, options) { + var db = this._database, + that = this, + part_size = this._blob_length, + start, + end, + start_index, + end_index; + + if (options === undefined) { options = {}; } + start = options.start || 0; + end = options.end || -1; + + if (start < 0 || (options.end !== undefined && options.end < 0)) { + throw new jIO.util.jIOError("_start and _end must be positive", + 400); + } + if (start > end && end !== -1) { + throw new jIO.util.jIOError("_start is greater than _end", + 400); + } + + start_index = Math.floor(start / part_size); + if (start === 0) { start_index -= 1; } + end_index = Math.floor(end / part_size); + if (end % part_size === 0) { + end_index -= 1; + } + + return new RSVP.Queue() + .push(function () { + return that._init_db_promise; + }) + .push(function () { + var command = "SELECT part, blob FROM attachment WHERE id = ? AND " + + "attachment = ? AND part >= ?", + argument_list = [id, name, start_index]; + + if (end !== -1) { + command += " AND part <= ?"; + argument_list.push(end_index); + } + return queueSql(db, [command], [argument_list]); + }) + .push(function (response_list) { + var i, + response, + blob_array = [], + blob, + type; + + response = response_list[0].rows; + if (response.length === 0) { + throw new jIO.util.jIOError("Cannot find document", 404); + } + for (i = 0; i < response.length; i += 1) { + if (response[i].part === -1) { + type = response[i].blob; + start_index += 1; + } else { + blob_array.push(jIO.util.dataURItoBlob(response[i].blob)); + } + } + if ((start === 0) && (options.end === undefined)) { + return new Blob(blob_array, {type: type}); + } + blob = new Blob(blob_array, {}); + return blob.slice(start - (start_index * part_size), + end === -1 ? blob.size : + end - (start_index * part_size), + "application/octet-stream"); + }); + }; + + WebSQLStorage.prototype.removeAttachment = function (id, name) { + var db = this._database, + that = this; + + return new RSVP.Queue() + .push(function () { + return that._init_db_promise; + }) + .push(function () { + return queueSql(db, ["DELETE FROM attachment WHERE " + + "id = ? AND attachment = ?"], [[id, name]]); + }) + .push(function (result) { + if (result[0].rowsAffected === 0) { + throw new jIO.util.jIOError("Cannot find document", 404); + } + return name; + }); + }; + + WebSQLStorage.prototype.hasCapacity = function (name) { + return (name === "list" || (name === "include")); + }; + + WebSQLStorage.prototype.buildQuery = function (options) { + var db = this._database, + that = this, + query = "SELECT id"; + + return new RSVP.Queue() + .push(function () { + return that._init_db_promise; + }) + .push(function () { + if (options === undefined) { options = {}; } + if (options.include_docs === true) { + query += ", data AS doc"; + } + query += " FROM document"; + return queueSql(db, [query], [[]]); + }) + .push(function (result) { + var array = [], + len = result[0].rows.length, + i; + + for (i = 0; i < len; i += 1) { + array.push(result[0].rows[i]); + array[i].value = {}; + if (array[i].doc !== undefined) { + array[i].doc = JSON.parse(array[i].doc); + } + } + return array; + }); + }; + + jIO.addStorage('websql', WebSQLStorage); + +}(jIO, RSVP, Blob, openDatabase)); +;/*jslint nomen: true */ +/*global RSVP, UriTemplate*/ +(function (jIO, RSVP, UriTemplate) { + "use strict"; + + var GET_POST_URL = "https://graph.facebook.com/v2.9/{+post_id}" + + "?fields={+fields}&access_token={+access_token}", + get_post_template = UriTemplate.parse(GET_POST_URL), + GET_FEED_URL = "https://graph.facebook.com/v2.9/{+user_id}/feed" + + "?fields={+fields}&limit={+limit}&since={+since}&access_token=" + + "{+access_token}", + get_feed_template = UriTemplate.parse(GET_FEED_URL); + + function FBStorage(spec) { + if (typeof spec.access_token !== 'string' || !spec.access_token) { + throw new TypeError("Access Token must be a string " + + "which contains more than one character."); + } + if (typeof spec.user_id !== 'string' || !spec.user_id) { + throw new TypeError("User ID must be a string " + + "which contains more than one character."); + } + this._access_token = spec.access_token; + this._user_id = spec.user_id; + this._default_field_list = spec.default_field_list || []; + this._default_limit = spec.default_limit || 500; + } + + FBStorage.prototype.get = function (id) { + var that = this; + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + type: "GET", + url: get_post_template.expand({post_id: id, + fields: that._default_field_list, access_token: that._access_token}) + }); + }) + .push(function (result) { + return JSON.parse(result.target.responseText); + }); + }; + + function paginateResult(url, result, select_list) { + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({ + type: "GET", + url: url + }); + }) + .push(function (response) { + return JSON.parse(response.target.responseText); + }, + function (err) { + throw new jIO.util.jIOError("Getting feed failed " + err.toString(), + err.target.status); + }) + .push(function (response) { + if (response.data.length === 0) { + return result; + } + var i, j, obj = {}; + for (i = 0; i < response.data.length; i += 1) { + obj.id = response.data[i].id; + obj.value = {}; + for (j = 0; j < select_list.length; j += 1) { + obj.value[select_list[j]] = response.data[i][select_list[j]]; + } + result.push(obj); + obj = {}; + } + return paginateResult(response.paging.next, result, select_list); + }); + } + + FBStorage.prototype.buildQuery = function (query) { + var that = this, fields = [], limit = this._default_limit, + template_argument = { + user_id: this._user_id, + limit: limit, + access_token: this._access_token + }; + if (query.include_docs) { + fields = fields.concat(that._default_field_list); + } + if (query.select_list) { + fields = fields.concat(query.select_list); + } + if (query.limit) { + limit = query.limit[1]; + } + template_argument.fields = fields; + template_argument.limit = limit; + return paginateResult(get_feed_template.expand(template_argument), [], + fields) + .push(function (result) { + if (!query.limit) { + return result; + } + return result.slice(query.limit[0], query.limit[1]); + }); + }; + + FBStorage.prototype.hasCapacity = function (name) { + var this_storage_capacity_list = ["list", "select", "include", "limit"]; + if (this_storage_capacity_list.indexOf(name) !== -1) { + return true; + } + }; + + jIO.addStorage('facebook', FBStorage); + +}(jIO, RSVP, UriTemplate)); \ No newline at end of file -- 2.30.9 From 0a16746e5d70a8b0e03a586a13fb7389558f8fbe Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Wed, 23 May 2018 16:06:36 +0000 Subject: [PATCH 02/46] Added src/jio.storage/bryanstorage.js and test/jio.storage/bryanstorage.tests.js for revision storage implementation. --- Gruntfile.js | 3 +- src/jio.storage/bryanstorage.js | 53 ++ test/jio.storage/bryanstorage.tests.js | 977 +++++++++++++++++++++++++ test/tests.html | 2 + 4 files changed, 1034 insertions(+), 1 deletion(-) create mode 100644 src/jio.storage/bryanstorage.js create mode 100644 test/jio.storage/bryanstorage.tests.js diff --git a/Gruntfile.js b/Gruntfile.js index 980c17f..7edb35e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -182,7 +182,8 @@ module.exports = function (grunt) { 'src/jio.storage/indexeddbstorage.js', 'src/jio.storage/cryptstorage.js', 'src/jio.storage/websqlstorage.js', - 'src/jio.storage/fbstorage.js' + 'src/jio.storage/fbstorage.js', + 'src/jio.storage/bryanstorage.js' ], dest: 'dist/<%= pkg.name %>-<%= pkg.version %>.js' // dest: 'jio.js' diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js new file mode 100644 index 0000000..c11d5f5 --- /dev/null +++ b/src/jio.storage/bryanstorage.js @@ -0,0 +1,53 @@ +/*jslint nomen: true*/ +/*global RSVP, jiodate*/ +(function (jIO) { + "use strict"; + + /** + * The jIO BryanStorage extension + * + * @class BryanStorage + * @constructor + */ + function BryanStorage(spec) { + this._sub_storage = jIO.createJIO(spec.sub_storage); + } + + BryanStorage.prototype.get = function () { + return this._sub_storage.get.apply(this._sub_storage, arguments); + }; + BryanStorage.prototype.allAttachments = function () { + return this._sub_storage.allAttachments.apply(this._sub_storage, arguments); + }; + BryanStorage.prototype.post = function () { + return this._sub_storage.post.apply(this._sub_storage, arguments); + }; + BryanStorage.prototype.put = function () { + return this._sub_storage.put.apply(this._sub_storage, arguments); + }; + BryanStorage.prototype.remove = function () { + return this._sub_storage.remove.apply(this._sub_storage, arguments); + }; + BryanStorage.prototype.getAttachment = function () { + return this._sub_storage.getAttachment.apply(this._sub_storage, arguments); + }; + BryanStorage.prototype.putAttachment = function () { + return this._sub_storage.putAttachment.apply(this._sub_storage, arguments); + }; + BryanStorage.prototype.removeAttachment = function () { + return this._sub_storage.removeAttachment.apply(this._sub_storage, + arguments); + }; + BryanStorage.prototype.repair = function () { + return this._sub_storage.repair.apply(this._sub_storage, arguments); + }; + BryanStorage.prototype.hasCapacity = function (name) { + return this._sub_storage.removeAttachment.apply(this._sub_storage, name); + }; + BryanStorage.prototype.buildQuery = function (options) { + return this._sub_storage.removeAttachment.apply(this._sub_storage, options); + }; + + jIO.addStorage('bryan', BryanStorage); + +}(jIO)); diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js new file mode 100644 index 0000000..3dee743 --- /dev/null +++ b/test/jio.storage/bryanstorage.tests.js @@ -0,0 +1,977 @@ +/*jslint nomen: true*/ +/*global Blob, jiodate*/ +(function (jIO, QUnit, Blob) { + "use strict"; + var test = QUnit.test, + stop = QUnit.stop, + start = QUnit.start, + ok = QUnit.ok, + expect = QUnit.expect, + deepEqual = QUnit.deepEqual, + equal = QUnit.equal, + module = QUnit.module, + throws = QUnit.throws; + + ///////////////////////////////////////////////////////////////// + // Custom test substorage definition + ///////////////////////////////////////////////////////////////// + function Storage200() { + return this; + } + jIO.addStorage('indexeddb', Storage200); + + ///////////////////////////////////////////////////////////////// + // bryanStorage.constructor + ///////////////////////////////////////////////////////////////// + module("bryanStorage.constructor"); + test("accept parameters", function () { + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "indexeddb" + } + }); + + ok(jio.__storage._sub_storage instanceof jio.constructor); + equal(jio.__storage._sub_storage.__type, "indexeddb"); + }); + + test("failed on wrong schema", function () { + throws( + function () { + jIO.createJIO({ + type: "bryan", + schema: {'date': {type: 'couscous'}}, + sub_storage: { + type: "indexeddb" + } + }); + }, + function (error) { + ok(error instanceof jIO.util.jIOError); + equal(error.status_code, 400); + equal(error.message, + "Wrong schema for property: date"); + return true; + } + ); + }); + + ///////////////////////////////////////////////////////////////// + // bryanStorage.get + ///////////////////////////////////////////////////////////////// + module("bryanStorage.get"); + test("get called substorage get", function () { + stop(); + expect(2); + + // create storage of type "bryan" with indexeddb as substorage + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "indexeddb" + } + }); + + + Storage200.prototype.get = function (id) { + equal(id, "bar", "get 200 called"); + return {title: "foo"}; + }; + + // jio.get uses the Storage200 .get() implementation defined immediately + // above + jio.get("bar") + .then(function (result) { + deepEqual(result, { + "title": "foo" + }, "Check document"); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + ///////////////////////////////////////////////////////////////// + // bryanStorage.allAttachments + ///////////////////////////////////////////////////////////////// + module("bryanStorage.allAttachments"); + test("allAttachments called substorage allAttachments", function () { + stop(); + expect(2); + + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "indexeddb" + } + }); + + Storage200.prototype.allAttachments = function (id) { + equal(id, "bar", "allAttachments, 200 called"); + return {attachmentname: {}}; + }; + + jio.allAttachments("bar") + .then(function (result) { + deepEqual(result, { + attachmentname: {} + }, "Check document"); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + ///////////////////////////////////////////////////////////////// + // bryanStorage.post + ///////////////////////////////////////////////////////////////// + module("bryanStorage.post"); + test("post called substorage post", function () { + stop(); + expect(2); + + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "indexeddb" + } + }); + + Storage200.prototype.post = function (param) { + deepEqual(param, {"title": "foo"}, "post 200 called"); + return "youhou"; + }; + + jio.post({"title": "foo"}) + .then(function (result) { + equal(result, "youhou"); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + ///////////////////////////////////////////////////////////////// + // bryanStorage.put + ///////////////////////////////////////////////////////////////// + module("bryanStorage.put"); + test("put called substorage put", function () { + stop(); + expect(3); + + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "indexeddb" + } + }); + Storage200.prototype.put = function (id, param) { + equal(id, "bar", "put 200 called"); + deepEqual(param, {"title": "foo"}, "put 200 called"); + return id; + }; + + // If .put does not give the appropriate return, fail assertion + jio.put("bar", {"title": "foo"}) + .then(function (result) { + equal(result, "bar"); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + ///////////////////////////////////////////////////////////////// + // bryanStorage.remove + ///////////////////////////////////////////////////////////////// + module("bryanStorage.remove"); + test("remove called substorage remove", function () { + stop(); + expect(2); + + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "indexeddb" + } + }); + Storage200.prototype.remove = function (id) { + deepEqual(id, "bar", "remove 200 called"); + return id; + }; + + jio.remove("bar") + .then(function (result) { + equal(result, "bar"); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + ///////////////////////////////////////////////////////////////// + // bryanStorage.getAttachment + ///////////////////////////////////////////////////////////////// + module("bryanStorage.getAttachment"); + test("getAttachment called substorage getAttachment", function () { + stop(); + expect(3); + + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "indexeddb" + } + }), + blob = new Blob([""]); + + Storage200.prototype.getAttachment = function (id, name) { + equal(id, "bar", "getAttachment 200 called"); + equal(name, "foo", "getAttachment 200 called"); + return blob; + }; + + jio.getAttachment("bar", "foo") + .then(function (result) { + equal(result, blob); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + ///////////////////////////////////////////////////////////////// + // bryanStorage.putAttachment + ///////////////////////////////////////////////////////////////// + module("bryanStorage.putAttachment"); + test("putAttachment called substorage putAttachment", function () { + stop(); + expect(4); + + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "indexeddb" + } + }), + blob = new Blob([""]); + + Storage200.prototype.putAttachment = function (id, name, blob2) { + equal(id, "bar", "putAttachment 200 called"); + equal(name, "foo", "putAttachment 200 called"); + deepEqual(blob2, blob, + "putAttachment 200 called"); + return "OK"; + }; + + jio.putAttachment("bar", "foo", blob) + .then(function (result) { + equal(result, "OK"); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + ///////////////////////////////////////////////////////////////// + // bryanStorage.removeAttachment + ///////////////////////////////////////////////////////////////// + module("bryanStorage.removeAttachment"); + test("removeAttachment called substorage removeAttachment", function () { + stop(); + expect(3); + + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "indexeddb" + } + }); + + Storage200.prototype.removeAttachment = function (id, name) { + equal(id, "bar", "removeAttachment 200 called"); + equal(name, "foo", "removeAttachment 200 called"); + return "Removed"; + }; + + jio.removeAttachment("bar", "foo") + .then(function (result) { + equal(result, "Removed"); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + ///////////////////////////////////////////////////////////////// + // bryanStorage.hasCapacity + ///////////////////////////////////////////////////////////////// + module("bryanStorage.hasCapacity"); + test("hasCapacity is false by default", function () { + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "indexeddb" + } + }); + + throws( + function () { + jio.hasCapacity("foo"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError); + equal(error.status_code, 501); + equal(error.message, + "Capacity 'foo' is not implemented on 'bryan'"); + return true; + } + ); + }); + + test("hasCapacity list return substorage value", function () { + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "indexeddb" + } + }); + + throws( + function () { + jio.hasCapacity("list"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError); + equal(error.status_code, 501); + equal(error.message, + "Capacity 'list' is not implemented on 'indexeddb'"); + return true; + } + ); + }); + + ///////////////////////////////////////////////////////////////// + // bryanStorage.buildbryan + ///////////////////////////////////////////////////////////////// + module("bryanStorage.buildbryan"); + + test("substorage should have 'list' capacity", function () { + stop(); + expect(3); + + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "indexeddb" + } + }); + + jio.allDocs({ + include_docs: true, + bryan: 'title: "two"' + }) + .then(function () { + ok(false); + }) + .fail(function (error) { + ok(error instanceof jIO.util.jIOError); + equal(error.status_code, 501); + equal(error.message, + "Capacity 'list' is not implemented on 'indexeddb'"); + }) + .always(function () { + start(); + }); + }); + + test("no manual bryan if substorage handle everything", function () { + stop(); + expect(2); + + function StorageAllDocsNoGet() { + return this; + } + StorageAllDocsNoGet.prototype.get = function () { + throw new Error("Unexpected get call"); + }; + StorageAllDocsNoGet.prototype.hasCapacity = function (capacity) { + if ((capacity === "list") || + (capacity === "sort") || + (capacity === "select") || + (capacity === "limit") || + (capacity === "bryan")) { + return true; + } + throw new Error("Unexpected " + capacity + " capacity check"); + }; + StorageAllDocsNoGet.prototype.buildbryan = function (options) { + deepEqual(options, { + sort_on: [["title", "ascending"]], + limit: [5], + select_list: ["title", "id"], + bryan: 'title: "two"' + }, + "buildbryan called"); + return "taboulet"; + }; + + jIO.addStorage('bryanStoragealldocsnoget', StorageAllDocsNoGet); + + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "bryanStoragealldocsnoget" + } + }); + + jio.allDocs({ + sort_on: [["title", "ascending"]], + limit: [5], + select_list: ["title", "id"], + bryan: 'title: "two"' + }) + .then(function (result) { + deepEqual(result, { + data: { + rows: "taboulet", + total_rows: 8 + } + }); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + test("manual bryan used if substorage does not handle sort", function () { + stop(); + expect(4); + + function StorageNoSortCapacity() { + return this; + } + StorageNoSortCapacity.prototype.get = function (id) { + if (id === "foo") { + equal(id, "foo", "Get foo"); + } else { + equal(id, "bar", "Get bar"); + } + return {title: id, id: "ID " + id, + "another": "property"}; + }; + StorageNoSortCapacity.prototype.hasCapacity = function (capacity) { + if ((capacity === "list") || + (capacity === "select") || + (capacity === "limit") || + (capacity === "bryan")) { + return true; + } + return false; + }; + StorageNoSortCapacity.prototype.buildbryan = function (options) { + deepEqual(options, {}, "No bryan parameter"); + var result2 = [{ + id: "foo", + value: {} + }, { + id: "bar", + value: {} + }]; + return result2; + }; + + jIO.addStorage('bryanStoragenosortcapacity', StorageNoSortCapacity); + + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "bryanStoragenosortcapacity" + } + }); + + jio.allDocs({ + sort_on: [["title", "ascending"]], + limit: [0, 5], + select_list: ["title", "id"], + bryan: 'title: "foo"' + }) + .then(function (result) { + deepEqual(result, { + data: { + rows: [{ + id: "foo", + doc: {}, + value: { + title: "foo", + id: "ID foo" + } + }], + total_rows: 1 + } + }); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + test("manual bryan used if substorage does not handle select", function () { + stop(); + expect(4); + + function StorageNoSelectCapacity() { + return this; + } + StorageNoSelectCapacity.prototype.get = function (id) { + if (id === "foo") { + equal(id, "foo", "Get foo"); + } else { + equal(id, "bar", "Get bar"); + } + return {title: id, id: "ID " + id, + "another": "property"}; + }; + StorageNoSelectCapacity.prototype.hasCapacity = function (capacity) { + if ((capacity === "list") || + (capacity === "sort") || + (capacity === "limit") || + (capacity === "bryan")) { + return true; + } + return false; + }; + StorageNoSelectCapacity.prototype.buildbryan = function (options) { + deepEqual(options, {}, "No bryan parameter"); + var result2 = [{ + id: "foo", + value: {} + }, { + id: "bar", + value: {} + }]; + return result2; + }; + + jIO.addStorage('bryanStoragenoselectcapacity', StorageNoSelectCapacity); + + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "bryanStoragenoselectcapacity" + } + }); + + jio.allDocs({ + sort_on: [["title", "ascending"]], + limit: [0, 5], + select_list: ["title", "id"], + bryan: 'title: "foo"' + }) + .then(function (result) { + deepEqual(result, { + data: { + rows: [{ + id: "foo", + doc: {}, + value: { + title: "foo", + id: "ID foo" + } + }], + total_rows: 1 + } + }); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + test("manual bryan used if substorage does not handle limit", function () { + stop(); + expect(4); + + function StorageNoLimitCapacity() { + return this; + } + StorageNoLimitCapacity.prototype.get = function (id) { + if (id === "foo") { + equal(id, "foo", "Get foo"); + } else { + equal(id, "bar", "Get bar"); + } + return {title: id, id: "ID " + id, + "another": "property"}; + }; + StorageNoLimitCapacity.prototype.hasCapacity = function (capacity) { + if ((capacity === "list") || + (capacity === "select") || + (capacity === "sort") || + (capacity === "bryan")) { + return true; + } + return false; + }; + StorageNoLimitCapacity.prototype.buildbryan = function (options) { + deepEqual(options, {}, "No bryan parameter"); + var result2 = [{ + id: "foo", + value: {} + }, { + id: "bar", + value: {} + }]; + return result2; + }; + + jIO.addStorage('bryanStoragenolimitcapacity', StorageNoLimitCapacity); + + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "bryanStoragenolimitcapacity" + } + }); + + jio.allDocs({ + sort_on: [["title", "ascending"]], + limit: [0, 5], + select_list: ["title", "id"], + bryan: 'title: "foo"' + }) + .then(function (result) { + deepEqual(result, { + data: { + rows: [{ + id: "foo", + doc: {}, + value: { + title: "foo", + id: "ID foo" + } + }], + total_rows: 1 + } + }); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + test("manual bryan used if substorage does not handle bryan", function () { + stop(); + expect(4); + + function StorageNobryanCapacity() { + return this; + } + StorageNobryanCapacity.prototype.get = function (id) { + if (id === "foo") { + equal(id, "foo", "Get foo"); + } else { + equal(id, "bar", "Get bar"); + } + return {title: id, id: "ID " + id, + "another": "property"}; + }; + StorageNobryanCapacity.prototype.hasCapacity = function (capacity) { + if ((capacity === "list") || + (capacity === "select") || + (capacity === "limit") || + (capacity === "sort")) { + return true; + } + return false; + }; + StorageNobryanCapacity.prototype.buildbryan = function (options) { + deepEqual(options, {}, "No bryan parameter"); + var result2 = [{ + id: "foo", + value: {} + }, { + id: "bar", + value: {} + }]; + return result2; + }; + + jIO.addStorage('bryanStoragenobryancapacity', StorageNobryanCapacity); + + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "bryanStoragenobryancapacity" + } + }); + + jio.allDocs({ + sort_on: [["title", "ascending"]], + limit: [0, 5], + select_list: ["title", "id"], + bryan: 'title: "foo"' + }) + .then(function (result) { + deepEqual(result, { + data: { + rows: [{ + id: "foo", + doc: {}, + value: { + title: "foo", + id: "ID foo" + } + }], + total_rows: 1 + } + }); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + test("does not fetch doc one by one if substorage handle include_docs", + function () { + stop(); + expect(2); + + function StorageIncludeDocsCapacity() { + return this; + } + StorageIncludeDocsCapacity.prototype.hasCapacity = function (capacity) { + if ((capacity === "list") || + (capacity === "include")) { + return true; + } + return false; + }; + StorageIncludeDocsCapacity.prototype.buildbryan = function (options) { + deepEqual(options, {include_docs: true}, "Include docs parameter"); + var result2 = [{ + id: "foo", + value: {}, + doc: { + title: "foo", + id: "ID foo", + another: "property" + } + }, { + id: "bar", + value: {}, + doc: { + title: "bar", + id: "ID bar", + another: "property" + } + }]; + return result2; + }; + + jIO.addStorage('bryanStorageincludedocscapacity', + StorageIncludeDocsCapacity); + + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "bryanStorageincludedocscapacity" + } + }); + + jio.allDocs({ + sort_on: [["title", "ascending"]], + limit: [0, 5], + select_list: ["title", "id"], + bryan: 'title: "foo"' + }) + .then(function (result) { + deepEqual(result, { + data: { + rows: [{ + id: "foo", + doc: {}, + value: { + title: "foo", + id: "ID foo" + } + }], + total_rows: 1 + } + }); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + test("manual bryan used and use schema", function () { + stop(); + expect(4); + + function StorageSchemaCapacity() { + return this; + } + StorageSchemaCapacity.prototype.get = function (id) { + var doc = { + title: id, + id: "ID " + id, + "another": "property" + }; + if (id === "foo") { + equal(id, "foo", "Get foo"); + doc.modification_date = "Fri, 08 Sep 2017 07:46:27 +0000"; + } else { + equal(id, "bar", "Get bar"); + doc.modification_date = "Thu, 07 Sep 2017 18:59:23 +0000"; + } + return doc; + }; + + StorageSchemaCapacity.prototype.hasCapacity = function (capacity) { + if ((capacity === "list")) { + return true; + } + return false; + }; + StorageSchemaCapacity.prototype.buildbryan = function (options) { + deepEqual(options, {}, "No bryan parameter"); + var result2 = [{ + id: "foo", + value: {} + }, { + id: "bar", + value: {} + }]; + return result2; + }; + + jIO.addStorage( + 'bryanStoragenoschemacapacity', + StorageSchemaCapacity + ); + + var jio = jIO.createJIO({ + type: "bryan", + schema: { + "modification_date": { + "type": "string", + "format": "date-time" + } + }, + sub_storage: { + type: "bryanStoragenoschemacapacity" + } + }); + + jio.allDocs({ + sort_on: [["modification_date", "descending"]], + limit: [0, 5], + select_list: ['modification_date'] + }) + .then(function (result) { + deepEqual(result, { + data: { + rows: [ + { + id: "foo", + doc: {}, + value: { + modification_date: "Fri, 08 Sep 2017 07:46:27 +0000" + } + }, { + id: "bar", + doc: {}, + value: { + modification_date: "Thu, 07 Sep 2017 18:59:23 +0000" + } + } + ], + total_rows: 2 + } + }); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + ///////////////////////////////////////////////////////////////// + // bryanStorage.repair + ///////////////////////////////////////////////////////////////// + module("bryanStorage.repair"); + test("repair called substorage repair", function () { + stop(); + expect(2); + + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "indexeddb" + } + }), + expected_options = {foo: "bar"}; + + Storage200.prototype.repair = function (options) { + deepEqual(options, expected_options, "repair 200 called"); + return "OK"; + }; + + jio.repair(expected_options) + .then(function (result) { + equal(result, "OK"); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + +}(jIO, QUnit, Blob)); diff --git a/test/tests.html b/test/tests.html index 7b484b1..00ba5db 100644 --- a/test/tests.html +++ b/test/tests.html @@ -59,6 +59,8 @@ + + -- 2.30.9 From 6f69309438f44c66450e7559f285557ca6acc220 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Thu, 24 May 2018 14:49:59 +0000 Subject: [PATCH 03/46] Added _ parameter to metadata of bryanstorage documents, which increments each time put, putAttachment, or removeAttachment is called on the document. Created tests to test these features. --- src/jio.storage/bryanstorage.js | 73 +++++- test/jio.storage/bryanstorage.tests.js | 293 ++++++++++++++++--------- 2 files changed, 257 insertions(+), 109 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index c11d5f5..9d92321 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -16,28 +16,87 @@ BryanStorage.prototype.get = function () { return this._sub_storage.get.apply(this._sub_storage, arguments); }; + BryanStorage.prototype.allAttachments = function () { return this._sub_storage.allAttachments.apply(this._sub_storage, arguments); }; + + // Not implemented for IndexedDB BryanStorage.prototype.post = function () { return this._sub_storage.post.apply(this._sub_storage, arguments); }; - BryanStorage.prototype.put = function () { - return this._sub_storage.put.apply(this._sub_storage, arguments); + + BryanStorage.prototype.put = function (id, new_metadata) { + var substorage = this._sub_storage; + + return this.get(id) + .push( + function (metadata) { + // Increments existing "_revision" attribute + new_metadata._revision = metadata._revision + 1; + return substorage.put(id, new_metadata); + }, + function () { + // Creates new attribute "_revision" = 0 + new_metadata._revision = 0; + return substorage.put(id, new_metadata); + } + ); }; + BryanStorage.prototype.remove = function () { return this._sub_storage.remove.apply(this._sub_storage, arguments); }; BryanStorage.prototype.getAttachment = function () { return this._sub_storage.getAttachment.apply(this._sub_storage, arguments); }; - BryanStorage.prototype.putAttachment = function () { - return this._sub_storage.putAttachment.apply(this._sub_storage, arguments); + BryanStorage.prototype.putAttachment = function (id, name, data) { + + // Save pointer to substorage for use in nested function + var substorage = this._sub_storage; + + // First, get document metadata to update "_revision" + return this.get(id, name) + + // Increment "_revision" parameter in document + .push(function (metadata) { + var new_metadata = metadata; + + // "_revision" is guaranteed to exist since the document already exists + new_metadata._revision = metadata._revision + 1; + return substorage.put(id, new_metadata); + }) + + // After metadata updates successfully, perform putAttachment + .push(function () { + return substorage.putAttachment(id, name, data); + }); }; - BryanStorage.prototype.removeAttachment = function () { - return this._sub_storage.removeAttachment.apply(this._sub_storage, - arguments); + + BryanStorage.prototype.removeAttachment = function (id, name) { + + // Save pointer to substorage for use in nested function + var substorage = this._sub_storage; + + // First, get document metadata to update "_revision" + return this.get(id, name) + + // Increment "_revision" parameter in document + .push(function (metadata) { + var new_metadata = metadata; + + // "_revision" is guaranteed to exist since the document already exists + new_metadata._revision = metadata._revision + 1; + return substorage.put(id, new_metadata); + }) + + // After metadata updates successfully, perform removeAttachment + .push(function () { + return substorage.removeAttachment(id, name); + }); }; + + // Not implemented for IndexedDB BryanStorage.prototype.repair = function () { return this._sub_storage.repair.apply(this._sub_storage, arguments); }; diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index 3dee743..b94bd35 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -12,13 +12,6 @@ module = QUnit.module, throws = QUnit.throws; - ///////////////////////////////////////////////////////////////// - // Custom test substorage definition - ///////////////////////////////////////////////////////////////// - function Storage200() { - return this; - } - jIO.addStorage('indexeddb', Storage200); ///////////////////////////////////////////////////////////////// // bryanStorage.constructor @@ -28,34 +21,14 @@ var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "indexeddb" + type: "memory" } }); ok(jio.__storage._sub_storage instanceof jio.constructor); - equal(jio.__storage._sub_storage.__type, "indexeddb"); + equal(jio.__storage._sub_storage.__type, "memory"); }); - test("failed on wrong schema", function () { - throws( - function () { - jIO.createJIO({ - type: "bryan", - schema: {'date': {type: 'couscous'}}, - sub_storage: { - type: "indexeddb" - } - }); - }, - function (error) { - ok(error instanceof jIO.util.jIOError); - equal(error.status_code, 400); - equal(error.message, - "Wrong schema for property: date"); - return true; - } - ); - }); ///////////////////////////////////////////////////////////////// // bryanStorage.get @@ -65,26 +38,19 @@ stop(); expect(2); - // create storage of type "bryan" with indexeddb as substorage + // create storage of type "bryan" with memory as substorage var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "indexeddb" + type: "memory" } }); - - - Storage200.prototype.get = function (id) { - equal(id, "bar", "get 200 called"); - return {title: "foo"}; - }; - - // jio.get uses the Storage200 .get() implementation defined immediately - // above + jio.put("bar", {"title": "foo"}); jio.get("bar") .then(function (result) { deepEqual(result, { - "title": "foo" + "title": "foo", + "_revision": 0 }, "Check document"); }) .fail(function (error) { @@ -93,8 +59,172 @@ .always(function () { start(); }); + + jio.get("bar") + .then(function (result) { + deepEqual(result, { + "title": "foo", + "_revision": 0 + }, "Check document"); + }) + .fail(function (error) { + ok(false, error); + }); + //.always(function () { + // start(); + //}); + }); + + ///////////////////////////////////////////////////////////////// + // _revision parameter initialization + ///////////////////////////////////////////////////////////////// + module("bryanStorage initialize _revision"); + test("verifying _revision updates correctly", function () { + stop(); + expect(2); + + // create storage of type "bryan" with memory as substorage + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "memory" + } + }); + jio.put("bar", {"title": "foo"}) + .push(function (result) { + equal(result, "bar"); + return jio.get("bar"); + }) + .push(function (result) { + deepEqual(result, { + "title": "foo", + "_revision": 0 + }, "Check document"); + }) + .fail(function (error) {ok(false, error); }) + .always(function () {start(); }); + }); + + + ///////////////////////////////////////////////////////////////// + // _revision parameter updating with put + ///////////////////////////////////////////////////////////////// + module("bryanStorage _revision with put"); + test("verifying _revision updates correctly", function () { + stop(); + expect(1); + + // create storage of type "bryan" with memory as substorage + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: {type: "memory"} + }); + jio.put("bar", {"title": "foo"}) + .push(function () {return jio.put("bar", {"title2": "foo2"}); }) + .push(function () {return jio.put("bar", {"title3": "foo3"}); }) + .push(function () {return jio.get("bar"); }) + .push(function (result) { + deepEqual(result, { + "title3": "foo3", + "_revision": 2 + }, "Check document after initialization"); + }) + .fail(function (error) {ok(false, error); }) + .always(function () {start(); }); }); + ///////////////////////////////////////////////////////////////// + // _revision parameter updating with putAttachment + ///////////////////////////////////////////////////////////////// + module("bryanStorage _revision with putAttachment"); + test("verifying _revision updates correctly after putAttachment", + function () { + stop(); + expect(1); + + // Create storage of type "bryan" with memory as substorage + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: {type: "memory"} + }); + + jio.put("bar", {"title": "foo"}) + + // Put two unique attachments in the document + .push(function () { + return jio.putAttachment( + "bar", + "blob", + new Blob(["text data"], {type: "text/plain"}) + ); + }) + .push(function () { + return jio.putAttachment( + "bar", + "blob2", + new Blob(["more text data"], {type: "text/plain"}) + ); + }) + + // Get metadata for document + .push(function () {return jio.get("bar"); }) + + // Verify "_revision" is incremented twice + .push(function (result) { + deepEqual(result, { + "title": "foo", + "_revision": 2 + }, "Check document after 2 revisions"); + }) + .fail(function (error) {ok(false, error); }) + .always(function () {start(); }); + } + ); + + ///////////////////////////////////////////////////////////////// + // _revision parameter updating with removeAttachment + ///////////////////////////////////////////////////////////////// + module("bryanStorage _revision with removeAttachment"); + test("verifying _revision updates correctly after removeAttachment", + function () { + stop(); + expect(1); + + // create storage of type "bryan" with memory as substorage + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: {type: "memory"} + }); + + jio.put("bar", {"title": "foo"}) + .push(function () { + return jio.putAttachment( + "bar", + "blob", + new Blob(["text data"], {type: "text/plain"}) + ); + }) + .push(function () { + return jio.putAttachment( + "bar", + "blob2", + new Blob(["more text data"], {type: "text/plain"}) + ); + }) + .push(function () {return jio.removeAttachment("bar", "blob"); }) + .push(function () {return jio.removeAttachment("bar", "blob2"); }) + .push(function () {return jio.get("bar"); }) + .push(function (result) { + deepEqual(result, { + "title": "foo", + "_revision": 4 + }, "Check document after 4 revisions"); + }) + .fail(function (error) {ok(false, error); }) + .always(function () {start(); }); + } + ); + ///////////////////////////////////////////////////////////////// // bryanStorage.allAttachments ///////////////////////////////////////////////////////////////// @@ -106,17 +236,12 @@ var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "indexeddb" + type: "memory" } }); - Storage200.prototype.allAttachments = function (id) { - equal(id, "bar", "allAttachments, 200 called"); - return {attachmentname: {}}; - }; - jio.allAttachments("bar") - .then(function (result) { + .push(function (result) { deepEqual(result, { attachmentname: {} }, "Check document"); @@ -129,6 +254,7 @@ }); }); + ///////////////////////////////////////////////////////////////// // bryanStorage.post ///////////////////////////////////////////////////////////////// @@ -140,17 +266,12 @@ var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "indexeddb" + type: "memory" } }); - Storage200.prototype.post = function (param) { - deepEqual(param, {"title": "foo"}, "post 200 called"); - return "youhou"; - }; - jio.post({"title": "foo"}) - .then(function (result) { + .push(function (result) { equal(result, "youhou"); }) .fail(function (error) { @@ -161,6 +282,7 @@ }); }); + ///////////////////////////////////////////////////////////////// // bryanStorage.put ///////////////////////////////////////////////////////////////// @@ -172,18 +294,13 @@ var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "indexeddb" + type: "memory" } }); - Storage200.prototype.put = function (id, param) { - equal(id, "bar", "put 200 called"); - deepEqual(param, {"title": "foo"}, "put 200 called"); - return id; - }; // If .put does not give the appropriate return, fail assertion jio.put("bar", {"title": "foo"}) - .then(function (result) { + .push(function (result) { equal(result, "bar"); }) .fail(function (error) { @@ -194,6 +311,7 @@ }); }); + ///////////////////////////////////////////////////////////////// // bryanStorage.remove ///////////////////////////////////////////////////////////////// @@ -205,16 +323,12 @@ var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "indexeddb" + type: "memory" } }); - Storage200.prototype.remove = function (id) { - deepEqual(id, "bar", "remove 200 called"); - return id; - }; jio.remove("bar") - .then(function (result) { + .push(function (result) { equal(result, "bar"); }) .fail(function (error) { @@ -236,17 +350,11 @@ var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "indexeddb" + type: "memory" } }), blob = new Blob([""]); - Storage200.prototype.getAttachment = function (id, name) { - equal(id, "bar", "getAttachment 200 called"); - equal(name, "foo", "getAttachment 200 called"); - return blob; - }; - jio.getAttachment("bar", "foo") .then(function (result) { equal(result, blob); @@ -270,19 +378,11 @@ var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "indexeddb" + type: "memory" } }), blob = new Blob([""]); - Storage200.prototype.putAttachment = function (id, name, blob2) { - equal(id, "bar", "putAttachment 200 called"); - equal(name, "foo", "putAttachment 200 called"); - deepEqual(blob2, blob, - "putAttachment 200 called"); - return "OK"; - }; - jio.putAttachment("bar", "foo", blob) .then(function (result) { equal(result, "OK"); @@ -306,16 +406,10 @@ var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "indexeddb" + type: "memory" } }); - Storage200.prototype.removeAttachment = function (id, name) { - equal(id, "bar", "removeAttachment 200 called"); - equal(name, "foo", "removeAttachment 200 called"); - return "Removed"; - }; - jio.removeAttachment("bar", "foo") .then(function (result) { equal(result, "Removed"); @@ -336,7 +430,7 @@ var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "indexeddb" + type: "memory" } }); @@ -358,7 +452,7 @@ var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "indexeddb" + type: "memory" } }); @@ -370,7 +464,7 @@ ok(error instanceof jIO.util.jIOError); equal(error.status_code, 501); equal(error.message, - "Capacity 'list' is not implemented on 'indexeddb'"); + "Capacity 'list' is not implemented on 'memory'"); return true; } ); @@ -388,7 +482,7 @@ var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "indexeddb" + type: "memory" } }); @@ -403,7 +497,7 @@ ok(error instanceof jIO.util.jIOError); equal(error.status_code, 501); equal(error.message, - "Capacity 'list' is not implemented on 'indexeddb'"); + "Capacity 'list' is not implemented on 'memory'"); }) .always(function () { start(); @@ -952,16 +1046,11 @@ var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "indexeddb" + type: "memory" } }), expected_options = {foo: "bar"}; - Storage200.prototype.repair = function (options) { - deepEqual(options, expected_options, "repair 200 called"); - return "OK"; - }; - jio.repair(expected_options) .then(function (result) { equal(result, "OK"); -- 2.30.9 From 9bbcbeb5d80fa13da3cdaafc9112acb15d9b43b2 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Tue, 29 May 2018 15:32:23 +0000 Subject: [PATCH 04/46] Updated bryanstorage to retain all versions of documents and only retrieve the most recent copy when get(id) is called. --- src/jio.storage/bryanstorage.js | 69 ++- test/jio.storage/bryanstorage.tests.js | 680 +++---------------------- 2 files changed, 137 insertions(+), 612 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index 9d92321..b3e6048 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -13,37 +13,76 @@ this._sub_storage = jIO.createJIO(spec.sub_storage); } - BryanStorage.prototype.get = function () { - return this._sub_storage.get.apply(this._sub_storage, arguments); + BryanStorage.prototype.get = function (id_in) { + var options = { + //query: 'id: "' + id_in + '"', + //sort_on: [['_revision', 'descending']], + //id: "tmp", + include_docs: true + }; + return this._sub_storage.allDocs(options) + + // Return document with most recent revision + .push(function (results) { + //<0 => a < b + var sorted_results = results.data.rows.sort(function (a, b) { + if (b.doc.id !== id_in || !b) {return -1; } + if (a.doc.id !== id_in || !a) {return 1; } + return b.doc._revision - a.doc._revision; + }); + if (sorted_results.length > 0 && sorted_results[0].doc.id === id_in) { + return sorted_results[0].doc; + } + return []; + }); }; - BryanStorage.prototype.allAttachments = function () { - return this._sub_storage.allAttachments.apply(this._sub_storage, arguments); - }; // Not implemented for IndexedDB - BryanStorage.prototype.post = function () { - return this._sub_storage.post.apply(this._sub_storage, arguments); + BryanStorage.prototype.post = function (metadata) { + + function S4() { + return ('0000' + Math.floor( + Math.random() * 0x10000 // 65536 + ).toString(16)).slice(-4); + } + var id = S4() + S4() + "-" + + S4() + "-" + + S4() + "-" + + S4() + "-" + + S4() + S4() + S4(); + + return this._sub_storage.put(id, metadata); }; BryanStorage.prototype.put = function (id, new_metadata) { - var substorage = this._sub_storage; - - return this.get(id) + var storage = this; + new_metadata.id = id; + return storage.get(id) .push( function (metadata) { + // Increments existing "_revision" attribute - new_metadata._revision = metadata._revision + 1; - return substorage.put(id, new_metadata); + if (metadata.hasOwnProperty('_revision')) { + new_metadata._revision = metadata._revision + 1; + } else { + new_metadata._revision = 0; + } + //return storage.post.apply(substorage, new_metadata); + return storage.post(new_metadata); }, function () { // Creates new attribute "_revision" = 0 new_metadata._revision = 0; - return substorage.put(id, new_metadata); + return storage.post(new_metadata); } ); }; + BryanStorage.prototype.allAttachments = function () { + return this._sub_storage.allAttachments.apply(this._sub_storage, arguments); + }; + BryanStorage.prototype.remove = function () { return this._sub_storage.remove.apply(this._sub_storage, arguments); }; @@ -103,8 +142,8 @@ BryanStorage.prototype.hasCapacity = function (name) { return this._sub_storage.removeAttachment.apply(this._sub_storage, name); }; - BryanStorage.prototype.buildQuery = function (options) { - return this._sub_storage.removeAttachment.apply(this._sub_storage, options); + BryanStorage.prototype.buildQuery = function () { + return this._sub_storage.buildQuery.apply(this._sub_storage, arguments); }; jIO.addStorage('bryan', BryanStorage); diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index b94bd35..da18601 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -9,8 +9,7 @@ expect = QUnit.expect, deepEqual = QUnit.deepEqual, equal = QUnit.equal, - module = QUnit.module, - throws = QUnit.throws; + module = QUnit.module; ///////////////////////////////////////////////////////////////// @@ -225,6 +224,32 @@ } ); + ///////////////////////////////////////////////////////////////// + // _revision parameter updating with RSVP all + ///////////////////////////////////////////////////////////////// + module("bryanStorage _revision with RSVP all"); + test("verifying _revision updates correctly when puts are done in parallel", + function () { + stop(); + expect(1); + + // create storage of type "bryan" with memory as substorage + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: {type: "memory"} + }); + + jio.put("bar", {"title": "foo"}); + RSVP.all( + jio.put("bar", {"title2": "foo2"}), + jio.put("bar", {"title3": "foo3"}) + ) + .push(function () {return jio.get("bar"); }) + .push(function (result) {equal(result._revision, 3, "parallel exec"); }) + .fail(function (error) {ok(false, error); }) + .always(function () {start(); }); + }); + ///////////////////////////////////////////////////////////////// // bryanStorage.allAttachments ///////////////////////////////////////////////////////////////// @@ -423,140 +448,31 @@ }); ///////////////////////////////////////////////////////////////// - // bryanStorage.hasCapacity + // bryanStorage revision history ///////////////////////////////////////////////////////////////// - module("bryanStorage.hasCapacity"); - test("hasCapacity is false by default", function () { - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "memory" - } - }); - - throws( - function () { - jio.hasCapacity("foo"); - }, - function (error) { - ok(error instanceof jIO.util.jIOError); - equal(error.status_code, 501); - equal(error.message, - "Capacity 'foo' is not implemented on 'bryan'"); - return true; - } - ); - }); - - test("hasCapacity list return substorage value", function () { - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "memory" - } - }); - - throws( - function () { - jio.hasCapacity("list"); - }, - function (error) { - ok(error instanceof jIO.util.jIOError); - equal(error.status_code, 501); - equal(error.message, - "Capacity 'list' is not implemented on 'memory'"); - return true; - } - ); - }); - - ///////////////////////////////////////////////////////////////// - // bryanStorage.buildbryan - ///////////////////////////////////////////////////////////////// - module("bryanStorage.buildbryan"); - - test("substorage should have 'list' capacity", function () { + module("bryanStorage revision history"); + test("put and get the correct version", function () { stop(); - expect(3); - - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "memory" - } - }); - - jio.allDocs({ - include_docs: true, - bryan: 'title: "two"' - }) - .then(function () { - ok(false); - }) - .fail(function (error) { - ok(error instanceof jIO.util.jIOError); - equal(error.status_code, 501); - equal(error.message, - "Capacity 'list' is not implemented on 'memory'"); - }) - .always(function () { - start(); - }); - }); - - test("no manual bryan if substorage handle everything", function () { - stop(); - expect(2); - - function StorageAllDocsNoGet() { - return this; - } - StorageAllDocsNoGet.prototype.get = function () { - throw new Error("Unexpected get call"); - }; - StorageAllDocsNoGet.prototype.hasCapacity = function (capacity) { - if ((capacity === "list") || - (capacity === "sort") || - (capacity === "select") || - (capacity === "limit") || - (capacity === "bryan")) { - return true; - } - throw new Error("Unexpected " + capacity + " capacity check"); - }; - StorageAllDocsNoGet.prototype.buildbryan = function (options) { - deepEqual(options, { - sort_on: [["title", "ascending"]], - limit: [5], - select_list: ["title", "id"], - bryan: 'title: "two"' - }, - "buildbryan called"); - return "taboulet"; - }; - - jIO.addStorage('bryanStoragealldocsnoget', StorageAllDocsNoGet); - + expect(1); var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "bryanStoragealldocsnoget" + type: "indexeddb", + database: "db_test1" } }); - - jio.allDocs({ - sort_on: [["title", "ascending"]], - limit: [5], - select_list: ["title", "id"], - bryan: 'title: "two"' + jio.put("doc1", { + "title": "rev0", + "subtitle": "subrev0" }) - .then(function (result) { + .push(function () {return jio.get("doc1"); }) + .push(function (result) { deepEqual(result, { - data: { - rows: "taboulet", - total_rows: 8 - } - }); + "title": "rev0", + "subtitle": "subrev0", + "_revision": 0, + "id": "doc1" + }, "Retrieve document correctly"); }) .fail(function (error) { ok(false, error); @@ -566,467 +482,62 @@ }); }); - test("manual bryan used if substorage does not handle sort", function () { + module("bryanStorage revision history multiple edits"); + test("modify first version but save both", function () { stop(); - expect(4); - - function StorageNoSortCapacity() { - return this; - } - StorageNoSortCapacity.prototype.get = function (id) { - if (id === "foo") { - equal(id, "foo", "Get foo"); - } else { - equal(id, "bar", "Get bar"); - } - return {title: id, id: "ID " + id, - "another": "property"}; - }; - StorageNoSortCapacity.prototype.hasCapacity = function (capacity) { - if ((capacity === "list") || - (capacity === "select") || - (capacity === "limit") || - (capacity === "bryan")) { - return true; - } - return false; - }; - StorageNoSortCapacity.prototype.buildbryan = function (options) { - deepEqual(options, {}, "No bryan parameter"); - var result2 = [{ - id: "foo", - value: {} - }, { - id: "bar", - value: {} - }]; - return result2; - }; - - jIO.addStorage('bryanStoragenosortcapacity', StorageNoSortCapacity); - + expect(2); var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "bryanStoragenosortcapacity" + type: "indexeddb", + database: "db_test2" } }); - - jio.allDocs({ - sort_on: [["title", "ascending"]], - limit: [0, 5], - select_list: ["title", "id"], - bryan: 'title: "foo"' + jio.put("other_doc", { + "attr": "version0", + "subattr": "subversion0" }) - .then(function (result) { - deepEqual(result, { - data: { - rows: [{ - id: "foo", - doc: {}, - value: { - title: "foo", - id: "ID foo" - } - }], - total_rows: 1 - } + .push(function () { + return jio.put("other_doc", { + "attr": "version1", + "subattr": "subversion1" }); }) - .fail(function (error) { - ok(false, error); - }) - .always(function () { - start(); - }); - }); - - test("manual bryan used if substorage does not handle select", function () { - stop(); - expect(4); - - function StorageNoSelectCapacity() { - return this; - } - StorageNoSelectCapacity.prototype.get = function (id) { - if (id === "foo") { - equal(id, "foo", "Get foo"); - } else { - equal(id, "bar", "Get bar"); - } - return {title: id, id: "ID " + id, - "another": "property"}; - }; - StorageNoSelectCapacity.prototype.hasCapacity = function (capacity) { - if ((capacity === "list") || - (capacity === "sort") || - (capacity === "limit") || - (capacity === "bryan")) { - return true; - } - return false; - }; - StorageNoSelectCapacity.prototype.buildbryan = function (options) { - deepEqual(options, {}, "No bryan parameter"); - var result2 = [{ - id: "foo", - value: {} - }, { - id: "bar", - value: {} - }]; - return result2; - }; - - jIO.addStorage('bryanStoragenoselectcapacity', StorageNoSelectCapacity); - - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "bryanStoragenoselectcapacity" - } - }); - - jio.allDocs({ - sort_on: [["title", "ascending"]], - limit: [0, 5], - select_list: ["title", "id"], - bryan: 'title: "foo"' - }) - .then(function (result) { - deepEqual(result, { - data: { - rows: [{ - id: "foo", - doc: {}, - value: { - title: "foo", - id: "ID foo" - } - }], - total_rows: 1 - } + .push(function () { + return jio.put("main_doc", { + "title": "rev0", + "subtitle": "subrev0" }); }) - .fail(function (error) { - ok(false, error); - }) - .always(function () { - start(); - }); - }); - - test("manual bryan used if substorage does not handle limit", function () { - stop(); - expect(4); - - function StorageNoLimitCapacity() { - return this; - } - StorageNoLimitCapacity.prototype.get = function (id) { - if (id === "foo") { - equal(id, "foo", "Get foo"); - } else { - equal(id, "bar", "Get bar"); - } - return {title: id, id: "ID " + id, - "another": "property"}; - }; - StorageNoLimitCapacity.prototype.hasCapacity = function (capacity) { - if ((capacity === "list") || - (capacity === "select") || - (capacity === "sort") || - (capacity === "bryan")) { - return true; - } - return false; - }; - StorageNoLimitCapacity.prototype.buildbryan = function (options) { - deepEqual(options, {}, "No bryan parameter"); - var result2 = [{ - id: "foo", - value: {} - }, { - id: "bar", - value: {} - }]; - return result2; - }; - - jIO.addStorage('bryanStoragenolimitcapacity', StorageNoLimitCapacity); - - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "bryanStoragenolimitcapacity" - } - }); - - jio.allDocs({ - sort_on: [["title", "ascending"]], - limit: [0, 5], - select_list: ["title", "id"], - bryan: 'title: "foo"' - }) - .then(function (result) { - deepEqual(result, { - data: { - rows: [{ - id: "foo", - doc: {}, - value: { - title: "foo", - id: "ID foo" - } - }], - total_rows: 1 - } + .push(function () { + return jio.put("main_doc", { + "title": "rev1", + "subtitle": "subrev1" }); }) - .fail(function (error) { - ok(false, error); - }) - .always(function () { - start(); - }); - }); - - test("manual bryan used if substorage does not handle bryan", function () { - stop(); - expect(4); - - function StorageNobryanCapacity() { - return this; - } - StorageNobryanCapacity.prototype.get = function (id) { - if (id === "foo") { - equal(id, "foo", "Get foo"); - } else { - equal(id, "bar", "Get bar"); - } - return {title: id, id: "ID " + id, - "another": "property"}; - }; - StorageNobryanCapacity.prototype.hasCapacity = function (capacity) { - if ((capacity === "list") || - (capacity === "select") || - (capacity === "limit") || - (capacity === "sort")) { - return true; - } - return false; - }; - StorageNobryanCapacity.prototype.buildbryan = function (options) { - deepEqual(options, {}, "No bryan parameter"); - var result2 = [{ - id: "foo", - value: {} - }, { - id: "bar", - value: {} - }]; - return result2; - }; - - jIO.addStorage('bryanStoragenobryancapacity', StorageNobryanCapacity); - - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "bryanStoragenobryancapacity" - } - }); - - jio.allDocs({ - sort_on: [["title", "ascending"]], - limit: [0, 5], - select_list: ["title", "id"], - bryan: 'title: "foo"' - }) - .then(function (result) { - deepEqual(result, { - data: { - rows: [{ - id: "foo", - doc: {}, - value: { - title: "foo", - id: "ID foo" - } - }], - total_rows: 1 - } + .push(function () { + return jio.put("main_doc", { + "title": "rev2", + "subtitle": "subrev2" }); }) - .fail(function (error) { - ok(false, error); - }) - .always(function () { - start(); - }); - }); - - test("does not fetch doc one by one if substorage handle include_docs", - function () { - stop(); - expect(2); - - function StorageIncludeDocsCapacity() { - return this; - } - StorageIncludeDocsCapacity.prototype.hasCapacity = function (capacity) { - if ((capacity === "list") || - (capacity === "include")) { - return true; - } - return false; - }; - StorageIncludeDocsCapacity.prototype.buildbryan = function (options) { - deepEqual(options, {include_docs: true}, "Include docs parameter"); - var result2 = [{ - id: "foo", - value: {}, - doc: { - title: "foo", - id: "ID foo", - another: "property" - } - }, { - id: "bar", - value: {}, - doc: { - title: "bar", - id: "ID bar", - another: "property" - } - }]; - return result2; - }; - - jIO.addStorage('bryanStorageincludedocscapacity', - StorageIncludeDocsCapacity); - - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "bryanStorageincludedocscapacity" - } - }); - - jio.allDocs({ - sort_on: [["title", "ascending"]], - limit: [0, 5], - select_list: ["title", "id"], - bryan: 'title: "foo"' + .push(function () {return jio.get("main_doc"); }) + .push(function (result) { + deepEqual(result, { + "title": "rev2", + "subtitle": "subrev2", + "_revision": 2, + "id": "main_doc" + }, "Retrieve main document correctly"); }) - .then(function (result) { - deepEqual(result, { - data: { - rows: [{ - id: "foo", - doc: {}, - value: { - title: "foo", - id: "ID foo" - } - }], - total_rows: 1 - } - }); - }) - .fail(function (error) { - ok(false, error); - }) - .always(function () { - start(); - }); - }); - - test("manual bryan used and use schema", function () { - stop(); - expect(4); - - function StorageSchemaCapacity() { - return this; - } - StorageSchemaCapacity.prototype.get = function (id) { - var doc = { - title: id, - id: "ID " + id, - "another": "property" - }; - if (id === "foo") { - equal(id, "foo", "Get foo"); - doc.modification_date = "Fri, 08 Sep 2017 07:46:27 +0000"; - } else { - equal(id, "bar", "Get bar"); - doc.modification_date = "Thu, 07 Sep 2017 18:59:23 +0000"; - } - return doc; - }; - - StorageSchemaCapacity.prototype.hasCapacity = function (capacity) { - if ((capacity === "list")) { - return true; - } - return false; - }; - StorageSchemaCapacity.prototype.buildbryan = function (options) { - deepEqual(options, {}, "No bryan parameter"); - var result2 = [{ - id: "foo", - value: {} - }, { - id: "bar", - value: {} - }]; - return result2; - }; - - jIO.addStorage( - 'bryanStoragenoschemacapacity', - StorageSchemaCapacity - ); - - var jio = jIO.createJIO({ - type: "bryan", - schema: { - "modification_date": { - "type": "string", - "format": "date-time" - } - }, - sub_storage: { - type: "bryanStoragenoschemacapacity" - } - }); - - jio.allDocs({ - sort_on: [["modification_date", "descending"]], - limit: [0, 5], - select_list: ['modification_date'] - }) - .then(function (result) { + .push(function () {return jio.get("other_doc"); }) + .push(function (result) { deepEqual(result, { - data: { - rows: [ - { - id: "foo", - doc: {}, - value: { - modification_date: "Fri, 08 Sep 2017 07:46:27 +0000" - } - }, { - id: "bar", - doc: {}, - value: { - modification_date: "Thu, 07 Sep 2017 18:59:23 +0000" - } - } - ], - total_rows: 2 - } - }); + "attr": "version1", + "subattr": "subversion1", + "_revision": 1, + "id": "other_doc" + }, "Retrieve other document correctly"); }) .fail(function (error) { ok(false, error); @@ -1035,32 +546,7 @@ start(); }); }); - ///////////////////////////////////////////////////////////////// - // bryanStorage.repair - ///////////////////////////////////////////////////////////////// - module("bryanStorage.repair"); - test("repair called substorage repair", function () { - stop(); - expect(2); - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "memory" - } - }), - expected_options = {foo: "bar"}; +}(jIO, QUnit, Blob)); - jio.repair(expected_options) - .then(function (result) { - equal(result, "OK"); - }) - .fail(function (error) { - ok(false, error); - }) - .always(function () { - start(); - }); - }); -}(jIO, QUnit, Blob)); -- 2.30.9 From 5762d991a700bd00a557cc3611b3b89467876ae4 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Wed, 30 May 2018 07:32:20 +0000 Subject: [PATCH 05/46] bryanstorage now uses querystorage and uuidstorage as substorages to implement uuid posting and querying the sub storage. --- src/jio.storage/bryanstorage.js | 62 +++++++++++--------------- test/jio.storage/bryanstorage.tests.js | 20 ++++----- 2 files changed, 37 insertions(+), 45 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index b3e6048..2982101 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -10,54 +10,46 @@ * @constructor */ function BryanStorage(spec) { - this._sub_storage = jIO.createJIO(spec.sub_storage); + //this._sub_storage = jIO.createJIO(spec.sub_storage); + this._sub_storage = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: spec.sub_storage + } + }); + } BryanStorage.prototype.get = function (id_in) { - var options = { - //query: 'id: "' + id_in + '"', - //sort_on: [['_revision', 'descending']], - //id: "tmp", - include_docs: true - }; + var substorage = this._sub_storage, + options = { + query: '(_doc_id: "' + id_in + '")',// AND (include_docs: true)', + sort_on: [['_revision', 'descending']] + //include_docs: true + }; return this._sub_storage.allDocs(options) - - // Return document with most recent revision - .push(function (results) { - //<0 => a < b - var sorted_results = results.data.rows.sort(function (a, b) { - if (b.doc.id !== id_in || !b) {return -1; } - if (a.doc.id !== id_in || !a) {return 1; } - return b.doc._revision - a.doc._revision; - }); - if (sorted_results.length > 0 && sorted_results[0].doc.id === id_in) { - return sorted_results[0].doc; + // Return query results if there are any, else throw error + .push(function (query_results) { + var docs = query_results.data.rows; + if (docs.length > 0) { + return substorage.get(docs[0].id); } - return []; + throw new jIO.util.jIOError( + "bryanstorage: cannot find object '" + id_in + "'", + 404 + ); }); }; - - // Not implemented for IndexedDB BryanStorage.prototype.post = function (metadata) { - - function S4() { - return ('0000' + Math.floor( - Math.random() * 0x10000 // 65536 - ).toString(16)).slice(-4); - } - var id = S4() + S4() + "-" + - S4() + "-" + - S4() + "-" + - S4() + "-" + - S4() + S4() + S4(); - - return this._sub_storage.put(id, metadata); + // Uses UuidStorage post + return this._sub_storage.post(metadata); }; BryanStorage.prototype.put = function (id, new_metadata) { var storage = this; - new_metadata.id = id; + new_metadata._doc_id = id; return storage.get(id) .push( function (metadata) { diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index da18601..8ba111b 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -455,12 +455,12 @@ stop(); expect(1); var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "indexeddb", - database: "db_test1" - } - }); + type: "bryan", + sub_storage: { + type: "indexeddb", + database: "newdb4" + } + }); jio.put("doc1", { "title": "rev0", "subtitle": "subrev0" @@ -471,7 +471,7 @@ "title": "rev0", "subtitle": "subrev0", "_revision": 0, - "id": "doc1" + "_doc_id": "doc1" }, "Retrieve document correctly"); }) .fail(function (error) { @@ -490,7 +490,7 @@ type: "bryan", sub_storage: { type: "indexeddb", - database: "db_test2" + database: "otherdb5" } }); jio.put("other_doc", { @@ -527,7 +527,7 @@ "title": "rev2", "subtitle": "subrev2", "_revision": 2, - "id": "main_doc" + "_doc_id": "main_doc" }, "Retrieve main document correctly"); }) .push(function () {return jio.get("other_doc"); }) @@ -536,7 +536,7 @@ "attr": "version1", "subattr": "subversion1", "_revision": 1, - "id": "other_doc" + "_doc_id": "other_doc" }, "Retrieve other document correctly"); }) .fail(function (error) { -- 2.30.9 From b2948e5d7908bdb3adf28b406b61b1fcbc70a769 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Wed, 30 May 2018 08:53:46 +0000 Subject: [PATCH 06/46] Added additional testing of the revisions and removed extraneous tests. --- src/jio.storage/bryanstorage.js | 7 +- test/jio.storage/bryanstorage.tests.js | 444 +++---------------------- 2 files changed, 49 insertions(+), 402 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index 2982101..cb35f55 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -18,7 +18,6 @@ sub_storage: spec.sub_storage } }); - } BryanStorage.prototype.get = function (id_in) { @@ -28,7 +27,7 @@ sort_on: [['_revision', 'descending']] //include_docs: true }; - return this._sub_storage.allDocs(options) + return this.allDocs(options) // Return query results if there are any, else throw error .push(function (query_results) { var docs = query_results.data.rows; @@ -71,6 +70,10 @@ ); }; + BryanStorage.prototype.allDocs = function () { + return this._sub_storage.allDocs.apply(this._sub_storage, arguments); + }; + BryanStorage.prototype.allAttachments = function () { return this._sub_storage.allAttachments.apply(this._sub_storage, arguments); }; diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index 8ba111b..0a33cb8 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -1,6 +1,6 @@ /*jslint nomen: true*/ /*global Blob, jiodate*/ -(function (jIO, QUnit, Blob) { +(function (jIO, QUnit) { "use strict"; var test = QUnit.test, stop = QUnit.stop, @@ -29,204 +29,10 @@ }); - ///////////////////////////////////////////////////////////////// - // bryanStorage.get - ///////////////////////////////////////////////////////////////// - module("bryanStorage.get"); - test("get called substorage get", function () { - stop(); - expect(2); - - // create storage of type "bryan" with memory as substorage - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "memory" - } - }); - jio.put("bar", {"title": "foo"}); - jio.get("bar") - .then(function (result) { - deepEqual(result, { - "title": "foo", - "_revision": 0 - }, "Check document"); - }) - .fail(function (error) { - ok(false, error); - }) - .always(function () { - start(); - }); - - jio.get("bar") - .then(function (result) { - deepEqual(result, { - "title": "foo", - "_revision": 0 - }, "Check document"); - }) - .fail(function (error) { - ok(false, error); - }); - //.always(function () { - // start(); - //}); - }); - - ///////////////////////////////////////////////////////////////// - // _revision parameter initialization - ///////////////////////////////////////////////////////////////// - module("bryanStorage initialize _revision"); - test("verifying _revision updates correctly", function () { - stop(); - expect(2); - - // create storage of type "bryan" with memory as substorage - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "memory" - } - }); - jio.put("bar", {"title": "foo"}) - .push(function (result) { - equal(result, "bar"); - return jio.get("bar"); - }) - .push(function (result) { - deepEqual(result, { - "title": "foo", - "_revision": 0 - }, "Check document"); - }) - .fail(function (error) {ok(false, error); }) - .always(function () {start(); }); - }); - - - ///////////////////////////////////////////////////////////////// - // _revision parameter updating with put - ///////////////////////////////////////////////////////////////// - module("bryanStorage _revision with put"); - test("verifying _revision updates correctly", function () { - stop(); - expect(1); - - // create storage of type "bryan" with memory as substorage - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: {type: "memory"} - }); - jio.put("bar", {"title": "foo"}) - .push(function () {return jio.put("bar", {"title2": "foo2"}); }) - .push(function () {return jio.put("bar", {"title3": "foo3"}); }) - .push(function () {return jio.get("bar"); }) - .push(function (result) { - deepEqual(result, { - "title3": "foo3", - "_revision": 2 - }, "Check document after initialization"); - }) - .fail(function (error) {ok(false, error); }) - .always(function () {start(); }); - }); - - ///////////////////////////////////////////////////////////////// - // _revision parameter updating with putAttachment - ///////////////////////////////////////////////////////////////// - module("bryanStorage _revision with putAttachment"); - test("verifying _revision updates correctly after putAttachment", - function () { - stop(); - expect(1); - - // Create storage of type "bryan" with memory as substorage - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: {type: "memory"} - }); - - jio.put("bar", {"title": "foo"}) - - // Put two unique attachments in the document - .push(function () { - return jio.putAttachment( - "bar", - "blob", - new Blob(["text data"], {type: "text/plain"}) - ); - }) - .push(function () { - return jio.putAttachment( - "bar", - "blob2", - new Blob(["more text data"], {type: "text/plain"}) - ); - }) - - // Get metadata for document - .push(function () {return jio.get("bar"); }) - - // Verify "_revision" is incremented twice - .push(function (result) { - deepEqual(result, { - "title": "foo", - "_revision": 2 - }, "Check document after 2 revisions"); - }) - .fail(function (error) {ok(false, error); }) - .always(function () {start(); }); - } - ); - - ///////////////////////////////////////////////////////////////// - // _revision parameter updating with removeAttachment - ///////////////////////////////////////////////////////////////// - module("bryanStorage _revision with removeAttachment"); - test("verifying _revision updates correctly after removeAttachment", - function () { - stop(); - expect(1); - - // create storage of type "bryan" with memory as substorage - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: {type: "memory"} - }); - - jio.put("bar", {"title": "foo"}) - .push(function () { - return jio.putAttachment( - "bar", - "blob", - new Blob(["text data"], {type: "text/plain"}) - ); - }) - .push(function () { - return jio.putAttachment( - "bar", - "blob2", - new Blob(["more text data"], {type: "text/plain"}) - ); - }) - .push(function () {return jio.removeAttachment("bar", "blob"); }) - .push(function () {return jio.removeAttachment("bar", "blob2"); }) - .push(function () {return jio.get("bar"); }) - .push(function (result) { - deepEqual(result, { - "title": "foo", - "_revision": 4 - }, "Check document after 4 revisions"); - }) - .fail(function (error) {ok(false, error); }) - .always(function () {start(); }); - } - ); - ///////////////////////////////////////////////////////////////// // _revision parameter updating with RSVP all ///////////////////////////////////////////////////////////////// + /** module("bryanStorage _revision with RSVP all"); test("verifying _revision updates correctly when puts are done in parallel", function () { @@ -249,216 +55,21 @@ .fail(function (error) {ok(false, error); }) .always(function () {start(); }); }); - - ///////////////////////////////////////////////////////////////// - // bryanStorage.allAttachments - ///////////////////////////////////////////////////////////////// - module("bryanStorage.allAttachments"); - test("allAttachments called substorage allAttachments", function () { - stop(); - expect(2); - - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "memory" - } - }); - - jio.allAttachments("bar") - .push(function (result) { - deepEqual(result, { - attachmentname: {} - }, "Check document"); - }) - .fail(function (error) { - ok(false, error); - }) - .always(function () { - start(); - }); - }); + **/ - ///////////////////////////////////////////////////////////////// - // bryanStorage.post - ///////////////////////////////////////////////////////////////// - module("bryanStorage.post"); - test("post called substorage post", function () { - stop(); - expect(2); - - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "memory" - } - }); - - jio.post({"title": "foo"}) - .push(function (result) { - equal(result, "youhou"); - }) - .fail(function (error) { - ok(false, error); - }) - .always(function () { - start(); - }); - }); - - - ///////////////////////////////////////////////////////////////// - // bryanStorage.put - ///////////////////////////////////////////////////////////////// - module("bryanStorage.put"); - test("put called substorage put", function () { - stop(); - expect(3); - - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "memory" - } - }); - - // If .put does not give the appropriate return, fail assertion - jio.put("bar", {"title": "foo"}) - .push(function (result) { - equal(result, "bar"); - }) - .fail(function (error) { - ok(false, error); - }) - .always(function () { - start(); - }); - }); - - - ///////////////////////////////////////////////////////////////// - // bryanStorage.remove - ///////////////////////////////////////////////////////////////// - module("bryanStorage.remove"); - test("remove called substorage remove", function () { - stop(); - expect(2); - - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "memory" - } - }); - - jio.remove("bar") - .push(function (result) { - equal(result, "bar"); - }) - .fail(function (error) { - ok(false, error); - }) - .always(function () { - start(); - }); - }); - - ///////////////////////////////////////////////////////////////// - // bryanStorage.getAttachment - ///////////////////////////////////////////////////////////////// - module("bryanStorage.getAttachment"); - test("getAttachment called substorage getAttachment", function () { - stop(); - expect(3); - - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "memory" - } - }), - blob = new Blob([""]); - - jio.getAttachment("bar", "foo") - .then(function (result) { - equal(result, blob); - }) - .fail(function (error) { - ok(false, error); - }) - .always(function () { - start(); - }); - }); - - ///////////////////////////////////////////////////////////////// - // bryanStorage.putAttachment - ///////////////////////////////////////////////////////////////// - module("bryanStorage.putAttachment"); - test("putAttachment called substorage putAttachment", function () { - stop(); - expect(4); - - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "memory" - } - }), - blob = new Blob([""]); - - jio.putAttachment("bar", "foo", blob) - .then(function (result) { - equal(result, "OK"); - }) - .fail(function (error) { - ok(false, error); - }) - .always(function () { - start(); - }); - }); - - ///////////////////////////////////////////////////////////////// - // bryanStorage.removeAttachment - ///////////////////////////////////////////////////////////////// - module("bryanStorage.removeAttachment"); - test("removeAttachment called substorage removeAttachment", function () { - stop(); - expect(3); - - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "memory" - } - }); - - jio.removeAttachment("bar", "foo") - .then(function (result) { - equal(result, "Removed"); - }) - .fail(function (error) { - ok(false, error); - }) - .always(function () { - start(); - }); - }); - ///////////////////////////////////////////////////////////////// // bryanStorage revision history ///////////////////////////////////////////////////////////////// - module("bryanStorage revision history"); + module("bryanStorage.revision_history"); test("put and get the correct version", function () { stop(); expect(1); var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "indexeddb", - database: "newdb4" + type: "memory" + //database: "newdb4" } }); jio.put("doc1", { @@ -482,15 +93,15 @@ }); }); - module("bryanStorage revision history multiple edits"); + module("bryanStorage.revision_history_multiple_edits"); test("modify first version but save both", function () { stop(); - expect(2); + expect(6); var jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "indexeddb", - database: "otherdb5" + type: "memory" + //database: "otherdb8" } }); jio.put("other_doc", { @@ -539,6 +150,39 @@ "_doc_id": "other_doc" }, "Retrieve other document correctly"); }) + .push(function () { + return jio.allDocs({ + query: '(_doc_id: "main_doc") AND (_revision: 0)', + sort_on: [['_revision', 'descending']] + }); + }) + .push(function (result) { + equal(result.length, 1, "Correct number of results returned"); + }) + .push(function () { + return jio.allDocs({ + query: '(_doc_id: "main_doc") AND (_revision: 1)' + }); + }) + .push(function (result) { + equal(result.length, 1, "Correct number of results returned"); + }) + .push(function () { + return jio.allDocs({ + query: '(_doc_id: "other_doc") AND (_revision: 0)' + }); + }) + .push(function (result) { + equal(result.length, 1, "Correct number of results returned"); + }) + .push(function () { + return jio.allDocs({ + query: '' + }); + }) + .push(function (result) { + equal(result.length, 5, "Correct number of results returned"); + }) .fail(function (error) { ok(false, error); }) @@ -547,6 +191,6 @@ }); }); -}(jIO, QUnit, Blob)); +}(jIO, QUnit)); -- 2.30.9 From a1a1117d2fc2086d1d0b365d7133421d24d260af Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Wed, 30 May 2018 08:56:24 +0000 Subject: [PATCH 07/46] Removed one last unnecessary test. --- test/jio.storage/bryanstorage.tests.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index 0a33cb8..ee75dab 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -12,23 +12,6 @@ module = QUnit.module; - ///////////////////////////////////////////////////////////////// - // bryanStorage.constructor - ///////////////////////////////////////////////////////////////// - module("bryanStorage.constructor"); - test("accept parameters", function () { - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "memory" - } - }); - - ok(jio.__storage._sub_storage instanceof jio.constructor); - equal(jio.__storage._sub_storage.__type, "memory"); - }); - - ///////////////////////////////////////////////////////////////// // _revision parameter updating with RSVP all ///////////////////////////////////////////////////////////////// -- 2.30.9 From 3598f85f2b18578f1d2976f00715be035cbf4e86 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Wed, 30 May 2018 13:52:38 +0000 Subject: [PATCH 08/46] Fixed the query test so now all test assertions run and pass. --- src/jio.storage/bryanstorage.js | 14 +++++++------ test/jio.storage/bryanstorage.tests.js | 28 +++++++++++++------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index cb35f55..e6d24d4 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -11,6 +11,7 @@ */ function BryanStorage(spec) { //this._sub_storage = jIO.createJIO(spec.sub_storage); + this._sub_storage = jIO.createJIO({ type: "query", sub_storage: { @@ -27,7 +28,7 @@ sort_on: [['_revision', 'descending']] //include_docs: true }; - return this.allDocs(options) + return substorage.allDocs(options) // Return query results if there are any, else throw error .push(function (query_results) { var docs = query_results.data.rows; @@ -70,8 +71,9 @@ ); }; - BryanStorage.prototype.allDocs = function () { - return this._sub_storage.allDocs.apply(this._sub_storage, arguments); + BryanStorage.prototype.allDocs = function (options) { + //console.log(options); + return this._sub_storage.allDocs.apply(this._sub_storage, options); }; BryanStorage.prototype.allAttachments = function () { @@ -134,8 +136,8 @@ BryanStorage.prototype.repair = function () { return this._sub_storage.repair.apply(this._sub_storage, arguments); }; - BryanStorage.prototype.hasCapacity = function (name) { - return this._sub_storage.removeAttachment.apply(this._sub_storage, name); + BryanStorage.prototype.hasCapacity = function () { + return this._sub_storage.hasCapacity.apply(this._sub_storage, arguments); }; BryanStorage.prototype.buildQuery = function () { return this._sub_storage.buildQuery.apply(this._sub_storage, arguments); @@ -143,4 +145,4 @@ jIO.addStorage('bryan', BryanStorage); -}(jIO)); +}(jIO)); \ No newline at end of file diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index ee75dab..21c73dc 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -52,7 +52,6 @@ type: "bryan", sub_storage: { type: "memory" - //database: "newdb4" } }); jio.put("doc1", { @@ -84,23 +83,22 @@ type: "bryan", sub_storage: { type: "memory" - //database: "otherdb8" } }); - jio.put("other_doc", { - "attr": "version0", - "subattr": "subversion0" + jio.put("main_doc", { + "title": "rev0", + "subtitle": "subrev0" }) .push(function () { return jio.put("other_doc", { - "attr": "version1", - "subattr": "subversion1" + "attr": "version0", + "subattr": "subversion0" }); }) .push(function () { - return jio.put("main_doc", { - "title": "rev0", - "subtitle": "subrev0" + return jio.put("other_doc", { + "attr": "version1", + "subattr": "subversion1" }); }) .push(function () { @@ -133,8 +131,9 @@ "_doc_id": "other_doc" }, "Retrieve other document correctly"); }) + .push(function () { - return jio.allDocs({ + return jio.buildQuery({ query: '(_doc_id: "main_doc") AND (_revision: 0)', sort_on: [['_revision', 'descending']] }); @@ -143,7 +142,7 @@ equal(result.length, 1, "Correct number of results returned"); }) .push(function () { - return jio.allDocs({ + return jio.buildQuery({ query: '(_doc_id: "main_doc") AND (_revision: 1)' }); }) @@ -151,7 +150,7 @@ equal(result.length, 1, "Correct number of results returned"); }) .push(function () { - return jio.allDocs({ + return jio.buildQuery({ query: '(_doc_id: "other_doc") AND (_revision: 0)' }); }) @@ -159,7 +158,7 @@ equal(result.length, 1, "Correct number of results returned"); }) .push(function () { - return jio.allDocs({ + return jio.buildQuery({ query: '' }); }) @@ -167,6 +166,7 @@ equal(result.length, 5, "Correct number of results returned"); }) .fail(function (error) { + //console.log(error); ok(false, error); }) .always(function () { -- 2.30.9 From 44628a99c0cc0195926cd2a44d0cee4da0046c7a Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Fri, 1 Jun 2018 16:01:35 +0000 Subject: [PATCH 09/46] Tests are not yet passing. An issue exists with bryanstorage buildQuery. --- src/jio.storage/bryanstorage.js | 121 ++++++++------ test/jio.storage/bryanstorage.tests.js | 218 +++++++++++++++++++++---- 2 files changed, 259 insertions(+), 80 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index e6d24d4..89a63f2 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -3,6 +3,9 @@ (function (jIO) { "use strict"; + // Metadata keys included for internal revisioning, but not shown to user + //var _revision_metadata = ["_revision", "_doc_id"]; + /** * The jIO BryanStorage extension * @@ -10,36 +13,15 @@ * @constructor */ function BryanStorage(spec) { - //this._sub_storage = jIO.createJIO(spec.sub_storage); this._sub_storage = jIO.createJIO({ type: "query", - sub_storage: { - type: "uuid", - sub_storage: spec.sub_storage - } + sub_storage: spec.sub_storage }); } BryanStorage.prototype.get = function (id_in) { - var substorage = this._sub_storage, - options = { - query: '(_doc_id: "' + id_in + '")',// AND (include_docs: true)', - sort_on: [['_revision', 'descending']] - //include_docs: true - }; - return substorage.allDocs(options) - // Return query results if there are any, else throw error - .push(function (query_results) { - var docs = query_results.data.rows; - if (docs.length > 0) { - return substorage.get(docs[0].id); - } - throw new jIO.util.jIOError( - "bryanstorage: cannot find object '" + id_in + "'", - 404 - ); - }); + return this._sub_storage.get(id_in); }; BryanStorage.prototype.post = function (metadata) { @@ -48,32 +30,52 @@ }; BryanStorage.prototype.put = function (id, new_metadata) { - var storage = this; - new_metadata._doc_id = id; - return storage.get(id) - .push( - function (metadata) { - - // Increments existing "_revision" attribute - if (metadata.hasOwnProperty('_revision')) { - new_metadata._revision = metadata._revision + 1; - } else { - new_metadata._revision = 0; - } - //return storage.post.apply(substorage, new_metadata); - return storage.post(new_metadata); - }, - function () { - // Creates new attribute "_revision" = 0 - new_metadata._revision = 0; - return storage.post(new_metadata); + var storage = this, + substorage = this._sub_storage, + previous_data; + + return this._sub_storage.get(id) + .push(function (latest_data) { + + // Prepare to post the current doc as a deprecated version + previous_data = latest_data; + previous_data._deprecated = true; + previous_data._doc_id = id; + + // Get most recent deprecated version's _revision attribute + var options = { + query: '(_doc_id: "' + id + '")', + sort_on: [['_revision', 'descending']], + limit: [0, 1] + }; + return substorage.buildQuery(options); + }) + .push(function (query_results) { + if (query_results.length > 0) { + var doc_id = query_results[0]; + return this._sub_storage.get(doc_id); } - ); - }; - - BryanStorage.prototype.allDocs = function (options) { - //console.log(options); - return this._sub_storage.allDocs.apply(this._sub_storage, options); + throw new jIO.util.jIOError( + "bryanstorage: query returned no results.'", + 404 + ); + }) + .push(function (doc) { + previous_data._revision = doc._revision + 1; + return storage.post(previous_data); + }, + function () { + // If the query turned up no results, + // there was exactly 1 version previously. + if (previous_data !== undefined) { + previous_data._revision = 0; + return storage.post(previous_data); + } + }) + // No matter what happened, need to put new document in + .push(function () { + return substorage.put(id, new_metadata); + }); }; BryanStorage.prototype.allAttachments = function () { @@ -139,8 +141,27 @@ BryanStorage.prototype.hasCapacity = function () { return this._sub_storage.hasCapacity.apply(this._sub_storage, arguments); }; - BryanStorage.prototype.buildQuery = function () { - return this._sub_storage.buildQuery.apply(this._sub_storage, arguments); + BryanStorage.prototype.allDocs = function (options) { + if (options === undefined) { + options = {}; + } + console.log("options", options); + /** + if (options === undefined) { + options = {query: ""}; + } + options.query = '(' + options.query + ') AND NOT (_deprecated = true)'; + console.log("query string: ", options.query); + **/ + return this._sub_storage.allDocs.apply(this._sub_storage, options); + //return this._sub_storage.buildQuery.apply(this._sub_storage, options); + }; + BryanStorage.prototype.buildQuery = function (options) { + if (options === undefined) { + options = {}; + } + console.log("options", options); + return this._sub_storage.buildQuery.apply(this._sub_storage, options); }; jIO.addStorage('bryan', BryanStorage); diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index 21c73dc..89c7f1b 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -47,11 +47,28 @@ module("bryanStorage.revision_history"); test("put and get the correct version", function () { stop(); - expect(1); - var jio = jIO.createJIO({ + expect(4); + var dbname = "testingdb4", + jio = jIO.createJIO({ type: "bryan", sub_storage: { - type: "memory" + type: "uuid", + sub_storage: { + //type: "memory" + type: "indexeddb", + database: dbname + } + } + }), + not_bryan = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + //type: "memory" + type: "indexeddb", + database: dbname + } } }); jio.put("doc1", { @@ -61,13 +78,82 @@ .push(function () {return jio.get("doc1"); }) .push(function (result) { deepEqual(result, { + "title": "rev0", + "subtitle": "subrev0" + }, "Retrieve first edition of document correctly"); + }) + .push(function () { + return jio.put("doc1", { + "title": "rev1", + "subtitle": "subrev1" + }); + }) + .push(function () { + return jio.put("doc1", { + "title": "rev2", + "subtitle": "subrev2" + }); + }) + .push(function () { + return jio.get("doc1"); + }) + .push(function (result) { + deepEqual(result, { + "title": "rev2", + "subtitle": "subrev2" + }, "Retrieve second edition of document correctly"); + }) + .push(function () { + var options = { + //query: ""//title: rev2" + }; + // + // + return jio.buildQuery(options); + //return jio.allDocs(options); + // + // + }) + .push(function (results) { + console.log("query results: ", results); + equal(results.data.rows.length, 1, "Query only returns latest version"); + if (results.data.rows.length > 0) { + return jio.get(results.data.rows[0].id); + } + }) + .push(function (result) { + deepEqual(result, { + "title": "rev2", + "subtitle": "subrev2" + }, "Retrieve queried document correctly"); + }) + + // When not_bryan queries the storage, all documents are returned. + .push(function () { + var options = { + query: "", + sort_on: [["_revision", "ascending"]] + }; + return jio.allDocs(options); + }) + .push(function (results) { + equal(results.length, 2, "should get all 2 revisions."); + if (results.length > 0) { + return not_bryan.get(results[0].id); + } + }) + .push(function (results) { + deepEqual(results, { "title": "rev0", "subtitle": "subrev0", + "_doc_id": "doc1", "_revision": 0, - "_doc_id": "doc1" - }, "Retrieve document correctly"); + "_deprecated": true + }, + "Get the earliest copy of the doc with all metadata."); }) .fail(function (error) { + console.log(error); ok(false, error); }) .always(function () { @@ -78,13 +164,24 @@ module("bryanStorage.revision_history_multiple_edits"); test("modify first version but save both", function () { stop(); - expect(6); + expect(7); var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "memory" - } - }); + type: "bryan", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: "testdb1" + } + } + }), + not_bryan = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "indexeddb", + database: "testdb1" + } + }); jio.put("main_doc", { "title": "rev0", "subtitle": "subrev0" @@ -113,49 +210,67 @@ "subtitle": "subrev2" }); }) + .push(function () { + return jio.put("main_doc", { + "title": "rev3", + "subtitle": "subrev3" + }); + }) .push(function () {return jio.get("main_doc"); }) .push(function (result) { deepEqual(result, { - "title": "rev2", - "subtitle": "subrev2", - "_revision": 2, - "_doc_id": "main_doc" + "title": "rev3", + "subtitle": "subrev3" }, "Retrieve main document correctly"); }) .push(function () {return jio.get("other_doc"); }) .push(function (result) { deepEqual(result, { "attr": "version1", - "subattr": "subversion1", - "_revision": 1, - "_doc_id": "other_doc" + "subattr": "subversion1" }, "Retrieve other document correctly"); }) - .push(function () { return jio.buildQuery({ - query: '(_doc_id: "main_doc") AND (_revision: 0)', - sort_on: [['_revision', 'descending']] + query: "" }); }) .push(function (result) { - equal(result.length, 1, "Correct number of results returned"); + //console.log(result); + equal(result.length, 2, "Empty query returns only non-deprecated docs"); + }) + .push(function () { + return jio.buildQuery({ + query: 'attr: "version1"' + }); + }) + .push(function (result) { + //console.log("res:", result); + if (result.length > 0) { + return jio.get(result[0].id); + } + }) + .push(function (result) { + deepEqual(result, { + "attr": "version1", + "subattr": "subversion1" + }, "Retrieve other document correctly"); }) .push(function () { return jio.buildQuery({ - query: '(_doc_id: "main_doc") AND (_revision: 1)' + query: '(_doc_id: "other_doc")' }); }) .push(function (result) { - equal(result.length, 1, "Correct number of results returned"); + equal(result.length, 0, "Correct number of results returned"); }) .push(function () { return jio.buildQuery({ - query: '(_doc_id: "other_doc") AND (_revision: 0)' + query: '(_revision: 0)' }); }) .push(function (result) { - equal(result.length, 1, "Correct number of results returned"); + equal(result.length, 0, "Correct number of results returned"); }) .push(function () { return jio.buildQuery({ @@ -163,7 +278,52 @@ }); }) .push(function (result) { - equal(result.length, 5, "Correct number of results returned"); + equal(result.length, 2, "Correct number of results returned"); + }) + + // When not_bryan queries the storage, all documents are returned. + .push(function () { + var options = { + query: "_doc_id: main_doc", + sort_on: [["_revision", "ascending"]] + }; + return not_bryan.buildQuery(options); + }) + .push(function (results) { + equal(results.length, 3, "should get all 3 deprecated versions."); + return not_bryan.get(results[0].id); + }) + .push(function (results) { + deepEqual(results, { + "title": "rev0", + "subtitle": "subrev0", + "_doc_id": "main_doc", + "_revision": 0, + "_deprecated": true + }, + "Get the earliest copy of the doc with all metadata."); + }) + + // When not_bryan queries the storage, all documents are returned. + .push(function () { + var options = { + query: "_doc_id: main_doc", + sort_on: [["_revision", "ascending"]] + }; + return not_bryan.buildQuery(options); + }) + .push(function (results) { + return not_bryan.get(results[1].id); + }) + .push(function (results) { + deepEqual(results, { + "title": "rev1", + "subtitle": "subrev1", + "_doc_id": "main_doc", + "_revision": 1, + "_deprecated": true + }, + "Get the earliest copy of the doc with all metadata."); }) .fail(function (error) { //console.log(error); @@ -174,6 +334,4 @@ }); }); -}(jIO, QUnit)); - - +}(jIO, QUnit)); \ No newline at end of file -- 2.30.9 From d7ecff55bce6919848f8a6859bb9977efed780d1 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Mon, 4 Jun 2018 08:54:19 +0000 Subject: [PATCH 10/46] Still debugging query issue. --- src/jio.storage/bryanstorage.js | 8 +++-- test/jio.storage/bryanstorage.tests.js | 41 ++++++++++++++++---------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index 89a63f2..9918296 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -48,7 +48,9 @@ sort_on: [['_revision', 'descending']], limit: [0, 1] }; - return substorage.buildQuery(options); + //return substorage.buildQuery(options); + return substorage.allDocs(options); + }) .push(function (query_results) { if (query_results.length > 0) { @@ -141,21 +143,21 @@ BryanStorage.prototype.hasCapacity = function () { return this._sub_storage.hasCapacity.apply(this._sub_storage, arguments); }; + /** BryanStorage.prototype.allDocs = function (options) { if (options === undefined) { options = {}; } console.log("options", options); - /** if (options === undefined) { options = {query: ""}; } options.query = '(' + options.query + ') AND NOT (_deprecated = true)'; console.log("query string: ", options.query); - **/ return this._sub_storage.allDocs.apply(this._sub_storage, options); //return this._sub_storage.buildQuery.apply(this._sub_storage, options); }; + **/ BryanStorage.prototype.buildQuery = function (options) { if (options === undefined) { options = {}; diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index 89c7f1b..02fa651 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -25,7 +25,12 @@ // create storage of type "bryan" with memory as substorage var jio = jIO.createJIO({ type: "bryan", - sub_storage: {type: "memory"} + sub_storage: { + type: "uuid", + sub_storage: { + type: "memory" + } + } }); jio.put("bar", {"title": "foo"}); @@ -54,12 +59,12 @@ sub_storage: { type: "uuid", sub_storage: { - //type: "memory" - type: "indexeddb", - database: dbname + type: "memory" + //type: "indexeddb", + //database: dbname } } - }), + });/**, not_bryan = jIO.createJIO({ type: "uuid", sub_storage: { @@ -71,10 +76,12 @@ } } }); + **/ jio.put("doc1", { "title": "rev0", "subtitle": "subrev0" }) + /** .push(function () {return jio.get("doc1"); }) .push(function (result) { deepEqual(result, { @@ -103,14 +110,15 @@ "subtitle": "subrev2" }, "Retrieve second edition of document correctly"); }) + **/ .push(function () { var options = { - //query: ""//title: rev2" + query: "title: rev0" }; // // - return jio.buildQuery(options); - //return jio.allDocs(options); + //return jio.buildQuery(options); + return jio.allDocs(options); // // }) @@ -127,7 +135,7 @@ "subtitle": "subrev2" }, "Retrieve queried document correctly"); }) - + /** // When not_bryan queries the storage, all documents are returned. .push(function () { var options = { @@ -152,6 +160,7 @@ }, "Get the earliest copy of the doc with all metadata."); }) + **/ .fail(function (error) { console.log(error); ok(false, error); @@ -231,7 +240,7 @@ }, "Retrieve other document correctly"); }) .push(function () { - return jio.buildQuery({ + return jio.allDocs({ query: "" }); }) @@ -240,7 +249,7 @@ equal(result.length, 2, "Empty query returns only non-deprecated docs"); }) .push(function () { - return jio.buildQuery({ + return jio.allDocs({ query: 'attr: "version1"' }); }) @@ -257,7 +266,7 @@ }, "Retrieve other document correctly"); }) .push(function () { - return jio.buildQuery({ + return jio.allDocs({ query: '(_doc_id: "other_doc")' }); }) @@ -265,7 +274,7 @@ equal(result.length, 0, "Correct number of results returned"); }) .push(function () { - return jio.buildQuery({ + return jio.allDocs({ query: '(_revision: 0)' }); }) @@ -273,7 +282,7 @@ equal(result.length, 0, "Correct number of results returned"); }) .push(function () { - return jio.buildQuery({ + return jio.allDocs({ query: '' }); }) @@ -287,7 +296,7 @@ query: "_doc_id: main_doc", sort_on: [["_revision", "ascending"]] }; - return not_bryan.buildQuery(options); + return not_bryan.allDocs(options); }) .push(function (results) { equal(results.length, 3, "should get all 3 deprecated versions."); @@ -310,7 +319,7 @@ query: "_doc_id: main_doc", sort_on: [["_revision", "ascending"]] }; - return not_bryan.buildQuery(options); + return not_bryan.allDocs(options); }) .push(function (results) { return not_bryan.get(results[1].id); -- 2.30.9 From adb69072525da2de204d524a46a2f117525608fb Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Mon, 4 Jun 2018 09:31:50 +0000 Subject: [PATCH 11/46] Tests are not passing, but no longer having error with options passed to QueryStorage.buildQuery as undefined. --- src/jio.storage/bryanstorage.js | 9 ++++-- test/jio.storage/bryanstorage.tests.js | 40 +++++++++++--------------- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index 9918296..71dd592 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -158,12 +158,15 @@ //return this._sub_storage.buildQuery.apply(this._sub_storage, options); }; **/ - BryanStorage.prototype.buildQuery = function (options) { + BryanStorage.prototype.buildQuery = function () { + /** if (options === undefined) { options = {}; } - console.log("options", options); - return this._sub_storage.buildQuery.apply(this._sub_storage, options); + options.query = '(' + options.query + ') AND NOT (_deprecated = true)'; + **/ + console.log("options", arguments); + return this._sub_storage.buildQuery.apply(this._sub_storage, arguments); }; jIO.addStorage('bryan', BryanStorage); diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index 02fa651..06edd9b 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -11,11 +11,10 @@ equal = QUnit.equal, module = QUnit.module; - + /** ///////////////////////////////////////////////////////////////// // _revision parameter updating with RSVP all ///////////////////////////////////////////////////////////////// - /** module("bryanStorage _revision with RSVP all"); test("verifying _revision updates correctly when puts are done in parallel", function () { @@ -34,11 +33,13 @@ }); jio.put("bar", {"title": "foo"}); - RSVP.all( + RSVP.all([ jio.put("bar", {"title2": "foo2"}), jio.put("bar", {"title3": "foo3"}) - ) - .push(function () {return jio.get("bar"); }) + ] + ); + //.push(function () {return jio.get("bar"); }) + jio.get("bar") .push(function (result) {equal(result._revision, 3, "parallel exec"); }) .fail(function (error) {ok(false, error); }) .always(function () {start(); }); @@ -53,18 +54,18 @@ test("put and get the correct version", function () { stop(); expect(4); - var dbname = "testingdb4", + var dbname = "freshdb0", jio = jIO.createJIO({ type: "bryan", sub_storage: { type: "uuid", sub_storage: { - type: "memory" - //type: "indexeddb", - //database: dbname + //type: "memory" + type: "indexeddb", + database: dbname } } - });/**, + }), not_bryan = jIO.createJIO({ type: "uuid", sub_storage: { @@ -76,12 +77,12 @@ } } }); - **/ + jio.put("doc1", { "title": "rev0", "subtitle": "subrev0" }) - /** + .push(function () {return jio.get("doc1"); }) .push(function (result) { deepEqual(result, { @@ -110,17 +111,10 @@ "subtitle": "subrev2" }, "Retrieve second edition of document correctly"); }) - **/ + .push(function () { - var options = { - query: "title: rev0" - }; - // - // - //return jio.buildQuery(options); + var options = {query: "title: rev0"}; return jio.allDocs(options); - // - // }) .push(function (results) { console.log("query results: ", results); @@ -135,7 +129,7 @@ "subtitle": "subrev2" }, "Retrieve queried document correctly"); }) - /** + // When not_bryan queries the storage, all documents are returned. .push(function () { var options = { @@ -160,7 +154,7 @@ }, "Get the earliest copy of the doc with all metadata."); }) - **/ + .fail(function (error) { console.log(error); ok(false, error); -- 2.30.9 From 30b0e964a51621f4861bfce104e7a8a7e66f51ef Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Tue, 5 Jun 2018 08:02:06 +0000 Subject: [PATCH 12/46] Revision history tests are passing as expected now. --- src/jio.storage/bryanstorage.js | 34 ++--- test/jio.storage/bryanstorage.tests.js | 197 +++++++++++++++---------- 2 files changed, 131 insertions(+), 100 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index 71dd592..6cf22c3 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -39,7 +39,7 @@ // Prepare to post the current doc as a deprecated version previous_data = latest_data; - previous_data._deprecated = true; + previous_data._deprecated = "true"; previous_data._doc_id = id; // Get most recent deprecated version's _revision attribute @@ -53,9 +53,9 @@ }) .push(function (query_results) { - if (query_results.length > 0) { - var doc_id = query_results[0]; - return this._sub_storage.get(doc_id); + if (query_results.data.rows.length > 0) { + var doc_id = query_results.data.rows[0].id; + return substorage.get(doc_id); } throw new jIO.util.jIOError( "bryanstorage: query returned no results.'", @@ -143,30 +143,16 @@ BryanStorage.prototype.hasCapacity = function () { return this._sub_storage.hasCapacity.apply(this._sub_storage, arguments); }; - /** - BryanStorage.prototype.allDocs = function (options) { - if (options === undefined) { - options = {}; - } - console.log("options", options); + + BryanStorage.prototype.buildQuery = function (options) { if (options === undefined) { options = {query: ""}; } - options.query = '(' + options.query + ') AND NOT (_deprecated = true)'; - console.log("query string: ", options.query); - return this._sub_storage.allDocs.apply(this._sub_storage, options); - //return this._sub_storage.buildQuery.apply(this._sub_storage, options); - }; - **/ - BryanStorage.prototype.buildQuery = function () { - /** - if (options === undefined) { - options = {}; + if (options.query !== "") { + options.query = "(" + options.query + ") AND "; } - options.query = '(' + options.query + ') AND NOT (_deprecated = true)'; - **/ - console.log("options", arguments); - return this._sub_storage.buildQuery.apply(this._sub_storage, arguments); + options.query = options.query + 'NOT (_deprecated: "true")'; + return this._sub_storage.buildQuery(options); }; jIO.addStorage('bryan', BryanStorage); diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index 06edd9b..6ac2c2a 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -30,21 +30,43 @@ type: "memory" } } - }); + }), + not_bryan = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "memory" + } + } + }); - jio.put("bar", {"title": "foo"}); + jio.put("bar", {"title": "foo0"}); RSVP.all([ - jio.put("bar", {"title2": "foo2"}), - jio.put("bar", {"title3": "foo3"}) - ] - ); - //.push(function () {return jio.get("bar"); }) - jio.get("bar") - .push(function (result) {equal(result._revision, 3, "parallel exec"); }) + jio.put("bar", {"title": "foo1"}), + jio.put("bar", {"title": "foo2"}), + jio.put("bar", {"title": "foo3"}), + jio.put("bar", {"title": "foo4"}) + ]) + .push(function () {return jio.get("bar"); }) + .push(function (result) { + deepEqual(result, { + "title": "foo4" + }); + }) + .push(function () {return not_bryan.allDocs({ + query: "_revision: 0" + }); + }) + .push(function (results) { + equal(results.data.rows.length, + 1, + "Only one document with _revision = 0"); + }) .fail(function (error) {ok(false, error); }) .always(function () {start(); }); }); - **/ + **/ ///////////////////////////////////////////////////////////////// @@ -53,8 +75,8 @@ module("bryanStorage.revision_history"); test("put and get the correct version", function () { stop(); - expect(4); - var dbname = "freshdb0", + expect(7); + var dbname = "rev_hist_db0", jio = jIO.createJIO({ type: "bryan", sub_storage: { @@ -76,20 +98,22 @@ database: dbname } } - }); + }), + query_input = + { + query: 'NOT (_deprecated: "true")', + sort_on: [['_revision', 'descending']] + }, + query_input2 = + { + query: 'title: "rev1"', + sort_on: [['_revision', 'descending']] + }; jio.put("doc1", { "title": "rev0", "subtitle": "subrev0" }) - - .push(function () {return jio.get("doc1"); }) - .push(function (result) { - deepEqual(result, { - "title": "rev0", - "subtitle": "subrev0" - }, "Retrieve first edition of document correctly"); - }) .push(function () { return jio.put("doc1", { "title": "rev1", @@ -103,60 +127,66 @@ }); }) .push(function () { - return jio.get("doc1"); + return jio.put("doc1", { + "title": "rev3", + "subtitle": "subrev3" + }); }) + .push(function () {return jio.get("doc1"); }) .push(function (result) { deepEqual(result, { - "title": "rev2", - "subtitle": "subrev2" - }, "Retrieve second edition of document correctly"); + "title": "rev3", + "subtitle": "subrev3" + }, "Retrieve first edition of document correctly"); }) - .push(function () { - var options = {query: "title: rev0"}; - return jio.allDocs(options); + return not_bryan.allDocs(query_input); }) .push(function (results) { - console.log("query results: ", results); - equal(results.data.rows.length, 1, "Query only returns latest version"); - if (results.data.rows.length > 0) { - return jio.get(results.data.rows[0].id); - } + equal(results.data.rows.length, 1, "Only 1 version isn't _deprecated"); + return jio.get(results.data.rows[0].id); }) .push(function (result) { deepEqual(result, { - "title": "rev2", - "subtitle": "subrev2" - }, "Retrieve queried document correctly"); + "title": "rev3", + "subtitle": "subrev3" + }, "Retrieve most recent edition by querying NOT _deprecated"); }) - - // When not_bryan queries the storage, all documents are returned. .push(function () { - var options = { - query: "", - sort_on: [["_revision", "ascending"]] - }; - return jio.allDocs(options); + return not_bryan.allDocs(query_input2); }) .push(function (results) { - equal(results.length, 2, "should get all 2 revisions."); - if (results.length > 0) { - return not_bryan.get(results[0].id); - } + equal(results.data.rows.length, 1, "Only one version is titled 'rev1'"); + return jio.get(results.data.rows[0].id); }) - .push(function (results) { - deepEqual(results, { - "title": "rev0", - "subtitle": "subrev0", - "_doc_id": "doc1", - "_revision": 0, - "_deprecated": true + .push(function (result) { + deepEqual(result, { + "title": "rev1", + "subtitle": "subrev1", + "_deprecated": "true", + "_revision": 1, + "_doc_id": "doc1" }, - "Get the earliest copy of the doc with all metadata."); + "Retrieve 1st edit by querying for title: 'rev1' with other storage"); + }) + .push(function () { + return jio.allDocs({query: ''}); + }) + .push(function (results) { + equal(results.data.rows.length, + 1, + "bryanstorage only sees latest version"); + return jio.get(results.data.rows[0].id); }) + .push(function (result) { + deepEqual(result, { + "title": "rev3", + "subtitle": "subrev3" + }, "Retrieve latest version correctly with bryanstorage"); + }) .fail(function (error) { - console.log(error); + //console.log(error); ok(false, error); }) .always(function () { @@ -164,25 +194,35 @@ }); }); + + ///////////////////////////////////////////////////////////////// + // bryanStorage.revision_history_multiple_edits + ///////////////////////////////////////////////////////////////// + + module("bryanStorage.revision_history_multiple_edits"); test("modify first version but save both", function () { stop(); - expect(7); - var jio = jIO.createJIO({ + expect(11); + var dbname = "testdb20", + jio = jIO.createJIO({ type: "bryan", sub_storage: { type: "uuid", sub_storage: { type: "indexeddb", - database: "testdb1" + database: dbname } } }), not_bryan = jIO.createJIO({ - type: "uuid", + type: "query", sub_storage: { - type: "indexeddb", - database: "testdb1" + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } } }); jio.put("main_doc", { @@ -239,8 +279,9 @@ }); }) .push(function (result) { - //console.log(result); - equal(result.length, 2, "Empty query returns only non-deprecated docs"); + equal(result.data.rows.length, + 2, + "Empty query returns only non-deprecated docs"); }) .push(function () { return jio.allDocs({ @@ -248,16 +289,18 @@ }); }) .push(function (result) { - //console.log("res:", result); - if (result.length > 0) { - return jio.get(result[0].id); + equal(result.data.rows.length, + 1, + "No deprecated results are returned."); + if (result.data.rows.length > 0) { + return jio.get(result.data.rows[0].id); } }) .push(function (result) { deepEqual(result, { "attr": "version1", "subattr": "subversion1" - }, "Retrieve other document correctly"); + }, "Only get most recent edit"); }) .push(function () { return jio.allDocs({ @@ -265,7 +308,7 @@ }); }) .push(function (result) { - equal(result.length, 0, "Correct number of results returned"); + equal(result.data.rows.length, 0, "Correct number of results returned"); }) .push(function () { return jio.allDocs({ @@ -273,7 +316,7 @@ }); }) .push(function (result) { - equal(result.length, 0, "Correct number of results returned"); + equal(result.data.rows.length, 0, "Correct number of results returned"); }) .push(function () { return jio.allDocs({ @@ -281,7 +324,7 @@ }); }) .push(function (result) { - equal(result.length, 2, "Correct number of results returned"); + equal(result.data.rows.length, 2, "Correct number of results returned"); }) // When not_bryan queries the storage, all documents are returned. @@ -293,8 +336,10 @@ return not_bryan.allDocs(options); }) .push(function (results) { - equal(results.length, 3, "should get all 3 deprecated versions."); - return not_bryan.get(results[0].id); + equal(results.data.rows.length, + 3, + "should get all 3 deprecated versions."); + return not_bryan.get(results.data.rows[0].id); }) .push(function (results) { deepEqual(results, { @@ -302,7 +347,7 @@ "subtitle": "subrev0", "_doc_id": "main_doc", "_revision": 0, - "_deprecated": true + "_deprecated": "true" }, "Get the earliest copy of the doc with all metadata."); }) @@ -316,7 +361,7 @@ return not_bryan.allDocs(options); }) .push(function (results) { - return not_bryan.get(results[1].id); + return not_bryan.get(results.data.rows[1].id); }) .push(function (results) { deepEqual(results, { @@ -324,7 +369,7 @@ "subtitle": "subrev1", "_doc_id": "main_doc", "_revision": 1, - "_deprecated": true + "_deprecated": "true" }, "Get the earliest copy of the doc with all metadata."); }) -- 2.30.9 From dc356c9802672b34965d31d2b2878cab5abdd11d Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Tue, 5 Jun 2018 10:09:28 +0000 Subject: [PATCH 13/46] Replaced _revision counter with a _timestamp identifier which allows parallel put calls to work. --- src/jio.storage/bryanstorage.js | 57 +++---------- test/jio.storage/bryanstorage.tests.js | 109 ++++++++++++++----------- 2 files changed, 71 insertions(+), 95 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index 6cf22c3..242ec74 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -25,58 +25,20 @@ }; BryanStorage.prototype.post = function (metadata) { - // Uses UuidStorage post return this._sub_storage.post(metadata); }; - BryanStorage.prototype.put = function (id, new_metadata) { - var storage = this, - substorage = this._sub_storage, - previous_data; + BryanStorage.prototype.put = function (id, metadata) { + var storage = this; - return this._sub_storage.get(id) - .push(function (latest_data) { - - // Prepare to post the current doc as a deprecated version - previous_data = latest_data; - previous_data._deprecated = "true"; - previous_data._doc_id = id; - - // Get most recent deprecated version's _revision attribute - var options = { - query: '(_doc_id: "' + id + '")', - sort_on: [['_revision', 'descending']], - limit: [0, 1] - }; - //return substorage.buildQuery(options); - return substorage.allDocs(options); - - }) - .push(function (query_results) { - if (query_results.data.rows.length > 0) { - var doc_id = query_results.data.rows[0].id; - return substorage.get(doc_id); - } - throw new jIO.util.jIOError( - "bryanstorage: query returned no results.'", - 404 - ); - }) - .push(function (doc) { - previous_data._revision = doc._revision + 1; - return storage.post(previous_data); - }, - function () { - // If the query turned up no results, - // there was exactly 1 version previously. - if (previous_data !== undefined) { - previous_data._revision = 0; - return storage.post(previous_data); - } - }) - // No matter what happened, need to put new document in + return this._sub_storage.put(id, metadata) .push(function () { - return substorage.put(id, new_metadata); + + // Also push a metadata document recording the posting time + metadata._deprecated = "true"; + metadata._doc_id = id; + metadata._timestamp = Date.now(); + return storage.post(metadata); }); }; @@ -151,6 +113,7 @@ if (options.query !== "") { options.query = "(" + options.query + ") AND "; } + options.query = options.query + 'NOT (_deprecated: "true")'; return this._sub_storage.buildQuery(options); }; diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index 6ac2c2a..e49b79b 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -11,72 +11,94 @@ equal = QUnit.equal, module = QUnit.module; - /** + ///////////////////////////////////////////////////////////////// // _revision parameter updating with RSVP all ///////////////////////////////////////////////////////////////// - module("bryanStorage _revision with RSVP all"); - test("verifying _revision updates correctly when puts are done in parallel", + module("bryanStorage revision with RSVP all"); + test("verifying updates correctly when puts are done in parallel", function () { stop(); - expect(1); + expect(3); // create storage of type "bryan" with memory as substorage - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "uuid", + var dbname = "rsvp_db_" + Date.now(), + jio = jIO.createJIO({ + type: "bryan", sub_storage: { - type: "memory" + type: "uuid", + sub_storage: { + //type: "memory" + type: "indexeddb", + database: dbname + } } - } - }), + }), not_bryan = jIO.createJIO({ type: "query", sub_storage: { type: "uuid", sub_storage: { - type: "memory" + //type: "memory" + type: "indexeddb", + database: dbname } } }); - jio.put("bar", {"title": "foo0"}); - RSVP.all([ - jio.put("bar", {"title": "foo1"}), - jio.put("bar", {"title": "foo2"}), - jio.put("bar", {"title": "foo3"}), - jio.put("bar", {"title": "foo4"}) - ]) + jio.put("bar", {"title": "foo0"}) + .push(function () { + return RSVP.all([ + jio.put("bar", {"title": "foo1"}), + jio.put("bar", {"title": "foo2"}), + jio.put("bar", {"title": "foo3"}), + jio.put("bar", {"title": "foo4"}) + ]); + }) .push(function () {return jio.get("bar"); }) .push(function (result) { deepEqual(result, { "title": "foo4" }); }) - .push(function () {return not_bryan.allDocs({ - query: "_revision: 0" - }); - }) + .push(function () { + return not_bryan.allDocs({ + query: "", + sort_on: [["_timestamp", "ascending"]] + }); + }) .push(function (results) { equal(results.data.rows.length, - 1, - "Only one document with _revision = 0"); + 6, + "Storage contains all 5 revisions plus the most recent one."); + return not_bryan.get(results.data.rows[1].id); + }) + .push(function (result) { + deepEqual(result, { + title: "foo0", + _doc_id: "bar", + _timestamp: result._timestamp, + _deprecated: "true" + }, + "Query returns the first edition of the document"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); }) - .fail(function (error) {ok(false, error); }) .always(function () {start(); }); }); - **/ ///////////////////////////////////////////////////////////////// // bryanStorage revision history ///////////////////////////////////////////////////////////////// + module("bryanStorage.revision_history"); test("put and get the correct version", function () { stop(); expect(7); - var dbname = "rev_hist_db0", + var dbname = "rev_hist_db" + Date.now(), jio = jIO.createJIO({ type: "bryan", sub_storage: { @@ -102,12 +124,12 @@ query_input = { query: 'NOT (_deprecated: "true")', - sort_on: [['_revision', 'descending']] + sort_on: [['_timestamp', 'descending']] }, query_input2 = { query: 'title: "rev1"', - sort_on: [['_revision', 'descending']] + sort_on: [['_timestamp', 'descending']] }; jio.put("doc1", { @@ -164,7 +186,7 @@ "title": "rev1", "subtitle": "subrev1", "_deprecated": "true", - "_revision": 1, + "_timestamp": result._timestamp, "_doc_id": "doc1" }, "Retrieve 1st edit by querying for title: 'rev1' with other storage"); @@ -199,12 +221,11 @@ // bryanStorage.revision_history_multiple_edits ///////////////////////////////////////////////////////////////// - module("bryanStorage.revision_history_multiple_edits"); test("modify first version but save both", function () { stop(); - expect(11); - var dbname = "testdb20", + expect(10); + var dbname = "rev_hist_mult_db" + Date.now(), jio = jIO.createJIO({ type: "bryan", sub_storage: { @@ -310,14 +331,6 @@ .push(function (result) { equal(result.data.rows.length, 0, "Correct number of results returned"); }) - .push(function () { - return jio.allDocs({ - query: '(_revision: 0)' - }); - }) - .push(function (result) { - equal(result.data.rows.length, 0, "Correct number of results returned"); - }) .push(function () { return jio.allDocs({ query: '' @@ -331,14 +344,14 @@ .push(function () { var options = { query: "_doc_id: main_doc", - sort_on: [["_revision", "ascending"]] + sort_on: [["_timestamp", "ascending"]] }; return not_bryan.allDocs(options); }) .push(function (results) { equal(results.data.rows.length, - 3, - "should get all 3 deprecated versions."); + 4, + "should get all 3 deprecated versions plus one copy of the latest."); return not_bryan.get(results.data.rows[0].id); }) .push(function (results) { @@ -346,7 +359,7 @@ "title": "rev0", "subtitle": "subrev0", "_doc_id": "main_doc", - "_revision": 0, + "_timestamp": results._timestamp, "_deprecated": "true" }, "Get the earliest copy of the doc with all metadata."); @@ -356,7 +369,7 @@ .push(function () { var options = { query: "_doc_id: main_doc", - sort_on: [["_revision", "ascending"]] + sort_on: [["_timestamp", "ascending"]] }; return not_bryan.allDocs(options); }) @@ -368,7 +381,7 @@ "title": "rev1", "subtitle": "subrev1", "_doc_id": "main_doc", - "_revision": 1, + "_timestamp": results._timestamp, "_deprecated": "true" }, "Get the earliest copy of the doc with all metadata."); -- 2.30.9 From 0d5eba329e42a1cb7de8675c57f74e8313c06839 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Tue, 5 Jun 2018 16:05:08 +0000 Subject: [PATCH 14/46] Revised bryanstorage so that a log of puts and removes is recorded. All tests involving getting, putting, removing, and querying from other storages are passing. Currently, the allDocs method is not implemented correctly, but there are several placeholder tests written which demonstrate the desired functionality. --- src/jio.storage/bryanstorage.js | 84 +++-- test/jio.storage/bryanstorage.tests.js | 423 +++++++------------------ 2 files changed, 176 insertions(+), 331 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index 242ec74..b5d9070 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -3,8 +3,13 @@ (function (jIO) { "use strict"; - // Metadata keys included for internal revisioning, but not shown to user - //var _revision_metadata = ["_revision", "_doc_id"]; + var unique_timestamp = function () { + // Used to distinguish between operations done within the same millisecond + var uuid = ('0000' + Math.floor(Math.random() * 0x10000) + .toString(16)).slice(-4), + timestamp = Date.now().toString(); + return timestamp + "-" + uuid; + }; /** * The jIO BryanStorage extension @@ -21,34 +26,69 @@ } BryanStorage.prototype.get = function (id_in) { - return this._sub_storage.get(id_in); + + // Query to get the last edit made to this document + var substorage = this._sub_storage, + options = { + query: "doc_id: " + id_in, + sort_on: [["timestamp", "descending"]], + limit: [0, 1] + }; + return substorage.allDocs(options) + .push(function (results) { + if (results.data.rows.length > 0) { + return substorage.get(results.data.rows[0].id); + } + throw new jIO.util.jIOError( + "bryanstorage: cannot find object '" + id_in + "'", + 404 + ); + }) + .push(function (result) { + // If last edit was a remove, throw a 'not found' error + if (result.op === "remove") { + throw new jIO.util.jIOError( + "bryanstorage: cannot find object '" + id_in + "' (removed)", + 404 + ); + } + // If last edit was a put, return the document data + if (result.op === "put") { + return result.doc; + } + }); }; BryanStorage.prototype.post = function (metadata) { return this._sub_storage.post(metadata); }; - BryanStorage.prototype.put = function (id, metadata) { - var storage = this; - - return this._sub_storage.put(id, metadata) - .push(function () { + BryanStorage.prototype.put = function (id, data) { + var timestamp = unique_timestamp(), + metadata = { + // XXX: remove this attribute once query can sort_on id + timestamp: timestamp, + doc_id: id, + doc: data, + op: "put" + }; + return this._sub_storage.put(timestamp, metadata); + }; - // Also push a metadata document recording the posting time - metadata._deprecated = "true"; - metadata._doc_id = id; - metadata._timestamp = Date.now(); - return storage.post(metadata); - }); + BryanStorage.prototype.remove = function (id) { + var timestamp = unique_timestamp(), + metadata = { + // XXX: remove this attribute once query can sort_on id + timestamp: timestamp, + doc_id: id, + op: "remove" + }; + return this._sub_storage.put(timestamp, metadata); }; BryanStorage.prototype.allAttachments = function () { return this._sub_storage.allAttachments.apply(this._sub_storage, arguments); }; - - BryanStorage.prototype.remove = function () { - return this._sub_storage.remove.apply(this._sub_storage, arguments); - }; BryanStorage.prototype.getAttachment = function () { return this._sub_storage.getAttachment.apply(this._sub_storage, arguments); }; @@ -107,14 +147,6 @@ }; BryanStorage.prototype.buildQuery = function (options) { - if (options === undefined) { - options = {query: ""}; - } - if (options.query !== "") { - options.query = "(" + options.query + ") AND "; - } - - options.query = options.query + 'NOT (_deprecated: "true")'; return this._sub_storage.buildQuery(options); }; diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index e49b79b..ce95a9e 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -15,11 +15,12 @@ ///////////////////////////////////////////////////////////////// // _revision parameter updating with RSVP all ///////////////////////////////////////////////////////////////// - module("bryanStorage revision with RSVP all"); + + module("bryanStorage.revision_with_RSVP_all"); test("verifying updates correctly when puts are done in parallel", function () { stop(); - expect(3); + expect(7); // create storage of type "bryan" with memory as substorage var dbname = "rsvp_db_" + Date.now(), @@ -52,35 +53,72 @@ jio.put("bar", {"title": "foo1"}), jio.put("bar", {"title": "foo2"}), jio.put("bar", {"title": "foo3"}), - jio.put("bar", {"title": "foo4"}) + jio.put("bar", {"title": "foo4"}), + jio.put("barbar", {"title": "attr0"}), + jio.put("barbar", {"title": "attr1"}), + jio.put("barbar", {"title": "attr2"}), + jio.put("barbar", {"title": "attr3"}) ]); }) .push(function () {return jio.get("bar"); }) .push(function (result) { - deepEqual(result, { - "title": "foo4" + ok(result.title !== "foo0", "Title should have changed from foo0"); + }) + .push(function () { + return not_bryan.allDocs({ + query: "", + sort_on: [["timestamp", "ascending"]] }); }) + .push(function (results) { + equal(results.data.rows.length, + 9, + "All nine versions exist in storage"); + return not_bryan.get(results.data.rows[0].id); + }) + .push(function (results) { + deepEqual(results, { + doc_id: "bar", + doc: { + title: "foo0" + }, + timestamp: results.timestamp, + op: "put" + }, "The first item in the log is pushing bar's title to 'foo0'"); + return jio.remove("bar"); + }) .push(function () { + return jio.get("bar"); + }) + .push(function () { + return jio.get("barbar"); + }, function (error) { + deepEqual( + error.message, + "bryanstorage: cannot find object 'bar' (removed)", + "Appropriate error is sent explaining object has been removed" + ); + return jio.get("barbar"); + }) + .push(function (result) { + ok(result.title !== undefined, "barbar exists and has proper form"); return not_bryan.allDocs({ query: "", - sort_on: [["_timestamp", "ascending"]] + sort_on: [["op", "descending"]] }); }) .push(function (results) { equal(results.data.rows.length, - 6, - "Storage contains all 5 revisions plus the most recent one."); - return not_bryan.get(results.data.rows[1].id); + 10, + "Remove operation is recorded"); + return not_bryan.get(results.data.rows[0].id); }) .push(function (result) { deepEqual(result, { - title: "foo0", - _doc_id: "bar", - _timestamp: result._timestamp, - _deprecated: "true" - }, - "Query returns the first edition of the document"); + doc_id: "bar", + timestamp: result.timestamp, + op: "remove" + }); }) .fail(function (error) { //console.log(error); @@ -91,308 +129,83 @@ ///////////////////////////////////////////////////////////////// - // bryanStorage revision history + // bryanStorage.querying_from_bryanstorage ///////////////////////////////////////////////////////////////// - module("bryanStorage.revision_history"); - test("put and get the correct version", function () { - stop(); - expect(7); - var dbname = "rev_hist_db" + Date.now(), - jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "uuid", - sub_storage: { - //type: "memory" - type: "indexeddb", - database: dbname - } - } - }), - not_bryan = jIO.createJIO({ - type: "uuid", - sub_storage: { - type: "query", - sub_storage: { - //type: "memory" - type: "indexeddb", - database: dbname - } - } - }), - query_input = - { - query: 'NOT (_deprecated: "true")', - sort_on: [['_timestamp', 'descending']] - }, - query_input2 = - { - query: 'title: "rev1"', - sort_on: [['_timestamp', 'descending']] - }; - - jio.put("doc1", { - "title": "rev0", - "subtitle": "subrev0" - }) - .push(function () { - return jio.put("doc1", { - "title": "rev1", - "subtitle": "subrev1" - }); - }) - .push(function () { - return jio.put("doc1", { - "title": "rev2", - "subtitle": "subrev2" - }); - }) - .push(function () { - return jio.put("doc1", { - "title": "rev3", - "subtitle": "subrev3" - }); - }) - .push(function () {return jio.get("doc1"); }) - .push(function (result) { - deepEqual(result, { - "title": "rev3", - "subtitle": "subrev3" - }, "Retrieve first edition of document correctly"); - }) - .push(function () { - return not_bryan.allDocs(query_input); - }) - .push(function (results) { - equal(results.data.rows.length, 1, "Only 1 version isn't _deprecated"); - return jio.get(results.data.rows[0].id); - }) - .push(function (result) { - deepEqual(result, { - "title": "rev3", - "subtitle": "subrev3" - }, "Retrieve most recent edition by querying NOT _deprecated"); - }) - .push(function () { - return not_bryan.allDocs(query_input2); - }) - .push(function (results) { - equal(results.data.rows.length, 1, "Only one version is titled 'rev1'"); - return jio.get(results.data.rows[0].id); - }) - .push(function (result) { - deepEqual(result, { - "title": "rev1", - "subtitle": "subrev1", - "_deprecated": "true", - "_timestamp": result._timestamp, - "_doc_id": "doc1" - }, - "Retrieve 1st edit by querying for title: 'rev1' with other storage"); - }) - .push(function () { - return jio.allDocs({query: ''}); - }) - .push(function (results) { - equal(results.data.rows.length, - 1, - "bryanstorage only sees latest version"); - return jio.get(results.data.rows[0].id); - }) - - .push(function (result) { - deepEqual(result, { - "title": "rev3", - "subtitle": "subrev3" - }, "Retrieve latest version correctly with bryanstorage"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () { - start(); - }); - }); - - - ///////////////////////////////////////////////////////////////// - // bryanStorage.revision_history_multiple_edits - ///////////////////////////////////////////////////////////////// + module("bryanStorage.querying_from_bryanstorage"); + test("verifying the correct results are returned from bryanStorage.allDocs", + function () { + stop(); + expect(1); - module("bryanStorage.revision_history_multiple_edits"); - test("modify first version but save both", function () { - stop(); - expect(10); - var dbname = "rev_hist_mult_db" + Date.now(), - jio = jIO.createJIO({ + // create storage of type "bryan" with memory as substorage + var jio = jIO.createJIO({ type: "bryan", sub_storage: { type: "uuid", sub_storage: { - type: "indexeddb", - database: dbname - } - } - }), - not_bryan = jIO.createJIO({ - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname + type: "memory" } } }); - jio.put("main_doc", { - "title": "rev0", - "subtitle": "subrev0" - }) - .push(function () { - return jio.put("other_doc", { - "attr": "version0", - "subattr": "subversion0" - }); - }) - .push(function () { - return jio.put("other_doc", { - "attr": "version1", - "subattr": "subversion1" - }); - }) - .push(function () { - return jio.put("main_doc", { - "title": "rev1", - "subtitle": "subrev1" - }); - }) - .push(function () { - return jio.put("main_doc", { - "title": "rev2", - "subtitle": "subrev2" - }); - }) - .push(function () { - return jio.put("main_doc", { - "title": "rev3", - "subtitle": "subrev3" - }); - }) - .push(function () {return jio.get("main_doc"); }) - .push(function (result) { - deepEqual(result, { - "title": "rev3", - "subtitle": "subrev3" - }, "Retrieve main document correctly"); - }) - .push(function () {return jio.get("other_doc"); }) - .push(function (result) { - deepEqual(result, { - "attr": "version1", - "subattr": "subversion1" - }, "Retrieve other document correctly"); - }) - .push(function () { - return jio.allDocs({ - query: "" - }); - }) - .push(function (result) { - equal(result.data.rows.length, - 2, - "Empty query returns only non-deprecated docs"); - }) - .push(function () { - return jio.allDocs({ - query: 'attr: "version1"' - }); - }) - .push(function (result) { - equal(result.data.rows.length, - 1, - "No deprecated results are returned."); - if (result.data.rows.length > 0) { - return jio.get(result.data.rows[0].id); - } - }) - .push(function (result) { - deepEqual(result, { - "attr": "version1", - "subattr": "subversion1" - }, "Only get most recent edit"); - }) - .push(function () { - return jio.allDocs({ - query: '(_doc_id: "other_doc")' - }); - }) - .push(function (result) { - equal(result.data.rows.length, 0, "Correct number of results returned"); - }) - .push(function () { - return jio.allDocs({ - query: '' - }); - }) - .push(function (result) { - equal(result.data.rows.length, 2, "Correct number of results returned"); - }) + jio.put("bar", {"title": "foo0"}) + .push(function () { + return RSVP.all([ + jio.put("bar", {"title": "foo1"}), + jio.put("bar", {"title": "foo2"}), + jio.put("bar", {"title": "foo3"}), + jio.put("barbar", {"title": "attr0"}), + jio.put("barbar", {"title": "attr1"}), + jio.put("barbar", {"title": "attr2"}) + ]); + }) + // Make two final puts so we know what to expect as the current state of + // each document. + .push(function () { + return jio.put("bar", {"title": "foo4"}); + }) + .push(function () { + return jio.put("barbar", {"title": "attr3"}); + }) - // When not_bryan queries the storage, all documents are returned. - .push(function () { - var options = { - query: "_doc_id: main_doc", - sort_on: [["_timestamp", "ascending"]] - }; - return not_bryan.allDocs(options); - }) - .push(function (results) { - equal(results.data.rows.length, - 4, - "should get all 3 deprecated versions plus one copy of the latest."); - return not_bryan.get(results.data.rows[0].id); - }) - .push(function (results) { - deepEqual(results, { - "title": "rev0", - "subtitle": "subrev0", - "_doc_id": "main_doc", - "_timestamp": results._timestamp, - "_deprecated": "true" + // Queries should only include information about the final two versions + .push(function () { + return jio.allDocs({ + query: "", + sort_on: [["title", "ascending"]] + }); + }) + .push(function (results) { + equal(results.data.rows.length, + 2, + "Empty query yields two results since there are two unique docs"); + return jio.get(results.data.rows[0].id); + }) + .push(function (result) { + deepEqual(result, { + title: "attr3" + }, "Retrieve the first title in the correct format (no metadata)"); }, - "Get the earliest copy of the doc with all metadata."); - }) + function () { + return ok(false, "Couldn't find document in storage"); + }) - // When not_bryan queries the storage, all documents are returned. - .push(function () { - var options = { - query: "_doc_id: main_doc", - sort_on: [["_timestamp", "ascending"]] - }; - return not_bryan.allDocs(options); - }) - .push(function (results) { - return not_bryan.get(results.data.rows[1].id); - }) - .push(function (results) { - deepEqual(results, { - "title": "rev1", - "subtitle": "subrev1", - "_doc_id": "main_doc", - "_timestamp": results._timestamp, - "_deprecated": "true" - }, - "Get the earliest copy of the doc with all metadata."); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () { - start(); - }); - }); + // Querying for a specific id + .push(function () { + return jio.allDocs({ + query: "id: bar" + }); + }) + .push(function (result) { + deepEqual(result, { + title: "foo4" + }, "Retrieve correct document in correct format (no metadata)"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); }(jIO, QUnit)); \ No newline at end of file -- 2.30.9 From 6b8af9e6c993461878a82f0c38952f02fdbf4d1b Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Wed, 6 Jun 2018 17:03:24 +0000 Subject: [PATCH 15/46] Implemented the method of storing every operation done to each document instead of the documents explicitely. The tests which do not pass are labelled NOT IMPLEMENTED due to current limitations on the ability to query sub-attributes of documents. --- src/jio.storage/bryanstorage.js | 70 +++++++++++++++++++- test/jio.storage/bryanstorage.tests.js | 90 +++++++++++++++++++++++--- 2 files changed, 149 insertions(+), 11 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index b5d9070..1a84515 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -44,7 +44,10 @@ 404 ); }) + + // Decide return based on last edit type .push(function (result) { + // If last edit was a remove, throw a 'not found' error if (result.op === "remove") { throw new jIO.util.jIOError( @@ -52,6 +55,7 @@ 404 ); } + // If last edit was a put, return the document data if (result.op === "put") { return result.doc; @@ -147,7 +151,71 @@ }; BryanStorage.prototype.buildQuery = function (options) { - return this._sub_storage.buildQuery(options); + if (options.sort_on === undefined) { + options.sort_on = []; + } + if (options.limit === undefined) { + options.limit = [0, -1]; + } + options.sort_on.push(["timestamp", "descending"]); + var meta_options = { + // XXX: I don't believe it's currently possible to query on sub-attributes + // so for now, we just use the inputted query, which obviously will fail + query: options.query, + + // XXX: same here, we cannot sort correctly because we cannot access + // attributes of doc + sort_on: options.sort_on + }, + substorage = this._sub_storage, + max_num_docs = options.limit[1], + first_doc = options.limit[0]; + + return this._sub_storage.allDocs(meta_options) + .push(function (results) { + var promises = results.data.rows.map(function (data) { + return substorage.get(data.id); + }); + return RSVP.all(promises); + }) + .push(function (results_array) { + var clean_data = [], + ind, + seen_docs = [], + current_doc, + counter = 0; + if (max_num_docs === -1) { + max_num_docs = results_array.length; + } + for (ind = 0; ind < results_array.length; ind += 1) { + current_doc = results_array[ind]; + + // If the latest version of this document has not yet been + // included in query result + if (seen_docs[current_doc.doc_id] !== true) { + + // If the latest edit was a put operation, add it to query + // results + if (current_doc.op === "put") { + if (counter >= first_doc) { + clean_data.push({ + doc: {}, + value: {}, + id: current_doc.doc_id + }); + if (clean_data.length === max_num_docs) { + return clean_data; + } + } + counter += 1; + } + // Mark document as read so no older edits are considered + seen_docs[current_doc.doc_id] = true; + } + } + // In passing results back to allDocs, formatting of query is handled + return clean_data; + }); }; jIO.addStorage('bryan', BryanStorage); diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index ce95a9e..855451f 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -136,7 +136,7 @@ test("verifying the correct results are returned from bryanStorage.allDocs", function () { stop(); - expect(1); + expect(10); // create storage of type "bryan" with memory as substorage var jio = jIO.createJIO({ @@ -151,21 +151,24 @@ jio.put("bar", {"title": "foo0"}) .push(function () { return RSVP.all([ + jio.remove("bar"), jio.put("bar", {"title": "foo1"}), jio.put("bar", {"title": "foo2"}), jio.put("bar", {"title": "foo3"}), jio.put("barbar", {"title": "attr0"}), jio.put("barbar", {"title": "attr1"}), - jio.put("barbar", {"title": "attr2"}) + jio.put("barbar", {"title": "attr2"}), + jio.put("barbarbar", {"title": "val0"}), + jio.put("barbarbarbar", {"title": "prop0"}) ]); }) // Make two final puts so we know what to expect as the current state of // each document. .push(function () { - return jio.put("bar", {"title": "foo4"}); + return jio.put("barbar", {"title": "attr3"}); }) .push(function () { - return jio.put("barbar", {"title": "attr3"}); + return jio.put("bar", {"title": "foo4"}); }) // Queries should only include information about the final two versions @@ -177,30 +180,97 @@ }) .push(function (results) { equal(results.data.rows.length, - 2, - "Empty query yields two results since there are two unique docs"); + 4, + "Empty query yields four results since there are four unique docs"); return jio.get(results.data.rows[0].id); - }) + }, + function (error) { + return ok(false, "Query failed: " + error); + }) .push(function (result) { deepEqual(result, { title: "attr3" - }, "Retrieve the first title in the correct format (no metadata)"); + }, + "NOT IMPLEMENTED: Retrieve correct sort order with no metadata"); }, function () { return ok(false, "Couldn't find document in storage"); }) + // Querying with a limit + .push(function () { + return jio.allDocs({ + query: "", + sort_on: [["title", "ascending"]], + limit: [0, 1] + }); + }) + .push(function (results) { + equal(results.data.rows.length, + 1, + "Since limit [0,1] was supplied, only 1st document is returned"); + return jio.get(results.data.rows[0].id); + }) + .push(function (result) { + deepEqual(result, { + title: "attr3" + }, + "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); + }) + + // Querying with a more complicated limit + .push(function () { + return jio.allDocs({ + query: "", + sort_on: [["title", "ascending"]], + limit: [2, 2] + }); + }) + .push(function (results) { + equal(results.data.rows.length, + 2, + "Retrieving the correct documents when options.limit is specified"); + + deepEqual(results.data.rows[0].id, + "barbarbarbar", + "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); + + deepEqual(results.data.rows[1].id, + "barbarbar", + "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); + + return jio.get(results.data.rows[0].id); + }) + .push(function (result) { + deepEqual(result, { + title: "property0" + }, + "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); + }) + // Querying for a specific id .push(function () { return jio.allDocs({ query: "id: bar" }); }) + .push(function (results) { + equal(results.data.rows.length, + 1, + "NOT IMPLEMENTED: query involving specific document attributes"); + return jio.get(results.data.rows[0].id); + }) .push(function (result) { deepEqual(result, { title: "foo4" - }, "Retrieve correct document in correct format (no metadata)"); - }) + }, + "NOT IMPLEMENTED: query involving specific document attributes"); + }, + function () { + ok(false, + "NOT IMPLEMENTED: query involving specific document attributes" + ); + }) .fail(function (error) { //console.log(error); -- 2.30.9 From 22aba909888fc7dc33561d7fea7523117316dd0d Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Thu, 7 Jun 2018 10:03:45 +0000 Subject: [PATCH 16/46] Added an optional second parameter for .get to allow access to previous revisions of a document. --- src/jio.storage/bryanstorage.js | 13 +++- test/jio.storage/bryanstorage.tests.js | 102 +++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 3 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index 1a84515..5a3a061 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -3,8 +3,11 @@ (function (jIO) { "use strict"; + // Used to distinguish between operations done within the same millisecond var unique_timestamp = function () { - // Used to distinguish between operations done within the same millisecond + + // XXX: replace this with UUIDStorage function call to S4() when it becomes + // publicly accessible var uuid = ('0000' + Math.floor(Math.random() * 0x10000) .toString(16)).slice(-4), timestamp = Date.now().toString(); @@ -25,14 +28,18 @@ }); } - BryanStorage.prototype.get = function (id_in) { + BryanStorage.prototype.get = function (id_in, revision_steps) { + // Default behavior, get() returns the most recent revision + if (revision_steps === undefined) { + revision_steps = 0; + } // Query to get the last edit made to this document var substorage = this._sub_storage, options = { query: "doc_id: " + id_in, sort_on: [["timestamp", "descending"]], - limit: [0, 1] + limit: [revision_steps, 1] }; return substorage.allDocs(options) .push(function (results) { diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index 855451f..0ed86fd 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -278,4 +278,106 @@ }) .always(function () {start(); }); }); + + ///////////////////////////////////////////////////////////////// + // Accessing older revisions + ///////////////////////////////////////////////////////////////// + + module("bryanStorage.accessing_older_revisions"); + test("Testing proper retrieval of older revisions of documents", + function () { + stop(); + expect(8); + + // create storage of type "bryan" with memory as substorage + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "uuid", + sub_storage: { + type: "memory" + } + } + }); + + jio.put("doc", { + "k0": "v0" + }) + .push(function () { + return jio.put("doc", {"k1": "v1"}); + }) + .push(function () { + return jio.put("doc", {"k2": "v2"}); + }) + .push(function () { + return jio.remove("doc"); + }) + .push(function () { + return jio.put("doc", {"k3": "v3"}); + }) + .push(function () { + return jio.put("doc", {"k4": "v4"}); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (result) { + deepEqual(result, + {"k4": "v4"}, + "By default, .get returns latest revision"); + return jio.get("doc", 0); + }) + .push(function (result) { + deepEqual(result, + {"k4": "v4"}, + ".get returns latest revision with second input = 0"); + return jio.get("doc", 1); + }) + .push(function (result) { + deepEqual(result, + {"k3": "v3"}, + "Walk back one revision with second input = 1"); + return jio.get("doc", 2); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "Current state of document is 'removed'."); + return jio.get("doc", 3); + }) + .push(function (result) { + deepEqual(result, + {"k2": "v2"}, + "Walk back three revisions with second input = 3"); + return jio.get("doc", 4); + }) + .push(function (result) { + deepEqual(result, + {"k1": "v1"}, + "Walk back four revisions with second input = 4"); + return jio.get("doc", 5); + }) + .push(function (result) { + deepEqual(result, + {"k0": "v0"}, + "Walk back five revisions with second input = 5"); + return jio.get("doc", 6); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "There are only 5 previous states of this document"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); }(jIO, QUnit)); \ No newline at end of file -- 2.30.9 From 69da7ee56be87aa06308ab501e56495cbfdb9444 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Thu, 7 Jun 2018 15:50:44 +0000 Subject: [PATCH 17/46] Added functionality to .get which allows the user to step backward through the absolute revision history of a document, as well as the branch of revisions which have produced the latest form. --- src/jio.storage/bryanstorage.js | 94 ++++- test/jio.storage/bryanstorage.tests.js | 520 ++++++++++++++++++++++++- 2 files changed, 593 insertions(+), 21 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index 5a3a061..d5bc35c 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -26,21 +26,44 @@ type: "query", sub_storage: spec.sub_storage }); + this._lastseen = undefined; } - BryanStorage.prototype.get = function (id_in, revision_steps) { + BryanStorage.prototype.get = function (id_in, revision) { + // Default behavior, get() returns the most recent revision - if (revision_steps === undefined) { - revision_steps = 0; + if (revision === undefined) { + revision = { + steps: 0, + path: "absolute" + }; + } + + // Default type of traversal is absolute: + // "absolute" -- step backward in chronological order of changes to document + // "consistent" -- step backward in chronological order of only edits the + // most recent version is based on. Other branches of edits are ignored + if (revision.path === undefined) { + revision.path = "absolute"; } // Query to get the last edit made to this document - var substorage = this._sub_storage, + var storage = this, + substorage = this._sub_storage, options = { query: "doc_id: " + id_in, - sort_on: [["timestamp", "descending"]], - limit: [revision_steps, 1] + sort_on: [["timestamp", "descending"]] }; + + // In "absolute" path, .get returns the revision.steps-most-recent revision + if (revision.path === "absolute") { + options.limit = [revision.steps, 1]; + + // In "consistent path, .get returns the most recent revision and looks + // deeper into history with the result's .lastseen attribute + } else if (revision.path === "consistent") { + options.limit = [0, 1]; + } return substorage.allDocs(options) .push(function (results) { if (results.data.rows.length > 0) { @@ -52,20 +75,62 @@ ); }) - // Decide return based on last edit type .push(function (result) { + // Function used to chain together substorage.get's for "consistent" + // traversal + function recurse_get(result) { + if (result.lastseen === undefined) { + throw new jIO.util.jIOError( + "bryanstorage: cannot find object '" + + id_in + + "' (end of history)", + 404 + ); + } + return substorage.get(result.lastseen); + } + // If last edit was a remove, throw a 'not found' error - if (result.op === "remove") { + if (result.op === "remove" && revision.path === "absolute") { throw new jIO.util.jIOError( "bryanstorage: cannot find object '" + id_in + "' (removed)", 404 ); } - // If last edit was a put, return the document data if (result.op === "put") { - return result.doc; + + // The query for "absolute" traversal returns exactly the document + // requested + if (revision.path === "absolute" || revision.steps === 0) { + storage._lastseen = result.timestamp; + return result.doc; + } + if (revision.path === "consistent") { + + + // Chain together promises to access history of document + var promise = substorage.get(result.lastseen); + while (revision.steps > 1) { + promise = promise.push(recurse_get); + revision.steps -= 1; + } + + // Once at desired depth, update storage._lastseen and return doc + return promise.push(function (result) { + storage._lastseen = result.timestamp; + if (result.op === "remove") { + throw new jIO.util.jIOError( + "bryanstorage: cannot find object '" + + result.doc_id + + "' (removed)", + 404 + ); + } + return result.doc; + }); + } } }); }; @@ -81,8 +146,11 @@ timestamp: timestamp, doc_id: id, doc: data, - op: "put" + op: "put", + lastseen: this._lastseen }; + this._lastseen = timestamp; + //console.log(metadata.doc.k, timestamp, metadata.lastseen); return this._sub_storage.put(timestamp, metadata); }; @@ -92,8 +160,10 @@ // XXX: remove this attribute once query can sort_on id timestamp: timestamp, doc_id: id, - op: "remove" + op: "remove", + lastseen: this._lastseen }; + this._lastseen = timestamp; return this._sub_storage.put(timestamp, metadata); }; diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index 0ed86fd..5efe852 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -83,7 +83,8 @@ title: "foo0" }, timestamp: results.timestamp, - op: "put" + op: "put", + lastseen: undefined }, "The first item in the log is pushing bar's title to 'foo0'"); return jio.remove("bar"); }) @@ -117,7 +118,8 @@ deepEqual(result, { doc_id: "bar", timestamp: result.timestamp, - op: "remove" + op: "remove", + lastseen: result.lastseen }); }) .fail(function (error) { @@ -325,19 +327,19 @@ deepEqual(result, {"k4": "v4"}, "By default, .get returns latest revision"); - return jio.get("doc", 0); + return jio.get("doc", {steps: 0}); }) .push(function (result) { deepEqual(result, {"k4": "v4"}, ".get returns latest revision with second input = 0"); - return jio.get("doc", 1); + return jio.get("doc", {steps: 1}); }) .push(function (result) { deepEqual(result, {"k3": "v3"}, "Walk back one revision with second input = 1"); - return jio.get("doc", 2); + return jio.get("doc", {steps: 2}); }) .push(function () { ok(false, "This query should have thrown a 404 error"); @@ -346,25 +348,25 @@ deepEqual(error.status_code, 404, "Current state of document is 'removed'."); - return jio.get("doc", 3); + return jio.get("doc", {steps: 3}); }) .push(function (result) { deepEqual(result, {"k2": "v2"}, "Walk back three revisions with second input = 3"); - return jio.get("doc", 4); + return jio.get("doc", {steps: 4}); }) .push(function (result) { deepEqual(result, {"k1": "v1"}, "Walk back four revisions with second input = 4"); - return jio.get("doc", 5); + return jio.get("doc", {steps: 5}); }) .push(function (result) { deepEqual(result, {"k0": "v0"}, "Walk back five revisions with second input = 5"); - return jio.get("doc", 6); + return jio.get("doc", {steps: 6}); }) .push(function () { ok(false, "This query should have thrown a 404 error"); @@ -380,4 +382,504 @@ }) .always(function () {start(); }); }); + + ///////////////////////////////////////////////////////////////// + // Accessing older revisions with multiple users + ///////////////////////////////////////////////////////////////// + + module("bryanStorage.accessing_older_revisions_multiple_users"); + test("Testing retrieval of older revisions of documents with multiple users", + function () { + stop(); + expect(34); + + // create storage of type "bryan" with memory as substorage + var dbname = "multi_user_db" + Date.now(), + jio1 = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + }), + jio2 = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + }), + jio3 = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + }); + + jio1.put("doc", { + "k": "v0.1" + }) + .push(function () { + return jio2.get("doc"); + }) + .push(function () { + return jio3.get("doc"); + }) + .push(function () { + return jio2.put("doc", { + "k": "v0.1.2" + }); + }) + .push(function () { + return jio3.put("doc", { + "k": "v0.1.3" + }); + }) + /** + .push(function () { + return jio2.put("doc", { + "k": "v0.1.2.2" + }); + }) + **/ + .push(function () { + return jio2.remove("doc"); + }) + .push(function () { + return jio3.put("doc", { + "k": "v0.1.3.3" + }); + }) + .push(function () { + return jio1.get("doc"); + }) + .push(function () { + return jio1.put("doc", { + "k": "v0.1.3.3.1" + }); + }) + .push(function () { + return jio2.put("doc", { + "k": "v0.1.2.2.2" + }); + }) + .push(function () { + return jio3.put("doc", { + "k": "v0.1.3.3.3" + }); + }) + .push(function () { + return jio1.get("doc"); + }) + // jio2 has a different version than 1 & 3 as its latest revision + /** + .push(function () { + return jio2.get("doc"); + }) + **/ + .push(function () { + return jio3.get("doc"); + }) + + // Test all lastseens are the same + .push(function () { + equal(jio1._lastseen, jio2._lastseen, "All users see same revision"); + equal(jio1._lastseen, jio3._lastseen, "All users see same revision"); + + // + // Test consistent history of user 1 + // + return jio1.get("doc", { + path: "consistent", + steps: 0 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.3" + }, "Get of depth 0 returns latest version" + ); + + return jio1.get("doc", { + path: "consistent", + steps: 1 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3" + }, "Get of consistent depth 1 returns correct version" + ); + + return jio1.get("doc", { + path: "consistent", + steps: 2 + }); + }) + + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3" + }, "Get of consistent depth 2 returns correct version" + ); + + return jio1.get("doc", { + path: "consistent", + steps: 3 + }); + }) + + .push(function (result) { + deepEqual(result, { + "k": "v0.1" + }, "Get of consistent depth 3 returns correct version" + ); + + return jio1.get("doc", { + path: "consistent", + steps: 4 + }); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "There are only 3 previous states of this document: " + error); + }) + .push(function () { + + // + // Test consistent history of user 2 (Is the same as 1 & 3 even though + // User 2 has not explicitly called .get since the latest changes + // were made) + // + return jio2.get("doc", { + path: "consistent", + steps: 0 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.3" + }, "Get of depth 0 returns latest version" + ); + + return jio2.get("doc", { + path: "consistent", + steps: 1 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3" + }, "Get of depth 0 returns latest version" + ); + + return jio2.get("doc", { + path: "consistent", + steps: 2 + }); + }) + + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3" + }, "Get of consistent depth 2 returns correct version" + ); + + return jio2.get("doc", { + path: "consistent", + steps: 3 + }); + }) + + .push(function (result) { + deepEqual(result, { + "k": "v0.1" + }, "Get of consistent depth 3 returns correct version" + ); + + return jio2.get("doc", { + path: "consistent", + steps: 4 + }); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "There are only 3 previous states of this document: " + error); + }) + .push(function () { + + // + // Test consistent history of user 3 (Should be same as user 1) + // + return jio3.get("doc", { + path: "consistent", + steps: 0 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.3" + }, "User 2 consistent history is same as user 1" + ); + return jio3.get("doc", { + path: "consistent", + steps: 1 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3" + }, "User 2 consistent history is same as user 1" + ); + return jio3.get("doc", { + path: "consistent", + steps: 2 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3" + }, "User 2 consistent history is same as user 1" + ); + return jio3.get("doc", { + path: "consistent", + steps: 3 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1" + }, "User 2 consistent history is same as user 1" + ); + return jio3.get("doc", { + path: "consistent", + steps: 4 + }); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "There are only 3 previous states of this document"); + }) + + // + // Test absolute history of user 1 + // + .push(function () { + return jio1.get("doc", { + path: "absolute", + steps: 0 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.3" + }, "Get of absolute depth 0 returns latest version" + ); + return jio1.get("doc", { + path: "absolute", + steps: 1 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.2.2.2" + }, "Get of absolute depth 1 returns correct version" + ); + return jio1.get("doc", { + path: "absolute", + steps: 2 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.1" + }, "Get of absolute depth 2 returns correct version" + ); + return jio1.get("doc", { + path: "absolute", + steps: 3 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3" + }, "Get of absolute depth 3 returns correct version" + ); + return jio1.get("doc", { + path: "absolute", + steps: 4 + }); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "Document has been removed at this point"); + return jio1.get("doc", { + path: "absolute", + steps: 5 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3" + }, "Get of absolute depth 5 returns correct version" + ); + return jio1.get("doc", { + path: "absolute", + steps: 6 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.2" + }, "Get of absolute depth 6 returns correct version" + ); + return jio1.get("doc", { + path: "absolute", + steps: 7 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1" + }); + return jio1.get("doc", { + path: "absolute", + steps: 8 + }); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "There are only 3 previous states of this document"); + }) + + // + // Test absolute history of user 2 + // + .push(function () { + return jio2.get("doc", { + path: "absolute", + steps: 0 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.3" + }); + return jio2.get("doc", { + path: "absolute", + steps: 1 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.2.2.2" + }); + return jio2.get("doc", { + path: "absolute", + steps: 2 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.1" + }); + return jio2.get("doc", { + path: "absolute", + steps: 3 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3" + }); + return jio2.get("doc", { + path: "absolute", + steps: 4 + }); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "Document has been removed at this point"); + return jio2.get("doc", { + path: "absolute", + steps: 5 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3" + }); + return jio2.get("doc", { + path: "absolute", + steps: 6 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.2" + }); + return jio2.get("doc", { + path: "absolute", + steps: 7 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1" + }); + return jio2.get("doc", { + path: "absolute", + steps: 8 + }); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "There are only 3 previous states of this document"); + }) + + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + }(jIO, QUnit)); \ No newline at end of file -- 2.30.9 From edae97f3578ea30f06314651769fca9b831a64c2 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Thu, 7 Jun 2018 15:56:54 +0000 Subject: [PATCH 18/46] Small error was preventing the last set of tests from passing in the most recent commit. --- test/jio.storage/bryanstorage.tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index 5efe852..0d16e06 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -391,7 +391,7 @@ test("Testing retrieval of older revisions of documents with multiple users", function () { stop(); - expect(34); + expect(35); // create storage of type "bryan" with memory as substorage var dbname = "multi_user_db" + Date.now(), -- 2.30.9 From 038c7a51a24465300d38d1990a69a39a3f451be1 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Fri, 8 Jun 2018 14:29:14 +0000 Subject: [PATCH 19/46] Implemented absolute, consistent, and leaves style getting of old revisions of documents and one large test case with three users to validate these methods. --- src/jio.storage/bryanstorage.js | 82 +++++++- test/jio.storage/bryanstorage.tests.js | 253 ++++++++++++++++++++++++- 2 files changed, 328 insertions(+), 7 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index d5bc35c..2c7b8a6 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -27,6 +27,7 @@ sub_storage: spec.sub_storage }); this._lastseen = undefined; + this.param = true; } BryanStorage.prototype.get = function (id_in, revision) { @@ -47,6 +48,14 @@ revision.path = "absolute"; } + if (revision.steps === undefined) { + revision.steps = 0; + } + + if (revision.steps === 0) { + revision.path = "absolute"; + } + // Query to get the last edit made to this document var storage = this, substorage = this._sub_storage, @@ -55,6 +64,72 @@ sort_on: [["timestamp", "descending"]] }; + if (revision.path === "leaves") { + return substorage.allDocs({ + query: "leaf: true", + sort_on: [["timestamp", "descending"]] + }) + .push(function (results) { + var promises = results.data.rows.map(function (res) { + return substorage.get(res.id); + }); + return RSVP.all(promises); + }) + .push(function (documents) { + var not_leaves = {}, + leaves = [], + ind, + doc_data, + fix_leaves = []; + function update_leaf_state(data) { + var new_data = data; + new_data.leaf = false; + return substorage.put(data.timestamp, new_data); + } + + // Loop through documents and push only leaf versions to leaves array + for (ind = 0; ind < documents.length; ind += 1) { + doc_data = documents[ind]; + not_leaves[doc_data.lastseen] = true; + if (not_leaves[doc_data.timestamp] === true) { + fix_leaves.push(update_leaf_state); + } else { + if (doc_data.op === "put") { + + // XXX: For cheaper evaluation, break out of the loop once + // leaves.length == revision.steps, and only fix the leaves in + // fix_leaves at that point. + // However, since we already spent time to retrieve all leaves, + // it may be better to go ahead and clean up all mislabelled + // leaves right now, so the next call to get.leaves is cheaper + leaves.push(doc_data.doc); + + // revision.steps is guaranteed to be >= 1 in this branch + // + if (leaves.length - 1 === revision.steps) { + storage._lastseen = doc_data.timestamp; + } + } + } + } + + // Fix all mislabelled leaves and then return the array of leaves + return RSVP.all(fix_leaves) + + //XXX: Not sure why I can't use a .push here instead of .then + .then(function () { + if (leaves.length - 1 >= revision.steps) { + return leaves[revision.steps]; + } + throw new jIO.util.jIOError( + "bryanstorage: there are fewer than " + + revision.steps + " leaf revisions for '" + id_in + "'", + 404 + ); + }); + }); + } + // In "absolute" path, .get returns the revision.steps-most-recent revision if (revision.path === "absolute") { options.limit = [revision.steps, 1]; @@ -147,7 +222,8 @@ doc_id: id, doc: data, op: "put", - lastseen: this._lastseen + lastseen: this._lastseen, + leaf: true }; this._lastseen = timestamp; //console.log(metadata.doc.k, timestamp, metadata.lastseen); @@ -161,9 +237,11 @@ timestamp: timestamp, doc_id: id, op: "remove", - lastseen: this._lastseen + lastseen: this._lastseen, + leaf: true }; this._lastseen = timestamp; + //console.log("removed", timestamp, metadata.lastseen); return this._sub_storage.put(timestamp, metadata); }; diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index 0d16e06..1bf15f5 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -84,7 +84,8 @@ }, timestamp: results.timestamp, op: "put", - lastseen: undefined + lastseen: undefined, + leaf: true }, "The first item in the log is pushing bar's title to 'foo0'"); return jio.remove("bar"); }) @@ -119,7 +120,8 @@ doc_id: "bar", timestamp: result.timestamp, op: "remove", - lastseen: result.lastseen + lastseen: result.lastseen, + leaf: true }); }) .fail(function (error) { @@ -391,7 +393,7 @@ test("Testing retrieval of older revisions of documents with multiple users", function () { stop(); - expect(35); + expect(51); // create storage of type "bryan" with memory as substorage var dbname = "multi_user_db" + Date.now(), @@ -493,8 +495,10 @@ // Test all lastseens are the same .push(function () { - equal(jio1._lastseen, jio2._lastseen, "All users see same revision"); - equal(jio1._lastseen, jio3._lastseen, "All users see same revision"); + // These are all undefined outside the storage definition, so these + // tests are meaningless + //equal(jio1._lastseen, jio2._lastseen, "All users see same version"); + //equal(jio1._lastseen, jio3._lastseen, "All users see same version"); // // Test consistent history of user 1 @@ -682,6 +686,10 @@ 404, "There are only 3 previous states of this document"); }) + // Reset jio3._lastseen to be at v0.1.3.3.3 + .push(function () { + return jio3.get("doc"); + }) // // Test absolute history of user 1 @@ -875,6 +883,241 @@ "There are only 3 previous states of this document"); }) + // + // Tests on checking out an older revision and making a new edit branch + // + .push(function () { + return jio1.get("doc", { + path: "absolute", + steps: 1 + }); + }) + .push(function () { + return jio1.put("doc", { + "k": "v0.1.2.2.2.1" + }); + }) + .push(function () { + return jio1.get("doc", { + path: "consistent", + steps: 1 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.2.2.2" + }, "The new document is added to the correct edit branch" + ); + return jio1.get("doc", { + path: "consistent", + steps: 2 + }); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "This document was removed at this time"); + return jio1.get("doc", { + path: "consistent", + steps: 3 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.2" + }, "The new document is added to the correct edit branch" + ); + return jio1.get("doc", { + path: "consistent", + steps: 4 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1" + }, "This edit branch also leads back to the original version" + ); + return jio1.get("doc", { + path: "consistent", + steps: 5 + }); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "There are no revisions before the original document"); + }) + .push(function () { + return jio3.put("doc", { + "k": "v0.1.3.3.3.3" + }); + }) + + // All three users have the same latest revision + .push(function () { + return jio1.get("doc"); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.3.3" + }, "User one accesses latest revision correctly" + ); + return jio2.get("doc"); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.3.3" + }, "User two accesses latest revision correctly" + ); + return jio3.get("doc"); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.3.3" + }, "User three accesses latest revision correctly" + ); + return jio2.get("doc", { + path: "consistent", + steps: 1 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.3" + }, "User 2 accesses the 1st edit in consistent traversal." + ); + }) + + // + // Testing .getting on leaf nodes + // + .push(function () { + return jio1.get("doc", { + path: "leaves" + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.3.3" + }, "First result is the most-recently-added leaf" + ); + return jio2.get("doc", { + path: "leaves", + steps: 1 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.2.2.2.1" + }, "Second result is the 2nd most-recently-added leaf" + ); + return jio3.get("doc", { + path: "leaves", + steps: 2, + db: "jio3" + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.1" + }, "Third result is the 3rd most-recently-added leaf" + ); + + // + // Editing document revisions stemming from the latest leaf nodes seen + // + return jio1.put("doc", { + "k": "v0.1.3.3.3.3.1" + }); + }) + .push(function () { + return jio3.remove("doc"); // removing v0.1.3.3.1 + }) + + // Check that jio1 sees latest non-removed revision + .push(function () { + return jio1.get("doc"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "The most recent edit was a remove, so throw error"); + }) + .push(function () { + // jio2 lastseen should point to "v0.1.2.2.2.1" + return jio2.put("doc", { + "k": "v0.1.2.2.2.1.2" + }); + }) + .push(function () { + return jio1.get("doc", { + path: "leaves", + steps: 0 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.2.2.2.1.2" + }, "Accessing the first leaf node at this time" + ); + return jio1.get("doc", { + path: "leaves", + steps: 1 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.3.3.1" + }, "Accessing the second leaf node at this time" + ); + return jio1.get("doc", { + path: "leaves", + steps: 2 + }); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "There are only two non-removed leaves"); + + // jio1 should still have lastseen at v0.1.3.3.3.3.1 + return jio1.put("doc", { + "k": "v0.1.3.3.3.3.1.1" + }); + }) + .push(function () { + return jio1.get("doc", { + path: "consistent", + steps: 1 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.3.3.1" + }, "If a .get fails, that should not reset ._lastseen parameter" + ); + return jio1.get("doc", { + path: "consistent", + steps: 2 + }); + }) + .push(function (result) { + deepEqual(result, { + "k": "v0.1.3.3.3.3" + }, "History of 0.1.2.2.2 has been constructed correctly."); + }) .fail(function (error) { //console.log(error); ok(false, error); -- 2.30.9 From 7d25b5ce033380253e2d99fe0540ccd943bb6e40 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Mon, 11 Jun 2018 10:13:24 +0000 Subject: [PATCH 20/46] allDocs method is updated to allow an options.revision_limit parameter which allows queries on older revisions of documents. --- src/jio.storage/bryanstorage.js | 221 ++----- test/jio.storage/bryanstorage.tests.js | 867 ++++++------------------- 2 files changed, 256 insertions(+), 832 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index 2c7b8a6..3ec912f 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -26,119 +26,22 @@ type: "query", sub_storage: spec.sub_storage }); - this._lastseen = undefined; - this.param = true; } - BryanStorage.prototype.get = function (id_in, revision) { + BryanStorage.prototype.get = function (id_in, steps) { - // Default behavior, get() returns the most recent revision - if (revision === undefined) { - revision = { - steps: 0, - path: "absolute" - }; - } - - // Default type of traversal is absolute: - // "absolute" -- step backward in chronological order of changes to document - // "consistent" -- step backward in chronological order of only edits the - // most recent version is based on. Other branches of edits are ignored - if (revision.path === undefined) { - revision.path = "absolute"; - } - - if (revision.steps === undefined) { - revision.steps = 0; - } - - if (revision.steps === 0) { - revision.path = "absolute"; + if (steps === undefined) { + steps = 0; } // Query to get the last edit made to this document - var storage = this, - substorage = this._sub_storage, + var substorage = this._sub_storage, options = { query: "doc_id: " + id_in, - sort_on: [["timestamp", "descending"]] + sort_on: [["timestamp", "descending"]], + limit: [steps, 1] }; - if (revision.path === "leaves") { - return substorage.allDocs({ - query: "leaf: true", - sort_on: [["timestamp", "descending"]] - }) - .push(function (results) { - var promises = results.data.rows.map(function (res) { - return substorage.get(res.id); - }); - return RSVP.all(promises); - }) - .push(function (documents) { - var not_leaves = {}, - leaves = [], - ind, - doc_data, - fix_leaves = []; - function update_leaf_state(data) { - var new_data = data; - new_data.leaf = false; - return substorage.put(data.timestamp, new_data); - } - - // Loop through documents and push only leaf versions to leaves array - for (ind = 0; ind < documents.length; ind += 1) { - doc_data = documents[ind]; - not_leaves[doc_data.lastseen] = true; - if (not_leaves[doc_data.timestamp] === true) { - fix_leaves.push(update_leaf_state); - } else { - if (doc_data.op === "put") { - - // XXX: For cheaper evaluation, break out of the loop once - // leaves.length == revision.steps, and only fix the leaves in - // fix_leaves at that point. - // However, since we already spent time to retrieve all leaves, - // it may be better to go ahead and clean up all mislabelled - // leaves right now, so the next call to get.leaves is cheaper - leaves.push(doc_data.doc); - - // revision.steps is guaranteed to be >= 1 in this branch - // - if (leaves.length - 1 === revision.steps) { - storage._lastseen = doc_data.timestamp; - } - } - } - } - - // Fix all mislabelled leaves and then return the array of leaves - return RSVP.all(fix_leaves) - - //XXX: Not sure why I can't use a .push here instead of .then - .then(function () { - if (leaves.length - 1 >= revision.steps) { - return leaves[revision.steps]; - } - throw new jIO.util.jIOError( - "bryanstorage: there are fewer than " + - revision.steps + " leaf revisions for '" + id_in + "'", - 404 - ); - }); - }); - } - - // In "absolute" path, .get returns the revision.steps-most-recent revision - if (revision.path === "absolute") { - options.limit = [revision.steps, 1]; - - // In "consistent path, .get returns the most recent revision and looks - // deeper into history with the result's .lastseen attribute - } else if (revision.path === "consistent") { - options.limit = [0, 1]; - } return substorage.allDocs(options) .push(function (results) { if (results.data.rows.length > 0) { @@ -151,62 +54,13 @@ }) .push(function (result) { - - // Function used to chain together substorage.get's for "consistent" - // traversal - function recurse_get(result) { - if (result.lastseen === undefined) { - throw new jIO.util.jIOError( - "bryanstorage: cannot find object '" + - id_in + - "' (end of history)", - 404 - ); - } - return substorage.get(result.lastseen); - } - - // If last edit was a remove, throw a 'not found' error - if (result.op === "remove" && revision.path === "absolute") { - throw new jIO.util.jIOError( - "bryanstorage: cannot find object '" + id_in + "' (removed)", - 404 - ); - } - if (result.op === "put") { - - // The query for "absolute" traversal returns exactly the document - // requested - if (revision.path === "absolute" || revision.steps === 0) { - storage._lastseen = result.timestamp; - return result.doc; - } - if (revision.path === "consistent") { - - - // Chain together promises to access history of document - var promise = substorage.get(result.lastseen); - while (revision.steps > 1) { - promise = promise.push(recurse_get); - revision.steps -= 1; - } - - // Once at desired depth, update storage._lastseen and return doc - return promise.push(function (result) { - storage._lastseen = result.timestamp; - if (result.op === "remove") { - throw new jIO.util.jIOError( - "bryanstorage: cannot find object '" + - result.doc_id + - "' (removed)", - 404 - ); - } - return result.doc; - }); - } + return result.doc; } + throw new jIO.util.jIOError( + "bryanstorage: cannot find object '" + id_in + "' (removed)", + 404 + ); }); }; @@ -221,12 +75,9 @@ timestamp: timestamp, doc_id: id, doc: data, - op: "put", - lastseen: this._lastseen, - leaf: true + op: "put" }; this._lastseen = timestamp; - //console.log(metadata.doc.k, timestamp, metadata.lastseen); return this._sub_storage.put(timestamp, metadata); }; @@ -236,12 +87,8 @@ // XXX: remove this attribute once query can sort_on id timestamp: timestamp, doc_id: id, - op: "remove", - lastseen: this._lastseen, - leaf: true + op: "remove" }; - this._lastseen = timestamp; - //console.log("removed", timestamp, metadata.lastseen); return this._sub_storage.put(timestamp, metadata); }; @@ -297,7 +144,6 @@ }); }; - // Not implemented for IndexedDB BryanStorage.prototype.repair = function () { return this._sub_storage.repair.apply(this._sub_storage, arguments); }; @@ -306,13 +152,22 @@ }; BryanStorage.prototype.buildQuery = function (options) { + if (options === undefined) { + options = {}; + } if (options.sort_on === undefined) { options.sort_on = []; } + options.sort_on.push(["timestamp", "descending"]); if (options.limit === undefined) { options.limit = [0, -1]; } - options.sort_on.push(["timestamp", "descending"]); + + // Default behavior is to return only the latest revision of each document + if (options.revision_limit === undefined) { + options.revision_limit = [0, 1]; + } + var meta_options = { // XXX: I don't believe it's currently possible to query on sub-attributes // so for now, we just use the inputted query, which obviously will fail @@ -327,36 +182,52 @@ first_doc = options.limit[0]; return this._sub_storage.allDocs(meta_options) + + // Get all documents found in query .push(function (results) { var promises = results.data.rows.map(function (data) { return substorage.get(data.id); }); return RSVP.all(promises); }) + .push(function (results_array) { var clean_data = [], ind, - seen_docs = [], + seen_docs = {}, current_doc, counter = 0; + + // Default behavior is to not limit the number of documents returned if (max_num_docs === -1) { max_num_docs = results_array.length; } for (ind = 0; ind < results_array.length; ind += 1) { current_doc = results_array[ind]; + // Initialize count of revisions + if (!seen_docs.hasOwnProperty(current_doc.doc_id)) { + seen_docs[current_doc.doc_id] = 0; + } + // If the latest version of this document has not yet been // included in query result - if (seen_docs[current_doc.doc_id] !== true) { + if (options.revision_limit[0] <= seen_docs[current_doc.doc_id] && + seen_docs[current_doc.doc_id] < options.revision_limit[0] + + options.revision_limit[1]) { - // If the latest edit was a put operation, add it to query + // If the latest edit was a put operation, add it to query // results if (current_doc.op === "put") { if (counter >= first_doc) { + + // Note the rev attribute added to the output data. + // This guarantees that `this.get(id, rev) === doc` clean_data.push({ - doc: {}, + doc: current_doc.doc, value: {}, - id: current_doc.doc_id + id: current_doc.doc_id, + rev: seen_docs[current_doc.doc_id] }); if (clean_data.length === max_num_docs) { return clean_data; @@ -364,9 +235,9 @@ } counter += 1; } - // Mark document as read so no older edits are considered - seen_docs[current_doc.doc_id] = true; } + // Keep track of how many times this doc_id has been seen + seen_docs[current_doc.doc_id] += 1; } // In passing results back to allDocs, formatting of query is handled return clean_data; diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index 1bf15f5..ccbb412 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -83,9 +83,7 @@ title: "foo0" }, timestamp: results.timestamp, - op: "put", - lastseen: undefined, - leaf: true + op: "put" }, "The first item in the log is pushing bar's title to 'foo0'"); return jio.remove("bar"); }) @@ -119,9 +117,7 @@ deepEqual(result, { doc_id: "bar", timestamp: result.timestamp, - op: "remove", - lastseen: result.lastseen, - leaf: true + op: "remove" }); }) .fail(function (error) { @@ -291,7 +287,7 @@ test("Testing proper retrieval of older revisions of documents", function () { stop(); - expect(8); + expect(12); // create storage of type "bryan" with memory as substorage var jio = jIO.createJIO({ @@ -303,13 +299,47 @@ } } }); - - jio.put("doc", { - "k0": "v0" - }) + jio.get("doc") + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "Document does not exist yet."); + }) + .push(function () { + return jio.put("doc", { + "k0": "v0" + }); + }) + .push(function () { + return jio.get("doc", 0); + }) + .push(function (result) { + deepEqual(result, { + "k0": "v0" + }); + }) .push(function () { return jio.put("doc", {"k1": "v1"}); }) + .push(function () { + return jio.get("doc", 0); + }) + .push(function (result) { + deepEqual(result, { + "k1": "v1" + }); + }) + .push(function () { + return jio.get("doc", 1); + }) + .push(function (result) { + deepEqual(result, { + "k0": "v0" + }); + }) .push(function () { return jio.put("doc", {"k2": "v2"}); }) @@ -329,19 +359,19 @@ deepEqual(result, {"k4": "v4"}, "By default, .get returns latest revision"); - return jio.get("doc", {steps: 0}); + return jio.get("doc", 0); }) .push(function (result) { deepEqual(result, {"k4": "v4"}, ".get returns latest revision with second input = 0"); - return jio.get("doc", {steps: 1}); + return jio.get("doc", 1); }) .push(function (result) { deepEqual(result, {"k3": "v3"}, "Walk back one revision with second input = 1"); - return jio.get("doc", {steps: 2}); + return jio.get("doc", 2); }) .push(function () { ok(false, "This query should have thrown a 404 error"); @@ -350,25 +380,25 @@ deepEqual(error.status_code, 404, "Current state of document is 'removed'."); - return jio.get("doc", {steps: 3}); + return jio.get("doc", 3); }) .push(function (result) { deepEqual(result, {"k2": "v2"}, "Walk back three revisions with second input = 3"); - return jio.get("doc", {steps: 4}); + return jio.get("doc", 4); }) .push(function (result) { deepEqual(result, {"k1": "v1"}, "Walk back four revisions with second input = 4"); - return jio.get("doc", {steps: 5}); + return jio.get("doc", 5); }) .push(function (result) { deepEqual(result, {"k0": "v0"}, "Walk back five revisions with second input = 5"); - return jio.get("doc", {steps: 6}); + return jio.get("doc", 6); }) .push(function () { ok(false, "This query should have thrown a 404 error"); @@ -386,38 +416,18 @@ }); ///////////////////////////////////////////////////////////////// - // Accessing older revisions with multiple users + // Accessing older revisions with two users ///////////////////////////////////////////////////////////////// - module("bryanStorage.accessing_older_revisions_multiple_users"); - test("Testing retrieval of older revisions of documents with multiple users", + module("bryanStorage.querying_old_revisions"); + test("Testing retrieval of older revisions via allDocs calls", function () { stop(); - expect(51); + expect(47); // create storage of type "bryan" with memory as substorage - var dbname = "multi_user_db" + Date.now(), - jio1 = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - }), - jio2 = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - }), - jio3 = jIO.createJIO({ + var dbname = "db-" + Date.now(), + jio = jIO.createJIO({ type: "bryan", sub_storage: { type: "uuid", @@ -427,696 +437,239 @@ } } }); - - jio1.put("doc", { - "k": "v0.1" + jio.put("doc", { + "k": "v0" }) .push(function () { - return jio2.get("doc"); - }) - .push(function () { - return jio3.get("doc"); - }) - .push(function () { - return jio2.put("doc", { - "k": "v0.1.2" + return jio.put("doc", { + "k": "v1" }); }) .push(function () { - return jio3.put("doc", { - "k": "v0.1.3" - }); - }) - /** - .push(function () { - return jio2.put("doc", { - "k": "v0.1.2.2" - }); - }) - **/ - .push(function () { - return jio2.remove("doc"); - }) - .push(function () { - return jio3.put("doc", { - "k": "v0.1.3.3" - }); - }) - .push(function () { - return jio1.get("doc"); - }) - .push(function () { - return jio1.put("doc", { - "k": "v0.1.3.3.1" + return jio.put("doc", { + "k": "v2" }); }) .push(function () { - return jio2.put("doc", { - "k": "v0.1.2.2.2" + return jio.put("doc", { + "k": "v3" }); }) .push(function () { - return jio3.put("doc", { - "k": "v0.1.3.3.3" - }); - }) - .push(function () { - return jio1.get("doc"); - }) - // jio2 has a different version than 1 & 3 as its latest revision - /** - .push(function () { - return jio2.get("doc"); - }) - **/ - .push(function () { - return jio3.get("doc"); - }) - - // Test all lastseens are the same - .push(function () { - // These are all undefined outside the storage definition, so these - // tests are meaningless - //equal(jio1._lastseen, jio2._lastseen, "All users see same version"); - //equal(jio1._lastseen, jio3._lastseen, "All users see same version"); - - // - // Test consistent history of user 1 - // - return jio1.get("doc", { - path: "consistent", - steps: 0 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.3" - }, "Get of depth 0 returns latest version" - ); - - return jio1.get("doc", { - path: "consistent", - steps: 1 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3" - }, "Get of consistent depth 1 returns correct version" - ); - - return jio1.get("doc", { - path: "consistent", - steps: 2 + return jio.allDocs({ + query: "", + revision_limit: [0, 1] }); }) - - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3" - }, "Get of consistent depth 2 returns correct version" - ); - - return jio1.get("doc", { - path: "consistent", - steps: 3 - }); + .push(function (results) { + deepEqual(results.data.rows.length, + 1, + "Only one query returned with options.revision_limit == [0,1]"); + return jio.get(results.data.rows[0].id); }) - .push(function (result) { deepEqual(result, { - "k": "v0.1" - }, "Get of consistent depth 3 returns correct version" - ); - - return jio1.get("doc", { - path: "consistent", - steps: 4 + "k": "v3" }); }) .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "There are only 3 previous states of this document: " + error); - }) - .push(function () { - - // - // Test consistent history of user 2 (Is the same as 1 & 3 even though - // User 2 has not explicitly called .get since the latest changes - // were made) - // - return jio2.get("doc", { - path: "consistent", - steps: 0 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.3" - }, "Get of depth 0 returns latest version" - ); - - return jio2.get("doc", { - path: "consistent", - steps: 1 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3" - }, "Get of depth 0 returns latest version" - ); - - return jio2.get("doc", { - path: "consistent", - steps: 2 - }); - }) - - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3" - }, "Get of consistent depth 2 returns correct version" - ); - - return jio2.get("doc", { - path: "consistent", - steps: 3 + return jio.allDocs({ + query: "", + revision_limit: [1, 1] }); }) - - .push(function (result) { - deepEqual(result, { - "k": "v0.1" - }, "Get of consistent depth 3 returns correct version" - ); - - return jio2.get("doc", { - path: "consistent", - steps: 4 + .push(function (results) { + deepEqual(results.data.rows.length, + 1, + "Only one query returned with options.revision_limit == [1,1]"); + deepEqual(results.data.rows[0].doc, { + "k": "v2" }); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "There are only 3 previous states of this document: " + error); - }) - .push(function () { - - // - // Test consistent history of user 3 (Should be same as user 1) - // - return jio3.get("doc", { - path: "consistent", - steps: 0 + return jio.allDocs({ + query: "", + revision_limit: [2, 1] }); }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.3" - }, "User 2 consistent history is same as user 1" - ); - return jio3.get("doc", { - path: "consistent", - steps: 1 + .push(function (results) { + deepEqual(results.data.rows.length, + 1, + "Only one query returned with options.revision_limit == [2,1]"); + deepEqual(results.data.rows[0].doc, { + "k": "v1" }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3" - }, "User 2 consistent history is same as user 1" - ); - return jio3.get("doc", { - path: "consistent", - steps: 2 + return jio.allDocs({ + query: "", + revision_limit: [3, 1] }); }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3" - }, "User 2 consistent history is same as user 1" - ); - return jio3.get("doc", { - path: "consistent", - steps: 3 + .push(function (results) { + deepEqual(results.data.rows.length, + 1, + "Only one query returned with options.revision_limit == [3,1]"); + deepEqual(results.data.rows[0].doc, { + "k": "v0" }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1" - }, "User 2 consistent history is same as user 1" - ); - return jio3.get("doc", { - path: "consistent", - steps: 4 + return jio.allDocs({ + query: "", + revision_limit: [4, 1] }); }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "There are only 3 previous states of this document"); - }) - // Reset jio3._lastseen to be at v0.1.3.3.3 - .push(function () { - return jio3.get("doc"); + .push(function (results) { + equal(results.data.rows.length, 0, "Past all previous revisions"); }) - - // - // Test absolute history of user 1 - // .push(function () { - return jio1.get("doc", { - path: "absolute", - steps: 0 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.3" - }, "Get of absolute depth 0 returns latest version" - ); - return jio1.get("doc", { - path: "absolute", - steps: 1 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.2.2.2" - }, "Get of absolute depth 1 returns correct version" - ); - return jio1.get("doc", { - path: "absolute", - steps: 2 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.1" - }, "Get of absolute depth 2 returns correct version" - ); - return jio1.get("doc", { - path: "absolute", - steps: 3 + return jio.allDocs({ + revision_limit: [0, 2] }); }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3" - }, "Get of absolute depth 3 returns correct version" - ); - return jio1.get("doc", { - path: "absolute", - steps: 4 + .push(function (results) { + equal(results.data.rows.length, 2); + deepEqual(results.data.rows[0].doc, { + "k": "v3" + }, "Only retrieve two most recent revions"); + deepEqual(results.data.rows[1].doc, { + "k": "v2" }); }) .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "Document has been removed at this point"); - return jio1.get("doc", { - path: "absolute", - steps: 5 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3" - }, "Get of absolute depth 5 returns correct version" - ); - return jio1.get("doc", { - path: "absolute", - steps: 6 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.2" - }, "Get of absolute depth 6 returns correct version" - ); - return jio1.get("doc", { - path: "absolute", - steps: 7 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1" - }); - return jio1.get("doc", { - path: "absolute", - steps: 8 - }); + return jio.remove("doc"); }) .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "There are only 3 previous states of this document"); - }) - - // - // Test absolute history of user 2 - // - .push(function () { - return jio2.get("doc", { - path: "absolute", - steps: 0 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.3" - }); - return jio2.get("doc", { - path: "absolute", - steps: 1 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.2.2.2" - }); - return jio2.get("doc", { - path: "absolute", - steps: 2 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.1" - }); - return jio2.get("doc", { - path: "absolute", - steps: 3 + return jio.allDocs({ + query: "", + revision_limit: [0, 1] }); }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3" - }); - return jio2.get("doc", { - path: "absolute", - steps: 4 - }); + .push(function (results) { + equal(results.data.rows.length, 0, + "Query does not return removed doc"); }) .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "Document has been removed at this point"); - return jio2.get("doc", { - path: "absolute", - steps: 5 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3" - }); - return jio2.get("doc", { - path: "absolute", - steps: 6 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.2" - }); - return jio2.get("doc", { - path: "absolute", - steps: 7 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1" - }); - return jio2.get("doc", { - path: "absolute", - steps: 8 + return jio.allDocs({ + query: "", + revision_limit: [1, 3] }); }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "There are only 3 previous states of this document"); - }) - - // - // Tests on checking out an older revision and making a new edit branch - // - .push(function () { - return jio1.get("doc", { - path: "absolute", - steps: 1 - }); + .push(function (results) { + equal(results.data.rows.length, 3); + deepEqual(results.data.rows[0].doc, { + "k": "v3" + }, "1st, 2nd, and 3rd versions removed from current are retrieved"); + deepEqual(results.data.rows[1].doc, { + "k": "v2" + }, "1st, 2nd, and 3rd versions removed from current are retrieved"); + deepEqual(results.data.rows[2].doc, { + "k": "v1" + }, "1st, 2nd, and 3rd versions removed from current are retrieved"); }) .push(function () { - return jio1.put("doc", { - "k": "v0.1.2.2.2.1" + return jio.put("doc2", { + "k2": "w0" }); }) .push(function () { - return jio1.get("doc", { - path: "consistent", - steps: 1 + return jio.allDocs({ + revision_limit: [1, 3] }); }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.2.2.2" - }, "The new document is added to the correct edit branch" - ); - return jio1.get("doc", { - path: "consistent", - steps: 2 - }); + .push(function (results) { + equal(results.data.rows.length, 3); + deepEqual(results.data.rows[0].doc, { + "k": "v3" + }, "Does not retrieve new document outside queried revision range"); + deepEqual(results.data.rows[1].doc, { + "k": "v2" + }, "Does not retrieve new document outside queried revision range"); + deepEqual(results.data.rows[2].doc, { + "k": "v1" + }, "Does not retrieve new document outside queried revision range"); }) .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "This document was removed at this time"); - return jio1.get("doc", { - path: "consistent", - steps: 3 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.2" - }, "The new document is added to the correct edit branch" - ); - return jio1.get("doc", { - path: "consistent", - steps: 4 + return jio.allDocs({ + revision_limit: [0, 2] }); }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1" - }, "This edit branch also leads back to the original version" - ); - return jio1.get("doc", { - path: "consistent", - steps: 5 - }); + .push(function (results) { + equal(results.data.rows.length, 2); + deepEqual(results.data.rows[0].doc, { + "k2": "w0" + }, "Retrieves all documents with versions in queried revision range"); + deepEqual(results.data.rows[1].doc, { + "k": "v3" + }, "Retrieves all documents with versions in queried revision range"); }) .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "There are no revisions before the original document"); - }) - .push(function () { - return jio3.put("doc", { - "k": "v0.1.3.3.3.3" + return jio.put("doc2", { + "k2": "w1" }); }) - - // All three users have the same latest revision .push(function () { - return jio1.get("doc"); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.3.3" - }, "User one accesses latest revision correctly" - ); - return jio2.get("doc"); + return jio.allDocs(); }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.3.3" - }, "User two accesses latest revision correctly" - ); - return jio3.get("doc"); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.3.3" - }, "User three accesses latest revision correctly" - ); - return jio2.get("doc", { - path: "consistent", - steps: 1 + .push(function (results) { + equal(results.data.rows.length, 1, + "There is only one non-removed doc"); + equal(results.data.rows[0].rev, 0); + deepEqual(results.data.rows[0].doc, { + "k2": "w1" }); }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.3" - }, "User 2 accesses the 1st edit in consistent traversal." - ); - }) - - // - // Testing .getting on leaf nodes - // .push(function () { - return jio1.get("doc", { - path: "leaves" - }); + return jio.remove("doc2"); }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.3.3" - }, "First result is the most-recently-added leaf" - ); - return jio2.get("doc", { - path: "leaves", - steps: 1 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.2.2.2.1" - }, "Second result is the 2nd most-recently-added leaf" - ); - return jio3.get("doc", { - path: "leaves", - steps: 2, - db: "jio3" + .push(function () { + return jio.allDocs({ + revision_limit: [0, 4] }); }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.1" - }, "Third result is the 3rd most-recently-added leaf" - ); - - // - // Editing document revisions stemming from the latest leaf nodes seen - // - return jio1.put("doc", { - "k": "v0.1.3.3.3.3.1" + .push(function (results) { + equal(results.data.rows.length, 5); + equal(results.data.rows[0].rev, 1, "Rev parameter is correct"); + equal(results.data.rows[1].rev, 2, "Rev parameter is correct"); + equal(results.data.rows[2].rev, 1, "Rev parameter is correct"); + equal(results.data.rows[3].rev, 2, "Rev parameter is correct"); + equal(results.data.rows[4].rev, 3, "Rev parameter is correct"); + deepEqual(results.data.rows[0].doc, { + "k2": "w1" }); - }) - .push(function () { - return jio3.remove("doc"); // removing v0.1.3.3.1 - }) - - // Check that jio1 sees latest non-removed revision - .push(function () { - return jio1.get("doc"); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "The most recent edit was a remove, so throw error"); - }) - .push(function () { - // jio2 lastseen should point to "v0.1.2.2.2.1" - return jio2.put("doc", { - "k": "v0.1.2.2.2.1.2" + deepEqual(results.data.rows[1].doc, { + "k2": "w0" }); - }) - .push(function () { - return jio1.get("doc", { - path: "leaves", - steps: 0 + deepEqual(results.data.rows[2].doc, { + "k": "v3" }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.2.2.2.1.2" - }, "Accessing the first leaf node at this time" - ); - return jio1.get("doc", { - path: "leaves", - steps: 1 + deepEqual(results.data.rows[3].doc, { + "k": "v2" }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.3.3.1" - }, "Accessing the second leaf node at this time" - ); - return jio1.get("doc", { - path: "leaves", - steps: 2 + deepEqual(results.data.rows[4].doc, { + "k": "v1" }); }) .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "There are only two non-removed leaves"); - - // jio1 should still have lastseen at v0.1.3.3.3.3.1 - return jio1.put("doc", { - "k": "v0.1.3.3.3.3.1.1" - }); - }) - .push(function () { - return jio1.get("doc", { - path: "consistent", - steps: 1 - }); - }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.3.3.1" - }, "If a .get fails, that should not reset ._lastseen parameter" - ); - return jio1.get("doc", { - path: "consistent", - steps: 2 + return jio.allDocs({ + limit: [1, 4], + revision_limit: [0, 4] }); }) - .push(function (result) { - deepEqual(result, { - "k": "v0.1.3.3.3.3" - }, "History of 0.1.2.2.2 has been constructed correctly."); + .push(function (results) { + equal(results.data.rows.length, 4, + "Correct number of results with options.limit set"); + equal(results.data.rows[0].rev, 2, "Rev parameter is correct"); + equal(results.data.rows[1].rev, 1, "Rev parameter is correct"); + equal(results.data.rows[2].rev, 2, "Rev parameter is correct"); + equal(results.data.rows[3].rev, 3, "Rev parameter is correct"); + deepEqual(results.data.rows[0].doc, { + "k2": "w0" + }, "Correct results with options.limit set"); + deepEqual(results.data.rows[1].doc, { + "k": "v3" + }, "Correct results with options.limit set"); + deepEqual(results.data.rows[2].doc, { + "k": "v2" + }, "Correct results with options.limit set"); + deepEqual(results.data.rows[3].doc, { + "k": "v1" + }, "Correct results with options.limit set"); }) .fail(function (error) { //console.log(error); -- 2.30.9 From c403f6bfd434ee7dabf34df71706d560b9c98429 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Tue, 12 Jun 2018 13:36:59 +0000 Subject: [PATCH 21/46] Get and allDocs can now be used to access older revisions without changes to the API. The approach to doing this with allDocs is very costly right now, but is being fixed for the next commit. --- src/jio.storage/bryanstorage.js | 212 ++++++++++++++++--------- test/jio.storage/bryanstorage.tests.js | 147 +++++++++++------ 2 files changed, 232 insertions(+), 127 deletions(-) diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js index 3ec912f..4f4d4fb 100644 --- a/src/jio.storage/bryanstorage.js +++ b/src/jio.storage/bryanstorage.js @@ -28,18 +28,15 @@ }); } - BryanStorage.prototype.get = function (id_in, steps) { + BryanStorage.prototype.get = function (id_in) { - if (steps === undefined) { - steps = 0; - } // Query to get the last edit made to this document var substorage = this._sub_storage, options = { query: "doc_id: " + id_in, sort_on: [["timestamp", "descending"]], - limit: [steps, 1] + limit: [0, 1] }; return substorage.allDocs(options) @@ -52,7 +49,6 @@ 404 ); }) - .push(function (result) { if (result.op === "put") { return result.doc; @@ -61,6 +57,48 @@ "bryanstorage: cannot find object '" + id_in + "' (removed)", 404 ); + + // If no documents returned in first query, check if the id is encoding + // revision information + }, function () { + var steps, + steps_loc = id_in.lastIndexOf("_-"); + // If revision signature is not in id_in, than return 404, since id + // is not found + if (steps_loc === -1) { + throw new jIO.util.jIOError( + "bryanstorage: cannot find object '" + id_in + "'", + 404 + ); + } + + // If revision signature is found, query storage based on this + steps = Number(id_in.slice(steps_loc + 2)); + id_in = id_in.slice(0, steps_loc); + options = { + query: "doc_id: " + id_in, + sort_on: [["timestamp", "descending"]], + limit: [steps, 1] + }; + return substorage.allDocs(options) + .push(function (results) { + if (results.data.rows.length > 0) { + return substorage.get(results.data.rows[0].id); + } + throw new jIO.util.jIOError( + "bryanstorage: cannot find object '" + id_in + "'", + 404 + ); + }) + .push(function (result) { + if (result.op === "put") { + return result.doc; + } + throw new jIO.util.jIOError( + "bryanstorage: cannot find object '" + id_in + "' (removed)", + 404 + ); + }); }); }; @@ -77,7 +115,6 @@ doc: data, op: "put" }; - this._lastseen = timestamp; return this._sub_storage.put(timestamp, metadata); }; @@ -152,34 +189,49 @@ }; BryanStorage.prototype.buildQuery = function (options) { + if (options === undefined) { options = {}; } - if (options.sort_on === undefined) { - options.sort_on = []; - } - options.sort_on.push(["timestamp", "descending"]); - if (options.limit === undefined) { - options.limit = [0, -1]; + if (options.query === undefined) { + options.query = ""; } - - // Default behavior is to return only the latest revision of each document - if (options.revision_limit === undefined) { - options.revision_limit = [0, 1]; - } - + options.query = jIO.QueryFactory.create(options.query); var meta_options = { - // XXX: I don't believe it's currently possible to query on sub-attributes - // so for now, we just use the inputted query, which obviously will fail - query: options.query, - - // XXX: same here, we cannot sort correctly because we cannot access - // attributes of doc - sort_on: options.sort_on - }, + // XXX: I don't believe it's currently possible to query on + // sub-attributes so for now, we just use the inputted query, which + // obviously will fail + query: "", + + // XXX: same here, we cannot sort correctly because we cannot access + // attributes of doc + sort_on: [["timestamp", "descending"]] + }, substorage = this._sub_storage, - max_num_docs = options.limit[1], - first_doc = options.limit[0]; + + // Check if query involved _REVISION. If not, we will later place a + // (*) AND (_REVISION: =0) as the default handling of revisions + rev_query = false, + query_obj = options.query, + query_stack = [], + ind; + + if (query_obj.hasOwnProperty("query_list")) { + query_stack.push(query_obj); + } else { + rev_query = (query_obj.key === "_REVISION"); + } + while (query_stack.length > 0 && (!rev_query)) { + query_obj = query_stack.pop(); + for (ind = 0; ind < query_obj.query_list.length; ind += 1) { + if (query_obj.query_list[ind].hasOwnProperty("query_list")) { + query_stack.push(query_obj.query_list[ind]); + } else if (query_obj.query_list[ind].key === "_REVISION") { + rev_query = true; + break; + } + } + } return this._sub_storage.allDocs(meta_options) @@ -191,56 +243,66 @@ return RSVP.all(promises); }) - .push(function (results_array) { - var clean_data = [], - ind, - seen_docs = {}, - current_doc, - counter = 0; - - // Default behavior is to not limit the number of documents returned - if (max_num_docs === -1) { - max_num_docs = results_array.length; - } - for (ind = 0; ind < results_array.length; ind += 1) { - current_doc = results_array[ind]; - - // Initialize count of revisions - if (!seen_docs.hasOwnProperty(current_doc.doc_id)) { - seen_docs[current_doc.doc_id] = 0; + .push(function (results) { + // Label all documents with their current revision status + var doc, + revision_tracker = {}, + promises; + for (ind = 0; ind < results.length; ind += 1) { + doc = results[ind]; + if (revision_tracker.hasOwnProperty(doc.doc_id)) { + revision_tracker[doc.doc_id] += 1; + } else { + revision_tracker[doc.doc_id] = 0; } + doc._REVISION = revision_tracker[doc.doc_id]; + } - // If the latest version of this document has not yet been - // included in query result - if (options.revision_limit[0] <= seen_docs[current_doc.doc_id] && - seen_docs[current_doc.doc_id] < options.revision_limit[0] + - options.revision_limit[1]) { - - // If the latest edit was a put operation, add it to query - // results - if (current_doc.op === "put") { - if (counter >= first_doc) { - - // Note the rev attribute added to the output data. - // This guarantees that `this.get(id, rev) === doc` - clean_data.push({ - doc: current_doc.doc, - value: {}, - id: current_doc.doc_id, - rev: seen_docs[current_doc.doc_id] - }); - if (clean_data.length === max_num_docs) { - return clean_data; - } - } - counter += 1; - } + // There must be a faster way + promises = results.map(function (data) { + return substorage.put(data.timestamp, data); + }); + return RSVP.all(promises); + }) + .push(function () { + var latest_rev_query; + latest_rev_query = jIO.QueryFactory.create( + "(_REVISION: >= 0) AND (NOT op: remove)" + ); + if (rev_query) { + latest_rev_query.query_list[0] = options.query; + } else { + latest_rev_query.query_list[0] = jIO.QueryFactory.create( + "(_REVISION: =0)" + ); + if (options.query.type === "simple" || + options.query.type === "complex") { + latest_rev_query.query_list.push(options.query); } - // Keep track of how many times this doc_id has been seen - seen_docs[current_doc.doc_id] += 1; } - // In passing results back to allDocs, formatting of query is handled - return clean_data; + // Build a query for final push + options.query = latest_rev_query; + if (options.sort_on === undefined) { + options.sort_on = []; + } + options.sort_on.push(["timestamp", "descending"]); + return substorage.allDocs(options); + }) + .push(function (results) { + var promises = results.data.rows.map(function (data) { + return substorage.get(data.id); + }); + return RSVP.all(promises); + }) + .push(function (results) { + return results + .map(function (current_doc) { + return { + doc: current_doc.doc, + value: {}, + id: current_doc.doc_id + }; + }); }); }; diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js index ccbb412..a6edfbc 100644 --- a/test/jio.storage/bryanstorage.tests.js +++ b/test/jio.storage/bryanstorage.tests.js @@ -287,7 +287,7 @@ test("Testing proper retrieval of older revisions of documents", function () { stop(); - expect(12); + expect(18); // create storage of type "bryan" with memory as substorage var jio = jIO.createJIO({ @@ -314,7 +314,7 @@ }); }) .push(function () { - return jio.get("doc", 0); + return jio.get("doc_-0"); }) .push(function (result) { deepEqual(result, { @@ -325,7 +325,7 @@ return jio.put("doc", {"k1": "v1"}); }) .push(function () { - return jio.get("doc", 0); + return jio.get("doc_-0"); }) .push(function (result) { deepEqual(result, { @@ -333,7 +333,7 @@ }); }) .push(function () { - return jio.get("doc", 1); + return jio.get("doc_-1"); }) .push(function (result) { deepEqual(result, { @@ -359,19 +359,19 @@ deepEqual(result, {"k4": "v4"}, "By default, .get returns latest revision"); - return jio.get("doc", 0); + return jio.get("doc"); }) .push(function (result) { deepEqual(result, {"k4": "v4"}, ".get returns latest revision with second input = 0"); - return jio.get("doc", 1); + return jio.get("doc_-1"); }) .push(function (result) { deepEqual(result, {"k3": "v3"}, "Walk back one revision with second input = 1"); - return jio.get("doc", 2); + return jio.get("doc_-2"); }) .push(function () { ok(false, "This query should have thrown a 404 error"); @@ -380,25 +380,25 @@ deepEqual(error.status_code, 404, "Current state of document is 'removed'."); - return jio.get("doc", 3); + return jio.get("doc_-3"); }) .push(function (result) { deepEqual(result, {"k2": "v2"}, "Walk back three revisions with second input = 3"); - return jio.get("doc", 4); + return jio.get("doc_-4"); }) .push(function (result) { deepEqual(result, {"k1": "v1"}, "Walk back four revisions with second input = 4"); - return jio.get("doc", 5); + return jio.get("doc_-5"); }) .push(function (result) { deepEqual(result, {"k0": "v0"}, "Walk back five revisions with second input = 5"); - return jio.get("doc", 6); + return jio.get("doc_-6"); }) .push(function () { ok(false, "This query should have thrown a 404 error"); @@ -408,6 +408,65 @@ 404, "There are only 5 previous states of this document"); }) + + // Adding documents with problematic doc_id's + .push(function () { + return jio.put("doc_-name", { + "key": "val0" + }); + }) + .push(function () { + return jio.put("document_-0", { + "key": "and val0" + }); + }) + .push(function () { + return jio.put("doc_-name", { + "key": "val1" + }); + }) + + .push(function () { + return jio.get("doc_-name"); + }) + .push(function (result) { + deepEqual(result, { + "key": "val1" + }); + return jio.get("doc_-name_-0"); + }) + .push(function (result) { + deepEqual(result, { + "key": "val1" + }); + return jio.get("doc_-name_-1"); + }) + .push(function (result) { + deepEqual(result, { + "key": "val0" + }); + return jio.get("document_-0"); + }) + .push(function (result) { + deepEqual(result, { + "key": "and val0" + }); + return jio.get("document_-0_-0"); + }) + .push(function (result) { + deepEqual(result, { + "key": "and val0" + }); + return jio.get("document_-0_-1"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "Document does not have this many revisions."); + }) .fail(function (error) { //console.log(error); ok(false, error); @@ -416,27 +475,25 @@ }); ///////////////////////////////////////////////////////////////// - // Accessing older revisions with two users + // Querying older revisions ///////////////////////////////////////////////////////////////// module("bryanStorage.querying_old_revisions"); test("Testing retrieval of older revisions via allDocs calls", function () { stop(); - expect(47); + expect(37); // create storage of type "bryan" with memory as substorage - var dbname = "db-" + Date.now(), - jio = jIO.createJIO({ - type: "bryan", + var jio = jIO.createJIO({ + type: "bryan", + sub_storage: { + type: "uuid", sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname - } + type: "memory" } - }); + } + }); jio.put("doc", { "k": "v0" }) @@ -457,8 +514,7 @@ }) .push(function () { return jio.allDocs({ - query: "", - revision_limit: [0, 1] + query: "_REVISION : 0" }); }) .push(function (results) { @@ -474,8 +530,7 @@ }) .push(function () { return jio.allDocs({ - query: "", - revision_limit: [1, 1] + query: "_REVISION : =1" }); }) .push(function (results) { @@ -486,8 +541,7 @@ "k": "v2" }); return jio.allDocs({ - query: "", - revision_limit: [2, 1] + query: "_REVISION : =2" }); }) .push(function (results) { @@ -498,8 +552,7 @@ "k": "v1" }); return jio.allDocs({ - query: "", - revision_limit: [3, 1] + query: "_REVISION : =3" }); }) .push(function (results) { @@ -510,8 +563,7 @@ "k": "v0" }); return jio.allDocs({ - query: "", - revision_limit: [4, 1] + query: "_REVISION : =4" }); }) .push(function (results) { @@ -519,7 +571,7 @@ }) .push(function () { return jio.allDocs({ - revision_limit: [0, 2] + query: "_REVISION: <= 1" }); }) .push(function (results) { @@ -536,7 +588,7 @@ }) .push(function () { return jio.allDocs({ - query: "", + query: "NOT (_REVISION: >= 1)", revision_limit: [0, 1] }); }) @@ -546,8 +598,7 @@ }) .push(function () { return jio.allDocs({ - query: "", - revision_limit: [1, 3] + query: "(_REVISION: >= 1) AND (_REVISION: <= 3)" }); }) .push(function (results) { @@ -569,7 +620,7 @@ }) .push(function () { return jio.allDocs({ - revision_limit: [1, 3] + query: "(_REVISION: >0) AND (_REVISION: <= 3)" }); }) .push(function (results) { @@ -586,7 +637,7 @@ }) .push(function () { return jio.allDocs({ - revision_limit: [0, 2] + query: "(_REVISION: = 0) OR (_REVISION: = 1)" }); }) .push(function (results) { @@ -609,7 +660,6 @@ .push(function (results) { equal(results.data.rows.length, 1, "There is only one non-removed doc"); - equal(results.data.rows[0].rev, 0); deepEqual(results.data.rows[0].doc, { "k2": "w1" }); @@ -619,16 +669,13 @@ }) .push(function () { return jio.allDocs({ - revision_limit: [0, 4] + query: + "_REVISION: 0 OR _REVISION: 1 OR " + + "(_REVISION: >= 2 AND _REVISION: <= 3)" }); }) .push(function (results) { equal(results.data.rows.length, 5); - equal(results.data.rows[0].rev, 1, "Rev parameter is correct"); - equal(results.data.rows[1].rev, 2, "Rev parameter is correct"); - equal(results.data.rows[2].rev, 1, "Rev parameter is correct"); - equal(results.data.rows[3].rev, 2, "Rev parameter is correct"); - equal(results.data.rows[4].rev, 3, "Rev parameter is correct"); deepEqual(results.data.rows[0].doc, { "k2": "w1" }); @@ -647,17 +694,13 @@ }) .push(function () { return jio.allDocs({ - limit: [1, 4], - revision_limit: [0, 4] + query: "_REVISION: <= 3", + limit: [1, 4] }); }) .push(function (results) { equal(results.data.rows.length, 4, - "Correct number of results with options.limit set"); - equal(results.data.rows[0].rev, 2, "Rev parameter is correct"); - equal(results.data.rows[1].rev, 1, "Rev parameter is correct"); - equal(results.data.rows[2].rev, 2, "Rev parameter is correct"); - equal(results.data.rows[3].rev, 3, "Rev parameter is correct"); + "Correct number of results with optins.limit set"); deepEqual(results.data.rows[0].doc, { "k2": "w0" }, "Correct results with options.limit set"); -- 2.30.9 From ca6fac12b9f14a5f8106f5ab6de73789251ae968 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Tue, 12 Jun 2018 14:46:12 +0000 Subject: [PATCH 22/46] Renamed bryanstorage and improved allDocs. --- src/jio.storage/historystorage.js | 271 +++++++++ test/jio.storage/historystorage.tests.js | 724 +++++++++++++++++++++++ 2 files changed, 995 insertions(+) create mode 100644 src/jio.storage/historystorage.js create mode 100644 test/jio.storage/historystorage.tests.js diff --git a/src/jio.storage/historystorage.js b/src/jio.storage/historystorage.js new file mode 100644 index 0000000..538d74b --- /dev/null +++ b/src/jio.storage/historystorage.js @@ -0,0 +1,271 @@ +/*jslint nomen: true*/ +/*global RSVP, jiodate*/ +(function (jIO) { + "use strict"; + + // Used to distinguish between operations done within the same millisecond + var unique_timestamp = function () { + + // XXX: replace this with UUIDStorage function call to S4() when it becomes + // publicly accessible + var uuid = ('0000' + Math.floor(Math.random() * 0x10000) + .toString(16)).slice(-4), + timestamp = Date.now().toString(); + return timestamp + "-" + uuid; + }; + + /** + * The jIO HistoryStorage extension + * + * @class HistoryStorage + * @constructor + */ + function HistoryStorage(spec) { + + this._sub_storage = jIO.createJIO({ + type: "query", + sub_storage: spec.sub_storage + }); + } + + HistoryStorage.prototype.get = function (id_in) { + + + // Query to get the last edit made to this document + var substorage = this._sub_storage, + options = { + query: "doc_id: " + id_in, + sort_on: [["timestamp", "descending"]], + limit: [0, 1] + }; + + return substorage.allDocs(options) + .push(function (results) { + if (results.data.rows.length > 0) { + return substorage.get(results.data.rows[0].id); + } + throw new jIO.util.jIOError( + "HistoryStorage: cannot find object '" + id_in + "'", + 404 + ); + }) + .push(function (result) { + if (result.op === "put") { + return result.doc; + } + throw new jIO.util.jIOError( + "HistoryStorage: cannot find object '" + id_in + "' (removed)", + 404 + ); + + // If no documents returned in first query, check if the id is encoding + // revision information + }, function () { + var steps, + steps_loc = id_in.lastIndexOf("_-"); + // If revision signature is not in id_in, than return 404, since id + // is not found + if (steps_loc === -1) { + throw new jIO.util.jIOError( + "HistoryStorage: cannot find object '" + id_in + "'", + 404 + ); + } + + // If revision signature is found, query storage based on this + steps = Number(id_in.slice(steps_loc + 2)); + id_in = id_in.slice(0, steps_loc); + options = { + query: "doc_id: " + id_in, + sort_on: [["timestamp", "descending"]], + limit: [steps, 1] + }; + return substorage.allDocs(options) + .push(function (results) { + if (results.data.rows.length > 0) { + return substorage.get(results.data.rows[0].id); + } + throw new jIO.util.jIOError( + "HistoryStorage: cannot find object '" + id_in + "'", + 404 + ); + }) + .push(function (result) { + if (result.op === "put") { + return result.doc; + } + throw new jIO.util.jIOError( + "HistoryStorage: cannot find object '" + id_in + "' (removed)", + 404 + ); + }); + }); + }; + + HistoryStorage.prototype.post = function (metadata) { + return this._sub_storage.post(metadata); + }; + + HistoryStorage.prototype.put = function (id, data) { + var timestamp = unique_timestamp(), + metadata = { + // XXX: remove this attribute once query can sort_on id + timestamp: timestamp, + doc_id: id, + doc: data, + op: "put" + }; + return this._sub_storage.put(timestamp, metadata); + }; + + HistoryStorage.prototype.remove = function (id) { + var timestamp = unique_timestamp(), + metadata = { + // XXX: remove this attribute once query can sort_on id + timestamp: timestamp, + doc_id: id, + op: "remove" + }; + return this._sub_storage.put(timestamp, metadata); + }; + + HistoryStorage.prototype.allAttachments = function () { + return this._sub_storage.allAttachments.apply(this._sub_storage, arguments); + }; + HistoryStorage.prototype.getAttachment = function () { + return this._sub_storage.getAttachment.apply(this._sub_storage, arguments); + }; + HistoryStorage.prototype.putAttachment = function () { + return this._sub_storage.putAttachment.apply(this._sub_storage, arguments); + }; + HistoryStorage.prototype.removeAttachment = function () { + return this._sub_storage.removeAttachment + .apply(this._sub_storage, arguments); + }; + HistoryStorage.prototype.repair = function () { + return this._sub_storage.repair.apply(this._sub_storage, arguments); + }; + HistoryStorage.prototype.hasCapacity = function () { + return this._sub_storage.hasCapacity.apply(this._sub_storage, arguments); + }; + + HistoryStorage.prototype.buildQuery = function (options) { + + if (options === undefined) { + options = {}; + } + if (options.query === undefined) { + options.query = ""; + } + if (options.sort_on === undefined) { + options.sort_on = []; + } + options.sort_on.push(["timestamp", "descending"]); + options.query = jIO.QueryFactory.create(options.query); + var meta_options = { + // XXX: I don't believe it's currently possible to query on + // sub-attributes so for now, we just use the inputted query, which + // obviously will fail + query: "", + + // XXX: same here, we cannot sort correctly because we cannot access + // attributes of doc + sort_on: options.sort_on + }, + substorage = this._sub_storage, + + // Check if query involved _REVISION. If not, we will later place a + // (*) AND (_REVISION: =0) as the default handling of revisions + rev_query = false, + query_obj = options.query, + query_stack = [], + ind; + if (query_obj.hasOwnProperty("query_list")) { + query_stack.push(query_obj); + } else { + rev_query = (query_obj.key === "_REVISION"); + } + while (query_stack.length > 0 && (!rev_query)) { + query_obj = query_stack.pop(); + for (ind = 0; ind < query_obj.query_list.length; ind += 1) { + if (query_obj.query_list[ind].hasOwnProperty("query_list")) { + query_stack.push(query_obj.query_list[ind]); + } else if (query_obj.query_list[ind].key === "_REVISION") { + rev_query = true; + break; + } + } + } + + return this._sub_storage.allDocs(meta_options) + + // Get all documents found in query + .push(function (results) { + var promises = results.data.rows.map(function (data) { + return substorage.get(data.id); + }); + return RSVP.all(promises); + }) + + .push(function (results) { + // Label all documents with their current revision status + var doc, + revision_tracker = {}, + latest_rev_query; + for (ind = 0; ind < results.length; ind += 1) { + doc = results[ind]; + if (revision_tracker.hasOwnProperty(doc.doc_id)) { + revision_tracker[doc.doc_id] += 1; + } else { + revision_tracker[doc.doc_id] = 0; + } + doc._REVISION = revision_tracker[doc.doc_id]; + } + + // Create a new query to only get non-removed revisions and abide by + // whatever the inputted query says + latest_rev_query = jIO.QueryFactory.create( + "(_REVISION: >= 0) AND (NOT op: remove)" + ); + if (rev_query) { + latest_rev_query.query_list[0] = options.query; + } else { + latest_rev_query.query_list[0] = jIO.QueryFactory.create( + "(_REVISION: =0)" + ); + if (options.query.type === "simple" || + options.query.type === "complex") { + latest_rev_query.query_list.push(options.query); + } + } + + return results + // Only return results which match latest_rev_query + .filter(function (doc) { + return latest_rev_query.match(doc); + }) + + // Only return the correct range of valid results specified by + // options.limit + .filter(function (doc, ind) { + if (doc && options.hasOwnProperty("limit")) { + return (ind >= options.limit[0] && + options.limit[1] + options.limit[0] > ind); + } + return true; + }) + + // Format results to be expected output of allDocs + .map(function (current_doc) { + return { + doc: current_doc.doc, + value: {}, + id: current_doc.doc_id + }; + }); + }); + }; + + jIO.addStorage('history', HistoryStorage); + +}(jIO)); \ No newline at end of file diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js new file mode 100644 index 0000000..d7d7e70 --- /dev/null +++ b/test/jio.storage/historystorage.tests.js @@ -0,0 +1,724 @@ +/*jslint nomen: true*/ +/*global Blob, jiodate*/ +(function (jIO, QUnit) { + "use strict"; + var test = QUnit.test, + stop = QUnit.stop, + start = QUnit.start, + ok = QUnit.ok, + expect = QUnit.expect, + deepEqual = QUnit.deepEqual, + equal = QUnit.equal, + module = QUnit.module; + + + ///////////////////////////////////////////////////////////////// + // _revision parameter updating with RSVP all + ///////////////////////////////////////////////////////////////// + + module("HistoryStorage.revision_with_RSVP_all"); + test("verifying updates correctly when puts are done in parallel", + function () { + stop(); + expect(7); + + // create storage of type "history" with memory as substorage + var dbname = "rsvp_db_" + Date.now(), + jio = jIO.createJIO({ + type: "history", + sub_storage: { + type: "uuid", + sub_storage: { + //type: "memory" + type: "indexeddb", + database: dbname + } + } + }), + not_history = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + //type: "memory" + type: "indexeddb", + database: dbname + } + } + }); + + jio.put("bar", {"title": "foo0"}) + .push(function () { + return RSVP.all([ + jio.put("bar", {"title": "foo1"}), + jio.put("bar", {"title": "foo2"}), + jio.put("bar", {"title": "foo3"}), + jio.put("bar", {"title": "foo4"}), + jio.put("barbar", {"title": "attr0"}), + jio.put("barbar", {"title": "attr1"}), + jio.put("barbar", {"title": "attr2"}), + jio.put("barbar", {"title": "attr3"}) + ]); + }) + .push(function () {return jio.get("bar"); }) + .push(function (result) { + ok(result.title !== "foo0", "Title should have changed from foo0"); + }) + .push(function () { + return not_history.allDocs({ + query: "", + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + equal(results.data.rows.length, + 9, + "All nine versions exist in storage"); + return not_history.get(results.data.rows[0].id); + }) + .push(function (results) { + deepEqual(results, { + doc_id: "bar", + doc: { + title: "foo0" + }, + timestamp: results.timestamp, + op: "put" + }, "The first item in the log is pushing bar's title to 'foo0'"); + return jio.remove("bar"); + }) + .push(function () { + return jio.get("bar"); + }) + .push(function () { + return jio.get("barbar"); + }, function (error) { + deepEqual( + error.message, + "HistoryStorage: cannot find object 'bar' (removed)", + "Appropriate error is sent explaining object has been removed" + ); + return jio.get("barbar"); + }) + .push(function (result) { + ok(result.title !== undefined, "barbar exists and has proper form"); + return not_history.allDocs({ + query: "", + sort_on: [["op", "descending"]] + }); + }) + .push(function (results) { + equal(results.data.rows.length, + 10, + "Remove operation is recorded"); + return not_history.get(results.data.rows[0].id); + }) + .push(function (result) { + deepEqual(result, { + doc_id: "bar", + timestamp: result.timestamp, + op: "remove" + }); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + + + ///////////////////////////////////////////////////////////////// + // historyStorage.querying_from_historystorage + ///////////////////////////////////////////////////////////////// + + module("HistoryStorage.querying_from_historystorage"); + test("verifying the correct results are returned from historyStorage.allDocs", + function () { + stop(); + expect(10); + + // create storage of type "history" with memory as substorage + var jio = jIO.createJIO({ + type: "history", + sub_storage: { + type: "uuid", + sub_storage: { + type: "memory" + } + } + }); + jio.put("bar", {"title": "foo0"}) + .push(function () { + return RSVP.all([ + jio.remove("bar"), + jio.put("bar", {"title": "foo1"}), + jio.put("bar", {"title": "foo2"}), + jio.put("bar", {"title": "foo3"}), + jio.put("barbar", {"title": "attr0"}), + jio.put("barbar", {"title": "attr1"}), + jio.put("barbar", {"title": "attr2"}), + jio.put("barbarbar", {"title": "val0"}), + jio.put("barbarbarbar", {"title": "prop0"}) + ]); + }) + // Make two final puts so we know what to expect as the current state of + // each document. + .push(function () { + return jio.put("barbar", {"title": "attr3"}); + }) + .push(function () { + return jio.put("bar", {"title": "foo4"}); + }) + + // Queries should only include information about the final two versions + .push(function () { + return jio.allDocs({ + query: "", + sort_on: [["title", "ascending"]] + }); + }) + .push(function (results) { + equal(results.data.rows.length, + 4, + "Empty query yields four results since there are four unique docs"); + return jio.get(results.data.rows[0].id); + }, + function (error) { + return ok(false, "Query failed: " + error); + }) + .push(function (result) { + deepEqual(result, { + title: "attr3" + }, + "NOT IMPLEMENTED: Retrieve correct sort order with no metadata"); + }, + function () { + return ok(false, "Couldn't find document in storage"); + }) + + // Querying with a limit + .push(function () { + return jio.allDocs({ + query: "", + sort_on: [["title", "ascending"]], + limit: [0, 1] + }); + }) + .push(function (results) { + equal(results.data.rows.length, + 1, + "Since limit [0,1] was supplied, only 1st document is returned"); + return jio.get(results.data.rows[0].id); + }) + .push(function (result) { + deepEqual(result, { + title: "attr3" + }, + "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); + }) + + // Querying with a more complicated limit + .push(function () { + return jio.allDocs({ + query: "", + sort_on: [["title", "ascending"]], + limit: [2, 2] + }); + }) + .push(function (results) { + equal(results.data.rows.length, + 2, + "Retrieving the correct documents when options.limit is specified"); + + deepEqual(results.data.rows[0].id, + "barbarbarbar", + "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); + + deepEqual(results.data.rows[1].id, + "barbarbar", + "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); + + return jio.get(results.data.rows[0].id); + }) + .push(function (result) { + deepEqual(result, { + title: "property0" + }, + "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); + }) + + // Querying for a specific id + .push(function () { + return jio.allDocs({ + query: "id: bar" + }); + }) + .push(function (results) { + equal(results.data.rows.length, + 1, + "NOT IMPLEMENTED: query involving specific document attributes"); + return jio.get(results.data.rows[0].id); + }) + .push(function (result) { + deepEqual(result, { + title: "foo4" + }, + "NOT IMPLEMENTED: query involving specific document attributes"); + }, + function () { + ok(false, + "NOT IMPLEMENTED: query involving specific document attributes" + ); + }) + + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + + ///////////////////////////////////////////////////////////////// + // Accessing older revisions + ///////////////////////////////////////////////////////////////// + + module("HistoryStorage.accessing_older_revisions"); + test("Testing proper retrieval of older revisions of documents", + function () { + stop(); + expect(18); + + // create storage of type "history" with memory as substorage + var jio = jIO.createJIO({ + type: "history", + sub_storage: { + type: "uuid", + sub_storage: { + type: "memory" + } + } + }); + jio.get("doc") + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "Document does not exist yet."); + }) + .push(function () { + return jio.put("doc", { + "k0": "v0" + }); + }) + .push(function () { + return jio.get("doc_-0"); + }) + .push(function (result) { + deepEqual(result, { + "k0": "v0" + }); + }) + .push(function () { + return jio.put("doc", {"k1": "v1"}); + }) + .push(function () { + return jio.get("doc_-0"); + }) + .push(function (result) { + deepEqual(result, { + "k1": "v1" + }); + }) + .push(function () { + return jio.get("doc_-1"); + }) + .push(function (result) { + deepEqual(result, { + "k0": "v0" + }); + }) + .push(function () { + return jio.put("doc", {"k2": "v2"}); + }) + .push(function () { + return jio.remove("doc"); + }) + .push(function () { + return jio.put("doc", {"k3": "v3"}); + }) + .push(function () { + return jio.put("doc", {"k4": "v4"}); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (result) { + deepEqual(result, + {"k4": "v4"}, + "By default, .get returns latest revision"); + return jio.get("doc"); + }) + .push(function (result) { + deepEqual(result, + {"k4": "v4"}, + ".get returns latest revision with second input = 0"); + return jio.get("doc_-1"); + }) + .push(function (result) { + deepEqual(result, + {"k3": "v3"}, + "Walk back one revision with second input = 1"); + return jio.get("doc_-2"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "Current state of document is 'removed'."); + return jio.get("doc_-3"); + }) + .push(function (result) { + deepEqual(result, + {"k2": "v2"}, + "Walk back three revisions with second input = 3"); + return jio.get("doc_-4"); + }) + .push(function (result) { + deepEqual(result, + {"k1": "v1"}, + "Walk back four revisions with second input = 4"); + return jio.get("doc_-5"); + }) + .push(function (result) { + deepEqual(result, + {"k0": "v0"}, + "Walk back five revisions with second input = 5"); + return jio.get("doc_-6"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "There are only 5 previous states of this document"); + }) + + // Adding documents with problematic doc_id's + .push(function () { + return jio.put("doc_-name", { + "key": "val0" + }); + }) + .push(function () { + return jio.put("document_-0", { + "key": "and val0" + }); + }) + .push(function () { + return jio.put("doc_-name", { + "key": "val1" + }); + }) + + .push(function () { + return jio.get("doc_-name"); + }) + .push(function (result) { + deepEqual(result, { + "key": "val1" + }); + return jio.get("doc_-name_-0"); + }) + .push(function (result) { + deepEqual(result, { + "key": "val1" + }); + return jio.get("doc_-name_-1"); + }) + .push(function (result) { + deepEqual(result, { + "key": "val0" + }); + return jio.get("document_-0"); + }) + .push(function (result) { + deepEqual(result, { + "key": "and val0" + }); + return jio.get("document_-0_-0"); + }) + .push(function (result) { + deepEqual(result, { + "key": "and val0" + }); + return jio.get("document_-0_-1"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + deepEqual(error.status_code, + 404, + "Document does not have this many revisions."); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + + ///////////////////////////////////////////////////////////////// + // Querying older revisions + ///////////////////////////////////////////////////////////////// + + module("HistoryStorage.querying_old_revisions"); + test("Testing retrieval of older revisions via allDocs calls", + function () { + stop(); + expect(37); + + // create storage of type "history" with memory as substorage + var jio = jIO.createJIO({ + type: "history", + sub_storage: { + type: "uuid", + sub_storage: { + type: "memory" + } + } + }); + jio.put("doc", { + "k": "v0" + }) + .push(function () { + return jio.put("doc", { + "k": "v1" + }); + }) + .push(function () { + return jio.put("doc", { + "k": "v2" + }); + }) + .push(function () { + return jio.put("doc", { + "k": "v3" + }); + }) + .push(function () { + return jio.allDocs({ + query: "_REVISION : 0" + }); + }) + .push(function (results) { + deepEqual(results.data.rows.length, + 1, + "Only one query returned with options.revision_limit == [0,1]"); + return jio.get(results.data.rows[0].id); + }) + .push(function (result) { + deepEqual(result, { + "k": "v3" + }); + }) + .push(function () { + return jio.allDocs({ + query: "_REVISION : =1" + }); + }) + .push(function (results) { + deepEqual(results.data.rows.length, + 1, + "Only one query returned with options.revision_limit == [1,1]"); + deepEqual(results.data.rows[0].doc, { + "k": "v2" + }); + return jio.allDocs({ + query: "_REVISION : =2" + }); + }) + .push(function (results) { + deepEqual(results.data.rows.length, + 1, + "Only one query returned with options.revision_limit == [2,1]"); + deepEqual(results.data.rows[0].doc, { + "k": "v1" + }); + return jio.allDocs({ + query: "_REVISION : =3" + }); + }) + .push(function (results) { + deepEqual(results.data.rows.length, + 1, + "Only one query returned with options.revision_limit == [3,1]"); + deepEqual(results.data.rows[0].doc, { + "k": "v0" + }); + return jio.allDocs({ + query: "_REVISION : =4" + }); + }) + .push(function (results) { + equal(results.data.rows.length, 0, "Past all previous revisions"); + }) + .push(function () { + return jio.allDocs({ + query: "_REVISION: <= 1" + }); + }) + .push(function (results) { + equal(results.data.rows.length, 2); + deepEqual(results.data.rows[0].doc, { + "k": "v3" + }, "Only retrieve two most recent revions"); + deepEqual(results.data.rows[1].doc, { + "k": "v2" + }); + }) + .push(function () { + return jio.remove("doc"); + }) + .push(function () { + return jio.allDocs({ + query: "NOT (_REVISION: >= 1)", + revision_limit: [0, 1] + }); + }) + .push(function (results) { + equal(results.data.rows.length, 0, + "Query does not return removed doc"); + }) + .push(function () { + return jio.allDocs({ + query: "(_REVISION: >= 1) AND (_REVISION: <= 3)" + }); + }) + .push(function (results) { + equal(results.data.rows.length, 3); + deepEqual(results.data.rows[0].doc, { + "k": "v3" + }, "1st, 2nd, and 3rd versions removed from current are retrieved"); + deepEqual(results.data.rows[1].doc, { + "k": "v2" + }, "1st, 2nd, and 3rd versions removed from current are retrieved"); + deepEqual(results.data.rows[2].doc, { + "k": "v1" + }, "1st, 2nd, and 3rd versions removed from current are retrieved"); + }) + .push(function () { + return jio.put("doc2", { + "k2": "w0" + }); + }) + .push(function () { + return jio.allDocs({ + query: "(_REVISION: >0) AND (_REVISION: <= 3)" + }); + }) + .push(function (results) { + equal(results.data.rows.length, 3); + deepEqual(results.data.rows[0].doc, { + "k": "v3" + }, "Does not retrieve new document outside queried revision range"); + deepEqual(results.data.rows[1].doc, { + "k": "v2" + }, "Does not retrieve new document outside queried revision range"); + deepEqual(results.data.rows[2].doc, { + "k": "v1" + }, "Does not retrieve new document outside queried revision range"); + }) + .push(function () { + return jio.allDocs({ + query: "(_REVISION: = 0) OR (_REVISION: = 1)" + }); + }) + .push(function (results) { + equal(results.data.rows.length, 2); + deepEqual(results.data.rows[0].doc, { + "k2": "w0" + }, "Retrieves all documents with versions in queried revision range"); + deepEqual(results.data.rows[1].doc, { + "k": "v3" + }, "Retrieves all documents with versions in queried revision range"); + }) + .push(function () { + return jio.put("doc2", { + "k2": "w1" + }); + }) + .push(function () { + return jio.allDocs(); + }) + .push(function (results) { + equal(results.data.rows.length, 1, + "There is only one non-removed doc"); + deepEqual(results.data.rows[0].doc, { + "k2": "w1" + }); + }) + .push(function () { + return jio.remove("doc2"); + }) + .push(function () { + return jio.allDocs({ + query: + "_REVISION: 0 OR _REVISION: 1 OR " + + "(_REVISION: >= 2 AND _REVISION: <= 3)" + }); + }) + .push(function (results) { + equal(results.data.rows.length, 5); + deepEqual(results.data.rows[0].doc, { + "k2": "w1" + }); + deepEqual(results.data.rows[1].doc, { + "k2": "w0" + }); + deepEqual(results.data.rows[2].doc, { + "k": "v3" + }); + deepEqual(results.data.rows[3].doc, { + "k": "v2" + }); + deepEqual(results.data.rows[4].doc, { + "k": "v1" + }); + }) + .push(function () { + return jio.allDocs({ + query: "_REVISION: <= 3", + limit: [1, 4] + }); + }) + .push(function (results) { + equal(results.data.rows.length, 4, + "Correct number of results with optins.limit set"); + deepEqual(results.data.rows[0].doc, { + "k2": "w0" + }, "Correct results with options.limit set"); + deepEqual(results.data.rows[1].doc, { + "k": "v3" + }, "Correct results with options.limit set"); + deepEqual(results.data.rows[2].doc, { + "k": "v2" + }, "Correct results with options.limit set"); + deepEqual(results.data.rows[3].doc, { + "k": "v1" + }, "Correct results with options.limit set"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + +}(jIO, QUnit)); \ No newline at end of file -- 2.30.9 From 7b0cdad4ad5a1344c63bc39f5d4c9e5dd8ad7e5d Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Tue, 12 Jun 2018 14:52:01 +0000 Subject: [PATCH 23/46] Commented out tests which intentionally ffail on unimplemented features. --- test/jio.storage/historystorage.tests.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index d7d7e70..160e9bc 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -131,7 +131,7 @@ ///////////////////////////////////////////////////////////////// // historyStorage.querying_from_historystorage ///////////////////////////////////////////////////////////////// - +/** module("HistoryStorage.querying_from_historystorage"); test("verifying the correct results are returned from historyStorage.allDocs", function () { @@ -278,6 +278,7 @@ }) .always(function () {start(); }); }); +**/ ///////////////////////////////////////////////////////////////// // Accessing older revisions -- 2.30.9 From 5f5edb5f4992df9258c4687fd180607b75b4f46d Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Tue, 12 Jun 2018 15:26:32 +0000 Subject: [PATCH 24/46] Made changes to Gruntfile.js and tests.html to update bryanstorage to historystorage. --- Gruntfile.js | 3 +- src/jio.storage/bryanstorage.js | 311 ----------- test/jio.storage/bryanstorage.tests.js | 724 ------------------------- test/tests.html | 6 +- 4 files changed, 5 insertions(+), 1039 deletions(-) delete mode 100644 src/jio.storage/bryanstorage.js delete mode 100644 test/jio.storage/bryanstorage.tests.js diff --git a/Gruntfile.js b/Gruntfile.js index 7edb35e..4a2f398 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -183,7 +183,8 @@ module.exports = function (grunt) { 'src/jio.storage/cryptstorage.js', 'src/jio.storage/websqlstorage.js', 'src/jio.storage/fbstorage.js', - 'src/jio.storage/bryanstorage.js' + 'src/jio.storage/historystorage.js', + 'src/jio.storage/revisionstorage.js' ], dest: 'dist/<%= pkg.name %>-<%= pkg.version %>.js' // dest: 'jio.js' diff --git a/src/jio.storage/bryanstorage.js b/src/jio.storage/bryanstorage.js deleted file mode 100644 index 4f4d4fb..0000000 --- a/src/jio.storage/bryanstorage.js +++ /dev/null @@ -1,311 +0,0 @@ -/*jslint nomen: true*/ -/*global RSVP, jiodate*/ -(function (jIO) { - "use strict"; - - // Used to distinguish between operations done within the same millisecond - var unique_timestamp = function () { - - // XXX: replace this with UUIDStorage function call to S4() when it becomes - // publicly accessible - var uuid = ('0000' + Math.floor(Math.random() * 0x10000) - .toString(16)).slice(-4), - timestamp = Date.now().toString(); - return timestamp + "-" + uuid; - }; - - /** - * The jIO BryanStorage extension - * - * @class BryanStorage - * @constructor - */ - function BryanStorage(spec) { - - this._sub_storage = jIO.createJIO({ - type: "query", - sub_storage: spec.sub_storage - }); - } - - BryanStorage.prototype.get = function (id_in) { - - - // Query to get the last edit made to this document - var substorage = this._sub_storage, - options = { - query: "doc_id: " + id_in, - sort_on: [["timestamp", "descending"]], - limit: [0, 1] - }; - - return substorage.allDocs(options) - .push(function (results) { - if (results.data.rows.length > 0) { - return substorage.get(results.data.rows[0].id); - } - throw new jIO.util.jIOError( - "bryanstorage: cannot find object '" + id_in + "'", - 404 - ); - }) - .push(function (result) { - if (result.op === "put") { - return result.doc; - } - throw new jIO.util.jIOError( - "bryanstorage: cannot find object '" + id_in + "' (removed)", - 404 - ); - - // If no documents returned in first query, check if the id is encoding - // revision information - }, function () { - var steps, - steps_loc = id_in.lastIndexOf("_-"); - // If revision signature is not in id_in, than return 404, since id - // is not found - if (steps_loc === -1) { - throw new jIO.util.jIOError( - "bryanstorage: cannot find object '" + id_in + "'", - 404 - ); - } - - // If revision signature is found, query storage based on this - steps = Number(id_in.slice(steps_loc + 2)); - id_in = id_in.slice(0, steps_loc); - options = { - query: "doc_id: " + id_in, - sort_on: [["timestamp", "descending"]], - limit: [steps, 1] - }; - return substorage.allDocs(options) - .push(function (results) { - if (results.data.rows.length > 0) { - return substorage.get(results.data.rows[0].id); - } - throw new jIO.util.jIOError( - "bryanstorage: cannot find object '" + id_in + "'", - 404 - ); - }) - .push(function (result) { - if (result.op === "put") { - return result.doc; - } - throw new jIO.util.jIOError( - "bryanstorage: cannot find object '" + id_in + "' (removed)", - 404 - ); - }); - }); - }; - - BryanStorage.prototype.post = function (metadata) { - return this._sub_storage.post(metadata); - }; - - BryanStorage.prototype.put = function (id, data) { - var timestamp = unique_timestamp(), - metadata = { - // XXX: remove this attribute once query can sort_on id - timestamp: timestamp, - doc_id: id, - doc: data, - op: "put" - }; - return this._sub_storage.put(timestamp, metadata); - }; - - BryanStorage.prototype.remove = function (id) { - var timestamp = unique_timestamp(), - metadata = { - // XXX: remove this attribute once query can sort_on id - timestamp: timestamp, - doc_id: id, - op: "remove" - }; - return this._sub_storage.put(timestamp, metadata); - }; - - BryanStorage.prototype.allAttachments = function () { - return this._sub_storage.allAttachments.apply(this._sub_storage, arguments); - }; - BryanStorage.prototype.getAttachment = function () { - return this._sub_storage.getAttachment.apply(this._sub_storage, arguments); - }; - BryanStorage.prototype.putAttachment = function (id, name, data) { - - // Save pointer to substorage for use in nested function - var substorage = this._sub_storage; - - // First, get document metadata to update "_revision" - return this.get(id, name) - - // Increment "_revision" parameter in document - .push(function (metadata) { - var new_metadata = metadata; - - // "_revision" is guaranteed to exist since the document already exists - new_metadata._revision = metadata._revision + 1; - return substorage.put(id, new_metadata); - }) - - // After metadata updates successfully, perform putAttachment - .push(function () { - return substorage.putAttachment(id, name, data); - }); - }; - - BryanStorage.prototype.removeAttachment = function (id, name) { - - // Save pointer to substorage for use in nested function - var substorage = this._sub_storage; - - // First, get document metadata to update "_revision" - return this.get(id, name) - - // Increment "_revision" parameter in document - .push(function (metadata) { - var new_metadata = metadata; - - // "_revision" is guaranteed to exist since the document already exists - new_metadata._revision = metadata._revision + 1; - return substorage.put(id, new_metadata); - }) - - // After metadata updates successfully, perform removeAttachment - .push(function () { - return substorage.removeAttachment(id, name); - }); - }; - - BryanStorage.prototype.repair = function () { - return this._sub_storage.repair.apply(this._sub_storage, arguments); - }; - BryanStorage.prototype.hasCapacity = function () { - return this._sub_storage.hasCapacity.apply(this._sub_storage, arguments); - }; - - BryanStorage.prototype.buildQuery = function (options) { - - if (options === undefined) { - options = {}; - } - if (options.query === undefined) { - options.query = ""; - } - options.query = jIO.QueryFactory.create(options.query); - var meta_options = { - // XXX: I don't believe it's currently possible to query on - // sub-attributes so for now, we just use the inputted query, which - // obviously will fail - query: "", - - // XXX: same here, we cannot sort correctly because we cannot access - // attributes of doc - sort_on: [["timestamp", "descending"]] - }, - substorage = this._sub_storage, - - // Check if query involved _REVISION. If not, we will later place a - // (*) AND (_REVISION: =0) as the default handling of revisions - rev_query = false, - query_obj = options.query, - query_stack = [], - ind; - - if (query_obj.hasOwnProperty("query_list")) { - query_stack.push(query_obj); - } else { - rev_query = (query_obj.key === "_REVISION"); - } - while (query_stack.length > 0 && (!rev_query)) { - query_obj = query_stack.pop(); - for (ind = 0; ind < query_obj.query_list.length; ind += 1) { - if (query_obj.query_list[ind].hasOwnProperty("query_list")) { - query_stack.push(query_obj.query_list[ind]); - } else if (query_obj.query_list[ind].key === "_REVISION") { - rev_query = true; - break; - } - } - } - - return this._sub_storage.allDocs(meta_options) - - // Get all documents found in query - .push(function (results) { - var promises = results.data.rows.map(function (data) { - return substorage.get(data.id); - }); - return RSVP.all(promises); - }) - - .push(function (results) { - // Label all documents with their current revision status - var doc, - revision_tracker = {}, - promises; - for (ind = 0; ind < results.length; ind += 1) { - doc = results[ind]; - if (revision_tracker.hasOwnProperty(doc.doc_id)) { - revision_tracker[doc.doc_id] += 1; - } else { - revision_tracker[doc.doc_id] = 0; - } - doc._REVISION = revision_tracker[doc.doc_id]; - } - - // There must be a faster way - promises = results.map(function (data) { - return substorage.put(data.timestamp, data); - }); - return RSVP.all(promises); - }) - .push(function () { - var latest_rev_query; - latest_rev_query = jIO.QueryFactory.create( - "(_REVISION: >= 0) AND (NOT op: remove)" - ); - if (rev_query) { - latest_rev_query.query_list[0] = options.query; - } else { - latest_rev_query.query_list[0] = jIO.QueryFactory.create( - "(_REVISION: =0)" - ); - if (options.query.type === "simple" || - options.query.type === "complex") { - latest_rev_query.query_list.push(options.query); - } - } - // Build a query for final push - options.query = latest_rev_query; - if (options.sort_on === undefined) { - options.sort_on = []; - } - options.sort_on.push(["timestamp", "descending"]); - return substorage.allDocs(options); - }) - .push(function (results) { - var promises = results.data.rows.map(function (data) { - return substorage.get(data.id); - }); - return RSVP.all(promises); - }) - .push(function (results) { - return results - .map(function (current_doc) { - return { - doc: current_doc.doc, - value: {}, - id: current_doc.doc_id - }; - }); - }); - }; - - jIO.addStorage('bryan', BryanStorage); - -}(jIO)); \ No newline at end of file diff --git a/test/jio.storage/bryanstorage.tests.js b/test/jio.storage/bryanstorage.tests.js deleted file mode 100644 index a6edfbc..0000000 --- a/test/jio.storage/bryanstorage.tests.js +++ /dev/null @@ -1,724 +0,0 @@ -/*jslint nomen: true*/ -/*global Blob, jiodate*/ -(function (jIO, QUnit) { - "use strict"; - var test = QUnit.test, - stop = QUnit.stop, - start = QUnit.start, - ok = QUnit.ok, - expect = QUnit.expect, - deepEqual = QUnit.deepEqual, - equal = QUnit.equal, - module = QUnit.module; - - - ///////////////////////////////////////////////////////////////// - // _revision parameter updating with RSVP all - ///////////////////////////////////////////////////////////////// - - module("bryanStorage.revision_with_RSVP_all"); - test("verifying updates correctly when puts are done in parallel", - function () { - stop(); - expect(7); - - // create storage of type "bryan" with memory as substorage - var dbname = "rsvp_db_" + Date.now(), - jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "uuid", - sub_storage: { - //type: "memory" - type: "indexeddb", - database: dbname - } - } - }), - not_bryan = jIO.createJIO({ - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - //type: "memory" - type: "indexeddb", - database: dbname - } - } - }); - - jio.put("bar", {"title": "foo0"}) - .push(function () { - return RSVP.all([ - jio.put("bar", {"title": "foo1"}), - jio.put("bar", {"title": "foo2"}), - jio.put("bar", {"title": "foo3"}), - jio.put("bar", {"title": "foo4"}), - jio.put("barbar", {"title": "attr0"}), - jio.put("barbar", {"title": "attr1"}), - jio.put("barbar", {"title": "attr2"}), - jio.put("barbar", {"title": "attr3"}) - ]); - }) - .push(function () {return jio.get("bar"); }) - .push(function (result) { - ok(result.title !== "foo0", "Title should have changed from foo0"); - }) - .push(function () { - return not_bryan.allDocs({ - query: "", - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - equal(results.data.rows.length, - 9, - "All nine versions exist in storage"); - return not_bryan.get(results.data.rows[0].id); - }) - .push(function (results) { - deepEqual(results, { - doc_id: "bar", - doc: { - title: "foo0" - }, - timestamp: results.timestamp, - op: "put" - }, "The first item in the log is pushing bar's title to 'foo0'"); - return jio.remove("bar"); - }) - .push(function () { - return jio.get("bar"); - }) - .push(function () { - return jio.get("barbar"); - }, function (error) { - deepEqual( - error.message, - "bryanstorage: cannot find object 'bar' (removed)", - "Appropriate error is sent explaining object has been removed" - ); - return jio.get("barbar"); - }) - .push(function (result) { - ok(result.title !== undefined, "barbar exists and has proper form"); - return not_bryan.allDocs({ - query: "", - sort_on: [["op", "descending"]] - }); - }) - .push(function (results) { - equal(results.data.rows.length, - 10, - "Remove operation is recorded"); - return not_bryan.get(results.data.rows[0].id); - }) - .push(function (result) { - deepEqual(result, { - doc_id: "bar", - timestamp: result.timestamp, - op: "remove" - }); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - - ///////////////////////////////////////////////////////////////// - // bryanStorage.querying_from_bryanstorage - ///////////////////////////////////////////////////////////////// - - module("bryanStorage.querying_from_bryanstorage"); - test("verifying the correct results are returned from bryanStorage.allDocs", - function () { - stop(); - expect(10); - - // create storage of type "bryan" with memory as substorage - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "uuid", - sub_storage: { - type: "memory" - } - } - }); - jio.put("bar", {"title": "foo0"}) - .push(function () { - return RSVP.all([ - jio.remove("bar"), - jio.put("bar", {"title": "foo1"}), - jio.put("bar", {"title": "foo2"}), - jio.put("bar", {"title": "foo3"}), - jio.put("barbar", {"title": "attr0"}), - jio.put("barbar", {"title": "attr1"}), - jio.put("barbar", {"title": "attr2"}), - jio.put("barbarbar", {"title": "val0"}), - jio.put("barbarbarbar", {"title": "prop0"}) - ]); - }) - // Make two final puts so we know what to expect as the current state of - // each document. - .push(function () { - return jio.put("barbar", {"title": "attr3"}); - }) - .push(function () { - return jio.put("bar", {"title": "foo4"}); - }) - - // Queries should only include information about the final two versions - .push(function () { - return jio.allDocs({ - query: "", - sort_on: [["title", "ascending"]] - }); - }) - .push(function (results) { - equal(results.data.rows.length, - 4, - "Empty query yields four results since there are four unique docs"); - return jio.get(results.data.rows[0].id); - }, - function (error) { - return ok(false, "Query failed: " + error); - }) - .push(function (result) { - deepEqual(result, { - title: "attr3" - }, - "NOT IMPLEMENTED: Retrieve correct sort order with no metadata"); - }, - function () { - return ok(false, "Couldn't find document in storage"); - }) - - // Querying with a limit - .push(function () { - return jio.allDocs({ - query: "", - sort_on: [["title", "ascending"]], - limit: [0, 1] - }); - }) - .push(function (results) { - equal(results.data.rows.length, - 1, - "Since limit [0,1] was supplied, only 1st document is returned"); - return jio.get(results.data.rows[0].id); - }) - .push(function (result) { - deepEqual(result, { - title: "attr3" - }, - "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); - }) - - // Querying with a more complicated limit - .push(function () { - return jio.allDocs({ - query: "", - sort_on: [["title", "ascending"]], - limit: [2, 2] - }); - }) - .push(function (results) { - equal(results.data.rows.length, - 2, - "Retrieving the correct documents when options.limit is specified"); - - deepEqual(results.data.rows[0].id, - "barbarbarbar", - "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); - - deepEqual(results.data.rows[1].id, - "barbarbar", - "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); - - return jio.get(results.data.rows[0].id); - }) - .push(function (result) { - deepEqual(result, { - title: "property0" - }, - "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); - }) - - // Querying for a specific id - .push(function () { - return jio.allDocs({ - query: "id: bar" - }); - }) - .push(function (results) { - equal(results.data.rows.length, - 1, - "NOT IMPLEMENTED: query involving specific document attributes"); - return jio.get(results.data.rows[0].id); - }) - .push(function (result) { - deepEqual(result, { - title: "foo4" - }, - "NOT IMPLEMENTED: query involving specific document attributes"); - }, - function () { - ok(false, - "NOT IMPLEMENTED: query involving specific document attributes" - ); - }) - - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - ///////////////////////////////////////////////////////////////// - // Accessing older revisions - ///////////////////////////////////////////////////////////////// - - module("bryanStorage.accessing_older_revisions"); - test("Testing proper retrieval of older revisions of documents", - function () { - stop(); - expect(18); - - // create storage of type "bryan" with memory as substorage - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "uuid", - sub_storage: { - type: "memory" - } - } - }); - jio.get("doc") - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "Document does not exist yet."); - }) - .push(function () { - return jio.put("doc", { - "k0": "v0" - }); - }) - .push(function () { - return jio.get("doc_-0"); - }) - .push(function (result) { - deepEqual(result, { - "k0": "v0" - }); - }) - .push(function () { - return jio.put("doc", {"k1": "v1"}); - }) - .push(function () { - return jio.get("doc_-0"); - }) - .push(function (result) { - deepEqual(result, { - "k1": "v1" - }); - }) - .push(function () { - return jio.get("doc_-1"); - }) - .push(function (result) { - deepEqual(result, { - "k0": "v0" - }); - }) - .push(function () { - return jio.put("doc", {"k2": "v2"}); - }) - .push(function () { - return jio.remove("doc"); - }) - .push(function () { - return jio.put("doc", {"k3": "v3"}); - }) - .push(function () { - return jio.put("doc", {"k4": "v4"}); - }) - .push(function () { - return jio.get("doc"); - }) - .push(function (result) { - deepEqual(result, - {"k4": "v4"}, - "By default, .get returns latest revision"); - return jio.get("doc"); - }) - .push(function (result) { - deepEqual(result, - {"k4": "v4"}, - ".get returns latest revision with second input = 0"); - return jio.get("doc_-1"); - }) - .push(function (result) { - deepEqual(result, - {"k3": "v3"}, - "Walk back one revision with second input = 1"); - return jio.get("doc_-2"); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "Current state of document is 'removed'."); - return jio.get("doc_-3"); - }) - .push(function (result) { - deepEqual(result, - {"k2": "v2"}, - "Walk back three revisions with second input = 3"); - return jio.get("doc_-4"); - }) - .push(function (result) { - deepEqual(result, - {"k1": "v1"}, - "Walk back four revisions with second input = 4"); - return jio.get("doc_-5"); - }) - .push(function (result) { - deepEqual(result, - {"k0": "v0"}, - "Walk back five revisions with second input = 5"); - return jio.get("doc_-6"); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "There are only 5 previous states of this document"); - }) - - // Adding documents with problematic doc_id's - .push(function () { - return jio.put("doc_-name", { - "key": "val0" - }); - }) - .push(function () { - return jio.put("document_-0", { - "key": "and val0" - }); - }) - .push(function () { - return jio.put("doc_-name", { - "key": "val1" - }); - }) - - .push(function () { - return jio.get("doc_-name"); - }) - .push(function (result) { - deepEqual(result, { - "key": "val1" - }); - return jio.get("doc_-name_-0"); - }) - .push(function (result) { - deepEqual(result, { - "key": "val1" - }); - return jio.get("doc_-name_-1"); - }) - .push(function (result) { - deepEqual(result, { - "key": "val0" - }); - return jio.get("document_-0"); - }) - .push(function (result) { - deepEqual(result, { - "key": "and val0" - }); - return jio.get("document_-0_-0"); - }) - .push(function (result) { - deepEqual(result, { - "key": "and val0" - }); - return jio.get("document_-0_-1"); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "Document does not have this many revisions."); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - ///////////////////////////////////////////////////////////////// - // Querying older revisions - ///////////////////////////////////////////////////////////////// - - module("bryanStorage.querying_old_revisions"); - test("Testing retrieval of older revisions via allDocs calls", - function () { - stop(); - expect(37); - - // create storage of type "bryan" with memory as substorage - var jio = jIO.createJIO({ - type: "bryan", - sub_storage: { - type: "uuid", - sub_storage: { - type: "memory" - } - } - }); - jio.put("doc", { - "k": "v0" - }) - .push(function () { - return jio.put("doc", { - "k": "v1" - }); - }) - .push(function () { - return jio.put("doc", { - "k": "v2" - }); - }) - .push(function () { - return jio.put("doc", { - "k": "v3" - }); - }) - .push(function () { - return jio.allDocs({ - query: "_REVISION : 0" - }); - }) - .push(function (results) { - deepEqual(results.data.rows.length, - 1, - "Only one query returned with options.revision_limit == [0,1]"); - return jio.get(results.data.rows[0].id); - }) - .push(function (result) { - deepEqual(result, { - "k": "v3" - }); - }) - .push(function () { - return jio.allDocs({ - query: "_REVISION : =1" - }); - }) - .push(function (results) { - deepEqual(results.data.rows.length, - 1, - "Only one query returned with options.revision_limit == [1,1]"); - deepEqual(results.data.rows[0].doc, { - "k": "v2" - }); - return jio.allDocs({ - query: "_REVISION : =2" - }); - }) - .push(function (results) { - deepEqual(results.data.rows.length, - 1, - "Only one query returned with options.revision_limit == [2,1]"); - deepEqual(results.data.rows[0].doc, { - "k": "v1" - }); - return jio.allDocs({ - query: "_REVISION : =3" - }); - }) - .push(function (results) { - deepEqual(results.data.rows.length, - 1, - "Only one query returned with options.revision_limit == [3,1]"); - deepEqual(results.data.rows[0].doc, { - "k": "v0" - }); - return jio.allDocs({ - query: "_REVISION : =4" - }); - }) - .push(function (results) { - equal(results.data.rows.length, 0, "Past all previous revisions"); - }) - .push(function () { - return jio.allDocs({ - query: "_REVISION: <= 1" - }); - }) - .push(function (results) { - equal(results.data.rows.length, 2); - deepEqual(results.data.rows[0].doc, { - "k": "v3" - }, "Only retrieve two most recent revions"); - deepEqual(results.data.rows[1].doc, { - "k": "v2" - }); - }) - .push(function () { - return jio.remove("doc"); - }) - .push(function () { - return jio.allDocs({ - query: "NOT (_REVISION: >= 1)", - revision_limit: [0, 1] - }); - }) - .push(function (results) { - equal(results.data.rows.length, 0, - "Query does not return removed doc"); - }) - .push(function () { - return jio.allDocs({ - query: "(_REVISION: >= 1) AND (_REVISION: <= 3)" - }); - }) - .push(function (results) { - equal(results.data.rows.length, 3); - deepEqual(results.data.rows[0].doc, { - "k": "v3" - }, "1st, 2nd, and 3rd versions removed from current are retrieved"); - deepEqual(results.data.rows[1].doc, { - "k": "v2" - }, "1st, 2nd, and 3rd versions removed from current are retrieved"); - deepEqual(results.data.rows[2].doc, { - "k": "v1" - }, "1st, 2nd, and 3rd versions removed from current are retrieved"); - }) - .push(function () { - return jio.put("doc2", { - "k2": "w0" - }); - }) - .push(function () { - return jio.allDocs({ - query: "(_REVISION: >0) AND (_REVISION: <= 3)" - }); - }) - .push(function (results) { - equal(results.data.rows.length, 3); - deepEqual(results.data.rows[0].doc, { - "k": "v3" - }, "Does not retrieve new document outside queried revision range"); - deepEqual(results.data.rows[1].doc, { - "k": "v2" - }, "Does not retrieve new document outside queried revision range"); - deepEqual(results.data.rows[2].doc, { - "k": "v1" - }, "Does not retrieve new document outside queried revision range"); - }) - .push(function () { - return jio.allDocs({ - query: "(_REVISION: = 0) OR (_REVISION: = 1)" - }); - }) - .push(function (results) { - equal(results.data.rows.length, 2); - deepEqual(results.data.rows[0].doc, { - "k2": "w0" - }, "Retrieves all documents with versions in queried revision range"); - deepEqual(results.data.rows[1].doc, { - "k": "v3" - }, "Retrieves all documents with versions in queried revision range"); - }) - .push(function () { - return jio.put("doc2", { - "k2": "w1" - }); - }) - .push(function () { - return jio.allDocs(); - }) - .push(function (results) { - equal(results.data.rows.length, 1, - "There is only one non-removed doc"); - deepEqual(results.data.rows[0].doc, { - "k2": "w1" - }); - }) - .push(function () { - return jio.remove("doc2"); - }) - .push(function () { - return jio.allDocs({ - query: - "_REVISION: 0 OR _REVISION: 1 OR " + - "(_REVISION: >= 2 AND _REVISION: <= 3)" - }); - }) - .push(function (results) { - equal(results.data.rows.length, 5); - deepEqual(results.data.rows[0].doc, { - "k2": "w1" - }); - deepEqual(results.data.rows[1].doc, { - "k2": "w0" - }); - deepEqual(results.data.rows[2].doc, { - "k": "v3" - }); - deepEqual(results.data.rows[3].doc, { - "k": "v2" - }); - deepEqual(results.data.rows[4].doc, { - "k": "v1" - }); - }) - .push(function () { - return jio.allDocs({ - query: "_REVISION: <= 3", - limit: [1, 4] - }); - }) - .push(function (results) { - equal(results.data.rows.length, 4, - "Correct number of results with optins.limit set"); - deepEqual(results.data.rows[0].doc, { - "k2": "w0" - }, "Correct results with options.limit set"); - deepEqual(results.data.rows[1].doc, { - "k": "v3" - }, "Correct results with options.limit set"); - deepEqual(results.data.rows[2].doc, { - "k": "v2" - }, "Correct results with options.limit set"); - deepEqual(results.data.rows[3].doc, { - "k": "v1" - }, "Correct results with options.limit set"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - -}(jIO, QUnit)); \ No newline at end of file diff --git a/test/tests.html b/test/tests.html index 00ba5db..c0390d3 100644 --- a/test/tests.html +++ b/test/tests.html @@ -60,15 +60,15 @@ + + -// 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset -// +0200, so we adjust the time as needed, to be valid. -// -// Keeping the time actually adds/subtracts (one hour) -// from the actual represented time. That is why we call updateOffset -// a second time. In case it wants us to change the offset again -// _changeInProgress == true case, then we have to adjust, because -// there is no such time in the given timezone. -function getSetOffset (input, keepLocalTime, keepMinutes) { - var offset = this._offset || 0, - localAdjust; - if (!this.isValid()) { - return input != null ? this : NaN; - } - if (input != null) { - if (typeof input === 'string') { - input = offsetFromString(matchShortOffset, input); - if (input === null) { - return this; - } - } else if (Math.abs(input) < 16 && !keepMinutes) { - input = input * 60; - } - if (!this._isUTC && keepLocalTime) { - localAdjust = getDateOffset(this); - } - this._offset = input; - this._isUTC = true; - if (localAdjust != null) { - this.add(localAdjust, 'm'); - } - if (offset !== input) { - if (!keepLocalTime || this._changeInProgress) { - addSubtract(this, createDuration(input - offset, 'm'), 1, false); - } else if (!this._changeInProgress) { - this._changeInProgress = true; - hooks.updateOffset(this, true); - this._changeInProgress = null; - } - } - return this; - } else { - return this._isUTC ? offset : getDateOffset(this); - } -} - -function getSetZone (input, keepLocalTime) { - if (input != null) { - if (typeof input !== 'string') { - input = -input; - } - - this.utcOffset(input, keepLocalTime); - - return this; - } else { - return -this.utcOffset(); - } -} - -function setOffsetToUTC (keepLocalTime) { - return this.utcOffset(0, keepLocalTime); -} - -function setOffsetToLocal (keepLocalTime) { - if (this._isUTC) { - this.utcOffset(0, keepLocalTime); - this._isUTC = false; - - if (keepLocalTime) { - this.subtract(getDateOffset(this), 'm'); - } - } - return this; -} - -function setOffsetToParsedOffset () { - if (this._tzm != null) { - this.utcOffset(this._tzm, false, true); - } else if (typeof this._i === 'string') { - var tZone = offsetFromString(matchOffset, this._i); - if (tZone != null) { - this.utcOffset(tZone); - } - else { - this.utcOffset(0, true); - } - } - return this; -} - -function hasAlignedHourOffset (input) { - if (!this.isValid()) { - return false; - } - input = input ? createLocal(input).utcOffset() : 0; - - return (this.utcOffset() - input) % 60 === 0; -} - -function isDaylightSavingTime () { - return ( - this.utcOffset() > this.clone().month(0).utcOffset() || - this.utcOffset() > this.clone().month(5).utcOffset() - ); -} - -function isDaylightSavingTimeShifted () { - if (!isUndefined(this._isDSTShifted)) { - return this._isDSTShifted; - } - - var c = {}; - - copyConfig(c, this); - c = prepareConfig(c); - - if (c._a) { - var other = c._isUTC ? createUTC(c._a) : createLocal(c._a); - this._isDSTShifted = this.isValid() && - compareArrays(c._a, other.toArray()) > 0; - } else { - this._isDSTShifted = false; - } - - return this._isDSTShifted; -} - -function isLocal () { - return this.isValid() ? !this._isUTC : false; -} - -function isUtcOffset () { - return this.isValid() ? this._isUTC : false; -} - -function isUtc () { - return this.isValid() ? this._isUTC && this._offset === 0 : false; -} - -// ASP.NET json date format regex -var aspNetRegex = /^(\-|\+)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/; - -// from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html -// somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere -// and further modified to allow for strings containing both week and day -var isoRegex = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/; - -function createDuration (input, key) { - var duration = input, - // matching against regexp is expensive, do it on demand - match = null, - sign, - ret, - diffRes; - - if (isDuration(input)) { - duration = { - ms : input._milliseconds, - d : input._days, - M : input._months - }; - } else if (isNumber(input)) { - duration = {}; - if (key) { - duration[key] = input; - } else { - duration.milliseconds = input; - } - } else if (!!(match = aspNetRegex.exec(input))) { - sign = (match[1] === '-') ? -1 : 1; - duration = { - y : 0, - d : toInt(match[DATE]) * sign, - h : toInt(match[HOUR]) * sign, - m : toInt(match[MINUTE]) * sign, - s : toInt(match[SECOND]) * sign, - ms : toInt(absRound(match[MILLISECOND] * 1000)) * sign // the millisecond decimal point is included in the match - }; - } else if (!!(match = isoRegex.exec(input))) { - sign = (match[1] === '-') ? -1 : (match[1] === '+') ? 1 : 1; - duration = { - y : parseIso(match[2], sign), - M : parseIso(match[3], sign), - w : parseIso(match[4], sign), - d : parseIso(match[5], sign), - h : parseIso(match[6], sign), - m : parseIso(match[7], sign), - s : parseIso(match[8], sign) - }; - } else if (duration == null) {// checks for null or undefined - duration = {}; - } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) { - diffRes = momentsDifference(createLocal(duration.from), createLocal(duration.to)); - - duration = {}; - duration.ms = diffRes.milliseconds; - duration.M = diffRes.months; - } - - ret = new Duration(duration); - - if (isDuration(input) && hasOwnProp(input, '_locale')) { - ret._locale = input._locale; - } - - return ret; -} - -createDuration.fn = Duration.prototype; -createDuration.invalid = createInvalid$1; - -function parseIso (inp, sign) { - // We'd normally use ~~inp for this, but unfortunately it also - // converts floats to ints. - // inp may be undefined, so careful calling replace on it. - var res = inp && parseFloat(inp.replace(',', '.')); - // apply sign while we're at it - return (isNaN(res) ? 0 : res) * sign; -} - -function positiveMomentsDifference(base, other) { - var res = {milliseconds: 0, months: 0}; - - res.months = other.month() - base.month() + - (other.year() - base.year()) * 12; - if (base.clone().add(res.months, 'M').isAfter(other)) { - --res.months; - } - - res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - - return res; -} - -function momentsDifference(base, other) { - var res; - if (!(base.isValid() && other.isValid())) { - return {milliseconds: 0, months: 0}; - } - - other = cloneWithOffset(other, base); - if (base.isBefore(other)) { - res = positiveMomentsDifference(base, other); - } else { - res = positiveMomentsDifference(other, base); - res.milliseconds = -res.milliseconds; - res.months = -res.months; - } - - return res; -} - -// TODO: remove 'name' arg after deprecation is removed -function createAdder(direction, name) { - return function (val, period) { - var dur, tmp; - //invert the arguments, but complain about it - if (period !== null && !isNaN(+period)) { - deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period). ' + - 'See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info.'); - tmp = val; val = period; period = tmp; - } - - val = typeof val === 'string' ? +val : val; - dur = createDuration(val, period); - addSubtract(this, dur, direction); - return this; - }; -} - -function addSubtract (mom, duration, isAdding, updateOffset) { - var milliseconds = duration._milliseconds, - days = absRound(duration._days), - months = absRound(duration._months); - - if (!mom.isValid()) { - // No op - return; - } - - updateOffset = updateOffset == null ? true : updateOffset; - - if (months) { - setMonth(mom, get(mom, 'Month') + months * isAdding); - } - if (days) { - set$1(mom, 'Date', get(mom, 'Date') + days * isAdding); - } - if (milliseconds) { - mom._d.setTime(mom._d.valueOf() + milliseconds * isAdding); - } - if (updateOffset) { - hooks.updateOffset(mom, days || months); - } -} - -var add = createAdder(1, 'add'); -var subtract = createAdder(-1, 'subtract'); - -function getCalendarFormat(myMoment, now) { - var diff = myMoment.diff(now, 'days', true); - return diff < -6 ? 'sameElse' : - diff < -1 ? 'lastWeek' : - diff < 0 ? 'lastDay' : - diff < 1 ? 'sameDay' : - diff < 2 ? 'nextDay' : - diff < 7 ? 'nextWeek' : 'sameElse'; -} - -function calendar$1 (time, formats) { - // We want to compare the start of today, vs this. - // Getting start-of-today depends on whether we're local/utc/offset or not. - var now = time || createLocal(), - sod = cloneWithOffset(now, this).startOf('day'), - format = hooks.calendarFormat(this, sod) || 'sameElse'; - - var output = formats && (isFunction(formats[format]) ? formats[format].call(this, now) : formats[format]); - - return this.format(output || this.localeData().calendar(format, this, createLocal(now))); -} - -function clone () { - return new Moment(this); -} - -function isAfter (input, units) { - var localInput = isMoment(input) ? input : createLocal(input); - if (!(this.isValid() && localInput.isValid())) { - return false; - } - units = normalizeUnits(!isUndefined(units) ? units : 'millisecond'); - if (units === 'millisecond') { - return this.valueOf() > localInput.valueOf(); - } else { - return localInput.valueOf() < this.clone().startOf(units).valueOf(); - } -} - -function isBefore (input, units) { - var localInput = isMoment(input) ? input : createLocal(input); - if (!(this.isValid() && localInput.isValid())) { - return false; - } - units = normalizeUnits(!isUndefined(units) ? units : 'millisecond'); - if (units === 'millisecond') { - return this.valueOf() < localInput.valueOf(); - } else { - return this.clone().endOf(units).valueOf() < localInput.valueOf(); - } -} - -function isBetween (from, to, units, inclusivity) { - inclusivity = inclusivity || '()'; - return (inclusivity[0] === '(' ? this.isAfter(from, units) : !this.isBefore(from, units)) && - (inclusivity[1] === ')' ? this.isBefore(to, units) : !this.isAfter(to, units)); -} - -function isSame (input, units) { - var localInput = isMoment(input) ? input : createLocal(input), - inputMs; - if (!(this.isValid() && localInput.isValid())) { - return false; - } - units = normalizeUnits(units || 'millisecond'); - if (units === 'millisecond') { - return this.valueOf() === localInput.valueOf(); - } else { - inputMs = localInput.valueOf(); - return this.clone().startOf(units).valueOf() <= inputMs && inputMs <= this.clone().endOf(units).valueOf(); - } -} - -function isSameOrAfter (input, units) { - return this.isSame(input, units) || this.isAfter(input,units); -} - -function isSameOrBefore (input, units) { - return this.isSame(input, units) || this.isBefore(input,units); -} - -function diff (input, units, asFloat) { - var that, - zoneDelta, - output; - - if (!this.isValid()) { - return NaN; - } - - that = cloneWithOffset(input, this); - - if (!that.isValid()) { - return NaN; - } - - zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4; - - units = normalizeUnits(units); - - switch (units) { - case 'year': output = monthDiff(this, that) / 12; break; - case 'month': output = monthDiff(this, that); break; - case 'quarter': output = monthDiff(this, that) / 3; break; - case 'second': output = (this - that) / 1e3; break; // 1000 - case 'minute': output = (this - that) / 6e4; break; // 1000 * 60 - case 'hour': output = (this - that) / 36e5; break; // 1000 * 60 * 60 - case 'day': output = (this - that - zoneDelta) / 864e5; break; // 1000 * 60 * 60 * 24, negate dst - case 'week': output = (this - that - zoneDelta) / 6048e5; break; // 1000 * 60 * 60 * 24 * 7, negate dst - default: output = this - that; - } - - return asFloat ? output : absFloor(output); -} - -function monthDiff (a, b) { - // difference in months - var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), - // b is in (anchor - 1 month, anchor + 1 month) - anchor = a.clone().add(wholeMonthDiff, 'months'), - anchor2, adjust; - - if (b - anchor < 0) { - anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); - // linear across the month - adjust = (b - anchor) / (anchor - anchor2); - } else { - anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); - // linear across the month - adjust = (b - anchor) / (anchor2 - anchor); - } - - //check for negative zero, return zero if negative zero - return -(wholeMonthDiff + adjust) || 0; -} - -hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; -hooks.defaultFormatUtc = 'YYYY-MM-DDTHH:mm:ss[Z]'; - -function toString () { - return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); -} - -function toISOString(keepOffset) { - if (!this.isValid()) { - return null; - } - var utc = keepOffset !== true; - var m = utc ? this.clone().utc() : this; - if (m.year() < 0 || m.year() > 9999) { - return formatMoment(m, utc ? 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]' : 'YYYYYY-MM-DD[T]HH:mm:ss.SSSZ'); - } - if (isFunction(Date.prototype.toISOString)) { - // native implementation is ~50x faster, use it when we can - if (utc) { - return this.toDate().toISOString(); - } else { - return new Date(this.valueOf() + this.utcOffset() * 60 * 1000).toISOString().replace('Z', formatMoment(m, 'Z')); - } - } - return formatMoment(m, utc ? 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]' : 'YYYY-MM-DD[T]HH:mm:ss.SSSZ'); -} - -/** - * Return a human readable representation of a moment that can - * also be evaluated to get a new moment which is the same - * - * @link https://nodejs.org/dist/latest/docs/api/util.html#util_custom_inspect_function_on_objects - */ -function inspect () { - if (!this.isValid()) { - return 'moment.invalid(/* ' + this._i + ' */)'; - } - var func = 'moment'; - var zone = ''; - if (!this.isLocal()) { - func = this.utcOffset() === 0 ? 'moment.utc' : 'moment.parseZone'; - zone = 'Z'; - } - var prefix = '[' + func + '("]'; - var year = (0 <= this.year() && this.year() <= 9999) ? 'YYYY' : 'YYYYYY'; - var datetime = '-MM-DD[T]HH:mm:ss.SSS'; - var suffix = zone + '[")]'; - - return this.format(prefix + year + datetime + suffix); -} - -function format (inputString) { - if (!inputString) { - inputString = this.isUtc() ? hooks.defaultFormatUtc : hooks.defaultFormat; - } - var output = formatMoment(this, inputString); - return this.localeData().postformat(output); -} - -function from (time, withoutSuffix) { - if (this.isValid() && - ((isMoment(time) && time.isValid()) || - createLocal(time).isValid())) { - return createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); - } else { - return this.localeData().invalidDate(); - } -} - -function fromNow (withoutSuffix) { - return this.from(createLocal(), withoutSuffix); -} - -function to (time, withoutSuffix) { - if (this.isValid() && - ((isMoment(time) && time.isValid()) || - createLocal(time).isValid())) { - return createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix); - } else { - return this.localeData().invalidDate(); - } -} - -function toNow (withoutSuffix) { - return this.to(createLocal(), withoutSuffix); -} - -// If passed a locale key, it will set the locale for this -// instance. Otherwise, it will return the locale configuration -// variables for this instance. -function locale (key) { - var newLocaleData; - - if (key === undefined) { - return this._locale._abbr; - } else { - newLocaleData = getLocale(key); - if (newLocaleData != null) { - this._locale = newLocaleData; - } - return this; - } -} - -var lang = deprecate( - 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', - function (key) { - if (key === undefined) { - return this.localeData(); - } else { - return this.locale(key); - } - } -); - -function localeData () { - return this._locale; -} - -function startOf (units) { - units = normalizeUnits(units); - // the following switch intentionally omits break keywords - // to utilize falling through the cases. - switch (units) { - case 'year': - this.month(0); - /* falls through */ - case 'quarter': - case 'month': - this.date(1); - /* falls through */ - case 'week': - case 'isoWeek': - case 'day': - case 'date': - this.hours(0); - /* falls through */ - case 'hour': - this.minutes(0); - /* falls through */ - case 'minute': - this.seconds(0); - /* falls through */ - case 'second': - this.milliseconds(0); - } - - // weeks are a special case - if (units === 'week') { - this.weekday(0); - } - if (units === 'isoWeek') { - this.isoWeekday(1); - } - - // quarters are also special - if (units === 'quarter') { - this.month(Math.floor(this.month() / 3) * 3); - } - - return this; -} - -function endOf (units) { - units = normalizeUnits(units); - if (units === undefined || units === 'millisecond') { - return this; - } - - // 'date' is an alias for 'day', so it should be considered as such. - if (units === 'date') { - units = 'day'; - } - - return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); -} - -function valueOf () { - return this._d.valueOf() - ((this._offset || 0) * 60000); -} - -function unix () { - return Math.floor(this.valueOf() / 1000); -} - -function toDate () { - return new Date(this.valueOf()); -} - -function toArray () { - var m = this; - return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; -} - -function toObject () { - var m = this; - return { - years: m.year(), - months: m.month(), - date: m.date(), - hours: m.hours(), - minutes: m.minutes(), - seconds: m.seconds(), - milliseconds: m.milliseconds() - }; -} - -function toJSON () { - // new Date(NaN).toJSON() === null - return this.isValid() ? this.toISOString() : null; -} - -function isValid$2 () { - return isValid(this); -} - -function parsingFlags () { - return extend({}, getParsingFlags(this)); -} - -function invalidAt () { - return getParsingFlags(this).overflow; -} - -function creationData() { - return { - input: this._i, - format: this._f, - locale: this._locale, - isUTC: this._isUTC, - strict: this._strict - }; -} - -// FORMATTING - -addFormatToken(0, ['gg', 2], 0, function () { - return this.weekYear() % 100; -}); - -addFormatToken(0, ['GG', 2], 0, function () { - return this.isoWeekYear() % 100; -}); - -function addWeekYearFormatToken (token, getter) { - addFormatToken(0, [token, token.length], 0, getter); -} - -addWeekYearFormatToken('gggg', 'weekYear'); -addWeekYearFormatToken('ggggg', 'weekYear'); -addWeekYearFormatToken('GGGG', 'isoWeekYear'); -addWeekYearFormatToken('GGGGG', 'isoWeekYear'); - -// ALIASES - -addUnitAlias('weekYear', 'gg'); -addUnitAlias('isoWeekYear', 'GG'); - -// PRIORITY - -addUnitPriority('weekYear', 1); -addUnitPriority('isoWeekYear', 1); - - -// PARSING - -addRegexToken('G', matchSigned); -addRegexToken('g', matchSigned); -addRegexToken('GG', match1to2, match2); -addRegexToken('gg', match1to2, match2); -addRegexToken('GGGG', match1to4, match4); -addRegexToken('gggg', match1to4, match4); -addRegexToken('GGGGG', match1to6, match6); -addRegexToken('ggggg', match1to6, match6); - -addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) { - week[token.substr(0, 2)] = toInt(input); -}); - -addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { - week[token] = hooks.parseTwoDigitYear(input); -}); - -// MOMENTS - -function getSetWeekYear (input) { - return getSetWeekYearHelper.call(this, - input, - this.week(), - this.weekday(), - this.localeData()._week.dow, - this.localeData()._week.doy); -} - -function getSetISOWeekYear (input) { - return getSetWeekYearHelper.call(this, - input, this.isoWeek(), this.isoWeekday(), 1, 4); -} - -function getISOWeeksInYear () { - return weeksInYear(this.year(), 1, 4); -} - -function getWeeksInYear () { - var weekInfo = this.localeData()._week; - return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); -} - -function getSetWeekYearHelper(input, week, weekday, dow, doy) { - var weeksTarget; - if (input == null) { - return weekOfYear(this, dow, doy).year; - } else { - weeksTarget = weeksInYear(input, dow, doy); - if (week > weeksTarget) { - week = weeksTarget; - } - return setWeekAll.call(this, input, week, weekday, dow, doy); - } -} - -function setWeekAll(weekYear, week, weekday, dow, doy) { - var dayOfYearData = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy), - date = createUTCDate(dayOfYearData.year, 0, dayOfYearData.dayOfYear); - - this.year(date.getUTCFullYear()); - this.month(date.getUTCMonth()); - this.date(date.getUTCDate()); - return this; -} - -// FORMATTING - -addFormatToken('Q', 0, 'Qo', 'quarter'); - -// ALIASES - -addUnitAlias('quarter', 'Q'); - -// PRIORITY - -addUnitPriority('quarter', 7); - -// PARSING - -addRegexToken('Q', match1); -addParseToken('Q', function (input, array) { - array[MONTH] = (toInt(input) - 1) * 3; -}); - -// MOMENTS - -function getSetQuarter (input) { - return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); -} - -// FORMATTING - -addFormatToken('D', ['DD', 2], 'Do', 'date'); - -// ALIASES - -addUnitAlias('date', 'D'); - -// PRIOROITY -addUnitPriority('date', 9); - -// PARSING - -addRegexToken('D', match1to2); -addRegexToken('DD', match1to2, match2); -addRegexToken('Do', function (isStrict, locale) { - // TODO: Remove "ordinalParse" fallback in next major release. - return isStrict ? - (locale._dayOfMonthOrdinalParse || locale._ordinalParse) : - locale._dayOfMonthOrdinalParseLenient; -}); - -addParseToken(['D', 'DD'], DATE); -addParseToken('Do', function (input, array) { - array[DATE] = toInt(input.match(match1to2)[0]); -}); - -// MOMENTS - -var getSetDayOfMonth = makeGetSet('Date', true); - -// FORMATTING - -addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); - -// ALIASES - -addUnitAlias('dayOfYear', 'DDD'); - -// PRIORITY -addUnitPriority('dayOfYear', 4); - -// PARSING - -addRegexToken('DDD', match1to3); -addRegexToken('DDDD', match3); -addParseToken(['DDD', 'DDDD'], function (input, array, config) { - config._dayOfYear = toInt(input); -}); - -// HELPERS - -// MOMENTS - -function getSetDayOfYear (input) { - var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1; - return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); -} - -// FORMATTING - -addFormatToken('m', ['mm', 2], 0, 'minute'); - -// ALIASES - -addUnitAlias('minute', 'm'); - -// PRIORITY - -addUnitPriority('minute', 14); - -// PARSING - -addRegexToken('m', match1to2); -addRegexToken('mm', match1to2, match2); -addParseToken(['m', 'mm'], MINUTE); - -// MOMENTS - -var getSetMinute = makeGetSet('Minutes', false); - -// FORMATTING - -addFormatToken('s', ['ss', 2], 0, 'second'); - -// ALIASES - -addUnitAlias('second', 's'); - -// PRIORITY - -addUnitPriority('second', 15); - -// PARSING - -addRegexToken('s', match1to2); -addRegexToken('ss', match1to2, match2); -addParseToken(['s', 'ss'], SECOND); - -// MOMENTS - -var getSetSecond = makeGetSet('Seconds', false); - -// FORMATTING - -addFormatToken('S', 0, 0, function () { - return ~~(this.millisecond() / 100); -}); - -addFormatToken(0, ['SS', 2], 0, function () { - return ~~(this.millisecond() / 10); -}); - -addFormatToken(0, ['SSS', 3], 0, 'millisecond'); -addFormatToken(0, ['SSSS', 4], 0, function () { - return this.millisecond() * 10; -}); -addFormatToken(0, ['SSSSS', 5], 0, function () { - return this.millisecond() * 100; -}); -addFormatToken(0, ['SSSSSS', 6], 0, function () { - return this.millisecond() * 1000; -}); -addFormatToken(0, ['SSSSSSS', 7], 0, function () { - return this.millisecond() * 10000; -}); -addFormatToken(0, ['SSSSSSSS', 8], 0, function () { - return this.millisecond() * 100000; -}); -addFormatToken(0, ['SSSSSSSSS', 9], 0, function () { - return this.millisecond() * 1000000; -}); - - -// ALIASES - -addUnitAlias('millisecond', 'ms'); - -// PRIORITY - -addUnitPriority('millisecond', 16); - -// PARSING - -addRegexToken('S', match1to3, match1); -addRegexToken('SS', match1to3, match2); -addRegexToken('SSS', match1to3, match3); - -var token; -for (token = 'SSSS'; token.length <= 9; token += 'S') { - addRegexToken(token, matchUnsigned); -} - -function parseMs(input, array) { - array[MILLISECOND] = toInt(('0.' + input) * 1000); -} - -for (token = 'S'; token.length <= 9; token += 'S') { - addParseToken(token, parseMs); -} -// MOMENTS - -var getSetMillisecond = makeGetSet('Milliseconds', false); - -// FORMATTING - -addFormatToken('z', 0, 0, 'zoneAbbr'); -addFormatToken('zz', 0, 0, 'zoneName'); - -// MOMENTS - -function getZoneAbbr () { - return this._isUTC ? 'UTC' : ''; -} - -function getZoneName () { - return this._isUTC ? 'Coordinated Universal Time' : ''; -} - -var proto = Moment.prototype; - -proto.add = add; -proto.calendar = calendar$1; -proto.clone = clone; -proto.diff = diff; -proto.endOf = endOf; -proto.format = format; -proto.from = from; -proto.fromNow = fromNow; -proto.to = to; -proto.toNow = toNow; -proto.get = stringGet; -proto.invalidAt = invalidAt; -proto.isAfter = isAfter; -proto.isBefore = isBefore; -proto.isBetween = isBetween; -proto.isSame = isSame; -proto.isSameOrAfter = isSameOrAfter; -proto.isSameOrBefore = isSameOrBefore; -proto.isValid = isValid$2; -proto.lang = lang; -proto.locale = locale; -proto.localeData = localeData; -proto.max = prototypeMax; -proto.min = prototypeMin; -proto.parsingFlags = parsingFlags; -proto.set = stringSet; -proto.startOf = startOf; -proto.subtract = subtract; -proto.toArray = toArray; -proto.toObject = toObject; -proto.toDate = toDate; -proto.toISOString = toISOString; -proto.inspect = inspect; -proto.toJSON = toJSON; -proto.toString = toString; -proto.unix = unix; -proto.valueOf = valueOf; -proto.creationData = creationData; -proto.year = getSetYear; -proto.isLeapYear = getIsLeapYear; -proto.weekYear = getSetWeekYear; -proto.isoWeekYear = getSetISOWeekYear; -proto.quarter = proto.quarters = getSetQuarter; -proto.month = getSetMonth; -proto.daysInMonth = getDaysInMonth; -proto.week = proto.weeks = getSetWeek; -proto.isoWeek = proto.isoWeeks = getSetISOWeek; -proto.weeksInYear = getWeeksInYear; -proto.isoWeeksInYear = getISOWeeksInYear; -proto.date = getSetDayOfMonth; -proto.day = proto.days = getSetDayOfWeek; -proto.weekday = getSetLocaleDayOfWeek; -proto.isoWeekday = getSetISODayOfWeek; -proto.dayOfYear = getSetDayOfYear; -proto.hour = proto.hours = getSetHour; -proto.minute = proto.minutes = getSetMinute; -proto.second = proto.seconds = getSetSecond; -proto.millisecond = proto.milliseconds = getSetMillisecond; -proto.utcOffset = getSetOffset; -proto.utc = setOffsetToUTC; -proto.local = setOffsetToLocal; -proto.parseZone = setOffsetToParsedOffset; -proto.hasAlignedHourOffset = hasAlignedHourOffset; -proto.isDST = isDaylightSavingTime; -proto.isLocal = isLocal; -proto.isUtcOffset = isUtcOffset; -proto.isUtc = isUtc; -proto.isUTC = isUtc; -proto.zoneAbbr = getZoneAbbr; -proto.zoneName = getZoneName; -proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth); -proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth); -proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear); -proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/', getSetZone); -proto.isDSTShifted = deprecate('isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information', isDaylightSavingTimeShifted); - -function createUnix (input) { - return createLocal(input * 1000); -} - -function createInZone () { - return createLocal.apply(null, arguments).parseZone(); -} - -function preParsePostFormat (string) { - return string; -} - -var proto$1 = Locale.prototype; - -proto$1.calendar = calendar; -proto$1.longDateFormat = longDateFormat; -proto$1.invalidDate = invalidDate; -proto$1.ordinal = ordinal; -proto$1.preparse = preParsePostFormat; -proto$1.postformat = preParsePostFormat; -proto$1.relativeTime = relativeTime; -proto$1.pastFuture = pastFuture; -proto$1.set = set; - -proto$1.months = localeMonths; -proto$1.monthsShort = localeMonthsShort; -proto$1.monthsParse = localeMonthsParse; -proto$1.monthsRegex = monthsRegex; -proto$1.monthsShortRegex = monthsShortRegex; -proto$1.week = localeWeek; -proto$1.firstDayOfYear = localeFirstDayOfYear; -proto$1.firstDayOfWeek = localeFirstDayOfWeek; - -proto$1.weekdays = localeWeekdays; -proto$1.weekdaysMin = localeWeekdaysMin; -proto$1.weekdaysShort = localeWeekdaysShort; -proto$1.weekdaysParse = localeWeekdaysParse; - -proto$1.weekdaysRegex = weekdaysRegex; -proto$1.weekdaysShortRegex = weekdaysShortRegex; -proto$1.weekdaysMinRegex = weekdaysMinRegex; - -proto$1.isPM = localeIsPM; -proto$1.meridiem = localeMeridiem; - -function get$1 (format, index, field, setter) { - var locale = getLocale(); - var utc = createUTC().set(setter, index); - return locale[field](utc, format); -} - -function listMonthsImpl (format, index, field) { - if (isNumber(format)) { - index = format; - format = undefined; - } - - format = format || ''; - - if (index != null) { - return get$1(format, index, field, 'month'); - } - - var i; - var out = []; - for (i = 0; i < 12; i++) { - out[i] = get$1(format, i, field, 'month'); - } - return out; -} - -// () -// (5) -// (fmt, 5) -// (fmt) -// (true) -// (true, 5) -// (true, fmt, 5) -// (true, fmt) -function listWeekdaysImpl (localeSorted, format, index, field) { - if (typeof localeSorted === 'boolean') { - if (isNumber(format)) { - index = format; - format = undefined; - } - - format = format || ''; - } else { - format = localeSorted; - index = format; - localeSorted = false; - - if (isNumber(format)) { - index = format; - format = undefined; - } - - format = format || ''; - } - - var locale = getLocale(), - shift = localeSorted ? locale._week.dow : 0; - - if (index != null) { - return get$1(format, (index + shift) % 7, field, 'day'); - } - - var i; - var out = []; - for (i = 0; i < 7; i++) { - out[i] = get$1(format, (i + shift) % 7, field, 'day'); - } - return out; -} - -function listMonths (format, index) { - return listMonthsImpl(format, index, 'months'); -} - -function listMonthsShort (format, index) { - return listMonthsImpl(format, index, 'monthsShort'); -} - -function listWeekdays (localeSorted, format, index) { - return listWeekdaysImpl(localeSorted, format, index, 'weekdays'); -} - -function listWeekdaysShort (localeSorted, format, index) { - return listWeekdaysImpl(localeSorted, format, index, 'weekdaysShort'); -} - -function listWeekdaysMin (localeSorted, format, index) { - return listWeekdaysImpl(localeSorted, format, index, 'weekdaysMin'); -} - -getSetGlobalLocale('en', { - dayOfMonthOrdinalParse: /\d{1,2}(th|st|nd|rd)/, - ordinal : function (number) { - var b = number % 10, - output = (toInt(number % 100 / 10) === 1) ? 'th' : - (b === 1) ? 'st' : - (b === 2) ? 'nd' : - (b === 3) ? 'rd' : 'th'; - return number + output; - } -}); - -// Side effect imports - -hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', getSetGlobalLocale); -hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', getLocale); - -var mathAbs = Math.abs; - -function abs () { - var data = this._data; - - this._milliseconds = mathAbs(this._milliseconds); - this._days = mathAbs(this._days); - this._months = mathAbs(this._months); - - data.milliseconds = mathAbs(data.milliseconds); - data.seconds = mathAbs(data.seconds); - data.minutes = mathAbs(data.minutes); - data.hours = mathAbs(data.hours); - data.months = mathAbs(data.months); - data.years = mathAbs(data.years); - - return this; -} - -function addSubtract$1 (duration, input, value, direction) { - var other = createDuration(input, value); - - duration._milliseconds += direction * other._milliseconds; - duration._days += direction * other._days; - duration._months += direction * other._months; - - return duration._bubble(); -} - -// supports only 2.0-style add(1, 's') or add(duration) -function add$1 (input, value) { - return addSubtract$1(this, input, value, 1); -} - -// supports only 2.0-style subtract(1, 's') or subtract(duration) -function subtract$1 (input, value) { - return addSubtract$1(this, input, value, -1); -} - -function absCeil (number) { - if (number < 0) { - return Math.floor(number); - } else { - return Math.ceil(number); - } -} - -function bubble () { - var milliseconds = this._milliseconds; - var days = this._days; - var months = this._months; - var data = this._data; - var seconds, minutes, hours, years, monthsFromDays; - - // if we have a mix of positive and negative values, bubble down first - // check: https://github.com/moment/moment/issues/2166 - if (!((milliseconds >= 0 && days >= 0 && months >= 0) || - (milliseconds <= 0 && days <= 0 && months <= 0))) { - milliseconds += absCeil(monthsToDays(months) + days) * 864e5; - days = 0; - months = 0; - } - - // The following code bubbles up values, see the tests for - // examples of what that means. - data.milliseconds = milliseconds % 1000; - - seconds = absFloor(milliseconds / 1000); - data.seconds = seconds % 60; - - minutes = absFloor(seconds / 60); - data.minutes = minutes % 60; - - hours = absFloor(minutes / 60); - data.hours = hours % 24; - - days += absFloor(hours / 24); - - // convert days to months - monthsFromDays = absFloor(daysToMonths(days)); - months += monthsFromDays; - days -= absCeil(monthsToDays(monthsFromDays)); - - // 12 months -> 1 year - years = absFloor(months / 12); - months %= 12; - - data.days = days; - data.months = months; - data.years = years; - - return this; -} - -function daysToMonths (days) { - // 400 years have 146097 days (taking into account leap year rules) - // 400 years have 12 months === 4800 - return days * 4800 / 146097; -} - -function monthsToDays (months) { - // the reverse of daysToMonths - return months * 146097 / 4800; -} - -function as (units) { - if (!this.isValid()) { - return NaN; - } - var days; - var months; - var milliseconds = this._milliseconds; - - units = normalizeUnits(units); - - if (units === 'month' || units === 'year') { - days = this._days + milliseconds / 864e5; - months = this._months + daysToMonths(days); - return units === 'month' ? months : months / 12; - } else { - // handle milliseconds separately because of floating point math errors (issue #1867) - days = this._days + Math.round(monthsToDays(this._months)); - switch (units) { - case 'week' : return days / 7 + milliseconds / 6048e5; - case 'day' : return days + milliseconds / 864e5; - case 'hour' : return days * 24 + milliseconds / 36e5; - case 'minute' : return days * 1440 + milliseconds / 6e4; - case 'second' : return days * 86400 + milliseconds / 1000; - // Math.floor prevents floating point math errors here - case 'millisecond': return Math.floor(days * 864e5) + milliseconds; - default: throw new Error('Unknown unit ' + units); - } - } -} - -// TODO: Use this.as('ms')? -function valueOf$1 () { - if (!this.isValid()) { - return NaN; - } - return ( - this._milliseconds + - this._days * 864e5 + - (this._months % 12) * 2592e6 + - toInt(this._months / 12) * 31536e6 - ); -} - -function makeAs (alias) { - return function () { - return this.as(alias); - }; -} - -var asMilliseconds = makeAs('ms'); -var asSeconds = makeAs('s'); -var asMinutes = makeAs('m'); -var asHours = makeAs('h'); -var asDays = makeAs('d'); -var asWeeks = makeAs('w'); -var asMonths = makeAs('M'); -var asYears = makeAs('y'); - -function clone$1 () { - return createDuration(this); -} - -function get$2 (units) { - units = normalizeUnits(units); - return this.isValid() ? this[units + 's']() : NaN; -} - -function makeGetter(name) { - return function () { - return this.isValid() ? this._data[name] : NaN; - }; -} - -var milliseconds = makeGetter('milliseconds'); -var seconds = makeGetter('seconds'); -var minutes = makeGetter('minutes'); -var hours = makeGetter('hours'); -var days = makeGetter('days'); -var months = makeGetter('months'); -var years = makeGetter('years'); - -function weeks () { - return absFloor(this.days() / 7); -} - -var round = Math.round; -var thresholds = { - ss: 44, // a few seconds to seconds - s : 45, // seconds to minute - m : 45, // minutes to hour - h : 22, // hours to day - d : 26, // days to month - M : 11 // months to year -}; - -// helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize -function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { - return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); -} - -function relativeTime$1 (posNegDuration, withoutSuffix, locale) { - var duration = createDuration(posNegDuration).abs(); - var seconds = round(duration.as('s')); - var minutes = round(duration.as('m')); - var hours = round(duration.as('h')); - var days = round(duration.as('d')); - var months = round(duration.as('M')); - var years = round(duration.as('y')); - - var a = seconds <= thresholds.ss && ['s', seconds] || - seconds < thresholds.s && ['ss', seconds] || - minutes <= 1 && ['m'] || - minutes < thresholds.m && ['mm', minutes] || - hours <= 1 && ['h'] || - hours < thresholds.h && ['hh', hours] || - days <= 1 && ['d'] || - days < thresholds.d && ['dd', days] || - months <= 1 && ['M'] || - months < thresholds.M && ['MM', months] || - years <= 1 && ['y'] || ['yy', years]; - - a[2] = withoutSuffix; - a[3] = +posNegDuration > 0; - a[4] = locale; - return substituteTimeAgo.apply(null, a); -} - -// This function allows you to set the rounding function for relative time strings -function getSetRelativeTimeRounding (roundingFunction) { - if (roundingFunction === undefined) { - return round; - } - if (typeof(roundingFunction) === 'function') { - round = roundingFunction; - return true; - } - return false; -} - -// This function allows you to set a threshold for relative time strings -function getSetRelativeTimeThreshold (threshold, limit) { - if (thresholds[threshold] === undefined) { - return false; - } - if (limit === undefined) { - return thresholds[threshold]; - } - thresholds[threshold] = limit; - if (threshold === 's') { - thresholds.ss = limit - 1; - } - return true; -} - -function humanize (withSuffix) { - if (!this.isValid()) { - return this.localeData().invalidDate(); - } - - var locale = this.localeData(); - var output = relativeTime$1(this, !withSuffix, locale); - - if (withSuffix) { - output = locale.pastFuture(+this, output); - } - - return locale.postformat(output); -} - -var abs$1 = Math.abs; - -function sign(x) { - return ((x > 0) - (x < 0)) || +x; -} - -function toISOString$1() { - // for ISO strings we do not use the normal bubbling rules: - // * milliseconds bubble up until they become hours - // * days do not bubble at all - // * months bubble up until they become years - // This is because there is no context-free conversion between hours and days - // (think of clock changes) - // and also not between days and months (28-31 days per month) - if (!this.isValid()) { - return this.localeData().invalidDate(); - } - - var seconds = abs$1(this._milliseconds) / 1000; - var days = abs$1(this._days); - var months = abs$1(this._months); - var minutes, hours, years; - - // 3600 seconds -> 60 minutes -> 1 hour - minutes = absFloor(seconds / 60); - hours = absFloor(minutes / 60); - seconds %= 60; - minutes %= 60; - - // 12 months -> 1 year - years = absFloor(months / 12); - months %= 12; - - - // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js - var Y = years; - var M = months; - var D = days; - var h = hours; - var m = minutes; - var s = seconds ? seconds.toFixed(3).replace(/\.?0+$/, '') : ''; - var total = this.asSeconds(); - - if (!total) { - // this is the same as C#'s (Noda) and python (isodate)... - // but not other JS (goog.date) - return 'P0D'; - } - - var totalSign = total < 0 ? '-' : ''; - var ymSign = sign(this._months) !== sign(total) ? '-' : ''; - var daysSign = sign(this._days) !== sign(total) ? '-' : ''; - var hmsSign = sign(this._milliseconds) !== sign(total) ? '-' : ''; - - return totalSign + 'P' + - (Y ? ymSign + Y + 'Y' : '') + - (M ? ymSign + M + 'M' : '') + - (D ? daysSign + D + 'D' : '') + - ((h || m || s) ? 'T' : '') + - (h ? hmsSign + h + 'H' : '') + - (m ? hmsSign + m + 'M' : '') + - (s ? hmsSign + s + 'S' : ''); -} - -var proto$2 = Duration.prototype; - -proto$2.isValid = isValid$1; -proto$2.abs = abs; -proto$2.add = add$1; -proto$2.subtract = subtract$1; -proto$2.as = as; -proto$2.asMilliseconds = asMilliseconds; -proto$2.asSeconds = asSeconds; -proto$2.asMinutes = asMinutes; -proto$2.asHours = asHours; -proto$2.asDays = asDays; -proto$2.asWeeks = asWeeks; -proto$2.asMonths = asMonths; -proto$2.asYears = asYears; -proto$2.valueOf = valueOf$1; -proto$2._bubble = bubble; -proto$2.clone = clone$1; -proto$2.get = get$2; -proto$2.milliseconds = milliseconds; -proto$2.seconds = seconds; -proto$2.minutes = minutes; -proto$2.hours = hours; -proto$2.days = days; -proto$2.weeks = weeks; -proto$2.months = months; -proto$2.years = years; -proto$2.humanize = humanize; -proto$2.toISOString = toISOString$1; -proto$2.toString = toISOString$1; -proto$2.toJSON = toISOString$1; -proto$2.locale = locale; -proto$2.localeData = localeData; - -proto$2.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', toISOString$1); -proto$2.lang = lang; - -// Side effect imports - -// FORMATTING - -addFormatToken('X', 0, 0, 'unix'); -addFormatToken('x', 0, 0, 'valueOf'); - -// PARSING - -addRegexToken('x', matchSigned); -addRegexToken('X', matchTimestamp); -addParseToken('X', function (input, array, config) { - config._d = new Date(parseFloat(input, 10) * 1000); -}); -addParseToken('x', function (input, array, config) { - config._d = new Date(toInt(input)); -}); - -// Side effect imports - - -hooks.version = '2.21.0'; - -setHookCallback(createLocal); - -hooks.fn = proto; -hooks.min = min; -hooks.max = max; -hooks.now = now; -hooks.utc = createUTC; -hooks.unix = createUnix; -hooks.months = listMonths; -hooks.isDate = isDate; -hooks.locale = getSetGlobalLocale; -hooks.invalid = createInvalid; -hooks.duration = createDuration; -hooks.isMoment = isMoment; -hooks.weekdays = listWeekdays; -hooks.parseZone = createInZone; -hooks.localeData = getLocale; -hooks.isDuration = isDuration; -hooks.monthsShort = listMonthsShort; -hooks.weekdaysMin = listWeekdaysMin; -hooks.defineLocale = defineLocale; -hooks.updateLocale = updateLocale; -hooks.locales = listLocales; -hooks.weekdaysShort = listWeekdaysShort; -hooks.normalizeUnits = normalizeUnits; -hooks.relativeTimeRounding = getSetRelativeTimeRounding; -hooks.relativeTimeThreshold = getSetRelativeTimeThreshold; -hooks.calendarFormat = getCalendarFormat; -hooks.prototype = proto; - -// currently HTML5 input type only supports 24-hour formats -hooks.HTML5_FMT = { - DATETIME_LOCAL: 'YYYY-MM-DDTHH:mm', // - DATETIME_LOCAL_SECONDS: 'YYYY-MM-DDTHH:mm:ss', // - DATETIME_LOCAL_MS: 'YYYY-MM-DDTHH:mm:ss.SSS', // - DATE: 'YYYY-MM-DD', // - TIME: 'HH:mm', // - TIME_SECONDS: 'HH:mm:ss', // - TIME_MS: 'HH:mm:ss.SSS', // - WEEK: 'YYYY-[W]WW', // - MONTH: 'YYYY-MM' // -}; - -return hooks; - -}))); -;/** - * Parse a text request to a json query object tree - * - * @param {String} string The string to parse - * @return {Object} The json query tree - */ -function parseStringToObject(string) { - -var arrayExtend = function () { - var j, i, newlist = [], list_list = arguments; - for (j = 0; j < list_list.length; j += 1) { - for (i = 0; i < list_list[j].length; i += 1) { - newlist.push(list_list[j][i]); - } - } - return newlist; - -}, mkSimpleQuery = function (key, value, operator) { - var object = {"type": "simple", "key": key, "value": value}; - if (operator !== undefined) { - object.operator = operator; - } - return object; - -}, mkNotQuery = function (query) { - if (query.operator === "NOT") { - return query.query_list[0]; - } - return {"type": "complex", "operator": "NOT", "query_list": [query]}; - -}, mkComplexQuery = function (operator, query_list) { - var i, query_list2 = []; - for (i = 0; i < query_list.length; i += 1) { - if (query_list[i].operator === operator) { - query_list2 = arrayExtend(query_list2, query_list[i].query_list); - } else { - query_list2.push(query_list[i]); - } - } - return {type:"complex",operator:operator,query_list:query_list2}; - -}, simpleQuerySetKey = function (query, key) { - var i; - if (query.type === "complex") { - for (i = 0; i < query.query_list.length; ++i) { - simpleQuerySetKey (query.query_list[i],key); - } - return true; - } - if (query.type === "simple" && !query.key) { - query.key = key; - return true; - } - return false; -}, - error_offsets = [], - error_lookaheads = [], - error_count = 0, - result; -;/* parser generated by jison 0.4.16 */ -/* - Returns a Parser object of the following structure: - - Parser: { - yy: {} - } - - Parser.prototype: { - yy: {}, - trace: function(), - symbols_: {associative list: name ==> number}, - terminals_: {associative list: number ==> name}, - productions_: [...], - performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), - table: [...], - defaultActions: {...}, - parseError: function(str, hash), - parse: function(input), - - lexer: { - EOF: 1, - parseError: function(str, hash), - setInput: function(input), - input: function(), - unput: function(str), - more: function(), - less: function(n), - pastInput: function(), - upcomingInput: function(), - showPosition: function(), - test_match: function(regex_match_array, rule_index), - next: function(), - lex: function(), - begin: function(condition), - popState: function(), - _currentRules: function(), - topState: function(), - pushState: function(condition), - - options: { - ranges: boolean (optional: true ==> token location info will include a .range[] member) - flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) - backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) - }, - - performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), - rules: [...], - conditions: {associative list: name ==> set}, - } - } - - - token location info (@$, _$, etc.): { - first_line: n, - last_line: n, - first_column: n, - last_column: n, - range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) - } - - - the parseError function receives a 'hash' object with these members for lexer and parser errors: { - text: (matched text) - token: (the produced terminal token, if any) - line: (yylineno) - } - while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { - loc: (yylloc) - expected: (string describing the set of expected tokens) - recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) - } -*/ -var parser = (function(){ -var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,5],$V1=[1,7],$V2=[1,8],$V3=[1,10],$V4=[1,12],$V5=[1,6,7,15],$V6=[1,6,7,9,12,14,15,16,19,21],$V7=[1,6,7,9,11,12,14,15,16,19,21],$V8=[2,17]; -var parser = {trace: function trace() { }, -yy: {}, -symbols_: {"error":2,"begin":3,"search_text":4,"end":5,"EOF":6,"NEWLINE":7,"and_expression":8,"OR":9,"boolean_expression":10,"AND":11,"NOT":12,"expression":13,"LEFT_PARENTHESE":14,"RIGHT_PARENTHESE":15,"WORD":16,"DEFINITION":17,"value":18,"OPERATOR":19,"string":20,"QUOTE":21,"QUOTED_STRING":22,"$accept":0,"$end":1}, -terminals_: {2:"error",6:"EOF",7:"NEWLINE",9:"OR",11:"AND",12:"NOT",14:"LEFT_PARENTHESE",15:"RIGHT_PARENTHESE",16:"WORD",17:"DEFINITION",19:"OPERATOR",21:"QUOTE",22:"QUOTED_STRING"}, -productions_: [0,[3,2],[5,0],[5,1],[5,1],[4,1],[4,2],[4,3],[8,1],[8,3],[10,2],[10,1],[13,3],[13,3],[13,1],[18,2],[18,1],[20,1],[20,3]], -performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { -/* this == yyval */ - -var $0 = $$.length - 1; -switch (yystate) { -case 1: - return $$[$0-1]; -break; -case 5: case 8: case 11: case 14: case 16: - this.$ = $$[$0]; -break; -case 6: - this.$ = mkComplexQuery('AND', [$$[$0-1], $$[$0]]); -break; -case 7: - this.$ = mkComplexQuery('OR', [$$[$0-2], $$[$0]]); -break; -case 9: - this.$ = mkComplexQuery('AND', [$$[$0-2], $$[$0]]); -break; -case 10: - this.$ = mkNotQuery($$[$0]); -break; -case 12: - this.$ = $$[$0-1]; -break; -case 13: - simpleQuerySetKey($$[$0], $$[$0-2]); this.$ = $$[$0]; -break; -case 15: - $$[$0].operator = $$[$0-1] ; this.$ = $$[$0]; -break; -case 17: - this.$ = mkSimpleQuery('', $$[$0]); -break; -case 18: - this.$ = mkSimpleQuery('', $$[$0-1]); -break; -} -}, -table: [{3:1,4:2,8:3,10:4,12:$V0,13:6,14:$V1,16:$V2,18:9,19:$V3,20:11,21:$V4},{1:[3]},{1:[2,2],5:13,6:[1,14],7:[1,15]},o($V5,[2,5],{8:3,10:4,13:6,18:9,20:11,4:16,9:[1,17],12:$V0,14:$V1,16:$V2,19:$V3,21:$V4}),o($V6,[2,8],{11:[1,18]}),{13:19,14:$V1,16:$V2,18:9,19:$V3,20:11,21:$V4},o($V7,[2,11]),{4:20,8:3,10:4,12:$V0,13:6,14:$V1,16:$V2,18:9,19:$V3,20:11,21:$V4},o($V7,$V8,{17:[1,21]}),o($V7,[2,14]),{16:[1,23],20:22,21:$V4},o($V7,[2,16]),{22:[1,24]},{1:[2,1]},{1:[2,3]},{1:[2,4]},o($V5,[2,6]),{4:25,8:3,10:4,12:$V0,13:6,14:$V1,16:$V2,18:9,19:$V3,20:11,21:$V4},{8:26,10:4,12:$V0,13:6,14:$V1,16:$V2,18:9,19:$V3,20:11,21:$V4},o($V7,[2,10]),{15:[1,27]},{13:28,14:$V1,16:$V2,18:9,19:$V3,20:11,21:$V4},o($V7,[2,15]),o($V7,$V8),{21:[1,29]},o($V5,[2,7]),o($V6,[2,9]),o($V7,[2,12]),o($V7,[2,13]),o($V7,[2,18])], -defaultActions: {13:[2,1],14:[2,3],15:[2,4]}, -parseError: function parseError(str, hash) { - if (hash.recoverable) { - this.trace(str); - } else { - function _parseError (msg, hash) { - this.message = msg; - this.hash = hash; - } - _parseError.prototype = new Error(); - - throw new _parseError(str, hash); - } -}, -parse: function parse(input) { - var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; - var args = lstack.slice.call(arguments, 1); - var lexer = Object.create(this.lexer); - var sharedState = { yy: {} }; - for (var k in this.yy) { - if (Object.prototype.hasOwnProperty.call(this.yy, k)) { - sharedState.yy[k] = this.yy[k]; - } - } - lexer.setInput(input, sharedState.yy); - sharedState.yy.lexer = lexer; - sharedState.yy.parser = this; - if (typeof lexer.yylloc == 'undefined') { - lexer.yylloc = {}; - } - var yyloc = lexer.yylloc; - lstack.push(yyloc); - var ranges = lexer.options && lexer.options.ranges; - if (typeof sharedState.yy.parseError === 'function') { - this.parseError = sharedState.yy.parseError; - } else { - this.parseError = Object.getPrototypeOf(this).parseError; - } - function popStack(n) { - stack.length = stack.length - 2 * n; - vstack.length = vstack.length - n; - lstack.length = lstack.length - n; - } - _token_stack: - var lex = function () { - var token; - token = lexer.lex() || EOF; - if (typeof token !== 'number') { - token = self.symbols_[token] || token; - } - return token; - }; - var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; - while (true) { - state = stack[stack.length - 1]; - if (this.defaultActions[state]) { - action = this.defaultActions[state]; - } else { - if (symbol === null || typeof symbol == 'undefined') { - symbol = lex(); - } - action = table[state] && table[state][symbol]; - } - if (typeof action === 'undefined' || !action.length || !action[0]) { - var errStr = ''; - expected = []; - for (p in table[state]) { - if (this.terminals_[p] && p > TERROR) { - expected.push('\'' + this.terminals_[p] + '\''); - } - } - if (lexer.showPosition) { - errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; - } else { - errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); - } - this.parseError(errStr, { - text: lexer.match, - token: this.terminals_[symbol] || symbol, - line: lexer.yylineno, - loc: yyloc, - expected: expected - }); - } - if (action[0] instanceof Array && action.length > 1) { - throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); - } - switch (action[0]) { - case 1: - stack.push(symbol); - vstack.push(lexer.yytext); - lstack.push(lexer.yylloc); - stack.push(action[1]); - symbol = null; - if (!preErrorSymbol) { - yyleng = lexer.yyleng; - yytext = lexer.yytext; - yylineno = lexer.yylineno; - yyloc = lexer.yylloc; - if (recovering > 0) { - recovering--; - } - } else { - symbol = preErrorSymbol; - preErrorSymbol = null; - } - break; - case 2: - len = this.productions_[action[1]][1]; - yyval.$ = vstack[vstack.length - len]; - yyval._$ = { - first_line: lstack[lstack.length - (len || 1)].first_line, - last_line: lstack[lstack.length - 1].last_line, - first_column: lstack[lstack.length - (len || 1)].first_column, - last_column: lstack[lstack.length - 1].last_column - }; - if (ranges) { - yyval._$.range = [ - lstack[lstack.length - (len || 1)].range[0], - lstack[lstack.length - 1].range[1] - ]; - } - r = this.performAction.apply(yyval, [ - yytext, - yyleng, - yylineno, - sharedState.yy, - action[1], - vstack, - lstack - ].concat(args)); - if (typeof r !== 'undefined') { - return r; - } - if (len) { - stack = stack.slice(0, -1 * len * 2); - vstack = vstack.slice(0, -1 * len); - lstack = lstack.slice(0, -1 * len); - } - stack.push(this.productions_[action[1]][0]); - vstack.push(yyval.$); - lstack.push(yyval._$); - newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; - stack.push(newState); - break; - case 3: - return true; - } - } - return true; -}}; -/* generated by jison-lex 0.3.4 */ -var lexer = (function(){ -var lexer = ({ - -EOF:1, - -parseError:function parseError(str, hash) { - if (this.yy.parser) { - this.yy.parser.parseError(str, hash); - } else { - throw new Error(str); - } - }, - -// resets the lexer, sets new input -setInput:function (input, yy) { - this.yy = yy || this.yy || {}; - this._input = input; - this._more = this._backtrack = this.done = false; - this.yylineno = this.yyleng = 0; - this.yytext = this.matched = this.match = ''; - this.conditionStack = ['INITIAL']; - this.yylloc = { - first_line: 1, - first_column: 0, - last_line: 1, - last_column: 0 - }; - if (this.options.ranges) { - this.yylloc.range = [0,0]; - } - this.offset = 0; - return this; - }, - -// consumes and returns one char from the input -input:function () { - var ch = this._input[0]; - this.yytext += ch; - this.yyleng++; - this.offset++; - this.match += ch; - this.matched += ch; - var lines = ch.match(/(?:\r\n?|\n).*/g); - if (lines) { - this.yylineno++; - this.yylloc.last_line++; - } else { - this.yylloc.last_column++; - } - if (this.options.ranges) { - this.yylloc.range[1]++; - } - - this._input = this._input.slice(1); - return ch; - }, - -// unshifts one char (or a string) into the input -unput:function (ch) { - var len = ch.length; - var lines = ch.split(/(?:\r\n?|\n)/g); - - this._input = ch + this._input; - this.yytext = this.yytext.substr(0, this.yytext.length - len); - //this.yyleng -= len; - this.offset -= len; - var oldLines = this.match.split(/(?:\r\n?|\n)/g); - this.match = this.match.substr(0, this.match.length - 1); - this.matched = this.matched.substr(0, this.matched.length - 1); - - if (lines.length - 1) { - this.yylineno -= lines.length - 1; - } - var r = this.yylloc.range; - - this.yylloc = { - first_line: this.yylloc.first_line, - last_line: this.yylineno + 1, - first_column: this.yylloc.first_column, - last_column: lines ? - (lines.length === oldLines.length ? this.yylloc.first_column : 0) - + oldLines[oldLines.length - lines.length].length - lines[0].length : - this.yylloc.first_column - len - }; - - if (this.options.ranges) { - this.yylloc.range = [r[0], r[0] + this.yyleng - len]; - } - this.yyleng = this.yytext.length; - return this; - }, - -// When called from action, caches matched text and appends it on next action -more:function () { - this._more = true; - return this; - }, - -// When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. -reject:function () { - if (this.options.backtrack_lexer) { - this._backtrack = true; - } else { - return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { - text: "", - token: null, - line: this.yylineno - }); - - } - return this; - }, - -// retain first n characters of the match -less:function (n) { - this.unput(this.match.slice(n)); - }, - -// displays already matched input, i.e. for error messages -pastInput:function () { - var past = this.matched.substr(0, this.matched.length - this.match.length); - return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); - }, - -// displays upcoming input, i.e. for error messages -upcomingInput:function () { - var next = this.match; - if (next.length < 20) { - next += this._input.substr(0, 20-next.length); - } - return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); - }, - -// displays the character position where the lexing error occurred, i.e. for error messages -showPosition:function () { - var pre = this.pastInput(); - var c = new Array(pre.length + 1).join("-"); - return pre + this.upcomingInput() + "\n" + c + "^"; - }, - -// test the lexed token: return FALSE when not a match, otherwise return token -test_match:function (match, indexed_rule) { - var token, - lines, - backup; - - if (this.options.backtrack_lexer) { - // save context - backup = { - yylineno: this.yylineno, - yylloc: { - first_line: this.yylloc.first_line, - last_line: this.last_line, - first_column: this.yylloc.first_column, - last_column: this.yylloc.last_column - }, - yytext: this.yytext, - match: this.match, - matches: this.matches, - matched: this.matched, - yyleng: this.yyleng, - offset: this.offset, - _more: this._more, - _input: this._input, - yy: this.yy, - conditionStack: this.conditionStack.slice(0), - done: this.done - }; - if (this.options.ranges) { - backup.yylloc.range = this.yylloc.range.slice(0); - } - } - - lines = match[0].match(/(?:\r\n?|\n).*/g); - if (lines) { - this.yylineno += lines.length; - } - this.yylloc = { - first_line: this.yylloc.last_line, - last_line: this.yylineno + 1, - first_column: this.yylloc.last_column, - last_column: lines ? - lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : - this.yylloc.last_column + match[0].length - }; - this.yytext += match[0]; - this.match += match[0]; - this.matches = match; - this.yyleng = this.yytext.length; - if (this.options.ranges) { - this.yylloc.range = [this.offset, this.offset += this.yyleng]; - } - this._more = false; - this._backtrack = false; - this._input = this._input.slice(match[0].length); - this.matched += match[0]; - token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); - if (this.done && this._input) { - this.done = false; - } - if (token) { - return token; - } else if (this._backtrack) { - // recover context - for (var k in backup) { - this[k] = backup[k]; - } - return false; // rule action called reject() implying the next rule should be tested instead. - } - return false; - }, - -// return next match in input -next:function () { - if (this.done) { - return this.EOF; - } - if (!this._input) { - this.done = true; - } - - var token, - match, - tempMatch, - index; - if (!this._more) { - this.yytext = ''; - this.match = ''; - } - var rules = this._currentRules(); - for (var i = 0; i < rules.length; i++) { - tempMatch = this._input.match(this.rules[rules[i]]); - if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { - match = tempMatch; - index = i; - if (this.options.backtrack_lexer) { - token = this.test_match(tempMatch, rules[i]); - if (token !== false) { - return token; - } else if (this._backtrack) { - match = false; - continue; // rule action called reject() implying a rule MISmatch. - } else { - // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) - return false; - } - } else if (!this.options.flex) { - break; - } - } - } - if (match) { - token = this.test_match(match, rules[index]); - if (token !== false) { - return token; - } - // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) - return false; - } - if (this._input === "") { - return this.EOF; - } else { - return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { - text: "", - token: null, - line: this.yylineno - }); - } - }, - -// return next match that has a token -lex:function lex() { - var r = this.next(); - if (r) { - return r; - } else { - return this.lex(); - } - }, - -// activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) -begin:function begin(condition) { - this.conditionStack.push(condition); - }, - -// pop the previously active lexer condition state off the condition stack -popState:function popState() { - var n = this.conditionStack.length - 1; - if (n > 0) { - return this.conditionStack.pop(); - } else { - return this.conditionStack[0]; - } - }, - -// produce the lexer rule set which is active for the currently active lexer condition state -_currentRules:function _currentRules() { - if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { - return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; - } else { - return this.conditions["INITIAL"].rules; - } - }, - -// return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available -topState:function topState(n) { - n = this.conditionStack.length - 1 - Math.abs(n || 0); - if (n >= 0) { - return this.conditionStack[n]; - } else { - return "INITIAL"; - } - }, - -// alias for begin(condition) -pushState:function pushState(condition) { - this.begin(condition); - }, - -// return the number of states currently on the stack -stateStackSize:function stateStackSize() { - return this.conditionStack.length; - }, -options: {}, -performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { -var YYSTATE=YY_START; -switch($avoiding_name_collisions) { -case 0:this.begin("letsquote"); return "QUOTE"; -break; -case 1:this.popState(); this.begin("endquote"); return "QUOTED_STRING"; -break; -case 2:this.popState(); return "QUOTE"; -break; -case 3:/* skip whitespace */ -break; -case 4:return "LEFT_PARENTHESE"; -break; -case 5:return "RIGHT_PARENTHESE"; -break; -case 6:return "AND"; -break; -case 7:return "OR"; -break; -case 8:return "NOT"; -break; -case 9:return "DEFINITION"; -break; -case 10:return 19; -break; -case 11:return 16; -break; -case 12:return 6; -break; -} -}, -rules: [/^(?:")/,/^(?:(\\"|[^"])*)/,/^(?:")/,/^(?:[^\S]+)/,/^(?:\()/,/^(?:\))/,/^(?:AND\b)/,/^(?:OR\b)/,/^(?:NOT\b)/,/^(?::)/,/^(?:(!?=|<=?|>=?))/,/^(?:[^\s\n"():>=?)$/i; - - /** - * Convert metadata values to array of strings. ex: - * - * "a" -> ["a"], - * {"content": "a"} -> ["a"] - * - * @param {Any} value The metadata value - * @return {Array} The value in string array format - */ - function metadataValueToStringArray(value) { - var i, new_value = []; - if (value === undefined) { - return undefined; - } - if (!Array.isArray(value)) { - value = [value]; - } - for (i = 0; i < value.length; i += 1) { - if (typeof value[i] === 'object') { - new_value[i] = value[i].content; - } else { - new_value[i] = value[i]; - } - } - return new_value; - } - - /** - * A sort function to sort items by key - * - * @param {Array} sort_list List of couples [key, direction] - * @return {Function} The sort function - */ - function generateSortFunction(key_schema, sort_list) { - return function sortByMultipleIndex(a, b) { - var result, - cast_to, - key = sort_list[0][0], - way = sort_list[0][1], - i, - l, - a_string_array, - b_string_array, - f_a, - f_b, - tmp; - - if (way === 'descending') { - result = 1; - } else if (way === 'ascending') { - result = -1; - } else { - throw new TypeError("Query.sortFunction(): " + - "Argument 2 must be 'ascending' or 'descending'"); - } - - if (key_schema !== undefined && - key_schema.key_set !== undefined && - key_schema.key_set[key] !== undefined && - key_schema.key_set[key].cast_to !== undefined) { - if (typeof key_schema.key_set[key].cast_to === "string") { - cast_to = key_schema.cast_lookup[key_schema.key_set[key].cast_to]; - } else { - cast_to = key_schema.key_set[key].cast_to; - } - f_a = cast_to(a[key]); - f_b = cast_to(b[key]); - if (typeof f_b.cmp === 'function') { - tmp = result * f_b.cmp(f_a); - if (tmp !== 0) { - return tmp; - } - if (sort_list.length > 1) { - return generateSortFunction(key_schema, sort_list.slice(1))(a, b); - } - return tmp; - } - if (f_a > f_b) { - return -result; - } - if (f_a < f_b) { - return result; - } - if (sort_list.length > 1) { - return generateSortFunction(key_schema, sort_list.slice(1))(a, b); - } - return 0; - } - - // this comparison is 5 times faster than json comparison - a_string_array = metadataValueToStringArray(a[key]) || []; - b_string_array = metadataValueToStringArray(b[key]) || []; - l = Math.max(a_string_array.length, b_string_array.length); - for (i = 0; i < l; i += 1) { - if (a_string_array[i] === undefined) { - return result; - } - if (b_string_array[i] === undefined) { - return -result; - } - if (a_string_array[i] > b_string_array[i]) { - return -result; - } - if (a_string_array[i] < b_string_array[i]) { - return result; - } - } - if (sort_list.length > 1) { - return generateSortFunction(key_schema, sort_list.slice(1))(a, b); - } - return 0; - - }; - } - - - /** - * Sort a list of items, according to keys and directions. - * - * @param {Array} sort_on_option List of couples [key, direction] - * @param {Array} list The item list to sort - * @return {Array} The filtered list - */ - function sortOn(sort_on_option, list, key_schema) { - if (!Array.isArray(sort_on_option)) { - throw new TypeError("jioquery.sortOn(): " + - "Argument 1 is not of type 'array'"); - } - list.sort(generateSortFunction( - key_schema, - sort_on_option - )); - return list; - } - - /** - * Limit a list of items, according to index and length. - * - * @param {Array} limit_option A couple [from, length] - * @param {Array} list The item list to limit - * @return {Array} The filtered list - */ - function limit(limit_option, list) { - if (!Array.isArray(limit_option)) { - throw new TypeError("jioquery.limit(): " + - "Argument 1 is not of type 'array'"); - } - if (!Array.isArray(list)) { - throw new TypeError("jioquery.limit(): " + - "Argument 2 is not of type 'array'"); - } - list.splice(0, limit_option[0]); - if (limit_option[1]) { - list.splice(limit_option[1]); - } - return list; - } - - /** - * Filter a list of items, modifying them to select only wanted keys. - * - * @param {Array} select_option Key list to keep - * @param {Array} list The item list to filter - * @return {Array} The filtered list - */ - function select(select_option, list) { - var i, j, new_item; - if (!Array.isArray(select_option)) { - throw new TypeError("jioquery.select(): " + - "Argument 1 is not of type Array"); - } - if (!Array.isArray(list)) { - throw new TypeError("jioquery.select(): " + - "Argument 2 is not of type Array"); - } - for (i = 0; i < list.length; i += 1) { - new_item = {}; - for (j = 0; j < select_option.length; j += 1) { - if (list[i].hasOwnProperty([select_option[j]])) { - new_item[select_option[j]] = list[i][select_option[j]]; - } - } - for (j in new_item) { - if (new_item.hasOwnProperty(j)) { - list[i] = new_item; - break; - } - } - } - return list; - } - - function checkKeySchema(key_schema) { - var prop; - - if (key_schema !== undefined) { - if (typeof key_schema !== 'object') { - throw new TypeError("Query().create(): " + - "key_schema is not of type 'object'"); - } - // key_set is mandatory - if (key_schema.key_set === undefined) { - throw new TypeError("Query().create(): " + - "key_schema has no 'key_set' property"); - } - for (prop in key_schema) { - if (key_schema.hasOwnProperty(prop)) { - switch (prop) { - case 'key_set': - case 'cast_lookup': - case 'match_lookup': - break; - default: - throw new TypeError("Query().create(): " + - "key_schema has unknown property '" + prop + "'"); - } - } - } - } - } - - /** - * The query to use to filter a list of objects. - * This is an abstract class. - * - * @class Query - * @constructor - */ - function Query(key_schema) { - - checkKeySchema(key_schema); - this._key_schema = key_schema || {}; - - /** - * Called before parsing the query. Must be overridden! - * - * @method onParseStart - * @param {Object} object The object shared in the parse process - * @param {Object} option Some option gave in parse() - */ - // this.onParseStart = emptyFunction; - - /** - * Called when parsing a simple query. Must be overridden! - * - * @method onParseSimpleQuery - * @param {Object} object The object shared in the parse process - * @param {Object} option Some option gave in parse() - */ - // this.onParseSimpleQuery = emptyFunction; - - /** - * Called when parsing a complex query. Must be overridden! - * - * @method onParseComplexQuery - * @param {Object} object The object shared in the parse process - * @param {Object} option Some option gave in parse() - */ - // this.onParseComplexQuery = emptyFunction; - - /** - * Called after parsing the query. Must be overridden! - * - * @method onParseEnd - * @param {Object} object The object shared in the parse process - * @param {Object} option Some option gave in parse() - */ - // this.onParseEnd = emptyFunction; - - return; - } - - /** - * Filter the item list with matching item only - * - * @method exec - * @param {Array} item_list The list of object - * @param {Object} [option] Some operation option - * @param {Array} [option.select_list] A object keys to retrieve - * @param {Array} [option.sort_on] Couples of object keys and "ascending" - * or "descending" - * @param {Array} [option.limit] Couple of integer, first is an index and - * second is the length. - */ - Query.prototype.exec = function (item_list, option) { - if (!Array.isArray(item_list)) { - throw new TypeError("Query().exec(): Argument 1 is not of type 'array'"); - } - if (option === undefined) { - option = {}; - } - if (typeof option !== 'object') { - throw new TypeError("Query().exec(): " + - "Optional argument 2 is not of type 'object'"); - } - var context = this, - i; - for (i = item_list.length - 1; i >= 0; i -= 1) { - if (!context.match(item_list[i])) { - item_list.splice(i, 1); - } - } - - if (option.sort_on) { - sortOn(option.sort_on, item_list, this._key_schema); - } - - if (option.limit) { - limit(option.limit, item_list); - } - - select(option.select_list || [], item_list); - - return new RSVP.Queue() - .push(function () { - return item_list; - }); - }; - - /** - * Test if an item matches this query - * - * @method match - * @param {Object} item The object to test - * @return {Boolean} true if match, false otherwise - */ - Query.prototype.match = function () { - return true; - }; - - /** - * Browse the Query in deep calling parser method in each step. - * - * `onParseStart` is called first, on end `onParseEnd` is called. - * It starts from the simple queries at the bottom of the tree calling the - * parser method `onParseSimpleQuery`, and go up calling the - * `onParseComplexQuery` method. - * - * @method parse - * @param {Object} option Any options you want (except 'parsed') - * @return {Any} The parse result - */ - Query.prototype.parse = function (option) { - var that = this, - object; - /** - * The recursive parser. - * - * @param {Object} object The object shared in the parse process - * @param {Object} options Some options usable in the parseMethods - * @return {Any} The parser result - */ - function recParse(object, option) { - var query = object.parsed, - queue = new RSVP.Queue(), - i; - - function enqueue(j) { - queue - .push(function () { - object.parsed = query.query_list[j]; - return recParse(object, option); - }) - .push(function () { - query.query_list[j] = object.parsed; - }); - } - - if (query.type === "complex") { - - - for (i = 0; i < query.query_list.length; i += 1) { - enqueue(i); - } - - return queue - .push(function () { - object.parsed = query; - return that.onParseComplexQuery(object, option); - }); - - } - if (query.type === "simple") { - return that.onParseSimpleQuery(object, option); - } - } - object = { - parsed: JSON.parse(JSON.stringify(that.serialized())) - }; - return new RSVP.Queue() - .push(function () { - return that.onParseStart(object, option); - }) - .push(function () { - return recParse(object, option); - }) - .push(function () { - return that.onParseEnd(object, option); - }) - .push(function () { - return object.parsed; - }); - - }; - - /** - * Convert this query to a parsable string. - * - * @method toString - * @return {String} The string version of this query - */ - Query.prototype.toString = function () { - return ""; - }; - - /** - * Convert this query to an jsonable object in order to be remake thanks to - * QueryFactory class. - * - * @method serialized - * @return {Object} The jsonable object - */ - Query.prototype.serialized = function () { - return undefined; - }; - - /** - * Provides static methods to create Query object - * - * @class QueryFactory - */ - function QueryFactory() { - return; - } - - /** - * Escapes regexp special chars from a string. - * - * @param {String} string The string to escape - * @return {String} The escaped string - */ - function stringEscapeRegexpCharacters(string) { - return string.replace(regexp_escape, "\\$&"); - } - - /** - * Inherits the prototype methods from one constructor into another. The - * prototype of `constructor` will be set to a new object created from - * `superConstructor`. - * - * @param {Function} constructor The constructor which inherits the super one - * @param {Function} superConstructor The super constructor - */ - function inherits(constructor, superConstructor) { - constructor.super_ = superConstructor; - constructor.prototype = Object.create(superConstructor.prototype, { - "constructor": { - "configurable": true, - "enumerable": false, - "writable": true, - "value": constructor - } - }); - } - - /** - * Convert a search text to a regexp. - * - * @param {String} string The string to convert - * @param {Boolean} [use_wildcard_character=true] Use wildcard "%" and "_" - * @return {RegExp} The search text regexp - */ - function searchTextToRegExp(string, use_wildcard_characters) { - if (typeof string !== 'string') { - throw new TypeError("jioquery.searchTextToRegExp(): " + - "Argument 1 is not of type 'string'"); - } - if (use_wildcard_characters === false) { - return new RegExp("^" + stringEscapeRegexpCharacters(string) + "$"); - } - return new RegExp("^" + stringEscapeRegexpCharacters(string) - .replace(regexp_percent, '[\\s\\S]*') - .replace(regexp_underscore, '.') + "$", "i"); - } - - /** - * The ComplexQuery inherits from Query, and compares one or several metadata - * values. - * - * @class ComplexQuery - * @extends Query - * @param {Object} [spec={}] The specifications - * @param {String} [spec.operator="AND"] The compare method to use - * @param {String} spec.key The metadata key - * @param {String} spec.value The value of the metadata to compare - */ - function ComplexQuery(spec, key_schema) { - Query.call(this, key_schema); - - /** - * Logical operator to use to compare object values - * - * @attribute operator - * @type String - * @default "AND" - * @optional - */ - this.operator = spec.operator; - - /** - * The sub Query list which are used to query an item. - * - * @attribute query_list - * @type Array - * @default [] - * @optional - */ - this.query_list = spec.query_list || []; - this.query_list = this.query_list.map( - // decorate the map to avoid sending the index as key_schema argument - function (o) { return QueryFactory.create(o, key_schema); } - ); - - } - inherits(ComplexQuery, Query); - - ComplexQuery.prototype.operator = "AND"; - ComplexQuery.prototype.type = "complex"; - - /** - * #crossLink "Query/match:method" - */ - ComplexQuery.prototype.match = function (item) { - var operator = this.operator; - if (!(regexp_operator.test(operator))) { - operator = "AND"; - } - return this[operator.toUpperCase()](item); - }; - - /** - * #crossLink "Query/toString:method" - */ - ComplexQuery.prototype.toString = function () { - var str_list = [], this_operator = this.operator; - if (this.operator === "NOT") { - str_list.push("NOT ("); - str_list.push(this.query_list[0].toString()); - str_list.push(")"); - return str_list.join(" "); - } - this.query_list.forEach(function (query) { - str_list.push("("); - str_list.push(query.toString()); - str_list.push(")"); - str_list.push(this_operator); - }); - str_list.length -= 1; - return str_list.join(" "); - }; - - /** - * #crossLink "Query/serialized:method" - */ - ComplexQuery.prototype.serialized = function () { - var s = { - "type": "complex", - "operator": this.operator, - "query_list": [] - }; - this.query_list.forEach(function (query) { - s.query_list.push( - typeof query.toJSON === "function" ? query.toJSON() : query - ); - }); - return s; - }; - ComplexQuery.prototype.toJSON = ComplexQuery.prototype.serialized; - - /** - * Comparison operator, test if all sub queries match the - * item value - * - * @method AND - * @param {Object} item The item to match - * @return {Boolean} true if all match, false otherwise - */ - ComplexQuery.prototype.AND = function (item) { - var result = true, - i = 0; - - while (result && (i !== this.query_list.length)) { - result = this.query_list[i].match(item); - i += 1; - } - return result; - - }; - - /** - * Comparison operator, test if one of the sub queries matches the - * item value - * - * @method OR - * @param {Object} item The item to match - * @return {Boolean} true if one match, false otherwise - */ - ComplexQuery.prototype.OR = function (item) { - var result = false, - i = 0; - - while ((!result) && (i !== this.query_list.length)) { - result = this.query_list[i].match(item); - i += 1; - } - - return result; - }; - - /** - * Comparison operator, test if the sub query does not match the - * item value - * - * @method NOT - * @param {Object} item The item to match - * @return {Boolean} true if one match, false otherwise - */ - ComplexQuery.prototype.NOT = function (item) { - return !this.query_list[0].match(item); - }; - - /** - * Creates Query object from a search text string or a serialized version - * of a Query. - * - * @method create - * @static - * @param {Object,String} object The search text or the serialized version - * of a Query - * @return {Query} A Query object - */ - QueryFactory.create = function (object, key_schema) { - if (object === "") { - return new Query(key_schema); - } - if (typeof object === "string") { - object = parseStringToObject(object); - } - if (typeof (object || {}).type === "string" && - query_class_dict[object.type]) { - return new query_class_dict[object.type](object, key_schema); - } - throw new TypeError("QueryFactory.create(): " + - "Argument 1 is not a search text or a parsable object"); - }; - - function objectToSearchText(query) { - var str_list = []; - if (query.type === "complex") { - str_list.push("("); - (query.query_list || []).forEach(function (sub_query) { - str_list.push(objectToSearchText(sub_query)); - str_list.push(query.operator); - }); - str_list.length -= 1; - str_list.push(")"); - return str_list.join(" "); - } - if (query.type === "simple") { - return (query.key ? query.key + ": " : "") + - (query.operator || "") + ' "' + query.value + '"'; - } - throw new TypeError("This object is not a query"); - } - - /** - * The SimpleQuery inherits from Query, and compares one metadata value - * - * @class SimpleQuery - * @extends Query - * @param {Object} [spec={}] The specifications - * @param {String} [spec.operator="="] The compare method to use - * @param {String} spec.key The metadata key - * @param {String} spec.value The value of the metadata to compare - */ - function SimpleQuery(spec, key_schema) { - Query.call(this, key_schema); - - /** - * Operator to use to compare object values - * - * @attribute operator - * @type String - * @optional - */ - this.operator = spec.operator; - - /** - * Key of the object which refers to the value to compare - * - * @attribute key - * @type String - */ - this.key = spec.key; - - /** - * Value is used to do the comparison with the object value - * - * @attribute value - * @type String - */ - this.value = spec.value; - - } - inherits(SimpleQuery, Query); - - SimpleQuery.prototype.type = "simple"; - - function checkKey(key) { - var prop; - - if (key.read_from === undefined) { - throw new TypeError("Custom key is missing the read_from property"); - } - - for (prop in key) { - if (key.hasOwnProperty(prop)) { - switch (prop) { - case 'read_from': - case 'cast_to': - case 'equal_match': - break; - default: - throw new TypeError("Custom key has unknown property '" + - prop + "'"); - } - } - } - } - - /** - * #crossLink "Query/match:method" - */ - SimpleQuery.prototype.match = function (item) { - var object_value = null, - equal_match = null, - cast_to = null, - matchMethod = null, - operator = this.operator, - value = null, - key = this.key, - k; - - if (!(regexp_comparaison.test(operator))) { - // `operator` is not correct, we have to change it to "like" or "=" - if (regexp_percent.test(this.value)) { - // `value` contains a non escaped `%` - operator = "like"; - } else { - // `value` does not contain non escaped `%` - operator = "="; - } - } - - matchMethod = this[operator]; - - if (this._key_schema.key_set && this._key_schema.key_set[key] !== undefined) { - key = this._key_schema.key_set[key]; - } - - // match with all the fields if key is empty - if (key === '') { - matchMethod = this.like; - value = '%' + this.value + '%'; - for (k in item) { - if (item.hasOwnProperty(k)) { - if (k !== '__id' && item[k]) { - if (matchMethod(item[k], value) === true) { - return true; - } - } - } - } - return false; - } - - if (typeof key === 'object') { - checkKey(key); - object_value = item[key.read_from]; - - equal_match = key.equal_match; - - // equal_match can be a string - if (typeof equal_match === 'string') { - // XXX raise error if equal_match not in match_lookup - equal_match = this._key_schema.match_lookup[equal_match]; - } - - // equal_match overrides the default '=' operator - if (equal_match !== undefined) { - matchMethod = (operator === "=" || operator === "like" ? - equal_match : matchMethod); - } - - value = this.value; - cast_to = key.cast_to; - if (cast_to) { - // cast_to can be a string - if (typeof cast_to === 'string') { - // XXX raise error if cast_to not in cast_lookup - cast_to = this._key_schema.cast_lookup[cast_to]; - } - - try { - value = cast_to(value); - } catch (e) { - value = undefined; - } - - try { - object_value = cast_to(object_value); - } catch (e) { - object_value = undefined; - } - } - } else { - object_value = item[key]; - value = this.value; - } - if (object_value === undefined || value === undefined) { - return false; - } - return matchMethod(object_value, value); - }; - - /** - * #crossLink "Query/toString:method" - */ - SimpleQuery.prototype.toString = function () { - return (this.key ? this.key + ":" : "") + - (this.operator ? " " + this.operator : "") + ' "' + this.value + '"'; - }; - - /** - * #crossLink "Query/serialized:method" - */ - SimpleQuery.prototype.serialized = function () { - var object = { - "type": "simple", - "key": this.key, - "value": this.value - }; - if (this.operator !== undefined) { - object.operator = this.operator; - } - return object; - }; - SimpleQuery.prototype.toJSON = SimpleQuery.prototype.serialized; - - /** - * Comparison operator, test if this query value matches the item value - * - * @method = - * @param {String} object_value The value to compare - * @param {String} comparison_value The comparison value - * @return {Boolean} true if match, false otherwise - */ - SimpleQuery.prototype["="] = function (object_value, comparison_value) { - var value, i; - if (!Array.isArray(object_value)) { - object_value = [object_value]; - } - for (i = 0; i < object_value.length; i += 1) { - value = object_value[i]; - if (typeof value === 'object' && value.hasOwnProperty('content')) { - value = value.content; - } - if (typeof value.cmp === "function") { - return (value.cmp(comparison_value) === 0); - } - if (comparison_value.toString() === value.toString()) { - return true; - } - } - return false; - }; - - /** - * Comparison operator, test if this query value matches the item value - * - * @method like - * @param {String} object_value The value to compare - * @param {String} comparison_value The comparison value - * @return {Boolean} true if match, false otherwise - */ - SimpleQuery.prototype.like = function (object_value, comparison_value) { - var value, i; - if (!Array.isArray(object_value)) { - object_value = [object_value]; - } - for (i = 0; i < object_value.length; i += 1) { - value = object_value[i]; - if (typeof value === 'object' && value.hasOwnProperty('content')) { - value = value.content; - } - if (typeof value.cmp === "function") { - return (value.cmp(comparison_value) === 0); - } - if ( - searchTextToRegExp(comparison_value.toString()).test(value.toString()) - ) { - return true; - } - } - return false; - }; - - /** - * Comparison operator, test if this query value does not match the item value - * - * @method != - * @param {String} object_value The value to compare - * @param {String} comparison_value The comparison value - * @return {Boolean} true if not match, false otherwise - */ - SimpleQuery.prototype["!="] = function (object_value, comparison_value) { - var value, i; - if (!Array.isArray(object_value)) { - object_value = [object_value]; - } - for (i = 0; i < object_value.length; i += 1) { - value = object_value[i]; - if (typeof value === 'object' && value.hasOwnProperty('content')) { - value = value.content; - } - if (typeof value.cmp === "function") { - return (value.cmp(comparison_value) !== 0); - } - if (comparison_value.toString() === value.toString()) { - return false; - } - } - return true; - }; - - /** - * Comparison operator, test if this query value is lower than the item value - * - * @method < - * @param {Number, String} object_value The value to compare - * @param {Number, String} comparison_value The comparison value - * @return {Boolean} true if lower, false otherwise - */ - SimpleQuery.prototype["<"] = function (object_value, comparison_value) { - var value; - if (!Array.isArray(object_value)) { - object_value = [object_value]; - } - value = object_value[0]; - if (typeof value === 'object' && value.hasOwnProperty('content')) { - value = value.content; - } - if (typeof value.cmp === "function") { - return (value.cmp(comparison_value) < 0); - } - return (value < comparison_value); - }; - - /** - * Comparison operator, test if this query value is equal or lower than the - * item value - * - * @method <= - * @param {Number, String} object_value The value to compare - * @param {Number, String} comparison_value The comparison value - * @return {Boolean} true if equal or lower, false otherwise - */ - SimpleQuery.prototype["<="] = function (object_value, comparison_value) { - var value; - if (!Array.isArray(object_value)) { - object_value = [object_value]; - } - value = object_value[0]; - if (typeof value === 'object' && value.hasOwnProperty('content')) { - value = value.content; - } - if (typeof value.cmp === "function") { - return (value.cmp(comparison_value) <= 0); - } - return (value <= comparison_value); - }; - - /** - * Comparison operator, test if this query value is greater than the item - * value - * - * @method > - * @param {Number, String} object_value The value to compare - * @param {Number, String} comparison_value The comparison value - * @return {Boolean} true if greater, false otherwise - */ - SimpleQuery.prototype[">"] = function (object_value, comparison_value) { - var value; - if (!Array.isArray(object_value)) { - object_value = [object_value]; - } - value = object_value[0]; - if (typeof value === 'object' && value.hasOwnProperty('content')) { - value = value.content; - } - if (typeof value.cmp === "function") { - return (value.cmp(comparison_value) > 0); - } - return (value > comparison_value); - }; - - /** - * Comparison operator, test if this query value is equal or greater than the - * item value - * - * @method >= - * @param {Number, String} object_value The value to compare - * @param {Number, String} comparison_value The comparison value - * @return {Boolean} true if equal or greater, false otherwise - */ - SimpleQuery.prototype[">="] = function (object_value, comparison_value) { - var value; - if (!Array.isArray(object_value)) { - object_value = [object_value]; - } - value = object_value[0]; - if (typeof value === 'object' && value.hasOwnProperty('content')) { - value = value.content; - } - if (typeof value.cmp === "function") { - return (value.cmp(comparison_value) >= 0); - } - return (value >= comparison_value); - }; - - query_class_dict.simple = SimpleQuery; - query_class_dict.complex = ComplexQuery; - - Query.parseStringToObject = parseStringToObject; - Query.objectToSearchText = objectToSearchText; - - window.Query = Query; - window.SimpleQuery = SimpleQuery; - window.ComplexQuery = ComplexQuery; - window.QueryFactory = QueryFactory; - -}(RSVP, window, parseStringToObject)); -;/*global window, moment */ -/*jslint nomen: true, maxlen: 200*/ -(function (window, moment) { - "use strict"; - -// /** -// * Add a secured (write permission denied) property to an object. -// * -// * @param {Object} object The object to fill -// * @param {String} key The object key where to store the property -// * @param {Any} value The value to store -// */ -// function _export(key, value) { -// Object.defineProperty(to_export, key, { -// "configurable": false, -// "enumerable": true, -// "writable": false, -// "value": value -// }); -// } - - var YEAR = 'year', - MONTH = 'month', - DAY = 'day', - HOUR = 'hour', - MIN = 'minute', - SEC = 'second', - MSEC = 'millisecond', - precision_grade = { - 'year': 0, - 'month': 1, - 'day': 2, - 'hour': 3, - 'minute': 4, - 'second': 5, - 'millisecond': 6 - }, - lesserPrecision = function (p1, p2) { - return (precision_grade[p1] < precision_grade[p2]) ? p1 : p2; - }, - JIODate; - - - JIODate = function (str) { - // in case of forgotten 'new' - if (!(this instanceof JIODate)) { - return new JIODate(str); - } - - if (str instanceof JIODate) { - this.mom = str.mom.clone(); - this._precision = str._precision; - return; - } - - if (str === undefined) { - this.mom = moment(); - this.setPrecision(MSEC); - return; - } - - this.mom = null; - this._str = str; - - // http://www.w3.org/TR/NOTE-datetime - // http://dotat.at/tmp/ISO_8601-2004_E.pdf - - // XXX these regexps fail to detect many invalid dates. - - if (str.match(/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+\-][0-2]\d:[0-5]\d|Z)/) - || str.match(/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d/)) { - // ISO, milliseconds - this.mom = moment(str); - this.setPrecision(MSEC); - } else if (str.match(/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+\-][0-2]\d:[0-5]\d|Z)/) - || str.match(/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/)) { - // ISO, seconds - this.mom = moment(str); - this.setPrecision(SEC); - } else if (str.match(/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+\-][0-2]\d:[0-5]\d|Z)/) - || str.match(/\d\d\d\d-\d\d-\d\d \d\d:\d\d/)) { - // ISO, minutes - this.mom = moment(str); - this.setPrecision(MIN); - } else if (str.match(/\d\d\d\d-\d\d-\d\d \d\d/)) { - this.mom = moment(str); - this.setPrecision(HOUR); - } else if (str.match(/\d\d\d\d-\d\d-\d\d/)) { - this.mom = moment(str); - this.setPrecision(DAY); - } else if (str.match(/\d\d\d\d-\d\d/)) { - this.mom = moment(str); - this.setPrecision(MONTH); - } else if (str.match(/\d\d\d\d/)) { - // Creating a moment with only the year will show this deprecation - // warning: - // - // Deprecation warning: moment construction falls back to js Date. This is - // discouraged and will be removed in upcoming major release. Please refer - // to https://github.com/moment/moment/issues/1407 for more info. - // - // TL;DR: parsing year-only strings with momentjs falls back to native - // Date and it won't correctly represent the year in local time if UTF - // offset is negative. - // - // The solution is to use the format parameter, so momentjs won't fall - // back to the native Date and we will have the correct year in local - // time. - // - this.mom = moment(str, 'YYYY'); - this.setPrecision(YEAR); - } - - if (!this.mom) { - throw new Error("Cannot parse: " + str); - } - - }; - - - JIODate.prototype.setPrecision = function (prec) { - this._precision = prec; - }; - - - JIODate.prototype.getPrecision = function () { - return this._precision; - }; - - - JIODate.prototype.cmp = function (other) { - var m1 = this.mom, - m2 = other.mom, - p = lesserPrecision(this._precision, other._precision); - return m1.isBefore(m2, p) ? -1 : (m1.isSame(m2, p) ? 0 : +1); - }; - - - JIODate.prototype.toPrecisionString = function (precision) { - var fmt; - - precision = precision || this._precision; - - fmt = { - 'millisecond': 'YYYY-MM-DD HH:mm:ss.SSS', - 'second': 'YYYY-MM-DD HH:mm:ss', - 'minute': 'YYYY-MM-DD HH:mm', - 'hour': 'YYYY-MM-DD HH', - 'day': 'YYYY-MM-DD', - 'month': 'YYYY-MM', - 'year': 'YYYY' - }[precision]; - - if (!fmt) { - throw new TypeError("Unsupported precision value '" + precision + "'"); - } - - return this.mom.format(fmt); - }; - - - JIODate.prototype.toString = function () { - return this._str; - }; - - -// _export('JIODate', JIODate); -// -// _export('YEAR', YEAR); -// _export('MONTH', MONTH); -// _export('DAY', DAY); -// _export('HOUR', HOUR); -// _export('MIN', MIN); -// _export('SEC', SEC); -// _export('MSEC', MSEC); - - window.jiodate = { - JIODate: JIODate, - YEAR: YEAR, - MONTH: MONTH, - DAY: DAY, - HOUR: HOUR, - MIN: MIN, - SEC: SEC, - MSEC: MSEC - }; -}(window, moment)); -;/*global window, RSVP, Blob, XMLHttpRequest, QueryFactory, Query, atob, - FileReader, ArrayBuffer, Uint8Array, navigator */ -(function (window, RSVP, Blob, QueryFactory, Query, atob, - FileReader, ArrayBuffer, Uint8Array, navigator) { - "use strict"; - - if (window.openDatabase === undefined) { - window.openDatabase = function () { - throw new Error('WebSQL is not supported by ' + navigator.userAgent); - }; - } - - /* Safari does not define DOMError */ - if (window.DOMError === undefined) { - window.DOMError = {}; - } - - var util = {}, - jIO; - - function jIOError(message, status_code) { - if ((message !== undefined) && (typeof message !== "string")) { - throw new TypeError('You must pass a string.'); - } - this.message = message || "Default Message"; - this.status_code = status_code || 500; - } - jIOError.prototype = new Error(); - jIOError.prototype.constructor = jIOError; - util.jIOError = jIOError; - - /** - * Send request with XHR and return a promise. xhr.onload: The promise is - * resolved when the status code is lower than 400 with the xhr object as - * first parameter. xhr.onerror: reject with xhr object as first - * parameter. xhr.onprogress: notifies the xhr object. - * - * @param {Object} param The parameters - * @param {String} [param.type="GET"] The request method - * @param {String} [param.dataType=""] The data type to retrieve - * @param {String} param.url The url - * @param {Any} [param.data] The data to send - * @param {Number} param.timeout The request timeout value - * @param {Function} [param.beforeSend] A function called just before the - * send request. The first parameter of this function is the XHR object. - * @return {Promise} The promise - */ - function ajax(param) { - var xhr = new XMLHttpRequest(); - return new RSVP.Promise(function (resolve, reject, notify) { - var k; - xhr.open(param.type || "GET", param.url, true); - xhr.responseType = param.dataType || ""; - if (typeof param.headers === 'object' && param.headers !== null) { - for (k in param.headers) { - if (param.headers.hasOwnProperty(k)) { - xhr.setRequestHeader(k, param.headers[k]); - } - } - } - xhr.addEventListener("load", function (e) { - if (e.target.status >= 400) { - return reject(e); - } - resolve(e); - }); - xhr.addEventListener("error", reject); - xhr.addEventListener("progress", notify); - if (typeof param.xhrFields === 'object' && param.xhrFields !== null) { - for (k in param.xhrFields) { - if (param.xhrFields.hasOwnProperty(k)) { - xhr[k] = param.xhrFields[k]; - } - } - } - if (param.timeout !== undefined && param.timeout !== 0) { - xhr.timeout = param.timeout; - xhr.ontimeout = function () { - return reject(new jIO.util.jIOError("Gateway Timeout", 504)); - }; - } - if (typeof param.beforeSend === 'function') { - param.beforeSend(xhr); - } - xhr.send(param.data); - }, function () { - xhr.abort(); - }); - } - util.ajax = ajax; - - function readBlobAsText(blob, encoding) { - var fr = new FileReader(); - return new RSVP.Promise(function (resolve, reject, notify) { - fr.addEventListener("load", resolve); - fr.addEventListener("error", reject); - fr.addEventListener("progress", notify); - fr.readAsText(blob, encoding); - }, function () { - fr.abort(); - }); - } - util.readBlobAsText = readBlobAsText; - - function readBlobAsArrayBuffer(blob) { - var fr = new FileReader(); - return new RSVP.Promise(function (resolve, reject, notify) { - fr.addEventListener("load", resolve); - fr.addEventListener("error", reject); - fr.addEventListener("progress", notify); - fr.readAsArrayBuffer(blob); - }, function () { - fr.abort(); - }); - } - util.readBlobAsArrayBuffer = readBlobAsArrayBuffer; - - function readBlobAsDataURL(blob) { - var fr = new FileReader(); - return new RSVP.Promise(function (resolve, reject, notify) { - fr.addEventListener("load", resolve); - fr.addEventListener("error", reject); - fr.addEventListener("progress", notify); - fr.readAsDataURL(blob); - }, function () { - fr.abort(); - }); - } - util.readBlobAsDataURL = readBlobAsDataURL; - - function stringify(obj) { - // Implement a stable JSON.stringify - // Object's keys are alphabetically ordered - var key, - key_list, - i, - value, - result_list; - if (obj === undefined) { - return undefined; - } - if (obj === null) { - return 'null'; - } - if (obj.constructor === Object) { - key_list = Object.keys(obj).sort(); - result_list = []; - for (i = 0; i < key_list.length; i += 1) { - key = key_list[i]; - value = stringify(obj[key]); - if (value !== undefined) { - result_list.push(stringify(key) + ':' + value); - } - } - return '{' + result_list.join(',') + '}'; - } - if (obj.constructor === Array) { - result_list = []; - for (i = 0; i < obj.length; i += 1) { - result_list.push(stringify(obj[i])); - } - return '[' + result_list.join(',') + ']'; - } - return JSON.stringify(obj); - } - util.stringify = stringify; - - - // https://gist.github.com/davoclavo/4424731 - function dataURItoBlob(dataURI) { - if (dataURI === 'data:') { - return new Blob(); - } - // convert base64 to raw binary data held in a string - var byteString = atob(dataURI.split(',')[1]), - // separate out the mime component - mimeString = dataURI.split(',')[0].split(':')[1], - // write the bytes of the string to an ArrayBuffer - arrayBuffer = new ArrayBuffer(byteString.length), - _ia = new Uint8Array(arrayBuffer), - i; - mimeString = mimeString.slice(0, mimeString.length - ";base64".length); - for (i = 0; i < byteString.length; i += 1) { - _ia[i] = byteString.charCodeAt(i); - } - return new Blob([arrayBuffer], {type: mimeString}); - } - - util.dataURItoBlob = dataURItoBlob; - - // tools - function checkId(argument_list, storage, method_name) { - if (typeof argument_list[0] !== 'string' || argument_list[0] === '') { - throw new jIO.util.jIOError( - "Document id must be a non empty string on '" + storage.__type + - "." + method_name + "'.", - 400 - ); - } - } - - function checkAttachmentId(argument_list, storage, method_name) { - if (typeof argument_list[1] !== 'string' || argument_list[1] === '') { - throw new jIO.util.jIOError( - "Attachment id must be a non empty string on '" + storage.__type + - "." + method_name + "'.", - 400 - ); - } - } - - function declareMethod(klass, name, precondition_function, post_function) { - klass.prototype[name] = function () { - var argument_list = arguments, - context = this, - precondition_result; - - return new RSVP.Queue() - .push(function () { - if (precondition_function !== undefined) { - return precondition_function.apply( - context.__storage, - [argument_list, context, name] - ); - } - }) - .push(function (result) { - var storage_method = context.__storage[name]; - precondition_result = result; - if (storage_method === undefined) { - throw new jIO.util.jIOError( - "Capacity '" + name + "' is not implemented on '" + - context.__type + "'", - 501 - ); - } - return storage_method.apply( - context.__storage, - argument_list - ); - }) - .push(function (result) { - if (post_function !== undefined) { - return post_function.call( - context, - argument_list, - result, - precondition_result - ); - } - return result; - }); - }; - // Allow chain - return this; - } - - - - - ///////////////////////////////////////////////////////////////// - // jIO Storage Proxy - ///////////////////////////////////////////////////////////////// - function JioProxyStorage(type, storage) { - if (!(this instanceof JioProxyStorage)) { - return new JioProxyStorage(); - } - this.__type = type; - this.__storage = storage; - } - - declareMethod(JioProxyStorage, "put", checkId, function (argument_list) { - return argument_list[0]; - }); - declareMethod(JioProxyStorage, "get", checkId); - declareMethod(JioProxyStorage, "bulk"); - declareMethod(JioProxyStorage, "remove", checkId, function (argument_list) { - return argument_list[0]; - }); - - JioProxyStorage.prototype.post = function () { - var context = this, - argument_list = arguments; - return new RSVP.Queue() - .push(function () { - var storage_method = context.__storage.post; - if (storage_method === undefined) { - throw new jIO.util.jIOError( - "Capacity 'post' is not implemented on '" + context.__type + "'", - 501 - ); - } - return context.__storage.post.apply(context.__storage, argument_list); - }); - }; - - declareMethod(JioProxyStorage, 'putAttachment', function (argument_list, - storage, - method_name) { - checkId(argument_list, storage, method_name); - checkAttachmentId(argument_list, storage, method_name); - - var options = argument_list[3] || {}; - - if (typeof argument_list[2] === 'string') { - argument_list[2] = new Blob([argument_list[2]], { - "type": options._content_type || options._mimetype || - "text/plain;charset=utf-8" - }); - } else if (!(argument_list[2] instanceof Blob)) { - throw new jIO.util.jIOError( - 'Attachment content is not a blob', - 400 - ); - } - }); - - declareMethod(JioProxyStorage, 'removeAttachment', function (argument_list, - storage, - method_name) { - checkId(argument_list, storage, method_name); - checkAttachmentId(argument_list, storage, method_name); - }); - - declareMethod(JioProxyStorage, 'getAttachment', function (argument_list, - storage, - method_name) { - var result = "blob"; -// if (param.storage_spec.type !== "indexeddb" && -// param.storage_spec.type !== "dav" && -// (param.kwargs._start !== undefined -// || param.kwargs._end !== undefined)) { -// restCommandRejecter(param, [ -// 'bad_request', -// 'unsupport', -// '_start, _end not support' -// ]); -// return false; -// } - checkId(argument_list, storage, method_name); - checkAttachmentId(argument_list, storage, method_name); - // Drop optional parameters, which are only used in postfunction - if (argument_list[2] !== undefined) { - result = argument_list[2].format || result; - delete argument_list[2].format; - } - return result; - }, function (argument_list, blob, convert) { - var result; - if (!(blob instanceof Blob)) { - throw new jIO.util.jIOError( - "'getAttachment' (" + argument_list[0] + " , " + - argument_list[1] + ") on '" + this.__type + - "' does not return a Blob.", - 501 - ); - } - if (convert === "blob") { - result = blob; - } else if (convert === "data_url") { - result = new RSVP.Queue() - .push(function () { - return jIO.util.readBlobAsDataURL(blob); - }) - .push(function (evt) { - return evt.target.result; - }); - } else if (convert === "array_buffer") { - result = new RSVP.Queue() - .push(function () { - return jIO.util.readBlobAsArrayBuffer(blob); - }) - .push(function (evt) { - return evt.target.result; - }); - } else if (convert === "text") { - result = new RSVP.Queue() - .push(function () { - return jIO.util.readBlobAsText(blob); - }) - .push(function (evt) { - return evt.target.result; - }); - } else if (convert === "json") { - result = new RSVP.Queue() - .push(function () { - return jIO.util.readBlobAsText(blob); - }) - .push(function (evt) { - return JSON.parse(evt.target.result); - }); - } else { - throw new jIO.util.jIOError( - this.__type + ".getAttachment format: '" + convert + - "' is not supported", - 400 - ); - } - return result; - }); - - JioProxyStorage.prototype.buildQuery = function () { - var storage_method = this.__storage.buildQuery, - context = this, - argument_list = arguments; - if (storage_method === undefined) { - throw new jIO.util.jIOError( - "Capacity 'buildQuery' is not implemented on '" + this.__type + "'", - 501 - ); - } - return new RSVP.Queue() - .push(function () { - return storage_method.apply( - context.__storage, - argument_list - ); - }); - }; - - JioProxyStorage.prototype.hasCapacity = function (name) { - var storage_method = this.__storage.hasCapacity, - capacity_method = this.__storage[name]; - if (capacity_method !== undefined) { - return true; - } - if ((storage_method === undefined) || - !storage_method.apply(this.__storage, arguments)) { - throw new jIO.util.jIOError( - "Capacity '" + name + "' is not implemented on '" + this.__type + "'", - 501 - ); - } - return true; - }; - - JioProxyStorage.prototype.allDocs = function (options) { - var context = this; - if (options === undefined) { - options = {}; - } - return new RSVP.Queue() - .push(function () { - if (context.hasCapacity("list") && - ((options.query === undefined) || context.hasCapacity("query")) && - ((options.sort_on === undefined) || context.hasCapacity("sort")) && - ((options.select_list === undefined) || - context.hasCapacity("select")) && - ((options.include_docs === undefined) || - context.hasCapacity("include")) && - ((options.limit === undefined) || context.hasCapacity("limit"))) { - return context.buildQuery(options); - } - }) - .push(function (result) { - return { - data: { - rows: result, - total_rows: result.length - } - }; - }); - }; - - declareMethod(JioProxyStorage, "allAttachments", checkId); - declareMethod(JioProxyStorage, "repair"); - - JioProxyStorage.prototype.repair = function () { - var context = this, - argument_list = arguments; - return new RSVP.Queue() - .push(function () { - var storage_method = context.__storage.repair; - if (storage_method !== undefined) { - return context.__storage.repair.apply(context.__storage, - argument_list); - } - }); - }; - - ///////////////////////////////////////////////////////////////// - // Storage builder - ///////////////////////////////////////////////////////////////// - function JioBuilder() { - if (!(this instanceof JioBuilder)) { - return new JioBuilder(); - } - this.__storage_types = {}; - } - - JioBuilder.prototype.createJIO = function (storage_spec, util) { - - if (typeof storage_spec.type !== 'string') { - throw new TypeError("Invalid storage description"); - } - if (!this.__storage_types[storage_spec.type]) { - throw new TypeError("Unknown storage '" + storage_spec.type + "'"); - } - - return new JioProxyStorage( - storage_spec.type, - new this.__storage_types[storage_spec.type](storage_spec, util) - ); - - }; - - JioBuilder.prototype.addStorage = function (type, Constructor) { - if (typeof type !== 'string') { - throw new TypeError( - "jIO.addStorage(): Argument 1 is not of type 'string'" - ); - } - if (typeof Constructor !== 'function') { - throw new TypeError("jIO.addStorage(): " + - "Argument 2 is not of type 'function'"); - } - if (this.__storage_types[type] !== undefined) { - throw new TypeError("jIO.addStorage(): Storage type already exists"); - } - this.__storage_types[type] = Constructor; - }; - - JioBuilder.prototype.util = util; - JioBuilder.prototype.QueryFactory = QueryFactory; - JioBuilder.prototype.Query = Query; - - ///////////////////////////////////////////////////////////////// - // global - ///////////////////////////////////////////////////////////////// - jIO = new JioBuilder(); - window.jIO = jIO; - -}(window, RSVP, Blob, QueryFactory, Query, atob, - FileReader, ArrayBuffer, Uint8Array, navigator)); -;/* - * Rusha, a JavaScript implementation of the Secure Hash Algorithm, SHA-1, - * as defined in FIPS PUB 180-1, tuned for high performance with large inputs. - * (http://github.com/srijs/rusha) - * - * Inspired by Paul Johnstons implementation (http://pajhome.org.uk/crypt/md5). - * - * Copyright (c) 2013 Sam Rijs (http://awesam.de). - * Released under the terms of the MIT license as follows: - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ -(function () { - // If we'e running in Node.JS, export a module. - if (typeof module !== 'undefined') { - module.exports = Rusha; - } else if (typeof window !== 'undefined') { - window.Rusha = Rusha; - } - // If we're running in a webworker, accept - // messages containing a jobid and a buffer - // or blob object, and return the hash result. - if (typeof FileReaderSync !== 'undefined') { - var reader = new FileReaderSync(), hasher = new Rusha(4 * 1024 * 1024); - self.onmessage = function onMessage(event) { - var hash, data = event.data.data; - try { - hash = hasher.digest(data); - self.postMessage({ - id: event.data.id, - hash: hash - }); - } catch (e) { - self.postMessage({ - id: event.data.id, - error: e.name - }); - } - }; - } - var util = { - getDataType: function (data) { - if (typeof data === 'string') { - return 'string'; - } - if (data instanceof Array) { - return 'array'; - } - if (typeof global !== 'undefined' && global.Buffer && global.Buffer.isBuffer(data)) { - return 'buffer'; - } - if (data instanceof ArrayBuffer) { - return 'arraybuffer'; - } - if (data.buffer instanceof ArrayBuffer) { - return 'view'; - } - if (data instanceof Blob) { - return 'blob'; - } - throw new Error('Unsupported data type.'); - } - }; - // The Rusha object is a wrapper around the low-level RushaCore. - // It provides means of converting different inputs to the - // format accepted by RushaCore as well as other utility methods. - function Rusha(chunkSize) { - 'use strict'; - // Private object structure. - var self$2 = { fill: 0 }; - // Calculate the length of buffer that the sha1 routine uses - // including the padding. - var padlen = function (len) { - for (len += 9; len % 64 > 0; len += 1); - return len; - }; - var padZeroes = function (bin, len) { - for (var i = len >> 2; i < bin.length; i++) - bin[i] = 0; - }; - var padData = function (bin, chunkLen, msgLen) { - bin[chunkLen >> 2] |= 128 << 24 - (chunkLen % 4 << 3); - bin[((chunkLen >> 2) + 2 & ~15) + 14] = msgLen >> 29; - bin[((chunkLen >> 2) + 2 & ~15) + 15] = msgLen << 3; - }; - // Convert a binary string and write it to the heap. - // A binary string is expected to only contain char codes < 256. - var convStr = function (H8, H32, start, len, off) { - var str = this, i, om = off % 4, lm = len % 4, j = len - lm; - if (j > 0) { - switch (om) { - case 0: - H8[off + 3 | 0] = str.charCodeAt(start); - case 1: - H8[off + 2 | 0] = str.charCodeAt(start + 1); - case 2: - H8[off + 1 | 0] = str.charCodeAt(start + 2); - case 3: - H8[off | 0] = str.charCodeAt(start + 3); - } - } - for (i = om; i < j; i = i + 4 | 0) { - H32[off + i >> 2] = str.charCodeAt(start + i) << 24 | str.charCodeAt(start + i + 1) << 16 | str.charCodeAt(start + i + 2) << 8 | str.charCodeAt(start + i + 3); - } - switch (lm) { - case 3: - H8[off + j + 1 | 0] = str.charCodeAt(start + j + 2); - case 2: - H8[off + j + 2 | 0] = str.charCodeAt(start + j + 1); - case 1: - H8[off + j + 3 | 0] = str.charCodeAt(start + j); - } - }; - // Convert a buffer or array and write it to the heap. - // The buffer or array is expected to only contain elements < 256. - var convBuf = function (H8, H32, start, len, off) { - var buf = this, i, om = off % 4, lm = len % 4, j = len - lm; - if (j > 0) { - switch (om) { - case 0: - H8[off + 3 | 0] = buf[start]; - case 1: - H8[off + 2 | 0] = buf[start + 1]; - case 2: - H8[off + 1 | 0] = buf[start + 2]; - case 3: - H8[off | 0] = buf[start + 3]; - } - } - for (i = 4 - om; i < j; i = i += 4 | 0) { - H32[off + i >> 2] = buf[start + i] << 24 | buf[start + i + 1] << 16 | buf[start + i + 2] << 8 | buf[start + i + 3]; - } - switch (lm) { - case 3: - H8[off + j + 1 | 0] = buf[start + j + 2]; - case 2: - H8[off + j + 2 | 0] = buf[start + j + 1]; - case 1: - H8[off + j + 3 | 0] = buf[start + j]; - } - }; - var convBlob = function (H8, H32, start, len, off) { - var blob = this, i, om = off % 4, lm = len % 4, j = len - lm; - var buf = new Uint8Array(reader.readAsArrayBuffer(blob.slice(start, start + len))); - if (j > 0) { - switch (om) { - case 0: - H8[off + 3 | 0] = buf[0]; - case 1: - H8[off + 2 | 0] = buf[1]; - case 2: - H8[off + 1 | 0] = buf[2]; - case 3: - H8[off | 0] = buf[3]; - } - } - for (i = 4 - om; i < j; i = i += 4 | 0) { - H32[off + i >> 2] = buf[i] << 24 | buf[i + 1] << 16 | buf[i + 2] << 8 | buf[i + 3]; - } - switch (lm) { - case 3: - H8[off + j + 1 | 0] = buf[j + 2]; - case 2: - H8[off + j + 2 | 0] = buf[j + 1]; - case 1: - H8[off + j + 3 | 0] = buf[j]; - } - }; - var convFn = function (data) { - switch (util.getDataType(data)) { - case 'string': - return convStr.bind(data); - case 'array': - return convBuf.bind(data); - case 'buffer': - return convBuf.bind(data); - case 'arraybuffer': - return convBuf.bind(new Uint8Array(data)); - case 'view': - return convBuf.bind(new Uint8Array(data.buffer, data.byteOffset, data.byteLength)); - case 'blob': - return convBlob.bind(data); - } - }; - var slice = function (data, offset) { - switch (util.getDataType(data)) { - case 'string': - return data.slice(offset); - case 'array': - return data.slice(offset); - case 'buffer': - return data.slice(offset); - case 'arraybuffer': - return data.slice(offset); - case 'view': - return data.buffer.slice(offset); - } - }; - // Convert an ArrayBuffer into its hexadecimal string representation. - var hex = function (arrayBuffer) { - var i, x, hex_tab = '0123456789abcdef', res = [], binarray = new Uint8Array(arrayBuffer); - for (i = 0; i < binarray.length; i++) { - x = binarray[i]; - res[i] = hex_tab.charAt(x >> 4 & 15) + hex_tab.charAt(x >> 0 & 15); - } - return res.join(''); - }; - var ceilHeapSize = function (v) { - // The asm.js spec says: - // The heap object's byteLength must be either - // 2^n for n in [12, 24) or 2^24 * n for n ≥ 1. - // Also, byteLengths smaller than 2^16 are deprecated. - var p; - // If v is smaller than 2^16, the smallest possible solution - // is 2^16. - if (v <= 65536) - return 65536; - // If v < 2^24, we round up to 2^n, - // otherwise we round up to 2^24 * n. - if (v < 16777216) { - for (p = 1; p < v; p = p << 1); - } else { - for (p = 16777216; p < v; p += 16777216); - } - return p; - }; - // Initialize the internal data structures to a new capacity. - var init = function (size) { - if (size % 64 > 0) { - throw new Error('Chunk size must be a multiple of 128 bit'); - } - self$2.maxChunkLen = size; - self$2.padMaxChunkLen = padlen(size); - // The size of the heap is the sum of: - // 1. The padded input message size - // 2. The extended space the algorithm needs (320 byte) - // 3. The 160 bit state the algoritm uses - self$2.heap = new ArrayBuffer(ceilHeapSize(self$2.padMaxChunkLen + 320 + 20)); - self$2.h32 = new Int32Array(self$2.heap); - self$2.h8 = new Int8Array(self$2.heap); - self$2.core = RushaCore({ - Int32Array: Int32Array, - DataView: DataView - }, {}, self$2.heap); - self$2.buffer = null; - }; - // Iinitializethe datastructures according - // to a chunk siyze. - init(chunkSize || 64 * 1024); - var initState = function (heap, padMsgLen) { - var io = new Int32Array(heap, padMsgLen + 320, 5); - io[0] = 1732584193; - io[1] = -271733879; - io[2] = -1732584194; - io[3] = 271733878; - io[4] = -1009589776; - }; - var padChunk = function (chunkLen, msgLen) { - var padChunkLen = padlen(chunkLen); - var view = new Int32Array(self$2.heap, 0, padChunkLen >> 2); - padZeroes(view, chunkLen); - padData(view, chunkLen, msgLen); - return padChunkLen; - }; - // Write data to the heap. - var write = function (data, chunkOffset, chunkLen) { - convFn(data)(self$2.h8, self$2.h32, chunkOffset, chunkLen, 0); - }; - // Initialize and call the RushaCore, - // assuming an input buffer of length len * 4. - var coreCall = function (data, chunkOffset, chunkLen, msgLen, finalize) { - var padChunkLen = chunkLen; - if (finalize) { - padChunkLen = padChunk(chunkLen, msgLen); - } - write(data, chunkOffset, chunkLen); - self$2.core.hash(padChunkLen, self$2.padMaxChunkLen); - }; - var getRawDigest = function (heap, padMaxChunkLen) { - var io = new Int32Array(heap, padMaxChunkLen + 320, 5); - var out = new Int32Array(5); - var arr = new DataView(out.buffer); - arr.setInt32(0, io[0], false); - arr.setInt32(4, io[1], false); - arr.setInt32(8, io[2], false); - arr.setInt32(12, io[3], false); - arr.setInt32(16, io[4], false); - return out; - }; - // Calculate the hash digest as an array of 5 32bit integers. - var rawDigest = this.rawDigest = function (str) { - var msgLen = str.byteLength || str.length || str.size || 0; - initState(self$2.heap, self$2.padMaxChunkLen); - var chunkOffset = 0, chunkLen = self$2.maxChunkLen, last; - for (chunkOffset = 0; msgLen > chunkOffset + chunkLen; chunkOffset += chunkLen) { - coreCall(str, chunkOffset, chunkLen, msgLen, false); - } - coreCall(str, chunkOffset, msgLen - chunkOffset, msgLen, true); - return getRawDigest(self$2.heap, self$2.padMaxChunkLen); - }; - // The digest and digestFrom* interface returns the hash digest - // as a hex string. - this.digest = this.digestFromString = this.digestFromBuffer = this.digestFromArrayBuffer = function (str) { - return hex(rawDigest(str).buffer); - }; - } - ; - // The low-level RushCore module provides the heart of Rusha, - // a high-speed sha1 implementation working on an Int32Array heap. - // At first glance, the implementation seems complicated, however - // with the SHA1 spec at hand, it is obvious this almost a textbook - // implementation that has a few functions hand-inlined and a few loops - // hand-unrolled. - function RushaCore(stdlib, foreign, heap) { - 'use asm'; - var H = new stdlib.Int32Array(heap); - function hash(k, x) { - // k in bytes - k = k | 0; - x = x | 0; - var i = 0, j = 0, y0 = 0, z0 = 0, y1 = 0, z1 = 0, y2 = 0, z2 = 0, y3 = 0, z3 = 0, y4 = 0, z4 = 0, t0 = 0, t1 = 0; - y0 = H[x + 320 >> 2] | 0; - y1 = H[x + 324 >> 2] | 0; - y2 = H[x + 328 >> 2] | 0; - y3 = H[x + 332 >> 2] | 0; - y4 = H[x + 336 >> 2] | 0; - for (i = 0; (i | 0) < (k | 0); i = i + 64 | 0) { - z0 = y0; - z1 = y1; - z2 = y2; - z3 = y3; - z4 = y4; - for (j = 0; (j | 0) < 64; j = j + 4 | 0) { - t1 = H[i + j >> 2] | 0; - t0 = ((y0 << 5 | y0 >>> 27) + (y1 & y2 | ~y1 & y3) | 0) + ((t1 + y4 | 0) + 1518500249 | 0) | 0; - y4 = y3; - y3 = y2; - y2 = y1 << 30 | y1 >>> 2; - y1 = y0; - y0 = t0; - ; - H[k + j >> 2] = t1; - } - for (j = k + 64 | 0; (j | 0) < (k + 80 | 0); j = j + 4 | 0) { - t1 = (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) << 1 | (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) >>> 31; - t0 = ((y0 << 5 | y0 >>> 27) + (y1 & y2 | ~y1 & y3) | 0) + ((t1 + y4 | 0) + 1518500249 | 0) | 0; - y4 = y3; - y3 = y2; - y2 = y1 << 30 | y1 >>> 2; - y1 = y0; - y0 = t0; - ; - H[j >> 2] = t1; - } - for (j = k + 80 | 0; (j | 0) < (k + 160 | 0); j = j + 4 | 0) { - t1 = (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) << 1 | (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) >>> 31; - t0 = ((y0 << 5 | y0 >>> 27) + (y1 ^ y2 ^ y3) | 0) + ((t1 + y4 | 0) + 1859775393 | 0) | 0; - y4 = y3; - y3 = y2; - y2 = y1 << 30 | y1 >>> 2; - y1 = y0; - y0 = t0; - ; - H[j >> 2] = t1; - } - for (j = k + 160 | 0; (j | 0) < (k + 240 | 0); j = j + 4 | 0) { - t1 = (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) << 1 | (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) >>> 31; - t0 = ((y0 << 5 | y0 >>> 27) + (y1 & y2 | y1 & y3 | y2 & y3) | 0) + ((t1 + y4 | 0) - 1894007588 | 0) | 0; - y4 = y3; - y3 = y2; - y2 = y1 << 30 | y1 >>> 2; - y1 = y0; - y0 = t0; - ; - H[j >> 2] = t1; - } - for (j = k + 240 | 0; (j | 0) < (k + 320 | 0); j = j + 4 | 0) { - t1 = (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) << 1 | (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) >>> 31; - t0 = ((y0 << 5 | y0 >>> 27) + (y1 ^ y2 ^ y3) | 0) + ((t1 + y4 | 0) - 899497514 | 0) | 0; - y4 = y3; - y3 = y2; - y2 = y1 << 30 | y1 >>> 2; - y1 = y0; - y0 = t0; - ; - H[j >> 2] = t1; - } - y0 = y0 + z0 | 0; - y1 = y1 + z1 | 0; - y2 = y2 + z2 | 0; - y3 = y3 + z3 | 0; - y4 = y4 + z4 | 0; - } - H[x + 320 >> 2] = y0; - H[x + 324 >> 2] = y1; - H[x + 328 >> 2] = y2; - H[x + 332 >> 2] = y3; - H[x + 336 >> 2] = y4; - } - return { hash: hash }; - } -}());;/* - * JIO extension for resource replication. - * Copyright (C) 2013, 2015 Nexedi SA - * - * This library is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -/*jslint nomen: true*/ -/*global jIO, RSVP, Rusha*/ - -(function (jIO, RSVP, Rusha, stringify) { - "use strict"; - - var rusha = new Rusha(), - CONFLICT_THROW = 0, - CONFLICT_KEEP_LOCAL = 1, - CONFLICT_KEEP_REMOTE = 2, - CONFLICT_CONTINUE = 3; - - function SkipError(message) { - if ((message !== undefined) && (typeof message !== "string")) { - throw new TypeError('You must pass a string.'); - } - this.message = message || "Skip some asynchronous code"; - } - SkipError.prototype = new Error(); - SkipError.prototype.constructor = SkipError; - - /**************************************************** - Use a local jIO to read/write/search documents - Synchronize in background those document with a remote jIO. - Synchronization status is stored for each document as an local attachment. - ****************************************************/ - - function generateHash(content) { - // XXX Improve performance by moving calculation to WebWorker - return rusha.digestFromString(content); - } - - function generateHashFromArrayBuffer(content) { - // XXX Improve performance by moving calculation to WebWorker - return rusha.digestFromArrayBuffer(content); - } - - function ReplicateStorage(spec) { - this._query_options = spec.query || {}; - if (spec.signature_hash_key !== undefined) { - this._query_options.select_list = [spec.signature_hash_key]; - } - this._signature_hash_key = spec.signature_hash_key; - - this._local_sub_storage = jIO.createJIO(spec.local_sub_storage); - this._remote_sub_storage = jIO.createJIO(spec.remote_sub_storage); - - if (spec.hasOwnProperty('signature_sub_storage')) { - this._signature_sub_storage = jIO.createJIO(spec.signature_sub_storage); - this._custom_signature_sub_storage = true; - } else { - this._signature_hash = "_replicate_" + generateHash( - stringify(spec.local_sub_storage) + - stringify(spec.remote_sub_storage) + - stringify(this._query_options) - ); - this._signature_sub_storage = jIO.createJIO({ - type: "query", - sub_storage: { - type: "document", - document_id: this._signature_hash, - sub_storage: spec.local_sub_storage - } - }); - this._custom_signature_sub_storage = false; - } - - this._use_remote_post = spec.use_remote_post || false; - // Number of request we allow browser execution for attachments - this._parallel_operation_attachment_amount = - spec.parallel_operation_attachment_amount || 1; - // Number of request we allow browser execution for documents - this._parallel_operation_amount = - spec.parallel_operation_amount || 1; - - this._conflict_handling = spec.conflict_handling || 0; - // 0: no resolution (ie, throw an Error) - // 1: keep the local state - // (overwrites the remote document with local content) - // (delete remote document if local is deleted) - // 2: keep the remote state - // (overwrites the local document with remote content) - // (delete local document if remote is deleted) - // 3: keep both copies (leave documents untouched, no signature update) - if ((this._conflict_handling !== CONFLICT_THROW) && - (this._conflict_handling !== CONFLICT_KEEP_LOCAL) && - (this._conflict_handling !== CONFLICT_KEEP_REMOTE) && - (this._conflict_handling !== CONFLICT_CONTINUE)) { - throw new jIO.util.jIOError("Unsupported conflict handling: " + - this._conflict_handling, 400); - } - - this._check_local_modification = spec.check_local_modification; - if (this._check_local_modification === undefined) { - this._check_local_modification = true; - } - this._check_local_creation = spec.check_local_creation; - if (this._check_local_creation === undefined) { - this._check_local_creation = true; - } - this._check_local_deletion = spec.check_local_deletion; - if (this._check_local_deletion === undefined) { - this._check_local_deletion = true; - } - this._check_remote_modification = spec.check_remote_modification; - if (this._check_remote_modification === undefined) { - this._check_remote_modification = true; - } - this._check_remote_creation = spec.check_remote_creation; - if (this._check_remote_creation === undefined) { - this._check_remote_creation = true; - } - this._check_remote_deletion = spec.check_remote_deletion; - if (this._check_remote_deletion === undefined) { - this._check_remote_deletion = true; - } - this._check_local_attachment_modification = - spec.check_local_attachment_modification; - if (this._check_local_attachment_modification === undefined) { - this._check_local_attachment_modification = false; - } - this._check_local_attachment_creation = - spec.check_local_attachment_creation; - if (this._check_local_attachment_creation === undefined) { - this._check_local_attachment_creation = false; - } - this._check_local_attachment_deletion = - spec.check_local_attachment_deletion; - if (this._check_local_attachment_deletion === undefined) { - this._check_local_attachment_deletion = false; - } - this._check_remote_attachment_modification = - spec.check_remote_attachment_modification; - if (this._check_remote_attachment_modification === undefined) { - this._check_remote_attachment_modification = false; - } - this._check_remote_attachment_creation = - spec.check_remote_attachment_creation; - if (this._check_remote_attachment_creation === undefined) { - this._check_remote_attachment_creation = false; - } - this._check_remote_attachment_deletion = - spec.check_remote_attachment_deletion; - if (this._check_remote_attachment_deletion === undefined) { - this._check_remote_attachment_deletion = false; - } - } - - ReplicateStorage.prototype.remove = function (id) { - if (id === this._signature_hash) { - throw new jIO.util.jIOError(this._signature_hash + " is frozen", - 403); - } - return this._local_sub_storage.remove.apply(this._local_sub_storage, - arguments); - }; - ReplicateStorage.prototype.post = function () { - return this._local_sub_storage.post.apply(this._local_sub_storage, - arguments); - }; - ReplicateStorage.prototype.put = function (id) { - if (id === this._signature_hash) { - throw new jIO.util.jIOError(this._signature_hash + " is frozen", - 403); - } - return this._local_sub_storage.put.apply(this._local_sub_storage, - arguments); - }; - ReplicateStorage.prototype.get = function () { - return this._local_sub_storage.get.apply(this._local_sub_storage, - arguments); - }; - ReplicateStorage.prototype.getAttachment = function () { - return this._local_sub_storage.getAttachment.apply(this._local_sub_storage, - arguments); - }; - ReplicateStorage.prototype.allAttachments = function () { - return this._local_sub_storage.allAttachments.apply(this._local_sub_storage, - arguments); - }; - ReplicateStorage.prototype.putAttachment = function (id) { - if (id === this._signature_hash) { - throw new jIO.util.jIOError(this._signature_hash + " is frozen", - 403); - } - return this._local_sub_storage.putAttachment.apply(this._local_sub_storage, - arguments); - }; - ReplicateStorage.prototype.removeAttachment = function (id) { - if (id === this._signature_hash) { - throw new jIO.util.jIOError(this._signature_hash + " is frozen", - 403); - } - return this._local_sub_storage.removeAttachment.apply( - this._local_sub_storage, - arguments - ); - }; - ReplicateStorage.prototype.hasCapacity = function () { - return this._local_sub_storage.hasCapacity.apply(this._local_sub_storage, - arguments); - }; - ReplicateStorage.prototype.buildQuery = function () { - // XXX Remove signature document? - return this._local_sub_storage.buildQuery.apply(this._local_sub_storage, - arguments); - }; - - function dispatchQueue(context, function_used, argument_list, - number_queue) { - var result_promise_list = [], - i; - - function pushAndExecute(queue) { - queue - .push(function () { - if (argument_list.length > 0) { - var argument_array = argument_list.shift(), - sub_queue = new RSVP.Queue(); - argument_array[0] = sub_queue; - function_used.apply(context, argument_array); - pushAndExecute(queue); - return sub_queue; - } - }); - } - for (i = 0; i < number_queue; i += 1) { - result_promise_list.push(new RSVP.Queue()); - pushAndExecute(result_promise_list[i]); - } - if (number_queue > 1) { - return RSVP.all(result_promise_list); - } - return result_promise_list[0]; - } - - function callAllDocsOnStorage(context, storage, cache, cache_key) { - return new RSVP.Queue() - .push(function () { - if (!cache.hasOwnProperty(cache_key)) { - return storage.allDocs(context._query_options) - .push(function (result) { - var i, - cache_entry = {}; - for (i = 0; i < result.data.total_rows; i += 1) { - cache_entry[result.data.rows[i].id] = result.data.rows[i].value; - } - cache[cache_key] = cache_entry; - }); - } - }) - .push(function () { - return cache[cache_key]; - }); - } - - function propagateAttachmentDeletion(context, skip_attachment_dict, - destination, - id, name) { - return destination.removeAttachment(id, name) - .push(function () { - return context._signature_sub_storage.removeAttachment(id, name); - }) - .push(function () { - skip_attachment_dict[name] = null; - }); - } - - function propagateAttachmentModification(context, skip_attachment_dict, - destination, - blob, hash, id, name) { - return destination.putAttachment(id, name, blob) - .push(function () { - return context._signature_sub_storage.putAttachment(id, name, - JSON.stringify({ - hash: hash - })); - }) - .push(function () { - skip_attachment_dict[name] = null; - }); - } - - function checkAndPropagateAttachment(context, - skip_attachment_dict, - status_hash, local_hash, blob, - source, destination, id, name, - conflict_force, conflict_revert, - conflict_ignore) { - var remote_blob; - return destination.getAttachment(id, name) - .push(function (result) { - remote_blob = result; - return jIO.util.readBlobAsArrayBuffer(remote_blob); - }) - .push(function (evt) { - return generateHashFromArrayBuffer( - evt.target.result - ); - }, function (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - remote_blob = null; - return null; - } - throw error; - }) - .push(function (remote_hash) { - if (local_hash === remote_hash) { - // Same modifications on both side - if (local_hash === null) { - // Deleted on both side, drop signature - return context._signature_sub_storage.removeAttachment(id, name) - .push(function () { - skip_attachment_dict[name] = null; - }); - } - - return context._signature_sub_storage.putAttachment(id, name, - JSON.stringify({ - hash: local_hash - })) - .push(function () { - skip_attachment_dict[name] = null; - }); - } - - if ((remote_hash === status_hash) || (conflict_force === true)) { - // Modified only locally. No conflict or force - if (local_hash === null) { - // Deleted locally - return propagateAttachmentDeletion(context, skip_attachment_dict, - destination, - id, name); - } - return propagateAttachmentModification(context, - skip_attachment_dict, - destination, blob, - local_hash, id, name); - } - - // Conflict cases - if (conflict_ignore === true) { - return; - } - - if ((conflict_revert === true) || (local_hash === null)) { - // Automatically resolve conflict or force revert - if (remote_hash === null) { - // Deleted remotely - return propagateAttachmentDeletion(context, skip_attachment_dict, - source, id, name); - } - return propagateAttachmentModification( - context, - skip_attachment_dict, - source, - remote_blob, - remote_hash, - id, - name - ); - } - - // Minimize conflict if it can be resolved - if (remote_hash === null) { - // Copy remote modification remotely - return propagateAttachmentModification(context, - skip_attachment_dict, - destination, blob, - local_hash, id, name); - } - throw new jIO.util.jIOError("Conflict on '" + id + - "' with attachment '" + - name + "'", - 409); - }); - } - - function checkAttachmentSignatureDifference(queue, context, - skip_attachment_dict, - source, - destination, id, name, - conflict_force, - conflict_revert, - conflict_ignore, - is_creation, is_modification) { - var blob, - status_hash; - queue - .push(function () { - // Optimisation to save a get call to signature storage - if (is_creation === true) { - return RSVP.all([ - source.getAttachment(id, name), - {hash: null} - ]); - } - if (is_modification === true) { - return RSVP.all([ - source.getAttachment(id, name), - context._signature_sub_storage.getAttachment( - id, - name, - {format: 'json'} - ) - ]); - } - throw new jIO.util.jIOError("Unexpected call of" - + " checkAttachmentSignatureDifference", - 409); - }) - .push(function (result_list) { - blob = result_list[0]; - status_hash = result_list[1].hash; - return jIO.util.readBlobAsArrayBuffer(blob); - }) - .push(function (evt) { - var array_buffer = evt.target.result, - local_hash = generateHashFromArrayBuffer(array_buffer); - - if (local_hash !== status_hash) { - return checkAndPropagateAttachment(context, - skip_attachment_dict, - status_hash, local_hash, blob, - source, destination, id, name, - conflict_force, conflict_revert, - conflict_ignore); - } - }); - } - - function checkAttachmentLocalDeletion(queue, context, - skip_attachment_dict, - destination, id, name, source, - conflict_force, conflict_revert, - conflict_ignore) { - var status_hash; - queue - .push(function () { - return context._signature_sub_storage.getAttachment(id, name, - {format: 'json'}); - }) - .push(function (result) { - status_hash = result.hash; - return checkAndPropagateAttachment(context, - skip_attachment_dict, - status_hash, null, null, - source, destination, id, name, - conflict_force, conflict_revert, - conflict_ignore); - }); - } - - function pushDocumentAttachment(context, - skip_attachment_dict, id, source, - destination, signature_allAttachments, - options) { - var local_dict = {}, - signature_dict = {}; - return source.allAttachments(id) - .push(undefined, function (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - return {}; - } - throw error; - }) - .push(function (source_allAttachments) { - var is_modification, - is_creation, - key, - argument_list = []; - for (key in source_allAttachments) { - if (source_allAttachments.hasOwnProperty(key)) { - if (!skip_attachment_dict.hasOwnProperty(key)) { - local_dict[key] = null; - } - } - } - for (key in signature_allAttachments) { - if (signature_allAttachments.hasOwnProperty(key)) { - if (!skip_attachment_dict.hasOwnProperty(key)) { - signature_dict[key] = null; - } - } - } - - for (key in local_dict) { - if (local_dict.hasOwnProperty(key)) { - is_modification = signature_dict.hasOwnProperty(key) - && options.check_modification; - is_creation = !signature_dict.hasOwnProperty(key) - && options.check_creation; - if (is_modification === true || is_creation === true) { - argument_list.push([undefined, - context, - skip_attachment_dict, - source, - destination, id, key, - options.conflict_force, - options.conflict_revert, - options.conflict_ignore, - is_creation, - is_modification]); - } - } - } - return dispatchQueue( - context, - checkAttachmentSignatureDifference, - argument_list, - context._parallel_operation_attachment_amount - ); - }) - .push(function () { - var key, argument_list = []; - if (options.check_deletion === true) { - for (key in signature_dict) { - if (signature_dict.hasOwnProperty(key)) { - if (!local_dict.hasOwnProperty(key)) { - argument_list.push([undefined, - context, - skip_attachment_dict, - destination, id, key, - source, - options.conflict_force, - options.conflict_revert, - options.conflict_ignore]); - } - } - } - return dispatchQueue( - context, - checkAttachmentLocalDeletion, - argument_list, - context._parallel_operation_attachment_amount - ); - } - }); - } - - function propagateFastAttachmentDeletion(queue, id, name, storage) { - return queue - .push(function () { - return storage.removeAttachment(id, name); - }); - } - - function propagateFastAttachmentModification(queue, id, key, source, - destination, signature, hash) { - return queue - .push(function () { - return signature.getAttachment(id, key, {format: 'json'}) - .push(undefined, function (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - return {hash: null}; - } - throw error; - }) - .push(function (result) { - if (result.hash !== hash) { - return source.getAttachment(id, key) - .push(function (blob) { - return destination.putAttachment(id, key, blob); - }) - .push(function () { - return signature.putAttachment(id, key, JSON.stringify({ - hash: hash - })); - }); - } - }); - - }); - } - - function repairFastDocumentAttachment(context, id, - signature_hash, - signature_attachment_hash, - signature_from_local) { - if (signature_hash === signature_attachment_hash) { - // No replication to do - return; - } - return new RSVP.Queue() - .push(function () { - return RSVP.all([ - context._signature_sub_storage.allAttachments(id), - context._local_sub_storage.allAttachments(id), - context._remote_sub_storage.allAttachments(id) - ]); - }) - .push(function (result_list) { - var key, - source_attachment_dict, - destination_attachment_dict, - source, - destination, - push_argument_list = [], - delete_argument_list = [], - signature_attachment_dict = result_list[0], - local_attachment_dict = result_list[1], - remote_attachment_list = result_list[2], - check_local_modification = - context._check_local_attachment_modification, - check_local_creation = context._check_local_attachment_creation, - check_local_deletion = context._check_local_attachment_deletion, - check_remote_modification = - context._check_remote_attachment_modification, - check_remote_creation = context._check_remote_attachment_creation, - check_remote_deletion = context._check_remote_attachment_deletion; - - if (signature_from_local) { - source_attachment_dict = local_attachment_dict; - destination_attachment_dict = remote_attachment_list; - source = context._local_sub_storage; - destination = context._remote_sub_storage; - } else { - source_attachment_dict = remote_attachment_list; - destination_attachment_dict = local_attachment_dict; - source = context._remote_sub_storage; - destination = context._local_sub_storage; - check_local_modification = check_remote_modification; - check_local_creation = check_remote_creation; - check_local_deletion = check_remote_deletion; - check_remote_creation = check_local_creation; - check_remote_deletion = check_local_deletion; - } - - // Push all source attachments - for (key in source_attachment_dict) { - if (source_attachment_dict.hasOwnProperty(key)) { - - if ((check_local_creation && - !signature_attachment_dict.hasOwnProperty(key)) || - (check_local_modification && - signature_attachment_dict.hasOwnProperty(key))) { - push_argument_list.push([ - undefined, - id, - key, - source, - destination, - context._signature_sub_storage, - signature_hash - ]); - } - } - } - - // Delete remaining signature + remote attachments - for (key in signature_attachment_dict) { - if (signature_attachment_dict.hasOwnProperty(key)) { - if (check_local_deletion && - !source_attachment_dict.hasOwnProperty(key)) { - delete_argument_list.push([ - undefined, - id, - key, - context._signature_sub_storage - ]); - } - } - } - for (key in destination_attachment_dict) { - if (destination_attachment_dict.hasOwnProperty(key)) { - if (!source_attachment_dict.hasOwnProperty(key)) { - if ((check_local_deletion && - signature_attachment_dict.hasOwnProperty(key)) || - (check_remote_creation && - !signature_attachment_dict.hasOwnProperty(key))) { - delete_argument_list.push([ - undefined, - id, - key, - destination - ]); - } - } - } - } - - return RSVP.all([ - dispatchQueue( - context, - propagateFastAttachmentModification, - push_argument_list, - context._parallel_operation_attachment_amount - ), - dispatchQueue( - context, - propagateFastAttachmentDeletion, - delete_argument_list, - context._parallel_operation_attachment_amount - ) - ]); - }) - .push(function () { - // Mark that all attachments have been synchronized - return context._signature_sub_storage.put(id, { - hash: signature_hash, - attachment_hash: signature_hash, - from_local: signature_from_local - }); - }); - } - - function repairDocumentAttachment(context, id, signature_hash_key, - signature_hash, - signature_attachment_hash, - signature_from_local) { - if (signature_hash_key !== undefined) { - return repairFastDocumentAttachment(context, id, - signature_hash, - signature_attachment_hash, - signature_from_local); - } - - var skip_attachment_dict = {}; - return new RSVP.Queue() - .push(function () { - if (context._check_local_attachment_modification || - context._check_local_attachment_creation || - context._check_local_attachment_deletion || - context._check_remote_attachment_modification || - context._check_remote_attachment_creation || - context._check_remote_attachment_deletion) { - return context._signature_sub_storage.allAttachments(id); - } - return {}; - }) - .push(undefined, function (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - return {}; - } - throw error; - }) - .push(function (signature_allAttachments) { - if (context._check_local_attachment_modification || - context._check_local_attachment_creation || - context._check_local_attachment_deletion) { - return pushDocumentAttachment( - context, - skip_attachment_dict, - id, - context._local_sub_storage, - context._remote_sub_storage, - signature_allAttachments, - { - conflict_force: (context._conflict_handling === - CONFLICT_KEEP_LOCAL), - conflict_revert: (context._conflict_handling === - CONFLICT_KEEP_REMOTE), - conflict_ignore: (context._conflict_handling === - CONFLICT_CONTINUE), - check_modification: - context._check_local_attachment_modification, - check_creation: context._check_local_attachment_creation, - check_deletion: context._check_local_attachment_deletion - } - ) - .push(function () { - return signature_allAttachments; - }); - } - return signature_allAttachments; - }) - .push(function (signature_allAttachments) { - if (context._check_remote_attachment_modification || - context._check_remote_attachment_creation || - context._check_remote_attachment_deletion) { - return pushDocumentAttachment( - context, - skip_attachment_dict, - id, - context._remote_sub_storage, - context._local_sub_storage, - signature_allAttachments, - { - use_revert_post: context._use_remote_post, - conflict_force: (context._conflict_handling === - CONFLICT_KEEP_REMOTE), - conflict_revert: (context._conflict_handling === - CONFLICT_KEEP_LOCAL), - conflict_ignore: (context._conflict_handling === - CONFLICT_CONTINUE), - check_modification: - context._check_remote_attachment_modification, - check_creation: context._check_remote_attachment_creation, - check_deletion: context._check_remote_attachment_deletion - } - ); - } - }); - } - - function propagateModification(context, source, destination, doc, hash, id, - skip_document_dict, - skip_deleted_document_dict, - options) { - var result = new RSVP.Queue(), - post_id, - to_skip = true, - from_local; - if (options === undefined) { - options = {}; - } - from_local = options.from_local; - - if (doc === null) { - result - .push(function () { - return source.get(id); - }) - .push(function (source_doc) { - doc = source_doc; - }, function (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - throw new SkipError(id); - } - throw error; - }); - } - if (options.use_post) { - result - .push(function () { - return destination.post(doc); - }) - .push(function (new_id) { - to_skip = false; - post_id = new_id; - return source.put(post_id, doc); - }) - .push(function () { - // Copy all attachments - // This is not related to attachment replication - // It's just about not losing user data - return source.allAttachments(id); - }) - .push(function (attachment_dict) { - var key, - copy_queue = new RSVP.Queue(); - - function copyAttachment(name) { - copy_queue - .push(function () { - return source.getAttachment(id, name); - }) - .push(function (blob) { - return source.putAttachment(post_id, name, blob); - }); - } - - for (key in attachment_dict) { - if (attachment_dict.hasOwnProperty(key)) { - copyAttachment(key); - } - } - return copy_queue; - }) - .push(function () { - return source.remove(id); - }) - .push(function () { - return context._signature_sub_storage.remove(id); - }) - .push(function () { - to_skip = true; - return context._signature_sub_storage.put(post_id, { - hash: hash, - from_local: from_local - }); - }) - .push(function () { - skip_document_dict[post_id] = null; - }); - } else { - result - .push(function () { - // Drop signature if the destination document was empty - // but a signature exists - if (options.create_new_document === true) { - delete skip_deleted_document_dict[id]; - return context._signature_sub_storage.remove(id); - } - }) - .push(function () { - return destination.put(id, doc); - }) - .push(function () { - return context._signature_sub_storage.put(id, { - hash: hash, - from_local: from_local - }); - }); - } - return result - .push(function () { - if (to_skip) { - skip_document_dict[id] = null; - } - }) - .push(undefined, function (error) { - if (error instanceof SkipError) { - return; - } - throw error; - }); - } - - function propagateDeletion(context, destination, id, skip_document_dict, - skip_deleted_document_dict) { - // Do not delete a document if it has an attachment - // ie, replication should prevent losing user data - // Synchronize attachments before, to ensure - // all of them will be deleted too - var result; - if (context._signature_hash_key !== undefined) { - result = destination.remove(id) - .push(function () { - return context._signature_sub_storage.remove(id); - }); - } else { - result = repairDocumentAttachment(context, id) - .push(function () { - return destination.allAttachments(id); - }) - .push(function (attachment_dict) { - if (JSON.stringify(attachment_dict) === "{}") { - return destination.remove(id) - .push(function () { - return context._signature_sub_storage.remove(id); - }); - } - }, function (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - return; - } - throw error; - }); - } - return result - .push(function () { - skip_document_dict[id] = null; - // No need to sync attachment twice on this document - skip_deleted_document_dict[id] = null; - }); - } - - function checkAndPropagate(context, skip_document_dict, - skip_deleted_document_dict, - cache, destination_key, - status_hash, local_hash, doc, - source, destination, id, - conflict_force, conflict_revert, - conflict_ignore, - options) { - var from_local = options.from_local; - return new RSVP.Queue() - .push(function () { - if (options.signature_hash_key !== undefined) { - return callAllDocsOnStorage(context, destination, - cache, destination_key) - .push(function (result) { - if (result.hasOwnProperty(id)) { - return [null, result[id][options.signature_hash_key]]; - } - return [null, null]; - }); - } - return destination.get(id) - .push(function (remote_doc) { - return [remote_doc, generateHash(stringify(remote_doc))]; - }, function (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - return [null, null]; - } - throw error; - }); - }) - - .push(function (remote_list) { - var remote_doc = remote_list[0], - remote_hash = remote_list[1]; - if (local_hash === remote_hash) { - // Same modifications on both side - if (local_hash === null) { - // Deleted on both side, drop signature - return context._signature_sub_storage.remove(id) - .push(function () { - skip_document_dict[id] = null; - }); - } - - return context._signature_sub_storage.put(id, { - hash: local_hash, - from_local: from_local - }) - .push(function () { - skip_document_dict[id] = null; - }); - } - - if ((remote_hash === status_hash) || (conflict_force === true)) { - // Modified only locally. No conflict or force - if (local_hash === null) { - // Deleted locally - return propagateDeletion(context, destination, id, - skip_document_dict, - skip_deleted_document_dict); - } - return propagateModification(context, source, destination, doc, - local_hash, id, skip_document_dict, - skip_deleted_document_dict, - {use_post: ((options.use_post) && - (remote_hash === null)), - from_local: from_local, - create_new_document: - ((remote_hash === null) && - (status_hash !== null)) - }); - } - - // Conflict cases - if (conflict_ignore === true) { - return; - } - - if ((conflict_revert === true) || (local_hash === null)) { - // Automatically resolve conflict or force revert - if (remote_hash === null) { - // Deleted remotely - return propagateDeletion(context, source, id, skip_document_dict, - skip_deleted_document_dict); - } - return propagateModification( - context, - destination, - source, - remote_doc, - remote_hash, - id, - skip_document_dict, - skip_deleted_document_dict, - {use_post: ((options.use_revert_post) && - (local_hash === null)), - from_local: !from_local, - create_new_document: ((local_hash === null) && - (status_hash !== null))} - ); - } - - // Minimize conflict if it can be resolved - if (remote_hash === null) { - // Copy remote modification remotely - return propagateModification(context, source, destination, doc, - local_hash, id, skip_document_dict, - skip_deleted_document_dict, - {use_post: options.use_post, - from_local: from_local, - create_new_document: - (status_hash !== null)}); - } - doc = doc || local_hash; - remote_doc = remote_doc || remote_hash; - throw new jIO.util.jIOError("Conflict on '" + id + "': " + - stringify(doc) + " !== " + - stringify(remote_doc), - 409); - }); - } - - function checkLocalDeletion(queue, context, skip_document_dict, - skip_deleted_document_dict, - cache, destination_key, - destination, id, source, - conflict_force, conflict_revert, - conflict_ignore, options) { - var status_hash; - queue - .push(function () { - return context._signature_sub_storage.get(id); - }) - .push(function (result) { - status_hash = result.hash; - return checkAndPropagate(context, skip_document_dict, - skip_deleted_document_dict, - cache, destination_key, - status_hash, null, null, - source, destination, id, - conflict_force, conflict_revert, - conflict_ignore, - options); - }); - } - - function checkSignatureDifference(queue, context, skip_document_dict, - skip_deleted_document_dict, - cache, destination_key, - source, destination, id, - conflict_force, conflict_revert, - conflict_ignore, - local_hash, status_hash, - options) { - queue - .push(function () { - if (local_hash === null) { - // Hash was not provided by the allDocs query - return source.get(id); - } - return null; - }) - .push(function (doc) { - if (local_hash === null) { - // Hash was not provided by the allDocs query - local_hash = generateHash(stringify(doc)); - } - - if (local_hash !== status_hash) { - return checkAndPropagate(context, skip_document_dict, - skip_deleted_document_dict, - cache, destination_key, - status_hash, local_hash, doc, - source, destination, id, - conflict_force, conflict_revert, - conflict_ignore, - options); - } - }); - } - - function pushStorage(context, skip_document_dict, - skip_deleted_document_dict, - cache, source_key, destination_key, - source, destination, signature_allDocs, options) { - var argument_list = [], - argument_list_deletion = []; - if (!options.hasOwnProperty("use_post")) { - options.use_post = false; - } - if (!options.hasOwnProperty("use_revert_post")) { - options.use_revert_post = false; - } - return callAllDocsOnStorage(context, source, cache, source_key) - .push(function (source_allDocs) { - var i, - local_dict = {}, - signature_dict = {}, - is_modification, - is_creation, - status_hash, - local_hash, - key, - queue = new RSVP.Queue(); - for (key in source_allDocs) { - if (source_allDocs.hasOwnProperty(key)) { - if (!skip_document_dict.hasOwnProperty(key)) { - local_dict[key] = source_allDocs[key]; - } - } - } - /* - for (i = 0; i < source_allDocs.data.total_rows; i += 1) { - if (!skip_document_dict.hasOwnProperty( - source_allDocs.data.rows[i].id - )) { - local_dict[source_allDocs.data.rows[i].id] = - source_allDocs.data.rows[i].value; - } - } - */ - for (i = 0; i < signature_allDocs.data.total_rows; i += 1) { - if (!skip_document_dict.hasOwnProperty( - signature_allDocs.data.rows[i].id - )) { - signature_dict[signature_allDocs.data.rows[i].id] = - signature_allDocs.data.rows[i].value.hash; - } - } - for (key in local_dict) { - if (local_dict.hasOwnProperty(key)) { - is_modification = signature_dict.hasOwnProperty(key) - && options.check_modification; - is_creation = !signature_dict.hasOwnProperty(key) - && options.check_creation; - - if (is_creation === true) { - status_hash = null; - } else if (is_modification === true) { - status_hash = signature_dict[key]; - } - - local_hash = null; - if (options.signature_hash_key !== undefined) { - local_hash = local_dict[key][options.signature_hash_key]; - if (is_modification === true) { - // Bypass fetching all documents and calculating the sha - // Compare the select list values returned by allDocs calls - is_modification = false; - if (local_hash !== status_hash) { - is_modification = true; - } - } - } - - if (is_modification === true || is_creation === true) { - argument_list.push([undefined, context, skip_document_dict, - skip_deleted_document_dict, - cache, destination_key, - source, destination, - key, - options.conflict_force, - options.conflict_revert, - options.conflict_ignore, - local_hash, status_hash, - options]); - } - } - } - queue - .push(function () { - return dispatchQueue( - context, - checkSignatureDifference, - argument_list, - options.operation_amount - ); - }); - for (key in signature_dict) { - if (signature_dict.hasOwnProperty(key)) { - if (!local_dict.hasOwnProperty(key)) { - if (options.check_deletion === true) { - argument_list_deletion.push([undefined, - context, - skip_document_dict, - skip_deleted_document_dict, - cache, destination_key, - destination, key, - source, - options.conflict_force, - options.conflict_revert, - options.conflict_ignore, - options]); - } else { - skip_deleted_document_dict[key] = null; - } - } - } - } - if (argument_list_deletion.length !== 0) { - queue.push(function () { - return dispatchQueue( - context, - checkLocalDeletion, - argument_list_deletion, - options.operation_amount - ); - }); - } - return queue; - }); - } - - function repairDocument(queue, context, id, signature_hash_key, - signature_hash, signature_attachment_hash, - signature_from_local) { - queue.push(function () { - return repairDocumentAttachment(context, id, signature_hash_key, - signature_hash, - signature_attachment_hash, - signature_from_local); - }); - } - - ReplicateStorage.prototype.repair = function () { - var context = this, - argument_list = arguments, - skip_document_dict = {}, - skip_deleted_document_dict = {}, - cache = {}; - - return new RSVP.Queue() - .push(function () { - // Ensure that the document storage is usable - if (context._custom_signature_sub_storage === false) { - // Do not sync the signature document - skip_document_dict[context._signature_hash] = null; - - return context._signature_sub_storage.__storage._sub_storage - .__storage._sub_storage.get( - context._signature_hash - ); - } - }) - .push(undefined, function (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - return context._signature_sub_storage.__storage._sub_storage - .__storage._sub_storage.put( - context._signature_hash, - {} - ); - } - throw error; - }) - - .push(function () { - return RSVP.all([ -// Don't repair local_sub_storage twice -// context._signature_sub_storage.repair.apply( -// context._signature_sub_storage, -// argument_list -// ), - context._local_sub_storage.repair.apply( - context._local_sub_storage, - argument_list - ), - context._remote_sub_storage.repair.apply( - context._remote_sub_storage, - argument_list - ) - ]); - }) - - .push(function () { - if (context._check_local_modification || - context._check_local_creation || - context._check_local_deletion || - context._check_remote_modification || - context._check_remote_creation || - context._check_remote_deletion) { - return context._signature_sub_storage.allDocs({ - select_list: ['hash'] - }); - } - }) - - .push(function (signature_allDocs) { - if (context._check_local_modification || - context._check_local_creation || - context._check_local_deletion) { - return pushStorage(context, skip_document_dict, - skip_deleted_document_dict, - cache, 'local', 'remote', - context._local_sub_storage, - context._remote_sub_storage, - signature_allDocs, - { - use_post: context._use_remote_post, - conflict_force: (context._conflict_handling === - CONFLICT_KEEP_LOCAL), - conflict_revert: (context._conflict_handling === - CONFLICT_KEEP_REMOTE), - conflict_ignore: (context._conflict_handling === - CONFLICT_CONTINUE), - check_modification: context._check_local_modification, - check_creation: context._check_local_creation, - check_deletion: context._check_local_deletion, - operation_amount: context._parallel_operation_amount, - signature_hash_key: context._signature_hash_key, - from_local: true - }) - .push(function () { - return signature_allDocs; - }); - } - return signature_allDocs; - }) - .push(function (signature_allDocs) { - if (context._check_remote_modification || - context._check_remote_creation || - context._check_remote_deletion) { - return pushStorage(context, skip_document_dict, - skip_deleted_document_dict, - cache, 'remote', 'local', - context._remote_sub_storage, - context._local_sub_storage, - signature_allDocs, { - use_revert_post: context._use_remote_post, - conflict_force: (context._conflict_handling === - CONFLICT_KEEP_REMOTE), - conflict_revert: (context._conflict_handling === - CONFLICT_KEEP_LOCAL), - conflict_ignore: (context._conflict_handling === - CONFLICT_CONTINUE), - check_modification: context._check_remote_modification, - check_creation: context._check_remote_creation, - check_deletion: context._check_remote_deletion, - operation_amount: context._parallel_operation_amount, - signature_hash_key: context._signature_hash_key, - from_local: false - }); - } - }) - .push(function () { - if (context._check_local_attachment_modification || - context._check_local_attachment_creation || - context._check_local_attachment_deletion || - context._check_remote_attachment_modification || - context._check_remote_attachment_creation || - context._check_remote_attachment_deletion) { - // Attachments are synchronized if and only if their parent document - // has been also marked as synchronized. - return context._signature_sub_storage.allDocs({ - select_list: ['hash', 'attachment_hash', 'from_local'] - }) - .push(function (result) { - var i, - local_argument_list = [], - row, - len = result.data.total_rows; - - for (i = 0; i < len; i += 1) { - row = result.data.rows[i]; - // Do not synchronize attachment if one version of the document - // is deleted but not pushed to the other storage - if (!skip_deleted_document_dict.hasOwnProperty(row.id)) { - local_argument_list.push( - [undefined, context, row.id, context._signature_hash_key, - row.value.hash, row.value.attachment_hash, - row.value.from_local] - ); - } - } - return dispatchQueue( - context, - repairDocument, - local_argument_list, - context._parallel_operation_amount - ); - }); - } - }); - }; - - jIO.addStorage('replicate', ReplicateStorage); - -}(jIO, RSVP, Rusha, jIO.util.stringify)); -;/* - * Copyright 2015, Nexedi SA - * Released under the LGPL license. - * http://www.gnu.org/licenses/lgpl.html - */ - -/*jslint nomen: true*/ -/*global Rusha*/ - -/** - * JIO Sha Storage. Type = 'sha'. - */ - -(function (Rusha) { - "use strict"; - - var rusha = new Rusha(); - - function ShaStorage(spec) { - this._sub_storage = jIO.createJIO(spec.sub_storage); - } - - ShaStorage.prototype.post = function (param) { - return this._sub_storage.put( - rusha.digestFromString(JSON.stringify(param)), - param - ); - }; - - ShaStorage.prototype.get = function () { - return this._sub_storage.get.apply(this._sub_storage, arguments); - }; - ShaStorage.prototype.remove = function () { - return this._sub_storage.remove.apply(this._sub_storage, arguments); - }; - ShaStorage.prototype.hasCapacity = function () { - return this._sub_storage.hasCapacity.apply(this._sub_storage, arguments); - }; - ShaStorage.prototype.buildQuery = function () { - return this._sub_storage.buildQuery.apply(this._sub_storage, arguments); - }; - ShaStorage.prototype.getAttachment = function () { - return this._sub_storage.getAttachment.apply(this._sub_storage, arguments); - }; - ShaStorage.prototype.putAttachment = function () { - return this._sub_storage.putAttachment.apply(this._sub_storage, arguments); - }; - ShaStorage.prototype.removeAttachment = function () { - return this._sub_storage.removeAttachment.apply(this._sub_storage, - arguments); - }; - ShaStorage.prototype.allAttachments = function () { - return this._sub_storage.allAttachments.apply(this._sub_storage, arguments); - }; - ShaStorage.prototype.repair = function () { - return this._sub_storage.repair.apply(this._sub_storage, arguments); - }; - - jIO.addStorage('sha', ShaStorage); - -}(Rusha)); -;/*jslint nomen: true*/ -(function (jIO) { - "use strict"; - - /** - * The jIO UUIDStorage extension - * - * @class UUIDStorage - * @constructor - */ - function UUIDStorage(spec) { - this._sub_storage = jIO.createJIO(spec.sub_storage); - } - - UUIDStorage.prototype.get = function () { - return this._sub_storage.get.apply(this._sub_storage, arguments); - }; - UUIDStorage.prototype.allAttachments = function () { - return this._sub_storage.allAttachments.apply(this._sub_storage, arguments); - }; - UUIDStorage.prototype.post = function (param) { - - function S4() { - return ('0000' + Math.floor( - Math.random() * 0x10000 /* 65536 */ - ).toString(16)).slice(-4); - } - - var id = S4() + S4() + "-" + - S4() + "-" + - S4() + "-" + - S4() + "-" + - S4() + S4() + S4(); - - return this.put(id, param); - }; - UUIDStorage.prototype.put = function () { - return this._sub_storage.put.apply(this._sub_storage, arguments); - }; - UUIDStorage.prototype.remove = function () { - return this._sub_storage.remove.apply(this._sub_storage, arguments); - }; - UUIDStorage.prototype.getAttachment = function () { - return this._sub_storage.getAttachment.apply(this._sub_storage, arguments); - }; - UUIDStorage.prototype.putAttachment = function () { - return this._sub_storage.putAttachment.apply(this._sub_storage, arguments); - }; - UUIDStorage.prototype.removeAttachment = function () { - return this._sub_storage.removeAttachment.apply(this._sub_storage, - arguments); - }; - UUIDStorage.prototype.repair = function () { - return this._sub_storage.repair.apply(this._sub_storage, arguments); - }; - UUIDStorage.prototype.hasCapacity = function (name) { - return this._sub_storage.hasCapacity(name); - }; - UUIDStorage.prototype.buildQuery = function () { - return this._sub_storage.buildQuery.apply(this._sub_storage, - arguments); - }; - - jIO.addStorage('uuid', UUIDStorage); - -}(jIO)); -;/* - * Copyright 2013, Nexedi SA - * Released under the LGPL license. - * http://www.gnu.org/licenses/lgpl.html - */ - -/*jslint nomen: true*/ -/*global jIO, RSVP*/ - -/** - * JIO Memory Storage. Type = 'memory'. - * Memory browser "database" storage. - * - * Storage Description: - * - * { - * "type": "memory" - * } - * - * @class MemoryStorage - */ - -(function (jIO, JSON, RSVP) { - "use strict"; - - /** - * The JIO MemoryStorage extension - * - * @class MemoryStorage - * @constructor - */ - function MemoryStorage() { - this._database = {}; - } - - MemoryStorage.prototype.put = function (id, metadata) { - if (!this._database.hasOwnProperty(id)) { - this._database[id] = { - attachments: {} - }; - } - this._database[id].doc = JSON.stringify(metadata); - return id; - }; - - MemoryStorage.prototype.get = function (id) { - try { - return JSON.parse(this._database[id].doc); - } catch (error) { - if (error instanceof TypeError) { - throw new jIO.util.jIOError( - "Cannot find document: " + id, - 404 - ); - } - throw error; - } - }; - - MemoryStorage.prototype.allAttachments = function (id) { - var key, - attachments = {}; - try { - for (key in this._database[id].attachments) { - if (this._database[id].attachments.hasOwnProperty(key)) { - attachments[key] = {}; - } - } - } catch (error) { - if (error instanceof TypeError) { - throw new jIO.util.jIOError( - "Cannot find document: " + id, - 404 - ); - } - throw error; - } - return attachments; - }; - - MemoryStorage.prototype.remove = function (id) { - delete this._database[id]; - return id; - }; - - MemoryStorage.prototype.getAttachment = function (id, name) { - try { - var result = this._database[id].attachments[name]; - if (result === undefined) { - throw new jIO.util.jIOError( - "Cannot find attachment: " + id + " , " + name, - 404 - ); - } - return jIO.util.dataURItoBlob(result); - } catch (error) { - if (error instanceof TypeError) { - throw new jIO.util.jIOError( - "Cannot find attachment: " + id + " , " + name, - 404 - ); - } - throw error; - } - }; - - MemoryStorage.prototype.putAttachment = function (id, name, blob) { - var attachment_dict; - try { - attachment_dict = this._database[id].attachments; - } catch (error) { - if (error instanceof TypeError) { - throw new jIO.util.jIOError("Cannot find document: " + id, 404); - } - throw error; - } - return new RSVP.Queue() - .push(function () { - return jIO.util.readBlobAsDataURL(blob); - }) - .push(function (evt) { - attachment_dict[name] = evt.target.result; - }); - }; - - MemoryStorage.prototype.removeAttachment = function (id, name) { - try { - delete this._database[id].attachments[name]; - } catch (error) { - if (error instanceof TypeError) { - throw new jIO.util.jIOError( - "Cannot find document: " + id, - 404 - ); - } - throw error; - } - }; - - - MemoryStorage.prototype.hasCapacity = function (name) { - return ((name === "list") || (name === "include")); - }; - - MemoryStorage.prototype.buildQuery = function (options) { - var rows = [], - i; - for (i in this._database) { - if (this._database.hasOwnProperty(i)) { - if (options.include_docs === true) { - rows.push({ - id: i, - value: {}, - doc: JSON.parse(this._database[i].doc) - }); - } else { - rows.push({ - id: i, - value: {} - }); - } - - } - } - return rows; - }; - - jIO.addStorage('memory', MemoryStorage); - -}(jIO, JSON, RSVP)); -;/*jslint nomen: true*/ -/*global RSVP, Blob, LZString, DOMException*/ -(function (RSVP, Blob, LZString, DOMException) { - "use strict"; - - /** - * The jIO ZipStorage extension - * - * @class ZipStorage - * @constructor - */ - - var MIME_TYPE = "application/x-jio-utf16_lz_string"; - - function ZipStorage(spec) { - this._sub_storage = jIO.createJIO(spec.sub_storage); - } - - ZipStorage.prototype.get = function () { - return this._sub_storage.get.apply(this._sub_storage, - arguments); - }; - - ZipStorage.prototype.post = function () { - return this._sub_storage.post.apply(this._sub_storage, - arguments); - }; - - ZipStorage.prototype.put = function () { - return this._sub_storage.put.apply(this._sub_storage, - arguments); - }; - - ZipStorage.prototype.remove = function () { - return this._sub_storage.remove.apply(this._sub_storage, - arguments); - }; - - ZipStorage.prototype.hasCapacity = function () { - return this._sub_storage.hasCapacity.apply(this._sub_storage, - arguments); - }; - - ZipStorage.prototype.buildQuery = function () { - return this._sub_storage.buildQuery.apply(this._sub_storage, - arguments); - }; - - ZipStorage.prototype.getAttachment = function (id, name) { - var that = this; - return that._sub_storage.getAttachment(id, name) - .push(function (blob) { - if (blob.type !== MIME_TYPE) { - return blob; - } - return new RSVP.Queue() - .push(function () { - return jIO.util.readBlobAsText(blob, 'utf16'); - }) - .push(function (evt) { - var result = - LZString.decompressFromUTF16(evt.target.result); - if (result === '') { - return blob; - } - try { - return jIO.util.dataURItoBlob( - result - ); - } catch (error) { - if (error instanceof DOMException) { - return blob; - } - throw error; - } - }); - }); - }; - - function myEndsWith(str, query) { - return (str.indexOf(query) === str.length - query.length); - } - - ZipStorage.prototype.putAttachment = function (id, name, blob) { - var that = this; - if ((blob.type.indexOf("text/") === 0) || myEndsWith(blob.type, "xml") || - myEndsWith(blob.type, "json")) { - return new RSVP.Queue() - .push(function () { - return jIO.util.readBlobAsDataURL(blob); - }) - .push(function (data) { - var result = LZString.compressToUTF16(data.target.result); - blob = new Blob([result], - {type: MIME_TYPE}); - return that._sub_storage.putAttachment(id, name, blob); - }); - } - return this._sub_storage.putAttachment.apply(this._sub_storage, - arguments); - }; - - ZipStorage.prototype.removeAttachment = function () { - return this._sub_storage.removeAttachment.apply(this._sub_storage, - arguments); - }; - - ZipStorage.prototype.allAttachments = function () { - return this._sub_storage.allAttachments.apply(this._sub_storage, - arguments); - }; - - jIO.addStorage('zip', ZipStorage); -}(RSVP, Blob, LZString, DOMException)); -;/*jslint nomen: true*/ -/*global jIO, DOMParser, Node */ -(function (jIO, DOMParser, Node) { - "use strict"; - - ///////////////////////////////////////////////////////////// - // OPML Parser - ///////////////////////////////////////////////////////////// - function OPMLParser(txt) { - this._dom_parser = new DOMParser().parseFromString(txt, 'text/xml'); - } - - OPMLParser.prototype.parseHead = function () { - // fetch all children instead - var channel_element = this._dom_parser.querySelector("opml > head"), - tag_element, - i, - result = {}; - - for (i = channel_element.childNodes.length - 1; i >= 0; i -= 1) { - tag_element = channel_element.childNodes[i]; - if (tag_element.nodeType === Node.ELEMENT_NODE) { - result[tag_element.tagName] = tag_element.textContent; - } - } - return result; - }; - - OPMLParser.prototype.parseOutline = function (result_list, outline_element, - prefix, include, id) { - var attribute, - i, - child, - result = {}; - - if ((id === prefix) || (id === undefined)) { - result_list.push({ - id: prefix, - value: {} - }); - if (include) { - for (i = outline_element.attributes.length - 1; i >= 0; i -= 1) { - attribute = outline_element.attributes[i]; - if (attribute.value) { - result[attribute.name] = attribute.value; - } - } - result_list[result_list.length - 1].doc = result; - } - } - - for (i = outline_element.childNodes.length - 1; i >= 0; i -= 1) { - child = outline_element.childNodes[i]; - if (child.tagName === 'outline') { - this.parseOutline(result_list, child, prefix + '/' + i, include, id); - } - } - }; - - OPMLParser.prototype.getDocumentList = function (include, id) { - var result_list, - item_list = this._dom_parser.querySelectorAll("body > outline"), - i; - - if ((id === '/0') || (id === undefined)) { - result_list = [{ - id: '/0', - value: {} - }]; - if (include) { - result_list[0].doc = this.parseHead(); - } - } else { - result_list = []; - } - - for (i = 0; i < item_list.length; i += 1) { - this.parseOutline(result_list, item_list[i], '/1/' + i, include, id); - } - return result_list; - }; - ///////////////////////////////////////////////////////////// - // ATOM Parser - ///////////////////////////////////////////////////////////// - function ATOMParser(txt) { - this._dom_parser = new DOMParser().parseFromString(txt, 'text/xml'); - } - ATOMParser.prototype.parseElement = function (element) { - var tag_element, - i, - j, - tag_name, - attribute, - result = {}; - - for (i = element.childNodes.length - 1; i >= 0; i -= 1) { - tag_element = element.childNodes[i]; - if ((tag_element.nodeType === Node.ELEMENT_NODE) && - (tag_element.tagName !== 'entry')) { - tag_name = tag_element.tagName; - // may have several links, with different rel value - // default is alternate - if (tag_name === 'link') { - tag_name += '_' + (tag_element.getAttribute('rel') || 'alternate'); - } else { - result[tag_name] = tag_element.textContent; - } - for (j = tag_element.attributes.length - 1; j >= 0; j -= 1) { - attribute = tag_element.attributes[j]; - if (attribute.value) { - result[tag_name + '_' + attribute.name] = - attribute.value; - } - } - - } - } - return result; - }; - ATOMParser.prototype.getDocumentList = function (include, id) { - var result_list, - item_list = this._dom_parser.querySelectorAll("feed > entry"), - i; - - if ((id === '/0') || (id === undefined)) { - result_list = [{ - id: '/0', - value: {} - }]; - if (include) { - result_list[0].doc = this.parseElement( - this._dom_parser.querySelector("feed") - ); - } - } else { - result_list = []; - } - - for (i = 0; i < item_list.length; i += 1) { - if ((id === '/0/' + i) || (id === undefined)) { - result_list.push({ - id: '/0/' + i, - value: {} - }); - if (include) { - result_list[result_list.length - 1].doc = - this.parseElement(item_list[i]); - } - } - } - return result_list; - }; - - ///////////////////////////////////////////////////////////// - // RSS Parser - ///////////////////////////////////////////////////////////// - function RSSParser(txt) { - this._dom_parser = new DOMParser().parseFromString(txt, 'text/xml'); - } - - RSSParser.prototype.parseElement = function (element) { - var tag_element, - i, - j, - attribute, - result = {}; - - for (i = element.childNodes.length - 1; i >= 0; i -= 1) { - tag_element = element.childNodes[i]; - if ((tag_element.nodeType === Node.ELEMENT_NODE) && - (tag_element.tagName !== 'item')) { - result[tag_element.tagName] = tag_element.textContent; - - for (j = tag_element.attributes.length - 1; j >= 0; j -= 1) { - attribute = tag_element.attributes[j]; - if (attribute.value) { - result[tag_element.tagName + '_' + attribute.name] = - attribute.value; - } - } - } - } - return result; - }; - - RSSParser.prototype.getDocumentList = function (include, id) { - var result_list, - item_list = this._dom_parser.querySelectorAll("rss > channel > item"), - i; - - if ((id === '/0') || (id === undefined)) { - result_list = [{ - id: '/0', - value: {} - }]; - if (include) { - result_list[0].doc = this.parseElement( - this._dom_parser.querySelector("rss > channel") - ); - } - } else { - result_list = []; - } - - for (i = 0; i < item_list.length; i += 1) { - if ((id === '/0/' + i) || (id === undefined)) { - result_list.push({ - id: '/0/' + i, - value: {} - }); - if (include) { - result_list[result_list.length - 1].doc = - this.parseElement(item_list[i]); - } - } - } - return result_list; - }; - - ///////////////////////////////////////////////////////////// - // Helpers - ///////////////////////////////////////////////////////////// - var parser_dict = { - 'rss': RSSParser, - 'opml': OPMLParser, - 'atom': ATOMParser - }; - - function getParser(storage) { - return storage._sub_storage.getAttachment(storage._document_id, - storage._attachment_id, - {format: 'text'}) - .push(function (txt) { - return new parser_dict[storage._parser_name](txt); - }); - } - - ///////////////////////////////////////////////////////////// - // Storage - ///////////////////////////////////////////////////////////// - function ParserStorage(spec) { - this._attachment_id = spec.attachment_id; - this._document_id = spec.document_id; - this._parser_name = spec.parser; - this._sub_storage = jIO.createJIO(spec.sub_storage); - } - - ParserStorage.prototype.hasCapacity = function (capacity) { - return (capacity === "list") || (capacity === 'include'); - }; - - ParserStorage.prototype.buildQuery = function (options) { - if (options === undefined) { - options = {}; - } - return getParser(this) - .push(function (parser) { - return parser.getDocumentList((options.include_docs || false)); - }); - }; - - ParserStorage.prototype.get = function (id) { - return getParser(this) - .push(function (parser) { - var result_list = parser.getDocumentList(true, id); - if (result_list.length) { - return result_list[0].doc; - } - throw new jIO.util.jIOError( - "Cannot find parsed document: " + id, - 404 - ); - }); - }; - - jIO.addStorage('parser', ParserStorage); - -}(jIO, DOMParser, Node)); -;/*global RSVP, Blob*/ -/*jslint nomen: true*/ -(function (jIO, RSVP, Blob) { - "use strict"; - - function HttpStorage(spec) { - if (spec.hasOwnProperty('catch_error')) { - this._catch_error = spec.catch_error; - } else { - this._catch_error = false; - } - // If timeout not set, use 0 for no timeout value - this._timeout = spec.timeout || 0; - } - - HttpStorage.prototype.get = function (id) { - var context = this; - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - type: 'HEAD', - url: id, - timeout: context._timeout - }); - }) - .push(undefined, function (error) { - if (context._catch_error) { - return error; - } - if ((error.target !== undefined) && - (error.target.status === 404)) { - throw new jIO.util.jIOError("Cannot find url " + id, 404); - } - throw error; - }) - .push(function (response) { - - var key_list = ["Content-Disposition", "Content-Type", "Date", - "Last-Modified", "Vary", "Cache-Control", "Etag", - "Accept-Ranges", "Content-Range"], - i, - key, - value, - result = {}; - result.Status = response.target.status; - for (i = 0; i < key_list.length; i += 1) { - key = key_list[i]; - value = response.target.getResponseHeader(key); - if (value !== null) { - result[key] = value; - } - } - return result; - }); - }; - - HttpStorage.prototype.allAttachments = function () { - return {enclosure: {}}; - }; - - HttpStorage.prototype.getAttachment = function (id, name) { - var context = this; - if (name !== 'enclosure') { - throw new jIO.util.jIOError("Forbidden attachment: " - + id + " , " + name, - 400); - } - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - type: 'GET', - url: id, - dataType: "blob", - timeout: context._timeout - }); - }) - .push(undefined, function (error) { - if (context._catch_error) { - return error; - } - if ((error.target !== undefined) && - (error.target.status === 404)) { - throw new jIO.util.jIOError("Cannot find url " + id, 404); - } - throw error; - }) - .push(function (response) { - return new Blob( - [response.target.response || response.target.responseText], - {"type": response.target.getResponseHeader('Content-Type') || - "application/octet-stream"} - ); - }); - }; - - jIO.addStorage('http', HttpStorage); - -}(jIO, RSVP, Blob));;/* - * Copyright 2013, Nexedi SA - * Released under the LGPL license. - * http://www.gnu.org/licenses/lgpl.html - */ -/** - * JIO Dropbox Storage. Type = "dropbox". - * Dropbox "database" storage. - */ -/*global Blob, jIO, RSVP*/ -/*jslint nomen: true*/ - -(function (jIO, RSVP, Blob, JSON) { - "use strict"; - var GET_URL = "https://content.dropboxapi.com/2/files/download", - UPLOAD_URL = "https://content.dropboxapi.com/2/files/upload", - REMOVE_URL = "https://api.dropboxapi.com/2/files/delete_v2", - CREATE_DIR_URL = "https://api.dropboxapi.com/2/files/create_folder_v2", - METADATA_URL = "https://api.dropboxapi.com/2/files/get_metadata", - LIST_FOLDER_URL = "https://api.dropboxapi.com/2/files/list_folder", - LIST_MORE_URL = "https://api.dropboxapi.com/2/files/list_folder/continue"; - - function restrictDocumentId(id) { - if (id.indexOf("/") !== 0) { - throw new jIO.util.jIOError("id " + id + " is forbidden (no begin /)", - 400); - } - if (id.lastIndexOf("/") !== (id.length - 1)) { - throw new jIO.util.jIOError("id " + id + " is forbidden (no end /)", - 400); - } - return id.slice(0, -1); - } - - function restrictAttachmentId(id) { - if (id.indexOf("/") !== -1) { - throw new jIO.util.jIOError("attachment " + id + " is forbidden", - 400); - } - } - - function recursiveAllAttachments(result, token, id, cursor) { - var data, - url; - if (cursor === undefined) { - data = { - "path": id, - "recursive": false, - "include_media_info": false, - "include_deleted": false, - "include_has_explicit_shared_members": false, - "include_mounted_folders": true - }; - url = LIST_FOLDER_URL; - } else { - data = {"cursor": cursor}; - url = LIST_MORE_URL; - } - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - type: "POST", - url: url, - headers: { - "Authorization": "Bearer " + token, - "Content-Type": "application/json" - }, - data: JSON.stringify(data) - }); - }) - .push(function (evt) { - var obj = JSON.parse(evt.target.response || evt.target.responseText), - i; - for (i = 0; i < obj.entries.length; i += 1) { - if (obj.entries[i][".tag"] === "file") { - result[obj.entries[i].name] = {}; - } - } - if (obj.has_more) { - return recursiveAllAttachments(result, token, id, obj.cursor); - } - return result; - }, function (error) { - if (error.target !== undefined && error.target.status === 409) { - var err_content = JSON.parse(error.target.response || - error.target.responseText); - if ((err_content.error['.tag'] === 'path') && - (err_content.error.path['.tag'] === 'not_folder')) { - throw new jIO.util.jIOError("Not a directory: " + id + "/", - 404); - } - if ((err_content.error['.tag'] === 'path') && - (err_content.error.path['.tag'] === 'not_found')) { - throw new jIO.util.jIOError("Cannot find document: " + id + "/", - 404); - } - } - throw error; - }); - } - - /** - * The JIO Dropbox Storage extension - * - * @class DropboxStorage - * @constructor - */ - function DropboxStorage(spec) { - if (typeof spec.access_token !== 'string' || !spec.access_token) { - throw new TypeError("Access Token' must be a string " + - "which contains more than one character."); - } - this._access_token = spec.access_token; - } - - DropboxStorage.prototype.put = function (id, param) { - var that = this; - id = restrictDocumentId(id); - if (Object.getOwnPropertyNames(param).length > 0) { - // Reject if param has some properties - throw new jIO.util.jIOError("Can not store properties: " + - Object.getOwnPropertyNames(param), 400); - } - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - type: "POST", - url: CREATE_DIR_URL, - headers: { - "Authorization": "Bearer " + that._access_token, - "Content-Type": "application/json" - }, - data: JSON.stringify({"path": id, "autorename": false}) - }); - }) - .push(undefined, function (err) { - if ((err.target !== undefined) && - (err.target.status === 409)) { - var err_content = JSON.parse(err.target.response || - err.target.responseText); - if ((err_content.error['.tag'] === 'path') && - (err_content.error.path['.tag'] === 'conflict')) { - // Directory already exists, no need to fail - return; - } - } - throw err; - }); - }; - - DropboxStorage.prototype.remove = function (id) { - id = restrictDocumentId(id); - return jIO.util.ajax({ - type: "POST", - url: REMOVE_URL, - headers: { - "Authorization": "Bearer " + this._access_token, - "Content-Type": "application/json" - }, - data: JSON.stringify({"path": id}) - }); - }; - - DropboxStorage.prototype.get = function (id) { - var that = this; - - if (id === "/") { - return {}; - } - id = restrictDocumentId(id); - - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - type: "POST", - url: METADATA_URL, - headers: { - "Authorization": "Bearer " + that._access_token, - "Content-Type": "application/json" - }, - data: JSON.stringify({"path": id}) - }); - }) - .push(function (evt) { - var obj = JSON.parse(evt.target.response || - evt.target.responseText); - if (obj[".tag"] === "folder") { - return {}; - } - throw new jIO.util.jIOError("Not a directory: " + id + "/", 404); - }, function (error) { - if (error.target !== undefined && error.target.status === 409) { - var err_content = JSON.parse(error.target.response || - error.target.responseText); - if ((err_content.error['.tag'] === 'path') && - (err_content.error.path['.tag'] === 'not_found')) { - throw new jIO.util.jIOError("Cannot find document: " + id + "/", - 404); - } - } - throw error; - }); - }; - - DropboxStorage.prototype.allAttachments = function (id) { - id = restrictDocumentId(id); - return recursiveAllAttachments({}, this._access_token, id); - }; - - //currently, putAttachment will fail with files larger than 150MB, - //due to the Dropbox API. the API provides the "chunked_upload" method - //to pass this limit, but upload process becomes more complex to implement. - // - //putAttachment will also create a folder if you try to put an attachment - //to an inexisting foler. - - DropboxStorage.prototype.putAttachment = function (id, name, blob) { - id = restrictDocumentId(id); - restrictAttachmentId(name); - - return jIO.util.ajax({ - type: "POST", - url: UPLOAD_URL, - headers: { - "Authorization": "Bearer " + this._access_token, - "Content-Type": "application/octet-stream", - "Dropbox-API-Arg": JSON.stringify({ - "path": id + "/" + name, - "mode": "overwrite", - "autorename": false, - "mute": false - }) - }, - data: blob - }); - }; - - DropboxStorage.prototype.getAttachment = function (id, name) { - var context = this; - - id = restrictDocumentId(id); - restrictAttachmentId(name); - - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - url: GET_URL, - type: "POST", - dataType: "blob", - headers: { - "Authorization": "Bearer " + context._access_token, - "Dropbox-API-Arg": JSON.stringify({"path": id + "/" + name}) - } - }); - }) - .push(function (evt) { - if (evt.target.response instanceof Blob) { - return evt.target.response; - } - return new Blob( - [evt.target.responseText], - {"type": evt.target.getResponseHeader('Content-Type') || - "application/octet-stream"} - ); - }, function (error) { - if (error.target !== undefined && error.target.status === 409) { - if (!(error.target.response instanceof Blob)) { - var err_content = JSON.parse(error.target.responseText); - if ((err_content.error['.tag'] === 'path') && - (err_content.error.path['.tag'] === 'not_found')) { - throw new jIO.util.jIOError("Cannot find attachment: " + - id + "/, " + name, 404); - } - throw error; - } - return new RSVP.Queue() - .push(function () { - return jIO.util.readBlobAsText(error.target.response); - }) - .push(function (evt) { - var err_content = JSON.parse(evt.target.result); - if ((err_content.error['.tag'] === 'path') && - (err_content.error.path['.tag'] === 'not_found')) { - throw new jIO.util.jIOError("Cannot find attachment: " + - id + "/, " + name, 404); - } - throw error; - }); - } - throw error; - }); - }; - - //removeAttachment removes also directories.(due to Dropbox API) - - DropboxStorage.prototype.removeAttachment = function (id, name) { - var that = this; - id = restrictDocumentId(id); - restrictAttachmentId(name); - - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - type: "POST", - url: REMOVE_URL, - headers: { - "Authorization": "Bearer " + that._access_token, - "Content-Type": "application/json" - }, - data: JSON.stringify({"path": id + "/" + name}) - }); - }).push(undefined, function (error) { - if (error.target !== undefined && error.target.status === 409) { - var err_content = JSON.parse(error.target.response || - error.target.responseText); - if ((err_content.error['.tag'] === 'path_lookup') && - (err_content.error.path_lookup['.tag'] === 'not_found')) { - throw new jIO.util.jIOError("Cannot find attachment: " + - id + "/, " + name, 404); - } - } - throw error; - }); - }; - - jIO.addStorage('dropbox', DropboxStorage); - -}(jIO, RSVP, Blob, JSON)); -;/* - * Copyright 2013, Nexedi SA - * Released under the LGPL license. - * http://www.gnu.org/licenses/lgpl.html - */ - -/*jslint nomen: true*/ -/*global jIO, RSVP, DOMParser, Blob */ - -// JIO Dav Storage Description : -// { -// type: "dav", -// url: {string}, -// basic_login: {string} // Basic authentication -// } - -// NOTE: to get the authentication type -> -// curl --verbose -X OPTION http://domain/ -// In the headers: "WWW-Authenticate: Basic realm="DAV-upload" - -(function (jIO, RSVP, DOMParser, Blob) { - "use strict"; - - function ajax(storage, options) { - if (options === undefined) { - options = {}; - } - if (storage._authorization !== undefined) { - if (options.headers === undefined) { - options.headers = {}; - } - options.headers.Authorization = storage._authorization; - } - - if (storage._with_credentials !== undefined) { - if (options.xhrFields === undefined) { - options.xhrFields = {}; - } - options.xhrFields.withCredentials = storage._with_credentials; - } -// if (start !== undefined) { -// if (end !== undefined) { -// headers.Range = "bytes=" + start + "-" + end; -// } else { -// headers.Range = "bytes=" + start + "-"; -// } -// } - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax(options); - }); - } - - function restrictDocumentId(id) { - if (id.indexOf("/") !== 0) { - throw new jIO.util.jIOError("id " + id + " is forbidden (no begin /)", - 400); - } - if (id.lastIndexOf("/") !== (id.length - 1)) { - throw new jIO.util.jIOError("id " + id + " is forbidden (no end /)", - 400); - } - return id; - } - - function restrictAttachmentId(id) { - if (id.indexOf("/") !== -1) { - throw new jIO.util.jIOError("attachment " + id + " is forbidden", - 400); - } - } - - /** - * The JIO WebDAV Storage extension - * - * @class DavStorage - * @constructor - */ - function DavStorage(spec) { - if (typeof spec.url !== 'string') { - throw new TypeError("DavStorage 'url' is not of type string"); - } - this._url = spec.url; - // XXX digest login - if (typeof spec.basic_login === 'string') { - this._authorization = "Basic " + spec.basic_login; - } - this._with_credentials = spec.with_credentials; - } - - DavStorage.prototype.put = function (id, param) { - var that = this; - id = restrictDocumentId(id); - if (Object.getOwnPropertyNames(param).length > 0) { - // Reject if param has some properties - throw new jIO.util.jIOError("Can not store properties: " + - Object.getOwnPropertyNames(param), 400); - } - return new RSVP.Queue() - .push(function () { - return ajax(that, { - type: "MKCOL", - url: that._url + id - }); - }) - .push(undefined, function (err) { - if ((err.target !== undefined) && - (err.target.status === 405)) { - return; - } - throw err; - }); - }; - - DavStorage.prototype.remove = function (id) { - id = restrictDocumentId(id); - return ajax(this, { - type: "DELETE", - url: this._url + id - }); - }; - - DavStorage.prototype.get = function (id) { - var context = this; - id = restrictDocumentId(id); - - return new RSVP.Queue() - .push(function () { - return ajax(context, { - type: "PROPFIND", - url: context._url + id, - dataType: "text", - headers: { - // Increasing this value is a performance killer - Depth: "1" - } - }); - }) - .push(function () { - return {}; - }, function (error) { - if ((error.target !== undefined) && - (error.target.status === 404)) { - throw new jIO.util.jIOError("Cannot find document", 404); - } - throw error; - }); - }; - - DavStorage.prototype.allAttachments = function (id) { - - var context = this; - id = restrictDocumentId(id); - - return new RSVP.Queue() - .push(function () { - return ajax(context, { - type: "PROPFIND", - url: context._url + id, - dataType: "text", - headers: { - // Increasing this value is a performance killer - Depth: "1" - } - }); - }) - - - .push(function (response) { - // Extract all meta informations and return them to JSON - - var i, - attachment = {}, - id, - attachment_list = new DOMParser().parseFromString( - response.target.responseText, - "text/xml" - ).querySelectorAll( - "D\\:response, response" - ); - - // exclude parent folder and browse - for (i = 1; i < attachment_list.length; i += 1) { - // XXX Only get files for now - id = attachment_list[i].querySelector("D\\:href, href"). - textContent.split('/').slice(-1)[0]; - // XXX Ugly - if ((id !== undefined) && (id !== "")) { - attachment[id] = {}; - } - } - return attachment; - - }, function (error) { - if ((error.target !== undefined) && - (error.target.status === 404)) { - throw new jIO.util.jIOError("Cannot find document", 404); - } - throw error; - }); - - }; - - - DavStorage.prototype.putAttachment = function (id, name, blob) { - var that = this; - id = restrictDocumentId(id); - restrictAttachmentId(name); - - return new RSVP.Queue() - .push(function () { - return ajax(that, { - type: "PUT", - url: that._url + id + name, - data: blob - }); - }) - .push(undefined, function (error) { - if (error.target.status === 403 || error.target.status === 424) { - throw new jIO.util.jIOError("Cannot access subdocument", 404); - } - throw error; - }); - }; - - DavStorage.prototype.getAttachment = function (id, name) { - var context = this; - id = restrictDocumentId(id); - restrictAttachmentId(name); - - return new RSVP.Queue() - .push(function () { - return ajax(context, { - type: "GET", - url: context._url + id + name, - dataType: "blob" - }); - }) - .push(function (response) { - return new Blob( - [response.target.response || response.target.responseText], - {"type": response.target.getResponseHeader('Content-Type') || - "application/octet-stream"} - ); - }, function (error) { - if ((error.target !== undefined) && - (error.target.status === 404)) { - throw new jIO.util.jIOError("Cannot find attachment: " - + id + " , " + name, - 404); - } - throw error; - }); - - }; - - DavStorage.prototype.removeAttachment = function (id, name) { - var context = this; - id = restrictDocumentId(id); - restrictAttachmentId(name); - - return new RSVP.Queue() - .push(function () { - return ajax(context, { - type: "DELETE", - url: context._url + id + name - }); - }) - .push(undefined, function (error) { - if ((error.target !== undefined) && - (error.target.status === 404)) { - throw new jIO.util.jIOError("Cannot find attachment: " - + id + " , " + name, - 404); - } - throw error; - }); - }; - - // JIO COMMANDS // - - // wedDav methods rfc4918 (short summary) - // COPY Reproduces single resources (files) and collections (directory - // trees). Will overwrite files (if specified by request) but will - // respond 209 (Conflict) if it would overwrite a tree - // DELETE deletes files and directory trees - // GET just the vanilla HTTP/1.1 behaviour - // HEAD ditto - // LOCK locks a resources - // MKCOL creates a directory - // MOVE Moves (rename or copy) a file or a directory tree. Will - // 'overwrite' files (if specified by the request) but will respond - // 209 (Conflict) if it would overwrite a tree. - // OPTIONS If WebDAV is enabled and available for the path this reports the - // WebDAV extension methods - // PROPFIND Retrieves the requested file characteristics, DAV lock status - // and 'dead' properties for individual files, a directory and its - // child files, or a directory tree - // PROPPATCHset and remove 'dead' meta-data properties - // PUT Update or create resource or collections - // UNLOCK unlocks a resource - - // Notes: all Ajax requests should be CORS (cross-domain) - // adding custom headers triggers preflight OPTIONS request! - // http://remysharp.com/2011/04/21/getting-cors-working/ - - jIO.addStorage('dav', DavStorage); - -}(jIO, RSVP, DOMParser, Blob)); -;/* - * Copyright 2015, Nexedi SA - * Released under the LGPL license. - * http://www.gnu.org/licenses/lgpl.html - */ -/** - * JIO Google Drive Storage. Type = "gdrive". - * Google Drive "database" storage. - */ -/*global jIO, Blob, RSVP, UriTemplate, JSON*/ -/*jslint nomen: true*/ - -(function (jIO, Blob, RSVP, UriTemplate, JSON) { - "use strict"; - - var UPLOAD_URL = "https://www.googleapis.com{/upload}/drive/v2/files{/id}" + - "{?uploadType,access_token}", - upload_template = UriTemplate.parse(UPLOAD_URL), - REMOVE_URL = "https://www.googleapis.com/drive/v2/" + - "files{/id,trash}{?access_token}", - remove_template = UriTemplate.parse(REMOVE_URL), - LIST_URL = "https://www.googleapis.com/drive/v2/files" + - "?prettyPrint=false{&pageToken}&q=trashed=false" + - "&fields=nextPageToken,items(id){&access_token}", - list_template = UriTemplate.parse(LIST_URL), - GET_URL = "https://www.googleapis.com/drive/v2/files{/id}{?alt}", - get_template = UriTemplate.parse(GET_URL); - - function handleError(error, id) { - if (error.target.status === 404) { - throw new jIO.util.jIOError( - "Cannot find document: " + id, - 404 - ); - } - throw error; - } - - function listPage(result, token) { - var i, - obj; - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - "type": "GET", - "url": list_template.expand({ - pageToken : (result.nextPageToken || ""), - access_token: token - }) - }); - }) - .push(function (data) { - obj = JSON.parse(data.target.response || data.target.responseText); - for (i = 0; i < obj.items.length; i += 1) { - obj.items[i].value = {}; - result.push(obj.items[i]); - } - result.nextPageToken = obj.nextPageToken; - return result; - }, handleError); - } - - function checkName(name) { - if (name !== "enclosure") { - throw new jIO.util.jIOError("Only support 'enclosure' attachment", 400); - } - } - - /** - * The JIO Google Drive Storage extension - * - * @class GdriveStorage - * @constructor - */ - function GdriveStorage(spec) { - if (spec === undefined || spec.access_token === undefined || - typeof spec.access_token !== 'string') { - throw new TypeError("Access Token must be a string " + - "which contains more than one character."); - } - if (spec.trashing !== undefined && - (spec.trashing !== true && spec.trashing !== false)) { - throw new TypeError("trashing parameter" + - " must be a boolean (true or false)"); - } - this._trashing = spec.trashing || true; - this._access_token = spec.access_token; - return; - } - - function recursiveAllDocs(result, accessToken) { - return new RSVP.Queue() - .push(function () { - return listPage(result, accessToken); - }) - .push(function () { - if (result.nextPageToken) { - return recursiveAllDocs(result, accessToken); - } - return result; - }); - } - - GdriveStorage.prototype.hasCapacity = function (name) { - return (name === "list"); - }; - - GdriveStorage.prototype.buildQuery = function () { - return recursiveAllDocs([], this._access_token); - }; - - function sendMetaData(id, param, token) { - var boundary = "-------314159265358979323846"; - - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - "type": id ? "PUT" : "POST", - "url": upload_template.expand({ - access_token: token, - id: id || [], - upload: id ? [] : "upload", - uploadType: "multipart" - }), - headers: { - "Content-Type" : 'multipart/related; boundary="' + boundary + '"' - }, - data: '--' + boundary + '\n' + - 'Content-Type: application/json; charset=UTF-8\n\n' + - JSON.stringify(param) + '\n\n--' + boundary + "--" - }); - }) - .push(function (result) { - var obj = JSON.parse(result.target.responseText); - - return obj.id; - }, - function (error) {handleError(error, id); }); - } - - GdriveStorage.prototype.put = function (id, param) { - return sendMetaData(id, param, this._access_token); - }; - - GdriveStorage.prototype.post = function (param) { - return sendMetaData(undefined, param, this._access_token); - }; - - function sendData(id, blob, token) { - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - "type": "PUT", - "url": upload_template.expand({ - access_token: token, - upload: "upload", - id: id, - uploadType: "media" - }), - data: blob - }); - }) - .push(function (data) { - data = JSON.parse(data.target.responseText); - if (data.mimeType === "application/vnd.google-apps.folder") { - throw new jIO.util.jIOError("cannot put attachments to folder", 400); - } - return data; - }, function (error) {handleError(error, id); }); - } - - GdriveStorage.prototype.putAttachment = function (id, name, blob) { - checkName(name); - return sendData(id, blob, this._access_token); - }; - - GdriveStorage.prototype.remove = function (id) { - var that = this; - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - type: that._trashing ? "POST" : "DELETE", - url: remove_template.expand({ - id : id, - access_token : that._access_token, - trash : that._trashing ? "trash" : [] - }) - }); - }) - .push(undefined, function (error) {handleError(error, id); }); - }; - - function getData(id, attach, token) { - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - type: "GET", - dataType: attach ? "blob" : "json", - url: get_template.expand({ - id: id, - alt: attach ? "media" : [], - access_token: token - }), - headers: { - "Authorization" : "Bearer " + token - } - }); - }) - .push(function (evt) { - return evt.target.response || - (attach ? new Blob([evt.target.responseText], - {"type" : - evt.target.responseHeaders["Content-Type"]}) : - JSON.parse(evt.target.responseText)); - }, function (error) {handleError(error, id); }); - } - - GdriveStorage.prototype.get = function (id) { - return getData(id, false, this._access_token); - }; - - GdriveStorage.prototype.getAttachment = function (id, name) { - checkName(name); - return getData(id, true, this._access_token); - }; - - GdriveStorage.prototype.allAttachments = function (id) { - var token = this._access_token; - - return new RSVP.Queue() - .push(function () { - return getData(id, false, token); - }) - .push(function (data) { - if (data.mimeType === "application/vnd.google-apps.folder") { - return {}; - } - return {"enclosure": {}}; - }); - }; - - jIO.addStorage('gdrive', GdriveStorage); - -}(jIO, Blob, RSVP, UriTemplate, JSON)); -;/*jslint nomen: true */ -/*global RSVP*/ - -/** - * JIO Union Storage. Type = 'union'. - * This provide a unified access other multiple storage. - * New document are created in the first sub storage. - * Document are searched in each sub storage until it is found. - * - * - * Storage Description: - * - * { - * "type": "union", - * "storage_list": [ - * sub_storage_description_1, - * sub_storage_description_2, - * - * sub_storage_description_X, - * ] - * } - * - * @class UnionStorage - */ - -(function (jIO, RSVP) { - "use strict"; - - /** - * The JIO UnionStorage extension - * - * @class UnionStorage - * @constructor - */ - function UnionStorage(spec) { - if (!Array.isArray(spec.storage_list)) { - throw new jIO.util.jIOError("storage_list is not an Array", 400); - } - var i; - this._storage_list = []; - for (i = 0; i < spec.storage_list.length; i += 1) { - this._storage_list.push(jIO.createJIO(spec.storage_list[i])); - } - } - - UnionStorage.prototype._getWithStorageIndex = function () { - var i, - index = 0, - context = this, - arg = arguments, - result = this._storage_list[0].get.apply(this._storage_list[0], arg); - - function handle404(j) { - result - .push(undefined, function (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - return context._storage_list[j].get.apply(context._storage_list[j], - arg) - .push(function (doc) { - index = j; - return doc; - }); - } - throw error; - }); - } - - for (i = 1; i < this._storage_list.length; i += 1) { - handle404(i); - } - return result - .push(function (doc) { - return [index, doc]; - }); - }; - - /* - * Get a document - * Try on each substorage on after the other - */ - UnionStorage.prototype.get = function () { - return this._getWithStorageIndex.apply(this, arguments) - .push(function (result) { - return result[1]; - }); - }; - - /* - * Get attachments list - * Try on each substorage on after the other - */ - UnionStorage.prototype.allAttachments = function () { - var argument_list = arguments, - context = this; - return this._getWithStorageIndex.apply(this, arguments) - .push(function (result) { - var sub_storage = context._storage_list[result[0]]; - return sub_storage.allAttachments.apply(sub_storage, argument_list); - }); - }; - - /* - * Post a document - * Simply store on the first substorage - */ - UnionStorage.prototype.post = function () { - return this._storage_list[0].post.apply(this._storage_list[0], arguments); - }; - - /* - * Put a document - * Search the document location, and modify it in its storage. - */ - UnionStorage.prototype.put = function () { - var arg = arguments, - context = this; - return this._getWithStorageIndex(arg[0]) - .push(undefined, function (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - // Document does not exist, create in first substorage - return [0]; - } - throw error; - }) - .push(function (result) { - // Storage found, modify in it directly - var sub_storage = context._storage_list[result[0]]; - return sub_storage.put.apply(sub_storage, arg); - }); - }; - - /* - * Remove a document - * Search the document location, and remove it from its storage. - */ - UnionStorage.prototype.remove = function () { - var arg = arguments, - context = this; - return this._getWithStorageIndex(arg[0]) - .push(function (result) { - // Storage found, remove from it directly - var sub_storage = context._storage_list[result[0]]; - return sub_storage.remove.apply(sub_storage, arg); - }); - }; - - UnionStorage.prototype.buildQuery = function () { - var promise_list = [], - i, - id_dict = {}, - len = this._storage_list.length, - sub_storage; - for (i = 0; i < len; i += 1) { - sub_storage = this._storage_list[i]; - promise_list.push(sub_storage.buildQuery.apply(sub_storage, arguments)); - } - return new RSVP.Queue() - .push(function () { - return RSVP.all(promise_list); - }) - .push(function (result_list) { - var result = [], - sub_result, - sub_result_len, - j; - len = result_list.length; - for (i = 0; i < len; i += 1) { - sub_result = result_list[i]; - sub_result_len = sub_result.length; - for (j = 0; j < sub_result_len; j += 1) { - if (!id_dict.hasOwnProperty(sub_result[j].id)) { - id_dict[sub_result[j].id] = null; - result.push(sub_result[j]); - } - } - } - return result; - }); - }; - - UnionStorage.prototype.hasCapacity = function (name) { - var i, - len, - result, - sub_storage; - if ((name === "list") || - (name === "query") || - (name === "select")) { - result = true; - len = this._storage_list.length; - for (i = 0; i < len; i += 1) { - sub_storage = this._storage_list[i]; - result = result && sub_storage.hasCapacity(name); - } - return result; - } - return false; - }; - - UnionStorage.prototype.repair = function () { - var i, - promise_list = []; - for (i = 0; i < this._storage_list.length; i += 1) { - promise_list.push(this._storage_list[i].repair.apply( - this._storage_list[i], - arguments - )); - } - return RSVP.all(promise_list); - }; - - UnionStorage.prototype.getAttachment = function () { - var argument_list = arguments, - context = this; - return this._getWithStorageIndex.apply(this, arguments) - .push(function (result) { - var sub_storage = context._storage_list[result[0]]; - return sub_storage.getAttachment.apply(sub_storage, argument_list); - }); - }; - - UnionStorage.prototype.putAttachment = function () { - var argument_list = arguments, - context = this; - return this._getWithStorageIndex.apply(this, arguments) - .push(function (result) { - var sub_storage = context._storage_list[result[0]]; - return sub_storage.putAttachment.apply(sub_storage, argument_list); - }); - }; - - UnionStorage.prototype.removeAttachment = function () { - var argument_list = arguments, - context = this; - return this._getWithStorageIndex.apply(this, arguments) - .push(function (result) { - var sub_storage = context._storage_list[result[0]]; - return sub_storage.removeAttachment.apply(sub_storage, argument_list); - }); - }; - - jIO.addStorage('union', UnionStorage); - -}(jIO, RSVP)); -;/* - * Copyright 2013, Nexedi SA - * Released under the LGPL license. - * http://www.gnu.org/licenses/lgpl.html - */ -// JIO ERP5 Storage Description : -// { -// type: "erp5" -// url: {string} -// } - -/*jslint nomen: true, unparam: true */ -/*global jIO, UriTemplate, FormData, RSVP, URI, Blob, - SimpleQuery, ComplexQuery*/ - -(function (jIO, UriTemplate, FormData, RSVP, URI, Blob, - SimpleQuery, ComplexQuery) { - "use strict"; - - function getSiteDocument(storage) { - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - "type": "GET", - "url": storage._url, - "xhrFields": { - withCredentials: true - } - }); - }) - .push(function (event) { - return JSON.parse(event.target.responseText); - }); - } - - function getDocumentAndHateoas(storage, id, options) { - if (options === undefined) { - options = {}; - } - return getSiteDocument(storage) - .push(function (site_hal) { - // XXX need to get modified metadata - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - "type": "GET", - "url": UriTemplate.parse(site_hal._links.traverse.href) - .expand({ - relative_url: id, - view: options._view - }), - "xhrFields": { - withCredentials: true - } - }); - }) - .push(undefined, function (error) { - if ((error.target !== undefined) && - (error.target.status === 404)) { - throw new jIO.util.jIOError("Cannot find document: " + id, 404); - } - throw error; - }); - }); - } - - var allowed_field_dict = { - "StringField": null, - "EmailField": null, - "IntegerField": null, - "FloatField": null, - "TextAreaField": null - }; - - function extractPropertyFromFormJSON(json) { - return new RSVP.Queue() - .push(function () { - var form = json._embedded._view, - converted_json = { - portal_type: json._links.type.name - }, - form_data_json = {}, - field, - key, - prefix_length, - result; - - if (json._links.hasOwnProperty('parent')) { - converted_json.parent_relative_url = - new URI(json._links.parent.href).segment(2); - } - - form_data_json.form_id = { - "key": [form.form_id.key], - "default": form.form_id["default"] - }; - // XXX How to store datetime - for (key in form) { - if (form.hasOwnProperty(key)) { - field = form[key]; - prefix_length = 0; - if (key.indexOf('my_') === 0 && field.editable) { - prefix_length = 3; - } - if (key.indexOf('your_') === 0) { - prefix_length = 5; - } - if ((prefix_length !== 0) && - (allowed_field_dict.hasOwnProperty(field.type))) { - form_data_json[key.substring(prefix_length)] = { - "default": field["default"], - "key": field.key - }; - converted_json[key.substring(prefix_length)] = field["default"]; - } - } - } - - result = { - data: converted_json, - form_data: form_data_json - }; - if (form.hasOwnProperty('_actions') && - form._actions.hasOwnProperty('put')) { - result.action_href = form._actions.put.href; - } - return result; - }); - } - - function extractPropertyFromForm(context, id) { - return context.getAttachment(id, "view") - .push(function (blob) { - return jIO.util.readBlobAsText(blob); - }) - .push(function (evt) { - return JSON.parse(evt.target.result); - }) - .push(function (json) { - return extractPropertyFromFormJSON(json); - }); - } - - // XXX docstring - function ERP5Storage(spec) { - if (typeof spec.url !== "string" || !spec.url) { - throw new TypeError("ERP5 'url' must be a string " + - "which contains more than one character."); - } - this._url = spec.url; - this._default_view_reference = spec.default_view_reference; - } - - function convertJSONToGet(json) { - var key, - result = json.data; - // Remove all ERP5 hateoas links / convert them into jIO ID - for (key in result) { - if (result.hasOwnProperty(key)) { - if (!result[key]) { - delete result[key]; - } - } - } - return result; - } - - ERP5Storage.prototype.get = function (id) { - return extractPropertyFromForm(this, id) - .push(function (result) { - return convertJSONToGet(result); - }); - }; - - ERP5Storage.prototype.post = function (data) { - var context = this, - new_id; - - return getSiteDocument(this) - .push(function (site_hal) { - var form_data = new FormData(); - form_data.append("portal_type", data.portal_type); - form_data.append("parent_relative_url", data.parent_relative_url); - return jIO.util.ajax({ - type: "POST", - url: site_hal._actions.add.href, - data: form_data, - xhrFields: { - withCredentials: true - } - }); - }) - .push(function (evt) { - var location = evt.target.getResponseHeader("X-Location"), - uri = new URI(location); - new_id = uri.segment(2); - return context.put(new_id, data); - }) - .push(function () { - return new_id; - }); - }; - - ERP5Storage.prototype.put = function (id, data) { - var context = this; - - return extractPropertyFromForm(context, id) - .push(function (result) { - var key, - json = result.form_data, - form_data = {}; - form_data[json.form_id.key] = json.form_id["default"]; - - // XXX How to store datetime:!!!!! - for (key in data) { - if (data.hasOwnProperty(key)) { - if (key === "form_id") { - throw new jIO.util.jIOError( - "ERP5: forbidden property: " + key, - 400 - ); - } - if ((key !== "portal_type") && (key !== "parent_relative_url")) { - if (!json.hasOwnProperty(key)) { - throw new jIO.util.jIOError( - "ERP5: can not store property: " + key, - 400 - ); - } - form_data[json[key].key] = data[key]; - } - } - } - if (!result.hasOwnProperty('action_href')) { - throw new jIO.util.jIOError( - "ERP5: can not modify document: " + id, - 403 - ); - } - return context.putAttachment( - id, - result.action_href, - new Blob([JSON.stringify(form_data)], {type: "application/json"}) - ); - }); - }; - - ERP5Storage.prototype.allAttachments = function (id) { - var context = this; - return getDocumentAndHateoas(this, id) - .push(function () { - if (context._default_view_reference === undefined) { - return { - links: {} - }; - } - return { - view: {}, - links: {} - }; - }); - }; - - ERP5Storage.prototype.getAttachment = function (id, action, options) { - if (options === undefined) { - options = {}; - } - if (action === "view") { - if (this._default_view_reference === undefined) { - throw new jIO.util.jIOError( - "Cannot find attachment view for: " + id, - 404 - ); - } - return getDocumentAndHateoas(this, id, - {"_view": this._default_view_reference}) - .push(function (response) { - var result = JSON.parse(response.target.responseText); - // Remove all ERP5 hateoas links / convert them into jIO ID - - // XXX Change default action to an jio urn with attachment name inside - // if Base_edit, do put URN - // if others, do post URN (ie, unique new attachment name) - // XXX Except this attachment name should be generated when - return new Blob( - [JSON.stringify(result)], - {"type": 'application/hal+json'} - ); - }); - } - if (action === "links") { - return getDocumentAndHateoas(this, id) - .push(function (response) { - return new Blob( - [JSON.stringify(JSON.parse(response.target.responseText))], - {"type": 'application/hal+json'} - ); - }); - } - if (action.indexOf(this._url) === 0) { - return new RSVP.Queue() - .push(function () { - var start, - end, - range, - request_options = { - "type": "GET", - "dataType": "blob", - "url": action, - "xhrFields": { - withCredentials: true - } - }; - if (options.start !== undefined || options.end !== undefined) { - start = options.start || 0; - end = options.end; - if (end !== undefined && end < 0) { - throw new jIO.util.jIOError("end must be positive", - 400); - } - if (start < 0) { - range = "bytes=" + start; - } else if (end === undefined) { - range = "bytes=" + start + "-"; - } else { - if (start > end) { - throw new jIO.util.jIOError("start is greater than end", - 400); - } - range = "bytes=" + start + "-" + end; - } - request_options.headers = {Range: range}; - } - return jIO.util.ajax(request_options); - }) - .push(function (evt) { - if (evt.target.response === undefined) { - return new Blob( - [evt.target.responseText], - {"type": evt.target.getResponseHeader("Content-Type")} - ); - } - return evt.target.response; - }); - } - throw new jIO.util.jIOError("ERP5: not support get attachment: " + action, - 400); - }; - - ERP5Storage.prototype.putAttachment = function (id, name, blob) { - // Assert we use a callable on a document from the ERP5 site - if (name.indexOf(this._url) !== 0) { - throw new jIO.util.jIOError("Can not store outside ERP5: " + - name, 400); - } - - return new RSVP.Queue() - .push(function () { - return jIO.util.readBlobAsText(blob); - }) - .push(function (evt) { - var form_data = JSON.parse(evt.target.result), - data = new FormData(), - array, - i, - key, - value; - for (key in form_data) { - if (form_data.hasOwnProperty(key)) { - if (Array.isArray(form_data[key])) { - array = form_data[key]; - } else { - array = [form_data[key]]; - } - for (i = 0; i < array.length; i += 1) { - value = array[i]; - if (typeof value === "object") { - data.append(key, jIO.util.dataURItoBlob(value.url), - value.file_name); - } else { - data.append(key, value); - } - } - } - } - return jIO.util.ajax({ - "type": "POST", - "url": name, - "data": data, - "dataType": "blob", - "xhrFields": { - withCredentials: true - } - }); - }); - }; - - ERP5Storage.prototype.hasCapacity = function (name) { - return ((name === "list") || (name === "query") || - (name === "select") || (name === "limit") || - (name === "sort")); - }; - - function isSingleLocalRoles(parsed_query) { - if ((parsed_query instanceof SimpleQuery) && - (parsed_query.operator === undefined) && - (parsed_query.key === 'local_roles')) { - // local_roles:"Assignee" - return parsed_query.value; - } - } - - function isSingleDomain(parsed_query) { - if ((parsed_query instanceof SimpleQuery) && - (parsed_query.operator === undefined) && - (parsed_query.key !== undefined) && - (parsed_query.key.indexOf('selection_domain_') === 0)) { - // domain_region:"europe/france" - var result = {}; - result[parsed_query.key.slice('selection_domain_'.length)] = - parsed_query.value; - return result; - } - } - - function isMultipleLocalRoles(parsed_query) { - var i, - sub_query, - is_multiple = true, - local_role_list = []; - if ((parsed_query instanceof ComplexQuery) && - (parsed_query.operator === 'OR')) { - - for (i = 0; i < parsed_query.query_list.length; i += 1) { - sub_query = parsed_query.query_list[i]; - if ((sub_query instanceof SimpleQuery) && - (sub_query.key !== undefined) && - (sub_query.key === 'local_roles')) { - local_role_list.push(sub_query.value); - } else { - is_multiple = false; - } - } - if (is_multiple) { - // local_roles:"Assignee" OR local_roles:"Assignor" - return local_role_list; - } - } - } - - ERP5Storage.prototype.buildQuery = function (options) { -// if (typeof options.query !== "string") { -// options.query = (options.query ? -// jIO.Query.objectToSearchText(options.query) : -// undefined); -// } - return getSiteDocument(this) - .push(function (site_hal) { - var query = options.query, - i, - key, - parsed_query, - sub_query, - result_list, - local_roles, - local_role_found = false, - selection_domain, - sort_list = []; - if (options.query) { - parsed_query = jIO.QueryFactory.create(options.query); - result_list = isSingleLocalRoles(parsed_query); - if (result_list) { - query = undefined; - local_roles = result_list; - } else { - result_list = isSingleDomain(parsed_query); - if (result_list) { - query = undefined; - selection_domain = result_list; - } else { - - result_list = isMultipleLocalRoles(parsed_query); - if (result_list) { - query = undefined; - local_roles = result_list; - } else if ((parsed_query instanceof ComplexQuery) && - (parsed_query.operator === 'AND')) { - - // portal_type:"Person" AND local_roles:"Assignee" - // AND selection_domain_region:"europe/france" - for (i = 0; i < parsed_query.query_list.length; i += 1) { - sub_query = parsed_query.query_list[i]; - - if (!local_role_found) { - result_list = isSingleLocalRoles(sub_query); - if (result_list) { - local_roles = result_list; - parsed_query.query_list.splice(i, 1); - query = jIO.Query.objectToSearchText(parsed_query); - local_role_found = true; - } else { - result_list = isMultipleLocalRoles(sub_query); - if (result_list) { - local_roles = result_list; - parsed_query.query_list.splice(i, 1); - query = jIO.Query.objectToSearchText(parsed_query); - local_role_found = true; - } - } - } - - result_list = isSingleDomain(sub_query); - if (result_list) { - parsed_query.query_list.splice(i, 1); - query = jIO.Query.objectToSearchText(parsed_query); - if (selection_domain) { - for (key in result_list) { - if (result_list.hasOwnProperty(key)) { - selection_domain[key] = result_list[key]; - } - } - } else { - selection_domain = result_list; - } - i -= 1; - } - - } - } - } - } - } - - if (options.sort_on) { - for (i = 0; i < options.sort_on.length; i += 1) { - sort_list.push(JSON.stringify(options.sort_on[i])); - } - } - - if (selection_domain) { - selection_domain = JSON.stringify(selection_domain); - } - - return jIO.util.ajax({ - "type": "GET", - "url": UriTemplate.parse(site_hal._links.raw_search.href) - .expand({ - query: query, - // XXX Force erp5 to return embedded document - select_list: options.select_list || ["title", "reference"], - limit: options.limit, - sort_on: sort_list, - local_roles: local_roles, - selection_domain: selection_domain - }), - "xhrFields": { - withCredentials: true - } - }); - }) - .push(function (response) { - return JSON.parse(response.target.responseText); - }) - .push(function (catalog_json) { - var data = catalog_json._embedded.contents, - count = data.length, - i, - uri, - item, - result = []; - for (i = 0; i < count; i += 1) { - item = data[i]; - uri = new URI(item._links.self.href); - delete item._links; - result.push({ - id: uri.segment(2), - value: item - }); - } - return result; - }); - }; - - jIO.addStorage("erp5", ERP5Storage); - -}(jIO, UriTemplate, FormData, RSVP, URI, Blob, - SimpleQuery, ComplexQuery)); -;/*jslint nomen: true*/ -/*global RSVP, jiodate*/ -(function (jIO, RSVP, jiodate) { - "use strict"; - - function dateType(str) { - return jiodate.JIODate(new Date(str).toISOString()); - } - - function initKeySchema(storage, spec) { - var property; - for (property in spec.schema) { - if (spec.schema.hasOwnProperty(property)) { - if (spec.schema[property].type === "string" && - spec.schema[property].format === "date-time") { - storage._key_schema.key_set[property] = { - read_from: property, - cast_to: "dateType" - }; - if (storage._key_schema.cast_lookup.dateType === undefined) { - storage._key_schema.cast_lookup.dateType = dateType; - } - } else { - throw new jIO.util.jIOError( - "Wrong schema for property: " + property, - 400 - ); - } - } - } - } - - /** - * The jIO QueryStorage extension - * - * @class QueryStorage - * @constructor - */ - function QueryStorage(spec) { - this._sub_storage = jIO.createJIO(spec.sub_storage); - this._key_schema = {key_set: {}, cast_lookup: {}}; - initKeySchema(this, spec); - } - - QueryStorage.prototype.get = function () { - return this._sub_storage.get.apply(this._sub_storage, arguments); - }; - QueryStorage.prototype.allAttachments = function () { - return this._sub_storage.allAttachments.apply(this._sub_storage, arguments); - }; - QueryStorage.prototype.post = function () { - return this._sub_storage.post.apply(this._sub_storage, arguments); - }; - QueryStorage.prototype.put = function () { - return this._sub_storage.put.apply(this._sub_storage, arguments); - }; - QueryStorage.prototype.remove = function () { - return this._sub_storage.remove.apply(this._sub_storage, arguments); - }; - QueryStorage.prototype.getAttachment = function () { - return this._sub_storage.getAttachment.apply(this._sub_storage, arguments); - }; - QueryStorage.prototype.putAttachment = function () { - return this._sub_storage.putAttachment.apply(this._sub_storage, arguments); - }; - QueryStorage.prototype.removeAttachment = function () { - return this._sub_storage.removeAttachment.apply(this._sub_storage, - arguments); - }; - QueryStorage.prototype.repair = function () { - return this._sub_storage.repair.apply(this._sub_storage, arguments); - }; - - QueryStorage.prototype.hasCapacity = function (name) { - var this_storage_capacity_list = ["limit", - "sort", - "select", - "query"]; - - if (this_storage_capacity_list.indexOf(name) !== -1) { - return true; - } - if (name === "list") { - return this._sub_storage.hasCapacity(name); - } - return false; - }; - QueryStorage.prototype.buildQuery = function (options) { - var substorage = this._sub_storage, - context = this, - sub_options = {}, - is_manual_query_needed = false, - is_manual_include_needed = false; - - if (substorage.hasCapacity("list")) { - - // Can substorage handle the queries if needed? - try { - if (((options.query === undefined) || - (substorage.hasCapacity("query"))) && - ((options.sort_on === undefined) || - (substorage.hasCapacity("sort"))) && - ((options.select_list === undefined) || - (substorage.hasCapacity("select"))) && - ((options.limit === undefined) || - (substorage.hasCapacity("limit")))) { - sub_options.query = options.query; - sub_options.sort_on = options.sort_on; - sub_options.select_list = options.select_list; - sub_options.limit = options.limit; - } - } catch (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 501)) { - is_manual_query_needed = true; - } else { - throw error; - } - } - - // Can substorage include the docs if needed? - try { - if ((is_manual_query_needed || - (options.include_docs === true)) && - (substorage.hasCapacity("include"))) { - sub_options.include_docs = true; - } - } catch (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 501)) { - is_manual_include_needed = true; - } else { - throw error; - } - } - - return substorage.buildQuery(sub_options) - - // Include docs if needed - .push(function (result) { - var include_query_list = [result], - len, - i; - - function safeGet(j) { - var id = result[j].id; - return substorage.get(id) - .push(function (doc) { - // XXX Can delete user data! - doc._id = id; - return doc; - }, function (error) { - // Document may have been dropped after listing - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - return; - } - throw error; - }); - } - - if (is_manual_include_needed) { - len = result.length; - for (i = 0; i < len; i += 1) { - include_query_list.push(safeGet(i)); - } - result = RSVP.all(include_query_list); - } - return result; - }) - .push(function (result) { - var original_result, - len, - i; - if (is_manual_include_needed) { - original_result = result[0]; - len = original_result.length; - for (i = 0; i < len; i += 1) { - original_result[i].doc = result[i + 1]; - } - result = original_result; - } - return result; - - }) - - // Manual query if needed - .push(function (result) { - var data_rows = [], - len, - i; - if (is_manual_query_needed) { - len = result.length; - for (i = 0; i < len; i += 1) { - result[i].doc.__id = result[i].id; - data_rows.push(result[i].doc); - } - if (options.select_list) { - options.select_list.push("__id"); - } - result = jIO.QueryFactory.create(options.query || "", - context._key_schema). - exec(data_rows, options); - } - return result; - }) - - // reconstruct filtered rows, preserving the order from docs - .push(function (result) { - var new_result = [], - element, - len, - i; - if (is_manual_query_needed) { - len = result.length; - for (i = 0; i < len; i += 1) { - element = { - id: result[i].__id, - value: options.select_list ? result[i] : {}, - doc: {} - }; - if (options.select_list) { - // Does not work if user manually request __id - delete element.value.__id; - } - if (options.include_docs) { - // XXX To implement - throw new Error("QueryStorage does not support include docs"); - } - new_result.push(element); - } - result = new_result; - } - return result; - }); - - } - }; - - jIO.addStorage('query', QueryStorage); - -}(jIO, RSVP, jiodate)); -;/*jslint nomen: true*/ -/*global RSVP, Blob*/ -(function (jIO, RSVP, Blob) { - "use strict"; - - /** - * The jIO FileSystemBridgeStorage extension - * - * @class FileSystemBridgeStorage - * @constructor - */ - function FileSystemBridgeStorage(spec) { - this._sub_storage = jIO.createJIO(spec.sub_storage); - } - var DOCUMENT_EXTENSION = ".json", - DOCUMENT_KEY = "/.jio_documents/", - ROOT = "/"; - - function endsWith(str, suffix) { - return str.indexOf(suffix, str.length - suffix.length) !== -1; - } - - FileSystemBridgeStorage.prototype.get = function (id) { - var context = this; - return new RSVP.Queue() - - // First, try to get explicit reference to the document - - .push(function () { - // First get the document itself if it exists - return context._sub_storage.getAttachment( - DOCUMENT_KEY, - id + DOCUMENT_EXTENSION, - {format: "json"} - ); - }) - .push(undefined, function (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - - // Second, try to get default attachment - return context._sub_storage.allAttachments(ROOT) - .push(function (attachment_dict) { - if (attachment_dict.hasOwnProperty(id)) { - return {}; - } - throw new jIO.util.jIOError("Cannot find document " + id, - 404); - }); - } - throw error; - }); - }; - - FileSystemBridgeStorage.prototype.allAttachments = function (id) { - var context = this; - return context._sub_storage.allAttachments(ROOT) - .push(function (attachment_dict) { - if (attachment_dict.hasOwnProperty(id)) { - return { - enclosure: {} - }; - } - // Second get the document itself if it exists - return context._sub_storage.getAttachment( - DOCUMENT_KEY, - id + DOCUMENT_EXTENSION - ) - .push(function () { - return {}; - }, function (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - throw new jIO.util.jIOError("Cannot find document " + id, - 404); - } - throw error; - }); - }); - - }; - - FileSystemBridgeStorage.prototype.put = function (doc_id, param) { - var context = this; - // XXX Handle conflict! - - return context._sub_storage.putAttachment( - DOCUMENT_KEY, - doc_id + DOCUMENT_EXTENSION, - new Blob([JSON.stringify(param)], {type: "application/json"}) - ) - .push(undefined, function (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - return context._sub_storage.put(DOCUMENT_KEY, {}) - .push(function () { - return context._sub_storage.putAttachment( - DOCUMENT_KEY, - doc_id + DOCUMENT_EXTENSION, - new Blob([JSON.stringify(param)], - {type: "application/json"}) - ); - }); - } - throw error; - }) - .push(function () { - return doc_id; - }); - - }; - - FileSystemBridgeStorage.prototype.remove = function (doc_id) { - var context = this, - got_error = false; - return new RSVP.Queue() - - // First, try to remove enclosure - .push(function () { - return context._sub_storage.removeAttachment( - ROOT, - doc_id - ); - }) - - .push(undefined, function (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - got_error = true; - return; - } - throw error; - }) - - // Second, try to remove explicit doc - .push(function () { - return context._sub_storage.removeAttachment( - DOCUMENT_KEY, - doc_id + DOCUMENT_EXTENSION - ); - }) - - .push(undefined, function (error) { - if ((!got_error) && (error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - return doc_id; - } - throw error; - }); - - }; - - FileSystemBridgeStorage.prototype.hasCapacity = function (capacity) { - return (capacity === "list"); - }; - - FileSystemBridgeStorage.prototype.buildQuery = function () { - var result_dict = {}, - context = this; - return new RSVP.Queue() - - // First, get list of explicit documents - - .push(function () { - return context._sub_storage.allAttachments(DOCUMENT_KEY); - }) - .push(function (result) { - var key; - for (key in result) { - if (result.hasOwnProperty(key)) { - if (endsWith(key, DOCUMENT_EXTENSION)) { - result_dict[key.substring( - 0, - key.length - DOCUMENT_EXTENSION.length - )] = null; - } - } - } - }, function (error) { - if ((error instanceof jIO.util.jIOError) && - (error.status_code === 404)) { - return; - } - throw error; - }) - - // Second, get list of enclosure - - .push(function () { - return context._sub_storage.allAttachments(ROOT); - }) - .push(function (result) { - var key; - for (key in result) { - if (result.hasOwnProperty(key)) { - result_dict[key] = null; - } - } - }) - - // Finally, build the result - - .push(function () { - var result = [], - key; - for (key in result_dict) { - if (result_dict.hasOwnProperty(key)) { - result.push({ - id: key, - value: {} - }); - } - } - return result; - }); - - }; - - FileSystemBridgeStorage.prototype.getAttachment = function (id, name) { - if (name !== "enclosure") { - throw new jIO.util.jIOError("Only support 'enclosure' attachment", - 400); - } - - return this._sub_storage.getAttachment(ROOT, id); - }; - - FileSystemBridgeStorage.prototype.putAttachment = function (id, name, blob) { - if (name !== "enclosure") { - throw new jIO.util.jIOError("Only support 'enclosure' attachment", - 400); - } - - return this._sub_storage.putAttachment( - ROOT, - id, - blob - ); - }; - - FileSystemBridgeStorage.prototype.removeAttachment = function (id, name) { - if (name !== "enclosure") { - throw new jIO.util.jIOError("Only support 'enclosure' attachment", - 400); - } - - return this._sub_storage.removeAttachment(ROOT, id); - }; - - FileSystemBridgeStorage.prototype.repair = function () { - return this._sub_storage.repair.apply(this._sub_storage, arguments); - }; - - jIO.addStorage('drivetojiomapping', FileSystemBridgeStorage); - -}(jIO, RSVP, Blob)); -;/*jslint nomen: true*/ -/*global Blob, RSVP, unescape, escape*/ -(function (jIO, Blob, RSVP, unescape, escape) { - "use strict"; - /** - * The jIO DocumentStorage extension - * - * @class DocumentStorage - * @constructor - */ - function DocumentStorage(spec) { - this._sub_storage = jIO.createJIO(spec.sub_storage); - this._document_id = spec.document_id; - this._repair_attachment = spec.repair_attachment || false; - } - - var DOCUMENT_EXTENSION = ".json", - DOCUMENT_REGEXP = new RegExp("^jio_document/([\\w=]+)" + - DOCUMENT_EXTENSION + "$"), - ATTACHMENT_REGEXP = new RegExp("^jio_attachment/([\\w=]+)/([\\w=]+)$"), - btoa = function (str) { - return window.btoa(unescape(encodeURIComponent(str))); - }, - atob = function (str) { - return decodeURIComponent(escape(window.atob(str))); - }; - - function getSubAttachmentIdFromParam(id, name) { - if (name === undefined) { - return 'jio_document/' + btoa(id) + DOCUMENT_EXTENSION; - } - return 'jio_attachment/' + btoa(id) + "/" + btoa(name); - } - - DocumentStorage.prototype.get = function (id) { - return this._sub_storage.getAttachment( - this._document_id, - getSubAttachmentIdFromParam(id), - {format: "json"} - ); - }; - - DocumentStorage.prototype.allAttachments = function (id) { - return this._sub_storage.allAttachments(this._document_id) - .push(function (result) { - var attachments = {}, - exec, - key; - for (key in result) { - if (result.hasOwnProperty(key)) { - if (ATTACHMENT_REGEXP.test(key)) { - exec = ATTACHMENT_REGEXP.exec(key); - try { - if (atob(exec[1]) === id) { - attachments[atob(exec[2])] = {}; - } - } catch (error) { - // Check if unable to decode base64 data - if (!error instanceof ReferenceError) { - throw error; - } - } - } - } - } - return attachments; - }); - }; - - DocumentStorage.prototype.put = function (doc_id, param) { - return this._sub_storage.putAttachment( - this._document_id, - getSubAttachmentIdFromParam(doc_id), - new Blob([JSON.stringify(param)], {type: "application/json"}) - ) - .push(function () { - return doc_id; - }); - - }; - - DocumentStorage.prototype.remove = function (id) { - var context = this; - return this.allAttachments(id) - .push(function (result) { - var key, - promise_list = []; - for (key in result) { - if (result.hasOwnProperty(key)) { - promise_list.push(context.removeAttachment(id, key)); - } - } - return RSVP.all(promise_list); - }) - .push(function () { - return context._sub_storage.removeAttachment( - context._document_id, - getSubAttachmentIdFromParam(id) - ); - }) - .push(function () { - return id; - }); - }; - - DocumentStorage.prototype.repair = function () { - var context = this; - return this._sub_storage.repair.apply(this._sub_storage, arguments) - .push(function (result) { - if (context._repair_attachment) { - return context._sub_storage.allAttachments(context._document_id) - .push(function (result_dict) { - var promise_list = [], - id_dict = {}, - attachment_dict = {}, - id, - attachment, - exec, - key; - for (key in result_dict) { - if (result_dict.hasOwnProperty(key)) { - id = undefined; - attachment = undefined; - if (DOCUMENT_REGEXP.test(key)) { - try { - id = atob(DOCUMENT_REGEXP.exec(key)[1]); - } catch (error) { - // Check if unable to decode base64 data - if (!error instanceof ReferenceError) { - throw error; - } - } - if (id !== undefined) { - id_dict[id] = null; - } - } else if (ATTACHMENT_REGEXP.test(key)) { - exec = ATTACHMENT_REGEXP.exec(key); - try { - id = atob(exec[1]); - attachment = atob(exec[2]); - } catch (error) { - // Check if unable to decode base64 data - if (!error instanceof ReferenceError) { - throw error; - } - } - if (attachment !== undefined) { - if (!id_dict.hasOwnProperty(id)) { - if (!attachment_dict.hasOwnProperty(id)) { - attachment_dict[id] = {}; - } - attachment_dict[id][attachment] = null; - } - } - } - } - } - for (id in attachment_dict) { - if (attachment_dict.hasOwnProperty(id)) { - if (!id_dict.hasOwnProperty(id)) { - for (attachment in attachment_dict[id]) { - if (attachment_dict[id].hasOwnProperty(attachment)) { - promise_list.push(context.removeAttachment( - id, - attachment - )); - } - } - } - } - } - return RSVP.all(promise_list); - }); - } - return result; - }); - }; - - DocumentStorage.prototype.hasCapacity = function (capacity) { - return (capacity === "list"); - }; - - DocumentStorage.prototype.buildQuery = function () { - return this._sub_storage.allAttachments(this._document_id) - .push(function (attachment_dict) { - var result = [], - key; - for (key in attachment_dict) { - if (attachment_dict.hasOwnProperty(key)) { - if (DOCUMENT_REGEXP.test(key)) { - try { - result.push({ - id: atob(DOCUMENT_REGEXP.exec(key)[1]), - value: {} - }); - } catch (error) { - // Check if unable to decode base64 data - if (!error instanceof ReferenceError) { - throw error; - } - } - } - } - } - return result; - }); - }; - - DocumentStorage.prototype.getAttachment = function (id, name) { - return this._sub_storage.getAttachment( - this._document_id, - getSubAttachmentIdFromParam(id, name) - ); - }; - - DocumentStorage.prototype.putAttachment = function (id, name, blob) { - return this._sub_storage.putAttachment( - this._document_id, - getSubAttachmentIdFromParam(id, name), - blob - ); - }; - - DocumentStorage.prototype.removeAttachment = function (id, name) { - return this._sub_storage.removeAttachment( - this._document_id, - getSubAttachmentIdFromParam(id, name) - ); - }; - - jIO.addStorage('document', DocumentStorage); - -}(jIO, Blob, RSVP, unescape, escape)); -;/* - * Copyright 2013, Nexedi SA - * Released under the LGPL license. - * http://www.gnu.org/licenses/lgpl.html - */ - -/*jslint nomen: true*/ -/*global jIO, sessionStorage, localStorage, RSVP */ - -/** - * JIO Local Storage. Type = 'local'. - * Local browser "database" storage. - * - * Storage Description: - * - * { - * "type": "local", - * "sessiononly": false - * } - * - * @class LocalStorage - */ - -(function (jIO, sessionStorage, localStorage, RSVP) { - "use strict"; - - function LocalStorage(spec) { - if (spec.sessiononly === true) { - this._storage = sessionStorage; - } else { - this._storage = localStorage; - } - } - - function restrictDocumentId(id) { - if (id !== "/") { - throw new jIO.util.jIOError("id " + id + " is forbidden (!== /)", - 400); - } - } - - LocalStorage.prototype.get = function (id) { - restrictDocumentId(id); - return {}; - }; - - LocalStorage.prototype.allAttachments = function (id) { - restrictDocumentId(id); - - var attachments = {}, - key; - - for (key in this._storage) { - if (this._storage.hasOwnProperty(key)) { - attachments[key] = {}; - } - } - return attachments; - }; - - LocalStorage.prototype.getAttachment = function (id, name) { - restrictDocumentId(id); - - var textstring = this._storage.getItem(name); - - if (textstring === null) { - throw new jIO.util.jIOError( - "Cannot find attachment " + name, - 404 - ); - } - return jIO.util.dataURItoBlob(textstring); - }; - - LocalStorage.prototype.putAttachment = function (id, name, blob) { - var context = this; - restrictDocumentId(id); - - // the document already exists - // download data - return new RSVP.Queue() - .push(function () { - return jIO.util.readBlobAsDataURL(blob); - }) - .push(function (e) { - context._storage.setItem(name, e.target.result); - }); - }; - - LocalStorage.prototype.removeAttachment = function (id, name) { - restrictDocumentId(id); - return this._storage.removeItem(name); - }; - - - LocalStorage.prototype.hasCapacity = function (name) { - return (name === "list"); - }; - - LocalStorage.prototype.buildQuery = function () { - return [{ - id: "/", - value: {} - }]; - }; - - jIO.addStorage('local', LocalStorage); - -}(jIO, sessionStorage, localStorage, RSVP)); -;/* - * Copyright 2014, Nexedi SA - * Released under the LGPL license. - * http://www.gnu.org/licenses/lgpl.html - */ - -/** - * JIO Indexed Database Storage. - * - * A local browser "database" storage greatly more powerful than localStorage. - * - * Description: - * - * { - * "type": "indexeddb", - * "database": - * } - * - * The database name will be prefixed by "jio:", so if the database property is - * "hello", then you can manually reach this database with - * `indexedDB.open("jio:hello");`. (Or - * `indexedDB.deleteDatabase("jio:hello");`.) - * - * For more informations: - * - * - http://www.w3.org/TR/IndexedDB/ - * - https://developer.mozilla.org/en-US/docs/IndexedDB/Using_IndexedDB - */ - -/*jslint nomen: true */ -/*global indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest, - DOMError, Event*/ - -(function (indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest, - DOMError) { - "use strict"; - - // Read only as changing it can lead to data corruption - var UNITE = 2000000; - - function IndexedDBStorage(description) { - if (typeof description.database !== "string" || - description.database === "") { - throw new TypeError("IndexedDBStorage 'database' description property " + - "must be a non-empty string"); - } - this._database_name = "jio:" + description.database; - } - - IndexedDBStorage.prototype.hasCapacity = function (name) { - return ((name === "list") || (name === "include")); - }; - - function buildKeyPath(key_list) { - return key_list.join("_"); - } - - function handleUpgradeNeeded(evt) { - var db = evt.target.result, - store; - - store = db.createObjectStore("metadata", { - keyPath: "_id", - autoIncrement: false - }); - // It is not possible to use openKeyCursor on keypath directly - // https://www.w3.org/Bugs/Public/show_bug.cgi?id=19955 - store.createIndex("_id", "_id", {unique: true}); - - store = db.createObjectStore("attachment", { - keyPath: "_key_path", - autoIncrement: false - }); - store.createIndex("_id", "_id", {unique: false}); - - store = db.createObjectStore("blob", { - keyPath: "_key_path", - autoIncrement: false - }); - store.createIndex("_id_attachment", - ["_id", "_attachment"], {unique: false}); - store.createIndex("_id", "_id", {unique: false}); - } - - function openIndexedDB(jio_storage) { - var db_name = jio_storage._database_name; - function resolver(resolve, reject) { - // Open DB // - var request = indexedDB.open(db_name); - request.onerror = function (error) { - if (request.result) { - request.result.close(); - } - if ((error !== undefined) && - (error.target instanceof IDBOpenDBRequest) && - (error.target.error instanceof DOMError)) { - reject("Connection to: " + db_name + " failed: " + - error.target.error.message); - } else { - reject(error); - } - }; - - request.onabort = function () { - request.result.close(); - reject("Aborting connection to: " + db_name); - }; - - request.ontimeout = function () { - request.result.close(); - reject("Connection to: " + db_name + " timeout"); - }; - - request.onblocked = function () { - request.result.close(); - reject("Connection to: " + db_name + " was blocked"); - }; - - // Create DB if necessary // - request.onupgradeneeded = handleUpgradeNeeded; - - request.onversionchange = function () { - request.result.close(); - reject(db_name + " was upgraded"); - }; - - request.onsuccess = function () { - resolve(request.result); - }; - } - // XXX Canceller??? - return new RSVP.Queue() - .push(function () { - return new RSVP.Promise(resolver); - }); - } - - function openTransaction(db, stores, flag, autoclosedb) { - var tx = db.transaction(stores, flag); - if (autoclosedb !== false) { - tx.oncomplete = function () { - db.close(); - }; - } - tx.onabort = function () { - db.close(); - }; - return tx; - } - - function handleCursor(request, callback, resolve, reject) { - request.onerror = function (error) { - if (request.transaction) { - request.transaction.abort(); - } - reject(error); - }; - - request.onsuccess = function (evt) { - var cursor = evt.target.result; - if (cursor) { - // XXX Wait for result - try { - callback(cursor); - } catch (error) { - reject(error); - } - - // continue to next iteration - cursor["continue"](); - } else { - resolve(); - } - }; - } - - IndexedDBStorage.prototype.buildQuery = function (options) { - var result_list = []; - - function pushIncludedMetadata(cursor) { - result_list.push({ - "id": cursor.key, - "value": {}, - "doc": cursor.value.doc - }); - } - - function pushMetadata(cursor) { - result_list.push({ - "id": cursor.key, - "value": {} - }); - } - return openIndexedDB(this) - .push(function (db) { - return new RSVP.Promise(function (resolve, reject) { - var tx = openTransaction(db, ["metadata"], "readonly"); - if (options.include_docs === true) { - handleCursor(tx.objectStore("metadata").index("_id").openCursor(), - pushIncludedMetadata, resolve, reject); - } else { - handleCursor(tx.objectStore("metadata").index("_id") - .openKeyCursor(), pushMetadata, resolve, reject); - } - }); - }) - .push(function () { - return result_list; - }); - }; - - function handleGet(store, id, resolve, reject) { - var request = store.get(id); - request.onerror = reject; - request.onsuccess = function () { - if (request.result) { - resolve(request.result); - } else { - reject(new jIO.util.jIOError( - "IndexedDB: cannot find object '" + id + "' in the '" + - store.name + "' store", - 404 - )); - } - }; - } - - IndexedDBStorage.prototype.get = function (id) { - return openIndexedDB(this) - .push(function (db) { - return new RSVP.Promise(function (resolve, reject) { - var transaction = openTransaction(db, ["metadata"], "readonly"); - handleGet( - transaction.objectStore("metadata"), - id, - resolve, - reject - ); - }); - }) - .push(function (result) { - return result.doc; - }); - }; - - IndexedDBStorage.prototype.allAttachments = function (id) { - var attachment_dict = {}; - - function addEntry(cursor) { - attachment_dict[cursor.value._attachment] = {}; - } - - return openIndexedDB(this) - .push(function (db) { - return new RSVP.Promise(function (resolve, reject) { - var transaction = openTransaction(db, ["metadata", "attachment"], - "readonly"); - function getAttachments() { - handleCursor( - transaction.objectStore("attachment").index("_id") - .openCursor(IDBKeyRange.only(id)), - addEntry, - resolve, - reject - ); - } - handleGet( - transaction.objectStore("metadata"), - id, - getAttachments, - reject - ); - }); - }) - .push(function () { - return attachment_dict; - }); - }; - - function handleRequest(request, resolve, reject) { - request.onerror = reject; - request.onsuccess = function () { - resolve(request.result); - }; - } - - IndexedDBStorage.prototype.put = function (id, metadata) { - return openIndexedDB(this) - .push(function (db) { - return new RSVP.Promise(function (resolve, reject) { - var transaction = openTransaction(db, ["metadata"], "readwrite"); - handleRequest( - transaction.objectStore("metadata").put({ - "_id": id, - "doc": metadata - }), - resolve, - reject - ); - }); - }); - }; - - function deleteEntry(cursor) { - cursor["delete"](); - } - - IndexedDBStorage.prototype.remove = function (id) { - var resolved_amount = 0; - return openIndexedDB(this) - .push(function (db) { - return new RSVP.Promise(function (resolve, reject) { - function resolver() { - if (resolved_amount < 2) { - resolved_amount += 1; - } else { - resolve(); - } - } - var transaction = openTransaction(db, ["metadata", "attachment", - "blob"], "readwrite"); - handleRequest( - transaction.objectStore("metadata")["delete"](id), - resolver, - reject - ); - // XXX Why not possible to delete with KeyCursor? - handleCursor(transaction.objectStore("attachment").index("_id") - .openCursor(IDBKeyRange.only(id)), - deleteEntry, - resolver, - reject - ); - handleCursor(transaction.objectStore("blob").index("_id") - .openCursor(IDBKeyRange.only(id)), - deleteEntry, - resolver, - reject - ); - }); - }); - }; - - IndexedDBStorage.prototype.getAttachment = function (id, name, options) { - var transaction, - type, - start, - end; - if (options === undefined) { - options = {}; - } - return openIndexedDB(this) - .push(function (db) { - return new RSVP.Promise(function (resolve, reject) { - transaction = openTransaction( - db, - ["attachment", "blob"], - "readonly" - ); - function getBlob(attachment) { - var total_length = attachment.info.length, - result_list = [], - store = transaction.objectStore("blob"), - start_index, - end_index; - type = attachment.info.content_type; - start = options.start || 0; - end = options.end || total_length; - if (end > total_length) { - end = total_length; - } - if (start < 0 || end < 0) { - throw new jIO.util.jIOError( - "_start and _end must be positive", - 400 - ); - } - if (start > end) { - throw new jIO.util.jIOError("_start is greater than _end", - 400); - } - start_index = Math.floor(start / UNITE); - end_index = Math.floor(end / UNITE) - 1; - if (end % UNITE === 0) { - end_index -= 1; - } - function resolver(result) { - if (result.blob !== undefined) { - result_list.push(result); - } - resolve(result_list); - } - function getPart(i) { - return function (result) { - if (result) { - result_list.push(result); - } - i += 1; - handleGet(store, - buildKeyPath([id, name, i]), - (i <= end_index) ? getPart(i) : resolver, - reject - ); - }; - } - getPart(start_index - 1)(); - } - // XXX Should raise if key is not good - handleGet(transaction.objectStore("attachment"), - buildKeyPath([id, name]), - getBlob, - reject - ); - }); - }) - .push(function (result_list) { - var array_buffer_list = [], - blob, - i, - index, - len = result_list.length; - for (i = 0; i < len; i += 1) { - array_buffer_list.push(result_list[i].blob); - } - if ((options.start === undefined) && (options.end === undefined)) { - return new Blob(array_buffer_list, {type: type}); - } - index = Math.floor(start / UNITE) * UNITE; - blob = new Blob(array_buffer_list, {type: "application/octet-stream"}); - return blob.slice(start - index, end - index, - "application/octet-stream"); - }); - }; - - function removeAttachment(transaction, id, name, resolve, reject) { - // XXX How to get the right attachment - function deleteContent() { - handleCursor( - transaction.objectStore("blob").index("_id_attachment") - .openCursor(IDBKeyRange.only([id, name])), - deleteEntry, - resolve, - reject - ); - } - handleRequest( - transaction.objectStore("attachment")["delete"]( - buildKeyPath([id, name]) - ), - deleteContent, - reject - ); - } - - IndexedDBStorage.prototype.putAttachment = function (id, name, blob) { - var blob_part = [], - transaction, - db; - - return openIndexedDB(this) - .push(function (database) { - db = database; - - // Split the blob first - return jIO.util.readBlobAsArrayBuffer(blob); - }) - .push(function (event) { - var array_buffer = event.target.result, - total_size = blob.size, - handled_size = 0; - - while (handled_size < total_size) { - blob_part.push(array_buffer.slice(handled_size, - handled_size + UNITE)); - handled_size += UNITE; - } - - // Remove previous attachment - transaction = openTransaction(db, ["attachment", "blob"], "readwrite"); - return new RSVP.Promise(function (resolve, reject) { - function write() { - var len = blob_part.length - 1, - attachment_store = transaction.objectStore("attachment"), - blob_store = transaction.objectStore("blob"); - function putBlobPart(i) { - return function () { - i += 1; - handleRequest( - blob_store.put({ - "_key_path": buildKeyPath([id, name, i]), - "_id" : id, - "_attachment" : name, - "_part" : i, - "blob": blob_part[i] - }), - (i < len) ? putBlobPart(i) : resolve, - reject - ); - }; - } - handleRequest( - attachment_store.put({ - "_key_path": buildKeyPath([id, name]), - "_id": id, - "_attachment": name, - "info": { - "content_type": blob.type, - "length": blob.size - } - }), - putBlobPart(-1), - reject - ); - } - removeAttachment(transaction, id, name, write, reject); - }); - }); - }; - - IndexedDBStorage.prototype.removeAttachment = function (id, name) { - return openIndexedDB(this) - .push(function (db) { - var transaction = openTransaction(db, ["attachment", "blob"], - "readwrite"); - return new RSVP.Promise(function (resolve, reject) { - removeAttachment(transaction, id, name, resolve, reject); - }); - }); - }; - - jIO.addStorage("indexeddb", IndexedDBStorage); -}(indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest, DOMError)); -;/* - * Copyright 2015, Nexedi SA - * Released under the LGPL license. - * http://www.gnu.org/licenses/lgpl.html - */ - -/*jslint nomen: true*/ -/*global jIO, RSVP, DOMException, Blob, crypto, Uint8Array, ArrayBuffer*/ - -(function (jIO, RSVP, DOMException, Blob, crypto, Uint8Array, ArrayBuffer) { - "use strict"; - - /* - The cryptography system used by this storage is AES-GCM. - Here is an example of how to generate a key to the json format: - - return new RSVP.Queue() - .push(function () { - return crypto.subtle.generateKey({name: "AES-GCM", length: 256}, - true, ["encrypt", "decrypt"]); - }) - .push(function (key) { - return crypto.subtle.exportKey("jwk", key); - }) - .push(function (json_key) { - var jio = jIO.createJIO({ - type: "crypt", - key: json_key, - sub_storage: {storage_definition} - }); - }); - - Find more informations about this cryptography system on - https://github.com/diafygi/webcrypto-examples#aes-gcm - */ - - /** - * The JIO Cryptography Storage extension - * - * @class CryptStorage - * @constructor - */ - - var MIME_TYPE = "application/x-jio-aes-gcm-encryption"; - - function CryptStorage(spec) { - this._key = spec.key; - this._jsonKey = true; - this._sub_storage = jIO.createJIO(spec.sub_storage); - } - - function convertKey(that) { - return new RSVP.Queue() - .push(function () { - return crypto.subtle.importKey("jwk", that._key, - "AES-GCM", false, - ["encrypt", "decrypt"]); - }) - .push(function (res) { - that._key = res; - that._jsonKey = false; - return; - }); - } - - CryptStorage.prototype.get = function () { - return this._sub_storage.get.apply(this._sub_storage, - arguments); - }; - - CryptStorage.prototype.post = function () { - return this._sub_storage.post.apply(this._sub_storage, - arguments); - }; - - CryptStorage.prototype.put = function () { - return this._sub_storage.put.apply(this._sub_storage, - arguments); - }; - - CryptStorage.prototype.remove = function () { - return this._sub_storage.remove.apply(this._sub_storage, - arguments); - }; - - CryptStorage.prototype.hasCapacity = function () { - return this._sub_storage.hasCapacity.apply(this._sub_storage, - arguments); - }; - - CryptStorage.prototype.buildQuery = function () { - return this._sub_storage.buildQuery.apply(this._sub_storage, - arguments); - }; - - - CryptStorage.prototype.putAttachment = function (id, name, blob) { - var initializaton_vector = crypto.getRandomValues(new Uint8Array(12)), - that = this; - - return new RSVP.Queue() - .push(function () { - if (that._jsonKey === true) { - return convertKey(that); - } - return; - }) - .push(function () { - return jIO.util.readBlobAsDataURL(blob); - }) - .push(function (dataURL) { - //string->arraybuffer - var strLen = dataURL.target.result.length, - buf = new ArrayBuffer(strLen), - bufView = new Uint8Array(buf), - i; - - dataURL = dataURL.target.result; - for (i = 0; i < strLen; i += 1) { - bufView[i] = dataURL.charCodeAt(i); - } - return crypto.subtle.encrypt({ - name : "AES-GCM", - iv : initializaton_vector - }, - that._key, buf); - }) - .push(function (coded) { - var blob = new Blob([initializaton_vector, coded], {type: MIME_TYPE}); - return that._sub_storage.putAttachment(id, name, blob); - }); - }; - - CryptStorage.prototype.getAttachment = function (id, name) { - var that = this; - - return that._sub_storage.getAttachment(id, name) - .push(function (blob) { - if (blob.type !== MIME_TYPE) { - return blob; - } - return new RSVP.Queue() - .push(function () { - if (that._jsonKey === true) { - return convertKey(that); - } - return; - }) - .push(function () { - return jIO.util.readBlobAsArrayBuffer(blob); - }) - .push(function (coded) { - var initializaton_vector; - - coded = coded.target.result; - initializaton_vector = new Uint8Array(coded.slice(0, 12)); - return new RSVP.Queue() - .push(function () { - return crypto.subtle.decrypt({ - name : "AES-GCM", - iv : initializaton_vector - }, - that._key, coded.slice(12)); - }) - .push(function (arr) { - //arraybuffer->string - arr = String.fromCharCode.apply(null, new Uint8Array(arr)); - return jIO.util.dataURItoBlob(arr); - }) - .push(undefined, function (error) { - if (error instanceof DOMException) { - return blob; - } - throw error; - }); - }); - }); - }; - - CryptStorage.prototype.removeAttachment = function () { - return this._sub_storage.removeAttachment.apply(this._sub_storage, - arguments); - }; - - CryptStorage.prototype.allAttachments = function () { - return this._sub_storage.allAttachments.apply(this._sub_storage, - arguments); - }; - - jIO.addStorage('crypt', CryptStorage); - -}(jIO, RSVP, DOMException, Blob, crypto, Uint8Array, ArrayBuffer)); -;/* - * Copyright 2013, Nexedi SA - * Released under the LGPL license. - * http://www.gnu.org/licenses/lgpl.html - */ -/** - * JIO Websql Storage. Type = "websql". - * websql "database" storage. - */ -/*global Blob, jIO, RSVP, openDatabase*/ -/*jslint nomen: true*/ - -(function (jIO, RSVP, Blob, openDatabase) { - - "use strict"; - - /** - * The JIO Websql Storage extension - * - * @class WebSQLStorage - * @constructor - */ - - function queueSql(db, query_list, argument_list) { - return new RSVP.Promise(function (resolve, reject) { - /*jslint unparam: true*/ - db.transaction(function (tx) { - var len = query_list.length, - result_list = [], - i; - - function resolveTransaction(tx, result) { - result_list.push(result); - if (result_list.length === len) { - resolve(result_list); - } - } - function rejectTransaction(tx, error) { - reject(error); - return true; - } - for (i = 0; i < len; i += 1) { - tx.executeSql(query_list[i], argument_list[i], resolveTransaction, - rejectTransaction); - } - }, function (tx, error) { - reject(error); - }); - /*jslint unparam: false*/ - }); - } - - function initDatabase(db) { - var query_list = [ - "CREATE TABLE IF NOT EXISTS document" + - "(id VARCHAR PRIMARY KEY NOT NULL, data TEXT)", - "CREATE TABLE IF NOT EXISTS attachment" + - "(id VARCHAR, attachment VARCHAR, part INT, blob TEXT)", - "CREATE TRIGGER IF NOT EXISTS removeAttachment " + - "BEFORE DELETE ON document FOR EACH ROW " + - "BEGIN DELETE from attachment WHERE id = OLD.id;END;", - "CREATE INDEX IF NOT EXISTS index_document ON document (id);", - "CREATE INDEX IF NOT EXISTS index_attachment " + - "ON attachment (id, attachment);" - ]; - return new RSVP.Queue() - .push(function () { - return queueSql(db, query_list, []); - }); - } - - function WebSQLStorage(spec) { - if (typeof spec.database !== 'string' || !spec.database) { - throw new TypeError("database must be a string " + - "which contains more than one character."); - } - this._database = openDatabase("jio:" + spec.database, - '1.0', '', 2 * 1024 * 1024); - if (spec.blob_length && - (typeof spec.blob_length !== "number" || - spec.blob_length < 20)) { - throw new TypeError("blob_len parameter must be a number >= 20"); - } - this._blob_length = spec.blob_length || 2000000; - this._init_db_promise = initDatabase(this._database); - } - - WebSQLStorage.prototype.put = function (id, param) { - var db = this._database, - that = this, - data_string = JSON.stringify(param); - - return new RSVP.Queue() - .push(function () { - return that._init_db_promise; - }) - .push(function () { - return queueSql(db, ["INSERT OR REPLACE INTO " + - "document(id, data) VALUES(?,?)"], - [[id, data_string]]); - }) - .push(function () { - return id; - }); - }; - - WebSQLStorage.prototype.remove = function (id) { - var db = this._database, - that = this; - - return new RSVP.Queue() - .push(function () { - return that._init_db_promise; - }) - .push(function () { - return queueSql(db, ["DELETE FROM document WHERE id = ?"], [[id]]); - }) - .push(function (result_list) { - if (result_list[0].rowsAffected === 0) { - throw new jIO.util.jIOError("Cannot find document", 404); - } - return id; - }); - - }; - - WebSQLStorage.prototype.get = function (id) { - var db = this._database, - that = this; - - return new RSVP.Queue() - .push(function () { - return that._init_db_promise; - }) - .push(function () { - return queueSql(db, ["SELECT data FROM document WHERE id = ?"], - [[id]]); - }) - .push(function (result_list) { - if (result_list[0].rows.length === 0) { - throw new jIO.util.jIOError("Cannot find document", 404); - } - return JSON.parse(result_list[0].rows[0].data); - }); - }; - - WebSQLStorage.prototype.allAttachments = function (id) { - var db = this._database, - that = this; - - return new RSVP.Queue() - .push(function () { - return that._init_db_promise; - }) - .push(function () { - return queueSql(db, [ - "SELECT id FROM document WHERE id = ?", - "SELECT DISTINCT attachment FROM attachment WHERE id = ?" - ], [[id], [id]]); - }) - .push(function (result_list) { - if (result_list[0].rows.length === 0) { - throw new jIO.util.jIOError("Cannot find document", 404); - } - - var len = result_list[1].rows.length, - obj = {}, - i; - - for (i = 0; i < len; i += 1) { - obj[result_list[1].rows[i].attachment] = {}; - } - return obj; - }); - }; - - function sendBlobPart(blob, argument_list, index, queue) { - queue.push(function () { - return jIO.util.readBlobAsDataURL(blob); - }) - .push(function (strBlob) { - argument_list[index + 2].push(strBlob.target.result); - return; - }); - } - - WebSQLStorage.prototype.putAttachment = function (id, name, blob) { - var db = this._database, - that = this, - part_size = this._blob_length; - - return new RSVP.Queue() - .push(function () { - return that._init_db_promise; - }) - .push(function () { - return queueSql(db, ["SELECT id FROM document WHERE id = ?"], [[id]]); - }) - .push(function (result) { - var query_list = [], - argument_list = [], - blob_size = blob.size, - queue = new RSVP.Queue(), - i, - index; - - if (result[0].rows.length === 0) { - throw new jIO.util.jIOError("Cannot access subdocument", 404); - } - query_list.push("DELETE FROM attachment WHERE id = ? " + - "AND attachment = ?"); - argument_list.push([id, name]); - query_list.push("INSERT INTO attachment(id, attachment, part, blob)" + - "VALUES(?, ?, ?, ?)"); - argument_list.push([id, name, -1, - blob.type || "application/octet-stream"]); - - for (i = 0, index = 0; i < blob_size; i += part_size, index += 1) { - query_list.push("INSERT INTO attachment(id, attachment, part, blob)" + - "VALUES(?, ?, ?, ?)"); - argument_list.push([id, name, index]); - sendBlobPart(blob.slice(i, i + part_size), argument_list, index, - queue); - } - queue.push(function () { - return queueSql(db, query_list, argument_list); - }); - return queue; - }); - }; - - WebSQLStorage.prototype.getAttachment = function (id, name, options) { - var db = this._database, - that = this, - part_size = this._blob_length, - start, - end, - start_index, - end_index; - - if (options === undefined) { options = {}; } - start = options.start || 0; - end = options.end || -1; - - if (start < 0 || (options.end !== undefined && options.end < 0)) { - throw new jIO.util.jIOError("_start and _end must be positive", - 400); - } - if (start > end && end !== -1) { - throw new jIO.util.jIOError("_start is greater than _end", - 400); - } - - start_index = Math.floor(start / part_size); - if (start === 0) { start_index -= 1; } - end_index = Math.floor(end / part_size); - if (end % part_size === 0) { - end_index -= 1; - } - - return new RSVP.Queue() - .push(function () { - return that._init_db_promise; - }) - .push(function () { - var command = "SELECT part, blob FROM attachment WHERE id = ? AND " + - "attachment = ? AND part >= ?", - argument_list = [id, name, start_index]; - - if (end !== -1) { - command += " AND part <= ?"; - argument_list.push(end_index); - } - return queueSql(db, [command], [argument_list]); - }) - .push(function (response_list) { - var i, - response, - blob_array = [], - blob, - type; - - response = response_list[0].rows; - if (response.length === 0) { - throw new jIO.util.jIOError("Cannot find document", 404); - } - for (i = 0; i < response.length; i += 1) { - if (response[i].part === -1) { - type = response[i].blob; - start_index += 1; - } else { - blob_array.push(jIO.util.dataURItoBlob(response[i].blob)); - } - } - if ((start === 0) && (options.end === undefined)) { - return new Blob(blob_array, {type: type}); - } - blob = new Blob(blob_array, {}); - return blob.slice(start - (start_index * part_size), - end === -1 ? blob.size : - end - (start_index * part_size), - "application/octet-stream"); - }); - }; - - WebSQLStorage.prototype.removeAttachment = function (id, name) { - var db = this._database, - that = this; - - return new RSVP.Queue() - .push(function () { - return that._init_db_promise; - }) - .push(function () { - return queueSql(db, ["DELETE FROM attachment WHERE " + - "id = ? AND attachment = ?"], [[id, name]]); - }) - .push(function (result) { - if (result[0].rowsAffected === 0) { - throw new jIO.util.jIOError("Cannot find document", 404); - } - return name; - }); - }; - - WebSQLStorage.prototype.hasCapacity = function (name) { - return (name === "list" || (name === "include")); - }; - - WebSQLStorage.prototype.buildQuery = function (options) { - var db = this._database, - that = this, - query = "SELECT id"; - - return new RSVP.Queue() - .push(function () { - return that._init_db_promise; - }) - .push(function () { - if (options === undefined) { options = {}; } - if (options.include_docs === true) { - query += ", data AS doc"; - } - query += " FROM document"; - return queueSql(db, [query], [[]]); - }) - .push(function (result) { - var array = [], - len = result[0].rows.length, - i; - - for (i = 0; i < len; i += 1) { - array.push(result[0].rows[i]); - array[i].value = {}; - if (array[i].doc !== undefined) { - array[i].doc = JSON.parse(array[i].doc); - } - } - return array; - }); - }; - - jIO.addStorage('websql', WebSQLStorage); - -}(jIO, RSVP, Blob, openDatabase)); -;/*jslint nomen: true */ -/*global RSVP, UriTemplate*/ -(function (jIO, RSVP, UriTemplate) { - "use strict"; - - var GET_POST_URL = "https://graph.facebook.com/v2.9/{+post_id}" + - "?fields={+fields}&access_token={+access_token}", - get_post_template = UriTemplate.parse(GET_POST_URL), - GET_FEED_URL = "https://graph.facebook.com/v2.9/{+user_id}/feed" + - "?fields={+fields}&limit={+limit}&since={+since}&access_token=" + - "{+access_token}", - get_feed_template = UriTemplate.parse(GET_FEED_URL); - - function FBStorage(spec) { - if (typeof spec.access_token !== 'string' || !spec.access_token) { - throw new TypeError("Access Token must be a string " + - "which contains more than one character."); - } - if (typeof spec.user_id !== 'string' || !spec.user_id) { - throw new TypeError("User ID must be a string " + - "which contains more than one character."); - } - this._access_token = spec.access_token; - this._user_id = spec.user_id; - this._default_field_list = spec.default_field_list || []; - this._default_limit = spec.default_limit || 500; - } - - FBStorage.prototype.get = function (id) { - var that = this; - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - type: "GET", - url: get_post_template.expand({post_id: id, - fields: that._default_field_list, access_token: that._access_token}) - }); - }) - .push(function (result) { - return JSON.parse(result.target.responseText); - }); - }; - - function paginateResult(url, result, select_list) { - return new RSVP.Queue() - .push(function () { - return jIO.util.ajax({ - type: "GET", - url: url - }); - }) - .push(function (response) { - return JSON.parse(response.target.responseText); - }, - function (err) { - throw new jIO.util.jIOError("Getting feed failed " + err.toString(), - err.target.status); - }) - .push(function (response) { - if (response.data.length === 0) { - return result; - } - var i, j, obj = {}; - for (i = 0; i < response.data.length; i += 1) { - obj.id = response.data[i].id; - obj.value = {}; - for (j = 0; j < select_list.length; j += 1) { - obj.value[select_list[j]] = response.data[i][select_list[j]]; - } - result.push(obj); - obj = {}; - } - return paginateResult(response.paging.next, result, select_list); - }); - } - - FBStorage.prototype.buildQuery = function (query) { - var that = this, fields = [], limit = this._default_limit, - template_argument = { - user_id: this._user_id, - limit: limit, - access_token: this._access_token - }; - if (query.include_docs) { - fields = fields.concat(that._default_field_list); - } - if (query.select_list) { - fields = fields.concat(query.select_list); - } - if (query.limit) { - limit = query.limit[1]; - } - template_argument.fields = fields; - template_argument.limit = limit; - return paginateResult(get_feed_template.expand(template_argument), [], - fields) - .push(function (result) { - if (!query.limit) { - return result; - } - return result.slice(query.limit[0], query.limit[1]); - }); - }; - - FBStorage.prototype.hasCapacity = function (name) { - var this_storage_capacity_list = ["list", "select", "include", "limit"]; - if (this_storage_capacity_list.indexOf(name) !== -1) { - return true; - } - }; - - jIO.addStorage('facebook', FBStorage); - -}(jIO, RSVP, UriTemplate)); \ No newline at end of file diff --git a/src/jio.storage/historystorage.js b/src/jio.storage/historystorage.js index 31bcbd5..59c9506 100644 --- a/src/jio.storage/historystorage.js +++ b/src/jio.storage/historystorage.js @@ -1,6 +1,6 @@ /*jslint nomen: true*/ -/*global RSVP*/ -(function (jIO, RSVP) { +/*global RSVP, SimpleQuery, ComplexQuery*/ +(function (jIO, RSVP, SimpleQuery, ComplexQuery) { "use strict"; // Used to distinguish between operations done within the same millisecond @@ -12,88 +12,7 @@ .toString(16)).slice(-4), timestamp = Date.now().toString(); return timestamp + "-" + uuid; - }, - // Helper function for getAttachment - findAttachment = function (substorage, name, metadata_query, steps) { - var options = { - query: metadata_query, - sort_on: [["timestamp", "descending"], ["op", "ascending"]], - select_list: ["op", "name"] - }; - return substorage.allDocs(options) - .push(function (results) { - var ind, - id = metadata_query.value, - count = 0; - // At the least, a document needs to have been put and an attachment - // needs to have been put - if (results.data.rows.length > 1) { - for (ind = 0; ind < results.data.rows.length; ind += 1) { - - // Cannot get the attachment of a removed document - if (results.data.rows[ind].value.op === "remove") { - throw new jIO.util.jIOError( - "HistoryStorage: cannot find attachment '" + name + - "' of object '" + id + "' (removed)", - 404 - ); - } - - // Make sure to get the correct revision of the attachment - // and throw 404 error if it was removed - if (results.data.rows[ind].value.name === name) { - if (count === steps) { - if (results.data.rows[ind].value.op === "removeAttachment") { - throw new jIO.util.jIOError( - "HistoryStorage: cannot find attachment '" + name + - "' of object '" + id + "' (removed)", - 404 - ); - } - return substorage.getAttachment( - results.data.rows[ind].id, - name - ); - } - count += 1; - } - } - } - throw new jIO.util.jIOError( - "HistoryStorage: cannot find attachment '" + name + - "' of object '" + id + "'", - 404 - ); - }); - }, - findDoc = function (substorage, metadata_query, steps) { - var options = { - query: metadata_query, - sort_on: [["timestamp", "descending"]], - select_list: ["op"], - limit: [steps, 1] - }; - return substorage.allDocs(options) - .push(function (results) { - var id_in = metadata_query.query_list[0].value; - if (results.data.rows.length > 0) { - if (results.data.rows[0].value.op === "put") { - return substorage.get(results.data.rows[0].id) - .push(function (result) { - return result.doc; - }); - } - throw new jIO.util.jIOError( - "HistoryStorage: cannot find object '" + id_in + "' (removed)", - 404 - ); - } - throw new jIO.util.jIOError( - "HistoryStorage: cannot find object '" + id_in + "'", - 404 - ); - }); - }; + }; /** @@ -104,55 +23,90 @@ */ function HistoryStorage(spec) { this._sub_storage = jIO.createJIO(spec.sub_storage); + this._timestamps = {}; } HistoryStorage.prototype.get = function (id_in) { // Query to get the last edit made to this document var substorage = this._sub_storage, - metadata_query; - - // Include id_in as value in query object for safety - metadata_query = jIO.QueryFactory.create( - "(doc_id: undefined) AND ((op: put) OR (op: remove))" - ); - metadata_query.query_list[0].value = id_in; - return findDoc(substorage, metadata_query, 0) - .push(undefined, - // If no documents returned in first query, check if the id is encoding - // revision information - function (error) { - - if (!(error instanceof jIO.util.jIOError) || - (error.status_code !== 404) || - (error.message !== "HistoryStorage: cannot find object '" + - id_in + "'")) { - throw error; + // Include id_in as value in query object for safety + metadata_query = new ComplexQuery({ + operator: "AND", + query_list: [ + new SimpleQuery({key: "doc_id", value: id_in}), + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "remove"}), + new SimpleQuery({key: "op", value: "put"}) + ] + }) + ] + }), + options = { + query: metadata_query, + select_list: ["op"], + limit: [0, 1], + sort_on: [["timestamp", "descending"]] + }; + return substorage.allDocs(options) + .push(function (results) { + if (results.data.rows.length > 0) { + if (results.data.rows[0].value.op === "put") { + return substorage.get(results.data.rows[0].id) + .push(function (result) { + return result.doc; + }, function (error) { + if (error.status_code === 400 && + error instanceof jIO.util.jIOError) { + throw new jIO.util.jIOError( + "HistoryStorage: cannot find object '" + id_in + + "'", + 404 + ); + } + }); } - - // "_-" is the revision signature used to indicate a previous revision - var steps, - steps_loc = id_in.lastIndexOf("_-"); - - // If the revision signature '_-' is not contained in the id, then - // the first findDoc call should have found the id if it exists - if (steps_loc === -1) { + throw new jIO.util.jIOError( + "HistoryStorage: cannot find object '" + id_in + "' (removed)", + 404 + ); + } + // Try again by treating id_in as a timestamp instead of a name + return substorage.get(id_in) + .push(function (result) { + if (result.op === "put") { + return result.doc; + } throw new jIO.util.jIOError( - "HistoryStorage: cannot find object '" + id_in + "'", + "HistoryStorage: cannot find object '" + id_in + + "' (removed)", 404 ); - } - - // If revision signature is found, query storage based on this - steps = Number(id_in.slice(steps_loc + 2)); - id_in = id_in.slice(0, steps_loc); - metadata_query.query_list[0].value = id_in; - return findDoc(substorage, metadata_query, steps); - }); + }, function (error) { + if (error.status_code === 400 && + error instanceof jIO.util.jIOError) { + throw new jIO.util.jIOError( + "HistoryStorage: cannot find object '" + id_in + + "'", + 404 + ); + } + }); + }); }; HistoryStorage.prototype.put = function (id, data) { + + if (data.hasOwnProperty("_timestamp")) { + throw new jIO.util.jIOError( + "Document cannot have metadata attribute '_timestamp'", + 422 + ); + } + var timestamp = unique_timestamp(), metadata = { // XXX: remove this attribute once query can sort_on id @@ -161,6 +115,11 @@ doc: data, op: "put" }; + if (this._timestamps.hasOwnProperty(id)) { + this._timestamps[id].push(timestamp); + } else { + this._timestamps[id] = [timestamp]; + } return this._sub_storage.put(timestamp, metadata); }; @@ -172,55 +131,57 @@ doc_id: id, op: "remove" }; + this._timestamps[id].push(timestamp); return this._sub_storage.put(timestamp, metadata); }; HistoryStorage.prototype.allAttachments = function (id) { - // XXX: Do we need to be able to retrieve older revisions with - // allAttachments? - - // XXX: If document is removed, should this throw a 404? - + // XXX: If instead you passed a timestamp in as `id`, we could retrieve all + // the attachments of the document at that point in time. Not sure if this + // would be useful. var substorage = this._sub_storage, - query_obj, - options; - - // Include id as value in query object for safety (as opposed to string - // concatenation) - query_obj = jIO.QueryFactory.create( - "(doc_id: undefined) AND ((op: putAttachment) OR (op: removeAttachment))" - ); - query_obj.query_list[0].value = id; + // Include id as value in query object for safety (as opposed to string + // concatenation) + query_obj = new ComplexQuery({ + operator: "AND", + query_list: [ + new SimpleQuery({key: "doc_id", value: id}), + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "putAttachment"}), + new SimpleQuery({key: "op", value: "removeAttachment"}) + ] + }) + ] + }), - // Only query for attachment edits - options = { - query: query_obj, - sort_on: [["timestamp", "descending"]] - }; + // Only query for attachment edits + options = { + query: query_obj, + sort_on: [["timestamp", "descending"]], + select_list: ["op", "timestamp", "name"] + }; return this._sub_storage.allDocs(options) - .push(function (results) { - var promises = results.data.rows.map(function (data) { - return substorage.get(data.id); - }); - return RSVP.all(promises); - }) .push(function (results) { var seen = {}, - attachments = {}, + attachments = [], + attachment_promises = [], ind, - doc; - // Only include attachments whose most recent edit is a putAttachment - // (and not a removeAttachment) - for (ind = 0; ind < results.length; ind += 1) { - doc = results[ind]; - if (!seen.hasOwnProperty(doc.name)) { - if (doc.op === "putAttachment") { - attachments[doc.name] = {}; - } - seen[doc.name] = {}; + entry; + attachments = results.data.rows.filter(function (docum) { + if (!seen.hasOwnProperty(docum.value.name)) { + var output = (docum.value.op === "putAttachment"); + seen[docum.value.name] = {}; + return output; } + }); + for (ind = 0; ind < attachments.length; ind += 1) { + entry = attachments[ind]; + attachment_promises[entry.value.name] = + substorage.getAttachment(entry.id, entry.value.name); } - return attachments; + return RSVP.hash(attachment_promises); }); }; @@ -234,6 +195,11 @@ op: "putAttachment" }, substorage = this._sub_storage; + if (this._timestamps[id].hasOwnProperty(name)) { + this._timestamps[id][name].push(timestamp); + } else { + this._timestamps[id][name] = [timestamp]; + } return this._sub_storage.put(timestamp, metadata) .push(function () { return substorage.putAttachment(timestamp, name, blob); @@ -244,45 +210,95 @@ // Query to get the last edit made to this document var substorage = this._sub_storage, - metadata_query; - - // Include id_in as value in query object for safety - metadata_query = jIO.QueryFactory.create( - "(doc_id: undefined)" - ); - metadata_query.value = id; - return findAttachment(substorage, name, metadata_query, 0) - .push(undefined, - - // If no documents returned in first query, check if the id is encoding - // revision information - function (error) { - - if (!(error instanceof jIO.util.jIOError) || - (error.status_code !== 404) || - (error.message !== "HistoryStorage: cannot find attachment '" + - name + "' of object '" + id + "'")) { - throw error; - } - var steps, - steps_loc = name.lastIndexOf("_-"); - - // If revision signature is not in id_in, than return 404, since id - // is not found - if (steps_loc === -1) { + // Include id_in as value in query object for safety + // "doc_id: id AND + // (op: remove OR ((op: putAttachment OR op: removeAttachment) AND + // name: name))" + metadata_query = new ComplexQuery({ + operator: "AND", + query_list: [ + new SimpleQuery({key: "doc_id", value: id}), + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "remove"}), + new ComplexQuery({ + operator: "AND", + query_list: [ + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "putAttachment"}), + new SimpleQuery({key: "op", value: "removeAttachment"}) + ] + }), + new SimpleQuery({key: "name", value: name}) + ] + }) + ] + }) + ] + }), + options = { + query: metadata_query, + sort_on: [["timestamp", "descending"]], + limit: [0, 1], + select_list: ["op", "name"] + }; + return substorage.allDocs(options) + .push(function (results) { + if (results.data.rows.length > 0) { + if (results.data.rows[0].value.op === "remove" || + results.data.rows[0].value.op === "removeAttachment") { throw new jIO.util.jIOError( - "HistoryStorage: cannot find attachment '" + name + - "' of object '" + id + "'", + "HistoryStorage: cannot find object '" + id + "' (removed)", 404 ); } - - // If revision signature is found, query storage based on this - steps = Number(name.slice(steps_loc + 2)); - name = name.slice(0, steps_loc); - return findAttachment(substorage, name, metadata_query, steps); - }); + return substorage.getAttachment(results.data.rows[0].id, name) + .push(undefined, function (error) { + if (error.status_code === 404 && + error instanceof jIO.util.jIOError) { + throw new jIO.util.jIOError( + "HistoryStorage: cannot find object '" + id + "'", + 404 + ); + } + throw error; + }); + } + return substorage.get(id) + .push(function (result) { + if (result.op === "putAttachment") { + return substorage.getAttachment(id, result.name); + } + throw new jIO.util.jIOError( + "HistoryStorage: cannot find object '" + id + "' (removed)", + 404 + ); + }, + function (error) { + if (error.status_code === 404 && + error instanceof jIO.util.jIOError) { + throw new jIO.util.jIOError( + "HistoryStorage: cannot find object '" + id + "'", + 404 + ); + } + throw error; + }) + .push(undefined, function (error) { + if (error.status_code === 404 && + error instanceof jIO.util.jIOError) { + throw new jIO.util.jIOError( + "HistoryStorage: cannot find object '" + id + "'", + 404 + ); + } + throw error; + }); + }); }; HistoryStorage.prototype.removeAttachment = function (id, name) { @@ -294,6 +310,7 @@ name: name, op: "removeAttachment" }; + this._timestamps[id][name].push(timestamp); return this._sub_storage.put(timestamp, metadata); }; HistoryStorage.prototype.repair = function () { @@ -328,19 +345,20 @@ query_obj = options.query, query_stack = [], ind; - if (query_obj.hasOwnProperty("query_list")) { + + if (query_obj instanceof ComplexQuery) { query_stack.push(query_obj); } else { - rev_query = (query_obj.key === "_REVISION"); + rev_query = (query_obj.key === "_timestamp"); } - // Traverse through query tree to find mentions of _REVISION + // Traverse through query tree to find mentions of _timestamp // and stop as soon as it is found once while (query_stack.length > 0 && (!rev_query)) { query_obj = query_stack.pop(); for (ind = 0; ind < query_obj.query_list.length; ind += 1) { if (query_obj.query_list[ind].hasOwnProperty("query_list")) { query_stack.push(query_obj.query_list[ind]); - } else if (query_obj.query_list[ind].key === "_REVISION") { + } else if (query_obj.query_list[ind].key === "_timestamp") { rev_query = true; break; } @@ -363,95 +381,50 @@ }); return RSVP.all(promises); }) - .push(function (results) { - // Label all documents with their current revision status - var docum, - revision_tracker = {}, - latest_rev_query, - results_reduced; - for (ind = 0; ind < results.length; ind += 1) { - docum = results[ind]; - if (revision_tracker.hasOwnProperty(docum.doc_id)) { - revision_tracker[docum.doc_id] += 1; - } else { - revision_tracker[docum.doc_id] = 0; + var seen = {}, + query_matches, + docs_to_query; + // If !rev_query, then by default only consider latest revisions of + // documents + results = results.filter(function (docum) { + if (rev_query) { + return docum.op === "put"; } - if (docum.op === "remove") { - docum.doc = {}; + if (!seen.hasOwnProperty(docum.doc_id)) { + seen[docum.doc_id] = {}; + return docum.op === "put"; } - - // Add op and _REVISION to the docum.doc (temporarily) so the - // document can be matched manually with the inputted query - results[ind].doc._REVISION = revision_tracker[docum.doc_id]; - results[ind].doc.op = docum.op; - } - - // Create a new query to only get non-removed revisions and abide by - // whatever the inputted query says - latest_rev_query = jIO.QueryFactory.create( - "(_REVISION: >= 0) AND (op: put)" - ); - - // If query does not use _REVISION, then by default set _REVISION := 0 - if (rev_query) { - latest_rev_query.query_list[0] = options.query; - } else { - latest_rev_query.query_list[0] = jIO.QueryFactory.create( - "(_REVISION: = 0)" - ); - - // Check if options.query is nonempty - if (options.query.type === "simple" || - options.query.type === "complex") { - latest_rev_query.query_list.push(options.query); + return false; + }); + docs_to_query = results.map(function (docum) { + // If it's a "remove" operation + if (!docum.hasOwnProperty("doc")) { + docum.doc = {}; } - } - //return results - results_reduced = results - // Only return results which match latest_rev_query - .filter(function (docum) { - var filtered_res = latest_rev_query.match(docum.doc); - - // Remove extra metadata used in revision query - delete docum.doc.op; - delete docum.doc._REVISION; - return filtered_res; - }); - return results_reduced - - // Only return the correct range of valid results specified by - // options.limit - .filter(function (doc, ind) { - if (doc && options.hasOwnProperty("limit")) { - return (ind >= options.limit[0] && - options.limit[1] + options.limit[0] > ind); - } - return true; - }) - - // Return certain attributes in .val as specified by - // options.select_list - .map(function (current_doc) { - var val = {}, - ind, - key; - for (ind = 0; ind < options.select_list.length; ind += 1) { - key = options.select_list[ind]; - if (current_doc.doc.hasOwnProperty(key)) { - val[key] = current_doc.doc[key]; - } - } - // Format results to be expected output of allDocs - return { - doc: current_doc.doc, - value: val, - id: current_doc.doc_id - }; - }); + docum.doc._doc_id = docum.doc_id; + docum.doc._timestamp = docum.timestamp; + return docum.doc; + }); + options.select_list.push("_doc_id"); + query_matches = options.query.exec(docs_to_query, options); + return query_matches; + }) + // Format the results of the query, and return + .push(function (query_matches) { + return query_matches.map(function (docum) { + var doc_id = docum._doc_id; + delete docum._timestamp; + delete docum._doc_id; + return { + doc: {}, + value: docum, + id: doc_id + }; + }); }); }; jIO.addStorage('history', HistoryStorage); -}(jIO, RSVP)); \ No newline at end of file +}(jIO, RSVP, SimpleQuery, ComplexQuery)); \ No newline at end of file diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index bf82708..930b623 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -24,182 +24,20 @@ ///////////////////////////////////////////////////////////////// - // historyStorage.querying_from_historystorage + // Attachments ///////////////////////////////////////////////////////////////// -/** - module("HistoryStorage.querying_from_historystorage"); - test("verifying the correct results are returned from historyStorage.allDocs", - function () { - stop(); - expect(10); + module("HistoryStorage.attachments", { + setup: function () { // create storage of type "history" with memory as substorage - var jio = jIO.createJIO({ + var dbname = "db_" + Date.now(); + this.blob1 = new Blob(['a']); + this.blob2 = new Blob(['b']); + this.blob3 = new Blob(['ccc']); + this.other_blob = new Blob(['1']); + this.jio = jIO.createJIO({ type: "history", sub_storage: { - type: "uuid", - sub_storage: { - type: "memory" - } - } - }); - jio.put("bar", {"title": "foo0"}) - .push(function () { - return RSVP.all([ - jio.remove("bar"), - jio.put("bar", {"title": "foo1"}), - jio.put("bar", {"title": "foo2"}), - jio.put("bar", {"title": "foo3"}), - jio.put("barbar", {"title": "attr0"}), - jio.put("barbar", {"title": "attr1"}), - jio.put("barbar", {"title": "attr2"}), - jio.put("barbarbar", {"title": "val0"}), - jio.put("barbarbarbar", {"title": "prop0"}) - ]); - }) - // Make two final puts so we know what to expect as the current state of - // each document. - .push(function () { - return jio.put("barbar", {"title": "attr3"}); - }) - .push(function () { - return jio.put("bar", {"title": "foo4"}); - }) - - // Queries should only include information about the final two versions - .push(function () { - return jio.allDocs({ - query: "", - sort_on: [["title", "ascending"]] - }); - }) - .push(function (results) { - equal(results.data.rows.length, - 4, - "Empty query yields four results since there are four unique docs"); - return jio.get(results.data.rows[0].id); - }, - function (error) { - return ok(false, "Query failed: " + error); - }) - .push(function (result) { - deepEqual(result, { - title: "attr3" - }, - "NOT IMPLEMENTED: Retrieve correct sort order with no metadata"); - }, - function () { - return ok(false, "Couldn't find document in storage"); - }) - - // Querying with a limit - .push(function () { - return jio.allDocs({ - query: "", - sort_on: [["title", "ascending"]], - limit: [0, 1] - }); - }) - .push(function (results) { - equal(results.data.rows.length, - 1, - "Since limit [0,1] was supplied, only 1st document is returned"); - return jio.get(results.data.rows[0].id); - }) - .push(function (result) { - deepEqual(result, { - title: "attr3" - }, - "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); - }) - - // Querying with a more complicated limit - .push(function () { - return jio.allDocs({ - query: "", - sort_on: [["title", "ascending"]], - limit: [2, 2] - }); - }) - .push(function (results) { - equal(results.data.rows.length, - 2, - "Retrieving the correct documents when options.limit is specified"); - - deepEqual(results.data.rows[0].id, - "barbarbarbar", - "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); - - deepEqual(results.data.rows[1].id, - "barbarbar", - "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); - - return jio.get(results.data.rows[0].id); - }) - .push(function (result) { - deepEqual(result, { - title: "property0" - }, - "NOT IMPLEMENTED: retrieving documents in specified sort_on order"); - }) - - // Querying for a specific id - .push(function () { - return jio.allDocs({ - query: "id: bar" - }); - }) - .push(function (results) { - equal(results.data.rows.length, - 1, - "NOT IMPLEMENTED: query involving specific document attributes"); - return jio.get(results.data.rows[0].id); - }) - .push(function (result) { - deepEqual(result, { - title: "foo4" - }, - "NOT IMPLEMENTED: query involving specific document attributes"); - }, - function () { - ok(false, - "NOT IMPLEMENTED: query involving specific document attributes" - ); - }) - - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); -**/ - ///////////////////////////////////////////////////////////////// - // Attachments - ///////////////////////////////////////////////////////////////// - - module("HistoryStorage.attachments"); - test("Testing proper adding/removing attachments", - function () { - stop(); - expect(26); - - // create storage of type "history" with memory as substorage - var dbname = "db_" + Date.now(), - jio = jIO.createJIO({ - type: "history", - sub_storage: { - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - } - }), - not_history = jIO.createJIO({ type: "query", sub_storage: { type: "uuid", @@ -208,24 +46,42 @@ database: dbname } } - }), + } + }); + this.not_history = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + }); + } + }); + test("Testing proper adding/removing attachments", + function () { + stop(); + expect(7); + var jio = this.jio, + timestamps = this.jio.__storage._timestamps, + blob2 = this.blob2, + blob1 = this.blob1, + other_blob = this.other_blob; - blob1 = new Blob(['a']), - blob2 = new Blob(['b']), - blob3 = new Blob(['ccc']), - other_blob = new Blob(['1']); jio.put("doc", {title: "foo0"}) .push(function () { return jio.put("doc2", {key: "val"}); }) .push(function () { - return jio.putAttachment("doc", "attached", blob1); + return jio.putAttachment("doc", "attacheddata", blob1); }) .push(function () { - return jio.putAttachment("doc", "attached", blob2); + return jio.putAttachment("doc", "attacheddata", blob2); }) .push(function () { - return jio.putAttachment("doc", "other_attached", other_blob); + return jio.putAttachment("doc", "other_attacheddata", other_blob); }) .push(function () { return jio.get("doc"); @@ -234,14 +90,17 @@ deepEqual(result, { title: "foo0" }, "Get does not return any attachment/revision information"); - return jio.getAttachment("doc", "attached"); + return jio.getAttachment("doc", "attacheddata"); }) .push(function (result) { deepEqual(result, blob2, "Return the attachment information with getAttachment" ); - return jio.getAttachment("doc", "attached_-0"); + return jio.getAttachment( + timestamps.doc.attacheddata[1], + "attacheddata" + ); }) .push(function (result) { deepEqual(result, @@ -249,8 +108,12 @@ "Return the attachment information with getAttachment for " + "current revision" ); - return jio.getAttachment("doc", "attached_-1"); + return jio.getAttachment( + timestamps.doc.attacheddata[0], + "attacheddata" + ); }, function (error) { + //console.log(error); ok(false, error); }) .push(function (result) { @@ -259,7 +122,7 @@ "Return the attachment information with getAttachment for " + "previous revision" ); - return jio.getAttachment("doc", "attached_-2"); + return jio.getAttachment(timestamps.doc[0], "attached"); }, function (error) { ok(false, error); }) @@ -270,22 +133,56 @@ ok(error instanceof jIO.util.jIOError, "Correct type of error"); deepEqual(error.status_code, 404, - "Error if you try to go back more revisions than what exists"); - return jio.getAttachment("doc", "other_attached"); + "Error if you try to go back to a nonexistent timestamp"); + return jio.getAttachment("doc", "other_attacheddata"); }) .push(function (result) { deepEqual(result, other_blob, "Other document successfully queried" ); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + + + test("Correctness of allAttachments method", + function () { + stop(); + expect(11); + var jio = this.jio, + not_history = this.not_history, + blob1 = this.blob1, + blob2 = this.blob2, + blob3 = this.blob3, + other_blob = this.other_blob; + + jio.put("doc", {title: "foo0"}) + .push(function () { + return jio.put("doc2", {key: "val"}); + }) + .push(function () { + return jio.putAttachment("doc", "attacheddata", blob1); + }) + .push(function () { + return jio.putAttachment("doc", "attacheddata", blob2); + }) + .push(function () { + return jio.putAttachment("doc", "other_attacheddata", other_blob); + }) + .push(function () { return jio.allAttachments("doc"); }) .push(function (results) { deepEqual(results, { - "attached": {}, - "other_attached": {} + "attacheddata": blob2, + "other_attacheddata": other_blob }, "allAttachments works as expected."); - return jio.removeAttachment("doc", "attached"); + return jio.removeAttachment("doc", "attacheddata"); }) .push(function () { return jio.get("doc"); @@ -293,8 +190,8 @@ .push(function (result) { deepEqual(result, { title: "foo0" - }, "Get does not return any attachment information (9)"); - return jio.getAttachment("doc", "attached"); + }, "Get does not return any attachment information"); + return jio.getAttachment("doc", "attacheddata"); }) .push(function () { ok(false, "This query should have thrown a 404 error"); @@ -303,14 +200,14 @@ ok(error instanceof jIO.util.jIOError, "Correct type of error"); deepEqual(error.status_code, 404, - "Removed attachments cannot be queried"); + "Removed attachments cannot be queried (4)"); return jio.allAttachments("doc"); }) .push(function (results) { deepEqual(results, { - "other_attached": {} + "other_attacheddata": blob2 }, "allAttachments works as expected with a removed attachment"); - return jio.putAttachment("doc", "attached", blob3); + return jio.putAttachment("doc", "attacheddata", blob3); }) .push(function () { return not_history.allDocs(); @@ -328,68 +225,18 @@ {timestamp: results[1].timestamp, doc_id: "doc2", doc: results[1].doc, op: "put"}, {timestamp: results[2].timestamp, - doc_id: "doc", name: "attached", op: "putAttachment"}, + doc_id: "doc", name: "attacheddata", op: "putAttachment"}, {timestamp: results[3].timestamp, - doc_id: "doc", name: "attached", op: "putAttachment"}, + doc_id: "doc", name: "attacheddata", op: "putAttachment"}, {timestamp: results[4].timestamp, - doc_id: "doc", name: "other_attached", op: "putAttachment"}, + doc_id: "doc", name: "other_attacheddata", op: "putAttachment"}, {timestamp: results[5].timestamp, - doc_id: "doc", name: "attached", op: "removeAttachment"}, + doc_id: "doc", name: "attacheddata", op: "removeAttachment"}, {timestamp: results[6].timestamp, - doc_id: "doc", name: "attached", op: "putAttachment"} + doc_id: "doc", name: "attacheddata", op: "putAttachment"} ], "Other storage can access all document revisions." ); }) - .push(function () { - return jio.getAttachment("doc", "attached"); - }) - .push(function (result) { - deepEqual(result, - blob3, - "Return the attachment information with getAttachment" - ); - return jio.getAttachment("doc", "attached_-0"); - }) - .push(function (result) { - deepEqual(result, - blob3, - "Return the attachment information with getAttachment" - ); - return jio.getAttachment("doc", "attached_-1"); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - deepEqual(error.status_code, - 404, - "Error if you try to go back to a removed attachment state"); - return jio.getAttachment("doc", "attached_-2"); - }) - .push(function (result) { - deepEqual(result, - blob2, - "Return the attachment information with getAttachment (17)" - ); - return jio.getAttachment("doc", "attached_-3"); - }) - .push(function (result) { - deepEqual(result, - blob1, - "Return the attachment information with getAttachment" - ); - return jio.getAttachment("doc", "attached_-4"); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - deepEqual(error.status_code, - 404, - "Error if you try to go back more revisions than what exists"); - }) .push(function () { return jio.allDocs(); }) @@ -412,11 +259,11 @@ })); }) .push(function (results) { - equal(results.length, 7, "Seven document revisions in storage (24)"); + equal(results.length, 7, "Seven document revisions in storage (17)"); return jio.remove("doc"); }) .push(function () { - return jio.getAttachment("doc", "attached"); + return jio.getAttachment("doc", "attacheddata"); }) .push(function () { ok(false, "This query should have thrown a 404 error"); @@ -436,196 +283,177 @@ ///////////////////////////////////////////////////////////////// - // Accessing older revisions + // Querying older revisions ///////////////////////////////////////////////////////////////// - module("HistoryStorage.getting_and_putting"); - test("Testing proper retrieval of older revisions of documents", - function () { - stop(); - expect(18); - + module("HistoryStorage.get", { + setup: function () { // create storage of type "history" with memory as substorage - var jio = jIO.createJIO({ + var dbname = "db_" + Date.now(); + this.jio = jIO.createJIO({ type: "history", sub_storage: { type: "query", sub_storage: { type: "uuid", sub_storage: { - type: "memory" + type: "indexeddb", + database: dbname } } } }); - jio.get("doc") - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "Document does not exist yet."); - }) - .push(function () { - return jio.put("doc", { - "k0": "v0" - }); - }) + this.not_history = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + }); + } + }); + + test("Handling bad input", + function () { + stop(); + expect(2); + var jio = this.jio, + BADINPUT_ERRCODE = 422; + + jio.put("doc", { + "_timestamp": 3, + "other_attr": "other_val" + }) .push(function () { - return jio.get("doc_-0"); - }) - .push(function (result) { - deepEqual(result, { - "k0": "v0" - }); + ok(false, "This statement should not be reached"); + }, function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + BADINPUT_ERRCODE, + "Can't save a document with a reserved keyword" + ); }) - .push(function () { - return jio.put("doc", {"k1": "v1"}); + .fail(function (error) { + //console.log(error); + ok(false, error); }) + .always(function () {start(); }); + }); + + test("Creating a document with put and retrieving it with get", + function () { + stop(); + expect(4); + var jio = this.jio, + not_history = this.not_history, + timestamps = jio.__storage._timestamps; + jio.put("doc", {title: "version0"}) .push(function () { - return jio.get("doc_-0"); + ok(timestamps.hasOwnProperty("doc"), + "jio._timestamps is updated with new document."); + equal(timestamps.doc.length, + 1, + "One revision is logged in jio._timestamps" + ); + return jio.get(timestamps.doc[0]); }) .push(function (result) { deepEqual(result, { - "k1": "v1" - }); - }) - .push(function () { - return jio.get("doc_-1"); + title: "version0" + }, "Get document from history storage"); + return not_history.get( + timestamps.doc[0] + ); }) .push(function (result) { deepEqual(result, { - "k0": "v0" - }); + timestamp: timestamps.doc[0], + op: "put", + doc_id: "doc", + doc: { + title: "version0" + } + }, "Get document from non-history storage"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); }) + .always(function () {start(); }); + }); + + test("Retrieving older revisions with get", + function () { + stop(); + expect(7); + var jio = this.jio, + timestamps = this.jio.__storage._timestamps; + + return jio.put("doc", {title: "t0", subtitle: "s0"}) .push(function () { - return jio.put("doc", {"k2": "v2"}); + return jio.put("doc", {title: "t1", subtitle: "s1"}); }) .push(function () { - return jio.remove("doc"); + return jio.put("doc", {title: "t2", subtitle: "s2"}); }) .push(function () { - return jio.put("doc", {"k3": "v3"}); + jio.remove("doc"); }) .push(function () { - return jio.put("doc", {"k4": "v4"}); + return jio.put("doc", {title: "t3", subtitle: "s3"}); }) .push(function () { return jio.get("doc"); }) .push(function (result) { - deepEqual(result, - {"k4": "v4"}, - "By default, .get returns latest revision"); - return jio.get("doc"); + deepEqual(result, { + title: "t3", + subtitle: "s3" + }, "Get returns latest revision"); + return jio.get(timestamps.doc[0]); }) .push(function (result) { - deepEqual(result, - {"k4": "v4"}, - ".get returns latest revision with second input = 0"); - return jio.get("doc_-1"); + deepEqual(result, { + title: "t0", + subtitle: "s0" + }, "Get returns first version"); + return jio.get(timestamps.doc[1]); }) .push(function (result) { - deepEqual(result, - {"k3": "v3"}, - "Walk back one revision with second input = 1"); - return jio.get("doc_-2"); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "Current state of document is 'removed'."); - return jio.get("doc_-3"); - }) - .push(function (result) { - deepEqual(result, - {"k2": "v2"}, - "Walk back three revisions with second input = 3"); - return jio.get("doc_-4"); + deepEqual(result, { + title: "t1", + subtitle: "s1" + }, "Get returns second version"); + return jio.get(timestamps.doc[2]); }) .push(function (result) { - deepEqual(result, - {"k1": "v1"}, - "Walk back four revisions with second input = 4"); - return jio.get("doc_-5"); - }) - .push(function (result) { - deepEqual(result, - {"k0": "v0"}, - "Walk back five revisions with second input = 5"); - return jio.get("doc_-6"); + deepEqual(result, { + title: "t2", + subtitle: "s2" + }, "Get returns third version"); + return jio.get(timestamps.doc[3]); }) .push(function () { - ok(false, "This query should have thrown a 404 error"); + ok(false, "This should have thrown a 404 error"); + return jio.get(timestamps.doc[4]); }, function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); deepEqual(error.status_code, 404, - "There are only 5 previous states of this document"); + "Error if you try to go back more revisions than what exists"); + return jio.get(timestamps.doc[4]); }) - - // Adding documents with problematic doc_id's - .push(function () { - return jio.put("doc_-name", { - "key": "val0" - }); - }) - .push(function () { - return jio.put("document_-0", { - "key": "and val0" - }); - }) - .push(function () { - return jio.put("doc_-name", { - "key": "val1" - }); - }) - - .push(function () { - return jio.get("doc_-name"); - }) - .push(function (result) { - deepEqual(result, { - "key": "val1" - }); - return jio.get("doc_-name_-0"); - }) - .push(function (result) { - deepEqual(result, { - "key": "val1" - }); - return jio.get("doc_-name_-1"); - }) .push(function (result) { deepEqual(result, { - "key": "val0" - }); - return jio.get("document_-0"); - }) - .push(function (result) { - deepEqual(result, { - "key": "and val0" - }); - return jio.get("document_-0_-0"); + title: "t3", + subtitle: "s3" + }, "Get returns latest version"); }) - .push(function (result) { - deepEqual(result, { - "key": "and val0" - }); - return jio.get("document_-0_-1"); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - deepEqual(error.status_code, - 404, - "Document does not have this many revisions."); - }) + .fail(function (error) { //console.log(error); ok(false, error); @@ -633,39 +461,12 @@ .always(function () {start(); }); }); - test("verifying updates correctly when puts are done in parallel", function () { stop(); - expect(7); - - // create storage of type "history" with memory as substorage - var dbname = "rsvp_db_" + Date.now(), - jio = jIO.createJIO({ - type: "history", - sub_storage: { - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - //type: "memory" - type: "indexeddb", - database: dbname - } - } - } - }), - not_history = jIO.createJIO({ - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - //type: "memory" - type: "indexeddb", - database: dbname - } - } - }); + expect(8); + var jio = this.jio, + not_history = this.not_history; jio.put("bar", {"title": "foo0"}) .push(function () { @@ -713,11 +514,8 @@ .push(function () { return jio.get("barbar"); }, function (error) { - deepEqual( - error.message, - "HistoryStorage: cannot find object 'bar' (removed)", - "Appropriate error is sent explaining object has been removed" - ); + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + equal(error.status_code, 404, "Correct error status code returned"); return jio.get("barbar"); }) .push(function (result) { @@ -747,268 +545,420 @@ .always(function () {start(); }); }); - ///////////////////////////////////////////////////////////////// // Querying older revisions ///////////////////////////////////////////////////////////////// - module("HistoryStorage.allDocs"); - test("Testing retrieval of older revisions via allDocs calls", - function () { - stop(); - expect(42); - + module("HistoryStorage.allDocs", { + setup: function () { // create storage of type "history" with memory as substorage - var jio = jIO.createJIO({ + var dbname = "db_" + Date.now(); + this.jio = jIO.createJIO({ type: "history", sub_storage: { type: "query", sub_storage: { type: "uuid", sub_storage: { - type: "memory" + type: "indexeddb", + database: dbname } } } }); - jio.put("doc", { - "k": "v0" - }) - .push(function () { - return jio.put("doc", { - "k": "v1" - }); - }) + this.not_history = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + }); + } + }); + test("Putting a document and retrieving it with allDocs", + function () { + stop(); + expect(7); + var jio = this.jio, + not_history = this.not_history; + jio.put("doc", {title: "version0"}) .push(function () { - return jio.put("doc", { - "k": "v2" - }); + return RSVP.all([ + jio.allDocs(), + jio.allDocs({query: "title: version0"}), + jio.allDocs({limit: [0, 1]}), + jio.allDocs({}) + ]); }) - .push(function () { - return jio.put("doc", { - "k": "v3" - }); + .push(function (results) { + var ind = 0; + for (ind = 0; ind < results.length - 1; ind += 1) { + deepEqual(results[ind], + results[ind + 1], + "Each query returns exactly the same correct output" + ); + } + return results[0]; }) - .push(function () { - return jio.allDocs({ - query: "_REVISION : 0" - }); + .push(function (results) { + equal(results.data.rows.length, + 1, + "Exactly one result returned"); + deepEqual(results.data.rows[0], { + doc: {}, + value: {}, + id: "doc" + }, + "Correct document format is returned." + ); + return not_history.allDocs(); }) .push(function (results) { - deepEqual(results.data.rows.length, + equal(results.data.rows.length, 1, - "Only one query returned with options.revision_limit == [0,1]"); - return jio.get(results.data.rows[0].id); + "Exactly one result returned"); + return not_history.get(results.data.rows[0].id); }) .push(function (result) { deepEqual(result, { - "k": "v3" - }, "One correct result."); + timestamp: jio.__storage._timestamps.doc[0], + doc_id: "doc", + doc: { + title: "version0" + }, + op: "put" + }, + "When a different type of storage queries historystorage, all " + + "metadata is returned correctly" + ); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); }) + .always(function () {start(); }); + }); + + + test("Putting a document, revising it, and retrieving revisions with allDocs", + function () { + stop(); + expect(13); + var context = this, + timestamps; + context.jio.put("doc", { + title: "version0", + subtitle: "subvers0" + }) .push(function () { - return jio.allDocs({ - query: "_REVISION : =1" + return context.jio.put("doc", { + title: "version1", + subtitle: "subvers1" }); }) - .push(function (results) { - deepEqual(results.data.rows.length, - 1, - "Only one query returned with options.revision_limit == [1,1]"); - deepEqual(results.data.rows[0].doc, { - "k": "v2" - }, "One correct result."); - return jio.allDocs({ - query: "_REVISION : =2" + .push(function () { + timestamps = context.jio.__storage._timestamps.doc; + return context.jio.put("doc", { + title: "version2", + subtitle: "subvers2" }); }) - .push(function (results) { - deepEqual(results.data.rows.length, - 1, - "Only one query returned with options.revision_limit == [2,1]"); - deepEqual(results.data.rows[0].doc, { - "k": "v1" - }); - return jio.allDocs({ - query: "_REVISION : =3" - }); + .push(function () { + + return RSVP.all([ + context.jio.allDocs({select_list: ["title", "subtitle"]}), + context.jio.allDocs({ + query: "", + select_list: ["title", "subtitle"] + }), + context.jio.allDocs({ + query: "title: version2", + select_list: ["title", "subtitle"] + }), + context.jio.allDocs({ + query: "NOT (title: version1)", + select_list: ["title", "subtitle"] + }), + context.jio.allDocs({ + query: "(NOT (subtitle: subvers1)) AND (NOT (title: version0))", + select_list: ["title", "subtitle"] + }), + context.jio.allDocs({ + limit: [0, 1], + sort_on: [["title", "ascending"]], + select_list: ["title", "subtitle"] + }) + ]); }) .push(function (results) { - deepEqual(results.data.rows.length, - 1, - "Only one query returned with options.revision_limit == [3,1]"); - deepEqual(results.data.rows[0].doc, { - "k": "v0" - }); - return jio.allDocs({ - query: "_REVISION : =4" - }); + var ind = 0; + for (ind = 0; ind < results.length - 1; ind += 1) { + deepEqual(results[ind], + results[ind + 1], + "Each query returns exactly the same correct output" + ); + } + return results[0]; }) .push(function (results) { - equal(results.data.rows.length, 0, "Past all previous revisions"); + equal(results.data.rows.length, + 1, + "Exactly one result returned"); + deepEqual(results.data.rows[0], { + value: { + title: "version2", + subtitle: "subvers2" + }, + doc: {}, + id: "doc" + }, + "Correct document format is returned." + ); }) .push(function () { - return jio.allDocs({ - query: "_REVISION: <= 1" - }); + return RSVP.all([ + context.jio.allDocs({ + query: "_timestamp: " + timestamps[1], + select_list: ["title", "subtitle"] + }), + context.jio.allDocs({ + query: "_timestamp: =" + timestamps[1], + select_list: ["title", "subtitle"] + }), + context.jio.allDocs({ + query: "_timestamp: >" + timestamps[0] + " AND title: version1", + select_list: ["title", "subtitle"] + }), + context.jio.allDocs({ + query: "_timestamp: > " + timestamps[0] + " AND _timestamp: < " + + timestamps[2], + select_list: ["title", "subtitle"] + }) + ]); }) .push(function (results) { - equal(results.data.rows.length, 2, - "Only retrieve two most recent revions"); - deepEqual(results.data.rows[0].doc, { - "k": "v3" - }, "First retrieved revision is correct"); - deepEqual(results.data.rows[1].doc, { - "k": "v2" - }, "Second retrieved revision is correct"); - }) - .push(function () { - return jio.remove("doc"); + equal(results[0].data.rows.length, + 1, + "Querying a specific timestamp retrieves one document"); + var ind = 0; + for (ind = 0; ind < results.length - 1; ind += 1) { + deepEqual(results[ind], + results[ind + 1], + "Each query returns exactly the same correct output" + ); + } + return results[0]; }) - .push(function () { - return jio.allDocs({ - query: "NOT (_REVISION: >= 1)", - revision_limit: [0, 1] + .push(function (results) { + deepEqual(results.data.rows[0], { + id: "doc", + value: { + title: "version1", + subtitle: "subvers1" + }, + doc: {} + }); + + return context.not_history.allDocs({ + sort_on: [["title", "ascending"]] }); }) .push(function (results) { - equal(results.data.rows.length, 0, - "Query does not return removed doc"); - }) - .push(function () { - return jio.allDocs({ - query: "(_REVISION: >= 1) AND (_REVISION: <= 3)" - }); + return RSVP.all(results.data.rows.map(function (d) { + return context.not_history.get(d.id); + })); }) .push(function (results) { - equal(results.data.rows.length, 3); - deepEqual(results.data.rows[0].doc, { - "k": "v3" - }, "1st, 2nd, and 3rd versions removed from current are retrieved"); - deepEqual(results.data.rows[1].doc, { - "k": "v2" - }, "1st, 2nd, and 3rd versions removed from current are retrieved"); - deepEqual(results.data.rows[2].doc, { - "k": "v1" - }, "1st, 2nd, and 3rd versions removed from current are retrieved"); + deepEqual(results, [ + { + timestamp: timestamps[0], + op: "put", + doc_id: "doc", + doc: { + title: "version0", + subtitle: "subvers0" + } + }, + { + timestamp: timestamps[1], + op: "put", + doc_id: "doc", + doc: { + title: "version1", + subtitle: "subvers1" + } + }, + { + timestamp: timestamps[2], + op: "put", + doc_id: "doc", + doc: { + title: "version2", + subtitle: "subvers2" + } + } + ], + "A different storage type can retrieve all versions as expected."); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); }) + .always(function () {start(); }); + }); + + + test( + "Putting and removing documents, latest revisions and no removed documents", + function () { + stop(); + expect(5); + var history = this.jio, + timestamps = this.jio.__storage._timestamps; + + history.put("doc_a", { + title_a: "rev0", + subtitle_a: "subrev0" + }) .push(function () { - return jio.put("doc2", { - "k2": "w0" + return history.put("doc_a", { + title_a: "rev1", + subtitle_a: "subrev1" }); }) .push(function () { - return jio.allDocs({ - query: "(_REVISION: >0) AND (_REVISION: <= 3)" + return history.put("doc_b", { + title_b: "rev0", + subtitle_b: "subrev0" }); }) - .push(function (results) { - equal(results.data.rows.length, 3); - deepEqual(results.data.rows[0].doc, { - "k": "v3" - }, "Does not retrieve new document outside queried revision range"); - deepEqual(results.data.rows[1].doc, { - "k": "v2" - }, "Does not retrieve new document outside queried revision range"); - deepEqual(results.data.rows[2].doc, { - "k": "v1" - }, "Does not retrieve new document outside queried revision range"); + .push(function () { + return history.remove("doc_b"); }) .push(function () { - return jio.allDocs({ - query: "(_REVISION: = 0) OR (_REVISION: = 1)" + return history.put("doc_c", { + title_c: "rev0", + subtitle_c: "subrev0" }); }) - .push(function (results) { - equal(results.data.rows.length, 2); - deepEqual(results.data.rows[0].doc, { - "k2": "w0" - }, "Retrieves all documents with versions in queried revision range"); - deepEqual(results.data.rows[1].doc, { - "k": "v3" - }, "Retrieves all documents with versions in queried revision range"); - }) .push(function () { - return jio.put("doc2", { - "k2": "w1" + return history.put("doc_c", { + title_c: "rev1", + subtitle_c: "subrev1" }); }) .push(function () { - return jio.allDocs(); + return history.allDocs({sort_on: [["timestamp", "descending"]]}); }) .push(function (results) { - equal(results.data.rows.length, 1, - "There is only one non-removed doc."); - deepEqual(results.data.rows[0].doc, { - "k2": "w1" - }, "Returned the one correct document."); + equal(results.data.rows.length, + 2, + "Only two non-removed unique documents exist." + ); + deepEqual(results.data.rows, [ + { + id: "doc_c", + value: {}, + doc: {} + }, + { + id: "doc_a", + value: {}, + doc: {} + } + ], + "Empty query returns latest revisions (and no removed documents)"); + equal(timestamps.doc_a.length, + 2, + "Correct number of revisions logged in doc_a"); + equal(timestamps.doc_b.length, + 2, + "Correct number of revisions logged in doc_b"); + equal(timestamps.doc_c.length, + 2, + "Correct number of revisions logged in doc_c"); }) - .push(function () { - return jio.remove("doc2"); + .fail(function (error) { + //console.log(error); + ok(false, error); }) + .always(function () {start(); }); + } + ); + + ///////////////////////////////////////////////////////////////// + // Complex Queries + ///////////////////////////////////////////////////////////////// + + test("More complex query with different options (without revision queries)", + function () { + stop(); + expect(2); + var history = this.jio, + docs = [ + { + "date": 1, + "type": "foo", + "title": "doc" + }, + { + "date": 2, + "type": "bar", + "title": "second_doc" + }, + { + "date": 2, + "type": "barbar", + "title": "third_doc" + } + ], + blobs = [ + new Blob(['a']), + new Blob(['bcd']), + new Blob(['eeee']) + ]; + history.put("doc", {}) .push(function () { - return jio.allDocs({ - query: - "_REVISION: 0 OR _REVISION: 1 OR " + - "(_REVISION: >= 2 AND _REVISION: <= 3)" - }); + return putFullDoc(history, "doc", docs[0], "data", blobs[0]); }) - .push(function (results) { - equal(results.data.rows.length, 5); - deepEqual(results.data.rows[0].doc, { - "k2": "w1" - }); - deepEqual(results.data.rows[1].doc, { - "k2": "w0" - }); - deepEqual(results.data.rows[2].doc, { - "k": "v3" - }); - deepEqual(results.data.rows[3].doc, { - "k": "v2" - }); - deepEqual(results.data.rows[4].doc, { - "k": "v1" - }); + .push(function () { + return putFullDoc(history, "second_doc", docs[1], "data", blobs[1]); }) .push(function () { - return jio.allDocs({ - query: "_REVISION: <= 3", - limit: [1, 4] - }); + return putFullDoc(history, "third_doc", docs[2], "data", blobs[2]); }) - .push(function (results) { - equal(results.data.rows.length, 4, - "Correct number of results with options.limit set"); - deepEqual(results.data.rows[0].doc, { - "k2": "w0" - }, "Correct results with options.limit set"); - deepEqual(results.data.rows[1].doc, { - "k": "v3" - }, "Correct results with options.limit set"); - deepEqual(results.data.rows[2].doc, { - "k": "v2" - }, "Correct results with options.limit set"); - deepEqual(results.data.rows[3].doc, { - "k": "v1" - }, "Correct results with options.limit set"); - - return jio.allDocs({ - query: "_REVISION: = 1", - select_list: ["k"] + .push(function () { + return history.allDocs({ + query: "(date: <= 2)", + select_list: ["date", "non-existent-key"], + sort_on: [["date", "ascending"], + ["non-existent-key", "ascending"] + ] }); }) .push(function (results) { - equal(results.data.rows.length, 2); - deepEqual(results.data.rows[0].doc, { - "k2": "w1" - }); - deepEqual(results.data.rows[0].value, {}); - deepEqual(results.data.rows[1].doc, { - "k": "v3" - }); - deepEqual(results.data.rows[1].value, { - "k": "v3" - }); + equal(results.data.rows.length, 3); + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc", + value: {date: 1} + }, + { + doc: {}, + id: "third_doc", + value: {date: 2} + }, + { + doc: {}, + id: "second_doc", + value: {date: 2} + } + ], + "Query gives correct results in correct order"); }) .fail(function (error) { //console.log(error); @@ -1018,67 +968,244 @@ }); ///////////////////////////////////////////////////////////////// - // Complex Queries + // Complex Queries with Revision Querying ///////////////////////////////////////////////////////////////// - //module("HistoryStorage.complex_queries"); - test("More complex queries with select_list option", + test("More complex query with different options (with revision queries)", function () { stop(); expect(3); - - // create storage of type "history" with memory as substorage - var jio = jIO.createJIO({ - type: "history", - sub_storage: { - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - type: "memory" - } + var jio = this.jio, + not_history = this.not_history, + timestamps = this.jio.__storage._timestamps, + docs = [ + { + "date": 1, + "type": "foo", + "title": "doc" + }, + { + "date": 2, + "type": "bar", + "title": "second_doc" } - } - }), - doc = { - "modification_date": "a", - "portal_type": "Foo", - "title": "foo_module/1" - }, - blob = new Blob(['a']); - putFullDoc(jio, "foo_module/1", doc, "data", blob) + ], + blobs = [ + new Blob(['a']), + new Blob(['bcd']), + new Blob(['a2']), + new Blob(['bcd2']), + new Blob(['a3']) + ]; + jio.put("doc", {}) .push(function () { - return jio.get("foo_module/1"); + return putFullDoc(jio, "doc", docs[0], "data", blobs[0]); }) - .push(function (result) { - deepEqual(result, { - "modification_date": "a", - "portal_type": "Foo", - "title": "foo_module/1" - }, "Can retrieve a document after attachment placed." - ); + .push(function () { + return putFullDoc(jio, "second_doc", docs[1], "data", blobs[1]); + }) + .push(function () { + docs[0].date = 4; + docs[0].type = "foo2"; + docs[1].date = 4; + docs[1].type = "bar2"; + }) + .push(function () { + return putFullDoc(jio, "doc", docs[0], "data", blobs[2]); + }) + .push(function () { + return jio.remove("second_doc"); + }) + .push(function () { + return putFullDoc(jio, "second_doc", docs[1], "data", blobs[3]); + }) + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["op", "doc_id", "timestamp"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: timestamps.second_doc.data[1], + value: { + "op": "putAttachment", + "doc_id": "second_doc", + "timestamp": timestamps.second_doc.data[1] + } + }, + { + doc: {}, + id: timestamps.second_doc[2], + value: { + "op": "put", + "doc_id": "second_doc", + "timestamp": timestamps.second_doc[2] + } + }, + { + doc: {}, + id: timestamps.second_doc[1], + value: { + "op": "remove", + "doc_id": "second_doc", + "timestamp": timestamps.second_doc[1] + } + }, + { + doc: {}, + id: timestamps.doc.data[1], + value: { + "op": "putAttachment", + "doc_id": "doc", + "timestamp": timestamps.doc.data[1] + } + }, + { + doc: {}, + id: timestamps.doc[2], + value: { + "op": "put", + "doc_id": "doc", + "timestamp": timestamps.doc[2] + } + }, + { + doc: {}, + id: timestamps.second_doc.data[0], + value: { + "op": "putAttachment", + "doc_id": "second_doc", + "timestamp": timestamps.second_doc.data[0] + } + }, + { + doc: {}, + id: timestamps.second_doc[0], + value: { + "op": "put", + "doc_id": "second_doc", + "timestamp": timestamps.second_doc[0] + } + }, + { + doc: {}, + id: timestamps.doc.data[0], + value: { + "op": "putAttachment", + "doc_id": "doc", + "timestamp": timestamps.doc.data[0] + } + }, + { + doc: {}, + id: timestamps.doc[1], + value: { + "op": "put", + "doc_id": "doc", + "timestamp": timestamps.doc[1] + } + }, + { + doc: {}, + id: timestamps.doc[0], + value: { + "op": "put", + "doc_id": "doc", + "timestamp": timestamps.doc[0] + } + } + ], "All operations are logged correctly"); + var promises = results.data.rows + .filter(function (doc) { + return (doc.value.op === "put"); + }) + .map(function (data) { + return not_history.get(data.id); + }); + return RSVP.all(promises) + .then(function (results) { + return results.map(function (docum) { + return docum.doc; + }); + }); + }) + .push(function (results) { + deepEqual(results, + [ + { + "date": 4, + "type": "bar2", + "title": "second_doc" + }, + { + "date": 4, + "type": "foo2", + "title": "doc" + }, + { + "date": 2, + "type": "bar", + "title": "second_doc" + }, + { + "date": 1, + "type": "foo", + "title": "doc" + }, + {} + ], "All versions of documents are stored correctly"); }) .push(function () { return jio.allDocs({ - query: "portal_type: Foo", - select_list: ["modification_date", "__id", "__id"], - sort_on: [["modification_date", "descending"], - ["timestamp", "descending"], - ["timestamp", "descending"] + query: "(_timestamp: >= " + timestamps.second_doc[0] + + " OR _timestamp: <= " + timestamps.doc[1] + + ") AND NOT (date: = 2)", + select_list: ["date", "non-existent-key", "type", "title"], + sort_on: [["date", "descending"], + ["non-existent-key", "ascending"], + ["_timestamp", "ascending"] ] }); }) .push(function (results) { - equal(results.data.rows.length, 1); - deepEqual(results.data.rows[0], { - doc: { - "modification_date": "a", - "portal_type": "Foo", - "title": "foo_module/1" + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc", + value: { + date: 4, + title: "doc", + type: "foo2" + } }, - id: "foo_module/1", - value: {modification_date: "a"} - }); + { + doc: {}, + id: "second_doc", + value: { + date: 4, + title: "second_doc", + type: "bar2" + } + }, + { + doc: {}, + id: "doc", + value: { + date: 1, + title: "doc", + type: "foo" + } + }, + { + doc: {}, + id: "doc", + value: {} + } + ], + "Query gives correct results in correct order"); }) .fail(function (error) { //console.log(error); @@ -1086,5 +1213,4 @@ }) .always(function () {start(); }); }); - }(jIO, RSVP, Blob, QUnit)); \ No newline at end of file -- 2.30.9 From aa6ed5aa9e9d9eee974ab826aef2131d294a2569 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Fri, 22 Jun 2018 16:38:31 +0000 Subject: [PATCH 30/46] Added one additional attachment test. --- test/jio.storage/historystorage.tests.js | 53 ++++++++++++++++++++++++ test/jio.storage/tmp.txt | 0 2 files changed, 53 insertions(+) create mode 100644 test/jio.storage/tmp.txt diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index 930b623..8ac2fc0 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -281,6 +281,59 @@ .always(function () {start(); }); }); + test("Correctness of allAttachments method", + function () { + stop(); + expect(4); + var jio = this.jio, + timestamps = jio.__storage._timestamps, + blob1 = this.blob1, + blob2 = this.blob2, + other_blob1 = this.other_blob, + other_blob2 = new Blob(['asdf']); + + putFullDoc(jio, "doc", {}, "data", blob1) + .push(function () { + return jio.putAttachment("doc", "data", blob2); + }) + .push(function () { + return jio.putAttachment("doc", "other_data", other_blob1); + }) + .push(function () { + return jio.putAttachment("doc", "other_data", other_blob2); + }) + .push(function () { + return jio.getAttachment(timestamps.doc.data[0], "data"); + }) + .push(function (result) { + deepEqual(result, blob1, "Get old version of first attachment"); + return jio.getAttachment(timestamps.doc.data[1], "data"); + }) + .push(function (result) { + deepEqual(result, blob2, "Get current version of first attachment"); + return jio.getAttachment(timestamps.doc.other_data[0], "other_data"); + }) + .push(function (result) { + deepEqual( + result, + other_blob1, + "Get old version of second attachment" + ); + return jio.getAttachment(timestamps.doc.other_data[1], "other_data"); + }) + .push(function (result) { + deepEqual( + result, + other_blob2, + "Get current version of second attachment" + ); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); ///////////////////////////////////////////////////////////////// // Querying older revisions diff --git a/test/jio.storage/tmp.txt b/test/jio.storage/tmp.txt new file mode 100644 index 0000000..e69de29 -- 2.30.9 From 79d7f77b3cc0def55cd6b1ef8d05360b8483cb30 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Mon, 25 Jun 2018 09:31:52 +0000 Subject: [PATCH 31/46] One additional invalid document name to check for and a temporary fix for remove operations sometimes being incorrectly ordered. --- src/jio.storage/historystorage.js | 42 +++++++------------ test/jio.storage/historystorage.tests.js | 52 ++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 32 deletions(-) diff --git a/src/jio.storage/historystorage.js b/src/jio.storage/historystorage.js index 59c9506..294c5fb 100644 --- a/src/jio.storage/historystorage.js +++ b/src/jio.storage/historystorage.js @@ -4,13 +4,14 @@ "use strict"; // Used to distinguish between operations done within the same millisecond - var unique_timestamp = function () { + var unique_timestamp = function (time) { // XXX: replace this with UUIDStorage function call to S4() when it becomes // publicly accessible var uuid = ('0000' + Math.floor(Math.random() * 0x10000) .toString(16)).slice(-4), - timestamp = Date.now().toString(); + //timestamp = Date.now().toString(); + timestamp = time.toString(); return timestamp + "-" + uuid; }; @@ -99,15 +100,19 @@ }; HistoryStorage.prototype.put = function (id, data) { - if (data.hasOwnProperty("_timestamp")) { throw new jIO.util.jIOError( "Document cannot have metadata attribute '_timestamp'", 422 ); } - - var timestamp = unique_timestamp(), + if (data.hasOwnProperty("_doc_id")) { + throw new jIO.util.jIOError( + "Document cannot have metadata attribute '_doc_id'", + 422 + ); + } + var timestamp = unique_timestamp(Date.now()), metadata = { // XXX: remove this attribute once query can sort_on id timestamp: timestamp, @@ -124,7 +129,7 @@ }; HistoryStorage.prototype.remove = function (id) { - var timestamp = unique_timestamp(), + var timestamp = unique_timestamp(Date.now() - 1), metadata = { // XXX: remove this attribute once query can sort_on id timestamp: timestamp, @@ -186,7 +191,7 @@ }; HistoryStorage.prototype.putAttachment = function (id, name, blob) { - var timestamp = unique_timestamp(), + var timestamp = unique_timestamp(Date.now()), metadata = { // XXX: remove this attribute once query can sort_on id timestamp: timestamp, @@ -268,26 +273,7 @@ throw error; }); } - return substorage.get(id) - .push(function (result) { - if (result.op === "putAttachment") { - return substorage.getAttachment(id, result.name); - } - throw new jIO.util.jIOError( - "HistoryStorage: cannot find object '" + id + "' (removed)", - 404 - ); - }, - function (error) { - if (error.status_code === 404 && - error instanceof jIO.util.jIOError) { - throw new jIO.util.jIOError( - "HistoryStorage: cannot find object '" + id + "'", - 404 - ); - } - throw error; - }) + return substorage.getAttachment(id, name) .push(undefined, function (error) { if (error.status_code === 404 && error instanceof jIO.util.jIOError) { @@ -302,7 +288,7 @@ }; HistoryStorage.prototype.removeAttachment = function (id, name) { - var timestamp = unique_timestamp(), + var timestamp = unique_timestamp(Date.now()), metadata = { // XXX: remove this attribute once query can sort_on id timestamp: timestamp, diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index 8ac2fc0..6c85a0e 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -63,12 +63,13 @@ test("Testing proper adding/removing attachments", function () { stop(); - expect(7); + expect(9); var jio = this.jio, timestamps = this.jio.__storage._timestamps, blob2 = this.blob2, blob1 = this.blob1, - other_blob = this.other_blob; + other_blob = this.other_blob, + otherother_blob = new Blob(['abcabc']); jio.put("doc", {title: "foo0"}) .push(function () { @@ -83,6 +84,16 @@ .push(function () { return jio.putAttachment("doc", "other_attacheddata", other_blob); }) + .push(function () { + return jio.putAttachment( + "doc", + "otherother_attacheddata", + otherother_blob + ); + }) + .push(function () { + return jio.removeAttachment("doc", "otherother_attacheddata"); + }) .push(function () { return jio.get("doc"); }) @@ -142,6 +153,18 @@ "Other document successfully queried" ); }) + .push(function () { + return jio.getAttachment("doc", "otherother_attacheddata"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Error if you try to get a removed attachment"); + }) .fail(function (error) { //console.log(error); ok(false, error); @@ -372,7 +395,7 @@ test("Handling bad input", function () { stop(); - expect(2); + expect(4); var jio = this.jio, BADINPUT_ERRCODE = 422; @@ -380,6 +403,21 @@ "_timestamp": 3, "other_attr": "other_val" }) + .push(function () { + ok(false, "This statement should not be reached"); + }, function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + BADINPUT_ERRCODE, + "Can't save a document with a reserved keyword" + ); + }) + .push(function () { + return jio.put("doc", { + "_doc_id": 3, + "other_attr": "other_val" + }); + }) .push(function () { ok(false, "This statement should not be reached"); }, function (error) { @@ -390,7 +428,7 @@ ); }) .fail(function (error) { - //console.log(error); + //console.log(error); ok(false, error); }) .always(function () {start(); }); @@ -467,6 +505,8 @@ subtitle: "s3" }, "Get returns latest revision"); return jio.get(timestamps.doc[0]); + }, function (err) { + ok(false, err); }) .push(function (result) { deepEqual(result, { @@ -481,6 +521,8 @@ subtitle: "s1" }, "Get returns second version"); return jio.get(timestamps.doc[2]); + }, function (err) { + ok(false, err); }) .push(function (result) { deepEqual(result, { @@ -488,6 +530,8 @@ subtitle: "s2" }, "Get returns third version"); return jio.get(timestamps.doc[3]); + }, function (err) { + ok(false, err); }) .push(function () { ok(false, "This should have thrown a 404 error"); -- 2.30.9 From 6f1464b155fd68c46ebea8f0dd5739102c6ea8cd Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Mon, 25 Jun 2018 12:39:35 +0000 Subject: [PATCH 32/46] Added a check to put() to make sure id is not in same format as a timestamp. --- src/jio.storage/historystorage.js | 18 +++++-- test/jio.storage/historystorage.tests.js | 69 +++++------------------- 2 files changed, 29 insertions(+), 58 deletions(-) diff --git a/src/jio.storage/historystorage.js b/src/jio.storage/historystorage.js index 294c5fb..9251ff2 100644 --- a/src/jio.storage/historystorage.js +++ b/src/jio.storage/historystorage.js @@ -13,7 +13,14 @@ //timestamp = Date.now().toString(); timestamp = time.toString(); return timestamp + "-" + uuid; - }; + }, + looks_like_timestamp = function (id) { + //1529928772623-02e6 + //A timestamp is of the form + //"[13 digit number]-[4 numbers/lowercase letters]" + var re = /^[0-9]{13}-[a-z0-9]{4}$/; + return re.test(id); + }; /** @@ -90,8 +97,7 @@ if (error.status_code === 400 && error instanceof jIO.util.jIOError) { throw new jIO.util.jIOError( - "HistoryStorage: cannot find object '" + id_in + - "'", + "HistoryStorage: cannot find object '" + id_in + "'", 404 ); } @@ -112,6 +118,12 @@ 422 ); } + if (looks_like_timestamp(id)) { + throw new jIO.util.jIOError( + "Document cannot have id of the same form as a timestamp", + 422 + ); + } var timestamp = unique_timestamp(Date.now()), metadata = { // XXX: remove this attribute once query can sort_on id diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index 6c85a0e..0792e25 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -60,6 +60,7 @@ }); } }); + test("Testing proper adding/removing attachments", function () { stop(); @@ -172,7 +173,6 @@ .always(function () {start(); }); }); - test("Correctness of allAttachments method", function () { stop(); @@ -304,59 +304,6 @@ .always(function () {start(); }); }); - test("Correctness of allAttachments method", - function () { - stop(); - expect(4); - var jio = this.jio, - timestamps = jio.__storage._timestamps, - blob1 = this.blob1, - blob2 = this.blob2, - other_blob1 = this.other_blob, - other_blob2 = new Blob(['asdf']); - - putFullDoc(jio, "doc", {}, "data", blob1) - .push(function () { - return jio.putAttachment("doc", "data", blob2); - }) - .push(function () { - return jio.putAttachment("doc", "other_data", other_blob1); - }) - .push(function () { - return jio.putAttachment("doc", "other_data", other_blob2); - }) - .push(function () { - return jio.getAttachment(timestamps.doc.data[0], "data"); - }) - .push(function (result) { - deepEqual(result, blob1, "Get old version of first attachment"); - return jio.getAttachment(timestamps.doc.data[1], "data"); - }) - .push(function (result) { - deepEqual(result, blob2, "Get current version of first attachment"); - return jio.getAttachment(timestamps.doc.other_data[0], "other_data"); - }) - .push(function (result) { - deepEqual( - result, - other_blob1, - "Get old version of second attachment" - ); - return jio.getAttachment(timestamps.doc.other_data[1], "other_data"); - }) - .push(function (result) { - deepEqual( - result, - other_blob2, - "Get current version of second attachment" - ); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); ///////////////////////////////////////////////////////////////// // Querying older revisions @@ -395,7 +342,7 @@ test("Handling bad input", function () { stop(); - expect(4); + expect(6); var jio = this.jio, BADINPUT_ERRCODE = 422; @@ -427,6 +374,18 @@ "Can't save a document with a reserved keyword" ); }) + .push(function () { + return jio.put("1234567891123-ab7d", {}); + }) + .push(function () { + ok(false, "This statement should not be reached"); + }, function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + BADINPUT_ERRCODE, + "Can't save a document with a timestamp-formatted id" + ); + }) .fail(function (error) { //console.log(error); ok(false, error); -- 2.30.9 From 87f4360ef3af8f49e462eb6e6a0fc67987f0204c Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Mon, 25 Jun 2018 13:06:44 +0000 Subject: [PATCH 33/46] Added additional allDocs test. --- test/jio.storage/historystorage.tests.js | 71 +++++++++++++++--------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index 0792e25..576f1b2 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -702,22 +702,22 @@ test("Putting a document, revising it, and retrieving revisions with allDocs", function () { stop(); - expect(13); - var context = this, - timestamps; - context.jio.put("doc", { + expect(14); + var jio = this.jio, + not_history = this.not_history, + timestamps = this.jio.__storage._timestamps; + jio.put("doc", { title: "version0", subtitle: "subvers0" }) .push(function () { - return context.jio.put("doc", { + return jio.put("doc", { title: "version1", subtitle: "subvers1" }); }) .push(function () { - timestamps = context.jio.__storage._timestamps.doc; - return context.jio.put("doc", { + return jio.put("doc", { title: "version2", subtitle: "subvers2" }); @@ -725,24 +725,24 @@ .push(function () { return RSVP.all([ - context.jio.allDocs({select_list: ["title", "subtitle"]}), - context.jio.allDocs({ + jio.allDocs({select_list: ["title", "subtitle"]}), + jio.allDocs({ query: "", select_list: ["title", "subtitle"] }), - context.jio.allDocs({ + jio.allDocs({ query: "title: version2", select_list: ["title", "subtitle"] }), - context.jio.allDocs({ + jio.allDocs({ query: "NOT (title: version1)", select_list: ["title", "subtitle"] }), - context.jio.allDocs({ + jio.allDocs({ query: "(NOT (subtitle: subvers1)) AND (NOT (title: version0))", select_list: ["title", "subtitle"] }), - context.jio.allDocs({ + jio.allDocs({ limit: [0, 1], sort_on: [["title", "ascending"]], select_list: ["title", "subtitle"] @@ -776,21 +776,22 @@ }) .push(function () { return RSVP.all([ - context.jio.allDocs({ - query: "_timestamp: " + timestamps[1], + jio.allDocs({ + query: "_timestamp: " + timestamps.doc[1], select_list: ["title", "subtitle"] }), - context.jio.allDocs({ - query: "_timestamp: =" + timestamps[1], + jio.allDocs({ + query: "_timestamp: =" + timestamps.doc[1], select_list: ["title", "subtitle"] }), - context.jio.allDocs({ - query: "_timestamp: >" + timestamps[0] + " AND title: version1", + jio.allDocs({ + query: "_timestamp: >" + timestamps.doc[0] + + " AND title: version1", select_list: ["title", "subtitle"] }), - context.jio.allDocs({ - query: "_timestamp: > " + timestamps[0] + " AND _timestamp: < " + - timestamps[2], + jio.allDocs({ + query: "_timestamp: > " + timestamps.doc[0] + + " AND _timestamp: < " + timestamps.doc[2], select_list: ["title", "subtitle"] }) ]); @@ -817,20 +818,36 @@ }, doc: {} }); + return jio.allDocs({ + query: "_timestamp: " + timestamps.doc[0], + select_list: ["title", "subtitle"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + value: { + title: "version0", + subtitle: "subvers0" + }, + doc: {}, + id: "doc" + } + ], "Query requesting one timestamp works."); - return context.not_history.allDocs({ + return not_history.allDocs({ sort_on: [["title", "ascending"]] }); }) .push(function (results) { return RSVP.all(results.data.rows.map(function (d) { - return context.not_history.get(d.id); + return not_history.get(d.id); })); }) .push(function (results) { deepEqual(results, [ { - timestamp: timestamps[0], + timestamp: timestamps.doc[0], op: "put", doc_id: "doc", doc: { @@ -839,7 +856,7 @@ } }, { - timestamp: timestamps[1], + timestamp: timestamps.doc[1], op: "put", doc_id: "doc", doc: { @@ -848,7 +865,7 @@ } }, { - timestamp: timestamps[2], + timestamp: timestamps.doc[2], op: "put", doc_id: "doc", doc: { -- 2.30.9 From 4306f29f87889c44a0808b1cb9b287649e240376 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Mon, 25 Jun 2018 14:23:26 +0000 Subject: [PATCH 34/46] Added a test to verify removeAttachment does not have the same timestamp issue as remove had. --- test/jio.storage/historystorage.tests.js | 38 +++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index 576f1b2..6a63bbd 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -173,6 +173,41 @@ .always(function () {start(); }); }); + + test("Ordering of put and remove attachments is correct", + function () { + stop(); + expect(1); + var jio = this.jio, + blob1 = this.blob1, + blob2 = this.blob2; + + jio.put("doc", {title: "foo0"}) + .push(function () { + return jio.putAttachment("doc", "data", blob1); + }) + .push(function () { + return jio.removeAttachment("doc", "data"); + }) + .push(function () { + return jio.putAttachment("doc", "data", blob2); + }) + .push(function () { + return jio.getAttachment("doc", "data"); + }) + .push(function (result) { + deepEqual(result, + blob2, + "removeAttachment happens before putAttachment" + ); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + test("Correctness of allAttachments method", function () { stop(); @@ -723,7 +758,6 @@ }); }) .push(function () { - return RSVP.all([ jio.allDocs({select_list: ["title", "subtitle"]}), jio.allDocs({ @@ -775,6 +809,8 @@ ); }) .push(function () { + // These are all equivalent queries in that they should return the + // same documents in the correct order return RSVP.all([ jio.allDocs({ query: "_timestamp: " + timestamps.doc[1], -- 2.30.9 From c6db8c507506f4a16c0b0ace286637801eb273b6 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Wed, 27 Jun 2018 09:18:46 +0000 Subject: [PATCH 35/46] Made changes to simplify buildQuery revision queries allowing any property names possible. --- src/jio.storage/historystorage.js | 347 +++++++------ test/jio.storage/historystorage.tests.js | 631 ++++++++++++++++------- 2 files changed, 632 insertions(+), 346 deletions(-) diff --git a/src/jio.storage/historystorage.js b/src/jio.storage/historystorage.js index 9251ff2..817d22a 100644 --- a/src/jio.storage/historystorage.js +++ b/src/jio.storage/historystorage.js @@ -4,7 +4,7 @@ "use strict"; // Used to distinguish between operations done within the same millisecond - var unique_timestamp = function (time) { + function generateUniqueTimestamp(time) { // XXX: replace this with UUIDStorage function call to S4() when it becomes // publicly accessible @@ -13,15 +13,28 @@ //timestamp = Date.now().toString(); timestamp = time.toString(); return timestamp + "-" + uuid; - }, - looks_like_timestamp = function (id) { - //1529928772623-02e6 - //A timestamp is of the form - //"[13 digit number]-[4 numbers/lowercase letters]" - var re = /^[0-9]{13}-[a-z0-9]{4}$/; - return re.test(id); - }; + } + + function isTimestamp(id) { + //A timestamp is of the form + //"[13 digit number]-[4 numbers/lowercase letters]" + var re = /^[0-9]{13}-[a-z0-9]{4}$/; + return re.test(id); + } + + function throwCantFindError(id) { + throw new jIO.util.jIOError( + "HistoryStorage: cannot find object '" + id + "'", + 404 + ); + } + function throwRemovedError(id) { + throw new jIO.util.jIOError( + "HistoryStorage: cannot find object '" + id + "' (removed)", + 404 + ); + } /** * The jIO HistoryStorage extension @@ -31,11 +44,28 @@ */ function HistoryStorage(spec) { this._sub_storage = jIO.createJIO(spec.sub_storage); - this._timestamps = {}; } HistoryStorage.prototype.get = function (id_in) { + if (isTimestamp(id_in)) { + + // Try to treat id_in as a timestamp instead of a name + return this._sub_storage.get(id_in) + .push(function (result) { + if (result.op === "put") { + return result.doc; + } + throwCantFindError(id_in); + }, function (error) { + if (error.status_code === 404 && + error instanceof jIO.util.jIOError) { + throwRemovedError(id_in); + } + throw error; + }); + } + // Query to get the last edit made to this document var substorage = this._sub_storage, @@ -66,65 +96,23 @@ return substorage.get(results.data.rows[0].id) .push(function (result) { return result.doc; - }, function (error) { - if (error.status_code === 400 && - error instanceof jIO.util.jIOError) { - throw new jIO.util.jIOError( - "HistoryStorage: cannot find object '" + id_in + - "'", - 404 - ); - } }); } - throw new jIO.util.jIOError( - "HistoryStorage: cannot find object '" + id_in + "' (removed)", - 404 - ); + throwRemovedError(id_in); } - // Try again by treating id_in as a timestamp instead of a name - return substorage.get(id_in) - .push(function (result) { - if (result.op === "put") { - return result.doc; - } - throw new jIO.util.jIOError( - "HistoryStorage: cannot find object '" + id_in + - "' (removed)", - 404 - ); - }, function (error) { - if (error.status_code === 400 && - error instanceof jIO.util.jIOError) { - throw new jIO.util.jIOError( - "HistoryStorage: cannot find object '" + id_in + "'", - 404 - ); - } - }); + throwCantFindError(id_in); }); }; HistoryStorage.prototype.put = function (id, data) { - if (data.hasOwnProperty("_timestamp")) { - throw new jIO.util.jIOError( - "Document cannot have metadata attribute '_timestamp'", - 422 - ); - } - if (data.hasOwnProperty("_doc_id")) { - throw new jIO.util.jIOError( - "Document cannot have metadata attribute '_doc_id'", - 422 - ); - } - if (looks_like_timestamp(id)) { + + if (isTimestamp(id)) { throw new jIO.util.jIOError( "Document cannot have id of the same form as a timestamp", 422 ); } - var timestamp = unique_timestamp(Date.now()), + var timestamp = generateUniqueTimestamp(Date.now()), metadata = { // XXX: remove this attribute once query can sort_on id timestamp: timestamp, @@ -132,60 +120,102 @@ doc: data, op: "put" }; - if (this._timestamps.hasOwnProperty(id)) { - this._timestamps[id].push(timestamp); - } else { - this._timestamps[id] = [timestamp]; - } return this._sub_storage.put(timestamp, metadata); }; HistoryStorage.prototype.remove = function (id) { - var timestamp = unique_timestamp(Date.now() - 1), + var timestamp = generateUniqueTimestamp(Date.now() - 1), metadata = { // XXX: remove this attribute once query can sort_on id timestamp: timestamp, doc_id: id, op: "remove" }; - this._timestamps[id].push(timestamp); return this._sub_storage.put(timestamp, metadata); }; HistoryStorage.prototype.allAttachments = function (id) { - // XXX: If instead you passed a timestamp in as `id`, we could retrieve all - // the attachments of the document at that point in time. Not sure if this - // would be useful. + // XXX: allAttachments with timestamp: + // should return all non-removed attachments at this point in time var substorage = this._sub_storage, - // Include id as value in query object for safety (as opposed to string - // concatenation) - query_obj = new ComplexQuery({ - operator: "AND", - query_list: [ - new SimpleQuery({key: "doc_id", value: id}), - new ComplexQuery({ - operator: "OR", - query_list: [ - new SimpleQuery({key: "op", value: "putAttachment"}), - new SimpleQuery({key: "op", value: "removeAttachment"}) - ] - }) - ] - }), + query_obj, + query_removed_check, + options, + query_doc_id, + options_remcheck; - // Only query for attachment edits - options = { - query: query_obj, - sort_on: [["timestamp", "descending"]], - select_list: ["op", "timestamp", "name"] - }; - return this._sub_storage.allDocs(options) + if (isTimestamp(id)) { + query_doc_id = new SimpleQuery({ + operator: "<=", + key: "timestamp", + value: id + }); + } else { + query_doc_id = new SimpleQuery({key: "doc_id", value: id}); + } + + query_removed_check = new ComplexQuery({ + operator: "AND", + query_list: [ + query_doc_id, + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "put"}), + new SimpleQuery({key: "op", value: "remove"}) + ] + }) + ] + }); + + query_obj = new ComplexQuery({ + operator: "AND", + query_list: [ + query_doc_id, + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "putAttachment"}), + new SimpleQuery({key: "op", value: "removeAttachment"}) + ] + }) + ] + }); + + + options_remcheck = { + query: query_removed_check, + select_list: ["op", "timestamp"], + //limit: [0, 1], + sort_on: [["timestamp", "descending"]] + }; + options = { + query: query_obj, + sort_on: [["timestamp", "descending"]], + select_list: ["op", "name"] + }; + + return this._sub_storage.allDocs(options_remcheck) + .push(function (results) { + if (results.data.total_rows > 0) { + if (results.data.rows[0].value.op === "remove") { + throwRemovedError(id); + } + } else { + throwCantFindError(id); + } + }) + .push(function () { + return substorage.allDocs(options); + }) .push(function (results) { var seen = {}, attachments = [], attachment_promises = [], ind, entry; + // Only return attachments if: + // (it is the most recent revision) AND (it is a putAttachment) attachments = results.data.rows.filter(function (docum) { if (!seen.hasOwnProperty(docum.value.name)) { var output = (docum.value.op === "putAttachment"); @@ -193,17 +223,19 @@ return output; } }); + // Assembles object of attachment_name: attachment_object for (ind = 0; ind < attachments.length; ind += 1) { entry = attachments[ind]; attachment_promises[entry.value.name] = substorage.getAttachment(entry.id, entry.value.name); } + return RSVP.hash(attachment_promises); }); }; HistoryStorage.prototype.putAttachment = function (id, name, blob) { - var timestamp = unique_timestamp(Date.now()), + var timestamp = generateUniqueTimestamp(Date.now()), metadata = { // XXX: remove this attribute once query can sort_on id timestamp: timestamp, @@ -212,11 +244,6 @@ op: "putAttachment" }, substorage = this._sub_storage; - if (this._timestamps[id].hasOwnProperty(name)) { - this._timestamps[id][name].push(timestamp); - } else { - this._timestamps[id][name] = [timestamp]; - } return this._sub_storage.put(timestamp, metadata) .push(function () { return substorage.putAttachment(timestamp, name, blob); @@ -225,6 +252,17 @@ HistoryStorage.prototype.getAttachment = function (id, name) { + if (isTimestamp(id)) { + return this._sub_storage.getAttachment(id, name) + .push(undefined, function (error) { + if (error.status_code === 404 && + error instanceof jIO.util.jIOError) { + throwCantFindError(id); + } + throw error; + }); + } + // Query to get the last edit made to this document var substorage = this._sub_storage, @@ -268,39 +306,16 @@ if (results.data.rows.length > 0) { if (results.data.rows[0].value.op === "remove" || results.data.rows[0].value.op === "removeAttachment") { - throw new jIO.util.jIOError( - "HistoryStorage: cannot find object '" + id + "' (removed)", - 404 - ); + throwRemovedError(id); } - return substorage.getAttachment(results.data.rows[0].id, name) - .push(undefined, function (error) { - if (error.status_code === 404 && - error instanceof jIO.util.jIOError) { - throw new jIO.util.jIOError( - "HistoryStorage: cannot find object '" + id + "'", - 404 - ); - } - throw error; - }); + return substorage.getAttachment(results.data.rows[0].id, name); } - return substorage.getAttachment(id, name) - .push(undefined, function (error) { - if (error.status_code === 404 && - error instanceof jIO.util.jIOError) { - throw new jIO.util.jIOError( - "HistoryStorage: cannot find object '" + id + "'", - 404 - ); - } - throw error; - }); + throwCantFindError(id); }); }; HistoryStorage.prototype.removeAttachment = function (id, name) { - var timestamp = unique_timestamp(Date.now()), + var timestamp = generateUniqueTimestamp(Date.now()), metadata = { // XXX: remove this attribute once query can sort_on id timestamp: timestamp, @@ -308,7 +323,6 @@ name: name, op: "removeAttachment" }; - this._timestamps[id][name].push(timestamp); return this._sub_storage.put(timestamp, metadata); }; HistoryStorage.prototype.repair = function () { @@ -319,49 +333,27 @@ }; HistoryStorage.prototype.buildQuery = function (options) { + // XXX: if include_revisions, we should also include the document results + // for different edits of attachments // Set default values - if (options === undefined) { - options = {}; - } - if (options.query === undefined) { - options.query = ""; - } - if (options.sort_on === undefined) { - options.sort_on = []; - } - if (options.select_list === undefined) { - options.select_list = []; + if (options === undefined) {options = {}; } + if (options.query === undefined) {options.query = ""; } + if (options.sort_on === undefined) {options.sort_on = []; } + if (options.select_list === undefined) {options.select_list = []; } + if (options.include_revisions === undefined) { + options.include_revisions = false; } options.sort_on.push(["timestamp", "descending"]); options.query = jIO.QueryFactory.create(options.query); + var meta_options, substorage = this._sub_storage, - // Check if query involved _REVISION. If not, we will later place a - // (*) AND (_REVISION: =0) as the default handling of revisions - rev_query = false, - query_obj = options.query, - query_stack = [], - ind; - - if (query_obj instanceof ComplexQuery) { - query_stack.push(query_obj); - } else { - rev_query = (query_obj.key === "_timestamp"); - } - // Traverse through query tree to find mentions of _timestamp - // and stop as soon as it is found once - while (query_stack.length > 0 && (!rev_query)) { - query_obj = query_stack.pop(); - for (ind = 0; ind < query_obj.query_list.length; ind += 1) { - if (query_obj.query_list[ind].hasOwnProperty("query_list")) { - query_stack.push(query_obj.query_list[ind]); - } else if (query_obj.query_list[ind].key === "_timestamp") { - rev_query = true; - break; - } - } - } + // Check if query involved _timestamp. + // If not, use default behavior and only query on latest revisions + rev_query = options.include_revisions, + doc_id_name, + timestamp_name; // Query for all edits putting or removing documents (and nothing about // attachments) @@ -382,7 +374,8 @@ .push(function (results) { var seen = {}, query_matches, - docs_to_query; + docs_to_query, + i; // If !rev_query, then by default only consider latest revisions of // documents results = results.filter(function (docum) { @@ -395,29 +388,47 @@ } return false; }); + + // If any documents have property _doc_id, __doc_id, etc, then set + // doc_id_name to the first string which is not a property of any + // of the documents + doc_id_name = "_doc_id"; + timestamp_name = "_timestamp"; + for (i = 0; i < results.length; i += 1) { + while (results[i].doc.hasOwnProperty(doc_id_name)) { + doc_id_name = "_" + doc_id_name; + } + while (results[i].doc.hasOwnProperty(timestamp_name)) { + timestamp_name = "_" + timestamp_name; + } + } + docs_to_query = results.map(function (docum) { - // If it's a "remove" operation + // If it's a "remove" operation then it has no doc property if (!docum.hasOwnProperty("doc")) { docum.doc = {}; } - docum.doc._doc_id = docum.doc_id; - docum.doc._timestamp = docum.timestamp; + docum.doc[doc_id_name] = docum.doc_id; + docum.doc[timestamp_name] = docum.timestamp; return docum.doc; }); - options.select_list.push("_doc_id"); + options.select_list.push(doc_id_name); + options.select_list.push(timestamp_name); query_matches = options.query.exec(docs_to_query, options); return query_matches; }) // Format the results of the query, and return .push(function (query_matches) { return query_matches.map(function (docum) { - var doc_id = docum._doc_id; - delete docum._timestamp; - delete docum._doc_id; + var doc_id = docum[doc_id_name], + time = docum[timestamp_name]; + delete docum[timestamp_name]; + delete docum[doc_id_name]; return { doc: {}, value: docum, - id: doc_id + id: doc_id, + timestamp: time }; }); }); diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index 6a63bbd..4ac5de3 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -64,36 +64,47 @@ test("Testing proper adding/removing attachments", function () { stop(); - expect(9); + expect(10); var jio = this.jio, - timestamps = this.jio.__storage._timestamps, + not_history = this.not_history, + timestamps, blob2 = this.blob2, blob1 = this.blob1, other_blob = this.other_blob, otherother_blob = new Blob(['abcabc']); - jio.put("doc", {title: "foo0"}) + jio.put("doc", {title: "foo0"}) // 0 .push(function () { - return jio.put("doc2", {key: "val"}); + return jio.put("doc2", {key: "val"}); // 1 }) .push(function () { - return jio.putAttachment("doc", "attacheddata", blob1); + return jio.putAttachment("doc", "attacheddata", blob1); // 2 }) .push(function () { - return jio.putAttachment("doc", "attacheddata", blob2); + return jio.putAttachment("doc", "attacheddata", blob2); // 3 }) .push(function () { - return jio.putAttachment("doc", "other_attacheddata", other_blob); + return jio.putAttachment("doc", "other_attacheddata", other_blob);// 4 }) .push(function () { - return jio.putAttachment( + return jio.putAttachment( // 5 "doc", "otherother_attacheddata", otherother_blob ); }) .push(function () { - return jio.removeAttachment("doc", "otherother_attacheddata"); + return jio.removeAttachment("doc", "otherother_attacheddata"); // 6 + }) + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); }) .push(function () { return jio.get("doc"); @@ -110,7 +121,7 @@ "Return the attachment information with getAttachment" ); return jio.getAttachment( - timestamps.doc.attacheddata[1], + timestamps[3], "attacheddata" ); }) @@ -121,7 +132,7 @@ "current revision" ); return jio.getAttachment( - timestamps.doc.attacheddata[0], + timestamps[2], "attacheddata" ); }, function (error) { @@ -134,7 +145,7 @@ "Return the attachment information with getAttachment for " + "previous revision" ); - return jio.getAttachment(timestamps.doc[0], "attached"); + return jio.getAttachment(timestamps[0], "attached"); }, function (error) { ok(false, error); }) @@ -146,6 +157,9 @@ deepEqual(error.status_code, 404, "Error if you try to go back to a nonexistent timestamp"); + deepEqual(error.message, + "HistoryStorage: cannot find object '" + timestamps[0] + "'", + "Error caught by history storage correctly"); return jio.getAttachment("doc", "other_attacheddata"); }) .push(function (result) { @@ -208,10 +222,10 @@ .always(function () {start(); }); }); - test("Correctness of allAttachments method", + test("Correctness of allAttachments method on current attachments", function () { stop(); - expect(11); + expect(14); var jio = this.jio, not_history = this.not_history, blob1 = this.blob1, @@ -317,7 +331,7 @@ })); }) .push(function (results) { - equal(results.length, 7, "Seven document revisions in storage (17)"); + equal(results.length, 7, "Seven document revisions in storage"); return jio.remove("doc"); }) .push(function () { @@ -332,6 +346,132 @@ 404, "Cannot get the attachment of a removed document"); }) + .push(function () { + return jio.allAttachments("doc"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "throws a jio error"); + deepEqual(error.status_code, + 404, + "allAttachments of a removed document throws a 404 error"); + deepEqual(error.message, + "HistoryStorage: cannot find object 'doc' (removed)", + "Error is handled by Historystorage."); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + + test("Correctness of allAttachments method on older revisions", + function () { + stop(); + expect(8); + var jio = this.jio, + not_history = this.not_history, + blob1 = new Blob(['a']), + blob11 = new Blob(['ab']), + blob2 = new Blob(['abc']), + blob22 = new Blob(['abcd']), + timestamps; + + jio.put("doc", {title: "foo0"}) // 0 + .push(function () { + return jio.putAttachment("doc", "data", blob1); + }) + .push(function () { + return jio.putAttachment("doc", "data2", blob2); + }) + .push(function () { + return jio.put("doc", {title: "foo1"}); // 1 + }) + .push(function () { + return jio.removeAttachment("doc", "data2"); + }) + .push(function () { + return jio.put("doc", {title: "foo2"}); // 2 + }) + .push(function () { + return jio.putAttachment("doc", "data", blob11); + }) + .push(function () { + return jio.remove("doc"); // 3 + }) + .push(function () { + return jio.put("doc", {title: "foo3"}); // 4 + }) + .push(function () { + return jio.putAttachment("doc", "data2", blob22); + }) + .push(function () { + return not_history.allDocs({ + query: "op: put OR op: remove", + sort_on: [["timestamp", "ascending"]], + select_list: ["timestamp"] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) + .push(function () { + return jio.allAttachments("doc"); + }) + .push(function (results) { + deepEqual(results, { + "data": blob11, + "data2": blob22 + }, + "Current state of document is correct"); + + return jio.allAttachments(timestamps[0]); + }) + .push(function (results) { + deepEqual(results, {}, "First version of document has 0 attachments"); + + return jio.allAttachments(timestamps[1]); + }) + .push(function (results) { + deepEqual(results, { + data: blob1, + data2: blob2 + }, "Both attachments are included in allAttachments"); + + return jio.allAttachments(timestamps[2]); + }) + .push(function (results) { + deepEqual(results, { + data: blob1 + }, "Removed attachment does not show up in allAttachments"); + return jio.allAttachments(timestamps[3]); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "throws a jio error"); + deepEqual(error.status_code, + 404, + "allAttachments of a removed document throws a 404 error"); + deepEqual(error.message, + "HistoryStorage: cannot find object '" + timestamps[3] + + "' (removed)", + "Error is handled by Historystorage."); + }) + .push(function () { + return jio.allAttachments(timestamps[4]); + }) + .push(function (results) { + deepEqual(results, { + data: blob11 + }); + }) .fail(function (error) { //console.log(error); ok(false, error); @@ -377,49 +517,48 @@ test("Handling bad input", function () { stop(); - expect(6); + expect(2); var jio = this.jio, BADINPUT_ERRCODE = 422; - jio.put("doc", { - "_timestamp": 3, - "other_attr": "other_val" - }) + jio.put("1234567891123-ab7d", {}) .push(function () { ok(false, "This statement should not be reached"); }, function (error) { ok(error instanceof jIO.util.jIOError, "Correct type of error"); deepEqual(error.status_code, BADINPUT_ERRCODE, - "Can't save a document with a reserved keyword" + "Can't save a document with a timestamp-formatted id" ); }) - .push(function () { - return jio.put("doc", { - "_doc_id": 3, - "other_attr": "other_val" - }); - }) - .push(function () { - ok(false, "This statement should not be reached"); - }, function (error) { - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - deepEqual(error.status_code, - BADINPUT_ERRCODE, - "Can't save a document with a reserved keyword" - ); + .fail(function (error) { + //console.log(error); + ok(false, error); }) + .always(function () {start(); }); + }); + + test("Getting a non-existent document", + function () { + stop(); + expect(3); + var jio = this.jio; + jio.put("not_doc", {}) .push(function () { - return jio.put("1234567891123-ab7d", {}); + return jio.get("doc"); }) .push(function () { ok(false, "This statement should not be reached"); }, function (error) { + //console.log(error); ok(error instanceof jIO.util.jIOError, "Correct type of error"); deepEqual(error.status_code, - BADINPUT_ERRCODE, - "Can't save a document with a timestamp-formatted id" + 404, + "Correct status code for getting a non-existent document" ); + deepEqual(error.message, + "HistoryStorage: cannot find object 'doc'", + "Error is handled by history storage before reaching console"); }) .fail(function (error) { //console.log(error); @@ -431,31 +570,39 @@ test("Creating a document with put and retrieving it with get", function () { stop(); - expect(4); + expect(7); var jio = this.jio, not_history = this.not_history, - timestamps = jio.__storage._timestamps; + timestamps; jio.put("doc", {title: "version0"}) .push(function () { - ok(timestamps.hasOwnProperty("doc"), - "jio._timestamps is updated with new document."); - equal(timestamps.doc.length, + return not_history.allDocs({ + select_list: ["timestamp"] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) + .push(function () { + equal(timestamps.length, 1, - "One revision is logged in jio._timestamps" + "One revision is saved in storage" ); - return jio.get(timestamps.doc[0]); + return jio.get(timestamps[0]); }) .push(function (result) { deepEqual(result, { title: "version0" }, "Get document from history storage"); return not_history.get( - timestamps.doc[0] + timestamps[0] ); }) .push(function (result) { deepEqual(result, { - timestamp: timestamps.doc[0], + timestamp: timestamps[0], op: "put", doc_id: "doc", doc: { @@ -463,6 +610,31 @@ } }, "Get document from non-history storage"); }) + .push(function () { + return jio.get("non-existent-doc"); + }) + .push(function () { + ok(false, "This should have thrown an error"); + }, function (error) { + //console.log(error); + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Can't access non-existent document" + ); + }) + .push(function () { + return jio.get("1234567891123-abcd"); + }) + .push(function () { + ok(false, "Trying to get a non-existent id should have raised 404"); + }, function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Can't access document by getting with non-existent id" + ); + }) .fail(function (error) { //console.log(error); ok(false, error); @@ -475,7 +647,8 @@ stop(); expect(7); var jio = this.jio, - timestamps = this.jio.__storage._timestamps; + not_history = this.not_history, + timestamps; return jio.put("doc", {title: "t0", subtitle: "s0"}) .push(function () { @@ -490,6 +663,17 @@ .push(function () { return jio.put("doc", {title: "t3", subtitle: "s3"}); }) + .push(function () { + return not_history.allDocs({ + select_list: ["timestamp"], + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) .push(function () { return jio.get("doc"); }) @@ -498,7 +682,7 @@ title: "t3", subtitle: "s3" }, "Get returns latest revision"); - return jio.get(timestamps.doc[0]); + return jio.get(timestamps[0]); }, function (err) { ok(false, err); }) @@ -507,14 +691,14 @@ title: "t0", subtitle: "s0" }, "Get returns first version"); - return jio.get(timestamps.doc[1]); + return jio.get(timestamps[1]); }) .push(function (result) { deepEqual(result, { title: "t1", subtitle: "s1" }, "Get returns second version"); - return jio.get(timestamps.doc[2]); + return jio.get(timestamps[2]); }, function (err) { ok(false, err); }) @@ -523,20 +707,20 @@ title: "t2", subtitle: "s2" }, "Get returns third version"); - return jio.get(timestamps.doc[3]); + return jio.get(timestamps[3]); }, function (err) { ok(false, err); }) .push(function () { ok(false, "This should have thrown a 404 error"); - return jio.get(timestamps.doc[4]); + return jio.get(timestamps[4]); }, function (error) { ok(error instanceof jIO.util.jIOError, "Correct type of error"); deepEqual(error.status_code, 404, "Error if you try to go back more revisions than what exists"); - return jio.get(timestamps.doc[4]); + return jio.get(timestamps[4]); }) .push(function (result) { deepEqual(result, { @@ -674,8 +858,18 @@ stop(); expect(7); var jio = this.jio, - not_history = this.not_history; + not_history = this.not_history, + timestamp; jio.put("doc", {title: "version0"}) + .push(function () { + return not_history.allDocs({ + query: "doc_id: doc", + select_list: ["timestamp"] + }); + }) + .push(function (results) { + timestamp = results.data.rows[0].value.timestamp; + }) .push(function () { return RSVP.all([ jio.allDocs(), @@ -701,7 +895,8 @@ deepEqual(results.data.rows[0], { doc: {}, value: {}, - id: "doc" + id: "doc", + timestamp: timestamp }, "Correct document format is returned." ); @@ -715,7 +910,7 @@ }) .push(function (result) { deepEqual(result, { - timestamp: jio.__storage._timestamps.doc[0], + timestamp: timestamp, doc_id: "doc", doc: { title: "version0" @@ -733,14 +928,69 @@ .always(function () {start(); }); }); + test("Putting doc with _doc_id and _timestamp properties" + + "and retrieving them with allDocs", + function () { + stop(); + expect(1); + var jio = this.jio, + not_history = this.not_history, + timestamp; + jio.put("doc", { + title: "version0", + _doc_id: "bar", + __doc_id: "bar2", + ___doc_id: "bar3", + _timestamp: "foo", + ____timestamp: "foo2" + }) + .push(function () { + return not_history.allDocs({ + query: "doc_id: doc", + select_list: ["timestamp"] + }); + }) + .push(function (results) { + timestamp = results.data.rows[0].value.timestamp; + }) + .push(function () { + return jio.allDocs({ + query: "title: version0 AND _timestamp: >= 0", + select_list: ["title", "_doc_id", "__doc_id", "___doc_id", + "_timestamp", "____timestamp"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc", + value: { + title: "version0", + _doc_id: "bar", + __doc_id: "bar2", + ___doc_id: "bar3", + _timestamp: "foo", + ____timestamp: "foo2" + }, + timestamp: timestamp + }], + "_doc_id properties are not overwritten in allDocs call"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); test("Putting a document, revising it, and retrieving revisions with allDocs", function () { stop(); - expect(14); + expect(10); var jio = this.jio, not_history = this.not_history, - timestamps = this.jio.__storage._timestamps; + timestamps; jio.put("doc", { title: "version0", subtitle: "subvers0" @@ -757,6 +1007,17 @@ subtitle: "subvers2" }); }) + .push(function () { + return not_history.allDocs({ + select_list: ["timestamp"], + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) .push(function () { return RSVP.all([ jio.allDocs({select_list: ["title", "subtitle"]}), @@ -803,73 +1064,52 @@ subtitle: "subvers2" }, doc: {}, - id: "doc" + id: "doc", + timestamp: timestamps[2] }, "Correct document format is returned." ); }) .push(function () { - // These are all equivalent queries in that they should return the - // same documents in the correct order - return RSVP.all([ - jio.allDocs({ - query: "_timestamp: " + timestamps.doc[1], - select_list: ["title", "subtitle"] - }), - jio.allDocs({ - query: "_timestamp: =" + timestamps.doc[1], - select_list: ["title", "subtitle"] - }), - jio.allDocs({ - query: "_timestamp: >" + timestamps.doc[0] + - " AND title: version1", - select_list: ["title", "subtitle"] - }), - jio.allDocs({ - query: "_timestamp: > " + timestamps.doc[0] + - " AND _timestamp: < " + timestamps.doc[2], - select_list: ["title", "subtitle"] - }) - ]); - }) - .push(function (results) { - equal(results[0].data.rows.length, - 1, - "Querying a specific timestamp retrieves one document"); - var ind = 0; - for (ind = 0; ind < results.length - 1; ind += 1) { - deepEqual(results[ind], - results[ind + 1], - "Each query returns exactly the same correct output" - ); - } - return results[0]; - }) - .push(function (results) { - deepEqual(results.data.rows[0], { - id: "doc", - value: { - title: "version1", - subtitle: "subvers1" - }, - doc: {} - }); return jio.allDocs({ - query: "_timestamp: " + timestamps.doc[0], - select_list: ["title", "subtitle"] + query: "", + select_list: ["title", "subtitle"], + include_revisions: true }); }) .push(function (results) { + equal(results.data.rows.length, + 3, + "Querying with include_revisions retrieves all versions"); deepEqual(results.data.rows, [ { + id: "doc", + value: { + title: "version2", + subtitle: "subvers2" + }, + doc: {}, + timestamp: timestamps[2] + }, + { + id: "doc", + value: { + title: "version1", + subtitle: "subvers1" + }, + doc: {}, + timestamp: timestamps[1] + }, + { + id: "doc", value: { title: "version0", subtitle: "subvers0" }, doc: {}, - id: "doc" + timestamp: timestamps[0] } - ], "Query requesting one timestamp works."); + ], "Full version history is included."); return not_history.allDocs({ sort_on: [["title", "ascending"]] @@ -883,7 +1123,7 @@ .push(function (results) { deepEqual(results, [ { - timestamp: timestamps.doc[0], + timestamp: timestamps[0], op: "put", doc_id: "doc", doc: { @@ -892,7 +1132,7 @@ } }, { - timestamp: timestamps.doc[1], + timestamp: timestamps[1], op: "put", doc_id: "doc", doc: { @@ -901,7 +1141,7 @@ } }, { - timestamp: timestamps.doc[2], + timestamp: timestamps[2], op: "put", doc_id: "doc", doc: { @@ -924,43 +1164,54 @@ "Putting and removing documents, latest revisions and no removed documents", function () { stop(); - expect(5); - var history = this.jio, - timestamps = this.jio.__storage._timestamps; + expect(3); + var jio = this.jio, + not_history = this.not_history, + timestamps; - history.put("doc_a", { + jio.put("doc_a", { title_a: "rev0", subtitle_a: "subrev0" }) .push(function () { - return history.put("doc_a", { + return jio.put("doc_a", { title_a: "rev1", subtitle_a: "subrev1" }); }) .push(function () { - return history.put("doc_b", { + return jio.put("doc_b", { title_b: "rev0", subtitle_b: "subrev0" }); }) .push(function () { - return history.remove("doc_b"); + return jio.remove("doc_b"); }) .push(function () { - return history.put("doc_c", { + return jio.put("doc_c", { title_c: "rev0", subtitle_c: "subrev0" }); }) .push(function () { - return history.put("doc_c", { + return jio.put("doc_c", { title_c: "rev1", subtitle_c: "subrev1" }); }) .push(function () { - return history.allDocs({sort_on: [["timestamp", "descending"]]}); + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return jio.allDocs({sort_on: [["timestamp", "descending"]]}); }) .push(function (results) { equal(results.data.rows.length, @@ -971,24 +1222,20 @@ { id: "doc_c", value: {}, - doc: {} + doc: {}, + timestamp: timestamps[5] }, { id: "doc_a", value: {}, - doc: {} + doc: {}, + timestamp: timestamps[1] } ], "Empty query returns latest revisions (and no removed documents)"); - equal(timestamps.doc_a.length, - 2, - "Correct number of revisions logged in doc_a"); - equal(timestamps.doc_b.length, - 2, - "Correct number of revisions logged in doc_b"); - equal(timestamps.doc_c.length, - 2, - "Correct number of revisions logged in doc_c"); + equal(timestamps.length, + 6, + "Correct number of revisions logged"); }) .fail(function (error) { //console.log(error); @@ -1006,7 +1253,9 @@ function () { stop(); expect(2); - var history = this.jio, + var jio = this.jio, + not_history = this.not_history, + timestamps, docs = [ { "date": 1, @@ -1029,19 +1278,29 @@ new Blob(['bcd']), new Blob(['eeee']) ]; - history.put("doc", {}) + jio.put("doc", {}) // 0 .push(function () { - return putFullDoc(history, "doc", docs[0], "data", blobs[0]); + return putFullDoc(jio, "doc", docs[0], "data", blobs[0]); // 1,2 }) .push(function () { - return putFullDoc(history, "second_doc", docs[1], "data", blobs[1]); + return putFullDoc(jio, "second_doc", docs[1], "data", blobs[1]);// 3,4 }) .push(function () { - return putFullDoc(history, "third_doc", docs[2], "data", blobs[2]); + return putFullDoc(jio, "third_doc", docs[2], "data", blobs[2]); // 5,6 }) .push(function () { - return history.allDocs({ - query: "(date: <= 2)", + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return jio.allDocs({ + query: "NOT (date: > 2)", select_list: ["date", "non-existent-key"], sort_on: [["date", "ascending"], ["non-existent-key", "ascending"] @@ -1054,17 +1313,20 @@ { doc: {}, id: "doc", - value: {date: 1} + value: {date: 1}, + timestamp: timestamps[1] }, { doc: {}, id: "third_doc", - value: {date: 2} + value: {date: 2}, + timestamp: timestamps[5] }, { doc: {}, id: "second_doc", - value: {date: 2} + value: {date: 2}, + timestamp: timestamps[3] } ], "Query gives correct results in correct order"); @@ -1086,7 +1348,7 @@ expect(3); var jio = this.jio, not_history = this.not_history, - timestamps = this.jio.__storage._timestamps, + timestamps, docs = [ { "date": 1, @@ -1106,11 +1368,11 @@ new Blob(['bcd2']), new Blob(['a3']) ]; - jio.put("doc", {}) - .push(function () { + jio.put("doc", {})// 0 + .push(function () {// 1,2 return putFullDoc(jio, "doc", docs[0], "data", blobs[0]); }) - .push(function () { + .push(function () {// 3,4 return putFullDoc(jio, "second_doc", docs[1], "data", blobs[1]); }) .push(function () { @@ -1119,15 +1381,25 @@ docs[1].date = 4; docs[1].type = "bar2"; }) - .push(function () { + .push(function () {// 5,6 return putFullDoc(jio, "doc", docs[0], "data", blobs[2]); }) - .push(function () { + .push(function () {// 7 return jio.remove("second_doc"); }) - .push(function () { + .push(function () {// 8,9 return putFullDoc(jio, "second_doc", docs[1], "data", blobs[3]); }) + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) .push(function () { return not_history.allDocs({ sort_on: [["timestamp", "descending"]], @@ -1138,92 +1410,92 @@ deepEqual(results.data.rows, [ { doc: {}, - id: timestamps.second_doc.data[1], + id: timestamps[9], value: { "op": "putAttachment", "doc_id": "second_doc", - "timestamp": timestamps.second_doc.data[1] + "timestamp": timestamps[9] } }, { doc: {}, - id: timestamps.second_doc[2], + id: timestamps[8], value: { "op": "put", "doc_id": "second_doc", - "timestamp": timestamps.second_doc[2] + "timestamp": timestamps[8] } }, { doc: {}, - id: timestamps.second_doc[1], + id: timestamps[7], value: { "op": "remove", "doc_id": "second_doc", - "timestamp": timestamps.second_doc[1] + "timestamp": timestamps[7] } }, { doc: {}, - id: timestamps.doc.data[1], + id: timestamps[6], value: { "op": "putAttachment", "doc_id": "doc", - "timestamp": timestamps.doc.data[1] + "timestamp": timestamps[6] } }, { doc: {}, - id: timestamps.doc[2], + id: timestamps[5], value: { "op": "put", "doc_id": "doc", - "timestamp": timestamps.doc[2] + "timestamp": timestamps[5] } }, { doc: {}, - id: timestamps.second_doc.data[0], + id: timestamps[4], value: { "op": "putAttachment", "doc_id": "second_doc", - "timestamp": timestamps.second_doc.data[0] + "timestamp": timestamps[4] } }, { doc: {}, - id: timestamps.second_doc[0], + id: timestamps[3], value: { "op": "put", "doc_id": "second_doc", - "timestamp": timestamps.second_doc[0] + "timestamp": timestamps[3] } }, { doc: {}, - id: timestamps.doc.data[0], + id: timestamps[2], value: { "op": "putAttachment", "doc_id": "doc", - "timestamp": timestamps.doc.data[0] + "timestamp": timestamps[2] } }, { doc: {}, - id: timestamps.doc[1], + id: timestamps[1], value: { "op": "put", "doc_id": "doc", - "timestamp": timestamps.doc[1] + "timestamp": timestamps[1] } }, { doc: {}, - id: timestamps.doc[0], + id: timestamps[0], value: { "op": "put", "doc_id": "doc", - "timestamp": timestamps.doc[0] + "timestamp": timestamps[0] } } ], "All operations are logged correctly"); @@ -1269,14 +1541,13 @@ }) .push(function () { return jio.allDocs({ - query: "(_timestamp: >= " + timestamps.second_doc[0] + - " OR _timestamp: <= " + timestamps.doc[1] + - ") AND NOT (date: = 2)", + query: "NOT (date: >= 2 AND date: <= 3)", select_list: ["date", "non-existent-key", "type", "title"], sort_on: [["date", "descending"], ["non-existent-key", "ascending"], ["_timestamp", "ascending"] - ] + ], + include_revisions: true }); }) .push(function (results) { @@ -1288,7 +1559,8 @@ date: 4, title: "doc", type: "foo2" - } + }, + timestamp: timestamps[5] }, { doc: {}, @@ -1297,7 +1569,8 @@ date: 4, title: "second_doc", type: "bar2" - } + }, + timestamp: timestamps[8] }, { doc: {}, @@ -1306,12 +1579,14 @@ date: 1, title: "doc", type: "foo" - } + }, + timestamp: timestamps[1] }, { doc: {}, id: "doc", - value: {} + value: {}, + timestamp: timestamps[0] } ], "Query gives correct results in correct order"); -- 2.30.9 From f9bdfa9bf9768d655872d4a70903501e9bbd0fc9 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Thu, 28 Jun 2018 08:34:02 +0000 Subject: [PATCH 36/46] Finished revisions to buildquery. --- src/jio.storage/historystorage.js | 132 +++++-- test/jio.storage/historystorage.tests.js | 445 ++++++++++++++++++++++- 2 files changed, 534 insertions(+), 43 deletions(-) diff --git a/src/jio.storage/historystorage.js b/src/jio.storage/historystorage.js index 817d22a..1970d95 100644 --- a/src/jio.storage/historystorage.js +++ b/src/jio.storage/historystorage.js @@ -44,6 +44,7 @@ */ function HistoryStorage(spec) { this._sub_storage = jIO.createJIO(spec.sub_storage); + this._include_revisions = spec.include_revisions; } HistoryStorage.prototype.get = function (id_in) { @@ -358,7 +359,7 @@ // Query for all edits putting or removing documents (and nothing about // attachments) meta_options = { - query: "(op: remove) OR (op: put)", + query: "",//(op: remove) OR (op: put)", sort_on: options.sort_on }; return this._sub_storage.allDocs(meta_options) @@ -376,44 +377,115 @@ query_matches, docs_to_query, i; - // If !rev_query, then by default only consider latest revisions of - // documents - results = results.filter(function (docum) { - if (rev_query) { - return docum.op === "put"; - } - if (!seen.hasOwnProperty(docum.doc_id)) { - seen[docum.doc_id] = {}; - return docum.op === "put"; - } - return false; - }); - // If any documents have property _doc_id, __doc_id, etc, then set - // doc_id_name to the first string which is not a property of any - // of the documents + doc_id_name = "_doc_id"; timestamp_name = "_timestamp"; for (i = 0; i < results.length; i += 1) { - while (results[i].doc.hasOwnProperty(doc_id_name)) { - doc_id_name = "_" + doc_id_name; - } - while (results[i].doc.hasOwnProperty(timestamp_name)) { - timestamp_name = "_" + timestamp_name; + if (results[i].op === "put") { + while (results[i].doc.hasOwnProperty(doc_id_name)) { + doc_id_name = "_" + doc_id_name; + } + while (results[i].doc.hasOwnProperty(timestamp_name)) { + timestamp_name = "_" + timestamp_name; + } } } - docs_to_query = results.map(function (docum) { - // If it's a "remove" operation then it has no doc property - if (!docum.hasOwnProperty("doc")) { - docum.doc = {}; - } - docum.doc[doc_id_name] = docum.doc_id; - docum.doc[timestamp_name] = docum.timestamp; - return docum.doc; - }); + if (rev_query) { + // Only query on documents which are puts are putAttachments + results = results.map(function (docum, ind) { + var data_key; + if (docum.op === "put") { + return docum; + } + if (docum.op === "putAttachment") { + docum.doc = {}; + for (i = ind + 1; i < results.length; i += 1) { + if (results[i].doc_id === docum.doc_id) { + if (results[i].op === "put") { + for (data_key in results[i].doc) { + if (results[i].doc.hasOwnProperty(data_key)) { + docum.doc[data_key] = results[i].doc[data_key]; + } + } + return docum; + } + if (results[i].doc_id === "remove") { + //console.log("not returning putAttachment at ", + // docum.timestamp, + // " because it was attached to a removed document"); + return false; + } + } + } + } + return false; + }); + } else { + results = results.map(function (docum, ind) { + var data_key; + if (docum.op === "put") { + if (!seen.hasOwnProperty(docum.doc_id)) { + seen[docum.doc_id] = {}; + //console.log("returning put at ", docum.timestamp, + // " because it is most recent edit to " + docum.doc_id); + return docum; + } + //console.log("not returning put at ", docum.timestamp, + // " because it was edited later"); + } else if (docum.op === "remove") { + seen[docum.doc_id] = {}; + } else if (docum.op === "putAttachment") { + if (!seen.hasOwnProperty(docum.doc_id)) { + seen[docum.doc_id] = {}; + docum.doc = {}; + for (i = ind + 1; i < results.length; i += 1) { + if (results[i].doc_id === docum.doc_id) { + if (results[i].op === "put") { + for (data_key in results[i].doc) { + if (results[i].doc.hasOwnProperty(data_key)) { + docum.doc[data_key] = results[i].doc[data_key]; + } + } + /**console.log("returning putAttachment at ", + docum.timestamp, + " because it is most recent edit to attachment " + + docum.name + " of document " + docum.doc_id); + **/ + return docum; + } + if (results[i].doc_id === "remove") { + /**console.log("not returning putAttachment at ", + docum.timestamp, + " because it was attached to a removed document"); + **/ + return false; + } + } + } + } + } else if (docum.op === "removeAttachment") { + seen[docum.doc_id] = {}; + } + return false; + }); + } + docs_to_query = results + .filter(function (docum) { + return docum; + }) + .map(function (docum) { + docum.doc[timestamp_name] = docum.timestamp; + docum.doc[doc_id_name] = docum.doc_id; + return docum.doc; + }); + options.select_list.push(doc_id_name); options.select_list.push(timestamp_name); + options.sort_on[options.sort_on.length - 1] = [ + timestamp_name, "descending" + ]; query_matches = options.query.exec(docs_to_query, options); return query_matches; }) diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index 4ac5de3..e1b9e7b 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -254,7 +254,7 @@ "attacheddata": blob2, "other_attacheddata": other_blob }, "allAttachments works as expected."); - return jio.removeAttachment("doc", "attacheddata"); + return jio.removeAttachment("doc", "attacheddata"); // }) .push(function () { return jio.get("doc"); @@ -279,7 +279,7 @@ deepEqual(results, { "other_attacheddata": blob2 }, "allAttachments works as expected with a removed attachment"); - return jio.putAttachment("doc", "attacheddata", blob3); + return jio.putAttachment("doc", "attacheddata", blob3); // }) .push(function () { return not_history.allDocs(); @@ -320,7 +320,7 @@ }) .push(function (result) { deepEqual(result, { - "title": "foo0" + "key": "val" }, "Get second document accessible from jio storage"); return not_history.allDocs(); @@ -1314,19 +1314,19 @@ doc: {}, id: "doc", value: {date: 1}, - timestamp: timestamps[1] + timestamp: timestamps[2] }, { doc: {}, id: "third_doc", value: {date: 2}, - timestamp: timestamps[5] + timestamp: timestamps[6] }, { doc: {}, id: "second_doc", value: {date: 2}, - timestamp: timestamps[3] + timestamp: timestamps[4] } ], "Query gives correct results in correct order"); @@ -1543,9 +1543,7 @@ return jio.allDocs({ query: "NOT (date: >= 2 AND date: <= 3)", select_list: ["date", "non-existent-key", "type", "title"], - sort_on: [["date", "descending"], - ["non-existent-key", "ascending"], - ["_timestamp", "ascending"] + sort_on: [["date", "descending"] ], include_revisions: true }); @@ -1554,13 +1552,13 @@ deepEqual(results.data.rows, [ { doc: {}, - id: "doc", + id: "second_doc", value: { date: 4, - title: "doc", - type: "foo2" + title: "second_doc", + type: "bar2" }, - timestamp: timestamps[5] + timestamp: timestamps[9] }, { doc: {}, @@ -1572,6 +1570,37 @@ }, timestamp: timestamps[8] }, + { + doc: {}, + id: "doc", + value: { + date: 4, + title: "doc", + type: "foo2" + }, + timestamp: timestamps[6] + }, + { + doc: {}, + id: "doc", + value: { + date: 4, + title: "doc", + type: "foo2" + }, + timestamp: timestamps[5] + }, + + { + doc: {}, + id: "doc", + value: { + date: 1, + title: "doc", + type: "foo" + }, + timestamp: timestamps[2] + }, { doc: {}, id: "doc", @@ -1597,4 +1626,394 @@ }) .always(function () {start(); }); }); + + + module("HistoryStorage.Full-Example", { + setup: function () { + // create storage of type "history" with memory as substorage + var dbname = "db_" + Date.now(); + this.blob1 = new Blob(['a']); + this.blob2 = new Blob(['b']); + this.blob3 = new Blob(['ccc']); + this.other_blob = new Blob(['1']); + this.jio = jIO.createJIO({ + type: "history", + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + }); + this.not_history = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + }); + } + }); + + test("Retrieving history with attachments", + function () { + stop(); + expect(1); + var jio = this.jio, + not_history = this.not_history, + timestamps, + blobs1 = [ + new Blob(['a']), + new Blob(['ab']), + new Blob(['abc']), + new Blob(['abcd']), + new Blob(['abcde']) + ], + blobs2 = [ + new Blob(['abcdef']), + new Blob(['abcdefg']), + new Blob(['abcdefgh']), + new Blob(['abcdefghi']), + new Blob(['abcdefghij']) + ]; + putFullDoc(jio, "doc", {title: "bar"}, "data", blobs1[0]) + .push(function () { + return putFullDoc(jio, "doc", {title: "bar0"}, "data", blobs1[1]); + }) + .push(function () { + return putFullDoc(jio, "doc", {title: "bar1"}, "data", blobs1[2]); + }) + .push(function () { + return putFullDoc(jio, "doc2", {title: "foo0"}, "data", blobs2[0]); + }) + .push(function () { + return putFullDoc(jio, "doc2", {title: "foo1"}, "data", blobs2[0]); + }) + .push(function () { + return putFullDoc(jio, "doc", {title: "bar2"}, "data", blobs1[3]); + }) + .push(function () { + return putFullDoc(jio, "doc", {title: "bar3"}, "data", blobs1[4]); + }) + + // Get timestamps + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + + .push(function () { + return jio.allDocs({ + select_list: ["title"], + include_revisions: true + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc", + value: {title: "bar3"}, + timestamp: timestamps[13] + }, + { + doc: {}, + id: "doc", + value: {title: "bar3"}, + timestamp: timestamps[12] + }, + { + doc: {}, + id: "doc", + value: {title: "bar2"}, + timestamp: timestamps[11] + }, + { + doc: {}, + id: "doc", + value: {title: "bar2"}, + timestamp: timestamps[10] + }, + { + doc: {}, + id: "doc2", + value: {title: "foo1"}, + timestamp: timestamps[9] + }, + { + doc: {}, + id: "doc2", + value: {title: "foo1"}, + timestamp: timestamps[8] + }, + { + doc: {}, + id: "doc2", + value: {title: "foo0"}, + timestamp: timestamps[7] + }, + { + doc: {}, + id: "doc2", + value: {title: "foo0"}, + timestamp: timestamps[6] + }, + { + doc: {}, + id: "doc", + value: {title: "bar1"}, + timestamp: timestamps[5] + }, + { + doc: {}, + id: "doc", + value: {title: "bar1"}, + timestamp: timestamps[4] + }, + { + doc: {}, + id: "doc", + value: {title: "bar0"}, + timestamp: timestamps[3] + }, + { + doc: {}, + id: "doc", + value: {title: "bar0"}, + timestamp: timestamps[2] + }, + { + doc: {}, + id: "doc", + value: {title: "bar"}, + timestamp: timestamps[1] + }, + { + doc: {}, + id: "doc", + value: {title: "bar"}, + timestamp: timestamps[0] + } + ], + "allDocs with include_revisions should return all revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + + + test("Retrieving history with attachments with less straightforward ordering", + function () { + stop(); + expect(1); + var jio = this.jio, + not_history = this.not_history, + timestamps, + blobs1 = [ + new Blob(['a']), + new Blob(['ab']), + new Blob(['abc']), + new Blob(['abcd']), + new Blob(['abcde']) + ]; + jio.put("doc", {title: "bar"}) + .push(function () { + return jio.put("doc", {title: "bar0"}); + }) + .push(function () { + return jio.putAttachment("doc", "data", blobs1[0]); + }) + .push(function () { + return jio.put("doc2", {title: "foo0"}); + }) + .push(function () { + return jio.putAttachment("doc", "data", blobs1[1]); + }) + + // Get timestamps + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + + .push(function () { + return jio.allDocs({ + select_list: ["title"], + include_revisions: true + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc", + value: {title: "bar0"}, + timestamp: timestamps[4] + }, + { + doc: {}, + id: "doc2", + value: {title: "foo0"}, + timestamp: timestamps[3] + }, + { + doc: {}, + id: "doc", + value: {title: "bar0"}, + timestamp: timestamps[2] + }, + { + doc: {}, + id: "doc", + value: {title: "bar0"}, + timestamp: timestamps[1] + }, + { + doc: {}, + id: "doc", + value: {title: "bar"}, + timestamp: timestamps[0] + } + ], + "allDocs with include_revisions should return all revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + + + test("Retrieving history with attachments with removals", + function () { + stop(); + expect(2); + var jio = this.jio, + not_history = this.not_history, + timestamps, + blobs1 = [ + new Blob(['a']), + new Blob(['ab']), + new Blob(['abc']), + new Blob(['abcd']), + new Blob(['abcde']) + ]; + jio.put("doc", {title: "bar"}) + .push(function () { + return jio.put("doc", {title: "bar0"}); + }) + .push(function () { + return jio.putAttachment("doc", "data", blobs1[0]); + }) + .push(function () { + return jio.put("doc2", {title: "foo0"}); + }) + .push(function () { + return jio.putAttachment("doc", "data", blobs1[1]); + }) + + // Get timestamps + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return jio.allDocs({ + select_list: ["title"], + include_revisions: false + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc", + value: {title: "bar0"}, + timestamp: timestamps[4] + }, + { + doc: {}, + id: "doc2", + value: {title: "foo0"}, + timestamp: timestamps[3] + } + ], + "allDocs with include_revisions false should return all revisions"); + }) + .push(function () { + return jio.allDocs({ + select_list: ["title"], + include_revisions: true + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc", + value: {title: "bar0"}, + timestamp: timestamps[4] + }, + { + doc: {}, + id: "doc2", + value: {title: "foo0"}, + timestamp: timestamps[3] + }, + { + doc: {}, + id: "doc", + value: {title: "bar0"}, + timestamp: timestamps[2] + }, + { + doc: {}, + id: "doc", + value: {title: "bar0"}, + timestamp: timestamps[1] + }, + { + doc: {}, + id: "doc", + value: {title: "bar"}, + timestamp: timestamps[0] + } + ], + "allDocs with include_revisions true should return all revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); }(jIO, RSVP, Blob, QUnit)); \ No newline at end of file -- 2.30.9 From 923a85d2d73c6ad6ecab30148e2471995ef76190 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Thu, 28 Jun 2018 10:10:37 +0000 Subject: [PATCH 37/46] Added revision_history property on historystorage initialization to allow any ids and metadata properties to be set without conflicts 'under the hood'. --- src/jio.storage/historystorage.js | 82 ++++---- test/jio.storage/historystorage.tests.js | 248 +++++++++++++++++------ 2 files changed, 226 insertions(+), 104 deletions(-) diff --git a/src/jio.storage/historystorage.js b/src/jio.storage/historystorage.js index 1970d95..6db51cd 100644 --- a/src/jio.storage/historystorage.js +++ b/src/jio.storage/historystorage.js @@ -15,13 +15,6 @@ return timestamp + "-" + uuid; } - function isTimestamp(id) { - //A timestamp is of the form - //"[13 digit number]-[4 numbers/lowercase letters]" - var re = /^[0-9]{13}-[a-z0-9]{4}$/; - return re.test(id); - } - function throwCantFindError(id) { throw new jIO.util.jIOError( "HistoryStorage: cannot find object '" + id + "'", @@ -44,24 +37,27 @@ */ function HistoryStorage(spec) { this._sub_storage = jIO.createJIO(spec.sub_storage); - this._include_revisions = spec.include_revisions; + if (spec.hasOwnProperty("include_revisions")) { + this._include_revisions = spec.include_revisions; + } else { + this._include_revisions = false; + } } HistoryStorage.prototype.get = function (id_in) { - if (isTimestamp(id_in)) { - + if (this._include_revisions) { // Try to treat id_in as a timestamp instead of a name return this._sub_storage.get(id_in) .push(function (result) { if (result.op === "put") { return result.doc; } - throwCantFindError(id_in); + throwRemovedError(id_in); }, function (error) { if (error.status_code === 404 && error instanceof jIO.util.jIOError) { - throwRemovedError(id_in); + throwCantFindError(id_in); } throw error; }); @@ -92,7 +88,7 @@ }; return substorage.allDocs(options) .push(function (results) { - if (results.data.rows.length > 0) { + if (results.data.total_rows > 0) { if (results.data.rows[0].value.op === "put") { return substorage.get(results.data.rows[0].id) .push(function (result) { @@ -107,12 +103,6 @@ HistoryStorage.prototype.put = function (id, data) { - if (isTimestamp(id)) { - throw new jIO.util.jIOError( - "Document cannot have id of the same form as a timestamp", - 422 - ); - } var timestamp = generateUniqueTimestamp(Date.now()), metadata = { // XXX: remove this attribute once query can sort_on id @@ -145,7 +135,7 @@ query_doc_id, options_remcheck; - if (isTimestamp(id)) { + if (this._include_revisions) { query_doc_id = new SimpleQuery({ operator: "<=", key: "timestamp", @@ -253,7 +243,7 @@ HistoryStorage.prototype.getAttachment = function (id, name) { - if (isTimestamp(id)) { + if (this._include_revisions) { return this._sub_storage.getAttachment(id, name) .push(undefined, function (error) { if (error.status_code === 404 && @@ -304,7 +294,7 @@ }; return substorage.allDocs(options) .push(function (results) { - if (results.data.rows.length > 0) { + if (results.data.total_rows > 0) { if (results.data.rows[0].value.op === "remove" || results.data.rows[0].value.op === "removeAttachment") { throwRemovedError(id); @@ -352,7 +342,7 @@ // Check if query involved _timestamp. // If not, use default behavior and only query on latest revisions - rev_query = options.include_revisions, + rev_query = this._include_revisions, doc_id_name, timestamp_name; @@ -393,13 +383,16 @@ } if (rev_query) { - // Only query on documents which are puts are putAttachments + + // We only query on versions mapping to puts or putAttachments results = results.map(function (docum, ind) { var data_key; if (docum.op === "put") { return docum; } if (docum.op === "putAttachment") { + // putAttachment document does not contain doc metadata, so we + // add it from the most recent non-removed put on same id docum.doc = {}; for (i = ind + 1; i < results.length; i += 1) { if (results[i].doc_id === docum.doc_id) { @@ -411,10 +404,9 @@ } return docum; } + // If most recent edit on document was a remove before this + // attachment, then don't include attachment in query if (results[i].doc_id === "remove") { - //console.log("not returning putAttachment at ", - // docum.timestamp, - // " because it was attached to a removed document"); return false; } } @@ -423,20 +415,26 @@ return false; }); } else { + + // Only query on latest revisions of non-removed documents/attachment + // edits results = results.map(function (docum, ind) { var data_key; if (docum.op === "put") { + // Mark as read and include in query if (!seen.hasOwnProperty(docum.doc_id)) { seen[docum.doc_id] = {}; - //console.log("returning put at ", docum.timestamp, - // " because it is most recent edit to " + docum.doc_id); return docum; } - //console.log("not returning put at ", docum.timestamp, - // " because it was edited later"); - } else if (docum.op === "remove") { + + } else if (docum.op === "remove" || + docum.op === "removeAttachment") { + // Mark as read but do not include in query seen[docum.doc_id] = {}; + } else if (docum.op === "putAttachment") { + // If latest edit, mark as read, add document metadata from most + // recent put, and add to query if (!seen.hasOwnProperty(docum.doc_id)) { seen[docum.doc_id] = {}; docum.doc = {}; @@ -448,41 +446,37 @@ docum.doc[data_key] = results[i].doc[data_key]; } } - /**console.log("returning putAttachment at ", - docum.timestamp, - " because it is most recent edit to attachment " + - docum.name + " of document " + docum.doc_id); - **/ return docum; } if (results[i].doc_id === "remove") { - /**console.log("not returning putAttachment at ", - docum.timestamp, - " because it was attached to a removed document"); - **/ + // If most recent edit on document was a remove before + // this attachment, then don't include attachment in query return false; } } } } - } else if (docum.op === "removeAttachment") { - seen[docum.doc_id] = {}; } return false; }); } + docs_to_query = results .filter(function (docum) { return docum; }) .map(function (docum) { + // Save timestamp and id information for retrieval at the end of + // buildQuery docum.doc[timestamp_name] = docum.timestamp; docum.doc[doc_id_name] = docum.doc_id; return docum.doc; }); - + // Return timestamp and id information from query options.select_list.push(doc_id_name); options.select_list.push(timestamp_name); + + // Sort on timestamp with updated timestamp_name options.sort_on[options.sort_on.length - 1] = [ timestamp_name, "descending" ]; diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index e1b9e7b..2c99746 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -48,6 +48,20 @@ } } }); + this.history = jIO.createJIO({ + type: "history", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + }); this.not_history = jIO.createJIO({ type: "query", sub_storage: { @@ -66,6 +80,7 @@ stop(); expect(10); var jio = this.jio, + history = this.history, not_history = this.not_history, timestamps, blob2 = this.blob2, @@ -120,7 +135,7 @@ blob2, "Return the attachment information with getAttachment" ); - return jio.getAttachment( + return history.getAttachment( timestamps[3], "attacheddata" ); @@ -131,7 +146,7 @@ "Return the attachment information with getAttachment for " + "current revision" ); - return jio.getAttachment( + return history.getAttachment( timestamps[2], "attacheddata" ); @@ -313,7 +328,7 @@ return jio.allDocs(); }) .push(function (results) { - equal(results.data.rows.length, + equal(results.data.total_rows, 2, "Two documents in accessible storage"); return jio.get(results.data.rows[1].id); @@ -373,6 +388,7 @@ stop(); expect(8); var jio = this.jio, + history = this.history, not_history = this.not_history, blob1 = new Blob(['a']), blob11 = new Blob(['ab']), @@ -430,12 +446,12 @@ }, "Current state of document is correct"); - return jio.allAttachments(timestamps[0]); + return history.allAttachments(timestamps[0]); }) .push(function (results) { deepEqual(results, {}, "First version of document has 0 attachments"); - return jio.allAttachments(timestamps[1]); + return history.allAttachments(timestamps[1]); }) .push(function (results) { deepEqual(results, { @@ -443,13 +459,13 @@ data2: blob2 }, "Both attachments are included in allAttachments"); - return jio.allAttachments(timestamps[2]); + return history.allAttachments(timestamps[2]); }) .push(function (results) { deepEqual(results, { data: blob1 }, "Removed attachment does not show up in allAttachments"); - return jio.allAttachments(timestamps[3]); + return history.allAttachments(timestamps[3]); }) .push(function () { ok(false, "This query should have thrown a 404 error"); @@ -465,7 +481,7 @@ "Error is handled by Historystorage."); }) .push(function () { - return jio.allAttachments(timestamps[4]); + return history.allAttachments(timestamps[4]); }) .push(function (results) { deepEqual(results, { @@ -501,6 +517,20 @@ } } }); + this.history = jIO.createJIO({ + type: "history", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + }); this.not_history = jIO.createJIO({ type: "query", sub_storage: { @@ -519,17 +549,60 @@ stop(); expect(2); var jio = this.jio, - BADINPUT_ERRCODE = 422; + history = this.history, + timestamp; + + jio.put("doc", {title: "foo"}) + .push(function () { + return history.allDocs(); + }) + .push(function (res) { + timestamp = res.data.rows[0].timestamp; + return history.put(timestamp, {key: "val"}); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (result) { + deepEqual(result, { + title: "foo" + }, "Saving document with timestamp id does not cause issues (1)"); + return history.get(timestamp); + }) + .push(function (result) { + deepEqual(result, { + title: "foo" + }, "Saving document with timestamp id does not cause issues (2)"); + return history.get(timestamp); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - jio.put("1234567891123-ab7d", {}) + test("Getting a non-existent document", + function () { + stop(); + expect(3); + var jio = this.jio; + jio.put("not_doc", {}) + .push(function () { + return jio.get("doc"); + }) .push(function () { ok(false, "This statement should not be reached"); }, function (error) { + //console.log(error); ok(error instanceof jIO.util.jIOError, "Correct type of error"); deepEqual(error.status_code, - BADINPUT_ERRCODE, - "Can't save a document with a timestamp-formatted id" + 404, + "Correct status code for getting a non-existent document" ); + deepEqual(error.message, + "HistoryStorage: cannot find object 'doc'", + "Error is handled by history storage before reaching console"); }) .fail(function (error) { //console.log(error); @@ -538,11 +611,13 @@ .always(function () {start(); }); }); - test("Getting a non-existent document", + test("Getting a document with timestamp when include_revisions is false", function () { stop(); - expect(3); - var jio = this.jio; + expect(9); + var jio = this.jio, + history = this.history, + timestamp; jio.put("not_doc", {}) .push(function () { return jio.get("doc"); @@ -560,6 +635,41 @@ "HistoryStorage: cannot find object 'doc'", "Error is handled by history storage before reaching console"); }) + .push(function () { + return history.allDocs(); + }) + .push(function (results) { + timestamp = results.data.rows[0].timestamp; + return jio.get(timestamp); + }) + .push(function () { + ok(false, "This statement should not be reached"); + }, function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Correct status code for getting a non-existent document" + ); + deepEqual(error.message, + "HistoryStorage: cannot find object '" + timestamp + "'", + "Error is handled by history storage before reaching console"); + }) + .push(function () { + return history.get("doc"); + }) + .push(function () { + ok(false, "This statement should not be reached"); + }, function (error) { + //console.log(error); + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Correct status code for getting a non-existent document" + ); + deepEqual(error.message, + "HistoryStorage: cannot find object 'doc'", + "Error is handled by history storage before reaching console"); + }) .fail(function (error) { //console.log(error); ok(false, error); @@ -570,8 +680,9 @@ test("Creating a document with put and retrieving it with get", function () { stop(); - expect(7); + expect(5); var jio = this.jio, + history = this.history, not_history = this.not_history, timestamps; jio.put("doc", {title: "version0"}) @@ -590,7 +701,7 @@ 1, "One revision is saved in storage" ); - return jio.get(timestamps[0]); + return history.get(timestamps[0]); }) .push(function (result) { deepEqual(result, { @@ -623,18 +734,6 @@ "Can't access non-existent document" ); }) - .push(function () { - return jio.get("1234567891123-abcd"); - }) - .push(function () { - ok(false, "Trying to get a non-existent id should have raised 404"); - }, function (error) { - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - deepEqual(error.status_code, - 404, - "Can't access document by getting with non-existent id" - ); - }) .fail(function (error) { //console.log(error); ok(false, error); @@ -647,6 +746,7 @@ stop(); expect(7); var jio = this.jio, + history = this.history, not_history = this.not_history, timestamps; @@ -682,7 +782,7 @@ title: "t3", subtitle: "s3" }, "Get returns latest revision"); - return jio.get(timestamps[0]); + return history.get(timestamps[0]); }, function (err) { ok(false, err); }) @@ -691,14 +791,14 @@ title: "t0", subtitle: "s0" }, "Get returns first version"); - return jio.get(timestamps[1]); + return history.get(timestamps[1]); }) .push(function (result) { deepEqual(result, { title: "t1", subtitle: "s1" }, "Get returns second version"); - return jio.get(timestamps[2]); + return history.get(timestamps[2]); }, function (err) { ok(false, err); }) @@ -707,20 +807,20 @@ title: "t2", subtitle: "s2" }, "Get returns third version"); - return jio.get(timestamps[3]); + return history.get(timestamps[3]); }, function (err) { ok(false, err); }) .push(function () { ok(false, "This should have thrown a 404 error"); - return jio.get(timestamps[4]); + return history.get(timestamps[4]); }, function (error) { ok(error instanceof jIO.util.jIOError, "Correct type of error"); deepEqual(error.status_code, 404, "Error if you try to go back more revisions than what exists"); - return jio.get(timestamps[4]); + return history.get(timestamps[4]); }) .push(function (result) { deepEqual(result, { @@ -767,7 +867,7 @@ }); }) .push(function (results) { - equal(results.data.rows.length, + equal(results.data.total_rows, 9, "All nine versions exist in storage"); return not_history.get(results.data.rows[0].id); @@ -801,7 +901,7 @@ }); }) .push(function (results) { - equal(results.data.rows.length, + equal(results.data.total_rows, 10, "Remove operation is recorded"); return not_history.get(results.data.rows[0].id); @@ -841,6 +941,20 @@ } } }); + this.history = jIO.createJIO({ + type: "history", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + }); this.not_history = jIO.createJIO({ type: "query", sub_storage: { @@ -889,7 +1003,7 @@ return results[0]; }) .push(function (results) { - equal(results.data.rows.length, + equal(results.data.total_rows, 1, "Exactly one result returned"); deepEqual(results.data.rows[0], { @@ -903,7 +1017,7 @@ return not_history.allDocs(); }) .push(function (results) { - equal(results.data.rows.length, + equal(results.data.total_rows, 1, "Exactly one result returned"); return not_history.get(results.data.rows[0].id); @@ -989,6 +1103,7 @@ stop(); expect(10); var jio = this.jio, + history = this.history, not_history = this.not_history, timestamps; jio.put("doc", { @@ -1055,7 +1170,7 @@ return results[0]; }) .push(function (results) { - equal(results.data.rows.length, + equal(results.data.total_rows, 1, "Exactly one result returned"); deepEqual(results.data.rows[0], { @@ -1071,14 +1186,13 @@ ); }) .push(function () { - return jio.allDocs({ + return history.allDocs({ query: "", - select_list: ["title", "subtitle"], - include_revisions: true + select_list: ["title", "subtitle"] }); }) .push(function (results) { - equal(results.data.rows.length, + equal(results.data.total_rows, 3, "Querying with include_revisions retrieves all versions"); deepEqual(results.data.rows, [ @@ -1214,7 +1328,7 @@ return jio.allDocs({sort_on: [["timestamp", "descending"]]}); }) .push(function (results) { - equal(results.data.rows.length, + equal(results.data.total_rows, 2, "Only two non-removed unique documents exist." ); @@ -1308,7 +1422,7 @@ }); }) .push(function (results) { - equal(results.data.rows.length, 3); + equal(results.data.total_rows, 3); deepEqual(results.data.rows, [ { doc: {}, @@ -1347,6 +1461,7 @@ stop(); expect(3); var jio = this.jio, + history = this.history, not_history = this.not_history, timestamps, docs = [ @@ -1540,12 +1655,11 @@ ], "All versions of documents are stored correctly"); }) .push(function () { - return jio.allDocs({ + return history.allDocs({ query: "NOT (date: >= 2 AND date: <= 3)", select_list: ["date", "non-existent-key", "type", "title"], sort_on: [["date", "descending"] - ], - include_revisions: true + ] }); }) .push(function (results) { @@ -1636,6 +1750,7 @@ this.blob2 = new Blob(['b']); this.blob3 = new Blob(['ccc']); this.other_blob = new Blob(['1']); + this.jio = jIO.createJIO({ type: "history", sub_storage: { @@ -1649,6 +1764,20 @@ } } }); + this.history = jIO.createJIO({ + type: "history", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + }); this.not_history = jIO.createJIO({ type: "query", sub_storage: { @@ -1667,6 +1796,7 @@ stop(); expect(1); var jio = this.jio, + history = this.history, not_history = this.not_history, timestamps, blobs1 = [ @@ -1716,9 +1846,8 @@ }) .push(function () { - return jio.allDocs({ - select_list: ["title"], - include_revisions: true + return history.allDocs({ + select_list: ["title"] }); }) .push(function (results) { @@ -1824,6 +1953,7 @@ expect(1); var jio = this.jio, not_history = this.not_history, + history = this.history, timestamps, blobs1 = [ new Blob(['a']), @@ -1859,9 +1989,8 @@ }) .push(function () { - return jio.allDocs({ - select_list: ["title"], - include_revisions: true + return history.allDocs({ + select_list: ["title"] }); }) .push(function (results) { @@ -1913,6 +2042,7 @@ expect(2); var jio = this.jio, not_history = this.not_history, + history = this.history, timestamps, blobs1 = [ new Blob(['a']), @@ -1948,8 +2078,7 @@ }) .push(function () { return jio.allDocs({ - select_list: ["title"], - include_revisions: false + select_list: ["title"] }); }) .push(function (results) { @@ -1970,9 +2099,8 @@ "allDocs with include_revisions false should return all revisions"); }) .push(function () { - return jio.allDocs({ - select_list: ["title"], - include_revisions: true + return history.allDocs({ + select_list: ["title"] }); }) .push(function (results) { -- 2.30.9 From 9bf585e501e300e7c3ddd4b6338a68b8e5e788a1 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Fri, 29 Jun 2018 13:57:30 +0000 Subject: [PATCH 38/46] Added additional tests for edgecase behavior. --- src/jio.storage/historystorage.js | 203 +++++------ test/jio.storage/historystorage.tests.js | 412 ++++++++++++++++++++++- 2 files changed, 508 insertions(+), 107 deletions(-) diff --git a/src/jio.storage/historystorage.js b/src/jio.storage/historystorage.js index 6db51cd..b16455c 100644 --- a/src/jio.storage/historystorage.js +++ b/src/jio.storage/historystorage.js @@ -46,46 +46,44 @@ HistoryStorage.prototype.get = function (id_in) { + // Query to get the last edit made to this document + var substorage = this._sub_storage, + doc_id_query, + metadata_query, + options; + if (this._include_revisions) { - // Try to treat id_in as a timestamp instead of a name - return this._sub_storage.get(id_in) - .push(function (result) { - if (result.op === "put") { - return result.doc; - } - throwRemovedError(id_in); - }, function (error) { - if (error.status_code === 404 && - error instanceof jIO.util.jIOError) { - throwCantFindError(id_in); - } - throw error; - }); + doc_id_query = new SimpleQuery({ + operator: "<=", + key: "timestamp", + value: id_in + }); + } else { + doc_id_query = new SimpleQuery({key: "doc_id", value: id_in}); } - // Query to get the last edit made to this document - var substorage = this._sub_storage, + // Include id_in as value in query object for safety + metadata_query = new ComplexQuery({ + operator: "AND", + query_list: [ + doc_id_query, + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "remove"}), + new SimpleQuery({key: "op", value: "put"}) + ] + }) + ] + }); + options = { + query: metadata_query, + select_list: ["op"], + limit: [0, 1], + sort_on: [["timestamp", "descending"]] + }; + - // Include id_in as value in query object for safety - metadata_query = new ComplexQuery({ - operator: "AND", - query_list: [ - new SimpleQuery({key: "doc_id", value: id_in}), - new ComplexQuery({ - operator: "OR", - query_list: [ - new SimpleQuery({key: "op", value: "remove"}), - new SimpleQuery({key: "op", value: "put"}) - ] - }) - ] - }), - options = { - query: metadata_query, - select_list: ["op"], - limit: [0, 1], - sort_on: [["timestamp", "descending"]] - }; return substorage.allDocs(options) .push(function (results) { if (results.data.total_rows > 0) { @@ -102,7 +100,6 @@ }; HistoryStorage.prototype.put = function (id, data) { - var timestamp = generateUniqueTimestamp(Date.now()), metadata = { // XXX: remove this attribute once query can sort_on id @@ -133,9 +130,11 @@ query_removed_check, options, query_doc_id, - options_remcheck; + options_remcheck, + include_revs = this._include_revisions, + have_seen_id = false; - if (this._include_revisions) { + if (include_revs) { query_doc_id = new SimpleQuery({ operator: "<=", key: "timestamp", @@ -143,6 +142,7 @@ }); } else { query_doc_id = new SimpleQuery({key: "doc_id", value: id}); + have_seen_id = true; } query_removed_check = new ComplexQuery({ @@ -177,7 +177,7 @@ options_remcheck = { query: query_removed_check, select_list: ["op", "timestamp"], - //limit: [0, 1], + limit: [0, 1], sort_on: [["timestamp", "descending"]] }; options = { @@ -187,8 +187,12 @@ }; return this._sub_storage.allDocs(options_remcheck) + // Check the document exists and is not removed .push(function (results) { if (results.data.total_rows > 0) { + if (results.data.rows[0].id === id) { + have_seen_id = true; + } if (results.data.rows[0].value.op === "remove") { throwRemovedError(id); } @@ -205,6 +209,18 @@ attachment_promises = [], ind, entry; + + // If input mapped to a real timestamp, then the first query result must + // have the inputted id. Otherwise, unexpected results could arise + // by inputting nonsensical strings as id when include_revisions = true + if (include_revs && + results.data.total_rows > 0 && + results.data.rows[0].id !== id && + !have_seen_id) { + throwCantFindError(id); + } + + // Only return attachments if: // (it is the most recent revision) AND (it is a putAttachment) attachments = results.data.rows.filter(function (docum) { @@ -257,7 +273,6 @@ // Query to get the last edit made to this document var substorage = this._sub_storage, - // Include id_in as value in query object for safety // "doc_id: id AND // (op: remove OR ((op: putAttachment OR op: removeAttachment) AND // name: name))" @@ -295,6 +310,7 @@ return substorage.allDocs(options) .push(function (results) { if (results.data.total_rows > 0) { + // XXX: issue if attachments are put on a removed document if (results.data.rows[0].value.op === "remove" || results.data.rows[0].value.op === "removeAttachment") { throwRemovedError(id); @@ -324,8 +340,6 @@ }; HistoryStorage.prototype.buildQuery = function (options) { - // XXX: if include_revisions, we should also include the document results - // for different edits of attachments // Set default values if (options === undefined) {options = {}; } if (options.query === undefined) {options.query = ""; } @@ -338,76 +352,68 @@ options.query = jIO.QueryFactory.create(options.query); var meta_options, - substorage = this._sub_storage, - - // Check if query involved _timestamp. - // If not, use default behavior and only query on latest revisions - rev_query = this._include_revisions, + include_revs = this._include_revisions, doc_id_name, timestamp_name; - // Query for all edits putting or removing documents (and nothing about - // attachments) + // Query for all edits meta_options = { - query: "",//(op: remove) OR (op: put)", - sort_on: options.sort_on + query: "", + sort_on: options.sort_on, + select_list: ["doc", "op", "doc_id"] }; return this._sub_storage.allDocs(meta_options) - - // Get all documents found in query - // XXX: Once include_docs is implemented, this step can be simplified - .push(function (results) { - var promises = results.data.rows.map(function (data) { - return substorage.get(data.id); - }); - return RSVP.all(promises); - }) .push(function (results) { + results = results.data.rows; var seen = {}, query_matches, docs_to_query, i; - doc_id_name = "_doc_id"; timestamp_name = "_timestamp"; for (i = 0; i < results.length; i += 1) { - if (results[i].op === "put") { - while (results[i].doc.hasOwnProperty(doc_id_name)) { + if (results[i].value.op === "put") { + while (results[i].value.doc.hasOwnProperty(doc_id_name)) { doc_id_name = "_" + doc_id_name; } - while (results[i].doc.hasOwnProperty(timestamp_name)) { + while (results[i].value.doc.hasOwnProperty(timestamp_name)) { timestamp_name = "_" + timestamp_name; } } } - if (rev_query) { + if (include_revs) { // We only query on versions mapping to puts or putAttachments results = results.map(function (docum, ind) { var data_key; - if (docum.op === "put") { + if (docum.value.op === "put") { + return docum; + } + if (docum.value.op === "remove") { + docum.value.doc = {}; return docum; } - if (docum.op === "putAttachment") { + if (docum.value.op === "putAttachment" || + docum.value.op === "removeAttachment") { + // putAttachment document does not contain doc metadata, so we // add it from the most recent non-removed put on same id - docum.doc = {}; + docum.value.doc = {}; for (i = ind + 1; i < results.length; i += 1) { - if (results[i].doc_id === docum.doc_id) { - if (results[i].op === "put") { - for (data_key in results[i].doc) { - if (results[i].doc.hasOwnProperty(data_key)) { - docum.doc[data_key] = results[i].doc[data_key]; + if (results[i].value.doc_id === docum.value.doc_id) { + if (results[i].value.op === "put") { + for (data_key in results[i].value.doc) { + if (results[i].value.doc.hasOwnProperty(data_key)) { + docum.value.doc[data_key] = + results[i].value.doc[data_key]; } } return docum; } - // If most recent edit on document was a remove before this - // attachment, then don't include attachment in query - if (results[i].doc_id === "remove") { - return false; + if (results[i].value.op === "remove") { + return docum; } } } @@ -420,35 +426,36 @@ // edits results = results.map(function (docum, ind) { var data_key; - if (docum.op === "put") { + if (docum.value.op === "put") { // Mark as read and include in query - if (!seen.hasOwnProperty(docum.doc_id)) { - seen[docum.doc_id] = {}; + if (!seen.hasOwnProperty(docum.value.doc_id)) { + seen[docum.value.doc_id] = {}; return docum; } - } else if (docum.op === "remove" || - docum.op === "removeAttachment") { + } else if (docum.value.op === "remove" || + docum.value.op === "removeAttachment") { // Mark as read but do not include in query - seen[docum.doc_id] = {}; + seen[docum.value.doc_id] = {}; - } else if (docum.op === "putAttachment") { + } else if (docum.value.op === "putAttachment") { // If latest edit, mark as read, add document metadata from most // recent put, and add to query - if (!seen.hasOwnProperty(docum.doc_id)) { - seen[docum.doc_id] = {}; - docum.doc = {}; + if (!seen.hasOwnProperty(docum.value.doc_id)) { + seen[docum.value.doc_id] = {}; + docum.value.doc = {}; for (i = ind + 1; i < results.length; i += 1) { - if (results[i].doc_id === docum.doc_id) { - if (results[i].op === "put") { - for (data_key in results[i].doc) { - if (results[i].doc.hasOwnProperty(data_key)) { - docum.doc[data_key] = results[i].doc[data_key]; + if (results[i].value.doc_id === docum.value.doc_id) { + if (results[i].value.op === "put") { + for (data_key in results[i].value.doc) { + if (results[i].value.doc.hasOwnProperty(data_key)) { + docum.value.doc[data_key] = + results[i].value.doc[data_key]; } } return docum; } - if (results[i].doc_id === "remove") { + if (results[i].value.doc_id === "remove") { // If most recent edit on document was a remove before // this attachment, then don't include attachment in query return false; @@ -462,16 +469,18 @@ } docs_to_query = results + // Filter out all docs flagged as false in previous map call .filter(function (docum) { return docum; }) .map(function (docum) { // Save timestamp and id information for retrieval at the end of // buildQuery - docum.doc[timestamp_name] = docum.timestamp; - docum.doc[doc_id_name] = docum.doc_id; - return docum.doc; + docum.value.doc[timestamp_name] = docum.id; + docum.value.doc[doc_id_name] = docum.value.doc_id; + return docum.value.doc; }); + // Return timestamp and id information from query options.select_list.push(doc_id_name); options.select_list.push(timestamp_name); diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index 2c99746..5b0be13 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -202,6 +202,41 @@ .always(function () {start(); }); }); + test("get attachment immediately after removing it", + function () { + stop(); + expect(3); + var jio = this.jio, + blob1 = this.blob1; + + jio.put("doc", {title: "foo0"}) + .push(function () { + return jio.putAttachment("doc", "attacheddata", blob1); + }) + .push(function () { + return jio.removeAttachment("doc", "attacheddata"); + }) + .push(function () { + return jio.getAttachment("doc", "attacheddata"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "throws a jio error"); + deepEqual(error.status_code, + 404, + "allAttachments of a removed document throws a 404 error"); + deepEqual(error.message, + "HistoryStorage: cannot find object 'doc' (removed)", + "Error is handled by Historystorage."); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); test("Ordering of put and remove attachments is correct", function () { @@ -386,7 +421,7 @@ test("Correctness of allAttachments method on older revisions", function () { stop(); - expect(8); + expect(11); var jio = this.jio, history = this.history, not_history = this.not_history, @@ -488,6 +523,21 @@ data: blob11 }); }) + .push(function () { + return history.allAttachments("not-a-timestamp-or-doc_id"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "throws a jio error"); + deepEqual(error.status_code, + 404, + "allAttachments of a removed document throws a 404 error"); + deepEqual(error.message, + "HistoryStorage: cannot find object 'not-a-timestamp-or-doc_id'", + "Error is handled by Historystorage."); + }) .fail(function (error) { //console.log(error); ok(false, error); @@ -496,6 +546,7 @@ }); + ///////////////////////////////////////////////////////////////// // Querying older revisions ///////////////////////////////////////////////////////////////// @@ -544,6 +595,118 @@ } }); + test("Removing documents before putting them", + function () { + stop(); + expect(4); + var jio = this.jio, + not_history = this.not_history, + timestamps; + + jio.remove("doc") + .push(function () { + return jio.put("doc2", {title: "foo"}); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function () { + ok(false, "This statement should not be reached"); + }, function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Correct status code for getting a non-existent document" + ); + deepEqual(error.message, + "HistoryStorage: cannot find object 'doc' (removed)", + "Error is handled by history storage before reaching console"); + }) + .push(function () { + return not_history.allDocs({ + select_list: ["timestamp"], + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) + .push(function () { + return jio.allDocs({select_list: ["title"]}); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: "doc2", + value: {title: "foo"}, + doc: {}, + timestamp: timestamps[1] + }], "DOcument that was removed before being put is not retrieved"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + + test("Removing documents and then putting them", + function () { + stop(); + expect(2); + var jio = this.jio, + history = this.history, + timestamps; + + jio.remove("doc") + .push(function () { + return jio.put("doc", {title: "foo"}); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (result) { + deepEqual(result, { + title: "foo" + }, "A put was the most recent edit on 'doc'"); + }) + .push(function () { + return history.allDocs({ + select_list: ["timestamp"] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.timestamp; + }); + }) + .push(function () { + return history.allDocs({select_list: ["title"]}); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: "doc", + value: {title: "foo"}, + doc: {}, + timestamp: timestamps[0] + }, + { + id: "doc", + value: {}, + doc: {}, + timestamp: timestamps[1] + }], "DOcument that was removed before being put is not retrieved"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + test("Handling bad input", function () { stop(); @@ -614,7 +777,7 @@ test("Getting a document with timestamp when include_revisions is false", function () { stop(); - expect(9); + expect(6); var jio = this.jio, history = this.history, timestamp; @@ -654,10 +817,13 @@ "HistoryStorage: cannot find object '" + timestamp + "'", "Error is handled by history storage before reaching console"); }) + /** + * XXX: I don't think this test is necessary .push(function () { return history.get("doc"); }) - .push(function () { + .push(function (res) { + console.log(res); ok(false, "This statement should not be reached"); }, function (error) { //console.log(error); @@ -670,6 +836,8 @@ "HistoryStorage: cannot find object 'doc'", "Error is handled by history storage before reaching console"); }) + **/ + .fail(function (error) { //console.log(error); ok(false, error); @@ -920,6 +1088,52 @@ .always(function () {start(); }); }); + test("Getting after attachments have been put", + function () { + stop(); + expect(4); + var jio = this.jio, + history = this.history, + blob = new Blob(['a']), + edit_log; + + jio.put("doc", {"title": "foo0"}) + .push(function () { + return jio.putAttachment("doc", "attachment", blob); + }) + .push(function () { + return jio.removeAttachment("doc", "attachment", blob); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (res) { + deepEqual(res, {title: "foo0"}); + return history.allDocs({select_list: ["title"]}); + }) + .push(function (results) { + edit_log = results.data.rows; + return history.get(edit_log[0].timestamp); + }) + .push(function (result) { + deepEqual(result, {title: "foo0"}); + return history.get(edit_log[1].timestamp); + }) + .push(function (result) { + deepEqual(result, {title: "foo0"}); + return history.get(edit_log[2].timestamp); + }) + .push(function (result) { + deepEqual(result, {title: "foo0"}); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + + ///////////////////////////////////////////////////////////////// // Querying older revisions ///////////////////////////////////////////////////////////////// @@ -1656,10 +1870,10 @@ }) .push(function () { return history.allDocs({ - query: "NOT (date: >= 2 AND date: <= 3)", + query: "NOT (date: >= 2 AND date: <= 3) AND " + + "(date: = 1 OR date: = 4)", select_list: ["date", "non-existent-key", "type", "title"], - sort_on: [["date", "descending"] - ] + sort_on: [["date", "descending"]] }); }) .push(function (results) { @@ -1724,16 +1938,194 @@ type: "foo" }, timestamp: timestamps[1] + } + ], + "Query gives correct results in correct order"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + + test( + "allDocs with include_revisions with an attachment on a removed document", + function () { + stop(); + expect(1); + var jio = this.jio, + history = this.history, + not_history = this.not_history, + timestamps, + blob = new Blob(['a']); + + jio.put("document", {title: "foo"}) + .push(function () { + return jio.remove("document"); + }) + .push(function () { + return jio.putAttachment("document", "attachment", blob); + }) + + // Get timestamps + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return history.allDocs({select_list: ["title"]}); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: "document", + doc: {}, + value: {}, + timestamp: timestamps[2] }, { + id: "document", doc: {}, - id: "doc", value: {}, + timestamp: timestamps[1] + }, + { + id: "document", + doc: {}, + value: {title: "foo"}, timestamp: timestamps[0] - } - ], - "Query gives correct results in correct order"); + }], + "Attachment on removed document is handled correctly" + ); + return not_history.allDocs({select_list: ["doc"]}); + }) + + .fail(function (error) { + //console.log(error); + ok(false, error); }) + .always(function () {start(); }); + } + ); + + test("allDocs with include_revisions with a removed attachment", + function () { + stop(); + expect(2); + var jio = this.jio, + history = this.history, + not_history = this.not_history, + timestamps, + blob = new Blob(['a']); + + jio.put("document", {title: "foo"}) + .push(function () { + return jio.putAttachment("document", "attachment", blob); + }) + .push(function () { + return jio.removeAttachment("document", "attachment"); + }) + + // Get timestamps + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + + .push(function () { + return history.allDocs({select_list: ["title"]}); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: "document", + doc: {}, + value: {title: "foo"}, + timestamp: timestamps[2] + }, + { + id: "document", + doc: {}, + value: {title: "foo"}, + timestamp: timestamps[1] + }, + { + id: "document", + doc: {}, + value: {title: "foo"}, + timestamp: timestamps[0] + }], + "Attachment on removed document is handled correctly" + ); + }) + .push(function () { + return jio.allAttachments("document"); + }) + .push(function (results) { + deepEqual(results, {}, "No non-removed attachments"); + }) + + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + + test("Parallel edits will not break anything", + function () { + stop(); + expect(2); + var jio = this.jio, + history = this.history, + blob1 = new Blob(['ab']), + blob2 = new Blob(['abc']), + blob3 = new Blob(['abcd']); + + jio.put("doc", {k: "v0"}) + .push(function () { + return RSVP.all([ + jio.put("doc", {k: "v"}), + jio.putAttachment("doc", "data", blob1), + jio.putAttachment("doc", "data2", blob2), + jio.putAttachment("doc", "data", blob3), + jio.removeAttachment("doc", "data"), + jio.removeAttachment("doc", "data2"), + jio.remove("doc"), + jio.remove("doc"), + jio.put("doc", {k: "v"}), + jio.put("doc", {k: "v"}), + jio.put("doc2", {k: "foo"}), + jio.remove("doc"), + jio.remove("doc") + ]); + }) + + .push(function () { + ok(true, "No errors thrown."); + return history.allDocs(); + }) + .push(function (results) { + var res = results.data.rows; + equal(res.length, + 14, + "All edits are recorded regardless of ordering"); + return jio.allDocs(); + }) + .fail(function (error) { //console.log(error); ok(false, error); -- 2.30.9 From 8694263303b5eca896dd354364a684b69f598867 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Fri, 6 Jul 2018 09:03:27 +0000 Subject: [PATCH 39/46] Fixed allDocs implementation to rely more on QueryStorage's allDocs. --- src/jio.storage/historystorage.js | 89 +-- test/jio.storage/historystorage.tests.js | 680 ++++++++++++----------- 2 files changed, 393 insertions(+), 376 deletions(-) diff --git a/src/jio.storage/historystorage.js b/src/jio.storage/historystorage.js index b16455c..f071675 100644 --- a/src/jio.storage/historystorage.js +++ b/src/jio.storage/historystorage.js @@ -236,7 +236,6 @@ attachment_promises[entry.value.name] = substorage.getAttachment(entry.id, entry.value.name); } - return RSVP.hash(attachment_promises); }); }; @@ -256,7 +255,6 @@ return substorage.putAttachment(timestamp, name, blob); }); }; - HistoryStorage.prototype.getAttachment = function (id, name) { if (this._include_revisions) { @@ -335,10 +333,11 @@ HistoryStorage.prototype.repair = function () { return this._sub_storage.repair.apply(this._sub_storage, arguments); }; - HistoryStorage.prototype.hasCapacity = function () { - return this._sub_storage.hasCapacity.apply(this._sub_storage, arguments); + HistoryStorage.prototype.hasCapacity = function (name) { + return name === 'list' || name === 'include'; }; + HistoryStorage.prototype.buildQuery = function (options) { // Set default values if (options === undefined) {options = {}; } @@ -348,41 +347,22 @@ if (options.include_revisions === undefined) { options.include_revisions = false; } - options.sort_on.push(["timestamp", "descending"]); options.query = jIO.QueryFactory.create(options.query); - var meta_options, - include_revs = this._include_revisions, - doc_id_name, - timestamp_name; - - // Query for all edits - meta_options = { + var meta_options = { query: "", - sort_on: options.sort_on, + sort_on: [["timestamp", "descending"]], select_list: ["doc", "op", "doc_id"] - }; + }, + include_revs = this._include_revisions; + return this._sub_storage.allDocs(meta_options) .push(function (results) { results = results.data.rows; var seen = {}, - query_matches, docs_to_query, i; - doc_id_name = "_doc_id"; - timestamp_name = "_timestamp"; - for (i = 0; i < results.length; i += 1) { - if (results[i].value.op === "put") { - while (results[i].value.doc.hasOwnProperty(doc_id_name)) { - doc_id_name = "_" + doc_id_name; - } - while (results[i].value.doc.hasOwnProperty(timestamp_name)) { - timestamp_name = "_" + timestamp_name; - } - } - } - if (include_revs) { // We only query on versions mapping to puts or putAttachments @@ -412,6 +392,8 @@ } return docum; } + // If most recent metadata edit before the attachment edit + // was a remove, then leave doc empty if (results[i].value.op === "remove") { return docum; } @@ -455,7 +437,7 @@ } return docum; } - if (results[i].value.doc_id === "remove") { + if (results[i].value.op === "remove") { // If most recent edit on document was a remove before // this attachment, then don't include attachment in query return false; @@ -467,45 +449,30 @@ return false; }); } - docs_to_query = results + // Filter out all docs flagged as false in previous map call .filter(function (docum) { return docum; }) - .map(function (docum) { - // Save timestamp and id information for retrieval at the end of - // buildQuery - docum.value.doc[timestamp_name] = docum.id; - docum.value.doc[doc_id_name] = docum.value.doc_id; - return docum.value.doc; - }); - // Return timestamp and id information from query - options.select_list.push(doc_id_name); - options.select_list.push(timestamp_name); + // Put into correct format to be passed back to query storage + .map(function (docum) { + docum.doc = docum.value.doc; + docum.id = docum.value.doc_id; + delete docum.value.doc_id; + delete docum.value.op; + + if (options.include_docs) { + docum.doc = docum.value.doc; + } else { + docum.doc = {}; + } - // Sort on timestamp with updated timestamp_name - options.sort_on[options.sort_on.length - 1] = [ - timestamp_name, "descending" - ]; - query_matches = options.query.exec(docs_to_query, options); - return query_matches; - }) - // Format the results of the query, and return - .push(function (query_matches) { - return query_matches.map(function (docum) { - var doc_id = docum[doc_id_name], - time = docum[timestamp_name]; - delete docum[timestamp_name]; - delete docum[doc_id_name]; - return { - doc: {}, - value: docum, - id: doc_id, - timestamp: time - }; - }); + docum.value = {}; + return docum; + }); + return docs_to_query; }); }; diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index 5b0be13..4b88037 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -22,6 +22,115 @@ }); } + module("HistoryStorage.post", { + setup: function () { + // create storage of type "history" with memory as substorage + var dbname = "db_" + Date.now(); + this.jio = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "history", + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + } + }); + this.history = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "history", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + } + }); + this.not_history = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + }); + } + }); + + test("Verifying simple post works", + function () { + stop(); + expect(2); + var jio = this.jio, + history = this.history, + not_history = this.not_history, + //timestamps, + id; + + return jio.post({title: "foo0"}) + .push(function (result) { + id = result; + return jio.put(result, {title: "foo1"}); + }) + .push(function (result) { + return jio.get(result); + }) + .push(function (res) { + deepEqual(res, { + title: "foo1" + }, "history storage only retrieves latest version"); + }) + .push(function () { + return not_history.allDocs({ + select_list: ["timestamp"] + }); + }) + .push(function () { + return history.allDocs({select_list: ["title"]}); + }) + .push(function (res) { + deepEqual(res.data.rows, [ + { + value: { + title: "foo1" + }, + doc: {}, + //timestamp: timestamps[1], + id: id + }, + { + value: { + title: "foo0" + }, + doc: {}, + //timestamp: timestamps[0], + id: id + } + ], + "Two revisions logged with correct metadata"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + ///////////////////////////////////////////////////////////////// // Attachments @@ -36,28 +145,34 @@ this.blob3 = new Blob(['ccc']); this.other_blob = new Blob(['1']); this.jio = jIO.createJIO({ - type: "history", + type: "query", sub_storage: { - type: "query", + type: "history", sub_storage: { - type: "uuid", + type: "query", sub_storage: { - type: "indexeddb", - database: dbname + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } } } } }); this.history = jIO.createJIO({ - type: "history", - include_revisions: true, + type: "query", sub_storage: { - type: "query", + type: "history", + include_revisions: true, sub_storage: { - type: "uuid", + type: "query", sub_storage: { - type: "indexeddb", - database: dbname + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } } } } @@ -556,28 +671,34 @@ // create storage of type "history" with memory as substorage var dbname = "db_" + Date.now(); this.jio = jIO.createJIO({ - type: "history", + type: "query", sub_storage: { - type: "query", + type: "history", sub_storage: { - type: "uuid", + type: "query", sub_storage: { - type: "indexeddb", - database: dbname + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } } } } }); this.history = jIO.createJIO({ - type: "history", - include_revisions: true, + type: "query", sub_storage: { - type: "query", + type: "history", + include_revisions: true, sub_storage: { - type: "uuid", + type: "query", sub_storage: { - type: "indexeddb", - database: dbname + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } } } } @@ -599,9 +720,7 @@ function () { stop(); expect(4); - var jio = this.jio, - not_history = this.not_history, - timestamps; + var jio = this.jio; jio.remove("doc") .push(function () { @@ -622,17 +741,6 @@ "HistoryStorage: cannot find object 'doc' (removed)", "Error is handled by history storage before reaching console"); }) - .push(function () { - return not_history.allDocs({ - select_list: ["timestamp"], - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.value.timestamp; - }); - }) .push(function () { return jio.allDocs({select_list: ["title"]}); }) @@ -641,9 +749,9 @@ { id: "doc2", value: {title: "foo"}, - doc: {}, - timestamp: timestamps[1] - }], "DOcument that was removed before being put is not retrieved"); + //timestamp: timestamps[1], + doc: {} + }], "Document that was removed before being put is not retrieved"); }) .fail(function (error) { //console.log(error); @@ -657,8 +765,7 @@ stop(); expect(2); var jio = this.jio, - history = this.history, - timestamps; + history = this.history; jio.remove("doc") .push(function () { @@ -672,16 +779,6 @@ title: "foo" }, "A put was the most recent edit on 'doc'"); }) - .push(function () { - return history.allDocs({ - select_list: ["timestamp"] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.timestamp; - }); - }) .push(function () { return history.allDocs({select_list: ["title"]}); }) @@ -690,14 +787,14 @@ { id: "doc", value: {title: "foo"}, - doc: {}, - timestamp: timestamps[0] + //timestamp: timestamps[0], + doc: {} }, { id: "doc", value: {}, - doc: {}, - timestamp: timestamps[1] + //timestamp: timestamps[1], + doc: {} }], "DOcument that was removed before being put is not retrieved"); }) .fail(function (error) { @@ -713,14 +810,15 @@ expect(2); var jio = this.jio, history = this.history, + not_history = this.not_history, timestamp; jio.put("doc", {title: "foo"}) .push(function () { - return history.allDocs(); + return not_history.allDocs(); }) .push(function (res) { - timestamp = res.data.rows[0].timestamp; + timestamp = res.data.rows[0].id; return history.put(timestamp, {key: "val"}); }) .push(function () { @@ -779,7 +877,7 @@ stop(); expect(6); var jio = this.jio, - history = this.history, + not_history = this.not_history, timestamp; jio.put("not_doc", {}) .push(function () { @@ -799,32 +897,14 @@ "Error is handled by history storage before reaching console"); }) .push(function () { - return history.allDocs(); + return not_history.allDocs(); }) .push(function (results) { - timestamp = results.data.rows[0].timestamp; + timestamp = results.data.rows[0].id; return jio.get(timestamp); }) .push(function () { ok(false, "This statement should not be reached"); - }, function (error) { - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - deepEqual(error.status_code, - 404, - "Correct status code for getting a non-existent document" - ); - deepEqual(error.message, - "HistoryStorage: cannot find object '" + timestamp + "'", - "Error is handled by history storage before reaching console"); - }) - /** - * XXX: I don't think this test is necessary - .push(function () { - return history.get("doc"); - }) - .push(function (res) { - console.log(res); - ok(false, "This statement should not be reached"); }, function (error) { //console.log(error); ok(error instanceof jIO.util.jIOError, "Correct type of error"); @@ -833,10 +913,9 @@ "Correct status code for getting a non-existent document" ); deepEqual(error.message, - "HistoryStorage: cannot find object 'doc'", + "HistoryStorage: cannot find object '" + timestamp + "'", "Error is handled by history storage before reaching console"); }) - **/ .fail(function (error) { //console.log(error); @@ -1093,6 +1172,7 @@ stop(); expect(4); var jio = this.jio, + not_history = this.not_history, history = this.history, blob = new Blob(['a']), edit_log; @@ -1108,20 +1188,22 @@ return jio.get("doc"); }) .push(function (res) { - deepEqual(res, {title: "foo0"}); - return history.allDocs({select_list: ["title"]}); + deepEqual(res, + {title: "foo0"}, + "Correct information returned"); + return not_history.allDocs({select_list: ["title"]}); }) .push(function (results) { edit_log = results.data.rows; - return history.get(edit_log[0].timestamp); + return history.get(edit_log[0].id); }) .push(function (result) { deepEqual(result, {title: "foo0"}); - return history.get(edit_log[1].timestamp); + return history.get(edit_log[1].id); }) .push(function (result) { deepEqual(result, {title: "foo0"}); - return history.get(edit_log[2].timestamp); + return history.get(edit_log[2].id); }) .push(function (result) { deepEqual(result, {title: "foo0"}); @@ -1141,30 +1223,36 @@ module("HistoryStorage.allDocs", { setup: function () { // create storage of type "history" with memory as substorage - var dbname = "db_" + Date.now(); + this.dbname = "db_" + Date.now(); this.jio = jIO.createJIO({ - type: "history", + type: "uuid", sub_storage: { type: "query", sub_storage: { - type: "uuid", + type: "history", sub_storage: { - type: "indexeddb", - database: dbname + type: "query", + sub_storage: { + type: "indexeddb", + database: this.dbname + } } } } }); this.history = jIO.createJIO({ - type: "history", - include_revisions: true, + type: "uuid", sub_storage: { type: "query", sub_storage: { - type: "uuid", + type: "history", + include_revisions: true, sub_storage: { - type: "indexeddb", - database: dbname + type: "query", + sub_storage: { + type: "indexeddb", + database: this.dbname + } } } } @@ -1175,7 +1263,7 @@ type: "uuid", sub_storage: { type: "indexeddb", - database: dbname + database: this.dbname } } }); @@ -1223,26 +1311,27 @@ deepEqual(results.data.rows[0], { doc: {}, value: {}, - id: "doc", - timestamp: timestamp + //timestamp: timestamp, + id: "doc" }, "Correct document format is returned." ); return not_history.allDocs(); }) .push(function (results) { + timestamp = results.data.rows[0].id; equal(results.data.total_rows, 1, "Exactly one result returned"); - return not_history.get(results.data.rows[0].id); + return not_history.get(timestamp); }) .push(function (result) { deepEqual(result, { - timestamp: timestamp, doc_id: "doc", doc: { title: "version0" }, + timestamp: timestamp, op: "put" }, "When a different type of storage queries historystorage, all " + @@ -1256,36 +1345,27 @@ .always(function () {start(); }); }); - test("Putting doc with _doc_id and _timestamp properties" + - "and retrieving them with allDocs", + test("Putting doc with troublesome properties and retrieving with allDocs", function () { stop(); expect(1); - var jio = this.jio, - not_history = this.not_history, - timestamp; + var jio = this.jio; jio.put("doc", { title: "version0", - _doc_id: "bar", - __doc_id: "bar2", - ___doc_id: "bar3", - _timestamp: "foo", - ____timestamp: "foo2" + doc_id: "bar", + _doc_id: "bar2", + timestamp: "foo", + _timestamp: "foo2", + id: "baz", + _id: "baz2", + __id: "baz3", + op: "zop" }) - .push(function () { - return not_history.allDocs({ - query: "doc_id: doc", - select_list: ["timestamp"] - }); - }) - .push(function (results) { - timestamp = results.data.rows[0].value.timestamp; - }) .push(function () { return jio.allDocs({ query: "title: version0 AND _timestamp: >= 0", - select_list: ["title", "_doc_id", "__doc_id", "___doc_id", - "_timestamp", "____timestamp"] + select_list: ["title", "doc_id", "_doc_id", "timestamp", + "_timestamp", "id", "_id", "__id", "op"] }); }) .push(function (results) { @@ -1293,17 +1373,20 @@ { doc: {}, id: "doc", + //timestamp: timestamp, value: { title: "version0", - _doc_id: "bar", - __doc_id: "bar2", - ___doc_id: "bar3", - _timestamp: "foo", - ____timestamp: "foo2" - }, - timestamp: timestamp + doc_id: "bar", + _doc_id: "bar2", + timestamp: "foo", + _timestamp: "foo2", + id: "baz", + _id: "baz2", + __id: "baz3", + op: "zop" + } }], - "_doc_id properties are not overwritten in allDocs call"); + "Poorly-named properties are not overwritten in allDocs call"); }) .fail(function (error) { //console.log(error); @@ -1393,8 +1476,8 @@ subtitle: "subvers2" }, doc: {}, - id: "doc", - timestamp: timestamps[2] + //timestamp: timestamps[2], + id: "doc" }, "Correct document format is returned." ); @@ -1416,8 +1499,8 @@ title: "version2", subtitle: "subvers2" }, - doc: {}, - timestamp: timestamps[2] + //timestamp: timestamps[2], + doc: {} }, { id: "doc", @@ -1425,8 +1508,8 @@ title: "version1", subtitle: "subvers1" }, - doc: {}, - timestamp: timestamps[1] + //timestamp: timestamps[1], + doc: {} }, { id: "doc", @@ -1434,8 +1517,8 @@ title: "version0", subtitle: "subvers0" }, - doc: {}, - timestamp: timestamps[0] + //timestamp: timestamps[0], + doc: {} } ], "Full version history is included."); @@ -1550,14 +1633,14 @@ { id: "doc_c", value: {}, - doc: {}, - timestamp: timestamps[5] + //timestamp: timestamps[5], + doc: {} }, { id: "doc_a", value: {}, - doc: {}, - timestamp: timestamps[1] + //timestamp: timestamps[1], + doc: {} } ], "Empty query returns latest revisions (and no removed documents)"); @@ -1582,8 +1665,6 @@ stop(); expect(2); var jio = this.jio, - not_history = this.not_history, - timestamps, docs = [ { "date": 1, @@ -1616,16 +1697,6 @@ .push(function () { return putFullDoc(jio, "third_doc", docs[2], "data", blobs[2]); // 5,6 }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) .push(function () { return jio.allDocs({ query: "NOT (date: > 2)", @@ -1641,20 +1712,20 @@ { doc: {}, id: "doc", - value: {date: 1}, - timestamp: timestamps[2] + //timestamp: timestamps[2], + value: {date: 1} }, { doc: {}, id: "third_doc", - value: {date: 2}, - timestamp: timestamps[6] + //timestamp: timestamps[6], + value: {date: 2} }, { doc: {}, id: "second_doc", - value: {date: 2}, - timestamp: timestamps[4] + //timestamp: timestamps[4], + value: {date: 2} } ], "Query gives correct results in correct order"); @@ -1881,63 +1952,63 @@ { doc: {}, id: "second_doc", + //timestamp: timestamps[9], value: { date: 4, title: "second_doc", type: "bar2" - }, - timestamp: timestamps[9] + } }, { doc: {}, id: "second_doc", + //timestamp: timestamps[8], value: { date: 4, title: "second_doc", type: "bar2" - }, - timestamp: timestamps[8] + } }, { doc: {}, id: "doc", + //timestamp: timestamps[6], value: { date: 4, title: "doc", type: "foo2" - }, - timestamp: timestamps[6] + } }, { doc: {}, id: "doc", + //timestamp: timestamps[5], value: { date: 4, title: "doc", type: "foo2" - }, - timestamp: timestamps[5] + } }, { doc: {}, id: "doc", + //timestamp: timestamps[2], value: { date: 1, title: "doc", type: "foo" - }, - timestamp: timestamps[2] + } }, { doc: {}, id: "doc", + //timestamp: timestamps[1], value: { date: 1, title: "doc", type: "foo" - }, - timestamp: timestamps[1] + } } ], "Query gives correct results in correct order"); @@ -1957,7 +2028,6 @@ var jio = this.jio, history = this.history, not_history = this.not_history, - timestamps, blob = new Blob(['a']); jio.put("document", {title: "foo"}) @@ -1967,18 +2037,6 @@ .push(function () { return jio.putAttachment("document", "attachment", blob); }) - - // Get timestamps - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) .push(function () { return history.allDocs({select_list: ["title"]}); }) @@ -1987,20 +2045,20 @@ { id: "document", doc: {}, - value: {}, - timestamp: timestamps[2] + //timestamp: timestamps[2], + value: {} }, { id: "document", doc: {}, - value: {}, - timestamp: timestamps[1] + //timestamp: timestamps[1], + value: {} }, { id: "document", doc: {}, - value: {title: "foo"}, - timestamp: timestamps[0] + //timestamp: timestamps[0], + value: {title: "foo"} }], "Attachment on removed document is handled correctly" ); @@ -2021,8 +2079,6 @@ expect(2); var jio = this.jio, history = this.history, - not_history = this.not_history, - timestamps, blob = new Blob(['a']); jio.put("document", {title: "foo"}) @@ -2033,18 +2089,6 @@ return jio.removeAttachment("document", "attachment"); }) - // Get timestamps - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) - .push(function () { return history.allDocs({select_list: ["title"]}); }) @@ -2053,20 +2097,20 @@ { id: "document", doc: {}, - value: {title: "foo"}, - timestamp: timestamps[2] + //timestamp: timestamps[2], + value: {title: "foo"} }, { id: "document", doc: {}, - value: {title: "foo"}, - timestamp: timestamps[1] + //timestamp: timestamps[1], + value: {title: "foo"} }, { id: "document", doc: {}, - value: {title: "foo"}, - timestamp: timestamps[0] + //timestamp: timestamps[0], + value: {title: "foo"} }], "Attachment on removed document is handled correctly" ); @@ -2133,6 +2177,48 @@ .always(function () {start(); }); }); + test("Adding second query storage on top of history", + function () { + stop(); + expect(1); + var jio = this.jio; + return jio.put("doca", {title: "foo0", date: 0}) + .push(function () { + return jio.put("docb", {title: "bar0", date: 0}); + }) + .push(function () { + return jio.put("docb", {title: "bar1", date: 0}); + }) + .push(function () { + return jio.put("doca", {title: "foo1", date: 1}); + }) + .push(function () { + return jio.put("docb", {title: "bar2", date: 2}); + }) + .push(function () { + return jio.allDocs({ + query: "title: foo1 OR title: bar2", + select_list: ["title"], + sort_on: [["date", "ascending"]], + limit: [0, 1] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doca", + value: {title: "foo1"} + } + ]); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + module("HistoryStorage.Full-Example", { setup: function () { @@ -2144,28 +2230,34 @@ this.other_blob = new Blob(['1']); this.jio = jIO.createJIO({ - type: "history", + type: "query", sub_storage: { - type: "query", + type: "history", sub_storage: { - type: "uuid", + type: "query", sub_storage: { - type: "indexeddb", - database: dbname + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } } } } }); this.history = jIO.createJIO({ - type: "history", - include_revisions: true, + type: "query", sub_storage: { - type: "query", + type: "history", + include_revisions: true, sub_storage: { - type: "uuid", + type: "query", sub_storage: { - type: "indexeddb", - database: dbname + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } } } } @@ -2189,8 +2281,6 @@ expect(1); var jio = this.jio, history = this.history, - not_history = this.not_history, - timestamps, blobs1 = [ new Blob(['a']), new Blob(['ab']), @@ -2225,18 +2315,6 @@ return putFullDoc(jio, "doc", {title: "bar3"}, "data", blobs1[4]); }) - // Get timestamps - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) - .push(function () { return history.allDocs({ select_list: ["title"] @@ -2247,86 +2325,86 @@ { doc: {}, id: "doc", - value: {title: "bar3"}, - timestamp: timestamps[13] + //timestamp: timestamps[13], + value: {title: "bar3"} }, { doc: {}, id: "doc", - value: {title: "bar3"}, - timestamp: timestamps[12] + //timestamp: timestamps[12], + value: {title: "bar3"} }, { doc: {}, id: "doc", - value: {title: "bar2"}, - timestamp: timestamps[11] + //timestamp: timestamps[11], + value: {title: "bar2"} }, { doc: {}, id: "doc", - value: {title: "bar2"}, - timestamp: timestamps[10] + //timestamp: timestamps[10], + value: {title: "bar2"} }, { doc: {}, id: "doc2", - value: {title: "foo1"}, - timestamp: timestamps[9] + //timestamp: timestamps[9], + value: {title: "foo1"} }, { doc: {}, id: "doc2", - value: {title: "foo1"}, - timestamp: timestamps[8] + //timestamp: timestamps[8], + value: {title: "foo1"} }, { doc: {}, id: "doc2", - value: {title: "foo0"}, - timestamp: timestamps[7] + //timestamp: timestamps[7], + value: {title: "foo0"} }, { doc: {}, id: "doc2", - value: {title: "foo0"}, - timestamp: timestamps[6] + //timestamp: timestamps[6], + value: {title: "foo0"} }, { doc: {}, id: "doc", - value: {title: "bar1"}, - timestamp: timestamps[5] + //timestamp: timestamps[5], + value: {title: "bar1"} }, { doc: {}, id: "doc", - value: {title: "bar1"}, - timestamp: timestamps[4] + //timestamp: timestamps[4], + value: {title: "bar1"} }, { doc: {}, id: "doc", - value: {title: "bar0"}, - timestamp: timestamps[3] + //timestamp: timestamps[3], + value: {title: "bar0"} }, { doc: {}, id: "doc", - value: {title: "bar0"}, - timestamp: timestamps[2] + //timestamp: timestamps[2], + value: {title: "bar0"} }, { doc: {}, id: "doc", - value: {title: "bar"}, - timestamp: timestamps[1] + //timestamp: timestamps[1], + value: {title: "bar"} }, { doc: {}, id: "doc", - value: {title: "bar"}, - timestamp: timestamps[0] + //timestamp: timestamps[0], + value: {title: "bar"} } ], "allDocs with include_revisions should return all revisions"); @@ -2344,9 +2422,7 @@ stop(); expect(1); var jio = this.jio, - not_history = this.not_history, history = this.history, - timestamps, blobs1 = [ new Blob(['a']), new Blob(['ab']), @@ -2368,18 +2444,6 @@ return jio.putAttachment("doc", "data", blobs1[1]); }) - // Get timestamps - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) - .push(function () { return history.allDocs({ select_list: ["title"] @@ -2390,32 +2454,32 @@ { doc: {}, id: "doc", - value: {title: "bar0"}, - timestamp: timestamps[4] + //timestamp: timestamps[4], + value: {title: "bar0"} }, { doc: {}, id: "doc2", - value: {title: "foo0"}, - timestamp: timestamps[3] + //timestamp: timestamps[3], + value: {title: "foo0"} }, { doc: {}, id: "doc", - value: {title: "bar0"}, - timestamp: timestamps[2] + //timestamp: timestamps[2], + value: {title: "bar0"} }, { doc: {}, id: "doc", - value: {title: "bar0"}, - timestamp: timestamps[1] + //timestamp: timestamps[1], + value: {title: "bar0"} }, { doc: {}, id: "doc", - value: {title: "bar"}, - timestamp: timestamps[0] + //timestamp: timestamps[0], + value: {title: "bar"} } ], "allDocs with include_revisions should return all revisions"); @@ -2433,9 +2497,7 @@ stop(); expect(2); var jio = this.jio, - not_history = this.not_history, history = this.history, - timestamps, blobs1 = [ new Blob(['a']), new Blob(['ab']), @@ -2456,18 +2518,6 @@ .push(function () { return jio.putAttachment("doc", "data", blobs1[1]); }) - - // Get timestamps - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) .push(function () { return jio.allDocs({ select_list: ["title"] @@ -2478,14 +2528,14 @@ { doc: {}, id: "doc", - value: {title: "bar0"}, - timestamp: timestamps[4] + //timestamp: timestamps[4], + value: {title: "bar0"} }, { doc: {}, id: "doc2", - value: {title: "foo0"}, - timestamp: timestamps[3] + //timestamp: timestamps[3], + value: {title: "foo0"} } ], "allDocs with include_revisions false should return all revisions"); @@ -2500,32 +2550,32 @@ { doc: {}, id: "doc", - value: {title: "bar0"}, - timestamp: timestamps[4] + //timestamp: timestamps[4], + value: {title: "bar0"} }, { doc: {}, id: "doc2", - value: {title: "foo0"}, - timestamp: timestamps[3] + //timestamp: timestamps[3], + value: {title: "foo0"} }, { doc: {}, id: "doc", - value: {title: "bar0"}, - timestamp: timestamps[2] + //timestamp: timestamps[2], + value: {title: "bar0"} }, { doc: {}, id: "doc", - value: {title: "bar0"}, - timestamp: timestamps[1] + //timestamp: timestamps[1], + value: {title: "bar0"} }, { doc: {}, id: "doc", - value: {title: "bar"}, - timestamp: timestamps[0] + //timestamp: timestamps[0], + value: {title: "bar"} } ], "allDocs with include_revisions true should return all revisions"); -- 2.30.9 From a1fa77a9bb288346b366f587480a92048deb0c7b Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Tue, 10 Jul 2018 13:12:14 +0000 Subject: [PATCH 40/46] Added packing function for removing old data without corruption. --- src/jio.storage/historystorage.js | 97 ++++- test/jio.storage/historystorage.tests.js | 473 +++++++++++++++++++++++ 2 files changed, 569 insertions(+), 1 deletion(-) diff --git a/src/jio.storage/historystorage.js b/src/jio.storage/historystorage.js index f071675..adb1ebb 100644 --- a/src/jio.storage/historystorage.js +++ b/src/jio.storage/historystorage.js @@ -15,6 +15,71 @@ return timestamp + "-" + uuid; } + function removeOldRevs( + substorage, + results, + keepDoc + ) { + var ind, + promises = [], + seen = {}, + docum, + log, + start_ind, + new_promises, + doc_id, + checkIsId, + removeDoc; + for (ind = 0; ind < results.data.rows.length; ind += 1) { + docum = results.data.rows[ind]; + // Count the number of revisions of each document, and delete older + // ones. + if (!seen.hasOwnProperty(docum.value.doc_id)) { + seen[docum.value.doc_id] = {count: 0}; + } + log = seen[docum.value.doc_id]; + log.count += 1; + //log.id = docum.id; + + // Record the index of the most recent edit that is before the cutoff + if (!log.hasOwnProperty("s") && !keepDoc({doc: docum, log: log})) { + log.s = ind; + } + + // Record the index of the most recent put or remove + if ((!log.hasOwnProperty("pr")) && + (docum.value.op === "put" || docum.value.op === "remove")) { + log.pr = ind; + log.final = ind; + } + + if ((docum.op === "putAttachment" || docum.op === "removeAttachment") && + log.hasOwnProperty(docum.name) && + !log[docum.name].hasOwnProperty("prA")) { + log[docum.name].prA = ind; + log.final = ind; + } + } + checkIsId = function (d) { + return d.value.doc_id === doc_id; + }; + removeDoc = function (d) { + return substorage.remove(d.id); + }; + for (doc_id in seen) { + if (seen.hasOwnProperty(doc_id)) { + log = seen[doc_id]; + start_ind = Math.max(log.s, log.final + 1); + new_promises = results.data.rows + .slice(start_ind) + .filter(checkIsId) + .map(removeDoc); + promises = promises.concat(new_promises); + } + } + return RSVP.all(promises); + } + function throwCantFindError(id) { throw new jIO.util.jIOError( "HistoryStorage: cannot find object '" + id + "'", @@ -42,6 +107,34 @@ } else { this._include_revisions = false; } + var substorage = this._sub_storage; + this.packOldRevisions = function (save_info) { + /** + save_info has this form: + { + keep_latest_num: 10, + keep_active_revs: timestamp + } + keep_latest_num = x: keep at most the x latest copies of each unique doc + keep_active_revs = x: throw away all outdated revisions from before x + **/ + var options = { + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "op"] + }, + keep_fixed_num = save_info.hasOwnProperty("keep_latest_num"); + return substorage.allDocs(options) + .push(function (results) { + if (keep_fixed_num) { + return removeOldRevs(substorage, results, function (data) { + return data.log.count <= save_info.keep_latest_num; + }); + } + return removeOldRevs(substorage, results, function (data) { + return data.doc.id > save_info.keep_active_revs; + }); + }); + }; } HistoryStorage.prototype.get = function (id_in) { @@ -330,14 +423,15 @@ }; return this._sub_storage.put(timestamp, metadata); }; + HistoryStorage.prototype.repair = function () { return this._sub_storage.repair.apply(this._sub_storage, arguments); }; + HistoryStorage.prototype.hasCapacity = function (name) { return name === 'list' || name === 'include'; }; - HistoryStorage.prototype.buildQuery = function (options) { // Set default values if (options === undefined) {options = {}; } @@ -442,6 +536,7 @@ // this attachment, then don't include attachment in query return false; } + docum.value.doc = {}; } } } diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index 4b88037..6ba1bd1 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -2586,4 +2586,477 @@ }) .always(function () {start(); }); }); + + + module("HistoryStorage.pack", { + setup: function () { + // create storage of type "history" with memory as substorage + var dbname = "db_" + Date.now(); + this.jio = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "history", + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + } + }); + this.history = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "history", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + } + }); + this.not_history = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + }); + this.blob = new Blob(['a']); + } + }); + + test("Verifying pack works with keep_latest_num", + function () { + stop(); + expect(2); + var jio = this.jio, + not_history = this.not_history; + return jio.put("doc_a", {title: "rev"}) + .push(function () { + return jio.put("doc_a", {title: "rev0"}); + }) + .push(function () { + return jio.put("doc_a", {title: "rev1"}); + }) + .push(function () { + return jio.put("doc_b", {title: "data"}); + }) + .push(function () { + return jio.put("doc_b", {title: "data0"}); + }) + .push(function () { + return jio.put("doc_a", {title: "rev2"}); + }) + .push(function () { + return jio.put("doc_b", {title: "data1"}); + }) + .push(function () { + return jio.put("doc_b", {title: "data2"}); + }) + .push(function () { + return jio.__storage._sub_storage.__storage._sub_storage + .__storage.packOldRevisions({ + keep_latest_num: 2 + }); + }) + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "timestamp", "op"] + }); + }) + .push(function (results) { + equal(results.data.total_rows, 4, "Correct amount of results"); + deepEqual(results.data.rows, [ + { + doc: {}, + id: results.data.rows[0].id, + value: { + doc: {title: "data2"}, + doc_id: "doc_b", + timestamp: results.data.rows[0].id, + op: "put" + } + }, + { + doc: {}, + id: results.data.rows[1].id, + value: { + doc: {title: "data1"}, + doc_id: "doc_b", + timestamp: results.data.rows[1].id, + op: "put" + } + }, + { + doc: {}, + id: results.data.rows[2].id, + value: { + doc: {title: "rev2"}, + doc_id: "doc_a", + timestamp: results.data.rows[2].id, + op: "put" + } + }, + { + doc: {}, + id: results.data.rows[3].id, + value: { + doc: {title: "rev1"}, + doc_id: "doc_a", + timestamp: results.data.rows[3].id, + op: "put" + } + } + ], + "Keep the correct documents after pack"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + + test("Verifying pack works with fixed timestamp", + function () { + stop(); + expect(2); + var jio = this.jio, + not_history = this.not_history, + timestamp; + return jio.allDocs() + .push(function () { + return RSVP.all([ + jio.put("doc_a", {title: "old_rev0"}), + jio.put("doc_a", {title: "old_rev1"}), + jio.put("doc_a", {title: "old_rev2"}), + jio.put("doc_b", {title: "old_data0"}), + jio.put("doc_b", {title: "old_data1"}), + jio.put("doc_b", {title: "old_data2"}), + jio.put("doc_c", {title: "latest_bar"}) + ]); + }) + .push(function () { + return not_history.allDocs({sort_on: [["timestamp", "descending"]]}); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + return jio.put("doc_a", {title: "latest_rev"}); + }) + .push(function () { + return jio.put("doc_b", {title: "latest_data"}); + }) + .push(function () { + return jio.__storage._sub_storage.__storage._sub_storage + .__storage.packOldRevisions({ + keep_active_revs: timestamp + }); + }) + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "timestamp"] + }); + }) + .push(function (results) { + equal(results.data.total_rows, 3, "Correct amount of results"); + deepEqual(results.data.rows, [ + { + doc: {}, + id: results.data.rows[0].id, + value: { + doc: {title: "latest_data"}, + doc_id: "doc_b", + timestamp: results.data.rows[0].id + } + }, + { + doc: {}, + id: results.data.rows[1].id, + value: { + doc: {title: "latest_rev"}, + doc_id: "doc_a", + timestamp: results.data.rows[1].id + } + }, + { + doc: {}, + id: results.data.rows[2].id, + value: { + doc: {title: "latest_bar"}, + doc_id: "doc_c", + timestamp: results.data.rows[2].id + } + } + ], + "Keep the correct documents after pack"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + + test("Verifying pack works with fixed timestamp and more complex operations", + function () { + stop(); + expect(2); + var jio = this.jio, + not_history = this.not_history, + timestamp; + return jio.allDocs() + .push(function () { + return RSVP.all([ + jio.put("doc_a", {title: "old_rev0"}), + jio.put("doc_a", {title: "old_rev1"}), + jio.put("doc_a", {title: "old_rev2"}), + jio.put("doc_b", {title: "latest_data"}) + ]); + }) + .push(function () { + return jio.allDocs({sort_on: [["timestamp", "descending"]]}); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + return jio.remove("doc_a"); + }) + .push(function () { + return jio.__storage._sub_storage.__storage._sub_storage + .__storage.packOldRevisions({ + keep_active_revs: timestamp + }); + }) + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "timestamp", "op"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: results.data.rows[0].id, + value: { + op: "remove", + doc_id: "doc_a", + timestamp: results.data.rows[0].id + } + }, + { + doc: {}, + id: results.data.rows[1].id, + value: { + doc: {title: "latest_data"}, + doc_id: "doc_b", + op: "put", + timestamp: results.data.rows[1].id + } + } + ], + "Keep the correct documents after pack"); + }) + .push(function () { + return jio.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc_b", + value: {title: "latest_data"} + } + ], + "Memory not corrupted by pack without include_revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + + test("Verifying pack works with fixed timestamp and more complex operations", + function () { + stop(); + expect(2); + var jio = this.jio, + not_history = this.not_history, + timestamp; + return jio.allDocs() + .push(function () { + return RSVP.all([ + jio.put("doc_a", {title: "old_rev0"}), + jio.put("doc_a", {title: "old_rev1"}), + jio.put("doc_a", {title: "old_rev2"}), + jio.put("doc_b", {title: "latest_data"}) + ]); + }) + .push(function () { + return jio.allDocs({sort_on: [["timestamp", "descending"]]}); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + return jio.remove("doc_a"); + }) + .push(function () { + return jio.__storage._sub_storage.__storage._sub_storage + .__storage.packOldRevisions({ + keep_active_revs: timestamp + }); + }) + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "timestamp", "op"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: results.data.rows[0].id, + value: { + op: "remove", + doc_id: "doc_a", + timestamp: results.data.rows[0].id + } + }, + { + doc: {}, + id: results.data.rows[1].id, + value: { + doc: {title: "latest_data"}, + doc_id: "doc_b", + op: "put", + timestamp: results.data.rows[1].id + } + } + ], + "Keep the correct documents after pack"); + }) + .push(function () { + return jio.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc_b", + value: {title: "latest_data"} + } + ], + "Memory not corrupted by pack without include_revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + + test("Verifying pack works with fixed timestamp and more complex operations", + function () { + stop(); + expect(2); + var jio = this.jio, + not_history = this.not_history, + timestamp, + blob = this.blob; + return jio.allDocs() + .push(function () { + return RSVP.all([ + jio.put("doc_a", {title: "old_rev0"}), + jio.putAttachment("doc_a", "attach_aa", blob), + jio.put("doc_b", {title: "latest_data"}) + ]); + }) + .push(function () { + return jio.allDocs({sort_on: [["timestamp", "descending"]]}); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + return jio.remove("doc_a"); + }) + .push(function () { + return jio.__storage._sub_storage.__storage._sub_storage + .__storage.packOldRevisions({ + keep_active_revs: timestamp + }); + }) + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "timestamp", "op"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: results.data.rows[0].id, + value: { + op: "remove", + doc_id: "doc_a", + timestamp: results.data.rows[0].id + } + }, + { + doc: {}, + id: results.data.rows[1].id, + value: { + doc: {title: "latest_data"}, + doc_id: "doc_b", + op: "put", + timestamp: results.data.rows[1].id + } + } + ], + "Keep the correct documents after pack"); + }) + .push(function () { + return jio.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc_b", + value: {title: "latest_data"} + } + ], + "Memory not corrupted by pack without include_revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + }(jIO, RSVP, Blob, QUnit)); \ No newline at end of file -- 2.30.9 From 3d03aafb2f24eba4348c7a4af37c3d881ca521ba Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Thu, 19 Jul 2018 08:58:11 +0000 Subject: [PATCH 41/46] Changed allDocs behavior for returned id when include_revisions is true --- src/jio.storage/historystorage.js | 12 +- test/jio.storage/historystorage.tests.js | 223 +++++++++++++---------- 2 files changed, 136 insertions(+), 99 deletions(-) diff --git a/src/jio.storage/historystorage.js b/src/jio.storage/historystorage.js index adb1ebb..c7d33a1 100644 --- a/src/jio.storage/historystorage.js +++ b/src/jio.storage/historystorage.js @@ -438,15 +438,12 @@ if (options.query === undefined) {options.query = ""; } if (options.sort_on === undefined) {options.sort_on = []; } if (options.select_list === undefined) {options.select_list = []; } - if (options.include_revisions === undefined) { - options.include_revisions = false; - } options.query = jIO.QueryFactory.create(options.query); var meta_options = { query: "", sort_on: [["timestamp", "descending"]], - select_list: ["doc", "op", "doc_id"] + select_list: ["doc", "op", "doc_id", "timestamp"] }, include_revs = this._include_revisions; @@ -554,8 +551,13 @@ // Put into correct format to be passed back to query storage .map(function (docum) { docum.doc = docum.value.doc; - docum.id = docum.value.doc_id; + if (include_revs) { + docum.id = docum.value.timestamp; + } else { + docum.id = docum.value.doc_id; + } delete docum.value.doc_id; + delete docum.value.timestamp; delete docum.value.op; if (options.include_docs) { diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index 6ba1bd1..b7f5ecb 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -79,12 +79,11 @@ var jio = this.jio, history = this.history, not_history = this.not_history, - //timestamps, - id; + timestamps; return jio.post({title: "foo0"}) .push(function (result) { - id = result; + //id = result; return jio.put(result, {title: "foo1"}); }) .push(function (result) { @@ -97,7 +96,12 @@ }) .push(function () { return not_history.allDocs({ - select_list: ["timestamp"] + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; }); }) .push(function () { @@ -110,16 +114,14 @@ title: "foo1" }, doc: {}, - //timestamp: timestamps[1], - id: id + id: timestamps[1] }, { value: { title: "foo0" }, doc: {}, - //timestamp: timestamps[0], - id: id + id: timestamps[0] } ], "Two revisions logged with correct metadata"); @@ -765,7 +767,9 @@ stop(); expect(2); var jio = this.jio, - history = this.history; + history = this.history, + timestamps, + not_history = this.not_history; jio.remove("doc") .push(function () { @@ -779,21 +783,30 @@ title: "foo" }, "A put was the most recent edit on 'doc'"); }) + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) .push(function () { return history.allDocs({select_list: ["title"]}); }) .push(function (results) { deepEqual(results.data.rows, [ { - id: "doc", + //id: "doc", value: {title: "foo"}, - //timestamp: timestamps[0], + id: timestamps[1], doc: {} }, { - id: "doc", value: {}, - //timestamp: timestamps[1], + id: timestamps[0], doc: {} }], "DOcument that was removed before being put is not retrieved"); }) @@ -1494,30 +1507,30 @@ "Querying with include_revisions retrieves all versions"); deepEqual(results.data.rows, [ { - id: "doc", + //id: results.data.rows[0].id, value: { title: "version2", subtitle: "subvers2" }, - //timestamp: timestamps[2], + id: timestamps[2], doc: {} }, { - id: "doc", + //id: results.data.rows[1].id, value: { title: "version1", subtitle: "subvers1" }, - //timestamp: timestamps[1], + id: timestamps[1], doc: {} }, { - id: "doc", + //id: results.data.rows[2].id, value: { title: "version0", subtitle: "subvers0" }, - //timestamp: timestamps[0], + id: timestamps[0], doc: {} } ], "Full version history is included."); @@ -1951,8 +1964,7 @@ deepEqual(results.data.rows, [ { doc: {}, - id: "second_doc", - //timestamp: timestamps[9], + id: timestamps[9], value: { date: 4, title: "second_doc", @@ -1961,8 +1973,7 @@ }, { doc: {}, - id: "second_doc", - //timestamp: timestamps[8], + id: timestamps[8], value: { date: 4, title: "second_doc", @@ -1971,8 +1982,7 @@ }, { doc: {}, - id: "doc", - //timestamp: timestamps[6], + id: timestamps[6], value: { date: 4, title: "doc", @@ -1981,8 +1991,7 @@ }, { doc: {}, - id: "doc", - //timestamp: timestamps[5], + id: timestamps[5], value: { date: 4, title: "doc", @@ -1992,8 +2001,7 @@ { doc: {}, - id: "doc", - //timestamp: timestamps[2], + id: timestamps[2], value: { date: 1, title: "doc", @@ -2002,8 +2010,7 @@ }, { doc: {}, - id: "doc", - //timestamp: timestamps[1], + id: timestamps[1], value: { date: 1, title: "doc", @@ -2028,7 +2035,8 @@ var jio = this.jio, history = this.history, not_history = this.not_history, - blob = new Blob(['a']); + blob = new Blob(['a']), + timestamps; jio.put("document", {title: "foo"}) .push(function () { @@ -2037,27 +2045,34 @@ .push(function () { return jio.putAttachment("document", "attachment", blob); }) + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) .push(function () { return history.allDocs({select_list: ["title"]}); }) .push(function (results) { deepEqual(results.data.rows, [ { - id: "document", + id: timestamps[2], doc: {}, - //timestamp: timestamps[2], value: {} }, { - id: "document", + id: timestamps[1], doc: {}, - //timestamp: timestamps[1], value: {} }, { - id: "document", + id: timestamps[0], doc: {}, - //timestamp: timestamps[0], value: {title: "foo"} }], "Attachment on removed document is handled correctly" @@ -2079,7 +2094,9 @@ expect(2); var jio = this.jio, history = this.history, - blob = new Blob(['a']); + blob = new Blob(['a']), + timestamps, + not_history = this.not_history; jio.put("document", {title: "foo"}) .push(function () { @@ -2088,28 +2105,34 @@ .push(function () { return jio.removeAttachment("document", "attachment"); }) - + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) .push(function () { return history.allDocs({select_list: ["title"]}); }) .push(function (results) { deepEqual(results.data.rows, [ { - id: "document", + id: timestamps[2], doc: {}, - //timestamp: timestamps[2], value: {title: "foo"} }, { - id: "document", + id: timestamps[1], doc: {}, - //timestamp: timestamps[1], value: {title: "foo"} }, { - id: "document", + id: timestamps[0], doc: {}, - //timestamp: timestamps[0], value: {title: "foo"} }], "Attachment on removed document is handled correctly" @@ -2281,6 +2304,8 @@ expect(1); var jio = this.jio, history = this.history, + timestamps, + not_history = this.not_history, blobs1 = [ new Blob(['a']), new Blob(['ab']), @@ -2314,6 +2339,16 @@ .push(function () { return putFullDoc(jio, "doc", {title: "bar3"}, "data", blobs1[4]); }) + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) .push(function () { return history.allDocs({ @@ -2324,86 +2359,72 @@ deepEqual(results.data.rows, [ { doc: {}, - id: "doc", - //timestamp: timestamps[13], + id: timestamps[13], value: {title: "bar3"} }, { doc: {}, - id: "doc", - //timestamp: timestamps[12], + id: timestamps[12], value: {title: "bar3"} }, { doc: {}, - id: "doc", - //timestamp: timestamps[11], + id: timestamps[11], value: {title: "bar2"} }, { doc: {}, - id: "doc", - //timestamp: timestamps[10], + id: timestamps[10], value: {title: "bar2"} }, { doc: {}, - id: "doc2", - //timestamp: timestamps[9], + id: timestamps[9], value: {title: "foo1"} }, { doc: {}, - id: "doc2", - //timestamp: timestamps[8], + id: timestamps[8], value: {title: "foo1"} }, { doc: {}, - id: "doc2", - //timestamp: timestamps[7], + id: timestamps[7], value: {title: "foo0"} }, { doc: {}, - id: "doc2", - //timestamp: timestamps[6], + id: timestamps[6], value: {title: "foo0"} }, { doc: {}, - id: "doc", - //timestamp: timestamps[5], + id: timestamps[5], value: {title: "bar1"} }, { doc: {}, - id: "doc", - //timestamp: timestamps[4], + id: timestamps[4], value: {title: "bar1"} }, { doc: {}, - id: "doc", - //timestamp: timestamps[3], + id: timestamps[3], value: {title: "bar0"} }, { doc: {}, - id: "doc", - //timestamp: timestamps[2], + id: timestamps[2], value: {title: "bar0"} }, { doc: {}, - id: "doc", - //timestamp: timestamps[1], + id: timestamps[1], value: {title: "bar"} }, { doc: {}, - id: "doc", - //timestamp: timestamps[0], + id: timestamps[0], value: {title: "bar"} } ], @@ -2423,6 +2444,8 @@ expect(1); var jio = this.jio, history = this.history, + not_history = this.not_history, + timestamps, blobs1 = [ new Blob(['a']), new Blob(['ab']), @@ -2443,6 +2466,16 @@ .push(function () { return jio.putAttachment("doc", "data", blobs1[1]); }) + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) .push(function () { return history.allDocs({ @@ -2453,32 +2486,27 @@ deepEqual(results.data.rows, [ { doc: {}, - id: "doc", - //timestamp: timestamps[4], + id: timestamps[4], value: {title: "bar0"} }, { doc: {}, - id: "doc2", - //timestamp: timestamps[3], + id: timestamps[3], value: {title: "foo0"} }, { doc: {}, - id: "doc", - //timestamp: timestamps[2], + id: timestamps[2], value: {title: "bar0"} }, { doc: {}, - id: "doc", - //timestamp: timestamps[1], + id: timestamps[1], value: {title: "bar0"} }, { doc: {}, - id: "doc", - //timestamp: timestamps[0], + id: timestamps[0], value: {title: "bar"} } ], @@ -2498,6 +2526,8 @@ expect(2); var jio = this.jio, history = this.history, + not_history = this.not_history, + timestamps, blobs1 = [ new Blob(['a']), new Blob(['ab']), @@ -2540,6 +2570,16 @@ ], "allDocs with include_revisions false should return all revisions"); }) + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) .push(function () { return history.allDocs({ select_list: ["title"] @@ -2549,32 +2589,27 @@ deepEqual(results.data.rows, [ { doc: {}, - id: "doc", - //timestamp: timestamps[4], + id: timestamps[4], value: {title: "bar0"} }, { doc: {}, - id: "doc2", - //timestamp: timestamps[3], + id: timestamps[3], value: {title: "foo0"} }, { doc: {}, - id: "doc", - //timestamp: timestamps[2], + id: timestamps[2], value: {title: "bar0"} }, { doc: {}, - id: "doc", - //timestamp: timestamps[1], + id: timestamps[1], value: {title: "bar0"} }, { doc: {}, - id: "doc", - //timestamp: timestamps[0], + id: timestamps[0], value: {title: "bar"} } ], -- 2.30.9 From ee3050e40758f16bfae87e67af1df43bb9078b7e Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Thu, 19 Jul 2018 14:01:05 +0000 Subject: [PATCH 42/46] Added new put functionality and tests on put and putAttachment. --- src/jio.storage/historystorage.js | 40 +++++++- test/jio.storage/historystorage.tests.js | 115 ++++++++++++++++++++++- 2 files changed, 153 insertions(+), 2 deletions(-) diff --git a/src/jio.storage/historystorage.js b/src/jio.storage/historystorage.js index c7d33a1..5a74d38 100644 --- a/src/jio.storage/historystorage.js +++ b/src/jio.storage/historystorage.js @@ -15,6 +15,13 @@ return timestamp + "-" + uuid; } + function isTimestamp(id) { + //A timestamp is of the form + //"[13 digit number]-[4 numbers/lowercase letters]" + var re = /^[0-9]{13}-[a-z0-9]{4}$/; + return re.test(id); + } + function removeOldRevs( substorage, results, @@ -193,7 +200,8 @@ }; HistoryStorage.prototype.put = function (id, data) { - var timestamp = generateUniqueTimestamp(Date.now()), + var substorage = this._sub_storage, + timestamp = generateUniqueTimestamp(Date.now()), metadata = { // XXX: remove this attribute once query can sort_on id timestamp: timestamp, @@ -201,6 +209,22 @@ doc: data, op: "put" }; + + if (this._include_revisions && isTimestamp(id)) { + return substorage.get(id) + .push(function (metadata) { + metadata.timestamp = timestamp; + metadata.doc = data; + return substorage.put(timestamp, metadata); + }, + function (error) { + if (error.status_code === 404 && + error instanceof jIO.util.jIOError) { + return substorage.put(timestamp, metadata); + } + throw error; + }); + } return this._sub_storage.put(timestamp, metadata); }; @@ -343,6 +367,20 @@ op: "putAttachment" }, substorage = this._sub_storage; + + if (this._include_revisions && isTimestamp(id)) { + return substorage.get(id) + .push(function (metadata) { + metadata.timestamp = timestamp; + metadata.name = name; + }, + function (error) { + if (!(error.status_code === 404 && + error instanceof jIO.util.jIOError)) { + throw error; + } + }); + } return this._sub_storage.put(timestamp, metadata) .push(function () { return substorage.putAttachment(timestamp, name, blob); diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index b7f5ecb..3da57f3 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -832,7 +832,7 @@ }) .push(function (res) { timestamp = res.data.rows[0].id; - return history.put(timestamp, {key: "val"}); + return jio.put(timestamp, {key: "val"}); }) .push(function () { return jio.get("doc"); @@ -1001,6 +1001,119 @@ .always(function () {start(); }); }); + test("Updating a document with include revisions", + function () { + stop(); + expect(1); + var jio = this.jio, + history = this.history, + not_history = this.not_history, + timestamps, + t_id; + jio.put("doc", {title: "version0"}) + .push(function () { + return history.put("doc", {title: "version1"}); + }) + .push(function () { + return not_history.allDocs({sort_on: [["timestamp", "ascending"]]}); + }) + .push(function (results) { + t_id = results.data.rows[0].id; + return history.put(t_id, {title: "version0.1"}); + }) + .push(function () { + return jio.put(t_id, {title: "label0"}); + }) + .push(function () { + return history.put("1234567891012-abcd", {k: "v"}); + }) + .push(function () { + return not_history.allDocs({ + select_list: ["timestamp"] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]], + select_list: ["timestamp", "op", "doc_id", "doc"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: timestamps[0], + doc: {}, + value: { + timestamp: timestamps[0], + op: "put", + doc_id: "doc", + doc: { + title: "version0" + } + } + }, + { + id: timestamps[1], + doc: {}, + value: { + timestamp: timestamps[1], + op: "put", + doc_id: "doc", + doc: { + title: "version1" + } + } + }, + { + id: timestamps[2], + doc: {}, + value: { + timestamp: timestamps[2], + op: "put", + doc_id: "doc", + doc: { + title: "version0.1" + } + } + }, + { + id: timestamps[3], + doc: {}, + value: { + timestamp: timestamps[3], + op: "put", + doc_id: timestamps[0], + doc: { + title: "label0" + } + } + }, + { + id: timestamps[4], + doc: {}, + value: { + timestamp: timestamps[4], + op: "put", + doc_id: "1234567891012-abcd", + doc: { + k: "v" + } + } + } + ], "Documents stored with correct metadata"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + test("Retrieving older revisions with get", function () { stop(); -- 2.30.9 From a1d4c5063f6527e8578838c72ab454ce470df715 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Fri, 3 Aug 2018 08:54:47 +0000 Subject: [PATCH 43/46] Changed behavior of allDocs with include_revisions. --- src/jio.storage/historystorage.js | 31 +++++++------ test/jio.storage/historystorage.tests.js | 57 ++++++++++++++++++++++++ test/jio.storage/tmp.txt | 0 3 files changed, 74 insertions(+), 14 deletions(-) delete mode 100644 test/jio.storage/tmp.txt diff --git a/src/jio.storage/historystorage.js b/src/jio.storage/historystorage.js index 5a74d38..1103a21 100644 --- a/src/jio.storage/historystorage.js +++ b/src/jio.storage/historystorage.js @@ -240,8 +240,6 @@ }; HistoryStorage.prototype.allAttachments = function (id) { - // XXX: allAttachments with timestamp: - // should return all non-removed attachments at this point in time var substorage = this._sub_storage, query_obj, query_removed_check, @@ -251,6 +249,8 @@ include_revs = this._include_revisions, have_seen_id = false; + // id is a timestamp, and allAttachments will return attachment versions + // up-to-and-including those made at time id if (include_revs) { query_doc_id = new SimpleQuery({ operator: "<=", @@ -290,7 +290,6 @@ ] }); - options_remcheck = { query: query_removed_check, select_list: ["op", "timestamp"], @@ -388,6 +387,8 @@ }; HistoryStorage.prototype.getAttachment = function (id, name) { + // In this case, id is a timestamp, so return attachment version at that + // time if (this._include_revisions) { return this._sub_storage.getAttachment(id, name) .push(undefined, function (error) { @@ -467,7 +468,10 @@ }; HistoryStorage.prototype.hasCapacity = function (name) { - return name === 'list' || name === 'include'; + return name === 'list' || + name === 'include' || + name === 'query' || + name === 'select'; }; HistoryStorage.prototype.buildQuery = function (options) { @@ -477,7 +481,6 @@ if (options.sort_on === undefined) {options.sort_on = []; } if (options.select_list === undefined) {options.select_list = []; } options.query = jIO.QueryFactory.create(options.query); - var meta_options = { query: "", sort_on: [["timestamp", "descending"]], @@ -485,6 +488,10 @@ }, include_revs = this._include_revisions; + if (include_revs) {// && options.query.key === "doc_id") { + meta_options.query = options.query; + } + return this._sub_storage.allDocs(meta_options) .push(function (results) { results = results.data.rows; @@ -588,23 +595,19 @@ // Put into correct format to be passed back to query storage .map(function (docum) { - docum.doc = docum.value.doc; + if (include_revs) { docum.id = docum.value.timestamp; + //docum.doc = docum.value.doc; } else { docum.id = docum.value.doc_id; + //docum.doc = docum.value.doc; } delete docum.value.doc_id; delete docum.value.timestamp; delete docum.value.op; - - if (options.include_docs) { - docum.doc = docum.value.doc; - } else { - docum.doc = {}; - } - - docum.value = {}; + docum.value = docum.value.doc; + //docum.value = {}; return docum; }); return docs_to_query; diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js index 3da57f3..79d7844 100644 --- a/test/jio.storage/historystorage.tests.js +++ b/test/jio.storage/historystorage.tests.js @@ -2265,6 +2265,63 @@ .always(function () {start(); }); }); + test("allDocs with include_revisions only one document", + function () { + stop(); + expect(1); + var jio = this.jio, + history = this.history, + timestamps, + not_history = this.not_history; + + jio.put("doc a", {title: "foo0"}) + .push(function () { + return jio.put("doc a", {title: "foo1"}); + }) + .push(function () { + return jio.put("doc b", {title: "bar0"}); + }) + .push(function () { + return jio.put("doc b", {title: "bar1"}); + }) + .push(function () { + return not_history.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return history.allDocs({ + query: 'doc_id: "doc a"', + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: timestamps[1], + doc: {}, + value: {title: "foo1"} + }, + { + id: timestamps[0], + doc: {}, + value: {title: "foo0"} + }], + "Only specified document revision history is returned" + ); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); + test("Parallel edits will not break anything", function () { stop(); diff --git a/test/jio.storage/tmp.txt b/test/jio.storage/tmp.txt deleted file mode 100644 index e69de29..0000000 -- 2.30.9 From 1f034e93f43bf41a5d5ba70982de15171a0468aa Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Fri, 3 Aug 2018 09:01:00 +0000 Subject: [PATCH 44/46] Changed the name of historystorage. --- src/jio.storage/historystorage.js | 619 --- src/jio.storage/revisionstorage.js | 1577 +++---- test/jio.storage/historystorage.tests.js | 3267 -------------- test/jio.storage/revisionstorage.tests.js | 4990 +++++++++++++-------- 4 files changed, 3745 insertions(+), 6708 deletions(-) delete mode 100644 src/jio.storage/historystorage.js delete mode 100644 test/jio.storage/historystorage.tests.js diff --git a/src/jio.storage/historystorage.js b/src/jio.storage/historystorage.js deleted file mode 100644 index 1103a21..0000000 --- a/src/jio.storage/historystorage.js +++ /dev/null @@ -1,619 +0,0 @@ -/*jslint nomen: true*/ -/*global RSVP, SimpleQuery, ComplexQuery*/ -(function (jIO, RSVP, SimpleQuery, ComplexQuery) { - "use strict"; - - // Used to distinguish between operations done within the same millisecond - function generateUniqueTimestamp(time) { - - // XXX: replace this with UUIDStorage function call to S4() when it becomes - // publicly accessible - var uuid = ('0000' + Math.floor(Math.random() * 0x10000) - .toString(16)).slice(-4), - //timestamp = Date.now().toString(); - timestamp = time.toString(); - return timestamp + "-" + uuid; - } - - function isTimestamp(id) { - //A timestamp is of the form - //"[13 digit number]-[4 numbers/lowercase letters]" - var re = /^[0-9]{13}-[a-z0-9]{4}$/; - return re.test(id); - } - - function removeOldRevs( - substorage, - results, - keepDoc - ) { - var ind, - promises = [], - seen = {}, - docum, - log, - start_ind, - new_promises, - doc_id, - checkIsId, - removeDoc; - for (ind = 0; ind < results.data.rows.length; ind += 1) { - docum = results.data.rows[ind]; - // Count the number of revisions of each document, and delete older - // ones. - if (!seen.hasOwnProperty(docum.value.doc_id)) { - seen[docum.value.doc_id] = {count: 0}; - } - log = seen[docum.value.doc_id]; - log.count += 1; - //log.id = docum.id; - - // Record the index of the most recent edit that is before the cutoff - if (!log.hasOwnProperty("s") && !keepDoc({doc: docum, log: log})) { - log.s = ind; - } - - // Record the index of the most recent put or remove - if ((!log.hasOwnProperty("pr")) && - (docum.value.op === "put" || docum.value.op === "remove")) { - log.pr = ind; - log.final = ind; - } - - if ((docum.op === "putAttachment" || docum.op === "removeAttachment") && - log.hasOwnProperty(docum.name) && - !log[docum.name].hasOwnProperty("prA")) { - log[docum.name].prA = ind; - log.final = ind; - } - } - checkIsId = function (d) { - return d.value.doc_id === doc_id; - }; - removeDoc = function (d) { - return substorage.remove(d.id); - }; - for (doc_id in seen) { - if (seen.hasOwnProperty(doc_id)) { - log = seen[doc_id]; - start_ind = Math.max(log.s, log.final + 1); - new_promises = results.data.rows - .slice(start_ind) - .filter(checkIsId) - .map(removeDoc); - promises = promises.concat(new_promises); - } - } - return RSVP.all(promises); - } - - function throwCantFindError(id) { - throw new jIO.util.jIOError( - "HistoryStorage: cannot find object '" + id + "'", - 404 - ); - } - - function throwRemovedError(id) { - throw new jIO.util.jIOError( - "HistoryStorage: cannot find object '" + id + "' (removed)", - 404 - ); - } - - /** - * The jIO HistoryStorage extension - * - * @class HistoryStorage - * @constructor - */ - function HistoryStorage(spec) { - this._sub_storage = jIO.createJIO(spec.sub_storage); - if (spec.hasOwnProperty("include_revisions")) { - this._include_revisions = spec.include_revisions; - } else { - this._include_revisions = false; - } - var substorage = this._sub_storage; - this.packOldRevisions = function (save_info) { - /** - save_info has this form: - { - keep_latest_num: 10, - keep_active_revs: timestamp - } - keep_latest_num = x: keep at most the x latest copies of each unique doc - keep_active_revs = x: throw away all outdated revisions from before x - **/ - var options = { - sort_on: [["timestamp", "descending"]], - select_list: ["doc", "doc_id", "op"] - }, - keep_fixed_num = save_info.hasOwnProperty("keep_latest_num"); - return substorage.allDocs(options) - .push(function (results) { - if (keep_fixed_num) { - return removeOldRevs(substorage, results, function (data) { - return data.log.count <= save_info.keep_latest_num; - }); - } - return removeOldRevs(substorage, results, function (data) { - return data.doc.id > save_info.keep_active_revs; - }); - }); - }; - } - - HistoryStorage.prototype.get = function (id_in) { - - // Query to get the last edit made to this document - var substorage = this._sub_storage, - doc_id_query, - metadata_query, - options; - - if (this._include_revisions) { - doc_id_query = new SimpleQuery({ - operator: "<=", - key: "timestamp", - value: id_in - }); - } else { - doc_id_query = new SimpleQuery({key: "doc_id", value: id_in}); - } - - // Include id_in as value in query object for safety - metadata_query = new ComplexQuery({ - operator: "AND", - query_list: [ - doc_id_query, - new ComplexQuery({ - operator: "OR", - query_list: [ - new SimpleQuery({key: "op", value: "remove"}), - new SimpleQuery({key: "op", value: "put"}) - ] - }) - ] - }); - options = { - query: metadata_query, - select_list: ["op"], - limit: [0, 1], - sort_on: [["timestamp", "descending"]] - }; - - - return substorage.allDocs(options) - .push(function (results) { - if (results.data.total_rows > 0) { - if (results.data.rows[0].value.op === "put") { - return substorage.get(results.data.rows[0].id) - .push(function (result) { - return result.doc; - }); - } - throwRemovedError(id_in); - } - throwCantFindError(id_in); - }); - }; - - HistoryStorage.prototype.put = function (id, data) { - var substorage = this._sub_storage, - timestamp = generateUniqueTimestamp(Date.now()), - metadata = { - // XXX: remove this attribute once query can sort_on id - timestamp: timestamp, - doc_id: id, - doc: data, - op: "put" - }; - - if (this._include_revisions && isTimestamp(id)) { - return substorage.get(id) - .push(function (metadata) { - metadata.timestamp = timestamp; - metadata.doc = data; - return substorage.put(timestamp, metadata); - }, - function (error) { - if (error.status_code === 404 && - error instanceof jIO.util.jIOError) { - return substorage.put(timestamp, metadata); - } - throw error; - }); - } - return this._sub_storage.put(timestamp, metadata); - }; - - HistoryStorage.prototype.remove = function (id) { - var timestamp = generateUniqueTimestamp(Date.now() - 1), - metadata = { - // XXX: remove this attribute once query can sort_on id - timestamp: timestamp, - doc_id: id, - op: "remove" - }; - return this._sub_storage.put(timestamp, metadata); - }; - - HistoryStorage.prototype.allAttachments = function (id) { - var substorage = this._sub_storage, - query_obj, - query_removed_check, - options, - query_doc_id, - options_remcheck, - include_revs = this._include_revisions, - have_seen_id = false; - - // id is a timestamp, and allAttachments will return attachment versions - // up-to-and-including those made at time id - if (include_revs) { - query_doc_id = new SimpleQuery({ - operator: "<=", - key: "timestamp", - value: id - }); - } else { - query_doc_id = new SimpleQuery({key: "doc_id", value: id}); - have_seen_id = true; - } - - query_removed_check = new ComplexQuery({ - operator: "AND", - query_list: [ - query_doc_id, - new ComplexQuery({ - operator: "OR", - query_list: [ - new SimpleQuery({key: "op", value: "put"}), - new SimpleQuery({key: "op", value: "remove"}) - ] - }) - ] - }); - - query_obj = new ComplexQuery({ - operator: "AND", - query_list: [ - query_doc_id, - new ComplexQuery({ - operator: "OR", - query_list: [ - new SimpleQuery({key: "op", value: "putAttachment"}), - new SimpleQuery({key: "op", value: "removeAttachment"}) - ] - }) - ] - }); - - options_remcheck = { - query: query_removed_check, - select_list: ["op", "timestamp"], - limit: [0, 1], - sort_on: [["timestamp", "descending"]] - }; - options = { - query: query_obj, - sort_on: [["timestamp", "descending"]], - select_list: ["op", "name"] - }; - - return this._sub_storage.allDocs(options_remcheck) - // Check the document exists and is not removed - .push(function (results) { - if (results.data.total_rows > 0) { - if (results.data.rows[0].id === id) { - have_seen_id = true; - } - if (results.data.rows[0].value.op === "remove") { - throwRemovedError(id); - } - } else { - throwCantFindError(id); - } - }) - .push(function () { - return substorage.allDocs(options); - }) - .push(function (results) { - var seen = {}, - attachments = [], - attachment_promises = [], - ind, - entry; - - // If input mapped to a real timestamp, then the first query result must - // have the inputted id. Otherwise, unexpected results could arise - // by inputting nonsensical strings as id when include_revisions = true - if (include_revs && - results.data.total_rows > 0 && - results.data.rows[0].id !== id && - !have_seen_id) { - throwCantFindError(id); - } - - - // Only return attachments if: - // (it is the most recent revision) AND (it is a putAttachment) - attachments = results.data.rows.filter(function (docum) { - if (!seen.hasOwnProperty(docum.value.name)) { - var output = (docum.value.op === "putAttachment"); - seen[docum.value.name] = {}; - return output; - } - }); - // Assembles object of attachment_name: attachment_object - for (ind = 0; ind < attachments.length; ind += 1) { - entry = attachments[ind]; - attachment_promises[entry.value.name] = - substorage.getAttachment(entry.id, entry.value.name); - } - return RSVP.hash(attachment_promises); - }); - }; - - HistoryStorage.prototype.putAttachment = function (id, name, blob) { - var timestamp = generateUniqueTimestamp(Date.now()), - metadata = { - // XXX: remove this attribute once query can sort_on id - timestamp: timestamp, - doc_id: id, - name: name, - op: "putAttachment" - }, - substorage = this._sub_storage; - - if (this._include_revisions && isTimestamp(id)) { - return substorage.get(id) - .push(function (metadata) { - metadata.timestamp = timestamp; - metadata.name = name; - }, - function (error) { - if (!(error.status_code === 404 && - error instanceof jIO.util.jIOError)) { - throw error; - } - }); - } - return this._sub_storage.put(timestamp, metadata) - .push(function () { - return substorage.putAttachment(timestamp, name, blob); - }); - }; - HistoryStorage.prototype.getAttachment = function (id, name) { - - // In this case, id is a timestamp, so return attachment version at that - // time - if (this._include_revisions) { - return this._sub_storage.getAttachment(id, name) - .push(undefined, function (error) { - if (error.status_code === 404 && - error instanceof jIO.util.jIOError) { - throwCantFindError(id); - } - throw error; - }); - } - - // Query to get the last edit made to this document - var substorage = this._sub_storage, - - // "doc_id: id AND - // (op: remove OR ((op: putAttachment OR op: removeAttachment) AND - // name: name))" - metadata_query = new ComplexQuery({ - operator: "AND", - query_list: [ - new SimpleQuery({key: "doc_id", value: id}), - new ComplexQuery({ - operator: "OR", - query_list: [ - new SimpleQuery({key: "op", value: "remove"}), - new ComplexQuery({ - operator: "AND", - query_list: [ - new ComplexQuery({ - operator: "OR", - query_list: [ - new SimpleQuery({key: "op", value: "putAttachment"}), - new SimpleQuery({key: "op", value: "removeAttachment"}) - ] - }), - new SimpleQuery({key: "name", value: name}) - ] - }) - ] - }) - ] - }), - options = { - query: metadata_query, - sort_on: [["timestamp", "descending"]], - limit: [0, 1], - select_list: ["op", "name"] - }; - return substorage.allDocs(options) - .push(function (results) { - if (results.data.total_rows > 0) { - // XXX: issue if attachments are put on a removed document - if (results.data.rows[0].value.op === "remove" || - results.data.rows[0].value.op === "removeAttachment") { - throwRemovedError(id); - } - return substorage.getAttachment(results.data.rows[0].id, name); - } - throwCantFindError(id); - }); - }; - - HistoryStorage.prototype.removeAttachment = function (id, name) { - var timestamp = generateUniqueTimestamp(Date.now()), - metadata = { - // XXX: remove this attribute once query can sort_on id - timestamp: timestamp, - doc_id: id, - name: name, - op: "removeAttachment" - }; - return this._sub_storage.put(timestamp, metadata); - }; - - HistoryStorage.prototype.repair = function () { - return this._sub_storage.repair.apply(this._sub_storage, arguments); - }; - - HistoryStorage.prototype.hasCapacity = function (name) { - return name === 'list' || - name === 'include' || - name === 'query' || - name === 'select'; - }; - - HistoryStorage.prototype.buildQuery = function (options) { - // Set default values - if (options === undefined) {options = {}; } - if (options.query === undefined) {options.query = ""; } - if (options.sort_on === undefined) {options.sort_on = []; } - if (options.select_list === undefined) {options.select_list = []; } - options.query = jIO.QueryFactory.create(options.query); - var meta_options = { - query: "", - sort_on: [["timestamp", "descending"]], - select_list: ["doc", "op", "doc_id", "timestamp"] - }, - include_revs = this._include_revisions; - - if (include_revs) {// && options.query.key === "doc_id") { - meta_options.query = options.query; - } - - return this._sub_storage.allDocs(meta_options) - .push(function (results) { - results = results.data.rows; - var seen = {}, - docs_to_query, - i; - - if (include_revs) { - - // We only query on versions mapping to puts or putAttachments - results = results.map(function (docum, ind) { - var data_key; - if (docum.value.op === "put") { - return docum; - } - if (docum.value.op === "remove") { - docum.value.doc = {}; - return docum; - } - if (docum.value.op === "putAttachment" || - docum.value.op === "removeAttachment") { - - // putAttachment document does not contain doc metadata, so we - // add it from the most recent non-removed put on same id - docum.value.doc = {}; - for (i = ind + 1; i < results.length; i += 1) { - if (results[i].value.doc_id === docum.value.doc_id) { - if (results[i].value.op === "put") { - for (data_key in results[i].value.doc) { - if (results[i].value.doc.hasOwnProperty(data_key)) { - docum.value.doc[data_key] = - results[i].value.doc[data_key]; - } - } - return docum; - } - // If most recent metadata edit before the attachment edit - // was a remove, then leave doc empty - if (results[i].value.op === "remove") { - return docum; - } - } - } - } - return false; - }); - } else { - - // Only query on latest revisions of non-removed documents/attachment - // edits - results = results.map(function (docum, ind) { - var data_key; - if (docum.value.op === "put") { - // Mark as read and include in query - if (!seen.hasOwnProperty(docum.value.doc_id)) { - seen[docum.value.doc_id] = {}; - return docum; - } - - } else if (docum.value.op === "remove" || - docum.value.op === "removeAttachment") { - // Mark as read but do not include in query - seen[docum.value.doc_id] = {}; - - } else if (docum.value.op === "putAttachment") { - // If latest edit, mark as read, add document metadata from most - // recent put, and add to query - if (!seen.hasOwnProperty(docum.value.doc_id)) { - seen[docum.value.doc_id] = {}; - docum.value.doc = {}; - for (i = ind + 1; i < results.length; i += 1) { - if (results[i].value.doc_id === docum.value.doc_id) { - if (results[i].value.op === "put") { - for (data_key in results[i].value.doc) { - if (results[i].value.doc.hasOwnProperty(data_key)) { - docum.value.doc[data_key] = - results[i].value.doc[data_key]; - } - } - return docum; - } - if (results[i].value.op === "remove") { - // If most recent edit on document was a remove before - // this attachment, then don't include attachment in query - return false; - } - docum.value.doc = {}; - } - } - } - } - return false; - }); - } - docs_to_query = results - - // Filter out all docs flagged as false in previous map call - .filter(function (docum) { - return docum; - }) - - // Put into correct format to be passed back to query storage - .map(function (docum) { - - if (include_revs) { - docum.id = docum.value.timestamp; - //docum.doc = docum.value.doc; - } else { - docum.id = docum.value.doc_id; - //docum.doc = docum.value.doc; - } - delete docum.value.doc_id; - delete docum.value.timestamp; - delete docum.value.op; - docum.value = docum.value.doc; - //docum.value = {}; - return docum; - }); - return docs_to_query; - }); - }; - - jIO.addStorage('history', HistoryStorage); - -}(jIO, RSVP, SimpleQuery, ComplexQuery)); \ No newline at end of file diff --git a/src/jio.storage/revisionstorage.js b/src/jio.storage/revisionstorage.js index 2e85aa8..286e3e2 100644 --- a/src/jio.storage/revisionstorage.js +++ b/src/jio.storage/revisionstorage.js @@ -1,1064 +1,619 @@ -/*jslint indent: 2, maxlen: 80, nomen: true */ -/*global jIO, hex_sha256, define */ - -/** - * JIO Revision Storage. - * It manages document version and can generate conflicts. - * Description: - * { - * "type": "revision", - * "sub_storage": - * } - */ -// define([module_name], [dependencies], module); -(function (dependencies, module) { - "use strict"; - if (typeof define === 'function' && define.amd) { - return define(dependencies, module); - } - module(jIO, {hex_sha256: hex_sha256}); -}(['jio', 'sha256'], function (jIO, sha256) { +/*jslint nomen: true*/ +/*global RSVP, SimpleQuery, ComplexQuery*/ +(function (jIO, RSVP, SimpleQuery, ComplexQuery) { "use strict"; - var tool = { - "readBlobAsBinaryString": jIO.util.readBlobAsBinaryString, - "uniqueJSONStringify": jIO.util.uniqueJSONStringify - }; - - jIO.addStorage("revision", function (spec) { - - var that = this, priv = {}; - spec = spec || {}; - // ATTRIBUTES // - priv.doc_tree_suffix = ".revision_tree.json"; - priv.sub_storage = spec.sub_storage; - // METHODS // - /** - * Clones an object in deep (without functions) - * @method clone - * @param {any} object The object to clone - * @return {any} The cloned object - */ - priv.clone = function (object) { - var tmp = JSON.stringify(object); - if (tmp === undefined) { - return undefined; - } - return JSON.parse(tmp); - }; + // Used to distinguish between operations done within the same millisecond + function generateUniqueTimestamp(time) { - /** - * Generate a new uuid - * @method generateUuid - * @return {string} The new uuid - */ - priv.generateUuid = function () { - var S4 = function () { - /* 65536 */ - var i, string = Math.floor( - Math.random() * 0x10000 - ).toString(16); - for (i = string.length; i < 4; i += 1) { - string = '0' + string; - } - return string; - }; - return S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + - S4() + S4(); - }; - - /** - * Generates a hash code of a string - * @method hashCode - * @param {string} string The string to hash - * @return {string} The string hash code - */ - priv.hashCode = function (string) { - return sha256.hex_sha256(string); - }; - - /** - * Checks a revision format - * @method checkDocumentRevisionFormat - * @param {object} doc The document object - * @return {object} null if ok, else error object - */ - priv.checkDocumentRevisionFormat = function (doc) { - var send_error = function (message) { - return { - "status": 409, - "message": message, - "reason": "Wrong revision" - }; - }; - if (typeof doc._rev === "string") { - if (/^[0-9]+-[0-9a-zA-Z]+$/.test(doc._rev) === false) { - return send_error("The document revision does not match " + - "^[0-9]+-[0-9a-zA-Z]+$"); - } - } - if (typeof doc._revs === "object") { - if (typeof doc._revs.start !== "number" || - typeof doc._revs.ids !== "object" || - typeof doc._revs.ids.length !== "number") { - return send_error( - "The document revision history is not well formated" - ); - } - } - if (typeof doc._revs_info === "object") { - if (typeof doc._revs_info.length !== "number") { - return send_error("The document revision information " + - "is not well formated"); - } - } - }; + // XXX: replace this with UUIDStorage function call to S4() when it becomes + // publicly accessible + var uuid = ('0000' + Math.floor(Math.random() * 0x10000) + .toString(16)).slice(-4), + //timestamp = Date.now().toString(); + timestamp = time.toString(); + return timestamp + "-" + uuid; + } - /** - * Creates a new document tree - * @method newDocTree - * @return {object} The new document tree - */ - priv.newDocTree = function () { - return {"children": []}; - }; + function isTimestamp(id) { + //A timestamp is of the form + //"[13 digit number]-[4 numbers/lowercase letters]" + var re = /^[0-9]{13}-[a-z0-9]{4}$/; + return re.test(id); + } - /** - * Convert revs_info to a simple revisions history - * @method revsInfoToHistory - * @param {array} revs_info The revs info - * @return {object} The revisions history - */ - priv.revsInfoToHistory = function (revs_info) { - var i, revisions = { - "start": 0, - "ids": [] - }; - revs_info = revs_info || []; - if (revs_info.length > 0) { - revisions.start = parseInt(revs_info[0].rev.split('-')[0], 10); - } - for (i = 0; i < revs_info.length; i += 1) { - revisions.ids.push(revs_info[i].rev.split('-')[1]); + function removeOldRevs( + substorage, + results, + keepDoc + ) { + var ind, + promises = [], + seen = {}, + docum, + log, + start_ind, + new_promises, + doc_id, + checkIsId, + removeDoc; + for (ind = 0; ind < results.data.rows.length; ind += 1) { + docum = results.data.rows[ind]; + // Count the number of revisions of each document, and delete older + // ones. + if (!seen.hasOwnProperty(docum.value.doc_id)) { + seen[docum.value.doc_id] = {count: 0}; } - return revisions; - }; + log = seen[docum.value.doc_id]; + log.count += 1; + //log.id = docum.id; - /** - * Convert the revision history object to an array of revisions. - * @method revisionHistoryToList - * @param {object} revs The revision history - * @return {array} The revision array - */ - priv.revisionHistoryToList = function (revs) { - var i, start = revs.start, new_list = []; - for (i = 0; i < revs.ids.length; i += 1, start -= 1) { - new_list.push(start + "-" + revs.ids[i]); + // Record the index of the most recent edit that is before the cutoff + if (!log.hasOwnProperty("s") && !keepDoc({doc: docum, log: log})) { + log.s = ind; } - return new_list; - }; - /** - * Convert revision list to revs info. - * @method revisionListToRevsInfo - * @param {array} revision_list The revision list - * @param {object} doc_tree The document tree - * @return {array} The document revs info - */ - priv.revisionListToRevsInfo = function (revision_list, doc_tree) { - var revisionListToRevsInfoRec, revs_info = [], j; - for (j = 0; j < revision_list.length; j += 1) { - revs_info.push({"rev": revision_list[j], "status": "missing"}); + // Record the index of the most recent put or remove + if ((!log.hasOwnProperty("pr")) && + (docum.value.op === "put" || docum.value.op === "remove")) { + log.pr = ind; + log.final = ind; } - revisionListToRevsInfoRec = function (index, doc_tree) { - var child, i; - if (index < 0) { - return; - } - for (i = 0; i < doc_tree.children.length; i += 1) { - child = doc_tree.children[i]; - if (child.rev === revision_list[index]) { - revs_info[index].status = child.status; - revisionListToRevsInfoRec(index - 1, child); - } - } - }; - revisionListToRevsInfoRec(revision_list.length - 1, doc_tree); - return revs_info; - }; - /** - * Update a document metadata revision properties - * @method fillDocumentRevisionProperties - * @param {object} doc The document object - * @param {object} doc_tree The document tree - */ - priv.fillDocumentRevisionProperties = function (doc, doc_tree) { - if (doc._revs_info) { - doc._revs = priv.revsInfoToHistory(doc._revs_info); - } else if (doc._revs) { - doc._revs_info = priv.revisionListToRevsInfo( - priv.revisionHistoryToList(doc._revs), - doc_tree - ); - } else if (doc._rev) { - doc._revs_info = priv.getRevisionInfo(doc._rev, doc_tree); - doc._revs = priv.revsInfoToHistory(doc._revs_info); - } else { - doc._revs_info = []; - doc._revs = {"start": 0, "ids": []}; + if ((docum.op === "putAttachment" || docum.op === "removeAttachment") && + log.hasOwnProperty(docum.name) && + !log[docum.name].hasOwnProperty("prA")) { + log[docum.name].prA = ind; + log.final = ind; } - if (doc._revs.start > 0) { - doc._rev = doc._revs.start + "-" + doc._revs.ids[0]; - } else { - delete doc._rev; + } + checkIsId = function (d) { + return d.value.doc_id === doc_id; + }; + removeDoc = function (d) { + return substorage.remove(d.id); + }; + for (doc_id in seen) { + if (seen.hasOwnProperty(doc_id)) { + log = seen[doc_id]; + start_ind = Math.max(log.s, log.final + 1); + new_promises = results.data.rows + .slice(start_ind) + .filter(checkIsId) + .map(removeDoc); + promises = promises.concat(new_promises); } - }; + } + return RSVP.all(promises); + } - /** - * Generates the next revision of a document. - * @methode generateNextRevision - * @param {object} doc The document metadata - * @param {boolean} deleted_flag The deleted flag - * @return {array} 0:The next revision number and 1:the hash code - */ - priv.generateNextRevision = function (doc, deleted_flag) { - var string, revision_history, revs_info; - doc = priv.clone(doc) || {}; - revision_history = doc._revs; - revs_info = doc._revs_info; - delete doc._rev; - delete doc._revs; - delete doc._revs_info; - string = tool.uniqueJSONStringify(doc) + - tool.uniqueJSONStringify(revision_history) + - JSON.stringify(deleted_flag ? true : false); - revision_history.start += 1; - revision_history.ids.unshift(priv.hashCode(string)); - doc._revs = revision_history; - doc._rev = revision_history.start + "-" + revision_history.ids[0]; - revs_info.unshift({ - "rev": doc._rev, - "status": deleted_flag ? "deleted" : "available" - }); - doc._revs_info = revs_info; - return doc; - }; + function throwCantFindError(id) { + throw new jIO.util.jIOError( + "RevisionStorage: cannot find object '" + id + "'", + 404 + ); + } - /** - * Gets the revs info from the document tree - * @method getRevisionInfo - * @param {string} revision The revision to search for - * @param {object} doc_tree The document tree - * @return {array} The revs info - */ - priv.getRevisionInfo = function (revision, doc_tree) { - var getRevisionInfoRec; - getRevisionInfoRec = function (doc_tree) { - var i, child, revs_info; - for (i = 0; i < doc_tree.children.length; i += 1) { - child = doc_tree.children[i]; - if (child.rev === revision) { - return [{"rev": child.rev, "status": child.status}]; - } - revs_info = getRevisionInfoRec(child); - if (revs_info.length > 0 || revision === undefined) { - revs_info.push({"rev": child.rev, "status": child.status}); - return revs_info; - } - } - return []; - }; - return getRevisionInfoRec(doc_tree); - }; + function throwRemovedError(id) { + throw new jIO.util.jIOError( + "RevisionStorage: cannot find object '" + id + "' (removed)", + 404 + ); + } - priv.updateDocumentTree = function (doc, doc_tree) { - var revs_info, updateDocumentTreeRec; - doc = priv.clone(doc); - revs_info = doc._revs_info; - updateDocumentTreeRec = function (doc_tree, revs_info) { - var i, child, info; - if (revs_info.length === 0) { - return; - } - info = revs_info.pop(); - for (i = 0; i < doc_tree.children.length; i += 1) { - child = doc_tree.children[i]; - if (child.rev === info.rev) { - return updateDocumentTreeRec(child, revs_info); - } - } - doc_tree.children.unshift({ - "rev": info.rev, - "status": info.status, - "children": [] + /** + * The jIO RevisionStorage extension + * + * @class RevisionStorage + * @constructor + */ + function RevisionStorage(spec) { + this._sub_storage = jIO.createJIO(spec.sub_storage); + if (spec.hasOwnProperty("include_revisions")) { + this._include_revisions = spec.include_revisions; + } else { + this._include_revisions = false; + } + var substorage = this._sub_storage; + this.packOldRevisions = function (save_info) { + /** + save_info has this form: + { + keep_latest_num: 10, + keep_active_revs: timestamp + } + keep_latest_num = x: keep at most the x latest copies of each unique doc + keep_active_revs = x: throw away all outdated revisions from before x + **/ + var options = { + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "op"] + }, + keep_fixed_num = save_info.hasOwnProperty("keep_latest_num"); + return substorage.allDocs(options) + .push(function (results) { + if (keep_fixed_num) { + return removeOldRevs(substorage, results, function (data) { + return data.log.count <= save_info.keep_latest_num; + }); + } + return removeOldRevs(substorage, results, function (data) { + return data.doc.id > save_info.keep_active_revs; + }); }); - updateDocumentTreeRec(doc_tree.children[0], revs_info); - }; - updateDocumentTreeRec(doc_tree, priv.clone(revs_info)); }; + } - priv.send = function (command, method, doc, option, callback) { - var storage = command.storage(priv.sub_storage); - function onSuccess(success) { - callback(undefined, success); - } - function onError(err) { - callback(err, undefined); - } - if (method === 'allDocs') { - storage.allDocs(option).then(onSuccess, onError); - } else { - storage[method](doc, option).then(onSuccess, onError); - } - }; + RevisionStorage.prototype.get = function (id_in) { - priv.getWinnerRevsInfo = function (doc_tree) { - var revs_info = [], getWinnerRevsInfoRec; - getWinnerRevsInfoRec = function (doc_tree, tmp_revs_info) { - var i; - if (doc_tree.rev) { - tmp_revs_info.unshift({ - "rev": doc_tree.rev, - "status": doc_tree.status - }); - } - if (doc_tree.children.length === 0) { - if (revs_info.length === 0 || - (revs_info[0].status !== "available" && - tmp_revs_info[0].status === "available") || - (tmp_revs_info[0].status === "available" && - revs_info.length < tmp_revs_info.length)) { - revs_info = priv.clone(tmp_revs_info); - } - } - for (i = 0; i < doc_tree.children.length; i += 1) { - getWinnerRevsInfoRec(doc_tree.children[i], tmp_revs_info); - } - tmp_revs_info.shift(); - }; - getWinnerRevsInfoRec(doc_tree, []); - return revs_info; - }; + // Query to get the last edit made to this document + var substorage = this._sub_storage, + doc_id_query, + metadata_query, + options; - priv.getConflicts = function (revision, doc_tree) { - var conflicts = [], getConflictsRec; - getConflictsRec = function (doc_tree) { - var i; - if (doc_tree.rev === revision) { - return; - } - if (doc_tree.children.length === 0) { - if (doc_tree.status !== "deleted") { - conflicts.push(doc_tree.rev); - } - } - for (i = 0; i < doc_tree.children.length; i += 1) { - getConflictsRec(doc_tree.children[i]); - } + if (this._include_revisions) { + doc_id_query = new SimpleQuery({ + operator: "<=", + key: "timestamp", + value: id_in + }); + } else { + doc_id_query = new SimpleQuery({key: "doc_id", value: id_in}); + } + + // Include id_in as value in query object for safety + metadata_query = new ComplexQuery({ + operator: "AND", + query_list: [ + doc_id_query, + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "remove"}), + new SimpleQuery({key: "op", value: "put"}) + ] + }) + ] + }); + options = { + query: metadata_query, + select_list: ["op"], + limit: [0, 1], + sort_on: [["timestamp", "descending"]] + }; + + + return substorage.allDocs(options) + .push(function (results) { + if (results.data.total_rows > 0) { + if (results.data.rows[0].value.op === "put") { + return substorage.get(results.data.rows[0].id) + .push(function (result) { + return result.doc; + }); + } + throwRemovedError(id_in); + } + throwCantFindError(id_in); + }); + }; + + RevisionStorage.prototype.put = function (id, data) { + var substorage = this._sub_storage, + timestamp = generateUniqueTimestamp(Date.now()), + metadata = { + // XXX: remove this attribute once query can sort_on id + timestamp: timestamp, + doc_id: id, + doc: data, + op: "put" }; - getConflictsRec(doc_tree); - return conflicts.length === 0 ? undefined : conflicts; - }; - priv.get = function (command, doc, option, callback) { - priv.send(command, "get", doc, option, callback); - }; - priv.put = function (command, doc, option, callback) { - priv.send(command, "put", doc, option, callback); - }; - priv.remove = function (command, doc, option, callback) { - priv.send(command, "remove", doc, option, callback); - }; - priv.getAttachment = function (command, attachment, option, callback) { - priv.send(command, "getAttachment", attachment, option, callback); - }; - priv.putAttachment = function (command, attachment, option, callback) { - priv.send(command, "putAttachment", attachment, option, callback); - }; - priv.removeAttachment = function (command, attachment, option, callback) { - priv.send(command, "removeAttachment", attachment, option, callback); - }; + if (this._include_revisions && isTimestamp(id)) { + return substorage.get(id) + .push(function (metadata) { + metadata.timestamp = timestamp; + metadata.doc = data; + return substorage.put(timestamp, metadata); + }, + function (error) { + if (error.status_code === 404 && + error instanceof jIO.util.jIOError) { + return substorage.put(timestamp, metadata); + } + throw error; + }); + } + return this._sub_storage.put(timestamp, metadata); + }; - priv.getDocument = function (command, doc, option, callback) { - doc = priv.clone(doc); - doc._id = doc._id + "." + doc._rev; - delete doc._attachment; - delete doc._rev; - delete doc._revs; - delete doc._revs_info; - priv.get(command, doc, option, callback); - }; - priv.putDocument = function (command, doc, option, callback) { - doc = priv.clone(doc); - doc._id = doc._id + "." + doc._rev; - delete doc._attachment; - delete doc._data; - delete doc._mimetype; - delete doc._content_type; - delete doc._rev; - delete doc._revs; - delete doc._revs_info; - priv.put(command, doc, option, callback); - }; + RevisionStorage.prototype.remove = function (id) { + var timestamp = generateUniqueTimestamp(Date.now() - 1), + metadata = { + // XXX: remove this attribute once query can sort_on id + timestamp: timestamp, + doc_id: id, + op: "remove" + }; + return this._sub_storage.put(timestamp, metadata); + }; - priv.getRevisionTree = function (command, doc, option, callback) { - doc = priv.clone(doc); - doc._id = doc._id + priv.doc_tree_suffix; - priv.get(command, doc, option, function (err, response) { - if (err) { - return callback(err, response); - } - if (response.data && response.data.children) { - response.data.children = JSON.parse(response.data.children); + RevisionStorage.prototype.allAttachments = function (id) { + var substorage = this._sub_storage, + query_obj, + query_removed_check, + options, + query_doc_id, + options_remcheck, + include_revs = this._include_revisions, + have_seen_id = false; + + // id is a timestamp, and allAttachments will return attachment versions + // up-to-and-including those made at time id + if (include_revs) { + query_doc_id = new SimpleQuery({ + operator: "<=", + key: "timestamp", + value: id + }); + } else { + query_doc_id = new SimpleQuery({key: "doc_id", value: id}); + have_seen_id = true; + } + + query_removed_check = new ComplexQuery({ + operator: "AND", + query_list: [ + query_doc_id, + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "put"}), + new SimpleQuery({key: "op", value: "remove"}) + ] + }) + ] + }); + + query_obj = new ComplexQuery({ + operator: "AND", + query_list: [ + query_doc_id, + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "putAttachment"}), + new SimpleQuery({key: "op", value: "removeAttachment"}) + ] + }) + ] + }); + + options_remcheck = { + query: query_removed_check, + select_list: ["op", "timestamp"], + limit: [0, 1], + sort_on: [["timestamp", "descending"]] + }; + options = { + query: query_obj, + sort_on: [["timestamp", "descending"]], + select_list: ["op", "name"] + }; + + return this._sub_storage.allDocs(options_remcheck) + // Check the document exists and is not removed + .push(function (results) { + if (results.data.total_rows > 0) { + if (results.data.rows[0].id === id) { + have_seen_id = true; + } + if (results.data.rows[0].value.op === "remove") { + throwRemovedError(id); + } + } else { + throwCantFindError(id); + } + }) + .push(function () { + return substorage.allDocs(options); + }) + .push(function (results) { + var seen = {}, + attachments = [], + attachment_promises = [], + ind, + entry; + + // If input mapped to a real timestamp, then the first query result must + // have the inputted id. Otherwise, unexpected results could arise + // by inputting nonsensical strings as id when include_revisions = true + if (include_revs && + results.data.total_rows > 0 && + results.data.rows[0].id !== id && + !have_seen_id) { + throwCantFindError(id); + } + + + // Only return attachments if: + // (it is the most recent revision) AND (it is a putAttachment) + attachments = results.data.rows.filter(function (docum) { + if (!seen.hasOwnProperty(docum.value.name)) { + var output = (docum.value.op === "putAttachment"); + seen[docum.value.name] = {}; + return output; + } + }); + // Assembles object of attachment_name: attachment_object + for (ind = 0; ind < attachments.length; ind += 1) { + entry = attachments[ind]; + attachment_promises[entry.value.name] = + substorage.getAttachment(entry.id, entry.value.name); } - return callback(err, response); + return RSVP.hash(attachment_promises); }); - }; + }; - priv.getAttachmentList = function (command, doc, option, callback) { - var attachment_id, dealResults, state = "ok", result_list = [], count = 0; - dealResults = function (attachment_id, attachment_meta) { - return function (err, response) { - if (state !== "ok") { - return; - } - count -= 1; - if (err) { - if (err.status === 404) { - result_list.push(undefined); - } else { - state = "error"; - return callback(err, undefined); + RevisionStorage.prototype.putAttachment = function (id, name, blob) { + var timestamp = generateUniqueTimestamp(Date.now()), + metadata = { + // XXX: remove this attribute once query can sort_on id + timestamp: timestamp, + doc_id: id, + name: name, + op: "putAttachment" + }, + substorage = this._sub_storage; + + if (this._include_revisions && isTimestamp(id)) { + return substorage.get(id) + .push(function (metadata) { + metadata.timestamp = timestamp; + metadata.name = name; + }, + function (error) { + if (!(error.status_code === 404 && + error instanceof jIO.util.jIOError)) { + throw error; } - } - result_list.push({ - "_attachment": attachment_id, - "_data": response.data, - "_content_type": attachment_meta.content_type }); - if (count === 0) { - state = "finished"; - callback(undefined, {"data": result_list}); - } - }; + } + return this._sub_storage.put(timestamp, metadata) + .push(function () { + return substorage.putAttachment(timestamp, name, blob); + }); + }; + RevisionStorage.prototype.getAttachment = function (id, name) { + + // In this case, id is a timestamp, so return attachment version at that + // time + if (this._include_revisions) { + return this._sub_storage.getAttachment(id, name) + .push(undefined, function (error) { + if (error.status_code === 404 && + error instanceof jIO.util.jIOError) { + throwCantFindError(id); + } + throw error; + }); + } + + // Query to get the last edit made to this document + var substorage = this._sub_storage, + + // "doc_id: id AND + // (op: remove OR ((op: putAttachment OR op: removeAttachment) AND + // name: name))" + metadata_query = new ComplexQuery({ + operator: "AND", + query_list: [ + new SimpleQuery({key: "doc_id", value: id}), + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "remove"}), + new ComplexQuery({ + operator: "AND", + query_list: [ + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "putAttachment"}), + new SimpleQuery({key: "op", value: "removeAttachment"}) + ] + }), + new SimpleQuery({key: "name", value: name}) + ] + }) + ] + }) + ] + }), + options = { + query: metadata_query, + sort_on: [["timestamp", "descending"]], + limit: [0, 1], + select_list: ["op", "name"] }; - for (attachment_id in doc._attachments) { - if (doc._attachments.hasOwnProperty(attachment_id)) { - count += 1; - priv.getAttachment( - command, - {"_id": doc._id, "_attachment": attachment_id}, - option, - dealResults(attachment_id, doc._attachments[attachment_id]) - ); - } - } - if (count === 0) { - callback(undefined, {"data": []}); - } - }; - - priv.putAttachmentList = function (command, doc, option, - attachment_list, callback) { - var i, dealResults, state = "ok", count = 0, attachment; - attachment_list = attachment_list || []; - dealResults = function () { - return function (err) { - if (state !== "ok") { - return; - } - count -= 1; - if (err) { - state = "error"; - return callback(err, undefined); - } - if (count === 0) { - state = "finished"; - callback(undefined, {}); + return substorage.allDocs(options) + .push(function (results) { + if (results.data.total_rows > 0) { + // XXX: issue if attachments are put on a removed document + if (results.data.rows[0].value.op === "remove" || + results.data.rows[0].value.op === "removeAttachment") { + throwRemovedError(id); } - }; - }; - for (i = 0; i < attachment_list.length; i += 1) { - attachment = attachment_list[i]; - if (attachment !== undefined) { - count += 1; - attachment._id = doc._id + "." + doc._rev; - priv.putAttachment(command, attachment, option, dealResults(i)); + return substorage.getAttachment(results.data.rows[0].id, name); } - } - if (count === 0) { - return callback(undefined, {}); - } - }; - - priv.putDocumentTree = function (command, doc, option, doc_tree, callback) { - doc_tree = priv.clone(doc_tree); - doc_tree._id = doc._id + priv.doc_tree_suffix; - if (doc_tree.children) { - doc_tree.children = JSON.stringify(doc_tree.children); - } - priv.put(command, doc_tree, option, callback); - }; + throwCantFindError(id); + }); + }; - priv.notFoundError = function (message, reason) { - return { - "status": 404, - "message": message, - "reason": reason + RevisionStorage.prototype.removeAttachment = function (id, name) { + var timestamp = generateUniqueTimestamp(Date.now()), + metadata = { + // XXX: remove this attribute once query can sort_on id + timestamp: timestamp, + doc_id: id, + name: name, + op: "removeAttachment" }; - }; + return this._sub_storage.put(timestamp, metadata); + }; - priv.conflictError = function (message, reason) { - return { - "status": 409, - "message": message, - "reason": reason - }; - }; + RevisionStorage.prototype.repair = function () { + return this._sub_storage.repair.apply(this._sub_storage, arguments); + }; - priv.revisionGenericRequest = function (command, doc, option, - specific_parameter, onEnd) { - var prev_doc, doc_tree, attachment_list, callback = {}; - if (specific_parameter.doc_id) { - doc._id = specific_parameter.doc_id; - } - if (specific_parameter.attachment_id) { - doc._attachment = specific_parameter.attachment_id; - } - callback.begin = function () { - var check_error; - doc._id = doc._id || priv.generateUuid(); // XXX should not generate id - if (specific_parameter.revision_needed && !doc._rev) { - return onEnd(priv.conflictError( - "Document update conflict", - "No document revision was provided" - ), undefined); - } - // check revision format - check_error = priv.checkDocumentRevisionFormat(doc); - if (check_error !== undefined) { - return onEnd(check_error, undefined); - } - priv.getRevisionTree(command, doc, option, callback.getRevisionTree); - }; - callback.getRevisionTree = function (err, response) { - var winner_info, previous_revision, generate_new_revision; - previous_revision = doc._rev; - generate_new_revision = doc._revs || doc._revs_info ? false : true; - if (err) { - if (err.status !== 404) { - err.message = "Cannot get document revision tree"; - return onEnd(err, undefined); - } - } - doc_tree = (response && response.data) || priv.newDocTree(); - if (specific_parameter.get || specific_parameter.getAttachment) { - if (!doc._rev) { - winner_info = priv.getWinnerRevsInfo(doc_tree); - if (winner_info.length === 0) { - return onEnd(priv.notFoundError( - "Document not found", - "missing" - ), undefined); - } - if (winner_info[0].status === "deleted") { - return onEnd(priv.notFoundError( - "Document not found", - "deleted" - ), undefined); - } - doc._rev = winner_info[0].rev; - } - priv.fillDocumentRevisionProperties(doc, doc_tree); - return priv.getDocument(command, doc, option, callback.getDocument); - } - priv.fillDocumentRevisionProperties(doc, doc_tree); - if (generate_new_revision) { - if (previous_revision && doc._revs_info.length === 0) { - // the document history has changed, it means that the document - // revision was wrong. Add a pseudo history to the document - doc._rev = previous_revision; - doc._revs = { - "start": parseInt(previous_revision.split("-")[0], 10), - "ids": [previous_revision.split("-")[1]] - }; - doc._revs_info = [{"rev": previous_revision, "status": "missing"}]; - } - doc = priv.generateNextRevision( - doc, - specific_parameter.remove - ); - } - if (doc._revs_info.length > 1) { - prev_doc = { - "_id": doc._id, - "_rev": doc._revs_info[1].rev - }; - if (!generate_new_revision && specific_parameter.putAttachment) { - prev_doc._rev = doc._revs_info[0].rev; - } - } - // force revs_info status - doc._revs_info[0].status = (specific_parameter.remove ? - "deleted" : "available"); - priv.updateDocumentTree(doc, doc_tree); - if (prev_doc) { - return priv.getDocument(command, prev_doc, - option, callback.getDocument); - } - if (specific_parameter.remove || specific_parameter.removeAttachment) { - return onEnd(priv.notFoundError( - "Unable to remove an inexistent document", - "missing" - ), undefined); - } - priv.putDocument(command, doc, option, callback.putDocument); - }; - callback.getDocument = function (err, res_doc) { - var k, conflicts; - if (err) { - if (err.status === 404) { - if (specific_parameter.remove || - specific_parameter.removeAttachment) { - return onEnd(priv.conflictError( - "Document update conflict", - "Document is missing" - ), undefined); - } - if (specific_parameter.get) { - return onEnd(priv.notFoundError( - "Unable to find the document", - "missing" - ), undefined); + RevisionStorage.prototype.hasCapacity = function (name) { + return name === 'list' || + name === 'include' || + name === 'query' || + name === 'select'; + }; + + RevisionStorage.prototype.buildQuery = function (options) { + // Set default values + if (options === undefined) {options = {}; } + if (options.query === undefined) {options.query = ""; } + if (options.sort_on === undefined) {options.sort_on = []; } + if (options.select_list === undefined) {options.select_list = []; } + options.query = jIO.QueryFactory.create(options.query); + var meta_options = { + query: "", + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "op", "doc_id", "timestamp"] + }, + include_revs = this._include_revisions; + + if (include_revs) {// && options.query.key === "doc_id") { + meta_options.query = options.query; + } + + return this._sub_storage.allDocs(meta_options) + .push(function (results) { + results = results.data.rows; + var seen = {}, + docs_to_query, + i; + + if (include_revs) { + + // We only query on versions mapping to puts or putAttachments + results = results.map(function (docum, ind) { + var data_key; + if (docum.value.op === "put") { + return docum; } - res_doc = {"data": {}}; - } else { - err.message = "Cannot get document"; - return onEnd(err, undefined); - } - } - res_doc = res_doc.data; - if (specific_parameter.get) { - res_doc._id = doc._id; - res_doc._rev = doc._rev; - if (option.conflicts === true) { - conflicts = priv.getConflicts(doc._rev, doc_tree); - if (conflicts) { - res_doc._conflicts = conflicts; + if (docum.value.op === "remove") { + docum.value.doc = {}; + return docum; } - } - if (option.revs === true) { - res_doc._revisions = doc._revs; - } - if (option.revs_info === true) { - res_doc._revs_info = doc._revs_info; - } - return onEnd(undefined, {"data": res_doc}); - } - if (specific_parameter.putAttachment || - specific_parameter.removeAttachment) { - // copy metadata (not beginning by "_" to document - for (k in res_doc) { - if (res_doc.hasOwnProperty(k) && !k.match("^_")) { - doc[k] = res_doc[k]; + if (docum.value.op === "putAttachment" || + docum.value.op === "removeAttachment") { + + // putAttachment document does not contain doc metadata, so we + // add it from the most recent non-removed put on same id + docum.value.doc = {}; + for (i = ind + 1; i < results.length; i += 1) { + if (results[i].value.doc_id === docum.value.doc_id) { + if (results[i].value.op === "put") { + for (data_key in results[i].value.doc) { + if (results[i].value.doc.hasOwnProperty(data_key)) { + docum.value.doc[data_key] = + results[i].value.doc[data_key]; + } + } + return docum; + } + // If most recent metadata edit before the attachment edit + // was a remove, then leave doc empty + if (results[i].value.op === "remove") { + return docum; + } + } + } } - } - } - if (specific_parameter.remove) { - priv.putDocumentTree(command, doc, option, - doc_tree, callback.putDocumentTree); + return false; + }); } else { - priv.getAttachmentList(command, res_doc, option, - callback.getAttachmentList); - } - }; - callback.getAttachmentList = function (err, res_list) { - var i, attachment_found = false; - if (err) { - err.message = "Cannot get attachment"; - return onEnd(err, undefined); - } - res_list = res_list.data; - attachment_list = res_list || []; - if (specific_parameter.getAttachment) { - // getting specific attachment - for (i = 0; i < attachment_list.length; i += 1) { - if (attachment_list[i] && - doc._attachment === - attachment_list[i]._attachment) { - return onEnd(undefined, {"data": attachment_list[i]._data}); - } - } - return onEnd(priv.notFoundError( - "Unable to get an inexistent attachment", - "missing" - ), undefined); - } - if (specific_parameter.remove_from_attachment_list) { - // removing specific attachment - for (i = 0; i < attachment_list.length; i += 1) { - if (attachment_list[i] && - specific_parameter.remove_from_attachment_list._attachment === - attachment_list[i]._attachment) { - attachment_found = true; - attachment_list[i] = undefined; - break; - } - } - if (!attachment_found) { - return onEnd(priv.notFoundError( - "Unable to remove an inexistent attachment", - "missing" - ), undefined); - } - } - priv.putDocument(command, doc, option, callback.putDocument); - }; - callback.putDocument = function (err) { - var i, attachment_found = false; - if (err) { - err.message = "Cannot post the document"; - return onEnd(err, undefined); - } - if (specific_parameter.add_to_attachment_list) { - // adding specific attachment - attachment_list = attachment_list || []; - for (i = 0; i < attachment_list.length; i += 1) { - if (attachment_list[i] && - specific_parameter.add_to_attachment_list._attachment === - attachment_list[i]._attachment) { - attachment_found = true; - attachment_list[i] = specific_parameter.add_to_attachment_list; - break; - } - } - if (!attachment_found) { - attachment_list.unshift(specific_parameter.add_to_attachment_list); - } - } - priv.putAttachmentList( - command, - doc, - option, - attachment_list, - callback.putAttachmentList - ); - }; - callback.putAttachmentList = function (err) { - if (err) { - err.message = "Cannot copy attacments to the document"; - return onEnd(err, undefined); - } - priv.putDocumentTree(command, doc, option, - doc_tree, callback.putDocumentTree); - }; - callback.putDocumentTree = function (err) { - var response_object; - if (err) { - err.message = "Cannot update the document history"; - return onEnd(err, undefined); - } - response_object = { - "id": doc._id, - "rev": doc._rev - }; - if (specific_parameter.putAttachment || - specific_parameter.removeAttachment || - specific_parameter.getAttachment) { - response_object.attachment = doc._attachment; - } - onEnd(undefined, response_object); - // if (option.keep_revision_history !== true) { - // // priv.remove(command, prev_doc, option, function () { - // // - change "available" status to "deleted" - // // - remove attachments - // // - done, no callback - // // }); - // } - }; - callback.begin(); - }; - - /** - * Post the document metadata and create or update a document tree. - * Options: - * - {boolean} keep_revision_history To keep the previous revisions - * (false by default) (NYI). - * @method post - * @param {object} command The JIO command - */ - that.post = function (command, metadata, option) { - priv.revisionGenericRequest( - command, - metadata, - option, - {}, - function (err, response) { - if (err) { - return command.error(err); - } - command.success({"id": response.id, "rev": response.rev}); - } - ); - }; - - /** - * Put the document metadata and create or update a document tree. - * Options: - * - {boolean} keep_revision_history To keep the previous revisions - * (false by default) (NYI). - * @method put - * @param {object} command The JIO command - */ - that.put = function (command, metadata, option) { - priv.revisionGenericRequest( - command, - metadata, - option, - {}, - function (err, response) { - if (err) { - return command.error(err); - } - command.success({"rev": response.rev}); - } - ); - }; + // Only query on latest revisions of non-removed documents/attachment + // edits + results = results.map(function (docum, ind) { + var data_key; + if (docum.value.op === "put") { + // Mark as read and include in query + if (!seen.hasOwnProperty(docum.value.doc_id)) { + seen[docum.value.doc_id] = {}; + return docum; + } - that.putAttachment = function (command, param, option) { - tool.readBlobAsBinaryString(param._blob).then(function (event) { - param._content_type = param._blob.type; - param._data = event.target.result; - delete param._blob; - priv.revisionGenericRequest( - command, - param, - option, - { - "doc_id": param._id, - "attachment_id": param._attachment, - "add_to_attachment_list": { - "_attachment": param._attachment, - "_content_type": param._content_type, - "_data": param._data - }, - "putAttachment": true - }, - function (err, response) { - if (err) { - return command.error(err); + } else if (docum.value.op === "remove" || + docum.value.op === "removeAttachment") { + // Mark as read but do not include in query + seen[docum.value.doc_id] = {}; + + } else if (docum.value.op === "putAttachment") { + // If latest edit, mark as read, add document metadata from most + // recent put, and add to query + if (!seen.hasOwnProperty(docum.value.doc_id)) { + seen[docum.value.doc_id] = {}; + docum.value.doc = {}; + for (i = ind + 1; i < results.length; i += 1) { + if (results[i].value.doc_id === docum.value.doc_id) { + if (results[i].value.op === "put") { + for (data_key in results[i].value.doc) { + if (results[i].value.doc.hasOwnProperty(data_key)) { + docum.value.doc[data_key] = + results[i].value.doc[data_key]; + } + } + return docum; + } + if (results[i].value.op === "remove") { + // If most recent edit on document was a remove before + // this attachment, then don't include attachment in query + return false; + } + docum.value.doc = {}; + } + } + } } - command.success({"rev": response.rev}); - } - ); - }, function () { - command.error("conflict", "broken blob", "Cannot read data to put"); - }); - }; - - that.remove = function (command, param, option) { - priv.revisionGenericRequest( - command, - param, - option, - { - "revision_needed": true, - "remove": true - }, - function (err, response) { - if (err) { - return command.error(err); - } - command.success({"rev": response.rev}); + return false; + }); } - ); - }; + docs_to_query = results - that.removeAttachment = function (command, param, option) { - priv.revisionGenericRequest( - command, - param, - option, - { - "doc_id": param._id, - "attachment_id": param._attachment, - "revision_needed": true, - "removeAttachment": true, - "remove_from_attachment_list": { - "_attachment": param._attachment - } - }, - function (err, response) { - if (err) { - return command.error(err); - } - command.success({"rev": response.rev}); - } - ); - }; + // Filter out all docs flagged as false in previous map call + .filter(function (docum) { + return docum; + }) - that.get = function (command, param, option) { - priv.revisionGenericRequest( - command, - param, - option, - { - "get": true - }, - function (err, response) { - if (err) { - return command.error(err); - } - command.success({"data": response.data}); - } - ); - }; + // Put into correct format to be passed back to query storage + .map(function (docum) { - that.getAttachment = function (command, param, option) { - priv.revisionGenericRequest( - command, - param, - option, - { - "doc_id": param._id, - "attachment_id": param._attachment, - "getAttachment": true - }, - function (err, response) { - if (err) { - return command.error(err); - } - command.success({"data": response.data}); - } - ); - }; - - that.allDocs = function (command, param, option) { - /*jslint unparam: true */ - var rows, result = {"total_rows": 0, "rows": []}, functions = {}; - functions.finished = 0; - functions.falseResponseGenerator = function (response, callback) { - callback(undefined, response); - }; - functions.fillResultGenerator = function (doc_id) { - return function (err, doc_tree) { - var document_revision, row, revs_info; - if (err) { - return command.error(err); - } - doc_tree = doc_tree.data; - if (typeof doc_tree.children === 'string') { - doc_tree.children = JSON.parse(doc_tree.children); - } - revs_info = priv.getWinnerRevsInfo(doc_tree); - document_revision = - rows.document_revisions[doc_id + "." + revs_info[0].rev]; - if (document_revision) { - row = { - "id": doc_id, - "key": doc_id, - "value": { - "rev": revs_info[0].rev - } - }; - if (document_revision.doc && option.include_docs) { - document_revision.doc._id = doc_id; - document_revision.doc._rev = revs_info[0].rev; - row.doc = document_revision.doc; - } - result.rows.push(row); - result.total_rows += 1; - } - functions.success(); - }; - }; - functions.success = function () { - functions.finished -= 1; - if (functions.finished === 0) { - command.success({"data": result}); - } - }; - priv.send(command, "allDocs", null, option, function (err, response) { - var i, row, selector, selected; - if (err) { - return command.error(err); - } - response = response.data; - selector = /\.revision_tree\.json$/; - rows = { - "revision_trees": { - // id.revision_tree.json: { - // id: blabla - // doc: {...} - // } - }, - "document_revisions": { - // id.rev: { - // id: blabla - // rev: 1-1 - // doc: {...} - // } - } - }; - while (response.rows.length > 0) { - // filling rows - row = response.rows.shift(); - selected = selector.exec(row.id); - if (selected) { - selected = selected.input.substring(0, selected.index); - // this is a revision tree - rows.revision_trees[row.id] = { - "id": selected - }; - if (row.doc) { - rows.revision_trees[row.id].doc = row.doc; - } - } else { - // this is a simple revision - rows.document_revisions[row.id] = { - "id": row.id.split(".").slice(0, -1), - "rev": row.id.split(".").slice(-1) - }; - if (row.doc) { - rows.document_revisions[row.id].doc = row.doc; - } - } - } - functions.finished += 1; - for (i in rows.revision_trees) { - if (rows.revision_trees.hasOwnProperty(i)) { - functions.finished += 1; - if (rows.revision_trees[i].doc) { - functions.falseResponseGenerator( - {"data": rows.revision_trees[i].doc}, - functions.fillResultGenerator(rows.revision_trees[i].id) - ); + if (include_revs) { + docum.id = docum.value.timestamp; + //docum.doc = docum.value.doc; } else { - priv.getRevisionTree( - command, - {"_id": rows.revision_trees[i].id}, - option, - functions.fillResultGenerator(rows.revision_trees[i].id) - ); + docum.id = docum.value.doc_id; + //docum.doc = docum.value.doc; } - } - } - functions.success(); + delete docum.value.doc_id; + delete docum.value.timestamp; + delete docum.value.op; + docum.value = docum.value.doc; + //docum.value = {}; + return docum; + }); + return docs_to_query; }); - }; - - // XXX - that.check = function (command) { - command.success(); - }; - - // XXX - that.repair = function (command) { - command.success(); - }; + }; - }); // end RevisionStorage + jIO.addStorage('revision', RevisionStorage); -})); +}(jIO, RSVP, SimpleQuery, ComplexQuery)); \ No newline at end of file diff --git a/test/jio.storage/historystorage.tests.js b/test/jio.storage/historystorage.tests.js deleted file mode 100644 index 79d7844..0000000 --- a/test/jio.storage/historystorage.tests.js +++ /dev/null @@ -1,3267 +0,0 @@ -/*jslint nomen: true*/ -/*global Blob*/ -(function (jIO, RSVP, Blob, QUnit) { - "use strict"; - var test = QUnit.test, - stop = QUnit.stop, - start = QUnit.start, - ok = QUnit.ok, - expect = QUnit.expect, - deepEqual = QUnit.deepEqual, - equal = QUnit.equal, - module = QUnit.module; - - function putFullDoc(storage, id, doc, attachment_name, attachment) { - return storage.put(id, doc) - .push(function () { - return storage.putAttachment( - id, - attachment_name, - attachment - ); - }); - } - - module("HistoryStorage.post", { - setup: function () { - // create storage of type "history" with memory as substorage - var dbname = "db_" + Date.now(); - this.jio = jIO.createJIO({ - type: "uuid", - sub_storage: { - type: "query", - sub_storage: { - type: "history", - sub_storage: { - type: "query", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - } - } - }); - this.history = jIO.createJIO({ - type: "uuid", - sub_storage: { - type: "query", - sub_storage: { - type: "history", - include_revisions: true, - sub_storage: { - type: "query", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - } - } - }); - this.not_history = jIO.createJIO({ - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - }); - } - }); - - test("Verifying simple post works", - function () { - stop(); - expect(2); - var jio = this.jio, - history = this.history, - not_history = this.not_history, - timestamps; - - return jio.post({title: "foo0"}) - .push(function (result) { - //id = result; - return jio.put(result, {title: "foo1"}); - }) - .push(function (result) { - return jio.get(result); - }) - .push(function (res) { - deepEqual(res, { - title: "foo1" - }, "history storage only retrieves latest version"); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) - .push(function () { - return history.allDocs({select_list: ["title"]}); - }) - .push(function (res) { - deepEqual(res.data.rows, [ - { - value: { - title: "foo1" - }, - doc: {}, - id: timestamps[1] - }, - { - value: { - title: "foo0" - }, - doc: {}, - id: timestamps[0] - } - ], - "Two revisions logged with correct metadata"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - - ///////////////////////////////////////////////////////////////// - // Attachments - ///////////////////////////////////////////////////////////////// - - module("HistoryStorage.attachments", { - setup: function () { - // create storage of type "history" with memory as substorage - var dbname = "db_" + Date.now(); - this.blob1 = new Blob(['a']); - this.blob2 = new Blob(['b']); - this.blob3 = new Blob(['ccc']); - this.other_blob = new Blob(['1']); - this.jio = jIO.createJIO({ - type: "query", - sub_storage: { - type: "history", - sub_storage: { - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - } - } - }); - this.history = jIO.createJIO({ - type: "query", - sub_storage: { - type: "history", - include_revisions: true, - sub_storage: { - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - } - } - }); - this.not_history = jIO.createJIO({ - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - }); - } - }); - - test("Testing proper adding/removing attachments", - function () { - stop(); - expect(10); - var jio = this.jio, - history = this.history, - not_history = this.not_history, - timestamps, - blob2 = this.blob2, - blob1 = this.blob1, - other_blob = this.other_blob, - otherother_blob = new Blob(['abcabc']); - - jio.put("doc", {title: "foo0"}) // 0 - .push(function () { - return jio.put("doc2", {key: "val"}); // 1 - }) - .push(function () { - return jio.putAttachment("doc", "attacheddata", blob1); // 2 - }) - .push(function () { - return jio.putAttachment("doc", "attacheddata", blob2); // 3 - }) - .push(function () { - return jio.putAttachment("doc", "other_attacheddata", other_blob);// 4 - }) - .push(function () { - return jio.putAttachment( // 5 - "doc", - "otherother_attacheddata", - otherother_blob - ); - }) - .push(function () { - return jio.removeAttachment("doc", "otherother_attacheddata"); // 6 - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) - .push(function () { - return jio.get("doc"); - }) - .push(function (result) { - deepEqual(result, { - title: "foo0" - }, "Get does not return any attachment/revision information"); - return jio.getAttachment("doc", "attacheddata"); - }) - .push(function (result) { - deepEqual(result, - blob2, - "Return the attachment information with getAttachment" - ); - return history.getAttachment( - timestamps[3], - "attacheddata" - ); - }) - .push(function (result) { - deepEqual(result, - blob2, - "Return the attachment information with getAttachment for " + - "current revision" - ); - return history.getAttachment( - timestamps[2], - "attacheddata" - ); - }, function (error) { - //console.log(error); - ok(false, error); - }) - .push(function (result) { - deepEqual(result, - blob1, - "Return the attachment information with getAttachment for " + - "previous revision" - ); - return jio.getAttachment(timestamps[0], "attached"); - }, function (error) { - ok(false, error); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - deepEqual(error.status_code, - 404, - "Error if you try to go back to a nonexistent timestamp"); - deepEqual(error.message, - "HistoryStorage: cannot find object '" + timestamps[0] + "'", - "Error caught by history storage correctly"); - return jio.getAttachment("doc", "other_attacheddata"); - }) - .push(function (result) { - deepEqual(result, - other_blob, - "Other document successfully queried" - ); - }) - .push(function () { - return jio.getAttachment("doc", "otherother_attacheddata"); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - deepEqual(error.status_code, - 404, - "Error if you try to get a removed attachment"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("get attachment immediately after removing it", - function () { - stop(); - expect(3); - var jio = this.jio, - blob1 = this.blob1; - - jio.put("doc", {title: "foo0"}) - .push(function () { - return jio.putAttachment("doc", "attacheddata", blob1); - }) - .push(function () { - return jio.removeAttachment("doc", "attacheddata"); - }) - .push(function () { - return jio.getAttachment("doc", "attacheddata"); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - ok(error instanceof jIO.util.jIOError, "throws a jio error"); - deepEqual(error.status_code, - 404, - "allAttachments of a removed document throws a 404 error"); - deepEqual(error.message, - "HistoryStorage: cannot find object 'doc' (removed)", - "Error is handled by Historystorage."); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Ordering of put and remove attachments is correct", - function () { - stop(); - expect(1); - var jio = this.jio, - blob1 = this.blob1, - blob2 = this.blob2; - - jio.put("doc", {title: "foo0"}) - .push(function () { - return jio.putAttachment("doc", "data", blob1); - }) - .push(function () { - return jio.removeAttachment("doc", "data"); - }) - .push(function () { - return jio.putAttachment("doc", "data", blob2); - }) - .push(function () { - return jio.getAttachment("doc", "data"); - }) - .push(function (result) { - deepEqual(result, - blob2, - "removeAttachment happens before putAttachment" - ); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Correctness of allAttachments method on current attachments", - function () { - stop(); - expect(14); - var jio = this.jio, - not_history = this.not_history, - blob1 = this.blob1, - blob2 = this.blob2, - blob3 = this.blob3, - other_blob = this.other_blob; - - jio.put("doc", {title: "foo0"}) - .push(function () { - return jio.put("doc2", {key: "val"}); - }) - .push(function () { - return jio.putAttachment("doc", "attacheddata", blob1); - }) - .push(function () { - return jio.putAttachment("doc", "attacheddata", blob2); - }) - .push(function () { - return jio.putAttachment("doc", "other_attacheddata", other_blob); - }) - .push(function () { - return jio.allAttachments("doc"); - }) - .push(function (results) { - deepEqual(results, { - "attacheddata": blob2, - "other_attacheddata": other_blob - }, "allAttachments works as expected."); - return jio.removeAttachment("doc", "attacheddata"); // - }) - .push(function () { - return jio.get("doc"); - }) - .push(function (result) { - deepEqual(result, { - title: "foo0" - }, "Get does not return any attachment information"); - return jio.getAttachment("doc", "attacheddata"); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - deepEqual(error.status_code, - 404, - "Removed attachments cannot be queried (4)"); - return jio.allAttachments("doc"); - }) - .push(function (results) { - deepEqual(results, { - "other_attacheddata": blob2 - }, "allAttachments works as expected with a removed attachment"); - return jio.putAttachment("doc", "attacheddata", blob3); // - }) - .push(function () { - return not_history.allDocs(); - }) - .push(function (results) { - var promises = results.data.rows.map(function (data) { - return not_history.get(data.id); - }); - return RSVP.all(promises); - }) - .push(function (results) { - deepEqual(results, [ - {timestamp: results[0].timestamp, - doc_id: "doc", doc: results[0].doc, op: "put"}, - {timestamp: results[1].timestamp, - doc_id: "doc2", doc: results[1].doc, op: "put"}, - {timestamp: results[2].timestamp, - doc_id: "doc", name: "attacheddata", op: "putAttachment"}, - {timestamp: results[3].timestamp, - doc_id: "doc", name: "attacheddata", op: "putAttachment"}, - {timestamp: results[4].timestamp, - doc_id: "doc", name: "other_attacheddata", op: "putAttachment"}, - {timestamp: results[5].timestamp, - doc_id: "doc", name: "attacheddata", op: "removeAttachment"}, - {timestamp: results[6].timestamp, - doc_id: "doc", name: "attacheddata", op: "putAttachment"} - ], "Other storage can access all document revisions." - ); - }) - .push(function () { - return jio.allDocs(); - }) - .push(function (results) { - equal(results.data.total_rows, - 2, - "Two documents in accessible storage"); - return jio.get(results.data.rows[1].id); - }) - .push(function (result) { - deepEqual(result, { - "key": "val" - }, "Get second document accessible from jio storage"); - - return not_history.allDocs(); - }) - .push(function (results) { - return RSVP.all(results.data.rows.map(function (d) { - return not_history.get(d.id); - })); - }) - .push(function (results) { - equal(results.length, 7, "Seven document revisions in storage"); - return jio.remove("doc"); - }) - .push(function () { - return jio.getAttachment("doc", "attacheddata"); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - deepEqual(error.status_code, - 404, - "Cannot get the attachment of a removed document"); - }) - .push(function () { - return jio.allAttachments("doc"); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - ok(error instanceof jIO.util.jIOError, "throws a jio error"); - deepEqual(error.status_code, - 404, - "allAttachments of a removed document throws a 404 error"); - deepEqual(error.message, - "HistoryStorage: cannot find object 'doc' (removed)", - "Error is handled by Historystorage."); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Correctness of allAttachments method on older revisions", - function () { - stop(); - expect(11); - var jio = this.jio, - history = this.history, - not_history = this.not_history, - blob1 = new Blob(['a']), - blob11 = new Blob(['ab']), - blob2 = new Blob(['abc']), - blob22 = new Blob(['abcd']), - timestamps; - - jio.put("doc", {title: "foo0"}) // 0 - .push(function () { - return jio.putAttachment("doc", "data", blob1); - }) - .push(function () { - return jio.putAttachment("doc", "data2", blob2); - }) - .push(function () { - return jio.put("doc", {title: "foo1"}); // 1 - }) - .push(function () { - return jio.removeAttachment("doc", "data2"); - }) - .push(function () { - return jio.put("doc", {title: "foo2"}); // 2 - }) - .push(function () { - return jio.putAttachment("doc", "data", blob11); - }) - .push(function () { - return jio.remove("doc"); // 3 - }) - .push(function () { - return jio.put("doc", {title: "foo3"}); // 4 - }) - .push(function () { - return jio.putAttachment("doc", "data2", blob22); - }) - .push(function () { - return not_history.allDocs({ - query: "op: put OR op: remove", - sort_on: [["timestamp", "ascending"]], - select_list: ["timestamp"] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.value.timestamp; - }); - }) - .push(function () { - return jio.allAttachments("doc"); - }) - .push(function (results) { - deepEqual(results, { - "data": blob11, - "data2": blob22 - }, - "Current state of document is correct"); - - return history.allAttachments(timestamps[0]); - }) - .push(function (results) { - deepEqual(results, {}, "First version of document has 0 attachments"); - - return history.allAttachments(timestamps[1]); - }) - .push(function (results) { - deepEqual(results, { - data: blob1, - data2: blob2 - }, "Both attachments are included in allAttachments"); - - return history.allAttachments(timestamps[2]); - }) - .push(function (results) { - deepEqual(results, { - data: blob1 - }, "Removed attachment does not show up in allAttachments"); - return history.allAttachments(timestamps[3]); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - ok(error instanceof jIO.util.jIOError, "throws a jio error"); - deepEqual(error.status_code, - 404, - "allAttachments of a removed document throws a 404 error"); - deepEqual(error.message, - "HistoryStorage: cannot find object '" + timestamps[3] + - "' (removed)", - "Error is handled by Historystorage."); - }) - .push(function () { - return history.allAttachments(timestamps[4]); - }) - .push(function (results) { - deepEqual(results, { - data: blob11 - }); - }) - .push(function () { - return history.allAttachments("not-a-timestamp-or-doc_id"); - }) - .push(function () { - ok(false, "This query should have thrown a 404 error"); - }, - function (error) { - ok(error instanceof jIO.util.jIOError, "throws a jio error"); - deepEqual(error.status_code, - 404, - "allAttachments of a removed document throws a 404 error"); - deepEqual(error.message, - "HistoryStorage: cannot find object 'not-a-timestamp-or-doc_id'", - "Error is handled by Historystorage."); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - - - ///////////////////////////////////////////////////////////////// - // Querying older revisions - ///////////////////////////////////////////////////////////////// - - module("HistoryStorage.get", { - setup: function () { - // create storage of type "history" with memory as substorage - var dbname = "db_" + Date.now(); - this.jio = jIO.createJIO({ - type: "query", - sub_storage: { - type: "history", - sub_storage: { - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - } - } - }); - this.history = jIO.createJIO({ - type: "query", - sub_storage: { - type: "history", - include_revisions: true, - sub_storage: { - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - } - } - }); - this.not_history = jIO.createJIO({ - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - }); - } - }); - - test("Removing documents before putting them", - function () { - stop(); - expect(4); - var jio = this.jio; - - jio.remove("doc") - .push(function () { - return jio.put("doc2", {title: "foo"}); - }) - .push(function () { - return jio.get("doc"); - }) - .push(function () { - ok(false, "This statement should not be reached"); - }, function (error) { - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - deepEqual(error.status_code, - 404, - "Correct status code for getting a non-existent document" - ); - deepEqual(error.message, - "HistoryStorage: cannot find object 'doc' (removed)", - "Error is handled by history storage before reaching console"); - }) - .push(function () { - return jio.allDocs({select_list: ["title"]}); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - id: "doc2", - value: {title: "foo"}, - //timestamp: timestamps[1], - doc: {} - }], "Document that was removed before being put is not retrieved"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Removing documents and then putting them", - function () { - stop(); - expect(2); - var jio = this.jio, - history = this.history, - timestamps, - not_history = this.not_history; - - jio.remove("doc") - .push(function () { - return jio.put("doc", {title: "foo"}); - }) - .push(function () { - return jio.get("doc"); - }) - .push(function (result) { - deepEqual(result, { - title: "foo" - }, "A put was the most recent edit on 'doc'"); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) - .push(function () { - return history.allDocs({select_list: ["title"]}); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - //id: "doc", - value: {title: "foo"}, - id: timestamps[1], - doc: {} - }, - { - value: {}, - id: timestamps[0], - doc: {} - }], "DOcument that was removed before being put is not retrieved"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Handling bad input", - function () { - stop(); - expect(2); - var jio = this.jio, - history = this.history, - not_history = this.not_history, - timestamp; - - jio.put("doc", {title: "foo"}) - .push(function () { - return not_history.allDocs(); - }) - .push(function (res) { - timestamp = res.data.rows[0].id; - return jio.put(timestamp, {key: "val"}); - }) - .push(function () { - return jio.get("doc"); - }) - .push(function (result) { - deepEqual(result, { - title: "foo" - }, "Saving document with timestamp id does not cause issues (1)"); - return history.get(timestamp); - }) - .push(function (result) { - deepEqual(result, { - title: "foo" - }, "Saving document with timestamp id does not cause issues (2)"); - return history.get(timestamp); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Getting a non-existent document", - function () { - stop(); - expect(3); - var jio = this.jio; - jio.put("not_doc", {}) - .push(function () { - return jio.get("doc"); - }) - .push(function () { - ok(false, "This statement should not be reached"); - }, function (error) { - //console.log(error); - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - deepEqual(error.status_code, - 404, - "Correct status code for getting a non-existent document" - ); - deepEqual(error.message, - "HistoryStorage: cannot find object 'doc'", - "Error is handled by history storage before reaching console"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Getting a document with timestamp when include_revisions is false", - function () { - stop(); - expect(6); - var jio = this.jio, - not_history = this.not_history, - timestamp; - jio.put("not_doc", {}) - .push(function () { - return jio.get("doc"); - }) - .push(function () { - ok(false, "This statement should not be reached"); - }, function (error) { - //console.log(error); - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - deepEqual(error.status_code, - 404, - "Correct status code for getting a non-existent document" - ); - deepEqual(error.message, - "HistoryStorage: cannot find object 'doc'", - "Error is handled by history storage before reaching console"); - }) - .push(function () { - return not_history.allDocs(); - }) - .push(function (results) { - timestamp = results.data.rows[0].id; - return jio.get(timestamp); - }) - .push(function () { - ok(false, "This statement should not be reached"); - }, function (error) { - //console.log(error); - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - deepEqual(error.status_code, - 404, - "Correct status code for getting a non-existent document" - ); - deepEqual(error.message, - "HistoryStorage: cannot find object '" + timestamp + "'", - "Error is handled by history storage before reaching console"); - }) - - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Creating a document with put and retrieving it with get", - function () { - stop(); - expect(5); - var jio = this.jio, - history = this.history, - not_history = this.not_history, - timestamps; - jio.put("doc", {title: "version0"}) - .push(function () { - return not_history.allDocs({ - select_list: ["timestamp"] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.value.timestamp; - }); - }) - .push(function () { - equal(timestamps.length, - 1, - "One revision is saved in storage" - ); - return history.get(timestamps[0]); - }) - .push(function (result) { - deepEqual(result, { - title: "version0" - }, "Get document from history storage"); - return not_history.get( - timestamps[0] - ); - }) - .push(function (result) { - deepEqual(result, { - timestamp: timestamps[0], - op: "put", - doc_id: "doc", - doc: { - title: "version0" - } - }, "Get document from non-history storage"); - }) - .push(function () { - return jio.get("non-existent-doc"); - }) - .push(function () { - ok(false, "This should have thrown an error"); - }, function (error) { - //console.log(error); - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - deepEqual(error.status_code, - 404, - "Can't access non-existent document" - ); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Updating a document with include revisions", - function () { - stop(); - expect(1); - var jio = this.jio, - history = this.history, - not_history = this.not_history, - timestamps, - t_id; - jio.put("doc", {title: "version0"}) - .push(function () { - return history.put("doc", {title: "version1"}); - }) - .push(function () { - return not_history.allDocs({sort_on: [["timestamp", "ascending"]]}); - }) - .push(function (results) { - t_id = results.data.rows[0].id; - return history.put(t_id, {title: "version0.1"}); - }) - .push(function () { - return jio.put(t_id, {title: "label0"}); - }) - .push(function () { - return history.put("1234567891012-abcd", {k: "v"}); - }) - .push(function () { - return not_history.allDocs({ - select_list: ["timestamp"] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.value.timestamp; - }); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]], - select_list: ["timestamp", "op", "doc_id", "doc"] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - id: timestamps[0], - doc: {}, - value: { - timestamp: timestamps[0], - op: "put", - doc_id: "doc", - doc: { - title: "version0" - } - } - }, - { - id: timestamps[1], - doc: {}, - value: { - timestamp: timestamps[1], - op: "put", - doc_id: "doc", - doc: { - title: "version1" - } - } - }, - { - id: timestamps[2], - doc: {}, - value: { - timestamp: timestamps[2], - op: "put", - doc_id: "doc", - doc: { - title: "version0.1" - } - } - }, - { - id: timestamps[3], - doc: {}, - value: { - timestamp: timestamps[3], - op: "put", - doc_id: timestamps[0], - doc: { - title: "label0" - } - } - }, - { - id: timestamps[4], - doc: {}, - value: { - timestamp: timestamps[4], - op: "put", - doc_id: "1234567891012-abcd", - doc: { - k: "v" - } - } - } - ], "Documents stored with correct metadata"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Retrieving older revisions with get", - function () { - stop(); - expect(7); - var jio = this.jio, - history = this.history, - not_history = this.not_history, - timestamps; - - return jio.put("doc", {title: "t0", subtitle: "s0"}) - .push(function () { - return jio.put("doc", {title: "t1", subtitle: "s1"}); - }) - .push(function () { - return jio.put("doc", {title: "t2", subtitle: "s2"}); - }) - .push(function () { - jio.remove("doc"); - }) - .push(function () { - return jio.put("doc", {title: "t3", subtitle: "s3"}); - }) - .push(function () { - return not_history.allDocs({ - select_list: ["timestamp"], - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.value.timestamp; - }); - }) - .push(function () { - return jio.get("doc"); - }) - .push(function (result) { - deepEqual(result, { - title: "t3", - subtitle: "s3" - }, "Get returns latest revision"); - return history.get(timestamps[0]); - }, function (err) { - ok(false, err); - }) - .push(function (result) { - deepEqual(result, { - title: "t0", - subtitle: "s0" - }, "Get returns first version"); - return history.get(timestamps[1]); - }) - .push(function (result) { - deepEqual(result, { - title: "t1", - subtitle: "s1" - }, "Get returns second version"); - return history.get(timestamps[2]); - }, function (err) { - ok(false, err); - }) - .push(function (result) { - deepEqual(result, { - title: "t2", - subtitle: "s2" - }, "Get returns third version"); - return history.get(timestamps[3]); - }, function (err) { - ok(false, err); - }) - .push(function () { - ok(false, "This should have thrown a 404 error"); - return history.get(timestamps[4]); - }, - function (error) { - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - deepEqual(error.status_code, - 404, - "Error if you try to go back more revisions than what exists"); - return history.get(timestamps[4]); - }) - .push(function (result) { - deepEqual(result, { - title: "t3", - subtitle: "s3" - }, "Get returns latest version"); - }) - - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("verifying updates correctly when puts are done in parallel", - function () { - stop(); - expect(8); - var jio = this.jio, - not_history = this.not_history; - - jio.put("bar", {"title": "foo0"}) - .push(function () { - return RSVP.all([ - jio.put("bar", {"title": "foo1"}), - jio.put("bar", {"title": "foo2"}), - jio.put("bar", {"title": "foo3"}), - jio.put("bar", {"title": "foo4"}), - jio.put("barbar", {"title": "attr0"}), - jio.put("barbar", {"title": "attr1"}), - jio.put("barbar", {"title": "attr2"}), - jio.put("barbar", {"title": "attr3"}) - ]); - }) - .push(function () {return jio.get("bar"); }) - .push(function (result) { - ok(result.title !== "foo0", "Title should have changed from foo0"); - }) - .push(function () { - return not_history.allDocs({ - query: "", - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - equal(results.data.total_rows, - 9, - "All nine versions exist in storage"); - return not_history.get(results.data.rows[0].id); - }) - .push(function (results) { - deepEqual(results, { - doc_id: "bar", - doc: { - title: "foo0" - }, - timestamp: results.timestamp, - op: "put" - }, "The first item in the log is pushing bar's title to 'foo0'"); - return jio.remove("bar"); - }) - .push(function () { - return jio.get("bar"); - }) - .push(function () { - return jio.get("barbar"); - }, function (error) { - ok(error instanceof jIO.util.jIOError, "Correct type of error"); - equal(error.status_code, 404, "Correct error status code returned"); - return jio.get("barbar"); - }) - .push(function (result) { - ok(result.title !== undefined, "barbar exists and has proper form"); - return not_history.allDocs({ - query: "", - sort_on: [["op", "descending"]] - }); - }) - .push(function (results) { - equal(results.data.total_rows, - 10, - "Remove operation is recorded"); - return not_history.get(results.data.rows[0].id); - }) - .push(function (result) { - deepEqual(result, { - doc_id: "bar", - timestamp: result.timestamp, - op: "remove" - }); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Getting after attachments have been put", - function () { - stop(); - expect(4); - var jio = this.jio, - not_history = this.not_history, - history = this.history, - blob = new Blob(['a']), - edit_log; - - jio.put("doc", {"title": "foo0"}) - .push(function () { - return jio.putAttachment("doc", "attachment", blob); - }) - .push(function () { - return jio.removeAttachment("doc", "attachment", blob); - }) - .push(function () { - return jio.get("doc"); - }) - .push(function (res) { - deepEqual(res, - {title: "foo0"}, - "Correct information returned"); - return not_history.allDocs({select_list: ["title"]}); - }) - .push(function (results) { - edit_log = results.data.rows; - return history.get(edit_log[0].id); - }) - .push(function (result) { - deepEqual(result, {title: "foo0"}); - return history.get(edit_log[1].id); - }) - .push(function (result) { - deepEqual(result, {title: "foo0"}); - return history.get(edit_log[2].id); - }) - .push(function (result) { - deepEqual(result, {title: "foo0"}); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - - ///////////////////////////////////////////////////////////////// - // Querying older revisions - ///////////////////////////////////////////////////////////////// - - module("HistoryStorage.allDocs", { - setup: function () { - // create storage of type "history" with memory as substorage - this.dbname = "db_" + Date.now(); - this.jio = jIO.createJIO({ - type: "uuid", - sub_storage: { - type: "query", - sub_storage: { - type: "history", - sub_storage: { - type: "query", - sub_storage: { - type: "indexeddb", - database: this.dbname - } - } - } - } - }); - this.history = jIO.createJIO({ - type: "uuid", - sub_storage: { - type: "query", - sub_storage: { - type: "history", - include_revisions: true, - sub_storage: { - type: "query", - sub_storage: { - type: "indexeddb", - database: this.dbname - } - } - } - } - }); - this.not_history = jIO.createJIO({ - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: this.dbname - } - } - }); - } - }); - test("Putting a document and retrieving it with allDocs", - function () { - stop(); - expect(7); - var jio = this.jio, - not_history = this.not_history, - timestamp; - jio.put("doc", {title: "version0"}) - .push(function () { - return not_history.allDocs({ - query: "doc_id: doc", - select_list: ["timestamp"] - }); - }) - .push(function (results) { - timestamp = results.data.rows[0].value.timestamp; - }) - .push(function () { - return RSVP.all([ - jio.allDocs(), - jio.allDocs({query: "title: version0"}), - jio.allDocs({limit: [0, 1]}), - jio.allDocs({}) - ]); - }) - .push(function (results) { - var ind = 0; - for (ind = 0; ind < results.length - 1; ind += 1) { - deepEqual(results[ind], - results[ind + 1], - "Each query returns exactly the same correct output" - ); - } - return results[0]; - }) - .push(function (results) { - equal(results.data.total_rows, - 1, - "Exactly one result returned"); - deepEqual(results.data.rows[0], { - doc: {}, - value: {}, - //timestamp: timestamp, - id: "doc" - }, - "Correct document format is returned." - ); - return not_history.allDocs(); - }) - .push(function (results) { - timestamp = results.data.rows[0].id; - equal(results.data.total_rows, - 1, - "Exactly one result returned"); - return not_history.get(timestamp); - }) - .push(function (result) { - deepEqual(result, { - doc_id: "doc", - doc: { - title: "version0" - }, - timestamp: timestamp, - op: "put" - }, - "When a different type of storage queries historystorage, all " + - "metadata is returned correctly" - ); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Putting doc with troublesome properties and retrieving with allDocs", - function () { - stop(); - expect(1); - var jio = this.jio; - jio.put("doc", { - title: "version0", - doc_id: "bar", - _doc_id: "bar2", - timestamp: "foo", - _timestamp: "foo2", - id: "baz", - _id: "baz2", - __id: "baz3", - op: "zop" - }) - .push(function () { - return jio.allDocs({ - query: "title: version0 AND _timestamp: >= 0", - select_list: ["title", "doc_id", "_doc_id", "timestamp", - "_timestamp", "id", "_id", "__id", "op"] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - doc: {}, - id: "doc", - //timestamp: timestamp, - value: { - title: "version0", - doc_id: "bar", - _doc_id: "bar2", - timestamp: "foo", - _timestamp: "foo2", - id: "baz", - _id: "baz2", - __id: "baz3", - op: "zop" - } - }], - "Poorly-named properties are not overwritten in allDocs call"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Putting a document, revising it, and retrieving revisions with allDocs", - function () { - stop(); - expect(10); - var jio = this.jio, - history = this.history, - not_history = this.not_history, - timestamps; - jio.put("doc", { - title: "version0", - subtitle: "subvers0" - }) - .push(function () { - return jio.put("doc", { - title: "version1", - subtitle: "subvers1" - }); - }) - .push(function () { - return jio.put("doc", { - title: "version2", - subtitle: "subvers2" - }); - }) - .push(function () { - return not_history.allDocs({ - select_list: ["timestamp"], - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.value.timestamp; - }); - }) - .push(function () { - return RSVP.all([ - jio.allDocs({select_list: ["title", "subtitle"]}), - jio.allDocs({ - query: "", - select_list: ["title", "subtitle"] - }), - jio.allDocs({ - query: "title: version2", - select_list: ["title", "subtitle"] - }), - jio.allDocs({ - query: "NOT (title: version1)", - select_list: ["title", "subtitle"] - }), - jio.allDocs({ - query: "(NOT (subtitle: subvers1)) AND (NOT (title: version0))", - select_list: ["title", "subtitle"] - }), - jio.allDocs({ - limit: [0, 1], - sort_on: [["title", "ascending"]], - select_list: ["title", "subtitle"] - }) - ]); - }) - .push(function (results) { - var ind = 0; - for (ind = 0; ind < results.length - 1; ind += 1) { - deepEqual(results[ind], - results[ind + 1], - "Each query returns exactly the same correct output" - ); - } - return results[0]; - }) - .push(function (results) { - equal(results.data.total_rows, - 1, - "Exactly one result returned"); - deepEqual(results.data.rows[0], { - value: { - title: "version2", - subtitle: "subvers2" - }, - doc: {}, - //timestamp: timestamps[2], - id: "doc" - }, - "Correct document format is returned." - ); - }) - .push(function () { - return history.allDocs({ - query: "", - select_list: ["title", "subtitle"] - }); - }) - .push(function (results) { - equal(results.data.total_rows, - 3, - "Querying with include_revisions retrieves all versions"); - deepEqual(results.data.rows, [ - { - //id: results.data.rows[0].id, - value: { - title: "version2", - subtitle: "subvers2" - }, - id: timestamps[2], - doc: {} - }, - { - //id: results.data.rows[1].id, - value: { - title: "version1", - subtitle: "subvers1" - }, - id: timestamps[1], - doc: {} - }, - { - //id: results.data.rows[2].id, - value: { - title: "version0", - subtitle: "subvers0" - }, - id: timestamps[0], - doc: {} - } - ], "Full version history is included."); - - return not_history.allDocs({ - sort_on: [["title", "ascending"]] - }); - }) - .push(function (results) { - return RSVP.all(results.data.rows.map(function (d) { - return not_history.get(d.id); - })); - }) - .push(function (results) { - deepEqual(results, [ - { - timestamp: timestamps[0], - op: "put", - doc_id: "doc", - doc: { - title: "version0", - subtitle: "subvers0" - } - }, - { - timestamp: timestamps[1], - op: "put", - doc_id: "doc", - doc: { - title: "version1", - subtitle: "subvers1" - } - }, - { - timestamp: timestamps[2], - op: "put", - doc_id: "doc", - doc: { - title: "version2", - subtitle: "subvers2" - } - } - ], - "A different storage type can retrieve all versions as expected."); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - - test( - "Putting and removing documents, latest revisions and no removed documents", - function () { - stop(); - expect(3); - var jio = this.jio, - not_history = this.not_history, - timestamps; - - jio.put("doc_a", { - title_a: "rev0", - subtitle_a: "subrev0" - }) - .push(function () { - return jio.put("doc_a", { - title_a: "rev1", - subtitle_a: "subrev1" - }); - }) - .push(function () { - return jio.put("doc_b", { - title_b: "rev0", - subtitle_b: "subrev0" - }); - }) - .push(function () { - return jio.remove("doc_b"); - }) - .push(function () { - return jio.put("doc_c", { - title_c: "rev0", - subtitle_c: "subrev0" - }); - }) - .push(function () { - return jio.put("doc_c", { - title_c: "rev1", - subtitle_c: "subrev1" - }); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) - .push(function () { - return jio.allDocs({sort_on: [["timestamp", "descending"]]}); - }) - .push(function (results) { - equal(results.data.total_rows, - 2, - "Only two non-removed unique documents exist." - ); - deepEqual(results.data.rows, [ - { - id: "doc_c", - value: {}, - //timestamp: timestamps[5], - doc: {} - }, - { - id: "doc_a", - value: {}, - //timestamp: timestamps[1], - doc: {} - } - ], - "Empty query returns latest revisions (and no removed documents)"); - equal(timestamps.length, - 6, - "Correct number of revisions logged"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - } - ); - - ///////////////////////////////////////////////////////////////// - // Complex Queries - ///////////////////////////////////////////////////////////////// - - test("More complex query with different options (without revision queries)", - function () { - stop(); - expect(2); - var jio = this.jio, - docs = [ - { - "date": 1, - "type": "foo", - "title": "doc" - }, - { - "date": 2, - "type": "bar", - "title": "second_doc" - }, - { - "date": 2, - "type": "barbar", - "title": "third_doc" - } - ], - blobs = [ - new Blob(['a']), - new Blob(['bcd']), - new Blob(['eeee']) - ]; - jio.put("doc", {}) // 0 - .push(function () { - return putFullDoc(jio, "doc", docs[0], "data", blobs[0]); // 1,2 - }) - .push(function () { - return putFullDoc(jio, "second_doc", docs[1], "data", blobs[1]);// 3,4 - }) - .push(function () { - return putFullDoc(jio, "third_doc", docs[2], "data", blobs[2]); // 5,6 - }) - .push(function () { - return jio.allDocs({ - query: "NOT (date: > 2)", - select_list: ["date", "non-existent-key"], - sort_on: [["date", "ascending"], - ["non-existent-key", "ascending"] - ] - }); - }) - .push(function (results) { - equal(results.data.total_rows, 3); - deepEqual(results.data.rows, [ - { - doc: {}, - id: "doc", - //timestamp: timestamps[2], - value: {date: 1} - }, - { - doc: {}, - id: "third_doc", - //timestamp: timestamps[6], - value: {date: 2} - }, - { - doc: {}, - id: "second_doc", - //timestamp: timestamps[4], - value: {date: 2} - } - ], - "Query gives correct results in correct order"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - ///////////////////////////////////////////////////////////////// - // Complex Queries with Revision Querying - ///////////////////////////////////////////////////////////////// - - test("More complex query with different options (with revision queries)", - function () { - stop(); - expect(3); - var jio = this.jio, - history = this.history, - not_history = this.not_history, - timestamps, - docs = [ - { - "date": 1, - "type": "foo", - "title": "doc" - }, - { - "date": 2, - "type": "bar", - "title": "second_doc" - } - ], - blobs = [ - new Blob(['a']), - new Blob(['bcd']), - new Blob(['a2']), - new Blob(['bcd2']), - new Blob(['a3']) - ]; - jio.put("doc", {})// 0 - .push(function () {// 1,2 - return putFullDoc(jio, "doc", docs[0], "data", blobs[0]); - }) - .push(function () {// 3,4 - return putFullDoc(jio, "second_doc", docs[1], "data", blobs[1]); - }) - .push(function () { - docs[0].date = 4; - docs[0].type = "foo2"; - docs[1].date = 4; - docs[1].type = "bar2"; - }) - .push(function () {// 5,6 - return putFullDoc(jio, "doc", docs[0], "data", blobs[2]); - }) - .push(function () {// 7 - return jio.remove("second_doc"); - }) - .push(function () {// 8,9 - return putFullDoc(jio, "second_doc", docs[1], "data", blobs[3]); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "descending"]], - select_list: ["op", "doc_id", "timestamp"] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - doc: {}, - id: timestamps[9], - value: { - "op": "putAttachment", - "doc_id": "second_doc", - "timestamp": timestamps[9] - } - }, - { - doc: {}, - id: timestamps[8], - value: { - "op": "put", - "doc_id": "second_doc", - "timestamp": timestamps[8] - } - }, - { - doc: {}, - id: timestamps[7], - value: { - "op": "remove", - "doc_id": "second_doc", - "timestamp": timestamps[7] - } - }, - { - doc: {}, - id: timestamps[6], - value: { - "op": "putAttachment", - "doc_id": "doc", - "timestamp": timestamps[6] - } - }, - { - doc: {}, - id: timestamps[5], - value: { - "op": "put", - "doc_id": "doc", - "timestamp": timestamps[5] - } - }, - { - doc: {}, - id: timestamps[4], - value: { - "op": "putAttachment", - "doc_id": "second_doc", - "timestamp": timestamps[4] - } - }, - { - doc: {}, - id: timestamps[3], - value: { - "op": "put", - "doc_id": "second_doc", - "timestamp": timestamps[3] - } - }, - { - doc: {}, - id: timestamps[2], - value: { - "op": "putAttachment", - "doc_id": "doc", - "timestamp": timestamps[2] - } - }, - { - doc: {}, - id: timestamps[1], - value: { - "op": "put", - "doc_id": "doc", - "timestamp": timestamps[1] - } - }, - { - doc: {}, - id: timestamps[0], - value: { - "op": "put", - "doc_id": "doc", - "timestamp": timestamps[0] - } - } - ], "All operations are logged correctly"); - var promises = results.data.rows - .filter(function (doc) { - return (doc.value.op === "put"); - }) - .map(function (data) { - return not_history.get(data.id); - }); - return RSVP.all(promises) - .then(function (results) { - return results.map(function (docum) { - return docum.doc; - }); - }); - }) - .push(function (results) { - deepEqual(results, - [ - { - "date": 4, - "type": "bar2", - "title": "second_doc" - }, - { - "date": 4, - "type": "foo2", - "title": "doc" - }, - { - "date": 2, - "type": "bar", - "title": "second_doc" - }, - { - "date": 1, - "type": "foo", - "title": "doc" - }, - {} - ], "All versions of documents are stored correctly"); - }) - .push(function () { - return history.allDocs({ - query: "NOT (date: >= 2 AND date: <= 3) AND " + - "(date: = 1 OR date: = 4)", - select_list: ["date", "non-existent-key", "type", "title"], - sort_on: [["date", "descending"]] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - doc: {}, - id: timestamps[9], - value: { - date: 4, - title: "second_doc", - type: "bar2" - } - }, - { - doc: {}, - id: timestamps[8], - value: { - date: 4, - title: "second_doc", - type: "bar2" - } - }, - { - doc: {}, - id: timestamps[6], - value: { - date: 4, - title: "doc", - type: "foo2" - } - }, - { - doc: {}, - id: timestamps[5], - value: { - date: 4, - title: "doc", - type: "foo2" - } - }, - - { - doc: {}, - id: timestamps[2], - value: { - date: 1, - title: "doc", - type: "foo" - } - }, - { - doc: {}, - id: timestamps[1], - value: { - date: 1, - title: "doc", - type: "foo" - } - } - ], - "Query gives correct results in correct order"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test( - "allDocs with include_revisions with an attachment on a removed document", - function () { - stop(); - expect(1); - var jio = this.jio, - history = this.history, - not_history = this.not_history, - blob = new Blob(['a']), - timestamps; - - jio.put("document", {title: "foo"}) - .push(function () { - return jio.remove("document"); - }) - .push(function () { - return jio.putAttachment("document", "attachment", blob); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) - .push(function () { - return history.allDocs({select_list: ["title"]}); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - id: timestamps[2], - doc: {}, - value: {} - }, - { - id: timestamps[1], - doc: {}, - value: {} - }, - { - id: timestamps[0], - doc: {}, - value: {title: "foo"} - }], - "Attachment on removed document is handled correctly" - ); - return not_history.allDocs({select_list: ["doc"]}); - }) - - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - } - ); - - test("allDocs with include_revisions with a removed attachment", - function () { - stop(); - expect(2); - var jio = this.jio, - history = this.history, - blob = new Blob(['a']), - timestamps, - not_history = this.not_history; - - jio.put("document", {title: "foo"}) - .push(function () { - return jio.putAttachment("document", "attachment", blob); - }) - .push(function () { - return jio.removeAttachment("document", "attachment"); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) - .push(function () { - return history.allDocs({select_list: ["title"]}); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - id: timestamps[2], - doc: {}, - value: {title: "foo"} - }, - { - id: timestamps[1], - doc: {}, - value: {title: "foo"} - }, - { - id: timestamps[0], - doc: {}, - value: {title: "foo"} - }], - "Attachment on removed document is handled correctly" - ); - }) - .push(function () { - return jio.allAttachments("document"); - }) - .push(function (results) { - deepEqual(results, {}, "No non-removed attachments"); - }) - - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("allDocs with include_revisions only one document", - function () { - stop(); - expect(1); - var jio = this.jio, - history = this.history, - timestamps, - not_history = this.not_history; - - jio.put("doc a", {title: "foo0"}) - .push(function () { - return jio.put("doc a", {title: "foo1"}); - }) - .push(function () { - return jio.put("doc b", {title: "bar0"}); - }) - .push(function () { - return jio.put("doc b", {title: "bar1"}); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) - .push(function () { - return history.allDocs({ - query: 'doc_id: "doc a"', - select_list: ["title"] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - id: timestamps[1], - doc: {}, - value: {title: "foo1"} - }, - { - id: timestamps[0], - doc: {}, - value: {title: "foo0"} - }], - "Only specified document revision history is returned" - ); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Parallel edits will not break anything", - function () { - stop(); - expect(2); - var jio = this.jio, - history = this.history, - blob1 = new Blob(['ab']), - blob2 = new Blob(['abc']), - blob3 = new Blob(['abcd']); - - jio.put("doc", {k: "v0"}) - .push(function () { - return RSVP.all([ - jio.put("doc", {k: "v"}), - jio.putAttachment("doc", "data", blob1), - jio.putAttachment("doc", "data2", blob2), - jio.putAttachment("doc", "data", blob3), - jio.removeAttachment("doc", "data"), - jio.removeAttachment("doc", "data2"), - jio.remove("doc"), - jio.remove("doc"), - jio.put("doc", {k: "v"}), - jio.put("doc", {k: "v"}), - jio.put("doc2", {k: "foo"}), - jio.remove("doc"), - jio.remove("doc") - ]); - }) - - .push(function () { - ok(true, "No errors thrown."); - return history.allDocs(); - }) - .push(function (results) { - var res = results.data.rows; - equal(res.length, - 14, - "All edits are recorded regardless of ordering"); - return jio.allDocs(); - }) - - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Adding second query storage on top of history", - function () { - stop(); - expect(1); - var jio = this.jio; - return jio.put("doca", {title: "foo0", date: 0}) - .push(function () { - return jio.put("docb", {title: "bar0", date: 0}); - }) - .push(function () { - return jio.put("docb", {title: "bar1", date: 0}); - }) - .push(function () { - return jio.put("doca", {title: "foo1", date: 1}); - }) - .push(function () { - return jio.put("docb", {title: "bar2", date: 2}); - }) - .push(function () { - return jio.allDocs({ - query: "title: foo1 OR title: bar2", - select_list: ["title"], - sort_on: [["date", "ascending"]], - limit: [0, 1] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - doc: {}, - id: "doca", - value: {title: "foo1"} - } - ]); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - - module("HistoryStorage.Full-Example", { - setup: function () { - // create storage of type "history" with memory as substorage - var dbname = "db_" + Date.now(); - this.blob1 = new Blob(['a']); - this.blob2 = new Blob(['b']); - this.blob3 = new Blob(['ccc']); - this.other_blob = new Blob(['1']); - - this.jio = jIO.createJIO({ - type: "query", - sub_storage: { - type: "history", - sub_storage: { - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - } - } - }); - this.history = jIO.createJIO({ - type: "query", - sub_storage: { - type: "history", - include_revisions: true, - sub_storage: { - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - } - } - }); - this.not_history = jIO.createJIO({ - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - }); - } - }); - - test("Retrieving history with attachments", - function () { - stop(); - expect(1); - var jio = this.jio, - history = this.history, - timestamps, - not_history = this.not_history, - blobs1 = [ - new Blob(['a']), - new Blob(['ab']), - new Blob(['abc']), - new Blob(['abcd']), - new Blob(['abcde']) - ], - blobs2 = [ - new Blob(['abcdef']), - new Blob(['abcdefg']), - new Blob(['abcdefgh']), - new Blob(['abcdefghi']), - new Blob(['abcdefghij']) - ]; - putFullDoc(jio, "doc", {title: "bar"}, "data", blobs1[0]) - .push(function () { - return putFullDoc(jio, "doc", {title: "bar0"}, "data", blobs1[1]); - }) - .push(function () { - return putFullDoc(jio, "doc", {title: "bar1"}, "data", blobs1[2]); - }) - .push(function () { - return putFullDoc(jio, "doc2", {title: "foo0"}, "data", blobs2[0]); - }) - .push(function () { - return putFullDoc(jio, "doc2", {title: "foo1"}, "data", blobs2[0]); - }) - .push(function () { - return putFullDoc(jio, "doc", {title: "bar2"}, "data", blobs1[3]); - }) - .push(function () { - return putFullDoc(jio, "doc", {title: "bar3"}, "data", blobs1[4]); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) - - .push(function () { - return history.allDocs({ - select_list: ["title"] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - doc: {}, - id: timestamps[13], - value: {title: "bar3"} - }, - { - doc: {}, - id: timestamps[12], - value: {title: "bar3"} - }, - { - doc: {}, - id: timestamps[11], - value: {title: "bar2"} - }, - { - doc: {}, - id: timestamps[10], - value: {title: "bar2"} - }, - { - doc: {}, - id: timestamps[9], - value: {title: "foo1"} - }, - { - doc: {}, - id: timestamps[8], - value: {title: "foo1"} - }, - { - doc: {}, - id: timestamps[7], - value: {title: "foo0"} - }, - { - doc: {}, - id: timestamps[6], - value: {title: "foo0"} - }, - { - doc: {}, - id: timestamps[5], - value: {title: "bar1"} - }, - { - doc: {}, - id: timestamps[4], - value: {title: "bar1"} - }, - { - doc: {}, - id: timestamps[3], - value: {title: "bar0"} - }, - { - doc: {}, - id: timestamps[2], - value: {title: "bar0"} - }, - { - doc: {}, - id: timestamps[1], - value: {title: "bar"} - }, - { - doc: {}, - id: timestamps[0], - value: {title: "bar"} - } - ], - "allDocs with include_revisions should return all revisions"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - - test("Retrieving history with attachments with less straightforward ordering", - function () { - stop(); - expect(1); - var jio = this.jio, - history = this.history, - not_history = this.not_history, - timestamps, - blobs1 = [ - new Blob(['a']), - new Blob(['ab']), - new Blob(['abc']), - new Blob(['abcd']), - new Blob(['abcde']) - ]; - jio.put("doc", {title: "bar"}) - .push(function () { - return jio.put("doc", {title: "bar0"}); - }) - .push(function () { - return jio.putAttachment("doc", "data", blobs1[0]); - }) - .push(function () { - return jio.put("doc2", {title: "foo0"}); - }) - .push(function () { - return jio.putAttachment("doc", "data", blobs1[1]); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) - - .push(function () { - return history.allDocs({ - select_list: ["title"] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - doc: {}, - id: timestamps[4], - value: {title: "bar0"} - }, - { - doc: {}, - id: timestamps[3], - value: {title: "foo0"} - }, - { - doc: {}, - id: timestamps[2], - value: {title: "bar0"} - }, - { - doc: {}, - id: timestamps[1], - value: {title: "bar0"} - }, - { - doc: {}, - id: timestamps[0], - value: {title: "bar"} - } - ], - "allDocs with include_revisions should return all revisions"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - - test("Retrieving history with attachments with removals", - function () { - stop(); - expect(2); - var jio = this.jio, - history = this.history, - not_history = this.not_history, - timestamps, - blobs1 = [ - new Blob(['a']), - new Blob(['ab']), - new Blob(['abc']), - new Blob(['abcd']), - new Blob(['abcde']) - ]; - jio.put("doc", {title: "bar"}) - .push(function () { - return jio.put("doc", {title: "bar0"}); - }) - .push(function () { - return jio.putAttachment("doc", "data", blobs1[0]); - }) - .push(function () { - return jio.put("doc2", {title: "foo0"}); - }) - .push(function () { - return jio.putAttachment("doc", "data", blobs1[1]); - }) - .push(function () { - return jio.allDocs({ - select_list: ["title"] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - doc: {}, - id: "doc", - //timestamp: timestamps[4], - value: {title: "bar0"} - }, - { - doc: {}, - id: "doc2", - //timestamp: timestamps[3], - value: {title: "foo0"} - } - ], - "allDocs with include_revisions false should return all revisions"); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "ascending"]] - }); - }) - .push(function (results) { - timestamps = results.data.rows.map(function (d) { - return d.id; - }); - }) - .push(function () { - return history.allDocs({ - select_list: ["title"] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - doc: {}, - id: timestamps[4], - value: {title: "bar0"} - }, - { - doc: {}, - id: timestamps[3], - value: {title: "foo0"} - }, - { - doc: {}, - id: timestamps[2], - value: {title: "bar0"} - }, - { - doc: {}, - id: timestamps[1], - value: {title: "bar0"} - }, - { - doc: {}, - id: timestamps[0], - value: {title: "bar"} - } - ], - "allDocs with include_revisions true should return all revisions"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - - module("HistoryStorage.pack", { - setup: function () { - // create storage of type "history" with memory as substorage - var dbname = "db_" + Date.now(); - this.jio = jIO.createJIO({ - type: "uuid", - sub_storage: { - type: "query", - sub_storage: { - type: "history", - sub_storage: { - type: "query", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - } - } - }); - this.history = jIO.createJIO({ - type: "uuid", - sub_storage: { - type: "query", - sub_storage: { - type: "history", - include_revisions: true, - sub_storage: { - type: "query", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - } - } - }); - this.not_history = jIO.createJIO({ - type: "query", - sub_storage: { - type: "uuid", - sub_storage: { - type: "indexeddb", - database: dbname - } - } - }); - this.blob = new Blob(['a']); - } - }); - - test("Verifying pack works with keep_latest_num", - function () { - stop(); - expect(2); - var jio = this.jio, - not_history = this.not_history; - return jio.put("doc_a", {title: "rev"}) - .push(function () { - return jio.put("doc_a", {title: "rev0"}); - }) - .push(function () { - return jio.put("doc_a", {title: "rev1"}); - }) - .push(function () { - return jio.put("doc_b", {title: "data"}); - }) - .push(function () { - return jio.put("doc_b", {title: "data0"}); - }) - .push(function () { - return jio.put("doc_a", {title: "rev2"}); - }) - .push(function () { - return jio.put("doc_b", {title: "data1"}); - }) - .push(function () { - return jio.put("doc_b", {title: "data2"}); - }) - .push(function () { - return jio.__storage._sub_storage.__storage._sub_storage - .__storage.packOldRevisions({ - keep_latest_num: 2 - }); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "descending"]], - select_list: ["doc", "doc_id", "timestamp", "op"] - }); - }) - .push(function (results) { - equal(results.data.total_rows, 4, "Correct amount of results"); - deepEqual(results.data.rows, [ - { - doc: {}, - id: results.data.rows[0].id, - value: { - doc: {title: "data2"}, - doc_id: "doc_b", - timestamp: results.data.rows[0].id, - op: "put" - } - }, - { - doc: {}, - id: results.data.rows[1].id, - value: { - doc: {title: "data1"}, - doc_id: "doc_b", - timestamp: results.data.rows[1].id, - op: "put" - } - }, - { - doc: {}, - id: results.data.rows[2].id, - value: { - doc: {title: "rev2"}, - doc_id: "doc_a", - timestamp: results.data.rows[2].id, - op: "put" - } - }, - { - doc: {}, - id: results.data.rows[3].id, - value: { - doc: {title: "rev1"}, - doc_id: "doc_a", - timestamp: results.data.rows[3].id, - op: "put" - } - } - ], - "Keep the correct documents after pack"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Verifying pack works with fixed timestamp", - function () { - stop(); - expect(2); - var jio = this.jio, - not_history = this.not_history, - timestamp; - return jio.allDocs() - .push(function () { - return RSVP.all([ - jio.put("doc_a", {title: "old_rev0"}), - jio.put("doc_a", {title: "old_rev1"}), - jio.put("doc_a", {title: "old_rev2"}), - jio.put("doc_b", {title: "old_data0"}), - jio.put("doc_b", {title: "old_data1"}), - jio.put("doc_b", {title: "old_data2"}), - jio.put("doc_c", {title: "latest_bar"}) - ]); - }) - .push(function () { - return not_history.allDocs({sort_on: [["timestamp", "descending"]]}); - }) - .push(function (results) { - timestamp = results.data.rows[0].id; - return jio.put("doc_a", {title: "latest_rev"}); - }) - .push(function () { - return jio.put("doc_b", {title: "latest_data"}); - }) - .push(function () { - return jio.__storage._sub_storage.__storage._sub_storage - .__storage.packOldRevisions({ - keep_active_revs: timestamp - }); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "descending"]], - select_list: ["doc", "doc_id", "timestamp"] - }); - }) - .push(function (results) { - equal(results.data.total_rows, 3, "Correct amount of results"); - deepEqual(results.data.rows, [ - { - doc: {}, - id: results.data.rows[0].id, - value: { - doc: {title: "latest_data"}, - doc_id: "doc_b", - timestamp: results.data.rows[0].id - } - }, - { - doc: {}, - id: results.data.rows[1].id, - value: { - doc: {title: "latest_rev"}, - doc_id: "doc_a", - timestamp: results.data.rows[1].id - } - }, - { - doc: {}, - id: results.data.rows[2].id, - value: { - doc: {title: "latest_bar"}, - doc_id: "doc_c", - timestamp: results.data.rows[2].id - } - } - ], - "Keep the correct documents after pack"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Verifying pack works with fixed timestamp and more complex operations", - function () { - stop(); - expect(2); - var jio = this.jio, - not_history = this.not_history, - timestamp; - return jio.allDocs() - .push(function () { - return RSVP.all([ - jio.put("doc_a", {title: "old_rev0"}), - jio.put("doc_a", {title: "old_rev1"}), - jio.put("doc_a", {title: "old_rev2"}), - jio.put("doc_b", {title: "latest_data"}) - ]); - }) - .push(function () { - return jio.allDocs({sort_on: [["timestamp", "descending"]]}); - }) - .push(function (results) { - timestamp = results.data.rows[0].id; - return jio.remove("doc_a"); - }) - .push(function () { - return jio.__storage._sub_storage.__storage._sub_storage - .__storage.packOldRevisions({ - keep_active_revs: timestamp - }); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "descending"]], - select_list: ["doc", "doc_id", "timestamp", "op"] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - doc: {}, - id: results.data.rows[0].id, - value: { - op: "remove", - doc_id: "doc_a", - timestamp: results.data.rows[0].id - } - }, - { - doc: {}, - id: results.data.rows[1].id, - value: { - doc: {title: "latest_data"}, - doc_id: "doc_b", - op: "put", - timestamp: results.data.rows[1].id - } - } - ], - "Keep the correct documents after pack"); - }) - .push(function () { - return jio.allDocs({ - sort_on: [["timestamp", "descending"]], - select_list: ["title"] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - doc: {}, - id: "doc_b", - value: {title: "latest_data"} - } - ], - "Memory not corrupted by pack without include_revisions"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Verifying pack works with fixed timestamp and more complex operations", - function () { - stop(); - expect(2); - var jio = this.jio, - not_history = this.not_history, - timestamp; - return jio.allDocs() - .push(function () { - return RSVP.all([ - jio.put("doc_a", {title: "old_rev0"}), - jio.put("doc_a", {title: "old_rev1"}), - jio.put("doc_a", {title: "old_rev2"}), - jio.put("doc_b", {title: "latest_data"}) - ]); - }) - .push(function () { - return jio.allDocs({sort_on: [["timestamp", "descending"]]}); - }) - .push(function (results) { - timestamp = results.data.rows[0].id; - return jio.remove("doc_a"); - }) - .push(function () { - return jio.__storage._sub_storage.__storage._sub_storage - .__storage.packOldRevisions({ - keep_active_revs: timestamp - }); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "descending"]], - select_list: ["doc", "doc_id", "timestamp", "op"] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - doc: {}, - id: results.data.rows[0].id, - value: { - op: "remove", - doc_id: "doc_a", - timestamp: results.data.rows[0].id - } - }, - { - doc: {}, - id: results.data.rows[1].id, - value: { - doc: {title: "latest_data"}, - doc_id: "doc_b", - op: "put", - timestamp: results.data.rows[1].id - } - } - ], - "Keep the correct documents after pack"); - }) - .push(function () { - return jio.allDocs({ - sort_on: [["timestamp", "descending"]], - select_list: ["title"] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - doc: {}, - id: "doc_b", - value: {title: "latest_data"} - } - ], - "Memory not corrupted by pack without include_revisions"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - - test("Verifying pack works with fixed timestamp and more complex operations", - function () { - stop(); - expect(2); - var jio = this.jio, - not_history = this.not_history, - timestamp, - blob = this.blob; - return jio.allDocs() - .push(function () { - return RSVP.all([ - jio.put("doc_a", {title: "old_rev0"}), - jio.putAttachment("doc_a", "attach_aa", blob), - jio.put("doc_b", {title: "latest_data"}) - ]); - }) - .push(function () { - return jio.allDocs({sort_on: [["timestamp", "descending"]]}); - }) - .push(function (results) { - timestamp = results.data.rows[0].id; - return jio.remove("doc_a"); - }) - .push(function () { - return jio.__storage._sub_storage.__storage._sub_storage - .__storage.packOldRevisions({ - keep_active_revs: timestamp - }); - }) - .push(function () { - return not_history.allDocs({ - sort_on: [["timestamp", "descending"]], - select_list: ["doc", "doc_id", "timestamp", "op"] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - doc: {}, - id: results.data.rows[0].id, - value: { - op: "remove", - doc_id: "doc_a", - timestamp: results.data.rows[0].id - } - }, - { - doc: {}, - id: results.data.rows[1].id, - value: { - doc: {title: "latest_data"}, - doc_id: "doc_b", - op: "put", - timestamp: results.data.rows[1].id - } - } - ], - "Keep the correct documents after pack"); - }) - .push(function () { - return jio.allDocs({ - sort_on: [["timestamp", "descending"]], - select_list: ["title"] - }); - }) - .push(function (results) { - deepEqual(results.data.rows, [ - { - doc: {}, - id: "doc_b", - value: {title: "latest_data"} - } - ], - "Memory not corrupted by pack without include_revisions"); - }) - .fail(function (error) { - //console.log(error); - ok(false, error); - }) - .always(function () {start(); }); - }); - -}(jIO, RSVP, Blob, QUnit)); \ No newline at end of file diff --git a/test/jio.storage/revisionstorage.tests.js b/test/jio.storage/revisionstorage.tests.js index 9642df0..f9ab01f 100644 --- a/test/jio.storage/revisionstorage.tests.js +++ b/test/jio.storage/revisionstorage.tests.js @@ -1,1899 +1,3267 @@ -/*jslint indent: 2, maxlen: 80, nomen: true */ -/*global define, jIO, test_util, hex_sha256, RSVP, test, ok, deepEqual, start, - stop, module */ - -// define([module_name], [dependencies], module); -(function (dependencies, module) { - "use strict"; - if (typeof define === 'function' && define.amd) { - return define(dependencies, module); - } - module(jIO, test_util, {hex_sha256: hex_sha256}, RSVP); -}([ - 'jio', - 'test_util', - 'sha256', - 'rsvp', - 'localstorage', - 'revisionstorage' -], function (jIO, util, sha256, RSVP) { +/*jslint nomen: true*/ +/*global Blob*/ +(function (jIO, RSVP, Blob, QUnit) { "use strict"; - - ////////////////////////////////////////////////////////////////////////////// - // Tools - - var tool = { - "deepClone": jIO.util.deepClone, - "uniqueJSONStringify": jIO.util.uniqueJSONStringify, - "readBlobAsBinaryString": jIO.util.readBlobAsBinaryString - }; - - function generateRevisionHash(doc, revisions, deleted_flag) { - var string; - doc = tool.deepClone(doc); - delete doc._rev; - delete doc._revs; - delete doc._revs_info; - string = tool.uniqueJSONStringify(doc) + - tool.uniqueJSONStringify(revisions) + - JSON.stringify(deleted_flag ? true : false); - return sha256.hex_sha256(string); - } - - function isRevision(revision) { - return (/^[0-9]+-[0-9a-zA-Z]+$/).test(revision); - } - - function success(promise) { - return new RSVP.Promise(function (resolve, reject, notify) { - /*jslint unparam: true*/ - promise.then(resolve, resolve, notify); - }, function () { - promise.cancel(); - }); - } - - function unexpectedError(error) { - if (error instanceof Error) { - deepEqual([ - error.name + ": " + error.message, - error - ], "UNEXPECTED ERROR", "Unexpected error"); - } else { - deepEqual(error, "UNEXPECTED ERROR", "Unexpected error"); - } - } - - ////////////////////////////////////////////////////////////////////////////// - // Tests - - module("Revision Storage + Local Storage"); - - test("Post", function () { - - var shared = {}, jio, jio_local; - - shared.workspace = {}; - shared.local_storage_description = { - "type": "local", - "username": "revision post", - "mode": "memory" - }; - - jio = jIO.createJIO({ - "type": "revision", - "sub_storage": shared.local_storage_description - }, {"workspace": shared.workspace}); - - jio_local = jIO.createJIO(shared.local_storage_description, { - "workspace": shared.workspace - }); - - stop(); - - // post without id - shared.revisions = {"start": 0, "ids": []}; - jio.post({}).then(function (response) { - - shared.uuid = response.id; - response.id = ""; - shared.rev = response.rev; - response.rev = ""; - ok(util.isUuid(shared.uuid), "Uuid should look like " + - "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx : " + shared.uuid); - ok(isRevision(shared.rev), "Revision should look like " + - "x-xxxxxxxxxxxxxxxxxxxxxxxxx... : " + shared.rev); - deepEqual( - shared.rev, - "1-" + generateRevisionHash({"_id": shared.uuid}, shared.revisions), - "Check revision value" - ); - deepEqual(response, { - "id": "", - "rev": "", - "method": "post", - "result": "success", - "status": 201, - "statusText": "Created" - }, "Post without id"); - - return jio_local.get({"_id": shared.uuid + "." + shared.rev}); - - }).then(function (answer) { - - deepEqual(answer.data, { - "_id": shared.uuid + "." + shared.rev - }, "Check document"); - - return jio_local.get({"_id": shared.uuid + ".revision_tree.json"}); - - }).then(function (answer) { - - shared.doc_tree = { - "_id": shared.uuid + ".revision_tree.json", - "children": JSON.stringify([{ - "rev": shared.rev, - "status": "available", - "children": [] - }]) - }; - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // post non empty document - shared.doc = {"_id": "post1", "title": "myPost1"}; - shared.rev = "1-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.post(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "post1", - "method": "post", - "result": "success", - "rev": shared.rev, - "status": 201, - "statusText": "Created" - }, "Post"); - - // check document - shared.doc._id = "post1." + shared.rev; - return jio_local.get(shared.doc); - - }).then(function (answer) { - - deepEqual(answer.data, { - "_id": shared.doc._id, - "title": "myPost1" - }, "Check document"); - - // check document tree - shared.doc_tree._id = "post1.revision_tree.json"; - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children[0] = { - "rev": shared.rev, - "status": "available", - "children": [] - }; - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - - return jio_local.get(shared.doc_tree); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // post and document already exists - shared.doc = {"_id": "post1", "title": "myPost2"}; - shared.rev = "1-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.post(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "post1", - "method": "post", - "result": "success", - "rev": shared.rev, - "status": 201, - "statusText": "Created" - }, "Post and document already exists"); - - // check document - shared.doc._id = "post1." + shared.rev; - return jio_local.get(shared.doc); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc, "Check document"); - - // check document tree - shared.doc_tree._id = "post1.revision_tree.json"; - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children.unshift({ - "rev": shared.rev, - "status": "available", - "children": [] - }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - - return jio_local.get(shared.doc_tree); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // post + revision - shared.doc = {"_id": "post1", "_rev": shared.rev, "title": "myPost2"}; - shared.revisions = {"start": 1, "ids": [shared.rev.split('-')[1]]}; - shared.rev = "2-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.post(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "post1", - "method": "post", - "result": "success", - "rev": shared.rev, - "status": 201, - "statusText": "Created" // XXX should be 204 no content - }, "Post + revision"); - - // // keep_revision_history - // ok (false, "keep_revision_history Option Not Implemented"); - - // check document - shared.doc._id = "post1." + shared.rev; - delete shared.doc._rev; - return jio_local.get(shared.doc); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc, "Check document"); - - // check document tree - shared.doc_tree._id = "post1.revision_tree.json"; - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children[0].children.unshift({ - "rev": shared.rev, - "status": "available", - "children": [] + var test = QUnit.test, + stop = QUnit.stop, + start = QUnit.start, + ok = QUnit.ok, + expect = QUnit.expect, + deepEqual = QUnit.deepEqual, + equal = QUnit.equal, + module = QUnit.module; + + function putFullDoc(storage, id, doc, attachment_name, attachment) { + return storage.put(id, doc) + .push(function () { + return storage.putAttachment( + id, + attachment_name, + attachment + ); }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - return jio_local.get(shared.doc_tree); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // add attachment - return jio_local.putAttachment({ - "_id": "post1." + shared.rev, - "_attachment": "attachment_test", - "_data": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "_content_type": "oh/yeah" - }); - - }).then(function () { - - // post + attachment copy - shared.doc = {"_id": "post1", "_rev": shared.rev, "title": "myPost2"}; - shared.revisions = { - "start": 2, - "ids": [shared.rev.split('-')[1], shared.revisions.ids[0]] - }; - shared.rev = "3-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.post(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "post1", - "method": "post", - "result": "success", - "rev": shared.rev, - "status": 201, - "statusText": "Created" - }, "Post + attachment copy"); - - // check attachment - return jio_local.getAttachment({ - "_id": "post1." + shared.rev, - "_attachment": "attachment_test" - }); - - }).then(function (answer) { - - return tool.readBlobAsBinaryString(answer.data); - - }).then(function (event) { - - deepEqual(event.target.result, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "Check Attachment"); - - // check document tree - shared.doc_tree._id = "post1.revision_tree.json"; - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children[0].children[0].children.unshift({ - "rev": shared.rev, - "status": "available", - "children": [] - }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - - return jio_local.get({"_id": shared.doc_tree._id}); - - }).then(function (answer) { - - deepEqual( - answer.data, - shared.doc_tree, - "Check document tree" - ); - - // post + wrong revision - shared.doc = {"_id": "post1", "_rev": "3-wr3", "title": "myPost3"}; - shared.revisions = {"start": 3, "ids": ["wr3"]}; - shared.rev = "4-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.post(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "post1", - "method": "post", - "rev": shared.rev, - "result": "success", - "status": 201, - "statusText": "Created" - }, "Post + wrong revision"); - - return success(jio_local.get({"_id": "post1.3-wr3"})); - - }).then(function (answer) { - - // check document - deepEqual(answer, { - "error": "not_found", - "id": "post1.3-wr3", - "message": "Cannot find document", - "method": "get", - "reason": "missing", - "result": "error", - "status": 404, - "statusText": "Not Found" - }, "Check document"); - - // check document - shared.doc._id = "post1." + shared.rev; - delete shared.doc._rev; - - return jio_local.get({"_id": shared.doc._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc, "Check document"); - - // check document tree - shared.doc_tree._id = "post1.revision_tree.json"; - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children.unshift({ - "rev": "3-wr3", - "status": "missing", - "children": [{ - "rev": shared.rev, - "status": "available", - "children": [] - }] - }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - - return jio_local.get({"_id": "post1.revision_tree.json"}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - }).fail(unexpectedError).always(start); - - }); - - test("Put", function () { - - var shared = {}, jio, jio_local; - - shared.workspace = {}; - shared.local_storage_description = { - "type": "local", - "username": "revision put", - "mode": "memory" - }; - - jio = jIO.createJIO({ - "type": "revision", - "sub_storage": shared.local_storage_description - }, {"workspace": shared.workspace}); - - jio_local = jIO.createJIO(shared.local_storage_description, { - "workspace": shared.workspace - }); + } - stop(); - - // put non empty document - shared.doc = {"_id": "put1", "title": "myPut1"}; - shared.revisions = {"start": 0, "ids": []}; - shared.rev = "1-" + generateRevisionHash(shared.doc, shared.revisions); - jio.put(shared.doc).then(function (answer) { - - deepEqual(answer, { - "id": "put1", - "method": "put", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" // XXX should 201 Created - }, "Create a document"); - - // check document - shared.doc._id = "put1." + shared.rev; - return jio_local.get({"_id": shared.doc._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc, "Check document"); - - // check document tree - shared.doc_tree = { - "_id": "put1.revision_tree.json", - "children": [{ - "rev": shared.rev, - "status": "available", - "children": [] - }] - }; - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - - return jio_local.get({"_id": shared.doc_tree._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // put without rev and document already exists - shared.doc = {"_id": "put1", "title": "myPut2"}; - shared.rev = "1-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.put(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "put1", - "method": "put", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" // XXX should be 201 Created - }, "Put same document without revision"); - - - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children.unshift({ - "rev": shared.rev, - "status": "available", - "children": [] - }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - - // put + revision - shared.doc = {"_id": "put1", "_rev": shared.rev, "title": "myPut2"}; - shared.revisions = {"start": 1, "ids": [shared.rev.split('-')[1]]}; - shared.rev = "2-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.put(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "put1", - "method": "put", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" - }, "Put + revision"); - - // check document - shared.doc._id = "put1." + shared.rev; - delete shared.doc._rev; - return jio_local.get({"_id": shared.doc._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc, "Check document"); - - // check document tree - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children[0].children.unshift({ - "rev": shared.rev, - "status": "available", - "children": [] - }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - return jio_local.get({"_id": shared.doc_tree._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // put + wrong revision - shared.doc = {"_id": "put1", "_rev": "3-wr3", "title": "myPut3"}; - shared.revisions = {"start": 3, "ids": ["wr3"]}; - shared.rev = "4-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.put(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "put1", - "method": "put", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" - }, "Put + wrong revision"); - - // check document - shared.doc._id = "put1." + shared.rev; - delete shared.doc._rev; - return jio_local.get({"_id": shared.doc._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc, "Check document"); - - // check document tree - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children.unshift({ - "rev": "3-wr3", - "status": "missing", - "children": [{ - "rev": shared.rev, - "status": "available", - "children": [] - }] + module("revisionStorage.post", { + setup: function () { + // create storage of type "revision" with memory as substorage + var dbname = "db_" + Date.now(); + this.jio = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "revision", + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + } }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - return jio_local.get({"_id": shared.doc_tree._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // put + revision history - shared.doc = { - "_id": "put1", - //"_revs": ["3-rh3", "2-rh2", "1-rh1"], // same as below - "_revs": {"start": 3, "ids": ["rh3", "rh2", "rh1"]}, - "title": "myPut3" - }; - return jio.put(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "put1", - "method": "put", - "result": "success", - "rev": "3-rh3", - "status": 204, - "statusText": "No Content" - }, "Put + revision history"); - - // check document - shared.doc._id = "put1.3-rh3"; - delete shared.doc._revs; - return jio_local.get({"_id": shared.doc._id}); - - }).then(function (answer) { - deepEqual(answer.data, shared.doc, "Check document"); - - // check document tree - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children.unshift({ - "rev": "1-rh1", - "status": "missing", - "children": [{ - "rev": "2-rh2", - "status": "missing", - "children": [{ - "rev": "3-rh3", - "status": "available", - "children": [] - }] - }] + this.revision = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "revision", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + } }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - return jio_local.get({"_id": shared.doc_tree._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // add attachment - shared.doc._attachments = { - "att1": { - "length": 1, - "content_type": "text/plain", - "digest": "sha256-ca978112ca1bbdcafac231b39a23dc4da" + - "786eff8147c4e72b9807785afee48bb" - }, - "att2": { - "length": 2, - "content_type": "dont/care", - "digest": "sha256-1e0bbd6c686ba050b8eb03ffeedc64fdc" + - "9d80947fce821abbe5d6dc8d252c5ac" + this.not_revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } } - }; - return RSVP.all([jio_local.putAttachment({ - "_id": "put1.3-rh3", - "_attachment": "att1", - "_data": "a", - "_content_type": "text/plain" - }), jio_local.putAttachment({ - "_id": "put1.3-rh3", - "_attachment": "att2", - "_data": "bc", - "_content_type": "dont/care" - })]); - - }).then(function () { - - // put + revision with attachment - shared.attachments = shared.doc._attachments; - shared.doc = {"_id": "put1", "_rev": "3-rh3", "title": "myPut4"}; - shared.revisions = {"start": 3, "ids": ["rh3", "rh2", "rh1"]}; - shared.rev = "4-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.put(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "put1", - "method": "put", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" - }, "Put + revision (document contains attachments)"); - - // check document - shared.doc._id = "put1." + shared.rev; - shared.doc._attachments = shared.attachments; - delete shared.doc._rev; - return jio_local.get({"_id": shared.doc._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc, "Check document"); - - // check attachments - return RSVP.all([jio_local.getAttachment({ - "_id": "put1." + shared.rev, - "_attachment": "att1" - }), jio_local.getAttachment({ - "_id": "put1." + shared.rev, - "_attachment": "att2" - })]); - - }).then(function (answers) { - - deepEqual(answers[0].data.type, "text/plain", "Check attachment 1 type"); - deepEqual(answers[1].data.type, "dont/care", "Check attachment 2 type"); - - return RSVP.all([ - tool.readBlobAsBinaryString(answers[0].data), - tool.readBlobAsBinaryString(answers[1].data) - ]); - - }).then(function (answers) { - - deepEqual(answers[0].target.result, "a", "Check attachment 1 content"); - deepEqual(answers[1].target.result, "bc", "Check attachment 2 content"); - - // check document tree - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children[0].children[0].children[0].children.unshift({ - "rev": shared.rev, - "status": "available", - "children": [] }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - return jio_local.get({"_id": shared.doc_tree._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - }).fail(unexpectedError).always(start); - + } }); - test("Put Attachment", function () { - - var shared = {}, jio, jio_local; - - shared.workspace = {}; - shared.local_storage_description = { - "type": "local", - "username": "revision putAttachment", - "mode": "memory" - }; - - jio = jIO.createJIO({ - "type": "revision", - "sub_storage": shared.local_storage_description - }, {"workspace": shared.workspace}); - - jio_local = jIO.createJIO(shared.local_storage_description, { - "workspace": shared.workspace + test("Verifying simple post works", + function () { + stop(); + expect(2); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps; + + return jio.post({title: "foo0"}) + .push(function (result) { + //id = result; + return jio.put(result, {title: "foo1"}); + }) + .push(function (result) { + return jio.get(result); + }) + .push(function (res) { + deepEqual(res, { + title: "foo1" + }, "revision storage only retrieves latest version"); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return revision.allDocs({select_list: ["title"]}); + }) + .push(function (res) { + deepEqual(res.data.rows, [ + { + value: { + title: "foo1" + }, + doc: {}, + id: timestamps[1] + }, + { + value: { + title: "foo0" + }, + doc: {}, + id: timestamps[0] + } + ], + "Two revisions logged with correct metadata"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); }); - stop(); - - // putAttachment without document - shared.revisions = {"start": 0, "ids": []}; - shared.rev_hash = generateRevisionHash({ - "_id": "doc1", - "_attachment": "attmt1", - "_data": "", - "_content_type": "" - }, shared.revisions); - shared.rev = "1-" + shared.rev_hash; - jio.putAttachment({ - "_id": "doc1", - "_attachment": "attmt1", - "_data": "" - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "attmt1", - "id": "doc1", - "method": "putAttachment", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" // XXX should be 201 Created - }, "PutAttachment without document"); - - return jio_local.get({"_id": "doc1." + shared.rev}); - - }).then(function (answer) { - - // check document - deepEqual( - answer.data, - { - "_id": "doc1." + shared.rev, - "_attachments": { - "attmt1": { - "content_type": "", - "length": 0, - "digest": "sha256-e3b0c44298fc1c149afbf4c8996fb9242" + - "7ae41e4649b934ca495991b7852b855" + + ///////////////////////////////////////////////////////////////// + // Attachments + ///////////////////////////////////////////////////////////////// + + module("revisionStorage.attachments", { + setup: function () { + // create storage of type "revision" with memory as substorage + var dbname = "db_" + Date.now(); + this.blob1 = new Blob(['a']); + this.blob2 = new Blob(['b']); + this.blob3 = new Blob(['ccc']); + this.other_blob = new Blob(['1']); + this.jio = jIO.createJIO({ + type: "query", + sub_storage: { + type: "revision", + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } } } - }, - "Check document" - ); - - // check attachment - return jio_local.getAttachment({ - "_id": "doc1." + shared.rev, - "_attachment": "attmt1" - }); - - }).then(function (answer) { - - return tool.readBlobAsBinaryString(answer.data); - - }).then(function (event) { - - deepEqual(event.target.result, "", "Check attachment"); - - // adding a metadata to the document - return jio_local.get({"_id": "doc1." + shared.rev}); - - }).then(function (answer) { - - answer.data._id = "doc1." + shared.rev; - answer.data.title = "My Title"; - return jio_local.put(answer.data); - - }).then(function () { - - // update attachment - shared.prev_rev = shared.rev; - shared.revisions = {"start": 1, "ids": [shared.rev_hash]}; - shared.rev_hash = generateRevisionHash({ - "_id": "doc1", - "_data": "abc", - "_content_type": "", - "_attachment": "attmt1" - }, shared.revisions); - shared.rev = "2-" + shared.rev_hash; - return jio.putAttachment({ - "_id": "doc1", - "_data": "abc", - "_attachment": "attmt1", - "_rev": shared.prev_rev + } }); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "attmt1", - "id": "doc1", - "method": "putAttachment", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" - }, "Update attachment"); - - // check document - return jio_local.get({"_id": "doc1." + shared.rev}); - - }).then(function (answer) { - - deepEqual( - answer.data, - { - "_id": "doc1." + shared.rev, - "title": "My Title", - "_attachments": { - "attmt1": { - "content_type": "", - "length": 3, - "digest": "sha256-ba7816bf8f01cfea414140de5dae2223b00361a3" + - "96177a9cb410ff61f20015ad" + this.revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "revision", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } } } - }, - "Check document" - ); - - // check attachment - return jio_local.getAttachment({ - "_id": "doc1." + shared.rev, - "_attachment": "attmt1" - }); - - }).then(function (answer) { - - return tool.readBlobAsBinaryString(answer.data); - - }).then(function (event) { - - deepEqual(event.target.result, "abc", "Check attachment"); - - // putAttachment new attachment - shared.prev_rev = shared.rev; - shared.revisions = { - "start": 2, - "ids": [shared.rev_hash, shared.revisions.ids[0]] - }; - shared.rev_hash = generateRevisionHash({ - "_id": "doc1", - "_data": "def", - "_attachment": "attmt2", - "_content_type": "" - }, shared.revisions); - shared.rev = "3-" + shared.rev_hash; - return jio.putAttachment({ - "_id": "doc1", - "_data": "def", - "_attachment": "attmt2", - "_rev": shared.prev_rev + } }); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "attmt2", - "id": "doc1", - "method": "putAttachment", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" // XXX should be 201 Created - }, "PutAttachment without document"); - - return jio_local.get({"_id": "doc1." + shared.rev}); - - }).then(function (answer) { - - deepEqual(answer.data, { - "_id": "doc1." + shared.rev, - "title": "My Title", - "_attachments": { - "attmt1": { - "content_type": "", - "length": 3, - "digest": "sha256-ba7816bf8f01cfea414140de5dae2223b00361a3" + - "96177a9cb410ff61f20015ad" - }, - "attmt2": { - "content_type": "", - "length": 3, - "digest": "sha256-cb8379ac2098aa165029e3938a51da0bcecfc008" + - "fd6795f401178647f96c5b34" + this.not_revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname } } - }, "Check document"); - - // check attachment - return jio_local.getAttachment({ - "_id": "doc1." + shared.rev, - "_attachment": "attmt2" }); - - }).then(function (answer) { - - return tool.readBlobAsBinaryString(answer.data); - - }).then(function (event) { - - deepEqual(event.target.result, "def", "Check attachment"); - - }).fail(unexpectedError).always(start); - + } }); - test("Get & GetAttachment", function () { - - var shared = {}, jio, jio_local; - - shared.workspace = {}; - shared.local_storage_description = { - "type": "local", - "username": "revision get", - "mode": "memory" - }; + test("Testing proper adding/removing attachments", + function () { + stop(); + expect(10); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps, + blob2 = this.blob2, + blob1 = this.blob1, + other_blob = this.other_blob, + otherother_blob = new Blob(['abcabc']); + + jio.put("doc", {title: "foo0"}) // 0 + .push(function () { + return jio.put("doc2", {key: "val"}); // 1 + }) + .push(function () { + return jio.putAttachment("doc", "attacheddata", blob1); // 2 + }) + .push(function () { + return jio.putAttachment("doc", "attacheddata", blob2); // 3 + }) + .push(function () { + return jio.putAttachment("doc", "other_attacheddata", other_blob);// 4 + }) + .push(function () { + return jio.putAttachment( // 5 + "doc", + "otherother_attacheddata", + otherother_blob + ); + }) + .push(function () { + return jio.removeAttachment("doc", "otherother_attacheddata"); // 6 + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (result) { + deepEqual(result, { + title: "foo0" + }, "Get does not return any attachment/revision information"); + return jio.getAttachment("doc", "attacheddata"); + }) + .push(function (result) { + deepEqual(result, + blob2, + "Return the attachment information with getAttachment" + ); + return revision.getAttachment( + timestamps[3], + "attacheddata" + ); + }) + .push(function (result) { + deepEqual(result, + blob2, + "Return the attachment information with getAttachment for " + + "current revision" + ); + return revision.getAttachment( + timestamps[2], + "attacheddata" + ); + }, function (error) { + //console.log(error); + ok(false, error); + }) + .push(function (result) { + deepEqual(result, + blob1, + "Return the attachment information with getAttachment for " + + "previous revision" + ); + return jio.getAttachment(timestamps[0], "attached"); + }, function (error) { + ok(false, error); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Error if you try to go back to a nonexistent timestamp"); + deepEqual(error.message, + "revisionStorage: cannot find object '" + timestamps[0] + "'", + "Error caught by revision storage correctly"); + return jio.getAttachment("doc", "other_attacheddata"); + }) + .push(function (result) { + deepEqual(result, + other_blob, + "Other document successfully queried" + ); + }) + .push(function () { + return jio.getAttachment("doc", "otherother_attacheddata"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Error if you try to get a removed attachment"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - jio = jIO.createJIO({ - "type": "revision", - "sub_storage": shared.local_storage_description - }, {"workspace": shared.workspace}); + test("get attachment immediately after removing it", + function () { + stop(); + expect(3); + var jio = this.jio, + blob1 = this.blob1; + + jio.put("doc", {title: "foo0"}) + .push(function () { + return jio.putAttachment("doc", "attacheddata", blob1); + }) + .push(function () { + return jio.removeAttachment("doc", "attacheddata"); + }) + .push(function () { + return jio.getAttachment("doc", "attacheddata"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "throws a jio error"); + deepEqual(error.status_code, + 404, + "allAttachments of a removed document throws a 404 error"); + deepEqual(error.message, + "revisionStorage: cannot find object 'doc' (removed)", + "Error is handled by revisionstorage."); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - jio_local = jIO.createJIO(shared.local_storage_description, { - "workspace": shared.workspace + test("Ordering of put and remove attachments is correct", + function () { + stop(); + expect(1); + var jio = this.jio, + blob1 = this.blob1, + blob2 = this.blob2; + + jio.put("doc", {title: "foo0"}) + .push(function () { + return jio.putAttachment("doc", "data", blob1); + }) + .push(function () { + return jio.removeAttachment("doc", "data"); + }) + .push(function () { + return jio.putAttachment("doc", "data", blob2); + }) + .push(function () { + return jio.getAttachment("doc", "data"); + }) + .push(function (result) { + deepEqual(result, + blob2, + "removeAttachment happens before putAttachment" + ); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); }); - stop(); - - success(jio.get({"_id": "get1"})).then(function (answer) { - - deepEqual(answer, { - "error": "not_found", - "id": "get1", - "message": "Document not found", - "method": "get", - "reason": "missing", - "result": "error", - "status": 404, - "statusText": "Not Found" - }, "Get inexistent document (winner) -> 404 Not Found"); - - return success(jio.getAttachment({"_id": "get1", "_attachment": "get2"})); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "get2", - "error": "not_found", - "id": "get1", - "message": "Document not found", - "method": "getAttachment", - "reason": "missing", - "result": "error", - "status": 404, - "statusText": "Not Found" - }, "Get inexistent attachment (winner) -> 404 Not Found"); - - // adding a document - shared.doctree = { - "_id": "get1.revision_tree.json", - "children": JSON.stringify([{ - "rev": "1-rev1", - "status": "available", - "children": [] - }]) - }; - shared.doc_myget1 = {"_id": "get1.1-rev1", "title": "myGet1"}; - - - return jio_local.put(shared.doctree); - }).then(function () { - return jio_local.put(shared.doc_myget1); - }).then(function () { - - // get document - shared.doc_myget1_cloned = tool.deepClone(shared.doc_myget1); - shared.doc_myget1_cloned._id = "get1"; - shared.doc_myget1_cloned._rev = "1-rev1"; - shared.doc_myget1_cloned._revisions = {"start": 1, "ids": ["rev1"]}; - shared.doc_myget1_cloned._revs_info = [{ - "rev": "1-rev1", - "status": "available" - }]; - - return jio.get({"_id": "get1"}, { - "revs_info": true, - "revs": true, - "conflicts": true - }); + test("Correctness of allAttachments method on current attachments", + function () { + stop(); + expect(14); + var jio = this.jio, + not_revision = this.not_revision, + blob1 = this.blob1, + blob2 = this.blob2, + blob3 = this.blob3, + other_blob = this.other_blob; + + jio.put("doc", {title: "foo0"}) + .push(function () { + return jio.put("doc2", {key: "val"}); + }) + .push(function () { + return jio.putAttachment("doc", "attacheddata", blob1); + }) + .push(function () { + return jio.putAttachment("doc", "attacheddata", blob2); + }) + .push(function () { + return jio.putAttachment("doc", "other_attacheddata", other_blob); + }) + .push(function () { + return jio.allAttachments("doc"); + }) + .push(function (results) { + deepEqual(results, { + "attacheddata": blob2, + "other_attacheddata": other_blob + }, "allAttachments works as expected."); + return jio.removeAttachment("doc", "attacheddata"); // + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (result) { + deepEqual(result, { + title: "foo0" + }, "Get does not return any attachment information"); + return jio.getAttachment("doc", "attacheddata"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Removed attachments cannot be queried (4)"); + return jio.allAttachments("doc"); + }) + .push(function (results) { + deepEqual(results, { + "other_attacheddata": blob2 + }, "allAttachments works as expected with a removed attachment"); + return jio.putAttachment("doc", "attacheddata", blob3); // + }) + .push(function () { + return not_revision.allDocs(); + }) + .push(function (results) { + var promises = results.data.rows.map(function (data) { + return not_revision.get(data.id); + }); + return RSVP.all(promises); + }) + .push(function (results) { + deepEqual(results, [ + {timestamp: results[0].timestamp, + doc_id: "doc", doc: results[0].doc, op: "put"}, + {timestamp: results[1].timestamp, + doc_id: "doc2", doc: results[1].doc, op: "put"}, + {timestamp: results[2].timestamp, + doc_id: "doc", name: "attacheddata", op: "putAttachment"}, + {timestamp: results[3].timestamp, + doc_id: "doc", name: "attacheddata", op: "putAttachment"}, + {timestamp: results[4].timestamp, + doc_id: "doc", name: "other_attacheddata", op: "putAttachment"}, + {timestamp: results[5].timestamp, + doc_id: "doc", name: "attacheddata", op: "removeAttachment"}, + {timestamp: results[6].timestamp, + doc_id: "doc", name: "attacheddata", op: "putAttachment"} + ], "Other storage can access all document revisions." + ); + }) + .push(function () { + return jio.allDocs(); + }) + .push(function (results) { + equal(results.data.total_rows, + 2, + "Two documents in accessible storage"); + return jio.get(results.data.rows[1].id); + }) + .push(function (result) { + deepEqual(result, { + "key": "val" + }, "Get second document accessible from jio storage"); + + return not_revision.allDocs(); + }) + .push(function (results) { + return RSVP.all(results.data.rows.map(function (d) { + return not_revision.get(d.id); + })); + }) + .push(function (results) { + equal(results.length, 7, "Seven document revisions in storage"); + return jio.remove("doc"); + }) + .push(function () { + return jio.getAttachment("doc", "attacheddata"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Cannot get the attachment of a removed document"); + }) + .push(function () { + return jio.allAttachments("doc"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "throws a jio error"); + deepEqual(error.status_code, + 404, + "allAttachments of a removed document throws a 404 error"); + deepEqual(error.message, + "revisionStorage: cannot find object 'doc' (removed)", + "Error is handled by revisionstorage."); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_myget1_cloned, "Get document (winner)"); - - // adding two documents - shared.doctree = { - "_id": "get1.revision_tree.json", - "children": JSON.stringify([{ - "rev": "1-rev1", - "status": "available", - "children": [] - }, { - "rev": "1-rev2", - "status": "available", - "children": [{ - "rev": "2-rev3", - "status": "available", - "children": [] - }] - }]) - }; - shared.doc_myget2 = {"_id": "get1.1-rev2", "title": "myGet2"}; - shared.doc_myget3 = {"_id": "get1.2-rev3", "title": "myGet3"}; - - return jio_local.put(shared.doctree); - }).then(function () { - return jio_local.put(shared.doc_myget2); - }).then(function () { - return jio_local.put(shared.doc_myget3); - }).then(function () { - - // get document - shared.doc_myget3_cloned = tool.deepClone(shared.doc_myget3); - shared.doc_myget3_cloned._id = "get1"; - shared.doc_myget3_cloned._rev = "2-rev3"; - shared.doc_myget3_cloned._revisions = - {"start": 2, "ids": ["rev3", "rev2"]}; - shared.doc_myget3_cloned._revs_info = [{ - "rev": "2-rev3", - "status": "available" - }, { - "rev": "1-rev2", - "status": "available" - }]; - shared.doc_myget3_cloned._conflicts = ["1-rev1"]; - - return jio.get({"_id": "get1"}, { - "revs_info": true, - "revs": true, - "conflicts": true - }); - }).then(function (answer) { - - deepEqual(answer.data, - shared.doc_myget3_cloned, - "Get document (winner, after posting another one)"); - - return success(jio.get({"_id": "get1", "_rev": "1-rev0"}, { - "revs_info": true, - "revs": true, - "conflicts": true - })); - - }).then(function (answer) { - - deepEqual(answer, { - "error": "not_found", - "id": "get1", - "message": "Unable to find the document", - "method": "get", - "reason": "missing", - "result": "error", - "status": 404, - "statusText": "Not Found" - }, "Get document (inexistent specific revision)"); - - // get specific document - shared.doc_myget2_cloned = tool.deepClone(shared.doc_myget2); - shared.doc_myget2_cloned._id = "get1"; - shared.doc_myget2_cloned._rev = "1-rev2"; - shared.doc_myget2_cloned._revisions = {"start": 1, "ids": ["rev2"]}; - shared.doc_myget2_cloned._revs_info = [{ - "rev": "1-rev2", - "status": "available" - }]; - shared.doc_myget2_cloned._conflicts = ["1-rev1"]; - return jio.get({"_id": "get1", "_rev": "1-rev2"}, { - "revs_info": true, - "revs": true, - "conflicts": true - }); + test("Correctness of allAttachments method on older revisions", + function () { + stop(); + expect(11); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + blob1 = new Blob(['a']), + blob11 = new Blob(['ab']), + blob2 = new Blob(['abc']), + blob22 = new Blob(['abcd']), + timestamps; + + jio.put("doc", {title: "foo0"}) // 0 + .push(function () { + return jio.putAttachment("doc", "data", blob1); + }) + .push(function () { + return jio.putAttachment("doc", "data2", blob2); + }) + .push(function () { + return jio.put("doc", {title: "foo1"}); // 1 + }) + .push(function () { + return jio.removeAttachment("doc", "data2"); + }) + .push(function () { + return jio.put("doc", {title: "foo2"}); // 2 + }) + .push(function () { + return jio.putAttachment("doc", "data", blob11); + }) + .push(function () { + return jio.remove("doc"); // 3 + }) + .push(function () { + return jio.put("doc", {title: "foo3"}); // 4 + }) + .push(function () { + return jio.putAttachment("doc", "data2", blob22); + }) + .push(function () { + return not_revision.allDocs({ + query: "op: put OR op: remove", + sort_on: [["timestamp", "ascending"]], + select_list: ["timestamp"] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) + .push(function () { + return jio.allAttachments("doc"); + }) + .push(function (results) { + deepEqual(results, { + "data": blob11, + "data2": blob22 + }, + "Current state of document is correct"); + + return revision.allAttachments(timestamps[0]); + }) + .push(function (results) { + deepEqual(results, {}, "First version of document has 0 attachments"); + + return revision.allAttachments(timestamps[1]); + }) + .push(function (results) { + deepEqual(results, { + data: blob1, + data2: blob2 + }, "Both attachments are included in allAttachments"); + + return revision.allAttachments(timestamps[2]); + }) + .push(function (results) { + deepEqual(results, { + data: blob1 + }, "Removed attachment does not show up in allAttachments"); + return revision.allAttachments(timestamps[3]); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "throws a jio error"); + deepEqual(error.status_code, + 404, + "allAttachments of a removed document throws a 404 error"); + deepEqual(error.message, + "revisionStorage: cannot find object '" + timestamps[3] + + "' (removed)", + "Error is handled by revisionstorage."); + }) + .push(function () { + return revision.allAttachments(timestamps[4]); + }) + .push(function (results) { + deepEqual(results, { + data: blob11 + }); + }) + .push(function () { + return revision.allAttachments("not-a-timestamp-or-doc_id"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "throws a jio error"); + deepEqual(error.status_code, + 404, + "allAttachments of a removed document throws a 404 error"); + deepEqual(error.message, + "revisionStorage: cannot find object 'not-a-timestamp-or-doc_id'", + "Error is handled by revisionstorage."); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function (answer) { - deepEqual(answer.data, - shared.doc_myget2_cloned, - "Get document (specific revision)"); - // adding an attachment - shared.attmt_myget3 = { - "get2": { - "length": 3, - "digest": "sha256-ba7816bf8f01cfea414140de5dae2223b00361a3" + - "96177a9cb410ff61f20015ad", - "content_type": "oh/yeah" + ///////////////////////////////////////////////////////////////// + // Querying older revisions + ///////////////////////////////////////////////////////////////// + + module("revisionStorage.get", { + setup: function () { + // create storage of type "revision" with memory as substorage + var dbname = "db_" + Date.now(); + this.jio = jIO.createJIO({ + type: "query", + sub_storage: { + type: "revision", + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } } - }; - shared.doc_myget3._attachments = shared.attmt_myget3; - - return jio_local.putAttachment({ - "_id": shared.doc_myget3._id, - "_attachment": "get2", - "_data": "abc", - "_content_type": "oh/yeah" }); - - }).then(function () { - - return jio.getAttachment({"_id": "get1", "_attachment": "get2"}); - - }).then(function (answer) { - - return tool.readBlobAsBinaryString(answer.data); - - }).then(function (event) { - - deepEqual(event.target.result, "abc", "Get attachment (winner)"); - - // get inexistent attachment specific rev - return success(jio.getAttachment({ - "_id": "get1", - "_attachment": "get2", - "_rev": "1-rev1" - }, { - "revs_info": true, - "revs": true, - "conflicts": true - })); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "get2", - "error": "not_found", - "id": "get1", - "message": "Unable to get an inexistent attachment", - "method": "getAttachment", - "reason": "missing", - "result": "error", - "status": 404, - "statusText": "Not Found" - }, "Get inexistent attachment (specific revision) -> 404 Not Found"); - - return jio.getAttachment({ - "_id": "get1", - "_attachment": "get2", - "_rev": "2-rev3" - }, { - "revs_info": true, - "revs": true, - "conflicts": true - }); - - }).then(function (answer) { - - return tool.readBlobAsBinaryString(answer.data); - - }).then(function (event) { - - deepEqual(event.target.result, - "abc", - "Get attachment (specific revision)"); - - // get document with attachment (specific revision) - delete shared.doc_myget2_cloned._attachments; - return jio.get({"_id": "get1", "_rev": "1-rev2"}, { - "revs_info": true, - "revs": true, - "conflicts": true + this.revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "revision", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + } }); - - }).then(function (answer) { - - deepEqual(answer.data, - shared.doc_myget2_cloned, - "Get document which have an attachment (specific revision)"); - - // get document with attachment (winner) - shared.doc_myget3_cloned._attachments = shared.attmt_myget3; - return jio.get({"_id": "get1"}, { - "revs_info": true, - "revs": true, - "conflicts": true + this.not_revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } }); + } + }); - }).then(function (answer) { - - deepEqual(answer.data, - shared.doc_myget3_cloned, - "Get document which have an attachment (winner)"); + test("Removing documents before putting them", + function () { + stop(); + expect(4); + var jio = this.jio; + + jio.remove("doc") + .push(function () { + return jio.put("doc2", {title: "foo"}); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function () { + ok(false, "This statement should not be reached"); + }, function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Correct status code for getting a non-existent document" + ); + deepEqual(error.message, + "revisionStorage: cannot find object 'doc' (removed)", + "Error is handled by revision storage before reaching console"); + }) + .push(function () { + return jio.allDocs({select_list: ["title"]}); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: "doc2", + value: {title: "foo"}, + //timestamp: timestamps[1], + doc: {} + }], "Document that was removed before being put is not retrieved"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).fail(unexpectedError).always(start); + test("Removing documents and then putting them", + function () { + stop(); + expect(2); + var jio = this.jio, + revision = this.revision, + timestamps, + not_revision = this.not_revision; + + jio.remove("doc") + .push(function () { + return jio.put("doc", {title: "foo"}); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (result) { + deepEqual(result, { + title: "foo" + }, "A put was the most recent edit on 'doc'"); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return revision.allDocs({select_list: ["title"]}); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + //id: "doc", + value: {title: "foo"}, + id: timestamps[1], + doc: {} + }, + { + value: {}, + id: timestamps[0], + doc: {} + }], "DOcument that was removed before being put is not retrieved"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }); + test("Handling bad input", + function () { + stop(); + expect(2); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamp; + + jio.put("doc", {title: "foo"}) + .push(function () { + return not_revision.allDocs(); + }) + .push(function (res) { + timestamp = res.data.rows[0].id; + return jio.put(timestamp, {key: "val"}); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (result) { + deepEqual(result, { + title: "foo" + }, "Saving document with timestamp id does not cause issues (1)"); + return revision.get(timestamp); + }) + .push(function (result) { + deepEqual(result, { + title: "foo" + }, "Saving document with timestamp id does not cause issues (2)"); + return revision.get(timestamp); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - test("Remove & Remove Attachment", function () { + test("Getting a non-existent document", + function () { + stop(); + expect(3); + var jio = this.jio; + jio.put("not_doc", {}) + .push(function () { + return jio.get("doc"); + }) + .push(function () { + ok(false, "This statement should not be reached"); + }, function (error) { + //console.log(error); + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Correct status code for getting a non-existent document" + ); + deepEqual(error.message, + "revisionStorage: cannot find object 'doc'", + "Error is handled by revision storage before reaching console"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - var shared = {}, jio, jio_local; + test("Getting a document with timestamp when include_revisions is false", + function () { + stop(); + expect(6); + var jio = this.jio, + not_revision = this.not_revision, + timestamp; + jio.put("not_doc", {}) + .push(function () { + return jio.get("doc"); + }) + .push(function () { + ok(false, "This statement should not be reached"); + }, function (error) { + //console.log(error); + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Correct status code for getting a non-existent document" + ); + deepEqual(error.message, + "revisionStorage: cannot find object 'doc'", + "Error is handled by revision storage before reaching console"); + }) + .push(function () { + return not_revision.allDocs(); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + return jio.get(timestamp); + }) + .push(function () { + ok(false, "This statement should not be reached"); + }, function (error) { + //console.log(error); + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Correct status code for getting a non-existent document" + ); + deepEqual(error.message, + "revisionStorage: cannot find object '" + timestamp + "'", + "Error is handled by revision storage before reaching console"); + }) + + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - shared.workspace = {}; - shared.local_storage_description = { - "type": "local", - "username": "revision remove", - "mode": "memory" - }; + test("Creating a document with put and retrieving it with get", + function () { + stop(); + expect(5); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps; + jio.put("doc", {title: "version0"}) + .push(function () { + return not_revision.allDocs({ + select_list: ["timestamp"] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) + .push(function () { + equal(timestamps.length, + 1, + "One revision is saved in storage" + ); + return revision.get(timestamps[0]); + }) + .push(function (result) { + deepEqual(result, { + title: "version0" + }, "Get document from revision storage"); + return not_revision.get( + timestamps[0] + ); + }) + .push(function (result) { + deepEqual(result, { + timestamp: timestamps[0], + op: "put", + doc_id: "doc", + doc: { + title: "version0" + } + }, "Get document from non-revision storage"); + }) + .push(function () { + return jio.get("non-existent-doc"); + }) + .push(function () { + ok(false, "This should have thrown an error"); + }, function (error) { + //console.log(error); + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Can't access non-existent document" + ); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - jio = jIO.createJIO({ - "type": "revision", - "sub_storage": shared.local_storage_description - }, {"workspace": shared.workspace}); + test("Updating a document with include revisions", + function () { + stop(); + expect(1); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps, + t_id; + jio.put("doc", {title: "version0"}) + .push(function () { + return revision.put("doc", {title: "version1"}); + }) + .push(function () { + return not_revision.allDocs({sort_on: [["timestamp", "ascending"]]}); + }) + .push(function (results) { + t_id = results.data.rows[0].id; + return revision.put(t_id, {title: "version0.1"}); + }) + .push(function () { + return jio.put(t_id, {title: "label0"}); + }) + .push(function () { + return revision.put("1234567891012-abcd", {k: "v"}); + }) + .push(function () { + return not_revision.allDocs({ + select_list: ["timestamp"] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]], + select_list: ["timestamp", "op", "doc_id", "doc"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: timestamps[0], + doc: {}, + value: { + timestamp: timestamps[0], + op: "put", + doc_id: "doc", + doc: { + title: "version0" + } + } + }, + { + id: timestamps[1], + doc: {}, + value: { + timestamp: timestamps[1], + op: "put", + doc_id: "doc", + doc: { + title: "version1" + } + } + }, + { + id: timestamps[2], + doc: {}, + value: { + timestamp: timestamps[2], + op: "put", + doc_id: "doc", + doc: { + title: "version0.1" + } + } + }, + { + id: timestamps[3], + doc: {}, + value: { + timestamp: timestamps[3], + op: "put", + doc_id: timestamps[0], + doc: { + title: "label0" + } + } + }, + { + id: timestamps[4], + doc: {}, + value: { + timestamp: timestamps[4], + op: "put", + doc_id: "1234567891012-abcd", + doc: { + k: "v" + } + } + } + ], "Documents stored with correct metadata"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - jio_local = jIO.createJIO(shared.local_storage_description, { - "workspace": shared.workspace + test("Retrieving older revisions with get", + function () { + stop(); + expect(7); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps; + + return jio.put("doc", {title: "t0", subtitle: "s0"}) + .push(function () { + return jio.put("doc", {title: "t1", subtitle: "s1"}); + }) + .push(function () { + return jio.put("doc", {title: "t2", subtitle: "s2"}); + }) + .push(function () { + jio.remove("doc"); + }) + .push(function () { + return jio.put("doc", {title: "t3", subtitle: "s3"}); + }) + .push(function () { + return not_revision.allDocs({ + select_list: ["timestamp"], + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (result) { + deepEqual(result, { + title: "t3", + subtitle: "s3" + }, "Get returns latest revision"); + return revision.get(timestamps[0]); + }, function (err) { + ok(false, err); + }) + .push(function (result) { + deepEqual(result, { + title: "t0", + subtitle: "s0" + }, "Get returns first version"); + return revision.get(timestamps[1]); + }) + .push(function (result) { + deepEqual(result, { + title: "t1", + subtitle: "s1" + }, "Get returns second version"); + return revision.get(timestamps[2]); + }, function (err) { + ok(false, err); + }) + .push(function (result) { + deepEqual(result, { + title: "t2", + subtitle: "s2" + }, "Get returns third version"); + return revision.get(timestamps[3]); + }, function (err) { + ok(false, err); + }) + .push(function () { + ok(false, "This should have thrown a 404 error"); + return revision.get(timestamps[4]); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Error if you try to go back more revisions than what exists"); + return revision.get(timestamps[4]); + }) + .push(function (result) { + deepEqual(result, { + title: "t3", + subtitle: "s3" + }, "Get returns latest version"); + }) + + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); }); - stop(); - - // 1. remove document without revision - success(jio.remove({"_id": "remove1"})).then(function (answer) { - - deepEqual(answer, { - "error": "conflict", - "id": "remove1", - "message": "Document update conflict", - "method": "remove", - "reason": "No document revision was provided", - "result": "error", - "status": 409, - "statusText": "Conflict" - }, "Remove document without revision -> 409 Conflict"); - - // 2. remove attachment without revision - return success(jio.removeAttachment({ - "_id": "remove1", - "_attachment": "remove2" - })); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "remove2", - "error": "conflict", - "id": "remove1", - "message": "Document update conflict", - "method": "removeAttachment", - "reason": "No document revision was provided", - "result": "error", - "status": 409, - "statusText": "Conflict" - }, "Remove attachment without revision -> 409 Conflict"); - - // adding a document with attachments - shared.doc_myremove1 = { - "_id": "remove1.1-veryoldrev", - "title": "myRemove1" - }; - - return jio_local.put(shared.doc_myremove1); - - }).then(function () { - - shared.doc_myremove1._id = "remove1.2-oldrev"; - shared.attachment_remove2 = { - "length": 3, - "digest": "md5-dontcare", - "content_type": "oh/yeah" - }; - shared.attachment_remove3 = { - "length": 5, - "digest": "sha256-383395a769131d15c1c6fc57c6abdb759ace9809" + - "c1ad20d1f491d90f7f02650e", - "content_type": "he/ho" - }; - shared.doc_myremove1._attachments = { - "remove2": shared.attachment_remove2, - "remove3": shared.attachment_remove3 - }; - - return jio_local.put(shared.doc_myremove1); - - }).then(function () { - - return jio_local.putAttachment({ - "_id": "remove1.2-oldrev", - "_attachment": "remove2", - "_data": "abc", - "_content_type": "oh/yeah" - }); + test("verifying updates correctly when puts are done in parallel", + function () { + stop(); + expect(8); + var jio = this.jio, + not_revision = this.not_revision; + + jio.put("bar", {"title": "foo0"}) + .push(function () { + return RSVP.all([ + jio.put("bar", {"title": "foo1"}), + jio.put("bar", {"title": "foo2"}), + jio.put("bar", {"title": "foo3"}), + jio.put("bar", {"title": "foo4"}), + jio.put("barbar", {"title": "attr0"}), + jio.put("barbar", {"title": "attr1"}), + jio.put("barbar", {"title": "attr2"}), + jio.put("barbar", {"title": "attr3"}) + ]); + }) + .push(function () {return jio.get("bar"); }) + .push(function (result) { + ok(result.title !== "foo0", "Title should have changed from foo0"); + }) + .push(function () { + return not_revision.allDocs({ + query: "", + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + equal(results.data.total_rows, + 9, + "All nine versions exist in storage"); + return not_revision.get(results.data.rows[0].id); + }) + .push(function (results) { + deepEqual(results, { + doc_id: "bar", + doc: { + title: "foo0" + }, + timestamp: results.timestamp, + op: "put" + }, "The first item in the log is pushing bar's title to 'foo0'"); + return jio.remove("bar"); + }) + .push(function () { + return jio.get("bar"); + }) + .push(function () { + return jio.get("barbar"); + }, function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + equal(error.status_code, 404, "Correct error status code returned"); + return jio.get("barbar"); + }) + .push(function (result) { + ok(result.title !== undefined, "barbar exists and has proper form"); + return not_revision.allDocs({ + query: "", + sort_on: [["op", "descending"]] + }); + }) + .push(function (results) { + equal(results.data.total_rows, + 10, + "Remove operation is recorded"); + return not_revision.get(results.data.rows[0].id); + }) + .push(function (result) { + deepEqual(result, { + doc_id: "bar", + timestamp: result.timestamp, + op: "remove" + }); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function () { + test("Getting after attachments have been put", + function () { + stop(); + expect(4); + var jio = this.jio, + not_revision = this.not_revision, + revision = this.revision, + blob = new Blob(['a']), + edit_log; + + jio.put("doc", {"title": "foo0"}) + .push(function () { + return jio.putAttachment("doc", "attachment", blob); + }) + .push(function () { + return jio.removeAttachment("doc", "attachment", blob); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (res) { + deepEqual(res, + {title: "foo0"}, + "Correct information returned"); + return not_revision.allDocs({select_list: ["title"]}); + }) + .push(function (results) { + edit_log = results.data.rows; + return revision.get(edit_log[0].id); + }) + .push(function (result) { + deepEqual(result, {title: "foo0"}); + return revision.get(edit_log[1].id); + }) + .push(function (result) { + deepEqual(result, {title: "foo0"}); + return revision.get(edit_log[2].id); + }) + .push(function (result) { + deepEqual(result, {title: "foo0"}); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - return jio_local.putAttachment({ - "_id": "remove1.2-oldrev", - "_attachment": "remove3", - "_data": "defgh", - "_content_type": "he/ho" - }); - }).then(function () { - - // add document tree - shared.doctree = { - "_id": "remove1.revision_tree.json", - "children": JSON.stringify([{ - "rev": "1-veryoldrev", - "status": "available", - "children": [{ - "rev": "2-oldrev", - "status": "available", - "children": [] - }] - }]) - }; - - return jio_local.put(shared.doctree); - - }).then(function () { - - // 3. remove inexistent attachment - return success(jio.removeAttachment({ - "_id": "remove1", - "_attachment": "remove0", - "_rev": "2-oldrev" - })); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "remove0", - "error": "not_found", - "id": "remove1", - "message": "Unable to remove an inexistent attachment", - "method": "removeAttachment", - "reason": "missing", - "result": "error", - "status": 404, - "statusText": "Not Found" - }, "Remove inexistent attachment -> 404 Not Found"); - - // 4. remove existing attachment - shared.rev_hash = generateRevisionHash({ - "_id": "remove1", - "_attachment": "remove2" - }, {"start": 2, "ids": ["oldrev", "veryoldrev"]}); - - return jio.removeAttachment({ - "_id": "remove1", - "_attachment": "remove2", - "_rev": "2-oldrev" + ///////////////////////////////////////////////////////////////// + // Querying older revisions + ///////////////////////////////////////////////////////////////// + + module("revisionStorage.allDocs", { + setup: function () { + // create storage of type "revision" with memory as substorage + this.dbname = "db_" + Date.now(); + this.jio = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "revision", + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: this.dbname + } + } + } + } }); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "remove2", - "id": "remove1", - "method": "removeAttachment", - "result": "success", - "rev": "3-" + shared.rev_hash, - "status": 204, - "statusText": "No Content" - }, "Remove existing attachment"); - - shared.doctree = { - "_id": "remove1.revision_tree.json", - "children": JSON.stringify([{ - "rev": "1-veryoldrev", - "status": "available", - "children": [{ - "rev": "2-oldrev", - "status": "available", - "children": [{ - "rev": "3-" + shared.rev_hash, - "status": "available", - "children": [] - }] - }] - }]) - }; - - // 5. check if document tree has been updated correctly - return jio_local.get({"_id": shared.doctree._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doctree, "Check document tree"); - - // 6. check if the attachment still exists - return jio_local.getAttachment({ - "_id": "remove1.2-oldrev", - "_attachment": "remove2" + this.revision = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "revision", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: this.dbname + } + } + } + } }); - - }).then(function (answer) { - - return tool.readBlobAsBinaryString(answer.data); - - }).then(function (event) { - - deepEqual(event.target.result, "abc", "Check attachment -> still exists"); - - // 7. check if document is updated - return jio_local.get({"_id": "remove1.3-" + shared.rev_hash}); - - }).then(function (answer) { - - deepEqual(answer.data, { - "_id": "remove1.3-" + shared.rev_hash, - "title": "myRemove1", - "_attachments": { - "remove3": shared.attachment_remove3 + this.not_revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: this.dbname + } } - }, "Check document"); - - // 8. remove document with wrong revision - return success(jio.remove({"_id": "remove1", "_rev": "1-a"})); - - }).then(function (answer) { - - deepEqual(answer, { - "error": "conflict", - "id": "remove1", - "message": "Document update conflict", - "method": "remove", - "reason": "Document is missing", - "result": "error", - "status": 409, - "statusText": "Conflict" - }, "Remove document with wrong revision -> 409 Conflict"); - - // 9. remove attachment wrong revision - return success(jio.removeAttachment({ - "_id": "remove1", - "_attachment": "remove2", - "_rev": "1-a" - })); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "remove2", - "error": "conflict", - "id": "remove1", - "message": "Document update conflict", - "method": "removeAttachment", - "reason": "Document is missing", - "result": "error", - "status": 409, - "statusText": "Conflict" - }, "Remove attachment with wrong revision -> 409 Conflict"); - - // 10. remove document - shared.last_rev = "3-" + shared.rev_hash; - shared.rev_hash = generateRevisionHash( - {"_id": "remove1"}, - {"start": 3, "ids": [shared.rev_hash, "oldrev", "veryoldrev"]}, - true - ); - return jio.remove({"_id": "remove1", "_rev": shared.last_rev}); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "remove1", - "method": "remove", - "result": "success", - "rev": "4-" + shared.rev_hash, - "status": 204, - "statusText": "No Content" - }, "Remove document"); - - // 11. check document tree - shared.doctree.children = JSON.parse(shared.doctree.children); - shared.doctree.children[0].children[0].children[0].children.unshift({ - "rev": "4-" + shared.rev_hash, - "status": "deleted", - "children": [] }); - shared.doctree.children = JSON.stringify(shared.doctree.children); - return jio_local.get({"_id": shared.doctree._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doctree, "Check document tree"); - - }).fail(unexpectedError).always(start); - + } }); + test("Putting a document and retrieving it with allDocs", + function () { + stop(); + expect(7); + var jio = this.jio, + not_revision = this.not_revision, + timestamp; + jio.put("doc", {title: "version0"}) + .push(function () { + return not_revision.allDocs({ + query: "doc_id: doc", + select_list: ["timestamp"] + }); + }) + .push(function (results) { + timestamp = results.data.rows[0].value.timestamp; + }) + .push(function () { + return RSVP.all([ + jio.allDocs(), + jio.allDocs({query: "title: version0"}), + jio.allDocs({limit: [0, 1]}), + jio.allDocs({}) + ]); + }) + .push(function (results) { + var ind = 0; + for (ind = 0; ind < results.length - 1; ind += 1) { + deepEqual(results[ind], + results[ind + 1], + "Each query returns exactly the same correct output" + ); + } + return results[0]; + }) + .push(function (results) { + equal(results.data.total_rows, + 1, + "Exactly one result returned"); + deepEqual(results.data.rows[0], { + doc: {}, + value: {}, + //timestamp: timestamp, + id: "doc" + }, + "Correct document format is returned." + ); + return not_revision.allDocs(); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + equal(results.data.total_rows, + 1, + "Exactly one result returned"); + return not_revision.get(timestamp); + }) + .push(function (result) { + deepEqual(result, { + doc_id: "doc", + doc: { + title: "version0" + }, + timestamp: timestamp, + op: "put" + }, + "When a different type of storage queries revisionstorage, all " + + "metadata is returned correctly" + ); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - test("allDocs", function () { - - var shared = {}, jio; - - shared.workspace = {}; - shared.local_storage_description = { - "type": "local", - "username": "revision alldocs", - "mode": "memory" - }; - - jio = jIO.createJIO({ - "type": "revision", - "sub_storage": shared.local_storage_description - }, {"workspace": shared.workspace}); - - stop(); - - // adding 3 documents - jio.put({"_id": "yes"}).then(function (answer) { - - shared.rev1 = answer.rev; - - return jio.put({"_id": "no"}); - - }).then(function (answer) { - - shared.rev2 = answer.rev; - - return jio.put({"_id": "maybe"}); + test("Putting doc with troublesome properties and retrieving with allDocs", + function () { + stop(); + expect(1); + var jio = this.jio; + jio.put("doc", { + title: "version0", + doc_id: "bar", + _doc_id: "bar2", + timestamp: "foo", + _timestamp: "foo2", + id: "baz", + _id: "baz2", + __id: "baz3", + op: "zop" + }) + .push(function () { + return jio.allDocs({ + query: "title: version0 AND _timestamp: >= 0", + select_list: ["title", "doc_id", "_doc_id", "timestamp", + "_timestamp", "id", "_id", "__id", "op"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc", + //timestamp: timestamp, + value: { + title: "version0", + doc_id: "bar", + _doc_id: "bar2", + timestamp: "foo", + _timestamp: "foo2", + id: "baz", + _id: "baz2", + __id: "baz3", + op: "zop" + } + }], + "Poorly-named properties are not overwritten in allDocs call"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function (answer) { + test("Putting a document, revising it, and retrieving revisions with allDocs", + function () { + stop(); + expect(10); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps; + jio.put("doc", { + title: "version0", + subtitle: "subvers0" + }) + .push(function () { + return jio.put("doc", { + title: "version1", + subtitle: "subvers1" + }); + }) + .push(function () { + return jio.put("doc", { + title: "version2", + subtitle: "subvers2" + }); + }) + .push(function () { + return not_revision.allDocs({ + select_list: ["timestamp"], + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) + .push(function () { + return RSVP.all([ + jio.allDocs({select_list: ["title", "subtitle"]}), + jio.allDocs({ + query: "", + select_list: ["title", "subtitle"] + }), + jio.allDocs({ + query: "title: version2", + select_list: ["title", "subtitle"] + }), + jio.allDocs({ + query: "NOT (title: version1)", + select_list: ["title", "subtitle"] + }), + jio.allDocs({ + query: "(NOT (subtitle: subvers1)) AND (NOT (title: version0))", + select_list: ["title", "subtitle"] + }), + jio.allDocs({ + limit: [0, 1], + sort_on: [["title", "ascending"]], + select_list: ["title", "subtitle"] + }) + ]); + }) + .push(function (results) { + var ind = 0; + for (ind = 0; ind < results.length - 1; ind += 1) { + deepEqual(results[ind], + results[ind + 1], + "Each query returns exactly the same correct output" + ); + } + return results[0]; + }) + .push(function (results) { + equal(results.data.total_rows, + 1, + "Exactly one result returned"); + deepEqual(results.data.rows[0], { + value: { + title: "version2", + subtitle: "subvers2" + }, + doc: {}, + //timestamp: timestamps[2], + id: "doc" + }, + "Correct document format is returned." + ); + }) + .push(function () { + return revision.allDocs({ + query: "", + select_list: ["title", "subtitle"] + }); + }) + .push(function (results) { + equal(results.data.total_rows, + 3, + "Querying with include_revisions retrieves all versions"); + deepEqual(results.data.rows, [ + { + //id: results.data.rows[0].id, + value: { + title: "version2", + subtitle: "subvers2" + }, + id: timestamps[2], + doc: {} + }, + { + //id: results.data.rows[1].id, + value: { + title: "version1", + subtitle: "subvers1" + }, + id: timestamps[1], + doc: {} + }, + { + //id: results.data.rows[2].id, + value: { + title: "version0", + subtitle: "subvers0" + }, + id: timestamps[0], + doc: {} + } + ], "Full version revision is included."); + + return not_revision.allDocs({ + sort_on: [["title", "ascending"]] + }); + }) + .push(function (results) { + return RSVP.all(results.data.rows.map(function (d) { + return not_revision.get(d.id); + })); + }) + .push(function (results) { + deepEqual(results, [ + { + timestamp: timestamps[0], + op: "put", + doc_id: "doc", + doc: { + title: "version0", + subtitle: "subvers0" + } + }, + { + timestamp: timestamps[1], + op: "put", + doc_id: "doc", + doc: { + title: "version1", + subtitle: "subvers1" + } + }, + { + timestamp: timestamps[2], + op: "put", + doc_id: "doc", + doc: { + title: "version2", + subtitle: "subvers2" + } + } + ], + "A different storage type can retrieve all versions as expected."); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - shared.rev3 = answer.rev; - // adding conflicts - return jio.put({"_id": "maybe"}); + test( + "Putting and removing documents, latest revisions and no removed documents", + function () { + stop(); + expect(3); + var jio = this.jio, + not_revision = this.not_revision, + timestamps; + + jio.put("doc_a", { + title_a: "rev0", + subtitle_a: "subrev0" + }) + .push(function () { + return jio.put("doc_a", { + title_a: "rev1", + subtitle_a: "subrev1" + }); + }) + .push(function () { + return jio.put("doc_b", { + title_b: "rev0", + subtitle_b: "subrev0" + }); + }) + .push(function () { + return jio.remove("doc_b"); + }) + .push(function () { + return jio.put("doc_c", { + title_c: "rev0", + subtitle_c: "subrev0" + }); + }) + .push(function () { + return jio.put("doc_c", { + title_c: "rev1", + subtitle_c: "subrev1" + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return jio.allDocs({sort_on: [["timestamp", "descending"]]}); + }) + .push(function (results) { + equal(results.data.total_rows, + 2, + "Only two non-removed unique documents exist." + ); + deepEqual(results.data.rows, [ + { + id: "doc_c", + value: {}, + //timestamp: timestamps[5], + doc: {} + }, + { + id: "doc_a", + value: {}, + //timestamp: timestamps[1], + doc: {} + } + ], + "Empty query returns latest revisions (and no removed documents)"); + equal(timestamps.length, + 6, + "Correct number of revisions logged"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + } + ); + + ///////////////////////////////////////////////////////////////// + // Complex Queries + ///////////////////////////////////////////////////////////////// + + test("More complex query with different options (without revision queries)", + function () { + stop(); + expect(2); + var jio = this.jio, + docs = [ + { + "date": 1, + "type": "foo", + "title": "doc" + }, + { + "date": 2, + "type": "bar", + "title": "second_doc" + }, + { + "date": 2, + "type": "barbar", + "title": "third_doc" + } + ], + blobs = [ + new Blob(['a']), + new Blob(['bcd']), + new Blob(['eeee']) + ]; + jio.put("doc", {}) // 0 + .push(function () { + return putFullDoc(jio, "doc", docs[0], "data", blobs[0]); // 1,2 + }) + .push(function () { + return putFullDoc(jio, "second_doc", docs[1], "data", blobs[1]);// 3,4 + }) + .push(function () { + return putFullDoc(jio, "third_doc", docs[2], "data", blobs[2]); // 5,6 + }) + .push(function () { + return jio.allDocs({ + query: "NOT (date: > 2)", + select_list: ["date", "non-existent-key"], + sort_on: [["date", "ascending"], + ["non-existent-key", "ascending"] + ] + }); + }) + .push(function (results) { + equal(results.data.total_rows, 3); + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc", + //timestamp: timestamps[2], + value: {date: 1} + }, + { + doc: {}, + id: "third_doc", + //timestamp: timestamps[6], + value: {date: 2} + }, + { + doc: {}, + id: "second_doc", + //timestamp: timestamps[4], + value: {date: 2} + } + ], + "Query gives correct results in correct order"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function () { + ///////////////////////////////////////////////////////////////// + // Complex Queries with Revision Querying + ///////////////////////////////////////////////////////////////// + + test("More complex query with different options (with revision queries)", + function () { + stop(); + expect(3); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps, + docs = [ + { + "date": 1, + "type": "foo", + "title": "doc" + }, + { + "date": 2, + "type": "bar", + "title": "second_doc" + } + ], + blobs = [ + new Blob(['a']), + new Blob(['bcd']), + new Blob(['a2']), + new Blob(['bcd2']), + new Blob(['a3']) + ]; + jio.put("doc", {})// 0 + .push(function () {// 1,2 + return putFullDoc(jio, "doc", docs[0], "data", blobs[0]); + }) + .push(function () {// 3,4 + return putFullDoc(jio, "second_doc", docs[1], "data", blobs[1]); + }) + .push(function () { + docs[0].date = 4; + docs[0].type = "foo2"; + docs[1].date = 4; + docs[1].type = "bar2"; + }) + .push(function () {// 5,6 + return putFullDoc(jio, "doc", docs[0], "data", blobs[2]); + }) + .push(function () {// 7 + return jio.remove("second_doc"); + }) + .push(function () {// 8,9 + return putFullDoc(jio, "second_doc", docs[1], "data", blobs[3]); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["op", "doc_id", "timestamp"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: timestamps[9], + value: { + "op": "putAttachment", + "doc_id": "second_doc", + "timestamp": timestamps[9] + } + }, + { + doc: {}, + id: timestamps[8], + value: { + "op": "put", + "doc_id": "second_doc", + "timestamp": timestamps[8] + } + }, + { + doc: {}, + id: timestamps[7], + value: { + "op": "remove", + "doc_id": "second_doc", + "timestamp": timestamps[7] + } + }, + { + doc: {}, + id: timestamps[6], + value: { + "op": "putAttachment", + "doc_id": "doc", + "timestamp": timestamps[6] + } + }, + { + doc: {}, + id: timestamps[5], + value: { + "op": "put", + "doc_id": "doc", + "timestamp": timestamps[5] + } + }, + { + doc: {}, + id: timestamps[4], + value: { + "op": "putAttachment", + "doc_id": "second_doc", + "timestamp": timestamps[4] + } + }, + { + doc: {}, + id: timestamps[3], + value: { + "op": "put", + "doc_id": "second_doc", + "timestamp": timestamps[3] + } + }, + { + doc: {}, + id: timestamps[2], + value: { + "op": "putAttachment", + "doc_id": "doc", + "timestamp": timestamps[2] + } + }, + { + doc: {}, + id: timestamps[1], + value: { + "op": "put", + "doc_id": "doc", + "timestamp": timestamps[1] + } + }, + { + doc: {}, + id: timestamps[0], + value: { + "op": "put", + "doc_id": "doc", + "timestamp": timestamps[0] + } + } + ], "All operations are logged correctly"); + var promises = results.data.rows + .filter(function (doc) { + return (doc.value.op === "put"); + }) + .map(function (data) { + return not_revision.get(data.id); + }); + return RSVP.all(promises) + .then(function (results) { + return results.map(function (docum) { + return docum.doc; + }); + }); + }) + .push(function (results) { + deepEqual(results, + [ + { + "date": 4, + "type": "bar2", + "title": "second_doc" + }, + { + "date": 4, + "type": "foo2", + "title": "doc" + }, + { + "date": 2, + "type": "bar", + "title": "second_doc" + }, + { + "date": 1, + "type": "foo", + "title": "doc" + }, + {} + ], "All versions of documents are stored correctly"); + }) + .push(function () { + return revision.allDocs({ + query: "NOT (date: >= 2 AND date: <= 3) AND " + + "(date: = 1 OR date: = 4)", + select_list: ["date", "non-existent-key", "type", "title"], + sort_on: [["date", "descending"]] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: timestamps[9], + value: { + date: 4, + title: "second_doc", + type: "bar2" + } + }, + { + doc: {}, + id: timestamps[8], + value: { + date: 4, + title: "second_doc", + type: "bar2" + } + }, + { + doc: {}, + id: timestamps[6], + value: { + date: 4, + title: "doc", + type: "foo2" + } + }, + { + doc: {}, + id: timestamps[5], + value: { + date: 4, + title: "doc", + type: "foo2" + } + }, + + { + doc: {}, + id: timestamps[2], + value: { + date: 1, + title: "doc", + type: "foo" + } + }, + { + doc: {}, + id: timestamps[1], + value: { + date: 1, + title: "doc", + type: "foo" + } + } + ], + "Query gives correct results in correct order"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - // adding 2 attachments - return jio.putAttachment({ - "_id": "yes", - "_attachment": "blue", - "_mimetype": "text/plain", - "_rev": shared.rev1, - "_data": "sky" - }); + test( + "allDocs with include_revisions with an attachment on a removed document", + function () { + stop(); + expect(1); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + blob = new Blob(['a']), + timestamps; + + jio.put("document", {title: "foo"}) + .push(function () { + return jio.remove("document"); + }) + .push(function () { + return jio.putAttachment("document", "attachment", blob); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return revision.allDocs({select_list: ["title"]}); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: timestamps[2], + doc: {}, + value: {} + }, + { + id: timestamps[1], + doc: {}, + value: {} + }, + { + id: timestamps[0], + doc: {}, + value: {title: "foo"} + }], + "Attachment on removed document is handled correctly" + ); + return not_revision.allDocs({select_list: ["doc"]}); + }) + + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + } + ); + + test("allDocs with include_revisions with a removed attachment", + function () { + stop(); + expect(2); + var jio = this.jio, + revision = this.revision, + blob = new Blob(['a']), + timestamps, + not_revision = this.not_revision; + + jio.put("document", {title: "foo"}) + .push(function () { + return jio.putAttachment("document", "attachment", blob); + }) + .push(function () { + return jio.removeAttachment("document", "attachment"); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return revision.allDocs({select_list: ["title"]}); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: timestamps[2], + doc: {}, + value: {title: "foo"} + }, + { + id: timestamps[1], + doc: {}, + value: {title: "foo"} + }, + { + id: timestamps[0], + doc: {}, + value: {title: "foo"} + }], + "Attachment on removed document is handled correctly" + ); + }) + .push(function () { + return jio.allAttachments("document"); + }) + .push(function (results) { + deepEqual(results, {}, "No non-removed attachments"); + }) + + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function (answer) { + test("allDocs with include_revisions only one document", + function () { + stop(); + expect(1); + var jio = this.jio, + revision = this.revision, + timestamps, + not_revision = this.not_revision; + + jio.put("doc a", {title: "foo0"}) + .push(function () { + return jio.put("doc a", {title: "foo1"}); + }) + .push(function () { + return jio.put("doc b", {title: "bar0"}); + }) + .push(function () { + return jio.put("doc b", {title: "bar1"}); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return revision.allDocs({ + query: 'doc_id: "doc a"', + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: timestamps[1], + doc: {}, + value: {title: "foo1"} + }, + { + id: timestamps[0], + doc: {}, + value: {title: "foo0"} + }], + "Only specified document revision revision is returned" + ); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - shared.rev1 = answer.rev; + test("Parallel edits will not break anything", + function () { + stop(); + expect(2); + var jio = this.jio, + revision = this.revision, + blob1 = new Blob(['ab']), + blob2 = new Blob(['abc']), + blob3 = new Blob(['abcd']); + + jio.put("doc", {k: "v0"}) + .push(function () { + return RSVP.all([ + jio.put("doc", {k: "v"}), + jio.putAttachment("doc", "data", blob1), + jio.putAttachment("doc", "data2", blob2), + jio.putAttachment("doc", "data", blob3), + jio.removeAttachment("doc", "data"), + jio.removeAttachment("doc", "data2"), + jio.remove("doc"), + jio.remove("doc"), + jio.put("doc", {k: "v"}), + jio.put("doc", {k: "v"}), + jio.put("doc2", {k: "foo"}), + jio.remove("doc"), + jio.remove("doc") + ]); + }) + + .push(function () { + ok(true, "No errors thrown."); + return revision.allDocs(); + }) + .push(function (results) { + var res = results.data.rows; + equal(res.length, + 14, + "All edits are recorded regardless of ordering"); + return jio.allDocs(); + }) + + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - return jio.putAttachment({ - "_id": "no", - "_attachment": "Heeeee!", - "_mimetype": "text/plain", - "_rev": shared.rev2, - "_data": "Hooooo!" - }); + test("Adding second query storage on top of revision", + function () { + stop(); + expect(1); + var jio = this.jio; + return jio.put("doca", {title: "foo0", date: 0}) + .push(function () { + return jio.put("docb", {title: "bar0", date: 0}); + }) + .push(function () { + return jio.put("docb", {title: "bar1", date: 0}); + }) + .push(function () { + return jio.put("doca", {title: "foo1", date: 1}); + }) + .push(function () { + return jio.put("docb", {title: "bar2", date: 2}); + }) + .push(function () { + return jio.allDocs({ + query: "title: foo1 OR title: bar2", + select_list: ["title"], + sort_on: [["date", "ascending"]], + limit: [0, 1] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doca", + value: {title: "foo1"} + } + ]); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function (answer) { - shared.rev2 = answer.rev; - shared.rows = { - "total_rows": 3, - "rows": [{ - "id": "maybe", - "key": "maybe", - "value": { - "rev": shared.rev3 - } - }, { - "id": "no", - "key": "no", - "value": { - "rev": shared.rev2 - } - }, { - "id": "yes", - "key": "yes", - "value": { - "rev": shared.rev1 + module("revisionStorage.Full-Example", { + setup: function () { + // create storage of type "revision" with memory as substorage + var dbname = "db_" + Date.now(); + this.blob1 = new Blob(['a']); + this.blob2 = new Blob(['b']); + this.blob3 = new Blob(['ccc']); + this.other_blob = new Blob(['1']); + + this.jio = jIO.createJIO({ + type: "query", + sub_storage: { + type: "revision", + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } } - }] - }; - - return jio.allDocs(); - - }).then(function (answer) { - - answer.data.rows.sort(function (a, b) { - return a.id > b.id ? 1 : a.id < b.id ? -1 : 0; + } }); - deepEqual(answer.data, shared.rows, "allDocs"); - - shared.rows.rows[0].doc = { - "_id": "maybe", - "_rev": shared.rev3 - }; - shared.rows.rows[1].doc = { - "_id": "no", - "_rev": shared.rev2, - "_attachments": { - "Heeeee!": { - "content_type": "text/plain", - "digest": "sha256-bb333a2679b9537548d359d3f0f8e5cdee541bc8" + - "bb38bd5091e889453c15bd5d", - "length": 7 + this.revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "revision", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } } } - }; - shared.rows.rows[2].doc = { - "_id": "yes", - "_rev": shared.rev1, - "_attachments": { - "blue": { - "content_type": "text/plain", - "digest": "sha256-05f514fae7ca5710f9e9289a20a5c9b372af781b" + - "fc94dd23d9cb8a044122460f", - "length": 3 + }); + this.not_revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname } } - }; - - return jio.allDocs({"include_docs": true}); - - }).then(function (answer) { - - answer.data.rows.sort(function (a, b) { - return a.id > b.id ? 1 : a.id < b.id ? -1 : 0; }); - deepEqual(answer.data, shared.rows, "allDocs + include docs"); - - }).fail(unexpectedError).always(start); - + } }); - - test("Scenario", function () { - - var shared = {}, jio, jio2; - - shared.workspace1 = {}; - shared.workspace2 = {}; - shared.local_storage_description = { - "type": "local", - "username": "revision scenario", - "mode": "memory" - }; - shared.revision_storage_desciption = { - "type": "revision", - "sub_storage": shared.local_storage_description - }; - - jio = jIO.createJIO(shared.revision_storage_desciption, { - "workspace": shared.workspace1 + test("Retrieving revision with attachments", + function () { + stop(); + expect(1); + var jio = this.jio, + revision = this.revision, + timestamps, + not_revision = this.not_revision, + blobs1 = [ + new Blob(['a']), + new Blob(['ab']), + new Blob(['abc']), + new Blob(['abcd']), + new Blob(['abcde']) + ], + blobs2 = [ + new Blob(['abcdef']), + new Blob(['abcdefg']), + new Blob(['abcdefgh']), + new Blob(['abcdefghi']), + new Blob(['abcdefghij']) + ]; + putFullDoc(jio, "doc", {title: "bar"}, "data", blobs1[0]) + .push(function () { + return putFullDoc(jio, "doc", {title: "bar0"}, "data", blobs1[1]); + }) + .push(function () { + return putFullDoc(jio, "doc", {title: "bar1"}, "data", blobs1[2]); + }) + .push(function () { + return putFullDoc(jio, "doc2", {title: "foo0"}, "data", blobs2[0]); + }) + .push(function () { + return putFullDoc(jio, "doc2", {title: "foo1"}, "data", blobs2[0]); + }) + .push(function () { + return putFullDoc(jio, "doc", {title: "bar2"}, "data", blobs1[3]); + }) + .push(function () { + return putFullDoc(jio, "doc", {title: "bar3"}, "data", blobs1[4]); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + + .push(function () { + return revision.allDocs({ + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: timestamps[13], + value: {title: "bar3"} + }, + { + doc: {}, + id: timestamps[12], + value: {title: "bar3"} + }, + { + doc: {}, + id: timestamps[11], + value: {title: "bar2"} + }, + { + doc: {}, + id: timestamps[10], + value: {title: "bar2"} + }, + { + doc: {}, + id: timestamps[9], + value: {title: "foo1"} + }, + { + doc: {}, + id: timestamps[8], + value: {title: "foo1"} + }, + { + doc: {}, + id: timestamps[7], + value: {title: "foo0"} + }, + { + doc: {}, + id: timestamps[6], + value: {title: "foo0"} + }, + { + doc: {}, + id: timestamps[5], + value: {title: "bar1"} + }, + { + doc: {}, + id: timestamps[4], + value: {title: "bar1"} + }, + { + doc: {}, + id: timestamps[3], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[2], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[1], + value: {title: "bar"} + }, + { + doc: {}, + id: timestamps[0], + value: {title: "bar"} + } + ], + "allDocs with include_revisions should return all revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); }); - stop(); - - // new application - ok(jio, "I open my application with revision and localstorage"); - // put non empty document A-1 - shared.doc = {"_id": "sample1", "title": "mySample1"}; - shared.revisions = {"start": 0, "ids": []}; - shared.hex = generateRevisionHash(shared.doc, shared.revisions); - shared.rev = "1-" + shared.hex; + test("Retrieving revision with attachments with less straightforward ordering", + function () { + stop(); + expect(1); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps, + blobs1 = [ + new Blob(['a']), + new Blob(['ab']), + new Blob(['abc']), + new Blob(['abcd']), + new Blob(['abcde']) + ]; + jio.put("doc", {title: "bar"}) + .push(function () { + return jio.put("doc", {title: "bar0"}); + }) + .push(function () { + return jio.putAttachment("doc", "data", blobs1[0]); + }) + .push(function () { + return jio.put("doc2", {title: "foo0"}); + }) + .push(function () { + return jio.putAttachment("doc", "data", blobs1[1]); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + + .push(function () { + return revision.allDocs({ + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: timestamps[4], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[3], + value: {title: "foo0"} + }, + { + doc: {}, + id: timestamps[2], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[1], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[0], + value: {title: "bar"} + } + ], + "allDocs with include_revisions should return all revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - jio.put(shared.doc).then(function (answer) { - deepEqual( - answer, - { - "id": "sample1", - "method": "put", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" - }, - "Then, I create a new document (no attachment), " + - "my application keeps the revision in memory" - ); - - // open new tab (JIO) - jio2 = jIO.createJIO(shared.revision_storage_desciption, { - "workspace": shared.workspace2 - }); - - // Create a new JIO in a new tab - ok(jio2, "Now, I am opening a new tab, with the same application" + - " and the same storage tree"); + test("Retrieving revision with attachments with removals", + function () { + stop(); + expect(2); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps, + blobs1 = [ + new Blob(['a']), + new Blob(['ab']), + new Blob(['abc']), + new Blob(['abcd']), + new Blob(['abcde']) + ]; + jio.put("doc", {title: "bar"}) + .push(function () { + return jio.put("doc", {title: "bar0"}); + }) + .push(function () { + return jio.putAttachment("doc", "data", blobs1[0]); + }) + .push(function () { + return jio.put("doc2", {title: "foo0"}); + }) + .push(function () { + return jio.putAttachment("doc", "data", blobs1[1]); + }) + .push(function () { + return jio.allDocs({ + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc", + //timestamp: timestamps[4], + value: {title: "bar0"} + }, + { + doc: {}, + id: "doc2", + //timestamp: timestamps[3], + value: {title: "foo0"} + } + ], + "allDocs with include_revisions false should return all revisions"); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return revision.allDocs({ + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: timestamps[4], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[3], + value: {title: "foo0"} + }, + { + doc: {}, + id: timestamps[2], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[1], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[0], + value: {title: "bar"} + } + ], + "allDocs with include_revisions true should return all revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - // Get the document from the first storage - shared.doc._rev = shared.rev; - shared.doc._revisions = {"ids": [shared.hex], "start": 1}; - shared.doc._revs_info = [{"rev": shared.rev, "status": "available"}]; - return jio2.get({"_id": "sample1", "_rev": shared.rev}, { - "revs_info": true, - "revs": true, - "conflicts": true + module("revisionStorage.pack", { + setup: function () { + // create storage of type "revision" with memory as substorage + var dbname = "db_" + Date.now(); + this.jio = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "revision", + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + } }); - - }).then(function (answer) { - - deepEqual( - answer.data, - shared.doc, - "And, on this new tab, I load the document, " + - "and my application keeps the revision in memory" - ); - - // MODIFY the 2nd version - shared.doc_2 = {"_id": "sample1", "_rev": shared.rev, - "title": "mySample2_modified"}; - shared.revisions_2 = {"start": 1, "ids": [shared.hex]}; - shared.hex_2 = generateRevisionHash(shared.doc_2, shared.revisions_2); - shared.rev_2 = "2-" + shared.hex_2; - - return jio2.put(shared.doc_2); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "sample1", - "method": "put", - "result": "success", - "rev": shared.rev_2, - "status": 204, - "statusText": "No Content" - }, "So, I can modify and update it"); - - // MODIFY first version - shared.doc_1 = { - "_id": "sample1", - "_rev": shared.rev, - "title": "mySample1_modified" - }; - shared.revisions_1 = {"start": 1, "ids": [shared.rev.split('-')[1]]}; - shared.hex_1 = generateRevisionHash(shared.doc_1, shared.revisions_1); - shared.rev_1 = "2-" + shared.hex_1; - - return jio.put(shared.doc_1); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "sample1", - "method": "put", - "result": "success", - "rev": shared.rev_1, - "status": 204, - "statusText": "No Content" - }, "Back to the first tab, I update the document."); - - // Close 1st tab - jio = undefined; - // Close 2nd tab - jio2 = undefined; - ok(true, "I close tab both tabs"); - - // Reopen JIO - jio = jIO.createJIO(shared.revision_storage_desciption, { - "workspace": shared.workspace1 + this.revision = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "revision", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + } }); - ok(jio, "Later, I open my application again"); - - // GET document without revision = winner & conflict! - shared.mydocSample3 = { - "_id": "sample1", - "title": "mySample1_modified", - "_rev": shared.rev_1 - }; - shared.mydocSample3._conflicts = [shared.rev_2]; - shared.mydocSample3._revs_info = [{ - "rev": shared.rev_1, - "status": "available" - }, { - "rev": shared.rev, - "status": "available" - }]; - shared.mydocSample3._revisions = { - "ids": [shared.hex_1, shared.hex], - "start": 2 - }; - return jio.get({"_id": "sample1"}, { - "revs_info": true, - "revs": true, - "conflicts": true + this.not_revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } }); + this.blob = new Blob(['a']); + } + }); - }).then(function (answer) { - - deepEqual( - answer.data, - shared.mydocSample3, - "I load the same document as before, " + - "and a popup shows that there is a conflict" - ); - - // REMOVE one of the two conflicting versions - shared.revisions = {"start": 2, "ids": [ - shared.rev_1.split('-')[1], - shared.rev.split('-')[1] - ]}; - shared.doc_myremove3 = {"_id": "sample1", "_rev": shared.rev_1}; - shared.rev_3 = "3-" + generateRevisionHash( - shared.doc_myremove3, - shared.revisions, - true - ); - - return jio.remove({"_id": "sample1", "_rev": shared.rev_1}); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "sample1", - "method": "remove", - "result": "success", - "rev": shared.rev_3, - "status": 204, - "statusText": "No Content" - }, "I choose one of the document and close the application."); - - // check to see if conflict still exists - shared.mydocSample4 = { - "_id": "sample1", - "title": "mySample2_modified", - "_rev": shared.rev_2 - }; - shared.mydocSample4._revs_info = [{ - "rev": shared.rev_2, - "status": "available" - }, { - "rev": shared.rev, - "status": "available" - }]; - shared.mydocSample4._revisions = { - "ids": [shared.hex_2, shared.hex], - "start": 2 - }; - - return jio.get({"_id": "sample1"}, { - "revs_info": true, - "revs": true, - "conflicts": true - }); + test("Verifying pack works with keep_latest_num", + function () { + stop(); + expect(2); + var jio = this.jio, + not_revision = this.not_revision; + return jio.put("doc_a", {title: "rev"}) + .push(function () { + return jio.put("doc_a", {title: "rev0"}); + }) + .push(function () { + return jio.put("doc_a", {title: "rev1"}); + }) + .push(function () { + return jio.put("doc_b", {title: "data"}); + }) + .push(function () { + return jio.put("doc_b", {title: "data0"}); + }) + .push(function () { + return jio.put("doc_a", {title: "rev2"}); + }) + .push(function () { + return jio.put("doc_b", {title: "data1"}); + }) + .push(function () { + return jio.put("doc_b", {title: "data2"}); + }) + .push(function () { + return jio.__storage._sub_storage.__storage._sub_storage + .__storage.packOldRevisions({ + keep_latest_num: 2 + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "timestamp", "op"] + }); + }) + .push(function (results) { + equal(results.data.total_rows, 4, "Correct amount of results"); + deepEqual(results.data.rows, [ + { + doc: {}, + id: results.data.rows[0].id, + value: { + doc: {title: "data2"}, + doc_id: "doc_b", + timestamp: results.data.rows[0].id, + op: "put" + } + }, + { + doc: {}, + id: results.data.rows[1].id, + value: { + doc: {title: "data1"}, + doc_id: "doc_b", + timestamp: results.data.rows[1].id, + op: "put" + } + }, + { + doc: {}, + id: results.data.rows[2].id, + value: { + doc: {title: "rev2"}, + doc_id: "doc_a", + timestamp: results.data.rows[2].id, + op: "put" + } + }, + { + doc: {}, + id: results.data.rows[3].id, + value: { + doc: {title: "rev1"}, + doc_id: "doc_a", + timestamp: results.data.rows[3].id, + op: "put" + } + } + ], + "Keep the correct documents after pack"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function (answer) { + test("Verifying pack works with fixed timestamp", + function () { + stop(); + expect(2); + var jio = this.jio, + not_revision = this.not_revision, + timestamp; + return jio.allDocs() + .push(function () { + return RSVP.all([ + jio.put("doc_a", {title: "old_rev0"}), + jio.put("doc_a", {title: "old_rev1"}), + jio.put("doc_a", {title: "old_rev2"}), + jio.put("doc_b", {title: "old_data0"}), + jio.put("doc_b", {title: "old_data1"}), + jio.put("doc_b", {title: "old_data2"}), + jio.put("doc_c", {title: "latest_bar"}) + ]); + }) + .push(function () { + return not_revision.allDocs({sort_on: [["timestamp", "descending"]]}); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + return jio.put("doc_a", {title: "latest_rev"}); + }) + .push(function () { + return jio.put("doc_b", {title: "latest_data"}); + }) + .push(function () { + return jio.__storage._sub_storage.__storage._sub_storage + .__storage.packOldRevisions({ + keep_active_revs: timestamp + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "timestamp"] + }); + }) + .push(function (results) { + equal(results.data.total_rows, 3, "Correct amount of results"); + deepEqual(results.data.rows, [ + { + doc: {}, + id: results.data.rows[0].id, + value: { + doc: {title: "latest_data"}, + doc_id: "doc_b", + timestamp: results.data.rows[0].id + } + }, + { + doc: {}, + id: results.data.rows[1].id, + value: { + doc: {title: "latest_rev"}, + doc_id: "doc_a", + timestamp: results.data.rows[1].id + } + }, + { + doc: {}, + id: results.data.rows[2].id, + value: { + doc: {title: "latest_bar"}, + doc_id: "doc_c", + timestamp: results.data.rows[2].id + } + } + ], + "Keep the correct documents after pack"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - deepEqual( - answer.data, - shared.mydocSample4, - "Test if conflict stiil exists" - ); + test("Verifying pack works with fixed timestamp and more complex operations", + function () { + stop(); + expect(2); + var jio = this.jio, + not_revision = this.not_revision, + timestamp; + return jio.allDocs() + .push(function () { + return RSVP.all([ + jio.put("doc_a", {title: "old_rev0"}), + jio.put("doc_a", {title: "old_rev1"}), + jio.put("doc_a", {title: "old_rev2"}), + jio.put("doc_b", {title: "latest_data"}) + ]); + }) + .push(function () { + return jio.allDocs({sort_on: [["timestamp", "descending"]]}); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + return jio.remove("doc_a"); + }) + .push(function () { + return jio.__storage._sub_storage.__storage._sub_storage + .__storage.packOldRevisions({ + keep_active_revs: timestamp + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "timestamp", "op"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: results.data.rows[0].id, + value: { + op: "remove", + doc_id: "doc_a", + timestamp: results.data.rows[0].id + } + }, + { + doc: {}, + id: results.data.rows[1].id, + value: { + doc: {title: "latest_data"}, + doc_id: "doc_b", + op: "put", + timestamp: results.data.rows[1].id + } + } + ], + "Keep the correct documents after pack"); + }) + .push(function () { + return jio.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc_b", + value: {title: "latest_data"} + } + ], + "Memory not corrupted by pack without include_revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).fail(unexpectedError).always(start); + test("Verifying pack works with fixed timestamp and more complex operations", + function () { + stop(); + expect(2); + var jio = this.jio, + not_revision = this.not_revision, + timestamp; + return jio.allDocs() + .push(function () { + return RSVP.all([ + jio.put("doc_a", {title: "old_rev0"}), + jio.put("doc_a", {title: "old_rev1"}), + jio.put("doc_a", {title: "old_rev2"}), + jio.put("doc_b", {title: "latest_data"}) + ]); + }) + .push(function () { + return jio.allDocs({sort_on: [["timestamp", "descending"]]}); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + return jio.remove("doc_a"); + }) + .push(function () { + return jio.__storage._sub_storage.__storage._sub_storage + .__storage.packOldRevisions({ + keep_active_revs: timestamp + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "timestamp", "op"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: results.data.rows[0].id, + value: { + op: "remove", + doc_id: "doc_a", + timestamp: results.data.rows[0].id + } + }, + { + doc: {}, + id: results.data.rows[1].id, + value: { + doc: {title: "latest_data"}, + doc_id: "doc_b", + op: "put", + timestamp: results.data.rows[1].id + } + } + ], + "Keep the correct documents after pack"); + }) + .push(function () { + return jio.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc_b", + value: {title: "latest_data"} + } + ], + "Memory not corrupted by pack without include_revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }); + test("Verifying pack works with fixed timestamp and more complex operations", + function () { + stop(); + expect(2); + var jio = this.jio, + not_revision = this.not_revision, + timestamp, + blob = this.blob; + return jio.allDocs() + .push(function () { + return RSVP.all([ + jio.put("doc_a", {title: "old_rev0"}), + jio.putAttachment("doc_a", "attach_aa", blob), + jio.put("doc_b", {title: "latest_data"}) + ]); + }) + .push(function () { + return jio.allDocs({sort_on: [["timestamp", "descending"]]}); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + return jio.remove("doc_a"); + }) + .push(function () { + return jio.__storage._sub_storage.__storage._sub_storage + .__storage.packOldRevisions({ + keep_active_revs: timestamp + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "timestamp", "op"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: results.data.rows[0].id, + value: { + op: "remove", + doc_id: "doc_a", + timestamp: results.data.rows[0].id + } + }, + { + doc: {}, + id: results.data.rows[1].id, + value: { + doc: {title: "latest_data"}, + doc_id: "doc_b", + op: "put", + timestamp: results.data.rows[1].id + } + } + ], + "Keep the correct documents after pack"); + }) + .push(function () { + return jio.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc_b", + value: {title: "latest_data"} + } + ], + "Memory not corrupted by pack without include_revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); -})); +}(jIO, RSVP, Blob, QUnit)); \ No newline at end of file -- 2.30.9 From a3043d5ba3e8b14b8175928dab21701c781d8436 Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Fri, 3 Aug 2018 09:08:34 +0000 Subject: [PATCH 45/46] Removed historystorage from gruntfile. --- Gruntfile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 4a2f398..52a2816 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -183,7 +183,6 @@ module.exports = function (grunt) { 'src/jio.storage/cryptstorage.js', 'src/jio.storage/websqlstorage.js', 'src/jio.storage/fbstorage.js', - 'src/jio.storage/historystorage.js', 'src/jio.storage/revisionstorage.js' ], dest: 'dist/<%= pkg.name %>-<%= pkg.version %>.js' -- 2.30.9 From b51c686077785b29eb7735f528ea61333fbb8c7b Mon Sep 17 00:00:00 2001 From: "bryan.kaperick" Date: Fri, 3 Aug 2018 09:09:57 +0000 Subject: [PATCH 46/46] Updated tests.html file --- test/tests.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/tests.html b/test/tests.html index c0390d3..65d388f 100644 --- a/test/tests.html +++ b/test/tests.html @@ -60,10 +60,7 @@ - - +