🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

bikini

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

bikini - npm Package Compare versions

Comparing version
0.0.3
to
0.5.0
.bowerrc

Sorry, the diff of this file is not supported yet

+35
{
"node": true,
"browser": true,
"esnext": true,
"bitwise": false,
"camelcase": true,
"curly": true,
"eqeqeq": true,
"immed": true,
"indent": 4,
"latedef": true,
"newcap": true,
"noarg": true,
"quotmark": "single",
"regexp": true,
"undef": true,
"unused": false,
"strict": false,
"trailing": true,
"smarttabs": true,
"jquery": true,
"globals": {
"Bikini": true,
"Backbone": false,
"_": false,
"moment": false,
"io": false,
"YES": true,
"NO": true,
"Modernizr": false,
"Hammer": false,
"FastClick": false,
"enquire": false
}
}
language: node_js
node_js:
- '0.10'
- '0.8'
before_script:
- npm install -g grunt-cli
- gem install compass
- sh init-repo.sh
{
"name": "bikini",
"version": "0.5.0",
"main": [
"./dist/*.js"
],
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"src",
"test",
"scripts",
"resources",
"sample",
"server",
"utils",
"tasks",
"doc-template",
"Gruntfile.js",
"*.sh",
"*.md",
"*.json",
"*.txt",
"*.bat"
],
"dependencies": {
"backbone": "~1.1.0",
"underscore": "~1.5.2",
"jquery": "~2.0.3",
"socket.io-client": "~0.9.16",
"momentjs": "~2.4.0"
},
"devDependencies": {
"mocha": "~1.13.0",
"chai": "~1.8.1"
}
}

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

