Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

first pass on mixins

  • Loading branch information...
commit 71f97d7b2a8c0e05ee118c69c5686f063e99bbc3 1 parent 96f2a57
Bruno Jouhier authored
View
39 README.md
@@ -210,6 +210,45 @@ optional.
There is also a migrations plugin you can check out, documentation can be found
in [persistence.migrations.docs.md](migrations/persistence.migrations.docs.md) file.
+Mix-ins
+-------
+
+You can also define mix-ins and apply them to entities of the model.
+
+A mix-in definition is similar to an entity definition. Just pass an additional `true`
+argument to the `persistence.define` function, to indicate that you are defining
+a mix-in. For example:
+
+ var Annotatable = persistence.define('Annotatable', {
+ lastAnnotated: "DATE"
+ }, true);
+
+You can define relationships between mix-in and entities. For example:
+
+ // A normal entity
+ var Note = persistence.define('Note', {
+ text: "TEXT"
+ });
+
+ // relationship between a mix-in and a normal entity
+ Annotatable.hasMany('notes', Note, 'annotated');
+
+Once you have defined a mix-in, you can apply it to any entity of your model,
+with the `Entity.is(mixin)` method. For example:
+
+ Project.is(Annotable);
+ Task.is(Annotable);
+
+Now, your `Project` and `Task` entities have an additional `lastAnnotated` property.
+They also have a one to many relationship called `notes` to the `Note` entity.
+And you can also traverse the reverse relationship from a `Note` to its `annotated` object
+
+Note that `annotated` is a polymorphic relationship as it may yield either a `Project`
+or a `Task` (or any other entity which is `Annotatable').
+
+Notes: this feature is very experimental at this stage. It needs more testing.
+ Support for "is a" relationships (classical inheritence) is also in the works.
+
Creating and manipulating objects
---------------------------------
View
46 lib/persistence.js
@@ -184,14 +184,15 @@ persistence.get = function(arg1, arg2) {
* values, e.g. {name: "TEXT", age: "INT"}
* @return the entity's constructor
*/
- persistence.define = function (entityName, fields) {
+ persistence.define = function (entityName, fields, isMixin) {
if (entityMeta[entityName]) { // Already defined, ignore
return getEntity(entityName);
}
var meta = {
name: entityName,
fields: fields,
- hasMany: {},
+ isMixin: isMixin,
+ hasMany: {},
hasOne: {}
};
entityMeta[entityName] = meta;
@@ -311,6 +312,8 @@ persistence.get = function(arg1, arg2) {
{ 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 instanciate mixin");
session = args.session;
obj = args.obj;
@@ -323,7 +326,7 @@ persistence.get = function(arg1, arg2) {
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)) {
@@ -351,15 +354,20 @@ persistence.get = function(arg1, arg2) {
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];
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
@@ -379,6 +387,16 @@ persistence.get = function(arg1, arg2) {
throw "Property '" + ref + "' with id: " + that._data[ref] + " not fetched, either prefetch it or fetch it manually.";
}
});
+ if (mixinClass) {
+ meta.fields[mixinClass] = persistence.typeMapper ? persistence.typeMapper.classNameType : "TEXT";
+ persistence.defineProp(that, mixinClass, function(val) {
+ var oldValue = that._data[mixinClass];
+ that._data[mixinClass] = val;
+ that._dirtyProperties[mixinClass] = oldValue;
+ }, function() {
+ return that._data[mixinClass];
+ })
+ }
}());
}
}
@@ -652,7 +670,10 @@ persistence.get = function(arg1, arg2) {
callback(this._data_obj[rel]);
}
} else {
- meta.hasOne[rel].type.load(tx, this._data[rel], function(obj) {
+ var type = meta.hasOne[rel].type;
+ if (type.meta.isMixin)
+ type = getEntity(this._data[rel + '_class']);
+ type.load(tx, this._data[rel], function(obj) {
that._data_obj[rel] = obj;
if(callback) {
callback(obj);
@@ -841,6 +862,23 @@ persistence.get = function(arg1, arg2) {
};
};
+ Entity.is = function(mixin){
+ var mixinMeta = mixin.meta;
+ for (var field in mixinMeta.fields) {
+ console.log("mixin field: " + field);
+ 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))
+ 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++) {
View
17 lib/persistence.store.sql.js
@@ -29,6 +29,11 @@ persistence.store.sql.config = function(persistence, dialect) {
idType: "VARCHAR(32)",
/**
+ * SQL type for class names (used by mixins)
+ */
+ classNameType: "TEXT",
+
+ /**
* Returns SQL type for column definition
*/
columnType: function(type){
@@ -164,6 +169,8 @@ persistence.store.sql.config = function(persistence, dialect) {
for (var entityName in entityMeta) {
if (entityMeta.hasOwnProperty(entityName)) {
meta = entityMeta[entityName];
+ if (meta.isMixin)
+ continue;
colDefs = [];
for (var prop in meta.fields) {
if (meta.fields.hasOwnProperty(prop)) {
@@ -174,6 +181,8 @@ persistence.store.sql.config = function(persistence, dialect) {
if (meta.hasOne.hasOwnProperty(rel)) {
otherMeta = meta.hasOne[rel].type.meta;
colDefs.push([rel, tm.idType]);
+ if (otherMeta.isMixin)
+ colDefs.push([rel + "_class", tm.classNameType]);
queries.push([dialect.createIndex(meta.name, [rel]), null]);
}
}
@@ -322,7 +331,7 @@ persistence.store.sql.config = function(persistence, dialect) {
if (session.trackedObjects[row[prefix + "id"]]) { // Cached version
return session.trackedObjects[row[prefix + "id"]];
}
- var tm = persistence.typeMapper;
+ var tm = persistence.typeMapper;
var rowMeta = persistence.getMeta(entityName);
var ent = persistence.define(entityName); // Get entity
if(!row[prefix+'id']) { // null value, no entity found
@@ -350,7 +359,7 @@ persistence.store.sql.config = function(persistence, dialect) {
*/
function save(obj, tx, callback) {
var meta = persistence.getMeta(obj._type);
- var tm = persistence.typeMapper;
+ var tm = persistence.typeMapper;
var properties = [];
var values = [];
var qs = [];
@@ -365,7 +374,7 @@ persistence.store.sql.config = function(persistence, dialect) {
for ( var p in obj._dirtyProperties) {
if (obj._dirtyProperties.hasOwnProperty(p)) {
properties.push("`" + p + "`");
- var type = meta.fields[p] || tm.idType;
+ var type = meta.fields[p] || tm.idType;
values.push(tm.entityValToDbVal(obj._data[p], type));
qs.push(tm.outVar("?", type));
propertyPairs.push("`" + p + "` = " + tm.outVar("?", type));
@@ -571,7 +580,7 @@ persistence.store.sql.config = function(persistence, dialect) {
}
var entityName = this._entityName;
var meta = persistence.getMeta(entityName);
- var tm = persistence.typeMapper;
+ var tm = persistence.typeMapper;
function selectAll (meta, tableAlias, prefix) {
var selectFields = [ tm.inIdVar("`" + tableAlias + "`.id") + " AS " + prefix + "id" ];
View
24 test/test.mixin.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+ <script src="qunit/jquery.js"></script>
+ <link rel="stylesheet" href="qunit/qunit.css" type="text/css" media="screen" />
+ <script type="text/javascript" src="qunit/qunit.js"></script>
+
+ <script src="http://code.google.com/apis/gears/gears_init.js"></script>
+ <script src="../lib/persistence.js" type="application/javascript"></script>
+ <script src="../lib/persistence.store.sql.js" type="application/javascript"></script>
+ <script src="../lib/persistence.store.websql.js" type="application/javascript"></script>
+ <script src="../lib/persistence.store.memory.js" type="application/javascript"></script>
+ <script type="text/javascript" src='util.js'></script>
+ <script type="text/javascript" src='test.mixin.js'></script>
+</head>
+<body>
+ <h1 id="qunit-header">persistence.js mixin tests</h1>
+ <h2 id="qunit-banner"></h2>
+ <h2 id="qunit-userAgent"></h2>
+ <ol id="qunit-tests"></ol>
+</body>
+</html>
+
View
106 test/test.mixin.js
@@ -0,0 +1,106 @@
+$(document).ready(function(){
+ persistence.store.websql.config(persistence, 'persistencetest', 'My db', 5 * 1024 * 1024);
+ //persistence.store.memory.config(persistence);
+ persistence.debug = true;
+ //persistence.debug = false;
+
+ var startTime = new Date().getTime();
+
+ var Project = persistence.define('Project', {
+ name: "TEXT"
+ });
+
+ var Task = persistence.define('Task', {
+ name: "TEXT"
+ });
+
+ var Tag = persistence.define('Tag', {
+ name: "TEXT"
+ });
+
+ Task.hasMany('tags', Tag, 'tasks');
+ Tag.hasMany('tasks', Task, 'tags');
+
+ Project.hasMany('tasks', Task, 'project');
+
+ var Note = persistence.define('Note', {
+ text: "TEXT"
+ });
+
+ var Annotatable = persistence.define('Annotatable', {
+ lastAnnotated: "DATE"
+ }, true);
+
+ Annotatable.hasMany('notes', Note, 'annotated');
+
+ Task.hasMany('tags', Tag, 'tasks');
+ Tag.hasMany('tasks', Task, 'tags');
+
+ Project.hasMany('tasks', Task, 'project');
+
+ Task.is(Annotatable);
+ Project.is(Annotatable);
+
+ window.Project = Project;
+ window.Task = Task
+ window.Project = Project;
+
+ module("Setup");
+
+ asyncTest("setting up database", 1, function(){
+ persistence.schemaSync(function(tx){
+ ok(true, 'schemaSync called callback function');
+ start();
+ });
+ });
+
+ module("Annotatable mixin", {
+ setup: function() {
+ stop();
+ persistence.reset(function() {
+ persistence.schemaSync(start);
+ });
+ }
+ });
+
+
+ asyncTest("creating mixin", 7, function(){
+ var now = new Date();
+ now.setMilliseconds(0);
+
+ var p = new Project({
+ name: "project p"
+ });
+ persistence.add(p);
+ var n1 = new Note({
+ text: "note 1"
+ });
+ var n2 = new Note({
+ text: "note 2"
+ });
+ p.notes.add(n1);
+ n2.annotated = p;
+ p.lastAnnotated = now;
+ persistence.flush(function(){
+
+ })
+ Project.all().list(function(projects){
+ persistence.clean();
+ equals(projects.length, 1)
+ var p = projects[0];
+ p.notes.order('text', true).list(function(notes){
+ equals(notes.length, 2);
+ equals(notes[0].text, "note 1");
+ equals(notes[1].text, "note 2");
+ notes[0].fetch("annotated", function(source){
+ equals(p.id, source.id);
+ equals(typeof source.lastAnnotated, typeof now);
+ equals(source.lastAnnotated.getTime(), now.getTime());
+ start();
+ })
+ });
+ });
+ });
+
+
+});
Please sign in to comment.
Something went wrong with that request. Please try again.