/*-------------------------------------------------------------------------- * * Key Oriented JSON Application Cache (KOJAC) * (c) 2011-12 Buzzware Solutions * https://github.com/buzzware/KOJAC * * KOJAC is freely distributable under the terms of an MIT-style license. * *--------------------------------------------------------------------------*/ Kojac = {}; /** * @class Kojac.Object * * Based on : * Simple JavaScript Inheritance * By John Resig http://ejohn.org/blog/simple-javascript-inheritance/ * MIT Licensed. * * Inspired by base2 and Prototype * * added setup method support inspired by CanJs as used in Kojac.Model * */ (function(){ var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; // The base JrClass implementation (does nothing) this.JrClass = function(aProperties){ if (initializing) { // making prototype if (aProperties) { _.extend(this,aProperties); _.cloneComplexValues(this); } } else { // making instance _.cloneComplexValues(this); if (this.init) this.init.call(this,aProperties); } }; this.JrClass.prototype.init = function(aProperties) { _.extend(this,aProperties); }; this.JrClass.prototype.toJSON = function() { // this adds an instance method used by JSON2 that returns an object containing all immediate and background properties (ie from the prototype) return _.clone(this); }; // Create a new JrClass that inherits from this class this.JrClass.extend = function(prop) { var _super = this.prototype; // Instantiate a base class (but only create the instance, // don't run the init constructor) initializing = true; var prototype = new this(); initializing = false; // The dummy class constructor function JrClass(aProperties) { if (initializing) { // making prototype if (aProperties) { _.extend(this,aProperties); _.cloneComplexValues(this); } } else { // making instance _.cloneComplexValues(this); if (this.init) this.init.call(this,aProperties); } } JrClass._superClass = this; if (_super.setup) prop = _super.setup.call(JrClass,prop); // Copy the properties over onto the new prototype for (var name in prop) { // Check if we're overwriting an existing function var t = typeof prop[name];//_.typeOf(prop[name]); if (t == "function" && typeof _super[name] == "function" && fnTest.test(prop[name])) { prototype[name] = (function(name, fn){ return function() { var tmp = this._super; // Add a new ._super() method that is the same method // but on the super-class this._super = _super[name]; // The method only need to be bound temporarily, so we // remove it when we're done executing var ret = fn.apply(this, arguments); this._super = tmp; return ret; }; })(name, prop[name]); } else if (t==='array' || t==='object') { prototype[name] = _.clone(prop[name]); } else { prototype[name] = prop[name]; } } // Populate our constructed prototype object JrClass.prototype = prototype; // Enforce the constructor to be what we expect JrClass.prototype.constructor = JrClass; // And make this class extendable JrClass.extend = arguments.callee; return JrClass; }; Kojac.Object = this.JrClass; })(); /* * @class Kojac.Utils * * Provides static functions used by Kojac */ Kojac.Utils = { /** * Converts one or more keys, given in multiple possible ways, to a standard array of strings * @param aKeys one or more keys eg. as array of strings, or single comma-separated list in a single string * @return {Array} array of single-key strings */ interpretKeys: function(aKeys) { if (_.isArray(aKeys)) return aKeys; if (_.isString(aKeys)) return aKeys.split(','); return []; }, /** * Convert object or array to [key1, value, key2, value] * @param aKeyValues array or object of keys with values * @return {Array} [key1, value, key2, value] */ toKeyValueArray: function(aKeyValues) { if (_.isArray(aKeyValues)) { var first = aKeyValues[0]; if (_.isArray(first)) // this style : [[key,value],[key,value]] return _.map(aKeyValues,function(o){ return _.flatten(o,true) }); else if (_.isObject(first)) { // this style : [{key: value},{key: value}] var result = []; for (var i=0; i=1) // resource r = parts[0]; else return []; var result = [r]; if (parts.length<2) return result; ia = parts[1]; parts = ia.split('.'); if (parts.length>=1) { // id id = parts[0]; var id_as_i = Number(id); if (_.isFinite(id_as_i)) id = id_as_i; result.push(id); } if (parts.length>=2) { // association result.push(parts[1]); } return result; } keyResource = function(aKey) { var parts = aKey.split('__'); return parts[0]; } keyId = function(aKey) { var parts = aKey.split('__'); return parts[1]; } Int = {name: 'Int', toString: function() {return 'Int';}}; // represents a virtual integer type Null = {name: 'Null', toString: function() {return 'Null';}}; // represents a virtual Null type Kojac.FieldTypes = [Null,Int,Number,String,Boolean,Date,Array,Object]; // all possible types for fields in Kojac.Model Kojac.FieldTypeStrings = ['Null','Int','Number','String','Boolean','Date','Array','Object']; // String names for FieldTypes Kojac.SimpleTypes = [Null,Int,Number,String,Boolean,Date]; // simple field types in Kojac.Model ie. Object and Array are considered complex /** * Extends Kojac.Object to support typed attributes * @class Kojac.Model * @extends Kojac.Object **/ Kojac.Model = Kojac.Object.extend({ /** * This method is called when inheriting a new model from Kojac.Model, and allows attributes to be defined as * name: Class (default value is null) * or * name: default value (class is inferred) * or * name: [Class,default value] * @param prop Hash of attributes defined as above * @return Hash of attributes in expected name:value format */ setup: function(prop) { this.__attributes = (this._superClass && this._superClass.__attributes && _.clone(this._superClass.__attributes)) || {}; //this.__defaults = (constructor.__defaults && _.clone(constructor.__defaults)) || {}; for (var p in prop) { if (['__defaults','__attributes'].indexOf(p)>=0) continue; var propValue = prop[p]; if (_.isArray(propValue) && propValue.length===2 && Kojac.FieldTypes.indexOf(propValue[0])>=0) { // in form property: [Type, Default Value] this.__attributes[p] = propValue[0]; prop[p] = propValue[1]; } else if (Kojac.FieldTypes.indexOf(propValue) >= 0) { // field type prop[p] = null; this.__attributes[p] = propValue; //this.__defaults[p] = null; } else if (_.isFunction(propValue)) { continue; } else { // default value var i = Kojac.FieldTypes.indexOf(Kojac.getPropertyValueType(propValue)); if (i >= 0) { this.__attributes[p] = Kojac.FieldTypes[i]; } else { this.__attributes[p] = null; } //this.__defaults[p] = v; } } return prop; }, /** * The base constructor for Kojac.Model. When creating an instance of a model, an optional hash aValues provides attribute values that override the default values * @param aValues * @constructor */ init: function(aValues){ // we don't use base init here if (!aValues) return; for (var p in aValues) { if (this.isAttribute(p)) { this.attr(p,aValues[p]); } else { this[p] = aValues[p]; } } }, /** * Determines whether the given name is defined as an attribute in the model definition. Attributes are properties with an additional class and default value * @param aName * @return {Boolean} */ isAttribute: function(aName) { return this.constructor.__attributes && (aName in this.constructor.__attributes); }, /** * Used various ways to access the attributes of a model instance. * 1. attr() returns an object of all attributes and their values * 2. attr() returns the value of a given attribute * 3. attr(,) sets an attribute to the given value after converting it to the attribute's class * 4. attr({Object}) sets each of the given attributes to the given value after converting to the attribute's class * @param aName * @param aValue * @return {*} */ attr: function(aName,aValue) { if (aName===undefined) { // read all attributes return _.pick(this, _.keys(this.constructor.__attributes)); } else if (aValue===undefined) { if (_.isObject(aName)) { // write all given attributes aValue = aName; aName = undefined; if (!this.constructor.__attributes) return {}; _.extend(this,_.pick(aValue,_.keys(this.constructor.__attributes))) } else { // read single attribute return (_.has(this.constructor.__attributes,aName) && this[aName]) || undefined; } } else { // write single attribute var t = this.constructor.__attributes[aName]; if (t) aValue = Kojac.interpretValueAsType(aValue,t); return (this[aName]=aValue); } } }); /** * Provides a dynamic asynchronous execution model. Handlers are added in queue or stack style, then executed in order, passing a given context object to each handler. * HandlerStack is a Javascript conversion of HandlerStack in the ActionScript Kojac library. * @class HandlerStack * @extends Kojac.Object */ HandlerStack = Kojac.Object.extend({ handlers: null, parameters: null, thises: null, parameter: null, context: null, error: null, deferred: null, nextHandlerIndex: -1, waitForCallNext: false, /** * @constructor */ init: function() { this._super.apply(this,arguments); this.clear(); }, // clears out all handlers and state clear: function() { this.handlers = []; this.parameters = []; this.thises = []; this.reset(); }, // clears execution state but keeps handlers and parameters for a potential re-call() reset: function() { this.parameter = null; this.context = null; this.error = null; this.deferred = null; this.nextHandlerIndex = -1; this.waitForCallNext = false; }, push: function (aFunction, aParameter, aThis) { this.handlers.unshift(aFunction); this.parameters.unshift(aParameter); this.thises.unshift(aThis); }, // push in function and parameters to execute next pushNext: function(aFunction, aParameter,aThis) { if (this.nextHandlerIndex<0) return this.push(aFunction,aParameter,aThis); this.handlers.splice(this.nextHandlerIndex,0,aFunction); this.parameters.splice(this.nextHandlerIndex,0,aParameter); this.thises.splice(this.nextHandlerIndex,0,aThis); }, add: function(aFunction, aParameter, aThis) { this.handlers.push(aFunction); this.parameters.push(aParameter); this.thises.push(aThis); }, callNext: function() { if (this.context.error) { if (!this.context.isRejected()) this.deferred.reject(this.context); return; } if ((this.handlers.length===0) || (this.nextHandlerIndex>=this.handlers.length)) { this.deferred.resolve(this.context); return; } var fn = this.handlers[this.nextHandlerIndex]; var d = this.parameters[this.nextHandlerIndex]; var th = this.thises[this.nextHandlerIndex]; this.nextHandlerIndex++; var me = this; setTimeout(function() { me.executeHandler(fn, d, th); }, 0); }, handleError: function(aError) { this.context.error = aError; this.deferred.reject(this.context); }, executeHandler: function(fn,d,th) { this.waitForCallNext = false; try { this.parameter = d; if (th) fn.call(th,this.context); else fn(this.context); } catch (e) { this.handleError(e); } if (!(this.waitForCallNext)) { this.callNext(); } }, run: function(aContext) { this.context = aContext; this.deferred = jQuery.Deferred(); this.deferred.promise(this.context); if (this.context.isResolved===undefined) this.context.isResolved = _.bind( function() { return this.state()==='resolved' }, this.context ); if (this.context.isRejected===undefined) this.context.isRejected = _.bind( function() { return this.state()==='rejected' }, this.context ); if (this.context.isPending===undefined) this.context.isPending = _.bind( function() { return this.state()==='pending' }, this.context ); this.nextHandlerIndex = 0; this.callNext(); return this.context; } }); /** * Represents a single Kojac operation ie. READ, WRITE, UPDATE, DELETE or EXECUTE * @class Kojac.Operation * @extends Kojac.Object */ Kojac.Operation = Kojac.Object.extend({ request: this, verb: null, key: null, value: undefined, results: {}, result_key: null, result: undefined, error: null, // set with some truthy error if this operation fails performed: false, fromCache: null, // null means not performed, true means got from cache, false means got from server. !!! Should split this into performed and fromCache receiveResult:function (aResponseOp) { if (!aResponseOp) { this.error = "no result"; } else if (aResponseOp.error) { this.error = aResponseOp.error; } else { var request_key = this.result_key || this.key; var response_key = aResponseOp.result_key || this.key; var final_result_key = this.result_key || response_key; // result_key should not be specified unless trying to override var results = _.isObjectStrict(aResponseOp.results) ? aResponseOp.results : _.createObject(response_key,aResponseOp.results); // fix up server mistake var result; if (aResponseOp.verb==='DESTROY') result = undefined; else result = results[response_key]; results = _.omit(results,response_key); // results now excludes primary result _.extend(this.results,results); // store other results this.result_key = final_result_key; this.results[final_result_key] = result; // store primary result } } }); /** * Represents a single Kojac request, analogous to a HTTP request. It may contain 1 or more operations * @class Kojac.Request * @extends Kojac.Object */ Kojac.Request = Kojac.Object.extend({ kojac: null, options: {}, ops: [], handlers: null, op: null, result: undefined, results: {}, error: null, // set with some truthy value if this whole request or any operation fails (will contain first error if multiple) newOperation: function() { var obj = new Kojac.Operation({request: this}); if (this.ops.length===0) this.op = obj; this.ops.push(obj); return obj; }, init: function(aProperties) { this._super.apply(this,arguments); this.handlers = new HandlerStack(); }, // {key: value} or [{key1: value},{key2: value}] or {key1: value, key2: value} // Can give existing keys with id, and will create a clone in database with a new id create: function(aKeyValues,aOptions) { var result_key = aOptions && _.removeKey(aOptions,'result_key'); var params = aOptions && _.removeKey(aOptions,'params'); // extract specific params var options = aOptions ? _.extend({cacheResults: true},aOptions) : {}; // extract known options var kvArray = Kojac.Utils.toKeyValueArray(aKeyValues); for (var i=0;i= 3) op.key = k; else op.key = keyResource(k); if ((i===0) && result_key) op.result_key = result_key; op.value = v; } return this; }, // !!! if aKeys is String, split on ',' into an array // known options will be moved from aOptions to op.options; remaining keys will be put into params read: function(aKeys,aOptions) { var keys = Kojac.Utils.interpretKeys(aKeys); var result_key = aOptions && _.removeKey(aOptions,'result_key'); // extract result_key var params = aOptions && _.removeKey(aOptions,'params'); // extract specific params var options = aOptions ? _.extend({cacheResults: true},aOptions) : {}; // extract known options var me = this; jQuery.each(keys,function(i,k) { var op = me.newOperation(); op.options = _.clone(options); op.params = params && _.clone(params); op.verb = 'READ'; op.key = k; if (i===0) op.result_key = result_key || k; else op.result_key = k; }); return this; }, cacheRead: function(aKeys,aOptions) { aOptions = _.extend({},aOptions,{preferCache: true}); return this.read(aKeys,aOptions); }, update: function(aKeyValues,aOptions) { var result_key = aOptions && _.removeKey(aOptions,'result_key'); var options = aOptions ? _.extend({cacheResults: true},aOptions) : {}; // extract known options var params = aOptions && _.removeKey(aOptions,'params'); // extract specific params var first=true; var kvArray = Kojac.Utils.toKeyValueArray(aKeyValues); for (var i=0;i