/*!
* Project: The M-Project - Mobile HTML5 Application Framework
* Copyright: (c) 2014 M-Way Solutions GmbH.
* Version: 0.5.0
* Date: Tue Mar 18 2014 17:16:14
* License: http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
*/
!function(a,b,c){var d=null;d="undefined"!=typeof exports?exports:a.Bikini={},d.Version=d.version="0.5.0",d.f=function(){},d.create=function(a){return new this(a)},d.design=function(a){var b=this.extend(a||{});return new b},d.extend=b.Model.extend,d.isCollection=function(a){return b.Collection.prototype.isPrototypeOf(a)},d.isModel=function(a){return b.Model.prototype.isPrototypeOf(a)},d.isEntity=function(a){return d.Entity.prototype.isPrototypeOf(a)},d.DATA={TYPE:{INTEGER:"integer",STRING:"string",TEXT:"text",DATE:"date",BOOLEAN:"boolean",FLOAT:"float",OBJECT:"object",ARRAY:"array",BINARY:"binary",OBJECTID:"objectid",NULL:"null"}},d.Object={_type:"Bikini.Object",_create:function(a){var b=function(){};return b.prototype=a,new b},include:function(a){for(var b in a){if(this.hasOwnProperty(b))throw d.Exception.RESERVED_WORD.getException();this[b]=a[b]}return this},design:function(a){var b=this._create(this);return b.include(this._normalize(a)),b},bindToCaller:function(a,b,c){return function(){if("function"!=typeof b||"object"!=typeof a)throw d.Exception.INVALID_INPUT_PARAMETER.getException();return Array.isArray(c)?b.apply(a,c):b.call(a,c)}},_normalize:function(a){return a=a&&"object"==typeof a?a:{}},handleCallback:function(a){var b=Array.prototype.slice.call(arguments,1);if(a){var c="object"==typeof a.target?a.target:this,d=a;if("function"==typeof a.action?d=a.action:"string"==typeof a.action&&(d=c[a.action]),"function"==typeof d)return this.bindToCaller(c,d,b)()}}},YES=!0,NO=!1,d.ObjectID=function(a){d.ObjectID.counter=d.ObjectID.counter||parseInt(Math.random()*Math.pow(16,6)),d.ObjectID.machineId=d.ObjectID.machineId||parseInt(Math.random()*Math.pow(16,6)),d.ObjectID.processId=d.ObjectID.processId||parseInt(Math.random()*Math.pow(16,4)),this._ObjectID(a)},d.ObjectID._looksLikeObjectID=function(a){return 24===a.length&&a.match(/^[0-9a-f]*$/)},c.extend(d.ObjectID.prototype,{_str:"",_ObjectID:function(a){if(a){if(a=a.toLowerCase(),!d.ObjectID._looksLikeObjectID(a))throw new Error("Invalid hexadecimal string for creating an ObjectID");this._str=a}else this._str=this._hexString(8,(new Date).getTime()/1e3)+this._hexString(6,d.ObjectID.machineId)+this._hexString(4,d.ObjectID.processId)+this._hexString(6,d.ObjectID.counter++);return this._str},_hexString:function(a,b){b=b||parseInt(Math.random()*Math.pow(16,a));for(var c=b.toString(16);c.length<a;)c="0"+c;return c.substr(0,a)},toString:function(){return"ObjectID('"+this._str+"')"},equals:function(a){return a instanceof this._ObjectID&&this.valueOf()===a.valueOf()},clone:function(){return new d.ObjectID(this._str)},typeName:function(){return"oid"},getTimestamp:function(){return 1e3*parseInt(this._str.substr(0,8),16)},getMachineId:function(){return parseInt(this._str.substr(8,6),16)},getProcessId:function(){return parseInt(this._str.substr(14,4),16)},getCounter:function(){return parseInt(this._str.substr(18,6),16)},valueOf:function(){return this._str},toJSON:function(){return this._str},toHexString:function(){return this._str},_selectorIsId:function(a){return"string"==typeof a||"number"==typeof a||a instanceof d.ObjectId},_selectorIsIdPerhapsAsObject:function(a){return this._selectorIsId(a)||a&&"object"==typeof a&&a._id&&this._selectorIsId(a._id)&&1===c.size(a)},_idsMatchedBySelector:function(a){if(this._selectorIsId(a))return[a];if(!a)return null;if(c.has(a,"_id"))return this._selectorIsId(a._id)?[a._id]:a._id&&a._id.$in&&c.isArray(a._id.$in)&&!c.isEmpty(a._id.$in)&&c.all(a._id.$in,this._selectorIsId)?a._id.$in:null;if(a.$and&&c.isArray(a.$and))for(var b=0;b<a.$and.length;++b){var d=this._idsMatchedBySelector(a.$and[b]);if(d)return d}return null}}),d.UniqueId=d.Object.design({uuid:function(a,b){var c="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split(""),d=[];b=b||c.length;var e;if(a)for(e=0;a>e;e++)d[e]=c[0|Math.random()*b];else{var f;for(d[8]=d[13]=d[18]=d[23]="-",d[14]="4",e=0;36>e;e++)d[e]||(f=0|16*Math.random(),d[e]=c[19===e?3&f|8:f])}return d.join("")}}),d.Base64=d.Object.design({type:"Bikini.Base64",_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encodeBinary:function(a){for(var b,c="",d=new Array(4),e=0,f=0;e<a.length;){b=new Array(3);for(var g=0;g<b.length;g++)b[g]=e<a.length?255&a.charCodeAt(e++):0;switch(d[0]=b[0]>>2,d[1]=(3&b[0])<<4|b[1]>>4,d[2]=(15&b[1])<<2|b[2]>>6,d[3]=63&b[2],f=e-(a.length-1)){case 2:d[3]=64,d[2]=64;break;case 1:d[3]=64}for(g=0;g<d.length;g++)c+=this._keyStr.charAt(d[g])}return c},encode:function(a){var b,c,e,f,g,h,i,j="",k=0;for(a=d.Cypher.utf8Encode(a);k<a.length;)b=a.charCodeAt(k++),c=a.charCodeAt(k++),e=a.charCodeAt(k++),f=b>>2,g=(3&b)<<4|c>>4,h=(15&c)<<2|e>>6,i=63&e,isNaN(c)?h=i=64:isNaN(e)&&(i=64),j+=this._keyStr.charAt(f)+this._keyStr.charAt(g)+this._keyStr.charAt(h)+this._keyStr.charAt(i);return j},binaryEncode:function(a){for(var b,c,d,e,f,g,h,i="",j=0;j<a.length;)b=a.charCodeAt(j++),c=a.charCodeAt(j++),d=a.charCodeAt(j++),e=b>>2,f=(3&b)<<4|c>>4,g=(15&c)<<2|d>>6,h=63&d,isNaN(c)?g=h=64:isNaN(d)&&(h=64),i+=this._keyStr.charAt(e)+this._keyStr.charAt(f)+this._keyStr.charAt(g)+this._keyStr.charAt(h);return i},decode:function(a){var b,c,e,f,g,h,i,j="",k=0;for(a=a.replace(/[^A-Za-z0-9\+\/\=]/g,"");k<a.length;)f=this._keyStr.indexOf(a.charAt(k++)),g=this._keyStr.indexOf(a.charAt(k++)),h=this._keyStr.indexOf(a.charAt(k++)),i=this._keyStr.indexOf(a.charAt(k++)),b=f<<2|g>>4,c=(15&g)<<4|h>>2,e=(3&h)<<6|i,j+=String.fromCharCode(b),64!==h&&(j+=String.fromCharCode(c)),64!==i&&(j+=String.fromCharCode(e));return d.Cypher.utf8Decode(j)}}),d.SHA256=d.Object.design({type:"Bikini.SHA256",chrsz:8,hexcase:0,hash:function(a){return a=d.Cypher.utf8Encode(a),this.binb2hex(this.coreSha256(this.str2binb(a),a.length*this.chrsz))},safeAdd:function(a,b){var c=(65535&a)+(65535&b),d=(a>>16)+(b>>16)+(c>>16);return d<<16|65535&c},S:function(a,b){return a>>>b|a<<32-b},R:function(a,b){return a>>>b},Ch:function(a,b,c){return a&b^~a&c},Maj:function(a,b,c){return a&b^a&c^b&c},Sigma0256:function(a){return this.S(a,2)^this.S(a,13)^this.S(a,22)},Sigma1256:function(a){return this.S(a,6)^this.S(a,11)^this.S(a,25)},Gamma0256:function(a){return this.S(a,7)^this.S(a,18)^this.R(a,3)},Gamma1256:function(a){return this.S(a,17)^this.S(a,19)^this.R(a,10)},coreSha256:function(a,b){var c,d,e,f,g,h,i,j,k,l,m,n,o=new Array(1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298),p=new Array(1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225),q=new Array(64);for(a[b>>5]|=128<<24-b%32,a[(b+64>>9<<4)+15]=b,k=0;k<a.length;k+=16){for(c=p[0],d=p[1],e=p[2],f=p[3],g=p[4],h=p[5],i=p[6],j=p[7],l=0;64>l;l++)q[l]=16>l?a[l+k]:this.safeAdd(this.safeAdd(this.safeAdd(this.Gamma1256(q[l-2]),q[l-7]),this.Gamma0256(q[l-15])),q[l-16]),m=this.safeAdd(this.safeAdd(this.safeAdd(this.safeAdd(j,this.Sigma1256(g)),this.Ch(g,h,i)),o[l]),q[l]),n=this.safeAdd(this.Sigma0256(c),this.Maj(c,d,e)),j=i,i=h,h=g,g=this.safeAdd(f,m),f=e,e=d,d=c,c=this.safeAdd(m,n);p[0]=this.safeAdd(c,p[0]),p[1]=this.safeAdd(d,p[1]),p[2]=this.safeAdd(e,p[2]),p[3]=this.safeAdd(f,p[3]),p[4]=this.safeAdd(g,p[4]),p[5]=this.safeAdd(h,p[5]),p[6]=this.safeAdd(i,p[6]),p[7]=this.safeAdd(j,p[7])}return p},str2binb:function(a){for(var b=[],c=(1<<this.chrsz)-1,d=0;d<a.length*this.chrsz;d+=this.chrsz)b[d>>5]|=(a.charCodeAt(d/this.chrsz)&c)<<24-d%32;return b},binb2hex:function(a){for(var b=this.hexcase?"0123456789ABCDEF":"0123456789abcdef",c="",d=0;d<4*a.length;d++)c+=b.charAt(a[d>>2]>>8*(3-d%4)+4&15)+b.charAt(a[d>>2]>>8*(3-d%4)&15);return c}}),d.Cypher=d.Object.design({type:"Bikini.Cypher",defaultDecoder:d.Base64,defaultEncoder:d.Base64,defaultHasher:d.SHA256,decode:function(a,b){return b&&b.decode?b.decode(a):this.defaultDecoder.decode(a)},encode:function(a,b){return b&&b.encode?b.encode(a):this.defaultEncoder.encode(a)},hash:function(a,b){return b&&b.hash?b.hash(a):this.defaultHasher.hash(a)},utf8Encode:function(a){a=a.replace(/\r\n/g,"\n");for(var b="",c=0;c<a.length;c++){var d=a.charCodeAt(c);128>d?b+=String.fromCharCode(d):d>127&&2048>d?(b+=String.fromCharCode(d>>6|192),b+=String.fromCharCode(63&d|128)):(b+=String.fromCharCode(d>>12|224),b+=String.fromCharCode(d>>6&63|128),b+=String.fromCharCode(63&d|128))}return b},utf8Decode:function(a){var b,c,d,e,f,g="";for(b=c=d=e=0;b<a.length;)c=a.charCodeAt(b),128>c?(g+=String.fromCharCode(c),b++):c>191&&224>c?(e=a.charCodeAt(b+1),g+=String.fromCharCode((31&c)<<6|63&e),b+=2):(e=a.charCodeAt(b+1),f=a.charCodeAt(b+2),g+=String.fromCharCode((15&c)<<12|(63&e)<<6|63&f),b+=3);return g}}),d.Date={create:function(){var a=moment.apply(this,arguments);return c.extend(a,this)}},d.Field=function(a){this.merge(a),this.initialize.apply(this,arguments)},d.Field.extend=d.extend,d.Field.create=d.create,d.Field.design=d.design,c.extend(d.Field.prototype,d.Object,{_type:"Bikini.Field",name:null,type:null,index:null,defaultValue:void 0,length:null,required:NO,persistent:YES,initialize:function(){},merge:function(a){a=c.isString(a)?{type:a}:a||{},this.name=c.isUndefined(a.name)?this.name:a.name,this.type=c.isUndefined(a.type)?this.type:a.type,this.index=c.isUndefined(a.index)?this.index:a.index,this.defaultValue=c.isUndefined(a.defaultValue)?this.defaultValue:a.defaultValue,this.length=c.isUndefined(a.length)?this.length:a.length,this.required=c.isUndefined(a.required)?this.required:a.required,this.persistent=c.isUndefined(a.persistent)?this.persistent:a.persistent},transform:function(a,b){b=b||this.type;try{if(c.isUndefined(a))return this.defaultValue;if(b===d.DATA.TYPE.STRING||b===d.DATA.TYPE.TEXT)return c.isObject(a)?JSON.stringify(a):c.isNull(a)?"null":a.toString();if(b===d.DATA.TYPE.INTEGER)return parseInt(a);if(b===d.DATA.TYPE.BOOLEAN)return a===!0||"true"===a;if(b===d.DATA.TYPE.FLOAT)return parseFloat(a);if(b===d.DATA.TYPE.OBJECT||b===d.DATA.TYPE.ARRAY){if(!c.isObject(a))return c.isString(a)?JSON.parse(a):null}else if(b===d.DATA.TYPE.DATE){if(!d.Date.isPrototypeOf(a)){var e=a?d.Date.create(a):null;return e&&e.isValid()?e:null}}else if(b===d.DATA.TYPE.OBJECTID&&!d.ObjectID.prototype.isPrototypeOf(a))return c.isString(a)?new d.ObjectID(a):null;return a}catch(f){console.error("Failed converting value! "+f.message)}},equals:function(a,b){var d=this.transform(a),e=this.transform(b);return this._equals(d,e,c.isArray(d))},isBinary:function(a){return"undefined"!=typeof Uint8Array&&a instanceof Uint8Array||a&&a.$Uint8ArrayPolyfill},detectType:function(a){return c.isNumber(a)?d.DATA.TYPE.FLOAT:c.isString(a)?d.DATA.TYPE.STRING:c.isBoolean(a)?d.DATA.TYPE.BOOLEAN:c.isArray(a)?d.DATA.TYPE.ARRAY:c.isNull(a)?d.DATA.TYPE.NULL:c.isDate(a)||d.Date.isPrototypeOf(a)?d.DATA.TYPE.DATE:d.ObjectID.prototype.isPrototypeOf(a)?d.DATA.TYPE.OBJECTID:this.isBinary(a)?d.DATA.TYPE.BINARY:d.DATA.TYPE.OBJECT},typeOrder:function(a){switch(a){case d.DATA.TYPE.NULL:return 0;case d.DATA.TYPE.FLOAT:return 1;case d.DATA.TYPE.STRING:return 2;case d.DATA.TYPE.OBJECT:return 3;case d.DATA.TYPE.ARRAY:return 4;case d.DATA.TYPE.BINARY:return 5;case d.DATA.TYPE.DATE:return 6}return-1},_equals:function(a,b,d){var e,f=this;if(a===b)return!0;if(!a||!b)return!1;if(!c.isObject(a)||!c.isObject(b))return!1;if(a instanceof Date&&b instanceof Date)return a.valueOf()===b.valueOf();if(this.isBinary(a)&&this.isBinary(b)){if(a.length!==b.length)return!1;for(e=0;e<a.length;e++)if(a[e]!==b[e])return!1;return!0}if(c.isFunction(a.equals))return a.equals(b);if(c.isArray(a)){if(!c.isArray(b))return!1;if(a.length!==b.length)return!1;for(e=0;e<a.length;e++)if(!f.equals(a[e],b[e],d))return!1;return!0}var g;if(d){var h=[];return c.each(b,function(a,b){h.push(b)}),e=0,g=c.all(a,function(a,c){return e>=h.length?!1:c!==h[e]?!1:f.equals(a,b[h[e]],d)?(e++,!0):!1}),g&&e===h.length}return e=0,g=c.all(a,function(a,g){return c.has(b,g)&&f.equals(a,b[g],d)?(e++,!0):!1}),g&&c.size(b)===e},_cmp:function(a,b){if(void 0===a)return void 0===b?0:-1;if(void 0===b)return 1;var c=0,e=this.detectType(a),f=this.detectType(b),g=this.typeOrder(e),h=this.typeOrder(f);if(g!==h)return h>g?-1:1;if(e!==f)throw new Error("Missing type coercion logic in _cmp");if(7===e&&(e=f=2,a=a.toHexString(),b=b.toHexString()),e===d.DATA.TYPE.DATE&&(e=f=1,a=a.getTime(),b=b.getTime()),e===d.DATA.TYPE.FLOAT)return a-b;if(f===d.DATA.TYPE.STRING)return b>a?-1:a===b?0:1;if(e===d.DATA.TYPE.OBJECT){var i=function(a){var b=[];for(var c in a)b.push(c),b.push(a[c]);return b};return this._cmp(i(a),i(b))}if(e===d.DATA.TYPE.ARRAY)for(c=0;;c++){if(c===a.length)return c===b.length?0:-1;if(c===b.length)return 1;var j=this._cmp(a[c],b[c]);if(0!==j)return j}if(e===d.DATA.TYPE.BINARY){if(a.length!==b.length)return a.length-b.length;for(c=0;c<a.length;c++){if(a[c]<b[c])return-1;if(a[c]>b[c])return 1}return 0}if(e===d.DATA.TYPE.BOOLEAN)return a?b?0:1:b?-1:0;if(e===d.DATA.TYPE.NULL)return 0;throw new Error("Unknown type to sort")}}),d.Entity=function(a){var b=this.fields;this.fields={},this._mergeFields(b),a=a||{},a.fields&&this._mergeFields(a.fields),this.typeMapping=a.typeMapping||this.typeMapping;var c=a.collection,d=a.model||(c?c.prototype.model:null);this.idAttribute=a.idAttribute||this.idAttribute||(d?d.prototype.idAttribute:""),this._updateFields(this.typeMapping),this.initialize.apply(this,arguments)},d.Entity.from=function(a,b){if(d.Entity.prototype.isPrototypeOf(a))b&&b.typeMapping&&a._updateFields(b.typeMapping);else if(c.isFunction(a)&&d.Entity.prototype.isPrototypeOf(a.prototype)){var e=a;a=new e(b)}else{"string"==typeof a&&(a={name:a});var f=d.Entity.extend(a);a=new f(b)}return a},d.Entity.extend=d.extend,d.Entity.create=d.create,d.Entity.design=d.design,c.extend(d.Entity.prototype,d.Object,{_type:"Bikini.Entity",name:"",idAttribute:"",fields:{},initialize:function(){},getFields:function(){return this.fields},getField:function(a){return this.fields[a]},getFieldName:function(a){var b=this.getField(a);return b&&b.name?b.name:a},getKey:function(){return this.idAttribute||d.Model.idAttribute},getKeys:function(){return this.splitKey(this.getKey())},splitKey:function(a){var b=[];return c.isString(a)&&c.each(a.split(","),function(a){var c=a.trim();c&&b.push(c)}),b},_mergeFields:function(a){c.isObject(this.fields)||(this.fields={});var b=this;c.isObject(a)&&c.each(a,function(a,c){b.fields[c]?b.fields[c].merge(a):b.fields[c]=new d.Field(a)})},_updateFields:function(a){var b=this;c.each(this.fields,function(c,d){c.persistent===NO?delete b.fields[d]:(c.name||(c.name=d),a&&a[c.type]&&(c.type=a[c.type]))})},toAttributes:function(a,b,d){if(d=d||this.fields,a&&!c.isEmpty(d)){var e,f={};return c.each(d,function(b,d){e=c.isFunction(a.get)?a.get(b.name):a[b.name],f[d]=e}),f}return a},fromAttributes:function(a,b){if(b=b||this.fields,a&&!c.isEmpty(b)){var d={};return c.each(b,function(b,e){var f=c.isFunction(a.get)?a.get(e):a[e];f=b.transform(f),c.isUndefined(f)||(d[b.name]=f)}),d}return a},setId:function(a,b){if(a&&b){var d=this.getKey()||a.idAttribute;d&&(c.isFunction(a.set)?a.set(d,b):a[d]=b)}return a},getId:function(a){if(a){var b=this.getKey()||a.idAttribute;if(b)return c.isFunction(a.get)?a.get(b):a[b]}}}),d.Security=d.Object.design({logon:function(a,b){var c=a?a.credentials:null;if(c)switch(c.type){case"basic":return this.logonBasicAuth(a,b)}this.handleCallback(b)},logonBasicAuth:function(a,b){var c=a.credentials;a.beforeSend=function(a){d.Security.setBasicAuth(a,c)},this.handleCallback(b)},setBasicAuth:function(a,b){if(b&&b.username&&a&&d.Base64){var c=d.Base64.encode(encodeURIComponent(b.username+":"+(b.password||"")));a.setRequestHeader("Authorization","Basic "+c)}}}),d.Model=b.Model.extend({constructor:function(a,c){this.init(a,c),b.Model.apply(this,arguments)}}),d.Model.create=d.create,d.Model.design=d.design,c.extend(d.Model.prototype,d.Object,{_type:"Bikini.Model",isModel:YES,entity:null,defaults:{},changedSinceSync:{},logon:d.Security.logon,init:function(a,b){b=b||{},this.collection=b.collection||this.collection,this.idAttribute=b.idAttribute||this.idAttribute,this.store=this.store||(this.collection?this.collection.store:null)||b.store,this.store&&c.isFunction(this.store.initModel)&&this.store.initModel(this,b),this.entity=this.entity||(this.collection?this.collection.entity:null)||b.entity,this.entity&&(this.entity=d.Entity.from(this.entity,{model:this.constructor,typeMapping:b.typeMapping}),this.idAttribute=this.entity.idAttribute||this.idAttribute),this.credentials=this.credentials||(this.collection?this.collection.credentials:null)||b.credentials,this.on("change",this.onChange,this),this.on("sync",this.onSync,this)},sync:function(a,d,e){e=e||{},e.credentials=e.credentials||this.credentials;var f=(e.store?e.store:null)||this.store,g=this,h=arguments;this.logon(e,function(){return f&&c.isFunction(f.sync)?f.sync.apply(g,h):b.sync.apply(g,h)})},onChange:function(a){var b=a.changedAttributes();if(c.isObject(b))for(var d in b)this.changedSinceSync[d]=b[d]},onSync:function(){this.changedSinceSync={}},getUrlRoot:function(){if(this.urlRoot)return c.isFunction(this.urlRoot)?this.urlRoot():this.urlRoot;if(this.collection)return this.collection.getUrlRoot();if(this.url){var a=c.isFunction(this.url)?this.url():this.url;return a&&this.id&&a.indexOf(this.id)>0?a.substr(0,a.indexOf(this.id)):a}},toJSON:function(a){a=a||{};var b=a.entity||this.entity;return d.isEntity(b)?b.fromAttributes(a.attrs||this.attributes):a.attrs||c.clone(this.attributes)},parse:function(a,b){b=b||{};var c=b.entity||this.entity;return d.isEntity(c)?c.toAttributes(a):a}}),d.Collection=b.Collection.extend({constructor:function(a){this.init(a),b.Collection.apply(this,arguments)}}),d.Collection.create=d.create,d.Collection.design=d.design,c.extend(d.Collection.prototype,d.Object,{_type:"Bikini.Collection",isCollection:YES,model:d.Model,entity:null,options:null,logon:d.Security.logon,init:function(a){a=a||{},this.store=a.store||this.store||(this.model?this.model.prototype.store:null),this.entity=a.entity||this.entity||(this.model?this.model.prototype.entity:null),this.options=a.options||this.options;var b=this.entity||this.entityFromUrl(this.url);b&&(this.entity=d.Entity.from(b,{model:this.model,typeMapping:a.typeMapping})),this._updateUrl(),this.store&&c.isFunction(this.store.initCollection)&&this.store.initCollection(this,a)},entityFromUrl:function(a){if(a){var b=d.Request.getLocation(this.url).pathname.match(/([^\/]+)\/?$/);if(b&&b.length>1)return b[1]}},sort:function(a){c.isObject(a&&a.sort)&&(this.comparator=d.DataSelector.compileSort(a.sort)),b.Collection.prototype.sort.apply(this,arguments)},select:function(a){var b=a&&a.query?d.DataSelector.create(a.query):null,c=d.Collection.create(null,{model:this.model});return a&&a.sort&&(c.comparator=d.DataSelector.compileSort(a.sort)),this.each(function(a){(!b||b.matches(a.attributes))&&c.add(a)}),c},destroy:function(a){a=a||{};var b=a.success;if(this.length>0){a.success=function(){0===this.length&&b&&b()};for(var c;c=this.first();)this.sync("delete",c,a),this.remove(c)}else b&&b()},sync:function(a,d,e){e=e||{},e.credentials=e.credentials||this.credentials;var f=(e.store?e.store:null)||this.store,g=this,h=arguments;this.logon(e,function(){return f&&c.isFunction(f.sync)?f.sync.apply(g,h):b.sync.apply(g,h)})},save:function(){this.each(function(a){a.save()})},getUrlParams:function(a){a=a||this.getUrl();var b=a.match(/\?([^#]*)/),d={};return b&&b.length>1&&c.each(b[1].split("&"),function(a){var b=a.split("=");d[b[0]]=b[1]}),d},getUrl:function(){return(c.isFunction(this.url)?this.url():this.url)||""},getUrlRoot:function(){var a=this.getUrl();return a?a.indexOf("?")>=0?a.substr(0,a.indexOf("?")):a:""},applyFilter:function(a){this.trigger("filter",this.filter(a))},_updateUrl:function(){var a=this.getUrlParams();if(this.options&&(this.url=this.getUrlRoot(),this.options.query&&(a.query=encodeURIComponent(JSON.stringify(this.options.query))),this.options.fields&&(a.fields=encodeURIComponent(JSON.stringify(this.options.fields))),this.options.sort&&(a.sort=encodeURIComponent(JSON.stringify(this.options.sort))),!c.isEmpty(a))){this.url+="?";var b=[];for(var d in a)b.push(d+(a[d]?"="+a[d]:""));this.url+=b.join("&")}}}),d.DataSelector=d.Object.design({_type:"Bikini.DataSelector",_selector:null,create:function(a){var b=this.design({_selector:null});return b.init(a),b},init:function(a){this._selector=this.compileSelector(a)},matches:function(a){return c.isFunction(this._selector)?this._selector(a):!1},hasOperators:function(a){var b;for(var c in a){var d="$"===c.substr(0,1);if(void 0===b)b=d;else if(b!==d)throw new Error("Inconsistent selector: "+a)}return!!b},compileSelector:function(a){if(c.isFunction(a))return function(b){return a.call(b)};if(this._selectorIsId(a))return function(b){var e=c.isFunction(b.getId)?b.getId():b._id||b.id;return d.Field.prototype.equals(e,a)};if(!a||"_id"in a&&!a._id)return function(){return!1};if(c.isBoolean(a)||c.isArray(a)||d.Field.prototype.isBinary(a))throw new Error("Invalid selector: "+a);return this.compileDocSelector(a)},compileDocSelector:function(a){var b=d.DataSelector,e=[];return c.each(a,function(a,d){if("$"===d.substr(0,1)){if(!c.has(b.LOGICAL_OPERATORS,d))throw new Error("Unrecognized logical operator: "+d);e.push(b.LOGICAL_OPERATORS[d](a))}else{var f=b._makeLookupFunction(d),g=b.compileValueSelector(a);e.push(function(a){var b=f(a);return c.any(b,g)})}}),function(a){var b=c.isFunction(a.getData)?a.getData():a;return c.all(e,function(a){return a(b)})}},compileValueSelector:function(a){var b=d.DataSelector;if(null===a)return function(a){return b._anyIfArray(a,function(a){return null===a})};if(!c.isObject(a))return function(c){return b._anyIfArray(c,function(b){return b===a})};if(c.isRegExp(a))return function(d){return c.isUndefined(d)?!1:b._anyIfArray(d,function(b){return a.test(b)})};if(c.isArray(a))return function(d){return c.isArray(d)?b._anyIfArrayPlus(d,function(c){return b._equal(a,c)}):!1};if(this.hasOperators(a)){var e=[];return c.each(a,function(d,f){if(!c.has(b.VALUE_OPERATORS,f))throw new Error("Unrecognized operator: "+f);e.push(b.VALUE_OPERATORS[f](d,a.$options))}),function(a){return c.all(e,function(b){return b(a)})}}return function(c){return b._anyIfArray(c,function(c){return b._equal(a,c)})}},_makeLookupFunction:function(a){var b,d,e,f=a.indexOf(".");if(-1===f)b=a;else{b=a.substr(0,f);var g=a.substr(f+1);d=this._makeLookupFunction(g),e=/^\d+(\.|$)/.test(g)}return function(a){if(null===a)return[void 0];var f=a[b];return d?c.isArray(f)&&0===f.length?[void 0]:((!c.isArray(f)||e)&&(f=[f]),Array.prototype.concat.apply([],c.map(f,d))):[f]}},_anyIfArray:function(a,b){return c.isArray(a)?c.any(a,b):b(a)},_anyIfArrayPlus:function(a,b){return b(a)?!0:c.isArray(a)&&c.any(a,b)},_selectorIsId:function(a){return c.isString(a)||c.isNumber(a)},_equal:function(a,b){return d.Field.prototype._equals(a,b,!0)},_cmp:function(a,b){return d.Field.prototype._cmp(a,b)},LOGICAL_OPERATORS:{$and:function(a){if(!c.isArray(a)||c.isEmpty(a))throw new Error("$and/$or/$nor must be nonempty array");var b=c.map(a,d.DataSelector.compileDocSelector);return function(a){return c.all(b,function(b){return b(a)})}},$or:function(a){if(!c.isArray(a)||c.isEmpty(a))throw new Error("$and/$or/$nor must be nonempty array");var b=c.map(a,d.DataSelector.compileDocSelector);return function(a){return c.any(b,function(b){return b(a)})}},$nor:function(a){if(!c.isArray(a)||c.isEmpty(a))throw new Error("$and/$or/$nor must be nonempty array");var b=c.map(a,d.DataSelector.compileDocSelector);return function(a){return c.all(b,function(b){return!b(a)})}},$where:function(a){if(!c.isFunction(a)){var b=a;a=function(){return b}}return function(b){return a.call(b)}}},VALUE_OPERATORS:{$in:function(a){if(!c.isArray(a))throw new Error("Argument to $in must be array");return function(b){return d.DataSelector._anyIfArrayPlus(b,function(b){return c.any(a,function(a){return d.DataSelector._equal(a,b)})})}},$all:function(a){if(!c.isArray(a))throw new Error("Argument to $all must be array");return function(b){return c.isArray(b)?c.all(a,function(a){return c.any(b,function(b){return d.DataSelector._equal(a,b)})}):!1}},$lt:function(a){return function(b){return d.DataSelector._anyIfArray(b,function(b){return d.DataSelector._cmp(b,a)<0})}},$lte:function(a){return function(b){return d.DataSelector._anyIfArray(b,function(b){return d.DataSelector._cmp(b,a)<=0})}},$gt:function(a){return function(b){return d.DataSelector._anyIfArray(b,function(b){return d.DataSelector._cmp(b,a)>0})}},$gte:function(a){return function(b){return d.DataSelector._anyIfArray(b,function(b){return d.DataSelector._cmp(b,a)>=0})}},$ne:function(a){return function(b){return!d.DataSelector._anyIfArrayPlus(b,function(b){return d.DataSelector._equal(b,a)})}},$nin:function(a){if(!c.isArray(a))throw new Error("Argument to $nin must be array");var b=this.VALUE_OPERATORS.$in(a);return function(a){return void 0===a?!0:!b(a)}},$exists:function(a){return function(b){return a===(void 0!==b)}},$mod:function(a){var b=a[0],c=a[1];return function(a){return d.DataSelector._anyIfArray(a,function(a){return a%b===c})}},$size:function(a){return function(b){return c.isArray(b)&&a===b.length}},$type:function(a){return function(b){return c.isUndefined(b)?!1:d.DataSelector._anyIfArray(b,function(b){return d.Field.prototype.detectType(b)===a})}},$regex:function(a,b){if(c.isUndefined(b)){if(/[^gim]/.test(b))throw new Error("Only the i, m, and g regexp options are supported");var e=c.isRegExp(a)?a.source:a;a=new RegExp(e,b)}else c.isRegExp(a)||(a=new RegExp(a));return function(b){return c.isUndefined(b)?!1:d.DataSelector._anyIfArray(b,function(b){return a.test(b)})}},$options:function(){return function(){return!0}},$elemMatch:function(a){var b=d.DataSelector.compileDocSelector(a);return function(a){return c.isArray(a)?c.any(a,function(a){return b(a)}):!1}},$not:function(a){var b=d.DataSelector.compileDocSelector(a);return function(a){return!b(a)}}},compileSort:function(a){var b=[];if(c.isArray(a))for(var e=0;e<a.length;e++)b.push("string"==typeof a[e]?{lookup:this._makeLookupFunction(a[e]),ascending:!0}:{lookup:this._makeLookupFunction(a[e][0]),ascending:"desc"!==a[e][1]});else{if("object"!=typeof a)throw new Error("Bad sort specification: ",JSON.stringify(a));for(var f in a)b.push({lookup:this._makeLookupFunction(f),ascending:a[f]>=0})}if(0===b.length)return function(){return 0};var g=function(a,b){var e,f=!0;return c.each(a,function(a){c.isArray(a)||(a=[a]),c.isArray(a)&&0===a.length&&(a=[void 0]),c.each(a,function(a){if(f)e=a,f=!1;else{var c=d.DataSelector._cmp(e,a);(b&&c>0||!b&&0>c)&&(e=a)}})}),e};return function(a,c){a=a.attributes?a.attributes:a,c=c.attributes?c.attributes:c;for(var e=0;e<b.length;++e){var f=b[e],h=g(f.lookup(a),f.ascending),i=g(f.lookup(c),f.ascending),j=d.DataSelector._cmp(h,i);if(0!==j)return f.ascending?j:-j}return 0}}}),d.SqlSelector=d.DataSelector.design({_type:"Bikini.SqlSelector",_selector:null,_query:null,_entity:null,create:function(a,b){var c=this.extend({_entity:b,_selector:null,_query:null});return c.init(a),c},init:function(a){this._selector=this.compileSelector(a),this._query=this.buildSqlQuery(a)},buildStatement:function(){return this._query},buildSqlQuery:function(a){if(a instanceof Function)return"";if(c.isString(a))return a;if(!a||"_id"in a&&!a._id)return"1=2";if(c.isBoolean(a)||c.isArray(a)||d.DataField.isBinary(a))throw new Error("Invalid selector: "+a);return this.buildSqlWhere(a)()},buildSqlWhere:function(a){var b=this,d=[];return c.each(a,function(a,e){if("$"===e.substr(0,1))d.push(b.buildLogicalOperator(e,a));else{var f=b.buildLookup(e),g=b.buildValueSelector(a);c.isFunction(g)&&d.push(function(){return g(f)})}}),function(){var a="";return c.each(d,function(d){c.isFunction(d)&&(a+=d.call(b))}),a}},buildValueSelector:function(a){var b=this;if(null===a)return function(a){return a+" IS NULL"};if(!c.isObject(a))return function(c){return c+" = "+b.buildValue(a)};if(c.isRegExp(a)){var d=a.toString(),e=d.match(/\/[\^]?([^^.*$'+()]*)[\$]?\//);if(e&&e.length>1){var f=d.indexOf("/^")<0?"%":"",g=d.indexOf("$/")<0?"%":"";return function(a){return a+' LIKE "'+f+e[1]+g+'"'}}return null}if(c.isArray(a))return null;if(this.hasOperators(a)){var h=[];return c.each(a,function(a,d){if(!c.has(b.VALUE_OPERATORS,d))throw new Error("Unrecognized operator: "+d);h.push(b.VALUE_OPERATORS[d](a,b))}),function(a){return b.LOGICAL_OPERATORS.$and(h,a)}}return function(c){return c+" = "+b.buildValue(a)}},buildLookup:function(a){var b=this._entity?this._entity.getField(a):null;return a=b&&b.name?b.name:a,'"'+a+'"'},buildValue:function(a){return c.isString(a)?'"'+a.replace(/"/g,'""')+'"':a},buildLogicalOperator:function(a,b){if(c.has(this.LOGICAL_OPERATORS,a)){if(!c.isArray(b)||c.isEmpty(b))throw new Error("$and/$or/$nor must be nonempty array");var d=c.map(b,this.buildSqlWhere,this),e=this;return function(b){return e.LOGICAL_OPERATORS[a](d,b)}}throw new Error("Unrecognized logical operator: "+a)},LOGICAL_OPERATORS:{$and:function(a,b){var d="",e=0;return c.each(a,function(a){var c=null!==a?a(b):"";c&&(e++,d+=d?" AND "+c:c)}),e>1?"( "+d+" )":d},$or:function(a,b){var d="",e=!1;return c.each(a,function(a){var c=null!==a?a(b):"";e|=!c,d+=d&&c?" OR "+c:c}),e?"":"( "+d+" )"},$nor:function(a,b){var d="",e=!1;return c.each(a,function(a){var c=null!==a?a(b):"";e|=!c,d+=d&&c?" OR "+c:c}),e?"":"NOT ( "+d+" )"}},VALUE_OPERATORS:{$in:function(){return null},$all:function(){return null},$lt:function(a,b){return function(c){return c+" < "+b.buildValue(a)}},$lte:function(a,b){return function(c){return c+" <= "+b.buildValue(a)}},$gt:function(a,b){return function(c){return c+" > "+b.buildValue(a)}},$gte:function(a,b){return function(c){return c+"">""+b.buildValue(a)}},$ne:function(a,b){return function(c){return c+" <> "+b.buildValue(a)}},$nin:function(){return null},$exists:function(){return function(a){return a+" IS NOT NULL"}},$mod:function(){return null},$size:function(){return null},$type:function(){return null},$regex:function(){return null},$options:function(){return null},$elemMatch:function(){return null},$not:function(a,b){var c=b.buildSqlWhere(a);return function(a){return"NOT ("+c(a)+")"}}}}),d.Store=function(){this.initialize.apply(this,arguments)},d.Store.extend=d.extend,d.Store.create=d.create,d.Store.design=d.design,c.extend(d.Store.prototype,b.Events,d.Object,{_type:"Bikini.Store",entities:null,options:null,name:"",typeMapping:function(){var a={};return a[d.DATA.TYPE.OBJECTID]=d.DATA.TYPE.STRING,a[d.DATA.TYPE.DATE]=d.DATA.TYPE.STRING,a[d.DATA.TYPE.BINARY]=d.DATA.TYPE.TEXT,a}(),initialize:function(a){a=a||{},this.options=this.options||{},this.options.name=this.name,this.options.typeMapping=this.typeMapping,this.options.entities=this.entities,c.extend(this.options,a||{}),this._setEntities(a.entities||{})},_setEntities:function(a){this.entities={};for(var b in a){var c=d.Entity.from(a[b],{store:this,typeMapping:this.options.typeMapping});c.name=c.name||b;var e=c.collection||d.Collection.extend({model:d.Model.extend({})}),f=e.prototype.model;e.prototype.entity=f.prototype.entity=b,e.prototype.store=f.prototype.store=this,c.idAttribute=c.idAttribute||f.prototype.idAttribute,this.entities[b]=c}},getEntity:function(a){if(a){var b=a.entity||a,d=c.isString(b)?b:b.name;if(d)return this.entities[d]||(b&&b.name?b:{name:d})}},getCollection:function(a){return c.isString(a)&&(a=this.entities[a]),a&&a.collection?d.Collection.prototype.isPrototypeOf(a.collection)?a.collection:new a.collection:void 0},createModel:function(a,b){if(c.isString(a)&&(a=this.entities[a]),a&&a.collection){var d=a.collection.model||a.collection.prototype.model;
if(d)return new d(b)}},getArray:function(a){return c.isArray(a)?a:d.isCollection(a)?a.models:c.isObject(a)?[a]:[]},getDataArray:function(a){var d=[];if(c.isArray(a)||b.Collection.prototype.isPrototypeOf(a))c.each(a,function(a){var b=this.getAttributes(a);b&&d.push(b)});else{var e=this.getAttributes(a);e&&d.push(this.getAttributes(e))}return d},getAttributes:function(a){return b.Model.prototype.isPrototypeOf(a)?a.attributes:c.isObject(a)?a:null},initModel:function(){},initCollection:function(){},initEntity:function(){},sync:function(){},fetch:function(a,b){if(!a||a.models||a.attributes||b||(b=a),a&&(a.models||a.attributes)||!b||!b.entity||(a=this.getCollection(b.entity)),a&&a.fetch){var d=c.extend({},b||{},{store:this});a.fetch(d)}},create:function(a,b,d){if(!a||a.models||d||(b=a,d=b),a&&a.models||!d||!d.entity||(a=this.getCollection(d.entity)),a&&a.create){var e=c.extend({},d||{},{store:this});a.create(b,e)}},save:function(a,b,d){if(!a||a.attributes||d||(b=a,d=b),a&&a.attributes||!d||!d.entity||(a=this.createModel(d.entity)),a&&a.save){var e=c.extend({},d||{},{store:this});a.save(b,e)}},destroy:function(a,b){if(a&&a.destroy){var d=c.extend({},b||{},{store:this});a.destroy(d)}},_checkEntity:function(a,b){if(!d.isEntity(b)){var c=d.Store.CONST.ERROR_NO_ENTITY;return console.error(c),this.handleCallback(a.error,c),this.handleCallback(a.finish,c),!1}return!0},_checkData:function(a,b){if(!(c.isArray(b)&&0!==b.length||c.isObject(b))){var e=d.Store.CONST.ERROR_NO_DATA;return console.error(e),this.handleCallback(a.error,e),this.handleCallback(a.finish,e),!1}return!0},handleSuccess:function(a){var b=Array.prototype.slice.call(arguments,1);a.success&&this.handleCallback.apply(this,[a.success].concat(b)),a.finish&&this.handleCallback.apply(this,[a.finish].concat(b))},handleError:function(a){var b=Array.prototype.slice.call(arguments,1);a.error&&this.handleCallback.apply(this,[a.error].concat(b)),a.finish&&this.handleCallback.apply(this,[a.finish].concat(b))},CONST:{ERROR_NO_ENTITY:"No valid entity specified. ",ERROR_NO_DATA:"No data passed. ",ERROR_LOAD_DATA:"Error while loading data from store. ",ERROR_SAVE_DATA:"Error while saving data to the store. ",ERROR_LOAD_IDS:"Error while loading ids from store. ",ERROR_SAVE_IDS:"Error while saving ids to the store. "}}),d.LocalStorageStore=d.Store.extend({_type:"Bikini.LocalStorageStore",ids:{},sync:function(a,b,e){e=e||{};var f,g=e.store||this.store,h=g.getEntity(b.entity||e.entity||this.entity);if(g&&h&&b){var i=b.id||("create"===a?(new d.ObjectID).toHexString():null);switch(f=e.attrs||b.toJSON(e),a){case"patch":case"update":case"create":"create"!==a&&(f=c.extend(g._getItem(h,i)||{},f)),b.id!==i&&b.idAttribute&&(f[b.idAttribute]=i),g._setItem(h,i,f);break;case"delete":g._removeItem(h,i);break;case"read":if(i)f=g._getItem(h,i);else{f=[];var j=g._getItemIds(h);for(i in j){var k=g._getItem(h,i);k&&f.push(k)}}break;default:return}}f?g.handleSuccess(e,f):g.handleError(e,d.Store.CONST.ERROR_NO_ENTITY)},drop:function(a){var b=this.getEntity(a);if(b&&b.name){for(var c=this._findAllKeys(b),e=0;e<c.length;e++)localStorage.removeItem(c[e]);localStorage.removeItem("__ids__"+b.name),this.handleSuccess(a)}else this.handleError(a,d.Store.CONST.ERROR_NO_ENTITY)},_getKey:function(a,b){return"_"+a.name+"_"+b},_getItem:function(a,b){var c;if(a&&b)try{c=JSON.parse(localStorage.getItem(this._getKey(a,b))),c?a.setId(c,b):this._delItemId(b)}catch(e){console.error(d.Store.CONST.ERROR_LOAD_DATA+e.message)}return c},_setItem:function(a,b,c){if(a&&b&&c)try{localStorage.setItem(this._getKey(a,b),JSON.stringify(c)),this._addItemId(a,b)}catch(e){console.error(d.Store.CONST.ERROR_SAVE_DATA+e.message)}},_removeItem:function(a,b){a&&b&&(localStorage.removeItem(this._getKey(a,b)),this._delItemId(a,b))},_addItemId:function(a,b){var c=this._getItemIds(a);b in c||(c[b]="",this._saveItemIds(a,c))},_delItemId:function(a,b){var c=this._getItemIds(a);b in c&&(delete c[b],this._saveItemIds(a,c))},_findAllKeys:function(a){var b=[],c=this._getKey(a,"");if(c)for(var d,e=localStorage.length,f=0;e>f;f++)d=localStorage.key(f),d&&d===c&&b.push(d);return b},_getItemIds:function(a){try{var b="__ids__"+a.name;return this.ids[a.name]||(this.ids[a.name]=JSON.parse(localStorage.getItem(b))||{}),this.ids[a.name]}catch(c){console.error(d.Store.CONST.ERROR_LOAD_IDS+c.message)}},_saveItemIds:function(a,b){try{var c="__ids__"+a.name;localStorage.setItem(c,JSON.stringify(b))}catch(e){console.error(d.Store.CONST.ERROR_SAVE_IDS+e.message)}}}),d.WebSqlStore=d.Store.extend({_type:"Bikini.WebSqlStore",_selector:null,options:null,name:"themproject",size:1048576,version:"1.0",db:null,dataField:{name:"data",type:"text",required:!0},idField:{name:"id",type:"string",required:!0},typeMapping:function(){var a={};return a[d.DATA.TYPE.OBJECTID]=d.DATA.TYPE.STRING,a[d.DATA.TYPE.DATE]=d.DATA.TYPE.STRING,a[d.DATA.TYPE.OBJECT]=d.DATA.TYPE.TEXT,a[d.DATA.TYPE.ARRAY]=d.DATA.TYPE.TEXT,a[d.DATA.TYPE.BINARY]=d.DATA.TYPE.TEXT,a}(),sqlTypeMapping:function(){var a={};return a[d.DATA.TYPE.STRING]="varchar(255)",a[d.DATA.TYPE.TEXT]="text",a[d.DATA.TYPE.OBJECT]="text",a[d.DATA.TYPE.ARRAY]="text",a[d.DATA.TYPE.FLOAT]="float",a[d.DATA.TYPE.INTEGER]="integer",a[d.DATA.TYPE.DATE]="varchar(255)",a[d.DATA.TYPE.BOOLEAN]="boolean",a}(),initialize:function(a){d.Store.prototype.initialize.apply(this,arguments),this.options=this.options||{},this.options.name=this.name,this.options.size=this.size,this.options.version=this.version,this.options.typeMapping=this.typeMapping,this.options.sqlTypeMapping=this.sqlTypeMapping,c.extend(this.options,a||{}),this._openDb({error:function(a){console.error(a)}})},sync:function(a,b,c){var e=c.store||this.store,f=d.isCollection(b)?b.models:[b];switch(c.entity=c.entity||this.entity,a){case"create":e._checkTable(c,function(){e._insertOrReplace(f,c)});break;case"update":case"patch":e._checkTable(c,function(){e._insertOrReplace(f,c)});break;case"delete":e._delete(f,c);break;case"read":e._select(this,c)}},select:function(a){this._select(null,a)},drop:function(a){this._dropTable(a)},createTable:function(a){this._createTable(a)},execute:function(a){this._executeSql(a)},_openDb:function(a){var b,c;if(!this.db)try{if(window.openDatabase){if(this.db=window.openDatabase(this.options.name,"","",this.options.size),this.entities)for(var d in this.entities)this._createTable({entity:this.entities[d]})}else b="Your browser does not support WebSQL databases."}catch(e){c=e}this.db?this.options.version&&this.db.version!==this.options.version?this._updateDb(a):this.handleSuccess(a,this.db):2===c||"2"===c?this._updateDb(a):(!b&&c&&(b=c),this.handleSuccess(a,b))},_updateDb:function(a){var b,d,e=this;try{var f=window.openDatabase(this.options.name,"","",this.options.size);try{var g=this._sqlUpdateDatabase(f.version,this.options.version);f.changeVersion(f.version,this.options.version,function(a){c.each(g,function(b){console.log("sql statement: "+b),d=b,a.executeSql(b)})},function(b){e.handleError(a,b,d)},function(){e.handleSuccess(a)})}catch(h){b=h.message,console.error("webSql change version failed, DB-Version: "+f.version)}}catch(h){b=h.message}b&&this.handleError(a,b)},_sqlUpdateDatabase:function(){var a=[];if(this.entities)for(var b in this.entities){var c=this.entities[b];a.push(this._sqlDropTable(c.name)),a.push(this._sqlCreateTable(c))}return a},_sqlDropTable:function(a){return"DROP TABLE IF EXISTS '"+a+"'"},_isAutoincrementKey:function(a,b){if(a&&b){var c=this.getField(a,b);return c&&c.type===d.DATA.TYPE.INTEGER}},_sqlPrimaryKey:function(a,b){return b&&1===b.length?this._isAutoincrementKey(a,b[0])?b[0]+" INTEGER PRIMARY KEY ASC AUTOINCREMENT UNIQUE":b[0]+" PRIMARY KEY ASC UNIQUE":""},_sqlConstraint:function(a,b){return b&&b.length>1?"PRIMARY KEY ("+b.join(",")+") ON CONFLICT REPLACE":""},_sqlCreateTable:function(a){var b=this,d=a.getKeys(),e=1===d.length?this._sqlPrimaryKey(a,d):"",f=d.length>1?this._sqlConstraint(a,d):a.constraint||"",g="",h=this.getFields(a);c.each(h,function(a){if(!e||a.name!==d[0]){var c=b._dbAttribute(a);c&&(g+=(g?", ":"")+c)}}),g||(g=this._dbAttribute(this.dataField));var i="CREATE TABLE IF NOT EXISTS '"+a.name+"' (";return i+=e?e+", ":"",i+=g,i+=f?", "+f:"",i+=");"},_sqlDelete:function(a,b){var c="DELETE FROM '"+b.name+"'",d=this._sqlWhere(a,b)||this._sqlWhereFromData(a,b);return d&&(c+=" WHERE "+d),c+=a.and?" AND "+a.and:""},_sqlWhere:function(a,b){this._selector=null;var e="";return c.isString(a.where)?e=a.where:c.isObject(a.where)&&(this._selector=d.SqlSelector.create(a.where,b),e=this._selector.buildStatement()),e},_sqlWhereFromData:function(a,b){var d=this,e=[];if(a&&a.models&&b&&b.idAttribute){var f,g=b.idAttribute,h=this.getField(b,g);if(c.each(a.models,function(a){f=a.id,c.isUndefined(f)||e.push(d._sqlValue(f,h))}),e.length>0)return g+" IN ("+e.join(",")+")"}return""},_sqlSelect:function(a,b){var c="SELECT ";a.fields?a.fields.length>1?c+=a.fields.join(", "):1===a.fields.length&&(c+=a.fields[0]):c+="*",c+=" FROM '"+b.name+"'",a.join&&(c+=" JOIN "+a.join),a.leftJoin&&(c+=" LEFT JOIN "+a.leftJoin);var d=this._sqlWhere(a,b)||this._sqlWhereFromData(a,b);return d&&(c+=" WHERE "+d),a.order&&(c+=" ORDER BY "+a.order),a.limit&&(c+=" LIMIT "+a.limit),a.offset&&(c+=" OFFSET "+a.offset),c},_sqlValue:function(a,b){var c=b&&b.type?b.type:d.Field.prototype.detectType(a);return c===d.DATA.TYPE.INTEGER||c===d.DATA.TYPE.FLOAT?a:c===d.DATA.TYPE.BOOLEAN?a?"1":"0":c===d.DATA.TYPE.NULL?"NULL":(a=d.Field.prototype.transform(a,d.DATA.TYPE.STRING),a=a.replace(/"/g,'""'),'"'+a+'"')},_dbAttribute:function(a){if(a&&a.name){var b=this.options.sqlTypeMapping[a.type],c=a.required?" NOT NULL":"";if(b)return a.name+" "+b.toUpperCase()+c}},_dropTable:function(a){var b=this.getEntity(a);if(b.db=null,this._checkDb(a)&&b){var c=this._sqlDropTable(b.name);this._executeTransaction(a,[c])}},_createTable:function(a){var b=this.getEntity(a);if(b.db=this.db,this._checkDb(a)&&this._checkEntity(a,b)){var c=this._sqlCreateTable(b);this._executeTransaction(a,[c])}},_checkTable:function(a,b){var c=this.getEntity(a);c&&!c.db?this._createTable({success:function(){b()},error:function(b){this.handleError(a,b)},entity:c}):b()},_insertOrReplace:function(a,b){var e=this.getEntity(b);if(this._checkDb(b)&&this._checkEntity(b,e)&&this._checkData(b,a)){for(var f=this._isAutoincrementKey(e,e.getKey()),g=[],h="INSERT OR REPLACE INTO '"+e.name+"' (",i=0;i<a.length;i++){var j=a[i],k="";f||j.id||!j.idAttribute||j.set(j.idAttribute,(new d.ObjectID).toHexString());var l,m,n=b.attrs||j.toJSON();if(c.isEmpty(e.fields)?(l=[j.id,JSON.stringify(n)],m=["id","data"]):(l=c.values(n),m=c.keys(n)),l.length>0){var o=new Array(l.length).join("?,")+"?",p="'"+m.join("','")+"'";k+=h+p+") VALUES ("+o+");",g.push({statement:k,arguments:l})}}this._executeTransaction(b,g)}},_select:function(a,b){var e=this.getEntity(b);if(this._checkDb(b)&&this._checkEntity(b,e)){var f,g=d.isCollection(a);g?a=[]:b.models=[a];var h=this._sqlSelect(b,e),i=this;this.db.readTransaction(function(b){var d=h.statement||h,j=h.arguments;f=d,console.log("sql statement: "+d),j&&console.log(" arguments: "+JSON.stringify(j)),b.executeSql(d,j,function(b,d){for(var f=d.rows.length,h=0;f>h;h++){var j,k=d.rows.item(h);if(c.isEmpty(e.fields)&&i._hasDefaultFields(k))try{j=JSON.parse(k.data)}catch(l){}else j=k;if(j&&(!i._selector||i._selector.matches(j))){if(!g){a=j;break}a.push(j)}}},function(a,b){console.error("webSql error: "+b.message)})},function(a){console.error("WebSql Syntax Error: "+a.message),i.handleError(b,a.message,f)},function(){i.handleSuccess(b,a)})}},_delete:function(a,b){var c=this.getEntity(b);if(this._checkDb(b)&&this._checkEntity(b,c)){b.models=a;var d=this._sqlDelete(b,c);this._executeTransaction(b,[d])}},_executeSql:function(a){a.sql&&this._executeTransaction(a,[a.sql])},_executeTransaction:function(a,b){var d,e;if(this._checkDb(a)){var f=this;try{this.db.transaction(function(a){c.each(b,function(b){var c=b.statement||b,d=b.arguments;e=c,console.log("sql statement: "+c),d&&console.log(" arguments: "+JSON.stringify(d)),a.executeSql(c,d)})},function(b){console.error(b.message),f.handleError(a,b.message,e)},function(){f.handleSuccess(a)})}catch(g){console.error(g.message)}}d&&this.handleCallback(a.error,d,e)},_hasDefaultFields:function(a){return c.every(c.keys(a),function(a){return a===this.idField.name||a===this.dataField.name},this)},_checkDb:function(a){if(!this.db){var b="db handler not initialized.";return console.error(b),this.handleError(a,b),!1}return!0},getFields:function(a){if(c.isEmpty(a.fields)){var b={};b.data=this.dataField;var d=a.idAttribute||"id";return b[d]=this.idField,b}return a.fields},getField:function(a,b){return this.getFields(a)[b]}}),d.BikiniStore=d.Store.extend({_type:"Bikini.BikiniStore",_selector:null,endpoints:{},options:null,localStore:d.WebSqlStore,useLocalStore:YES,useSocketNotify:YES,useOfflineChanges:YES,isConnected:NO,typeMapping:{binary:"text",date:"string"},initialize:function(a){d.Store.prototype.initialize.apply(this,arguments),this.options=this.options||{},this.options.useLocalStore=this.useLocalStore,this.options.useSocketNotify=this.useSocketNotify,this.options.useOfflineChanges=this.useOfflineChanges,this.options.socketPath=this.socketPath,this.options.localStore=this.localStore,this.options.typeMapping=this.typeMapping,this.options.useSocketNotify&&"object"!=typeof io&&(console.log("Socket.IO not present !!"),this.options.useSocketNotify=NO),c.extend(this.options,a||{})},initModel:function(){},initCollection:function(a){var b=a.getUrlRoot(),c=this.getEntity(a.entity);if(b&&c){var d=c.name,e=this._hashCode(b),f=c.credentials||a.credentials,g=f&&f.username?f.username:"",h=d+g+e;a.channel=h;var i=this,j=this.endpoints[e];if(!j){var k=this.getLocation(b);j={},j.baseUrl=b,j.readUrl=a.getUrl(),j.host=k.protocol+"//"+k.host,j.path=k.pathname,j.entity=c,j.channel=h,j.credentials=f,j.socketPath=this.options.socketPath,j.localStore=this.createLocalStore(j),j.messages=this.createMsgCollection(j),j.socket=this.createSocket(j),j.info=this.fetchServerInfo(j),i.endpoints[e]=j}a.endpoint=j,a.listenTo(this,j.channel,this.onMessage,a)}},getEndpoint:function(a){if(a){var b=this._hashCode(a);return this.endpoints[b]}},createLocalStore:function(a,b){if(this.options.useLocalStore&&a){var c={};return c[a.entity.name]={name:a.channel,idAttribute:b},this.options.localStore.create({entities:c})}},createMsgCollection:function(a){if(this.options.useOfflineChanges&&a){var b=d.Collection.design({url:a.url,entity:"msg-"+a.channel,store:this.options.localStore.create()}),c=this;return b.fetch({success:function(){c.sendMessages(a)}}),b}},createSocket:function(a,b){if(this.options.useSocketNotify&&a.socketPath&&a){var c=this,d=a.host,e=a.path;e=a.socketPath||e+("/"===e.charAt(e.length-1)?"":"/")+"live";var f=e&&0===e.indexOf("/")?e.substr(1):e;return a.socket=io.connect(d,{resource:f}),a.socket.on("connect",function(){c._bindChannel(a,b),c.onConnect(a)}),a.socket.on("disconnect",function(){console.log("socket.io: disconnect"),c.onDisconnect(a)}),a.socket}},_bindChannel:function(a,b){var c=this;if(a&&a.socket){var d=a.channel,e=a.socket,f=this.getLastMessageTime(d);b=b||a.entity.name,e.on(d,function(a){a&&(c.trigger(d,a),c.options.useLocalStore&&c.setLastMessageTime(d,a.time))}),e.emit("bind",{entity:b,channel:d,time:f})}},getLastMessageTime:function(a){return localStorage.getItem("__"+a+"last_msg_time")||0},setLastMessageTime:function(a,b){b&&localStorage.setItem("__"+a+"last_msg_time",b)},_hashCode:function(a){var b,c=0;if(0===a.length)return c;for(var d=0,e=a.length;e>d;d++)b=a.charCodeAt(d),c=(c<<5)-c+b,c|=0;return c},onConnect:function(a){this.isConnected=YES,this.fetchChanges(a),this.sendMessages(a)},onDisconnect:function(a){this.isConnected=NO,a.socket&&a.socket.socket&&a.socket.socket.onDisconnect()},onMessage:function(a){if(a&&a.method){var b=this.endpoint?this.endpoint.localStore:null,c={store:b,entity:this.entity,merge:YES,fromMessage:YES,parse:YES},d=a.data;switch(a.method){case"patch":case"update":case"create":c.patch="patch"===a.method;var e=a.id?this.get(a.id):null;e?e.save(d,c):this.create(d,c);break;case"delete":if(a.id)if("all"===a.id){for(;e=this.first();)b&&b.sync.apply(this,["delete",e,{store:b,fromMessage:YES}]),this.remove(e);this.store.setLastMessageTime(this.endpoint.channel,"")}else{var f=this.get(a.id);f&&f.destroy(c)}}}},sync:function(a,b,c){var e=c.store||this.store;if(c.fromMessage)return e.handleCallback(c.success);var f=e.getEndpoint(this.getUrlRoot());if(e&&f){var g=this.channel;d.isModel(b)&&!b.id&&b.set(b.idAttribute,(new d.ObjectID).toHexString());var h=e.getLastMessageTime(g);"read"===a&&f.localStore&&h?"read"===a&&e.fetchChanges(f):e.addMessage(a,b,f.localStore?{}:c,f),f.localStore&&(c.store=f.localStore,f.localStore.sync.apply(this,arguments))}},addMessage:function(a,b,d,e){var f=this;if(a&&b){var g=b.changedSinceSync,h=null,i=YES;switch(a){case"update":case"create":h=d.attrs||b.toJSON();break;case"patch":if(c.isEmpty(g))return;h=b.toJSON({attrs:g});break;case"delete":break;default:i=NO}var j={_id:b.id,id:b.id,method:a,data:h},k=function(a,c){f.emitMessage(a,c,d,b)};i?this.storeMessage(e,j,k):k(e,j)}},emitMessage:function(a,b,e,f){var g=a.channel,h=this,i=d.isModel(f)||"read"!==b.method?a.baseUrl:a.readUrl;b.id&&"create"!==b.method&&(i+="/"+b.id),f.sync.apply(f,[b.method,f,{url:i,error:function(c,d){!c.responseText&&h.options.useOfflineChanges?(h.onDisconnect(a),h.handleCallback(e.success,b.data)):h.removeMessage(a,b,function(){h.handleCallback(e.error,d)})},success:function(d){h.isConnected||h.onConnect(a),h.removeMessage(a,b,function(a,b){if(e.success){var f=d;h.handleCallback(e.success,f)}else if("read"===b.method)for(var i=c.isArray(d)?d:[d],j=0;j<i.length;j++)d=i[j],d&&h.trigger(g,{id:d._id,method:"update",data:d});else h.trigger(g,b)})},store:{}}])},fetchChanges:function(a){var b=this,c=a?a.channel:"",e=b.getLastMessageTime(c);if(a&&a.baseUrl&&c&&e){var f=new d.Collection({});f.fetch({url:a.baseUrl+"/changes/"+e,success:function(){f.each(function(a){a.time&&a.method&&(b.options.useLocalStore&&b.setLastMessageTime(c,a.time),b.trigger(c,a))})},credentials:a.credentials})}},fetchServerInfo:function(a){var b=this;if(a&&a.baseUrl){var c=new d.Model,e=b.getLastMessageTime(a.channel);c.fetch({url:a.baseUrl+"/info",success:function(){if(!e&&c.get("time")&&b.setLastMessageTime(a.channel,c.get("time")),!a.socketPath&&c.get("socketPath")){a.socketPath=c.get("socketPath");var d=c.get("entity")||a.entity.name;b.options.useSocketNotify&&b.createSocket(a,d)}},credentials:a.credentials})}},sendMessages:function(a){if(a&&a.messages){var b=this;a.messages.each(function(c){var d;try{d=JSON.parse(c.get("msg"))}catch(e){}var f=c.get("channel");if(d&&f){var g=b.createModel({collection:a.messages},d.data);b.emitMessage(a,d,{},g)}else c.destroy()})}},mergeMessages:function(a){return a},storeMessage:function(a,b,d){if(a&&a.messages&&b){var e=a.channel,f=a.messages.get(b._id);if(f){var g=JSON.parse(f.get("msg"));f.save({msg:JSON.stringify(c.extend(g,b))})}else a.messages.create({_id:b._id,id:b.id,msg:JSON.stringify(b),channel:e})}d(a,b)},removeMessage:function(a,b,c){if(a&&a.messages){var d=a.messages.get(b._id);d&&d.destroy()}c(a,b)},clear:function(a){if(a){var b=this.getEndpoint(a.getUrlRoot());b&&(b.messages&&b.messages.destroy(),a.reset(),this.setLastMessageTime(b.channel,""))}},getLocation:function(a){var b=document.createElement("a");return b.href=a||this.url,""===b.host&&(b.href=b.href),b}})}(this,Backbone,_,$);
//# sourceMappingURL=bikini.map
/**
* The-M-Project Build Script
* Version: 0.1.0
*/
var _ = require('lodash');
var path = require('path');
module.exports = function (grunt) {
// show elapsed time at the end
require('time-grunt')(grunt);
// load all grunt tasks
require('load-grunt-tasks')(grunt);
var additionalMarkdownFiles = {
'https://raw.github.com/mwaylabs/The-M-Project-Sample-Apps/master/README.md': 'Sample-Apps.md',
'https://raw.github.com/mwaylabs/generator-m/master/README.md': 'Generator.md',
'https://raw.github.com/mwaylabs/The-M-Project-Sample-Apps/master/demoapp/README.md': 'Demo-App.md'
};
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
meta: {
banner: '/*!\n' +
'* Project: The M-Project - Mobile HTML5 Application Framework\n' +
'* Copyright: (c) <%= grunt.template.today("yyyy") %> M-Way Solutions GmbH.\n' +
'* Version: <%= pkg.version %>\n' +
'* Date: <%= grunt.template.today() %>\n' +
'* License: http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt\n' +
'*/'
},
jsonlint: {
sample: {
src: [ 'package.json', 'bower.json' ]
}
},
jshint: {
options: {
jshintrc: '.jshintrc',
ignores: ['src/_*.js', 'src/bikini*.js' ]
},
src: [ 'src/**/*.js' ]
},
preprocess: {
options: {
context: {
BANNER: '<%= meta.banner %>',
VERSION: '<%= pkg.version %>'
}
},
dev: {
files: {
'.tmp/bikini.js': 'src/bikini.js'
}
},
dist: {
files: {
'dist/bikini.js': 'src/bikini.js'
}
}
},
uglify: {
options: {
preserveComments: 'some'
},
core: {
src: 'dist/bikini.js',
dest: 'dist/bikini.min.js',
options: {
sourceMap: 'dist/bikini.map',
sourceMappingURL: 'bikini.map',
sourceMapPrefix: 1
}
}
},
watch: {
js: {
files: ['src/**/*'],
tasks: ['build-js'],
options: {
spawn: false
}
},
test: {
files: ['test/**/*'],
tasks: ['test'],
options: {
spawn: false
}
}
},
mocha: {
options: {
bail: true,
reporter: "Spec"
},
all: ['test/test.html']
},
jsdoc : {
dist : {
src: ['doc-template/.tmp/index.md','src/connection/*.js','src/core/*.js','src/data/*.js','src/data/stores/*.js','src/interfaces/*.js','src/ui/*.js','src/ui/layouts/*.js','src/ui/views/*.js','src/utility/*.js'],
options:{
destination: 'doc',
template: "doc-template",
configure: "doc-template/jsdoc.conf.json",
tutorials: "doc-template/additional",
private: false
}
}
},
'curl-dir': {
customFilepaths: {
src: (function() {
return Object.keys(additionalMarkdownFiles);
})(),
router: function (url) {
return additionalMarkdownFiles[url];
},
dest: 'doc-template/additional'
}
},
clean: {
md: {
src: [
'doc-template/additional/Sample-Apps.md',
'doc-template/additional/Demo-App.md',
'doc-template/additional/Generator.md',
'doc-template/.tmp/index.md'
]
}
},
express: {
test: {
options: {
script: './server/server.js'
}
}
}
});
grunt.loadTasks('tasks');
grunt.loadNpmTasks('grunt-express-server');
grunt.registerTask('build', ['preprocess:dev']);
grunt.registerTask('dev', ['default', 'watch']);
grunt.registerTask('test', ['jshint', /*'express:test',*/ 'mocha']);
grunt.registerTask('dist', ['jshint', 'preprocess:dist', 'uglify']);
grunt.registerTask('precommit', ['travis']);
grunt.registerTask('travis', ['jsonlint', 'default', 'test']);
grunt.registerTask('default', ['build']);
grunt.registerTask('build-doc', ['clean:md','curl-dir', 'rewriteMarkdownFiles', 'jsdoc', 'clean:md']);
};
@echo off
echo initializin project
echo Installing required npm packages ... & npm install & echo Installing required bower packages ... & bower install & echo Installing git hooks ... & xcopy scripts\pre-commit .git\hooks\pre-commit & pause
#!/usr/bin/env bash
#
# Script to initialize the repo
# - install required node packages
# - install required bower packages
# - install git hooks
node=`which node 2>&1`
if [ $? -ne 0 ]; then
echo "Please install NodeJS."
echo "http://nodejs.org/"
exit 1
fi
npm=`which npm 2>&1`
if [ $? -ne 0 ]; then
echo "Please install NPM."
fi
bower=`which bower 2>&1`
if [ $? -ne 0 ]; then
echo "Installing Bower ..."
npm install -g bower
fi
echo "Installing required npm packages ..."
npm install
echo "Installing required bower packages ..."
bower install
# Don't need git hooks in travis-ci
if [ -z "${TRAVIS}" ]; then
echo "Installing git hooks ..."
cp scripts/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
fi
echo "Done."
Copyright (c) 2013 M-Way Solutions GmbH
http://www.mwaysolutions.com/
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.

Sorry, the diff of this file is not supported yet

exports.listen = function(server, resource) {
var io = require('socket.io').listen( server, { resource: resource } );
// enable ore default transports
io.set('transports', [
'websocket',
'xhr-polling',
'jsonp-polling'
]);
var bikini = {
io: io,
bindings: {},
live: io.sockets.authorization(function (handshakeData, callback) {
handshakeData.name = handshakeData.query.name;
callback(null, true);
}).on('connection', function (socket) {
socket.on('bind', function(binding) {
if (binding && binding.entity && binding.channel) {
var entity = binding.entity;
var channel = binding.channel;
bikini.bindings[channel] = binding;
// listen to this channel
socket.on(channel, function(msg, fn) {
bikini.handleMessage(entity, msg, function(data, error) {
// if the response is an object message has succeeded
if (typeof data === 'object') {
msg.data = data;
msg.time = new Date().getTime();
msg.id = data._id;
if (msg.method != 'read') {
socket.broadcast.emit(channel, msg);
}
} else if (!error) {
error = typeof data === 'string' ? data : 'error processing message!';
}
// callback to the client, send error if failed
fn(msg, error);
});
});
// send update messages, saved since time
if (binding && binding.time) {
bikini.readMessages(entity, binding.time, function(msg) {
if (msg) {
socket.emit(channel, msg);
}
});
}
}
});
}),
handleMessage: function(entity, msg, callback) {
if (msg && msg.method && msg.id && msg.data) {
if (typeof callback === 'function') {
callback(msg.data);
}
}
},
sendMessage: function(entity, msg) {
if (entity && msg && msg.method) {
for (var channel in bikini.bindings) {
if (bikini.bindings[channel].entity === entity) {
io.sockets.emit(channel, msg);
}
}
}
},
readMessages: function(entity, time, callback) {
}
};
return bikini;
};
exports.create = function(dbName) {
var mongodb = require('mongodb');
var server = new mongodb.Server("127.0.0.1", 27017, {});
var ObjectID = mongodb.ObjectID;
var rest = {
db: null,
toId: function(id, createNewIfEmpty) {
try {
if (id || createNewIfEmpty) {
return new ObjectID(id);
}
} catch (e) {
}
return id; // parseInt(id) !== NaN ? parseInt(id) : id;
},
toInt: function(s) {
try {
return parseInt(s);
} catch(e) {}
},
fromJson: function(json) {
var obj;
try {
if (json) {
obj = JSON.parse(json);
}
} catch (e) {
}
return obj || {};
},
//Find documents
find: function(req, res) {
var name = req.params.name;
var query = this.fromJson(req.query.query);
var fields = this.fromJson(req.query.fields);
var sort = this.fromJson(req.query.sort);
var limit = this.toInt(req.query.limit);
var offset = this.toInt(req.query.offset);
var collection = new mongodb.Collection(this.db, name);
var cursor = collection.find(query, fields);
if (sort) {
cursor.sort(sort);
}
if (limit) {
cursor.limit(limit);
}
if (offset) {
cursor.skip(offset);
}
cursor.toArray(function(err, docs) {
if(err){
res.send(400, err);
} else {
if (req.query.var) {
var script = req.query.var + " = " + JSON.stringify(docs) + ";";
res.send(script);
} else {
res.send(docs);
}
}
});
},
//Find a specific document
findOne: function(req, res) {
var name = req.params.name;
var id = this.toId(req.params.id);
if (typeof id === 'undefined' || id === '') {
return res.send(400, "invalid id.");
}
var collection = new mongodb.Collection(this.db, name);
collection.find({ "_id" : id }, { limit:1 }).nextObject(function(err, doc){
if(err){
res.send(400, err);
} else if (doc) {
res.send(doc);
} else {
res.send(404, 'Document not found!');
}
}
);
},
//Find a specific document
findChanges: function(req, res) {
var name = req.params.name;
var time = parseInt(req.params.time);
if (!time && time !== 0) {
return res.send(400, "invalid timestamp.");
}
var messages = [];
this.readMessages(name, time, function(message) {
if (message) {
messages.push(message);
} else {
res.send(messages);
}
})
},
//Create new document(s)
create: function(req, res, fromMessage) {
var name = req.params.name;
var doc = req.body;
// if this is an array
if (Array.isArray(doc)) {
var error, data = [];
var count = 0, len = doc.length;
if (len > 0) {
var resp = {
send: function(code, response) {
count++;
response = response || code;
if (typeof response === 'object') {
data.push(response);
} else {
error = response;
}
if (count >= len) {
if (data.length > 0) {
res.send(data);
} else {
res.send(400, error);
}
}
}
};
for (var i = 0; i < len ; i++) {
this.createOne(name, doc[i], fromMessage, resp);
}
} else {
res.send(data);
}
} else {
this.createOne(name, doc, fromMessage, res);
}
},
//Create new document(s)
createOne: function(name, doc, fromMessage, res) {
var id = this.toId(doc._id, true);
if (typeof id === 'undefined' || id === '') {
return res.send(400, "invalid id.");
}
doc._id = id;
doc._time = new Date().getTime();
var collection = new mongodb.Collection(this.db, name);
collection.insert(
doc,
{safe:true},
function(err, docs) {
if(err) {
res.send(400, err);
} else {
var doc = docs && docs.length > 0 ? docs[0] : null;
if (doc) {
var msg = {
method: 'create',
id: doc._id,
time: doc._time,
data: doc
};
rest.onSuccess(name, msg, fromMessage);
res.send(doc);
} else {
res.send(400, 'failed to create document.');
}
}
}
);
},
//Update a document
update: function(req, res, fromMessage) {
var name = req.params.name;
var doc = req.body;
var id = this.toId(req.params.id || doc._id);
if (typeof id === 'undefined' || id === '') {
return res.send(400, "invalid id.");
}
doc._id = id;
doc._time = new Date().getTime();
var collection = new mongodb.Collection(this.db, name);
collection.update(
{ "_id" : id },
doc,
{ safe:true, upsert:true },
function(err, n) {
if(err) {
res.send("Oops!: " + err);
} else {
if (n==0) {
res.send(404, 'Document not found!');
} else {
var msg = {
method: 'update',
id: doc._id,
time: doc._time,
data: doc
};
rest.onSuccess(name, msg, fromMessage);
res.send(doc);
}
}
}
);
},
//Delete a contact
delete: function(req, res, fromMessage) {
var name = req.params.name;
var id = this.toId(req.params.id);
if (typeof id === 'undefined' || id === '') {
return res.send(400, "invalid id.");
}
var doc = {};
doc._id = id;
doc._time = new Date().getTime();
var collection = new mongodb.Collection(this.db, name);
if (id === 'all' || id === 'clean') {
collection.drop(function (err) {
if(err) {
res.send(400, err);
} else {
var msg = {
method: 'delete',
id: doc._id,
time: doc._time
};
rest.onSuccess(name, msg, fromMessage);
res.send(doc);
}
});
} else {
collection.remove({ "_id" : id }, { }, function(err, n){
if(err) {
res.send(400, err);
} else {
if (n==0) {
res.send(404, 'Document not found!');
}
if (n > 0) {
var msg = {
method: 'delete',
id: doc._id,
time: doc._time,
data: doc
};
rest.onSuccess(name, msg, fromMessage);
res.send(doc);
}
}
}
);
}
},
//save a new change message
saveMessage: function(entity, msg) {
if (!msg._id) {
msg._id = new ObjectID();
}
var collection = new mongodb.Collection(this.db, "__msg__" + entity);
if (msg.method === 'delete' && (msg.id === 'all' || msg.id === 'clean')) {
collection.remove(function () {
if (msg.id === 'all') {
collection.insert(msg, { safe: false } );
}
});
} else {
collection.insert(msg, { safe: false } );
}
},
//read the change message, since time
readMessages: function(entity, time, callback) {
var collection = new mongodb.Collection(this.db, "__msg__" + entity);
time = parseInt(time);
if (time || time === 0) {
collection.ensureIndex(
{ time: 1, id: 1 },
{ unique:true, background:true, w:1 },
function(err, indexName) {
var cursor = collection.find({ time: { $gt: time } });
var id, lastMsg;
cursor.sort([['id', 1], ['time', 1]]).each(function(err, msg) {
if (msg && msg.id) {
// the same id, merge document
if (id && id === msg.id) {
if (lastMsg) {
msg = rest.mergeMessage(lastMsg, msg);
}
} else if (lastMsg) {
// send the document to the client
callback(lastMsg);
}
lastMsg = msg;
id = msg.id;
} else {
if (lastMsg) {
callback(lastMsg);
}
callback(null);
}
});
}
);
} else {
callback(null);
}
},
mergeMessage: function(doc1, doc2) {
doc1 = doc1 || {};
if (doc2) {
doc1.id = doc2.id;
doc1.time = doc2.time;
if (doc2.method && (doc1.method !== 'create' || doc2.method === 'delete')) {
doc1.method = doc2.method;
}
doc1.data = doc1.data || {};
if (doc2.data) {
for (var key in doc2.data) {
doc1.data[key] = doc2.data[key];
}
}
}
return doc1;
},
onSuccess: function(entity, msg, fromMessage) {
this.saveMessage(entity, msg);
if (!fromMessage) {
this.sendMessage(entity, msg);
}
},
sendMessage: function(entity, msg) {
},
handleMessage: function(entity, msg, callback) {
if (entity && msg && msg.method) {
var req = {
params: { name: entity, id: msg.id },
query: {},
body: msg.data
};
var resp = {
send: function(data, error) {
if (typeof callback === 'function') {
callback(data, error);
}
}
};
switch(msg.method) {
case 'create':
this.create(req, resp, true);
break;
case 'update':
this.update(req, resp, true);
break;
case 'patch':
this.update(req, resp, true);
break;
case 'delete':
this.delete(req, resp, true);
break;
case 'read':
if (msg.id) {
this.findOne(req, resp, true);
} else {
this.find(req, resp, true);
}
break;
default:
return;
}
}
}
};
new mongodb.Db(dbName, server, {w: 1}).open(function (error, client) {
if (error) throw error;
rest.db = client;
});
return rest;
};
{
"name": "tmp2server",
"description": "tmp2 test server",
"version": "0.0.1",
"private": true,
"dependencies": {
"express": "",
"socket.io": "",
"mongodb": ""
},
"_": {
"socket.io-mongo": "",
"derby": ""
}
}
<html>
<body>
<h1>The-M-Project 2.0 - node Test Server</h1>
</body>
</html>
var fs = require('fs');
var http = require('http');
var express = require('express');
var handlebars = require('handlebars');
var liveDbMongo = require('livedb-mongo');
var redis = require('redis').createClient();
var racerBrowserChannel = require('racer-browserchannel');
var racer = require('racer');
redis.select(14);
var store = racer.createStore({
db: liveDbMongo('localhost:27017/racer-pad?auto_reconnect', {safe: true})
, redis: redis
});
app = express();
app
.use(express.favicon())
.use(express.compress())
.use(racerBrowserChannel(store))
.use(store.modelMiddleware())
.use(app.router)
app.use(function(err, req, res, next) {
console.error(err.stack || (new Error(err)).stack);
res.send(500, 'Something broke!');
});
function scriptBundle(cb) {
// Use Browserify to generate a script file containing all of the client-side
// scripts, Racer, and BrowserChannel
store.bundle(__dirname + '/client.js', function(err, js) {
if (err) return cb(err);
cb(null, js);
});
}
// Immediately cache the result of the bundling in production mode, which is
// deteremined by the NODE_ENV environment variable. In development, the bundle
// will be recreated on every page refresh
if (racer.util.isProduction) {
scriptBundle(function(err, js) {
if (err) return;
scriptBundle = function(cb) {
cb(null, js);
};
});
}
app.get('/script.js', function(req, res, next) {
scriptBundle(function(err, js) {
if (err) return next(err);
res.type('js');
res.send(js);
});
});
var indexTemplate = fs.readFileSync(__dirname + '/index.handlebars', 'utf-8');
var indexPage = handlebars.compile(indexTemplate);
app.get('/:roomId', function(req, res, next) {
var model = req.getModel();
// Only handle URLs that use alphanumberic characters, underscores, and dashes
if (!/^[a-zA-Z0-9_-]+$/.test(req.params.roomId)) return next();
// Prevent the browser from storing the HTML response in its back cache, since
// that will cause it to render with the data from the initial load first
res.setHeader('Cache-Control', 'no-store');
var roomPath = 'rooms.' + req.params.roomId;
model.subscribe(roomPath, function(err) {
if (err) return next(err);
model.ref('_page.room', roomPath);
model.bundle(function(err, bundle) {
if (err) return next(err);
var html = indexPage({
room: req.params.roomId
, text: model.get(roomPath)
// Escape bundle for use in an HTML attribute in single quotes, since
// JSON will have lots of double quotes
, bundle: JSON.stringify(bundle).replace(/'/g, '&#39;')
});
res.send(html);
});
});
});
app.get('/', function(req, res) {
res.redirect('/home');
});
var port = process.env.PORT || 3000;
http.createServer(app).listen(port, function() {
console.log('Go to http://localhost:' + port);
});
# The-M-Project 2.0 - node Test Server
For use during development of a node.js based server features.
# Installation
npm install
# Start server
node server.js
var exec = require('child_process').exec;
//start mongodb
exec('mongod run --config /usr/local/etc/mongod.conf', function(){});
var express = require('express');
var server = express();
var http_server = require('http').createServer(server);
var socketPath = '/bikini/live';
var sockets = require('./bikini_live.js').listen(http_server, socketPath);
var rest = require('./mongodb_rest.js').create("test");
var PORT = 8200;
var path = "/bikini";
http_server.listen(PORT);
console.log('http://127.0.0.1:' + PORT);
server.use(express.static(__dirname + '/public/'));
server.use(express.static(__dirname + '/../framework/'));
server.use(express.bodyParser());
/* Allow Access-Control-Allow-Origin to everyone */
server.use(function(req, res, next) {
var oneof = false;
if(req.headers.origin) {
res.header('Access-Control-Allow-Origin', req.headers.origin);
oneof = true;
}
if(req.headers['access-control-request-method']) {
res.header('Access-Control-Allow-Methods', req.headers['access-control-request-method']);
oneof = true;
}
if(req.headers['access-control-request-headers']) {
res.header('Access-Control-Allow-Headers', req.headers['access-control-request-headers']);
oneof = true;
}
if(oneof) {
res.header('Access-Control-Max-Age', 60 * 60 * 24 * 365);
}
// intercept OPTIONS method
if (oneof && req.method == 'OPTIONS') {
res.send(200);
}
else {
next();
}
});
//Find documents
server.get(path+"/:name/info", function(req, res) {
res.send({
time: new Date().getTime(),
socketPath: socketPath,
entity: req.params.name
});
});
//Find documents
server.get(path+"/:name", function(req, res) {
rest.find(req, res);
});
//Find a specific document
server.get(path+'/:name/:id', function(req, res) {
rest.findOne(req, res);
});
//Find a changes since given time
server.get(path+'/:name/changes/:time', function(req, res) {
rest.findChanges(req, res);
});
//Create document(s)
server.post(path+'/:name', function(req, res) {
rest.create(req, res);
});
//Update a document
server.put(path+'/:name/:id', function(req, res) {
rest.update(req, res);
});
//Delete a document
server.delete(path+'/:name/:id', function(req, res){
rest.delete(req, res);
});
// overriden functions
rest.sendMessage = function(entity, msg) {
rest.saveMessage(entity, msg);
sockets.sendMessage(entity, msg);
};
sockets.handleMessage = function(entity, msg, callback) {
if (msg && msg.method) {
return rest.handleMessage(entity, msg, callback);
}
};
sockets.readMessages = function(entity, time, callback) {
rest.readMessages(entity, time, callback);
};
var express = require('express');
var server = express();
var http_server = require('http').createServer(server);
sharejs = require('share');
http_server.listen(8100);
server.use(express.static(__dirname + '/public/'));
var options = {db: {type: 'none'}}; // See docs for options. {type: 'redis'} to enable persistance.
/*
var options = {
db: { type: 'couchdb' },
browserChannel: { cors: '*' },
auth: function(client, action) {
// This auth handler rejects any ops bound for docs starting with 'readonly'.
if (action.name === 'submit op' && action.docName.match(/^readonly/)) {
action.reject();
} else {
action.accept();
}
}
};
*/
console.log("ShareJS v" + sharejs.version);
console.log("Options: ", options);
// Attach the sharejs REST and Socket.io interfaces to the server
sharejs.server.attach(server, options);
exports.listen = function(server) {
var io = require('socket.io').listen(server);
// var RedisStore = require('socket.io/lib/stores/redis')
// , redis = require('socket.io/node_modules/redis')
// , pub = redis.createClient()
// , sub = redis.createClient()
// , client = redis.createClient();
//
// io.set('store', new RedisStore({
// redisPub : pub,
// redisSub : sub,
// redisClient : client
// }));
// var MongoStore = require('socket.io-mongo');
// io.configure(function() {
// var store = new MongoStore({ url: 'mongodb://localhost:27017/test' });
// store.on('error', console.error);
// io.set('store', store);
// });
var sockets = {
io: io,
chatMessages: [],
test: io.of('/test').on('connection', function (socket) {
socket.emit('connected', { hello: 'world' });
socket.on('news', function (data) {
console.log(data);
socket.emit('news', { hello: data });
});
}),
chat: io.of('/chat').authorization(function (handshakeData, callback) {
handshakeData.name = handshakeData.query.name;
callback(null, true);
}).on('connection', function (socket) {
var name = socket.handshake.name || 'Nobody';
socket.emit('connected', socket.id);
for (var i=0; i<sockets.chatMessages.length; i++) {
socket.emit('message', sockets.chatMessages[i]);
}
var time = new Date().getTime();
// tell all others, there is a new user
socket.broadcast.emit('new-user', { name: name, time: time });
// send new messages to everyone in "/chat", including the sender
socket.on('message', function(data) {
sockets.chatMessages.push(data);
if (sockets.chatMessages.length > 20) {
sockets.chatMessages.shift();
}
sockets.chat.emit('message', data);
});
}),
liveData: io.of('/live_data').authorization(function (handshakeData, callback) {
handshakeData.name = handshakeData.query.name;
callback(null, true);
}).on('connection', function (socket) {
socket.on('bind', function(data) {
var entity = typeof data === 'object' ? data.entity : data;
if (entity && typeof entity === 'string') {
var channel = 'entity_' + entity;
// listen to this channel
socket.on(channel, function(msg, fn) {
sockets.handleMessage(entity, msg, function(data, error) {
// if the response is an object message has succeeded
if (typeof data === 'object') {
msg.data = data;
msg.time = new Date().getTime();
msg.id = data._id;
if (msg.method != 'read') {
socket.broadcast.emit(channel, msg);
}
} else if (!error) {
error = typeof data === 'string' ? data : 'error processing message!';
}
// callback to the client, send error if failed
fn(msg, error);
});
});
// send update messages, saved since time
if (data && data.time) {
sockets.readMessages(entity, data.time, function(msg) {
if (msg) {
socket.emit(channel, msg);
}
});
}
}
});
}),
handleMessage: function(entity, msg, callback) {
if (msg && msg.method && msg.id && msg.data) {
if (typeof callback === 'function') {
callback(msg.data);
}
}
},
sendMessage: function(entity, msg) {
var channel = 'entity_' + entity;
if (msg && msg.method) {
this.liveData.emit(channel, msg);
}
},
readMessages: function(entity, time, callback) {
}
};
return sockets;
};
// @echo BANNER
(function (global, Backbone, _, $) {
// @include ./core/bikini.js
// @include ./utility/objectid.js
// @include ./utility/uuid.js
// @include ./utility/base64.js
// @include ./utility/sha256.js
// @include ./utility/cypher.js
// @include ./utility/date.js
// @include ./data/field.js
// @include ./data/entity.js
// @include ./data/security.js
// @include ./data/model.js
// @include ./data/collection.js
// @include ./data/data_selector.js
// @include ./data/sql_selector.js
// @include ./data/stores/store.js
// @include ./data/stores/local_storage.js
// @include ./data/stores/web_sql.js
// @include ./data/stores/bikini_store.js
})(this, Backbone, _, $);
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
/**
* Defines the general namespace
*
* @type {Object}
*/
var Bikini = null;
if( typeof exports !== 'undefined' ) {
Bikini = exports;
} else {
Bikini = global.Bikini = {};
}
/**
* Version number of current release
* @type {String}
*/
Bikini.Version = Bikini.version = '/* @echo VERSION */';
/**
* Empty function to be used when
* no functionality is needed
*
* @type {Function}
*/
Bikini.f = function() {
};
Bikini.create = function( args ) {
return new this(args);
};
Bikini.design = function( obj ) {
var O = this.extend(obj || {});
return new O();
};
Bikini.extend = Backbone.Model.extend;
Bikini.isCollection = function( collection ) {
return Backbone.Collection.prototype.isPrototypeOf(collection);
};
Bikini.isModel = function( model ) {
return Backbone.Model.prototype.isPrototypeOf(model);
};
Bikini.isEntity = function( entity ) {
return Bikini.Entity.prototype.isPrototypeOf(entity);
};
/***
* Data type Constants.
*/
Bikini.DATA = {
TYPE: {
INTEGER: 'integer',
STRING: 'string',
TEXT: 'text',
DATE: 'date',
BOOLEAN: 'boolean',
FLOAT: 'float',
OBJECT: 'object',
ARRAY: 'array',
BINARY: 'binary',
OBJECTID: 'objectid',
NULL: 'null'
}
};
Bikini.Object = {
/**
* The type of this object.
*
* @type String
*/
_type: 'Bikini.Object',
/**
* Creates an object based on a passed prototype.
*
* @param {Object} proto The prototype of the new object.
*/
_create: function( proto ) {
var F = function() {
};
F.prototype = proto;
return new F();
},
/**
* Includes passed properties into a given object.
*
* @param {Object} properties The properties to be included into the given object.
*/
include: function( properties ) {
for( var prop in properties ) {
if( this.hasOwnProperty(prop) ) {
throw Bikini.Exception.RESERVED_WORD.getException();
}
this[prop] = properties[prop];
}
return this;
},
/**
* Creates a new class and extends it with all functions of the defined super class
* The function takes multiple input arguments. Each argument serves as additional
* super classes - see mixins.
*
* @param {Object} properties The properties to be included into the given object.
*/
design: function( properties ) {
/* create the new object */
// var obj = Bikini.Object._create(this);
var obj = this._create(this);
/* assign the properties passed with the arguments array */
obj.include(this._normalize(properties));
/* return the new object */
return obj;
},
/**
* Binds a method to its caller, so it is always executed within the right scope.
*
* @param {Object} caller The scope of the method that should be bound.
* @param {Function} method The method to be bound.
* @param {Object} arg One or more arguments. If more, then apply is used instead of call.
*/
bindToCaller: function( caller, method, arg ) {
return function() {
if( typeof method !== 'function' || typeof caller !== 'object' ) {
throw Bikini.Exception.INVALID_INPUT_PARAMETER.getException();
}
if( Array.isArray(arg) ) {
return method.apply(caller, arg);
}
return method.call(caller, arg);
};
},
/**
* This method is used internally to normalize the properties object that is used
* for extending a given object.
*
* @param obj
* @returns {Object}
* @private
*/
_normalize: function( obj ) {
obj = obj && typeof obj === 'object' ? obj : {};
return obj;
},
/**
* Calls a method defined by a handler
*
* @param {Object} handler A function, or an object including target and action to use with bindToCaller.
*/
handleCallback: function( handler ) {
var args = Array.prototype.slice.call(arguments, 1);
if( handler ) {
var target = typeof handler.target === 'object' ? handler.target : this;
var action = handler;
if( typeof handler.action === 'function' ) {
action = handler.action;
} else if( typeof handler.action === 'string' ) {
action = target[handler.action];
}
if( typeof action === 'function' ) {
return this.bindToCaller(target, action, args)();
}
}
}
};
/**
* Readable alias for true
*
* @type {Boolean}
*/
YES = true;
/**
* Readable alias for false
*
* @type {Boolean}
*/
NO = false;
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
/**
* The Bikini.Collection can be used like a Backbone Collection,
*
* but there are some enhancements to fetch, save and delete the
* contained models from or to other "data stores".
*
* see LocalStorageStore, WebSqlStore or BikiniStore for examples
*
* @module Bikini.Collection
*
* @type {*}
* @extends Backbone.Collection
*
*/
Bikini.Collection = Backbone.Collection.extend({
constructor: function (options) {
this.init(options);
Backbone.Collection.apply(this, arguments);
}
});
Bikini.Collection.create = Bikini.create;
Bikini.Collection.design = Bikini.design;
_.extend(Bikini.Collection.prototype, Bikini.Object, {
_type: 'Bikini.Collection',
isCollection: YES,
model: Bikini.Model,
entity: null,
options: null,
logon: Bikini.Security.logon,
init: function (options) {
options = options || {};
this.store = options.store || this.store || (this.model ? this.model.prototype.store : null);
this.entity = options.entity || this.entity || (this.model ? this.model.prototype.entity : null);
this.options = options.options || this.options;
var entity = this.entity || this.entityFromUrl(this.url);
if (entity) {
this.entity = Bikini.Entity.from(entity, { model: this.model, typeMapping: options.typeMapping });
}
this._updateUrl();
if (this.store && _.isFunction(this.store.initCollection)) {
this.store.initCollection(this, options);
}
},
entityFromUrl: function (url) {
if (url) {
// extract last path part as entity name
var parts = Bikini.Request.getLocation(this.url).pathname.match(/([^\/]+)\/?$/);
if (parts && parts.length > 1) {
return parts[1];
}
}
},
sort: function (options) {
if (_.isObject(options && options.sort)) {
this.comparator = Bikini.DataSelector.compileSort(options.sort);
}
Backbone.Collection.prototype.sort.apply(this, arguments);
},
select: function (options) {
var selector = options && options.query ? Bikini.DataSelector.create(options.query) : null;
var collection = Bikini.Collection.create(null, { model: this.model });
if (options && options.sort) {
collection.comparator = Bikini.DataSelector.compileSort(options.sort);
}
this.each(function (model) {
if (!selector || selector.matches(model.attributes)) {
collection.add(model);
}
});
return collection;
},
destroy: function (options) {
options = options || {};
var success = options.success;
if (this.length > 0) {
options.success = function () {
if (this.length === 0 && success) {
success();
}
};
var model;
while ((model = this.first())) {
this.sync('delete', model, options);
this.remove(model);
}
} else if (success) {
success();
}
},
sync: function (method, model, options) {
options = options || {};
options.credentials = options.credentials || this.credentials;
var store = (options.store ? options.store : null) || this.store;
var that = this;
var args = arguments;
this.logon(options, function (result) {
if (store && _.isFunction(store.sync)) {
return store.sync.apply(that, args);
} else {
return Backbone.sync.apply(that, args);
}
});
},
/**
* save all containing models
*/
save: function() {
this.each(function(model) {
model.save();
});
},
getUrlParams: function (url) {
url = url || this.getUrl();
var m = url.match(/\?([^#]*)/);
var params = {};
if (m && m.length > 1) {
_.each(m[1].split('&'), function (p) {
var a = p.split('=');
params[a[0]] = a[1];
});
}
return params;
},
getUrl: function (collection) {
return (_.isFunction(this.url) ? this.url() : this.url) || '';
},
getUrlRoot: function () {
var url = this.getUrl();
return url ? ( url.indexOf('?') >= 0 ? url.substr(0, url.indexOf('?')) : url) : '';
},
applyFilter: function (callback) {
this.trigger('filter', this.filter(callback));
},
_updateUrl: function () {
var params = this.getUrlParams();
if (this.options) {
this.url = this.getUrlRoot();
if (this.options.query) {
params.query = encodeURIComponent(JSON.stringify(this.options.query));
}
if (this.options.fields) {
params.fields = encodeURIComponent(JSON.stringify(this.options.fields));
}
if (this.options.sort) {
params.sort = encodeURIComponent(JSON.stringify(this.options.sort));
}
if (!_.isEmpty(params)) {
this.url += '?';
var a = [];
for (var k in params) {
a.push(k + (params[k] ? '=' + params[k] : ''));
}
this.url += a.join('&');
}
}
}
});
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
// Bikini.DataSelector uses code from meteor.js
// https://github.com/meteor/meteor/tree/master/packages/minimongo
//
// Thanks for sharing!
/**
*
* @module Bikini.DataSelector
*
* @type {*}
* @extends Bikini.Object
*/
Bikini.DataSelector = Bikini.Object.design({
_type: 'Bikini.DataSelector',
_selector: null,
create: function (docSelector) {
var selector = this.design({
_selector: null
});
selector.init(docSelector);
return selector;
},
init: function (docSelector) {
this._selector = this.compileSelector(docSelector);
},
matches: function (value) {
if (_.isFunction(this._selector)) {
return this._selector(value);
}
return false;
},
hasOperators: function (valueSelector) {
var theseAreOperators;
for (var selKey in valueSelector) {
var thisIsOperator = selKey.substr(0, 1) === '$';
if (theseAreOperators === undefined) {
theseAreOperators = thisIsOperator;
} else if (theseAreOperators !== thisIsOperator) {
throw new Error('Inconsistent selector: ' + valueSelector);
}
}
return !!theseAreOperators; // {} has no operators
},
// Given a selector, return a function that takes one argument, a
// document, and returns true if the document matches the selector,
// else false.
compileSelector: function (selector) {
// you can pass a literal function instead of a selector
if ( _.isFunction(selector)) {
return function (doc) {
return selector.call(doc);
};
}
// shorthand -- scalars match _id
if (this._selectorIsId(selector)) {
return function (record) {
var id = _.isFunction(record.getId) ? record.getId() : (record._id || record.id);
return Bikini.Field.prototype.equals(id, selector);
};
}
// protect against dangerous selectors. falsey and {_id: falsey} are both
// likely programmer error, and not what you want, particularly for
// destructive operations.
if (!selector || (('_id' in selector) && !selector._id)) {
return function (doc) {
return false;
};
}
// Top level can't be an array or true or binary.
if (_.isBoolean(selector) || _.isArray(selector) || Bikini.Field.prototype.isBinary(selector)) {
throw new Error('Invalid selector: ' + selector);
}
return this.compileDocSelector(selector);
},
// The main compilation function for a given selector.
compileDocSelector: function (docSelector) {
var that = Bikini.DataSelector;
var perKeySelectors = [];
_.each(docSelector, function (subSelector, key) {
if (key.substr(0, 1) === '$') {
// Outer operators are either logical operators (they recurse back into
// this function), or $where.
if (!_.has(that.LOGICAL_OPERATORS, key)) {
throw new Error('Unrecognized logical operator: ' + key);
}
perKeySelectors.push(that.LOGICAL_OPERATORS[key](subSelector));
} else {
var lookUpByIndex = that._makeLookupFunction(key);
var valueSelectorFunc = that.compileValueSelector(subSelector);
perKeySelectors.push(function (doc) {
var branchValues = lookUpByIndex(doc);
// We apply the selector to each 'branched' value and return true if any
// match. This isn't 100% consistent with MongoDB; eg, see:
// https://jira.mongodb.org/browse/SERVER-8585
return _.any(branchValues, valueSelectorFunc);
});
}
});
return function (record) {
var doc = _.isFunction(record.getData) ? record.getData() : record;
return _.all(perKeySelectors, function (f) {
return f(doc);
});
};
},
compileValueSelector: function (valueSelector) {
var that = Bikini.DataSelector;
if (valueSelector === null) { // undefined or null
return function (value) {
return that._anyIfArray(value, function (x) {
return x === null; // undefined or null
});
};
}
// Selector is a non-null primitive (and not an array or RegExp either).
if (!_.isObject(valueSelector)) {
return function (value) {
return that._anyIfArray(value, function (x) {
return x === valueSelector;
});
};
}
if (_.isRegExp(valueSelector)) {
return function (value) {
if (_.isUndefined(value)) {
return false;
}
return that._anyIfArray(value, function (x) {
return valueSelector.test(x);
});
};
}
// Arrays match either identical arrays or arrays that contain it as a value.
if (_.isArray(valueSelector)) {
return function (value) {
if (!_.isArray(value)) {
return false;
}
return that._anyIfArrayPlus(value, function (x) {
return that._equal(valueSelector, x);
});
};
}
// It's an object, but not an array or regexp.
if (this.hasOperators(valueSelector)) {
var operatorFunctions = [];
_.each(valueSelector, function (operand, operator) {
if (!_.has(that.VALUE_OPERATORS, operator)) {
throw new Error('Unrecognized operator: ' + operator);
}
operatorFunctions.push(that.VALUE_OPERATORS[operator](operand, valueSelector.$options));
});
return function (value) {
return _.all(operatorFunctions, function (f) {
return f(value);
});
};
}
// It's a literal; compare value (or element of value array) directly to the
// selector.
return function (value) {
return that._anyIfArray(value, function (x) {
return that._equal(valueSelector, x);
});
};
},
// _makeLookupFunction(key) returns a lookup function.
//
// A lookup function takes in a document and returns an array of matching
// values. This array has more than one element if any segment of the key other
// than the last one is an array. ie, any arrays found when doing non-final
// lookups result in this function 'branching'; each element in the returned
// array represents the value found at this branch. If any branch doesn't have a
// final value for the full key, its element in the returned list will be
// undefined. It always returns a non-empty array.
//
// _makeLookupFunction('a.x')({a: {x: 1}}) returns [1]
// _makeLookupFunction('a.x')({a: {x: [1]}}) returns [[1]]
// _makeLookupFunction('a.x')({a: 5}) returns [undefined]
// _makeLookupFunction('a.x')({a: [{x: 1},
// {x: [2]},
// {y: 3}]})
// returns [1, [2], undefined]
_makeLookupFunction: function (key) {
var dotLocation = key.indexOf('.');
var first, lookupRest, nextIsNumeric;
if (dotLocation === -1) {
first = key;
} else {
first = key.substr(0, dotLocation);
var rest = key.substr(dotLocation + 1);
lookupRest = this._makeLookupFunction(rest);
// Is the next (perhaps final) piece numeric (ie, an array lookup?)
nextIsNumeric = /^\d+(\.|$)/.test(rest);
}
return function (doc) {
if (doc === null) { // null or undefined
return [undefined];
}
var firstLevel = doc[first];
// We don't 'branch' at the final level.
if (!lookupRest) {
return [firstLevel];
}
// It's an empty array, and we're not done: we won't find anything.
if (_.isArray(firstLevel) && firstLevel.length === 0) {
return [undefined];
}
// For each result at this level, finish the lookup on the rest of the key,
// and return everything we find. Also, if the next result is a number,
// don't branch here.
//
// Technically, in MongoDB, we should be able to handle the case where
// objects have numeric keys, but Mongo doesn't actually handle this
// consistently yet itself, see eg
// https://jira.mongodb.org/browse/SERVER-2898
// https://github.com/mongodb/mongo/blob/master/jstests/array_match2.js
if (!_.isArray(firstLevel) || nextIsNumeric) {
firstLevel = [firstLevel];
}
return Array.prototype.concat.apply([], _.map(firstLevel, lookupRest));
};
},
_anyIfArray: function (x, f) {
if (_.isArray(x)) {
return _.any(x, f);
}
return f(x);
},
_anyIfArrayPlus: function (x, f) {
if (f(x)) {
return true;
}
return _.isArray(x) && _.any(x, f);
},
// Is this selector just shorthand for lookup by _id?
_selectorIsId: function (selector) {
return _.isString(selector) || _.isNumber(selector);
},
// deep equality test: use for literal document and array matches
_equal: function (a, b) {
return Bikini.Field.prototype._equals(a, b, true);
},
_cmp: function (a, b) {
return Bikini.Field.prototype._cmp(a, b);
},
LOGICAL_OPERATORS: {
'$and': function (subSelector) {
if (!_.isArray(subSelector) || _.isEmpty(subSelector)) {
throw new Error('$and/$or/$nor must be nonempty array');
}
var subSelectorFunctions = _.map(subSelector, Bikini.DataSelector.compileDocSelector);
return function (doc) {
return _.all(subSelectorFunctions, function (f) {
return f(doc);
});
};
},
'$or': function (subSelector) {
if (!_.isArray(subSelector) || _.isEmpty(subSelector)) {
throw new Error('$and/$or/$nor must be nonempty array');
}
var subSelectorFunctions = _.map(subSelector, Bikini.DataSelector.compileDocSelector);
return function (doc) {
return _.any(subSelectorFunctions, function (f) {
return f(doc);
});
};
},
'$nor': function (subSelector) {
if (!_.isArray(subSelector) || _.isEmpty(subSelector)) {
throw new Error('$and/$or/$nor must be nonempty array');
}
var subSelectorFunctions = _.map(subSelector, Bikini.DataSelector.compileDocSelector);
return function (doc) {
return _.all(subSelectorFunctions, function (f) {
return !f(doc);
});
};
},
'$where': function (selectorValue) {
if (!_.isFunction(selectorValue)) {
var value = selectorValue;
selectorValue = function() { return value; };
}
return function (doc) {
return selectorValue.call(doc);
};
}
},
VALUE_OPERATORS: {
'$in': function (operand) {
if (!_.isArray(operand)) {
throw new Error('Argument to $in must be array');
}
return function (value) {
return Bikini.DataSelector._anyIfArrayPlus(value, function (x) {
return _.any(operand, function (operandElt) {
return Bikini.DataSelector._equal(operandElt, x);
});
});
};
},
'$all': function (operand) {
if (!_.isArray(operand)) {
throw new Error('Argument to $all must be array');
}
return function (value) {
if (!_.isArray(value)) {
return false;
}
return _.all(operand, function (operandElt) {
return _.any(value, function (valueElt) {
return Bikini.DataSelector._equal(operandElt, valueElt);
});
});
};
},
'$lt': function (operand) {
return function (value) {
return Bikini.DataSelector._anyIfArray(value, function (x) {
return Bikini.DataSelector._cmp(x, operand) < 0;
});
};
},
'$lte': function (operand) {
return function (value) {
return Bikini.DataSelector._anyIfArray(value, function (x) {
return Bikini.DataSelector._cmp(x, operand) <= 0;
});
};
},
'$gt': function (operand) {
return function (value) {
return Bikini.DataSelector._anyIfArray(value, function (x) {
return Bikini.DataSelector._cmp(x, operand) > 0;
});
};
},
'$gte': function (operand) {
return function (value) {
return Bikini.DataSelector._anyIfArray(value, function (x) {
return Bikini.DataSelector._cmp(x, operand) >= 0;
});
};
},
'$ne': function (operand) {
return function (value) {
return !Bikini.DataSelector._anyIfArrayPlus(value, function (x) {
return Bikini.DataSelector._equal(x, operand);
});
};
},
'$nin': function (operand) {
if (!_.isArray(operand)) {
throw new Error('Argument to $nin must be array');
}
var inFunction = this.VALUE_OPERATORS.$in(operand);
return function (value) {
// Field doesn't exist, so it's not-in operand
if (value === undefined) {
return true;
}
return !inFunction(value);
};
},
'$exists': function (operand) {
return function (value) {
return operand === (value !== undefined);
};
},
'$mod': function (operand) {
var divisor = operand[0], remainder = operand[1];
return function (value) {
return Bikini.DataSelector._anyIfArray(value, function (x) {
return x % divisor === remainder;
});
};
},
'$size': function (operand) {
return function (value) {
return _.isArray(value) && operand === value.length;
};
},
'$type': function (operand) {
return function (value) {
// A nonexistent field is of no type.
if (_.isUndefined(value)) {
return false;
}
return Bikini.DataSelector._anyIfArray(value, function (x) {
return Bikini.Field.prototype.detectType(x) === operand;
});
};
},
'$regex': function (operand, options) {
if (_.isUndefined(options)) {
// Options passed in $options (even the empty string) always overrides
// options in the RegExp object itself.
// Be clear that we only support the JS-supported options, not extended
// ones (eg, Mongo supports x and s). Ideally we would implement x and s
// by transforming the regexp, but not today...
if (/[^gim]/.test(options)) {
throw new Error('Only the i, m, and g regexp options are supported');
}
var regexSource = _.isRegExp(operand) ? operand.source : operand;
operand = new RegExp(regexSource, options);
} else if (!_.isRegExp(operand)) {
operand = new RegExp(operand);
}
return function (value) {
if (_.isUndefined(value)) {
return false;
}
return Bikini.DataSelector._anyIfArray(value, function (x) {
return operand.test(x);
});
};
},
'$options': function (operand) {
// evaluation happens at the $regex function above
return function (value) {
return true;
};
},
'$elemMatch': function (operand) {
var matcher = Bikini.DataSelector.compileDocSelector(operand);
return function (value) {
if (!_.isArray(value)) {
return false;
}
return _.any(value, function (x) {
return matcher(x);
});
};
},
'$not': function (operand) {
var matcher = Bikini.DataSelector.compileDocSelector(operand);
return function (value) {
return !matcher(value);
};
}
},
// Give a sort spec, which can be in any of these forms:
// {'key1': 1, 'key2': -1}
// [['key1', 'asc'], ['key2', 'desc']]
// ['key1', ['key2', 'desc']]
//
// (.. with the first form being dependent on the key enumeration
// behavior of your javascript VM, which usually does what you mean in
// this case if the key names don't look like integers ..)
//
// return a function that takes two objects, and returns -1 if the
// first object comes first in order, 1 if the second object comes
// first, or 0 if neither object comes before the other.
compileSort: function (spec) {
var sortSpecParts = [];
if (_.isArray(spec)) {
for (var i = 0; i < spec.length; i++) {
if (typeof spec[i] === 'string') {
sortSpecParts.push({
lookup: this._makeLookupFunction(spec[i]),
ascending: true
});
} else {
sortSpecParts.push({
lookup: this._makeLookupFunction(spec[i][0]),
ascending: spec[i][1] !== 'desc'
});
}
}
} else if (typeof spec === 'object') {
for (var key in spec) {
sortSpecParts.push({
lookup: this._makeLookupFunction(key),
ascending: spec[key] >= 0
});
}
} else {
throw new Error('Bad sort specification: ', JSON.stringify(spec));
}
if (sortSpecParts.length === 0) {
return function () {
return 0;
};
}
// reduceValue takes in all the possible values for the sort key along various
// branches, and returns the min or max value (according to the bool
// findMin). Each value can itself be an array, and we look at its values
// too. (ie, we do a single level of flattening on branchValues, then find the
// min/max.)
var reduceValue = function (branchValues, findMin) {
var reduced;
var first = true;
// Iterate over all the values found in all the branches, and if a value is
// an array itself, iterate over the values in the array separately.
_.each(branchValues, function (branchValue) {
// Value not an array? Pretend it is.
if (!_.isArray(branchValue)) {
branchValue = [branchValue];
}
// Value is an empty array? Pretend it was missing, since that's where it
// should be sorted.
if (_.isArray(branchValue) && branchValue.length === 0) {
branchValue = [undefined];
}
_.each(branchValue, function (value) {
// We should get here at least once: lookup functions return non-empty
// arrays, so the outer loop runs at least once, and we prevented
// branchValue from being an empty array.
if (first) {
reduced = value;
first = false;
} else {
// Compare the value we found to the value we found so far, saving it
// if it's less (for an ascending sort) or more (for a descending
// sort).
var cmp = Bikini.DataSelector._cmp(reduced, value);
if ((findMin && cmp > 0) || (!findMin && cmp < 0)) {
reduced = value;
}
}
});
});
return reduced;
};
return function (a, b) {
a = a.attributes ? a.attributes : a;
b = b.attributes ? b.attributes : b;
for (var i = 0; i < sortSpecParts.length; ++i) {
var specPart = sortSpecParts[i];
var aValue = reduceValue(specPart.lookup(a), specPart.ascending);
var bValue = reduceValue(specPart.lookup(b), specPart.ascending);
var compare = Bikini.DataSelector._cmp(aValue, bValue);
if (compare !== 0) {
return specPart.ascending ? compare : -compare;
}
}
return 0;
};
}
});
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
/**
*
* @module Bikini.Entity
*
*/
/**
* Holds description about fields and other entity properties.
* Also helper functions for field and transform operations
* @module Bikini.Entity
*
* @param options
* @constructor
*/
Bikini.Entity = function (options) {
var fields = this.fields;
this.fields = {};
this._mergeFields(fields);
options = options || {};
if (options.fields) {
this._mergeFields(options.fields);
}
this.typeMapping = options.typeMapping || this.typeMapping;
var collection = options.collection;
var model = options.model || (collection ? collection.prototype.model : null);
this.idAttribute = options.idAttribute || this.idAttribute || (model ? model.prototype.idAttribute : '');
this._updateFields(this.typeMapping);
this.initialize.apply(this, arguments);
};
/**
* create a new entity from an other entity or given properties
*
* @param entity
* @param options
* @returns {*}
*/
Bikini.Entity.from = function (entity, options) {
// is not an instance of Bikini.Entity
if (!Bikini.Entity.prototype.isPrototypeOf(entity)) {
// if this is a prototype of an entity, create an instance
if (_.isFunction(entity) &&
Bikini.Entity.prototype.isPrototypeOf(entity.prototype)) {
var Entity = entity;
entity = new Entity(options);
} else {
if (typeof entity === 'string') {
entity = {
name: entity
};
}
// if this is just a config create a new Entity
var E = Bikini.Entity.extend(entity);
entity = new E(options);
}
} else if (options && options.typeMapping) {
entity._updateFields(options.typeMapping);
}
return entity;
};
Bikini.Entity.extend = Bikini.extend;
Bikini.Entity.create = Bikini.create;
Bikini.Entity.design = Bikini.design;
_.extend(Bikini.Entity.prototype, Bikini.Object, {
/**
* The type of this object.
*
* @type String
*/
_type: 'Bikini.Entity',
/**
* Entity name, used for tables or collections
*
* @type String
*/
name: '',
/**
* idAttribute, should be the same as in the corresponding model
*
* @type String
*/
idAttribute: '',
/**
*
*
* @type Object
*/
fields: {},
/**
* initialize function will be called after creating an entity
*/
initialize: function () {
},
/**
* get the field list of this entity
*
* @returns {Object}
*/
getFields: function () {
return this.fields;
},
/**
* get a specified field from this entity
*
* @param fieldKey
* @returns Bikini.Field instance
*/
getField: function (fieldKey) {
return this.fields[fieldKey];
},
/**
* get the translated name of a field
*
* @param fieldKey
* @returns String
*/
getFieldName: function (fieldKey) {
var field = this.getField(fieldKey);
return field && field.name ? field.name : fieldKey;
},
/**
* get the primary key of this entity
*
* @returns String
*/
getKey: function () {
return this.idAttribute || Bikini.Model.idAttribute;
},
/**
* get a list of keys for this entity
*
* @returns {Array}
*/
getKeys: function () {
return this.splitKey(this.getKey());
},
/**
* Splits a comma separated list of keys to a key array
*
* @returns {Array} array of keys
*/
splitKey: function (key) {
var keys = [];
if (_.isString(key)) {
_.each(key.split(','), function (key) {
var k = key.trim();
if (k) {
keys.push(k);
}
});
}
return keys;
},
/**
* merge a new list of fields into the exiting fields
*
* @param newFields
* @private
*/
_mergeFields: function (newFields) {
if (!_.isObject(this.fields)) {
this.fields = {};
}
var that = this;
if (_.isObject(newFields)) {
_.each(newFields, function (value, key) {
if (!that.fields[key]) {
that.fields[key] = new Bikini.Field(value);
} else {
that.fields[key].merge(value);
}
});
}
},
/**
* check and update missing properties of fields
*
* @param typeMapping
* @private
*/
_updateFields: function (typeMapping) {
var that = this;
_.each(this.fields, function (value, key) {
// remove unused properties
if (value.persistent === NO) {
delete that.fields[key];
} else {
// add missing names
if (!value.name) {
value.name = key;
}
// apply default type conversions
if (typeMapping && typeMapping[value.type]) {
value.type = typeMapping[value.type];
}
}
});
},
/**
* transform the given data to attributes
* considering the field specifications
*
* @param data
* @param id
* @param fields
* @returns {*}
*/
toAttributes: function (data, id, fields) {
fields = fields || this.fields;
if (data && !_.isEmpty(fields)) {
// map field names
var value, attributes = {};
_.each(fields, function (field, key) {
value = _.isFunction(data.get) ? data.get(field.name) : data[field.name];
attributes[key] = value;
});
return attributes;
}
return data;
},
/**
* transform the given attributes to the destination data format
* considering the field specifications
*
* @param attrs
* @param fields
* @returns {*}
*/
fromAttributes: function (attrs, fields) {
fields = fields || this.fields;
if (attrs && !_.isEmpty(fields)) {
var data = {};
_.each(fields, function (field, key) {
var value = _.isFunction(attrs.get) ? attrs.get(key) : attrs[key];
value = field.transform(value);
if (!_.isUndefined(value)) {
data[field.name] = value;
}
});
return data;
}
return attrs;
},
/**
* set the id of the given model or attributes
*
* @param attrs
* @param id
* @returns {*}
*/
setId: function (attrs, id) {
if (attrs && id) {
var key = this.getKey() || attrs.idAttribute;
if (key) {
if (_.isFunction(attrs.set)) {
attrs.set(key, id);
} else {
attrs[key] = id;
}
}
}
return attrs;
},
/**
* get the id of the given model or attributes
*
* @param attrs
* @returns {*|Object|key|*}
*/
getId: function (attrs) {
if (attrs) {
var key = this.getKey() || attrs.idAttribute;
if (key) {
return _.isFunction(attrs.get) ? attrs.get(key) : attrs[key];
}
}
}
});
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
/**
*
* @module Bikini.Field
*
*/
/**
* Field describing a data attribute
*
* contains functions to comperate, detect and convert data type
*
* @param options
* @constructor
*/
Bikini.Field = function (options) {
this.merge(options);
this.initialize.apply(this, arguments);
};
Bikini.Field.extend = Bikini.extend;
Bikini.Field.create = Bikini.create;
Bikini.Field.design = Bikini.design;
_.extend(Bikini.Field.prototype, Bikini.Object, {
/**
* The type of this object.
*
* @type String
*/
_type: 'Bikini.Field',
name: null,
type: null,
index: null,
defaultValue: undefined,
length: null,
required: NO,
persistent: YES,
initialize: function () {
},
/**
* merge field properties into this instance
*
* @param obj
*/
merge: function (obj) {
obj = _.isString(obj) ? { type: obj } : (obj || {});
this.name = !_.isUndefined(obj.name) ? obj.name : this.name;
this.type = !_.isUndefined(obj.type) ? obj.type : this.type;
this.index = !_.isUndefined(obj.index) ? obj.index : this.index;
this.defaultValue = !_.isUndefined(obj.defaultValue) ? obj.defaultValue : this.defaultValue;
this.length = !_.isUndefined(obj.length) ? obj.length : this.length;
this.required = !_.isUndefined(obj.required) ? obj.required : this.required;
this.persistent = !_.isUndefined(obj.persistent) ? obj.persistent : this.persistent;
},
/**
* converts the give value into the required data type
*
* @param value
* @param type
* @returns {*}
*/
transform: function (value, type) {
type = type || this.type;
try {
if (_.isUndefined(value)) {
return this.defaultValue;
}
if (type === Bikini.DATA.TYPE.STRING || type === Bikini.DATA.TYPE.TEXT) {
if (_.isObject(value)) {
return JSON.stringify(value);
} else {
return _.isNull(value) ? 'null' : value.toString();
}
} else if (type === Bikini.DATA.TYPE.INTEGER) {
return parseInt(value);
} else if (type === Bikini.DATA.TYPE.BOOLEAN) {
return value === true || value === 'true'; // true, 1, "1" or "true"
} else if (type === Bikini.DATA.TYPE.FLOAT) {
return parseFloat(value);
} else if (type === Bikini.DATA.TYPE.OBJECT || type === Bikini.DATA.TYPE.ARRAY) {
if (!_.isObject(value)) {
return _.isString(value) ? JSON.parse(value) : null;
}
} else if (type === Bikini.DATA.TYPE.DATE) {
if (!Bikini.Date.isPrototypeOf(value)) {
var date = value ? Bikini.Date.create(value) : null;
return date && date.isValid() ? date : null;
}
} else if (type === Bikini.DATA.TYPE.OBJECTID) {
if (!Bikini.ObjectID.prototype.isPrototypeOf(value)) {
return _.isString(value) ? new Bikini.ObjectID(value) : null;
}
}
return value;
} catch (e) {
console.error('Failed converting value! ' + e.message);
}
},
/**
* check to values to be equal for the type of this field
*
* @param a
* @param b
* @returns {*}
*/
equals: function (a, b) {
var v1 = this.transform(a);
var v2 = this.transform(b);
return this._equals(v1, v2, _.isArray(v1));
},
/**
* check if this field holds binary data
*
* @param obj
* @returns {boolean|*}
*/
isBinary: function (obj) {
return (typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array) || (obj && obj.$Uint8ArrayPolyfill);
},
/**
* detect the type of a given value
*
* @param v
* @returns {*}
*/
detectType: function (v) {
if (_.isNumber(v)) {
return Bikini.DATA.TYPE.FLOAT;
}
if (_.isString(v)) {
return Bikini.DATA.TYPE.STRING;
}
if (_.isBoolean(v)) {
return Bikini.DATA.TYPE.BOOLEAN;
}
if (_.isArray(v)) {
return Bikini.DATA.TYPE.ARRAY;
}
if (_.isNull(v)) {
return Bikini.DATA.TYPE.NULL;
}
if (_.isDate(v) || Bikini.Date.isPrototypeOf(v)) {
return Bikini.DATA.TYPE.DATE;
}
if (Bikini.ObjectID.prototype.isPrototypeOf(v)) {
return Bikini.DATA.TYPE.OBJECTID;
}
if (this.isBinary(v)) {
return Bikini.DATA.TYPE.BINARY;
}
return Bikini.DATA.TYPE.OBJECT;
},
/**
* returns the sort order for the given type, used by sorting different type
*
* @param type
* @returns {number}
*/
typeOrder: function (type) {
switch (type) {
case Bikini.DATA.TYPE.NULL :
return 0;
case Bikini.DATA.TYPE.FLOAT :
return 1;
case Bikini.DATA.TYPE.STRING :
return 2;
case Bikini.DATA.TYPE.OBJECT :
return 3;
case Bikini.DATA.TYPE.ARRAY :
return 4;
case Bikini.DATA.TYPE.BINARY :
return 5;
case Bikini.DATA.TYPE.DATE :
return 6;
}
return -1;
},
_equals: function (a, b, keyOrderSensitive) {
var that = this;
var i;
if (a === b) {
return true;
}
if (!a || !b) { // if either one is false, they'd have to be === to be equal
return false;
}
if (!(_.isObject(a) && _.isObject(b))) {
return false;
}
if (a instanceof Date && b instanceof Date) {
return a.valueOf() === b.valueOf();
}
if (this.isBinary(a) && this.isBinary(b)) {
if (a.length !== b.length) {
return false;
}
for (i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
if (_.isFunction(a.equals)) {
return a.equals(b);
}
if (_.isArray(a)) {
if (!_.isArray(b)) {
return false;
}
if (a.length !== b.length) {
return false;
}
for (i = 0; i < a.length; i++) {
if (!that.equals(a[i], b[i], keyOrderSensitive)) {
return false;
}
}
return true;
}
// fall back to structural equality of objects
var ret;
if (keyOrderSensitive) {
var bKeys = [];
_.each(b, function (val, x) {
bKeys.push(x);
});
i = 0;
ret = _.all(a, function (val, x) {
if (i >= bKeys.length) {
return false;
}
if (x !== bKeys[i]) {
return false;
}
if (!that.equals(val, b[bKeys[i]], keyOrderSensitive)) {
return false;
}
i++;
return true;
});
return ret && i === bKeys.length;
} else {
i = 0;
ret = _.all(a, function (val, key) {
if (!_.has(b, key)) {
return false;
}
if (!that.equals(val, b[key], keyOrderSensitive)) {
return false;
}
i++;
return true;
});
return ret && _.size(b) === i;
}
},
/**
* compare two values of unknown type according to BSON ordering
* semantics. (as an extension, consider 'undefined' to be less than
* any other value.) return negative if a is less, positive if b is
* less, or 0 if equal
*
* @param a
* @param b
* @returns {*}
* @private
*/
_cmp: function (a, b) {
if (a === undefined) {
return b === undefined ? 0 : -1;
}
if (b === undefined) {
return 1;
}
var i = 0;
var ta = this.detectType(a);
var tb = this.detectType(b);
var oa = this.typeOrder(ta);
var ob = this.typeOrder(tb);
if (oa !== ob) {
return oa < ob ? -1 : 1;
}
if (ta !== tb) {
throw new Error('Missing type coercion logic in _cmp');
}
if (ta === 7) { // ObjectID
// Convert to string.
ta = tb = 2;
a = a.toHexString();
b = b.toHexString();
}
if (ta === Bikini.DATA.TYPE.DATE) {
// Convert to millis.
ta = tb = 1;
a = a.getTime();
b = b.getTime();
}
if (ta === Bikini.DATA.TYPE.FLOAT) {
return a - b;
}
if (tb === Bikini.DATA.TYPE.STRING) {
return a < b ? -1 : (a === b ? 0 : 1);
}
if (ta === Bikini.DATA.TYPE.OBJECT) {
// this could be much more efficient in the expected case ...
var toArray = function (obj) {
var ret = [];
for (var key in obj) {
ret.push(key);
ret.push(obj[key]);
}
return ret;
};
return this._cmp(toArray(a), toArray(b));
}
if (ta === Bikini.DATA.TYPE.ARRAY) { // Array
for (i = 0; ; i++) {
if (i === a.length) {
return (i === b.length) ? 0 : -1;
}
if (i === b.length) {
return 1;
}
var s = this._cmp(a[i], b[i]);
if (s !== 0) {
return s;
}
}
}
if (ta === Bikini.DATA.TYPE.BINARY) {
if (a.length !== b.length) {
return a.length - b.length;
}
for (i = 0; i < a.length; i++) {
if (a[i] < b[i]) {
return -1;
}
if (a[i] > b[i]) {
return 1;
}
}
return 0;
}
if (ta === Bikini.DATA.TYPE.BOOLEAN) {
if (a) {
return b ? 0 : 1;
}
return b ? -1 : 0;
}
if (ta === Bikini.DATA.TYPE.NULL) {
return 0;
}
// if( ta === Bikini.DATA.TYPE.REGEXP ) {
// throw Error("Sorting not supported on regular expression");
// } // XXX
// if( ta === 13 ) // javascript code
// {
// throw Error("Sorting not supported on Javascript code");
// } // XXX
throw new Error('Unknown type to sort');
}
});
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
/**
*
* @module Bikini.Model
*
* @type {*}
* @extends Backbone.Model
*/
Bikini.Model = Backbone.Model.extend({
constructor: function( attributes, options ) {
this.init(attributes, options);
Backbone.Model.apply(this, arguments);
}
});
Bikini.Model.create = Bikini.create;
Bikini.Model.design = Bikini.design;
_.extend(Bikini.Model.prototype, Bikini.Object, {
_type: 'Bikini.Model',
isModel: YES,
entity: null,
defaults: {},
changedSinceSync: {},
logon: Bikini.Security.logon,
init: function( attributes, options ) {
options = options || {};
this.collection = options.collection || this.collection;
this.idAttribute = options.idAttribute || this.idAttribute;
this.store = this.store || (this.collection ? this.collection.store : null) || options.store;
if( this.store && _.isFunction(this.store.initModel) ) {
this.store.initModel(this, options);
}
this.entity = this.entity || (this.collection ? this.collection.entity : null) || options.entity;
if( this.entity ) {
this.entity = Bikini.Entity.from(this.entity, { model: this.constructor, typeMapping: options.typeMapping });
this.idAttribute = this.entity.idAttribute || this.idAttribute;
}
this.credentials = this.credentials || (this.collection ? this.collection.credentials : null) || options.credentials;
this.on('change', this.onChange, this);
this.on('sync', this.onSync, this);
},
sync: function( method, model, options ) {
options = options || {};
options.credentials = options.credentials || this.credentials;
var store = (options.store ? options.store : null) || this.store;
var that = this;
var args = arguments;
this.logon(options, function( result ) {
if( store && _.isFunction(store.sync) ) {
return store.sync.apply(that, args);
} else {
return Backbone.sync.apply(that, args);
}
});
},
onChange: function( model, options ) {
// For each `set` attribute, update or delete the current value.
var attrs = model.changedAttributes();
if( _.isObject(attrs) ) {
for( var key in attrs ) {
this.changedSinceSync[key] = attrs[key];
}
}
},
onSync: function( model, options ) {
this.changedSinceSync = {};
},
getUrlRoot: function() {
if( this.urlRoot ) {
return _.isFunction(this.urlRoot) ? this.urlRoot() : this.urlRoot;
} else if( this.collection ) {
return this.collection.getUrlRoot();
} else if( this.url ) {
var url = _.isFunction(this.url) ? this.url() : this.url;
if( url && this.id && url.indexOf(this.id) > 0 ) {
return url.substr(0, url.indexOf(this.id));
}
return url;
}
},
toJSON: function( options ) {
options = options || {};
var entity = options.entity || this.entity;
if( Bikini.isEntity(entity) ) {
return entity.fromAttributes(options.attrs || this.attributes);
}
return options.attrs || _.clone(this.attributes);
},
parse: function( resp, options ) {
options = options || {};
var entity = options.entity || this.entity;
if( Bikini.isEntity(entity) ) {
return entity.toAttributes(resp);
}
return resp;
}
});
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
/**
*
* @module Bikini.Security
*
* @type {{logon: Function, logonBasicAuth: Function, logonMcapAuth: Function, getHost: Function}}
*/
Bikini.Security = Bikini.Object.design({
logon: function (options, callback) {
var credentials = options ? options.credentials : null;
if (credentials) {
switch (credentials.type) {
case 'basic':
return this.logonBasicAuth(options, callback);
}
}
this.handleCallback(callback);
},
logonBasicAuth: function (options, callback) {
var credentials = options.credentials;
options.beforeSend = function (xhr) {
Bikini.Security.setBasicAuth(xhr, credentials);
};
this.handleCallback(callback);
},
setBasicAuth: function( xhr, credentials ) {
if( credentials && credentials.username && xhr && Bikini.Base64 ) {
var basicAuth = Bikini.Base64.encode(encodeURIComponent(credentials.username + ':' + (credentials.password || '')));
xhr.setRequestHeader('Authorization', 'Basic ' + basicAuth);
}
}
});
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
/**
*
* @module Bikini.SqlSelector
*
* @type {*}
* @extends Bikini.DataSelector
*/
Bikini.SqlSelector = Bikini.DataSelector.design({
_type: 'Bikini.SqlSelector',
_selector: null,
_query: null,
_entity: null,
create: function (docSelector, entity) {
var selector = this.extend({
_entity: entity,
_selector: null,
_query: null
});
selector.init(docSelector);
return selector;
},
init: function (docSelector) {
this._selector = this.compileSelector(docSelector);
this._query = this.buildSqlQuery(docSelector);
},
buildStatement: function (obj) {
return this._query;
},
buildSqlQuery: function (selector, connector) {
// you can pass a literal function instead of a selector
if (selector instanceof Function) {
return '';
}
// shorthand -- sql
if (_.isString(selector)) {
return selector;
}
// protect against dangerous selectors. falsey and {_id: falsey} are both
// likely programmer error, and not what you want, particularly for
// destructive operations.
if (!selector || (('_id' in selector) && !selector._id)) {
return '1=2';
}
// Top level can't be an array or true or binary.
if (_.isBoolean(selector) || _.isArray(selector) || Bikini.DataField.isBinary(selector)) {
throw new Error('Invalid selector: ' + selector);
}
return this.buildSqlWhere(selector)();
},
// The main compilation function for a given selector.
buildSqlWhere: function (docSelector) {
var where = '';
var that = this;
var perKeySelectors = [];
_.each(docSelector, function (subSelector, key) {
if (key.substr(0, 1) === '$') {
// Outer operators are either logical operators (they recurse back into
// this function), or $where.
perKeySelectors.push(that.buildLogicalOperator(key, subSelector));
} else {
var valueLookup = that.buildLookup(key);
var valueSelector = that.buildValueSelector(subSelector);
if (_.isFunction(valueSelector)) {
perKeySelectors.push(function () {
return valueSelector(valueLookup);
});
}
}
});
return function () {
var sql = '';
_.each(perKeySelectors, function (f) {
if (_.isFunction(f)) {
sql += f.call(that);
}
});
return sql;
};
},
buildValueSelector: function (valueSelector) {
var that = this;
if (valueSelector === null) { // undefined or null
return function (key) {
return key + ' IS NULL';
};
}
// Selector is a non-null primitive (and not an array or RegExp either).
if (!_.isObject(valueSelector)) {
return function (key) {
return key + ' = ' + that.buildValue(valueSelector);
};
}
if (_.isRegExp(valueSelector)) {
var regEx = valueSelector.toString();
var match = regEx.match(/\/[\^]?([^^.*$'+()]*)[\$]?\//);
if (match && match.length > 1) {
var prefix = regEx.indexOf('/^') < 0 ? '%' : '';
var suffix = regEx.indexOf('$/') < 0 ? '%' : '';
return function (key) {
return key + ' LIKE "' + prefix + match[1] + suffix + '"';
};
}
return null;
}
// Arrays match either identical arrays or arrays that contain it as a value.
if (_.isArray(valueSelector)) {
return null;
}
// It's an object, but not an array or regexp.
if (this.hasOperators(valueSelector)) {
var operatorFunctions = [];
_.each(valueSelector, function (operand, operator) {
if (!_.has(that.VALUE_OPERATORS, operator)) {
throw new Error('Unrecognized operator: ' + operator);
}
operatorFunctions.push(that.VALUE_OPERATORS[operator](operand, that));
});
return function (key) {
return that.LOGICAL_OPERATORS.$and(operatorFunctions, key);
};
}
// It's a literal; compare value (or element of value array) directly to the
// selector.
return function (key) {
return key + ' = ' + that.buildValue(valueSelector);
};
},
buildLookup: function (key) {
var field = this._entity ? this._entity.getField(key) : null;
key = field && field.name ? field.name : key;
return '"' + key + '"';
},
buildValue: function (value) {
if (_.isString(value)) {
return '"' + value.replace(/"/g, '""') + '"';
}
return value;
},
buildLogicalOperator: function (operator, subSelector) {
if (!_.has(this.LOGICAL_OPERATORS, operator)) {
throw new Error('Unrecognized logical operator: ' + operator);
} else {
if (!_.isArray(subSelector) || _.isEmpty(subSelector)) {
throw new Error('$and/$or/$nor must be nonempty array');
}
var subSelectorFunction = _.map(subSelector, this.buildSqlWhere, this);
var that = this;
return function (key) {
return that.LOGICAL_OPERATORS[operator](subSelectorFunction, key);
};
}
},
LOGICAL_OPERATORS: {
'$and': function (subSelectorFunction, key) {
var sql = '';
var count = 0;
_.each(subSelectorFunction, function (f) {
var s = f !== null ? f(key) : '';
if (s) {
count++;
sql += sql ? ' AND ' + s : s;
}
});
return count > 1 ? '( ' + sql + ' )' : sql;
},
'$or': function (subSelectorFunction, key) {
var sql = '';
var miss = false;
_.each(subSelectorFunction, function (f) {
var s = f !== null ? f(key) : '';
miss |= !s;
sql += sql && s ? ' OR ' + s : s;
});
return miss ? '' : '( ' + sql + ' )';
},
'$nor': function (subSelectorFunction, key) {
var sql = '';
var miss = false;
_.each(subSelectorFunction, function (f) {
var s = f !== null ? f(key) : '';
miss |= !s;
sql += sql && s ? ' OR ' + s : s;
});
return miss ? '' : 'NOT ( ' + sql + ' )';
}
},
VALUE_OPERATORS: {
'$in': function (operand) {
return null;
},
'$all': function (operand) {
return null;
},
'$lt': function (operand, that) {
return function (key) {
return key + ' < ' + that.buildValue(operand);
};
},
'$lte': function (operand, that) {
return function (key) {
return key + ' <= ' + that.buildValue(operand);
};
},
'$gt': function (operand, that) {
return function (key) {
return key + ' > ' + that.buildValue(operand);
};
},
'$gte': function (operand, that) {
return function (key) {
return key + '' > '' + that.buildValue(operand);
};
},
'$ne': function (operand, that) {
return function (key) {
return key + ' <> ' + that.buildValue(operand);
};
},
'$nin': function (operand) {
return null;
},
'$exists': function (operand, that) {
return function (key) {
return key + ' IS NOT NULL';
};
},
'$mod': function (operand) {
return null;
},
'$size': function (operand) {
return null;
},
'$type': function (operand) {
return null;
},
'$regex': function (operand, options) {
return null;
},
'$options': function (operand) {
return null;
},
'$elemMatch': function (operand) {
return null;
},
'$not': function (operand, that) {
var matcher = that.buildSqlWhere(operand);
return function (key) {
return 'NOT (' + matcher(key) + ')';
};
}
}
});
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
/**
* The Bikini.BikiniStore is used to connect a model collection to an
* bikini server.
*
* This will give you an online and offline store with live data updates.
*
* @module Bikini.BikiniStore
*
* @type {*}
* @extends Bikini.Store
*
* @example
*
* // The default configuration will save the complete model data as a json,
* // and the offline change log to a local WebSql database, synchronize it
* // trough REST calls with the server and receive live updates via a socket.io connection.
*
* var MyCollection = Bikini.Collection.extend({
* model: MyModel,
* url: 'http://myBikiniServer.com:8200/bikini/myCollection',
* store: new Bikini.BikiniStore( {
* useLocalStore: YES, // (default) store the data for offline use
* useSocketNotify: YES, // (default) register at the server for live updates
* useOfflineChanges: YES // (default) allow changes to the offline data
* })
* });
*
*/
Bikini.BikiniStore = Bikini.Store.extend({
_type: 'Bikini.BikiniStore',
_selector: null,
endpoints: {},
options: null,
localStore: Bikini.WebSqlStore,
useLocalStore: YES,
useSocketNotify: YES,
useOfflineChanges: YES,
isConnected: NO,
typeMapping: {
'binary': 'text',
'date': 'string'
},
initialize: function( options ) {
Bikini.Store.prototype.initialize.apply(this, arguments);
this.options = this.options || {};
this.options.useLocalStore = this.useLocalStore;
this.options.useSocketNotify = this.useSocketNotify;
this.options.useOfflineChanges = this.useOfflineChanges;
this.options.socketPath = this.socketPath;
this.options.localStore = this.localStore;
this.options.typeMapping = this.typeMapping;
if( this.options.useSocketNotify && typeof io !== 'object' ) {
console.log('Socket.IO not present !!');
this.options.useSocketNotify = NO;
}
_.extend(this.options, options || {});
},
initModel: function( model ) {
},
initCollection: function( collection ) {
var url = collection.getUrlRoot();
var entity = this.getEntity(collection.entity);
if( url && entity ) {
var name = entity.name;
var hash = this._hashCode(url);
var credentials = entity.credentials || collection.credentials;
var user = credentials && credentials.username ? credentials.username : '';
var channel = name + user + hash;
collection.channel = channel;
// get or create endpoint for this url
var that = this;
var endpoint = this.endpoints[hash];
if( !endpoint ) {
var href = this.getLocation(url);
endpoint = {};
endpoint.baseUrl = url;
endpoint.readUrl = collection.getUrl();
endpoint.host = href.protocol + '//' + href.host;
endpoint.path = href.pathname;
endpoint.entity = entity;
endpoint.channel = channel;
endpoint.credentials = credentials;
endpoint.socketPath = this.options.socketPath;
endpoint.localStore = this.createLocalStore(endpoint);
endpoint.messages = this.createMsgCollection(endpoint);
endpoint.socket = this.createSocket(endpoint);
endpoint.info = this.fetchServerInfo(endpoint);
that.endpoints[hash] = endpoint;
}
collection.endpoint = endpoint;
collection.listenTo(this, endpoint.channel, this.onMessage, collection);
}
},
getEndpoint: function( url ) {
if( url ) {
var hash = this._hashCode(url);
return this.endpoints[hash];
}
},
createLocalStore: function( endpoint, idAttribute ) {
if( this.options.useLocalStore && endpoint ) {
var entities = {};
entities[endpoint.entity.name] = {
name: endpoint.channel,
idAttribute: idAttribute
};
return this.options.localStore.create({
entities: entities
});
}
},
createMsgCollection: function( endpoint ) {
if( this.options.useOfflineChanges && endpoint ) {
var messages = Bikini.Collection.design({
url: endpoint.url,
entity: 'msg-' + endpoint.channel,
store: this.options.localStore.create()
});
var that = this;
messages.fetch({
success: function() {
that.sendMessages(endpoint);
}
});
return messages;
}
},
createSocket: function( endpoint, name ) {
if( this.options.useSocketNotify && endpoint.socketPath && endpoint ) {
var that = this;
var url = endpoint.host;
var path = endpoint.path;
path = endpoint.socketPath || (path + (path.charAt(path.length - 1) === '/' ? '' : '/' ) + 'live');
// remove leading /
var resource = (path && path.indexOf('/') === 0) ? path.substr(1) : path;
endpoint.socket = io.connect(url, { resource: resource });
endpoint.socket.on('connect', function() {
that._bindChannel(endpoint, name);
that.onConnect(endpoint);
});
endpoint.socket.on('disconnect', function() {
console.log('socket.io: disconnect');
that.onDisconnect(endpoint);
});
return endpoint.socket;
}
},
_bindChannel: function(endpoint, name ) {
var that = this;
if (endpoint && endpoint.socket) {
var channel = endpoint.channel;
var socket = endpoint.socket;
var time = this.getLastMessageTime(channel);
name = name || endpoint.entity.name;
socket.on(channel, function( msg ) {
if( msg ) {
that.trigger(channel, msg);
if (that.options.useLocalStore) {
that.setLastMessageTime(channel, msg.time);
}
}
});
socket.emit('bind', {
entity: name,
channel: channel,
time: time
});
}
},
getLastMessageTime: function( channel ) {
return localStorage.getItem('__' + channel + 'last_msg_time') || 0;
},
setLastMessageTime: function( channel, time ) {
if( time ) {
localStorage.setItem('__' + channel + 'last_msg_time', time);
}
},
_hashCode: function( str ) {
var hash = 0, char;
if( str.length === 0 ) {
return hash;
}
for( var i = 0, l = str.length; i < l; i++ ) {
char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash |= 0; // Convert to 32bit integer
}
return hash;
},
onConnect: function( endpoint ) {
this.isConnected = YES;
this.fetchChanges(endpoint );
this.sendMessages(endpoint );
},
onDisconnect: function(endpoint) {
this.isConnected = NO;
if (endpoint.socket && endpoint.socket.socket) {
endpoint.socket.socket.onDisconnect();
}
},
onMessage: function( msg ) {
if( msg && msg.method ) {
var localStore = this.endpoint ? this.endpoint.localStore : null;
var options = {
store: localStore,
entity: this.entity,
merge: YES,
fromMessage: YES,
parse: YES
};
var attrs = msg.data;
switch( msg.method ) {
case 'patch':
case 'update':
case 'create':
options.patch = msg.method === 'patch';
var model = msg.id ? this.get(msg.id) : null;
if( model ) {
model.save(attrs, options);
} else {
this.create(attrs, options);
}
break;
case 'delete':
if( msg.id ) {
if( msg.id === 'all' ) {
while( (model = this.first()) ) {
if( localStore ) {
localStore.sync.apply(this, [
'delete',
model,
{ store: localStore, fromMessage: YES }
]);
}
this.remove(model);
}
this.store.setLastMessageTime(this.endpoint.channel, '');
} else {
var msgModel = this.get(msg.id);
if( msgModel ) {
msgModel.destroy(options);
}
}
}
break;
default:
break;
}
}
},
sync: function( method, model, options ) {
var that = options.store || this.store;
if( options.fromMessage ) {
return that.handleCallback(options.success);
}
var endpoint = that.getEndpoint(this.getUrlRoot());
if( that && endpoint ) {
var channel = this.channel;
if( Bikini.isModel(model) && !model.id ) {
model.set(model.idAttribute, new Bikini.ObjectID().toHexString());
}
var time = that.getLastMessageTime(channel);
// only send read messages if no other store can do this
// or for initial load
if( method !== 'read' || !endpoint.localStore || !time ) {
// do backbone rest
that.addMessage(method, model, // we don't need to call callbacks if an other store handle this
endpoint.localStore ? {} : options, endpoint);
} else if( method === 'read' ) {
that.fetchChanges(endpoint);
}
if( endpoint.localStore ) {
options.store = endpoint.localStore;
endpoint.localStore.sync.apply(this, arguments);
}
}
},
addMessage: function( method, model, options, endpoint ) {
var that = this;
if( method && model ) {
var changes = model.changedSinceSync;
var data = null;
var storeMsg = YES;
switch( method ) {
case 'update':
case 'create':
data = options.attrs || model.toJSON();
break;
case 'patch':
if( _.isEmpty(changes) ) {
return;
}
data = model.toJSON({ attrs: changes });
break;
case 'delete':
break;
default:
storeMsg = NO;
break;
}
var msg = {
_id: model.id,
id: model.id,
method: method,
data: data
};
var emit = function( endpoint, msg ) {
that.emitMessage(endpoint, msg, options, model);
};
if( storeMsg ) {
this.storeMessage(endpoint, msg, emit);
} else {
emit(endpoint, msg);
}
}
},
emitMessage: function( endpoint, msg, options, model ) {
var channel = endpoint.channel;
var that = this;
var url = Bikini.isModel(model) || msg.method !== 'read' ? endpoint.baseUrl : endpoint.readUrl;
if( msg.id && msg.method !== 'create' ) {
url += '/' + msg.id;
}
model.sync.apply(model, [msg.method, model, {
url: url,
error: function( xhr, status ) {
if( !xhr.responseText && that.options.useOfflineChanges ) {
// this seams to be only a connection problem, so we keep the message an call success
that.onDisconnect(endpoint);
that.handleCallback(options.success, msg.data);
} else {
that.removeMessage(endpoint, msg, function( endpoint, msg ) {
// Todo: revert changed data
that.handleCallback(options.error, status);
});
}
},
success: function( data ) {
if (!that.isConnected) {
that.onConnect(endpoint);
}
that.removeMessage(endpoint, msg, function( endpoint, msg ) {
if( options.success ) {
var resp = data;
that.handleCallback(options.success, resp);
} else {
// that.setLastMessageTime(channel, msg.time);
if( msg.method === 'read' ) {
var array = _.isArray(data) ? data : [ data ];
for( var i = 0; i < array.length; i++ ) {
data = array[i];
if( data ) {
that.trigger(channel, {
id: data._id,
method: 'update',
data: data
});
}
}
} else {
that.trigger(channel, msg);
}
}
});
},
store: {}
}]);
},
fetchChanges: function( endpoint ) {
var that = this;
var channel = endpoint ? endpoint.channel : '';
var time = that.getLastMessageTime(channel);
if( endpoint && endpoint.baseUrl && channel && time ) {
var changes = new Bikini.Collection({});
changes.fetch({
url: endpoint.baseUrl + '/changes/' + time,
success: function() {
changes.each(function( msg ) {
if( msg.time && msg.method ) {
if (that.options.useLocalStore) {
that.setLastMessageTime(channel, msg.time);
}
that.trigger(channel, msg);
}
});
},
credentials: endpoint.credentials
});
}
},
fetchServerInfo: function( endpoint ) {
var that = this;
if( endpoint && endpoint.baseUrl ) {
var info = new Bikini.Model();
var time = that.getLastMessageTime(endpoint.channel);
info.fetch({
url: endpoint.baseUrl + '/info',
success: function() {
if( !time && info.get('time') ) {
that.setLastMessageTime(endpoint.channel, info.get('time'));
}
if( !endpoint.socketPath && info.get('socketPath') ) {
endpoint.socketPath = info.get('socketPath');
var name = info.get('entity') || endpoint.entity.name;
if( that.options.useSocketNotify ) {
that.createSocket(endpoint, name);
}
}
},
credentials: endpoint.credentials
});
}
},
sendMessages: function( endpoint ) {
if( endpoint && endpoint.messages ) {
var that = this;
endpoint.messages.each(function( message ) {
var msg;
try {
msg = JSON.parse(message.get('msg'));
} catch( e ) {
}
var channel = message.get('channel');
if( msg && channel ) {
var model = that.createModel({ collection: endpoint.messages }, msg.data);
that.emitMessage(endpoint, msg, {}, model);
} else {
message.destroy();
}
});
}
},
mergeMessages: function( data, id ) {
return data;
},
storeMessage: function( endpoint, msg, callback ) {
if( endpoint && endpoint.messages && msg ) {
var channel = endpoint.channel;
var message = endpoint.messages.get(msg._id);
if( message ) {
var oldMsg = JSON.parse(message.get('msg'));
message.save({
msg: JSON.stringify(_.extend(oldMsg, msg))
});
} else {
endpoint.messages.create({
_id: msg._id,
id: msg.id,
msg: JSON.stringify(msg),
channel: channel
});
}
}
callback(endpoint, msg);
},
removeMessage: function( endpoint, msg, callback ) {
if( endpoint && endpoint.messages ) {
var message = endpoint.messages.get(msg._id);
if( message ) {
message.destroy();
}
}
callback(endpoint, msg);
},
clear: function( collection ) {
if( collection ) {
var endpoint = this.getEndpoint(collection.getUrlRoot());
if( endpoint ) {
if( endpoint.messages ) {
endpoint.messages.destroy();
}
collection.reset();
this.setLastMessageTime(endpoint.channel, '');
}
}
},
/*
url = "http://example.com:3000/pathname/?search=test#hash";
location.protocol; // => "http:"
location.host; // => "example.com:3000"
location.hostname; // => "example.com"
location.port; // => "3000"
location.pathname; // => "/pathname/"
location.hash; // => "#hash"
location.search; // => "?search=test"
*/
getLocation: function( url ) {
var location = document.createElement('a');
location.href = url || this.url;
// IE doesn't populate all link properties when setting .href with a relative URL,
// however .href will return an absolute URL which then can be used on itself
// to populate these additional fields.
if( location.host === '' ) {
location.href = location.href;
}
return location;
}
});
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
/**
* The Bikini.LocalStorageStore can be used to store model collection into
* the localStorage
*
* @module Bikini.LocalStorageStore
*
* @type {*}
* @extends Bikini.Store
*
* @example
*
* // The LocalStorageStore will save each model data as a json under his id,
* // and keeps all id's under an extra key for faster access
*
* var MyCollection = Bikini.Collection.extend({
* store: Bikini.LocalStorageStore.create(),
* entity: 'myEntityName'
* });
*
*/
Bikini.LocalStorageStore = Bikini.Store.extend({
_type: 'Bikini.LocalStorageStore',
ids: {},
sync: function( method, model, options ) {
options = options || {};
var that = options.store || this.store;
var entity = that.getEntity(model.entity || options.entity || this.entity);
var attrs;
if( that && entity && model ) {
var id = model.id || (method === 'create' ? new Bikini.ObjectID().toHexString() : null);
attrs = options.attrs || model.toJSON(options);
switch( method ) {
case 'patch':
case 'update':
case 'create':
if (method !== 'create') {
attrs = _.extend(that._getItem(entity, id) || {}, attrs);
}
if( model.id !== id && model.idAttribute ) {
attrs[model.idAttribute] = id;
}
that._setItem(entity, id, attrs);
break;
case 'delete' :
that._removeItem(entity, id);
break;
case 'read' :
if( id ) {
attrs = that._getItem(entity, id);
} else {
attrs = [];
var ids = that._getItemIds(entity);
for( id in ids ) {
var itemData = that._getItem(entity, id);
if( itemData ) {
attrs.push(itemData);
}
}
}
break;
default:
return;
}
}
if( attrs ) {
that.handleSuccess(options, attrs);
} else {
that.handleError(options, Bikini.Store.CONST.ERROR_NO_ENTITY);
}
},
drop: function( options ) {
var entity = this.getEntity(options);
if( entity && entity.name ) {
var keys = this._findAllKeys(entity);
for (var i=0; i<keys.length; i++) {
localStorage.removeItem(keys[i]);
}
localStorage.removeItem('__ids__' + entity.name);
this.handleSuccess(options);
} else {
this.handleError(options, Bikini.Store.CONST.ERROR_NO_ENTITY);
}
},
_getKey: function( entity, id ) {
return '_' + entity.name + '_' + id;
},
_getItem: function( entity, id ) {
var attrs;
if( entity && id ) {
try {
attrs = JSON.parse(localStorage.getItem(this._getKey(entity, id)));
if( attrs ) {
entity.setId(attrs, id); // fix id
} else {
this._delItemId(id);
}
} catch( e ) {
console.error(Bikini.Store.CONST.ERROR_LOAD_DATA + e.message);
}
}
return attrs;
},
_setItem: function( entity, id, attrs ) {
if( entity && id && attrs ) {
try {
localStorage.setItem(this._getKey(entity, id), JSON.stringify(attrs));
this._addItemId(entity, id);
} catch( e ) {
console.error(Bikini.Store.CONST.ERROR_SAVE_DATA + e.message);
}
}
},
_removeItem: function( entity, id ) {
if( entity && id ) {
localStorage.removeItem(this._getKey(entity, id));
this._delItemId(entity, id);
}
},
_addItemId: function( entity, id ) {
var ids = this._getItemIds(entity);
if( !(id in ids) ) {
ids[id] = '';
this._saveItemIds(entity, ids);
}
},
_delItemId: function( entity, id ) {
var ids = this._getItemIds(entity);
if( id in ids ) {
delete ids[id];
this._saveItemIds(entity, ids);
}
},
_findAllKeys: function (entity) {
var keys = [];
var prefixItem = this._getKey(entity, '');
if( prefixItem ) {
var key, len = localStorage.length;
for (var i=0; i < len; i++) {
key = localStorage.key(i);
if (key && key === prefixItem) {
keys.push(key);
}
}
}
return keys;
},
_getItemIds: function( entity ) {
try {
var key = '__ids__' + entity.name;
if( !this.ids[entity.name] ) {
this.ids[entity.name] = JSON.parse(localStorage.getItem(key)) || {};
}
return this.ids[entity.name];
} catch( e ) {
console.error(Bikini.Store.CONST.ERROR_LOAD_IDS + e.message);
}
},
_saveItemIds: function( entity, ids ) {
try {
var key = '__ids__' + entity.name;
localStorage.setItem(key, JSON.stringify(ids));
} catch( e ) {
console.error(Bikini.Store.CONST.ERROR_SAVE_IDS + e.message);
}
}
});
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
/**
* Base class to build a custom data store.
*
* See: Bikini.LocalStorageStore, Bikini.WebSqlStore and Bikini.BikiniStore
*
* @module Bikini.Store
*
*/
Bikini.Store = function() {
this.initialize.apply(this, arguments);
};
Bikini.Store.extend = Bikini.extend;
Bikini.Store.create = Bikini.create;
Bikini.Store.design = Bikini.design;
// Attach all inheritable methods to the Connector prototype.
_.extend(Bikini.Store.prototype, Backbone.Events, Bikini.Object, {
_type: 'Bikini.Store',
entities: null,
options: null,
name: '',
typeMapping: (function() {
var map = {};
map [Bikini.DATA.TYPE.OBJECTID] = Bikini.DATA.TYPE.STRING;
map [Bikini.DATA.TYPE.DATE] = Bikini.DATA.TYPE.STRING;
map [Bikini.DATA.TYPE.BINARY] = Bikini.DATA.TYPE.TEXT;
return map;
})(),
initialize: function( options ) {
options = options || {};
this.options = this.options || {};
this.options.name = this.name;
this.options.typeMapping = this.typeMapping;
this.options.entities = this.entities;
_.extend(this.options, options || {});
this._setEntities(options.entities || {});
},
_setEntities: function( entities ) {
this.entities = {};
for( var name in entities ) {
var entity = Bikini.Entity.from(entities[name], {
store: this,
typeMapping: this.options.typeMapping
});
entity.name = entity.name || name;
// connect collection and model to this store
var collection = entity.collection || Bikini.Collection.extend({ model: Bikini.Model.extend({}) });
var model = collection.prototype.model;
// set new entity and name
collection.prototype.entity = model.prototype.entity = name;
collection.prototype.store = model.prototype.store = this;
entity.idAttribute = entity.idAttribute || model.prototype.idAttribute;
this.entities[name] = entity;
}
},
getEntity: function( obj ) {
if( obj ) {
var entity = obj.entity || obj;
var name = _.isString(entity) ? entity : entity.name;
if( name ) {
return this.entities[name] || (entity && entity.name ? entity : { name: name });
}
}
},
getCollection: function( entity ) {
if( _.isString(entity) ) {
entity = this.entities[entity];
}
if( entity && entity.collection ) {
if( Bikini.Collection.prototype.isPrototypeOf(entity.collection) ) {
return entity.collection;
} else {
return new entity.collection();
}
}
},
createModel: function( entity, attrs ) {
if( _.isString(entity) ) {
entity = this.entities[entity];
}
if( entity && entity.collection ) {
var Model = entity.collection.model || entity.collection.prototype.model;
if( Model ) {
return new Model(attrs);
}
}
},
getArray: function( data ) {
if( _.isArray(data) ) {
return data;
} else if( Bikini.isCollection(data) ) {
return data.models;
}
return _.isObject(data) ? [ data ] : [];
},
getDataArray: function( data ) {
var array = [];
if( _.isArray(data) || Backbone.Collection.prototype.isPrototypeOf(data) ) {
_.each(data, function( d ) {
var attrs = this.getAttributes(d);
if( attrs ) {
array.push(attrs);
}
});
} else {
var attrs = this.getAttributes(data);
if( attrs ) {
array.push(this.getAttributes(attrs));
}
}
return array;
},
getAttributes: function( model ) {
if( Backbone.Model.prototype.isPrototypeOf(model) ) {
return model.attributes;
}
return _.isObject(model) ? model : null;
},
initModel: function( model ) {
},
initCollection: function( collection ) {
},
initEntity: function( entity ) {
},
sync: function( method, model, options ) {
},
/**
*
* @param collection usally a collection, but can also be a model
* @param options
*/
fetch: function( collection, options ) {
if( collection && !collection.models && !collection.attributes && !options ) {
options = collection;
}
if( (!collection || (!collection.models && !collection.attributes)) && options && options.entity ) {
collection = this.getCollection(options.entity);
}
if( collection && collection.fetch ) {
var opts = _.extend({}, options || {}, { store: this });
collection.fetch(opts);
}
},
create: function( collection, model, options ) {
if( collection && !collection.models && !options ) {
model = collection;
options = model;
}
if( (!collection || !collection.models) && options && options.entity ) {
collection = this.getCollection(options.entity);
}
if( collection && collection.create ) {
var opts = _.extend({}, options || {}, { store: this });
collection.create(model, opts);
}
},
save: function( model, attr, options ) {
if( model && !model.attributes && !options ) {
attr = model;
options = attr;
}
if( (!model || !model.attributes) && options && options.entity ) {
model = this.createModel(options.entity);
}
if( model && model.save ) {
var opts = _.extend({}, options || {}, { store: this });
model.save(attr, opts);
}
},
destroy: function( model, options ) {
if( model && model.destroy ) {
var opts = _.extend({}, options || {}, { store: this });
model.destroy(opts);
}
},
_checkEntity: function( obj, entity ) {
if( !Bikini.isEntity(entity) ) {
var error = Bikini.Store.CONST.ERROR_NO_ENTITY;
console.error(error);
this.handleCallback(obj.error, error);
this.handleCallback(obj.finish, error);
return false;
}
return true;
},
_checkData: function( obj, data ) {
if( (!_.isArray(data) || data.length === 0) && !_.isObject(data) ) {
var error = Bikini.Store.CONST.ERROR_NO_DATA;
console.error(error);
this.handleCallback(obj.error, error);
this.handleCallback(obj.finish, error);
return false;
}
return true;
},
handleSuccess: function( obj ) {
var args = Array.prototype.slice.call(arguments, 1);
if( obj.success ) {
this.handleCallback.apply(this, [ obj.success ].concat(args));
}
if( obj.finish ) {
this.handleCallback.apply(this, [ obj.finish ].concat(args));
}
},
handleError: function( obj ) {
var args = Array.prototype.slice.call(arguments, 1);
if( obj.error ) {
this.handleCallback.apply(this, [ obj.error ].concat(args));
}
if( obj.finish ) {
this.handleCallback.apply(this, [ obj.finish ].concat(args));
}
},
CONST: {
ERROR_NO_ENTITY: 'No valid entity specified. ',
ERROR_NO_DATA: 'No data passed. ',
ERROR_LOAD_DATA: 'Error while loading data from store. ',
ERROR_SAVE_DATA: 'Error while saving data to the store. ',
ERROR_LOAD_IDS: 'Error while loading ids from store. ',
ERROR_SAVE_IDS: 'Error while saving ids to the store. '
}
});
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
/**
* The Bikini.WebSqlStore can be used to store model collection into
* the webSql database
*
* @module Bikini.WebSqlStore
*
* @type {*}
* @extends Bikini.Store
*
* @example
*
* // The default configuration will save the complete model data as json
* // into a database column with the name "data"
*
* var MyCollection = Bikini.Collection.extend({
* model: MyModel,
* entity: 'MyTableName',
* store: new Bikini.WebSqlStorageStore()
* });
*
* // If you want to use specific columns you can specify the fields
* // in the entity of your model like this:
*
* var MyModel = Bikini.Model.extend({
* idAttribute: 'id',
* fields: {
* id: { type: Bikini.DATA.TYPE.STRING, required: YES, index: YES },
* sureName: { name: 'USERNAME', type: Bikini.DATA.TYPE.STRING },
* firstName: { type: Bikini.DATA.TYPE.STRING, length: 200 },
* age: { type: Bikini.DATA.TYPE.INTEGER }
* }
* });
*
*
*/
Bikini.WebSqlStore = Bikini.Store.extend({
_type: 'Bikini.WebSqlStore',
_selector: null,
options: null,
name: 'themproject',
size: 1024 * 1024, // 1 MB
version: '1.0',
db: null,
dataField: { name: 'data', type: 'text', required: true },
idField: { name: 'id', type: 'string', required: true },
typeMapping: (function() {
var map = {};
map [Bikini.DATA.TYPE.OBJECTID] = Bikini.DATA.TYPE.STRING;
map [Bikini.DATA.TYPE.DATE] = Bikini.DATA.TYPE.STRING;
map [Bikini.DATA.TYPE.OBJECT] = Bikini.DATA.TYPE.TEXT;
map [Bikini.DATA.TYPE.ARRAY] = Bikini.DATA.TYPE.TEXT;
map [Bikini.DATA.TYPE.BINARY] = Bikini.DATA.TYPE.TEXT;
return map;
})(),
sqlTypeMapping: (function() {
var map = {};
map [Bikini.DATA.TYPE.STRING] = 'varchar(255)';
map [Bikini.DATA.TYPE.TEXT] = 'text';
map [Bikini.DATA.TYPE.OBJECT] = 'text';
map [Bikini.DATA.TYPE.ARRAY] = 'text';
map [Bikini.DATA.TYPE.FLOAT] = 'float';
map [Bikini.DATA.TYPE.INTEGER] = 'integer';
map [Bikini.DATA.TYPE.DATE] = 'varchar(255)';
map [Bikini.DATA.TYPE.BOOLEAN] = 'boolean';
return map;
})(),
initialize: function( options ) {
Bikini.Store.prototype.initialize.apply(this, arguments);
this.options = this.options || {};
this.options.name = this.name;
this.options.size = this.size;
this.options.version = this.version;
this.options.typeMapping = this.typeMapping;
this.options.sqlTypeMapping = this.sqlTypeMapping;
_.extend(this.options, options || {});
this._openDb({
error: function( msg ) {
console.error(msg);
}
});
},
sync: function( method, model, options ) {
var that = options.store || this.store;
var models = Bikini.isCollection(model) ? model.models : [ model ];
options.entity = options.entity || this.entity;
switch( method ) {
case 'create':
that._checkTable(options, function() {
that._insertOrReplace(models, options);
});
break;
case 'update':
case 'patch':
that._checkTable(options, function() {
that._insertOrReplace(models, options);
});
break;
case 'delete':
that._delete(models, options);
break;
case 'read':
that._select(this, options);
break;
default:
break;
}
},
select: function( options ) {
this._select(null, options);
},
drop: function( options ) {
this._dropTable(options);
},
createTable: function( options ) {
this._createTable(options);
},
execute: function( options ) {
this._executeSql(options);
},
/**
* @private
*/
_openDb: function( options ) {
var error, dbError;
/* openDatabase(db_name, version, description, estimated_size, callback) */
if( !this.db ) {
try {
if( !window.openDatabase ) {
error = 'Your browser does not support WebSQL databases.';
} else {
this.db = window.openDatabase(this.options.name, '', '', this.options.size);
if( this.entities ) {
for( var key in this.entities ) {
this._createTable({ entity: this.entities[key] });
}
}
}
} catch( e ) {
dbError = e;
}
}
if( this.db ) {
if( this.options.version && this.db.version !== this.options.version ) {
this._updateDb(options);
} else {
this.handleSuccess(options, this.db);
}
} else if( dbError === 2 || dbError === '2') {
// Version number mismatch.
this._updateDb(options);
} else {
if( !error && dbError ) {
error = dbError;
}
this.handleSuccess(options, error);
}
},
_updateDb: function( options ) {
var error;
var lastSql;
var that = this;
try {
var db = window.openDatabase(this.options.name, '', '', this.options.size);
try {
var arSql = this._sqlUpdateDatabase(db.version, this.options.version);
db.changeVersion(db.version, this.options.version, function( tx ) {
_.each(arSql, function( sql ) {
console.log('sql statement: ' + sql);
lastSql = sql;
tx.executeSql(sql);
});
}, function( msg ) {
that.handleError(options, msg, lastSql);
}, function() {
that.handleSuccess(options);
});
} catch( e ) {
error = e.message;
console.error('webSql change version failed, DB-Version: ' + db.version);
}
} catch( e ) {
error = e.message;
}
if( error ) {
this.handleError(options, error);
}
},
_sqlUpdateDatabase: function( oldVersion, newVersion ) {
// create sql array, simply drop and create the database
var sql = [];
if( this.entities ) {
for( var name in this.entities ) {
var entity = this.entities[name];
sql.push(this._sqlDropTable(entity.name));
sql.push(this._sqlCreateTable(entity));
}
}
return sql;
},
_sqlDropTable: function( name ) {
return 'DROP TABLE IF EXISTS \'' + name + '\'';
},
_isAutoincrementKey: function( entity, key ) {
if( entity && key ) {
var column = this.getField(entity, key);
return column && column.type === Bikini.DATA.TYPE.INTEGER;
}
},
_sqlPrimaryKey: function( entity, keys ) {
if( keys && keys.length === 1 ) {
if( this._isAutoincrementKey(entity, keys[0]) ) {
return keys[0] + ' INTEGER PRIMARY KEY ASC AUTOINCREMENT UNIQUE';
} else {
return keys[0] + ' PRIMARY KEY ASC UNIQUE';
}
}
return '';
},
_sqlConstraint: function( entity, keys ) {
if( keys && keys.length > 1 ) {
return 'PRIMARY KEY (' + keys.join(',') + ') ON CONFLICT REPLACE';
}
return '';
},
_sqlCreateTable: function( entity ) {
var that = this;
var keys = entity.getKeys();
var primaryKey = keys.length === 1 ? this._sqlPrimaryKey(entity, keys) : '';
var constraint = keys.length > 1 ? this._sqlConstraint(entity, keys) : (entity.constraint || '');
var columns = '';
var fields = this.getFields(entity);
_.each(fields, function( field ) {
// skip ID, it is defined manually above
if( !primaryKey || field.name !== keys[0] ) {
// only add valid types
var attr = that._dbAttribute(field);
if( attr ) {
columns += (columns ? ', ' : '') + attr;
}
}
});
if( !columns ) {
columns = this._dbAttribute(this.dataField);
}
var sql = 'CREATE TABLE IF NOT EXISTS \'' + entity.name + '\' (';
sql += primaryKey ? primaryKey + ', ' : '';
sql += columns;
sql += constraint ? ', ' + constraint : '';
sql += ');';
return sql;
},
_sqlDelete: function(options, entity ) {
var sql = 'DELETE FROM \'' + entity.name + '\'';
var where = this._sqlWhere(options, entity) || this._sqlWhereFromData(options, entity);
if( where ) {
sql += ' WHERE ' + where;
}
sql += options.and ? ' AND ' + options.and : '';
return sql;
},
_sqlWhere: function( options, entity ) {
this._selector = null;
var sql = '';
if( _.isString(options.where) ) {
sql = options.where;
} else if( _.isObject(options.where) ) {
this._selector = Bikini.SqlSelector.create(options.where, entity);
sql = this._selector.buildStatement();
}
return sql;
},
_sqlWhereFromData: function(options, entity ) {
var that = this;
var ids = [];
if( options && options.models && entity && entity.idAttribute ) {
var id, key = entity.idAttribute;
var field = this.getField(entity, key);
_.each(options.models, function( model ) {
id = model.id;
if( !_.isUndefined(id) ) {
ids.push(that._sqlValue(id, field));
}
});
if( ids.length > 0 ) {
return key + ' IN (' + ids.join(',') + ')';
}
}
return '';
},
_sqlSelect: function( options, entity ) {
var sql = 'SELECT ';
if( options.fields ) {
if( options.fields.length > 1 ) {
sql += options.fields.join(', ');
} else if( options.fields.length === 1 ) {
sql += options.fields[0];
}
} else {
sql += '*';
}
sql += ' FROM \'' + entity.name + '\'';
if( options.join ) {
sql += ' JOIN ' + options.join;
}
if( options.leftJoin ) {
sql += ' LEFT JOIN ' + options.leftJoin;
}
var where = this._sqlWhere(options, entity) || this._sqlWhereFromData(options, entity);
if( where ) {
sql += ' WHERE ' + where;
}
if( options.order ) {
sql += ' ORDER BY ' + options.order;
}
if( options.limit ) {
sql += ' LIMIT ' + options.limit;
}
if( options.offset ) {
sql += ' OFFSET ' + options.offset;
}
return sql;
},
_sqlValue: function( value, field ) {
var type = field && field.type ? field.type : Bikini.Field.prototype.detectType(value);
if( type === Bikini.DATA.TYPE.INTEGER || type === Bikini.DATA.TYPE.FLOAT ) {
return value;
} else if( type === Bikini.DATA.TYPE.BOOLEAN ) {
return value ? '1' : '0';
} else if( type === Bikini.DATA.TYPE.NULL ) {
return 'NULL';
}
value = Bikini.Field.prototype.transform(value, Bikini.DATA.TYPE.STRING);
value = value.replace(/"/g, '""');
return '"' + value + '"';
},
_dbAttribute: function( field ) {
if( field && field.name ) {
var type = this.options.sqlTypeMapping[field.type];
var isReqStr = field.required ? ' NOT NULL' : '';
if( type ) {
return field.name + ' ' + type.toUpperCase() + isReqStr;
}
}
},
_dropTable: function( options ) {
var entity = this.getEntity(options);
entity.db = null;
if( this._checkDb(options) && entity ) {
var sql = this._sqlDropTable(entity.name);
// reset flag
this._executeTransaction(options, [sql]);
}
},
_createTable: function( options ) {
var entity = this.getEntity(options);
entity.db = this.db;
if( this._checkDb(options) && this._checkEntity(options, entity) ) {
var sql = this._sqlCreateTable(entity);
// reset flag
this._executeTransaction(options, [sql]);
}
},
_checkTable: function( options, callback ) {
var entity = this.getEntity(options);
var that = this;
if( entity && !entity.db ) {
this._createTable({
success: function() {
callback();
},
error: function( error ) {
this.handleError(options, error);
},
entity: entity
});
} else {
callback();
}
},
_insertOrReplace: function( models, options ) {
var entity = this.getEntity(options);
if( this._checkDb(options) && this._checkEntity(options, entity) && this._checkData(options, models) ) {
var isAutoInc = this._isAutoincrementKey(entity, entity.getKey());
var statements = [];
var sqlTemplate = 'INSERT OR REPLACE INTO \'' + entity.name + '\' (';
for( var i = 0; i < models.length; i++ ) {
var model = models[i];
var statement = ''; // the actual sql insert string with values
if( !isAutoInc && !model.id && model.idAttribute ) {
model.set(model.idAttribute, new Bikini.ObjectID().toHexString());
}
var value = options.attrs || model.toJSON();
var args, keys;
if( !_.isEmpty(entity.fields) ) {
args = _.values(value);
keys = _.keys(value);
} else {
args = [ model.id, JSON.stringify(value) ];
keys = [ 'id', 'data'];
}
if( args.length > 0 ) {
var values = new Array(args.length).join('?,') + '?';
var columns = '\'' + keys.join('\',\'') + '\'';
statement += sqlTemplate + columns + ') VALUES (' + values + ');';
statements.push({ statement: statement, arguments: args });
}
}
this._executeTransaction(options, statements);
}
},
_select: function( result, options ) {
var entity = this.getEntity(options);
if( this._checkDb(options) && this._checkEntity(options, entity) ) {
var lastStatement;
var isCollection = Bikini.isCollection(result);
if( isCollection ) {
result = [];
} else {
options.models = [ result ];
}
var stm = this._sqlSelect(options, entity);
var that = this;
this.db.readTransaction(function( t ) {
var statement = stm.statement || stm;
var args = stm.arguments;
lastStatement = statement;
console.log('sql statement: ' + statement);
if( args ) {
console.log(' arguments: ' + JSON.stringify(args));
}
t.executeSql(statement, args, function( tx, res ) {
var len = res.rows.length;//, i;
for( var i = 0; i < len; i++ ) {
var item = res.rows.item(i);
var attrs;
if( !_.isEmpty(entity.fields) || !that._hasDefaultFields(item) ) {
attrs = item;
} else {
try {
attrs = JSON.parse(item.data);
} catch( e ) {
}
}
if( attrs && (!that._selector || that._selector.matches(attrs)) ) {
if( isCollection ) {
result.push(attrs);
} else {
result = attrs;
break;
}
}
}
}, function (t, e) {
// error
console.error('webSql error: ' + e.message);
});
}, function( sqlError ) { // errorCallback
console.error('WebSql Syntax Error: ' + sqlError.message);
that.handleError(options, sqlError.message, lastStatement);
}, function() { // voidCallback (success)
that.handleSuccess(options, result);
});
}
},
_delete: function( models, options ) {
var entity = this.getEntity(options);
if( this._checkDb(options) && this._checkEntity(options, entity) ) {
options.models = models;
var sql = this._sqlDelete(options, entity);
// reset flag
this._executeTransaction(options, [sql]);
}
},
_executeSql: function( options ) {
if( options.sql ) {
this._executeTransaction(options, [options.sql]);
}
},
_executeTransaction: function( options, statements ) {
var error;
var lastStatement;
if( this._checkDb(options) ) {
var that = this;
try {
/* transaction has 3 parameters: the transaction callback, the error callback and the success callback */
this.db.transaction(function( t ) {
_.each(statements, function( stm ) {
var statement = stm.statement || stm;
var args = stm.arguments;
lastStatement = statement;
console.log('sql statement: ' + statement);
if( args ) {
console.log(' arguments: ' + JSON.stringify(args));
}
t.executeSql(statement, args);
});
}, function( sqlError ) { // errorCallback
console.error(sqlError.message);
that.handleError(options, sqlError.message, lastStatement);
}, function() {
that.handleSuccess(options);
});
} catch( e ) {
console.error(e.message);
}
}
if( error ) {
this.handleCallback(options.error, error, lastStatement);
}
},
_hasDefaultFields: function( item ) {
return _.every(_.keys(item), function( key ) {
return key === this.idField.name || key === this.dataField.name;
}, this);
},
_checkDb: function( options ) {
// has to be initialized first
if( !this.db ) {
var error = 'db handler not initialized.';
console.error(error);
this.handleError(options, error);
return false;
}
return true;
},
getFields: function( entity ) {
if( !_.isEmpty(entity.fields) ) {
return entity.fields;
} else {
var fields = {};
fields.data = this.dataField;
var idAttribute = entity.idAttribute || 'id';
fields[idAttribute] = this.idField;
return fields;
}
},
getField: function( entity, key ) {
return this.getFields(entity)[key];
}
});
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
/**
* This prototype defines decoding and encoding mechanisms based on the Base64 algorithm. You
* normally don't call this object respectively its methods directly, but let Bikini.Cypher handle
* this.
* @module Bikini.Base64
*
* @extends Bikini.Object
*/
Bikini.Base64 = Bikini.Object.design(/** @scope Bikini.Base64.prototype */ {
/**
* The type of this object.
*
* @type String
*/
type: 'Bikini.Base64',
/**
* The key string for the base 64 decoding and encoding.
*
* @type String
*/
_keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
/**
* This method encodes a given binary input, using the base64 encoding.
*
* @param {String} input The binary to be encoded. (e.g. an requested image)
* @returns {String} The base64 encoded string.
*/
encodeBinary: function( input ) {
var output = '';
var bytebuffer;
var encodedCharIndexes = new Array(4);
var inx = 0;
var paddingBytes = 0;
while( inx < input.length ) {
// Fill byte buffer array
bytebuffer = new Array(3);
for( var jnx = 0; jnx < bytebuffer.length; jnx++ ) {
if( inx < input.length ) {
bytebuffer[jnx] = input.charCodeAt(inx++) & 0xff;
} // throw away high-order byte, as documented at: https://developer.mozilla.org/En/Using_XMLHttpRequest#Handling_binary_data
else {
bytebuffer[jnx] = 0;
}
}
// Get each encoded character, 6 bits at a time
// index 1: first 6 bits
encodedCharIndexes[0] = bytebuffer[0] >> 2;
// index 2: second 6 bits (2 least significant bits from input byte 1 + 4 most significant bits from byte 2)
encodedCharIndexes[1] = ((bytebuffer[0] & 0x3) << 4) | (bytebuffer[1] >> 4);
// index 3: third 6 bits (4 least significant bits from input byte 2 + 2 most significant bits from byte 3)
encodedCharIndexes[2] = ((bytebuffer[1] & 0x0f) << 2) | (bytebuffer[2] >> 6);
// index 3: forth 6 bits (6 least significant bits from input byte 3)
encodedCharIndexes[3] = bytebuffer[2] & 0x3f;
// Determine whether padding happened, and adjust accordingly
paddingBytes = inx - (input.length - 1);
switch( paddingBytes ) {
case 2:
// Set last 2 characters to padding char
encodedCharIndexes[3] = 64;
encodedCharIndexes[2] = 64;
break;
case 1:
// Set last character to padding char
encodedCharIndexes[3] = 64;
break;
default:
break; // No padding - proceed
}
// Now we will grab each appropriate character out of our keystring
// based on our index array and append it to the output string
for( jnx = 0; jnx < encodedCharIndexes.length; jnx++ ) {
output += this._keyStr.charAt(encodedCharIndexes[jnx]);
}
}
return output;
},
/**
* This method encodes a given input string, using the base64 encoding.
*
* @param {String} input The string to be encoded.
* @returns {String} The base64 encoded string.
*/
encode: function( input ) {
var output = '';
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
input = Bikini.Cypher.utf8Encode(input);
while( i < input.length ) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if( isNaN(chr2) ) {
enc3 = enc4 = 64;
} else if( isNaN(chr3) ) {
enc4 = 64;
}
output += this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
}
return output;
},
binaryEncode: function( input ) {
var output = '';
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
while( i < input.length ) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if( isNaN(chr2) ) {
enc3 = enc4 = 64;
} else if( isNaN(chr3) ) {
enc4 = 64;
}
output += this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
}
return output;
},
/**
* This method decodes a given input string, using the base64 decoding.
*
* @param {String} input The string to be decoded.
* @returns {String} The base64 decoded string.
*/
decode: function( input ) {
var output = '';
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, '');
while( i < input.length ) {
enc1 = this._keyStr.indexOf(input.charAt(i++));
enc2 = this._keyStr.indexOf(input.charAt(i++));
enc3 = this._keyStr.indexOf(input.charAt(i++));
enc4 = this._keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if( enc3 !== 64 ) {
output = output + String.fromCharCode(chr2);
}
if( enc4 !== 64 ) {
output = output + String.fromCharCode(chr3);
}
}
return Bikini.Cypher.utf8Decode(output);
}
});
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
/**
* Bikini.Cypher defines a prototype for handling decoding, encoding and hashing of string
* based values.
* @module Bikini.Cypher
*
* @extends Bikini.Object
*/
Bikini.Cypher = Bikini.Object.design(/** @scope Bikini.Cypher.prototype */ {
/**
* The type of this object.
*
* @type String
*/
type: 'Bikini.Cypher',
/**
* The default decoder.
*
* @type Bikini.Base64
*/
defaultDecoder: Bikini.Base64,
/**
* The default encoder.
*
* @type Bikini.Base64
*/
defaultEncoder: Bikini.Base64,
/**
* The default hash algorithm.
*
* @type Bikini.SHA256
*/
defaultHasher: Bikini.SHA256,
/**
* This method is the one that initiates the decoding of a given string, based on either
* the default decoder or a custom decoder.
*
* @param {String} input The input string to be decoded.
* @param {Object} algorithm The algorithm object containing a decode method.
* @returns {String} The decoded string.
*/
decode: function( input, algorithm ) {
if( algorithm && algorithm.decode ) {
return algorithm.decode(input);
} else {
return this.defaultDecoder.decode(input);
}
},
/**
* This method is the one that initiates the encoding of a given string, based on either
* the default encoder or a custom encoder.
*
* @param {String} input The input string to be decoded.
* @param {Object} algorithm The algorithm object containing a encode method.
* @returns {String} The encoded string.
*/
encode: function( input, algorithm ) {
if( algorithm && algorithm.encode ) {
return algorithm.encode(input);
} else {
return this.defaultEncoder.encode(input);
}
},
/**
* This method is the one that initiates the hashing of a given string, based on either
* the default hashing algorithm or a custom hashing algorithm.
*
* @param {String} input The input string to be hashed.
* @param {Object} algorithm The algorithm object containing a hash method.
* @returns {String} The hashed string.
*/
hash: function( input, algorithm ) {
if( algorithm && algorithm.hash ) {
return algorithm.hash(input);
} else {
return this.defaultHasher.hash(input);
}
},
/**
* Private method for UTF-8 encoding
*
* @private
* @param {String} string The string to be encoded.
* @returns {String} The utf8 encoded string.
*/
utf8Encode: function( string ) {
string = string.replace(/\r\n/g, '\n');
var utf8String = '';
for( var n = 0; n < string.length; n++ ) {
var c = string.charCodeAt(n);
if( c < 128 ) {
utf8String += String.fromCharCode(c);
} else if( (c > 127) && (c < 2048) ) {
utf8String += String.fromCharCode((c >> 6) | 192);
utf8String += String.fromCharCode((c & 63) | 128);
} else {
utf8String += String.fromCharCode((c >> 12) | 224);
utf8String += String.fromCharCode(((c >> 6) & 63) | 128);
utf8String += String.fromCharCode((c & 63) | 128);
}
}
return utf8String;
},
/**
* Private method for UTF-8 decoding
*
* @private
* @param {String} string The string to be decoded.
* @returns {String} The utf8 decoded string.
*/
utf8Decode: function( utf8String ) {
var string = '';
var i;
var c;
var c1;
var c2;
var c3;
i = c = c1 = c2 = 0;
while( i < utf8String.length ) {
c = utf8String.charCodeAt(i);
if( c < 128 ) {
string += String.fromCharCode(c);
i++;
} else if( (c > 191) && (c < 224) ) {
c2 = utf8String.charCodeAt(i + 1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
} else {
c2 = utf8String.charCodeAt(i + 1);
c3 = utf8String.charCodeAt(i + 2);
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return string;
}
});
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
/**
*
* @module Bikini.Date
*
* @extends Bikini.Object
*/
Bikini.Date = {
/**
* This method is used to create a new instance of Bikini.Date based on the data
* library moment.js.
*
* @returns {Object}
*/
create: function() {
var m = moment.apply(this, arguments);
return _.extend(m, this);
}
};
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
// ===========================================================================
//
// Bikini.ObjectId uses code from meteor.js
// https://github.com/meteor/meteor/blob/master/packages/minimongo
//
// Thanks for sharing!
//
// ===========================================================================
// m_require('core/foundation/object.js');
/**
*
* @module Bikini.ObjectID
*
*/
Bikini.ObjectID = function( hexString ) {
Bikini.ObjectID.counter = Bikini.ObjectID.counter || parseInt(Math.random() * Math.pow(16, 6));
Bikini.ObjectID.machineId = Bikini.ObjectID.machineId || parseInt(Math.random() * Math.pow(16, 6));
Bikini.ObjectID.processId = Bikini.ObjectID.processId || parseInt(Math.random() * Math.pow(16, 4));
this._ObjectID(hexString);
};
Bikini.ObjectID._looksLikeObjectID = function( str ) {
return str.length === 24 && str.match(/^[0-9a-f]*$/);
};
_.extend(Bikini.ObjectID.prototype, {
_str: '',
_ObjectID: function( hexString ) {
//random-based impl of Mongo ObjectID
if( hexString ) {
hexString = hexString.toLowerCase();
if( !Bikini.ObjectID._looksLikeObjectID(hexString) ) {
throw new Error('Invalid hexadecimal string for creating an ObjectID');
}
// meant to work with _.isEqual(), which relies on structural equality
this._str = hexString;
} else {
this._str =
this._hexString(8, new Date().getTime()/1000) + // a 4-byte value from the Unix timestamp
this._hexString(6, Bikini.ObjectID.machineId) + // a 3-byte machine identifier
this._hexString(4, Bikini.ObjectID.processId) + // a 2-byte process identifier
this._hexString(6, Bikini.ObjectID.counter++); // a 3-byte counter, starting with a random value.
}
return this._str;
},
_hexString: function(len, num) {
num = num || parseInt(Math.random() * Math.pow(16,len));
var str = num.toString(16);
while(str.length < len) {
str = '0'+str;
}
return str.substr(0, len);
},
toString: function() {
return 'ObjectID(\'' + this._str + '\')';
},
equals: function( other ) {
return other instanceof this._ObjectID && this.valueOf() === other.valueOf();
},
clone: function() {
return new Bikini.ObjectID(this._str);
},
typeName: function() {
return 'oid';
},
getTimestamp: function() {
return parseInt(this._str.substr(0, 8), 16)*1000;
},
getMachineId: function() {
return parseInt(this._str.substr(8, 6), 16);
},
getProcessId: function() {
return parseInt(this._str.substr(14, 4), 16);
},
getCounter: function() {
return parseInt(this._str.substr(18, 6), 16);
},
valueOf: function() {
return this._str;
},
toJSON: function() {
return this._str;
},
toHexString: function() {
return this._str;
},
// Is this selector just shorthand for lookup by _id?
_selectorIsId: function( selector ) {
return (typeof selector === 'string') ||
(typeof selector === 'number') ||
selector instanceof Bikini.ObjectId;
},
// Is the selector just lookup by _id (shorthand or not)?
_selectorIsIdPerhapsAsObject: function( selector ) {
return this._selectorIsId(selector) || (selector && typeof selector === 'object' && selector._id && this._selectorIsId(selector._id) && _.size(selector) === 1);
},
// If this is a selector which explicitly constrains the match by ID to a finite
// number of documents, returns a list of their IDs. Otherwise returns
// null. Note that the selector may have other restrictions so it may not even
// match those document! We care about $in and $and since those are generated
// access-controlled update and remove.
_idsMatchedBySelector: function( selector ) {
// Is the selector just an ID?
if( this._selectorIsId(selector) ) {
return [selector];
}
if( !selector ) {
return null;
}
// Do we have an _id clause?
if( _.has(selector, '_id') ) {
// Is the _id clause just an ID?
if( this._selectorIsId(selector._id) ) {
return [selector._id];
}
// Is the _id clause {_id: {$in: ["x", "y", "z"]}}?
if( selector._id && selector._id.$in && _.isArray(selector._id.$in) && !_.isEmpty(selector._id.$in) && _.all(selector._id.$in, this._selectorIsId) ) {
return selector._id.$in;
}
return null;
}
// If this is a top-level $and, and any of the clauses constrain their
// documents, then the whole selector is constrained by any one clause's
// constraint. (Well, by their intersection, but that seems unlikely.)
if( selector.$and && _.isArray(selector.$and) ) {
for( var i = 0; i < selector.$and.length; ++i ) {
var subIds = this._idsMatchedBySelector(selector.$and[i]);
if( subIds ) {
return subIds;
}
}
}
return null;
}
});
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
/**
* This prototype defines a hashing mechanism based on the SHA256 algorithm. You normally
* don't call this object respectively its methods directly, but let Bikini.Cypher handle
* this.
* @module Bikini.SHA256
*
* @extends Bikini.Object
*/
Bikini.SHA256 = Bikini.Object.design(/** @scope Bikini.SHA256.prototype */ {
/**
* The type of this object.
*
* @type String
*/
type: 'Bikini.SHA256',
/**
* Defines the bits per input character: 8 - ASCII, 16 - Unicode
*
* @type Number
*/
chrsz: 8,
/**
* Defines the hex output format: 0 - lowercase, 1 - uppercase
*
* @type Number
*/
hexcase: 0,
/**
* This method is called from the 'outside world', controls the hashing and
* finally returns the hash value.
*
* @param {String} input The input string to be hashed.
* @returns {String} The sha256 hashed string.
*/
hash: function( input ) {
input = Bikini.Cypher.utf8Encode(input);
return this.binb2hex(this.coreSha256(this.str2binb(input), input.length * this.chrsz));
},
/**
* @private
*/
safeAdd: function( x, y ) {
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
},
/**
* @private
*/
S: function( X, n ) {
return ( X >>> n ) | (X << (32 - n));
},
/**
* @private
*/
R: function( X, n ) {
return ( X >>> n );
},
/**
* @private
*/
Ch: function( x, y, z ) {
return ((x & y) ^ ((~x) & z));
},
/**
* @private
*/
Maj: function( x, y, z ) {
return ((x & y) ^ (x & z) ^ (y & z));
},
/**
* @private
*/
Sigma0256: function( x ) {
return (this.S(x, 2) ^ this.S(x, 13) ^ this.S(x, 22));
},
/**
* @private
*/
Sigma1256: function( x ) {
return (this.S(x, 6) ^ this.S(x, 11) ^ this.S(x, 25));
},
/**
* @private
*/
Gamma0256: function( x ) {
return (this.S(x, 7) ^ this.S(x, 18) ^ this.R(x, 3));
},
/**
* @private
*/
Gamma1256: function( x ) {
return (this.S(x, 17) ^ this.S(x, 19) ^ this.R(x, 10));
},
/**
* @private
*/
coreSha256: function( m, l ) {
var K = new Array(0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2);
var HASH = new Array(0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19);
var W = new Array(64);
var a, b, c, d, e, f, g, h, i, j;
var T1, T2;
m[l >> 5] |= 0x80 << (24 - l % 32);
m[((l + 64 >> 9) << 4) + 15] = l;
for( i = 0; i < m.length; i += 16 ) {
a = HASH[0];
b = HASH[1];
c = HASH[2];
d = HASH[3];
e = HASH[4];
f = HASH[5];
g = HASH[6];
h = HASH[7];
for( j = 0; j < 64; j++ ) {
if( j < 16 ) {
W[j] = m[j + i];
} else {
W[j] = this.safeAdd(this.safeAdd(this.safeAdd(this.Gamma1256(W[j - 2]), W[j - 7]), this.Gamma0256(W[j - 15])), W[j - 16]);
}
T1 = this.safeAdd(this.safeAdd(this.safeAdd(this.safeAdd(h, this.Sigma1256(e)), this.Ch(e, f, g)), K[j]), W[j]);
T2 = this.safeAdd(this.Sigma0256(a), this.Maj(a, b, c));
h = g;
g = f;
f = e;
e = this.safeAdd(d, T1);
d = c;
c = b;
b = a;
a = this.safeAdd(T1, T2);
}
HASH[0] = this.safeAdd(a, HASH[0]);
HASH[1] = this.safeAdd(b, HASH[1]);
HASH[2] = this.safeAdd(c, HASH[2]);
HASH[3] = this.safeAdd(d, HASH[3]);
HASH[4] = this.safeAdd(e, HASH[4]);
HASH[5] = this.safeAdd(f, HASH[5]);
HASH[6] = this.safeAdd(g, HASH[6]);
HASH[7] = this.safeAdd(h, HASH[7]);
}
return HASH;
},
/**
* @private
*/
str2binb: function( str ) {
var bin = [];
var mask = (1 << this.chrsz) - 1;
for( var i = 0; i < str.length * this.chrsz; i += this.chrsz ) {
bin[i >> 5] |= (str.charCodeAt(i / this.chrsz) & mask) << (24 - i % 32);
}
return bin;
},
/**
* @private
*/
binb2hex: function( binarray ) {
var hexTab = this.hexcase ? '0123456789ABCDEF' : '0123456789abcdef';
var str = '';
for( var i = 0; i < binarray.length * 4; i++ ) {
str += hexTab.charAt((binarray[i >> 2] >> ((3 - i % 4) * 8 + 4)) & 0xF) + hexTab.charAt((binarray[i >> 2] >> ((3 - i % 4) * 8 )) & 0xF);
}
return str;
}
});
// Copyright (c) 2013 M-Way Solutions GmbH
// http://github.com/mwaylabs/The-M-Project/blob/absinthe/MIT-LICENSE.txt
// Returns a unique identifier
/**
*
* @module Bikini.UniqueId
*
* @type {*}
* @extends Bikini.Object
*/
Bikini.UniqueId = Bikini.Object.design({
uuid: function(len, radix) {
// based on Robert Kieffer's randomUUID.js at http://www.broofa.com
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var uuid = [];
//len = len ? len : 32;
radix = radix || chars.length;
var i;
if (len) {
for (i = 0; i < len; i++) {
uuid[i] = chars[0 | Math.random() * radix];
}
} else {
// rfc4122, version 4 form
var r;
// rfc4122 requires these characters
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
// Fill in random data. At i==19 set the high bits of clock sequence as
// per rfc4122, sec. 4.1.5
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random() * 16;
uuid[i] = chars[(i === 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
}
});
'use strict';
module.exports = function( grunt ) {
grunt.registerTask('rewriteMarkdownFiles', function() {
var content = grunt.file.read('README.md');
var overviewText = content.slice(content.indexOf('## Overview'), content.indexOf("## What's new"));
content = content.replace('![The-M-Project Absinthe][logo]', '');
content = content.replace(overviewText, '');
grunt.file.write('doc-template/.tmp/index.md', content);
});
}
describe('Bikini Namespace', function () {
it('Bikini', function() {
assert.ok(Bikini, 'Bikini is defined.');
assert.valueOf(Bikini, 'object', 'Bikini is an object.');
});
it('Bikini.Version', function() {
assert.ok(Bikini.Version && Bikini.hasOwnProperty('Version'), 'Bikini.Version is defined');
assert.ok(typeof Bikini.Version === 'string', 'Bikini.Version is a string');
assert.ok(parseInt(Bikini.Version.split('.')[0]) >= 2, 'old TMP version ');
});
it('Bikini.f', function() {
assert.ok(Bikini.f, 'Bikini.f is defined');
assert.ok(typeof(Bikini.f) === 'function', 'Bikini.f is a function');
});
it('YES / NO', function() {
assert.ok(!NO, 'NO is defined');
assert.ok(typeof NO === 'boolean', 'NO is a boolean');
assert.ok(NO === false, 'NO equals false');
assert.ok(YES, 'YES is defined');
assert.ok(typeof YES === 'boolean', 'YES is a boolean');
assert.ok(YES === true, 'YES equals true');
});
it('locale storage', function () {
var putSomethingToTheLocaleStorage = function () {
Bikini.Application = Bikini.Application || {};
Bikini.Application.name = Bikini.Application.name || 'test';
localStorage.setItem('test0', 'test0');
localStorage.setItem('test1', 'test1');
localStorage.setItem(Bikini.LOCAL_STORAGE_PREFIX + Bikini.Application.name + Bikini.LOCAL_STORAGE_SUFFIX + 'test0', Bikini.LOCAL_STORAGE_PREFIX + Bikini.Application.name + Bikini.LOCAL_STORAGE_SUFFIX + 'test0');
localStorage.setItem(Bikini.LOCAL_STORAGE_PREFIX + Bikini.Application.name + Bikini.LOCAL_STORAGE_SUFFIX + 'test1', Bikini.LOCAL_STORAGE_PREFIX + Bikini.Application.name + Bikini.LOCAL_STORAGE_SUFFIX + 'test1');
};
assert.ok(window && window.localStorage, 'localStorage available');
putSomethingToTheLocaleStorage();
localStorage.clear('f');
assert.equal(localStorage.length, 0, 'localStorage is available and the clear function works with the parameter "f"');
putSomethingToTheLocaleStorage();
localStorage.clear();
assert.equal(localStorage.length, 0, 'localStorage.clear() is implemented as anticipated in the spec');
});
it('Bikini.isCollection', function () {
assert.isFalse(Bikini.isCollection());
assert.isFalse(Bikini.isCollection(''));
assert.isFalse(Bikini.isCollection(0));
assert.isFalse(Bikini.isCollection(1));
assert.isFalse(Bikini.isCollection({}));
assert.isFalse(Bikini.isCollection([]));
assert.isFalse(Bikini.isCollection(Bikini.Collection));
assert.isTrue(Bikini.isCollection(Bikini.Collection.create()));
assert.isTrue(Bikini.isCollection(Bikini.Collection.extend().create()));
assert.isFalse(Bikini.isCollection(Bikini.Model));
assert.isFalse(Bikini.isCollection(Bikini.Model.create()));
assert.isFalse(Bikini.isCollection(Bikini.Model.extend().create()));
});
it('Bikini.isModel', function () {
assert.isFalse(Bikini.isModel());
assert.isFalse(Bikini.isModel(''));
assert.isFalse(Bikini.isModel(0));
assert.isFalse(Bikini.isModel(1));
assert.isFalse(Bikini.isModel({}));
assert.isFalse(Bikini.isModel([]));
assert.isFalse(Bikini.isModel(Bikini.Collection));
assert.isFalse(Bikini.isModel(Bikini.Collection.create()));
assert.isFalse(Bikini.isModel(Bikini.Collection.extend().create()));
assert.isFalse(Bikini.isModel(Bikini.Model));
assert.isTrue(Bikini.isModel(Bikini.Model.create()));
assert.isTrue(Bikini.isModel(Bikini.Model.extend().create()));
});
});
describe('Bikini.Object', function () {
it('basic', function () {
assert.isDefined(Bikini.Object);
assert.isDefined(Bikini.Object._type);
assert.isObject(Bikini.Object);
assert.isString(Bikini.Object._type);
assert.equal(Bikini.Object._type, 'Bikini.Object');
});
it('methods', function () {
assert.isDefined(Bikini.Object._create);
assert.isDefined(Bikini.Object._normalize);
assert.isDefined(Bikini.Object.include);
assert.isDefined(Bikini.Object.design);
assert.isDefined(Bikini.Object.bindToCaller);
assert.isDefined(Bikini.Object.handleCallback);
assert.isFunction(Bikini.Object._create);
assert.isFunction(Bikini.Object._normalize);
assert.isFunction(Bikini.Object.include);
assert.isFunction(Bikini.Object.design);
assert.isFunction(Bikini.Object.bindToCaller);
assert.isFunction(Bikini.Object.handleCallback);
});
});
@charset "utf-8";
body {
margin:0;
}
#mocha {
font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: 60px 50px;
}
#mocha ul, #mocha li {
margin: 0;
padding: 0;
}
#mocha ul {
list-style: none;
}
#mocha h1, #mocha h2 {
margin: 0;
}
#mocha h1 {
margin-top: 15px;
font-size: 1em;
font-weight: 200;
}
#mocha h1 a {
text-decoration: none;
color: inherit;
}
#mocha h1 a:hover {
text-decoration: underline;
}
#mocha .suite .suite h1 {
margin-top: 0;
font-size: .8em;
}
#mocha .hidden {
display: none;
}
#mocha h2 {
font-size: 12px;
font-weight: normal;
cursor: pointer;
}
#mocha .suite {
margin-left: 15px;
}
#mocha .test {
margin-left: 15px;
overflow: hidden;
}
#mocha .test.pending:hover h2::after {
content: '(pending)';
font-family: arial, sans-serif;
}
#mocha .test.pass.medium .duration {
background: #C09853;
}
#mocha .test.pass.slow .duration {
background: #B94A48;
}
#mocha .test.pass::before {
content: '✓';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: #00d6b2;
}
#mocha .test.pass .duration {
font-size: 9px;
margin-left: 5px;
padding: 2px 5px;
color: white;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-ms-border-radius: 5px;
-o-border-radius: 5px;
border-radius: 5px;
}
#mocha .test.pass.fast .duration {
display: none;
}
#mocha .test.pending {
color: #0b97c4;
}
#mocha .test.pending::before {
content: '◦';
color: #0b97c4;
}
#mocha .test.fail {
color: #c00;
}
#mocha .test.fail pre {
color: black;
}
#mocha .test.fail::before {
content: '✖';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: #c00;
}
#mocha .test pre.error {
color: #c00;
max-height: 300px;
overflow: auto;
}
#mocha .test pre {
display: block;
float: left;
clear: left;
font: 12px/1.5 monaco, monospace;
margin: 5px;
padding: 15px;
border: 1px solid #eee;
border-bottom-color: #ddd;
-webkit-border-radius: 3px;
-webkit-box-shadow: 0 1px 3px #eee;
-moz-border-radius: 3px;
-moz-box-shadow: 0 1px 3px #eee;
border-radius: 3px;
}
#mocha .test h2 {
position: relative;
}
#mocha .test a.replay {
position: absolute;
top: 3px;
right: 0;
text-decoration: none;
vertical-align: middle;
display: block;
width: 15px;
height: 15px;
line-height: 15px;
text-align: center;
background: #eee;
font-size: 15px;
-moz-border-radius: 15px;
border-radius: 15px;
-webkit-transition: opacity 200ms;
-moz-transition: opacity 200ms;
transition: opacity 200ms;
opacity: 0.3;
color: #888;
}
#mocha .test:hover a.replay {
opacity: 1;
}
#mocha-report.pass .test.fail {
display: none;
}
#mocha-report.fail .test.pass {
display: none;
}
#mocha-report.pending .test.pass,
#mocha-report.pending .test.fail {
display: none;
}
#mocha-report.pending .test.pass.pending {
display: block;
}
#mocha-error {
color: #c00;
font-size: 1.5em;
font-weight: 100;
letter-spacing: 1px;
}
#mocha-stats {
position: fixed;
top: 15px;
right: 10px;
font-size: 12px;
margin: 0;
color: #888;
z-index: 1;
}
#mocha-stats .progress {
float: right;
padding-top: 0;
}
#mocha-stats em {
color: black;
}
#mocha-stats a {
text-decoration: none;
color: inherit;
}
#mocha-stats a:hover {
border-bottom: 1px solid #eee;
}
#mocha-stats li {
display: inline-block;
margin: 0 5px;
list-style: none;
padding-top: 11px;
}
#mocha-stats canvas {
width: 40px;
height: 40px;
}
#mocha code .comment { color: #ddd }
#mocha code .init { color: #2F6FAD }
#mocha code .string { color: #5890AD }
#mocha code .keyword { color: #8A6343 }
#mocha code .number { color: #2F6FAD }
@media screen and (max-device-width: 480px) {
#mocha {
margin: 60px 0px;
}
#mocha #stats {
position: absolute;
}
}
describe('Bikini.BikiniStore', function() {
var TEST = {
data : {
firstName: 'Max',
sureName: 'Mustermann',
age: 33
}
};
it('creating bikini store', function() {
assert.isString(serverUrl, 'Server url is defined.');
assert.isFunction(Bikini.BikiniStore, 'Bikini.BikiniStore is defined');
TEST.store = Bikini.BikiniStore.design({
useLocalStore: true,
useSocketNotify: false
});
assert.isObject(TEST.store, 'store successfully created.');
});
it('creating collection', function() {
assert.isFunction(Bikini.Collection, 'Bikini.Collection is defined');
TEST.TestModel = Bikini.Model.extend({
idAttribute: '_id',
entity: {
name: 'test',
fields: {
_id: { type: Bikini.DATA.TYPE.STRING, required: YES, index: YES },
sureName: { name: 'USERNAME', type: Bikini.DATA.TYPE.STRING, required: YES, index: YES },
firstName: { type: Bikini.DATA.TYPE.STRING, length: 200 },
age: { type: Bikini.DATA.TYPE.INTEGER }
}
}
});
assert.isFunction(TEST.TestModel, 'TestModel model successfully extended.');
TEST.url = serverUrl+'/bikini/test';
TEST.TestsModelCollection = Bikini.Collection.extend({
model: TEST.TestModel,
url: TEST.url,
store: TEST.store,
options: {
sort: { sureName: 1 },
fields: { USERNAME: 1, sureName: 1, firstName: 1, age : 1 },
query: { age : { $gte : 25 } }
}
});
assert.isFunction(TEST.TestsModelCollection, 'Test collection successfully extended.');
TEST.Tests = TEST.TestsModelCollection.create();
assert.isObject(TEST.Tests, 'Test collection successfully created.');
assert.equal(TEST.Tests.store, TEST.store, 'Test collection has the correct store.');
var url = TEST.Tests.getUrl();
assert.ok(url !== TEST.url, 'The base url has been extended.');
assert.equal(url.indexOf(TEST.url), 0, 'the new url starts with the set url.');
assert.ok(url.indexOf('query=')>0, 'query is part of the url.');
assert.ok(url.indexOf('fields=')>0, 'fields is part of the url.');
assert.ok(url.indexOf('sort=')>0, 'sort is part of the url.');
// try to clean everything
TEST.store.clear(TEST.Tests);
});
it('create record', function(done) {
TEST.Tests.create(TEST.data,
{
success: function(model) {
assert.isObject(model, 'new record created successfully.');
TEST.id = model.id;
assert.ok(TEST.id, 'new record has an id.');
done();
},
error: function() {
assert.ok(false, 'new record created successfully.');
done();
}
}
);
});
it('read record', function() {
var model = TEST.Tests.get(TEST.id);
assert.ok(model, "record found");
assert.equal(model.get('firstName'), TEST.data.firstName, "found record has the correct 'firstname' value");
assert.equal(model.get('sureName'), TEST.data.sureName, "found record has the correct 'sureName' value");
assert.equal(model.get('age'), TEST.data.age, "found record has the correct 'age' value");
});
it('fetching data with new model', function(done) {
TEST.TestModel2 = Bikini.Model.extend({
url : TEST.url,
idAttribute: '_id',
store: TEST.store,
entity: {
name: 'test'
}
});
var data = { _id: TEST.id };
var model = TEST.TestModel2.create(data);
assert.isObject(model, "new model created");
assert.ok(_.isEqual(model.attributes, data), "new model holds correct data attributes");
model.fetch({
success: function() {
assert.ok(true, 'model has been fetched.');
assert.equal(model.get('firstName'), TEST.data.firstName, "found record has the correct 'firstname' value");
assert.equal(model.get('USERNAME'), TEST.data.sureName, "found record has the correct 'USERNAME' value");
assert.equal(model.get('age'), TEST.data.age, "found record has the correct 'age' value");
done();
},
error: function(error) {
assert.ok(false, 'model has been fetched.');
done();
}
})
});
it('fetching collection', function(done) {
TEST.Tests.reset();
assert.equal(TEST.Tests.length, 0, 'reset has cleared the collection.');
TEST.Tests.fetch({
success: function(collection) {
assert.isObject(TEST.Tests.get(TEST.id), 'The model is still there');
done();
},
error: function() {
assert.ok(false, 'Test collection fetched successfully.');
done();
}
});
});
it('read record', function() {
var model = TEST.Tests.get(TEST.id);
assert.ok(model, "record found");
assert.equal(model.get('firstName'), TEST.data.firstName, "found record has the correct 'firstname' value");
assert.equal(model.get('sureName'), TEST.data.sureName, "found record has the correct 'sureName' value");
assert.equal(model.get('age'), TEST.data.age, "found record has the correct 'age' value");
});
it('delete record', function(done) {
var model = TEST.Tests.get(TEST.id);
assert.isObject(model, 'model found in collection');
assert.equal(model.id, TEST.id, 'model has the correct id');
model.destroy(
{
success: function(model) {
assert.ok(true, 'record has been deleted.');
done();
},
error: function() {
assert.ok(false, 'record has been deleted.');
done();
}
});
});
it('cleanup records bikini', function(done) {
if (TEST.Tests.length === 0) {
done();
} else {
var model, hasError = false, isDone = false;
while ((model = TEST.Tests.first()) && !hasError) {
model.destroy({
success: function() {
if (TEST.Tests.length == 0 && !isDone) {
isDone = true;
assert.equal(TEST.Tests.length, 0, 'collection is empty');
done();
}
},
error: function() {
hasError = isDone = true;
assert.ok(false, 'cleanup records bikini error');
done();
}
});
}
}
});
});
describe('Bikini.Collection', function() {
var testConnection = function( callback, done ) {
$.ajax({
timeout: 3000,
dataType: "json",
url: serverUrl+'/bikini/test/info',
success: function() {
callback();
},
error: function() {
assert.isTrue(YES);
console.log('timeout on Colletction test');
done();
}
});
}
var TEST = {};
TEST.url = serverUrl+'/bikini/developers';
TEST.data = [
{
sureName: 'Laubach',
firstName: 'Dominik',
age: 27
},
{
sureName: 'Hanowski',
firstName: 'Marco',
age: 27
},
{
sureName: 'Stierle',
firstName: 'Frank',
age: 43
},
{
sureName: 'Werler',
firstName: 'Sebastian',
age: 30
},
{
sureName: 'Buck',
firstName: 'Stefan',
age: 26
}
];
it('basic', function() {
assert.isDefined(Bikini.Collection);
assert.isDefined(Bikini.Collection.design);
assert.isDefined(Bikini.Collection.create);
assert.isDefined(Bikini.Collection.extend);
assert.isFunction(Bikini.Collection);
assert.isFunction(Bikini.Collection.design);
assert.isFunction(Bikini.Collection.create);
assert.isFunction(Bikini.Collection.extend);
var instance = Bikini.Collection.create();
assert.isDefined(instance);
assert.isObject(instance);
assert.isDefined(instance._type);
assert.isString(instance._type);
assert.equal(instance._type, 'Bikini.Collection');
});
it('creating collection', function() {
assert.typeOf(Bikini.Collection, 'function', 'Bikini.Collection is defined');
TEST.Developer = Bikini.Model.extend({
idAttribute: '_id',
entity: {
name: 'Developer',
fields: {
_id: { type: Bikini.DATA.TYPE.STRING },
sureName: { name: 'lastName', type: Bikini.DATA.TYPE.STRING, required: YES, index: true },
firstName: { type: Bikini.DATA.TYPE.STRING, length: 200 },
age: { type: Bikini.DATA.TYPE.INTEGER }
}
}
});
assert.ok(typeof TEST.Developer === 'function', 'Developer model successfully extended.');
TEST.DeveloperCollection = Bikini.Collection.extend({
url: TEST.url,
model: TEST.Developer
});
assert.ok(typeof TEST.DeveloperCollection === 'function', 'Developer collection successfully extended.');
TEST.Developers = new TEST.DeveloperCollection();
assert.ok(typeof TEST.Developers === 'object', 'Developer collection successfully created.');
});
it('adding data', function() {
TEST.Developers.add(TEST.data);
assert.equal(TEST.Developers.length, 5, 'All records were added.');
assert.equal(TEST.Developers.at(2).get('sureName'), TEST.data[2].sureName, 'sureName of 3. model has correct value');
assert.equal(TEST.Developers.at(3).get('firstName'), TEST.data[3].firstName, 'firstName of 4. model has correct value');
assert.equal(TEST.Developers.at(4).get('age'), TEST.data[4].age, 'age of 5. model has correct value');
assert.ok(TEST.Developer.prototype.isPrototypeOf(TEST.Developers.at(0)), 'Records successfully converted to model records.');
});
it('sorting data', function() {
TEST.Developers.sort({ 'sort': { 'sureName': -1 } });
var p1 = TEST.Developers.at(0);
assert.ok(p1.get('sureName') === 'Werler', 'Records correctly sorted descending by "sureName".');
TEST.Developers.comparator = function( m1, m2 ) {
return m2.get('age') - m1.get('age');
};
TEST.Developers.sort();
var p2 = TEST.Developers.at(0);
assert.ok(p2.get('sureName') === 'Stierle', 'Records correctly sorted by passed in sort function');
});
it('filtering data', function() {
// filter all devs older or equal to 26
var a1 = TEST.Developers.filter(function( rec ) {
return rec.get('age') >= 26;
});
assert.ok(a1.length === 5, 'Records successfully filtered. Everyone is 26 or older.');
// filter all devs older than 26
var a2 = TEST.Developers.filter(function( rec ) {
return rec.get('age') > 26;
});
assert.ok(a2.length === 4, 'Records successfully filtered. One dev is younger than 27.');
});
it('finding data', function() {
TEST.Developers.reset();
TEST.Developers.add(TEST.data);
var result = TEST.Developers.select({
query: { sureName: 'Stierle' }
});
assert.ok(typeof result === 'object', 'Find for value has a response object.');
assert.ok(Bikini.isCollection(result), 'The response is a Bikini.Collection.');
assert.ok(result.length === 1, 'The response holds one record.');
// get first person record
var p = result.at(0);
assert.ok(p.get('sureName') === 'Stierle', 'Field "sureName" has correct value.');
// SELECT * FROM Developer WHERE firstName like 'S%'
result = TEST.Developers.select({
query: { firstName: /^S/ }
});
assert.ok(typeof result === 'object', 'Find with RegEx has a response object.');
assert.ok(result.length === 2, 'The response holds 2 records.');
// SELECT * FROM Developer WHERE sureName like '%er%' OR (age > 25 AND age <= 26)
result = TEST.Developers.select({
query: { $or: [
{ sureName: /.?er/ }, // should match 'Stierle' and 'Werler'
{ age: { $gt: 25, $lte: 26 } }
] // should match Stefan Buck '26'
},
sort: ['age']
});
assert.typeOf(result, 'object', 'Find with $or, $gt, $lte and sort by "age" has a response object.');
assert.equal(result.length, 3, 'The response holds 3 records.');
var d1 = result.at(0); // has to be Stefan Buck
assert.equal(d1.get('sureName'), 'Buck', 'The first developer field "sureName" has correct value.');
var d2 = result.at(1); // has to be Sebastian Werler
assert.equal(d2.get('sureName'), 'Werler', 'The second developer field "sureName" has correct value.');
var d3 = result.at(2); // has to be Frank Stierle
assert.equal(d3.get('sureName'), 'Stierle', 'The third developer field "sureName" has correct value.');
});
it('creating data (on server)', function( done ) {
this.timeout(4000);
TEST.Developers.reset();
assert.equal(TEST.Developers.length, 0, 'All records were removed.');
var callback = function(){
TEST.Developers.create(TEST.data[0], {
success: function( model ) {
assert.isObject(model, 'data created on server');
assert.equal(model.get('sureName'), TEST.data[0].sureName, 'sureName of created model has correct value');
assert.equal(model.get('firstName'), TEST.data[0].firstName, 'firstName of created model has correct value');
assert.equal(model.get('age'), TEST.data[0].age, 'age of created model has correct value');
assert.ok(model.id, 'id of created model has been created');
TEST.id = model.id;
done();
},
error: function( error ) {
assert.ok(false, 'creating data on server: ' + error);
done();
}
});
};
testConnection(callback, done);
});
it('fetching data (from server)', function( done ) {
this.timeout(4000);
var callback = function(){
TEST.Developers.reset();
assert.equal(TEST.Developers.length, 0, 'All records were removed.');
TEST.Developers.fetch({
success: function( collection ) {
assert.isObject(collection, 'collection returned in success');
var model = collection.get(TEST.id);
assert.isObject(model, 'data found on server');
assert.equal(model.get('sureName'), TEST.data[0].sureName, 'sureName of created model has correct value');
assert.equal(model.get('firstName'), TEST.data[0].firstName, 'firstName of created model has correct value');
assert.equal(model.get('age'), TEST.data[0].age, 'age of created model has correct value');
done();
},
error: function( error ) {
assert.ok(false, 'collection fetched: ' + error);
done();
}
});
}
testConnection(callback, done);
});
it('delete records (on server)', function( done ) {
this.timeout(4000);
var callback = function(){
if( TEST.Developers.length === 0 ) {
done();
} else {
TEST.Developers.on('destroy', function( event ) {
if( TEST.Developers.length == 0 ) {
assert.equal(TEST.Developers.length, 0, 'collection is empty');
done();
}
});
var model;
while( model = TEST.Developers.first() ) {
model.destroy();
}
}
}
testConnection(callback, done);
});
});
describe('Bikini.Entity', function() {
var TEST = {
fields: {
id: { type: Bikini.DATA.TYPE.STRING, required: YES, index: YES },
sureName: { name: 'USERNAME', type: Bikini.DATA.TYPE.STRING, required: YES, index: YES },
firstName: { type: Bikini.DATA.TYPE.STRING, length: 200 },
age: Bikini.DATA.TYPE.INTEGER,
birthday: Bikini.DATA.TYPE.DATE
}
}
TEST.attributes = {
id: 1000,
sureName: 'Nachname',
firstName: 'Vorname',
age: '33',
birthday: '1999-09-23'
}
it('creating an entity', function() {
var entity = Bikini.Entity.design({
fields: TEST.fields
});
assert.isObject(entity, 'entity successfully created.');
TEST.entity = entity;
var id = entity.fields.id;
assert.isObject(id, 'id field successfully created.');
assert.equal(id.type, TEST.fields.id.type, 'id field has correct type.');
assert.ok(id.required, 'id field is required.');
assert.ok(id.index, 'id field is index.');
var sureName = entity.fields.sureName;
assert.isObject(sureName, 'sureName field successfully created.');
assert.equal(sureName.type, TEST.fields.sureName.type, 'sureName field has correct type.');
assert.ok(sureName.required, 'sureName field is required.');
assert.ok(sureName.index, 'sureName field is index.');
var firstName = entity.fields.firstName;
assert.isObject(firstName, 'firstName field successfully created.');
assert.equal(firstName.type, TEST.fields.firstName.type, 'firstName field has correct type.');
assert.ok(!firstName.required, 'firstName field is not required.');
assert.ok(!firstName.index, 'firstName field is not index.');
var age = entity.fields.age;
assert.isObject(age, 'age field successfully created.');
assert.equal(age.type, TEST.fields.age, 'age field has correct type.');
assert.ok(!age.required, 'age field is not required.');
assert.ok(!age.index, 'age field is not index.');
var birthday = entity.fields.birthday;
assert.isObject(birthday, 'birthday field successfully created.');
assert.equal(birthday.type, TEST.fields.birthday, 'birthday field has correct type.');
assert.ok(!birthday.required, 'birthday field is not required.');
assert.ok(!birthday.index, 'birthday field is not index.');
});
it('transform attributes to data', function() {
TEST.data = TEST.entity.fromAttributes(TEST.attributes);
assert.isString(TEST.data.id, 'id has the correct type');
assert.equal(TEST.data.id, TEST.attributes.id + '', 'id holds the correct data');
assert.isString(TEST.data.USERNAME, 'USERNAME has the correct type');
assert.equal(TEST.data.USERNAME, TEST.attributes.sureName, 'USERNAME (sureName) holds the correct data');
assert.isString(TEST.data.firstName, 'sureName has the correct type');
assert.equal(TEST.data.firstName, TEST.attributes.firstName, 'firstName holds the correct data');
assert.isNumber(TEST.data.age, 'age has the correct type');
assert.equal(TEST.data.age, parseInt(TEST.attributes.age), 'firstName holds the correct data');
assert.isObject(TEST.data.birthday, 'birthday has the correct type');
assert.equal(TEST.data.birthday.format('YYYY-MM-DD'), TEST.attributes.birthday, 'birthday holds the correct data');
});
});
describe('Bikini.Field', function() {
var TEST = {
fields: {
_id: { type: Bikini.DATA.TYPE.STRING, required: YES, index: YES },
sureName: { name: 'USERNAME', type: Bikini.DATA.TYPE.STRING, required: YES, index: YES },
firstName: { type: Bikini.DATA.TYPE.STRING, length: 200 },
age: Bikini.DATA.TYPE.INTEGER,
birthday: Bikini.DATA.TYPE.DATE
}
}
it('creating a field', function() {
var id = Bikini.Field.create( TEST.fields._id );
assert.isObject(id, 'id field successfully created.');
assert.equal(id.type, TEST.fields._id.type, 'id field has correct type.');
assert.ok(id.required, 'id field is required.');
assert.ok(id.index, 'id field is index.');
TEST.id = id;
var sureName = Bikini.Field.create( TEST.fields.sureName );
assert.isObject(sureName, 'sureName field successfully created.');
assert.equal(sureName.type, TEST.fields.sureName.type, 'sureName field has correct type.');
assert.ok(sureName.required, 'sureName field is required.');
assert.ok(sureName.index, 'sureName field is index.');
TEST.sureName = sureName;
var firstName = Bikini.Field.create( TEST.fields.firstName );
assert.isObject(firstName, 'firstName field successfully created.');
assert.equal(firstName.type, TEST.fields.firstName.type, 'firstName field has correct type.');
assert.ok(!firstName.required, 'firstName field is not required.');
assert.ok(!firstName.index, 'firstName field is not index.');
TEST.firstName = firstName;
var age = Bikini.Field.create( TEST.fields.age );
assert.isObject(age, 'age field successfully created.');
assert.equal(age.type, TEST.fields.age, 'age field has correct type.');
assert.ok(!age.required, 'age field is not required.');
assert.ok(!age.index, 'age field is not index.');
TEST.age = age;
var birthday = Bikini.Field.create( TEST.fields.birthday);
assert.isObject(birthday, 'birthday field successfully created.');
assert.equal(birthday.type, TEST.fields.birthday, 'birthday field has correct type.');
assert.ok(!birthday.required, 'birthday field is not required.');
assert.ok(!birthday.index, 'birthday field is not index.');
TEST.birthday = birthday;
});
it('converting a field', function() {
var value = TEST.sureName.transform(1000);
assert.isString(value, 'sureName transforms integer into string');
value = TEST.age.transform('23');
assert.isNumber(value, 'age transforms string into integer');
value = TEST.birthday.transform('2013-11-22');
assert.isObject(value, 'birthday transforms string into date');
});
it('comparing a field', function() {
assert.ok(TEST.sureName.equals(1000, '1000'), '1000 == "1000" for field type string')
assert.ok(!TEST.age.equals(1000, '1002'), '1000 != "1002" for field type integer')
assert.ok(TEST.age.equals(1000, '1000'), '1000 == "1000" for field type integer')
assert.ok(TEST.age.equals(10.4, '10.7'), '10.4 == "10.7" for field type integer')
});
});
describe('Bikini.LocalStorageStore', function() {
var TEST = {};
it('creating local storage store', function() {
assert.typeOf(Bikini.LocalStorageStore, 'function', 'Bikini.LocalStorageStore is defined');
TEST.store = Bikini.LocalStorageStore.design();
assert.typeOf(TEST.store, 'object', 'store successfully created.');
});
TEST.dropEntityTest = function (done) {
TEST.store.drop({
entity: {
name: 'test'
},
success: function() {
assert.ok(true, 'drop table test');
done();
},
error: function(error) {
assert.ok(false, 'drop table test: ' + error);
done();
}
});
};
it('simple model with LocalStorageStore', function( done ) {
TEST.SimpleModel = Bikini.Model.extend({
store: Bikini.LocalStorageStore.create(),
entity: 'test'
});
assert.typeOf(TEST.SimpleModel, 'function', 'Simple model successfully extended.');
TEST.Simple = TEST.SimpleModel.create({
firstname: 'Max',
lastname: 'Mustermann'
});
assert.typeOf(TEST.Simple, 'object', 'Simple model successfully created.');
TEST.Simple.save({}, // save existing data
{
success: function(model) {
assert.ok(model, 'new record exists.');
TEST.id = model.id;
assert.ok(TEST.id, 'new record has an id.');
done();
},
error: function(model, error) {
assert(false, 'error creating record: '+ error);
done();
}
}
);
});
it('simple collection with LocalStorageStore', function( done ) {
TEST.SimpleModelCollection = Bikini.Collection.extend({
store: Bikini.LocalStorageStore.create(),
entity: 'test'
});
assert.typeOf(TEST.SimpleModelCollection, 'function', 'Simple collection successfully extended.');
TEST.Simple = TEST.SimpleModelCollection.create();
assert.typeOf(TEST.Simple, 'object', 'Simple collection successfully created.');
TEST.Simple.create(TEST.data,
{
success: function(model) {
assert.ok(model, 'new record exists.');
TEST.id = model.id;
assert.ok(TEST.id, 'new record has an id.');
done();
},
error: function(model, error) {
assert(false, 'error creating record: '+ error);
done();
}
}
);
});
it('drop table', TEST.dropEntityTest);
it('creating collection', function() {
assert.typeOf(Bikini.Collection, 'function', 'Bikini.Collection is defined');
TEST.TestModel = Bikini.Model.extend({
idAttribute: '_id',
entity: {
name: 'test',
fields: {
_id: { type: Bikini.DATA.TYPE.STRING, required: YES },
sureName: { name: 'USERNAME', type: Bikini.DATA.TYPE.STRING, required: YES },
firstName: { type: Bikini.DATA.TYPE.STRING, length: 200 },
age: { type: Bikini.DATA.TYPE.INTEGER }
}
}
});
assert.typeOf(TEST.TestModel, 'function', 'TestModel model successfully extended.');
TEST.TestModelCollection = Bikini.Collection.extend({
model: TEST.TestModel,
store: TEST.store
});
assert.typeOf(TEST.TestModelCollection, 'function', 'Test collection successfully extended.');
TEST.Test = TEST.TestModelCollection.create();
assert.typeOf(TEST.Test, 'object', 'Test collection successfully created.');
assert.ok(TEST.Test.store === TEST.store, 'Test collection has the correct store.');
});
it('create record', function(done) {
TEST.data = {
firstName: 'Max',
sureName: 'Mustermann',
age: 33
};
TEST.Test.create(TEST.data,
{
success: function(model) {
assert.ok(model, 'new record created successfully.');
TEST.id = model.id;
assert.ok(TEST.id, 'new record has an id.');
assert.equal(model.get('firstName'), TEST.data.firstName, "created record has the correct 'firstname' value");
assert.equal(model.get('sureName'), TEST.data.sureName, "created record has the correct 'sureName' value");
assert.equal(model.get('age'), TEST.data.age, "created record has the correct 'age' value");
done();
},
error: function() {
assert.ok(false, 'new record created successfully.');
done();
}
}
);
});
it('read record', function() {
var model = TEST.Test.get(TEST.id);
assert.ok(model, "record found");
assert.equal(model.get('firstName'), TEST.data.firstName, "found record has the correct 'firstname' value");
assert.equal(model.get('sureName'), TEST.data.sureName, "found record has the correct 'sureName' value");
assert.equal(model.get('age'), TEST.data.age, "found record has the correct 'age' value");
});
it('fetching data with new model', function(done) {
TEST.TestModel2 = Bikini.Model.extend({
idAttribute: '_id',
store: TEST.store,
entity: {
name: 'test'
}
});
var model = TEST.TestModel2.create({ _id: TEST.id });
assert.isObject(model, "new model created");
model.fetch({
success: function() {
assert.ok(true, 'model has been fetched.');
assert.equal(model.get('firstName'), TEST.data.firstName, "found record has the correct 'firstname' value");
assert.equal(model.get('USERNAME'), TEST.data.sureName, "found record has the correct 'USERNAME' value");
assert.equal(model.get('age'), TEST.data.age, "found record has the correct 'age' value");
done();
},
error: function(error) {
assert.ok(false, 'model has been fetched.');
done();
}
})
});
it('delete record', function(done) {
TEST.Test.get(TEST.id).destroy(
{
success: function(model) {
assert.ok(true, 'record has been deleted.');
done();
},
error: function() {
assert.ok(false, 'record has been deleted.');
done();
}
}
);
});
it('fetching collection', function(done) {
TEST.Test.fetch({
success: function(collection) {
assert.ok(true, 'Test collection fetched successfully.');
TEST.count = TEST.Test.length;
done();
},
error: function() {
assert.ok(false, 'Test collection fetched successfully.');
done();
}
});
});
it('cleanup records local storage', function(done) {
if (TEST.Test.length === 0) {
done();
} else {
TEST.Test.on('all', function(event) {
if (event === 'destroy' && TEST.Test.length == 0) {
done();
}
});
var model;
while (model = TEST.Test.first()) {
model.destroy();
}
}
});
});
describe('Bikini.Model', function() {
var TEST = {};
it('basic', function () {
assert.isDefined(Bikini.Model);
assert.isDefined(Bikini.Model.design);
assert.isDefined(Bikini.Model.create);
assert.isDefined(Bikini.Model.extend);
assert.isFunction(Bikini.Model.design);
assert.isFunction(Bikini.Model.create);
assert.isFunction(Bikini.Model.extend);
var instance = Bikini.Model.create();
assert.isDefined(instance);
assert.isObject(instance);
assert.isDefined(instance._type);
assert.isString(instance._type);
assert.equal(instance._type, 'Bikini.Model');
});
it ('creating model', function() {
assert.typeOf(Bikini.Model, 'function', 'Bikini.Model is defined.');
var Person = Bikini.Model.extend({
idAttribute: 'id',
defaults: { bmi: 0.0 },
entity: {
name: 'person',
fields: {
id: { type: Bikini.DATA.TYPE.INTEGER, required: YES },
firstName: { type: Bikini.DATA.TYPE.STRING, length: 200 },
sureName: { type: Bikini.DATA.TYPE.STRING, required: YES, index: true },
birthDate: { type: Bikini.DATA.TYPE.DATE },
bmi: { type: Bikini.DATA.TYPE.FLOAT },
notes: { type: Bikini.DATA.TYPE.TEXT },
address: { type: Bikini.DATA.TYPE.OBJECT },
displayName: { type: Bikini.DATA.TYPE.STRING, persistent: NO }
}
}
});
assert.typeOf(Person, 'function', 'person model could be extended.');
TEST.Person = Person.create();
assert.typeOf(TEST.Person, 'object', 'empty person model could be created.');
var p = Person.create({
firstName: 'Max',
sureName: 'Mustermann',
birthDate: Bikini.Date.create('01.02.2003'),
notes: 'Notes to this person',
address: { street: 'Leitzstraße', house_nr: 45, zip: '70469', city: 'Stuttgart' }
});
assert.ok(typeof p === 'object', 'person record could be created.');
assert.ok(p.get('firstName') === 'Max', 'Field "firstName" is set.');
assert.ok(p.get('sureName') === 'Mustermann', 'Field "sureName" is set.');
assert.ok(p.get('bmi') === 0.0, 'Field "bmi" has correct default value.');
assert.ok(p.get('notes') === 'Notes to this person', 'Field "note" has correct value.');
assert.ok(typeof p.get('id') === 'undefined', 'Field "id" is undefined, as expected.');
assert.ok(p.get('address').street === 'Leitzstraße', 'Field "address" has correct value.');
});
});
describe('Bikini.WebSqlStore', function() {
var TEST = {
data : {
firstName: 'Max',
sureName: 'Mustermann',
age: 33
}
};
it('creating websql store', function() {
assert.typeOf(window.openDatabase, 'function', 'Browser supports WebSql');
assert.typeOf(Bikini.WebSqlStore, 'function', 'Bikini.LocalStorageStore is defined');
TEST.store = Bikini.WebSqlStore.design();
assert.typeOf(TEST.store, 'object', 'store successfully created.');
});
TEST.dropTableTest = function (done) {
TEST.store.drop({
entity: 'test',
success: function() {
assert.ok(true, 'drop table test');
done();
},
error: function(error) {
assert.ok(false, 'drop table test: ' + error);
done();
}
});
};
it('drop table', TEST.dropTableTest);
it('simple websql store', function( done ) {
TEST.SimpleModel = Bikini.Model.extend({
idAttribute: '_id'
});
assert.typeOf(TEST.SimpleModel, 'function', 'SimpleModel model successfully extended.');
TEST.SimpleModelCollection = Bikini.Collection.extend({
model: TEST.SimpleModel,
store: new Bikini.WebSqlStore(),
entity: 'test'
});
assert.typeOf(TEST.SimpleModelCollection, 'function', 'Simple collection successfully extended.');
TEST.Simple = TEST.SimpleModelCollection.create();
assert.typeOf(TEST.Simple, 'object', 'Simple collection successfully created.');
TEST.Simple.create(TEST.data,
{
success: function(model) {
assert.ok(model, 'new record exists.');
TEST.id = model.id;
assert.ok(TEST.id, 'new record has an id.');
done();
},
error: function(error) {
assert.ok(false, 'new record created: '+ JSON.stringify(error));
done();
}
}
);
});
it('drop table', TEST.dropTableTest);
it('creating collection', function() {
assert.typeOf(Bikini.Collection, 'function', 'Bikini.Collection is defined');
TEST.TestModel = Bikini.Model.extend({
idAttribute: '_id',
entity: {
name: 'test',
fields: {
_id: { type: Bikini.DATA.TYPE.STRING, required: YES, index: YES },
sureName: { name: 'USERNAME', type: Bikini.DATA.TYPE.STRING, required: YES, index: YES },
firstName: { type: Bikini.DATA.TYPE.STRING, length: 200 },
age: { type: Bikini.DATA.TYPE.INTEGER }
}
}
});
assert.typeOf(TEST.TestModel, 'function', 'TestModel model successfully extended.');
TEST.TestModelCollection = Bikini.Collection.extend({
model: TEST.TestModel,
store: TEST.store
});
assert.typeOf(TEST.TestModelCollection, 'function', 'Test collection successfully extended.');
TEST.Tests = TEST.TestModelCollection.create();
assert.typeOf(TEST.Tests, 'object', 'Test collection successfully created.');
assert.ok(TEST.Tests.store === TEST.store, 'Test collection has the correct store.');
});
it('create record 1', function(done) {
TEST.Tests.create(TEST.data,
{
success: function(model) {
assert.ok(model, 'new record exists.');
TEST.id = model.id;
assert.ok(TEST.id, 'new record has an id.');
done();
},
error: function(error) {
assert.ok(false, 'new record created: '+ JSON.stringify(error));
done();
}
}
);
});
it('create record 2', function(done) {
TEST.Tests.create(TEST.data,
{
success: function(model) {
assert.ok(model, 'new record exists.');
TEST.id = model.id;
assert.ok(TEST.id, 'new record has an id.');
done();
},
error: function(error) {
assert.ok(false, 'new record created: '+ JSON.stringify(error));
done();
}
}
);
});
it('read record', function() {
var model = TEST.Tests.get(TEST.id);
assert.ok(model, "record found");
assert.equal(model.get('firstName'), TEST.data.firstName, "found record has the correct 'firstname' value");
assert.equal(model.get('sureName'), TEST.data.sureName, "found record has the correct 'sureName' value");
assert.equal(model.get('age'), TEST.data.age, "found record has the correct 'age' value");
});
it('fetching data with new model', function(done) {
TEST.TestModel2 = Bikini.Model.extend({
idAttribute: '_id',
store: TEST.store,
entity: {
name: 'test'
}
});
var model = TEST.TestModel2.create({ _id: TEST.id });
assert.isObject(model, "new model created");
model.fetch({
success: function() {
assert.ok(true, 'model has been fetched.');
assert.equal(model.id, TEST.id, "found record has the correct id");
assert.equal(model.get('firstName'), TEST.data.firstName, "found record has the correct 'firstname' value");
assert.equal(model.get('USERNAME'), TEST.data.sureName, "found record has the correct 'USERNAME' value");
assert.equal(model.get('age'), TEST.data.age, "found record has the correct 'age' value");
done();
},
error: function(error) {
assert.ok(false, 'model has been fetched.');
done();
}
})
});
it('delete record', function(done) {
TEST.Tests.get(TEST.id).destroy(
{
success: function(model) {
assert.ok(true, 'record has been deleted.');
done();
},
error: function() {
assert.ok(false, 'record has been deleted.');
done();
}
}
);
});
it('fetching collection', function(done) {
TEST.Tests.fetch({
success: function(collection) {
assert.ok(true, 'Test collection fetched successfully.');
TEST.count = TEST.Tests.length;
done();
},
error: function() {
assert.ok(false, 'Test collection fetched successfully.');
done();
}
});
});
it('cleanup records websql', function(done) {
if (TEST.Tests.length === 0) {
done();
} else {
TEST.Tests.on('all', function(event) {
if (event === 'destroy' && TEST.Tests.length == 0) {
done();
}
});
var model;
while (model = TEST.Tests.first()) {
model.destroy();
}
}
});
it('drop table', TEST.dropTableTest);
it('create record (no schema)', function(done) {
TEST.Tests2 = Bikini.Collection.design({
model: TEST.TestModel2,
store: TEST.store
});
assert.isObject(TEST.Tests2, "Bikini.Collection.design created a new collection");
TEST.data = {
firstName: 'Max',
sureName: 'Mustermann',
age: 33
};
TEST.Tests2.create(TEST.data,
{
success: function(model) {
assert.ok(model, 'new record exists.');
TEST.id = model.id;
assert.ok(TEST.id, 'new record has an id.');
done();
},
error: function(error) {
assert.ok(false, 'new record created: ' + error);
done();
}
}
);
});
it('read record', function() {
var model = TEST.Tests2.get(TEST.id);
assert.ok(model, "record found");
assert.equal(model.get('firstName'), TEST.data.firstName, "found record has the correct 'firstname' value");
assert.equal(model.get('sureName'), TEST.data.sureName, "found record has the correct 'sureName' value");
assert.equal(model.get('age'), TEST.data.age, "found record has the correct 'age' value");
});
it('drop table', TEST.dropTableTest);
});
/*
* Is injected into the spec runner file
* Copyright (c) 2012 Kelly Miyashiro
* Copyright (c) 2012 "Cowboy" Ben Alman
* Licensed under the MIT license.
* http://benalman.com/about/license/
*/
/*global mocha:true, alert:true, window:true */
(function() {
// Send messages to the parent phantom.js process via alert! Good times!!
function sendMessage() {
var args = [].slice.call(arguments);
alert(JSON.stringify(args));
}
// Create a listener who'll bubble events from Phantomjs to Grunt
function createGruntListener(ev, runner) {
runner.on(ev, function(test, err) {
var data = {
err: err
};
if (test) {
data.title = test.title;
data.fullTitle = test.fullTitle();
data.duration = test.duration;
data.slow = test.slow;
}
sendMessage('mocha.' + ev, data);
});
}
var GruntReporter = function(runner){
// 1.4.2 moved reporters to Mocha instead of mocha
var mochaInstance = window.Mocha || window.mocha;
if (!mochaInstance) {
throw new Error('Mocha was not found, make sure you include Mocha in your HTML spec file.');
}
// Setup HTML reporter to output data on the screen
mochaInstance.reporters.HTML.call(this, runner);
// Create a Grunt listener for each Mocha events
var events = [
'start',
'test',
'test end',
'suite',
'suite end',
'fail',
'pass',
'pending',
'end'
];
for(var i = 0; i < events.length; i++) {
createGruntListener(events[i], runner);
}
};
var options = window.PHANTOMJS;
if (options) {
// Default mocha options
var config = {
ui: 'bdd',
ignoreLeaks: true,
reporter: GruntReporter
},
run = options.run,
key;
if (options) {
// If options is a string, assume it is to set the UI (bdd/tdd etc)
if (typeof options === "string") {
config.ui = options;
} else {
// Extend defaults with passed options
for (key in options.mocha) {
config[key] = options.mocha[key];
}
}
}
config.reporter = GruntReporter;
mocha.setup(config);
// task option `run`, automatically runs mocha for grunt only
if (run) {
mocha.run();
}
}
}());

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

