UNPKG

generator-steroids

Version:
1,415 lines (1,300 loc) 82.1 kB
/** * Copyright (c) 2010 Zef Hemel <zef@zef.me> * * 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. */ if (typeof exports !== 'undefined') { exports.createPersistence = function() { return initPersistence({}) } var singleton; if (typeof (exports.__defineGetter__) === 'function') { exports.__defineGetter__("persistence", function () { if (!singleton) singleton = exports.createPersistence(); return singleton; }); } else { Object.defineProperty(exports, "persistence", { get: function () { if (!singleton) singleton = exports.createPersistence(); return singleton; }, enumerable: true, configurable: true }); } } else { window = window || {}; window.persistence = initPersistence(window.persistence || {}); } function initPersistence(persistence) { if (persistence.isImmutable) // already initialized return persistence; /** * Check for immutable fields */ persistence.isImmutable = function(fieldName) { return (fieldName == "id"); }; /** * Default implementation for entity-property */ persistence.defineProp = function(scope, field, setterCallback, getterCallback) { if (typeof (scope.__defineSetter__) === 'function' && typeof (scope.__defineGetter__) === 'function') { scope.__defineSetter__(field, function (value) { setterCallback(value); }); scope.__defineGetter__(field, function () { return getterCallback(); }); } else { Object.defineProperty(scope, field, { get: getterCallback, set: function (value) { setterCallback(value); }, enumerable: true, configurable: true }); } }; /** * Default implementation for entity-property setter */ persistence.set = function(scope, fieldName, value) { if (persistence.isImmutable(fieldName)) throw new Error("immutable field: "+fieldName); scope[fieldName] = value; }; /** * Default implementation for entity-property getter */ persistence.get = function(arg1, arg2) { return (arguments.length == 1) ? arg1 : arg1[arg2]; }; (function () { var entityMeta = {}; var entityClassCache = {}; persistence.getEntityMeta = function() { return entityMeta; } // Per-session data persistence.trackedObjects = {}; persistence.objectsToRemove = {}; persistence.objectsRemoved = []; // {id: ..., type: ...} persistence.globalPropertyListeners = {}; // EntityType__prop -> QueryColleciton obj persistence.queryCollectionCache = {}; // entityName -> uniqueString -> QueryCollection persistence.getObjectsToRemove = function() { return this.objectsToRemove; }; persistence.getTrackedObjects = function() { return this.trackedObjects; }; // Public Extension hooks persistence.entityDecoratorHooks = []; persistence.flushHooks = []; persistence.schemaSyncHooks = []; // Enable debugging (display queries using console.log etc) persistence.debug = true; persistence.subscribeToGlobalPropertyListener = function(coll, entityName, property) { var key = entityName + '__' + property; if(key in this.globalPropertyListeners) { var listeners = this.globalPropertyListeners[key]; for(var i = 0; i < listeners.length; i++) { if(listeners[i] === coll) { return; } } this.globalPropertyListeners[key].push(coll); } else { this.globalPropertyListeners[key] = [coll]; } } persistence.unsubscribeFromGlobalPropertyListener = function(coll, entityName, property) { var key = entityName + '__' + property; var listeners = this.globalPropertyListeners[key]; for(var i = 0; i < listeners.length; i++) { if(listeners[i] === coll) { listeners.splice(i, 1); return; } } } persistence.propertyChanged = function(obj, property, oldValue, newValue) { if(!this.trackedObjects[obj.id]) return; // not yet added, ignore for now var entityName = obj._type; var key = entityName + '__' + property; if(key in this.globalPropertyListeners) { var listeners = this.globalPropertyListeners[key]; for(var i = 0; i < listeners.length; i++) { var coll = listeners[i]; var dummyObj = obj._data; dummyObj[property] = oldValue; var matchedBefore = coll._filter.match(dummyObj); dummyObj[property] = newValue; var matchedAfter = coll._filter.match(dummyObj); if(matchedBefore != matchedAfter) { coll.triggerEvent('change', coll, obj); } } } } persistence.objectRemoved = function(obj) { var entityName = obj._type; if(this.queryCollectionCache[entityName]) { var colls = this.queryCollectionCache[entityName]; for(var key in colls) { if(colls.hasOwnProperty(key)) { var coll = colls[key]; if(coll._filter.match(obj)) { // matched the filter -> was part of collection coll.triggerEvent('change', coll, obj); } } } } } /** * Retrieves metadata about entity, mostly for internal use */ function getMeta(entityName) { return entityMeta[entityName]; } persistence.getMeta = getMeta; /** * A database session */ function Session(conn) { this.trackedObjects = {}; this.objectsToRemove = {}; this.objectsRemoved = []; this.globalPropertyListeners = {}; // EntityType__prop -> QueryColleciton obj this.queryCollectionCache = {}; // entityName -> uniqueString -> QueryCollection this.conn = conn; } Session.prototype = persistence; // Inherit everything from the root persistence object persistence.Session = Session; /** * Define an entity * * @param entityName * the name of the entity (also the table name in the database) * @param fields * an object with property names as keys and SQLite types as * values, e.g. {name: "TEXT", age: "INT"} * @return the entity's constructor */ persistence.define = function (entityName, fields) { if (entityMeta[entityName]) { // Already defined, ignore return getEntity(entityName); } var meta = { name: entityName, fields: fields, isMixin: false, indexes: [], hasMany: {}, hasOne: {} }; entityMeta[entityName] = meta; return getEntity(entityName); }; /** * Checks whether an entity exists * * @param entityName * the name of the entity (also the table name in the database) * @return `true` if the entity exists, otherwise `false` */ persistence.isDefined = function (entityName) { return !!entityMeta[entityName]; } /** * Define a mixin * * @param mixinName * the name of the mixin * @param fields * an object with property names as keys and SQLite types as * values, e.g. {name: "TEXT", age: "INT"} * @return the entity's constructor */ persistence.defineMixin = function (mixinName, fields) { var Entity = this.define(mixinName, fields); Entity.meta.isMixin = true; return Entity; }; persistence.isTransaction = function(obj) { return !obj || (obj && obj.executeSql); }; persistence.isSession = function(obj) { return !obj || (obj && obj.schemaSync); }; /** * Adds the object to tracked entities to be persisted * * @param obj * the object to be tracked */ persistence.add = function (obj) { if(!obj) return; if (!this.trackedObjects[obj.id]) { this.trackedObjects[obj.id] = obj; if(obj._new) { for(var p in obj._data) { if(obj._data.hasOwnProperty(p)) { this.propertyChanged(obj, p, undefined, obj._data[p]); } } } } return this; }; /** * Marks the object to be removed (on next flush) * @param obj object to be removed */ persistence.remove = function(obj) { if (obj._new) { delete this.trackedObjects[obj.id]; } else { if (!this.objectsToRemove[obj.id]) { this.objectsToRemove[obj.id] = obj; } this.objectsRemoved.push({id: obj.id, entity: obj._type}); } this.objectRemoved(obj); return this; }; /** * Clean the persistence context of cached entities and such. */ persistence.clean = function () { this.trackedObjects = {}; this.objectsToRemove = {}; this.objectsRemoved = []; this.globalPropertyListeners = {}; this.queryCollectionCache = {}; }; /** * asynchronous sequential version of Array.prototype.forEach * @param array the array to iterate over * @param fn the function to apply to each item in the array, function * has two argument, the first is the item value, the second a * callback function * @param callback the function to call when the forEach has ended */ persistence.asyncForEach = function(array, fn, callback) { array = array.slice(0); // Just to be sure function processOne() { var item = array.pop(); fn(item, function(result, err) { if(array.length > 0) { processOne(); } else { callback(result, err); } }); } if(array.length > 0) { processOne(); } else { callback(); } }; /** * asynchronous parallel version of Array.prototype.forEach * @param array the array to iterate over * @param fn the function to apply to each item in the array, function * has two argument, the first is the item value, the second a * callback function * @param callback the function to call when the forEach has ended */ persistence.asyncParForEach = function(array, fn, callback) { var completed = 0; var arLength = array.length; if(arLength === 0) { callback(); } for(var i = 0; i < arLength; i++) { fn(array[i], function(result, err) { completed++; if(completed === arLength) { callback(result, err); } }); } }; /** * Retrieves or creates an entity constructor function for a given * entity name * @return the entity constructor function to be invoked with `new fn()` */ function getEntity(entityName) { if (entityClassCache[entityName]) { return entityClassCache[entityName]; } var meta = entityMeta[entityName]; /** * @constructor */ function Entity (session, obj, noEvents) { var args = argspec.getArgs(arguments, [ { name: "session", optional: true, check: persistence.isSession, defaultValue: persistence }, { name: "obj", optional: true, check: function(obj) { return obj; }, defaultValue: {} } ]); if (meta.isMixin) throw new Error("Cannot instantiate mixin"); session = args.session; obj = args.obj; var that = this; this.id = obj.id || persistence.createUUID(); this._new = true; this._type = entityName; this._dirtyProperties = {}; this._data = {}; this._data_obj = {}; // references to objects this._session = session || persistence; this.subscribers = {}; // observable for ( var field in meta.fields) { (function () { if (meta.fields.hasOwnProperty(field)) { var f = field; // Javascript scopes/closures SUCK persistence.defineProp(that, f, function(val) { // setterCallback var oldValue = that._data[f]; if(oldValue !== val || (oldValue && val && oldValue.getTime && val.getTime)) { // Don't mark properties as dirty and trigger events unnecessarily that._data[f] = val; that._dirtyProperties[f] = oldValue; that.triggerEvent('set', that, f, val); that.triggerEvent('change', that, f, val); session.propertyChanged(that, f, oldValue, val); } }, function() { // getterCallback return that._data[f]; }); that._data[field] = defaultValue(meta.fields[field]); } }()); } for ( var it in meta.hasOne) { if (meta.hasOne.hasOwnProperty(it)) { (function () { var ref = it; var mixinClass = meta.hasOne[it].type.meta.isMixin ? ref + '_class' : null; persistence.defineProp(that, ref, function(val) { // setterCallback var oldValue = that._data[ref]; var oldValueObj = that._data_obj[ref] || session.trackedObjects[that._data[ref]]; if (val == null) { that._data[ref] = null; that._data_obj[ref] = undefined; if (mixinClass) that[mixinClass] = ''; } else if (val.id) { that._data[ref] = val.id; that._data_obj[ref] = val; if (mixinClass) that[mixinClass] = val._type; session.add(val); session.add(that); } else { // let's assume it's an id that._data[ref] = val; } that._dirtyProperties[ref] = oldValue; that.triggerEvent('set', that, ref, val); that.triggerEvent('change', that, ref, val); // Inverse if(meta.hasOne[ref].inverseProperty) { var newVal = that[ref]; if(newVal) { var inverse = newVal[meta.hasOne[ref].inverseProperty]; if(inverse.list && inverse._filter) { inverse.triggerEvent('change', that, ref, val); } } if(oldValueObj) { console.log("OldValue", oldValueObj); var inverse = oldValueObj[meta.hasOne[ref].inverseProperty]; if(inverse.list && inverse._filter) { inverse.triggerEvent('change', that, ref, val); } } } }, function() { // getterCallback if (!that._data[ref]) { return null; } else if(that._data_obj[ref] !== undefined) { return that._data_obj[ref]; } else if(that._data[ref] && session.trackedObjects[that._data[ref]]) { that._data_obj[ref] = session.trackedObjects[that._data[ref]]; return that._data_obj[ref]; } else { throw new Error("Property '" + ref + "' of '" + meta.name + "' with id: " + that._data[ref] + " not fetched, either prefetch it or fetch it manually."); } }); }()); } } for ( var it in meta.hasMany) { if (meta.hasMany.hasOwnProperty(it)) { (function () { var coll = it; if (meta.hasMany[coll].manyToMany) { persistence.defineProp(that, coll, function(val) { // setterCallback if(val && val._items) { // Local query collection, just add each item // TODO: this is technically not correct, should clear out existing items too var items = val._items; for(var i = 0; i < items.length; i++) { persistence.get(that, coll).add(items[i]); } } else { throw new Error("Not yet supported."); } }, function() { // getterCallback if (that._data[coll]) { return that._data[coll]; } else { var rel = meta.hasMany[coll]; var inverseMeta = rel.type.meta; var inv = inverseMeta.hasMany[rel.inverseProperty]; var direct = rel.mixin ? rel.mixin.meta.name : meta.name; var inverse = inv.mixin ? inv.mixin.meta.name : inverseMeta.name; var queryColl = new persistence.ManyToManyDbQueryCollection(session, inverseMeta.name); queryColl.initManyToMany(that, coll); queryColl._manyToManyFetch = { table: rel.tableName, prop: direct + '_' + coll, inverseProp: inverse + '_' + rel.inverseProperty, id: that.id }; that._data[coll] = queryColl; return session.uniqueQueryCollection(queryColl); } }); } else { // one to many persistence.defineProp(that, coll, function(val) { // setterCallback if(val && val._items) { // Local query collection, just add each item // TODO: this is technically not correct, should clear out existing items too var items = val._items; for(var i = 0; i < items.length; i++) { persistence.get(that, coll).add(items[i]); } } else { throw new Error("Not yet supported."); } }, function() { // getterCallback if (that._data[coll]) { return that._data[coll]; } else { var queryColl = session.uniqueQueryCollection(new persistence.DbQueryCollection(session, meta.hasMany[coll].type.meta.name).filter(meta.hasMany[coll].inverseProperty, '=', that)); that._data[coll] = queryColl; return queryColl; } }); } }()); } } if(this.initialize) { this.initialize(); } for ( var f in obj) { if (obj.hasOwnProperty(f)) { if(f !== 'id') { persistence.set(that, f, obj[f]); } } } } // Entity Entity.prototype = new Observable(); Entity.meta = meta; Entity.prototype.equals = function(other) { return this.id == other.id; }; Entity.prototype.toJSON = function() { var json = {id: this.id}; for(var p in this._data) { if(this._data.hasOwnProperty(p)) { if (typeof this._data[p] == "object" && this._data[p] != null) { if (this._data[p].toJSON != undefined) { json[p] = this._data[p].toJSON(); } } else { json[p] = this._data[p]; } } } return json; }; /** * Select a subset of data as a JSON structure (Javascript object) * * A property specification is passed that selects the * properties to be part of the resulting JSON object. Examples: * ['id', 'name'] -> Will return an object with the id and name property of this entity * ['*'] -> Will return an object with all the properties of this entity, not recursive * ['project.name'] -> will return an object with a project property which has a name * property containing the project name (hasOne relationship) * ['project.[id, name]'] -> will return an object with a project property which has an * id and name property containing the project name * (hasOne relationship) * ['tags.name'] -> will return an object with an array `tags` property containing * objects each with a single property: name * * @param tx database transaction to use, leave out to start a new one * @param props a property specification * @param callback(result) */ Entity.prototype.selectJSON = function(tx, props, callback) { var that = this; var args = argspec.getArgs(arguments, [ { name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null }, { name: "props", optional: false }, { name: "callback", optional: false } ]); tx = args.tx; props = args.props; callback = args.callback; if(!tx) { this._session.transaction(function(tx) { that.selectJSON(tx, props, callback); }); return; } var includeProperties = {}; props.forEach(function(prop) { var current = includeProperties; var parts = prop.split('.'); for(var i = 0; i < parts.length; i++) { var part = parts[i]; if(i === parts.length-1) { if(part === '*') { current.id = true; for(var p in meta.fields) { if(meta.fields.hasOwnProperty(p)) { current[p] = true; } } for(var p in meta.hasOne) { if(meta.hasOne.hasOwnProperty(p)) { current[p] = true; } } for(var p in meta.hasMany) { if(meta.hasMany.hasOwnProperty(p)) { current[p] = true; } } } else if(part[0] === '[') { part = part.substring(1, part.length-1); var propList = part.split(/,\s*/); propList.forEach(function(prop) { current[prop] = true; }); } else { current[part] = true; } } else { current[part] = current[part] || {}; current = current[part]; } } }); buildJSON(this, tx, includeProperties, callback); }; function buildJSON(that, tx, includeProperties, callback) { var session = that._session; var properties = []; var meta = getMeta(that._type); var fieldSpec = meta.fields; for(var p in includeProperties) { if(includeProperties.hasOwnProperty(p)) { properties.push(p); } } var cheapProperties = []; var expensiveProperties = []; properties.forEach(function(p) { if(includeProperties[p] === true && !meta.hasMany[p]) { // simple, loaded field cheapProperties.push(p); } else { expensiveProperties.push(p); } }); var itemData = that._data; var item = {}; cheapProperties.forEach(function(p) { if(p === 'id') { item.id = that.id; } else if(meta.hasOne[p]) { item[p] = itemData[p] ? {id: itemData[p]} : null; } else { item[p] = persistence.entityValToJson(itemData[p], fieldSpec[p]); } }); properties = expensiveProperties.slice(); persistence.asyncForEach(properties, function(p, callback) { if(meta.hasOne[p]) { that.fetch(tx, p, function(obj) { if(obj) { buildJSON(obj, tx, includeProperties[p], function(result) { item[p] = result; callback(); }); } else { item[p] = null; callback(); } }); } else if(meta.hasMany[p]) { persistence.get(that, p).list(function(objs) { item[p] = []; persistence.asyncForEach(objs, function(obj, callback) { var obj = objs.pop(); if(includeProperties[p] === true) { item[p].push({id: obj.id}); callback(); } else { buildJSON(obj, tx, includeProperties[p], function(result) { item[p].push(result); callback(); }); } }, callback); }); } }, function() { callback(item); }); }; // End of buildJson Entity.prototype.fetch = function(tx, rel, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'rel', optional: false, check: argspec.hasType('string') }, { name: 'callback', optional: false, check: argspec.isCallback() } ]); tx = args.tx; rel = args.rel; callback = args.callback; var that = this; var session = this._session; if(!tx) { session.transaction(function(tx) { that.fetch(tx, rel, callback); }); return; } if(!this._data[rel]) { // null if(callback) { callback(null); } } else if(this._data_obj[rel]) { // already loaded if(callback) { callback(this._data_obj[rel]); } } else { var type = meta.hasOne[rel].type; if (type.meta.isMixin) { type = getEntity(this._data[rel + '_class']); } type.load(session, tx, this._data[rel], function(obj) { that._data_obj[rel] = obj; if(callback) { callback(obj); } }); } }; /** * Currently this is only required when changing JSON properties */ Entity.prototype.markDirty = function(prop) { this._dirtyProperties[prop] = true; }; /** * Returns a QueryCollection implementation matching all instances * of this entity in the database */ Entity.all = function(session) { var args = argspec.getArgs(arguments, [ { name: 'session', optional: true, check: persistence.isSession, defaultValue: persistence } ]); session = args.session; return session.uniqueQueryCollection(new AllDbQueryCollection(session, entityName)); }; Entity.fromSelectJSON = function(session, tx, jsonObj, callback) { var args = argspec.getArgs(arguments, [ { name: 'session', optional: true, check: persistence.isSession, defaultValue: persistence }, { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'jsonObj', optional: false }, { name: 'callback', optional: false, check: argspec.isCallback() } ]); session = args.session; tx = args.tx; jsonObj = args.jsonObj; callback = args.callback; if(!tx) { session.transaction(function(tx) { Entity.fromSelectJSON(session, tx, jsonObj, callback); }); return; } if(typeof jsonObj === 'string') { jsonObj = JSON.parse(jsonObj); } if(!jsonObj) { callback(null); return; } function loadedObj(obj) { if(!obj) { obj = new Entity(session); if(jsonObj.id) { obj.id = jsonObj.id; } } session.add(obj); var expensiveProperties = []; for(var p in jsonObj) { if(jsonObj.hasOwnProperty(p)) { if(p === 'id') { continue; } else if(meta.fields[p]) { // regular field persistence.set(obj, p, persistence.jsonToEntityVal(jsonObj[p], meta.fields[p])); } else if(meta.hasOne[p] || meta.hasMany[p]){ expensiveProperties.push(p); } } } persistence.asyncForEach(expensiveProperties, function(p, callback) { if(meta.hasOne[p]) { meta.hasOne[p].type.fromSelectJSON(session, tx, jsonObj[p], function(result) { persistence.set(obj, p, result); callback(); }); } else if(meta.hasMany[p]) { var coll = persistence.get(obj, p); var ar = jsonObj[p].slice(0); var PropertyEntity = meta.hasMany[p].type; // get all current items coll.list(tx, function(currentItems) { persistence.asyncForEach(ar, function(item, callback) { PropertyEntity.fromSelectJSON(session, tx, item, function(result) { // Check if not already in collection for(var i = 0; i < currentItems.length; i++) { if(currentItems[i].id === result.id) { callback(); return; } } coll.add(result); callback(); }); }, function() { callback(); }); }); } }, function() { callback(obj); }); } if(jsonObj.id) { Entity.load(session, tx, jsonObj.id, loadedObj); } else { loadedObj(new Entity(session)); } }; Entity.load = function(session, tx, id, callback) { var args = argspec.getArgs(arguments, [ { name: 'session', optional: true, check: persistence.isSession, defaultValue: persistence }, { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'id', optional: false, check: argspec.hasType('string') }, { name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} } ]); Entity.findBy(args.session, args.tx, "id", args.id, args.callback); }; Entity.findBy = function(session, tx, property, value, callback) { var args = argspec.getArgs(arguments, [ { name: 'session', optional: true, check: persistence.isSession, defaultValue: persistence }, { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'property', optional: false, check: argspec.hasType('string') }, { name: 'value', optional: false }, { name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} } ]); session = args.session; tx = args.tx; property = args.property; value = args.value; callback = args.callback; if(property === 'id' && value in session.trackedObjects) { callback(session.trackedObjects[value]); return; } if(!tx) { session.transaction(function(tx) { Entity.findBy(session, tx, property, value, callback); }); return; } Entity.all(session).filter(property, "=", value).one(tx, function(obj) { callback(obj); }); } Entity.index = function(cols,options) { var opts = options || {}; if (typeof cols=="string") { cols = [cols]; } opts.columns = cols; meta.indexes.push(opts); }; /** * Declares a one-to-many or many-to-many relationship to another entity * Whether 1:N or N:M is chosed depends on the inverse declaration * @param collName the name of the collection (becomes a property of * Entity instances * @param otherEntity the constructor function of the entity to define * the relation to * @param inverseRel the name of the inverse property (to be) defined on otherEntity */ Entity.hasMany = function (collName, otherEntity, invRel) { var otherMeta = otherEntity.meta; if (otherMeta.hasMany[invRel]) { // other side has declared it as a one-to-many relation too -> it's in // fact many-to-many var tableName = meta.name + "_" + collName + "_" + otherMeta.name; var inverseTableName = otherMeta.name + '_' + invRel + '_' + meta.name; if (tableName > inverseTableName) { // Some arbitrary way to deterministically decide which table to generate tableName = inverseTableName; } meta.hasMany[collName] = { type: otherEntity, inverseProperty: invRel, manyToMany: true, tableName: tableName }; otherMeta.hasMany[invRel] = { type: Entity, inverseProperty: collName, manyToMany: true, tableName: tableName }; delete meta.hasOne[collName]; delete meta.fields[collName + "_class"]; // in case it existed } else { meta.hasMany[collName] = { type: otherEntity, inverseProperty: invRel }; otherMeta.hasOne[invRel] = { type: Entity, inverseProperty: collName }; if (meta.isMixin) otherMeta.fields[invRel + "_class"] = persistence.typeMapper ? persistence.typeMapper.classNameType : "TEXT"; } } Entity.hasOne = function (refName, otherEntity, inverseProperty) { meta.hasOne[refName] = { type: otherEntity, inverseProperty: inverseProperty }; if (otherEntity.meta.isMixin) meta.fields[refName + "_class"] = persistence.typeMapper ? persistence.typeMapper.classNameType : "TEXT"; }; Entity.is = function(mixin){ var mixinMeta = mixin.meta; if (!mixinMeta.isMixin) throw new Error("not a mixin: " + mixin); mixin.meta.mixedIns = mixin.meta.mixedIns || []; mixin.meta.mixedIns.push(meta); for (var field in mixinMeta.fields) { if (mixinMeta.fields.hasOwnProperty(field)) meta.fields[field] = mixinMeta.fields[field]; } for (var it in mixinMeta.hasOne) { if (mixinMeta.hasOne.hasOwnProperty(it)) meta.hasOne[it] = mixinMeta.hasOne[it]; } for (var it in mixinMeta.hasMany) { if (mixinMeta.hasMany.hasOwnProperty(it)) { mixinMeta.hasMany[it].mixin = mixin; meta.hasMany[it] = mixinMeta.hasMany[it]; } } } // Allow decorator functions to add more stuff var fns = persistence.entityDecoratorHooks; for(var i = 0; i < fns.length; i++) { fns[i](Entity); } entityClassCache[entityName] = Entity; return Entity; } persistence.jsonToEntityVal = function(value, type) { if(type) { switch(type) { case 'DATE': if(typeof value === 'number') { if (value > 1000000000000) { // it's in milliseconds return new Date(value); } else { return new Date(value * 1000); } } else { return null; } break; default: return value; } } else { return value; } }; persistence.entityValToJson = function(value, type) { if(type) { switch(type) { case 'DATE': if(value) { value = new Date(value); return Math.round(value.getTime() / 1000); } else { return null; } break; default: return value; } } else { return value; } }; /** * Dumps the entire database into an object (that can be serialized to JSON for instance) * @param tx transaction to use, use `null` to start a new one * @param entities a list of entity constructor functions to serialize, use `null` for all * @param callback (object) the callback function called with the results. */ persistence.dump = function(tx, entities, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'entities', optional: true, check: function(obj) { return !obj || (obj && obj.length && !obj.apply); }, defaultValue: null }, { name: 'callback', optional: false, check: argspec.isCallback(), defaultValue: function(){} } ]); tx = args.tx; entities = args.entities; callback = args.callback; if(!entities) { // Default: all entity types entities = []; for(var e in entityClassCache) { if(entityClassCache.hasOwnProperty(e)) { entities.push(entityClassCache[e]); } } } var result = {}; persistence.asyncParForEach(entities, function(Entity, callback) { Entity.all().list(tx, function(all) { var items = []; persistence.asyncParForEach(all, function(e, callback) { var rec = {}; var fields = Entity.meta.fields; for(var f in fields) { if(fields.hasOwnProperty(f)) { rec[f] = persistence.entityValToJson(e._data[f], fields[f]); } } var refs = Entity.meta.hasOne; for(var r in refs) { if(refs.hasOwnProperty(r)) { rec[r] = e._data[r]; } } var colls = Entity.meta.hasMany; var collArray = []; for(var coll in colls) { if(colls.hasOwnProperty(coll)) { collArray.push(coll); } } persistence.asyncParForEach(collArray, function(collP, callback) { var coll = persistence.get(e, collP); coll.list(tx, function(results) { rec[collP] = results.map(function(r) { return r.id; }); callback(); }); }, function() { rec.id = e.id; items.push(rec); callback(); }); }, function() { result[Entity.meta.name] = items; callback(); }); }); }, function() { callback(result); }); }; /** * Loads a set of entities from a dump object * @param tx transaction to use, use `null` to start a new one * @param dump the dump object * @param callback the callback function called when done. */ persistence.load = function(tx, dump, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'dump', optional: false }, { name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} } ]); tx = args.tx; dump = args.dump; callback = args.callback; var finishedCount = 0; var collItemsToAdd = []; var session = this; for(var entityName in dump) { if(dump.hasOwnProperty(entityName)) { var Entity = getEntity(entityName); var fields = Entity.meta.fields; var instances = dump[entityName]; for(var i = 0; i < instances.length; i++) { var instance = instances[i]; var ent = new Entity(); ent.id = instance.id; for(var p in instance) { if(instance.hasOwnProperty(p)) { if (persistence.isImmutable(p)) { ent[p] = instance[p]; } else if(Entity.meta.hasMany[p]) { // collection var many = Entity.meta.hasMany[p]; if(many.manyToMany && Entity.meta.name < many.type.meta.name) { // Arbitrary way to avoid double adding continue; } var coll = persistence.get(ent, p); if(instance[p].length > 0) { instance[p].forEach(function(it) { collItemsToAdd.push({Entity: Entity, coll: coll, id: it}); }); } } else { persistence.set(ent, p, persistence.jsonToEntityVal(instance[p], fields[p])); } } } this.add(ent); } } } session.flush(tx, function() { persistence.asyncForEach(collItemsToAdd, function(collItem, callback) { collItem.Entity.load(session, tx, collItem.id, function(obj) { collItem.coll.add(obj); callback(); }); }, function() { session.flush(tx, callback); }); }); }; /** * Dumps the entire database to a JSON string * @param tx transaction to use, use `null` to start a new one * @param entities a list of entity constructor functions to serialize, use `null` for all * @param callback (jsonDump) the callback function called with the results. */ persistence.dumpToJson = function(tx, entities, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'entities', optional: true, check: function(obj) { return obj && obj.length && !obj.apply; }, defaultValue: null }, { name: 'callback', optional: false, check: argspec.isCallback(), defaultValue: function(){} } ]); tx = args.tx; entities = args.entities; callback = args.callback; this.dump(tx, entities, function(obj) { callback(JSON.stringify(obj)); }); }; /** * Loads data from a JSON string (as dumped by `dumpToJson`) * @param tx transaction to use, use `null` to start a new one * @param jsonDump JSON string * @param callback the callback function called when done. */ persistence.loadFromJson = function(tx, jsonDump, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'jsonDump', optional: false }, { name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} } ]); tx = args.tx; jsonDump = args.jsonDump; callback = args.callback; this.load(tx, JSON.parse(jsonDump), callback); }; /** * Generates a UUID according to http://www.ietf.org/rfc/rfc4122.txt */ function createUUID () { if(persistence.typeMapper && persistence.typeMapper.newUuid) { return persistence.typeMapper.newUuid(); } var s = []; var hexDigits = "0123456789ABCDEF"; for ( var i = 0; i < 32; i++) { s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); } s[12] = "4"; s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); var uuid = s.join(""); return uuid; } persistence.createUUID = createUUID; function defaultValue(type) { if(persistence.typeMapper && persistence.typeMapper.defaultValue) { return persistence.typeMapper.defaultValue(type); } switch(type) { case "TEXT": return ""; case "BOOL": return false; default: if(type.indexOf("INT") !== -1) { return 0; } else if(type.indexOf("CHAR") !== -1) { return ""; } else { return null; } } } function arrayContains(ar, item) { var l = ar.length; for(var i = 0; i < l; i++) { var el = ar[i]; if(el.equals && el.equals(item)) { return true; } else if(el === item) { return true; } } return false; } function arrayRemove(ar, item) { var l = ar.length; for(var i = 0; i < l; i++) { var el = ar[i]; if(el.equals && el.equals(item)) { ar.splice(i, 1); return; } else if(el === item) { ar.splice(i, 1); return; } } } ////////////////// QUERY COLLECTIONS \\\\\\\\\\\\\\\\\\\\\\\ function Subscription(obj, eventType, fn) { this.obj = obj; this.eventType = eventType; this.fn = fn; } Subscription.prototype.unsubscribe = function() { this.obj.removeEventListener(this.eventType, this.fn); }; /** * Simple observable function constructor * @constructor */ function Observable() { this.subscribers = {}; } Observable.prototype.addEventListener = function (eventType, fn) { if (!this.subscribers[eventType]) { this.subscribers[eventType] = []; } this.subscribers[eventType].push(fn); return new Subscription(this, eventType, fn); }; Observable.prototype.removeEventListener = function(eventType, fn) { var subscribers = this.subscribers[eventType]; for ( var i = 0; i < subscribers.length; i++) { if(subscribers[i] == fn) { this.subscribers[eventType].splice(i, 1); return true; } } return false; }; Observable.prototype.triggerEvent = function (eventType) { if (!this.subscribers[eventType]) { // No subscribers to this event type return; } var subscribers = this.subscribers[eventType].slice(0); for(var i = 0; i < subscribers.length; i++) { subscribers[i].apply(null, arguments); } }; /* * Each filter has 4 methods: * - sql(prefix, values) -- returns a SQL representation of this filter, * possibly pushing additional query arguments to `values` if ?'s are used * in the query * - match(o) -- returns whether the filter matches the object o. * - makeFit(o) -- attempts to adapt the object o in such a way that it matches * this filter. * - makeNotFit(o) -- the oppositive of makeFit, makes the object o NOT match * this filter */ /** * Default filter that does not filter on anything * currently it generates a 1=1 SQL query, which is kind of ugly */ function NullFilter () { } NullFilter.prototype.match = function (o) { return true; }; NullFilter.prototype.makeFit = function(o) { }; NullFilter.prototype.makeNotFit = function(o) { }; NullFilter.prototype.toUniqueString = function() { return "NULL"; }; NullFilter.prototype.subscribeGlobally = function() { }; NullFilter.prototype.unsubscribeGlobally = function() { }; /** * Filter that makes sure that both its left and right filter match * @param left left-hand filter object * @param right right-hand filter object */ function AndFilter (left, right) { thi