diff --git a/README.md b/README.md index b9aa808..a950005 100644 --- a/README.md +++ b/README.md @@ -347,6 +347,35 @@ And of course the methods to define relationships to other entities: * `EntityName.hasOne(property, Entity)` defines a 1:1 or N:1 relationship + +Entity objects +-------------- + +Entity instances also have a few predefined properties and methods you +should be aware of: + +* `obj.id`, contains the identifier of your entity, this is a + automatically generated (approximation of a) UUID. You should + never write to this property. +* `obj.fetch(prop, callback)`, if an object has a `hasOne` + relationship to another which has not yet been fetched from the + database (e.g. when `prefetch` wasn't used), you can fetch in manually + using `fetch`. When the property object is retrieved the callback function + is invoked with the result, the result is also cached in the entity + object itself. +* `obj.selectJSON([tx], propertySpec, callback)`, sometime you need to extract + a subset of data from an entity. You for instance need to post a JSON representation of your entity, but do not want to include all properties. `selectJSON` allows you to do that. The `propertySpec` arguments expects an array with property names. Some 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 + + Query collections ----------------- diff --git a/lib/persistence.js b/lib/persistence.js index 6765c03..8c9ceb9 100644 --- a/lib/persistence.js +++ b/lib/persistence.js @@ -414,6 +414,181 @@ persistence.get = function(arg1, arg2) { 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]; + } + } + }); + this.buildJSON(tx, includeProperties, callback); + }; + + Entity.prototype.buildJSON = function(tx, includeProperties, callback) { + var session = this._session; + var properties = []; + var fieldSpec = meta.fields; + var that = this; + + 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 = this._data; + var item = {}; + + cheapProperties.forEach(function(p) { + if(p === 'id') { + item.id = that.id; + } else if(meta.hasOne[p]) { + item[p] = {id: itemData[p]}; + } else { + item[p] = persistence.entityValToJson(itemData[p], fieldSpec[p]); + } + }); + properties = expensiveProperties.slice(); + + function processOneProperty() { + var p = properties.pop(); + + if(meta.hasOne[p]) { + that.fetch(session, tx, p, function(obj) { + obj.buildJSON(tx, includeProperties[p], function(result) { + item[p] = result; + if(properties.length > 0) { + processOneProperty(); + } else { + callback(item); + } + }); + }); + } else if(meta.hasMany[p]) { + persistence.get(that, p).list(function(objs) { + item[p] = []; + function oneObj() { + var obj = objs.pop(); + function next() { + if(objs.length > 0) { + oneObj(); + } else { + if(properties.length > 0) { + processOneProperty(); + } else { + callback(item); + } + } + } + if(includeProperties[p] === true) { + item[p].push({id: obj.id}); + next(); + } else { + obj.buildJSON(tx, includeProperties[p], function(result) { + item[p].push(result); + next(); + }); + } + } + if(objs.length > 0) { + oneObj(); + } else { + if(properties.length > 0) { + processOneProperty(); + } else { + callback(item); + } + } + }); + } + } + if(properties.length > 0) { + processOneProperty(); + } else { + callback(item); + } + }; + Entity.prototype.fetch = function(session, tx, rel, callback) { var args = argspec.getArgs(arguments, [ { name: 'session', optional: true, check: persistence.isSession, defaultValue: persistence }, @@ -586,7 +761,11 @@ persistence.get = function(arg1, arg2) { if(type) { switch(type) { case 'DATE': - return Math.round(value.getTime() / 1000); + if(value) { + return Math.round(value.getTime() / 1000); + } else { + return null; + } break; default: return value;