<html>
<head>
<meta charset="utf-8">
<title>Mocha Tests</title>
<link rel="stylesheet" href="css/mocha.css"/>
</head>
<body>
<div id="mocha"></div>
<script src="js/chai.js"></script>
<script src="js/mocha.js"></script>
<script src="js/bridge.js"></script>
<script src="../bower_components/jquery/jquery.js"></script>
<script src="../bower_components/underscore/underscore.js"></script>
<script src="../bower_components/backbone/backbone.js"></script>
<script src="../bower_components/socket.io-client/dist/socket.io.js"></script>
<script src="../bower_components/momentjs/moment.js"></script>
<script src="../.tmp/bikini.js"></script>
<script>
assert = chai.assert;
expect = chai.expect;
mocha.setup('bdd');
</script>
<script src="test.js"></script>
<script src="core/test.bikini.js"></script>
<script src="core/test.object.js"></script>
<script src="data/test.field.js"></script>
<script src="data/test.entity.js"></script>
<script src="data/test.model.js"></script>
<script src="data/test.collection.js"></script>
<script src="data/test.local_storage.js"></script>
<script src="data/test.websql.js"></script>
<script src="data/test.bikini.js"></script>
<script>
mocha.checkLeaks();
mocha.globals(['jQuery']);
mocha.run();
</script>
/**
* Dependencies.
*/
if ('undefined' != typeof require) {
chai = require('chai');
assert = chai.assert;
expect = chai.expect;
}
serverUrl = "http://localhost:8100";
describe('Mocha', function() {
it('assert samples', function() {
var x = {};
assert.valueOf(x, 'object', 'x is an object.');
assert.isObject(x, 'x is an object.');
assert.ok(1 !== 2, '1 is not 2');
assert.equal(-1, [1,2,3].indexOf(5));
assert.equal(-1, [1,2,3].indexOf(0));
});
it('expect samples', function() {
var foo = 'bar'
var beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };
expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.length(3);
expect(beverages).to.have.property('tea').with.length(3);
});
});
# Technologie:
## html5
<http://www.w3.org/html/logo/downloads/HTML5_Logo.svg>
## javascript
<http://seravo.fi/uploads/seravo/2013/06/JavaScript-logo-300x300.png>
## css3
<http://ohdoylerules.com/content/images/css3.svg>
# Core:
## Backbone
<http://backbonejs.org/docs/images/backbone.png>
## jQuery mobile:
<http://jquerymobile.com/wp-content/uploads/2012/09/jquery-mobile-logo-03.png>
## Topcoat:
<http://topcoat.io/>
## Bootstrap:
<http://getbootstrap.com/>
## Hammer:
<http://eightmedia.github.io/hammer.js/img/hammer-figure.png>
## stickit
<http://nytimes.github.io/backbone.stickit/>
# Build Tool:
## Grunt:
<http://gruntjs.com/img/grunt-logo.svg>
## Bower:
<http://bower.io/img/bower-logo.png>
## Yeoman:
<http://yeoman.io/assets/img/yeoman-logo.png>
## node
<http://nodejs.org/images/logo.png>
# Libraties
## modernizr
<http://modernizr.com/i/img/logo-x12.png>
# Tools
## github
<https://github.com/logos>
## webstorm
<http://www.jetbrains.com/company/press/logos.html>
# Platforms
## android
<http://developer.android.com/distribute/googleplay/promote/brand.html>
<http://developer.android.com/images/brand/Android_Robot_200.png>
## ios
????
## blackberry
## windows phone
# Company
## M-Way
<https://wiki.mwaysolutions.com/confluence/display/MWSG/M-Way+general>
# Links
http://backbonejs.org/
http://underscorejs.org/
http://jquery.com
http://getbootstrap.com/
http://eightmedia.github.io/hammer.js/
http://nytimes.github.io/backbone.stickit/
http://socket.io/
http://modernizr.com/
http://momentjs.com/
https://github.com/barisaydinoglu/Detectizr
http://fontawesome.io/
https://github.com/alexgibson/shake.js/
https://github.com/ftlabs/fastclick
https://github.com/mwaylabs/grunt-tmpl
https://github.com/mwaylabs/tmpl
+39
-18
{
"name": "bikini",
"description": "a offline first technology to keep remote and local data in sync",
"homepage": "http://the-m-project.org",
"author": "M-Way Solutions GmbH",
"version": "0.0.3",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "3.4.7",
"socket.io": "0.9.16",
"mongodb": "1.3.23"
},
"repository": {
"type": "git",
"url": "https://github.com/mwaylabs/bikini"
}
}
"name": "bikini",
"version": "0.5.0",
"description": "",
"keywords": [
"the-m-project",
"bikini",
"backbone",
"framework",
"mvc"
],
"homepage": "http://the-m-project.org",
"author": "M-Way Solutions GmbH",
"main": "Gruntfile.js",
"scripts": {
"test": "grunt travis --verbose"
},
"devDependencies": {
"grunt": "0.4.2",
"grunt-contrib-jshint": "~0.7.2",
"grunt-contrib-uglify": "~0.2.7",
"grunt-contrib-watch": "~0.5.3",
"grunt-contrib-copy": "~0.4.1",
"load-grunt-tasks": "~0.2.0",
"grunt-preprocess": "~3.0.1",
"grunt-mocha": "~0.4.6",
"grunt-jsonlint": "~1.0.4",
"lodash": "~2.4.1",
"time-grunt": "~0.2.3",
"grunt-jsdoc": "~0.4.2",
"grunt-curl": "~1.2.1",
"grunt-contrib-clean": "~0.5.0",
"grunt-express-server": "~0.4.11"
},
"engines": {
"node": ">=0.8.0",
"npm": ">=1.2.10"
},
"dependencies": {}
}
+630
-11

