orm
Version:
NodeJS Object-relational mapping
335 lines (284 loc) • 10.1 kB
JavaScript
var _ = require("lodash");
var util = require("../Utilities");
var ORMError = require("../Error");
var Accessors = { "get": "get", "set": "set", "has": "has", "del": "remove" };
var Promise = require("bluebird");
var ACCESSOR_METHODS = ["hasAccessor", "getAccessor", "setAccessor", "delAccessor"];
exports.prepare = function (Model, associations) {
Model.hasOne = function () {
var assocName;
var assocTemplateName;
var association = {
name : Model.table,
model : Model,
reversed : false,
extension : false,
autoFetch : false,
autoFetchLimit : 2,
required : false
};
for (var i = 0; i < arguments.length; i++) {
switch (typeof arguments[i]) {
case "string":
association.name = arguments[i];
break;
case "function":
if (arguments[i].table) {
association.model = arguments[i];
}
break;
case "object":
association = _.extend(association, arguments[i]);
break;
}
}
assocName = ucfirst(association.name);
assocTemplateName = association.accessor || assocName;
if (!association.hasOwnProperty("field")) {
association.field = util.formatField(association.model, association.name, association.required, association.reversed);
} else if(!association.extension) {
association.field = util.wrapFieldObject({
field: association.field, model: Model, altName: Model.table,
mapsTo: association.mapsTo
});
}
util.convertPropToJoinKeyProp(association.field, {
makeKey: false, required: association.required
});
for (var k in Accessors) {
if (!association.hasOwnProperty(k + "Accessor")) {
association[k + "Accessor"] = Accessors[k] + assocTemplateName;
}
}
associations.push(association);
for (k in association.field) {
if (!association.field.hasOwnProperty(k)) {
continue;
}
if (!association.reversed) {
Model.addProperty(
_.extend({}, association.field[k], { klass: 'hasOne' }),
false
);
}
}
if (association.reverse) {
association.model.hasOne(association.reverse, Model, {
reversed : true,
accessor : association.reverseAccessor,
reverseAccessor: undefined,
field : association.field,
autoFetch : association.autoFetch,
autoFetchLimit : association.autoFetchLimit
});
}
Model["findBy" + assocTemplateName] = function () {
var cb = null, conditions = null, options = {};
for (var i = 0; i < arguments.length; i++) {
switch (typeof arguments[i]) {
case "function":
cb = arguments[i];
break;
case "object":
if (conditions === null) {
conditions = arguments[i];
} else {
options = arguments[i];
}
break;
}
}
if (conditions === null) {
throw new ORMError(".findBy(" + assocName + ") is missing a conditions object", 'PARAM_MISMATCH');
}
options.__merge = {
from : { table: association.model.table, field: (association.reversed ? Object.keys(association.field) : association.model.id) },
to : { table: Model.table, field: (association.reversed ? association.model.id : Object.keys(association.field) ) },
where : [ association.model.table, conditions ],
table : Model.table
};
options.extra = [];
if (typeof cb === "function") {
return Model.find({}, options, cb);
}
return Model.find({}, options);
};
return this;
};
};
exports.extend = function (Model, Instance, Driver, associations) {
for (var i = 0; i < associations.length; i++) {
extendInstance(Model, Instance, Driver, associations[i]);
}
};
exports.autoFetch = function (Instance, associations, opts, cb) {
if (associations.length === 0) {
return cb();
}
var pending = associations.length;
var autoFetchDone = function autoFetchDone() {
pending -= 1;
if (pending === 0) {
return cb();
}
};
for (var i = 0; i < associations.length; i++) {
autoFetchInstance(Instance, associations[i], opts, autoFetchDone);
}
};
function extendInstance(Model, Instance, Driver, association) {
var promiseFunctionPostfix = Model.settings.get('promiseFunctionPostfix');
Object.defineProperty(Instance, association.hasAccessor, {
value: function (opts, cb) {
if (typeof opts === "function") {
cb = opts;
opts = {};
}
if (util.hasValues(Instance, Object.keys(association.field))) {
association.model.get(util.values(Instance, Object.keys(association.field)), opts, function (err, instance) {
return cb(err, instance ? true : false);
});
} else {
cb(null, false);
}
return this;
},
enumerable: false,
writable: true
});
Object.defineProperty(Instance, association.getAccessor, {
value: function (opts, cb) {
if (typeof opts === "function") {
cb = opts;
opts = {};
}
var saveAndReturn = function (err, Assoc) {
if (!err) {
Instance[association.name] = Assoc;
}
return cb(err, Assoc);
};
if (association.reversed) {
if (util.hasValues(Instance, Model.id)) {
if (typeof cb !== "function") {
return association.model.find(util.getConditions(Model, Object.keys(association.field), Instance), opts);
}
association.model.find(util.getConditions(Model, Object.keys(association.field), Instance), opts, saveAndReturn);
} else {
cb(null);
}
} else {
if (Instance.isShell()) {
Model.get(util.values(Instance, Model.id), function (err, instance) {
if (err || !util.hasValues(instance, Object.keys(association.field))) {
return cb(null);
}
association.model.get(util.values(instance, Object.keys(association.field)), opts, saveAndReturn);
});
} else if (util.hasValues(Instance, Object.keys(association.field))) {
association.model.get(util.values(Instance, Object.keys(association.field)), opts, saveAndReturn);
} else {
cb(null);
}
}
return this;
},
enumerable: false,
writable: true
});
Object.defineProperty(Instance, association.setAccessor, {
value: function (OtherInstance, cb) {
if (association.reversed) {
Instance.save(function (err) {
if (err) {
return cb(err);
}
if (!Array.isArray(OtherInstance)) {
util.populateConditions(Model, Object.keys(association.field), Instance, OtherInstance, true);
return OtherInstance.save({}, { saveAssociations: false }, cb);
}
var associations = _.clone(OtherInstance);
var saveNext = function () {
if (!associations.length) {
return cb();
}
var other = associations.pop();
util.populateConditions(Model, Object.keys(association.field), Instance, other, true);
other.save({}, { saveAssociations: false }, function (err) {
if (err) {
return cb(err);
}
saveNext();
});
};
return saveNext();
});
} else {
OtherInstance.save({}, { saveAssociations: false }, function (err) {
if (err) {
return cb(err);
}
Instance[association.name] = OtherInstance;
util.populateConditions(association.model, Object.keys(association.field), OtherInstance, Instance);
return Instance.save({}, { saveAssociations: false }, cb);
});
}
return this;
},
enumerable: false,
writable: true
});
if (!association.reversed) {
Object.defineProperty(Instance, association.delAccessor, {
value: function (cb) {
for (var k in association.field) {
if (association.field.hasOwnProperty(k)) {
Instance[k] = null;
}
}
Instance.save({}, { saveAssociations: false }, function (err) {
if (!err) {
delete Instance[association.name];
}
return cb();
});
return this;
},
enumerable: false,
writable: true
});
}
for (var i = 0; i < ACCESSOR_METHODS.length; i++) {
var name = ACCESSOR_METHODS[i];
var asyncNameAccessorName = association[name] + promiseFunctionPostfix;
if (name === "delAccessor" && !Instance[association.delAccessor]) continue;
Object.defineProperty(Instance, asyncNameAccessorName, {
value: Promise.promisify(Instance[association[name]]),
enumerable: false,
writable: true
});
}
}
function autoFetchInstance(Instance, association, opts, cb) {
if (!Instance.saved()) {
return cb();
}
if (!opts.hasOwnProperty("autoFetchLimit") || typeof opts.autoFetchLimit === "undefined") {
opts.autoFetchLimit = association.autoFetchLimit;
}
if (opts.autoFetchLimit === 0 || (!opts.autoFetch && !association.autoFetch)) {
return cb();
}
// When we have a new non persisted instance for which the association field (eg owner_id)
// is set, we don't want to auto fetch anything, since `new Model(owner_id: 12)` takes no
// callback, and hence this lookup would complete at an arbitrary point in the future.
// The associated entity should probably be fetched when the instance is persisted.
if (Instance.isPersisted()) {
Instance[association.getAccessor]({ autoFetchLimit: opts.autoFetchLimit - 1 }, cb);
} else {
return cb();
}
}
function ucfirst(text) {
return text[0].toUpperCase() + text.substr(1);
}