patio
Version:
Patio query engine and ORM
531 lines (467 loc) • 18.5 kB
JavaScript
var comb = require("comb"),
define = comb.define,
isUndefined = comb.isUndefined,
isUndefinedOrNull = comb.isUndefinedOrNull,
isBoolean = comb.isBoolean,
isString = comb.isString,
isHash = comb.isHash,
when = comb.when,
isFunction = comb.isFunction,
isInstanceOf = comb.isInstanceOf,
Promise = comb.Promise,
PromiseList = comb.PromiseList,
array = comb.array,
toArray = array.toArray,
isArray = comb.isArray,
Middleware = comb.plugins.Middleware,
PatioError = require("../errors").PatioError;
var fetch = {
LAZY: "lazy",
EAGER: "eager"
};
/**
* @class
* Base class for all associations.
*
* </br>
* <b>NOT to be instantiated directly</b>
* Its just documented for reference.
*
* @constructs
* @param {Object} options
* @param {String} options.model a string to look up the model that we are associated with
* @param {Function} options.filter a callback to find association if a filter is defined then
* the association is read only
* @param {Object} options.key object with left key and right key
* @param {String|Object} options.orderBy<String|Object> - how to order our association @see Dataset.order
* @param {fetch.EAGER|fetch.LAZY} options.fetchType the fetch type of the model if fetch.Eager is supplied then
* the associations are automatically filled, if fetch.Lazy is supplied
* then a promise is returned and is called back with the loaded models
* @property {Model} model the model associatied with this association.
* @name Association
* @memberOf patio.associations
* */
define(Middleware, {
instance: {
/**@lends patio.associations.Association.prototype*/
type: "",
//Our associated model
_model: null,
/**
* Fetch type
*/
fetchType: fetch.LAZY,
/**how to order our association*/
orderBy: null,
/**Our filter method*/
filter: null,
__hooks: null,
isOwner: true,
createSetter: true,
isCascading: false,
supportsStringKey: true,
supportsHashKey: true,
supportsCompositeKey: true,
supportsLeftAndRightKey: true,
/**
*
*Method to call to look up association,
*called after the model has been filtered
**/
_fetchMethod: "all",
constructor: function (options, patio, filter) {
options = options || {};
if (!options.model) {
throw new Error("Model is required for " + this.type + " association");
}
this._model = options.model;
this.patio = patio;
this.__opts = options;
!isUndefined(options.isCascading) && (this.isCascading = options.isCascading);
this.filter = filter;
this.readOnly = isBoolean(options.readOnly) ? options.readOnly : false;
this.__hooks =
{before: {add: null, remove: null, "set": null, load: null}, after: {add: null, remove: null, "set": null, load: null}};
var hooks = ["Add", "Remove", "Set", "Load"];
["before", "after"].forEach(function (h) {
hooks.forEach(function (a) {
var hookName = h + a, hook;
if (isFunction((hook = options[hookName]))) {
this.__hooks[h][a.toLowerCase()] = hook;
}
}, this);
}, this);
this.fetchType = options.fetchType || fetch.LAZY;
},
_callHook: function (hook, action, args) {
var func = this.__hooks[hook][action], ret;
if (isFunction(func)) {
ret = func.apply(this, args);
}
return ret;
},
_clearAssociations: function (model) {
if (!this.readOnly) {
delete model.__associations[this.name];
}
},
_forceReloadAssociations: function (model) {
if (!this.readOnly) {
delete model.__associations[this.name];
return model[this.name];
}
},
/**
* @return {Boolean} true if the association is eager.
*/
isEager: function () {
return this.fetchType === fetch.EAGER;
},
_checkAssociationKey: function (parent) {
var q = {};
this._setAssociationKeys(parent, q);
return Object.keys(q).every(function (k) {
return q[k] !== null;
});
},
_getAssociationKey: function () {
var options = this.__opts, key, ret = [], lk, rk;
if (!isUndefinedOrNull((key = options.key))) {
if (this.supportsStringKey && isString(key)) {
//normalize the key first!
ret = [
[this.isOwner ? this.defaultLeftKey : key],
[this.isOwner ? key : this.defaultRightKey]
];
} else if (this.supportsHashKey && isHash(key)) {
var leftKey = Object.keys(key)[0];
var rightKey = key[leftKey];
ret = [
[leftKey],
[rightKey]
];
} else if (this.supportsCompositeKey && isArray(key)) {
ret = [
[key],
null
];
}
} else if (this.supportsLeftAndRightKey && (!isUndefinedOrNull((lk = options.leftKey)) && !isUndefinedOrNull((rk = options.rightKey)))) {
ret = [
toArray(lk), toArray(rk)
];
} else {
//todo handle composite primary keys
ret = [
[this.defaultLeftKey],
[this.defaultRightKey]
];
}
return ret;
},
_setAssociationKeys: function (parent, model, val) {
var keys = this._getAssociationKey(parent), leftKey = keys[0], rightKey = keys[1], i = leftKey.length - 1;
if (leftKey && rightKey) {
for (; i >= 0; i--) {
model[rightKey[i]] = !isUndefined(val) ? val : parent[leftKey[i]] ? parent[leftKey[i]] : null;
}
} else {
for (; i >= 0; i--) {
var k = leftKey[i];
model[k] = !isUndefined(val) ? val : parent[k] ? parent[k] : null;
}
}
},
_setDatasetOptions: function (ds) {
var options = this.__opts || {};
var order, limit, distinct, select, query;
//allow for multi key ordering
if (!isUndefined((select = this.select))) {
ds = ds.select.apply(ds, toArray(select));
}
if (!isUndefined((query = options.query)) || !isUndefined((query = options.conditions))) {
ds = ds.filter(query);
}
if (isFunction(this.filter)) {
var ret = this.filter.apply(this, [ds]);
if (isInstanceOf(ret, ds._static)) {
ds = ret;
}
}
if (!isUndefined((distinct = options.distinct))) {
ds = ds.limit.apply(ds, toArray(distinct));
}
if (!isUndefined((order = options.orderBy)) || !isUndefined((order = options.order))) {
ds = ds.order.apply(ds, toArray(order));
}
if (!isUndefined((limit = options.limit))) {
ds = ds.limit.apply(ds, toArray(limit));
}
return ds;
},
/**
*Filters our associated dataset to load our association.
*
*@return {Dataset} the dataset with all filters applied.
**/
_filter: function (parent) {
var options = this.__opts || {};
var ds, self = this;
if (!isUndefined((ds = options.dataset)) && isFunction(ds)) {
ds = ds.apply(parent, [parent]);
}
if (!ds) {
var q = {};
this._setAssociationKeys(parent, q);
ds = this.model.dataset.naked().filter(q);
var recip = this.model._findAssociation(this);
recip && (recip = recip[1]);
ds.rowCb = function (item) {
var model = self._toModel(item, true);
recip && recip.__setValue(model, parent);
//call hook to finish other model associations
return model._hook("post", "load").chain(function () {
return model;
});
};
} else if (!ds.rowCb && this.model) {
ds.rowCb = function (item) {
var model = self._toModel(item, true);
//call hook to finish other model associations
return model._hook("post", "load").chain(function () {
return model;
});
};
}
return this._setDatasetOptions(ds);
},
__setValue: function (parent, model) {
var associations;
if (this._fetchMethod === "all") {
associations = !isArray(model) ? [model] : model;
} else {
associations = isArray(model) ? model[0] : model;
}
parent.__associations[this.name] = associations;
return parent.__associations[this.name];
},
fetch: function (parent) {
var ret = new Promise();
if (this._checkAssociationKey(parent)) {
var self = this;
return this._filter(parent)[this._fetchMethod]().chain(function (result) {
self.__setValue(parent, result);
parent = null;
return result;
});
} else {
this.__setValue(parent, null);
ret.callback(null);
}
return ret;
},
/**
* Middleware called before a model is removed.
* </br>
* <b> This is called in the scope of the model</b>
* @param {Function} next function to pass control up the middleware stack.
* @param {_Association} self reference to the Association that is being acted up.
*/
_preRemove: function (next, model) {
if (this.isOwner && !this.isCascading) {
var q = {};
this._setAssociationKeys(model, q, null);
model[this.associatedDatasetName].update(q).classic(next);
} else {
next();
}
},
/**
* Middleware called aft era model is removed.
* </br>
* <b> This is called in the scope of the model</b>
* @param {Function} next function to pass control up the middleware stack.
* @param {_Association} self reference to the Association that is being called.
*/
_postRemove: function (next, model) {
next();
},
/**
* Middleware called before a model is saved.
* </br>
* <b> This is called in the scope of the model</b>
* @param {Function} next function to pass control up the middleware stack.
* @param {_Association} self reference to the Association that is being called.
*/
_preSave: function (next, model) {
next();
},
/**
* Middleware called after a model is saved.
* </br>
* <b> This is called in the scope of the model</b>
* @param {Function} next function to pass control up the middleware stack.
* @param {_Association} self reference to the Association that is being called.
*/
_postSave: function (next, model) {
next();
},
/**
* Middleware called before a model is updated.
* </br>
* <b> This is called in the scope of the model</b>
* @param {Function} next function to pass control up the middleware stack.
* @param {_Association} self reference to the Association that is being called.
*/
_preUpdate: function (next, model) {
next();
},
/**
* Middleware called before a model is updated.
* </br>
* <b> This is called in the scope of the model</b>
* @param {Function} next function to pass control up the middleware stack.
* @param {_Association} self reference to the Association that is being called.
*/
_postUpdate: function (next, model) {
next();
},
/**
* Middleware called before a model is loaded.
* </br>
* <b> This is called in the scope of the model</b>
* @param {Function} next function to pass control up the middleware stack.
* @param {_Association} self reference to the Association that is being called.
*/
_preLoad: function (next, model) {
next();
},
/**
* Middleware called after a model is loaded.
* </br>
* <b> This is called in the scope of the model</b>
* @param {Function} next function to pass control up the middleware stack.
* @param {_Association} self reference to the Association that is being called.
*/
_postLoad: function (next, model) {
next();
},
/**
* Alias used to explicitly set an association on a model.
* @param {*} val the value to set the association to
* @param {_Association} self reference to the Association that is being called.
*/
_setter: function (val, model) {
model.__associations[this.name] = val;
},
associationLoaded: function (model) {
return model.__associations.hasOwnProperty(this.name);
},
getAssociation: function (model) {
return model.__associations[this.name];
},
/**
* Alias used to explicitly get an association on a model.
* @param {_Association} self reference to the Association that is being called.
*/
_getter: function (model) {
//if we have them return them;
if (this.associationLoaded(model)) {
var assoc = this.getAssociation(model);
return this.isEager() ? assoc : when(assoc);
} else if (model.isNew) {
return null;
} else {
return this.fetch(model);
}
},
_toModel: function (val, fromDb) {
var Model = this.model;
if (!isUndefinedOrNull(Model)) {
if (!isInstanceOf(val, Model)) {
val = new this.model(val, fromDb);
}
} else {
throw new PatioError("Invalid model " + this.name);
}
return val;
},
/**
* Method to inject functionality into a model. This method alters the model
* to prepare it for associations, and initializes all required middleware calls
* to fulfill requirements needed to loaded the associations.
*
* @param {Model} parent the model that is having an associtaion set on it.
* @param {String} name the name of the association.
*/
inject: function (parent, name) {
this.name = name;
var self = this;
this.parent = parent;
var parentProto = parent.prototype;
parentProto["__defineGetter__"](name, function () {
return self._getter(this);
});
parentProto["__defineGetter__"](this.associatedDatasetName, function () {
return self._filter(this);
});
if (!this.readOnly && this.createSetter) {
//define a setter because we arent read only
parentProto["__defineSetter__"](name, function (vals) {
self._setter(vals, this);
});
}
//set up all callbacks
["pre", "post"].forEach(function (op) {
["save", "update", "remove", "load"].forEach(function (type) {
parent[op](type, function (next) {
return self["_" + op + type.charAt(0).toUpperCase() + type.slice(1)](next, this);
});
}, this);
}, this);
},
getters: {
select: function () {
return this.__opts.select;
},
defaultLeftKey: function () {
var ret = "";
if (this.isOwner) {
ret = this.__opts.primaryKey || this.parent.primaryKey[0];
} else {
ret = this.model.tableName + "Id";
}
return ret;
},
defaultRightKey: function () {
return this.associatedModelKey;
},
//Returns our model
model: function () {
return this["__model__"] || (this["__model__"] = this.patio.getModel(this._model, this.parent.db));
},
associatedModelKey: function () {
var ret = "";
if (this.isOwner) {
ret = this.__opts.primaryKey || this.parent.tableName + "Id";
} else {
ret = this.model.primaryKey[0];
}
return ret;
},
associatedDatasetName: function () {
return this.name + "Dataset";
},
removeAssociationFlagName: function () {
return "__remove" + this.name + "association";
}
}
},
static: {
/**@lends patio.associations.Association*/
fetch: {
LAZY: "lazy",
EAGER: "eager"
}
}
}).as(module);