@@ -1,18 +0,637 @@

# Bikini
Offline/online synchronization with Bikini: All a model needs
![The-M-Project Absinthe][logo]
# The-M-Project 2.0 (Absinthe) Beta Release [![Build Status](https://travis-ci.org/mwaylabs/bikini.png?branch=master)](https://travis-ci.org/mwaylabs/bikini)
## The-M-Project 2.0 - node Test Server
The-M-Project is a Mobile HTML5 JavaScript Framework that helps you build great mobile apps, easy and fast.
For use during development of a node.js based server features.
**Version:** The-M-Project v.2.0 beta
## Requirements
- node
- a mongodb database
**Codename:** Absinthe
## Installation
## Overview
- [What's new](#whats-new)
- [Demo](#demo)
- [Roadmap](#roadmap)
- [Changelog](#changelog)
- [Further Reading and Repositories](#further-reading-and-repositories)
- [Application Lifecycle](#application-lifecycle)
- [Folder structure](#folder-structure)
- [Bikini](#bikini)
- [Model–view–controller](#modelviewcontroller)
- [Layouts](#layouts-1)
- [Q&A](#qa)
- [compass](#sass-compass)
- [Styleguide](#styleguide)
- [Development Process](#development-process)
- [Setup for framework developer (Mac/Linux)](#setup-for-framework-developer-maclinux)
- [Setup for framework developer (Windows)](#setup-for-framework-developer-windows)
npm install
## What's new
The-M-Project 1.x as we call it was from our point of view pretty good, but has here and there little tweaks. We could have fix some bugs and work with the existing one. In this process we questioned everything and after huge discussions we decided to take what's good and remove everything we don't like. Furthermore we added everything what we think a mobile HTML5/JavaScript framework needs. The following list gives you an overview of changes:
## Start server
- The-M-Project is based on [Backbone.js](http://backbonejs.org/)
- [Bikini](#bikini) - a implementation of Model/Server connectivity to write realtime, collaborative apps
- Espresso (command line tool) is now based on [Grunt](http://gruntjs.com/) and [Yeoman](http://yeoman.io/)
- No jQuery mobile support at the moment
- Different Themes out of the Box
- Android
- iOS 7
- [Default](#styleguide)
- URL navigation and deep linking
- 60+ CSS Transitions
- [Sass](http://sass-lang.com/) support
- Best-of-Breed
- [backbone.stickit](http://nytimes.github.io/backbone.stickit/)
- [Hammer.js](http://eightmedia.github.io/hammer.js/)
- [Font Awesome](http://fontawesome.io/)
- [Bootstrap Grid system](http://getbootstrap.com/css/#grid)
- [jQuery 2.0](http://jquery.com/)
- [Socket.IO](http://socket.io/)
- Libraries inside The-M-Project
- [Underscore.js](http://underscorejs.org/)
- [Modernizr](http://modernizr.com/)
- [Detectizr](https://github.com/barisaydinoglu/Detectizr)
- [Moment.js](http://momentjs.com/)
- [Shake.js](http://alxgbsn.co.uk/)
- [Page Transitions](https://github.com/codrops/PageTransitions)
- Generator
- [Bower](http://bower.io/)
- [Grunt](http://gruntjs.com/)
- [Yeoman](http://yeoman.io/)
- Stable on the following devices
- Android 4.4 (Nexus 4, Nexus 5, Nexus 7, Nexus 10)
- Android > 4 (S3)
- iOS 7 (iPad 2 - Air, iPhone 4 - 5s, iPad mini/retina)
- iOS 6 (iPad 2 - 4, iPhone 4 - 5, iPad mini)
- Kindle Fire
- Tested on the following devices
- Android > 2.3 (with scrolling issues Nexus 1)
- Android (Acer Iconia Tab, Motorola Xoom)
- Microsoft Surface
- BlackBerry Z10
node server.js
## Demo
### Kitchensink Demo
[![Try the Kitchensink][tryKitchensink]](http://www.the-m-project.org/apps/absinthe/kitchensink/index.html)
The Kitchensink gives a good overview of all Views and Layouts so far. Play around and see what the The-M-Project offers to you.
Beside the Kitchensink we have a second ready to launch sample app called Addressbook.
### Addressbook Demo
[![Try the Addressbook][tryAddressbook]](http://www.the-m-project.org/apps/absinthe/addressbook/index.html)
The Addressbook is the sample app for [Bikini](#bikini). [Open the app](http://www.the-m-project.org/apps/absinthe/addressbook/index.html) in different browser windows or devices for the full experience. A small [node.js](http://nodejs.org/) server is connected to a [MongoDB](http://www.mongodb.org/). The application syncronises via bikini the contact collection and stores it to the [WebSQL](http://en.wikipedia.org/wiki/Web_SQL_Database) Database. This guarantees offline and online manipulation of the data.
## Roadmap
### Absinthe release
- The-M-Project v.2.0 beta 1
- All the fancy stuff we mentioned above
- The-M-Project v.2.0 beta 2 (We are here - tagname beta3)
- MenuView
- SideMenu
- MenuLayout
- Scaffold for MenuLayout
- The-M-Project v.2.0 RC1
- touchable ToggleSwitch
- Feedback from the Community
- Complete documentation
- Tutorials
- More Samples
- test coverage >100% https://coveralls.io/r/mwaylabs/generator-m?branch=
- Themes for Android and iOS
- The-M-Project v.2.0
- Final release
### Changelog
## The-M-Project v.2.0 beta 3
### Quickfixes like:
- Ignore unnecessary files and folder
- MenuView update/improvements
- CSS/SASS update/improvements
- Buttongroup setActive
- enhanced API for M.ViewManager.getView
- fixes to offline data handling
- Filter for M.ListView
## The-M-Project v.2.0 beta 2
- CSS/SASS update/improvements
- M.ToolbarView
- M.ButtonView
- Refactoring View: templates to single files
- Implemented Switch layout
- Getter for M.ListView
- Improved online/offline detection of bikini
- Implemented M.MenuView and MenuLayout
## The-M-Project v.2.0 beta 1
- Initial beta release
### Future plans
- The-M-Project 2.1
- Full support for Windows
- Enterprise Edition
- Extending Bikini
- A lot more to come
## Further Reading and Repositories
Use the yeoman [generator](https://github.com/mwaylabs/generator-m/) to develop your first app.
### Documentation
- [JSDoc](http://www.the-m-project.org/docs/absinthe/)
### Tutorials and Samples
- [write your first app from scratch](https://github.com/mwaylabs/The-M-Project-Sample-Apps/blob/master/demoapp/README.md)
- [GitHub](https://github.com/mwaylabs/The-M-Project-Sample-Apps)
### templ
This grunt plugin is similar to grunt-contrib-jst.
- [GitHub](https://github.com/mwaylabs/grunt-tmpl)
## Application Lifecycle
1. index.html
- Start point of a application is the index.html file. After all dependencies and application files are loaded the framework uses a [Backbone.Router](http://backbonejs.org/#Router) to call the responsible controller.
- The routes are defined inside the `main.js`
2. Controller
**There are 3 entry points to a controller.**
1. Application Start
- If the application was started the first time the Router calls the `applicationStart` of the provided Controller.
2. Show
- If a page switch happens the router calls the `show` function of the provided Controller
3. Application Ready
- After the application did load the `applicationReady` function is called on every Controller. In every case it gets called after the `applicationStart`
## Folder structure
```
.
├── Gruntfile.js
├── node_modules
├── app
│   ├── bower_components
│   ├── i18n
│   │   └── en.js
│   ├── icons
│   │   ├── android-l.png
│   │   ├── android-m.png
│   │   ├── android-s.png
│   │   ├── apple-ipad-retina.png
│   │   ├── apple-ipad.png
│   │   ├── apple-iphone-retina.png
│   │   ├── apple-iphone.png
│   │   └── favicon.png
│   ├── images
│   ├── index.html
│   ├── scripts
│   │   ├── config.js
│   │   ├── controllers
│   │   │   ├── absinthe.js
│   │   │   └── beer.js
│   │   ├── main.js
│   │   ├── models
│   │   ├── collections
│   │   ├── layouts
│   │   └── views
│   │   ├── absinthe.js
│   │   └── beer.js
│   └── styles
│   └── main.css
├── bower.json
├── grunt.config.js
├── package.json
└── test
├── index.html
├── lib
│   ├── chai.js
│   ├── expect.js
│   └── mocha
│   ├── mocha.css
│   └── mocha.js
└── spec
└── test.js
```
### app - The Application
The app folder contains all app relevant files.
#### index.html
The starting point of the application is the index.html. You can add scripts by yourself. But don't delete any comments. The generator uses them to add code inside the file. If you create a controller with the generator the index will auto generate the script tag.
#### i18n
You find all the language files inside the i18n(internationalisation) folder.
#### icons
Out of the box we have provide relevant The-M-Project icons. If you add an application to the Home-Screen of your phone, these icons are used.
#### splash
Out of the box we have provide relevant The-M-Project splash screens. If you add an application to the Home-Screen of your phone, these splash screens are used.
#### images
Put all the images inside this directory.
#### scripts
Contains the most JavaScript files - like Model, View and Controller
##### config.js
Configuration for the application.
##### main.js
Contains all controllers and is used by the generator.
##### controllers
Contains all controllers and is used by the generator.
##### views
Contains all views and is used by the generator.
##### models
Contains all models and is used by the generator.
##### collections
Contains all collections and is used by the generator.
##### layouts
Contains all layouts and is used by the generator.
### test
Default/example test for the application
### grunt.config.js
This file allows you to modify the default grunt options without a full understanding how grunt works.
- paths
- `dist` - The location for the build
- `app` - The location for the app root
- server
- `openBrowser` - Open the app in your default browser
- `autoReload` - Reloads web server you save a file in your project
- `port` - The port on which the web server will respond
- `proxies` - We use [grunt-connect-proxy](https://github.com/drewzboto/grunt-connect-proxy) for the proxy task.
- test
- `port` - The port on which the web server will respond
### Don't worry about
**package.json** - The-M-Project npm module
**bower.json** - The-M-Project Bower module
**Gruntfile.js** - Contains the configuration for the grunt tasks e.g. ```grunt server``` or ```grunt build```
**node_modules** - Contains the Node dependencies
**bower.json** - Manage the bower components
**bower_components** - Contains the Bower dependencies
## Bikini
**Bikini – everything a model needs.**
Without expense to the developer, data is synchronised from the server to the client. Changes are broadcast to all connected clients live, are available offline and changeable, and by limiting the transfer of modified records loading time and traffic can be optimised. Bikini is the connection between the Model and a Storage. It provides several adapters to access local and remote data storage.
## Model–view–controller
`M.View`, `M.Controller`, `M.Model` and `M.Collection` extending from Backbone.js. You can use them like you would use them with Backbone itself.
`M.Controller` implements `Backbone.Events` but does not extend anything else.
### inheritance
It is possible to extend from every `M` Object by calling the `extend` method. The first parameter are the options applied to the extended Object and overwrite the existing ones. `extend` always returns a function.
```javascript
M.CustomView = M.View.extend({
// overwrite a property
_type: 'M.CustomView',
// implement an own property
myOwnProperty: 1
});
```
### instances
To create an instance of an extended object you can use `new` or `create` which calls `new` by itself.
```javascript
// create an instance with new
var v = new M.View();
M.isView(v); //true
// create an instance with create
var v = M.View.create();
M.isView(v); //true
```
### M.View
`M.View.extend` accepts two parameters. The first one is for view options and the second one for child views
`M.View.create` accepts three parameters. The first one is for view options and the second one for child views and the third one to use the first one as scope.
## Layouts
A template defines the look and feel of a page. Every Controller can set its own template or use a existing one from other controllers. After the layout is set the Controller add its Views to the Layout. This triggers the render process of the inserted Views.
### Blank
A blank/empty layout.
![Blank Layout][blank-layout]
### Switch Layout
Switch through different pages with over 60 transitions
![Switch Layout][switch-layout]
### Switch Layout (Header/Content)
Switch through different pages that have a Header and Content with over 60 transitions
![Switch Layout with Header and Content][switch-header-content-layout]
## Q&A
- *Is The-M-Project Absinthe release backward compatible?*
- unfortunately not
- *What happens to The-M-Project before Absinthe?*
- We call it `The-M-Project 1.x`
- The source code is still available on [GitHub](https://github.com/mwaylabs/The-M-Project)
- We still use it on our own to build projects/products
- The [Sample-Apps](https://github.com/mwaylabs/The-M-Project-Sample-Apps/tree/1.x) aren't gone either
- [Espresso](https://github.com/mwaylabs/Espresso) is still available
- *Why Absinthe?*
- Since Google uses sweets, Apple animals and version numbers are boring we switched the release names to alcoholics ;)
- Need inspiration? B(eer), B(randy) C(ognac), C(idre), D(aiquiri), E(gg nog), F(euerzangenbowle), G(in) - the list goes on like this. So stay tuned for the upcoming releases.
## Sass Compass
### What is SASS ?
SASS (Syntactically Awesome Style Sheets) is a programming language created for front end web development that defines a new set of rules and functions to empower and enhance CSS. With this programming language, you can create complex designs with minimal code in the most efficient way possible.
### What is Compass ?
Compass is a framework for SASS, the good thing about Compass is that it comes with a lot of CSS3 mixins and useful CSS stuff.
### How to install it ?
For installing SASS Compass you need to have Ruby installed.
This is very pretty simple for MAC users because there Ruby is already installed.
If your are a MAC user you just have to type
`gem install compass` into your console.
If your are a Windows user you have to install Ruby first. The installer can be downloaded [here](http://rubyinstaller.org/downloads/).
Afterwards if you have added Ruby to your PATH variable you can also type `gem install compass` into your console to install it.
### Where can I find more information about SASS Compass ?
For more informations about SASS Compass just visit their [website](http://compass-style.org/). They have a great blog and many examples to get a good insight into it.
## Styleguide
![The-M-Project Absinthe][styleguide-image]
[blank-layout]: http://www.the-m-project.org/docs/absinthe/img/layouts/Blank.png
[switch-header-content-layout]: http://www.the-m-project.org/docs/absinthe/img/layouts/Swipe_HeaderContent.png
[switch-layout]: http://www.the-m-project.org/docs/absinthe/img/layouts/Swipe_Blank.png
[logo]: http://www.the-m-project.org/docs/absinthe/img/the-m-project-logo.png
[styleguide-image]: http://www.the-m-project.org/docs/absinthe/img/styleguide.png
[tryKitchensink]: http://www.the-m-project.org/docs/absinthe/img/try-kitchensink.png
[tryAddressbook]: http://www.the-m-project.org/docs/absinthe/img/try-addressbook.png
## Development Process
- There should be a test for every component
- add the test into the responding folder
- add the test into the test/test.html
- There is a pre-commit hook
- jshint
- testrunner - run all tests
- [Travis](https://travis-ci.org/mwaylabs/The-M-Project) is used as build server
- The generator uses [coveralls](https://coveralls.io/r/mwaylabs/generator-m?branch=master) as code analyse tool
## Setup for framework developer (Mac/Linux)
### Dependencies
- [node](http://nodejs.org/)
- [grunt](http://gruntjs.com/)
- [bower](http://bower.io/)
- [ruby](https://www.ruby-lang.org/en/)
- [compass](http://compass-style.org/)
### Checkout the repository
```
git clone https://github.com/mwaylabs/The-M-Project.git
cd The-M-Project
git checkout master
```
### init the project
The script runs the following commands/checks:
- check dependencies
- npm install
- bower install
- add git pre-commit hook
```bash
sh init-repo.sh
```
### start building the framework
```bash
grunt dev
```
### running a sample application
```bash
//navigate to a sample app
cd sample/addressbook/
//run the setup script
sh setup-dev.sh
//answer every question with y (4xy)
//start the app server
grunt server
//open your browser
open localhost:9000
```
## commit hook
- run jshint
- run tests
## test
### node
```bash
grunt test
```
### browser
```bash
open test/index.html
```
## Setup for framework developer (Windows)
### Dependencies
- [node](http://nodejs.org/)
- [grunt](http://gruntjs.com/)
- [bower](http://bower.io/)
- [ruby](https://www.ruby-lang.org/en/)
- [compass](http://compass-style.org/)
### Installation order (for newbies)
First you have to install:
- [node](http://nodejs.org/)
- [ruby](http://rubyinstaller.org/downloads/)
Add node and ruby to your PATH variable like it is described in the next section.
(If you install node and ruby with the installer you can specify with the installer to add them directly to your PATH variable )
Install the node-modules:
```bash
npm install -g grunt-cli
```
```bash
npm install -g bower
```
Add the node-modules to your PATH variable like it is described in the next section
Install the ruby-module
```bash
gem install compass
```
### Add all the Modules to your PATH Variable
#### Windows Vista & Windows 7
(If you install node and ruby with the installer you can specify to add them directly to your PATH variable)
- From the Desktop, right-click My Computer and click Properties.
- Click Advanced System Settings link in the left column.
- In the System Properties window click the Environment Variables button.
- Highlight the Path variable and click the Edit button.
Check your PATH variable for these entries. If they are not there, add them.
- ruby: ";C:\Ruby193\bin"
- node: ";C:\Program Files\nodejs"
- node-module like grunt and bower: ";C:\Users\[USERNAME]\AppData\Roaming\npm"
#### Windows 8
(You can specify in the installation of node and ruby to add them directly to your PATH variable)
- from the Desktop click the Windows key + X, then click System.
- Click Advanced System Settings link in the left column.
- In the System Properties window click the Environment Variables button.
- Highlight the Path variable and click the Edit button.
Check your PATH variable for these entries. If they are not there, add them to the end of your PATH variable.
- ruby: ";C:\Ruby193\bin"
- node: ";C:\Program Files\nodejs"
- node-module like grunt and bower: ";C:\Users\[USERNAME]\AppData\Roaming\npm"
### Checkout the repository
Go to the Directory where you want to have the Project.
```bash
git clone https://github.com/mwaylabs/The-M-Project.git
cd The-M-Project
git fetch --all
git checkout master
git pull origin master
```
### Init the project
An easy way to run the shell script in Windows is to have [msysgit](https://code.google.com/p/msysgit/downloads/list) installed. You can find the sh.exe in C:\Program Files (x86)\Git\bin.
If you add ";C:\Program Files (x86)\Git\bin" to the end of your PATH variable you can execute the following shell command.
```bash
sh init-repo.sh
```
The script runs the following commands/checks:
- check dependencies
- npm install
- bower install
- add git pre-commit hook
If the shell-script isn't working well you can also execute the important commands by hand. (Be sure to have Node and Bower installed)
```bash
npm install
```
```bash
bower install
```
### Start building the framework
```bash
grunt dev
```
### Running a sample application
//navigate to a sample app
```bash
cd sample/addressbook/
```
//run the setup script
```bash
sh setup-dev.sh
```
//answer every question with y (4xy)
//start the app server
```bash
grunt server
```
//open your browser
```bash
open localhost:9000
```
## commit hook
- run jshint
- run tests
## test
### node
```bash
grunt test
```
### browser
```bash
open test/index.html
```
exports.listen = function(server, resource) {
var io = require('socket.io').listen( server, { resource: resource } );
var bikini = {
io: io,
bindings: {},
live: io.sockets.authorization(function (handshakeData, callback) {
handshakeData.name = handshakeData.query.name;
callback(null, true);
}).on('connection', function (socket) {
socket.on('bind', function(binding) {
if (binding && binding.entity && binding.channel) {
var entity = binding.entity;
var channel = binding.channel;
bikini.bindings[channel] = binding;
// listen to this channel
socket.on(channel, function(msg, fn) {
bikini.handleMessage(entity, msg, function(data, error) {
// if the response is an object message has succeeded
if (typeof data === 'object') {
msg.data = data;
msg.time = new Date().getTime();
msg.id = data._id;
if (msg.method != 'read') {
socket.broadcast.emit(channel, msg);
}
} else if (!error) {
error = typeof data === 'string' ? data : 'error processing message!';
}
// callback to the client, send error if failed
fn(msg, error);
});
});
// send update messages, saved since time
if (binding && binding.time) {
bikini.readMessages(entity, binding.time, function(msg) {
if (msg) {
socket.emit(channel, msg);
}
});
}
}
});
}),
handleMessage: function(entity, msg, callback) {
if (msg && msg.method && msg.id && msg.data) {
if (typeof callback === 'function') {
callback(msg.data);
}
}
},
sendMessage: function(entity, msg) {
if (entity && msg && msg.method) {
for (var channel in bikini.bindings) {
if (bikini.bindings[channel].entity === entity) {
io.sockets.emit(channel, msg);
}
}
}
},
readMessages: function(entity, time, callback) {
}
};
return bikini;
};
exports.create = function(dbName) {
var mongodb = require('mongodb');
var server = new mongodb.Server("127.0.0.1", 27017, {});
var ObjectID = mongodb.ObjectID;
var rest = {
db: null,
toId: function(id, createNewIfEmpty) {
try {
if (id || createNewIfEmpty) {
return new ObjectID(id);
}
} catch (e) {
}
return id; // parseInt(id) !== NaN ? parseInt(id) : id;
},
toInt: function(s) {
try {
return parseInt(s);
} catch(e) {}
},
fromJson: function(json) {
var obj;
try {
if (json) {
obj = JSON.parse(json);
}
} catch (e) {
}
return obj || {};
},
//Find documents
find: function(req, res) {
var name = req.params.name;
var query = this.fromJson(req.query.query);
var fields = this.fromJson(req.query.fields);
var sort = this.fromJson(req.query.sort);
var limit = this.toInt(req.query.limit);
var offset = this.toInt(req.query.offset);
var collection = new mongodb.Collection(this.db, name);
var cursor = collection.find(query, fields);
if (sort) {
cursor.sort(sort);
}
if (limit) {
cursor.limit(limit);
}
if (offset) {
cursor.skip(offset);
}
cursor.toArray(function(err, docs) {
if(err){
res.send(400, err);
} else {
if (req.query.var) {
var script = req.query.var + " = " + JSON.stringify(docs) + ";";
res.send(script);
} else {
res.send(docs);
}
}
});
},
//Find a specific document
findOne: function(req, res) {
var name = req.params.name;
var id = this.toId(req.params.id);
if (typeof id === 'undefined' || id === '') {
return res.send(400, "invalid id.");
}
var collection = new mongodb.Collection(this.db, name);
collection.find({ "_id" : id }, { limit:1 }).nextObject(function(err, doc){
if(err){
res.send(400, err);
} else if (doc) {
res.send(doc);
} else {
res.send(404, 'Document not found!');
}
}
);
},
//Find a specific document
findChanges: function(req, res) {
var name = req.params.name;
var time = parseInt(req.params.time);
if (!time && time !== 0) {
return res.send(400, "invalid timestamp.");
}
var messages = [];
this.readMessages(name, time, function(message) {
if (message) {
messages.push(message);
} else {
res.send(messages);
}
})
},
//Create new document(s)
create: function(req, res, fromMessage) {
var name = req.params.name;
var doc = req.body;
// if this is an array
if (Array.isArray(doc)) {
var error, data = [];
var count = 0, len = doc.length;
if (len > 0) {
var resp = {
send: function(code, response) {
count++;
response = response || code;
if (typeof response === 'object') {
data.push(response);
} else {
error = response;
}
if (count >= len) {
if (data.length > 0) {
res.send(data);
} else {
res.send(400, error);
}
}
}
};
for (var i = 0; i < len ; i++) {
this.createOne(name, doc[i], fromMessage, resp);
}
} else {
res.send(data);
}
} else {
this.createOne(name, doc, fromMessage, res);
}
},
//Create new document(s)
createOne: function(name, doc, fromMessage, res) {
var id = this.toId(doc._id, true);
if (typeof id === 'undefined' || id === '') {
return res.send(400, "invalid id.");
}
doc._id = id;
doc._time = new Date().getTime();
var collection = new mongodb.Collection(this.db, name);
collection.insert(
doc,
{safe:true},
function(err, docs) {
if(err) {
res.send(400, err);
} else {
var doc = docs && docs.length > 0 ? docs[0] : null;
if (doc) {
var msg = {
method: 'create',
id: doc._id,
time: doc._time,
data: doc
};
rest.onSuccess(name, msg, fromMessage);
res.send(doc);
} else {
res.send(400, 'failed to create document.');
}
}
}
);
},
//Update a document
update: function(req, res, fromMessage) {
var name = req.params.name;
var doc = req.body;
var id = this.toId(req.params.id || doc._id);
if (typeof id === 'undefined' || id === '') {
return res.send(400, "invalid id.");
}
doc._id = id;
doc._time = new Date().getTime();
var collection = new mongodb.Collection(this.db, name);
collection.update(
{ "_id" : id },
doc,
{ safe:true, upsert:true },
function(err, n) {
if(err) {
res.send("Oops!: " + err);
} else {
if (n==0) {
res.send(404, 'Document not found!');
} else {
var msg = {
method: 'update',
id: doc._id,
time: doc._time,
data: doc
};
rest.onSuccess(name, msg, fromMessage);
res.send(doc);
}
}
}
);
},
//Delete a contact
delete: function(req, res, fromMessage) {
var name = req.params.name;
var id = this.toId(req.params.id);
if (typeof id === 'undefined' || id === '') {
return res.send(400, "invalid id.");
}
var doc = {};
doc._id = id;
doc._time = new Date().getTime();
var collection = new mongodb.Collection(this.db, name);
if (id === 'all' || id === 'clean') {
collection.drop(function (err) {
if(err) {
res.send(400, err);
} else {
var msg = {
method: 'delete',
id: doc._id,
time: doc._time
};
rest.onSuccess(name, msg, fromMessage);
res.send(doc);
}
});
} else {
collection.remove({ "_id" : id }, { }, function(err, n){
if(err) {
res.send(400, err);
} else {
if (n==0) {
res.send(404, 'Document not found!');
}
if (n > 0) {
var msg = {
method: 'delete',
id: doc._id,
time: doc._time,
data: doc
};
rest.onSuccess(name, msg, fromMessage);
res.send(doc);
}
}
}
);
}
},
//save a new change message
saveMessage: function(entity, msg) {
if (!msg._id) {
msg._id = new ObjectID();
}
var collection = new mongodb.Collection(this.db, "__msg__" + entity);
if (msg.method === 'delete' && (msg.id === 'all' || msg.id === 'clean')) {
collection.remove(function () {
if (msg.id === 'all') {
collection.insert(msg, { safe: false } );
}
});
} else {
collection.insert(msg, { safe: false } );
}
},
//read the change message, since time
readMessages: function(entity, time, callback) {
var collection = new mongodb.Collection(this.db, "__msg__" + entity);
time = parseInt(time);
if (time || time === 0) {
collection.ensureIndex(
{ time: 1, id: 1 },
{ unique:true, background:true, w:1 },
function(err, indexName) {
var cursor = collection.find({ time: { $gt: time } });
var id, lastMsg;
cursor.sort([['id', 1], ['time', 1]]).each(function(err, msg) {
if (msg && msg.id) {
// the same id, merge document
if (id && id.equals(msg.id)) {
if (lastMsg) {
msg = rest.mergeMessage(lastMsg, msg);
}
} else if (lastMsg) {
// send the document to the client
callback(lastMsg);
}
lastMsg = msg;
id = msg.id;
} else {
if (lastMsg) {
callback(lastMsg);
}
callback(null);
}
});
}
);
} else {
callback(null);
}
},
mergeMessage: function(doc1, doc2) {
doc1 = doc1 || {};
if (doc2) {
doc1.id = doc2.id;
doc1.time = doc2.time;
if (doc2.method && (doc1.method !== 'create' || doc2.method === 'delete')) {
doc1.method = doc2.method;
}
doc1.data = doc1.data || {};
if (doc2.data) {
for (var key in doc2.data) {
doc1.data[key] = doc2.data[key];
}
}
}
return doc1;
},
onSuccess: function(entity, msg, fromMessage) {
this.saveMessage(entity, msg);
if (!fromMessage) {
this.sendMessage(entity, msg);
}
},
sendMessage: function(entity, msg) {
},
handleMessage: function(entity, msg, callback) {
if (entity && msg && msg.method) {
var req = {
params: { name: entity, id: msg.id },
query: {},
body: msg.data
};
var resp = {
send: function(data, error) {
if (typeof callback === 'function') {
callback(data, error);
}
}
};
switch(msg.method) {
case 'create':
this.create(req, resp, true);
break;
case 'update':
this.update(req, resp, true);
break;
case 'patch':
this.update(req, resp, true);
break;
case 'delete':
this.delete(req, resp, true);
break;
case 'read':
if (msg.id) {
this.findOne(req, resp, true);
} else {
this.find(req, resp, true);
}
break;
default:
return;
}
}
}
};
new mongodb.Db(dbName, server, {w: 1}).open(function (error, client) {
if (error) throw error;
rest.db = client;
});
return rest;
};
<html>
<body>
<h1>The-M-Project 2.0 - node Test Server</h1>
</body>
</html>
var fs = require('fs');
var http = require('http');
var express = require('express');
var handlebars = require('handlebars');
var liveDbMongo = require('livedb-mongo');
var redis = require('redis').createClient();
var racerBrowserChannel = require('racer-browserchannel');
var racer = require('racer');
redis.select(14);
var store = racer.createStore({
db: liveDbMongo('localhost:27017/racer-pad?auto_reconnect', {safe: true})
, redis: redis
});
app = express();
app
.use(express.favicon())
.use(express.compress())
.use(racerBrowserChannel(store))
.use(store.modelMiddleware())
.use(app.router)
app.use(function(err, req, res, next) {
console.error(err.stack || (new Error(err)).stack);
res.send(500, 'Something broke!');
});
function scriptBundle(cb) {
// Use Browserify to generate a script file containing all of the client-side
// scripts, Racer, and BrowserChannel
store.bundle(__dirname + '/client.js', function(err, js) {
if (err) return cb(err);
cb(null, js);
});
}
// Immediately cache the result of the bundling in production mode, which is
// deteremined by the NODE_ENV environment variable. In development, the bundle
// will be recreated on every page refresh
if (racer.util.isProduction) {
scriptBundle(function(err, js) {
if (err) return;
scriptBundle = function(cb) {
cb(null, js);
};
});
}
app.get('/script.js', function(req, res, next) {
scriptBundle(function(err, js) {
if (err) return next(err);
res.type('js');
res.send(js);
});
});
var indexTemplate = fs.readFileSync(__dirname + '/index.handlebars', 'utf-8');
var indexPage = handlebars.compile(indexTemplate);
app.get('/:roomId', function(req, res, next) {
var model = req.getModel();
// Only handle URLs that use alphanumberic characters, underscores, and dashes
if (!/^[a-zA-Z0-9_-]+$/.test(req.params.roomId)) return next();
// Prevent the browser from storing the HTML response in its back cache, since
// that will cause it to render with the data from the initial load first
res.setHeader('Cache-Control', 'no-store');
var roomPath = 'rooms.' + req.params.roomId;
model.subscribe(roomPath, function(err) {
if (err) return next(err);
model.ref('_page.room', roomPath);
model.bundle(function(err, bundle) {
if (err) return next(err);
var html = indexPage({
room: req.params.roomId
, text: model.get(roomPath)
// Escape bundle for use in an HTML attribute in single quotes, since
// JSON will have lots of double quotes
, bundle: JSON.stringify(bundle).replace(/'/g, '&#39;')
});
res.send(html);
});
});
});
app.get('/', function(req, res) {
res.redirect('/home');
});
var port = process.env.PORT || 3000;
http.createServer(app).listen(port, function() {
console.log('Go to http://localhost:' + port);
});
var exec = require('child_process').exec;
//start mongodb
exec('mongod run --config /usr/local/etc/mongod.conf', function(){});
var express = require('express');
var server = express();
var http_server = require('http').createServer(server);
var socketPath = '/bikini/live';
var sockets = require('./bikini_live.js').listen(http_server, socketPath);
var rest = require('./mongodb_rest.js').create("test");
var PORT = 8200;
var path = "/bikini";
http_server.listen(PORT);
console.log('http://127.0.0.1:' + PORT);
server.use(express.static(__dirname + '/public/'));
server.use(express.static(__dirname + '/../framework/'));
server.use(express.bodyParser());
/* Allow Access-Control-Allow-Origin to everyone */
server.use(function(req, res, next) {
var oneof = false;
if(req.headers.origin) {
res.header('Access-Control-Allow-Origin', req.headers.origin);
oneof = true;
}
if(req.headers['access-control-request-method']) {
res.header('Access-Control-Allow-Methods', req.headers['access-control-request-method']);
oneof = true;
}
if(req.headers['access-control-request-headers']) {
res.header('Access-Control-Allow-Headers', req.headers['access-control-request-headers']);
oneof = true;
}
if(oneof) {
res.header('Access-Control-Max-Age', 60 * 60 * 24 * 365);
}
// intercept OPTIONS method
if (oneof && req.method == 'OPTIONS') {
res.send(200);
}
else {
next();
}
});
//Find documents
server.get(path+"/:name/info", function(req, res) {
res.send({
time: new Date().getTime(),
socketPath: socketPath,
entity: req.params.name
});
});
//Find documents
server.get(path+"/:name", function(req, res) {
rest.find(req, res);
});
//Find a specific document
server.get(path+'/:name/:id', function(req, res) {
rest.findOne(req, res);
});
//Find a changes since given time
server.get(path+'/:name/changes/:time', function(req, res) {
rest.findChanges(req, res);
});
//Create document(s)
server.post(path+'/:name', function(req, res) {
rest.create(req, res);
});
//Update a document
server.put(path+'/:name/:id', function(req, res) {
rest.update(req, res);
});
//Delete a document
server.delete(path+'/:name/:id', function(req, res){
rest.delete(req, res);
});
// overriden functions
rest.sendMessage = function(entity, msg) {
rest.saveMessage(entity, msg);
sockets.sendMessage(entity, msg);
};
sockets.handleMessage = function(entity, msg, callback) {
if (msg && msg.method) {
return rest.handleMessage(entity, msg, callback);
}
};
sockets.readMessages = function(entity, time, callback) {
rest.readMessages(entity, time, callback);
};
var express = require('express');
var server = express();
var http_server = require('http').createServer(server);
sharejs = require('share');
http_server.listen(8100);
server.use(express.static(__dirname + '/public/'));
var options = {db: {type: 'none'}}; // See docs for options. {type: 'redis'} to enable persistance.
/*
var options = {
db: { type: 'couchdb' },
browserChannel: { cors: '*' },
auth: function(client, action) {
// This auth handler rejects any ops bound for docs starting with 'readonly'.
if (action.name === 'submit op' && action.docName.match(/^readonly/)) {
action.reject();
} else {
action.accept();
}
}
};
*/
console.log("ShareJS v" + sharejs.version);
console.log("Options: ", options);
// Attach the sharejs REST and Socket.io interfaces to the server
sharejs.server.attach(server, options);
exports.listen = function(server) {
var io = require('socket.io').listen(server);
// var RedisStore = require('socket.io/lib/stores/redis')
// , redis = require('socket.io/node_modules/redis')
// , pub = redis.createClient()
// , sub = redis.createClient()
// , client = redis.createClient();
//
// io.set('store', new RedisStore({
// redisPub : pub,
// redisSub : sub,
// redisClient : client
// }));
// var MongoStore = require('socket.io-mongo');
// io.configure(function() {
// var store = new MongoStore({ url: 'mongodb://localhost:27017/test' });
// store.on('error', console.error);
// io.set('store', store);
// });
var sockets = {
io: io,
chatMessages: [],
test: io.of('/test').on('connection', function (socket) {
socket.emit('connected', { hello: 'world' });
socket.on('news', function (data) {
console.log(data);
socket.emit('news', { hello: data });
});
}),
chat: io.of('/chat').authorization(function (handshakeData, callback) {
handshakeData.name = handshakeData.query.name;
callback(null, true);
}).on('connection', function (socket) {
var name = socket.handshake.name || 'Nobody';
socket.emit('connected', socket.id);
for (var i=0; i<sockets.chatMessages.length; i++) {
socket.emit('message', sockets.chatMessages[i]);
}
var time = new Date().getTime();
// tell all others, there is a new user
socket.broadcast.emit('new-user', { name: name, time: time });
// send new messages to everyone in "/chat", including the sender
socket.on('message', function(data) {
sockets.chatMessages.push(data);
if (sockets.chatMessages.length > 20) {
sockets.chatMessages.shift();
}
sockets.chat.emit('message', data);
});
}),
liveData: io.of('/live_data').authorization(function (handshakeData, callback) {
handshakeData.name = handshakeData.query.name;
callback(null, true);
}).on('connection', function (socket) {
socket.on('bind', function(data) {
var entity = typeof data === 'object' ? data.entity : data;
if (entity && typeof entity === 'string') {
var channel = 'entity_' + entity;
// listen to this channel
socket.on(channel, function(msg, fn) {
sockets.handleMessage(entity, msg, function(data, error) {
// if the response is an object message has succeeded
if (typeof data === 'object') {
msg.data = data;
msg.time = new Date().getTime();
msg.id = data._id;
if (msg.method != 'read') {
socket.broadcast.emit(channel, msg);
}
} else if (!error) {
error = typeof data === 'string' ? data : 'error processing message!';
}
// callback to the client, send error if failed
fn(msg, error);
});
});
// send update messages, saved since time
if (data && data.time) {
sockets.readMessages(entity, data.time, function(msg) {
if (msg) {
socket.emit(channel, msg);
}
});
}
}
});
}),
handleMessage: function(entity, msg, callback) {
if (msg && msg.method && msg.id && msg.data) {
if (typeof callback === 'function') {
callback(msg.data);
}
}
},
sendMessage: function(entity, msg) {
var channel = 'entity_' + entity;
if (msg && msg.method) {
this.liveData.emit(channel, msg);
}
},
readMessages: function(entity, time, callback) {
}
};
return sockets;
};